@forgebook/testing 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +29 -2
- package/dist/bin/forgebook-heights/forgebook-heights.d.mts +3 -0
- package/dist/bin/forgebook-heights/forgebook-heights.d.mts.map +1 -0
- package/dist/bin/forgebook-heights/forgebook-heights.mjs +124 -0
- package/dist/bin/forgebook-heights/forgebook-heights.mjs.map +1 -0
- package/dist/bin/forgebook-heights/measureHeights.d.ts +84 -0
- package/dist/bin/forgebook-heights/measureHeights.d.ts.map +1 -0
- package/dist/bin/forgebook-heights/measureHeights.js +180 -0
- package/dist/bin/forgebook-heights/measureHeights.js.map +1 -0
- package/dist/bin/forgebook-heights/measureHeights.types.d.ts +27 -0
- package/dist/bin/forgebook-heights/measureHeights.types.d.ts.map +1 -0
- package/dist/bin/forgebook-heights/measureHeights.types.js +7 -0
- package/dist/bin/forgebook-heights/measureHeights.types.js.map +1 -0
- package/dist/bin/forgebook-shot/forgebook-shot.d.mts +3 -0
- package/dist/bin/forgebook-shot/forgebook-shot.d.mts.map +1 -0
- package/dist/bin/forgebook-shot/forgebook-shot.mjs +110 -0
- package/dist/bin/forgebook-shot/forgebook-shot.mjs.map +1 -0
- package/dist/bin/forgebook-shot/storyShot.d.ts +34 -0
- package/dist/bin/forgebook-shot/storyShot.d.ts.map +1 -0
- package/dist/bin/forgebook-shot/storyShot.js +83 -0
- package/dist/bin/forgebook-shot/storyShot.js.map +1 -0
- package/dist/bin/forgebook-shot/storyShot.types.d.ts +32 -0
- package/dist/bin/forgebook-shot/storyShot.types.d.ts.map +1 -0
- package/dist/bin/forgebook-shot/storyShot.types.js +1 -0
- package/dist/bin/forgebook-shot/storyShot.types.js.map +1 -0
- package/dist/src/browserTests.d.ts +55 -0
- package/dist/src/browserTests.d.ts.map +1 -0
- package/dist/src/browserTests.js +137 -0
- package/dist/src/browserTests.js.map +1 -0
- package/dist/src/fonts/Inter-OFL.txt +92 -0
- package/dist/src/fonts/Inter-latin-ext.woff2 +0 -0
- package/dist/src/fonts/Inter-latin.woff2 +0 -0
- package/dist/src/fonts/JetBrainsMono-OFL.txt +93 -0
- package/dist/src/fonts/JetBrainsMono-latin-ext.woff2 +0 -0
- package/dist/src/fonts/JetBrainsMono-latin.woff2 +0 -0
- package/dist/src/fonts/fonts.css +34 -0
- package/dist/src/overlays.d.ts +87 -0
- package/dist/src/overlays.d.ts.map +1 -0
- package/dist/src/overlays.js +103 -0
- package/dist/src/overlays.js.map +1 -0
- package/dist/src/playwrightConfig.d.ts +26 -0
- package/dist/src/playwrightConfig.d.ts.map +1 -0
- package/dist/src/playwrightConfig.js +62 -0
- package/dist/src/playwrightConfig.js.map +1 -0
- package/dist/src/serverTests.d.ts +44 -0
- package/dist/src/serverTests.d.ts.map +1 -0
- package/dist/src/serverTests.js +101 -0
- package/dist/src/serverTests.js.map +1 -0
- package/dist/src/spills.d.ts +28 -0
- package/dist/src/spills.d.ts.map +1 -0
- package/dist/src/spills.js +53 -0
- package/dist/src/spills.js.map +1 -0
- package/dist/src/storyControls.d.ts +24 -0
- package/dist/src/storyControls.d.ts.map +1 -0
- package/dist/src/storyControls.js +29 -0
- package/dist/src/storyControls.js.map +1 -0
- package/dist/src/storyFiles.d.ts +34 -0
- package/dist/src/storyFiles.d.ts.map +1 -0
- package/dist/src/storyFiles.js +66 -0
- package/dist/src/storyFiles.js.map +1 -0
- package/dist/src/testViteConfig.d.ts +21 -0
- package/dist/src/testViteConfig.d.ts.map +1 -0
- package/dist/src/testViteConfig.js +26 -0
- package/dist/src/testViteConfig.js.map +1 -0
- package/dist/src/visualBrowser.d.ts +18 -0
- package/dist/src/visualBrowser.d.ts.map +1 -0
- package/dist/src/visualBrowser.js +78 -0
- package/dist/src/visualBrowser.js.map +1 -0
- package/package.json +84 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ensiria
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,30 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @forgebook/testing
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Tests from a package's [Forgebook](https://www.npmjs.com/package/forgebook) stories, so writing a story is writing its
|
|
4
|
+
tests:
|
|
5
|
+
|
|
6
|
+
- **in the browser** (Playwright), one test per story and variant (theme, contrast, style sheets, a narrow width): it
|
|
7
|
+
fails on an error in the page, an [axe](https://github.com/dequelabs/axe-core) violation of WCAG 2.2 A and AA, an
|
|
8
|
+
element that spills out of its parent, or an overlay left open past the story's frame;
|
|
9
|
+
- **visual**, the same frames compared with reference screenshots, in the Playwright container so fonts and rendering
|
|
10
|
+
match everywhere;
|
|
11
|
+
- **on the server** (Vitest), each story rendered to a string, so no component reads `window` or `document` while it
|
|
12
|
+
renders, and its controls checked against its args.
|
|
13
|
+
|
|
14
|
+
**Requirements:** `forgebook` · Vite 8 · `@playwright/test` 1.64+ · Vitest 5 ·
|
|
15
|
+
[MIT licence](https://github.com/Ensiria/forgebook/blob/develop/packages/testing/LICENSE)
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
npm install --save-dev @forgebook/testing @playwright/test vitest
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
| Import or command | What it is |
|
|
22
|
+
|---|---|
|
|
23
|
+
| `@forgebook/testing/browser` | The browser and visual tests of every story |
|
|
24
|
+
| `@forgebook/testing/server` | The server-render tests of every story |
|
|
25
|
+
| `@forgebook/testing/playwright`, `@forgebook/testing/vite` | The Playwright configurations and the Vite config of the test frame page |
|
|
26
|
+
| `forgebook-heights` | Measures every story's height at a few widths, for the catalogue's loading boxes |
|
|
27
|
+
| `forgebook-shot` | Opens one story of a running catalogue and reports it with a picture |
|
|
28
|
+
|
|
29
|
+
Each module's TSDoc describes its setup; a step-by-step guide is still to be written. More:
|
|
30
|
+
[Forgebook's README](https://github.com/Ensiria/forgebook/blob/develop/packages/forgebook/README.md).
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"forgebook-heights.d.mts","sourceRoot":"","sources":["../../../bin/forgebook-heights/forgebook-heights.mts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* The `forgebook-heights` command: opens every story of a catalogue in a browser and measures how tall its frame is
|
|
4
|
+
* at a few widths, for the shell to give a story's box that height while it loads.
|
|
5
|
+
*
|
|
6
|
+
* @remarks
|
|
7
|
+
* Usage: `forgebook-heights --help` (generated by `@ensiria/cli-kit`). It measures a running catalogue, or serves a
|
|
8
|
+
* built one with `--serve`, which loads far faster; the file it writes is generated, never committed.
|
|
9
|
+
*
|
|
10
|
+
* @module
|
|
11
|
+
*/
|
|
12
|
+
import { resolve } from "node:path";
|
|
13
|
+
import { writeJson } from "@ensiria/cli-kit/files";
|
|
14
|
+
import { plural } from "@ensiria/cli-kit/format";
|
|
15
|
+
import { progress } from "@ensiria/cli-kit/progress";
|
|
16
|
+
import { defineScript, ScriptError, usageError } from "@ensiria/cli-kit/script";
|
|
17
|
+
import { HEIGHT_WIDTHS } from "forgebook/story-heights";
|
|
18
|
+
import { heightsDocument, measureStories, openCatalogue, parseWidths } from "./measureHeights.js";
|
|
19
|
+
/** The command, as the hints print it. */
|
|
20
|
+
const COMMAND = "forgebook-heights";
|
|
21
|
+
await defineScript({
|
|
22
|
+
name: "forgebook-heights",
|
|
23
|
+
command: COMMAND,
|
|
24
|
+
summary: "Measure every story's height at a few widths, for the catalogue's loading boxes.",
|
|
25
|
+
icon: "check",
|
|
26
|
+
json: true,
|
|
27
|
+
description: "Opens every story's frame (the address the catalogue's stage opens) in Chromium, waits until it is ready, and reads its height at each width, the way the frame reports it to the shell. Writes the heights as JSON for the catalogue's `heights` setting, which gives a story's box that height while it loads. The stories are measured in the default view; a story that fails or never answers is left out and reported.",
|
|
28
|
+
options: {
|
|
29
|
+
catalogue: {
|
|
30
|
+
type: "string",
|
|
31
|
+
default: "http://localhost:5310/",
|
|
32
|
+
valueName: "url",
|
|
33
|
+
env: "CATALOGUE_URL",
|
|
34
|
+
description: "The running catalogue's address, when --serve is not given."
|
|
35
|
+
},
|
|
36
|
+
serve: {
|
|
37
|
+
type: "string",
|
|
38
|
+
valueName: "dir",
|
|
39
|
+
description: "A built catalogue to serve for the run (its build folder): much faster than a dev server."
|
|
40
|
+
},
|
|
41
|
+
out: {
|
|
42
|
+
type: "string",
|
|
43
|
+
required: true,
|
|
44
|
+
valueName: "file",
|
|
45
|
+
description: "Where to write the heights (a .json)."
|
|
46
|
+
},
|
|
47
|
+
widths: {
|
|
48
|
+
type: "string",
|
|
49
|
+
default: HEIGHT_WIDTHS.join(","),
|
|
50
|
+
valueName: "px,px",
|
|
51
|
+
description: "The widths to measure at, separated by commas."
|
|
52
|
+
},
|
|
53
|
+
concurrency: {
|
|
54
|
+
type: "string",
|
|
55
|
+
default: "6",
|
|
56
|
+
valueName: "count",
|
|
57
|
+
description: "How many stories load at once."
|
|
58
|
+
},
|
|
59
|
+
timeout: {
|
|
60
|
+
type: "string",
|
|
61
|
+
default: "30000",
|
|
62
|
+
valueName: "ms",
|
|
63
|
+
description: "How long to wait for one story."
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
examples: [
|
|
67
|
+
{
|
|
68
|
+
command: `${COMMAND} --serve dist --out dist/story-heights.json`,
|
|
69
|
+
description: "Measure a built catalogue and write the heights beside it."
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
command: `${COMMAND} --out public/story-heights.json`,
|
|
73
|
+
description: "Measure the running development catalogue."
|
|
74
|
+
}
|
|
75
|
+
],
|
|
76
|
+
run: async (context) => {
|
|
77
|
+
const { options, log } = context;
|
|
78
|
+
const widths = parseWidths(options.widths);
|
|
79
|
+
if (typeof widths === "string")
|
|
80
|
+
throw usageError(widths);
|
|
81
|
+
const concurrency = Number(options.concurrency);
|
|
82
|
+
const timeout = Number(options.timeout);
|
|
83
|
+
if (!Number.isInteger(concurrency) || concurrency < 1)
|
|
84
|
+
throw usageError("--concurrency needs a whole number");
|
|
85
|
+
if (!Number.isInteger(timeout) || timeout < 1)
|
|
86
|
+
throw usageError("--timeout needs a whole number of ms");
|
|
87
|
+
const out = resolve(options.out);
|
|
88
|
+
const catalogue = await openCatalogue(options.catalogue, options.serve === undefined ? undefined : resolve(options.serve));
|
|
89
|
+
try {
|
|
90
|
+
let bar;
|
|
91
|
+
const result = await measureStories({
|
|
92
|
+
address: catalogue.address,
|
|
93
|
+
widths,
|
|
94
|
+
concurrency,
|
|
95
|
+
timeout,
|
|
96
|
+
onStart: (stories) => {
|
|
97
|
+
bar = progress({ label: "Measuring the stories", total: stories, unit: "stories" }, context);
|
|
98
|
+
},
|
|
99
|
+
onStory: () => bar?.tick()
|
|
100
|
+
}).catch((error) => {
|
|
101
|
+
bar?.fail();
|
|
102
|
+
throw new ScriptError(error instanceof Error ? error.message : String(error), {
|
|
103
|
+
hint: `Is the catalogue running at ${catalogue.address}? Or build it and pass --serve`
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
bar?.done(`${plural(result.measured.size, "story", "stories")} measured`);
|
|
107
|
+
writeJson(out, heightsDocument(widths, result.measured));
|
|
108
|
+
for (const failure of result.failures)
|
|
109
|
+
log.warn(`${failure.story}: ${failure.reason}`);
|
|
110
|
+
log.success(`heights written to ${out}`);
|
|
111
|
+
return {
|
|
112
|
+
out,
|
|
113
|
+
widths,
|
|
114
|
+
stories: result.storyIds.length,
|
|
115
|
+
measured: result.measured.size,
|
|
116
|
+
failures: result.failures
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
finally {
|
|
120
|
+
await catalogue.close();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}).run();
|
|
124
|
+
//# sourceMappingURL=forgebook-heights.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"forgebook-heights.mjs","sourceRoot":"","sources":["../../../bin/forgebook-heights/forgebook-heights.mts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The logic of `forgebook-heights`: the widths it is asked for, the document it writes, and the run in a browser that
|
|
3
|
+
* opens every story's frame and measures it at each width.
|
|
4
|
+
*
|
|
5
|
+
* @remarks
|
|
6
|
+
* The frame page is the one the catalogue's stage opens (D-56, D-69), and its height is read as the frame reports it to
|
|
7
|
+
* the shell (the body's own height), so a measured height is the one the stage would get. A story loads once at the
|
|
8
|
+
* first width and is then resized through the others: a reload per width would multiply the run by them. `--serve`
|
|
9
|
+
* serves a built catalogue with Vite's preview server, where a frame loads in one request per chunk instead of the
|
|
10
|
+
* development server's hundreds of modules. `parseWidths` and `heightsDocument` are pure.
|
|
11
|
+
*
|
|
12
|
+
* @module
|
|
13
|
+
*/
|
|
14
|
+
import { type Browser } from "@playwright/test";
|
|
15
|
+
import type { StoryHeights } from "forgebook/story-heights";
|
|
16
|
+
import type { HeightFailure } from "./measureHeights.types.ts";
|
|
17
|
+
/**
|
|
18
|
+
* The widths a `--widths` text names: whole numbers of px within the stage's range, ascending, once each.
|
|
19
|
+
*
|
|
20
|
+
* @param text - the widths, separated by commas (`360,768,1440`).
|
|
21
|
+
* @returns the widths, or the message that says what is wrong.
|
|
22
|
+
*/
|
|
23
|
+
export declare const parseWidths: (text: string) => number[] | string;
|
|
24
|
+
/**
|
|
25
|
+
* The document the shell reads: the stories measured, in the catalogue's order.
|
|
26
|
+
*
|
|
27
|
+
* @param widths - the widths measured at.
|
|
28
|
+
* @param measured - each measured story's heights, at `widths`.
|
|
29
|
+
*/
|
|
30
|
+
export declare const heightsDocument: (widths: readonly number[], measured: ReadonlyMap<string, readonly number[]>) => StoryHeights;
|
|
31
|
+
/** A catalogue to measure: its address, and how to let it go when the run is over. */
|
|
32
|
+
type Catalogue = {
|
|
33
|
+
/** The catalogue's address, ending with `/`. */
|
|
34
|
+
readonly address: string;
|
|
35
|
+
/** Stops what was started to serve it; nothing for a catalogue already running. */
|
|
36
|
+
readonly close: () => Promise<void>;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* The catalogue to measure: one already running, or a built one served for the run.
|
|
40
|
+
*
|
|
41
|
+
* @param catalogue - the running catalogue's address, used when there is nothing to serve.
|
|
42
|
+
* @param serve - the folder of a built catalogue to serve; none to use the running one.
|
|
43
|
+
*/
|
|
44
|
+
export declare const openCatalogue: (catalogue: string, serve: string | undefined) => Promise<Catalogue>;
|
|
45
|
+
/**
|
|
46
|
+
* The catalogue's story ids, as its shell writes them into the page.
|
|
47
|
+
*
|
|
48
|
+
* @param browser - the browser.
|
|
49
|
+
* @param address - the catalogue's address.
|
|
50
|
+
* @param timeout - how long to wait for the shell, in ms.
|
|
51
|
+
*/
|
|
52
|
+
export declare const catalogueStoryIds: (browser: Browser, address: string, timeout: number) => Promise<string[]>;
|
|
53
|
+
/** What a measuring run is told. */
|
|
54
|
+
export type MeasureRequest = {
|
|
55
|
+
/** The catalogue's address. */
|
|
56
|
+
readonly address: string;
|
|
57
|
+
/** The widths, ascending. */
|
|
58
|
+
readonly widths: readonly number[];
|
|
59
|
+
/** How many stories load at once. */
|
|
60
|
+
readonly concurrency: number;
|
|
61
|
+
/** How long to wait for a story, in ms. */
|
|
62
|
+
readonly timeout: number;
|
|
63
|
+
/** Told the number of stories before the first is measured. */
|
|
64
|
+
readonly onStart: (stories: number) => void;
|
|
65
|
+
/** Told after each story, measured or not. */
|
|
66
|
+
readonly onStory: (storyId: string) => void;
|
|
67
|
+
};
|
|
68
|
+
/** What a measuring run found. */
|
|
69
|
+
export type MeasureResult = {
|
|
70
|
+
/** Every story's id, in the catalogue's order. */
|
|
71
|
+
readonly storyIds: readonly string[];
|
|
72
|
+
/** The measured stories' heights. */
|
|
73
|
+
readonly measured: ReadonlyMap<string, readonly number[]>;
|
|
74
|
+
/** The stories that could not be measured. */
|
|
75
|
+
readonly failures: readonly HeightFailure[];
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* Measures every story of a catalogue at each width.
|
|
79
|
+
*
|
|
80
|
+
* @param request - the catalogue, the widths, how many at once, and what to tell along the way.
|
|
81
|
+
*/
|
|
82
|
+
export declare const measureStories: (request: MeasureRequest) => Promise<MeasureResult>;
|
|
83
|
+
export {};
|
|
84
|
+
//# sourceMappingURL=measureHeights.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"measureHeights.d.ts","sourceRoot":"","sources":["../../../bin/forgebook-heights/measureHeights.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAAE,KAAK,OAAO,EAAuB,MAAM,kBAAkB,CAAC;AAErE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAK5D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAQ/D;;;;;GAKG;AACH,eAAO,MAAM,WAAW,SAAU,MAAM,KAAG,MAAM,EAAE,GAAG,MAcrD,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,eAAe,WAClB,SAAS,MAAM,EAAE,YACf,WAAW,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,KAC/C,YAAgF,CAAC;AAEpF,sFAAsF;AACtF,KAAK,SAAS,GAAG;IACf,gDAAgD;IAChD,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,mFAAmF;IACnF,QAAQ,CAAC,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CACrC,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,aAAa,cAAqB,MAAM,SAAS,MAAM,GAAG,SAAS,KAAG,OAAO,CAAC,SAAS,CAiBnG,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB,YACnB,OAAO,WACP,MAAM,WACN,MAAM,KACd,OAAO,CAAC,MAAM,EAAE,CAWlB,CAAC;AAyDF,oCAAoC;AACpC,MAAM,MAAM,cAAc,GAAG;IAC3B,+BAA+B;IAC/B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,6BAA6B;IAC7B,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,qCAAqC;IACrC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,2CAA2C;IAC3C,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,+DAA+D;IAC/D,QAAQ,CAAC,OAAO,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5C,8CAA8C;IAC9C,QAAQ,CAAC,OAAO,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CAC7C,CAAC;AAEF,kCAAkC;AAClC,MAAM,MAAM,aAAa,GAAG;IAC1B,kDAAkD;IAClD,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,qCAAqC;IACrC,QAAQ,CAAC,QAAQ,EAAE,WAAW,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,CAAC;IAC1D,8CAA8C;IAC9C,QAAQ,CAAC,QAAQ,EAAE,SAAS,aAAa,EAAE,CAAC;CAC7C,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,cAAc,YAAmB,cAAc,KAAG,OAAO,CAAC,aAAa,CAmCnF,CAAC"}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The logic of `forgebook-heights`: the widths it is asked for, the document it writes, and the run in a browser that
|
|
3
|
+
* opens every story's frame and measures it at each width.
|
|
4
|
+
*
|
|
5
|
+
* @remarks
|
|
6
|
+
* The frame page is the one the catalogue's stage opens (D-56, D-69), and its height is read as the frame reports it to
|
|
7
|
+
* the shell (the body's own height), so a measured height is the one the stage would get. A story loads once at the
|
|
8
|
+
* first width and is then resized through the others: a reload per width would multiply the run by them. `--serve`
|
|
9
|
+
* serves a built catalogue with Vite's preview server, where a frame loads in one request per chunk instead of the
|
|
10
|
+
* development server's hundreds of modules. `parseWidths` and `heightsDocument` are pure.
|
|
11
|
+
*
|
|
12
|
+
* @module
|
|
13
|
+
*/
|
|
14
|
+
import { chromium } from "@playwright/test";
|
|
15
|
+
import { STATUS_ATTRIBUTE } from "forgebook/frame-status";
|
|
16
|
+
import { STORY_IDS_ELEMENT } from "forgebook/story-ids-element";
|
|
17
|
+
import { frameAddressOf } from "forgebook/view-state";
|
|
18
|
+
import { preview } from "vite";
|
|
19
|
+
/** The narrowest and widest widths a story is measured at, in CSS px: the stage's own range. */
|
|
20
|
+
const WIDTH_RANGE = { min: 240, max: 2560 };
|
|
21
|
+
/** The viewport's height while measuring: a story taller than it still reports its whole height. */
|
|
22
|
+
const VIEWPORT_HEIGHT = 900;
|
|
23
|
+
/**
|
|
24
|
+
* The widths a `--widths` text names: whole numbers of px within the stage's range, ascending, once each.
|
|
25
|
+
*
|
|
26
|
+
* @param text - the widths, separated by commas (`360,768,1440`).
|
|
27
|
+
* @returns the widths, or the message that says what is wrong.
|
|
28
|
+
*/
|
|
29
|
+
export const parseWidths = (text) => {
|
|
30
|
+
const parts = text
|
|
31
|
+
.split(",")
|
|
32
|
+
.map((part) => part.trim())
|
|
33
|
+
.filter((part) => part !== "");
|
|
34
|
+
if (parts.length === 0)
|
|
35
|
+
return "--widths needs at least one width";
|
|
36
|
+
const wrong = parts.find((part) => {
|
|
37
|
+
const width = Number(part);
|
|
38
|
+
return !Number.isInteger(width) || width < WIDTH_RANGE.min || width > WIDTH_RANGE.max;
|
|
39
|
+
});
|
|
40
|
+
if (wrong !== undefined) {
|
|
41
|
+
return `--widths takes whole numbers of px from ${WIDTH_RANGE.min} to ${WIDTH_RANGE.max}, not "${wrong}"`;
|
|
42
|
+
}
|
|
43
|
+
return [...new Set(parts.map(Number))].sort((left, right) => left - right);
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* The document the shell reads: the stories measured, in the catalogue's order.
|
|
47
|
+
*
|
|
48
|
+
* @param widths - the widths measured at.
|
|
49
|
+
* @param measured - each measured story's heights, at `widths`.
|
|
50
|
+
*/
|
|
51
|
+
export const heightsDocument = (widths, measured) => ({ widths: [...widths], heights: Object.fromEntries(measured) });
|
|
52
|
+
/**
|
|
53
|
+
* The catalogue to measure: one already running, or a built one served for the run.
|
|
54
|
+
*
|
|
55
|
+
* @param catalogue - the running catalogue's address, used when there is nothing to serve.
|
|
56
|
+
* @param serve - the folder of a built catalogue to serve; none to use the running one.
|
|
57
|
+
*/
|
|
58
|
+
export const openCatalogue = async (catalogue, serve) => {
|
|
59
|
+
if (serve === undefined) {
|
|
60
|
+
return { address: catalogue.endsWith("/") ? catalogue : `${catalogue}/`, close: async () => undefined };
|
|
61
|
+
}
|
|
62
|
+
const server = await preview({
|
|
63
|
+
root: serve,
|
|
64
|
+
configFile: false,
|
|
65
|
+
logLevel: "silent",
|
|
66
|
+
build: { outDir: "." },
|
|
67
|
+
preview: { host: "127.0.0.1", port: 0, strictPort: false }
|
|
68
|
+
});
|
|
69
|
+
const [address] = server.resolvedUrls?.local ?? [];
|
|
70
|
+
if (address === undefined) {
|
|
71
|
+
await server.close();
|
|
72
|
+
throw new Error(`Vite's preview server did not say where it serves ${serve}`);
|
|
73
|
+
}
|
|
74
|
+
return { address, close: () => server.close() };
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* The catalogue's story ids, as its shell writes them into the page.
|
|
78
|
+
*
|
|
79
|
+
* @param browser - the browser.
|
|
80
|
+
* @param address - the catalogue's address.
|
|
81
|
+
* @param timeout - how long to wait for the shell, in ms.
|
|
82
|
+
*/
|
|
83
|
+
export const catalogueStoryIds = async (browser, address, timeout) => {
|
|
84
|
+
const page = await browser.newPage();
|
|
85
|
+
try {
|
|
86
|
+
await page.goto(address);
|
|
87
|
+
const element = page.locator(`#${STORY_IDS_ELEMENT}`);
|
|
88
|
+
await element.waitFor({ state: "attached", timeout });
|
|
89
|
+
const ids = JSON.parse((await element.textContent()) ?? "[]");
|
|
90
|
+
return Array.isArray(ids) ? ids.filter((id) => typeof id === "string") : [];
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
await page.close();
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
/** The frame's height as it reports it to the shell: its body's own height, in whole px. */
|
|
97
|
+
const frameHeight = async (page) =>
|
|
98
|
+
// a text expression: the command runs on Node, where the page's own globals are not typed
|
|
99
|
+
Number(await page.evaluate("Math.ceil(document.body.getBoundingClientRect().height)"));
|
|
100
|
+
/** Waits until the page laid out what a resize changed: two animation frames. */
|
|
101
|
+
const settled = async (page) => {
|
|
102
|
+
await page.evaluate("new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve(undefined))))");
|
|
103
|
+
};
|
|
104
|
+
/**
|
|
105
|
+
* One story's heights at each width, or why it could not be measured.
|
|
106
|
+
*
|
|
107
|
+
* @param page - a page to measure in.
|
|
108
|
+
* @param address - the catalogue's address.
|
|
109
|
+
* @param storyId - the story's id.
|
|
110
|
+
* @param widths - the widths, ascending.
|
|
111
|
+
* @param timeout - how long to wait for the story, in ms.
|
|
112
|
+
*/
|
|
113
|
+
const measureStory = async (page, address, storyId, widths, timeout) => {
|
|
114
|
+
const [first = WIDTH_RANGE.min] = widths;
|
|
115
|
+
await page.setViewportSize({ width: first, height: VIEWPORT_HEIGHT });
|
|
116
|
+
await page.goto(new URL(frameAddressOf("frame.html", storyId, ""), address).href);
|
|
117
|
+
const status = await page
|
|
118
|
+
.waitForFunction(`document.documentElement.getAttribute(${JSON.stringify(STATUS_ATTRIBUTE)})`, undefined, {
|
|
119
|
+
timeout
|
|
120
|
+
})
|
|
121
|
+
.then(async (handle) => String(await handle.jsonValue()))
|
|
122
|
+
.catch(() => "timeout");
|
|
123
|
+
if (status === "timeout")
|
|
124
|
+
return { story: storyId, reason: `no answer in ${timeout} ms` };
|
|
125
|
+
if (status !== "ready") {
|
|
126
|
+
const error = await page.locator(".forgebook-frame-error__message").first().textContent();
|
|
127
|
+
return { story: storyId, reason: error ?? "the story failed" };
|
|
128
|
+
}
|
|
129
|
+
const heights = [];
|
|
130
|
+
for (const width of widths) {
|
|
131
|
+
await page.setViewportSize({ width, height: VIEWPORT_HEIGHT });
|
|
132
|
+
await settled(page);
|
|
133
|
+
heights.push(await frameHeight(page));
|
|
134
|
+
}
|
|
135
|
+
return heights;
|
|
136
|
+
};
|
|
137
|
+
/**
|
|
138
|
+
* Measures every story of a catalogue at each width.
|
|
139
|
+
*
|
|
140
|
+
* @param request - the catalogue, the widths, how many at once, and what to tell along the way.
|
|
141
|
+
*/
|
|
142
|
+
export const measureStories = async (request) => {
|
|
143
|
+
const browser = await chromium.launch();
|
|
144
|
+
try {
|
|
145
|
+
const storyIds = await catalogueStoryIds(browser, request.address, request.timeout);
|
|
146
|
+
request.onStart(storyIds.length);
|
|
147
|
+
const measured = new Map();
|
|
148
|
+
const failures = [];
|
|
149
|
+
let next = 0;
|
|
150
|
+
// each worker takes the next story until none is left, so a slow story never holds the others
|
|
151
|
+
const worker = async () => {
|
|
152
|
+
const page = await browser.newPage();
|
|
153
|
+
try {
|
|
154
|
+
for (let index = next++; index < storyIds.length; index = next++) {
|
|
155
|
+
const storyId = storyIds[index] ?? "";
|
|
156
|
+
const result = await measureStory(page, request.address, storyId, request.widths, request.timeout);
|
|
157
|
+
if ("reason" in result)
|
|
158
|
+
failures.push(result);
|
|
159
|
+
else
|
|
160
|
+
measured.set(storyId, result);
|
|
161
|
+
request.onStory(storyId);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
finally {
|
|
165
|
+
await page.close();
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
await Promise.all(Array.from({ length: Math.max(1, request.concurrency) }, worker));
|
|
169
|
+
// the catalogue's order, not the order the workers finished in
|
|
170
|
+
const ordered = new Map(storyIds.flatMap((id) => {
|
|
171
|
+
const heights = measured.get(id);
|
|
172
|
+
return heights === undefined ? [] : [[id, heights]];
|
|
173
|
+
}));
|
|
174
|
+
return { storyIds, measured: ordered, failures };
|
|
175
|
+
}
|
|
176
|
+
finally {
|
|
177
|
+
await browser.close();
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
//# sourceMappingURL=measureHeights.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"measureHeights.js","sourceRoot":"","sources":["../../../bin/forgebook-heights/measureHeights.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `--json` document of `forgebook-heights`: how many stories were measured, which failed and why, and where the
|
|
3
|
+
* heights went.
|
|
4
|
+
*
|
|
5
|
+
* @module
|
|
6
|
+
*/
|
|
7
|
+
/** A story that could not be measured. */
|
|
8
|
+
export type HeightFailure = {
|
|
9
|
+
/** The story's id. */
|
|
10
|
+
readonly story: string;
|
|
11
|
+
/** Why: its error, or that it did not answer in time. */
|
|
12
|
+
readonly reason: string;
|
|
13
|
+
};
|
|
14
|
+
/** What `forgebook-heights` reports. */
|
|
15
|
+
export type HeightsReport = {
|
|
16
|
+
/** The file the heights were written to. */
|
|
17
|
+
readonly out: string;
|
|
18
|
+
/** The widths measured at, in CSS px. */
|
|
19
|
+
readonly widths: readonly number[];
|
|
20
|
+
/** How many stories the catalogue has. */
|
|
21
|
+
readonly stories: number;
|
|
22
|
+
/** How many were measured. */
|
|
23
|
+
readonly measured: number;
|
|
24
|
+
/** The ones that failed, left out of the file. */
|
|
25
|
+
readonly failures: readonly HeightFailure[];
|
|
26
|
+
};
|
|
27
|
+
//# sourceMappingURL=measureHeights.types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"measureHeights.types.d.ts","sourceRoot":"","sources":["../../../bin/forgebook-heights/measureHeights.types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,0CAA0C;AAC1C,MAAM,MAAM,aAAa,GAAG;IAC1B,sBAAsB;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,yDAAyD;IACzD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB,CAAC;AAEF,wCAAwC;AACxC,MAAM,MAAM,aAAa,GAAG;IAC1B,4CAA4C;IAC5C,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,yCAAyC;IACzC,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,0CAA0C;IAC1C,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,8BAA8B;IAC9B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,kDAAkD;IAClD,QAAQ,CAAC,QAAQ,EAAE,SAAS,aAAa,EAAE,CAAC;CAC7C,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"measureHeights.types.js","sourceRoot":"","sources":["../../../bin/forgebook-heights/measureHeights.types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"forgebook-shot.d.mts","sourceRoot":"","sources":["../../../bin/forgebook-shot/forgebook-shot.mts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* The `forgebook-shot` command: opens one story of a running catalogue in a browser, waits for it, and reports how it
|
|
4
|
+
* ended, its size, a picture and, when asked, the accessibility rules it breaks: what an agent needs to check a story
|
|
5
|
+
* it wrote without a person looking.
|
|
6
|
+
*
|
|
7
|
+
* @remarks
|
|
8
|
+
* Usage: `forgebook-shot --help` (generated by `@ensiria/cli-kit`). The catalogue must be running
|
|
9
|
+
* (`forgebook dev`).
|
|
10
|
+
*
|
|
11
|
+
* @module
|
|
12
|
+
*/
|
|
13
|
+
import { resolve } from "node:path";
|
|
14
|
+
import { defineScript, ScriptError, usageError } from "@ensiria/cli-kit/script";
|
|
15
|
+
import { takeShot } from "./storyShot.js";
|
|
16
|
+
/** The command, as the hints print it. */
|
|
17
|
+
const COMMAND = "forgebook-shot";
|
|
18
|
+
await defineScript({
|
|
19
|
+
name: "forgebook-shot",
|
|
20
|
+
command: COMMAND,
|
|
21
|
+
summary: "Open one story of a running catalogue, wait for it, and report it with a picture.",
|
|
22
|
+
icon: "check",
|
|
23
|
+
json: true,
|
|
24
|
+
description: "Opens the story's frame (the address the catalogue's stage and the generated tests open) in Chromium with the given view, waits until it says ready or failed, and reports its size, the page's console errors, a full-page PNG when --out is given, and the axe violations of WCAG 2.2 A and AA with --a11y. A story that fails or never answers ends the command with an error.",
|
|
25
|
+
options: {
|
|
26
|
+
catalogue: {
|
|
27
|
+
type: "string",
|
|
28
|
+
default: "http://localhost:5310/",
|
|
29
|
+
valueName: "url",
|
|
30
|
+
env: "CATALOGUE_URL",
|
|
31
|
+
description: "The running catalogue's address."
|
|
32
|
+
},
|
|
33
|
+
story: {
|
|
34
|
+
type: "string",
|
|
35
|
+
required: true,
|
|
36
|
+
valueName: "id",
|
|
37
|
+
description: "The story's id, as the catalogue's address writes it (acme-ui:feedback-counter--plain)."
|
|
38
|
+
},
|
|
39
|
+
view: {
|
|
40
|
+
type: "string",
|
|
41
|
+
default: "",
|
|
42
|
+
valueName: "query",
|
|
43
|
+
description: "The view, as the catalogue's address writes it (theme=dark&contrast=more&lang=it)."
|
|
44
|
+
},
|
|
45
|
+
width: {
|
|
46
|
+
type: "string",
|
|
47
|
+
default: "1024",
|
|
48
|
+
valueName: "px",
|
|
49
|
+
description: "The width the story is drawn at."
|
|
50
|
+
},
|
|
51
|
+
out: { type: "string", valueName: "file", description: "Where to write the picture (a .png)." },
|
|
52
|
+
a11y: { type: "boolean", description: "Also check the story's accessibility (axe, WCAG 2.2 A and AA)." },
|
|
53
|
+
timeout: {
|
|
54
|
+
type: "string",
|
|
55
|
+
default: "30000",
|
|
56
|
+
valueName: "ms",
|
|
57
|
+
description: "How long to wait for the story."
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
examples: [
|
|
61
|
+
{
|
|
62
|
+
command: `${COMMAND} --story acme-ui:feedback-counter--plain --out counter.png`,
|
|
63
|
+
description: "A picture of a story, in the default view."
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
command: `${COMMAND} --story acme-ui:feedback-counter--plain --view theme=dark --a11y --json`,
|
|
67
|
+
description: "The dark theme, with its accessibility, as JSON."
|
|
68
|
+
}
|
|
69
|
+
],
|
|
70
|
+
run: async ({ options, log }) => {
|
|
71
|
+
const width = Number(options.width);
|
|
72
|
+
const timeout = Number(options.timeout);
|
|
73
|
+
if (!Number.isInteger(width) || width < 1)
|
|
74
|
+
throw usageError("--width needs a whole number of px");
|
|
75
|
+
if (!Number.isInteger(timeout) || timeout < 1)
|
|
76
|
+
throw usageError("--timeout needs a whole number of ms");
|
|
77
|
+
const shot = await takeShot({
|
|
78
|
+
catalogue: options.catalogue,
|
|
79
|
+
story: options.story,
|
|
80
|
+
view: options.view,
|
|
81
|
+
width,
|
|
82
|
+
out: options.out === undefined ? undefined : resolve(options.out),
|
|
83
|
+
isA11y: options.a11y,
|
|
84
|
+
timeout
|
|
85
|
+
});
|
|
86
|
+
if (shot.status === "timeout") {
|
|
87
|
+
throw new ScriptError(`${shot.story} did not answer in ${timeout} ms`, {
|
|
88
|
+
hint: `Is the catalogue running at ${options.catalogue}? Open ${shot.address}`
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
for (const message of shot.consoleErrors)
|
|
92
|
+
log.warn(message);
|
|
93
|
+
if (shot.status === "error") {
|
|
94
|
+
throw new ScriptError(`${shot.story} failed: ${shot.error ?? "no message"}`, {
|
|
95
|
+
hint: `Open ${shot.address} to see it`
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
log.success(`${shot.story} is shown, ${shot.size.width} × ${shot.size.height} px`);
|
|
99
|
+
if (shot.screenshot !== undefined)
|
|
100
|
+
log.info(`picture: ${shot.screenshot}`);
|
|
101
|
+
if (shot.violations !== undefined) {
|
|
102
|
+
if (shot.violations.length === 0)
|
|
103
|
+
log.success("no accessibility rule broken");
|
|
104
|
+
for (const violation of shot.violations)
|
|
105
|
+
log.error(`${violation.id}: ${violation.help}`);
|
|
106
|
+
}
|
|
107
|
+
return shot;
|
|
108
|
+
}
|
|
109
|
+
}).run();
|
|
110
|
+
//# sourceMappingURL=forgebook-shot.mjs.map
|