[go: nahoru, domu]

blob: cef557cf52ca7940ca916c7c3e5f776b20c9a2bd [file] [log] [blame]
Adam Powella1f55b92020-04-20 16:32:38 -07001/*
2 * Copyright 2020 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package androidx.ui.core
18
19import androidx.compose.Applier
Adam Powella1f55b92020-04-20 16:32:38 -070020import androidx.compose.Composable
21import androidx.compose.Composer
Leland Richardson656c1132020-06-03 09:12:42 -070022import androidx.compose.ExperimentalComposeApi
23import androidx.compose.InternalComposeApi
Adam Powella1f55b92020-04-20 16:32:38 -070024import androidx.compose.Recomposer
25import androidx.compose.SlotTable
26import androidx.compose.currentComposer
Louis Pullen-Freilich4b2e7e02020-07-21 17:26:40 +010027import androidx.compose.runtime.dispatch.MonotonicFrameClock
Adam Powella1f55b92020-04-20 16:32:38 -070028import androidx.compose.invalidate
29import androidx.compose.remember
Adam Powell11d00c72020-04-15 09:28:13 -070030import androidx.compose.withRunningRecomposer
31import kotlinx.coroutines.channels.Channel
32import kotlinx.coroutines.runBlocking
Adam Powellb0c1b402020-06-24 20:26:09 -070033import kotlinx.coroutines.withContext
Adam Powella1f55b92020-04-20 16:32:38 -070034import org.junit.Assert.assertEquals
35import org.junit.Assert.assertNotEquals
36import org.junit.Assert.assertNotNull
37import org.junit.Assert.assertTrue
38import org.junit.Test
39
40private class TestTagModifier<T>(val name: String, val value: T) : Modifier.Element
41
42fun <T> Modifier.testTag(name: String, value: T) = this + TestTagModifier(name, value)
43
44fun <T> Modifier.getTestTag(name: String, default: T): T = foldIn(default) { acc, element ->
45 @Suppress("UNCHECKED_CAST")
46 if (element is TestTagModifier<*> && element.name == name) element.value as T else acc
47}
48
Leland Richardson656c1132020-06-03 09:12:42 -070049@OptIn(InternalComposeApi::class)
Adam Powella1f55b92020-04-20 16:32:38 -070050class ComposedModifierTest {
51
Adam Powella1f55b92020-04-20 16:32:38 -070052 /**
53 * Confirm that a [composed] modifier correctly constructs separate instances when materialized
54 */
55 @Test
Adam Powellb0c1b402020-06-24 20:26:09 -070056 fun materializeComposedModifier() = runBlocking(TestFrameClock()) {
Adam Powella1f55b92020-04-20 16:32:38 -070057 // Note: assumes single-threaded composition
58 var counter = 0
59 val sourceMod = Modifier.testTag("static", 0)
60 .composed { testTag("dynamic", ++counter) }
61
Adam Powellb0c1b402020-06-24 20:26:09 -070062 withRunningRecomposer { recomposer ->
Adam Powell11d00c72020-04-15 09:28:13 -070063 lateinit var firstMaterialized: Modifier
64 lateinit var secondMaterialized: Modifier
65 compose(recomposer) {
66 firstMaterialized = currentComposer.materialize(sourceMod)
67 secondMaterialized = currentComposer.materialize(sourceMod)
68 }
69
70 assertNotEquals("I recomposed some modifiers", 0, counter)
71
72 assertEquals(
73 "first static value equal to source",
74 sourceMod.getTestTag("static", Int.MIN_VALUE),
75 firstMaterialized.getTestTag("static", Int.MAX_VALUE)
76 )
77 assertEquals(
78 "second static value equal to source",
79 sourceMod.getTestTag("static", Int.MIN_VALUE),
80 secondMaterialized.getTestTag("static", Int.MAX_VALUE)
81 )
82 assertEquals(
83 "dynamic value not present in source",
84 Int.MIN_VALUE,
85 sourceMod.getTestTag("dynamic", Int.MIN_VALUE)
86 )
87 assertNotEquals(
88 "dynamic value present in first materialized",
89 Int.MIN_VALUE,
90 firstMaterialized.getTestTag("dynamic", Int.MIN_VALUE)
91 )
92 assertNotEquals(
93 "dynamic value present in second materialized",
94 Int.MIN_VALUE,
95 firstMaterialized.getTestTag("dynamic", Int.MIN_VALUE)
96 )
97 assertNotEquals(
98 "first and second dynamic values must be unequal",
99 firstMaterialized.getTestTag("dynamic", Int.MIN_VALUE),
100 secondMaterialized.getTestTag("dynamic", Int.MIN_VALUE)
101 )
Adam Powella1f55b92020-04-20 16:32:38 -0700102 }
Adam Powella1f55b92020-04-20 16:32:38 -0700103 }
104
105 /**
106 * Confirm that recomposition occurs on invalidation
107 */
108 @Test
Adam Powell11d00c72020-04-15 09:28:13 -0700109 fun recomposeComposedModifier() = runBlocking {
Adam Powella1f55b92020-04-20 16:32:38 -0700110 // Manually invalidate the composition of the modifier instead of using mutableStateOf
111 // Frame-based recomposition requires the FrameManager be up and running.
112 var value = 0
113 lateinit var invalidator: () -> Unit
114
115 val sourceMod = Modifier.composed {
116 invalidator = invalidate
117 testTag("changing", value)
118 }
119
Adam Powell11d00c72020-04-15 09:28:13 -0700120 val frameClock = TestFrameClock()
Adam Powellb0c1b402020-06-24 20:26:09 -0700121 withContext(frameClock) {
122 withRunningRecomposer { recomposer ->
123 lateinit var materialized: Modifier
124 compose(recomposer) {
125 materialized = currentComposer.materialize(sourceMod)
126 }
127
128 assertEquals(
129 "initial composition value",
130 0,
131 materialized.getTestTag("changing", Int.MIN_VALUE)
132 )
133
134 value = 5
135 invalidator()
136 frameClock.frame(0L)
137
138 assertEquals(
139 "recomposed composition value",
140 5,
141 materialized.getTestTag("changing", Int.MIN_VALUE)
142 )
Adam Powell11d00c72020-04-15 09:28:13 -0700143 }
Adam Powella1f55b92020-04-20 16:32:38 -0700144 }
Adam Powella1f55b92020-04-20 16:32:38 -0700145 }
146
147 @Test
Adam Powell11d00c72020-04-15 09:28:13 -0700148 fun rememberComposedModifier() = runBlocking {
Adam Powella1f55b92020-04-20 16:32:38 -0700149 lateinit var invalidator: () -> Unit
150 val sourceMod = Modifier.composed {
151 invalidator = invalidate
152 val state = remember { Any() }
153 testTag("remembered", state)
154 }
155
Adam Powell11d00c72020-04-15 09:28:13 -0700156 val frameClock = TestFrameClock()
157
Adam Powellb0c1b402020-06-24 20:26:09 -0700158 withContext(frameClock) {
159 withRunningRecomposer { recomposer ->
160 val results = mutableListOf<Any?>()
161 val notFound = Any()
162 compose(recomposer) {
163 results.add(
164 currentComposer.materialize(sourceMod).getTestTag("remembered", notFound)
165 )
166 }
167
168 assertTrue("one item added for initial composition", results.size == 1)
169 assertNotNull("remembered object not null", results[0])
170
171 invalidator()
172 frameClock.frame(0)
173
174 assertEquals("two items added after recomposition", 2, results.size)
175 assertTrue("no null items", results.none { it === notFound })
176 assertEquals("remembered references are equal", results[0], results[1])
Adam Powell11d00c72020-04-15 09:28:13 -0700177 }
Adam Powella1f55b92020-04-20 16:32:38 -0700178 }
Adam Powella1f55b92020-04-20 16:32:38 -0700179 }
180
181 @Test
Adam Powell11d00c72020-04-15 09:28:13 -0700182 fun nestedComposedModifiers() = runBlocking {
Adam Powella1f55b92020-04-20 16:32:38 -0700183 val mod = Modifier.composed {
184 composed {
185 testTag("nested", 10)
186 }
187 }
188
Adam Powell11d00c72020-04-15 09:28:13 -0700189 val frameClock = TestFrameClock()
Adam Powella1f55b92020-04-20 16:32:38 -0700190
Adam Powellb0c1b402020-06-24 20:26:09 -0700191 withContext(frameClock) {
192 withRunningRecomposer { recomposer ->
193 lateinit var materialized: Modifier
194 compose(recomposer) {
195 materialized = currentComposer.materialize(mod)
196 }
Adam Powell11d00c72020-04-15 09:28:13 -0700197
Adam Powellb0c1b402020-06-24 20:26:09 -0700198 assertEquals(
199 "fully unwrapped composed modifier value",
200 10,
201 materialized.getTestTag("nested", 0)
202 )
203 }
Adam Powell11d00c72020-04-15 09:28:13 -0700204 }
Adam Powella1f55b92020-04-20 16:32:38 -0700205 }
206}
207
Leland Richardson19285b12020-06-18 13:37:21 -0700208@OptIn(InternalComposeApi::class, ExperimentalComposeApi::class)
Adam Powella1f55b92020-04-20 16:32:38 -0700209private fun compose(
Adam Powell11d00c72020-04-15 09:28:13 -0700210 recomposer: Recomposer,
Louis Pullen-Freilich3a54b942020-05-07 13:23:03 +0100211 block: @Composable () -> Unit
Leland Richardson19285b12020-06-18 13:37:21 -0700212): Composer<Unit> {
213 return Composer(
214 SlotTable(),
215 EmptyApplier(),
216 recomposer
217 ).apply {
218 composeRoot {
219 @Suppress("UNCHECKED_CAST")
220 val fn = block as (Composer<*>, Int, Int) -> Unit
221 fn(this, 0, 0)
222 }
223 applyChanges()
224 slotTable.verifyWellFormed()
225 }
Adam Powella1f55b92020-04-20 16:32:38 -0700226}
227
Adam Powell28b24c32020-06-15 17:08:49 -0700228private class TestFrameClock : MonotonicFrameClock {
Adam Powell11d00c72020-04-15 09:28:13 -0700229
230 private val frameCh = Channel<Long>()
231
232 suspend fun frame(frameTimeNanos: Long) {
233 frameCh.send(frameTimeNanos)
Adam Powell85b14d02020-05-19 11:39:03 -0700234 }
Adam Powell11d00c72020-04-15 09:28:13 -0700235
Adam Powelle6bbf202020-06-05 16:19:08 -0700236 override suspend fun <R> withFrameNanos(onFrame: (Long) -> R): R = onFrame(frameCh.receive())
Adam Powella1f55b92020-04-20 16:32:38 -0700237}
238
Leland Richardson656c1132020-06-03 09:12:42 -0700239@OptIn(ExperimentalComposeApi::class)
Leland Richardson19285b12020-06-18 13:37:21 -0700240class EmptyApplier : Applier<Unit> {
241 override val current: Unit = Unit
242 override fun down(node: Unit) {}
243 override fun up() {}
244 override fun insert(index: Int, instance: Unit) {
245 error("Unexpected")
Adam Powella1f55b92020-04-20 16:32:38 -0700246 }
Leland Richardson19285b12020-06-18 13:37:21 -0700247 override fun remove(index: Int, count: Int) {
248 error("Unexpected")
249 }
250 override fun move(from: Int, to: Int, count: Int) {
251 error("Unexpected")
252 }
Adam Powell8dec9b72020-06-19 14:24:14 -0700253 override fun clear() {}
Leland Richardson19285b12020-06-18 13:37:21 -0700254}