@delacour/warden-e2e 0.12.0 → 0.13.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.
@@ -723,8 +723,9 @@ var UNCHECKED_WORDS = new Set(["0", "off", "false", "unchecked"]);
723
723
  function nonEmpty(value) {
724
724
  return value === undefined || value === "" ? undefined : value;
725
725
  }
726
+ var MASKED_VALUE = /^[\u2022\u25CF]+$/;
726
727
  function isSecure(element, traits) {
727
- return element.role === "securefield" || traits.has("password") || traits.has("secure") || element.rawType.toLowerCase().includes("secure");
728
+ return element.role === "textfield" && element.value !== undefined && MASKED_VALUE.test(element.value) || element.role === "securefield" || traits.has("password") || traits.has("secure") || element.rawType.toLowerCase().includes("secure");
728
729
  }
729
730
  function deriveChecked(element, role, spelled, traits, value) {
730
731
  if (spelled?.checked !== undefined)
@@ -6298,6 +6299,9 @@ function getSession(opts = {}) {
6298
6299
  slot()[SESSION_KEY] = session;
6299
6300
  return session;
6300
6301
  }
6302
+ function peekSession() {
6303
+ return slot()[SESSION_KEY];
6304
+ }
6301
6305
 
6302
6306
  // ../../packages/e2e/src/a11y/speech.ts
6303
6307
  var ACTIONABLE_ROLES = new Set([
@@ -9323,6 +9327,6 @@ function onlyWasSeen() {
9323
9327
 
9324
9328
  // ../../packages/e2e/src/version.ts
9325
9329
  var WORKSPACE_VERSION = "0.0.0-workspace";
9326
- var WARDEN_E2E_VERSION2 = "0.12.0";
9330
+ var WARDEN_E2E_VERSION2 = "0.13.0";
9327
9331
 
9328
- export { E2eError2, registerA11yTarget, a11yTargetOf, canActOnElements, noteSpanDetail, addSpanDetail, canObserve, canDetectAlerts, settleDetailOf, DEFAULT_SETTLE_QUIET_MS, DEFAULT_SETTLE_TIMEOUT_MS, settleByHash, snapshotHash, ok, err, swipeVector, screenRect, fieldValue, PINCH_MS, ROTATE_MS, PATH_MS, centreOf, pinchRadius, rotateRadius, requireGesture, assertScale, assertDegrees, isInteractiveRole, resolveRole, normalizeText, matchesText, indexSnapshot, testIdQuery, textQuery, roleQuery, selectorExpression, renderSnapshot, renderHeader, resolve, locateByTestIdViaReact, isLocator, Locator, pickOne, textOf, scrollUntilVisible, formatNativeTail, realNativeLogDeps, captureNativeLog, bunExec, execError, driverError, driverErr, logEventSchema, fatalEventSchema, responseSchema, mapWireError, createLineSplitter, encodeRequest, bunSpawn, stdioTransport, socketTransport, createSimdClient, spawnSimd, connectSimd, isRecoverableLoss, CAMERA_FACINGS, CONDITION_AREAS, resolveNetwork, resolveTextSize, androidFontScale, ENV_NAMES, DEFAULT_ASSERTION_TIMEOUT_MS, readEnv, runStep, setDefaultHidRecovery, setDefaultNativeLog, setDefaultDriverFactory, hasDefaultDriverFactory, setDefaultFixtureFactory, hasDefaultFixtureFactory, attemptSession, getSession, accessibleName, announce, failing, formatIssues, toE2eError, settled, unwrap, parseUiadA11yEvents, parseInjectEvents, withA11yRead, renderA11yScreen, createDeviceA11y, IOS_DEVICE_CAPABILITIES, sniffCameraMedia, cameraFeedDir, clearCameraFeeds, isLeaseAlive, processAlive, describeOwner, isPhysicalIosId, wardenHome, defaultDbPath, ownerKey, openStore, callerFile, formatCoord, DEFAULT_SPEED_MPS, DEFAULT_INTERVAL_S, realTickerClock, startRouteTicker, startFlap, registerConditions, conditionsOf, conditionSinkFor, createDeviceConditions, parseBudgets, createDevicePerf, perfStartFor, spawnWarden, hash6, attemptDir, screenshotPath, specIdOf, BUN_TIMEOUT_MARGIN_MS2, ownHook, currentScope, createScope, runInScope, hooksFor, resolveUse, registrationDefaults, markOnly, onlyWasSeen, WORKSPACE_VERSION, WARDEN_E2E_VERSION2 };
9332
+ export { E2eError2, registerA11yTarget, a11yTargetOf, canActOnElements, noteSpanDetail, addSpanDetail, canObserve, canDetectAlerts, settleDetailOf, DEFAULT_SETTLE_QUIET_MS, DEFAULT_SETTLE_TIMEOUT_MS, settleByHash, snapshotHash, ok, err, swipeVector, screenRect, fieldValue, PINCH_MS, ROTATE_MS, PATH_MS, centreOf, pinchRadius, rotateRadius, requireGesture, assertScale, assertDegrees, isInteractiveRole, resolveRole, normalizeText, matchesText, indexSnapshot, testIdQuery, textQuery, roleQuery, selectorExpression, renderSnapshot, renderHeader, resolve, locateByTestIdViaReact, isLocator, Locator, pickOne, textOf, scrollUntilVisible, formatNativeTail, realNativeLogDeps, captureNativeLog, bunExec, execError, driverError, driverErr, logEventSchema, fatalEventSchema, responseSchema, mapWireError, createLineSplitter, encodeRequest, bunSpawn, stdioTransport, socketTransport, createSimdClient, spawnSimd, connectSimd, isRecoverableLoss, parseDuration, CAMERA_FACINGS, CONDITION_AREAS, resolveNetwork, resolveTextSize, androidFontScale, ENV_NAMES, DEFAULT_ASSERTION_TIMEOUT_MS, readEnv, runStep, setDefaultHidRecovery, setDefaultNativeLog, setDefaultDriverFactory, hasDefaultDriverFactory, setDefaultFixtureFactory, hasDefaultFixtureFactory, attemptSession, getSession, peekSession, accessibleName, announce, failing, formatIssues, toE2eError, settled, unwrap, parseUiadA11yEvents, parseInjectEvents, withA11yRead, renderA11yScreen, createDeviceA11y, IOS_DEVICE_CAPABILITIES, sniffCameraMedia, cameraFeedDir, clearCameraFeeds, isLeaseAlive, processAlive, describeOwner, isPhysicalIosId, wardenHome, defaultDbPath, ownerKey, openStore, callerFile, formatCoord, DEFAULT_SPEED_MPS, DEFAULT_INTERVAL_S, realTickerClock, startRouteTicker, startFlap, registerConditions, conditionsOf, conditionSinkFor, createDeviceConditions, parseBudgets, createDevicePerf, perfStartFor, spawnWarden, hash6, attemptDir, screenshotPath, specIdOf, BUN_TIMEOUT_MARGIN_MS2, ownHook, currentScope, createScope, runInScope, hooksFor, resolveUse, registrationDefaults, markOnly, onlyWasSeen, WORKSPACE_VERSION, WARDEN_E2E_VERSION2 };
package/index.mjs CHANGED
@@ -15,9 +15,11 @@ import {
15
15
  formatNativeTail,
16
16
  captureNativeLog,
17
17
  isRecoverableLoss,
18
+ parseDuration,
18
19
  runStep,
19
20
  attemptSession,
20
21
  getSession,
22
+ peekSession,
21
23
  accessibleName,
22
24
  announce,
23
25
  failing,
@@ -42,7 +44,7 @@ import {
42
44
  registrationDefaults,
43
45
  markOnly,
44
46
  WARDEN_E2E_VERSION2
45
- } from "./chunk-qz94hsb6.mjs";
47
+ } from "./chunk-tnr47n9c.mjs";
46
48
  // ../../packages/e2e/src/expect/expect.ts
47
49
  import { expect as bunExpect } from "bun:test";
48
50
 
@@ -572,6 +574,116 @@ function createExpect(deps) {
572
574
  return call;
573
575
  }
574
576
  var expect = createExpect({ softSink: currentSoftSink });
577
+ // ../../packages/e2e/src/run.ts
578
+ var DEFAULT_TIMEOUT_MS = 60000;
579
+ var MESSAGE_TAIL = 2000;
580
+ var invalid = (message) => new E2eError2("INVALID_ARGUMENT", `run: ${message}`);
581
+ function timeoutMs(value) {
582
+ if (value === undefined)
583
+ return DEFAULT_TIMEOUT_MS;
584
+ if (typeof value === "number") {
585
+ if (!Number.isFinite(value) || value <= 0)
586
+ throw invalid(`timeout must be over 0, got ${String(value)}`);
587
+ return value;
588
+ }
589
+ const parsed = parseDuration(value);
590
+ if (!parsed.success)
591
+ throw invalid(`timeout: ${parsed.error}`);
592
+ return parsed.data;
593
+ }
594
+ function argvOf(command) {
595
+ if (typeof command === "string") {
596
+ if (command.trim() === "")
597
+ throw invalid("command is empty");
598
+ return ["sh", "-c", command];
599
+ }
600
+ if (command.length === 0 || command[0] === "")
601
+ throw invalid("argv is empty");
602
+ return [...command];
603
+ }
604
+ var programOf = (command) => typeof command === "string" ? "sh -c" : command[0] ?? "";
605
+ function envOf(extra) {
606
+ const merged = { ...process.env, ...extra };
607
+ const env = {};
608
+ for (const [key, value] of Object.entries(merged))
609
+ if (value !== undefined)
610
+ env[key] = value;
611
+ return env;
612
+ }
613
+ function tail(text) {
614
+ const trimmed = text.trim();
615
+ return trimmed.length > MESSAGE_TAIL ? `\u2026${trimmed.slice(-MESSAGE_TAIL)}` : trimmed;
616
+ }
617
+ function resultOf(exitCode, stdout, stderr, ms) {
618
+ return {
619
+ ok: exitCode === 0,
620
+ exitCode,
621
+ stdout,
622
+ stderr,
623
+ ms,
624
+ json() {
625
+ try {
626
+ return JSON.parse(stdout);
627
+ } catch {
628
+ throw invalid(`stdout is not JSON: ${tail(stdout).slice(0, 200) || "(empty)"}`);
629
+ }
630
+ }
631
+ };
632
+ }
633
+ async function exec(command, options) {
634
+ const argv = argvOf(command);
635
+ const limit = timeoutMs(options.timeout);
636
+ const started = Date.now();
637
+ let proc;
638
+ try {
639
+ proc = Bun.spawn(argv, {
640
+ cwd: options.cwd,
641
+ env: envOf(options.env),
642
+ stdin: "pipe",
643
+ stdout: "pipe",
644
+ stderr: "pipe"
645
+ });
646
+ } catch (error) {
647
+ const reason = error instanceof Error ? error.message : String(error);
648
+ throw new E2eError2("RUN_FAILED", `run: could not start ${programOf(command)}: ${reason}`);
649
+ }
650
+ if (options.input !== undefined)
651
+ proc.stdin.write(options.input);
652
+ await proc.stdin.end();
653
+ let timedOut = false;
654
+ const timer = setTimeout(() => {
655
+ timedOut = true;
656
+ proc.kill("SIGKILL");
657
+ }, limit);
658
+ try {
659
+ const [stdout, stderr, exitCode] = await Promise.all([
660
+ new Response(proc.stdout).text(),
661
+ new Response(proc.stderr).text(),
662
+ proc.exited
663
+ ]);
664
+ const result = resultOf(exitCode, stdout, stderr, Date.now() - started);
665
+ if (timedOut) {
666
+ throw new E2eError2("TIMEOUT", `run: ${programOf(command)} did not finish within ${limit} ms (killed)`);
667
+ }
668
+ if (!result.ok && options.check !== false) {
669
+ const output = tail(stderr) || tail(stdout);
670
+ throw new E2eError2("RUN_FAILED", `run: ${programOf(command)} failed with exit ${exitCode}${output === "" ? "" : `
671
+ ${output}`}`);
672
+ }
673
+ return result;
674
+ } finally {
675
+ clearTimeout(timer);
676
+ }
677
+ }
678
+ function createRun(deps) {
679
+ return (command, options = {}) => deps.span("run", () => exec(command, options), {
680
+ verdict: (result) => result.ok ? { ok: true } : { ok: false, code: "RUN_FAILED" },
681
+ detail: (result) => ({ exitCode: result.exitCode })
682
+ });
683
+ }
684
+ var run = createRun({
685
+ span: (op, fn, options) => peekSession()?.span(op, fn, options) ?? fn()
686
+ });
575
687
  // ../../packages/e2e/src/runner/test.ts
576
688
  import { afterAll as bunAfterAll, beforeAll as bunBeforeAll, describe as bunDescribe, test as bunTest } from "bun:test";
577
689
 
@@ -1277,5 +1389,6 @@ export {
1277
1389
  beforeEach,
1278
1390
  describe,
1279
1391
  expect,
1392
+ run,
1280
1393
  test
1281
1394
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@delacour/warden-e2e",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "description": "TypeScript e2e testing for React Native and Expo on iOS simulators and Android emulators, run by `warden e2e`: test, expect, screen, device, app",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -41,7 +41,7 @@
41
41
  "LICENSE"
42
42
  ],
43
43
  "peerDependencies": {
44
- "@delacour/warden": "0.12.0"
44
+ "@delacour/warden": "0.13.0"
45
45
  },
46
46
  "dependencies": {
47
47
  "zod": "^4.4.3"
package/preload.mjs CHANGED
@@ -109,7 +109,7 @@ import {
109
109
  onlyWasSeen,
110
110
  WORKSPACE_VERSION,
111
111
  WARDEN_E2E_VERSION2
112
- } from "./chunk-qz94hsb6.mjs";
112
+ } from "./chunk-tnr47n9c.mjs";
113
113
 
114
114
  // ../../packages/e2e/src/preload.ts
115
115
  import { afterAll, setDefaultTimeout } from "bun:test";
@@ -7728,6 +7728,7 @@ function locateHelper(input) {
7728
7728
  function resolveHelper(o) {
7729
7729
  if (o.helperPath !== undefined)
7730
7730
  return { success: true, data: o.helperPath };
7731
+ const version = o.version ?? o.env?.wardenVersion;
7731
7732
  const located = locateHelper({
7732
7733
  env: { [ENV_NAMES.simd]: o.env?.simd, [ENV_NAMES.helperDir]: o.env?.helperDir },
7733
7734
  execPath: o.execPath ?? process.execPath,
@@ -7735,7 +7736,7 @@ function resolveHelper(o) {
7735
7736
  wardenHome: o.wardenHome ?? wardenHome(),
7736
7737
  exists: o.exists ?? existsSync7,
7737
7738
  realpath: o.realpath ?? realpathSync,
7738
- ...o.version ? { version: o.version } : {}
7739
+ ...version ? { version } : {}
7739
7740
  });
7740
7741
  return located.success ? { success: true, data: located.data.path } : located;
7741
7742
  }
@@ -7816,6 +7817,8 @@ function toSnapshot(data, takenAt) {
7816
7817
  };
7817
7818
  if (data.modal)
7818
7819
  snapshot.modal = data.modal;
7820
+ if (data.fallback?.from === "axbridge")
7821
+ snapshot.fellBackFrom = "axbridge";
7819
7822
  return snapshot;
7820
7823
  }
7821
7824
 
@@ -8401,6 +8404,9 @@ var DEFAULT_QUIET_MS = 250;
8401
8404
  var DEFAULT_SETTLE_TIMEOUT_MS2 = 5000;
8402
8405
  var SETTLE_MARGIN_MS = 2000;
8403
8406
  var REBIND_TIMEOUT_MS = 1e4;
8407
+ var COLD_BRIDGE_BUDGET_MS = 60000;
8408
+ var COLD_BRIDGE_FIRST_WAIT_MS = 2000;
8409
+ var COLD_BRIDGE_WAIT_MS = 5000;
8404
8410
  var GESTURE_MARGIN_MS = 5000;
8405
8411
  var FRONTMOST_TIMEOUT_MS = 3000;
8406
8412
  var LAUNCH_TIMEOUT_MS = 30000;
@@ -8490,6 +8496,32 @@ function noteHelperHealth(health) {
8490
8496
  if (health.frontmost)
8491
8497
  noteSpanDetail("frontmostUnresolvedAgoMs", health.frontmost.unresolvedAgoMs);
8492
8498
  }
8499
+ function coldBridgeWait(error, attempt, spentMs, budgetMs) {
8500
+ if (!bridgeNotReady(error))
8501
+ return;
8502
+ const wait = attempt === 1 ? COLD_BRIDGE_FIRST_WAIT_MS : COLD_BRIDGE_WAIT_MS;
8503
+ return spentMs + wait >= budgetMs ? undefined : wait;
8504
+ }
8505
+ async function retryColdSnapshot(read, ctx) {
8506
+ const started = ctx.now();
8507
+ let waited = 0;
8508
+ for (let attempt = 1;; attempt++) {
8509
+ const result = await read();
8510
+ const spent = Math.max(ctx.now() - started, waited);
8511
+ const wait = result.success ? undefined : coldBridgeWait(result.error, attempt, spent, ctx.budgetMs);
8512
+ if (wait === undefined) {
8513
+ if (attempt > 1)
8514
+ noteSpanDetail("coldBridgeMs", spent);
8515
+ return result;
8516
+ }
8517
+ ctx.log?.(`[ios] snapshot try ${attempt} failed, the bridge is not up yet: retrying`);
8518
+ await ctx.sleep(wait);
8519
+ waited += wait;
8520
+ }
8521
+ }
8522
+ function bridgeNotReady(error) {
8523
+ return error.code === "E_TIMEOUT" || error.code === "E_AX_UNAVAILABLE";
8524
+ }
8493
8525
  function noteRebind(data) {
8494
8526
  noteSpanDetail("rebind", data.bound ? "bound" : data.reason ?? "unbound");
8495
8527
  noteSpanDetail("rebindMs", data.waitedMs);
@@ -8562,6 +8594,7 @@ function createIosDriver(swap, simctl, opts) {
8562
8594
  let settleUnsupported = !nativeSettle;
8563
8595
  let settleAxFailures = 0;
8564
8596
  let rebindUnsupported = false;
8597
+ let bridgeProven = false;
8565
8598
  let gestureUnsupported = false;
8566
8599
  let appOpsOutdated = false;
8567
8600
  const capabilities = {
@@ -8681,7 +8714,7 @@ function createIosDriver(swap, simctl, opts) {
8681
8714
  }
8682
8715
  return { success: true, data: toSnapshot(result.data, now()) };
8683
8716
  };
8684
- async function snapshot(o) {
8717
+ async function snapshotOnce(o) {
8685
8718
  const generation = swap.generation();
8686
8719
  const first = await fetchSnapshot(o);
8687
8720
  if (first.success) {
@@ -8702,6 +8735,19 @@ function createIosDriver(swap, simctl, opts) {
8702
8735
  axFailures = 0;
8703
8736
  return fetchSnapshot(o);
8704
8737
  }
8738
+ async function snapshot(o) {
8739
+ if (bridgeProven)
8740
+ return snapshotOnce(o);
8741
+ const result = await retryColdSnapshot(() => snapshotOnce(o), {
8742
+ now,
8743
+ sleep,
8744
+ budgetMs: opts.coldBridgeBudgetMs ?? COLD_BRIDGE_BUDGET_MS,
8745
+ ...opts.log ? { log: opts.log } : {}
8746
+ });
8747
+ if (result.success)
8748
+ bridgeProven = true;
8749
+ return result;
8750
+ }
8705
8751
  async function typeText(text) {
8706
8752
  const typed = await sendVoid("typeText", { text }, typeTextBudgetMs(text));
8707
8753
  if (typed.success || typed.error.code !== "E_UNSUPPORTED_TEXT")
@@ -8762,6 +8808,8 @@ function createIosDriver(swap, simctl, opts) {
8762
8808
  return false;
8763
8809
  }
8764
8810
  noteRebind(reply.data);
8811
+ if (reply.data.bound)
8812
+ bridgeProven = true;
8765
8813
  return reply.data.bound;
8766
8814
  }
8767
8815
  function noteSettleAxFailure(message) {
@@ -9082,6 +9130,7 @@ async function openIosDriver(o) {
9082
9130
  now,
9083
9131
  sleep,
9084
9132
  log,
9133
+ ...o.coldBridgeBudgetMs !== undefined ? { coldBridgeBudgetMs: o.coldBridgeBudgetMs } : {},
9085
9134
  ...network ? { network } : {},
9086
9135
  a11y: () => iosA11yFor(o, now, sleep),
9087
9136
  nativeAudit: (ctx) => nativeAuditFor(o, exec, log, ctx),
@@ -10485,11 +10534,13 @@ var EXCERPT_LINES = 40;
10485
10534
  var MAX_ALERT_ANSWERS = 3;
10486
10535
  var REACT_THROTTLE_MS = 450;
10487
10536
  var REACT_TIMEOUT_MS = 2000;
10537
+ var FIRST_READER_RETRY_MS = 5000;
10488
10538
 
10489
10539
  class Runtime {
10490
10540
  host;
10491
10541
  reactCache;
10492
10542
  indexed = new WeakMap;
10543
+ firstReaderRetryAt = 0;
10493
10544
  constructor(host) {
10494
10545
  this.host = host;
10495
10546
  }
@@ -10529,19 +10580,30 @@ class Runtime {
10529
10580
  const result = canObserve(driver) ? await driver.observe({ reuse }) : await driver.snapshot();
10530
10581
  if (!result.success)
10531
10582
  return { ok: false, error: result.error };
10583
+ const snapshot = await this.restoreDegraded(driver, result.data);
10532
10584
  const observation = {
10533
10585
  ok: true,
10534
- observed: { driver, snapshot: result.data, index: this.indexOf(result.data) }
10586
+ observed: { driver, snapshot, index: this.indexOf(snapshot) }
10535
10587
  };
10536
10588
  if (before !== undefined)
10537
10589
  observation.earlier = this.earlierThan(driver, before, result.data);
10538
10590
  if (options.alerts === false || answered >= MAX_ALERT_ANSWERS)
10539
10591
  return observation;
10540
- if (!await this.answeredAlert(driver, result.data))
10592
+ if (!await this.answeredAlert(driver, snapshot))
10541
10593
  return observation;
10542
10594
  reuse = false;
10543
10595
  }
10544
10596
  }
10597
+ async restoreDegraded(driver, snapshot) {
10598
+ const first = snapshot.fellBackFrom;
10599
+ if (first === undefined || this.clock.now() < this.firstReaderRetryAt)
10600
+ return snapshot;
10601
+ const again = await driver.snapshot({ backend: first });
10602
+ if (again.success && again.data.backend === first)
10603
+ return again.data;
10604
+ this.firstReaderRetryAt = this.clock.now() + FIRST_READER_RETRY_MS;
10605
+ return snapshot;
10606
+ }
10545
10607
  earlierThan(driver, before, look) {
10546
10608
  const after = canObserve(driver) ? driver.previous?.() : undefined;
10547
10609
  const same = after !== undefined && after.generation === before.generation && after.snapshot === look;
@@ -18,6 +18,7 @@ type RunSettings = {
18
18
  passes: number;
19
19
  count?: number | undefined;
20
20
  profile?: string | undefined;
21
+ runtime?: string | undefined;
21
22
  devices?: Array<{
22
23
  profile: string;
23
24
  default: boolean;
@@ -141,6 +142,7 @@ export declare const e2eSuiteSchema: z.ZodObject<{
141
142
  retry: z.ZodDefault<z.ZodNumber>;
142
143
  count: z.ZodOptional<z.ZodNumber>;
143
144
  profile: z.ZodOptional<z.ZodString>;
145
+ runtime: z.ZodOptional<z.ZodString>;
144
146
  app: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodObject<{
145
147
  installMode: z.ZodOptional<z.ZodEnum<{
146
148
  clean: "clean";
@@ -333,6 +333,7 @@ export declare const wardenConfigSchema: z.ZodObject<{
333
333
  retry: z.ZodDefault<z.ZodNumber>;
334
334
  count: z.ZodOptional<z.ZodNumber>;
335
335
  profile: z.ZodOptional<z.ZodString>;
336
+ runtime: z.ZodOptional<z.ZodString>;
336
337
  app: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodObject<{
337
338
  installMode: z.ZodOptional<z.ZodEnum<{
338
339
  clean: "clean";
@@ -687,6 +687,16 @@ export interface WardenE2eSuite {
687
687
  count?: number | undefined;
688
688
  /** Device profile (not with `devices`). */
689
689
  profile?: string | undefined;
690
+ /**
691
+ * iOS simulator runtime the run's devices boot on, and whose golden they clone: `latest`, `iOS-26-5` or `26.5`.
692
+ * Pin it so the run (locally and on CI) sees one accessibility tree: values change between iOS releases.
693
+ * A runtime that is not installed fails the run before any device is claimed, listing the installed ones; it never
694
+ * falls back to another. Applies to every `devices` leg. Skipped on Android. `--runtime` overrides it.
695
+ * `warden golden key --profile <profile> --runtime <runtime>` names the golden archive the run uses.
696
+ *
697
+ * @example "27.0"
698
+ */
699
+ runtime?: string | undefined;
690
700
  /** Ensure the app on every device first. */
691
701
  app?: WardenAppOption | undefined;
692
702
  /** Ports leased for the run (`<from>[:<span>]`), exported as `WARDEN_PORT_<i>` / `WARDEN_PORTS` to serve, setup and every spec process. */
@@ -134,6 +134,11 @@ export type Snapshot = {
134
134
  flat: Element[];
135
135
  truncated: boolean;
136
136
  modal?: ModalInfo;
137
+ /**
138
+ * Set when `backend` answered because this reader failed or was skipped first (iOS: `axbridge` stalled, `ax` read in
139
+ * its place). The fallback reader can lack what the first one has, so a missing node on such a snapshot is not final.
140
+ */
141
+ fellBackFrom?: SnapshotBackend;
137
142
  };
138
143
  export type Orientation = "portrait" | "portraitUpsideDown" | "landscapeLeft" | "landscapeRight";
139
144
  export type HardwareButton = "home" | "back" | "lock" | "volumeUp" | "volumeDown" | "shake";
@@ -25,7 +25,7 @@ import type { SimdSpawn } from "./simd-transport";
25
25
  * cannot type, `settle` polls snapshot hashes when the helper has no native quiescence, `screenshot` uses
26
26
  * `simctl io` when the helper cannot, and a helper built before the app ops leaves app lifecycle to `simctl`.
27
27
  */
28
- export type IosDriverEnv = Pick<E2eEnv, "simd" | "simdSocket" | "helperDir">;
28
+ export type IosDriverEnv = Pick<E2eEnv, "simd" | "simdSocket" | "helperDir"> & Partial<Pick<E2eEnv, "wardenVersion">>;
29
29
  /** The iOS driver is a `Driver` plus the optional capabilities of `driver/alerts.ts`. */
30
30
  export type IosDriver = Driver & SelectsAll & PastesText & ReadsAlertOn & ProbesFrontmost & ReadsA11y & ReadsA11yEvents & AuditsNatively;
31
31
  export type OpenIosDriverOptions = {
@@ -46,6 +46,8 @@ export type OpenIosDriverOptions = {
46
46
  client?: SimdClient;
47
47
  now?: () => number;
48
48
  sleep?: (ms: number) => Promise<void>;
49
+ /** how long the session's first snapshot waits for an in-sim bridge that is up but silent (default 60 s); 0 tries once */
50
+ coldBridgeBudgetMs?: number;
49
51
  /** for `locateHelper` / `$WARDEN_HOME/simd/<version>`; defaults read the process */
50
52
  version?: string;
51
53
  wardenHome?: string;
@@ -82,6 +84,13 @@ export type OpenIosDriverOptions = {
82
84
  /** test seams for the XCUITest audit's runner */
83
85
  nativeAudit?: Pick<IosNativeAuditOptions, "ensureBuild" | "spawn" | "connect">;
84
86
  };
87
+ /**
88
+ * How long the first snapshot of a session waits for an in-sim bridge that is up but silent. A freshly cloned
89
+ * simulator on a loaded host can take longer than one read's budget (axbridge 4 s + AX fallback 5 s) to answer the
90
+ * first time, and every read after it is fast, so the first one retries while the failure says "not ready yet".
91
+ * Once a read or a rebind has succeeded the normal per-call budgets apply.
92
+ */
93
+ export declare const COLD_BRIDGE_BUDGET_MS = 60000;
85
94
  /** USB HID keyboard-page usage ids for the keys `Driver.key` offers. */
86
95
  export declare const KEY_USAGE: Readonly<Record<KeyName, number>>;
87
96
  /** The fields of a failed snapshot's `detail` the helper sets (which reader, why, both readers' errors), on its span. */
@@ -93,6 +102,8 @@ export declare function createIosDriver(swap: SwappableClient, simctl: Simctl, o
93
102
  now: () => number;
94
103
  sleep: (ms: number) => Promise<void>;
95
104
  log?: Logger;
105
+ /** how long the first snapshot waits for a bridge that is not up yet (default {@link COLD_BRIDGE_BUDGET_MS}) */
106
+ coldBridgeBudgetMs?: number;
96
107
  /** told about every successful `clock` op */
97
108
  clockRecorder?: ClockRecorder;
98
109
  /** device settings (`ios-settings.ts`); absent: those ops answer `E_UNSUPPORTED` */
@@ -1,5 +1,5 @@
1
1
  import type { ErrorResult } from "../../types/src/result";
2
- export type E2eErrorCode = "LOCATOR_NOT_FOUND" | "LOCATOR_AMBIGUOUS" | "INVALID_LOCATOR" | "ACTION_FAILED" | "TIMEOUT" | "NOT_ACTIONABLE" | "INVALID_ARGUMENT" | "SESSION" | "DRIVER" | "ASSERTION" | "PERF_BUDGET";
2
+ export type E2eErrorCode = "LOCATOR_NOT_FOUND" | "LOCATOR_AMBIGUOUS" | "INVALID_LOCATOR" | "ACTION_FAILED" | "TIMEOUT" | "NOT_ACTIONABLE" | "INVALID_ARGUMENT" | "SESSION" | "DRIVER" | "ASSERTION" | "PERF_BUDGET" | "RUN_FAILED";
3
3
  /** Plain shape of a spec error: what `e2eErr()` accepts and what a `Result` carries. */
4
4
  export type E2eErrorShape = {
5
5
  code: E2eErrorCode;
@@ -10,6 +10,8 @@ export { expect } from "./expect/expect";
10
10
  export type { PerfBreach, PerfBudgets } from "./perf/budgets";
11
11
  export type { DevicePerf } from "./perf/device-perf";
12
12
  export type { PerfMetrics, PerfReport, PerfStartOptions } from "./perf/perf.types";
13
+ export type { Run, RunCommand, RunDuration, RunOptions, RunResult } from "./run";
14
+ export { run } from "./run";
13
15
  export type { App, Device, Screen, SystemAlerts, TestFixtures } from "./runner/fixtures.types";
14
16
  export { afterAll, afterEach, beforeAll, beforeEach, describe, test } from "./runner/test";
15
17
  export type { Locator, LocatorReading, PinchOptions, RotateOptions, TapOptions, TapUntil } from "./screen/locator";
@@ -0,0 +1,48 @@
1
+ import type { SpanRunner } from "./runner/span";
2
+ /** Milliseconds, or a duration string (`500ms`, `30s`, `2m`). */
3
+ export type RunDuration = string | number;
4
+ export type RunOptions = {
5
+ /** working directory; default the spec process's (the project root under `warden e2e`) */
6
+ cwd?: string;
7
+ /** merged over the spec's environment; `undefined` removes a variable */
8
+ env?: Record<string, string | undefined>;
9
+ /** kill the command after this long and throw `TIMEOUT`; default `60s` */
10
+ timeout?: RunDuration;
11
+ /** written to the command's stdin */
12
+ input?: string;
13
+ /** throw `RUN_FAILED` on a non-zero exit; default true. `false` returns the result with `ok: false` */
14
+ check?: boolean;
15
+ };
16
+ export type RunResult = {
17
+ /** exit code was 0 */
18
+ ok: boolean;
19
+ exitCode: number;
20
+ stdout: string;
21
+ stderr: string;
22
+ /** wall time of the command */
23
+ ms: number;
24
+ /** `stdout` parsed as JSON; throws `INVALID_ARGUMENT` when it is not JSON */
25
+ json(): unknown;
26
+ };
27
+ /**
28
+ * A shell command (`string`, run with `sh -c`) or an argv (`string[]`, no shell: nothing is expanded or split).
29
+ */
30
+ export type RunCommand = string | readonly string[];
31
+ export type Run = (command: RunCommand, options?: RunOptions) => Promise<RunResult>;
32
+ export type RunDeps = {
33
+ /** times the call as a span in the running attempt's trace */
34
+ span: SpanRunner;
35
+ };
36
+ /** `run` with its span runner injected (tests); specs use {@link run}. */
37
+ export declare function createRun(deps: RunDeps): Run;
38
+ /**
39
+ * Runs a command on the host (the machine running the spec, not the device) and returns its exit code and output.
40
+ * Use it to seed a backend, call a script, or reset state before a test. A non-zero exit throws `RUN_FAILED`
41
+ * (`check: false` returns it instead), a command over `timeout` (default 60 s) is killed and throws `TIMEOUT`.
42
+ * Inside a test it is one `run` span in `trace.jsonl` (exit code only, never the command).
43
+ *
44
+ * @example
45
+ * await run("bun scripts/seed.ts");
46
+ * const { id } = (await run(["bun", "scripts/create-user.ts", "--json"])).json() as { id: string };
47
+ */
48
+ export declare const run: Run;
@@ -78,6 +78,8 @@ export declare class Runtime {
78
78
  private reactCache;
79
79
  /** the index of each snapshot, so a snapshot that several reads share is indexed once */
80
80
  private readonly indexed;
81
+ /** the time before which a degraded look is not retried on its first reader, after that reader failed to answer */
82
+ private firstReaderRetryAt;
81
83
  constructor(host: ScreenHost);
82
84
  get clock(): Clock;
83
85
  actionTimeout(): number;
@@ -95,6 +97,14 @@ export declare class Runtime {
95
97
  * `systemAlerts` on, an alert the screen shows is answered and the screen looked at again.
96
98
  */
97
99
  tryObserve(options?: ObserveOptions): Promise<Observation>;
100
+ /**
101
+ * A snapshot the driver answered from a fallback reader (`Snapshot.fellBackFrom`, iOS `ax` after `axbridge` stalled)
102
+ * can lack what the first reader has: React Native testIDs on static text. Such a look is read once more on the
103
+ * first reader, and that read replaces it when it answers. When it does not (the guest is still silent) the
104
+ * fallback snapshot stands and the first reader is left alone for `FIRST_READER_RETRY_MS`, so a dead guest costs a
105
+ * poll one deadline, not one per attempt. Every locator and matcher looks through here, so none needs its own rule.
106
+ */
107
+ private restoreDegraded;
98
108
  /** What came before `look`, given the cache's answer from just before it was taken. */
99
109
  private earlierThan;
100
110
  /** Whether `snapshot` showed an alert that has now been answered. A screen without one costs no extra read. */
@@ -123,5 +123,7 @@ export type AttemptBinding = {
123
123
  export declare function attemptSession(session: Session, binding: () => AttemptBinding): Session;
124
124
  /** The process-wide session (`globalThis[Symbol.for("./session")]`), created on first call. */
125
125
  export declare function getSession(opts?: SessionOptions): Session;
126
+ /** The process-wide session if one exists; never creates one (so a caller outside a test opens no device). */
127
+ export declare function peekSession(): Session | undefined;
126
128
  /** Closes and forgets the process-wide session, if any. */
127
129
  export declare function resetSession(): Promise<void>;