@qoretechnologies/qlip 0.1.0-beta.20260128131929

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 (62) hide show
  1. package/README.md +131 -0
  2. package/dist/browser.d.ts +2 -0
  3. package/dist/browser.d.ts.map +1 -0
  4. package/dist/browser.js +1 -0
  5. package/dist/config/parameters.d.ts +22 -0
  6. package/dist/config/parameters.d.ts.map +1 -0
  7. package/dist/config/parameters.js +24 -0
  8. package/dist/fs/output.d.ts +42 -0
  9. package/dist/fs/output.d.ts.map +1 -0
  10. package/dist/fs/output.js +83 -0
  11. package/dist/index.d.ts +4 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +2 -0
  14. package/dist/plugin/vitestPlugin.d.ts +4 -0
  15. package/dist/plugin/vitestPlugin.d.ts.map +1 -0
  16. package/dist/plugin/vitestPlugin.js +71 -0
  17. package/dist/runtime/context.d.ts +15 -0
  18. package/dist/runtime/context.d.ts.map +1 -0
  19. package/dist/runtime/context.js +70 -0
  20. package/dist/runtime/screenshot.d.ts +10 -0
  21. package/dist/runtime/screenshot.d.ts.map +1 -0
  22. package/dist/runtime/screenshot.js +443 -0
  23. package/dist/runtime/setup.d.ts +2 -0
  24. package/dist/runtime/setup.d.ts.map +1 -0
  25. package/dist/runtime/setup.js +15 -0
  26. package/dist/stories/AnimatedBox.d.ts +4 -0
  27. package/dist/stories/AnimatedBox.d.ts.map +1 -0
  28. package/dist/stories/AnimatedBox.js +7 -0
  29. package/dist/stories/AnimatedBox.stories.d.ts +12 -0
  30. package/dist/stories/AnimatedBox.stories.d.ts.map +1 -0
  31. package/dist/stories/AnimatedBox.stories.js +21 -0
  32. package/dist/stories/Button.d.ts +16 -0
  33. package/dist/stories/Button.d.ts.map +1 -0
  34. package/dist/stories/Button.js +7 -0
  35. package/dist/stories/Button.stories.d.ts +21 -0
  36. package/dist/stories/Button.stories.d.ts.map +1 -0
  37. package/dist/stories/Button.stories.js +40 -0
  38. package/dist/stories/FailOnPurpose.stories.d.ts +12 -0
  39. package/dist/stories/FailOnPurpose.stories.d.ts.map +1 -0
  40. package/dist/stories/FailOnPurpose.stories.js +32 -0
  41. package/dist/stories/Header.d.ts +13 -0
  42. package/dist/stories/Header.d.ts.map +1 -0
  43. package/dist/stories/Header.js +4 -0
  44. package/dist/stories/Header.stories.d.ts +19 -0
  45. package/dist/stories/Header.stories.d.ts.map +1 -0
  46. package/dist/stories/Header.stories.js +26 -0
  47. package/dist/stories/IgnoreMask.d.ts +3 -0
  48. package/dist/stories/IgnoreMask.d.ts.map +1 -0
  49. package/dist/stories/IgnoreMask.js +5 -0
  50. package/dist/stories/IgnoreMask.stories.d.ts +14 -0
  51. package/dist/stories/IgnoreMask.stories.d.ts.map +1 -0
  52. package/dist/stories/IgnoreMask.stories.js +12 -0
  53. package/dist/stories/Page.d.ts +4 -0
  54. package/dist/stories/Page.d.ts.map +1 -0
  55. package/dist/stories/Page.js +8 -0
  56. package/dist/stories/Page.stories.d.ts +13 -0
  57. package/dist/stories/Page.stories.d.ts.map +1 -0
  58. package/dist/stories/Page.stories.js +27 -0
  59. package/dist/types.d.ts +88 -0
  60. package/dist/types.d.ts.map +1 -0
  61. package/dist/types.js +1 -0
  62. package/package.json +64 -0
