// built-in skill: visual diff against baselines import { launchReapedChrome } from '../../../../../scripts/lib/reap-browser' import type { RNXSkill } from '../types' export const skill: RNXSkill = { name: 'visual-diff', description: 'Compare current rendering against baseline screenshots', type: 'review', version: '1.0.0', triggers: ['visual diff', 'compare', 'visual regression', 'diff screenshots'], tools: [ { name: 'visual_diff', description: 'Compare current screen against a baseline screenshot', parameters: { baseline: { type: 'string', description: 'Path to baseline screenshot', required: true, }, name: { type: 'string', description: 'Name for the diff output', required: true }, threshold: { type: 'number', description: 'Pixel match threshold (0-1, default 0.1)', }, }, async execute(params, context) { const { chromium } = await import('playwright') const path = await import('path') const fs = await import('fs') // sootsim renders CanvasKit — require GPU-backed Chrome (Metal). the // bundled swiftshader shell software-renders the canvas and pegs every // core; never use it for sootsim. const browser = await launchReapedChrome((options) => chromium.launch(options), { headless: true, }) const page = await browser.newPage({ viewport: { width: 500, height: 900 } }) try { await page.goto(context.url, { waitUntil: 'networkidle' }) await page.waitForTimeout(2000) // capture current const currentPath = path.join(context.outputDir, `${params.name}-current.png`) fs.mkdirSync(path.dirname(currentPath), { recursive: true }) await page.screenshot({ path: currentPath }) // compare using pixelmatch if available try { const { PNG } = await import('pngjs') const pixelmatch = (await import('pixelmatch')).default const baseline = PNG.sync.read(fs.readFileSync(params.baseline)) const current = PNG.sync.read(fs.readFileSync(currentPath)) const { width, height } = baseline const diff = new PNG({ width, height }) const numDiffPixels = pixelmatch( baseline.data, current.data, diff.data, width, height, { threshold: params.threshold || 0.1 }, ) const diffPath = path.join(context.outputDir, `${params.name}-diff.png`) fs.writeFileSync(diffPath, PNG.sync.write(diff)) const totalPixels = width * height const diffPercent = ((numDiffPixels / totalPixels) * 100).toFixed(2) return { success: numDiffPixels === 0, message: `${diffPercent}% difference (${numDiffPixels} pixels)`, artifacts: [ { type: 'screenshot', name: `${params.name}-current`, path: currentPath }, { type: 'screenshot', name: `${params.name}-diff`, path: diffPath }, ], data: { diffPixels: numDiffPixels, totalPixels, diffPercent }, } } catch { return { success: true, message: 'Screenshot captured (pixelmatch not available for comparison)', artifacts: [ { type: 'screenshot', name: `${params.name}-current`, path: currentPath }, ], } } } finally { await browser.close() } }, }, ], }