1 /*
<lambda>null2 * Copyright (C) 2024 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 @file:OptIn(ExperimentalCoroutinesApi::class)
18
19 package com.android.systemui.kosmos
20
21 import com.android.systemui.SysuiTestCase
22 import com.android.systemui.compose.runTestWithSnapshots
23 import com.android.systemui.coroutines.FlowValue
24 import com.android.systemui.coroutines.collectLastValue
25 import com.android.systemui.coroutines.collectValues
26 import com.android.systemui.kosmos.Kosmos.Fixture
27 import kotlin.coroutines.CoroutineContext
28 import kotlin.time.Duration
29 import kotlinx.coroutines.ExperimentalCoroutinesApi
30 import kotlinx.coroutines.flow.Flow
31 import kotlinx.coroutines.flow.StateFlow
32 import kotlinx.coroutines.launch
33 import kotlinx.coroutines.test.StandardTestDispatcher
34 import kotlinx.coroutines.test.TestScope
35 import kotlinx.coroutines.test.UnconfinedTestDispatcher
36 import kotlinx.coroutines.test.advanceTimeBy
37 import kotlinx.coroutines.test.runCurrent
38 import org.mockito.kotlin.verify
39
40 var Kosmos.testDispatcher by Fixture { StandardTestDispatcher() }
41
42 /**
43 * Force this Kosmos to use a [StandardTestDispatcher], regardless of the current Kosmos default. In
44 * short, no launch blocks will be run on this dispatcher until `TestCoroutineScheduler.runCurrent`
45 * is called. See [StandardTestDispatcher] for details.
46 *
47 * For details on this migration, see http://go/thetiger
48 */
<lambda>null49 fun Kosmos.useStandardTestDispatcher() = apply { testDispatcher = StandardTestDispatcher() }
50
51 /**
52 * Force this Kosmos to use an [UnconfinedTestDispatcher], regardless of the current Kosmos default.
53 * In short, launch blocks will be executed eagerly without waiting for
54 * `TestCoroutineScheduler.runCurrent`. See [UnconfinedTestDispatcher] for details.
55 *
56 * For details on this migration, see http://go/thetiger
57 */
<lambda>null58 fun Kosmos.useUnconfinedTestDispatcher() = apply { testDispatcher = UnconfinedTestDispatcher() }
59
<lambda>null60 var Kosmos.testScope by Fixture { TestScope(testDispatcher) }
<lambda>null61 var Kosmos.backgroundScope by Fixture { testScope.backgroundScope }
<lambda>null62 var Kosmos.applicationCoroutineScope by Fixture { testScope.backgroundScope }
63 var Kosmos.testCase: SysuiTestCase by Fixture()
<lambda>null64 var Kosmos.backgroundCoroutineContext: CoroutineContext by Fixture { testDispatcher }
<lambda>null65 var Kosmos.mainCoroutineContext: CoroutineContext by Fixture { testDispatcher }
66
67 /**
68 * Run this test body with a [Kosmos] as receiver, and using the [testScope] currently installed in
69 * that Kosmos instance
70 */
kosmosnull71 fun Kosmos.runTest(testBody: suspend Kosmos.() -> Unit) = let { kosmos ->
72 testScope.runTestWithSnapshots { kosmos.testBody() }
73 }
74
runCurrentnull75 fun Kosmos.runCurrent() = testScope.runCurrent()
76
77 fun Kosmos.advanceTimeBy(duration: Duration) = testScope.advanceTimeBy(duration)
78
79 fun <T> Kosmos.collectLastValue(flow: Flow<T>) = testScope.collectLastValue(flow)
80
81 fun <T> Kosmos.collectValues(flow: Flow<T>): FlowValue<List<T>> = testScope.collectValues(flow)
82
83 /**
84 * Retrieve the current value of this [StateFlow] safely. Needs a [TestScope] in order to make sure
85 * that all pending tasks have run before returning a value. Tests that directly access
86 * [StateFlow.value] may be incorrect, since the value returned may be stale if the current test
87 * dispatcher is a [StandardTestDispatcher].
88 *
89 * If you want to assert on a [Flow] that is not a [StateFlow], please use
90 * [TestScope.collectLastValue], to make sure that the desired value is captured when emitted.
91 */
92 fun <T> TestScope.currentValue(stateFlow: StateFlow<T>): T {
93 val values = mutableListOf<T>()
94 val job = backgroundScope.launch { stateFlow.collect(values::add) }
95 runCurrent()
96 job.cancel()
97 // StateFlow should always have at least one value
98 return values.last()
99 }
100
101 /** Retrieve the current value of this [StateFlow] safely. See `currentValue(TestScope)`. */
currentValuenull102 fun <T> Kosmos.currentValue(fn: () -> T) = testScope.currentValue(fn)
103
104 /**
105 * Retrieve the result of [fn] after running all pending tasks. Do not use to retrieve the value of
106 * a flow directly; for that, use either `currentValue(StateFlow)` or [collectLastValue]
107 */
108 fun <T> TestScope.currentValue(fn: () -> T): T {
109 runCurrent()
110 return fn()
111 }
112
113 /** Retrieve the result of [fn] after running all pending tasks. See `TestScope.currentValue(fn)` */
currentValuenull114 fun <T> Kosmos.currentValue(stateFlow: StateFlow<T>): T {
115 return testScope.currentValue(stateFlow)
116 }
117
118 /** Safely verify that a mock has been called after the test scope has caught up */
verifyCurrentnull119 fun <T> TestScope.verifyCurrent(mock: T): T {
120 runCurrent()
121 return verify(mock)
122 }
123
124 /**
125 * Safely verify that a mock has been called after the test scope has caught up. See
126 * `TestScope.verifyCurrent`
127 */
verifyCurrentnull128 fun <T> Kosmos.verifyCurrent(mock: T) = testScope.verifyCurrent(mock)
129