Override WindowMetrics in InterestsListDetailScreenTest

Change-Id: Id1b0566f4c1705372699c69741dc1016ca6e169d
jdk/fake_window_metrics
Jonathan Koren 5 months ago
parent 9e4532f0eb
commit f49dda4ddc

@ -126,6 +126,7 @@ dependencies {
androidTestImplementation(libs.androidx.test.espresso.core) androidTestImplementation(libs.androidx.test.espresso.core)
androidTestImplementation(libs.androidx.navigation.testing) androidTestImplementation(libs.androidx.navigation.testing)
androidTestImplementation(libs.androidx.compose.ui.test) androidTestImplementation(libs.androidx.compose.ui.test)
androidTestImplementation(libs.androidx.window.testing)
androidTestImplementation(libs.hilt.android.testing) androidTestImplementation(libs.hilt.android.testing)
baselineProfile(projects.benchmarks) baselineProfile(projects.benchmarks)

@ -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))
}
}

@ -17,8 +17,8 @@
package com.google.samples.apps.nowinandroid.ui.interests2pane package com.google.samples.apps.nowinandroid.ui.interests2pane
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.compose.material3.adaptive.Posture import androidx.compose.runtime.Composable
import androidx.compose.material3.adaptive.WindowAdaptiveInfo import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.test.DeviceConfigurationOverride import androidx.compose.ui.test.DeviceConfigurationOverride
import androidx.compose.ui.test.ForcedSize import androidx.compose.ui.test.ForcedSize
import androidx.compose.ui.test.assertIsDisplayed 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.DpSize
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.test.espresso.Espresso 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.data.repository.TopicsRepository
import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme 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.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.ui.stringResource
import com.google.samples.apps.nowinandroid.uitesthiltmanifest.HiltComponentActivity import com.google.samples.apps.nowinandroid.uitesthiltmanifest.HiltComponentActivity
import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.BindValue
@ -61,6 +61,9 @@ class InterestsListDetailScreenTest {
@get:Rule(order = 2) @get:Rule(order = 2)
val composeTestRule = createAndroidComposeRule<HiltComponentActivity>() val composeTestRule = createAndroidComposeRule<HiltComponentActivity>()
@get:Rule(order = 3)
val windowMetricsCalculatorRule = FakeWindowMetricsCalculatorRule()
@Inject @Inject
lateinit var topicsRepository: TopicsRepository lateinit var topicsRepository: TopicsRepository
@ -72,16 +75,9 @@ class InterestsListDetailScreenTest {
get() = "topic:${this.id}" get() = "topic:${this.id}"
// Overrides for device sizes. // Overrides for device sizes.
private enum class TestDeviceConfig(widthDp: Float, heightDp: Float) { private enum class TestDeviceConfig(val dpSize: DpSize) {
Compact(412f, 915f), Compact(DpSize(412.dp, 915.dp)),
Expanded(1200f, 840f), Expanded(DpSize(1200.dp, 840.dp)),
;
val sizeOverride = DeviceConfigurationOverride.ForcedSize(DpSize(widthDp.dp, heightDp.dp))
val adaptiveInfo = WindowAdaptiveInfo(
windowSizeClass = WindowSizeClass.compute(widthDp, heightDp),
windowPosture = Posture(),
)
} }
@Before @Before
@ -94,15 +90,28 @@ class InterestsListDetailScreenTest {
topicsRepository.getTopics().first() 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 @Test
fun expandedWidth_initialState_showsTwoPanesWithPlaceholder() { fun expandedWidth_initialState_showsTwoPanesWithPlaceholder() {
composeTestRule.apply { composeTestRule.apply {
setContent { setContent {
with(TestDeviceConfig.Expanded) { TestDeviceConfig.Expanded.Override {
DeviceConfigurationOverride(override = sizeOverride) {
NiaTheme { NiaTheme {
InterestsListDetailScreen(windowAdaptiveInfo = adaptiveInfo) InterestsListDetailScreen()
}
} }
} }
} }
@ -116,11 +125,9 @@ class InterestsListDetailScreenTest {
fun compactWidth_initialState_showsListPane() { fun compactWidth_initialState_showsListPane() {
composeTestRule.apply { composeTestRule.apply {
setContent { setContent {
with(TestDeviceConfig.Compact) { TestDeviceConfig.Compact.Override {
DeviceConfigurationOverride(override = sizeOverride) {
NiaTheme { NiaTheme {
InterestsListDetailScreen(windowAdaptiveInfo = adaptiveInfo) InterestsListDetailScreen()
}
} }
} }
} }
@ -134,11 +141,9 @@ class InterestsListDetailScreenTest {
fun expandedWidth_topicSelected_updatesDetailPane() { fun expandedWidth_topicSelected_updatesDetailPane() {
composeTestRule.apply { composeTestRule.apply {
setContent { setContent {
with(TestDeviceConfig.Expanded) { TestDeviceConfig.Expanded.Override {
DeviceConfigurationOverride(override = sizeOverride) {
NiaTheme { NiaTheme {
InterestsListDetailScreen(windowAdaptiveInfo = adaptiveInfo) InterestsListDetailScreen()
}
} }
} }
} }
@ -156,11 +161,9 @@ class InterestsListDetailScreenTest {
fun compactWidth_topicSelected_showsTopicDetailPane() { fun compactWidth_topicSelected_showsTopicDetailPane() {
composeTestRule.apply { composeTestRule.apply {
setContent { setContent {
with(TestDeviceConfig.Compact) { TestDeviceConfig.Compact.Override {
DeviceConfigurationOverride(override = sizeOverride) {
NiaTheme { NiaTheme {
InterestsListDetailScreen(windowAdaptiveInfo = adaptiveInfo) InterestsListDetailScreen()
}
} }
} }
} }
@ -179,16 +182,14 @@ class InterestsListDetailScreenTest {
var unhandledBackPress = false var unhandledBackPress = false
composeTestRule.apply { composeTestRule.apply {
setContent { setContent {
with(TestDeviceConfig.Expanded) { TestDeviceConfig.Expanded.Override {
DeviceConfigurationOverride(override = sizeOverride) {
NiaTheme { NiaTheme {
// Back press should not be handled by the two pane layout, and thus // Back press should not be handled by the two pane layout, and thus
// "fall through" to this BackHandler. // "fall through" to this BackHandler.
BackHandler { BackHandler {
unhandledBackPress = true unhandledBackPress = true
} }
InterestsListDetailScreen(windowAdaptiveInfo = adaptiveInfo) InterestsListDetailScreen()
}
} }
} }
} }
@ -206,11 +207,9 @@ class InterestsListDetailScreenTest {
fun compactWidth_backPressFromTopicDetail_showsListPane() { fun compactWidth_backPressFromTopicDetail_showsListPane() {
composeTestRule.apply { composeTestRule.apply {
setContent { setContent {
with(TestDeviceConfig.Compact) { TestDeviceConfig.Compact.Override {
DeviceConfigurationOverride(override = sizeOverride) {
NiaTheme { NiaTheme {
InterestsListDetailScreen(windowAdaptiveInfo = adaptiveInfo) InterestsListDetailScreen()
}
} }
} }
} }

@ -18,14 +18,11 @@ package com.google.samples.apps.nowinandroid.ui.interests2pane
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi 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.AnimatedPane
import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffold import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffold
import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole
import androidx.compose.material3.adaptive.layout.PaneAdaptedValue import androidx.compose.material3.adaptive.layout.PaneAdaptedValue
import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldDestinationItem 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.ThreePaneScaffoldNavigator
import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -74,13 +71,11 @@ fun NavGraphBuilder.interestsListDetailScreen() {
@Composable @Composable
internal fun InterestsListDetailScreen( internal fun InterestsListDetailScreen(
viewModel: Interests2PaneViewModel = hiltViewModel(), viewModel: Interests2PaneViewModel = hiltViewModel(),
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfo(),
) { ) {
val selectedTopicId by viewModel.selectedTopicId.collectAsStateWithLifecycle() val selectedTopicId by viewModel.selectedTopicId.collectAsStateWithLifecycle()
InterestsListDetailScreen( InterestsListDetailScreen(
selectedTopicId = selectedTopicId, selectedTopicId = selectedTopicId,
onTopicClick = viewModel::onTopicClick, onTopicClick = viewModel::onTopicClick,
windowAdaptiveInfo = windowAdaptiveInfo,
) )
} }
@ -89,10 +84,8 @@ internal fun InterestsListDetailScreen(
internal fun InterestsListDetailScreen( internal fun InterestsListDetailScreen(
selectedTopicId: String?, selectedTopicId: String?,
onTopicClick: (String) -> Unit, onTopicClick: (String) -> Unit,
windowAdaptiveInfo: WindowAdaptiveInfo,
) { ) {
val listDetailNavigator = rememberListDetailPaneScaffoldNavigator( val listDetailNavigator = rememberListDetailPaneScaffoldNavigator(
scaffoldDirective = calculatePaneScaffoldDirective(windowAdaptiveInfo),
initialDestinationHistory = listOfNotNull( initialDestinationHistory = listOfNotNull(
ThreePaneScaffoldDestinationItem(ListDetailPaneScaffoldRole.List), ThreePaneScaffoldDestinationItem(ListDetailPaneScaffoldRole.List),
ThreePaneScaffoldDestinationItem<Nothing>(ListDetailPaneScaffoldRole.Detail).takeIf { ThreePaneScaffoldDestinationItem<Nothing>(ListDetailPaneScaffoldRole.Detail).takeIf {

@ -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-test-uiautomator = { group = "androidx.test.uiautomator", name = "uiautomator", version.ref = "androidxUiAutomator" }
androidx-tracing-ktx = { group = "androidx.tracing", name = "tracing-ktx", version.ref = "androidxTracing" } 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-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-ktx = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "androidxWork" }
androidx-work-testing = { group = "androidx.work", name = "work-testing", 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" } coil-kt = { group = "io.coil-kt", name = "coil", version.ref = "coil" }

Loading…
Cancel
Save