1 /*
<lambda>null2  * Copyright 2021 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  *      https://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 
17 @file:Suppress("DEPRECATION")
18 
19 package com.google.accompanist.insets
20 
21 import android.os.Build
22 import androidx.compose.foundation.background
23 import androidx.compose.foundation.layout.Box
24 import androidx.compose.foundation.layout.PaddingValues
25 import androidx.compose.foundation.layout.fillMaxSize
26 import androidx.compose.foundation.layout.fillMaxWidth
27 import androidx.compose.foundation.layout.height
28 import androidx.compose.foundation.layout.requiredSize
29 import androidx.compose.foundation.layout.sizeIn
30 import androidx.compose.material.BottomAppBar
31 import androidx.compose.material.FabPosition
32 import androidx.compose.material.FloatingActionButton
33 import androidx.compose.material.Icon
34 import androidx.compose.material.MaterialTheme
35 import androidx.compose.material.ScaffoldState
36 import androidx.compose.material.Surface
37 import androidx.compose.material.Text
38 import androidx.compose.material.icons.Icons
39 import androidx.compose.material.icons.filled.Favorite
40 import androidx.compose.material.rememberScaffoldState
41 import androidx.compose.runtime.Composable
42 import androidx.compose.runtime.mutableStateOf
43 import androidx.compose.ui.Modifier
44 import androidx.compose.ui.draw.shadow
45 import androidx.compose.ui.geometry.Offset
46 import androidx.compose.ui.graphics.Color
47 import androidx.compose.ui.graphics.asAndroidBitmap
48 import androidx.compose.ui.layout.LayoutCoordinates
49 import androidx.compose.ui.layout.onGloballyPositioned
50 import androidx.compose.ui.layout.positionInParent
51 import androidx.compose.ui.layout.positionInRoot
52 import androidx.compose.ui.platform.testTag
53 import androidx.compose.ui.semantics.semantics
54 import androidx.compose.ui.test.SemanticsNodeInteraction
55 import androidx.compose.ui.test.assertHeightIsEqualTo
56 import androidx.compose.ui.test.assertWidthIsEqualTo
57 import androidx.compose.ui.test.captureToImage
58 import androidx.compose.ui.test.junit4.ComposeContentTestRule
59 import androidx.compose.ui.test.junit4.createComposeRule
60 import androidx.compose.ui.test.onNodeWithTag
61 import androidx.compose.ui.test.performTouchInput
62 import androidx.compose.ui.test.swipeLeft
63 import androidx.compose.ui.test.swipeRight
64 import androidx.compose.ui.unit.Dp
65 import androidx.compose.ui.unit.IntSize
66 import androidx.compose.ui.unit.dp
67 import androidx.compose.ui.unit.toSize
68 import androidx.compose.ui.zIndex
69 import androidx.test.ext.junit.runners.AndroidJUnit4
70 import androidx.test.filters.SdkSuppress
71 import com.google.accompanist.insets.ui.FabPlacement
72 import com.google.accompanist.insets.ui.LocalFabPlacement
73 import com.google.accompanist.insets.ui.Scaffold
74 import com.google.accompanist.internal.test.IgnoreOnRobolectric
75 import com.google.common.truth.Truth.assertThat
76 import kotlinx.coroutines.runBlocking
77 import org.junit.Ignore
78 import org.junit.Rule
79 import org.junit.Test
80 import org.junit.experimental.categories.Category
81 import org.junit.runner.RunWith
82 
83 @RunWith(AndroidJUnit4::class)
84 class ScaffoldTest {
85 
86     @get:Rule
87     val rule = createComposeRule()
88 
89     private val scaffoldTag = "Scaffold"
90 
91     @Test
92     fun scaffold_onlyContent_takesWholeScreen() {
93         rule.setMaterialContentForSizeAssertions(
94             parentMaxWidth = 100.dp,
95             parentMaxHeight = 100.dp
96         ) {
97             Scaffold {
98                 Text("Scaffold body")
99             }
100         }
101             .assertWidthIsEqualTo(100.dp)
102             .assertHeightIsEqualTo(100.dp)
103     }
104 
105     @Test
106     fun scaffold_onlyContent_stackSlot() {
107         var child1: Offset = Offset.Zero
108         var child2: Offset = Offset.Zero
109         rule.setMaterialContent {
110             Scaffold {
111                 Text(
112                     "One",
113                     Modifier.onGloballyPositioned { child1 = it.positionInParent() }
114                 )
115                 Text(
116                     "Two",
117                     Modifier.onGloballyPositioned { child2 = it.positionInParent() }
118                 )
119             }
120         }
121         assertThat(child1.y).isEqualTo(child2.y)
122         assertThat(child1.x).isEqualTo(child2.x)
123     }
124 
125     @Test
126     fun scaffold_AppbarAndContent_inStack() {
127         var appbarPosition: Offset = Offset.Zero
128         var appbarSize: IntSize = IntSize.Zero
129         var contentPosition: Offset = Offset.Zero
130         var contentPadding = PaddingValues(0.dp)
131 
132         rule.setMaterialContent {
133             Scaffold(
134                 topBar = {
135                     Box(
136                         Modifier
137                             .fillMaxWidth()
138                             .height(50.dp)
139                             .background(color = Color.Red)
140                             .onGloballyPositioned { positioned: LayoutCoordinates ->
141                                 appbarPosition = positioned.localToRoot(Offset.Zero)
142                                 appbarSize = positioned.size
143                             }
144                     )
145                 }
146             ) { paddingValues ->
147                 contentPadding = paddingValues
148 
149                 Box(
150                     Modifier
151                         .fillMaxWidth()
152                         .height(50.dp)
153                         .background(Color.Blue)
154                         .onGloballyPositioned { positioned ->
155                             contentPosition = positioned.localToRoot(Offset.Zero)
156                         }
157                 )
158             }
159         }
160 
161         assertThat(appbarPosition.y).isWithin(0.1f).of(0f)
162         assertThat(appbarPosition.y).isEqualTo(contentPosition.y)
163         assertThat(with(rule.density) { contentPadding.calculateTopPadding().roundToPx() })
164             .isEqualTo(appbarSize.height)
165     }
166 
167     @Test
168     fun scaffold_bottomBarAndContent_inStack() {
169         var appbarPosition: Offset = Offset.Zero
170         var appbarSize: IntSize = IntSize.Zero
171         var contentPosition: Offset = Offset.Zero
172         var contentSize: IntSize = IntSize.Zero
173         var contentPadding = PaddingValues(0.dp)
174 
175         rule.setMaterialContent {
176             Scaffold(
177                 bottomBar = {
178                     Box(
179                         Modifier
180                             .fillMaxWidth()
181                             .height(50.dp)
182                             .background(color = Color.Red)
183                             .onGloballyPositioned { positioned: LayoutCoordinates ->
184                                 appbarPosition = positioned.positionInParent()
185                                 appbarSize = positioned.size
186                             }
187                     )
188                 }
189             ) { paddingValues ->
190                 contentPadding = paddingValues
191 
192                 Box(
193                     Modifier
194                         .fillMaxSize()
195                         .height(50.dp)
196                         .background(color = Color.Blue)
197                         .onGloballyPositioned { positioned: LayoutCoordinates ->
198                             contentPosition = positioned.positionInParent()
199                             contentSize = positioned.size
200                         }
201                 )
202             }
203         }
204 
205         val appBarBottom = appbarPosition.y + appbarSize.height
206         val contentBottom = contentPosition.y + contentSize.height
207 
208         assertThat(appBarBottom).isEqualTo(contentBottom)
209         assertThat(with(rule.density) { contentPadding.calculateBottomPadding().roundToPx() })
210             .isEqualTo(appbarSize.height)
211     }
212 
213     @Test
214     @Ignore("unignore once animation sync is ready (b/147291885)")
215     fun scaffold_drawer_gestures() {
216         var drawerChildPosition: Offset = Offset.Zero
217         val drawerGesturedEnabledState = mutableStateOf(false)
218         rule.setContent {
219             Box(Modifier.testTag(scaffoldTag)) {
220                 Scaffold(
221                     drawerContent = {
222                         Box(
223                             Modifier
224                                 .fillMaxWidth()
225                                 .height(50.dp)
226                                 .background(color = Color.Blue)
227                                 .onGloballyPositioned { positioned: LayoutCoordinates ->
228                                     drawerChildPosition = positioned.positionInParent()
229                                 }
230                         )
231                     },
232                     drawerGesturesEnabled = drawerGesturedEnabledState.value
233                 ) {
234                     Box(
235                         Modifier
236                             .fillMaxWidth()
237                             .height(50.dp)
238                             .background(color = Color.Blue)
239                     )
240                 }
241             }
242         }
243         assertThat(drawerChildPosition.x).isLessThan(0f)
244         rule.onNodeWithTag(scaffoldTag).performTouchInput {
245             swipeRight()
246         }
247         assertThat(drawerChildPosition.x).isLessThan(0f)
248         rule.onNodeWithTag(scaffoldTag).performTouchInput {
249             swipeLeft()
250         }
251         assertThat(drawerChildPosition.x).isLessThan(0f)
252 
253         rule.runOnUiThread {
254             drawerGesturedEnabledState.value = true
255         }
256 
257         rule.onNodeWithTag(scaffoldTag).performTouchInput {
258             swipeRight()
259         }
260         assertThat(drawerChildPosition.x).isEqualTo(0f)
261         rule.onNodeWithTag(scaffoldTag).performTouchInput {
262             swipeLeft()
263         }
264         assertThat(drawerChildPosition.x).isLessThan(0f)
265     }
266 
267     @Test
268     @Ignore("unignore once animation sync is ready (b/147291885)")
269     fun scaffold_drawer_manualControl(): Unit = runBlocking {
270         var drawerChildPosition: Offset = Offset.Zero
271         lateinit var scaffoldState: ScaffoldState
272         rule.setContent {
273             scaffoldState = rememberScaffoldState()
274             Box(Modifier.testTag(scaffoldTag)) {
275                 Scaffold(
276                     scaffoldState = scaffoldState,
277                     drawerContent = {
278                         Box(
279                             Modifier
280                                 .fillMaxWidth()
281                                 .height(50.dp)
282                                 .background(color = Color.Blue)
283                                 .onGloballyPositioned { positioned: LayoutCoordinates ->
284                                     drawerChildPosition = positioned.positionInParent()
285                                 }
286                         )
287                     }
288                 ) {
289                     Box(
290                         Modifier
291                             .fillMaxWidth()
292                             .height(50.dp)
293                             .background(color = Color.Blue)
294                     )
295                 }
296             }
297         }
298         assertThat(drawerChildPosition.x).isLessThan(0f)
299         scaffoldState.drawerState.open()
300         assertThat(drawerChildPosition.x).isLessThan(0f)
301         scaffoldState.drawerState.close()
302         assertThat(drawerChildPosition.x).isLessThan(0f)
303     }
304 
305     @Test
306     fun scaffold_centerDockedFab_position() {
307         var fabPosition: Offset = Offset.Zero
308         var fabSize: IntSize = IntSize.Zero
309         var bottomBarPosition: Offset = Offset.Zero
310         rule.setContent {
311             Scaffold(
312                 floatingActionButton = {
313                     FloatingActionButton(
314                         modifier = Modifier.onGloballyPositioned { positioned ->
315                             fabSize = positioned.size
316                             fabPosition = positioned.positionInRoot()
317                         },
318                         onClick = {}
319                     ) {
320                         Icon(Icons.Filled.Favorite, null)
321                     }
322                 },
323                 floatingActionButtonPosition = FabPosition.Center,
324                 isFloatingActionButtonDocked = true,
325                 bottomBar = {
326                     BottomAppBar(
327                         Modifier
328                             .onGloballyPositioned { positioned: LayoutCoordinates ->
329                                 bottomBarPosition = positioned.positionInRoot()
330                             }
331                     ) {}
332                 }
333             ) {
334                 Text("body")
335             }
336         }
337         val expectedFabY = bottomBarPosition.y - (fabSize.height / 2)
338         assertThat(fabPosition.y).isEqualTo(expectedFabY)
339     }
340 
341     @Test
342     fun scaffold_fab_position_nestedScaffold() {
343         var fabPosition: Offset = Offset.Zero
344         var fabSize: IntSize = IntSize.Zero
345         var bottomBarPosition: Offset = Offset.Zero
346         rule.setContent {
347             Scaffold(
348                 bottomBar = {
349                     BottomAppBar(
350                         Modifier
351                             .onGloballyPositioned { positioned: LayoutCoordinates ->
352                                 bottomBarPosition = positioned.positionInRoot()
353                             }
354                     ) {}
355                 }
356             ) {
357                 Scaffold(
358                     floatingActionButton = {
359                         FloatingActionButton(
360                             modifier = Modifier.onGloballyPositioned { positioned ->
361                                 fabSize = positioned.size
362                                 fabPosition = positioned.positionInRoot()
363                             },
364                             onClick = {}
365                         ) {
366                             Icon(Icons.Filled.Favorite, null)
367                         }
368                     },
369                 ) {
370                     Text("body")
371                 }
372             }
373         }
374 
375         val expectedFabY = bottomBarPosition.y - fabSize.height -
376             with(rule.density) { 16.dp.roundToPx() }
377         assertThat(fabPosition.y).isEqualTo(expectedFabY)
378     }
379 
380     @Test
381     fun scaffold_endDockedFab_position() {
382         var fabPosition: Offset = Offset.Zero
383         var fabSize: IntSize = IntSize.Zero
384         var bottomBarPosition: Offset = Offset.Zero
385         rule.setContent {
386             Scaffold(
387                 floatingActionButton = {
388                     FloatingActionButton(
389                         modifier = Modifier.onGloballyPositioned { positioned ->
390                             fabSize = positioned.size
391                             fabPosition = positioned.positionInRoot()
392                         },
393                         onClick = {}
394                     ) {
395                         Icon(Icons.Filled.Favorite, null)
396                     }
397                 },
398                 floatingActionButtonPosition = FabPosition.End,
399                 isFloatingActionButtonDocked = true,
400                 bottomBar = {
401                     BottomAppBar(
402                         Modifier
403                             .onGloballyPositioned { positioned: LayoutCoordinates ->
404                                 bottomBarPosition = positioned.positionInRoot()
405                             }
406                     ) {}
407                 }
408             ) {
409                 Text("body")
410             }
411         }
412         val expectedFabY = bottomBarPosition.y - (fabSize.height / 2)
413         assertThat(fabPosition.y).isEqualTo(expectedFabY)
414     }
415 
416     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
417     @Category(IgnoreOnRobolectric::class)
418     @Test
419     fun scaffold_topAppBarIsDrawnOnTopOfContent() {
420         rule.setContent {
421             Box(
422                 Modifier
423                     .requiredSize(10.dp, 20.dp)
424                     .semantics(mergeDescendants = true) {}
425                     .testTag("Scaffold")
426             ) {
427                 Scaffold(
428                     topBar = {
429                         Box(
430                             Modifier
431                                 .requiredSize(10.dp)
432                                 .shadow(4.dp)
433                                 .zIndex(4f)
434                                 .background(color = Color.White)
435                         )
436                     }
437                 ) {
438                     Box(
439                         Modifier
440                             .requiredSize(10.dp)
441                             .background(color = Color.White)
442                     )
443                 }
444             }
445         }
446 
447         rule.onNodeWithTag("Scaffold")
448             .captureToImage()
449             .asAndroidBitmap()
450             .apply {
451                 // asserts the appbar(top half part) has the shadow
452                 val yPos = height / 2 + 2
453                 assertThat(Color(getPixel(0, yPos))).isNotEqualTo(Color.White)
454                 assertThat(Color(getPixel(width / 2, yPos))).isNotEqualTo(Color.White)
455                 assertThat(Color(getPixel(width - 1, yPos))).isNotEqualTo(Color.White)
456             }
457     }
458 
459     @Test
460     fun scaffold_geometry_fabSize() {
461         var fabSize: IntSize = IntSize.Zero
462         val showFab = mutableStateOf(true)
463         var fabPlacement: FabPlacement? = null
464         rule.setContent {
465             val fab = @Composable {
466                 if (showFab.value) {
467                     FloatingActionButton(
468                         modifier = Modifier.onGloballyPositioned { positioned ->
469                             fabSize = positioned.size
470                         },
471                         onClick = {}
472                     ) {
473                         Icon(Icons.Filled.Favorite, null)
474                     }
475                 }
476             }
477             Scaffold(
478                 floatingActionButton = fab,
479                 floatingActionButtonPosition = FabPosition.End,
480                 bottomBar = {
481                     fabPlacement = LocalFabPlacement.current
482                 }
483             ) {
484                 Text("body")
485             }
486         }
487         rule.runOnIdle {
488             assertThat(fabPlacement?.width).isEqualTo(fabSize.width)
489             assertThat(fabPlacement?.height).isEqualTo(fabSize.height)
490             showFab.value = false
491         }
492 
493         rule.runOnIdle {
494             assertThat(fabPlacement).isEqualTo(null)
495             assertThat(fabPlacement).isEqualTo(null)
496         }
497     }
498 
499     @Test
500     fun scaffold_innerPadding_lambdaParam() {
501         var bottomBarSize: IntSize = IntSize.Zero
502         lateinit var innerPadding: PaddingValues
503 
504         lateinit var scaffoldState: ScaffoldState
505         rule.setContent {
506             scaffoldState = rememberScaffoldState()
507             Scaffold(
508                 scaffoldState = scaffoldState,
509                 bottomBar = {
510                     Box(
511                         Modifier
512                             .fillMaxWidth()
513                             .height(100.dp)
514                             .background(color = Color.Red)
515                             .onGloballyPositioned { positioned: LayoutCoordinates ->
516                                 bottomBarSize = positioned.size
517                             }
518                     )
519                 }
520             ) {
521                 innerPadding = it
522                 Text("body")
523             }
524         }
525         rule.runOnIdle {
526             with(rule.density) {
527                 assertThat(innerPadding.calculateBottomPadding())
528                     .isEqualTo(bottomBarSize.toSize().height.toDp())
529             }
530         }
531     }
532 }
533 
setMaterialContentnull534 private fun ComposeContentTestRule.setMaterialContent(
535     modifier: Modifier = Modifier,
536     composable: @Composable () -> Unit
537 ) {
538     setContent {
539         MaterialTheme {
540             Surface(modifier = modifier, content = composable)
541         }
542     }
543 }
544 
545 /**
546  * Constant to emulate very big but finite constraints
547  */
548 private val BigTestMaxWidth = 5000.dp
549 private val BigTestMaxHeight = 5000.dp
550 
ComposeContentTestRulenull551 private fun ComposeContentTestRule.setMaterialContentForSizeAssertions(
552     parentMaxWidth: Dp = BigTestMaxWidth,
553     parentMaxHeight: Dp = BigTestMaxHeight,
554     // TODO : figure out better way to make it flexible
555     content: @Composable () -> Unit
556 ): SemanticsNodeInteraction {
557     setContent {
558         MaterialTheme {
559             Surface {
560                 Box {
561                     Box(
562                         Modifier
563                             .sizeIn(
564                                 maxWidth = parentMaxWidth,
565                                 maxHeight = parentMaxHeight
566                             )
567                             .testTag("containerForSizeAssertion")
568                     ) {
569                         content()
570                     }
571                 }
572             }
573         }
574     }
575 
576     return onNodeWithTag("containerForSizeAssertion")
577 }
578