@recursica/adapter-tester 2.0.0 → 2.0.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 +1 -1
- package/dist/cli.cjs +2 -2
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +4 -4
- package/dist/cli.js.map +1 -1
- package/dist/devServer.d.ts.map +1 -1
- package/dist/harness/mantineSourceOfTruth.d.ts.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.html +37 -37
- package/dist/index.js +1 -1
- package/dist/{mantineSourceOfTruth-BiSfXKff.js → mantineSourceOfTruth-BtZhbJA_.js} +47 -26
- package/dist/{mantineSourceOfTruth-BiSfXKff.js.map → mantineSourceOfTruth-BtZhbJA_.js.map} +1 -1
- package/dist/mantineSourceOfTruth-D6tezfE1.cjs +75 -0
- package/dist/{mantineSourceOfTruth-DCoJ2dU-.cjs.map → mantineSourceOfTruth-D6tezfE1.cjs.map} +1 -1
- package/dist/testing/runVisualRegression.d.ts.map +1 -1
- package/dist/testing.cjs +1 -1
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.js +101 -77
- package/dist/testing.js.map +1 -1
- package/package.json +1 -1
- package/public/index.html +37 -37
- package/dist/mantineSourceOfTruth-DCoJ2dU-.cjs +0 -54
package/dist/testing.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { test as
|
|
2
|
-
import
|
|
3
|
-
import { PNG as
|
|
4
|
-
import
|
|
5
|
-
import { g as
|
|
6
|
-
const
|
|
1
|
+
import { test as x, expect as U } from "@playwright/test";
|
|
2
|
+
import z from "pixelmatch";
|
|
3
|
+
import { PNG as m } from "pngjs";
|
|
4
|
+
import w from "node:fs";
|
|
5
|
+
import { g as I } from "./config-CDxTeSAY.js";
|
|
6
|
+
const R = ["Theme", "Tokens", "Introduction"], E = [
|
|
7
7
|
"color",
|
|
8
8
|
"background-color",
|
|
9
9
|
"padding-top",
|
|
@@ -25,23 +25,23 @@ const z = ["Theme", "Tokens", "Introduction"], j = [
|
|
|
25
25
|
"gap",
|
|
26
26
|
"height"
|
|
27
27
|
];
|
|
28
|
-
function
|
|
28
|
+
function C(t) {
|
|
29
29
|
return t.toLowerCase().replace(/[^a-z0-9]+/g, "-");
|
|
30
30
|
}
|
|
31
|
-
async function
|
|
32
|
-
let
|
|
31
|
+
async function A(t, e) {
|
|
32
|
+
let s;
|
|
33
33
|
try {
|
|
34
34
|
const i = await fetch(`${t.url}/index.json`);
|
|
35
35
|
if (!i.ok)
|
|
36
36
|
throw new Error(
|
|
37
37
|
`Failed to fetch Storybook index: ${i.statusText}`
|
|
38
38
|
);
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
(
|
|
42
|
-
(
|
|
39
|
+
const a = (await i.json()).entries || {};
|
|
40
|
+
s = Object.values(a).filter(
|
|
41
|
+
(n) => n.type === "story" && !e.some(
|
|
42
|
+
(d) => n.title === d || n.title.startsWith(`${d}/`)
|
|
43
43
|
)
|
|
44
|
-
),
|
|
44
|
+
), s.sort((n, d) => n.id.localeCompare(d.id));
|
|
45
45
|
} catch (i) {
|
|
46
46
|
throw console.error(
|
|
47
47
|
"Failed to load Storybook index from",
|
|
@@ -51,97 +51,121 @@ async function R(t, r) {
|
|
|
51
51
|
`Storybook source of truth "${t.name}" is not responsive or index.json is missing. Please ensure its Storybook is running.`
|
|
52
52
|
);
|
|
53
53
|
}
|
|
54
|
-
return
|
|
54
|
+
return s;
|
|
55
55
|
}
|
|
56
|
-
async function
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
56
|
+
async function B(t) {
|
|
57
|
+
try {
|
|
58
|
+
const e = await fetch(`${t.url}/index.json`);
|
|
59
|
+
if (!e.ok)
|
|
60
|
+
return console.warn(
|
|
61
|
+
`Could not load Storybook index from target "${t.name}" (${e.status} ${e.statusText}) — every story will be reported as skipped for this target.`
|
|
62
|
+
), /* @__PURE__ */ new Set();
|
|
63
|
+
const i = (await e.json()).entries || {}, g = Object.values(i).filter((a) => a.type === "story").map((a) => a.id);
|
|
64
|
+
return new Set(g);
|
|
65
|
+
} catch (e) {
|
|
66
|
+
return console.warn(
|
|
67
|
+
`Could not load Storybook index from target "${t.name}" — every story will be reported as skipped for this target.`,
|
|
68
|
+
e
|
|
69
|
+
), /* @__PURE__ */ new Set();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
async function H(t) {
|
|
73
|
+
const e = I(t), s = t.targets.filter((r) => !r.sourceOfTruth), i = t.excludeTitlePrefixes ?? R, g = t.relaxedThresholdStoryIds ?? [], a = t.relaxedThresholdPixels, n = await A(e, i), d = /* @__PURE__ */ new Map();
|
|
74
|
+
for (const r of s)
|
|
75
|
+
d.set(r.name, await B(r));
|
|
76
|
+
for (const r of s) {
|
|
77
|
+
const N = d.get(r.name) ?? /* @__PURE__ */ new Set();
|
|
78
|
+
x.describe(`${e.name} vs ${r.name} — Dynamic Visual Regression Suite`, () => {
|
|
79
|
+
for (const o of n)
|
|
80
|
+
x(`Visual regression for: ${o.title} - ${o.name} (${o.id})`, async ({
|
|
81
|
+
browser: O
|
|
82
|
+
}, h) => {
|
|
83
|
+
x.skip(
|
|
84
|
+
!N.has(o.id),
|
|
85
|
+
`${r.name} has no story with id "${o.id}" yet — not yet implemented, not a failure.`
|
|
86
|
+
);
|
|
87
|
+
const c = await O.newPage(), l = await O.newPage();
|
|
88
|
+
await c.setViewportSize({ width: 800, height: 600 }), await l.setViewportSize({ width: 800, height: 600 }), await Promise.all([
|
|
89
|
+
c.goto(
|
|
90
|
+
`${e.url}/iframe.html?id=${o.id}&viewMode=story`,
|
|
68
91
|
{ waitUntil: "networkidle" }
|
|
69
92
|
),
|
|
70
|
-
|
|
71
|
-
`${
|
|
93
|
+
l.goto(
|
|
94
|
+
`${r.url}/iframe.html?id=${o.id}&viewMode=story`,
|
|
72
95
|
{ waitUntil: "networkidle" }
|
|
73
96
|
)
|
|
74
97
|
]), await Promise.all([
|
|
75
|
-
|
|
76
|
-
|
|
98
|
+
c.waitForSelector("#storybook-root"),
|
|
99
|
+
l.waitForSelector("#storybook-root")
|
|
77
100
|
]), await Promise.all([
|
|
78
|
-
|
|
79
|
-
|
|
101
|
+
c.waitForTimeout(300),
|
|
102
|
+
l.waitForTimeout(300)
|
|
80
103
|
]);
|
|
81
|
-
const
|
|
82
|
-
function
|
|
83
|
-
const
|
|
84
|
-
tag:
|
|
104
|
+
const k = ($) => {
|
|
105
|
+
function M(T) {
|
|
106
|
+
const b = {
|
|
107
|
+
tag: T.tagName.toLowerCase(),
|
|
85
108
|
styles: {},
|
|
86
109
|
children: []
|
|
87
|
-
},
|
|
88
|
-
for (const
|
|
89
|
-
|
|
90
|
-
for (const
|
|
91
|
-
|
|
92
|
-
return
|
|
110
|
+
}, _ = window.getComputedStyle(T);
|
|
111
|
+
for (const y of $)
|
|
112
|
+
b.styles[y] = _.getPropertyValue(y);
|
|
113
|
+
for (const y of Array.from(T.children))
|
|
114
|
+
b.children.push(M(y));
|
|
115
|
+
return b;
|
|
93
116
|
}
|
|
94
|
-
const
|
|
95
|
-
return
|
|
96
|
-
},
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
),
|
|
100
|
-
|
|
101
|
-
|
|
117
|
+
const D = document.querySelector("#storybook-root");
|
|
118
|
+
return D ? M(D) : null;
|
|
119
|
+
}, P = await c.evaluate(
|
|
120
|
+
k,
|
|
121
|
+
E
|
|
122
|
+
), F = await l.evaluate(
|
|
123
|
+
k,
|
|
124
|
+
E
|
|
102
125
|
);
|
|
103
|
-
await
|
|
104
|
-
body: JSON.stringify(O, null, 2),
|
|
105
|
-
contentType: "application/json"
|
|
106
|
-
}), await l.attach(`${e.name} DOM Tree JSON`, {
|
|
126
|
+
await h.attach(`${e.name} DOM Tree JSON`, {
|
|
107
127
|
body: JSON.stringify(P, null, 2),
|
|
108
128
|
contentType: "application/json"
|
|
129
|
+
}), await h.attach(`${r.name} DOM Tree JSON`, {
|
|
130
|
+
body: JSON.stringify(F, null, 2),
|
|
131
|
+
contentType: "application/json"
|
|
109
132
|
});
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
`/tmp/${o.id}-${
|
|
113
|
-
JSON.stringify(O, null, 2)
|
|
114
|
-
), g.writeFileSync(
|
|
115
|
-
`/tmp/${o.id}-${k}DOM.json`,
|
|
133
|
+
const j = C(e.name), v = C(r.name);
|
|
134
|
+
w.writeFileSync(
|
|
135
|
+
`/tmp/${o.id}-${j}DOM.json`,
|
|
116
136
|
JSON.stringify(P, null, 2)
|
|
137
|
+
), w.writeFileSync(
|
|
138
|
+
`/tmp/${o.id}-${v}DOM.json`,
|
|
139
|
+
JSON.stringify(F, null, 2)
|
|
117
140
|
);
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
141
|
+
const f = await c.screenshot(), p = await l.screenshot(), u = m.sync.read(f), L = m.sync.read(p), S = new m({ width: u.width, height: u.height }), V = z(
|
|
142
|
+
u.data,
|
|
143
|
+
L.data,
|
|
144
|
+
S.data,
|
|
145
|
+
u.width,
|
|
146
|
+
u.height,
|
|
124
147
|
{ threshold: 0.1 }
|
|
125
148
|
);
|
|
126
|
-
|
|
127
|
-
body:
|
|
149
|
+
w.writeFileSync(`/tmp/${o.id}-diff.png`, m.sync.write(S)), w.writeFileSync(`/tmp/${o.id}-${j}.png`, f), w.writeFileSync(`/tmp/${o.id}-${v}.png`, p), await h.attach("Visual Diff Overlay", {
|
|
150
|
+
body: m.sync.write(S),
|
|
128
151
|
contentType: "image/png"
|
|
129
|
-
}), await
|
|
130
|
-
`${
|
|
152
|
+
}), await h.attach(
|
|
153
|
+
`${e.name} Screenshot (Source of Truth)`,
|
|
131
154
|
{
|
|
132
|
-
body:
|
|
155
|
+
body: f,
|
|
133
156
|
contentType: "image/png"
|
|
134
157
|
}
|
|
135
|
-
), await
|
|
136
|
-
body:
|
|
158
|
+
), await h.attach(`${r.name} Screenshot (Target)`, {
|
|
159
|
+
body: p,
|
|
137
160
|
contentType: "image/png"
|
|
138
161
|
});
|
|
139
|
-
const
|
|
140
|
-
|
|
162
|
+
const J = a !== void 0 && g.some(($) => o.id.startsWith($)) ? a : t.diffThresholdPixels;
|
|
163
|
+
U.soft(V).toBeLessThan(J);
|
|
141
164
|
});
|
|
142
165
|
});
|
|
166
|
+
}
|
|
143
167
|
}
|
|
144
168
|
export {
|
|
145
|
-
|
|
169
|
+
H as runVisualRegression
|
|
146
170
|
};
|
|
147
171
|
//# sourceMappingURL=testing.js.map
|
package/dist/testing.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"testing.js","sources":["../src/testing/runVisualRegression.ts"],"sourcesContent":["import { test, expect } from \"@playwright/test\";\nimport pixelmatch from \"pixelmatch\";\nimport { PNG } from \"pngjs\";\nimport fs from \"node:fs\";\nimport type { AdapterTarget, AdapterTesterConfig } from \"../config.js\";\nimport { getSourceOfTruth } from \"../config.js\";\n\nconst DEFAULT_EXCLUDE_TITLE_PREFIXES = [\"Theme\", \"Tokens\", \"Introduction\"];\n\nconst COMMON_STYLES_TO_CAPTURE = [\n \"color\",\n \"background-color\",\n \"padding-top\",\n \"padding-right\",\n \"padding-bottom\",\n \"padding-left\",\n \"margin-top\",\n \"margin-right\",\n \"margin-bottom\",\n \"margin-left\",\n \"border-top-left-radius\",\n \"border-top-right-radius\",\n \"border-bottom-left-radius\",\n \"border-bottom-right-radius\",\n \"font-size\",\n \"font-weight\",\n \"display\",\n \"flex-direction\",\n \"gap\",\n \"height\",\n];\n\ninterface StorybookEntry {\n type: string;\n id: string;\n name: string;\n title: string;\n}\n\nfunction slug(name: string): string {\n return name.toLowerCase().replace(/[^a-z0-9]+/g, \"-\");\n}\n\nasync function fetchStories(\n sourceOfTruth: AdapterTarget,\n excludeTitlePrefixes: string[],\n): Promise<StorybookEntry[]> {\n let stories: StorybookEntry[];\n try {\n const response = await fetch(`${sourceOfTruth.url}/index.json`);\n if (!response.ok) {\n throw new Error(\n `Failed to fetch Storybook index: ${response.statusText}`,\n );\n }\n const data = (await response.json()) as any;\n const entries = data.entries || {};\n stories = Object.values(entries).filter(\n (entry: any) =>\n entry.type === \"story\" &&\n !excludeTitlePrefixes.some(\n (prefix) =>\n entry.title === prefix || entry.title.startsWith(`${prefix}/`),\n ),\n ) as StorybookEntry[];\n stories.sort((a, b) => a.id.localeCompare(b.id));\n } catch (error) {\n console.error(\n \"Failed to load Storybook index from\",\n `${sourceOfTruth.url}/index.json`,\n error,\n );\n throw new Error(\n `Storybook source of truth \"${sourceOfTruth.name}\" is not responsive or index.json is missing. Please ensure its Storybook is running.`,\n );\n }\n return stories;\n}\n\n/**\n * Defines a Playwright suite that diffs every target in `config.targets`\n * against `config.targets`' single `sourceOfTruth` entry, story by story.\n * Call this with a top-level `await` from a Playwright `*.spec.ts` file — it\n * calls `test.describe` at module scope, so it must run inside Playwright's\n * test runner during test-graph compilation.\n */\nexport async function runVisualRegression(\n config: AdapterTesterConfig,\n): Promise<void> {\n const sourceOfTruth = getSourceOfTruth(config);\n const targets = config.targets.filter((target) => !target.sourceOfTruth);\n const excludeTitlePrefixes =\n config.excludeTitlePrefixes ?? DEFAULT_EXCLUDE_TITLE_PREFIXES;\n const relaxedThresholdStoryIds = config.relaxedThresholdStoryIds ?? [];\n const relaxedThresholdPixels = config.relaxedThresholdPixels;\n\n // Fetched once at test-graph compilation time and shared by every target's\n // describe block below.\n const stories = await fetchStories(sourceOfTruth, excludeTitlePrefixes);\n\n for (const target of targets) {\n test.describe(`${sourceOfTruth.name} vs ${target.name} — Dynamic Visual Regression Suite`, () => {\n for (const story of stories) {\n test(`Visual regression for: ${story.title} - ${story.name} (${story.id})`, async ({\n browser,\n }, testInfo) => {\n const sourcePage = await browser.newPage();\n const targetPage = await browser.newPage();\n\n await sourcePage.setViewportSize({ width: 800, height: 600 });\n await targetPage.setViewportSize({ width: 800, height: 600 });\n\n await Promise.all([\n sourcePage.goto(\n `${sourceOfTruth.url}/iframe.html?id=${story.id}&viewMode=story`,\n { waitUntil: \"networkidle\" },\n ),\n targetPage.goto(\n `${target.url}/iframe.html?id=${story.id}&viewMode=story`,\n { waitUntil: \"networkidle\" },\n ),\n ]);\n\n await Promise.all([\n sourcePage.waitForSelector(\"#storybook-root\"),\n targetPage.waitForSelector(\"#storybook-root\"),\n ]);\n\n await Promise.all([\n sourcePage.waitForTimeout(300),\n targetPage.waitForTimeout(300),\n ]);\n\n // 1. EXTRACT DOM STRUCTURE AND STYLES\n const extractFn = (stylesToCapture: string[]) => {\n function crawl(el: Element): any {\n const data: any = {\n tag: el.tagName.toLowerCase(),\n styles: {},\n children: [],\n };\n const computed = window.getComputedStyle(el);\n for (const prop of stylesToCapture) {\n data.styles[prop] = computed.getPropertyValue(prop);\n }\n for (const child of Array.from(el.children)) {\n data.children.push(crawl(child));\n }\n return data;\n }\n const root = document.querySelector(\"#storybook-root\");\n return root ? crawl(root) : null;\n };\n\n const sourceDOM = await sourcePage.evaluate(\n extractFn,\n COMMON_STYLES_TO_CAPTURE,\n );\n const targetDOM = await targetPage.evaluate(\n extractFn,\n COMMON_STYLES_TO_CAPTURE,\n );\n\n await testInfo.attach(`${sourceOfTruth.name} DOM Tree JSON`, {\n body: JSON.stringify(sourceDOM, null, 2),\n contentType: \"application/json\",\n });\n await testInfo.attach(`${target.name} DOM Tree JSON`, {\n body: JSON.stringify(targetDOM, null, 2),\n contentType: \"application/json\",\n });\n\n const sourceSlug = slug(sourceOfTruth.name);\n const targetSlug = slug(target.name);\n fs.writeFileSync(\n `/tmp/${story.id}-${sourceSlug}DOM.json`,\n JSON.stringify(sourceDOM, null, 2),\n );\n fs.writeFileSync(\n `/tmp/${story.id}-${targetSlug}DOM.json`,\n JSON.stringify(targetDOM, null, 2),\n );\n\n // 2. CAPTURE Headless Viewport Screenshots\n const sourceBuffer = await sourcePage.screenshot();\n const targetBuffer = await targetPage.screenshot();\n\n // 3. PIXEL-BY-PIXEL COMPARISON\n const img1 = PNG.sync.read(sourceBuffer);\n const img2 = PNG.sync.read(targetBuffer);\n const diff = new PNG({ width: img1.width, height: img1.height });\n\n const diffPixels = pixelmatch(\n img1.data,\n img2.data,\n diff.data,\n img1.width,\n img1.height,\n { threshold: 0.1 },\n );\n\n fs.writeFileSync(`/tmp/${story.id}-diff.png`, PNG.sync.write(diff));\n fs.writeFileSync(`/tmp/${story.id}-${sourceSlug}.png`, sourceBuffer);\n fs.writeFileSync(`/tmp/${story.id}-${targetSlug}.png`, targetBuffer);\n\n await testInfo.attach(\"Visual Diff Overlay\", {\n body: PNG.sync.write(diff),\n contentType: \"image/png\",\n });\n await testInfo.attach(\n `${sourceOfTruth.name} Screenshot (Source of Truth)`,\n {\n body: sourceBuffer,\n contentType: \"image/png\",\n },\n );\n await testInfo.attach(`${target.name} Screenshot (Target)`, {\n body: targetBuffer,\n contentType: \"image/png\",\n });\n\n const threshold =\n relaxedThresholdPixels !== undefined &&\n relaxedThresholdStoryIds.some((id) => story.id.startsWith(id))\n ? relaxedThresholdPixels\n : config.diffThresholdPixels;\n\n // Perform soft assertions so failures are logged but other stories continue\n expect.soft(diffPixels).toBeLessThan(threshold);\n });\n }\n });\n }\n}\n"],"names":["DEFAULT_EXCLUDE_TITLE_PREFIXES","COMMON_STYLES_TO_CAPTURE","slug","name","fetchStories","sourceOfTruth","excludeTitlePrefixes","stories","response","entries","entry","prefix","b","error","runVisualRegression","config","getSourceOfTruth","targets","target","relaxedThresholdStoryIds","relaxedThresholdPixels","test","story","browser","testInfo","sourcePage","targetPage","extractFn","stylesToCapture","crawl","el","data","computed","prop","child","root","sourceDOM","targetDOM","sourceSlug","targetSlug","fs","sourceBuffer","targetBuffer","img1","PNG","img2","diff","diffPixels","pixelmatch","threshold","id","expect"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"testing.js","sources":["../src/testing/runVisualRegression.ts"],"sourcesContent":["import { test, expect } from \"@playwright/test\";\nimport pixelmatch from \"pixelmatch\";\nimport { PNG } from \"pngjs\";\nimport fs from \"node:fs\";\nimport type { AdapterTarget, AdapterTesterConfig } from \"../config.js\";\nimport { getSourceOfTruth } from \"../config.js\";\n\nconst DEFAULT_EXCLUDE_TITLE_PREFIXES = [\"Theme\", \"Tokens\", \"Introduction\"];\n\nconst COMMON_STYLES_TO_CAPTURE = [\n \"color\",\n \"background-color\",\n \"padding-top\",\n \"padding-right\",\n \"padding-bottom\",\n \"padding-left\",\n \"margin-top\",\n \"margin-right\",\n \"margin-bottom\",\n \"margin-left\",\n \"border-top-left-radius\",\n \"border-top-right-radius\",\n \"border-bottom-left-radius\",\n \"border-bottom-right-radius\",\n \"font-size\",\n \"font-weight\",\n \"display\",\n \"flex-direction\",\n \"gap\",\n \"height\",\n];\n\ninterface StorybookEntry {\n type: string;\n id: string;\n name: string;\n title: string;\n}\n\nfunction slug(name: string): string {\n return name.toLowerCase().replace(/[^a-z0-9]+/g, \"-\");\n}\n\nasync function fetchStories(\n sourceOfTruth: AdapterTarget,\n excludeTitlePrefixes: string[],\n): Promise<StorybookEntry[]> {\n let stories: StorybookEntry[];\n try {\n const response = await fetch(`${sourceOfTruth.url}/index.json`);\n if (!response.ok) {\n throw new Error(\n `Failed to fetch Storybook index: ${response.statusText}`,\n );\n }\n const data = (await response.json()) as any;\n const entries = data.entries || {};\n stories = Object.values(entries).filter(\n (entry: any) =>\n entry.type === \"story\" &&\n !excludeTitlePrefixes.some(\n (prefix) =>\n entry.title === prefix || entry.title.startsWith(`${prefix}/`),\n ),\n ) as StorybookEntry[];\n stories.sort((a, b) => a.id.localeCompare(b.id));\n } catch (error) {\n console.error(\n \"Failed to load Storybook index from\",\n `${sourceOfTruth.url}/index.json`,\n error,\n );\n throw new Error(\n `Storybook source of truth \"${sourceOfTruth.name}\" is not responsive or index.json is missing. Please ensure its Storybook is running.`,\n );\n }\n return stories;\n}\n\n/**\n * Fetches just the set of story ids a target's own Storybook actually has —\n * used to skip a comparison up front rather than let it burn the full\n * per-test timeout navigating to a story id the target doesn't implement\n * yet. A target with a partially-built component set (a fresh adapter with\n * a handful of real components and many still-stub ones, for example) is\n * the normal case, not an error, so a failed/empty fetch here degrades to\n * \"assume nothing exists yet\" (every story against this target gets\n * skipped) rather than throwing — unlike `fetchStories` above, where the\n * source of truth being unreachable really is fatal to the whole run.\n */\nasync function fetchTargetStoryIds(target: AdapterTarget): Promise<Set<string>> {\n try {\n const response = await fetch(`${target.url}/index.json`);\n if (!response.ok) {\n console.warn(\n `Could not load Storybook index from target \"${target.name}\" (${response.status} ${response.statusText}) — every story will be reported as skipped for this target.`,\n );\n return new Set();\n }\n const data = (await response.json()) as any;\n const entries = data.entries || {};\n const ids = Object.values(entries)\n .filter((entry: any) => entry.type === \"story\")\n .map((entry: any) => entry.id as string);\n return new Set(ids);\n } catch (error) {\n console.warn(\n `Could not load Storybook index from target \"${target.name}\" — every story will be reported as skipped for this target.`,\n error,\n );\n return new Set();\n }\n}\n\n/**\n * Defines a Playwright suite that diffs every target in `config.targets`\n * against `config.targets`' single `sourceOfTruth` entry, story by story.\n * Call this with a top-level `await` from a Playwright `*.spec.ts` file — it\n * calls `test.describe` at module scope, so it must run inside Playwright's\n * test runner during test-graph compilation.\n */\nexport async function runVisualRegression(\n config: AdapterTesterConfig,\n): Promise<void> {\n const sourceOfTruth = getSourceOfTruth(config);\n const targets = config.targets.filter((target) => !target.sourceOfTruth);\n const excludeTitlePrefixes =\n config.excludeTitlePrefixes ?? DEFAULT_EXCLUDE_TITLE_PREFIXES;\n const relaxedThresholdStoryIds = config.relaxedThresholdStoryIds ?? [];\n const relaxedThresholdPixels = config.relaxedThresholdPixels;\n\n // Fetched once at test-graph compilation time and shared by every target's\n // describe block below.\n const stories = await fetchStories(sourceOfTruth, excludeTitlePrefixes);\n\n // One index fetch per target, not per story — a fresh/partially-built adapter is expected to\n // be missing most of the source of truth's stories, and each miss should be a cheap, fast\n // skip rather than a full-timeout navigation to a story id that will never resolve.\n const targetStoryIds = new Map<string, Set<string>>();\n for (const target of targets) {\n targetStoryIds.set(target.name, await fetchTargetStoryIds(target));\n }\n\n for (const target of targets) {\n const storyIds = targetStoryIds.get(target.name) ?? new Set<string>();\n test.describe(`${sourceOfTruth.name} vs ${target.name} — Dynamic Visual Regression Suite`, () => {\n for (const story of stories) {\n test(`Visual regression for: ${story.title} - ${story.name} (${story.id})`, async ({\n browser,\n }, testInfo) => {\n test.skip(\n !storyIds.has(story.id),\n `${target.name} has no story with id \"${story.id}\" yet — not yet implemented, not a failure.`,\n );\n\n const sourcePage = await browser.newPage();\n const targetPage = await browser.newPage();\n\n await sourcePage.setViewportSize({ width: 800, height: 600 });\n await targetPage.setViewportSize({ width: 800, height: 600 });\n\n await Promise.all([\n sourcePage.goto(\n `${sourceOfTruth.url}/iframe.html?id=${story.id}&viewMode=story`,\n { waitUntil: \"networkidle\" },\n ),\n targetPage.goto(\n `${target.url}/iframe.html?id=${story.id}&viewMode=story`,\n { waitUntil: \"networkidle\" },\n ),\n ]);\n\n await Promise.all([\n sourcePage.waitForSelector(\"#storybook-root\"),\n targetPage.waitForSelector(\"#storybook-root\"),\n ]);\n\n await Promise.all([\n sourcePage.waitForTimeout(300),\n targetPage.waitForTimeout(300),\n ]);\n\n // 1. EXTRACT DOM STRUCTURE AND STYLES\n const extractFn = (stylesToCapture: string[]) => {\n function crawl(el: Element): any {\n const data: any = {\n tag: el.tagName.toLowerCase(),\n styles: {},\n children: [],\n };\n const computed = window.getComputedStyle(el);\n for (const prop of stylesToCapture) {\n data.styles[prop] = computed.getPropertyValue(prop);\n }\n for (const child of Array.from(el.children)) {\n data.children.push(crawl(child));\n }\n return data;\n }\n const root = document.querySelector(\"#storybook-root\");\n return root ? crawl(root) : null;\n };\n\n const sourceDOM = await sourcePage.evaluate(\n extractFn,\n COMMON_STYLES_TO_CAPTURE,\n );\n const targetDOM = await targetPage.evaluate(\n extractFn,\n COMMON_STYLES_TO_CAPTURE,\n );\n\n await testInfo.attach(`${sourceOfTruth.name} DOM Tree JSON`, {\n body: JSON.stringify(sourceDOM, null, 2),\n contentType: \"application/json\",\n });\n await testInfo.attach(`${target.name} DOM Tree JSON`, {\n body: JSON.stringify(targetDOM, null, 2),\n contentType: \"application/json\",\n });\n\n const sourceSlug = slug(sourceOfTruth.name);\n const targetSlug = slug(target.name);\n fs.writeFileSync(\n `/tmp/${story.id}-${sourceSlug}DOM.json`,\n JSON.stringify(sourceDOM, null, 2),\n );\n fs.writeFileSync(\n `/tmp/${story.id}-${targetSlug}DOM.json`,\n JSON.stringify(targetDOM, null, 2),\n );\n\n // 2. CAPTURE Headless Viewport Screenshots\n const sourceBuffer = await sourcePage.screenshot();\n const targetBuffer = await targetPage.screenshot();\n\n // 3. PIXEL-BY-PIXEL COMPARISON\n const img1 = PNG.sync.read(sourceBuffer);\n const img2 = PNG.sync.read(targetBuffer);\n const diff = new PNG({ width: img1.width, height: img1.height });\n\n const diffPixels = pixelmatch(\n img1.data,\n img2.data,\n diff.data,\n img1.width,\n img1.height,\n { threshold: 0.1 },\n );\n\n fs.writeFileSync(`/tmp/${story.id}-diff.png`, PNG.sync.write(diff));\n fs.writeFileSync(`/tmp/${story.id}-${sourceSlug}.png`, sourceBuffer);\n fs.writeFileSync(`/tmp/${story.id}-${targetSlug}.png`, targetBuffer);\n\n await testInfo.attach(\"Visual Diff Overlay\", {\n body: PNG.sync.write(diff),\n contentType: \"image/png\",\n });\n await testInfo.attach(\n `${sourceOfTruth.name} Screenshot (Source of Truth)`,\n {\n body: sourceBuffer,\n contentType: \"image/png\",\n },\n );\n await testInfo.attach(`${target.name} Screenshot (Target)`, {\n body: targetBuffer,\n contentType: \"image/png\",\n });\n\n const threshold =\n relaxedThresholdPixels !== undefined &&\n relaxedThresholdStoryIds.some((id) => story.id.startsWith(id))\n ? relaxedThresholdPixels\n : config.diffThresholdPixels;\n\n // Perform soft assertions so failures are logged but other stories continue\n expect.soft(diffPixels).toBeLessThan(threshold);\n });\n }\n });\n }\n}\n"],"names":["DEFAULT_EXCLUDE_TITLE_PREFIXES","COMMON_STYLES_TO_CAPTURE","slug","name","fetchStories","sourceOfTruth","excludeTitlePrefixes","stories","response","entries","entry","prefix","a","b","error","fetchTargetStoryIds","target","ids","runVisualRegression","config","getSourceOfTruth","targets","relaxedThresholdStoryIds","relaxedThresholdPixels","targetStoryIds","storyIds","test","story","browser","testInfo","sourcePage","targetPage","extractFn","stylesToCapture","crawl","el","data","computed","prop","child","root","sourceDOM","targetDOM","sourceSlug","targetSlug","fs","sourceBuffer","targetBuffer","img1","PNG","img2","diff","diffPixels","pixelmatch","threshold","id","expect"],"mappings":"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"}
|
package/package.json
CHANGED
package/public/index.html
CHANGED
|
@@ -165,11 +165,11 @@
|
|
|
165
165
|
<body>
|
|
166
166
|
<div class="pane" id="leftPane">
|
|
167
167
|
<div class="header">
|
|
168
|
-
<span id="
|
|
169
|
-
<span class="status connected" id="
|
|
168
|
+
<span id="ownLabel">Target</span>
|
|
169
|
+
<span class="status connected" id="ownStatus">Connected & Syncing</span>
|
|
170
170
|
</div>
|
|
171
|
-
<!-- Load
|
|
172
|
-
<iframe id="
|
|
171
|
+
<!-- Load this project's own Storybook via the SAME ORIGIN proxy -->
|
|
172
|
+
<iframe id="own" src="/?path=/story/ui-kit-slider--default"></iframe>
|
|
173
173
|
</div>
|
|
174
174
|
|
|
175
175
|
<div class="divider" id="vDivider"></div>
|
|
@@ -177,13 +177,13 @@
|
|
|
177
177
|
<div class="pane" id="rightPane">
|
|
178
178
|
<div class="right-top" id="rightTop">
|
|
179
179
|
<div class="header">
|
|
180
|
-
<span id="
|
|
181
|
-
<span class="status connected" id="
|
|
180
|
+
<span id="sourceOfTruthLabel">Source of Truth</span>
|
|
181
|
+
<span class="status connected" id="sourceOfTruthStatus">Ready</span>
|
|
182
182
|
</div>
|
|
183
|
-
<!-- Spacer to align the view with the
|
|
183
|
+
<!-- Spacer to align the view with the own Storybook's header (40px) -->
|
|
184
184
|
<div style="height: 40px; flex: 0 0 40px; background-color: #fff; border-bottom: 1px solid #e6e6e6; box-sizing: border-box;"></div>
|
|
185
|
-
<!-- Load the bare
|
|
186
|
-
<iframe id="
|
|
185
|
+
<!-- Load the bare source-of-truth component preview -->
|
|
186
|
+
<iframe id="sourceOfTruth" src=""></iframe>
|
|
187
187
|
</div>
|
|
188
188
|
|
|
189
189
|
<div class="h-divider" id="hDivider"></div>
|
|
@@ -214,13 +214,13 @@
|
|
|
214
214
|
|
|
215
215
|
<script>
|
|
216
216
|
// Injected by the dev server — see src/devServer.ts.
|
|
217
|
-
const devConfig = window.__ADAPTER_TESTER__ || { sourceOfTruthName: 'Source of Truth',
|
|
217
|
+
const devConfig = window.__ADAPTER_TESTER__ || { ownName: 'Target', sourceOfTruthName: 'Source of Truth', sourceOfTruthPort: 6011 };
|
|
218
|
+
document.getElementById('ownLabel').textContent = devConfig.ownName;
|
|
218
219
|
document.getElementById('sourceOfTruthLabel').textContent = `${devConfig.sourceOfTruthName} (Source of Truth)`;
|
|
219
|
-
document.getElementById('targetLabel').textContent = devConfig.targetName;
|
|
220
220
|
|
|
221
|
-
const
|
|
222
|
-
const
|
|
223
|
-
const
|
|
221
|
+
const ownIframe = document.getElementById('own');
|
|
222
|
+
const sourceOfTruthIframe = document.getElementById('sourceOfTruth');
|
|
223
|
+
const sourceOfTruthStatus = document.getElementById('sourceOfTruthStatus');
|
|
224
224
|
|
|
225
225
|
// Sync initial iframe sources with outer URL if params exist, else fall
|
|
226
226
|
// back to a sensible default story so both panes load something on open.
|
|
@@ -228,17 +228,17 @@
|
|
|
228
228
|
const initialId = outerParams.get('id') || 'ui-kit-slider--default';
|
|
229
229
|
const initialArgs = outerParams.get('args');
|
|
230
230
|
|
|
231
|
-
let
|
|
231
|
+
let ownSrc = `/?path=/story/${initialId}`;
|
|
232
232
|
if (initialArgs) {
|
|
233
|
-
|
|
233
|
+
ownSrc += `&args=${initialArgs}`;
|
|
234
234
|
}
|
|
235
|
-
|
|
235
|
+
ownIframe.src = ownSrc;
|
|
236
236
|
|
|
237
|
-
let
|
|
237
|
+
let sotSrc = `http://localhost:${devConfig.sourceOfTruthPort}/iframe.html?id=${initialId}&viewMode=story`;
|
|
238
238
|
if (initialArgs) {
|
|
239
|
-
|
|
239
|
+
sotSrc += `&args=${initialArgs}`;
|
|
240
240
|
}
|
|
241
|
-
|
|
241
|
+
sourceOfTruthIframe.src = sotSrc;
|
|
242
242
|
|
|
243
243
|
// Resizers
|
|
244
244
|
const vDivider = document.getElementById('vDivider');
|
|
@@ -263,8 +263,8 @@
|
|
|
263
263
|
vDivider.addEventListener('mousedown', (e) => {
|
|
264
264
|
isVResizing = true;
|
|
265
265
|
document.body.style.cursor = 'col-resize';
|
|
266
|
-
|
|
267
|
-
|
|
266
|
+
ownIframe.style.pointerEvents = 'none';
|
|
267
|
+
sourceOfTruthIframe.style.pointerEvents = 'none';
|
|
268
268
|
});
|
|
269
269
|
|
|
270
270
|
document.addEventListener('mousemove', (e) => {
|
|
@@ -288,8 +288,8 @@
|
|
|
288
288
|
isVResizing = false;
|
|
289
289
|
isHResizing = false;
|
|
290
290
|
document.body.style.cursor = 'default';
|
|
291
|
-
|
|
292
|
-
|
|
291
|
+
ownIframe.style.pointerEvents = 'auto';
|
|
292
|
+
sourceOfTruthIframe.style.pointerEvents = 'auto';
|
|
293
293
|
}
|
|
294
294
|
});
|
|
295
295
|
|
|
@@ -298,8 +298,8 @@
|
|
|
298
298
|
hDivider.addEventListener('mousedown', (e) => {
|
|
299
299
|
isHResizing = true;
|
|
300
300
|
document.body.style.cursor = 'row-resize';
|
|
301
|
-
|
|
302
|
-
|
|
301
|
+
ownIframe.style.pointerEvents = 'none';
|
|
302
|
+
sourceOfTruthIframe.style.pointerEvents = 'none';
|
|
303
303
|
});
|
|
304
304
|
|
|
305
305
|
// Auto-save notes logic
|
|
@@ -406,7 +406,7 @@
|
|
|
406
406
|
let lastUrl = '';
|
|
407
407
|
setInterval(() => {
|
|
408
408
|
try {
|
|
409
|
-
const currentUrl =
|
|
409
|
+
const currentUrl = ownIframe.contentWindow.location.href;
|
|
410
410
|
|
|
411
411
|
if (currentUrl && currentUrl !== lastUrl && currentUrl !== 'about:blank') {
|
|
412
412
|
lastUrl = currentUrl;
|
|
@@ -440,19 +440,19 @@
|
|
|
440
440
|
window.history.replaceState({}, '', outerUrl.pathname + outerUrl.search);
|
|
441
441
|
}
|
|
442
442
|
|
|
443
|
-
let
|
|
443
|
+
let sotUrl = `http://localhost:${devConfig.sourceOfTruthPort}/iframe.html?id=${id}&viewMode=story`;
|
|
444
444
|
if (args) {
|
|
445
|
-
|
|
445
|
+
sotUrl += `&args=${args}`;
|
|
446
446
|
}
|
|
447
|
-
|
|
448
|
-
if (
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
447
|
+
|
|
448
|
+
if (sourceOfTruthIframe.src !== sotUrl) {
|
|
449
|
+
sourceOfTruthStatus.textContent = 'Updating...';
|
|
450
|
+
sourceOfTruthStatus.classList.remove('connected');
|
|
451
|
+
sourceOfTruthIframe.src = sotUrl;
|
|
452
|
+
|
|
453
453
|
setTimeout(() => {
|
|
454
|
-
|
|
455
|
-
|
|
454
|
+
sourceOfTruthStatus.textContent = 'Synced';
|
|
455
|
+
sourceOfTruthStatus.classList.add('connected');
|
|
456
456
|
}, 500);
|
|
457
457
|
}
|
|
458
458
|
}
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
"use strict";const o=require("node:fs"),t=require("node:path"),i="^8.0.0",s="^10.3.3",n="^19.0.0",m={"@storybook/addon-docs":s,"@storybook/addon-a11y":s,"storybook-dark-mode":"^5.0.0"},p={"react-markdown":"^10.1.0"};function l(e){return{name:"adapter-tester-mantine-source-of-truth-harness",private:!0,type:"module",scripts:{storybook:`storybook dev -p ${e.port}`},dependencies:{"@recursica/mantine-adapter":e.mantineAdapterVersion,"@recursica/storybook-template":e.storybookTemplateVersion,"@recursica/official-release":"latest","@recursica/adapter-common":"latest","@mantine/core":i,"@mantine/dates":i,react:n,"react-dom":n,storybook:s,"@storybook/react-vite":s,...m,...p}}}const f=`import { createMainConfig } from "@recursica/storybook-template/main";
|
|
2
|
-
|
|
3
|
-
const config = createMainConfig({
|
|
4
|
-
stories: [
|
|
5
|
-
"../node_modules/@recursica/mantine-adapter/src/**/*.stories.@(js|jsx|mjs|ts|tsx)",
|
|
6
|
-
],
|
|
7
|
-
enableCORS: true,
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
// react-docgen-typescript can't resolve a TS project for a config file living
|
|
11
|
-
// in .storybook/ when the component source it's docgen'ing lives three
|
|
12
|
-
// directories down inside node_modules — it throws "Cannot read properties
|
|
13
|
-
// of undefined (reading 'fileExists')", which surfaces as a plain 404 on
|
|
14
|
-
// preview.tsx. Docgen only powers Storybook's Controls/Docs tables, which
|
|
15
|
-
// this harness never renders, so disabling it is a safe workaround (see
|
|
16
|
-
// PROPOSAL-installed-package-harness.md, gap 3).
|
|
17
|
-
config.typescript = { ...config.typescript, reactDocgen: false };
|
|
18
|
-
|
|
19
|
-
export default config;
|
|
20
|
-
`,y=`import type { Preview } from "@storybook/react-vite";
|
|
21
|
-
import { createPreviewConfig } from "@recursica/storybook-template/preview";
|
|
22
|
-
import { MantineProvider } from "@mantine/core";
|
|
23
|
-
import "@mantine/core/styles.css";
|
|
24
|
-
import "@mantine/dates/styles.css";
|
|
25
|
-
import "@recursica/adapter-common/style.css";
|
|
26
|
-
import "@recursica/official-release/recursica_variables_scoped.css";
|
|
27
|
-
import recursicaTokens from "@recursica/official-release/recursica_tokens.json";
|
|
28
|
-
import recursicaBrand from "@recursica/official-release/recursica_brand.json";
|
|
29
|
-
import recursicaUIKit from "@recursica/official-release/recursica_ui-kit.json";
|
|
30
|
-
|
|
31
|
-
const basePreview = createPreviewConfig({
|
|
32
|
-
defaultTheme: "light",
|
|
33
|
-
recursicaTokensJsonPath: recursicaTokens,
|
|
34
|
-
recursicaBrandJsonPath: recursicaBrand,
|
|
35
|
-
recursicaUIKitJsonPath: recursicaUIKit,
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
const preview: Preview = {
|
|
39
|
-
...basePreview,
|
|
40
|
-
decorators: [
|
|
41
|
-
(Story) => (
|
|
42
|
-
<MantineProvider>
|
|
43
|
-
<Story />
|
|
44
|
-
</MantineProvider>
|
|
45
|
-
),
|
|
46
|
-
...(basePreview.decorators || []),
|
|
47
|
-
],
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
export default preview;
|
|
51
|
-
`;function a(e){const{dir:r,port:c,mantineAdapterVersion:u="latest",storybookTemplateVersion:d="latest"}=e;return o.mkdirSync(t.join(r,".storybook"),{recursive:!0}),o.writeFileSync(t.join(r,"package.json"),JSON.stringify(l({mantineAdapterVersion:u,storybookTemplateVersion:d,port:c}),null,2)+`
|
|
52
|
-
`),o.writeFileSync(t.join(r,".storybook/main.ts"),f),o.writeFileSync(t.join(r,".storybook/preview.tsx"),y),o.writeFileSync(t.join(r,".gitignore"),`node_modules
|
|
53
|
-
`),r}function k(e){const r=a(e);return{command:"npm install --no-audit --no-fund && npm run storybook",port:e.port,cwd:r,reuseExistingServer:!process.env.CI,timeout:180*1e3}}exports.mantineSourceOfTruthWebServer=k;exports.scaffoldMantineSourceOfTruthHarness=a;
|
|
54
|
-
//# sourceMappingURL=mantineSourceOfTruth-DCoJ2dU-.cjs.map
|