@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.
@@ -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 (!isRn) {
104
+ if (isRn) {
105
+ results.push({ name: "project-type", severity: "ok", message: "React Native / Expo detected" });
106
+ } else {
105
107
  results.push({
106
- name: "rn-project",
107
- severity: "warning",
108
- message: "package.json doesn't declare react-native or expo.",
109
- detail: "unotest-mobile is iOS RN/Expo-focused. Continuing \u2014 but make sure this is intentional."
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: "rn-project",
117
- severity: "warning",
118
- message: "No readable package.json in current directory.",
119
- detail: "Run init from your project's root."
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
@@ -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 (!isRn) {
109
+ if (isRn) {
110
+ results.push({ name: "project-type", severity: "ok", message: "React Native / Expo detected" });
111
+ } else {
110
112
  results.push({
111
- name: "rn-project",
112
- severity: "warning",
113
- message: "package.json doesn't declare react-native or expo.",
114
- detail: "unotest-mobile is iOS RN/Expo-focused. Continuing \u2014 but make sure this is intentional."
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: "rn-project",
122
- severity: "warning",
123
- message: "No readable package.json in current directory.",
124
- detail: "Run init from your project's root."
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 below.
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
- # --- App under test --------------------------------------------------------
195
- APP_BUNDLE_ID=com.example.myapp
196
- APP_URL_SCHEME=myapp
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
- # Docker-compose Postgres/MySQL \u2014 ensure host port is exposed
213
- # (\`ports: ["5432:5432"]\`). Native clients connect from the host, not from
214
- # inside the compose network.
215
- DATABASE_URL=postgresql://postgres:postgres@localhost:5432/myapp_test
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
- # --- Simulators ------------------------------------------------------------
218
- # Names must match \`xcrun simctl list devices\`.
219
- SIM_A_NAME=iPhone 15
220
- SIM_B_NAME=iPhone 15 Plus
221
- SIM_POOL=A,B
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
- # --- Dev tooling -----------------------------------------------------------
224
- METRO_URL=http://localhost:8081
225
- # Expo dev-client: auto-open dev-client deep link after a clean launch.
226
- EXPO_DEV_CLIENT=false
227
- SESSION_LOG_PATH=sessions/current.jsonl
228
- ARTIFACTS_DIR=artifacts
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
- # --- D-17 paused-runtime TTL ----------------------------------------------
237
- # How long a paused-on-failure runtime stays alive before auto-abort (ms).
238
- # Default 30 minutes. Inspecting the runtime resets the timer.
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 parseArgs(argv) {
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(parseArgs, "parseArgs");
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 main() {
350
- const opts = parseArgs(process.argv.slice(2));
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(main, "main");
452
- process.exit(main());
453
- //# sourceMappingURL=init.js.map
505
+ __name(runInit, "runInit");
506
+
507
+ // src/runner/init.ts
508
+ process.exit(runInit());