@@ -42,53 +42,55 @@ import androidx.compose.ui.unit.IntSize
42
42
import kotlin.coroutines.CoroutineContext
43
43
import kotlin.coroutines.EmptyCoroutineContext
44
44
import kotlin.coroutines.cancellation.CancellationException
45
- import kotlin.jvm.JvmName
46
45
import kotlin.math.roundToInt
47
46
import kotlin.time.Duration
48
- import kotlin.time.Duration.Companion.seconds
47
+ import kotlinx.coroutines.CoroutineDispatcher
48
+ import kotlinx.coroutines.CoroutineExceptionHandler
49
49
import kotlinx.coroutines.CoroutineScope
50
50
import kotlinx.coroutines.ExperimentalCoroutinesApi
51
+ import kotlinx.coroutines.Job
51
52
import kotlinx.coroutines.awaitCancellation
52
53
import kotlinx.coroutines.cancel
53
54
import kotlinx.coroutines.delay
54
55
import kotlinx.coroutines.isActive
55
56
import kotlinx.coroutines.launch
57
+ import kotlinx.coroutines.test.StandardTestDispatcher
58
+ import kotlinx.coroutines.test.TestCoroutineScheduler
56
59
import kotlinx.coroutines.test.TestDispatcher
57
60
import kotlinx.coroutines.test.TestResult
58
- import kotlinx.coroutines.test.TestScope
59
61
import kotlinx.coroutines.test.UnconfinedTestDispatcher
60
- import kotlinx.coroutines.test.runTest
61
62
import kotlinx.coroutines.yield
62
63
import org.jetbrains.skia.Color
63
64
import org.jetbrains.skia.IRect
64
65
import org.jetbrains.skia.Surface
65
66
import org.jetbrains.skiko.currentNanoTime
66
67
67
- @ExperimentalTestApi
68
+ // @ExperimentalTestApi
68
69
// @Deprecated(
69
70
// level = DeprecationLevel.HIDDEN,
70
71
// message = "Replaced with same function, but with suspend block, runTextContext, testTimeout"
71
72
// )
72
- fun runComposeUiTest (
73
- effectContext : CoroutineContext = EmptyCoroutineContext ,
74
- block : ComposeUiTest .() -> Unit
75
- ) {
76
- SkikoComposeUiTest (effectContext = effectContext).runTest(block)
77
- }
73
+ // fun runComposeUiTest(
74
+ // effectContext: CoroutineContext = EmptyCoroutineContext,
75
+ // block: ComposeUiTest.() -> Unit
76
+ // ): TestResult {
77
+ // return SkikoComposeUiTest(effectContext = effectContext).runTest(block)
78
+ // }
78
79
79
80
@ExperimentalTestApi
80
- @Deprecated(
81
- level = DeprecationLevel .HIDDEN ,
82
- message = " TODO: Adopt runComposeUiTest with suspend lambda"
83
- )
84
81
actual fun runComposeUiTest (
85
82
effectContext : CoroutineContext ,
86
83
runTestContext : CoroutineContext ,
87
84
testTimeout : Duration ,
88
85
block : suspend ComposeUiTest .() -> Unit
89
86
): TestResult {
90
- // TODO: https://youtrack.jetbrains.com/issue/CMP-7994
91
- TODO (" Adopt runComposeUiTest with suspend lambda" )
87
+ return runSkikoComposeUiTest(
88
+ effectContext = effectContext,
89
+ runTestContext = runTestContext,
90
+ testTimeout = testTimeout,
91
+ ) {
92
+ block()
93
+ }
92
94
}
93
95
94
96
@ExperimentalTestApi
@@ -97,12 +99,17 @@ fun runSkikoComposeUiTest(
97
99
density : Density = Density (1f),
98
100
// TODO(https://github.com/JetBrains/compose-multiplatform/issues/2960) Support effectContext
99
101
effectContext : CoroutineContext = EmptyCoroutineContext ,
100
- block : SkikoComposeUiTest .() -> Unit
101
- ) {
102
- SkikoComposeUiTest (
102
+ runTestContext : CoroutineContext = EmptyCoroutineContext ,
103
+ testTimeout : Duration = Duration .INFINITE ,
104
+ block : suspend SkikoComposeUiTest .() -> Unit
105
+ ): TestResult {
106
+ @OptIn(InternalTestApi ::class )
107
+ return SkikoComposeUiTest (
103
108
width = size.width.roundToInt(),
104
109
height = size.height.roundToInt(),
105
110
effectContext = effectContext,
111
+ testTimeout = testTimeout,
112
+ runTestContext = runTestContext,
106
113
density = density
107
114
).runTest(block)
108
115
}
@@ -114,18 +121,24 @@ fun runInternalSkikoComposeUiTest(
114
121
height : Int = 768,
115
122
density : Density = Density (1f),
116
123
effectContext : CoroutineContext = EmptyCoroutineContext ,
124
+ runTestContext : CoroutineContext = EmptyCoroutineContext ,
125
+ testTimeout : Duration = Duration .INFINITE ,
117
126
semanticsOwnerListener : PlatformContext .SemanticsOwnerListener ? = null,
118
127
coroutineDispatcher : TestDispatcher = defaultTestDispatcher(),
119
- block : SkikoComposeUiTest .() -> Unit
120
- ) {
121
- SkikoComposeUiTest (
122
- width = width,
123
- height = height,
124
- effectContext = effectContext,
125
- density = density,
126
- semanticsOwnerListener = semanticsOwnerListener,
127
- coroutineDispatcher = coroutineDispatcher,
128
- ).runTest(block)
128
+ block : suspend SkikoComposeUiTest .() -> Unit
129
+ ): TestResult {
130
+ return kotlinx.coroutines.test.runTest {
131
+ SkikoComposeUiTest (
132
+ width = width,
133
+ height = height,
134
+ effectContext = effectContext,
135
+ runTestContext = runTestContext,
136
+ testTimeout = testTimeout,
137
+ density = density,
138
+ semanticsOwnerListener = semanticsOwnerListener,
139
+ coroutineDispatcher = coroutineDispatcher,
140
+ ).runTest(block)
141
+ }
129
142
}
130
143
131
144
/* *
@@ -139,7 +152,7 @@ private const val IDLING_RESOURCES_CHECK_INTERVAL_MS = 20L
139
152
*/
140
153
@OptIn(ExperimentalCoroutinesApi ::class )
141
154
@InternalTestApi
142
- fun defaultTestDispatcher () = UnconfinedTestDispatcher ()
155
+ fun defaultTestDispatcher (): TestDispatcher = UnconfinedTestDispatcher ()
143
156
144
157
/* *
145
158
* @param effectContext The [CoroutineContext] used to run the composition. The context for
@@ -152,6 +165,8 @@ open class SkikoComposeUiTest @InternalTestApi constructor(
152
165
height : Int = 768 ,
153
166
// TODO(https://github.com/JetBrains/compose-multiplatform/issues/2960) Support effectContext
154
167
effectContext : CoroutineContext = EmptyCoroutineContext ,
168
+ private val runTestContext : CoroutineContext = EmptyCoroutineContext ,
169
+ private val testTimeout : Duration = Duration .INFINITE ,
155
170
override val density : Density = Density (1f),
156
171
private val semanticsOwnerListener : PlatformContext .SemanticsOwnerListener ? ,
157
172
coroutineDispatcher : TestDispatcher = defaultTestDispatcher(),
@@ -176,9 +191,24 @@ open class SkikoComposeUiTest @InternalTestApi constructor(
176
191
semanticsOwnerListener = null ,
177
192
)
178
193
179
- private val composeRootRegistry = ComposeRootRegistry ()
194
+ constructor (
195
+ width: Int = 1024 ,
196
+ height: Int = 768 ,
197
+ effectContext: CoroutineContext = EmptyCoroutineContext ,
198
+ runTestContext: CoroutineContext = EmptyCoroutineContext ,
199
+ testTimeout: Duration = Duration .INFINITE ,
200
+ density: Density = Density (1f )
201
+ ) : this (
202
+ width = width,
203
+ height = height,
204
+ effectContext = effectContext,
205
+ runTestContext = runTestContext,
206
+ testTimeout = testTimeout,
207
+ density = density,
208
+ semanticsOwnerListener = null ,
209
+ )
180
210
181
- private val testScope = TestScope (coroutineDispatcher )
211
+ private val composeRootRegistry = ComposeRootRegistry ( )
182
212
override val mainClock: MainTestClock = MainTestClockImpl (
183
213
testScheduler = coroutineDispatcher.scheduler,
184
214
frameDelayMillis = FRAME_DELAY_MILLIS
@@ -205,26 +235,50 @@ open class SkikoComposeUiTest @InternalTestApi constructor(
205
235
private val testOwner = SkikoTestOwner ()
206
236
private val testContext = TestContext (testOwner)
207
237
208
- fun <R > runTest (block : SkikoComposeUiTest .() -> R ): R {
209
- return composeRootRegistry.withRegistry {
210
- withScene {
238
+ fun runTest (
239
+ block : suspend SkikoComposeUiTest .() -> Unit
240
+ ): TestResult {
241
+ composeRootRegistry.setupRegistry()
242
+ scene = runOnUiThread(::createUi)
243
+
244
+ @OptIn(ExperimentalStdlibApi ::class )
245
+ val testDispatcher = runTestContext[CoroutineDispatcher ] as ? TestDispatcher
246
+ ? : StandardTestDispatcher ()
247
+
248
+ // It's required for a test block to run in a coroutine context with some
249
+ // elements from the current Recomposer context (e.g. MonotonicFrameClock)
250
+ val combinedCoroutineContext =
251
+ scene.compositionContext.effectCoroutineContext
252
+ .minusKey(CoroutineExceptionHandler .Key )
253
+ .minusKey(Job .Key )
254
+ .minusKey(TestCoroutineScheduler .Key )
255
+ .plus(runTestContext)
256
+ .plus(testDispatcher)
257
+
258
+ return kotlinx.coroutines.test.runTest(
259
+ timeout = testTimeout,
260
+ context = combinedCoroutineContext
261
+ ) {
262
+ try {
211
263
withRenderLoop {
212
264
block()
213
265
}
266
+ } finally {
267
+ runOnUiThread(scene::close)
268
+ composeRootRegistry.tearDownRegistry()
269
+ uncaughtExceptionHandler.throwUncaught()
214
270
}
215
271
}
216
272
}
217
273
218
- private fun <R > withScene (block : () -> R ): R {
274
+ private inline fun <R > withScene (block : () -> R ): R {
219
275
scene = runOnUiThread(::createUi)
220
276
try {
221
277
return block()
222
278
} finally {
223
279
// Close the scene before calling testScope.runTest so that all the coroutines are
224
280
// cancelled when we call it.
225
281
runOnUiThread(scene::close)
226
- // call runTest instead of deprecated cleanupTestCoroutines()
227
- testScope.runTest { }
228
282
uncaughtExceptionHandler.throwUncaught()
229
283
}
230
284
}
@@ -257,7 +311,7 @@ open class SkikoComposeUiTest @InternalTestApi constructor(
257
311
)
258
312
}
259
313
260
- private fun createUi () = CanvasLayersComposeScene (
314
+ private fun createUi (): ComposeScene = CanvasLayersComposeScene (
261
315
density = density,
262
316
size = size,
263
317
coroutineContext = coroutineContext,
0 commit comments