@roopesh.yadava/qa-pack 1.5.0 → 1.5.1

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.
@@ -0,0 +1,221 @@
1
+ # Mobile BDD Templates & Reference
2
+
3
+ > Load this file at the start of Phase 1 (Gherkin generation) and Phase 3 (POM), alongside
4
+ > `LOCATOR_PATTERNS.md` and `MOBILE_MCP_REFERENCE.md`.
5
+ > Contains the Gherkin format, step definition template, POM class template, platform-
6
+ > conditional step pattern, app-lifecycle steps, and permission-dialog handling.
7
+
8
+ ---
9
+
10
+ ## Gherkin Format Example
11
+
12
+ ```gherkin
13
+ Feature: <feature title from card>
14
+
15
+ Background: (optional — only if 2+ scenarios share the same Given)
16
+ Given the app is launched fresh
17
+
18
+ Rule: <business rule — taken verbatim from AC/COS>
19
+
20
+ @android @ios
21
+ Example: <Persona> as <role> <scenario description>
22
+ Given <persona> is on the <screen name> screen
23
+ When <persona> <action described as intent, not mechanics>
24
+ Then <persona> should <observable outcome>
25
+
26
+ @android
27
+ Example: <Android-only variation — e.g. hardware back button behavior>
28
+ Given ...
29
+ When <persona> presses the hardware back button
30
+ Then ...
31
+
32
+ @ios
33
+ Example: <iOS-only variation — e.g. swipe-back gesture>
34
+ Given ...
35
+ When <persona> swipes back from the left edge
36
+ Then ...
37
+
38
+ Scenario Outline: <persona> <flow description> with multiple <data type>
39
+ Given <persona> is on the <screen name> screen
40
+ When <persona> enters credentials "<username>" and "<password>"
41
+ Then <persona> should see "<expected_message>"
42
+
43
+ Examples:
44
+ | persona | username | password | expected_message |
45
+ | John | john@test.com | Admin@1234 | Verification passed |
46
+ | Maria | maria@test.com | Branch@1234 | Verification passed |
47
+ ```
48
+
49
+ **Platform tags:** tag every `Example`/`Scenario` `@android`, `@ios`, or both, when the
50
+ underlying interaction differs (see `LOCATOR_PATTERNS.md` §7 hardware BACK). Untagged
51
+ scenarios run on whichever `--project` is active per the config's platform matrix.
52
+
53
+ ---
54
+
55
+ ## Step Definition Template
56
+
57
+ ```javascript
58
+ const { Given, When, Then, Before, After } = require('@cucumber/cucumber');
59
+ const { faker } = require('@faker-js/faker');
60
+ const LoginScreen = require('../../Pages/Auth/login-screen.cjs');
61
+
62
+ let loginScreen;
63
+
64
+ // ── Unconditional Before — resolve platform + fresh app state ────────────────
65
+ Before(async function () {
66
+ this.platform = this.config.platform; // 'android' | 'ios' — set once, reused by every
67
+ // platform-conditional step in this run
68
+ });
69
+
70
+ // ── Steps ──────────────────────────────────────────────────────────────────
71
+
72
+ Given('{word} is on the {string} screen', async function (persona, screenName) {
73
+ loginScreen = new LoginScreen(this.screen);
74
+ await loginScreen.navigateTo(screenName);
75
+ });
76
+
77
+ When('{word} enters credentials {string} and {string}', async function (persona, username, password) {
78
+ await loginScreen.fillCredentials(username, password);
79
+ });
80
+
81
+ Then('{word} should see {string}', async function (persona, expectedMessage) {
82
+ await loginScreen.verifyMessage(expectedMessage);
83
+ });
84
+
85
+ // ── Platform-conditional step (see LOCATOR_PATTERNS.md §7) ───────────────────
86
+ When('{word} goes back', async function (persona) {
87
+ if (this.platform === 'android') {
88
+ await this.screen.pressButton('BACK');
89
+ } else {
90
+ await loginScreen.tapBackButton(); // POM method — POM decides the iOS-specific locator
91
+ }
92
+ });
93
+
94
+ // ── App lifecycle steps — reusable across every feature ──────────────────────
95
+ Given('the app is launched fresh', async function () {
96
+ await this.device.launchApp({ newInstance: true });
97
+ });
98
+
99
+ When('{word} backgrounds the app', async function (persona) {
100
+ await this.screen.pressButton('HOME');
101
+ });
102
+
103
+ When('{word} resumes the app', async function (persona) {
104
+ await this.device.launchApp(); // foregrounds an existing instance, does not cold-start
105
+ });
106
+
107
+ // ── Permission dialog handling — call after any step that may trigger one ────
108
+ When('the app requests permission', async function () {
109
+ const elements = await this.device.listElementsOnScreen();
110
+ const allowButton = elements.find(el => /allow|while using the app/i.test(el.text));
111
+ if (allowButton) {
112
+ await this.screen.getByText(/allow|while using the app/i).tap();
113
+ }
114
+ // No dialog present → no-op, don't fail the scenario over an OS dialog that didn't fire.
115
+ });
116
+ ```
117
+
118
+ ---
119
+
120
+ ## POM Class Template
121
+
122
+ ```javascript
123
+ const { expect } = require('@mobilewright/test');
124
+
125
+ class LoginScreen {
126
+ /**
127
+ * @param {import('@mobilewright/test').Screen} screen
128
+ */
129
+ constructor(screen) {
130
+ this.screen = screen;
131
+
132
+ // ── Locators (constructor ONLY — never inside methods) ───────────────
133
+ // All locators confirmed via mobile_list_elements_on_screen on the live device
134
+ // Priority: resource-id/accessibility-id → getByRole/getByLabel → getByText → getByType → coordinates
135
+ this.usernameInput = screen.getByTestId('login-username');
136
+ this.passwordInput = screen.getByTestId('login-password');
137
+ this.submitButton = screen.getByTestId('login-submit');
138
+ this.errorMessage = screen.getByTestId('login-error');
139
+ this.backButton = screen.getByTestId('nav-back-button'); // iOS-specific usage — see step def
140
+ }
141
+
142
+ // ── Navigation ───────────────────────────────────────────────────────
143
+
144
+ async navigateTo(screenName) {
145
+ // Screen reached via app navigation, not a URL — describe the path taken,
146
+ // not just the destination, since there's no direct deep link by default.
147
+ await this.screen.getByText(screenName).tap();
148
+ }
149
+
150
+ // ── Actions ──────────────────────────────────────────────────────────
151
+
152
+ async fillCredentials(username, password) {
153
+ await this.usernameInput.fill(username);
154
+ await this.passwordInput.fill(password);
155
+ await this.submitButton.tap();
156
+ }
157
+
158
+ async tapBackButton() {
159
+ await this.backButton.tap();
160
+ }
161
+
162
+ // ── Assertions ───────────────────────────────────────────────────────
163
+
164
+ async verifyMessage(expectedText) {
165
+ await expect(this.screen.getByText(expectedText)).toBeVisible();
166
+ }
167
+ }
168
+
169
+ module.exports = LoginScreen;
170
+ ```
171
+
172
+ ---
173
+
174
+ ## Faker.js Patterns (same library, same discipline as the web `automation` skill)
175
+
176
+ ```javascript
177
+ const { faker } = require('@faker-js/faker');
178
+
179
+ faker.string.alphanumeric(15) // random name
180
+ `test+${faker.string.alphanumeric(8)}@yourdomain.com` // email
181
+ faker.number.int({ min: 1, max: 100 })
182
+ faker.date.future().toISOString().split('T')[0] // 'YYYY-MM-DD'
183
+ ```
184
+
185
+ Generate inside the method — never in the constructor or at module scope.
186
+
187
+ ---
188
+
189
+ ## App-Lifecycle Step Catalog (reuse these before writing a new lifecycle step)
190
+
191
+ | Step | Underlying call |
192
+ |---|---|
193
+ | `the app is launched fresh` | `mobile_launch_app` / `device.launchApp({ newInstance: true })` — cold start |
194
+ | `{word} backgrounds the app` | `screen.pressButton('HOME')` |
195
+ | `{word} resumes the app` | `device.launchApp()` (no `newInstance`) — foregrounds, doesn't cold-start (see `LOCATOR_PATTERNS.md` §10) |
196
+ | `the app is force-closed` | `mobile_terminate_app` |
197
+ | `the app requests permission` | dump screen, tap Allow/While-using if a system dialog is present, no-op otherwise |
198
+ | `{word} rotates the device to {string}` | `mobile_set_orientation` + re-dump per `LOCATOR_PATTERNS.md` §9 |
199
+ | `{word} opens the deep link {string}` | `mobile_open_url` |
200
+
201
+ ---
202
+
203
+ ## Quick Reference
204
+
205
+ | Content | Location |
206
+ |---------|----------|
207
+ | Business rule | `Rule:` in `.feature` |
208
+ | Platform-specific variation | `@android` / `@ios` tag on the `Example`/`Scenario` |
209
+ | Persona/role variation | `Example:` block |
210
+ | Same flow, different data | `Scenario Outline` + `Examples:` |
211
+ | Locators discovered | `mobile_list_elements_on_screen` on the live device |
212
+ | Locators written | POM constructor only |
213
+ | Actions | POM async methods |
214
+ | Assertions | POM `verify*` methods |
215
+ | Generated test data | `faker.js` — inside methods |
216
+ | Direct `screen.getByX()` calls in step files | ❌ Never — always go through POM methods |
217
+ | Hardcoded coordinates | ❌ Last resort only, see `LOCATOR_PATTERNS.md` |
218
+ | Hardcoded waits | ❌ Never — use `expect(...).toBeVisible()` |
219
+ | `import`/`export` | ❌ Use `require`/`module.exports` (CommonJS, matches the web skill's convention unless the repo's own discovered convention says otherwise) |
220
+ | Gate 1 skipped | ❌ Never — always confirm Gherkin before writing code |
221
+ | Gate 2 skipped | ❌ Never — always confirm step defs before writing POM |
@@ -0,0 +1,205 @@
1
+ # Mobile Locator Patterns — Hard-Won Lessons
2
+
3
+ > Load this file before Phase 2 (Step Definitions) and Phase 3 (POM), alongside
4
+ > `BDD_TEMPLATES.md` and `MOBILE_MCP_REFERENCE.md`. Apply every rule below before writing
5
+ > any locator.
6
+
7
+ ## Locator Priority (strictly enforced)
8
+
9
+ | Priority | Method | Use when |
10
+ |----------|--------|----------|
11
+ | 1 ✅ | `screen.getByTestId('...')` | Always prefer — maps to `resource-id` (Android) / `accessibility-id` (iOS). Ask for one to be added if missing rather than falling further down this table |
12
+ | 2 ✅ | `screen.getByRole(...)` | When no resource-id/accessibility-id exists |
13
+ | 3 ⚠️ | `screen.getByLabel('...')` | Accessibility label present, no role match |
14
+ | 4 ⚠️ | `screen.getByText('...')` | Visible text only — fragile across locales, use a regex where wording may vary |
15
+ | 5 ⚠️ | `screen.getByType('...')` | Element type only — broad, matches multiple elements easily |
16
+ | 6 ❌ | `mobile_click_on_screen_at_coordinates(x, y)` | Last resort only — breaks on any resolution/orientation/layout change. Comment why no better locator existed |
17
+
18
+ ---
19
+
20
+ ### 1. `resource-id` and `accessibility-id` are not the same field
21
+
22
+ Android exposes `resource-id`; iOS exposes `accessibility-id` (sometimes surfaced as `name`
23
+ depending on the inspection tool). `mobile_list_elements_on_screen` returns whichever field
24
+ the platform actually has — checking only one will silently miss the other platform's
25
+ elements in a cross-platform project.
26
+
27
+ ```javascript
28
+ // ❌ WRONG — only checks the Android field name, misses every iOS element
29
+ const hasTestId = element['resource-id'];
30
+
31
+ // ✅ CORRECT — check both, platform-agnostic
32
+ const hasTestId = element['resource-id'] || element['accessibility-id'] || element['name'];
33
+ ```
34
+
35
+ `screen.getByTestId(...)` in MobileWright abstracts this for you at the framework level —
36
+ but when reasoning from a raw `mobile_list_elements_on_screen` dump during authoring or
37
+ self-heal, check both fields yourself before concluding "no testid on this element."
38
+
39
+ ---
40
+
41
+ ### 2. WebView / hybrid screens don't expose native resource-ids for their content
42
+
43
+ A screen embedding a WebView (in-app browser, hybrid checkout, help center) reports the
44
+ WebView container as one native element — the HTML content inside it is invisible to
45
+ `mobile_list_elements_on_screen`.
46
+
47
+ ```javascript
48
+ // ❌ WRONG — assumes the hybrid content has a resource-id, times out
49
+ this.helpArticleLink = screen.getByTestId('help-article-3');
50
+
51
+ // ✅ CORRECT — flag it, don't retry the native locator harder
52
+ // "This screen is a WebView — native resource-id inspection won't find its content.
53
+ // Falls back to getByText() on visible text, or needs a web-style inspection strategy."
54
+ this.helpArticleLink = screen.getByText('Refund policy');
55
+ ```
56
+
57
+ ---
58
+
59
+ ### 3. System permission dialogs interrupt the flow unpredictably
60
+
61
+ A location/camera/notification/contacts permission prompt is OS-level chrome, not app UI —
62
+ it can appear right after a navigation step and block every subsequent tap until dismissed.
63
+
64
+ ```javascript
65
+ // ❌ WRONG — assumes the next screen loaded; every following action times out
66
+ await screen.getByTestId('enable-location').tap();
67
+ await screen.getByTestId('map-view').waitFor(); // times out — a permission dialog is covering it
68
+
69
+ // ✅ CORRECT — check for a system dialog before assuming the locator is wrong
70
+ const elements = await mobile_list_elements_on_screen();
71
+ if (elements.some(el => /allow|deny|while using the app/i.test(el.text))) {
72
+ await screen.getByText(/allow|while using the app/i).tap();
73
+ }
74
+ await screen.getByTestId('map-view').waitFor();
75
+ ```
76
+
77
+ If a test hits "element not found" right after a step that plausibly triggers a permission
78
+ prompt, dump the screen and check for dialog text before concluding the locator is broken.
79
+
80
+ ---
81
+
82
+ ### 4. Coordinate taps are resolution/DPI-relative — never hardcode across a device matrix
83
+
84
+ A coordinate captured on one simulator breaks on a different device profile, and definitely
85
+ breaks across a `projects` matrix spanning multiple device sizes.
86
+
87
+ ```javascript
88
+ // ❌ WRONG — works on iPhone 15 Pro sim, wrong on every other device in the matrix
89
+ await mobile_click_on_screen_at_coordinates(187, 640);
90
+
91
+ // ✅ CORRECT — resolve locator by identifier, use coordinates only as documented last resort
92
+ await screen.getByTestId('agree-checkbox').tap();
93
+ ```
94
+
95
+ ---
96
+
97
+ ### 5. The soft keyboard shifts layout — re-dump before any coordinate fallback
98
+
99
+ Focusing a text field can push screen content upward to make room for the keyboard. Any
100
+ coordinates recorded before that focus are now wrong.
101
+
102
+ ```javascript
103
+ // ❌ WRONG — coordinate recorded before the keyboard appeared
104
+ await screen.getByLabel('Email').fill('user@example.com');
105
+ await mobile_click_on_screen_at_coordinates(200, 500); // may now hit a different element
106
+
107
+ // ✅ CORRECT — re-dump after any focus change if a coordinate fallback is unavoidable
108
+ await screen.getByLabel('Email').fill('user@example.com');
109
+ const elements = await mobile_list_elements_on_screen(); // re-dump post-keyboard
110
+ ```
111
+
112
+ ---
113
+
114
+ ### 6. OTP autofill can race a manual `type` action
115
+
116
+ Both platforms often auto-fill an SMS OTP into the field without any typed input. A step that
117
+ types digit-by-digit can collide with the autofill and leave the field in an unexpected state.
118
+
119
+ ```javascript
120
+ // ❌ WRONG — assumes typing succeeded, doesn't verify against a possible autofill race
121
+ await screen.getByTestId('otp-input').fill(otpCode);
122
+ await screen.getByTestId('verify-button').tap();
123
+
124
+ // ✅ CORRECT — verify the field's actual value before proceeding
125
+ await screen.getByTestId('otp-input').fill(otpCode);
126
+ await expect(screen.getByTestId('otp-input')).toHaveText(otpCode);
127
+ await screen.getByTestId('verify-button').tap();
128
+ ```
129
+
130
+ ---
131
+
132
+ ### 7. Hardware BACK is Android-only
133
+
134
+ `screen.pressButton('BACK')` has no iOS equivalent — iOS uses an edge swipe gesture or an
135
+ explicit on-screen back element instead.
136
+
137
+ ```javascript
138
+ // ❌ WRONG — a shared step across both platforms with no branch
139
+ When('{word} goes back', async function () {
140
+ await this.screen.pressButton('BACK'); // does nothing meaningful on iOS
141
+ });
142
+
143
+ // ✅ CORRECT — platform-conditional step, resolved from the World/config
144
+ When('{word} goes back', async function () {
145
+ if (this.platform === 'android') {
146
+ await this.screen.pressButton('BACK');
147
+ } else {
148
+ await this.screen.getByTestId('nav-back-button').tap(); // or a swipe gesture
149
+ }
150
+ });
151
+ ```
152
+
153
+ ---
154
+
155
+ ### 8. Dynamic/virtualized lists — an element not in the dump may just be off-screen
156
+
157
+ Long lists (`RecyclerView`, `UICollectionView`, virtualized FlatLists) only render items near
158
+ the viewport. `mobile_list_elements_on_screen` returning no match for a list item doesn't
159
+ always mean the locator is wrong.
160
+
161
+ ```javascript
162
+ // ❌ WRONG — concludes the locator is broken after one failed dump
163
+ // "getByTestId('item-42') not found — locator must be wrong"
164
+
165
+ // ✅ CORRECT — swipe toward the expected item first, then re-check
166
+ await screen.swipe('up', { distance: 400 });
167
+ const elements = await mobile_list_elements_on_screen(); // re-dump after scroll
168
+ ```
169
+
170
+ ---
171
+
172
+ ### 9. Orientation changes re-lay the whole screen
173
+
174
+ A `resource-id`/`accessibility-id` usually survives rotation; its on-screen coordinates never
175
+ do. Any scenario that rotates mid-flow must re-dump before falling back to a coordinate
176
+ locator afterward.
177
+
178
+ ```javascript
179
+ // ❌ WRONG — coordinate captured in portrait, used again after rotating to landscape
180
+ await mobile_set_orientation('landscape');
181
+ await mobile_click_on_screen_at_coordinates(300, 120); // stale — layout has changed
182
+
183
+ // ✅ CORRECT
184
+ await mobile_set_orientation('landscape');
185
+ const elements = await mobile_list_elements_on_screen(); // re-dump post-rotation
186
+ ```
187
+
188
+ ---
189
+
190
+ ### 10. Backgrounding vs terminating are different device states
191
+
192
+ `mobile_terminate_app` forces a cold start on next launch. Pressing `HOME`
193
+ (`screen.pressButton('HOME')`) leaves the app alive in the background. A scenario testing
194
+ "resume where you left off" needs HOME + relaunch, not terminate + launch — using the wrong
195
+ one tests the wrong behavior entirely.
196
+
197
+ ```javascript
198
+ // ❌ WRONG — tests cold-start behavior when the scenario is about background resume
199
+ await mobile_terminate_app(bundleId);
200
+ await mobile_launch_app(bundleId);
201
+
202
+ // ✅ CORRECT — genuine background/resume
203
+ await screen.pressButton('HOME');
204
+ await mobile_launch_app(bundleId); // brings the backgrounded app back to foreground
205
+ ```
@@ -0,0 +1,234 @@
1
+ # Mobile MCP + MobileWright — Command & Tool Reference
2
+
3
+ > Load this file once at Phase 0 (Environment + Device Discovery). Do not reload later in
4
+ > the run — reference tools/commands by name from context, same discipline as the web
5
+ > `automation` skill applies to `LOCATOR_PATTERNS.md`.
6
+
7
+ ## Two tool surfaces — know which one to reach for
8
+
9
+ | Surface | Role | When to use |
10
+ |---|---|---|
11
+ | **Mobile MCP** (`mobile-next/mobile-mcp`) | Live device control — the mobile equivalent of Playwright MCP. Drives the actual simulator/emulator/real device during authoring and self-heal. | Discovering devices/apps, inspecting the current screen, tapping/typing/swiping to explore a flow before writing a locator, re-inspecting on self-heal |
12
+ | **MobileWright** (`mobilewright` CLI + `@mobilewright/test`) | The authored test framework — what the generated Gherkin/step-defs/POM actually run against. | Writing the real test code, running the real suite, generating reports |
13
+
14
+ Generated tests never call Mobile MCP tools directly. Mobile MCP is this skill's
15
+ authoring/inspection tool — exactly how Playwright MCP is used by the web `automation`
16
+ skill, never referenced from inside the `.feature`/step-def/POM files it produces.
17
+
18
+ ## Mobile MCP — Tool Catalog
19
+
20
+ From `mobile-next/mobile-mcp`. Verify exact names **and parameters** against whatever server
21
+ is actually connected before relying on them — MCP servers version-drift like anything else;
22
+ if a tool listed here doesn't exist, or takes different arguments than assumed, treat the
23
+ connected server as the current truth, not this table. In particular: when Phase 0c resolves
24
+ more than one connected device, check whether each tool call below needs an explicit device
25
+ identifier passed alongside it — don't assume every call implicitly targets "the" device
26
+ once more than one is available.
27
+
28
+ ### Device Management
29
+ | Tool | Purpose |
30
+ |---|---|
31
+ | `mobile_list_available_devices` | Lists simulators, emulators, and connected real devices |
32
+ | `mobile_get_screen_size` | Screen dimensions in pixels — needed before any coordinate-based tap |
33
+ | `mobile_get_orientation` / `mobile_set_orientation` | Read / change portrait ↔ landscape |
34
+
35
+ ### App Management
36
+ | Tool | Purpose |
37
+ |---|---|
38
+ | `mobile_list_apps` | Installed apps on the device |
39
+ | `mobile_launch_app` | Start an app by package/bundle ID |
40
+ | `mobile_terminate_app` | Stop a running app — cold-start on next launch (distinct from HOME — see `LOCATOR_PATTERNS.md` §10) |
41
+ | `mobile_install_app` | Deploy `.apk` / `.ipa` / `.app` / `.zip` |
42
+ | `mobile_uninstall_app` | Remove by bundle ID / package name |
43
+
44
+ ### Screen Interaction & UI Inspection
45
+ | Tool | Purpose | Token cost |
46
+ |---|---|---|
47
+ | `mobile_list_elements_on_screen` | Reads the real accessibility tree — coordinates + properties **including `resource-id` and accessibility-id fields**. Mobile equivalent of `browser_snapshot()` / DOM inspection. | Expensive — full tree. Once per new/changed screen, never per interaction |
48
+ | `mobile_take_screenshot` | Captures current screen, returned inline | Expensive if not saved |
49
+ | `mobile_save_screenshot` | Persists screenshot straight to a file | Cheap — zero response tokens, use for all evidence capture |
50
+ | `mobile_click_on_screen_at_coordinates` | Tap at x,y | Last-resort locator method only |
51
+ | `mobile_double_tap_on_screen` | Double-tap at x,y | — |
52
+ | `mobile_long_press_on_screen_at_coordinates` | Long-press at x,y | — |
53
+ | `mobile_swipe_on_screen` | Directional swipe (up/down/left/right) | — |
54
+
55
+ ### Input & Navigation
56
+ | Tool | Purpose |
57
+ |---|---|
58
+ | `mobile_type_keys` | Enter text, optional submit |
59
+ | `mobile_press_button` | `HOME` / `BACK` (Android only) / `VOLUME_UP` / `VOLUME_DOWN` / `ENTER` |
60
+ | `mobile_open_url` | Open a URL in the device browser — deep-link entry points |
61
+
62
+ ### Recording & Diagnostics
63
+ | Tool | Purpose |
64
+ |---|---|
65
+ | `mobile_start_screen_recording` / `mobile_stop_screen_recording` | Video capture across a multi-step flow |
66
+ | `mobile_list_crashes` / `mobile_get_crash` | Retrieve crash reports — pull the crash report into a bug candidate's Description when a self-heal assertion failure coincides with a crash |
67
+
68
+ ## Token Discipline
69
+
70
+ Mirrors the web `automation`/`manual-testing` skills' Playwright rules — same principle, different tool names:
71
+
72
+ | Operation | Use | Avoid | Why |
73
+ |---|---|---|---|
74
+ | Screen inspection | `mobile_list_elements_on_screen` once per new/changed screen | Calling it after every single tap | Full accessibility tree — same cost profile as `browser_snapshot()` |
75
+ | Evidence capture | `mobile_save_screenshot` | `mobile_take_screenshot` without saving | Inline image return burns response tokens |
76
+ | Repeated screens | Fingerprint check (below) before re-inspecting | Re-dumping a screen whose resource-ids haven't moved | Same principle as the web skill's DOM fingerprint cache |
77
+
78
+ ## Resource-ID / Accessibility-ID Detection ("is there a testid on this screen")
79
+
80
+ Before writing any locator, dump the current screen and check whether the target element
81
+ carries a stable identifier:
82
+
83
+ ```
84
+ mobile_list_elements_on_screen()
85
+ ```
86
+
87
+ For each element in the result, check in this order:
88
+ 1. **`resource-id`** (Android) or **`accessibility-id`** (iOS) — if present, this is the
89
+ mobile equivalent of a web `data-testid` and maps directly to MobileWright's
90
+ `screen.getByTestId(...)`. Check both field names — they aren't interchangeable across
91
+ platforms (see `LOCATOR_PATTERNS.md` §1).
92
+ 2. If absent, check for an accessibility label/role (`getByLabel` / `getByRole`).
93
+ 3. If neither, fall through to the Locator Priority table in `LOCATOR_PATTERNS.md`.
94
+
95
+ If `npx mobilewright --help` (checked once at Phase 0) reveals a dedicated UI-dump command in
96
+ the installed version, prefer it for authoring convenience — but `mobile_list_elements_on_screen`
97
+ stays the source of truth during self-heal since it reflects the live device, not a static
98
+ dump. Don't assume a specific dump subcommand name beyond what `--help`/`doctor` actually shows
99
+ for the installed version.
100
+
101
+ ## MobileWright — Setup & Environment Commands
102
+
103
+ ```bash
104
+ npx mobilewright doctor # environment health check (Xcode, ADB, simulators)
105
+ npx mobilewright doctor --json # machine-readable, for logging without narrating to chat
106
+ npx mobilewright doctor --category ios|android|system
107
+ npx mobilewright devices # list connected devices/simulators/emulators
108
+ npm init mobilewright@latest # scaffold config + example test (skips existing files)
109
+ ```
110
+
111
+ `mobilecli` (the device server) must already be running in a separate terminal —
112
+ `mobilecli start`. If any MCP tool call or `doctor` reports no device / connection refused,
113
+ stop and ask the user to confirm `mobilecli` is running before proceeding — same pattern as
114
+ the web skill's Playwright MCP pre-flight check.
115
+
116
+ ## MobileWright — Config Reference
117
+
118
+ `mobilewright.config.ts` (or `.js`) at the project root. Config values are defaults —
119
+ options passed directly to a launch call always win.
120
+
121
+ ```typescript
122
+ import { defineConfig } from 'mobilewright';
123
+
124
+ export default defineConfig({
125
+ platform: 'android', // 'ios' | 'android'
126
+ bundleId: 'com.example.app',
127
+ deviceName: /Pixel 7/,
128
+ installApps: './builds/app.apk',
129
+ timeout: 10_000,
130
+ retries: 1,
131
+ reporter: 'html',
132
+ });
133
+ ```
134
+
135
+ Multi-platform matrix:
136
+ ```typescript
137
+ export default defineConfig({
138
+ projects: [
139
+ { name: 'iOS', platform: 'ios', bundleId: 'com.app.ios' },
140
+ { name: 'Android', platform: 'android', bundleId: 'com.app.android' },
141
+ ],
142
+ });
143
+ ```
144
+
145
+ ## MobileWright — Test-Writing API (what generated POM/step defs call)
146
+
147
+ ```typescript
148
+ import { test, expect } from '@mobilewright/test';
149
+
150
+ // Finding elements — priority order matches LOCATOR_PATTERNS.md
151
+ screen.getByTestId('submit-button'); // 1st — maps to resource-id/accessibility-id
152
+ screen.getByRole('button', { name: 'Submit' }); // 2nd
153
+ screen.getByLabel('Username'); // 2nd (accessibility label)
154
+ screen.getByText('Sign In'); // 3rd — fragile across locales
155
+ screen.getByType('TextField'); // 4th — broad, last resort before coordinates
156
+
157
+ // Actions
158
+ await screen.getByText('Sign In').tap();
159
+ await screen.getByRole('button', { name: 'Submit' }).doubleTap();
160
+ await screen.getByText('Options').longPress();
161
+ await screen.getByLabel('Email').fill('user@example.com');
162
+ await screen.swipe('up');
163
+ await screen.swipe('down', { distance: 300 });
164
+ await screen.pressButton('HOME');
165
+ await screen.pressButton('BACK'); // Android only
166
+
167
+ // Assertions
168
+ await expect(screen.getByText('Welcome')).toBeVisible();
169
+ await expect(screen.getByRole('button', { name: 'Submit' })).toBeEnabled();
170
+ await expect(screen.getByTestId('greeting')).toHaveText('Hello, World');
171
+ await expect(screen.getByText('Error')).not.toBeVisible();
172
+ ```
173
+
174
+ ## MobileWright — Running Tests
175
+
176
+ Native runner:
177
+ ```bash
178
+ npx mobilewright test # all tests
179
+ npx mobilewright test tests/login.test.ts # one file
180
+ npx mobilewright test --grep "sign in"
181
+ npx mobilewright test --project=ios
182
+ npx mobilewright test --list # list without running
183
+ ```
184
+
185
+ BDD/Cucumber profile — this skill's default output shape, matching the pack's existing
186
+ reuse-first Gherkin approach:
187
+ ```bash
188
+ npm test # all features, default profile
189
+ npm run test:android # PLATFORM=android
190
+ npm run test:ios # PLATFORM=ios
191
+ npm run test:dry # validate steps without touching a device — mobile
192
+ # equivalent of `cucumber-js --dry-run`
193
+ npx cucumber-js test/features/Auth/login.feature
194
+ npx cucumber-js --tags @smoke
195
+ ```
196
+
197
+ Reports:
198
+ ```bash
199
+ npx mobilewright show-report
200
+ npx mobilewright show-report --host 0.0.0.0 --port 8080
201
+ npm run report:open # BDD profile's cucumber-report.html
202
+ ```
203
+
204
+ ## MobileWright — Project Structure (BDD/Cucumber profile)
205
+
206
+ Discover this at Phase 0 — never impose it if the repo already has its own layout, same rule
207
+ the web `automation` skill follows for its tree (including preserving any existing typos):
208
+
209
+ ```
210
+ test/
211
+ app/ # .apk / .ipa binaries
212
+ features/ # Gherkin .feature files
213
+ Pages/ # Page Object Model classes
214
+ step_definitions/ # Cucumber step implementations (Driver.js = World + hooks)
215
+ TestData/ # Faker-based test data generators
216
+ Utils/BasePage.js # Base class for all page objects
217
+ cucumber.js # Cucumber profile config
218
+ mobilewright.config.js
219
+ .env.example / .env
220
+ ```
221
+
222
+ ## Debugging
223
+
224
+ ```bash
225
+ DEBUG=mw:* npx mobilewright test # verbose logging
226
+ MWDEBUGIMPL=1 npx mobilewright test # verbose driver output
227
+ ```
228
+
229
+ | Issue | Fix |
230
+ |---|---|
231
+ | `mobilecli: command not found` | `npm install -g mobilecli@latest` |
232
+ | No devices found / connection refused | `adb kill-server && adb start-server && adb devices`, or confirm `mobilecli start` is running |
233
+ | Tests time out immediately | Confirm `mobilecli` running, device ID matches `mobilewright devices`, raise timeout |
234
+ | iOS not running on Linux/Windows | iOS requires macOS + Xcode — use `platform: 'android'` elsewhere |