From 6c7a126d641700b0f65c57e3c21b0f41fb1f35e6 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Mon, 24 Aug 2026 09:47:09 +1000 Subject: [PATCH 1/2] feat: add QA test tags to the shared Open URL confirmation dialog The e2e suite (session-appium) could not assert anything about the Open URL confirmation dialog, because it carried no ids of its own: its body only had the generic AccessibilityId_modalMessage that every modal shares, and neither the dialog root nor the "Open" button had anything at all. A spec could not tell this dialog apart from any other modal, let alone assert WHICH url was about to be opened. Tagging it here rather than at a call site covers every urlOpen confirmation in the app at once - OpenURLAlertDialog is shared by settings, the token page, the Pro settings dialogs, in-app review, the start-conversation sheet and the invite screen. The strings match the ones the Desktop client uses, so the harness needs one locator rather than one per platform (open-url-confirm-button already exists in session-desktop's OpenUrlModal). AlertDialogContent hardcoded the modal-message id, so the body could not be tagged by a caller. Rather than restructure the component, this adds an optional, defaulted textQaTag parameter threaded through both AlertDialog overloads, falling back to AccessibilityId_modalMessage when absent - so every other dialog is unchanged. This matches the existing convention in Components.kt (@StringRes textQaTag with a `?:` fallback). The "Copy URL" button is deliberately left untagged: the harness's CopyURLButton locator finds it by its "Copy URL" label, which is the fallback DialogButtonData uses when no qaTag is given. Adding a testTag would change its resource-id and break review_triggers.spec.ts. Co-Authored-By: Claude Opus 5 --- .../thoughtcrime/securesms/ui/dialog/AlertDialog.kt | 8 +++++++- .../org/thoughtcrime/securesms/ui/dialog/LinkDialog.kt | 5 ++++- content-descriptions/src/main/res/values/strings.xml | 10 ++++++++++ 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/dialog/AlertDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/dialog/AlertDialog.kt index f8fcd2032d..248801a01f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/dialog/AlertDialog.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/dialog/AlertDialog.kt @@ -1,6 +1,7 @@ package org.thoughtcrime.securesms.ui.dialog import android.widget.Toast +import androidx.annotation.StringRes import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.layout.Box @@ -97,6 +98,7 @@ fun AlertDialog( title: String? = null, text: String? = null, maxLines: Int? = null, + @StringRes textQaTag: Int? = null, buttons: List? = null, showCloseButton: Boolean = false, content: @Composable () -> Unit = {} @@ -107,6 +109,7 @@ fun AlertDialog( title = if(title != null) AnnotatedString(title) else null, text = if(text != null) AnnotatedString(text) else null, maxLines = maxLines, + textQaTag = textQaTag, buttons = buttons, showCloseButton = showCloseButton, content = content @@ -121,6 +124,7 @@ fun AlertDialog( title: AnnotatedString? = null, text: AnnotatedString? = null, maxLines: Int? = null, + @StringRes textQaTag: Int? = null, buttons: List? = null, showCloseButton: Boolean = false, content: @Composable () -> Unit = {} @@ -134,6 +138,7 @@ fun AlertDialog( title = title, text = text, maxLines = maxLines, + textQaTag = textQaTag, buttons = buttons, showCloseButton = showCloseButton, content = content @@ -178,6 +183,7 @@ fun AlertDialogContent( title: AnnotatedString? = null, text: AnnotatedString? = null, maxLines: Int? = null, + @StringRes textQaTag: Int? = null, buttons: List? = null, showCloseButton: Boolean = false, content: @Composable () -> Unit = {} @@ -235,7 +241,7 @@ fun AlertDialogContent( textAlign = TextAlign.Center, style = textStyle, modifier = textModifier - .qaTag(R.string.AccessibilityId_modalMessage) + .qaTag(textQaTag ?: R.string.AccessibilityId_modalMessage) ) } content() diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/dialog/LinkDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/dialog/LinkDialog.kt index 437a5c8a26..97f9392263 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/dialog/LinkDialog.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/dialog/LinkDialog.kt @@ -17,6 +17,7 @@ import org.thoughtcrime.securesms.links.LinkType.CommunityLink.DisplayType.* import org.thoughtcrime.securesms.ui.GetString import org.thoughtcrime.securesms.ui.components.annotatedStringResource import org.thoughtcrime.securesms.ui.openUrl +import org.thoughtcrime.securesms.ui.qaTag import org.thoughtcrime.securesms.ui.theme.LocalColors import org.thoughtcrime.securesms.ui.theme.PreviewTheme @@ -67,14 +68,16 @@ fun OpenURLAlertDialog( AlertDialog( - modifier = modifier, + modifier = modifier.qaTag(R.string.qa_open_url_dialog), title = AnnotatedString(stringResource(R.string.urlOpen)), text = annotatedStringResource(text = unformattedText), + textQaTag = R.string.qa_open_url_description, maxLines = 5, showCloseButton = true, // display the 'x' button buttons = listOf( DialogButtonData( text = GetString(R.string.open), + qaTag = stringResource(R.string.qa_open_url_confirm_button), color = LocalColors.current.danger, dismissOnClick = false, onClick = { diff --git a/content-descriptions/src/main/res/values/strings.xml b/content-descriptions/src/main/res/values/strings.xml index c2f9b88846..3df9c07290 100644 --- a/content-descriptions/src/main/res/values/strings.xml +++ b/content-descriptions/src/main/res/values/strings.xml @@ -440,4 +440,14 @@ equivalent control there is not reachable, and its specs locate the affordance differently. --> message-read-more + + open-url-dialog + open-url-description + open-url-confirm-button + \ No newline at end of file From 8baca3ed6c588d6bf55d3d0105c941ecde808fd3 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Mon, 24 Aug 2026 11:02:21 +1000 Subject: [PATCH 2/2] feat: tag the Pro screen action's label, not just its button A `qaTag` on the primary action covers a node whose own `text` is empty: Compose puts the copy on an untagged child TextView, so a UI test filtering that button by its text matches nothing. Confirmed from a device page-source dump - the `pro-screen-action` node reports `text=""` with a `android.widget.TextView text="Request Refund"` child. That matters because the label is what distinguishes the refund states from each other: the store's window being open or closed changes the CTA from "Open {platform} Website" to "Request Refund", and that is the difference a user can act on. iOS already exposes its label, so the e2e suite could assert this on one platform and not the other. Threads an optional `textQaTag` down to the label, defaulted to null so every existing caller is unchanged, and applies it on the Pro screens' primary action. Same shape as the `textQaTag` this branch already added to `AlertDialogContent`. Co-Authored-By: Claude Opus 5 --- .../prosettings/BaseProSettingsScreens.kt | 2 ++ .../securesms/ui/components/Button.kt | 17 ++++++++++++----- .../src/main/res/values/strings.xml | 4 ++++ 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/BaseProSettingsScreens.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/BaseProSettingsScreens.kt index fd56b19de8..3712528add 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/BaseProSettingsScreens.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/BaseProSettingsScreens.kt @@ -200,6 +200,7 @@ fun BaseCellButtonProSettingsScreen( .widthIn(max = LocalDimensions.current.maxContentWidth) .qaTag(R.string.qa_pro_screen_action), text = buttonText, + textQaTag = R.string.qa_pro_screen_action_label, onClick = onButtonClick ) } else { @@ -208,6 +209,7 @@ fun BaseCellButtonProSettingsScreen( .widthIn(max = LocalDimensions.current.maxContentWidth) .qaTag(R.string.qa_pro_screen_action), text = buttonText, + textQaTag = R.string.qa_pro_screen_action_label, onClick = onButtonClick ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/components/Button.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/components/Button.kt index d54ceceb30..fda859b6d0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/components/Button.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/components/Button.kt @@ -39,6 +39,7 @@ import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.filter import network.loki.messenger.R import org.thoughtcrime.securesms.ui.LaunchedEffectAsync +import androidx.annotation.StringRes import org.thoughtcrime.securesms.ui.qaTag import org.thoughtcrime.securesms.ui.theme.LocalColors import org.thoughtcrime.securesms.ui.theme.LocalDimensions @@ -106,9 +107,13 @@ fun Button( shape: Shape = buttonShape, minWidth: Dp = LocalDimensions.current.minButtonWidth, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + /// Tags the label rather than the button. A `qaTag` on the button covers a node whose own `text` is + /// empty - the copy sits on this child - so a UI test that filters a button by its text matches + /// nothing. Null leaves the label untagged, which is every existing caller. + @StringRes textQaTag: Int? = null, ) { Button(onClick, type, modifier, enabled, style, shape, minWidth = minWidth, interactionSource = interactionSource) { - Text(text) + Text(text, modifier = Modifier.qaTag(textQaTag)) } } @@ -128,11 +133,12 @@ fun Button( ) } -@Composable fun AccentFillButtonRect(text: String, modifier: Modifier = Modifier, enabled: Boolean = true, onClick: () -> Unit) { +@Composable fun AccentFillButtonRect(text: String, modifier: Modifier = Modifier, enabled: Boolean = true, @StringRes textQaTag: Int? = null, onClick: () -> Unit) { Button( text, onClick, ButtonType.AccentFill, modifier, enabled, style = ButtonStyle.XLarge, - shape = sessionShapes().extraSmall + shape = sessionShapes().extraSmall, + textQaTag = textQaTag ) } @@ -144,11 +150,12 @@ fun Button( ) } -@Composable fun DangerFillButtonRect(text: String, modifier: Modifier = Modifier, enabled: Boolean = true, onClick: () -> Unit) { +@Composable fun DangerFillButtonRect(text: String, modifier: Modifier = Modifier, enabled: Boolean = true, @StringRes textQaTag: Int? = null, onClick: () -> Unit) { Button( text, onClick, ButtonType.DangerFill, modifier, enabled, style = ButtonStyle.XLarge, - shape = sessionShapes().extraSmall + shape = sessionShapes().extraSmall, + textQaTag = textQaTag ) } diff --git a/content-descriptions/src/main/res/values/strings.xml b/content-descriptions/src/main/res/values/strings.xml index 3df9c07290..5d64208b91 100644 --- a/content-descriptions/src/main/res/values/strings.xml +++ b/content-descriptions/src/main/res/values/strings.xml @@ -411,6 +411,10 @@ pro-screen-title pro-screen-description pro-screen-action + + pro-screen-action-label pro-link-cell-device