@forgebook/testing 0.1.8 → 0.2.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/README.md +33 -0
- package/dist/bin/forgebook-heights/forgebook-heights.d.mts +0 -1
- package/dist/bin/forgebook-heights/forgebook-heights.mjs +0 -1
- package/dist/bin/forgebook-heights/measureHeights.d.ts +0 -1
- package/dist/bin/forgebook-heights/measureHeights.js +2 -2
- package/dist/bin/forgebook-heights/measureHeights.types.d.ts +0 -1
- package/dist/bin/forgebook-heights/measureHeights.types.js +0 -1
- package/dist/bin/forgebook-shot/forgebook-shot.d.mts +0 -1
- package/dist/bin/forgebook-shot/forgebook-shot.mjs +0 -1
- package/dist/bin/forgebook-shot/storyShot.d.ts +0 -1
- package/dist/bin/forgebook-shot/storyShot.js +2 -2
- package/dist/bin/forgebook-shot/storyShot.types.d.ts +0 -1
- package/dist/bin/forgebook-shot/storyShot.types.js +0 -1
- package/dist/src/browserTests.d.ts +0 -17
- package/dist/src/browserTests.js +72 -35
- package/dist/src/checkPage.d.ts +16 -0
- package/dist/src/checkPage.js +68 -0
- package/dist/src/checkStories.d.ts +0 -1
- package/dist/src/checkStories.js +18 -11
- package/dist/src/conformance.d.ts +46 -0
- package/dist/src/conformance.js +146 -0
- package/dist/src/deviceShot.d.ts +18 -0
- package/dist/src/deviceShot.js +29 -0
- package/dist/src/diffStories.d.ts +77 -0
- package/dist/src/diffStories.js +251 -0
- package/dist/src/overlays.d.ts +0 -1
- package/dist/src/overlays.js +0 -1
- package/dist/src/playwrightConfig.d.ts +0 -1
- package/dist/src/playwrightConfig.js +8 -3
- package/dist/src/semanticJudge.d.ts +50 -0
- package/dist/src/semanticJudge.js +111 -0
- package/dist/src/serverTests.d.ts +0 -1
- package/dist/src/serverTests.js +51 -39
- package/dist/src/shotStories.d.ts +2 -2
- package/dist/src/shotStories.js +31 -6
- package/dist/src/storyCheck.d.ts +9 -2
- package/dist/src/storyCheck.js +45 -3
- package/dist/src/storyControls.d.ts +0 -1
- package/dist/src/storyControls.js +0 -1
- package/dist/src/storyFiles.d.ts +0 -1
- package/dist/src/storyFiles.js +0 -1
- package/dist/src/storyPages.d.ts +9 -2
- package/dist/src/storyPages.js +19 -5
- package/dist/src/storyTestPlan.d.ts +44 -0
- package/dist/src/storyTestPlan.js +20 -0
- package/dist/src/testViteConfig.d.ts +2 -3
- package/dist/src/testViteConfig.js +4 -4
- package/dist/src/touchWindow.d.ts +0 -1
- package/dist/src/touchWindow.js +0 -1
- package/dist/src/visualBrowser.d.ts +0 -1
- package/dist/src/visualBrowser.js +0 -1
- package/package.json +16 -4
- package/dist/bin/forgebook-heights/forgebook-heights.d.mts.map +0 -1
- package/dist/bin/forgebook-heights/forgebook-heights.mjs.map +0 -1
- package/dist/bin/forgebook-heights/measureHeights.d.ts.map +0 -1
- package/dist/bin/forgebook-heights/measureHeights.js.map +0 -1
- package/dist/bin/forgebook-heights/measureHeights.types.d.ts.map +0 -1
- package/dist/bin/forgebook-heights/measureHeights.types.js.map +0 -1
- package/dist/bin/forgebook-shot/forgebook-shot.d.mts.map +0 -1
- package/dist/bin/forgebook-shot/forgebook-shot.mjs.map +0 -1
- package/dist/bin/forgebook-shot/storyShot.d.ts.map +0 -1
- package/dist/bin/forgebook-shot/storyShot.js.map +0 -1
- package/dist/bin/forgebook-shot/storyShot.types.d.ts.map +0 -1
- package/dist/bin/forgebook-shot/storyShot.types.js.map +0 -1
- package/dist/src/browserTests.d.ts.map +0 -1
- package/dist/src/browserTests.js.map +0 -1
- package/dist/src/checkStories.d.ts.map +0 -1
- package/dist/src/checkStories.js.map +0 -1
- package/dist/src/overlays.d.ts.map +0 -1
- package/dist/src/overlays.js.map +0 -1
- package/dist/src/playwrightConfig.d.ts.map +0 -1
- package/dist/src/playwrightConfig.js.map +0 -1
- package/dist/src/serverTests.d.ts.map +0 -1
- package/dist/src/serverTests.js.map +0 -1
- package/dist/src/shotStories.d.ts.map +0 -1
- package/dist/src/shotStories.js.map +0 -1
- package/dist/src/storyCheck.d.ts.map +0 -1
- package/dist/src/storyCheck.js.map +0 -1
- package/dist/src/storyControls.d.ts.map +0 -1
- package/dist/src/storyControls.js.map +0 -1
- package/dist/src/storyFiles.d.ts.map +0 -1
- package/dist/src/storyFiles.js.map +0 -1
- package/dist/src/storyPages.d.ts.map +0 -1
- package/dist/src/storyPages.js.map +0 -1
- package/dist/src/testViteConfig.d.ts.map +0 -1
- package/dist/src/testViteConfig.js.map +0 -1
- package/dist/src/touchWindow.d.ts.map +0 -1
- package/dist/src/touchWindow.js.map +0 -1
- package/dist/src/visualBrowser.d.ts.map +0 -1
- package/dist/src/visualBrowser.js.map +0 -1
package/README.md
CHANGED
|
@@ -107,12 +107,23 @@ import { storyTestViteConfig } from "@forgebook/testing/vite";
|
|
|
107
107
|
export default storyTestViteConfig(import.meta.dirname, react());
|
|
108
108
|
```
|
|
109
109
|
|
|
110
|
+
The config holds the framework's own Vite plugins (React's, or Solid's that compiles the stories' JSX), as the catalogue
|
|
111
|
+
does: don't add them again.
|
|
112
|
+
|
|
110
113
|
Then `npx playwright install chromium` once, and `playwright test -c test/stories/playwright.config.ts` starts the
|
|
111
114
|
frame page and runs one test per story and variant (light, dark, both with more contrast, a narrow width, and the
|
|
112
115
|
structure view), starting Vite through the package manager that runs them (`vite` in `testServer.ts` names
|
|
113
116
|
another command). A story that breaks a rule fails all its variants; `storyBrowserTests` also takes `check`, a rule of
|
|
114
117
|
the package's own run on every frame.
|
|
115
118
|
|
|
119
|
+
A story about logic (`parameters: { logic: true }`, a hook's or a function's scene) is tested once, in the catalogue's
|
|
120
|
+
default view, named `logic`: the variants only change how it looks. Its test still waits for the frame to be ready, so
|
|
121
|
+
its play has to pass, and fails on an error in the page; axe runs too, since the scene is a page a person uses and a
|
|
122
|
+
demo that fails WCAG is a bad example to copy. The checks about looks are left out: the spills and the overlays (layout
|
|
123
|
+
at the variants' widths) and the package's own `check`, and a screenshot run takes no reference image of it.
|
|
124
|
+
`forgebook check` runs the same checks on it, and with `--all-variants` both it and `forgebook shot` open it once, in
|
|
125
|
+
the default view.
|
|
126
|
+
|
|
116
127
|
### On the server
|
|
117
128
|
|
|
118
129
|
One test file and a Vitest configuration with the catalogue's plugin, which reads the source's
|
|
@@ -158,3 +169,25 @@ translation catalogue), its HTML checked.
|
|
|
158
169
|
| `forgebook-shot` | Opens one story of a running catalogue and reports it with a picture |
|
|
159
170
|
|
|
160
171
|
More: [Forgebook's README](https://github.com/ensiria/forgebook/blob/develop/packages/forgebook/README.md).
|
|
172
|
+
|
|
173
|
+
## Writing a framework adapter
|
|
174
|
+
|
|
175
|
+
A framework is an adapter package: a renderer, a code writer and a docs reader. `@forgebook/testing/conformance` is what
|
|
176
|
+
every adapter passes, in its own Vitest with a simulated page, so a framework is done when this is green:
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
import { describeAdapter } from "@forgebook/testing/conformance";
|
|
180
|
+
|
|
181
|
+
describeAdapter({ name: "Solid", renderer, drawing, decorator, codeOf, docsOf });
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
`drawing(name)` gives a story with a button showing `Hello <name> <clicks>`, its clicks in its own state, which throws
|
|
185
|
+
for `THROWING_NAME`. `decorator(label)` draws a story inside a `section` with that `data-decorator`. A renderer's
|
|
186
|
+
`renderToString` may return a promise. The frame takes only the drawing half, a `FrameRenderer` of `forgebook/renderer`:
|
|
187
|
+
a framework's frame page imports a module without the server render, so the server renderer stays out of a static
|
|
188
|
+
catalogue and of a first start's pre-bundle (`reactFrameRenderer` in `@forgebook/react`). A framework whose page and
|
|
189
|
+
server builds differ runs the suite twice, with `half: "page"` in a simulated page and `half: "server"` in Node: Svelte
|
|
190
|
+
compiles a component apart for each, and Solid's `solid-js/web` is its browser build or its server build, never both.
|
|
191
|
+
`test/conformance.test.tsx` in `@forgebook/react` and `@forgebook/preact`, `test/conformance.test.ts` in
|
|
192
|
+
`@forgebook/vue`, `@forgebook/web-components`, `@forgebook/html` and `@forgebook/angular`, and the two `conformance`
|
|
193
|
+
tests of `@forgebook/svelte` and of `@forgebook/solid` are complete examples.
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* @module
|
|
13
13
|
*/
|
|
14
14
|
import { chromium } from "@playwright/test";
|
|
15
|
+
import { codeLiteral } from "forgebook/code-literal";
|
|
15
16
|
import { STATUS_ATTRIBUTE } from "forgebook/frame-status";
|
|
16
17
|
import { STORY_IDS_ELEMENT } from "forgebook/story-ids-element";
|
|
17
18
|
import { frameAddressOf } from "forgebook/view-state";
|
|
@@ -115,7 +116,7 @@ const measureStory = async (page, address, storyId, widths, timeout) => {
|
|
|
115
116
|
await page.setViewportSize({ width: first, height: VIEWPORT_HEIGHT });
|
|
116
117
|
await page.goto(new URL(frameAddressOf("frame.html", storyId, ""), address).href);
|
|
117
118
|
const status = await page
|
|
118
|
-
.waitForFunction(`document.documentElement.getAttribute(${
|
|
119
|
+
.waitForFunction(`document.documentElement.getAttribute(${codeLiteral(STATUS_ATTRIBUTE)})`, undefined, {
|
|
119
120
|
timeout
|
|
120
121
|
})
|
|
121
122
|
.then(async (handle) => String(await handle.jsonValue()))
|
|
@@ -177,4 +178,3 @@ export const measureStories = async (request) => {
|
|
|
177
178
|
await browser.close();
|
|
178
179
|
}
|
|
179
180
|
};
|
|
180
|
-
//# sourceMappingURL=measureHeights.js.map
|
|
@@ -31,4 +31,3 @@ export declare const shotAddress: (catalogue: string, story: string, view: strin
|
|
|
31
31
|
* @returns the report; a frame that never answers is `timeout`, not an exception.
|
|
32
32
|
*/
|
|
33
33
|
export declare const takeShot: (request: ShotRequest) => Promise<StoryShot>;
|
|
34
|
-
//# sourceMappingURL=storyShot.d.ts.map
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import { AxeBuilder } from "@axe-core/playwright";
|
|
12
12
|
import { chromium } from "@playwright/test";
|
|
13
13
|
import { DEFAULT_A11Y_TAGS } from "forgebook/catalogue";
|
|
14
|
+
import { codeLiteral } from "forgebook/code-literal";
|
|
14
15
|
import { STATUS_ATTRIBUTE } from "forgebook/frame-status";
|
|
15
16
|
import { frameAddressOf } from "forgebook/view-state";
|
|
16
17
|
/**
|
|
@@ -44,7 +45,7 @@ export const takeShot = async (request) => {
|
|
|
44
45
|
await page.goto(address);
|
|
45
46
|
// a text expression: the command runs on Node, where the page's own globals are not typed
|
|
46
47
|
const status = await page
|
|
47
|
-
.waitForFunction(`document.documentElement.getAttribute(${
|
|
48
|
+
.waitForFunction(`document.documentElement.getAttribute(${codeLiteral(STATUS_ATTRIBUTE)})`, undefined, {
|
|
48
49
|
timeout: request.timeout
|
|
49
50
|
})
|
|
50
51
|
.then(async (handle) => ((await handle.jsonValue()) === "ready" ? "ready" : "error"))
|
|
@@ -80,4 +81,3 @@ export const takeShot = async (request) => {
|
|
|
80
81
|
await browser.close();
|
|
81
82
|
}
|
|
82
83
|
};
|
|
83
|
-
//# sourceMappingURL=storyShot.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
//# sourceMappingURL=storyShot.types.js.map
|
|
@@ -1,19 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* One Playwright test per story and variant, on the catalogue's frame pages: the address the catalogue shows is the
|
|
3
|
-
* address the tests open.
|
|
4
|
-
*
|
|
5
|
-
* @remarks
|
|
6
|
-
* Each test opens the frame for a story in a variant (the theme, the contrast, the style sheets), waits for the
|
|
7
|
-
* frame to say the story is shown (`data-forgebook-status`), its story clock held at its start, and fails on an error in the console or the page, or on
|
|
8
|
-
* an axe violation of WCAG 2.2 A and AA, on an element that spills out of its parent (`forgebook/spills`: what overlaps a
|
|
9
|
-
* neighbour when the story is narrower than it was drawn for), on an overlay its play leaves open past the frame the
|
|
10
|
-
* catalogue would give it (`overlays.ts`: the story then declares `parameters.height`), or on the source's own
|
|
11
|
-
* `check`. The variants include a
|
|
12
|
-
* narrow frame, as wide as the catalogue's stage beside the props panel. With `screenshot` it compares the frame with its reference image instead,
|
|
13
|
-
* for the visual tests, which run in the Playwright container so fonts and rendering are the same everywhere.
|
|
14
|
-
*
|
|
15
|
-
* @module
|
|
16
|
-
*/
|
|
17
1
|
import { type Page } from "@playwright/test";
|
|
18
2
|
import { type CatalogueLists } from "forgebook/catalogue";
|
|
19
3
|
import { type ViewSwitch } from "forgebook/switches";
|
|
@@ -68,4 +52,3 @@ export type StoryBrowserTestsOptions = StoryFilesOptions & {
|
|
|
68
52
|
* the source's own.
|
|
69
53
|
*/
|
|
70
54
|
export declare const storyBrowserTests: (options: StoryBrowserTestsOptions) => void;
|
|
71
|
-
//# sourceMappingURL=browserTests.d.ts.map
|
package/dist/src/browserTests.js
CHANGED
|
@@ -11,19 +11,23 @@
|
|
|
11
11
|
* `check`. The variants include a
|
|
12
12
|
* narrow frame, as wide as the catalogue's stage beside the props panel. With `screenshot` it compares the frame with its reference image instead,
|
|
13
13
|
* for the visual tests, which run in the Playwright container so fonts and rendering are the same everywhere.
|
|
14
|
+
* A story about logic (`parameters.logic`) is tested once, in the default view, without the layout checks, and never
|
|
15
|
+
* pictured (`storyTestPlan.ts`).
|
|
14
16
|
*
|
|
15
17
|
* @module
|
|
16
18
|
*/
|
|
19
|
+
import { existsSync } from "node:fs";
|
|
17
20
|
import { expect, test } from "@playwright/test";
|
|
18
21
|
import { catalogueSettings } from "forgebook/catalogue";
|
|
19
22
|
import { STATUS_ATTRIBUTE } from "forgebook/frame-status";
|
|
20
23
|
import { heldClockAddress } from "forgebook/story-clock";
|
|
21
|
-
import { storyTree, testedStoryLocations } from "forgebook/story-tree";
|
|
24
|
+
import { formerStoryId, storyTree, testedStoryLocations } from "forgebook/story-tree";
|
|
22
25
|
import { DEFAULT_SWITCHES, switchWithRole } from "forgebook/switches";
|
|
23
26
|
import { defaultView, frameAddress } from "forgebook/view-state";
|
|
24
27
|
import { isOverlayChecked } from "./overlays.js";
|
|
25
28
|
import { axeProblems, overlayProblems, spillProblems, watchedErrors } from "./storyCheck.js";
|
|
26
29
|
import { indexedSource } from "./storyFiles.js";
|
|
30
|
+
import { plannedStoryTests } from "./storyTestPlan.js";
|
|
27
31
|
/** The narrow variant's width: about the catalogue's stage beside the props panel in a laptop window, in CSS px. */
|
|
28
32
|
export const NARROW_WIDTH = 560;
|
|
29
33
|
/**
|
|
@@ -58,6 +62,41 @@ export const storyVariants = (switches) => {
|
|
|
58
62
|
};
|
|
59
63
|
/** The variants of a catalogue with the default switches: light, dark, both at more contrast, structure, narrow. */
|
|
60
64
|
export const STORY_VARIANTS = storyVariants(DEFAULT_SWITCHES);
|
|
65
|
+
/** A problem list's messages, so a failing test names each problem. */
|
|
66
|
+
const messages = (problems) => problems.map((problem) => problem.message);
|
|
67
|
+
/**
|
|
68
|
+
* Runs the checks of one planned test on a shown story: axe, then for a story about its looks the spills, the
|
|
69
|
+
* source's own check, and the overlays last (it resizes the window, which may close what the story left open). A story
|
|
70
|
+
* about logic gets axe only.
|
|
71
|
+
*
|
|
72
|
+
* @param page - the story's frame, ready.
|
|
73
|
+
* @param planned - the story, its variant and whether its layout is checked.
|
|
74
|
+
* @param a11y - the catalogue's accessibility rules.
|
|
75
|
+
* @param check - the source's own check, if any.
|
|
76
|
+
*/
|
|
77
|
+
const expectChecksPass = async (page, planned, a11y, check) => {
|
|
78
|
+
expect(messages(await axeProblems(page, a11y))).toEqual([]);
|
|
79
|
+
if (!planned.hasLookChecks)
|
|
80
|
+
return;
|
|
81
|
+
expect(messages(await spillProblems(page))).toEqual([]);
|
|
82
|
+
await check?.(page);
|
|
83
|
+
expect(messages(await overlayProblems(page, isOverlayChecked(planned.variant.view), planned.story.height))).toEqual([]);
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* A story's screenshot name in a variant: by its id, or by the id an earlier rule gave it while only a screenshot of
|
|
87
|
+
* that name is stored, so the screenshots taken before the ids followed Storybook's keep comparing.
|
|
88
|
+
*
|
|
89
|
+
* @param id - the story's id.
|
|
90
|
+
* @param formerId - the id the former rule gave it.
|
|
91
|
+
* @param variant - the variant's name.
|
|
92
|
+
*/
|
|
93
|
+
const screenshotName = (id, formerId, variant) => {
|
|
94
|
+
const named = (storyId) => `${storyId.replace(":", "--")}--${variant}.png`;
|
|
95
|
+
const isStored = (name) => existsSync(test.info().snapshotPath(name, { kind: "screenshot" }));
|
|
96
|
+
return named(formerId) !== named(id) && !isStored(named(id)) && isStored(named(formerId))
|
|
97
|
+
? named(formerId)
|
|
98
|
+
: named(id);
|
|
99
|
+
};
|
|
61
100
|
/**
|
|
62
101
|
* Declares the tests, one `describe` per component and one test per story and variant.
|
|
63
102
|
*
|
|
@@ -69,44 +108,42 @@ export const storyBrowserTests = (options) => {
|
|
|
69
108
|
const variants = options.variants ?? storyVariants(switches);
|
|
70
109
|
const catalogue = catalogueSettings("", switches, options.lists);
|
|
71
110
|
const defaults = defaultView(catalogue);
|
|
72
|
-
const
|
|
111
|
+
const source = indexedSource(options);
|
|
112
|
+
const tree = storyTree([source]);
|
|
73
113
|
const groups = Map.groupBy(testedStoryLocations(tree), (location) => location.component.title);
|
|
74
114
|
for (const [title, locations] of groups) {
|
|
115
|
+
const planned = plannedStoryTests(locations.map((location) => location.story), variants, screenshot);
|
|
116
|
+
// a component whose stories are all about logic has nothing to picture
|
|
117
|
+
if (planned.length === 0)
|
|
118
|
+
continue;
|
|
75
119
|
test.describe(title, () => {
|
|
76
|
-
for (const { story } of
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
await check?.(page);
|
|
103
|
-
// last: it resizes the window, which may close what the story left open
|
|
104
|
-
expect(messages(await overlayProblems(page, isOverlayChecked(variant.view), story.height))).toEqual([]);
|
|
105
|
-
expect(errors).toEqual([]);
|
|
106
|
-
});
|
|
107
|
-
}
|
|
120
|
+
for (const { story, variant, hasLookChecks } of planned) {
|
|
121
|
+
test(`${story.name} · ${variant.name}`, async ({ page }) => {
|
|
122
|
+
const errors = watchedErrors(page);
|
|
123
|
+
if (variant.width !== undefined)
|
|
124
|
+
await page.setViewportSize({ width: variant.width, height: 800 });
|
|
125
|
+
// a Live story is checked and pictured at its start: its clock held, so its image never changes;
|
|
126
|
+
// always on its page's own colour, whatever a variant names: a backdrop is for looking
|
|
127
|
+
const view = {
|
|
128
|
+
...defaults,
|
|
129
|
+
...variant.view,
|
|
130
|
+
switches: { ...defaults.switches, ...variant.view.switches },
|
|
131
|
+
backdrop: "plain"
|
|
132
|
+
};
|
|
133
|
+
await page.goto(heldClockAddress(frameAddress(frameUrl, story.id, view, switches)));
|
|
134
|
+
await expect(page.locator("html")).toHaveAttribute(STATUS_ATTRIBUTE, "ready");
|
|
135
|
+
if (screenshot) {
|
|
136
|
+
const name = screenshotName(story.id, formerStoryId(source.id, title, story.exportName), variant.name);
|
|
137
|
+
await expect(page).toHaveScreenshot(name, {
|
|
138
|
+
fullPage: true,
|
|
139
|
+
animations: "disabled"
|
|
140
|
+
});
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
await expectChecksPass(page, { story, variant, hasLookChecks }, catalogue.a11y, check);
|
|
144
|
+
expect(errors).toEqual([]);
|
|
145
|
+
});
|
|
108
146
|
}
|
|
109
147
|
});
|
|
110
148
|
}
|
|
111
149
|
};
|
|
112
|
-
//# sourceMappingURL=browserTests.js.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type CatalogueLists } from "forgebook/catalogue";
|
|
2
|
+
import type { StoryCheckReport } from "forgebook/story-check";
|
|
3
|
+
/** What a page check is asked. */
|
|
4
|
+
export type PageCheckRequest = {
|
|
5
|
+
/** The pages' addresses. */
|
|
6
|
+
readonly addresses: readonly string[];
|
|
7
|
+
/** The catalogue's lists: its accessibility rules. */
|
|
8
|
+
readonly lists: CatalogueLists;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Checks pages, on one browser.
|
|
12
|
+
*
|
|
13
|
+
* @param request - the addresses and the catalogue's settings.
|
|
14
|
+
* @returns one result per page, in the request's order.
|
|
15
|
+
*/
|
|
16
|
+
export declare const checkPages: ({ addresses, lists }: PageCheckRequest) => Promise<StoryCheckReport>;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A real page of an application checked as a story is: accessibility, what spills, and its errors.
|
|
3
|
+
*
|
|
4
|
+
* @remarks
|
|
5
|
+
* An agent verifies the screen it built, not only the stories behind it, so `forgebook check-page` opens any address.
|
|
6
|
+
* Loaded from the project only when it runs, like `checkStories.ts`. No frame says this page is ready, so it counts as
|
|
7
|
+
* ready when the network goes quiet, or after `QUIET_MS` for a page that keeps a connection open, which is checked as it
|
|
8
|
+
* stands then. The report has a story check's shape, the address standing for the story's id.
|
|
9
|
+
*
|
|
10
|
+
* @module
|
|
11
|
+
*/
|
|
12
|
+
import { chromium } from "@playwright/test";
|
|
13
|
+
import { catalogueSettings } from "forgebook/catalogue";
|
|
14
|
+
import { axeProblems, errorProblems, focusProblems, spillProblems, watchedErrors } from "./storyCheck.js";
|
|
15
|
+
/** How long a page gets for its network to go quiet, in milliseconds. */
|
|
16
|
+
const QUIET_MS = 10_000;
|
|
17
|
+
/** One page, checked. */
|
|
18
|
+
const checkedPage = async (address, open, a11y) => {
|
|
19
|
+
const page = await open();
|
|
20
|
+
const errors = watchedErrors(page);
|
|
21
|
+
const result = (problems) => ({
|
|
22
|
+
storyId: address,
|
|
23
|
+
view: "",
|
|
24
|
+
frame: address,
|
|
25
|
+
problems
|
|
26
|
+
});
|
|
27
|
+
try {
|
|
28
|
+
const response = await page.goto(address);
|
|
29
|
+
await page.waitForLoadState("networkidle", { timeout: QUIET_MS }).catch(() => undefined);
|
|
30
|
+
if (response !== null && !response.ok()) {
|
|
31
|
+
return result([{ kind: "frame", message: `It answered ${response.status()}` }]);
|
|
32
|
+
}
|
|
33
|
+
return result([
|
|
34
|
+
...(await axeProblems(page, a11y)),
|
|
35
|
+
...(await spillProblems(page, "body")),
|
|
36
|
+
...(await focusProblems(page)),
|
|
37
|
+
...errorProblems(errors)
|
|
38
|
+
]);
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
return result([{ kind: "frame", message: error instanceof Error ? error.message : String(error) }]);
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
await page.close();
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Checks pages, on one browser.
|
|
49
|
+
*
|
|
50
|
+
* @param request - the addresses and the catalogue's settings.
|
|
51
|
+
* @returns one result per page, in the request's order.
|
|
52
|
+
*/
|
|
53
|
+
export const checkPages = async ({ addresses, lists }) => {
|
|
54
|
+
const { a11y } = catalogueSettings("", [], lists);
|
|
55
|
+
const browser = await chromium.launch();
|
|
56
|
+
try {
|
|
57
|
+
// from a context of its own: axe opens pages of its own, which a bare `browser.newPage()` cannot share
|
|
58
|
+
const context = await browser.newContext();
|
|
59
|
+
const results = [];
|
|
60
|
+
for (const address of addresses) {
|
|
61
|
+
results.push(await checkedPage(address, () => context.newPage(), a11y));
|
|
62
|
+
}
|
|
63
|
+
return { schema: 1, results };
|
|
64
|
+
}
|
|
65
|
+
finally {
|
|
66
|
+
await browser.close();
|
|
67
|
+
}
|
|
68
|
+
};
|
|
@@ -6,4 +6,3 @@ import type { StoryCheckReport, StoryCheckRequest } from "forgebook/story-check"
|
|
|
6
6
|
* @returns one result per story and view, in the request's order.
|
|
7
7
|
*/
|
|
8
8
|
export declare const checkStories: (request: StoryCheckRequest) => Promise<StoryCheckReport>;
|
|
9
|
-
//# sourceMappingURL=checkStories.d.ts.map
|
package/dist/src/checkStories.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { catalogueSettings } from "forgebook/catalogue";
|
|
2
2
|
import { isOverlayChecked } from "./overlays.js";
|
|
3
|
-
import {
|
|
3
|
+
import { askModel, storySight } from "./semanticJudge.js";
|
|
4
|
+
import { axeProblems, errorProblems, focusProblems, overlayProblems, spillProblems, watchedErrors } from "./storyCheck.js";
|
|
4
5
|
import { eachStoryPage, openedStory, READY_MS } from "./storyPages.js";
|
|
6
|
+
import { hasLookChecks } from "./storyTestPlan.js";
|
|
5
7
|
/**
|
|
6
8
|
* One story in one view, checked.
|
|
7
9
|
*
|
|
@@ -9,7 +11,7 @@ import { eachStoryPage, openedStory, READY_MS } from "./storyPages.js";
|
|
|
9
11
|
* @param storyPage - the story and the view.
|
|
10
12
|
* @param a11y - the catalogue's accessibility settings.
|
|
11
13
|
*/
|
|
12
|
-
const checkedPage = async (page, storyPage, a11y) => {
|
|
14
|
+
const checkedPage = async (page, storyPage, a11y, semantic) => {
|
|
13
15
|
const errors = watchedErrors(page);
|
|
14
16
|
const status = await openedStory(page, storyPage);
|
|
15
17
|
const result = (problems) => ({
|
|
@@ -25,13 +27,19 @@ const checkedPage = async (page, storyPage, a11y) => {
|
|
|
25
27
|
]);
|
|
26
28
|
if (status === "error")
|
|
27
29
|
return result([{ kind: "frame", message: "The story could not be shown" }, ...errorProblems(errors)]);
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
// a story about logic gets the suite's checks of a scene, not those about looks (`storyTestPlan.ts`)
|
|
31
|
+
const lookProblems = hasLookChecks(storyPage.story)
|
|
32
|
+
? [
|
|
33
|
+
...(await spillProblems(page)),
|
|
34
|
+
// last of the page's checks: it resizes the window, which may close what the story left open
|
|
35
|
+
...(await overlayProblems(page, isOverlayChecked(storyPage.view), storyPage.story.height))
|
|
36
|
+
]
|
|
37
|
+
: [];
|
|
38
|
+
const axe = await axeProblems(page, a11y);
|
|
39
|
+
const checked = result([...axe, ...lookProblems, ...(await focusProblems(page)), ...errorProblems(errors)]);
|
|
40
|
+
return semantic === undefined
|
|
41
|
+
? checked
|
|
42
|
+
: { ...checked, judgements: await askModel(semantic, await storySight(page)) };
|
|
35
43
|
};
|
|
36
44
|
/**
|
|
37
45
|
* Checks the stories a request names, in its views, on one browser.
|
|
@@ -41,7 +49,6 @@ const checkedPage = async (page, storyPage, a11y) => {
|
|
|
41
49
|
*/
|
|
42
50
|
export const checkStories = async (request) => {
|
|
43
51
|
const { a11y } = catalogueSettings("", request.switches, request.lists);
|
|
44
|
-
const results = await eachStoryPage(request, (page, storyPage) => checkedPage(page, storyPage, a11y));
|
|
52
|
+
const results = await eachStoryPage(request, (page, storyPage) => checkedPage(page, storyPage, a11y, request.semantic));
|
|
45
53
|
return { schema: 1, results };
|
|
46
54
|
};
|
|
47
|
-
//# sourceMappingURL=checkStories.js.map
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { StoryCodeWriter, StoryDrawing, StoryRenderer } from "forgebook/renderer";
|
|
2
|
+
import type { LoadedDecorator } from "forgebook/story-module";
|
|
3
|
+
/** A half of the suite: what a story does in a page, or on a server. */
|
|
4
|
+
export type AdapterHalf = "page" | "server";
|
|
5
|
+
/** The name a subject's story throws for. */
|
|
6
|
+
export declare const THROWING_NAME = "boom";
|
|
7
|
+
/** What an adapter hands the suite. */
|
|
8
|
+
export type AdapterSubject = {
|
|
9
|
+
/** The framework's name, in the tests' titles. */
|
|
10
|
+
readonly name: string;
|
|
11
|
+
/** Its renderer. */
|
|
12
|
+
readonly renderer: StoryRenderer;
|
|
13
|
+
/**
|
|
14
|
+
* A drawing of a story with a button that shows `Hello <name> <clicks>`, counting its clicks in its own state, and that
|
|
15
|
+
* throws while drawn when the name is `THROWING_NAME`.
|
|
16
|
+
*
|
|
17
|
+
* @param name - the name, as the story's args carry it.
|
|
18
|
+
*/
|
|
19
|
+
readonly drawing: (name: string) => StoryDrawing;
|
|
20
|
+
/**
|
|
21
|
+
* A decorator that draws the story inside a `section` with this `data-decorator` value, written as the framework
|
|
22
|
+
* writes one.
|
|
23
|
+
*
|
|
24
|
+
* @param label - the value.
|
|
25
|
+
*/
|
|
26
|
+
readonly decorator: (label: string) => LoadedDecorator;
|
|
27
|
+
/** The code writer the shell uses. */
|
|
28
|
+
readonly codeOf: StoryCodeWriter;
|
|
29
|
+
/**
|
|
30
|
+
* The framework's docs of a story file's component, as `extractStoryComponent` reads them. None skips the check, for an
|
|
31
|
+
* adapter with no documented types yet.
|
|
32
|
+
*/
|
|
33
|
+
readonly docsOf?: () => unknown;
|
|
34
|
+
/**
|
|
35
|
+
* The half of the suite this run checks, for a framework whose components are compiled apart for a page and for a
|
|
36
|
+
* server: its package runs the suite once in a simulated page (`page`, with the code writer and the docs) and once in
|
|
37
|
+
* Node (`server`). Both halves when none.
|
|
38
|
+
*/
|
|
39
|
+
readonly half?: AdapterHalf;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* The suite of an adapter.
|
|
43
|
+
*
|
|
44
|
+
* @param subject - the adapter's subject.
|
|
45
|
+
*/
|
|
46
|
+
export declare const describeAdapter: (subject: AdapterSubject) => void;
|