@fohte/storybook-addon 0.1.6 → 0.1.8

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 CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  Runs three checks right after each story renders, and fails the story's test when one trips:
6
6
 
7
- - **overflow-check** — flags elements whose content is clipped by their container (scrollWidth > clientWidth), which usually means a silent layout bug.
7
+ - **overflow-check** — flags elements whose content is clipped by their container (scrollWidth > clientWidth), or the story's own root overflowing the viewport itself, which usually means a silent layout bug.
8
8
  - **external-resource-check** — flags stories that load a non-same-origin resource (font, image, stylesheet), which makes VRT captures non-deterministic.
9
9
  - **unhandled-api-request-check** — flags stories that hit an API endpoint with no MSW handler, which would otherwise render with MSW's error response instead of failing.
10
10
 
@@ -42,6 +42,10 @@ This wires up `overflow-check` and `external-resource-check` — no further setu
42
42
 
43
43
  ### overflow-check
44
44
 
45
+ Besides scanning canvasElement's descendants, this check also compares `document.documentElement.scrollWidth` against `window.innerWidth`, catching canvasElement itself (or anything above it) overflowing the viewport — which the descendant scan can never see under `layout: 'centered'`, since a flex item's automatic minimum size keeps a fixed-width story from self-overflowing there. This is on by default alongside the descendant scan; `parameters.overflowCheck.disable` turns off both.
46
+
47
+ `ignoreSelectors`/`globalIgnoreSelectors` below only filter the descendant scan, not the viewport check — the viewport check has no selector-level exemption and can only be turned off entirely via `disable`.
48
+
45
49
  To exempt a selector across every story (e.g. a component whose oversized hit target never visibly clips anything), set `parameters.overflowCheck.globalIgnoreSelectors` once in `.storybook/preview.ts`. It's additive with a story's own `parameters.overflowCheck.ignoreSelectors` — both apply, since they're separate keys and Storybook deep-merges parameter objects by key (only arrays at the same key replace wholesale, so a story-level `ignoreSelectors` can't drop the global list):
46
50
 
47
51
  ```ts
@@ -83,6 +83,18 @@ function findOverflows(root, ignoreSelectors) {
83
83
  }
84
84
  return groupByAncestor(entries);
85
85
  }
86
+ // `findOverflows` never sees canvasElement itself overflowing. Under
87
+ // `layout: 'centered'`, canvasElement is a flex item that never shrinks
88
+ // below its own content width, so the clip only shows up on
89
+ // `document.documentElement`, above canvasElement.
90
+ function findViewportOverflow() {
91
+ const overflowPx = document.documentElement.scrollWidth - window.innerWidth;
92
+ if (overflowPx <= 0)
93
+ return [];
94
+ return [
95
+ `document.documentElement.scrollWidth=${String(document.documentElement.scrollWidth)} is ${String(overflowPx)}px wider than window.innerWidth=${String(window.innerWidth)}. This usually means the story's own wrapper uses a fixed width (e.g. Tailwind's \`w-*\`) instead of a max-width (\`max-w-*\`).`,
96
+ ];
97
+ }
86
98
  function overflowCheckParameters(storyParameters) {
87
99
  if (typeof storyParameters !== 'object' || storyParameters === null) {
88
100
  return undefined;
@@ -152,6 +164,7 @@ export const overflowCheck = {
152
164
  ...globalIgnoreSelectorsOf(params),
153
165
  ...ignoreSelectorsOf(params),
154
166
  ]), 'Story has element(s) overflowing their container (clipped and invisible)');
167
+ throwIfNotEmpty(findViewportOverflow(), 'Story overflows the viewport itself (not just an inner element)');
155
168
  },
156
169
  };
157
170
  //# sourceMappingURL=overflow-check.js.map
@@ -71,18 +71,23 @@ const PENDING_FETCH_TIMEOUT_MS = 2000;
71
71
  // every other check.
72
72
  export async function waitForPendingApiRequests() {
73
73
  const { pendingFetches } = globalState();
74
- const deadline = Date.now() + PENDING_FETCH_TIMEOUT_MS;
74
+ // performance.now(), not Date.now(): a consuming app's test setup may call
75
+ // vi.setSystemTime() without vi.useFakeTimers() (e.g. to pin screenshots to
76
+ // a fixed date), which freezes Date.now() while leaving setTimeout on the
77
+ // real clock — Date.now() < deadline would then stay true forever and this
78
+ // loop would never exit on its own.
79
+ const deadline = performance.now() + PENDING_FETCH_TIMEOUT_MS;
75
80
  // A tracked fetch's own resolution can synchronously trigger another
76
81
  // tracked fetch (e.g. fetch(user).then(() => fetch(user.posts))) — re-check
77
82
  // pendingFetches after each round instead of racing a single snapshot of
78
83
  // it, so a same-story follow-up fetch is also waited for, within the same
79
84
  // overall deadline.
80
- while (pendingFetches.size > 0 && Date.now() < deadline) {
85
+ while (pendingFetches.size > 0 && performance.now() < deadline) {
81
86
  // Each iteration re-reads pendingFetches, which the previous iteration's
82
87
  // wait may have grown, so this can't be hoisted out of the loop.
83
88
  await Promise.race([
84
89
  Promise.all(pendingFetches),
85
- new Promise((resolve) => setTimeout(resolve, deadline - Date.now())),
90
+ new Promise((resolve) => setTimeout(resolve, deadline - performance.now())),
86
91
  ]);
87
92
  }
88
93
  if (pendingFetches.size > 0) {
package/package.json CHANGED
@@ -56,25 +56,25 @@
56
56
  "@eslint/eslintrc": "3.3.6",
57
57
  "@fohte/eslint-config": "0.4.1",
58
58
  "@ninoseki/eslint-plugin-neverthrow": "0.2.0",
59
- "@storybook/addon-vitest": "10.5.8",
60
- "@storybook/html-vite": "10.5.8",
59
+ "@storybook/addon-vitest": "10.5.10",
60
+ "@storybook/html-vite": "10.5.10",
61
61
  "@storycap-testrun/browser": "2.1.1",
62
62
  "@tsconfig/node-lts": "24.0.1",
63
63
  "@tsconfig/strictest": "2.0.8",
64
64
  "@types/jsdom": "30.0.0",
65
65
  "@types/node": "24.13.3",
66
- "@vitest/browser-playwright": "4.1.10",
66
+ "@vitest/browser-playwright": "4.1.11",
67
67
  "concurrently": "10.0.5",
68
68
  "eslint": "10.6.0",
69
69
  "jsdom": "30.0.1",
70
70
  "playwright": "1.62.1",
71
71
  "prettier": "3.9.6",
72
72
  "rimraf": "6.1.3",
73
- "storybook": "10.5.8",
73
+ "storybook": "10.5.10",
74
74
  "typescript": "6.0.3",
75
- "vitest": "4.1.10"
75
+ "vitest": "4.1.11"
76
76
  },
77
- "version": "0.1.6",
77
+ "version": "0.1.8",
78
78
  "scripts": {
79
79
  "clean": "rimraf lib tsconfig.tsbuildinfo",
80
80
  "prebuild": "pnpm run clean",