diff --git a/.github/workflows/Build.yaml b/.github/workflows/Build.yaml
index 54dbcb252..a047a451a 100644
--- a/.github/workflows/Build.yaml
+++ b/.github/workflows/Build.yaml
@@ -34,16 +34,16 @@ jobs:
uses: gradle/gradle-build-action@v2
- name: Check spotless
- run: ./gradlew spotlessCheck --init-script gradle/init.gradle.kts --no-configuration-cache --stacktrace
+ run: ./gradlew spotlessCheck --init-script gradle/init.gradle.kts --no-configuration-cache
- name: Check lint
- run: ./gradlew lintDemoDebug --stacktrace
+ run: ./gradlew lintDemoDebug
- name: Build all build type and flavor permutations
- run: ./gradlew assemble --stacktrace
+ run: ./gradlew assemble
- name: Run local tests
- run: ./gradlew testDemoDebug testProdDebug --stacktrace
+ run: ./gradlew testDemoDebug testProdDebug
- name: Upload build outputs (APKs)
uses: actions/upload-artifact@v3
@@ -90,7 +90,7 @@ jobs:
disable-animations: true
disk-size: 6000M
heap-size: 600M
- script: ./gradlew connectedProdDebugAndroidTest -x :benchmark:connectedProdBenchmarkAndroidTest --stacktrace
+ script: ./gradlew connectedProdDebugAndroidTest -x :benchmark:connectedProdBenchmarkAndroidTest
- name: Upload test reports
if: always()
diff --git a/.run/Generate Demo Baseline Profile.run.xml b/.run/Generate Demo Baseline Profile.run.xml
new file mode 100644
index 000000000..68174ea4b
--- /dev/null
+++ b/.run/Generate Demo Baseline Profile.run.xml
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true
+ true
+ false
+
+
+
diff --git a/README.md b/README.md
index 93eda3294..521a6cdea 100644
--- a/README.md
+++ b/README.md
@@ -1,19 +1,21 @@

