@uniaria/testing 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +39 -2
- package/dist/browser.d.ts +73 -0
- package/dist/browser.js +352 -0
- package/dist/browser.js.map +1 -0
- package/dist/commands-DPaip6f1.d.ts +61 -0
- package/dist/commands.d.ts +2 -0
- package/dist/commands.js +341 -0
- package/dist/commands.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +54 -0
- package/dist/index.js.map +1 -0
- package/dist/scenario-DMaR4JJc.d.ts +383 -0
- package/dist/template-Cn6bp45u.js +152 -0
- package/dist/template-Cn6bp45u.js.map +1 -0
- package/package.json +73 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Andrey Yamanov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,40 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @uniaria/testing
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
How UniAria checks that a component behaves the same in every adapter. A
|
|
4
|
+
component's behavior is described once, as a conformance suite of scenarios
|
|
5
|
+
that mount it, drive it with real mouse, keyboard and touch input, and check
|
|
6
|
+
the DOM, focus, events, state and what a screen reader says. Each adapter
|
|
7
|
+
implements one `mount` function, and every suite runs against it in Chromium,
|
|
8
|
+
Firefox and WebKit.
|
|
9
|
+
|
|
10
|
+
Use it to write an adapter for another framework and prove it with the suites
|
|
11
|
+
the React and custom-element adapters pass, or to test components of your own
|
|
12
|
+
with real input and spoken output.
|
|
13
|
+
|
|
14
|
+
> **Pre-release.** The API can change in any 0.x version.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
pnpm add -D @uniaria/testing vitest @vitest/browser-playwright
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
It needs Vitest 5 in browser mode with the Playwright provider. Real screen
|
|
23
|
+
readers need [`@guidepup/guidepup`](https://www.guidepup.dev), an optional
|
|
24
|
+
peer.
|
|
25
|
+
|
|
26
|
+
| Entry | Runs in | Has |
|
|
27
|
+
| --------------------------- | --------------------------- | -------------------------------------------------------------------- |
|
|
28
|
+
| `@uniaria/testing` | Anywhere, Node included | `defineConformance`, the markup templates and the contract types |
|
|
29
|
+
| `@uniaria/testing/browser` | Vitest browser mode | `runConformance`, real input, `emulateMedia` and `spokenPhrases` |
|
|
30
|
+
| `@uniaria/testing/commands` | Node, in your Vitest config | `uniCommands` and `screenReaderCommands`, the Node side of the above |
|
|
31
|
+
|
|
32
|
+
## Documentation
|
|
33
|
+
|
|
34
|
+
The [Testing API](https://github.com/tenphi/uniaria/blob/main/docs/api/testing.md)
|
|
35
|
+
page, in the repository's
|
|
36
|
+
[`docs`](https://github.com/tenphi/uniaria/tree/main/docs) folder.
|
|
37
|
+
|
|
38
|
+
## License
|
|
39
|
+
|
|
40
|
+
[MIT](https://github.com/tenphi/uniaria/blob/main/LICENSE)
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { _ as ConformanceAdapter, a as MouseButton, i as KeyboardInput, m as TouchInput, n as ConformanceSuite, o as PointerInput } from "./scenario-DMaR4JJc.js";
|
|
2
|
+
import { t as MediaFeatures } from "./commands-DPaip6f1.js";
|
|
3
|
+
//#region src/conformance/input.d.ts
|
|
4
|
+
declare module 'vitest/browser' {
|
|
5
|
+
interface BrowserCommands {
|
|
6
|
+
uniPointerDown(selector: string, button: MouseButton): Promise<void>;
|
|
7
|
+
uniPointerMove(selector: string, where: 'center' | 'outside'): Promise<void>;
|
|
8
|
+
uniPointerUp(button: MouseButton): Promise<void>;
|
|
9
|
+
uniWheel(deltaX: number, deltaY: number): Promise<void>;
|
|
10
|
+
uniKeyDown(key: string): Promise<void>;
|
|
11
|
+
uniKeyUp(key: string): Promise<void>;
|
|
12
|
+
uniTouchSupported(): Promise<boolean>;
|
|
13
|
+
uniTouch(phase: 'touchStart' | 'touchMove' | 'touchEnd' | 'touchCancel', selector?: string, where?: 'center' | 'outside'): Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/** Whether this browser takes `touch` input (Chromium only). */
|
|
17
|
+
export declare function isTouchSupported(): Promise<boolean>;
|
|
18
|
+
export declare const pointer: PointerInput;
|
|
19
|
+
export declare const keyboard: KeyboardInput;
|
|
20
|
+
export declare const touch: TouchInput;
|
|
21
|
+
/**
|
|
22
|
+
* Releases every button and key a scenario left held, and cancels a touch
|
|
23
|
+
* left down, so it presses nothing in the next scenario.
|
|
24
|
+
*/
|
|
25
|
+
export declare function releaseInput(): Promise<void>;
|
|
26
|
+
//#endregion
|
|
27
|
+
//#region src/media.d.ts
|
|
28
|
+
declare module 'vitest/browser' {
|
|
29
|
+
interface BrowserCommands {
|
|
30
|
+
uniEmulateMedia(features: MediaFeatures): Promise<void>;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Emulates media features for the whole page until set again; `null` ends
|
|
35
|
+
* a feature's emulation. Reset what you set after each test, since the
|
|
36
|
+
* page outlives it. Resolves once the page has restyled: Firefox and WebKit
|
|
37
|
+
* re-evaluate their stylesheets' media queries on the next frame, after
|
|
38
|
+
* `matchMedia()` already answers.
|
|
39
|
+
*/
|
|
40
|
+
export declare function emulateMedia(features: MediaFeatures): Promise<void>;
|
|
41
|
+
//#endregion
|
|
42
|
+
//#region src/conformance/run.d.ts
|
|
43
|
+
interface RunOptions {
|
|
44
|
+
/**
|
|
45
|
+
* The highest roadmap phase this adapter implements. Scenarios tagged with
|
|
46
|
+
* a later phase are skipped. Default: run everything.
|
|
47
|
+
*/
|
|
48
|
+
readonly phase?: number;
|
|
49
|
+
/**
|
|
50
|
+
* Runs the scenarios that need a real screen reader
|
|
51
|
+
* (`needs: ['screenReader']`), and only those, with the machine's screen
|
|
52
|
+
* reader started for the whole file. Without it they aren't registered.
|
|
53
|
+
* Needs `screenReaderCommands` from `@uniaria/testing/commands`.
|
|
54
|
+
*/
|
|
55
|
+
readonly screenReader?: boolean;
|
|
56
|
+
}
|
|
57
|
+
export declare function runConformance(adapter: ConformanceAdapter, suites: readonly ConformanceSuite<any, any>[], options?: RunOptions): void;
|
|
58
|
+
//#endregion
|
|
59
|
+
//#region src/conformance/spoken.d.ts
|
|
60
|
+
/**
|
|
61
|
+
* Spoken output through `@guidepup/virtual-screen-reader`, the simulated
|
|
62
|
+
* screen reader every pull request runs. It reads the accessibility tree the
|
|
63
|
+
* browser exposes, so phrases follow the library's format:
|
|
64
|
+
* `button, Save, disabled`.
|
|
65
|
+
*/
|
|
66
|
+
/**
|
|
67
|
+
* Reads `container` (the whole document by default) from start to end and
|
|
68
|
+
* returns every phrase, `document` to `end of document`.
|
|
69
|
+
*/
|
|
70
|
+
export declare function spokenPhrases(container?: Element): Promise<string[]>;
|
|
71
|
+
//#endregion
|
|
72
|
+
export type { MediaFeatures, RunOptions };
|
|
73
|
+
//# sourceMappingURL=browser.d.ts.map
|
package/dist/browser.js
ADDED
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import { s as partSelector } from "./template-Cn6bp45u.js";
|
|
2
|
+
import { commands, userEvent } from "vitest/browser";
|
|
3
|
+
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
|
|
4
|
+
import { virtual } from "@guidepup/virtual-screen-reader";
|
|
5
|
+
//#region src/conformance/input.ts
|
|
6
|
+
/**
|
|
7
|
+
* Browser side of the conformance input helpers: real mouse, keyboard and
|
|
8
|
+
* touch input through the Playwright commands in `../commands.ts`. Held
|
|
9
|
+
* buttons, keys and fingers are tracked so the runner can release them after
|
|
10
|
+
* each scenario, even one that failed mid-press.
|
|
11
|
+
*/
|
|
12
|
+
const heldButtons = /* @__PURE__ */ new Set();
|
|
13
|
+
const heldKeys = /* @__PURE__ */ new Set();
|
|
14
|
+
let isFingerDown = false;
|
|
15
|
+
let touchSupport;
|
|
16
|
+
/** Whether this browser takes `touch` input (Chromium only). */
|
|
17
|
+
function isTouchSupported() {
|
|
18
|
+
return touchSupport ??= commands.uniTouchSupported();
|
|
19
|
+
}
|
|
20
|
+
/** A selector that finds exactly this element, without touching the DOM. */
|
|
21
|
+
function selectorFor(element) {
|
|
22
|
+
const steps = [];
|
|
23
|
+
let node = element;
|
|
24
|
+
while (node.parentElement) {
|
|
25
|
+
const index = Array.prototype.indexOf.call(node.parentElement.children, node);
|
|
26
|
+
steps.unshift(`${node.localName}:nth-child(${index + 1})`);
|
|
27
|
+
node = node.parentElement;
|
|
28
|
+
}
|
|
29
|
+
return [node.localName, ...steps].join(" > ");
|
|
30
|
+
}
|
|
31
|
+
const down = async (target, options) => {
|
|
32
|
+
const button = options?.button ?? "left";
|
|
33
|
+
await commands.uniPointerDown(selectorFor(target), button);
|
|
34
|
+
heldButtons.add(button);
|
|
35
|
+
};
|
|
36
|
+
const up = async (options) => {
|
|
37
|
+
const button = options?.button ?? "left";
|
|
38
|
+
await commands.uniPointerUp(button);
|
|
39
|
+
heldButtons.delete(button);
|
|
40
|
+
};
|
|
41
|
+
/** How long `pointer.wheel` waits for its event to reach the page. */
|
|
42
|
+
const WHEEL_TIMEOUT = 1e3;
|
|
43
|
+
/** How long `pointer.wheel` waits for its event after a scroll. */
|
|
44
|
+
const SCROLL_GRACE = 250;
|
|
45
|
+
/**
|
|
46
|
+
* Resolves once the page gets a `wheel` event: Firefox's wheel command can
|
|
47
|
+
* resolve before it. A scroll gives the event `SCROLL_GRACE` more, then
|
|
48
|
+
* resolves: WebKit sends no `wheel` event while a button is held, only the
|
|
49
|
+
* scroll, and a scroll can come first that isn't this wheel's (Firefox
|
|
50
|
+
* scrolling one ahead of its event, or one left from an earlier step). A
|
|
51
|
+
* wheel that neither reaches the page nor scrolls gives up after
|
|
52
|
+
* `WHEEL_TIMEOUT`.
|
|
53
|
+
*
|
|
54
|
+
* It resolves in a task after the event, not in the capture listener:
|
|
55
|
+
* there, the caller would go on in the microtasks after that listener,
|
|
56
|
+
* before the event reached the page's own listeners.
|
|
57
|
+
*/
|
|
58
|
+
function wheelArrival() {
|
|
59
|
+
return new Promise((resolve) => {
|
|
60
|
+
let timer = setTimeout(done, WHEEL_TIMEOUT);
|
|
61
|
+
function done() {
|
|
62
|
+
clearTimeout(timer);
|
|
63
|
+
window.removeEventListener("wheel", done, true);
|
|
64
|
+
window.removeEventListener("scroll", scrolled, true);
|
|
65
|
+
setTimeout(resolve);
|
|
66
|
+
}
|
|
67
|
+
function scrolled() {
|
|
68
|
+
window.removeEventListener("scroll", scrolled, true);
|
|
69
|
+
clearTimeout(timer);
|
|
70
|
+
timer = setTimeout(done, SCROLL_GRACE);
|
|
71
|
+
}
|
|
72
|
+
window.addEventListener("wheel", done, true);
|
|
73
|
+
window.addEventListener("scroll", scrolled, true);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
const pointer = {
|
|
77
|
+
down,
|
|
78
|
+
up,
|
|
79
|
+
move: (target) => commands.uniPointerMove(selectorFor(target), "center"),
|
|
80
|
+
moveOutside: (target) => commands.uniPointerMove(selectorFor(target), "outside"),
|
|
81
|
+
async wheel({ deltaX = 0, deltaY = 0 }) {
|
|
82
|
+
const arrival = wheelArrival();
|
|
83
|
+
await commands.uniWheel(deltaX, deltaY);
|
|
84
|
+
await arrival;
|
|
85
|
+
},
|
|
86
|
+
async click(target, options) {
|
|
87
|
+
await down(target, options);
|
|
88
|
+
await up(options);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
const keyDown = async (key) => {
|
|
92
|
+
await commands.uniKeyDown(key);
|
|
93
|
+
heldKeys.add(key);
|
|
94
|
+
};
|
|
95
|
+
const keyUp = async (key) => {
|
|
96
|
+
await commands.uniKeyUp(key);
|
|
97
|
+
heldKeys.delete(key);
|
|
98
|
+
};
|
|
99
|
+
const keyboard = {
|
|
100
|
+
down: keyDown,
|
|
101
|
+
up: keyUp,
|
|
102
|
+
async press(key) {
|
|
103
|
+
await keyDown(key);
|
|
104
|
+
await keyUp(key);
|
|
105
|
+
},
|
|
106
|
+
tab: (options) => userEvent.tab({ shift: options?.isBackward ?? false })
|
|
107
|
+
};
|
|
108
|
+
const touchDown = async (target) => {
|
|
109
|
+
await commands.uniTouch("touchStart", selectorFor(target));
|
|
110
|
+
isFingerDown = true;
|
|
111
|
+
};
|
|
112
|
+
const touchUp = async () => {
|
|
113
|
+
await commands.uniTouch("touchEnd");
|
|
114
|
+
isFingerDown = false;
|
|
115
|
+
};
|
|
116
|
+
const touch = {
|
|
117
|
+
down: touchDown,
|
|
118
|
+
up: touchUp,
|
|
119
|
+
move: (target) => commands.uniTouch("touchMove", selectorFor(target)),
|
|
120
|
+
moveOutside: (target) => commands.uniTouch("touchMove", selectorFor(target), "outside"),
|
|
121
|
+
async tap(target) {
|
|
122
|
+
await touchDown(target);
|
|
123
|
+
await touchUp();
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
/**
|
|
127
|
+
* Releases every button and key a scenario left held, and cancels a touch
|
|
128
|
+
* left down, so it presses nothing in the next scenario.
|
|
129
|
+
*/
|
|
130
|
+
async function releaseInput() {
|
|
131
|
+
for (const button of [...heldButtons]) await up({ button });
|
|
132
|
+
for (const key of [...heldKeys]) await keyUp(key);
|
|
133
|
+
if (isFingerDown) {
|
|
134
|
+
await commands.uniTouch("touchCancel");
|
|
135
|
+
isFingerDown = false;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
//#endregion
|
|
139
|
+
//#region src/media.ts
|
|
140
|
+
/**
|
|
141
|
+
* Media emulation for browser tests, through the `uniEmulateMedia` command
|
|
142
|
+
* in `../commands.ts`: forced colors (Chromium only), `prefers-contrast`,
|
|
143
|
+
* reduced motion and the color scheme.
|
|
144
|
+
*/
|
|
145
|
+
/**
|
|
146
|
+
* Emulates media features for the whole page until set again; `null` ends
|
|
147
|
+
* a feature's emulation. Reset what you set after each test, since the
|
|
148
|
+
* page outlives it. Resolves once the page has restyled: Firefox and WebKit
|
|
149
|
+
* re-evaluate their stylesheets' media queries on the next frame, after
|
|
150
|
+
* `matchMedia()` already answers.
|
|
151
|
+
*/
|
|
152
|
+
async function emulateMedia(features) {
|
|
153
|
+
await commands.uniEmulateMedia(features);
|
|
154
|
+
for (let frame = 0; frame < 2; frame++) await new Promise((resolve) => requestAnimationFrame(resolve));
|
|
155
|
+
}
|
|
156
|
+
//#endregion
|
|
157
|
+
//#region src/conformance/screen-reader.ts
|
|
158
|
+
/**
|
|
159
|
+
* Browser side of the real screen reader (`ScreenReaderInput`): the
|
|
160
|
+
* Guidepup commands in `../screen-reader-commands.ts`. The runner starts it
|
|
161
|
+
* once per file and scenarios reach it through their context.
|
|
162
|
+
*/
|
|
163
|
+
let name;
|
|
164
|
+
/** Starts the machine's screen reader and moves it into the page. */
|
|
165
|
+
async function startScreenReader() {
|
|
166
|
+
name = await commands.uniScreenReaderStart();
|
|
167
|
+
}
|
|
168
|
+
async function stopScreenReader() {
|
|
169
|
+
name = void 0;
|
|
170
|
+
await commands.uniScreenReaderStop();
|
|
171
|
+
}
|
|
172
|
+
const screenReader = {
|
|
173
|
+
get name() {
|
|
174
|
+
if (!name) throw new Error("[@uniaria/testing] No screen reader runs.");
|
|
175
|
+
return name;
|
|
176
|
+
},
|
|
177
|
+
async startFrom(element) {
|
|
178
|
+
await pointer.click(element);
|
|
179
|
+
await commands.uniScreenReaderSettle();
|
|
180
|
+
},
|
|
181
|
+
press: (key) => commands.uniScreenReaderPress(key),
|
|
182
|
+
type: (text) => commands.uniScreenReaderType(text),
|
|
183
|
+
next: () => commands.uniScreenReaderNext(),
|
|
184
|
+
async readFocus() {
|
|
185
|
+
const focus = await commands.uniScreenReaderReadFocus();
|
|
186
|
+
console.info(`[${name}] ${expect.getState().currentTestName} (focus): ${JSON.stringify(focus)}`);
|
|
187
|
+
return focus;
|
|
188
|
+
},
|
|
189
|
+
async spoken() {
|
|
190
|
+
const phrases = await commands.uniScreenReaderSpoken();
|
|
191
|
+
console.info(`[${name}] ${expect.getState().currentTestName}: ${JSON.stringify(phrases)}`);
|
|
192
|
+
return phrases.join(" / ");
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
/** The context's screen reader where none runs. */
|
|
196
|
+
const noScreenReader = new Proxy({}, { get() {
|
|
197
|
+
throw new Error("[@uniaria/testing] A real screen reader runs only in the screen-reader project, for scenarios that declare needs: ['screenReader'].");
|
|
198
|
+
} });
|
|
199
|
+
//#endregion
|
|
200
|
+
//#region src/conformance/spoken.ts
|
|
201
|
+
/**
|
|
202
|
+
* Spoken output through `@guidepup/virtual-screen-reader`, the simulated
|
|
203
|
+
* screen reader every pull request runs. It reads the accessibility tree the
|
|
204
|
+
* browser exposes, so phrases follow the library's format:
|
|
205
|
+
* `button, Save, disabled`.
|
|
206
|
+
*/
|
|
207
|
+
/** Upper bound on steps, so a broken tree cannot hang a test. */
|
|
208
|
+
const MAX_STEPS = 500;
|
|
209
|
+
/**
|
|
210
|
+
* Reads `container` (the whole document by default) from start to end and
|
|
211
|
+
* returns every phrase, `document` to `end of document`.
|
|
212
|
+
*/
|
|
213
|
+
async function spokenPhrases(container = document.body) {
|
|
214
|
+
if (listening) throw new Error("[@uniaria/testing] spoken() reads the page with the same virtual screen reader; it cannot run while listening to live regions.");
|
|
215
|
+
await virtual.start({ container });
|
|
216
|
+
try {
|
|
217
|
+
for (let step = 0; step < MAX_STEPS && await virtual.lastSpokenPhrase() !== "end of document"; step++) await virtual.next();
|
|
218
|
+
return await virtual.spokenPhraseLog();
|
|
219
|
+
} finally {
|
|
220
|
+
await virtual.stop();
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
/** How the virtual screen reader logs a live region: `assertive: Saved`. */
|
|
224
|
+
const LIVE_PHRASE = /^(assertive|polite): /;
|
|
225
|
+
let listening = false;
|
|
226
|
+
/**
|
|
227
|
+
* Starts the virtual screen reader on `container` (the whole document by
|
|
228
|
+
* default) so it hears live regions, and returns a reader for what they have
|
|
229
|
+
* announced since, in order: `assertive: Saved`, `polite: 3 results`. A
|
|
230
|
+
* region only speaks when content changes inside it after listening starts,
|
|
231
|
+
* as with a real screen reader. Stop with `stopListening()`.
|
|
232
|
+
*/
|
|
233
|
+
async function listenLive(container = document.body) {
|
|
234
|
+
if (listening) throw new Error("[@uniaria/testing] Already listening to live regions.");
|
|
235
|
+
await virtual.start({ container });
|
|
236
|
+
listening = true;
|
|
237
|
+
return async () => (await virtual.spokenPhraseLog()).filter((phrase) => LIVE_PHRASE.test(phrase));
|
|
238
|
+
}
|
|
239
|
+
/** Stops `listenLive()`, if it is listening. */
|
|
240
|
+
async function stopListening() {
|
|
241
|
+
if (!listening) return;
|
|
242
|
+
listening = false;
|
|
243
|
+
await virtual.stop();
|
|
244
|
+
}
|
|
245
|
+
//#endregion
|
|
246
|
+
//#region src/conformance/run.ts
|
|
247
|
+
/**
|
|
248
|
+
* Runs conformance suites against one adapter as Vitest browser tests. The
|
|
249
|
+
* runner owns everything adapter-neutral: a fresh container per mount, part
|
|
250
|
+
* lookup, the event log, real input, spoken output, cleanup after each
|
|
251
|
+
* scenario, and skipping scenarios from later roadmap phases or that need
|
|
252
|
+
* input the browser can't take.
|
|
253
|
+
*
|
|
254
|
+
* An adapter's conformance test file is one call:
|
|
255
|
+
* `runConformance(reactAdapter, suites, { phase: 1 })`. Its screen-reader
|
|
256
|
+
* file passes `screenReader: true` too.
|
|
257
|
+
*/
|
|
258
|
+
const needsScreenReader = (child) => child.kind === "group" ? child.children.some(needsScreenReader) : child.needs.includes("screenReader");
|
|
259
|
+
/** Whether a run registers this child: a screen-reader run, only theirs. */
|
|
260
|
+
const isInRun = (child, isScreenReaderRun) => child.kind === "group" ? child.children.some((each) => isInRun(each, isScreenReaderRun)) : needsScreenReader(child) === isScreenReaderRun;
|
|
261
|
+
function partsOf(container, component, name) {
|
|
262
|
+
return Array.from(container.querySelectorAll(partSelector(component, name)));
|
|
263
|
+
}
|
|
264
|
+
function toNodes(template) {
|
|
265
|
+
return Array.isArray(template) ? template : [template];
|
|
266
|
+
}
|
|
267
|
+
function runConformance(adapter, suites, options = {}) {
|
|
268
|
+
const phase = options.phase ?? Number.POSITIVE_INFINITY;
|
|
269
|
+
const isScreenReaderRun = options.screenReader ?? false;
|
|
270
|
+
if (isScreenReaderRun) {
|
|
271
|
+
beforeAll(startScreenReader);
|
|
272
|
+
afterAll(stopScreenReader);
|
|
273
|
+
}
|
|
274
|
+
for (const suite of suites) {
|
|
275
|
+
if (!suite.children.some((child) => isInRun(child, isScreenReaderRun))) continue;
|
|
276
|
+
describe(`${suite.component} (${adapter.name})`, () => {
|
|
277
|
+
const mounted = [];
|
|
278
|
+
afterEach(async () => {
|
|
279
|
+
await releaseInput();
|
|
280
|
+
await stopListening();
|
|
281
|
+
for (const { mount, container } of mounted.splice(0)) {
|
|
282
|
+
await mount.unmount();
|
|
283
|
+
container.remove();
|
|
284
|
+
}
|
|
285
|
+
if (document.activeElement instanceof HTMLElement) document.activeElement.blur();
|
|
286
|
+
for (const region of document.querySelectorAll("[aria-live]")) region.replaceChildren();
|
|
287
|
+
});
|
|
288
|
+
const context = {
|
|
289
|
+
pointer,
|
|
290
|
+
keyboard,
|
|
291
|
+
touch,
|
|
292
|
+
screenReader: isScreenReaderRun ? screenReader : noScreenReader,
|
|
293
|
+
spoken: () => spokenPhrases(),
|
|
294
|
+
listen: () => listenLive(),
|
|
295
|
+
async mount(mountOptions) {
|
|
296
|
+
const container = document.createElement("div");
|
|
297
|
+
document.body.appendChild(container);
|
|
298
|
+
const events = [];
|
|
299
|
+
const mount = await adapter.mount({
|
|
300
|
+
component: suite.component,
|
|
301
|
+
props: mountOptions.props ?? {},
|
|
302
|
+
template: toNodes(mountOptions.template),
|
|
303
|
+
events: mountOptions.events ?? [],
|
|
304
|
+
record: (name, detail) => events.push({
|
|
305
|
+
name,
|
|
306
|
+
detail
|
|
307
|
+
}),
|
|
308
|
+
container
|
|
309
|
+
});
|
|
310
|
+
mounted.push({
|
|
311
|
+
mount,
|
|
312
|
+
container
|
|
313
|
+
});
|
|
314
|
+
return {
|
|
315
|
+
container,
|
|
316
|
+
events,
|
|
317
|
+
part(name = "root") {
|
|
318
|
+
const found = partsOf(container, suite.component, name);
|
|
319
|
+
if (found.length !== 1) throw new Error(`Expected one "${name}" part of ${suite.component}, found ${found.length}. Check the root's data-uni, an outside part's data-uni-part, or data-element inside the root.`);
|
|
320
|
+
return found[0];
|
|
321
|
+
},
|
|
322
|
+
parts: (name) => partsOf(container, suite.component, name),
|
|
323
|
+
eventNames: () => events.map((event) => event.name),
|
|
324
|
+
clearEvents: () => {
|
|
325
|
+
events.length = 0;
|
|
326
|
+
},
|
|
327
|
+
state: () => mount.state(),
|
|
328
|
+
setProps: (props) => mount.setProps(props),
|
|
329
|
+
call: (action) => mount.call(action),
|
|
330
|
+
renderCount: () => mount.renderCount()
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
const register = (children) => {
|
|
335
|
+
for (const child of children) {
|
|
336
|
+
if (!isInRun(child, isScreenReaderRun)) continue;
|
|
337
|
+
if (child.kind === "group") describe(child.name, () => register(child.children));
|
|
338
|
+
else if (child.phase > phase) it.skip(`${child.name} (phase ${child.phase})`);
|
|
339
|
+
else it(child.name, async ({ skip }) => {
|
|
340
|
+
if (child.needs.includes("touch") && !await isTouchSupported()) skip("needs touch input, which only Chromium takes");
|
|
341
|
+
await child.run(context);
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
register(suite.children);
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
//#endregion
|
|
350
|
+
export { emulateMedia, isTouchSupported, keyboard, pointer, releaseInput, runConformance, spokenPhrases, touch };
|
|
351
|
+
|
|
352
|
+
//# sourceMappingURL=browser.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser.js","names":[],"sources":["../src/conformance/input.ts","../src/media.ts","../src/conformance/screen-reader.ts","../src/conformance/spoken.ts","../src/conformance/run.ts"],"sourcesContent":["/**\n * Browser side of the conformance input helpers: real mouse, keyboard and\n * touch input through the Playwright commands in `../commands.ts`. Held\n * buttons, keys and fingers are tracked so the runner can release them after\n * each scenario, even one that failed mid-press.\n */\n\nimport { commands, userEvent } from 'vitest/browser';\n\nimport type {\n KeyboardInput,\n MouseButton,\n PointerInput,\n TouchInput,\n} from './scenario';\n\ndeclare module 'vitest/browser' {\n interface BrowserCommands {\n uniPointerDown(selector: string, button: MouseButton): Promise<void>;\n uniPointerMove(\n selector: string,\n where: 'center' | 'outside',\n ): Promise<void>;\n uniPointerUp(button: MouseButton): Promise<void>;\n uniWheel(deltaX: number, deltaY: number): Promise<void>;\n uniKeyDown(key: string): Promise<void>;\n uniKeyUp(key: string): Promise<void>;\n uniTouchSupported(): Promise<boolean>;\n uniTouch(\n phase: 'touchStart' | 'touchMove' | 'touchEnd' | 'touchCancel',\n selector?: string,\n where?: 'center' | 'outside',\n ): Promise<void>;\n }\n}\n\nconst heldButtons = new Set<MouseButton>();\nconst heldKeys = new Set<string>();\nlet isFingerDown = false;\nlet touchSupport: Promise<boolean> | undefined;\n\n/** Whether this browser takes `touch` input (Chromium only). */\nexport function isTouchSupported(): Promise<boolean> {\n return (touchSupport ??= commands.uniTouchSupported());\n}\n\n/** A selector that finds exactly this element, without touching the DOM. */\nexport function selectorFor(element: Element): string {\n const steps: string[] = [];\n let node: Element = element;\n while (node.parentElement) {\n const index = Array.prototype.indexOf.call(\n node.parentElement.children,\n node,\n );\n steps.unshift(`${node.localName}:nth-child(${index + 1})`);\n node = node.parentElement;\n }\n return [node.localName, ...steps].join(' > ');\n}\n\nconst down: PointerInput['down'] = async (target, options) => {\n const button = options?.button ?? 'left';\n await commands.uniPointerDown(selectorFor(target), button);\n heldButtons.add(button);\n};\n\nconst up: PointerInput['up'] = async (options) => {\n const button = options?.button ?? 'left';\n await commands.uniPointerUp(button);\n heldButtons.delete(button);\n};\n\n/** How long `pointer.wheel` waits for its event to reach the page. */\nconst WHEEL_TIMEOUT = 1000;\n\n/** How long `pointer.wheel` waits for its event after a scroll. */\nconst SCROLL_GRACE = 250;\n\n/**\n * Resolves once the page gets a `wheel` event: Firefox's wheel command can\n * resolve before it. A scroll gives the event `SCROLL_GRACE` more, then\n * resolves: WebKit sends no `wheel` event while a button is held, only the\n * scroll, and a scroll can come first that isn't this wheel's (Firefox\n * scrolling one ahead of its event, or one left from an earlier step). A\n * wheel that neither reaches the page nor scrolls gives up after\n * `WHEEL_TIMEOUT`.\n *\n * It resolves in a task after the event, not in the capture listener:\n * there, the caller would go on in the microtasks after that listener,\n * before the event reached the page's own listeners.\n */\nexport function wheelArrival(): Promise<void> {\n return new Promise((resolve) => {\n let timer = setTimeout(done, WHEEL_TIMEOUT);\n function done() {\n clearTimeout(timer);\n window.removeEventListener('wheel', done, true);\n window.removeEventListener('scroll', scrolled, true);\n setTimeout(resolve);\n }\n function scrolled() {\n window.removeEventListener('scroll', scrolled, true);\n clearTimeout(timer);\n timer = setTimeout(done, SCROLL_GRACE);\n }\n window.addEventListener('wheel', done, true);\n window.addEventListener('scroll', scrolled, true);\n });\n}\n\nexport const pointer: PointerInput = {\n down,\n up,\n move: (target) => commands.uniPointerMove(selectorFor(target), 'center'),\n moveOutside: (target) =>\n commands.uniPointerMove(selectorFor(target), 'outside'),\n async wheel({ deltaX = 0, deltaY = 0 }) {\n const arrival = wheelArrival();\n await commands.uniWheel(deltaX, deltaY);\n await arrival;\n },\n async click(target, options) {\n await down(target, options);\n await up(options);\n },\n};\n\nconst keyDown: KeyboardInput['down'] = async (key) => {\n await commands.uniKeyDown(key);\n heldKeys.add(key);\n};\n\nconst keyUp: KeyboardInput['up'] = async (key) => {\n await commands.uniKeyUp(key);\n heldKeys.delete(key);\n};\n\nexport const keyboard: KeyboardInput = {\n down: keyDown,\n up: keyUp,\n async press(key) {\n await keyDown(key);\n await keyUp(key);\n },\n tab: (options) => userEvent.tab({ shift: options?.isBackward ?? false }),\n};\n\nconst touchDown: TouchInput['down'] = async (target) => {\n await commands.uniTouch('touchStart', selectorFor(target));\n isFingerDown = true;\n};\n\nconst touchUp: TouchInput['up'] = async () => {\n await commands.uniTouch('touchEnd');\n isFingerDown = false;\n};\n\nexport const touch: TouchInput = {\n down: touchDown,\n up: touchUp,\n move: (target) => commands.uniTouch('touchMove', selectorFor(target)),\n moveOutside: (target) =>\n commands.uniTouch('touchMove', selectorFor(target), 'outside'),\n async tap(target) {\n await touchDown(target);\n await touchUp();\n },\n};\n\n/**\n * Releases every button and key a scenario left held, and cancels a touch\n * left down, so it presses nothing in the next scenario.\n */\nexport async function releaseInput(): Promise<void> {\n for (const button of [...heldButtons]) await up({ button });\n for (const key of [...heldKeys]) await keyUp(key);\n if (isFingerDown) {\n await commands.uniTouch('touchCancel');\n isFingerDown = false;\n }\n}\n","/**\n * Media emulation for browser tests, through the `uniEmulateMedia` command\n * in `../commands.ts`: forced colors (Chromium only), `prefers-contrast`,\n * reduced motion and the color scheme.\n */\n\nimport { commands } from 'vitest/browser';\n\nimport type { MediaFeatures } from './commands';\n\nexport type { MediaFeatures } from './commands';\n\ndeclare module 'vitest/browser' {\n interface BrowserCommands {\n uniEmulateMedia(features: MediaFeatures): Promise<void>;\n }\n}\n\n/**\n * Emulates media features for the whole page until set again; `null` ends\n * a feature's emulation. Reset what you set after each test, since the\n * page outlives it. Resolves once the page has restyled: Firefox and WebKit\n * re-evaluate their stylesheets' media queries on the next frame, after\n * `matchMedia()` already answers.\n */\nexport async function emulateMedia(features: MediaFeatures): Promise<void> {\n await commands.uniEmulateMedia(features);\n for (let frame = 0; frame < 2; frame++) {\n await new Promise((resolve) => requestAnimationFrame(resolve));\n }\n}\n","/**\n * Browser side of the real screen reader (`ScreenReaderInput`): the\n * Guidepup commands in `../screen-reader-commands.ts`. The runner starts it\n * once per file and scenarios reach it through their context.\n */\n\nimport { expect } from 'vitest';\nimport { commands } from 'vitest/browser';\n\nimport { pointer } from './input';\nimport type { ScreenReaderInput, ScreenReaderName } from './scenario';\n\ndeclare module 'vitest/browser' {\n interface BrowserCommands {\n uniScreenReaderStart(): Promise<ScreenReaderName>;\n uniScreenReaderStop(): Promise<void>;\n uniScreenReaderSettle(): Promise<void>;\n uniScreenReaderPress(key: string): Promise<void>;\n uniScreenReaderType(text: string): Promise<void>;\n uniScreenReaderNext(): Promise<void>;\n uniScreenReaderReadFocus(): Promise<string>;\n uniScreenReaderSpoken(): Promise<string[]>;\n }\n}\n\nlet name: ScreenReaderName | undefined;\n\n/** Starts the machine's screen reader and moves it into the page. */\nexport async function startScreenReader(): Promise<void> {\n name = await commands.uniScreenReaderStart();\n}\n\nexport async function stopScreenReader(): Promise<void> {\n name = undefined;\n await commands.uniScreenReaderStop();\n}\n\nexport const screenReader: ScreenReaderInput = {\n get name() {\n if (!name) throw new Error('[@uniaria/testing] No screen reader runs.');\n return name;\n },\n async startFrom(element) {\n // A real click: focus from script can leave the keyboard in the\n // browser's toolbar, where the next Tab would go.\n await pointer.click(element);\n await commands.uniScreenReaderSettle();\n },\n press: (key) => commands.uniScreenReaderPress(key),\n type: (text) => commands.uniScreenReaderType(text),\n next: () => commands.uniScreenReaderNext(),\n async readFocus() {\n const focus = await commands.uniScreenReaderReadFocus();\n // eslint-disable-next-line no-console\n console.info(\n `[${name}] ${expect.getState().currentTestName} (focus): ${JSON.stringify(focus)}`,\n );\n return focus;\n },\n async spoken() {\n const phrases = await commands.uniScreenReaderSpoken();\n // The nightly log is the record of what each screen reader said.\n // eslint-disable-next-line no-console\n console.info(\n `[${name}] ${expect.getState().currentTestName}: ${JSON.stringify(phrases)}`,\n );\n return phrases.join(' / ');\n },\n};\n\n/** The context's screen reader where none runs. */\nexport const noScreenReader: ScreenReaderInput = new Proxy(\n {} as ScreenReaderInput,\n {\n get() {\n throw new Error(\n \"[@uniaria/testing] A real screen reader runs only in the screen-reader project, for scenarios that declare needs: ['screenReader'].\",\n );\n },\n },\n);\n","/**\n * Spoken output through `@guidepup/virtual-screen-reader`, the simulated\n * screen reader every pull request runs. It reads the accessibility tree the\n * browser exposes, so phrases follow the library's format:\n * `button, Save, disabled`.\n */\n\nimport { virtual } from '@guidepup/virtual-screen-reader';\n\n/** Upper bound on steps, so a broken tree cannot hang a test. */\nconst MAX_STEPS = 500;\n\n/**\n * Reads `container` (the whole document by default) from start to end and\n * returns every phrase, `document` to `end of document`.\n */\nexport async function spokenPhrases(\n container: Element = document.body,\n): Promise<string[]> {\n if (listening) {\n throw new Error(\n '[@uniaria/testing] spoken() reads the page with the same virtual screen reader; it cannot run while listening to live regions.',\n );\n }\n await virtual.start({ container });\n try {\n for (\n let step = 0;\n step < MAX_STEPS &&\n (await virtual.lastSpokenPhrase()) !== 'end of document';\n step++\n ) {\n await virtual.next();\n }\n return await virtual.spokenPhraseLog();\n } finally {\n await virtual.stop();\n }\n}\n\n/** How the virtual screen reader logs a live region: `assertive: Saved`. */\nconst LIVE_PHRASE = /^(assertive|polite): /;\n\nlet listening = false;\n\n/**\n * Starts the virtual screen reader on `container` (the whole document by\n * default) so it hears live regions, and returns a reader for what they have\n * announced since, in order: `assertive: Saved`, `polite: 3 results`. A\n * region only speaks when content changes inside it after listening starts,\n * as with a real screen reader. Stop with `stopListening()`.\n */\nexport async function listenLive(\n container: Element = document.body,\n): Promise<() => Promise<string[]>> {\n if (listening) {\n throw new Error('[@uniaria/testing] Already listening to live regions.');\n }\n await virtual.start({ container });\n listening = true;\n return async () =>\n (await virtual.spokenPhraseLog()).filter((phrase) =>\n LIVE_PHRASE.test(phrase),\n );\n}\n\n/** Stops `listenLive()`, if it is listening. */\nexport async function stopListening(): Promise<void> {\n if (!listening) return;\n listening = false;\n await virtual.stop();\n}\n","/**\n * Runs conformance suites against one adapter as Vitest browser tests. The\n * runner owns everything adapter-neutral: a fresh container per mount, part\n * lookup, the event log, real input, spoken output, cleanup after each\n * scenario, and skipping scenarios from later roadmap phases or that need\n * input the browser can't take.\n *\n * An adapter's conformance test file is one call:\n * `runConformance(reactAdapter, suites, { phase: 1 })`. Its screen-reader\n * file passes `screenReader: true` too.\n */\n\nimport { afterAll, afterEach, beforeAll, describe, it } from 'vitest';\n\nimport type {\n AdapterMount,\n ConformanceAdapter,\n MountOptions,\n Mounted,\n RecordedEvent,\n} from './contract';\nimport {\n isTouchSupported,\n keyboard,\n pointer,\n releaseInput,\n touch,\n} from './input';\nimport type {\n ConformanceSuite,\n Scenario,\n ScenarioContext,\n ScenarioGroup,\n} from './scenario';\nimport {\n noScreenReader,\n screenReader,\n startScreenReader,\n stopScreenReader,\n} from './screen-reader';\nimport { listenLive, spokenPhrases, stopListening } from './spoken';\nimport type { TemplateNode } from './template';\nimport { partSelector } from './template';\n\nexport interface RunOptions {\n /**\n * The highest roadmap phase this adapter implements. Scenarios tagged with\n * a later phase are skipped. Default: run everything.\n */\n readonly phase?: number;\n /**\n * Runs the scenarios that need a real screen reader\n * (`needs: ['screenReader']`), and only those, with the machine's screen\n * reader started for the whole file. Without it they aren't registered.\n * Needs `screenReaderCommands` from `@uniaria/testing/commands`.\n */\n readonly screenReader?: boolean;\n}\n\ntype Child = Scenario<unknown, unknown> | ScenarioGroup<unknown, unknown>;\n\nconst needsScreenReader = (child: Child): boolean =>\n child.kind === 'group'\n ? child.children.some(needsScreenReader)\n : child.needs.includes('screenReader');\n\n/** Whether a run registers this child: a screen-reader run, only theirs. */\nconst isInRun = (child: Child, isScreenReaderRun: boolean): boolean =>\n child.kind === 'group'\n ? child.children.some((each) => isInRun(each, isScreenReaderRun))\n : needsScreenReader(child) === isScreenReaderRun;\n\nfunction partsOf(container: HTMLElement, component: string, name: string) {\n return Array.from(\n container.querySelectorAll<HTMLElement>(partSelector(component, name)),\n );\n}\n\nfunction toNodes(\n template: TemplateNode | readonly TemplateNode[],\n): readonly TemplateNode[] {\n return Array.isArray(template) ? template : [template as TemplateNode];\n}\n\nexport function runConformance(\n adapter: ConformanceAdapter,\n // Suites differ in their props and state types.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n suites: readonly ConformanceSuite<any, any>[],\n options: RunOptions = {},\n): void {\n const phase = options.phase ?? Number.POSITIVE_INFINITY;\n const isScreenReaderRun = options.screenReader ?? false;\n\n if (isScreenReaderRun) {\n // Starting takes seconds, and the screen reader is one per machine.\n beforeAll(startScreenReader);\n afterAll(stopScreenReader);\n }\n\n for (const suite of suites) {\n if (!suite.children.some((child) => isInRun(child, isScreenReaderRun))) {\n continue;\n }\n describe(`${suite.component} (${adapter.name})`, () => {\n const mounted: { mount: AdapterMount; container: HTMLElement }[] = [];\n\n afterEach(async () => {\n await releaseInput();\n await stopListening();\n for (const { mount, container } of mounted.splice(0)) {\n await mount.unmount();\n container.remove();\n }\n if (document.activeElement instanceof HTMLElement) {\n document.activeElement.blur();\n }\n // Live regions outside the containers (the announcer's) keep their\n // messages for seconds, and the next scenario's spoken() reads the\n // whole document.\n for (const region of document.querySelectorAll('[aria-live]')) {\n region.replaceChildren();\n }\n });\n\n const context: ScenarioContext<unknown, unknown> = {\n pointer,\n keyboard,\n touch,\n screenReader: isScreenReaderRun ? screenReader : noScreenReader,\n spoken: () => spokenPhrases(),\n listen: () => listenLive(),\n async mount(mountOptions: MountOptions<unknown>) {\n const container = document.createElement('div');\n document.body.appendChild(container);\n const events: RecordedEvent[] = [];\n\n const mount = await adapter.mount({\n component: suite.component,\n props: (mountOptions.props ?? {}) as Record<string, unknown>,\n template: toNodes(mountOptions.template),\n events: mountOptions.events ?? [],\n record: (name, detail) => events.push({ name, detail }),\n container,\n });\n mounted.push({ mount, container });\n\n const result: Mounted<unknown, unknown> = {\n container,\n events,\n part(name = 'root') {\n const found = partsOf(container, suite.component, name);\n if (found.length !== 1) {\n throw new Error(\n `Expected one \"${name}\" part of ${suite.component}, found ${found.length}. ` +\n \"Check the root's data-uni, an outside part's data-uni-part, or data-element inside the root.\",\n );\n }\n return found[0]!;\n },\n parts: (name) => partsOf(container, suite.component, name),\n eventNames: () => events.map((event) => event.name),\n clearEvents: () => {\n events.length = 0;\n },\n state: () => mount.state(),\n setProps: (props) =>\n mount.setProps(props as Record<string, unknown>),\n call: (action) => mount.call(action),\n renderCount: () => mount.renderCount(),\n };\n return result;\n },\n };\n\n const register = (children: readonly Child[]) => {\n for (const child of children) {\n if (!isInRun(child, isScreenReaderRun)) continue;\n if (child.kind === 'group') {\n describe(child.name, () => register(child.children));\n } else if (child.phase > phase) {\n it.skip(`${child.name} (phase ${child.phase})`);\n } else {\n it(child.name, async ({ skip }) => {\n if (\n child.needs.includes('touch') &&\n !(await isTouchSupported())\n ) {\n skip('needs touch input, which only Chromium takes');\n }\n await child.run(context);\n });\n }\n }\n };\n register(suite.children);\n });\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { BrowserCommand } from "vitest/node";
|
|
2
|
+
//#region src/screen-reader-commands.d.ts
|
|
3
|
+
declare const screenReaderCommands: {
|
|
4
|
+
uniScreenReaderStart: BrowserCommand<[], any>;
|
|
5
|
+
uniScreenReaderStop: BrowserCommand<[], any>;
|
|
6
|
+
uniScreenReaderSettle: BrowserCommand<[], any>;
|
|
7
|
+
uniScreenReaderPress: BrowserCommand<[key: string], any>;
|
|
8
|
+
uniScreenReaderType: BrowserCommand<[text: string], any>;
|
|
9
|
+
uniScreenReaderNext: BrowserCommand<[], any>;
|
|
10
|
+
uniScreenReaderReadFocus: BrowserCommand<[], any>;
|
|
11
|
+
uniScreenReaderSpoken: BrowserCommand<[], any>;
|
|
12
|
+
};
|
|
13
|
+
//#endregion
|
|
14
|
+
//#region src/commands.d.ts
|
|
15
|
+
type MouseButton = 'left' | 'right' | 'middle';
|
|
16
|
+
declare const uniPointerDown: BrowserCommand<[selector: string, button: MouseButton]>;
|
|
17
|
+
declare const uniPointerMove: BrowserCommand<[selector: string, where: 'center' | 'outside']>;
|
|
18
|
+
declare const uniPointerUp: BrowserCommand<[button: MouseButton]>;
|
|
19
|
+
/** Turns the wheel wherever the pointer is. */
|
|
20
|
+
declare const uniWheel: BrowserCommand<[deltaX: number, deltaY: number]>;
|
|
21
|
+
declare const uniKeyDown: BrowserCommand<[key: string]>;
|
|
22
|
+
declare const uniKeyUp: BrowserCommand<[key: string]>;
|
|
23
|
+
type TouchPhase = 'touchStart' | 'touchMove' | 'touchEnd' | 'touchCancel';
|
|
24
|
+
/**
|
|
25
|
+
* Whether `uniTouch` works in this browser. Only Chromium takes real touch
|
|
26
|
+
* input through DevTools (`Input.dispatchTouchEvent`); Playwright's own
|
|
27
|
+
* touchscreen needs `hasTouch` on the whole context, which would make every
|
|
28
|
+
* other test run as if on a touch device.
|
|
29
|
+
*/
|
|
30
|
+
declare const uniTouchSupported: BrowserCommand<[]>;
|
|
31
|
+
/**
|
|
32
|
+
* A finger at the target's centre (or 24px below it, `outside`) for
|
|
33
|
+
* `touchStart` and `touchMove`; lifted for `touchEnd` and `touchCancel`.
|
|
34
|
+
*/
|
|
35
|
+
declare const uniTouch: BrowserCommand<[phase: TouchPhase, selector?: string, where?: 'center' | 'outside']>;
|
|
36
|
+
/** Media features Playwright emulates; `null` ends a feature's emulation. */
|
|
37
|
+
interface MediaFeatures {
|
|
38
|
+
readonly forcedColors?: 'active' | 'none' | null;
|
|
39
|
+
readonly contrast?: 'more' | 'no-preference' | null;
|
|
40
|
+
readonly reducedMotion?: 'reduce' | 'no-preference' | null;
|
|
41
|
+
readonly colorScheme?: 'light' | 'dark' | 'no-preference' | null;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Emulates media features for the whole page, frames included. Forced
|
|
45
|
+
* colors work in Chromium only.
|
|
46
|
+
*/
|
|
47
|
+
declare const uniEmulateMedia: BrowserCommand<[features: MediaFeatures]>;
|
|
48
|
+
declare const uniCommands: {
|
|
49
|
+
uniTouchSupported: BrowserCommand<[], any>;
|
|
50
|
+
uniTouch: BrowserCommand<[phase: TouchPhase, selector?: string | undefined, where?: "center" | "outside" | undefined], any>;
|
|
51
|
+
uniPointerDown: BrowserCommand<[selector: string, button: MouseButton], any>;
|
|
52
|
+
uniPointerMove: BrowserCommand<[selector: string, where: "center" | "outside"], any>;
|
|
53
|
+
uniPointerUp: BrowserCommand<[button: MouseButton], any>;
|
|
54
|
+
uniWheel: BrowserCommand<[deltaX: number, deltaY: number], any>;
|
|
55
|
+
uniKeyDown: BrowserCommand<[key: string], any>;
|
|
56
|
+
uniKeyUp: BrowserCommand<[key: string], any>;
|
|
57
|
+
uniEmulateMedia: BrowserCommand<[features: MediaFeatures], any>;
|
|
58
|
+
};
|
|
59
|
+
//#endregion
|
|
60
|
+
export { uniKeyUp as a, uniPointerUp as c, uniWheel as d, screenReaderCommands as f, uniKeyDown as i, uniTouch as l, uniCommands as n, uniPointerDown as o, uniEmulateMedia as r, uniPointerMove as s, MediaFeatures as t, uniTouchSupported as u };
|
|
61
|
+
//# sourceMappingURL=commands-DPaip6f1.d.ts.map
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { a as uniKeyUp, c as uniPointerUp, d as uniWheel, f as screenReaderCommands, i as uniKeyDown, l as uniTouch, n as uniCommands, o as uniPointerDown, r as uniEmulateMedia, s as uniPointerMove, t as MediaFeatures, u as uniTouchSupported } from "./commands-DPaip6f1.js";
|
|
2
|
+
export { MediaFeatures, screenReaderCommands, uniCommands, uniEmulateMedia, uniKeyDown, uniKeyUp, uniPointerDown, uniPointerMove, uniPointerUp, uniTouch, uniTouchSupported, uniWheel };
|