[go: nahoru, domu]

blob: cef557cf52ca7940ca916c7c3e5f776b20c9a2bd [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.ui.core
import androidx.compose.Applier
import androidx.compose.Composable
import androidx.compose.Composer
import androidx.compose.ExperimentalComposeApi
import androidx.compose.InternalComposeApi
import androidx.compose.Recomposer
import androidx.compose.SlotTable
import androidx.compose.currentComposer
import androidx.compose.runtime.dispatch.MonotonicFrameClock
import androidx.compose.invalidate
import androidx.compose.remember
import androidx.compose.withRunningRecomposer
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Test
private class TestTagModifier<T>(val name: String, val value: T) : Modifier.Element
fun <T> Modifier.testTag(name: String, value: T) = this + TestTagModifier(name, value)
fun <T> Modifier.getTestTag(name: String, default: T): T = foldIn(default) { acc, element ->
@Suppress("UNCHECKED_CAST")
if (element is TestTagModifier<*> && element.name == name) element.value as T else acc
}
@OptIn(InternalComposeApi::class)
class ComposedModifierTest {
/**
* Confirm that a [composed] modifier correctly constructs separate instances when materialized
*/
@Test
fun materializeComposedModifier() = runBlocking(TestFrameClock()) {
// Note: assumes single-threaded composition
var counter = 0
val sourceMod = Modifier.testTag("static", 0)
.composed { testTag("dynamic", ++counter) }
withRunningRecomposer { recomposer ->
lateinit var firstMaterialized: Modifier
lateinit var secondMaterialized: Modifier
compose(recomposer) {
firstMaterialized = currentComposer.materialize(sourceMod)
secondMaterialized = currentComposer.materialize(sourceMod)
}
assertNotEquals("I recomposed some modifiers", 0, counter)
assertEquals(
"first static value equal to source",
sourceMod.getTestTag("static", Int.MIN_VALUE),
firstMaterialized.getTestTag("static", Int.MAX_VALUE)
)
assertEquals(
"second static value equal to source",
sourceMod.getTestTag("static", Int.MIN_VALUE),
secondMaterialized.getTestTag("static", Int.MAX_VALUE)
)
assertEquals(
"dynamic value not present in source",
Int.MIN_VALUE,
sourceMod.getTestTag("dynamic", Int.MIN_VALUE)
)
assertNotEquals(
"dynamic value present in first materialized",
Int.MIN_VALUE,
firstMaterialized.getTestTag("dynamic", Int.MIN_VALUE)
)
assertNotEquals(
"dynamic value present in second materialized",
Int.MIN_VALUE,
firstMaterialized.getTestTag("dynamic", Int.MIN_VALUE)
)
assertNotEquals(
"first and second dynamic values must be unequal",
firstMaterialized.getTestTag("dynamic", Int.MIN_VALUE),
secondMaterialized.getTestTag("dynamic", Int.MIN_VALUE)
)
}
}
/**
* Confirm that recomposition occurs on invalidation
*/
@Test
fun recomposeComposedModifier() = runBlocking {
// Manually invalidate the composition of the modifier instead of using mutableStateOf
// Frame-based recomposition requires the FrameManager be up and running.
var value = 0
lateinit var invalidator: () -> Unit
val sourceMod = Modifier.composed {
invalidator = invalidate
testTag("changing", value)
}
val frameClock = TestFrameClock()
withContext(frameClock) {
withRunningRecomposer { recomposer ->
lateinit var materialized: Modifier
compose(recomposer) {
materialized = currentComposer.materialize(sourceMod)
}
assertEquals(
"initial composition value",
0,
materialized.getTestTag("changing", Int.MIN_VALUE)
)
value = 5
invalidator()
frameClock.frame(0L)
assertEquals(
"recomposed composition value",
5,
materialized.getTestTag("changing", Int.MIN_VALUE)
)
}
}
}
@Test
fun rememberComposedModifier() = runBlocking {
lateinit var invalidator: () -> Unit
val sourceMod = Modifier.composed {
invalidator = invalidate
val state = remember { Any() }
testTag("remembered", state)
}
val frameClock = TestFrameClock()
withContext(frameClock) {
withRunningRecomposer { recomposer ->
val results = mutableListOf<Any?>()
val notFound = Any()
compose(recomposer) {
results.add(
currentComposer.materialize(sourceMod).getTestTag("remembered", notFound)
)
}
assertTrue("one item added for initial composition", results.size == 1)
assertNotNull("remembered object not null", results[0])
invalidator()
frameClock.frame(0)
assertEquals("two items added after recomposition", 2, results.size)
assertTrue("no null items", results.none { it === notFound })
assertEquals("remembered references are equal", results[0], results[1])
}
}
}
@Test
fun nestedComposedModifiers() = runBlocking {
val mod = Modifier.composed {
composed {
testTag("nested", 10)
}
}
val frameClock = TestFrameClock()
withContext(frameClock) {
withRunningRecomposer { recomposer ->
lateinit var materialized: Modifier
compose(recomposer) {
materialized = currentComposer.materialize(mod)
}
assertEquals(
"fully unwrapped composed modifier value",
10,
materialized.getTestTag("nested", 0)
)
}
}
}
}
@OptIn(InternalComposeApi::class, ExperimentalComposeApi::class)
private fun compose(
recomposer: Recomposer,
block: @Composable () -> Unit
): Composer<Unit> {
return Composer(
SlotTable(),
EmptyApplier(),
recomposer
).apply {
composeRoot {
@Suppress("UNCHECKED_CAST")
val fn = block as (Composer<*>, Int, Int) -> Unit
fn(this, 0, 0)
}
applyChanges()
slotTable.verifyWellFormed()
}
}
private class TestFrameClock : MonotonicFrameClock {
private val frameCh = Channel<Long>()
suspend fun frame(frameTimeNanos: Long) {
frameCh.send(frameTimeNanos)
}
override suspend fun <R> withFrameNanos(onFrame: (Long) -> R): R = onFrame(frameCh.receive())
}
@OptIn(ExperimentalComposeApi::class)
class EmptyApplier : Applier<Unit> {
override val current: Unit = Unit
override fun down(node: Unit) {}
override fun up() {}
override fun insert(index: Int, instance: Unit) {
error("Unexpected")
}
override fun remove(index: Int, count: Int) {
error("Unexpected")
}
override fun move(from: Int, to: Int, count: Int) {
error("Unexpected")
}
override fun clear() {}
}