@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.
@@ -0,0 +1,383 @@
1
+ //#region src/conformance/template.d.ts
2
+ /**
3
+ * Adapter-neutral markup for conformance scenarios. A scenario describes the
4
+ * DOM a consumer would write — part elements, their content and the
5
+ * attributes the consumer owns — and each adapter renders it its own way:
6
+ * React spreads part props onto these elements, the custom element wraps them
7
+ * in its host. Scenarios never import a definition, so they can be written
8
+ * before the implementation exists.
9
+ */
10
+ interface TemplateElement {
11
+ /** Tag name, as the consumer would write it (`button`, `a`, `div`). */
12
+ readonly tag: string;
13
+ /** The UniAria part this element is, if any (`root`, `trigger`). */
14
+ readonly part?: string;
15
+ /** Consumer-owned attributes, with DOM names (`class`, `for`, `aria-label`). */
16
+ readonly attrs: Readonly<Record<string, string>>;
17
+ readonly children: readonly TemplateNode[];
18
+ }
19
+ /**
20
+ * Another UniAria component inside the markup, such as a checkbox in its group.
21
+ * React renders it with its own `useComponent()`; custom elements wrap its
22
+ * children in its own host.
23
+ */
24
+ interface TemplateComponent {
25
+ /** The nested component's definition name: `checkbox`. */
26
+ readonly component: string;
27
+ /** Its props, passed the way the adapter's consumers pass them. */
28
+ readonly props: Readonly<Record<string, unknown>>;
29
+ /** Its markup: part elements in it are its own parts. */
30
+ readonly children: readonly TemplateNode[];
31
+ }
32
+ /** What a `rows()` node renders from (Listbox virtualization spec). */
33
+ interface RenderedRows {
34
+ /** The component's `renderedKeys`: the options to render, in list order. */
35
+ readonly keys: readonly string[];
36
+ /** Its `renderedSections`: the groups to render, in list order. */
37
+ readonly sections: readonly string[];
38
+ }
39
+ /**
40
+ * Rows the consumer renders from the component's public state rather than
41
+ * from every item, as a virtualized list's consumer does. React renders
42
+ * them in a child component that selects `renderedKeys` and
43
+ * `renderedSections`, so the owner never renders again for them; custom
44
+ * elements render them again on `renderedkeyschange`, synchronously,
45
+ * keeping each element whose part and `data-key` stay.
46
+ */
47
+ interface TemplateRows {
48
+ readonly rows: (rendered: RenderedRows) => readonly TemplateNode[];
49
+ }
50
+ /** An element, a nested component, rows or a text node. */
51
+ type TemplateNode = TemplateElement | TemplateComponent | TemplateRows | string;
52
+ type TemplateAttrs = Readonly<Record<string, string>> & {
53
+ readonly part?: string;
54
+ };
55
+ /**
56
+ * Builds a template element: `h('button', { part: 'root' }, 'Save')`. The
57
+ * `part` key names the UniAria part; every other key is a consumer-owned
58
+ * attribute.
59
+ */
60
+ declare function h(tag: string, attrs?: TemplateAttrs | null, ...children: TemplateNode[]): TemplateElement;
61
+ /**
62
+ * Nests another component: `nested('checkbox', { value: 'email' }, …)`.
63
+ * Its children's `part`s are its own parts.
64
+ */
65
+ declare function nested(component: string, props?: Readonly<Record<string, unknown>> | null, ...children: TemplateNode[]): TemplateComponent;
66
+ /**
67
+ * Rows rendered from the component's `renderedKeys` and `renderedSections`:
68
+ * `rows(({ keys }) => keys.map((key) => h('li', { part: 'option', 'data-key': key }, key)))`.
69
+ * Each row's top element carries `data-key`, as a consumer's keyed list does.
70
+ */
71
+ declare function rows(render: (rendered: RenderedRows) => readonly TemplateNode[]): TemplateRows;
72
+ /** Whether a node is a nested component. */
73
+ declare function isNested(node: TemplateNode): node is TemplateComponent;
74
+ /** Whether a node is rows rendered from the component's state. */
75
+ declare function isRows(node: TemplateNode): node is TemplateRows;
76
+ /**
77
+ * Stands for `virtualizer` from `@uniaria/core/virtual` in a scenario's
78
+ * props: scenarios never import the implementation, so each adapter passes
79
+ * the real one in its place.
80
+ */
81
+ declare const VIRTUALIZER: unique symbol;
82
+ /**
83
+ * The component's part elements in document order, for adapters that render
84
+ * templates; nested components' parts and rows are left out.
85
+ */
86
+ declare function templateParts(nodes: readonly TemplateNode[]): TemplateElement[];
87
+ /**
88
+ * The CSS selector for a part of `component` (root AGENTS.md → Part
89
+ * markers): the root by `data-uni`, a part outside the root by
90
+ * `data-uni-part`, any other part by its `data-element` inside the root,
91
+ * minus those inside another component's root. A part no root is around
92
+ * counts too, as in a component rendered without its root, which has nothing
93
+ * else to scope its parts by. One compound, so `[data-key="…"]` can follow
94
+ * it.
95
+ */
96
+ declare function partSelector(component: string, part: string): string;
97
+ /** Which part of `component` an element is, by its markers. */
98
+ declare function partName(element: Element, component: string): string | undefined;
99
+ /** How `renderTemplate()` renders what only the adapter knows. */
100
+ interface RenderTemplateOptions {
101
+ /** Adds attributes to part elements as they are created. */
102
+ readonly decorate?: (element: HTMLElement, node: TemplateElement) => void;
103
+ /** Creates the element for a nested component; its children go into it. */
104
+ readonly host?: (node: TemplateComponent) => HTMLElement;
105
+ /** Is told where a `rows()` node goes, and renders it there itself. */
106
+ readonly rowsIn?: (node: TemplateRows, parent: ParentNode) => void;
107
+ /** A component's parts outside its root: its definition's `outsideParts`. */
108
+ readonly outsideParts?: (component: string) => readonly string[];
109
+ }
110
+ /**
111
+ * Renders a template of the component `component` to real DOM, marking part
112
+ * elements the way a consumer of the custom elements would (`partMarker()`):
113
+ * the root with `data-uni`, a part outside the root with `data-uni-part`,
114
+ * the others with `data-element`.
115
+ */
116
+ declare function renderTemplate(nodes: readonly TemplateNode[], doc: Document, component: string, { decorate, host, rowsIn, outsideParts }?: RenderTemplateOptions): DocumentFragment;
117
+ //#endregion
118
+ //#region src/conformance/contract.d.ts
119
+ /** What the runner asks an adapter to render. */
120
+ interface MountRequest {
121
+ /** Definition name: `button`. */
122
+ readonly component: string;
123
+ /** Initial props, passed the way the adapter's consumers pass them. */
124
+ readonly props: Readonly<Record<string, unknown>>;
125
+ /** The consumer's markup. Part elements carry `part`. */
126
+ readonly template: readonly TemplateNode[];
127
+ /** Component events to observe; call `record` whenever one fires. */
128
+ readonly events: readonly string[];
129
+ record(event: string, detail: unknown): void;
130
+ /** An empty element already attached to `document.body`. Render into it. */
131
+ readonly container: HTMLElement;
132
+ }
133
+ /**
134
+ * What an adapter returns from `mount()`.
135
+ *
136
+ * Guarantees every adapter must keep, because scenarios assert right after an
137
+ * awaited interaction:
138
+ * - Attribute writes for a state change are visible when the interaction that
139
+ * caused it resolves (the DOM binder writes synchronously after a flush).
140
+ * - `setProps()` resolves once the DOM reflects the new props.
141
+ */
142
+ interface AdapterMount {
143
+ /** Merges props, the way the adapter's consumers update them. */
144
+ setProps(props: Readonly<Record<string, unknown>>): Promise<void>;
145
+ /**
146
+ * Calls one of the component's public actions (`open`), the way the
147
+ * adapter's consumers do: the React handle's `actions`, the element's
148
+ * method. Resolves once the DOM reflects it.
149
+ */
150
+ call(action: string): Promise<void>;
151
+ /** The object the definition's `expose()` returns right now. */
152
+ state(): Readonly<Record<string, unknown>>;
153
+ /**
154
+ * How many times the framework rendered the component that owns the
155
+ * instance, including the first render. Adapters without renders (custom
156
+ * elements) return 0.
157
+ */
158
+ renderCount(): number;
159
+ /** Removes everything rendered and stops the instance. */
160
+ unmount(): Promise<void>;
161
+ }
162
+ interface ConformanceAdapter {
163
+ /** `react`, `elements`. Appears in test names. */
164
+ readonly name: string;
165
+ mount(request: MountRequest): Promise<AdapterMount>;
166
+ }
167
+ interface RecordedEvent {
168
+ /** camelCase event name: `press`. */
169
+ readonly name: string;
170
+ /** The event payload: the React callback argument or the DOM event's `detail`. */
171
+ readonly detail: unknown;
172
+ }
173
+ interface MountOptions<Props> {
174
+ /** The consumer's markup: one node or several. */
175
+ readonly template: TemplateNode | readonly TemplateNode[];
176
+ readonly props?: Partial<Props>;
177
+ /** Component events to record, camelCase. */
178
+ readonly events?: readonly string[];
179
+ }
180
+ /** A mounted component, as scenarios see it. */
181
+ interface Mounted<Props, State> {
182
+ /** The element the adapter rendered into. */
183
+ readonly container: HTMLElement;
184
+ /**
185
+ * The element of a part of the mounted component (`root` by default)
186
+ * inside the container, found by `partSelector()`: the root by
187
+ * `data-uni="<component>"`, a part outside the root by
188
+ * `data-uni-part="<component>-<part>"`, any other part by its
189
+ * `data-element` inside the root. Throws when there is none or more than one, so a
190
+ * missing marker fails loudly.
191
+ */
192
+ part(name?: string): HTMLElement;
193
+ parts(name: string): HTMLElement[];
194
+ /** Every recorded event so far, in order. */
195
+ readonly events: readonly RecordedEvent[];
196
+ /** Just the names of the recorded events, in order. */
197
+ eventNames(): string[];
198
+ clearEvents(): void;
199
+ state(): State;
200
+ setProps(props: Partial<Props>): Promise<void>;
201
+ /** Calls a public action of the component, such as `open`. */
202
+ call(action: string): Promise<void>;
203
+ renderCount(): number;
204
+ }
205
+ //#endregion
206
+ //#region src/conformance/scenario.d.ts
207
+ type MouseButton = 'left' | 'right' | 'middle';
208
+ /** Real mouse input through Playwright, at the target's centre. */
209
+ interface PointerInput {
210
+ /** Moves to the target and presses a button (left by default). */
211
+ down(target: Element, options?: {
212
+ button?: MouseButton;
213
+ }): Promise<void>;
214
+ /** Moves to the target's centre with the button state unchanged. */
215
+ move(target: Element): Promise<void>;
216
+ /** Moves to a point 24px below the target. */
217
+ moveOutside(target: Element): Promise<void>;
218
+ /** Releases a button (left by default) wherever the pointer is. */
219
+ up(options?: {
220
+ button?: MouseButton;
221
+ }): Promise<void>;
222
+ /**
223
+ * Turns the wheel wherever the pointer is, with the modifier keys held
224
+ * down: a negative `deltaY` scrolls up. One call is one `wheel` event.
225
+ */
226
+ wheel(delta: {
227
+ deltaX?: number;
228
+ deltaY?: number;
229
+ }): Promise<void>;
230
+ /** `down` on the target, then `up`. */
231
+ click(target: Element, options?: {
232
+ button?: MouseButton;
233
+ }): Promise<void>;
234
+ }
235
+ /**
236
+ * Real keyboard input through Playwright, sent to the focused element. Key
237
+ * names follow Playwright: `Enter`, `Space`, `Escape`, `ArrowDown`.
238
+ */
239
+ interface KeyboardInput {
240
+ /** Presses and holds a key. Calling it again while held sends a repeat. */
241
+ down(key: string): Promise<void>;
242
+ up(key: string): Promise<void>;
243
+ /** `down`, then `up`. */
244
+ press(key: string): Promise<void>;
245
+ /** Moves focus with Tab, or Shift+Tab with `isBackward`. */
246
+ tab(options?: {
247
+ isBackward?: boolean;
248
+ }): Promise<void>;
249
+ }
250
+ /**
251
+ * Real touch input, one finger at a time, through DevTools: only Chromium
252
+ * takes it, so scenarios that use it declare `needs: ['touch']`. The browser
253
+ * turns it into touch and pointer events, compatibility mouse events and a
254
+ * click, and a finger that moves away starts a scroll.
255
+ */
256
+ interface TouchInput {
257
+ /** Puts a finger down on the target's centre. */
258
+ down(target: Element): Promise<void>;
259
+ /** Moves the finger to the target's centre. */
260
+ move(target: Element): Promise<void>;
261
+ /** Moves the finger 24px below the target. */
262
+ moveOutside(target: Element): Promise<void>;
263
+ /** Lifts the finger wherever it is. */
264
+ up(): Promise<void>;
265
+ /** `down` on the target, then `up`. */
266
+ tap(target: Element): Promise<void>;
267
+ }
268
+ /** Which real screen reader a scenario runs with. */
269
+ type ScreenReaderName = 'voiceover' | 'nvda';
270
+ /**
271
+ * A real screen reader driven through Guidepup: VoiceOver on macOS, NVDA on
272
+ * Windows, both with Chromium. Keys go through the operating system, so the
273
+ * screen reader hears them as a user's and speaks what they do. Only in
274
+ * scenarios that declare `needs: ['screenReader']`, which run nightly.
275
+ *
276
+ * The screen readers word things differently (`check box` in NVDA,
277
+ * `checkbox` in VoiceOver), so scenarios match what they need to hear, not
278
+ * whole phrases.
279
+ */
280
+ interface ScreenReaderInput {
281
+ /** Which screen reader runs. */
282
+ readonly name: ScreenReaderName;
283
+ /**
284
+ * Clicks `element` (a text field before the component), waits for the
285
+ * screen reader to speak it and forgets what it said: the scenario starts
286
+ * from there, usually with Tab into the component.
287
+ */
288
+ startFrom(element: HTMLElement): Promise<void>;
289
+ /**
290
+ * Presses a key through the screen reader (Playwright's names: `Tab`,
291
+ * `ArrowDown`, `Shift+Tab`) and waits until it stops speaking.
292
+ */
293
+ press(key: string): Promise<void>;
294
+ /** Types text through the screen reader, a key per character. */
295
+ type(text: string): Promise<void>;
296
+ /** Moves the screen reader's own cursor to the next item. */
297
+ next(): Promise<void>;
298
+ /**
299
+ * What the screen reader holds for the element with keyboard focus: NVDA
300
+ * reads it again (NVDA+Tab), VoiceOver gives its cursor's item text. A
301
+ * check that holds when the speech on arrival is cut short.
302
+ */
303
+ readFocus(): Promise<string>;
304
+ /**
305
+ * What it has said since `startFrom` or the last call: each key's phrases
306
+ * in order, joined with ` / `.
307
+ */
308
+ spoken(): Promise<string>;
309
+ }
310
+ /** Input a scenario needs beyond mouse and keyboard. */
311
+ type InputNeed = 'touch' | 'screenReader';
312
+ interface ScenarioContext<Props, State> {
313
+ /** Mounts the suite's component through the adapter under test. */
314
+ mount(options: MountOptions<Props>): Promise<Mounted<Props, State>>;
315
+ pointer: PointerInput;
316
+ keyboard: KeyboardInput;
317
+ /** Only in scenarios that declare `needs: ['touch']`. */
318
+ touch: TouchInput;
319
+ /** Only in scenarios that declare `needs: ['screenReader']`. */
320
+ screenReader: ScreenReaderInput;
321
+ /**
322
+ * Reads the whole document with the virtual screen reader and returns every
323
+ * phrase, from `document` to `end of document`.
324
+ */
325
+ spoken(): Promise<string[]>;
326
+ /**
327
+ * Starts hearing live regions with the virtual screen reader. The returned
328
+ * function gives what they have announced since, in order, each phrase
329
+ * prefixed with its politeness: `assertive: Saved`. Listening lasts until
330
+ * the scenario ends, and `spoken()` can't run meanwhile.
331
+ */
332
+ listen(): Promise<() => Promise<string[]>>;
333
+ }
334
+ type ScenarioRun<Props, State> = (context: ScenarioContext<Props, State>) => Promise<void>;
335
+ interface ScenarioOptions {
336
+ /** Roadmap phase whose features the scenario needs. Default 1. */
337
+ readonly phase?: number;
338
+ /**
339
+ * Input beyond mouse and keyboard. Where the browser can't take it, the
340
+ * scenario is skipped; a scenario that needs a real screen reader runs
341
+ * only where one runs.
342
+ */
343
+ readonly needs?: readonly InputNeed[];
344
+ }
345
+ interface Scenario<Props, State> {
346
+ readonly kind: 'scenario';
347
+ readonly name: string;
348
+ readonly phase: number;
349
+ readonly needs: readonly InputNeed[];
350
+ readonly run: ScenarioRun<Props, State>;
351
+ }
352
+ interface ScenarioGroup<Props, State> {
353
+ readonly kind: 'group';
354
+ readonly name: string;
355
+ readonly children: readonly (Scenario<Props, State> | ScenarioGroup<Props, State>)[];
356
+ }
357
+ interface ConformanceSuite<Props, State> {
358
+ /** Definition name: `button`. */
359
+ readonly component: string;
360
+ readonly children: readonly (Scenario<Props, State> | ScenarioGroup<Props, State>)[];
361
+ }
362
+ interface ConformanceBuilder<Props, State> {
363
+ scenario(name: string, run: ScenarioRun<Props, State>, options?: ScenarioOptions): void;
364
+ group(name: string, define: () => void): void;
365
+ }
366
+ /**
367
+ * Declares a component's conformance suite:
368
+ *
369
+ * ```ts
370
+ * export const buttonConformance = defineConformance<ButtonProps, ButtonState>(
371
+ * 'button',
372
+ * ({ scenario, group }) => {
373
+ * group('keyboard', () => {
374
+ * scenario('Enter presses on keyup', async ({ mount, keyboard }) => { … });
375
+ * });
376
+ * },
377
+ * );
378
+ * ```
379
+ */
380
+ declare function defineConformance<Props, State>(component: string, define: (builder: ConformanceBuilder<Props, State>) => void): ConformanceSuite<Props, State>;
381
+ //#endregion
382
+ export { h as A, RenderedRows as C, TemplateNode as D, TemplateElement as E, partSelector as F, renderTemplate as I, rows as L, isRows as M, nested as N, TemplateRows as O, partName as P, templateParts as R, RenderTemplateOptions as S, TemplateComponent as T, ConformanceAdapter as _, MouseButton as a, Mounted as b, ScenarioContext as c, ScenarioRun as d, ScreenReaderInput as f, AdapterMount as g, defineConformance as h, KeyboardInput as i, isNested as j, VIRTUALIZER as k, ScenarioGroup as l, TouchInput as m, ConformanceSuite as n, PointerInput as o, ScreenReaderName as p, InputNeed as r, Scenario as s, ConformanceBuilder as t, ScenarioOptions as u, MountOptions as v, TemplateAttrs as w, RecordedEvent as x, MountRequest as y };
383
+ //# sourceMappingURL=scenario-DMaR4JJc.d.ts.map
@@ -0,0 +1,152 @@
1
+ import { partMarker } from "@uniaria/core";
2
+ //#region src/conformance/template.ts
3
+ /**
4
+ * Adapter-neutral markup for conformance scenarios. A scenario describes the
5
+ * DOM a consumer would write — part elements, their content and the
6
+ * attributes the consumer owns — and each adapter renders it its own way:
7
+ * React spreads part props onto these elements, the custom element wraps them
8
+ * in its host. Scenarios never import a definition, so they can be written
9
+ * before the implementation exists.
10
+ */
11
+ /**
12
+ * Builds a template element: `h('button', { part: 'root' }, 'Save')`. The
13
+ * `part` key names the UniAria part; every other key is a consumer-owned
14
+ * attribute.
15
+ */
16
+ function h(tag, attrs, ...children) {
17
+ const { part, ...rest } = attrs ?? {};
18
+ return part === void 0 ? {
19
+ tag,
20
+ attrs: rest,
21
+ children
22
+ } : {
23
+ tag,
24
+ part,
25
+ attrs: rest,
26
+ children
27
+ };
28
+ }
29
+ /**
30
+ * Nests another component: `nested('checkbox', { value: 'email' }, …)`.
31
+ * Its children's `part`s are its own parts.
32
+ */
33
+ function nested(component, props, ...children) {
34
+ return {
35
+ component,
36
+ props: props ?? {},
37
+ children
38
+ };
39
+ }
40
+ /**
41
+ * Rows rendered from the component's `renderedKeys` and `renderedSections`:
42
+ * `rows(({ keys }) => keys.map((key) => h('li', { part: 'option', 'data-key': key }, key)))`.
43
+ * Each row's top element carries `data-key`, as a consumer's keyed list does.
44
+ */
45
+ function rows(render) {
46
+ return { rows: render };
47
+ }
48
+ /** Whether a node is a nested component. */
49
+ function isNested(node) {
50
+ return typeof node !== "string" && "component" in node;
51
+ }
52
+ /** Whether a node is rows rendered from the component's state. */
53
+ function isRows(node) {
54
+ return typeof node !== "string" && "rows" in node;
55
+ }
56
+ /**
57
+ * Stands for `virtualizer` from `@uniaria/core/virtual` in a scenario's
58
+ * props: scenarios never import the implementation, so each adapter passes
59
+ * the real one in its place.
60
+ */
61
+ const VIRTUALIZER = Symbol.for("uniaria.conformance.virtualizer");
62
+ /**
63
+ * The component's part elements in document order, for adapters that render
64
+ * templates; nested components' parts and rows are left out.
65
+ */
66
+ function templateParts(nodes) {
67
+ const found = [];
68
+ const walk = (list) => {
69
+ for (const node of list) {
70
+ if (typeof node === "string" || isNested(node) || isRows(node)) continue;
71
+ if (node.part !== void 0) found.push(node);
72
+ walk(node.children);
73
+ }
74
+ };
75
+ walk(nodes);
76
+ return found;
77
+ }
78
+ /** `[attribute="value"]` for a part's marker. */
79
+ function markerSelector(component, part, isOutside = false) {
80
+ const [attribute, value] = partMarker(component, part, isOutside);
81
+ return `[${attribute}="${value}"]`;
82
+ }
83
+ /**
84
+ * The CSS selector for a part of `component` (root AGENTS.md → Part
85
+ * markers): the root by `data-uni`, a part outside the root by
86
+ * `data-uni-part`, any other part by its `data-element` inside the root,
87
+ * minus those inside another component's root. A part no root is around
88
+ * counts too, as in a component rendered without its root, which has nothing
89
+ * else to scope its parts by. One compound, so `[data-key="…"]` can follow
90
+ * it.
91
+ */
92
+ function partSelector(component, part) {
93
+ const root = markerSelector(component, "root");
94
+ if (part === "root") return root;
95
+ const marker = markerSelector(component, part);
96
+ return `:is(${markerSelector(component, part, true)}, ${root} ${marker}:not(${root} [data-uni]:not(${root}) *), ${marker}:not([data-uni] *))`;
97
+ }
98
+ /** Which part of `component` an element is, by its markers. */
99
+ function partName(element, component) {
100
+ if (element.getAttribute("data-uni") === component) return "root";
101
+ const outside = element.getAttribute("data-uni-part");
102
+ if (outside?.startsWith(`${component}-`)) return outside.slice(component.length + 1).replace(/-([a-z0-9])/g, (_, letter) => letter.toUpperCase());
103
+ const marker = element.getAttribute("data-element");
104
+ if (!marker) return void 0;
105
+ const part = marker.charAt(0).toLowerCase() + marker.slice(1);
106
+ return element.matches(partSelector(component, part)) ? part : void 0;
107
+ }
108
+ /**
109
+ * Renders a template of the component `component` to real DOM, marking part
110
+ * elements the way a consumer of the custom elements would (`partMarker()`):
111
+ * the root with `data-uni`, a part outside the root with `data-uni-part`,
112
+ * the others with `data-element`.
113
+ */
114
+ function renderTemplate(nodes, doc, component, { decorate, host, rowsIn, outsideParts } = {}) {
115
+ const fragment = doc.createDocumentFragment();
116
+ const append = (parent, list, owner = component) => {
117
+ for (const node of list) {
118
+ if (typeof node === "string") {
119
+ parent.appendChild(doc.createTextNode(node));
120
+ continue;
121
+ }
122
+ if (isRows(node)) {
123
+ if (!rowsIn) throw new Error("The template has rows, which need the component.");
124
+ rowsIn(node, parent);
125
+ continue;
126
+ }
127
+ if (isNested(node)) {
128
+ if (!host) throw new Error(`The template nests "${node.component}", which needs a host.`);
129
+ const element = host(node);
130
+ append(element, node.children, node.component);
131
+ parent.appendChild(element);
132
+ continue;
133
+ }
134
+ const element = doc.createElement(node.tag);
135
+ for (const [name, value] of Object.entries(node.attrs)) element.setAttribute(name, value);
136
+ const { part } = node;
137
+ if (part !== void 0) {
138
+ const isOutside = outsideParts?.(owner).includes(part) ?? false;
139
+ element.setAttribute(...partMarker(owner, part, isOutside));
140
+ }
141
+ if (part !== void 0) decorate?.(element, node);
142
+ append(element, node.children, owner);
143
+ parent.appendChild(element);
144
+ }
145
+ };
146
+ append(fragment, nodes);
147
+ return fragment;
148
+ }
149
+ //#endregion
150
+ export { nested as a, renderTemplate as c, isRows as i, rows as l, h as n, partName as o, isNested as r, partSelector as s, VIRTUALIZER as t, templateParts as u };
151
+
152
+ //# sourceMappingURL=template-Cn6bp45u.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"template-Cn6bp45u.js","names":[],"sources":["../src/conformance/template.ts"],"sourcesContent":["/**\n * Adapter-neutral markup for conformance scenarios. A scenario describes the\n * DOM a consumer would write — part elements, their content and the\n * attributes the consumer owns — and each adapter renders it its own way:\n * React spreads part props onto these elements, the custom element wraps them\n * in its host. Scenarios never import a definition, so they can be written\n * before the implementation exists.\n */\n\nimport { partMarker } from '@uniaria/core';\n\nexport interface TemplateElement {\n /** Tag name, as the consumer would write it (`button`, `a`, `div`). */\n readonly tag: string;\n /** The UniAria part this element is, if any (`root`, `trigger`). */\n readonly part?: string;\n /** Consumer-owned attributes, with DOM names (`class`, `for`, `aria-label`). */\n readonly attrs: Readonly<Record<string, string>>;\n readonly children: readonly TemplateNode[];\n}\n\n/**\n * Another UniAria component inside the markup, such as a checkbox in its group.\n * React renders it with its own `useComponent()`; custom elements wrap its\n * children in its own host.\n */\nexport interface TemplateComponent {\n /** The nested component's definition name: `checkbox`. */\n readonly component: string;\n /** Its props, passed the way the adapter's consumers pass them. */\n readonly props: Readonly<Record<string, unknown>>;\n /** Its markup: part elements in it are its own parts. */\n readonly children: readonly TemplateNode[];\n}\n\n/** What a `rows()` node renders from (Listbox virtualization spec). */\nexport interface RenderedRows {\n /** The component's `renderedKeys`: the options to render, in list order. */\n readonly keys: readonly string[];\n /** Its `renderedSections`: the groups to render, in list order. */\n readonly sections: readonly string[];\n}\n\n/**\n * Rows the consumer renders from the component's public state rather than\n * from every item, as a virtualized list's consumer does. React renders\n * them in a child component that selects `renderedKeys` and\n * `renderedSections`, so the owner never renders again for them; custom\n * elements render them again on `renderedkeyschange`, synchronously,\n * keeping each element whose part and `data-key` stay.\n */\nexport interface TemplateRows {\n readonly rows: (rendered: RenderedRows) => readonly TemplateNode[];\n}\n\n/** An element, a nested component, rows or a text node. */\nexport type TemplateNode =\n TemplateElement | TemplateComponent | TemplateRows | string;\n\nexport type TemplateAttrs = Readonly<Record<string, string>> & {\n readonly part?: string;\n};\n\n/**\n * Builds a template element: `h('button', { part: 'root' }, 'Save')`. The\n * `part` key names the UniAria part; every other key is a consumer-owned\n * attribute.\n */\nexport function h(\n tag: string,\n attrs?: TemplateAttrs | null,\n ...children: TemplateNode[]\n): TemplateElement {\n const { part, ...rest } = attrs ?? {};\n return part === undefined\n ? { tag, attrs: rest, children }\n : { tag, part, attrs: rest, children };\n}\n\n/**\n * Nests another component: `nested('checkbox', { value: 'email' }, …)`.\n * Its children's `part`s are its own parts.\n */\nexport function nested(\n component: string,\n props?: Readonly<Record<string, unknown>> | null,\n ...children: TemplateNode[]\n): TemplateComponent {\n return { component, props: props ?? {}, children };\n}\n\n/**\n * Rows rendered from the component's `renderedKeys` and `renderedSections`:\n * `rows(({ keys }) => keys.map((key) => h('li', { part: 'option', 'data-key': key }, key)))`.\n * Each row's top element carries `data-key`, as a consumer's keyed list does.\n */\nexport function rows(\n render: (rendered: RenderedRows) => readonly TemplateNode[],\n): TemplateRows {\n return { rows: render };\n}\n\n/** Whether a node is a nested component. */\nexport function isNested(node: TemplateNode): node is TemplateComponent {\n return typeof node !== 'string' && 'component' in node;\n}\n\n/** Whether a node is rows rendered from the component's state. */\nexport function isRows(node: TemplateNode): node is TemplateRows {\n return typeof node !== 'string' && 'rows' in node;\n}\n\n/**\n * Stands for `virtualizer` from `@uniaria/core/virtual` in a scenario's\n * props: scenarios never import the implementation, so each adapter passes\n * the real one in its place.\n */\nexport const VIRTUALIZER: unique symbol = Symbol.for(\n 'uniaria.conformance.virtualizer',\n);\n\n/**\n * The component's part elements in document order, for adapters that render\n * templates; nested components' parts and rows are left out.\n */\nexport function templateParts(\n nodes: readonly TemplateNode[],\n): TemplateElement[] {\n const found: TemplateElement[] = [];\n const walk = (list: readonly TemplateNode[]) => {\n for (const node of list) {\n if (typeof node === 'string' || isNested(node) || isRows(node)) continue;\n if (node.part !== undefined) found.push(node);\n walk(node.children);\n }\n };\n walk(nodes);\n return found;\n}\n\n/** `[attribute=\"value\"]` for a part's marker. */\nfunction markerSelector(component: string, part: string, isOutside = false) {\n const [attribute, value] = partMarker(component, part, isOutside);\n return `[${attribute}=\"${value}\"]`;\n}\n\n/**\n * The CSS selector for a part of `component` (root AGENTS.md → Part\n * markers): the root by `data-uni`, a part outside the root by\n * `data-uni-part`, any other part by its `data-element` inside the root,\n * minus those inside another component's root. A part no root is around\n * counts too, as in a component rendered without its root, which has nothing\n * else to scope its parts by. One compound, so `[data-key=\"…\"]` can follow\n * it.\n */\nexport function partSelector(component: string, part: string): string {\n const root = markerSelector(component, 'root');\n if (part === 'root') return root;\n const marker = markerSelector(component, part);\n return (\n `:is(${markerSelector(component, part, true)}, ` +\n `${root} ${marker}:not(${root} [data-uni]:not(${root}) *), ` +\n `${marker}:not([data-uni] *))`\n );\n}\n\n/** Which part of `component` an element is, by its markers. */\nexport function partName(\n element: Element,\n component: string,\n): string | undefined {\n if (element.getAttribute('data-uni') === component) return 'root';\n const outside = element.getAttribute('data-uni-part');\n if (outside?.startsWith(`${component}-`)) {\n return outside\n .slice(component.length + 1)\n .replace(/-([a-z0-9])/g, (_, letter: string) => letter.toUpperCase());\n }\n const marker = element.getAttribute('data-element');\n if (!marker) return undefined;\n const part = marker.charAt(0).toLowerCase() + marker.slice(1);\n return element.matches(partSelector(component, part)) ? part : undefined;\n}\n\n/** How `renderTemplate()` renders what only the adapter knows. */\nexport interface RenderTemplateOptions {\n /** Adds attributes to part elements as they are created. */\n readonly decorate?: (element: HTMLElement, node: TemplateElement) => void;\n /** Creates the element for a nested component; its children go into it. */\n readonly host?: (node: TemplateComponent) => HTMLElement;\n /** Is told where a `rows()` node goes, and renders it there itself. */\n readonly rowsIn?: (node: TemplateRows, parent: ParentNode) => void;\n /** A component's parts outside its root: its definition's `outsideParts`. */\n readonly outsideParts?: (component: string) => readonly string[];\n}\n\n/**\n * Renders a template of the component `component` to real DOM, marking part\n * elements the way a consumer of the custom elements would (`partMarker()`):\n * the root with `data-uni`, a part outside the root with `data-uni-part`,\n * the others with `data-element`.\n */\nexport function renderTemplate(\n nodes: readonly TemplateNode[],\n doc: Document,\n component: string,\n { decorate, host, rowsIn, outsideParts }: RenderTemplateOptions = {},\n): DocumentFragment {\n const fragment = doc.createDocumentFragment();\n const append = (\n parent: ParentNode,\n list: readonly TemplateNode[],\n owner = component,\n ) => {\n for (const node of list) {\n if (typeof node === 'string') {\n parent.appendChild(doc.createTextNode(node));\n continue;\n }\n if (isRows(node)) {\n if (!rowsIn) {\n throw new Error('The template has rows, which need the component.');\n }\n rowsIn(node, parent);\n continue;\n }\n if (isNested(node)) {\n if (!host) {\n throw new Error(\n `The template nests \"${node.component}\", which needs a host.`,\n );\n }\n const element = host(node);\n append(element, node.children, node.component);\n parent.appendChild(element);\n continue;\n }\n const element = doc.createElement(node.tag);\n for (const [name, value] of Object.entries(node.attrs)) {\n element.setAttribute(name, value);\n }\n const { part } = node;\n if (part !== undefined) {\n const isOutside = outsideParts?.(owner).includes(part) ?? false;\n element.setAttribute(...partMarker(owner, part, isOutside));\n }\n if (part !== undefined) decorate?.(element, node);\n append(element, node.children, owner);\n parent.appendChild(element);\n }\n };\n append(fragment, nodes);\n return fragment;\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,76 @@
1
1
  {
2
2
  "name": "@uniaria/testing",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
4
+ "description": "Testing tools for UniAria: test instances without a framework, the conformance suite runner and screen-reader helpers",
5
+ "type": "module",
6
+ "sideEffects": false,
7
+ "exports": {
8
+ ".": {
9
+ "uniaria-source": "./src/index.ts",
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ },
13
+ "./browser": {
14
+ "uniaria-source": "./src/browser.ts",
15
+ "types": "./dist/browser.d.ts",
16
+ "default": "./dist/browser.js"
17
+ },
18
+ "./commands": {
19
+ "uniaria-source": "./src/commands.ts",
20
+ "types": "./dist/commands.d.ts",
21
+ "default": "./dist/commands.js"
22
+ },
23
+ "./package.json": "./package.json"
24
+ },
25
+ "files": [
26
+ "dist"
27
+ ],
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "git+https://github.com/tenphi/uniaria.git",
31
+ "directory": "packages/testing"
32
+ },
33
+ "keywords": [
34
+ "headless",
35
+ "ui",
36
+ "testing",
37
+ "accessibility",
38
+ "screen-reader"
39
+ ],
40
+ "author": "Andrey Yamanov",
41
+ "license": "MIT",
42
+ "publishConfig": {
43
+ "access": "public"
44
+ },
45
+ "bugs": {
46
+ "url": "https://github.com/tenphi/uniaria/issues"
47
+ },
48
+ "homepage": "https://github.com/tenphi/uniaria#readme",
49
+ "engines": {
50
+ "node": ">=22"
51
+ },
52
+ "dependencies": {
53
+ "@guidepup/virtual-screen-reader": "^0.33.0",
54
+ "@uniaria/core": "0.1.0"
55
+ },
56
+ "peerDependencies": {
57
+ "@guidepup/guidepup": "^0.35.0",
58
+ "@vitest/browser-playwright": "^5.0.0",
59
+ "vitest": "^5.0.0"
60
+ },
61
+ "peerDependenciesMeta": {
62
+ "@guidepup/guidepup": {
63
+ "optional": true
64
+ },
65
+ "@vitest/browser-playwright": {
66
+ "optional": true
67
+ }
68
+ },
69
+ "devDependencies": {
70
+ "@guidepup/guidepup": "^0.35.0"
71
+ },
72
+ "scripts": {
73
+ "build": "tsdown",
74
+ "typecheck": "tsc -p tsconfig.json"
75
+ }
6
76
  }