@scenetest/scenes 0.2.0 → 0.4.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 (63) hide show
  1. package/dist/__tests__/dsl.test.js +20 -0
  2. package/dist/__tests__/dsl.test.js.map +1 -1
  3. package/dist/__tests__/keyboard.test.d.ts +2 -0
  4. package/dist/__tests__/keyboard.test.d.ts.map +1 -0
  5. package/dist/__tests__/keyboard.test.js +332 -0
  6. package/dist/__tests__/keyboard.test.js.map +1 -0
  7. package/dist/__tests__/markdown-scene.test.js +85 -5
  8. package/dist/__tests__/markdown-scene.test.js.map +1 -1
  9. package/dist/__tests__/reactive.test.js +259 -6
  10. package/dist/__tests__/reactive.test.js.map +1 -1
  11. package/dist/__tests__/runner.test.d.ts +2 -0
  12. package/dist/__tests__/runner.test.d.ts.map +1 -0
  13. package/dist/__tests__/runner.test.js +182 -0
  14. package/dist/__tests__/runner.test.js.map +1 -0
  15. package/dist/actor.d.ts +21 -4
  16. package/dist/actor.d.ts.map +1 -1
  17. package/dist/actor.js +176 -12
  18. package/dist/actor.js.map +1 -1
  19. package/dist/builtin-macros.d.ts +87 -0
  20. package/dist/builtin-macros.d.ts.map +1 -0
  21. package/dist/builtin-macros.js +154 -0
  22. package/dist/builtin-macros.js.map +1 -0
  23. package/dist/cli.js +12 -0
  24. package/dist/cli.js.map +1 -1
  25. package/dist/devices.d.ts +6 -0
  26. package/dist/devices.d.ts.map +1 -1
  27. package/dist/devices.js +13 -0
  28. package/dist/devices.js.map +1 -1
  29. package/dist/dsl.d.ts +4 -0
  30. package/dist/dsl.d.ts.map +1 -1
  31. package/dist/dsl.js +18 -1
  32. package/dist/dsl.js.map +1 -1
  33. package/dist/index.d.ts +5 -2
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +5 -1
  36. package/dist/index.js.map +1 -1
  37. package/dist/keyboard.d.ts +168 -0
  38. package/dist/keyboard.d.ts.map +1 -0
  39. package/dist/keyboard.js +370 -0
  40. package/dist/keyboard.js.map +1 -0
  41. package/dist/markdown-scene.d.ts +4 -2
  42. package/dist/markdown-scene.d.ts.map +1 -1
  43. package/dist/markdown-scene.js +21 -9
  44. package/dist/markdown-scene.js.map +1 -1
  45. package/dist/reactive.d.ts +25 -2
  46. package/dist/reactive.d.ts.map +1 -1
  47. package/dist/reactive.js +163 -16
  48. package/dist/reactive.js.map +1 -1
  49. package/dist/runner.d.ts +11 -5
  50. package/dist/runner.d.ts.map +1 -1
  51. package/dist/runner.js +141 -35
  52. package/dist/runner.js.map +1 -1
  53. package/dist/swarm.d.ts +1 -1
  54. package/dist/swarm.d.ts.map +1 -1
  55. package/dist/swarm.js +8 -5
  56. package/dist/swarm.js.map +1 -1
  57. package/dist/team-manager.d.ts +48 -3
  58. package/dist/team-manager.d.ts.map +1 -1
  59. package/dist/team-manager.js +150 -15
  60. package/dist/team-manager.js.map +1 -1
  61. package/dist/types.d.ts +133 -2
  62. package/dist/types.d.ts.map +1 -1
  63. package/package.json +3 -3
