Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adopt IdlingResource move from commonMain to supporting platforms #1822

Merged
merged 6 commits into from
Feb 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions compose/ui/ui-test/api/desktop/ui-test.api
Original file line number Diff line number Diff line change
Expand Up @@ -95,11 +95,9 @@ public abstract interface class androidx/compose/ui/test/ComposeUiTest : android
public abstract fun enableAccessibilityChecks ()V
public abstract fun getDensity ()Landroidx/compose/ui/unit/Density;
public abstract fun getMainClock ()Landroidx/compose/ui/test/MainTestClock;
public abstract fun registerIdlingResource (Landroidx/compose/ui/test/IdlingResource;)V
public abstract fun runOnIdle (Lkotlin/jvm/functions/Function0;)Ljava/lang/Object;
public abstract fun runOnUiThread (Lkotlin/jvm/functions/Function0;)Ljava/lang/Object;
public abstract fun setContent (Lkotlin/jvm/functions/Function2;)V
public abstract fun unregisterIdlingResource (Landroidx/compose/ui/test/IdlingResource;)V
public abstract fun waitForIdle ()V
public abstract fun waitUntil (Ljava/lang/String;JLkotlin/jvm/functions/Function0;)V
public static synthetic fun waitUntil$default (Landroidx/compose/ui/test/ComposeUiTest;Ljava/lang/String;JLkotlin/jvm/functions/Function0;ILjava/lang/Object;)V
Expand Down Expand Up @@ -131,6 +129,15 @@ public final class androidx/compose/ui/test/ComposeUiTest_skikoKt {
public static synthetic fun runSkikoComposeUiTest-Cqks5Fs$default (JLandroidx/compose/ui/unit/Density;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V
}

public final class androidx/compose/ui/test/DesktopComposeUiTest : androidx/compose/ui/test/SkikoComposeUiTest {
public static final field $stable I
public fun <init> ()V
public fun <init> (IILkotlin/coroutines/CoroutineContext;Landroidx/compose/ui/unit/Density;)V
public synthetic fun <init> (IILkotlin/coroutines/CoroutineContext;Landroidx/compose/ui/unit/Density;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun registerIdlingResource (Landroidx/compose/ui/test/IdlingResource;)V
public final fun unregisterIdlingResource (Landroidx/compose/ui/test/IdlingResource;)V
}

public abstract interface class androidx/compose/ui/test/DeviceConfigurationOverride {
public static final field Companion Landroidx/compose/ui/test/DeviceConfigurationOverride$Companion;
public abstract fun Override (Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;I)V
Expand Down Expand Up @@ -638,13 +645,14 @@ public final class androidx/compose/ui/test/SemanticsSelector {
public final fun map (Ljava/lang/Iterable;Ljava/lang/String;)Landroidx/compose/ui/test/SelectionResult;
}

public final class androidx/compose/ui/test/SkikoComposeUiTest : androidx/compose/ui/test/ComposeUiTest {
public class androidx/compose/ui/test/SkikoComposeUiTest : androidx/compose/ui/test/ComposeUiTest {
public static final field $stable I
public field scene Landroidx/compose/ui/scene/ComposeScene;
public fun <init> (IILkotlin/coroutines/CoroutineContext;Landroidx/compose/ui/unit/Density;)V
public synthetic fun <init> (IILkotlin/coroutines/CoroutineContext;Landroidx/compose/ui/unit/Density;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun <init> (IILkotlin/coroutines/CoroutineContext;Landroidx/compose/ui/unit/Density;Landroidx/compose/ui/platform/PlatformContext$SemanticsOwnerListener;Lkotlinx/coroutines/test/TestDispatcher;)V
public synthetic fun <init> (IILkotlin/coroutines/CoroutineContext;Landroidx/compose/ui/unit/Density;Landroidx/compose/ui/platform/PlatformContext$SemanticsOwnerListener;Lkotlinx/coroutines/test/TestDispatcher;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
protected fun areAllResourcesIdle ()Z
public fun awaitIdle (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public final fun captureToImage ()Landroidx/compose/ui/graphics/ImageBitmap;
public final fun captureToImage (Landroidx/compose/ui/semantics/SemanticsNode;)Landroidx/compose/ui/graphics/ImageBitmap;
Expand All @@ -656,13 +664,11 @@ public final class androidx/compose/ui/test/SkikoComposeUiTest : androidx/compos
public final fun getScene ()Landroidx/compose/ui/scene/ComposeScene;
public fun onAllNodes (Landroidx/compose/ui/test/SemanticsMatcher;Z)Landroidx/compose/ui/test/SemanticsNodeInteractionCollection;
public fun onNode (Landroidx/compose/ui/test/SemanticsMatcher;Z)Landroidx/compose/ui/test/SemanticsNodeInteraction;
public fun registerIdlingResource (Landroidx/compose/ui/test/IdlingResource;)V
public fun runOnIdle (Lkotlin/jvm/functions/Function0;)Ljava/lang/Object;
public fun runOnUiThread (Lkotlin/jvm/functions/Function0;)Ljava/lang/Object;
public final fun runTest (Lkotlin/jvm/functions/Function1;)Ljava/lang/Object;
public fun setContent (Lkotlin/jvm/functions/Function2;)V
public final fun setScene (Landroidx/compose/ui/scene/ComposeScene;)V
public fun unregisterIdlingResource (Landroidx/compose/ui/test/IdlingResource;)V
public fun waitForIdle ()V
public fun waitUntil (Ljava/lang/String;JLkotlin/jvm/functions/Function0;)V
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ class CustomEffectContextTest {
runOnIdle {
val elementFromComposition =
compositionScope.coroutineContext[TestCoroutineContextElement]
Truth.assertThat(elementFromComposition).isSameInstanceAs(testElement)
assertThat(elementFromComposition).isSameInstanceAs(testElement)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -676,9 +676,11 @@ actual sealed interface ComposeUiTest : SemanticsNodeInteractionsProvider {
condition: () -> Boolean
)

actual fun registerIdlingResource(idlingResource: IdlingResource)
/** Registers an [IdlingResource] in this test. */
fun registerIdlingResource(idlingResource: IdlingResource)

actual fun unregisterIdlingResource(idlingResource: IdlingResource)
/** Unregisters an [IdlingResource] from this test. */
fun unregisterIdlingResource(idlingResource: IdlingResource)

actual fun setContent(composable: @Composable () -> Unit)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,12 +172,6 @@ expect sealed interface ComposeUiTest : SemanticsNodeInteractionsProvider {
condition: () -> Boolean
)

/** Registers an [IdlingResource] in this test. */
fun registerIdlingResource(idlingResource: IdlingResource)

/** Unregisters an [IdlingResource] from this test. */
fun unregisterIdlingResource(idlingResource: IdlingResource)

/**
* Sets the given [composable] as the content to be tested. This should be called exactly once
* per test.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,10 @@

package androidx.compose.ui.test

import androidx.compose.ui.unit.Density
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext

@OptIn(ExperimentalTestApi::class)
typealias DesktopComposeUiTest = SkikoComposeUiTest

/**
* Variant of [runComposeUiTest] that allows you to specify the size of the surface.
*
Expand All @@ -38,5 +36,36 @@ fun runDesktopComposeUiTest(
effectContext: CoroutineContext = EmptyCoroutineContext,
block: DesktopComposeUiTest.() -> Unit
) {
DesktopComposeUiTest(width, height, effectContext).runTest(block)
with(DesktopComposeUiTest(width, height, effectContext)) {
runTest { block() }
}
}

@ExperimentalTestApi
class DesktopComposeUiTest(
width: Int = 1024,
height: Int = 768,
effectContext: CoroutineContext = EmptyCoroutineContext,
density: Density = Density(1f),
) : SkikoComposeUiTest(width, height, effectContext, density) {

private val idlingResources = mutableSetOf<IdlingResource>()

override fun areAllResourcesIdle(): Boolean {
return synchronized(idlingResources) {
idlingResources.all { it.isIdleNow }
}
}

fun registerIdlingResource(idlingResource: IdlingResource) {
synchronized(idlingResources) {
idlingResources.add(idlingResource)
}
}

fun unregisterIdlingResource(idlingResource: IdlingResource) {
synchronized(idlingResources) {
idlingResources.remove(idlingResource)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,15 @@ import java.nio.file.Files
import kotlin.io.path.readBytes
import kotlin.io.path.writeBytes
import kotlin.test.Test
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.jetbrains.skia.EncodedImageFormat
import org.jetbrains.skia.Image

/**
* Tests desktop-specified Test APIs.
* Tests desktop-specific Test APIs.
*/
@OptIn(ExperimentalTestApi::class)
class DesktopTestsTest {
Expand Down Expand Up @@ -125,4 +129,45 @@ class DesktopTestsTest {
}
}
}

@Test
fun testIdlingResource() = runDesktopComposeUiTest {
var text by mutableStateOf("")
setContent {
Text(
text = text,
modifier = Modifier.testTag("text")
)
}

var isIdle = true
val idlingResource = object : IdlingResource {
override val isIdleNow: Boolean
get() = isIdle
}

fun test(expectedValue: String) {
text = "first"
isIdle = false
val job = CoroutineScope(Dispatchers.Default).launch {
delay(1000)
text = "second"
isIdle = true
}
try {
onNodeWithTag("text").assertTextEquals(expectedValue)
} finally {
job.cancel()
}
}

// With the idling resource registered, we expect the test to wait until the second value
// has been set.
registerIdlingResource(idlingResource)
test(expectedValue = "second")

// Without the idling resource registered, we expect the test to see the first value
unregisterIdlingResource(idlingResource)
test(expectedValue = "first")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,6 @@ import androidx.compose.ui.platform.WindowInfo
import androidx.compose.ui.scene.ComposeScene
import androidx.compose.ui.scene.CanvasLayersComposeScene
import androidx.compose.ui.semantics.SemanticsNode
import androidx.compose.ui.test.platform.makeSynchronizedObject
import androidx.compose.ui.test.platform.synchronized
import androidx.compose.ui.text.input.EditCommand
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.ImeOptions
Expand Down Expand Up @@ -126,7 +124,7 @@ fun defaultTestDispatcher() = UnconfinedTestDispatcher()
*/
@ExperimentalTestApi
@OptIn(InternalTestApi::class, InternalComposeUiApi::class)
class SkikoComposeUiTest @InternalTestApi constructor(
open class SkikoComposeUiTest @InternalTestApi constructor(
Copy link
Member Author

@eymar eymar Feb 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why I think it's okay to make it open:

  • it's experimental, so such changes are okay. If someone uses/extends it, it's at their own risk.
  • SkikoComposeUiTest is not available in common tests (with Android), which I believe is the main place where the tests should be added in Compose Multiplatform case. Those tests will use only ComposeUiTest interface.
  • In the future, I believe we will need to update this API, since the intention is to not expose "skiko". SkikoComposeUiTest exposes scene, which is convenient, but it's not supposed to be used outside of Compose - InternalComposeUiApi.

width: Int = 1024,
height: Int = 768,
// TODO(/~https://github.com/JetBrains/compose-multiplatform/issues/2960) Support effectContext
Expand Down Expand Up @@ -190,9 +188,6 @@ class SkikoComposeUiTest @InternalTestApi constructor(
private val testOwner = SkikoTestOwner()
private val testContext = TestContext(testOwner)

private val idlingResources = mutableSetOf<IdlingResource>()
private val idlingResourcesLock = makeSynchronizedObject(idlingResources)

fun <R> runTest(block: SkikoComposeUiTest.() -> R): R {
return composeRootRegistry.withRegistry {
withScene {
Expand Down Expand Up @@ -334,21 +329,9 @@ class SkikoComposeUiTest @InternalTestApi constructor(
// TODO Add a wait function variant without conditions (timeout exceptions)
}

override fun registerIdlingResource(idlingResource: IdlingResource) {
synchronized(idlingResourcesLock) {
idlingResources.add(idlingResource)
}
}

override fun unregisterIdlingResource(idlingResource: IdlingResource) {
synchronized(idlingResourcesLock) {
idlingResources.remove(idlingResource)
}
}

private fun areAllResourcesIdle() = synchronized(idlingResourcesLock) {
idlingResources.all { it.isIdleNow }
}
// Only DesktopComposeUiTest supports IdlingResource registration,
// so by default SkikoComposeUiTest doesn't expect any IdlingResource
protected open fun areAllResourcesIdle() = true

override fun setContent(composable: @Composable () -> Unit) {
if (isOnUiThread()) {
Expand Down Expand Up @@ -510,8 +493,6 @@ actual sealed interface ComposeUiTest : SemanticsNodeInteractionsProvider {
timeoutMillis: Long,
condition: () -> Boolean
)
actual fun registerIdlingResource(idlingResource: IdlingResource)
actual fun unregisterIdlingResource(idlingResource: IdlingResource)
actual fun setContent(composable: @Composable () -> Unit)

actual fun enableAccessibilityChecks()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,7 @@ import kotlin.test.assertTrue
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
import kotlinx.atomicfu.atomic
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runTest


Expand Down Expand Up @@ -124,47 +121,6 @@ class TestBasicsTest {
}
}

@Test
fun testIdlingResource() = runComposeUiTest {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's moved to IdlingResourceTest in deskopTest

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd move it to DesktopTestsTest. I don't think it deserves its own file.

(if you do move it, please also fix the typo in the kdoc for that class: "specified" -> "specific").

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

var text by mutableStateOf("")
setContent {
Text(
text = text,
modifier = Modifier.testTag("text")
)
}

var isIdle = true
val idlingResource = object : IdlingResource {
override val isIdleNow: Boolean
get() = isIdle
}

fun test(expectedValue: String) {
text = "first"
isIdle = false
val job = CoroutineScope(Dispatchers.Default).launch {
delay(1000)
text = "second"
isIdle = true
}
try {
onNodeWithTag("text").assertTextEquals(expectedValue)
} finally {
job.cancel()
}
}

// With the idling resource registered, we expect the test to wait until the second value
// has been set.
registerIdlingResource(idlingResource)
test(expectedValue = "second")

// Without the idling resource registered, we expect the test to see the first value
unregisterIdlingResource(idlingResource)
test(expectedValue = "first")
}

@Test
fun infiniteDelayLoopInLaunchedEffectDoesNotHang() = runComposeUiTest {
runTest(timeout = 500.milliseconds) {
Expand Down