Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -453,8 +453,7 @@ open class SkikoComposeUiTest @InternalTestApi constructor(
}

override fun <T> runWithoutImplicitWait(block: () -> T): T {
// TODO https://youtrack.jetbrains.com/issue/CMP-10244/ui-test.-Implement-runWithoutImplicitWait
throw NotImplementedError("runWithoutImplicitWait is not implemented.")
return testOwner.withImplicitWaitSuppression(isSuppressed = true, block = block)
}

override fun waitUntil(
Expand Down Expand Up @@ -534,6 +533,21 @@ open class SkikoComposeUiTest @InternalTestApi constructor(
return captureToImage(fetchSemanticsNode())
}

/** Executes the given [block] while temporarily setting the implicit wait suppression state. */
private inline fun <T> TestOwner.withImplicitWaitSuppression(
isSuppressed: Boolean,
block: () -> T,
): T {
val previousState = this.isImplicitWaitSuppressed
this.isImplicitWaitSuppressed = isSuppressed
return try {
block()
} finally {
// Always restore the original synchronization state
this.isImplicitWaitSuppressed = previousState
}
}

@OptIn(InternalComposeUiApi::class)
internal inner class SkikoTestOwner : TestOwner {
override var isImplicitWaitSuppressed: Boolean = false
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
/*
* Copyright 2026 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 androidx.compose.ui.test

import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.v2.runComposeUiTest
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import androidx.kruth.assertThat
import kotlin.test.Test

// Copied from androidDeviceTest
class RunWithoutImplicitWaitTest {
@OptIn(ExperimentalTestApi::class)
@Test
fun triggeredAnimationAndCaptureMotionValues() = runComposeUiTest {
var size by mutableStateOf(64.dp)

var animationIsDone = false
setContent {
CompositionLocalProvider(LocalDensity provides Density(1f)) {
Box(
modifier =
Modifier.testTag("foo")
.animateContentSize { _, _ -> animationIsDone = true }
.size(size)
.background(Color.Red)
)
}
}

mainClock.autoAdvance = false

val timeSeries = mutableListOf<IntSize>()
size = 32.dp

while (!animationIsDone) {
mainClock.advanceTimeByFrame()
waitForIdle()
runOnUiThread { runWithoutImplicitWait { captureMotionTestValues(timeSeries) } }
}
assertThat(timeSeries)
.containsExactly(
IntSize(64, 64),
IntSize(64, 64),
IntSize(63, 63),
IntSize(60, 60),
IntSize(56, 56),
IntSize(52, 52),
IntSize(49, 49),
IntSize(46, 46),
IntSize(43, 43),
IntSize(41, 41),
IntSize(39, 39),
IntSize(37, 37),
IntSize(36, 36),
IntSize(35, 35),
IntSize(35, 35),
IntSize(34, 34),
IntSize(34, 34),
IntSize(33, 33),
IntSize(32, 32),
)
.inOrder()
}

@OptIn(ExperimentalTestApi::class)
@Test
fun runWithoutImplicitWait_doesNotTriggerWaitForIdle() = runComposeUiTest {
var isNodeVisible by mutableStateOf(true)

setContent {
if (isNodeVisible) {
Box(modifier = Modifier.testTag("dummy_node"))
}
}

isNodeVisible = false

runOnUiThread {
runWithoutImplicitWait {
// In a normal context, onNodeWithTag() forces a waitForIdle(),
// which would execute the pending recomposition.
onNodeWithTag("dummy_node")
.assertExists(
"waitForIdle() was called internally, breaking the suppression contract!"
)
}
}
// Calling onNodeWithTag outside runWithoutImplicitWait will now force idle.
// This executes the pending recomposition, removing the node, and hence it won't exist
// anymore.
onNodeWithTag("dummy_node").assertDoesNotExist()
}

/**
* Illustrative implementation of a "sample the property values of the current frame" method.
*
* Motion tests do exactly that, just with more syntactic sugar 🍬.
*/
@OptIn(ExperimentalTestApi::class)
private fun ComposeUiTest.captureMotionTestValues(fooSizeTimeSeries: MutableList<IntSize>) {
// Capture a value and add to the time series.
fooSizeTimeSeries.add(onNodeWithTag("foo").fetchSemanticsNode().size)

repeat(100) {
// simulation of capturing multiple properties.
// For making the point, this just repeatedly captures the same property.
onNodeWithTag("foo").fetchSemanticsNode().size
}
}
}
Loading