From 85046ec301ca98425f033833455cd39c57877858 Mon Sep 17 00:00:00 2001 From: lilin Date: Wed, 4 Feb 2026 00:16:23 +0900 Subject: [PATCH 1/5] =?UTF-8?q?Android=20Test=E3=81=AE=E6=BA=96=E5=82=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle.kts | 4 +++- .../gamelibrary/ExampleInstrumentedTest.kt | 22 ------------------- .../lilin/gamelibrary/util/HiltTestRunner.kt | 16 ++++++++++++++ gradle/libs.versions.toml | 1 + 4 files changed, 20 insertions(+), 23 deletions(-) delete mode 100644 app/src/androidTest/kotlin/com/lilin/gamelibrary/ExampleInstrumentedTest.kt create mode 100644 app/src/androidTest/kotlin/com/lilin/gamelibrary/util/HiltTestRunner.kt 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/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/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/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 From 0afd7183b17b0593c340ca50b4aba23ca5a9bce6 Mon Sep 17 00:00:00 2001 From: lilin Date: Wed, 4 Feb 2026 00:16:50 +0900 Subject: [PATCH 2/5] =?UTF-8?q?testTag=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../feature/detail/GameDetailScreen.kt | 30 +++++++++++++++---- .../feature/discovery/DiscoveryScreen.kt | 26 ++++++++++++---- .../feature/favorite/FavoriteScreen.kt | 6 +++- .../feature/search/SearchScreen.kt | 10 +++++-- .../sectiondetail/SectionDetailScreen.kt | 12 ++++++-- .../gamelibrary/ui/component/NavigationBar.kt | 2 ++ .../component/detail/GameDetailBottomBar.kt | 8 ++++- .../ui/component/discovery/GameCard.kt | 4 ++- .../ui/component/discovery/GameSections.kt | 7 +++++ .../component/favorite/DeleteConfirmDialog.kt | 4 ++- .../favorite/FavoriteGameCompactCard.kt | 5 +++- .../ui/component/search/SearchBottomBar.kt | 4 ++- .../sectiondetail/SectionDetailBottomBar.kt | 8 ++++- 13 files changed, 103 insertions(+), 23 deletions(-) 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, From 1080f1c8f9ac612823de7dea93713eb1b639a0b0 Mon Sep 17 00:00:00 2001 From: lilin Date: Wed, 4 Feb 2026 00:17:42 +0900 Subject: [PATCH 3/5] =?UTF-8?q?test=E7=94=A8=E3=81=AE=E6=8B=A1=E5=BC=B5?= =?UTF-8?q?=E9=96=A2=E6=95=B0=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lilin/gamelibrary/util/CheckViewUtil.kt | 170 ++++++++++++++++++ 1 file changed, 170 insertions(+) create mode 100644 app/src/androidTest/kotlin/com/lilin/gamelibrary/util/CheckViewUtil.kt 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", + ) +} From 7161c13e5f914144812765df0ce2dd771aefcba1 Mon Sep 17 00:00:00 2001 From: lilin Date: Wed, 4 Feb 2026 09:27:53 +0900 Subject: [PATCH 4/5] =?UTF-8?q?test=E4=BD=9C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DiscoveryScreenNavigationTest.kt | 240 ++++++++++++++++++ 1 file changed, 240 insertions(+) create mode 100644 app/src/androidTest/kotlin/com/lilin/gamelibrary/DiscoveryScreenNavigationTest.kt 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..8edb905 --- /dev/null +++ b/app/src/androidTest/kotlin/com/lilin/gamelibrary/DiscoveryScreenNavigationTest.kt @@ -0,0 +1,240 @@ +@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() { + 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() + + // LazyRowを右方向にスクロールしてNewReleaseSeeMoreCardを探してクリック(詳細画面へ遷移) + composeTestRule + .scrollRightUntilNodeExists( + scrollableTag = "NewReleaseGamesSectionLazyRow", + targetTag = "NewReleaseSeeMoreCard", + ) + .performClick() + + 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() + + 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() + + 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() + + 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() + + composeTestRule + .waitUntilNodeExists("FavoriteGameCompactCard") + .assertIsDisplayed() + + composeTestRule + .waitUntilNodeExists("DeleteButton") + .performClick() + + composeTestRule + .waitUntilNodeExists("DeleteDialog") + .assertIsDisplayed() + + composeTestRule + .waitUntilNodeExists("ConfirmDeleteButton") + .performClick() + + composeTestRule + .waitUntilNodeExists("FavoriteEmptyView") + .performClick() + } +} From d9bdf8d02079b85729e4aac0238334edde8edf29 Mon Sep 17 00:00:00 2001 From: lilin Date: Wed, 11 Feb 2026 01:25:10 +0900 Subject: [PATCH 5/5] =?UTF-8?q?=E6=95=B4=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DiscoveryScreenNavigationTest.kt | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/app/src/androidTest/kotlin/com/lilin/gamelibrary/DiscoveryScreenNavigationTest.kt b/app/src/androidTest/kotlin/com/lilin/gamelibrary/DiscoveryScreenNavigationTest.kt index 8edb905..090aab7 100644 --- a/app/src/androidTest/kotlin/com/lilin/gamelibrary/DiscoveryScreenNavigationTest.kt +++ b/app/src/androidTest/kotlin/com/lilin/gamelibrary/DiscoveryScreenNavigationTest.kt @@ -40,6 +40,20 @@ class DiscoveryScreenNavigationTest { @Test fun navigateToNewReleaseGamesFromDiscoveryScreen() { + fromDiscoveryScreenToSeeMore() + + fromSeeMoreToSearchScreen() + + fromSearchToFavoriteScreen() + + fromFavoriteToDetail() + + fromDetailToFavorite() + + checkFavoriteGameAndDelete() + } + + private fun fromDiscoveryScreenToSeeMore() { composeTestRule .waitUntilNodeExists("DiscoveryTopBar") .assertIsDisplayed() @@ -66,14 +80,15 @@ class DiscoveryScreenNavigationTest { .waitUntilNodeExists("NewReleaseGamesSectionLazyRow") .performScrollTo() - // LazyRowを右方向にスクロールしてNewReleaseSeeMoreCardを探してクリック(詳細画面へ遷移) composeTestRule .scrollRightUntilNodeExists( scrollableTag = "NewReleaseGamesSectionLazyRow", targetTag = "NewReleaseSeeMoreCard", ) .performClick() + } + private fun fromSeeMoreToSearchScreen() { composeTestRule .waitUntilNodeExists("SectionDetailBottomBar") .assertIsDisplayed() @@ -101,7 +116,9 @@ class DiscoveryScreenNavigationTest { composeTestRule .waitUntilNodeExists("BackKey") .performClick() + } + private fun fromSearchToFavoriteScreen() { composeTestRule .waitUntilNodeExists("Search") .performClick() @@ -130,7 +147,9 @@ class DiscoveryScreenNavigationTest { composeTestRule .waitUntilNodeExists("Favorite") .performClick() + } + private fun fromFavoriteToDetail() { composeTestRule .waitUntilNodeExists("FavoriteEmptyView") .performClick() @@ -160,7 +179,9 @@ class DiscoveryScreenNavigationTest { composeTestRule .onAllNodesWithTag("TrendingGameCard")[0] .performClick() + } + private fun fromDetailToFavorite() { composeTestRule .waitUntilNodeExists("GameDetailTopBar") .assertIsDisplayed() @@ -216,7 +237,9 @@ class DiscoveryScreenNavigationTest { composeTestRule .waitUntilNodeExists("Favorite") .performClick() + } + private fun checkFavoriteGameAndDelete() { composeTestRule .waitUntilNodeExists("FavoriteGameCompactCard") .assertIsDisplayed()