@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
@@ -0,0 +1,443 @@
1
+ import { resolveQlipOptions } from '../config/parameters.js';
2
+ import { AUTO_ERROR_SCREENSHOT_BASE, buildAutoScreenshotPath, buildManualScreenshotPath, joinPath, sanitizeSegment, } from '../fs/output.js';
3
+ import { initRuntimeState, markStorybookWarning, nextStepName, pushEntry, updateStats, } from './context.js';
4
+ const ensureBrowserContext = async () => {
5
+ if (!globalThis.__vitest_browser__) {
6
+ throw new Error('[qlip] screenshot() can only be used in Vitest Browser Mode.');
7
+ }
8
+ const { page, commands } = await import('@vitest/browser/context');
9
+ if (!page || !commands) {
10
+ throw new Error('[qlip] Playwright page is unavailable. Ensure Storybook Vitest addon is enabled.');
11
+ }
12
+ return { page, commands };
13
+ };
14
+ const resolveStoryInfo = (ctx) => {
15
+ const storyId = ctx.id ? String(ctx.id) : '';
16
+ return {
17
+ id: storyId,
18
+ title: ctx.title,
19
+ name: ctx.name,
20
+ parameters: ctx.parameters?.qlip,
21
+ };
22
+ };
23
+ const readStoryFromStore = (storyId) => {
24
+ const globalAny = globalThis;
25
+ const store = globalAny.__STORYBOOK_PREVIEW__?.storyStore ??
26
+ globalAny.__STORYBOOK_STORY_STORE__;
27
+ const previewIndex = globalAny.__STORYBOOK_PREVIEW__?.storyIndex;
28
+ const rawIndex = store?.storyIndex?.v ??
29
+ store?.storyIndex ??
30
+ previewIndex?.v ??
31
+ previewIndex;
32
+ const entryMap = rawIndex?.entries ??
33
+ rawIndex;
34
+ const entry = entryMap?.[storyId];
35
+ if (!entry) {
36
+ return null;
37
+ }
38
+ return {
39
+ title: entry.title,
40
+ name: entry.name,
41
+ };
42
+ };
43
+ const shouldCaptureOnError = (defaults, story) => story?.captureOnError ?? defaults.captureOnError;
44
+ const applyAnimationControl = ({ disableAnimations, pauseAnimationsAtEnd, }) => {
45
+ if (!globalThis.document) {
46
+ return;
47
+ }
48
+ const doc = globalThis.document;
49
+ const id = '__qlip-animation-control';
50
+ const existing = doc.getElementById(id);
51
+ if (!disableAnimations && !pauseAnimationsAtEnd) {
52
+ if (existing) {
53
+ existing.remove();
54
+ }
55
+ return;
56
+ }
57
+ const style = existing ?? doc.createElement('style');
58
+ style.id = id;
59
+ if (disableAnimations) {
60
+ style.textContent = `
61
+ *, *::before, *::after {
62
+ animation: none !important;
63
+ transition: none !important;
64
+ scroll-behavior: auto !important;
65
+ }
66
+ `;
67
+ }
68
+ else {
69
+ style.textContent = `
70
+ *, *::before, *::after {
71
+ animation-play-state: paused !important;
72
+ transition-duration: 0s !important;
73
+ transition-delay: 0s !important;
74
+ }
75
+ `;
76
+ }
77
+ if (!existing) {
78
+ doc.head.appendChild(style);
79
+ }
80
+ if (typeof doc.getAnimations === 'function') {
81
+ const animations = doc.getAnimations();
82
+ for (const animation of animations) {
83
+ if (disableAnimations && typeof animation.finish === 'function') {
84
+ animation.finish();
85
+ }
86
+ else if (pauseAnimationsAtEnd &&
87
+ typeof animation.pause === 'function') {
88
+ animation.pause();
89
+ }
90
+ }
91
+ }
92
+ };
93
+ const waitForDomIdle = async (idleMs, maxWaitMs) => {
94
+ if (idleMs <= 0) {
95
+ return;
96
+ }
97
+ const doc = globalThis.document;
98
+ if (!doc || typeof MutationObserver === 'undefined') {
99
+ await new Promise((resolve) => setTimeout(resolve, idleMs));
100
+ return;
101
+ }
102
+ await new Promise((resolve) => {
103
+ const start = Date.now();
104
+ const resolvedMaxWait = Math.max(idleMs, maxWaitMs);
105
+ let lastChange = Date.now();
106
+ let done = false;
107
+ const finish = () => {
108
+ if (done) {
109
+ return;
110
+ }
111
+ done = true;
112
+ observer.disconnect();
113
+ clearInterval(checkInterval);
114
+ resolve();
115
+ };
116
+ const observer = new MutationObserver(() => {
117
+ lastChange = Date.now();
118
+ });
119
+ observer.observe(doc.documentElement, {
120
+ attributes: true,
121
+ childList: true,
122
+ subtree: true,
123
+ characterData: true,
124
+ });
125
+ const checkInterval = setInterval(() => {
126
+ const now = Date.now();
127
+ if (now - lastChange >= idleMs || now - start >= resolvedMaxWait) {
128
+ finish();
129
+ }
130
+ }, Math.min(50, idleMs));
131
+ });
132
+ };
133
+ const applyIgnoreMasks = (selectors) => {
134
+ if (!globalThis.document || selectors.length === 0) {
135
+ return () => undefined;
136
+ }
137
+ const doc = globalThis.document;
138
+ const masks = [];
139
+ for (const selector of selectors) {
140
+ const nodes = Array.from(doc.querySelectorAll(selector));
141
+ for (const node of nodes) {
142
+ const rect = node.getBoundingClientRect();
143
+ if (rect.width <= 0 || rect.height <= 0) {
144
+ continue;
145
+ }
146
+ const mask = doc.createElement('div');
147
+ mask.setAttribute('data-qlip-mask', selector);
148
+ mask.style.position = 'fixed';
149
+ mask.style.left = `${rect.left}px`;
150
+ mask.style.top = `${rect.top}px`;
151
+ mask.style.width = `${rect.width}px`;
152
+ mask.style.height = `${rect.height}px`;
153
+ mask.style.background = '#000';
154
+ mask.style.pointerEvents = 'none';
155
+ mask.style.zIndex = '2147483647';
156
+ doc.body.appendChild(mask);
157
+ masks.push(mask);
158
+ }
159
+ }
160
+ return () => {
161
+ for (const mask of masks) {
162
+ mask.remove();
163
+ }
164
+ };
165
+ };
166
+ const pickUniqueErrorName = (entries) => {
167
+ const reserved = new Set(entries
168
+ .filter((entry) => entry.screenshotName.startsWith(AUTO_ERROR_SCREENSHOT_BASE))
169
+ .map((entry) => entry.screenshotName));
170
+ if (!reserved.has(AUTO_ERROR_SCREENSHOT_BASE)) {
171
+ return AUTO_ERROR_SCREENSHOT_BASE;
172
+ }
173
+ let index = 2;
174
+ while (reserved.has(`${AUTO_ERROR_SCREENSHOT_BASE}-${index}`)) {
175
+ index += 1;
176
+ }
177
+ return `${AUTO_ERROR_SCREENSHOT_BASE}-${index}`;
178
+ };
179
+ const resolveManualArgs = (nameOrOptions, options) => {
180
+ if (typeof nameOrOptions === 'string') {
181
+ return { name: nameOrOptions, options };
182
+ }
183
+ return { name: nameOrOptions?.name, options: nameOrOptions };
184
+ };
185
+ const buildEntry = ({ kind, storyId, storyTitle, storyName, screenshotName, relativePath, viewport, status, error, timingsMs, }) => ({
186
+ kind,
187
+ storyId,
188
+ storyTitle,
189
+ storyName,
190
+ screenshotName,
191
+ path: relativePath,
192
+ viewport,
193
+ status,
194
+ error,
195
+ timings: { ms: timingsMs },
196
+ });
197
+ const captureScreenshot = async ({ kind, story, screenshotName, options, }) => {
198
+ const runtime = initRuntimeState();
199
+ if (!runtime) {
200
+ console.warn('[qlip] Runtime config is missing. screenshot() only works in Vitest with qlipVitestPlugin enabled.');
201
+ return;
202
+ }
203
+ const { page, commands } = await ensureBrowserContext();
204
+ if (!story.id) {
205
+ if (!runtime.warnedMissingStorybook) {
206
+ console.warn('[qlip] No story context detected. Qlip only captures Storybook Vitest stories.');
207
+ markStorybookWarning(runtime);
208
+ }
209
+ return;
210
+ }
211
+ const resolved = resolveQlipOptions({
212
+ defaults: runtime.config.defaults,
213
+ story: story.parameters,
214
+ override: options,
215
+ });
216
+ const name = kind === 'manual'
217
+ ? sanitizeSegment(screenshotName ?? nextStepName(runtime, story.id))
218
+ : 'auto';
219
+ const captureStart = Date.now();
220
+ if (resolved.skip) {
221
+ pushEntry(runtime, buildEntry({
222
+ kind,
223
+ storyId: story.id,
224
+ storyTitle: story.title,
225
+ storyName: story.name,
226
+ screenshotName: name,
227
+ relativePath: kind === 'manual'
228
+ ? buildManualScreenshotPath({
229
+ buildDir: runtime.config.buildDir,
230
+ storyId: story.id,
231
+ storyTitle: story.title,
232
+ storyName: story.name,
233
+ screenshotName: name,
234
+ }).relativePath
235
+ : buildAutoScreenshotPath({
236
+ buildDir: runtime.config.buildDir,
237
+ storyId: story.id,
238
+ storyTitle: story.title,
239
+ storyName: story.name,
240
+ }).relativePath,
241
+ viewport: resolved.viewport,
242
+ status: 'skipped',
243
+ error: null,
244
+ timingsMs: Date.now() - captureStart,
245
+ }));
246
+ updateStats(runtime, {
247
+ storiesTotal: kind === 'auto'
248
+ ? runtime.manifest.stats.storiesTotal + 1
249
+ : runtime.manifest.stats.storiesTotal,
250
+ skipped: runtime.manifest.stats.skipped + 1,
251
+ });
252
+ await commands.writeFile(joinPath(runtime.config.buildDir, 'manifest.json'), JSON.stringify(runtime.manifest, null, 2), 'utf-8');
253
+ return;
254
+ }
255
+ let relativePath = '';
256
+ let absolutePath = '';
257
+ if (kind === 'manual') {
258
+ const pathInfo = buildManualScreenshotPath({
259
+ buildDir: runtime.config.buildDir,
260
+ storyId: story.id,
261
+ storyTitle: story.title,
262
+ storyName: story.name,
263
+ screenshotName: name,
264
+ });
265
+ relativePath = pathInfo.relativePath;
266
+ absolutePath = pathInfo.absolutePath;
267
+ }
268
+ else {
269
+ const pathInfo = buildAutoScreenshotPath({
270
+ buildDir: runtime.config.buildDir,
271
+ storyId: story.id,
272
+ storyTitle: story.title,
273
+ storyName: story.name,
274
+ });
275
+ relativePath = pathInfo.relativePath;
276
+ absolutePath = pathInfo.absolutePath;
277
+ }
278
+ let status = 'captured';
279
+ let error = null;
280
+ let cleanupMasks = null;
281
+ try {
282
+ await page.viewport(resolved.viewport.width, resolved.viewport.height);
283
+ applyAnimationControl({
284
+ disableAnimations: resolved.disableAnimations,
285
+ pauseAnimationsAtEnd: resolved.pauseAnimationsAtEnd,
286
+ });
287
+ await waitForDomIdle(resolved.waitForIdleMs, resolved.maxWaitForIdleMs);
288
+ cleanupMasks = applyIgnoreMasks(resolved.ignoreElements);
289
+ await page.screenshot({ path: absolutePath, save: true });
290
+ }
291
+ catch (err) {
292
+ status = 'failed';
293
+ const typedError = err;
294
+ error = {
295
+ message: typedError.message,
296
+ stack: typedError.stack,
297
+ };
298
+ }
299
+ finally {
300
+ if (cleanupMasks) {
301
+ cleanupMasks();
302
+ }
303
+ }
304
+ const entry = buildEntry({
305
+ kind,
306
+ storyId: story.id,
307
+ storyTitle: story.title,
308
+ storyName: story.name,
309
+ screenshotName: name,
310
+ relativePath,
311
+ viewport: resolved.viewport,
312
+ status,
313
+ error,
314
+ timingsMs: Date.now() - captureStart,
315
+ });
316
+ pushEntry(runtime, entry);
317
+ if (kind === 'auto') {
318
+ updateStats(runtime, {
319
+ storiesTotal: runtime.manifest.stats.storiesTotal + 1,
320
+ capturedAuto: status === 'captured'
321
+ ? runtime.manifest.stats.capturedAuto + 1
322
+ : runtime.manifest.stats.capturedAuto,
323
+ failed: status === 'failed'
324
+ ? runtime.manifest.stats.failed + 1
325
+ : runtime.manifest.stats.failed,
326
+ });
327
+ }
328
+ else {
329
+ updateStats(runtime, {
330
+ capturedManual: status === 'captured'
331
+ ? runtime.manifest.stats.capturedManual + 1
332
+ : runtime.manifest.stats.capturedManual,
333
+ failed: status === 'failed'
334
+ ? runtime.manifest.stats.failed + 1
335
+ : runtime.manifest.stats.failed,
336
+ });
337
+ }
338
+ await commands.writeFile(joinPath(runtime.config.buildDir, 'manifest.json'), JSON.stringify(runtime.manifest, null, 2), 'utf-8');
339
+ };
340
+ export const screenshot = async (ctx, nameOrOptions, options) => {
341
+ const runtime = initRuntimeState();
342
+ if (!runtime) {
343
+ return;
344
+ }
345
+ const { name, options: resolvedOptions } = resolveManualArgs(nameOrOptions, options);
346
+ const story = resolveStoryInfo(ctx);
347
+ const params = story.parameters;
348
+ const finalOptions = resolveQlipOptions({
349
+ defaults: runtime.config.defaults,
350
+ story: params,
351
+ override: resolvedOptions,
352
+ });
353
+ if (!finalOptions.manual) {
354
+ return;
355
+ }
356
+ await captureScreenshot({
357
+ kind: 'manual',
358
+ story,
359
+ screenshotName: name,
360
+ options: resolvedOptions,
361
+ });
362
+ };
363
+ export const captureAutoScreenshot = async (ctx) => {
364
+ const runtime = initRuntimeState();
365
+ if (!runtime) {
366
+ return;
367
+ }
368
+ const storyContext = ctx.story ?? {};
369
+ const story = resolveStoryInfo(storyContext);
370
+ const params = story.parameters;
371
+ const resolvedOptions = resolveQlipOptions({ defaults: runtime.config.defaults, story: params });
372
+ if (!resolvedOptions.auto) {
373
+ return;
374
+ }
375
+ const metaStoryId = ctx.task.meta
376
+ ?.storyId;
377
+ if (!story.id && typeof metaStoryId === 'string') {
378
+ story.id = metaStoryId;
379
+ }
380
+ if (story.id && (!story.title || !story.name)) {
381
+ const storeStory = readStoryFromStore(story.id);
382
+ if (storeStory?.title && !story.title) {
383
+ story.title = storeStory.title;
384
+ }
385
+ if (storeStory?.name && !story.name) {
386
+ story.name = storeStory.name;
387
+ }
388
+ }
389
+ if (!story.name && ctx.task.name) {
390
+ story.name = ctx.task.name;
391
+ }
392
+ if (!story.title && ctx.task.suite?.name) {
393
+ story.title = ctx.task.suite.name;
394
+ }
395
+ await captureScreenshot({
396
+ kind: 'auto',
397
+ story,
398
+ });
399
+ };
400
+ export const captureErrorScreenshot = async (ctx) => {
401
+ const runtime = initRuntimeState();
402
+ if (!runtime) {
403
+ return;
404
+ }
405
+ const storyContext = ctx.story ?? {};
406
+ const story = resolveStoryInfo(storyContext);
407
+ const errorMetaStoryId = ctx.task.meta
408
+ ?.storyId;
409
+ if (!story.id && typeof errorMetaStoryId === 'string') {
410
+ story.id = errorMetaStoryId;
411
+ }
412
+ const params = story.parameters;
413
+ const resolvedOptions = resolveQlipOptions({ defaults: runtime.config.defaults, story: params });
414
+ if (resolvedOptions.skip) {
415
+ return;
416
+ }
417
+ if (!resolvedOptions.error) {
418
+ return;
419
+ }
420
+ if (!shouldCaptureOnError(runtime.config.defaults, params)) {
421
+ return;
422
+ }
423
+ if (story.id && (!story.title || !story.name)) {
424
+ const storeStory = readStoryFromStore(story.id);
425
+ if (storeStory?.title && !story.title) {
426
+ story.title = storeStory.title;
427
+ }
428
+ if (storeStory?.name && !story.name) {
429
+ story.name = storeStory.name;
430
+ }
431
+ }
432
+ if (!story.name && ctx.task.name) {
433
+ story.name = ctx.task.name;
434
+ }
435
+ if (!story.title && ctx.task.suite?.name) {
436
+ story.title = ctx.task.suite.name;
437
+ }
438
+ await captureScreenshot({
439
+ kind: 'manual',
440
+ story,
441
+ screenshotName: pickUniqueErrorName(runtime.manifest.entries),
442
+ });
443
+ };
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=setup.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../../src/runtime/setup.ts"],"names":[],"mappings":""}
@@ -0,0 +1,15 @@
1
+ import { afterEach } from 'vitest';
2
+ import { captureAutoScreenshot, captureErrorScreenshot } from './screenshot.js';
3
+ import { getRuntimeState, initRuntimeState } from './context.js';
4
+ const isBrowser = () => typeof globalThis.__vitest_browser__ !== 'undefined';
5
+ if (isBrowser()) {
6
+ initRuntimeState();
7
+ afterEach(async (context) => {
8
+ await captureAutoScreenshot(context);
9
+ const runtime = getRuntimeState();
10
+ if (runtime &&
11
+ context.task.result?.state === 'fail') {
12
+ await captureErrorScreenshot(context);
13
+ }
14
+ });
15
+ }
@@ -0,0 +1,4 @@
1
+ import React from 'react';
2
+ import './animated-box.css';
3
+ export declare const AnimatedBox: React.FC;
4
+ //# sourceMappingURL=AnimatedBox.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AnimatedBox.d.ts","sourceRoot":"","sources":["../../src/stories/AnimatedBox.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,OAAO,oBAAoB,CAAC;AAE5B,eAAO,MAAM,WAAW,EAAE,KAAK,CAAC,EAe/B,CAAC"}
@@ -0,0 +1,7 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React from 'react';
3
+ import './animated-box.css';
4
+ export const AnimatedBox = () => {
5
+ const [active, setActive] = React.useState(false);
6
+ return (_jsxs("div", { className: "qlip-anim-stage", children: [_jsx("button", { type: "button", className: "qlip-anim-trigger", onClick: () => setActive((prev) => !prev), children: "Toggle" }), _jsx("div", { className: `qlip-anim-box ${active ? 'is-active' : ''}` })] }));
7
+ };
@@ -0,0 +1,12 @@
1
+ import type { StoryObj } from '@storybook/react-vite';
2
+ declare const meta: {
3
+ title: string;
4
+ component: import("react").FC<{}>;
5
+ parameters: {
6
+ layout: string;
7
+ };
8
+ };
9
+ export default meta;
10
+ type Story = StoryObj<typeof meta>;
11
+ export declare const ShouldFinishFullyAnimated: Story;
12
+ //# sourceMappingURL=AnimatedBox.stories.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AnimatedBox.stories.d.ts","sourceRoot":"","sources":["../../src/stories/AnimatedBox.stories.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAQ,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAK5D,QAAA,MAAM,IAAI;;;;;;CAM0B,CAAC;AAErC,eAAe,IAAI,CAAC;AACpB,KAAK,KAAK,GAAG,QAAQ,CAAC,OAAO,IAAI,CAAC,CAAC;AAGnC,eAAO,MAAM,yBAAyB,EAAE,KAUvC,CAAC"}
@@ -0,0 +1,21 @@
1
+ import { userEvent, within } from 'storybook/test';
2
+ import { AnimatedBox } from './AnimatedBox.js';
3
+ const meta = {
4
+ title: 'Example/ShouldFinishFullyAnimated',
5
+ component: AnimatedBox,
6
+ parameters: {
7
+ layout: 'centered',
8
+ },
9
+ };
10
+ export default meta;
11
+ export const ShouldFinishFullyAnimated = {
12
+ parameters: {
13
+ qlip: {
14
+ disableAnimations: true,
15
+ },
16
+ },
17
+ play: async (ctx) => {
18
+ const canvas = within(ctx.canvasElement);
19
+ await userEvent.click(canvas.getByRole('button', { name: /toggle/i }));
20
+ },
21
+ };
@@ -0,0 +1,16 @@
1
+ import './button.css';
2
+ export interface ButtonProps {
3
+ /** Is this the principal call to action on the page? */
4
+ primary?: boolean;
5
+ /** What background color to use */
6
+ backgroundColor?: string;
7
+ /** How large should the button be? */
8
+ size?: 'small' | 'medium' | 'large';
9
+ /** Button contents */
10
+ label: string;
11
+ /** Optional click handler */
12
+ onClick?: () => void;
13
+ }
14
+ /** Primary UI component for user interaction */
15
+ export declare const Button: ({ primary, size, backgroundColor, label, ...props }: ButtonProps) => import("react/jsx-runtime").JSX.Element;
16
+ //# sourceMappingURL=Button.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Button.d.ts","sourceRoot":"","sources":["../../src/stories/Button.tsx"],"names":[],"mappings":"AAEA,OAAO,cAAc,CAAC;AAEtB,MAAM,WAAW,WAAW;IAC1B,wDAAwD;IACxD,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,mCAAmC;IACnC,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,sCAAsC;IACtC,IAAI,CAAC,EAAE,OAAO,GAAG,QAAQ,GAAG,OAAO,CAAC;IACpC,sBAAsB;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,6BAA6B;IAC7B,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;CACtB;AAED,gDAAgD;AAChD,eAAO,MAAM,MAAM,GAAI,qDAMpB,WAAW,4CAYb,CAAC"}
@@ -0,0 +1,7 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import './button.css';
3
+ /** Primary UI component for user interaction */
4
+ export const Button = ({ primary = false, size = 'medium', backgroundColor, label, ...props }) => {
5
+ const mode = primary ? 'storybook-button--primary' : 'storybook-button--secondary';
6
+ return (_jsx("button", { type: "button", className: ['storybook-button', `storybook-button--${size}`, mode].join(' '), style: { backgroundColor }, ...props, children: label }));
7
+ };
@@ -0,0 +1,21 @@
1
+ import type { StoryObj } from '@storybook/react-vite';
2
+ declare const meta: {
3
+ title: string;
4
+ component: ({ primary, size, backgroundColor, label, ...props }: import("./Button.js").ButtonProps) => import("react/jsx-runtime").JSX.Element;
5
+ tags: string[];
6
+ argTypes: {
7
+ backgroundColor: {
8
+ control: "color";
9
+ };
10
+ };
11
+ args: {
12
+ onClick: import("@vitest/spy").Mock<(...args: any[]) => any>;
13
+ };
14
+ };
15
+ export default meta;
16
+ type Story = StoryObj<typeof meta>;
17
+ export declare const Primary: Story;
18
+ export declare const Secondary: Story;
19
+ export declare const Large: Story;
20
+ export declare const Small: Story;
21
+ //# sourceMappingURL=Button.stories.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Button.stories.d.ts","sourceRoot":"","sources":["../../src/stories/Button.stories.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAQ,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAO5D,QAAA,MAAM,IAAI;;;;;;;;;;;;CAWqB,CAAC;AAEhC,eAAe,IAAI,CAAC;AACpB,KAAK,KAAK,GAAG,QAAQ,CAAC,OAAO,IAAI,CAAC,CAAC;AAGnC,eAAO,MAAM,OAAO,EAAE,KAKrB,CAAC;AAEF,eAAO,MAAM,SAAS,EAAE,KAIvB,CAAC;AAEF,eAAO,MAAM,KAAK,EAAE,KAKnB,CAAC;AAEF,eAAO,MAAM,KAAK,EAAE,KAKnB,CAAC"}
@@ -0,0 +1,40 @@
1
+ import { fn } from 'storybook/test';
2
+ import { Button } from './Button.js';
3
+ // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
4
+ const meta = {
5
+ title: 'Example/Button',
6
+ component: Button,
7
+ // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
8
+ tags: ['autodocs'],
9
+ // More on argTypes: https://storybook.js.org/docs/api/argtypes
10
+ argTypes: {
11
+ backgroundColor: { control: 'color' },
12
+ },
13
+ // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#story-args
14
+ args: { onClick: fn() },
15
+ };
16
+ export default meta;
17
+ // More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
18
+ export const Primary = {
19
+ args: {
20
+ primary: true,
21
+ label: 'Button',
22
+ },
23
+ };
24
+ export const Secondary = {
25
+ args: {
26
+ label: 'Button',
27
+ },
28
+ };
29
+ export const Large = {
30
+ args: {
31
+ size: 'large',
32
+ label: 'Button',
33
+ },
34
+ };
35
+ export const Small = {
36
+ args: {
37
+ size: 'small',
38
+ label: 'Button',
39
+ },
40
+ };
@@ -0,0 +1,12 @@
1
+ import type { StoryObj } from '@storybook/react-vite';
2
+ declare const meta: {
3
+ title: string;
4
+ component: import("react").FC<{}>;
5
+ parameters: {
6
+ layout: string;
7
+ };
8
+ };
9
+ export default meta;
10
+ type Story = StoryObj<typeof meta>;
11
+ export declare const CaptureOnTestFailure: Story;
12
+ //# sourceMappingURL=FailOnPurpose.stories.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FailOnPurpose.stories.d.ts","sourceRoot":"","sources":["../../src/stories/FailOnPurpose.stories.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAQ,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAM5D,QAAA,MAAM,IAAI;;;;;;CAM0B,CAAC;AAErC,eAAe,IAAI,CAAC;AACpB,KAAK,KAAK,GAAG,QAAQ,CAAC,OAAO,IAAI,CAAC,CAAC;AAGnC,eAAO,MAAM,oBAAoB,EAAE,KAyBlC,CAAC"}
@@ -0,0 +1,32 @@
1
+ import { userEvent, within } from 'storybook/test';
2
+ import { AnimatedBox } from './AnimatedBox.js';
3
+ const meta = {
4
+ title: 'Example/Capture on Test Failure',
5
+ component: AnimatedBox,
6
+ parameters: {
7
+ layout: 'centered',
8
+ },
9
+ };
10
+ export default meta;
11
+ export const CaptureOnTestFailure = {
12
+ parameters: {
13
+ qlip: {
14
+ captureOnError: true,
15
+ // To test error screenshot capture, temporarily set skip to false
16
+ skip: true,
17
+ },
18
+ },
19
+ play: async (ctx) => {
20
+ // Check if this story should be skipped (to prevent CI failures)
21
+ // The skip parameter prevents screenshot capture, but we also need to skip the test
22
+ const qlipParams = ctx.parameters?.qlip;
23
+ if (qlipParams?.skip) {
24
+ console.log('FailOnPurpose test skipped - set qlip.skip to false to test error screenshots');
25
+ return;
26
+ }
27
+ const canvas = within(ctx.canvasElement);
28
+ // This will intentionally fail - clicking a non-existent button
29
+ // An error screenshot will be captured due to captureOnError: true
30
+ await userEvent.click(canvas.getByRole('button', { name: /missingButton/i }));
31
+ },
32
+ };