@irsprs/mobwright 0.1.0
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/CHANGELOG.md +50 -0
- package/LICENSE +19 -0
- package/README.md +776 -0
- package/dist/cli/index.cjs +796 -0
- package/dist/cli/index.cjs.map +1 -0
- package/dist/cli/index.d.cts +1 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +773 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/index.cjs +1219 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +546 -0
- package/dist/index.d.ts +546 -0
- package/dist/index.js +1163 -0
- package/dist/index.js.map +1 -0
- package/package.json +100 -0
package/README.md
ADDED
|
@@ -0,0 +1,776 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
<img src="img/mobwright-logo.png" alt="Mobwright" width="280" />
|
|
3
|
+
<br />
|
|
4
|
+
<br />
|
|
5
|
+
|
|
6
|
+
<p>
|
|
7
|
+
<strong>Playwright-style mobile E2E testing — powered by AI</strong>
|
|
8
|
+
</p>
|
|
9
|
+
|
|
10
|
+
<p>
|
|
11
|
+
Write mobile tests that feel like Playwright.<br />
|
|
12
|
+
Run them on Android emulators & iOS simulators through Appium.<br />
|
|
13
|
+
Let AI resolve locators when you don't want to dig through XML.
|
|
14
|
+
</p>
|
|
15
|
+
|
|
16
|
+
<br />
|
|
17
|
+
|
|
18
|
+
<p>
|
|
19
|
+
<a href="#-quick-start"><img src="https://img.shields.io/badge/-Quick%20Start-6C63FF?style=for-the-badge&logoColor=white" alt="Quick Start" /></a>
|
|
20
|
+
<a href="#-api-reference"><img src="https://img.shields.io/badge/-API%20Docs-00D4AA?style=for-the-badge&logoColor=white" alt="API Docs" /></a>
|
|
21
|
+
<a href="#-ai-providers"><img src="https://img.shields.io/badge/-AI%20Providers-FF6B6B?style=for-the-badge&logoColor=white" alt="AI Providers" /></a>
|
|
22
|
+
<a href="#-examples"><img src="https://img.shields.io/badge/-Examples-FFA94D?style=for-the-badge&logoColor=white" alt="Examples" /></a>
|
|
23
|
+
</p>
|
|
24
|
+
|
|
25
|
+
<br />
|
|
26
|
+
|
|
27
|
+
<p>
|
|
28
|
+
<img src="https://img.shields.io/badge/status-pre--0.1-orange?style=flat-square" alt="Status" />
|
|
29
|
+
<img src="https://img.shields.io/badge/node-%3E%3D18.18-339933?style=flat-square&logo=node.js&logoColor=white" alt="Node" />
|
|
30
|
+
<img src="https://img.shields.io/badge/typescript-5.4+-3178C6?style=flat-square&logo=typescript&logoColor=white" alt="TypeScript" />
|
|
31
|
+
<img src="https://img.shields.io/badge/playwright-%3E%3D1.40-2EAD33?style=flat-square&logo=playwright&logoColor=white" alt="Playwright" />
|
|
32
|
+
<img src="https://img.shields.io/badge/appium-powered-662D91?style=flat-square&logo=appium&logoColor=white" alt="Appium" />
|
|
33
|
+
<img src="https://img.shields.io/badge/license-MIT-blue?style=flat-square" alt="MIT License" />
|
|
34
|
+
</p>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
> ⚠️ **Pre-1.0 — here be dragons.** The Locator API and AI provider abstraction are stable. `device.ai()`, CLI commands, and custom matchers are shipped and validated end-to-end. Some sharp edges remain. Pin your version until 1.0.
|
|
40
|
+
|
|
41
|
+
<br />
|
|
42
|
+
|
|
43
|
+
## ✨ Why Mobwright?
|
|
44
|
+
|
|
45
|
+
| Pain Point | Mobwright Solution |
|
|
46
|
+
|---|---|
|
|
47
|
+
| 🤯 Appium's verbose API | Playwright-style `.locator()`, `.tap()`, `.fill()` |
|
|
48
|
+
| 😵 Flaky element lookups | Built-in **auto-wait** — polls until visible + enabled |
|
|
49
|
+
| 🔎 Hunting through XML trees | **`device.ai('description')`** — describe elements in plain English |
|
|
50
|
+
| 🧩 Separate Android/iOS test files | Unified selector syntax, write once for both platforms |
|
|
51
|
+
| ⚙️ Complex session management | One session per test, automatic setup & teardown |
|
|
52
|
+
| 🥱 Setting up a new project | `npx mobwright init` scaffolds everything in seconds |
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
<br />
|
|
56
|
+
|
|
57
|
+
## 📋 Prerequisites
|
|
58
|
+
|
|
59
|
+
Before using Mobwright, ensure you have the following installed:
|
|
60
|
+
|
|
61
|
+
| Requirement | Version | Purpose |
|
|
62
|
+
|---|---|---|
|
|
63
|
+
| **Node.js** | ≥ 18.18 | Runtime |
|
|
64
|
+
| **pnpm** | ≥ 8.x | Package manager (recommended) |
|
|
65
|
+
| **Appium** | ≥ 2.x | Mobile automation server |
|
|
66
|
+
| **Android SDK** | Latest | Android emulator & ADB |
|
|
67
|
+
| **Xcode** | ≥ 15 | iOS simulator (macOS only) |
|
|
68
|
+
|
|
69
|
+
### Appium Drivers
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
# Install Appium globally
|
|
73
|
+
npm install -g appium
|
|
74
|
+
|
|
75
|
+
# Install platform drivers
|
|
76
|
+
appium driver install uiautomator2 # Android
|
|
77
|
+
appium driver install xcuitest # iOS
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
> 💡 **Tip:** After install, run `npx mobwright doctor` to verify everything's wired up correctly.
|
|
81
|
+
|
|
82
|
+
<br />
|
|
83
|
+
|
|
84
|
+
## 🚀 Quick Start
|
|
85
|
+
|
|
86
|
+
### 1. Scaffold a new project
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
npx mobwright init my-mobile-tests
|
|
90
|
+
cd my-mobile-tests
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The init wizard asks which platforms (Android, iOS, both), whether to enable AI, and whether to install dependencies. Non-interactive mode is supported too:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
npx mobwright init my-tests --platforms=android,ios --ai=deepseek --yes
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### 2. Configure environment
|
|
100
|
+
|
|
101
|
+
`mobwright init` generates a `.env.example` for you. Copy and fill in:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
cp .env.example .env
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
```ini
|
|
108
|
+
# ── Android ──────────────────────────────────────────
|
|
109
|
+
MOBWRIGHT_APP_PATH=/absolute/path/to/your/app.apk
|
|
110
|
+
MOBWRIGHT_ANDROID_AVD=Pixel_6_API_34
|
|
111
|
+
# MOBWRIGHT_ANDROID_APP_PACKAGE=com.example
|
|
112
|
+
# MOBWRIGHT_ANDROID_APP_ACTIVITY=com.example.MainActivity
|
|
113
|
+
|
|
114
|
+
# ── iOS ──────────────────────────────────────────────
|
|
115
|
+
MOBWRIGHT_IOS_DEVICE=iPhone 15
|
|
116
|
+
MOBWRIGHT_IOS_BUNDLE_ID=com.example.app
|
|
117
|
+
# MOBWRIGHT_IOS_APP_PATH=/absolute/path/to/My.app
|
|
118
|
+
# MOBWRIGHT_IOS_UDID=...
|
|
119
|
+
# MOBWRIGHT_IOS_PLATFORM_VERSION=17.5
|
|
120
|
+
|
|
121
|
+
# ── AI (optional) ───────────────────────────────────
|
|
122
|
+
# MOBWRIGHT_AI_PROVIDER=deepseek
|
|
123
|
+
# MOBWRIGHT_AI_API_KEY=sk-...
|
|
124
|
+
# MOBWRIGHT_AI_MODEL=deepseek-chat
|
|
125
|
+
# MOBWRIGHT_AI_BASE_URL=https://api.deepseek.com
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### 3. Verify your environment
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
npx mobwright doctor
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
You'll get a colorized summary of what's working and what's missing. Fix anything red before continuing.
|
|
135
|
+
|
|
136
|
+
### 4. Write your first test
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
import { test, expect } from 'mobwright';
|
|
140
|
+
|
|
141
|
+
test('app launches and shows welcome screen', async ({ device }) => {
|
|
142
|
+
const welcomeText = device.locator('~welcomeMessage');
|
|
143
|
+
|
|
144
|
+
// toBeVisible auto-retries up to 5s
|
|
145
|
+
await expect(welcomeText).toBeVisible();
|
|
146
|
+
await expect(welcomeText).toHaveText(/welcome/i);
|
|
147
|
+
|
|
148
|
+
// Tap a button
|
|
149
|
+
await device.locator('~getStartedButton').tap();
|
|
150
|
+
|
|
151
|
+
// Fill an input
|
|
152
|
+
await device.locator('#emailInput').fill('user@example.com');
|
|
153
|
+
|
|
154
|
+
// Or describe elements in English — AI finds the selector for you
|
|
155
|
+
await device.ai('the blue Continue button at the bottom').tap();
|
|
156
|
+
});
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### 5. Start Appium & run
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
# Terminal 1: Start Appium server
|
|
163
|
+
appium
|
|
164
|
+
|
|
165
|
+
# Terminal 2: Run tests via mobwright (recommended)
|
|
166
|
+
npx mobwright test --project android
|
|
167
|
+
npx mobwright test --project ios
|
|
168
|
+
|
|
169
|
+
# Or use playwright directly — both work
|
|
170
|
+
npx playwright test --project android
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
`mobwright test` is a thin wrapper that pre-flights the Appium connection and forwards everything else to `playwright test`. Every Playwright flag (`--grep`, `--ui`, `--workers`, etc.) works.
|
|
174
|
+
|
|
175
|
+
<br />
|
|
176
|
+
|
|
177
|
+
## 🛠️ CLI
|
|
178
|
+
|
|
179
|
+
Mobwright ships three commands:
|
|
180
|
+
|
|
181
|
+
| Command | What it does |
|
|
182
|
+
|---|---|
|
|
183
|
+
| `mobwright init [dir]` | Scaffold a new project with config, env example, and sample tests |
|
|
184
|
+
| `mobwright test` | Run your test suite (pre-flight Appium check + forwards to `playwright test`) |
|
|
185
|
+
| `mobwright doctor` | Check your environment for required tools, drivers, and env vars |
|
|
186
|
+
|
|
187
|
+
Run `mobwright help` for full flag reference.
|
|
188
|
+
|
|
189
|
+
<br />
|
|
190
|
+
|
|
191
|
+
## 🎯 Selector Syntax
|
|
192
|
+
|
|
193
|
+
Mobwright provides a concise selector syntax that works across both platforms:
|
|
194
|
+
|
|
195
|
+
| Prefix | Strategy | Example | Android Maps To | iOS Maps To |
|
|
196
|
+
|---|---|---|---|---|
|
|
197
|
+
| `~` | Accessibility ID | `~loginButton` | `content-desc` | `name` |
|
|
198
|
+
| `#` | Resource ID | `#emailInput` | `resource-id` (ends-with) | `name` |
|
|
199
|
+
| `//` | XPath | `//android.widget.Button` | Raw XPath | Raw XPath |
|
|
200
|
+
| *(none)* | Accessibility ID | `loginButton` | `content-desc` | `name` |
|
|
201
|
+
|
|
202
|
+
> **💡 Tip:** Prefer `~accessibilityId` selectors — they're the most reliable across platforms and are the recommended approach for cross-platform tests.
|
|
203
|
+
|
|
204
|
+
<br />
|
|
205
|
+
|
|
206
|
+
## 📖 API Reference
|
|
207
|
+
|
|
208
|
+
### `device` — The Test Fixture
|
|
209
|
+
|
|
210
|
+
Every test receives a `device` object that represents the connected mobile device.
|
|
211
|
+
|
|
212
|
+
```typescript
|
|
213
|
+
import { test, expect } from 'mobwright';
|
|
214
|
+
|
|
215
|
+
test('example', async ({ device }) => {
|
|
216
|
+
// device is ready — Appium session is active
|
|
217
|
+
});
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
| Property | Type | Description |
|
|
221
|
+
|---|---|---|
|
|
222
|
+
| `device.platform` | `Platform` | Current platform (`'android'` or `'ios'`) |
|
|
223
|
+
| `device.project` | `string` | Project name from Playwright config |
|
|
224
|
+
| `device.aiProvider` | `AIProvider \| undefined` | Attached AI provider (if configured) |
|
|
225
|
+
|
|
226
|
+
### `device.locator(selector, options?)`
|
|
227
|
+
|
|
228
|
+
Create a lazy reference to a UI element. **Does not query the device** until an action is called.
|
|
229
|
+
|
|
230
|
+
```typescript
|
|
231
|
+
const button = device.locator('~submitButton');
|
|
232
|
+
const input = device.locator('#emailField');
|
|
233
|
+
const element = device.locator('//android.widget.TextView[@text="Hello"]');
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
**Options:**
|
|
237
|
+
|
|
238
|
+
| Option | Type | Default | Description |
|
|
239
|
+
|---|---|---|---|
|
|
240
|
+
| `timeout` | `number` | `5000` | Max wait time in ms for actions |
|
|
241
|
+
|
|
242
|
+
### `device.getByText(text, options?)`
|
|
243
|
+
|
|
244
|
+
Locate an element by its visible text or label.
|
|
245
|
+
|
|
246
|
+
```typescript
|
|
247
|
+
const heading = device.getByText('Welcome to the App');
|
|
248
|
+
await heading.waitFor();
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### `device.ai(description, options?)`
|
|
252
|
+
|
|
253
|
+
Natural-language locator. AI resolves the description to a concrete selector on first action, then caches the result for the rest of the test.
|
|
254
|
+
|
|
255
|
+
```typescript
|
|
256
|
+
// Same surface as Locator — tap, fill, getText, isVisible, waitFor
|
|
257
|
+
await device.ai('the blue Continue button').tap();
|
|
258
|
+
await device.ai('the email input field').fill('me@example.com');
|
|
259
|
+
await device.ai('the welcome banner').waitFor();
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
**Options:**
|
|
263
|
+
|
|
264
|
+
| Option | Type | Default | Description |
|
|
265
|
+
|---|---|---|---|
|
|
266
|
+
| `timeout` | `number` | `5000` | Max wait time in ms for actions |
|
|
267
|
+
| `minConfidence` | `number` | `0.5` | Minimum confidence; throws below this |
|
|
268
|
+
| `treeCharBudget` | `number` | `12000` | Max chars of accessibility tree sent to AI |
|
|
269
|
+
|
|
270
|
+
Requires `MOBWRIGHT_AI_PROVIDER` and `MOBWRIGHT_AI_API_KEY` set. Throws `AIError` if not configured.
|
|
271
|
+
|
|
272
|
+
### `device.screenshot()`
|
|
273
|
+
|
|
274
|
+
Take a screenshot. Returns raw PNG bytes as a `Buffer`.
|
|
275
|
+
|
|
276
|
+
```typescript
|
|
277
|
+
const png = await device.screenshot();
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### `device.getPageSource()`
|
|
281
|
+
|
|
282
|
+
Get the current accessibility tree as an XML string. Useful for debugging and AI features.
|
|
283
|
+
|
|
284
|
+
```typescript
|
|
285
|
+
const xml = await device.getPageSource();
|
|
286
|
+
console.log(xml);
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
---
|
|
290
|
+
|
|
291
|
+
### `Locator` — Element Actions
|
|
292
|
+
|
|
293
|
+
All locator actions **auto-wait** for the element to be present, visible, and enabled before executing.
|
|
294
|
+
|
|
295
|
+
#### Core Actions
|
|
296
|
+
|
|
297
|
+
| Method | Returns | Description |
|
|
298
|
+
|---|---|---|
|
|
299
|
+
| `.tap()` | `Promise<void>` | Tap (click) the element |
|
|
300
|
+
| `.fill(text)` | `Promise<void>` | Clear and type text into the element |
|
|
301
|
+
| `.getText()` | `Promise<string>` | Get the visible text content |
|
|
302
|
+
| `.isVisible()` | `Promise<boolean>` | Check visibility instantly (no waiting) |
|
|
303
|
+
| `.isEnabled()` | `Promise<boolean>` | Check enabled state instantly (no waiting) |
|
|
304
|
+
| `.waitFor(options?)` | `Promise<void>` | Wait for element to become visible |
|
|
305
|
+
| `.tapAndHold(options?)` | `Promise<void>` | Long-press the element |
|
|
306
|
+
|
|
307
|
+
#### Swipe Gestures
|
|
308
|
+
|
|
309
|
+
```typescript
|
|
310
|
+
// Swipe within the element bounds
|
|
311
|
+
await element.swipeLeft();
|
|
312
|
+
await element.swipeRight();
|
|
313
|
+
await element.swipeUp();
|
|
314
|
+
await element.swipeDown();
|
|
315
|
+
|
|
316
|
+
// With options
|
|
317
|
+
await element.swipeLeft({ duration: 600, distance: 0.8 });
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
| Method | Options | Description |
|
|
321
|
+
|---|---|---|
|
|
322
|
+
| `.swipeLeft(opts?)` | `duration`, `distance` | Swipe left within bounds |
|
|
323
|
+
| `.swipeRight(opts?)` | `duration`, `distance` | Swipe right within bounds |
|
|
324
|
+
| `.swipeUp(opts?)` | `duration`, `distance` | Swipe up within bounds |
|
|
325
|
+
| `.swipeDown(opts?)` | `duration`, `distance` | Swipe down within bounds |
|
|
326
|
+
|
|
327
|
+
#### Scroll Gestures
|
|
328
|
+
|
|
329
|
+
Slower and longer than swipe — ideal for scrolling through lists and pages.
|
|
330
|
+
|
|
331
|
+
```typescript
|
|
332
|
+
await scrollableList.scrollDown();
|
|
333
|
+
await scrollableList.scrollUp({ duration: 1000, distance: 0.9 });
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
| Method | Options | Description |
|
|
337
|
+
|---|---|---|
|
|
338
|
+
| `.scrollUp(opts?)` | `duration`, `distance` | Scroll up inside a container |
|
|
339
|
+
| `.scrollDown(opts?)` | `duration`, `distance` | Scroll down inside a container |
|
|
340
|
+
|
|
341
|
+
---
|
|
342
|
+
|
|
343
|
+
### `expect` — Auto-retrying Matchers
|
|
344
|
+
|
|
345
|
+
Mobwright extends Playwright's `expect` with mobile-aware matchers. Each one auto-retries until the assertion passes or the timeout elapses.
|
|
346
|
+
|
|
347
|
+
```typescript
|
|
348
|
+
import { test, expect } from 'mobwright';
|
|
349
|
+
|
|
350
|
+
test('login form', async ({ device }) => {
|
|
351
|
+
await expect(device.locator('~loginBtn')).toBeVisible();
|
|
352
|
+
await expect(device.locator('~welcomeText')).toHaveText('Welcome!');
|
|
353
|
+
await expect(device.locator('~submit')).toBeEnabled();
|
|
354
|
+
|
|
355
|
+
// Also works with AI locators
|
|
356
|
+
await expect(device.ai('the success message')).toBeVisible();
|
|
357
|
+
});
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
| Matcher | Auto-retries | Description |
|
|
361
|
+
|---|:---:|---|
|
|
362
|
+
| `toBeVisible(opts?)` | ✅ | Element is present and displayed |
|
|
363
|
+
| `toHaveText(text \| regex, opts?)` | ✅ | Element's visible text matches |
|
|
364
|
+
| `toBeEnabled(opts?)` | ✅ | Element is enabled (interactable) |
|
|
365
|
+
|
|
366
|
+
All standard Playwright matchers (`toBe`, `toEqual`, `toThrow`, etc.) still work normally. Negation is supported: `expect(locator).not.toBeVisible()`.
|
|
367
|
+
|
|
368
|
+
<br />
|
|
369
|
+
|
|
370
|
+
## 🤖 AI Providers
|
|
371
|
+
|
|
372
|
+
Mobwright supports **pluggable AI providers** for intelligent locator resolution — describe an element in natural language and let the AI find the right selector from the accessibility tree.
|
|
373
|
+
|
|
374
|
+
### Supported Providers
|
|
375
|
+
|
|
376
|
+
| Provider | Default Model | Approx Cost / Call |
|
|
377
|
+
|---|---|---|
|
|
378
|
+
| **Anthropic** | `claude-haiku-4-5-20251001` | ~$0.001 |
|
|
379
|
+
| **OpenAI** | `gpt-4o-mini` | ~$0.005 |
|
|
380
|
+
| **DeepSeek** | `deepseek-chat` | ~$0.0005 |
|
|
381
|
+
|
|
382
|
+
> 💰 **Cost note:** DeepSeek is ~10× cheaper than OpenAI for locator resolution with comparable quality. It's the recommended default for CI usage.
|
|
383
|
+
|
|
384
|
+
### Configuration
|
|
385
|
+
|
|
386
|
+
Set these environment variables to enable AI features:
|
|
387
|
+
|
|
388
|
+
```ini
|
|
389
|
+
MOBWRIGHT_AI_PROVIDER=deepseek # 'anthropic' | 'openai' | 'deepseek'
|
|
390
|
+
MOBWRIGHT_AI_API_KEY=sk-your-key-here
|
|
391
|
+
MOBWRIGHT_AI_MODEL=deepseek-chat # Optional: override default model
|
|
392
|
+
MOBWRIGHT_AI_BASE_URL=https://api.deepseek.com # Optional: custom endpoint
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
### Using AI in Tests
|
|
396
|
+
|
|
397
|
+
The recommended API is `device.ai()` — lazy resolution with caching and confidence gating:
|
|
398
|
+
|
|
399
|
+
```typescript
|
|
400
|
+
test('checkout with AI locators', async ({ device }) => {
|
|
401
|
+
await device.ai('the blue Continue button').tap();
|
|
402
|
+
await device.ai('the credit card number field').fill('4242424242424242');
|
|
403
|
+
await device.ai('the green Pay button').tap();
|
|
404
|
+
});
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
### Programmatic Usage (Low-level)
|
|
408
|
+
|
|
409
|
+
You can also use the provider directly via `device.aiProvider`:
|
|
410
|
+
|
|
411
|
+
```typescript
|
|
412
|
+
import { createProvider } from 'mobwright';
|
|
413
|
+
|
|
414
|
+
const provider = createProvider({
|
|
415
|
+
provider: 'deepseek',
|
|
416
|
+
model: 'deepseek-chat',
|
|
417
|
+
apiKey: process.env.DEEPSEEK_API_KEY!,
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
const result = await provider.resolveLocator({
|
|
421
|
+
description: 'the login button on the welcome screen',
|
|
422
|
+
accessibilityTree: xmlSource,
|
|
423
|
+
platform: Platform.ANDROID,
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
console.log(result);
|
|
427
|
+
// {
|
|
428
|
+
// selector: 'loginButton',
|
|
429
|
+
// strategy: 'accessibility-id',
|
|
430
|
+
// confidence: 0.95,
|
|
431
|
+
// rationale: 'Found element with content-desc="loginButton"...'
|
|
432
|
+
// }
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
### AI Provider Interface
|
|
436
|
+
|
|
437
|
+
Implement your own provider by satisfying the `AIProvider` interface:
|
|
438
|
+
|
|
439
|
+
```typescript
|
|
440
|
+
interface AIProvider {
|
|
441
|
+
readonly name: string;
|
|
442
|
+
resolveLocator(input: ResolveLocatorInput): Promise<ResolveLocatorResult>;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
interface ResolveLocatorInput {
|
|
446
|
+
description: string; // Natural-language element description
|
|
447
|
+
accessibilityTree: string; // XML page source
|
|
448
|
+
screenshot?: Buffer; // Optional screenshot for vision models
|
|
449
|
+
platform: Platform; // 'android' | 'ios'
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
interface ResolveLocatorResult {
|
|
453
|
+
selector: string; // e.g. 'loginButton', 'email', '//xpath'
|
|
454
|
+
strategy: SelectorStrategy; // 'accessibility-id' | 'id' | 'xpath' | 'text'
|
|
455
|
+
confidence: number; // 0..1 — <0.5 is treated as a miss
|
|
456
|
+
rationale?: string; // Free-text explanation for debugging
|
|
457
|
+
}
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
<br />
|
|
461
|
+
|
|
462
|
+
## 📂 Project Structure
|
|
463
|
+
|
|
464
|
+
```
|
|
465
|
+
mobwright/
|
|
466
|
+
├── src/
|
|
467
|
+
│ ├── index.ts # Public API exports
|
|
468
|
+
│ ├── config.ts # defineConfig() helper
|
|
469
|
+
│ ├── types.ts # Core type definitions
|
|
470
|
+
│ ├── ai/ # AI provider layer
|
|
471
|
+
│ │ ├── provider.ts # AIProvider interface
|
|
472
|
+
│ │ ├── types.ts # Input/output types
|
|
473
|
+
│ │ ├── factory.ts # createProvider() factory
|
|
474
|
+
│ │ ├── anthropic.ts # Anthropic (Claude) provider
|
|
475
|
+
│ │ ├── openai.ts # OpenAI-compatible provider
|
|
476
|
+
│ │ ├── deepseek.ts # DeepSeek provider
|
|
477
|
+
│ │ ├── prompts.ts # Shared prompt templates
|
|
478
|
+
│ │ └── errors.ts # AI-specific error classes
|
|
479
|
+
│ ├── appium/ # Appium session & capabilities
|
|
480
|
+
│ │ ├── capabilities.ts # W3C capability builder
|
|
481
|
+
│ │ ├── session.ts # Session create/destroy
|
|
482
|
+
│ │ └── ios-utils.ts # iOS-specific utilities
|
|
483
|
+
│ ├── device/ # Device & Locator core
|
|
484
|
+
│ │ ├── device.ts # Device class (test fixture)
|
|
485
|
+
│ │ ├── locator.ts # Locator class (auto-wait, gestures)
|
|
486
|
+
│ │ ├── ai-locator.ts # AILocator class (device.ai)
|
|
487
|
+
│ │ ├── tree.ts # Accessibility tree cleanup
|
|
488
|
+
│ │ └── selectors.ts # Selector parsing & conversion
|
|
489
|
+
│ ├── fixtures/ # Playwright fixture integration
|
|
490
|
+
│ │ └── device.ts # test.extend<{ device }>
|
|
491
|
+
│ ├── expect/ # Custom matchers
|
|
492
|
+
│ │ └── matchers.ts # toBeVisible, toHaveText, toBeEnabled
|
|
493
|
+
│ ├── cli/ # CLI commands
|
|
494
|
+
│ │ ├── index.ts # Entry point
|
|
495
|
+
│ │ ├── doctor.ts # Environment health check
|
|
496
|
+
│ │ ├── init.ts # Project scaffold
|
|
497
|
+
│ │ ├── test.ts # Test runner wrapper
|
|
498
|
+
│ │ └── templates.ts # Generated file templates
|
|
499
|
+
│ └── utils/ # Shared utilities
|
|
500
|
+
│ └── retry.ts # pollUntil helper
|
|
501
|
+
├── examples/
|
|
502
|
+
│ └── basic/ # Working example project
|
|
503
|
+
│ ├── playwright.config.ts
|
|
504
|
+
│ ├── tests/
|
|
505
|
+
│ │ ├── android/ # Android-specific tests
|
|
506
|
+
│ │ ├── ios/ # iOS-specific tests
|
|
507
|
+
│ │ └── shared/ # Cross-platform tests
|
|
508
|
+
│ └── .env.example
|
|
509
|
+
├── tests/ # Library unit tests (Vitest)
|
|
510
|
+
│ ├── unit/
|
|
511
|
+
│ └── integration/
|
|
512
|
+
└── package.json
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
<br />
|
|
516
|
+
|
|
517
|
+
## 🧪 Examples
|
|
518
|
+
|
|
519
|
+
### Basic Onboarding Flow
|
|
520
|
+
|
|
521
|
+
```typescript
|
|
522
|
+
import { test, expect } from 'mobwright';
|
|
523
|
+
|
|
524
|
+
test('walk through onboarding and get started', async ({ device }) => {
|
|
525
|
+
const forwardButton = device.locator('~forwardButton');
|
|
526
|
+
|
|
527
|
+
// Tap through 3 onboarding pages
|
|
528
|
+
for (let i = 0; i < 3; i++) {
|
|
529
|
+
await forwardButton.tap();
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Verify "Get Started" is visible, then tap
|
|
533
|
+
const getStarted = device.locator('~getStartedButton');
|
|
534
|
+
await expect(getStarted).toBeVisible();
|
|
535
|
+
await getStarted.tap();
|
|
536
|
+
|
|
537
|
+
// Confirm we left onboarding
|
|
538
|
+
await expect(forwardButton).not.toBeVisible();
|
|
539
|
+
});
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
### Swipe Through a Carousel
|
|
543
|
+
|
|
544
|
+
```typescript
|
|
545
|
+
test('swipe through product carousel', async ({ device }) => {
|
|
546
|
+
const carousel = device.locator('//android.view.View[@scrollable="true"]');
|
|
547
|
+
await carousel.waitFor({ timeout: 30_000 });
|
|
548
|
+
|
|
549
|
+
for (let i = 0; i < 3; i++) {
|
|
550
|
+
await carousel.swipeLeft();
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
const lastPage = device.getByText('Start Shopping');
|
|
554
|
+
await expect(lastPage).toBeVisible();
|
|
555
|
+
});
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
### Login Flow
|
|
559
|
+
|
|
560
|
+
```typescript
|
|
561
|
+
test('login with email and password', async ({ device }) => {
|
|
562
|
+
// Navigate to login
|
|
563
|
+
await device.locator('~loginButton').tap();
|
|
564
|
+
|
|
565
|
+
// Fill credentials
|
|
566
|
+
await device.locator('#emailInput').fill('user@example.com');
|
|
567
|
+
await device.locator('#passwordInput').fill('SecurePass123');
|
|
568
|
+
|
|
569
|
+
// Submit
|
|
570
|
+
await device.locator('~submitButton').tap();
|
|
571
|
+
|
|
572
|
+
// Verify success — toBeVisible auto-retries up to 15s
|
|
573
|
+
const dashboard = device.getByText('Dashboard');
|
|
574
|
+
await expect(dashboard).toBeVisible({ timeout: 15_000 });
|
|
575
|
+
});
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
### AI-Powered Test (the headline feature)
|
|
579
|
+
|
|
580
|
+
```typescript
|
|
581
|
+
test('checkout via natural language', async ({ device }) => {
|
|
582
|
+
if (!device.aiProvider) {
|
|
583
|
+
test.skip(true, 'AI not configured');
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// Let AI find each element from a description
|
|
588
|
+
await device.ai('the Continue button on the onboarding screen').tap();
|
|
589
|
+
await device.ai('the search input field').fill('Wikipedia');
|
|
590
|
+
await device.ai('the first search result').tap();
|
|
591
|
+
|
|
592
|
+
// Mix and match with regular locators
|
|
593
|
+
await expect(device.locator('~articleTitle')).toBeVisible();
|
|
594
|
+
});
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
<br />
|
|
598
|
+
|
|
599
|
+
## 🔧 Running the Example Project
|
|
600
|
+
|
|
601
|
+
The `examples/basic/` directory contains a fully working example.
|
|
602
|
+
|
|
603
|
+
```bash
|
|
604
|
+
# 1. Clone & install
|
|
605
|
+
git clone https://gitlab.com/automation-repository/mobwright-at.git
|
|
606
|
+
cd mobwright-at
|
|
607
|
+
pnpm install
|
|
608
|
+
|
|
609
|
+
# 2. Configure your devices
|
|
610
|
+
cp examples/basic/.env.example examples/basic/.env
|
|
611
|
+
# Edit examples/basic/.env with your device & app paths
|
|
612
|
+
|
|
613
|
+
# 3. Start Appium
|
|
614
|
+
appium
|
|
615
|
+
|
|
616
|
+
# 4. Run tests from the example directory
|
|
617
|
+
cd examples/basic
|
|
618
|
+
pnpm test --project android --grep '@initial\.test'
|
|
619
|
+
pnpm test --project ios --grep '@initial\.test'
|
|
620
|
+
|
|
621
|
+
# Or from the monorepo root
|
|
622
|
+
pnpm --filter mobwright-basic-example test --project android
|
|
623
|
+
pnpm --filter mobwright-basic-example test --project ios
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
<br />
|
|
627
|
+
|
|
628
|
+
## ⚙️ Configuration Reference
|
|
629
|
+
|
|
630
|
+
### Environment Variables
|
|
631
|
+
|
|
632
|
+
| Variable | Required | Description |
|
|
633
|
+
|---|---|---|
|
|
634
|
+
| **Android** | | |
|
|
635
|
+
| `MOBWRIGHT_APP_PATH` | ✅ | Absolute path to `.apk` file |
|
|
636
|
+
| `MOBWRIGHT_ANDROID_AVD` | ✅ | Emulator AVD name |
|
|
637
|
+
| `MOBWRIGHT_ANDROID_APP_PACKAGE` | | Override app package |
|
|
638
|
+
| `MOBWRIGHT_ANDROID_APP_ACTIVITY` | | Override launcher activity |
|
|
639
|
+
| **iOS** | | |
|
|
640
|
+
| `MOBWRIGHT_IOS_DEVICE` | | Simulator name (default: `iPhone 15`) |
|
|
641
|
+
| `MOBWRIGHT_IOS_BUNDLE_ID` | ⚠️ | Bundle ID (required if no app path) |
|
|
642
|
+
| `MOBWRIGHT_IOS_APP_PATH` | ⚠️ | Path to `.app` bundle (required if not installed) |
|
|
643
|
+
| `MOBWRIGHT_IOS_UDID` | | Simulator UDID for faster matching |
|
|
644
|
+
| `MOBWRIGHT_IOS_PLATFORM_VERSION` | | e.g. `17.5` |
|
|
645
|
+
| **AI** | | |
|
|
646
|
+
| `MOBWRIGHT_AI_PROVIDER` | | `anthropic`, `openai`, or `deepseek` |
|
|
647
|
+
| `MOBWRIGHT_AI_API_KEY` | | Provider API key |
|
|
648
|
+
| `MOBWRIGHT_AI_MODEL` | | Override default model |
|
|
649
|
+
| `MOBWRIGHT_AI_BASE_URL` | | Custom API endpoint |
|
|
650
|
+
| **Appium** | | |
|
|
651
|
+
| `MOBWRIGHT_APPIUM_PORT` | | Custom Appium port (default: `4723`) |
|
|
652
|
+
|
|
653
|
+
### `defineConfig()` — Type-Safe Configuration
|
|
654
|
+
|
|
655
|
+
```typescript
|
|
656
|
+
import { defineConfig, Platform } from 'mobwright';
|
|
657
|
+
|
|
658
|
+
export default defineConfig({
|
|
659
|
+
actionTimeout: 10_000,
|
|
660
|
+
projects: [
|
|
661
|
+
{
|
|
662
|
+
name: 'android',
|
|
663
|
+
use: {
|
|
664
|
+
platform: Platform.ANDROID,
|
|
665
|
+
device: { provider: 'emulator', name: 'Pixel_6_API_34' },
|
|
666
|
+
buildPath: './app-debug.apk',
|
|
667
|
+
},
|
|
668
|
+
},
|
|
669
|
+
{
|
|
670
|
+
name: 'ios',
|
|
671
|
+
use: {
|
|
672
|
+
platform: Platform.IOS,
|
|
673
|
+
device: { provider: 'simulator', name: 'iPhone 15' },
|
|
674
|
+
buildPath: './MyApp.app',
|
|
675
|
+
bundleId: 'com.example.myapp',
|
|
676
|
+
},
|
|
677
|
+
},
|
|
678
|
+
],
|
|
679
|
+
ai: {
|
|
680
|
+
provider: 'deepseek',
|
|
681
|
+
model: 'deepseek-chat',
|
|
682
|
+
apiKey: process.env.DEEPSEEK_API_KEY!,
|
|
683
|
+
},
|
|
684
|
+
});
|
|
685
|
+
```
|
|
686
|
+
|
|
687
|
+
<br />
|
|
688
|
+
|
|
689
|
+
## ⚡ Parallelism
|
|
690
|
+
|
|
691
|
+
| Strategy | Supported | Notes |
|
|
692
|
+
|---|:---:|---|
|
|
693
|
+
| Serial (1 worker) | ✅ v0.1 | Stable on both Android and iOS |
|
|
694
|
+
| iOS multi-simulator parallel | ✅ v0.1 | Multiple booted simulators + `workers: N` works today |
|
|
695
|
+
| Android multi-emulator parallel | 🔜 v0.2 | Needs worker→device routing |
|
|
696
|
+
| CI job-level parallelism | ✅ | Parallelize across machines |
|
|
697
|
+
| Cloud farms (BrowserStack) | 🔜 v0.2 | Native parallelism |
|
|
698
|
+
|
|
699
|
+
### Why iOS but not Android?
|
|
700
|
+
|
|
701
|
+
iOS simulator instances are fully isolated — each has its own UDID, filesystem, UI thread, and WebDriverAgent. Multiple Appium sessions can run in parallel without stepping on each other.
|
|
702
|
+
|
|
703
|
+
Android UiAutomator2 hooks the OS UI thread, and only one instrumentation can exist per device. Running `workers: 2` against a single emulator crashes. Multi-emulator parallel (with worker→device routing) is on the v0.2 roadmap.
|
|
704
|
+
|
|
705
|
+
<br />
|
|
706
|
+
|
|
707
|
+
## 🗺️ Roadmap
|
|
708
|
+
|
|
709
|
+
| Feature | Status |
|
|
710
|
+
|---|---|
|
|
711
|
+
| Playwright-style API (`locator`, `tap`, `fill`, `getText`) | ✅ Shipped |
|
|
712
|
+
| Auto-wait (poll until actionable) | ✅ Shipped |
|
|
713
|
+
| Android + iOS support | ✅ Shipped |
|
|
714
|
+
| Unified selector syntax | ✅ Shipped |
|
|
715
|
+
| Swipe & scroll gestures | ✅ Shipped |
|
|
716
|
+
| Multi-provider AI abstraction (Anthropic, OpenAI, DeepSeek) | ✅ Shipped |
|
|
717
|
+
| `device.ai('description').tap()` — NL locators | ✅ Shipped |
|
|
718
|
+
| Custom matchers (`toBeVisible`, `toHaveText`, `toBeEnabled`) | ✅ Shipped |
|
|
719
|
+
| iOS multi-simulator parallelism | ✅ Shipped |
|
|
720
|
+
| CLI (`mobwright init`, `test`, `doctor`) | ✅ Shipped |
|
|
721
|
+
| Smart iOS app install detection | ✅ Shipped |
|
|
722
|
+
| Self-healing locators | 🔜 v0.2 |
|
|
723
|
+
| Android multi-emulator parallelism | 🔜 v0.2 |
|
|
724
|
+
| BrowserStack / LambdaTest / Sauce Labs | 🔜 v0.2 |
|
|
725
|
+
| Real-device support | 🔜 v0.2 |
|
|
726
|
+
| AI failure analysis & reports | 🔜 v0.3 |
|
|
727
|
+
| Vision-based assertions (`toLookLike`) | 🔜 v0.3 |
|
|
728
|
+
| Terminal recorder (`mobwright record`) | 🔜 v0.4 |
|
|
729
|
+
| Browser-based codegen inspector | 🔜 v0.5 |
|
|
730
|
+
|
|
731
|
+
<br />
|
|
732
|
+
|
|
733
|
+
## 🧑💻 Development
|
|
734
|
+
|
|
735
|
+
```bash
|
|
736
|
+
# Clone
|
|
737
|
+
git clone https://gitlab.com/automation-repository/mobwright-at.git
|
|
738
|
+
cd mobwright-at
|
|
739
|
+
|
|
740
|
+
# Install dependencies
|
|
741
|
+
pnpm install
|
|
742
|
+
|
|
743
|
+
# Build the library
|
|
744
|
+
pnpm build
|
|
745
|
+
|
|
746
|
+
# Watch mode (rebuilds on changes)
|
|
747
|
+
pnpm dev
|
|
748
|
+
|
|
749
|
+
# Run unit tests (Vitest)
|
|
750
|
+
pnpm test
|
|
751
|
+
|
|
752
|
+
# Run unit tests in watch mode
|
|
753
|
+
pnpm test:watch
|
|
754
|
+
|
|
755
|
+
# Lint & format
|
|
756
|
+
pnpm lint
|
|
757
|
+
pnpm format
|
|
758
|
+
|
|
759
|
+
# Type check
|
|
760
|
+
pnpm typecheck
|
|
761
|
+
```
|
|
762
|
+
|
|
763
|
+
<br />
|
|
764
|
+
|
|
765
|
+
## 📄 License
|
|
766
|
+
|
|
767
|
+
[MIT](LICENSE) © Irsyad Prasetyo
|
|
768
|
+
|
|
769
|
+
---
|
|
770
|
+
|
|
771
|
+
<div align="center">
|
|
772
|
+
<br />
|
|
773
|
+
<p>
|
|
774
|
+
<sub>Built with ❤️ for QA engineers who deserve better mobile testing tools.</sub>
|
|
775
|
+
</p>
|
|
776
|
+
</div>
|