@delacour/warden-e2e 0.14.0 → 0.15.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.
Files changed (34) hide show
  1. package/{chunk-zk3wxa70.mjs → chunk-e3cry5br.mjs} +287 -37
  2. package/index.mjs +32 -3
  3. package/package.json +2 -2
  4. package/preload.mjs +304 -30
  5. package/types/core/src/affected/affected.schema.d.ts +5 -0
  6. package/types/core/src/builds/builds.defaults.d.ts +8 -0
  7. package/types/core/src/builds/config.schema.d.ts +9 -0
  8. package/types/core/src/builds/define-config.d.ts +9 -1
  9. package/types/core/src/conditions/conditions.config.d.ts +6 -1
  10. package/types/core/src/conditions/conditions.types.d.ts +21 -0
  11. package/types/core/src/conditions/inject-state.d.ts +3 -2
  12. package/types/e2e/src/a11y/a11y-target.d.ts +2 -0
  13. package/types/e2e/src/conditions/condition-events.d.ts +6 -1
  14. package/types/e2e/src/conditions/device-conditions.d.ts +8 -2
  15. package/types/e2e/src/device.d.ts +10 -2
  16. package/types/e2e/src/driver/android/android-settings.d.ts +9 -2
  17. package/types/e2e/src/driver/ax-wedge.d.ts +38 -0
  18. package/types/e2e/src/driver/bridge-stall.d.ts +4 -0
  19. package/types/e2e/src/driver/driver.types.d.ts +30 -1
  20. package/types/e2e/src/driver/ios/inject.d.ts +17 -2
  21. package/types/e2e/src/driver/ios/ios-driver.d.ts +7 -2
  22. package/types/e2e/src/driver/ios/ios-keyboard.d.ts +39 -0
  23. package/types/e2e/src/driver/ios/simd-protocol.d.ts +99 -0
  24. package/types/e2e/src/driver/keyboard-state.d.ts +9 -0
  25. package/types/e2e/src/driver/physical-conditions.d.ts +1 -1
  26. package/types/e2e/src/driver/read-deadline.d.ts +4 -0
  27. package/types/e2e/src/driver/settle.d.ts +3 -1
  28. package/types/e2e/src/expect/a11y-matchers.d.ts +7 -0
  29. package/types/e2e/src/index.d.ts +2 -1
  30. package/types/e2e/src/screen/actions.d.ts +7 -0
  31. package/types/e2e/src/screen/context.d.ts +3 -1
  32. package/types/e2e/src/screen/keyboard-cover.d.ts +26 -0
  33. package/types/e2e/src/screen/locator.d.ts +16 -7
  34. package/types/e2e/src/session.d.ts +6 -0
@@ -264,7 +264,7 @@ async function settleByHash(poll, deps) {
264
264
  return { quiet: false, waitedMs: t - start };
265
265
  }
266
266
  }
