Files
mcp_web_dev_server/dist/tools/browser/visualDiff.js
T
2026-07-31 13:12:54 -04:00

89 lines
3.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { PNG } from 'pngjs';
import pixelmatch from 'pixelmatch';
import { z } from 'zod';
import { config } from '../../config.js';
import { readBaseline, resolveBaselinePath } from '../../lib/baselines.js';
import { errorResult, jsonResult, toErrorMessage } from '../shared.js';
import { browserManager } from './browserManager.js';
function toDataUri(png) {
return `data:image/png;base64,${png.toString('base64')}`;
}
export function comparePngBuffers(baselinePng, currentPng, threshold = 0.1) {
const baseline = PNG.sync.read(baselinePng);
const current = PNG.sync.read(currentPng);
if (baseline.width !== current.width || baseline.height !== current.height) {
throw new Error(`Screenshot dimensions differ from baseline (${baseline.width}x${baseline.height} vs ${current.width}x${current.height}). ` +
'Recapture the baseline at the same viewport/selector size.');
}
const { width, height } = baseline;
const diff = new PNG({ width, height });
const mismatchedPixels = pixelmatch(baseline.data, current.data, diff.data, width, height, {
threshold,
});
const total = width * height;
const mismatchPercent = total === 0 ? 0 : (mismatchedPixels / total) * 100;
return {
width,
height,
mismatchedPixels,
mismatchPercent,
diffPng: PNG.sync.write(diff),
};
}
export function registerBrowserVisualDiffTool(server) {
server.registerTool('browser_visual_diff', {
title: 'Visual Screenshot Diff',
description: 'Capture the current page/element and compare it to a previously saved baseline with pixelmatch. Returns mismatch stats and a diff PNG data URI.',
inputSchema: {
name: z.string().min(1).describe('Baseline name previously saved with browser_screenshot_baseline.'),
selector: z.string().optional().describe('Optional CSS selector to screenshot for comparison.'),
fullPage: z
.boolean()
.optional()
.default(false)
.describe('Capture the full scrollable page when selector is omitted.'),
threshold: z
.number()
.min(0)
.max(1)
.optional()
.default(0.1)
.describe('pixelmatch threshold (01). Lower is stricter.'),
},
}, async ({ name, selector, fullPage, threshold }) => {
try {
const baselinePng = readBaseline(name);
const session = await browserManager.getSession();
let currentPng;
if (selector) {
const element = await session.page.locator(selector).first();
await element.waitFor({ state: 'visible', timeout: 5000 });
currentPng = await element.screenshot({ type: 'png' });
}
else {
currentPng = await session.page.screenshot({ fullPage, type: 'png' });
}
const comparison = comparePngBuffers(baselinePng, currentPng, threshold ?? 0.1);
const diffDataUri = toDataUri(comparison.diffPng);
return jsonResult({
name,
baselinePath: resolveBaselinePath(name),
selector: selector ?? null,
width: comparison.width,
height: comparison.height,
mismatchedPixels: comparison.mismatchedPixels,
mismatchPercent: Number(comparison.mismatchPercent.toFixed(4)),
passed: comparison.mismatchedPixels === 0,
threshold: threshold ?? 0.1,
diffDataUri,
truncatedDiffDataUri: diffDataUri.length > config.maxToolOutputChars
? `${diffDataUri.slice(0, config.maxToolOutputChars)}...`
: diffDataUri,
});
}
catch (error) {
return errorResult(toErrorMessage(error));
}
});
}
//# sourceMappingURL=visualDiff.js.map