Fixing UI tests

Change-Id: Iaa0ce3ae92e0c736aef5809c1e471c9cbff695a9
pull/2/head
Jolanda Verhoef 3 years ago
parent 53248e6dac
commit 1b06dc97ea

@ -36,7 +36,8 @@ android {
versionCode 1 versionCode 1
versionName "0.0.1" // X.Y.Z; X = Major, Y = minor, Z = Patch level versionName "0.0.1" // X.Y.Z; X = Major, Y = minor, Z = Patch level
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" // Custom test runner to set up Hilt dependency graph
testInstrumentationRunner "com.google.samples.apps.nowinandroid.NiaTestRunner"
vectorDrawables { vectorDrawables {
useSupportLibrary true useSupportLibrary true
} }
@ -179,6 +180,8 @@ dependencies {
androidTestImplementation libs.androidx.test.runner androidTestImplementation libs.androidx.test.runner
androidTestImplementation libs.androidx.test.rules androidTestImplementation libs.androidx.test.rules
androidTestImplementation libs.androidx.compose.ui.test androidTestImplementation libs.androidx.compose.ui.test
androidTestImplementation libs.hilt.android.testing
kaptAndroidTest libs.hilt.compiler
// androidx.test is forcing JUnit, 4.12. This forces it to use 4.13 // androidx.test is forcing JUnit, 4.12. This forces it to use 4.13
configurations.configureEach { configurations.configureEach {

@ -0,0 +1,31 @@
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid
import android.app.Application
import android.content.Context
import androidx.test.runner.AndroidJUnitRunner
import dagger.hilt.android.testing.HiltTestApplication
/**
* A custom runner to set up the instrumented application class for tests.
*/
class NiaTestRunner : AndroidJUnitRunner() {
override fun newApplication(cl: ClassLoader?, name: String?, context: Context?): Application {
return super.newApplication(cl, HiltTestApplication::class.java.name, context)
}
}

@ -0,0 +1,81 @@
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid.di
import androidx.datastore.core.DataStore
import androidx.datastore.core.DataStoreFactory
import com.google.samples.apps.nowinandroid.data.UserPreferences
import com.google.samples.apps.nowinandroid.data.UserPreferencesSerializer
import com.google.samples.apps.nowinandroid.data.fake.FakeNewsRepository
import com.google.samples.apps.nowinandroid.data.fake.FakeTopicsRepository
import com.google.samples.apps.nowinandroid.data.repository.NewsRepository
import com.google.samples.apps.nowinandroid.data.repository.TopicsRepository
import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.hilt.components.SingletonComponent
import dagger.hilt.testing.TestInstallIn
import javax.inject.Singleton
import kotlinx.serialization.json.Json
import org.junit.rules.TemporaryFolder
/**
* The [TestAppModule] replaces [AppModule] during instrumentation tests. It creates test doubles
* where necessary. It also includes logic to prevent multiple data stores with the same file name
* from being created during one test execution context.
*/
@Module
@TestInstallIn(
components = [SingletonComponent::class],
replaces = [AppModule::class]
)
interface TestAppModule {
// Use a fake repository as a test double, so we don't have a network dependency.
@Binds
fun bindsTopicRepository(fakeTopicsRepository: FakeTopicsRepository): TopicsRepository
// Use a fake repository as a test double, so we don't have a network dependency.
@Binds
fun bindsNewsResourceRepository(fakeNewsRepository: FakeNewsRepository): NewsRepository
// Use the default dispatchers. For the high-level UI tests, we don't want to override these.
@Binds
fun bindsNiaDispatchers(defaultNiaDispatchers: DefaultNiaDispatchers): NiaDispatchers
companion object {
@Provides
@Singleton
fun providesUserPreferencesDataStore(
userPreferencesSerializer: UserPreferencesSerializer,
tmpFolder: TemporaryFolder
): DataStore<UserPreferences> {
return DataStoreFactory.create(
serializer = userPreferencesSerializer,
) {
tmpFolder.newFile("user_preferences_test.pb")
}
}
@Provides
@Singleton
fun providesNetworkJson(): Json = Json {
ignoreUnknownKeys = true
}
}
}

@ -16,128 +16,160 @@
package com.google.samples.apps.nowinandroid.ui package com.google.samples.apps.nowinandroid.ui
import androidx.compose.ui.test.junit4.ComposeTestRule import androidx.compose.ui.test.assertIsOn
import androidx.compose.ui.test.hasText
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performClick
import androidx.test.espresso.Espresso import androidx.test.espresso.Espresso
import androidx.test.espresso.NoActivityResumedException import androidx.test.espresso.NoActivityResumedException
import com.google.samples.apps.nowinandroid.MainActivity import com.google.samples.apps.nowinandroid.MainActivity
import com.google.samples.apps.nowinandroid.R
import dagger.hilt.android.testing.BindValue
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Before
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TemporaryFolder
/** /**
* Tests all the navigation flows that are handled by the navigation library. * Tests all the navigation flows that are handled by the navigation library.
*/ */
@HiltAndroidTest
class NavigationTest { class NavigationTest {
/**
* Manages the components' state and is used to perform injection on your test
*/
@get:Rule(order = 0)
val hiltRule = HiltAndroidRule(this)
/** /**
* Use the primary activity to initialize the app normally. * Use the primary activity to initialize the app normally.
*
* TODO: Bind fakes as needed to the Dagger graph to allow for easier testing
*/ */
@get:Rule @get:Rule(order = 1)
val composeTestRule = createAndroidComposeRule<MainActivity>() val composeTestRule = createAndroidComposeRule<MainActivity>()
/**
* Create a temporary folder used to create a Data Store file. This guarantees that
* the file is removed in between each test, preventing a crash.
*/
@BindValue @get:Rule(order = 2)
val tmpFolder: TemporaryFolder = TemporaryFolder.builder().assureDeletion().build()
// The strings used for matching in these tests
private lateinit var done: String
private lateinit var navigateUp: String
private lateinit var forYouLoading: String
private lateinit var forYou: String
private lateinit var episodes: String
private lateinit var saved: String
private lateinit var topics: String
@Before
fun setup() {
composeTestRule.activity.apply {
done = getString(R.string.done)
navigateUp = getString(R.string.navigate_up)
forYouLoading = getString(R.string.for_you_loading)
forYou = getString(R.string.for_you)
episodes = getString(R.string.episodes)
saved = getString(R.string.saved)
topics = getString(R.string.following)
}
}
@Test @Test
fun firstScreenIsForYou() { fun firstScreen_isForYou() {
composeTestRule.forYouDestinationTopMatcher() composeTestRule.apply {
.assertExists("Could not find FOR YOU text on first screen after app open") // WAIT for initial content to be shown
waitUntil { onAllNodes(hasText("HEADLINES")).fetchSemanticsNodes().isNotEmpty() }
// VERIFY first topic is displayed
onNodeWithText("HEADLINES").assertExists()
}
} }
// TODO: implement tests related to navigation & resetting of destinations (b/213307564) // TODO: implement tests related to navigation & resetting of destinations (b/213307564)
// Restoring content should be tested with another tab than the For You one, as that will
// still succeed even when restoring state is turned off.
/** /**
* As per guidelines: * When navigating between the different top level destinations, we should restore the state
* * of previously visited destinations.
* When you select a navigation bar item (one thats not currently selected), the app navigates
* to that destinations screen.
*
* Any prior user interactions and temporary screen states are reset, such as scroll position,
* tab selection, and inline search.
*
* This default behavior can be overridden when needed to improve the user experience. For
* example, an Android app that requires frequent switching between sections can preserve each
* sections state.
*/ */
// @Test @Test
// fun navigateToUnselectedTabResetsContent1() { fun navigationBar_navigateToPreviouslySelectedTab_restoresContent() {
// // GIVEN the user was previously on the Following destination composeTestRule.apply {
// composeTestRule.followingDestinationTopMatcher().performClick() // WAIT for initial content to be shown
// // and scrolled down waitUntil { onAllNodes(hasText("HEADLINES")).fetchSemanticsNodes().isNotEmpty() }
// [IMPLEMENT] Match the root scrollable container and scroll down to an item below the fold // GIVEN the user follows a topic
// composeTestRule.followingDestinationTopMatcher() onNodeWithText("HEADLINES").performClick()
// .assertDoesNotExist() // verify we scrolled beyond the top // WHEN the user navigates to the Topics destination
// // and then navigated back to the For You destination onNodeWithText(topics).performClick()
// composeTestRule.forYouDestinationTopMatcher().performClick() // AND the user navigates to the For You destination
// // WHEN the user presses the Topic navigation bar item onNodeWithText(forYou).performClick()
// composeTestRule.followingDestinationTopMatcher().performClick() // THEN the state of the For You destination is restored
// // THEN the Following destination shows at the top. onNodeWithText("HEADLINES").assertIsOn()
// composeTestRule.followingDestinationTopMatcher() }
// .assertExists("Screen did not correctly reset to the top after re-navigating to it") }
// }
// @Test
// fun navigateToUnselectedTabResetsContent2() {
// // GIVEN the user was previously on the Following destination
// composeTestRule.followingDestinationTopMatcher().performClick()
// // and navigated to the Topic detail destination
// [IMPLEMENT] Navigate to topic detail destination
// composeTestRule.followingDestinationTopMatcher()
// .assertDoesNotExist() // verify we are not on Following overview destination any more
// // and then navigated back to the For You destination
// composeTestRule.forYouDestinationTopMatcher().performClick()
// // WHEN the user presses the Topic navigation bar item
// composeTestRule.followingDestinationTopMatcher().performClick()
// // THEN the Following destination shows at the top.
// composeTestRule.followingDestinationTopMatcher()
// .assertExists("Screen did not correctly reset to the top after re-navigating to it")
// }
// @Test /**
// fun reselectingTabResetsContent1() { * When reselecting a tab, it should show that tab's start destination and restore its state.
// // GIVEN the user is on the For You destination */
// // and has scrolled down @Test
// // WHEN the user taps the For You navigation bar item fun navigationBar_reselectTab_keepsState() {
// // THEN the For You destination shows at the top of the destination composeTestRule.apply {
// } // WAIT for initial content to be shown
waitUntil { onAllNodes(hasText("HEADLINES")).fetchSemanticsNodes().isNotEmpty() }
// GIVEN the user follows a topic
onNodeWithText("HEADLINES").performClick()
// WHEN the user taps the For You navigation bar item
onNodeWithText(forYou).performClick()
// THEN the state of the For You destination is restored
onNodeWithText("HEADLINES").assertIsOn()
}
}
// @Test // @Test
// fun reselectingTabResetsContent2() { // fun navigationBar_reselectTab_resetsToStartDestination() {
// // GIVEN the user is on the Following destination // // GIVEN the user is on the Topics destination and scrolls
// // and navigates to the Topic Detail destination // // and navigates to the Topic Detail destination
// // WHEN the user taps the Following navigation bar item // // WHEN the user taps the Topics navigation bar item
// // THEN the Following destination shows at the top of the destination // // THEN the Topics destination shows in the same scrolled state
// } // }
/* /*
* Top level destinations should never show an up affordance. * Top level destinations should never show an up affordance.
*/ */
@Test @Test
fun topLevelDestinationsDoNotShowUpArrow() { fun topLevelDestinations_doNotShowUpArrow() {
// GIVEN the user is on any of the top level destinations, THEN the Up arrow is not shown. composeTestRule.apply {
composeTestRule.onNodeWithContentDescription("Navigate up").assertDoesNotExist() // GIVEN the user is on any of the top level destinations, THEN the Up arrow is not shown.
composeTestRule.onNodeWithText("Episodes").performClick() onNodeWithContentDescription(navigateUp).assertDoesNotExist()
composeTestRule.onNodeWithContentDescription("Navigate up").assertDoesNotExist() onNodeWithText(episodes).performClick()
composeTestRule.onNodeWithText("Saved").performClick() onNodeWithContentDescription(navigateUp).assertDoesNotExist()
composeTestRule.onNodeWithContentDescription("Navigate up").assertDoesNotExist() onNodeWithText(saved).performClick()
composeTestRule.onNodeWithText("Following").performClick() onNodeWithContentDescription(navigateUp).assertDoesNotExist()
composeTestRule.onNodeWithContentDescription("Navigate up").assertDoesNotExist() onNodeWithText(topics).performClick()
onNodeWithContentDescription(navigateUp).assertDoesNotExist()
}
} }
/* /*
* There should always be at most one instance of a top-level destination at the same time. * There should always be at most one instance of a top-level destination at the same time.
*/ */
@Test(expected = NoActivityResumedException::class) @Test(expected = NoActivityResumedException::class)
fun backFromHomeDestinationQuitsApp() { fun homeDestination_back_quitsApp() {
// GIVEN the user navigates to the Episodes destination composeTestRule.apply {
composeTestRule.onNodeWithText("Episodes").performClick() // GIVEN the user navigates to the Episodes destination
// and then navigates to the For you destination onNodeWithText(episodes).performClick()
composeTestRule.onNodeWithText("For you").performClick() // and then navigates to the For you destination
// WHEN the user uses the system button/gesture to go back onNodeWithText(forYou).performClick()
Espresso.pressBack() // WHEN the user uses the system button/gesture to go back
// THEN the app quits Espresso.pressBack()
// THEN the app quits
}
} }
/* /*
@ -145,26 +177,18 @@ class NavigationTest {
* to the "For you" destination, no matter which destinations you visited in between. * to the "For you" destination, no matter which destinations you visited in between.
*/ */
@Test @Test
fun backFromDestinationReturnsToForYou() { fun navigationBar_backFromAnyDestination_returnsToForYou() {
// GIVEN the user navigated to the Episodes destination composeTestRule.apply {
composeTestRule.onNodeWithText("Episodes").performClick() // WAIT for initial content to be shown
// and then navigated to the Following destination waitUntil { onAllNodes(hasText("HEADLINES")).fetchSemanticsNodes().isNotEmpty() }
composeTestRule.onNodeWithText("Following").performClick() // GIVEN the user navigated to the Episodes destination
// WHEN the user uses the system button/gesture to go back, onNodeWithText(episodes).performClick()
Espresso.pressBack() // and then navigated to the Topics destination
// THEN the app shows the For You destination onNodeWithText(topics).performClick()
composeTestRule.forYouDestinationTopMatcher().assertExists() // WHEN the user uses the system button/gesture to go back,
Espresso.pressBack()
// THEN the app shows the For You destination
onNodeWithText("HEADLINES").assertExists()
}
} }
/*
* Matches an element at the top of the For You destination. Should be updated when the
* destination is implemented.
*/
private fun ComposeTestRule.forYouDestinationTopMatcher() = onNodeWithTag("FOR YOU")
/*
* Matches an element at the top of the Following destination. Should be updated when the
* destination is implemented.
*/
private fun ComposeTestRule.followingDestinationTopMatcher() = onNodeWithText("FOLLOWING")
} }

