@delacour/warden-e2e 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/{chunk-yb1hcjqm.mjs → chunk-vs5npy59.mjs} +3370 -218
  2. package/index.mjs +247 -16
  3. package/package.json +2 -2
  4. package/preload.mjs +8264 -2387
  5. package/types/core/src/a11y/a11y.config.d.ts +47 -0
  6. package/types/core/src/affected/affected.schema.d.ts +142 -1
  7. package/types/core/src/allocate.d.ts +2 -1
  8. package/types/core/src/android-keyguard.d.ts +36 -0
  9. package/types/core/src/batch/preset.schema.d.ts +13 -1
  10. package/types/core/src/biometrics.d.ts +15 -0
  11. package/types/core/src/builds/app-option.schema.d.ts +1 -1
  12. package/types/core/src/builds/builds.defaults.d.ts +15 -0
  13. package/types/core/src/builds/config.d.ts +5 -1
  14. package/types/core/src/builds/config.schema.d.ts +275 -2
  15. package/types/core/src/builds/define-config.d.ts +159 -0
  16. package/types/core/src/builds/install-device.d.ts +37 -0
  17. package/types/core/src/builds/install.d.ts +17 -3
  18. package/types/core/src/builds/kinds.d.ts +21 -3
  19. package/types/core/src/builds/lock.d.ts +38 -0
  20. package/types/core/src/builds/resolve.d.ts +12 -2
  21. package/types/core/src/camera-mode.d.ts +17 -0
  22. package/types/core/src/camera-placeholder.d.ts +9 -0
  23. package/types/core/src/camera-profile.d.ts +12 -0
  24. package/types/core/src/claim-errors.d.ts +34 -0
  25. package/types/core/src/conditions/a11y-keys.d.ts +45 -0
  26. package/types/core/src/conditions/camera-media.d.ts +30 -0
  27. package/types/core/src/conditions/conditions.config.d.ts +141 -0
  28. package/types/core/src/conditions/conditions.d.ts +22 -0
  29. package/types/core/src/conditions/conditions.types.d.ts +171 -0
  30. package/types/core/src/conditions/inject-state.d.ts +13 -0
  31. package/types/core/src/conditions/launch-settings.d.ts +14 -0
  32. package/types/core/src/conditions/network.presets.d.ts +42 -0
  33. package/types/core/src/conditions/text-size.d.ts +19 -0
  34. package/types/core/src/golden/android-biometrics.d.ts +35 -0
  35. package/types/core/src/ios-runner/build.d.ts +28 -0
  36. package/types/core/src/ios-runner/cache.d.ts +44 -0
  37. package/types/core/src/ios-runner/classify.d.ts +7 -0
  38. package/types/core/src/ios-runner/pack.d.ts +17 -0
  39. package/types/core/src/ios-runner/runner-build.types.d.ts +76 -0
  40. package/types/core/src/ios-runner/signing.d.ts +40 -0
  41. package/types/core/src/ios-runner/sim-build.d.ts +36 -0
  42. package/types/core/src/ios-runner/stage.d.ts +26 -0
  43. package/types/core/src/lease-events.d.ts +126 -0
  44. package/types/core/src/netproxy/control.d.ts +75 -0
  45. package/types/core/src/netproxy/decide.d.ts +22 -0
  46. package/types/core/src/netproxy/handle-file.d.ts +12 -0
  47. package/types/core/src/netproxy/lease-proxy.d.ts +53 -0
  48. package/types/core/src/netproxy/netproxy.main.d.ts +40 -0
  49. package/types/core/src/netproxy/pipe.d.ts +37 -0
  50. package/types/core/src/netproxy/proxy.d.ts +48 -0
  51. package/types/core/src/netproxy/shaping.d.ts +43 -0
  52. package/types/core/src/ownership.d.ts +44 -0
  53. package/types/core/src/providers/android.d.ts +46 -2
  54. package/types/core/src/providers/provider.types.d.ts +9 -0
  55. package/types/core/src/queue.d.ts +43 -0
  56. package/types/core/src/store.d.ts +61 -5
  57. package/types/core/src/types.d.ts +57 -0
  58. package/types/core/src/xcode.d.ts +22 -0
  59. package/types/e2e/src/a11y/a11y-target.d.ts +23 -0
  60. package/types/e2e/src/a11y/a11y.types.d.ts +81 -0
  61. package/types/e2e/src/a11y/announcements.d.ts +24 -0
  62. package/types/e2e/src/a11y/audit.d.ts +22 -0
  63. package/types/e2e/src/a11y/native/atf.d.ts +51 -0
  64. package/types/e2e/src/a11y/native/xcuitest.d.ts +60 -0
  65. package/types/e2e/src/a11y/order.d.ts +5 -0
  66. package/types/e2e/src/a11y/rules/duplicate-name.d.ts +6 -0
  67. package/types/e2e/src/a11y/rules/hidden-focusable.d.ts +3 -0
  68. package/types/e2e/src/a11y/rules/image-no-name.d.ts +6 -0
  69. package/types/e2e/src/a11y/rules/label-in-name.d.ts +7 -0
  70. package/types/e2e/src/a11y/rules/missing-name.d.ts +3 -0
  71. package/types/e2e/src/a11y/rules/role-in-name.d.ts +3 -0
  72. package/types/e2e/src/a11y/rules/rule.context.d.ts +22 -0
  73. package/types/e2e/src/a11y/rules/target-size.d.ts +21 -0
  74. package/types/e2e/src/a11y/rules/text-clipped.d.ts +9 -0
  75. package/types/e2e/src/a11y/speech.d.ts +26 -0
  76. package/types/e2e/src/a11y/talkback-log.d.ts +13 -0
  77. package/types/e2e/src/alert-buttons.d.ts +9 -0
  78. package/types/e2e/src/app.d.ts +3 -2
  79. package/types/e2e/src/conditions/condition-events.d.ts +68 -0
  80. package/types/e2e/src/conditions/device-conditions.d.ts +198 -0
  81. package/types/e2e/src/conditions/geo.d.ts +15 -0
  82. package/types/e2e/src/conditions/gpx.d.ts +8 -0
  83. package/types/e2e/src/conditions/lease-recorder.d.ts +27 -0
  84. package/types/e2e/src/conditions/lease-store.d.ts +9 -0
  85. package/types/e2e/src/conditions/location-plan.d.ts +17 -0
  86. package/types/e2e/src/conditions/location-scenarios.data.d.ts +17 -0
  87. package/types/e2e/src/conditions/route-ticker.d.ts +53 -0
  88. package/types/e2e/src/conditions/scheduler.d.ts +83 -0
  89. package/types/e2e/src/device-a11y.d.ts +97 -0
  90. package/types/e2e/src/device.d.ts +67 -4
  91. package/types/e2e/src/driver/a11y-events.d.ts +30 -0
  92. package/types/e2e/src/driver/a11y-read.d.ts +12 -0
  93. package/types/e2e/src/driver/android/android-animations.d.ts +7 -0
  94. package/types/e2e/src/driver/android/android-app.d.ts +4 -0
  95. package/types/e2e/src/driver/android/android-argv.d.ts +21 -0
  96. package/types/e2e/src/driver/android/android-camera-host.d.ts +23 -0
  97. package/types/e2e/src/driver/android/android-camera.d.ts +28 -0
  98. package/types/e2e/src/driver/android/android-clock.d.ts +28 -0
  99. package/types/e2e/src/driver/android/android-device.d.ts +8 -0
  100. package/types/e2e/src/driver/android/android-driver.d.ts +22 -0
  101. package/types/e2e/src/driver/android/android-network.d.ts +66 -0
  102. package/types/e2e/src/driver/android/android-phone.d.ts +47 -0
  103. package/types/e2e/src/driver/android/android-screen-reader.d.ts +23 -0
  104. package/types/e2e/src/driver/android/android-settings.d.ts +41 -0
  105. package/types/e2e/src/driver/android/android-spoken.d.ts +18 -0
  106. package/types/e2e/src/driver/android/android-state.d.ts +10 -0
  107. package/types/e2e/src/driver/android/android-talkback.d.ts +57 -0
  108. package/types/e2e/src/driver/android/atf-device.d.ts +23 -0
  109. package/types/e2e/src/driver/android/atf-dex.generated.d.ts +7 -0
  110. package/types/e2e/src/driver/android/uiad-dex.generated.d.ts +5 -5
  111. package/types/e2e/src/driver/android/uiad-ops.d.ts +27 -1
  112. package/types/e2e/src/driver/android/uiad-protocol.d.ts +8 -0
  113. package/types/e2e/src/driver/android/uiad-screen-reader.d.ts +33 -0
  114. package/types/e2e/src/driver/clock-recorder.d.ts +9 -0
  115. package/types/e2e/src/driver/device-settings.d.ts +10 -0
  116. package/types/e2e/src/driver/driver.types.d.ts +242 -1
  117. package/types/e2e/src/driver/element-actions.d.ts +16 -0
  118. package/types/e2e/src/driver/ios/device/capabilities.d.ts +3 -0
  119. package/types/e2e/src/driver/ios/device/device-driver.d.ts +49 -0
  120. package/types/e2e/src/driver/ios/device/devicectl.d.ts +38 -0
  121. package/types/e2e/src/driver/ios/device/runner-protocol.d.ts +21 -0
  122. package/types/e2e/src/driver/ios/hid-recovery.d.ts +15 -0
  123. package/types/e2e/src/driver/ios/inject.d.ts +124 -0
  124. package/types/e2e/src/driver/ios/ios-a11y.d.ts +73 -0
  125. package/types/e2e/src/driver/ios/ios-audit.d.ts +36 -0
  126. package/types/e2e/src/driver/ios/ios-driver.d.ts +50 -1
  127. package/types/e2e/src/driver/ios/ios-network.d.ts +51 -0
  128. package/types/e2e/src/driver/ios/ios-settings.d.ts +44 -0
  129. package/types/e2e/src/driver/ios/sim-runner.d.ts +41 -0
  130. package/types/e2e/src/driver/ios/simctl.d.ts +41 -1
  131. package/types/e2e/src/driver/ios/simd-transport.d.ts +11 -2
  132. package/types/e2e/src/driver/ios/type-budget.d.ts +6 -1
  133. package/types/e2e/src/driver/native-audit.d.ts +26 -0
  134. package/types/e2e/src/driver/network-proxy.d.ts +52 -0
  135. package/types/e2e/src/driver/open.d.ts +20 -0
  136. package/types/e2e/src/driver/physical-conditions.d.ts +11 -0
  137. package/types/e2e/src/driver/requirements.d.ts +14 -0
  138. package/types/e2e/src/driver/screen-reader.d.ts +26 -0
  139. package/types/e2e/src/driver/typing.d.ts +1 -1
  140. package/types/e2e/src/env.d.ts +47 -0
  141. package/types/e2e/src/expect/a11y-matchers.d.ts +33 -0
  142. package/types/e2e/src/expect/expect.d.ts +7 -2
  143. package/types/e2e/src/expect/matchers.d.ts +14 -0
  144. package/types/e2e/src/fixture-session.d.ts +4 -0
  145. package/types/e2e/src/fixtures.d.ts +6 -1
  146. package/types/e2e/src/index.d.ts +6 -0
  147. package/types/e2e/src/runner/callsite.d.ts +3 -1
  148. package/types/e2e/src/runner/scope.d.ts +1 -0
  149. package/types/e2e/src/runner/test.d.ts +6 -0
  150. package/types/e2e/src/screen/render.d.ts +8 -0
  151. package/types/e2e/src/traced-device.d.ts +2 -1
