@unotest/mobile 0.8.2 → 0.9.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/write-e2e-test.md +23 -14
- package/CHANGELOG.md +48 -0
- package/README.md +72 -3
- package/bin/{mcp.js → unotest-mobile.js} +9 -0
- package/dist/dsl/mobile-dsl-language-service.d.ts +12 -0
- package/dist/dsl/mobile-dsl-language-service.js +1056 -0
- package/dist/mcp/server.js +1095 -2886
- package/dist/runner/cli.js +4247 -4991
- package/dist/runner/doctor.js +4 -3
- package/dist/runner/init.js +67 -73
- package/dist/runner/install.js +103 -89
- package/dist/runner/mobile-runner-adapter.d.ts +6 -0
- package/dist/runner/mobile-runner-adapter.js +227 -0
- package/package.json +39 -33
|
@@ -6,7 +6,7 @@ description: Write a new E2E test for the project under test using @unotest/mobi
|
|
|
6
6
|
# Skill: write-e2e-test
|
|
7
7
|
|
|
8
8
|
You are writing an end-to-end test using `@unotest/mobile` against an
|
|
9
|
-
iOS React Native app. The test lives in `unotest/e2e/<name>.js` in the
|
|
9
|
+
iOS React Native app. The test lives in `unotest/e2e-mobile/<name>.js` in the
|
|
10
10
|
project under test.
|
|
11
11
|
|
|
12
12
|
**Default mode is verify-against-live-app.** A test that hasn't been
|
|
@@ -35,7 +35,7 @@ Five phases, in this order. Don't skip phases.
|
|
|
35
35
|
|
|
36
36
|
### Phase 0 — read context
|
|
37
37
|
|
|
38
|
-
`unotest/e2e/_template/example.js` shows the exact DSL syntax (bare
|
|
38
|
+
`unotest/e2e-mobile/_template/example.js` shows the exact DSL syntax (bare
|
|
39
39
|
top-level functions, no `import`/`export`/`async`/`await`/`const`/`let`/
|
|
40
40
|
`var`/arrow-functions). Read it first. The DSL looks like JavaScript
|
|
41
41
|
but is a frozen subset — when your default JS instincts disagree with
|
|
@@ -196,9 +196,12 @@ save_exploration_as_test { explorationId, scenarioName: "<name>" }
|
|
|
196
196
|
with `// SKIPPED` comments.
|
|
197
197
|
- File exists? Pass `overwrite: true`.
|
|
198
198
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
`
|
|
199
|
+
Every executable statement in a `test_*` body MUST live inside a
|
|
200
|
+
`step("human-readable intent", () => { ... })` block - the linter's
|
|
201
|
+
`validator:step-coverage` rule enforces it. The generator groups
|
|
202
|
+
recorded sections into `step(...)` blocks automatically; keep the
|
|
203
|
+
labels meaningful when editing (they're what an agent reads when
|
|
204
|
+
repairing a broken step).
|
|
202
205
|
|
|
203
206
|
### Phase 5 — reset state, then run_test
|
|
204
207
|
|
|
@@ -213,6 +216,10 @@ Via MCP — reinstall the app to wipe its sandbox AND auth tokens:
|
|
|
213
216
|
app_install { path: "<APP_PATH from unotest/.env>", clean: true }
|
|
214
217
|
```
|
|
215
218
|
|
|
219
|
+
To just PREPARE devices (boot SIM_POOL + install APP_PATH, no wipe),
|
|
220
|
+
the viewer's App panel runs `npx unotest-mobile app-server` and holds
|
|
221
|
+
it; runs then find booted sims with the build installed.
|
|
222
|
+
|
|
216
223
|
**Do NOT pass `erase: true`.** `erase: true` runs `simctl erase` which
|
|
217
224
|
wipes the entire simulator including WebDriverAgent. `clean: true`
|
|
218
225
|
alone is the right soft-reset — it now resets the keychain too, so
|
|
@@ -354,7 +361,7 @@ Time: `today()`, `daysFromNow(n)`, `nowMs()`.
|
|
|
354
361
|
|
|
355
362
|
## Helpers convention
|
|
356
363
|
|
|
357
|
-
Reusable steps go in `unotest/e2e/_helpers/<group>.js`. Helper naming
|
|
364
|
+
Reusable steps go in `unotest/e2e-mobile/_helpers/<group>.js`. Helper naming
|
|
358
365
|
is **snake_case** (`signin_as`, `seed_workout`). **Before recording an
|
|
359
366
|
inline flow, check `_helpers/` for an existing helper** — `Glob` or
|
|
360
367
|
`Read`. If `signin_as(email, password)` exists, the generated test
|
|
@@ -363,7 +370,7 @@ should call it instead of re-recording four steps. (After
|
|
|
363
370
|
for the helper call.)
|
|
364
371
|
|
|
365
372
|
```
|
|
366
|
-
unotest/e2e/_helpers/auth.js:
|
|
373
|
+
unotest/e2e-mobile/_helpers/auth.js:
|
|
367
374
|
function signin_as(email, password) { ... }
|
|
368
375
|
```
|
|
369
376
|
|
|
@@ -394,13 +401,15 @@ npx unotest-mobile lint
|
|
|
394
401
|
```
|
|
395
402
|
|
|
396
403
|
Lint codes to recognize:
|
|
397
|
-
-
|
|
398
|
-
-
|
|
399
|
-
-
|
|
400
|
-
|
|
401
|
-
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
+
- E2 — statement/expression outside the frozen subset (drift to plain JS).
|
|
405
|
+
- E3 — `setDevice()` with an unknown sim slot.
|
|
406
|
+
- E8 — duplicate helper name / shadowing a built-in.
|
|
407
|
+
- validator:unknown-function — typo'd or undefined function name.
|
|
408
|
+
- validator:arity — wrong arg count for a built-in.
|
|
409
|
+
- validator:arg-kind — wrong arg TYPE / order, e.g.
|
|
410
|
+
`swipe(getByTestId("x"), "up")` — argTypes are `("up"|..., selector)`.
|
|
411
|
+
- validator:step-coverage — a statement in a `test_*` body is not inside
|
|
412
|
+
`step("description", () => { ... })`. Wrap it.
|
|
404
413
|
|
|
405
414
|
If lint flags `import`/`export`/`await`/`const` — re-read the DSL
|
|
406
415
|
section above and rewrite.
|
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,54 @@ All notable changes to `@unotest/mobile` will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
6
6
|
|
|
7
|
+
## [0.9.1] - 2026-06-14
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- **`@unotest/studio` integration** — the mobile runner participates in the
|
|
12
|
+
desktop shell ("mobile in studio").
|
|
13
|
+
- Ecosystem version lockstep to `0.9.1`.
|
|
14
|
+
|
|
15
|
+
## [0.9.0] - 2026-06-11
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- **Viewer integration.** The mobile runner now drives the localhost
|
|
20
|
+
`@unotest/viewer`: run artifacts (`steps.jsonl` / `runtime.json` / manifest),
|
|
21
|
+
live runtime state, the debug control plane (step / continue / abort / pause +
|
|
22
|
+
breakpoints), and snapshot / console / failure capture — so a mobile run is
|
|
23
|
+
inspectable in the same UI as web.
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- **Workspace overlay + shared packages.** Moved into the `@unotest` pnpm
|
|
28
|
+
workspace overlay; adopts the shared `@unotest/core` (run / runtime writers,
|
|
29
|
+
debug-commands watcher) and the `@unotest/dsl` executor instead of forked
|
|
30
|
+
copies.
|
|
31
|
+
|
|
32
|
+
## [0.8.3] — 2026-05-18
|
|
33
|
+
|
|
34
|
+
### Added — README onboarding protocol for AI agents
|
|
35
|
+
|
|
36
|
+
- **New top-of-README section.** A developer dropping the npm page
|
|
37
|
+
link into their AI agent's context (Cursor, Claude Desktop, Claude
|
|
38
|
+
Code in another project, etc.) now gets a 5-step protocol explicitly
|
|
39
|
+
addressed to the agent: verify host → pick working directory → ask
|
|
40
|
+
for `.app` path → run interactive setup in user's terminal → hand off
|
|
41
|
+
to Claude Code in the same directory. Spells out where the agent's
|
|
42
|
+
responsibility ends (not test authoring; that's a separate Claude
|
|
43
|
+
Code session via MCP) and what NOT to run via its own `Bash`-style
|
|
44
|
+
shell tool (the install command is interactive — needs a real TTY
|
|
45
|
+
for the simulator picker; tool-spawned shells either hang or fall
|
|
46
|
+
back to a non-interactive code path that can confuse the user).
|
|
47
|
+
- The working-directory step is explicit because `init` drops
|
|
48
|
+
`unotest/`, `.mcp.json`, `.claude/skills/`, and `AGENTS.md` into
|
|
49
|
+
cwd, and `.mcp.json`'s location determines what Claude Code sees on
|
|
50
|
+
its next launch. Recommends the app repo root by default; monorepo
|
|
51
|
+
root for monorepos; explicit warning that a separate test repo
|
|
52
|
+
divorces tests from the app and should be chosen only when the user
|
|
53
|
+
asks for it.
|
|
54
|
+
|
|
7
55
|
## [0.8.2] — 2026-05-18
|
|
8
56
|
|
|
9
57
|
### Fixed — `type()` no longer drops 1-3 characters mid-string
|
package/README.md
CHANGED
|
@@ -5,6 +5,74 @@ that lets Claude (or any MCP client) drive your iOS Simulator and write
|
|
|
5
5
|
real test scenarios for your app — works with any backend stack
|
|
6
6
|
(Node/Python/Rails/Go/Java), no JS expertise required from you.
|
|
7
7
|
|
|
8
|
+
## If you're an AI agent helping a user set this up
|
|
9
|
+
|
|
10
|
+
*(Humans: skip this section, jump to [Quick start](#quick-start).)*
|
|
11
|
+
|
|
12
|
+
You were given this page because a developer wants `@unotest/mobile`
|
|
13
|
+
set up in their project. Follow this protocol exactly — don't improvise.
|
|
14
|
+
|
|
15
|
+
**1. Verify the host.** Run `xcrun simctl list devices` via your shell
|
|
16
|
+
tool. If it errors with "command not found" or similar, the user isn't
|
|
17
|
+
on macOS or is missing Xcode CLI tools. Stop and tell them: "This is
|
|
18
|
+
iOS-only — Apple licensing prevents iOS simulators from running on
|
|
19
|
+
Linux/Windows. You'll need macOS with Xcode installed." Also verify
|
|
20
|
+
`node --version` ≥ 20.
|
|
21
|
+
|
|
22
|
+
**2. Pick a working directory with the user.** Setup writes
|
|
23
|
+
`unotest/` (test scaffold), `.mcp.json` (Claude Code config),
|
|
24
|
+
`.claude/skills/` (skill the in-project agent reads), and an
|
|
25
|
+
`AGENTS.md` into the current directory, and updates `.gitignore` if
|
|
26
|
+
present. Where these land determines what Claude Code sees later, so
|
|
27
|
+
ask the user explicitly:
|
|
28
|
+
|
|
29
|
+
- **Default — the app's repo root.** Same directory as their
|
|
30
|
+
`package.json` (RN/Expo) or where their `.xcodeproj` lives (native).
|
|
31
|
+
Tests live next to app code, ship via the same git history, work
|
|
32
|
+
cleanly in CI.
|
|
33
|
+
- **Monorepo:** the monorepo root, not the app subfolder. Claude Code
|
|
34
|
+
is typically opened at the monorepo root, and `.mcp.json` must live
|
|
35
|
+
there to be auto-discovered.
|
|
36
|
+
- **Separate test repo:** any directory the user picks. Choose this
|
|
37
|
+
only if they explicitly want tests divorced from the app.
|
|
38
|
+
|
|
39
|
+
Once the user picks, have them `cd` there. All commands below run
|
|
40
|
+
from that directory.
|
|
41
|
+
|
|
42
|
+
**3. Ask the user for the `.app` path.** `@unotest/mobile` drives a
|
|
43
|
+
real installed app — it needs the path to a built `.app` bundle.
|
|
44
|
+
Ask: *"What's the absolute path to your built `.app`?"* Common
|
|
45
|
+
locations:
|
|
46
|
+
- React Native / Expo: `./ios/build/Build/Products/Debug-iphonesimulator/<App>.app`
|
|
47
|
+
after `npx expo run:ios` or `pod install && xcodebuild`.
|
|
48
|
+
- Native Swift: open Xcode → Product → Show Build Folder → `Products/Debug-iphonesimulator/`.
|
|
49
|
+
|
|
50
|
+
If they don't have one yet, tell them to build first; don't guess
|
|
51
|
+
paths.
|
|
52
|
+
|
|
53
|
+
**4. Tell the user to run the setup command in the chosen directory.**
|
|
54
|
+
**Do not run this yourself via Bash** — it's interactive (simulator
|
|
55
|
+
picker, permission prompts) and needs a real TTY. Your shell tool
|
|
56
|
+
will hang or get a non-interactive fallback. Give them this exact
|
|
57
|
+
line to paste (from the directory picked in step 2):
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
npx @unotest/mobile@latest install <their-app-path> --update-env
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
They'll see: env check → file scaffold → simulator picker (arrow keys)
|
|
64
|
+
→ permission grant prompt → install logs. Wait for the line
|
|
65
|
+
`✓ Installed <bundle.id>`. That's the success signal.
|
|
66
|
+
|
|
67
|
+
**5. Hand off to Claude Code.** Once install finishes, instruct the
|
|
68
|
+
user: *"Open Claude Code in **the same directory you ran setup in**,
|
|
69
|
+
then ask it to write your first test."* A separate Claude Code session
|
|
70
|
+
(not you) connects to the MCP server bootstrapped in step 4 and takes
|
|
71
|
+
over from there. Your job ends here — you're setup, not test authoring.
|
|
72
|
+
|
|
73
|
+
If any step fails, surface the actual error verbatim to the user and
|
|
74
|
+
stop. Don't try alternative paths.
|
|
75
|
+
|
|
8
76
|
## Quick start
|
|
9
77
|
|
|
10
78
|
```bash
|
|
@@ -22,7 +90,7 @@ npx @unotest/mobile@latest e2e <test-name>
|
|
|
22
90
|
```
|
|
23
91
|
|
|
24
92
|
That's it. You never edit DSL by hand — the agent writes the scenarios,
|
|
25
|
-
you review the resulting `.js` files in `unotest/e2e/`.
|
|
93
|
+
you review the resulting `.js` files in `unotest/e2e-mobile/`.
|
|
26
94
|
|
|
27
95
|
## How it works
|
|
28
96
|
|
|
@@ -48,7 +116,7 @@ you review the resulting `.js` files in `unotest/e2e/`.
|
|
|
48
116
|
returns a structured tree (roles, testID, text, bounds); we render it
|
|
49
117
|
as a token-cheap text outline. Works on RN/Expo and on plain Swift
|
|
50
118
|
apps too, as long as they expose accessibility.
|
|
51
|
-
- **Tests are plain `.js` files in your repo.** `unotest/e2e/*.js`.
|
|
119
|
+
- **Tests are plain `.js` files in your repo.** `unotest/e2e-mobile/*.js`.
|
|
52
120
|
Goes into git, into code review, into CI. No proprietary format, no
|
|
53
121
|
binary blob.
|
|
54
122
|
- **The DSL is a sandboxed JS subset.** Scenarios run in a vendored AST
|
|
@@ -82,8 +150,9 @@ agent already knows; you don't wire anything.
|
|
|
82
150
|
| `npx @unotest/mobile@latest install <path>` | Install a `.app` on the configured sim; auto-detect bundle ID + permissions; `--update-env` persists. |
|
|
83
151
|
| `npx @unotest/mobile@latest init` | Just bootstrap `unotest/` + `.mcp.json` without installing an app. |
|
|
84
152
|
| `npx @unotest/mobile@latest doctor` | Re-check the environment (Xcode, sim, Node, WDA cache). |
|
|
85
|
-
| `npx @unotest/mobile@latest e2e <name>` | Run `unotest/e2e/<name>.js`. |
|
|
153
|
+
| `npx @unotest/mobile@latest e2e <name>` | Run `unotest/e2e-mobile/<name>.js`. |
|
|
86
154
|
| `npx @unotest/mobile@latest lint` | Static check of all scenarios + helpers. |
|
|
155
|
+
| `npx @unotest/mobile@latest viewer` | Start the local results viewer (HTTP+WS) with mobile as the active target; opens in your browser. |
|
|
87
156
|
| `npx @unotest/mobile@latest` (no args) | Run as MCP stdio server (this is what Claude Code launches). |
|
|
88
157
|
|
|
89
158
|
## License
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// unotest-mobile doctor → re-run environment checks
|
|
7
7
|
// unotest-mobile e2e <name> → run scenario unotest/e2e/<name>.js
|
|
8
8
|
// unotest-mobile lint → lint scenarios + helpers
|
|
9
|
+
// unotest-mobile viewer → start the local results viewer (HTTP+WS)
|
|
9
10
|
//
|
|
10
11
|
// All entries are compiled by tsup to dist/ before publish; this dispatcher
|
|
11
12
|
// just resolves the right one and execs it via plain node. No tsx at
|
|
@@ -41,6 +42,8 @@ if (sub === "--help" || sub === "-h") {
|
|
|
41
42
|
` install <path> Install a .app bundle on the configured simulator slot(s).\n` +
|
|
42
43
|
` e2e <name> Run scenario unotest/e2e/<name>.js.\n` +
|
|
43
44
|
` lint Static check of all scenarios + helpers.\n` +
|
|
45
|
+
` viewer Start the local results viewer (HTTP+WS server).\n` +
|
|
46
|
+
` app-server Boot SIM_POOL sims + install APP_PATH; hold until Ctrl-C.\n` +
|
|
44
47
|
` (no command) Run as MCP stdio server.\n\n` +
|
|
45
48
|
`Flags:\n` +
|
|
46
49
|
` --version, -v Print package version.\n` +
|
|
@@ -70,6 +73,12 @@ function dispatch(sub) {
|
|
|
70
73
|
return { entry: `${baseDir}/runner/cli${ext}`, forwardArgs: ["lint"] };
|
|
71
74
|
case "e2e":
|
|
72
75
|
return { entry: `${baseDir}/runner/cli${ext}`, forwardArgs: args.slice(1) };
|
|
76
|
+
case "viewer":
|
|
77
|
+
return { entry: `${baseDir}/runner/cli${ext}`, forwardArgs: ["viewer"] };
|
|
78
|
+
case "app-server":
|
|
79
|
+
// Device holder (M-16): boot SIM_POOL + install APP_PATH, hold
|
|
80
|
+
// until SIGTERM. Spawned by the viewer's App panel.
|
|
81
|
+
return { entry: `${baseDir}/runner/cli${ext}`, forwardArgs: ["app-server"] };
|
|
73
82
|
default:
|
|
74
83
|
// No-arg invocation → MCP server (current claude mcp add behavior).
|
|
75
84
|
// Unknown subcommands also land here — print a hint to stderr so
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { IDslLanguageService, DslVocabEntry, DslValidateContext, DslDiagnostic } from '@unotest/protocol';
|
|
2
|
+
|
|
3
|
+
declare class MobileDslLanguageService implements IDslLanguageService {
|
|
4
|
+
private readonly vocab;
|
|
5
|
+
private readonly registry;
|
|
6
|
+
constructor();
|
|
7
|
+
getVocab(): DslVocabEntry[];
|
|
8
|
+
validate(source: string, ctx?: DslValidateContext): DslDiagnostic[];
|
|
9
|
+
}
|
|
10
|
+
declare const mobileDslLanguageService: MobileDslLanguageService;
|
|
11
|
+
|
|
12
|
+
export { MobileDslLanguageService, mobileDslLanguageService as default, mobileDslLanguageService };
|