@delacour/warden-e2e 0.10.0 → 0.12.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/preload.mjs CHANGED
@@ -39,6 +39,7 @@ import {
39
39
  isLocator,
40
40
  Locator,
41
41
  scrollUntilVisible,
42
+ realNativeLogDeps,
42
43
  bunExec,
43
44
  execError,
44
45
  driverError,
@@ -104,12 +105,11 @@ import {
104
105
  createDevicePerf,
105
106
  spawnWarden,
106
107
  screenshotPath,
107
- realNativeLogDeps,
108
108
  BUN_TIMEOUT_MARGIN_MS2,
109
109
  onlyWasSeen,
110
110
  WORKSPACE_VERSION,
111
111
  WARDEN_E2E_VERSION2
112
- } from "./chunk-p7618aj5.mjs";
112
+ } from "./chunk-qz94hsb6.mjs";
113
113
 
114
114
  // ../../packages/e2e/src/preload.ts
115
115
  import { afterAll, setDefaultTimeout } from "bun:test";
@@ -306,35 +306,46 @@ function parseAdbDeviceList(stdout) {
306
306
  const entries = [];
307
307
  for (const raw of stdout.split(`
308
308
  `)) {
309
- const line = raw.trim();
310
- const row = /^(\S+)\s+(.+)$/.exec(line);
311
- if (!row?.[1] || !row[2] || line.startsWith("List of devices") || line.startsWith("*"))
312
- continue;
313
- const serial = row[1];
314
- const rest = row[2];
315
- const noPermissions = rest.startsWith("no permissions");
316
- const state = noPermissions ? "no permissions" : rest.split(/\s+/)[0] ?? "";
317
- const emulator = /^emulator-(\d+)$/.exec(serial);
318
- if (emulator?.[1]) {
319
- entries.push({ kind: "emulator", serial, port: Number(emulator[1]), state });
320
- continue;
321
- }
322
- const attributes = noPermissions ? rest.slice(rest.lastIndexOf("]") + 1) : rest.slice(state.length);
323
- const attr = (key) => new RegExp(`(?:^|\\s)${key}:(\\S+)`).exec(attributes)?.[1];
324
- const networked = /^[^:\s]+:\d+$|_adb-tls-/.test(serial);
325
- const model = attr("model");
326
- const product = attr("product");
327
- entries.push({
328
- kind: "other",
329
- serial,
330
- state,
331
- transport: networked ? "wifi" : "usb",
332
- ...model !== undefined ? { model } : {},
333
- ...product !== undefined ? { product } : {}
334
- });
309
+ const entry = parseAdbRow(raw.trim());
310
+ if (entry)
311
+ entries.push(entry);
335
312
  }
336
313
  return entries;
337
314
  }
