@fohte/storybook-addon 0.1.7 → 0.1.9
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
|
|
@@ -108,6 +112,17 @@ Screenshots for a project land in `<rootDir>/__screenshots__/<screenshotsSubdir>
|
|
|
108
112
|
|
|
109
113
|
`storycapNetworkIdle` and `storycapFullPageStitch` are also exported on their own, for building a project without `createStorybookProject`. `storycapFullPageStitch` must be placed after storycap's own plugin in the `plugins` array — it works by overriding the `__storycap_takeScreenshot` command storycap registers, and Vite resolves conflicting plugin `config()` keys in plugin order, later wins.
|
|
110
114
|
|
|
115
|
+
`createStorybookProject` blocks non-`localhost` network requests in the browser by passing `BLOCK_EXTERNAL_REQUESTS_ARGS` to `playwright()`'s `launchOptions.args`, so a story can't depend on an external CDN request completing before the test's `afterEach` runs. It's a Chromium-only launch flag (matching `createStorybookProject`'s own `instances: [{ browser: 'chromium' }]`) — a `firefox`/`webkit` instance would silently get no blocking at all. If you build your `playwright()` call by hand instead of using `createStorybookProject`, spread this in yourself:
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
import { BLOCK_EXTERNAL_REQUESTS_ARGS } from '@fohte/storybook-addon/vitest-plugin'
|
|
119
|
+
import { playwright } from '@vitest/browser-playwright'
|
|
120
|
+
|
|
121
|
+
playwright({
|
|
122
|
+
launchOptions: { args: [...BLOCK_EXTERNAL_REQUESTS_ARGS] },
|
|
123
|
+
})
|
|
124
|
+
```
|
|
125
|
+
|
|
111
126
|
Your `setupFiles` entry needs an `afterEach` that calls storycap's own `screenshot()` — see [`@storycap-testrun/browser`'s "Setup Screenshot Capture"](https://github.com/reg-viz/storycap-testrun/tree/main/packages/browser#2-setup-screenshot-capture) for the exact shape. Don't call `setProjectAnnotations()` there, and don't even mention that identifier in a comment: `@storybook/addon-vitest` decides whether to inject this package's checks by a plain substring search over the setup file's source text, so its mere presence silently disables every check in the project, with no error.
|
|
112
127
|
|
|
113
128
|
### Check failures don't block the screenshot
|
package/lib/checks/check.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { assert } from 'vitest';
|
|
2
1
|
export function throwIfNotEmpty(urls, message) {
|
|
3
2
|
if (urls.length === 0)
|
|
4
3
|
return;
|
|
5
4
|
const list = urls.join('\n');
|
|
6
5
|
urls.length = 0;
|
|
7
|
-
assert
|
|
6
|
+
// eslint-disable-next-line no-restricted-syntax -- interop boundary: Storybook's afterEach (preview.ts) only reports a check as a story failure when assert() throws
|
|
7
|
+
throw new Error(`${message}:\n${list}`);
|
|
8
8
|
}
|
|
9
9
|
//# sourceMappingURL=check.js.map
|
|
@@ -13,9 +13,16 @@ function isExternalResourceUrl(url) {
|
|
|
13
13
|
return ((protocol === 'http:' || protocol === 'https:') &&
|
|
14
14
|
origin !== window.location.origin);
|
|
15
15
|
}
|
|
16
|
+
function isPerformanceResourceTiming(entry) {
|
|
17
|
+
return entry.entryType === 'resource';
|
|
18
|
+
}
|
|
16
19
|
new PerformanceObserver((list) => {
|
|
17
20
|
for (const entry of list.getEntries()) {
|
|
18
|
-
if (
|
|
21
|
+
if (!isPerformanceResourceTiming(entry))
|
|
22
|
+
continue;
|
|
23
|
+
// Entries are queued at completion, so responseEnd (not startTime) is
|
|
24
|
+
// what must be compared against resetAt.
|
|
25
|
+
if (entry.responseEnd >= resetAt && isExternalResourceUrl(entry.name)) {
|
|
19
26
|
externalResourceUrls.push(entry.name);
|
|
20
27
|
}
|
|
21
28
|
}
|
|
@@ -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
|
package/lib/vitest-plugin.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export declare const storycapNetworkIdle: {
|
|
|
3
3
|
name: string;
|
|
4
4
|
transform(code: string, id: string): string | null;
|
|
5
5
|
};
|
|
6
|
+
export declare const BLOCK_EXTERNAL_REQUESTS_ARGS: string[];
|
|
6
7
|
export interface CreateStorybookProjectOptions {
|
|
7
8
|
/** Project name, as shown by `vitest --project <name>`. */
|
|
8
9
|
name: string;
|
package/lib/vitest-plugin.js
CHANGED
|
@@ -45,6 +45,12 @@ export const storycapNetworkIdle = {
|
|
|
45
45
|
return patched;
|
|
46
46
|
},
|
|
47
47
|
};
|
|
48
|
+
// Blocks all requests except to localhost (Vitest's dev server), so a story
|
|
49
|
+
// can't depend on an external CDN request finishing before `afterEach`.
|
|
50
|
+
// Doesn't affect Playwright's own CDP connection, which uses a local pipe, not DNS.
|
|
51
|
+
export const BLOCK_EXTERNAL_REQUESTS_ARGS = [
|
|
52
|
+
'--host-resolver-rules=MAP * ~NOTFOUND, EXCLUDE localhost',
|
|
53
|
+
];
|
|
48
54
|
export function createStorybookProject({ name, rootDir, viewport, screenshotsSubdir, setupFiles, excludeTags = [], ciMaxWorkers = 8, }) {
|
|
49
55
|
const configDir = path.join(rootDir, '.storybook');
|
|
50
56
|
const screenshotsDir = path.join(rootDir, '__screenshots__', screenshotsSubdir);
|
|
@@ -116,6 +122,7 @@ export function createStorybookProject({ name, rootDir, viewport, screenshotsSub
|
|
|
116
122
|
// silently truncates every fullPage tile's clip rect below it.
|
|
117
123
|
provider: playwright({
|
|
118
124
|
contextOptions: { timezoneId: 'Asia/Tokyo', viewport },
|
|
125
|
+
launchOptions: { args: [...BLOCK_EXTERNAL_REQUESTS_ARGS] },
|
|
119
126
|
}),
|
|
120
127
|
headless: true,
|
|
121
128
|
instances: [{ browser: 'chromium' }],
|
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.
|
|
60
|
-
"@storybook/html-vite": "10.5.
|
|
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.
|
|
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.
|
|
73
|
+
"storybook": "10.5.10",
|
|
74
74
|
"typescript": "6.0.3",
|
|
75
|
-
"vitest": "4.1.
|
|
75
|
+
"vitest": "4.1.11"
|
|
76
76
|
},
|
|
77
|
-
"version": "0.1.
|
|
77
|
+
"version": "0.1.9",
|
|
78
78
|
"scripts": {
|
|
79
79
|
"clean": "rimraf lib tsconfig.tsbuildinfo",
|
|
80
80
|
"prebuild": "pnpm run clean",
|