diff --git a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/components/ChatTopBar.kt b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/components/ChatTopBar.kt index 0bfe253e4..65df4cc67 100644 --- a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/components/ChatTopBar.kt +++ b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/components/ChatTopBar.kt @@ -33,6 +33,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp @@ -322,7 +323,12 @@ private fun MessageOverflow(actions: List) { var expanded by remember { mutableStateOf(false) } Box { - AppBarDefaults.Overflow(onClick = { expanded = true }) + // Which actions end up under here falls out of the width, so a UI test cannot know whether + // to reach for the icon or the menu. Tagging the button lets it ask. + AppBarDefaults.Overflow( + modifier = Modifier.testTag("action_message_overflow"), + onClick = { expanded = true }, + ) DropdownMenu( expanded = expanded, containerColor = CodeTheme.colors.brandLight, diff --git a/maestro/README.md b/maestro/README.md index 6219513c9..36313d1da 100644 --- a/maestro/README.md +++ b/maestro/README.md @@ -91,6 +91,11 @@ Two anchors exist purely for the tests and are worth knowing about: otherwise be unaddressable. They carry `nav_scanner`, `nav_wallet`, `nav_chats` and `nav_tipcard` (`NavigationBar.kt`). Tabs are *replaced* on a single root back stack, so Back never unwinds between them — `helpers/close_open_sheet.yaml` is how flows get home. +- **`action_message_overflow`** is on the message selection bar's overflow button. The bar keeps + as many actions inline as its width budget allows and puts the rest behind that button, so a + test cannot know from the flow whether a given action is an icon or a menu item. + `subflows/assert_message_action.yaml` and `subflows/tap_message_action.yaml` probe the icon + first and fall back to the menu, which is why both take an `ACTION_ID` and an `ACTION_LABEL`. - **`token_info_screen`** is on both the pushed currency-info screen *and* the expanded-card overlay (`CurrencyInfoExpansion.kt`). Tapping a card in the wallet expands it in place rather than pushing a screen, so the overlay carries the same anchor and flows don't care which @@ -132,6 +137,12 @@ maestro/run.sh maestro/tipping_setup.yaml - `deposit.yaml` — menu → Add Money → Other Wallet → USDC deposit (fund-safe) - `tipping_setup.yaml` — create account → set up tip card → tip card renders - `tip_chat.yaml` — open the tip conversation from the Chats tab and send a message +- `chat_message_selection.yaml` — long-press a sent message: the selection bar offers Delete, + Copy and Edit, and both the back arrow and the Back gesture drop it without touching the message +- `chat_message_edit.yaml` — edit a sent message: the composer swaps send for cancel/confirm, and + the unsent draft the edit displaced comes back on cancel and on confirm alike +- `chat_message_delete.yaml` — delete for everyone: the confirmation sheet, Back leaving the + message alone, and confirming taking it out of the transcript - `blocking.yaml` — block a chat participant from their profile, verify in My Account → Blocked, then unblock (leaves the account clean) - `tip_deeplink.yaml` — open a tip-card deeplink (`TIPCARD_DEEPLINK`) → presents the tip flow diff --git a/maestro/chat_message_delete.yaml b/maestro/chat_message_delete.yaml new file mode 100644 index 000000000..8faf1edce --- /dev/null +++ b/maestro/chat_message_delete.yaml @@ -0,0 +1,55 @@ +appId: com.flipcash.app.android +name: "Chat — delete a message for everyone" +tags: + - smoke + - chat +--- +# Delete is the one message action behind a confirmation. Closing the sheet ends the selection +# either way, so the flow checks both the cancel and the confirm. +# +# Fund-safe, and self-cleaning: the message it deletes is the one it sent. +- runFlow: + file: subflows/login_with_flags.yaml + env: + BETA_FLAGS: "" +- runFlow: subflows/open_tip_chat.yaml + +- evalScript: ${output.body = 'e2e delete ' + Date.now()} +- tapOn: { id: chat_message_input } +- inputText: "${output.body}" +- tapOn: { id: chat_send_icon } +- extendedWaitUntil: { visible: "${output.body}", timeout: 10000 } + +# Backing out of the confirmation leaves the message alone — and drops the selection with it, +# so the user isn't left behind the sheet with a bar they just backed out of. +- runFlow: + file: subflows/select_own_message.yaml + env: + MESSAGE_BODY: "${output.body}" +- runFlow: + file: subflows/tap_message_action.yaml + env: + ACTION_ID: action_delete_message + ACTION_LABEL: "Delete" +- extendedWaitUntil: { visible: "Delete Message?", timeout: 5000 } +- assertVisible: "This can't be undone" +- back +- assertNotVisible: "Delete Message?" +- assertNotVisible: { id: action_clear_message_selection } +- assertVisible: "${output.body}" + +# Confirming takes it out of the transcript. +- runFlow: + file: subflows/select_own_message.yaml + env: + MESSAGE_BODY: "${output.body}" +- runFlow: + file: subflows/tap_message_action.yaml + env: + ACTION_ID: action_delete_message + ACTION_LABEL: "Delete" +- extendedWaitUntil: { visible: "Delete For Everyone", timeout: 5000 } +- tapOn: "Delete For Everyone" +- extendedWaitUntil: { notVisible: "${output.body}", timeout: 10000 } +- assertNotVisible: { id: action_clear_message_selection } +- assertVisible: { id: chat_screen } diff --git a/maestro/chat_message_edit.yaml b/maestro/chat_message_edit.yaml new file mode 100644 index 000000000..3d0576fc0 --- /dev/null +++ b/maestro/chat_message_edit.yaml @@ -0,0 +1,75 @@ +appId: com.flipcash.app.android +name: "Chat — edit a message, and the draft it interrupts" +tags: + - smoke + - chat +--- +# Editing takes the composer over: the leading control becomes cancel, the send glyph becomes a +# checkmark, and whatever the composer held is stashed and handed back when the edit ends. +# +# Fund-safe. The edit runs against a message this flow just sent, so it stays inside +# `message_edit_window` if the server sets one; a very short window would make this flaky here +# and unusable in the app. +- runFlow: + file: subflows/login_with_flags.yaml + env: + BETA_FLAGS: "" +- runFlow: subflows/open_tip_chat.yaml + +- evalScript: ${output.body = 'e2e edit ' + Date.now()} +- evalScript: ${output.draft = 'e2e draft ' + Date.now()} +- tapOn: { id: chat_message_input } +- inputText: "${output.body}" +- tapOn: { id: chat_send_icon } +- extendedWaitUntil: { visible: "${output.body}", timeout: 10000 } + +# An unsent draft in the composer, for the edit to displace. +- tapOn: { id: chat_message_input } +- inputText: "${output.draft}" +- assertVisible: "${output.draft}" + +# Starting an edit swaps the bar for the edit controls and stashes the draft. The draft text is +# the discriminating assertion: the message body is in the transcript either way. +- runFlow: + file: subflows/select_own_message.yaml + env: + MESSAGE_BODY: "${output.body}" +- runFlow: + file: subflows/tap_message_action.yaml + env: + ACTION_ID: action_edit_message + ACTION_LABEL: "Edit" +- assertVisible: { id: chat_cancel_edit } +- assertVisible: { id: chat_confirm_edit_icon } +- assertNotVisible: { id: chat_send_icon } +- assertNotVisible: "${output.draft}" + +# Cancelling hands the draft back untouched. +- tapOn: { id: chat_cancel_edit } +- assertNotVisible: { id: chat_cancel_edit } +- assertVisible: "${output.draft}" +- assertVisible: { id: chat_send_icon } + +# Edit for real this time. The composer already claims focus, so the text goes straight in. +- runFlow: + file: subflows/select_own_message.yaml + env: + MESSAGE_BODY: "${output.body}" +- runFlow: + file: subflows/tap_message_action.yaml + env: + ACTION_ID: action_edit_message + ACTION_LABEL: "Edit" +- inputText: " edited" +- tapOn: { id: chat_confirm_edit_icon } + +# The transcript carries the new body, the composer is back to sending, and the draft survived +# the confirm as well as it survived the cancel. +# +# The body needs a trailing `.*`: an edited bubble appends an inline placeholder to reserve room +# for the "Edited" marker, and the placeholder's alternate text — a figure space — lands in the +# accessibility text. Maestro matches the whole string, so the body on its own misses. +- extendedWaitUntil: { visible: "${output.body} edited.*", timeout: 10000 } +- assertNotVisible: { id: chat_cancel_edit } +- assertVisible: { id: chat_send_icon } +- assertVisible: "${output.draft}" diff --git a/maestro/chat_message_selection.yaml b/maestro/chat_message_selection.yaml new file mode 100644 index 000000000..79666d309 --- /dev/null +++ b/maestro/chat_message_selection.yaml @@ -0,0 +1,63 @@ +appId: com.flipcash.app.android +name: "Chat — long-press selection bar" +tags: + - smoke + - chat +--- +# The bar a long-press puts up over the conversation title: it offers the actions the message +# allows, and both the back arrow and the Back gesture drop it without touching the message. +# +# Fund-safe. Leaves the message it sends in the transcript, like tip_chat.yaml. +- runFlow: + file: subflows/login_with_flags.yaml + env: + BETA_FLAGS: "" +- runFlow: subflows/open_tip_chat.yaml + +# Stamped per run so a run that aborts part-way can't leave behind a message a later run +# long-presses instead of its own. +- evalScript: ${output.body = 'e2e select ' + Date.now()} +- tapOn: { id: chat_message_input } +- inputText: "${output.body}" +- tapOn: { id: chat_send_icon } +- extendedWaitUntil: { visible: "${output.body}", timeout: 10000 } + +# The composer keeps focus after a send, and an open IME swallows the Back gesture below before +# the conversation's handler ever sees it. A tap on the transcript is how the app itself puts the +# keyboard away, so use that rather than spending a Back on it. +- tapOn: "${output.body}" + +# Own confirmed text: copy, edit and delete, whether inline or under the overflow. +- runFlow: + file: subflows/select_own_message.yaml + env: + MESSAGE_BODY: "${output.body}" +- runFlow: + file: subflows/assert_message_action.yaml + env: + ACTION_ID: action_delete_message + ACTION_LABEL: "Delete" +- runFlow: + file: subflows/assert_message_action.yaml + env: + ACTION_ID: action_copy_message + ACTION_LABEL: "Copy" +- runFlow: + file: subflows/assert_message_action.yaml + env: + ACTION_ID: action_edit_message + ACTION_LABEL: "Edit" + +# The back arrow drops the selection and leaves the message alone. +- tapOn: { id: action_clear_message_selection } +- assertNotVisible: { id: action_clear_message_selection } +- assertVisible: "${output.body}" + +# Back is registered inside the conversation, so it unwinds the bar rather than popping the chat. +- runFlow: + file: subflows/select_own_message.yaml + env: + MESSAGE_BODY: "${output.body}" +- back +- assertNotVisible: { id: action_clear_message_selection } +- assertVisible: { id: chat_screen } diff --git a/maestro/subflows/assert_message_action.yaml b/maestro/subflows/assert_message_action.yaml new file mode 100644 index 000000000..9f7867cd7 --- /dev/null +++ b/maestro/subflows/assert_message_action.yaml @@ -0,0 +1,20 @@ +# Asserts one selection-bar action is offered, wherever the width put it. +# Requires env: ACTION_ID, ACTION_LABEL. Leaves the bar as it found it. +appId: com.flipcash.app.android +--- +- evalScript: ${output.actionWasInline = false} +- runFlow: + when: + visible: + id: "${ACTION_ID}" + commands: + - evalScript: ${output.actionWasInline = true} +- runFlow: + when: + true: ${output.actionWasInline == false} + commands: + # The menu is the only place left it could be, so opening it is the assertion. + - tapOn: { id: action_message_overflow } + - assertVisible: "${ACTION_LABEL}" + - back + - assertVisible: { id: action_clear_message_selection } diff --git a/maestro/subflows/open_tip_chat.yaml b/maestro/subflows/open_tip_chat.yaml new file mode 100644 index 000000000..f29a8bfa4 --- /dev/null +++ b/maestro/subflows/open_tip_chat.yaml @@ -0,0 +1,14 @@ +# Chats tab → the contact's tip conversation. Requires env: CONTACT_NAME. +appId: com.flipcash.app.android +--- +# The list is ordered by recency and carries the account's own self-tip chat, so index 0 is not +# stably the contact — open it by name. +- tapOn: { id: nav_chats } +- extendedWaitUntil: { visible: { id: tips_screen }, timeout: 8000 } +- assertVisible: { id: send_contact_row } +- scrollUntilVisible: + element: "${CONTACT_NAME}" + direction: DOWN +- tapOn: "${CONTACT_NAME}" +- extendedWaitUntil: { visible: { id: chat_screen }, timeout: 8000 } +- assertVisible: { id: chat_message_input } diff --git a/maestro/subflows/select_own_message.yaml b/maestro/subflows/select_own_message.yaml new file mode 100644 index 000000000..86b271266 --- /dev/null +++ b/maestro/subflows/select_own_message.yaml @@ -0,0 +1,12 @@ +# Long-presses a message and waits for the selection bar. Requires env: MESSAGE_BODY. +# +# Retried because a just-sent message is not selectable yet: resolveCapabilities returns nothing +# while eventSequence is 0, and the text is in the transcript before the server acknowledges it. +# The long-press is a one-shot, so waiting has to wrap the press rather than follow it. +appId: com.flipcash.app.android +--- +- retry: + maxRetries: 4 + commands: + - longPressOn: "${MESSAGE_BODY}" + - assertVisible: { id: action_clear_message_selection } diff --git a/maestro/subflows/tap_message_action.yaml b/maestro/subflows/tap_message_action.yaml new file mode 100644 index 000000000..855f8ad8f --- /dev/null +++ b/maestro/subflows/tap_message_action.yaml @@ -0,0 +1,22 @@ +# Taps one selection-bar action wherever it ended up. Requires env: ACTION_ID, ACTION_LABEL. +# +# The bar keeps as many actions inline as its width budget allows and puts the rest under an +# overflow, so which of the two a given action is behind depends on the screen. The result of the +# visibility check is latched before the tap, because tapping the action closes the bar and would +# otherwise make a second `notVisible` branch fire as well. +appId: com.flipcash.app.android +--- +- evalScript: ${output.actionWasInline = false} +- runFlow: + when: + visible: + id: "${ACTION_ID}" + commands: + - evalScript: ${output.actionWasInline = true} + - tapOn: { id: "${ACTION_ID}" } +- runFlow: + when: + true: ${output.actionWasInline == false} + commands: + - tapOn: { id: action_message_overflow } + - tapOn: "${ACTION_LABEL}" diff --git a/maestro/tip_chat.yaml b/maestro/tip_chat.yaml index 2251af2d1..28ef5466c 100644 --- a/maestro/tip_chat.yaml +++ b/maestro/tip_chat.yaml @@ -8,25 +8,18 @@ tags: file: subflows/login_with_flags.yaml env: BETA_FLAGS: "" - -# The chats tab lists tip conversations. Open the contact's by name: the list is ordered by -# recency and carries the account's own self-tip chat, so index 0 is not stably the contact. -- tapOn: { id: nav_chats } -- extendedWaitUntil: { visible: { id: tips_screen }, timeout: 8000 } -- assertVisible: { id: send_contact_row } -- scrollUntilVisible: - element: "${CONTACT_NAME}" - direction: DOWN -- tapOn: "${CONTACT_NAME}" - -# In the tip conversation: a tip event and the message composer are present. Which way the tip -# went is live data, so match either direction. -- extendedWaitUntil: { visible: { id: chat_screen }, timeout: 8000 } -- assertVisible: { id: chat_message_input } -- assertVisible: "You tipped|You received a tip" +- runFlow: subflows/open_tip_chat.yaml # Send a message and confirm it lands in the transcript. - tapOn: { id: chat_message_input } - inputText: "gg" - tapOn: { id: chat_send_icon } - extendedWaitUntil: { visible: { text: "gg" }, timeout: 8000 } + +# A tip event is present in the conversation. Which way it went is live data, so match either. +# Scrolled to rather than asserted in place: the transcript is shared and append-only, so every +# run of this flow and the chat-action flows pushes the tip events further up it. Last, because +# the send above needs the list at the bottom where it opened. +- scrollUntilVisible: + element: "You tipped|You received a tip" + direction: UP