|
| 1 | +/* |
| 2 | + * Copyright 2026 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 | +package androidx.compose.ui.test |
| 18 | + |
| 19 | +import androidx.compose.animation.animateContentSize |
| 20 | +import androidx.compose.foundation.background |
| 21 | +import androidx.compose.foundation.layout.Box |
| 22 | +import androidx.compose.foundation.layout.size |
| 23 | +import androidx.compose.runtime.CompositionLocalProvider |
| 24 | +import androidx.compose.runtime.getValue |
| 25 | +import androidx.compose.runtime.mutableStateOf |
| 26 | +import androidx.compose.runtime.setValue |
| 27 | +import androidx.compose.ui.Modifier |
| 28 | +import androidx.compose.ui.graphics.Color |
| 29 | +import androidx.compose.ui.platform.LocalDensity |
| 30 | +import androidx.compose.ui.platform.testTag |
| 31 | +import androidx.compose.ui.test.v2.runComposeUiTest |
| 32 | +import androidx.compose.ui.unit.Density |
| 33 | +import androidx.compose.ui.unit.IntSize |
| 34 | +import androidx.compose.ui.unit.dp |
| 35 | +import androidx.kruth.assertThat |
| 36 | +import kotlin.test.Test |
| 37 | + |
| 38 | +// Copied from androidDeviceTest |
| 39 | +class RunWithoutImplicitWaitTest { |
| 40 | + @OptIn(ExperimentalTestApi::class) |
| 41 | + @Test |
| 42 | + fun triggeredAnimationAndCaptureMotionValues() = runComposeUiTest { |
| 43 | + var size by mutableStateOf(64.dp) |
| 44 | + |
| 45 | + var animationIsDone = false |
| 46 | + setContent { |
| 47 | + CompositionLocalProvider(LocalDensity provides Density(1f)) { |
| 48 | + Box( |
| 49 | + modifier = |
| 50 | + Modifier.testTag("foo") |
| 51 | + .animateContentSize { _, _ -> animationIsDone = true } |
| 52 | + .size(size) |
| 53 | + .background(Color.Red) |
| 54 | + ) |
| 55 | + } |
| 56 | + } |
| 57 | + |
| 58 | + mainClock.autoAdvance = false |
| 59 | + |
| 60 | + val timeSeries = mutableListOf<IntSize>() |
| 61 | + size = 32.dp |
| 62 | + |
| 63 | + while (!animationIsDone) { |
| 64 | + mainClock.advanceTimeByFrame() |
| 65 | + waitForIdle() |
| 66 | + runOnUiThread { runWithoutImplicitWait { captureMotionTestValues(timeSeries) } } |
| 67 | + } |
| 68 | + assertThat(timeSeries) |
| 69 | + .containsExactly( |
| 70 | + IntSize(64, 64), |
| 71 | + IntSize(64, 64), |
| 72 | + IntSize(63, 63), |
| 73 | + IntSize(60, 60), |
| 74 | + IntSize(56, 56), |
| 75 | + IntSize(52, 52), |
| 76 | + IntSize(49, 49), |
| 77 | + IntSize(46, 46), |
| 78 | + IntSize(43, 43), |
| 79 | + IntSize(41, 41), |
| 80 | + IntSize(39, 39), |
| 81 | + IntSize(37, 37), |
| 82 | + IntSize(36, 36), |
| 83 | + IntSize(35, 35), |
| 84 | + IntSize(35, 35), |
| 85 | + IntSize(34, 34), |
| 86 | + IntSize(34, 34), |
| 87 | + IntSize(33, 33), |
| 88 | + IntSize(32, 32), |
| 89 | + ) |
| 90 | + .inOrder() |
| 91 | + } |
| 92 | + |
| 93 | + @OptIn(ExperimentalTestApi::class) |
| 94 | + @Test |
| 95 | + fun runWithoutImplicitWait_doesNotTriggerWaitForIdle() = runComposeUiTest { |
| 96 | + var isNodeVisible by mutableStateOf(true) |
| 97 | + |
| 98 | + setContent { |
| 99 | + if (isNodeVisible) { |
| 100 | + Box(modifier = Modifier.testTag("dummy_node")) |
| 101 | + } |
| 102 | + } |
| 103 | + |
| 104 | + isNodeVisible = false |
| 105 | + |
| 106 | + runOnUiThread { |
| 107 | + runWithoutImplicitWait { |
| 108 | + // In a normal context, onNodeWithTag() forces a waitForIdle(), |
| 109 | + // which would execute the pending recomposition. |
| 110 | + onNodeWithTag("dummy_node") |
| 111 | + .assertExists( |
| 112 | + "waitForIdle() was called internally, breaking the suppression contract!" |
| 113 | + ) |
| 114 | + } |
| 115 | + } |
| 116 | + // Calling onNodeWithTag outside runWithoutImplicitWait will now force idle. |
| 117 | + // This executes the pending recomposition, removing the node, and hence it won't exist |
| 118 | + // anymore. |
| 119 | + onNodeWithTag("dummy_node").assertDoesNotExist() |
| 120 | + } |
| 121 | + |
| 122 | + /** |
| 123 | + * Illustrative implementation of a "sample the property values of the current frame" method. |
| 124 | + * |
| 125 | + * Motion tests do exactly that, just with more syntactic sugar 🍬. |
| 126 | + */ |
| 127 | + @OptIn(ExperimentalTestApi::class) |
| 128 | + private fun ComposeUiTest.captureMotionTestValues(fooSizeTimeSeries: MutableList<IntSize>) { |
| 129 | + // Capture a value and add to the time series. |
| 130 | + fooSizeTimeSeries.add(onNodeWithTag("foo").fetchSemanticsNode().size) |
| 131 | + |
| 132 | + repeat(100) { |
| 133 | + // simulation of capturing multiple properties. |
| 134 | + // For making the point, this just repeatedly captures the same property. |
| 135 | + onNodeWithTag("foo").fetchSemanticsNode().size |
| 136 | + } |
| 137 | + } |
| 138 | +} |
0 commit comments