mattermost-mobile-upstream/detox/maestro/GUIDELINES.md
yasser khan dfc94b3c3c
test(e2e): close manual release-test gaps (#10125)
Closes manual release-test gaps for mobile (Detox + Maestro), automating new test cases and hardening flaky/CI-only failures uncovered along the way.

### New coverage automated
- **MM-T416** offline/online post-retry (Android via airplane mode; iOS ultimately marked unsupported — see below)
- **MM-T1194 / MM-T1193** custom ToS accept/decline survives relaunch
- **MM-T3251** status change via account UI (cross-user visibility)
- **MM-T510** channel-wide/keyword mention delivery — later expanded to a full 5-channel mention matrix with exact server-side counts
- **MM-T3261 (_1/_2)** Report a Problem external hand-off (browser link mode, email mode)
- **MM-T2940** iOS managed-config (MDM) pre-fill/autoconnect
- **MM-T3271/3272** opening the app from a push notification (synthetic notification payloads, background/closed states)
- Un-skipped several tests that turned out to already pass (stale skips)

### Root-caused failing tests
- **Android SQLite corruption**: traced to native Kotlin DB connections not opening in WAL mode while JSI did (post #10109) — journal-mode mismatch corrupted the store on push-triggered writes. Fixed properly with a **WAL patch** (#10117) rather than the original destructive "wipe app database" workaround, which was reverted.
- **Fabric crash** (`addViewAt`/reparent) on `SlideUpPanelItem` taps — root-caused to view flattening racing sheet mounts; fixed with `collapsable={false}` (went through a revert/re-revert once CI reproduced the crash without it).
- **Search race**: stale in-flight search results could overwrite a cleared search box — fixed with request-id tagging.
- **iOS offline detection**: multiple iterations (AAAA sampling → Cloudflare-prefix check → final decision) landed on refusing iOS offline simulation outright, since all E2E servers are Cloudflare-fronted/anycast and the address space can't be enumerated/blocked.
- **MM-T3261 config patch** intermittently silently ignored by the server — root-caused to `ReportAProblemType` being environment-managed on PR Spinwicks (config PATCH returns 200 but is silently dropped); now detected and reported as a skip with proven cause, not a failure.
- **MM-T416 failed-post timing**: multiple rounds of tightening the wait bound to match the client's actual retry/backoff/timeout ceiling (ended at ~150s, derived from OkHttp/retry-interceptor math, not guessed).
- **MM-T5603 upload wait**: raised to match the client's real 180s upload timeout.
- **Classification banner suite**: fixed to verify config state directly instead of trusting a PATCH reply that the cloud sometimes drops.

### CI/reporting infra fixes
- Dedicated Maestro steps (MM-T3261, MM-T67856_4) weren't feeding into the merged JUnit report and couldn't gate pass/fail — fixed report merging/naming so they now count.
- Android Maestro logcat capture added (previously iOS-only diagnostics).
- Jest retry logic no longer discards passing results from a shard killed by timeout.
- Sharding raised 24→28.
- Removed dead Detox Pilot/Playbooks scaffolding (unused, uncovered by CI).

### Cleanup
- Comment/docs pass stripping narrative session-archaeology from long explanatory comments, keeping only load-bearing facts and CI references.
2026-09-11 09:48:02 +05:30

384 lines
17 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Maestro E2E — Authoring Guidelines
Single source of truth for engineers, QA, and AI tools writing Maestro flows in this repository.
For setup and CI wiring see [README.md](./README.md).
---
## 1. Scope: when Maestro vs Detox
| Use Maestro | Use Detox |
|---|---|
| Cross-app flows (share extension, Safari/Chrome hand-off) | In-app navigation and form interaction |
| OS permission dialogs (mic, camera, photos) | API-driven setup and DB assertions |
| Multi-device orchestration | Fast, JS-bridge-heavy regression |
| External browser / mail composer verification | Component-level unit tests (Jest) |
Do **not** justify a Maestro flow by referencing Detox limitations in flow comments — state the **user-visible behavior** under test.
---
## 2. Flow file header contract
Every file under `detox/maestro/flows/**/*.yml` **must** start with this block **before** `tags:` / `appId:`:
```yaml
# MM-TXXXX: <one-line behavior summary>
#
# PRE-CONDITIONS:
# - <server config, seed script, or device state required>
#
# REQUIRED ENV VARS:
# - SITE_1_URL, TEST_USER_EMAIL, TEST_USER_PASSWORD, TEST_CHANNEL_NAME
# - (list only vars this flow reads)
#
# ASSERTIONS:
# - <observable outcome 1>
# - <observable outcome 2>
#
# testIDs:
# - settings.report_problem.option
# - report_problem.screen
tags:
- MM-TXXXX
appId: ${MAESTRO_APP_ID}
---
```
### Header rules
| Field | Required | Content |
|---|---|---|
| Ticket line | Yes | `# MM-TXXXX: <behavior>` — what the user sees, not how the app implements it |
| PRE-CONDITIONS | Yes | Server settings, seed steps, permissions, fixtures |
| REQUIRED ENV VARS | Yes | Only variables referenced in the flow |
| ASSERTIONS | Yes | Pass/fail outcomes in product language |
| testIDs | Yes | Every `id:` selector used in the flow (grep `app/` for source of truth) |
| `tags:` | Yes | At least the Zephyr ticket id |
| `appId` | Yes | `${MAESTRO_APP_ID}` |
**Exempt from this contract** (enforced by `scripts/validate-flow-headers.sh`, which scans only `flows/**` and skips `_`-prefixed basenames):
- `subflows/**` — reusable fragments, not test cases.
- `_*.yml` anywhere — helper fragments invoked by another flow.
- `preflight/**` — environment gates run by CI before the reported suite (e.g. `preflight/_connect_check.yml` verifies the device can reach `SITE_1_URL`). These have no Zephyr id because they assert no product behavior; a failure means the environment is broken, not that a test failed.
Exempt files still need `appId: ${MAESTRO_APP_ID}` and a comment block explaining what they do and why they exist.
### Body comment markers — `# #` steps, `# *` assertions
Below the `---`, every comment block that introduces one or more commands **must** start with a marker:
| Marker | Means | Use for |
|---|---|---|
| `# #` | Test step / action | `tapOn`, `inputText`, `launchApp`, `runFlow`, `swipe`, `pressKey`, `runScript`, waits that exist only to reach the next step |
| `# *` | Assertion / expected result | `assertVisible`, `assertNotVisible`, and `extendedWaitUntil` used to verify an outcome |
This mirrors the Detox convention (`// #` / `// *`, see `detox/CLAUDE.md`) so both suites map onto the same test management system.
```yaml
# GOOD
# # Open Settings and reveal Report a Problem.
# Report a Problem can sit below the fold; also wait for config sync on first login.
- tapOn:
id: "account.settings.option"
# * Verify Settings is visible again with Report a Problem listed
- extendedWaitUntil:
visible:
id: "settings.report_problem.option"
timeout: 10000
# BAD — no marker; a "why" comment is not a substitute for one
# Report a Problem can sit below the fold.
- tapOn:
id: "account.settings.option"
# BAD — decorative section banners
# --- Open channel info ---
# ── Path 1: Leave from the full call screen ──
```
Rules:
- Only the **first** line of a block carries the marker. Continuation lines stay plain `#`, same as Detox.
- The marker goes at the block's own indentation, including inside nested `commands:` lists.
- A comment block that introduces no command (a trailing note at end of file) needs no marker.
- Applies to `flows/**`, `subflows/**`, `preflight/**`, and `_`-prefixed fragments — the header contract exemptions do **not** exempt a file from this.
- Enforced by `scripts/validate-flow-headers.sh` (`npm run validate-headers`), which runs in CI as the *Validate Maestro flow headers* job.
### Comment style — DO / DON'T
```yaml
# GOOD — behavior and expected outcome
# MM-T67856: Tapping Report a Problem leaves Settings and opens the report form.
# PRE-CONDITIONS:
# - Logged-in user on an unlicensed (free-edition) server
# ASSERTIONS:
# - Settings is visible again after dismissing the report form
# BAD — implementation / Detox references (do not write these)
# On free edition, report_problem.tsx sets skipReportAProblemScreen = ...
# The original Detox spec at detox/e2e/... is describe.skip(...)
# WatermelonDB observables for AllowDownloadLogs may lag on first login
```
Inline step comments should explain **why a wait or branch exists** in test terms, not cite React components or Detox patterns:
```yaml
# GOOD
# iOS may show an in-app report screen or an external browser sheet — handle both.
# BAD
# SFSafariViewController hosts the form when isFreeEdition is true in report_problem.tsx
```
---
## 3. Maestro platform rules
These align with [Maestro docs](https://docs.maestro.dev/) and lessons from Mattermost mobile CI.
### Selectors
- **Always** `id:` (testID). **Never** `point:` except to dismiss system overlays with no accessibility node.
- **Exception**: Apple system apps (Photos, Safari share sheet) have no testIDs — share-extension coordinate-tap utils live on `rf-split/maestro-ios-deferred-flows` (not in default CI on `rf-split/maestro`).
- If an element lacks a testID, add one in `app/` — do not work around with coordinates.
- **`text:` is allowed in exactly two cases** (both already relied on by merged flows; prefer `id:` everywhere else):
1. **iOS `FloatingTextInput` fields.** The component passes its testID to the container, not to the inner `TextInput`, so on iOS neither the field nor its value is queryable by `id:` — only the rendered label is. Hence `text: "Enter Server URL"` / `text: "Display Name"` in `subflows/server/` **on iOS only**. Android exposes these fields normally and **must** use `id:` (`server_form.server_url.input`, `server_form.server_display_name.input`). CI 33915931136: Android `tapOn: text: Display Name` failed 10/10 flows. The inline validation error *is* addressable: `<testID>.error` (see `app/components/floating_input/floating_input_container.tsx`).
2. **System (SpringBoard / Android framework) alert buttons** — `Allow`, `Okay`, `Not now`, `No thanks`. These are OS-owned views with no app testID and cannot be given one. Keep them `optional: true`; do not substitute `point:`, which is more brittle than the label.
- Verify hierarchy before authoring: `maestro --device <id> hierarchy`
- `text:` is a **full-string regex**, so a prefix or fragment matches nothing — silently under `optional: true`. `text: "mattermost.com"` does not match the title "Mattermost | Collaboration Platform…". Match the whole string or use a regex that spans it, and beware labels assembled from server config (the login placeholder gains an "AD/LDAP Username" segment only when LDAP is enabled).
testIDs follow `component.subcomponent.element`:
```yaml
- tapOn:
id: "channel.post_draft.send_action.send.button"
- tapOn:
id: "tab_bar.home.tab"
- tapOn:
id: "login_form.username.input"
- tapOn:
id: "channel_header.channel_quick_actions.button"
- tapOn:
id: "share_extension.channel.option"
```
### Waits
- Prefer `extendedWaitUntil` on a specific element over `waitForAnimationToEnd`.
- Use `waitForAnimationToEnd` only when no stable element exists yet (e.g. immediately after login tap).
### Platform branches
```yaml
- runFlow:
when:
platform: iOS
commands:
- ...
```
Keep shared steps outside `runFlow` blocks.
### Environment variables
Never hardcode server URLs, credentials, or channel names:
| Variable | Description |
|----------|-------------|
| `${MAESTRO_APP_ID}` | App bundle ID (e.g. `com.mattermost.rnbeta`) |
| `${SITE_1_URL}` | Primary test server URL |
| `${TEST_USER_EMAIL}` | Test user email |
| `${TEST_USER_PASSWORD}` | Test user password |
| `${TEST_CHANNEL_NAME}` | Test channel name (set by seed script) |
| `${TEST_TEAM_NAME}` | Test team name (set by seed script) |
### App launch
Use `runFlow: ../../subflows/auth/login.yml` — do not add raw `launchApp` when login already handles stale sessions, call screens, and permission dialogs. Follow with `navigate_to_channel.yml` when needed.
### Toggles
Parent toggle rows are not tappable. Use the Switch child testID:
```yaml
- tapOn:
id: "report_problem.enable_log_attachments.toggled.false.button"
optional: true
```
Format: `<parent_id>.toggled.{true|false}.button` where the boolean is the **current** state.
Server preferences survive `clearState: true` — the next flow cannot assume default toggle state. Use `optional: true` on toggle taps or drive to a definitive end state.
After mutating a toggle:
1. Assert the new toggle visual state.
2. `waitForAnimationToEnd: {timeout: 3000}` (async server save).
3. Then any `stopApp: true` relaunch.
### Modal exit on iOS
Use `launchApp: {stopApp: true, clearState: false}` — not `back` — to leave modal-presented screens.
### `optional: true`
Allowed **only** on toggle taps (idempotent state) and genuinely intermittent system UI (permission prompts, Save Password, onboarding). Do **not** use on ordinary taps to silence failures.
```yaml
- tapOn:
text: "Allow"
optional: true
- tapOn:
text: "Not Now"
optional: true
```
### Bottom sheets (iOS)
`@gorhom/bottom-sheet` content is invisible to XCUITest unless `accessibilityViewIsModal={true}` on the content container (already set in `app/screens/bottom_sheet/index.tsx`). Callers must pass explicit `testID` on inner `OptionItem` components.
### Screenshots
- `takeScreenshot: <kebab-case-name>` at key transitions and at flow end.
- Names describe **state**, not implementation: `report-problem-dismissed`, not `after-breadcrumb-tap`.
### Sub-flows
1. `runFlow: ../../subflows/auth/login.yml` first when auth is required.
2. `runFlow: ../../subflows/navigation/navigate_to_channel.yml` before channel actions.
3. Seed via shell (`npx tsx detox/maestro/fixtures/seed.ts`) — not inside the YAML.
### Server config beyond defaults
1. Tag the flow uniquely (e.g. `MM-T67856_4`) and add the tag to `config/exclude_tags.json` so the default batch does not run it with the setting flipped.
2. Flip the setting inside the flow, not in the workflow: an `onFlowStart` hook that runs a `runScript` fixture (Maestro's built-in `http` client; `--env` values are globals) and an `onFlowComplete` hook that restores it. `fixtures/set_allow_download_logs.js` is the reference. This keeps a local `maestro test` of the file identical to CI.
3. Give it a dedicated run step in `e2e-maestro-template.yml` plus a **standalone `always()` restore step**. The hook covers every normal exit, including failures; the workflow step covers a runner hard-kill, which never reaches `onFlowComplete` and would leave the setting flipped for every later job on a shared server.
`AllowDownloadLogs` is under `SupportSettings`, not `ServiceSettings`:
```bash
curl -sS -X PUT \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"SupportSettings":{"AllowDownloadLogs":false}}' \
"$SITE_1_URL/api/v4/config/patch"
```
### Cross-platform notes
- **iOS share extension** (deferred branch `rf-split/maestro-ios-deferred-flows`): Maestro must be attached to the host app (Safari/Photos) before the share sheet opens; share target label is **Mattermost**, not **Mattermost Beta**.
- **Android Chrome**: Fresh emulators show first-run / ads interstitials — use `subflows/browser/dismiss_chrome_interstitial.yml` or CI Chrome pre-warm (see `create_android_emulator.sh`).
- **Android port forwarding**: `adb reverse tcp:8065 tcp:8065` when using localhost URLs.
- **iOS timezone**: `xcrun simctl spawn $SIMULATOR_ID launchctl setenv TZ "America/New_York"` (Xcode 26 removed `simctl timezone`).
- **Android timezone**: `adb shell setprop persist.sys.timezone "America/New_York"`.
---
## 4. Server provisioning & fixtures
Maestro flows run against Matterwick cloud servers (`SITE_1_URL`, `SITE_2_URL`, `SITE_3_URL`). CI provisions via `detox/provision_server.js` + `detox/maestro/fixtures/seed.ts`.
| Setting | Set by | Why |
|---|---|---|
| Trial Enterprise license | `detox/provision_server.js` | Calls + Custom Profile Attributes |
| Calls plugin + `DefaultEnabled` | `detox/provision_server.js` | Calls flows |
| Per-channel calls enable | `detox/maestro/fixtures/seed.ts` | Cloud servers may not auto-enable per channel |
| `EnableChannelBookmarks` | `detox/provision_server.js` | Bookmark flows |
| `ExperimentalViewArchivedChannels` | `detox/provision_server.js` | Archive flows |
| `RateLimitSettings.Enable=false` | `detox/provision_server.js` | Prevents 429s on burst traffic |
| `AllowDownloadLogs=false` (MM-T67856_4 only) | Dedicated CI step | Isolated attach-logs variant |
**iOS file bookmark fixture**: document picker reads from FileProvider.LocalStorage — CI copies `test_bookmark.png` there in `e2e-maestro-template.yml`.
**Android file bookmark fixture**: `adb push` to `/sdcard/Download/test_bookmark.png` + `MEDIA_SCANNER_SCAN_FILE` broadcast in `create_android_emulator.sh`.
**Calls flows**: after login, WebSocket-driven config can take 10–30s — use `waitForAnimationToEnd: {timeout: 25000}` at the top of calls flows (see `call_ui_permission.yml`).
Local parity: [README.md § Official local workflow](./README.md#official-local-workflow) and `./scripts/setup_local.sh`.
---
## 5. CLI version pin
Single source of truth: [`maestro-version.json`](./maestro-version.json)
- Local install: read version from `maestro-version.json`, then `curl -fsSL "https://get.maestro.mobile.dev" | bash`
- CI reads `maestro-version.json` and caches by commit SHA.
- Bump only after running the full suite on both platforms; update version **and** commit together.
---
## 6. CI layout
| Workflow | Role |
|---|---|
| `e2e-detox-pr.yml` | Matterwick entry point: builds, provisioning, header validation, dispatches Maestro |
| `e2e-maestro-pr.yml` | Maestro status contexts, orchestration (reusable) |
| `e2e-maestro-template.yml` | Simulator/emulator, seed, batch test runner, report |
Maestro runs via `detox/maestro/scripts/run_ci_batches.sh` (one flow per batch). JUnit reports are merged before HTML generation.
Device targets (same as Detox):
- iOS: iPhone 17 Pro, iOS 26.2 — `detox/scripts/preboot_ios_simulator.sh`
- Android: `detox_pixel_8_api_35` — `detox/create_android_emulator.sh` with `MAESTRO_ANDROID=true`
See [README.md § CI Integration](./README.md#ci-integration) for the full diagram.
---
## 7. Pre-PR checklist
Before opening a PR, verify every changed flow:
- [ ] Header has ticket, PRE-CONDITIONS, REQUIRED ENV VARS, ASSERTIONS, testIDs
- [ ] No Detox references, `app/` paths, or React implementation details in comments
- [ ] Every body comment block introducing a command starts with `# #` (step) or `# *` (assertion)
- [ ] All interactions use `id:` selectors verified via `grep -rn 'testID=' app/`
- [ ] `tags:` includes the Zephyr id
- [ ] Login sub-flow present when auth is required
- [ ] Toggle taps use `.toggled.{true|false}.button` pattern
- [ ] iOS modal exits use `launchApp:{stopApp:true,clearState:false}`
- [ ] No `optional: true` on non-toggle, non-system-dialog taps
- [ ] Flow ends with `takeScreenshot`
- [ ] Non-default server config uses exclude-tag + dedicated CI step
- [ ] `npm run validate-headers` passes locally
```bash
cd detox/maestro && npm run validate-headers
```
CI runs the same script in `e2e-detox-pr.yml` (`validate-maestro-flow-headers`) before Maestro tests.
**Forbidden** without artifact evidence:
- Adding `optional: true` to a non-toggle tap to silence a failure.
- Increasing a timeout because "it's flaky" — find the real reason.
- Platform-only `runFlow.when` branches instead of fixing the actual issue.
---
## 8. Debugging a failing flow
1. Check `~/.maestro/tests/<latest>/` for screenshots and `view-hierarchy.xml`.
2. Classify: **testID drift** (grep `app/`), **navigation timeout** (inspect hierarchy), or **server state** (`curl ${SITE_1_URL}/api/v4/config`).
3. Re-run with the same env as CI:
```bash
source detox/maestro/.maestro-test-env.sh
~/.maestro/bin/maestro test detox/maestro/flows/<flow>.yml
```
For CI parity, use `detox/maestro/scripts/run_ci_batches.sh` (batch mode) with the CI-built `.app`/APK — not isolated single-flow runs only.
Use the Maestro CLI for final verification (matches CI). Use `maestro hierarchy` / MCP inspect tools during authoring iteration.
---
## 9. Example — canonical header
See [`flows/account/attach_logs.yml`](./flows/account/attach_logs.yml) for the reference implementation.