@fohte/storybook-addon 0.1.0 → 0.1.2
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
|
@@ -35,6 +35,18 @@ export default config
|
|
|
35
35
|
|
|
36
36
|
This wires up `overflow-check` and `external-resource-check` — no further setup needed.
|
|
37
37
|
|
|
38
|
+
### overflow-check
|
|
39
|
+
|
|
40
|
+
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):
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
import type { Parameters } from 'storybook/internal/types'
|
|
44
|
+
|
|
45
|
+
export const parameters: Parameters = {
|
|
46
|
+
overflowCheck: { globalIgnoreSelectors: ['[data-slot="checkbox"]'] },
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
38
50
|
### unhandled-api-request-check
|
|
39
51
|
|
|
40
52
|
This check needs to know which paths count as "API requests" (that's app-specific), and needs to be fed unhandled requests from your MSW setup. Configure it once and call `reportUnhandledApiRequest()` from `onUnhandledRequest` in `.storybook/preview.ts`:
|
package/lib/checks/check.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export type StorybookCheck = {
|
|
2
2
|
reset: () => void;
|
|
3
|
-
assert: (storyParameters?: unknown) => void;
|
|
3
|
+
assert: (storyParameters?: unknown, canvasElement?: Element) => void;
|
|
4
4
|
};
|
|
5
5
|
export declare function throwIfNotEmpty(urls: string[], message: string): void;
|
|
6
6
|
//# sourceMappingURL=check.d.ts.map
|
|
@@ -1,32 +1,4 @@
|
|
|
1
1
|
import { throwIfNotEmpty } from '#checks/check.js';
|
|
2
|
-
// The consuming app's preview.ts calls reset()/assert() around each story
|
|
3
|
-
// render (see preview.ts's beforeEach/afterEach). The story's container is a
|
|
4
|
-
// bare <div> appended to document.body before anything is mounted into it —
|
|
5
|
-
// reset() never gets `context.canvasElement` itself, so this is the only way
|
|
6
|
-
// to reach the same element. Portal-based components (dialogs, popovers,
|
|
7
|
-
// selects, etc.) append their own <div>s directly to document.body too, but
|
|
8
|
-
// only *after* the story has mounted — so the container is reliably the
|
|
9
|
-
// *first* element appended to body once a story starts, not the last. A
|
|
10
|
-
// MutationObserver set up in reset() (which runs before the story mounts)
|
|
11
|
-
// captures it.
|
|
12
|
-
let storyRoot = null;
|
|
13
|
-
let bodyObserver = null;
|
|
14
|
-
function watchStoryRoot() {
|
|
15
|
-
storyRoot = null;
|
|
16
|
-
bodyObserver?.disconnect();
|
|
17
|
-
bodyObserver = new MutationObserver((mutations) => {
|
|
18
|
-
for (const mutation of mutations) {
|
|
19
|
-
for (const node of mutation.addedNodes) {
|
|
20
|
-
if (node instanceof Element) {
|
|
21
|
-
storyRoot = node;
|
|
22
|
-
bodyObserver?.disconnect();
|
|
23
|
-
return;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
});
|
|
28
|
-
bodyObserver.observe(document.body, { childList: true });
|
|
29
|
-
}
|
|
30
2
|
function describeElement(el) {
|
|
31
3
|
const id = el.id ? `#${el.id}` : '';
|
|
32
4
|
const classAttr = el.getAttribute('class');
|
|
@@ -143,20 +115,43 @@ function ignoreSelectorsOf(overflowCheck) {
|
|
|
143
115
|
return [];
|
|
144
116
|
return ignoreSelectors.filter((s) => typeof s === 'string');
|
|
145
117
|
}
|
|
118
|
+
// A separate key from `ignoreSelectors`, set once by the consuming app in
|
|
119
|
+
// its top-level `preview.ts` `parameters` export (not via a module-level
|
|
120
|
+
// setter — a story's own `parameters.overflowCheck` and the app's global one
|
|
121
|
+
// live in different Storybook parameter scopes, which Storybook deep-merges
|
|
122
|
+
// by object key, so a story setting `ignoreSelectors` can't drop this list).
|
|
123
|
+
// This also sidesteps bundlers that resolve this addon's module twice for
|
|
124
|
+
// one consumer (once from the app's own import, once via Storybook's addon
|
|
125
|
+
// loader) — `context.parameters` comes from Storybook itself, not from
|
|
126
|
+
// either module instance, so it's unaffected either way.
|
|
127
|
+
function globalIgnoreSelectorsOf(overflowCheck) {
|
|
128
|
+
if (overflowCheck === undefined)
|
|
129
|
+
return [];
|
|
130
|
+
if (!('globalIgnoreSelectors' in overflowCheck))
|
|
131
|
+
return [];
|
|
132
|
+
const { globalIgnoreSelectors } = overflowCheck;
|
|
133
|
+
if (!Array.isArray(globalIgnoreSelectors))
|
|
134
|
+
return [];
|
|
135
|
+
return globalIgnoreSelectors.filter((s) => typeof s === 'string');
|
|
136
|
+
}
|
|
146
137
|
export const overflowCheck = {
|
|
147
|
-
reset:
|
|
148
|
-
assert: (storyParameters) => {
|
|
138
|
+
reset: () => { },
|
|
139
|
+
assert: (storyParameters, canvasElement) => {
|
|
149
140
|
const params = overflowCheckParameters(storyParameters);
|
|
150
141
|
if (isDisabled(params))
|
|
151
142
|
return;
|
|
152
|
-
if (
|
|
153
|
-
//
|
|
154
|
-
//
|
|
155
|
-
// than fail, since there is no root
|
|
156
|
-
|
|
143
|
+
if (canvasElement == null) {
|
|
144
|
+
// Real Storybook runs always pass canvasElement (it's the story's
|
|
145
|
+
// mount container); this only triggers a caller invoking assert()
|
|
146
|
+
// directly without one. Skip rather than fail, since there is no root
|
|
147
|
+
// to scan.
|
|
148
|
+
console.warn('[overflow-check] no canvasElement in story context; skipping overflow scan');
|
|
157
149
|
return;
|
|
158
150
|
}
|
|
159
|
-
throwIfNotEmpty(findOverflows(
|
|
151
|
+
throwIfNotEmpty(findOverflows(canvasElement, [
|
|
152
|
+
...globalIgnoreSelectorsOf(params),
|
|
153
|
+
...ignoreSelectorsOf(params),
|
|
154
|
+
]), 'Story has element(s) overflowing their container (clipped and invisible)');
|
|
160
155
|
},
|
|
161
156
|
};
|
|
162
157
|
//# sourceMappingURL=overflow-check.js.map
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
import type { StorybookCheck } from '#checks/check.js';
|
|
2
|
+
interface UnhandledApiRequestState {
|
|
3
|
+
pathPrefixes: string[];
|
|
4
|
+
unhandledApiRequestUrls: string[];
|
|
5
|
+
}
|
|
6
|
+
declare global {
|
|
7
|
+
var __fohteStorybookAddonUnhandledApiRequestState__: UnhandledApiRequestState | undefined;
|
|
8
|
+
}
|
|
2
9
|
export declare function configureUnhandledApiRequestCheck(options: {
|
|
3
10
|
pathPrefixes: string[];
|
|
4
11
|
}): void;
|
|
5
12
|
export declare function reportUnhandledApiRequest(url: string): boolean;
|
|
6
13
|
export declare const unhandledApiRequestCheck: StorybookCheck;
|
|
14
|
+
export {};
|
|
7
15
|
//# sourceMappingURL=unhandled-api-request-check.d.ts.map
|
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
import { throwIfNotEmpty } from '#checks/check.js';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
// by the consuming app rather than hardcoded here.
|
|
10
|
-
let pathPrefixes = [];
|
|
2
|
+
function globalState() {
|
|
3
|
+
globalThis.__fohteStorybookAddonUnhandledApiRequestState__ ??= {
|
|
4
|
+
pathPrefixes: [],
|
|
5
|
+
unhandledApiRequestUrls: [],
|
|
6
|
+
};
|
|
7
|
+
return globalThis.__fohteStorybookAddonUnhandledApiRequestState__;
|
|
8
|
+
}
|
|
11
9
|
export function configureUnhandledApiRequestCheck(options) {
|
|
12
|
-
pathPrefixes = options.pathPrefixes;
|
|
10
|
+
globalState().pathPrefixes = options.pathPrefixes;
|
|
13
11
|
}
|
|
14
12
|
// Returns whether the request was recorded, so the caller can decide whether
|
|
15
13
|
// to also print MSW's own error for it.
|
|
@@ -17,18 +15,19 @@ export function reportUnhandledApiRequest(url) {
|
|
|
17
15
|
const parsed = new URL(url);
|
|
18
16
|
if (parsed.origin !== window.location.origin)
|
|
19
17
|
return false;
|
|
20
|
-
|
|
18
|
+
const state = globalState();
|
|
19
|
+
if (!state.pathPrefixes.some((prefix) => parsed.pathname.startsWith(prefix))) {
|
|
21
20
|
return false;
|
|
22
21
|
}
|
|
23
|
-
unhandledApiRequestUrls.push(parsed.pathname);
|
|
22
|
+
state.unhandledApiRequestUrls.push(parsed.pathname);
|
|
24
23
|
return true;
|
|
25
24
|
}
|
|
26
25
|
export const unhandledApiRequestCheck = {
|
|
27
26
|
reset: () => {
|
|
28
|
-
unhandledApiRequestUrls.length = 0;
|
|
27
|
+
globalState().unhandledApiRequestUrls.length = 0;
|
|
29
28
|
},
|
|
30
29
|
assert: () => {
|
|
31
|
-
throwIfNotEmpty(unhandledApiRequestUrls, 'Story made unhandled API request(s); add an MSW handler for');
|
|
30
|
+
throwIfNotEmpty(globalState().unhandledApiRequestUrls, 'Story made unhandled API request(s); add an MSW handler for');
|
|
32
31
|
},
|
|
33
32
|
};
|
|
34
33
|
//# sourceMappingURL=unhandled-api-request-check.js.map
|
package/lib/preview.js
CHANGED
|
@@ -37,7 +37,8 @@ export const beforeEach = () => {
|
|
|
37
37
|
check.reset();
|
|
38
38
|
};
|
|
39
39
|
export const afterEach = (context) => {
|
|
40
|
-
for (const check of checks)
|
|
41
|
-
check.assert(context.parameters);
|
|
40
|
+
for (const check of checks) {
|
|
41
|
+
check.assert(context.parameters, context.canvasElement);
|
|
42
|
+
}
|
|
42
43
|
};
|
|
43
44
|
//# sourceMappingURL=preview.js.map
|