@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.
Files changed (90) hide show
  1. package/README.md +33 -0
  2. package/dist/bin/forgebook-heights/forgebook-heights.d.mts +0 -1
  3. package/dist/bin/forgebook-heights/forgebook-heights.mjs +0 -1
  4. package/dist/bin/forgebook-heights/measureHeights.d.ts +0 -1
  5. package/dist/bin/forgebook-heights/measureHeights.js +2 -2
  6. package/dist/bin/forgebook-heights/measureHeights.types.d.ts +0 -1
  7. package/dist/bin/forgebook-heights/measureHeights.types.js +0 -1
  8. package/dist/bin/forgebook-shot/forgebook-shot.d.mts +0 -1
  9. package/dist/bin/forgebook-shot/forgebook-shot.mjs +0 -1
  10. package/dist/bin/forgebook-shot/storyShot.d.ts +0 -1
  11. package/dist/bin/forgebook-shot/storyShot.js +2 -2
  12. package/dist/bin/forgebook-shot/storyShot.types.d.ts +0 -1
  13. package/dist/bin/forgebook-shot/storyShot.types.js +0 -1
  14. package/dist/src/browserTests.d.ts +0 -17
  15. package/dist/src/browserTests.js +72 -35
  16. package/dist/src/checkPage.d.ts +16 -0
  17. package/dist/src/checkPage.js +68 -0
  18. package/dist/src/checkStories.d.ts +0 -1
  19. package/dist/src/checkStories.js +18 -11
  20. package/dist/src/conformance.d.ts +46 -0
  21. package/dist/src/conformance.js +146 -0
  22. package/dist/src/deviceShot.d.ts +18 -0
  23. package/dist/src/deviceShot.js +29 -0
  24. package/dist/src/diffStories.d.ts +77 -0
  25. package/dist/src/diffStories.js +251 -0
  26. package/dist/src/overlays.d.ts +0 -1
  27. package/dist/src/overlays.js +0 -1
  28. package/dist/src/playwrightConfig.d.ts +0 -1
  29. package/dist/src/playwrightConfig.js +8 -3
  30. package/dist/src/semanticJudge.d.ts +50 -0
  31. package/dist/src/semanticJudge.js +111 -0
  32. package/dist/src/serverTests.d.ts +0 -1
  33. package/dist/src/serverTests.js +51 -39
  34. package/dist/src/shotStories.d.ts +2 -2
  35. package/dist/src/shotStories.js +31 -6
  36. package/dist/src/storyCheck.d.ts +9 -2
  37. package/dist/src/storyCheck.js +45 -3
  38. package/dist/src/storyControls.d.ts +0 -1
  39. package/dist/src/storyControls.js +0 -1
  40. package/dist/src/storyFiles.d.ts +0 -1
  41. package/dist/src/storyFiles.js +0 -1
  42. package/dist/src/storyPages.d.ts +9 -2
  43. package/dist/src/storyPages.js +19 -5
  44. package/dist/src/storyTestPlan.d.ts +44 -0
  45. package/dist/src/storyTestPlan.js +20 -0
  46. package/dist/src/testViteConfig.d.ts +2 -3
  47. package/dist/src/testViteConfig.js +4 -4
  48. package/dist/src/touchWindow.d.ts +0 -1
  49. package/dist/src/touchWindow.js +0 -1
  50. package/dist/src/visualBrowser.d.ts +0 -1
  51. package/dist/src/visualBrowser.js +0 -1
  52. package/package.json +16 -4
  53. package/dist/bin/forgebook-heights/forgebook-heights.d.mts.map +0 -1
  54. package/dist/bin/forgebook-heights/forgebook-heights.mjs.map +0 -1
  55. package/dist/bin/forgebook-heights/measureHeights.d.ts.map +0 -1
  56. package/dist/bin/forgebook-heights/measureHeights.js.map +0 -1
  57. package/dist/bin/forgebook-heights/measureHeights.types.d.ts.map +0 -1
  58. package/dist/bin/forgebook-heights/measureHeights.types.js.map +0 -1
  59. package/dist/bin/forgebook-shot/forgebook-shot.d.mts.map +0 -1
  60. package/dist/bin/forgebook-shot/forgebook-shot.mjs.map +0 -1
  61. package/dist/bin/forgebook-shot/storyShot.d.ts.map +0 -1
  62. package/dist/bin/forgebook-shot/storyShot.js.map +0 -1
  63. package/dist/bin/forgebook-shot/storyShot.types.d.ts.map +0 -1
  64. package/dist/bin/forgebook-shot/storyShot.types.js.map +0 -1
  65. package/dist/src/browserTests.d.ts.map +0 -1
  66. package/dist/src/browserTests.js.map +0 -1
  67. package/dist/src/checkStories.d.ts.map +0 -1
  68. package/dist/src/checkStories.js.map +0 -1
  69. package/dist/src/overlays.d.ts.map +0 -1
  70. package/dist/src/overlays.js.map +0 -1
  71. package/dist/src/playwrightConfig.d.ts.map +0 -1
  72. package/dist/src/playwrightConfig.js.map +0 -1
  73. package/dist/src/serverTests.d.ts.map +0 -1
  74. package/dist/src/serverTests.js.map +0 -1
  75. package/dist/src/shotStories.d.ts.map +0 -1
  76. package/dist/src/shotStories.js.map +0 -1
  77. package/dist/src/storyCheck.d.ts.map +0 -1
  78. package/dist/src/storyCheck.js.map +0 -1
  79. package/dist/src/storyControls.d.ts.map +0 -1
  80. package/dist/src/storyControls.js.map +0 -1
  81. package/dist/src/storyFiles.d.ts.map +0 -1
  82. package/dist/src/storyFiles.js.map +0 -1
  83. package/dist/src/storyPages.d.ts.map +0 -1
  84. package/dist/src/storyPages.js.map +0 -1
  85. package/dist/src/testViteConfig.d.ts.map +0 -1
  86. package/dist/src/testViteConfig.js.map +0 -1
  87. package/dist/src/touchWindow.d.ts.map +0 -1
  88. package/dist/src/touchWindow.js.map +0 -1
  89. package/dist/src/visualBrowser.d.ts.map +0 -1
  90. 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.
