Skip to content

Commit 1e12cea

Browse files
committed
ComposeUiTest adoption
1 parent 024e85a commit 1e12cea

File tree

9 files changed

+444
-46
lines changed

9 files changed

+444
-46
lines changed

compose/ui/ui-test/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ if (AndroidXComposePlugin.isMultiplatformEnabled(project)) {
173173
api(project(":compose:material:material"))
174174
implementation(libs.kotlinTest)
175175
implementation(libs.atomicFu)
176+
implementation(project(":kruth:kruth"))
176177
}
177178
skikoTest.dependsOn(commonTest)
178179

compose/ui/ui-test/src/desktopMain/kotlin/androidx/compose/ui/test/ComposeUiTest.desktop.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,10 @@ fun runDesktopComposeUiTest(
3636
effectContext: CoroutineContext = EmptyCoroutineContext,
3737
block: DesktopComposeUiTest.() -> Unit
3838
) {
39-
with(DesktopComposeUiTest(width, height, effectContext)) {
40-
runTest { block() }
39+
kotlinx.coroutines.test.runTest {
40+
with(DesktopComposeUiTest(width, height, effectContext)) {
41+
runTest { block() }
42+
}
4143
}
4244
}
4345

compose/ui/ui-test/src/skikoMain/kotlin/androidx/compose/ui/test/ComposeRootRegistry.skiko.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,14 @@ internal class ComposeRootRegistry : PlatformContext.RootForTestListener {
4040
/**
4141
* Sets up this registry to be notified of any [PlatformRootForTest] created
4242
*/
43-
private fun setupRegistry() {
43+
internal fun setupRegistry() {
4444
isTracking = true
4545
}
4646

4747
/**
4848
* Cleans up the changes made by [setupRegistry]. Call this after your test has run.
4949
*/
50-
private fun tearDownRegistry() {
50+
internal fun tearDownRegistry() {
5151
// Stop accepting new roots
5252
isTracking = false
5353
synchronized(lock) {
@@ -79,7 +79,7 @@ internal class ComposeRootRegistry : PlatformContext.RootForTestListener {
7979
return synchronized(lock) { roots.toSet() }
8080
}
8181

82-
fun <R> withRegistry(block: () -> R): R {
82+
inline fun <R> withRegistry(block: () -> R): R {
8383
try {
8484
setupRegistry()
8585
return block()

compose/ui/ui-test/src/skikoMain/kotlin/androidx/compose/ui/test/ComposeUiTest.skiko.kt

Lines changed: 94 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -42,53 +42,55 @@ import androidx.compose.ui.unit.IntSize
4242
import kotlin.coroutines.CoroutineContext
4343
import kotlin.coroutines.EmptyCoroutineContext
4444
import kotlin.coroutines.cancellation.CancellationException
45-
import kotlin.jvm.JvmName
4645
import kotlin.math.roundToInt
4746
import kotlin.time.Duration
48-
import kotlin.time.Duration.Companion.seconds
47+
import kotlinx.coroutines.CoroutineDispatcher
48+
import kotlinx.coroutines.CoroutineExceptionHandler
4949
import kotlinx.coroutines.CoroutineScope
5050
import kotlinx.coroutines.ExperimentalCoroutinesApi
51+
import kotlinx.coroutines.Job
5152
import kotlinx.coroutines.awaitCancellation
5253
import kotlinx.coroutines.cancel
5354
import kotlinx.coroutines.delay
5455
import kotlinx.coroutines.isActive
5556
import kotlinx.coroutines.launch
57+
import kotlinx.coroutines.test.StandardTestDispatcher
58+
import kotlinx.coroutines.test.TestCoroutineScheduler
5659
import kotlinx.coroutines.test.TestDispatcher
5760
import kotlinx.coroutines.test.TestResult
58-
import kotlinx.coroutines.test.TestScope
5961
import kotlinx.coroutines.test.UnconfinedTestDispatcher
60-
import kotlinx.coroutines.test.runTest
6162
import kotlinx.coroutines.yield
6263
import org.jetbrains.skia.Color
6364
import org.jetbrains.skia.IRect
6465
import org.jetbrains.skia.Surface
6566
import org.jetbrains.skiko.currentNanoTime
6667

67-
@ExperimentalTestApi
68+
//@ExperimentalTestApi
6869
//@Deprecated(
6970
// level = DeprecationLevel.HIDDEN,
7071
// message = "Replaced with same function, but with suspend block, runTextContext, testTimeout"
7172
//)
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+
//}
7879

7980
@ExperimentalTestApi
80-
@Deprecated(
81-
level = DeprecationLevel.HIDDEN,
82-
message = "TODO: Adopt runComposeUiTest with suspend lambda"
83-
)
8481
actual fun runComposeUiTest(
8582
effectContext: CoroutineContext,
8683
runTestContext: CoroutineContext,
8784
testTimeout: Duration,
8885
block: suspend ComposeUiTest.() -> Unit
8986
): 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+
}
9294
}
9395

9496
@ExperimentalTestApi
@@ -97,12 +99,17 @@ fun runSkikoComposeUiTest(
9799
density: Density = Density(1f),
98100
// TODO(https://github.com/JetBrains/compose-multiplatform/issues/2960) Support effectContext
99101
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(
103108
width = size.width.roundToInt(),
104109
height = size.height.roundToInt(),
105110
effectContext = effectContext,
111+
testTimeout = testTimeout,
112+
runTestContext = runTestContext,
106113
density = density
107114
).runTest(block)
108115
}
@@ -114,18 +121,24 @@ fun runInternalSkikoComposeUiTest(
114121
height: Int = 768,
115122
density: Density = Density(1f),
116123
effectContext: CoroutineContext = EmptyCoroutineContext,
124+
runTestContext: CoroutineContext = EmptyCoroutineContext,
125+
testTimeout: Duration = Duration.INFINITE,
117126
semanticsOwnerListener: PlatformContext.SemanticsOwnerListener? = null,
118127
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+
}
129142
}
130143

131144
/**
@@ -139,7 +152,7 @@ private const val IDLING_RESOURCES_CHECK_INTERVAL_MS = 20L
139152
*/
140153
@OptIn(ExperimentalCoroutinesApi::class)
141154
@InternalTestApi
142-
fun defaultTestDispatcher() = UnconfinedTestDispatcher()
155+
fun defaultTestDispatcher(): TestDispatcher = UnconfinedTestDispatcher()
143156

144157
/**
145158
* @param effectContext The [CoroutineContext] used to run the composition. The context for
@@ -152,6 +165,8 @@ open class SkikoComposeUiTest @InternalTestApi constructor(
152165
height: Int = 768,
153166
// TODO(https://github.com/JetBrains/compose-multiplatform/issues/2960) Support effectContext
154167
effectContext: CoroutineContext = EmptyCoroutineContext,
168+
private val runTestContext: CoroutineContext = EmptyCoroutineContext,
169+
private val testTimeout: Duration = Duration.INFINITE,
155170
override val density: Density = Density(1f),
156171
private val semanticsOwnerListener: PlatformContext.SemanticsOwnerListener?,
157172
coroutineDispatcher: TestDispatcher = defaultTestDispatcher(),
@@ -176,9 +191,24 @@ open class SkikoComposeUiTest @InternalTestApi constructor(
176191
semanticsOwnerListener = null,
177192
)
178193

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+
)
180210

181-
private val testScope = TestScope(coroutineDispatcher)
211+
private val composeRootRegistry = ComposeRootRegistry()
182212
override val mainClock: MainTestClock = MainTestClockImpl(
183213
testScheduler = coroutineDispatcher.scheduler,
184214
frameDelayMillis = FRAME_DELAY_MILLIS
@@ -205,26 +235,50 @@ open class SkikoComposeUiTest @InternalTestApi constructor(
205235
private val testOwner = SkikoTestOwner()
206236
private val testContext = TestContext(testOwner)
207237

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 {
211263
withRenderLoop {
212264
block()
213265
}
266+
} finally {
267+
runOnUiThread(scene::close)
268+
composeRootRegistry.tearDownRegistry()
269+
uncaughtExceptionHandler.throwUncaught()
214270
}
215271
}
216272
}
217273

218-
private fun <R> withScene(block: () -> R): R {
274+
private inline fun <R> withScene(block: () -> R): R {
219275
scene = runOnUiThread(::createUi)
220276
try {
221277
return block()
222278
} finally {
223279
// Close the scene before calling testScope.runTest so that all the coroutines are
224280
// cancelled when we call it.
225281
runOnUiThread(scene::close)
226-
// call runTest instead of deprecated cleanupTestCoroutines()
227-
testScope.runTest { }
228282
uncaughtExceptionHandler.throwUncaught()
229283
}
230284
}
@@ -257,7 +311,7 @@ open class SkikoComposeUiTest @InternalTestApi constructor(
257311
)
258312
}
259313

260-
private fun createUi() = CanvasLayersComposeScene(
314+
private fun createUi(): ComposeScene = CanvasLayersComposeScene(
261315
density = density,
262316
size = size,
263317
coroutineContext = coroutineContext,

0 commit comments

Comments
 (0)