[go: nahoru, domu]

blob: a522dc83cc899c400d29633aa3f71e569bd42e3e [file] [log] [blame]
Vineet Kumaraaed2552022-08-28 17:53:07 +05301/*
2 * Copyright 2023 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.tv.material3
18
Vighnesh Rautbc816512023-03-15 16:31:27 +053019import androidx.compose.foundation.background
Vineet Kumaraaed2552022-08-28 17:53:07 +053020import androidx.compose.foundation.border
21import androidx.compose.foundation.focusable
22import androidx.compose.foundation.layout.Box
23import androidx.compose.foundation.layout.Row
Aditya Arora0aa705f2023-04-03 20:34:37 +053024import androidx.compose.foundation.layout.fillMaxSize
Vineet Kumaraaed2552022-08-28 17:53:07 +053025import androidx.compose.foundation.layout.fillMaxWidth
26import androidx.compose.foundation.layout.size
27import androidx.compose.foundation.layout.width
28import androidx.compose.foundation.text.BasicText
29import androidx.compose.runtime.CompositionLocalProvider
Vighnesh Rautbc816512023-03-15 16:31:27 +053030import androidx.compose.runtime.getValue
31import androidx.compose.runtime.mutableStateOf
Vineet Kumaraaed2552022-08-28 17:53:07 +053032import androidx.compose.runtime.remember
Vighnesh Rautbc816512023-03-15 16:31:27 +053033import androidx.compose.runtime.setValue
Vineet Kumaraaed2552022-08-28 17:53:07 +053034import androidx.compose.ui.ExperimentalComposeUiApi
35import androidx.compose.ui.Modifier
36import androidx.compose.ui.focus.FocusRequester
37import androidx.compose.ui.focus.focusRequester
Vighnesh Rautbc816512023-03-15 16:31:27 +053038import androidx.compose.ui.focus.onFocusChanged
Vineet Kumaraaed2552022-08-28 17:53:07 +053039import androidx.compose.ui.geometry.Rect
40import androidx.compose.ui.graphics.Color
41import androidx.compose.ui.input.key.Key
42import androidx.compose.ui.platform.LocalLayoutDirection
43import androidx.compose.ui.platform.testTag
44import androidx.compose.ui.semantics.SemanticsNode
45import androidx.compose.ui.test.ExperimentalTestApi
46import androidx.compose.ui.test.SemanticsNodeInteraction
47import androidx.compose.ui.test.SemanticsNodeInteractionCollection
48import androidx.compose.ui.test.assertIsDisplayed
49import androidx.compose.ui.test.assertIsEqualTo
Vighnesh Rautbc816512023-03-15 16:31:27 +053050import androidx.compose.ui.test.assertIsFocused
Aditya Arora0aa705f2023-04-03 20:34:37 +053051import androidx.compose.ui.test.assertIsNotFocused
Vineet Kumaraaed2552022-08-28 17:53:07 +053052import androidx.compose.ui.test.assertWidthIsEqualTo
53import androidx.compose.ui.test.getUnclippedBoundsInRoot
54import androidx.compose.ui.test.junit4.createComposeRule
55import androidx.compose.ui.test.onAllNodesWithText
56import androidx.compose.ui.test.onNodeWithTag
57import androidx.compose.ui.test.onRoot
58import androidx.compose.ui.test.performKeyInput
59import androidx.compose.ui.test.pressKey
60import androidx.compose.ui.unit.Dp
61import androidx.compose.ui.unit.DpRect
62import androidx.compose.ui.unit.LayoutDirection
63import androidx.compose.ui.unit.dp
64import androidx.compose.ui.unit.toSize
65import androidx.test.platform.app.InstrumentationRegistry
66import org.junit.Rule
67import org.junit.Test
68
69@OptIn(ExperimentalTvMaterial3Api::class)
70class ModalNavigationDrawerTest {
71 @get:Rule
72 val rule = createComposeRule()
73
74 @Test
75 fun modalNavigationDrawer_initialStateClosed_closedStateComposableDisplayed() {
76 rule.setContent {
77 ModalNavigationDrawer(
78 drawerState = remember { DrawerState(DrawerValue.Closed) },
79 drawerContent = {
80 BasicText(text = if (it == DrawerValue.Open) "Opened" else "Closed")
81 }
82 ) { Box(Modifier.size(200.dp)) }
83 }
84
85 rule.onAllNodesWithText("Closed").assertAnyAreDisplayed()
86 }
87
88 @Test
89 fun modalNavigationDrawer_initialStateOpen_openStateComposableDisplayed() {
90 rule.setContent {
91 ModalNavigationDrawer(
92 drawerState = remember { DrawerState(DrawerValue.Open) },
93 drawerContent = {
94 BasicText(text = if (it == DrawerValue.Open) "Opened" else "Closed")
95 }) { BasicText("other content") }
96 }
97
98 rule.onAllNodesWithText("Opened").assertAnyAreDisplayed()
99 }
100
101 @Test
102 fun modalNavigationDrawer_focusInsideDrawer_openedStateComposableDisplayed() {
103 InstrumentationRegistry.getInstrumentation().setInTouchMode(false)
104 val drawerFocusRequester = FocusRequester()
105 rule.setContent {
106 val navigationDrawerValue = remember { DrawerState(DrawerValue.Closed) }
107 ModalNavigationDrawer(
108 modifier = Modifier.focusRequester(drawerFocusRequester),
109 drawerState = navigationDrawerValue,
110 drawerContent = {
111 BasicText(
vinekumard654f4d2023-04-12 18:52:45 +0530112 modifier = Modifier.focusable(),
113 text = if (it == DrawerValue.Open) "Opened" else "Closed"
Vineet Kumaraaed2552022-08-28 17:53:07 +0530114 )
115 }) { BasicText("other content") }
116 }
117
118 rule.onAllNodesWithText("Closed").assertAnyAreDisplayed()
119
120 rule.runOnIdle {
121 drawerFocusRequester.requestFocus()
122 }
123
124 rule.onAllNodesWithText("Opened").assertAnyAreDisplayed()
125 }
126
127 @OptIn(ExperimentalTestApi::class, ExperimentalComposeUiApi::class)
128 @Test
129 fun modalNavigationDrawer_focusMovesOutOfDrawer_closedStateComposableDisplayed() {
130 InstrumentationRegistry.getInstrumentation().setInTouchMode(false)
131 val drawerFocusRequester = FocusRequester()
132 rule.setContent {
133 val navigationDrawerValue = remember { DrawerState(DrawerValue.Closed) }
134 Row {
135 ModalNavigationDrawer(
Vighnesh Rautbc816512023-03-15 16:31:27 +0530136 modifier = Modifier
137 .focusRequester(drawerFocusRequester)
138 .focusable(false),
Vineet Kumaraaed2552022-08-28 17:53:07 +0530139 drawerState = navigationDrawerValue,
140 drawerContent = {
vinekumard654f4d2023-04-12 18:52:45 +0530141 BasicText(
142 modifier = Modifier.focusable(),
143 text = if (it == DrawerValue.Open) "Opened" else "Closed"
144 )
Vineet Kumaraaed2552022-08-28 17:53:07 +0530145 }) {
146 Box(modifier = Modifier.focusable()) {
147 BasicText("Button")
148 }
149 }
150 }
151 }
152 rule.runOnIdle {
153 drawerFocusRequester.requestFocus()
154 }
155 rule.onAllNodesWithText("Opened").assertAnyAreDisplayed()
156 rule.onRoot().performKeyInput { pressKey(Key.DirectionRight) }
157 rule.onAllNodesWithText("Closed").assertAnyAreDisplayed()
158 }
159
vinekumard654f4d2023-04-12 18:52:45 +0530160 @OptIn(ExperimentalTestApi::class)
Vineet Kumaraaed2552022-08-28 17:53:07 +0530161 @Test
162 fun modalNavigationDrawer_focusMovesIntoDrawer_openStateComposableDisplayed() {
163 InstrumentationRegistry.getInstrumentation().setInTouchMode(false)
164 val buttonFocusRequester = FocusRequester()
165 rule.setContent {
166 val navigationDrawerValue = remember { DrawerState(DrawerValue.Closed) }
167 Row {
168 ModalNavigationDrawer(
169 drawerState = navigationDrawerValue,
170 drawerContent = {
Vighnesh Rautbc816512023-03-15 16:31:27 +0530171 var isFocused by remember { mutableStateOf(false) }
172 BasicText(
173 text = if (it == DrawerValue.Open) "Opened" else "Closed",
174 modifier = Modifier
175 .onFocusChanged { focusState ->
176 isFocused = focusState.isFocused
177 }
178 .background(if (isFocused) Color.Green else Color.Yellow)
179 .focusable()
180 .testTag("drawerItem")
181 )
Vineet Kumaraaed2552022-08-28 17:53:07 +0530182 }) {
183 Box(
184 modifier = Modifier
185 .focusRequester(buttonFocusRequester)
186 .focusable()
187 ) {
188 BasicText("Button")
189 }
190 }
191 }
192 }
193 rule.runOnIdle {
194 buttonFocusRequester.requestFocus()
195 }
196 rule.onAllNodesWithText("Closed").assertAnyAreDisplayed()
197 rule.onRoot().performKeyInput { pressKey(Key.DirectionLeft) }
Vighnesh Rautbc816512023-03-15 16:31:27 +0530198 rule.waitForIdle()
Vineet Kumaraaed2552022-08-28 17:53:07 +0530199 rule.onAllNodesWithText("Opened").assertAnyAreDisplayed()
Vighnesh Rautbc816512023-03-15 16:31:27 +0530200 rule.onNodeWithTag("drawerItem").assertIsFocused()
Vineet Kumaraaed2552022-08-28 17:53:07 +0530201 }
202
203 @Test
204 fun modalNavigationDrawer_closedState_widthOfDrawerIsWidthOfContent() {
205 val contentWidthBoxTag = "contentWidthBox"
206 val totalWidth = 100.dp
207 val closedDrawerContentWidth = 30.dp
208 val expectedContentWidth = totalWidth - closedDrawerContentWidth
209 rule.setContent {
210 Box(modifier = Modifier.width(totalWidth)) {
211 NavigationDrawer(
212 drawerState = remember { DrawerState(DrawerValue.Closed) },
213 drawerContent = {
214 Box(Modifier.width(closedDrawerContentWidth)) {
215 // extra long content wrapped in a drawer-width restricting box
216 Box(Modifier.width(closedDrawerContentWidth * 10))
217 }
218 }
Vighnesh Rautbc816512023-03-15 16:31:27 +0530219 ) { Box(
220 Modifier
221 .fillMaxWidth()
222 .testTag(contentWidthBoxTag)) }
Vineet Kumaraaed2552022-08-28 17:53:07 +0530223 }
224 }
225
226 rule.onNodeWithTag(contentWidthBoxTag).assertWidthIsEqualTo(expectedContentWidth)
227 }
228
229 @Test
230 fun modalNavigationDrawer_openState_widthOfDrawerIsWidthOfContent() {
231 val contentWidthBoxTag = "contentWidthBox"
232 val totalWidth = 100.dp
233 val openDrawerContentWidth = 70.dp
234 val expectedContentWidth = totalWidth - openDrawerContentWidth
235 rule.setContent {
236 Box(modifier = Modifier.width(totalWidth)) {
237 NavigationDrawer(
238 drawerState = remember { DrawerState(DrawerValue.Closed) },
239 drawerContent = {
240 Box(Modifier.width(openDrawerContentWidth)) {
241 Box(Modifier.width(openDrawerContentWidth * 10))
242 }
243 }
Vighnesh Rautbc816512023-03-15 16:31:27 +0530244 ) { Box(
245 Modifier
246 .fillMaxWidth()
247 .testTag(contentWidthBoxTag)) }
Vineet Kumaraaed2552022-08-28 17:53:07 +0530248 }
249 }
250
251 rule.onNodeWithTag(contentWidthBoxTag).assertWidthIsEqualTo(expectedContentWidth)
252 }
253
254 @Test
255 fun modalNavigationDrawer_rtl_drawerIsDrawnAtTheStart() {
256 val contentWidthBoxTag = "contentWidthBox"
257 val drawerContentBoxTag = "drawerContentBox"
258 rule.setContent {
259 CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
260 ModalNavigationDrawer(
261 drawerState = remember { DrawerState(DrawerValue.Closed) },
262 drawerContent = {
Vighnesh Rautbc816512023-03-15 16:31:27 +0530263 Box(
264 Modifier
265 .testTag(drawerContentBoxTag)
266 .border(2.dp, Color.Red)) {
Vineet Kumaraaed2552022-08-28 17:53:07 +0530267 BasicText(text = if (it == DrawerValue.Open) "Opened" else "Closed")
268 }
269 }
Vighnesh Rautbc816512023-03-15 16:31:27 +0530270 ) { Box(
271 Modifier
272 .fillMaxWidth()
273 .testTag(contentWidthBoxTag)) }
Vineet Kumaraaed2552022-08-28 17:53:07 +0530274 }
275 }
276
277 val rightEdgeOfRoot = rule.onRoot().getUnclippedBoundsInRoot().right
278 rule.onNodeWithTag(drawerContentBoxTag).assertRightPositionInRootIsEqualTo(rightEdgeOfRoot)
279 }
280
281 @Test
282 fun modalNavigationDrawer_rtl_drawerExpandsTowardsEnd() {
283 val contentWidthBoxTag = "contentWidthBox"
284 val drawerContentBoxTag = "drawerContentBox"
285 var drawerState: DrawerState? = null
286 rule.setContent {
287 CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
288 drawerState = remember { DrawerState(DrawerValue.Closed) }
289 ModalNavigationDrawer(
290 drawerState = drawerState!!,
291 drawerContent = {
Vighnesh Rautbc816512023-03-15 16:31:27 +0530292 Box(
293 Modifier
294 .testTag(drawerContentBoxTag)
295 .border(2.dp, Color.Red)) {
Vineet Kumaraaed2552022-08-28 17:53:07 +0530296 BasicText(text = if (it == DrawerValue.Open) "Opened" else "Closed")
297 }
298 }
Vighnesh Rautbc816512023-03-15 16:31:27 +0530299 ) { Box(
300 Modifier
301 .fillMaxWidth()
302 .testTag(contentWidthBoxTag)) }
Vineet Kumaraaed2552022-08-28 17:53:07 +0530303 }
304 }
305
306 val endPositionInClosedState =
307 rule.onNodeWithTag(drawerContentBoxTag).getUnclippedBoundsInRoot().left
308
309 rule.runOnIdle { drawerState?.setValue(DrawerValue.Open) }
310 val endPositionInOpenState =
311 rule.onNodeWithTag(drawerContentBoxTag).getUnclippedBoundsInRoot().left
312
313 assert(endPositionInClosedState.value > endPositionInOpenState.value)
314 }
315
Aditya Arora0aa705f2023-04-03 20:34:37 +0530316 @OptIn(ExperimentalTestApi::class, ExperimentalComposeUiApi::class)
317 @Test
318 fun modalNavigationDrawer_parentContainerGainsFocus_onBackPress() {
319 val drawerFocusRequester = FocusRequester()
320 rule.setContent {
321 Box(
322 modifier = Modifier
323 .testTag("box-container")
324 .fillMaxSize()
325 .focusable()
326 ) {
327 ModalNavigationDrawer(
328 modifier = Modifier.focusRequester(drawerFocusRequester),
329 drawerState = remember { DrawerState(DrawerValue.Closed) },
330 drawerContent = {
331 BasicText(
332 text = if (it == DrawerValue.Open) "Opened" else "Closed",
333 modifier = Modifier.focusable()
334 )
335 }
336 ) {
337 BasicText("other content")
338 }
339 }
340 }
341
342 rule.onAllNodesWithText("Closed").assertAnyAreDisplayed()
343
344 rule.runOnIdle {
345 drawerFocusRequester.requestFocus()
346 }
347
348 rule.onAllNodesWithText("Opened").assertAnyAreDisplayed()
349 rule.onNodeWithTag("box-container").assertIsNotFocused()
350
351 // Trigger back press
352 rule.onRoot().performKeyInput { pressKey(Key.Back) }
353 rule.waitForIdle()
354
355 // Check if the parent container gains focus
356 rule.onNodeWithTag("box-container").assertIsFocused()
357 }
358
Vineet Kumaraaed2552022-08-28 17:53:07 +0530359 private fun SemanticsNodeInteractionCollection.assertAnyAreDisplayed() {
360 val result = (0 until fetchSemanticsNodes().size).map { get(it) }.any {
361 try {
362 it.assertIsDisplayed()
363 true
364 } catch (e: AssertionError) {
365 false
366 }
367 }
368
369 if (!result) throw AssertionError("Assert failed: None of the components are displayed!")
370 }
371
372 private fun SemanticsNodeInteraction.assertRightPositionInRootIsEqualTo(
373 expectedRight: Dp
374 ): SemanticsNodeInteraction {
375 return withUnclippedBoundsInRoot {
376 it.right.assertIsEqualTo(expectedRight, "right")
377 }
378 }
379
380 private fun SemanticsNodeInteraction.withUnclippedBoundsInRoot(
381 assertion: (DpRect) -> Unit
382 ): SemanticsNodeInteraction {
383 val node = fetchSemanticsNode("Failed to retrieve bounds of the node.")
384 val bounds = with(node.layoutInfo.density) {
385 node.unclippedBoundsInRoot.let {
386 DpRect(it.left.toDp(), it.top.toDp(), it.right.toDp(), it.bottom.toDp())
387 }
388 }
389 assertion.invoke(bounds)
390 return this
391 }
392
393 private val SemanticsNode.unclippedBoundsInRoot: Rect
394 get() {
395 return if (layoutInfo.isPlaced) {
396 Rect(positionInRoot, size.toSize())
397 } else {
398 Dp.Unspecified.value.let { Rect(it, it, it, it) }
399 }
400 }
401}