diff options
4 files changed, 64 insertions, 60 deletions
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt index e56b965d9402..3187cca6ca45 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt @@ -37,7 +37,6 @@ import com.android.systemui.authentication.shared.model.AuthenticationMethodMode import com.android.systemui.bouncer.ui.viewmodel.PasswordBouncerViewModel import com.android.systemui.bouncer.ui.viewmodel.PinBouncerViewModel import com.android.systemui.bouncer.ui.viewmodel.bouncerSceneContentViewModel -import com.android.systemui.coroutines.collectLastValue import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor import com.android.systemui.flags.EnableSceneContainer @@ -48,9 +47,9 @@ import com.android.systemui.keyguard.ui.viewmodel.lockscreenUserActionsViewModel import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.collectLastValue import com.android.systemui.kosmos.currentValue -import com.android.systemui.kosmos.runCurrent import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.testScope +import com.android.systemui.kosmos.verifyCurrent import com.android.systemui.lifecycle.activateIn import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest @@ -77,12 +76,9 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.launch import kotlinx.coroutines.test.advanceTimeBy -import kotlinx.coroutines.test.runCurrent -import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mockito.Mockito.verify /** * Integration test cases for the Scene Framework. @@ -137,10 +133,10 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { sceneContainerViewModel.activateIn(testScope) assertWithMessage("Initial scene key mismatch!") - .that(sceneContainerViewModel.currentScene.value) + .that(currentValue(sceneContainerViewModel.currentScene)) .isEqualTo(sceneContainerConfig.initialSceneKey) assertWithMessage("Initial scene container visibility mismatch!") - .that(sceneContainerViewModel.isVisible) + .that(currentValue { sceneContainerViewModel.isVisible }) .isTrue() } @@ -337,7 +333,6 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { .that(bouncerActionButton) .isNotNull() kosmos.bouncerSceneContentViewModel.onActionButtonClicked(bouncerActionButton!!) - runCurrent() // TODO(b/369765704): Assert that an activity was started once we use ActivityStarter. } @@ -358,9 +353,8 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { .that(bouncerActionButton) .isNotNull() kosmos.bouncerSceneContentViewModel.onActionButtonClicked(bouncerActionButton!!) - runCurrent() - verify(mockTelecomManager).showInCallScreen(any()) + verifyCurrent(mockTelecomManager).showInCallScreen(any()) } @Test @@ -413,7 +407,7 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { * the UI must gradually transition between scenes. */ private fun Kosmos.getCurrentSceneInUi(): SceneKey { - return when (val state = transitionState.value) { + return when (val state = currentValue(transitionState)) { is ObservableTransitionState.Idle -> state.currentScene is ObservableTransitionState.Transition.ChangeScene -> state.fromScene is ObservableTransitionState.Transition.ShowOrHideOverlay -> state.currentScene @@ -436,7 +430,6 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { // is not an observable that can trigger a new evaluation. fakeDeviceEntryRepository.setLockscreenEnabled(enableLockscreen) fakeAuthenticationRepository.setAuthenticationMethod(authMethod) - testScope.runCurrent() } /** Emulates a phone call in progress. */ @@ -447,7 +440,6 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { setIsInCall(true) setCallState(TelephonyManager.CALL_STATE_OFFHOOK) } - testScope.runCurrent() } /** @@ -480,24 +472,21 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { isInitiatedByUserInput = false, isUserInputOngoing = flowOf(false), ) - testScope.runCurrent() // Report progress of transition. - while (progressFlow.value < 1f) { + while (currentValue(progressFlow) < 1f) { progressFlow.value += 0.2f - testScope.runCurrent() } // End the transition and report the change. transitionState.value = ObservableTransitionState.Idle(to) fakeSceneDataSource.unpause(force = true) - testScope.runCurrent() assertWithMessage("Visibility mismatch after scene transition from $from to $to!") - .that(sceneContainerViewModel.isVisible) + .that(currentValue { sceneContainerViewModel.isVisible }) .isEqualTo(expectedVisible) - assertThat(sceneContainerViewModel.currentScene.value).isEqualTo(to) + assertThat(currentValue(sceneContainerViewModel.currentScene)).isEqualTo(to) bouncerSceneJob = if (to == Scenes.Bouncer) { @@ -510,7 +499,6 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { bouncerSceneJob?.cancel() null } - testScope.runCurrent() } /** @@ -556,13 +544,12 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { ) powerInteractor.setAwakeForTest() - testScope.runCurrent() } /** Unlocks the device by entering the correct PIN. Ends up in the Gone scene. */ private fun Kosmos.unlockDevice() { assertWithMessage("Cannot unlock a device that's already unlocked!") - .that(deviceEntryInteractor.isUnlocked.value) + .that(currentValue(deviceEntryInteractor.isUnlocked)) .isFalse() emulateUserDrivenTransition(Scenes.Bouncer) @@ -595,7 +582,6 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { pinBouncerViewModel.onPinButtonClicked(digit) } pinBouncerViewModel.onAuthenticateButtonClicked() - testScope.runCurrent() } /** @@ -625,26 +611,23 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { } pinBouncerViewModel.onAuthenticateButtonClicked() fakeMobileConnectionsRepository.isAnySimSecure.value = false - testScope.runCurrent() setAuthMethod(authMethodAfterSimUnlock, enableLockscreen) - testScope.runCurrent() } /** Changes device wakefulness state from asleep to awake, going through intermediary states. */ private fun Kosmos.wakeUpDevice() { - val wakefulnessModel = powerInteractor.detailedWakefulness.value + val wakefulnessModel = currentValue(powerInteractor.detailedWakefulness) assertWithMessage("Cannot wake up device as it's already awake!") .that(wakefulnessModel.isAwake()) .isFalse() powerInteractor.setAwakeForTest() - testScope.runCurrent() } /** Changes device wakefulness state from awake to asleep, going through intermediary states. */ private suspend fun Kosmos.putDeviceToSleep(waitForLock: Boolean = true) { - val wakefulnessModel = powerInteractor.detailedWakefulness.value + val wakefulnessModel = currentValue(powerInteractor.detailedWakefulness) assertWithMessage("Cannot put device to sleep as it's already asleep!") .that(wakefulnessModel.isAwake()) .isTrue() @@ -659,22 +642,18 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { ) .toLong() ) - } else { - testScope.runCurrent() } } /** Emulates the dismissal of the IME (soft keyboard). */ private fun Kosmos.dismissIme() { - (bouncerSceneContentViewModel.authMethodViewModel.value as? PasswordBouncerViewModel)?.let { - it.onImeDismissed() - testScope.runCurrent() - } + (currentValue(bouncerSceneContentViewModel.authMethodViewModel) + as? PasswordBouncerViewModel) + ?.let { it.onImeDismissed() } } private fun Kosmos.introduceLockedSim() { setAuthMethod(AuthenticationMethodModel.Sim) fakeMobileConnectionsRepository.isAnySimSecure.value = true - testScope.runCurrent() } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/GeneralKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/GeneralKosmos.kt index 7ee9d84d84fb..669162d30b80 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/GeneralKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/GeneralKosmos.kt @@ -17,6 +17,7 @@ import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest +import org.mockito.kotlin.verify var Kosmos.testDispatcher by Fixture { StandardTestDispatcher() } @@ -82,6 +83,32 @@ fun <T> TestScope.currentValue(stateFlow: StateFlow<T>): T { } /** Retrieve the current value of this [StateFlow] safely. See `currentValue(TestScope)`. */ +fun <T> Kosmos.currentValue(fn: () -> T) = testScope.currentValue(fn) + +/** + * Retrieve the result of [fn] after running all pending tasks. Do not use to retrieve the value of + * a flow directly; for that, use either `currentValue(StateFlow)` or [collectLastValue] + */ +@OptIn(ExperimentalCoroutinesApi::class) +fun <T> TestScope.currentValue(fn: () -> T): T { + runCurrent() + return fn() +} + +/** Retrieve the result of [fn] after running all pending tasks. See `TestScope.currentValue(fn)` */ fun <T> Kosmos.currentValue(stateFlow: StateFlow<T>): T { return testScope.currentValue(stateFlow) } + +/** Safely verify that a mock has been called after the test scope has caught up */ +@OptIn(ExperimentalCoroutinesApi::class) +fun <T> TestScope.verifyCurrent(mock: T): T { + runCurrent() + return verify(mock) +} + +/** + * Safely verify that a mock has been called after the test scope has caught up. See + * `TestScope.verifyCurrent` + */ +fun <T> Kosmos.verifyCurrent(mock: T) = testScope.verifyCurrent(mock) diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/FakeSceneDataSource.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/FakeSceneDataSource.kt index f52572a9e42d..f5eebb46c2ec 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/FakeSceneDataSource.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/FakeSceneDataSource.kt @@ -19,13 +19,13 @@ package com.android.systemui.scene.shared.model import com.android.compose.animation.scene.OverlayKey import com.android.compose.animation.scene.SceneKey import com.android.compose.animation.scene.TransitionKey +import com.android.systemui.kosmos.currentValue import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.test.TestScope -class FakeSceneDataSource( - initialSceneKey: SceneKey, -) : SceneDataSource { +class FakeSceneDataSource(initialSceneKey: SceneKey, val testScope: TestScope) : SceneDataSource { private val _currentScene = MutableStateFlow(initialSceneKey) override val currentScene: StateFlow<SceneKey> = _currentScene.asStateFlow() @@ -33,18 +33,20 @@ class FakeSceneDataSource( private val _currentOverlays = MutableStateFlow<Set<OverlayKey>>(emptySet()) override val currentOverlays: StateFlow<Set<OverlayKey>> = _currentOverlays.asStateFlow() - var isPaused = false - private set + private var _isPaused = false + val isPaused + get() = testScope.currentValue { _isPaused } - var pendingScene: SceneKey? = null - private set + private var _pendingScene: SceneKey? = null + val pendingScene + get() = testScope.currentValue { _pendingScene } var pendingOverlays: Set<OverlayKey>? = null private set override fun changeScene(toScene: SceneKey, transitionKey: TransitionKey?) { - if (isPaused) { - pendingScene = toScene + if (_isPaused) { + _pendingScene = toScene } else { _currentScene.value = toScene } @@ -55,7 +57,7 @@ class FakeSceneDataSource( } override fun showOverlay(overlay: OverlayKey, transitionKey: TransitionKey?) { - if (isPaused) { + if (_isPaused) { pendingOverlays = (pendingOverlays ?: currentOverlays.value) + overlay } else { _currentOverlays.value += overlay @@ -63,7 +65,7 @@ class FakeSceneDataSource( } override fun hideOverlay(overlay: OverlayKey, transitionKey: TransitionKey?) { - if (isPaused) { + if (_isPaused) { pendingOverlays = (pendingOverlays ?: currentOverlays.value) - overlay } else { _currentOverlays.value -= overlay @@ -82,9 +84,9 @@ class FakeSceneDataSource( * last one will be remembered. */ fun pause() { - check(!isPaused) { "Can't pause what's already paused!" } + check(!_isPaused) { "Can't pause what's already paused!" } - isPaused = true + _isPaused = true } /** @@ -100,15 +102,12 @@ class FakeSceneDataSource( * * If [expectedScene] is provided, will assert that it's indeed the latest called. */ - fun unpause( - force: Boolean = false, - expectedScene: SceneKey? = null, - ) { - check(force || isPaused) { "Can't unpause what's already not paused!" } - - isPaused = false - pendingScene?.let { _currentScene.value = it } - pendingScene = null + fun unpause(force: Boolean = false, expectedScene: SceneKey? = null) { + check(force || _isPaused) { "Can't unpause what's already not paused!" } + + _isPaused = false + _pendingScene?.let { _currentScene.value = it } + _pendingScene = null pendingOverlays?.let { _currentOverlays.value = it } pendingOverlays = null diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/SceneDataSourceKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/SceneDataSourceKosmos.kt index f5196866ae6f..7eebfc305682 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/SceneDataSourceKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/SceneDataSourceKosmos.kt @@ -19,13 +19,12 @@ package com.android.systemui.scene.shared.model import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.kosmos.testScope import com.android.systemui.scene.initialSceneKey import com.android.systemui.scene.sceneContainerConfig val Kosmos.fakeSceneDataSource by Fixture { - FakeSceneDataSource( - initialSceneKey = initialSceneKey, - ) + FakeSceneDataSource(initialSceneKey = initialSceneKey, testScope = testScope) } val Kosmos.sceneDataSourceDelegator by Fixture { |