bmad-method-test-architecture-enterprise 1.22.3 → 1.22.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +18 -0
- package/docs/reference/knowledge-base.md +14 -14
- package/package.json +1 -1
- package/src/agents/bmad-tea/resources/knowledge/evidence-integrity.md +36 -12
- package/src/agents/bmad-tea/resources/knowledge/maestro-flows.md +114 -15
- package/src/agents/bmad-tea/resources/knowledge/mobile-ci-device-lab.md +89 -21
- package/src/agents/bmad-tea/resources/knowledge/mobile-test-strategy.md +15 -11
- package/src/agents/bmad-tea/resources/tea-index.csv +4 -4
- package/src/workflows/testarch/bmad-testarch-atdd/resources/knowledge/evidence-integrity.md +36 -12
- package/src/workflows/testarch/bmad-testarch-atdd/resources/knowledge/maestro-flows.md +114 -15
- package/src/workflows/testarch/bmad-testarch-atdd/resources/knowledge/mobile-ci-device-lab.md +89 -21
- package/src/workflows/testarch/bmad-testarch-atdd/resources/knowledge/mobile-test-strategy.md +15 -11
- package/src/workflows/testarch/bmad-testarch-atdd/resources/tea-index.csv +4 -4
- package/src/workflows/testarch/bmad-testarch-automate/resources/knowledge/evidence-integrity.md +36 -12
- package/src/workflows/testarch/bmad-testarch-automate/resources/knowledge/maestro-flows.md +114 -15
- package/src/workflows/testarch/bmad-testarch-automate/resources/knowledge/mobile-ci-device-lab.md +89 -21
- package/src/workflows/testarch/bmad-testarch-automate/resources/knowledge/mobile-test-strategy.md +15 -11
- package/src/workflows/testarch/bmad-testarch-automate/resources/tea-index.csv +4 -4
- package/src/workflows/testarch/bmad-testarch-ci/resources/knowledge/evidence-integrity.md +36 -12
- package/src/workflows/testarch/bmad-testarch-ci/resources/knowledge/maestro-flows.md +114 -15
- package/src/workflows/testarch/bmad-testarch-ci/resources/knowledge/mobile-ci-device-lab.md +89 -21
- package/src/workflows/testarch/bmad-testarch-ci/resources/knowledge/mobile-test-strategy.md +15 -11
- package/src/workflows/testarch/bmad-testarch-ci/resources/tea-index.csv +4 -4
- package/src/workflows/testarch/bmad-testarch-framework/resources/knowledge/evidence-integrity.md +36 -12
- package/src/workflows/testarch/bmad-testarch-framework/resources/knowledge/maestro-flows.md +114 -15
- package/src/workflows/testarch/bmad-testarch-framework/resources/knowledge/mobile-ci-device-lab.md +89 -21
- package/src/workflows/testarch/bmad-testarch-framework/resources/knowledge/mobile-test-strategy.md +15 -11
- package/src/workflows/testarch/bmad-testarch-framework/resources/tea-index.csv +4 -4
- package/src/workflows/testarch/bmad-testarch-nfr/resources/knowledge/evidence-integrity.md +36 -12
- package/src/workflows/testarch/bmad-testarch-nfr/resources/knowledge/maestro-flows.md +114 -15
- package/src/workflows/testarch/bmad-testarch-nfr/resources/knowledge/mobile-ci-device-lab.md +89 -21
- package/src/workflows/testarch/bmad-testarch-nfr/resources/knowledge/mobile-test-strategy.md +15 -11
- package/src/workflows/testarch/bmad-testarch-nfr/resources/tea-index.csv +4 -4
- package/src/workflows/testarch/bmad-testarch-test-design/resources/knowledge/evidence-integrity.md +36 -12
- package/src/workflows/testarch/bmad-testarch-test-design/resources/knowledge/maestro-flows.md +114 -15
- package/src/workflows/testarch/bmad-testarch-test-design/resources/knowledge/mobile-ci-device-lab.md +89 -21
- package/src/workflows/testarch/bmad-testarch-test-design/resources/knowledge/mobile-test-strategy.md +15 -11
- package/src/workflows/testarch/bmad-testarch-test-design/resources/tea-index.csv +4 -4
- package/src/workflows/testarch/bmad-testarch-test-review/resources/knowledge/evidence-integrity.md +36 -12
- package/src/workflows/testarch/bmad-testarch-test-review/resources/knowledge/maestro-flows.md +114 -15
- package/src/workflows/testarch/bmad-testarch-test-review/resources/knowledge/mobile-ci-device-lab.md +89 -21
- package/src/workflows/testarch/bmad-testarch-test-review/resources/knowledge/mobile-test-strategy.md +15 -11
- package/src/workflows/testarch/bmad-testarch-test-review/resources/tea-index.csv +4 -4
- package/src/workflows/testarch/bmad-testarch-trace/resources/knowledge/evidence-integrity.md +36 -12
- package/src/workflows/testarch/bmad-testarch-trace/resources/knowledge/maestro-flows.md +114 -15
- package/src/workflows/testarch/bmad-testarch-trace/resources/knowledge/mobile-ci-device-lab.md +89 -21
- package/src/workflows/testarch/bmad-testarch-trace/resources/knowledge/mobile-test-strategy.md +15 -11
- package/src/workflows/testarch/bmad-testarch-trace/resources/tea-index.csv +4 -4
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"name": "bmad-method-test-architecture-enterprise",
|
|
32
32
|
"source": "./",
|
|
33
33
|
"description": "Master Test Architect module for quality strategy, test automation, CI/CD quality gates, and structured testing education. Part of the BMad Method ecosystem.",
|
|
34
|
-
"version": "1.22.
|
|
34
|
+
"version": "1.22.4",
|
|
35
35
|
"author": {
|
|
36
36
|
"name": "Murat K Ozcan (TEA Creator) & Brian (BMad) Madison"
|
|
37
37
|
},
|
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.22.4] - 2026-08-14
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `mobile-ci-device-lab.md`: why a development build is frequently not the fix for a development shell. `developmentClient: true` sets the Gradle task to `:app:assembleDebug`, and a debug variant does not embed the JS bundle, so the app still needs a live packager and a manifest exchange at launch. Only a release variant embeds it, and SDK 54's `debugOptimized` is a debug variant that optimizes C++ only. Adds the release-APK path that composes with CI caching (`expo prebuild` plus `./gradlew :app:assembleRelease`, since `eas build --local` documents "Caching is not supported" and still needs an account), the note that a locally prebuilt release APK is debug-signed and therefore installs with no credentials, the `__DEV__`-is-false-in-release trap and the `EXPO_PUBLIC_` replacement, and that `scheme` has no effect in a shell so deep links cannot be tested there at all.
|
|
15
|
+
- `mobile-ci-device-lab.md`: the code-signed manifest as a CI-only failure. A shell that sends `expo-expect-signature` makes the CLI fetch a development code-signing certificate when the app config carries `extra.eas.projectId`; that fetch needs an account session, and under `EXPO_NO_INTERACTIVE=1` it fails with `CommandError: Input is required, but 'npx expo' is in non-interactive mode.` Names both fixes (an `EXPO_TOKEN` secret, or serving the manifest `--offline`), records that the trigger is `extra.eas.projectId` and not `owner`, and extends the manifest health-check rule to send every header the client sends, after a probe that omitted only the signature header reported a healthy manifest through five consecutive red runs.
|
|
16
|
+
- `mobile-ci-device-lab.md`: a section on repairing locally created AVDs. `avdmanager` cannot parse a dotted API level, so it writes `target=android-0`, which silently drops hardware acceleration and leaves the device `offline` with nothing in the log naming the cause; `hw.gpu.enabled=no` and `hw.keyboard=no` are equally wrong for a UI driver. Also `-gpu auto` over `-gpu swiftshader_indirect` for multiple emulators, and `-no-snapshot-save` over `-no-snapshot`, which also refuses to load a snapshot.
|
|
17
|
+
- `mobile-ci-device-lab.md`: per-device identity for sharded runs. Android has no readable equivalent of the iOS simulator name (`expo-device` reads `Settings.Global.DEVICE_NAME` on API 32+ and `bluetooth_name` below), and every emulator reports the product model, so four differently-named AVDs produce four identical keys and four shards share one fixture account while every flow still passes. The identity has to be written with `adb shell settings put`, read back from the namespace the app reads, and a duplicate has to fail the job.
|
|
18
|
+
- `maestro-flows.md`: `text:` selectors are regular expressions matched against the element's entire text. Worked example of an assertion that could never have matched at any value because its parentheses were a capture group, plus the escaping and whole-element-matching rules.
|
|
19
|
+
- `maestro-flows.md`: a COMPLETED tap is not a handled tap. `tapOn` reports COMPLETED once the touch is dispatched, and touches are lost between the driver and the app's handler often enough to matter (five taps recorded for two selections on one passing run). The fix is a per-action assertion inside a small `retry`, which keeps the check falsifiable while absorbing a driver-level loss, plus the reason an end-of-sequence assertion cannot diagnose which tap was lost.
|
|
20
|
+
- `evidence-integrity.md`: two further ways a verdict is unearned. A probe that sends a different request than the client takes a different branch and can be healthy while the app fails, both correctly at once. Reading back a written setting proves the write, not the behavior, which is what `hide_error_dialogs` does when the platform latched it at boot. Adds the discipline of recording what a change actually did rather than what it was for, so a fix kept for a different reason is not read as a found root cause by the next investigation.
|
|
21
|
+
- `mobile-test-strategy.md`: end-to-end runs must not depend on a live feature-flag, personalization, or experiment service evaluating a user the run created seconds earlier. Start the environment with the remote provider unconfigured so the seeded local value wins.
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- `mobile-ci-device-lab.md`: corrected the `--driver-host-port` guidance. The flag is absent from both `maestro --help` and `maestro test --help` on 2.8.0, so one Maestro process per machine with `--shard-split` is the rule rather than a default with an escape hatch. Adds that `--udid` / `--device` takes a comma-separated list on both platforms, so one sharding implementation covers Android serials and iOS UDIDs.
|
|
26
|
+
- `mobile-ci-device-lab.md` and `mobile-test-strategy.md`: shard count is chosen on measured wall clock rather than per-flow duration. Measured on one 14-core host: four emulators finished in 21.1 minutes against 27.3 for two, with per-flow times stretching under the oversubscription. Also adds `--test-output-dir` and `--flatten-debug-output` as the way to stop guessing the artifact layout, and the rule that a cascade of fast artifact-less failures after a driver timeout is one defect rather than four.
|
|
27
|
+
|
|
10
28
|
## [1.22.3] - 2026-08-14
|
|
11
29
|
|
|
12
30
|
### Added
|
|
@@ -73,15 +73,15 @@ CI/CD patterns, burn-in testing, and selective test execution.
|
|
|
73
73
|
|
|
74
74
|
Test quality standards, test level selection, TDD patterns, and the generation-safety gate.
|
|
75
75
|
|
|
76
|
-
| Fragment | Tier | Description
|
|
77
|
-
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- |
|
|
78
|
-
| [test-quality](https://github.com/bmad-code-org/bmad-method-test-architecture-enterprise/blob/main/src/agents/bmad-tea/resources/knowledge/test-quality.md) | core | Execution limits, isolation rules, green criteria
|
|
79
|
-
| [test-levels](https://github.com/bmad-code-org/bmad-method-test-architecture-enterprise/blob/main/src/agents/bmad-tea/resources/knowledge/test-levels-framework.md) | core | Guidelines for choosing unit, integration, or end-to-end coverage
|
|
80
|
-
| [test-priorities](https://github.com/bmad-code-org/bmad-method-test-architecture-enterprise/blob/main/src/agents/bmad-tea/resources/knowledge/test-priorities-matrix.md) | core | P0–P3 criteria, coverage targets, execution ordering
|
|
81
|
-
| [test-healing-patterns](https://github.com/bmad-code-org/bmad-method-test-architecture-enterprise/blob/main/src/agents/bmad-tea/resources/knowledge/test-healing-patterns.md) | core | Common failure patterns and automated fixes
|
|
82
|
-
| [confidence-gate](https://github.com/bmad-code-org/bmad-method-test-architecture-enterprise/blob/main/src/agents/bmad-tea/resources/knowledge/confidence-gate.md) | core | 1-10 confidence score with a stop-and-ask rule below threshold, so the agent declares unknowns instead of fabricating them
|
|
83
|
-
| [evidence-integrity](https://github.com/bmad-code-org/bmad-method-test-architecture-enterprise/blob/main/src/agents/bmad-tea/resources/knowledge/evidence-integrity.md) | core | Checks that cannot fail, three-state diagnostics, verifying framework properties before use,
|
|
84
|
-
| [component-tdd](https://github.com/bmad-code-org/bmad-method-test-architecture-enterprise/blob/main/src/agents/bmad-tea/resources/knowledge/component-tdd.md) | extended | Red→green→refactor workflow, provider isolation
|
|
76
|
+
| Fragment | Tier | Description | Key Topics |
|
|
77
|
+
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------- |
|
|
78
|
+
| [test-quality](https://github.com/bmad-code-org/bmad-method-test-architecture-enterprise/blob/main/src/agents/bmad-tea/resources/knowledge/test-quality.md) | core | Execution limits, isolation rules, green criteria | DoD, best practices, anti-patterns |
|
|
79
|
+
| [test-levels](https://github.com/bmad-code-org/bmad-method-test-architecture-enterprise/blob/main/src/agents/bmad-tea/resources/knowledge/test-levels-framework.md) | core | Guidelines for choosing unit, integration, or end-to-end coverage | Test pyramid, level selection |
|
|
80
|
+
| [test-priorities](https://github.com/bmad-code-org/bmad-method-test-architecture-enterprise/blob/main/src/agents/bmad-tea/resources/knowledge/test-priorities-matrix.md) | core | P0–P3 criteria, coverage targets, execution ordering | Prioritization, risk-based testing |
|
|
81
|
+
| [test-healing-patterns](https://github.com/bmad-code-org/bmad-method-test-architecture-enterprise/blob/main/src/agents/bmad-tea/resources/knowledge/test-healing-patterns.md) | core | Common failure patterns and automated fixes | Debugging, healing, fixes |
|
|
82
|
+
| [confidence-gate](https://github.com/bmad-code-org/bmad-method-test-architecture-enterprise/blob/main/src/agents/bmad-tea/resources/knowledge/confidence-gate.md) | core | 1-10 confidence score with a stop-and-ask rule below threshold, so the agent declares unknowns instead of fabricating them | Agent safety, generation, governance |
|
|
83
|
+
| [evidence-integrity](https://github.com/bmad-code-org/bmad-method-test-architecture-enterprise/blob/main/src/agents/bmad-tea/resources/knowledge/evidence-integrity.md) | core | Checks that cannot fail, three-state diagnostics, probes that issue the client's own request, verifying the outcome rather than the act, verifying framework properties before use, stating environment asymmetry, and recording what a change did rather than what it was for | Falsifiability, hollow green, diagnostics |
|
|
84
|
+
| [component-tdd](https://github.com/bmad-code-org/bmad-method-test-architecture-enterprise/blob/main/src/agents/bmad-tea/resources/knowledge/component-tdd.md) | extended | Red→green→refactor workflow, provider isolation | TDD, component testing |
|
|
85
85
|
|
|
86
86
|
**Used in:** `test-design`, `atdd`, `automate`, `test-review`, `trace`
|
|
87
87
|
|
|
@@ -179,11 +179,11 @@ Delivery-side testing for asynchronous, eventually-consistent webhook flows usin
|
|
|
179
179
|
|
|
180
180
|
Maestro device flows and the level discipline that decides what becomes a flow at all. Loaded when `test_stack_type` is `mobile` or when the review set contains a Maestro flow (`.yaml`/`.yml` under `maestro/` or `.maestro/`, or `*.flow.yaml` or `*.flow.yml`).
|
|
181
181
|
|
|
182
|
-
| Fragment | Tier | Description
|
|
183
|
-
| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- |
|
|
184
|
-
| [mobile-test-strategy](https://github.com/bmad-code-org/bmad-method-test-architecture-enterprise/blob/main/src/agents/bmad-tea/resources/knowledge/mobile-test-strategy.md) | specialized | Mobile test level framework, what belongs in a device flow, mobile risk categories, device matrix, CI shape
|
|
185
|
-
| [maestro-flows](https://github.com/bmad-code-org/bmad-method-test-architecture-enterprise/blob/main/src/agents/bmad-tea/resources/knowledge/maestro-flows.md) | specialized | Flow structure, selector hierarchy, `clearState` isolation, synchronization without sleeps, subflow composition, command semantics that differ by platform | Maestro, selectors, isolation, anti-patterns
|
|
186
|
-
| [mobile-ci-device-lab](https://github.com/bmad-code-org/bmad-method-test-architecture-enterprise/blob/main/src/agents/bmad-tea/resources/knowledge/mobile-ci-device-lab.md) | specialized | Build artifact selection, emulator snapshot caching,
|
|
182
|
+
| Fragment | Tier | Description | Key Topics |
|
|
183
|
+
| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- |
|
|
184
|
+
| [mobile-test-strategy](https://github.com/bmad-code-org/bmad-method-test-architecture-enterprise/blob/main/src/agents/bmad-tea/resources/knowledge/mobile-test-strategy.md) | specialized | Mobile test level framework, what belongs in a device flow, mobile risk categories, device matrix, CI shape, no live third-party flag evaluation in the run path | Levels, risk, device matrix, permissions, lifecycle |
|
|
185
|
+
| [maestro-flows](https://github.com/bmad-code-org/bmad-method-test-architecture-enterprise/blob/main/src/agents/bmad-tea/resources/knowledge/maestro-flows.md) | specialized | Flow structure, selector hierarchy, `clearState` isolation, synchronization without sleeps, subflow composition, command semantics that differ by platform, `text:` selectors as whole-element regex, taps that report COMPLETED without being handled | Maestro, selectors, isolation, regex selectors, anti-patterns |
|
|
186
|
+
| [mobile-ci-device-lab](https://github.com/bmad-code-org/bmad-method-test-architecture-enterprise/blob/main/src/agents/bmad-tea/resources/knowledge/mobile-ci-device-lab.md) | specialized | Build artifact selection including why a debug-variant development build does not solve it, dev-server manifest signing in non-interactive CI, emulator snapshot caching, repairing locally created AVDs, per-device identity for sharded runs, runner version pinning, artifact layout and failure diagnosis | Build artifact, emulator, caching, pinning, sharding, artifacts |
|
|
187
187
|
|
|
188
188
|
**Used in:** `framework`, `automate`, `atdd`, `test-design`, `test-review`, `ci` (when `test_stack_type` is `mobile` or a Maestro flow is present)
|
|
189
189
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "bmad-method-test-architecture-enterprise",
|
|
4
|
-
"version": "1.22.
|
|
4
|
+
"version": "1.22.4",
|
|
5
5
|
"description": "Master Test Architect for quality strategy, test automation, and release gates",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"bmad",
|
|
@@ -119,6 +119,8 @@ fi
|
|
|
119
119
|
|
|
120
120
|
**Rule**: a probe must observe the thing it claims to observe, never a proxy that merely correlates with it. Ask what else could make this check pass. Here the fix is the same as Example 4: open a throwaway listener inside the test process and assert that the process itself accepted a socket.
|
|
121
121
|
|
|
122
|
+
**A probe must also send the same request the real client sends.** A harness health-checked its development server's manifest endpoint and reported "manifest served, HTTP 200, multipart/mixed" through five consecutive red runs. It omitted one request header the app under test always sends, and that header selects a different branch through the server's middleware. Both results were correct at the same time: the endpoint the probe asked for was healthy, and the endpoint the app asked for was failing. Copy the client's method, headers, and body shape into the probe, or derive the probe from the client's own code path, and log which request was actually sent so the next reader can check the correspondence instead of assuming it.
|
|
123
|
+
|
|
122
124
|
**Key points**:
|
|
123
125
|
|
|
124
126
|
- A non-zero exit means "the command failed," which is not the same claim as "the condition is false"
|
|
@@ -165,20 +167,39 @@ A local pass proves the application path. It proves nothing about acceleration,
|
|
|
165
167
|
|
|
166
168
|
The probe would have reported the right answer and changed nothing, and the app would have loaded over one route while its API calls went over another. Order of operations is part of correctness in a harness: resolve every environment-dependent value first, then derive. If a value is discovered after its consumers are built, the discovery is telemetry rather than configuration.
|
|
167
169
|
|
|
170
|
+
### Example 7: Verifying the Act Is Not Verifying the Outcome
|
|
171
|
+
|
|
172
|
+
**Context**: A setting written to a running device, read back, and reported as being in effect.
|
|
173
|
+
|
|
174
|
+
A harness set `hide_error_dialogs=1` on an emulator, read the value back, confirmed it matched, and logged "system crash dialogs suppressed". The write had happened. The dialog appeared anyway: that setting is latched into the framework at boot and on configuration change, so writing it to an already-running device does not necessarily take effect. Reading a value back proves the write, and the write was never the claim.
|
|
175
|
+
|
|
176
|
+
**Rule**: name the observable the claim is about, and check that one. For a suppressed dialog it is the absence of the dialog in the view hierarchy, not the presence of the flag in the settings store. This is the same substitution as a proxy probe, moved one step earlier: the act stands in for the outcome instead of a correlate standing in for the condition. Configuration that a platform reads once, at a moment you do not control, is where it hides.
|
|
177
|
+
|
|
178
|
+
### Example 8: Record What a Change Did, Not What It Was For
|
|
179
|
+
|
|
180
|
+
**Context**: A fix introduced for one failure and kept after it turned out not to fix it.
|
|
181
|
+
|
|
182
|
+
One investigation removed a live feature-flag service from an end-to-end run, so a per-user flag that had been evaluated remotely fell back to a seeded database row. **It did not fix the flow it was introduced for.** It was kept anyway, because removing a remote dependency from an E2E run is correct on its own terms, and the write-up says all three things: what it was for, that it did not do that, and why it stayed.
|
|
183
|
+
|
|
184
|
+
A change described by its intent after its effect is known is a landmine for the next investigation, because the next reader takes the commit message as evidence that the cause was found and stops looking. State the outcome separately from the intent. "Kept for a different reason, and labelled as such" costs one sentence and saves someone a re-derivation.
|
|
185
|
+
|
|
168
186
|
## Anti-Patterns
|
|
169
187
|
|
|
170
|
-
| Anti-pattern | Why it fails
|
|
171
|
-
| ------------------------------------------------------- |
|
|
172
|
-
| Assertion with a soft or optional modifier as default | Cannot go red; reports coverage that does not exist
|
|
173
|
-
| `continue-on-error` on the test step | The suite cannot fail the build
|
|
174
|
-
| Runner manifest listing a subset of the suite | Files silently never run; the count is the only clue
|
|
175
|
-
| Missing tool reported as a failed condition | Sends the investigation at the wrong subsystem
|
|
176
|
-
| Probe observing a proxy that correlates with the target | Passes for a reason unrelated to the claim, and a green is never re-examined
|
|
177
|
-
|
|
|
178
|
-
|
|
|
179
|
-
|
|
|
180
|
-
|
|
|
181
|
-
|
|
|
188
|
+
| Anti-pattern | Why it fails | Fix |
|
|
189
|
+
| ------------------------------------------------------- | ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
|
|
190
|
+
| Assertion with a soft or optional modifier as default | Cannot go red; reports coverage that does not exist | Reserve softness for genuinely optional UI, and assert the outcome hard |
|
|
191
|
+
| `continue-on-error` on the test step | The suite cannot fail the build | Put it on artifact collection only; use `if: always()` for uploads |
|
|
192
|
+
| Runner manifest listing a subset of the suite | Files silently never run; the count is the only clue | Include by pattern; assert the executed count against the file count |
|
|
193
|
+
| Missing tool reported as a failed condition | Sends the investigation at the wrong subsystem | Three-state probes; distinct exit code for could-not-measure |
|
|
194
|
+
| Probe observing a proxy that correlates with the target | Passes for a reason unrelated to the claim, and a green is never re-examined | Ask what else could make this pass; observe the thing itself |
|
|
195
|
+
| Probe sending a different request than the client | Takes a different branch through the server: healthy probe, failing app, both correct | Copy the client's method, headers, and body into the probe, and log what was sent |
|
|
196
|
+
| Written setting read back and reported as effect | Proves the write; some configuration latches at boot and never applies live | Assert the observable the claim is about, not the act that was supposed to produce it |
|
|
197
|
+
| Change described by its intent once its effect is known | Reads as a found root cause and stops the next investigation looking | Record what it actually did, and why it was kept |
|
|
198
|
+
| Verdict emitted by the side that cannot observe it | Proves the wrong namespace | Move the assertion to the party whose route or state is in question |
|
|
199
|
+
| Comment asserting a mechanism with no source read | Propagates into other files and into other people's reasoning | Cite the doc or source line, or omit the mechanism |
|
|
200
|
+
| Local result used as a CI argument, asymmetry unstated | Hides the axes that actually differ | Tabulate the differing axes with the claim |
|
|
201
|
+
| Environment probe running after its consumers | The answer arrives too late to configure anything | Resolve environment-dependent values first, then derive |
|
|
202
|
+
| Reverting a newly-red check as a regression | Restores the hollow green and loses the finding | Treat the red as the pre-existing defect it exposed |
|
|
182
203
|
|
|
183
204
|
## Evidence Integrity Checklist
|
|
184
205
|
|
|
@@ -189,12 +210,15 @@ The probe would have reported the right answer and changed nothing, and the app
|
|
|
189
210
|
- [ ] **Platform-divergent assertions split**: no single assertion whose meaning depends on how a platform composes its view tree
|
|
190
211
|
- [ ] **Probes are three-state**: pass, fail, and could-not-measure, with distinct exit codes
|
|
191
212
|
- [ ] **Probes observe their own claim**: no proxy that merely correlates with the condition being reported
|
|
213
|
+
- [ ] **Probes issue the client's request**: same method, headers, and body shape as the code path they stand in for
|
|
214
|
+
- [ ] **Outcomes verified, not acts**: a write, a set flag, or a dispatched action is not evidence that behavior changed
|
|
192
215
|
- [ ] **Instruments verified before readings**: the probe confirms its tool exists before interpreting its result
|
|
193
216
|
- [ ] **Framework properties verified**: every key, flag, and command confirmed against the pinned version's docs or artifact
|
|
194
217
|
- [ ] **Mechanism comments cited**: any comment claiming why something works names its source
|
|
195
218
|
- [ ] **Verdicts emitted by the proving party**: cross-boundary claims asserted on the side that can observe them
|
|
196
219
|
- [ ] **Environment asymmetry stated**: local-versus-CI arguments list the differing axes
|
|
197
220
|
- [ ] **Resolution precedes derivation**: environment-dependent values resolved before any consumer is built
|
|
221
|
+
- [ ] **Effects recorded separately from intent**: a change kept for a reason other than the one it was made for says so
|
|
198
222
|
|
|
199
223
|
## Integration Points
|
|
200
224
|
|
|
@@ -296,6 +296,25 @@ tags:
|
|
|
296
296
|
point: '50%,73%' # map canvas: no accessibility node exists for the pin
|
|
297
297
|
```
|
|
298
298
|
|
|
299
|
+
```yaml
|
|
300
|
+
# `scrollUntilVisible` travels in ONE direction (default `DOWN`, 20s timeout, and a
|
|
301
|
+
# 100% visibility threshold) and stops where it stopped. A later search for an
|
|
302
|
+
# element ABOVE the current position keeps scrolling down and times out, which
|
|
303
|
+
# reads as "element missing" rather than "wrong direction".
|
|
304
|
+
- scrollUntilVisible:
|
|
305
|
+
element:
|
|
306
|
+
id: 'garment_row_9'
|
|
307
|
+
- scrollUntilVisible:
|
|
308
|
+
element:
|
|
309
|
+
id: 'garment_row_1' # already scrolled past; this fails as if the row were gone
|
|
310
|
+
|
|
311
|
+
# ✅ Name the direction, or return to a known position before searching again
|
|
312
|
+
- scrollUntilVisible:
|
|
313
|
+
element:
|
|
314
|
+
id: 'garment_row_1'
|
|
315
|
+
direction: UP
|
|
316
|
+
```
|
|
317
|
+
|
|
299
318
|
**Waiting, precisely**:
|
|
300
319
|
|
|
301
320
|
- Assertions carry a **default timeout of about 7 seconds**. `extendedWaitUntil` is the sanctioned way to ask for longer, and it names the condition while doing so.
|
|
@@ -309,22 +328,99 @@ tags:
|
|
|
309
328
|
- A command that reports COMPLETED while doing nothing turns every assertion downstream of it into decoration
|
|
310
329
|
- Prefer commands whose failure is observable over commands whose success is unconditional
|
|
311
330
|
|
|
331
|
+
### Example 7: `text:` Selectors Are Regular Expressions
|
|
332
|
+
|
|
333
|
+
**Context**: An assertion that reads exactly like the label on screen and could never have matched it.
|
|
334
|
+
|
|
335
|
+
**Implementation**:
|
|
336
|
+
|
|
337
|
+
```yaml
|
|
338
|
+
# ❌ Every `text:` value is a regex, and it must match the element's ENTIRE text.
|
|
339
|
+
# The parentheses here are a capture group, so this pattern demands the literal
|
|
340
|
+
# string `Garments 2 of <anything> selected`, while the label on screen reads
|
|
341
|
+
# `Garments (2 of 10 selected)`. It could not have matched at any selection count.
|
|
342
|
+
- assertVisible:
|
|
343
|
+
text: 'Garments (2 of .* selected)'
|
|
344
|
+
|
|
345
|
+
# ✅ Escape the characters that are literal
|
|
346
|
+
- assertVisible:
|
|
347
|
+
text: 'Garments \(2 of .* selected\)'
|
|
348
|
+
|
|
349
|
+
# ✅ Or match on the stable part, padded to cover the whole element
|
|
350
|
+
- assertVisible:
|
|
351
|
+
text: '.*2 of 10 selected.*'
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
**Key points**:
|
|
355
|
+
|
|
356
|
+
- `(`, `)`, `[`, `]`, `.`, `*`, `+`, `?`, `|`, `{`, `}`, `^`, `$` are all pattern syntax inside a `text:` value. Any label containing one needs escaping.
|
|
357
|
+
- Matching is against the **entire** element text, so a substring that is plainly on screen still fails without `.*` on both sides.
|
|
358
|
+
- The defect is invisible in review, because the YAML reads as the sentence a human sees on screen. Add the pattern to whatever lint the repository can host, because human review is demonstrably not the control that catches it.
|
|
359
|
+
- **Which direction it breaks depends on the command.** In `assertVisible` the impossible pattern fails after the default timeout and reads as a missing element, sending the diagnosis at the app rather than at the selector. In `assertNotVisible` it passes unconditionally, which is a check that cannot fail: see `evidence-integrity.md`.
|
|
360
|
+
|
|
361
|
+
### Example 8: A COMPLETED Tap Is Not a Handled Tap
|
|
362
|
+
|
|
363
|
+
**Context**: A checkbox in a virtualized list. Maestro reported the tap COMPLETED and the app never saw it.
|
|
364
|
+
|
|
365
|
+
Measured in isolation: the tap completed in 2.4 seconds, the target was present at `[45,1807][1035,1924]` with `clickable=true`, nothing sat above it in the hierarchy at the point tapped, and the count label still read `0 of 10` ten seconds later. Not a scroll, not an overlay, not the keyboard. `tapOn` reports COMPLETED once it has resolved the element and dispatched a touch, so its status describes the driver's action and not the app's response. One passing run recorded five taps for two selections, which makes the loss frequent rather than exotic.
|
|
366
|
+
|
|
367
|
+
**Implementation**:
|
|
368
|
+
|
|
369
|
+
```yaml
|
|
370
|
+
# ❌ The tap's status is not evidence that the state changed, and a single
|
|
371
|
+
# assertion at the end cannot say WHICH tap was lost.
|
|
372
|
+
- tapOn:
|
|
373
|
+
id: 'garment_checkbox_0'
|
|
374
|
+
- tapOn:
|
|
375
|
+
id: 'garment_checkbox_1'
|
|
376
|
+
- assertVisible:
|
|
377
|
+
text: '.*2 of 10 selected.*'
|
|
378
|
+
|
|
379
|
+
# ✅ Pair each tap with the assertion that proves it landed, inside a small retry.
|
|
380
|
+
# The assertion is a real one, so a genuinely broken selection still fails.
|
|
381
|
+
- retry:
|
|
382
|
+
maxRetries: 3
|
|
383
|
+
commands:
|
|
384
|
+
- tapOn:
|
|
385
|
+
id: 'garment_checkbox_0'
|
|
386
|
+
- assertVisible:
|
|
387
|
+
text: '.*1 of 10 selected.*'
|
|
388
|
+
- retry:
|
|
389
|
+
maxRetries: 3
|
|
390
|
+
commands:
|
|
391
|
+
- tapOn:
|
|
392
|
+
id: 'garment_checkbox_1'
|
|
393
|
+
- assertVisible:
|
|
394
|
+
text: '.*2 of 10 selected.*'
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
**Key points**:
|
|
398
|
+
|
|
399
|
+
- `retry` takes `maxRetries` between `0` and `3`, defaulting to `1`. Retrying one nondeterministic step is the sanctioned use; the documentation calls wrapping a large part of a flow an anti-pattern, and wrapping the entire flow explicitly unpredictable.
|
|
400
|
+
- **Assert per action, not once at the end.** An end-of-sequence assertion cannot name which tap was lost, and that ambiguity is what produces a confident wrong first hypothesis.
|
|
401
|
+
- The retry does not weaken the check. The assertion inside it has to pass on its own, so what the retry absorbs is a lost touch, which is a property of the driver rather than of the app.
|
|
402
|
+
- If a step routinely needs its retry to land, that is a finding about the driver or the list, not an ordinary step. Record it rather than letting the retry hide it.
|
|
403
|
+
|
|
312
404
|
## Anti-Patterns
|
|
313
405
|
|
|
314
|
-
| Anti-pattern
|
|
315
|
-
|
|
|
316
|
-
| Flow with no `assertVisible`/`assertTrue`
|
|
317
|
-
| `sleep` used as synchronization
|
|
318
|
-
| `tapOn: index:` on a list
|
|
319
|
-
| `tapOn: point:` coordinates
|
|
320
|
-
| No `clearState`
|
|
321
|
-
| Hardcoded credential or PII
|
|
322
|
-
| One flow covering six user journeys
|
|
323
|
-
| Required assertion inside `when:`
|
|
324
|
-
| `back` used as a cross-platform step
|
|
325
|
-
| `hideKeyboard` with a modal open
|
|
326
|
-
| `optional: true` on the assertion that carries the outcome
|
|
327
|
-
| `waitForAnimationToEnd` used as a wait-for-content
|
|
406
|
+
| Anti-pattern | Why it fails | Fix |
|
|
407
|
+
| ------------------------------------------------------------------ | ----------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- |
|
|
408
|
+
| Flow with no `assertVisible`/`assertTrue` | Passes as long as taps land; proves nothing about behavior | Assert the destination state of every flow |
|
|
409
|
+
| `sleep` used as synchronization | Flaky under load, slow when not | `extendedWaitUntil` on the real condition |
|
|
410
|
+
| `tapOn: index:` on a list | Breaks when the list reorders or the backend returns a different order | Scope by `text` with `below`/`containsChild` |
|
|
411
|
+
| `tapOn: point:` coordinates | Breaks on a different screen size or density | Address the element by `id` |
|
|
412
|
+
| No `clearState` | Flow depends on whatever ran before it; unreproducible in isolation | `clearState` before `launchApp` |
|
|
413
|
+
| Hardcoded credential or PII | Leaks in the repo and in CI logs | `${ENV_VAR}` sourced from the CI secret store |
|
|
414
|
+
| One flow covering six user journeys | A failure names the flow, not the behavior; slow to diagnose | One journey per flow, composed from subflows |
|
|
415
|
+
| Required assertion inside `when:` | Turns a real failure into a silent pass | Guard only genuinely optional UI (permission dialogs, upsells) |
|
|
416
|
+
| `back` used as a cross-platform step | Documented for Android and Web only; on iOS it does nothing and reports COMPLETED | Split with `runFlow: when: platform:`; tap the app's own control on iOS |
|
|
417
|
+
| `hideKeyboard` with a modal open | The Android implementation is the system back key, which dismisses a React Native modal | Tap a non-interactive element to drop the keyboard |
|
|
418
|
+
| `optional: true` on the assertion that carries the outcome | The step cannot fail, so the flow reports coverage it does not have | Assert hard; reserve `optional` for genuinely optional UI |
|
|
419
|
+
| `waitForAnimationToEnd` used as a wait-for-content | It succeeds when its cap is reached, so it cannot fail | `extendedWaitUntil` on the content that must appear |
|
|
420
|
+
| Unescaped regex characters in a `text:` selector | The value is a regex matched against the element's entire text, so it can never pass | Escape literal `(`, `)`, `[`, `]`, `.`; pad partial matches with `.*` |
|
|
421
|
+
| Tap status treated as proof the app handled the tap | `tapOn` reports COMPLETED once the touch is dispatched, and touches do get lost | Pair each state-changing tap with its own assertion inside a `retry` |
|
|
422
|
+
| One assertion at the end of a tap sequence | Cannot name which tap was lost, so the first hypothesis is a guess | Assert after every action that changes state |
|
|
423
|
+
| `scrollUntilVisible` reused after an earlier search moved the list | It travels only in the direction given, so the second search scrolls away from the target | Name the opposite `direction`, or return to a known position first |
|
|
328
424
|
|
|
329
425
|
## Maestro Flow Checklist
|
|
330
426
|
|
|
@@ -341,6 +437,9 @@ Before merging a flow:
|
|
|
341
437
|
- [ ] **Runs on both target platforms**, or declares its platform branch explicitly
|
|
342
438
|
- [ ] **Cross-platform commands verified**: every command used on both platforms is documented for both, or split by `runFlow: when: platform:`
|
|
343
439
|
- [ ] **Every assertion can fail**: no `optional: true` on the assertion that carries the flow's outcome, and no assertion sitting downstream of a command that no-ops on that platform
|
|
440
|
+
- [ ] **`text:` selectors read as regex**: literal `(`, `)`, `[`, `]`, `.` escaped, and whole-element matching accounted for
|
|
441
|
+
- [ ] **Every state-changing tap has its own assertion**, rather than one assertion covering a sequence
|
|
442
|
+
- [ ] **`retry` scoped to a single step**, with `maxRetries` inside the documented 0-3 range
|
|
344
443
|
|
|
345
444
|
## Integration Points
|
|
346
445
|
|
|
@@ -348,4 +447,4 @@ Before merging a flow:
|
|
|
348
447
|
- **Related fragments**: `mobile-test-strategy.md` (what belongs in a flow at all), `mobile-ci-device-lab.md` (the build artifact the flows run against, and the CI mechanics around them), `evidence-integrity.md` (why a step that cannot fail is the most expensive defect in a suite), `test-priorities-matrix.md` (P0-P3 tagging), `test-quality.md` (determinism and isolation standards), `selector-resilience.md` (the browser analogue of the selector hierarchy)
|
|
349
448
|
- **Tools**: `maestro test`, `maestro studio` (interactive flow authoring and element inspection), `maestro record`
|
|
350
449
|
|
|
351
|
-
_Source: Maestro flow syntax and command reference, mobile test-isolation practice, TEA test-quality standards applied to declarative
|
|
450
|
+
_Source: Maestro 2.8.0 flow syntax and command reference (selectors, `retry`, `scrollUntilVisible`), mobile test-isolation practice, TEA test-quality standards applied to declarative flows, and defects measured in a live Maestro suite_
|
|
@@ -31,6 +31,17 @@ Expo's own CI tutorial builds a dedicated EAS profile for this (`e2e-test`, with
|
|
|
31
31
|
|
|
32
32
|
The cost of getting this wrong is measurable in flow source. In one audit, about 120 of 195 lines in a single launch subflow existed solely to fight the development shell (dev-server readiness, manifest retries, a third-party app's own UI), and most of the defects fixed that week would not have existed against a compiled build. Workarounds for a wrong artifact do not stay in the harness; they migrate into the flows and become the suite.
|
|
33
33
|
|
|
34
|
+
### "Development Build" Is Not Automatically the Fix
|
|
35
|
+
|
|
36
|
+
A development build is the usual proposal once the shell is ruled out, and on Android it frequently changes nothing. In EAS, `developmentClient: true` sets the Gradle task to `:app:assembleDebug`, and a **debug variant does not embed the JS bundle**. The app still needs a live packager and a manifest exchange at launch, which is the same CI-only network surface the shell had. Only a **release** variant embeds the bundle. SDK 54's `debugOptimized` is the near miss worth naming: it optimizes the C++ layer and remains a debug variant, so the bundle is still served rather than embedded.
|
|
37
|
+
|
|
38
|
+
Getting to a release-variant APK without an account or a build service:
|
|
39
|
+
|
|
40
|
+
- **`eas build --local` composes badly with CI caching.** Expo documents "Caching is not supported" for local builds, and they still require `eas login` or an `EXPO_TOKEN`. `npx expo prebuild` followed by `./gradlew :app:assembleRelease` is the path that caches. See <https://docs.expo.dev/build-reference/local-builds/>.
|
|
41
|
+
- **A locally prebuilt release APK is debug-signed.** The generated `android/app/build.gradle` sets `release { signingConfig signingConfigs.debug }`, so the artifact installs on an emulator with no credentials, which is exactly what a device lab needs and not something to "fix".
|
|
42
|
+
- **`__DEV__` is `false` in a release build.** Any E2E affordance gated behind it silently disappears in the one build the suite is meant to run against. Move the switch to an `EXPO_PUBLIC_`-prefixed variable, which is inlined into the bundle at build time. Expo documents these as "visible in plain-text in your compiled application", so whatever the switch gates must be safe to ship: a throwaway credential against a disposable environment, never a real one.
|
|
43
|
+
- **Deep links cannot be exercised in the shell at all.** `scheme` is documented as "a build-time configuration, it has no effect in Expo Go", so a custom-scheme link there fails with `Activity not started, unable to resolve Intent`. Deep linking is how most device flows enter a screen, which makes this decisive on its own.
|
|
44
|
+
|
|
34
45
|
## If the Suite Must Run Against a Dev Server
|
|
35
46
|
|
|
36
47
|
Sometimes the compiled build is not ready yet and the dev-server path has to work for one release. Treat it as a temporary configuration with these constraints:
|
|
@@ -38,11 +49,27 @@ Sometimes the compiled build is not ready yet and the dev-server path has to wor
|
|
|
38
49
|
- **Reach the host over the debug bridge, not the guest NIC.** `adb reverse tcp:8081 tcp:8081` tunnels over the adb transport, so it survives emulator network breakage that would kill a `10.0.2.2` route. Pin the device with `adb -s <serial>` when more than one is attached.
|
|
39
50
|
- **Do not verify the forward by connecting to it from the device.** A reverse mapping gives the device a local listener on that port unconditionally, so the connect succeeds whether or not anything on the host is behind it. That probe measures that the mapping exists and reports that the server is reachable. Prove it from the host process instead: accept a socket and assert that the accept happened.
|
|
40
51
|
- **Do not assume the toolchain set it up.** Expo CLI issues `adb reverse` from the path where the CLI itself opens the app. Start the server without that flag and the forward silently never happens.
|
|
41
|
-
- **
|
|
52
|
+
- **The shell may be asking for a SIGNED manifest, and signing needs an account.** Expo Go sends `expo-expect-signature` with `keyid="expo-root"`. When the app config carries `extra.eas.projectId`, `@expo/cli` answers by fetching a development code-signing certificate from Expo's API and caching it under `~/.expo/codesigning/<projectId>`. That fetch resolves the current user, and with no session it prompts; under `EXPO_NO_INTERACTIVE=1` the prompt cannot be answered and the request dies with `CommandError: Input is required, but 'npx expo' is in non-interactive mode.` A developer machine never sees this, because `~/.expo/state.json` holds a session and the certificate is already cached. A fresh runner has neither, which is why the failure is CI-only and survives every emulator, image, and network change tried against it. **The trigger is `extra.eas.projectId`, not `owner`**; removing `owner` changes nothing. Two fixes work: an `EXPO_TOKEN` secret (Expo's documented CI authentication), or starting the server `--offline`, which skips the network requests behind the signing path and serves an unsigned manifest, which the shell accepts. Apply whichever you choose on every path, local and CI, so the two do not diverge on the one axis that only breaks in CI.
|
|
53
|
+
- **Health-check the manifest the way the client asks for it.** A bare `GET /` with no headers returns `200` and a browser interstitial, so a harness can log "dev server reachable" while every client request fails. Send **every** header the client sends (the platform header, `accept: multipart/mixed`, and the signature-expectation header above), and log the response body: the CLI serializes manifest-path errors as a JSON `error` payload with status `500`. One harness omitted only the signature header and reported "manifest served, HTTP 200, multipart/mixed" through five consecutive red runs, because that one header selects a different branch through the middleware than the app under test takes. See `evidence-integrity.md`: a probe must issue the request it stands in for.
|
|
42
54
|
- **Read the discriminating log line.** `Remote update request not successful` is emitted at exactly one place in `expo-updates`, guarded by the HTTP client's 200-299 check. If it appears, an HTTP response arrived with an error status, which makes it a manifest or HTTP problem and rules out connectivity. The surrounding generic lines (`Failed to download remote update`, `Failed to launch embedded or launchable update`) appear for any failure including connection-refused, so only the specific line carries information. Source: `packages/expo-updates/android/src/main/java/expo/modules/updates/loader/FileDownloader.kt` in <https://github.com/expo/expo>.
|
|
43
55
|
- **Expect the app config to be evaluated per request.** The manifest handler re-reads the project config on every manifest request, so config plugins run per request. Anything environment-sensitive in that config is a live macOS-versus-Linux divergence axis.
|
|
44
56
|
- **Do not build on undocumented packager host variables.** They carry a "drop the undocumented env variables" note upstream, and setting one can break a working `adb reverse` plus loopback setup by advertising a different host back to the client.
|
|
45
57
|
|
|
58
|
+
## Local Emulators Need Repair After Creation
|
|
59
|
+
|
|
60
|
+
`avdmanager create avd` does not hand back a device a UI driver can use. Three defects, all measured on an Apple Silicon host, all needing a post-creation edit:
|
|
61
|
+
|
|
62
|
+
- **`target=android-0` in the AVD's `.ini` pointer file.** `avdmanager` cannot parse a dotted API level, so a system image such as `system-images;android-36.1;google_apis;arm64-v8a` writes `target=android-0`. The emulator cannot resolve the platform, silently drops hardware acceleration (`hvf is not enabled on this aarch64 host`, then `qemu_mprotect__osdep: mprotect failed: Permission denied`) and software-emulates ARM64 on an ARM64 host. The device never leaves `offline` in `adb devices`, with nothing in the log naming the cause. The same parse failure leaves `avd.id` and `avd.name` as the literal string `<build>`. Correcting `target` makes the identical AVD boot with no acceleration warnings.
|
|
63
|
+
- **`hw.gpu.enabled=no`**, which leaves gfxstream logging `Failed to make display surface context current` and boot never completing.
|
|
64
|
+
- **`hw.keyboard=no`**, wrong for any suite that types through `adb`.
|
|
65
|
+
|
|
66
|
+
Two launch flags worth pinning while you are there:
|
|
67
|
+
|
|
68
|
+
- **`-gpu auto`, not `-gpu swiftshader_indirect`.** Software rendering is fine for a single emulator and does not survive several at once on the same host.
|
|
69
|
+
- **`-no-snapshot-save`, not `-no-snapshot`.** The latter also refuses to LOAD a snapshot, which makes every boot cold and quietly undoes the caching work below.
|
|
70
|
+
|
|
71
|
+
**Read the values back and fail on a mismatch.** A creation script that writes the right lines and never checks them produces exactly the failure this section describes: an AVD that looks configured, boots to `offline`, and costs a full run to diagnose.
|
|
72
|
+
|
|
46
73
|
## Android Emulator on Hosted Runners
|
|
47
74
|
|
|
48
75
|
Using `reactivecircus/android-emulator-runner`:
|
|
@@ -55,11 +82,36 @@ Using `reactivecircus/android-emulator-runner`:
|
|
|
55
82
|
- **Do not use ATD images for UI-driver suites.** The automated-test-device variants strip SystemUI, the launcher, and the IME, and disable hardware rendering. A UI driver needs exactly those. The gain is roughly a fifth of runtime and it is not worth a suite that cannot see the system UI.
|
|
56
83
|
- **Treat a known-bad base image as a hypothesis to falsify, never as a diagnosis.** Specific API levels do go bad on hosted runners for months at a time, with open reports of no network connectivity or a system-UI ANR that holds window focus, so the tracker is worth reading before pinning an older level. It is not worth believing on a symptom match. One investigation adopted a reported no-network defect as its root cause on the strength of a false-negative probe, bumped the API level on that basis, and reproduced the identical failure on the new level. Changing the image is a test of the hypothesis, and a green run is the only thing that confirms it.
|
|
57
84
|
|
|
85
|
+
## Per-Device Identity for Sharded Runs
|
|
86
|
+
|
|
87
|
+
When flows create and delete data for the signed-in user, each parallel device needs its own fixture account, and the app has to know which account is its own. The usual mechanism is a device-name-to-credential map in the bundle, with the app selecting its entry by reading its own device name.
|
|
88
|
+
|
|
89
|
+
On iOS the simulator name is a device property, so the app reads it. **On Android there is nothing to read.** `expo-device`'s `deviceName` resolves `Settings.Global.DEVICE_NAME` on API 32 and above, and the `bluetooth_name` secure setting below that; on an emulator both default to the product model. Measured: an AVD named `Medium_Phone_API_36.1` reports `sdk_gphone64_arm64`. **Four differently-named AVDs produce four identical map keys.** Every shard then signs in as the same user, the shards delete each other's data mid-flow, and every flow still passes. This is the most expensive shape of hollow green in this fragment, because the green is stable and what it hides is a data race.
|
|
90
|
+
|
|
91
|
+
The identity has to be **written** per device before the run, then proven:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
# The API level picks the namespace. Writing the wrong one succeeds and changes
|
|
95
|
+
# nothing the app can read, which is a silent version of the same defect.
|
|
96
|
+
api=$(adb -s "$serial" shell getprop ro.build.version.sdk | tr -d '\r')
|
|
97
|
+
if [ "$api" -ge 32 ]; then
|
|
98
|
+
adb -s "$serial" shell settings put global device_name "$name"
|
|
99
|
+
read_back=$(adb -s "$serial" shell settings get global device_name | tr -d '\r')
|
|
100
|
+
else
|
|
101
|
+
adb -s "$serial" shell settings put secure bluetooth_name "$name"
|
|
102
|
+
read_back=$(adb -s "$serial" shell settings get secure bluetooth_name | tr -d '\r')
|
|
103
|
+
fi
|
|
104
|
+
[ "$read_back" = "$name" ] || { echo "FAIL: $serial reports '$read_back'"; exit 1; }
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Then reconcile across the booted set: **a duplicate key is a hard error, never a warning.** A warning here becomes a shared account, and a shared account becomes a suite that cannot fail for the right reason.
|
|
108
|
+
|
|
58
109
|
## Version Drift and Artifact Layout
|
|
59
110
|
|
|
60
111
|
- **Pin the runner and assert what resolved.** Package-manager and `curl | bash` installers both float. Set an explicit version variable, and assert the reported version in CI. Checking that the binary exists does not catch drift; one project ran eight releases apart between local and CI without noticing.
|
|
61
112
|
- **Do not hardcode the artifact layout.** Older Maestro versions wrote a flat run directory: `commands-(Flow Name).json` and `screenshot-<status>-<epoch>-(Flow Name).png` side by side. Newer versions write a directory per flow: `<timestamp>/<Flow Name>/commands.json`, plus `screen-hierarchy/step-NNN-<command>-<target>.json`, `screenshots/`, and `logs/`. The change landed somewhere between those, so pin nothing to a version and glob nothing flat. Resolve the newest run directory and walk it.
|
|
62
113
|
- **The per-step hierarchy files are the upgrade worth having.** `screen-hierarchy/step-NNN-*.json` makes each step's view tree separately addressable, which is a strictly better diagnostic surface than one blob per flow: you can read what was on screen at the step before the failure, not only at the failure.
|
|
114
|
+
- **Or take the layout out of the equation.** `maestro test --test-output-dir <dir>` writes `manifest.json`, `commands.json`, and `logs/` directly into a directory you name, and `--flatten-debug-output` writes without per-run subfolders or timestamps. Both are in `maestro test --help` on 2.8.0. Naming the directory beats globbing for the newest one, and it survives the next layout change.
|
|
63
115
|
- **Upload the hierarchy dump, always.** It is the artifact people forget and the one that identifies a selector break.
|
|
64
116
|
|
|
65
117
|
## Diagnosing a Failed Run
|
|
@@ -71,31 +123,44 @@ Read, in this order:
|
|
|
71
123
|
3. **Device logs** for the app's own errors.
|
|
72
124
|
4. **The screenshot, last and with suspicion.** It is captured after teardown, so it frequently shows the launcher rather than the failing screen. Diagnosing from it produces confident wrong answers.
|
|
73
125
|
|
|
126
|
+
**Count root causes, not red flows.** One serial run failed with `Maestro Android driver did not start up in time`, and three further flows then failed in one to two seconds each with no artifacts written. Four red flows, one defect. A flow that failed in seconds and wrote nothing did not run; treat it as could-not-measure and diagnose the first failure, because a defect count inflated by a cascade sends the investigation at four subsystems instead of one.
|
|
127
|
+
|
|
74
128
|
## Parallelism
|
|
75
129
|
|
|
76
130
|
- `--shard-split N` divides the suite across N already-booted devices. `--shard-all N` runs the whole suite on each. Boot the devices first; neither flag provisions them.
|
|
77
|
-
-
|
|
131
|
+
- `--udid` (aliased `--device`) takes a comma-separated list on **both** platforms, so a single `--shard-split` invocation drives Android emulator serials and iOS simulator UDIDs through the same code path. One sharding implementation covers both.
|
|
132
|
+
- **Run one Maestro process per machine.** Two concurrent single-shard processes on one host have been observed to collide on the driver connection, failing with `Failed to connect to /127.0.0.1:7001` and `only one gesture can be performed at a time`. Drive every attached device from a single process with `--shard-split`. **There is no per-process driver port to escape with**: `--driver-host-port` is absent from both `maestro --help` and `maestro test --help` on 2.8.0. A handover note claiming an earlier release added it did not survive the check, which is `evidence-integrity.md`'s verify-the-property rule applied to a flag someone else told you about.
|
|
133
|
+
- **Choose the shard count on measured wall clock, not on per-flow duration.** Same suite, same 14-core / 48 GB host, same day: **four emulators finished in 21.1 minutes, two in 27.3.** Four does oversubscribe the host, with load average around 20 and per-flow times stretching from roughly 40 seconds to several minutes, and the total is still shorter. Wall clock is what gates a pull request, so the per-flow number tempts you to the wrong conclusion. Serial for comparison was roughly 1.5 to 2 hours.
|
|
78
134
|
|
|
79
135
|
## Anti-Patterns
|
|
80
136
|
|
|
81
|
-
| Anti-pattern
|
|
82
|
-
|
|
|
83
|
-
| E2E against a prebuilt development shell
|
|
84
|
-
|
|
|
85
|
-
|
|
|
86
|
-
|
|
|
87
|
-
|
|
|
88
|
-
|
|
|
89
|
-
|
|
|
90
|
-
|
|
|
91
|
-
|
|
|
92
|
-
|
|
|
93
|
-
|
|
|
94
|
-
|
|
|
137
|
+
| Anti-pattern | Why it fails | Fix |
|
|
138
|
+
| ------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
|
|
139
|
+
| E2E against a prebuilt development shell | Native modules absent; adds a CI-only launch path; flows fill with workarounds | Build a release-shaped artifact and install it |
|
|
140
|
+
| Development build adopted as the fix for the dev-server dependency | `developmentClient: true` builds a debug variant, which does not embed the JS bundle, so a packager is still required | Build a release variant; only it embeds the bundle |
|
|
141
|
+
| E2E affordance gated behind `__DEV__` | `__DEV__` is false in a release build, so the affordance is absent from the build under test | Gate on an `EXPO_PUBLIC_` variable and keep what it gates safe to ship in plain text |
|
|
142
|
+
| Signed-manifest path left intact in CI | The certificate fetch needs an account session; non-interactive CI cannot answer the prompt | Supply `EXPO_TOKEN`, or serve the manifest `--offline`, on every path |
|
|
143
|
+
| AVD used as the creation tool produced it | `target=android-0`, GPU off, keyboard off: acceleration silently drops or boot never completes | Repair the `.ini` and `config.ini`, then read the values back |
|
|
144
|
+
| Android device identity read rather than written | Every emulator reports the product model, so parallel shards share one map key and one account | Write `device_name` / `bluetooth_name` per device and hard-fail duplicates |
|
|
145
|
+
| Shard count chosen from per-flow duration | Oversubscription stretches each flow while still shortening the run | Choose on measured wall clock, which is what gates the PR |
|
|
146
|
+
| Every red flow counted as its own defect | A driver timeout cascades into fast, artifact-less failures | Diagnose the first failure; artifact-less seconds-long failures are could-not-measure |
|
|
147
|
+
| Multi-line `script:` in the emulator action | Each line is a separate `sh -c`; `set -e` and every variable are lost | One line invoking a real script file |
|
|
148
|
+
| Hardware inputs on both the create and the test step | `config.ini` is re-appended every run; the snapshot is rejected at boot | Pass them on the creation step only, or not at all |
|
|
149
|
+
| Cache key without an image version component | Runner-image bump silently invalidates the snapshot; permanent cold boots | Key on API level, target, arch, and image version |
|
|
150
|
+
| Combined cache step for the AVD | Saves only on success, so the run that built the snapshot never stores it | Split `cache/restore` and `cache/save` |
|
|
151
|
+
| ATD image under a UI driver | SystemUI, launcher, and IME are stripped; hardware rendering is off | Use a standard system image |
|
|
152
|
+
| Floating runner install | Local and CI drift apart silently; behavior differs with no version in the logs | Pin the version and assert the resolved version |
|
|
153
|
+
| Flat artifact glob | Breaks on the run-directory layout change | Resolve the newest run directory and walk it |
|
|
154
|
+
| Diagnosing from the failure screenshot | Taken after teardown; usually shows the launcher | Read the per-step status and the hierarchy dump |
|
|
155
|
+
| Host-side reachability check standing in for the device | Different network namespace; proves nothing about the guest | Prove it from the device, or forward the port over the debug bridge |
|
|
156
|
+
| Device-side connect used to verify an `adb reverse` forward | The mapping itself answers, so the check passes with nothing behind it | Accept a socket in the host process and assert the accept happened |
|
|
157
|
+
| Retries added over a configuration defect | Converts a reproducible failure into an intermittent one | Fix the configuration; keep retries for genuinely nondeterministic steps |
|
|
95
158
|
|
|
96
159
|
## Device Lab Checklist
|
|
97
160
|
|
|
98
161
|
- [ ] **Artifact decided first**: flows run against a release-shaped or development build, never a prebuilt development shell
|
|
162
|
+
- [ ] **Release variant confirmed**: the installed artifact embeds the JS bundle and launches with no packager running
|
|
163
|
+
- [ ] **No `__DEV__`-gated test affordance**: E2E switches ride an `EXPO_PUBLIC_` variable and gate nothing that must stay secret
|
|
99
164
|
- [ ] **Runner version pinned and asserted**: CI fails if the resolved version is not the pinned one
|
|
100
165
|
- [ ] **Emulator script is one line**: any real logic lives in a checked-in script file
|
|
101
166
|
- [ ] **Snapshot restore proven**: boot time recorded, and a rejected snapshot fails the job rather than passing slowly
|
|
@@ -103,14 +168,17 @@ Read, in this order:
|
|
|
103
168
|
- [ ] **Cache key carries an image version component**
|
|
104
169
|
- [ ] **Hardware acceleration verified on**, not left to chance
|
|
105
170
|
- [ ] **Standard system image**, not an ATD variant
|
|
106
|
-
- [ ] **
|
|
107
|
-
- [ ] **
|
|
108
|
-
- [ ] **
|
|
171
|
+
- [ ] **Locally created AVDs asserted after creation**: `target`, GPU, and keyboard read back rather than assumed written
|
|
172
|
+
- [ ] **Per-device identity written and proven unique** before any sharded run, with a duplicate failing the job
|
|
173
|
+
- [ ] **Artifacts uploaded**: per-step statuses, hierarchy dumps, screenshots, and device logs, resolved by run directory or written to a named output directory
|
|
174
|
+
- [ ] **Dev-server path, if used, is explicitly temporary**: port forwarded over the debug bridge, manifest health-checked with every header the client sends, signing resolved for a non-interactive session, and error bodies logged
|
|
175
|
+
- [ ] **Sharding matches the booted device count**, driven by one runner process per machine
|
|
176
|
+
- [ ] **Shard count justified by measured wall clock**, not by per-flow duration
|
|
109
177
|
|
|
110
178
|
## Integration Points
|
|
111
179
|
|
|
112
180
|
- **Used in workflows**: `*ci` (pipeline shape, caching, artifacts), `*framework` (scaffolding the device suite and its scripts), `*automate` (flows must not encode harness workarounds), `*nfr-assess` (boot and run duration as evidence)
|
|
113
181
|
- **Related fragments**: `mobile-test-strategy.md` (what belongs on a device at all), `maestro-flows.md` (flow-level quality and command semantics), `evidence-integrity.md` (three-state diagnostics and hollow green, which is where most of these defects hide), `ci-burn-in.md` (burn-in and sharding mechanics)
|
|
114
|
-
- **Tools**: `maestro test`, `adb`, `avdmanager`, `reactivecircus/android-emulator-runner`, EAS or the platform build toolchain
|
|
182
|
+
- **Tools**: `maestro test`, `adb`, `avdmanager`, `emulator`, `reactivecircus/android-emulator-runner`, EAS or the platform build toolchain
|
|
115
183
|
|
|
116
|
-
_Source: Maestro CLI
|
|
184
|
+
_Source: Maestro 2.8.0 CLI help and documentation; Expo app-config, local-build, and environment-variable documentation; `@expo/cli` code-signing source; `reactivecircus/android-emulator-runner` source and issue tracker; defects, measurements, and timings from live Maestro device-lab investigations on hosted runners and an Apple Silicon host_
|