diff options
author | 2024-02-19 20:00:16 +0800 | |
---|---|---|
committer | 2024-02-21 18:14:20 +0800 | |
commit | 07e7d706615002209031da3eab3a475cfeee3df8 (patch) | |
tree | 34ee9639bcbbcef36dc64aef9a90fe64e877a46b | |
parent | ef8fd88713fbfd928c1f0ad62a3f86402352b552 (diff) |
Support SettingsDropdownCheckBox non changeable items
If not changeable, cannot check or uncheck this option.
Bug: 326172568
Test: manual - with gallery
Test: unit test
Change-Id: I671e21c5238c71a8bffd7bf3b4aced2f8977ec71
9 files changed, 492 insertions, 357 deletions
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt index 460a6f7261ae..e1853675d6d4 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt @@ -28,7 +28,7 @@ import com.android.settingslib.spa.gallery.dialog.DialogMainPageProvider import com.android.settingslib.spa.gallery.dialog.NavDialogProvider import com.android.settingslib.spa.gallery.editor.EditorMainPageProvider import com.android.settingslib.spa.gallery.editor.SettingsExposedDropdownMenuBoxPageProvider -import com.android.settingslib.spa.gallery.editor.SettingsExposedDropdownMenuCheckBoxProvider +import com.android.settingslib.spa.gallery.editor.SettingsDropdownCheckBoxProvider import com.android.settingslib.spa.gallery.home.HomePageProvider import com.android.settingslib.spa.gallery.itemList.ItemListPageProvider import com.android.settingslib.spa.gallery.itemList.ItemOperatePageProvider @@ -100,7 +100,7 @@ class GallerySpaEnvironment(context: Context) : SpaEnvironment(context) { EditorMainPageProvider, SettingsOutlinedTextFieldPageProvider, SettingsExposedDropdownMenuBoxPageProvider, - SettingsExposedDropdownMenuCheckBoxProvider, + SettingsDropdownCheckBoxProvider, SettingsTextFieldPasswordPageProvider, SearchScaffoldPageProvider, SuwScaffoldPageProvider, diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/editor/EditorMainPageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/editor/EditorMainPageProvider.kt index 4875ea99d4d6..9f2158a13f25 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/editor/EditorMainPageProvider.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/editor/EditorMainPageProvider.kt @@ -37,7 +37,7 @@ object EditorMainPageProvider : SettingsPageProvider { .build(), SettingsExposedDropdownMenuBoxPageProvider.buildInjectEntry().setLink(fromPage = owner) .build(), - SettingsExposedDropdownMenuCheckBoxProvider.buildInjectEntry().setLink(fromPage = owner) + SettingsDropdownCheckBoxProvider.buildInjectEntry().setLink(fromPage = owner) .build(), SettingsTextFieldPasswordPageProvider.buildInjectEntry().setLink(fromPage = owner) .build(), diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/editor/SettingsDropdownCheckBoxProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/editor/SettingsDropdownCheckBoxProvider.kt new file mode 100644 index 000000000000..33ab75d6189d --- /dev/null +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/editor/SettingsDropdownCheckBoxProvider.kt @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.spa.gallery.editor + +import android.os.Bundle +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.tooling.preview.Preview +import com.android.settingslib.spa.framework.common.SettingsEntryBuilder +import com.android.settingslib.spa.framework.common.SettingsPageProvider +import com.android.settingslib.spa.framework.common.createSettingsPage +import com.android.settingslib.spa.framework.compose.navigator +import com.android.settingslib.spa.framework.theme.SettingsTheme +import com.android.settingslib.spa.widget.editor.SettingsDropdownCheckBox +import com.android.settingslib.spa.widget.editor.SettingsDropdownCheckOption +import com.android.settingslib.spa.widget.preference.Preference +import com.android.settingslib.spa.widget.preference.PreferenceModel +import com.android.settingslib.spa.widget.scaffold.RegularScaffold + +private const val TITLE = "Sample SettingsDropdownCheckBox" + +object SettingsDropdownCheckBoxProvider : SettingsPageProvider { + override val name = "SettingsDropdownCheckBox" + + override fun getTitle(arguments: Bundle?): String { + return TITLE + } + + @Composable + override fun Page(arguments: Bundle?) { + RegularScaffold(title = TITLE) { + SettingsDropdownCheckBox( + label = "SettingsDropdownCheckBox", + options = remember { + listOf( + SettingsDropdownCheckOption("Item 1"), + SettingsDropdownCheckOption("Item 2"), + SettingsDropdownCheckOption("Item 3"), + ) + }, + ) + SettingsDropdownCheckBox( + label = "Empty list", + options = emptyList(), + ) + SettingsDropdownCheckBox( + label = "Disabled", + options = remember { + listOf( + SettingsDropdownCheckOption("Item 1", selected = mutableStateOf(true)), + SettingsDropdownCheckOption("Item 2"), + SettingsDropdownCheckOption("Item 3"), + ) + }, + enabled = false, + ) + SettingsDropdownCheckBox( + label = "With disabled item", + options = remember { + listOf( + SettingsDropdownCheckOption("Item 1"), + SettingsDropdownCheckOption("Item 2"), + SettingsDropdownCheckOption( + text = "Disabled item 1", + changeable = false, + selected = mutableStateOf(true), + ), + SettingsDropdownCheckOption("Disabled item 2", changeable = false), + ) + }, + ) + SettingsDropdownCheckBox( + label = "With select all", + options = remember { + listOf( + SettingsDropdownCheckOption("All", isSelectAll = true), + SettingsDropdownCheckOption("Item 1"), + SettingsDropdownCheckOption("Item 2"), + SettingsDropdownCheckOption("Item 3"), + ) + }, + ) + SettingsDropdownCheckBox( + label = "With disabled item and select all", + options = + remember { + listOf( + SettingsDropdownCheckOption("All", isSelectAll = true, changeable = false), + SettingsDropdownCheckOption("Item 1"), + SettingsDropdownCheckOption("Item 2"), + SettingsDropdownCheckOption( + text = "Disabled item 1", + changeable = false, + selected = mutableStateOf(true), + ), + SettingsDropdownCheckOption("Disabled item 2", changeable = false), + ) + }, + ) + } + } + + fun buildInjectEntry(): SettingsEntryBuilder { + return SettingsEntryBuilder.createInject(owner = createSettingsPage()).setUiLayoutFn { + Preference(object : PreferenceModel { + override val title = TITLE + override val onClick = navigator(name) + }) + } + } +} + +@Preview(showBackground = true) +@Composable +private fun SettingsDropdownCheckBoxPagePreview() { + SettingsTheme { + SettingsDropdownCheckBoxProvider.Page(null) + } +} diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/editor/SettingsExposedDropdownMenuCheckBoxProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/editor/SettingsExposedDropdownMenuCheckBoxProvider.kt deleted file mode 100644 index d28964676bdd..000000000000 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/editor/SettingsExposedDropdownMenuCheckBoxProvider.kt +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.settingslib.spa.gallery.editor - -import android.os.Bundle -import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateListOf -import androidx.compose.runtime.remember -import androidx.compose.ui.tooling.preview.Preview -import com.android.settingslib.spa.framework.common.SettingsEntryBuilder -import com.android.settingslib.spa.framework.common.SettingsPageProvider -import com.android.settingslib.spa.framework.common.createSettingsPage -import com.android.settingslib.spa.framework.compose.navigator -import com.android.settingslib.spa.framework.theme.SettingsTheme -import com.android.settingslib.spa.widget.editor.SettingsExposedDropdownMenuCheckBox -import com.android.settingslib.spa.widget.preference.Preference -import com.android.settingslib.spa.widget.preference.PreferenceModel -import com.android.settingslib.spa.widget.scaffold.RegularScaffold - -private const val TITLE = "Sample SettingsExposedDropdownMenuCheckBox" - -object SettingsExposedDropdownMenuCheckBoxProvider : SettingsPageProvider { - override val name = "SettingsExposedDropdownMenuCheckBox" - private const val exposedDropdownMenuCheckBoxLabel = "ExposedDropdownMenuCheckBoxLabel" - private val options = listOf("item1", "item2", "item3") - private val selectedOptionsState1 = mutableStateListOf(0, 1) - - override fun getTitle(arguments: Bundle?): String { - return TITLE - } - - @Composable - override fun Page(arguments: Bundle?) { - RegularScaffold(title = TITLE) { - SettingsExposedDropdownMenuCheckBox( - label = exposedDropdownMenuCheckBoxLabel, - options = options, - selectedOptionsState = remember { selectedOptionsState1 }, - enabled = true, - onSelectedOptionStateChange = {}, - ) - } - } - - fun buildInjectEntry(): SettingsEntryBuilder { - return SettingsEntryBuilder.createInject(owner = createSettingsPage()).setUiLayoutFn { - Preference(object : PreferenceModel { - override val title = TITLE - override val onClick = navigator(name) - }) - } - } -} - -@Preview(showBackground = true) -@Composable -private fun SettingsExposedDropdownMenuCheckBoxPagePreview() { - SettingsTheme { - SettingsExposedDropdownMenuCheckBoxProvider.Page(null) - } -}
\ No newline at end of file diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsDropdownCheckBox.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsDropdownCheckBox.kt new file mode 100644 index 000000000000..57963e6eaa40 --- /dev/null +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsDropdownCheckBox.kt @@ -0,0 +1,184 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.spa.widget.editor + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Checkbox +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExposedDropdownMenuBox +import androidx.compose.material3.ExposedDropdownMenuDefaults +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.android.settingslib.spa.framework.theme.SettingsDimension +import com.android.settingslib.spa.framework.theme.SettingsOpacity.alphaForEnabled +import com.android.settingslib.spa.framework.theme.SettingsTheme +import com.android.settingslib.spa.widget.editor.SettingsDropdownCheckOption.Companion.changeable + +data class SettingsDropdownCheckOption( + /** The displayed text of this option. */ + val text: String, + + /** If true, check / uncheck this item will check / uncheck all enabled options. */ + val isSelectAll: Boolean = false, + + /** If not changeable, cannot check or uncheck this option. */ + val changeable: Boolean = true, + + /** The selected state of this option. */ + val selected: MutableState<Boolean> = mutableStateOf(false), + + /** Get called when the option is clicked, no matter if it's changeable. */ + val onClick: () -> Unit = {}, +) { + companion object { + val List<SettingsDropdownCheckOption>.changeable: Boolean + get() = filter { !it.isSelectAll }.any { it.changeable } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SettingsDropdownCheckBox( + label: String, + options: List<SettingsDropdownCheckOption>, + emptyText: String = "", + enabled: Boolean = true, + errorMessage: String? = null, + onSelectedStateChange: () -> Unit = {}, +) { + var dropDownWidth by remember { mutableIntStateOf(0) } + var expanded by remember { mutableStateOf(false) } + val changeable = enabled && options.changeable + ExposedDropdownMenuBox( + expanded = expanded, + onExpandedChange = { expanded = changeable && it }, + modifier = Modifier + .width(350.dp) + .padding(SettingsDimension.textFieldPadding) + .onSizeChanged { dropDownWidth = it.width }, + ) { + OutlinedTextField( + // The `menuAnchor` modifier must be passed to the text field for correctness. + modifier = Modifier + .menuAnchor() + .fillMaxWidth(), + value = getDisplayText(options) ?: emptyText, + onValueChange = {}, + label = { Text(text = label) }, + trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded) }, + readOnly = true, + enabled = changeable, + isError = errorMessage != null, + supportingText = errorMessage?.let { { Text(text = it) } }, + ) + ExposedDropdownMenu( + expanded = expanded, + modifier = Modifier.width(with(LocalDensity.current) { dropDownWidth.toDp() }), + onDismissRequest = { expanded = false }, + ) { + for (option in options) { + CheckboxItem(option) { + option.onClick() + if (option.changeable) { + checkboxItemOnClick(options, option) + onSelectedStateChange() + } + } + } + } + } +} + +private fun getDisplayText(options: List<SettingsDropdownCheckOption>): String? { + val selectedOptions = options.filter { it.selected.value } + if (selectedOptions.isEmpty()) return null + return selectedOptions.filter { it.isSelectAll }.ifEmpty { selectedOptions } + .joinToString { it.text } +} + +private fun checkboxItemOnClick( + options: List<SettingsDropdownCheckOption>, + clickedOption: SettingsDropdownCheckOption, +) { + if (!clickedOption.changeable) return + val newChecked = !clickedOption.selected.value + if (clickedOption.isSelectAll) { + for (option in options.filter { it.changeable }) option.selected.value = newChecked + } else { + clickedOption.selected.value = newChecked + } + val (selectAllOptions, regularOptions) = options.partition { it.isSelectAll } + val isAllRegularOptionsChecked = regularOptions.all { it.selected.value } + selectAllOptions.forEach { it.selected.value = isAllRegularOptionsChecked } +} + +@Composable +private fun CheckboxItem( + option: SettingsDropdownCheckOption, + onClick: (SettingsDropdownCheckOption) -> Unit, +) { + TextButton( + onClick = { onClick(option) }, + modifier = Modifier.fillMaxWidth(), + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(SettingsDimension.itemPaddingAround), + verticalAlignment = Alignment.CenterVertically + ) { + Checkbox( + checked = option.selected.value, + onCheckedChange = null, + enabled = option.changeable, + ) + Text(text = option.text, modifier = Modifier.alphaForEnabled(option.changeable)) + } + } +} + +@Preview +@Composable +private fun ActionButtonsPreview() { + val item1 = SettingsDropdownCheckOption("item1") + val item2 = SettingsDropdownCheckOption("item2") + val item3 = SettingsDropdownCheckOption("item3") + val options = listOf(item1, item2, item3) + SettingsTheme { + SettingsDropdownCheckBox( + label = "label", + options = options, + ) + } +} diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsExposedDropdownMenuCheckBox.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsExposedDropdownMenuCheckBox.kt deleted file mode 100644 index e704505117cf..000000000000 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsExposedDropdownMenuCheckBox.kt +++ /dev/null @@ -1,164 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.settingslib.spa.widget.editor - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.material3.Checkbox -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.ExposedDropdownMenuBox -import androidx.compose.material3.ExposedDropdownMenuDefaults -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.mutableStateListOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.runtime.snapshots.SnapshotStateList -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.layout.onSizeChanged -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.android.settingslib.spa.framework.theme.SettingsDimension -import com.android.settingslib.spa.framework.theme.SettingsTheme - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun SettingsExposedDropdownMenuCheckBox( - label: String, - options: List<String>, - selectedOptionsState: SnapshotStateList<Int>, - emptyVal: String = "", - enabled: Boolean, - errorMessage: String? = null, - onSelectedOptionStateChange: () -> Unit, -) { - var dropDownWidth by remember { mutableIntStateOf(0) } - var expanded by remember { mutableStateOf(false) } - val allIndex = options.indexOf("*") - ExposedDropdownMenuBox( - expanded = expanded, - onExpandedChange = { expanded = it }, - modifier = Modifier - .width(350.dp) - .padding(SettingsDimension.textFieldPadding) - .onSizeChanged { dropDownWidth = it.width }, - ) { - OutlinedTextField( - // The `menuAnchor` modifier must be passed to the text field for correctness. - modifier = Modifier - .menuAnchor() - .fillMaxWidth(), - value = if (selectedOptionsState.size == 0) emptyVal - else if (selectedOptionsState.contains(allIndex)) "*" - else selectedOptionsState.joinToString { options[it] }, - onValueChange = {}, - label = { Text(text = label) }, - trailingIcon = { - ExposedDropdownMenuDefaults.TrailingIcon( - expanded = expanded - ) - }, - readOnly = true, - enabled = enabled, - isError = errorMessage != null, - supportingText = { - if (errorMessage != null) { - Text(text = errorMessage) - } - } - ) - if (options.isNotEmpty()) { - ExposedDropdownMenu( - expanded = expanded, - modifier = Modifier.width(with(LocalDensity.current) { dropDownWidth.toDp() }), - onDismissRequest = { expanded = false }, - ) { - options.forEachIndexed { index, option -> - CheckboxItem( - selectedOptionsState, - index, - allIndex, - onSelectedOptionStateChange, - option, - ) - } - } - } - } -} - -@Composable -private fun CheckboxItem( - selectedOptionsState: SnapshotStateList<Int>, - index: Int, - allIndex: Int, - onSelectedOptionStateChange: () -> Unit, - option: String -) { - TextButton( - modifier = Modifier.fillMaxWidth(), - onClick = { - if (selectedOptionsState.contains(index)) { - if (index == allIndex) { - selectedOptionsState.clear() - } else { - selectedOptionsState.remove(index) - selectedOptionsState.remove(allIndex) - } - } else { - selectedOptionsState.add(index) - } - onSelectedOptionStateChange() - }) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(SettingsDimension.itemPaddingAround), - verticalAlignment = Alignment.CenterVertically - ) { - Checkbox( - checked = selectedOptionsState.contains(index), - onCheckedChange = null, - ) - Text(text = option) - } - } -} - -@Preview -@Composable -private fun ActionButtonsPreview() { - val options = listOf("item1", "item2", "item3") - val selectedOptionsState = remember { mutableStateListOf(0, 1) } - SettingsTheme { - SettingsExposedDropdownMenuCheckBox( - label = "label", - options = options, - selectedOptionsState = selectedOptionsState, - enabled = true, - onSelectedOptionStateChange = {}) - } -} diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/editor/SettingsDropdownCheckBoxTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/editor/SettingsDropdownCheckBoxTest.kt new file mode 100644 index 000000000000..72b7b98c2a94 --- /dev/null +++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/editor/SettingsDropdownCheckBoxTest.kt @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.spa.widget.editor + +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.hasAnyAncestor +import androidx.compose.ui.test.hasText +import androidx.compose.ui.test.isPopup +import androidx.compose.ui.test.junit4.ComposeContentTestRule +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.settingslib.spa.testutils.hasRole +import com.google.common.truth.Truth.assertThat +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class SettingsDropdownCheckBoxTest { + @get:Rule + val composeTestRule = createComposeRule() + + @Test + fun dropdownCheckBox_displayed() { + val item1 = SettingsDropdownCheckOption("item1") + val item2 = SettingsDropdownCheckOption("item2") + val item3 = SettingsDropdownCheckOption("item3") + composeTestRule.setContent { + SettingsDropdownCheckBox( + label = LABEL, + options = listOf(item1, item2, item3), + ) + } + + composeTestRule.onNodeWithText(LABEL).assertIsDisplayed() + } + + @Test + fun dropdownCheckBox_expanded() { + val item1 = SettingsDropdownCheckOption("item1") + val item2 = SettingsDropdownCheckOption("item2") + val item3 = SettingsDropdownCheckOption("item3") + composeTestRule.setContent { + SettingsDropdownCheckBox( + label = LABEL, + options = listOf(item1, item2, item3), + ) + } + composeTestRule.onOption(item3).assertDoesNotExist() + + composeTestRule.onNodeWithText(LABEL).performClick() + + composeTestRule.onOption(item3).assertIsDisplayed() + } + + @Test + fun dropdownCheckBox_valueAdded() { + val item1 = SettingsDropdownCheckOption("item1") + val item2 = SettingsDropdownCheckOption("item2") + val item3 = SettingsDropdownCheckOption("item3") + composeTestRule.setContent { + SettingsDropdownCheckBox( + label = LABEL, + options = listOf(item1, item2, item3), + ) + } + composeTestRule.onDropdownBox(item3.text).assertDoesNotExist() + + composeTestRule.onNodeWithText(LABEL).performClick() + composeTestRule.onOption(item3).performClick() + + composeTestRule.onDropdownBox(item3.text).assertIsDisplayed() + assertThat(item3.selected.value).isTrue() + } + + @Test + fun dropdownCheckBox_valueDeleted() { + val item1 = SettingsDropdownCheckOption("item1") + val item2 = SettingsDropdownCheckOption("item2", selected = mutableStateOf(true)) + val item3 = SettingsDropdownCheckOption("item3") + composeTestRule.setContent { + SettingsDropdownCheckBox( + label = LABEL, + options = listOf(item1, item2, item3), + ) + } + composeTestRule.onDropdownBox(item2.text).assertIsDisplayed() + + composeTestRule.onNodeWithText(LABEL).performClick() + composeTestRule.onOption(item2).performClick() + + composeTestRule.onDropdownBox(item2.text).assertDoesNotExist() + assertThat(item2.selected.value).isFalse() + } + + @Test + fun dropdownCheckBox_withSelectAll() { + val selectAll = SettingsDropdownCheckOption("All", isSelectAll = true) + val item1 = SettingsDropdownCheckOption("item1") + val item2 = SettingsDropdownCheckOption("item2") + composeTestRule.setContent { + SettingsDropdownCheckBox( + label = LABEL, + options = listOf(selectAll, item1, item2), + ) + } + + composeTestRule.onNodeWithText(LABEL).performClick() + composeTestRule.onOption(selectAll).performClick() + + composeTestRule.onDropdownBox(selectAll.text).assertIsDisplayed() + composeTestRule.onDropdownBox(item1.text).assertDoesNotExist() + composeTestRule.onDropdownBox(item2.text).assertDoesNotExist() + assertThat(item1.selected.value).isTrue() + assertThat(item2.selected.value).isTrue() + } + + private companion object { + const val LABEL = "Label" + } +} + +private fun ComposeContentTestRule.onDropdownBox(text: String) = + onNode(hasRole(Role.DropdownList) and hasText(text)) + +private fun ComposeContentTestRule.onOption(option: SettingsDropdownCheckOption) = + onNode(hasAnyAncestor(isPopup()) and hasText(option.text)) diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/editor/SettingsExposedDropdownMenuCheckBoxTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/editor/SettingsExposedDropdownMenuCheckBoxTest.kt deleted file mode 100644 index 2b78ed7d6fa2..000000000000 --- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/editor/SettingsExposedDropdownMenuCheckBoxTest.kt +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.settingslib.spa.widget.editor - -import androidx.compose.runtime.mutableStateListOf -import androidx.compose.runtime.remember -import androidx.compose.ui.test.SemanticsNodeInteraction -import androidx.compose.ui.test.assertIsDisplayed -import androidx.compose.ui.test.hasText -import androidx.compose.ui.test.isFocused -import androidx.compose.ui.test.junit4.ComposeContentTestRule -import androidx.compose.ui.test.junit4.createComposeRule -import androidx.compose.ui.test.onNodeWithText -import androidx.compose.ui.test.performClick -import androidx.test.ext.junit.runners.AndroidJUnit4 -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class SettingsExposedDropdownMenuCheckBoxTest { - @get:Rule - val composeTestRule = createComposeRule() - private val item1 = "item1" - private val item2 = "item2" - private val item3 = "item3" - private val options = listOf(item1, item2, item3) - private val selectedOptionsState1 = mutableStateListOf(0, 1) - private val exposedDropdownMenuCheckBoxLabel = "ExposedDropdownMenuCheckBoxLabel" - - @Test - fun exposedDropdownMenuCheckBox_displayed() { - composeTestRule.setContent { - SettingsExposedDropdownMenuCheckBox( - label = exposedDropdownMenuCheckBoxLabel, - options = options, - selectedOptionsState = remember { selectedOptionsState1 }, - enabled = true, - ) {} - } - composeTestRule.onNodeWithText( - exposedDropdownMenuCheckBoxLabel, substring = true - ).assertIsDisplayed() - } - - @Test - fun exposedDropdownMenuCheckBox_expanded() { - composeTestRule.setContent { - SettingsExposedDropdownMenuCheckBox( - label = exposedDropdownMenuCheckBoxLabel, - options = options, - selectedOptionsState = remember { selectedOptionsState1 }, - enabled = true, - ) {} - } - composeTestRule.onNodeWithText(item3, substring = true).assertDoesNotExist() - composeTestRule.onNodeWithText(exposedDropdownMenuCheckBoxLabel, substring = true) - .performClick() - composeTestRule.onNodeWithText(item3, substring = true).assertIsDisplayed() - } - - @Test - fun exposedDropdownMenuCheckBox_valueAdded() { - composeTestRule.setContent { - SettingsExposedDropdownMenuCheckBox( - label = exposedDropdownMenuCheckBoxLabel, - options = options, - selectedOptionsState = remember { selectedOptionsState1 }, - enabled = true, - ) {} - } - composeTestRule.onNodeWithText(item3, substring = true).assertDoesNotExist() - composeTestRule.onNodeWithText(exposedDropdownMenuCheckBoxLabel, substring = true) - .performClick() - composeTestRule.onNodeWithText(item3, substring = true).performClick() - composeTestRule.onFocusedText(item3).assertIsDisplayed() - } - - @Test - fun exposedDropdownMenuCheckBox_valueDeleted() { - composeTestRule.setContent { - SettingsExposedDropdownMenuCheckBox( - label = exposedDropdownMenuCheckBoxLabel, - options = options, - selectedOptionsState = remember { selectedOptionsState1 }, - enabled = true, - ) {} - } - composeTestRule.onNodeWithText(item2, substring = true).assertIsDisplayed() - composeTestRule.onNodeWithText(exposedDropdownMenuCheckBoxLabel, substring = true) - .performClick() - composeTestRule.onNotFocusedText(item2).performClick() - composeTestRule.onFocusedText(item2).assertDoesNotExist() - } -} - -fun ComposeContentTestRule.onFocusedText(text: String): SemanticsNodeInteraction = - onNode(isFocused() and hasText(text, substring = true)) - -fun ComposeContentTestRule.onNotFocusedText(text: String): SemanticsNodeInteraction = - onNode(!isFocused() and hasText(text, substring = true))
\ No newline at end of file diff --git a/packages/SettingsLib/Spa/testutils/src/com/android/settingslib/spa/testutils/SemanticsMatcher.kt b/packages/SettingsLib/Spa/testutils/src/com/android/settingslib/spa/testutils/SemanticsMatcher.kt new file mode 100644 index 000000000000..856bed603354 --- /dev/null +++ b/packages/SettingsLib/Spa/testutils/src/com/android/settingslib/spa/testutils/SemanticsMatcher.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.spa.testutils + +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.SemanticsProperties +import androidx.compose.ui.semantics.getOrNull +import androidx.compose.ui.test.SemanticsMatcher + +fun hasRole(role: Role) = SemanticsMatcher("${SemanticsProperties.Role.name} has $role") { + it.config.getOrNull(SemanticsProperties.Role) == role +} |