@ -21,6 +21,8 @@ import androidx.compose.ui.test.assertHasClickAction
import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsEnabled import androidx.compose.ui.test.assertIsEnabled
import androidx.compose.ui.test.assertIsNotEnabled import androidx.compose.ui.test.assertIsNotEnabled
import androidx.compose.ui.test.assertIsOff
import androidx.compose.ui.test.assertIsOn
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onNodeWithText
@ -82,19 +84,19 @@ class ForYouScreenTest {
composeTestRule composeTestRule
.onNodeWithText("HEADLINES") .onNodeWithText("HEADLINES")
.assertIsDisplayed() .assertIsDisplayed()
// .assertIsOff() .assertIsOff()
.assertHasClickAction() .assertHasClickAction()
composeTestRule composeTestRule
.onNodeWithText("UI") .onNodeWithText("UI")
.assertIsDisplayed() .assertIsDisplayed()
// .assertIsOff() .assertIsOff()
.assertHasClickAction() .assertHasClickAction()
composeTestRule composeTestRule
.onNodeWithText("TOOLS") .onNodeWithText("TOOLS")
.assertIsDisplayed() .assertIsDisplayed()
// .assertIsOff() .assertIsOff()
.assertHasClickAction() .assertHasClickAction()
composeTestRule composeTestRule
@ -136,19 +138,19 @@ class ForYouScreenTest {
composeTestRule composeTestRule
.onNodeWithText("HEADLINES") .onNodeWithText("HEADLINES")
.assertIsDisplayed() .assertIsDisplayed()
// .assertIsOff() .assertIsOff()
.assertHasClickAction() .assertHasClickAction()
composeTestRule composeTestRule
.onNodeWithText("UI") .onNodeWithText("UI")
.assertIsDisplayed() .assertIsDisplayed()
// .assertIsOn() .assertIsOn()
.assertHasClickAction() .assertHasClickAction()
composeTestRule composeTestRule
.onNodeWithText("TOOLS") .onNodeWithText("TOOLS")
.assertIsDisplayed() .assertIsDisplayed()
// .assertIsOff() .assertIsOff()
.assertHasClickAction() .assertHasClickAction()
composeTestRule composeTestRule

@ -27,7 +27,7 @@ object FakeDataSource {
val sampleTopic = NetworkTopic( val sampleTopic = NetworkTopic(
id = 0, id = 0,
name = "Headlines", name = "Headlines",
description = "", description = "At vero eos et accusamus et iusto odio dignissimos ducimus qui.",
) )
val sampleResource = NetworkNewsResource( val sampleResource = NetworkNewsResource(
id = 1, id = 1,

@ -25,9 +25,11 @@ import kotlinx.coroutines.flow.flowOf
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
/** /**
* [NewsRepository] implementation that provides static news resources to aid development * Fake implementation of the [NewsRepository] that retrieves the news resources from a JSON String.
*
* This allows us to run the app with fake data, without needing an internet connection or working
* backend.
*/ */
class FakeNewsRepository @Inject constructor( class FakeNewsRepository @Inject constructor(
private val dispatchers: NiaDispatchers, private val dispatchers: NiaDispatchers,
private val networkJson: Json private val networkJson: Json

@ -28,6 +28,13 @@ import kotlinx.coroutines.flow.flowOn
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
/**
* Fake implementation of the [TopicsRepository] that retrieves the topics from a JSON String, and
* uses a local DataStore instance to save and retrieve followed topic ids.
*
* This allows us to run the app with fake data, without needing an internet connection or working
* backend.
*/
class FakeTopicsRepository @Inject constructor( class FakeTopicsRepository @Inject constructor(
private val dispatchers: NiaDispatchers, private val dispatchers: NiaDispatchers,
private val networkJson: Json, private val networkJson: Json,

@ -19,7 +19,6 @@ package com.google.samples.apps.nowinandroid.ui
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
@ -45,7 +44,7 @@ fun NiaNavGraph(
startDestination = startDestination, startDestination = startDestination,
) { ) {
composable(NiaDestinations.FOR_YOU_ROUTE) { composable(NiaDestinations.FOR_YOU_ROUTE) {
ForYouRoute(modifier = modifier.testTag("FOR YOU")) ForYouRoute(modifier)
} }
composable(NiaDestinations.EPISODES_ROUTE) { composable(NiaDestinations.EPISODES_ROUTE) {
Text("EPISODES", modifier) Text("EPISODES", modifier)
@ -56,7 +55,7 @@ fun NiaNavGraph(
composable(NiaDestinations.FOLLOWING_ROUTE) { composable(NiaDestinations.FOLLOWING_ROUTE) {
FollowingRoute( FollowingRoute(
navigateToTopic = { navController.navigate(NiaDestinations.TOPIC_ROUTE) }, navigateToTopic = { navController.navigate(NiaDestinations.TOPIC_ROUTE) },
modifier = modifier.testTag(NiaDestinations.FOLLOWING_ROUTE), modifier = modifier
) )
} }
composable(NiaDestinations.TOPIC_ROUTE) { composable(NiaDestinations.TOPIC_ROUTE) {

@ -16,6 +16,7 @@
package com.google.samples.apps.nowinandroid.ui package com.google.samples.apps.nowinandroid.ui
import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
/** /**
@ -37,8 +38,17 @@ object NiaDestinations {
class NiaNavigationActions(private val navController: NavHostController) { class NiaNavigationActions(private val navController: NavHostController) {
fun navigateToTopLevelDestination(route: String) { fun navigateToTopLevelDestination(route: String) {
navController.navigate(route) { navController.navigate(route) {
// Pop up to the start destination of the graph to
// avoid building up a large stack of destinations
// on the back stack as users select items
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
// Avoid multiple copies of the same destination when
// reselecting the same item
launchSingleTop = true launchSingleTop = true
navController.graph.startDestinationRoute?.let { popUpTo(it) } // Restore state when reselecting a previously selected item
restoreState = true
} }
} }
} }

@ -23,6 +23,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.selection.toggleable
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
@ -34,6 +35,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.key import androidx.compose.runtime.key
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
@ -117,7 +119,10 @@ private fun LazyListScope.TopicSelection(
ButtonDefaults.buttonColors() ButtonDefaults.buttonColors()
} else { } else {
ButtonDefaults.outlinedButtonColors() ButtonDefaults.outlinedButtonColors()
} },
modifier = Modifier.toggleable(
value = isSelected, role = Role.Button, onValueChange = {}
)
) { ) {
Text( Text(
text = topic.name.uppercase(), text = topic.name.uppercase(),

@ -20,6 +20,7 @@
<string name="saved">Saved</string> <string name="saved">Saved</string>
<string name="done">Done</string> <string name="done">Done</string>
<string name="for_you_loading">Loading for you…</string> <string name="for_you_loading">Loading for you…</string>
<string name="navigate_up">Navigate up</string>
<!-- NewsResource Card --> <!-- NewsResource Card -->
<string name="bookmark">Bookmark</string> <string name="bookmark">Bookmark</string>

@ -58,7 +58,8 @@ androidx-test-espresso-core = { group = "androidx.test.espresso", name = "espres
androidx-test-runner = { group = "androidx.test", name = "runner", version.ref = "androidxTest" } androidx-test-runner = { group = "androidx.test", name = "runner", version.ref = "androidxTest" }
androidx-test-rules = { group = "androidx.test", name = "rules", version.ref = "androidxTest" } androidx-test-rules = { group = "androidx.test", name = "rules", version.ref = "androidxTest" }
hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" } hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
hilt-compiler = { group = "com.google.dagger", name = "hilt-compiler", version.ref = "hilt" } hilt-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-gradlePlugin = { group = "com.google.dagger", name = "hilt-android-gradle-plugin", version.ref = "hilt" } hilt-gradlePlugin = { group = "com.google.dagger", name = "hilt-android-gradle-plugin", version.ref = "hilt" }
junit4 = { group = "junit", name = "junit", version.ref = "junit4" } junit4 = { group = "junit", name = "junit", version.ref = "junit4" }
kotlin-gradlePlugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin" } kotlin-gradlePlugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin" }

Loading…
Cancel
Save