mobilewright 0.0.19 → 0.0.20

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.
Files changed (2) hide show
  1. package/README.md +373 -0
  2. package/package.json +5 -5
package/README.md ADDED
@@ -0,0 +1,373 @@
1
+ # Mobilewright
2
+
3
+ Any device - Any app - Any platform: One API - Agent-ready.
4
+ - Control iOS & Android devices with a unified API.
5
+ - Deterministic execution, no flaky tests, full tracing.
6
+ - Built for developers and AI agents.
7
+
8
+ A TypeScript framework for mobile device automation, inspired by [Playwright](https://playwright.dev/)'s architecture and developer experience. Mobilewright targets iOS and Android devices, simulators, and emulators through a clean, auto-waiting API built on top of [mobilecli](https://github.com/mobile-next/mobilecli).
9
+
10
+
11
+ ## Features
12
+
13
+ - **Playwright-style API** — `screen.getByRole('button').tap()`, just like `page.getByRole('button').click()`
14
+ - **Zero config** — auto-discovers booted simulators
15
+ - **Cross-platform** — unified interface for iOS and Android
16
+ - **Auto-waiting** — actions wait for elements to be visible, enabled, and stable before interacting
17
+ - **Chainable locators** — `screen.getByType('Cell').getByLabel('Item 1')`
18
+ - **Retry assertions** — `expect(locator).toBeVisible()` polls until satisfied or timeout
19
+ - **Remote support** — connect to mobilecli on another machine for device lab setups
20
+ - **Test fixtures** — `@mobilewright/test` extends Playwright Test with `screen` and `device` fixtures
21
+
22
+ ## Quick Start
23
+
24
+ ```bash
25
+ npm install mobilewright
26
+ ```
27
+
28
+ ```typescript
29
+ import { ios, expect } from 'mobilewright';
30
+
31
+ const device = await ios.launch({ bundleId: 'com.example.myapp' });
32
+ const { screen } = device;
33
+
34
+ await screen.getByLabel('Email').fill('user@example.com');
35
+ await screen.getByLabel('Password').fill('password123');
36
+ await screen.getByRole('button', { name: 'Sign In' }).tap();
37
+
38
+ await expect(screen.getByText('Welcome back')).toBeVisible();
39
+ const screenshot = await screen.screenshot();
40
+
41
+ await device.close();
42
+ ```
43
+
44
+ ## Prerequisites
45
+
46
+ - Node.js >= 18
47
+ - A booted iOS simulator, Android emulator, or connected real device
48
+
49
+ Run `mobilewright doctor` to verify your environment is ready:
50
+
51
+ ```bash
52
+ npx mobilewright doctor
53
+ ```
54
+
55
+ It checks Xcode, Android SDK, simulators, ADB, and other dependencies — and tells you exactly what's missing and how to fix it. Add `--json` for machine-readable output.
56
+
57
+ ## Packages
58
+
59
+ | Package | Description |
60
+ |---|---|
61
+ | `mobilewright` | Main entry point — `ios`, `android` launchers, `expect`, config, CLI |
62
+ | `@mobilewright/test` | Test fixtures |
63
+ | `@mobilewright/protocol` | TypeScript interfaces (`MobilewrightDriver`, `ViewNode`) |
64
+ | `@mobilewright/driver-mobilecli` | WebSocket JSON-RPC client for mobilecli |
65
+ | `@mobilewright/mobilewright-core` | `Device`, `Screen`, `Locator`, `expect` — the user-facing API |
66
+
67
+ Most users only need `mobilewright` (or `@mobilewright/test` for vitest integration).
68
+
69
+ ## API Reference
70
+
71
+ ### Launchers — `ios` and `android`
72
+
73
+ The top-level entry points. Like Playwright's `chromium` / `firefox` / `webkit`.
74
+
75
+ ```typescript
76
+ import { ios, android } from 'mobilewright';
77
+
78
+ // Launch with auto-discovery (finds first booted simulator)
79
+ const device = await ios.launch();
80
+
81
+ // Launch a specific app
82
+ const device = await ios.launch({ bundleId: 'com.example.app' });
83
+
84
+ // Target a specific simulator by name
85
+ const device = await ios.launch({ deviceName: 'My.*iPhone' });
86
+
87
+ // Explicit device UDID (skips discovery)
88
+ const device = await ios.launch({ deviceId: '5A5FCFCA-...' });
89
+
90
+ // List available devices
91
+ const devices = await ios.devices();
92
+ const devices = await android.devices();
93
+ ```
94
+
95
+ `launch()` handles the full lifecycle:
96
+ 1. Checks if mobilecli is reachable (auto-starts it for local URLs if not running)
97
+ 2. Checks mobilecli version (warns if older than minimum supported)
98
+ 3. Discovers booted devices (prefers simulators over real devices)
99
+ 4. Connects and optionally launches the app
100
+ 5. On `device.close()`, kills the auto-started server
101
+
102
+ ### Screen
103
+
104
+ Entry point for finding and interacting with elements. Access via `device.screen`.
105
+
106
+ **Locator factories:**
107
+
108
+ ```typescript
109
+ screen.getByLabel('Email') // accessibility label
110
+ screen.getByTestId('login-button') // accessibility identifier
111
+ screen.getByText('Welcome') // visible text (exact match)
112
+ screen.getByText(/welcome/i) // RegExp match
113
+ screen.getByText('welcome', { exact: false }) // substring match
114
+ screen.getByType('TextField') // element type
115
+ screen.getByRole('button', { name: 'Sign In' }) // semantic role + name filter
116
+ screen.getByPlaceholder('Search...') // placeholder text
117
+ screen.getByPlaceholder('search', { exact: false }) // substring match
118
+ ```
119
+
120
+ **Direct actions:**
121
+
122
+ ```typescript
123
+ await screen.screenshot() // capture PNG
124
+ await screen.screenshot({ format: 'jpeg', quality: 80 })
125
+ await screen.swipe('up')
126
+ await screen.swipe('down', { distance: 300, duration: 500 })
127
+ await screen.pressButton('HOME')
128
+ await screen.tap(195, 400) // raw coordinate tap
129
+ ```
130
+
131
+ ### Locator
132
+
133
+ Lazy, chainable element reference. No queries execute until you call an action or assertion.
134
+
135
+ **Actions** (all auto-wait for the element to be visible, enabled, and have stable bounds):
136
+
137
+ ```typescript
138
+ await locator.tap()
139
+ await locator.doubleTap()
140
+ await locator.longPress({ duration: 1000 })
141
+ await locator.fill('hello@example.com') // tap to focus + type text
142
+ ```
143
+
144
+ **Queries:**
145
+
146
+ ```typescript
147
+ await locator.isVisible() // boolean
148
+ await locator.isEnabled() // boolean
149
+ await locator.isSelected() // boolean
150
+ await locator.isFocused() // boolean
151
+ await locator.isChecked() // boolean
152
+ await locator.getText() // waits for visibility first
153
+ await locator.getValue() // raw value (e.g. text field content)
154
+ ```
155
+
156
+ **Explicit waiting:**
157
+
158
+ ```typescript
159
+ await locator.waitFor({ state: 'visible' })
160
+ await locator.waitFor({ state: 'hidden' })
161
+ await locator.waitFor({ state: 'enabled' })
162
+ await locator.waitFor({ state: 'disabled', timeout: 10_000 })
163
+ ```
164
+
165
+ **Chaining** — scope queries within a parent element's bounds:
166
+
167
+ ```typescript
168
+ // Tap the delete button inside the first row
169
+ const row = screen.getByType('Cell');
170
+ await row.getByRole('button', { name: 'Delete' }).tap();
171
+
172
+ // Get text from a navigation bar
173
+ const title = await screen.getByType('NavigationBar').getByType('StaticText').getText();
174
+ ```
175
+
176
+ When chaining, child lookups use bounds-based containment: any element whose bounds fit within the parent's bounds is considered a child. This works correctly with mobilecli's flat element lists.
177
+
178
+ ### Device
179
+
180
+ Manages the connection lifecycle and exposes device/app-level controls.
181
+
182
+ ```typescript
183
+ // Orientation
184
+ await device.setOrientation('landscape');
185
+ const orientation = await device.getOrientation();
186
+
187
+ // URLs / deep links (goto is a Playwright-style alias for openUrl)
188
+ await device.goto('myapp://settings');
189
+ await device.openUrl('https://example.com');
190
+
191
+ // App lifecycle
192
+ await device.launchApp('com.example.app', { locale: 'fr_FR' });
193
+ await device.terminateApp('com.example.app');
194
+ const apps = await device.listApps();
195
+ const foreground = await device.getForegroundApp();
196
+ await device.installApp('/path/to/app.ipa');
197
+ await device.uninstallApp('com.example.app');
198
+
199
+ // Cleanup (disconnects + stops auto-started mobilecli)
200
+ await device.close();
201
+ ```
202
+
203
+ ### Assertions — `expect`
204
+
205
+ All assertions poll repeatedly until satisfied or timeout (default 5s). Supports `.not` for negation.
206
+
207
+ ```typescript
208
+ import { expect } from 'mobilewright';
209
+
210
+ await expect(locator).toBeVisible();
211
+ await expect(locator).not.toBeVisible();
212
+
213
+ await expect(locator).toBeEnabled();
214
+ await expect(locator).not.toBeEnabled();
215
+
216
+ await expect(locator).toHaveText('Welcome back!');
217
+ await expect(locator).toHaveText(/welcome/i);
218
+ await expect(locator).toContainText('back');
219
+
220
+ await expect(locator).toBeVisible({ timeout: 10_000 });
221
+ ```
222
+
223
+ ### Role Mapping
224
+
225
+ `getByRole` maps semantic roles to platform-specific element types:
226
+
227
+ | Role | iOS | Android |
228
+ |---|---|---|
229
+ | `button` | Button, ImageButton | Button, ImageButton, ReactViewGroup* |
230
+ | `textfield` | TextField, SecureTextField, SearchField | EditText, ReactEditText |
231
+ | `text` | StaticText | TextView, Text, ReactTextView |
232
+ | `image` | Image | ImageView, ReactImageView |
233
+ | `switch` | Switch | Switch, Toggle |
234
+ | `checkbox` | -- | Checkbox |
235
+ | `slider` | Slider | SeekBar |
236
+ | `list` | Table, CollectionView, ScrollView | ListView, RecyclerView, ReactScrollView |
237
+ | `header` | NavigationBar | Toolbar |
238
+ | `link` | Link | Link |
239
+
240
+ \* ReactViewGroup matches `button` only when the element has `clickable="true"` or `accessible="true"` in its raw attributes, to avoid false positives since React Native uses ReactViewGroup for all container views.
241
+
242
+ Falls back to direct type matching if no mapping exists.
243
+
244
+ ## Configuration
245
+
246
+ Create a `mobilewright.config.ts` in your project root:
247
+
248
+ ```typescript
249
+ import { defineConfig } from 'mobilewright';
250
+
251
+ export default defineConfig({
252
+ platform: 'ios',
253
+ bundleId: 'com.example.myapp',
254
+ deviceName: 'iPhone 16',
255
+ timeout: 10_000,
256
+ });
257
+ ```
258
+
259
+ All options:
260
+
261
+ | Option | Type | Description |
262
+ |---|---|---|
263
+ | `platform` | `'ios' \| 'android'` | Device platform (optional) |
264
+ | `bundleId` | `string` | App bundle ID (optional) |
265
+ | `deviceId` | `string` | Explicit device UDID (optional) |
266
+ | `deviceName` | `string` | RegExp to match device name (optional) |
267
+ | `timeout` | `number` | Global locator timeout in ms (optional) |
268
+
269
+ Config values are used as defaults — `LaunchOptions` passed to `ios.launch()` always take precedence.
270
+
271
+ Mobilewright will use the first device that matches your configured criteria.
272
+
273
+ ## Test Fixtures
274
+
275
+ `@mobilewright/test` extends [Playwright Test](https://playwright.dev/docs/test-intro) with mobile-specific fixtures:
276
+
277
+ ```typescript
278
+ import { test, expect } from '@mobilewright/test';
279
+
280
+ // Configure the app bundle and video recording for all tests in this file
281
+ test.use({ bundleId: 'com.example.myapp', video: 'on' });
282
+
283
+ test('can sign in', async ({ device, screen, bundleId }) => {
284
+ // Fresh-launch the app before the test
285
+ await device.terminateApp(bundleId).catch(() => {});
286
+ await device.launchApp(bundleId);
287
+
288
+ await screen.getByLabel('Email').fill('user@example.com');
289
+ await screen.getByLabel('Password').fill('password123');
290
+ await screen.getByRole('button', { name: 'Sign In' }).tap();
291
+
292
+ await expect(screen.getByText('Welcome back')).toBeVisible();
293
+ });
294
+ ```
295
+
296
+ The `device` fixture connects once per worker (reading from `mobilewright.config.ts`) and calls `device.close()` after all tests complete. The `screen` fixture provides `device.screen` to each test, with automatic screenshot-on-failure and optional video recording.
297
+
298
+ ## CLI
299
+
300
+ ### `mobilewright init`
301
+
302
+ Scaffold a `mobilewright.config.ts` and `example.test.ts` in the current directory. Skips files that already exist.
303
+
304
+ ```bash
305
+ npx mobilewright init
306
+ ```
307
+
308
+ ```
309
+ created mobilewright.config.ts
310
+ created example.test.ts
311
+ ```
312
+
313
+ ### `mobilewright devices`
314
+
315
+ List all connected devices, simulators, and emulators.
316
+
317
+ ```bash
318
+ npx mobilewright devices
319
+ ```
320
+
321
+ ```
322
+ ID Name Platform Type State
323
+ -------------------------------------------------------------------------------------------------
324
+ 00008110-0011281A112A801E VPhone ios real-device booted
325
+ 5A5FCFCA-27EC-4D1B-B412-BAE629154EE0 iPhone 17 Pro ios simulator booted
326
+ ```
327
+
328
+ ### `mobilewright test`
329
+
330
+ Run your tests. Auto-discovers `mobilewright.config.ts` in the current directory.
331
+
332
+ ```bash
333
+ npx mobilewright test
334
+ npx mobilewright test login.test.ts # run a specific file
335
+ npx mobilewright test --grep "sign in" # filter by test name
336
+ npx mobilewright test --reporter html # generate HTML report
337
+ npx mobilewright test --retries 2 # retry flaky tests
338
+ npx mobilewright test --workers 4 # parallel workers
339
+ npx mobilewright test --list # list tests without running
340
+ ```
341
+
342
+ ### `mobilewright show-report`
343
+
344
+ Open the HTML report generated by `--reporter html`.
345
+
346
+ ```bash
347
+ npx mobilewright show-report
348
+ npx mobilewright show-report mobilewright-report/
349
+ ```
350
+
351
+ ## Contributing
352
+
353
+ ```bash
354
+ # Run the repository's own unit tests
355
+ npm test
356
+ ```
357
+
358
+ ## Framework Support
359
+
360
+ | Framework | iOS | Android | Notes |
361
+ |---|---|---|---|
362
+ | UIKit / Storyboards | ✅ | — | Full native element types, all locators work |
363
+ | SwiftUI | ✅ | — | Maps to standard `XCUIElementType` accessibility tree |
364
+ | Jetpack Compose | — | ✅ | Renders to native Android accessibility nodes |
365
+ | Android Views (XML layouts) | — | ✅ | Full native element types, all locators work |
366
+ | React Native | ✅ | ✅ | Uses real native components; RN-specific types mapped to roles |
367
+ | Expo | ✅ | ✅ | Same as React Native (Expo builds to RN) |
368
+ | Flutter | ⏳ | ⏳ | Renders via Skia/Impeller, not native views — requires Dart VM Service driver |
369
+ | .NET MAUI | ✅ | ✅ | Compiles to native controls on both platforms |
370
+ | Kotlin Multiplatform (shared UI) | ⏳ | ✅ | Android native works; iOS Compose Multiplatform support in progress |
371
+ | Cordova / Capacitor | ✅ | ✅ | WebView content accessible via native accessibility tree |
372
+ | NativeScript | ✅ | ✅ | Renders to native views on both platforms |
373
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mobilewright",
3
- "version": "0.0.19",
3
+ "version": "0.0.20",
4
4
  "description": "Mobile device automation framework inspired by Playwright",
5
5
  "homepage": "https://mobilewright.dev",
6
6
  "license": "Apache-2.0",
@@ -33,10 +33,10 @@
33
33
  "assets"
34
34
  ],
35
35
  "dependencies": {
36
- "@mobilewright/core": "^0.0.19",
37
- "@mobilewright/driver-mobilecli": "^0.0.19",
38
- "@mobilewright/protocol": "^0.0.19",
39
- "@mobilewright/test": "^0.0.19",
36
+ "@mobilewright/core": "^0.0.20",
37
+ "@mobilewright/driver-mobilecli": "^0.0.20",
38
+ "@mobilewright/protocol": "^0.0.20",
39
+ "@mobilewright/test": "^0.0.20",
40
40
  "commander": "^14.0.3",
41
41
  "playwright": "1.58.2",
42
42
  "ws": "^8.18.0"