blob: bf75954ff1fa17f4a7c66c887747a5c24e21c130 [file] [log] [blame]
/*
* Copyright 2021 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.runtime
import androidx.compose.runtime.mock.Text
import androidx.compose.runtime.mock.compositionTest
import org.junit.Assert
import org.junit.Ignore
import org.junit.Test
class LiveEditTests {
@Test
fun testRestartableFunctionPreservesParentAndSiblingState() = liveEditTest {
EnsureStatePreservedAndNotRecomposed("a")
RestartGroup {
Text("Hello World")
EnsureStatePreservedAndNotRecomposed("b")
Target("c")
}
}
// TODO: This should pass but doesn't. Need to investigate why.
@Ignore
fun testNonRestartableTargetAtRootScope() = liveEditTest {
Target("b", restartable = false)
}
@Test
fun testTargetSiblings() = liveEditTest {
Target("a")
Target("b")
}
@Test
fun testMultipleFunctionPreservesParentAndSiblingState() = liveEditTest {
EnsureStatePreservedAndNotRecomposed("a")
Target("b")
RestartGroup {
Text("Hello World")
EnsureStatePreservedAndNotRecomposed("c")
Target("d")
Target("e")
}
Target("f")
}
@Test
fun testChildGroupStateIsDestroyed() = liveEditTest {
EnsureStatePreservedAndNotRecomposed("a")
RestartGroup {
Text("Hello World")
EnsureStatePreservedAndNotRecomposed("b")
Target("c") {
Text("Hello World")
EnsureStateLost("d")
}
}
}
@Test
fun testTargetWithinTarget() = liveEditTest {
EnsureStatePreservedAndNotRecomposed("a")
RestartGroup {
Text("Hello World")
EnsureStatePreservedAndNotRecomposed("b")
Target("c") {
Text("Hello World")
EnsureStateLost("d")
RestartGroup {
MarkAsTarget()
}
}
}
}
@Test
fun testNonRestartableFunctionPreservesParentAndSiblingState() = liveEditTest {
EnsureStatePreservedAndNotRecomposed("a")
RestartGroup {
Text("Hello World")
EnsureStatePreservedButRecomposed("b")
Target("c", restartable = false)
}
}
@Test
fun testMultipleNonRestartableFunctionPreservesParentAndSiblingState() = liveEditTest {
RestartGroup {
EnsureStatePreservedButRecomposed("a")
Target("b", restartable = false)
RestartGroup {
Text("Hello World")
EnsureStatePreservedButRecomposed("c")
Target("d", restartable = false)
Target("e", restartable = false)
}
Target("f", restartable = false)
}
}
@Test
fun testLambda() = liveEditTest {
RestartGroup {
MarkAsTarget()
EnsureStateLost("a")
Text("Hello World")
}
}
@Test
fun testInlineComposableLambda() = liveEditTest {
RestartGroup {
InlineTarget("a")
EnsureStatePreservedButRecomposed("b")
Text("Hello World")
}
}
}
@Composable
@NonRestartableComposable
fun LiveEditTestScope.EnsureStatePreservedButRecomposed(ref: String) {
Expect(
ref,
compose = 2,
onRememberd = 1,
onForgotten = 0,
onAbandoned = 0,
)
}
@Composable
@NonRestartableComposable
fun LiveEditTestScope.EnsureStatePreservedAndNotRecomposed(ref: String) {
Expect(
ref,
compose = 1,
onRememberd = 1,
onForgotten = 0,
onAbandoned = 0,
)
}
@Composable
@NonRestartableComposable
fun LiveEditTestScope.EnsureStateLost(ref: String) {
Expect(
ref,
compose = 2,
onRememberd = 2,
onForgotten = 1,
onAbandoned = 0,
)
}
@Composable
@NonRestartableComposable
fun LiveEditTestScope.Expect(
ref: String,
compose: Int,
onRememberd: Int,
onForgotten: Int,
onAbandoned: Int,
) {
log(ref, "compose")
remember {
object : RememberObserver {
override fun onRemembered() {
log(ref, "onRemembered")
}
override fun onForgotten() {
log(ref, "onForgotten")
}
override fun onAbandoned() {
log(ref, "onAbandoned")
}
}
}
expectLogCount(ref, "compose", compose)
expectLogCount(ref, "onRemembered", onRememberd)
expectLogCount(ref, "onForgotten", onForgotten)
expectLogCount(ref, "onAbandoned", onAbandoned)
}
@Composable fun LiveEditTestScope.Target(
ref: String,
restartable: Boolean = true,
content: @Composable () -> Unit = {}
) {
if (restartable) currentRecomposeScope
MarkAsTarget()
Expect(
ref,
compose = 2,
onRememberd = 2,
onForgotten = 1,
onAbandoned = 0,
)
content()
}
@Composable fun LiveEditTestScope.InlineTarget(
ref: String,
content: @Composable () -> Unit = {}
) {
MarkAsTarget()
Expect(
ref,
compose = 2,
onRememberd = 2,
onForgotten = 1,
onAbandoned = 0,
)
content()
}
@Composable
@ExplicitGroupsComposable
fun LiveEditTestScope.MarkAsTarget() {
addTargetKey((currentComposer as ComposerImpl).parentKey())
}
fun liveEditTest(fn: @Composable LiveEditTestScope.() -> Unit) = compositionTest {
with(LiveEditTestScope()) {
compose { fn(this) }
invalidateTargets()
advance()
runChecks()
}
}
@Stable
class LiveEditTestScope {
private val targetKeys = mutableSetOf<Int>()
private val checks = mutableListOf<() -> Unit>()
private val logs = mutableListOf<Pair<String, String>>()
fun invalidateTargets() {
for (key in targetKeys) {
invalidateGroupsWithKey(key)
}
}
fun runChecks() {
for (check in checks) {
check()
}
}
fun addTargetKey(key: Int) {
targetKeys.add(key)
}
fun log(ref: String, msg: String) {
logs.add(ref to msg)
}
fun addLogCheck(ref: String, validate: (List<String>) -> Unit) {
checks.add {
validate(logs.filter { it.first == ref }.map { it.second }.toList())
}
}
fun expectLogCount(ref: String, msg: String, expected: Int) {
addLogCheck(ref) { logs ->
val actual = logs.filter { m -> m == msg }.count()
Assert.assertEquals(
"Ref $ref had an unexpected # of '$msg' logs",
expected,
actual
)
}
}
}