diff --git a/.github/workflows/Build.yaml b/.github/workflows/Build.yaml index 01280ba501..13a1205188 100644 --- a/.github/workflows/Build.yaml +++ b/.github/workflows/Build.yaml @@ -140,6 +140,13 @@ jobs: name: local-test-results path: '**/build/test-results/test*UnitTest/**.xml' + - name: Upload screenshot results (PNG) + if: always() + uses: actions/upload-artifact@v4 + with: + name: screenshot-test-results + path: '**/build/outputs/roborazzi/*_compare.png' + - name: Check lint run: ./now-in-android/gradlew :app:lintProdRelease :app-nia-catalog:lintRelease :lint:lint -p now-in-android diff --git a/app/build.gradle.kts b/app/build.gradle.kts index afd37736f2..ae1c0d6865 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -113,6 +113,7 @@ dependencies { testImplementation(projects.core.dataTest) testImplementation(projects.core.testing) + testImplementation(projects.sync.syncTest) testImplementation(libs.androidx.compose.ui.test) testImplementation(libs.hilt.android.testing) testImplementation(libs.work.testing) diff --git a/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppScreenSizesScreenshotTests.kt b/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppScreenSizesScreenshotTests.kt index e843352468..32bdfe9d9b 100644 --- a/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppScreenSizesScreenshotTests.kt +++ b/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppScreenSizesScreenshotTests.kt @@ -16,7 +16,6 @@ package com.google.samples.apps.nowinandroid.ui -import android.util.Log import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi import androidx.compose.material3.windowsizeclass.WindowSizeClass import androidx.compose.runtime.CompositionLocalProvider @@ -28,10 +27,6 @@ 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.samples.apps.nowinandroid.core.data.repository.TopicsRepository import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository @@ -109,17 +104,6 @@ class NiaAppScreenSizesScreenshotTests { @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 diff --git a/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/SnackbarScreenshotTests.kt b/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/SnackbarScreenshotTests.kt index c3ad185912..ab67f8399d 100644 --- a/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/SnackbarScreenshotTests.kt +++ b/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/SnackbarScreenshotTests.kt @@ -16,13 +16,14 @@ package com.google.samples.apps.nowinandroid.ui -import android.util.Log import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.material3.SnackbarDuration.Indefinite import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi import androidx.compose.material3.windowsizeclass.WindowSizeClass +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.test.DeviceConfigurationOverride import androidx.compose.ui.test.ForcedSize import androidx.compose.ui.test.junit4.createAndroidComposeRule @@ -30,10 +31,6 @@ 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.samples.apps.nowinandroid.core.data.repository.TopicsRepository import com.google.samples.apps.nowinandroid.core.data.repository.UserNewsResourceRepository @@ -60,6 +57,7 @@ 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 /** @@ -112,17 +110,6 @@ class SnackbarScreenshotTests { @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 @@ -135,6 +122,12 @@ class SnackbarScreenshotTests { } } + @Before + fun setTimeZone() { + // Make time zone deterministic in tests + TimeZone.setDefault(TimeZone.getTimeZone("UTC")) + } + @Test fun phone_noSnackbar() { val snackbarHostState = SnackbarHostState() @@ -207,22 +200,27 @@ class SnackbarScreenshotTests { ) { lateinit var scope: CoroutineScope composeTestRule.setContent { - scope = rememberCoroutineScope() - - DeviceConfigurationOverride( - DeviceConfigurationOverride.ForcedSize(DpSize(width, height)), + CompositionLocalProvider( + // Replaces images with placeholders + LocalInspectionMode provides true, ) { - BoxWithConstraints { - val appState = rememberNiaAppState( - windowSizeClass = WindowSizeClass.calculateFromSize( - DpSize(maxWidth, maxHeight), - ), - networkMonitor = networkMonitor, - userNewsResourceRepository = userNewsResourceRepository, - timeZoneMonitor = timeZoneMonitor, - ) - NiaTheme { - NiaApp(appState, snackbarHostState, false, {}, {}) + scope = rememberCoroutineScope() + + DeviceConfigurationOverride( + DeviceConfigurationOverride.ForcedSize(DpSize(width, height)), + ) { + BoxWithConstraints { + val appState = rememberNiaAppState( + windowSizeClass = WindowSizeClass.calculateFromSize( + DpSize(maxWidth, maxHeight), + ), + networkMonitor = networkMonitor, + userNewsResourceRepository = userNewsResourceRepository, + timeZoneMonitor = timeZoneMonitor, + ) + NiaTheme { + NiaApp(appState, snackbarHostState, false, {}, {}) + } } } } diff --git a/app/src/testDemo/screenshots/snackbar_compact_medium.png b/app/src/testDemo/screenshots/snackbar_compact_medium.png index 70d15deb3c..147c9ce6b0 100644 Binary files a/app/src/testDemo/screenshots/snackbar_compact_medium.png and b/app/src/testDemo/screenshots/snackbar_compact_medium.png differ diff --git a/app/src/testDemo/screenshots/snackbar_compact_medium_noSnackbar.png b/app/src/testDemo/screenshots/snackbar_compact_medium_noSnackbar.png index 9c3eb133d7..2767ff9b55 100644 Binary files a/app/src/testDemo/screenshots/snackbar_compact_medium_noSnackbar.png and b/app/src/testDemo/screenshots/snackbar_compact_medium_noSnackbar.png differ diff --git a/app/src/testDemo/screenshots/snackbar_expanded_expanded.png b/app/src/testDemo/screenshots/snackbar_expanded_expanded.png index e4c7520abb..5360d68de1 100644 Binary files a/app/src/testDemo/screenshots/snackbar_expanded_expanded.png and b/app/src/testDemo/screenshots/snackbar_expanded_expanded.png differ diff --git a/app/src/testDemo/screenshots/snackbar_medium_medium.png b/app/src/testDemo/screenshots/snackbar_medium_medium.png index a9f131fd71..b601121019 100644 Binary files a/app/src/testDemo/screenshots/snackbar_medium_medium.png and b/app/src/testDemo/screenshots/snackbar_medium_medium.png differ diff --git a/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/AndroidCompose.kt b/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/AndroidCompose.kt index 234313e1fc..e38c5b3009 100644 --- a/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/AndroidCompose.kt +++ b/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/AndroidCompose.kt @@ -57,6 +57,7 @@ internal fun Project.configureAndroidCompose( kotlinOptions { freeCompilerArgs += buildComposeMetricsParameters() freeCompilerArgs += stabilityConfiguration() + freeCompilerArgs += strongSkippingConfiguration() } } } @@ -84,6 +85,7 @@ private fun Project.buildComposeMetricsParameters(): List { "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=" + reportsFolder.absolutePath ) } + return metricParameters.toList() } @@ -91,3 +93,8 @@ private fun Project.stabilityConfiguration() = listOf( "-P", "plugin:androidx.compose.compiler.plugins.kotlin:stabilityConfigurationPath=${project.rootDir.absolutePath}/compose_compiler_config.conf", ) + +private fun Project.strongSkippingConfiguration() = listOf( + "-P", + "plugin:androidx.compose.compiler.plugins.kotlin:experimentalStrongSkipping=true", +) diff --git a/compose_compiler_config.conf b/compose_compiler_config.conf index 2341256f43..d47946206e 100644 --- a/compose_compiler_config.conf +++ b/compose_compiler_config.conf @@ -2,5 +2,10 @@ // It allows us to define classes that our not part of our codebase without wrapping them in a stable class. // For more information, check https://developer.android.com/jetpack/compose/performance/stability/fix#configuration-file +// We always use immutable classes for our data model, to avoid running the Compose compiler +// in the module we declare it to be stable here. +com.google.samples.apps.nowinandroid.core.model.data.* + +// Java standard library classes java.time.ZoneId java.time.ZoneOffset diff --git a/sync/sync-test/src/main/kotlin/com/google/samples/apps/nowinandroid/core/sync/test/TestSyncModule.kt b/sync/sync-test/src/main/kotlin/com/google/samples/apps/nowinandroid/core/sync/test/TestSyncModule.kt index d1c0f562f0..ceca1cb5cd 100644 --- a/sync/sync-test/src/main/kotlin/com/google/samples/apps/nowinandroid/core/sync/test/TestSyncModule.kt +++ b/sync/sync-test/src/main/kotlin/com/google/samples/apps/nowinandroid/core/sync/test/TestSyncModule.kt @@ -18,6 +18,8 @@ package com.google.samples.apps.nowinandroid.core.sync.test import com.google.samples.apps.nowinandroid.core.data.util.SyncManager import com.google.samples.apps.nowinandroid.sync.di.SyncModule +import com.google.samples.apps.nowinandroid.sync.status.StubSyncSubscriber +import com.google.samples.apps.nowinandroid.sync.status.SyncSubscriber import dagger.Binds import dagger.Module import dagger.hilt.components.SingletonComponent @@ -33,4 +35,9 @@ internal interface TestSyncModule { fun bindsSyncStatusMonitor( syncStatusMonitor: NeverSyncingSyncManager, ): SyncManager + + @Binds + fun bindsSyncSubscriber( + syncSubscriber: StubSyncSubscriber, + ): SyncSubscriber } diff --git a/sync/work/src/main/kotlin/com/google/samples/apps/nowinandroid/sync/status/StubSyncSubscriber.kt b/sync/work/src/main/kotlin/com/google/samples/apps/nowinandroid/sync/status/StubSyncSubscriber.kt index 83286eeec1..0ef90fb29c 100644 --- a/sync/work/src/main/kotlin/com/google/samples/apps/nowinandroid/sync/status/StubSyncSubscriber.kt +++ b/sync/work/src/main/kotlin/com/google/samples/apps/nowinandroid/sync/status/StubSyncSubscriber.kt @@ -24,7 +24,7 @@ private const val TAG = "StubSyncSubscriber" /** * Stub implementation of [SyncSubscriber] */ -internal class StubSyncSubscriber @Inject constructor() : SyncSubscriber { +class StubSyncSubscriber @Inject constructor() : SyncSubscriber { override suspend fun subscribe() { Log.d(TAG, "Subscribing to sync") }