package/README.md ADDED
@@ -0,0 +1,131 @@
1
+ # Qlip
2
+
3
+ Qlip is a Storybook screenshot capture tool that plugs into the Storybook Vitest addon. Add one Vitest plugin and every story automatically gets a screenshot after the test finishes. Use `screenshot()` inside a play function to capture intermediate states.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install --save-dev @qoretechnologies/qlip
9
+ ```
10
+
11
+ ## Vitest setup
12
+
13
+ ```ts
14
+ import { defineConfig } from 'vitest/config';
15
+ import path from 'node:path';
16
+ import { fileURLToPath } from 'node:url';
17
+ import { storybookTest } from '@storybook/addon-vitest/vitest-plugin';
18
+ import { playwright } from '@vitest/browser-playwright';
19
+ import { qlipVitestPlugin } from '@qoretechnologies/qlip';
20
+
21
+ const dirname =
22
+ typeof __dirname !== 'undefined'
23
+ ? __dirname
24
+ : path.dirname(fileURLToPath(import.meta.url));
25
+
26
+ export default defineConfig({
27
+ test: {
28
+ projects: [
29
+ {
30
+ extends: true,
31
+ plugins: [
32
+ storybookTest({
33
+ configDir: path.join(dirname, '.storybook'),
34
+ }),
35
+ qlipVitestPlugin(),
36
+ ],
37
+ test: {
38
+ name: 'storybook',
39
+ browser: {
40
+ enabled: true,
41
+ headless: true,
42
+ provider: playwright({}),
43
+ instances: [{ browser: 'chromium' }],
44
+ },
45
+ setupFiles: ['.storybook/vitest.setup.ts'],
46
+ },
47
+ },
48
+ ],
49
+ },
50
+ });
51
+ ```
52
+
53
+ ## Manual screenshots inside play
54
+
55
+ Use the existing **"Logged In"** story as a real-world example:
56
+
57
+ ```ts
58
+ import { screenshot } from '@qoretechnologies/qlip';
59
+
60
+ export const LoggedIn = {
61
+ play: async (ctx) => {
62
+ // interactions and assertions...
63
+ await screenshot(ctx, 'after-login');
64
+ },
65
+ };
66
+ ```
67
+
68
+ ## Parameters
69
+
70
+ Configure screenshots per story via `parameters.qlip`:
71
+
72
+ ```ts
73
+ export const LoggedIn = {
74
+ parameters: {
75
+ qlip: {
76
+ skip: false,
77
+ viewport: { width: 1280, height: 720 },
78
+ disableAnimations: true,
79
+ pauseAnimationsAtEnd: false,
80
+ captureOnError: false,
81
+ waitForIdleMs: 300,
82
+ maxWaitForIdleMs: 2000,
83
+ ignoreElements: ['.toast', '[data-qlip-ignore]'],
84
+ },
85
+ },
86
+ };
87
+ ```
88
+
89
+ Options precedence:
90
+
91
+ 1. Explicit options passed to `screenshot()`
92
+ 2. `parameters.qlip`
93
+ 3. Plugin defaults
94
+
95
+ ## Plugin options
96
+
97
+ ```ts
98
+ qlipVitestPlugin({
99
+ outputDir: './qlip/screenshots',
100
+ viewport: { width: 1280, height: 720 },
101
+ disableAnimations: false,
102
+ pauseAnimationsAtEnd: false,
103
+ captureOnError: false,
104
+ waitForIdleMs: 300,
105
+ maxWaitForIdleMs: 2000,
106
+ ignoreElements: [],
107
+ });
108
+ ```
109
+
110
+ `waitForIdleMs` waits for DOM mutations to settle before taking a screenshot. This is especially useful for animation libraries like `react-spring` that update inline styles via `requestAnimationFrame`, which bypasses CSS-based animation disabling. Increase it if you still catch mid-transition frames, or lower it for faster runs when your UI is static. `maxWaitForIdleMs` caps the wait so stories with continuously changing UI still complete.
111
+
112
+ `ignoreElements` lets you provide CSS selectors to mask before capture. Qlip draws solid overlays on matching elements so layout stays intact while visual diffs ignore those regions.
113
+
114
+ ## Output layout
115
+
116
+ ```
117
+ ./qlip/screenshots/
118
+ <buildId>/
119
+ stories/
120
+ auto/<storyTitle>--<storyName>.png
121
+ manual/<storyTitle>--<storyName>--<screenshotName>.png
122
+ error/<storyTitle>--<storyName>--qlip-auto-error-capture.png
123
+ manifest.json
124
+ ```
125
+
126
+ - `buildId` defaults to `YYYYMMDD-HHmmss`
127
+ - `parameters.qlip.skip === true` disables all captures for that story
128
+
129
+ ## Manifest
130
+
131
+ Each run writes `manifest.json` inside the build folder with tool metadata, defaults, stats, and per-screenshot entries (auto + manual).
@@ -0,0 +1,2 @@
1
+ export { screenshot } from './runtime/screenshot.js';
2
+ //# sourceMappingURL=browser.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../src/browser.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC"}
@@ -0,0 +1 @@
1
+ export { screenshot } from './runtime/screenshot.js';
@@ -0,0 +1,22 @@
1
+ import { QlipParameters, QlipResolvedDefaults, QlipScreenshotOptions } from '../types.js';
2
+ export interface QlipResolvedOptions {
3
+ skip: boolean;
4
+ viewport: {
5
+ width: number;
6
+ height: number;
7
+ };
8
+ disableAnimations: boolean;
9
+ pauseAnimationsAtEnd: boolean;
10
+ waitForIdleMs: number;
11
+ maxWaitForIdleMs: number;
12
+ ignoreElements: string[];
13
+ auto: boolean;
14
+ manual: boolean;
15
+ error: boolean;
16
+ }
17
+ export declare const resolveQlipOptions: ({ defaults, story, override, }: {
18
+ defaults: QlipResolvedDefaults;
19
+ story?: QlipParameters;
20
+ override?: QlipScreenshotOptions;
21
+ }) => QlipResolvedOptions;
22
+ //# sourceMappingURL=parameters.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parameters.d.ts","sourceRoot":"","sources":["../../src/config/parameters.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,oBAAoB,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAE1F,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,OAAO,CAAC;IACd,QAAQ,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAC5C,iBAAiB,EAAE,OAAO,CAAC;IAC3B,oBAAoB,EAAE,OAAO,CAAC;IAC9B,aAAa,EAAE,MAAM,CAAC;IACtB,gBAAgB,EAAE,MAAM,CAAC;IACzB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,IAAI,EAAE,OAAO,CAAC;IACd,MAAM,EAAE,OAAO,CAAC;IAChB,KAAK,EAAE,OAAO,CAAC;CAChB;AAED,eAAO,MAAM,kBAAkB,GAAI,gCAIhC;IACD,QAAQ,EAAE,oBAAoB,CAAC;IAC/B,KAAK,CAAC,EAAE,cAAc,CAAC;IACvB,QAAQ,CAAC,EAAE,qBAAqB,CAAC;CAClC,KAAG,mBA4BH,CAAC"}
@@ -0,0 +1,24 @@
1
+ export const resolveQlipOptions = ({ defaults, story, override, }) => {
2
+ return {
3
+ skip: override?.skip ?? story?.skip ?? defaults.skip,
4
+ viewport: override?.viewport ?? story?.viewport ?? defaults.viewport,
5
+ disableAnimations: override?.disableAnimations ??
6
+ story?.disableAnimations ??
7
+ defaults.disableAnimations,
8
+ pauseAnimationsAtEnd: override?.pauseAnimationsAtEnd ??
9
+ story?.pauseAnimationsAtEnd ??
10
+ defaults.pauseAnimationsAtEnd,
11
+ waitForIdleMs: override?.waitForIdleMs ??
12
+ story?.waitForIdleMs ??
13
+ defaults.waitForIdleMs,
14
+ maxWaitForIdleMs: override?.maxWaitForIdleMs ??
15
+ story?.maxWaitForIdleMs ??
16
+ defaults.maxWaitForIdleMs,
17
+ ignoreElements: override?.ignoreElements ??
18
+ story?.ignoreElements ??
19
+ defaults.ignoreElements,
20
+ auto: override?.auto ?? story?.auto ?? defaults.auto,
21
+ manual: override?.manual ?? story?.manual ?? defaults.manual,
22
+ error: override?.error ?? story?.error ?? defaults.error,
23
+ };
24
+ };
@@ -0,0 +1,42 @@
1
+ import { QlipManifest, QlipResolvedDefaults } from '../types.js';
2
+ export declare const TOOL_NAME = "qlip";
3
+ export declare const DEFAULT_OUTPUT_DIR = "./qlip/screenshots";
4
+ export declare const AUTO_ERROR_SCREENSHOT_BASE = "qlip-auto-error-capture";
5
+ export declare const DEFAULT_VIEWPORT: {
6
+ width: number;
7
+ height: number;
8
+ };
9
+ export declare const generateBuildId: (date?: Date) => string;
10
+ export declare const sanitizeSegment: (value: string) => string;
11
+ export declare const joinPath: (...parts: string[]) => string;
12
+ export declare const buildAutoScreenshotPath: ({ buildDir, storyId, storyTitle, storyName, }: {
13
+ buildDir: string;
14
+ storyId: string;
15
+ storyTitle?: string;
16
+ storyName?: string;
17
+ }) => {
18
+ relativePath: string;
19
+ absolutePath: string;
20
+ };
21
+ export declare const buildManualScreenshotPath: ({ buildDir, storyId, storyTitle, storyName, screenshotName, }: {
22
+ buildDir: string;
23
+ storyId: string;
24
+ storyTitle?: string;
25
+ storyName?: string;
26
+ screenshotName: string;
27
+ }) => {
28
+ safeName: string;
29
+ relativePath: string;
30
+ absolutePath: string;
31
+ };
32
+ export declare const createManifest: ({ buildId, outputDir, defaults, tool, createdAt, }: {
33
+ buildId: string;
34
+ outputDir: string;
35
+ defaults: QlipResolvedDefaults;
36
+ tool: {
37
+ name: string;
38
+ version: string;
39
+ };
40
+ createdAt?: Date;
41
+ }) => QlipManifest;
42
+ //# sourceMappingURL=output.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"output.d.ts","sourceRoot":"","sources":["../../src/fs/output.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAEjE,eAAO,MAAM,SAAS,SAAS,CAAC;AAEhC,eAAO,MAAM,kBAAkB,uBAAuB,CAAC;AACvD,eAAO,MAAM,0BAA0B,4BAA4B,CAAC;AACpE,eAAO,MAAM,gBAAgB;;;CAA+B,CAAC;AAE7D,eAAO,MAAM,eAAe,GAAI,OAAM,IAAiB,KAAG,MASzD,CAAC;AAEF,eAAO,MAAM,eAAe,GAAI,OAAO,MAAM,KAAG,MACD,CAAC;AAEhD,eAAO,MAAM,QAAQ,GAAI,GAAG,OAAO,MAAM,EAAE,KAAG,MAgB7C,CAAC;AA+BF,eAAO,MAAM,uBAAuB,GAAI,+CAKrC;IACD,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;;;CAOA,CAAC;AAEF,eAAO,MAAM,yBAAyB,GAAI,+DAMvC;IACD,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;CACxB;;;;CAYA,CAAC;AAEF,eAAO,MAAM,cAAc,GAAI,oDAM5B;IACD,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,oBAAoB,CAAC;IAC/B,IAAI,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IACxC,SAAS,CAAC,EAAE,IAAI,CAAC;CAClB,KAAG,YAeF,CAAC"}
@@ -0,0 +1,83 @@
1
+ export const TOOL_NAME = 'qlip';
2
+ export const DEFAULT_OUTPUT_DIR = './qlip/screenshots';
3
+ export const AUTO_ERROR_SCREENSHOT_BASE = 'qlip-auto-error-capture';
4
+ export const DEFAULT_VIEWPORT = { width: 1280, height: 720 };
5
+ export const generateBuildId = (date = new Date()) => {
6
+ const pad = (num) => String(num).padStart(2, '0');
7
+ const year = date.getFullYear();
8
+ const month = pad(date.getMonth() + 1);
9
+ const day = pad(date.getDate());
10
+ const hours = pad(date.getHours());
11
+ const minutes = pad(date.getMinutes());
12
+ const seconds = pad(date.getSeconds());
13
+ return `${year}${month}${day}-${hours}${minutes}${seconds}`;
14
+ };
15
+ export const sanitizeSegment = (value) => value.trim().replace(/[^a-zA-Z0-9_.-]/g, '_');
16
+ export const joinPath = (...parts) => {
17
+ const normalized = parts
18
+ .filter((part) => part.length > 0)
19
+ .map((part) => part.replace(/\\/g, '/'));
20
+ if (normalized.length === 0) {
21
+ return '';
22
+ }
23
+ let result = normalized[0];
24
+ for (let index = 1; index < normalized.length; index += 1) {
25
+ const part = normalized[index].replace(/^\/+/, '');
26
+ result = result.replace(/\/+$/, '');
27
+ result = `${result}/${part}`;
28
+ }
29
+ return result;
30
+ };
31
+ const splitStoryId = (storyId) => {
32
+ const [titlePart, namePart] = storyId.split('--');
33
+ return {
34
+ titlePart: titlePart || storyId,
35
+ namePart: namePart || undefined,
36
+ };
37
+ };
38
+ const buildStoryBaseName = ({ storyTitle, storyName, storyId, }) => {
39
+ const { titlePart, namePart } = splitStoryId(storyId);
40
+ const resolvedTitle = storyTitle ?? titlePart;
41
+ const resolvedName = storyName && storyName !== 'storyFn' ? storyName : namePart ?? storyId;
42
+ const safeTitle = sanitizeSegment(resolvedTitle);
43
+ const safeName = sanitizeSegment(resolvedName);
44
+ return `${safeTitle}--${safeName}`;
45
+ };
46
+ const buildKindDir = (kind) => joinPath('stories', kind);
47
+ export const buildAutoScreenshotPath = ({ buildDir, storyId, storyTitle, storyName, }) => {
48
+ const baseName = buildStoryBaseName({ storyTitle, storyName, storyId });
49
+ const relativePath = `${buildKindDir('auto')}/${baseName}.png`;
50
+ return {
51
+ relativePath,
52
+ absolutePath: joinPath(buildDir, relativePath),
53
+ };
54
+ };
55
+ export const buildManualScreenshotPath = ({ buildDir, storyId, storyTitle, storyName, screenshotName, }) => {
56
+ const baseName = buildStoryBaseName({ storyTitle, storyName, storyId });
57
+ const safeName = sanitizeSegment(screenshotName);
58
+ const kind = safeName.startsWith(AUTO_ERROR_SCREENSHOT_BASE)
59
+ ? 'error'
60
+ : 'manual';
61
+ const relativePath = `${buildKindDir(kind)}/${baseName}--${safeName}.png`;
62
+ return {
63
+ safeName,
64
+ relativePath,
65
+ absolutePath: joinPath(buildDir, relativePath),
66
+ };
67
+ };
68
+ export const createManifest = ({ buildId, outputDir, defaults, tool, createdAt = new Date(), }) => ({
69
+ tool,
70
+ buildId,
71
+ createdAt: createdAt.toISOString(),
72
+ outputDir,
73
+ defaults,
74
+ stats: {
75
+ storiesTotal: 0,
76
+ capturedAuto: 0,
77
+ capturedManual: 0,
78
+ skipped: 0,
79
+ failed: 0,
80
+ durationMs: 0,
81
+ },
82
+ entries: [],
83
+ });
@@ -0,0 +1,4 @@
1
+ export { qlipVitestPlugin } from './plugin/vitestPlugin.js';
2
+ export { screenshot } from './runtime/screenshot.js';
3
+ export type { QlipManifest, QlipManifestEntry, QlipParameters, QlipPluginOptions, QlipResolvedDefaults, QlipScreenshotOptions, QlipViewport, } from './types.js';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAC5D,OAAO,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AACrD,YAAY,EACV,YAAY,EACZ,iBAAiB,EACjB,cAAc,EACd,iBAAiB,EACjB,oBAAoB,EACpB,qBAAqB,EACrB,YAAY,GACb,MAAM,YAAY,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { qlipVitestPlugin } from './plugin/vitestPlugin.js';
2
+ export { screenshot } from './runtime/screenshot.js';
@@ -0,0 +1,4 @@
1
+ import type { Plugin } from 'vitest/config';
2
+ import { QlipPluginOptions } from '../types.js';
3
+ export declare const qlipVitestPlugin: (options?: QlipPluginOptions) => Plugin;
4
+ //# sourceMappingURL=vitestPlugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vitestPlugin.d.ts","sourceRoot":"","sources":["../../src/plugin/vitestPlugin.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAQ5C,OAAO,EAAE,iBAAiB,EAAqB,MAAM,aAAa,CAAC;AAInE,eAAO,MAAM,gBAAgB,GAC3B,UAAS,iBAAsB,KAC9B,MAsEF,CAAC"}
@@ -0,0 +1,71 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import { existsSync } from 'node:fs';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import pkg from '../../package.json' with { type: 'json' };
6
+ import { DEFAULT_OUTPUT_DIR, DEFAULT_VIEWPORT, TOOL_NAME, generateBuildId, } from '../fs/output.js';
7
+ const normalizePath = (value) => value.replace(/\\/g, '/');
8
+ export const qlipVitestPlugin = (options = {}) => {
9
+ const buildId = options.buildId ?? generateBuildId();
10
+ let runtimeConfig = null;
11
+ return {
12
+ name: 'qlip-vitest-plugin',
13
+ enforce: 'pre',
14
+ config: (config) => {
15
+ const root = config.root ?? process.cwd();
16
+ const outputDir = options.outputDir ?? DEFAULT_OUTPUT_DIR;
17
+ const resolvedOutputDir = normalizePath(path.resolve(root, outputDir));
18
+ const buildDir = normalizePath(path.join(resolvedOutputDir, buildId));
19
+ const version = pkg.version ?? '0.0.0';
20
+ runtimeConfig = {
21
+ buildId,
22
+ outputDir: resolvedOutputDir,
23
+ buildDir,
24
+ defaults: {
25
+ outputDir,
26
+ viewport: options.viewport ?? DEFAULT_VIEWPORT,
27
+ skip: false,
28
+ disableAnimations: options.disableAnimations ?? false,
29
+ pauseAnimationsAtEnd: options.pauseAnimationsAtEnd ?? false,
30
+ captureOnError: options.captureOnError ?? false,
31
+ waitForIdleMs: options.waitForIdleMs ?? 300,
32
+ maxWaitForIdleMs: options.maxWaitForIdleMs ?? 2000,
33
+ ignoreElements: options.ignoreElements ?? [],
34
+ auto: options.auto ?? true,
35
+ manual: options.manual ?? true,
36
+ error: options.error ?? true,
37
+ },
38
+ tool: { name: TOOL_NAME, version },
39
+ };
40
+ const setupTs = fileURLToPath(new URL('../runtime/setup.ts', import.meta.url));
41
+ const setupJs = fileURLToPath(new URL('../runtime/setup.js', import.meta.url));
42
+ const setupFile = existsSync(setupTs) ? setupTs : setupJs;
43
+ const existingSetupFiles = config.test?.setupFiles;
44
+ const setupFiles = new Set();
45
+ if (typeof existingSetupFiles === 'string') {
46
+ setupFiles.add(existingSetupFiles);
47
+ }
48
+ else if (Array.isArray(existingSetupFiles)) {
49
+ existingSetupFiles.forEach((file) => setupFiles.add(file));
50
+ }
51
+ setupFiles.add(setupFile);
52
+ return {
53
+ define: {
54
+ __QLIP_CONFIG__: JSON.stringify(runtimeConfig),
55
+ },
56
+ test: {
57
+ setupFiles: Array.from(setupFiles),
58
+ },
59
+ };
60
+ },
61
+ async configResolved() {
62
+ if (!runtimeConfig) {
63
+ return;
64
+ }
65
+ await fs.mkdir(runtimeConfig.buildDir, { recursive: true });
66
+ await fs.mkdir(path.join(runtimeConfig.buildDir, 'stories'), {
67
+ recursive: true,
68
+ });
69
+ },
70
+ };
71
+ };
@@ -0,0 +1,15 @@
1
+ import { QlipManifest, QlipManifestEntry, QlipRuntimeConfig } from '../types.js';
2
+ export interface QlipRuntimeState {
3
+ config: QlipRuntimeConfig;
4
+ manifest: QlipManifest;
5
+ counters: Map<string, number>;
6
+ startedAt: number;
7
+ warnedMissingStorybook: boolean;
8
+ }
9
+ export declare const getRuntimeState: () => QlipRuntimeState | null;
10
+ export declare const initRuntimeState: () => QlipRuntimeState | null;
11
+ export declare const nextStepName: (state: QlipRuntimeState, storyId: string) => string;
12
+ export declare const pushEntry: (state: QlipRuntimeState, entry: QlipManifestEntry) => void;
13
+ export declare const updateStats: (state: QlipRuntimeState, updates: Partial<QlipManifest["stats"]>) => void;
14
+ export declare const markStorybookWarning: (state: QlipRuntimeState) => void;
15
+ //# sourceMappingURL=context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../src/runtime/context.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,YAAY,EACZ,iBAAiB,EACjB,iBAAiB,EAClB,MAAM,aAAa,CAAC;AAGrB,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,iBAAiB,CAAC;IAC1B,QAAQ,EAAE,YAAY,CAAC;IACvB,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,sBAAsB,EAAE,OAAO,CAAC;CACjC;AAyBD,eAAO,MAAM,eAAe,QAAO,gBAAgB,GAAG,IAKrD,CAAC;AAEF,eAAO,MAAM,gBAAgB,QAAO,gBAAgB,GAAG,IA+BtD,CAAC;AAEF,eAAO,MAAM,YAAY,GACvB,OAAO,gBAAgB,EACvB,SAAS,MAAM,KACd,MAKF,CAAC;AAEF,eAAO,MAAM,SAAS,GACpB,OAAO,gBAAgB,EACvB,OAAO,iBAAiB,SAGzB,CAAC;AAEF,eAAO,MAAM,WAAW,GAAI,OAAO,gBAAgB,EAAE,SAAS,OAAO,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC,SAM3F,CAAC;AAEF,eAAO,MAAM,oBAAoB,GAAI,OAAO,gBAAgB,SAE3D,CAAC"}
@@ -0,0 +1,70 @@
1
+ import { createManifest } from '../fs/output.js';
2
+ const GLOBAL_KEY = '__QLIP_RUNTIME__';
3
+ const readConfig = () => {
4
+ const globalValue = globalThis
5
+ .__QLIP_CONFIG__;
6
+ if (globalValue) {
7
+ return globalValue;
8
+ }
9
+ if (typeof __QLIP_CONFIG__ === 'undefined') {
10
+ return undefined;
11
+ }
12
+ if (typeof __QLIP_CONFIG__ === 'string') {
13
+ try {
14
+ return JSON.parse(__QLIP_CONFIG__);
15
+ }
16
+ catch {
17
+ return undefined;
18
+ }
19
+ }
20
+ return __QLIP_CONFIG__;
21
+ };
22
+ export const getRuntimeState = () => {
23
+ const globalState = globalThis;
24
+ return globalState[GLOBAL_KEY] ?? null;
25
+ };
26
+ export const initRuntimeState = () => {
27
+ const existing = getRuntimeState();
28
+ if (existing) {
29
+ return existing;
30
+ }
31
+ const runtimeConfig = readConfig();
32
+ if (!runtimeConfig) {
33
+ return null;
34
+ }
35
+ const manifest = createManifest({
36
+ buildId: runtimeConfig.buildId,
37
+ outputDir: runtimeConfig.buildDir,
38
+ defaults: runtimeConfig.defaults,
39
+ tool: runtimeConfig.tool,
40
+ });
41
+ const runtime = {
42
+ config: runtimeConfig,
43
+ manifest,
44
+ counters: new Map(),
45
+ startedAt: Date.now(),
46
+ warnedMissingStorybook: false,
47
+ };
48
+ const globalState = globalThis;
49
+ globalState[GLOBAL_KEY] = runtime;
50
+ return runtime;
51
+ };
52
+ export const nextStepName = (state, storyId) => {
53
+ const current = state.counters.get(storyId) ?? 0;
54
+ const next = current + 1;
55
+ state.counters.set(storyId, next);
56
+ return `step-${next}`;
57
+ };
58
+ export const pushEntry = (state, entry) => {
59
+ state.manifest.entries.push(entry);
60
+ };
61
+ export const updateStats = (state, updates) => {
62
+ state.manifest.stats = {
63
+ ...state.manifest.stats,
64
+ ...updates,
65
+ };
66
+ state.manifest.stats.durationMs = Date.now() - state.startedAt;
67
+ };
68
+ export const markStorybookWarning = (state) => {
69
+ state.warnedMissingStorybook = true;
70
+ };
@@ -0,0 +1,10 @@
1
+ import { QlipScreenshotOptions, QlipStoryContext } from '../types.js';
2
+ import type { TestContext } from 'vitest';
3
+ export declare const screenshot: (ctx: QlipStoryContext, nameOrOptions?: string | QlipScreenshotOptions, options?: QlipScreenshotOptions) => Promise<void>;
4
+ type QlipTestContext = TestContext & {
5
+ story?: QlipStoryContext;
6
+ };
7
+ export declare const captureAutoScreenshot: (ctx: QlipTestContext) => Promise<void>;
8
+ export declare const captureErrorScreenshot: (ctx: QlipTestContext) => Promise<void>;
9
+ export {};
10
+ //# sourceMappingURL=screenshot.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"screenshot.d.ts","sourceRoot":"","sources":["../../src/runtime/screenshot.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,qBAAqB,EACrB,gBAAgB,EAEjB,MAAM,aAAa,CAAC;AAgBrB,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAC;AAyc1C,eAAO,MAAM,UAAU,GACrB,KAAK,gBAAgB,EACrB,gBAAgB,MAAM,GAAG,qBAAqB,EAC9C,UAAU,qBAAqB,kBA0BhC,CAAC;AAEF,KAAK,eAAe,GAAG,WAAW,GAAG;IAAE,KAAK,CAAC,EAAE,gBAAgB,CAAA;CAAE,CAAC;AAElE,eAAO,MAAM,qBAAqB,GAAU,KAAK,eAAe,kBAoC/D,CAAC;AAEF,eAAO,MAAM,sBAAsB,GAAU,KAAK,eAAe,kBA2ChE,CAAC"}