[go: nahoru, domu]

blob: 4b0cb268a481f26cbc54493bcc443825d00e73f4 [file] [log] [blame]
Leland Richardson6cab6e32019-05-02 11:52:13 -07001/*
Leland Richardson00850282020-03-04 14:40:37 -08002 * Copyright 2020 The Android Open Source Project
Leland Richardson6cab6e32019-05-02 11:52:13 -07003 *
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
Leland Richardson00850282020-03-04 14:40:37 -080017package androidx.compose.test
Chuck Jazdzewski724fffc2018-08-10 13:42:56 -070018
19import android.view.View
20import android.view.ViewGroup
21import android.widget.LinearLayout
22import android.widget.TextView
Leland Richardson00850282020-03-04 14:40:37 -080023import androidx.compose.Composable
Chuck Jazdzewski0a90de92020-05-21 10:03:47 -070024import androidx.compose.ExperimentalComposeApi
Andrey Kulikovd13aefe2020-03-16 13:52:22 +000025import androidx.compose.clearRoots
Chuck Jazdzewskib15857c2020-05-26 17:14:02 -070026import androidx.compose.getValue
Leland Richardson00850282020-03-04 14:40:37 -080027import androidx.compose.invalidate
28import androidx.compose.key
Chuck Jazdzewskib15857c2020-05-26 17:14:02 -070029import androidx.compose.mutableStateOf
30import androidx.compose.remember
31import androidx.compose.setValue
Chuck Jazdzewski0a90de92020-05-21 10:03:47 -070032import androidx.compose.snapshots.currentSnapshot
Cătălin Tudora2ed24e2019-07-04 13:50:58 +010033import androidx.test.ext.junit.runners.AndroidJUnit4
Alexandre Elias558d1c92020-02-20 18:04:25 -080034import androidx.test.filters.MediumTest
Cătălin Tudora2ed24e2019-07-04 13:50:58 +010035import junit.framework.TestCase.assertEquals
36import junit.framework.TestCase.assertFalse
37import junit.framework.TestCase.assertNotSame
38import junit.framework.TestCase.assertTrue
Leland Richardson7f848ab2019-12-12 13:43:41 -080039import org.junit.After
Cătălin Tudora2ed24e2019-07-04 13:50:58 +010040import org.junit.Rule
Chuck Jazdzewski724fffc2018-08-10 13:42:56 -070041import org.junit.Test
42import org.junit.runner.RunWith
Chuck Jazdzewskid0235045922020-07-22 11:14:20 -070043import kotlin.test.assertNotNull
Chuck Jazdzewski724fffc2018-08-10 13:42:56 -070044
Alexandre Elias558d1c92020-02-20 18:04:25 -080045@MediumTest
Cătălin Tudora2ed24e2019-07-04 13:50:58 +010046@RunWith(AndroidJUnit4::class)
Leland Richardson76600e32020-01-15 17:22:29 -080047class RecomposerTests : BaseComposeTest() {
Leland Richardson7f848ab2019-12-12 13:43:41 -080048 @After
49 fun teardown() {
Leland Richardson00850282020-03-04 14:40:37 -080050 clearRoots()
Leland Richardson7f848ab2019-12-12 13:43:41 -080051 }
52
Cătălin Tudora2ed24e2019-07-04 13:50:58 +010053 @get:Rule
Leland Richardson76600e32020-01-15 17:22:29 -080054 override val activityRule = makeTestActivityRule()
Cătălin Tudora2ed24e2019-07-04 13:50:58 +010055
Chuck Jazdzewski724fffc2018-08-10 13:42:56 -070056 @Test
Chuck Jazdzewskifff2da22019-08-05 10:14:43 -070057 fun testNativeViewWithAttributes() {
58 compose {
Alexandre Eliasc69fc4a2020-02-20 17:50:33 -080059 TextView(id = 456, text = "some text")
Leland Richardson76600e32020-01-15 17:22:29 -080060 }.then { activity ->
61 assertEquals(1, activity.root.childCount)
Chuck Jazdzewskifff2da22019-08-05 10:14:43 -070062
63 val tv = activity.findViewById(456) as TextView
64 assertEquals("some text", tv.text)
65
Leland Richardson76600e32020-01-15 17:22:29 -080066 assertEquals(tv, activity.root.getChildAt(0))
Chuck Jazdzewski724fffc2018-08-10 13:42:56 -070067 }
Chuck Jazdzewski724fffc2018-08-10 13:42:56 -070068 }
69
70 @Test
71 fun testSlotKeyChangeCausesRecreate() {
72 var i = 1
Chuck Jazdzewskifff2da22019-08-05 10:14:43 -070073 var tv1: TextView? = null
Leland Richardson75f3ba92020-02-20 19:54:44 -080074 val trigger = Trigger()
Chuck Jazdzewski724fffc2018-08-10 13:42:56 -070075 compose {
Leland Richardson75f3ba92020-02-20 19:54:44 -080076 trigger.subscribe()
Chuck Jazdzewski724fffc2018-08-10 13:42:56 -070077 // this should cause the textview to get recreated on every compose
78 i++
79
Leland Richardson534a3462020-02-05 23:26:02 -080080 key(i) {
Alexandre Eliasc69fc4a2020-02-20 17:50:33 -080081 TextView(id = 456, text = "some text")
Chuck Jazdzewski724fffc2018-08-10 13:42:56 -070082 }
Leland Richardson76600e32020-01-15 17:22:29 -080083 }.then { activity ->
Chuck Jazdzewskifff2da22019-08-05 10:14:43 -070084 tv1 = activity.findViewById(456) as TextView
Leland Richardson75f3ba92020-02-20 19:54:44 -080085 trigger.recompose()
86 }.then { activity ->
Chuck Jazdzewski724fffc2018-08-10 13:42:56 -070087 assertEquals("Compose got called twice", 3, i)
88
89 val tv2 = activity.findViewById(456) as TextView
90
Chuck Jazdzewski9d160f32019-04-08 09:47:44 -070091 assertFalse(
92 "The text views should be different instances",
93 tv1 === tv2
94 )
Chuck Jazdzewski724fffc2018-08-10 13:42:56 -070095
Chuck Jazdzewski9d160f32019-04-08 09:47:44 -070096 assertEquals(
97 "The unused child got removed from the view hierarchy",
98 1,
Leland Richardson76600e32020-01-15 17:22:29 -080099 activity.root.childCount
Chuck Jazdzewski9d160f32019-04-08 09:47:44 -0700100 )
Chuck Jazdzewski724fffc2018-08-10 13:42:56 -0700101 }
102 }
103
104 @Test
105 fun testViewWithViewChildren() {
106 compose {
Leland Richardson534a3462020-02-05 23:26:02 -0800107 LinearLayout(id = 345) {
Alexandre Eliasc69fc4a2020-02-20 17:50:33 -0800108 TextView(id = 456, text = "some text")
109 TextView(id = 567, text = "some text")
Chuck Jazdzewski724fffc2018-08-10 13:42:56 -0700110 }
Leland Richardson76600e32020-01-15 17:22:29 -0800111 }.then { activity ->
Chuck Jazdzewski724fffc2018-08-10 13:42:56 -0700112 val ll = activity.findViewById(345) as LinearLayout
113 val tv1 = activity.findViewById(456) as TextView
114 val tv2 = activity.findViewById(567) as TextView
115
Leland Richardson76600e32020-01-15 17:22:29 -0800116 assertEquals("The linear layout should be the only child of root", 1,
117 activity.root.childCount)
Chuck Jazdzewskifff2da22019-08-05 10:14:43 -0700118 assertEquals("Both children should have been added", 2, ll.childCount)
Chuck Jazdzewski9d160f32019-04-08 09:47:44 -0700119 assertTrue(
120 "Should be the expected TextView (1)",
121 ll.getChildAt(0) === tv1
122 )
123 assertTrue(
124 "Should be the expected TextView (2)",
125 ll.getChildAt(1) === tv2
126 )
Chuck Jazdzewski724fffc2018-08-10 13:42:56 -0700127 }
128 }
129
130 @Test
131 fun testForLoop() {
132 val items = listOf(1, 2, 3, 4, 5, 6)
133 compose {
Leland Richardson534a3462020-02-05 23:26:02 -0800134 LinearLayout(id = 345) {
Leland Richardson76600e32020-01-15 17:22:29 -0800135 for (i in items) {
Alexandre Eliasc69fc4a2020-02-20 17:50:33 -0800136 TextView(id = 456, text = "some text $i")
Chuck Jazdzewski724fffc2018-08-10 13:42:56 -0700137 }
138 }
Leland Richardson76600e32020-01-15 17:22:29 -0800139 }.then { activity ->
Chuck Jazdzewski724fffc2018-08-10 13:42:56 -0700140 val ll = activity.findViewById(345) as LinearLayout
141
Leland Richardson76600e32020-01-15 17:22:29 -0800142 assertEquals("The linear layout should be the only child of root", 1,
143 activity.root.childCount)
Chuck Jazdzewski724fffc2018-08-10 13:42:56 -0700144 assertEquals("Each item in the for loop should be a child", items.size, ll.childCount)
145 items.forEachIndexed { index, i ->
Chuck Jazdzewski9d160f32019-04-08 09:47:44 -0700146 assertEquals(
147 "Should be the correct child", "some text $i",
148 (ll.getChildAt(index) as TextView).text
149 )
Chuck Jazdzewski724fffc2018-08-10 13:42:56 -0700150 }
151 }
152 }
153
154 @Test
155 fun testRecompose() {
156 val counter = Counter()
157
Leland Richardson76600e32020-01-15 17:22:29 -0800158 compose {
Leland Richardson00850282020-03-04 14:40:37 -0800159 RecomposeTestComponentsA(
Leland Richardson534a3462020-02-05 23:26:02 -0800160 counter,
Leland Richardson00850282020-03-04 14:40:37 -0800161 ClickAction.Recompose
Leland Richardson534a3462020-02-05 23:26:02 -0800162 )
Leland Richardson76600e32020-01-15 17:22:29 -0800163 }.then { activity ->
Chuck Jazdzewski724fffc2018-08-10 13:42:56 -0700164 // everything got rendered once
165 assertEquals(1, counter["A"])
166 assertEquals(1, counter["100"])
167 assertEquals(1, counter["101"])
168 assertEquals(1, counter["102"])
169
Chuck Jazdzewski724fffc2018-08-10 13:42:56 -0700170 (activity.findViewById(100) as TextView).performClick()
171 (activity.findViewById(102) as TextView).performClick()
172
173 // nothing should happen synchronously
174 assertEquals(1, counter["A"])
175 assertEquals(1, counter["100"])
176 assertEquals(1, counter["101"])
177 assertEquals(1, counter["102"])
Leland Richardson76600e32020-01-15 17:22:29 -0800178 }.then { activity ->
Chuck Jazdzewski724fffc2018-08-10 13:42:56 -0700179 // only the clicked view got rerendered
180 assertEquals(1, counter["A"])
181 assertEquals(2, counter["100"])
182 assertEquals(1, counter["101"])
183 assertEquals(2, counter["102"])
184
Chuck Jazdzewski724fffc2018-08-10 13:42:56 -0700185 // recompose() both the parent and the child... and show that the child only
186 // recomposes once as a result
187 (activity.findViewById(99) as LinearLayout).performClick()
188 (activity.findViewById(102) as TextView).performClick()
Leland Richardson76600e32020-01-15 17:22:29 -0800189 }.then {
Chuck Jazdzewski724fffc2018-08-10 13:42:56 -0700190
Chuck Jazdzewski724fffc2018-08-10 13:42:56 -0700191 assertEquals(2, counter["A"])
Leland Richardson75f3ba92020-02-20 19:54:44 -0800192 assertEquals(3, counter["100"])
193 assertEquals(2, counter["101"])
Leland Richardsonf87e0e92019-01-08 19:09:03 -0800194 assertEquals(3, counter["102"])
Chuck Jazdzewski724fffc2018-08-10 13:42:56 -0700195 }
196 }
197
198 @Test
Matvei Malkov3bb20c32019-01-21 16:59:16 +0000199 fun testRootRecompose() {
200 val counter = Counter()
Leland Richardson75f3ba92020-02-20 19:54:44 -0800201 val trigger = Trigger()
Matvei Malkov3bb20c32019-01-21 16:59:16 +0000202
Leland Richardson6cab6e32019-05-02 11:52:13 -0700203 val listener =
Leland Richardson00850282020-03-04 14:40:37 -0800204 ClickAction.PerformOnView {
Leland Richardson75f3ba92020-02-20 19:54:44 -0800205 trigger.recompose()
Matvei Malkov3bb20c32019-01-21 16:59:16 +0000206 }
Matvei Malkov3bb20c32019-01-21 16:59:16 +0000207
Leland Richardson76600e32020-01-15 17:22:29 -0800208 compose {
Leland Richardson75f3ba92020-02-20 19:54:44 -0800209 trigger.subscribe()
Leland Richardson00850282020-03-04 14:40:37 -0800210 RecomposeTestComponentsA(
Leland Richardson534a3462020-02-05 23:26:02 -0800211 counter,
212 listener
213 )
Leland Richardson76600e32020-01-15 17:22:29 -0800214 }.then { activity ->
Matvei Malkov3bb20c32019-01-21 16:59:16 +0000215 // everything got rendered once
216 assertEquals(1, counter["A"])
217 assertEquals(1, counter["100"])
218 assertEquals(1, counter["101"])
219 assertEquals(1, counter["102"])
220
Matvei Malkov3bb20c32019-01-21 16:59:16 +0000221 (activity.findViewById(100) as TextView).performClick()
222 (activity.findViewById(102) as TextView).performClick()
223
224 // nothing should happen synchronously
225 assertEquals(1, counter["A"])
226 assertEquals(1, counter["100"])
227 assertEquals(1, counter["101"])
228 assertEquals(1, counter["102"])
Leland Richardson76600e32020-01-15 17:22:29 -0800229 }.then { activity ->
Leland Richardson75f3ba92020-02-20 19:54:44 -0800230 // as we recompose ROOT on every tap, everything should be increased once, because two
231 // clicks layed to one frame. None of these components are skippable, so each increments
Matvei Malkov3bb20c32019-01-21 16:59:16 +0000232 assertEquals(2, counter["A"])
Leland Richardson75f3ba92020-02-20 19:54:44 -0800233 assertEquals(2, counter["100"])
234 assertEquals(2, counter["101"])
235 assertEquals(2, counter["102"])
Matvei Malkov3bb20c32019-01-21 16:59:16 +0000236
Matvei Malkov3bb20c32019-01-21 16:59:16 +0000237 (activity.findViewById(99) as LinearLayout).performClick()
238 (activity.findViewById(102) as TextView).performClick()
Leland Richardson76600e32020-01-15 17:22:29 -0800239 }.then {
Leland Richardson75f3ba92020-02-20 19:54:44 -0800240 // again, no matter what we tapped, we want to recompose root, so all counts increased
Matvei Malkov3bb20c32019-01-21 16:59:16 +0000241 assertEquals(3, counter["A"])
Leland Richardson75f3ba92020-02-20 19:54:44 -0800242 assertEquals(3, counter["100"])
243 assertEquals(3, counter["101"])
244 assertEquals(3, counter["102"])
Matvei Malkov3bb20c32019-01-21 16:59:16 +0000245 }
246 }
Matvei Malkov3bb20c32019-01-21 16:59:16 +0000247
Chuck Jazdzewski9d160f32019-04-08 09:47:44 -0700248 // components for testing recompose behavior above
Leland Richardson00850282020-03-04 14:40:37 -0800249 sealed class ClickAction {
250 object Recompose : ClickAction()
251 class PerformOnView(val action: (View) -> Unit) : ClickAction()
252 }
Matvei Malkov3bb20c32019-01-21 16:59:16 +0000253
Leland Richardson00850282020-03-04 14:40:37 -0800254 @Composable fun RecomposeTestComponentsB(counter: Counter, listener: ClickAction, id: Int = 0) {
255 counter.inc("$id")
Matvei Malkov3bb20c32019-01-21 16:59:16 +0000256
Leland Richardson00850282020-03-04 14:40:37 -0800257 val recompose = invalidate
Leland Richardson76600e32020-01-15 17:22:29 -0800258
Leland Richardson00850282020-03-04 14:40:37 -0800259 TextView(id = id, onClickListener = View.OnClickListener {
260 @Suppress("DEPRECATION")
261 when (listener) {
262 is ClickAction.Recompose -> recompose()
263 is ClickAction.PerformOnView -> listener.action.invoke(it)
264 }
265 })
266 }
Matvei Malkov3bb20c32019-01-21 16:59:16 +0000267
Leland Richardson00850282020-03-04 14:40:37 -0800268 @Composable fun RecomposeTestComponentsA(counter: Counter, listener: ClickAction) {
269 counter.inc("A")
270 val recompose = invalidate
Alexandre Eliasc69fc4a2020-02-20 17:50:33 -0800271 LinearLayout(id = 99, onClickListener = View.OnClickListener {
Leland Richardson534a3462020-02-05 23:26:02 -0800272 @Suppress("DEPRECATION")
273 when (listener) {
274 is ClickAction.Recompose -> recompose()
275 is ClickAction.PerformOnView -> listener.action.invoke(it)
276 }
277 }) {
278 for (id in 100..102) {
279 key(id) {
Leland Richardson00850282020-03-04 14:40:37 -0800280 RecomposeTestComponentsB(
281 counter,
282 listener,
283 id
284 )
Matvei Malkov3bb20c32019-01-21 16:59:16 +0000285 }
286 }
287 }
Chuck Jazdzewski724fffc2018-08-10 13:42:56 -0700288 }
289
290 @Test
Chuck Jazdzewskifff2da22019-08-05 10:14:43 -0700291 fun testCorrectViewTree() {
292 compose {
Alexandre Eliasc69fc4a2020-02-20 17:50:33 -0800293 LinearLayout {
294 LinearLayout { }
295 LinearLayout { }
296 }
297 LinearLayout { }
Leland Richardson76600e32020-01-15 17:22:29 -0800298 }.then { activity ->
299 assertChildHierarchy(activity.root) {
Chuck Jazdzewskifff2da22019-08-05 10:14:43 -0700300 """
301 <LinearLayout>
302 <LinearLayout />
303 <LinearLayout />
304 </LinearLayout>
Chuck Jazdzewski724fffc2018-08-10 13:42:56 -0700305 <LinearLayout />
Chuck Jazdzewskifff2da22019-08-05 10:14:43 -0700306 """
307 }
Chuck Jazdzewski724fffc2018-08-10 13:42:56 -0700308 }
309 }
310
311 @Test
312 fun testCorrectViewTreeWithComponents() {
313
Leland Richardson76600e32020-01-15 17:22:29 -0800314 @Composable fun B() {
Leland Richardson534a3462020-02-05 23:26:02 -0800315 TextView()
Chuck Jazdzewski724fffc2018-08-10 13:42:56 -0700316 }
317
318 compose {
Alexandre Eliasc69fc4a2020-02-20 17:50:33 -0800319 LinearLayout {
320 LinearLayout {
321 B()
322 }
323 LinearLayout {
324 B()
325 }
326 }
Leland Richardson76600e32020-01-15 17:22:29 -0800327 }.then { activity ->
Chuck Jazdzewski724fffc2018-08-10 13:42:56 -0700328
Leland Richardson76600e32020-01-15 17:22:29 -0800329 assertChildHierarchy(activity.root) {
Chuck Jazdzewski724fffc2018-08-10 13:42:56 -0700330 """
331 <LinearLayout>
332 <LinearLayout>
333 <TextView />
334 </LinearLayout>
335 <LinearLayout>
336 <TextView />
337 </LinearLayout>
338 </LinearLayout>
339 """
340 }
341 }
342 }
343
344 @Test
345 fun testCorrectViewTreeWithComponentWithMultipleRoots() {
346
Leland Richardson76600e32020-01-15 17:22:29 -0800347 @Composable fun B() {
Alexandre Eliasc69fc4a2020-02-20 17:50:33 -0800348 TextView()
349 TextView()
Chuck Jazdzewski724fffc2018-08-10 13:42:56 -0700350 }
351
352 compose {
Alexandre Eliasc69fc4a2020-02-20 17:50:33 -0800353 LinearLayout {
354 LinearLayout {
355 B()
356 }
357 LinearLayout {
358 B()
359 }
360 }
Leland Richardson76600e32020-01-15 17:22:29 -0800361 }.then {
Chuck Jazdzewski724fffc2018-08-10 13:42:56 -0700362
Leland Richardson76600e32020-01-15 17:22:29 -0800363 assertChildHierarchy(activity.root) {
Chuck Jazdzewski724fffc2018-08-10 13:42:56 -0700364 """
365 <LinearLayout>
366 <LinearLayout>
367 <TextView />
368 <TextView />
369 </LinearLayout>
370 <LinearLayout>
371 <TextView />
372 <TextView />
373 </LinearLayout>
374 </LinearLayout>
375 """
376 }
377 }
378 }
Chuck Jazdzewski7e19aa52019-04-19 12:36:48 -0700379
Chuck Jazdzewskib15857c2020-05-26 17:14:02 -0700380 @Test // regression b/157111271
381 fun testInsertDuringRecomposition() {
382 var includeA by mutableStateOf(false)
383 var someState by mutableStateOf(0)
384 var someOtherState by mutableStateOf(1)
385
386 @Composable fun B(@Suppress("UNUSED_PARAMETER") value: Int) {
387 // empty
388 }
389
390 @Composable fun A() {
391 B(someState)
392 someState++
393 }
394
395 @Composable fun T() {
396 subCompose {
397 // Take up some slot space
398 // This makes it more likely to reproduce bug 157111271.
399 remember(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15) {
400 1
401 }
402 if (includeA) {
403 Wrapper {
404 B(0)
405 B(someOtherState)
406 B(2)
407 B(3)
408 B(4)
409 A()
410 }
411 }
412 }
413 }
414
415 compose {
416 T()
417 }.then {
418 includeA = true
419 }.then {
420 someOtherState = 10
421 }.then {
422 // force recompose
423 }
424 }
425
Chuck Jazdzewskid0235045922020-07-22 11:14:20 -0700426 @Test // regression test for b/161892016
427 fun testMultipleRecompose() {
428 class A
429
430 var state1 by mutableStateOf(1)
431 var state2 by mutableStateOf(1)
432
433 @Composable fun validate(a: A?) {
434 assertNotNull(a)
435 }
436
437 @Composable fun use(@Suppress("UNUSED_PARAMETER") i: Int) { }
438
439 @Composable fun useA(a: A = A()) {
440 validate(a)
441 use(state2)
442 }
443
444 @Composable fun test() {
445 use(state1)
446 useA()
447 }
448
449 compose {
450 test()
451 }.then {
452 // Recompose test() skipping useA()
453 state1 = 2
454 }.then {
455 // Recompose useA(). In the bug this causes validate() to be passed a null because
456 // the recompose scope is updated with a lambda that captures the parameters when the
457 // default parameter expressions are skipped.
458 state2 = 2
459 }.then {
460 // force recompose to validate a.
461 }
462 }
463
Chuck Jazdzewski7e19aa52019-04-19 12:36:48 -0700464 @Test
Chuck Jazdzewski0a90de92020-05-21 10:03:47 -0700465 @OptIn(ExperimentalComposeApi::class)
Chuck Jazdzewski7e19aa52019-04-19 12:36:48 -0700466 fun testFrameTransition() {
Chuck Jazdzewski0a90de92020-05-21 10:03:47 -0700467 var snapshotId: Int? = null
Chuck Jazdzewski7e19aa52019-04-19 12:36:48 -0700468 compose {
Chuck Jazdzewski0a90de92020-05-21 10:03:47 -0700469 snapshotId = currentSnapshot().id
Leland Richardson76600e32020-01-15 17:22:29 -0800470 }.then {
Chuck Jazdzewski0a90de92020-05-21 10:03:47 -0700471 assertNotSame(snapshotId, currentSnapshot().id)
Chuck Jazdzewski7e19aa52019-04-19 12:36:48 -0700472 }
473 }
Chuck Jazdzewskib15857c2020-05-26 17:14:02 -0700474}
475
476@Composable
477fun Wrapper(children: @Composable () -> Unit) {
478 children()
Chuck Jazdzewski724fffc2018-08-10 13:42:56 -0700479}
480
481fun assertChildHierarchy(root: ViewGroup, getHierarchy: () -> String) {
482 val realHierarchy = printChildHierarchy(root)
483
Chuck Jazdzewskifff2da22019-08-05 10:14:43 -0700484 assertEquals(
Chuck Jazdzewski724fffc2018-08-10 13:42:56 -0700485 normalizeString(getHierarchy()),
486 realHierarchy.trim()
487 )
488}
489
490fun normalizeString(str: String): String {
Chuck Jazdzewski9d160f32019-04-08 09:47:44 -0700491 val lines = str.split('\n').dropWhile { it.isBlank() }.dropLastWhile {
492 it.isBlank()
493 }
Chuck Jazdzewski724fffc2018-08-10 13:42:56 -0700494 if (lines.isEmpty()) return ""
495 val toRemove = lines.first().takeWhile { it == ' ' }.length
496 return lines.joinToString("\n") { it.substring(Math.min(toRemove, it.length)) }
497}
498
499fun printChildHierarchy(root: ViewGroup): String {
500 val sb = StringBuilder()
501 for (i in 0 until root.childCount) {
502 printView(root.getChildAt(i), 0, sb)
503 }
504 return sb.toString()
505}
506
507fun printView(view: View, indent: Int, sb: StringBuilder) {
508 val whitespace = " ".repeat(indent)
509 val name = view.javaClass.simpleName
510 val attributes = printAttributes(view)
511 if (view is ViewGroup && view.childCount > 0) {
Jim Sprocha88c07a2020-06-25 13:00:03 -0700512 sb.appendLine("$whitespace<$name$attributes>")
Chuck Jazdzewski724fffc2018-08-10 13:42:56 -0700513 for (i in 0 until view.childCount) {
514 printView(view.getChildAt(i), indent + 4, sb)
515 }
Jim Sprocha88c07a2020-06-25 13:00:03 -0700516 sb.appendLine("$whitespace</$name>")
Chuck Jazdzewski724fffc2018-08-10 13:42:56 -0700517 } else {
Jim Sprocha88c07a2020-06-25 13:00:03 -0700518 sb.appendLine("$whitespace<$name$attributes />")
Chuck Jazdzewski724fffc2018-08-10 13:42:56 -0700519 }
520}
521
522fun printAttributes(view: View): String {
523 val attrs = mutableListOf<String>()
524
525 // NOTE: right now we only look for id and text as attributes to print out... but we are
526 // free to add more if it makes sense
527 if (view.id != -1) {
528 attrs.add("id=${view.id}")
529 }
530
531 if (view is TextView && view.text.length > 0) {
532 attrs.add("text='${view.text}'")
533 }
534
535 val result = attrs.joinToString(" ", prefix = " ")
536 if (result.length == 1) {
537 return ""
538 }
539 return result
540}
Leland Richardson76600e32020-01-15 17:22:29 -0800541
542class Counter {
543 private var counts = mutableMapOf<String, Int>()
544 fun inc(key: String) = counts.getOrPut(key, { 0 }).let { counts[key] = it + 1 }
545 fun reset() {
546 counts = mutableMapOf()
547 }
548
549 operator fun get(key: String) = counts[key] ?: 0
550}