frameshot-mcp 0.3.0 → 0.7.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.
@@ -0,0 +1,690 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __export = (target, all) => {
3
+ for (var name in all)
4
+ __defProp(target, name, { get: all[name], enumerable: true });
5
+ };
6
+
7
+ // src/infrastructure/browser-pool.ts
8
+ import { chromium, firefox, webkit } from "playwright";
9
+ var BrowserPool = class {
10
+ pool = /* @__PURE__ */ new Map();
11
+ async warmup(engines) {
12
+ await Promise.all(engines.map((e) => this.getSlot(e)));
13
+ }
14
+ async getPage(engine) {
15
+ const slot = await this.getSlot(engine);
16
+ return slot.page;
17
+ }
18
+ async setViewport(engine, viewport) {
19
+ const slot = await this.getSlot(engine);
20
+ const current = slot.page.viewportSize();
21
+ if (current?.width !== viewport.width || current?.height !== viewport.height) {
22
+ await slot.page.setViewportSize(viewport);
23
+ }
24
+ }
25
+ async shutdown() {
26
+ for (const slot of this.pool.values()) {
27
+ await slot.browser.close().catch(() => {
28
+ });
29
+ }
30
+ this.pool.clear();
31
+ }
32
+ async getSlot(engine) {
33
+ const existing = this.pool.get(engine);
34
+ if (existing?.browser.isConnected() && existing.ready) {
35
+ return existing;
36
+ }
37
+ const launcher = { chromium, firefox, webkit }[engine];
38
+ let browser;
39
+ try {
40
+ browser = await launcher.launch({
41
+ headless: true,
42
+ ...engine === "chromium" ? { channel: "chrome" } : {}
43
+ });
44
+ } catch (_e) {
45
+ throw new Error(
46
+ `${engine} is not installed. Run: npx playwright install ${engine}`
47
+ );
48
+ }
49
+ const context = await browser.newContext({
50
+ viewport: { width: 1280, height: 800 },
51
+ deviceScaleFactor: 2
52
+ });
53
+ const page = await context.newPage();
54
+ await page.setContent(
55
+ '<html><head><script src="https://cdn.tailwindcss.com?plugins=forms,typography,aspect-ratio"></script></head><body></body></html>',
56
+ { waitUntil: "networkidle" }
57
+ );
58
+ const slot = { browser, page, ready: true };
59
+ this.pool.set(engine, slot);
60
+ return slot;
61
+ }
62
+ };
63
+
64
+ // src/infrastructure/html-builder.ts
65
+ var HtmlBuilder = class {
66
+ build(code, framework, options) {
67
+ const { darkMode, css, tailwindVersion } = options;
68
+ const tailwind = tailwindVersion === "4" ? '<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>' : '<script src="https://cdn.tailwindcss.com?plugins=forms,typography,aspect-ratio"></script>';
69
+ const tailwindConfig = tailwindVersion === "4" ? "" : "<script>tailwind.config={darkMode:'class'}</script>";
70
+ const customCss = css ? `<style>${css}</style>` : "";
71
+ const baseStyle = `<style>*{margin:0;box-sizing:border-box}body{padding:16px;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif}</style>`;
72
+ const htmlClass = darkMode ? ' class="dark"' : "";
73
+ if (framework === "html") {
74
+ if (code.includes("<html")) return code;
75
+ return `<!DOCTYPE html><html${htmlClass}><head><meta charset="utf-8">${tailwind}${tailwindConfig}${baseStyle}${customCss}</head><body>${code}</body></html>`;
76
+ }
77
+ if (framework === "react") {
78
+ const cleanedCode = code.replace(/['"]use client['"];?\n?/g, "").replace(/['"]use server['"];?\n?/g, "").replace(/import\s+.*?\s+from\s+['"]next\/image['"];?\n?/g, "").replace(/import\s+.*?\s+from\s+['"]next\/link['"];?\n?/g, "");
79
+ return `<!DOCTYPE html><html${htmlClass}><head><meta charset="utf-8">${tailwind}${tailwindConfig}
80
+ <script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
81
+ <script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
82
+ <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
83
+ ${baseStyle}${customCss}</head><body><div id="root"></div>
84
+ <script type="text/babel">
85
+ const Image = (props) => React.createElement('img', {...props, src: props.src?.src || props.src});
86
+ const Link = ({href, children, ...props}) => React.createElement('a', {href, ...props}, children);
87
+ ${cleanedCode}
88
+ const _C = typeof App!=='undefined'?App:typeof Component!=='undefined'?Component:typeof Default!=='undefined'?Default:null;
89
+ if(_C)ReactDOM.createRoot(document.getElementById('root')).render(React.createElement(_C));
90
+ </script></body></html>`;
91
+ }
92
+ if (framework === "vue") {
93
+ return `<!DOCTYPE html><html${htmlClass}><head><meta charset="utf-8">${tailwind}${tailwindConfig}
94
+ <script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
95
+ ${baseStyle}${customCss}</head><body><div id="app"></div>
96
+ <script>
97
+ const{createApp,ref,reactive,computed,onMounted,watch,watchEffect}=Vue;
98
+ ${code}
99
+ const _C=typeof App!=='undefined'?App:typeof Component!=='undefined'?Component:null;
100
+ if(_C)createApp(_C).mount('#app');
101
+ </script></body></html>`;
102
+ }
103
+ if (framework === "svelte") {
104
+ return `<!DOCTYPE html><html${htmlClass}><head><meta charset="utf-8">${tailwind}${tailwindConfig}
105
+ <script src="https://unpkg.com/svelte@4/compiler.cjs"></script>
106
+ ${baseStyle}${customCss}</head><body><div id="app"></div>
107
+ <script type="module">
108
+ import "https://unpkg.com/svelte@4/internal/index.mjs";
109
+ ${code}
110
+ const _C=typeof App!=='undefined'?App:typeof Component!=='undefined'?Component:null;
111
+ if(_C)new _C({target:document.getElementById('app')});
112
+ </script></body></html>`;
113
+ }
114
+ return code;
115
+ }
116
+ };
117
+
118
+ // src/infrastructure/image-comparator.ts
119
+ import pixelmatch from "pixelmatch";
120
+ import { PNG } from "pngjs";
121
+ var ImageComparator = class {
122
+ diff(imageA, imageB, threshold = 0.1) {
123
+ const bufA = Buffer.from(imageA, "base64");
124
+ const bufB = Buffer.from(imageB, "base64");
125
+ const pngA = PNG.sync.read(bufA);
126
+ const pngB = PNG.sync.read(bufB);
127
+ const width = Math.max(pngA.width, pngB.width);
128
+ const height = Math.max(pngA.height, pngB.height);
129
+ const normalizedA = new PNG({ width, height });
130
+ const normalizedB = new PNG({ width, height });
131
+ PNG.bitblt(pngA, normalizedA, 0, 0, pngA.width, pngA.height, 0, 0);
132
+ PNG.bitblt(pngB, normalizedB, 0, 0, pngB.width, pngB.height, 0, 0);
133
+ const diffPng = new PNG({ width, height });
134
+ const diffPixels = pixelmatch(
135
+ normalizedA.data,
136
+ normalizedB.data,
137
+ diffPng.data,
138
+ width,
139
+ height,
140
+ { threshold }
141
+ );
142
+ const totalPixels = width * height;
143
+ return {
144
+ diff: PNG.sync.write(diffPng).toString("base64"),
145
+ diffPixels,
146
+ totalPixels,
147
+ diffPercentage: Math.round(diffPixels / totalPixels * 1e4) / 100
148
+ };
149
+ }
150
+ composite(images, columns, labelHeight = 30) {
151
+ const pngs = images.map((buf) => PNG.sync.read(buf));
152
+ const cellWidth = Math.max(...pngs.map((p) => p.width));
153
+ const cellHeight = Math.max(...pngs.map((p) => p.height));
154
+ const rows = Math.ceil(pngs.length / columns);
155
+ const gridWidth = cellWidth * columns;
156
+ const gridHeight = (cellHeight + labelHeight) * rows;
157
+ const grid = new PNG({ width: gridWidth, height: gridHeight });
158
+ grid.data.fill(255);
159
+ for (let i = 0; i < pngs.length; i++) {
160
+ const col = i % columns;
161
+ const row = Math.floor(i / columns);
162
+ const x = col * cellWidth;
163
+ const y = row * (cellHeight + labelHeight) + labelHeight;
164
+ PNG.bitblt(pngs[i], grid, 0, 0, pngs[i].width, pngs[i].height, x, y);
165
+ }
166
+ return {
167
+ image: PNG.sync.write(grid).toString("base64"),
168
+ width: gridWidth,
169
+ height: gridHeight
170
+ };
171
+ }
172
+ };
173
+
174
+ // src/infrastructure/snapshot-store.ts
175
+ var SnapshotStore = class {
176
+ store = /* @__PURE__ */ new Map();
177
+ save(key, image) {
178
+ this.store.set(key, { image, timestamp: Date.now() });
179
+ }
180
+ get(key) {
181
+ return this.store.get(key);
182
+ }
183
+ list() {
184
+ return Array.from(this.store.entries()).map(([key, val]) => ({
185
+ key,
186
+ timestamp: val.timestamp
187
+ }));
188
+ }
189
+ };
190
+
191
+ // src/domain/types.ts
192
+ var DEVICE_PRESETS = {
193
+ mobile: { width: 375, height: 667 },
194
+ tablet: { width: 768, height: 1024 },
195
+ desktop: { width: 1280, height: 800 }
196
+ };
197
+ var EXT_TO_FRAMEWORK = {
198
+ ".jsx": "react",
199
+ ".tsx": "react",
200
+ ".vue": "vue",
201
+ ".svelte": "svelte",
202
+ ".html": "html",
203
+ ".htm": "html"
204
+ };
205
+
206
+ // src/use-cases/audit.ts
207
+ var AuditUseCase = class {
208
+ constructor(pool, htmlBuilder) {
209
+ this.pool = pool;
210
+ this.htmlBuilder = htmlBuilder;
211
+ }
212
+ pool;
213
+ htmlBuilder;
214
+ async auditA11y(code, framework, options = {}) {
215
+ const html = this.htmlBuilder.build(code, framework, {
216
+ darkMode: options.darkMode ?? false,
217
+ css: options.css ?? "",
218
+ tailwindVersion: options.tailwindVersion ?? "3"
219
+ });
220
+ const page = await this.pool.getPage("chromium");
221
+ const viewport = options.viewport ?? { width: 1280, height: 800 };
222
+ await this.pool.setViewport("chromium", viewport);
223
+ await page.setContent(html, { waitUntil: "load", timeout: 1e4 });
224
+ const axeSource = await import("axe-core").then((m) => m.source);
225
+ await page.addScriptTag({ content: axeSource });
226
+ const results = await page.evaluate(() => {
227
+ return window.axe.run();
228
+ });
229
+ return {
230
+ violations: results.violations.map(
231
+ (v) => ({
232
+ id: v.id,
233
+ impact: v.impact,
234
+ description: v.description,
235
+ helpUrl: v.helpUrl,
236
+ nodes: v.nodes.map((n) => ({
237
+ html: n.html,
238
+ target: n.target
239
+ }))
240
+ })
241
+ ),
242
+ passes: results.passes.length,
243
+ incomplete: results.incomplete.length
244
+ };
245
+ }
246
+ async perfAudit(code, framework, options = {}) {
247
+ const html = this.htmlBuilder.build(code, framework, {
248
+ darkMode: options.darkMode ?? false,
249
+ css: options.css ?? "",
250
+ tailwindVersion: options.tailwindVersion ?? "3"
251
+ });
252
+ const page = await this.pool.getPage("chromium");
253
+ const viewport = options.viewport ?? { width: 1280, height: 800 };
254
+ await this.pool.setViewport("chromium", viewport);
255
+ const start = performance.now();
256
+ await page.setContent(html, { waitUntil: "load", timeout: 1e4 });
257
+ const renderTimeMs = Math.round(performance.now() - start);
258
+ const metrics = await page.evaluate(() => {
259
+ const all = document.querySelectorAll("*");
260
+ let maxDepth = 0;
261
+ for (const el of all) {
262
+ let depth = 0;
263
+ let node = el;
264
+ while (node) {
265
+ depth++;
266
+ node = node.parentElement;
267
+ }
268
+ if (depth > maxDepth) maxDepth = depth;
269
+ }
270
+ return {
271
+ domElements: all.length,
272
+ domDepth: maxDepth,
273
+ scriptCount: document.querySelectorAll("script").length,
274
+ styleSheetCount: document.styleSheets.length,
275
+ imageCount: document.querySelectorAll("img").length,
276
+ totalDomSize: document.documentElement.outerHTML.length
277
+ };
278
+ });
279
+ return { renderTimeMs, ...metrics };
280
+ }
281
+ };
282
+
283
+ // src/use-cases/catalog.ts
284
+ import { readdirSync, readFileSync, statSync } from "fs";
285
+ import { extname, join, relative } from "path";
286
+ var CatalogUseCase = class {
287
+ constructor(renderUseCase) {
288
+ this.renderUseCase = renderUseCase;
289
+ }
290
+ renderUseCase;
291
+ async renderCatalog(directory, options = {}) {
292
+ const { recursive = false, ...renderOptions } = options;
293
+ const files = this.scanDirectory(directory, recursive);
294
+ const results = [];
295
+ for (const filePath of files) {
296
+ const relativePath = relative(directory, filePath);
297
+ const framework = this.detectFramework(filePath);
298
+ const code = readFileSync(filePath, "utf-8");
299
+ try {
300
+ const [result] = await this.renderUseCase.render(code, framework, {
301
+ ...renderOptions,
302
+ engines: ["chromium"]
303
+ });
304
+ results.push({
305
+ path: relativePath,
306
+ framework,
307
+ image: result.image,
308
+ width: result.width,
309
+ height: result.height,
310
+ consoleErrors: result.consoleErrors
311
+ });
312
+ } catch {
313
+ results.push({
314
+ path: relativePath,
315
+ framework,
316
+ image: "",
317
+ width: 0,
318
+ height: 0,
319
+ consoleErrors: [`Failed to render ${relativePath}`]
320
+ });
321
+ }
322
+ }
323
+ return results;
324
+ }
325
+ scanDirectory(dir, recursive) {
326
+ const componentExtensions = /* @__PURE__ */ new Set([
327
+ ".jsx",
328
+ ".tsx",
329
+ ".vue",
330
+ ".svelte",
331
+ ".html",
332
+ ".htm"
333
+ ]);
334
+ const files = [];
335
+ const entries = readdirSync(dir);
336
+ for (const entry of entries) {
337
+ if (entry.startsWith(".") || entry === "node_modules") continue;
338
+ const fullPath = join(dir, entry);
339
+ const stat = statSync(fullPath);
340
+ if (stat.isDirectory() && recursive) {
341
+ files.push(...this.scanDirectory(fullPath, recursive));
342
+ } else if (stat.isFile() && componentExtensions.has(extname(entry).toLowerCase())) {
343
+ files.push(fullPath);
344
+ }
345
+ }
346
+ return files;
347
+ }
348
+ detectFramework(filePath) {
349
+ return EXT_TO_FRAMEWORK[extname(filePath).toLowerCase()] ?? "react";
350
+ }
351
+ };
352
+
353
+ // src/use-cases/diff.ts
354
+ var DiffUseCase = class {
355
+ constructor(renderUseCase, imageComparator) {
356
+ this.renderUseCase = renderUseCase;
357
+ this.imageComparator = imageComparator;
358
+ }
359
+ renderUseCase;
360
+ imageComparator;
361
+ async diffComponent(before, after, framework, options = {}) {
362
+ const [beforeResults, afterResults] = await Promise.all([
363
+ this.renderUseCase.render(before, framework, {
364
+ ...options,
365
+ engines: ["chromium"]
366
+ }),
367
+ this.renderUseCase.render(after, framework, {
368
+ ...options,
369
+ engines: ["chromium"]
370
+ })
371
+ ]);
372
+ const comparison = this.imageComparator.diff(
373
+ beforeResults[0].image,
374
+ afterResults[0].image
375
+ );
376
+ return {
377
+ before: beforeResults[0].image,
378
+ after: afterResults[0].image,
379
+ ...comparison
380
+ };
381
+ }
382
+ async diffFromReference(code, framework, referenceImage, options = {}) {
383
+ const { threshold = 0.1, ...renderOptions } = options;
384
+ const [renderResult] = await this.renderUseCase.render(code, framework, {
385
+ ...renderOptions,
386
+ engines: ["chromium"]
387
+ });
388
+ const comparison = this.imageComparator.diff(
389
+ referenceImage,
390
+ renderResult.image,
391
+ threshold
392
+ );
393
+ return {
394
+ rendered: renderResult.image,
395
+ ...comparison,
396
+ passed: comparison.diffPercentage === 0
397
+ };
398
+ }
399
+ };
400
+
401
+ // src/use-cases/render.ts
402
+ var RenderUseCase = class {
403
+ constructor(pool, htmlBuilder, imageComparator) {
404
+ this.pool = pool;
405
+ this.htmlBuilder = htmlBuilder;
406
+ this.imageComparator = imageComparator;
407
+ }
408
+ pool;
409
+ htmlBuilder;
410
+ imageComparator;
411
+ async render(code, framework, options = {}) {
412
+ const opts = this.resolveOptions(options);
413
+ const html = this.htmlBuilder.build(code, framework, {
414
+ darkMode: opts.darkMode,
415
+ css: opts.css,
416
+ tailwindVersion: opts.tailwindVersion
417
+ });
418
+ return Promise.all(
419
+ opts.engines.map((engine) => this.renderHtml(engine, html, opts))
420
+ );
421
+ }
422
+ async renderInteraction(code, framework, interactions, options = {}) {
423
+ const opts = this.resolveOptions(options);
424
+ const html = this.htmlBuilder.build(code, framework, {
425
+ darkMode: opts.darkMode,
426
+ css: opts.css,
427
+ tailwindVersion: opts.tailwindVersion
428
+ });
429
+ const page = await this.pool.getPage("chromium");
430
+ await this.pool.setViewport("chromium", opts.viewport);
431
+ await page.setContent(html, { waitUntil: "load", timeout: 1e4 });
432
+ for (const interaction of interactions) {
433
+ switch (interaction.action) {
434
+ case "click":
435
+ if (interaction.selector) await page.click(interaction.selector);
436
+ break;
437
+ case "hover":
438
+ if (interaction.selector) await page.hover(interaction.selector);
439
+ break;
440
+ case "focus":
441
+ if (interaction.selector) await page.focus(interaction.selector);
442
+ break;
443
+ case "type":
444
+ if (interaction.selector && interaction.value)
445
+ await page.fill(interaction.selector, interaction.value);
446
+ break;
447
+ case "wait":
448
+ await page.waitForTimeout(interaction.ms ?? 300);
449
+ break;
450
+ }
451
+ }
452
+ const screenshot = await page.screenshot({ type: "png", fullPage: true });
453
+ const metrics = await page.evaluate(() => ({
454
+ w: document.documentElement.scrollWidth,
455
+ h: document.documentElement.scrollHeight
456
+ }));
457
+ return {
458
+ image: screenshot.toString("base64"),
459
+ width: metrics.w,
460
+ height: metrics.h
461
+ };
462
+ }
463
+ async captureAnimation(code, framework, options = {}) {
464
+ const { frames = 5, duration = 1e3 } = options;
465
+ const opts = this.resolveOptions(options);
466
+ const html = this.htmlBuilder.build(code, framework, {
467
+ darkMode: opts.darkMode,
468
+ css: opts.css,
469
+ tailwindVersion: opts.tailwindVersion
470
+ });
471
+ const page = await this.pool.getPage("chromium");
472
+ await this.pool.setViewport("chromium", opts.viewport);
473
+ await page.setContent(html, { waitUntil: "load", timeout: 1e4 });
474
+ const interval = duration / (frames - 1);
475
+ const results = [];
476
+ for (let i = 0; i < frames; i++) {
477
+ if (i > 0) await page.waitForTimeout(interval);
478
+ const screenshot = await page.screenshot({
479
+ type: "png",
480
+ fullPage: false
481
+ });
482
+ results.push({
483
+ timestamp: Math.round(i * interval),
484
+ image: screenshot.toString("base64")
485
+ });
486
+ }
487
+ return results;
488
+ }
489
+ async renderGrid(cells, framework, options = {}) {
490
+ const { columns = Math.min(cells.length, 3) } = options;
491
+ const screenshots = await Promise.all(
492
+ cells.map(async (cell) => {
493
+ const [result] = await this.render(cell.code, framework, {
494
+ ...options,
495
+ engines: ["chromium"]
496
+ });
497
+ return Buffer.from(result.image, "base64");
498
+ })
499
+ );
500
+ const composite = this.imageComparator.composite(screenshots, columns);
501
+ return { ...composite, cells: cells.length };
502
+ }
503
+ async renderMatrix(code, framework, viewports, themes, options = {}) {
504
+ const combinations = [];
505
+ for (const viewport of viewports) {
506
+ for (const theme of themes) {
507
+ combinations.push({ viewport, theme });
508
+ }
509
+ }
510
+ return Promise.all(
511
+ combinations.map(async ({ viewport, theme }) => {
512
+ const [result] = await this.render(code, framework, {
513
+ ...options,
514
+ viewport: { width: viewport.width, height: viewport.height },
515
+ darkMode: theme === "dark",
516
+ engines: ["chromium"],
517
+ fullPage: true
518
+ });
519
+ return {
520
+ viewport: viewport.label,
521
+ theme,
522
+ image: result.image,
523
+ width: result.width,
524
+ height: result.height,
525
+ consoleErrors: result.consoleErrors
526
+ };
527
+ })
528
+ );
529
+ }
530
+ async renderHtml(engine, html, options) {
531
+ const page = await this.pool.getPage(engine);
532
+ await this.pool.setViewport(engine, options.viewport);
533
+ const consoleErrors = [];
534
+ const onError = (msg) => {
535
+ if (msg.type() === "error") consoleErrors.push(msg.text());
536
+ };
537
+ page.on("console", onError);
538
+ page.on("pageerror", (err) => consoleErrors.push(err.message));
539
+ await page.setContent(html, { waitUntil: "load", timeout: 1e4 });
540
+ if (options.waitFor > 0) {
541
+ await page.waitForTimeout(options.waitFor);
542
+ }
543
+ const screenshot = await page.screenshot({
544
+ type: "png",
545
+ fullPage: options.fullPage
546
+ });
547
+ const metrics = await page.evaluate(() => ({
548
+ w: document.documentElement.scrollWidth,
549
+ h: document.documentElement.scrollHeight
550
+ }));
551
+ page.removeListener("console", onError);
552
+ return {
553
+ engine,
554
+ image: screenshot.toString("base64"),
555
+ width: metrics.w,
556
+ height: metrics.h,
557
+ consoleErrors
558
+ };
559
+ }
560
+ resolveOptions(partial) {
561
+ return {
562
+ viewport: partial.viewport ?? { width: 1280, height: 800 },
563
+ engines: partial.engines ?? ["chromium"],
564
+ fullPage: partial.fullPage ?? true,
565
+ darkMode: partial.darkMode ?? false,
566
+ css: partial.css ?? "",
567
+ tailwindVersion: partial.tailwindVersion ?? "3",
568
+ waitFor: partial.waitFor ?? 0
569
+ };
570
+ }
571
+ };
572
+
573
+ // src/use-cases/screenshot.ts
574
+ var ScreenshotUseCase = class {
575
+ constructor(pool) {
576
+ this.pool = pool;
577
+ }
578
+ pool;
579
+ async screenshotUrl(url, options = {}) {
580
+ const engines = options.engines ?? ["chromium"];
581
+ const { waitForNetworkIdle = true } = options;
582
+ return Promise.all(
583
+ engines.map(async (engine) => {
584
+ const { width = 1280, height = 800 } = options.viewport ?? {};
585
+ const fullPage = options.fullPage ?? true;
586
+ const waitFor = options.waitFor ?? 0;
587
+ const page = await this.pool.getPage(engine);
588
+ await this.pool.setViewport(engine, { width, height });
589
+ const consoleErrors = [];
590
+ const onError = (msg) => {
591
+ if (msg.type() === "error") consoleErrors.push(msg.text());
592
+ };
593
+ page.on("console", onError);
594
+ page.on("pageerror", (err) => consoleErrors.push(err.message));
595
+ await page.goto(url, { waitUntil: "domcontentloaded", timeout: 15e3 });
596
+ if (waitForNetworkIdle) {
597
+ await page.waitForLoadState("networkidle", { timeout: 5e3 }).catch(() => {
598
+ });
599
+ }
600
+ if (options.waitForSelector) {
601
+ await page.waitForSelector(options.waitForSelector, {
602
+ timeout: 1e4
603
+ });
604
+ }
605
+ if (waitFor > 0) {
606
+ await page.waitForTimeout(waitFor);
607
+ }
608
+ const screenshot = await page.screenshot({ type: "png", fullPage });
609
+ const metrics = await page.evaluate(() => ({
610
+ w: document.documentElement.scrollWidth,
611
+ h: document.documentElement.scrollHeight
612
+ }));
613
+ page.removeListener("console", onError);
614
+ return {
615
+ engine,
616
+ image: screenshot.toString("base64"),
617
+ width: metrics.w,
618
+ height: metrics.h,
619
+ consoleErrors
620
+ };
621
+ })
622
+ );
623
+ }
624
+ async screenshotUrlWithRetry(url, options = {}) {
625
+ const { retryCount = 0, ...screenshotOpts } = options;
626
+ const maxAttempts = retryCount + 1;
627
+ let lastError;
628
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
629
+ try {
630
+ if (attempt > 0) {
631
+ const delay = 300 * 2 ** (attempt - 1);
632
+ await new Promise((resolve) => setTimeout(resolve, delay));
633
+ }
634
+ return await this.screenshotUrl(url, screenshotOpts);
635
+ } catch (error) {
636
+ lastError = error;
637
+ }
638
+ }
639
+ throw lastError;
640
+ }
641
+ };
642
+
643
+ // src/use-cases/snapshot.ts
644
+ var SnapshotUseCase = class {
645
+ constructor(store, renderUseCase, diffUseCase) {
646
+ this.store = store;
647
+ this.renderUseCase = renderUseCase;
648
+ this.diffUseCase = diffUseCase;
649
+ }
650
+ store;
651
+ renderUseCase;
652
+ diffUseCase;
653
+ async save(key, code, framework, options = {}) {
654
+ const [result] = await this.renderUseCase.render(code, framework, {
655
+ ...options,
656
+ engines: ["chromium"]
657
+ });
658
+ this.store.save(key, result.image);
659
+ return { image: result.image, width: result.width, height: result.height };
660
+ }
661
+ async check(key, code, framework, options = {}) {
662
+ const snapshot = this.store.get(key);
663
+ if (!snapshot) return null;
664
+ return this.diffUseCase.diffFromReference(
665
+ code,
666
+ framework,
667
+ snapshot.image,
668
+ options
669
+ );
670
+ }
671
+ list() {
672
+ return this.store.list();
673
+ }
674
+ };
675
+
676
+ export {
677
+ __export,
678
+ BrowserPool,
679
+ HtmlBuilder,
680
+ ImageComparator,
681
+ SnapshotStore,
682
+ DEVICE_PRESETS,
683
+ EXT_TO_FRAMEWORK,
684
+ AuditUseCase,
685
+ CatalogUseCase,
686
+ DiffUseCase,
687
+ RenderUseCase,
688
+ ScreenshotUseCase,
689
+ SnapshotUseCase
690
+ };