@unotest/mobile 0.8.3 → 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.
@@ -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 exec = opts.exec ?? defaultExec;
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",
@@ -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 exec = opts.exec ?? defaultExec;
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: `// id-smoke-welcome
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
- setDevice("A");
138
- appLaunch(true);
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\`/arrow-functions. Assignments are bare-name: \`x = expr;\`.
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
- setDevice("A");
163
- appLaunch(true);
163
+ step("launch the app", () => {
164
+ setDevice("A");
165
+ appLaunch(true);
166
+ });
164
167
 
165
- type(getByTestId("email-input"), "user@example.com");
166
- type(getByTestId("password-input"), "secret123");
167
- tap(getByTestId("signin-button"));
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
- waitFor(getByTestId("home-screen"), 15000);
174
+ step("home screen appears", () => {
175
+ waitFor(getByTestId("home-screen"), 15000);
176
+ });
170
177
  }
171
178
  `,
172
- scenarioTemplate: `// id-<your-scenario-id>
173
- // <one-line description>
174
- // #888888
179
+ scenarioTemplate: `// <one-line description>
175
180
  function test_<your_name>() {
176
- // 1. SETUP \u2014 DB / API fixtures (helpers from _helpers/)
177
- // wipe_e2e_users();
178
- // user_id = seed_user("e2e@example.com", "e2e-pass");
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
- // 2. ENTER \u2014 bring app to initial UI state
181
- setDevice("A");
182
- appLaunch(true);
183
- waitFor(getByTestId("screen-welcome"), 15000);
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
- // 3. ACT \u2014 actions you're testing
186
- // signin("e2e@example.com", "e2e-pass");
187
- // tap(getByTestId("btn-something"));
193
+ step("<the actions you're testing>", () => {
194
+ // signin("e2e@example.com", "e2e-pass");
195
+ // tap(getByTestId("btn-something"));
196
+ });
188
197
 
189
- // 4. ASSERT \u2014 UI + DB / API checks
190
- // assertVisible(getByTestId("screen-target"));
191
- // count = dbQuery("SELECT count(*)::text FROM x WHERE ...");
192
- // assertEqual(count, "1");
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/sessions/current.jsonl
284
- # ARTIFACTS_DIR=unotest/artifacts
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/artifacts/",
296
- "unotest/sessions/"
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
- function appendUniqueLines(existingContent, linesToAdd) {
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
  )
@@ -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 { config as loadDotenv } from "dotenv";
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 ensureLoaded() {
23
+ function applyEnvLayers() {
15
24
  if (loaded) return;
16
- loadDotenv({ path: ENV_FILE_PATH });
25
+ applyCoreEnvLayers(process.cwd(), MOBILE_SUFFIX);
17
26
  loaded = true;
18
27
  }
19
- __name(ensureLoaded, "ensureLoaded");
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
- SESSION_LOG_PATH: z.string().default("unotest/sessions/current.jsonl"),
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/artifacts"),
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/artifacts/` tree by the init template.
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
- ensureLoaded();
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 exec4 = opts.exec ?? defaultExec;
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: `// id-smoke-welcome
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
- setDevice("A");
807
- appLaunch(true);
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\`/arrow-functions. Assignments are bare-name: \`x = expr;\`.
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
- setDevice("A");
832
- appLaunch(true);
854
+ step("launch the app", () => {
855
+ setDevice("A");
856
+ appLaunch(true);
857
+ });
833
858
 
834
- type(getByTestId("email-input"), "user@example.com");
835
- type(getByTestId("password-input"), "secret123");
836
- tap(getByTestId("signin-button"));
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
- waitFor(getByTestId("home-screen"), 15000);
865
+ step("home screen appears", () => {
866
+ waitFor(getByTestId("home-screen"), 15000);
867
+ });
839
868
  }
840
869
  `,
841
- scenarioTemplate: `// id-<your-scenario-id>
842
- // <one-line description>
843
- // #888888
870
+ scenarioTemplate: `// <one-line description>
844
871
  function test_<your_name>() {
845
- // 1. SETUP \u2014 DB / API fixtures (helpers from _helpers/)
846
- // wipe_e2e_users();
847
- // user_id = seed_user("e2e@example.com", "e2e-pass");
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
- // 2. ENTER \u2014 bring app to initial UI state
850
- setDevice("A");
851
- appLaunch(true);
852
- waitFor(getByTestId("screen-welcome"), 15000);
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
- // 3. ACT \u2014 actions you're testing
855
- // signin("e2e@example.com", "e2e-pass");
856
- // tap(getByTestId("btn-something"));
884
+ step("<the actions you're testing>", () => {
885
+ // signin("e2e@example.com", "e2e-pass");
886
+ // tap(getByTestId("btn-something"));
887
+ });
857
888
 
858
- // 4. ASSERT \u2014 UI + DB / API checks
859
- // assertVisible(getByTestId("screen-target"));
860
- // count = dbQuery("SELECT count(*)::text FROM x WHERE ...");
861
- // assertEqual(count, "1");
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/sessions/current.jsonl
953
- # ARTIFACTS_DIR=unotest/artifacts
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/artifacts/",
965
- "unotest/sessions/"
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
- function appendUniqueLines(existingContent, linesToAdd) {
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
- const envValue = env.APP_PERMISSIONS;
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
- loadDotenv2({ path: ENV_FILE_PATH });
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
- runMain(main, 1);
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,
@@ -0,0 +1,6 @@
1
+ import { IRunnerAdapter } from '@unotest/protocol';
2
+
3
+ /** Singleton — the adapter is purely a function bag. */
4
+ declare const mobileRunnerAdapter: IRunnerAdapter;
5
+
6
+ export { mobileRunnerAdapter as default, mobileRunnerAdapter };