@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/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>