[go: nahoru, domu]

blob: 65431be04a88eb48ff4e49b891edb24058c9624c [file] [log] [blame]
/*
* Copyright 2019 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.ui.test.android
import android.os.Handler
import android.os.Looper
import androidx.compose.runtime.ExperimentalComposeApi
import androidx.compose.runtime.Recomposer
import androidx.compose.runtime.snapshots.Snapshot
import androidx.test.espresso.IdlingRegistry
import androidx.test.espresso.IdlingResource
import androidx.ui.test.TestAnimationClock
import androidx.ui.test.runOnUiThread
import java.util.concurrent.atomic.AtomicBoolean
/**
* In case Espresso times out, implementing this interface enables our resources to explain why
* they failed to synchronize in case they were busy.
*/
internal interface IdlingResourceWithDiagnostics {
// TODO: Consider this as a public API.
fun getDiagnosticMessageIfBusy(): String?
}
/**
* Register compose's idling check to Espresso.
*
* This makes sure that Espresso is able to wait for any pending changes in Compose. This
* resource is automatically registered when any compose testing APIs are used including
* [createAndroidComposeRule]. If you for some reasons want to only use Espresso but still have it
* wait for Compose being idle you can use this function.
*/
fun registerComposeWithEspresso() {
ComposeIdlingResource.registerSelfIntoEspresso()
FirstDrawIdlingResource.registerSelfIntoEspresso()
}
/**
* Unregisters resource registered as part of [registerComposeWithEspresso].
*/
fun unregisterComposeFromEspresso() {
ComposeIdlingResource.unregisterSelfFromEspresso()
FirstDrawIdlingResource.unregisterSelfFromEspresso()
}
/**
* Registers the given [clock] so Espresso can await the animations subscribed to that clock.
*/
fun registerTestClock(clock: TestAnimationClock) {
ComposeIdlingResource.registerTestClock(clock)
}
/**
* Unregisters the [clock] that was registered with [registerTestClock].
*/
fun unregisterTestClock(clock: TestAnimationClock) {
ComposeIdlingResource.unregisterTestClock(clock)
}
/**
* Provides an idle check to be registered into Espresso.
*
* This makes sure that Espresso is able to wait for any pending changes in Compose. This
* resource is automatically registered when any compose testing APIs are used including
* [createAndroidComposeRule]. If you for some reasons want to only use Espresso but still have it
* wait for Compose being idle you can register this yourself via [registerSelfIntoEspresso].
*/
internal object ComposeIdlingResource : BaseIdlingResource(), IdlingResourceWithDiagnostics {
override fun getName(): String = "ComposeIdlingResource"
private var isIdleCheckScheduled = false
private val clocks = mutableSetOf<TestAnimationClock>()
private val handler = Handler(Looper.getMainLooper())
private var hadAnimationClocksIdle = true
private var hadNoSnapshotChanges = true
private var hadNoRecomposerChanges = true
/**
* Returns whether or not Compose is idle, without starting to poll if it is not.
*/
@OptIn(ExperimentalComposeApi::class)
fun isIdle(): Boolean {
return runOnUiThread {
hadNoSnapshotChanges = !Snapshot.current.hasPendingChanges()
hadNoRecomposerChanges = !Recomposer.current().hasPendingChanges()
hadAnimationClocksIdle = areAllClocksIdle()
hadNoSnapshotChanges && hadNoRecomposerChanges && hadAnimationClocksIdle
}
}
/**
* Returns whether or not Compose is idle, and starts polling if it is not. Will always be
* called from the main thread by Espresso, and should _only_ be called from Espresso. Use
* [isIdle] if you need to query the idleness of Compose manually.
*/
override fun isIdleNow(): Boolean {
val isIdle = isIdle()
if (!isIdle) {
scheduleIdleCheck()
}
return isIdle
}
private fun scheduleIdleCheck() {
if (!isIdleCheckScheduled) {
isIdleCheckScheduled = true
handler.postDelayed({
isIdleCheckScheduled = false
if (isIdle()) {
transitionToIdle()
} else {
scheduleIdleCheck()
}
}, /* delayMillis = */ 20)
}
}
internal fun registerTestClock(clock: TestAnimationClock) {
synchronized(clocks) {
clocks.add(clock)
}
}
internal fun unregisterTestClock(clock: TestAnimationClock) {
synchronized(clocks) {
clocks.remove(clock)
}
}
private fun areAllClocksIdle(): Boolean {
return synchronized(clocks) {
clocks.all { it.isIdle }
}
}
override fun getDiagnosticMessageIfBusy(): String? {
val wasIdle = hadNoSnapshotChanges && hadNoRecomposerChanges && hadAnimationClocksIdle
if (wasIdle) {
return null
}
val busyReasons = mutableListOf<String>()
if (!hadAnimationClocksIdle) {
busyReasons.add("animations")
}
val busyRecomposing = !(hadNoRecomposerChanges && hadNoSnapshotChanges)
if (busyRecomposing) {
busyReasons.add("pending recompositions")
}
var message = "$name is busy due to ${busyReasons.joinToString(", ")}.\n"
if (busyRecomposing) {
message += "- Note: Timeout on pending recomposition means that there are most likely" +
" infinite re-compositions happening in the tested code.\n"
message += "- Debug: hadRecomposerChanges = ${!hadNoRecomposerChanges}, "
message += "hadSnapshotChanges = ${!hadNoSnapshotChanges} "
}
return message
}
}
private object FirstDrawIdlingResource : BaseIdlingResource() {
override fun getName(): String = "FirstDrawIdlingResource"
override fun isIdleNow(): Boolean {
return FirstDrawRegistry.haveAllDrawn().also {
if (!it) {
FirstDrawRegistry.setOnDrawnCallback(::transitionToIdle)
}
}
}
override fun unregisterSelfFromEspresso() {
super.unregisterSelfFromEspresso()
FirstDrawRegistry.setOnDrawnCallback(null)
}
}
internal sealed class BaseIdlingResource : IdlingResource {
private val isRegistered = AtomicBoolean(false)
private var resourceCallback: IdlingResource.ResourceCallback? = null
final override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback?) {
resourceCallback = callback
}
protected fun transitionToIdle() {
resourceCallback?.onTransitionToIdle()
}
/**
* Registers this resource into Espresso.
*
* Can be called multiple times.
*/
internal fun registerSelfIntoEspresso() {
if (isRegistered.compareAndSet(false, true)) {
IdlingRegistry.getInstance().register(this)
}
}
/**
* Unregisters this resource from Espresso.
*
* Can be called multiple times.
*/
internal open fun unregisterSelfFromEspresso() {
if (isRegistered.compareAndSet(true, false)) {
IdlingRegistry.getInstance().unregister(this)
}
}
}