diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractorTest.kt index 645b1cde632f5..23466cc20f449 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractorTest.kt @@ -41,6 +41,7 @@ import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.nullable import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.TestCoroutineScheduler import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -55,7 +56,7 @@ class FooterActionsInteractorTest : SysuiTestCase() { @Before fun setUp() { - utils = FooterActionsTestUtils(context, TestableLooper.get(this)) + utils = FooterActionsTestUtils(context, TestableLooper.get(this), TestCoroutineScheduler()) } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModelTest.kt index 081a2181cfe5c..47afa70fa84b8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModelTest.kt @@ -29,6 +29,7 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.Icon +import com.android.systemui.coroutines.collectLastValue import com.android.systemui.qs.FakeFgsManagerController import com.android.systemui.qs.QSSecurityFooterUtils import com.android.systemui.qs.footer.FooterActionsTestUtils @@ -44,12 +45,9 @@ import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.nullable import com.android.systemui.util.settings.FakeSettings import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.launch -import kotlinx.coroutines.test.TestCoroutineScheduler import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import org.junit.Before @@ -62,16 +60,20 @@ import org.mockito.Mockito.`when` as whenever @RunWith(AndroidTestingRunner::class) @RunWithLooper class FooterActionsViewModelTest : SysuiTestCase() { + private val testScope = TestScope() private lateinit var utils: FooterActionsTestUtils - private val testDispatcher = UnconfinedTestDispatcher(TestCoroutineScheduler()) @Before fun setUp() { - utils = FooterActionsTestUtils(context, TestableLooper.get(this)) + utils = FooterActionsTestUtils(context, TestableLooper.get(this), testScope.testScheduler) + } + + private fun runTest(block: suspend TestScope.() -> Unit) { + testScope.runTest(testBody = block) } @Test - fun settingsButton() = runBlockingTest { + fun settingsButton() = runTest { val underTest = utils.footerActionsViewModel(showPowerButton = false) val settings = underTest.settings @@ -87,7 +89,7 @@ class FooterActionsViewModelTest : SysuiTestCase() { } @Test - fun powerButton() = runBlockingTest { + fun powerButton() = runTest { // Without power button. val underTestWithoutPower = utils.footerActionsViewModel(showPowerButton = false) assertThat(underTestWithoutPower.power).isNull() @@ -114,7 +116,7 @@ class FooterActionsViewModelTest : SysuiTestCase() { } @Test - fun userSwitcher() = runBlockingTest { + fun userSwitcher() = runTest { val picture: Drawable = mock() val userInfoController = FakeUserInfoController(FakeInfo(picture = picture)) val settings = FakeSettings() @@ -135,7 +137,6 @@ class FooterActionsViewModelTest : SysuiTestCase() { showPowerButton = false, footerActionsInteractor = utils.footerActionsInteractor( - bgDispatcher = testDispatcher, userSwitcherRepository = utils.userSwitcherRepository( userTracker = userTracker, @@ -143,22 +144,12 @@ class FooterActionsViewModelTest : SysuiTestCase() { userManager = userManager, userInfoController = userInfoController, userSwitcherController = userSwitcherControllerWrapper.controller, - bgDispatcher = testDispatcher, ), ) ) // Collect the user switcher into currentUserSwitcher. - var currentUserSwitcher: FooterActionsButtonViewModel? = null - val job = launch { underTest.userSwitcher.collect { currentUserSwitcher = it } } - fun currentUserSwitcher(): FooterActionsButtonViewModel? { - // Make sure we finish collecting the current user switcher. This is necessary because - // combined flows launch multiple coroutines in the current scope so we need to make - // sure we process all coroutines triggered by our flow collection before we make - // assertions on the current buttons. - advanceUntilIdle() - return currentUserSwitcher - } + val currentUserSwitcher = collectLastValue(underTest.userSwitcher) // The user switcher is disabled. assertThat(currentUserSwitcher()).isNull() @@ -203,12 +194,10 @@ class FooterActionsViewModelTest : SysuiTestCase() { // in guest mode. userInfoController.updateInfo { this.picture = mock() } assertThat(iconTint()).isNull() - - job.cancel() } @Test - fun security() = runBlockingTest { + fun security() = runTest { val securityController = FakeSecurityController() val qsSecurityFooterUtils = mock() @@ -224,22 +213,15 @@ class FooterActionsViewModelTest : SysuiTestCase() { footerActionsInteractor = utils.footerActionsInteractor( qsSecurityFooterUtils = qsSecurityFooterUtils, - bgDispatcher = testDispatcher, securityRepository = utils.securityRepository( securityController = securityController, - bgDispatcher = testDispatcher, ), ), ) // Collect the security model into currentSecurity. - var currentSecurity: FooterActionsSecurityButtonViewModel? = null - val job = launch { underTest.security.collect { currentSecurity = it } } - fun currentSecurity(): FooterActionsSecurityButtonViewModel? { - advanceUntilIdle() - return currentSecurity - } + val currentSecurity = collectLastValue(underTest.security) // By default, we always return a null SecurityButtonConfig. assertThat(currentSecurity()).isNull() @@ -270,12 +252,10 @@ class FooterActionsViewModelTest : SysuiTestCase() { security = currentSecurity() assertThat(security).isNotNull() assertThat(security!!.onClick).isNull() - - job.cancel() } @Test - fun foregroundServices() = runBlockingTest { + fun foregroundServices() = runTest { val securityController = FakeSecurityController() val fgsManagerController = FakeFgsManagerController( @@ -300,21 +280,14 @@ class FooterActionsViewModelTest : SysuiTestCase() { securityRepository = utils.securityRepository( securityController, - bgDispatcher = testDispatcher, ), foregroundServicesRepository = utils.foregroundServicesRepository(fgsManagerController), - bgDispatcher = testDispatcher, ), ) // Collect the security model into currentSecurity. - var currentForegroundServices: FooterActionsForegroundServicesButtonViewModel? = null - val job = launch { underTest.foregroundServices.collect { currentForegroundServices = it } } - fun currentForegroundServices(): FooterActionsForegroundServicesButtonViewModel? { - advanceUntilIdle() - return currentForegroundServices - } + val currentForegroundServices = collectLastValue(underTest.foregroundServices) // We don't show the foreground services button if the number of running packages is not // > 1. @@ -356,12 +329,10 @@ class FooterActionsViewModelTest : SysuiTestCase() { } securityController.updateState {} assertThat(currentForegroundServices()?.displayText).isFalse() - - job.cancel() } @Test - fun observeDeviceMonitoringDialogRequests() = runBlockingTest { + fun observeDeviceMonitoringDialogRequests() = runTest { val qsSecurityFooterUtils = mock() val broadcastDispatcher = mock() @@ -390,7 +361,6 @@ class FooterActionsViewModelTest : SysuiTestCase() { utils.footerActionsInteractor( qsSecurityFooterUtils = qsSecurityFooterUtils, broadcastDispatcher = broadcastDispatcher, - bgDispatcher = testDispatcher, ), ) @@ -415,7 +385,4 @@ class FooterActionsViewModelTest : SysuiTestCase() { underTest.onVisibilityChangeRequested(visible = true) assertThat(underTest.isVisible.value).isTrue() } - - private fun runBlockingTest(block: suspend TestScope.() -> Unit) = - runTest(testDispatcher) { block() } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/coroutines/Flow.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/coroutines/Flow.kt new file mode 100644 index 0000000000000..b7a8d2e9f6844 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/coroutines/Flow.kt @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2022 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 + * + * http://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.android.systemui.coroutines + +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent + +/** Collect [flow] in a new [Job] and return a getter for the last collected value. */ +fun TestScope.collectLastValue( + flow: Flow, + context: CoroutineContext = EmptyCoroutineContext, + start: CoroutineStart = CoroutineStart.DEFAULT, +): () -> T? { + var lastValue: T? = null + backgroundScope.launch(context, start) { flow.collect { lastValue = it } } + return { + runCurrent() + lastValue + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/footer/FooterActionsTestUtils.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/footer/FooterActionsTestUtils.kt index 63448e236867c..1a893f8c523c0 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/footer/FooterActionsTestUtils.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/footer/FooterActionsTestUtils.kt @@ -56,7 +56,8 @@ import com.android.systemui.util.mockito.mock import com.android.systemui.util.settings.FakeSettings import com.android.systemui.util.settings.GlobalSettings import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.test.TestCoroutineDispatcher +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestCoroutineScheduler /** * Util class to create real implementations of the FooterActions repositories, viewModel and @@ -65,6 +66,7 @@ import kotlinx.coroutines.test.TestCoroutineDispatcher class FooterActionsTestUtils( private val context: Context, private val testableLooper: TestableLooper, + private val scheduler: TestCoroutineScheduler, ) { /** Enable or disable the user switcher in the settings. */ fun setUserSwitcherEnabled(settings: GlobalSettings, enabled: Boolean, userId: Int) { @@ -105,7 +107,7 @@ class FooterActionsTestUtils( foregroundServicesRepository: ForegroundServicesRepository = foregroundServicesRepository(), userSwitcherRepository: UserSwitcherRepository = userSwitcherRepository(), broadcastDispatcher: BroadcastDispatcher = mock(), - bgDispatcher: CoroutineDispatcher = TestCoroutineDispatcher(), + bgDispatcher: CoroutineDispatcher = StandardTestDispatcher(scheduler), ): FooterActionsInteractor { return FooterActionsInteractorImpl( activityStarter, @@ -126,7 +128,7 @@ class FooterActionsTestUtils( /** Create a [SecurityRepository] to be used in tests. */ fun securityRepository( securityController: SecurityController = FakeSecurityController(), - bgDispatcher: CoroutineDispatcher = TestCoroutineDispatcher(), + bgDispatcher: CoroutineDispatcher = StandardTestDispatcher(scheduler), ): SecurityRepository { return SecurityRepositoryImpl( securityController, @@ -145,7 +147,7 @@ class FooterActionsTestUtils( fun userSwitcherRepository( @Application context: Context = this.context.applicationContext, bgHandler: Handler = Handler(testableLooper.looper), - bgDispatcher: CoroutineDispatcher = TestCoroutineDispatcher(), + bgDispatcher: CoroutineDispatcher = StandardTestDispatcher(scheduler), userManager: UserManager = mock(), userTracker: UserTracker = FakeUserTracker(), userSwitcherController: UserSwitcherController = mock(),