Merge "Add an overload of waitUntil that takes a custom message." into androidx-main am: 6bb44267cf

Original change: https://android-review.googlesource.com/c/platform/frameworks/support/+/2924898

Change-Id: I05e43d35bf381e282b5bf2a550aa7d92c9c07611
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/compose/ui/ui-test-junit4/api/current.txt b/compose/ui/ui-test-junit4/api/current.txt
index 0364dec..edfb551 100644
--- a/compose/ui/ui-test-junit4/api/current.txt
+++ b/compose/ui/ui-test-junit4/api/current.txt
@@ -59,6 +59,7 @@
     method public <T> T runOnUiThread(kotlin.jvm.functions.Function0<? extends T> action);
     method public void unregisterIdlingResource(androidx.compose.ui.test.IdlingResource idlingResource);
     method public void waitForIdle();
+    method public default void waitUntil(String conditionDescription, optional long timeoutMillis, kotlin.jvm.functions.Function0<java.lang.Boolean> condition);
     method public void waitUntil(optional long timeoutMillis, kotlin.jvm.functions.Function0<java.lang.Boolean> condition);
     method @SuppressCompatibility @androidx.compose.ui.test.ExperimentalTestApi public void waitUntilAtLeastOneExists(androidx.compose.ui.test.SemanticsMatcher matcher, optional long timeoutMillis);
     method @SuppressCompatibility @androidx.compose.ui.test.ExperimentalTestApi public void waitUntilDoesNotExist(androidx.compose.ui.test.SemanticsMatcher matcher, optional long timeoutMillis);
diff --git a/compose/ui/ui-test-junit4/api/restricted_current.txt b/compose/ui/ui-test-junit4/api/restricted_current.txt
index 0364dec..edfb551 100644
--- a/compose/ui/ui-test-junit4/api/restricted_current.txt
+++ b/compose/ui/ui-test-junit4/api/restricted_current.txt
@@ -59,6 +59,7 @@
     method public <T> T runOnUiThread(kotlin.jvm.functions.Function0<? extends T> action);
     method public void unregisterIdlingResource(androidx.compose.ui.test.IdlingResource idlingResource);
     method public void waitForIdle();
+    method public default void waitUntil(String conditionDescription, optional long timeoutMillis, kotlin.jvm.functions.Function0<java.lang.Boolean> condition);
     method public void waitUntil(optional long timeoutMillis, kotlin.jvm.functions.Function0<java.lang.Boolean> condition);
     method @SuppressCompatibility @androidx.compose.ui.test.ExperimentalTestApi public void waitUntilAtLeastOneExists(androidx.compose.ui.test.SemanticsMatcher matcher, optional long timeoutMillis);
     method @SuppressCompatibility @androidx.compose.ui.test.ExperimentalTestApi public void waitUntilDoesNotExist(androidx.compose.ui.test.SemanticsMatcher matcher, optional long timeoutMillis);
diff --git a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidComposeTestRule.android.kt b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidComposeTestRule.android.kt
index 0ee15e0..149daf9 100644
--- a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidComposeTestRule.android.kt
+++ b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidComposeTestRule.android.kt
@@ -303,7 +303,15 @@
     override suspend fun awaitIdle() = composeTest.awaitIdle()
 
     override fun waitUntil(timeoutMillis: Long, condition: () -> Boolean) =
-        composeTest.waitUntil(timeoutMillis, condition)
+        composeTest.waitUntil(conditionDescription = null, timeoutMillis, condition)
+
+    override fun waitUntil(
+        conditionDescription: String,
+        timeoutMillis: Long,
+        condition: () -> Boolean
+    ) {
+        composeTest.waitUntil(conditionDescription, timeoutMillis, condition)
+    }
 
     @ExperimentalTestApi
     override fun waitUntilNodeCount(matcher: SemanticsMatcher, count: Int, timeoutMillis: Long) =
diff --git a/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/junit4/DesktopComposeTestRule.desktop.kt b/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/junit4/DesktopComposeTestRule.desktop.kt
index 9fd4197..43987e6 100644
--- a/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/junit4/DesktopComposeTestRule.desktop.kt
+++ b/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/junit4/DesktopComposeTestRule.desktop.kt
@@ -91,7 +91,15 @@
     override suspend fun awaitIdle() = composeTest.awaitIdle()
 
     override fun waitUntil(timeoutMillis: Long, condition: () -> Boolean) =
