diff --git a/app/build.gradle.kts b/app/build.gradle.kts index bf868b3..6dedfdb 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -26,7 +26,7 @@ android { versionCode = 1 versionName = "1.0" - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + testInstrumentationRunner = "com.lilin.gamelibrary.util.HiltTestRunner" val properties = Properties() properties.load(project.rootProject.file("local.properties").inputStream()) @@ -193,6 +193,8 @@ dependencies { androidTestImplementation(libs.androidx.espresso.core) androidTestImplementation(platform(libs.androidx.compose.bom)) androidTestImplementation(libs.androidx.compose.ui.test.junit4) + androidTestImplementation(libs.hilt.android.testing) + kspAndroidTest(libs.hilt.android.compiler) debugImplementation(libs.androidx.compose.ui.tooling) debugImplementation(libs.androidx.compose.ui.test.manifest) diff --git a/app/src/androidTest/kotlin/com/lilin/gamelibrary/DiscoveryScreenNavigationTest.kt b/app/src/androidTest/kotlin/com/lilin/gamelibrary/DiscoveryScreenNavigationTest.kt new file mode 100644 index 0000000..090aab7 --- /dev/null +++ b/app/src/androidTest/kotlin/com/lilin/gamelibrary/DiscoveryScreenNavigationTest.kt @@ -0,0 +1,263 @@ +@file:OptIn(ExperimentalTestApi::class) + +package com.lilin.gamelibrary + +import androidx.compose.ui.input.key.Key +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onAllNodesWithTag +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performKeyInput +import androidx.compose.ui.test.performScrollTo +import androidx.compose.ui.test.performTextInput +import androidx.compose.ui.test.pressKey +import com.lilin.gamelibrary.ui.MainActivity +import com.lilin.gamelibrary.util.scrollDownUntilNodeExists +import com.lilin.gamelibrary.util.scrollRightUntilNodeExists +import com.lilin.gamelibrary.util.scrollUpUntilNodeExists +import com.lilin.gamelibrary.util.waitUntilNodeExists +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import org.junit.Before +import org.junit.Rule +import org.junit.Test + +@HiltAndroidTest +class DiscoveryScreenNavigationTest { + + @get:Rule(order = 0) + val hiltRule = HiltAndroidRule(this) + + @get:Rule(order = 1) + val composeTestRule = createAndroidComposeRule() + + @Before + fun setup() { + hiltRule.inject() + } + + @Test + fun navigateToNewReleaseGamesFromDiscoveryScreen() { + fromDiscoveryScreenToSeeMore() + + fromSeeMoreToSearchScreen() + + fromSearchToFavoriteScreen() + + fromFavoriteToDetail() + + fromDetailToFavorite() + + checkFavoriteGameAndDelete() + } + + private fun fromDiscoveryScreenToSeeMore() { + composeTestRule + .waitUntilNodeExists("DiscoveryTopBar") + .assertIsDisplayed() + + composeTestRule + .waitUntilNodeExists("DiscoveryLazyColumn") + .assertIsDisplayed() + + composeTestRule + .waitUntilNodeExists("TrendingGames") + .assertIsDisplayed() + + composeTestRule + .waitUntilNodeExists("HighRatedGames") + .performScrollTo() + .assertIsDisplayed() + + composeTestRule + .waitUntilNodeExists("NewReleaseGames") + .performScrollTo() + .assertIsDisplayed() + + composeTestRule + .waitUntilNodeExists("NewReleaseGamesSectionLazyRow") + .performScrollTo() + + composeTestRule + .scrollRightUntilNodeExists( + scrollableTag = "NewReleaseGamesSectionLazyRow", + targetTag = "NewReleaseSeeMoreCard", + ) + .performClick() + } + + private fun fromSeeMoreToSearchScreen() { + composeTestRule + .waitUntilNodeExists("SectionDetailBottomBar") + .assertIsDisplayed() + + composeTestRule + .waitUntilNodeExists("SectionDetailLazyVerticalGrid") + .assertIsDisplayed() + + composeTestRule + .waitUntilNodeExists("ListMode") + .performClick() + + composeTestRule + .waitUntilNodeExists("SectionDetailLazyColumn") + .assertIsDisplayed() + + composeTestRule + .waitUntilNodeExists("GridMode") + .performClick() + + composeTestRule + .waitUntilNodeExists("SectionDetailLazyVerticalGrid") + .assertIsDisplayed() + + composeTestRule + .waitUntilNodeExists("BackKey") + .performClick() + } + + private fun fromSearchToFavoriteScreen() { + composeTestRule + .waitUntilNodeExists("Search") + .performClick() + + composeTestRule + .waitUntilNodeExists("EmptyView") + .assertIsDisplayed() + + composeTestRule + .waitUntilNodeExists("SearchBottomBar") + .assertIsDisplayed() + + composeTestRule + .waitUntilNodeExists("SearchTextField") + .apply { + performTextInput("a") + performKeyInput { + pressKey(Key.Enter) + } + } + + composeTestRule + .waitUntilNodeExists("SearchResultsCompactList") + .assertIsDisplayed() + + composeTestRule + .waitUntilNodeExists("Favorite") + .performClick() + } + + private fun fromFavoriteToDetail() { + composeTestRule + .waitUntilNodeExists("FavoriteEmptyView") + .performClick() + + composeTestRule + .waitUntilNodeExists("Home") + .performClick() + + composeTestRule + .waitUntilNodeExists("DiscoveryTopBar") + .assertIsDisplayed() + + composeTestRule + .waitUntilNodeExists("DiscoveryLazyColumn") + .assertIsDisplayed() + + composeTestRule + .scrollUpUntilNodeExists( + scrollableTag = "DiscoveryLazyColumn", + targetTag = "TrendingGamesSectionLazyRow", + ) + .assertIsDisplayed() + + composeTestRule + .waitUntilNodeExists("TrendingGameCard") + + composeTestRule + .onAllNodesWithTag("TrendingGameCard")[0] + .performClick() + } + + private fun fromDetailToFavorite() { + composeTestRule + .waitUntilNodeExists("GameDetailTopBar") + .assertIsDisplayed() + + composeTestRule + .waitUntilNodeExists("GameDetailBottomBar") + .assertIsDisplayed() + + composeTestRule + .waitUntilNodeExists("GameDetailSuccessContent") + .assertIsDisplayed() + + composeTestRule + .waitUntilNodeExists("GameBackgroundImageSection") + .assertIsDisplayed() + + composeTestRule + .scrollDownUntilNodeExists( + scrollableTag = "GameDetailSuccessContent", + targetTag = "GameBasicInfoSection", + ) + .assertIsDisplayed() + + composeTestRule + .scrollDownUntilNodeExists( + scrollableTag = "GameDetailSuccessContent", + targetTag = "GameRatingSummarySection", + ) + .assertIsDisplayed() + + composeTestRule + .scrollDownUntilNodeExists( + scrollableTag = "GameDetailSuccessContent", + targetTag = "GameDescriptionSection", + ) + .assertIsDisplayed() + + composeTestRule + .scrollDownUntilNodeExists( + scrollableTag = "GameDetailSuccessContent", + targetTag = "GameTagsSection", + ) + .assertIsDisplayed() + + composeTestRule + .onNodeWithTag("FavoriteButton") + .performClick() + + composeTestRule + .onNodeWithTag("BackKey") + .performClick() + + composeTestRule + .waitUntilNodeExists("Favorite") + .performClick() + } + + private fun checkFavoriteGameAndDelete() { + composeTestRule + .waitUntilNodeExists("FavoriteGameCompactCard") + .assertIsDisplayed() + + composeTestRule + .waitUntilNodeExists("DeleteButton") + .performClick() + + composeTestRule + .waitUntilNodeExists("DeleteDialog") + .assertIsDisplayed() + + composeTestRule + .waitUntilNodeExists("ConfirmDeleteButton") + .performClick() + + composeTestRule + .waitUntilNodeExists("FavoriteEmptyView") + .performClick() + } +} diff --git a/app/src/androidTest/kotlin/com/lilin/gamelibrary/ExampleInstrumentedTest.kt b/app/src/androidTest/kotlin/com/lilin/gamelibrary/ExampleInstrumentedTest.kt deleted file mode 100644 index 5c55286..0000000 --- a/app/src/androidTest/kotlin/com/lilin/gamelibrary/ExampleInstrumentedTest.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.lilin.gamelibrary - -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.platform.app.InstrumentationRegistry -import org.junit.Assert.assertEquals -import org.junit.Test -import org.junit.runner.RunWith - -/** - * Instrumented test, which will execute on an Android device. - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -@RunWith(AndroidJUnit4::class) -class ExampleInstrumentedTest { - @Test - fun useAppContext() { - // Context of the app under test. - val appContext = InstrumentationRegistry.getInstrumentation().targetContext - assertEquals("com.lilin.gamelibrary", appContext.packageName) - } -} diff --git a/app/src/androidTest/kotlin/com/lilin/gamelibrary/util/CheckViewUtil.kt b/app/src/androidTest/kotlin/com/lilin/gamelibrary/util/CheckViewUtil.kt new file mode 100644 index 0000000..fad2d0f --- /dev/null +++ b/app/src/androidTest/kotlin/com/lilin/gamelibrary/util/CheckViewUtil.kt @@ -0,0 +1,170 @@ +package com.lilin.gamelibrary.util + +import androidx.compose.ui.test.SemanticsNodeInteraction +import androidx.compose.ui.test.junit4.ComposeContentTestRule +import androidx.compose.ui.test.onAllNodesWithTag +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performTouchInput +import androidx.compose.ui.test.swipeDown +import androidx.compose.ui.test.swipeLeft +import androidx.compose.ui.test.swipeRight +import androidx.compose.ui.test.swipeUp + +/** + * 指定したtestTagのNodeが表示されるまで5秒まつ + * + * @param testTag test対象のタグ + * @param timeoutMillis 表示されるまでの待機時間 + * @return 指定したテストタグを持つComposeNodeに対するSemanticsNodeInteractionオブジェクト + */ +fun ComposeContentTestRule.waitUntilNodeExists( + testTag: String, + timeoutMillis: Long = 5_000L, +): SemanticsNodeInteraction { + waitUntil(timeoutMillis) { + onAllNodesWithTag(testTag).fetchSemanticsNodes().isNotEmpty() + } + + return onNodeWithTag(testTag) +} + +/** + * スクロール可能なコンテナ内で、指定したtestTagのNodeが見つかるまで横スクロール(右方向)を繰り返す + * + * @param scrollableTag スクロール可能なコンテナのtestTag(例: LazyRow) + * @param targetTag 探したい要素のtestTag + * @param maxScrollAttempts 最大スクロール試行回数(デフォルト: 10回) + * @return 指定したテストタグを持つComposeNodeに対するSemanticsNodeInteractionオブジェクト + * @throws AssertionError 指定回数スクロールしても要素が見つからなかった場合 + */ +fun ComposeContentTestRule.scrollRightUntilNodeExists( + scrollableTag: String, + targetTag: String, + maxScrollAttempts: Int = 10, +): SemanticsNodeInteraction { + var attempts = 0 + + while (attempts < maxScrollAttempts) { + val nodes = onAllNodesWithTag(targetTag).fetchSemanticsNodes() + if (nodes.isNotEmpty()) { + return onNodeWithTag(targetTag) + } + + onNodeWithTag(scrollableTag).performTouchInput { + swipeLeft() + } + + attempts++ + waitForIdle() + } + + throw AssertionError( + "Tag '$targetTag' not found in '$scrollableTag' after $maxScrollAttempts attempts", + ) +} + +/** + * スクロール可能なコンテナ内で、指定したtestTagのNodeが見つかるまで横スクロール(左方向)を繰り返す + * + * @param scrollableTag スクロール可能なコンテナのtestTag(例: LazyRow) + * @param targetTag 探したい要素のtestTag + * @param maxScrollAttempts 最大スクロール試行回数(デフォルト: 10回) + * @return 指定したテストタグを持つComposeNodeに対するSemanticsNodeInteractionオブジェクト + * @throws AssertionError 指定回数スクロールしても要素が見つからなかった場合 + */ +@Suppress("Unused") +fun ComposeContentTestRule.scrollLeftUntilNodeExists( + scrollableTag: String, + targetTag: String, + maxScrollAttempts: Int = 10, +): SemanticsNodeInteraction { + var attempts = 0 + + while (attempts < maxScrollAttempts) { + val nodes = onAllNodesWithTag(targetTag).fetchSemanticsNodes() + if (nodes.isNotEmpty()) { + return onNodeWithTag(targetTag) + } + + onNodeWithTag(scrollableTag).performTouchInput { + swipeRight() + } + + attempts++ + waitForIdle() + } + + throw AssertionError( + "Tag '$targetTag' not found in '$scrollableTag' (left) after $maxScrollAttempts tries", + ) +} + +/** + * スクロール可能なコンテナ内で、指定したtestTagのNodeが見つかるまで縦スクロール(上方向)を繰り返す + * + * @param scrollableTag スクロール可能なコンテナのtestTag(例: LazyColumn) + * @param targetTag 探したい要素のtestTag + * @param maxScrollAttempts 最大スクロール試行回数(デフォルト: 10回) + * @return 指定したテストタグを持つComposeNodeに対するSemanticsNodeInteractionオブジェクト + * @throws AssertionError 指定回数スクロールしても要素が見つからなかった場合 + */ +fun ComposeContentTestRule.scrollUpUntilNodeExists( + scrollableTag: String, + targetTag: String, + maxScrollAttempts: Int = 10, +): SemanticsNodeInteraction { + var attempts = 0 + + while (attempts < maxScrollAttempts) { + val nodes = onAllNodesWithTag(targetTag).fetchSemanticsNodes() + if (nodes.isNotEmpty()) { + return onNodeWithTag(targetTag) + } + + onNodeWithTag(scrollableTag).performTouchInput { + swipeDown() + } + + attempts++ + waitForIdle() + } + + throw AssertionError( + "Tag '$targetTag' not found in '$scrollableTag' (up) after $maxScrollAttempts tries", + ) +} + +/** + * スクロール可能なコンテナ内で、指定したtestTagのNodeが見つかるまで縦スクロール(下方向)を繰り返す + * + * @param scrollableTag スクロール可能なコンテナのtestTag(例: LazyColumn) + * @param targetTag 探したい要素のtestTag + * @param maxScrollAttempts 最大スクロール試行回数(デフォルト: 10回) + * @return 指定したテストタグを持つComposeNodeに対するSemanticsNodeInteractionオブジェクト + * @throws AssertionError 指定回数スクロールしても要素が見つからなかった場合 + */ +fun ComposeContentTestRule.scrollDownUntilNodeExists( + scrollableTag: String, + targetTag: String, + maxScrollAttempts: Int = 10, +): SemanticsNodeInteraction { + var attempts = 0 + + while (attempts < maxScrollAttempts) { + val nodes = onAllNodesWithTag(targetTag).fetchSemanticsNodes() + if (nodes.isNotEmpty()) { + return onNodeWithTag(targetTag) + } + + onNodeWithTag(scrollableTag).performTouchInput { + swipeUp() + } + + attempts++ + waitForIdle() + } + + throw AssertionError( + "Tag '$targetTag' not found in '$scrollableTag' (down) after $maxScrollAttempts tries", + ) +} diff --git a/app/src/androidTest/kotlin/com/lilin/gamelibrary/util/HiltTestRunner.kt b/app/src/androidTest/kotlin/com/lilin/gamelibrary/util/HiltTestRunner.kt new file mode 100644 index 0000000..48b0ad8 --- /dev/null +++ b/app/src/androidTest/kotlin/com/lilin/gamelibrary/util/HiltTestRunner.kt @@ -0,0 +1,16 @@ +package com.lilin.gamelibrary.util + +import android.app.Application +import android.content.Context +import androidx.test.runner.AndroidJUnitRunner +import dagger.hilt.android.testing.HiltTestApplication + +class HiltTestRunner : AndroidJUnitRunner() { + override fun newApplication( + classLoader: ClassLoader?, + className: String?, + context: Context?, + ): Application { + return super.newApplication(classLoader, HiltTestApplication::class.java.name, context) + } +} diff --git a/app/src/main/kotlin/com/lilin/gamelibrary/feature/detail/GameDetailScreen.kt b/app/src/main/kotlin/com/lilin/gamelibrary/feature/detail/GameDetailScreen.kt index c2df35a..a9a8857 100644 --- a/app/src/main/kotlin/com/lilin/gamelibrary/feature/detail/GameDetailScreen.kt +++ b/app/src/main/kotlin/com/lilin/gamelibrary/feature/detail/GameDetailScreen.kt @@ -38,6 +38,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign @@ -104,6 +105,7 @@ fun GameDetailScreen( GameDetailTopAppBar( title = gameTitle, scrollBehavior = scrollBehavior, + modifier = Modifier.testTag("GameDetailTopBar"), ) }, bottomBar = { @@ -118,6 +120,7 @@ fun GameDetailScreen( }, isSuccessState = isSuccessState, isFavorite = if (isSuccessState) (uiState as GameDetailUiState.Success).isFavorite else false, + modifier = Modifier.testTag("GameDetailBottomBar"), ) }, contentWindowInsets = WindowInsets.navigationBars, @@ -167,7 +170,7 @@ private fun GameDetailScreen( gameDetail = uiState.gameDetail, scrollBehavior = scrollBehavior, bottomBarPadding = bottomBarPadding, - modifier = modifier, + modifier = modifier.testTag("GameDetailSuccessContent"), ) } } @@ -232,23 +235,38 @@ private fun GameDetailSuccessContent( verticalArrangement = Arrangement.spacedBy(8.dp), ) { item { - GameBackgroundImageSection(gameDetail = gameDetail) + GameBackgroundImageSection( + gameDetail = gameDetail, + modifier = Modifier.testTag("GameBackgroundImageSection"), + ) } item { - GameBasicInfoSection(gameDetail = gameDetail) + GameBasicInfoSection( + gameDetail = gameDetail, + modifier = Modifier.testTag("GameBasicInfoSection"), + ) } item { - GameRatingSummarySection(gameDetail = gameDetail) + GameRatingSummarySection( + gameDetail = gameDetail, + modifier = Modifier.testTag("GameRatingSummarySection"), + ) } item { - GameDescriptionSection(gameDetail = gameDetail) + GameDescriptionSection( + gameDetail = gameDetail, + modifier = Modifier.testTag("GameDescriptionSection"), + ) } item { - GameTagsSection(gameDetail = gameDetail) + GameTagsSection( + gameDetail = gameDetail, + modifier = Modifier.testTag("GameTagsSection"), + ) } } } diff --git a/app/src/main/kotlin/com/lilin/gamelibrary/feature/discovery/DiscoveryScreen.kt b/app/src/main/kotlin/com/lilin/gamelibrary/feature/discovery/DiscoveryScreen.kt index e057738..7662553 100644 --- a/app/src/main/kotlin/com/lilin/gamelibrary/feature/discovery/DiscoveryScreen.kt +++ b/app/src/main/kotlin/com/lilin/gamelibrary/feature/discovery/DiscoveryScreen.kt @@ -48,6 +48,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign @@ -169,9 +170,13 @@ private fun DiscoveryScreen( 0 }, scrollBehavior = scrollBehavior, + modifier = Modifier.testTag("SectionDetailTopAppBar"), ) } else { - DiscoveryTopBar(scrollBehavior = scrollBehavior) + DiscoveryTopBar( + scrollBehavior = scrollBehavior, + modifier = Modifier.testTag("DiscoveryTopBar"), + ) } }, bottomBar = { @@ -181,6 +186,7 @@ private fun DiscoveryScreen( onClickHighRatedTab = { changeSectionTab(SectionType.HIGH_RATED) }, onClickNewReleaseTab = { changeSectionTab(SectionType.NEW_RELEASE) }, selectedTab = selectedTab, + modifier = Modifier.testTag("DiscoveryBottomBar"), ) } }, @@ -340,7 +346,8 @@ private fun CompactContent( ) { LazyColumn( modifier = modifier - .nestedScroll(scrollBehavior.nestedScrollConnection), + .nestedScroll(scrollBehavior.nestedScrollConnection) + .testTag("DiscoveryLazyColumn"), contentPadding = PaddingValues(bottom = 12.dp), ) { item { @@ -350,7 +357,8 @@ private fun CompactContent( onNavigateToSectionDetail = onNavigateToSectionDetail, onRetry = onRetryTrendSection, onReload = onReloadTrendSection, - modifier = Modifier, + modifier = Modifier + .testTag("TrendingGames"), ) } @@ -361,7 +369,8 @@ private fun CompactContent( onNavigateToSectionDetail = onNavigateToSectionDetail, onRetry = onRetryHighRatedSection, onReload = onReloadHighRatedSection, - modifier = Modifier, + modifier = Modifier + .testTag("HighRatedGames"), ) } @@ -372,7 +381,8 @@ private fun CompactContent( onNavigateToSectionDetail = onNavigateToSectionDetail, onRetry = onRetryNewReleaseSection, onReload = onReloadNewReleaseSection, - modifier = Modifier, + modifier = Modifier + .testTag("NewReleaseGames"), ) } } @@ -407,6 +417,8 @@ private fun LeastMediumContent( TrendingGameCard( game = game, onClick = { onGameClick(game.id) }, + modifier = Modifier + .testTag("TrendingGameCard"), ) } @@ -414,6 +426,8 @@ private fun LeastMediumContent( HighRatedGameCard( game = game, onClick = { onGameClick(game.id) }, + modifier = Modifier + .testTag("HighRatedGameCard"), ) } @@ -421,6 +435,8 @@ private fun LeastMediumContent( NewReleaseGameCard( game = game, onClick = { onGameClick(game.id) }, + modifier = Modifier + .testTag("NewReleaseGameCard"), ) } } diff --git a/app/src/main/kotlin/com/lilin/gamelibrary/feature/favorite/FavoriteScreen.kt b/app/src/main/kotlin/com/lilin/gamelibrary/feature/favorite/FavoriteScreen.kt index 00dc58e..7e0e4b4 100644 --- a/app/src/main/kotlin/com/lilin/gamelibrary/feature/favorite/FavoriteScreen.kt +++ b/app/src/main/kotlin/com/lilin/gamelibrary/feature/favorite/FavoriteScreen.kt @@ -35,6 +35,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign @@ -155,7 +156,9 @@ private fun FavoriteScreen( when (uiState) { is FavoriteUiState.Empty -> { FavoriteEmptyContent( - modifier = modifier.fillMaxSize(), + modifier = modifier + .fillMaxSize() + .testTag("FavoriteEmptyView"), ) } @@ -198,6 +201,7 @@ private fun FavoriteScreen( onClickDelete = { gameId -> onClickDelete(gameId) }, + modifier = Modifier.testTag("FavoriteGameCompactCard"), ) } } diff --git a/app/src/main/kotlin/com/lilin/gamelibrary/feature/search/SearchScreen.kt b/app/src/main/kotlin/com/lilin/gamelibrary/feature/search/SearchScreen.kt index 2e7c717..ccb43bf 100644 --- a/app/src/main/kotlin/com/lilin/gamelibrary/feature/search/SearchScreen.kt +++ b/app/src/main/kotlin/com/lilin/gamelibrary/feature/search/SearchScreen.kt @@ -56,6 +56,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign @@ -157,7 +158,9 @@ fun SearchScreen( query = query, onQueryChange = viewModel::onQueryChange, onSearch = viewModel::search, - modifier = Modifier.padding(bottom = 10.dp), + modifier = Modifier + .padding(bottom = 10.dp) + .testTag("SearchBottomBar"), ) } }, @@ -201,7 +204,9 @@ private fun SearchScreen( when (searchUiState) { is SearchUiState.None -> { SearchEmptyState( - modifier = Modifier.fillMaxSize(), + modifier = Modifier + .fillMaxSize() + .testTag("EmptyView"), ) } @@ -242,6 +247,7 @@ private fun SearchScreen( bottomBarPadding = bottomBarPadding, onLoadNextPage = onLoadNextPage, navigateToDetail = navigateToDetail, + modifier = Modifier.testTag("SearchResultsCompactList") ) } } diff --git a/app/src/main/kotlin/com/lilin/gamelibrary/feature/sectiondetail/SectionDetailScreen.kt b/app/src/main/kotlin/com/lilin/gamelibrary/feature/sectiondetail/SectionDetailScreen.kt index 0ec9c6d..1dd0497 100644 --- a/app/src/main/kotlin/com/lilin/gamelibrary/feature/sectiondetail/SectionDetailScreen.kt +++ b/app/src/main/kotlin/com/lilin/gamelibrary/feature/sectiondetail/SectionDetailScreen.kt @@ -46,6 +46,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign @@ -129,7 +130,8 @@ fun SectionDetailScreen( currentDisplayMode = displayMode, onBackClick = onBackClick, onDisplayModeChange = viewModel::changeDisplayMode, - modifier = Modifier, + modifier = Modifier + .testTag("SectionDetailBottomBar"), ) }, contentWindowInsets = WindowInsets.navigationBars, @@ -262,7 +264,9 @@ private fun GameGridContent( LazyVerticalGrid( state = gridScrollState, columns = GridCells.Fixed(2), - modifier = Modifier.fillMaxSize(), + modifier = Modifier + .fillMaxSize() + .testTag("SectionDetailLazyVerticalGrid"), contentPadding = PaddingValues( start = 20.dp, end = 20.dp, @@ -325,7 +329,9 @@ private fun GameListContent( bottom = bottomBarPadding, ), verticalArrangement = Arrangement.spacedBy(12.dp), - modifier = modifier.fillMaxSize(), + modifier = modifier + .fillMaxSize() + .testTag("SectionDetailLazyColumn"), ) { items(items = games, key = { it.id }) { game -> GameListCard( diff --git a/app/src/main/kotlin/com/lilin/gamelibrary/ui/component/NavigationBar.kt b/app/src/main/kotlin/com/lilin/gamelibrary/ui/component/NavigationBar.kt index 1ceaaf8..f97761c 100644 --- a/app/src/main/kotlin/com/lilin/gamelibrary/ui/component/NavigationBar.kt +++ b/app/src/main/kotlin/com/lilin/gamelibrary/ui/component/NavigationBar.kt @@ -7,6 +7,7 @@ import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -42,6 +43,7 @@ fun GameLibraryNavigationBar( contentDescription = stringResource(topLevelRoute.label), ) }, + modifier = Modifier.testTag(stringResource(topLevelRoute.label)), label = { Text(text = stringResource(topLevelRoute.label)) }, diff --git a/app/src/main/kotlin/com/lilin/gamelibrary/ui/component/detail/GameDetailBottomBar.kt b/app/src/main/kotlin/com/lilin/gamelibrary/ui/component/detail/GameDetailBottomBar.kt index 21e852b..15b6206 100644 --- a/app/src/main/kotlin/com/lilin/gamelibrary/ui/component/detail/GameDetailBottomBar.kt +++ b/app/src/main/kotlin/com/lilin/gamelibrary/ui/component/detail/GameDetailBottomBar.kt @@ -16,6 +16,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -39,7 +40,10 @@ fun GameDetailBottomBar( horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, ) { - IconButton(onClick = onBackClick) { + IconButton( + onClick = onBackClick, + modifier = Modifier.testTag("BackKey"), + ) { Icon( imageVector = Icons.AutoMirrored.Rounded.ArrowBack, contentDescription = stringResource(R.string.detail_bottom_bar_back), @@ -51,6 +55,7 @@ fun GameDetailBottomBar( IconButton( onClick = onFavoriteClick, enabled = isSuccessState, + modifier = Modifier.testTag("FavoriteButton"), ) { Icon( imageVector = if (isFavorite) { @@ -74,6 +79,7 @@ fun GameDetailBottomBar( IconButton( onClick = onShareClick, enabled = isSuccessState, + modifier = Modifier.testTag("ShareButton"), ) { Icon( imageVector = Icons.Rounded.Share, diff --git a/app/src/main/kotlin/com/lilin/gamelibrary/ui/component/discovery/GameCard.kt b/app/src/main/kotlin/com/lilin/gamelibrary/ui/component/discovery/GameCard.kt index e4e5657..886d03a 100644 --- a/app/src/main/kotlin/com/lilin/gamelibrary/ui/component/discovery/GameCard.kt +++ b/app/src/main/kotlin/com/lilin/gamelibrary/ui/component/discovery/GameCard.kt @@ -28,6 +28,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.shadow import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.platform.testTag import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource @@ -72,7 +73,8 @@ fun TrendingGameCard( shape = RoundedCornerShape(16.dp), ambientColor = TrendingGradientStart.copy(alpha = 0.3f), spotColor = TrendingGradientStart.copy(alpha = 0.3f), - ), + ) + .testTag("TrendingGameCard"), shape = RoundedCornerShape(16.dp), elevation = CardDefaults.cardElevation(0.dp), ) { diff --git a/app/src/main/kotlin/com/lilin/gamelibrary/ui/component/discovery/GameSections.kt b/app/src/main/kotlin/com/lilin/gamelibrary/ui/component/discovery/GameSections.kt index 3afe3ea..1925a98 100644 --- a/app/src/main/kotlin/com/lilin/gamelibrary/ui/component/discovery/GameSections.kt +++ b/app/src/main/kotlin/com/lilin/gamelibrary/ui/component/discovery/GameSections.kt @@ -10,6 +10,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.testTag import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.lilin.gamelibrary.domain.model.Game @@ -38,6 +39,7 @@ fun TrendingGamesSection( LazyRow( horizontalArrangement = Arrangement.spacedBy(16.dp), contentPadding = PaddingValues(horizontal = 16.dp), + modifier = Modifier.testTag("TrendingGamesSectionLazyRow") ) { items(items = games, key = { it.id }) { game -> TrendingGameCard( @@ -50,6 +52,7 @@ fun TrendingGamesSection( SeeMoreCard( sectionType = SectionType.TRENDING, onClick = onSeeMoreClick, + modifier = Modifier.testTag("TrendingSeeMoreCard") ) } } @@ -78,6 +81,7 @@ fun HighRatedGamesSection( LazyRow( horizontalArrangement = Arrangement.spacedBy(16.dp), contentPadding = PaddingValues(horizontal = 16.dp), + modifier = Modifier.testTag("HighRatedGamesSectionLazyRow") ) { items(items = games, key = { it.id }) { game -> HighRatedGameCard( @@ -90,6 +94,7 @@ fun HighRatedGamesSection( SeeMoreCard( sectionType = SectionType.HIGH_RATED, onClick = onSeeMoreClick, + modifier = Modifier.testTag("HighRatedSeeMoreCard") ) } } @@ -118,6 +123,7 @@ fun NewReleaseGamesSection( LazyRow( horizontalArrangement = Arrangement.spacedBy(16.dp), contentPadding = PaddingValues(horizontal = 16.dp), + modifier = Modifier.testTag("NewReleaseGamesSectionLazyRow") ) { items(items = games, key = { it.id }) { game -> NewReleaseGameCard( @@ -130,6 +136,7 @@ fun NewReleaseGamesSection( SeeMoreCard( sectionType = SectionType.NEW_RELEASE, onClick = onSeeMoreClick, + modifier = Modifier.testTag("NewReleaseSeeMoreCard") ) } } diff --git a/app/src/main/kotlin/com/lilin/gamelibrary/ui/component/favorite/DeleteConfirmDialog.kt b/app/src/main/kotlin/com/lilin/gamelibrary/ui/component/favorite/DeleteConfirmDialog.kt index ca44dc7..8c14f17 100644 --- a/app/src/main/kotlin/com/lilin/gamelibrary/ui/component/favorite/DeleteConfirmDialog.kt +++ b/app/src/main/kotlin/com/lilin/gamelibrary/ui/component/favorite/DeleteConfirmDialog.kt @@ -8,6 +8,7 @@ import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview @@ -48,6 +49,7 @@ fun DeleteConfirmDialog( colors = ButtonDefaults.textButtonColors( contentColor = MaterialTheme.colorScheme.error, ), + modifier = Modifier.testTag("ConfirmDeleteButton"), ) { Text(text = stringResource(R.string.favorite_alert_dialog_delete)) } @@ -57,7 +59,7 @@ fun DeleteConfirmDialog( Text(text = stringResource(R.string.favorite_alert_dialog_cancel)) } }, - modifier = modifier, + modifier = modifier.testTag("DeleteDialog"), ) } diff --git a/app/src/main/kotlin/com/lilin/gamelibrary/ui/component/favorite/FavoriteGameCompactCard.kt b/app/src/main/kotlin/com/lilin/gamelibrary/ui/component/favorite/FavoriteGameCompactCard.kt index 78eaab1..4358a85 100644 --- a/app/src/main/kotlin/com/lilin/gamelibrary/ui/component/favorite/FavoriteGameCompactCard.kt +++ b/app/src/main/kotlin/com/lilin/gamelibrary/ui/component/favorite/FavoriteGameCompactCard.kt @@ -40,6 +40,7 @@ import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.intl.Locale @@ -228,7 +229,9 @@ fun FavoriteGameCompactCard( ) { IconButton( onClick = { onClickDelete(game.id) }, - modifier = Modifier.size(48.dp), + modifier = Modifier + .size(48.dp) + .testTag("DeleteButton"), ) { Icon( imageVector = Icons.Filled.Cancel, diff --git a/app/src/main/kotlin/com/lilin/gamelibrary/ui/component/search/SearchBottomBar.kt b/app/src/main/kotlin/com/lilin/gamelibrary/ui/component/search/SearchBottomBar.kt index e98cf3e..85606b6 100644 --- a/app/src/main/kotlin/com/lilin/gamelibrary/ui/component/search/SearchBottomBar.kt +++ b/app/src/main/kotlin/com/lilin/gamelibrary/ui/component/search/SearchBottomBar.kt @@ -32,6 +32,7 @@ import androidx.compose.ui.draw.scale import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType @@ -126,7 +127,8 @@ fun SearchBottomBar( .padding(horizontal = 12.dp) .onFocusChanged { focusState -> isFocused = focusState.isFocused - }, + } + .testTag("SearchTextField"), ) } } diff --git a/app/src/main/kotlin/com/lilin/gamelibrary/ui/component/sectiondetail/SectionDetailBottomBar.kt b/app/src/main/kotlin/com/lilin/gamelibrary/ui/component/sectiondetail/SectionDetailBottomBar.kt index 8bd5af7..1cf5a86 100644 --- a/app/src/main/kotlin/com/lilin/gamelibrary/ui/component/sectiondetail/SectionDetailBottomBar.kt +++ b/app/src/main/kotlin/com/lilin/gamelibrary/ui/component/sectiondetail/SectionDetailBottomBar.kt @@ -15,6 +15,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -36,7 +37,10 @@ fun SectionDetailBottomBar( .padding(vertical = 10.dp), verticalAlignment = Alignment.CenterVertically, ) { - IconButton(onClick = onBackClick) { + IconButton( + onClick = onBackClick, + modifier = Modifier.testTag("BackKey") + ) { Icon( imageVector = Icons.AutoMirrored.Rounded.ArrowBack, contentDescription = stringResource(R.string.detail_bottom_bar_back), @@ -51,6 +55,7 @@ fun SectionDetailBottomBar( ) { IconButton( onClick = { onDisplayModeChange(DisplayMode.GRID_COLUMN) }, + modifier = Modifier.testTag("GridMode"), ) { Icon( imageVector = Icons.Filled.GridView, @@ -65,6 +70,7 @@ fun SectionDetailBottomBar( IconButton( onClick = { onDisplayModeChange(DisplayMode.LIST) }, + modifier = Modifier.testTag("ListMode"), ) { Icon( imageVector = Icons.AutoMirrored.Filled.ViewList, diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d9a091b..aab2573 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -70,6 +70,7 @@ compose-shimmer = { group = "com.valentinilk.shimmer", name = "compose-shimmer", # DI hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" } hilt-android-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" } +hilt-android-testing = { group = "com.google.dagger", name = "hilt-android-testing", version.ref = "hilt" } hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "hiltNavigation" } # roborazzi