package/index.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  // @bun
2
2
  import {
3
3
  E2eError2,
4
+ a11yTargetOf,
4
5
  canObserve,
5
6
  fieldValue,
6
7
  normalizeText,
@@ -11,16 +12,25 @@ import {
11
12
  isLocator,
12
13
  pickOne,
13
14
  textOf,
14
- isHidLost,
15
+ isRecoverableLoss,
15
16
  runStep,
16
17
  attemptSession,
17
18
  getSession,
19
+ accessibleName,
20
+ announce,
21
+ failing,
22
+ formatIssues,
23
+ toE2eError,
24
+ withA11yRead,
25
+ renderA11yScreen,
26
+ IOS_DEVICE_CAPABILITIES,
27
+ callerFile,
28
+ conditionsOf,
18
29
  perfStartFor,
19
30
  hash6,
20
31
  attemptDir,
21
32
  specIdOf,
22
33
  captureNativeLog,
23
- callerFile,
24
34
  BUN_TIMEOUT_MARGIN_MS2,
25
35
  ownHook,
26
36
  currentScope,
@@ -31,10 +41,106 @@ import {
31
41
  registrationDefaults,
32
42
  markOnly,
33
43
  WARDEN_E2E_VERSION2
34
- } from "./chunk-yb1hcjqm.mjs";
44
+ } from "./chunk-vs5npy59.mjs";
35
45
  // ../../packages/e2e/src/expect/expect.ts
36
46
  import { expect as bunExpect } from "bun:test";
37
47
 
48
+ // ../../packages/e2e/src/expect/a11y-matchers.ts
49
+ function show(match) {
50
+ return typeof match === "string" ? JSON.stringify(match) : String(match);
51
+ }
52
+ function checkMillis(matcher, key, value) {
53
+ if (value !== undefined && (typeof value !== "number" || !Number.isFinite(value) || value < 0)) {
54
+ throw new E2eError2("INVALID_ARGUMENT", `${matcher}: "${key}" must be a non-negative number of milliseconds`);
55
+ }
56
+ }
57
+
58
+ class DeviceAssertions {
59
+ target;
60
+ context;
61
+ constructor(target, context) {
62
+ this.target = target;
63
+ this.context = context;
64
+ }
65
+ get not() {
66
+ return new DeviceAssertions(this.target, { ...this.context, negate: !this.context.negate });
67
+ }
68
+ async toPassA11yAudit(options = {}) {
69
+ const call = `toPassA11yAudit(${Object.keys(options).length === 0 ? "" : JSON.stringify(options)})`;
70
+ return this.run(call, async () => {
71
+ const result = await this.target.audit(options);
72
+ const min = options.minSeverity ?? "error";
73
+ const bad = failing(result, min);
74
+ const passes = bad.length === 0;
75
+ if (passes !== this.context.negate)
76
+ return;
77
+ const table = formatIssues(result, min);
78
+ const lines = [
79
+ `expect(device)${this.context.negate ? ".not" : ""}.${call}`,
80
+ `Expected: ${this.context.negate ? "at least one" : "no"} ${min}${min === "error" ? "" : " or worse"} accessibility issue`,
81
+ `Received: ${table}`
82
+ ];
83
+ throw await this.failure(lines);
84
+ });
85
+ }
86
+ async toHaveAnnounced(expected, options = {}) {
87
+ for (const key of Object.keys(options)) {
88
+ if (key !== "timeoutMs" && key !== "timeout" && key !== "since") {
89
+ throw new E2eError2("INVALID_ARGUMENT", `toHaveAnnounced does not take "${key}"; it takes timeoutMs, since`);
90
+ }
91
+ }
92
+ checkMillis("toHaveAnnounced", "timeoutMs", options.timeoutMs);
93
+ checkMillis("toHaveAnnounced", "timeout", options.timeout);
94
+ const timeoutMs = options.timeoutMs ?? options.timeout ?? this.target.assertionTimeout();
95
+ const call = `toHaveAnnounced(${show(expected)})`;
96
+ return this.run(call, async () => {
97
+ const waited = await this.target.waitForAnnouncement(expected, {
98
+ timeoutMs,
99
+ ...options.since === undefined ? {} : { since: options.since }
100
+ });
101
+ const { negate } = this.context;
102
+ if (waited.success) {
103
+ if (!negate)
104
+ return;
105
+ throw await this.failure([
106
+ `expect(device).not.${call}`,
107
+ `Expected: no announcement matching ${show(expected)}`,
108
+ `Received: ${JSON.stringify(waited.data.event.text ?? "")}`
109
+ ]);
110
+ }
111
+ if (waited.error.code !== "E_TIMEOUT")
112
+ throw toE2eError(waited.error, "read announcements");
113
+ if (negate)
114
+ return;
115
+ const seen = waited.error.detail?.seen ?? [];
116
+ throw await this.failure([
117
+ `expect(device).${call}`,
118
+ `Expected: an announcement matching ${show(expected)} within ${timeoutMs} ms`,
119
+ `Received: ${seen.length === 0 ? "(no announcements)" : seen.map((text) => JSON.stringify(text)).join(", ")}`
120
+ ]);
121
+ });
122
+ }
123
+ async failure(lines) {
124
+ const screen = await this.target.a11yScreen();
125
+ const head = lines.join(`
126
+ `);
127
+ return new E2eError2("ASSERTION", screen === undefined ? head : `${head}
128
+
129
+ ${screen}`, screen);
130
+ }
131
+ async run(call, body) {
132
+ try {
133
+ await this.target.step(`expect device.${call}`, body);
134
+ } catch (error) {
135
+ if (this.context.soft !== undefined && error instanceof E2eError2 && error.code !== "TIMEOUT") {
136
+ this.context.soft(error);
137
+ return;
138
+ }
139
+ throw error;
140
+ }
141
+ }
142
+ }
143
+
38
144
  // ../../packages/e2e/src/expect/matchers.ts
39
145
  var NEGATION_GRACE_MS = 1000;
40
146
  var NEGATION_QUIET_MS = 250;
@@ -45,7 +151,7 @@ function missing() {
45
151
  function judged(matches, received) {
46
152
  return { kind: "judged", matches, received };
47
153
  }
48
- function show(match) {
154
+ function show2(match) {
49
155
  return typeof match === "string" ? JSON.stringify(match) : String(match);
50
156
  }
51
157
  function assertOptions(matcher, options, allowed) {
@@ -112,8 +218,8 @@ class LocatorAssertions {
112
218
  assertOptions("toHaveText", options, ["exact", "timeout"]);
113
219
  const exact = options.exact ?? true;
114
220
  return this.assert({
115
- call: `toHaveText(${show(expected)})`,
116
- expected: exact ? show(expected) : `${show(expected)} (substring, ignoring case)`,
221
+ call: `toHaveText(${show2(expected)})`,
222
+ expected: exact ? show2(expected) : `${show2(expected)} (substring, ignoring case)`,
117
223
  strict: true,
118
224
  judge: (entry) => {
119
225
  if (entry === undefined)
@@ -127,8 +233,8 @@ class LocatorAssertions {
127
233
  assertOptions("toContainText", options, ["ignoreCase", "timeout"]);
128
234
  const ignoreCase = options.ignoreCase ?? false;
129
235
  return this.assert({
130
- call: `toContainText(${show(expected)})`,
131
- expected: `text containing ${show(expected)}`,
236
+ call: `toContainText(${show2(expected)})`,
237
+ expected: `text containing ${show2(expected)}`,
132
238
  strict: true,
133
239
  judge: (entry) => {
134
240
  if (entry === undefined)
@@ -141,8 +247,8 @@ class LocatorAssertions {
141
247
  async toHaveValue(expected, options = {}) {
142
248
  assertOptions("toHaveValue", options, ["timeout"]);
143
249
  return this.assert({
144
- call: `toHaveValue(${show(expected)})`,
145
- expected: show(expected),
250
+ call: `toHaveValue(${show2(expected)})`,
251
+ expected: show2(expected),
146
252
  strict: true,
147
253
  judge: (entry) => {
148
254
  if (entry === undefined)
@@ -189,6 +295,78 @@ class LocatorAssertions {
189
295
  judge: (entry) => entry === undefined ? missing() : judged(entry.states.disabled, entry.states.disabled ? "disabled" : "enabled")
190
296
  }, options);
191
297
  }
298
+ async toHaveAccessibleName(expected, options = {}) {
299
+ assertOptions("toHaveAccessibleName", options, ["timeout"]);
300
+ return this.assert({
301
+ call: `toHaveAccessibleName(${show2(expected)})`,
302
+ expected: show2(expected),
303
+ strict: true,
304
+ a11y: true,
305
+ judge: (entry, _count, ctx) => {
306
+ if (entry === undefined)
307
+ return missing();
308
+ const name = accessibleName(ctx.element(entry), ctx.platform) ?? "";
309
+ return judged(matchesText(name, expected, { exact: true }), JSON.stringify(name));
310
+ }
311
+ }, options);
312
+ }
313
+ async toHaveRole(expected, options = {}) {
314
+ assertOptions("toHaveRole", options, ["timeout"]);
315
+ return this.assert({
316
+ call: `toHaveRole(${JSON.stringify(expected)})`,
317
+ expected: JSON.stringify(expected),
318
+ strict: true,
319
+ judge: (entry) => entry === undefined ? missing() : judged(entry.role === expected, JSON.stringify(entry.role))
320
+ }, options);
321
+ }
322
+ async toHaveHint(expected, options = {}) {
323
+ assertOptions("toHaveHint", options, ["timeout"]);
324
+ return this.assert({
325
+ call: `toHaveHint(${show2(expected)})`,
326
+ expected: show2(expected),
327
+ strict: true,
328
+ a11y: true,
329
+ judge: (entry, _count, ctx) => {
330
+ if (entry === undefined)
331
+ return missing();
332
+ const hint = ctx.element(entry).a11y?.hint;
333
+ if (hint === undefined || hint === "")
334
+ return judged(false, "(no hint)");
335
+ return judged(matchesText(hint, expected, { exact: true }), JSON.stringify(hint));
336
+ }
337
+ }, options);
338
+ }
339
+ async toAnnounce(expected, options = {}) {
340
+ assertOptions("toAnnounce", options, ["exact", "timeout"]);
341
+ const exact = options.exact ?? true;
342
+ return this.assert({
343
+ call: `toAnnounce(${show2(expected)})`,
344
+ expected: exact ? show2(expected) : `${show2(expected)} (substring, ignoring case)`,
345
+ strict: true,
346
+ a11y: true,
347
+ judge: (entry, _count, ctx) => {
348
+ if (entry === undefined)
349
+ return missing();
350
+ const text = announce(ctx.element(entry), ctx.platform).text;
351
+ return judged(matchesText(text, expected, { exact }), JSON.stringify(text));
352
+ }
353
+ }, options);
354
+ }
355
+ async toBeA11yFocused(options = {}) {
356
+ assertOptions("toBeA11yFocused", options, ["timeout"]);
357
+ return this.assert({
358
+ call: "toBeA11yFocused()",
359
+ expected: "a11y-focused",
360
+ strict: true,
361
+ a11y: true,
362
+ judge: (entry, _count, ctx) => {
363
+ if (entry === undefined)
364
+ return missing();
365
+ const focused = ctx.element(entry).a11y?.a11yFocused === true;
366
+ return judged(focused, focused ? "a11y-focused" : "not a11y-focused");
367
+ }
368
+ }, options);
369
+ }
192
370
  async toHaveCount(expected, options = {}) {
193
371
  assertOptions("toHaveCount", options, ["timeout"]);
194
372
  if (!Number.isInteger(expected) || expected < 0) {
@@ -229,7 +407,8 @@ class LocatorAssertions {
229
407
  const entries = resolve(this.locator.expr, observed.index);
230
408
  if (spec.strict && entries.length > 1)
231
409
  pickOne(rt, this.locator.expr, entries, observed);
232
- const judgement = spec.judge(entries[0], entries.length);
410
+ const { seen, context } = await judgementContext(spec, observed);
411
+ const judgement = spec.judge(entries[0], entries.length, context);
233
412
  const verdict = await hold.next(judgement.kind === "judged" && judgement.matches !== negate, observed.driver);
234
413
  if (verdict === "pass")
235
414
  return { done: true, value: true };
@@ -237,7 +416,7 @@ class LocatorAssertions {
237
416
  done: false,
238
417
  state: {
239
418
  received: judgement.kind === "missing" ? NOT_FOUND : judgement.received,
240
- snapshot: observed.snapshot
419
+ snapshot: seen
241
420
  },
242
421
  ...verdict.wakeMs === undefined ? {} : { maxWaitMs: verdict.wakeMs }
243
422
  };
@@ -253,7 +432,7 @@ class LocatorAssertions {
253
432
  `Expected: ${negate ? "not " : ""}${spec.expected}`,
254
433
  `Received: ${last?.received ?? NOT_FOUND}`
255
434
  ];
256
- const screen = last?.snapshot === undefined ? undefined : this.locator.runtime.excerpt(last.snapshot);
435
+ const screen = last?.snapshot === undefined ? undefined : spec.a11y === true ? renderA11yScreen(last.snapshot) : this.locator.runtime.excerpt(last.snapshot);
257
436
  const message = screen === undefined ? lines.join(`
258
437
  `) : `${lines.join(`
259
438
  `)}
@@ -262,6 +441,19 @@ ${screen}`;
262
441
  return new E2eError2("ASSERTION", message, screen);
263
442
  }
264
443
  }
444
+ async function judgementContext(spec, observed) {
445
+ let seen = observed.snapshot;
446
+ if (spec.a11y === true) {
447
+ const merged = await withA11yRead(observed.driver, observed.snapshot);
448
+ if (merged.success)
449
+ seen = merged.data;
450
+ }
451
+ const byId = new Map(seen.flat.map((element) => [element.id, element]));
452
+ return {
453
+ seen,
454
+ context: { platform: seen.platform, element: (entry) => byId.get(entry.element.id) ?? entry.element }
455
+ };
456
+ }
265
457
  function conditionHold(negate, holdMs, now) {
266
458
  let since;
267
459
  let asked = false;
@@ -345,11 +537,22 @@ function softMatchers(target, sink) {
345
537
  }
346
538
  var NOT_STATIC = new Set(["length", "name", "prototype"]);
347
539
  function createExpect(deps) {
348
- const call = (actual, message) => isLocator(actual) ? new LocatorAssertions(actual, { negate: false }) : valueExpect(actual, message);
540
+ const call = (actual, message) => {
541
+ if (isLocator(actual))
542
+ return new LocatorAssertions(actual, { negate: false });
543
+ const target = a11yTargetOf(actual);
544
+ if (target !== undefined)
545
+ return new DeviceAssertions(target, { negate: false });
546
+ return valueExpect(actual, message);
547
+ };
349
548
  const soft = (actual, message) => {
350
549
  const sink = deps.softSink();
351
550
  if (isLocator(actual))
352
551
  return new LocatorAssertions(actual, { negate: false, ...sink === undefined ? {} : { soft: sink } });
552
+ const target = a11yTargetOf(actual);
553
+ if (target !== undefined) {
554
+ return new DeviceAssertions(target, { negate: false, ...sink === undefined ? {} : { soft: sink } });
555
+ }
353
556
  const matchers = valueExpect(actual, message);
354
557
  return sink === undefined ? matchers : softMatchers(matchers, sink);
355
558
  };
@@ -371,6 +574,25 @@ var expect = createExpect({ softSink: currentSoftSink });
371
574
  // ../../packages/e2e/src/runner/test.ts
372
575
  import { afterAll as bunAfterAll, beforeAll as bunBeforeAll, describe as bunDescribe, test as bunTest } from "bun:test";
373
576
 
577
+ // ../../packages/e2e/src/driver/requirements.ts
578
+ function staticCapabilities(target) {
579
+ return target.platform === "ios" && target.deviceKind === "physical" ? IOS_DEVICE_CAPABILITIES : undefined;
580
+ }
581
+ var met = (capabilities, requirement) => {
582
+ switch (requirement) {
583
+ case "pinch":
584
+ return capabilities.pinch;
585
+ case "multiTouch":
586
+ return capabilities.maxPointers >= 2;
587
+ }
588
+ };
589
+ function missingRequirements(target, requires) {
590
+ const capabilities = staticCapabilities(target);
591
+ if (capabilities === undefined || requires === undefined)
592
+ return [];
593
+ return requires.filter((requirement) => !met(capabilities, requirement));
594
+ }
595
+
374
596
  // ../../packages/e2e/src/runner/attempt.ts
375
597
  import { readdirSync } from "fs";
376
598
  import { join as join2 } from "path";
@@ -789,7 +1011,7 @@ async function concludeAttempt(session, spec, ctx, run, outcome, print, clock) {
789
1011
  ctx.files.add(shot);
790
1012
  const result = buildResult(spec, ctx, run, outcome.failed ? { failed: true, error: outcome.failure, timedOut: outcome.deadlineHit } : { failed: false });
791
1013
  print(formatConsoleLine({ type: "test-end", result }, { clock }));
792
- if (outcome.failed && session.recoverHid !== undefined && isHidLost(outcome.failure) && await session.recoverHid()) {
1014
+ if (outcome.failed && session.recoverHid !== undefined && isRecoverableLoss(outcome.failure, session.env.deviceKind) && await session.recoverHid()) {
793
1015
  return "again";
794
1016
  }
795
1017
  if (!outcome.failed || spec.final)
@@ -838,6 +1060,12 @@ async function setUpAndRun(session, spec, use, signal, state, teardowns) {
838
1060
  }
839
1061
  });
840
1062
  }
1063
+ const conditions = conditionsOf(base.device);
1064
+ if (conditions !== undefined) {
1065
+ teardowns.push({ name: "conditions", run: () => conditions.teardown(), when: () => conditions.dirty() });
1066
+ if (conditions.declared)
1067
+ await session.step("conditions", "conditions.baseline", () => conditions.applyBaseline());
1068
+ }
841
1069
  if (use.launch === "fresh")
842
1070
  await session.step("app", "app.open", () => base.app.open());
843
1071
  for (const entry of spec.fixtures) {
@@ -892,6 +1120,8 @@ async function runCleanup(session, spec, ctx, built, teardowns, budget, bind) {
892
1120
  }
893
1121
  }
894
1122
  for (const fixture of [...teardowns].reverse()) {
1123
+ if (fixture.when?.() === false)
1124
+ continue;
895
1125
  await attempt(`fixture ${fixture.name} teardown`, () => step("fixture", `${fixture.name} teardown`, fixture.run));
896
1126
  }
897
1127
  cleanup.abort(new E2eError2("SESSION", "attempt finished"));
@@ -921,6 +1151,7 @@ function registerTest(fixtures, mode, title, second, third) {
921
1151
  const use = resolveUse(scope, defaults, options.timeout);
922
1152
  const retry = options.retry ?? 0;
923
1153
  const wrongPlatform = options.platforms !== undefined && defaults.platform !== undefined && !options.platforms.includes(defaults.platform);
1154
+ const lacking = missingRequirements({ platform: defaults.platform, deviceKind: defaults.deviceKind }, options.requires).length > 0;
924
1155
  const only = mode === "only" || options.only === true;
925
1156
  if (only)
926
1157
  markOnly();
@@ -942,7 +1173,7 @@ function registerTest(fixtures, mode, title, second, third) {
942
1173
  });
943
1174
  };
944
1175
  const bunOptions = { timeout: bunTimeoutMs, retry };
945
- if (mode === "skip" || options.skip === true || wrongPlatform)
1176
+ if (mode === "skip" || options.skip === true || wrongPlatform || lacking)
946
1177
  bunTest.skip(title, run, bunOptions);
947
1178
  else if (only)
948
1179
  bunTest.only(title, run, bunOptions);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@delacour/warden-e2e",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "TypeScript spec API for `warden e2e`: test, expect, screen, device, app",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -37,7 +37,7 @@
37
37
  "LICENSE"
38
38
  ],
39
39
  "peerDependencies": {
40
- "@delacour/warden": "0.6.0"
40
+ "@delacour/warden": "0.8.0"
41
41
  },
42
42
  "dependencies": {
43
43
  "zod": "^4.4.3"