@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.
- package/dist/__tests__/dsl.test.js +20 -0
- package/dist/__tests__/dsl.test.js.map +1 -1
- package/dist/__tests__/keyboard.test.d.ts +2 -0
- package/dist/__tests__/keyboard.test.d.ts.map +1 -0
- package/dist/__tests__/keyboard.test.js +332 -0
- package/dist/__tests__/keyboard.test.js.map +1 -0
- package/dist/__tests__/markdown-scene.test.js +85 -5
- package/dist/__tests__/markdown-scene.test.js.map +1 -1
- package/dist/__tests__/reactive.test.js +259 -6
- package/dist/__tests__/reactive.test.js.map +1 -1
- package/dist/__tests__/runner.test.d.ts +2 -0
- package/dist/__tests__/runner.test.d.ts.map +1 -0
- package/dist/__tests__/runner.test.js +182 -0
- package/dist/__tests__/runner.test.js.map +1 -0
- package/dist/actor.d.ts +21 -4
- package/dist/actor.d.ts.map +1 -1
- package/dist/actor.js +176 -12
- package/dist/actor.js.map +1 -1
- package/dist/builtin-macros.d.ts +87 -0
- package/dist/builtin-macros.d.ts.map +1 -0
- package/dist/builtin-macros.js +154 -0
- package/dist/builtin-macros.js.map +1 -0
- package/dist/cli.js +12 -0
- package/dist/cli.js.map +1 -1
- package/dist/devices.d.ts +6 -0
- package/dist/devices.d.ts.map +1 -1
- package/dist/devices.js +13 -0
- package/dist/devices.js.map +1 -1
- package/dist/dsl.d.ts +4 -0
- package/dist/dsl.d.ts.map +1 -1
- package/dist/dsl.js +18 -1
- package/dist/dsl.js.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/keyboard.d.ts +168 -0
- package/dist/keyboard.d.ts.map +1 -0
- package/dist/keyboard.js +370 -0
- package/dist/keyboard.js.map +1 -0
- package/dist/markdown-scene.d.ts +4 -2
- package/dist/markdown-scene.d.ts.map +1 -1
- package/dist/markdown-scene.js +21 -9
- package/dist/markdown-scene.js.map +1 -1
- package/dist/reactive.d.ts +25 -2
- package/dist/reactive.d.ts.map +1 -1
- package/dist/reactive.js +163 -16
- package/dist/reactive.js.map +1 -1
- package/dist/runner.d.ts +11 -5
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +141 -35
- package/dist/runner.js.map +1 -1
- package/dist/swarm.d.ts +1 -1
- package/dist/swarm.d.ts.map +1 -1
- package/dist/swarm.js +8 -5
- package/dist/swarm.js.map +1 -1
- package/dist/team-manager.d.ts +48 -3
- package/dist/team-manager.d.ts.map +1 -1
- package/dist/team-manager.js +150 -15
- package/dist/team-manager.js.map +1 -1
- package/dist/types.d.ts +133 -2
- package/dist/types.d.ts.map +1 -1
- 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"}
|
package/dist/keyboard.js
ADDED
|
@@ -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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"}
|
package/dist/markdown-scene.d.ts
CHANGED
|
@@ -58,8 +58,10 @@
|
|
|
58
58
|
export interface MarkdownScene {
|
|
59
59
|
name: string;
|
|
60
60
|
group?: string;
|
|
61
|
-
/** Pre-cleanup
|
|
62
|
-
cleanup
|
|
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,
|
|
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"}
|
package/dist/markdown-scene.js
CHANGED
|
@@ -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
|
|
114
|
-
// `cleanup: <expression>` —
|
|
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
|
|
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
|
|
448
|
-
if (
|
|
449
|
-
sceneRegistry[sceneRegistry.length - 1]
|
|
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
|
}
|