@crvy/strybk 0.0.5 → 0.0.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/CHANGELOG.md +45 -0
- package/README.md +31 -0
- package/dist/src/config.d.ts +0 -1
- package/dist/src/config.js +0 -1
- package/dist/src/generate/index.js +2 -24
- package/dist/src/generate/render.js +35 -5
- package/dist/src/playwright/fixtures.d.ts +2 -0
- package/dist/src/playwright/fixtures.js +16 -0
- package/dist/src/playwright/index.d.ts +1 -0
- package/dist/src/storybook/creeveyParams.d.ts +45 -0
- package/dist/src/storybook/creeveyParams.js +63 -0
- package/dist/src/storybook/extract.d.ts +4 -0
- package/dist/src/storybook/extract.js +11 -0
- package/package.json +4 -4
- package/dist/src/generate/metadata.d.ts +0 -5
- package/dist/src/generate/metadata.js +0 -124
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,51 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.0.7] - 2026-08-26
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Nest one describe per story title segment
|
|
13
|
+
|
|
14
|
+
### Documentation
|
|
15
|
+
|
|
16
|
+
- Add nested describe titles design spec
|
|
17
|
+
- Add nested describe titles implementation plan
|
|
18
|
+
- Note nested describes and --grep caveat
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- Escape backslashes in generated titles and story names
|
|
23
|
+
|
|
24
|
+
### Miscellaneous
|
|
25
|
+
|
|
26
|
+
- Upgrade bun to 1.4.0
|
|
27
|
+
## [0.0.6] - 2026-08-24
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
|
|
31
|
+
- Add creevey regex deserialization helpers
|
|
32
|
+
- Add creevey skip-option matching
|
|
33
|
+
- Resolve creevey story params from extracted stories
|
|
34
|
+
- Extract merged storybook params at runtime
|
|
35
|
+
- Render captureElement/skip-aware generated tests
|
|
36
|
+
- Add creevey fixture and worker story cache
|
|
37
|
+
|
|
38
|
+
### Changed
|
|
39
|
+
|
|
40
|
+
- Drop generate-time creevey source-parsing
|
|
41
|
+
- Unexport shouldSkipByOption helper
|
|
42
|
+
|
|
43
|
+
### Documentation
|
|
44
|
+
|
|
45
|
+
- Add captureElement + runtime creevey resolution design spec
|
|
46
|
+
- Add captureElement runtime-resolution implementation plan
|
|
47
|
+
- Document creevey capture/skip/ignoreElements and Playwright prerequisite
|
|
48
|
+
- Add migration note and clarify skip in-dimension matching
|
|
49
|
+
|
|
50
|
+
### Testing
|
|
51
|
+
|
|
52
|
+
- Cover _stories and creevey fixture wiring
|
|
8
53
|
## [0.0.5] - 2026-07-06
|
|
9
54
|
|
|
10
55
|
### Added
|
package/README.md
CHANGED
|
@@ -36,6 +36,37 @@ export default defineConfig({
|
|
|
36
36
|
});
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
+
## Generated tests
|
|
40
|
+
|
|
41
|
+
Each generated spec renders a Playwright test per story. At runtime, `parameters.creevey` (read from the running Storybook, merged across global / kind / story levels) drives capture and skip behavior — no Storybook addon required:
|
|
42
|
+
|
|
43
|
+
Generated suites nest one `test.describe` per title segment — `Components/Button` becomes `describe('Components') > describe('Button')` — so the Playwright HTML reporter shows a collapsible tree. Snapshot filenames are unaffected. Note that `--grep` patterns containing `/` no longer match (Playwright greps the space-joined title path); grep by a single segment instead, e.g. `--grep CommentLine`.
|
|
44
|
+
|
|
45
|
+
- `captureElement: '<selector>'` — captures `page.locator('<selector>')`.
|
|
46
|
+
- `captureElement: null` (or unset) — captures the viewport.
|
|
47
|
+
- `skip: { '<reason>': { in, kinds, stories } }` — marks the test skipped with `<reason>`. Note: `in` matches the **Playwright project name** (not the browser engine), so with the default project name `chromium`, a rule like `{ in: 'chrome' }` won't match — name your Playwright projects to line up with your `in:` rules, or scope rules via `kinds`/`stories`.
|
|
48
|
+
- `ignoreElements: '<selector>' | ['<selector>']` — masks those elements via `toHaveScreenshot({ mask })`.
|
|
49
|
+
|
|
50
|
+
Example:
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
// stories/MyModal.stories.tsx
|
|
54
|
+
export default {
|
|
55
|
+
title: "MyModal",
|
|
56
|
+
parameters: { creevey: { captureElement: "#storybook-root" } },
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export const Default = {
|
|
60
|
+
parameters: { creevey: { ignoreElements: ".timestamp" } },
|
|
61
|
+
};
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
**Prerequisites for running specs:** `@playwright/test` (peer dependency) and browser binaries (`npx playwright install`). The `crvy-strybk generate` command itself needs neither — only a running Storybook to fetch `index.json`.
|
|
65
|
+
|
|
66
|
+
## Upgrading from 0.0.x
|
|
67
|
+
|
|
68
|
+
`metadataExtractors` has been removed. `skip` and `captureElement` now resolve automatically at runtime — remove any `metadataExtractors: ["creevey"]` line from your `strybk.config.ts` and re-run `crvy-strybk generate`. Skipped stories now appear as `skipped` in Playwright reports rather than being omitted from the spec file.
|
|
69
|
+
|
|
39
70
|
## Changelog
|
|
40
71
|
|
|
41
72
|
Preview the next changelog entry:
|
package/dist/src/config.d.ts
CHANGED
package/dist/src/config.js
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
2
1
|
import { discoverStoryFiles } from "./discover.js";
|
|
3
|
-
import { extractCreeveyMetadata, FILE_POLICY_KEY } from "./metadata.js";
|
|
4
2
|
import { renderScreenshotSpec } from "./render.js";
|
|
5
3
|
const normalizePath = (value) => value.replace(/\\/gu, "/").replace(/^\.\//u, "");
|
|
6
4
|
const matchesByImportPath = (filePath, importPath) => {
|
|
@@ -13,40 +11,20 @@ const resolveStoryTitle = (storyFile, indexEntries) => {
|
|
|
13
11
|
return pathMatch?.title ?? null;
|
|
14
12
|
};
|
|
15
13
|
const escapeForRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
16
|
-
const toStoryIdSegment = (value) => value
|
|
17
|
-
.replace(/([a-z0-9])([A-Z])/gu, "$1-$2")
|
|
18
|
-
.replace(/[^a-zA-Z0-9]+/gu, "-")
|
|
19
|
-
.replace(/^-+|-+$/gu, "")
|
|
20
|
-
.toLowerCase();
|
|
21
|
-
const isStorySkipped = (story, storyMetadata) => {
|
|
22
|
-
if (story.exportName !== undefined) {
|
|
23
|
-
return storyMetadata[story.exportName]?.skip === true;
|
|
24
|
-
}
|
|
25
|
-
const storyIdSegment = story.id.split("--").slice(1).join("--");
|
|
26
|
-
return Object.entries(storyMetadata).some(([exportName, policy]) => policy.skip === true && toStoryIdSegment(exportName) === storyIdSegment);
|
|
27
|
-
};
|
|
28
14
|
export async function generateScreenshots(args) {
|
|
29
15
|
const storyFiles = await discoverStoryFiles(args.config.storyGlobs, args.configDir === undefined ? {} : { cwd: args.configDir });
|
|
30
16
|
const generatedRegionName = args.config.generatedRegionName ?? "auto-screenshots";
|
|
31
17
|
const manualRegionPattern = new RegExp(`// @generated-end ${escapeForRegExp(generatedRegionName)}\\s*([\\s\\S]*)$`, "u");
|
|
32
|
-
const shouldExtractCreeveyMetadata = args.config.metadataExtractors?.includes("creevey") ?? false;
|
|
33
18
|
return storyFiles.flatMap((storyFile) => {
|
|
34
19
|
const title = resolveStoryTitle(storyFile, args.indexEntries);
|
|
35
20
|
if (title === null) {
|
|
36
21
|
return [];
|
|
37
22
|
}
|
|
38
23
|
const stories = args.indexEntries.filter((entry) => entry.title === title);
|
|
39
|
-
const storyMetadata = shouldExtractCreeveyMetadata
|
|
40
|
-
? extractCreeveyMetadata(readFileSync(storyFile.filePath, "utf8"))
|
|
41
|
-
: {};
|
|
42
|
-
const isFileSkipped = storyMetadata[FILE_POLICY_KEY]?.skip === true;
|
|
43
|
-
const filteredStories = isFileSkipped
|
|
44
|
-
? []
|
|
45
|
-
: stories.filter((story) => !isStorySkipped(story, storyMetadata));
|
|
46
24
|
const outputPath = args.config.resolveSpecPath({ storyFilePath: storyFile.filePath });
|
|
47
25
|
const existing = args.readExistingFile?.(outputPath) ?? null;
|
|
48
26
|
const manualRegion = existing?.match(manualRegionPattern)?.[1]?.trim() ?? "";
|
|
49
|
-
if (
|
|
27
|
+
if (stories.length === 0 && manualRegion.length === 0) {
|
|
50
28
|
return [];
|
|
51
29
|
}
|
|
52
30
|
return [
|
|
@@ -55,7 +33,7 @@ export async function generateScreenshots(args) {
|
|
|
55
33
|
content: renderScreenshotSpec({
|
|
56
34
|
config: args.config,
|
|
57
35
|
title,
|
|
58
|
-
stories
|
|
36
|
+
stories,
|
|
59
37
|
manualRegion,
|
|
60
38
|
}),
|
|
61
39
|
},
|
|
@@ -1,8 +1,38 @@
|
|
|
1
|
-
const escapeSingleQuotes = (value) => value.replace(/'/gu, "\\'");
|
|
1
|
+
const escapeSingleQuotes = (value) => value.replace(/\\/gu, "\\\\").replace(/'/gu, "\\'");
|
|
2
|
+
const splitTitleSegments = (title) => title
|
|
3
|
+
.split("/")
|
|
4
|
+
.map((segment) => segment.trim())
|
|
5
|
+
.filter((segment) => segment.length > 0);
|
|
6
|
+
const renderTest = (story, depth) => {
|
|
7
|
+
const indent = " ".repeat(depth);
|
|
8
|
+
return [
|
|
9
|
+
`${indent}test('${escapeSingleQuotes(story.name)}', async ({ sharedPage, creevey }) => {`,
|
|
10
|
+
`${indent} const { skip, reason, captureElement, ignoreElements } = creevey.params('${story.id}');`,
|
|
11
|
+
`${indent} test.skip(skip, reason);`,
|
|
12
|
+
`${indent} await switchStory(sharedPage, '${story.id}');`,
|
|
13
|
+
`${indent} const target = captureElement ? sharedPage.locator(captureElement) : sharedPage;`,
|
|
14
|
+
`${indent} await expect(target).toHaveScreenshot({`,
|
|
15
|
+
`${indent} mask: ignoreElements.map((selector) => sharedPage.locator(selector)),`,
|
|
16
|
+
`${indent} });`,
|
|
17
|
+
`${indent}});`,
|
|
18
|
+
].join("\n");
|
|
19
|
+
};
|
|
20
|
+
const wrapInDescribes = (segments, tests, depth) => {
|
|
21
|
+
const indent = " ".repeat(depth);
|
|
22
|
+
const [segment, ...rest] = segments;
|
|
23
|
+
if (segment === undefined) {
|
|
24
|
+
return tests;
|
|
25
|
+
}
|
|
26
|
+
return [
|
|
27
|
+
`${indent}test.describe('${escapeSingleQuotes(segment)}', () => {`,
|
|
28
|
+
wrapInDescribes(rest, tests, depth + 1),
|
|
29
|
+
`${indent}});`,
|
|
30
|
+
].join("\n");
|
|
31
|
+
};
|
|
2
32
|
export function renderScreenshotSpec(args) {
|
|
3
33
|
const generatedRegionName = args.config.generatedRegionName ?? "auto-screenshots";
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
return `import { test, expect, switchStory } from '@crvy/strybk';\n\n// @generated-begin ${generatedRegionName}\
|
|
34
|
+
const segments = splitTitleSegments(args.title);
|
|
35
|
+
const tests = args.stories.map((story) => renderTest(story, segments.length)).join("\n\n");
|
|
36
|
+
const body = wrapInDescribes(segments, tests, 0);
|
|
37
|
+
return `import { test, expect, switchStory } from '@crvy/strybk';\n\n// @generated-begin ${generatedRegionName}\n${body}\n// @generated-end ${generatedRegionName}\n\n${args.manualRegion}`;
|
|
8
38
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { Page, PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions, TestType } from "@playwright/test";
|
|
2
|
+
import type { CreeveyApi } from "../storybook/creeveyParams.js";
|
|
2
3
|
type StrybkFixtures = {
|
|
3
4
|
sharedPage: Page;
|
|
5
|
+
creevey: CreeveyApi;
|
|
4
6
|
};
|
|
5
7
|
type PublicStrybkTest = TestType<PlaywrightTestArgs & PlaywrightTestOptions & StrybkFixtures, PlaywrightWorkerArgs & PlaywrightWorkerOptions>;
|
|
6
8
|
type StrybkFixtureHandles = {
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { resolveCreeveyStory } from "../storybook/creeveyParams.js";
|
|
1
2
|
import { createChannelDriver } from "../storybook/channelDriver.js";
|
|
3
|
+
import { extractStories } from "../storybook/extract.js";
|
|
2
4
|
import { loadPlaywrightTestRuntime } from "./runtime.js";
|
|
3
5
|
const animationDisablerStyles = [
|
|
4
6
|
"*, *::before, *::after {",
|
|
@@ -67,6 +69,13 @@ export const createStrybkFixtures = () => {
|
|
|
67
69
|
},
|
|
68
70
|
{ scope: "worker" },
|
|
69
71
|
],
|
|
72
|
+
_stories: [
|
|
73
|
+
async ({ _workerPage }, use) => {
|
|
74
|
+
const stories = await extractStories(_workerPage);
|
|
75
|
+
await use(stories);
|
|
76
|
+
},
|
|
77
|
+
{ scope: "worker" },
|
|
78
|
+
],
|
|
70
79
|
sharedPage: async ({ _workerPage }, use, testInfo) => {
|
|
71
80
|
const storybookGlobals = getStorybookGlobals(testInfo);
|
|
72
81
|
if (storybookGlobals !== undefined) {
|
|
@@ -76,6 +85,13 @@ export const createStrybkFixtures = () => {
|
|
|
76
85
|
await use(_workerPage);
|
|
77
86
|
await restoreSharedPageBaseline(_workerPage, testInfo.project.use.baseURL);
|
|
78
87
|
},
|
|
88
|
+
creevey: async ({ _stories }, use, testInfo) => {
|
|
89
|
+
const browser = testInfo.project.name;
|
|
90
|
+
const api = {
|
|
91
|
+
params: (storyId) => resolveCreeveyStory(_stories, storyId, browser),
|
|
92
|
+
};
|
|
93
|
+
await use(api);
|
|
94
|
+
},
|
|
79
95
|
});
|
|
80
96
|
return {
|
|
81
97
|
expect,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { switchStory } from "./switchStory.js";
|
|
2
2
|
export declare const test: import("playwright/test").TestType<import("playwright/test").PlaywrightTestArgs & import("playwright/test").PlaywrightTestOptions & {
|
|
3
3
|
sharedPage: import("playwright-core").Page;
|
|
4
|
+
creevey: import("../storybook/creeveyParams.js").CreeveyApi;
|
|
4
5
|
}, import("playwright/test").PlaywrightWorkerArgs & import("playwright/test").PlaywrightWorkerOptions>;
|
|
5
6
|
export declare const expect: import("playwright/test").Expect<{}>;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export interface SerializedRegExp {
|
|
2
|
+
__regexp: true;
|
|
3
|
+
source: string;
|
|
4
|
+
flags: string;
|
|
5
|
+
}
|
|
6
|
+
export declare const isSerializedRegExp: (value: unknown) => value is SerializedRegExp;
|
|
7
|
+
export declare const deserializeRegExp: ({ source, flags }: SerializedRegExp) => RegExp;
|
|
8
|
+
export interface SkipOption {
|
|
9
|
+
in?: string | string[] | RegExp | SerializedRegExp;
|
|
10
|
+
kinds?: string | string[] | RegExp | SerializedRegExp;
|
|
11
|
+
stories?: string | string[] | RegExp | SerializedRegExp;
|
|
12
|
+
}
|
|
13
|
+
export type SkipOptions = boolean | string | Record<string, SkipOption | SkipOption[]>;
|
|
14
|
+
export declare const shouldSkip: (browser: string, meta: {
|
|
15
|
+
title: string;
|
|
16
|
+
name: string;
|
|
17
|
+
}, skipOptions: SkipOptions) => boolean | string;
|
|
18
|
+
export interface CreeveyStoryParams {
|
|
19
|
+
captureElement?: string | null;
|
|
20
|
+
ignoreElements?: string | string[] | null;
|
|
21
|
+
skip?: SkipOptions;
|
|
22
|
+
}
|
|
23
|
+
export interface NormalizedCreeveyParams {
|
|
24
|
+
skip: boolean;
|
|
25
|
+
reason?: string;
|
|
26
|
+
captureElement: string | null;
|
|
27
|
+
ignoreElements: string[];
|
|
28
|
+
}
|
|
29
|
+
export interface StoriesRaw {
|
|
30
|
+
[storyId: string]: {
|
|
31
|
+
title: string;
|
|
32
|
+
name: string;
|
|
33
|
+
parameters?: {
|
|
34
|
+
creevey?: CreeveyStoryParams;
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export interface CreeveyApi {
|
|
39
|
+
params(storyId: string): NormalizedCreeveyParams;
|
|
40
|
+
}
|
|
41
|
+
export declare const normalizeCreeveyParams: (raw: CreeveyStoryParams | undefined, browser: string, meta: {
|
|
42
|
+
title: string;
|
|
43
|
+
name: string;
|
|
44
|
+
}) => NormalizedCreeveyParams;
|
|
45
|
+
export declare const resolveCreeveyStory: (stories: StoriesRaw, storyId: string, browser: string) => NormalizedCreeveyParams;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
export const isSerializedRegExp = (value) => typeof value === "object" && value !== null && Reflect.get(value, "__regexp") === true;
|
|
2
|
+
export const deserializeRegExp = ({ source, flags }) => new RegExp(source, flags);
|
|
3
|
+
const matchBy = (pattern, value) => (typeof pattern === "string" && pattern === value) ||
|
|
4
|
+
(Array.isArray(pattern) && pattern.includes(value)) ||
|
|
5
|
+
(pattern instanceof RegExp && pattern.test(value)) ||
|
|
6
|
+
(isSerializedRegExp(pattern) && deserializeRegExp(pattern).test(value)) ||
|
|
7
|
+
pattern === undefined;
|
|
8
|
+
const shouldSkipByOption = (browser, meta, skipOption, reason) => {
|
|
9
|
+
if (Array.isArray(skipOption)) {
|
|
10
|
+
for (const option of skipOption) {
|
|
11
|
+
const result = shouldSkipByOption(browser, meta, option, reason);
|
|
12
|
+
if (result !== false) {
|
|
13
|
+
return result;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
const { in: browsers, kinds, stories } = skipOption;
|
|
19
|
+
const skipByBrowser = matchBy(browsers, browser);
|
|
20
|
+
const skipByKind = matchBy(kinds, meta.title);
|
|
21
|
+
const skipByStory = matchBy(stories, meta.name);
|
|
22
|
+
return skipByBrowser && skipByKind && skipByStory && reason;
|
|
23
|
+
};
|
|
24
|
+
export const shouldSkip = (browser, meta, skipOptions) => {
|
|
25
|
+
if (typeof skipOptions !== "object") {
|
|
26
|
+
return skipOptions;
|
|
27
|
+
}
|
|
28
|
+
for (const reason of Object.keys(skipOptions)) {
|
|
29
|
+
const result = shouldSkipByOption(browser, meta, skipOptions[reason], reason);
|
|
30
|
+
if (result !== false) {
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return false;
|
|
35
|
+
};
|
|
36
|
+
const toArray = (value) => {
|
|
37
|
+
if (value === null || value === undefined) {
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
return Array.isArray(value) ? value : [value];
|
|
41
|
+
};
|
|
42
|
+
export const normalizeCreeveyParams = (raw, browser, meta) => {
|
|
43
|
+
if (raw === undefined) {
|
|
44
|
+
return { skip: false, captureElement: null, ignoreElements: [] };
|
|
45
|
+
}
|
|
46
|
+
const skipResult = raw.skip === undefined ? false : shouldSkip(browser, meta, raw.skip);
|
|
47
|
+
return {
|
|
48
|
+
skip: skipResult !== false,
|
|
49
|
+
reason: typeof skipResult === "string" ? skipResult : undefined,
|
|
50
|
+
captureElement: raw.captureElement === undefined ? null : raw.captureElement,
|
|
51
|
+
ignoreElements: toArray(raw.ignoreElements),
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
export const resolveCreeveyStory = (stories, storyId, browser) => {
|
|
55
|
+
const story = stories[storyId];
|
|
56
|
+
if (story === undefined) {
|
|
57
|
+
throw new Error(`Story '${storyId}' not found in extracted Storybook stories`);
|
|
58
|
+
}
|
|
59
|
+
return normalizeCreeveyParams(story.parameters?.creevey, browser, {
|
|
60
|
+
title: story.title,
|
|
61
|
+
name: story.name,
|
|
62
|
+
});
|
|
63
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
const isStoriesRaw = (value) => typeof value === "object" && value !== null;
|
|
2
|
+
export const toStoriesRaw = (value) => {
|
|
3
|
+
if (!isStoriesRaw(value)) {
|
|
4
|
+
throw new Error("Storybook preview not available; is Storybook fully loaded?");
|
|
5
|
+
}
|
|
6
|
+
return value;
|
|
7
|
+
};
|
|
8
|
+
export const extractStories = async (page) => toStoriesRaw(await page.evaluate(() => {
|
|
9
|
+
const preview = window.__STORYBOOK_PREVIEW__;
|
|
10
|
+
return preview?.extract?.();
|
|
11
|
+
}));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crvy/strybk",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.7",
|
|
4
4
|
"description": "Generator-first Playwright screenshot testing for Storybook.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"crvy",
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
},
|
|
73
73
|
"devDependencies": {
|
|
74
74
|
"@playwright/test": "1.59.1",
|
|
75
|
-
"@types/bun": "^1.
|
|
75
|
+
"@types/bun": "^1.4.0",
|
|
76
76
|
"@types/node": "25.9.0",
|
|
77
77
|
"git-cliff": "2.13.1",
|
|
78
78
|
"jscpd": "^4.0.8",
|
|
@@ -88,8 +88,8 @@
|
|
|
88
88
|
"@playwright/test": ">=1.59.0"
|
|
89
89
|
},
|
|
90
90
|
"engines": {
|
|
91
|
-
"bun": ">=1.
|
|
91
|
+
"bun": ">=1.4.0",
|
|
92
92
|
"node": ">=22"
|
|
93
93
|
},
|
|
94
|
-
"packageManager": "bun@1.
|
|
94
|
+
"packageManager": "bun@1.4.0"
|
|
95
95
|
}
|
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
export const FILE_POLICY_KEY = "__file__";
|
|
2
|
-
const skipWhitespace = (source, startIndex) => {
|
|
3
|
-
let index = startIndex;
|
|
4
|
-
while (/\s/u.test(source[index] ?? "")) {
|
|
5
|
-
index += 1;
|
|
6
|
-
}
|
|
7
|
-
return index;
|
|
8
|
-
};
|
|
9
|
-
const extractObjectLiteral = (source, startIndex) => {
|
|
10
|
-
const objectStartIndex = skipWhitespace(source, startIndex);
|
|
11
|
-
if (source[objectStartIndex] !== "{") {
|
|
12
|
-
return null;
|
|
13
|
-
}
|
|
14
|
-
let depth = 0;
|
|
15
|
-
let inSingleQuote = false;
|
|
16
|
-
let inDoubleQuote = false;
|
|
17
|
-
let inTemplateString = false;
|
|
18
|
-
let inLineComment = false;
|
|
19
|
-
let inBlockComment = false;
|
|
20
|
-
for (let index = objectStartIndex; index < source.length; index += 1) {
|
|
21
|
-
const char = source[index] ?? "";
|
|
22
|
-
const nextChar = source[index + 1] ?? "";
|
|
23
|
-
const previousChar = source[index - 1] ?? "";
|
|
24
|
-
if (inLineComment) {
|
|
25
|
-
if (char === "\n") {
|
|
26
|
-
inLineComment = false;
|
|
27
|
-
}
|
|
28
|
-
continue;
|
|
29
|
-
}
|
|
30
|
-
if (inBlockComment) {
|
|
31
|
-
if (previousChar === "*" && char === "/") {
|
|
32
|
-
inBlockComment = false;
|
|
33
|
-
}
|
|
34
|
-
continue;
|
|
35
|
-
}
|
|
36
|
-
if (inSingleQuote) {
|
|
37
|
-
if (char === "'" && previousChar !== "\\") {
|
|
38
|
-
inSingleQuote = false;
|
|
39
|
-
}
|
|
40
|
-
continue;
|
|
41
|
-
}
|
|
42
|
-
if (inDoubleQuote) {
|
|
43
|
-
if (char === '"' && previousChar !== "\\") {
|
|
44
|
-
inDoubleQuote = false;
|
|
45
|
-
}
|
|
46
|
-
continue;
|
|
47
|
-
}
|
|
48
|
-
if (inTemplateString) {
|
|
49
|
-
if (char === "`" && previousChar !== "\\") {
|
|
50
|
-
inTemplateString = false;
|
|
51
|
-
}
|
|
52
|
-
continue;
|
|
53
|
-
}
|
|
54
|
-
if (char === "/" && nextChar === "/") {
|
|
55
|
-
inLineComment = true;
|
|
56
|
-
index += 1;
|
|
57
|
-
continue;
|
|
58
|
-
}
|
|
59
|
-
if (char === "/" && nextChar === "*") {
|
|
60
|
-
inBlockComment = true;
|
|
61
|
-
index += 1;
|
|
62
|
-
continue;
|
|
63
|
-
}
|
|
64
|
-
if (char === "'") {
|
|
65
|
-
inSingleQuote = true;
|
|
66
|
-
continue;
|
|
67
|
-
}
|
|
68
|
-
if (char === '"') {
|
|
69
|
-
inDoubleQuote = true;
|
|
70
|
-
continue;
|
|
71
|
-
}
|
|
72
|
-
if (char === "`") {
|
|
73
|
-
inTemplateString = true;
|
|
74
|
-
continue;
|
|
75
|
-
}
|
|
76
|
-
if (char === "{") {
|
|
77
|
-
depth += 1;
|
|
78
|
-
continue;
|
|
79
|
-
}
|
|
80
|
-
if (char === "}") {
|
|
81
|
-
depth -= 1;
|
|
82
|
-
if (depth === 0) {
|
|
83
|
-
return source.slice(objectStartIndex, index + 1);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
return null;
|
|
88
|
-
};
|
|
89
|
-
const extractSkipPolicy = (source, scope) => {
|
|
90
|
-
if (source === null) {
|
|
91
|
-
return undefined;
|
|
92
|
-
}
|
|
93
|
-
const pattern = scope === "parameters"
|
|
94
|
-
? /\bcreevey\s*:\s*\{[\s\S]*?\bskip\s*:\s*(true|false)\b/u
|
|
95
|
-
: /\bparameters\s*:\s*\{[\s\S]*?\bcreevey\s*:\s*\{[\s\S]*?\bskip\s*:\s*(true|false)\b/u;
|
|
96
|
-
const skipMatch = source.match(pattern);
|
|
97
|
-
return skipMatch ? { skip: skipMatch[1] === "true" } : undefined;
|
|
98
|
-
};
|
|
99
|
-
const collectPolicies = (source, pattern, scope) => Array.from(source.matchAll(pattern)).flatMap((match) => {
|
|
100
|
-
const index = match.index ?? 0;
|
|
101
|
-
const policy = extractSkipPolicy(extractObjectLiteral(source, index + match[0].length), scope);
|
|
102
|
-
return policy ? [[index, match[1], policy]] : [];
|
|
103
|
-
});
|
|
104
|
-
export function extractCreeveyMetadata(source) {
|
|
105
|
-
const storyPolicies = [
|
|
106
|
-
...collectPolicies(source, /(\w+)\.parameters\s*=/gu, "parameters"),
|
|
107
|
-
...collectPolicies(source, /export\s+const\s+(\w+)(?:\s*:\s*[^=]+)?\s*=/gu, "object"),
|
|
108
|
-
];
|
|
109
|
-
const constPolicies = new Map(collectPolicies(source, /const\s+(\w+)(?:\s*:\s*[^=]+)?\s*=/gu, "object").map(([, name, policy]) => [name, policy]));
|
|
110
|
-
const filePolicies = [
|
|
111
|
-
...Array.from(source.matchAll(/export\s+default\b/gu)).flatMap((match) => {
|
|
112
|
-
const index = match.index ?? 0;
|
|
113
|
-
const policy = extractSkipPolicy(extractObjectLiteral(source, index + match[0].length), "object");
|
|
114
|
-
return policy ? [[index, FILE_POLICY_KEY, policy]] : [];
|
|
115
|
-
}),
|
|
116
|
-
...Array.from(source.matchAll(/export\s+default\s+(\w+)\s*;/gu)).flatMap((match) => {
|
|
117
|
-
const policy = constPolicies.get(match[1]);
|
|
118
|
-
return policy ? [[match.index ?? 0, FILE_POLICY_KEY, policy]] : [];
|
|
119
|
-
}),
|
|
120
|
-
];
|
|
121
|
-
return Object.fromEntries([...storyPolicies, ...filePolicies]
|
|
122
|
-
.sort((left, right) => left[0] - right[0])
|
|
123
|
-
.map(([, name, policy]) => [name, policy]));
|
|
124
|
-
}
|