xref: /aosp_15_r20/frameworks/base/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt (revision d57664e9bc4670b3ecf6748a746a57c557b6bc9e)
1 /*
<lambda>null2  * Copyright (C) 2024 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 
17 package com.android.systemui.qs.panels.ui.compose
18 
19 import androidx.compose.foundation.layout.fillMaxSize
20 import androidx.compose.runtime.Composable
21 import androidx.compose.runtime.getValue
22 import androidx.compose.runtime.mutableStateOf
23 import androidx.compose.runtime.setValue
24 import androidx.compose.ui.Modifier
25 import androidx.compose.ui.semantics.SemanticsProperties
26 import androidx.compose.ui.test.SemanticsMatcher
27 import androidx.compose.ui.test.assert
28 import androidx.compose.ui.test.filter
29 import androidx.compose.ui.test.hasContentDescription
30 import androidx.compose.ui.test.junit4.ComposeContentTestRule
31 import androidx.compose.ui.test.junit4.createComposeRule
32 import androidx.compose.ui.test.onChildren
33 import androidx.compose.ui.test.onNodeWithContentDescription
34 import androidx.compose.ui.test.onNodeWithTag
35 import androidx.compose.ui.test.onNodeWithText
36 import androidx.compose.ui.text.AnnotatedString
37 import androidx.test.ext.junit.runners.AndroidJUnit4
38 import androidx.test.filters.FlakyTest
39 import androidx.test.filters.SmallTest
40 import com.android.systemui.SysuiTestCase
41 import com.android.systemui.common.shared.model.ContentDescription
42 import com.android.systemui.common.shared.model.Icon
43 import com.android.systemui.qs.panels.shared.model.SizedTile
44 import com.android.systemui.qs.panels.shared.model.SizedTileImpl
45 import com.android.systemui.qs.panels.ui.compose.infinitegrid.DefaultEditTileGrid
46 import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel
47 import com.android.systemui.qs.pipeline.shared.TileSpec
48 import com.android.systemui.qs.shared.model.TileCategory
49 import org.junit.Rule
50 import org.junit.Test
51 import org.junit.runner.RunWith
52 
53 @FlakyTest(bugId = 360351805)
54 @SmallTest
55 @RunWith(AndroidJUnit4::class)
56 class DragAndDropTest : SysuiTestCase() {
57     @get:Rule val composeRule = createComposeRule()
58 
59     // TODO(ostonge): Investigate why drag isn't detected when using performTouchInput
60     @Composable
61     private fun EditTileGridUnderTest(
62         listState: EditTileListState,
63         onSetTiles: (List<TileSpec>) -> Unit,
64     ) {
65         DefaultEditTileGrid(
66             listState = listState,
67             otherTiles = listOf(),
68             columns = 4,
69             largeTilesSpan = 4,
70             modifier = Modifier.fillMaxSize(),
71             onRemoveTile = {},
72             onSetTiles = onSetTiles,
73             onResize = { _, _ -> },
74             onStopEditing = {},
75             onReset = null,
76         )
77     }
78 
79     @Test
80     fun draggedTile_shouldDisappear() {
81         var tiles by mutableStateOf(TestEditTiles)
82         val listState = EditTileListState(tiles, columns = 4, largeTilesSpan = 2)
83         composeRule.setContent {
84             EditTileGridUnderTest(listState) {
85                 tiles = it.map { tileSpec -> createEditTile(tileSpec.spec) }
86             }
87         }
88         composeRule.waitForIdle()
89 
90         listState.onStarted(TestEditTiles[0])
91 
92         // Tile is being dragged, it should be replaced with a placeholder
93         composeRule.onNodeWithContentDescription("tileA").assertDoesNotExist()
94 
95         // Available tiles should disappear
96         composeRule.onNodeWithTag(AVAILABLE_TILES_GRID_TEST_TAG).assertDoesNotExist()
97 
98         // Remove drop zone should appear
99         composeRule.onNodeWithText("Remove").assertExists()
100 
101         // Every other tile should still be in the same order
102         composeRule.assertTileGridContainsExactly(listOf("tileB", "tileC", "tileD_large", "tileE"))
103     }
104 
105     @Test
106     fun draggedTile_shouldChangePosition() {
107         var tiles by mutableStateOf(TestEditTiles)
108         val listState = EditTileListState(tiles, columns = 4, largeTilesSpan = 2)
109         composeRule.setContent {
110             EditTileGridUnderTest(listState) {
111                 tiles = it.map { tileSpec -> createEditTile(tileSpec.spec) }
112             }
113         }
114         composeRule.waitForIdle()
115 
116         listState.onStarted(TestEditTiles[0])
117         listState.onMoved(1, false)
118         listState.onDrop()
119 
120         // Available tiles should re-appear
121         composeRule.onNodeWithTag(AVAILABLE_TILES_GRID_TEST_TAG).assertExists()
122 
123         // Remove drop zone should disappear
124         composeRule.onNodeWithText("Remove").assertDoesNotExist()
125 
126         // Tile A and B should swap places
127         composeRule.assertTileGridContainsExactly(
128             listOf("tileB", "tileA", "tileC", "tileD_large", "tileE")
129         )
130     }
131 
132     @Test
133     fun draggedTileOut_shouldBeRemoved() {
134         var tiles by mutableStateOf(TestEditTiles)
135         val listState = EditTileListState(tiles, columns = 4, largeTilesSpan = 2)
136         composeRule.setContent {
137             EditTileGridUnderTest(listState) {
138                 tiles = it.map { tileSpec -> createEditTile(tileSpec.spec) }
139             }
140         }
141         composeRule.waitForIdle()
142 
143         listState.onStarted(TestEditTiles[0])
144         listState.movedOutOfBounds()
145         listState.onDrop()
146 
147         // Available tiles should re-appear
148         composeRule.onNodeWithTag(AVAILABLE_TILES_GRID_TEST_TAG).assertExists()
149 
150         // Remove drop zone should disappear
151         composeRule.onNodeWithText("Remove").assertDoesNotExist()
152 
153         // Tile A is gone
154         composeRule.assertTileGridContainsExactly(listOf("tileB", "tileC", "tileD_large", "tileE"))
155     }
156 
157     @Test
158     fun draggedNewTileIn_shouldBeAdded() {
159         var tiles by mutableStateOf(TestEditTiles)
160         val listState = EditTileListState(tiles, columns = 4, largeTilesSpan = 2)
161         composeRule.setContent {
162             EditTileGridUnderTest(listState) {
163                 tiles = it.map { tileSpec -> createEditTile(tileSpec.spec) }
164             }
165         }
166         composeRule.waitForIdle()
167 
168         listState.onStarted(createEditTile("newTile"))
169         // Insert after tileD, which is at index 4
170         // [ a ] [ b ] [ c ] [ empty ]
171         // [ tile d ] [ e ]
172         listState.onMoved(4, insertAfter = true)
173         listState.onDrop()
174 
175         // Available tiles should re-appear
176         composeRule.onNodeWithTag(AVAILABLE_TILES_GRID_TEST_TAG).assertExists()
177 
178         // Remove drop zone should disappear
179         composeRule.onNodeWithText("Remove").assertDoesNotExist()
180 
181         // newTile is added after tileD
182         composeRule.assertTileGridContainsExactly(
183             listOf("tileA", "tileB", "tileC", "tileD_large", "newTile", "tileE")
184         )
185     }
186 
187     private fun ComposeContentTestRule.assertTileGridContainsExactly(specs: List<String>) {
188         onNodeWithTag(CURRENT_TILES_GRID_TEST_TAG)
189             .onChildren()
190             .filter(SemanticsMatcher.keyIsDefined(SemanticsProperties.ContentDescription))
191             .apply {
192                 fetchSemanticsNodes().forEachIndexed { index, _ ->
193                     get(index).assert(hasContentDescription(specs[index]))
194                 }
195             }
196     }
197 
198     companion object {
199         private const val CURRENT_TILES_GRID_TEST_TAG = "CurrentTilesGrid"
200         private const val AVAILABLE_TILES_GRID_TEST_TAG = "AvailableTilesGrid"
201 
202         private fun createEditTile(tileSpec: String): SizedTile<EditTileViewModel> {
203             return SizedTileImpl(
204                 EditTileViewModel(
205                     tileSpec = TileSpec.create(tileSpec),
206                     icon =
207                         Icon.Resource(
208                             android.R.drawable.star_on,
209                             ContentDescription.Loaded(tileSpec),
210                         ),
211                     label = AnnotatedString(tileSpec),
212                     appName = null,
213                     isCurrent = true,
214                     availableEditActions = emptySet(),
215                     category = TileCategory.UNKNOWN,
216                 ),
217                 getWidth(tileSpec),
218             )
219         }
220 
221         private fun getWidth(tileSpec: String): Int {
222             return if (tileSpec.endsWith("large")) {
223                 2
224             } else {
225                 1
226             }
227         }
228 
229         private val TestEditTiles =
230             listOf(
231                 createEditTile("tileA"),
232                 createEditTile("tileB"),
233                 createEditTile("tileC"),
234                 createEditTile("tileD_large"),
235                 createEditTile("tileE"),
236             )
237     }
238 }
239