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