diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9c6989f67..ad8e9cf90 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -126,6 +126,7 @@ dependencies { androidTestImplementation(libs.androidx.test.espresso.core) androidTestImplementation(libs.androidx.navigation.testing) androidTestImplementation(libs.androidx.compose.ui.test) + androidTestImplementation(libs.androidx.window.testing) androidTestImplementation(libs.hilt.android.testing) baselineProfile(projects.benchmarks) diff --git a/app/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/ui/FakeWindowMetricsCalculatorRule.kt b/app/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/ui/FakeWindowMetricsCalculatorRule.kt new file mode 100644 index 000000000..63ac62eae --- /dev/null +++ b/app/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/ui/FakeWindowMetricsCalculatorRule.kt @@ -0,0 +1,82 @@ +/* + * Copyright 2024 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.app.Activity +import android.content.Context +import android.graphics.Rect +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.unit.toIntSize +import androidx.window.layout.WindowMetrics +import androidx.window.layout.WindowMetricsCalculator +import androidx.window.layout.WindowMetricsCalculatorDecorator +import org.junit.rules.TestRule +import org.junit.runner.Description +import org.junit.runners.model.Statement + +/** + * A test rule which allows overriding the reported WindowMetrics with an arbitrary size, defaulting + * to [Size.Zero]. The size is not persisted and will be reset after each test. + */ +class FakeWindowMetricsCalculatorRule : TestRule { + + private val calculator = FakeWindowMetricsCalculator() + + fun setWindowSize(size: Size) { + calculator.windowSize = size + } + + override fun apply(base: Statement?, description: Description?): Statement { + return object : Statement() { + override fun evaluate() { + WindowMetricsCalculator.overrideDecorator( + object : WindowMetricsCalculatorDecorator { + override fun decorate(calculator: WindowMetricsCalculator): WindowMetricsCalculator = + calculator + }, + ) + try { + base?.evaluate() + } finally { + WindowMetricsCalculator.reset() + calculator.resetWindowSize() + } + } + } + } +} + +internal class FakeWindowMetricsCalculator : WindowMetricsCalculator { + + var windowSize = Size.Zero + + fun resetWindowSize() { + windowSize = Size.Zero + } + + override fun computeCurrentWindowMetrics(context: Context) = compute() + + override fun computeMaximumWindowMetrics(context: Context) = compute() + + override fun computeCurrentWindowMetrics(activity: Activity) = compute() + + override fun computeMaximumWindowMetrics(activity: Activity) = compute() + + private fun compute(): WindowMetrics = windowSize.toIntSize().run { + WindowMetrics(Rect(0, 0, width, height)) + } +} diff --git a/app/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/ui/interests2pane/InterestsListDetailScreenTest.kt b/app/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/ui/interests2pane/InterestsListDetailScreenTest.kt index 21ac3e920..568ca2e09 100644 --- a/app/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/ui/interests2pane/InterestsListDetailScreenTest.kt +++ b/app/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/ui/interests2pane/InterestsListDetailScreenTest.kt @@ -17,8 +17,8 @@ package com.google.samples.apps.nowinandroid.ui.interests2pane import androidx.activity.compose.BackHandler -import androidx.compose.material3.adaptive.Posture -import androidx.compose.material3.adaptive.WindowAdaptiveInfo +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.test.DeviceConfigurationOverride import androidx.compose.ui.test.ForcedSize import androidx.compose.ui.test.assertIsDisplayed @@ -30,10 +30,10 @@ import androidx.compose.ui.test.performClick import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp import androidx.test.espresso.Espresso -import androidx.window.core.layout.WindowSizeClass import com.google.samples.apps.nowinandroid.core.data.repository.TopicsRepository import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme import com.google.samples.apps.nowinandroid.core.model.data.Topic +import com.google.samples.apps.nowinandroid.ui.FakeWindowMetricsCalculatorRule import com.google.samples.apps.nowinandroid.ui.stringResource import com.google.samples.apps.nowinandroid.uitesthiltmanifest.HiltComponentActivity import dagger.hilt.android.testing.BindValue @@ -61,6 +61,9 @@ class InterestsListDetailScreenTest { @get:Rule(order = 2) val composeTestRule = createAndroidComposeRule() + @get:Rule(order = 3) + val windowMetricsCalculatorRule = FakeWindowMetricsCalculatorRule() + @Inject lateinit var topicsRepository: TopicsRepository @@ -72,16 +75,9 @@ class InterestsListDetailScreenTest { get() = "topic:${this.id}" // Overrides for device sizes. - private enum class TestDeviceConfig(widthDp: Float, heightDp: Float) { - Compact(412f, 915f), - Expanded(1200f, 840f), - ; - - val sizeOverride = DeviceConfigurationOverride.ForcedSize(DpSize(widthDp.dp, heightDp.dp)) - val adaptiveInfo = WindowAdaptiveInfo( - windowSizeClass = WindowSizeClass.compute(widthDp, heightDp), - windowPosture = Posture(), - ) + private enum class TestDeviceConfig(val dpSize: DpSize) { + Compact(DpSize(412.dp, 915.dp)), + Expanded(DpSize(1200.dp, 840.dp)), } @Before @@ -94,15 +90,28 @@ class InterestsListDetailScreenTest { topicsRepository.getTopics().first() } + /** + * Sets up a ForcedSize override with a matching size in the window metrics calculator rule. + */ + @Composable + private fun TestDeviceConfig.Override( + content: @Composable () -> Unit + ) { + DeviceConfigurationOverride(override = DeviceConfigurationOverride.ForcedSize(dpSize)) { + with(LocalDensity.current) { + windowMetricsCalculatorRule.setWindowSize(dpSize.toSize()) + } + content() + } + } + @Test fun expandedWidth_initialState_showsTwoPanesWithPlaceholder() { composeTestRule.apply { setContent { - with(TestDeviceConfig.Expanded) { - DeviceConfigurationOverride(override = sizeOverride) { - NiaTheme { - InterestsListDetailScreen(windowAdaptiveInfo = adaptiveInfo) - } + TestDeviceConfig.Expanded.Override { + NiaTheme { + InterestsListDetailScreen() } } } @@ -116,11 +125,9 @@ class InterestsListDetailScreenTest { fun compactWidth_initialState_showsListPane() { composeTestRule.apply { setContent { - with(TestDeviceConfig.Compact) { - DeviceConfigurationOverride(override = sizeOverride) { - NiaTheme { - InterestsListDetailScreen(windowAdaptiveInfo = adaptiveInfo) - } + TestDeviceConfig.Compact.Override { + NiaTheme { + InterestsListDetailScreen() } } } @@ -134,11 +141,9 @@ class InterestsListDetailScreenTest { fun expandedWidth_topicSelected_updatesDetailPane() { composeTestRule.apply { setContent { - with(TestDeviceConfig.Expanded) { - DeviceConfigurationOverride(override = sizeOverride) { - NiaTheme { - InterestsListDetailScreen(windowAdaptiveInfo = adaptiveInfo) - } + TestDeviceConfig.Expanded.Override { + NiaTheme { + InterestsListDetailScreen() } } } @@ -156,11 +161,9 @@ class InterestsListDetailScreenTest { fun compactWidth_topicSelected_showsTopicDetailPane() { composeTestRule.apply { setContent { - with(TestDeviceConfig.Compact) { - DeviceConfigurationOverride(override = sizeOverride) { - NiaTheme { - InterestsListDetailScreen(windowAdaptiveInfo = adaptiveInfo) - } + TestDeviceConfig.Compact.Override { + NiaTheme { + InterestsListDetailScreen() } } } @@ -179,16 +182,14 @@ class InterestsListDetailScreenTest { var unhandledBackPress = false composeTestRule.apply { setContent { - with(TestDeviceConfig.Expanded) { - DeviceConfigurationOverride(override = sizeOverride) { - NiaTheme { - // Back press should not be handled by the two pane layout, and thus - // "fall through" to this BackHandler. - BackHandler { - unhandledBackPress = true - } - InterestsListDetailScreen(windowAdaptiveInfo = adaptiveInfo) + TestDeviceConfig.Expanded.Override { + NiaTheme { + // Back press should not be handled by the two pane layout, and thus + // "fall through" to this BackHandler. + BackHandler { + unhandledBackPress = true } + InterestsListDetailScreen() } } } @@ -206,11 +207,9 @@ class InterestsListDetailScreenTest { fun compactWidth_backPressFromTopicDetail_showsListPane() { composeTestRule.apply { setContent { - with(TestDeviceConfig.Compact) { - DeviceConfigurationOverride(override = sizeOverride) { - NiaTheme { - InterestsListDetailScreen(windowAdaptiveInfo = adaptiveInfo) - } + TestDeviceConfig.Compact.Override { + NiaTheme { + InterestsListDetailScreen() } } } 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 919cb44f2..ada4e49d1 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 @@ -18,14 +18,11 @@ package com.google.samples.apps.nowinandroid.ui.interests2pane import androidx.activity.compose.BackHandler import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi -import androidx.compose.material3.adaptive.WindowAdaptiveInfo -import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo import androidx.compose.material3.adaptive.layout.AnimatedPane import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffold import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole import androidx.compose.material3.adaptive.layout.PaneAdaptedValue import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldDestinationItem -import androidx.compose.material3.adaptive.layout.calculatePaneScaffoldDirective import androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator import androidx.compose.runtime.Composable @@ -74,13 +71,11 @@ fun NavGraphBuilder.interestsListDetailScreen() { @Composable internal fun InterestsListDetailScreen( viewModel: Interests2PaneViewModel = hiltViewModel(), - windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfo(), ) { val selectedTopicId by viewModel.selectedTopicId.collectAsStateWithLifecycle() InterestsListDetailScreen( selectedTopicId = selectedTopicId, onTopicClick = viewModel::onTopicClick, - windowAdaptiveInfo = windowAdaptiveInfo, ) } @@ -89,10 +84,8 @@ internal fun InterestsListDetailScreen( internal fun InterestsListDetailScreen( selectedTopicId: String?, onTopicClick: (String) -> Unit, - windowAdaptiveInfo: WindowAdaptiveInfo, ) { val listDetailNavigator = rememberListDetailPaneScaffoldNavigator( - scaffoldDirective = calculatePaneScaffoldDirective(windowAdaptiveInfo), initialDestinationHistory = listOfNotNull( ThreePaneScaffoldDestinationItem(ListDetailPaneScaffoldRole.List), ThreePaneScaffoldDestinationItem(ListDetailPaneScaffoldRole.Detail).takeIf { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7047ac665..3cc353ef1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -107,6 +107,7 @@ androidx-test-runner = { group = "androidx.test", name = "runner", version.ref = 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-window-testing = { group = "androidx.window", name = "window-testing", 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" }