TestOwner.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.ui.node.RootForTest
import androidx.compose.ui.semantics.SemanticsNode
import androidx.compose.ui.semantics.getAllSemanticsNodes
import androidx.compose.ui.text.ExperimentalTextApi
import androidx.compose.ui.text.input.TextInputForTests
/**
* Provides necessary services to facilitate testing.
*
* This is typically implemented by entities like test rule.
*/
@InternalTestApi
interface TestOwner {
/**
* Clock that drives frames and recompositions in compose tests.
*/
val mainClock: MainTestClock
/**
* Runs [action] on the main thread to perform text input via the [TextInputForTests] for the
* active text input service for the given semantics node.
*/
@OptIn(ExperimentalTextApi::class)
fun performTextInput(node: SemanticsNode, action: TextInputForTests.() -> Unit)
/**
* Runs the given [action] on the ui thread.
*
* This is a blocking call.
*/
// TODO: Does ui-test really need it? Can it use coroutine context on Owner?
fun <T> runOnUiThread(action: () -> T): T
/**
* Collects all [RootForTest]s from all compose hierarchies.
*
* This is a blocking call. Returns only after compose is idle.
*
* Can crash in case it hits time out. This is not supposed to be handled as it
* surfaces only in incorrect tests.
*
* @param atLeastOneRootExpected Whether the caller expects that at least one compose root is
* present in the tested app. This affects synchronization efforts / timeouts of this API.
*/
fun getRoots(atLeastOneRootExpected: Boolean): Set<RootForTest>
}
@InternalTestApi
fun createTestContext(owner: TestOwner): TestContext {
return TestContext(owner)
}
@OptIn(InternalTestApi::class)
class TestContext internal constructor(internal val testOwner: TestOwner) {
/**
* Stores the [InputDispatcherState] of each [RootForTest]. The state will be restored in an
* [InputDispatcher] when it is created for an owner that has a state stored. To avoid leaking
* the [RootForTest], the [identityHashCode] of the root is used as the key instead of the
* actual object.
*/
internal val states = mutableMapOf<Int, InputDispatcherState>()
/**
* Collects all [SemanticsNode]s from all compose hierarchies.
*
* This is a blocking call. Returns only after compose is idle.
*
* Can crash in case it hits time out. This is not supposed to be handled as it
* surfaces only in incorrect tests.
*/
internal fun getAllSemanticsNodes(
atLeastOneRootRequired: Boolean,
useUnmergedTree: Boolean
): Iterable<SemanticsNode> {
val roots = testOwner.getRoots(atLeastOneRootRequired).also {
check(!atLeastOneRootRequired || it.isNotEmpty()) {
"No compose hierarchies found in the app. Possible reasons include: " +
"(1) the Activity that calls setContent did not launch; " +
"(2) setContent was not called; " +
"(3) setContent was called before the ComposeTestRule ran. " +
"If setContent is called by the Activity, make sure the Activity is " +
"launched after the ComposeTestRule runs"
}
}
return roots.flatMap {
it.semanticsOwner.getAllSemanticsNodes(mergingEnabled = !useUnmergedTree)
}
}
}