@delacour/warden-e2e 0.7.0 → 0.9.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 (94) hide show
  1. package/{chunk-empqr9n0.mjs → chunk-y0y7frfz.mjs} +2010 -169
  2. package/index.mjs +214 -13
  3. package/package.json +2 -2
  4. package/preload.mjs +3007 -800
  5. package/types/core/src/a11y/a11y.config.d.ts +47 -0
  6. package/types/core/src/affected/affected.schema.d.ts +49 -1
  7. package/types/core/src/android-keyguard.d.ts +2 -0
  8. package/types/core/src/batch/preset.schema.d.ts +12 -1
  9. package/types/core/src/builds/app-option.schema.d.ts +1 -1
  10. package/types/core/src/builds/config.schema.d.ts +93 -2
  11. package/types/core/src/builds/define-config.d.ts +55 -0
  12. package/types/core/src/builds/kinds.d.ts +6 -0
  13. package/types/core/src/camera-mode.d.ts +17 -0
  14. package/types/core/src/camera-placeholder.d.ts +9 -0
  15. package/types/core/src/camera-profile.d.ts +12 -0
  16. package/types/core/src/claim-errors.d.ts +34 -0
  17. package/types/core/src/conditions/a11y-keys.d.ts +45 -0
  18. package/types/core/src/conditions/camera-media.d.ts +30 -0
  19. package/types/core/src/conditions/conditions.config.d.ts +24 -0
  20. package/types/core/src/conditions/conditions.types.d.ts +30 -0
  21. package/types/core/src/conditions/inject-state.d.ts +1 -1
  22. package/types/core/src/ios-runner/build.d.ts +28 -0
  23. package/types/core/src/ios-runner/cache.d.ts +44 -0
  24. package/types/core/src/ios-runner/pack.d.ts +17 -0
  25. package/types/core/src/ios-runner/signing.d.ts +40 -0
  26. package/types/core/src/ios-runner/sim-build.d.ts +36 -0
  27. package/types/core/src/ios-runner/stage.d.ts +26 -0
  28. package/types/core/src/lease-events.d.ts +126 -0
  29. package/types/core/src/ownership.d.ts +44 -0
  30. package/types/core/src/providers/android.d.ts +10 -1
  31. package/types/core/src/providers/provider.types.d.ts +9 -0
  32. package/types/core/src/queue.d.ts +43 -0
  33. package/types/core/src/store.d.ts +61 -5
  34. package/types/core/src/types.d.ts +10 -0
  35. package/types/core/src/xcode.d.ts +22 -0
  36. package/types/e2e/src/a11y/a11y-target.d.ts +23 -0
  37. package/types/e2e/src/a11y/a11y.types.d.ts +81 -0
  38. package/types/e2e/src/a11y/announcements.d.ts +24 -0
  39. package/types/e2e/src/a11y/audit.d.ts +22 -0
  40. package/types/e2e/src/a11y/native/atf.d.ts +51 -0
  41. package/types/e2e/src/a11y/native/xcuitest.d.ts +60 -0
  42. package/types/e2e/src/a11y/order.d.ts +5 -0
  43. package/types/e2e/src/a11y/rules/duplicate-name.d.ts +6 -0
  44. package/types/e2e/src/a11y/rules/hidden-focusable.d.ts +3 -0
  45. package/types/e2e/src/a11y/rules/image-no-name.d.ts +6 -0
  46. package/types/e2e/src/a11y/rules/label-in-name.d.ts +7 -0
  47. package/types/e2e/src/a11y/rules/missing-name.d.ts +3 -0
  48. package/types/e2e/src/a11y/rules/role-in-name.d.ts +3 -0
  49. package/types/e2e/src/a11y/rules/rule.context.d.ts +22 -0
  50. package/types/e2e/src/a11y/rules/target-size.d.ts +21 -0
  51. package/types/e2e/src/a11y/rules/text-clipped.d.ts +9 -0
  52. package/types/e2e/src/a11y/speech.d.ts +26 -0
  53. package/types/e2e/src/a11y/talkback-log.d.ts +13 -0
  54. package/types/e2e/src/app.d.ts +0 -1
  55. package/types/e2e/src/conditions/condition-events.d.ts +6 -1
  56. package/types/e2e/src/conditions/device-conditions.d.ts +30 -2
  57. package/types/e2e/src/device-a11y.d.ts +97 -0
  58. package/types/e2e/src/device.d.ts +11 -2
  59. package/types/e2e/src/driver/a11y-events.d.ts +30 -0
  60. package/types/e2e/src/driver/a11y-read.d.ts +12 -0
  61. package/types/e2e/src/driver/android/android-animations.d.ts +7 -0
  62. package/types/e2e/src/driver/android/android-argv.d.ts +2 -0
  63. package/types/e2e/src/driver/android/android-camera-host.d.ts +23 -0
  64. package/types/e2e/src/driver/android/android-camera.d.ts +28 -0
  65. package/types/e2e/src/driver/android/android-driver.d.ts +5 -0
  66. package/types/e2e/src/driver/android/android-screen-reader.d.ts +23 -0
  67. package/types/e2e/src/driver/android/android-settings.d.ts +9 -2
  68. package/types/e2e/src/driver/android/android-spoken.d.ts +18 -0
  69. package/types/e2e/src/driver/android/android-talkback.d.ts +57 -0
  70. package/types/e2e/src/driver/android/atf-device.d.ts +23 -0
  71. package/types/e2e/src/driver/android/atf-dex.generated.d.ts +7 -0
  72. package/types/e2e/src/driver/android/uiad-dex.generated.d.ts +5 -5
  73. package/types/e2e/src/driver/android/uiad-ops.d.ts +27 -1
  74. package/types/e2e/src/driver/android/uiad-protocol.d.ts +8 -0
  75. package/types/e2e/src/driver/android/uiad-screen-reader.d.ts +33 -0
  76. package/types/e2e/src/driver/driver.types.d.ts +71 -5
  77. package/types/e2e/src/driver/element-actions.d.ts +16 -0
  78. package/types/e2e/src/driver/ios/inject.d.ts +16 -2
  79. package/types/e2e/src/driver/ios/ios-a11y.d.ts +73 -0
  80. package/types/e2e/src/driver/ios/ios-audit.d.ts +36 -0
  81. package/types/e2e/src/driver/ios/ios-driver.d.ts +21 -1
  82. package/types/e2e/src/driver/ios/ios-network.d.ts +7 -2
  83. package/types/e2e/src/driver/ios/sim-runner.d.ts +41 -0
  84. package/types/e2e/src/driver/ios/simd-transport.d.ts +11 -2
  85. package/types/e2e/src/driver/native-audit.d.ts +26 -0
  86. package/types/e2e/src/driver/physical-conditions.d.ts +1 -1
  87. package/types/e2e/src/driver/screen-reader.d.ts +26 -0
  88. package/types/e2e/src/env.d.ts +16 -1
  89. package/types/e2e/src/expect/a11y-matchers.d.ts +33 -0
  90. package/types/e2e/src/expect/expect.d.ts +7 -2
  91. package/types/e2e/src/expect/matchers.d.ts +14 -0
  92. package/types/e2e/src/index.d.ts +5 -1
  93. package/types/e2e/src/runner/callsite.d.ts +3 -1
  94. package/types/e2e/src/screen/render.d.ts +8 -0
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,
@@ -15,14 +16,21 @@ import {
15
16
  runStep,
16
17
  attemptSession,
17
18
  getSession,
19
+ accessibleName,
20
+ announce,
21
+ failing,
22
+ formatIssues,
23
+ toE2eError,
24
+ withA11yRead,
25
+ renderA11yScreen,
18
26
  IOS_DEVICE_CAPABILITIES,
27
+ callerFile,
19
28
  conditionsOf,
20
29
  perfStartFor,
21
30
  hash6,
22
31
  attemptDir,
23
32
  specIdOf,
24
33
  captureNativeLog,
25
- callerFile,
26
34
  BUN_TIMEOUT_MARGIN_MS2,
27
35
  ownHook,
28
36
  currentScope,
@@ -33,10 +41,106 @@ import {
33
41
  registrationDefaults,
34
42
  markOnly,
35
43
  WARDEN_E2E_VERSION2
36
- } from "./chunk-empqr9n0.mjs";
44
+ } from "./chunk-y0y7frfz.mjs";
37
45
  // ../../packages/e2e/src/expect/expect.ts
