@vizejs/vite-plugin-musea 0.58.0 → 0.60.0

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 (126) hide show
  1. package/README.md +5 -3
  2. package/dist/a11y/index.d.mts +2 -0
  3. package/dist/a11y/index.mjs +2 -0
  4. package/dist/a11y-DNCg2qCB.mjs +318 -0
  5. package/dist/a11y-DNCg2qCB.mjs.map +1 -0
  6. package/dist/autogen/index.d.mts +66 -0
  7. package/dist/autogen/index.d.mts.map +1 -0
  8. package/dist/autogen/index.mjs +2 -0
  9. package/dist/autogen-3-y1d0ou.mjs +213 -0
  10. package/dist/autogen-3-y1d0ou.mjs.map +1 -0
  11. package/dist/cli/index.d.mts +40 -0
  12. package/dist/cli/index.d.mts.map +1 -0
  13. package/dist/cli/index.mjs +407 -0
  14. package/dist/cli/index.mjs.map +1 -0
  15. package/dist/gallery/assets/abap-DVwoIrM0.js +1 -0
  16. package/dist/gallery/assets/apex-DnfrpC_v.js +1 -0
  17. package/dist/gallery/assets/azcli-CE6n6ErR.js +1 -0
  18. package/dist/gallery/assets/bat-ainFW1qj.js +1 -0
  19. package/dist/gallery/assets/bicep-Lzdk7NqX.js +2 -0
  20. package/dist/gallery/assets/cameligo-BjyZ5cgY.js +1 -0
  21. package/dist/gallery/assets/clojure-B-6owjux.js +1 -0
  22. package/dist/gallery/assets/codicon-DCmgc-ay.ttf +0 -0
  23. package/dist/gallery/assets/coffee-npIvPqmH.js +1 -0
  24. package/dist/gallery/assets/cpp--A8GPZYM.js +1 -0
  25. package/dist/gallery/assets/csharp-Daa9qY-p.js +1 -0
  26. package/dist/gallery/assets/csp-N0NfYrCc.js +1 -0
  27. package/dist/gallery/assets/css-CUssoHYv.js +3 -0
  28. package/dist/gallery/assets/css.worker-BXrDisZh.js +88 -0
  29. package/dist/gallery/assets/cssMode-CZi1tKoG.js +4 -0
  30. package/dist/gallery/assets/cypher-CmQpMFeE.js +1 -0
  31. package/dist/gallery/assets/dart-DOjZJNmR.js +1 -0
  32. package/dist/gallery/assets/dockerfile-CxIfCbxU.js +1 -0
  33. package/dist/gallery/assets/ecl-AnA3JP56.js +1 -0
  34. package/dist/gallery/assets/editor-B55U_qvj.css +1 -0
  35. package/dist/gallery/assets/editor-F8AxQWwE.css +1 -0
  36. package/dist/gallery/assets/editor.api-BwuW2BPp.js +644 -0
  37. package/dist/gallery/assets/editor.main-CLp9tio8.js +63 -0
  38. package/dist/gallery/assets/editor.worker-B0BIIYAR.js +12 -0
  39. package/dist/gallery/assets/elixir-D-N3eh22.js +1 -0
  40. package/dist/gallery/assets/flow9-BR6FT9qg.js +1 -0
  41. package/dist/gallery/assets/freemarker2-6ytRrkSy.js +3 -0
  42. package/dist/gallery/assets/fsharp-DR0IQ95q.js +1 -0
  43. package/dist/gallery/assets/go-B27OpVON.js +1 -0
  44. package/dist/gallery/assets/graphql-Cw7HtomI.js +1 -0
  45. package/dist/gallery/assets/handlebars-CyBWisCf.js +1 -0
  46. package/dist/gallery/assets/hcl-CDDd0gYG.js +1 -0
  47. package/dist/gallery/assets/html-DIa-9Pkf.js +1 -0
  48. package/dist/gallery/assets/html.worker-_AJvPiQl.js +495 -0
  49. package/dist/gallery/assets/htmlMode-icpREKp1.js +4 -0
  50. package/dist/gallery/assets/index-BpuWoCWJ.js +63 -0
  51. package/dist/gallery/assets/index-GLIbHWvP.css +1 -0
  52. package/dist/gallery/assets/ini-ChiSjCUM.js +1 -0
  53. package/dist/gallery/assets/java-CKVuuvX6.js +1 -0
  54. package/dist/gallery/assets/javascript-RCTvBrji.js +1 -0
  55. package/dist/gallery/assets/json.worker-3yqvOk70.js +51 -0
  56. package/dist/gallery/assets/jsonMode-qd4RG8Ju.js +10 -0
  57. package/dist/gallery/assets/julia-BcKGx43g.js +1 -0
  58. package/dist/gallery/assets/kotlin-C7EpOAJu.js +1 -0
  59. package/dist/gallery/assets/less-BFpYPxgE.js +2 -0
  60. package/dist/gallery/assets/lexon-DDPF3See.js +1 -0
  61. package/dist/gallery/assets/liquid-BNx9Bbe-.js +1 -0
  62. package/dist/gallery/assets/lua-CmzM4S9z.js +1 -0
  63. package/dist/gallery/assets/m3-C75GLUav.js +1 -0
  64. package/dist/gallery/assets/markdown-B6XL0Y9j.js +1 -0
  65. package/dist/gallery/assets/mdx-83wfQOq8.js +1 -0
  66. package/dist/gallery/assets/mips-BG4Fy7Bl.js +1 -0
  67. package/dist/gallery/assets/monaco.contribution-Kdi83zyS.js +2 -0
  68. package/dist/gallery/assets/msdax-H0aqYz0U.js +1 -0
  69. package/dist/gallery/assets/mysql-CDbOhBhf.js +1 -0
  70. package/dist/gallery/assets/objective-c-DKE6-VEf.js +1 -0
  71. package/dist/gallery/assets/pascal-DBuqflGM.js +1 -0
  72. package/dist/gallery/assets/pascaligo-BVtulzHb.js +1 -0
  73. package/dist/gallery/assets/perl-xkTv78ng.js +1 -0
  74. package/dist/gallery/assets/pgsql-Cxti3J5E.js +1 -0
  75. package/dist/gallery/assets/php-Bh5BD3dg.js +1 -0
  76. package/dist/gallery/assets/pla-DSsYzlXV.js +1 -0
  77. package/dist/gallery/assets/postiats-De0qivlp.js +1 -0
  78. package/dist/gallery/assets/powerquery-KGKq89F-.js +1 -0
  79. package/dist/gallery/assets/powershell-Djwhihrv.js +1 -0
  80. package/dist/gallery/assets/protobuf-Jbp01qUU.js +2 -0
  81. package/dist/gallery/assets/pug-BntfJCN7.js +1 -0
  82. package/dist/gallery/assets/python-3KqxExGZ.js +1 -0
  83. package/dist/gallery/assets/qsharp-CHH1r_aq.js +1 -0
  84. package/dist/gallery/assets/r-BbeUcBN9.js +1 -0
  85. package/dist/gallery/assets/razor-JcmtZaHa.js +1 -0
  86. package/dist/gallery/assets/redis-DR9m_VtD.js +1 -0
  87. package/dist/gallery/assets/redshift-D97Qa-FW.js +1 -0
  88. package/dist/gallery/assets/restructuredtext-DQ1MtboI.js +1 -0
  89. package/dist/gallery/assets/ruby-ByLGeogt.js +1 -0
  90. package/dist/gallery/assets/rust-CIqtS9ON.js +1 -0
  91. package/dist/gallery/assets/sb-ByVTEZ1d.js +1 -0
  92. package/dist/gallery/assets/scala-DvkPypTh.js +1 -0
  93. package/dist/gallery/assets/scheme-CQy1Ya2H.js +1 -0
  94. package/dist/gallery/assets/scss-DLIO8qmP.js +3 -0
  95. package/dist/gallery/assets/shell-BZaILY8J.js +1 -0
  96. package/dist/gallery/assets/solidity-D80FpOWz.js +1 -0
  97. package/dist/gallery/assets/sophia-DXh1T4eB.js +1 -0
  98. package/dist/gallery/assets/sparql-DHSgmKlJ.js +1 -0
  99. package/dist/gallery/assets/sql-9GboOSCN.js +1 -0
  100. package/dist/gallery/assets/st--m1Z2h3c.js +1 -0
  101. package/dist/gallery/assets/swift-DMo7Bf1r.js +1 -0
  102. package/dist/gallery/assets/systemverilog-D6kP5wsA.js +1 -0
  103. package/dist/gallery/assets/tcl-HAhMyY2Y.js +1 -0
  104. package/dist/gallery/assets/ts.worker-B0Jjxwwp.js +51339 -0
  105. package/dist/gallery/assets/tsMode-IOAbDJ00.js +11 -0
  106. package/dist/gallery/assets/twig-RNzllx71.js +1 -0
  107. package/dist/gallery/assets/typescript-DcM7falK.js +1 -0
  108. package/dist/gallery/assets/typespec-DeyXqKVJ.js +1 -0
  109. package/dist/gallery/assets/vb-BfpeX2r9.js +1 -0
  110. package/dist/gallery/assets/wgsl-B52428dy.js +298 -0
  111. package/dist/gallery/assets/xml-PWwG1uVM.js +1 -0
  112. package/dist/gallery/assets/yaml-1K-iqUR7.js +1 -0
  113. package/dist/gallery/index.html +19 -0
  114. package/dist/index-CoAc76Ob.d.mts +151 -0
  115. package/dist/index-CoAc76Ob.d.mts.map +1 -0
  116. package/dist/index.d.mts +253 -0
  117. package/dist/index.d.mts.map +1 -0
  118. package/dist/index.mjs +3039 -0
  119. package/dist/index.mjs.map +1 -0
  120. package/dist/vrt-CMJXvKjY.d.mts +289 -0
  121. package/dist/vrt-CMJXvKjY.d.mts.map +1 -0
  122. package/dist/vrt-CjFf5GR0.mjs +767 -0
  123. package/dist/vrt-CjFf5GR0.mjs.map +1 -0
  124. package/dist/vrt.d.mts +2 -0
  125. package/dist/vrt.mjs +2 -0
  126. package/package.json +4 -4
