@shivaedev/test-story 0.13.0 → 0.15.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 +15 -0
- package/README.md +28 -3
- package/dist/server/fakeHttpService.d.ts +16 -4
- package/dist/server/fakeHttpService.d.ts.map +1 -1
- package/dist/server/fakeHttpService.js +5 -4
- package/dist/server/fakeHttpService.js.map +1 -1
- package/dist/server/internal/fakeHttpAnswer.d.ts +5 -0
- package/dist/server/internal/fakeHttpAnswer.d.ts.map +1 -0
- package/dist/server/internal/fakeHttpAnswer.js +37 -0
- package/dist/server/internal/fakeHttpAnswer.js.map +1 -0
- package/dist/server/internal/fakeHttpControls.d.ts +8 -7
- package/dist/server/internal/fakeHttpControls.d.ts.map +1 -1
- package/dist/server/internal/fakeHttpControls.js +20 -16
- package/dist/server/internal/fakeHttpControls.js.map +1 -1
- package/dist/server/internal/fakeHttpGates.d.ts +5 -3
- package/dist/server/internal/fakeHttpGates.d.ts.map +1 -1
- package/dist/server/internal/fakeHttpGates.js +13 -9
- package/dist/server/internal/fakeHttpGates.js.map +1 -1
- package/dist/server/internal/fakeHttpReceive.d.ts.map +1 -1
- package/dist/server/internal/fakeHttpReceive.js +3 -18
- package/dist/server/internal/fakeHttpReceive.js.map +1 -1
- package/dist/server/internal/fakeHttpScripts.d.ts +7 -0
- package/dist/server/internal/fakeHttpScripts.d.ts.map +1 -0
- package/dist/server/internal/fakeHttpScripts.js +35 -0
- package/dist/server/internal/fakeHttpScripts.js.map +1 -0
- package/dist/server/internal/fakeHttpSelection.d.ts +6 -0
- package/dist/server/internal/fakeHttpSelection.d.ts.map +1 -0
- package/dist/server/internal/fakeHttpSelection.js +9 -0
- package/dist/server/internal/fakeHttpSelection.js.map +1 -0
- package/dist/server/internal/fakeHttpSend.d.ts +4 -1
- package/dist/server/internal/fakeHttpSend.d.ts.map +1 -1
- package/dist/server/internal/fakeHttpSend.js +8 -0
- package/dist/server/internal/fakeHttpSend.js.map +1 -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/fakeHttpService.ts +23 -4
- package/src/server/internal/fakeHttpAnswer.ts +48 -0
- package/src/server/internal/fakeHttpControls.ts +23 -19
- package/src/server/internal/fakeHttpGates.ts +16 -11
- package/src/server/internal/fakeHttpReceive.ts +4 -20
- package/src/server/internal/fakeHttpScripts.ts +40 -0
- package/src/server/internal/fakeHttpSelection.ts +10 -0
- package/src/server/internal/fakeHttpSend.ts +10 -1
- package/src/server/internal/screenAccessibility.ts +50 -0
- package/src/server/internal/screenElements.ts +18 -16
- package/src/server/screenDriver.ts +16 -11
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export function fakeHttpSelection(route, scope = {}) {
|
|
2
|
+
const params = Object.entries(scope.params ?? {}).sort(([left], [right]) => left.localeCompare(right));
|
|
3
|
+
const label = params.length > 0 ? `${route} matching ${JSON.stringify(Object.fromEntries(params))}` : route;
|
|
4
|
+
return {
|
|
5
|
+
label,
|
|
6
|
+
matches: (name, request) => name === route && params.every(([key, value]) => request.params[key] === value),
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=fakeHttpSelection.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fakeHttpSelection.js","sourceRoot":"","sources":["../../../src/server/internal/fakeHttpSelection.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,iBAAiB,CAAC,KAAa,EAAE,KAAK,GAAkB,EAAE;IACzE,MAAM,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,KAAK,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,CAAC;IACvG,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,KAAK,aAAa,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC;IAC5G,OAAO;QACN,KAAK;QACL,OAAO,EAAE,CAAC,IAAY,EAAE,OAAwB,EAAE,EAAE,CAAC,IAAI,KAAK,KAAK,IAAI,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,KAAK,CAAC;KACpI,CAAC;AACH,CAAC"}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import type { ServerResponse } from "node:http";
|
|
2
|
-
import type { FakeHttpReply } from "#fakeHttpService.ts";
|
|
2
|
+
import type { FakeHttpFault, FakeHttpReply } from "#fakeHttpService.ts";
|
|
3
3
|
export declare function fakeHttpConnection(response: ServerResponse): {
|
|
4
4
|
closed: Promise<undefined>;
|
|
5
5
|
pause: (pending: Promise<void> | undefined) => Promise<void>;
|
|
6
6
|
};
|
|
7
7
|
export declare function fakeHttpSend(response: ServerResponse, reply: FakeHttpReply, pause?: () => Promise<void>): Promise<void>;
|
|
8
|
+
export declare function fakeHttpStrike(response: ServerResponse, fault: Exclude<FakeHttpFault, {
|
|
9
|
+
kind: "status";
|
|
10
|
+
}>, reply?: FakeHttpReply): Promise<void>;
|
|
8
11
|
//# sourceMappingURL=fakeHttpSend.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fakeHttpSend.d.ts","sourceRoot":"","sources":["../../../src/server/internal/fakeHttpSend.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAChD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"fakeHttpSend.d.ts","sourceRoot":"","sources":["../../../src/server/internal/fakeHttpSend.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAChD,OAAO,KAAK,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAExE,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,cAAc;;qBAIlC,OAAO,CAAC,IAAI,CAAC,GAAG,SAAS;EAMjD;AAwBD,wBAAsB,YAAY,CAAC,QAAQ,EAAE,cAAc,EAAE,KAAK,EAAE,aAAa,EAAE,KAAK,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,iBAiB7G;AAED,wBAAsB,cAAc,CAAC,QAAQ,EAAE,cAAc,EAAE,KAAK,EAAE,OAAO,CAAC,aAAa,EAAE;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,CAAC,EAAE,KAAK,GAAE,aAAkB,iBAO1I"}
|
|
@@ -48,4 +48,12 @@ export async function fakeHttpSend(response, reply, pause) {
|
|
|
48
48
|
response.end(body);
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
|
+
export async function fakeHttpStrike(response, fault, reply = {}) {
|
|
52
|
+
if (fault.kind === "reset") {
|
|
53
|
+
response.writeHead(reply.status ?? 200, { "content-type": "application/json", ...reply.headers });
|
|
54
|
+
response.flushHeaders();
|
|
55
|
+
await new Promise((resolve) => response.write("", () => resolve()));
|
|
56
|
+
}
|
|
57
|
+
response.destroy();
|
|
58
|
+
}
|
|
51
59
|
//# sourceMappingURL=fakeHttpSend.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fakeHttpSend.js","sourceRoot":"","sources":["../../../src/server/internal/fakeHttpSend.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"fakeHttpSend.js","sourceRoot":"","sources":["../../../src/server/internal/fakeHttpSend.ts"],"names":[],"mappings":"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"}
|
|
@@ -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.15.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",
|
|
@@ -22,7 +22,21 @@ export interface FakeHttpReply {
|
|
|
22
22
|
|
|
23
23
|
export type FakeHttpFault = { readonly kind: "status"; readonly reply: FakeHttpReply } | { readonly kind: "drop" | "reset" };
|
|
24
24
|
|
|
25
|
+
export interface FakeHttpScope {
|
|
26
|
+
readonly params?: Readonly<Record<string, string>>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface FakeHttpScriptOptions extends FakeHttpScope {
|
|
30
|
+
readonly phase?: "before" | "after";
|
|
31
|
+
readonly repeat?: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface FakeHttpScript {
|
|
35
|
+
readonly lift: Effect.Effect<void>;
|
|
36
|
+
}
|
|
37
|
+
|
|
25
38
|
export type FakeHttpRoute = {
|
|
39
|
+
readonly guard?: (request: FakeHttpRequest) => boolean;
|
|
26
40
|
readonly method?: string;
|
|
27
41
|
readonly path: string;
|
|
28
42
|
} & (
|
|
@@ -57,8 +71,10 @@ export const fakeHttpService = Effect.fnUntraced(function* (options: FakeHttpSer
|
|
|
57
71
|
}
|
|
58
72
|
}
|
|
59
73
|
return {
|
|
60
|
-
failOnce: (route: string, reply: FakeHttpReply
|
|
61
|
-
|
|
74
|
+
failOnce: (route: string, reply: FakeHttpReply, scope?: FakeHttpScope) =>
|
|
75
|
+
Effect.sync(() => checkRoute(route)).pipe(Effect.andThen(controls.queue(route, reply, scope))),
|
|
76
|
+
fault: (route: string, fault: FakeHttpFault, scope?: FakeHttpScope) =>
|
|
77
|
+
Effect.sync(() => checkRoute(route)).pipe(Effect.andThen(controls.fault(route, fault, scope))),
|
|
62
78
|
hold: (route: string) => Effect.sync(() => checkRoute(route)).pipe(Effect.andThen(controls.hold(route))),
|
|
63
79
|
narrate: (sink: (line: string) => void) => {
|
|
64
80
|
narrate = sink;
|
|
@@ -68,11 +84,14 @@ export const fakeHttpService = Effect.fnUntraced(function* (options: FakeHttpSer
|
|
|
68
84
|
checkRoute(route);
|
|
69
85
|
checkRoute(arrival);
|
|
70
86
|
}).pipe(Effect.andThen(controls.releaseOnArrival(route, arrival))),
|
|
71
|
-
replyOnce: (route: string, reply: FakeHttpReply
|
|
87
|
+
replyOnce: (route: string, reply: FakeHttpReply, scope?: FakeHttpScope) =>
|
|
88
|
+
Effect.sync(() => checkRoute(route)).pipe(Effect.andThen(controls.queue(route, reply, scope))),
|
|
72
89
|
get requests(): readonly FakeHttpRequest[] {
|
|
73
90
|
return requests.slice();
|
|
74
91
|
},
|
|
75
|
-
|
|
92
|
+
scriptFault: (route: string, fault: FakeHttpFault, scriptOptions?: FakeHttpScriptOptions) =>
|
|
93
|
+
Effect.sync(() => checkRoute(route)).pipe(Effect.andThen(controls.scriptFault(route, fault, scriptOptions))),
|
|
94
|
+
stall: (route: string, scope?: FakeHttpScope) => Effect.sync(() => checkRoute(route)).pipe(Effect.andThen(controls.stall(route, scope))),
|
|
76
95
|
url,
|
|
77
96
|
};
|
|
78
97
|
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { ServerResponse } from "node:http";
|
|
2
|
+
import type { FakeHttpReply, FakeHttpRequest, FakeHttpRoute } from "#fakeHttpService.ts";
|
|
3
|
+
import type { fakeHttpControls } from "#internal/fakeHttpControls.ts";
|
|
4
|
+
import { fakeHttpSend, fakeHttpStrike } from "#internal/fakeHttpSend.ts";
|
|
5
|
+
|
|
6
|
+
function pageReply(route: FakeHttpRoute, request: FakeHttpRequest): FakeHttpReply {
|
|
7
|
+
if (route.reply) {
|
|
8
|
+
return route.reply(request);
|
|
9
|
+
}
|
|
10
|
+
const page = Number(request.query.get("page") ?? "1");
|
|
11
|
+
if (!Number.isInteger(page) || page < 1) {
|
|
12
|
+
return { body: { error: "page must be a positive integer" }, status: 400 };
|
|
13
|
+
}
|
|
14
|
+
return { body: { items: route.pages[page - 1] ?? [], next: page < route.pages.length ? page + 1 : null } };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function fakeHttpAnswer(
|
|
18
|
+
name: string,
|
|
19
|
+
route: FakeHttpRoute,
|
|
20
|
+
request: FakeHttpRequest,
|
|
21
|
+
response: ServerResponse,
|
|
22
|
+
controls: ReturnType<typeof fakeHttpControls>,
|
|
23
|
+
tell: (line: string) => void,
|
|
24
|
+
) {
|
|
25
|
+
const { method, path } = request;
|
|
26
|
+
controls.arrive(name, request, response);
|
|
27
|
+
if (!(await controls.pause(name, response))) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const before = controls.takeFault(name, request, "before");
|
|
31
|
+
if (before && before.kind !== "status") {
|
|
32
|
+
tell(`fake HTTP ${name} fault ${before.kind} before reply`);
|
|
33
|
+
await fakeHttpStrike(response, before);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
let reply = before?.reply ?? pageReply(route, request);
|
|
37
|
+
const after = before ? undefined : controls.takeFault(name, request, "after");
|
|
38
|
+
if (after) {
|
|
39
|
+
tell(`fake HTTP ${name} fault ${after.kind} after reply`);
|
|
40
|
+
if (after.kind !== "status") {
|
|
41
|
+
await fakeHttpStrike(response, after, reply);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
reply = after.reply;
|
|
45
|
+
}
|
|
46
|
+
tell(`fake HTTP answered ${method} ${path} with ${reply.status ?? 200}`);
|
|
47
|
+
await fakeHttpSend(response, reply, controls.stallBody(name, request));
|
|
48
|
+
}
|
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
import type { ServerResponse } from "node:http";
|
|
2
2
|
import { Effect } from "effect";
|
|
3
|
-
import type { FakeHttpFault, FakeHttpReply } from "#fakeHttpService.ts";
|
|
3
|
+
import type { FakeHttpFault, FakeHttpReply, FakeHttpRequest, FakeHttpScope, FakeHttpScriptOptions } from "#fakeHttpService.ts";
|
|
4
4
|
import { fakeHttpGates } from "#internal/fakeHttpGates.ts";
|
|
5
|
+
import { fakeHttpScripts } from "#internal/fakeHttpScripts.ts";
|
|
6
|
+
import { fakeHttpSelection } from "#internal/fakeHttpSelection.ts";
|
|
5
7
|
import { fakeHttpConnection, fakeHttpSend } from "#internal/fakeHttpSend.ts";
|
|
6
8
|
|
|
7
9
|
export function fakeHttpControls(tell: (line: string) => void) {
|
|
8
|
-
const
|
|
10
|
+
const scripts = fakeHttpScripts(tell);
|
|
9
11
|
const holds = fakeHttpGates(tell, "hold");
|
|
10
12
|
const stalls = fakeHttpGates(tell, "stall");
|
|
11
13
|
const arrivals = new Map<string, Set<() => void>>();
|
|
12
|
-
const active = new Map<ServerResponse, string>();
|
|
14
|
+
const active = new Map<ServerResponse, { route: string; request: FakeHttpRequest }>();
|
|
13
15
|
return {
|
|
14
|
-
arrive: (route: string, response: ServerResponse) => {
|
|
15
|
-
active.set(response, route);
|
|
16
|
+
arrive: (route: string, request: FakeHttpRequest, response: ServerResponse) => {
|
|
17
|
+
active.set(response, { request, route });
|
|
16
18
|
response.once("close", () => active.delete(response));
|
|
17
19
|
for (const release of arrivals.get(route) ?? []) {
|
|
18
20
|
release();
|
|
@@ -26,20 +28,23 @@ export function fakeHttpControls(tell: (line: string) => void) {
|
|
|
26
28
|
holds.close();
|
|
27
29
|
stalls.close();
|
|
28
30
|
arrivals.clear();
|
|
29
|
-
|
|
31
|
+
scripts.clear();
|
|
30
32
|
},
|
|
31
|
-
fault: (route: string, fault: FakeHttpFault) =>
|
|
33
|
+
fault: (route: string, fault: FakeHttpFault, scope?: FakeHttpScope) =>
|
|
32
34
|
Effect.promise(async () => {
|
|
35
|
+
const selection = fakeHttpSelection(route, scope);
|
|
33
36
|
const response = [...active].find(
|
|
34
|
-
([candidate,
|
|
35
|
-
|
|
37
|
+
([candidate, entry]) =>
|
|
38
|
+
selection.matches(entry.route, entry.request)
|
|
39
|
+
&& !(candidate.destroyed || candidate.writableEnded)
|
|
40
|
+
&& candidate.headersSent === (fault.kind === "reset"),
|
|
36
41
|
)?.[0];
|
|
37
42
|
if (!response) {
|
|
38
43
|
throw new Error(
|
|
39
44
|
`Fake HTTP route "${route}" has no request ready for a ${fault.kind} fault.\nhelp: Wait for a hold before a status or drop fault, or for response headers before a reset fault.`,
|
|
40
45
|
);
|
|
41
46
|
}
|
|
42
|
-
tell(`fake HTTP ${
|
|
47
|
+
tell(`fake HTTP ${selection.label} fault ${fault.kind}`);
|
|
43
48
|
if (fault.kind === "status") {
|
|
44
49
|
await fakeHttpSend(response, fault.reply);
|
|
45
50
|
} else {
|
|
@@ -51,12 +56,10 @@ export function fakeHttpControls(tell: (line: string) => void) {
|
|
|
51
56
|
await fakeHttpConnection(response).pause(holds.get(route)?.enter());
|
|
52
57
|
return !(response.destroyed || response.writableEnded || response.headersSent);
|
|
53
58
|
},
|
|
54
|
-
queue: (route: string, reply: FakeHttpReply) =>
|
|
59
|
+
queue: (route: string, reply: FakeHttpReply, scope?: FakeHttpScope) =>
|
|
55
60
|
Effect.sync(() => {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
replies.set(route, queue);
|
|
59
|
-
tell(`fake HTTP ${route} will answer once with ${reply.status ?? 200}`);
|
|
61
|
+
scripts.make(route, { kind: "status", reply }, scope, false);
|
|
62
|
+
tell(`fake HTTP ${fakeHttpSelection(route, scope).label} will answer once with ${reply.status ?? 200}`);
|
|
60
63
|
}),
|
|
61
64
|
releaseOnArrival: (route: string, arrival: string) =>
|
|
62
65
|
Effect.sync(() => {
|
|
@@ -69,11 +72,12 @@ export function fakeHttpControls(tell: (line: string) => void) {
|
|
|
69
72
|
arrivals.set(arrival, releases);
|
|
70
73
|
tell(`fake HTTP ${route} will release when ${arrival} arrives`);
|
|
71
74
|
}),
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
+
scriptFault: (route: string, fault: FakeHttpFault, options?: FakeHttpScriptOptions) => Effect.sync(() => scripts.make(route, fault, options)),
|
|
76
|
+
stall: (route: string, scope?: FakeHttpScope) => Effect.sync(() => stalls.make(route, scope)),
|
|
77
|
+
stallBody: (route: string, request: FakeHttpRequest) => {
|
|
78
|
+
const stall = stalls.get(route, request);
|
|
75
79
|
return stall ? () => stall.enter() : undefined;
|
|
76
80
|
},
|
|
77
|
-
|
|
81
|
+
takeFault: scripts.take,
|
|
78
82
|
};
|
|
79
83
|
}
|
|
@@ -1,38 +1,43 @@
|
|
|
1
1
|
import { Deferred, Effect } from "effect";
|
|
2
|
-
import type { FakeHttpHold } from "#fakeHttpService.ts";
|
|
2
|
+
import type { FakeHttpHold, FakeHttpRequest, FakeHttpScope } from "#fakeHttpService.ts";
|
|
3
|
+
import { fakeHttpSelection } from "#internal/fakeHttpSelection.ts";
|
|
3
4
|
|
|
4
5
|
export function fakeHttpGates(tell: (line: string) => void, kind: "hold" | "stall") {
|
|
5
|
-
const gates = new Map<string, { enter: () => Promise<void>; release: () => void }>();
|
|
6
|
+
const gates = new Map<string, { enter: () => Promise<void>; release: () => void; matches: ReturnType<typeof fakeHttpSelection>["matches"] }>();
|
|
6
7
|
return {
|
|
7
8
|
close: () => {
|
|
8
9
|
for (const gate of gates.values()) {
|
|
9
10
|
gate.release();
|
|
10
11
|
}
|
|
11
12
|
},
|
|
12
|
-
get: (route: string) =>
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
get: (route: string, request?: FakeHttpRequest) =>
|
|
14
|
+
request ? [...gates.values()].find((gate) => gate.matches(route, request)) : gates.get(route),
|
|
15
|
+
make: (route: string, scope?: FakeHttpScope): FakeHttpHold => {
|
|
16
|
+
const selection = fakeHttpSelection(route, scope);
|
|
17
|
+
const label = selection.label;
|
|
18
|
+
if (gates.has(label)) {
|
|
15
19
|
throw new Error(`Fake HTTP route "${route}" already has a ${kind}.\nhelp: Release its current ${kind} before scripting another.`);
|
|
16
20
|
}
|
|
17
21
|
const waiting = Deferred.makeUnsafe<void>();
|
|
18
22
|
const released = Promise.withResolvers<void>();
|
|
19
23
|
const gate = {
|
|
20
24
|
enter: () => {
|
|
21
|
-
tell(kind === "hold" ? `fake HTTP ${
|
|
25
|
+
tell(kind === "hold" ? `fake HTTP ${label} has a waiting request` : `fake HTTP ${label} body stalled`);
|
|
22
26
|
Deferred.doneUnsafe(waiting, Effect.void);
|
|
23
27
|
return released.promise;
|
|
24
28
|
},
|
|
29
|
+
matches: selection.matches,
|
|
25
30
|
release: () => {
|
|
26
|
-
if (gates.get(
|
|
31
|
+
if (gates.get(label) !== gate) {
|
|
27
32
|
return;
|
|
28
33
|
}
|
|
29
|
-
gates.delete(
|
|
34
|
+
gates.delete(label);
|
|
30
35
|
released.resolve();
|
|
31
|
-
tell(kind === "hold" ? `fake HTTP ${
|
|
36
|
+
tell(kind === "hold" ? `fake HTTP ${label} released` : `fake HTTP ${label} body released`);
|
|
32
37
|
},
|
|
33
38
|
};
|
|
34
|
-
gates.set(
|
|
35
|
-
tell(kind === "hold" ? `fake HTTP ${
|
|
39
|
+
gates.set(label, gate);
|
|
40
|
+
tell(kind === "hold" ? `fake HTTP ${label} held` : `fake HTTP ${label} body will stall`);
|
|
36
41
|
return { release: Effect.sync(gate.release), waiting: Deferred.await(waiting) };
|
|
37
42
|
},
|
|
38
43
|
};
|