blob: 198e493388cc0c07ccf73597b546d7f87e772837 [file] [log] [blame]
Vighnesh Rautcb8c6312022-10-06 13:08:59 +05301/*
2 * Copyright 2022 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
Vineet Kumar507211d2023-01-04 19:16:38 +053017package androidx.tv.material3
Vighnesh Rautcb8c6312022-10-06 13:08:59 +053018
Vighnesh Raut7e0b7c12023-03-10 16:20:44 +053019import android.os.Build
Vighnesh Rautcb8c6312022-10-06 13:08:59 +053020import androidx.compose.foundation.background
21import androidx.compose.foundation.border
22import androidx.compose.foundation.focusable
23import androidx.compose.foundation.layout.Box
24import androidx.compose.foundation.layout.Column
25import androidx.compose.foundation.layout.Spacer
26import androidx.compose.foundation.layout.fillMaxSize
27import androidx.compose.foundation.layout.height
28import androidx.compose.foundation.layout.size
29import androidx.compose.foundation.layout.width
30import androidx.compose.foundation.shape.RoundedCornerShape
Vighnesh Raut7ff66b42022-11-29 19:45:56 +053031import androidx.compose.foundation.text.BasicText
32import androidx.compose.runtime.Composable
Vighnesh Rautcb8c6312022-10-06 13:08:59 +053033import androidx.compose.runtime.LaunchedEffect
34import androidx.compose.runtime.getValue
35import androidx.compose.runtime.mutableStateOf
36import androidx.compose.runtime.remember
37import androidx.compose.runtime.setValue
Vighnesh Raut7e0b7c12023-03-10 16:20:44 +053038import androidx.compose.testutils.assertAgainstGolden
Vighnesh Rautcb8c6312022-10-06 13:08:59 +053039import androidx.compose.ui.Modifier
40import androidx.compose.ui.focus.FocusRequester
41import androidx.compose.ui.focus.focusRequester
42import androidx.compose.ui.graphics.Color
43import androidx.compose.ui.input.key.NativeKeyEvent
44import androidx.compose.ui.platform.testTag
Vighnesh Raut7ff66b42022-11-29 19:45:56 +053045import androidx.compose.ui.test.assertIsDisplayed
Vighnesh Rautcb8c6312022-10-06 13:08:59 +053046import androidx.compose.ui.test.assertIsFocused
Vighnesh Raut7e0b7c12023-03-10 16:20:44 +053047import androidx.compose.ui.test.captureToImage
Vighnesh Rautcb8c6312022-10-06 13:08:59 +053048import androidx.compose.ui.test.junit4.createComposeRule
49import androidx.compose.ui.test.onNodeWithTag
Vighnesh Raut7ff66b42022-11-29 19:45:56 +053050import androidx.compose.ui.test.onNodeWithText
51import androidx.compose.ui.unit.DpRect
Vighnesh Rautcb8c6312022-10-06 13:08:59 +053052import androidx.compose.ui.unit.dp
Vighnesh Raut7e0b7c12023-03-10 16:20:44 +053053import androidx.test.filters.SdkSuppress
Vighnesh Rautcb8c6312022-10-06 13:08:59 +053054import androidx.test.platform.app.InstrumentationRegistry
Vighnesh Raut7e0b7c12023-03-10 16:20:44 +053055import androidx.test.screenshot.AndroidXScreenshotTestRule
Vighnesh Rautcb8c6312022-10-06 13:08:59 +053056import org.junit.Rule
57import org.junit.Test
58
59class TabRowTest {
Vighnesh Rautcb8c6312022-10-06 13:08:59 +053060 @get:Rule
61 val rule = createComposeRule()
62
Vighnesh Raut7e0b7c12023-03-10 16:20:44 +053063 @get:Rule
64 val screenshotRule = AndroidXScreenshotTestRule(TV_GOLDEN_MATERIAL3)
65
66 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
67 @Test
68 fun tabRow_pillIndicatorScreenshot() {
69 val tabs = constructTabs(count = 3)
70 val testTag = "TabRowTestTag"
71
72 setContent(
73 tabs = tabs,
74 contentBuilder = {
75 Box {
76 var selectedTabIndex by remember { mutableStateOf(0) }
77 TabRowSample(
78 tabs = tabs,
79 modifier = Modifier.testTag(testTag),
80 selectedTabIndex = selectedTabIndex,
81 onFocus = { selectedTabIndex = it }
82 )
83 }
84 }
85 )
86
87 rule
88 .onNodeWithTag(testTag)
89 .captureToImage()
90 .assertAgainstGolden(screenshotRule, "tab_row_pill_indicator_default")
91 }
92
Vighnesh Rautcb8c6312022-10-06 13:08:59 +053093 @Test
Vighnesh Raute7829872023-01-02 15:27:40 +053094 fun tabRow_shouldNotCrashWithOnly1Tab() {
95 val tabs = constructTabs(count = 1)
96
97 setContent(tabs)
98 }
99
100 @Test
vinekumar1c015252023-01-03 14:40:51 +0530101 fun tabRow_shouldNotCrashWithNoTabs() {
102 val tabs = constructTabs(count = 0)
103
104 setContent(tabs)
105 }
106
107 @Test
Vighnesh Rautcb8c6312022-10-06 13:08:59 +0530108 fun tabRow_firstTabIsSelected() {
109 val tabs = constructTabs()
110 val firstTab = tabs[0]
111
112 setContent(tabs)
113
114 rule.onNodeWithTag(firstTab).assertIsFocused()
115 }
116
117 @Test
118 fun tabRow_dPadRightMovesFocusToSecondTab() {
119 val tabs = constructTabs()
120 val firstTab = tabs[0]
121 val secondTab = tabs[1]
122
123 setContent(tabs)
124
125 // First tab should be focused
126 rule.onNodeWithTag(firstTab).assertIsFocused()
127
128 rule.waitForIdle()
129
130 // Move to next tab
131 performKeyPress(NativeKeyEvent.KEYCODE_DPAD_RIGHT)
132
133 rule.waitForIdle()
134
135 // Second tab should be focused
136 rule.onNodeWithTag(secondTab).assertIsFocused()
137 }
138
139 @Test
140 fun tabRow_dPadLeftMovesFocusToPreviousTab() {
141 val tabs = constructTabs()
142 val firstTab = tabs[0]
143 val secondTab = tabs[1]
144 val thirdTab = tabs[2]
145
146 setContent(tabs)
147
148 // First tab should be focused
149 rule.onNodeWithTag(firstTab).assertIsFocused()
150
151 rule.waitForIdle()
152
153 // Move to next tab
154 performKeyPress(NativeKeyEvent.KEYCODE_DPAD_RIGHT)
155
156 rule.waitForIdle()
157
158 // Second tab should be focused
159 rule.onNodeWithTag(secondTab).assertIsFocused()
160
161 // Move to next tab
162 performKeyPress(NativeKeyEvent.KEYCODE_DPAD_RIGHT)
163
164 rule.waitForIdle()
165
166 // Third tab should be focused
167 rule.onNodeWithTag(thirdTab).assertIsFocused()
168
169 // Move to previous tab
170 performKeyPress(NativeKeyEvent.KEYCODE_DPAD_LEFT)
171
172 rule.waitForIdle()
173
174 // Second tab should be focused
175 rule.onNodeWithTag(secondTab).assertIsFocused()
176
177 // Move to previous tab
178 performKeyPress(NativeKeyEvent.KEYCODE_DPAD_LEFT)
179
180 rule.waitForIdle()
181
182 // First tab should be focused
183 rule.onNodeWithTag(firstTab).assertIsFocused()
184 }
185
Vighnesh Raut21b29252023-01-05 18:06:55 +0530186 @OptIn(ExperimentalTvMaterial3Api::class)
Vighnesh Raut7ff66b42022-11-29 19:45:56 +0530187 @Test
188 fun tabRow_changeActiveTabOnClick() {
189 val tabs = constructTabs(count = 2)
Vighnesh Rautcb8c6312022-10-06 13:08:59 +0530190
Vighnesh Raut7ff66b42022-11-29 19:45:56 +0530191 val firstPanel = "Panel 1"
192 val secondPanel = "Panel 2"
Vighnesh Rautcb8c6312022-10-06 13:08:59 +0530193
Vighnesh Raut7ff66b42022-11-29 19:45:56 +0530194 setContent(
195 tabs,
196 contentBuilder = @Composable {
197 var focusedTabIndex by remember { mutableStateOf(0) }
198 var activeTabIndex by remember { mutableStateOf(focusedTabIndex) }
199 TabRowSample(
200 tabs = tabs,
201 selectedTabIndex = activeTabIndex,
202 onFocus = { focusedTabIndex = it },
203 onClick = { activeTabIndex = it },
204 buildTabPanel = @Composable { index, _ ->
205 BasicText(text = "Panel ${index + 1}")
206 },
207 indicator = @Composable { tabPositions ->
208 // FocusedTab's indicator
209 TabRowDefaults.PillIndicator(
210 currentTabPosition = tabPositions[focusedTabIndex],
211 activeColor = Color.Blue.copy(alpha = 0.4f),
212 inactiveColor = Color.Transparent,
213 )
214
215 // SelectedTab's indicator
216 TabRowDefaults.PillIndicator(
217 currentTabPosition = tabPositions[activeTabIndex]
218 )
Vighnesh Rautcb8c6312022-10-06 13:08:59 +0530219 }
Vighnesh Rautcb8c6312022-10-06 13:08:59 +0530220 )
Vighnesh Rautcb8c6312022-10-06 13:08:59 +0530221 }
Vighnesh Raut7ff66b42022-11-29 19:45:56 +0530222 )
223
224 rule.onNodeWithText(firstPanel).assertIsDisplayed()
225
226 // Move focus to next tab
227 performKeyPress(NativeKeyEvent.KEYCODE_DPAD_RIGHT)
228
229 rule.waitForIdle()
230
231 rule.onNodeWithText(firstPanel).assertIsDisplayed()
232 rule.onNodeWithText(secondPanel).assertDoesNotExist()
233
234 // Click on the new focused tab
235 performKeyPress(NativeKeyEvent.KEYCODE_DPAD_CENTER)
236
237 rule.onNodeWithText(firstPanel).assertDoesNotExist()
238 rule.onNodeWithText(secondPanel).assertIsDisplayed()
239 }
240
241 private fun setContent(
242 tabs: List<String>,
243 contentBuilder: @Composable () -> Unit = {
244 var selectedTabIndex by remember { mutableStateOf(0) }
245 TabRowSample(
246 tabs = tabs,
247 selectedTabIndex = selectedTabIndex,
248 onFocus = { selectedTabIndex = it }
249 )
250 },
251 ) {
252 rule.setContent {
253 contentBuilder()
Vighnesh Rautcb8c6312022-10-06 13:08:59 +0530254 }
255
256 rule.waitForIdle()
257
258 // Move the focus TabRow
Vighnesh Raut7ff66b42022-11-29 19:45:56 +0530259 performKeyPress(NativeKeyEvent.KEYCODE_DPAD_DOWN)
Vighnesh Rautcb8c6312022-10-06 13:08:59 +0530260
261 rule.waitForIdle()
262 }
Vighnesh Raut7ff66b42022-11-29 19:45:56 +0530263}
Vighnesh Rautcb8c6312022-10-06 13:08:59 +0530264
Vighnesh Raut21b29252023-01-05 18:06:55 +0530265@OptIn(ExperimentalTvMaterial3Api::class)
Vighnesh Raut7ff66b42022-11-29 19:45:56 +0530266@Composable
267private fun TabRowSample(
268 tabs: List<String>,
269 selectedTabIndex: Int,
Vighnesh Raut7e0b7c12023-03-10 16:20:44 +0530270 modifier: Modifier = Modifier,
Vighnesh Raut7ff66b42022-11-29 19:45:56 +0530271 onFocus: (index: Int) -> Unit = {},
272 onClick: (index: Int) -> Unit = onFocus,
273 buildTab: @Composable ((index: Int, tab: String) -> Unit) = @Composable { index, tab ->
274 TabSample(
275 selected = selectedTabIndex == index,
276 onFocus = { onFocus(index) },
277 onClick = { onClick(index) },
278 modifier = Modifier.testTag(tab),
279 )
280 },
vinekumar1c015252023-01-03 14:40:51 +0530281 indicator: @Composable ((tabPositions: List<DpRect>) -> Unit)? = null,
Vighnesh Raut7ff66b42022-11-29 19:45:56 +0530282 buildTabPanel: @Composable ((index: Int, tab: String) -> Unit) = @Composable { _, tab ->
283 BasicText(text = tab)
284 },
285) {
Vighnesh Rauteb682422022-12-17 09:50:12 +0530286 val fr = remember { FocusRequester() }
Vighnesh Raut7ff66b42022-11-29 19:45:56 +0530287
288 Column(
289 modifier = Modifier
290 .fillMaxSize()
291 .background(Color.Black)
292 ) {
293 // Added so that this can get focus and pass it to the tab row
294 Box(
295 modifier = Modifier
296 .size(50.dp)
297 .focusRequester(fr)
298 .background(Color.White)
299 .focusable()
300 )
301
302 // Send focus to button
303 LaunchedEffect(Unit) {
304 fr.requestFocus()
Vighnesh Rautcb8c6312022-10-06 13:08:59 +0530305 }
Vighnesh Rautcb8c6312022-10-06 13:08:59 +0530306
vinekumar1c015252023-01-03 14:40:51 +0530307 if (indicator != null) {
308 TabRow(
309 selectedTabIndex = selectedTabIndex,
Vighnesh Raut7e0b7c12023-03-10 16:20:44 +0530310 modifier = modifier,
vinekumar1c015252023-01-03 14:40:51 +0530311 indicator = indicator,
312 separator = { Spacer(modifier = Modifier.width(12.dp)) },
313 ) {
314 tabs.forEachIndexed { index, tab -> buildTab(index, tab) }
315 }
316 } else {
317 TabRow(
318 selectedTabIndex = selectedTabIndex,
Vighnesh Raut7e0b7c12023-03-10 16:20:44 +0530319 modifier = modifier,
vinekumar1c015252023-01-03 14:40:51 +0530320 separator = { Spacer(modifier = Modifier.width(12.dp)) },
321 ) {
322 tabs.forEachIndexed { index, tab -> buildTab(index, tab) }
323 }
Vighnesh Raut7ff66b42022-11-29 19:45:56 +0530324 }
325
vinekumar1c015252023-01-03 14:40:51 +0530326 tabs.elementAtOrNull(selectedTabIndex)?.let { buildTabPanel(selectedTabIndex, it) }
Vighnesh Raut7ff66b42022-11-29 19:45:56 +0530327 }
328}
329
Vighnesh Raut21b29252023-01-05 18:06:55 +0530330@OptIn(ExperimentalTvMaterial3Api::class)
Vighnesh Raut7ff66b42022-11-29 19:45:56 +0530331@Composable
332private fun TabSample(
333 selected: Boolean,
334 modifier: Modifier = Modifier,
335 onFocus: () -> Unit = {},
336 onClick: () -> Unit = {},
337 tag: String = "Tab",
338) {
339 Tab(
340 selected = selected,
341 onFocus = onFocus,
342 onClick = onClick,
343 modifier = modifier
344 .width(100.dp)
345 .height(50.dp)
346 .testTag(tag)
347 .border(2.dp, Color.White, RoundedCornerShape(50))
348 ) {}
349}
350
351private fun performKeyPress(keyCode: Int, count: Int = 1) {
352 for (i in 1..count) {
353 InstrumentationRegistry
354 .getInstrumentation()
355 .sendKeyDownUpSync(keyCode)
356 }
357}
358
359private fun constructTabs(
360 count: Int = 3,
361 buildTab: (index: Int) -> String = { "Season $it" }
vinekumar1c015252023-01-03 14:40:51 +0530362): List<String> = List(count, buildTab)