38
46
  import { expect as bunExpect } from "bun:test";
39
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
+
40
144
  // ../../packages/e2e/src/expect/matchers.ts
41
145
  var NEGATION_GRACE_MS = 1000;
42
146
  var NEGATION_QUIET_MS = 250;
@@ -47,7 +151,7 @@ function missing() {
47
151
  function judged(matches, received) {
48
152
  return { kind: "judged", matches, received };
49
153
  }
50
- function show(match) {
154
+ function show2(match) {
51
155
  return typeof match === "string" ? JSON.stringify(match) : String(match);
52
156
  }
53
157
  function assertOptions(matcher, options, allowed) {
@@ -114,8 +218,8 @@ class LocatorAssertions {
114
218
  assertOptions("toHaveText", options, ["exact", "timeout"]);
115
219
  const exact = options.exact ?? true;
116
220
  return this.assert({
117
- call: `toHaveText(${show(expected)})`,
118
- expected: exact ? show(expected) : `${show(expected)} (substring, ignoring case)`,
221
+ call: `toHaveText(${show2(expected)})`,
222
+ expected: exact ? show2(expected) : `${show2(expected)} (substring, ignoring case)`,
119
223
  strict: true,
120
224
  judge: (entry) => {
121
225
  if (entry === undefined)
@@ -129,8 +233,8 @@ class LocatorAssertions {
129
233
  assertOptions("toContainText", options, ["ignoreCase", "timeout"]);
130
234
  const ignoreCase = options.ignoreCase ?? false;
131
235
  return this.assert({
132
- call: `toContainText(${show(expected)})`,
133
- expected: `text containing ${show(expected)}`,
236
+ call: `toContainText(${show2(expected)})`,
237
+ expected: `text containing ${show2(expected)}`,
134
238
  strict: true,
135
239
  judge: (entry) => {
136
240
  if (entry === undefined)
@@ -143,8 +247,8 @@ class LocatorAssertions {
143
247
  async toHaveValue(expected, options = {}) {
144
248
  assertOptions("toHaveValue", options, ["timeout"]);
145
249
  return this.assert({
146
- call: `toHaveValue(${show(expected)})`,
147
- expected: show(expected),
250
+ call: `toHaveValue(${show2(expected)})`,
251
+ expected: show2(expected),
148
252
  strict: true,
149
253
  judge: (entry) => {
150
254
  if (entry === undefined)
@@ -191,6 +295,78 @@ class LocatorAssertions {
191
295
  judge: (entry) => entry === undefined ? missing() : judged(entry.states.disabled, entry.states.disabled ? "disabled" : "enabled")
192
296
  }, options);
193
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
+ }
194
370
  async toHaveCount(expected, options = {}) {
195
371
  assertOptions("toHaveCount", options, ["timeout"]);
196
372
  if (!Number.isInteger(expected) || expected < 0) {
@@ -231,7 +407,8 @@ class LocatorAssertions {
231
407
  const entries = resolve(this.locator.expr, observed.index);
232
408
  if (spec.strict && entries.length > 1)
233
409
  pickOne(rt, this.locator.expr, entries, observed);
234
- 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);
235
412
  const verdict = await hold.next(judgement.kind === "judged" && judgement.matches !== negate, observed.driver);
236
413
  if (verdict === "pass")
237
414
  return { done: true, value: true };
@@ -239,7 +416,7 @@ class LocatorAssertions {
239
416
  done: false,
240
417
  state: {
241
418
  received: judgement.kind === "missing" ? NOT_FOUND : judgement.received,
242
- snapshot: observed.snapshot
419
+ snapshot: seen
243
420
  },
244
421
  ...verdict.wakeMs === undefined ? {} : { maxWaitMs: verdict.wakeMs }
245
422
  };
@@ -255,7 +432,7 @@ class LocatorAssertions {
255
432
  `Expected: ${negate ? "not " : ""}${spec.expected}`,
256
433
  `Received: ${last?.received ?? NOT_FOUND}`
257
434
  ];
258
- 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);
259
436
  const message = screen === undefined ? lines.join(`
260
437
  `) : `${lines.join(`
261
438
  `)}
@@ -264,6 +441,19 @@ ${screen}`;
264
441
  return new E2eError2("ASSERTION", message, screen);
265
442
  }
266
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
+ }
267
457
  function conditionHold(negate, holdMs, now) {
268
458
  let since;
269
459
  let asked = false;
@@ -347,11 +537,22 @@ function softMatchers(target, sink) {
347
537
  }
348
538
  var NOT_STATIC = new Set(["length", "name", "prototype"]);
349
539
  function createExpect(deps) {
350
- 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
+ };
351
548
  const soft = (actual, message) => {
352
549
  const sink = deps.softSink();
353
550
  if (isLocator(actual))
354
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
+ }
355
556
  const matchers = valueExpect(actual, message);
356
557
  return sink === undefined ? matchers : softMatchers(matchers, sink);
357
558
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@delacour/warden-e2e",
3
- "version": "0.7.0",
3
+ "version": "0.9.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.7.0"
40
+ "@delacour/warden": "0.9.0"
41
41
  },
42
42
  "dependencies": {
43
43
  "zod": "^4.4.3"