From 1c525b9dfc588105e1fd8f3ad2d4196803c19cd7 Mon Sep 17 00:00:00 2001 From: Patrick Honkonen <1883101+SaintPatrck@users.noreply.github.com> Date: Fri, 8 Aug 2025 10:43:34 -0400 Subject: [PATCH 1/3] [PM-24575] Add feature flag for AEAD enrollment on key rotation (#5665) --- .../feature/debugmenu/components/FeatureFlagListItems.kt | 5 +++++ .../feature/debugmenu/components/FeatureFlagListItems.kt | 5 +++++ .../com/bitwarden/core/data/manager/model/FlagKey.kt | 8 ++++++++ .../com/bitwarden/core/data/manager/model/FlagKeyTest.kt | 5 +++++ ui/src/main/res/values/strings_non_localized.xml | 1 + 5 files changed, 24 insertions(+) diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/debugmenu/components/FeatureFlagListItems.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/debugmenu/components/FeatureFlagListItems.kt index 341694b4e1..e745315e62 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/debugmenu/components/FeatureFlagListItems.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/debugmenu/components/FeatureFlagListItems.kt @@ -31,6 +31,7 @@ fun FlagKey.ListItemContent( FlagKey.CipherKeyEncryption, FlagKey.UserManagedPrivilegedApps, FlagKey.RemoveCardPolicy, + FlagKey.EnrollAeadOnKeyRotation, -> { @Suppress("UNCHECKED_CAST") BooleanFlagItem( @@ -83,4 +84,8 @@ private fun FlagKey.getDisplayLabel(): String = when (this) { FlagKey.BitwardenAuthenticationEnabled -> { stringResource(BitwardenString.bitwarden_authentication_enabled) } + + FlagKey.EnrollAeadOnKeyRotation -> { + stringResource(BitwardenString.enroll_aead_on_key_rotation) + } } diff --git a/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/platform/feature/debugmenu/components/FeatureFlagListItems.kt b/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/platform/feature/debugmenu/components/FeatureFlagListItems.kt index 5c6c2c7342..a825fad4c2 100644 --- a/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/platform/feature/debugmenu/components/FeatureFlagListItems.kt +++ b/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/platform/feature/debugmenu/components/FeatureFlagListItems.kt @@ -28,6 +28,7 @@ fun FlagKey.ListItemContent( FlagKey.CredentialExchangeProtocolImport, FlagKey.RemoveCardPolicy, FlagKey.UserManagedPrivilegedApps, + FlagKey.EnrollAeadOnKeyRotation, -> BooleanFlagItem( label = flagKey.getDisplayLabel(), key = flagKey as FlagKey, @@ -76,4 +77,8 @@ private fun FlagKey.getDisplayLabel(): String = when (this) { FlagKey.BitwardenAuthenticationEnabled -> { stringResource(BitwardenString.bitwarden_authentication_enabled) } + + FlagKey.EnrollAeadOnKeyRotation -> { + stringResource(BitwardenString.enroll_aead_on_key_rotation) + } } diff --git a/core/src/main/kotlin/com/bitwarden/core/data/manager/model/FlagKey.kt b/core/src/main/kotlin/com/bitwarden/core/data/manager/model/FlagKey.kt index c2ad8d30f9..477b9a3787 100644 --- a/core/src/main/kotlin/com/bitwarden/core/data/manager/model/FlagKey.kt +++ b/core/src/main/kotlin/com/bitwarden/core/data/manager/model/FlagKey.kt @@ -89,6 +89,14 @@ sealed class FlagKey { override val defaultValue: Boolean = false } + /** + * Represents the feature flag to enable the enrollment of AEAD on key rotation. + */ + data object EnrollAeadOnKeyRotation : FlagKey() { + override val keyName: String = "enroll-aead-on-key-rotation" + override val defaultValue: Boolean = false + } + //region Dummy keys for testing /** * Data object holding the key for a [Boolean] flag to be used in tests. diff --git a/core/src/test/kotlin/com/bitwarden/core/data/manager/model/FlagKeyTest.kt b/core/src/test/kotlin/com/bitwarden/core/data/manager/model/FlagKeyTest.kt index b620912d5a..b2cfb18614 100644 --- a/core/src/test/kotlin/com/bitwarden/core/data/manager/model/FlagKeyTest.kt +++ b/core/src/test/kotlin/com/bitwarden/core/data/manager/model/FlagKeyTest.kt @@ -32,6 +32,10 @@ class FlagKeyTest { FlagKey.BitwardenAuthenticationEnabled.keyName, "bitwarden-authentication-enabled", ) + assertEquals( + FlagKey.EnrollAeadOnKeyRotation.keyName, + "enroll-aead-on-key-rotation", + ) } @Test @@ -44,6 +48,7 @@ class FlagKeyTest { FlagKey.UserManagedPrivilegedApps, FlagKey.RemoveCardPolicy, FlagKey.BitwardenAuthenticationEnabled, + FlagKey.EnrollAeadOnKeyRotation, ).all { !it.defaultValue }, diff --git a/ui/src/main/res/values/strings_non_localized.xml b/ui/src/main/res/values/strings_non_localized.xml index efb1d557ca..0d4dd6a7fd 100644 --- a/ui/src/main/res/values/strings_non_localized.xml +++ b/ui/src/main/res/values/strings_non_localized.xml @@ -28,6 +28,7 @@ 2FAS (no password) LastPass (.json) Aegis (.json) + Enroll AEAD on key rotation From e8f12427441faba03580c7f22ea877d481e82a5e Mon Sep 17 00:00:00 2001 From: David Perez Date: Fri, 8 Aug 2025 13:24:43 -0500 Subject: [PATCH 2/3] Add header and custom supportContent functionality to BitwardenMultiSelectButton (#5669) --- .../dropdown/BitwardenMultiSelectButton.kt | 144 ++++++++++++++++-- .../dropdown/model/MultiSelectOption.kt | 28 ++++ .../appearance/AppearanceScreenTest.kt | 12 +- .../settings/autofill/AutoFillScreenTest.kt | 34 ++--- .../FlightRecorderScreenTest.kt | 10 +- .../feature/settings/other/OtherScreenTest.kt | 15 +- .../feature/generator/GeneratorScreenTest.kt | 11 +- .../VaultMoveToOrganizationScreenTest.kt | 25 +-- .../button/BitwardenTextSelectionButton.kt | 68 +++++++-- 9 files changed, 244 insertions(+), 103 deletions(-) create mode 100644 app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/components/dropdown/model/MultiSelectOption.kt diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/components/dropdown/BitwardenMultiSelectButton.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/components/dropdown/BitwardenMultiSelectButton.kt index 2b638e7917..129a37de12 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/components/dropdown/BitwardenMultiSelectButton.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/components/dropdown/BitwardenMultiSelectButton.kt @@ -1,8 +1,15 @@ package com.x8bit.bitwarden.ui.platform.components.dropdown +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -12,6 +19,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.semantics.Role import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import com.bitwarden.ui.platform.base.util.nullableTestTag import com.bitwarden.ui.platform.components.button.BitwardenTextSelectionButton import com.bitwarden.ui.platform.components.model.CardStyle import com.bitwarden.ui.platform.components.model.TooltipData @@ -19,8 +27,10 @@ import com.bitwarden.ui.platform.theme.BitwardenTheme import com.bitwarden.ui.util.asText import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenSelectionDialog import com.x8bit.bitwarden.ui.platform.components.dialog.row.BitwardenSelectionRow +import com.x8bit.bitwarden.ui.platform.components.dropdown.model.MultiSelectOption import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList /** * A custom composable representing a multi-select button. @@ -32,8 +42,8 @@ import kotlinx.collections.immutable.persistentListOf * @param options A list of strings representing the available options in the dialog. * @param selectedOption The currently selected option that is displayed in the [OutlinedTextField] * (or `null` if no option is selected). - * @param onOptionSelected A lambda that is invoked when an option - * is selected from the dropdown menu. + * @param onOptionSelected A lambda that is invoked when an option is selected from the dropdown + * menu. * @param isEnabled Whether or not the button is enabled. * @param cardStyle Indicates the type of card style to be applied. * @param modifier A [Modifier] that you can use to apply custom modifications to the composable. @@ -61,18 +71,84 @@ fun BitwardenMultiSelectButton( textFieldTestTag: String? = null, actionsPadding: PaddingValues = PaddingValues(end = 4.dp), actions: @Composable RowScope.() -> Unit = {}, +) { + BitwardenMultiSelectButton( + label = label, + options = options.map { MultiSelectOption.Row(it) }.toImmutableList(), + selectedOption = selectedOption?.let { MultiSelectOption.Row(it) }, + onOptionSelected = { onOptionSelected(it.title) }, + cardStyle = cardStyle, + modifier = modifier, + isEnabled = isEnabled, + supportingContent = supportingText?.let { + { + Text( + text = it, + style = BitwardenTheme.typography.bodySmall, + color = BitwardenTheme.colorScheme.text.secondary, + modifier = Modifier.fillMaxWidth(), + ) + } + }, + tooltip = tooltip, + insets = insets, + textFieldTestTag = textFieldTestTag, + actionsPadding = actionsPadding, + actions = actions, + ) +} + +/** + * A custom composable representing a multi-select button. + * + * This composable displays an [OutlinedTextField] with a dropdown icon as a trailing icon. + * When the field is clicked, a dropdown menu appears with a list of options to select from. + * + * @param label The descriptive text label for the [OutlinedTextField]. + * @param options A list of [MultiSelectOption] representing the available options in the dialog. + * @param selectedOption The currently selected option that is displayed in the [OutlinedTextField] + * (or `null` if no option is selected). + * @param onOptionSelected A lambda that is invoked when an option is selected from the dropdown + * menu. + * @param isEnabled Whether or not the button is enabled. + * @param supportingContent An optional supporting content that will appear below the button. + * @param cardStyle Indicates the type of card style to be applied. + * @param modifier A [Modifier] that you can use to apply custom modifications to the composable. + * @param tooltip A nullable [TooltipData], representing the tooltip icon. + * @param insets Inner padding to be applied withing the card. + * @param textFieldTestTag The optional test tag associated with the inner text field. + * @param actionsPadding Padding to be applied to the [actions] block. + * @param actions A lambda containing the set of actions (usually icons or similar) to display + * in the app bar's trailing side. This lambda extends [RowScope], allowing flexibility in + * defining the layout of the actions. + */ +@Composable +fun BitwardenMultiSelectButton( + label: String, + options: ImmutableList, + selectedOption: MultiSelectOption.Row?, + onOptionSelected: (MultiSelectOption.Row) -> Unit, + cardStyle: CardStyle?, + modifier: Modifier = Modifier, + isEnabled: Boolean = true, + supportingContent: @Composable (ColumnScope.() -> Unit)?, + tooltip: TooltipData? = null, + insets: PaddingValues = PaddingValues(), + textFieldTestTag: String? = null, + actionsPadding: PaddingValues = PaddingValues(end = 4.dp), + actions: @Composable RowScope.() -> Unit = {}, ) { var shouldShowDialog by rememberSaveable { mutableStateOf(false) } BitwardenTextSelectionButton( label = label, - selectedOption = selectedOption, + selectedOption = selectedOption?.title, onClick = { shouldShowDialog = true }, cardStyle = cardStyle, enabled = isEnabled, - supportingText = supportingText, + supportingContent = supportingContent, tooltip = tooltip, insets = insets, textFieldTestTag = textFieldTestTag, @@ -87,14 +163,60 @@ fun BitwardenMultiSelectButton( title = label, onDismissRequest = { shouldShowDialog = false }, ) { - options.forEach { optionString -> + BitwardenMultiSelectDialogContent( + options = options, + selectedOption = selectedOption, + onOptionSelected = { selectedItem -> + shouldShowDialog = false + onOptionSelected(selectedItem) + }, + ) + } + } +} + +/** + * Renders the list of items within a multi-select dialog. + * + * This composable is typically used as the content for [BitwardenSelectionDialog]. + * + * @param options A list of strings representing the available options in the dialog. + * @param selectedOption The currently selected option that is displayed in the [OutlinedTextField] + * (or `null` if no option is selected). + * @param onOptionSelected A lambda that is invoked when an option + * is selected from the dropdown menu. + */ +@Composable +fun ColumnScope.BitwardenMultiSelectDialogContent( + options: ImmutableList, + selectedOption: MultiSelectOption.Row?, + onOptionSelected: (MultiSelectOption.Row) -> Unit, +) { + options.forEach { + when (it) { + is MultiSelectOption.Header -> { + Column( + modifier = Modifier + .nullableTestTag(tag = it.testTag) + .padding(horizontal = 16.dp) + .fillMaxWidth(), + ) { + Spacer(modifier = Modifier.height(height = 4.dp)) + Text( + modifier = Modifier.fillMaxWidth(), + text = it.title, + color = BitwardenTheme.colorScheme.text.secondary, + style = BitwardenTheme.typography.titleSmall, + ) + } + } + + is MultiSelectOption.Row -> { BitwardenSelectionRow( - text = optionString.asText(), - isSelected = optionString == selectedOption, - onClick = { - shouldShowDialog = false - onOptionSelected(optionString) - }, + text = it.title.asText(), + isSelected = it == selectedOption, + onClick = { onOptionSelected(it) }, + modifier = Modifier.nullableTestTag(tag = it.testTag), ) } } diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/components/dropdown/model/MultiSelectOption.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/components/dropdown/model/MultiSelectOption.kt new file mode 100644 index 0000000000..7b19ab271a --- /dev/null +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/components/dropdown/model/MultiSelectOption.kt @@ -0,0 +1,28 @@ +package com.x8bit.bitwarden.ui.platform.components.dropdown.model + +/** + * Represents an option in a multi-select list, which can either be a header or a selectable row. + */ +sealed class MultiSelectOption { + /** + * The text to display for the option. + */ + abstract val title: String + + /** + * Represents a header item in a multi-select list. Headers are used to visually group related + * options within the list. + */ + data class Header( + override val title: String, + val testTag: String? = null, + ) : MultiSelectOption() + + /** + * Represents a selectable row item in a multi-select list. + */ + data class Row( + override val title: String, + val testTag: String? = null, + ) : MultiSelectOption() +} diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/appearance/AppearanceScreenTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/appearance/AppearanceScreenTest.kt index 1a031dbd22..4fed63d208 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/appearance/AppearanceScreenTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/appearance/AppearanceScreenTest.kt @@ -124,9 +124,7 @@ class AppearanceScreenTest : BitwardenComposeTest() { @Test fun `on theme row click should display theme selection dialog`() { composeTestRule - .onNodeWithContentDescription( - label = "Default (System). Theme. Change the application's color theme", - ) + .onNodeWithContentDescription(label = "Default (System). Theme") .performScrollTo() .performClick() composeTestRule @@ -138,9 +136,7 @@ class AppearanceScreenTest : BitwardenComposeTest() { @Test fun `on theme selection dialog item click should send ThemeChange`() { composeTestRule - .onNodeWithContentDescription( - label = "Default (System). Theme. Change the application's color theme", - ) + .onNodeWithContentDescription(label = "Default (System). Theme") .performScrollTo() .performClick() composeTestRule @@ -161,9 +157,7 @@ class AppearanceScreenTest : BitwardenComposeTest() { @Test fun `on theme selection dialog cancel click should dismiss dialog`() { composeTestRule - .onNodeWithContentDescription( - label = "Default (System). Theme. Change the application's color theme", - ) + .onNodeWithContentDescription(label = "Default (System). Theme") .performScrollTo() .performClick() composeTestRule diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillScreenTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillScreenTest.kt index 1152cc0265..7792bfcf78 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillScreenTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillScreenTest.kt @@ -240,9 +240,7 @@ class AutoFillScreenTest : BitwardenComposeTest() { } composeTestRule .onNodeWithContentDescription( - label = "Popup (shows over input field). Display autofill suggestions. " + - "Choose how your autofill suggestions will appear when you sign in " + - "to other apps on your device.", + label = "Popup (shows over input field). Display autofill suggestions", ) .performScrollTo() .performClick() @@ -267,18 +265,14 @@ class AutoFillScreenTest : BitwardenComposeTest() { } composeTestRule .onNodeWithContentDescription( - label = "Inline (shows in keyboard). Display autofill suggestions. " + - "Choose how your autofill suggestions will appear when you sign in " + - "to other apps on your device.", + label = "Inline (shows in keyboard). Display autofill suggestions", ) .performScrollTo() .assertIsDisplayed() mutableStateFlow.update { it.copy(autofillStyle = AutofillStyle.POPUP) } composeTestRule .onNodeWithContentDescription( - label = "Popup (shows over input field). Display autofill suggestions. " + - "Choose how your autofill suggestions will appear when you sign in " + - "to other apps on your device.", + label = "Popup (shows over input field). Display autofill suggestions", ) .performScrollTo() .assertIsDisplayed() @@ -292,9 +286,7 @@ class AutoFillScreenTest : BitwardenComposeTest() { composeTestRule .onNodeWithContentDescription( - label = "Inline (shows in keyboard). Display autofill suggestions. " + - "Choose how your autofill suggestions will appear when you sign in " + - "to other apps on your device.", + label = "Inline (shows in keyboard). Display autofill suggestions", ) .performScrollTo() .assertIsDisplayed() @@ -305,9 +297,7 @@ class AutoFillScreenTest : BitwardenComposeTest() { composeTestRule .onNodeWithContentDescription( - label = "Inline (shows in keyboard). Display autofill suggestions. " + - "Choose how your autofill suggestions will appear when you sign in " + - "to other apps on your device.", + label = "Inline (shows in keyboard). Display autofill suggestions", ) .assertDoesNotExist() } @@ -350,9 +340,7 @@ class AutoFillScreenTest : BitwardenComposeTest() { composeTestRule .onNodeWithContentDescription( - label = "Inline (shows in keyboard). Display autofill suggestions. " + - "Choose how your autofill suggestions will appear when you sign in " + - "to other apps on your device.", + label = "Inline (shows in keyboard). Display autofill suggestions", ) .performScrollTo() .assertIsDisplayed() @@ -363,9 +351,7 @@ class AutoFillScreenTest : BitwardenComposeTest() { composeTestRule .onNodeWithContentDescription( - label = "Inline (shows in keyboard). Display autofill suggestions. " + - "Choose how your autofill suggestions will appear when you sign in " + - "to other apps on your device.", + label = "Inline (shows in keyboard). Display autofill suggestions", ) .assertDoesNotExist() } @@ -418,7 +404,7 @@ class AutoFillScreenTest : BitwardenComposeTest() { fun `on default URI match type click should display dialog`() { composeTestRule.assertNoDialogExists() composeTestRule - .onNodeWithContentDescription(label = "Default URI match detection.", substring = true) + .onNodeWithText(text = "Default URI match detection") .performScrollTo() .assert(!hasAnyAncestor(isDialog())) .performClick() @@ -432,7 +418,7 @@ class AutoFillScreenTest : BitwardenComposeTest() { @Test fun `on default URI match type dialog item click should send DefaultUriMatchTypeSelect and close the dialog`() { composeTestRule - .onNodeWithContentDescription(label = "Default URI match detection.", substring = true) + .onNodeWithText(text = "Default URI match detection") .performScrollTo() .performClick() @@ -454,7 +440,7 @@ class AutoFillScreenTest : BitwardenComposeTest() { @Test fun `on default URI match type dialog cancel click should close the dialog`() { composeTestRule - .onNodeWithContentDescription(label = "Default URI match detection.", substring = true) + .onNodeWithText(text = "Default URI match detection") .performScrollTo() .performClick() diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/flightrecorder/FlightRecorderScreenTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/flightrecorder/FlightRecorderScreenTest.kt index 3df1f2d151..a94ff8ca26 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/flightrecorder/FlightRecorderScreenTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/flightrecorder/FlightRecorderScreenTest.kt @@ -1,13 +1,16 @@ package com.x8bit.bitwarden.ui.platform.feature.settings.flightrecorder -import androidx.compose.ui.test.assert +import androidx.compose.ui.test.filterToOne import androidx.compose.ui.test.hasAnyAncestor import androidx.compose.ui.test.isDialog import androidx.compose.ui.test.isDisplayed +import androidx.compose.ui.test.onAllNodesWithText import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.onRoot import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performScrollTo +import androidx.compose.ui.test.printToLog import androidx.core.net.toUri import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow import com.bitwarden.ui.util.assertNoDialogExists @@ -83,13 +86,14 @@ class FlightRecorderScreenTest : BitwardenComposeTest() { @Test fun `on logging duration click should display select dialog`() { composeTestRule.assertNoDialogExists() + composeTestRule.onRoot().printToLog("BRAIN") composeTestRule .onNodeWithContentDescription(label = "1 hour. Logging duration") .performScrollTo() .performClick() composeTestRule - .onNodeWithText(text = "Logging duration") - .assert(hasAnyAncestor(isDialog())) + .onAllNodesWithText(text = "Logging duration") + .filterToOne(hasAnyAncestor(isDialog())) .isDisplayed() } diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/other/OtherScreenTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/other/OtherScreenTest.kt index fc21b65086..95b8a7184c 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/other/OtherScreenTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/other/OtherScreenTest.kt @@ -97,10 +97,7 @@ class OtherScreenTest : BitwardenComposeTest() { @Test fun `on clear clipboard row click should show show clipboard selection dialog`() { composeTestRule - .onNodeWithContentDescription( - label = "Never. Clear clipboard. " + - "Automatically clear copied values from your clipboard.", - ) + .onNodeWithContentDescription(label = "Never. Clear clipboard") .performScrollTo() .performClick() composeTestRule @@ -112,10 +109,7 @@ class OtherScreenTest : BitwardenComposeTest() { @Test fun `on clear clipboard dialog item click should send ClearClipboardFrequencyChange`() { composeTestRule - .onNodeWithContentDescription( - label = "Never. Clear clipboard. " + - "Automatically clear copied values from your clipboard.", - ) + .onNodeWithContentDescription(label = "Never. Clear clipboard") .performScrollTo() .performClick() composeTestRule @@ -136,10 +130,7 @@ class OtherScreenTest : BitwardenComposeTest() { @Test fun `on clear clipboard dialog cancel should dismiss dialog`() { composeTestRule - .onNodeWithContentDescription( - label = "Never. Clear clipboard. " + - "Automatically clear copied values from your clipboard.", - ) + .onNodeWithContentDescription(label = "Never. Clear clipboard") .performScrollTo() .performClick() composeTestRule.onNodeWithText("Cancel").performClick() diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorScreenTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorScreenTest.kt index 500a0dd375..7d85e2a570 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorScreenTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorScreenTest.kt @@ -229,7 +229,6 @@ class GeneratorScreenTest : BitwardenComposeTest() { .assertDoesNotExist() } - @Suppress("MaxLineLength") @Test fun `clicking a UsernameOption should send UsernameTypeOption action`() { updateState( @@ -242,9 +241,7 @@ class GeneratorScreenTest : BitwardenComposeTest() { // Opens the menu composeTestRule - .onNodeWithContentDescription( - label = "Plus addressed email. Username type. Use your email provider's subaddress capabilities", - ) + .onNodeWithContentDescription(label = "Plus addressed email. Username type") .performClick() // Choose the option from the menu @@ -1459,12 +1456,8 @@ class GeneratorScreenTest : BitwardenComposeTest() { fun `in Username state, clicking the tooltip icon should send the TooltipClick action`() { updateState(DEFAULT_STATE.copy(selectedType = GeneratorState.MainType.Username())) - @Suppress("MaxLineLength") composeTestRule - .onNodeWithContentDescription( - label = "Plus addressed email. Username type. Use your email provider's subaddress capabilities", - useUnmergedTree = true, - ) + .onNodeWithContentDescription(label = "Plus addressed email. Username type") // Find the button .onChildren() .filterToOne(hasClickAction()) diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/movetoorganization/VaultMoveToOrganizationScreenTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/movetoorganization/VaultMoveToOrganizationScreenTest.kt index fdc6d8eb27..5964446103 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/movetoorganization/VaultMoveToOrganizationScreenTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/movetoorganization/VaultMoveToOrganizationScreenTest.kt @@ -121,7 +121,7 @@ class VaultMoveToOrganizationScreenTest : BitwardenComposeTest() { @Test fun `the organization option field description should update according to state`() { composeTestRule - .onNodeWithContentDescription(label = "Choose an organization that", substring = true) + .onNodeWithText(text = "Choose an organization that", substring = true) .assertIsDisplayed() mutableStateFlow.update { currentState -> @@ -130,7 +130,7 @@ class VaultMoveToOrganizationScreenTest : BitwardenComposeTest() { composeTestRule composeTestRule - .onNodeWithContentDescription(label = "Choose an organization that", substring = true) + .onNodeWithText(text = "Choose an organization that", substring = true) .assertIsNotDisplayed() } @@ -169,12 +169,7 @@ class VaultMoveToOrganizationScreenTest : BitwardenComposeTest() { @Test fun `selecting an organization should send OrganizationSelect action`() { composeTestRule - .onNodeWithContentDescriptionAfterScroll( - label = "mockOrganizationName-1. Organization. " + - "Choose an organization that you wish to move this item to. Moving to an " + - "organization transfers ownership of the item to that organization. You " + - "will no longer be the direct owner of this item once it has been moved.", - ) + .onNodeWithContentDescriptionAfterScroll(label = "mockOrganizationName-1. Organization") .performClick() // Choose the option from the menu composeTestRule @@ -205,12 +200,7 @@ class VaultMoveToOrganizationScreenTest : BitwardenComposeTest() { @Test fun `the organization option field should display according to state`() { composeTestRule - .onNodeWithContentDescriptionAfterScroll( - label = "mockOrganizationName-1. Organization. " + - "Choose an organization that you wish to move this item to. Moving to an " + - "organization transfers ownership of the item to that organization. You " + - "will no longer be the direct owner of this item once it has been moved.", - ) + .onNodeWithContentDescriptionAfterScroll(label = "mockOrganizationName-1. Organization") .assertIsDisplayed() mutableStateFlow.update { currentState -> @@ -223,12 +213,7 @@ class VaultMoveToOrganizationScreenTest : BitwardenComposeTest() { } composeTestRule - .onNodeWithContentDescriptionAfterScroll( - label = "mockOrganizationName-2. Organization. " + - "Choose an organization that you wish to move this item to. Moving to an " + - "organization transfers ownership of the item to that organization. You " + - "will no longer be the direct owner of this item once it has been moved.", - ) + .onNodeWithContentDescriptionAfterScroll(label = "mockOrganizationName-2. Organization") .assertIsDisplayed() } diff --git a/ui/src/main/kotlin/com/bitwarden/ui/platform/components/button/BitwardenTextSelectionButton.kt b/ui/src/main/kotlin/com/bitwarden/ui/platform/components/button/BitwardenTextSelectionButton.kt index 069158cf32..718bbb0f9d 100644 --- a/ui/src/main/kotlin/com/bitwarden/ui/platform/components/button/BitwardenTextSelectionButton.kt +++ b/ui/src/main/kotlin/com/bitwarden/ui/platform/components/button/BitwardenTextSelectionButton.kt @@ -2,6 +2,7 @@ package com.bitwarden.ui.platform.components.button import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope @@ -20,10 +21,10 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.semantics.CustomAccessibilityAction import androidx.compose.ui.semantics.Role -import androidx.compose.ui.semantics.clearAndSetSemantics import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.customActions import androidx.compose.ui.semantics.role +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -42,7 +43,6 @@ import com.bitwarden.ui.platform.theme.BitwardenTheme /** * A button which uses a read-only text field for layout and style purposes. */ -@Suppress("LongMethod") @Composable fun BitwardenTextSelectionButton( label: String, @@ -58,15 +58,60 @@ fun BitwardenTextSelectionButton( semanticRole: Role = Role.Button, actionsPadding: PaddingValues = PaddingValues(end = 4.dp), actions: @Composable RowScope.() -> Unit = {}, +) { + BitwardenTextSelectionButton( + label = label, + selectedOption = selectedOption, + onClick = onClick, + cardStyle = cardStyle, + modifier = modifier, + enabled = enabled, + tooltip = tooltip, + insets = insets, + textFieldTestTag = textFieldTestTag, + semanticRole = semanticRole, + actionsPadding = actionsPadding, + actions = actions, + supportingContent = supportingText?.let { + { + Text( + text = it, + style = BitwardenTheme.typography.bodySmall, + color = BitwardenTheme.colorScheme.text.secondary, + modifier = Modifier.fillMaxWidth(), + ) + } + }, + ) +} + +/** + * + * A button which uses a read-only text field for layout and style purposes. + */ +@Suppress("LongMethod") +@Composable +fun BitwardenTextSelectionButton( + label: String, + selectedOption: String?, + onClick: () -> Unit, + cardStyle: CardStyle?, + modifier: Modifier = Modifier, + enabled: Boolean = true, + tooltip: TooltipData? = null, + insets: PaddingValues = PaddingValues(), + textFieldTestTag: String? = null, + semanticRole: Role = Role.Button, + actionsPadding: PaddingValues = PaddingValues(end = 4.dp), + supportingContent: @Composable (ColumnScope.() -> Unit)?, + actions: @Composable RowScope.() -> Unit = {}, ) { Column( modifier = modifier .defaultMinSize(minHeight = 60.dp) - .clearAndSetSemantics { + .semantics { role = semanticRole - contentDescription = supportingText - ?.let { "$selectedOption. $label. $it" } - ?: "$selectedOption. $label" + contentDescription = "$selectedOption. $label" customActions = persistentListOfNotNull( tooltip?.let { CustomAccessibilityAction( @@ -132,7 +177,7 @@ fun BitwardenTextSelectionButton( .nullableTestTag(tag = textFieldTestTag) .fillMaxWidth(), ) - supportingText + supportingContent ?.let { content -> Spacer(modifier = Modifier.height(height = 6.dp)) BitwardenHorizontalDivider( @@ -145,14 +190,7 @@ fun BitwardenTextSelectionButton( modifier = Modifier .defaultMinSize(minHeight = 48.dp) .padding(vertical = 12.dp, horizontal = 16.dp), - content = { - Text( - text = content, - style = BitwardenTheme.typography.bodySmall, - color = BitwardenTheme.colorScheme.text.secondary, - modifier = Modifier.fillMaxWidth(), - ) - }, + content = content, ) } ?: Spacer(modifier = Modifier.height(height = cardStyle?.let { 6.dp } ?: 0.dp)) From 9b120701ebcdcd6b9663e75e52624ff2326a7f68 Mon Sep 17 00:00:00 2001 From: Matt Andreko Date: Fri, 8 Aug 2025 16:58:04 -0400 Subject: [PATCH 3/3] Fix reusable scan in CI build (#5668) --- .github/workflows/scan-ci.yml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/.github/workflows/scan-ci.yml b/.github/workflows/scan-ci.yml index 324073cab5..11fe939e15 100644 --- a/.github/workflows/scan-ci.yml +++ b/.github/workflows/scan-ci.yml @@ -9,16 +9,9 @@ on: permissions: {} jobs: - check-run: - name: Check PR run - uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main - permissions: - contents: read - sast: name: Checkmarx uses: bitwarden/gh-actions/.github/workflows/_checkmarx.yml@main - needs: check-run secrets: AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} @@ -32,7 +25,6 @@ jobs: quality: name: Sonar uses: bitwarden/gh-actions/.github/workflows/_sonar.yml@main - needs: check-run secrets: AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}