-        composeTest.waitUntil(timeoutMillis, condition)
+        composeTest.waitUntil(conditionDescription = null, timeoutMillis, condition)
+
+    override fun waitUntil(
+        conditionDescription: String,
+        timeoutMillis: Long,
+        condition: () -> Boolean
+    ) {
+        composeTest.waitUntil(conditionDescription, timeoutMillis, condition)
+    }
 
     @ExperimentalTestApi
     override fun waitUntilNodeCount(matcher: SemanticsMatcher, count: Int, timeoutMillis: Long) =
diff --git a/compose/ui/ui-test-junit4/src/jvmMain/kotlin/androidx/compose/ui/test/junit4/ComposeTestRule.jvm.kt b/compose/ui/ui-test-junit4/src/jvmMain/kotlin/androidx/compose/ui/test/junit4/ComposeTestRule.jvm.kt
index 9ee2023..14c64be 100644
--- a/compose/ui/ui-test-junit4/src/jvmMain/kotlin/androidx/compose/ui/test/junit4/ComposeTestRule.jvm.kt
+++ b/compose/ui/ui-test-junit4/src/jvmMain/kotlin/androidx/compose/ui/test/junit4/ComposeTestRule.jvm.kt
@@ -133,6 +133,38 @@
     fun waitUntil(timeoutMillis: Long = 1_000, condition: () -> Boolean)
 
     /**
+     * Blocks until the given condition is satisfied.
+     *
+     * In case the main clock auto advancement is enabled (by default is), this will also keep
+     * advancing the clock on a frame by frame basis and yield for other async work at the end of
+     * each frame. If the advancement of the main clock is not enabled this will work as a
+     * countdown latch without any other advancements.
+     *
+     * There is also [MainTestClock.advanceTimeUntil] which is faster as it does not yield back
+     * the UI thread.
+     *
+     * This method should be used in cases where [MainTestClock.advanceTimeUntil]
+     * is not enough.
+     *
+     * @param timeoutMillis The time after which this method throws an exception if the given
+     * condition is not satisfied. This is the wall clock time not the main clock one.
+     * @param conditionDescription A human-readable description of [condition] that will be included
+     * in the timeout exception if thrown.
+     * @param condition Condition that must be satisfied in order for this method to successfully
+     * finish.
+     *
+     * @throws androidx.compose.ui.test.ComposeTimeoutException If the condition is not satisfied
+     * after [timeoutMillis].
+     */
+    fun waitUntil(
+        conditionDescription: String,
+        timeoutMillis: Long = 1_000,
+        condition: () -> Boolean
+    ) {
+        waitUntil(timeoutMillis, condition)
+    }
+
+    /**
      * Blocks until the number of nodes matching the given [matcher] is equal to the given [count].
      *
      * @see ComposeTestRule.waitUntil
diff --git a/compose/ui/ui-test/api/current.txt b/compose/ui/ui-test/api/current.txt
index 32645f8..eab3e6b 100644
--- a/compose/ui/ui-test/api/current.txt
+++ b/compose/ui/ui-test/api/current.txt
@@ -100,7 +100,7 @@
     method public void setContent(kotlin.jvm.functions.Function0<kotlin.Unit> composable);
     method public void unregisterIdlingResource(androidx.compose.ui.test.IdlingResource idlingResource);
     method public void waitForIdle();
-    method public void waitUntil(optional long timeoutMillis, kotlin.jvm.functions.Function0<java.lang.Boolean> condition);
+    method public void waitUntil(optional String? conditionDescription, optional long timeoutMillis, kotlin.jvm.functions.Function0<java.lang.Boolean> condition);
     property public abstract androidx.compose.ui.unit.Density density;
     property public abstract androidx.compose.ui.test.MainTestClock mainClock;
   }
diff --git a/compose/ui/ui-test/api/restricted_current.txt b/compose/ui/ui-test/api/restricted_current.txt
index bdff3c0..1dfc373 100644
--- a/compose/ui/ui-test/api/restricted_current.txt
+++ b/compose/ui/ui-test/api/restricted_current.txt
@@ -100,7 +100,7 @@
     method public void setContent(kotlin.jvm.functions.Function0<kotlin.Unit> composable);
     method public void unregisterIdlingResource(androidx.compose.ui.test.IdlingResource idlingResource);
     method public void waitForIdle();
-    method public void waitUntil(optional long timeoutMillis, kotlin.jvm.functions.Function0<java.lang.Boolean> condition);
+    method public void waitUntil(optional String? conditionDescription, optional long timeoutMillis, kotlin.jvm.functions.Function0<java.lang.Boolean> condition);
     property public abstract androidx.compose.ui.unit.Density density;
     property public abstract androidx.compose.ui.test.MainTestClock mainClock;
   }
diff --git a/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/junit4/ComposeTestRuleWaitUntilTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/junit4/ComposeTestRuleWaitUntilTest.kt
index 3da62e9..12f2c73 100644
--- a/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/junit4/ComposeTestRuleWaitUntilTest.kt
+++ b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/junit4/ComposeTestRuleWaitUntilTest.kt
@@ -53,6 +53,22 @@
     )
 
     @Test
+    fun waitUntil_includesConditionDescription_whenSpecified() {
+        rule.setContent {
+            TaggedBox()
+        }
+
+        expectError<ComposeTimeoutException>(
+            // This is actually regex, so special characters need to be escaped.
+            expectedMessage = "Condition \\(foo\\) still not satisfied after $Timeout ms"
+        ) {
+            rule.waitUntil("foo", timeoutMillis = Timeout) {
+                false
+            }
+        }
+    }
+
+    @Test
     fun waitUntilNodeCount_succeedsWhen_nodeCountCorrect() {
         rule.setContent {
             TaggedBox()
@@ -72,7 +88,8 @@
         }
 
         expectError<ComposeTimeoutException>(
-            expectedMessage = "Condition still not satisfied after $Timeout ms"
+            expectedMessage = "Condition \\(exactly 2 nodes match \\(TestTag = 'TestTag'\\)\\) " +
+                "still not satisfied after $Timeout ms"
         ) {
             rule.waitUntilNodeCount(hasTestTag(TestTag), 2, Timeout)
         }
@@ -95,7 +112,8 @@
         }
 
         expectError<ComposeTimeoutException>(
-            expectedMessage = "Condition still not satisfied after $Timeout ms"
+            expectedMessage = "Condition \\(at least one node matches " +
+                "\\(TestTag = 'TestTag'\\)\\) still not satisfied after $Timeout ms"
         ) {
             rule.waitUntilAtLeastOneExists(hasTestTag(TestTag), Timeout)
         }
@@ -118,7 +136,8 @@
         }
 
         expectError<ComposeTimeoutException>(
-            expectedMessage = "Condition still not satisfied after $Timeout ms"
+            expectedMessage = "Condition \\(exactly 1 nodes match \\(TestTag = 'TestTag'\\)\\) " +
+                "still not satisfied after $Timeout ms"
         ) {
             rule.waitUntilExactlyOneExists(hasTestTag(TestTag), Timeout)
         }
@@ -140,7 +159,8 @@
         }
 
         expectError<ComposeTimeoutException>(
-            expectedMessage = "Condition still not satisfied after $Timeout ms"
+            expectedMessage = "Condition \\(exactly 0 nodes match \\(TestTag = 'TestTag'\\)\\) " +
+                "still not satisfied after $Timeout ms"
         ) {
             rule.waitUntilDoesNotExist(hasTestTag(TestTag), timeoutMillis = Timeout)
         }
diff --git a/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/junit4/WaitUntilNodeCountTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/junit4/WaitUntilNodeCountTest.kt
index 0d78ba2..bf923fd 100644
--- a/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/junit4/WaitUntilNodeCountTest.kt
+++ b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/junit4/WaitUntilNodeCountTest.kt
@@ -72,7 +72,8 @@
         }
 
         expectError<ComposeTimeoutException>(
-            expectedMessage = "Condition still not satisfied after $Timeout ms"
+            expectedMessage = "Condition \\(exactly 2 nodes match \\(TestTag = 'TestTag'\\)\\) " +
+                "still not satisfied after $Timeout ms"
         ) {
             waitUntilNodeCount(hasTestTag(TestTag), 2, Timeout)
         }
@@ -95,7 +96,8 @@
         }
 
         expectError<ComposeTimeoutException>(
-            expectedMessage = "Condition still not satisfied after $Timeout ms"
+            expectedMessage = "Condition \\(at least one node matches " +
+                "\\(TestTag = 'TestTag'\\)\\) still not satisfied after $Timeout ms"
         ) {
             waitUntilAtLeastOneExists(hasTestTag(TestTag), Timeout)
         }
@@ -118,7 +120,8 @@
         }
 
         expectError<ComposeTimeoutException>(
-            expectedMessage = "Condition still not satisfied after $Timeout ms"
+            expectedMessage = "Condition \\(exactly 1 nodes match \\(TestTag = 'TestTag'\\)\\) " +
+                "still not satisfied after $Timeout ms"
         ) {
             waitUntilExactlyOneExists(hasTestTag(TestTag), Timeout)
         }
@@ -140,7 +143,8 @@
         }
 
         expectError<ComposeTimeoutException>(
-            expectedMessage = "Condition still not satisfied after $Timeout ms"
+            expectedMessage = "Condition \\(exactly 0 nodes match \\(TestTag = 'TestTag'\\)\\) " +
+                "still not satisfied after $Timeout ms"
         ) {
             waitUntilDoesNotExist(hasTestTag(TestTag), timeoutMillis = Timeout)
         }
diff --git a/compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/ComposeUiTest.android.kt b/compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/ComposeUiTest.android.kt
index 72a79c6..887b0d9 100644
--- a/compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/ComposeUiTest.android.kt
+++ b/compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/ComposeUiTest.android.kt
@@ -425,7 +425,11 @@
             coroutineExceptionHandler.throwUncaught()
         }
 
-        override fun waitUntil(timeoutMillis: Long, condition: () -> Boolean) {
+        override fun waitUntil(
+            conditionDescription: String?,
+            timeoutMillis: Long,
+            condition: () -> Boolean
+        ) {
             val startTime = System.nanoTime()
             while (!condition()) {
                 if (mainClockImpl.autoAdvance) {
@@ -435,7 +439,7 @@
                 Thread.sleep(10)
                 if (System.nanoTime() - startTime > timeoutMillis * NanoSecondsPerMilliSecond) {
                     throw ComposeTimeoutException(
-                        "Condition still not satisfied after $timeoutMillis ms"
+                        buildWaitUntilTimeoutMessage(timeoutMillis, conditionDescription)
                     )
                 }
             }
@@ -556,7 +560,11 @@
     actual fun <T> runOnIdle(action: () -> T): T
     actual fun waitForIdle()
     actual suspend fun awaitIdle()
-    actual fun waitUntil(timeoutMillis: Long, condition: () -> Boolean)
+    actual fun waitUntil(
+        conditionDescription: String?,
+        timeoutMillis: Long,
+        condition: () -> Boolean
+    )
     actual fun registerIdlingResource(idlingResource: IdlingResource)
     actual fun unregisterIdlingResource(idlingResource: IdlingResource)
     actual fun setContent(composable: @Composable () -> Unit)
diff --git a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/ComposeUiTest.kt b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/ComposeUiTest.kt
index 3414dab..52c4acc 100644
--- a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/ComposeUiTest.kt
+++ b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/ComposeUiTest.kt
@@ -158,13 +158,19 @@
      *
      * @param timeoutMillis The time after which this method throws an exception if the given
      * condition is not satisfied. This observes wall clock time, not [frame time][mainClock].
+     * @param conditionDescription An optional human-readable description of [condition] that will
+     * be included in the timeout exception if thrown.
      * @param condition Condition that must be satisfied in order for this method to successfully
      * finish.
      *
      * @throws androidx.compose.ui.test.ComposeTimeoutException If the condition is not satisfied
      * after [timeoutMillis] (in wall clock time).
      */
