diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 34b5d7b95..afd37736f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -100,6 +100,7 @@ dependencies { implementation(libs.androidx.navigation.compose) implementation(libs.androidx.profileinstaller) implementation(libs.androidx.tracing.ktx) + implementation(libs.androidx.window.core) implementation(libs.kotlinx.coroutines.guava) implementation(libs.coil.kt) diff --git a/app/dependencies/prodReleaseRuntimeClasspath.txt b/app/dependencies/prodReleaseRuntimeClasspath.txt index 85adaf6fc..7cd6df2d2 100644 --- a/app/dependencies/prodReleaseRuntimeClasspath.txt +++ b/app/dependencies/prodReleaseRuntimeClasspath.txt @@ -2,8 +2,8 @@ androidx.activity:activity-compose:1.8.0 androidx.activity:activity-ktx:1.8.0 androidx.activity:activity:1.8.0 androidx.annotation:annotation-experimental:1.4.0 -androidx.annotation:annotation-jvm:1.7.1 -androidx.annotation:annotation:1.7.1 +androidx.annotation:annotation-jvm:1.8.0-beta01 +androidx.annotation:annotation:1.8.0-beta01 androidx.appcompat:appcompat-resources:1.6.1 androidx.appcompat:appcompat:1.6.1 androidx.arch.core:core-common:2.2.0 @@ -13,20 +13,20 @@ androidx.browser:browser:1.8.0 androidx.collection:collection-jvm:1.4.0 androidx.collection:collection-ktx:1.4.0 androidx.collection:collection:1.4.0 -androidx.compose.animation:animation-android:1.6.3 -androidx.compose.animation:animation-core-android:1.6.3 -androidx.compose.animation:animation-core:1.6.3 -androidx.compose.animation:animation:1.6.3 +androidx.compose.animation:animation-android:1.7.0-alpha06 +androidx.compose.animation:animation-core-android:1.7.0-alpha06 +androidx.compose.animation:animation-core:1.7.0-alpha06 +androidx.compose.animation:animation:1.7.0-alpha06 androidx.compose.foundation:foundation-android:1.6.3 androidx.compose.foundation:foundation-layout-android:1.6.3 androidx.compose.foundation:foundation-layout:1.6.3 androidx.compose.foundation:foundation:1.6.3 -androidx.compose.material3.adaptive:adaptive-android:1.0.0-alpha08 -androidx.compose.material3.adaptive:adaptive-layout-android:1.0.0-alpha08 -androidx.compose.material3.adaptive:adaptive-layout:1.0.0-alpha08 -androidx.compose.material3.adaptive:adaptive-navigation-android:1.0.0-alpha08 -androidx.compose.material3.adaptive:adaptive-navigation:1.0.0-alpha08 -androidx.compose.material3.adaptive:adaptive:1.0.0-alpha08 +androidx.compose.material3.adaptive:adaptive-android:1.0.0-alpha10 +androidx.compose.material3.adaptive:adaptive-layout-android:1.0.0-alpha10 +androidx.compose.material3.adaptive:adaptive-layout:1.0.0-alpha10 +androidx.compose.material3.adaptive:adaptive-navigation-android:1.0.0-alpha10 +androidx.compose.material3.adaptive:adaptive-navigation:1.0.0-alpha10 +androidx.compose.material3.adaptive:adaptive:1.0.0-alpha10 androidx.compose.material3:material3-android:1.2.1 androidx.compose.material3:material3-window-size-class-android:1.2.1 androidx.compose.material3:material3-window-size-class:1.2.1 @@ -37,25 +37,25 @@ androidx.compose.material:material-icons-extended-android:1.6.3 androidx.compose.material:material-icons-extended:1.6.3 androidx.compose.material:material-ripple-android:1.6.3 androidx.compose.material:material-ripple:1.6.3 -androidx.compose.runtime:runtime-android:1.6.3 -androidx.compose.runtime:runtime-saveable-android:1.6.3 -androidx.compose.runtime:runtime-saveable:1.6.3 +androidx.compose.runtime:runtime-android:1.7.0-alpha06 +androidx.compose.runtime:runtime-saveable-android:1.7.0-alpha06 +androidx.compose.runtime:runtime-saveable:1.7.0-alpha06 androidx.compose.runtime:runtime-tracing:1.0.0-beta01 -androidx.compose.runtime:runtime:1.6.3 -androidx.compose.ui:ui-android:1.6.3 -androidx.compose.ui:ui-geometry-android:1.6.3 -androidx.compose.ui:ui-geometry:1.6.3 -androidx.compose.ui:ui-graphics-android:1.6.3 -androidx.compose.ui:ui-graphics:1.6.3 -androidx.compose.ui:ui-text-android:1.6.3 -androidx.compose.ui:ui-text:1.6.3 -androidx.compose.ui:ui-tooling-preview-android:1.6.3 -androidx.compose.ui:ui-tooling-preview:1.6.3 -androidx.compose.ui:ui-unit-android:1.6.3 -androidx.compose.ui:ui-unit:1.6.3 -androidx.compose.ui:ui-util-android:1.6.3 -androidx.compose.ui:ui-util:1.6.3 -androidx.compose.ui:ui:1.6.3 +androidx.compose.runtime:runtime:1.7.0-alpha06 +androidx.compose.ui:ui-android:1.7.0-alpha06 +androidx.compose.ui:ui-geometry-android:1.7.0-alpha06 +androidx.compose.ui:ui-geometry:1.7.0-alpha06 +androidx.compose.ui:ui-graphics-android:1.7.0-alpha06 +androidx.compose.ui:ui-graphics:1.7.0-alpha06 +androidx.compose.ui:ui-text-android:1.7.0-alpha06 +androidx.compose.ui:ui-text:1.7.0-alpha06 +androidx.compose.ui:ui-tooling-preview-android:1.7.0-alpha06 +androidx.compose.ui:ui-tooling-preview:1.7.0-alpha06 +androidx.compose.ui:ui-unit-android:1.7.0-alpha06 +androidx.compose.ui:ui-unit:1.7.0-alpha06 +androidx.compose.ui:ui-util-android:1.7.0-alpha06 +androidx.compose.ui:ui-util:1.7.0-alpha06 +androidx.compose.ui:ui:1.7.0-alpha06 androidx.compose:compose-bom:2024.02.02 androidx.concurrent:concurrent-futures:1.1.0 androidx.core:core-ktx:1.12.0 @@ -74,26 +74,32 @@ androidx.emoji2:emoji2-views-helper:1.3.0 androidx.emoji2:emoji2:1.3.0 androidx.exifinterface:exifinterface:1.3.7 androidx.fragment:fragment:1.5.1 +androidx.graphics:graphics-path:1.0.0-beta02 androidx.hilt:hilt-common:1.1.0 androidx.hilt:hilt-navigation-compose:1.2.0 androidx.hilt:hilt-navigation:1.2.0 androidx.hilt:hilt-work:1.1.0 androidx.interpolator:interpolator:1.0.0 androidx.legacy:legacy-support-core-utils:1.0.0 -androidx.lifecycle:lifecycle-common-java8:2.7.0 -androidx.lifecycle:lifecycle-common:2.7.0 -androidx.lifecycle:lifecycle-livedata-core-ktx:2.7.0 -androidx.lifecycle:lifecycle-livedata-core:2.7.0 -androidx.lifecycle:lifecycle-livedata:2.7.0 -androidx.lifecycle:lifecycle-process:2.7.0 -androidx.lifecycle:lifecycle-runtime-compose:2.7.0 -androidx.lifecycle:lifecycle-runtime-ktx:2.7.0 -androidx.lifecycle:lifecycle-runtime:2.7.0 -androidx.lifecycle:lifecycle-service:2.7.0 -androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0 -androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0 -androidx.lifecycle:lifecycle-viewmodel-savedstate:2.7.0 -androidx.lifecycle:lifecycle-viewmodel:2.7.0 +androidx.lifecycle:lifecycle-common-java8:2.8.0-alpha04 +androidx.lifecycle:lifecycle-common-jvm:2.8.0-alpha04 +androidx.lifecycle:lifecycle-common:2.8.0-alpha04 +androidx.lifecycle:lifecycle-livedata-core-ktx:2.8.0-alpha04 +androidx.lifecycle:lifecycle-livedata-core:2.8.0-alpha04 +androidx.lifecycle:lifecycle-livedata:2.8.0-alpha04 +androidx.lifecycle:lifecycle-process:2.8.0-alpha04 +androidx.lifecycle:lifecycle-runtime-android:2.8.0-alpha04 +androidx.lifecycle:lifecycle-runtime-compose:2.8.0-alpha04 +androidx.lifecycle:lifecycle-runtime-ktx-android:2.8.0-alpha04 +androidx.lifecycle:lifecycle-runtime-ktx:2.8.0-alpha04 +androidx.lifecycle:lifecycle-runtime:2.8.0-alpha04 +androidx.lifecycle:lifecycle-service:2.8.0-alpha04 +androidx.lifecycle:lifecycle-viewmodel-android:2.8.0-alpha04 +androidx.lifecycle:lifecycle-viewmodel-compose-android:2.8.0-alpha04 +androidx.lifecycle:lifecycle-viewmodel-compose:2.8.0-alpha04 +androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.0-alpha04 +androidx.lifecycle:lifecycle-viewmodel-savedstate:2.8.0-alpha04 +androidx.lifecycle:lifecycle-viewmodel:2.8.0-alpha04 androidx.loader:loader:1.0.0 androidx.localbroadcastmanager:localbroadcastmanager:1.0.0 androidx.metrics:metrics-performance:1.0.0-alpha04 @@ -123,9 +129,9 @@ androidx.vectordrawable:vectordrawable:1.1.0 androidx.versionedparcelable:versionedparcelable:1.1.1 androidx.viewpager:viewpager:1.0.0 androidx.window.extensions.core:core:1.0.0 -androidx.window:window-core-android:1.3.0-alpha02 -androidx.window:window-core:1.3.0-alpha02 -androidx.window:window:1.3.0-alpha02 +androidx.window:window-core-android:1.3.0-alpha03 +androidx.window:window-core:1.3.0-alpha03 +androidx.window:window:1.3.0-alpha03 androidx.work:work-runtime-ktx:2.9.0 androidx.work:work-runtime:2.9.0 com.caverock:androidsvg-aar:1.4 diff --git a/app/prodRelease-badging.txt b/app/prodRelease-badging.txt index 9ae76fff2..769e0a6e4 100644 --- a/app/prodRelease-badging.txt +++ b/app/prodRelease-badging.txt @@ -6,9 +6,9 @@ uses-permission: name='android.permission.ACCESS_NETWORK_STATE' uses-permission: name='android.permission.POST_NOTIFICATIONS' uses-permission: name='android.permission.WAKE_LOCK' uses-permission: name='com.google.android.c2dm.permission.RECEIVE' -uses-permission: name='com.google.android.finsky.permission.BIND_GET_INSTALL_REFERRER_SERVICE' uses-permission: name='android.permission.RECEIVE_BOOT_COMPLETED' uses-permission: name='android.permission.FOREGROUND_SERVICE' +uses-permission: name='com.google.android.finsky.permission.BIND_GET_INSTALL_REFERRER_SERVICE' uses-permission: name='com.google.samples.apps.nowinandroid.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION' application-label:'Now in Android' application-label-af:'Now in Android' @@ -105,9 +105,9 @@ application-icon-640:'res/mipmap-anydpi-v26/ic_launcher.xml' application-icon-65534:'res/mipmap-anydpi-v26/ic_launcher.xml' application: label='Now in Android' icon='res/mipmap-anydpi-v26/ic_launcher.xml' launchable-activity: name='com.google.samples.apps.nowinandroid.MainActivity' label='' icon='' -uses-library-not-required:'android.ext.adservices' uses-library-not-required:'androidx.window.extensions' uses-library-not-required:'androidx.window.sidecar' +uses-library-not-required:'android.ext.adservices' feature-group: label='' uses-feature: name='android.hardware.faketouch' uses-implied-feature: name='android.hardware.faketouch' reason='default feature for all apps' @@ -119,3 +119,4 @@ supports-screens: 'small' 'normal' 'large' 'xlarge' supports-any-density: 'true' locales: '--_--' 'af' 'am' 'ar' 'as' 'az' 'be' 'bg' 'bn' 'bs' 'ca' 'cs' 'da' 'de' 'el' 'en-AU' 'en-CA' 'en-GB' 'en-IN' 'en-XC' 'es' 'es-US' 'et' 'eu' 'fa' 'fi' 'fr' 'fr-CA' 'gl' 'gu' 'hi' 'hr' 'hu' 'hy' 'in' 'is' 'it' 'iw' 'ja' 'ka' 'kk' 'km' 'kn' 'ko' 'ky' 'lo' 'lt' 'lv' 'mk' 'ml' 'mn' 'mr' 'ms' 'my' 'nb' 'ne' 'nl' 'or' 'pa' 'pl' 'pt' 'pt-BR' 'pt-PT' 'ro' 'ru' 'si' 'sk' 'sl' 'sq' 'sr' 'sr-Latn' 'sv' 'sw' 'ta' 'te' 'th' 'tl' 'tr' 'uk' 'ur' 'uz' 'vi' 'zh-CN' 'zh-HK' 'zh-TW' 'zu' densities: '120' '160' '240' '320' '480' '640' '65534' +native-code: 'arm64-v8a' 'armeabi-v7a' 'x86' 'x86_64' diff --git a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ui/NiaApp.kt b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ui/NiaApp.kt index 4658e3609..8cbabc247 100644 --- a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ui/NiaApp.kt +++ b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ui/NiaApp.kt @@ -75,10 +75,6 @@ import com.google.samples.apps.nowinandroid.navigation.NiaNavHost import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination import com.google.samples.apps.nowinandroid.feature.settings.R as settingsR -@OptIn( - ExperimentalMaterial3Api::class, - ExperimentalComposeUiApi::class, -) @Composable fun NiaApp(appState: NiaAppState, modifier: Modifier = Modifier) { val shouldShowGradientBackground = @@ -108,95 +104,122 @@ fun NiaApp(appState: NiaAppState, modifier: Modifier = Modifier) { } } - if (showSettingsDialog) { - SettingsDialog( - onDismiss = { showSettingsDialog = false }, + NiaApp( + appState = appState, + snackbarHostState = snackbarHostState, + showSettingsDialog = showSettingsDialog, + onSettingsDismissed = { showSettingsDialog = false }, + onTopAppBarActionClick = { showSettingsDialog = true }, + ) + } + } +} + +@Composable +@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class) +internal fun NiaApp( + appState: NiaAppState, + snackbarHostState: SnackbarHostState, + showSettingsDialog: Boolean, + onSettingsDismissed: () -> Unit, + onTopAppBarActionClick: () -> Unit, + modifier: Modifier = Modifier, +) { + val unreadDestinations by appState.topLevelDestinationsWithUnreadResources + .collectAsStateWithLifecycle() + + if (showSettingsDialog) { + SettingsDialog( + onDismiss = { onSettingsDismissed() }, + ) + } + Scaffold( + modifier = modifier.semantics { + testTagsAsResourceId = true + }, + containerColor = Color.Transparent, + contentColor = MaterialTheme.colorScheme.onBackground, + contentWindowInsets = WindowInsets(0, 0, 0, 0), + snackbarHost = { SnackbarHost(snackbarHostState) }, + bottomBar = { + if (appState.shouldShowBottomBar) { + NiaBottomBar( + destinations = appState.topLevelDestinations, + destinationsWithUnreadResources = unreadDestinations, + onNavigateToDestination = appState::navigateToTopLevelDestination, + currentDestination = appState.currentDestination, + modifier = Modifier.testTag("NiaBottomBar"), + ) + } + }, + ) { padding -> + Row( + Modifier + .fillMaxSize() + .padding(padding) + .consumeWindowInsets(padding) + .windowInsetsPadding( + WindowInsets.safeDrawing.only( + WindowInsetsSides.Horizontal, + ), + ), + ) { + if (appState.shouldShowNavRail) { + NiaNavRail( + destinations = appState.topLevelDestinations, + destinationsWithUnreadResources = unreadDestinations, + onNavigateToDestination = appState::navigateToTopLevelDestination, + currentDestination = appState.currentDestination, + modifier = Modifier + .testTag("NiaNavRail") + .safeDrawingPadding(), ) } - val unreadDestinations by appState.topLevelDestinationsWithUnreadResources.collectAsStateWithLifecycle() - - Scaffold( - modifier = Modifier.semantics { - testTagsAsResourceId = true - }, - containerColor = Color.Transparent, - contentColor = MaterialTheme.colorScheme.onBackground, - contentWindowInsets = WindowInsets(0, 0, 0, 0), - snackbarHost = { SnackbarHost(snackbarHostState) }, - bottomBar = { - if (appState.shouldShowBottomBar) { - NiaBottomBar( - destinations = appState.topLevelDestinations, - destinationsWithUnreadResources = unreadDestinations, - onNavigateToDestination = appState::navigateToTopLevelDestination, - currentDestination = appState.currentDestination, - modifier = Modifier.testTag("NiaBottomBar"), - ) - } - }, - ) { padding -> - Row( - Modifier - .fillMaxSize() - .padding(padding) - .consumeWindowInsets(padding) - .windowInsetsPadding( - WindowInsets.safeDrawing.only( - WindowInsetsSides.Horizontal, - ), + Column(Modifier.fillMaxSize()) { + // Show the top app bar on top level destinations. + val destination = appState.currentTopLevelDestination + val shouldShowTopAppBar = destination != null + if (destination != null) { + NiaTopAppBar( + titleRes = destination.titleTextId, + navigationIcon = NiaIcons.Search, + navigationIconContentDescription = stringResource( + id = settingsR.string.feature_settings_top_app_bar_navigation_icon_description, ), - ) { - if (appState.shouldShowNavRail) { - NiaNavRail( - destinations = appState.topLevelDestinations, - destinationsWithUnreadResources = unreadDestinations, - onNavigateToDestination = appState::navigateToTopLevelDestination, - currentDestination = appState.currentDestination, - modifier = Modifier - .testTag("NiaNavRail") - .safeDrawingPadding(), - ) - } - - Column(Modifier.fillMaxSize()) { - // Show the top app bar on top level destinations. - val destination = appState.currentTopLevelDestination - if (destination != null) { - NiaTopAppBar( - titleRes = destination.titleTextId, - navigationIcon = NiaIcons.Search, - navigationIconContentDescription = stringResource( - id = settingsR.string.feature_settings_top_app_bar_navigation_icon_description, - ), - actionIcon = NiaIcons.Settings, - actionIconContentDescription = stringResource( - id = settingsR.string.feature_settings_top_app_bar_action_icon_description, - ), - colors = TopAppBarDefaults.centerAlignedTopAppBarColors( - containerColor = Color.Transparent, - ), - onActionClick = { showSettingsDialog = true }, - onNavigationClick = { appState.navigateToSearch() }, - ) - } + actionIcon = NiaIcons.Settings, + actionIconContentDescription = stringResource( + id = settingsR.string.feature_settings_top_app_bar_action_icon_description, + ), + colors = TopAppBarDefaults.centerAlignedTopAppBarColors( + containerColor = Color.Transparent, + ), + onActionClick = { onTopAppBarActionClick() }, + onNavigationClick = { appState.navigateToSearch() }, + ) + } - NiaNavHost( - appState = appState, - onShowSnackbar = { message, action -> - snackbarHostState.showSnackbar( - message = message, - actionLabel = action, - duration = Short, - ) == ActionPerformed - }, + NiaNavHost( + appState = appState, + onShowSnackbar = { message, action -> + snackbarHostState.showSnackbar( + message = message, + actionLabel = action, + duration = Short, + ) == ActionPerformed + }, + modifier = if (shouldShowTopAppBar) { + Modifier.consumeWindowInsets( + WindowInsets.safeDrawing.only(WindowInsetsSides.Top), ) - } - - // TODO: We may want to add padding or spacer when the snackbar is shown so that - // content doesn't display behind it. - } + } else { + Modifier + }, + ) } + + // TODO: We may want to add padding or spacer when the snackbar is shown so that + // content doesn't display behind it. } } } diff --git a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ui/interests2pane/InterestsListDetailScreen.kt b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ui/interests2pane/InterestsListDetailScreen.kt index 335f83371..4cc4345ef 100644 --- a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ui/interests2pane/InterestsListDetailScreen.kt +++ b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ui/interests2pane/InterestsListDetailScreen.kt @@ -76,7 +76,7 @@ internal fun InterestsListDetailScreen( selectedTopicId: String?, onTopicClick: (String) -> Unit, ) { - val listDetailNavigator = rememberListDetailPaneScaffoldNavigator() + val listDetailNavigator = rememberListDetailPaneScaffoldNavigator() BackHandler(listDetailNavigator.canNavigateBack()) { listDetailNavigator.navigateBack() } 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 new file mode 100644 index 000000000..fa1ba1036 --- /dev/null +++ b/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/SnackbarScreenshotTests.kt @@ -0,0 +1,238 @@ +/* + * 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.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.rememberCoroutineScope +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.UserNewsResourceRepository +import com.google.samples.apps.nowinandroid.core.data.test.repository.FakeUserDataRepository +import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor +import com.google.samples.apps.nowinandroid.core.data.util.TimeZoneMonitor +import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme +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.CoroutineScope +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +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 javax.inject.Inject + +/** + * Tests that the Snackbar is correctly displayed 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") +@LooperMode(LooperMode.Mode.PAUSED) +@HiltAndroidTest +class SnackbarScreenshotTests { + + /** + * 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() + + @Inject + lateinit var networkMonitor: NetworkMonitor + + @Inject + lateinit var timeZoneMonitor: TimeZoneMonitor + + @Inject + lateinit var userDataRepository: FakeUserDataRepository + + @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), + ) + } + } + + @Test + fun phone_noSnackbar() { + val snackbarHostState = SnackbarHostState() + testSnackbarScreenshotWithSize( + snackbarHostState, + 400.dp, + 500.dp, + "snackbar_compact_medium_noSnackbar", + action = { }, + ) + } + + @Test + fun snackbarShown_phone() { + val snackbarHostState = SnackbarHostState() + testSnackbarScreenshotWithSize( + snackbarHostState, + 400.dp, + 500.dp, + "snackbar_compact_medium", + ) { + snackbarHostState.showSnackbar( + "This is a test snackbar message", + actionLabel = "Action Label", + duration = Indefinite, + ) + } + } + + @Test + fun snackbarShown_foldable() { + val snackbarHostState = SnackbarHostState() + testSnackbarScreenshotWithSize( + snackbarHostState, + 600.dp, + 600.dp, + "snackbar_medium_medium", + ) { + snackbarHostState.showSnackbar( + "This is a test snackbar message", + actionLabel = "Action Label", + duration = Indefinite, + ) + } + } + + @Test + fun snackbarShown_tablet() { + val snackbarHostState = SnackbarHostState() + testSnackbarScreenshotWithSize( + snackbarHostState, + 900.dp, + 900.dp, + "snackbar_expanded_expanded", + ) { + snackbarHostState.showSnackbar( + "This is a test snackbar message", + actionLabel = "Action Label", + duration = Indefinite, + ) + } + } + + private fun testSnackbarScreenshotWithSize( + snackbarHostState: SnackbarHostState, + width: Dp, + height: Dp, + screenshotName: String, + action: suspend () -> Unit, + ) { + lateinit var scope: CoroutineScope + composeTestRule.setContent { + scope = rememberCoroutineScope() + + TestHarness(size = DpSize(width, height)) { + BoxWithConstraints { + val appState = rememberNiaAppState( + windowSizeClass = WindowSizeClass.calculateFromSize( + DpSize(maxWidth, maxHeight), + ), + networkMonitor = networkMonitor, + userNewsResourceRepository = userNewsResourceRepository, + timeZoneMonitor = timeZoneMonitor, + ) + NiaTheme { + NiaApp(appState, snackbarHostState, false, {}, {}) + } + } + } + } + + scope.launch { + action() + } + + composeTestRule.onRoot() + .captureRoboImage( + "src/testDemo/screenshots/$screenshotName.png", + roborazziOptions = DefaultRoborazziOptions, + ) + } +} diff --git a/app/src/testDemo/screenshots/snackbar_compact_medium.png b/app/src/testDemo/screenshots/snackbar_compact_medium.png new file mode 100644 index 000000000..70d15deb3 Binary files /dev/null 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 new file mode 100644 index 000000000..9c3eb133d Binary files /dev/null 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 new file mode 100644 index 000000000..e4c7520ab Binary files /dev/null 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 new file mode 100644 index 000000000..a9f131fd7 Binary files /dev/null and b/app/src/testDemo/screenshots/snackbar_medium_medium.png differ diff --git a/core/data-test/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/test/repository/FakeNewsRepository.kt b/core/data-test/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/test/repository/FakeNewsRepository.kt index 070c7ed38..0cdec6090 100644 --- a/core/data-test/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/test/repository/FakeNewsRepository.kt +++ b/core/data-test/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/test/repository/FakeNewsRepository.kt @@ -39,7 +39,7 @@ import javax.inject.Inject * This allows us to run the app with fake data, without needing an internet connection or working * backend. */ -internal class FakeNewsRepository @Inject constructor( +class FakeNewsRepository @Inject constructor( @Dispatcher(IO) private val ioDispatcher: CoroutineDispatcher, private val datasource: DemoNiaNetworkDataSource, ) : NewsRepository { diff --git a/core/data-test/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/test/repository/FakeUserDataRepository.kt b/core/data-test/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/test/repository/FakeUserDataRepository.kt index 4871baad9..61ab422af 100644 --- a/core/data-test/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/test/repository/FakeUserDataRepository.kt +++ b/core/data-test/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/test/repository/FakeUserDataRepository.kt @@ -30,7 +30,7 @@ import javax.inject.Inject * This allows us to run the app with fake data, without needing an internet connection or working * backend. */ -internal class FakeUserDataRepository @Inject constructor( +class FakeUserDataRepository @Inject constructor( private val niaPreferencesDataSource: NiaPreferencesDataSource, ) : UserDataRepository { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 29daf9a70..e39a6d7fb 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,7 +10,7 @@ androidxBrowser = "1.8.0" androidxComposeBom = "2024.02.02" androidxComposeCompiler = "1.5.8" androidxComposeUiTest = "1.7.0-alpha05" -androidxComposeMaterial3Adaptive = "1.0.0-alpha08" +androidxComposeMaterial3Adaptive = "1.0.0-alpha10" androidxComposeRuntimeTracing = "1.0.0-beta01" androidxCore = "1.12.0" androidxCoreSplashscreen = "1.0.1" @@ -28,7 +28,7 @@ androidxTestRules = "1.5.0" androidxTestRunner = "1.5.2" androidxTracing = "1.3.0-alpha02" androidxUiAutomator = "2.2.0" -androidxWindowManager = "1.2.0" +androidxWindowManager = "1.3.0-alpha03" androidxWork = "2.9.0" coil = "2.6.0" dependencyGuard = "0.4.3" @@ -101,6 +101,7 @@ androidx-test-rules = { group = "androidx.test", name = "rules", version.ref = " androidx-test-runner = { group = "androidx.test", name = "runner", version.ref = "androidxTestRunner" } androidx-test-uiautomator = { group = "androidx.test.uiautomator", name = "uiautomator", version.ref = "androidxUiAutomator" } androidx-tracing-ktx = { group = "androidx.tracing", name = "tracing-ktx", version.ref = "androidxTracing" } +androidx-window-core = { group = "androidx.window", name = "window-core", version.ref = "androidxWindowManager" } androidx-work-ktx = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "androidxWork" } androidx-work-testing = { group = "androidx.work", name = "work-testing", version.ref = "androidxWork" } coil-kt = { group = "io.coil-kt", name = "coil", version.ref = "coil" }