@@ -0,0 +1,767 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { PNG } from "pngjs";
4
+ //#region src/vrt/comparison.ts
5
+ /**
6
+ * Pixel comparison utilities for VRT.
7
+ *
8
+ * Provides PNG reading/writing, color delta calculation using YIQ color space,
9
+ * and anti-aliasing detection for pixel-level image comparison.
10
+ */
11
+ /**
12
+ * Read PNG file and return PNG object.
13
+ */
14
+ async function readPng(filepath) {
15
+ return new Promise((resolve, reject) => {
16
+ fs.createReadStream(filepath).pipe(new PNG()).on("parsed", function() {
17
+ resolve(this);
18
+ }).on("error", reject);
19
+ });
20
+ }
21
+ /**
22
+ * Write PNG object to file.
23
+ */
24
+ async function writePng(png, filepath) {
25
+ return new Promise((resolve, reject) => {
26
+ png.pack().pipe(fs.createWriteStream(filepath)).on("finish", resolve).on("error", reject);
27
+ });
28
+ }
29
+ /**
30
+ * Calculate color delta using YIQ color space.
31
+ */
32
+ function colorDelta(r1, g1, b1, a1, r2, g2, b2, a2) {
33
+ if (a1 !== 255) {
34
+ r1 = blend(r1, 255, a1 / 255);
35
+ g1 = blend(g1, 255, a1 / 255);
36
+ b1 = blend(b1, 255, a1 / 255);
37
+ }
38
+ if (a2 !== 255) {
39
+ r2 = blend(r2, 255, a2 / 255);
40
+ g2 = blend(g2, 255, a2 / 255);
41
+ b2 = blend(b2, 255, a2 / 255);
42
+ }
43
+ const y1 = r1 * .29889531 + g1 * .58662247 + b1 * .11448223;
44
+ const i1 = r1 * .59597799 - g1 * .2741761 - b1 * .32180189;
45
+ const q1 = r1 * .21147017 - g1 * .52261711 + b1 * .31114694;
46
+ const y2 = r2 * .29889531 + g2 * .58662247 + b2 * .11448223;
47
+ const i2 = r2 * .59597799 - g2 * .2741761 - b2 * .32180189;
48
+ const q2 = r2 * .21147017 - g2 * .52261711 + b2 * .31114694;
49
+ const dy = y1 - y2;
50
+ const di = i1 - i2;
51
+ const dq = q1 - q2;
52
+ return dy * dy * .5053 + di * di * .299 + dq * dq * .1957;
53
+ }
54
+ /**
55
+ * Blend foreground with background using alpha.
56
+ */
57
+ function blend(fg, bg, alpha) {
58
+ return bg + (fg - bg) * alpha;
59
+ }
60
+ /**
61
+ * Check if file exists.
62
+ */
63
+ async function fileExists(filepath) {
64
+ try {
65
+ await fs.promises.access(filepath);
66
+ return true;
67
+ } catch {
68
+ return false;
69
+ }
70
+ }
71
+ /**
72
+ * Simple anti-aliasing detection.
73
+ * A pixel is likely anti-aliased if its neighbors have high contrast in opposite directions.
74
+ */
75
+ function isAntiAliased(img1, img2, x, y, width, height) {
76
+ const minX = Math.max(0, x - 1);
77
+ const maxX = Math.min(width - 1, x + 1);
78
+ const minY = Math.max(0, y - 1);
79
+ const maxY = Math.min(height - 1, y + 1);
80
+ let zeroes = 0;
81
+ let positives = 0;
82
+ let negatives = 0;
83
+ for (let ny = minY; ny <= maxY; ny++) for (let nx = minX; nx <= maxX; nx++) {
84
+ if (nx === x && ny === y) continue;
85
+ const idx = (ny * width + nx) * 4;
86
+ const delta = colorDelta(img1.data[idx], img1.data[idx + 1], img1.data[idx + 2], img1.data[idx + 3], img2.data[idx], img2.data[idx + 1], img2.data[idx + 2], img2.data[idx + 3]);
87
+ if (delta === 0) zeroes++;
88
+ else if (delta > 0) positives++;
89
+ else negatives++;
90
+ }
91
+ return zeroes > 0 && (positives > 0 || negatives > 0) && positives + negatives < 4;
92
+ }
93
+ /**
94
+ * Simple glob matching for pattern-based filtering.
95
+ */
96
+ function matchGlob(filepath, pattern) {
97
+ const regex = pattern.replace(/\./g, "\\.").replace(/\*\*/g, ".*").replace(/\*(?!\*)/g, "[^/]*");
98
+ return new RegExp(`^${regex}$`).test(filepath);
99
+ }
100
+ /**
101
+ * Escape HTML special characters.
102
+ */
103
+ function escapeHtml(str) {
104
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#x27;");
105
+ }
106
+ //#endregion
107
+ //#region src/vrt/utils.ts
108
+ /**
109
+ * Build URL for variant preview.
110
+ */
111
+ function buildVariantUrl(baseUrl, artPath, variantName) {
112
+ return `${baseUrl}/__musea__/preview?art=${encodeURIComponent(artPath)}&variant=${encodeURIComponent(variantName)}`;
113
+ }
114
+ /**
115
+ * Compute VRT summary statistics from a list of results.
116
+ */
117
+ function computeSummary(results, startTime) {
118
+ return {
119
+ total: results.length,
120
+ passed: results.filter((r) => r.passed && !r.isNew).length,
121
+ failed: results.filter((r) => !r.passed && !r.error).length,
122
+ new: results.filter((r) => r.isNew).length,
123
+ skipped: results.filter((r) => r.error).length,
124
+ duration: Date.now() - startTime
125
+ };
126
+ }
127
+ //#endregion
128
+ //#region src/vrt/runner-comparison.ts
129
+ /**
130
+ * Capture screenshot and compare with baseline.
131
+ *
132
+ * Standalone function that operates on a MuseaVrtRunner instance.
133
+ */
134
+ async function captureAndCompare(runner, art, variantName, viewport, baseUrl) {
135
+ const browser = runner.getBrowser();
136
+ if (!browser) throw new Error("VRT runner not initialized. Call init() first.");
137
+ const options = runner.getOptions();
138
+ const capture = runner.getCapture();
139
+ const comparison = runner.getComparison();
140
+ const snapshotDir = options.snapshotDir;
141
+ const snapshotName = `${path.basename(art.path, ".art.vue")}--${variantName}--${viewport.name || `${viewport.width}x${viewport.height}`}.png`;
142
+ const snapshotPath = path.join(snapshotDir, snapshotName);
143
+ const currentPath = path.join(snapshotDir, "current", snapshotName);
144
+ const diffPath = path.join(snapshotDir, "diff", snapshotName);
145
+ await fs.promises.mkdir(path.dirname(snapshotPath), { recursive: true });
146
+ await fs.promises.mkdir(path.join(snapshotDir, "current"), { recursive: true });
147
+ await fs.promises.mkdir(path.join(snapshotDir, "diff"), { recursive: true });
148
+ let context = null;
149
+ let page = null;
150
+ try {
151
+ context = await browser.newContext({
152
+ viewport: {
153
+ width: viewport.width,
154
+ height: viewport.height
155
+ },
156
+ deviceScaleFactor: viewport.deviceScaleFactor ?? 1
157
+ });
158
+ page = await context.newPage();
159
+ const variantUrl = buildVariantUrl(baseUrl, art.path, variantName);
160
+ const waitUntil = capture.waitForNetwork ? "networkidle" : "load";
161
+ await page.goto(variantUrl, { waitUntil });
162
+ await page.waitForSelector(capture.waitSelector, { timeout: 1e4 });
163
+ await page.waitForTimeout(capture.settleTime);
164
+ if (capture.hideElements.length > 0) for (const selector of capture.hideElements) await page.evaluate((sel) => {
165
+ document.querySelectorAll(sel).forEach((el) => {
166
+ el.style.visibility = "hidden";
167
+ });
168
+ }, selector);
169
+ if (capture.maskElements.length > 0) for (const selector of capture.maskElements) await page.evaluate((sel) => {
170
+ document.querySelectorAll(sel).forEach((el) => {
171
+ const htmlEl = el;
172
+ htmlEl.style.background = "#ff00ff";
173
+ htmlEl.style.color = "transparent";
174
+ htmlEl.innerHTML = "";
175
+ });
176
+ }, selector);
177
+ await page.screenshot({
178
+ path: currentPath,
179
+ fullPage: capture.fullPage
180
+ });
181
+ if (!await fileExists(snapshotPath)) {
182
+ await fs.promises.copyFile(currentPath, snapshotPath);
183
+ return {
184
+ artPath: art.path,
185
+ variantName,
186
+ viewport,
187
+ passed: true,
188
+ snapshotPath,
189
+ currentPath,
190
+ isNew: true
191
+ };
192
+ }
193
+ const comparisonResult = await compareImages(snapshotPath, currentPath, diffPath, comparison);
194
+ const passed = comparisonResult.diffPercentage <= options.threshold;
195
+ return {
196
+ artPath: art.path,
197
+ variantName,
198
+ viewport,
199
+ passed,
200
+ snapshotPath,
201
+ currentPath,
202
+ diffPath: passed ? void 0 : diffPath,
203
+ diffPercentage: comparisonResult.diffPercentage,
204
+ diffPixels: comparisonResult.diffPixels,
205
+ totalPixels: comparisonResult.totalPixels
206
+ };
207
+ } catch (error) {
208
+ return {
209
+ artPath: art.path,
210
+ variantName,
211
+ viewport,
212
+ passed: false,
213
+ snapshotPath,
214
+ error: error instanceof Error ? error.message : String(error)
215
+ };
216
+ } finally {
217
+ if (page) await page.close();
218
+ if (context) await context.close();
219
+ }
220
+ }
221
+ /**
222
+ * Compare two PNG images and generate a diff image.
223
+ * Returns pixel difference statistics.
224
+ */
225
+ async function compareImages(baselinePath, currentPath, diffPath, comparison) {
226
+ const baseline = await readPng(baselinePath);
227
+ const current = await readPng(currentPath);
228
+ if (baseline.width !== current.width || baseline.height !== current.height) {
229
+ const width = Math.max(baseline.width, current.width);
230
+ const height = Math.max(baseline.height, current.height);
231
+ const diff = new PNG({
232
+ width,
233
+ height
234
+ });
235
+ for (let i = 0; i < diff.data.length; i += 4) {
236
+ diff.data[i] = 255;
237
+ diff.data[i + 1] = 0;
238
+ diff.data[i + 2] = 0;
239
+ diff.data[i + 3] = 255;
240
+ }
241
+ await writePng(diff, diffPath);
242
+ return {
243
+ diffPixels: width * height,
244
+ totalPixels: width * height,
245
+ diffPercentage: 100
246
+ };
247
+ }
248
+ const width = baseline.width;
249
+ const height = baseline.height;
250
+ const totalPixels = width * height;
251
+ const diff = new PNG({
252
+ width,
253
+ height
254
+ });
255
+ const useAntiAliasing = comparison.antiAliasing ?? true;
256
+ const useAlpha = comparison.alpha ?? true;
257
+ const diffColor = comparison.diffColor ?? {
258
+ r: 255,
259
+ g: 0,
260
+ b: 0
261
+ };
262
+ let diffPixels = 0;
263
+ const threshold = .1;
264
+ for (let y = 0; y < height; y++) for (let x = 0; x < width; x++) {
265
+ const idx = (y * width + x) * 4;
266
+ const r1 = baseline.data[idx];
267
+ const g1 = baseline.data[idx + 1];
268
+ const b1 = baseline.data[idx + 2];
269
+ const a1 = useAlpha ? baseline.data[idx + 3] : 255;
270
+ const r2 = current.data[idx];
271
+ const g2 = current.data[idx + 1];
272
+ const b2 = current.data[idx + 2];
273
+ if (colorDelta(r1, g1, b1, a1, r2, g2, b2, useAlpha ? current.data[idx + 3] : 255) > threshold * 255 * 255) if (useAntiAliasing && isAntiAliased(baseline, current, x, y, width, height)) {
274
+ diff.data[idx] = 255;
275
+ diff.data[idx + 1] = 200;
276
+ diff.data[idx + 2] = 0;
277
+ diff.data[idx + 3] = 128;
278
+ } else {
279
+ diffPixels++;
280
+ diff.data[idx] = diffColor.r;
281
+ diff.data[idx + 1] = diffColor.g;
282
+ diff.data[idx + 2] = diffColor.b;
283
+ diff.data[idx + 3] = 255;
284
+ }
285
+ else {
286
+ const gray = Math.round((r2 + g2 + b2) / 3);
287
+ diff.data[idx] = gray;
288
+ diff.data[idx + 1] = gray;
289
+ diff.data[idx + 2] = gray;
290
+ diff.data[idx + 3] = 128;
291
+ }
292
+ }
293
+ if (diffPixels > 0) await writePng(diff, diffPath);
294
+ const diffPercentage = diffPixels / totalPixels * 100;
295
+ return {
296
+ diffPixels,
297
+ totalPixels,
298
+ diffPercentage
299
+ };
300
+ }
301
+ //#endregion
302
+ //#region src/vrt/runner.ts
303
+ /**
304
+ * VRT runner using Playwright.
305
+ */
306
+ var MuseaVrtRunner = class {
307
+ options;
308
+ capture;
309
+ comparison;
310
+ ci;
311
+ browser = null;
312
+ startTime = 0;
313
+ constructor(options = {}) {
314
+ this.options = {
315
+ snapshotDir: options.snapshotDir ?? ".vize/snapshots",
316
+ threshold: options.threshold ?? .1,
317
+ viewports: options.viewports ?? [{
318
+ width: 1280,
319
+ height: 720,
320
+ name: "desktop"
321
+ }, {
322
+ width: 375,
323
+ height: 667,
324
+ name: "mobile"
325
+ }]
326
+ };
327
+ this.capture = {
328
+ fullPage: options.capture?.fullPage ?? false,
329
+ waitForNetwork: options.capture?.waitForNetwork ?? true,
330
+ settleTime: options.capture?.settleTime ?? 100,
331
+ waitSelector: options.capture?.waitSelector ?? ".musea-variant",
332
+ hideElements: options.capture?.hideElements ?? [],
333
+ maskElements: options.capture?.maskElements ?? []
334
+ };
335
+ this.comparison = options.comparison ?? {};
336
+ this.ci = options.ci ?? {};
337
+ }
338
+ /** @internal */
339
+ getBrowser() {
340
+ return this.browser;
341
+ }
342
+ /** @internal */
343
+ getOptions() {
344
+ return this.options;
345
+ }
346
+ /** @internal */
347
+ getCapture() {
348
+ return this.capture;
349
+ }
350
+ /** @internal */
351
+ getComparison() {
352
+ return this.comparison;
353
+ }
354
+ /**
355
+ * Initialize Playwright browser.
356
+ */
357
+ async init() {
358
+ const { chromium } = await import("playwright");
359
+ this.browser = await chromium.launch({ headless: true });
360
+ this.startTime = Date.now();
361
+ }
362
+ /**
363
+ * Close browser and cleanup.
364
+ */
365
+ async close() {
366
+ if (this.browser) {
367
+ await this.browser.close();
368
+ this.browser = null;
369
+ }
370
+ }
371
+ /**
372
+ * Alias for init() - used by the plugin API.
373
+ */
374
+ async start() {
375
+ return this.init();
376
+ }
377
+ /**
378
+ * Alias for close() - used by the plugin API.
379
+ */
380
+ async stop() {
381
+ return this.close();
382
+ }
383
+ /**
384
+ * Run VRT tests for all Art files.
385
+ */
386
+ async runAllTests(artFiles, baseUrl) {
387
+ if (!this.browser) throw new Error("VRT runner not initialized. Call init() first.");
388
+ const results = [];
389
+ const retries = this.ci.retries ?? 0;
390
+ for (const art of artFiles) for (const variant of art.variants) {
391
+ if (variant.skipVrt) continue;
392
+ const viewports = variant.args?.viewport ? [variant.args.viewport] : this.options.viewports;
393
+ for (const viewport of viewports) {
394
+ let result = null;
395
+ let attempts = 0;
396
+ while (attempts <= retries) {
397
+ result = await this.captureAndCompare(art, variant.name, viewport, baseUrl);
398
+ if (result.passed || result.isNew || !result.error) break;
399
+ attempts++;
400
+ if (attempts <= retries) console.log(`[vrt] Retry ${attempts}/${retries}: ${path.basename(art.path)}/${variant.name}`);
401
+ }
402
+ if (result) results.push(result);
403
+ }
404
+ }
405
+ return results;
406
+ }
407
+ /**
408
+ * Run VRT tests - alias used by the plugin API that accepts options.
409
+ */
410
+ async runTests(artFiles, baseUrl, _options) {
411
+ const results = await this.runAllTests(artFiles, baseUrl);
412
+ if (_options?.updateSnapshots) await this.updateBaselines(results);
413
+ return results;
414
+ }
415
+ /**
416
+ * Capture screenshot and compare with baseline.
417
+ */
418
+ async captureAndCompare(art, variantName, viewport, baseUrl) {
419
+ return captureAndCompare(this, art, variantName, viewport, baseUrl);
420
+ }
421
+ /**
422
+ * Get the Playwright Page for external use (e.g., a11y auditing).
423
+ */
424
+ async createPage(viewport) {
425
+ if (!this.browser) throw new Error("VRT runner not initialized. Call init() first.");
426
+ const context = await this.browser.newContext({
427
+ viewport: {
428
+ width: viewport.width,
429
+ height: viewport.height
430
+ },
431
+ deviceScaleFactor: viewport.deviceScaleFactor ?? 1
432
+ });
433
+ return {
434
+ page: await context.newPage(),
435
+ context
436
+ };
437
+ }
438
+ /**
439
+ * Update baseline snapshots with current screenshots.
440
+ */
441
+ async updateBaselines(results) {
442
+ let updated = 0;
443
+ const snapshotDir = this.options.snapshotDir;
444
+ const currentDir = path.join(snapshotDir, "current");
445
+ for (const result of results) {
446
+ const currentPath = path.join(currentDir, path.basename(result.snapshotPath));
447
+ if (await fileExists(currentPath)) {
448
+ await fs.promises.copyFile(currentPath, result.snapshotPath);
449
+ updated++;
450
+ console.log(`[vrt] Updated: ${path.basename(result.snapshotPath)}`);
451
+ }
452
+ }
453
+ return updated;
454
+ }
455
+ /**
456
+ * Approve specific failed results (update their baselines).
457
+ */
458
+ async approveResults(results, pattern) {
459
+ const toApprove = pattern ? results.filter((r) => {
460
+ const name = `${path.basename(r.artPath, ".art.vue")}/${r.variantName}`;
461
+ return name.includes(pattern) || matchGlob(name, pattern);
462
+ }) : results.filter((r) => !r.passed && !r.error);
463
+ return this.updateBaselines(toApprove);
464
+ }
465
+ /**
466
+ * Clean orphaned snapshots (no corresponding art/variant).
467
+ */
468
+ async cleanOrphans(artFiles) {
469
+ const snapshotDir = this.options.snapshotDir;
470
+ let cleaned = 0;
471
+ try {
472
+ const files = await fs.promises.readdir(snapshotDir);
473
+ const validNames = /* @__PURE__ */ new Set();
474
+ for (const art of artFiles) {
475
+ const artBaseName = path.basename(art.path, ".art.vue");
476
+ for (const variant of art.variants) {
477
+ if (variant.skipVrt) continue;
478
+ for (const viewport of this.options.viewports) {
479
+ const viewportName = viewport.name || `${viewport.width}x${viewport.height}`;
480
+ validNames.add(`${artBaseName}--${variant.name}--${viewportName}.png`);
481
+ }
482
+ }
483
+ }
484
+ for (const file of files) if (file.endsWith(".png") && !validNames.has(file)) {
485
+ await fs.promises.unlink(path.join(snapshotDir, file));
486
+ cleaned++;
487
+ console.log(`[vrt] Cleaned: ${file}`);
488
+ }
489
+ } catch {}
490
+ return cleaned;
491
+ }
492
+ /**
493
+ * Get VRT summary statistics.
494
+ */
495
+ getSummary(results) {
496
+ return computeSummary(results, this.startTime);
497
+ }
498
+ };
499
+ //#endregion
500
+ //#region src/vrt/report.ts
501
+ /**
502
+ * VRT report generation for Musea.
503
+ *
504
+ * Generates HTML and JSON reports from VRT results for visual review
505
+ * and CI integration.
506
+ */
507
+ /**
508
+ * Generate VRT report in HTML format.
509
+ * Supports side-by-side, overlay, and slider comparison modes.
510
+ */
511
+ function generateVrtReport(results, summary) {
512
+ const formatDuration = (ms) => {
513
+ if (ms < 1e3) return `${ms}ms`;
514
+ const seconds = Math.floor(ms / 1e3);
515
+ const minutes = Math.floor(seconds / 60);
516
+ if (minutes === 0) return `${seconds}s`;
517
+ return `${minutes}m ${seconds % 60}s`;
518
+ };
519
+ const timestamp = (/* @__PURE__ */ new Date()).toLocaleString("ja-JP", {
520
+ year: "numeric",
521
+ month: "2-digit",
522
+ day: "2-digit",
523
+ hour: "2-digit",
524
+ minute: "2-digit"
525
+ });
526
+ return `<!DOCTYPE html>
527
+ <html lang="en">
528
+ <head>
529
+ <meta charset="UTF-8">
530
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
531
+ <title>VRT Report - Musea</title>
532
+ <style>
533
+ :root {
534
+ --musea-bg-primary: #0d0d0d;
535
+ --musea-bg-secondary: #1a1815;
536
+ --musea-bg-tertiary: #252220;
537
+ --musea-accent: #a34828;
538
+ --musea-accent-hover: #c45a32;
539
+ --musea-text: #e6e9f0;
540
+ --musea-text-muted: #7b8494;
541
+ --musea-border: #3a3530;
542
+ --musea-success: #4ade80;
543
+ --musea-error: #f87171;
544
+ --musea-info: #60a5fa;
545
+ --musea-warning: #fbbf24;
546
+ }
547
+ * { box-sizing: border-box; margin: 0; padding: 0; }
548
+ body {
549
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
550
+ background: var(--musea-bg-primary);
551
+ color: var(--musea-text);
552
+ min-height: 100vh;
553
+ line-height: 1.5;
554
+ }
555
+
556
+ .header {
557
+ background: var(--musea-bg-secondary);
558
+ border-bottom: 1px solid var(--musea-border);
559
+ padding: 1rem 2rem;
560
+ display: flex;
561
+ align-items: center;
562
+ justify-content: space-between;
563
+ position: sticky;
564
+ top: 0;
565
+ z-index: 100;
566
+ }
567
+ .header-left { display: flex; align-items: center; gap: 1rem; }
568
+ .logo { font-size: 1.25rem; font-weight: 700; color: var(--musea-accent); }
569
+ .header-title { color: var(--musea-text-muted); font-size: 0.875rem; }
570
+ .header-meta { display: flex; align-items: center; gap: 1.5rem; font-size: 0.8125rem; color: var(--musea-text-muted); }
571
+ .header-meta span { display: flex; align-items: center; gap: 0.375rem; }
572
+
573
+ .main { max-width: 1400px; margin: 0 auto; padding: 2rem; }
574
+
575
+ .summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 1rem; margin-bottom: 2rem; }
576
+ .stat { background: var(--musea-bg-secondary); border: 1px solid var(--musea-border); border-radius: 8px; padding: 1.25rem; position: relative; overflow: hidden; }
577
+ .stat::before { content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 3px; }
578
+ .stat.passed::before { background: var(--musea-success); }
579
+ .stat.failed::before { background: var(--musea-error); }
580
+ .stat.new::before { background: var(--musea-info); }
581
+ .stat.skipped::before { background: var(--musea-warning); }
582
+ .stat-value { font-size: 2rem; font-weight: 700; font-variant-numeric: tabular-nums; line-height: 1; margin-bottom: 0.25rem; }
583
+ .stat.passed .stat-value { color: var(--musea-success); }
584
+ .stat.failed .stat-value { color: var(--musea-error); }
585
+ .stat.new .stat-value { color: var(--musea-info); }
586
+ .stat.skipped .stat-value { color: var(--musea-warning); }
587
+ .stat-label { color: var(--musea-text-muted); font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.08em; font-weight: 500; }
588
+
589
+ .filters { display: flex; gap: 0.5rem; margin-bottom: 1.5rem; flex-wrap: wrap; }
590
+ .filter-btn { background: var(--musea-bg-secondary); border: 1px solid var(--musea-border); color: var(--musea-text); padding: 0.5rem 1rem; border-radius: 6px; cursor: pointer; font-size: 0.8125rem; font-weight: 500; transition: all 0.15s ease; }
591
+ .filter-btn:hover { background: var(--musea-bg-tertiary); border-color: var(--musea-text-muted); }
592
+ .filter-btn.active { background: var(--musea-accent); border-color: var(--musea-accent); color: #fff; }
593
+ .filter-btn .count { opacity: 0.7; margin-left: 0.25rem; }
594
+
595
+ /* Comparison mode toggle */
596
+ .compare-modes { display: flex; gap: 0.25rem; margin-bottom: 1.5rem; background: var(--musea-bg-secondary); border-radius: 6px; padding: 0.25rem; width: fit-content; }
597
+ .compare-mode-btn { background: none; border: none; color: var(--musea-text-muted); padding: 0.375rem 0.75rem; border-radius: 4px; cursor: pointer; font-size: 0.75rem; font-weight: 500; transition: all 0.15s ease; }
598
+ .compare-mode-btn.active { background: var(--musea-bg-tertiary); color: var(--musea-text); }
599
+
600
+ .results { display: flex; flex-direction: column; gap: 0.75rem; }
601
+ .result { background: var(--musea-bg-secondary); border: 1px solid var(--musea-border); border-radius: 8px; overflow: hidden; transition: border-color 0.15s ease; }
602
+ .result:hover { border-color: var(--musea-text-muted); }
603
+ .result-header { padding: 1rem 1.25rem; display: flex; justify-content: space-between; align-items: center; cursor: pointer; border-left: 3px solid transparent; background: var(--musea-bg-tertiary); }
604
+ .result.passed .result-header { border-left-color: var(--musea-success); }
605
+ .result.failed .result-header { border-left-color: var(--musea-error); }
606
+ .result.new .result-header { border-left-color: var(--musea-info); }
607
+ .result.error .result-header { border-left-color: var(--musea-warning); }
608
+
609
+ .result-info { display: flex; align-items: center; gap: 1rem; }
610
+ .result-name { font-weight: 600; font-size: 0.9375rem; }
611
+ .result-meta { color: var(--musea-text-muted); font-size: 0.8125rem; padding: 0.125rem 0.5rem; background: var(--musea-bg-secondary); border-radius: 4px; }
612
+ .result-badge { padding: 0.25rem 0.625rem; border-radius: 4px; font-size: 0.6875rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; }
613
+ .result.passed .result-badge { background: rgba(74, 222, 128, 0.15); color: var(--musea-success); }
614
+ .result.failed .result-badge { background: rgba(248, 113, 113, 0.15); color: var(--musea-error); }
615
+ .result.new .result-badge { background: rgba(96, 165, 250, 0.15); color: var(--musea-info); }
616
+ .result.error .result-badge { background: rgba(251, 191, 36, 0.15); color: var(--musea-warning); }
617
+
618
+ .result-body { border-top: 1px solid var(--musea-border); }
619
+ .result-details { padding: 0.875rem 1.25rem; font-size: 0.8125rem; color: var(--musea-text-muted); font-family: 'SF Mono', 'Fira Code', monospace; background: var(--musea-bg-primary); }
620
+ .result-details.error { color: var(--musea-error); }
621
+
622
+ .result-images { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1rem; padding: 1.25rem; background: var(--musea-bg-primary); }
623
+ .result-images.overlay { grid-template-columns: 1fr; }
624
+ .image-container { background: var(--musea-bg-secondary); border: 1px solid var(--musea-border); border-radius: 6px; overflow: hidden; }
625
+ .image-label { padding: 0.625rem 0.875rem; font-size: 0.6875rem; font-weight: 600; color: var(--musea-text-muted); text-transform: uppercase; letter-spacing: 0.08em; background: var(--musea-bg-tertiary); border-bottom: 1px solid var(--musea-border); }
626
+ .image-wrapper { padding: 0.5rem; background: repeating-conic-gradient(var(--musea-bg-tertiary) 0% 25%, var(--musea-bg-secondary) 0% 50%) 50% / 16px 16px; }
627
+ .image-container img { width: 100%; height: auto; display: block; border-radius: 2px; }
628
+
629
+ /* Slider comparison */
630
+ .slider-compare { position: relative; overflow: hidden; }
631
+ .slider-compare img { display: block; width: 100%; }
632
+ .slider-overlay { position: absolute; top: 0; left: 0; bottom: 0; overflow: hidden; border-right: 2px solid var(--musea-accent); }
633
+ .slider-overlay img { display: block; min-width: 100%; height: 100%; object-fit: cover; }
634
+
635
+ .empty-state { text-align: center; padding: 4rem 2rem; color: var(--musea-text-muted); }
636
+ .all-passed { background: rgba(74, 222, 128, 0.1); border: 1px solid rgba(74, 222, 128, 0.2); border-radius: 8px; padding: 1.5rem; text-align: center; margin-bottom: 1.5rem; }
637
+ .all-passed-icon { font-size: 2.5rem; margin-bottom: 0.5rem; }
638
+ .all-passed-text { color: var(--musea-success); font-weight: 600; }
639
+ </style>
640
+ </head>
641
+ <body>
642
+ <header class="header">
643
+ <div class="header-left">
644
+ <div class="logo">Musea</div>
645
+ <span class="header-title">Visual Regression Report</span>
646
+ </div>
647
+ <div class="header-meta">
648
+ <span>
649
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
650
+ <circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/>
651
+ </svg>
652
+ ${formatDuration(summary.duration)}
653
+ </span>
654
+ <span>
655
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
656
+ <rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/>
657
+ </svg>
658
+ ${timestamp}
659
+ </span>
660
+ </div>
661
+ </header>
662
+
663
+ <main class="main">
664
+ <div class="summary">
665
+ <div class="stat passed"><div class="stat-value">${summary.passed}</div><div class="stat-label">Passed</div></div>
666
+ <div class="stat failed"><div class="stat-value">${summary.failed}</div><div class="stat-label">Failed</div></div>
667
+ <div class="stat new"><div class="stat-value">${summary.new}</div><div class="stat-label">New</div></div>
668
+ <div class="stat skipped"><div class="stat-value">${summary.skipped}</div><div class="stat-label">Skipped</div></div>
669
+ </div>
670
+
671
+ ${summary.failed === 0 && summary.skipped === 0 && summary.total > 0 ? `<div class="all-passed">
672
+ <div class="all-passed-icon">✓</div>
673
+ <div class="all-passed-text">All ${summary.total} visual tests passed</div>
674
+ </div>` : ""}
675
+
676
+ <div class="filters">
677
+ <button class="filter-btn active" data-filter="all">All<span class="count">(${summary.total})</span></button>
678
+ <button class="filter-btn" data-filter="failed">Failed<span class="count">(${summary.failed})</span></button>
679
+ <button class="filter-btn" data-filter="passed">Passed<span class="count">(${summary.passed})</span></button>
680
+ <button class="filter-btn" data-filter="new">New<span class="count">(${summary.new})</span></button>
681
+ </div>
682
+
683
+ <div class="compare-modes">
684
+ <button class="compare-mode-btn active" data-mode="side-by-side">Side by Side</button>
685
+ <button class="compare-mode-btn" data-mode="overlay">Overlay</button>
686
+ <button class="compare-mode-btn" data-mode="slider">Slider</button>
687
+ </div>
688
+
689
+ <div class="results">
690
+ ${results.length === 0 ? `<div class="empty-state"><p>No visual tests found</p></div>` : results.map((r) => {
691
+ const status = r.error ? "error" : r.isNew ? "new" : r.passed ? "passed" : "failed";
692
+ const badge = r.error ? "Error" : r.isNew ? "New" : r.passed ? "Passed" : "Failed";
693
+ const artName = path.basename(r.artPath, ".art.vue");
694
+ const viewportName = r.viewport.name || `${r.viewport.width}×${r.viewport.height}`;
695
+ let details = "";
696
+ if (r.error) details = `<div class="result-details error">${escapeHtml(r.error)}</div>`;
697
+ else if (r.diffPercentage !== void 0) details = `<div class="result-details">diff: ${r.diffPercentage.toFixed(3)}% (${r.diffPixels?.toLocaleString() ?? "0"} / ${r.totalPixels?.toLocaleString() ?? "0"} pixels)</div>`;
698
+ let images = "";
699
+ if (!r.error && !r.passed && r.diffPath) images = `<div class="result-images" data-baseline="file://${r.snapshotPath}" data-current="file://${r.currentPath}" data-diff="file://${r.diffPath}">
700
+ ${r.snapshotPath ? `<div class="image-container"><div class="image-label">Baseline</div><div class="image-wrapper"><img src="file://${r.snapshotPath}" alt="Baseline" loading="lazy" /></div></div>` : ""}
701
+ ${r.currentPath ? `<div class="image-container"><div class="image-label">Current</div><div class="image-wrapper"><img src="file://${r.currentPath}" alt="Current" loading="lazy" /></div></div>` : ""}
702
+ ${r.diffPath ? `<div class="image-container"><div class="image-label">Diff</div><div class="image-wrapper"><img src="file://${r.diffPath}" alt="Diff" loading="lazy" /></div></div>` : ""}
703
+ </div>`;
704
+ const hasBody = details || images;
705
+ return `<div class="result ${status}" data-status="${status}">
706
+ <div class="result-header">
707
+ <div class="result-info">
708
+ <div class="result-name">${escapeHtml(artName)} / ${escapeHtml(r.variantName)}</div>
709
+ <div class="result-meta">${escapeHtml(viewportName)}</div>
710
+ </div>
711
+ <span class="result-badge">${badge}</span>
712
+ </div>
713
+ ${hasBody ? `<div class="result-body">${details}${images}</div>` : ""}
714
+ </div>`;
715
+ }).join("")}
716
+ </div>
717
+ </main>
718
+
719
+ <script>
720
+ // Filter buttons
721
+ document.querySelectorAll('.filter-btn').forEach(btn => {
722
+ btn.addEventListener('click', () => {
723
+ document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
724
+ btn.classList.add('active');
725
+ const filter = btn.dataset.filter;
726
+ document.querySelectorAll('.result').forEach(result => {
727
+ result.style.display = (filter === 'all' || result.dataset.status === filter) ? 'block' : 'none';
728
+ });
729
+ });
730
+ });
731
+
732
+ // Compare mode buttons
733
+ document.querySelectorAll('.compare-mode-btn').forEach(btn => {
734
+ btn.addEventListener('click', () => {
735
+ document.querySelectorAll('.compare-mode-btn').forEach(b => b.classList.remove('active'));
736
+ btn.classList.add('active');
737
+ // Mode switching would update result-images display; this is a static report for now
738
+ });
739
+ });
740
+ <\/script>
741
+ </body>
742
+ </html>`;
743
+ }
744
+ /**
745
+ * Generate VRT JSON report for CI integration.
746
+ */
747
+ function generateVrtJsonReport(results, summary) {
748
+ return JSON.stringify({
749
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
750
+ summary,
751
+ results: results.map((r) => ({
752
+ art: path.basename(r.artPath, ".art.vue"),
753
+ variant: r.variantName,
754
+ viewport: r.viewport.name || `${r.viewport.width}x${r.viewport.height}`,
755
+ status: r.error ? "error" : r.isNew ? "new" : r.passed ? "passed" : "failed",
756
+ diffPercentage: r.diffPercentage,
757
+ error: r.error
758
+ }))
759
+ }, null, 2);
760
+ }
761
+ //#endregion
762
+ //#region src/vrt.ts
763
+ var vrt_default = MuseaVrtRunner;
764
+ //#endregion
765
+ export { MuseaVrtRunner as i, generateVrtJsonReport as n, generateVrtReport as r, vrt_default as t };
766
+
767
+ //# sourceMappingURL=vrt-CjFf5GR0.mjs.map