267
- function tuple(element) {
267
+ function elementKey(element) {
268
268
  const { x, y, width, height } = element.frame;
269
269
  return JSON.stringify([
270
270
  element.role,
@@ -280,7 +280,7 @@ function tuple(element) {
280
280
  function snapshotHash(snapshot) {
281
281
  let text = "";
282
282
  for (const element of snapshot.flat)
283
- text += `${tuple(element)}
283
+ text += `${elementKey(element)}
284
284
  `;
285
285
  return Bun.hash(text).toString(36);
286
286
  }
@@ -434,6 +434,11 @@ function fieldContent(entry) {
434
434
  const text = fieldValue(entry);
435
435
  return text === "" ? { kind: "empty" } : { kind: "text", text };
436
436
  }
437
+ function maskedReading(entry, text) {
438
+ const content = fieldContent(entry);
439
+ const count = content.kind === "dots" ? content.count : 0;
440
+ return count === graphemes(text) ? text : "\u2022".repeat(count);
441
+ }
437
442
  function clearKeyPresses(entry) {
438
443
  const content = fieldContent(entry);
439
444
  switch (content.kind) {
@@ -757,7 +762,7 @@ function nonEmpty(value) {
757
762
  }
758
763
  var MASKED_VALUE = /^[\u2022\u25CF]+$/;
759
764
  function isSecure(element, traits) {
760
- 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");
765
+ return element.role === "textfield" && element.value !== undefined && MASKED_VALUE.test(element.value) || element.role === "securefield" || traits.has("password") || traits.has("secure") || traits.has("secure-text-field") || element.rawType.toLowerCase().includes("secure");
761
766
  }
762
767
  function deriveChecked(element, role, spelled, traits, value) {
763
768
  if (spelled?.checked !== undefined)
@@ -1016,6 +1021,60 @@ async function typeOver(driver, sleep, prepared, text, via) {
1016
1021
  return typeText(driver, sleep, text);
1017
1022
  }
1018
1023
 
1024
+ // ../../packages/e2e/src/driver/keyboard-state.ts
1025
+ function keyboardElement(snapshot) {
1026
+ const { height } = snapshot.screen;
1027
+ const onScreen = snapshot.flat.filter((e) => e.role === "keyboard" && e.hidden !== true && e.frame.width > 0 && e.frame.height > 0 && (height <= 0 || e.frame.y < height && e.frame.y + e.frame.height > 0));
1028
+ return onScreen.sort((a, b) => b.frame.width * b.frame.height - a.frame.width * a.frame.height)[0];
1029
+ }
1030
+ function keyboardVisible(snapshot) {
1031
+ return keyboardElement(snapshot) !== undefined;
1032
+ }
1033
+ function keyboardFrame(snapshot) {
1034
+ return keyboardElement(snapshot)?.frame;
1035
+ }
1036
+
1037
+ // ../../packages/e2e/src/screen/keyboard-cover.ts
1038
+ var COVER_MARGIN = 40;
1039
+ var SWIPE_INSET = 12;
1040
+ var SCROLL_SWIPE = { durationMs: 350, momentum: false };
1041
+ var SCROLL_ROLES = new Set(["scrollview", "list"]);
1042
+ function scrollContainer(entry) {
1043
+ for (let node = entry.parent;node !== undefined; node = node.parent) {
1044
+ if (SCROLL_ROLES.has(node.element.role))
1045
+ return node;
1046
+ }
1047
+ return;
1048
+ }
1049
+ function coverOf(entry, point, snapshot) {
1050
+ const keyboard = keyboardFrame(snapshot);
1051
+ if (keyboard === undefined || entry.element.role === "keyboard")
1052
+ return;
1053
+ const inside = point.x >= keyboard.x && point.x <= keyboard.x + keyboard.width && point.y >= keyboard.y && point.y <= keyboard.y + keyboard.height;
1054
+ if (!inside)
1055
+ return;
1056
+ const container = scrollContainer(entry);
1057
+ const shift = point.y - (keyboard.y - COVER_MARGIN);
1058
+ if (container === undefined)
1059
+ return { top: keyboard.y, shift };
1060
+ const { frame } = container.element;
1061
+ const bottom = Math.min(frame.y + frame.height, keyboard.y);
1062
+ const free = { x: frame.x, y: frame.y, width: frame.width, height: Math.max(0, bottom - frame.y) };
1063
+ return free.height > 2 * SWIPE_INSET ? { top: keyboard.y, scroller: free, shift } : { top: keyboard.y, shift };
1064
+ }
1065
+ function scrollPath(scroller, shift) {
1066
+ const x = scroller.x + scroller.width / 2;
1067
+ const room = scroller.height - 2 * SWIPE_INSET;
1068
+ const travel = Math.max(1, Math.min(shift, room));
1069
+ const from = scroller.y + scroller.height - SWIPE_INSET;
1070
+ return { from: { x, y: from }, to: { x, y: from - travel } };
1071
+ }
1072
+ var COVER_SCROLL_SWIPE = SCROLL_SWIPE;
1073
+ async function dismissKeyboard(driver) {
1074
+ const done = await driver.dismissKeyboard();
1075
+ return done.success ? { success: true, data: undefined } : done;
1076
+ }
1077
+
1019
1078
  // ../../packages/e2e/src/screen/selector.ts
1020
1079
  var VALUE_KEYS = ["id", "label", "text", "role", "value"];
1021
1080
  var KEY_ORDER = [...VALUE_KEYS, "editable", "nth"];
@@ -1581,7 +1640,8 @@ async function lookUpTestIdViaReact(facet, testId, view, options = {}) {
1581
1640
  if (!result.success)
1582
1641
  return { answered: false };
1583
1642
  const matches = [];
1584
- for (const component of result.data.components) {
1643
+ const { components } = result.data;
1644
+ for (const component of components) {
1585
1645
  const { testID, rect } = component;
1586
1646
  if (testID === undefined || rect === undefined || !matchesText(testID, testId))
1587
1647
  continue;
@@ -1591,11 +1651,25 @@ async function lookUpTestIdViaReact(facet, testId, view, options = {}) {
1591
1651
  source: "react",
1592
1652
  testId: testID,
1593
1653
  name: component.name,
1594
- ...component.text === undefined ? {} : { text: component.text }
1654
+ ...textOf2(component, components)
1595
1655
  });
1596
1656
  }
1597
1657
  return { answered: true, matches };
1598
1658
  }
1659
+ function textOf2(component, components) {
1660
+ if (component.text !== undefined)
1661
+ return { text: component.text };
1662
+ const inside = new Set([component.id]);
1663
+ const texts = [];
1664
+ for (const other of components) {
1665
+ if (other.parent === null || !inside.has(other.parent))
1666
+ continue;
1667
+ inside.add(other.id);
1668
+ if (other.text !== undefined)
1669
+ texts.push(other.text);
1670
+ }
1671
+ return texts.length === 0 ? {} : { text: texts.join(" ") };
1672
+ }
1599
1673
  function reactTapPoint(match, position) {
1600
1674
  if (position === undefined)
1601
1675
  return match.tapPoint;
@@ -1643,6 +1717,8 @@ var DEFAULT_MAX_SWIPES = 10;
1643
1717
  var VERIFY_MS = 1000;
1644
1718
  var CONFIRM_FLOOR_MS = 500;
1645
1719
  var SCROLL_SETTLE = { quietMs: 150, timeoutMs: 1000 };
1720
+ var MAX_COVER_SCROLLS = 3;
1721
+ var QUICK_CHECK_WINDOW_MS = 2000;
1646
1722
  function assertOptions(verb, options, allowed) {
1647
1723
  const unknown = Object.keys(options).filter((key) => !allowed.includes(key));
1648
1724
  if (unknown.length > 0) {
@@ -1762,11 +1838,12 @@ class Locator {
1762
1838
  return degradedFailure(this.toString(), read, waitedMs, this.reactNote(), screen);
1763
1839
  }
1764
1840
  async conclusive(options = {}) {
1765
- const first = await this.look(options);
1841
+ const first = await this.look(options.reuse === undefined ? {} : { reuse: options.reuse });
1766
1842
  const settled = await this.settled(first.observed);
1767
1843
  if (settled !== undefined)
1768
1844
  return settled;
1769
- const timeoutMs = this.rt.actionTimeout();
1845
+ const actionMs = this.rt.actionTimeout();
1846
+ const timeoutMs = options.quick === true ? Math.min(actionMs, QUICK_CHECK_WINDOW_MS) : actionMs;
1770
1847
  let last;
1771
1848
  const result = await this.rt.poll(async () => {
1772
1849
  const observation = await this.rt.tryObserve({ reuse: false });
@@ -1828,7 +1905,7 @@ class Locator {
1828
1905
  }
1829
1906
  count() {
1830
1907
  return this.traced("count", async () => {
1831
- const { entries, react } = await this.conclusive();
1908
+ const { entries, react } = await this.conclusive({ quick: true });
1832
1909
  return react?.length ?? entries.length;
1833
1910
  });
1834
1911
  }
@@ -1865,7 +1942,7 @@ class Locator {
1865
1942
  }
1866
1943
  isVisible() {
1867
1944
  return this.traced("isVisible", async () => {
1868
- const { entries, react, observed } = await this.conclusive();
1945
+ const { entries, react, observed } = await this.conclusive({ quick: true });
1869
1946
  if (react !== undefined)
1870
1947
  return reactVisible(this.reactOnly(react, observed), observed);
1871
1948
  return entries.length > 0 && this.exactlyOne(entries, observed).visible;
@@ -2038,6 +2115,26 @@ class Locator {
2038
2115
  return;
2039
2116
  return { entry, observed };
2040
2117
  }
2118
+ async clearOfKeyboard(found, spec, position, timeoutMs) {
2119
+ const coverAt = (at) => coverOf(at.entry, tapPointOf(at.entry, position), at.observed.snapshot);
2120
+ let current = found;
2121
+ let cover = coverAt(current);
2122
+ if (cover === undefined)
2123
+ return found;
2124
+ const { driver } = found.observed;
2125
+ for (let scrolls = 0;cover?.scroller !== undefined && scrolls < MAX_COVER_SCROLLS; scrolls += 1) {
2126
+ const path = scrollPath(cover.scroller, cover.shift);
2127
+ this.rt.drive("scrollIntoView", await driver.swipe(path.from, path.to, COVER_SCROLL_SWIPE));
2128
+ await driver.settle(SCROLL_SETTLE);
2129
+ current = await this.actionable(spec, timeoutMs);
2130
+ cover = coverAt(current);
2131
+ }
2132
+ if (cover === undefined)
2133
+ return current;
2134
+ this.rt.drive("dismissKeyboard", await dismissKeyboard(driver));
2135
+ await driver.settle(SCROLL_SETTLE);
2136
+ return this.actionable(spec, timeoutMs);
2137
+ }
2041
2138
  async tap(options = {}) {
2042
2139
  assertOptions("tap", options, ["position", "timeout", "until", "retries", "windowMs", "pollMs"]);
2043
2140
  const retry = retryPlan(options);
@@ -2047,7 +2144,7 @@ class Locator {
2047
2144
  if (retry === undefined)
2048
2145
  return;
2049
2146
  for (let resent = 0;resent < retry.retries; resent += 1) {
2050
- const outcome = await this.arrivedWithin(retry, snapshotHash(before));
2147
+ const outcome = await this.arrivedWithin(retry, before);
2051
2148
  if (outcome !== "press-again")
2052
2149
  return;
2053
2150
  before = await this.tapOnce(options);
@@ -2057,19 +2154,31 @@ class Locator {
2057
2154
  async tapOnce(options) {
2058
2155
  const withPosition = options.position !== undefined;
2059
2156
  const spec = { enabled: !withPosition, tapPointOnScreen: !withPosition };
2060
- const found = (options.until === undefined ? await this.aimFromKept(spec, options.position) : undefined) ?? await this.actionable(spec, options.timeout ?? this.rt.actionTimeout(), true);
2157
+ const timeoutMs = options.timeout ?? this.rt.actionTimeout();
2158
+ const look = (options.until === undefined ? await this.aimFromKept(spec, options.position) : undefined) ?? await this.actionable(spec, timeoutMs, true);
2159
+ const found = "entry" in look ? await this.clearOfKeyboard(look, spec, options.position, timeoutMs) : look;
2061
2160
  const point = "react" in found ? reactTapPoint(found.react, options.position) : tapPointOf(found.entry, options.position);
2062
2161
  const { driver } = found.observed;
2162
+ const aimed = {
2163
+ snapshot: found.observed.snapshot,
2164
+ target: "entry" in found ? found.entry.element : undefined
2165
+ };
2063
2166
  if (!withPosition && "entry" in found && canActOnElements(driver)) {
2064
2167
  if (this.rt.drive("tap", await driver.pressElement(found.entry.element)))
2065
- return found.observed.snapshot;
2168
+ return aimed;
2066
2169
  }
2067
2170
  this.rt.drive("tap", await driver.tap(point));
2068
- return found.observed.snapshot;
2171
+ return aimed;
2069
2172
  }
2070
- async arrivedWithin(plan, pressedOn) {
2173
+ async arrivedWithin(plan, aimed) {
2071
2174
  const { until } = plan;
2072
- const watch = { pressedOn, started: this.rt.clock.now(), changed: false, unchanged: 0 };
2175
+ const watch = {
2176
+ before: new Set(aimed.snapshot.flat.map(elementKey)),
2177
+ target: aimed.target === undefined ? undefined : elementKey(aimed.target),
2178
+ started: this.rt.clock.now(),
2179
+ changed: false,
2180
+ unchanged: 0
2181
+ };
2073
2182
  const result = await this.rt.poll(async (attempt) => {
2074
2183
  if (typeof until !== "function")
2075
2184
  return this.checkLocatorUntil(until, watch, attempt);
@@ -2092,7 +2201,7 @@ class Locator {
2092
2201
  }
2093
2202
  if (await until.shownIn(observation.observed))
2094
2203
  return { done: true, value: "arrived" };
2095
- if (snapshotHash(observation.observed.snapshot) === watch.pressedOn)
2204
+ if (!reacted(observation.observed.snapshot, watch))
2096
2205
  watch.unchanged += 1;
2097
2206
  else {
2098
2207
  watch.changed = true;
@@ -2104,7 +2213,9 @@ class Locator {
2104
2213
  async longPress(options = {}) {
2105
2214
  assertOptions("longPress", options, ["durationMs", "timeout"]);
2106
2215
  return this.act("longPress", async () => {
2107
- const { entry, observed } = await this.actionable({ enabled: true, tapPointOnScreen: true }, options.timeout ?? this.rt.actionTimeout());
2216
+ const spec = { enabled: true, tapPointOnScreen: true };
2217
+ const timeoutMs = options.timeout ?? this.rt.actionTimeout();
2218
+ const { entry, observed } = await this.clearOfKeyboard(await this.actionable(spec, timeoutMs), spec, undefined, timeoutMs);
2108
2219
  const opts = options.durationMs === undefined ? undefined : { durationMs: options.durationMs };
2109
2220
  this.rt.drive("longPress", await observed.driver.longPress(tapPointOf(entry), opts));
2110
2221
  });
@@ -2115,18 +2226,18 @@ class Locator {
2115
2226
  return this.act("fill", async () => {
2116
2227
  const budget = this.rt.budget(options.timeout);
2117
2228
  let target = await this.actionable({ enabled: true, tapPointOnScreen: false }, budget.remaining());
2229
+ const verify = !target.entry.secure;
2118
2230
  let firstHeld;
2119
2231
  for (let attempt = 1;; attempt += 1) {
2120
2232
  const point = focusPoint(target.entry.element.frame);
2121
2233
  this.rt.drive("fill", await focusClearType(target.observed.driver, this.pause, point, target.entry, text, clearMethodFor(attempt), process.env, viaFor(attempt, via)));
2122
- if (target.entry.secure)
2234
+ if (!verify)
2123
2235
  return;
2124
2236
  const outcome = await this.confirm(target, text, budget);
2125
2237
  if (outcome.ok)
2126
2238
  return;
2127
2239
  if (attempt === 2) {
2128
- const earlier = firstHeld === undefined ? "" : ` (first attempt held ${JSON.stringify(firstHeld)})`;
2129
- throw new E2eError2("ACTION_FAILED", `${this}: fill did not take: expected ${JSON.stringify(text)} but the field holds ${JSON.stringify(outcome.held)}${earlier}`, outcome.screen);
2240
+ throw new E2eError2("ACTION_FAILED", `${this}: ${fillMismatch(text, target.entry.secure, outcome.held, firstHeld)}`, outcome.screen);
2130
2241
  }
2131
2242
  firstHeld = outcome.held;
2132
2243
  target = await this.actionable({ enabled: true, tapPointOnScreen: false }, budget.remaining());
@@ -2249,7 +2360,11 @@ class Locator {
2249
2360
  async confirm({ entry, observed }, text, budget) {
2250
2361
  const span = Math.max(budget.remaining(), CONFIRM_FLOOR_MS);
2251
2362
  let lastSeen;
2252
- const readValue = (found) => text === "" ? fieldValue(found) : rawValue(found);
2363
+ const readValue = (found) => {
2364
+ if (text === "")
2365
+ return fieldValue(found);
2366
+ return found.secure ? maskedReading(found, text) : rawValue(found);
2367
+ };
2253
2368
  const outcome = await confirmFieldValue({
2254
2369
  text,
2255
2370
  now: this.rt.clock.now,
@@ -2307,6 +2422,16 @@ var TAP_WINDOW_MS = 4000;
2307
2422
  var TAP_CHECK_MS = 250;
2308
2423
  var UNCHANGED_CHECKS = 2;
2309
2424
  var UNCHANGED_AFTER_MS = 750;
2425
+ function reacted(snapshot, watch) {
2426
+ const now = new Set;
2427
+ for (const element of snapshot.flat) {
2428
+ const key = elementKey(element);
2429
+ if (!watch.before.has(key))
2430
+ return true;
2431
+ now.add(key);
2432
+ }
2433
+ return watch.target !== undefined && !now.has(watch.target);
2434
+ }
2310
2435
  function wholeNumber(name, value, atLeast) {
2311
2436
  if (!Number.isInteger(value) || value < atLeast) {
2312
2437
  throw new E2eError2("INVALID_ARGUMENT", `tap ${name} must be an integer of at least ${atLeast}, got ${String(value)}`);
@@ -2349,6 +2474,11 @@ function viaFor(attempt, via) {
2349
2474
  function clearMethodFor(attempt) {
2350
2475
  return attempt === 1 ? "auto" : "counted";
2351
2476
  }
2477
+ function fillMismatch(text, secure, held, firstHeld) {
2478
+ const expected = secure ? `${graphemes(text)} characters (secure)` : JSON.stringify(text);
2479
+ const earlier = firstHeld === undefined ? "" : ` (first attempt held ${JSON.stringify(firstHeld)})`;
2480
+ return `fill did not take: expected ${expected} but the field holds ${JSON.stringify(held)}${earlier}`;
2481
+ }
2352
2482
  function rawValue(entry) {
2353
2483
  return entry.value ?? (entry.role === "textbox" || entry.role === "searchbox" ? entry.element.text ?? "" : "");
2354
2484
  }
@@ -2493,7 +2623,7 @@ function describeException(details) {
2493
2623
  return String(thrown.value);
2494
2624
  return details.text ?? "JS exception";
2495
2625
  }
2496
- function textOf2(data) {
2626
+ function textOf3(data) {
2497
2627
  if (typeof data === "string")
2498
2628
  return data;
2499
2629
  if (data instanceof ArrayBuffer)
@@ -2516,7 +2646,7 @@ function taggedBindingPayload(params) {
2516
2646
  return tagged.success ? { requestId: tagged.data.requestId, payload } : undefined;
2517
2647
  }
2518
2648
  function parseEnvelope(data) {
2519
- const text = textOf2(data);
2649
+ const text = textOf3(data);
2520
2650
  if (text === undefined)
2521
2651
  return;
2522
2652
  let json;
@@ -4551,6 +4681,14 @@ async function captureNativeLog(target, deps, win) {
4551
4681
  }
4552
4682
  }
4553
4683
 
4684
+ // ../../packages/e2e/src/driver/ax-wedge.ts
4685
+ function canRecoverAxWedge(driver) {
4686
+ return typeof driver.axWedge === "function";
4687
+ }
4688
+ function restartedWithin(state, windowMs) {
4689
+ return state.last?.outcome === "restarted" && state.last.agoMs <= windowMs;
4690
+ }
4691
+
4554
4692
  // ../../packages/e2e/src/driver/ios/hid-recovery.ts
4555
4693
  import { existsSync as existsSync2 } from "fs";
4556
4694
 
@@ -4639,7 +4777,9 @@ var bridgeStallSchema = z6.looseObject({
4639
4777
  cooldownRemainingMs: z6.number().optional(),
4640
4778
  probes: z6.number(),
4641
4779
  recycles: z6.number().optional(),
4642
- recyclesExhausted: z6.boolean().optional()
4780
+ recyclesExhausted: z6.boolean().optional(),
4781
+ axWedge: z6.string().optional(),
4782
+ axWedgeRestarts: z6.number().optional()
4643
4783
  });
4644
4784
  var SPAN_FIELDS = [
4645
4785
  ["stallStage", "stage"],
@@ -4650,7 +4790,9 @@ var SPAN_FIELDS = [
4650
4790
  ["stallCooldownMs", "cooldownRemainingMs"],
4651
4791
  ["stallProbes", "probes"],
4652
4792
  ["stallRecycles", "recycles"],
4653
- ["stallRecyclesExhausted", "recyclesExhausted"]
4793
+ ["stallRecyclesExhausted", "recyclesExhausted"],
4794
+ ["stallAxWedge", "axWedge"],
4795
+ ["stallAxWedgeRestarts", "axWedgeRestarts"]
4654
4796
  ];
4655
4797
  function toBridgeStall(wire) {
4656
4798
  const stall = { stage: wire.stage, cold: wire.cold, probes: wire.probes };
@@ -4666,6 +4808,10 @@ function toBridgeStall(wire) {
4666
4808
  stall.recycles = wire.recycles;
4667
4809
  if (wire.recyclesExhausted !== undefined)
4668
4810
  stall.recyclesExhausted = wire.recyclesExhausted;
4811
+ if (wire.axWedge !== undefined)
4812
+ stall.axWedge = wire.axWedge;
4813
+ if (wire.axWedgeRestarts !== undefined)
4814
+ stall.axWedgeRestarts = wire.axWedgeRestarts;
4669
4815
  return stall;
4670
4816
  }
4671
4817
  function noteBridgeStall(stall) {
@@ -4689,7 +4835,8 @@ function bridgeStallFromSpan(detail) {
4689
4835
  ["stallDeadlineMs", "deadlineMs"],
4690
4836
  ["stallGuestAgeMs", "guestAgeMs"],
4691
4837
  ["stallCooldownMs", "cooldownRemainingMs"],
4692
- ["stallRecycles", "recycles"]
4838
+ ["stallRecycles", "recycles"],
4839
+ ["stallAxWedgeRestarts", "axWedgeRestarts"]
4693
4840
  ];
4694
4841
  for (const [key, field] of numbers) {
4695
4842
  const value = detail[key];
@@ -4698,9 +4845,20 @@ function bridgeStallFromSpan(detail) {
4698
4845
  }
4699
4846
  if (typeof detail.stallRecyclesExhausted === "boolean")
4700
4847
  stall.recyclesExhausted = detail.stallRecyclesExhausted;
4848
+ if (typeof detail.stallAxWedge === "string")
4849
+ stall.axWedge = detail.stallAxWedge;
4701
4850
  return stall;
4702
4851
  }
4703
4852
  var seconds = (ms) => `${(ms / 1000).toFixed(1)} s`;
4853
+ function axWedgeParts(stall) {
4854
+ const parts = [];
4855
+ if (stall.axWedge === "checking")
4856
+ parts.push("both readers silent: checking for a wedged accessibility service");
4857
+ if (stall.axWedgeRestarts !== undefined) {
4858
+ parts.push(`backboardd restarted ${stall.axWedgeRestarts} time${stall.axWedgeRestarts === 1 ? "" : "s"}`);
4859
+ }
4860
+ return parts;
4861
+ }
4704
4862
  function formatBridgeStall(stall) {
4705
4863
  const parts = [`stage ${stall.stage}`];
4706
4864
  if (stall.readMs !== undefined) {
@@ -4717,7 +4875,7 @@ function formatBridgeStall(stall) {
4717
4875
  const guests = stall.recycles === 1 ? "guest" : "guests";
4718
4876
  parts.push(`${stall.recycles} stalled ${guests} recycled${stall.recyclesExhausted === true ? ", none left" : ""}`);
4719
4877
  }
4720
- return parts.join(" \xB7 ");
4878
+ return [...parts, ...axWedgeParts(stall)].join(" \xB7 ");
4721
4879
  }
4722
4880
 
4723
4881
  // ../../packages/e2e/src/driver/ios/simd-protocol.ts
@@ -4879,6 +5037,7 @@ var healthDataSchema = z7.looseObject({
4879
5037
  coreSimulatorVersion: z7.string().nullish(),
4880
5038
  frontmost: z7.object({ unresolvedAgoMs: z7.number(), method: z7.string() }).optional(),
4881
5039
  bridgeRecycles: z7.object({ count: z7.number(), exhausted: z7.boolean() }).optional(),
5040
+ axWedge: z7.lazy(() => axWedgeDataSchema).optional(),
4882
5041
  state: z7.string().optional(),
4883
5042
  uptimeMs: z7.number().optional()
4884
5043
  });
@@ -4945,6 +5104,17 @@ var orientationDataSchema = z7.object({
4945
5104
  ]),
4946
5105
  source: z7.enum(["device", "written", "assumed"]).optional()
4947
5106
  });
5107
+ var axWedgeDataSchema = z7.looseObject({
5108
+ restarts: z7.number(),
5109
+ maxRestarts: z7.number(),
5110
+ recovering: z7.boolean(),
5111
+ checked: z7.enum(["restarted", "not-wedged", "failed"]).optional(),
5112
+ last: z7.discriminatedUnion("outcome", [
5113
+ z7.looseObject({ outcome: z7.literal("restarted"), agoMs: z7.number(), ms: z7.number().optional() }),
5114
+ z7.looseObject({ outcome: z7.literal("not-wedged"), agoMs: z7.number() }),
5115
+ z7.looseObject({ outcome: z7.literal("failed"), agoMs: z7.number(), error: z7.string() })
5116
+ ]).optional()
5117
+ });
4948
5118
  var opSchemas = {
4949
5119
  health: healthDataSchema,
4950
5120
  snapshot: snapshotDataSchema,
@@ -4956,7 +5126,8 @@ var opSchemas = {
4956
5126
  frontmost: frontmostDataSchema,
4957
5127
  orientation: orientationDataSchema,
4958
5128
  launch: launchDataSchema,
4959
- terminate: terminateDataSchema
5129
+ terminate: terminateDataSchema,
5130
+ axWedge: axWedgeDataSchema
4960
5131
  };
4961
5132
  function hasOpSchema(op) {
4962
5133
  return Object.hasOwn(opSchemas, op);
@@ -5722,6 +5893,10 @@ var TEXT_SIZE_NAMES = [
5722
5893
  "accessibility-extra-extra-large",
5723
5894
  "accessibility-extra-extra-extra-large"
5724
5895
  ];
5896
+ var KEYBOARD_MODES = ["software", "hardware"];
5897
+ var DEFAULT_KEYBOARD_MODE = "software";
5898
+ var IME_STASH_ENABLED = "warden_ime_enabled";
5899
+ var IME_STASH_DEFAULT = "warden_ime_default";
5725
5900
  var CAMERA_FACINGS = ["back", "front"];
5726
5901
  var CONDITION_AREAS = [
5727
5902
  "network",
@@ -5731,7 +5906,8 @@ var CONDITION_AREAS = [
5731
5906
  "locale",
5732
5907
  "a11y",
5733
5908
  "battery",
5734
- "camera"
5909
+ "camera",
5910
+ "keyboard"
5735
5911
  ];
5736
5912
 
5737
5913
  // ../../packages/core/src/conditions/network.presets.ts
@@ -5876,7 +6052,8 @@ var conditionsConfigSchema = z9.object({
5876
6052
  permissions: z9.partialRecord(z9.enum(PERMISSION_NAMES), z9.enum(["granted", "denied", "unset"])).optional(),
5877
6053
  locale: z9.string().min(1).optional(),
5878
6054
  timezone: z9.string().min(1).optional(),
5879
- a11y: a11yConfigSchema.optional()
6055
+ a11y: a11yConfigSchema.optional(),
6056
+ keyboard: z9.enum(KEYBOARD_MODES).optional()
5880
6057
  }).strict();
5881
6058
  var cameraFeedInput = z9.union([
5882
6059
  z9.string().min(1),
@@ -5943,6 +6120,8 @@ function normalizeConditions(config) {
5943
6120
  conditions.locale = config.locale;
5944
6121
  if (config.timezone !== undefined)
5945
6122
  conditions.timezone = config.timezone;
6123
+ if (config.keyboard !== undefined)
6124
+ conditions.keyboard = { mode: config.keyboard };
5946
6125
  if (config.a11y !== undefined) {
5947
6126
  const a11y = { ...config.a11y };
5948
6127
  if (config.a11y.textSize !== undefined) {
@@ -6318,6 +6497,8 @@ ${read.error}`);
6318
6497
  throw new E2eError2("SESSION", "not running under warden: WARDEN_UDID is not set");
6319
6498
  return read.data;
6320
6499
  }
6500
+ var AX_WEDGE_WAIT_MS = 60000;
6501
+ var AX_WEDGE_POLL_MS = 1000;
6321
6502
  var WARM_TRIES = 15;
6322
6503
  var WARM_INTERVAL_MS = 2000;
6323
6504
  function createSession(opts = {}) {
@@ -6362,6 +6543,27 @@ function createSession(opts = {}) {
6362
6543
  await sleep(WARM_INTERVAL_MS);
6363
6544
  }
6364
6545
  }
6546
+ let axWedgeRerunFor = 0;
6547
+ async function recoverAxWedge(attemptMs) {
6548
+ const driver = current;
6549
+ if (closing !== undefined || driver === undefined || !canRecoverAxWedge(driver))
6550
+ return false;
6551
+ const sleep = hid?.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
6552
+ let state = await driver.axWedge();
6553
+ let waited = 0;
6554
+ while (state.success && state.data.recovering && waited < AX_WEDGE_WAIT_MS) {
6555
+ await sleep(AX_WEDGE_POLL_MS);
6556
+ waited += AX_WEDGE_POLL_MS;
6557
+ state = await driver.axWedge();
6558
+ }
6559
+ if (!state.success || state.data.restarts <= axWedgeRerunFor)
6560
+ return false;
6561
+ if (!restartedWithin(state.data, attemptMs + waited))
6562
+ return false;
6563
+ axWedgeRerunFor = state.data.restarts;
6564
+ note(`sim ${resolved.deviceName ?? resolved.udid}: accessibility service wedged, warden-simd restarted backboardd (the app was quit); running the test again`);
6565
+ return true;
6566
+ }
6365
6567
  async function recoverRunner(deps, name) {
6366
6568
  if (hidRecovered) {
6367
6569
  note(`iPhone ${name}: the runner connection was lost again after a restart; check the phone and retry`);
@@ -6439,6 +6641,7 @@ function createSession(opts = {}) {
6439
6641
  fixtures: opts.fixtures ?? defaultFixtures,
6440
6642
  nativeLog: opts.nativeLog ?? defaultNativeLog,
6441
6643
  recoverHid: hid === undefined || resolved.platform !== "ios" ? undefined : recoverHid,
6644
+ recoverAxWedge: resolved.platform !== "ios" || resolved.deviceKind === "physical" ? undefined : recoverAxWedge,
6442
6645
  resetDevice,
6443
6646
  log: opts.log ?? writeLine,
6444
6647
  get attempt() {
@@ -6577,6 +6780,9 @@ function attemptSession(session, binding) {
6577
6780
  get recoverHid() {
6578
6781
  return session.recoverHid;
6579
6782
  },
6783
+ get recoverAxWedge() {
6784
+ return session.recoverAxWedge;
6785
+ },
6580
6786
  resetDevice: () => session.resetDevice(),
6581
6787
  get fixtures() {
6582
6788
  return session.fixtures;
@@ -7207,15 +7413,15 @@ function runAudit(snapshot, platform, opts = {}) {
7207
7413
  function failing(result, minSeverity = "error") {
7208
7414
  return result.issues.filter((i) => SEVERITY_RANK[i.severity] >= SEVERITY_RANK[minSeverity]);
7209
7415
  }
7210
- function elementKey(i) {
7416
+ function elementKey2(i) {
7211
7417
  return i.identifier ?? i.elementId;
7212
7418
  }
7213
7419
  function mergeIssues(base, extra, skipped = []) {
7214
7420
  const issues = [...base.issues];
7215
7421
  const added = [];
7216
7422
  for (const e of extra) {
7217
- const key = elementKey(e);
7218
- const idx = issues.findIndex((i) => key !== undefined && elementKey(i) === key && (e.wcag !== undefined ? i.wcag === e.wcag : i.rule === e.rule && i.message === e.message));
7423
+ const key = elementKey2(e);
7424
+ const idx = issues.findIndex((i) => key !== undefined && elementKey2(i) === key && (e.wcag !== undefined ? i.wcag === e.wcag : i.rule === e.rule && i.message === e.message));
7219
7425
  const hit = idx === -1 ? undefined : issues[idx];
7220
7426
  if (hit === undefined) {
7221
7427
  added.push(e);
@@ -7558,6 +7764,12 @@ function createDeviceA11y(deps) {
7558
7764
  const merged = await withA11yRead(driver, taken.data);
7559
7765
  return renderA11yScreen(merged.success ? merged.data : taken.data);
7560
7766
  },
7767
+ async keyboardVisible() {
7768
+ const state = await settled(() => driver.keyboardState(), "keyboardState", signal());
7769
+ if (!state.success)
7770
+ throw toE2eError(state.error, "read the keyboard");
7771
+ return state.data.visible;
7772
+ },
7561
7773
  assertionTimeout: () => env.assertionTimeoutMs,
7562
7774
  step: (label, fn) => deps.step === undefined ? fn() : deps.step(label, fn)
7563
7775
  };
@@ -8353,6 +8565,7 @@ var KNOWN_AREAS = new Set([
8353
8565
  "locale",
8354
8566
  "a11y",
8355
8567
  "battery",
8568
+ "keyboard",
8356
8569
  "camera"
8357
8570
  ]);
8358
8571
 
@@ -8726,6 +8939,8 @@ function createDeviceConditions(deps) {
8726
8939
  let localeDirty = false;
8727
8940
  let a11yDirty = false;
8728
8941
  let batteryDirty = false;
8942
+ let keyboardDirty = false;
8943
+ let currentKeyboard = DEFAULT_KEYBOARD_MODE;
8729
8944
  let cameraDirty = false;
8730
8945
  let currentCamera = {};
8731
8946
  const permissionStates = new Map;
@@ -9102,6 +9317,28 @@ the app runs without the warden network dylib: declare \`conditions.network\` fo
9102
9317
  emit({ area: "battery", value });
9103
9318
  return state;
9104
9319
  }
9320
+ const ownsDevice = recorder !== undefined && env.deviceKind !== "physical";
9321
+ const keyboardGoal = baseline.keyboard?.mode ?? DEFAULT_KEYBOARD_MODE;
9322
+ async function applyKeyboard(input) {
9323
+ const mode = input === "toggle" ? currentKeyboard === "software" ? "hardware" : "software" : input;
9324
+ if (!KEYBOARD_MODES.includes(mode)) {
9325
+ throw invalid2(`unknown keyboard mode "${String(input)}" (use software, hardware or toggle)`);
9326
+ }
9327
+ keyboardDirty = true;
9328
+ const run = settingOp("keyboard");
9329
+ const state = await call("keyboard", () => run(mode));
9330
+ currentKeyboard = mode;
9331
+ const value = {
9332
+ mode,
9333
+ ...state.previousIme === undefined ? {} : { previousIme: state.previousIme }
9334
+ };
9335
+ if (mode === DEFAULT_KEYBOARD_MODE)
9336
+ recorder?.settle("keyboard", value, true);
9337
+ else
9338
+ recorder?.record("keyboard", value);
9339
+ emit({ area: "keyboard", value });
9340
+ return state;
9341
+ }
9105
9342
  const home = deps.wardenHome ?? wardenHome(process.env);
9106
9343
  async function applyCamera(next) {
9107
9344
  cameraDirty = true;
@@ -9204,7 +9441,7 @@ the app runs without the warden network dylib: declare \`conditions.network\` fo
9204
9441
  throw new E2eError2("ACTION_FAILED", `a network flap or timeline stopped on its own after failing: ${failures[0]}`);
9205
9442
  }
9206
9443
  }
9207
- const declared = baseline.network !== undefined || baseline.location !== undefined || baseline.clock !== undefined || baseline.permissions !== undefined || baseline.locale !== undefined || baseline.timezone !== undefined || baseline.a11y !== undefined || env.cameraFeeds !== undefined;
9444
+ const declared = baseline.network !== undefined || baseline.location !== undefined || baseline.clock !== undefined || baseline.permissions !== undefined || baseline.locale !== undefined || baseline.timezone !== undefined || baseline.a11y !== undefined || baseline.keyboard !== undefined || ownsDevice || env.cameraFeeds !== undefined;
9208
9445
  async function applyBaselinePermissions() {
9209
9446
  if (baseline.permissions === undefined)
9210
9447
  return;
@@ -9241,6 +9478,10 @@ the app runs without the warden network dylib: declare \`conditions.network\` fo
9241
9478
  async () => {
9242
9479
  if (env.cameraFeeds !== undefined)
9243
9480
  await applyCamera(baselineCamera());
9481
+ },
9482
+ async () => {
9483
+ if (baseline.keyboard !== undefined || ownsDevice)
9484
+ await applyKeyboard(keyboardGoal);
9244
9485
  }
9245
9486
  ];
9246
9487
  const teardownSteps = [
@@ -9280,6 +9521,13 @@ the app runs without the warden network dylib: declare \`conditions.network\` fo
9280
9521
  batteryDirty = false;
9281
9522
  }
9282
9523
  },
9524
+ {
9525
+ dirty: () => keyboardDirty,
9526
+ run: async () => {
9527
+ await applyKeyboard(keyboardGoal);
9528
+ keyboardDirty = false;
9529
+ }
9530
+ },
9283
9531
  {
9284
9532
  dirty: () => cameraDirty,
9285
9533
  run: async () => {
@@ -9290,7 +9538,7 @@ the app runs without the warden network dylib: declare \`conditions.network\` fo
9290
9538
  ];
9291
9539
  const internals = {
9292
9540
  declared,
9293
- dirty: () => networkDirty || locationDirty || clockDirty || permissionsDirty || localeDirty || a11yDirty || batteryDirty || cameraDirty || runs.size > 0,
9541
+ dirty: () => networkDirty || locationDirty || clockDirty || permissionsDirty || localeDirty || a11yDirty || batteryDirty || keyboardDirty || cameraDirty || runs.size > 0,
9294
9542
  wantsInject: () => baseline.network !== undefined || baseline.clock !== undefined || currentNetwork.kind !== "online" || currentClock.kind !== "real",
9295
9543
  async applyBaseline() {
9296
9544
  for (const step of baselineSteps)
@@ -9303,6 +9551,7 @@ the app runs without the warden network dylib: declare \`conditions.network\` fo
9303
9551
  localeDirty = false;
9304
9552
  a11yDirty = false;
9305
9553
  batteryDirty = false;
9554
+ keyboardDirty = false;
9306
9555
  },
9307
9556
  async teardown() {
9308
9557
  const errors = [];
@@ -9331,6 +9580,7 @@ the app runs without the warden network dylib: declare \`conditions.network\` fo
9331
9580
  locale: applyLocale,
9332
9581
  a11y: applyA11y,
9333
9582
  battery: applyBattery,
9583
+ keyboard: applyKeyboard,
9334
9584
  camera: cameraApi,
9335
9585
  memoryWarning: async (options) => {
9336
9586
  const op = settingOp("memoryWarning");
@@ -9650,6 +9900,6 @@ function onlyWasSeen() {
9650
9900
 
9651
9901
  // ../../packages/e2e/src/version.ts
9652
9902
  var WORKSPACE_VERSION = "0.0.0-workspace";
9653
- var WARDEN_E2E_VERSION2 = "0.14.0";
9903
+ var WARDEN_E2E_VERSION2 = "0.15.0";
9654
9904
 
9655
- export { E2eError2, registerA11yTarget, a11yTargetOf, canActOnElements, noteSpanDetail, addSpanDetail, canObserve, canDetectAlerts, settleDetailOf, DEFAULT_SETTLE_QUIET_MS, DEFAULT_SETTLE_TIMEOUT_MS, hashFallbackQuietMs, settleByHash, snapshotHash, ok, err, swipeVector, screenRect, isOnScreen, fieldValue, PINCH_MS, ROTATE_MS, PATH_MS, centreOf, pinchRadius, rotateRadius, requireGesture, assertScale, assertDegrees, isInteractiveRole, resolveRole, normalizeText, matchesText, indexSnapshot, testIdQuery, textQuery, roleQuery, selectorExpression, renderSnapshot, renderHeader, lookUpTestIdViaReact, isLocator, Locator, pickOne, textOf, scrollUntilVisible, formatNativeTail, realNativeLogDeps, captureNativeLog, bunExec, execError, driverError, driverErr, bridgeStallSchema, toBridgeStall, noteBridgeStall, bridgeStallFromSpan, formatBridgeStall, 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 };
9905
+ export { E2eError2, registerA11yTarget, a11yTargetOf, canActOnElements, noteSpanDetail, addSpanDetail, canObserve, canDetectAlerts, settleDetailOf, DEFAULT_SETTLE_QUIET_MS, DEFAULT_SETTLE_TIMEOUT_MS, hashFallbackQuietMs, settleByHash, snapshotHash, ok, err, swipeVector, screenRect, isOnScreen, fieldValue, PINCH_MS, ROTATE_MS, PATH_MS, centreOf, pinchRadius, rotateRadius, requireGesture, assertScale, assertDegrees, isInteractiveRole, resolveRole, normalizeText, matchesText, indexSnapshot, keyboardVisible, testIdQuery, textQuery, roleQuery, selectorExpression, renderSnapshot, renderHeader, lookUpTestIdViaReact, isLocator, Locator, pickOne, textOf, scrollUntilVisible, formatNativeTail, realNativeLogDeps, captureNativeLog, bunExec, execError, driverError, driverErr, bridgeStallSchema, toBridgeStall, noteBridgeStall, bridgeStallFromSpan, formatBridgeStall, logEventSchema, fatalEventSchema, responseSchema, mapWireError, createLineSplitter, encodeRequest, bunSpawn, stdioTransport, socketTransport, createSimdClient, spawnSimd, connectSimd, isRecoverableLoss, parseDuration, KEYBOARD_MODES, DEFAULT_KEYBOARD_MODE, IME_STASH_ENABLED, IME_STASH_DEFAULT, 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 };