Adds Screenshot testing with Roborazzi (#876)

* Adds screenshot tests using Roborazzi (Robolectric Native Graphics)

- Adds Roborazzi to convention plugins
- Adds Screenshot helper in :core-testing
- Creates screenshot suites for :app and :feature-foryou


* CI and spotless


* Moves :app tests to testDemo and makes NiaAppScreenSizesScreenshotTests prettier


* CI: Moves local tests to their own step


* CI: Adds --rerun to screenshot task


* CI: Moves screenshots before local tests


* CI: Fixes wrong if statement in workflow


* CI WIP: trying to trigger the push step


* CI: Re-enables roborazzi verification


* Fixes flaky screenshot tests by setting LocalInspectionMode on


* CI: screenshot commits now use the original author intead of bot account


* CI: Disables globbing because file_pattern didn't work


* CI: Trying new file pattern for png files


* CI: Adds a check for forks


* 🤖 Updates screenshots

* Code review: toml cleanup, comments


* Use new github.event.pull_request.head.repo.fork

Co-authored-by: Simon Marquis <contact@simon-marquis.fr>

* Uses Robolectric qualifiers to set the dpi, adds section to README


* Spotless


* Delegates creation of repository to Hilt in test


* Revert "Use new github.event.pull_request.head.repo.fork"


* 🤖 Updates screenshots

* Empty commit to trigger GHA on main branch


* Makes time zones deterministic in screenshot tests


* Increases GMD timeout to 90m, but it has to be reduced


---------

Co-authored-by: Simon Marquis <contact@simon-marquis.fr>
pull/918/head
Jose Alcérreca 1 year ago committed by GitHub
parent 4716c7f841
commit 886158d3cb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -13,7 +13,7 @@ concurrency:
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 60 timeout-minutes: 90
steps: steps:
- name: Checkout - name: Checkout
@ -43,9 +43,6 @@ jobs:
- name: Build all build type and flavor permutations - name: Build all build type and flavor permutations
run: ./gradlew assemble run: ./gradlew assemble
- name: Run local tests
run: ./gradlew testDemoDebug testProdDebug
- name: Upload build outputs (APKs) - name: Upload build outputs (APKs)
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: with:
@ -59,6 +56,65 @@ jobs:
name: lint-reports name: lint-reports
path: '**/build/reports/lint-results-*.html' path: '**/build/reports/lint-results-*.html'
test:
runs-on: ubuntu-latest
permissions:
contents: write
timeout-minutes: 60
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Validate Gradle Wrapper
uses: gradle/wrapper-validation-action@v1
- name: Copy CI gradle.properties
run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
distribution: 'zulu'
java-version: 17
- name: Setup Gradle
uses: gradle/gradle-build-action@v2
- name: Run all local screenshot tests (Roborazzi)
id: screenshotsverify
continue-on-error: true
run: ./gradlew verifyRoborazziDemoDebug
- name: Prevent pushing new screenshots if this is a fork
id: checkfork
continue-on-error: false
if: steps.screenshotsverify.outcome == 'failure' && github.event.pull_request.head.repo.full_name != github.repository
run: |
echo "::error::Screenshot tests failed, please create a PR in your fork first." && exit 1
# Runs if previous job failed
- name: Generate new screenshots if verification failed and it's a PR
id: screenshotsrecord
if: steps.screenshotsverify.outcome == 'failure' && github.event_name == 'pull_request'
run: |
./gradlew recordRoborazziDemoDebug
- name: Push new screenshots if available
uses: stefanzweifel/git-auto-commit-action@v4
if: steps.screenshotsrecord.outcome == 'success'
with:
file_pattern: '*/*.png'
disable_globbing: true
commit_message: "🤖 Updates screenshots"
# Run local tests after screenshot tests to avoid wrong UP-TO-DATE. TODO: Ignore screenshots.
- name: Run local tests
if: always()
run: ./gradlew testDemoDebug testProdDebug
- name: Upload test results (XML) - name: Upload test results (XML)
if: always() if: always()
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
@ -113,7 +169,7 @@ jobs:
androidTest-GMD: androidTest-GMD:
needs: build needs: build
runs-on: macOS-latest # enables hardware acceleration in the virtual machine runs-on: macOS-latest # enables hardware acceleration in the virtual machine
timeout-minutes: 55 timeout-minutes: 90
steps: steps:
- name: Checkout - name: Checkout

@ -109,6 +109,13 @@ Examples:
manipulate the state of the `Test` repository and verify the resulting behavior, instead of manipulate the state of the `Test` repository and verify the resulting behavior, instead of
checking that specific repository methods were called. checking that specific repository methods were called.
## Screenshot tests
**Now In Android** uses [Roborazzi](https://github.com/takahirom/roborazzi) to do screenshot tests
of certain screens and components. To run these tests, run the `verifyRoborazziDemoDebug` or
`recordRoborazziDemoDebug` tasks. Note that screenshots are recorded on CI, using Linux, and other
platforms might generate slightly different images, making the tests fail.
# UI # UI
The app was designed using [Material 3 guidelines](https://m3.material.io/). Learn more about the design process and The app was designed using [Material 3 guidelines](https://m3.material.io/). Learn more about the design process and
obtain the design files in the [Now in Android Material 3 Case Study](https://goo.gle/nia-figma) (design assets [also available as a PDF](docs/Now-In-Android-Design-File.pdf)). obtain the design files in the [Now in Android Material 3 Case Study](https://goo.gle/nia-figma) (design assets [also available as a PDF](docs/Now-In-Android-Design-File.pdf)).

@ -120,4 +120,16 @@ dependencies {
implementation(libs.androidx.profileinstaller) implementation(libs.androidx.profileinstaller)
implementation(libs.kotlinx.coroutines.guava) implementation(libs.kotlinx.coroutines.guava)
implementation(libs.coil.kt) implementation(libs.coil.kt)
// Core functions
testImplementation(project(":core:testing"))
testImplementation(project(":core:datastore-test"))
testImplementation(project(":core:data-test"))
testImplementation(project(":core:network"))
testImplementation(libs.androidx.navigation.testing)
testImplementation(libs.accompanist.testharness)
testImplementation(kotlin("test"))
implementation(libs.work.testing)
kaptTest(libs.hilt.compiler)
} }

@ -0,0 +1,242 @@
/*
* 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.ui
import android.util.Log
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
import androidx.compose.material3.windowsizeclass.WindowSizeClass
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onRoot
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import androidx.test.platform.app.InstrumentationRegistry
import androidx.work.Configuration
import androidx.work.testing.SynchronousExecutor
import androidx.work.testing.WorkManagerTestInitHelper
import com.github.takahirom.roborazzi.captureRoboImage
import com.google.accompanist.testharness.TestHarness
import com.google.samples.apps.nowinandroid.core.data.repository.TopicsRepository
import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository
import com.google.samples.apps.nowinandroid.core.data.repository.UserNewsResourceRepository
import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor
import com.google.samples.apps.nowinandroid.core.testing.util.DefaultRoborazziOptions
import com.google.samples.apps.nowinandroid.uitesthiltmanifest.HiltComponentActivity
import dagger.hilt.android.testing.BindValue
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.HiltTestApplication
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import org.robolectric.annotation.GraphicsMode
import org.robolectric.annotation.LooperMode
import java.util.TimeZone
import javax.inject.Inject
/**
* Tests that the navigation UI is rendered correctly on different screen sizes.
*/
@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
@RunWith(RobolectricTestRunner::class)
@GraphicsMode(GraphicsMode.Mode.NATIVE)
// Configure Robolectric to use a very large screen size that can fit all of the test sizes.
// This allows enough room to render the content under test without clipping or scaling.
@Config(application = HiltTestApplication::class, qualifiers = "w1000dp-h1000dp-480dpi", sdk = [33])
@LooperMode(LooperMode.Mode.PAUSED)
@HiltAndroidTest
class NiaAppScreenSizesScreenshotTests {
/**
* Manages the components' state and is used to perform injection on your test
*/
@get:Rule(order = 0)
val hiltRule = HiltAndroidRule(this)
/**
* Create a temporary folder used to create a Data Store file. This guarantees that
* the file is removed in between each test, preventing a crash.
*/
@BindValue
@get:Rule(order = 1)
val tmpFolder: TemporaryFolder = TemporaryFolder.builder().assureDeletion().build()
/**
* Use a test activity to set the content on.
*/
@get:Rule(order = 2)
val composeTestRule = createAndroidComposeRule<HiltComponentActivity>()
@Inject
lateinit var networkMonitor: NetworkMonitor
@Inject
lateinit var userDataRepository: UserDataRepository
@Inject
lateinit var topicsRepository: TopicsRepository
@Inject
lateinit var userNewsResourceRepository: UserNewsResourceRepository
@Before
fun setup() {
val config = Configuration.Builder()
.setMinimumLoggingLevel(Log.DEBUG)
.setExecutor(SynchronousExecutor())
.build()
// Initialize WorkManager for instrumentation tests.
WorkManagerTestInitHelper.initializeTestWorkManager(
InstrumentationRegistry.getInstrumentation().context,
config,
)
hiltRule.inject()
// Configure user data
runBlocking {
userDataRepository.setShouldHideOnboarding(true)
userDataRepository.setFollowedTopicIds(
setOf(topicsRepository.getTopics().first().first().id),
)
}
}
@Before
fun setTimeZone() {
// Make time zone deterministic in tests
TimeZone.setDefault(TimeZone.getTimeZone("UTC"))
}
private fun testNiaAppScreenshotWithSize(width: Dp, height: Dp, screenshotName: String) {
composeTestRule.setContent {
CompositionLocalProvider(
LocalInspectionMode provides true,
) {
TestHarness(size = DpSize(width, height)) {
BoxWithConstraints {
NiaApp(
windowSizeClass = WindowSizeClass.calculateFromSize(
DpSize(maxWidth, maxHeight),
),
networkMonitor = networkMonitor,
userNewsResourceRepository = userNewsResourceRepository,
)
}
}
}
}
composeTestRule.onRoot()
.captureRoboImage(
"src/testDemo/screenshots/$screenshotName.png",
roborazziOptions = DefaultRoborazziOptions,
)
}
@Test
fun compactWidth_compactHeight_showsNavigationBar() {
testNiaAppScreenshotWithSize(
610.dp,
400.dp,
"compactWidth_compactHeight_showsNavigationBar",
)
}
@Test
fun mediumWidth_compactHeight_showsNavigationRail() {
testNiaAppScreenshotWithSize(
610.dp,
400.dp,
"mediumWidth_compactHeight_showsNavigationRail",
)
}
@Test
fun expandedWidth_compactHeight_showsNavigationRail() {
testNiaAppScreenshotWithSize(
900.dp,
400.dp,
"expandedWidth_compactHeight_showsNavigationRail",
)
}
@Test
fun compactWidth_mediumHeight_showsNavigationBar() {
testNiaAppScreenshotWithSize(
400.dp,
500.dp,
"compactWidth_mediumHeight_showsNavigationBar",
)
}
@Test
fun mediumWidth_mediumHeight_showsNavigationRail() {
testNiaAppScreenshotWithSize(
610.dp,
500.dp,
"mediumWidth_mediumHeight_showsNavigationRail",
)
}
@Test
fun expandedWidth_mediumHeight_showsNavigationRail() {
testNiaAppScreenshotWithSize(
900.dp,
500.dp,
"expandedWidth_mediumHeight_showsNavigationRail",
)
}
@Test
fun compactWidth_expandedHeight_showsNavigationBar() {
testNiaAppScreenshotWithSize(
400.dp,
1000.dp,
"compactWidth_expandedHeight_showsNavigationBar",
)
}
@Test
fun mediumWidth_expandedHeight_showsNavigationRail() {
testNiaAppScreenshotWithSize(
610.dp,
1000.dp,
"mediumWidth_expandedHeight_showsNavigationRail",
)
}
@Test
fun expandedWidth_expandedHeight_showsNavigationRail() {
testNiaAppScreenshotWithSize(
900.dp,
1000.dp,
"expandedWidth_expandedHeight_showsNavigationRail",
)
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

@ -24,6 +24,9 @@ class AndroidApplicationComposeConventionPlugin : Plugin<Project> {
override fun apply(target: Project) { override fun apply(target: Project) {
with(target) { with(target) {
pluginManager.apply("com.android.application") pluginManager.apply("com.android.application")
// Screenshot Tests
pluginManager.apply("io.github.takahirom.roborazzi")
val extension = extensions.getByType<ApplicationExtension>() val extension = extensions.getByType<ApplicationExtension>()
configureAndroidCompose(extension) configureAndroidCompose(extension)
} }

@ -33,6 +33,7 @@ class AndroidHiltConventionPlugin : Plugin<Project> {
"implementation"(libs.findLibrary("hilt.android").get()) "implementation"(libs.findLibrary("hilt.android").get())
"kapt"(libs.findLibrary("hilt.compiler").get()) "kapt"(libs.findLibrary("hilt.compiler").get())
"kaptAndroidTest"(libs.findLibrary("hilt.compiler").get()) "kaptAndroidTest"(libs.findLibrary("hilt.compiler").get())
"kaptTest"(libs.findLibrary("hilt.compiler").get())
} }
} }

@ -24,6 +24,9 @@ class AndroidLibraryComposeConventionPlugin : Plugin<Project> {
override fun apply(target: Project) { override fun apply(target: Project) {
with(target) { with(target) {
pluginManager.apply("com.android.library") pluginManager.apply("com.android.library")
// Screenshot Tests
pluginManager.apply("io.github.takahirom.roborazzi")
val extension = extensions.getByType<LibraryExtension>() val extension = extensions.getByType<LibraryExtension>()
configureAndroidCompose(extension) configureAndroidCompose(extension)
} }

@ -41,6 +41,18 @@ internal fun Project.configureAndroidCompose(
val bom = libs.findLibrary("androidx-compose-bom").get() val bom = libs.findLibrary("androidx-compose-bom").get()
add("implementation", platform(bom)) add("implementation", platform(bom))
add("androidTestImplementation", platform(bom)) add("androidTestImplementation", platform(bom))
// Add ComponentActivity to debug manfest
add("debugImplementation", libs.findLibrary("androidx.compose.ui.testManifest").get())
// Screenshot Tests on JVM
add("testImplementation", libs.findLibrary("robolectric").get())
add("testImplementation", libs.findLibrary("roborazzi").get())
}
testOptions {
unitTests {
// For Robolectric
isIncludeAndroidResources = true
}
} }
} }

@ -39,5 +39,6 @@ plugins {
alias(libs.plugins.gms) apply false alias(libs.plugins.gms) apply false
alias(libs.plugins.hilt) apply false alias(libs.plugins.hilt) apply false
alias(libs.plugins.ksp) apply false alias(libs.plugins.ksp) apply false
alias(libs.plugins.roborazzi) apply false
alias(libs.plugins.secrets) apply false alias(libs.plugins.secrets) apply false
} }

@ -24,6 +24,8 @@ android {
} }
dependencies { dependencies {
api(libs.accompanist.testharness)
api(libs.androidx.activity.compose)
api(libs.androidx.compose.ui.test) api(libs.androidx.compose.ui.test)
api(libs.androidx.test.core) api(libs.androidx.test.core)
api(libs.androidx.test.espresso.core) api(libs.androidx.test.espresso.core)
@ -32,6 +34,8 @@ dependencies {
api(libs.hilt.android.testing) api(libs.hilt.android.testing)
api(libs.junit4) api(libs.junit4)
api(libs.kotlinx.coroutines.test) api(libs.kotlinx.coroutines.test)
api(libs.roborazzi)
api(libs.robolectric.shadows)
api(libs.turbine) api(libs.turbine)
debugApi(libs.androidx.compose.ui.testManifest) debugApi(libs.androidx.compose.ui.testManifest)

@ -0,0 +1,90 @@
/*
* Copyright 2023 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.testing.util
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.compose.ui.test.onRoot
import androidx.test.ext.junit.rules.ActivityScenarioRule
import com.github.takahirom.roborazzi.RoborazziOptions
import com.github.takahirom.roborazzi.RoborazziOptions.CompareOptions
import com.github.takahirom.roborazzi.RoborazziOptions.RecordOptions
import com.github.takahirom.roborazzi.captureRoboImage
import org.robolectric.RuntimeEnvironment
val DefaultRoborazziOptions =
RoborazziOptions(
compareOptions = CompareOptions(changeThreshold = 0f), // Pixel-perfect matching
recordOptions = RecordOptions(resizeScale = 0.5), // Reduce the size of the PNGs
)
fun <A : ComponentActivity> AndroidComposeTestRule<ActivityScenarioRule<A>, A>.captureMultiDevice(
screenshotName: String,
body: @Composable () -> Unit,
) {
listOf(
"phone" to "spec:shape=Normal,width=640,height=360,unit=dp,dpi=480",
"foldable" to "spec:shape=Normal,width=673,height=841,unit=dp,dpi=480",
"tablet" to "spec:shape=Normal,width=1280,height=800,unit=dp,dpi=480",
).forEach {
this.captureForDevice(it.first, it.second, screenshotName, body)
}
}
fun <A : ComponentActivity> AndroidComposeTestRule<ActivityScenarioRule<A>, A>.captureForDevice(
deviceName: String,
deviceSpec: String,
screenshotName: String,
body: @Composable () -> Unit,
roborazziOptions: RoborazziOptions = DefaultRoborazziOptions,
) {
val (width, height, dpi) = extractSpecs(deviceSpec)
// Set qualifiers from specs
RuntimeEnvironment.setQualifiers("w${width}dp-h${height}dp-${dpi}dpi")
this.activity.setContent {
CompositionLocalProvider(
LocalInspectionMode provides true,
) {
body()
}
}
this.onRoot()
.captureRoboImage(
"src/test/screenshots/${screenshotName}_$deviceName.png",
roborazziOptions = roborazziOptions,
)
}
/**
* Extracts some properties from the spec string. Note that this function is not exhaustive.
*/
private fun extractSpecs(deviceSpec: String): TestDeviceSpecs {
val specs = deviceSpec.substringAfter("spec:")
.split(",").map { it.split("=") }.associate { it[0] to it[1] }
val width = specs["width"]?.toInt() ?: 640
val height = specs["height"]?.toInt() ?: 480
val dpi = specs["dpi"]?.toInt() ?: 480
return TestDeviceSpecs(width, height, dpi)
}
data class TestDeviceSpecs(val width: Int, val height: Int, val dpi: Int)

@ -44,6 +44,7 @@ dependencies {
implementation(project(":core:designsystem")) implementation(project(":core:designsystem"))
implementation(project(":core:domain")) implementation(project(":core:domain"))
implementation(project(":core:model")) implementation(project(":core:model"))
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.browser) implementation(libs.androidx.browser)
implementation(libs.androidx.core.ktx) implementation(libs.androidx.core.ktx)
implementation(libs.coil.kt) implementation(libs.coil.kt)

@ -0,0 +1,150 @@
/*
* Copyright 2023 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.ui.test.junit4.createAndroidComposeRule
import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme
import com.google.samples.apps.nowinandroid.core.testing.util.captureMultiDevice
import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState
import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState.Success
import com.google.samples.apps.nowinandroid.core.ui.UserNewsResourcePreviewParameterProvider
import com.google.samples.apps.nowinandroid.feature.foryou.OnboardingUiState.Loading
import com.google.samples.apps.nowinandroid.feature.foryou.OnboardingUiState.NotShown
import dagger.hilt.android.testing.HiltTestApplication
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import org.robolectric.annotation.GraphicsMode
import org.robolectric.annotation.LooperMode
import java.util.TimeZone
/**
* Screenshot tests for the [ForYouScreen].
*/
@RunWith(RobolectricTestRunner::class)
@GraphicsMode(GraphicsMode.Mode.NATIVE)
@Config(application = HiltTestApplication::class, sdk = [33])
@LooperMode(LooperMode.Mode.PAUSED)
class ForYouScreenScreenshotTests {
/**
* Use a test activity to set the content on.
*/
@get:Rule
val composeTestRule = createAndroidComposeRule<ComponentActivity>()
private val userNewsResources = UserNewsResourcePreviewParameterProvider().values.first()
@Before
fun setTimeZone() {
// Make time zone deterministic in tests
TimeZone.setDefault(TimeZone.getTimeZone("UTC"))
}
@Test
fun testForYouScreenPopulatedFeed() {
composeTestRule.captureMultiDevice("ForYouScreenPopulatedFeed") {
NiaTheme {
ForYouScreen(
isSyncing = false,
onboardingUiState = NotShown,
feedState = Success(
feed = userNewsResources,
),
onTopicCheckedChanged = { _, _ -> },
saveFollowedTopics = {},
onNewsResourcesCheckedChanged = { _, _ -> },
onNewsResourceViewed = {},
onTopicClick = {},
deepLinkedUserNewsResource = null,
onDeepLinkOpened = {},
)
}
}
}
@Test
fun testForYouScreenLoading() {
composeTestRule.captureMultiDevice("ForYouScreenLoading") {
NiaTheme {
ForYouScreen(
isSyncing = false,
onboardingUiState = Loading,
feedState = NewsFeedUiState.Loading,
onTopicCheckedChanged = { _, _ -> },
saveFollowedTopics = {},
onNewsResourcesCheckedChanged = { _, _ -> },
onNewsResourceViewed = {},
onTopicClick = {},
deepLinkedUserNewsResource = null,
onDeepLinkOpened = {},
)
}
}
}
@Test
fun testForYouScreenTopicSelection() {
composeTestRule.captureMultiDevice("ForYouScreenTopicSelection") {
NiaTheme {
ForYouScreen(
isSyncing = false,
onboardingUiState = OnboardingUiState.Shown(
topics = userNewsResources.flatMap { news -> news.followableTopics }
.distinctBy { it.topic.id },
),
feedState = NewsFeedUiState.Success(
feed = userNewsResources,
),
onTopicCheckedChanged = { _, _ -> },
saveFollowedTopics = {},
onNewsResourcesCheckedChanged = { _, _ -> },
onNewsResourceViewed = {},
onTopicClick = {},
deepLinkedUserNewsResource = null,
onDeepLinkOpened = {},
)
}
}
}
@Test
fun testForYouScreenPopulatedAndLoading() {
composeTestRule.captureMultiDevice("ForYouScreenPopulatedAndLoading") {
NiaTheme {
ForYouScreen(
isSyncing = true,
onboardingUiState = OnboardingUiState.Loading,
feedState = NewsFeedUiState.Success(
feed = userNewsResources,
),
onTopicCheckedChanged = { _, _ -> },
saveFollowedTopics = {},
onNewsResourcesCheckedChanged = { _, _ -> },
onNewsResourceViewed = {},
onTopicClick = {},
deepLinkedUserNewsResource = null,
onDeepLinkOpened = {},
)
}
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 221 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

@ -49,6 +49,8 @@ protobuf = "3.24.0"
protobufPlugin = "0.9.4" protobufPlugin = "0.9.4"
retrofit = "2.9.0" retrofit = "2.9.0"
retrofitKotlinxSerializationJson = "1.0.0" retrofitKotlinxSerializationJson = "1.0.0"
robolectric = "4.10.3"
roborazzi = "1.5.0-alpha-2"
room = "2.5.2" room = "2.5.2"
secrets = "2.0.1" secrets = "2.0.1"
turbine = "0.12.1" turbine = "0.12.1"
@ -125,6 +127,9 @@ protobuf-kotlin-lite = { group = "com.google.protobuf", name = "protobuf-kotlin-
protobuf-protoc = { group = "com.google.protobuf", name = "protoc", version.ref = "protobuf" } protobuf-protoc = { group = "com.google.protobuf", name = "protoc", version.ref = "protobuf" }
retrofit-core = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" } retrofit-core = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
retrofit-kotlin-serialization = { group = "com.jakewharton.retrofit", name = "retrofit2-kotlinx-serialization-converter", version.ref = "retrofitKotlinxSerializationJson" } retrofit-kotlin-serialization = { group = "com.jakewharton.retrofit", name = "retrofit2-kotlinx-serialization-converter", version.ref = "retrofitKotlinxSerializationJson" }
robolectric = { group = "org.robolectric", name = "robolectric", version.ref = "robolectric" }
robolectric-shadows = { group = "org.robolectric", name = "shadows-framework", version.ref = "robolectric" }
roborazzi = { group = "io.github.takahirom.roborazzi", name = "roborazzi", version.ref = "roborazzi" }
room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" } room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }
room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" } room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" }
room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" } room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }
@ -136,6 +141,7 @@ firebase-crashlytics-gradlePlugin = { group = "com.google.firebase", name = "fir
firebase-performance-gradlePlugin = { group = "com.google.firebase", name = "perf-plugin", version.ref = "firebasePerfPlugin" } firebase-performance-gradlePlugin = { group = "com.google.firebase", name = "perf-plugin", version.ref = "firebasePerfPlugin" }
kotlin-gradlePlugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin" } kotlin-gradlePlugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin" }
ksp-gradlePlugin = { group = "com.google.devtools.ksp", name = "com.google.devtools.ksp.gradle.plugin", version.ref = "ksp" } ksp-gradlePlugin = { group = "com.google.devtools.ksp", name = "com.google.devtools.ksp.gradle.plugin", version.ref = "ksp" }
work-testing = { group = "androidx.work", name = "work-testing", version = "2.8.1" }
[plugins] [plugins]
android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" } android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" }
@ -149,4 +155,5 @@ kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
protobuf = { id = "com.google.protobuf", version.ref = "protobufPlugin" } protobuf = { id = "com.google.protobuf", version.ref = "protobufPlugin" }
roborazzi = { id = "io.github.takahirom.roborazzi", version.ref = "roborazzi" }
secrets = { id = "com.google.android.libraries.mapsplatform.secrets-gradle-plugin", version.ref = "secrets" } secrets = { id = "com.google.android.libraries.mapsplatform.secrets-gradle-plugin", version.ref = "secrets" }

Loading…
Cancel
Save