Skip to content

Commit

Permalink
#17 in progress
Browse files Browse the repository at this point in the history
- espresso tests for main screen
  • Loading branch information
mjureczko committed Nov 27, 2024
1 parent 17b68c7 commit 6f0982b
Show file tree
Hide file tree
Showing 30 changed files with 403 additions and 178 deletions.
20 changes: 15 additions & 5 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,13 @@ android {
sourceSets {
androidTest {
manifest.srcFile 'src/androidTest/AndroidManifest.xml'
java.srcDirs = ['src/androidTest/java']
java.srcDirs = ['src/androidTest/java', 'src/testSharedClone/java']
res.srcDirs = ['src/androidTest/res']
resources.srcDirs = ['src/androidTest/resources', 'src/testSharedClone/resources']
}
test {
java.srcDirs += 'src/testShared/java'
resources.srcDirs += 'src/testShared/resources'
}
}
compileOptions {
Expand Down Expand Up @@ -148,7 +153,7 @@ dependencies {
implementation 'com.google.accompanist:accompanist-permissions:0.30.1'
implementation 'commons-io:commons-io:2.11.0'
implementation 'com.mapbox.maps:android:10.18.0'
implementation 'com.facebook.android:facebook-share:latest.release'
implementation 'com.facebook.android:facebook-share:17.0.2'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.1'
implementation 'org.apache.commons:commons-math3:3.6.1'
Expand All @@ -157,23 +162,28 @@ dependencies {
testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: '5.8.2'
testRuntimeOnly group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: '5.8.2'
testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-params', version: '5.8.2'
testImplementation group: 'com.ocadotechnology.gembus', name: 'test-arranger', version: '1.4.9'
testImplementation("com.ocadotechnology.gembus:test-arranger:1.6.4-SNAPSHOT") {
exclude group: "dk.brics.automaton", module: "automaton"
}
testImplementation group: 'org.assertj', name: 'assertj-core', version: '3.26.3'
testImplementation group: 'org.mockito', name: 'mockito-core', version: '5.12.0'
testImplementation group: 'org.mockito', name: 'mockito-junit-jupiter', version: '4.4.0'
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.0'
testImplementation 'org.mockito.kotlin:mockito-kotlin:5.4.0'

androidTestImplementation("com.ocadotechnology.gembus:test-arranger:1.6.4-SNAPSHOT") {
exclude group: "dk.brics.automaton", module: "automaton"
}
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test:core:1.4.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
androidTestImplementation "androidx.test.espresso:espresso-intents:3.5.1"
androidTestImplementation group: 'org.mockito', name: 'mockito-core', version: '4.4.0'
androidTestImplementation 'org.mockito:mockito-inline:2.13.0'
androidTestImplementation group: 'org.mockito', name: 'mockito-android', version: '5.12.0'
androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version"
androidTestImplementation 'androidx.test:rules:1.5.0'
androidTestImplementation 'androidx.test:runner:1.5.2'
androidTestImplementation "com.google.dagger:hilt-android-testing:${hilt_version}"

kaptAndroidTest "com.google.dagger:hilt-android-compiler:${hilt_version}"
debugImplementation "androidx.compose.ui:ui-tooling:$compose_version"
debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package pl.marianjureczko.poszukiwacz

import android.Manifest
import androidx.compose.ui.test.SemanticsNodeInteraction
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performClick
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.GrantPermissionRule
import dagger.hilt.android.testing.HiltAndroidRule
import org.junit.Before
import org.junit.Rule
import pl.marianjureczko.poszukiwacz.activity.main.MainActivity

abstract class AbstractUITest {
@get:Rule(order = 0)
val hiltRule = HiltAndroidRule(this)

@get:Rule(order = 1)
val composeRule = createAndroidComposeRule<MainActivity>()

@get:Rule(order = 3)
val permissionRule: GrantPermissionRule = GrantPermissionRule.grant(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.CAMERA,
Manifest.permission.RECORD_AUDIO
)
val context = InstrumentationRegistry.getInstrumentation().targetContext

@Before
fun init() {
hiltRule.inject()
}

fun performClick(contentDescription: String) {
composeRule
.onNodeWithContentDescription(contentDescription)
.assertExists()
.performClick()
composeRule.waitForIdle()
}

fun getNode(contentDescription: String): SemanticsNodeInteraction {
return composeRule.onNodeWithContentDescription(contentDescription)
}

fun assertImageIsDisplayed(drawableId: Int) {
composeRule.onNodeWithTag(drawableId.toString())
.assertIsDisplayed()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package pl.marianjureczko.poszukiwacz

import androidx.test.platform.app.InstrumentationRegistry
import dalvik.system.DexFile
import org.junit.Test


class CustomArrangersDetector {

@Test
fun findArrangerConstructors() {
val testClasses = getTestApkClasses()
val customArrangers = convertToComaSeparatedCustomArrangers(testClasses)
println("### $customArrangers ###")
}

private fun convertToComaSeparatedCustomArrangers(classes: List<String>): String {
return classes
.filter { c -> c.endsWith("Arranger") && !c.endsWith(".Arranger") && !c.endsWith(".CustomArranger") }
.joinToString(separator = ",")
}

private fun getTestApkClasses(): List<String> {
try {
val testApkPath: String = InstrumentationRegistry.getInstrumentation()
.getContext()
.getApplicationInfo()
.sourceDir
val dexFile = DexFile(testApkPath)
return dexFile.entries().asSequence().toList()
} catch (e: Exception) {
throw RuntimeException("Failed to retrieve test APK classes", e)
}
}
}
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package pl.marianjureczko.poszukiwacz

import androidx.compose.ui.test.SemanticsNodeInteraction
import androidx.compose.ui.test.assertTextEquals
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTextInput
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import org.junit.After
import org.junit.Test
import pl.marianjureczko.poszukiwacz.model.Route
import pl.marianjureczko.poszukiwacz.model.RouteArranger
import pl.marianjureczko.poszukiwacz.screen.main.CONFIRM_ROUTE_NAME_BUTTON
import pl.marianjureczko.poszukiwacz.screen.main.DELETE_ROUTE_BUTTON
import pl.marianjureczko.poszukiwacz.screen.main.EDIT_ROUTE_BUTTON
import pl.marianjureczko.poszukiwacz.screen.main.ENTER_ROUTE_NAME_TITLE
import pl.marianjureczko.poszukiwacz.screen.main.NEW_ROUTE_BUTTON
import pl.marianjureczko.poszukiwacz.screen.main.ROUTE_NAME_TEXT_EDIT
import pl.marianjureczko.poszukiwacz.screen.treasureseditor.TREASURE_ITEM_ROW
import pl.marianjureczko.poszukiwacz.screen.treasureseditor.TREASURE_ITEM_TEXT
import pl.marianjureczko.poszukiwacz.shared.di.PortsModule
import pl.marianjureczko.poszukiwacz.ui.components.TOPBAR_SCREEN_TITLE
import pl.marianjureczko.poszukiwacz.ui.components.YES_BUTTON
import java.time.LocalDateTime
import java.time.ZoneOffset


@UninstallModules(PortsModule::class)
@HiltAndroidTest
class MainScreenTest : UiTest() {

var route: Route = getRouteFromStorage()

@After
fun restoreRoute() {
if(TestPortsModule.storage.routes.isEmpty()) {
val newRoute = RouteArranger.routeWithoutTipFiles()
TestPortsModule.storage.routes[newRoute.name] = newRoute
}
route = getRouteFromStorage()
}

@Test
fun shouldGoToTreasureEditorScreen_whenCreatingNewRoute() {
//given
val button: SemanticsNodeInteraction = getNode(NEW_ROUTE_BUTTON)

//then
button.assertTextEquals(context.getString(R.string.new_route_button))
button.performClick()
composeRule.waitForIdle()

val dialogTitle = getNode(ENTER_ROUTE_NAME_TITLE)
dialogTitle.assertTextEquals(context.getString(R.string.route_name_prompt))

val nameInput = getNode(ROUTE_NAME_TEXT_EDIT)
val routeName = "TEST_" + LocalDateTime.now().toEpochSecond(ZoneOffset.UTC)
nameInput.assertExists()
.performTextInput(routeName)
composeRule.waitForIdle()

performClick(CONFIRM_ROUTE_NAME_BUTTON)

getNode(TOPBAR_SCREEN_TITLE)
.assertTextEquals("${context.getString(R.string.route)} $routeName")
getNode(TREASURE_ITEM_ROW)
.assertDoesNotExist()
}

@Test
fun shouldGoToTreasureEditorScreen_whenClickOnEditButton() {
//given
//
// composeRule.setContent {
// // Your composable function that renders the screen
// MainScreen { }()
// }
composeRule.waitForIdle()

//when
performClick(EDIT_ROUTE_BUTTON + route.name)

//then
val screenTitle: SemanticsNodeInteraction = getNode(TOPBAR_SCREEN_TITLE)
screenTitle.assertTextEquals("${context.getString(R.string.route)} ${route.name}")
route.treasures.forEach { treasure ->
val treasureText: SemanticsNodeInteraction = getNode("$TREASURE_ITEM_TEXT ${treasure.id}")
treasureText.assertTextEquals(treasure.prettyName())
}
}

@Test
fun shouldRemoveRoute_whenClickingRemoveButton() {
//given
composeRule.waitForIdle()

//when
performClick(DELETE_ROUTE_BUTTON + route.name)
performClick(YES_BUTTON)

//then
getNode(EDIT_ROUTE_BUTTON + route.name)
.assertDoesNotExist()
}

private fun getRouteFromStorage() = TestPortsModule.storage.routes.values.first()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package pl.marianjureczko.poszukiwacz

import com.google.android.gms.location.FusedLocationProviderClient
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import org.mockito.Mockito.mock
import pl.marianjureczko.poszukiwacz.shared.port.CameraPort
import pl.marianjureczko.poszukiwacz.shared.port.LocationPort
import pl.marianjureczko.poszukiwacz.shared.port.StorageHelper
import javax.inject.Singleton

@Module
@InstallIn(SingletonComponent::class)
object TestPortsModule {

val storage = TestStoragePort()
val locationClient = mock<FusedLocationProviderClient>()
val location = mock<LocationPort>()
val camera = mock<CameraPort>()

@Singleton
@Provides
fun fusedLocationClient(): FusedLocationProviderClient {
return locationClient
}

@Singleton
@Provides
fun locationPort(): LocationPort {
return location
}

@Singleton
@Provides
fun photoPort(): CameraPort {
return camera
}

@Singleton
@Provides
fun storageHelper(): StorageHelper {
return storage
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package pl.marianjureczko.poszukiwacz

open class UiTest: AbstractUITest() {

}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class MainScreenTest : UiTest() {


@Test
fun shouldShowGuideWithImages() {
fun shouldGuideViewsWithImages_whenArrowIsClicked() {
//given
// composeRule.waitForIdle()
val text: SemanticsNodeInteraction = getNode(GUIDE_TEXT)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,55 +1,10 @@
package pl.marianjureczko.poszukiwacz

import androidx.compose.ui.test.SemanticsNodeInteraction
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsEnabled
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performClick
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.GrantPermissionRule
import dagger.hilt.android.testing.HiltAndroidRule
import org.junit.Before
import org.junit.Rule
import pl.marianjureczko.poszukiwacz.activity.main.MainActivity
import pl.marianjureczko.poszukiwacz.screen.main.START_BUTTON

open class UiTest {
@get:Rule(order = 0)
val hiltRule = HiltAndroidRule(this)

@get:Rule(order = 1)
val composeRule = createAndroidComposeRule<MainActivity>()

@get:Rule(order = 3)
val permissionRule: GrantPermissionRule = GrantPermissionRule.grant(
android.Manifest.permission.ACCESS_FINE_LOCATION,
android.Manifest.permission.CAMERA
)
val context = InstrumentationRegistry.getInstrumentation().targetContext

@Before
fun init() {
hiltRule.inject()
}

fun performClick(contentDescription: String) {
composeRule
.onNodeWithContentDescription(contentDescription)
.assertExists()
.performClick()
}

fun getNode(contentDescription: String): SemanticsNodeInteraction {
return composeRule.onNodeWithContentDescription(contentDescription)
.assertExists()
}

fun assertImageIsDisplayed(drawableId: Int) {
composeRule.onNodeWithTag(drawableId.toString())
.assertIsDisplayed()
}
open class UiTest: AbstractUITest() {

protected fun goToSearching() {
var buttonDisabled = true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,13 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.res.stringResource
import androidx.navigation.NavController
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import pl.marianjureczko.poszukiwacz.R
import pl.marianjureczko.poszukiwacz.permissions.RequirementsForNavigation
import pl.marianjureczko.poszukiwacz.shared.GoToSearching
import pl.marianjureczko.poszukiwacz.shared.GoToTreasureEditor
import pl.marianjureczko.poszukiwacz.ui.components.TopBar
import pl.marianjureczko.poszukiwacz.ui.handlePermissionWithExitOnDenied

@OptIn(ExperimentalPermissionsApi::class)
@SuppressLint("UnusedMaterialScaffoldPaddingParameter")
@Composable
fun MainScreen(
Expand Down
Loading

0 comments on commit 6f0982b

Please sign in to comment.