@unotest/mobile 0.8.3 → 0.10.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 +23 -14
- package/CHANGELOG.md +56 -15
- package/README.md +4 -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
package/dist/runner/doctor.js
CHANGED
|
@@ -15,9 +15,9 @@ var ExecError = class extends Error {
|
|
|
15
15
|
__name(this, "ExecError");
|
|
16
16
|
}
|
|
17
17
|
};
|
|
18
|
-
function defaultExec(cmd, args) {
|
|
18
|
+
function defaultExec(cmd, args, cwd) {
|
|
19
19
|
try {
|
|
20
|
-
return execFileSync(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
20
|
+
return execFileSync(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], cwd });
|
|
21
21
|
} catch {
|
|
22
22
|
throw new ExecError(cmd, args);
|
|
23
23
|
}
|
|
@@ -27,7 +27,8 @@ function runEnvironmentChecks(opts = {}) {
|
|
|
27
27
|
const results = [];
|
|
28
28
|
const platform = opts.platform ?? process.platform;
|
|
29
29
|
const nodeVersion = opts.nodeVersion ?? process.versions.node;
|
|
30
|
-
const
|
|
30
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
31
|
+
const exec = opts.exec ?? ((cmd, args) => defaultExec(cmd, args, cwd));
|
|
31
32
|
if (platform !== "darwin") {
|
|
32
33
|
results.push({
|
|
33
34
|
name: "platform",
|
package/dist/runner/init.js
CHANGED
|
@@ -20,9 +20,9 @@ var ExecError = class extends Error {
|
|
|
20
20
|
__name(this, "ExecError");
|
|
21
21
|
}
|
|
22
22
|
};
|
|
23
|
-
function defaultExec(cmd, args) {
|
|
23
|
+
function defaultExec(cmd, args, cwd) {
|
|
24
24
|
try {
|
|
25
|
-
return execFileSync(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
25
|
+
return execFileSync(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], cwd });
|
|
26
26
|
} catch {
|
|
27
27
|
throw new ExecError(cmd, args);
|
|
28
28
|
}
|
|
@@ -32,7 +32,8 @@ function runEnvironmentChecks(opts = {}) {
|
|
|
32
32
|
const results = [];
|
|
33
33
|
const platform = opts.platform ?? process.platform;
|
|
34
34
|
const nodeVersion = opts.nodeVersion ?? process.versions.node;
|
|
35
|
-
const
|
|
35
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
36
|
+
const exec = opts.exec ?? ((cmd, args) => defaultExec(cmd, args, cwd));
|
|
36
37
|
if (platform !== "darwin") {
|
|
37
38
|
results.push({
|
|
38
39
|
name: "platform",
|
|
@@ -130,12 +131,12 @@ __name(runEnvironmentChecks, "runEnvironmentChecks");
|
|
|
130
131
|
|
|
131
132
|
// src/runner/init/file-templates.ts
|
|
132
133
|
var templates = {
|
|
133
|
-
smokeWelcome: `//
|
|
134
|
-
// First-run sanity: harness reaches sim, WDA, and app launch handshake
|
|
135
|
-
// #00aa00
|
|
134
|
+
smokeWelcome: `// First-run sanity: harness reaches sim, WDA, and app launch handshake
|
|
136
135
|
function test_smoke_welcome() {
|
|
137
|
-
|
|
138
|
-
|
|
136
|
+
step("launch the app", () => {
|
|
137
|
+
setDevice("A");
|
|
138
|
+
appLaunch(true);
|
|
139
|
+
});
|
|
139
140
|
}
|
|
140
141
|
`,
|
|
141
142
|
// Canonical syntax reference for AI agents writing new scenarios. The
|
|
@@ -146,50 +147,60 @@ function test_smoke_welcome() {
|
|
|
146
147
|
e2eTemplateExample: `// unotest-mobile JS-DSL \u2014 NOT Node.js.
|
|
147
148
|
//
|
|
148
149
|
// Bare top-level functions only. NO \`import\`/\`export\`/\`async\`/\`await\`/
|
|
149
|
-
// \`const\`/\`let\`/\`var
|
|
150
|
+
// \`const\`/\`let\`/\`var\`. Assignments are bare-name: \`x = expr;\`.
|
|
150
151
|
// Member access (\`obj.field\`) and object literals (\`{key: value}\`) are
|
|
151
|
-
// forbidden \u2014 payloads are passed as JSON strings.
|
|
152
|
+
// forbidden \u2014 payloads are passed as JSON strings. Every statement in a
|
|
153
|
+
// test_* body lives inside step("description", () => { ... }) \u2014 the only
|
|
154
|
+
// place an arrow function appears.
|
|
152
155
|
//
|
|
153
156
|
// After editing any scenario in this directory:
|
|
154
157
|
// npx unotest-mobile lint
|
|
155
158
|
//
|
|
156
159
|
// Below: minimal valid example. Copy its shape when writing new scenarios.
|
|
157
160
|
|
|
158
|
-
// id-example-001
|
|
159
161
|
// Example sign-in flow
|
|
160
|
-
// #4a90e2
|
|
161
162
|
function test_example() {
|
|
162
|
-
|
|
163
|
-
|
|
163
|
+
step("launch the app", () => {
|
|
164
|
+
setDevice("A");
|
|
165
|
+
appLaunch(true);
|
|
166
|
+
});
|
|
164
167
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
+
step("sign in with valid credentials", () => {
|
|
169
|
+
type(getByTestId("email-input"), "user@example.com");
|
|
170
|
+
type(getByTestId("password-input"), "secret123");
|
|
171
|
+
tap(getByTestId("signin-button"));
|
|
172
|
+
});
|
|
168
173
|
|
|
169
|
-
|
|
174
|
+
step("home screen appears", () => {
|
|
175
|
+
waitFor(getByTestId("home-screen"), 15000);
|
|
176
|
+
});
|
|
170
177
|
}
|
|
171
178
|
`,
|
|
172
|
-
scenarioTemplate: `//
|
|
173
|
-
// <one-line description>
|
|
174
|
-
// #888888
|
|
179
|
+
scenarioTemplate: `// <one-line description>
|
|
175
180
|
function test_<your_name>() {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
181
|
+
step("seed fixtures", () => {
|
|
182
|
+
// DB / API fixtures (helpers from _helpers/)
|
|
183
|
+
// wipe_e2e_users();
|
|
184
|
+
// user_id = seed_user("e2e@example.com", "e2e-pass");
|
|
185
|
+
});
|
|
179
186
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
187
|
+
step("bring the app to its initial state", () => {
|
|
188
|
+
setDevice("A");
|
|
189
|
+
appLaunch(true);
|
|
190
|
+
waitFor(getByTestId("screen-welcome"), 15000);
|
|
191
|
+
});
|
|
184
192
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
193
|
+
step("<the actions you're testing>", () => {
|
|
194
|
+
// signin("e2e@example.com", "e2e-pass");
|
|
195
|
+
// tap(getByTestId("btn-something"));
|
|
196
|
+
});
|
|
188
197
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
198
|
+
step("<what proves it worked>", () => {
|
|
199
|
+
// UI + DB / API checks
|
|
200
|
+
// assertVisible(getByTestId("screen-target"));
|
|
201
|
+
// count = dbQuery("SELECT count(*)::text FROM x WHERE ...");
|
|
202
|
+
// assertEqual(count, "1");
|
|
203
|
+
});
|
|
193
204
|
}
|
|
194
205
|
`,
|
|
195
206
|
agentsMd: `# AI Agents: how to write e2e tests for this project
|
|
@@ -199,9 +210,9 @@ for end-to-end testing of iOS React Native flows.
|
|
|
199
210
|
|
|
200
211
|
## Layout
|
|
201
212
|
|
|
202
|
-
- \`unotest/e2e/\` \u2014 scenarios (one \`test_*\` entry function per file)
|
|
203
|
-
- \`unotest/e2e/_helpers/\` \u2014 project-specific helpers, visible to all scenarios
|
|
204
|
-
- \`unotest/e2e/<feature>/\` \u2014 feature-scoped subfolders (recommended)
|
|
213
|
+
- \`unotest/e2e-mobile/\` \u2014 scenarios (one \`test_*\` entry function per file)
|
|
214
|
+
- \`unotest/e2e-mobile/_helpers/\` \u2014 project-specific helpers, visible to all scenarios
|
|
215
|
+
- \`unotest/e2e-mobile/<feature>/\` \u2014 feature-scoped subfolders (recommended)
|
|
205
216
|
|
|
206
217
|
## How to write a test
|
|
207
218
|
|
|
@@ -212,7 +223,7 @@ both Claude Code and other AI agents. Read it before writing tests.
|
|
|
212
223
|
|
|
213
224
|
## Running
|
|
214
225
|
|
|
215
|
-
- \`npx @unotest/mobile e2e <name>\` \u2014 run \`unotest/e2e/<name>.js\`
|
|
226
|
+
- \`npx @unotest/mobile e2e <name>\` \u2014 run \`unotest/e2e-mobile/<name>.js\`
|
|
216
227
|
- \`npx @unotest/mobile lint\` \u2014 static check of all scenarios
|
|
217
228
|
- \`npx @unotest/mobile doctor\` \u2014 re-check environment
|
|
218
229
|
|
|
@@ -280,8 +291,8 @@ WDA_PORTS=A=8100,B=8101
|
|
|
280
291
|
|
|
281
292
|
# --- Artifacts / sessions / paused-runtime TTL -----------------------------
|
|
282
293
|
# Defaults are sensible \u2014 uncomment only to override.
|
|
283
|
-
# SESSION_LOG_PATH=unotest
|
|
284
|
-
# ARTIFACTS_DIR=unotest
|
|
294
|
+
# SESSION_LOG_PATH=unotest/.sessions-mobile/current.jsonl
|
|
295
|
+
# ARTIFACTS_DIR=unotest/.artifacts-mobile
|
|
285
296
|
# PAUSED_RUNTIME_TTL_MS=1800000 # 30 min before paused-on-failure auto-abort
|
|
286
297
|
|
|
287
298
|
# --- Expo dev-client (reserved for future use) -----------------------------
|
|
@@ -292,8 +303,12 @@ WDA_PORTS=A=8100,B=8101
|
|
|
292
303
|
"",
|
|
293
304
|
"# unotest-mobile",
|
|
294
305
|
"unotest/.env",
|
|
295
|
-
"unotest
|
|
296
|
-
"unotest
|
|
306
|
+
"unotest/.env-mobile",
|
|
307
|
+
"unotest/.secrets-mobile",
|
|
308
|
+
"unotest/.runs-mobile/",
|
|
309
|
+
"unotest/.artifacts-mobile/",
|
|
310
|
+
"unotest/.sessions-mobile/",
|
|
311
|
+
"unotest/.debug-mobile/"
|
|
297
312
|
],
|
|
298
313
|
/**
|
|
299
314
|
* Build the `.mcp.json` entry for this package, pinned to the supplied
|
|
@@ -351,28 +366,7 @@ function mergeMcpConfig(existingSource, serverName, serverConfig, options = {})
|
|
|
351
366
|
__name(mergeMcpConfig, "mergeMcpConfig");
|
|
352
367
|
|
|
353
368
|
// src/runner/init/gitignore-updater.ts
|
|
354
|
-
|
|
355
|
-
const existing = existingContent ?? "";
|
|
356
|
-
const existingLines = new Set(existing.split("\n").map((l) => l.trim()));
|
|
357
|
-
const added = [];
|
|
358
|
-
const alreadyPresent = [];
|
|
359
|
-
for (const line of linesToAdd) {
|
|
360
|
-
const trimmed = line.trim();
|
|
361
|
-
if (trimmed === "" || existingLines.has(trimmed)) {
|
|
362
|
-
if (trimmed !== "") alreadyPresent.push(line);
|
|
363
|
-
continue;
|
|
364
|
-
}
|
|
365
|
-
added.push(line);
|
|
366
|
-
existingLines.add(trimmed);
|
|
367
|
-
}
|
|
368
|
-
if (added.length === 0) {
|
|
369
|
-
return { added, alreadyPresent, newContent: existing };
|
|
370
|
-
}
|
|
371
|
-
const sep = existing === "" ? "" : existing.endsWith("\n") ? "" : "\n";
|
|
372
|
-
const newContent = existing + sep + added.join("\n") + "\n";
|
|
373
|
-
return { added, alreadyPresent, newContent };
|
|
374
|
-
}
|
|
375
|
-
__name(appendUniqueLines, "appendUniqueLines");
|
|
369
|
+
import { appendUniqueLines } from "@unotest/core";
|
|
376
370
|
|
|
377
371
|
// src/runner/init/run.ts
|
|
378
372
|
var here = dirname(fileURLToPath(import.meta.url));
|
|
@@ -438,20 +432,20 @@ function runInit(argv = process.argv.slice(2)) {
|
|
|
438
432
|
}
|
|
439
433
|
console.log("\nFiles:");
|
|
440
434
|
const summary = [];
|
|
441
|
-
ensureDir(join(target, "unotest/e2e/_helpers"));
|
|
442
|
-
ensureDir(join(target, "unotest/e2e/_template"));
|
|
435
|
+
ensureDir(join(target, "unotest/e2e-mobile/_helpers"));
|
|
436
|
+
ensureDir(join(target, "unotest/e2e-mobile/_template"));
|
|
443
437
|
summary.push({
|
|
444
|
-
path: "unotest/e2e/smoke-welcome.js",
|
|
445
|
-
status: writeIfNeeded(join(target, "unotest/e2e/smoke-welcome.js"), templates.smokeWelcome, opts.force)
|
|
438
|
+
path: "unotest/e2e-mobile/smoke-welcome.js",
|
|
439
|
+
status: writeIfNeeded(join(target, "unotest/e2e-mobile/smoke-welcome.js"), templates.smokeWelcome, opts.force)
|
|
446
440
|
});
|
|
447
441
|
summary.push({
|
|
448
|
-
path: "unotest/e2e/_template.js",
|
|
449
|
-
status: writeIfNeeded(join(target, "unotest/e2e/_template.js"), templates.scenarioTemplate, opts.force)
|
|
442
|
+
path: "unotest/e2e-mobile/_template.js",
|
|
443
|
+
status: writeIfNeeded(join(target, "unotest/e2e-mobile/_template.js"), templates.scenarioTemplate, opts.force)
|
|
450
444
|
});
|
|
451
445
|
summary.push({
|
|
452
|
-
path: "unotest/e2e/_template/example.js",
|
|
446
|
+
path: "unotest/e2e-mobile/_template/example.js",
|
|
453
447
|
status: writeIfNeeded(
|
|
454
|
-
join(target, "unotest/e2e/_template/example.js"),
|
|
448
|
+
join(target, "unotest/e2e-mobile/_template/example.js"),
|
|
455
449
|
templates.e2eTemplateExample,
|
|
456
450
|
opts.force
|
|
457
451
|
)
|
package/dist/runner/install.js
CHANGED
|
@@ -2,21 +2,30 @@ var __defProp = Object.defineProperty;
|
|
|
2
2
|
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
3
3
|
|
|
4
4
|
// src/runner/install.ts
|
|
5
|
-
import { config as loadDotenv2 } from "dotenv";
|
|
6
5
|
import { existsSync as existsSync4 } from "fs";
|
|
7
6
|
import { resolve as resolve3 } from "path";
|
|
7
|
+
import { pathToFileURL } from "url";
|
|
8
8
|
|
|
9
9
|
// src/config/env.ts
|
|
10
|
-
import {
|
|
10
|
+
import { applyEnvLayers as applyCoreEnvLayers } from "@unotest/core";
|
|
11
11
|
import { z } from "zod";
|
|
12
|
+
|
|
13
|
+
// src/runner/scenarios-dir.ts
|
|
14
|
+
import { runsDirFor, testDirFor, UNOTEST_DIR } from "@unotest/protocol";
|
|
15
|
+
var MOBILE_SUFFIX = "-mobile";
|
|
16
|
+
var MOBILE_TEST_DIR = testDirFor(MOBILE_SUFFIX);
|
|
17
|
+
var MOBILE_SCENARIOS_DIR = `${UNOTEST_DIR}/${MOBILE_TEST_DIR}`;
|
|
18
|
+
var MOBILE_RUNS_DIR = `${UNOTEST_DIR}/${runsDirFor(MOBILE_SUFFIX)}`;
|
|
19
|
+
|
|
20
|
+
// src/config/env.ts
|
|
12
21
|
var ENV_FILE_PATH = "unotest/.env";
|
|
13
22
|
var loaded = false;
|
|
14
|
-
function
|
|
23
|
+
function applyEnvLayers() {
|
|
15
24
|
if (loaded) return;
|
|
16
|
-
|
|
25
|
+
applyCoreEnvLayers(process.cwd(), MOBILE_SUFFIX);
|
|
17
26
|
loaded = true;
|
|
18
27
|
}
|
|
19
|
-
__name(
|
|
28
|
+
__name(applyEnvLayers, "applyEnvLayers");
|
|
20
29
|
var EnvSchema = z.object({
|
|
21
30
|
// APP_BUNDLE_ID — required at appLaunch / install time. We let the schema
|
|
22
31
|
// accept it as optional so commands that don't touch the app (`doctor`,
|
|
@@ -63,7 +72,10 @@ var EnvSchema = z.object({
|
|
|
63
72
|
// `exp+<APP_URL_SCHEME>://expo-development-client/?url=<METRO_URL>` after
|
|
64
73
|
// `app_launch clean: true` to bypass the launcher. (Flow not yet wired.)
|
|
65
74
|
EXPO_DEV_CLIENT: z.string().optional().transform((v) => v === "true" || v === "1"),
|
|
66
|
-
|
|
75
|
+
// Dot-prefixed + target-suffixed per the M-10 artifact convention
|
|
76
|
+
// (`.sessions-mobile`, like `.runs-mobile`): one `.gitignore` wildcard
|
|
77
|
+
// covers all runtime debris, and web/mobile artifacts never mix.
|
|
78
|
+
SESSION_LOG_PATH: z.string().default("unotest/.sessions-mobile/current.jsonl"),
|
|
67
79
|
// Explicit kill-switch for session recording. When "1"/"true", or when
|
|
68
80
|
// SESSION_LOG_PATH is empty, buildApp wires a NoopSessionRecorder. Used
|
|
69
81
|
// by evals harness and any consumer that wants the MCP server to make
|
|
@@ -72,10 +84,10 @@ var EnvSchema = z.object({
|
|
|
72
84
|
// When true, recorder writes the FULL tool result alongside the
|
|
73
85
|
// truncated preview. Off by default — snapshots can be megabytes.
|
|
74
86
|
SESSION_LOG_FULL: z.string().optional().transform((v) => v === "1" || v === "true"),
|
|
75
|
-
ARTIFACTS_DIR: z.string().default("unotest
|
|
87
|
+
ARTIFACTS_DIR: z.string().default("unotest/.artifacts-mobile"),
|
|
76
88
|
// Where ExplorationService persists per-session JSONL recording logs.
|
|
77
89
|
// Default: <ARTIFACTS_DIR>/explorations. Folded into the gitignored
|
|
78
|
-
// `unotest
|
|
90
|
+
// `unotest/.artifacts-mobile/` tree by the init template.
|
|
79
91
|
EXPLORATIONS_DIR: z.string().optional(),
|
|
80
92
|
// WDA per-slot port mapping (D-13 parallel multi-device). Stored as a
|
|
81
93
|
// comma-separated `slot=port` list, e.g. "A=8100,B=8101". Each slot present
|
|
@@ -99,7 +111,7 @@ var EnvSchema = z.object({
|
|
|
99
111
|
var cached = null;
|
|
100
112
|
function loadEnv() {
|
|
101
113
|
if (cached) return cached;
|
|
102
|
-
|
|
114
|
+
applyEnvLayers();
|
|
103
115
|
const parsed = EnvSchema.safeParse(process.env);
|
|
104
116
|
if (!parsed.success) {
|
|
105
117
|
const issues = parsed.error.issues.map((i) => ` - ${i.path.join(".")}: ${i.message}`).join("\n");
|
|
@@ -457,6 +469,16 @@ var SimctlAdapter = class {
|
|
|
457
469
|
}
|
|
458
470
|
};
|
|
459
471
|
|
|
472
|
+
// src/runner/install/permissions.ts
|
|
473
|
+
function permissionsFromEnv(env) {
|
|
474
|
+
const envValue = env.APP_PERMISSIONS;
|
|
475
|
+
if (envValue && envValue.trim().length > 0) {
|
|
476
|
+
return envValue.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
477
|
+
}
|
|
478
|
+
return void 0;
|
|
479
|
+
}
|
|
480
|
+
__name(permissionsFromEnv, "permissionsFromEnv");
|
|
481
|
+
|
|
460
482
|
// src/util/cli-entry.ts
|
|
461
483
|
function runMain(main2, errorExitCode) {
|
|
462
484
|
main2().then(
|
|
@@ -689,9 +711,9 @@ var ExecError = class extends Error {
|
|
|
689
711
|
__name(this, "ExecError");
|
|
690
712
|
}
|
|
691
713
|
};
|
|
692
|
-
function defaultExec(cmd, args) {
|
|
714
|
+
function defaultExec(cmd, args, cwd) {
|
|
693
715
|
try {
|
|
694
|
-
return execFileSync(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
716
|
+
return execFileSync(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], cwd });
|
|
695
717
|
} catch {
|
|
696
718
|
throw new ExecError(cmd, args);
|
|
697
719
|
}
|
|
@@ -701,7 +723,8 @@ function runEnvironmentChecks(opts = {}) {
|
|
|
701
723
|
const results = [];
|
|
702
724
|
const platform = opts.platform ?? process.platform;
|
|
703
725
|
const nodeVersion = opts.nodeVersion ?? process.versions.node;
|
|
704
|
-
const
|
|
726
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
727
|
+
const exec4 = opts.exec ?? ((cmd, args) => defaultExec(cmd, args, cwd));
|
|
705
728
|
if (platform !== "darwin") {
|
|
706
729
|
results.push({
|
|
707
730
|
name: "platform",
|
|
@@ -799,12 +822,12 @@ __name(runEnvironmentChecks, "runEnvironmentChecks");
|
|
|
799
822
|
|
|
800
823
|
// src/runner/init/file-templates.ts
|
|
801
824
|
var templates = {
|
|
802
|
-
smokeWelcome: `//
|
|
803
|
-
// First-run sanity: harness reaches sim, WDA, and app launch handshake
|
|
804
|
-
// #00aa00
|
|
825
|
+
smokeWelcome: `// First-run sanity: harness reaches sim, WDA, and app launch handshake
|
|
805
826
|
function test_smoke_welcome() {
|
|
806
|
-
|
|
807
|
-
|
|
827
|
+
step("launch the app", () => {
|
|
828
|
+
setDevice("A");
|
|
829
|
+
appLaunch(true);
|
|
830
|
+
});
|
|
808
831
|
}
|
|
809
832
|
`,
|
|
810
833
|
// Canonical syntax reference for AI agents writing new scenarios. The
|
|
@@ -815,50 +838,60 @@ function test_smoke_welcome() {
|
|
|
815
838
|
e2eTemplateExample: `// unotest-mobile JS-DSL \u2014 NOT Node.js.
|
|
816
839
|
//
|
|
817
840
|
// Bare top-level functions only. NO \`import\`/\`export\`/\`async\`/\`await\`/
|
|
818
|
-
// \`const\`/\`let\`/\`var
|
|
841
|
+
// \`const\`/\`let\`/\`var\`. Assignments are bare-name: \`x = expr;\`.
|
|
819
842
|
// Member access (\`obj.field\`) and object literals (\`{key: value}\`) are
|
|
820
|
-
// forbidden \u2014 payloads are passed as JSON strings.
|
|
843
|
+
// forbidden \u2014 payloads are passed as JSON strings. Every statement in a
|
|
844
|
+
// test_* body lives inside step("description", () => { ... }) \u2014 the only
|
|
845
|
+
// place an arrow function appears.
|
|
821
846
|
//
|
|
822
847
|
// After editing any scenario in this directory:
|
|
823
848
|
// npx unotest-mobile lint
|
|
824
849
|
//
|
|
825
850
|
// Below: minimal valid example. Copy its shape when writing new scenarios.
|
|
826
851
|
|
|
827
|
-
// id-example-001
|
|
828
852
|
// Example sign-in flow
|
|
829
|
-
// #4a90e2
|
|
830
853
|
function test_example() {
|
|
831
|
-
|
|
832
|
-
|
|
854
|
+
step("launch the app", () => {
|
|
855
|
+
setDevice("A");
|
|
856
|
+
appLaunch(true);
|
|
857
|
+
});
|
|
833
858
|
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
859
|
+
step("sign in with valid credentials", () => {
|
|
860
|
+
type(getByTestId("email-input"), "user@example.com");
|
|
861
|
+
type(getByTestId("password-input"), "secret123");
|
|
862
|
+
tap(getByTestId("signin-button"));
|
|
863
|
+
});
|
|
837
864
|
|
|
838
|
-
|
|
865
|
+
step("home screen appears", () => {
|
|
866
|
+
waitFor(getByTestId("home-screen"), 15000);
|
|
867
|
+
});
|
|
839
868
|
}
|
|
840
869
|
`,
|
|
841
|
-
scenarioTemplate: `//
|
|
842
|
-
// <one-line description>
|
|
843
|
-
// #888888
|
|
870
|
+
scenarioTemplate: `// <one-line description>
|
|
844
871
|
function test_<your_name>() {
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
872
|
+
step("seed fixtures", () => {
|
|
873
|
+
// DB / API fixtures (helpers from _helpers/)
|
|
874
|
+
// wipe_e2e_users();
|
|
875
|
+
// user_id = seed_user("e2e@example.com", "e2e-pass");
|
|
876
|
+
});
|
|
848
877
|
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
878
|
+
step("bring the app to its initial state", () => {
|
|
879
|
+
setDevice("A");
|
|
880
|
+
appLaunch(true);
|
|
881
|
+
waitFor(getByTestId("screen-welcome"), 15000);
|
|
882
|
+
});
|
|
853
883
|
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
884
|
+
step("<the actions you're testing>", () => {
|
|
885
|
+
// signin("e2e@example.com", "e2e-pass");
|
|
886
|
+
// tap(getByTestId("btn-something"));
|
|
887
|
+
});
|
|
857
888
|
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
889
|
+
step("<what proves it worked>", () => {
|
|
890
|
+
// UI + DB / API checks
|
|
891
|
+
// assertVisible(getByTestId("screen-target"));
|
|
892
|
+
// count = dbQuery("SELECT count(*)::text FROM x WHERE ...");
|
|
893
|
+
// assertEqual(count, "1");
|
|
894
|
+
});
|
|
862
895
|
}
|
|
863
896
|
`,
|
|
864
897
|
agentsMd: `# AI Agents: how to write e2e tests for this project
|
|
@@ -868,9 +901,9 @@ for end-to-end testing of iOS React Native flows.
|
|
|
868
901
|
|
|
869
902
|
## Layout
|
|
870
903
|
|
|
871
|
-
- \`unotest/e2e/\` \u2014 scenarios (one \`test_*\` entry function per file)
|
|
872
|
-
- \`unotest/e2e/_helpers/\` \u2014 project-specific helpers, visible to all scenarios
|
|
873
|
-
- \`unotest/e2e/<feature>/\` \u2014 feature-scoped subfolders (recommended)
|
|
904
|
+
- \`unotest/e2e-mobile/\` \u2014 scenarios (one \`test_*\` entry function per file)
|
|
905
|
+
- \`unotest/e2e-mobile/_helpers/\` \u2014 project-specific helpers, visible to all scenarios
|
|
906
|
+
- \`unotest/e2e-mobile/<feature>/\` \u2014 feature-scoped subfolders (recommended)
|
|
874
907
|
|
|
875
908
|
## How to write a test
|
|
876
909
|
|
|
@@ -881,7 +914,7 @@ both Claude Code and other AI agents. Read it before writing tests.
|
|
|
881
914
|
|
|
882
915
|
## Running
|
|
883
916
|
|
|
884
|
-
- \`npx @unotest/mobile e2e <name>\` \u2014 run \`unotest/e2e/<name>.js\`
|
|
917
|
+
- \`npx @unotest/mobile e2e <name>\` \u2014 run \`unotest/e2e-mobile/<name>.js\`
|
|
885
918
|
- \`npx @unotest/mobile lint\` \u2014 static check of all scenarios
|
|
886
919
|
- \`npx @unotest/mobile doctor\` \u2014 re-check environment
|
|
887
920
|
|
|
@@ -949,8 +982,8 @@ WDA_PORTS=A=8100,B=8101
|
|
|
949
982
|
|
|
950
983
|
# --- Artifacts / sessions / paused-runtime TTL -----------------------------
|
|
951
984
|
# Defaults are sensible \u2014 uncomment only to override.
|
|
952
|
-
# SESSION_LOG_PATH=unotest
|
|
953
|
-
# ARTIFACTS_DIR=unotest
|
|
985
|
+
# SESSION_LOG_PATH=unotest/.sessions-mobile/current.jsonl
|
|
986
|
+
# ARTIFACTS_DIR=unotest/.artifacts-mobile
|
|
954
987
|
# PAUSED_RUNTIME_TTL_MS=1800000 # 30 min before paused-on-failure auto-abort
|
|
955
988
|
|
|
956
989
|
# --- Expo dev-client (reserved for future use) -----------------------------
|
|
@@ -961,8 +994,12 @@ WDA_PORTS=A=8100,B=8101
|
|
|
961
994
|
"",
|
|
962
995
|
"# unotest-mobile",
|
|
963
996
|
"unotest/.env",
|
|
964
|
-
"unotest
|
|
965
|
-
"unotest
|
|
997
|
+
"unotest/.env-mobile",
|
|
998
|
+
"unotest/.secrets-mobile",
|
|
999
|
+
"unotest/.runs-mobile/",
|
|
1000
|
+
"unotest/.artifacts-mobile/",
|
|
1001
|
+
"unotest/.sessions-mobile/",
|
|
1002
|
+
"unotest/.debug-mobile/"
|
|
966
1003
|
],
|
|
967
1004
|
/**
|
|
968
1005
|
* Build the `.mcp.json` entry for this package, pinned to the supplied
|
|
@@ -1020,28 +1057,7 @@ function mergeMcpConfig(existingSource, serverName, serverConfig, options = {})
|
|
|
1020
1057
|
__name(mergeMcpConfig, "mergeMcpConfig");
|
|
1021
1058
|
|
|
1022
1059
|
// src/runner/init/gitignore-updater.ts
|
|
1023
|
-
|
|
1024
|
-
const existing = existingContent ?? "";
|
|
1025
|
-
const existingLines = new Set(existing.split("\n").map((l) => l.trim()));
|
|
1026
|
-
const added = [];
|
|
1027
|
-
const alreadyPresent = [];
|
|
1028
|
-
for (const line of linesToAdd) {
|
|
1029
|
-
const trimmed = line.trim();
|
|
1030
|
-
if (trimmed === "" || existingLines.has(trimmed)) {
|
|
1031
|
-
if (trimmed !== "") alreadyPresent.push(line);
|
|
1032
|
-
continue;
|
|
1033
|
-
}
|
|
1034
|
-
added.push(line);
|
|
1035
|
-
existingLines.add(trimmed);
|
|
1036
|
-
}
|
|
1037
|
-
if (added.length === 0) {
|
|
1038
|
-
return { added, alreadyPresent, newContent: existing };
|
|
1039
|
-
}
|
|
1040
|
-
const sep = existing === "" ? "" : existing.endsWith("\n") ? "" : "\n";
|
|
1041
|
-
const newContent = existing + sep + added.join("\n") + "\n";
|
|
1042
|
-
return { added, alreadyPresent, newContent };
|
|
1043
|
-
}
|
|
1044
|
-
__name(appendUniqueLines, "appendUniqueLines");
|
|
1060
|
+
import { appendUniqueLines } from "@unotest/core";
|
|
1045
1061
|
|
|
1046
1062
|
// src/runner/init/run.ts
|
|
1047
1063
|
var here = dirname(fileURLToPath(import.meta.url));
|
|
@@ -1107,20 +1123,20 @@ function runInit(argv = process.argv.slice(2)) {
|
|
|
1107
1123
|
}
|
|
1108
1124
|
console.log("\nFiles:");
|
|
1109
1125
|
const summary = [];
|
|
1110
|
-
ensureDir(join(target, "unotest/e2e/_helpers"));
|
|
1111
|
-
ensureDir(join(target, "unotest/e2e/_template"));
|
|
1126
|
+
ensureDir(join(target, "unotest/e2e-mobile/_helpers"));
|
|
1127
|
+
ensureDir(join(target, "unotest/e2e-mobile/_template"));
|
|
1112
1128
|
summary.push({
|
|
1113
|
-
path: "unotest/e2e/smoke-welcome.js",
|
|
1114
|
-
status: writeIfNeeded(join(target, "unotest/e2e/smoke-welcome.js"), templates.smokeWelcome, opts.force)
|
|
1129
|
+
path: "unotest/e2e-mobile/smoke-welcome.js",
|
|
1130
|
+
status: writeIfNeeded(join(target, "unotest/e2e-mobile/smoke-welcome.js"), templates.smokeWelcome, opts.force)
|
|
1115
1131
|
});
|
|
1116
1132
|
summary.push({
|
|
1117
|
-
path: "unotest/e2e/_template.js",
|
|
1118
|
-
status: writeIfNeeded(join(target, "unotest/e2e/_template.js"), templates.scenarioTemplate, opts.force)
|
|
1133
|
+
path: "unotest/e2e-mobile/_template.js",
|
|
1134
|
+
status: writeIfNeeded(join(target, "unotest/e2e-mobile/_template.js"), templates.scenarioTemplate, opts.force)
|
|
1119
1135
|
});
|
|
1120
1136
|
summary.push({
|
|
1121
|
-
path: "unotest/e2e/_template/example.js",
|
|
1137
|
+
path: "unotest/e2e-mobile/_template/example.js",
|
|
1122
1138
|
status: writeIfNeeded(
|
|
1123
|
-
join(target, "unotest/e2e/_template/example.js"),
|
|
1139
|
+
join(target, "unotest/e2e-mobile/_template/example.js"),
|
|
1124
1140
|
templates.e2eTemplateExample,
|
|
1125
1141
|
opts.force
|
|
1126
1142
|
)
|
|
@@ -1678,11 +1694,7 @@ __name(parseArgs, "parseArgs");
|
|
|
1678
1694
|
function resolveCliPermissions(opts, env) {
|
|
1679
1695
|
if (opts.permissionsFlag !== void 0) return opts.permissionsFlag;
|
|
1680
1696
|
if (opts.noPermissions) return [];
|
|
1681
|
-
|
|
1682
|
-
if (envValue && envValue.trim().length > 0) {
|
|
1683
|
-
return envValue.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
1684
|
-
}
|
|
1685
|
-
return void 0;
|
|
1697
|
+
return permissionsFromEnv(env);
|
|
1686
1698
|
}
|
|
1687
1699
|
__name(resolveCliPermissions, "resolveCliPermissions");
|
|
1688
1700
|
function shouldPersistPermissions(resolved, currentEnvValue) {
|
|
@@ -1725,7 +1737,7 @@ Examples:
|
|
|
1725
1737
|
}
|
|
1726
1738
|
__name(printHelp, "printHelp");
|
|
1727
1739
|
async function ensureSimsResolved(opts) {
|
|
1728
|
-
|
|
1740
|
+
applyEnvLayers();
|
|
1729
1741
|
const currentPoolStr = process.env.SIM_POOL ?? "A,B";
|
|
1730
1742
|
const currentPool = currentPoolStr.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1731
1743
|
const simBySlot = {};
|
|
@@ -2014,7 +2026,9 @@ Or set APP_PATH in unotest/.env to skip the argument.
|
|
|
2014
2026
|
return 0;
|
|
2015
2027
|
}
|
|
2016
2028
|
__name(main, "main");
|
|
2017
|
-
|
|
2029
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
2030
|
+
runMain(main, 1);
|
|
2031
|
+
}
|
|
2018
2032
|
export {
|
|
2019
2033
|
parseArgs,
|
|
2020
2034
|
resolveCliPermissions,
|