-Now in Android App [Work in progress 🚧]
+
+
+Now in Android App
==================
**Learn how this app was designed and built in the [design case study](https://goo.gle/nia-figma), [architecture learning journey](docs/ArchitectureLearningJourney.md) and [modularization learning journey](docs/ModularizationLearningJourney.md).**
This is the repository for the [Now in Android](https://developer.android.com/series/now-in-android)
-app.
+app. It is a **work in progress** 🚧.
**Now in Android** is a fully functional Android app built entirely with Kotlin and Jetpack Compose. It
follows Android design and development best practices and is intended to be a useful reference
for developers. As a running app, it's intended to help developers keep up-to-date with the world
of Android development by providing regular news updates.
-The app is currently in early stage development and is not yet available on the Play Store.
+The app is currently in development. The `demoRelease` variant is [available on the Play Store in open beta](https://play.google.com/store/apps/details?id=com.google.samples.apps.nowinandroid).
# Features
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index b67a3f965..c4d60e202 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -26,8 +26,8 @@ plugins {
android {
defaultConfig {
applicationId = "com.google.samples.apps.nowinandroid"
- versionCode = 2
- versionName = "0.0.2" // X.Y.Z; X = Major, Y = minor, Z = Patch level
+ versionCode = 3
+ versionName = "0.0.3" // X.Y.Z; X = Major, Y = minor, Z = Patch level
// Custom test runner to set up Hilt dependency graph
testInstrumentationRunner = "com.google.samples.apps.nowinandroid.core.testing.NiaTestRunner"
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 4287a5fb9..5c3b889d2 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -32,8 +32,7 @@
+ android:exported="true">
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index ba503b90d..47ca6df06 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -15,12 +15,6 @@
limitations under the License.
-->
- #FFBB86FC
- #FF6200EE
- #FF3700B3
- #FF03DAC5
- #FF018786
-
#4D000000
diff --git a/benchmark/build.gradle.kts b/benchmark/build.gradle.kts
index 931fd6d00..0cbde5bd3 100644
--- a/benchmark/build.gradle.kts
+++ b/benchmark/build.gradle.kts
@@ -55,9 +55,9 @@ android {
testOptions {
managedDevices {
devices {
- create("pixel5Api30") {
- device = "Pixel 5"
- apiLevel = 30
+ create("pixel6Api31") {
+ device = "Pixel 6"
+ apiLevel = 31
systemImageSource = "aosp"
}
}
diff --git a/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/Flavor.kt b/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/Flavor.kt
index f3a12db4f..e251267ce 100644
--- a/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/Flavor.kt
+++ b/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/Flavor.kt
@@ -14,8 +14,8 @@ enum class FlavorDimension {
// purposes, or from a production backend server which supplies up-to-date, real content.
// These two product flavors reflect this behaviour.
enum class Flavor (val dimension : FlavorDimension, val applicationIdSuffix : String? = null) {
- demo(FlavorDimension.contentType, ".demo"),
- prod(FlavorDimension.contentType)
+ demo(FlavorDimension.contentType),
+ prod(FlavorDimension.contentType, ".prod")
}
fun Project.configureFlavors(
diff --git a/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstUserDataRepository.kt b/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstUserDataRepository.kt
index 8af0d3711..e95c13460 100644
--- a/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstUserDataRepository.kt
+++ b/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstUserDataRepository.kt
@@ -50,4 +50,7 @@ class OfflineFirstUserDataRepository @Inject constructor(
override suspend fun setDarkThemeConfig(darkThemeConfig: DarkThemeConfig) =
niaPreferencesDataSource.setDarkThemeConfig(darkThemeConfig)
+
+ override suspend fun setShouldHideOnboarding(shouldHideOnboarding: Boolean) =
+ niaPreferencesDataSource.setShouldHideOnboarding(shouldHideOnboarding)
}
diff --git a/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/UserDataRepository.kt b/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/UserDataRepository.kt
index 7554d3f03..8cd22c18f 100644
--- a/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/UserDataRepository.kt
+++ b/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/UserDataRepository.kt
@@ -62,4 +62,9 @@ interface UserDataRepository {
* Sets the desired dark theme config.
*/
suspend fun setDarkThemeConfig(darkThemeConfig: DarkThemeConfig)
+
+ /**
+ * Sets whether the user has completed the onboarding process.
+ */
+ suspend fun setShouldHideOnboarding(shouldHideOnboarding: Boolean)
}
diff --git a/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/fake/FakeUserDataRepository.kt b/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/fake/FakeUserDataRepository.kt
index 58714cf40..4752cd7a1 100644
--- a/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/fake/FakeUserDataRepository.kt
+++ b/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/fake/FakeUserDataRepository.kt
@@ -63,4 +63,8 @@ class FakeUserDataRepository @Inject constructor(
override suspend fun setDarkThemeConfig(darkThemeConfig: DarkThemeConfig) {
niaPreferencesDataSource.setDarkThemeConfig(darkThemeConfig)
}
+
+ override suspend fun setShouldHideOnboarding(shouldHideOnboarding: Boolean) {
+ niaPreferencesDataSource.setShouldHideOnboarding(shouldHideOnboarding)
+ }
}
diff --git a/core/data/src/test/java/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstUserDataRepositoryTest.kt b/core/data/src/test/java/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstUserDataRepositoryTest.kt
index fd4b2ccde..c1d740602 100644
--- a/core/data/src/test/java/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstUserDataRepositoryTest.kt
+++ b/core/data/src/test/java/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstUserDataRepositoryTest.kt
@@ -58,7 +58,8 @@ class OfflineFirstUserDataRepositoryTest {
followedTopics = emptySet(),
followedAuthors = emptySet(),
themeBrand = ThemeBrand.DEFAULT,
- darkThemeConfig = DarkThemeConfig.FOLLOW_SYSTEM
+ darkThemeConfig = DarkThemeConfig.FOLLOW_SYSTEM,
+ shouldHideOnboarding = false
),
subject.userDataStream.first()
)
@@ -187,4 +188,15 @@ class OfflineFirstUserDataRepositoryTest {
.first()
)
}
+
+ @Test
+ fun whenUserCompletesOnboarding_thenRemovesAllInterests_shouldHideOnboardingIsFalse() =
+ runTest {
+ subject.setFollowedTopicIds(setOf("1"))
+ subject.setShouldHideOnboarding(true)
+ assertEquals(true, subject.userDataStream.first().shouldHideOnboarding)
+
+ subject.setFollowedTopicIds(emptySet())
+ assertEquals(false, subject.userDataStream.first().shouldHideOnboarding)
+ }
}
diff --git a/core/datastore/build.gradle.kts b/core/datastore/build.gradle.kts
index 5d33f2bfb..8f3d7ece6 100644
--- a/core/datastore/build.gradle.kts
+++ b/core/datastore/build.gradle.kts
@@ -59,6 +59,7 @@ dependencies {
implementation(project(":core:model"))
testImplementation(project(":core:testing"))
+ testImplementation(project(":core:datastore-test"))
implementation(libs.kotlinx.coroutines.android)
diff --git a/core/datastore/src/main/java/com/google/samples/apps/nowinandroid/core/datastore/NiaPreferencesDataSource.kt b/core/datastore/src/main/java/com/google/samples/apps/nowinandroid/core/datastore/NiaPreferencesDataSource.kt
index 433130679..0f0931e6b 100644
--- a/core/datastore/src/main/java/com/google/samples/apps/nowinandroid/core/datastore/NiaPreferencesDataSource.kt
+++ b/core/datastore/src/main/java/com/google/samples/apps/nowinandroid/core/datastore/NiaPreferencesDataSource.kt
@@ -52,7 +52,8 @@ class NiaPreferencesDataSource @Inject constructor(
DarkThemeConfigProto.DARK_THEME_CONFIG_LIGHT ->
DarkThemeConfig.LIGHT
DarkThemeConfigProto.DARK_THEME_CONFIG_DARK -> DarkThemeConfig.DARK
- }
+ },
+ shouldHideOnboarding = it.shouldHideOnboarding
)
}
@@ -62,6 +63,7 @@ class NiaPreferencesDataSource @Inject constructor(
it.copy {
followedTopicIds.clear()
followedTopicIds.putAll(topicIds.associateWith { true })
+ updateShouldHideOnboardingIfNecessary()
}
}
} catch (ioException: IOException) {
@@ -78,6 +80,7 @@ class NiaPreferencesDataSource @Inject constructor(
} else {
followedTopicIds.remove(topicId)
}
+ updateShouldHideOnboardingIfNecessary()
}
}
} catch (ioException: IOException) {
@@ -91,6 +94,7 @@ class NiaPreferencesDataSource @Inject constructor(
it.copy {
followedAuthorIds.clear()
followedAuthorIds.putAll(authorIds.associateWith { true })
+ updateShouldHideOnboardingIfNecessary()
}
}
} catch (ioException: IOException) {
@@ -107,6 +111,7 @@ class NiaPreferencesDataSource @Inject constructor(
} else {
followedAuthorIds.remove(authorId)
}
+ updateShouldHideOnboardingIfNecessary()
}
}
} catch (ioException: IOException) {
@@ -188,4 +193,19 @@ class NiaPreferencesDataSource @Inject constructor(
Log.e("NiaPreferences", "Failed to update user preferences", ioException)
}
}
+
+ suspend fun setShouldHideOnboarding(shouldHideOnboarding: Boolean) {
+ userPreferences.updateData {
+ it.copy {
+ this.shouldHideOnboarding = shouldHideOnboarding
+ }
+ }
+ }
+}
+
+private fun UserPreferencesKt.Dsl.updateShouldHideOnboardingIfNecessary() {
+
+ if (followedTopicIds.isEmpty() && followedAuthorIds.isEmpty()) {
+ shouldHideOnboarding = false
+ }
}
diff --git a/core/datastore/src/main/proto/com/google/samples/apps/nowinandroid/data/user_preferences.proto b/core/datastore/src/main/proto/com/google/samples/apps/nowinandroid/data/user_preferences.proto
index f0eca3d32..b7d33dcaf 100644
--- a/core/datastore/src/main/proto/com/google/samples/apps/nowinandroid/data/user_preferences.proto
+++ b/core/datastore/src/main/proto/com/google/samples/apps/nowinandroid/data/user_preferences.proto
@@ -43,4 +43,6 @@ message UserPreferences {
ThemeBrandProto theme_brand = 16;
DarkThemeConfigProto dark_theme_config = 17;
+
+ bool should_hide_onboarding = 18;
}
diff --git a/core/datastore/src/test/java/com/google/samples/apps/nowinandroid/core/datastore/NiaPreferencesDataSourceTest.kt b/core/datastore/src/test/java/com/google/samples/apps/nowinandroid/core/datastore/NiaPreferencesDataSourceTest.kt
new file mode 100644
index 000000000..d5e4be841
--- /dev/null
+++ b/core/datastore/src/test/java/com/google/samples/apps/nowinandroid/core/datastore/NiaPreferencesDataSourceTest.kt
@@ -0,0 +1,137 @@
+/*
+ * 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.core.datastore
+
+import com.google.samples.apps.nowinandroid.core.datastore.test.testUserPreferencesDataStore
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.test.runTest
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.TemporaryFolder
+
+class NiaPreferencesDataSourceTest {
+ private lateinit var subject: NiaPreferencesDataSource
+
+ @get:Rule
+ val tmpFolder: TemporaryFolder = TemporaryFolder.builder().assureDeletion().build()
+
+ @Before
+ fun setup() {
+ subject = NiaPreferencesDataSource(
+ tmpFolder.testUserPreferencesDataStore()
+ )
+ }
+
+ @Test
+ fun shouldHideOnboardingIsFalseByDefault() = runTest {
+ assertEquals(false, subject.userDataStream.first().shouldHideOnboarding)
+ }
+
+ @Test
+ fun userShouldHideOnboardingIsTrueWhenSet() = runTest {
+ subject.setShouldHideOnboarding(true)
+ assertEquals(true, subject.userDataStream.first().shouldHideOnboarding)
+ }
+
+ @Test
+ fun userShouldHideOnboarding_unfollowsLastAuthor_shouldHideOnboardingIsFalse() = runTest {
+
+ // Given: user completes onboarding by selecting a single author.
+ subject.toggleFollowedAuthorId("1", true)
+ subject.setShouldHideOnboarding(true)
+
+ // When: they unfollow that author.
+ subject.toggleFollowedAuthorId("1", false)
+
+ // Then: onboarding should be shown again
+ assertEquals(false, subject.userDataStream.first().shouldHideOnboarding)
+ }
+
+ @Test
+ fun userShouldHideOnboarding_unfollowsLastTopic_shouldHideOnboardingIsFalse() = runTest {
+
+ // Given: user completes onboarding by selecting a single topic.
+ subject.toggleFollowedTopicId("1", true)
+ subject.setShouldHideOnboarding(true)
+
+ // When: they unfollow that topic.
+ subject.toggleFollowedTopicId("1", false)
+
+ // Then: onboarding should be shown again
+ assertEquals(false, subject.userDataStream.first().shouldHideOnboarding)
+ }
+
+ @Test
+ fun userShouldHideOnboarding_unfollowsAllAuthors_shouldHideOnboardingIsFalse() = runTest {
+
+ // Given: user completes onboarding by selecting several authors.
+ subject.setFollowedAuthorIds(setOf("1", "2"))
+ subject.setShouldHideOnboarding(true)
+
+ // When: they unfollow those authors.
+ subject.setFollowedAuthorIds(emptySet())
+
+ // Then: onboarding should be shown again
+ assertEquals(false, subject.userDataStream.first().shouldHideOnboarding)
+ }
+
+ @Test
+ fun userShouldHideOnboarding_unfollowsAllTopics_shouldHideOnboardingIsFalse() = runTest {
+
+ // Given: user completes onboarding by selecting several topics.
+ subject.setFollowedTopicIds(setOf("1", "2"))
+ subject.setShouldHideOnboarding(true)
+
+ // When: they unfollow those topics.
+ subject.setFollowedTopicIds(emptySet())
+
+ // Then: onboarding should be shown again
+ assertEquals(false, subject.userDataStream.first().shouldHideOnboarding)
+ }
+
+ @Test
+ fun userShouldHideOnboarding_unfollowsAllTopicsButNotAuthors_shouldHideOnboardingIsTrue() =
+ runTest {
+ // Given: user completes onboarding by selecting several topics and authors.
+ subject.setFollowedTopicIds(setOf("1", "2"))
+ subject.setFollowedAuthorIds(setOf("3", "4"))
+ subject.setShouldHideOnboarding(true)
+
+ // When: they unfollow just the topics.
+ subject.setFollowedTopicIds(emptySet())
+
+ // Then: onboarding should still be dismissed
+ assertEquals(true, subject.userDataStream.first().shouldHideOnboarding)
+ }
+
+ @Test
+ fun userShouldHideOnboarding_unfollowsAllAuthorsButNotTopics_shouldHideOnboardingIsTrue() =
+ runTest {
+ // Given: user completes onboarding by selecting several topics and authors.
+ subject.setFollowedTopicIds(setOf("1", "2"))
+ subject.setFollowedAuthorIds(setOf("3", "4"))
+ subject.setShouldHideOnboarding(true)
+
+ // When: they unfollow just the authors.
+ subject.setFollowedAuthorIds(emptySet())
+
+ // Then: onboarding should still be dismissed
+ assertEquals(true, subject.userDataStream.first().shouldHideOnboarding)
+ }
+}
diff --git a/core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/GetFollowableTopicsStreamUseCase.kt b/core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/GetFollowableTopicsStreamUseCase.kt
index 80dea1898..2f1fada9d 100644
--- a/core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/GetFollowableTopicsStreamUseCase.kt
+++ b/core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/GetFollowableTopicsStreamUseCase.kt
@@ -36,29 +36,18 @@ class GetFollowableTopicsStreamUseCase @Inject constructor(
/**
* Returns a list of topics with their associated followed state.
*
- * @param followedTopicIdsStream - the set of topic ids which are currently being followed. By
- * default the followed topic ids are supplied from the user data repository, but in certain
- * scenarios, such as when creating a temporary set of followed topics, you may wish to override
- * this parameter to supply your own list of topic ids. @see ForYouViewModel for an example of
- * this.
* @param sortBy - the field used to sort the topics. Default NONE = no sorting.
*/
- operator fun invoke(
- followedTopicIdsStream: Flow> =
- userDataRepository.userDataStream.map { userdata ->
- userdata.followedTopics
- },
- sortBy: TopicSortField = NONE
- ): Flow> {
+ operator fun invoke(sortBy: TopicSortField = NONE): Flow> {
return combine(
- followedTopicIdsStream,
+ userDataRepository.userDataStream,
topicsRepository.getTopicsStream()
- ) { followedIds, topics ->
+ ) { userData, topics ->
val followedTopics = topics
.map { topic ->
FollowableTopic(
topic = topic,
- isFollowed = topic.id in followedIds
+ isFollowed = topic.id in userData.followedTopics
)
}
when (sortBy) {
diff --git a/core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/GetPersistentSortedFollowableAuthorsStreamUseCase.kt b/core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/GetPersistentSortedFollowableAuthorsStreamUseCase.kt
deleted file mode 100644
index fda0b4728..000000000
--- a/core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/GetPersistentSortedFollowableAuthorsStreamUseCase.kt
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * 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.core.domain
-
-import com.google.samples.apps.nowinandroid.core.data.repository.AuthorsRepository
-import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository
-import com.google.samples.apps.nowinandroid.core.domain.model.FollowableAuthor
-import javax.inject.Inject
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.flatMapLatest
-import kotlinx.coroutines.flow.map
-
-/**
- * A use case which obtains a sorted list of authors with their followed state obtained from user
- * data.
- */
-class GetPersistentSortedFollowableAuthorsStreamUseCase @Inject constructor(
- authorsRepository: AuthorsRepository,
- private val userDataRepository: UserDataRepository
-) {
- private val getSortedFollowableAuthorsStream =
- GetSortedFollowableAuthorsStreamUseCase(authorsRepository)
-
- /**
- * Returns a list of authors with their associated followed state sorted alphabetically by name.
- */
- operator fun invoke(): Flow> {
- return userDataRepository.userDataStream.map { userdata ->
- userdata.followedAuthors
- }.flatMapLatest {
- getSortedFollowableAuthorsStream(it)
- }
- }
-}
diff --git a/core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/GetSortedFollowableAuthorsStreamUseCase.kt b/core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/GetSortedFollowableAuthorsStreamUseCase.kt
index e1709e4cd..498558c2f 100644
--- a/core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/GetSortedFollowableAuthorsStreamUseCase.kt
+++ b/core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/GetSortedFollowableAuthorsStreamUseCase.kt
@@ -17,33 +17,34 @@
package com.google.samples.apps.nowinandroid.core.domain
import com.google.samples.apps.nowinandroid.core.data.repository.AuthorsRepository
+import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository
import com.google.samples.apps.nowinandroid.core.domain.model.FollowableAuthor
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.combine
/**
* A use case which obtains a list of authors sorted alphabetically by name with their followed
* state.
*/
class GetSortedFollowableAuthorsStreamUseCase @Inject constructor(
- private val authorsRepository: AuthorsRepository
+ private val authorsRepository: AuthorsRepository,
+ private val userDataRepository: UserDataRepository
) {
/**
* Returns a list of authors with their associated followed state sorted alphabetically by name.
- *
- * @param followedTopicIds - the set of topic ids which are currently being followed.
*/
- operator fun invoke(followedAuthorIds: Set): Flow> {
- return authorsRepository.getAuthorsStream().map { authors ->
- authors
- .map { author ->
- FollowableAuthor(
- author = author,
- isFollowed = author.id in followedAuthorIds
- )
- }
+ operator fun invoke(): Flow> =
+ combine(
+ authorsRepository.getAuthorsStream(),
+ userDataRepository.userDataStream
+ ) { authors, userData ->
+ authors.map { author ->
+ FollowableAuthor(
+ author = author,
+ isFollowed = author.id in userData.followedAuthors
+ )
+ }
.sortedBy { it.author.name }
}
- }
}
diff --git a/core/domain/src/test/java/com/google/samples/apps/nowinandroid/core/domain/GetFollowableTopicsStreamUseCaseTest.kt b/core/domain/src/test/java/com/google/samples/apps/nowinandroid/core/domain/GetFollowableTopicsStreamUseCaseTest.kt
index c406de47b..101590605 100644
--- a/core/domain/src/test/java/com/google/samples/apps/nowinandroid/core/domain/GetFollowableTopicsStreamUseCaseTest.kt
+++ b/core/domain/src/test/java/com/google/samples/apps/nowinandroid/core/domain/GetFollowableTopicsStreamUseCaseTest.kt
@@ -16,7 +16,6 @@
package com.google.samples.apps.nowinandroid.core.domain
-import androidx.compose.runtime.snapshotFlow
import com.google.samples.apps.nowinandroid.core.domain.TopicSortField.NAME
import com.google.samples.apps.nowinandroid.core.domain.model.FollowableTopic
import com.google.samples.apps.nowinandroid.core.model.data.Topic
@@ -63,31 +62,6 @@ class GetFollowableTopicsStreamUseCaseTest {
)
}
- @Test
- fun whenFollowedTopicIdsSupplied_differentFollowedTopicsAreReturned() = runTest {
-
- // Obtain a stream of followable topics, specifying a list of topic ids which are currently
- // followed.
- val followableTopics = useCase(
- followedTopicIdsStream = snapshotFlow { setOf(testTopics[1].id) }
- )
-
- // Send some test topics and their followed state.
- topicsRepository.sendTopics(testTopics)
- userDataRepository.setFollowedTopicIds(setOf(testTopics[0].id))
-
- // Check that the topic ids supplied to the use case are used for the bookmark state, not
- // the topic ids in the user data repository.
- assertEquals(
- followableTopics.first(),
- listOf(
- FollowableTopic(testTopics[0], false),
- FollowableTopic(testTopics[1], true),
- FollowableTopic(testTopics[2], false),
- )
- )
- }
-
@Test
fun whenSortOrderIsByName_topicsSortedByNameAreReturned() = runTest {
diff --git a/core/domain/src/test/java/com/google/samples/apps/nowinandroid/core/domain/GetPersistentSortedFollowableAuthorsStreamUseCaseTest.kt b/core/domain/src/test/java/com/google/samples/apps/nowinandroid/core/domain/GetPersistentSortedFollowableAuthorsStreamUseCaseTest.kt
deleted file mode 100644
index 250a5e89d..000000000
--- a/core/domain/src/test/java/com/google/samples/apps/nowinandroid/core/domain/GetPersistentSortedFollowableAuthorsStreamUseCaseTest.kt
+++ /dev/null
@@ -1,95 +0,0 @@
-/*
- * 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.core.domain
-
-import com.google.samples.apps.nowinandroid.core.domain.model.FollowableAuthor
-import com.google.samples.apps.nowinandroid.core.model.data.Author
-import com.google.samples.apps.nowinandroid.core.testing.repository.TestAuthorsRepository
-import com.google.samples.apps.nowinandroid.core.testing.repository.TestUserDataRepository
-import com.google.samples.apps.nowinandroid.core.testing.util.MainDispatcherRule
-import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.test.runTest
-import org.junit.Assert.assertEquals
-import org.junit.Rule
-import org.junit.Test
-
-class GetPersistentSortedFollowableAuthorsStreamUseCaseTest {
-
- @get:Rule
- val mainDispatcherRule = MainDispatcherRule()
-
- private val authorsRepository = TestAuthorsRepository()
- private val userDataRepository = TestUserDataRepository()
-
- val useCase = GetPersistentSortedFollowableAuthorsStreamUseCase(
- authorsRepository = authorsRepository,
- userDataRepository = userDataRepository
- )
-
- @Test
- fun whenFollowedAuthorsSupplied_sortedFollowableAuthorsAreReturned() = runTest {
-
- // Obtain the stream of authors.
- val followableAuthorsStream = useCase()
-
- // Supply some authors and their followed state in user data.
- authorsRepository.sendAuthors(sampleAuthors)
- userDataRepository.setFollowedAuthorIds(setOf(sampleAuthor1.id, sampleAuthor3.id))
-
- // Check that the authors have been sorted, and that the followed state is correct.
- assertEquals(
- listOf(
- FollowableAuthor(sampleAuthor2, false),
- FollowableAuthor(sampleAuthor1, true),
- FollowableAuthor(sampleAuthor3, true)
- ),
- followableAuthorsStream.first()
- )
- }
-}
-
-private val sampleAuthor1 =
- Author(
- id = "Author1",
- name = "Mandy",
- imageUrl = "",
- twitter = "",
- mediumPage = "",
- bio = "",
- )
-
-private val sampleAuthor2 =
- Author(
- id = "Author2",
- name = "Andy",
- imageUrl = "",
- twitter = "",
- mediumPage = "",
- bio = "",
- )
-
-private val sampleAuthor3 =
- Author(
- id = "Author3",
- name = "Sandy",
- imageUrl = "",
- twitter = "",
- mediumPage = "",
- bio = "",
- )
-
-private val sampleAuthors = listOf(sampleAuthor1, sampleAuthor2, sampleAuthor3)
diff --git a/core/domain/src/test/java/com/google/samples/apps/nowinandroid/core/domain/GetSortedFollowableAuthorsStreamUseCaseTest.kt b/core/domain/src/test/java/com/google/samples/apps/nowinandroid/core/domain/GetSortedFollowableAuthorsStreamUseCaseTest.kt
index c1ae1e961..a08ca1b3a 100644
--- a/core/domain/src/test/java/com/google/samples/apps/nowinandroid/core/domain/GetSortedFollowableAuthorsStreamUseCaseTest.kt
+++ b/core/domain/src/test/java/com/google/samples/apps/nowinandroid/core/domain/GetSortedFollowableAuthorsStreamUseCaseTest.kt
@@ -19,6 +19,7 @@ package com.google.samples.apps.nowinandroid.core.domain
import com.google.samples.apps.nowinandroid.core.domain.model.FollowableAuthor
import com.google.samples.apps.nowinandroid.core.model.data.Author
import com.google.samples.apps.nowinandroid.core.testing.repository.TestAuthorsRepository
+import com.google.samples.apps.nowinandroid.core.testing.repository.TestUserDataRepository
import com.google.samples.apps.nowinandroid.core.testing.util.MainDispatcherRule
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
@@ -32,14 +33,21 @@ class GetSortedFollowableAuthorsStreamUseCaseTest {
val mainDispatcherRule = MainDispatcherRule()
private val authorsRepository = TestAuthorsRepository()
+ private val userDataRepository = TestUserDataRepository()
- val useCase = GetSortedFollowableAuthorsStreamUseCase(authorsRepository)
+ val useCase = GetSortedFollowableAuthorsStreamUseCase(
+ authorsRepository = authorsRepository,
+ userDataRepository = userDataRepository
+ )
@Test
fun whenFollowedAuthorsSupplied_sortedFollowableAuthorsAreReturned() = runTest {
+ // Specify some authors which the user is following.
+ userDataRepository.setFollowedAuthorIds(setOf(sampleAuthor1.id))
+
// Obtain the stream of authors, specifying their followed state.
- val followableAuthorsStream = useCase(followedAuthorIds = setOf(sampleAuthor1.id))
+ val followableAuthorsStream = useCase()
// Supply some authors.
authorsRepository.sendAuthors(sampleAuthors)
diff --git a/core/model/src/main/java/com/google/samples/apps/nowinandroid/core/model/data/UserData.kt b/core/model/src/main/java/com/google/samples/apps/nowinandroid/core/model/data/UserData.kt
index 13f1dd737..c3941cb0f 100644
--- a/core/model/src/main/java/com/google/samples/apps/nowinandroid/core/model/data/UserData.kt
+++ b/core/model/src/main/java/com/google/samples/apps/nowinandroid/core/model/data/UserData.kt
@@ -25,4 +25,5 @@ data class UserData(
val followedAuthors: Set,
val themeBrand: ThemeBrand,
val darkThemeConfig: DarkThemeConfig,
+ val shouldHideOnboarding: Boolean
)
diff --git a/core/testing/src/main/java/com/google/samples/apps/nowinandroid/core/testing/repository/TestUserDataRepository.kt b/core/testing/src/main/java/com/google/samples/apps/nowinandroid/core/testing/repository/TestUserDataRepository.kt
index 4dfd573a1..507fa1f7b 100644
--- a/core/testing/src/main/java/com/google/samples/apps/nowinandroid/core/testing/repository/TestUserDataRepository.kt
+++ b/core/testing/src/main/java/com/google/samples/apps/nowinandroid/core/testing/repository/TestUserDataRepository.kt
@@ -30,7 +30,8 @@ private val emptyUserData = UserData(
followedTopics = emptySet(),
followedAuthors = emptySet(),
themeBrand = ThemeBrand.DEFAULT,
- darkThemeConfig = DarkThemeConfig.FOLLOW_SYSTEM
+ darkThemeConfig = DarkThemeConfig.FOLLOW_SYSTEM,
+ shouldHideOnboarding = false
)
class TestUserDataRepository : UserDataRepository {
@@ -90,6 +91,12 @@ class TestUserDataRepository : UserDataRepository {
}
}
+ override suspend fun setShouldHideOnboarding(shouldHideOnboarding: Boolean) {
+ currentUserData.let { current ->
+ _userData.tryEmit(current.copy(shouldHideOnboarding = shouldHideOnboarding))
+ }
+ }
+
/**
* A test-only API to allow setting/unsetting of bookmarks.
*
diff --git a/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/JankStatsExtensions.kt b/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/JankStatsExtensions.kt
index 7a3500fbb..d4ca9180d 100644
--- a/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/JankStatsExtensions.kt
+++ b/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/JankStatsExtensions.kt
@@ -30,9 +30,9 @@ import androidx.metrics.performance.PerformanceMetricsState.Holder
import kotlinx.coroutines.CoroutineScope
/**
- * Retrieves [PerformanceMetricsState.MetricsStateHolder] from current [LocalView] and
+ * Retrieves [PerformanceMetricsState.Holder] from current [LocalView] and
* remembers it until the View changes.
- * @see PerformanceMetricsState.getForHierarchy
+ * @see PerformanceMetricsState.getHolderForHierarchy
*/
@Composable
fun rememberMetricsStateHolder(): Holder {
diff --git a/feature/author/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/author/AuthorRobot.kt b/feature/author/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/author/AuthorRobot.kt
new file mode 100644
index 000000000..98cc43a5e
--- /dev/null
+++ b/feature/author/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/author/AuthorRobot.kt
@@ -0,0 +1,66 @@
+/*
+ * 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.feature.author
+
+import androidx.activity.ComponentActivity
+import androidx.compose.ui.test.junit4.AndroidComposeTestRule
+import androidx.compose.ui.test.onNodeWithContentDescription
+import androidx.compose.ui.test.onNodeWithText
+import androidx.test.ext.junit.rules.ActivityScenarioRule
+import com.google.samples.apps.nowinandroid.core.domain.model.FollowableAuthor
+import com.google.samples.apps.nowinandroid.core.model.data.NewsResource
+
+internal class AuthorRobot(
+ private val composeTestRule: AndroidComposeTestRule<
+ ActivityScenarioRule, ComponentActivity>
+) {
+ fun setContent(authorUiState: AuthorUiState, newsUiState: NewsUiState) {
+ composeTestRule.setContent {
+ AuthorScreen(
+ authorUiState = authorUiState,
+ newsUiState = newsUiState,
+ onBackClick = { },
+ onFollowClick = { },
+ onBookmarkChanged = { _, _ -> },
+ )
+ }
+ }
+
+ fun loadingIndicatorExists() {
+ val authorLoading = composeTestRule.activity.getString(R.string.author_loading)
+
+ composeTestRule
+ .onNodeWithContentDescription(authorLoading)
+ .assertExists()
+ }
+
+ fun authorExists(author: FollowableAuthor) {
+ composeTestRule
+ .onNodeWithText(author.author.name)
+ .assertExists()
+
+ composeTestRule
+ .onNodeWithText(author.author.bio)
+ .assertExists()
+ }
+
+ fun newsResourceExists(newsResource: NewsResource) {
+ composeTestRule
+ .onNodeWithText(newsResource.title)
+ .assertExists()
+ }
+}
diff --git a/feature/author/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/author/AuthorScreenTest.kt b/feature/author/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/author/AuthorScreenTest.kt
index b955912f2..74968f764 100644
--- a/feature/author/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/author/AuthorScreenTest.kt
+++ b/feature/author/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/author/AuthorScreenTest.kt
@@ -17,16 +17,15 @@
package com.google.samples.apps.nowinandroid.feature.author
import androidx.activity.ComponentActivity
+import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.compose.ui.test.junit4.createAndroidComposeRule
-import androidx.compose.ui.test.onNodeWithContentDescription
-import androidx.compose.ui.test.onNodeWithText
+import androidx.test.ext.junit.rules.ActivityScenarioRule
import com.google.samples.apps.nowinandroid.core.domain.model.FollowableAuthor
import com.google.samples.apps.nowinandroid.core.domain.model.SaveableNewsResource
import com.google.samples.apps.nowinandroid.core.model.data.Author
import com.google.samples.apps.nowinandroid.core.model.data.NewsResource
import com.google.samples.apps.nowinandroid.core.model.data.NewsResourceType.Video
import kotlinx.datetime.Instant
-import org.junit.Before
import org.junit.Rule
import org.junit.Test
@@ -40,108 +39,78 @@ class AuthorScreenTest {
@get:Rule
val composeTestRule = createAndroidComposeRule()
- private lateinit var authorLoading: String
-
- @Before
- fun setup() {
- composeTestRule.activity.apply {
- authorLoading = getString(R.string.author_loading)
- }
- }
-
@Test
fun niaLoadingWheel_whenScreenIsLoading_showLoading() {
- composeTestRule.setContent {
- AuthorScreen(
- authorUiState = AuthorUiState.Loading,
- newsUiState = NewsUiState.Loading,
- onBackClick = { },
- onFollowClick = { },
- onBookmarkChanged = { _, _ -> },
- )
+ launchAuthorRobot(
+ composeTestRule, AuthorUiState.Loading, NewsUiState.Loading,
+ ) {
+ loadingIndicatorExists()
}
-
- composeTestRule
- .onNodeWithContentDescription(authorLoading)
- .assertExists()
}
@Test
fun authorTitle_whenAuthorIsSuccess_isShown() {
val testAuthor = testAuthors.first()
- composeTestRule.setContent {
- AuthorScreen(
- authorUiState = AuthorUiState.Success(testAuthor),
- newsUiState = NewsUiState.Loading,
- onBackClick = { },
- onFollowClick = { },
- onBookmarkChanged = { _, _ -> },
- )
+ launchAuthorRobot(
+ composeTestRule,
+ AuthorUiState.Success(testAuthor),
+ NewsUiState.Loading
+ ) {
+ authorExists(testAuthor)
}
-
- // Name is shown
- composeTestRule
- .onNodeWithText(testAuthor.author.name)
- .assertExists()
-
- // Bio is shown
- composeTestRule
- .onNodeWithText(testAuthor.author.bio)
- .assertExists()
}
@Test
fun news_whenAuthorIsLoading_isNotShown() {
- composeTestRule.setContent {
- AuthorScreen(
- authorUiState = AuthorUiState.Loading,
- newsUiState = NewsUiState.Success(
- sampleNewsResources.mapIndexed { index, newsResource ->
- SaveableNewsResource(
- newsResource = newsResource,
- isSaved = index % 2 == 0,
- )
- }
- ),
- onBackClick = { },
- onFollowClick = { },
- onBookmarkChanged = { _, _ -> },
- )
+ val newsUiState = NewsUiState.Success(
+ sampleNewsResources.mapIndexed { index, newsResource ->
+ SaveableNewsResource(
+ newsResource = newsResource,
+ isSaved = index % 2 == 0,
+ )
+ }
+ )
+ launchAuthorRobot(
+ composeTestRule,
+ AuthorUiState.Loading,
+ newsUiState
+ ) {
+ loadingIndicatorExists()
}
-
- // Loading indicator shown
- composeTestRule
- .onNodeWithContentDescription(authorLoading)
- .assertExists()
}
@Test
fun news_whenSuccessAndAuthorIsSuccess_isShown() {
val testAuthor = testAuthors.first()
- composeTestRule.setContent {
- AuthorScreen(
- authorUiState = AuthorUiState.Success(testAuthor),
- newsUiState = NewsUiState.Success(
- sampleNewsResources.mapIndexed { index, newsResource ->
- SaveableNewsResource(
- newsResource = newsResource,
- isSaved = index % 2 == 0,
- )
- }
- ),
- onBackClick = { },
- onFollowClick = { },
- onBookmarkChanged = { _, _ -> },
- )
+ val newsUiState = NewsUiState.Success(
+ sampleNewsResources.mapIndexed { index, newsResource ->
+ SaveableNewsResource(
+ newsResource = newsResource,
+ isSaved = index % 2 == 0,
+ )
+ }
+ )
+ launchAuthorRobot(
+ composeTestRule,
+ AuthorUiState.Success(testAuthor),
+ newsUiState
+ ) {
+ newsResourceExists(sampleNewsResources.first())
}
-
- // First news title shown
- composeTestRule
- .onNodeWithText(sampleNewsResources.first().title)
- .assertExists()
}
}
+private fun launchAuthorRobot(
+ composeTestRule: AndroidComposeTestRule<
+ ActivityScenarioRule, ComponentActivity>,
+ authorUiState: AuthorUiState,
+ newsUiState: NewsUiState,
+ func: AuthorRobot.() -> Unit
+) = AuthorRobot(composeTestRule).apply {
+ setContent(authorUiState, newsUiState)
+ func()
+}
+
private const val AUTHOR_1_NAME = "Author 1"
private const val AUTHOR_2_NAME = "Author 2"
private const val AUTHOR_3_NAME = "Author 3"
diff --git a/feature/bookmarks/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksRobot.kt b/feature/bookmarks/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksRobot.kt
new file mode 100644
index 000000000..675becca5
--- /dev/null
+++ b/feature/bookmarks/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksRobot.kt
@@ -0,0 +1,87 @@
+/*
+ * 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.feature.bookmarks
+
+import androidx.activity.ComponentActivity
+import androidx.compose.ui.test.assertCountEquals
+import androidx.compose.ui.test.assertHasClickAction
+import androidx.compose.ui.test.filter
+import androidx.compose.ui.test.hasAnyAncestor
+import androidx.compose.ui.test.hasScrollToNodeAction
+import androidx.compose.ui.test.hasText
+import androidx.compose.ui.test.junit4.AndroidComposeTestRule
+import androidx.compose.ui.test.onAllNodesWithContentDescription
+import androidx.compose.ui.test.onFirst
+import androidx.compose.ui.test.onNodeWithContentDescription
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performClick
+import androidx.compose.ui.test.performScrollToNode
+import androidx.test.ext.junit.rules.ActivityScenarioRule
+import com.google.samples.apps.nowinandroid.core.model.data.NewsResource
+import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState
+
+internal class BookmarksRobot(
+ private val composeTestRule: AndroidComposeTestRule<
+ ActivityScenarioRule, ComponentActivity>
+) {
+ private val removedBookmarks = mutableSetOf()
+
+ fun setContent(newsFeedUiState: NewsFeedUiState) {
+ composeTestRule.setContent {
+ BookmarksScreen(feedState = newsFeedUiState, removeFromBookmarks = {
+ removedBookmarks.add(it)
+ })
+ }
+ }
+
+ fun loadingIndicatorShown() {
+ composeTestRule.onNodeWithContentDescription(
+ composeTestRule.activity.resources.getString(R.string.saved_loading)
+ ).assertExists()
+ }
+
+ fun clickableNewsResourceExists(newsResource: NewsResource) {
+ composeTestRule.onNodeWithText(
+ newsResource.title, substring = true
+ ).assertExists().assertHasClickAction()
+ }
+
+ fun scrollToNewsResource(newsResource: NewsResource) {
+ composeTestRule.onNode(hasScrollToNodeAction()).performScrollToNode(
+ hasText(
+ newsResource.title, substring = true
+ )
+ )
+ }
+
+ fun clickNewsResourceBookmark(newsResource: NewsResource) {
+ composeTestRule.onAllNodesWithContentDescription(
+ composeTestRule.activity.getString(
+ com.google.samples.apps.nowinandroid.core.ui.R.string.unbookmark
+ )
+ ).filter(
+ hasAnyAncestor(
+ hasText(
+ newsResource.title, substring = true
+ )
+ )
+ ).assertCountEquals(1).onFirst().performClick()
+ }
+
+ fun removedNewsResourceBookmark(newsResource: NewsResource) =
+ removedBookmarks.contains(newsResource.id)
+}
diff --git a/feature/bookmarks/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreenTest.kt b/feature/bookmarks/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreenTest.kt
index f018be8c0..70314c87e 100644
--- a/feature/bookmarks/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreenTest.kt
+++ b/feature/bookmarks/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreenTest.kt
@@ -17,25 +17,12 @@
package com.google.samples.apps.nowinandroid.feature.bookmarks
import androidx.activity.ComponentActivity
-import androidx.compose.material3.windowsizeclass.WindowSizeClass
-import androidx.compose.ui.test.assertCountEquals
-import androidx.compose.ui.test.assertHasClickAction
-import androidx.compose.ui.test.filter
-import androidx.compose.ui.test.hasAnyAncestor
-import androidx.compose.ui.test.hasScrollToNodeAction
-import androidx.compose.ui.test.hasText
+import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.compose.ui.test.junit4.createAndroidComposeRule
-import androidx.compose.ui.test.onAllNodesWithContentDescription
-import androidx.compose.ui.test.onFirst
-import androidx.compose.ui.test.onNodeWithContentDescription
-import androidx.compose.ui.test.onNodeWithText
-import androidx.compose.ui.test.performClick
-import androidx.compose.ui.test.performScrollToNode
+import androidx.test.ext.junit.rules.ActivityScenarioRule
import com.google.samples.apps.nowinandroid.core.domain.model.SaveableNewsResource
import com.google.samples.apps.nowinandroid.core.model.data.previewNewsResources
import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState
-import org.junit.Assert.assertEquals
-import org.junit.Assert.assertTrue
import org.junit.Rule
import org.junit.Test
@@ -49,93 +36,50 @@ class BookmarksScreenTest {
@Test
fun loading_showsLoadingSpinner() {
- composeTestRule.setContent {
- BookmarksScreen(
- feedState = NewsFeedUiState.Loading,
- removeFromBookmarks = { }
- )
+ launchBookmarksRobot(
+ composeTestRule,
+ NewsFeedUiState.Loading
+ ) {
+ loadingIndicatorShown()
}
-
- composeTestRule
- .onNodeWithContentDescription(
- composeTestRule.activity.resources.getString(R.string.saved_loading)
- )
- .assertExists()
}
@Test
fun feed_whenHasBookmarks_showsBookmarks() {
- lateinit var windowSizeClass: WindowSizeClass
-
- composeTestRule.setContent {
- BookmarksScreen(
- feedState = NewsFeedUiState.Success(
- previewNewsResources.take(2)
- .map { SaveableNewsResource(it, true) }
- ),
- removeFromBookmarks = { }
+ launchBookmarksRobot(
+ composeTestRule,
+ NewsFeedUiState.Success(
+ previewNewsResources.take(2)
+ .map { SaveableNewsResource(it, true) }
)
+ ) {
+ clickNewsResourceBookmark(previewNewsResources[0])
+ scrollToNewsResource(previewNewsResources[1])
+ clickableNewsResourceExists(previewNewsResources[1])
}
-
- composeTestRule
- .onNodeWithText(
- previewNewsResources[0].title,
- substring = true
- )
- .assertExists()
- .assertHasClickAction()
-
- composeTestRule.onNode(hasScrollToNodeAction())
- .performScrollToNode(
- hasText(
- previewNewsResources[1].title,
- substring = true
- )
- )
-
- composeTestRule
- .onNodeWithText(
- previewNewsResources[1].title,
- substring = true
- )
- .assertExists()
- .assertHasClickAction()
}
@Test
fun feed_whenRemovingBookmark_removesBookmark() {
- var removeFromBookmarksCalled = false
-
- composeTestRule.setContent {
- BookmarksScreen(
- feedState = NewsFeedUiState.Success(
- previewNewsResources.take(2)
- .map { SaveableNewsResource(it, true) }
- ),
- removeFromBookmarks = { newsResourceId ->
- assertEquals(previewNewsResources[0].id, newsResourceId)
- removeFromBookmarksCalled = true
- }
+ launchBookmarksRobot(
+ composeTestRule,
+ NewsFeedUiState.Success(
+ previewNewsResources.take(2)
+ .map { SaveableNewsResource(it, true) }
)
+ ) {
+ clickNewsResourceBookmark(previewNewsResources[0])
+ removedNewsResourceBookmark(previewNewsResources[0])
}
-
- composeTestRule
- .onAllNodesWithContentDescription(
- composeTestRule.activity.getString(
- com.google.samples.apps.nowinandroid.core.ui.R.string.unbookmark
- )
- ).filter(
- hasAnyAncestor(
- hasText(
- previewNewsResources[0].title,
- substring = true
- )
- )
- )
- .assertCountEquals(1)
- .onFirst()
- .performClick()
-
- assertTrue(removeFromBookmarksCalled)
}
}
+
+private fun launchBookmarksRobot(
+ composeTestRule: AndroidComposeTestRule<
+ ActivityScenarioRule, ComponentActivity>,
+ newsFeedUiState: NewsFeedUiState,
+ func: BookmarksRobot.() -> Unit
+) = BookmarksRobot(composeTestRule).apply {
+ setContent(newsFeedUiState)
+ func()
+}
diff --git a/feature/foryou/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouRobot.kt b/feature/foryou/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouRobot.kt
new file mode 100644
index 000000000..79a1888b2
--- /dev/null
+++ b/feature/foryou/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouRobot.kt
@@ -0,0 +1,124 @@
+/*
+ * 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.feature.foryou
+
+import androidx.activity.ComponentActivity
+import androidx.compose.foundation.layout.BoxWithConstraints
+import androidx.compose.ui.test.assertHasClickAction
+import androidx.compose.ui.test.assertIsEnabled
+import androidx.compose.ui.test.assertIsNotEnabled
+import androidx.compose.ui.test.hasScrollToNodeAction
+import androidx.compose.ui.test.hasText
+import androidx.compose.ui.test.junit4.AndroidComposeTestRule
+import androidx.compose.ui.test.onFirst
+import androidx.compose.ui.test.onNodeWithContentDescription
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performScrollToNode
+import androidx.test.ext.junit.rules.ActivityScenarioRule
+import com.google.samples.apps.nowinandroid.core.domain.model.FollowableAuthor
+import com.google.samples.apps.nowinandroid.core.domain.model.FollowableTopic
+import com.google.samples.apps.nowinandroid.core.model.data.NewsResource
+import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState
+import com.google.samples.apps.nowinandroid.feature.foryou.R.string
+
+internal class ForYouRobot(
+ private val composeTestRule: AndroidComposeTestRule<
+ ActivityScenarioRule, ComponentActivity>
+) {
+ private val doneButtonMatcher by lazy {
+ hasText(composeTestRule.activity.resources.getString(string.done))
+ }
+
+ fun setContent(
+ isSyncing: Boolean,
+ onboardingState: OnboardingUiState,
+ feedState: NewsFeedUiState,
+ ) {
+ composeTestRule.setContent {
+ BoxWithConstraints {
+ ForYouScreen(
+ isSyncing = isSyncing,
+ onboardingUiState = onboardingState,
+ feedState = feedState,
+ onTopicCheckedChanged = { _, _ -> },
+ onAuthorCheckedChanged = { _, _ -> },
+ saveFollowedTopics = {},
+ onNewsResourcesCheckedChanged = { _, _ -> }
+ )
+ }
+ }
+ }
+
+ fun loadingIndicatorExists() {
+ composeTestRule
+ .onNodeWithContentDescription(
+ composeTestRule.activity.resources.getString(string.for_you_loading)
+ )
+ .assertExists()
+ }
+
+ fun clickableAuthorExists(author: FollowableAuthor) {
+ composeTestRule
+ .onNodeWithText(author.author.name)
+ .assertExists()
+ .assertHasClickAction()
+ }
+
+ fun clickableTopicExists(topic: FollowableTopic) {
+ composeTestRule
+ .onNodeWithText(topic.topic.name)
+ .assertExists()
+ .assertHasClickAction()
+ }
+
+ fun scrollToDoneButton() {
+ composeTestRule
+ .onAllNodes(hasScrollToNodeAction())
+ .onFirst()
+ .performScrollToNode(doneButtonMatcher)
+ }
+
+ fun clickableDoneButtonExists(isEnabled: Boolean) {
+ composeTestRule
+ .onNode(doneButtonMatcher)
+ .assertExists()
+ .assertHasClickAction()
+ .apply {
+ if (isEnabled) assertIsEnabled() else assertIsNotEnabled()
+ }
+ }
+
+ fun clickableNewsResourceExists(newsResource: NewsResource) {
+ composeTestRule
+ .onNodeWithText(
+ newsResource.title,
+ substring = true
+ )
+ .assertExists()
+ .assertHasClickAction()
+ }
+
+ fun scrollToNewsResource(newsResource: NewsResource) {
+ composeTestRule.onNode(hasScrollToNodeAction())
+ .performScrollToNode(
+ hasText(
+ newsResource.title,
+ substring = true
+ )
+ )
+ }
+}
diff --git a/feature/foryou/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreenTest.kt b/feature/foryou/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreenTest.kt
index af5f07372..e5eef11a3 100644
--- a/feature/foryou/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreenTest.kt
+++ b/feature/foryou/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreenTest.kt
@@ -17,17 +17,9 @@
package com.google.samples.apps.nowinandroid.feature.foryou
import androidx.activity.ComponentActivity
-import androidx.compose.foundation.layout.BoxWithConstraints
-import androidx.compose.ui.test.assertHasClickAction
-import androidx.compose.ui.test.assertIsEnabled
-import androidx.compose.ui.test.assertIsNotEnabled
-import androidx.compose.ui.test.hasScrollToNodeAction
-import androidx.compose.ui.test.hasText
+import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.compose.ui.test.junit4.createAndroidComposeRule
-import androidx.compose.ui.test.onFirst
-import androidx.compose.ui.test.onNodeWithContentDescription
-import androidx.compose.ui.test.onNodeWithText
-import androidx.compose.ui.test.performScrollToNode
+import androidx.test.ext.junit.rules.ActivityScenarioRule
import com.google.samples.apps.nowinandroid.core.domain.model.FollowableAuthor
import com.google.samples.apps.nowinandroid.core.domain.model.FollowableTopic
import com.google.samples.apps.nowinandroid.core.domain.model.SaveableNewsResource
@@ -42,305 +34,168 @@ class ForYouScreenTest {
@get:Rule
val composeTestRule = createAndroidComposeRule()
- private val doneButtonMatcher by lazy {
- hasText(
- composeTestRule.activity.resources.getString(R.string.done)
- )
- }
-
@Test
fun circularProgressIndicator_whenScreenIsLoading_exists() {
- composeTestRule.setContent {
- BoxWithConstraints {
- ForYouScreen(
- isSyncing = false,
- interestsSelectionState = ForYouInterestsSelectionUiState.Loading,
- feedState = NewsFeedUiState.Loading,
- onTopicCheckedChanged = { _, _ -> },
- onAuthorCheckedChanged = { _, _ -> },
- saveFollowedTopics = {},
- onNewsResourcesCheckedChanged = { _, _ -> }
- )
- }
+ launchForYouRobot(
+ composeTestRule = composeTestRule,
+ isSyncing = false,
+ onboardingState = OnboardingUiState.Loading,
+ feedState = NewsFeedUiState.Loading,
+ ) {
+ loadingIndicatorExists()
}
-
- composeTestRule
- .onNodeWithContentDescription(
- composeTestRule.activity.resources.getString(R.string.for_you_loading)
- )
- .assertExists()
}
@Test
fun circularProgressIndicator_whenScreenIsSyncing_exists() {
- composeTestRule.setContent {
- BoxWithConstraints {
- ForYouScreen(
- isSyncing = true,
- interestsSelectionState = ForYouInterestsSelectionUiState.NoInterestsSelection,
- feedState = NewsFeedUiState.Success(emptyList()),
- onTopicCheckedChanged = { _, _ -> },
- onAuthorCheckedChanged = { _, _ -> },
- saveFollowedTopics = {},
- onNewsResourcesCheckedChanged = { _, _ -> }
- )
- }
+ launchForYouRobot(
+ composeTestRule = composeTestRule,
+ isSyncing = true,
+ onboardingState = OnboardingUiState.NotShown,
+ feedState = NewsFeedUiState.Success(emptyList()),
+ ) {
+ loadingIndicatorExists()
}
-
- composeTestRule
- .onNodeWithContentDescription(
- composeTestRule.activity.resources.getString(R.string.for_you_loading)
- )
- .assertExists()
}
@Test
fun topicSelector_whenNoTopicsSelected_showsAuthorAndTopicChipsAndDisabledDoneButton() {
- composeTestRule.setContent {
- BoxWithConstraints {
- ForYouScreen(
- isSyncing = false,
- interestsSelectionState =
- ForYouInterestsSelectionUiState.WithInterestsSelection(
- topics = testTopics,
- authors = testAuthors
- ),
- feedState = NewsFeedUiState.Success(
- feed = emptyList()
- ),
- onTopicCheckedChanged = { _, _ -> },
- onAuthorCheckedChanged = { _, _ -> },
- saveFollowedTopics = {},
- onNewsResourcesCheckedChanged = { _, _ -> }
- )
+ launchForYouRobot(
+ composeTestRule = composeTestRule,
+ isSyncing = false,
+ onboardingState = OnboardingUiState.Shown(
+ topics = testTopics,
+ authors = testAuthors
+ ),
+ feedState = NewsFeedUiState.Success(emptyList()),
+ ) {
+ testAuthors.forEach { author ->
+ clickableAuthorExists(author)
}
- }
- testAuthors.forEach { testAuthor ->
- composeTestRule
- .onNodeWithText(testAuthor.author.name)
- .assertExists()
- .assertHasClickAction()
- }
+ testTopics.forEach { topic ->
+ clickableTopicExists(topic)
+ }
- testTopics.forEach { testTopic ->
- composeTestRule
- .onNodeWithText(testTopic.topic.name)
- .assertExists()
- .assertHasClickAction()
+ scrollToDoneButton()
+ clickableDoneButtonExists(false)
}
-
- // Scroll until the Done button is visible
- composeTestRule
- .onAllNodes(hasScrollToNodeAction())
- .onFirst()
- .performScrollToNode(doneButtonMatcher)
-
- composeTestRule
- .onNode(doneButtonMatcher)
- .assertExists()
- .assertIsNotEnabled()
- .assertHasClickAction()
}
@Test
fun topicSelector_whenSomeTopicsSelected_showsAuthorAndTopicChipsAndEnabledDoneButton() {
- composeTestRule.setContent {
- BoxWithConstraints {
- ForYouScreen(
- isSyncing = false,
- interestsSelectionState =
- ForYouInterestsSelectionUiState.WithInterestsSelection(
- // Follow one topic
- topics = testTopics.mapIndexed { index, testTopic ->
- testTopic.copy(isFollowed = index == 1)
- },
- authors = testAuthors
- ),
- feedState = NewsFeedUiState.Success(
- feed = emptyList()
- ),
- onTopicCheckedChanged = { _, _ -> },
- onAuthorCheckedChanged = { _, _ -> },
- saveFollowedTopics = {},
- onNewsResourcesCheckedChanged = { _, _ -> }
- )
+ launchForYouRobot(
+ composeTestRule = composeTestRule,
+ isSyncing = false,
+ onboardingState =
+ OnboardingUiState.Shown(
+ // Follow one topic
+ topics = testTopics.mapIndexed { index, testTopic ->
+ testTopic.copy(isFollowed = index == 1)
+ },
+ authors = testAuthors
+ ),
+ feedState = NewsFeedUiState.Success(emptyList()),
+ ) {
+ testAuthors.forEach { author ->
+ clickableAuthorExists(author)
}
- }
- testAuthors.forEach { testAuthor ->
- composeTestRule
- .onNodeWithText(testAuthor.author.name)
- .assertExists()
- .assertHasClickAction()
- }
+ testTopics.forEach { topic ->
+ clickableTopicExists(topic)
+ }
- testTopics.forEach { testTopic ->
- composeTestRule
- .onNodeWithText(testTopic.topic.name)
- .assertExists()
- .assertHasClickAction()
+ scrollToDoneButton()
+ clickableDoneButtonExists(true)
}
-
- // Scroll until the Done button is visible
- composeTestRule
- .onAllNodes(hasScrollToNodeAction())
- .onFirst()
- .performScrollToNode(doneButtonMatcher)
-
- composeTestRule
- .onNode(doneButtonMatcher)
- .assertExists()
- .assertIsEnabled()
- .assertHasClickAction()
}
@Test
fun topicSelector_whenSomeAuthorsSelected_showsAuthorAndTopicChipsAndEnabledDoneButton() {
- composeTestRule.setContent {
- BoxWithConstraints {
- ForYouScreen(
- isSyncing = false,
- interestsSelectionState =
- ForYouInterestsSelectionUiState.WithInterestsSelection(
- // Follow one topic
- topics = testTopics,
- authors = testAuthors.mapIndexed { index, testAuthor ->
- testAuthor.copy(isFollowed = index == 1)
- }
- ),
- feedState = NewsFeedUiState.Success(
- feed = emptyList()
- ),
- onTopicCheckedChanged = { _, _ -> },
- onAuthorCheckedChanged = { _, _ -> },
- saveFollowedTopics = {},
- onNewsResourcesCheckedChanged = { _, _ -> }
- )
+ launchForYouRobot(
+ composeTestRule = composeTestRule,
+ isSyncing = false,
+ onboardingState = OnboardingUiState.Shown(
+ // Follow one topic
+ topics = testTopics,
+ authors = testAuthors.mapIndexed { index, testAuthor ->
+ testAuthor.copy(isFollowed = index == 1)
+ }
+ ),
+ feedState = NewsFeedUiState.Success(emptyList()),
+ ) {
+ testAuthors.forEach { author ->
+ clickableAuthorExists(author)
}
- }
- testAuthors.forEach { testAuthor ->
- composeTestRule
- .onNodeWithText(testAuthor.author.name)
- .assertExists()
- .assertHasClickAction()
- }
+ testTopics.forEach { topic ->
+ clickableTopicExists(topic)
+ }
- testTopics.forEach { testTopic ->
- composeTestRule
- .onNodeWithText(testTopic.topic.name)
- .assertExists()
- .assertHasClickAction()
+ scrollToDoneButton()
+ clickableDoneButtonExists(true)
}
-
- // Scroll until the Done button is visible
- composeTestRule
- .onAllNodes(hasScrollToNodeAction())
- .onFirst()
- .performScrollToNode(doneButtonMatcher)
-
- composeTestRule
- .onNode(doneButtonMatcher)
- .assertExists()
- .assertIsEnabled()
- .assertHasClickAction()
}
@Test
fun feed_whenInterestsSelectedAndLoading_showsLoadingIndicator() {
- composeTestRule.setContent {
- BoxWithConstraints {
- ForYouScreen(
- isSyncing = false,
- interestsSelectionState =
- ForYouInterestsSelectionUiState.WithInterestsSelection(
- topics = testTopics,
- authors = testAuthors
- ),
- feedState = NewsFeedUiState.Loading,
- onTopicCheckedChanged = { _, _ -> },
- onAuthorCheckedChanged = { _, _ -> },
- saveFollowedTopics = {},
- onNewsResourcesCheckedChanged = { _, _ -> }
- )
- }
+ launchForYouRobot(
+ composeTestRule = composeTestRule,
+ isSyncing = false,
+ onboardingState =
+ OnboardingUiState.Shown(
+ topics = testTopics,
+ authors = testAuthors
+ ),
+ feedState = NewsFeedUiState.Loading,
+ ) {
+ loadingIndicatorExists()
}
-
- composeTestRule
- .onNodeWithContentDescription(
- composeTestRule.activity.resources.getString(R.string.for_you_loading)
- )
- .assertExists()
}
@Test
fun feed_whenNoInterestsSelectionAndLoading_showsLoadingIndicator() {
- composeTestRule.setContent {
- BoxWithConstraints {
- ForYouScreen(
- isSyncing = false,
- interestsSelectionState = ForYouInterestsSelectionUiState.NoInterestsSelection,
- feedState = NewsFeedUiState.Loading,
- onTopicCheckedChanged = { _, _ -> },
- onAuthorCheckedChanged = { _, _ -> },
- saveFollowedTopics = {},
- onNewsResourcesCheckedChanged = { _, _ -> }
- )
- }
+ launchForYouRobot(
+ composeTestRule = composeTestRule,
+ isSyncing = false,
+ onboardingState = OnboardingUiState.NotShown,
+ feedState = NewsFeedUiState.Loading,
+ ) {
+ loadingIndicatorExists()
}
-
- composeTestRule
- .onNodeWithContentDescription(
- composeTestRule.activity.resources.getString(R.string.for_you_loading)
- )
- .assertExists()
}
@Test
fun feed_whenNoInterestsSelectionAndLoaded_showsFeed() {
- composeTestRule.setContent {
- ForYouScreen(
- isSyncing = false,
- interestsSelectionState = ForYouInterestsSelectionUiState.NoInterestsSelection,
- feedState = NewsFeedUiState.Success(
- feed = previewNewsResources.map {
- SaveableNewsResource(it, false)
- }
- ),
- onTopicCheckedChanged = { _, _ -> },
- onAuthorCheckedChanged = { _, _ -> },
- saveFollowedTopics = {},
- onNewsResourcesCheckedChanged = { _, _ -> }
- )
+ launchForYouRobot(
+ composeTestRule = composeTestRule,
+ isSyncing = false,
+ onboardingState = OnboardingUiState.NotShown,
+ feedState = NewsFeedUiState.Success(
+ feed = previewNewsResources.map {
+ SaveableNewsResource(it, false)
+ }
+ ),
+ ) {
+ clickableNewsResourceExists(previewNewsResources[0])
+ scrollToNewsResource(previewNewsResources[1])
+ clickableNewsResourceExists(previewNewsResources[1])
}
-
- composeTestRule
- .onNodeWithText(
- previewNewsResources[0].title,
- substring = true
- )
- .assertExists()
- .assertHasClickAction()
-
- composeTestRule.onNode(hasScrollToNodeAction())
- .performScrollToNode(
- hasText(
- previewNewsResources[1].title,
- substring = true
- )
- )
-
- composeTestRule
- .onNodeWithText(
- previewNewsResources[1].title,
- substring = true
- )
- .assertExists()
- .assertHasClickAction()
}
}
+private fun launchForYouRobot(
+ composeTestRule: AndroidComposeTestRule<
+ ActivityScenarioRule, ComponentActivity>,
+ isSyncing: Boolean,
+ onboardingState: OnboardingUiState,
+ feedState: NewsFeedUiState,
+ func: ForYouRobot.() -> Unit
+) = ForYouRobot(composeTestRule).apply {
+ setContent(isSyncing, onboardingState, feedState)
+ func()
+}
+
private val testTopic = Topic(
id = "",
name = "",
diff --git a/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/AuthorsCarousel.kt b/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/AuthorsCarousel.kt
index d6d686d4b..0a7955c2f 100644
--- a/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/AuthorsCarousel.kt
+++ b/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/AuthorsCarousel.kt
@@ -43,10 +43,12 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
+import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.onClick
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.stateDescription
import androidx.compose.ui.text.font.FontWeight
@@ -54,7 +56,6 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
-import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaToggleButton
import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons
import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme
import com.google.samples.apps.nowinandroid.core.domain.model.FollowableAuthor
@@ -73,8 +74,8 @@ fun AuthorsCarousel(
LazyRow(
modifier = modifier.testTag(tag),
- contentPadding = PaddingValues(24.dp),
- horizontalArrangement = Arrangement.spacedBy(24.dp),
+ contentPadding = PaddingValues(16.dp),
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
state = lazyListState
) {
items(items = authors, key = { item -> item.author.id }) { followableAuthor ->
@@ -97,24 +98,32 @@ fun AuthorItem(
modifier: Modifier = Modifier,
) {
val followDescription = if (following) {
- stringResource(id = R.string.following)
+ stringResource(R.string.following)
} else {
- stringResource(id = R.string.not_following)
+ stringResource(R.string.not_following)
+ }
+ val followActionLabel = if (following) {
+ stringResource(R.string.unfollow)
+ } else {
+ stringResource(R.string.follow)
}
Column(
modifier = modifier
.toggleable(
value = following,
- enabled = true,
role = Role.Button,
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(bounded = false),
onValueChange = { newFollowing -> onAuthorClick(newFollowing) },
)
+ .padding(8.dp)
.sizeIn(maxWidth = 48.dp)
.semantics(mergeDescendants = true) {
+ // Add information for A11y services, explaining what each state means and
+ // what will happen when the user interacts with the author item.
stateDescription = "$followDescription ${author.name}"
+ onClick(label = followActionLabel, action = null)
}
) {
Box(
@@ -136,28 +145,27 @@ fun AuthorItem(
AsyncImage(
modifier = authorImageModifier,
model = author.imageUrl,
- contentScale = ContentScale.Fit,
+ contentScale = ContentScale.Crop,
contentDescription = null
)
}
- NiaToggleButton(
- checked = following,
- onCheckedChange = onAuthorClick,
- modifier = Modifier.align(Alignment.BottomEnd),
- icon = {
- Icon(
- imageVector = NiaIcons.Add,
- contentDescription = null
- )
- },
- checkedIcon = {
- Icon(
- imageVector = NiaIcons.Check,
- contentDescription = null
- )
- },
- size = 24.dp,
- backgroundColor = MaterialTheme.colorScheme.surface
+ val backgroundColor =
+ if (following)
+ MaterialTheme.colorScheme.primaryContainer
+ else
+ MaterialTheme.colorScheme.surface
+ Icon(
+ imageVector = if (following) NiaIcons.Check else NiaIcons.Add,
+ contentDescription = null,
+ modifier = Modifier
+ .align(Alignment.BottomEnd)
+ .size(18.dp)
+ .drawBehind {
+ drawCircle(
+ color = backgroundColor,
+ radius = 12.dp.toPx()
+ )
+ }
)
}
Spacer(modifier = Modifier.height(4.dp))
diff --git a/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/FollowedInterestsUiState.kt b/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/FollowedInterestsUiState.kt
deleted file mode 100644
index 744cebbaa..000000000
--- a/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/FollowedInterestsUiState.kt
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * 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.feature.foryou
-
-/**
- * A sealed hierarchy for the user's current followed interests state.
- */
-sealed interface FollowedInterestsUiState {
-
- /**
- * The current state is unknown (hasn't loaded yet)
- */
- object Unknown : FollowedInterestsUiState
-
- /**
- * The user hasn't followed any interests yet.
- */
- object None : FollowedInterestsUiState
-
- /**
- * The user has followed the given (non-empty) set of [topicIds] or [authorIds].
- */
- data class FollowedInterests(
- val topicIds: Set,
- val authorIds: Set
- ) : FollowedInterestsUiState
-}
diff --git a/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt b/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt
index bcff09fae..4651e1204 100644
--- a/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt
+++ b/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt
@@ -94,17 +94,17 @@ internal fun ForYouRoute(
modifier: Modifier = Modifier,
viewModel: ForYouViewModel = hiltViewModel()
) {
- val interestsSelectionState by viewModel.interestsSelectionUiState.collectAsStateWithLifecycle()
+ val onboardingUiState by viewModel.onboardingUiState.collectAsStateWithLifecycle()
val feedState by viewModel.feedState.collectAsStateWithLifecycle()
val isSyncing by viewModel.isSyncing.collectAsStateWithLifecycle()
ForYouScreen(
isSyncing = isSyncing,
- interestsSelectionState = interestsSelectionState,
+ onboardingUiState = onboardingUiState,
feedState = feedState,
onTopicCheckedChanged = viewModel::updateTopicSelection,
onAuthorCheckedChanged = viewModel::updateAuthorSelection,
- saveFollowedTopics = viewModel::saveFollowedInterests,
+ saveFollowedTopics = viewModel::dismissOnboarding,
onNewsResourcesCheckedChanged = viewModel::updateNewsResourceSaved,
modifier = modifier
)
@@ -113,7 +113,7 @@ internal fun ForYouRoute(
@Composable
internal fun ForYouScreen(
isSyncing: Boolean,
- interestsSelectionState: ForYouInterestsSelectionUiState,
+ onboardingUiState: OnboardingUiState,
feedState: NewsFeedUiState,
onTopicCheckedChanged: (String, Boolean) -> Unit,
onAuthorCheckedChanged: (String, Boolean) -> Unit,
@@ -125,11 +125,11 @@ internal fun ForYouScreen(
// Workaround to call Activity.reportFullyDrawn from Jetpack Compose.
// This code should be called when the UI is ready for use
// and relates to Time To Full Display.
- val interestsLoaded =
- interestsSelectionState !is ForYouInterestsSelectionUiState.Loading
+ val onboardingLoaded =
+ onboardingUiState !is OnboardingUiState.Loading
val feedLoaded = feedState !is NewsFeedUiState.Loading
- if (interestsLoaded && feedLoaded) {
+ if (onboardingLoaded && feedLoaded) {
val localView = LocalView.current
// We use Unit to call reportFullyDrawn only on the first recomposition,
// however it will be called again if this composable goes out of scope.
@@ -156,8 +156,8 @@ internal fun ForYouScreen(
.testTag("forYou:feed"),
state = state
) {
- interestsSelection(
- interestsSelectionState = interestsSelectionState,
+ onboarding(
+ onboardingUiState = onboardingUiState,
onAuthorCheckedChanged = onAuthorCheckedChanged,
onTopicCheckedChanged = onTopicCheckedChanged,
saveFollowedTopics = saveFollowedTopics,
@@ -193,7 +193,7 @@ internal fun ForYouScreen(
AnimatedVisibility(
visible = isSyncing ||
feedState is NewsFeedUiState.Loading ||
- interestsSelectionState is ForYouInterestsSelectionUiState.Loading
+ onboardingUiState is OnboardingUiState.Loading
) {
val loadingContentDescription = stringResource(id = R.string.for_you_loading)
Box(
@@ -208,23 +208,23 @@ internal fun ForYouScreen(
}
/**
- * An extension on [LazyListScope] defining the interests selection portion of the for you screen.
- * Depending on the [interestsSelectionState], this might emit no items.
+ * An extension on [LazyListScope] defining the onboarding portion of the for you screen.
+ * Depending on the [onboardingUiState], this might emit no items.
*
*/
-private fun LazyGridScope.interestsSelection(
- interestsSelectionState: ForYouInterestsSelectionUiState,
+private fun LazyGridScope.onboarding(
+ onboardingUiState: OnboardingUiState,
onAuthorCheckedChanged: (String, Boolean) -> Unit,
onTopicCheckedChanged: (String, Boolean) -> Unit,
saveFollowedTopics: () -> Unit,
interestsItemModifier: Modifier = Modifier
) {
- when (interestsSelectionState) {
- ForYouInterestsSelectionUiState.Loading,
- ForYouInterestsSelectionUiState.LoadFailed,
- ForYouInterestsSelectionUiState.NoInterestsSelection -> Unit
+ when (onboardingUiState) {
+ OnboardingUiState.Loading,
+ OnboardingUiState.LoadFailed,
+ OnboardingUiState.NotShown -> Unit
- is ForYouInterestsSelectionUiState.WithInterestsSelection -> {
+ is OnboardingUiState.Shown -> {
item(span = { GridItemSpan(maxLineSpan) }) {
Column(modifier = interestsItemModifier) {
Text(
@@ -244,14 +244,14 @@ private fun LazyGridScope.interestsSelection(
style = MaterialTheme.typography.bodyMedium
)
AuthorsCarousel(
- authors = interestsSelectionState.authors,
+ authors = onboardingUiState.authors,
onAuthorClick = onAuthorCheckedChanged,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp)
)
TopicSelection(
- interestsSelectionState,
+ onboardingUiState,
onTopicCheckedChanged,
Modifier.padding(bottom = 8.dp)
)
@@ -262,7 +262,7 @@ private fun LazyGridScope.interestsSelection(
) {
NiaFilledButton(
onClick = saveFollowedTopics,
- enabled = interestsSelectionState.canSaveInterests,
+ enabled = onboardingUiState.isDismissable,
modifier = Modifier
.padding(horizontal = 40.dp)
.width(364.dp)
@@ -280,7 +280,7 @@ private fun LazyGridScope.interestsSelection(
@Composable
private fun TopicSelection(
- interestsSelectionState: ForYouInterestsSelectionUiState.WithInterestsSelection,
+ onboardingUiState: OnboardingUiState.Shown,
onTopicCheckedChanged: (String, Boolean) -> Unit,
modifier: Modifier = Modifier
) = trace("TopicSelection") {
@@ -306,7 +306,7 @@ private fun TopicSelection(
.heightIn(max = max(240.dp, with(LocalDensity.current) { 240.sp.toDp() }))
.fillMaxWidth()
) {
- items(interestsSelectionState.topics) {
+ items(onboardingUiState.topics) {
SingleTopicButton(
name = it.topic.name,
topicId = it.topic.id,
@@ -396,7 +396,7 @@ fun ForYouScreenPopulatedFeed() {
NiaTheme {
ForYouScreen(
isSyncing = false,
- interestsSelectionState = ForYouInterestsSelectionUiState.NoInterestsSelection,
+ onboardingUiState = OnboardingUiState.NotShown,
feedState = NewsFeedUiState.Success(
feed = previewNewsResources.map {
SaveableNewsResource(it, false)
@@ -418,7 +418,7 @@ fun ForYouScreenOfflinePopulatedFeed() {
NiaTheme {
ForYouScreen(
isSyncing = false,
- interestsSelectionState = ForYouInterestsSelectionUiState.NoInterestsSelection,
+ onboardingUiState = OnboardingUiState.NotShown,
feedState = NewsFeedUiState.Success(
feed = previewNewsResources.map {
SaveableNewsResource(it, false)
@@ -440,7 +440,7 @@ fun ForYouScreenTopicSelection() {
NiaTheme {
ForYouScreen(
isSyncing = false,
- interestsSelectionState = ForYouInterestsSelectionUiState.WithInterestsSelection(
+ onboardingUiState = OnboardingUiState.Shown(
topics = previewTopics.map { FollowableTopic(it, false) },
authors = previewAuthors.map { FollowableAuthor(it, false) }
),
@@ -465,7 +465,7 @@ fun ForYouScreenLoading() {
NiaTheme {
ForYouScreen(
isSyncing = false,
- interestsSelectionState = ForYouInterestsSelectionUiState.Loading,
+ onboardingUiState = OnboardingUiState.Loading,
feedState = NewsFeedUiState.Loading,
onTopicCheckedChanged = { _, _ -> },
onAuthorCheckedChanged = { _, _ -> },
@@ -483,7 +483,7 @@ fun ForYouScreenPopulatedAndLoading() {
NiaTheme {
ForYouScreen(
isSyncing = true,
- interestsSelectionState = ForYouInterestsSelectionUiState.Loading,
+ onboardingUiState = OnboardingUiState.Loading,
feedState = NewsFeedUiState.Success(
feed = previewNewsResources.map {
SaveableNewsResource(it, false)
diff --git a/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModel.kt b/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModel.kt
index 548e786fe..c79c9d777 100644
--- a/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModel.kt
+++ b/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModel.kt
@@ -16,14 +16,9 @@
package com.google.samples.apps.nowinandroid.feature.foryou
-import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.snapshotFlow
-import androidx.compose.runtime.snapshots.Snapshot.Companion.withMutableSnapshot
-import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
-import androidx.lifecycle.viewmodel.compose.SavedStateHandleSaveableApi
-import androidx.lifecycle.viewmodel.compose.saveable
import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository
import com.google.samples.apps.nowinandroid.core.data.util.SyncStatusMonitor
import com.google.samples.apps.nowinandroid.core.domain.GetFollowableTopicsStreamUseCase
@@ -31,9 +26,6 @@ import com.google.samples.apps.nowinandroid.core.domain.GetSaveableNewsResources
import com.google.samples.apps.nowinandroid.core.domain.GetSortedFollowableAuthorsStreamUseCase
import com.google.samples.apps.nowinandroid.core.domain.model.SaveableNewsResource
import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState
-import com.google.samples.apps.nowinandroid.feature.foryou.FollowedInterestsUiState.FollowedInterests
-import com.google.samples.apps.nowinandroid.feature.foryou.FollowedInterestsUiState.None
-import com.google.samples.apps.nowinandroid.feature.foryou.FollowedInterestsUiState.Unknown
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
@@ -41,56 +33,22 @@ import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest
-import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
-@OptIn(SavedStateHandleSaveableApi::class)
@HiltViewModel
class ForYouViewModel @Inject constructor(
syncStatusMonitor: SyncStatusMonitor,
private val userDataRepository: UserDataRepository,
private val getSaveableNewsResourcesStream: GetSaveableNewsResourcesStreamUseCase,
getSortedFollowableAuthorsStream: GetSortedFollowableAuthorsStreamUseCase,
- getFollowableTopicsStream: GetFollowableTopicsStreamUseCase,
- savedStateHandle: SavedStateHandle
+ getFollowableTopicsStream: GetFollowableTopicsStreamUseCase
) : ViewModel() {
- private val followedInterestsUiState: StateFlow =
- userDataRepository.userDataStream
- .map { userData ->
- if (userData.followedAuthors.isEmpty() && userData.followedTopics.isEmpty()) {
- None
- } else {
- FollowedInterests(
- authorIds = userData.followedAuthors,
- topicIds = userData.followedTopics
- )
- }
- }
- .stateIn(
- scope = viewModelScope,
- started = SharingStarted.WhileSubscribed(5_000),
- initialValue = Unknown
- )
-
- /**
- * The in-progress set of topics to be selected, persisted through process death with a
- * [SavedStateHandle].
- */
- private var inProgressTopicSelection by savedStateHandle.saveable {
- mutableStateOf>(emptySet())
- }
-
- /**
- * The in-progress set of authors to be selected, persisted through process death with a
- * [SavedStateHandle].
- */
- private var inProgressAuthorSelection by savedStateHandle.saveable {
- mutableStateOf>(emptySet())
- }
+ private val shouldShowOnboarding: Flow =
+ userDataRepository.userDataStream.map { !it.shouldHideOnboarding }
val isSyncing = syncStatusMonitor.isSyncing
.stateIn(
@@ -100,35 +58,23 @@ class ForYouViewModel @Inject constructor(
)
val feedState: StateFlow =
- combine(
- followedInterestsUiState,
- snapshotFlow { inProgressTopicSelection },
- snapshotFlow { inProgressAuthorSelection }
- ) { followedInterestsUiState, inProgressTopicSelection, inProgressAuthorSelection ->
- when (followedInterestsUiState) {
- // If we don't know the current selection state, emit loading.
- Unknown -> flowOf(NewsFeedUiState.Loading)
- // If the user has followed topics, use those followed topics to populate the feed
- is FollowedInterests -> {
+ userDataRepository.userDataStream
+ .map { userData ->
+ // If the user hasn't completed the onboarding and hasn't selected any interests
+ // show an empty news list to clearly demonstrate that their selections affect the
+ // news articles they will see.
+ if (!userData.shouldHideOnboarding &&
+ userData.followedAuthors.isEmpty() &&
+ userData.followedTopics.isEmpty()
+ ) {
+ snapshotFlow { NewsFeedUiState.Success(emptyList()) }
+ } else {
getSaveableNewsResourcesStream(
- filterTopicIds = followedInterestsUiState.topicIds,
- filterAuthorIds = followedInterestsUiState.authorIds
+ filterTopicIds = userData.followedTopics,
+ filterAuthorIds = userData.followedAuthors
).mapToFeedState()
}
- // If the user hasn't followed interests yet, show a realtime populated feed based
- // on the in-progress interests selections, if there are any.
- None -> {
- if (inProgressTopicSelection.isEmpty() && inProgressAuthorSelection.isEmpty()) {
- flowOf(NewsFeedUiState.Success(emptyList()))
- } else {
- getSaveableNewsResourcesStream(
- filterTopicIds = inProgressTopicSelection,
- filterAuthorIds = inProgressAuthorSelection
- ).mapToFeedState()
- }
- }
}
- }
// Flatten the feed flows.
// As the selected topics and topic state changes, this will cancel the old feed
// monitoring and start the new one.
@@ -139,58 +85,36 @@ class ForYouViewModel @Inject constructor(
initialValue = NewsFeedUiState.Loading
)
- val interestsSelectionUiState: StateFlow =
+ val onboardingUiState: StateFlow =
combine(
- followedInterestsUiState,
- getFollowableTopicsStream(
- followedTopicIdsStream = snapshotFlow { inProgressTopicSelection }
- ),
- snapshotFlow { inProgressAuthorSelection }.flatMapLatest {
- getSortedFollowableAuthorsStream(it)
- }
- ) { followedInterestsUiState, topics, authors ->
- when (followedInterestsUiState) {
- Unknown -> ForYouInterestsSelectionUiState.Loading
- is FollowedInterests -> ForYouInterestsSelectionUiState.NoInterestsSelection
- None -> {
- if (topics.isEmpty() && authors.isEmpty()) {
- ForYouInterestsSelectionUiState.LoadFailed
- } else {
- ForYouInterestsSelectionUiState.WithInterestsSelection(
- topics = topics,
- authors = authors
- )
- }
- }
+ shouldShowOnboarding,
+ getFollowableTopicsStream(),
+ getSortedFollowableAuthorsStream()
+ ) { shouldShowOnboarding, topics, authors ->
+ if (shouldShowOnboarding) {
+ OnboardingUiState.Shown(
+ topics = topics,
+ authors = authors
+ )
+ } else {
+ OnboardingUiState.NotShown
}
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
- initialValue = ForYouInterestsSelectionUiState.Loading
+ initialValue = OnboardingUiState.Loading
)
fun updateTopicSelection(topicId: String, isChecked: Boolean) {
- withMutableSnapshot {
- inProgressTopicSelection =
- // Update the in-progress selection based on whether the topic id was checked
- if (isChecked) {
- inProgressTopicSelection + topicId
- } else {
- inProgressTopicSelection - topicId
- }
+ viewModelScope.launch {
+ userDataRepository.toggleFollowedTopicId(topicId, isChecked)
}
}
fun updateAuthorSelection(authorId: String, isChecked: Boolean) {
- withMutableSnapshot {
- inProgressAuthorSelection =
- // Update the in-progress selection based on whether the author id was checked
- if (isChecked) {
- inProgressAuthorSelection + authorId
- } else {
- inProgressAuthorSelection - authorId
- }
+ viewModelScope.launch {
+ userDataRepository.toggleFollowedAuthorId(authorId, isChecked)
}
}
@@ -200,20 +124,9 @@ class ForYouViewModel @Inject constructor(
}
}
- fun saveFollowedInterests() {
- // Don't attempt to save anything if nothing is selected
- if (inProgressTopicSelection.isEmpty() && inProgressAuthorSelection.isEmpty()) {
- return
- }
-
+ fun dismissOnboarding() {
viewModelScope.launch {
- userDataRepository.setFollowedTopicIds(inProgressTopicSelection)
- userDataRepository.setFollowedAuthorIds(inProgressAuthorSelection)
- // Clear out the old selection, in case we return to onboarding
- withMutableSnapshot {
- inProgressTopicSelection = emptySet()
- inProgressAuthorSelection = emptySet()
- }
+ userDataRepository.setShouldHideOnboarding(true)
}
}
}
diff --git a/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouInterestsSelectionUiState.kt b/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/OnboardingUiState.kt
similarity index 59%
rename from feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouInterestsSelectionUiState.kt
rename to feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/OnboardingUiState.kt
index e1e0d56d0..4760a4d13 100644
--- a/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouInterestsSelectionUiState.kt
+++ b/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/OnboardingUiState.kt
@@ -20,35 +20,35 @@ import com.google.samples.apps.nowinandroid.core.domain.model.FollowableAuthor
import com.google.samples.apps.nowinandroid.core.domain.model.FollowableTopic
/**
- * A sealed hierarchy describing the interests selection state for the for you screen.
+ * A sealed hierarchy describing the onboarding state for the for you screen.
*/
-sealed interface ForYouInterestsSelectionUiState {
+sealed interface OnboardingUiState {
/**
- * The interests selection state is loading.
+ * The onboarding state is loading.
*/
- object Loading : ForYouInterestsSelectionUiState
+ object Loading : OnboardingUiState
/**
- * The interests selection state was unable to load.
+ * The onboarding state was unable to load.
*/
- object LoadFailed : ForYouInterestsSelectionUiState
+ object LoadFailed : OnboardingUiState
/**
- * There is no interests selection state.
+ * There is no onboarding state.
*/
- object NoInterestsSelection : ForYouInterestsSelectionUiState
+ object NotShown : OnboardingUiState
/**
- * There is a interests selection state, with the given lists of topics and authors.
+ * There is a onboarding state, with the given lists of topics and authors.
*/
- data class WithInterestsSelection(
+ data class Shown(
val topics: List,
val authors: List
- ) : ForYouInterestsSelectionUiState {
+ ) : OnboardingUiState {
/**
- * True if the current in-progress selection can be saved.
+ * True if the onboarding can be dismissed.
*/
- val canSaveInterests: Boolean get() =
+ val isDismissable: Boolean get() =
topics.any { it.isFollowed } || authors.any { it.isFollowed }
}
}
diff --git a/feature/foryou/src/main/res/values/strings.xml b/feature/foryou/src/main/res/values/strings.xml
index 8f109ef69..1880ab953 100644
--- a/feature/foryou/src/main/res/values/strings.xml
+++ b/feature/foryou/src/main/res/values/strings.xml
@@ -27,5 +27,7 @@
You are following
You are not following
+ Follow
+ Unfollow
diff --git a/feature/foryou/src/test/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModelTest.kt b/feature/foryou/src/test/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModelTest.kt
index 4a6ce9c12..5f21f9d06 100644
--- a/feature/foryou/src/test/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModelTest.kt
+++ b/feature/foryou/src/test/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModelTest.kt
@@ -16,7 +16,6 @@
package com.google.samples.apps.nowinandroid.feature.foryou
-import androidx.lifecycle.SavedStateHandle
import com.google.samples.apps.nowinandroid.core.domain.GetFollowableTopicsStreamUseCase
import com.google.samples.apps.nowinandroid.core.domain.GetSaveableNewsResourcesStreamUseCase
import com.google.samples.apps.nowinandroid.core.domain.GetSortedFollowableAuthorsStreamUseCase
@@ -65,7 +64,8 @@ class ForYouViewModelTest {
userDataRepository = userDataRepository
)
private val getSortedFollowableAuthorsStream = GetSortedFollowableAuthorsStreamUseCase(
- authorsRepository = authorsRepository
+ authorsRepository = authorsRepository,
+ userDataRepository = userDataRepository
)
private val getFollowableTopicsStreamUseCase = GetFollowableTopicsStreamUseCase(
topicsRepository = topicsRepository,
@@ -80,16 +80,15 @@ class ForYouViewModelTest {
userDataRepository = userDataRepository,
getSaveableNewsResourcesStream = getSaveableNewsResourcesStreamUseCase,
getSortedFollowableAuthorsStream = getSortedFollowableAuthorsStream,
- getFollowableTopicsStream = getFollowableTopicsStreamUseCase,
- savedStateHandle = SavedStateHandle()
+ getFollowableTopicsStream = getFollowableTopicsStreamUseCase
)
}
@Test
fun stateIsInitiallyLoading() = runTest {
assertEquals(
- ForYouInterestsSelectionUiState.Loading,
- viewModel.interestsSelectionUiState.value
+ OnboardingUiState.Loading,
+ viewModel.onboardingUiState.value
)
assertEquals(NewsFeedUiState.Loading, viewModel.feedState.value)
}
@@ -97,14 +96,14 @@ class ForYouViewModelTest {
@Test
fun stateIsLoadingWhenFollowedTopicsAreLoading() = runTest {
val collectJob1 =
- launch(UnconfinedTestDispatcher()) { viewModel.interestsSelectionUiState.collect() }
+ launch(UnconfinedTestDispatcher()) { viewModel.onboardingUiState.collect() }
val collectJob2 = launch(UnconfinedTestDispatcher()) { viewModel.feedState.collect() }
topicsRepository.sendTopics(sampleTopics)
assertEquals(
- ForYouInterestsSelectionUiState.Loading,
- viewModel.interestsSelectionUiState.value
+ OnboardingUiState.Loading,
+ viewModel.onboardingUiState.value
)
assertEquals(NewsFeedUiState.Loading, viewModel.feedState.value)
@@ -130,14 +129,14 @@ class ForYouViewModelTest {
@Test
fun stateIsLoadingWhenFollowedAuthorsAreLoading() = runTest {
val collectJob1 =
- launch(UnconfinedTestDispatcher()) { viewModel.interestsSelectionUiState.collect() }
+ launch(UnconfinedTestDispatcher()) { viewModel.onboardingUiState.collect() }
val collectJob2 = launch(UnconfinedTestDispatcher()) { viewModel.feedState.collect() }
authorsRepository.sendAuthors(sampleAuthors)
assertEquals(
- ForYouInterestsSelectionUiState.Loading,
- viewModel.interestsSelectionUiState.value
+ OnboardingUiState.Loading,
+ viewModel.onboardingUiState.value
)
assertEquals(NewsFeedUiState.Loading, viewModel.feedState.value)
@@ -146,16 +145,16 @@ class ForYouViewModelTest {
}
@Test
- fun stateIsLoadingWhenTopicsAreLoading() = runTest {
+ fun onboardingStateIsLoadingWhenTopicsAreLoading() = runTest {
val collectJob1 =
- launch(UnconfinedTestDispatcher()) { viewModel.interestsSelectionUiState.collect() }
+ launch(UnconfinedTestDispatcher()) { viewModel.onboardingUiState.collect() }
val collectJob2 = launch(UnconfinedTestDispatcher()) { viewModel.feedState.collect() }
userDataRepository.setFollowedTopicIds(emptySet())
assertEquals(
- ForYouInterestsSelectionUiState.Loading,
- viewModel.interestsSelectionUiState.value
+ OnboardingUiState.Loading,
+ viewModel.onboardingUiState.value
)
assertEquals(NewsFeedUiState.Success(emptyList()), viewModel.feedState.value)
@@ -164,16 +163,16 @@ class ForYouViewModelTest {
}
@Test
- fun stateIsLoadingWhenAuthorsAreLoading() = runTest {
+ fun onboardingStateIsLoadingWhenAuthorsAreLoading() = runTest {
val collectJob1 =
- launch(UnconfinedTestDispatcher()) { viewModel.interestsSelectionUiState.collect() }
+ launch(UnconfinedTestDispatcher()) { viewModel.onboardingUiState.collect() }
val collectJob2 = launch(UnconfinedTestDispatcher()) { viewModel.feedState.collect() }
userDataRepository.setFollowedAuthorIds(emptySet())
assertEquals(
- ForYouInterestsSelectionUiState.Loading,
- viewModel.interestsSelectionUiState.value
+ OnboardingUiState.Loading,
+ viewModel.onboardingUiState.value
)
assertEquals(NewsFeedUiState.Success(emptyList()), viewModel.feedState.value)
@@ -182,9 +181,9 @@ class ForYouViewModelTest {
}
@Test
- fun stateIsInterestsSelectionWhenNewsResourcesAreLoading() = runTest {
+ fun onboardingIsShownWhenNewsResourcesAreLoading() = runTest {
val collectJob1 =
- launch(UnconfinedTestDispatcher()) { viewModel.interestsSelectionUiState.collect() }
+ launch(UnconfinedTestDispatcher()) { viewModel.onboardingUiState.collect() }
val collectJob2 = launch(UnconfinedTestDispatcher()) { viewModel.feedState.collect() }
topicsRepository.sendTopics(sampleTopics)
@@ -193,7 +192,7 @@ class ForYouViewModelTest {
userDataRepository.setFollowedAuthorIds(emptySet())
assertEquals(
- ForYouInterestsSelectionUiState.WithInterestsSelection(
+ OnboardingUiState.Shown(
topics = listOf(
FollowableTopic(
topic = Topic(
@@ -265,7 +264,7 @@ class ForYouViewModelTest {
)
),
),
- viewModel.interestsSelectionUiState.value
+ viewModel.onboardingUiState.value
)
assertEquals(
NewsFeedUiState.Success(
@@ -279,9 +278,9 @@ class ForYouViewModelTest {
}
@Test
- fun stateIsInterestsSelectionAfterLoadingEmptyFollowedTopicsAndAuthors() = runTest {
+ fun onboardingIsShownAfterLoadingEmptyFollowedTopicsAndAuthors() = runTest {
val collectJob1 =
- launch(UnconfinedTestDispatcher()) { viewModel.interestsSelectionUiState.collect() }
+ launch(UnconfinedTestDispatcher()) { viewModel.onboardingUiState.collect() }
val collectJob2 = launch(UnconfinedTestDispatcher()) { viewModel.feedState.collect() }
topicsRepository.sendTopics(sampleTopics)
@@ -291,7 +290,7 @@ class ForYouViewModelTest {
newsRepository.sendNewsResources(sampleNewsResources)
assertEquals(
- ForYouInterestsSelectionUiState.WithInterestsSelection(
+ OnboardingUiState.Shown(
topics = listOf(
FollowableTopic(
topic = Topic(
@@ -363,7 +362,7 @@ class ForYouViewModelTest {
)
),
),
- viewModel.interestsSelectionUiState.value
+ viewModel.onboardingUiState.value
)
assertEquals(
NewsFeedUiState.Success(
@@ -378,27 +377,28 @@ class ForYouViewModelTest {
}
@Test
- fun stateIsWithoutInterestsSelectionAfterLoadingFollowedTopics() = runTest {
+ fun onboardingIsNotShownAfterUserDismissesOnboarding() = runTest {
val collectJob1 =
- launch(UnconfinedTestDispatcher()) { viewModel.interestsSelectionUiState.collect() }
+ launch(UnconfinedTestDispatcher()) { viewModel.onboardingUiState.collect() }
val collectJob2 = launch(UnconfinedTestDispatcher()) { viewModel.feedState.collect() }
authorsRepository.sendAuthors(sampleAuthors)
userDataRepository.setFollowedAuthorIds(emptySet())
topicsRepository.sendTopics(sampleTopics)
userDataRepository.setFollowedTopicIds(setOf("0", "1"))
+ viewModel.dismissOnboarding()
assertEquals(
- ForYouInterestsSelectionUiState.NoInterestsSelection,
- viewModel.interestsSelectionUiState.value
+ OnboardingUiState.NotShown,
+ viewModel.onboardingUiState.value
)
assertEquals(NewsFeedUiState.Loading, viewModel.feedState.value)
newsRepository.sendNewsResources(sampleNewsResources)
assertEquals(
- ForYouInterestsSelectionUiState.NoInterestsSelection,
- viewModel.interestsSelectionUiState.value
+ OnboardingUiState.NotShown,
+ viewModel.onboardingUiState.value
)
assertEquals(
NewsFeedUiState.Success(
@@ -417,52 +417,10 @@ class ForYouViewModelTest {
collectJob2.cancel()
}
- @Test
- fun stateIsWithoutInterestsSelectionAfterLoadingFollowedAuthors() = runTest {
- val collectJob1 =
- launch(UnconfinedTestDispatcher()) { viewModel.interestsSelectionUiState.collect() }
- val collectJob2 = launch(UnconfinedTestDispatcher()) { viewModel.feedState.collect() }
-
- authorsRepository.sendAuthors(sampleAuthors)
- userDataRepository.setFollowedAuthorIds(setOf("0", "1"))
- topicsRepository.sendTopics(sampleTopics)
- userDataRepository.setFollowedTopicIds(emptySet())
-
- assertEquals(
- ForYouInterestsSelectionUiState.NoInterestsSelection,
- viewModel.interestsSelectionUiState.value
- )
- assertEquals(
- NewsFeedUiState.Loading,
- viewModel.feedState.value
- )
-
- newsRepository.sendNewsResources(sampleNewsResources)
-
- assertEquals(
- ForYouInterestsSelectionUiState.NoInterestsSelection,
- viewModel.interestsSelectionUiState.value
- )
- assertEquals(
- NewsFeedUiState.Success(
- feed = sampleNewsResources.map {
- SaveableNewsResource(
- newsResource = it,
- isSaved = false
- )
- }
- ),
- viewModel.feedState.value
- )
-
- collectJob1.cancel()
- collectJob2.cancel()
- }
-
@Test
fun topicSelectionUpdatesAfterSelectingTopic() = runTest {
val collectJob1 =
- launch(UnconfinedTestDispatcher()) { viewModel.interestsSelectionUiState.collect() }
+ launch(UnconfinedTestDispatcher()) { viewModel.onboardingUiState.collect() }
val collectJob2 = launch(UnconfinedTestDispatcher()) { viewModel.feedState.collect() }
topicsRepository.sendTopics(sampleTopics)
@@ -472,7 +430,7 @@ class ForYouViewModelTest {
newsRepository.sendNewsResources(sampleNewsResources)
assertEquals(
- ForYouInterestsSelectionUiState.WithInterestsSelection(
+ OnboardingUiState.Shown(
topics = listOf(
FollowableTopic(
topic = Topic(
@@ -544,7 +502,7 @@ class ForYouViewModelTest {
)
),
),
- viewModel.interestsSelectionUiState.value
+ viewModel.onboardingUiState.value
)
assertEquals(
NewsFeedUiState.Success(
@@ -556,7 +514,7 @@ class ForYouViewModelTest {
viewModel.updateTopicSelection("1", isChecked = true)
assertEquals(
- ForYouInterestsSelectionUiState.WithInterestsSelection(
+ OnboardingUiState.Shown(
topics = listOf(
FollowableTopic(
topic = Topic(
@@ -628,7 +586,7 @@ class ForYouViewModelTest {
)
),
),
- viewModel.interestsSelectionUiState.value
+ viewModel.onboardingUiState.value
)
assertEquals(
NewsFeedUiState.Success(
@@ -651,9 +609,9 @@ class ForYouViewModelTest {
}
@Test
- fun topicSelectionUpdatesAfterSelectingAuthor() = runTest {
+ fun authorSelectionUpdatesAfterSelectingAuthor() = runTest {
val collectJob1 =
- launch(UnconfinedTestDispatcher()) { viewModel.interestsSelectionUiState.collect() }
+ launch(UnconfinedTestDispatcher()) { viewModel.onboardingUiState.collect() }
val collectJob2 = launch(UnconfinedTestDispatcher()) { viewModel.feedState.collect() }
topicsRepository.sendTopics(sampleTopics)
@@ -663,7 +621,7 @@ class ForYouViewModelTest {
newsRepository.sendNewsResources(sampleNewsResources)
assertEquals(
- ForYouInterestsSelectionUiState.WithInterestsSelection(
+ OnboardingUiState.Shown(
topics = listOf(
FollowableTopic(
topic = Topic(
@@ -735,7 +693,7 @@ class ForYouViewModelTest {
)
),
),
- viewModel.interestsSelectionUiState.value
+ viewModel.onboardingUiState.value
)
assertEquals(
NewsFeedUiState.Success(
@@ -747,7 +705,7 @@ class ForYouViewModelTest {
viewModel.updateAuthorSelection("1", isChecked = true)
assertEquals(
- ForYouInterestsSelectionUiState.WithInterestsSelection(
+ OnboardingUiState.Shown(
topics = listOf(
FollowableTopic(
topic = Topic(
@@ -819,7 +777,7 @@ class ForYouViewModelTest {
)
),
),
- viewModel.interestsSelectionUiState.value
+ viewModel.onboardingUiState.value
)
assertEquals(
NewsFeedUiState.Success(
@@ -844,7 +802,7 @@ class ForYouViewModelTest {
@Test
fun topicSelectionUpdatesAfterUnselectingTopic() = runTest {
val collectJob1 =
- launch(UnconfinedTestDispatcher()) { viewModel.interestsSelectionUiState.collect() }
+ launch(UnconfinedTestDispatcher()) { viewModel.onboardingUiState.collect() }
val collectJob2 = launch(UnconfinedTestDispatcher()) { viewModel.feedState.collect() }
topicsRepository.sendTopics(sampleTopics)
@@ -857,7 +815,7 @@ class ForYouViewModelTest {
advanceUntilIdle()
assertEquals(
- ForYouInterestsSelectionUiState.WithInterestsSelection(
+ OnboardingUiState.Shown(
topics = listOf(
FollowableTopic(
topic = Topic(
@@ -929,7 +887,7 @@ class ForYouViewModelTest {
)
),
),
- viewModel.interestsSelectionUiState.value
+ viewModel.onboardingUiState.value
)
assertEquals(
NewsFeedUiState.Success(
@@ -943,9 +901,9 @@ class ForYouViewModelTest {
}
@Test
- fun topicSelectionUpdatesAfterUnselectingAuthor() = runTest {
+ fun authorSelectionUpdatesAfterUnselectingAuthor() = runTest {
val collectJob1 =
- launch(UnconfinedTestDispatcher()) { viewModel.interestsSelectionUiState.collect() }
+ launch(UnconfinedTestDispatcher()) { viewModel.onboardingUiState.collect() }
val collectJob2 = launch(UnconfinedTestDispatcher()) { viewModel.feedState.collect() }
topicsRepository.sendTopics(sampleTopics)
@@ -958,7 +916,7 @@ class ForYouViewModelTest {
assertEquals(
- ForYouInterestsSelectionUiState.WithInterestsSelection(
+ OnboardingUiState.Shown(
topics = listOf(
FollowableTopic(
topic = Topic(
@@ -1030,331 +988,7 @@ class ForYouViewModelTest {
)
),
),
- viewModel.interestsSelectionUiState.value
- )
- assertEquals(
- NewsFeedUiState.Success(
- feed = emptyList()
- ),
- viewModel.feedState.value
- )
-
- collectJob1.cancel()
- collectJob2.cancel()
- }
-
- @Test
- fun topicSelectionUpdatesAfterSavingTopicsOnly() = runTest {
- val collectJob1 =
- launch(UnconfinedTestDispatcher()) { viewModel.interestsSelectionUiState.collect() }
- val collectJob2 = launch(UnconfinedTestDispatcher()) { viewModel.feedState.collect() }
-
- topicsRepository.sendTopics(sampleTopics)
- userDataRepository.setFollowedTopicIds(emptySet())
- authorsRepository.sendAuthors(sampleAuthors)
- userDataRepository.setFollowedAuthorIds(emptySet())
- newsRepository.sendNewsResources(sampleNewsResources)
- viewModel.updateTopicSelection("1", isChecked = true)
-
- viewModel.saveFollowedInterests()
-
- assertEquals(
- ForYouInterestsSelectionUiState.NoInterestsSelection,
- viewModel.interestsSelectionUiState.value
- )
- assertEquals(
- NewsFeedUiState.Success(
- feed = listOf(
- SaveableNewsResource(
- newsResource = sampleNewsResources[1],
- isSaved = false,
- ),
- SaveableNewsResource(
- newsResource = sampleNewsResources[2],
- isSaved = false,
- )
- )
- ),
- viewModel.feedState.value
- )
- assertEquals(setOf("1"), userDataRepository.getCurrentFollowedTopics())
- assertEquals(emptySet(), userDataRepository.getCurrentFollowedAuthors())
-
- collectJob1.cancel()
- collectJob2.cancel()
- }
-
- @Test
- fun topicSelectionUpdatesAfterSavingAuthorsOnly() = runTest {
- val collectJob1 =
- launch(UnconfinedTestDispatcher()) { viewModel.interestsSelectionUiState.collect() }
- val collectJob2 = launch(UnconfinedTestDispatcher()) { viewModel.feedState.collect() }
-
- topicsRepository.sendTopics(sampleTopics)
- userDataRepository.setFollowedTopicIds(emptySet())
- authorsRepository.sendAuthors(sampleAuthors)
- userDataRepository.setFollowedAuthorIds(emptySet())
- newsRepository.sendNewsResources(sampleNewsResources)
- viewModel.updateAuthorSelection("0", isChecked = true)
-
- viewModel.saveFollowedInterests()
-
- assertEquals(
- ForYouInterestsSelectionUiState.NoInterestsSelection,
- viewModel.interestsSelectionUiState.value
- )
- assertEquals(
- NewsFeedUiState.Success(
- feed = listOf(
- SaveableNewsResource(
- newsResource = sampleNewsResources[0],
- isSaved = false
- ),
- )
- ),
- viewModel.feedState.value
- )
- assertEquals(emptySet(), userDataRepository.getCurrentFollowedTopics())
- assertEquals(setOf("0"), userDataRepository.getCurrentFollowedAuthors())
-
- collectJob1.cancel()
- collectJob2.cancel()
- }
-
- @Test
- fun topicSelectionUpdatesAfterSavingAuthorsAndTopics() = runTest {
- val collectJob1 =
- launch(UnconfinedTestDispatcher()) { viewModel.interestsSelectionUiState.collect() }
- val collectJob2 = launch(UnconfinedTestDispatcher()) { viewModel.feedState.collect() }
-
- topicsRepository.sendTopics(sampleTopics)
- userDataRepository.setFollowedTopicIds(emptySet())
- authorsRepository.sendAuthors(sampleAuthors)
- userDataRepository.setFollowedAuthorIds(emptySet())
- newsRepository.sendNewsResources(sampleNewsResources)
- viewModel.updateAuthorSelection("1", isChecked = true)
- viewModel.updateTopicSelection("1", isChecked = true)
-
- viewModel.saveFollowedInterests()
-
- assertEquals(
- ForYouInterestsSelectionUiState.NoInterestsSelection,
- viewModel.interestsSelectionUiState.value
- )
- assertEquals(
- NewsFeedUiState.Success(
- feed = listOf(
- SaveableNewsResource(
- newsResource = sampleNewsResources[1],
- isSaved = false
- ),
- SaveableNewsResource(
- newsResource = sampleNewsResources[2],
- isSaved = false
- )
- )
- ),
- viewModel.feedState.value
- )
- assertEquals(setOf("1"), userDataRepository.getCurrentFollowedTopics())
- assertEquals(setOf("1"), userDataRepository.getCurrentFollowedAuthors())
-
- collectJob1.cancel()
- collectJob2.cancel()
- }
-
- @Test
- fun topicSelectionIsResetAfterSavingTopicsAndRemovingThem() = runTest {
- val collectJob1 =
- launch(UnconfinedTestDispatcher()) { viewModel.interestsSelectionUiState.collect() }
- val collectJob2 = launch(UnconfinedTestDispatcher()) { viewModel.feedState.collect() }
-
- topicsRepository.sendTopics(sampleTopics)
- userDataRepository.setFollowedTopicIds(emptySet())
- authorsRepository.sendAuthors(sampleAuthors)
- userDataRepository.setFollowedAuthorIds(emptySet())
- newsRepository.sendNewsResources(sampleNewsResources)
- viewModel.updateTopicSelection("1", isChecked = true)
- viewModel.saveFollowedInterests()
-
- userDataRepository.setFollowedTopicIds(emptySet())
-
- assertEquals(
- ForYouInterestsSelectionUiState.WithInterestsSelection(
- topics = listOf(
- FollowableTopic(
- topic = Topic(
- id = "0",
- name = "Headlines",
- shortDescription = "",
- longDescription = "long description",
- url = "URL",
- imageUrl = "image URL",
- ),
- isFollowed = false
- ),
- FollowableTopic(
- topic = Topic(
- id = "1",
- name = "UI",
- shortDescription = "",
- longDescription = "long description",
- url = "URL",
- imageUrl = "image URL",
- ),
- isFollowed = false
- ),
- FollowableTopic(
- topic = Topic(
- id = "2",
- name = "Tools",
- shortDescription = "",
- longDescription = "long description",
- url = "URL",
- imageUrl = "image URL",
- ),
- isFollowed = false
- )
- ),
- authors = listOf(
- FollowableAuthor(
- author = Author(
- id = "0",
- name = "Android Dev",
- imageUrl = "",
- twitter = "",
- mediumPage = "",
- bio = "",
- ),
- isFollowed = false
- ),
- FollowableAuthor(
- author = Author(
- id = "1",
- name = "Android Dev 2",
- imageUrl = "",
- twitter = "",
- mediumPage = "",
- bio = "",
- ),
- isFollowed = false
- ),
- FollowableAuthor(
- author = Author(
- id = "2",
- name = "Android Dev 3",
- imageUrl = "",
- twitter = "",
- mediumPage = "",
- bio = "",
- ),
- isFollowed = false
- )
- )
- ),
- viewModel.interestsSelectionUiState.value
- )
- assertEquals(
- NewsFeedUiState.Success(
- feed = emptyList()
- ),
- viewModel.feedState.value
- )
-
- collectJob1.cancel()
- collectJob2.cancel()
- }
-
- @Test
- fun authorSelectionIsResetAfterSavingAuthorsAndRemovingThem() = runTest {
- val collectJob1 =
- launch(UnconfinedTestDispatcher()) { viewModel.interestsSelectionUiState.collect() }
- val collectJob2 = launch(UnconfinedTestDispatcher()) { viewModel.feedState.collect() }
-
- topicsRepository.sendTopics(sampleTopics)
- userDataRepository.setFollowedTopicIds(emptySet())
- authorsRepository.sendAuthors(sampleAuthors)
- userDataRepository.setFollowedAuthorIds(emptySet())
- newsRepository.sendNewsResources(sampleNewsResources)
- viewModel.updateAuthorSelection("1", isChecked = true)
- viewModel.saveFollowedInterests()
-
- userDataRepository.setFollowedAuthorIds(emptySet())
-
- assertEquals(
- ForYouInterestsSelectionUiState.WithInterestsSelection(
- topics = listOf(
- FollowableTopic(
- topic = Topic(
- id = "0",
- name = "Headlines",
- shortDescription = "",
- longDescription = "long description",
- url = "URL",
- imageUrl = "image URL",
- ),
- isFollowed = false
- ),
- FollowableTopic(
- topic = Topic(
- id = "1",
- name = "UI",
- shortDescription = "",
- longDescription = "long description",
- url = "URL",
- imageUrl = "image URL",
- ),
- isFollowed = false
- ),
- FollowableTopic(
- topic = Topic(
- id = "2",
- name = "Tools",
- shortDescription = "",
- longDescription = "long description",
- url = "URL",
- imageUrl = "image URL",
- ),
- isFollowed = false
- )
- ),
- authors = listOf(
- FollowableAuthor(
- author = Author(
- id = "0",
- name = "Android Dev",
- imageUrl = "",
- twitter = "",
- mediumPage = "",
- bio = "",
- ),
- isFollowed = false
- ),
- FollowableAuthor(
- author = Author(
- id = "1",
- name = "Android Dev 2",
- imageUrl = "",
- twitter = "",
- mediumPage = "",
- bio = "",
- ),
- isFollowed = false
- ),
- FollowableAuthor(
- author = Author(
- id = "2",
- name = "Android Dev 3",
- imageUrl = "",
- twitter = "",
- mediumPage = "",
- bio = "",
- ),
- isFollowed = false
- )
- )
- ),
- viewModel.interestsSelectionUiState.value
+ viewModel.onboardingUiState.value
)
assertEquals(
NewsFeedUiState.Success(
@@ -1370,19 +1004,20 @@ class ForYouViewModelTest {
@Test
fun newsResourceSelectionUpdatesAfterLoadingFollowedTopics() = runTest {
val collectJob1 =
- launch(UnconfinedTestDispatcher()) { viewModel.interestsSelectionUiState.collect() }
+ launch(UnconfinedTestDispatcher()) { viewModel.onboardingUiState.collect() }
val collectJob2 = launch(UnconfinedTestDispatcher()) { viewModel.feedState.collect() }
topicsRepository.sendTopics(sampleTopics)
userDataRepository.setFollowedTopicIds(setOf("1"))
authorsRepository.sendAuthors(sampleAuthors)
userDataRepository.setFollowedAuthorIds(setOf("1"))
+ userDataRepository.setShouldHideOnboarding(true)
newsRepository.sendNewsResources(sampleNewsResources)
viewModel.updateNewsResourceSaved("2", true)
assertEquals(
- ForYouInterestsSelectionUiState.NoInterestsSelection,
- viewModel.interestsSelectionUiState.value
+ OnboardingUiState.NotShown,
+ viewModel.onboardingUiState.value
)
assertEquals(
NewsFeedUiState.Success(
diff --git a/feature/interests/src/androidTest/java/com/google/samples/apps/nowinandroid/interests/InterestsRobot.kt b/feature/interests/src/androidTest/java/com/google/samples/apps/nowinandroid/interests/InterestsRobot.kt
new file mode 100644
index 000000000..3cad8ec22
--- /dev/null
+++ b/feature/interests/src/androidTest/java/com/google/samples/apps/nowinandroid/interests/InterestsRobot.kt
@@ -0,0 +1,102 @@
+/*
+ * 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.interests
+
+import androidx.activity.ComponentActivity
+import androidx.annotation.StringRes
+import androidx.compose.ui.test.assertCountEquals
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.junit4.AndroidComposeTestRule
+import androidx.compose.ui.test.onAllNodesWithContentDescription
+import androidx.compose.ui.test.onAllNodesWithText
+import androidx.compose.ui.test.onNodeWithContentDescription
+import androidx.compose.ui.test.onNodeWithText
+import androidx.test.ext.junit.rules.ActivityScenarioRule
+import com.google.samples.apps.nowinandroid.feature.interests.InterestsScreen
+import com.google.samples.apps.nowinandroid.feature.interests.InterestsTabState
+import com.google.samples.apps.nowinandroid.feature.interests.InterestsUiState
+import com.google.samples.apps.nowinandroid.feature.interests.R.string
+
+internal class InterestsRobot(
+ private val composeTestRule: AndroidComposeTestRule<
+ ActivityScenarioRule, ComponentActivity>
+) {
+ private val interestsLoading = getString(string.interests_loading)
+ private val interestsEmptyHeader = getString(string.interests_empty_header)
+ private val interestsTopicCardFollowButton =
+ getString(string.interests_card_follow_button_content_desc)
+ private val interestsTopicCardUnfollowButton =
+ getString(string.interests_card_unfollow_button_content_desc)
+
+ fun setContent(uiState: InterestsUiState, tabIndex: Int = 0) {
+ composeTestRule.setContent {
+ InterestsScreen(
+ uiState = uiState,
+ tabState = InterestsTabState(
+ titles = listOf(string.interests_topics, string.interests_people),
+ currentIndex = tabIndex
+ ),
+ followAuthor = { _, _ -> },
+ followTopic = { _, _ -> },
+ navigateToAuthor = {},
+ navigateToTopic = {},
+ switchTab = {},
+ )
+ }
+ }
+
+ fun interestsLoadingExists() {
+ composeTestRule
+ .onNodeWithContentDescription(interestsLoading)
+ .assertExists()
+ }
+
+ fun nodeWithTextDisplayed(text: String) {
+ composeTestRule
+ .onNodeWithText(text)
+ .assertIsDisplayed()
+ }
+
+ fun nodesWithTextCountEquals(text: String, count: Int) {
+ composeTestRule
+ .onAllNodesWithText(text)
+ .assertCountEquals(count)
+ }
+
+ fun interestsTopicCardFollowButtonCountEquals(count: Int) {
+ nodesWithContentDescriptionCountEquals(interestsTopicCardFollowButton, count)
+ }
+
+ fun interestsTopicCardUnfollowButtonCountEquals(count: Int) {
+ nodesWithContentDescriptionCountEquals(interestsTopicCardUnfollowButton, count)
+ }
+
+ private fun nodesWithContentDescriptionCountEquals(text: String, count: Int) {
+ composeTestRule
+ .onAllNodesWithContentDescription(text)
+ .assertCountEquals(count)
+ }
+
+ fun interestsEmptyHeaderDisplayed() {
+ composeTestRule
+ .onNodeWithText(interestsEmptyHeader)
+ .assertIsDisplayed()
+ }
+
+ private fun getString(@StringRes stringId: Int) =
+ composeTestRule.activity.resources.getString(stringId)
+}
diff --git a/feature/interests/src/androidTest/java/com/google/samples/apps/nowinandroid/interests/InterestsScreenTest.kt b/feature/interests/src/androidTest/java/com/google/samples/apps/nowinandroid/interests/InterestsScreenTest.kt
index 4b067734f..0db29f970 100644
--- a/feature/interests/src/androidTest/java/com/google/samples/apps/nowinandroid/interests/InterestsScreenTest.kt
+++ b/feature/interests/src/androidTest/java/com/google/samples/apps/nowinandroid/interests/InterestsScreenTest.kt
@@ -17,23 +17,14 @@
package com.google.samples.apps.nowinandroid.interests
import androidx.activity.ComponentActivity
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.test.assertCountEquals
-import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.compose.ui.test.junit4.createAndroidComposeRule
-import androidx.compose.ui.test.onAllNodesWithContentDescription
-import androidx.compose.ui.test.onAllNodesWithText
-import androidx.compose.ui.test.onNodeWithContentDescription
-import androidx.compose.ui.test.onNodeWithText
+import androidx.test.ext.junit.rules.ActivityScenarioRule
import com.google.samples.apps.nowinandroid.core.domain.model.FollowableAuthor
import com.google.samples.apps.nowinandroid.core.domain.model.FollowableTopic
import com.google.samples.apps.nowinandroid.core.model.data.Author
import com.google.samples.apps.nowinandroid.core.model.data.Topic
-import com.google.samples.apps.nowinandroid.feature.interests.InterestsScreen
-import com.google.samples.apps.nowinandroid.feature.interests.InterestsTabState
import com.google.samples.apps.nowinandroid.feature.interests.InterestsUiState
-import com.google.samples.apps.nowinandroid.feature.interests.R
-import org.junit.Before
import org.junit.Rule
import org.junit.Test
@@ -47,142 +38,94 @@ class InterestsScreenTest {
@get:Rule
val composeTestRule = createAndroidComposeRule()
- private lateinit var interestsLoading: String
- private lateinit var interestsEmptyHeader: String
- private lateinit var interestsTopicCardFollowButton: String
- private lateinit var interestsTopicCardUnfollowButton: String
-
- @Before
- fun setup() {
- composeTestRule.activity.apply {
- interestsLoading = getString(R.string.interests_loading)
- interestsEmptyHeader = getString(R.string.interests_empty_header)
- interestsTopicCardFollowButton =
- getString(R.string.interests_card_follow_button_content_desc)
- interestsTopicCardUnfollowButton =
- getString(R.string.interests_card_unfollow_button_content_desc)
- }
- }
-
@Test
fun niaLoadingWheel_inTopics_whenScreenIsLoading_showLoading() {
- composeTestRule.setContent {
- InterestsScreen(uiState = InterestsUiState.Loading, tabIndex = 0)
+ launchBookmarksRobot(
+ composeTestRule,
+ InterestsUiState.Loading,
+ 0
+ ) {
+ interestsLoadingExists()
}
-
- composeTestRule
- .onNodeWithContentDescription(interestsLoading)
- .assertExists()
}
@Test
fun niaLoadingWheel_inAuthors_whenScreenIsLoading_showLoading() {
- composeTestRule.setContent {
- InterestsScreen(uiState = InterestsUiState.Loading, tabIndex = 1)
+ launchBookmarksRobot(
+ composeTestRule,
+ InterestsUiState.Loading,
+ 1
+ ) {
+ interestsLoadingExists()
}
-
- composeTestRule
- .onNodeWithContentDescription(interestsLoading)
- .assertExists()
}
@Test
fun interestsWithTopics_whenTopicsFollowed_showFollowedAndUnfollowedTopicsWithInfo() {
- composeTestRule.setContent {
- InterestsScreen(
- uiState = InterestsUiState.Interests(topics = testTopics, authors = listOf()),
- tabIndex = 0
- )
+ launchBookmarksRobot(
+ composeTestRule,
+ InterestsUiState.Interests(topics = testTopics, authors = listOf()),
+ 0
+ ) {
+ nodeWithTextDisplayed(TOPIC_1_NAME)
+ nodeWithTextDisplayed(TOPIC_2_NAME)
+ nodeWithTextDisplayed(TOPIC_3_NAME)
+
+ nodesWithTextCountEquals(TOPIC_SHORT_DESC, testTopics.count())
+
+ interestsTopicCardFollowButtonCountEquals(numberOfUnfollowedTopics)
+ interestsTopicCardUnfollowButtonCountEquals(numberOfFollowedTopics)
}
-
- composeTestRule
- .onNodeWithText(TOPIC_1_NAME)
- .assertIsDisplayed()
- composeTestRule
- .onNodeWithText(TOPIC_2_NAME)
- .assertIsDisplayed()
- composeTestRule
- .onNodeWithText(TOPIC_3_NAME)
- .assertIsDisplayed()
-
- composeTestRule
- .onAllNodesWithText(TOPIC_SHORT_DESC)
- .assertCountEquals(testTopics.count())
-
- composeTestRule
- .onAllNodesWithContentDescription(interestsTopicCardFollowButton)
- .assertCountEquals(numberOfUnfollowedTopics)
-
- composeTestRule
- .onAllNodesWithContentDescription(interestsTopicCardUnfollowButton)
- .assertCountEquals(testAuthors.filter { it.isFollowed }.size)
}
@Test
fun interestsWithTopics_whenAuthorsFollowed_showFollowedAndUnfollowedTopicsWithInfo() {
- composeTestRule.setContent {
- InterestsScreen(
- uiState = InterestsUiState.Interests(topics = listOf(), authors = testAuthors),
- tabIndex = 1
- )
+ launchBookmarksRobot(
+ composeTestRule,
+ InterestsUiState.Interests(topics = listOf(), authors = testAuthors),
+ 1
+ ) {
+ nodeWithTextDisplayed("Android Dev")
+ nodeWithTextDisplayed("Android Dev 2")
+ nodeWithTextDisplayed("Android Dev 3")
+
+ interestsTopicCardFollowButtonCountEquals(numberOfUnfollowedAuthors)
+ interestsTopicCardUnfollowButtonCountEquals(numberOfFollowedAuthors)
}
-
- composeTestRule
- .onNodeWithText("Android Dev")
- .assertIsDisplayed()
- composeTestRule
- .onNodeWithText("Android Dev 2")
- .assertIsDisplayed()
- composeTestRule
- .onNodeWithText("Android Dev 3")
- .assertIsDisplayed()
-
- composeTestRule
- .onAllNodesWithContentDescription(interestsTopicCardFollowButton)
- .assertCountEquals(numberOfUnfollowedAuthors)
-
- composeTestRule
- .onAllNodesWithContentDescription(interestsTopicCardUnfollowButton)
- .assertCountEquals(testTopics.filter { it.isFollowed }.size)
}
@Test
fun topicsEmpty_whenDataIsEmptyOccurs_thenShowEmptyScreen() {
- composeTestRule.setContent {
- InterestsScreen(uiState = InterestsUiState.Empty, tabIndex = 0)
+ launchBookmarksRobot(
+ composeTestRule,
+ InterestsUiState.Empty,
+ 0
+ ) {
+ interestsEmptyHeaderDisplayed()
}
-
- composeTestRule
- .onNodeWithText(interestsEmptyHeader)
- .assertIsDisplayed()
}
@Test
fun authorsEmpty_whenDataIsEmptyOccurs_thenShowEmptyScreen() {
- composeTestRule.setContent {
- InterestsScreen(uiState = InterestsUiState.Empty, tabIndex = 1)
+ launchBookmarksRobot(
+ composeTestRule,
+ InterestsUiState.Empty,
+ 1
+ ) {
+ interestsEmptyHeaderDisplayed()
}
-
- composeTestRule
- .onNodeWithText(interestsEmptyHeader)
- .assertIsDisplayed()
}
+}
- @Composable
- private fun InterestsScreen(uiState: InterestsUiState, tabIndex: Int = 0) {
- InterestsScreen(
- uiState = uiState,
- tabState = InterestsTabState(
- titles = listOf(R.string.interests_topics, R.string.interests_people),
- currentIndex = tabIndex
- ),
- followAuthor = { _, _ -> },
- followTopic = { _, _ -> },
- navigateToAuthor = {},
- navigateToTopic = {},
- switchTab = {},
- )
- }
+private fun launchBookmarksRobot(
+ composeTestRule: AndroidComposeTestRule<
+ ActivityScenarioRule, ComponentActivity>,
+ uiState: InterestsUiState,
+ tabIndex: Int,
+ func: InterestsRobot.() -> Unit
+) = InterestsRobot(composeTestRule).apply {
+ setContent(uiState, tabIndex)
+ func()
}
private const val TOPIC_1_NAME = "Headlines"
@@ -266,4 +209,7 @@ private val testAuthors = listOf(
)
private val numberOfUnfollowedTopics = testTopics.filter { !it.isFollowed }.size
+private val numberOfFollowedTopics = testTopics.filter { it.isFollowed }.size
+
private val numberOfUnfollowedAuthors = testAuthors.filter { !it.isFollowed }.size
+private val numberOfFollowedAuthors = testAuthors.filter { it.isFollowed }.size
diff --git a/feature/interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/InterestsViewModel.kt b/feature/interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/InterestsViewModel.kt
index dcf46caf2..01d8b0d6e 100644
--- a/feature/interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/InterestsViewModel.kt
+++ b/feature/interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/InterestsViewModel.kt
@@ -20,7 +20,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository
import com.google.samples.apps.nowinandroid.core.domain.GetFollowableTopicsStreamUseCase
-import com.google.samples.apps.nowinandroid.core.domain.GetPersistentSortedFollowableAuthorsStreamUseCase
+import com.google.samples.apps.nowinandroid.core.domain.GetSortedFollowableAuthorsStreamUseCase
import com.google.samples.apps.nowinandroid.core.domain.TopicSortField
import com.google.samples.apps.nowinandroid.core.domain.model.FollowableAuthor
import com.google.samples.apps.nowinandroid.core.domain.model.FollowableTopic
@@ -39,7 +39,7 @@ import kotlinx.coroutines.launch
class InterestsViewModel @Inject constructor(
val userDataRepository: UserDataRepository,
getFollowableTopicsStream: GetFollowableTopicsStreamUseCase,
- getPersistentSortedFollowableAuthorsStream: GetPersistentSortedFollowableAuthorsStreamUseCase
+ getSortedFollowableAuthorsStream: GetSortedFollowableAuthorsStreamUseCase
) : ViewModel() {
private val _tabState = MutableStateFlow(
@@ -51,7 +51,7 @@ class InterestsViewModel @Inject constructor(
val tabState: StateFlow = _tabState.asStateFlow()
val uiState: StateFlow = combine(
- getPersistentSortedFollowableAuthorsStream(),
+ getSortedFollowableAuthorsStream(),
getFollowableTopicsStream(sortBy = TopicSortField.NAME),
InterestsUiState::Interests
).stateIn(
diff --git a/feature/interests/src/test/java/com/google/samples/apps/nowinandroid/interests/InterestsViewModelTest.kt b/feature/interests/src/test/java/com/google/samples/apps/nowinandroid/interests/InterestsViewModelTest.kt
index 862faec6b..6ecf3930a 100644
--- a/feature/interests/src/test/java/com/google/samples/apps/nowinandroid/interests/InterestsViewModelTest.kt
+++ b/feature/interests/src/test/java/com/google/samples/apps/nowinandroid/interests/InterestsViewModelTest.kt
@@ -17,7 +17,7 @@
package com.google.samples.apps.nowinandroid.interests
import com.google.samples.apps.nowinandroid.core.domain.GetFollowableTopicsStreamUseCase
-import com.google.samples.apps.nowinandroid.core.domain.GetPersistentSortedFollowableAuthorsStreamUseCase
+import com.google.samples.apps.nowinandroid.core.domain.GetSortedFollowableAuthorsStreamUseCase
import com.google.samples.apps.nowinandroid.core.domain.model.FollowableAuthor
import com.google.samples.apps.nowinandroid.core.domain.model.FollowableTopic
import com.google.samples.apps.nowinandroid.core.model.data.Author
@@ -53,8 +53,8 @@ class InterestsViewModelTest {
topicsRepository = topicsRepository,
userDataRepository = userDataRepository
)
- private val getPersistentSortedFollowableAuthorsStream =
- GetPersistentSortedFollowableAuthorsStreamUseCase(
+ private val getSortedFollowableAuthorsStream =
+ GetSortedFollowableAuthorsStreamUseCase(
authorsRepository = authorsRepository,
userDataRepository = userDataRepository
)
@@ -65,7 +65,7 @@ class InterestsViewModelTest {
viewModel = InterestsViewModel(
userDataRepository = userDataRepository,
getFollowableTopicsStream = getFollowableTopicsStreamUseCase,
- getPersistentSortedFollowableAuthorsStream = getPersistentSortedFollowableAuthorsStream
+ getSortedFollowableAuthorsStream = getSortedFollowableAuthorsStream
)
}
diff --git a/feature/settings/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/settings/SettingsDialogRobot.kt b/feature/settings/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/settings/SettingsDialogRobot.kt
index c94f7b29b..a8f895815 100644
--- a/feature/settings/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/settings/SettingsDialogRobot.kt
+++ b/feature/settings/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/settings/SettingsDialogRobot.kt
@@ -1,17 +1,17 @@
/*
* 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
+ * 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
+ * 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.
+ * 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.feature.settings
@@ -24,7 +24,8 @@ import androidx.compose.ui.test.onNodeWithText
import androidx.test.ext.junit.rules.ActivityScenarioRule
internal class SettingsDialogRobot(
- private val composeTestRule: AndroidComposeTestRule, ComponentActivity>
+ private val composeTestRule: AndroidComposeTestRule<
+ ActivityScenarioRule, ComponentActivity>
) {
fun setContent(settingsUiState: SettingsUiState) {
composeTestRule.setContent {
@@ -57,4 +58,4 @@ internal class SettingsDialogRobot(
fun getString(@StringRes stringId: Int) =
composeTestRule.activity.resources.getString(stringId)
-}
\ No newline at end of file
+}
diff --git a/feature/settings/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/settings/SettingsDialogTest.kt b/feature/settings/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/settings/SettingsDialogTest.kt
index bf1af77dc..6870b2197 100644
--- a/feature/settings/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/settings/SettingsDialogTest.kt
+++ b/feature/settings/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/settings/SettingsDialogTest.kt
@@ -82,7 +82,8 @@ class SettingsDialogTest {
}
private fun launchSettingsDialogRobot(
- composeTestRule: AndroidComposeTestRule, ComponentActivity>,
+ composeTestRule: AndroidComposeTestRule<
+ ActivityScenarioRule, ComponentActivity>,
settingsUiState: SettingsUiState,
func: SettingsDialogRobot.() -> Unit
) = SettingsDialogRobot(composeTestRule).apply {
diff --git a/feature/topic/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/topic/TopicRobot.kt b/feature/topic/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/topic/TopicRobot.kt
index 06fc4d43b..b4ba08927 100644
--- a/feature/topic/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/topic/TopicRobot.kt
+++ b/feature/topic/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/topic/TopicRobot.kt
@@ -1,17 +1,17 @@
/*
* 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
+ * 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
+ * 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.
+ * 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.feature.topic
@@ -29,7 +29,8 @@ import com.google.samples.apps.nowinandroid.core.domain.model.FollowableTopic
import com.google.samples.apps.nowinandroid.core.model.data.NewsResource
internal class TopicRobot(
- private val composeTestRule: AndroidComposeTestRule, ComponentActivity>
+ private val composeTestRule: AndroidComposeTestRule<
+ ActivityScenarioRule, ComponentActivity>
) {
private val topicLoading = composeTestRule.activity.resources.getString(R.string.topic_loading)
@@ -67,4 +68,4 @@ internal class TopicRobot(
.onFirst()
.performScrollToNode(hasText(newsResource.title))
}
-}
\ No newline at end of file
+}
diff --git a/feature/topic/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/topic/TopicScreenTest.kt b/feature/topic/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/topic/TopicScreenTest.kt
index 1df807635..d37efb4b9 100644
--- a/feature/topic/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/topic/TopicScreenTest.kt
+++ b/feature/topic/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/topic/TopicScreenTest.kt
@@ -103,7 +103,8 @@ class TopicScreenTest {
}
private fun launchTopicRobot(
- composeTestRule: AndroidComposeTestRule, ComponentActivity>,
+ composeTestRule: AndroidComposeTestRule<
+ ActivityScenarioRule, ComponentActivity>,
topicUiState: TopicUiState,
newsUiState: NewsUiState,
func: TopicRobot.() -> Unit
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index dafd53947..8d13d36d5 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -10,7 +10,7 @@ androidxComposeRuntimeTracing = "1.0.0-alpha01"
androidxCore = "1.9.0"
androidxCoreSplashscreen = "1.0.0"
androidxDataStore = "1.0.0"
-androidxEspresso = "3.4.0"
+androidxEspresso = "3.5.0"
androidxHiltNavigationCompose = "1.0.0"
androidxLifecycle = "2.6.0-alpha03"
androidxMacroBenchmark = "1.1.0"
@@ -19,10 +19,10 @@ androidxMetrics = "1.0.0-alpha03"
androidxProfileinstaller = "1.2.0"
androidxStartup = "1.1.1"
androidxWindowManager = "1.0.0"
-androidxTestCore = "1.5.0-rc01"
-androidxTestExt = "1.1.3"
-androidxTestRunner = "1.4.0"
-androidxTestRules = "1.4.0"
+androidxTestCore = "1.5.0"
+androidxTestExt = "1.1.4"
+androidxTestRunner = "1.5.1"
+androidxTestRules = "1.5.0"
androidxTracing = "1.1.0"
androidxUiAutomator = "2.2.0"
androidxWork = "2.7.1"
@@ -128,4 +128,4 @@ hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
protobuf = { id = "com.google.protobuf", version.ref = "protobufPlugin" }
-secrets = { id = "com.google.android.libraries.mapsplatform.secrets-gradle-plugin", version.ref = "secrets" }
\ No newline at end of file
+secrets = { id = "com.google.android.libraries.mapsplatform.secrets-gradle-plugin", version.ref = "secrets" }
diff --git a/sync/README.md b/sync/README.md
index 5fb075605..b100e27ad 100644
--- a/sync/README.md
+++ b/sync/README.md
@@ -1,3 +1,3 @@
# :sync module
-
+