mattermost-mobile-upstream/detox
yasser khan a5d81c6b24
ci(e2e): port E2E CI stack to release-2.43 to unblock merges (#10004)
* chore(e2e): Fix e2e artifact upload path for CMT workflow (#9955)

* chore(e2e): Fix e2e artifact upload path for CMT workflow

* test(e2e): Replace MM-T placeholder test keys with real Zephyr Scale IDs

- Classification Banner (18 tests): MM-T6196–MM-T6205, MM-T6206–MM-T6208, MM-T6209–MM-T6213
- mm_blocks (incoming webhook + ephemeral): MM-T6214–MM-T6240 (27 tests)
- Fixes TM4J test-case linking by ensuring all test titles use real MM-T#### keys
- Tightens getTM4JTestCases filter regex from /(MM-T)\w+/ to /MM-T\d+/ to prevent
  crashes when non-numeric MM-T placeholders slip through before Zephyr linkage


- Scan all Xcode_*.app directories and filter for major version >= 26 to
  future-proof against Xcode 30+ releases (current glob pattern maxes at 29)
- Extract version from directory name, validate major >= 26, sort and select highest
- Add least-privilege job-level permissions (contents: read) to build jobs
  (build-ios-simulator, build-android-apk, build-android-apk-maestro) to scope
  actions/checkout and actions/upload-artifact access

* fix: apply CodeRabbit review feedback

Require Xcode 26.1+ for iOS prepare, standardize E2E titles to MM-TXXXX_N,
and bound Zephyr test-case id matching to complete identifiers.


* fix(e2e): Grant actions:write on CMT build jobs for artifact upload

Job-level contents:read alone strips upload-artifact permission; match
e2e-detox-pr.yml so ios/android/maestro builds can publish artifacts.

* feat(e2e): Integrate TSIO reporting (#9925)


Retire legacy maestro per-platform contexts from cancel-e2e-runs and
document the canonical PR, master, and CMT status context mapping.
Wire Detox + Maestro (PR/main/CMT) into Test System IO with a single
commit status (e2e/mobile; CMT uses e2e/compatibility-matrix-testing)
and groups mobile-pr / mobile-main / cmt-mobile.

Upload shard results with correct framework labels, merge Jest shards
into TSIO Detox shape, stub missing/dead shards as failures, and post
channel summaries after status settles. Matterwick MASTER is treated as
MAIN for mobile-main routing.

Also harden report/fail paths, stage Maestro screenshots per flow for
TSIO galleries, fix a few Detox setup/testID issues, and skip known
platform flakes so the pipeline stays actionable while TSIO lands.

* ci(e2e): per-job TSIO report groups, commit statuses, and webhook sidecar (#9969)

* ci(e2e): per-job TSIO report groups and commit statuses

E2E jobs previously shared a single `e2e/mobile` commit status backed by one
TSIO report group, so concurrent platform legs raced to finalize the group and
clobbered each other's results. Give each Detox/Maestro job its own report
group (`mobile-<flow>-<job>`, total_reports_expected=1) and its own commit
status context (`detox-ios`, `detox-android`, `detox-ipad`, `maestro-ios`,
`maestro-android`); CMT reports per shard plus an umbrella context.

Channel notifications move to a single rollup step so N platform legs no
longer spam the webhook, and the five-server topology is dispatched with
explicit per-platform inputs that still resolve to the existing SITE_*_URL
contract consumed by the test config.

Also adds the webhook sidecar launcher, Android emulator CI prep, and TSIO
screenshot path linking that keeps folder basenames matching Jest full names.

The new detox type-check job surfaced pre-existing broken import paths in
maestro/fixtures/calls_seed.ts, which pointed at detox/detox/e2e; corrected so
the guard passes on its own first run.

Note: the CMT matrix now reads per-platform server URLs (`ios_site_1_url`,
`android_site_1_url`, `site_3_url`) instead of a single `url`, which needs the
matching Matterwick dispatch change to land alongside this.

Co-authored-by: Cursor <cursoragent@cursor.com>

* ci(e2e): aggregate CMT channel rollup from TSIO and wire notify secrets

The CMT umbrella posted a placeholder summary built from CI job outcomes
(passed:0, failed:0|1, reports:[]), so the release channel message carried no
per-leg detail. It now runs the same tsio-channel-notify-rollup.js path as
PR/main: cmt-identity emits a per-shard TSIO config map and the rollup polls
every shard group, so counts and per-leg rows come from TSIO. The rollup job
also needs id-token: write to mint its OIDC token.

Webhook secrets used by the per-job TSIO finalize steps were referenced but
never declared in the templates' workflow_call.secrets, so GitHub resolved them
to empty strings and channel notify silently no-opped. Declare them in the
iOS/Android/Maestro templates and forward them from e2e-detox.yml and the CMT
callers. The Maestro PR path worked only because its callers use
secrets: inherit.

Also from review:
- split-tests.js emitted two matrix jobs at parallelism=1 when mm_blocks specs
  were present; merge into one shard instead of exceeding the requested count.
- Move the Android ARTIFACT_RUN_ID emptiness check ahead of download-artifact,
  which consumes it and otherwise fails with an opaque API error.
- resolve-tsio-job-config.sh now reuses webhookBucketForReportName instead of
  re-deriving the bucket prefix with a looser startsWith check.

Co-authored-by: Cursor <cursoragent@cursor.com>

* ci(e2e): add local preflight for CI PRs

The webhook-secret and OIDC-permission gaps in this branch were invisible
locally and in a green CI run — the affected steps exit 0 and just skip the
notify or commit status. Add a script engineers can run before opening a PR that
touches .github/ or detox/utils:

  cd detox && npm run preflight:ci        # :ci-fast skips lint + tsc

It runs actionlint (which flags secrets not declared in a callee's
workflow_call.secrets), scans for exit-code capture broken by a trailing
backslash, asserts every job running a TSIO OIDC script declares
id-token: write, then runs the detox/utils tests and npm run check. actionlint
and the CodeRabbit CLI are optional and skipped with an install hint when absent.

Co-authored-by: Cursor <cursoragent@cursor.com>

* ci(e2e): use mktemp for preflight logs

Fixed /tmp log paths can be pre-created as symlinks, so the preflight
script would truncate whatever they point at (CWE-377). Create both logs
with mktemp and drop them on exit.

Co-authored-by: Cursor <cursoragent@cursor.com>

* ci(e2e): address review feedback on TSIO statuses and sidecar

A cancelled E2E run reached no conclusion, so posting `success` on its
commit statuses let a PR merge on a check that never ran. Cancelled runs
now post `pending`; `success` is reserved for the one deliberate bypass,
the E2E/Override label.

Commit-status contexts move into the `e2e-test/*` namespace to match the
mattermost monorepo, with the prefix owned by build-tsio-job-config.js and
mirrored in the cancel action's context list. Legacy unprefixed and `e2e/*`
contexts are dropped rather than carried forward.

The `preflight:ci` script and its OIDC-permission checker duplicated what
actionlint, the unit tests and `npm run check` already do, so they are
removed and detox/CLAUDE.md documents the underlying failure modes and the
commands to catch them instead.

cloudflared and the TSIO CLI are pinned to known-good releases rather than
`latest`, and detox/README.md now documents the webhook sidecar: when a
tunnel is needed, the WEBHOOK_PUBLIC_BASE_URL / CLOUDFLARED_TUNNEL_TOKEN
precedence, and why quick tunnels report callbacks as unreachable.

Co-authored-by: Cursor <cursoragent@cursor.com>

* ci(e2e): detect upstream cancellation and surface status reset failures

generate-report runs with always(), so job.status stays "success" when an
upstream job is cancelled and TSIO was published as a real failure. Read the
generate-specs/e2e-* needs results so a cancelled run is reported as such.

In cancel-e2e-runs, only reset the E2E commit statuses for runs that were
actually stopped — a run we failed to cancel keeps publishing its own results.
Aggregate createCommitStatus failures across every context and fail the action
afterwards instead of silently warning, so a stale check cannot go unnoticed.

---------

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(e2e): Quarantine proven flakes and harden Detox harness (#9971)

* ci(e2e): make E2E/Override waive checks instead of cancelling runs (#9975)

* ci(e2e): make E2E/Override waive checks instead of cancelling runs

E2E/Override previously cancelled in-flight E2E runs and posted success on
their commit statuses. Cancelling destroys the very signal the maintainer
might still want, and reporting success for a run that never finished
attributes a conclusion to work that never happened.

Override now means one thing only: the required e2e-test/* contexts go green
so the PR can merge. Runs keep going and keep reporting to TSIO.

- Add .github/actions/e2e-override-status to post success (or pending) on all
  five required e2e-test/* contexts for a SHA.
- Label manager posts success on `labeled` and pending on `unlabeled`, so
  removing the escape hatch re-blocks the PR instead of leaving a stale waiver
  on the head SHA. It comments the runs it left running rather than killing
  them, and drops to `actions: read` now that it never cancels.
- tsio-report-status re-reads the PR labels before posting a failure. Because
  runs are no longer cancelled, a result landing after the override was applied
  would otherwise re-red a check the maintainer deliberately bypassed. The
  lookup fails closed so an unreadable label list cannot turn a real failure
  green, and the step itself no longer fails under override.
- Guard e2e-cancel-on-label-removal: stripping E2E/Run* is exactly what the
  Honor E2E/Override step does, so without this the indirect unlabeled event
  would cancel the runs the redesign is meant to preserve.
- e2e-detox-pr posts the success itself when the policy skips E2E, since every
  downstream job is skipped and a skipped job leaves the required contexts
  absent, blocking the merge forever.
- cancel-e2e-runs drops status_state: a cancelled run has no conclusion to
  report, so it always resets to pending.
- Thread `issues: read` through the reusable workflow callers so finalize can
  read the labels; a reusable workflow cannot escalate past its caller.

Co-authored-by: Cursor <cursoragent@cursor.com>

* ci(e2e): derive required E2E contexts from one definition

The override and cancel actions each carried their own copy of the five
required e2e-test/* contexts, kept aligned with build-tsio-job-config.js by
comment alone. A job renamed in PR_MAIN_JOBS would silently leave the actions
waiving or resetting a context that no longer exists, so a PR either cannot
merge or merges while a real context still blocks it.

Export E2E_STATUS_CONTEXTS from detox/utils/build-tsio-job-config.js, derived
from PR_MAIN_JOBS, and require it from both composite actions. github-script
resolves relative requires against the workspace, and every call site already
checks out the repo before using the action.

Also set persist-credentials: false on the e2e-label-manager checkout, matching
the other checkout steps.

Co-authored-by: Cursor <cursoragent@cursor.com>

* ci(e2e): drop the cancel action for native concurrency, keep GitHub calls in Actions

Review feedback from @saturninoabril.

Superseding an in-flight E2E run is what GitHub's `concurrency` already does, so
`e2e-detox-pr.yml` now declares `concurrency: e2e-<ref>` with `cancel-in-progress`
instead of cancelling from a step inside the run that supersedes it. The older run
is cancelled the moment the new dispatch is created rather than once a runner picks
up the first job.

That removes the only two remaining callers of `.github/actions/cancel-e2e-runs`,
along with the commit statuses it forced to `pending` on cancel — a cancelled run
has no conclusion to report, so the state is left to GitHub. Forcing a commit status
is now reserved for real test results and the override waiver. The action is deleted.

`e2e-cancel-on-label-removal.yml` goes with it: with `E2E/Run` on its way out and
override no longer cancelling, removing the label to stop a run has no remaining
practical case, and dropping the workflow also removes the back-door cancel guard
it needed.

The `E2E/Override` lookup moves out of `tsio-report-status.js` into a new
`.github/actions/e2e-override-label` composite action, which the finalize steps
call and pass in via `--e2e-override`. Talking to GitHub stays in the Actions
layer and the reporter is left deciding on its inputs.

Co-authored-by: Cursor <cursoragent@cursor.com>

* ci(e2e): paginate PR label lookups for E2E/Override

Co-authored-by: Cursor <cursoragent@cursor.com>

* ci(e2e): re-post E2E/Override success on new commits

Override success is SHA-scoped: it is posted only on the head SHA at
label-apply time (e2e-label-manager 'labeled' path) and when Matterwick
dispatches e2e-detox-pr.yml (post-e2e-override-status). On a push after
E2E/Override is applied, refresh-e2e-run-label strips the run labels and
returns — Matterwick is not dispatched, so the new head SHA gets no
e2e-test/* statuses. With those checks Required, the new commit is
blocked until the label is re-toggled.

Carry the override onto the new commit: refresh-e2e-run-label now emits
an e2e_override output, and when it is set a gated checkout +
e2e-override-status step re-posts success on the new head SHA. Reuses
the existing e2e-override-status action (same 5 required e2e-test/*
contexts, same 'E2E bypassed' description), so it is idempotent with the
labeled path. Adds statuses: write / contents: read to the job.

Note: refresh-e2e-run-label is gated on base.ref == 'main', so stacked
PRs (base != main) are not covered; non-main-base already posts success
at dispatch time.

---------

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-04 16:50:34 +05:30
..
android_emulator test(detox): re-submit RF→Detox migration without unintended UI changes (#9847) 2026-06-19 22:44:02 +05:30
e2e ci(e2e): port E2E CI stack to release-2.43 to unblock merges (#10004) 2026-08-04 16:50:34 +05:30
maestro ci(e2e): port E2E CI stack to release-2.43 to unblock merges (#10004) 2026-08-04 16:50:34 +05:30
provision ci(e2e): port E2E CI stack to release-2.43 to unblock merges (#10004) 2026-08-04 16:50:34 +05:30
scripts ci(e2e): port E2E CI stack to release-2.43 to unblock merges (#10004) 2026-08-04 16:50:34 +05:30
utils ci(e2e): port E2E CI stack to release-2.43 to unblock merges (#10004) 2026-08-04 16:50:34 +05:30
.babelrc Barebones code for v2 2020-11-17 14:20:58 -03:00
.detoxrc.json test(detox): re-submit RF→Detox migration without unintended UI changes (#9847) 2026-06-19 22:44:02 +05:30
CLAUDE.md ci(e2e): port E2E CI stack to release-2.43 to unblock merges (#10004) 2026-08-04 16:50:34 +05:30
create_android_emulator.sh ci(e2e): port E2E CI stack to release-2.43 to unblock merges (#10004) 2026-08-04 16:50:34 +05:30
generate_unified_report.js test(detox): re-submit RF→Detox migration without unintended UI changes (#9847) 2026-06-19 22:44:02 +05:30
inject-detox-settings.js Revert "e2e(detox): Rainforest to Detox migration (#9790)" (#9834) 2026-06-15 17:57:01 +02:00
package-lock.json ci(e2e): port E2E CI stack to release-2.43 to unblock merges (#10004) 2026-08-04 16:50:34 +05:30
package.json ci(e2e): port E2E CI stack to release-2.43 to unblock merges (#10004) 2026-08-04 16:50:34 +05:30
README.md ci(e2e): port E2E CI stack to release-2.43 to unblock merges (#10004) 2026-08-04 16:50:34 +05:30
save_maestro_zephyr.js feat(e2e): Run e2e against different server version when rc branch is pushed (#9800) 2026-07-07 18:02:13 +05:30
save_report.js ci(e2e): port E2E CI stack to release-2.43 to unblock merges (#10004) 2026-08-04 16:50:34 +05:30
tsconfig.json test(maestro): Add maestro test framework (#9788) 2026-06-26 15:08:55 +05:30
webhook_server.js Mattermost Blocks (#9764) 2026-06-29 18:42:16 +02:00

How to Run Detox Tests

This guide will help you set up and run Detox tests for your project.

Install Dependencies

First, navigate to the root directory of your project and install the necessary dependencies by running:

npm install

navigate to the detox folder and run npm install

Configure your server details

detox/e2e/support/test_config.ts defaults to one local server on port 8065. For iOS, both the test runner and simulator use http://localhost:8065. For Android, the test runner uses http://localhost:8065 while the emulator uses http://10.0.2.2:8065.

If you use a remote server, set the logical site variable to a URL reachable from both the host test runner and the device:

  export SITE_1_URL="https://your-server.example.com"
  export ADMIN_USERNAME="your-username"
  export ADMIN_PASSWORD="your-password"
  export ADMIN_EMAIL="your-email"

You can instead create a detox/.env file:

  # detox/.env
  SITE_1_URL=https://your-server.example.com
  ADMIN_USERNAME=your-username
  ADMIN_PASSWORD=your-password
  ADMIN_EMAIL=your-email@example.com

Note: no need to provide all variables, only the ones that differ.

SITE_2_URL and SITE_3_URL are only needed for tests that add distinct Mattermost servers, such as server_login/server_list.e2e.ts. If they are unset, their URL helpers alias site 1 and tests that require distinct servers are skipped. The five-server ANDROID_SITE_* and IOS_SITE_* topology is assigned by CI and is not required for ordinary local runs.

Android

Build Detox Android App

To build the Detox Android app, navigate to the detox folder and run:

npm run e2e:android-build

The debug apk will be built and available at android/app/build/outputs/apk/debug/app-debug.apk

Run Detox Android Tests

Create emulator

./create_android_emulator.sh SDK_VERSION AVD_NAME

# example ./create_android_emulator.sh 34 pixel_5a_avd
# example ./create_android_emulator.sh 34 pixel_5a_avd --headless
# If we want to see the emulator logs. Run it in debug mode example ./create_android_emulator.sh 34 pixel_5a_avd --debug

To execute the Detox tests on Android, navigate to the detox folder and run:

npm run e2e:android-test

# To run a particular tests

npm run e2e:android-test <path to test file>

Local runs now default to a lower-memory setup so they coexist better with other tools:

  • Detox uses 1 worker unless you set DETOX_MAX_WORKERS.
  • Detox caps Node heap at 4096MB unless you already set NODE_OPTIONS.
  • Android Detox builds cap Gradle heap at 3072MB and use at most 2 Gradle workers unless you override MM_GRADLE_XMX_MB or MM_GRADLE_WORKERS_MAX.
  • Android emulator RAM defaults to 3072MB when created or refreshed; override with MM_ANDROID_EMULATOR_RAM_MB.

iOS

Build iOS Simulator

To build the iOS simulator for Detox, from the project folder run:

npm run e2e:ios-build

This will build the Simulator .zip file at the root folder.

Create a folder named mobile-artifacts at the project root. Move the zip file under mobile-artifacts and unzip it there.

# From project root
mkdir mobile-artifacts

Configure your local

Update detox/.detoxrc.json to specify which device you want to run it. Change:

 "device": {
     "type": "__DEVICE_NAME__",
     "os": "__DEVICE_OS_VERSION__"
 },

to something like:

 "device": {
     "type": "iPhone 17 Pro",
     "os": "iOS 26.4"
 },

To get your values run xcrun simctl list devices to find both the device and iOS version.

Run iOS Tests

To execute the Detox tests on iOS, navigate to the detox folder and run:

npm run e2e:ios-test

# To run a particular tests

npm run e2e:android-test path to test file.

Disabling Password Autofill

from iOS v26 password autofill can interfere with login tests by automatically filling credentials. To disable this feature on your simulator:

# Interactive mode - select simulator from list
npm run e2e:ios-disable-autofill

# Or specify simulator ID directly
npm run e2e:ios-disable-autofill -- --simulator-id SIMULATOR_UDID

When to use this:

  • if using iOS 26 or later
  • Before running iOS E2E tests if you notice password fields being auto-filled
  • When login tests fail unexpectedly due to autofill interference
  • After creating a new iOS simulator for testing

Note: CI environments automatically disable this setting, so this is only needed for local development.

Finding your simulator ID:

xcrun simctl list devices | grep Booted

TIP : For iOS, you can download the simulator from ~Mobile: Test build or ~Release: Mobile Apps channel in the community.

Results

The Local Runs generate artifacts under detox/artifacts/ios-debug-** or detox/artifacts/android-debug-**. You can see the html report, failure screenshot under that folder.

Webhook sidecar (mm_blocks / interactive dialog specs)

The mm_blocks_* and interactive-dialog specs register integrations that the Mattermost server calls back into. detox/webhook_server.js serves those callbacks on :3000, and the server must be able to reach it:

  • Local server (SITE_1_URL=http://localhost:8065): no tunnel needed, the server reaches 127.0.0.1:3000 directly.

    cd detox && npm run start:webhook
    
  • Remote/cloud test server: the server cannot reach your machine, so the sidecar needs a public HTTPS origin. detox/scripts/start_webhook_sidecar.sh starts webhook_server.js and resolves that origin, in priority order:

    Env Behaviour
    WEBHOOK_PUBLIC_BASE_URL Use an already-routable HTTPS origin as-is (named tunnel, ngrok, reverse proxy). Preferred.
    WEBHOOK_PUBLIC_BASE_URL + CLOUDFLARED_TUNNEL_TOKEN Run a named Cloudflare tunnel for that hostname. The token is passed via TUNNEL_TOKEN because argv is world-readable.
    neither Fall back to a trycloudflare.com quick tunnel. Time-boxed and DNS-flaky, so treat it as a last resort.
    cd detox
    SITE_1_URL="https://your-test-server" \
    WEBHOOK_PUBLIC_BASE_URL="https://your-tunnel-hostname" \
    bash scripts/start_webhook_sidecar.sh
    

    Pin a different cloudflared with CLOUDFLARED_VERSION; the script defaults to a known-good release rather than latest.

The script always exits 0 — it exports WEBHOOK_SIDECAR_READY and WEBHOOK_CALLBACKS_REACHABLE instead of failing, and specs that need a webhook fail fast on their own health check. A quick tunnel passes outbound health checks but does not reliably deliver server-to-sidecar callbacks, so it reports WEBHOOK_CALLBACKS_REACHABLE=false.

Playbooks Tests (AI-Powered Testing)

The Playbooks tests leverage AI-powered testing through the Wix Pilot framework, enabling natural language test creation and execution.

How It Works

The Playbooks tests utilize the pilot library, which connects to Anthropic Claude via the ClaudePromptHandler. This innovative approach allows you to:

  • Write tests using natural language prompts
  • Automatically translate prompts into executable test steps
  • Maintain tests more intuitively through conversational interfaces
  • Handle complex scenarios with AI-generated test logic

The system integrates seamlessly with your existing Detox test infrastructure while adding AI capabilities.

Best Practices

For reliable and effective Playbooks tests, follow these guidelines from the Pilot Best Practices Guide:

  1. Prompt Clarity:

    • Write clear, concise prompts
    • Use specific action-oriented language
    • Avoid ambiguous phrasing
  2. Test Design:

    • Keep prompts focused on single scenarios
    • Use deterministic language to prevent flaky tests
    • Structure complex tests as sequences of simple prompts
  3. Maintenance:

    • Review AI-generated test steps before committing
    • Version control both prompts and generated code
    • Monitor test stability regularly
  4. Integration:

    • Combine AI tests with traditional assertions
    • Use AI for complex scenarios, traditional methods for simple verifications
    • Document which tests are AI-generated

Getting Started

To run Playbooks tests:

  1. Set up your environment:

    export ANTHROPIC_API_KEY='your-api-key-here'
    
  2. Run tests normally:

    • The system will automatically initialize the ClaudePromptHandler when the API key is detected
    • No additional configuration is required beyond the API key
  3. Execution examples:

    # Run all Playbooks tests
    npm run e2e:android-test tests/playbooks/
    
    # Run a specific Playbook test
    npm run e2e:ios-test tests/playbooks/playbooks_basic.e2e.ts
    

Technical Notes

  • The Pilot framework handles the translation between natural language prompts and executable code
  • Tests are cached locally after generation for faster subsequent runs
  • All AI interactions are logged in the test artifacts for debugging
  • The system falls back to traditional testing methods if the API key isn't available

For complete documentation, see: