@shivaedev/test-story 0.13.0 → 0.14.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/CHANGELOG.md +7 -0
- package/README.md +5 -1
- package/dist/server/internal/screenAccessibility.d.ts +9 -0
- package/dist/server/internal/screenAccessibility.d.ts.map +1 -0
- package/dist/server/internal/screenAccessibility.js +44 -0
- package/dist/server/internal/screenAccessibility.js.map +1 -0
- package/dist/server/internal/screenElements.d.ts +6 -5
- package/dist/server/internal/screenElements.d.ts.map +1 -1
- package/dist/server/internal/screenElements.js +9 -11
- package/dist/server/internal/screenElements.js.map +1 -1
- package/dist/server/screenDriver.d.ts +10 -6
- package/dist/server/screenDriver.d.ts.map +1 -1
- package/dist/server/screenDriver.js +10 -7
- package/dist/server/screenDriver.js.map +1 -1
- package/package.json +2 -2
- package/src/server/internal/screenAccessibility.ts +50 -0
- package/src/server/internal/screenElements.ts +18 -16
- package/src/server/screenDriver.ts +16 -11
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.14.0 - 2026-10-19
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- `screenDriver` actions and text/value readouts accept `ScreenTarget` selectors or `{ role, name }` targets, with exact string or regular-expression accessible names, hidden-element exclusion, candidate diagnostics and role/name narration.
|
|
8
|
+
- `screenDriver.accessible(element)` computes an existing element's accessible name and role without narration; `accessible(selector)` keeps its narrated readout.
|
|
9
|
+
|
|
3
10
|
## 0.13.0 - 2026-10-19
|
|
4
11
|
|
|
5
12
|
### Added
|
package/README.md
CHANGED
|
@@ -581,8 +581,12 @@ verbs: ({ view, requests }, story) => screenDriver({
|
|
|
581
581
|
|
|
582
582
|
`text(selector?)` returns rendered text from the selected element or the whole root. `value(selector)` returns the current input or textarea value. Both narrate what they read through `tell` and leave assertions to the spec. Entry narration omits the supplied value.
|
|
583
583
|
|
|
584
|
+
`click`, `enter`, `submit`, `text` and `value` also accept a `ScreenTarget` object such as `{ role: "button", name: "Save" }` or `{ role: "textbox", name: /email/i }`. The role and computed accessible name must match exactly when strings are used; regular expressions match the name. Like Testing Library's role queries, these targets exclude hidden elements, including `aria-hidden` ancestors. Missing or ambiguous targets fail with the requested role, name and the visible candidates on screen. Narration names the role and name instead of a selector.
|
|
585
|
+
|
|
584
586
|
`accessible(selector)` returns `{ name, role }` computed by `dom-accessibility-api`, including native roles and label references that take precedence over `aria-label`. It narrates the readout and uses the same single rendered target checks as other readouts. Assert on this result when checking a control's accessible name; reading its raw attributes does not compute that name. This DOM computation does not inspect a browser's accessibility tree or prove speech from a screen reader.
|
|
585
587
|
|
|
588
|
+
`accessible(element)` uses the same computation on an `Element` the kit already holds and tells no story line.
|
|
589
|
+
|
|
586
590
|
For React, pass `act: async (action) => { await act(action); }` using React's `act`. The adapter wraps each action and flushes renderer work before checking the pending signal, including while waiting for an asynchronous response. The [React driver stories](https://github.com/ShivaeDev/platform/blob/main/packages/effect-react/src/screenDriver.dom.spec.tsx) cover a native pending atom, a rendered response and a controlled field without React warnings. The driver itself has no renderer dependency.
|
|
587
591
|
|
|
588
592
|
Settlement has a live-time limit of 3 seconds, configurable with the driver's `timeoutMs` or per call with `settle({ timeoutMs })`, and fails with the current screen text and advice when that limit expires. Use the existing DOM environment and scoped `routeOutbound` routing to reach `fakeHttpService`; the driver does not install another transport. The [screen stories](src/server/screenSignals.dom.spec.ts) hold a real HTTP response across repeated renders and sign in by filling and submitting a form against a fake identity provider. These fixtures prove DOM interactions and loopback HTTP, not a browser or a production authentication system.
|
|
@@ -597,7 +601,7 @@ Import each module with the prefix `@shivaedev/test-story/`; there is no root en
|
|
|
597
601
|
| `domFetch.ts` | `domFetch(baseUrl)` |
|
|
598
602
|
| `seedCookies.ts` | `seedCookies(cookies)`, `DomCookie` |
|
|
599
603
|
| `routeOutbound.ts` | `routeOutbound(routes)` |
|
|
600
|
-
| `screenDriver.ts` | `screenDriver(options)`, `ScreenDriverOptions`, `ScreenDriver` |
|
|
604
|
+
| `screenDriver.ts` | `screenDriver(options)`, `ScreenDriverOptions`, `ScreenDriver`, `ScreenTarget` |
|
|
601
605
|
| `domNavigate.ts` | `domNavigate(url?)` |
|
|
602
606
|
| `genreTags.ts` | `genreTags(...kitNames)`, `GenreTag` |
|
|
603
607
|
| `domEnvironment.config.ts` | Default Vitest environment preserving Node host globals and happy-dom document transport |
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ScreenTarget } from "#screenDriver.ts";
|
|
2
|
+
export declare function accessibility(element: Element): {
|
|
3
|
+
name: string;
|
|
4
|
+
role: string | null;
|
|
5
|
+
};
|
|
6
|
+
export declare function targetName(target: ScreenTarget): string;
|
|
7
|
+
export declare function isRendered(found: HTMLElement): boolean;
|
|
8
|
+
export declare function roleElement(root: HTMLElement, target: Exclude<ScreenTarget, string>): HTMLElement;
|
|
9
|
+
//# sourceMappingURL=screenAccessibility.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"screenAccessibility.d.ts","sourceRoot":"","sources":["../../../src/server/internal/screenAccessibility.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAErD,wBAAgB,aAAa,CAAC,OAAO,EAAE,OAAO;IACpC,IAAI;IAAkC,IAAI;EACnD;AAED,wBAAgB,UAAU,CAAC,MAAM,EAAE,YAAY,GAAG,MAAM,CAIvD;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,WAAW,WAY5C;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,OAAO,CAAC,YAAY,EAAE,MAAM,CAAC,eAsBnF"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { computeAccessibleName, getRole, isInaccessible } from "dom-accessibility-api";
|
|
2
|
+
export function accessibility(element) {
|
|
3
|
+
return { name: computeAccessibleName(element), role: getRole(element) };
|
|
4
|
+
}
|
|
5
|
+
export function targetName(target) {
|
|
6
|
+
return typeof target === "string"
|
|
7
|
+
? target
|
|
8
|
+
: `${target.role} named ${typeof target.name === "string" ? JSON.stringify(target.name) : String(target.name)}`;
|
|
9
|
+
}
|
|
10
|
+
export function isRendered(found) {
|
|
11
|
+
const visibility = getComputedStyle(found).visibility;
|
|
12
|
+
if (visibility === "hidden" || visibility === "collapse") {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
for (let ancestor = found; ancestor; ancestor = ancestor.parentElement) {
|
|
16
|
+
const style = getComputedStyle(ancestor);
|
|
17
|
+
if (ancestor.hidden || style.display === "none" || style.contentVisibility === "hidden") {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
export function roleElement(root, target) {
|
|
24
|
+
const candidates = Array.from(root.querySelectorAll("*"))
|
|
25
|
+
.filter((element) => isRendered(element) && !isInaccessible(element))
|
|
26
|
+
.map((element) => ({ ...accessibility(element), element }))
|
|
27
|
+
.filter(({ role }) => role !== null);
|
|
28
|
+
const matches = candidates.filter(({ role, name }) => {
|
|
29
|
+
if (role !== target.role) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
if (typeof target.name === "string") {
|
|
33
|
+
return name === target.name;
|
|
34
|
+
}
|
|
35
|
+
return new RegExp(target.name.source, target.name.flags).test(name);
|
|
36
|
+
});
|
|
37
|
+
const found = matches[0];
|
|
38
|
+
if (matches.length === 1 && found) {
|
|
39
|
+
return found.element;
|
|
40
|
+
}
|
|
41
|
+
const listing = candidates.map(({ role, name }) => ` - ${role} named ${JSON.stringify(name)}`).join("\n");
|
|
42
|
+
throw new Error(`The screen has ${matches.length} matches for ${targetName(target)}.\nhelp: Use a role and name for exactly one visible target.\nCandidates on screen:\n${listing || " (none)"}`);
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=screenAccessibility.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"screenAccessibility.js","sourceRoot":"","sources":["../../../src/server/internal/screenAccessibility.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import type { ScreenTarget } from "#screenDriver.ts";
|
|
1
2
|
export declare function screenElements(root: () => HTMLElement): {
|
|
2
|
-
element: (selector:
|
|
3
|
-
enabled: (found: HTMLElement, selector:
|
|
4
|
-
enter: (selector:
|
|
5
|
-
form: (selector:
|
|
6
|
-
input: (selector:
|
|
3
|
+
element: (selector: ScreenTarget) => HTMLElement;
|
|
4
|
+
enabled: (found: HTMLElement, selector: ScreenTarget) => void;
|
|
5
|
+
enter: (selector: ScreenTarget, value: string) => void;
|
|
6
|
+
form: (selector: ScreenTarget) => HTMLFormElement;
|
|
7
|
+
input: (selector: ScreenTarget) => HTMLInputElement | HTMLTextAreaElement;
|
|
7
8
|
rendered: (found: HTMLElement, selector: string) => HTMLElement;
|
|
8
9
|
};
|
|
9
10
|
//# sourceMappingURL=screenElements.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"screenElements.d.ts","sourceRoot":"","sources":["../../../src/server/internal/screenElements.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"screenElements.d.ts","sourceRoot":"","sources":["../../../src/server/internal/screenElements.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAErD,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,WAAW;wBAO1B,YAAY;qBAsBf,WAAW,YAAY,YAAY;sBAQxC,YAAY,SAAS,MAAM;qBAY5B,YAAY;sBA7BL,YAAY;sBAnBZ,WAAW,YAAY,MAAM;EA2DtD"}
|
|
@@ -1,17 +1,15 @@
|
|
|
1
|
+
import { isRendered, roleElement, targetName } from "#internal/screenAccessibility.ts";
|
|
1
2
|
export function screenElements(root) {
|
|
2
3
|
function rendered(found, selector) {
|
|
3
|
-
|
|
4
|
-
let hidden = visibility === "hidden" || visibility === "collapse";
|
|
5
|
-
for (let ancestor = found; ancestor; ancestor = ancestor.parentElement) {
|
|
6
|
-
const style = getComputedStyle(ancestor);
|
|
7
|
-
hidden ||= Boolean(ancestor.hidden) || style.display === "none" || style.contentVisibility === "hidden";
|
|
8
|
-
}
|
|
9
|
-
if (hidden) {
|
|
4
|
+
if (!isRendered(found)) {
|
|
10
5
|
throw new Error(`The screen's ${selector} is not rendered.\nhelp: Reveal the target and its ancestors before acting on or reading it.`);
|
|
11
6
|
}
|
|
12
7
|
return found;
|
|
13
8
|
}
|
|
14
9
|
function element(selector) {
|
|
10
|
+
if (typeof selector !== "string") {
|
|
11
|
+
return roleElement(root(), selector);
|
|
12
|
+
}
|
|
15
13
|
const matches = root().querySelectorAll(selector);
|
|
16
14
|
const found = matches[0];
|
|
17
15
|
if (matches.length !== 1 || found === undefined) {
|
|
@@ -22,13 +20,13 @@ export function screenElements(root) {
|
|
|
22
20
|
function input(selector) {
|
|
23
21
|
const found = element(selector);
|
|
24
22
|
if (!(found instanceof HTMLInputElement || found instanceof HTMLTextAreaElement)) {
|
|
25
|
-
throw new Error(`The screen's ${selector} is not a text field.\nhelp: Select a rendered input or textarea to enter or read a value.`);
|
|
23
|
+
throw new Error(`The screen's ${targetName(selector)} is not a text field.\nhelp: Select a rendered input or textarea to enter or read a value.`);
|
|
26
24
|
}
|
|
27
25
|
return found;
|
|
28
26
|
}
|
|
29
27
|
function enabled(found, selector) {
|
|
30
28
|
if (found.matches(":disabled") || found.closest("[inert]")) {
|
|
31
|
-
throw new Error(`The screen's ${selector} is disabled.\nhelp: Wait for the app to enable the control before acting on it.`);
|
|
29
|
+
throw new Error(`The screen's ${targetName(selector)} is disabled.\nhelp: Wait for the app to enable the control before acting on it.`);
|
|
32
30
|
}
|
|
33
31
|
}
|
|
34
32
|
return {
|
|
@@ -38,7 +36,7 @@ export function screenElements(root) {
|
|
|
38
36
|
const field = input(selector);
|
|
39
37
|
enabled(field, selector);
|
|
40
38
|
if (field.readOnly) {
|
|
41
|
-
throw new Error(`The screen's ${selector} is read-only.\nhelp: Select an editable input or textarea.`);
|
|
39
|
+
throw new Error(`The screen's ${targetName(selector)} is read-only.\nhelp: Select an editable input or textarea.`);
|
|
42
40
|
}
|
|
43
41
|
field.focus();
|
|
44
42
|
const prototype = field instanceof HTMLInputElement ? HTMLInputElement.prototype : HTMLTextAreaElement.prototype;
|
|
@@ -49,7 +47,7 @@ export function screenElements(root) {
|
|
|
49
47
|
form: (selector) => {
|
|
50
48
|
const found = element(selector);
|
|
51
49
|
if (!(found instanceof HTMLFormElement)) {
|
|
52
|
-
throw new Error(`The screen's ${selector} is not a form.\nhelp: Select a rendered form to submit.`);
|
|
50
|
+
throw new Error(`The screen's ${targetName(selector)} is not a form.\nhelp: Select a rendered form to submit.`);
|
|
53
51
|
}
|
|
54
52
|
enabled(found, selector);
|
|
55
53
|
return found;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"screenElements.js","sourceRoot":"","sources":["../../../src/server/internal/screenElements.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,cAAc,CAAC,IAAuB;IACrD,SAAS,QAAQ,CAAC,KAAkB,EAAE,QAAgB;QACrD,
|
|
1
|
+
{"version":3,"file":"screenElements.js","sourceRoot":"","sources":["../../../src/server/internal/screenElements.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { Effect } from "effect";
|
|
2
|
+
export type ScreenTarget = string | {
|
|
3
|
+
readonly role: string;
|
|
4
|
+
readonly name: string | RegExp;
|
|
5
|
+
};
|
|
2
6
|
export interface ScreenDriverOptions {
|
|
3
7
|
readonly act?: (action: () => Promise<void>) => PromiseLike<void>;
|
|
4
8
|
readonly pending: () => boolean;
|
|
@@ -8,17 +12,17 @@ export interface ScreenDriverOptions {
|
|
|
8
12
|
}
|
|
9
13
|
export type ScreenDriver = ReturnType<typeof screenDriver>;
|
|
10
14
|
export declare function screenDriver({ root, pending, tell, act, timeoutMs }: ScreenDriverOptions): {
|
|
11
|
-
accessible: (selector: string) => {
|
|
15
|
+
accessible: (selector: string | Element) => {
|
|
12
16
|
name: string;
|
|
13
17
|
role: string | null;
|
|
14
18
|
};
|
|
15
|
-
click: (selector:
|
|
16
|
-
enter: (selector:
|
|
19
|
+
click: (selector: ScreenTarget) => Effect.Effect<void, never, never>;
|
|
20
|
+
enter: (selector: ScreenTarget, value: string) => Effect.Effect<void, never, never>;
|
|
17
21
|
settle: (options?: {
|
|
18
22
|
readonly timeoutMs?: number;
|
|
19
23
|
}) => Effect.Effect<void, never, never>;
|
|
20
|
-
submit: (selector?:
|
|
21
|
-
text: (selector?:
|
|
22
|
-
value: (selector:
|
|
24
|
+
submit: (selector?: ScreenTarget) => Effect.Effect<void, never, never>;
|
|
25
|
+
text: (selector?: ScreenTarget) => string;
|
|
26
|
+
value: (selector: ScreenTarget) => string;
|
|
23
27
|
};
|
|
24
28
|
//# sourceMappingURL=screenDriver.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"screenDriver.d.ts","sourceRoot":"","sources":["../../src/server/screenDriver.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"screenDriver.d.ts","sourceRoot":"","sources":["../../src/server/screenDriver.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAKhC,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAA;CAAE,CAAC;AAE9F,MAAM,WAAW,mBAAmB;IACnC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,KAAK,WAAW,CAAC,IAAI,CAAC,CAAC;IAClE,QAAQ,CAAC,OAAO,EAAE,MAAM,OAAO,CAAC;IAChC,QAAQ,CAAC,IAAI,EAAE,WAAW,GAAG,CAAC,MAAM,WAAW,CAAC,CAAC;IACjD,QAAQ,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACtC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,MAAM,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AAE3D,wBAAgB,YAAY,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,GAA0B,EAAE,SAAgB,EAAE,EAAE,mBAAmB;2BAgC9F,MAAM,GAAG,OAAO;;;;sBASrB,YAAY;sBAOZ,YAAY,SAAS,MAAM;uBAvCrB;QAAE,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE;wBAyCpC,YAAY;sBACb,YAAY;sBAKZ,YAAY;EAM/B"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { computeAccessibleName, getRole } from "dom-accessibility-api";
|
|
2
1
|
import { Effect } from "effect";
|
|
3
2
|
import { withLive } from "@shivaedev/effect-test/withLive.ts";
|
|
3
|
+
import { accessibility, targetName } from "#internal/screenAccessibility.ts";
|
|
4
4
|
import { screenElements } from "#internal/screenElements.ts";
|
|
5
5
|
export function screenDriver({ root, pending, tell, act = (action) => action(), timeoutMs = 3000 }) {
|
|
6
6
|
const currentRoot = typeof root === "function" ? root : () => root;
|
|
@@ -31,28 +31,31 @@ export function screenDriver({ root, pending, tell, act = (action) => action(),
|
|
|
31
31
|
}
|
|
32
32
|
return {
|
|
33
33
|
accessible: (selector) => {
|
|
34
|
+
if (typeof selector !== "string") {
|
|
35
|
+
return accessibility(selector);
|
|
36
|
+
}
|
|
34
37
|
const element = elements.element(selector);
|
|
35
|
-
const accessible =
|
|
38
|
+
const accessible = accessibility(element);
|
|
36
39
|
tell(`read the accessibility of ${selector}: ${JSON.stringify(accessible)}`);
|
|
37
40
|
return accessible;
|
|
38
41
|
},
|
|
39
|
-
click: (selector) => perform(`click ${selector}`, () => {
|
|
42
|
+
click: (selector) => perform(`click ${targetName(selector)}`, () => {
|
|
40
43
|
const element = elements.element(selector);
|
|
41
44
|
elements.enabled(element, selector);
|
|
42
45
|
element.focus();
|
|
43
46
|
element.click();
|
|
44
47
|
}),
|
|
45
|
-
enter: (selector, value) => perform(`enter a value in ${selector}`, () => elements.enter(selector, value)),
|
|
48
|
+
enter: (selector, value) => perform(`enter a value in ${targetName(selector)}`, () => elements.enter(selector, value)),
|
|
46
49
|
settle,
|
|
47
|
-
submit: (selector = "form") => perform(`submit ${selector}`, () => elements.form(selector).requestSubmit()),
|
|
50
|
+
submit: (selector = "form") => perform(`submit ${targetName(selector)}`, () => elements.form(selector).requestSubmit()),
|
|
48
51
|
text: (selector) => {
|
|
49
52
|
const text = (selector === undefined ? elements.rendered(currentRoot(), "root") : elements.element(selector)).innerText;
|
|
50
|
-
tell(`read ${selector
|
|
53
|
+
tell(`read ${selector === undefined ? "the screen" : targetName(selector)}: ${JSON.stringify(text)}`);
|
|
51
54
|
return text;
|
|
52
55
|
},
|
|
53
56
|
value: (selector) => {
|
|
54
57
|
const value = elements.input(selector).value;
|
|
55
|
-
tell(`read the value in ${selector}: ${JSON.stringify(value)}`);
|
|
58
|
+
tell(`read the value in ${targetName(selector)}: ${JSON.stringify(value)}`);
|
|
56
59
|
return value;
|
|
57
60
|
},
|
|
58
61
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"screenDriver.js","sourceRoot":"","sources":["../../src/server/screenDriver.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"screenDriver.js","sourceRoot":"","sources":["../../src/server/screenDriver.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shivaedev/test-story",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Tests that read like stories over the real engine: a story kit per engine with its own it, traits per feature, and a failure that prints the story, where it stopped and the engine's state",
|
|
5
5
|
"homepage": "https://github.com/ShivaeDev/platform/tree/main/packages/test-story#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@effect/vitest": "4.0.4",
|
|
49
|
-
"@shivaedev/quality": "0.
|
|
49
|
+
"@shivaedev/quality": "0.29.0",
|
|
50
50
|
"@types/node": "24.19.2",
|
|
51
51
|
"effect": "4.0.4",
|
|
52
52
|
"happy-dom": "20.14.6",
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { computeAccessibleName, getRole, isInaccessible } from "dom-accessibility-api";
|
|
2
|
+
import type { ScreenTarget } from "#screenDriver.ts";
|
|
3
|
+
|
|
4
|
+
export function accessibility(element: Element) {
|
|
5
|
+
return { name: computeAccessibleName(element), role: getRole(element) };
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function targetName(target: ScreenTarget): string {
|
|
9
|
+
return typeof target === "string"
|
|
10
|
+
? target
|
|
11
|
+
: `${target.role} named ${typeof target.name === "string" ? JSON.stringify(target.name) : String(target.name)}`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function isRendered(found: HTMLElement) {
|
|
15
|
+
const visibility = getComputedStyle(found).visibility;
|
|
16
|
+
if (visibility === "hidden" || visibility === "collapse") {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
for (let ancestor: HTMLElement | null = found; ancestor; ancestor = ancestor.parentElement) {
|
|
20
|
+
const style = getComputedStyle(ancestor);
|
|
21
|
+
if (ancestor.hidden || style.display === "none" || style.contentVisibility === "hidden") {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function roleElement(root: HTMLElement, target: Exclude<ScreenTarget, string>) {
|
|
29
|
+
const candidates = Array.from(root.querySelectorAll<HTMLElement>("*"))
|
|
30
|
+
.filter((element) => isRendered(element) && !isInaccessible(element))
|
|
31
|
+
.map((element) => ({ ...accessibility(element), element }))
|
|
32
|
+
.filter(({ role }) => role !== null);
|
|
33
|
+
const matches = candidates.filter(({ role, name }) => {
|
|
34
|
+
if (role !== target.role) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
if (typeof target.name === "string") {
|
|
38
|
+
return name === target.name;
|
|
39
|
+
}
|
|
40
|
+
return new RegExp(target.name.source, target.name.flags).test(name);
|
|
41
|
+
});
|
|
42
|
+
const found = matches[0];
|
|
43
|
+
if (matches.length === 1 && found) {
|
|
44
|
+
return found.element;
|
|
45
|
+
}
|
|
46
|
+
const listing = candidates.map(({ role, name }) => ` - ${role} named ${JSON.stringify(name)}`).join("\n");
|
|
47
|
+
throw new Error(
|
|
48
|
+
`The screen has ${matches.length} matches for ${targetName(target)}.\nhelp: Use a role and name for exactly one visible target.\nCandidates on screen:\n${listing || " (none)"}`,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
@@ -1,17 +1,17 @@
|
|
|
1
|
+
import { isRendered, roleElement, targetName } from "#internal/screenAccessibility.ts";
|
|
2
|
+
import type { ScreenTarget } from "#screenDriver.ts";
|
|
3
|
+
|
|
1
4
|
export function screenElements(root: () => HTMLElement) {
|
|
2
5
|
function rendered(found: HTMLElement, selector: string) {
|
|
3
|
-
|
|
4
|
-
let hidden = visibility === "hidden" || visibility === "collapse";
|
|
5
|
-
for (let ancestor: HTMLElement | null = found; ancestor; ancestor = ancestor.parentElement) {
|
|
6
|
-
const style = getComputedStyle(ancestor);
|
|
7
|
-
hidden ||= Boolean(ancestor.hidden) || style.display === "none" || style.contentVisibility === "hidden";
|
|
8
|
-
}
|
|
9
|
-
if (hidden) {
|
|
6
|
+
if (!isRendered(found)) {
|
|
10
7
|
throw new Error(`The screen's ${selector} is not rendered.\nhelp: Reveal the target and its ancestors before acting on or reading it.`);
|
|
11
8
|
}
|
|
12
9
|
return found;
|
|
13
10
|
}
|
|
14
|
-
function element(selector:
|
|
11
|
+
function element(selector: ScreenTarget) {
|
|
12
|
+
if (typeof selector !== "string") {
|
|
13
|
+
return roleElement(root(), selector);
|
|
14
|
+
}
|
|
15
15
|
const matches = root().querySelectorAll<HTMLElement>(selector);
|
|
16
16
|
const found = matches[0];
|
|
17
17
|
if (matches.length !== 1 || found === undefined) {
|
|
@@ -21,26 +21,28 @@ export function screenElements(root: () => HTMLElement) {
|
|
|
21
21
|
}
|
|
22
22
|
return rendered(found, selector);
|
|
23
23
|
}
|
|
24
|
-
function input(selector:
|
|
24
|
+
function input(selector: ScreenTarget) {
|
|
25
25
|
const found = element(selector);
|
|
26
26
|
if (!(found instanceof HTMLInputElement || found instanceof HTMLTextAreaElement)) {
|
|
27
|
-
throw new Error(
|
|
27
|
+
throw new Error(
|
|
28
|
+
`The screen's ${targetName(selector)} is not a text field.\nhelp: Select a rendered input or textarea to enter or read a value.`,
|
|
29
|
+
);
|
|
28
30
|
}
|
|
29
31
|
return found;
|
|
30
32
|
}
|
|
31
|
-
function enabled(found: HTMLElement, selector:
|
|
33
|
+
function enabled(found: HTMLElement, selector: ScreenTarget) {
|
|
32
34
|
if (found.matches(":disabled") || found.closest("[inert]")) {
|
|
33
|
-
throw new Error(`The screen's ${selector} is disabled.\nhelp: Wait for the app to enable the control before acting on it.`);
|
|
35
|
+
throw new Error(`The screen's ${targetName(selector)} is disabled.\nhelp: Wait for the app to enable the control before acting on it.`);
|
|
34
36
|
}
|
|
35
37
|
}
|
|
36
38
|
return {
|
|
37
39
|
element,
|
|
38
40
|
enabled,
|
|
39
|
-
enter: (selector:
|
|
41
|
+
enter: (selector: ScreenTarget, value: string) => {
|
|
40
42
|
const field = input(selector);
|
|
41
43
|
enabled(field, selector);
|
|
42
44
|
if (field.readOnly) {
|
|
43
|
-
throw new Error(`The screen's ${selector} is read-only.\nhelp: Select an editable input or textarea.`);
|
|
45
|
+
throw new Error(`The screen's ${targetName(selector)} is read-only.\nhelp: Select an editable input or textarea.`);
|
|
44
46
|
}
|
|
45
47
|
field.focus();
|
|
46
48
|
const prototype = field instanceof HTMLInputElement ? HTMLInputElement.prototype : HTMLTextAreaElement.prototype;
|
|
@@ -48,10 +50,10 @@ export function screenElements(root: () => HTMLElement) {
|
|
|
48
50
|
field.dispatchEvent(new Event("input", { bubbles: true }));
|
|
49
51
|
field.dispatchEvent(new Event("change", { bubbles: true }));
|
|
50
52
|
},
|
|
51
|
-
form: (selector:
|
|
53
|
+
form: (selector: ScreenTarget) => {
|
|
52
54
|
const found = element(selector);
|
|
53
55
|
if (!(found instanceof HTMLFormElement)) {
|
|
54
|
-
throw new Error(`The screen's ${selector} is not a form.\nhelp: Select a rendered form to submit.`);
|
|
56
|
+
throw new Error(`The screen's ${targetName(selector)} is not a form.\nhelp: Select a rendered form to submit.`);
|
|
55
57
|
}
|
|
56
58
|
enabled(found, selector);
|
|
57
59
|
return found;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import { computeAccessibleName, getRole } from "dom-accessibility-api";
|
|
2
1
|
import { Effect } from "effect";
|
|
3
2
|
import { withLive } from "@shivaedev/effect-test/withLive.ts";
|
|
3
|
+
import { accessibility, targetName } from "#internal/screenAccessibility.ts";
|
|
4
4
|
import { screenElements } from "#internal/screenElements.ts";
|
|
5
5
|
|
|
6
|
+
export type ScreenTarget = string | { readonly role: string; readonly name: string | RegExp };
|
|
7
|
+
|
|
6
8
|
export interface ScreenDriverOptions {
|
|
7
9
|
readonly act?: (action: () => Promise<void>) => PromiseLike<void>;
|
|
8
10
|
readonly pending: () => boolean;
|
|
@@ -45,30 +47,33 @@ export function screenDriver({ root, pending, tell, act = (action) => action(),
|
|
|
45
47
|
return withLive(wait.pipe(Effect.timeoutOrElse({ duration: limit, orElse: timedOut })));
|
|
46
48
|
}
|
|
47
49
|
return {
|
|
48
|
-
accessible: (selector: string) => {
|
|
50
|
+
accessible: (selector: string | Element) => {
|
|
51
|
+
if (typeof selector !== "string") {
|
|
52
|
+
return accessibility(selector);
|
|
53
|
+
}
|
|
49
54
|
const element = elements.element(selector);
|
|
50
|
-
const accessible =
|
|
55
|
+
const accessible = accessibility(element);
|
|
51
56
|
tell(`read the accessibility of ${selector}: ${JSON.stringify(accessible)}`);
|
|
52
57
|
return accessible;
|
|
53
58
|
},
|
|
54
|
-
click: (selector:
|
|
55
|
-
perform(`click ${selector}`, () => {
|
|
59
|
+
click: (selector: ScreenTarget) =>
|
|
60
|
+
perform(`click ${targetName(selector)}`, () => {
|
|
56
61
|
const element = elements.element(selector);
|
|
57
62
|
elements.enabled(element, selector);
|
|
58
63
|
element.focus();
|
|
59
64
|
element.click();
|
|
60
65
|
}),
|
|
61
|
-
enter: (selector:
|
|
66
|
+
enter: (selector: ScreenTarget, value: string) => perform(`enter a value in ${targetName(selector)}`, () => elements.enter(selector, value)),
|
|
62
67
|
settle,
|
|
63
|
-
submit: (selector = "form") => perform(`submit ${selector}`, () => elements.form(selector).requestSubmit()),
|
|
64
|
-
text: (selector?:
|
|
68
|
+
submit: (selector: ScreenTarget = "form") => perform(`submit ${targetName(selector)}`, () => elements.form(selector).requestSubmit()),
|
|
69
|
+
text: (selector?: ScreenTarget) => {
|
|
65
70
|
const text = (selector === undefined ? elements.rendered(currentRoot(), "root") : elements.element(selector)).innerText;
|
|
66
|
-
tell(`read ${selector
|
|
71
|
+
tell(`read ${selector === undefined ? "the screen" : targetName(selector)}: ${JSON.stringify(text)}`);
|
|
67
72
|
return text;
|
|
68
73
|
},
|
|
69
|
-
value: (selector:
|
|
74
|
+
value: (selector: ScreenTarget) => {
|
|
70
75
|
const value = elements.input(selector).value;
|
|
71
|
-
tell(`read the value in ${selector}: ${JSON.stringify(value)}`);
|
|
76
|
+
tell(`read the value in ${targetName(selector)}: ${JSON.stringify(value)}`);
|
|
72
77
|
return value;
|
|
73
78
|
},
|
|
74
79
|
};
|