@fohte/storybook-addon 0.1.5 → 0.1.7

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
@@ -78,6 +78,8 @@ initialize({
78
78
 
79
79
  Builds a Vitest browser-mode project that runs Storybook stories through [`@storybook/addon-vitest`](https://storybook.js.org/docs/writing-tests/integrations/vitest-addon) and captures a screenshot of each with [`@storycap-testrun/browser`](https://github.com/reg-viz/storycap-testrun), patched to shorten storycap's hardcoded 500ms network-idle wait to 100ms and to fix a tiling bug in its fullPage capture (`clip` staying anchored to the top of the viewport instead of following the browser's clamped scroll position, which duplicated content and cut off the bottom of any story taller than the viewport).
80
80
 
81
+ `viewport` applies for the whole test, not just the screenshot: mount and `play()` render at it too. `@storybook/addon-vitest` would otherwise silently mount/run every story at its own fixed 1200x900 default instead.
82
+
81
83
  ```ts
82
84
  import path from 'node:path'
83
85
  import { fileURLToPath } from 'node:url'
@@ -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) {
@@ -48,6 +48,12 @@ export declare function createStorybookProject({ name, rootDir, viewport, screen
48
48
  }[];
49
49
  };
50
50
  setupFiles: string[];
51
+ provide: {
52
+ fohteStorybookAddonViewport: {
53
+ width: number;
54
+ height: number;
55
+ };
56
+ };
51
57
  maxWorkers?: number;
52
58
  sequence?: {
53
59
  groupOrder: number;
@@ -1,4 +1,6 @@
1
+ import { existsSync } from 'node:fs';
1
2
  import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
2
4
  import { storybookTest } from '@storybook/addon-vitest/vitest-plugin';
3
5
  import storycap from '@storycap-testrun/browser/vitest-plugin';
4
6
  import { playwright } from '@vitest/browser-playwright';
@@ -47,6 +49,17 @@ export function createStorybookProject({ name, rootDir, viewport, screenshotsSub
47
49
  const configDir = path.join(rootDir, '.storybook');
48
50
  const screenshotsDir = path.join(rootDir, '__screenshots__', screenshotsSubdir);
49
51
  const maxWorkers = process.env['CI'] != null ? ciMaxWorkers : undefined;
52
+ const viewportSetupFile = fileURLToPath(import.meta.resolve('#vitest-viewport-setup.js'));
53
+ // `import.meta.resolve` maps `#*.js` straight to `./lib/*.js` (this
54
+ // package's own build output) without checking the file exists — running
55
+ // against a fresh clone or a stale `lib/` (before `pnpm run build`, or
56
+ // after editing vitest-viewport-setup.ts without rebuilding) would
57
+ // otherwise leave Vitest to fail on a missing/stale setupFile with no clue
58
+ // this is why.
59
+ if (!existsSync(viewportSetupFile)) {
60
+ // eslint-disable-next-line no-restricted-syntax -- config-load-time invariant check, mirrors storycapNetworkIdle's fail-fast pattern above
61
+ throw new Error(`createStorybookProject: ${viewportSetupFile} not found. Run \`pnpm run build\` first.`);
62
+ }
50
63
  const plugins = [
51
64
  storycapNetworkIdle,
52
65
  storybookTest({
@@ -107,7 +120,17 @@ export function createStorybookProject({ name, rootDir, viewport, screenshotsSub
107
120
  headless: true,
108
121
  instances: [{ browser: 'chromium' }],
109
122
  },
110
- setupFiles,
123
+ // @storybook/addon-vitest overrides the story viewport to its own
124
+ // 1200x900 default right before mount/play() unless
125
+ // parameters.viewport/globals.viewport are set — vitest-viewport-setup.ts
126
+ // sets them via setProjectAnnotations() so mount/play() see the same
127
+ // `viewport` as the Playwright page and the screenshot above. It must
128
+ // run after the consumer's own setupFiles in case any of them also
129
+ // call setProjectAnnotations().
130
+ setupFiles: [...setupFiles, viewportSetupFile],
131
+ provide: {
132
+ fohteStorybookAddonViewport: viewport,
133
+ },
111
134
  },
112
135
  };
113
136
  }
@@ -0,0 +1,10 @@
1
+ declare module 'vitest' {
2
+ interface ProvidedContext {
3
+ fohteStorybookAddonViewport: {
4
+ width: number;
5
+ height: number;
6
+ };
7
+ }
8
+ }
9
+ export {};
10
+ //# sourceMappingURL=vitest-viewport-setup.d.ts.map
@@ -0,0 +1,50 @@
1
+ import { setProjectAnnotations } from 'storybook/preview-api';
2
+ import { getProjectAnnotations } from 'virtual:/@storybook/builder-vite/project-annotations.js';
3
+ import { beforeEach, inject } from 'vitest';
4
+ const VIEWPORT_NAME = '__fohteStorybookAddonViewport';
5
+ const { width, height } = inject('fohteStorybookAddonViewport');
6
+ // setProjectAnnotations() replaces the project annotations wholesale rather
7
+ // than merging into them, so composing in the consumer's own
8
+ // getProjectAnnotations() first is required — passing only the viewport
9
+ // override below would drop the consumer's own preview.ts
10
+ // renderer/decorators/loaders. Both are static per test file (the consumer's
11
+ // preview.ts and this project's `viewport` don't change mid-run), so this is
12
+ // computed once at setup-file top level rather than inside beforeEach below.
13
+ const projectAnnotations = getProjectAnnotations();
14
+ const viewportOverride = {
15
+ parameters: {
16
+ viewport: {
17
+ defaultViewport: VIEWPORT_NAME,
18
+ viewports: {
19
+ [VIEWPORT_NAME]: {
20
+ name: 'createStorybookProject viewport',
21
+ styles: {
22
+ width: `${String(width)}px`,
23
+ height: `${String(height)}px`,
24
+ },
25
+ },
26
+ },
27
+ },
28
+ },
29
+ initialGlobals: { viewport: { value: VIEWPORT_NAME } },
30
+ };
31
+ // @storybook/addon-vitest's own testStory() calls setViewport() right before
32
+ // mount/play(), falling back to its built-in 1200x900 default whenever
33
+ // parameters.viewport/globals.viewport aren't set — silently overriding the
34
+ // `viewport` createStorybookProject() was given for the screenshot capture
35
+ // itself. setProjectAnnotations() is the public integration point Storybook
36
+ // added for exactly this in 10.3.
37
+ //
38
+ // The call itself runs in beforeEach, not at setup-file top level:
39
+ // @storybook/addon-vitest always injects its own
40
+ // internal/setup-file-with-project-annotations alongside consumer
41
+ // setupFiles — its "already provisioned" detection only looks inside the
42
+ // consumer's `.storybook` dir, which this file isn't in. That file's
43
+ // top-level setProjectAnnotations(getProjectAnnotations()) call would run
44
+ // after this file's own top level and silently wipe this override.
45
+ // beforeEach always runs after every setupFile finishes loading, so calling
46
+ // setProjectAnnotations() here instead wins regardless of setupFiles order.
47
+ beforeEach(() => {
48
+ setProjectAnnotations([projectAnnotations, viewportOverride]);
49
+ });
50
+ //# sourceMappingURL=vitest-viewport-setup.js.map
package/package.json CHANGED
@@ -59,12 +59,12 @@
59
59
  "@storybook/addon-vitest": "10.5.8",
60
60
  "@storybook/html-vite": "10.5.8",
61
61
  "@storycap-testrun/browser": "2.1.1",
62
- "@tsconfig/node-lts": "24.0.0",
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
66
  "@vitest/browser-playwright": "4.1.10",
67
- "concurrently": "10.0.4",
67
+ "concurrently": "10.0.5",
68
68
  "eslint": "10.6.0",
69
69
  "jsdom": "30.0.1",
70
70
  "playwright": "1.62.1",
@@ -74,7 +74,7 @@
74
74
  "typescript": "6.0.3",
75
75
  "vitest": "4.1.10"
76
76
  },
77
- "version": "0.1.5",
77
+ "version": "0.1.7",
78
78
  "scripts": {
79
79
  "clean": "rimraf lib tsconfig.tsbuildinfo",
80
80
  "prebuild": "pnpm run clean",