[go: nahoru, domu]

blob: d461899a6b4e03d16909a37c8cc4da606e3e1594 [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.
*/
@file:Suppress("PLUGIN_ERROR")
package androidx.compose.runtime
import android.view.View
import android.widget.Button
import android.widget.TextView
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.assertNull
import junit.framework.TestCase.assertTrue
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@MediumTest
@RunWith(AndroidJUnit4::class)
class EffectsTests : BaseComposeTest() {
private val NeverEqualObject = object {
override fun equals(other: Any?): Boolean {
return false
}
}
@get:Rule
override val activityRule = makeTestActivityRule()
@Test
fun testMemoization1() {
var inc = 0
compose {
remember { ++inc }
}.then { _ ->
assertEquals(1, inc)
}.then { _ ->
assertEquals(1, inc)
}
}
@Test
fun testMemoization2() {
var calculations = 0
var compositions = 0
var calculation = 0
var key = 0
val trigger = Trigger()
compose {
trigger.subscribe()
compositions++
calculation = remember(key) { 100 * ++calculations }
}.then { _ ->
assertEquals(1, calculations)
assertEquals(100, calculation)
assertEquals(1, compositions)
trigger.recompose()
}.then { _ ->
assertEquals(1, calculations)
assertEquals(100, calculation)
assertEquals(2, compositions)
key++
trigger.recompose()
}.then { _ ->
assertEquals(2, calculations)
assertEquals(200, calculation)
assertEquals(3, compositions)
trigger.recompose()
}.then { _ ->
assertEquals(2, calculations)
assertEquals(200, calculation)
assertEquals(4, compositions)
key++
trigger.recompose()
}.then { _ ->
assertEquals(3, calculations)
assertEquals(300, calculation)
assertEquals(5, compositions)
}
}
@Test
@Ignore("b/179279455")
fun testState1() {
val tv1Id = 100
var inc = 0
var local = mutableStateOf("invalid")
compose {
local = remember { mutableStateOf("Hello world! ${inc++}") } // NOTYPO
TextView(id = tv1Id, text = local.value)
}.then { activity ->
val helloText = activity.findViewById(tv1Id) as TextView
assertEquals("Hello world! 0", helloText.text)
assertEquals(local.value, helloText.text)
}.then { activity ->
val helloText = activity.findViewById(tv1Id) as TextView
assertEquals("Hello world! 0", helloText.text)
assertEquals(local.value, helloText.text)
local.value = "New string"
}.then { activity ->
val helloText = activity.findViewById(tv1Id) as TextView
assertEquals("New string", helloText.text)
assertEquals(local.value, helloText.text)
}
}
@Test
@Ignore("b/179279455")
fun testState2() {
val tv1Id = 100
val tv2Id = 200
var local1 = mutableStateOf("invalid")
var local2 = mutableStateOf("invalid")
compose {
local1 = remember { mutableStateOf("First") }
local2 = remember { mutableStateOf("Second") }
TextView(id = tv1Id, text = local1.value)
TextView(id = tv2Id, text = local2.value)
}.then { activity ->
val tv1 = activity.findViewById(tv1Id) as TextView
val tv2 = activity.findViewById(tv2Id) as TextView
assertEquals("First", tv1.text)
assertEquals("Second", tv2.text)
assertEquals(local1.value, tv1.text)
assertEquals(local2.value, tv2.text)
}.then { activity ->
val tv1 = activity.findViewById(tv1Id) as TextView
val tv2 = activity.findViewById(tv2Id) as TextView
assertEquals("First", tv1.text)
assertEquals("Second", tv2.text)
assertEquals(local1.value, tv1.text)
assertEquals(local2.value, tv2.text)
local1.value = "New First"
}.then { activity ->
val tv1 = activity.findViewById(tv1Id) as TextView
val tv2 = activity.findViewById(tv2Id) as TextView
assertEquals("New First", tv1.text)
assertEquals("Second", tv2.text)
assertEquals(local1.value, tv1.text)
assertEquals(local2.value, tv2.text)
}
}
@Test
fun testState3() {
// Test property delegation for State/MutableState
val initial = "initial"
val expected = "expected"
val myState = mutableStateOf(initial)
val readonly: State<String> = myState
val reader by readonly
var writer by myState
writer = expected
assertEquals("state object after write", expected, myState.value)
assertEquals("reader after write", expected, reader)
assertEquals("writer after write", expected, writer)
}
@Test
fun testCommit1() {
var mount by mutableStateOf(true)
val logHistory = mutableListOf<String>()
fun log(x: String) = logHistory.add(x)
@Composable
fun Unmountable() {
log("Unmountable:start")
DisposableEffect(NeverEqualObject) {
log("onCommit")
onDispose {
log("onDispose")
}
}
log("Unmountable:end")
}
compose {
log("compose:start")
if (mount) {
Unmountable()
}
log("compose:end")
}.then { _ ->
assertArrayEquals(
listOf(
"compose:start",
"Unmountable:start",
"Unmountable:end",
"compose:end",
"onCommit"
),
logHistory
)
mount = false
}.then { _ ->
assertArrayEquals(
listOf(
"compose:start",
"Unmountable:start",
"Unmountable:end",
"compose:end",
"onCommit",
"compose:start",
"compose:end",
"onDispose"
),
logHistory
)
}
}
@Test
fun testCommit2() {
var mount by mutableStateOf(true)
val logHistory = mutableListOf<String>()
fun log(x: String) = logHistory.add(x)
@Composable
fun Unmountable() {
DisposableEffect(NeverEqualObject) {
log("onCommit:a2")
onDispose {
log("onDispose:a2")
}
}
DisposableEffect(NeverEqualObject) {
log("onCommit:b2")
onDispose {
log("onDispose:b2")
}
}
}
compose {
DisposableEffect(NeverEqualObject) {
log("onCommit:a1")
onDispose {
log("onDispose:a1")
}
}
if (mount) {
Unmountable()
}
DisposableEffect(NeverEqualObject) {
log("onCommit:b1")
onDispose {
log("onDispose:b1")
}
}
}.then { _ ->
assertArrayEquals(
listOf(
"onCommit:a1",
"onCommit:a2",
"onCommit:b2",
"onCommit:b1"
),
logHistory
)
mount = false
log("recompose")
}.then { _ ->
assertArrayEquals(
listOf(
"onCommit:a1",
"onCommit:a2",
"onCommit:b2",
"onCommit:b1",
"recompose",
"onDispose:b2",
"onDispose:a2",
"onDispose:b1",
"onDispose:a1",
"onCommit:a1",
"onCommit:b1"
),
logHistory
)
}
}
@Test
fun testCommit3() {
var x = 0
val trigger = Trigger()
val logHistory = mutableListOf<String>()
fun log(x: String) = logHistory.add(x)
compose {
trigger.subscribe()
DisposableEffect(NeverEqualObject) {
val y = x++
log("onCommit:$y")
onDispose {
log("dispose:$y")
}
}
}.then { _ ->
log("recompose")
trigger.recompose()
}.then { _ ->
assertArrayEquals(
listOf(
"onCommit:0",
"recompose",
"dispose:0",
"onCommit:1"
),
logHistory
)
}
}
@Test
fun testCommit31() {
var a = 0
var b = 0
val trigger = Trigger()
val logHistory = mutableListOf<String>()
fun log(x: String) = logHistory.add(x)
compose {
trigger.subscribe()
DisposableEffect(NeverEqualObject) {
val y = a++
log("onCommit a:$y")
onDispose {
log("dispose a:$y")
}
}
DisposableEffect(NeverEqualObject) {
val y = b++
log("onCommit b:$y")
onDispose {
log("dispose b:$y")
}
}
}.then { _ ->
log("recompose")
trigger.recompose()
}.then { _ ->
assertArrayEquals(
listOf(
"onCommit a:0",
"onCommit b:0",
"recompose",
"dispose b:0",
"dispose a:0",
"onCommit a:1",
"onCommit b:1"
),
logHistory
)
}
}
@Test
fun testCommit4() {
var x = 0
var key = 123
val trigger = Trigger()
val logHistory = mutableListOf<String>()
fun log(x: String) = logHistory.add(x)
compose {
trigger.subscribe()
DisposableEffect(key) {
val y = x++
log("onCommit:$y")
onDispose {
log("dispose:$y")
}
}
}.then { _ ->
log("recompose")
trigger.recompose()
}.then { _ ->
assertArrayEquals(
listOf(
"onCommit:0",
"recompose"
),
logHistory
)
log("recompose (key -> 345)")
key = 345
trigger.recompose()
}.then { _ ->
assertArrayEquals(
listOf(
"onCommit:0",
"recompose",
"recompose (key -> 345)",
"dispose:0",
"onCommit:1"
),
logHistory
)
}
}
@Test
fun testCommit5() {
var a = 0
var b = 0
var c = 0
val trigger = Trigger()
val logHistory = mutableListOf<String>()
fun log(x: String) = logHistory.add(x)
@Composable
fun Sub() {
trigger.subscribe()
DisposableEffect(NeverEqualObject) {
val y = c++
log("onCommit c:$y")
onDispose {
log("dispose c:$y")
}
}
}
compose {
trigger.subscribe()
DisposableEffect(NeverEqualObject) {
val y = a++
log("onCommit a:$y")
onDispose {
log("dispose a:$y")
}
}
DisposableEffect(NeverEqualObject) {
val y = b++
log("onCommit b:$y")
onDispose {
log("dispose b:$y")
}
}
Sub()
}.then { _ ->
log("recompose")
trigger.recompose()
}.then { _ ->
assertArrayEquals(
listOf(
"onCommit a:0",
"onCommit b:0",
"onCommit c:0",
"recompose",
"dispose c:0",
"dispose b:0",
"dispose a:0",
"onCommit a:1",
"onCommit b:1",
"onCommit c:1"
),
logHistory
)
}
}
@Test
fun testCommit6() {
var readValue = 0
@Composable
fun UpdateStateInCommit() {
var value by remember { mutableStateOf(1) }
readValue = value
SideEffect {
value = 2
}
}
compose {
UpdateStateInCommit()
}.then { _ ->
assertEquals(2, readValue)
}
}
@Test
fun testOnDispose1() {
var mount by mutableStateOf(true)
val logHistory = mutableListOf<String>()
fun log(x: String) = logHistory.add(x)
@Composable
fun DisposeLogger(msg: String) {
DisposableEffect(Unit) {
onDispose { log(msg) }
}
}
compose {
DisposeLogger(msg = "onDispose:1")
if (mount) {
DisposeLogger(msg = "onDispose:2")
}
}.then { _ ->
assertArrayEquals(
emptyList<String>(),
logHistory
)
mount = false
log("recompose")
}.then { _ ->
assertArrayEquals(
listOf("recompose", "onDispose:2"),
logHistory
)
}
}
@Test
@Ignore("b/179279455")
fun testCompositionLocal1() {
val tv1Id = 100
val Foo = compositionLocalOf<String>()
var current by mutableStateOf("Hello World")
@Composable
fun Bar() {
val foo = Foo.current
TextView(id = tv1Id, text = foo)
}
compose {
CompositionLocalProvider(Foo provides current) {
Bar()
}
}.then { activity ->
val helloText = activity.findViewById(tv1Id) as TextView
assertEquals(current, helloText.text)
current = "abcd"
}.then { activity ->
val helloText = activity.findViewById(tv1Id) as TextView
assertEquals(current, helloText.text)
}
}
@Test
@Ignore("b/179279455")
fun testCompositionLocal2() {
val MyCompositionLocal = compositionLocalOf<Int> { throw Exception("not set") }
var scope: RecomposeScope? = null
var compositionLocalValue = 1
@Composable fun SimpleComposable2() {
val value = MyCompositionLocal.current
TextView(text = "$value")
}
@Composable fun SimpleComposable() {
scope = currentRecomposeScope
CompositionLocalProvider(MyCompositionLocal provides compositionLocalValue++) {
SimpleComposable2()
Button(id = 123)
}
}
@Composable fun Root() {
SimpleComposable()
}
var firstButton: Button? = null
compose {
Root()
}.then {
firstButton = it.findViewById<Button>(123)
assertTrue("Expected button to be created", firstButton != null)
scope?.invalidate()
}.then {
assertEquals(
"Expected button to not be recreated",
it.findViewById<Button>(123),
firstButton
)
}
}
@Test
@Ignore("b/179279455")
fun testCompositionLocal_RecomposeScope() {
val MyCompositionLocal = compositionLocalOf<Int> { throw Exception("not set") }
var scope: RecomposeScope? = null
var componentComposed = false
var compositionLocalValue = 1
@Composable fun SimpleComposable2() {
componentComposed = true
val value = MyCompositionLocal.current
TextView(text = "$value")
}
@Composable fun SimpleComposable() {
scope = currentRecomposeScope
CompositionLocalProvider(MyCompositionLocal provides compositionLocalValue++) {
SimpleComposable2()
Button(id = 123)
}
}
@Composable fun Root() {
SimpleComposable()
}
var firstButton: Button? = null
compose {
Root()
}.then {
assertTrue("Expected component to be composed", componentComposed)
firstButton = it.findViewById<Button>(123)
assertTrue("Expected button to be created", firstButton != null)
componentComposed = false
scope?.invalidate()
}.then {
assertTrue("Expected component to be composed", componentComposed)
assertEquals(
"Expected button to not be recreated",
firstButton,
it.findViewById<Button>(123)
)
}
}
@Test
@Ignore("b/179279455")
fun testUpdatedComposition() {
val tv1Id = 100
var inc = 0
compose {
val local = remember { mutableStateOf("Hello world! ${inc++}") } // NOTYPO
TextView(id = tv1Id, text = local.value)
}.then { activity ->
val helloText = activity.findViewById(tv1Id) as TextView
assertEquals("Hello world! 0", helloText.text)
}.then { activity ->
val helloText = activity.findViewById(tv1Id) as TextView
assertEquals("Hello world! 0", helloText.text)
}
}
@Test // regression test for 165416179
@Ignore("b/179279455")
fun recomposeNewModifiedState() {
val id = 100
var enabler by mutableStateOf(false)
compose {
// "enabler" forces the test into the recompose code path
// rather than initial composition
if (enabler) {
var state by remember { mutableStateOf(0) }
DisposableEffect(Unit) {
state = 1
onDispose { }
}
TextView(id = id, text = "$state")
Button(
id = id + 1,
text = "click me",
onClickListener = View.OnClickListener {
state++
println("Setting state to $state")
}
)
}
}.then { activity ->
assertNull(
"Text present in initial composition",
activity.findViewById(id)
)
enabler = true
}.then { activity ->
val text = (activity.findViewById(id) as? TextView)?.text
// Text could be either "0" or "1" here depending on timing. It is most likely still
// "0" but could be "1" if the recompose implied by the onCommit lands before this
// code runs.
assertTrue(text == "0" || text == "1")
}.then { activity ->
// Here this should run after the recompose lands.
assertEquals(
"1",
(activity.findViewById(id) as? TextView)?.text
)
// Cause the state to advance.
(activity.findViewById<Button>(id + 1)?.callOnClick())
}.then { activity ->
assertEquals(
"2",
(activity.findViewById(id) as? TextView)?.text
)
}
}
}
fun <T> assertArrayEquals(
expected: Collection<T>,
actual: Collection<T>,
transform: (T) -> String = { "$it" }
) {
assertEquals(
expected.joinToString("\n", transform = transform),
actual.joinToString("\n", transform = transform)
)
}
class Trigger() {
val count = mutableStateOf(0)
fun subscribe() { count.value }
fun recompose() { count.value += 1 }
}