@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.
- package/claude/skills/SKILLS_CONTEXT.md +15 -4
- package/claude/skills/manual-testing/SKILL.md +97 -24
- package/claude/skills/manual-testing/TEST_DESIGN_GUIDE.md +200 -0
- package/claude/skills/mobile-automation/BDD_TEMPLATES.md +221 -0
- package/claude/skills/mobile-automation/LOCATOR_PATTERNS.md +205 -0
- package/claude/skills/mobile-automation/MOBILE_MCP_REFERENCE.md +234 -0
- package/claude/skills/mobile-automation/SKILL.md +593 -0
- package/claude/skills/qa-agent/toolkit/qa-toolkit.cjs +62 -9
- package/package.json +1 -1
|
@@ -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 |
|