315
+ function parseAdbRow(line) {
316
+ const row = /^(\S+)\s+(.+)$/.exec(line);
317
+ if (!row?.[1] || !row[2] || line.startsWith("List of devices") || line.startsWith("*"))
318
+ return;
319
+ const serial = row[1];
320
+ const rest = row[2];
321
+ const noPermissions = rest.startsWith("no permissions");
322
+ const state = noPermissions ? "no permissions" : rest.split(/\s+/)[0] ?? "";
323
+ const emulator = /^emulator-(\d+)$/.exec(serial);
324
+ if (emulator?.[1])
325
+ return { kind: "emulator", serial, port: Number(emulator[1]), state };
326
+ const attributes = noPermissions ? rest.slice(rest.lastIndexOf("]") + 1) : rest.slice(state.length);
327
+ const attr = (key) => new RegExp(`(?:^|\\s)${key}:(\\S+)`).exec(attributes)?.[1];
328
+ const networked = /^[^:\s]+:\d+$|_adb-tls-/.test(serial);
329
+ const model = attr("model");
330
+ const product = attr("product");
331
+ return {
332
+ kind: "other",
333
+ serial,
334
+ state,
335
+ transport: networked ? "wifi" : "usb",
336
+ ...model !== undefined ? { model } : {},
337
+ ...product !== undefined ? { product } : {}
338
+ };
339
+ }
340
+ function physicalOf(entry, model, hwSerial) {
341
+ return {
342
+ platform: "android",
343
+ transport: entry.transport,
344
+ adbState: entry.state,
345
+ ...hwSerial !== undefined ? { hwSerial } : {},
346
+ ...model !== undefined ? { model } : {}
347
+ };
348
+ }
338
349
  function parseAdbDevices(stdout) {
339
350
  const emulators = [];
340
351
  for (const entry of parseAdbDeviceList(stdout)) {
@@ -663,14 +674,7 @@ function createAndroidProvider(deps, options = {}) {
663
674
  return device;
664
675
  if (model !== undefined)
665
676
  device.profile = profileSlug(model);
666
- const hwSerial = info?.hwSerial;
667
- device.physical = {
668
- platform: "android",
669
- transport: entry.transport,
670
- adbState: entry.state,
671
- ...hwSerial !== undefined ? { hwSerial } : {},
672
- ...model !== undefined ? { model } : {}
673
- };
677
+ device.physical = physicalOf(entry, model, info?.hwSerial);
674
678
  return device;
675
679
  }
676
680
  async function avdNameOf(serial) {
@@ -824,6 +828,37 @@ function createAndroidProvider(deps, options = {}) {
824
828
  deps.log?.(`${serial}: launch from snapshot ${snapshot} failed (${launched.error}) \u2014 retrying with a cold boot`);
825
829
  return attempt(undefined);
826
830
  }
831
+ async function refuseBootingPhone(id) {
832
+ const listed = await listAdbDevices(deps);
833
+ const state = listed.success ? listed.data.find((e) => e.serial === id)?.state : undefined;
834
+ if (state === "unauthorized")
835
+ return err(`${id} is unauthorized: accept the USB debugging prompt on the phone, then retry`);
836
+ return err(`${id} is a physical device: warden never boots one (state: ${state ?? "not connected"})`);
837
+ }
838
+ async function pollRegistered(id, polls) {
839
+ for (let i = 0;; i++) {
840
+ const emulators = await listEmulators(deps);
841
+ if (!emulators.success)
842
+ return emulators;
843
+ if (emulators.data.some((e) => e.serial === id))
844
+ return ok(true);
845
+ if (i >= polls)
846
+ return ok(false);
847
+ await sleep(REGISTER_POLL_MS);
848
+ }
849
+ }
850
+ async function pollGone(id, killError) {
851
+ const deadline = deps.now() + RELAUNCH_GONE_TIMEOUT_MS;
852
+ for (;; ) {
853
+ const listed = await listEmulators(deps);
854
+ if (listed.success && !listed.data.some((e) => e.serial === id))
855
+ return ok(undefined);
856
+ if (deps.now() >= deadline) {
857
+ return err(`${id} did not shut down within ${RELAUNCH_GONE_TIMEOUT_MS / 1000}s${killError === undefined ? "" : `: ${killError}`}`);
858
+ }
859
+ await sleep(REGISTER_POLL_MS);
860
+ }
861
+ }
827
862
  return {
828
863
  platform: "android",
829
864
  async inventory() {
@@ -878,25 +913,15 @@ function createAndroidProvider(deps, options = {}) {
878
913
  return ok(device);
879
914
  },
880
915
  async boot(id) {
881
- if (!isEmulatorSerial(id)) {
882
- const listed = await listAdbDevices(deps);
883
- const state = listed.success ? listed.data.find((e) => e.serial === id)?.state : undefined;
884
- if (state === "unauthorized")
885
- return err(`${id} is unauthorized: accept the USB debugging prompt on the phone, then retry`);
886
- return err(`${id} is a physical device: warden never boots one (state: ${state ?? "not connected"})`);
887
- }
916
+ if (!isEmulatorSerial(id))
917
+ return refuseBootingPhone(id);
888
918
  const launched = deps.store.listDevices("android").some((d) => d.id === id);
889
919
  const polls = launched ? Math.ceil(REGISTER_TIMEOUT_MS / REGISTER_POLL_MS) : 0;
890
- for (let i = 0;; i++) {
891
- const emulators = await listEmulators(deps);
892
- if (!emulators.success)
893
- return emulators;
894
- if (emulators.data.some((e) => e.serial === id))
895
- return ok(undefined);
896
- if (i >= polls)
897
- break;
898
- await sleep(REGISTER_POLL_MS);
899
- }
920
+ const registered = await pollRegistered(id, polls);
921
+ if (!registered.success)
922
+ return registered;
923
+ if (registered.data)
924
+ return ok(undefined);
900
925
  return launched ? err(`${id} did not register with adb within ${REGISTER_TIMEOUT_MS / 1000}s of its launch`) : err(`${id} is not running \u2014 android devices are created, not booted`);
901
926
  },
902
927
  async waitReady(id, timeoutMs) {
@@ -936,16 +961,9 @@ function createAndroidProvider(deps, options = {}) {
936
961
  if (!resolved.success)
937
962
  return resolved;
938
963
  const killed = await run(deps, adb(id, "emu", "kill"), { timeoutMs: EMU_CONSOLE_TIMEOUT_MS });
939
- const deadline = deps.now() + RELAUNCH_GONE_TIMEOUT_MS;
940
- for (;; ) {
941
- const listed = await listEmulators(deps);
942
- if (listed.success && !listed.data.some((e) => e.serial === id))
943
- break;
944
- if (deps.now() >= deadline) {
945
- return err(`${id} did not shut down within ${RELAUNCH_GONE_TIMEOUT_MS / 1000}s${killed.success ? "" : `: ${killed.error}`}`);
946
- }
947
- await sleep(REGISTER_POLL_MS);
948
- }
964
+ const gone = await pollGone(id, killed.success ? undefined : killed.error);
965
+ if (!gone.success)
966
+ return gone;
949
967
  await sleep(RELAUNCH_PORT_SETTLE_MS);
950
968
  const launched = await launchOrColdBoot(resolved.data.avdName, port, id, undefined, true);
951
969
  if (!launched.success)
@@ -2107,6 +2125,26 @@ async function preparePhone(session, clock, facts, log) {
2107
2125
  return { success: true, data: undefined };
2108
2126
  }
2109
2127
  var SETTING_VALUE = /^(null|\d)$/;
2128
+ function restoreCommands(rotation, uimode) {
2129
+ const commands = [];
2130
+ if (rotation) {
2131
+ for (const key of ["accelerometer_rotation", "user_rotation"]) {
2132
+ commands.push(rotation[key] === "null" ? ["settings", "delete", "system", key] : ["settings", "put", "system", key, rotation[key]]);
2133
+ }
2134
+ }
2135
+ if (uimode)
2136
+ commands.push(["cmd", "uimode", "night", uimode]);
2137
+ return commands;
2138
+ }
2139
+ async function runAll(session, commands) {
2140
+ let failed = false;
2141
+ for (const argv of commands) {
2142
+ const res = await session.run(shellCommand(argv));
2143
+ if (!res.success || res.data.exitCode !== 0)
2144
+ failed = true;
2145
+ }
2146
+ return failed;
2147
+ }
2110
2148
  async function createSettingsGuard(deps) {
2111
2149
  const { session, serial, sdk, state, log } = deps;
2112
2150
  let rotation = deps.pending.rotation;
@@ -2142,22 +2180,10 @@ async function createSettingsGuard(deps) {
2142
2180
  return { success: true, data: undefined };
2143
2181
  },
2144
2182
  async restore() {
2145
- const commands = [];
2146
- if (rotation) {
2147
- for (const key of ["accelerometer_rotation", "user_rotation"]) {
2148
- commands.push(rotation[key] === "null" ? ["settings", "delete", "system", key] : ["settings", "put", "system", key, rotation[key]]);
2149
- }
2150
- }
2151
- if (uimode)
2152
- commands.push(["cmd", "uimode", "night", uimode]);
2183
+ const commands = restoreCommands(rotation, uimode);
2153
2184
  if (commands.length === 0)
2154
2185
  return;
2155
- let failed = false;
2156
- for (const argv of commands) {
2157
- const res = await session.run(shellCommand(argv));
2158
- if (!res.success || res.data.exitCode !== 0)
2159
- failed = true;
2160
- }
2186
+ const failed = await runAll(session, commands);
2161
2187
  if (failed) {
2162
2188
  log?.("could not restore the rotation / night mode; the next driver on this device will");
2163
2189
  return;
@@ -2487,63 +2513,50 @@ function createAndroidCamera(deps) {
2487
2513
  const staged = [];
2488
2514
  const mode = {};
2489
2515
  for (const facing of CAMERA_FACINGS) {
2490
- const feed = feeds[facing];
2491
- const png = join5(dir, `${facing}.png`);
2492
- const tmpPng = join5(dir, `.${facing}.png.tmp-${process.pid}`);
2493
- if (feed === undefined || feed.kind === "video") {
2494
- staged.push({ tmp: tmpPng, final: png });
2495
- writeFileSync3(tmpPng, PLACEHOLDER_PNG);
2496
- }
2497
- if (feed === undefined)
2498
- continue;
2499
- const sniffed = sniffCameraMedia(feed.path);
2500
- if (!sniffed.success)
2501
- return driverErr("E_BAD_REQUEST", sniffed.error.message);
2502
- if (sniffed.data.kind !== feed.kind) {
2503
- return driverErr("E_BAD_REQUEST", `${feed.path} is ${sniffed.data.kind} media, not a ${feed.kind} feed`);
2504
- }
2505
- if (feed.kind === "video") {
2506
- const name = `${facing}${extname(feed.path).toLowerCase() || ".mp4"}`;
2507
- const tmp = join5(dir, `.${name}.tmp-${process.pid}`);
2508
- copyFileSync(feed.path, tmp);
2509
- staged.push({ tmp, final: join5(dir, name) });
2510
- mode[facing] = name;
2511
- continue;
2512
- }
2513
- staged.push({ tmp: tmpPng, final: png });
2514
- if (sniffed.data.ext === "png") {
2515
- copyFileSync(feed.path, tmpPng);
2516
- continue;
2517
- }
2518
- const converted = await reencode(feed.path, tmpPng);
2519
- if (!converted.success)
2520
- return converted;
2516
+ const failed = await prepareFacing(facing, feeds[facing], staged, mode);
2517
+ if (failed)
2518
+ return failed;
2521
2519
  }
2522
2520
  return { success: true, data: { staged, mode } };
2523
2521
  }
2522
+ async function prepareFacing(facing, feed, staged, mode) {
2523
+ const png = join5(dir, `${facing}.png`);
2524
+ const tmpPng = join5(dir, `.${facing}.png.tmp-${process.pid}`);
2525
+ if (feed === undefined || feed.kind === "video") {
2526
+ staged.push({ tmp: tmpPng, final: png });
2527
+ writeFileSync3(tmpPng, PLACEHOLDER_PNG);
2528
+ }
2529
+ if (feed === undefined)
2530
+ return;
2531
+ const sniffed = sniffCameraMedia(feed.path);
2532
+ if (!sniffed.success)
2533
+ return driverErr("E_BAD_REQUEST", sniffed.error.message);
2534
+ if (sniffed.data.kind !== feed.kind) {
2535
+ return driverErr("E_BAD_REQUEST", `${feed.path} is ${sniffed.data.kind} media, not a ${feed.kind} feed`);
2536
+ }
2537
+ if (feed.kind === "video")
2538
+ return stageVideo(facing, feed.path, staged, mode);
2539
+ staged.push({ tmp: tmpPng, final: png });
2540
+ if (sniffed.data.ext === "png") {
2541
+ copyFileSync(feed.path, tmpPng);
2542
+ return;
2543
+ }
2544
+ const converted = await reencode(feed.path, tmpPng);
2545
+ return converted.success ? undefined : converted;
2546
+ }
2547
+ function stageVideo(facing, path, staged, mode) {
2548
+ const name = `${facing}${extname(path).toLowerCase() || ".mp4"}`;
2549
+ const tmp = join5(dir, `.${name}.tmp-${process.pid}`);
2550
+ copyFileSync(path, tmp);
2551
+ staged.push({ tmp, final: join5(dir, name) });
2552
+ mode[facing] = name;
2553
+ return;
2554
+ }
2524
2555
  function discard(staged) {
2525
2556
  for (const { tmp } of staged)
2526
2557
  rmSync3(tmp, { force: true });
2527
2558
  }
2528
- return async (op) => {
2529
- const profile = deps.profile();
2530
- if (profile === undefined || !splitCameraProfile(profile).camera) {
2531
- return driverErr("E_CAMERA_DEVICE", `${deps.serial} is not a camera emulator: its camera source is fixed when it launches, so claim it with --camera (profile \`camera: true\`)`);
2532
- }
2533
- const feeds = op ?? {};
2534
- const before = readCameraMode(dir);
2535
- let prepared;
2536
- try {
2537
- mkdirSync4(dir, { recursive: true });
2538
- prepared = await prepare(feeds);
2539
- } catch (error) {
2540
- return driverErr("E_IO", `camera feed: ${message(error)}`);
2541
- }
2542
- if (!prepared.success) {
2543
- rmTemps(dir);
2544
- return prepared;
2545
- }
2546
- const { staged, mode } = prepared.data;
2559
+ function commit(before, staged, mode) {
2547
2560
  try {
2548
2561
  for (const { tmp, final } of staged)
2549
2562
  renameSync3(tmp, final);
@@ -2553,19 +2566,48 @@ function createAndroidCamera(deps) {
2553
2566
  rmSync3(join5(dir, old), { force: true });
2554
2567
  }
2555
2568
  writeCameraMode(dir, mode);
2569
+ return;
2556
2570
  } catch (error) {
2557
2571
  discard(staged);
2558
2572
  return driverErr("E_IO", `camera feed: ${message(error)}`);
2559
2573
  }
2574
+ }
2575
+ async function prepareGuarded(feeds) {
2576
+ try {
2577
+ mkdirSync4(dir, { recursive: true });
2578
+ return await prepare(feeds);
2579
+ } catch (error) {
2580
+ return driverErr("E_IO", `camera feed: ${message(error)}`);
2581
+ }
2582
+ }
2583
+ async function relaunchForMode(before, mode) {
2584
+ const restarted = await deps.relaunch();
2585
+ if (restarted.success)
2586
+ return;
2587
+ return driverErr("E_NOT_BOOTED", `restarting ${deps.serial} to switch its camera between ${usesVideo(before) ? "video" : "image"} and ${usesVideo(mode) ? "video" : "image"} failed: ${restarted.error}`);
2588
+ }
2589
+ return async (op) => {
2590
+ const profile = deps.profile();
2591
+ if (profile === undefined || !splitCameraProfile(profile).camera) {
2592
+ return driverErr("E_CAMERA_DEVICE", `${deps.serial} is not a camera emulator: its camera source is fixed when it launches, so claim it with --camera (profile \`camera: true\`)`);
2593
+ }
2594
+ const feeds = op ?? {};
2595
+ const before = readCameraMode(dir);
2596
+ const prepared = await prepareGuarded(feeds);
2597
+ if (!prepared.success) {
2598
+ rmTemps(dir);
2599
+ return prepared;
2600
+ }
2601
+ const { staged, mode } = prepared.data;
2602
+ const committed = commit(before, staged, mode);
2603
+ if (committed)
2604
+ return committed;
2560
2605
  deps.record?.(feeds);
2561
2606
  const state = { feeds };
2562
2607
  if (sameCameraMode(before, mode))
2563
2608
  return { success: true, data: { ...state, appliesOnCameraOpen: true, rebooted: false } };
2564
- const restarted = await deps.relaunch();
2565
- if (!restarted.success) {
2566
- return driverErr("E_NOT_BOOTED", `restarting ${deps.serial} to switch its camera between ${usesVideo(before) ? "video" : "image"} and ${usesVideo(mode) ? "video" : "image"} failed: ${restarted.error}`);
2567
- }
2568
- return { success: true, data: { ...state, rebooted: true } };
2609
+ const restarted = await relaunchForMode(before, mode);
2610
+ return restarted ?? { success: true, data: { ...state, rebooted: true } };
2569
2611
  };
2570
2612
  }
2571
2613
  function rmTemps(dir) {
@@ -3911,67 +3953,84 @@ var SWITCHES = [
3911
3953
  ["highContrastText", "secure", "high_text_contrast_enabled"],
3912
3954
  ["colorInversion", "secure", "accessibility_display_inversion_enabled"]
3913
3955
  ];
3956
+ var stepFontScale = async (c, op, run) => {
3957
+ if (op !== null && op.textSize === undefined)
3958
+ return;
3959
+ const font = fontScaleOf(op?.textSize);
3960
+ if ("success" in font)
3961
+ return font;
3962
+ run.notes = font.notes;
3963
+ const done = await c.putSetting("system", "font_scale", String(font.scale));
3964
+ if (!done.success)
3965
+ return done;
3966
+ run.applied.push("textSize");
3967
+ return;
3968
+ };
3969
+ var stepBoldText = async (c, op, run) => {
3970
+ if (op !== null && op.boldText === undefined)
3971
+ return;
3972
+ const done = await c.putSetting("secure", "font_weight_adjustment", String(op?.boldText === true ? BOLD_TEXT_ADJUSTMENT : 0));
3973
+ if (!done.success)
3974
+ return done;
3975
+ run.applied.push("boldText");
3976
+ return;
3977
+ };
3978
+ var stepSwitches = async (c, op, run) => {
3979
+ for (const [field, table, key] of SWITCHES) {
3980
+ if (op !== null && op[field] === undefined)
3981
+ continue;
3982
+ const done = await c.putSetting(table, key, op?.[field] === true ? "1" : "0");
3983
+ if (!done.success)
3984
+ return done;
3985
+ if (op !== null)
3986
+ run.applied.push(field);
3987
+ }
3988
+ return;
3989
+ };
3990
+ var stepReduceMotion = async (c, op, run) => {
3991
+ if (op !== null && op.reduceMotion === undefined)
3992
+ return;
3993
+ const done = await c.putSetting("global", "transition_animation_scale", op?.reduceMotion === true ? "0" : transitionOff(c));
3994
+ if (!done.success)
3995
+ return done;
3996
+ if (op !== null)
3997
+ run.applied.push("reduceMotion");
3998
+ return;
3999
+ };
4000
+ var stepScreenReader = async (c, op, run) => {
4001
+ if (op !== null && op.screenReader === undefined)
4002
+ return;
4003
+ const done = await (op?.screenReader === true ? c.talkback.enable() : c.talkback.disable());
4004
+ if (!done.success)
4005
+ return done;
4006
+ if (op !== null) {
4007
+ run.screenReader = done.data;
4008
+ run.applied.push("screenReader");
4009
+ run.screenReaderPrevious = c.talkback.previous();
4010
+ }
4011
+ return;
4012
+ };
4013
+ var A11Y_STEPS = [stepFontScale, stepBoldText, stepSwitches, stepReduceMotion, stepScreenReader];
3914
4014
  function a11yOp(c) {
3915
4015
  return async (op) => {
3916
- const applied = [];
3917
- const skipped = [];
3918
- let notes = [];
3919
- if (op === null || op.textSize !== undefined) {
3920
- const font = fontScaleOf(op?.textSize);
3921
- if ("success" in font)
3922
- return font;
3923
- notes = font.notes;
3924
- const done = await c.putSetting("system", "font_scale", String(font.scale));
3925
- if (!done.success)
3926
- return done;
3927
- applied.push("textSize");
3928
- }
3929
- if (op === null || op.boldText !== undefined) {
3930
- const done = await c.putSetting("secure", "font_weight_adjustment", String(op?.boldText === true ? BOLD_TEXT_ADJUSTMENT : 0));
3931
- if (!done.success)
3932
- return done;
3933
- applied.push("boldText");
3934
- }
3935
- for (const [field, table, key] of SWITCHES) {
3936
- if (op === null || op[field] !== undefined) {
3937
- const done = await c.putSetting(table, key, op?.[field] === true ? "1" : "0");
3938
- if (!done.success)
3939
- return done;
3940
- if (op !== null)
3941
- applied.push(field);
3942
- }
3943
- }
3944
- if (op === null || op.reduceMotion !== undefined) {
3945
- const done = await c.putSetting("global", "transition_animation_scale", op?.reduceMotion === true ? "0" : transitionOff(c));
3946
- if (!done.success)
3947
- return done;
3948
- if (op !== null)
3949
- applied.push("reduceMotion");
3950
- }
3951
- let screenReader;
3952
- let screenReaderPrevious;
3953
- if (op === null || op.screenReader !== undefined) {
3954
- const done = await (op?.screenReader === true ? c.talkback.enable() : c.talkback.disable());
3955
- if (!done.success)
3956
- return done;
3957
- if (op !== null) {
3958
- screenReader = done.data;
3959
- applied.push("screenReader");
3960
- screenReaderPrevious = c.talkback.previous();
3961
- }
4016
+ const run = { applied: [], notes: [] };
4017
+ for (const step of A11Y_STEPS) {
4018
+ const failed = await step(c, op, run);
4019
+ if (failed)
4020
+ return failed;
3962
4021
  }
3963
- skipped.push(...a11ySkips(op));
3964
- if (applied.length === 0 && skipped.length > 0) {
4022
+ const skipped = a11ySkips(op);
4023
+ if (run.applied.length === 0 && skipped.length > 0) {
3965
4024
  return unsupported(skipped.map((s) => ({ name: s.setting, reason: s.reason })));
3966
4025
  }
3967
4026
  return {
3968
4027
  success: true,
3969
4028
  data: {
3970
- applied,
4029
+ applied: run.applied,
3971
4030
  skipped,
3972
- notes,
3973
- ...screenReader === undefined ? {} : { screenReader },
3974
- ...screenReaderPrevious === undefined ? {} : { screenReaderPrevious }
4031
+ notes: run.notes,
4032
+ ...run.screenReader === undefined ? {} : { screenReader: run.screenReader },
4033
+ ...run.screenReaderPrevious === undefined ? {} : { screenReaderPrevious: run.screenReaderPrevious }
3975
4034
  }
3976
4035
  };
3977
4036
  };
@@ -3982,6 +4041,14 @@ function batteryRequestProblem(op) {
3982
4041
  return bad;
3983
4042
  return batteryArgvs(op).length === 0 ? driverErr("E_BAD_REQUEST", "battery needs a level, charging, or both") : undefined;
3984
4043
  }
4044
+ async function applyBattery(c, op) {
4045
+ for (const cmd of batteryArgvs(op)) {
4046
+ const done = await c.must(cmd);
4047
+ if (!done.success)
4048
+ return done;
4049
+ }
4050
+ return;
4051
+ }
3985
4052
  function batteryOp(c) {
3986
4053
  return async (op) => {
3987
4054
  if (!c.serial.startsWith("emulator-")) {
@@ -3994,11 +4061,9 @@ function batteryOp(c) {
3994
4061
  const bad = batteryRequestProblem(op);
3995
4062
  if (bad)
3996
4063
  return bad;
3997
- for (const cmd of batteryArgvs(op)) {
3998
- const done = await c.must(cmd);
3999
- if (!done.success)
4000
- return done;
4001
- }
4064
+ const failed = await applyBattery(c, op);
4065
+ if (failed)
4066
+ return failed;
4002
4067
  return {
4003
4068
  success: true,
4004
4069
  data: {
@@ -5156,21 +5221,25 @@ function createUiadOps(deps) {
5156
5221
  await backend.failed(error);
5157
5222
  return neverSent(error) && canRetry ? "retry" : "surface";
5158
5223
  }
5224
+ async function attemptCall(op, use, idempotent, surfaceRefusals, canRetry) {
5225
+ const server = await backend.server();
5226
+ if (server === undefined)
5227
+ return { retry: false, outcome: undefined };
5228
+ const res = await use(server);
5229
+ if (res.success)
5230
+ return { retry: false, outcome: res };
5231
+ if (surfaceRefusals && REFUSALS.has(res.error.code))
5232
+ return { retry: false, outcome: res };
5233
+ const verdict = await judge(op, res.error, canRetry);
5234
+ if (verdict === "retry")
5235
+ return { retry: true };
5236
+ return { retry: false, outcome: verdict === "surface" && !idempotent ? res : undefined };
5237
+ }
5159
5238
  async function call(op, use, idempotent, surfaceRefusals = false) {
5160
5239
  for (let attempt = 0;attempt < 2; attempt++) {
5161
- const server = await backend.server();
5162
- if (server === undefined)
5163
- return;
5164
- const res = await use(server);
5165
- if (res.success)
5166
- return res;
5167
- if (surfaceRefusals && REFUSALS.has(res.error.code))
5168
- return res;
5169
- const verdict = await judge(op, res.error, attempt === 0);
5170
- if (verdict === "surface" && !idempotent)
5171
- return res;
5172
- if (verdict !== "retry")
5173
- return;
5240
+ const tried = await attemptCall(op, use, idempotent, surfaceRefusals, attempt === 0);
5241
+ if (!tried.retry)
5242
+ return tried.outcome;
5174
5243
  }
5175
5244
  return;
5176
5245
  }
@@ -5336,6 +5405,27 @@ function elementAt(flat, point) {
5336
5405
  const deepest = (list) => list.reduce((best, e) => best === undefined || e.depth >= best.depth ? e : best, undefined);
5337
5406
  return deepest(inside.filter((e) => e.hittable)) ?? deepest(inside) ?? null;
5338
5407
  }
5408
+ async function bootAndProbe(options, session, clock) {
5409
+ const { serial, log } = options;
5410
+ const booted = await waitForBoot(session, serial, clock, {
5411
+ ...options.bootTimeoutMs !== undefined ? { timeoutMs: options.bootTimeoutMs } : {},
5412
+ ...log ? { log } : {}
5413
+ });
5414
+ if (!booted.success)
5415
+ return booted;
5416
+ const sdk = booted.data.sdk;
5417
+ const phone = booted.data.physical;
5418
+ if (phone) {
5419
+ const awake = await preparePhone(session, clock, phone, log);
5420
+ if (!awake.success)
5421
+ return awake;
5422
+ }
5423
+ const probed = options.screen ?? booted.data.screen;
5424
+ const screen = probed ? { success: true, data: probed } : await readScreen(session, serial);
5425
+ if (!screen.success)
5426
+ return screen;
5427
+ return { success: true, data: { sdk, phone, screen: screen.data } };
5428
+ }
5339
5429
  async function openAndroidDriver(options) {
5340
5430
  const { serial, adb, log } = options;
5341
5431
  const clock = options.clock ?? realClock;
@@ -5351,23 +5441,10 @@ async function openAndroidDriver(options) {
5351
5441
  return { success: false, error };
5352
5442
  };
5353
5443
  const savedState = options.state?.read(serial);
5354
- const booted = await waitForBoot(session, serial, clock, {
5355
- ...options.bootTimeoutMs !== undefined ? { timeoutMs: options.bootTimeoutMs } : {},
5356
- ...log ? { log } : {}
5357
- });
5358
- if (!booted.success)
5359
- return failed(booted.error);
5360
- const sdk = booted.data.sdk;
5361
- const phone = booted.data.physical;
5362
- if (phone) {
5363
- const awake = await preparePhone(session, clock, phone, log);
5364
- if (!awake.success)
5365
- return failed(awake.error);
5366
- }
5367
- const probed = options.screen ?? booted.data.screen;
5368
- const screen = probed ? { success: true, data: probed } : await readScreen(session, serial);
5369
- if (!screen.success)
5370
- return failed(screen.error);
5444
+ const probed = await bootAndProbe(options, session, clock);
5445
+ if (!probed.success)
5446
+ return failed(probed.error);
5447
+ const { sdk, phone, screen } = probed.data;
5371
5448
  const saved = forSdk(await savedState ?? {}, sdk);
5372
5449
  const animations = await disableAnimations({
5373
5450
  session,
@@ -5387,7 +5464,7 @@ async function openAndroidDriver(options) {
5387
5464
  }) : undefined;
5388
5465
  return {
5389
5466
  success: true,
5390
- data: buildDriver(options, session, screen.data, clock, execBinary, {
5467
+ data: buildDriver(options, session, screen, clock, execBinary, {
5391
5468
  sdk,
5392
5469
  savedRoute: saved.dumpRoute,
5393
5470
  savedUiadUnsupported: saved.uiadUnsupported,
@@ -5425,12 +5502,72 @@ function openUiad(input) {
5425
5502
  atf: () => ensureAtfJar({ session, adb, serial, ...log ? { log } : {} })
5426
5503
  });
5427
5504
  }
5428
- function buildDriver(options, session, baseScreen, clock, execBinary, facts) {
5505
+ async function dumpWithRetry(dumper, clock, log) {
5506
+ let dumped = await dumper.dump();
5507
+ for (let attempt = 1;attempt < SNAPSHOT_ATTEMPTS2 && !dumped.success && dumped.error.code === "E_AX_UNAVAILABLE"; attempt++) {
5508
+ log?.(`uiautomator unavailable, retrying (${attempt}/${SNAPSHOT_ATTEMPTS2 - 1})`);
5509
+ await clock.sleep(SNAPSHOT_RETRY_MS2);
5510
+ dumped = await dumper.dump();
5511
+ }
5512
+ return dumped;
5513
+ }
5514
+ function parseDump(xml, baseScreen, clock) {
5515
+ const rotation = hierarchyRotation(xml);
5516
+ const screen = rotatedScreen(baseScreen, rotation);
5517
+ let parsed;
5518
+ try {
5519
+ parsed = parseUiautomatorXml(xml, screen);
5520
+ } catch (error) {
5521
+ if (error instanceof UiautomatorParseError)
5522
+ return driverErr("E_AX_UNAVAILABLE", error.message);
5523
+ throw error;
5524
+ }
5525
+ return {
5526
+ success: true,
5527
+ data: {
5528
+ platform: "android",
5529
+ backend: "uiautomator",
5530
+ takenAt: clock.now(),
5531
+ screen,
5532
+ ...parsed,
5533
+ truncated: false
5534
+ }
5535
+ };
5536
+ }
5537
+ async function touchSensor(exec, adb, serial, fingerId) {
5538
+ const touched = await exec(emuFingerTouchArgv(adb, serial, fingerId), { timeoutMs: 1e4 });
5539
+ if (touched.exitCode === 0 && !/^KO\b/m.test(touched.stdout))
5540
+ return { success: true, data: undefined };
5541
+ return driverErr("E_IO", `adb emu finger touch failed: ${`${touched.stdout}${touched.stderr}`.trim().slice(0, 200)}`);
5542
+ }
5543
+ function biometricsOp(session, exec, adb, serial) {
5544
+ return async (op) => {
5545
+ if (!isEmulatorSerial2(serial)) {
5546
+ return driverErr("E_UNSUPPORTED", `biometrics need an emulator, not the physical device ${serial}`);
5547
+ }
5548
+ if (op === "enroll" || op === "unenroll") {
5549
+ return driverErr("E_UNSUPPORTED", `Android fingerprint enrollment comes from the golden snapshot (profile \`biometrics: true\`); \`${op}\` is not available at run time`);
5550
+ }
5551
+ if (op !== "status") {
5552
+ const touched = await touchSensor(exec, adb, serial, op === "match" ? 1 : UNENROLLED_FINGER_ID);
5553
+ if (!touched.success)
5554
+ return touched;
5555
+ }
5556
+ const dump = await runChecked(session, shellCommand(dumpsysFingerprintArgv()), "E_UNSUPPORTED");
5557
+ if (!dump.success)
5558
+ return dump;
5559
+ const state = { enrolled: parseFingerprintEnrolled(dump.data), modality: "fingerprint" };
5560
+ return { success: true, data: state };
5561
+ };
5562
+ }
5563
+ function capabilitiesOf(serverOn, shellPointers) {
5564
+ return serverOn ? { ...CAPABILITIES, pinch: true, maxPointers: 2, unicodeText: true, nativeSettle: true } : { ...CAPABILITIES, maxPointers: shellPointers };
5565
+ }
5566
+ function openDumper(options, session, facts, saving) {
5429
5567
  const { serial, adb, exec, log } = options;
5430
5568
  const { sdk } = facts;
5431
5569
  const initialRoute = options.dumpRoute ?? facts.savedRoute;
5432
- const saving = [];
5433
- const dumper = createDumper({
5570
+ return createDumper({
5434
5571
  session,
5435
5572
  exec,
5436
5573
  adb,
@@ -5444,40 +5581,19 @@ function buildDriver(options, session, baseScreen, clock, execBinary, facts) {
5444
5581
  saving.push(options.state.update(serial, patchFor(sdk, { dumpRoute: route })));
5445
5582
  }
5446
5583
  });
5584
+ }
5585
+ function buildDriver(options, session, baseScreen, clock, execBinary, facts) {
5586
+ const { serial, adb, exec, log } = options;
5587
+ const { sdk } = facts;
5588
+ const saving = [];
5589
+ const dumper = openDumper(options, session, facts, saving);
5447
5590
  const uiad = openUiad({ options, session, baseScreen, clock, facts, saving });
5448
- async function dumpWithRetry() {
5449
- let dumped = await dumper.dump();
5450
- for (let attempt = 1;attempt < SNAPSHOT_ATTEMPTS2 && !dumped.success && dumped.error.code === "E_AX_UNAVAILABLE"; attempt++) {
5451
- log?.(`uiautomator unavailable, retrying (${attempt}/${SNAPSHOT_ATTEMPTS2 - 1})`);
5452
- await clock.sleep(SNAPSHOT_RETRY_MS2);
5453
- dumped = await dumper.dump();
5454
- }
5455
- return dumped;
5456
- }
5457
5591
  async function dumpSnapshot() {
5458
5592
  noteSpanDetail("backend", "dump");
5459
- const dumped = await dumpWithRetry();
5593
+ const dumped = await dumpWithRetry(dumper, clock, log);
5460
5594
  if (!dumped.success)
5461
5595
  return dumped;
5462
- let parsed;
5463
- const rotation = hierarchyRotation(dumped.data.xml);
5464
- const screen = rotatedScreen(baseScreen, rotation);
5465
- try {
5466
- parsed = parseUiautomatorXml(dumped.data.xml, screen);
5467
- } catch (error) {
5468
- if (error instanceof UiautomatorParseError)
5469
- return driverErr("E_AX_UNAVAILABLE", error.message);
5470
- throw error;
5471
- }
5472
- const taken = {
5473
- platform: "android",
5474
- backend: "uiautomator",
5475
- takenAt: clock.now(),
5476
- screen,
5477
- ...parsed,
5478
- truncated: false
5479
- };
5480
- return { success: true, data: taken };
5596
+ return parseDump(dumped.data.xml, baseScreen, clock);
5481
5597
  }
5482
5598
  async function takeSnapshot() {
5483
5599
  return await uiad?.snapshot() ?? dumpSnapshot();
@@ -5613,18 +5729,20 @@ function buildDriver(options, session, baseScreen, clock, execBinary, facts) {
5613
5729
  }
5614
5730
  return { success: true, data: { path: opts.path } };
5615
5731
  };
5616
- const orientation = async (set) => {
5617
- if (set) {
5618
- const saved = await facts.settings?.beforeRotate();
5619
- if (saved && !saved.success)
5620
- return saved;
5621
- for (const argv of rotationArgvs(orientationToRotation(set))) {
5622
- const res = await input(shellCommand(argv));
5623
- if (!res.success)
5624
- return res;
5625
- }
5626
- return { success: true, data: set };
5732
+ const setOrientation = async (set) => {
5733
+ const saved = await facts.settings?.beforeRotate();
5734
+ if (saved && !saved.success)
5735
+ return saved;
5736
+ for (const argv of rotationArgvs(orientationToRotation(set))) {
5737
+ const res = await input(shellCommand(argv));
5738
+ if (!res.success)
5739
+ return res;
5627
5740
  }
5741
+ return { success: true, data: set };
5742
+ };
5743
+ const orientation = async (set) => {
5744
+ if (set)
5745
+ return setOrientation(set);
5628
5746
  const res = await session.run(shellCommand(userRotationArgv()));
5629
5747
  if (!res.success)
5630
5748
  return res;
@@ -5774,30 +5892,7 @@ function buildDriver(options, session, baseScreen, clock, execBinary, facts) {
5774
5892
  });
5775
5893
  return unit;
5776
5894
  };
5777
- const touchSensor = async (fingerId) => {
5778
- const touched = await options.exec(emuFingerTouchArgv(adb, serial, fingerId), { timeoutMs: 1e4 });
5779
- if (touched.exitCode === 0 && !/^KO\b/m.test(touched.stdout))
5780
- return { success: true, data: undefined };
5781
- return driverErr("E_IO", `adb emu finger touch failed: ${`${touched.stdout}${touched.stderr}`.trim().slice(0, 200)}`);
5782
- };
5783
- const biometrics = async (op) => {
5784
- if (!isEmulatorSerial2(serial)) {
5785
- return driverErr("E_UNSUPPORTED", `biometrics need an emulator, not the physical device ${serial}`);
5786
- }
5787
- if (op === "enroll" || op === "unenroll") {
5788
- return driverErr("E_UNSUPPORTED", `Android fingerprint enrollment comes from the golden snapshot (profile \`biometrics: true\`); \`${op}\` is not available at run time`);
5789
- }
5790
- if (op !== "status") {
5791
- const touched = await touchSensor(op === "match" ? 1 : UNENROLLED_FINGER_ID);
5792
- if (!touched.success)
5793
- return touched;
5794
- }
5795
- const dump = await runChecked(session, shellCommand(dumpsysFingerprintArgv()), "E_UNSUPPORTED");
5796
- if (!dump.success)
5797
- return dump;
5798
- const state = { enrolled: parseFingerprintEnrolled(dump.data), modality: "fingerprint" };
5799
- return { success: true, data: state };
5800
- };
5895
+ const biometrics = biometricsOp(session, options.exec, adb, serial);
5801
5896
  const clipboard = async () => driverErr("E_UNSUPPORTED", "Android has no public shell clipboard access; type ASCII with typeText");
5802
5897
  const health = async () => {
5803
5898
  const [release, sdk, model] = await Promise.all(["ro.build.version.release", "ro.build.version.sdk", "ro.product.model"].map((k) => session.run(shellCommand(getpropArgv(k)))));
@@ -5816,7 +5911,7 @@ function buildDriver(options, session, baseScreen, clock, execBinary, facts) {
5816
5911
  platform: "android",
5817
5912
  id: serial,
5818
5913
  get capabilities() {
5819
- return uiad?.enabled() ? { ...CAPABILITIES, pinch: true, maxPointers: 2, unicodeText: true, nativeSettle: true } : { ...CAPABILITIES, maxPointers: maxPointers() };
5914
+ return capabilitiesOf(uiad?.enabled() === true, maxPointers());
5820
5915
  },
5821
5916
  health,
5822
5917
  snapshot,
@@ -5832,8 +5927,10 @@ function buildDriver(options, session, baseScreen, clock, execBinary, facts) {
5832
5927
  key,
5833
5928
  pressKeyRepeat,
5834
5929
  ...uiad !== undefined || sdk !== undefined && sdk >= SELECT_ALL_MIN_SDK ? { selectAll } : {},
5835
- ...uiad !== undefined ? { setText, a11yEvents, ...screenReader } : {},
5836
5930
  ...uiad !== undefined ? {
5931
+ setText,
5932
+ a11yEvents,
5933
+ ...screenReader,
5837
5934
  auditNatively: (opts) => atfAudit({ run: (o) => uiad.audit(o) }, opts?.contrast === undefined ? {} : { contrast: opts.contrast })
5838
5935
  } : {},
5839
5936
  pressButton,
@@ -6382,18 +6479,27 @@ function isRecord4(value) {
6382
6479
  return typeof value === "object" && value !== null && !Array.isArray(value);
6383
6480
  }
6384
6481
  var optionalString = (value) => typeof value === "string" && value !== "" ? value : undefined;
6482
+ function readFrame(frame) {
6483
+ if (!Array.isArray(frame) || frame.length !== 4 || !frame.every((n) => typeof n === "number"))
6484
+ return;
6485
+ return frame;
6486
+ }
6487
+ function actionsValid(actions) {
6488
+ return actions === undefined || Array.isArray(actions) && actions.every((a) => typeof a === "string");
6489
+ }
6490
+ function traitsValid(traits) {
6491
+ return typeof traits === "string" && /^0x[0-9a-f]+$/i.test(traits);
6492
+ }
6385
6493
  function readNode2(value) {
6386
6494
  if (!isRecord4(value) || typeof value.ae !== "boolean" || typeof value.cls !== "string")
6387
6495
  return;
6388
- const { frame, traits, actions } = value;
6389
- if (!Array.isArray(frame) || frame.length !== 4 || !frame.every((n) => typeof n === "number"))
6390
- return;
6391
- if (typeof traits !== "string" || !/^0x[0-9a-f]+$/i.test(traits))
6496
+ const { traits, actions } = value;
6497
+ const box = readFrame(value.frame);
6498
+ if (!box)
6392
6499
  return;
6393
- if (actions !== undefined && (!Array.isArray(actions) || !actions.every((a) => typeof a === "string"))) {
6500
+ if (!traitsValid(traits) || !actionsValid(actions))
6394
6501
  return;
6395
- }
6396
- const [x, y, width, height] = frame;
6502
+ const [x, y, width, height] = box;
6397
6503
  const label = optionalString(value.label);
6398
6504
  const hint = optionalString(value.hint);
6399
6505
  const text = optionalString(value.value);
@@ -7389,6 +7495,79 @@ function withStagedSettings(requested, settings) {
7389
7495
  args: [...requested?.args ?? [], ...args]
7390
7496
  };
7391
7497
  }
7498
+ var NOTHING_APPLIED = { ok: true, applied: [] };
7499
+ async function a11yTextSizeStep(run, udid, op) {
7500
+ const size = op === null ? undefined : op.textSize;
7501
+ if (op !== null && size === undefined)
7502
+ return NOTHING_APPLIED;
7503
+ const resolved = op === null ? undefined : resolveTextSize(size);
7504
+ if (resolved && !resolved.success)
7505
+ return { ok: false, failure: driverErr("E_BAD_REQUEST", resolved.error) };
7506
+ const done = await run(contentSizeArgv(udid, resolved ? resolved.data.category : DEFAULT_CONTENT_SIZE));
7507
+ return done.success ? { ok: true, applied: ["textSize"] } : { ok: false, failure: done };
7508
+ }
7509
+ async function a11yContrastStep(run, udid, op) {
7510
+ if (op !== null && op.contrast === undefined)
7511
+ return NOTHING_APPLIED;
7512
+ const done = await run(contrastArgv(udid, op?.contrast === "increased"));
7513
+ return done.success ? { ok: true, applied: ["contrast"] } : { ok: false, failure: done };
7514
+ }
7515
+ async function a11yDefaultsStep(run, udid, op) {
7516
+ if (op === null) {
7517
+ for (const argv of iosDefaultsDeleteArgvs(udid, IOS_DEFAULTS_FIELDS)) {
7518
+ const done = await run(argv, false, iosDefaultsMissing);
7519
+ if (!done.success)
7520
+ return { ok: false, failure: done };
7521
+ }
7522
+ return NOTHING_APPLIED;
7523
+ }
7524
+ const entries = IOS_DEFAULTS_FIELDS.flatMap((field) => {
7525
+ const on = op[field];
7526
+ return on === undefined ? [] : [[field, on]];
7527
+ });
7528
+ for (const argv of iosDefaultsWriteArgvs(udid, entries)) {
7529
+ const done = await run(argv);
7530
+ if (!done.success)
7531
+ return { ok: false, failure: done };
7532
+ }
7533
+ return { ok: true, applied: entries.map(([field]) => field) };
7534
+ }
7535
+ function a11ySkipped(op) {
7536
+ const skipped = [];
7537
+ for (const setting of ["highContrastText", "colorInversion"]) {
7538
+ if (op?.[setting] !== undefined)
7539
+ skipped.push({ setting, reason: "Android only" });
7540
+ }
7541
+ return skipped;
7542
+ }
7543
+ function a11yScreenReader(op) {
7544
+ if (op?.screenReader === undefined)
7545
+ return;
7546
+ return op.screenReader ? { mode: "simulated", engine: "voiceover" } : { mode: "off" };
7547
+ }
7548
+ async function batteryClear(run, udid) {
7549
+ const done = await run(statusBarClearArgv(udid));
7550
+ return done.success ? { success: true, data: { cosmetic: true } } : done;
7551
+ }
7552
+ async function batteryOverride(run, udid, op) {
7553
+ const bad = badRequest(batteryProblem(op.level));
7554
+ if (bad)
7555
+ return bad;
7556
+ if (op.level === undefined && op.charging === undefined) {
7557
+ return driverErr("E_BAD_REQUEST", "battery needs a level, charging, or both");
7558
+ }
7559
+ const done = await run(statusBarBatteryArgv(udid, op));
7560
+ if (!done.success)
7561
+ return done;
7562
+ return {
7563
+ success: true,
7564
+ data: {
7565
+ ...op.level === undefined ? {} : { level: op.level },
7566
+ ...op.charging === undefined ? {} : { charging: op.charging },
7567
+ cosmetic: true
7568
+ }
7569
+ };
7570
+ }
7392
7571
  function createIosSettings(deps) {
7393
7572
  const { udid, exec } = deps;
7394
7573
  let memoryStaged = {};
@@ -7467,76 +7646,25 @@ function createIosSettings(deps) {
7467
7646
  },
7468
7647
  async a11y(op) {
7469
7648
  const applied = [];
7470
- const skipped = [];
7471
- const size = op === null ? undefined : op.textSize;
7472
- if (op === null || size !== undefined) {
7473
- const resolved = op === null ? undefined : resolveTextSize(size);
7474
- if (resolved && !resolved.success)
7475
- return driverErr("E_BAD_REQUEST", resolved.error);
7476
- const done = await simctl(contentSizeArgv(udid, resolved ? resolved.data.category : DEFAULT_CONTENT_SIZE));
7477
- if (!done.success)
7478
- return done;
7479
- applied.push("textSize");
7480
- }
7481
- if (op === null || op.contrast !== undefined) {
7482
- const done = await simctl(contrastArgv(udid, op?.contrast === "increased"));
7483
- if (!done.success)
7484
- return done;
7485
- applied.push("contrast");
7486
- }
7487
- if (op === null) {
7488
- for (const argv of iosDefaultsDeleteArgvs(udid, IOS_DEFAULTS_FIELDS)) {
7489
- const done = await simctl(argv, false, iosDefaultsMissing);
7490
- if (!done.success)
7491
- return done;
7492
- }
7493
- } else {
7494
- const entries = IOS_DEFAULTS_FIELDS.flatMap((field) => {
7495
- const on = op[field];
7496
- return on === undefined ? [] : [[field, on]];
7497
- });
7498
- for (const argv of iosDefaultsWriteArgvs(udid, entries)) {
7499
- const done = await simctl(argv);
7500
- if (!done.success)
7501
- return done;
7502
- }
7503
- applied.push(...entries.map(([field]) => field));
7504
- }
7505
- for (const setting of ["highContrastText", "colorInversion"]) {
7506
- if (op?.[setting] !== undefined)
7507
- skipped.push({ setting, reason: "Android only" });
7649
+ const steps = [a11yTextSizeStep, a11yContrastStep, a11yDefaultsStep];
7650
+ for (const step of steps) {
7651
+ const outcome = await step(simctl, udid, op);
7652
+ if (!outcome.ok)
7653
+ return outcome.failure;
7654
+ applied.push(...outcome.applied);
7508
7655
  }
7656
+ const skipped = a11ySkipped(op);
7509
7657
  if (applied.length === 0 && skipped.length > 0) {
7510
7658
  return driverErr("E_UNSUPPORTED", skipped.map((s) => `${s.setting}: ${s.reason}`).join("; "));
7511
7659
  }
7512
- const screenReader = op?.screenReader === undefined ? undefined : op.screenReader ? { mode: "simulated", engine: "voiceover" } : { mode: "off" };
7660
+ const screenReader = a11yScreenReader(op);
7513
7661
  return {
7514
7662
  success: true,
7515
7663
  data: { applied, skipped, notes: [], ...screenReader ? { screenReader } : {} }
7516
7664
  };
7517
7665
  },
7518
7666
  async battery(op) {
7519
- if (op === null) {
7520
- const done = await simctl(statusBarClearArgv(udid));
7521
- return done.success ? { success: true, data: { cosmetic: true } } : done;
7522
- }
7523
- const bad = badRequest(batteryProblem(op.level));
7524
- if (bad)
7525
- return bad;
7526
- if (op.level === undefined && op.charging === undefined) {
7527
- return driverErr("E_BAD_REQUEST", "battery needs a level, charging, or both");
7528
- }
7529
- const done = await simctl(statusBarBatteryArgv(udid, op));
7530
- if (!done.success)
7531
- return done;
7532
- return {
7533
- success: true,
7534
- data: {
7535
- ...op.level === undefined ? {} : { level: op.level },
7536
- ...op.charging === undefined ? {} : { charging: op.charging },
7537
- cosmetic: true
7538
- }
7539
- };
7667
+ return op === null ? batteryClear(simctl, udid) : batteryOverride(simctl, udid, op);
7540
7668
  },
7541
7669
  async memoryWarning() {
7542
7670
  return driverErr("E_UNSUPPORTED", "no way to deliver a memory warning to an iOS app was found: `notifyutil -p com.apple.system.lowmemory` is accepted but a UIKit app never receives UIApplicationDidReceiveMemoryWarning (verified); use Simulator > Debug > Simulate Memory Warning by hand");
@@ -8432,6 +8560,7 @@ function createIosDriver(swap, simctl, opts) {
8432
8560
  const nativeSettle = ready.ax.default === "axbridge" && ready.ax.bridge === "available";
8433
8561
  const bridgeUp = ready.ax.bridge === "available";
8434
8562
  let settleUnsupported = !nativeSettle;
8563
+ let settleAxFailures = 0;
8435
8564
  let rebindUnsupported = false;
8436
8565
  let gestureUnsupported = false;
8437
8566
  let appOpsOutdated = false;
@@ -8608,6 +8737,19 @@ function createIosDriver(swap, simctl, opts) {
8608
8737
  throw error;
8609
8738
  }
8610
8739
  }
8740
+ async function settleAfterLaunch(bundleId) {
8741
+ lastLaunched = bundleId;
8742
+ launchedBundle = bundleId;
8743
+ if (await rebind(bundleId))
8744
+ launchedBundle = undefined;
8745
+ const loaded = await net?.confirmLoaded();
8746
+ if (loaded && !loaded.success)
8747
+ return loaded.error;
8748
+ const a11yLoaded = await a11y?.confirmLoaded();
8749
+ if (a11yLoaded && !a11yLoaded.success)
8750
+ return a11yLoaded.error;
8751
+ return;
8752
+ }
8611
8753
  async function rebind(bundleId) {
8612
8754
  if (rebindUnsupported || settleUnsupported)
8613
8755
  return false;
@@ -8622,10 +8764,20 @@ function createIosDriver(swap, simctl, opts) {
8622
8764
  noteRebind(reply.data);
8623
8765
  return reply.data.bound;
8624
8766
  }
8767
+ function noteSettleAxFailure(message) {
8768
+ settleAxFailures += 1;
8769
+ noteSpanDetail("nativeError", "E_AX_UNAVAILABLE");
8770
+ if (settleAxFailures >= 2) {
8771
+ settleUnsupported = true;
8772
+ opts.log?.(`[ios] native settle could not bind the AX observer twice in a row, polling snapshot hashes: ${message}`);
8773
+ }
8774
+ return;
8775
+ }
8625
8776
  async function settleViaHelper(quietMs, timeoutMs) {
8626
8777
  const expected = launchedBundle;
8627
8778
  const native = await request("settle", { quietMs, settleTimeoutMs: timeoutMs, ...expected === undefined ? {} : { bundleId: expected } }, { timeoutMs: timeoutMs + SETTLE_MARGIN_MS });
8628
8779
  if (native.success) {
8780
+ settleAxFailures = 0;
8629
8781
  if (expected !== undefined && native.data.reason !== "frontmost_mismatch")
8630
8782
  launchedBundle = undefined;
8631
8783
  return { success: true, data: nativeDetail(native.data) };
@@ -8637,6 +8789,8 @@ function createIosDriver(swap, simctl, opts) {
8637
8789
  data: { quiet: false, waitedMs: timeoutMs + SETTLE_MARGIN_MS, reason: "helper_timeout" }
8638
8790
  };
8639
8791
  }
8792
+ if (native.error.code === "E_AX_UNAVAILABLE")
8793
+ return noteSettleAxFailure(native.error.message);
8640
8794
  if (native.error.code !== "E_UNSUPPORTED")
8641
8795
  return native;
8642
8796
  settleUnsupported = true;
@@ -8801,19 +8955,10 @@ function createIosDriver(swap, simctl, opts) {
8801
8955
  const o = withEnv.data;
8802
8956
  const viaHelper = await appOp("launch", params({ bundleId, args: o?.args && [...o.args], env: o?.env, terminateRunning: o?.relaunch !== false }), LAUNCH_TIMEOUT_MS);
8803
8957
  const launched = await appOutcome(viaHelper, () => simctl.launch(bundleId, o), (data) => noteSpanDetail("pid", data.pid));
8804
- if (launched.success) {
8805
- lastLaunched = bundleId;
8806
- launchedBundle = bundleId;
8807
- if (await rebind(bundleId))
8808
- launchedBundle = undefined;
8809
- const loaded = await net?.confirmLoaded();
8810
- if (loaded && !loaded.success)
8811
- return loaded;
8812
- const a11yLoaded = await a11y?.confirmLoaded();
8813
- if (a11yLoaded && !a11yLoaded.success)
8814
- return a11yLoaded;
8815
- }
8816
- return launched;
8958
+ if (!launched.success)
8959
+ return launched;
8960
+ const unconfirmed = await settleAfterLaunch(bundleId);
8961
+ return unconfirmed ? { success: false, error: unconfirmed } : launched;
8817
8962
  },
8818
8963
  terminate: async (bundleId) => {
8819
8964
  const viaHelper = await appOp("terminate", { bundleId }, APP_OP_TIMEOUT_MS);
@@ -8879,6 +9024,33 @@ async function connect(o, log) {
8879
9024
  ...timeoutOption(o)
8880
9025
  });
8881
9026
  }
9027
+ function nativeAuditFor(o, exec, log, ctx) {
9028
+ return createIosNativeAudit({
9029
+ udid: o.udid,
9030
+ exec,
9031
+ bundleId: () => o.bundleId ?? ctx.lastBundle(),
9032
+ ...o.wardenHome ? { wardenHome: o.wardenHome } : {},
9033
+ ...o.version ? { version: o.version } : {},
9034
+ ...o.runnerSources ? { runnerSources: o.runnerSources } : {},
9035
+ ...o.nativeAudit ?? {},
9036
+ ...o.debug ? { log } : {}
9037
+ });
9038
+ }
9039
+ function settingsFor(o, exec, ctx) {
9040
+ return createIosSettings({
9041
+ udid: o.udid,
9042
+ exec,
9043
+ lastBundle: ctx.lastBundle,
9044
+ ...o.bundleId ? { defaultBundle: o.bundleId } : {},
9045
+ staged: o.stagedSettings ?? stagedSettingsFor(o.wardenHome ?? wardenHome(), o.udid)
9046
+ });
9047
+ }
9048
+ function helperPreflight(o) {
9049
+ if (o.client || (o.socketPath ?? o.env?.simdSocket))
9050
+ return;
9051
+ const helper = resolveHelper(o);
9052
+ return helper.success ? undefined : helper;
9053
+ }
8882
9054
  async function openIosDriver(o) {
8883
9055
  const log = o.log ?? (() => {
8884
9056
  return;
@@ -8886,11 +9058,9 @@ async function openIosDriver(o) {
8886
9058
  const now = o.now ?? Date.now;
8887
9059
  const exec = o.debug ? timedExec(o.exec, log, now) : o.exec;
8888
9060
  const simctl = createSimctl({ udid: o.udid, exec, ...o.devicesRoot ? { devicesRoot: o.devicesRoot } : {} });
8889
- if (!o.client && !(o.socketPath ?? o.env?.simdSocket)) {
8890
- const helper = resolveHelper(o);
8891
- if (!helper.success)
8892
- return helper;
8893
- }
9061
+ const preflight = helperPreflight(o);
9062
+ if (preflight)
9063
+ return preflight;
8894
9064
  const connecting = o.client ? Promise.resolve({ success: true, data: o.client }) : connect(o, log);
8895
9065
  const [client, automation] = await Promise.all([connecting, simctl.enableAutomation()]);
8896
9066
  if (!automation.success)
@@ -8914,23 +9084,8 @@ async function openIosDriver(o) {
8914
9084
  log,
8915
9085
  ...network ? { network } : {},
8916
9086
  a11y: () => iosA11yFor(o, now, sleep),
8917
- nativeAudit: (ctx) => createIosNativeAudit({
8918
- udid: o.udid,
8919
- exec,
8920
- bundleId: () => o.bundleId ?? ctx.lastBundle(),
8921
- ...o.wardenHome ? { wardenHome: o.wardenHome } : {},
8922
- ...o.version ? { version: o.version } : {},
8923
- ...o.runnerSources ? { runnerSources: o.runnerSources } : {},
8924
- ...o.nativeAudit ?? {},
8925
- ...o.debug ? { log } : {}
8926
- }),
8927
- settings: (ctx) => createIosSettings({
8928
- udid: o.udid,
8929
- exec,
8930
- lastBundle: ctx.lastBundle,
8931
- ...o.bundleId ? { defaultBundle: o.bundleId } : {},
8932
- staged: o.stagedSettings ?? stagedSettingsFor(o.wardenHome ?? wardenHome(), o.udid)
8933
- }),
9087
+ nativeAudit: (ctx) => nativeAuditFor(o, exec, log, ctx),
9088
+ settings: (ctx) => settingsFor(o, exec, ctx),
8934
9089
  ...o.clockRecorder ? { clockRecorder: o.clockRecorder } : {}
8935
9090
  })
8936
9091
  };
@@ -9517,15 +9672,20 @@ var stderrLog = (line) => {
9517
9672
  process.stderr.write(`${line}
9518
9673
  `);
9519
9674
  };
9675
+ function leaseSeams(env, deps, given) {
9676
+ const networkProxy = given.networkProxy ?? leaseProxyFor(env, deps);
9677
+ const clockRecorder = given.clockRecorder ?? leaseClockRecorderFor(env, deps);
9678
+ return {
9679
+ ...networkProxy === undefined ? {} : { networkProxy },
9680
+ ...clockRecorder === undefined ? {} : { clockRecorder }
9681
+ };
9682
+ }
9520
9683
  function androidOptions(env, deps, debug) {
9521
9684
  const android = deps.android ?? {};
9522
9685
  const log = deps.log ?? (debug ? stderrLog : undefined);
9523
- const networkProxy = android.networkProxy ?? leaseProxyFor(env, deps);
9524
- const clockRecorder = android.clockRecorder ?? leaseClockRecorderFor(env, deps);
9525
9686
  return {
9526
9687
  ...android,
9527
- ...networkProxy === undefined ? {} : { networkProxy },
9528
- ...clockRecorder === undefined ? {} : { clockRecorder },
9688
+ ...leaseSeams(env, deps, android),
9529
9689
  serial: env.udid,
9530
9690
  adb: android.adb ?? androidTools(process.env).adb,
9531
9691
  exec: deps.exec ?? bunExec,
@@ -9557,12 +9717,9 @@ async function openDriver(env, deps = {}) {
9557
9717
  case "ios": {
9558
9718
  if (isPhysicalIosId(env.udid))
9559
9719
  return openIosDeviceDriver(deviceOptions(env, deps, debug));
9560
- const networkProxy = deps.networkProxy ?? leaseProxyFor(env, deps);
9561
- const clockRecorder = deps.clockRecorder ?? leaseClockRecorderFor(env, deps);
9562
9720
  return openIosDriver({
9563
9721
  ...deps,
9564
- ...networkProxy === undefined ? {} : { networkProxy },
9565
- ...clockRecorder === undefined ? {} : { clockRecorder },
9722
+ ...leaseSeams(env, deps, deps),
9566
9723
  udid: env.udid,
9567
9724
  bundleId: env.bundleId,
9568
9725
  env,
@@ -10111,6 +10268,12 @@ function createDevice({
10111
10268
  signal,
10112
10269
  step: (label, fn) => session.step === undefined ? fn() : session.step("locator", label, fn)
10113
10270
  });
10271
+ async function closeAutofillSaveSheet(action) {
10272
+ if (typeof action !== "string") {
10273
+ throw new E2eError2("ACTION_FAILED", `the autofill "Save password?" sheet is closed with Back only, never "${action.button}": use accept or dismiss`);
10274
+ }
10275
+ await unwrap(() => driver.pressButton("back"), "dismiss autofill save sheet", signal());
10276
+ }
10114
10277
  const device = {
10115
10278
  a11yAudit: a11y.a11yAudit,
10116
10279
  screenReader: a11y.screenReader,
@@ -10138,10 +10301,7 @@ function createDevice({
10138
10301
  if (isOpenInSheet(info) && typeof action === "string" && linkInFlight(session))
10139
10302
  return false;
10140
10303
  if (driver.platform === "android" && isAutofillSaveSheet(info.buttons)) {
10141
- if (typeof action !== "string") {
10142
- throw new E2eError2("ACTION_FAILED", `the autofill "Save password?" sheet is closed with Back only, never "${action.button}": use accept or dismiss`);
10143
- }
10144
- await unwrap(() => driver.pressButton("back"), "dismiss autofill save sheet", signal());
10304
+ await closeAutofillSaveSheet(action);
10145
10305
  return true;
10146
10306
  }
10147
10307
  const target = alertTarget(info, action, driver.platform);
@@ -10469,6 +10629,23 @@ function abortError(signal) {
10469
10629
  function isFinitePoint(point) {
10470
10630
  return Number.isFinite(point.x) && Number.isFinite(point.y);
10471
10631
  }
10632
+ async function swipeDirection(rt, options) {
10633
+ const observed = await rt.observe();
10634
+ if (canActOnElements(observed.driver)) {
10635
+ if (rt.drive("swipe", await observed.driver.scrollElement(options.direction)))
10636
+ return;
10637
+ }
10638
+ const vector = swipeVector(options.direction, screenRect(observed.snapshot.screen), options.momentum);
10639
+ rt.drive("swipe", await observed.driver.swipe(vector.from, vector.to, vector.options));
10640
+ }
10641
+ async function swipeBetween(rt, host, options) {
10642
+ if (!isFinitePoint(options.from) || !isFinitePoint(options.to)) {
10643
+ throw new E2eError2("INVALID_ARGUMENT", "swipe needs finite coordinates for from and to");
10644
+ }
10645
+ const driver = await host.driver();
10646
+ const opts = options.durationMs === undefined ? undefined : { durationMs: options.durationMs };
10647
+ rt.drive("swipe", await driver.swipe(options.from, options.to, opts));
10648
+ }
10472
10649
  function createScreen(host) {
10473
10650
  const rt = new Runtime(host);
10474
10651
  const lazy = (query) => new Locator({ kind: "query", query }, rt);
@@ -10480,22 +10657,9 @@ function createScreen(host) {
10480
10657
  throw new E2eError2("INVALID_ARGUMENT", "swipe takes either { direction, momentum? } or { from, to, durationMs? }, not a mix");
10481
10658
  }
10482
10659
  return rt.step("swipe", async () => {
10483
- if ("direction" in options) {
10484
- const observed = await rt.observe();
10485
- if (canActOnElements(observed.driver)) {
10486
- if (rt.drive("swipe", await observed.driver.scrollElement(options.direction)))
10487
- return;
10488
- }
10489
- const vector = swipeVector(options.direction, screenRect(observed.snapshot.screen), options.momentum);
10490
- rt.drive("swipe", await observed.driver.swipe(vector.from, vector.to, vector.options));
10491
- return;
10492
- }
10493
- if (!isFinitePoint(options.from) || !isFinitePoint(options.to)) {
10494
- throw new E2eError2("INVALID_ARGUMENT", "swipe needs finite coordinates for from and to");
10495
- }
10496
- const driver = await host.driver();
10497
- const opts = options.durationMs === undefined ? undefined : { durationMs: options.durationMs };
10498
- rt.drive("swipe", await driver.swipe(options.from, options.to, opts));
10660
+ if ("direction" in options)
10661
+ return swipeDirection(rt, options);
10662
+ return swipeBetween(rt, host, options);
10499
10663
  });
10500
10664
  };
10501
10665
  return {