@@ -0,0 +1,168 @@
1
+ import type { Page, Locator } from 'playwright';
2
+ /**
3
+ * Navigation mode for an actor.
4
+ *
5
+ * - `'pointer'` — mouse/touch interaction. When fuzzy-fingers is enabled,
6
+ * clicks occasionally miss the target first, pause, then click correctly
7
+ * (simulating imprecise human touch input).
8
+ *
9
+ * - `'keyboard'` — navigate entirely via Tab key and activate via Enter/Space.
10
+ * Tests that the app is keyboard-accessible.
11
+ *
12
+ * All modes are transparent to test authors. The same `click('submit')` call
13
+ * works in every mode.
14
+ */
15
+ export type NavigationMode = 'pointer' | 'keyboard';
16
+ /**
17
+ * Options for tabToElement
18
+ */
19
+ interface TabToElementOptions {
20
+ /** Maximum number of Tab presses before giving up. Default: 100. */
21
+ maxTabs?: number;
22
+ /** Action timeout in ms (for the overall operation). */
23
+ timeout?: number;
24
+ }
25
+ /**
26
+ * Tab forward through the page until the target element (or a focusable
27
+ * descendant of it) receives focus.
28
+ *
29
+ * This simulates a real keyboard user pressing Tab repeatedly to reach
30
+ * an interactive element. If the element can't be reached after `maxTabs`
31
+ * presses, it throws — indicating a keyboard-accessibility problem.
32
+ *
33
+ * @param page - Playwright Page
34
+ * @param target - Locator for the element to reach
35
+ * @param opts - Options (maxTabs, timeout)
36
+ */
37
+ export declare function tabToElement(page: Page, target: Locator, opts?: TabToElementOptions): Promise<void>;
38
+ /**
39
+ * Press Enter on the currently focused element.
40
+ */
41
+ export declare function pressEnter(page: Page): Promise<void>;
42
+ /**
43
+ * Press Space on the currently focused element.
44
+ * Used for toggling checkboxes and activating certain controls.
45
+ */
46
+ export declare function pressSpace(page: Page): Promise<void>;
47
+ /**
48
+ * Type text character by character into the currently focused element.
49
+ * Clears existing content first by selecting all.
50
+ */
51
+ export declare function clearAndType(page: Page, value: string): Promise<void>;
52
+ /**
53
+ * Navigate a <select> element's options using arrow keys.
54
+ * Assumes the select is already focused.
55
+ *
56
+ * Strategy: open the dropdown, then press ArrowDown/ArrowUp to reach
57
+ * the desired option, then press Enter to select it.
58
+ *
59
+ * For simplicity, we use a hybrid: the select is focused via Tab,
60
+ * then we use Playwright's selectOption which works via the browser
61
+ * API. This is a pragmatic choice — real keyboard users interact
62
+ * with selects differently across browsers.
63
+ */
64
+ export declare function keyboardSelectOption(page: Page, target: Locator, value: string): Promise<void>;
65
+ /**
66
+ * Fuzzy-finger strategy, alternated per interaction:
67
+ *
68
+ * - `'miss-center'` — clicks 15px from the element's center instead of
69
+ * dead-center. Tests WCAG 2.5.8 minimum target size (24×24 CSS-px).
70
+ *
71
+ * - `'miss-edge'` — clicks a few pixels *outside* the element's bounding
72
+ * box. Tests touch-target *spacing* between neighbors.
73
+ *
74
+ * Both strategies follow the same flow: miss → pause → correct click.
75
+ * If the correct click succeeds, we move on silently (humans miss all
76
+ * the time). If the correct click fails because the element vanished
77
+ * (the mis-click activated something else), we throw FuzzyFingerError.
78
+ */
79
+ type FuzzyFingerStrategy = 'miss-center' | 'miss-edge';
80
+ /**
81
+ * Thrown when a fuzzy-finger mis-click caused the target element to
82
+ * vanish (because the mis-click activated a neighboring element that
83
+ * navigated away or altered the DOM).
84
+ *
85
+ * This is a real UX problem: touch targets are too close together.
86
+ * The `strategy` field tells you which test surfaced the issue:
87
+ * - `'miss-center'` → element is undersized (WCAG 2.5.8)
88
+ * - `'miss-edge'` → element is too close to a neighbor
89
+ */
90
+ export declare class FuzzyFingerError extends Error {
91
+ readonly strategy: FuzzyFingerStrategy;
92
+ readonly selector: string;
93
+ readonly originalError: Error;
94
+ constructor(strategy: FuzzyFingerStrategy, selector: string, originalError: Error);
95
+ }
96
+ /**
97
+ * Fuzzy-finger click: miss → pause → correct click.
98
+ *
99
+ * Simulates imprecise human touch input. The mis-click lands either
100
+ * 15px from center (testing target size) or just outside the bounding
101
+ * box (testing target spacing), alternating between strategies.
102
+ *
103
+ * After the mis-click, pauses ~1s (like a human noticing the miss),
104
+ * then clicks the correct element. If the correct click succeeds,
105
+ * we move on silently — humans miss all the time, no big deal.
106
+ *
107
+ * If the correct click *fails* (element vanished because the mis-click
108
+ * activated a neighbor), we throw FuzzyFingerError — that's a real
109
+ * touch-target problem in the UI.
110
+ */
111
+ export declare function fuzzyFingerClick(page: Page, target: Locator, timeout: number, selector?: string): Promise<void>;
112
+ /**
113
+ * Fuzzy-finger fill: miss → pause → correct click → fill.
114
+ */
115
+ export declare function fuzzyFingerFill(page: Page, target: Locator, value: string, timeout: number, selector?: string): Promise<void>;
116
+ /**
117
+ * Fuzzy-finger check: miss → pause → correct check.
118
+ */
119
+ export declare function fuzzyFingerCheck(page: Page, target: Locator, timeout: number, selector?: string): Promise<void>;
120
+ /**
121
+ * Assigns navigation modes to actors via round-robin rotation.
122
+ *
123
+ * The default pool alternates: pointer, keyboard.
124
+ * Fuzzy-finger behavior is controlled separately and applies to
125
+ * pointer-mode actors.
126
+ *
127
+ * @example
128
+ * ```ts
129
+ * const rotation = new NavigationModeRotation()
130
+ * rotation.next() // 'pointer'
131
+ * rotation.next() // 'keyboard'
132
+ * rotation.next() // 'pointer'
133
+ * ```
134
+ *
135
+ * @example Keyboard only disabled
136
+ * ```ts
137
+ * const rotation = new NavigationModeRotation(['pointer'])
138
+ * rotation.next() // 'pointer'
139
+ * rotation.next() // 'pointer'
140
+ * ```
141
+ */
142
+ export declare class NavigationModeRotation {
143
+ private pool;
144
+ private index;
145
+ constructor(modes?: NavigationMode[]);
146
+ /**
147
+ * Get the next navigation mode in rotation.
148
+ */
149
+ next(): NavigationMode;
150
+ /**
151
+ * Peek at the next mode without advancing.
152
+ */
153
+ peek(): NavigationMode;
154
+ /**
155
+ * Reset rotation to the beginning.
156
+ */
157
+ reset(): void;
158
+ /**
159
+ * Current rotation index (for reporting).
160
+ */
161
+ get currentIndex(): number;
162
+ /**
163
+ * The full mode pool.
164
+ */
165
+ get modes(): readonly NavigationMode[];
166
+ }
167
+ export {};
168
+ //# sourceMappingURL=keyboard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keyboard.d.ts","sourceRoot":"","sources":["../src/keyboard.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,YAAY,CAAA;AAE/C;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,cAAc,GAAG,SAAS,GAAG,UAAU,CAAA;AAEnD;;GAEG;AACH,UAAU,mBAAmB;IAC3B,oEAAoE;IACpE,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,wDAAwD;IACxD,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,YAAY,CAChC,IAAI,EAAE,IAAI,EACV,MAAM,EAAE,OAAO,EACf,IAAI,GAAE,mBAAwB,GAC7B,OAAO,CAAC,IAAI,CAAC,CAuDf;AAED;;GAEG;AACH,wBAAsB,UAAU,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAE1D;AAED;;;GAGG;AACH,wBAAsB,UAAU,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAE1D;AAED;;;GAGG;AACH,wBAAsB,YAAY,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAM3E;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,oBAAoB,CACxC,IAAI,EAAE,IAAI,EACV,MAAM,EAAE,OAAO,EACf,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,IAAI,CAAC,CAMf;AAMD;;;;;;;;;;;;;GAaG;AACH,KAAK,mBAAmB,GAAG,aAAa,GAAG,WAAW,CAAA;AA+FtD;;;;;;;;;GASG;AACH,qBAAa,gBAAiB,SAAQ,KAAK;IACzC,QAAQ,CAAC,QAAQ,EAAE,mBAAmB,CAAA;IACtC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,aAAa,EAAE,KAAK,CAAA;gBAEjB,QAAQ,EAAE,mBAAmB,EAAE,QAAQ,EAAE,MAAM,EAAE,aAAa,EAAE,KAAK;CAUlF;AAID;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,IAAI,EACV,MAAM,EAAE,OAAO,EACf,OAAO,EAAE,MAAM,EACf,QAAQ,SAAY,GACnB,OAAO,CAAC,IAAI,CAAC,CAoCf;AAED;;GAEG;AACH,wBAAsB,eAAe,CACnC,IAAI,EAAE,IAAI,EACV,MAAM,EAAE,OAAO,EACf,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,MAAM,EACf,QAAQ,SAAY,GACnB,OAAO,CAAC,IAAI,CAAC,CA0Bf;AAED;;GAEG;AACH,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,IAAI,EACV,MAAM,EAAE,OAAO,EACf,OAAO,EAAE,MAAM,EACf,QAAQ,SAAY,GACnB,OAAO,CAAC,IAAI,CAAC,CA0Bf;AAMD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,qBAAa,sBAAsB;IACjC,OAAO,CAAC,IAAI,CAAkB;IAC9B,OAAO,CAAC,KAAK,CAAI;gBAEL,KAAK,CAAC,EAAE,cAAc,EAAE;IAIpC;;OAEG;IACH,IAAI,IAAI,cAAc;IAMtB;;OAEG;IACH,IAAI,IAAI,cAAc;IAItB;;OAEG;IACH,KAAK,IAAI,IAAI;IAIb;;OAEG;IACH,IAAI,YAAY,IAAI,MAAM,CAEzB;IAED;;OAEG;IACH,IAAI,KAAK,IAAI,SAAS,cAAc,EAAE,CAErC;CACF"}
@@ -0,0 +1,370 @@
1
+ /**
2
+ * Tab forward through the page until the target element (or a focusable
3
+ * descendant of it) receives focus.
4
+ *
5
+ * This simulates a real keyboard user pressing Tab repeatedly to reach
6
+ * an interactive element. If the element can't be reached after `maxTabs`
7
+ * presses, it throws — indicating a keyboard-accessibility problem.
8
+ *
9
+ * @param page - Playwright Page
10
+ * @param target - Locator for the element to reach
11
+ * @param opts - Options (maxTabs, timeout)
12
+ */
13
+ export async function tabToElement(page, target, opts = {}) {
14
+ const maxTabs = opts.maxTabs ?? 100;
15
+ const timeout = opts.timeout ?? 10000;
16
+ const deadline = Date.now() + timeout;
17
+ // Get a handle to the target element so we can compare in-page
18
+ const targetHandle = await target.elementHandle({ timeout: Math.min(5000, timeout) });
19
+ if (!targetHandle) {
20
+ throw new Error('Keyboard navigation: target element not found in DOM');
21
+ }
22
+ // Track where we started to detect full cycles
23
+ let startElement = null;
24
+ for (let i = 0; i < maxTabs; i++) {
25
+ if (Date.now() > deadline) {
26
+ throw new Error(`Keyboard navigation: timed out after ${timeout}ms trying to Tab to element`);
27
+ }
28
+ await page.keyboard.press('Tab');
29
+ // Check if focus landed on or within the target element
30
+ const result = await targetHandle.evaluate((el) => {
31
+ const active = document.activeElement;
32
+ if (!active || active === document.body)
33
+ return 'nobody';
34
+ if (el === active || el.contains(active))
35
+ return 'found';
36
+ // Return a stable identifier so we can detect cycles
37
+ return active.tagName + active.id;
38
+ });
39
+ if (result === 'found') {
40
+ return; // Successfully tabbed to element
41
+ }
42
+ // Detect full cycle (focus wrapped back to start)
43
+ if (i === 0) {
44
+ startElement = result;
45
+ }
46
+ else if (i > 2 && result === startElement) {
47
+ throw new Error('Keyboard navigation: Tab focus cycled back to the starting element without ' +
48
+ 'reaching the target. The element may not be keyboard-accessible ' +
49
+ '(missing tabindex, not a natively focusable element, or hidden from tab order).');
50
+ }
51
+ }
52
+ throw new Error(`Keyboard navigation: could not reach element via Tab after ${maxTabs} key presses. ` +
53
+ 'The element may not be keyboard-accessible (missing tabindex, not a ' +
54
+ 'natively focusable element, or the page has too many focusable elements).');
55
+ }
56
+ /**
57
+ * Press Enter on the currently focused element.
58
+ */
59
+ export async function pressEnter(page) {
60
+ await page.keyboard.press('Enter');
61
+ }
62
+ /**
63
+ * Press Space on the currently focused element.
64
+ * Used for toggling checkboxes and activating certain controls.
65
+ */
66
+ export async function pressSpace(page) {
67
+ await page.keyboard.press('Space');
68
+ }
69
+ /**
70
+ * Type text character by character into the currently focused element.
71
+ * Clears existing content first by selecting all.
72
+ */
73
+ export async function clearAndType(page, value) {
74
+ // Select all existing content and delete it
75
+ await page.keyboard.press('Control+a');
76
+ await page.keyboard.press('Backspace');
77
+ // Type the new value character by character
78
+ await page.keyboard.type(value);
79
+ }
80
+ /**
81
+ * Navigate a <select> element's options using arrow keys.
82
+ * Assumes the select is already focused.
83
+ *
84
+ * Strategy: open the dropdown, then press ArrowDown/ArrowUp to reach
85
+ * the desired option, then press Enter to select it.
86
+ *
87
+ * For simplicity, we use a hybrid: the select is focused via Tab,
88
+ * then we use Playwright's selectOption which works via the browser
89
+ * API. This is a pragmatic choice — real keyboard users interact
90
+ * with selects differently across browsers.
91
+ */
92
+ export async function keyboardSelectOption(page, target, value) {
93
+ // The element is already focused via Tab. Use selectOption which
94
+ // works via the browser's native select API. This is consistent
95
+ // across browsers and matches what keyboard users experience when
96
+ // interacting with native selects (which vary by platform).
97
+ await target.selectOption(value);
98
+ }
99
+ /** Global counter — alternates strategy on every fuzzy-finger interaction. */
100
+ let fuzzyFingerCounter = 0;
101
+ function nextStrategy() {
102
+ return fuzzyFingerCounter++ % 2 === 0 ? 'miss-center' : 'miss-edge';
103
+ }
104
+ // -- miss-center helpers ----------------------------------------------------
105
+ /**
106
+ * Distance from center for the miss-center strategy (px).
107
+ *
108
+ * WCAG 2.5.8 requires a minimum 24×24 CSS-px target size, so the
109
+ * center-to-edge distance is 12px. Clicking 15px from center will
110
+ * miss a compliant-minimum element, surfacing undersized targets.
111
+ */
112
+ const CENTER_OFFSET_PX = 15;
113
+ /**
114
+ * Pick a random point that is CENTER_OFFSET_PX away from the
115
+ * element's center, in a random direction.
116
+ */
117
+ function missCenterPoint(box) {
118
+ const cx = box.x + box.width / 2;
119
+ const cy = box.y + box.height / 2;
120
+ const angle = Math.random() * 2 * Math.PI;
121
+ return {
122
+ x: cx + CENTER_OFFSET_PX * Math.cos(angle),
123
+ y: cy + CENTER_OFFSET_PX * Math.sin(angle),
124
+ };
125
+ }
126
+ // -- miss-edge helpers ------------------------------------------------------
127
+ /**
128
+ * Overshoot distance beyond the element edge (px).
129
+ *
130
+ * We land 3px *outside* the bounding box. If the element meets the
131
+ * WCAG minimum (12px from center to edge), the mis-tap ends up ~15px
132
+ * from center — outside the target but close enough to hit a neighbor
133
+ * that is packed too tightly.
134
+ */
135
+ const EDGE_OVERSHOOT_PX = 3;
136
+ /**
137
+ * Pick a random point just outside the element's bounding box.
138
+ *
139
+ * Strategy: pick a random edge (top / right / bottom / left),
140
+ * place the coordinate EDGE_OVERSHOOT_PX beyond that edge, and
141
+ * randomize position along the edge so we don't always hit the
142
+ * same neighbor.
143
+ */
144
+ function missEdgePoint(box) {
145
+ const edge = Math.floor(Math.random() * 4); // 0=top, 1=right, 2=bottom, 3=left
146
+ const t = 0.2 + Math.random() * 0.6; // bias toward center of edge, not corners
147
+ switch (edge) {
148
+ case 0: // top — above the element
149
+ return { x: box.x + box.width * t, y: box.y - EDGE_OVERSHOOT_PX };
150
+ case 1: // right — past the right edge
151
+ return { x: box.x + box.width + EDGE_OVERSHOOT_PX, y: box.y + box.height * t };
152
+ case 2: // bottom — below the element
153
+ return { x: box.x + box.width * t, y: box.y + box.height + EDGE_OVERSHOOT_PX };
154
+ case 3: // left — past the left edge
155
+ default:
156
+ return { x: box.x - EDGE_OVERSHOOT_PX, y: box.y + box.height * t };
157
+ }
158
+ }
159
+ // -- shared helpers ---------------------------------------------------------
160
+ /**
161
+ * Probability of a mis-click on any given interaction.
162
+ * ~20% (1 in 5) — most clicks go through cleanly, but every now and
163
+ * then one goes slightly astray.
164
+ */
165
+ const MISS_PROBABILITY = 0.2;
166
+ /**
167
+ * Returns true if this interaction should be a mis-click.
168
+ */
169
+ function shouldMiss() {
170
+ return Math.random() < MISS_PROBABILITY;
171
+ }
172
+ /**
173
+ * Pause duration (ms) after a mis-click before the correction.
174
+ * Short pause — human noticing the miss and re-tapping.
175
+ */
176
+ const FUZZY_PAUSE_MS = 100;
177
+ // -- diagnostic error -------------------------------------------------------
178
+ /**
179
+ * Thrown when a fuzzy-finger mis-click caused the target element to
180
+ * vanish (because the mis-click activated a neighboring element that
181
+ * navigated away or altered the DOM).
182
+ *
183
+ * This is a real UX problem: touch targets are too close together.
184
+ * The `strategy` field tells you which test surfaced the issue:
185
+ * - `'miss-center'` → element is undersized (WCAG 2.5.8)
186
+ * - `'miss-edge'` → element is too close to a neighbor
187
+ */
188
+ export class FuzzyFingerError extends Error {
189
+ strategy;
190
+ selector;
191
+ originalError;
192
+ constructor(strategy, selector, originalError) {
193
+ const reason = strategy === 'miss-center'
194
+ ? `target too small (mis-click ${CENTER_OFFSET_PX}px from center hit something else — WCAG 2.5.8 requires minimum 24×24 CSS-px)`
195
+ : `target too close to neighbor (mis-click ${EDGE_OVERSHOOT_PX}px outside edge activated adjacent element)`;
196
+ super(`Fuzzy-finger failure on "${selector}": ${reason}`);
197
+ this.name = 'FuzzyFingerError';
198
+ this.strategy = strategy;
199
+ this.selector = selector;
200
+ this.originalError = originalError;
201
+ }
202
+ }
203
+ // -- exported fuzzy-finger actions ------------------------------------------
204
+ /**
205
+ * Fuzzy-finger click: miss → pause → correct click.
206
+ *
207
+ * Simulates imprecise human touch input. The mis-click lands either
208
+ * 15px from center (testing target size) or just outside the bounding
209
+ * box (testing target spacing), alternating between strategies.
210
+ *
211
+ * After the mis-click, pauses ~1s (like a human noticing the miss),
212
+ * then clicks the correct element. If the correct click succeeds,
213
+ * we move on silently — humans miss all the time, no big deal.
214
+ *
215
+ * If the correct click *fails* (element vanished because the mis-click
216
+ * activated a neighbor), we throw FuzzyFingerError — that's a real
217
+ * touch-target problem in the UI.
218
+ */
219
+ export async function fuzzyFingerClick(page, target, timeout, selector = '(scope)') {
220
+ await target.waitFor({ state: 'visible', timeout });
221
+ // ~1 in 5 clicks will miss — most go through cleanly
222
+ if (shouldMiss()) {
223
+ const strategy = nextStrategy();
224
+ const box = await target.boundingBox({ timeout });
225
+ if (box) {
226
+ // Step 1: Miss click
227
+ const missPoint = strategy === 'miss-center' ? missCenterPoint(box) : missEdgePoint(box);
228
+ await page.mouse.click(missPoint.x, missPoint.y);
229
+ // Step 2: Pause (human noticing the miss)
230
+ await new Promise(resolve => setTimeout(resolve, FUZZY_PAUSE_MS));
231
+ }
232
+ // Step 3: Correct click — this is where we detect problems
233
+ try {
234
+ await target.click({ timeout });
235
+ }
236
+ catch (err) {
237
+ // The correct click failed. Did the element vanish because our
238
+ // mis-click activated something else?
239
+ const stillExists = await target.count() > 0;
240
+ if (!stillExists) {
241
+ // Element is gone — the mis-click caused navigation or DOM change
242
+ throw new FuzzyFingerError(strategy, selector, err instanceof Error ? err : new Error(String(err)));
243
+ }
244
+ // Element still exists but click failed for another reason — real bug
245
+ throw err;
246
+ }
247
+ return;
248
+ }
249
+ // Normal click (no mis-click this time)
250
+ await target.click({ timeout });
251
+ }
252
+ /**
253
+ * Fuzzy-finger fill: miss → pause → correct click → fill.
254
+ */
255
+ export async function fuzzyFingerFill(page, target, value, timeout, selector = '(scope)') {
256
+ await target.waitFor({ state: 'visible', timeout });
257
+ if (shouldMiss()) {
258
+ const strategy = nextStrategy();
259
+ const box = await target.boundingBox({ timeout });
260
+ if (box) {
261
+ const missPoint = strategy === 'miss-center' ? missCenterPoint(box) : missEdgePoint(box);
262
+ await page.mouse.click(missPoint.x, missPoint.y);
263
+ await new Promise(resolve => setTimeout(resolve, FUZZY_PAUSE_MS));
264
+ }
265
+ try {
266
+ await target.fill(value, { timeout });
267
+ }
268
+ catch (err) {
269
+ const stillExists = await target.count() > 0;
270
+ if (!stillExists) {
271
+ throw new FuzzyFingerError(strategy, selector, err instanceof Error ? err : new Error(String(err)));
272
+ }
273
+ throw err;
274
+ }
275
+ return;
276
+ }
277
+ await target.fill(value, { timeout });
278
+ }
279
+ /**
280
+ * Fuzzy-finger check: miss → pause → correct check.
281
+ */
282
+ export async function fuzzyFingerCheck(page, target, timeout, selector = '(scope)') {
283
+ await target.waitFor({ state: 'visible', timeout });
284
+ if (shouldMiss()) {
285
+ const strategy = nextStrategy();
286
+ const box = await target.boundingBox({ timeout });
287
+ if (box) {
288
+ const missPoint = strategy === 'miss-center' ? missCenterPoint(box) : missEdgePoint(box);
289
+ await page.mouse.click(missPoint.x, missPoint.y);
290
+ await new Promise(resolve => setTimeout(resolve, FUZZY_PAUSE_MS));
291
+ }
292
+ try {
293
+ await target.check({ timeout });
294
+ }
295
+ catch (err) {
296
+ const stillExists = await target.count() > 0;
297
+ if (!stillExists) {
298
+ throw new FuzzyFingerError(strategy, selector, err instanceof Error ? err : new Error(String(err)));
299
+ }
300
+ throw err;
301
+ }
302
+ return;
303
+ }
304
+ await target.check({ timeout });
305
+ }
306
+ // ---------------------------------------------------------------------------
307
+ // NavigationModeRotation
308
+ // ---------------------------------------------------------------------------
309
+ /**
310
+ * Assigns navigation modes to actors via round-robin rotation.
311
+ *
312
+ * The default pool alternates: pointer, keyboard.
313
+ * Fuzzy-finger behavior is controlled separately and applies to
314
+ * pointer-mode actors.
315
+ *
316
+ * @example
317
+ * ```ts
318
+ * const rotation = new NavigationModeRotation()
319
+ * rotation.next() // 'pointer'
320
+ * rotation.next() // 'keyboard'
321
+ * rotation.next() // 'pointer'
322
+ * ```
323
+ *
324
+ * @example Keyboard only disabled
325
+ * ```ts
326
+ * const rotation = new NavigationModeRotation(['pointer'])
327
+ * rotation.next() // 'pointer'
328
+ * rotation.next() // 'pointer'
329
+ * ```
330
+ */
331
+ export class NavigationModeRotation {
332
+ pool;
333
+ index = 0;
334
+ constructor(modes) {
335
+ this.pool = modes && modes.length > 0 ? modes : ['pointer', 'keyboard'];
336
+ }
337
+ /**
338
+ * Get the next navigation mode in rotation.
339
+ */
340
+ next() {
341
+ const mode = this.pool[this.index % this.pool.length];
342
+ this.index++;
343
+ return mode;
344
+ }
345
+ /**
346
+ * Peek at the next mode without advancing.
347
+ */
348
+ peek() {
349
+ return this.pool[this.index % this.pool.length];
350
+ }
351
+ /**
352
+ * Reset rotation to the beginning.
353
+ */
354
+ reset() {
355
+ this.index = 0;
356
+ }
357
+ /**
358
+ * Current rotation index (for reporting).
359
+ */
360
+ get currentIndex() {
361
+ return this.index;
362
+ }
363
+ /**
364
+ * The full mode pool.
365
+ */
366
+ get modes() {
367
+ return this.pool;
368
+ }
369
+ }
370
+ //# sourceMappingURL=keyboard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keyboard.js","sourceRoot":"","sources":["../src/keyboard.ts"],"names":[],"mappings":"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"}
@@ -58,8 +58,10 @@
58
58
  export interface MarkdownScene {
59
59
  name: string;
60
60
  group?: string;
61
- /** Pre-cleanup expression from `cleanup:` directive */
62
- cleanup?: string;
61
+ /** Pre/post-cleanup expressions from `cleanup:` directives (one per line) */
62
+ cleanup: string[];
63
+ /** Setup expressions from `setup:` directives — run after pre-cleanup, before scene */
64
+ setup: string[];
63
65
  blocks: ActorBlock[];
64
66
  }
