From 2acce326b78c690ef12d5940ce4437fc530a4a5c Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Sat, 31 Oct 2026 16:49:40 +0100 Subject: [PATCH 01/11] test: open and select messages by their list row, not the avatar checkbox, in the ios flows --- .maestro/features/07-move-delete-tag.yaml | 9 +++++++-- .maestro/subflows/open-message.yaml | 15 ++++++++++++--- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/.maestro/features/07-move-delete-tag.yaml b/.maestro/features/07-move-delete-tag.yaml index 38dfc5d9..bb4ffbbd 100644 --- a/.maestro/features/07-move-delete-tag.yaml +++ b/.maestro/features/07-move-delete-tag.yaml @@ -63,7 +63,10 @@ tags: direction: DOWN centerElement: true timeout: 20000 -- longPressOn: ".*Grüße aus München.*" +# The row, not its avatar checkbox (whose label has the subject too). +- longPressOn: + id: email-row + text: ".*Grüße aus München.*" - extendedWaitUntil: visible: "Clear selection" timeout: 10000 @@ -76,7 +79,9 @@ tags: visible: "Clear selection" commands: - tapOn: "Clear selection" -- longPressOn: ".*Grüße aus München.*" +- longPressOn: + id: email-row + text: ".*Grüße aus München.*" - tapOn: "Unstar|Star" - runFlow: when: diff --git a/.maestro/subflows/open-message.yaml b/.maestro/subflows/open-message.yaml index e61ad435..c66b492d 100644 --- a/.maestro/subflows/open-message.yaml +++ b/.maestro/subflows/open-message.yaml @@ -3,6 +3,9 @@ # covers it (the tap then opens another tab): on the phone, scroll it to the # middle. The iPad's list is a column Maestro's scroll (in the middle of the # screen) doesn't reach; its rows fit on screen. +# The avatar's "Select : " checkbox mentions the subject +# too, and a tap on it selects the row instead of opening it (#91): match +# the row itself by its id. appId: org.bulwarkmail.mobile --- - runFlow: @@ -10,7 +13,9 @@ appId: org.bulwarkmail.mobile true: ${DEVICE != 'tablet'} commands: - scrollUntilVisible: - element: ".*${SUBJECT}.*" + element: + id: email-row + text: ".*${SUBJECT}.*" direction: DOWN centerElement: true visibilityPercentage: 100 @@ -20,13 +25,17 @@ appId: org.bulwarkmail.mobile true: ${DEVICE == 'tablet'} commands: - scrollUntilVisible: - element: ".*${SUBJECT}.*" + element: + id: email-row + text: ".*${SUBJECT}.*" direction: DOWN timeout: 30000 # A tap while the list still moves lands on another row. - waitForAnimationToEnd: timeout: 5000 -- tapOn: ".*${SUBJECT}.*" +- tapOn: + id: email-row + text: ".*${SUBJECT}.*" # The message toolbar's "more" button: "Reply All" shows on the phone, but # the iPad pane has a quick reply box instead for mail to one recipient. - extendedWaitUntil: From 8fae4e6398466a8f83efdbb6b16d33ced7288ffe Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Sat, 31 Oct 2026 16:49:50 +0100 Subject: [PATCH 02/11] test: swipe the message row, not its avatar, in the ios swipe flow --- .maestro/features/06-swipe-undo.yaml | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/.maestro/features/06-swipe-undo.yaml b/.maestro/features/06-swipe-undo.yaml index e19a97c2..9d1d07e4 100644 --- a/.maestro/features/06-swipe-undo.yaml +++ b/.maestro/features/06-swipe-undo.yaml @@ -11,8 +11,12 @@ tags: direction: DOWN centerElement: true timeout: 20000 +# Swipe the row itself: the avatar checkbox's label has the subject too, and +# a swipe that starts on it only selects the row (#91). - swipe: - from: ".*Grüße aus München.*" + from: + id: email-row + text: ".*Grüße aus München.*" direction: LEFT duration: 400 # The undo bar shows for 5 s; when it goes, the Compose button slides into @@ -27,14 +31,18 @@ tags: - takeScreenshot: swipe-02-undone - swipe: - from: ".*Grüße aus München.*" + from: + id: email-row + text: ".*Grüße aus München.*" direction: RIGHT duration: 400 - waitForAnimationToEnd: timeout: 3000 - takeScreenshot: swipe-03-read-toggled - swipe: - from: ".*Grüße aus München.*" + from: + id: email-row + text: ".*Grüße aus München.*" direction: RIGHT duration: 400 - waitForAnimationToEnd: From b75b4faa69feb3288cd9e11f9c705e29e1262521 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Sat, 31 Oct 2026 16:50:06 +0100 Subject: [PATCH 03/11] test: expect the upload and attach from files options in the ios compose flow --- .maestro/features/03-compose-send.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.maestro/features/03-compose-send.yaml b/.maestro/features/03-compose-send.yaml index 771f4f0e..b115bd5a 100644 --- a/.maestro/features/03-compose-send.yaml +++ b/.maestro/features/03-compose-send.yaml @@ -32,7 +32,9 @@ tags: visible: "Photos & Videos" timeout: 10000 - assertVisible: "Camera" -- assertVisible: "Files" +# A server with Files offers "Upload from this device" and "Attach from +# Files" instead of a plain "Files". +- assertVisible: "Upload from this device|Files" - takeScreenshot: compose-02-attach-sheet - evalScript: ${output.pickPhoto = ORIENTATION != 'landscape'} # The system photo picker runs in another process: XCTest sees none of its From 897d4ae32df9f63182f49eed91e0123a6f947f96 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Sat, 31 Oct 2026 16:50:21 +0100 Subject: [PATCH 04/11] test: confirm cancel send under the new back-to-drafts message in the ios flow --- .maestro/features/08-scheduled-send.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.maestro/features/08-scheduled-send.yaml b/.maestro/features/08-scheduled-send.yaml index b6eab573..777b217a 100644 --- a/.maestro/features/08-scheduled-send.yaml +++ b/.maestro/features/08-scheduled-send.yaml @@ -48,9 +48,11 @@ tags: - extendedWaitUntil: visible: "Keep scheduled" timeout: 10000 +# The dialog's button (below its message), not its title or the button +# behind it. Cancelling now moves the message back to Drafts. - tapOn: text: "Cancel send" - below: "Keep scheduled|.*will not be delivered.*" + below: ".*will not be (sent|delivered).*" - extendedWaitUntil: notVisible: ".*${output.subject}.*" timeout: 30000 From 1769a4aa623ce74b445c85916458e1d3a2af30cf Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Sat, 31 Oct 2026 16:50:46 +0100 Subject: [PATCH 05/11] test: customize the default reminders before adding one in the ios calendar flow --- .maestro/features/09-calendar.yaml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.maestro/features/09-calendar.yaml b/.maestro/features/09-calendar.yaml index a975fd1e..5e3d4ffb 100644 --- a/.maestro/features/09-calendar.yaml +++ b/.maestro/features/09-calendar.yaml @@ -46,6 +46,19 @@ tags: - pressKey: Enter - waitForAnimationToEnd: timeout: 3000 +# A new event starts on the calendar's default reminders; Customize turns +# them into the event's own list, which has Add reminder. +- scrollUntilVisible: + element: "Customize|Add reminder" + direction: DOWN + timeout: 10000 +- runFlow: + when: + visible: "Customize" + commands: + - tapOn: "Customize" + - waitForAnimationToEnd: + timeout: 3000 - scrollUntilVisible: element: "Add reminder" direction: DOWN From 1b6384edecc603ed11e9f5293f6d18579d17e171 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Sat, 31 Oct 2026 16:51:23 +0100 Subject: [PATCH 06/11] test: tap allow on the ios notification prompt once and start the calendar flow's event today --- .maestro/features/09-calendar.yaml | 7 ++++++- .maestro/subflows/allow-notifications.yaml | 4 ++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/.maestro/features/09-calendar.yaml b/.maestro/features/09-calendar.yaml index 5e3d4ffb..b7043dc7 100644 --- a/.maestro/features/09-calendar.yaml +++ b/.maestro/features/09-calendar.yaml @@ -34,7 +34,12 @@ tags: timeout: 5000 - takeScreenshot: calendar-04-week -# Create +# Create. The button starts the event on the selected day: make that today +# (a stray tap, such as one through the notification prompt, can select +# another day), so the schedule below finds it after Go to today. +- tapOn: "Go to today" +- waitForAnimationToEnd: + timeout: 3000 - tapOn: "New event" - extendedWaitUntil: visible: "Save" diff --git a/.maestro/subflows/allow-notifications.yaml b/.maestro/subflows/allow-notifications.yaml index 154ee098..e9b72487 100644 --- a/.maestro/subflows/allow-notifications.yaml +++ b/.maestro/subflows/allow-notifications.yaml @@ -11,9 +11,13 @@ appId: org.bulwarkmail.mobile - setOrientation: PORTRAIT - waitForAnimationToEnd: timeout: 3000 + # Maestro repeats a tap that seems to change nothing; here the repeat + # landed on the app once the alert was gone (a day in the calendar's + # month grid, which opened that day). Tap once. - tapOn: text: "Allow" optional: true + retryTapIfNoChange: false - runFlow: when: true: ${ORIENTATION == 'landscape'} From b6d364f732cdd12d004751444a5e84e819072945 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Sat, 31 Oct 2026 16:51:43 +0100 Subject: [PATCH 07/11] test: expect delete to move the folder to the trash without a question in the ios files flow --- .maestro/features/11-files.yaml | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/.maestro/features/11-files.yaml b/.maestro/features/11-files.yaml index 55ccb38e..5a149af6 100644 --- a/.maestro/features/11-files.yaml +++ b/.maestro/features/11-files.yaml @@ -44,20 +44,22 @@ tags: timeout: 20000 - takeScreenshot: files-04-folder-created - longPressOn: ".*${output.folder}.*" +# Delete moves it to the trash at once, with Undo and no question (like the +# webmail). A tap while the actions sheet still slides in can miss: tap +# again while Delete is still there. +- extendedWaitUntil: + visible: "Delete" + timeout: 10000 - repeat: while: - notVisible: "Delete this item.*" + visible: "Delete" times: 3 commands: - - tapOn: "Delete" + - tapOn: + text: "Delete" + optional: true - waitForAnimationToEnd: timeout: 3000 -- extendedWaitUntil: - visible: "Delete this item.*" - timeout: 10000 -- tapOn: - text: "Delete" - below: "Delete this item.*" - extendedWaitUntil: notVisible: ".*${output.folder}.*" timeout: 20000 From 6a52715704cecfc97bd52b341a95df10a9574d41 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Sat, 31 Oct 2026 16:52:07 +0100 Subject: [PATCH 08/11] test: search settings pages by words that keep their row above the keyboard in the ios settings flow --- .maestro/features/12-settings.yaml | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/.maestro/features/12-settings.yaml b/.maestro/features/12-settings.yaml index 7d50cc4f..2023fb84 100644 --- a/.maestro/features/12-settings.yaml +++ b/.maestro/features/12-settings.yaml @@ -11,6 +11,11 @@ tags: visible: "Search settings" timeout: 20000 - takeScreenshot: settings-00-list +# The search also lists the settings inside each page under it, so a common +# word ("Folders", "Calendar") pushes the page's row below the keyboard. +# Each Q matches its page near the top. A PROOF the search lists under the +# row would pass on the iPad without the page, so those differ. The Themes +# cards are one element each, named by the theme: check the page's text. - runFlow: {file: ../subflows/settings-page.yaml, env: {NAME: account, ROW: ".*@example.org, .*", PROOF: "Logged-in accounts|Display Name"}} - runFlow: {file: ../subflows/settings-page.yaml, env: {NAME: language, ROW: "Language, Region & Time, .*", Q: "Language", PROOF: "Date Format"}} - runFlow: {file: ../subflows/settings-page.yaml, env: {NAME: notifications, ROW: "Notifications, .*", Q: "Notifications", PROOF: "Email Notifications"}} @@ -22,16 +27,16 @@ tags: - runFlow: {file: ../subflows/settings-page.yaml, env: {NAME: vacation, ROW: "Out of Office, .*", Q: "Office", PROOF: "Reply automatically"}} - runFlow: {file: ../subflows/settings-page.yaml, env: {NAME: filters, ROW: "Filters, .*", Q: "Filters", PROOF: "Email Filters"}} - runFlow: {file: ../subflows/settings-page.yaml, env: {NAME: templates, ROW: "Templates, .*", Q: "Templates", PROOF: "Email Templates"}} -- runFlow: {file: ../subflows/settings-page.yaml, env: {NAME: folders, ROW: "Folders, .*", Q: "Folders", PROOF: ".*Inbox.*"}} +- runFlow: {file: ../subflows/settings-page.yaml, env: {NAME: folders, ROW: "Folders, .*", Q: "New folder", PROOF: ".*Inbox.*"}} - runFlow: {file: ../subflows/settings-page.yaml, env: {NAME: tags, ROW: "Tags, .*", Q: "Tags", PROOF: "Email Tags"}} - runFlow: {file: ../subflows/settings-page.yaml, env: {NAME: downloads, ROW: "Downloads, .*", Q: "Downloads", PROOF: "Email .*eml.* filename"}} - runFlow: {file: ../subflows/settings-page.yaml, env: {NAME: security, ROW: "Security, .*", Q: "Security", PROOF: "Account Security|Detecting server capabilities.*"}} -- runFlow: {file: ../subflows/settings-page.yaml, env: {NAME: content, ROW: "Content & Senders, .*", Q: "Content", PROOF: "External Content"}} -- runFlow: {file: ../subflows/settings-page.yaml, env: {NAME: calendar, ROW: "Calendar, .*", Q: "Calendar", PROOF: "Default view"}} -- runFlow: {file: ../subflows/settings-page.yaml, env: {NAME: contacts, ROW: "Contacts, .*", Q: "Contacts", PROOF: "Group by first letter"}} +- runFlow: {file: ../subflows/settings-page.yaml, env: {NAME: content, ROW: "Content & Senders, .*", Q: "Content &", PROOF: "External Content"}} +- runFlow: {file: ../subflows/settings-page.yaml, env: {NAME: calendar, ROW: "Calendar, .*", Q: "Week start", PROOF: "Default view"}} +- runFlow: {file: ../subflows/settings-page.yaml, env: {NAME: contacts, ROW: "Contacts, .*", Q: "Group by", PROOF: "Sort by last name"}} - runFlow: {file: ../subflows/settings-page.yaml, env: {NAME: files, ROW: "Files, .*", Q: "Files", PROOF: "Default View"}} - runFlow: {file: ../subflows/settings-page.yaml, env: {NAME: sidebar-apps, ROW: "Sidebar Apps, .*", Q: "Sidebar", PROOF: "Keep Apps Loaded"}} -- runFlow: {file: ../subflows/settings-page.yaml, env: {NAME: themes, ROW: "Themes, .*", Q: "Themes", PROOF: "The default light and dark palettes."}} +- runFlow: {file: ../subflows/settings-page.yaml, env: {NAME: themes, ROW: "Themes, .*", Q: "Themes", PROOF: "Pick a colou?r theme.*"}} - runFlow: {file: ../subflows/settings-page.yaml, env: {NAME: storage, ROW: "Storage & backup, .*", Q: "Storage", PROOF: "Offline mail.*"}} - runFlow: {file: ../subflows/settings-page.yaml, env: {NAME: about, ROW: "About, .*", Q: "About", PROOF: "This device"}} - tapOn: From da41b3f0608f5ac44fb3522304c0a11e458d5d85 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Sat, 31 Oct 2026 16:52:29 +0100 Subject: [PATCH 09/11] test: scroll sign out clear of the tab bar before tapping it in the ios flows --- .maestro/subflows/sign-out.yaml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.maestro/subflows/sign-out.yaml b/.maestro/subflows/sign-out.yaml index fb16fe3a..7c8cde8e 100644 --- a/.maestro/subflows/sign-out.yaml +++ b/.maestro/subflows/sign-out.yaml @@ -25,6 +25,14 @@ appId: org.bulwarkmail.mobile element: "Sign out" direction: DOWN timeout: 30000 + # The scroll can stop with the button half under the tab bar, where + # a tap reaches the tab bar instead: scroll on to the end of the list. + - swipe: + start: 50%,70% + end: 50%,30% + duration: 400 + - waitForAnimationToEnd: + timeout: 3000 - tapOn: "Sign out" - extendedWaitUntil: visible: "The account is removed from this device.*" From aabfe3f6a775fe20ddbc06de48faf667f2f47ba0 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Sat, 31 Oct 2026 16:52:57 +0100 Subject: [PATCH 10/11] test: accept the camera pre-prompt wording in the ios scanner flow --- .maestro/signin/14-scanner-camera.yaml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.maestro/signin/14-scanner-camera.yaml b/.maestro/signin/14-scanner-camera.yaml index 94b0c4f8..829c5716 100644 --- a/.maestro/signin/14-scanner-camera.yaml +++ b/.maestro/signin/14-scanner-camera.yaml @@ -10,8 +10,11 @@ tags: env: CAMERA: deny - tapOn: "Scan a sign-in code.*" +# Denied for good: "Camera access is disabled..."; not asked yet (the +# simulator doesn't always apply the denial up front): "Bulwark Mail needs +# camera access..." (English catalog) or the built-in fallback wording. - extendedWaitUntil: - visible: "Camera access is.*|Scanning a sign-in QR code uses the camera.*" + visible: "Camera access is.*|.*needs camera access.*|Scanning a sign-in QR code uses the camera.*" timeout: 15000 - takeScreenshot: scanner-01-denied - tapOn: "Close" From 69f277e1cef6733860420047864244ba1823ec54 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Sat, 31 Oct 2026 16:53:40 +0100 Subject: [PATCH 11/11] test: move to the webmail password field with safari's next key in the ios sign-in flows --- .maestro/subflows/type-web.yaml | 9 +++++++-- .maestro/subflows/webmail-login-page.yaml | 13 +++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/.maestro/subflows/type-web.yaml b/.maestro/subflows/type-web.yaml index 4f899808..f145068f 100644 --- a/.maestro/subflows/type-web.yaml +++ b/.maestro/subflows/type-web.yaml @@ -4,10 +4,15 @@ # field and type again. The caret is still at the end of what was just # typed, so eraseText (which deletes before the caret) empties it; selecting # the text with a long press didn't work and appended instead. SECRET=yes -# skips the check (a password field shows dots). +# skips the check (a password field shows dots). TAP=no types into the +# field that has the focus already. appId: org.bulwarkmail.mobile --- -- tapOn: ${HINT} +- runFlow: + when: + true: ${TAP != 'no'} + commands: + - tapOn: ${HINT} - waitForAnimationToEnd: timeout: 3000 - inputText: ${TEXT} diff --git a/.maestro/subflows/webmail-login-page.yaml b/.maestro/subflows/webmail-login-page.yaml index e2c8680c..c2b5512f 100644 --- a/.maestro/subflows/webmail-login-page.yaml +++ b/.maestro/subflows/webmail-login-page.yaml @@ -11,12 +11,25 @@ appId: org.bulwarkmail.mobile env: HINT: "user@example.com" TEXT: ${LOGIN} +# On the iPhone, Safari's keyboard toolbar (Previous, Next, Done) can sit +# over the password field, and a tap there leaves the focus in Email (the +# password then went into Email). Its Next moves the focus to the password. +- evalScript: ${output.webNext = false} +- runFlow: + when: + visible: "Next" + commands: + - tapOn: "Next" + - waitForAnimationToEnd: + timeout: 2000 + - evalScript: ${output.webNext = true} - runFlow: file: type-web.yaml env: HINT: "Enter your password" TEXT: ${PASSWORD} SECRET: "yes" + TAP: "${output.webNext ? 'no' : 'yes'}" - tapOn: "Sign in" - runFlow: when: