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