Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -322,7 +323,12 @@ private fun MessageOverflow(actions: List<MessageAction>) {

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,
Expand Down
11 changes: 11 additions & 0 deletions maestro/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
55 changes: 55 additions & 0 deletions maestro/chat_message_delete.yaml
Original file line number Diff line number Diff line change
@@ -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 }
75 changes: 75 additions & 0 deletions maestro/chat_message_edit.yaml
Original file line number Diff line number Diff line change
@@ -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}"
63 changes: 63 additions & 0 deletions maestro/chat_message_selection.yaml
Original file line number Diff line number Diff line change
@@ -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 }
20 changes: 20 additions & 0 deletions maestro/subflows/assert_message_action.yaml
Original file line number Diff line number Diff line change
@@ -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 }
14 changes: 14 additions & 0 deletions maestro/subflows/open_tip_chat.yaml
Original file line number Diff line number Diff line change
@@ -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 }
12 changes: 12 additions & 0 deletions maestro/subflows/select_own_message.yaml
Original file line number Diff line number Diff line change
@@ -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 }
22 changes: 22 additions & 0 deletions maestro/subflows/tap_message_action.yaml
Original file line number Diff line number Diff line change
@@ -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}"
25 changes: 9 additions & 16 deletions maestro/tip_chat.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading