@unotest/mobile 0.1.0 → 0.8.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/.claude/skills/write-e2e-test.md +373 -261
- package/CHANGELOG.md +468 -0
- package/README.md +11 -4
- package/bin/mcp.js +66 -8
- package/dist/mcp/server.js +2262 -408
- package/dist/runner/cli.js +1237 -181
- package/dist/runner/doctor.js +11 -12
- package/dist/runner/init.js +114 -59
- package/dist/runner/install.js +1967 -0
- package/package.json +10 -3
- package/dist/mcp/server.js.map +0 -1
- package/dist/runner/cli.js.map +0 -1
- package/dist/runner/doctor.js.map +0 -1
- package/dist/runner/init.js.map +0 -1
package/dist/runner/doctor.js
CHANGED
|
@@ -101,22 +101,22 @@ function runEnvironmentChecks(opts = {}) {
|
|
|
101
101
|
const pkg = JSON.parse(pkgRaw);
|
|
102
102
|
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
103
103
|
const isRn = "react-native" in deps || "expo" in deps;
|
|
104
|
-
if (
|
|
104
|
+
if (isRn) {
|
|
105
|
+
results.push({ name: "project-type", severity: "ok", message: "React Native / Expo detected" });
|
|
106
|
+
} else {
|
|
105
107
|
results.push({
|
|
106
|
-
name: "
|
|
107
|
-
severity: "
|
|
108
|
-
message: "
|
|
109
|
-
detail: "
|
|
108
|
+
name: "project-type",
|
|
109
|
+
severity: "ok",
|
|
110
|
+
message: "no React Native / Expo dependency in package.json",
|
|
111
|
+
detail: "Assuming a native iOS app (Swift/SwiftUI/Obj-C) or a non-Node project. Selectors resolve via the iOS accessibility tree \u2014 set accessibilityIdentifier on the views you want to target."
|
|
110
112
|
});
|
|
111
|
-
} else {
|
|
112
|
-
results.push({ name: "rn-project", severity: "ok", message: "React Native / Expo detected" });
|
|
113
113
|
}
|
|
114
114
|
} catch {
|
|
115
115
|
results.push({
|
|
116
|
-
name: "
|
|
117
|
-
severity: "
|
|
118
|
-
message: "
|
|
119
|
-
detail: "
|
|
116
|
+
name: "project-type",
|
|
117
|
+
severity: "ok",
|
|
118
|
+
message: "no package.json in current directory",
|
|
119
|
+
detail: "That's fine for native iOS projects. If this is a Node-based project, run from its root so unotest-mobile can detect React Native / Expo."
|
|
120
120
|
});
|
|
121
121
|
}
|
|
122
122
|
return results;
|
|
@@ -155,4 +155,3 @@ ${warnings} warning(s). Tests may still work, but review them.`);
|
|
|
155
155
|
}
|
|
156
156
|
__name(main, "main");
|
|
157
157
|
process.exit(main());
|
|
158
|
-
//# sourceMappingURL=doctor.js.map
|
package/dist/runner/init.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
var __defProp = Object.defineProperty;
|
|
2
2
|
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
3
3
|
|
|
4
|
-
// src/runner/init.ts
|
|
4
|
+
// src/runner/init/run.ts
|
|
5
5
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
6
6
|
import { dirname, join, relative, resolve } from "path";
|
|
7
7
|
import { fileURLToPath } from "url";
|
|
@@ -106,22 +106,22 @@ function runEnvironmentChecks(opts = {}) {
|
|
|
106
106
|
const pkg = JSON.parse(pkgRaw);
|
|
107
107
|
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
108
108
|
const isRn = "react-native" in deps || "expo" in deps;
|
|
109
|
-
if (
|
|
109
|
+
if (isRn) {
|
|
110
|
+
results.push({ name: "project-type", severity: "ok", message: "React Native / Expo detected" });
|
|
111
|
+
} else {
|
|
110
112
|
results.push({
|
|
111
|
-
name: "
|
|
112
|
-
severity: "
|
|
113
|
-
message: "
|
|
114
|
-
detail: "
|
|
113
|
+
name: "project-type",
|
|
114
|
+
severity: "ok",
|
|
115
|
+
message: "no React Native / Expo dependency in package.json",
|
|
116
|
+
detail: "Assuming a native iOS app (Swift/SwiftUI/Obj-C) or a non-Node project. Selectors resolve via the iOS accessibility tree \u2014 set accessibilityIdentifier on the views you want to target."
|
|
115
117
|
});
|
|
116
|
-
} else {
|
|
117
|
-
results.push({ name: "rn-project", severity: "ok", message: "React Native / Expo detected" });
|
|
118
118
|
}
|
|
119
119
|
} catch {
|
|
120
120
|
results.push({
|
|
121
|
-
name: "
|
|
122
|
-
severity: "
|
|
123
|
-
message: "
|
|
124
|
-
detail: "
|
|
121
|
+
name: "project-type",
|
|
122
|
+
severity: "ok",
|
|
123
|
+
message: "no package.json in current directory",
|
|
124
|
+
detail: "That's fine for native iOS projects. If this is a Node-based project, run from its root so unotest-mobile can detect React Native / Expo."
|
|
125
125
|
});
|
|
126
126
|
}
|
|
127
127
|
return results;
|
|
@@ -137,6 +137,37 @@ function test_smoke_welcome() {
|
|
|
137
137
|
setDevice("A");
|
|
138
138
|
appLaunch(true);
|
|
139
139
|
}
|
|
140
|
+
`,
|
|
141
|
+
// Canonical syntax reference for AI agents writing new scenarios. The
|
|
142
|
+
// sentinel banner up top is the strongest single-line signal against the
|
|
143
|
+
// default "this is modern JS" assumption that drives agents to write
|
|
144
|
+
// import/export/async/await. Skill references this file by path; eval
|
|
145
|
+
// harness seeds the same content into temp workdirs.
|
|
146
|
+
e2eTemplateExample: `// unotest-mobile JS-DSL \u2014 NOT Node.js.
|
|
147
|
+
//
|
|
148
|
+
// Bare top-level functions only. NO \`import\`/\`export\`/\`async\`/\`await\`/
|
|
149
|
+
// \`const\`/\`let\`/\`var\`/arrow-functions. Assignments are bare-name: \`x = expr;\`.
|
|
150
|
+
// Member access (\`obj.field\`) and object literals (\`{key: value}\`) are
|
|
151
|
+
// forbidden \u2014 payloads are passed as JSON strings.
|
|
152
|
+
//
|
|
153
|
+
// After editing any scenario in this directory:
|
|
154
|
+
// npx unotest-mobile lint
|
|
155
|
+
//
|
|
156
|
+
// Below: minimal valid example. Copy its shape when writing new scenarios.
|
|
157
|
+
|
|
158
|
+
// id-example-001
|
|
159
|
+
// Example sign-in flow
|
|
160
|
+
// #4a90e2
|
|
161
|
+
function test_example() {
|
|
162
|
+
setDevice("A");
|
|
163
|
+
appLaunch(true);
|
|
164
|
+
|
|
165
|
+
type(getByTestId("email-input"), "user@example.com");
|
|
166
|
+
type(getByTestId("password-input"), "secret123");
|
|
167
|
+
tap(getByTestId("signin-button"));
|
|
168
|
+
|
|
169
|
+
waitFor(getByTestId("home-screen"), 15000);
|
|
170
|
+
}
|
|
140
171
|
`,
|
|
141
172
|
scenarioTemplate: `// id-<your-scenario-id>
|
|
142
173
|
// <one-line description>
|
|
@@ -189,54 +220,68 @@ Inside Claude Code, prefer the MCP \`run_test\` tool with
|
|
|
189
220
|
\`pauseOnFailure: true\` \u2014 it pauses on the failed step so you can
|
|
190
221
|
\`inspect_runtime\`, fix the scenario, and \`resume\`.
|
|
191
222
|
`,
|
|
192
|
-
envExample: `# unotest-mobile \u2014 copied to unotest/.env by \`init\`. Fill in
|
|
223
|
+
envExample: `# unotest-mobile \u2014 copied to unotest/.env by \`init\`. Fill in only what
|
|
224
|
+
# your scenarios actually use. Most variables are optional.
|
|
193
225
|
|
|
194
|
-
# ---
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
INVITE_DEEPLINK_PREFIX=myapp://invite/
|
|
198
|
-
|
|
199
|
-
# --- Backend ---------------------------------------------------------------
|
|
200
|
-
API_BASE_URL=http://localhost:3000/api
|
|
201
|
-
# Optional. Absolute path used as default cwd for the \`shell(...)\` DSL
|
|
202
|
-
# primitive. Set if your scenarios shell out to project-local CLIs
|
|
203
|
-
# (pnpm cli, rails runner, etc.) that must run from the project root.
|
|
204
|
-
PROJECT_ROOT=
|
|
205
|
-
|
|
206
|
-
# --- Database --------------------------------------------------------------
|
|
207
|
-
# Pick the driver matching your backend. Install peer-deps only as needed:
|
|
208
|
-
# npm i -D pg @types/pg # for postgresql://
|
|
209
|
-
# npm i -D mysql2 # for mysql://
|
|
210
|
-
# (SQLite via better-sqlite3 is bundled \u2014 no separate install.)
|
|
226
|
+
# --- Simulators (required for any UI scenario) -----------------------------
|
|
227
|
+
# Names must match \`xcrun simctl list devices\`. SIM_POOL controls which
|
|
228
|
+
# slots are active; each slot in the pool needs the matching SIM_<slot>_NAME.
|
|
211
229
|
#
|
|
212
|
-
#
|
|
213
|
-
#
|
|
214
|
-
#
|
|
215
|
-
|
|
230
|
+
# If you have multiple sims with the same name across iOS versions,
|
|
231
|
+
# disambiguate via "<name> @ <runtime>" \u2014 runtime is a substring match,
|
|
232
|
+
# so "iOS 17" or "iOS 17.5" both work.
|
|
233
|
+
# SIM_A_NAME= # pick interactively on first \`install\`
|
|
234
|
+
# SIM_A_NAME=iPhone 16 @ iOS 17.5 # pin to a specific runtime
|
|
235
|
+
# SIM_B_NAME= # only needed if SIM_POOL includes "B"
|
|
236
|
+
# SIM_POOL=A,B # default \u2014 drop "B" if you only need one sim
|
|
216
237
|
|
|
217
|
-
# ---
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
238
|
+
# --- App under test (required if scenarios call appLaunch / openDeeplink) --
|
|
239
|
+
APP_BUNDLE_ID=com.example.myapp
|
|
240
|
+
# APP_URL_SCHEME=myapp # only used by Expo dev-client recovery flow
|
|
241
|
+
# Optional. Path to a built .app bundle. When set, \`unotest-mobile install\`
|
|
242
|
+
# (and the \`app_install\` MCP tool) can be invoked without an explicit path.
|
|
243
|
+
# Useful for repeated installs after each app rebuild.
|
|
244
|
+
# APP_PATH=./build-sim/Build/Products/Release-iphonesimulator/MyApp.app
|
|
245
|
+
# Comma-separated \`simctl privacy\` services to auto-grant on \`install --clean\`.
|
|
246
|
+
# Populated by \`unotest-mobile install --update-env\` from your app's
|
|
247
|
+
# Info.plist NS*UsageDescription keys. Pre-empts the iOS permission dialog
|
|
248
|
+
# on first launch \u2014 those dialogs live in SpringBoard, not in the app's
|
|
249
|
+
# a11y tree, and would otherwise block your scenarios.
|
|
250
|
+
# APP_PERMISSIONS=location,motion
|
|
251
|
+
|
|
252
|
+
# --- Backend (optional \u2014 only required if scenarios use apiCall / db) ------
|
|
253
|
+
# API_BASE_URL=http://localhost:3000/api
|
|
254
|
+
# DATABASE_URL=postgresql://postgres:postgres@localhost:5432/myapp_test
|
|
255
|
+
# Drivers (install peer-deps as needed):
|
|
256
|
+
# npm i -D pg @types/pg # for postgresql://
|
|
257
|
+
# npm i -D mysql2 # for mysql://
|
|
258
|
+
# SQLite via better-sqlite3 is bundled \u2014 no separate install.
|
|
259
|
+
# Docker-compose Postgres/MySQL: ensure host port is exposed
|
|
260
|
+
# (\`ports: ["5432:5432"]\`). Native clients connect from the host, not
|
|
261
|
+
# from inside the compose network.
|
|
222
262
|
|
|
223
|
-
# ---
|
|
224
|
-
|
|
225
|
-
#
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
263
|
+
# --- Optional: shell() default cwd -----------------------------------------
|
|
264
|
+
# Absolute path used as default cwd for the \`shell(...)\` DSL primitive.
|
|
265
|
+
# When unset, shell commands run from process.cwd(). Set to your project
|
|
266
|
+
# root if your scenarios shell out to project-local CLIs that must run
|
|
267
|
+
# from there.
|
|
268
|
+
# PROJECT_ROOT=
|
|
229
269
|
|
|
230
270
|
# --- WebDriverAgent --------------------------------------------------------
|
|
231
271
|
# Per-slot WDA ports. Each slot in SIM_POOL needs one. Format: "slot=port".
|
|
232
272
|
WDA_PORTS=A=8100,B=8101
|
|
233
|
-
WDA_DEFAULT_ACTION_WAIT_MS=2000
|
|
234
|
-
WDA_DEFAULT_WAITFOR_TIMEOUT_MS=10000
|
|
273
|
+
# WDA_DEFAULT_ACTION_WAIT_MS=2000
|
|
274
|
+
# WDA_DEFAULT_WAITFOR_TIMEOUT_MS=10000
|
|
275
|
+
|
|
276
|
+
# --- Artifacts / sessions / paused-runtime TTL -----------------------------
|
|
277
|
+
# Defaults are sensible \u2014 uncomment only to override.
|
|
278
|
+
# SESSION_LOG_PATH=unotest/sessions/current.jsonl
|
|
279
|
+
# ARTIFACTS_DIR=unotest/artifacts
|
|
280
|
+
# PAUSED_RUNTIME_TTL_MS=1800000 # 30 min before paused-on-failure auto-abort
|
|
235
281
|
|
|
236
|
-
# ---
|
|
237
|
-
#
|
|
238
|
-
#
|
|
239
|
-
PAUSED_RUNTIME_TTL_MS=1800000
|
|
282
|
+
# --- Expo dev-client (reserved for future use) -----------------------------
|
|
283
|
+
# METRO_URL=http://localhost:8081
|
|
284
|
+
# EXPO_DEV_CLIENT=false
|
|
240
285
|
`,
|
|
241
286
|
gitignoreLines: [
|
|
242
287
|
"",
|
|
@@ -314,16 +359,16 @@ function appendUniqueLines(existingContent, linesToAdd) {
|
|
|
314
359
|
}
|
|
315
360
|
__name(appendUniqueLines, "appendUniqueLines");
|
|
316
361
|
|
|
317
|
-
// src/runner/init.ts
|
|
362
|
+
// src/runner/init/run.ts
|
|
318
363
|
var here = dirname(fileURLToPath(import.meta.url));
|
|
319
364
|
var packageRoot = resolve(here, "..", "..");
|
|
320
|
-
function
|
|
365
|
+
function parseInitArgs(argv) {
|
|
321
366
|
return {
|
|
322
367
|
force: argv.includes("--force"),
|
|
323
368
|
allowNonMacos: argv.includes("--allow-non-macos")
|
|
324
369
|
};
|
|
325
370
|
}
|
|
326
|
-
__name(
|
|
371
|
+
__name(parseInitArgs, "parseInitArgs");
|
|
327
372
|
function symbol(severity) {
|
|
328
373
|
return severity === "ok" ? "\u2713" : severity === "warning" ? "\u26A0" : "\u2717";
|
|
329
374
|
}
|
|
@@ -346,8 +391,8 @@ function readPackageFile(relativePath) {
|
|
|
346
391
|
return readFileSync(abs, "utf8");
|
|
347
392
|
}
|
|
348
393
|
__name(readPackageFile, "readPackageFile");
|
|
349
|
-
function
|
|
350
|
-
const opts =
|
|
394
|
+
function runInit(argv = process.argv.slice(2)) {
|
|
395
|
+
const opts = parseInitArgs(argv);
|
|
351
396
|
const target = process.cwd();
|
|
352
397
|
console.log("unotest-mobile init \u2014 bootstrapping project\n");
|
|
353
398
|
console.log("Environment:");
|
|
@@ -365,6 +410,7 @@ function main() {
|
|
|
365
410
|
console.log("\nFiles:");
|
|
366
411
|
const summary = [];
|
|
367
412
|
ensureDir(join(target, "unotest/e2e/_helpers"));
|
|
413
|
+
ensureDir(join(target, "unotest/e2e/_template"));
|
|
368
414
|
summary.push({
|
|
369
415
|
path: "unotest/e2e/smoke-welcome.js",
|
|
370
416
|
status: writeIfNeeded(join(target, "unotest/e2e/smoke-welcome.js"), templates.smokeWelcome, opts.force)
|
|
@@ -373,6 +419,14 @@ function main() {
|
|
|
373
419
|
path: "unotest/e2e/_template.js",
|
|
374
420
|
status: writeIfNeeded(join(target, "unotest/e2e/_template.js"), templates.scenarioTemplate, opts.force)
|
|
375
421
|
});
|
|
422
|
+
summary.push({
|
|
423
|
+
path: "unotest/e2e/_template/example.js",
|
|
424
|
+
status: writeIfNeeded(
|
|
425
|
+
join(target, "unotest/e2e/_template/example.js"),
|
|
426
|
+
templates.e2eTemplateExample,
|
|
427
|
+
opts.force
|
|
428
|
+
)
|
|
429
|
+
});
|
|
376
430
|
summary.push({
|
|
377
431
|
path: "unotest/AGENTS.md",
|
|
378
432
|
status: writeIfNeeded(join(target, "unotest/AGENTS.md"), templates.agentsMd, opts.force)
|
|
@@ -448,6 +502,7 @@ Re-check environment anytime: \`npx @unotest/mobile doctor\`
|
|
|
448
502
|
`);
|
|
449
503
|
return 0;
|
|
450
504
|
}
|
|
451
|
-
__name(
|
|
452
|
-
|
|
453
|
-
|
|
505
|
+
__name(runInit, "runInit");
|
|
506
|
+
|
|
507
|
+
// src/runner/init.ts
|
|
508
|
+
process.exit(runInit());
|