@@ -1,3 +1,2 @@
1
1
  #!/usr/bin/env node
2
2
  export {};
3
- //# sourceMappingURL=forgebook-heights.d.mts.map
@@ -121,4 +121,3 @@ await defineScript({
121
121
  }
122
122
  }
123
123
  }).run();
124
- //# sourceMappingURL=forgebook-heights.mjs.map
@@ -81,4 +81,3 @@ export type MeasureResult = {
81
81
  */
82
82
  export declare const measureStories: (request: MeasureRequest) => Promise<MeasureResult>;
83
83
  export {};
84
- //# sourceMappingURL=measureHeights.d.ts.map
@@ -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(${JSON.stringify(STATUS_ATTRIBUTE)})`, undefined, {
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
@@ -24,4 +24,3 @@ export type HeightsReport = {
24
24
  /** The ones that failed, left out of the file. */
25
25
  readonly failures: readonly HeightFailure[];
26
26
  };
27
- //# sourceMappingURL=measureHeights.types.d.ts.map
@@ -4,4 +4,3 @@
4
4
  *
5
5
  * @module
6
6
  */
7
- //# sourceMappingURL=measureHeights.types.js.map
@@ -1,3 +1,2 @@
1
1
  #!/usr/bin/env node
2
2
  export {};
3
- //# sourceMappingURL=forgebook-shot.d.mts.map
@@ -107,4 +107,3 @@ await defineScript({
107
107
  return shot;
108
108
  }
109
109
  }).run();
110
- //# sourceMappingURL=forgebook-shot.mjs.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(${JSON.stringify(STATUS_ATTRIBUTE)})`, undefined, {
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
@@ -29,4 +29,3 @@ export type StoryShot = {
29
29
  /** The accessibility rules the story breaks, when asked (`--a11y`). */
30
30
  readonly violations?: readonly Violation[];
31
31
  };
32
- //# sourceMappingURL=storyShot.types.d.ts.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
@@ -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 tree = storyTree([indexedSource(options)]);
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 locations) {
77
- for (const variant of variants) {
78
- test(`${story.name} · ${variant.name}`, async ({ page }) => {
79
- const errors = watchedErrors(page);
80
- if (variant.width !== undefined)
81
- await page.setViewportSize({ width: variant.width, height: 800 });
82
- // a Live story is checked and pictured at its start: its clock held, so its image never changes;
83
- // always on its page's own colour, whatever a variant names: a backdrop is for looking
84
- const view = {
85
- ...defaults,
86
- ...variant.view,
87
- switches: { ...defaults.switches, ...variant.view.switches },
88
- backdrop: "plain"
89
- };
90
- await page.goto(heldClockAddress(frameAddress(frameUrl, story.id, view, switches)));
91
- await expect(page.locator("html")).toHaveAttribute(STATUS_ATTRIBUTE, "ready");
92
- if (screenshot) {
93
- await expect(page).toHaveScreenshot(`${story.id.replace(":", "--")}--${variant.name}.png`, {
94
- fullPage: true,
95
- animations: "disabled"
96
- });
97
- return;
98
- }
99
- const messages = (problems) => problems.map((problem) => problem.message);
100
- expect(messages(await axeProblems(page, catalogue.a11y))).toEqual([]);
101
- expect(messages(await spillProblems(page))).toEqual([]);
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
@@ -1,7 +1,9 @@
1
1
  import { catalogueSettings } from "forgebook/catalogue";
2
2
  import { isOverlayChecked } from "./overlays.js";
3
- import { axeProblems, errorProblems, overlayProblems, spillProblems, watchedErrors } from "./storyCheck.js";
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
- return result([
29
- ...(await axeProblems(page, a11y)),
30
- ...(await spillProblems(page)),
31
- // last of the page's checks: it resizes the window, which may close what the story left open
32
- ...(await overlayProblems(page, isOverlayChecked(storyPage.view), storyPage.story.height)),
33
- ...errorProblems(errors)
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;