@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 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
- # Temporary Holding Version
1
+ # @uniaria/testing
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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
@@ -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 };