65
67
  export interface ActorBlock {
@@ -1 +1 @@
1
- {"version":3,"file":"markdown-scene.d.ts","sourceRoot":"","sources":["../src/markdown-scene.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwDG;AAYH,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,uDAAuD;IACvD,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,UAAU,EAAE,CAAA;CACrB;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,WAAW,EAAE,CAAA;CACvB;AAED;;;;;GAKG;AACH,MAAM,MAAM,QAAQ,GAChB;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAChD;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAA;AAEtC,MAAM,MAAM,WAAW,GACnB;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAChC;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACjC;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,GACnD;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,QAAQ,EAAE,CAAA;CAAE,CAAA;AAMrD;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,GACf,aAAa,EAAE,CAyJjB;AAmLD;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,aAAa,EAAE,EACvB,QAAQ,EAAE,MAAM,GACf,IAAI,CAsHN;AAMD;;;;;GAKG;AACH,wBAAsB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAOvE"}
1
+ {"version":3,"file":"markdown-scene.d.ts","sourceRoot":"","sources":["../src/markdown-scene.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwDG;AAYH,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,6EAA6E;IAC7E,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,uFAAuF;IACvF,KAAK,EAAE,MAAM,EAAE,CAAA;IACf,MAAM,EAAE,UAAU,EAAE,CAAA;CACrB;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,WAAW,EAAE,CAAA;CACvB;AAED;;;;;GAKG;AACH,MAAM,MAAM,QAAQ,GAChB;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAChD;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAA;AAEtC,MAAM,MAAM,WAAW,GACnB;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAChC;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACjC;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,GACnD;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,QAAQ,EAAE,CAAA;CAAE,CAAA;AAMrD;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,GACf,aAAa,EAAE,CAkKjB;AAmLD;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,aAAa,EAAE,EACvB,QAAQ,EAAE,MAAM,GACf,IAAI,CAwHN;AAMD;;;;;GAKG;AACH,wBAAsB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAOvE"}
@@ -90,7 +90,7 @@ export function parseMarkdownScenes(content, filePath) {
90
90
  // # = group, ## = scene
91
91
  if (/^## /.test(trimmed)) {
92
92
  const name = trimmed.slice(3).trim();
93
- currentScene = { name, group: currentGroup, blocks: [] };
93
+ currentScene = { name, group: currentGroup, cleanup: [], setup: [], blocks: [] };
94
94
  scenes.push(currentScene);
95
95
  currentBlock = null;
96
96
  continue;
@@ -104,17 +104,23 @@ export function parseMarkdownScenes(content, filePath) {
104
104
  // No ## headings — # = scene
105
105
  if (/^# /.test(trimmed)) {
106
106
  const name = trimmed.slice(2).trim();
107
- currentScene = { name, group: undefined, blocks: [] };
107
+ currentScene = { name, group: undefined, cleanup: [], setup: [], blocks: [] };
108
108
  scenes.push(currentScene);
109
109
  currentBlock = null;
110
110
  continue;
111
111
  }
112
112
  }
113
- // ── Cleanup directive ─────────────────────────────────────────────
114
- // `cleanup: <expression>` — one per scene, before actor cues
113
+ // ── Cleanup / setup directives ────────────────────────────────────
114
+ // `cleanup: <expression>` — multiple allowed, run before AND after scene
115
+ // `setup: <expression>` — multiple allowed, run after pre-cleanup, before scene
115
116
  const cleanupMatch = trimmed.match(/^cleanup:\s+(.+)$/);
116
117
  if (cleanupMatch && currentScene && !currentBlock) {
117
- currentScene.cleanup = cleanupMatch[1];
118
+ currentScene.cleanup.push(cleanupMatch[1]);
119
+ continue;
120
+ }
121
+ const setupMatch = trimmed.match(/^setup:\s+(.+)$/);
122
+ if (setupMatch && currentScene && !currentBlock) {
123
+ currentScene.setup.push(setupMatch[1]);
118
124
  continue;
119
125
  }
120
126
  // ── Content lines (need an active scene) ──────────────────────────
@@ -124,6 +130,8 @@ export function parseMarkdownScenes(content, filePath) {
124
130
  currentScene = {
125
131
  name: path.basename(filePath, '.spec.md'),
126
132
  group: undefined,
133
+ cleanup: [],
134
+ setup: [],
127
135
  blocks: [],
128
136
  };
129
137
  scenes.push(currentScene);
@@ -221,7 +229,7 @@ function stripListPrefix(line) {
221
229
  const KNOWN_ACTIONS = [
222
230
  'openTo', 'see', 'seeInView', 'notSee', 'seeText', 'seeToast', 'click',
223
231
  'typeInto', 'check', 'select', 'wait', 'emit', 'waitFor',
224
- 'warnIf', 'up', 'prev', 'scrollToBottom', 'if',
232
+ 'warnIf', 'up', 'prev', 'scrollToBottom', 'if', 'pressKey',
225
233
  ];
226
234
  /** Check if a line looks like a DSL action or macro invocation */
227
235
  function isActionLine(line) {
@@ -444,9 +452,13 @@ export function registerMarkdownScenes(scenes, filePath) {
444
452
  }
445
453
  }
446
454
  });
447
- // Propagate cleanup expression to the registered scene
448
- if (mdScene.cleanup && sceneRegistry.length > 0) {
449
- sceneRegistry[sceneRegistry.length - 1].cleanup = mdScene.cleanup;
455
+ // Propagate cleanup/setup expressions to the registered scene
456
+ if (sceneRegistry.length > 0) {
457
+ const registered = sceneRegistry[sceneRegistry.length - 1];
458
+ if (mdScene.cleanup.length > 0)
459
+ registered.cleanup = mdScene.cleanup;
460
+ if (mdScene.setup.length > 0)
461
+ registered.setup = mdScene.setup;
450
462
  }
451
463
  }
452
464
  }