-    fun waitUntil(timeoutMillis: Long = 1_000, condition: () -> Boolean)
+    fun waitUntil(
+        conditionDescription: String? = null,
+        timeoutMillis: Long = 1_000,
+        condition: () -> Boolean
+    )
 
     /**
      * Registers an [IdlingResource] in this test.
@@ -192,7 +198,7 @@
  * @see ComposeUiTest.waitUntil
  *
  * @param matcher The matcher that will be used to filter nodes.
- * @param count The number of nodes that are expected to
+ * @param count The number of nodes that are expected to be matched.
  * @param timeoutMillis The time after which this method throws an exception if the number of nodes
  * that match the [matcher] is not [count]. This observes wall clock time, not frame time.
  *
@@ -205,7 +211,7 @@
     count: Int,
     timeoutMillis: Long = 1_000L
 ) {
-    waitUntil(timeoutMillis) {
+    waitUntil("exactly $count nodes match (${matcher.description})", timeoutMillis) {
         onAllNodes(matcher).fetchSemanticsNodes().size == count
     }
 }
@@ -227,7 +233,7 @@
     matcher: SemanticsMatcher,
     timeoutMillis: Long = 1_000L
 ) {
-    waitUntil(timeoutMillis) {
+    waitUntil("at least one node matches (${matcher.description})", timeoutMillis) {
         onAllNodes(matcher).fetchSemanticsNodes().isNotEmpty()
     }
 }
@@ -269,3 +275,16 @@
 ) = waitUntilNodeCount(matcher, 0, timeoutMillis)
 
 internal const val NanoSecondsPerMilliSecond = 1_000_000L
+
+internal fun buildWaitUntilTimeoutMessage(
+    timeoutMillis: Long,
+    conditionDescription: String?
+): String = buildString {
+    append("Condition ")
+    if (conditionDescription != null) {
+        append('(')
+        append(conditionDescription)
+        append(") ")
+    }
+    append("still not satisfied after $timeoutMillis ms")
+}
diff --git a/compose/ui/ui-test/src/desktopMain/kotlin/androidx/compose/ui/test/ComposeUiTest.desktop.kt b/compose/ui/ui-test/src/desktopMain/kotlin/androidx/compose/ui/test/ComposeUiTest.desktop.kt
index f619bc3..5ee58ae 100644
--- a/compose/ui/ui-test/src/desktopMain/kotlin/androidx/compose/ui/test/ComposeUiTest.desktop.kt
+++ b/compose/ui/ui-test/src/desktopMain/kotlin/androidx/compose/ui/test/ComposeUiTest.desktop.kt
@@ -143,13 +143,17 @@
         return action().also { waitForIdle() }
     }
 
-    override fun waitUntil(timeoutMillis: Long, condition: () -> Boolean) {
+    override fun waitUntil(
+        conditionDescription: String?,
+        timeoutMillis: Long,
+        condition: () -> Boolean
+    ) {
         val startTime = System.nanoTime()
         while (!condition()) {
             renderNextFrame()
             if (System.nanoTime() - startTime > timeoutMillis * NanoSecondsPerMilliSecond) {
                 throw ComposeTimeoutException(
-                    "Condition still not satisfied after $timeoutMillis ms"
+                    buildWaitUntilTimeoutMessage(timeoutMillis, conditionDescription)
                 )
             }
         }
@@ -222,7 +226,11 @@
     actual fun <T> runOnIdle(action: () -> T): T
     actual fun waitForIdle()
     actual suspend fun awaitIdle()
-    actual fun waitUntil(timeoutMillis: Long, condition: () -> Boolean)
+    actual fun waitUntil(
+        conditionDescription: String?,
+        timeoutMillis: Long,
+        condition: () -> Boolean
+    )
     actual fun registerIdlingResource(idlingResource: IdlingResource)
     actual fun unregisterIdlingResource(idlingResource: IdlingResource)
     actual fun setContent(composable: @Composable () -> Unit)
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/platform/WindowInfoTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/platform/WindowInfoTest.kt
index a0f6645..362847e 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/platform/WindowInfoTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/platform/WindowInfoTest.kt
@@ -34,7 +34,7 @@
     fun launchFragment_windowInfo_isWindowFocused_true() {
         runComposeUiTest {
             launchFragmentInContainer<TestFragment>().onFragment {
-                waitUntil(5_000) { it.isWindowFocused == true }
+                waitUntil("isWindowFocused", timeoutMillis = 5_000) { it.isWindowFocused == true }
             }
         }
     }