CoroutineBuilders.kt

/*
 * Copyright 2020 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.core.ManualFrameClock
import androidx.compose.animation.core.MonotonicFrameAnimationClock
import androidx.compose.animation.core.advanceClockMillis
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.yield

/**
 * Runs a new coroutine and blocks the current thread interruptibly until it completes, passing a
 * new [ManualFrameClock] to the code [block]. This is intended to be used by tests instead of
 * [runBlocking] if they want to use a [ManualFrameClock].
 *
 * The clock will start at time 0L and should be driven manually from your test, from the
 * [main dispatcher][Dispatchers.Main]. Pass the clock to the animation that you want to
 * control in your test, and then [advance][advanceClockMillis] it as necessary. After the block
 * has completed, the clock will be forwarded with 10 second increments until it has drained all
 * work that took frames from that clock. If the work never ends, this function never ends, so
 * make sure that all animations driven by this clock are finite.
 *
 * For example:
 * ```
 * @Test
 * fun myTest() = runWithManualClock { clock ->
 *     // set some compose content
 *     testRule.setContent {
 *         MyAnimation(animationClock = clock)
 *     }
 *     // advance the clock by 1 second
 *     withContext(TestUiDispatcher.Main) {
 *         clock.advanceClock(1000L)
 *     }
 *     // await composition(s)
 *     waitForIdle()
 *     // check if the animation is finished or not
 *     if (clock.hasAwaiters) {
 *         println("The animation is still running")
 *     } else {
 *         println("The animation is done")
 *     }
 * }
 * ```
 * Here, `MyAnimation` is an animation that takes frames from the `animationClock` passed to it.
 *
 * It is good practice to add the animation clock to the parameters of an animation state to
 * improve testability. For example, [DrawerState][androidx.compose.material.DrawerState] accepts
 * an animation clock in the form of [AnimationClockObservable][androidx.compose.animation.core
 * .AnimationClockObservable]. Wrap the [ManualFrameClock] in a [MonotonicFrameAnimationClock]
 * and pass the wrapped clock if you want to manually drive such animations.
 *
 * @param compatibleWithManualAnimationClock If set to `true`, and this clock is used in a
 * [MonotonicFrameAnimationClock], will make the MonotonicFrameAnimationClock behave the same
 * as [ManualAnimationClock][androidx.compose.animation.core.ManualAnimationClock] and send the
 * first frame immediately upon subscription. Avoid reliance on this if possible. `false` by
 * default.
 */
@ExperimentalTestApi
fun <R> runBlockingWithManualClock(
    compatibleWithManualAnimationClock: Boolean = false,
    block: suspend CoroutineScope.(clock: ManualFrameClock) -> R
) {
    @Suppress("DEPRECATION")
    val clock = ManualFrameClock(0L, compatibleWithManualAnimationClock)
    return runBlocking(clock) {
        block(clock)
        while (clock.hasAwaiters) {
            clock.advanceClockMillis(10_000L)
            // Give awaiters the chance to await again
            yield()
        }
    }
}