@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.
Files changed (70) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +29 -2
  3. package/dist/bin/forgebook-heights/forgebook-heights.d.mts +3 -0
  4. package/dist/bin/forgebook-heights/forgebook-heights.d.mts.map +1 -0
  5. package/dist/bin/forgebook-heights/forgebook-heights.mjs +124 -0
  6. package/dist/bin/forgebook-heights/forgebook-heights.mjs.map +1 -0
  7. package/dist/bin/forgebook-heights/measureHeights.d.ts +84 -0
  8. package/dist/bin/forgebook-heights/measureHeights.d.ts.map +1 -0
  9. package/dist/bin/forgebook-heights/measureHeights.js +180 -0
  10. package/dist/bin/forgebook-heights/measureHeights.js.map +1 -0
  11. package/dist/bin/forgebook-heights/measureHeights.types.d.ts +27 -0
  12. package/dist/bin/forgebook-heights/measureHeights.types.d.ts.map +1 -0
  13. package/dist/bin/forgebook-heights/measureHeights.types.js +7 -0
  14. package/dist/bin/forgebook-heights/measureHeights.types.js.map +1 -0
  15. package/dist/bin/forgebook-shot/forgebook-shot.d.mts +3 -0
  16. package/dist/bin/forgebook-shot/forgebook-shot.d.mts.map +1 -0
  17. package/dist/bin/forgebook-shot/forgebook-shot.mjs +110 -0
  18. package/dist/bin/forgebook-shot/forgebook-shot.mjs.map +1 -0
  19. package/dist/bin/forgebook-shot/storyShot.d.ts +34 -0
  20. package/dist/bin/forgebook-shot/storyShot.d.ts.map +1 -0
  21. package/dist/bin/forgebook-shot/storyShot.js +83 -0
  22. package/dist/bin/forgebook-shot/storyShot.js.map +1 -0
  23. package/dist/bin/forgebook-shot/storyShot.types.d.ts +32 -0
  24. package/dist/bin/forgebook-shot/storyShot.types.d.ts.map +1 -0
  25. package/dist/bin/forgebook-shot/storyShot.types.js +1 -0
  26. package/dist/bin/forgebook-shot/storyShot.types.js.map +1 -0
  27. package/dist/src/browserTests.d.ts +55 -0
  28. package/dist/src/browserTests.d.ts.map +1 -0
  29. package/dist/src/browserTests.js +137 -0
  30. package/dist/src/browserTests.js.map +1 -0
  31. package/dist/src/fonts/Inter-OFL.txt +92 -0
  32. package/dist/src/fonts/Inter-latin-ext.woff2 +0 -0
  33. package/dist/src/fonts/Inter-latin.woff2 +0 -0
  34. package/dist/src/fonts/JetBrainsMono-OFL.txt +93 -0
  35. package/dist/src/fonts/JetBrainsMono-latin-ext.woff2 +0 -0
  36. package/dist/src/fonts/JetBrainsMono-latin.woff2 +0 -0
  37. package/dist/src/fonts/fonts.css +34 -0
  38. package/dist/src/overlays.d.ts +87 -0
  39. package/dist/src/overlays.d.ts.map +1 -0
  40. package/dist/src/overlays.js +103 -0
  41. package/dist/src/overlays.js.map +1 -0
  42. package/dist/src/playwrightConfig.d.ts +26 -0
  43. package/dist/src/playwrightConfig.d.ts.map +1 -0
  44. package/dist/src/playwrightConfig.js +62 -0
  45. package/dist/src/playwrightConfig.js.map +1 -0
  46. package/dist/src/serverTests.d.ts +44 -0
  47. package/dist/src/serverTests.d.ts.map +1 -0
  48. package/dist/src/serverTests.js +101 -0
  49. package/dist/src/serverTests.js.map +1 -0
  50. package/dist/src/spills.d.ts +28 -0
  51. package/dist/src/spills.d.ts.map +1 -0
  52. package/dist/src/spills.js +53 -0
  53. package/dist/src/spills.js.map +1 -0
  54. package/dist/src/storyControls.d.ts +24 -0
  55. package/dist/src/storyControls.d.ts.map +1 -0
  56. package/dist/src/storyControls.js +29 -0
  57. package/dist/src/storyControls.js.map +1 -0
  58. package/dist/src/storyFiles.d.ts +34 -0
  59. package/dist/src/storyFiles.d.ts.map +1 -0
  60. package/dist/src/storyFiles.js +66 -0
  61. package/dist/src/storyFiles.js.map +1 -0
  62. package/dist/src/testViteConfig.d.ts +21 -0
  63. package/dist/src/testViteConfig.d.ts.map +1 -0
  64. package/dist/src/testViteConfig.js +26 -0
  65. package/dist/src/testViteConfig.js.map +1 -0
  66. package/dist/src/visualBrowser.d.ts +18 -0
  67. package/dist/src/visualBrowser.d.ts.map +1 -0
  68. package/dist/src/visualBrowser.js +78 -0
  69. package/dist/src/visualBrowser.js.map +1 -0
  70. package/package.json +84 -3
