[go: nahoru, domu]

blob: f66b09848a1dd97b50075ab78edebc2e12d2dbd9 [file] [log] [blame]
/*
* 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.junit4
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ExperimentalComposeApi
import androidx.compose.runtime.Recomposer
import androidx.compose.runtime.snapshots.Snapshot
import androidx.compose.ui.node.Owner
import androidx.compose.ui.platform.DesktopOwner
import androidx.compose.ui.platform.DesktopOwners
import androidx.compose.ui.platform.setContent
import androidx.compose.ui.semantics.SemanticsNode
import androidx.compose.ui.test.ExperimentalTesting
import androidx.compose.ui.test.IdlingResource
import androidx.compose.ui.test.InternalTestingApi
import androidx.compose.ui.test.SemanticsMatcher
import androidx.compose.ui.test.SemanticsNodeInteraction
import androidx.compose.ui.test.SemanticsNodeInteractionCollection
import androidx.compose.ui.test.TestOwner
import androidx.compose.ui.test.createTestContext
import androidx.compose.ui.text.input.EditOperation
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntSize
import kotlinx.coroutines.delay
import org.jetbrains.skija.Surface
import org.junit.runner.Description
import org.junit.runners.model.Statement
import java.util.LinkedList
import java.util.concurrent.ExecutionException
import java.util.concurrent.FutureTask
import javax.swing.SwingUtilities.invokeAndWait
import javax.swing.SwingUtilities.isEventDispatchThread
actual fun createComposeRule(): ComposeTestRule = DesktopComposeTestRule()
@OptIn(InternalTestingApi::class)
class DesktopComposeTestRule : ComposeTestRule {
companion object {
var current: DesktopComposeTestRule? = null
}
var owners: DesktopOwners? = null
private var owner: DesktopOwner? = null
@ExperimentalTesting
override val clockTestRule: AnimationClockTestRule = DesktopAnimationClockTestRule()
override val density: Density
get() = TODO()
override val displaySize: IntSize get() = IntSize(1024, 768)
val executionQueue = LinkedList<() -> Unit>()
private val testOwner = DesktopTestOwner(this)
private val testContext = createTestContext(testOwner)
override fun apply(base: Statement, description: Description?): Statement {
current = this
return object : Statement() {
override fun evaluate() {
base.evaluate()
runExecutionQueue()
runOnUiThread {
owner?.dispose()
owner = null
}
}
}
}
private fun runExecutionQueue() {
while (executionQueue.isNotEmpty()) {
executionQueue.removeFirst()()
}
}
@OptIn(ExperimentalComposeApi::class)
private fun isIdle() =
!Snapshot.current.hasPendingChanges() &&
!Recomposer.current().hasInvalidations()
override fun waitForIdle() {
while (!isIdle()) {
runExecutionQueue()
Thread.sleep(10)
}
}
@ExperimentalTesting
override suspend fun awaitIdle() {
while (!isIdle()) {
runExecutionQueue()
delay(10)
}
}
override fun <T> runOnUiThread(action: () -> T): T {
val task: FutureTask<T> = FutureTask(action)
invokeAndWait(task)
try {
return task.get()
} catch (e: ExecutionException) { // Expose the original exception
throw e.cause!!
}
}
override fun <T> runOnIdle(action: () -> T): T {
// We are waiting for idle before and AFTER `action` to guarantee that changes introduced
// in `action` are propagated to components. In Android's version, it's executed in the
// Main thread which has similar effects. This code could be reconsidered after
// stabilization of the new rendering/dispatching model
waitForIdle()
return action().also { waitForIdle() }
}
override fun registerIdlingResource(idlingResource: IdlingResource) {
// TODO: implement
}
override fun unregisterIdlingResource(idlingResource: IdlingResource) {
// TODO: implement
}
override fun setContent(composable: @Composable () -> Unit) {
check(owner == null) {
"Cannot call setContent twice per test!"
}
if (isEventDispatchThread()) {
performSetContent(composable)
} else {
runOnUiThread {
performSetContent(composable)
}
// Only wait for idleness if not on the UI thread. If we are on the UI thread, the
// caller clearly wants to keep tight control over execution order, so don't go
// executing future tasks on the main thread.
waitForIdle()
}
}
private fun performSetContent(composable: @Composable() () -> Unit) {
val surface = Surface.makeRasterN32Premul(displaySize.width, displaySize.height)
val canvas = surface.canvas
val owners = DesktopOwners(invalidate = {}).also {
owners = it
}
val owner = DesktopOwner(owners)
owner.setContent(content = composable)
owner.setSize(displaySize.width, displaySize.height)
owner.measureAndLayout()
owner.draw(canvas)
this.owner = owner
}
override fun onNode(
matcher: SemanticsMatcher,
useUnmergedTree: Boolean
): SemanticsNodeInteraction {
return SemanticsNodeInteraction(testContext, useUnmergedTree, matcher)
}
override fun onAllNodes(
matcher: SemanticsMatcher,
useUnmergedTree: Boolean
): SemanticsNodeInteractionCollection {
return SemanticsNodeInteractionCollection(testContext, useUnmergedTree, matcher)
}
private class DesktopTestOwner(val rule: DesktopComposeTestRule) : TestOwner {
override fun sendTextInputCommand(node: SemanticsNode, command: List<EditOperation>) {
TODO()
}
override fun sendImeAction(node: SemanticsNode, actionSpecified: ImeAction) {
TODO()
}
override fun <T> runOnUiThread(action: () -> T): T {
return rule.runOnUiThread(action)
}
override fun getOwners(): Set<Owner> {
return rule.owners!!.list
}
}
}