| /* |
| * Copyright 2019 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:OptIn(ExperimentalComposeApi::class, InternalComposeApi::class) |
| package androidx.compose.runtime |
| |
| import androidx.compose.runtime.dispatch.MonotonicFrameClock |
| import androidx.compose.runtime.mock.Contact |
| import androidx.compose.runtime.mock.ContactModel |
| import androidx.compose.runtime.mock.MockComposeScope |
| import androidx.compose.runtime.mock.MockViewListValidator |
| import androidx.compose.runtime.mock.MockViewValidator |
| import androidx.compose.runtime.mock.Point |
| import androidx.compose.runtime.mock.Report |
| import androidx.compose.runtime.mock.View |
| import androidx.compose.runtime.mock.ViewApplier |
| import androidx.compose.runtime.mock.contact |
| import androidx.compose.runtime.mock.edit |
| import androidx.compose.runtime.mock.linear |
| import androidx.compose.runtime.mock.memoize |
| import androidx.compose.runtime.mock.points |
| import androidx.compose.runtime.mock.repeat |
| import androidx.compose.runtime.mock.reportsReport |
| import androidx.compose.runtime.mock.reportsTo |
| import androidx.compose.runtime.mock.selectContact |
| import androidx.compose.runtime.mock.skip |
| import androidx.compose.runtime.mock.text |
| import kotlinx.coroutines.CoroutineScope |
| import kotlinx.coroutines.Job |
| import kotlinx.coroutines.launch |
| import kotlinx.coroutines.suspendCancellableCoroutine |
| import org.junit.After |
| import kotlin.test.Test |
| import kotlin.test.assertEquals |
| import kotlin.test.assertFalse |
| import kotlin.test.assertNotEquals |
| import kotlin.test.assertTrue |
| |
| @Composable fun Container(body: @Composable () -> Unit) = body() |
| |
| @Stable |
| class CompositionTests { |
| |
| @After |
| fun teardown() { |
| clearRoots() |
| } |
| |
| @Test |
| fun testComposeAModel() { |
| val model = testModel() |
| val result = compose { |
| selectContact(model) |
| } |
| |
| result.validate { |
| linear { |
| linear { |
| text("Filter:") |
| edit("") |
| } |
| linear { |
| text(value = "Contacts:") |
| linear { |
| contact(bob) |
| contact(jon) |
| contact(steve) |
| } |
| } |
| } |
| } |
| } |
| |
| @Test |
| fun testRecomposeWithoutChanges() { |
| val model = testModel() |
| val result = compose { |
| selectContact(model) |
| } |
| |
| result.expectNoChanges() |
| |
| result.validate { |
| selectContact(model) |
| } |
| } |
| |
| @Test |
| fun testInsertAContact() { |
| val model = |
| testModel(mutableListOf(bob, jon)) |
| var changed = {} |
| val result = compose { |
| changed = invalidate |
| selectContact(model) |
| } |
| |
| result.validate { |
| linear { |
| skip() |
| linear { |
| skip() |
| linear { |
| contact(bob) |
| contact(jon) |
| } |
| } |
| } |
| } |
| |
| model.add(steve, after = bob) |
| changed() |
| result.expectChanges() |
| |
| result.validate { |
| linear { |
| skip() |
| linear { |
| skip() |
| linear { |
| contact(bob) |
| contact(steve) |
| contact(jon) |
| } |
| } |
| } |
| } |
| } |
| |
| @Test |
| fun testMoveAContact() { |
| val model = testModel( |
| mutableListOf( |
| bob, |
| steve, |
| jon |
| ) |
| ) |
| var changed = {} |
| val result = compose { |
| changed = invalidate |
| selectContact(model) |
| } |
| |
| model.move(steve, after = jon) |
| changed() |
| result.expectChanges() |
| |
| result.validate { |
| linear { |
| skip() |
| linear { |
| skip() |
| linear { |
| contact(bob) |
| contact(jon) |
| contact(steve) |
| } |
| } |
| } |
| } |
| } |
| |
| @Test |
| fun testChangeTheFilter() { |
| val model = testModel( |
| mutableListOf( |
| bob, |
| steve, |
| jon |
| ) |
| ) |
| var changed: (() -> Unit)? = null |
| val result = compose { |
| changed = invalidate |
| selectContact(model) |
| } |
| |
| model.filter = "Jon" |
| changed!!() |
| result.expectChanges() |
| |
| result.validate { |
| linear { |
| skip() |
| linear { |
| skip() |
| linear { |
| contact(jon) |
| } |
| } |
| } |
| } |
| } |
| |
| @Test |
| fun testComposeCompositionWithMultipleRoots() { |
| val reports = listOf( |
| jim_reports_to_sally, |
| rob_reports_to_alice, |
| clark_reports_to_lois |
| ) |
| |
| val result = compose { |
| reportsReport(reports) |
| } |
| |
| result.validate { |
| reportsReport(reports) |
| } |
| } |
| |
| @Test |
| fun testMoveCompositionWithMultipleRoots() { |
| var reports = listOf( |
| jim_reports_to_sally, |
| rob_reports_to_alice, |
| clark_reports_to_lois |
| ) |
| var changed: (() -> Unit)? = null |
| val result = compose { |
| changed = invalidate |
| reportsReport(reports) |
| } |
| |
| reports = listOf( |
| jim_reports_to_sally, |
| clark_reports_to_lois, |
| rob_reports_to_alice |
| ) |
| changed!!() |
| result.expectChanges() |
| |
| result.validate { |
| reportsReport(reports) |
| } |
| } |
| |
| @Test |
| fun testReplace() { |
| var includeA = true |
| var changed: (() -> Unit)? = null |
| @Composable fun MockComposeScope.composition() { |
| changed = invalidate |
| text("Before") |
| if (includeA) { |
| linear { |
| text("A") |
| } |
| } else { |
| edit("B") |
| } |
| text("After") |
| } |
| fun MockViewValidator.composition() { |
| text("Before") |
| if (includeA) { |
| linear { |
| text("A") |
| } |
| } else { |
| edit("B") |
| } |
| text("After") |
| } |
| val result = compose { |
| composition() |
| } |
| result.validate { |
| composition() |
| } |
| includeA = false |
| changed!!() |
| result.expectChanges() |
| result.validate { |
| composition() |
| } |
| includeA = true |
| changed!!() |
| result.expectChanges() |
| result.validate { |
| composition() |
| } |
| } |
| |
| @Test |
| fun testInsertWithMultipleRoots() { |
| var chars = listOf('a', 'b', 'c') |
| var changed: (() -> Unit)? = null |
| |
| @Composable fun MockComposeScope.textOf(c: Char) { |
| text(c.toString()) |
| } |
| |
| fun MockViewValidator.textOf(c: Char) { |
| text(c.toString()) |
| } |
| |
| @Composable fun MockComposeScope.chars(chars: Iterable<Char>) { |
| repeat(of = chars) { c -> textOf(c) } |
| } |
| |
| fun MockViewValidator.validatechars(chars: Iterable<Char>) { |
| repeat(of = chars) { c -> textOf(c) } |
| } |
| |
| val result = compose { |
| changed = invalidate |
| chars(chars) |
| chars(chars) |
| chars(chars) |
| } |
| |
| result.validate { |
| validatechars(chars) |
| validatechars(chars) |
| validatechars(chars) |
| } |
| |
| chars = listOf('a', 'b', 'x', 'c') |
| changed!!() |
| result.expectChanges() |
| |
| result.validate { |
| validatechars(chars) |
| validatechars(chars) |
| validatechars(chars) |
| } |
| } |
| |
| @Test |
| fun testSimpleMemoize() { |
| val points = listOf(Point(1, 2), Point(2, 3)) |
| val result = compose { |
| points(points) |
| } |
| |
| result.validate { points(points) } |
| |
| val changes = result.recompose() |
| assertFalse(changes) |
| } |
| |
| @Test |
| fun testMovingMemoization() { |
| var points = listOf( |
| Point(1, 2), |
| Point(2, 3), |
| Point(4, 5), |
| Point(6, 7) |
| ) |
| var changed: (() -> Unit)? = null |
| val result = compose { |
| changed = invalidate |
| points(points) |
| } |
| |
| result.validate { points(points) } |
| |
| points = listOf( |
| Point(1, 2), |
| Point(4, 5), |
| Point(2, 3), |
| Point(6, 7) |
| ) |
| changed!!() |
| result.expectChanges() |
| |
| result.validate { points(points) } |
| } |
| |
| @Test |
| fun testComponent() { |
| @Composable fun MockComposeScope.Reporter(report: Report? = null) { |
| if (report != null) { |
| text(report.from) |
| text("reports to") |
| text(report.to) |
| } else { |
| text("no report to report") |
| } |
| } |
| |
| @Composable fun MockComposeScope.reportsReport(reports: Iterable<Report>) { |
| linear { |
| repeat(of = reports) { report -> |
| Reporter(report) |
| } |
| } |
| } |
| |
| val reports = listOf( |
| jim_reports_to_sally, |
| rob_reports_to_alice, |
| clark_reports_to_lois |
| ) |
| val result = compose { |
| reportsReport(reports) |
| } |
| |
| result.validate { |
| linear { |
| reportsTo(jim_reports_to_sally) |
| reportsTo(rob_reports_to_alice) |
| reportsTo(clark_reports_to_lois) |
| } |
| } |
| |
| result.expectNoChanges() |
| } |
| |
| @Test |
| fun testComposeTwoAttributeComponent() { |
| @Composable fun MockComposeScope.Two2(first: Int = 1, second: Int = 2) { |
| linear { |
| text("$first $second") |
| } |
| } |
| |
| fun MockViewValidator.two(first: Int, second: Int) { |
| linear { |
| text("$first $second") |
| } |
| } |
| |
| val result = compose { |
| Two2(41, 42) |
| } |
| |
| result.validate { |
| two(41, 42) |
| } |
| } |
| |
| @Test |
| fun testComposeThreeAttributeComponent() { |
| @Composable fun MockComposeScope.Three3(first: Int = 1, second: Int = 2, third: Int = 3) { |
| linear { |
| text("$first $second $third") |
| } |
| } |
| |
| fun MockViewValidator.three(first: Int, second: Int, third: Int) { |
| linear { |
| text("$first $second $third") |
| } |
| } |
| |
| val result = compose { |
| Three3(41, 42, 43) |
| } |
| |
| result.validate { |
| three(41, 42, 43) |
| } |
| } |
| |
| @Test |
| fun testComposeFourOrMoreAttributeComponent() { |
| @Composable fun MockComposeScope.Four4( |
| first: Int = 1, |
| second: Int = 2, |
| third: Int = 3, |
| fourth: Int = 4 |
| ) { |
| linear { |
| text("$first $second $third $fourth") |
| } |
| } |
| |
| fun MockViewValidator.four(first: Int, second: Int, third: Int, fourth: Int) { |
| linear { |
| text("$first $second $third $fourth") |
| } |
| } |
| |
| val result = compose { |
| Four4(41, 42, 43, 44) |
| } |
| |
| result.validate { |
| four(41, 42, 43, 44) |
| } |
| } |
| |
| @Test |
| fun testSkippingACall() { |
| |
| @Composable fun MockComposeScope.show(value: Int) { |
| linear { |
| text("$value") |
| } |
| linear { |
| text("value") |
| } |
| } |
| |
| fun MockViewValidator.show(value: Int) { |
| linear { |
| text("$value") |
| } |
| linear { |
| text("value") |
| } |
| } |
| |
| @Composable fun MockComposeScope.test(showThree: Boolean) { |
| show(1) |
| show(2) |
| if (showThree) { |
| show(3) |
| } |
| } |
| |
| var showThree = false |
| |
| var recomposeTest: () -> Unit = { } |
| |
| @Composable fun MockComposeScope.Test() { |
| recomposeTest = invalidate |
| test(showThree) |
| } |
| |
| fun MockViewValidator.test(showThree: Boolean) { |
| show(1) |
| show(2) |
| if (showThree) { |
| show(3) |
| } |
| } |
| |
| val composition: @Composable MockComposeScope.() -> Unit = { |
| Test() |
| } |
| val validation: MockViewValidator.() -> Unit = { |
| test(showThree) |
| } |
| |
| val result = compose(block = composition) |
| result.validate(validation) |
| |
| showThree = true |
| recomposeTest() |
| result.expectChanges() |
| result.validate(validation) |
| } |
| |
| @Test |
| fun testComponentWithVarCtorParameter() { |
| @Composable fun MockComposeScope.One(first: Int) { |
| text("$first") |
| } |
| |
| fun MockViewValidator.one(first: Int) { |
| text("$first") |
| } |
| |
| @Composable fun MockComposeScope.callOne(value: Int) { |
| One(first = value) |
| } |
| |
| var value = 42 |
| var changed: (() -> Unit)? = null |
| val result = compose { |
| changed = invalidate |
| callOne(value) |
| } |
| |
| result.validate { |
| one(42) |
| } |
| |
| value = 43 |
| changed!!() |
| result.expectChanges() |
| |
| result.validate { |
| one(43) |
| } |
| } |
| |
| @Test |
| fun testComponentWithValCtorParameter() { |
| @Composable fun MockComposeScope.One(first: Int) { |
| text("$first") |
| } |
| |
| fun MockViewValidator.one(first: Int) { |
| text("$first") |
| } |
| |
| @Composable fun MockComposeScope.callOne(value: Int) { |
| One(first = value) |
| } |
| |
| var value = 42 |
| var changed: (() -> Unit)? = null |
| val result = compose { |
| changed = invalidate |
| callOne(value) |
| } |
| |
| result.validate { |
| one(42) |
| } |
| |
| value = 43 |
| changed!!() |
| result.expectChanges() |
| |
| result.validate { |
| one(43) |
| } |
| |
| changed!!() |
| result.expectNoChanges() |
| } |
| |
| @Test |
| fun testComposePartOfTree() { |
| var recomposeLois: (() -> Unit)? = null |
| |
| @Composable fun MockComposeScope.Reporter(report: Report? = null) { |
| if (report != null) { |
| if (report.from == "Lois" || report.to == "Lois") recomposeLois = invalidate |
| text(report.from) |
| text("reports to") |
| text(report.to) |
| } else { |
| text("no report to report") |
| } |
| } |
| |
| @Composable fun MockComposeScope.reportsReport(reports: Iterable<Report>) { |
| linear { |
| repeat(of = reports) { report -> |
| Reporter(report) |
| } |
| } |
| } |
| |
| val r = Report("Lois", "Perry") |
| val reports = listOf( |
| jim_reports_to_sally, |
| rob_reports_to_alice, |
| clark_reports_to_lois, r) |
| val result = compose { |
| reportsReport(reports) |
| } |
| |
| result.validate { |
| linear { |
| reportsTo(jim_reports_to_sally) |
| reportsTo(rob_reports_to_alice) |
| reportsTo(clark_reports_to_lois) |
| reportsTo(r) |
| } |
| } |
| |
| result.expectNoChanges() |
| |
| // Demote Perry |
| r.from = "Perry" |
| r.to = "Lois" |
| |
| // Compose only the Lois report |
| recomposeLois?.let { it() } |
| |
| result.expectChanges() |
| |
| result.validate { |
| linear { |
| reportsTo(jim_reports_to_sally) |
| reportsTo(rob_reports_to_alice) |
| reportsTo(clark_reports_to_lois) |
| reportsTo(r) |
| } |
| } |
| } |
| |
| @Test |
| fun testRecomposeWithReplace() { |
| var recomposeLois: (() -> Unit)? = null |
| var key = 0 |
| |
| @Composable fun MockComposeScope.Reporter(report: Report? = null) { |
| if (report != null) { |
| if (report.from == "Lois" || report.to == "Lois") recomposeLois = invalidate |
| key(key) { |
| text(report.from) |
| text("reports to") |
| text(report.to) |
| } |
| } else { |
| text("no report to report") |
| } |
| } |
| |
| @Composable fun MockComposeScope.reportsReport(reports: Iterable<Report>) { |
| linear { |
| repeat(of = reports) { report -> |
| Reporter(report) |
| } |
| } |
| } |
| |
| val r = Report("Lois", "Perry") |
| val reports = listOf( |
| jim_reports_to_sally, |
| rob_reports_to_alice, |
| clark_reports_to_lois, r) |
| val result = compose { |
| reportsReport(reports) |
| } |
| |
| result.validate { |
| linear { |
| reportsTo(jim_reports_to_sally) |
| reportsTo(rob_reports_to_alice) |
| reportsTo(clark_reports_to_lois) |
| reportsTo(r) |
| } |
| } |
| |
| result.expectNoChanges() |
| |
| // Demote Perry |
| r.from = "Perry" |
| r.to = "Lois" |
| |
| // Cause a new group to be generated in the component |
| key = 2 |
| |
| // Compose only the Lois report |
| recomposeLois?.let { it() } |
| |
| result.expectChanges() |
| |
| result.validate { |
| linear { |
| reportsTo(jim_reports_to_sally) |
| reportsTo(rob_reports_to_alice) |
| reportsTo(clark_reports_to_lois) |
| reportsTo(r) |
| } |
| } |
| } |
| |
| @Test |
| fun testInvalidationAfterRemoval() { |
| var recomposeLois = {} |
| val key = 0 |
| |
| @Composable fun MockComposeScope.Reporter(report: Report? = null) { |
| if (report != null) { |
| val callback = invalidate |
| if (report.from == "Lois" || report.to == "Lois") recomposeLois = callback |
| key(key) { |
| text(report.from) |
| text("reports to") |
| text(report.to) |
| } |
| } else { |
| text("no report to report") |
| } |
| } |
| |
| @Composable fun MockComposeScope.reportsReport( |
| reports: Iterable<Report>, |
| include: (report: Report) -> Boolean |
| ) { |
| linear { |
| repeat(of = reports) { report -> |
| if (include(report)) { |
| Reporter(report) |
| } |
| } |
| } |
| } |
| |
| val r = Report("Lois", "Perry") |
| val reports = listOf( |
| jim_reports_to_sally, |
| rob_reports_to_alice, |
| clark_reports_to_lois, |
| r |
| ) |
| val all: (report: Report) -> Boolean = { true } |
| val notLois: (report: Report) -> Boolean = { it.from != "Lois" && it.to != "Lois" } |
| |
| var filter = all |
| var changed = {} |
| val result = compose { |
| changed = invalidate |
| reportsReport(reports, filter) |
| } |
| |
| result.validate { |
| linear { |
| reportsTo(jim_reports_to_sally) |
| reportsTo(rob_reports_to_alice) |
| reportsTo(clark_reports_to_lois) |
| reportsTo(r) |
| } |
| } |
| |
| filter = notLois |
| changed() |
| result.expectChanges() |
| |
| result.validate { |
| linear { |
| reportsTo(jim_reports_to_sally) |
| reportsTo(rob_reports_to_alice) |
| } |
| } |
| |
| // Invalidate Lois which is now removed. |
| recomposeLois() |
| result.expectNoChanges() |
| |
| result.validate { |
| linear { |
| reportsTo(jim_reports_to_sally) |
| reportsTo(rob_reports_to_alice) |
| } |
| } |
| } |
| |
| // remember() |
| |
| @Test |
| fun testSimpleRemember() { |
| var count = 0 |
| var changed: (() -> Unit)? = null |
| |
| class Wrapper(val value: Int) { |
| init { |
| count++ |
| } |
| } |
| |
| @Composable fun MockComposeScope.test(value: Int) { |
| changed = invalidate |
| val w = remember { Wrapper(value) } |
| text("value = ${w.value}") |
| } |
| |
| fun MockViewValidator.test(value: Int) { |
| text("value = $value") |
| } |
| |
| val result = compose { |
| test(1) |
| } |
| |
| result.validate { test(1) } |
| |
| assertEquals(1, count) |
| |
| changed!!() |
| result.expectNoChanges() |
| |
| // Expect the previous instance to be remembered |
| assertEquals(1, count) |
| } |
| |
| @Test |
| fun testRememberOneParameter() { |
| var count = 0 |
| |
| class Wrapper(val value: Int) { |
| init { |
| count++ |
| } |
| } |
| |
| @Composable fun MockComposeScope.test(value: Int) { |
| val w = remember(value) { Wrapper(value) } |
| text("value = ${w.value}") |
| } |
| |
| fun MockViewValidator.test(value: Int) { |
| text("value = $value") |
| } |
| |
| var value = 1 |
| var changed: (() -> Unit)? = null |
| val result = compose { |
| changed = invalidate |
| test(value) |
| } |
| |
| result.validate { test(1) } |
| |
| value = 2 |
| changed!!() |
| result.expectChanges() |
| |
| result.validate { test(2) } |
| |
| changed!!() |
| result.expectNoChanges() |
| |
| result.validate { test(2) } |
| |
| assertEquals(2, count) |
| } |
| |
| @Test |
| fun testRememberTwoParameters() { |
| var count = 0 |
| |
| class Wrapper(val a: Int, val b: Int) { |
| init { |
| count++ |
| } |
| } |
| |
| @Composable fun MockComposeScope.test(a: Int, b: Int) { |
| val w = remember(a, b) { Wrapper(a, b) } |
| text("a = ${w.a} b = ${w.b}") |
| } |
| |
| fun MockViewValidator.test(a: Int, b: Int) { |
| text("a = $a b = $b") |
| } |
| |
| var p1 = 1 |
| var p2 = 2 |
| var changed: (() -> Unit)? = null |
| |
| val result = compose { |
| changed = invalidate |
| test(p1, p2) |
| } |
| |
| result.validate { test(1, 2) } |
| |
| p1 = 2 |
| p2 = 3 |
| changed!!() |
| result.expectChanges() |
| |
| result.validate { test(2, 3) } |
| |
| changed!!() |
| result.expectNoChanges() |
| |
| result.validate { test(2, 3) } |
| |
| assertEquals(2, count) |
| } |
| |
| @Test |
| fun testRememberThreeParameters() { |
| var count = 0 |
| |
| class Wrapper(val a: Int, val b: Int, val c: Int) { |
| init { |
| count++ |
| } |
| } |
| |
| @Composable fun MockComposeScope.test(a: Int, b: Int, c: Int) { |
| val w = remember(a, b, c) { Wrapper(a, b, c) } |
| text("a = ${w.a} b = ${w.b} c = ${w.c}") |
| } |
| |
| fun MockViewValidator.test(a: Int, b: Int, c: Int) { |
| text("a = $a b = $b c = $c") |
| } |
| |
| var p3 = 3 |
| var changed: (() -> Unit)? = null |
| val result = compose { |
| changed = invalidate |
| test(1, 2, p3) |
| } |
| |
| result.validate { test(1, 2, 3) } |
| |
| p3 = 4 |
| changed!!() |
| result.expectChanges() |
| |
| result.validate { test(1, 2, 4) } |
| |
| changed!!() |
| result.expectNoChanges() |
| |
| result.validate { test(1, 2, 4) } |
| |
| assertEquals(2, count) |
| } |
| |
| @Test |
| fun testRememberFourParameters() { |
| var count = 0 |
| |
| class Wrapper(val a: Int, val b: Int, val c: Int, val d: Int) { |
| init { |
| count++ |
| } |
| } |
| |
| @Composable fun MockComposeScope.test(a: Int, b: Int, c: Int, d: Int) { |
| val w = remember(a, b, c, d) { Wrapper(a, b, c, d) } |
| text("a = ${w.a} b = ${w.b} c = ${w.c} d = ${w.d}") |
| } |
| |
| fun MockViewValidator.test(a: Int, b: Int, c: Int, d: Int) { |
| text("a = $a b = $b c = $c d = $d") |
| } |
| |
| var p3 = 3 |
| var p4 = 4 |
| var changed: (() -> Unit)? = null |
| |
| val result = compose { |
| changed = invalidate |
| test(1, 2, p3, p4) |
| } |
| |
| result.validate { test(1, 2, 3, 4) } |
| |
| p3 = 4 |
| p4 = 5 |
| changed!!() |
| result.expectChanges() |
| |
| result.validate { test(1, 2, 4, 5) } |
| |
| changed!!() |
| result.expectNoChanges() |
| |
| result.validate { test(1, 2, 4, 5) } |
| |
| assertEquals(2, count) |
| } |
| |
| @Test |
| fun testRememberFiveParameters() { |
| var count = 0 |
| |
| class Wrapper(val a: Int, val b: Int, val c: Int, val d: Int, val e: Int) { |
| init { |
| count++ |
| } |
| } |
| |
| @Composable fun MockComposeScope.test(a: Int, b: Int, c: Int, d: Int, e: Int) { |
| val w = remember(a, b, c, d, e) { Wrapper(a, b, c, d, e) } |
| text("a = ${w.a} b = ${w.b} c = ${w.c} d = ${w.d} e = ${w.e}") |
| } |
| |
| fun MockViewValidator.test(a: Int, b: Int, c: Int, d: Int, e: Int) { |
| text("a = $a b = $b c = $c d = $d e = $e") |
| } |
| |
| var lastParameter = 5 |
| var changed: (() -> Unit)? = null |
| val result = compose { |
| changed = invalidate |
| test(1, 2, 3, 4, lastParameter) |
| } |
| |
| result.validate { test(1, 2, 3, 4, 5) } |
| |
| lastParameter = 6 |
| changed!!() |
| |
| result.expectChanges() |
| |
| result.validate { test(1, 2, 3, 4, 6) } |
| |
| result.expectNoChanges() |
| |
| result.validate { test(1, 2, 3, 4, 6) } |
| |
| assertEquals(2, count) |
| } |
| |
| @Test |
| fun testInsertGroupInContainer() { |
| val values = mutableListOf(0) |
| var changed: (() -> Unit)? = null |
| |
| @Composable fun MockComposeScope.composition() { |
| linear { |
| changed = invalidate |
| for (value in values) { |
| memoize(value, value) { |
| text("$value") |
| } |
| } |
| } |
| } |
| |
| fun MockViewValidator.composition() { |
| linear { |
| for (value in values) |
| text("$value") |
| } |
| } |
| |
| val result = compose { composition() } |
| |
| result.validate { composition() } |
| |
| for (i in 1..10) { |
| values.add(i) |
| changed!!() |
| result.expectChanges() |
| result.validate { composition() } |
| } |
| } |
| |
| // b/148273328 |
| @Test |
| fun testInsertInGroups() { |
| |
| var threeVisible = false |
| var changed: (() -> Unit)? = null |
| |
| @Composable fun MockComposeScope.composition() { |
| linear { |
| text("one") |
| text("two") |
| changed = invalidate |
| if (threeVisible) { |
| text("three") |
| text("four") |
| } |
| linear { |
| text("five") |
| } |
| } |
| } |
| |
| fun MockViewValidator.composition() { |
| linear { |
| text("one") |
| text("two") |
| if (threeVisible) { |
| text("three") |
| text("four") |
| } |
| linear { |
| text("five") |
| } |
| } |
| } |
| |
| val result = compose { composition() } |
| result.validate { composition() } |
| |
| threeVisible = true |
| changed!!() |
| result.expectChanges() |
| |
| result.validate { composition() } |
| } |
| |
| @Test |
| fun testStartJoin() { |
| var text = "Starting" |
| var myInvalidate: (() -> Unit)? = null |
| @Composable fun MockComposeScope.composition() { |
| linear { |
| myInvalidate = invalidate |
| text(text) |
| } |
| } |
| |
| fun MockViewValidator.composition() { |
| linear { |
| text(text) |
| } |
| } |
| |
| val result = compose { composition() } |
| |
| result.validate { composition() } |
| |
| text = "Ending" |
| myInvalidate?.let { it() } |
| |
| result.expectChanges() |
| |
| result.validate { composition() } |
| } |
| |
| @Test |
| fun testInvalidateJoin_End() { |
| var text = "Starting" |
| var includeNested = true |
| var invalidate1 = {} |
| var invalidate2 = {} |
| |
| @Composable fun MockComposeScope.composition() { |
| linear { |
| invalidate1 = invalidate |
| text(text) |
| if (includeNested) { |
| invalidate2 = invalidate |
| text("Nested in $text") |
| } |
| } |
| } |
| |
| fun MockViewValidator.composition() { |
| linear { |
| text(text) |
| if (includeNested) { |
| text("Nested in $text") |
| } |
| } |
| } |
| |
| val result = compose { composition() } |
| |
| result.validate { composition() } |
| |
| text = "Ending" |
| includeNested = false |
| invalidate1() |
| invalidate2() |
| |
| result.expectChanges() |
| |
| result.validate { composition() } |
| |
| result.expectNoChanges() |
| |
| result.validate { composition() } |
| } |
| |
| @Test |
| fun testInvalidateJoin_Start() { |
| var text = "Starting" |
| var includeNested = true |
| var invalidate1: (() -> Unit)? = null |
| var invalidate2: (() -> Unit)? = null |
| |
| @Composable fun MockComposeScope.composition() { |
| linear { |
| invalidate1 = invalidate |
| if (includeNested) { |
| invalidate2 = invalidate |
| text("Nested in $text") |
| } |
| text(text) |
| } |
| } |
| |
| fun MockViewValidator.composition() { |
| linear { |
| if (includeNested) { |
| text("Nested in $text") |
| } |
| text(text) |
| } |
| } |
| |
| val result = compose { composition() } |
| |
| result.validate { composition() } |
| |
| text = "Ending" |
| includeNested = false |
| invalidate1?.invoke() |
| invalidate2?.invoke() |
| |
| result.expectChanges() |
| |
| result.validate { composition() } |
| |
| result.expectNoChanges() |
| |
| result.validate { composition() } |
| } |
| |
| // b/132638679 |
| @Test |
| fun testJoinInvalidate() { |
| var texts = 5 |
| var invalidateOuter: (() -> Unit)? = null |
| var invalidateInner: (() -> Unit)? = null |
| |
| @Composable fun MockComposeScope.composition() { |
| linear { |
| invalidateOuter = invalidate |
| for (i in 1..texts) { |
| text("Some text") |
| } |
| |
| Container { |
| text("Some text") |
| |
| // Force the invalidation to survive the compose |
| val innerInvalidate = invalidate |
| innerInvalidate() |
| invalidateInner = innerInvalidate |
| } |
| } |
| } |
| |
| val result = compose { composition() } |
| |
| texts = 4 |
| invalidateOuter?.invoke() |
| invalidateInner?.invoke() |
| result.expectChanges() |
| |
| texts = 3 |
| invalidateOuter?.invoke() |
| result.expectChanges() |
| } |
| |
| @Test |
| fun testLifecycle_Enter_Simple() { |
| val lifecycleObject = object : CompositionLifecycleObserver { |
| var count = 0 |
| override fun onEnter() { |
| count++ |
| } |
| |
| override fun onLeave() { |
| count-- |
| } |
| } |
| |
| @Composable fun MockComposeScope.composition() { |
| linear { |
| remember { lifecycleObject } |
| text("Some text") |
| } |
| } |
| |
| fun MockViewValidator.composition() { |
| linear { |
| text("Some text") |
| } |
| } |
| |
| var changed: (() -> Unit)? = null |
| val result = compose { |
| changed = invalidate |
| composition() |
| } |
| result.validate { composition() } |
| |
| assertEquals(1, lifecycleObject.count, "object should have been notified of an enter") |
| |
| changed!!() |
| result.expectNoChanges() |
| result.validate { composition() } |
| |
| assertEquals(1, lifecycleObject.count, "Object should have only been notified once") |
| } |
| |
| @Test |
| fun testLifecycle_Enter_SingleNotification() { |
| val lifecycleObject = object : CompositionLifecycleObserver { |
| var count = 0 |
| override fun onEnter() { |
| count++ |
| } |
| |
| override fun onLeave() { |
| count-- |
| } |
| } |
| |
| @Composable fun MockComposeScope.composition() { |
| linear { |
| val l = remember { lifecycleObject } |
| assertEquals(lifecycleObject, l, "Lifecycle object should be returned") |
| text("Some text") |
| } |
| linear { |
| val l = remember { lifecycleObject } |
| assertEquals(lifecycleObject, l, "Lifecycle object should be returned") |
| text("Some other text") |
| } |
| } |
| |
| fun MockViewValidator.composition() { |
| linear { |
| text("Some text") |
| } |
| linear { |
| text("Some other text") |
| } |
| } |
| |
| var changed: (() -> Unit)? = null |
| val result = compose { |
| changed = invalidate |
| composition() |
| } |
| result.validate { composition() } |
| |
| assertEquals(1, lifecycleObject.count, "object should have been notified of an enter") |
| |
| changed!!() |
| result.expectNoChanges() |
| result.validate { composition() } |
| |
| assertEquals(1, lifecycleObject.count, "Object should have only been notified once") |
| } |
| |
| @Test |
| fun testLifecycle_Leave_Simple() { |
| val lifecycleObject = object : CompositionLifecycleObserver { |
| var count = 0 |
| override fun onEnter() { |
| count++ |
| } |
| |
| override fun onLeave() { |
| count-- |
| } |
| } |
| |
| @Composable fun MockComposeScope.composition(includeLifecycleObject: Boolean) { |
| linear { |
| if (includeLifecycleObject) { |
| linear { |
| val l = remember { lifecycleObject } |
| assertEquals(lifecycleObject, l, "Lifecycle object should be returned") |
| text("Some text") |
| } |
| } |
| } |
| } |
| |
| fun MockViewValidator.composition(includeLifecycleObject: Boolean) { |
| linear { |
| if (includeLifecycleObject) { |
| linear { |
| text("Some text") |
| } |
| } |
| } |
| } |
| |
| var changed: (() -> Unit)? = null |
| var value = true |
| val result = compose { |
| changed = invalidate |
| composition(value) |
| } |
| result.validate { composition(true) } |
| |
| assertEquals(1, lifecycleObject.count, "object should have been notified of an enter") |
| |
| changed!!() |
| result.expectNoChanges() |
| result.validate { composition(true) } |
| |
| assertEquals(1, lifecycleObject.count, "Object should have only been notified once") |
| |
| value = false |
| changed!!() |
| result.expectChanges() |
| result.validate { composition(false) } |
| |
| assertEquals(0, lifecycleObject.count, "Object should have been notified of a leave") |
| } |
| |
| @Test |
| fun testLifecycle_Leave_NoLeaveOnReenter() { |
| var expectedEnter = true |
| var expectedLeave = true |
| val lifecycleObject = object : CompositionLifecycleObserver { |
| var count = 0 |
| override fun onEnter() { |
| count++ |
| assertTrue(expectedEnter, "No enter expected") |
| } |
| |
| override fun onLeave() { |
| count-- |
| assertTrue(expectedLeave, "No leave expected") |
| } |
| } |
| |
| @Composable fun MockComposeScope.composition(a: Boolean, b: Boolean, c: Boolean) { |
| linear { |
| if (a) { |
| key(1) { linear { |
| val l = remember { lifecycleObject } |
| assertEquals(lifecycleObject, l, "Lifecycle object should be returned") |
| text("a") |
| } } |
| } |
| if (b) { |
| key(2) { linear { |
| val l = remember { lifecycleObject } |
| assertEquals(lifecycleObject, l, "Lifecycle object should be returned") |
| text("b") |
| } } |
| } |
| if (c) { |
| key(3) { linear { |
| val l = remember { lifecycleObject } |
| assertEquals(lifecycleObject, l, "Lifecycle object should be returned") |
| text("c") |
| } } |
| } |
| } |
| } |
| |
| fun MockViewValidator.composition(a: Boolean, b: Boolean, c: Boolean) { |
| linear { |
| if (a) { |
| linear { |
| text("a") |
| } |
| } |
| if (b) { |
| linear { |
| text("b") |
| } |
| } |
| if (c) { |
| linear { |
| text("c") |
| } |
| } |
| } |
| } |
| |
| expectedEnter = true |
| expectedLeave = false |
| |
| var a = true |
| var b = false |
| var c = false |
| var changed: (() -> Unit)? = null |
| val result = compose { |
| changed = invalidate |
| composition(a = a, b = b, c = c) |
| } |
| result.validate { |
| composition( |
| a = true, |
| b = false, |
| c = false |
| ) |
| } |
| |
| assertEquals( |
| 1, |
| lifecycleObject.count, |
| "object should have been notified of an enter" |
| ) |
| |
| expectedEnter = false |
| expectedLeave = false |
| changed!!() |
| result.expectNoChanges() |
| result.validate { |
| composition( |
| a = true, |
| b = false, |
| c = false |
| ) |
| } |
| assertEquals( |
| 1, |
| lifecycleObject.count, |
| "Object should have only been notified once" |
| ) |
| |
| expectedEnter = false |
| expectedLeave = false |
| a = false |
| b = true |
| c = false |
| changed!!() |
| result.expectChanges() |
| result.validate { |
| composition( |
| a = false, |
| b = true, |
| c = false |
| ) |
| } |
| assertEquals(1, lifecycleObject.count, "No enter or leaves") |
| |
| expectedEnter = false |
| expectedLeave = false |
| a = false |
| b = false |
| c = true |
| changed!!() |
| result.expectChanges() |
| result.validate { |
| composition( |
| a = false, |
| b = false, |
| c = true |
| ) |
| } |
| assertEquals(1, lifecycleObject.count, "No enter or leaves") |
| |
| expectedEnter = false |
| expectedLeave = false |
| a = true |
| b = false |
| c = false |
| changed!!() |
| result.expectChanges() |
| result.validate { |
| composition( |
| a = true, |
| b = false, |
| c = false |
| ) |
| } |
| assertEquals(1, lifecycleObject.count, "No enter or leaves") |
| |
| expectedEnter = false |
| expectedLeave = true |
| a = false |
| b = false |
| c = false |
| changed!!() |
| result.expectChanges() |
| result.validate { |
| composition( |
| a = false, |
| b = false, |
| c = false |
| ) |
| } |
| assertEquals(0, lifecycleObject.count, "A leave") |
| } |
| |
| @Test |
| fun testLifecycle_Leave_LeaveOnReplace() { |
| val lifecycleObject1 = object : CompositionLifecycleObserver { |
| var count = 0 |
| override fun onEnter() { |
| count++ |
| } |
| |
| override fun onLeave() { |
| count-- |
| } |
| } |
| |
| val lifecycleObject2 = object : CompositionLifecycleObserver { |
| var count = 0 |
| override fun onEnter() { |
| count++ |
| } |
| |
| override fun onLeave() { |
| count-- |
| } |
| } |
| |
| var lifecycleObject: Any = lifecycleObject1 |
| var changed = {} |
| |
| @Composable fun MockComposeScope.composition(obj: Any) { |
| linear { |
| key(1) { linear { |
| remember(obj) { obj } |
| text("Some value") |
| } } |
| } |
| } |
| |
| fun MockViewValidator.composition() { |
| linear { |
| linear { |
| text("Some value") |
| } |
| } |
| } |
| |
| val result = compose { |
| changed = invalidate |
| composition(obj = lifecycleObject) |
| } |
| result.validate { composition() } |
| assertEquals(1, lifecycleObject1.count, "first object should enter") |
| assertEquals(0, lifecycleObject2.count, "second object should not have entered") |
| |
| lifecycleObject = lifecycleObject2 |
| changed() |
| result.expectChanges() |
| result.validate { composition() } |
| assertEquals(0, lifecycleObject1.count, "first object should have left") |
| assertEquals(1, lifecycleObject2.count, "second object should have entered") |
| |
| lifecycleObject = object {} |
| changed() |
| result.expectChanges() |
| result.validate { composition() } |
| assertEquals(0, lifecycleObject1.count, "first object should have left") |
| assertEquals(0, lifecycleObject2.count, "second object should have left") |
| } |
| |
| @Test |
| fun testLifecycle_EnterLeaveOrder() { |
| var order = 0 |
| val objects = mutableListOf<Any>() |
| val newLifecycleObject = { name: String -> |
| object : CompositionLifecycleObserver, Counted, |
| Ordered, Named { |
| override var name = name |
| override var count = 0 |
| override var enterOrder = -1 |
| override var leaveOrder = -1 |
| override fun onEnter() { |
| assertEquals(-1, enterOrder, "Only one call to onEnter expected") |
| enterOrder = order++ |
| count++ |
| } |
| |
| override fun onLeave() { |
| assertEquals(-1, leaveOrder, "Only one call to onLeave expected") |
| leaveOrder = order++ |
| count-- |
| } |
| }.also { objects.add(it) } |
| } |
| |
| @Composable fun MockComposeScope.lifecycleUser(name: String) { |
| linear { |
| remember(name) { newLifecycleObject(name) } |
| text(value = name) |
| } |
| } |
| |
| /* |
| A |
| |- B |
| | |- C |
| | |- D |
| |- E |
| |- F |
| | |- G |
| | |- H |
| | |-I |
| |- J |
| |
| Should enter as: A, B, C, D, E, F, G, H, I, J |
| Should leave as: J, I, H, G, F, E, D, C, B, A |
| */ |
| |
| @Composable fun MockComposeScope.tree() { |
| linear { |
| lifecycleUser("A") |
| linear { |
| lifecycleUser("B") |
| linear { |
| lifecycleUser("C") |
| lifecycleUser("D") |
| } |
| lifecycleUser("E") |
| lifecycleUser("F") |
| linear { |
| lifecycleUser("G") |
| lifecycleUser("H") |
| linear { |
| lifecycleUser("I") |
| } |
| } |
| lifecycleUser("J") |
| } |
| } |
| } |
| |
| @Composable fun MockComposeScope.composition(includeTree: Boolean) { |
| linear { |
| if (includeTree) tree() |
| } |
| } |
| |
| var value = true |
| var changed: (() -> Unit)? = null |
| |
| val result = compose { |
| changed = invalidate |
| composition(value) |
| } |
| |
| assertTrue( |
| objects.mapNotNull { it as? Counted }.map { it.count == 1 }.all { it }, |
| "All object should have entered" |
| ) |
| |
| value = false |
| changed!!() |
| result.expectChanges() |
| |
| assertTrue( |
| objects.mapNotNull { it as? Counted }.map { it.count == 0 }.all { it }, |
| "All object should have left" |
| ) |
| |
| assertArrayEquals( |
| "Expected enter order", |
| arrayOf("A", "B", "C", "D", "E", "F", "G", "H", "I", "J"), |
| objects.mapNotNull { it as? Ordered }.sortedBy { it.enterOrder }.map { |
| (it as Named).name |
| }.toTypedArray() |
| ) |
| |
| assertArrayEquals( |
| "Expected leave order", |
| arrayOf("J", "I", "H", "G", "F", "E", "D", "C", "B", "A"), |
| objects.mapNotNull { it as? Ordered }.sortedBy { it.leaveOrder }.map { |
| (it as Named).name |
| }.toTypedArray() |
| ) |
| } |
| |
| @Test |
| fun testCompoundKeyHashStaysTheSameAfterRecompositions() { |
| val outerKeys = mutableListOf<Int>() |
| val innerKeys = mutableListOf<Int>() |
| var previousOuterKeysSize = 0 |
| var previousInnerKeysSize = 0 |
| var outerInvalidate: (() -> Unit) = {} |
| var innerInvalidate: (() -> Unit) = {} |
| |
| @Composable |
| fun MockComposeScope.test() { |
| outerInvalidate = invalidate |
| outerKeys.add(currentComposer.currentCompoundKeyHash) |
| Container { |
| innerInvalidate = invalidate |
| innerKeys.add(currentComposer.currentCompoundKeyHash) |
| } |
| // asserts that the key is correctly rolled back after start and end of Observe |
| assertEquals(outerKeys.last(), currentComposer.currentCompoundKeyHash) |
| } |
| |
| val result = compose { |
| test() |
| } |
| |
| assertNotEquals(previousOuterKeysSize, outerKeys.size) |
| assertNotEquals(previousInnerKeysSize, innerKeys.size) |
| |
| previousOuterKeysSize = outerKeys.size |
| outerInvalidate() |
| result.expectNoChanges() |
| assertNotEquals(previousOuterKeysSize, outerKeys.size) |
| |
| previousInnerKeysSize = innerKeys.size |
| innerInvalidate() |
| result.expectNoChanges() |
| assertNotEquals(previousInnerKeysSize, innerKeys.size) |
| |
| assertNotEquals(innerKeys[0], outerKeys[0]) |
| innerKeys.forEach { |
| assertEquals(innerKeys[0], it) |
| } |
| outerKeys.forEach { |
| assertEquals(outerKeys[0], it) |
| } |
| } |
| |
| @Test // b/152753046 |
| fun testSwappingGroups() { |
| val items = mutableListOf(0, 1, 2, 3, 4) |
| var invalidateComposition = {} |
| |
| @Composable |
| fun MockComposeScope.noNodes() { } |
| |
| @Composable |
| fun MockComposeScope.test() { |
| invalidateComposition = invalidate |
| for (item in items) { |
| key(item) { |
| noNodes() |
| } |
| } |
| } |
| |
| val result = compose { |
| test() |
| } |
| |
| // Swap 2 and 3 |
| items[2] = 3 |
| items[3] = 2 |
| invalidateComposition() |
| |
| result.expectChanges() |
| } |
| |
| @Test // b/154650546 |
| fun testInsertOnMultipleLevels() { |
| val items = mutableListOf( |
| 1 to mutableListOf( |
| 0, 1, 2, 3, 4), |
| 3 to mutableListOf( |
| 0, 1, 2, 3, 4) |
| ) |
| |
| val invalidates = mutableListOf<() -> Unit>() |
| fun invalidateComposition() { |
| invalidates.forEach { it() } |
| invalidates.clear() |
| } |
| |
| @Composable |
| fun MockComposeScope.numbers(numbers: List<Int>) { |
| linear { |
| linear { |
| invalidates.add(invalidate) |
| for (number in numbers) { |
| text("$number") |
| } |
| } |
| } |
| } |
| |
| @Composable |
| fun MockComposeScope.item(number: Int, numbers: List<Int>) { |
| linear { |
| invalidates.add(invalidate) |
| text("$number") |
| numbers(numbers) |
| } |
| } |
| |
| @Composable |
| fun MockComposeScope.test() { |
| invalidates.add(invalidate) |
| |
| linear { |
| invalidates.add(invalidate) |
| for ((number, numbers) in items) { |
| item(number, numbers) |
| } |
| } |
| } |
| |
| fun MockViewValidator.numbers(numbers: List<Int>) { |
| linear { |
| linear { |
| for (number in numbers) { |
| text("$number") |
| } |
| } |
| } |
| } |
| |
| fun MockViewValidator.item(number: Int, numbers: List<Int>) { |
| linear { |
| text("$number") |
| numbers(numbers) |
| } |
| } |
| |
| fun MockViewValidator.test() { |
| linear { |
| for ((number, numbers) in items) { |
| item(number, numbers) |
| } |
| } |
| } |
| |
| val result = compose { |
| test() |
| } |
| |
| fun validate() { |
| result.validate { |
| test() |
| } |
| } |
| |
| validate() |
| |
| // Add numbers to the list at 0 and 1 |
| items[0].second.add(2, 100) |
| items[1].second.add(3, 200) |
| |
| // Add a list to the root. |
| items.add(1, 2 to mutableListOf(0, 1, 2)) |
| |
| invalidateComposition() |
| |
| result.expectChanges() |
| validate() |
| } |
| |
| @Test |
| fun testInsertingAfterSkipping() { |
| val items = mutableListOf( |
| 1 to listOf(0, 1, 2, 3, 4) |
| ) |
| |
| val invalidates = mutableListOf<() -> Unit>() |
| fun invalidateComposition() { |
| invalidates.forEach { it() } |
| invalidates.clear() |
| } |
| |
| @Composable |
| fun MockComposeScope.test() { |
| invalidates.add(invalidate) |
| |
| linear { |
| for ((item, numbers) in items) { |
| text(item.toString()) |
| linear { |
| invalidates.add(invalidate) |
| for (number in numbers) { |
| text(number.toString()) |
| } |
| } |
| } |
| } |
| } |
| |
| fun MockViewValidator.test() { |
| linear { |
| for ((item, numbers) in items) { |
| text(item.toString()) |
| linear { |
| for (number in numbers) { |
| text(number.toString()) |
| } |
| } |
| } |
| } |
| } |
| |
| val result = compose { |
| test() |
| } |
| |
| result.validate { |
| test() |
| } |
| |
| items.add(2 to listOf(3, 4, 5, 6)) |
| invalidateComposition() |
| |
| result.expectChanges() |
| result.validate { |
| test() |
| } |
| } |
| |
| @Test |
| fun evenOddRecomposeGroup() { |
| var includeEven = true |
| var includeOdd = true |
| val invalidates = mutableListOf<() -> Unit>() |
| |
| fun invalidateComposition() { |
| for (invalidate in invalidates) { |
| invalidate() |
| } |
| invalidates.clear() |
| } |
| |
| @Composable |
| fun MockComposeScope.wrapper(children: @Composable () -> Unit) { |
| children() |
| } |
| |
| @Composable |
| fun MockComposeScope.emitText() { |
| invalidates.add(invalidate) |
| if (includeOdd) { |
| key(1) { |
| text("odd 1") |
| } |
| } |
| if (includeEven) { |
| key(2) { |
| text("even 2") |
| } |
| } |
| if (includeOdd) { |
| key(3) { |
| text("odd 3") |
| } |
| } |
| if (includeEven) { |
| key(4) { |
| text("even 4") |
| } |
| } |
| } |
| |
| @Composable |
| fun MockComposeScope.test() { |
| linear { |
| wrapper { |
| emitText() |
| } |
| emitText() |
| wrapper { |
| emitText() |
| } |
| emitText() |
| } |
| } |
| |
| fun MockViewValidator.wrapper(children: () -> Unit) { |
| children() |
| } |
| |
| fun MockViewValidator.emitText() { |
| if (includeOdd) { |
| text("odd 1") |
| } |
| if (includeEven) { |
| text("even 2") |
| } |
| if (includeOdd) { |
| text("odd 3") |
| } |
| if (includeEven) { |
| text("even 4") |
| } |
| } |
| |
| fun MockViewValidator.test() { |
| linear { |
| wrapper { |
| emitText() |
| } |
| emitText() |
| wrapper { |
| emitText() |
| } |
| emitText() |
| } |
| } |
| |
| val result = compose { |
| test() |
| } |
| |
| fun validate() { |
| result.validate { |
| test() |
| } |
| } |
| validate() |
| |
| includeEven = false |
| invalidateComposition() |
| result.expectChanges() |
| validate() |
| |
| includeEven = true |
| includeOdd = false |
| invalidateComposition() |
| result.expectChanges() |
| validate() |
| |
| includeEven = false |
| includeOdd = false |
| invalidateComposition() |
| result.expectChanges() |
| validate() |
| |
| includeEven = true |
| invalidateComposition() |
| result.expectChanges() |
| validate() |
| |
| includeOdd = true |
| invalidateComposition() |
| result.expectChanges() |
| validate() |
| } |
| |
| @Test |
| fun evenOddWithMovement() { |
| var includeEven = true |
| var includeOdd = true |
| var order = listOf(1, 2, 3, 4) |
| val invalidates = mutableListOf<() -> Unit>() |
| |
| fun invalidateComposition() { |
| for (invalidate in invalidates) { |
| invalidate() |
| } |
| invalidates.clear() |
| } |
| |
| @Composable |
| fun MockComposeScope.emitText(all: Boolean) { |
| invalidates.add(invalidate) |
| for (i in order) { |
| if (i % 2 == 1 && (all || includeOdd)) { |
| key(i) { |
| text("odd $i") |
| } |
| } |
| if (i % 2 == 0 && (all || includeEven)) { |
| key(i) { |
| text("even $i") |
| } |
| } |
| } |
| } |
| |
| @Composable |
| fun MockComposeScope.test() { |
| linear { |
| invalidates.add(invalidate) |
| for (i in order) { |
| key(i) { |
| text("group $i") |
| if (i == 2 || (includeEven && includeOdd)) { |
| text("including everything") |
| } else { |
| if (includeEven) { |
| text("including evens") |
| } |
| if (includeOdd) { |
| text("including odds") |
| } |
| } |
| emitText(i == 2) |
| } |
| } |
| emitText(false) |
| } |
| } |
| |
| fun MockViewValidator.emitText(all: Boolean) { |
| for (i in order) { |
| if (i % 2 == 1 && (includeOdd || all)) { |
| text("odd $i") |
| } |
| if (i % 2 == 0 && (includeEven || all)) { |
| text("even $i") |
| } |
| } |
| } |
| |
| fun MockViewValidator.test() { |
| linear { |
| for (i in order) { |
| text("group $i") |
| if (i == 2 || (includeEven && includeOdd)) { |
| text("including everything") |
| } else { |
| if (includeEven) { |
| text("including evens") |
| } |
| if (includeOdd) { |
| text("including odds") |
| } |
| } |
| emitText(i == 2) |
| } |
| emitText(false) |
| } |
| } |
| |
| val result = compose { |
| test() |
| } |
| |
| fun validate() { |
| result.validate { |
| test() |
| } |
| } |
| validate() |
| |
| order = listOf(1, 2, 4, 3) |
| includeEven = false |
| invalidateComposition() |
| result.expectChanges() |
| validate() |
| |
| order = listOf(1, 4, 2, 3) |
| includeEven = true |
| includeOdd = false |
| invalidateComposition() |
| result.expectChanges() |
| validate() |
| |
| order = listOf(3, 4, 2, 1) |
| includeEven = false |
| includeOdd = false |
| invalidateComposition() |
| result.expectChanges() |
| validate() |
| |
| order = listOf(4, 3, 2, 1) |
| includeEven = true |
| invalidateComposition() |
| result.expectChanges() |
| validate() |
| |
| order = listOf(1, 2, 3, 4) |
| includeOdd = true |
| invalidateComposition() |
| result.expectChanges() |
| validate() |
| } |
| } |
| |
| private fun <T> assertArrayEquals(message: String, expected: Array<T>, received: Array<T>) { |
| fun Array<T>.getString() = this.joinToString(", ") { it.toString() } |
| fun err(msg: String): Nothing = error("$message: $msg, expected: [${ |
| expected.getString()}], received: [${received.getString()}]") |
| if (expected.size != received.size) err("sizes are different") |
| expected.indices.forEach { index -> |
| if (expected[index] != received[index]) |
| err("item at index $index was different (expected [${ |
| expected[index]}], received: [${received[index]}]") |
| } |
| } |
| |
| private class CompositionResult( |
| val composer: Composer<*>, |
| val root: View |
| ) { |
| fun validate(block: MockViewValidator.() -> Unit) { |
| MockViewListValidator(root.children).validate(block) |
| } |
| |
| fun expectNoChanges() { |
| val changes = composer.recompose() && composer.changeCount > 0 |
| assertFalse(changes) |
| } |
| |
| fun expectChanges() { |
| val changes = composer.recompose() && composer.changeCount > 0 |
| assertTrue(changes, "Expected changes") |
| composer.applyChanges() |
| composer.slotTable.verifyWellFormed() |
| } |
| |
| fun recompose(): Boolean = composer.recompose() |
| } |
| |
| private fun compose( |
| block: @Composable MockComposeScope.() -> Unit |
| ): CompositionResult { |
| val root = View().apply { name = "root" } |
| val composer = run { |
| val scope = CoroutineScope(Job()) |
| val clock = object : MonotonicFrameClock { |
| override suspend fun <R> withFrameNanos(onFrame: (Long) -> R): R { |
| // The original version of this test used a mock Recomposer |
| // that never successfully scheduled a frame. |
| suspendCancellableCoroutine<Unit> {} |
| return onFrame(0) |
| } |
| } |
| val recomposer = Recomposer().apply { |
| scope.launch(clock) { runRecomposeAndApplyChanges() } |
| } |
| Composer( |
| SlotTable(), |
| ViewApplier(root), |
| recomposer |
| ) |
| } |
| |
| val mockScope = MockComposeScope() |
| composer.composeRoot { |
| invokeComposable(composer) { |
| mockScope.block() |
| } |
| } |
| composer.applyChanges() |
| composer.slotTable.verifyWellFormed() |
| |
| return CompositionResult(composer, root) |
| } |
| |
| // Contact test data |
| private val bob = Contact("Bob Smith", email = "bob@smith.com") |
| private val jon = Contact(name = "Jon Alberton", email = "jon@alberton.com") |
| private val steve = Contact("Steve Roberson", email = "steverob@somemail.com") |
| |
| private fun testModel( |
| contacts: MutableList<Contact> = mutableListOf( |
| bob, |
| jon, |
| steve |
| ) |
| ) = ContactModel(filter = "", contacts = contacts) |
| |
| // Report test data |
| private val jim_reports_to_sally = Report("Jim", "Sally") |
| private val rob_reports_to_alice = Report("Rob", "Alice") |
| private val clark_reports_to_lois = Report("Clark", "Lois") |
| |
| private interface Counted { |
| val count: Int |
| } |
| |
| private interface Ordered { |
| val enterOrder: Int |
| val leaveOrder: Int |
| } |
| |
| private interface Named { |
| val name: String |
| } |