@@ -0,0 +1,87 @@
1
+ /**
2
+ * The overlays a story leaves open past its frame: what a frame as tall as the story would cut or push to the other
3
+ * side, so the story needs `parameters.height` (D-469).
4
+ *
5
+ * @remarks
6
+ * The catalogue sizes a story's frame to its document's body, and the window of the frame is the overlays' window;
7
+ * the tests open the frame in a window taller than that, where an overlay opened near the story's foot still fits. So
8
+ * after the play an overlay is measured against the frame the shell would give it: the body's height, or the height
9
+ * the story declares when that is more. Without knowing
10
+ * the library (D-67), an overlay is any element with `position: fixed` that shows (a box, not hidden, not
11
+ * transparent) and whose box ends more than a pixel below the body. A fixed element placed against the window rather
12
+ * than against what opened it (a full-window layer, a toast region pinned to the window's foot, a drawer) is told
13
+ * apart by covering the window's whole height, or by growing the window: it moves or grows with it, an anchored
14
+ * overlay stays where it is.
15
+ *
16
+ * Limits: an overlay that is not `position: fixed` (an absolute popover inside the story) is not seen; an overlay the
17
+ * library shrinks to the room there is (a list that scrolls in the frame) fits and is not flagged, though it may be
18
+ * cramped; only the state after the play is measured, not what opens and closes during it; and an anchored overlay
19
+ * the library fits to the test's window (a list as tall as the room below it) grows with the window and reads as
20
+ * pinned to it, which takes a list taller than the test's window.
21
+ *
22
+ * @module
23
+ */
24
+ import type { Page } from "@playwright/test";
25
+ import type { ViewState } from "forgebook/view-state";
26
+ /** A shown fixed element's box, in CSS px from the window's top. */
27
+ export type FixedBox = {
28
+ /** The element, as a short CSS path (`div.menu`), with its order among the elements of that path (`#0`). */
29
+ readonly key: string;
30
+ /** Its top edge. */
31
+ readonly top: number;
32
+ /** Its bottom edge. */
33
+ readonly bottom: number;
34
+ };
35
+ /** The frame's height as the shell gives it, and every shown fixed element. */
36
+ export type FixedLayout = {
37
+ /** The height the shell gives the story's frame, in CSS px: the body's, or the declared one when more. */
38
+ readonly frameHeight: number;
39
+ /** The window's height, in CSS px. */
40
+ readonly windowHeight: number;
41
+ /** Every shown `position: fixed` element, in document order. */
42
+ readonly boxes: readonly FixedBox[];
43
+ };
44
+ /** An overlay that reaches past the frame. */
45
+ export type OverlayPastFrame = {
46
+ /** The element, as `FixedBox.key` names it. */
47
+ readonly element: string;
48
+ /** How far its box ends below the frame's foot, in whole CSS px. */
49
+ readonly by: number;
50
+ /** The frame's height, in CSS px. */
51
+ readonly frameHeight: number;
52
+ };
53
+ /** How much taller the window is made to tell the elements pinned to it, in CSS px. */
54
+ export declare const WINDOW_GROWTH = 120;
55
+ /**
56
+ * The frame's height and every shown fixed element, measured in the story's page (`page.evaluate`), so it takes no
57
+ * import.
58
+ */
59
+ export declare const fixedLayout: () => FixedLayout;
60
+ /**
61
+ * The overlays past the frame, from the layout after the play and the same layout in a window grown by `growth`.
62
+ *
63
+ * @param layout - the layout after the play.
64
+ * @param grown - the layout once the window is `growth` px taller; none counts every box as anchored.
65
+ * @param growth - how much taller the grown window is, in CSS px.
66
+ * @returns the boxes that end past the frame and did not follow the window's foot. Pure.
67
+ */
68
+ export declare const overlaysPastFrame: (layout: FixedLayout, grown: FixedLayout | undefined, growth: number) => OverlayPastFrame[];
69
+ /**
70
+ * Whether a view is checked for overlays left past the frame: not with the structure sheets only, where a library's
71
+ * surfaces are bare by its own rules (sizes are look, so an overlay has no cap and no width there), so no declared
72
+ * height could hold them.
73
+ *
74
+ * @param view - the switches a test variant sets over the defaults.
75
+ * @returns `false` for a structure-only view, `true` otherwise.
76
+ */
77
+ export declare const isOverlayChecked: (view: Partial<ViewState>) => boolean;
78
+ /**
79
+ * The overlays a story's page leaves open past its frame, measured once its play is over.
80
+ *
81
+ * @param page - the story's frame page, ready.
82
+ * @param sceneHeight - the height the story declares (`parameters.height`), in CSS px; none by default.
83
+ * @returns one entry per overlay that ends below the frame's foot; the window is grown and given back only when a
84
+ * fixed element ends there.
85
+ */
86
+ export declare const findOverlaysPastFrame: (page: Page, sceneHeight?: number) => Promise<OverlayPastFrame[]>;
87
+ //# sourceMappingURL=overlays.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"overlays.d.ts","sourceRoot":"","sources":["../../src/overlays.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAC7C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAEtD,oEAAoE;AACpE,MAAM,MAAM,QAAQ,GAAG;IACrB,4GAA4G;IAC5G,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,oBAAoB;IACpB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,uBAAuB;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB,CAAC;AAEF,+EAA+E;AAC/E,MAAM,MAAM,WAAW,GAAG;IACxB,0GAA0G;IAC1G,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,sCAAsC;IACtC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,gEAAgE;IAChE,QAAQ,CAAC,KAAK,EAAE,SAAS,QAAQ,EAAE,CAAC;CACrC,CAAC;AAEF,8CAA8C;AAC9C,MAAM,MAAM,gBAAgB,GAAG;IAC7B,+CAA+C;IAC/C,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,oEAAoE;IACpE,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,qCAAqC;IACrC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B,CAAC;AAEF,uFAAuF;AACvF,eAAO,MAAM,aAAa,MAAM,CAAC;AAKjC;;;GAGG;AACH,eAAO,MAAM,WAAW,QAAO,WAsB9B,CAAC;AAUF;;;;;;;GAOG;AACH,eAAO,MAAM,iBAAiB,WACpB,WAAW,SACZ,WAAW,GAAG,SAAS,UACtB,MAAM,KACb,gBAAgB,EAclB,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,gBAAgB,SAAU,OAAO,CAAC,SAAS,CAAC,KAAG,OAAsC,CAAC;AAMnG;;;;;;;GAOG;AACH,eAAO,MAAM,qBAAqB,SAAgB,IAAI,2BAAoB,OAAO,CAAC,gBAAgB,EAAE,CAenG,CAAC"}
@@ -0,0 +1,103 @@
1
+ /** How much taller the window is made to tell the elements pinned to it, in CSS px. */
2
+ export const WINDOW_GROWTH = 120;
3
+ /** How far past the frame's foot a box may end without counting, in CSS px: rounding. */
4
+ const TOLERANCE = 1;
5
+ /**
6
+ * The frame's height and every shown fixed element, measured in the story's page (`page.evaluate`), so it takes no
7
+ * import.
8
+ */
9
+ export const fixedLayout = () => {
10
+ const pathOf = (element) => {
11
+ const classes = [...element.classList].slice(0, 2).join(".");
12
+ return classes === "" ? element.tagName.toLowerCase() : `${element.tagName.toLowerCase()}.${classes}`;
13
+ };
14
+ const counts = new Map();
15
+ const boxes = [];
16
+ for (const element of document.body.querySelectorAll("*")) {
17
+ const style = getComputedStyle(element);
18
+ if (style.position !== "fixed" || style.visibility === "hidden" || style.opacity === "0")
19
+ continue;
20
+ const box = element.getBoundingClientRect();
21
+ if (box.width === 0 || box.height === 0)
22
+ continue;
23
+ const path = pathOf(element);
24
+ const count = counts.get(path) ?? 0;
25
+ counts.set(path, count + 1);
26
+ boxes.push({ key: `${path}#${count}`, top: box.top, bottom: box.bottom });
27
+ }
28
+ return {
29
+ frameHeight: Math.ceil(document.body.getBoundingClientRect().height),
30
+ windowHeight: window.innerHeight,
31
+ boxes
32
+ };
33
+ };
34
+ /**
35
+ * Whether a box ends below the frame's foot.
36
+ *
37
+ * @param box - the box.
38
+ * @param frameHeight - the frame's height.
39
+ */
40
+ const endsPast = (box, frameHeight) => box.bottom > frameHeight + TOLERANCE;
41
+ /**
42
+ * The overlays past the frame, from the layout after the play and the same layout in a window grown by `growth`.
43
+ *
44
+ * @param layout - the layout after the play.
45
+ * @param grown - the layout once the window is `growth` px taller; none counts every box as anchored.
46
+ * @param growth - how much taller the grown window is, in CSS px.
47
+ * @returns the boxes that end past the frame and did not follow the window's foot. Pure.
48
+ */
49
+ export const overlaysPastFrame = (layout, grown, growth) => {
50
+ const grownBoxes = new Map((grown?.boxes ?? []).map((box) => [box.key, box]));
51
+ return layout.boxes.flatMap((box) => {
52
+ if (!endsPast(box, layout.frameHeight))
53
+ return [];
54
+ const after = grownBoxes.get(box.key);
55
+ // a layer over the whole window, or a box that moved or grew with the window's foot: placed against the window,
56
+ // not against what opened it (a layer whose overlay closed on the resize is gone from the grown layout)
57
+ const isWindowLayer = box.top <= TOLERANCE && box.bottom >= layout.windowHeight - TOLERANCE;
58
+ const isPinnedToWindow = after !== undefined && after.bottom - box.bottom >= growth / 2;
59
+ if (isWindowLayer || isPinnedToWindow)
60
+ return [];
61
+ return [
62
+ { element: box.key, by: Math.round(box.bottom - layout.frameHeight), frameHeight: layout.frameHeight }
63
+ ];
64
+ });
65
+ };
66
+ /**
67
+ * Whether a view is checked for overlays left past the frame: not with the structure sheets only, where a library's
68
+ * surfaces are bare by its own rules (sizes are look, so an overlay has no cap and no width there), so no declared
69
+ * height could hold them.
70
+ *
71
+ * @param view - the switches a test variant sets over the defaults.
72
+ * @returns `false` for a structure-only view, `true` otherwise.
73
+ */
74
+ export const isOverlayChecked = (view) => view.sheets !== "structure";
75
+ /** Waits for two frames of the page, so a resize has been laid out and anything that follows it has moved. */
76
+ const twoFrames = () => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve())));
77
+ /**
78
+ * The overlays a story's page leaves open past its frame, measured once its play is over.
79
+ *
80
+ * @param page - the story's frame page, ready.
81
+ * @param sceneHeight - the height the story declares (`parameters.height`), in CSS px; none by default.
82
+ * @returns one entry per overlay that ends below the frame's foot; the window is grown and given back only when a
83
+ * fixed element ends there.
84
+ */
85
+ export const findOverlaysPastFrame = async (page, sceneHeight = 0) => {
86
+ const measure = async () => {
87
+ const measured = await page.evaluate(fixedLayout);
88
+ return { ...measured, frameHeight: Math.max(measured.frameHeight, sceneHeight) };
89
+ };
90
+ const layout = await measure();
91
+ if (!layout.boxes.some((box) => endsPast(box, layout.frameHeight)))
92
+ return [];
93
+ const size = page.viewportSize();
94
+ if (size === null)
95
+ return overlaysPastFrame(layout, undefined, WINDOW_GROWTH);
96
+ await page.setViewportSize({ width: size.width, height: size.height + WINDOW_GROWTH });
97
+ await page.evaluate(twoFrames);
98
+ const grown = await measure();
99
+ await page.setViewportSize(size);
100
+ await page.evaluate(twoFrames);
101
+ return overlaysPastFrame(layout, grown, WINDOW_GROWTH);
102
+ };
103
+ //# sourceMappingURL=overlays.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"overlays.js","sourceRoot":"","sources":["../../src/overlays.ts"],"names":[],"mappings":"AAwDA,uFAAuF;AACvF,MAAM,CAAC,MAAM,aAAa,GAAG,GAAG,CAAC;AAEjC,yFAAyF;AACzF,MAAM,SAAS,GAAG,CAAC,CAAC;AAEpB;;;GAGG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,GAAgB,EAAE;IAC3C,MAAM,MAAM,GAAG,CAAC,OAAgB,EAAU,EAAE;QAC1C,MAAM,OAAO,GAAG,CAAC,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAC7D,OAAO,OAAO,KAAK,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,WAAW,EAAE,IAAI,OAAO,EAAE,CAAC;IACxG,CAAC,CAAC;IACF,MAAM,MAAM,GAAG,IAAI,GAAG,EAAkB,CAAC;IACzC,MAAM,KAAK,GAAe,EAAE,CAAC;IAC7B,KAAK,MAAM,OAAO,IAAI,QAAQ,CAAC,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,EAAE,CAAC;QAC1D,MAAM,KAAK,GAAG,gBAAgB,CAAC,OAAO,CAAC,CAAC;QACxC,IAAI,KAAK,CAAC,QAAQ,KAAK,OAAO,IAAI,KAAK,CAAC,UAAU,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,KAAK,GAAG;YAAE,SAAS;QACnG,MAAM,GAAG,GAAG,OAAO,CAAC,qBAAqB,EAAE,CAAC;QAC5C,IAAI,GAAG,CAAC,KAAK,KAAK,CAAC,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC;YAAE,SAAS;QAClD,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC;QAC7B,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACpC,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC;QAC5B,KAAK,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,GAAG,IAAI,IAAI,KAAK,EAAE,EAAE,GAAG,EAAE,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC;IAC5E,CAAC;IACD,OAAO;QACL,WAAW,EAAE,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,qBAAqB,EAAE,CAAC,MAAM,CAAC;QACpE,YAAY,EAAE,MAAM,CAAC,WAAW;QAChC,KAAK;KACN,CAAC;AACJ,CAAC,CAAC;AAEF;;;;;GAKG;AACH,MAAM,QAAQ,GAAG,CAAC,GAAa,EAAE,WAAmB,EAAW,EAAE,CAAC,GAAG,CAAC,MAAM,GAAG,WAAW,GAAG,SAAS,CAAC;AAEvG;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAC/B,MAAmB,EACnB,KAA8B,EAC9B,MAAc,EACM,EAAE;IACtB,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC,CAAC,KAAK,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC;IAC9E,OAAO,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,EAAE;QAClC,IAAI,CAAC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC,WAAW,CAAC;YAAE,OAAO,EAAE,CAAC;QAClD,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACtC,gHAAgH;QAChH,wGAAwG;QACxG,MAAM,aAAa,GAAG,GAAG,CAAC,GAAG,IAAI,SAAS,IAAI,GAAG,CAAC,MAAM,IAAI,MAAM,CAAC,YAAY,GAAG,SAAS,CAAC;QAC5F,MAAM,gBAAgB,GAAG,KAAK,KAAK,SAAS,IAAI,KAAK,CAAC,MAAM,GAAG,GAAG,CAAC,MAAM,IAAI,MAAM,GAAG,CAAC,CAAC;QACxF,IAAI,aAAa,IAAI,gBAAgB;YAAE,OAAO,EAAE,CAAC;QACjD,OAAO;YACL,EAAE,OAAO,EAAE,GAAG,CAAC,GAAG,EAAE,EAAE,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,GAAG,MAAM,CAAC,WAAW,CAAC,EAAE,WAAW,EAAE,MAAM,CAAC,WAAW,EAAE;SACvG,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,IAAwB,EAAW,EAAE,CAAC,IAAI,CAAC,MAAM,KAAK,WAAW,CAAC;AAEnG,8GAA8G;AAC9G,MAAM,SAAS,GAAG,GAAkB,EAAE,CACpC,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,qBAAqB,CAAC,GAAG,EAAE,CAAC,qBAAqB,CAAC,GAAG,EAAE,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC;AAEhG;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,KAAK,EAAE,IAAU,EAAE,WAAW,GAAG,CAAC,EAA+B,EAAE;IACtG,MAAM,OAAO,GAAG,KAAK,IAA0B,EAAE;QAC/C,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;QAClD,OAAO,EAAE,GAAG,QAAQ,EAAE,WAAW,EAAE,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAC,EAAE,CAAC;IACnF,CAAC,CAAC;IACF,MAAM,MAAM,GAAG,MAAM,OAAO,EAAE,CAAC;IAC/B,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC,WAAW,CAAC,CAAC;QAAE,OAAO,EAAE,CAAC;IAC9E,MAAM,IAAI,GAAG,IAAI,CAAC,YAAY,EAAE,CAAC;IACjC,IAAI,IAAI,KAAK,IAAI;QAAE,OAAO,iBAAiB,CAAC,MAAM,EAAE,SAAS,EAAE,aAAa,CAAC,CAAC;IAC9E,MAAM,IAAI,CAAC,eAAe,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,GAAG,aAAa,EAAE,CAAC,CAAC;IACvF,MAAM,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;IAC/B,MAAM,KAAK,GAAG,MAAM,OAAO,EAAE,CAAC;IAC9B,MAAM,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC;IACjC,MAAM,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;IAC/B,OAAO,iBAAiB,CAAC,MAAM,EAAE,KAAK,EAAE,aAAa,CAAC,CAAC;AACzD,CAAC,CAAC"}
@@ -0,0 +1,26 @@
1
+ import type { PlaywrightTestConfig } from "@playwright/test";
2
+ /** Where a package's story tests live and the ports they use. */
3
+ export type StoryTestServer = {
4
+ /** The folder of the specs, the frame page, its Vite config and the visual tests' global setup. */
5
+ readonly dir: string;
6
+ /** The port the test instance of the frame page listens on. */
7
+ readonly port: number;
8
+ /** The port the container's browser listens on, for the visual tests. */
9
+ readonly browserPort: number;
10
+ /** How many tests run at once; every core when left out or not a positive number. */
11
+ readonly workers?: number;
12
+ };
13
+ /**
14
+ * The browser tests' configuration: `stories.spec.ts` in the host's Chromium.
15
+ *
16
+ * @param server - the package's folder and ports.
17
+ */
18
+ export declare const storyTestConfig: (server: StoryTestServer) => PlaywrightTestConfig;
19
+ /**
20
+ * The visual tests' configuration: `visual.spec.ts` against the reference images in `__screenshots__`, in the
21
+ * Playwright container's browser (started by `visualBrowser.ts` on a developer's machine, already there in CI).
22
+ *
23
+ * @param server - the package's folder and ports.
24
+ */
25
+ export declare const visualTestConfig: (server: StoryTestServer) => PlaywrightTestConfig;
26
+ //# sourceMappingURL=playwrightConfig.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"playwrightConfig.d.ts","sourceRoot":"","sources":["../../src/playwrightConfig.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAE7D,iEAAiE;AACjE,MAAM,MAAM,eAAe,GAAG;IAC5B,mGAAmG;IACnG,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,+DAA+D;IAC/D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,yEAAyE;IACzE,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,qFAAqF;IACrF,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B,CAAC;AAaF;;;;GAIG;AACH,eAAO,MAAM,eAAe,WAAY,eAAe,KAAG,oBAQxD,CAAC;AAEH;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,WAAY,eAAe,KAAG,oBAkB1D,CAAC"}
@@ -0,0 +1,62 @@
1
+ /**
2
+ * The two Playwright configurations of a package's story tests: the browser tests on the host's Chromium, and the
3
+ * visual tests in the Playwright container's browser, both on the package's own test instance of the frame page.
4
+ *
5
+ * @remarks
6
+ * Published (`forgebook/testing/playwright`; D-167). The package keeps a folder with its specs
7
+ * (`stories.spec.ts`, `visual.spec.ts`), its frame page (`frame.html`), the Vite config that serves it
8
+ * (`vite.config.ts`, from `storyTestViteConfig`) and the visual tests' global setup (`visualBrowser.ts`, from
9
+ * `startVisualBrowser`); each package names its own ports, so two packages' tests run side by side. In CI the visual
10
+ * tests already run inside the Playwright image (`CI` set), so they use its browser directly.
11
+ *
12
+ * @module
13
+ */
14
+ import { availableParallelism } from "node:os";
15
+ /** The workers to use. */
16
+ const workersOf = ({ workers }) => workers !== undefined && workers > 0 ? workers : availableParallelism();
17
+ /** The test instance of the frame page, started by Playwright and stopped after the run. */
18
+ const webServer = ({ dir, port }) => ({
19
+ command: `pnpm exec vite --config ${dir}/vite.config.ts --host 127.0.0.1 --port ${port} --strictPort`,
20
+ url: `http://127.0.0.1:${port}/frame.html`,
21
+ reuseExistingServer: false
22
+ });
23
+ /**
24
+ * The browser tests' configuration: `stories.spec.ts` in the host's Chromium.
25
+ *
26
+ * @param server - the package's folder and ports.
27
+ */
28
+ export const storyTestConfig = (server) => ({
29
+ testDir: server.dir,
30
+ testMatch: "stories.spec.ts",
31
+ fullyParallel: true,
32
+ workers: workersOf(server),
33
+ reporter: [["list"]],
34
+ webServer: webServer(server),
35
+ use: { browserName: "chromium", baseURL: `http://127.0.0.1:${server.port}/` }
36
+ });
37
+ /**
38
+ * The visual tests' configuration: `visual.spec.ts` against the reference images in `__screenshots__`, in the
39
+ * Playwright container's browser (started by `visualBrowser.ts` on a developer's machine, already there in CI).
40
+ *
41
+ * @param server - the package's folder and ports.
42
+ */
43
+ export const visualTestConfig = (server) => {
44
+ const isInContainer = process.env["CI"] !== undefined;
45
+ return {
46
+ testDir: server.dir,
47
+ testMatch: "visual.spec.ts",
48
+ snapshotPathTemplate: "{testDir}/__screenshots__/{arg}{ext}",
49
+ fullyParallel: true,
50
+ workers: workersOf(server),
51
+ reporter: [["list"]],
52
+ webServer: webServer(server),
53
+ ...(isInContainer ? {} : { globalSetup: `${server.dir}/visualBrowser.ts` }),
54
+ use: {
55
+ browserName: "chromium",
56
+ viewport: { width: 1024, height: 768 },
57
+ baseURL: `http://${isInContainer ? "127.0.0.1" : "host.docker.internal"}:${server.port}/`,
58
+ ...(isInContainer ? {} : { connectOptions: { wsEndpoint: `ws://127.0.0.1:${server.browserPort}/` } })
59
+ }
60
+ };
61
+ };
62
+ //# sourceMappingURL=playwrightConfig.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"playwrightConfig.js","sourceRoot":"","sources":["../../src/playwrightConfig.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAC;AAgB/C,0BAA0B;AAC1B,MAAM,SAAS,GAAG,CAAC,EAAE,OAAO,EAAmB,EAAU,EAAE,CACzD,OAAO,KAAK,SAAS,IAAI,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,oBAAoB,EAAE,CAAC;AAE1E,4FAA4F;AAC5F,MAAM,SAAS,GAAG,CAAC,EAAE,GAAG,EAAE,IAAI,EAAmB,EAAkD,EAAE,CAAC,CAAC;IACrG,OAAO,EAAE,2BAA2B,GAAG,2CAA2C,IAAI,eAAe;IACrG,GAAG,EAAE,oBAAoB,IAAI,aAAa;IAC1C,mBAAmB,EAAE,KAAK;CAC3B,CAAC,CAAC;AAEH;;;;GAIG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,MAAuB,EAAwB,EAAE,CAAC,CAAC;IACjF,OAAO,EAAE,MAAM,CAAC,GAAG;IACnB,SAAS,EAAE,iBAAiB;IAC5B,aAAa,EAAE,IAAI;IACnB,OAAO,EAAE,SAAS,CAAC,MAAM,CAAC;IAC1B,QAAQ,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC;IACpB,SAAS,EAAE,SAAS,CAAC,MAAM,CAAC;IAC5B,GAAG,EAAE,EAAE,WAAW,EAAE,UAAU,EAAE,OAAO,EAAE,oBAAoB,MAAM,CAAC,IAAI,GAAG,EAAE;CAC9E,CAAC,CAAC;AAEH;;;;;GAKG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,MAAuB,EAAwB,EAAE;IAChF,MAAM,aAAa,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,SAAS,CAAC;IACtD,OAAO;QACL,OAAO,EAAE,MAAM,CAAC,GAAG;QACnB,SAAS,EAAE,gBAAgB;QAC3B,oBAAoB,EAAE,sCAAsC;QAC5D,aAAa,EAAE,IAAI;QACnB,OAAO,EAAE,SAAS,CAAC,MAAM,CAAC;QAC1B,QAAQ,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC;QACpB,SAAS,EAAE,SAAS,CAAC,MAAM,CAAC;QAC5B,GAAG,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,GAAG,MAAM,CAAC,GAAG,mBAAmB,EAAE,CAAC;QAC3E,GAAG,EAAE;YACH,WAAW,EAAE,UAAU;YACvB,QAAQ,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE;YACtC,OAAO,EAAE,UAAU,aAAa,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,sBAAsB,IAAI,MAAM,CAAC,IAAI,GAAG;YACzF,GAAG,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,EAAE,UAAU,EAAE,kBAAkB,MAAM,CAAC,WAAW,GAAG,EAAE,EAAE,CAAC;SACtG;KACF,CAAC;AACJ,CAAC,CAAC"}
@@ -0,0 +1,44 @@
1
+ /**
2
+ * One Vitest test per story: the framework renders it to a string without errors (`renderToString` of its renderer),
3
+ * so no component reads `window` or `document` while rendering (D-71), and its controls tell the truth
4
+ * (`storyControlsProblem`).
5
+ *
6
+ * @remarks
7
+ * The stories load through the source's own globs, which Vitest understands. `wrap` puts each story inside a
8
+ * provider (a partial translation catalogue, when the library has one) for a second pass, and can check its HTML. The
9
+ * source's `setup` runs first.
10
+ *
11
+ * @module
12
+ */
13
+ import type { StorySource } from "@forgebook/story-format/source";
14
+ import type { StoryRenderer } from "forgebook/renderer";
15
+ import { type LoadedDecorator } from "forgebook/story-module";
16
+ /** A second pass: each story inside a decorator, its HTML optionally checked. */
17
+ export type StoryServerPass = {
18
+ /** The pass's name, in each test's title (`partial catalogue`). */
19
+ readonly name: string;
20
+ /**
21
+ * The decorator the story is drawn in (`(story) => <Provider messages={partial}>{story()}</Provider>`), outside its
22
+ * own; none draws it alone.
23
+ */
24
+ readonly decorator: LoadedDecorator | undefined;
25
+ /** Checks the story's HTML, with Vitest's `expect` (no message key shown, for a partial catalogue); none by default. */
26
+ readonly check?: (html: string) => void;
27
+ };
28
+ /** The generated tests' options. */
29
+ export type StoryServerTestsOptions = {
30
+ /** The framework that draws the stories (`reactRenderer` of `@forgebook/react`). */
31
+ readonly renderer: StoryRenderer;
32
+ /** The catalogue's previews' modules (its presets' first), whose decorators and parameters every story gets. */
33
+ readonly previews?: readonly unknown[];
34
+ /** A second pass over every story. */
35
+ readonly wrap?: StoryServerPass;
36
+ };
37
+ /**
38
+ * Declares the tests, one `describe` per component and one test per story (and per story wrapped).
39
+ *
40
+ * @param source - the source, with its globs.
41
+ * @param options - the framework, and an optional wrapper for a second pass.
42
+ */
43
+ export declare const storyServerTests: (source: StorySource, options: StoryServerTestsOptions) => void;
44
+ //# sourceMappingURL=serverTests.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serverTests.d.ts","sourceRoot":"","sources":["../../src/serverTests.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gCAAgC,CAAC;AAGlE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAGxD,OAAO,EAAE,KAAK,eAAe,EAA0C,MAAM,wBAAwB,CAAC;AAOtG,iFAAiF;AACjF,MAAM,MAAM,eAAe,GAAG;IAC5B,mEAAmE;IACnE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,QAAQ,CAAC,SAAS,EAAE,eAAe,GAAG,SAAS,CAAC;IAChD,wHAAwH;IACxH,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CACzC,CAAC;AAEF,oCAAoC;AACpC,MAAM,MAAM,uBAAuB,GAAG;IACpC,oFAAoF;IACpF,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC;IACjC,gHAAgH;IAChH,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,OAAO,EAAE,CAAC;IACvC,sCAAsC;IACtC,QAAQ,CAAC,IAAI,CAAC,EAAE,eAAe,CAAC;CACjC,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,WAAY,WAAW,WAAW,uBAAuB,KAAG,IAmExF,CAAC"}
@@ -0,0 +1,101 @@
1
+ /**
2
+ * One Vitest test per story: the framework renders it to a string without errors (`renderToString` of its renderer),
3
+ * so no component reads `window` or `document` while rendering (D-71), and its controls tell the truth
4
+ * (`storyControlsProblem`).
5
+ *
6
+ * @remarks
7
+ * The stories load through the source's own globs, which Vitest understands. `wrap` puts each story inside a
8
+ * provider (a partial translation catalogue, when the library has one) for a second pass, and can check its HTML. The
9
+ * source's `setup` runs first.
10
+ *
11
+ * @module
12
+ */
13
+ import { storyTextOf } from "@forgebook/story-format/texts";
14
+ import { heldFrameClock } from "forgebook/frame-clock";
15
+ import { sourceFiles } from "forgebook/source-files";
16
+ import { prepareStory } from "forgebook/story-hooks";
17
+ import { mergedPreviews, readPreview, readStory } from "forgebook/story-module";
18
+ import { BASE_LANGUAGE, withStoryTexts } from "forgebook/story-texts";
19
+ import { storyTree, testedStoryLocations } from "forgebook/story-tree";
20
+ import { beforeAll, describe, expect, it } from "vitest";
21
+ import { storyControlsProblem } from "./storyControls.js";
22
+ /**
23
+ * Declares the tests, one `describe` per component and one test per story (and per story wrapped).
24
+ *
25
+ * @param source - the source, with its globs.
26
+ * @param options - the framework, and an optional wrapper for a second pass.
27
+ */
28
+ export const storyServerTests = (source, options) => {
29
+ const passes = [
30
+ { name: "alone", decorator: undefined },
31
+ ...(options.wrap === undefined ? [] : [options.wrap])
32
+ ];
33
+ const preview = mergedPreviews((options.previews ?? []).map(readPreview));
34
+ beforeAll(async () => {
35
+ await source.setup?.();
36
+ await preview.setup?.();
37
+ });
38
+ const locations = testedStoryLocations(storyTree([source]));
39
+ for (const [title, entries] of Map.groupBy(locations, (location) => location.component.title)) {
40
+ describe(title, () => {
41
+ for (const { story, component } of entries) {
42
+ it.each(passes)(`${story.name} renders on the server ($name)`, async (pass) => {
43
+ const load = sourceFiles(source).stories[component.file];
44
+ if (load === undefined)
45
+ throw new Error(`No loader for ${component.file}`);
46
+ const loaded = readStory(await load(), story.exportName, preview);
47
+ if (typeof loaded === "string")
48
+ throw new Error(loaded);
49
+ // the stories' own texts: what the catalogue shows when no language is chosen
50
+ const texts = source.texts?.[BASE_LANGUAGE] ?? {};
51
+ const args = withStoryTexts(loaded.args, storyTextOf(texts));
52
+ // a Live story renders at its start, as the frame's tests picture it (D-504)
53
+ const clock = heldFrameClock(loaded.clockStart ?? 0);
54
+ // the loaders and set-ups run as in the frame, so a story drawn from what they load renders here too
55
+ const identity = { id: story.id, name: story.name, title: component.title };
56
+ const prepared = await prepareStory(loaded, {
57
+ ...identity,
58
+ args,
59
+ text: storyTextOf(texts),
60
+ clock,
61
+ globals: {},
62
+ parameters: loaded.parameters
63
+ });
64
+ let html = "";
65
+ try {
66
+ // a render error fails the test: the server does not run error boundaries, and none may be swallowed
67
+ expect(() => {
68
+ html = options.renderer.renderToString({
69
+ identity,
70
+ story: loaded,
71
+ args,
72
+ texts,
73
+ clock,
74
+ decorators: [
75
+ ...preview.decorators,
76
+ ...(pass.decorator === undefined ? [] : [pass.decorator])
77
+ ],
78
+ globals: {},
79
+ loaded: prepared.loaded
80
+ });
81
+ }).not.toThrow();
82
+ }
83
+ finally {
84
+ await prepared.cleanup();
85
+ }
86
+ pass.check?.(html);
87
+ });
88
+ it(`${story.name} declares the controls it uses`, async () => {
89
+ const load = sourceFiles(source).stories[component.file];
90
+ if (load === undefined)
91
+ throw new Error(`No loader for ${component.file}`);
92
+ const loaded = readStory(await load(), story.exportName);
93
+ if (typeof loaded === "string")
94
+ throw new Error(loaded);
95
+ expect(storyControlsProblem(loaded)).toBeUndefined();
96
+ });
97
+ }
98
+ });
99
+ }
100
+ };
101
+ //# sourceMappingURL=serverTests.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serverTests.js","sourceRoot":"","sources":["../../src/serverTests.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,28 @@
1
+ /**
2
+ * The elements of a story that spill out of their parent: what overlaps a neighbour or runs off the frame when the
3
+ * story is narrower than it was drawn for.
4
+ *
5
+ * @remarks
6
+ * Runs in the story's page (`page.evaluate`), so it takes no import. An element spills when its box reaches past its
7
+ * parent's box by more than a pixel, sideways, and the parent shows its overflow: a parent that clips or scrolls
8
+ * (`overflow` other than `visible`) and an element taken out of the flow (`absolute`, `fixed`) are left out, as is
9
+ * anything inside a frame-level layer (`data-forgebook-allow-spill`, for a story that shows a spill on purpose).
10
+ *
11
+ * @module
12
+ */
13
+ /** One spill: the element, its parent, and by how many pixels it reaches past. */
14
+ export type Spill = {
15
+ /** The element, as a short CSS path (`div.story-settings`). */
16
+ readonly element: string;
17
+ /** Its parent, the same way. */
18
+ readonly parent: string;
19
+ /** How far it reaches past the parent, in CSS px. */
20
+ readonly by: number;
21
+ };
22
+ /**
23
+ * The spills inside a root, measured in the page.
24
+ *
25
+ * @param rootSelector - the story's root (`.forgebook-frame`).
26
+ */
27
+ export declare const findSpills: (rootSelector: string) => Spill[];
28
+ //# sourceMappingURL=spills.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spills.d.ts","sourceRoot":"","sources":["../../src/spills.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,kFAAkF;AAClF,MAAM,MAAM,KAAK,GAAG;IAClB,+DAA+D;IAC/D,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,gCAAgC;IAChC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,qDAAqD;IACrD,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,UAAU,iBAAkB,MAAM,KAAG,KAAK,EAgCtD,CAAC"}
@@ -0,0 +1,53 @@
1
+ /**
2
+ * The elements of a story that spill out of their parent: what overlaps a neighbour or runs off the frame when the
3
+ * story is narrower than it was drawn for.
4
+ *
5
+ * @remarks
6
+ * Runs in the story's page (`page.evaluate`), so it takes no import. An element spills when its box reaches past its
7
+ * parent's box by more than a pixel, sideways, and the parent shows its overflow: a parent that clips or scrolls
8
+ * (`overflow` other than `visible`) and an element taken out of the flow (`absolute`, `fixed`) are left out, as is
9
+ * anything inside a frame-level layer (`data-forgebook-allow-spill`, for a story that shows a spill on purpose).
10
+ *
11
+ * @module
12
+ */
13
+ /**
14
+ * The spills inside a root, measured in the page.
15
+ *
16
+ * @param rootSelector - the story's root (`.forgebook-frame`).
17
+ */
18
+ export const findSpills = (rootSelector) => {
19
+ const pathOf = (element) => {
20
+ const classes = [...element.classList].slice(0, 2).join(".");
21
+ return classes === "" ? element.tagName.toLowerCase() : `${element.tagName.toLowerCase()}.${classes}`;
22
+ };
23
+ // an element out of the flow, a parent that clips or scrolls, and an allowed spill are not measured
24
+ const isMeasured = (element, parent) => {
25
+ if (element.closest("[data-forgebook-allow-spill]") !== null)
26
+ return false;
27
+ const style = getComputedStyle(element);
28
+ const parentStyle = getComputedStyle(parent);
29
+ return (style.position !== "absolute" &&
30
+ style.position !== "fixed" &&
31
+ style.display !== "contents" &&
32
+ parentStyle.display !== "contents" &&
33
+ parentStyle.overflowX === "visible");
34
+ };
35
+ const overreach = (element, parent) => {
36
+ const box = element.getBoundingClientRect();
37
+ const parentBox = parent.getBoundingClientRect();
38
+ if (box.width === 0 || box.height === 0)
39
+ return 0;
40
+ return Math.max(box.right - parentBox.right, parentBox.left - box.left);
41
+ };
42
+ const root = document.querySelector(rootSelector);
43
+ if (root === null)
44
+ return [];
45
+ return [...root.querySelectorAll("*")].flatMap((element) => {
46
+ const parent = element.parentElement;
47
+ if (parent === null || !isMeasured(element, parent))
48
+ return [];
49
+ const by = overreach(element, parent);
50
+ return by > 1 ? [{ element: pathOf(element), parent: pathOf(parent), by: Math.round(by) }] : [];
51
+ });
52
+ };
53
+ //# sourceMappingURL=spills.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spills.js","sourceRoot":"","sources":["../../src/spills.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAYH;;;;GAIG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,YAAoB,EAAW,EAAE;IAC1D,MAAM,MAAM,GAAG,CAAC,OAAgB,EAAU,EAAE;QAC1C,MAAM,OAAO,GAAG,CAAC,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAC7D,OAAO,OAAO,KAAK,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,WAAW,EAAE,IAAI,OAAO,EAAE,CAAC;IACxG,CAAC,CAAC;IACF,oGAAoG;IACpG,MAAM,UAAU,GAAG,CAAC,OAAgB,EAAE,MAAe,EAAW,EAAE;QAChE,IAAI,OAAO,CAAC,OAAO,CAAC,8BAA8B,CAAC,KAAK,IAAI;YAAE,OAAO,KAAK,CAAC;QAC3E,MAAM,KAAK,GAAG,gBAAgB,CAAC,OAAO,CAAC,CAAC;QACxC,MAAM,WAAW,GAAG,gBAAgB,CAAC,MAAM,CAAC,CAAC;QAC7C,OAAO,CACL,KAAK,CAAC,QAAQ,KAAK,UAAU;YAC7B,KAAK,CAAC,QAAQ,KAAK,OAAO;YAC1B,KAAK,CAAC,OAAO,KAAK,UAAU;YAC5B,WAAW,CAAC,OAAO,KAAK,UAAU;YAClC,WAAW,CAAC,SAAS,KAAK,SAAS,CACpC,CAAC;IACJ,CAAC,CAAC;IACF,MAAM,SAAS,GAAG,CAAC,OAAgB,EAAE,MAAe,EAAU,EAAE;QAC9D,MAAM,GAAG,GAAG,OAAO,CAAC,qBAAqB,EAAE,CAAC;QAC5C,MAAM,SAAS,GAAG,MAAM,CAAC,qBAAqB,EAAE,CAAC;QACjD,IAAI,GAAG,CAAC,KAAK,KAAK,CAAC,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,CAAC,CAAC;QAClD,OAAO,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,GAAG,SAAS,CAAC,KAAK,EAAE,SAAS,CAAC,IAAI,GAAG,GAAG,CAAC,IAAI,CAAC,CAAC;IAC1E,CAAC,CAAC;IACF,MAAM,IAAI,GAAG,QAAQ,CAAC,aAAa,CAAC,YAAY,CAAC,CAAC;IAClD,IAAI,IAAI,KAAK,IAAI;QAAE,OAAO,EAAE,CAAC;IAC7B,OAAO,CAAC,GAAG,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QACzD,MAAM,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC;QACrC,IAAI,MAAM,KAAK,IAAI,IAAI,CAAC,UAAU,CAAC,OAAO,EAAE,MAAM,CAAC;YAAE,OAAO,EAAE,CAAC;QAC/D,MAAM,EAAE,GAAG,SAAS,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;QACtC,OAAO,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,EAAE,EAAE,EAAE,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IAClG,CAAC,CAAC,CAAC;AACL,CAAC,CAAC"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Whether a story's controls tell the truth: the props panel shows a control only when changing it changes the story.
3
+ *
4
+ * @remarks
5
+ * A render that ignores its args draws fixed examples, so its args reach nothing: it declares
6
+ * `parameters.controls: false`. A render ignores its args when it takes no parameter, or when its first parameter is
7
+ * named with a leading underscore, the convention for a parameter left unused: a fixed story that reads its texts
8
+ * takes the context as its second parameter (`(_args, { text }) => …`). A story that declares no controls must have a
9
+ * render that ignores its args: without a render the component takes them, and a render that reads them follows them,
10
+ * unless the only arg it follows is the one its Replay toggles (an arrival replayed on demand), which the panel does
11
+ * not drive. The parameters are read from the function's `length` and its source, so a render written `(args) => …`
12
+ * that ignores them is not caught. Pure: story in, problem out.
13
+ *
14
+ * @module
15
+ */
16
+ import type { LoadedStory } from "forgebook/story-module";
17
+ /**
18
+ * What is wrong with a story's controls, or `undefined` when nothing is.
19
+ *
20
+ * @param story - the loaded story.
21
+ * @returns why the panel would show controls that do nothing, or hide ones that work.
22
+ */
23
+ export declare const storyControlsProblem: (story: LoadedStory) => string | undefined;
24
+ //# sourceMappingURL=storyControls.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storyControls.d.ts","sourceRoot":"","sources":["../../src/storyControls.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAa1D;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,UAAW,WAAW,KAAG,MAAM,GAAG,SAYlE,CAAC"}
@@ -0,0 +1,29 @@
1
+ /** A function's source that opens on a first parameter named with a leading underscore: `(_args, …) => …`. */
2
+ const FIRST_PARAMETER_UNUSED = /^(?:async\s*)?(?:function\b[^(]*)?\(\s*_/;
3
+ /**
4
+ * Whether a render ignores its args: it takes no parameter, or names its first one as unused (`_args`).
5
+ *
6
+ * @param render - the story's render.
7
+ */
8
+ const ignoresArgs = (render) => render.length === 0 || FIRST_PARAMETER_UNUSED.test(render.toString());
9
+ /**
10
+ * What is wrong with a story's controls, or `undefined` when nothing is.
11
+ *
12
+ * @param story - the loaded story.
13
+ * @returns why the panel would show controls that do nothing, or hide ones that work.
14
+ */
15
+ export const storyControlsProblem = (story) => {
16
+ if (story.component === undefined)
17
+ return undefined;
18
+ const isFixed = story.render !== undefined && ignoresArgs(story.render);
19
+ const hasNoControls = story.controls !== "all" && story.controls.length === 0;
20
+ if (isFixed && !hasNoControls) {
21
+ return "its render ignores its args, so the panel's controls would change nothing: declare `parameters.controls: false`";
22
+ }
23
+ const isReplayDriven = story.render !== undefined && typeof story.replay === "object";
24
+ if (!isFixed && hasNoControls && !isReplayDriven) {
25
+ return "it declares `parameters.controls: false` but its args reach the story: remove it, or list the props it reads";
26
+ }
27
+ return undefined;
28
+ };
29
+ //# sourceMappingURL=storyControls.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storyControls.js","sourceRoot":"","sources":["../../src/storyControls.ts"],"names":[],"mappings":"AAiBA,8GAA8G;AAC9G,MAAM,sBAAsB,GAAG,0CAA0C,CAAC;AAE1E;;;;GAIG;AACH,MAAM,WAAW,GAAG,CAAC,MAA0C,EAAW,EAAE,CAC1E,MAAM,CAAC,MAAM,KAAK,CAAC,IAAI,sBAAsB,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC;AAExE;;;;;GAKG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,KAAkB,EAAsB,EAAE;IAC7E,IAAI,KAAK,CAAC,SAAS,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IACpD,MAAM,OAAO,GAAG,KAAK,CAAC,MAAM,KAAK,SAAS,IAAI,WAAW,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IACxE,MAAM,aAAa,GAAG,KAAK,CAAC,QAAQ,KAAK,KAAK,IAAI,KAAK,CAAC,QAAQ,CAAC,MAAM,KAAK,CAAC,CAAC;IAC9E,IAAI,OAAO,IAAI,CAAC,aAAa,EAAE,CAAC;QAC9B,OAAO,iHAAiH,CAAC;IAC3H,CAAC;IACD,MAAM,cAAc,GAAG,KAAK,CAAC,MAAM,KAAK,SAAS,IAAI,OAAO,KAAK,CAAC,MAAM,KAAK,QAAQ,CAAC;IACtF,IAAI,CAAC,OAAO,IAAI,aAAa,IAAI,CAAC,cAAc,EAAE,CAAC;QACjD,OAAO,8GAA8G,CAAC;IACxH,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC,CAAC"}