Chart-render regression testing across the three chart-library families - Canvas (Chart.js: locator screenshot snapshot + `canvas.toDataURL()` diff with animations disabled), SVG (D3: `outerHTML` structural snapshot with generated-ID normalization + per-element data-binding tests), and declarative specs (Vega / Vega-Lite: JSON Schema validation + Vega-Lite → Vega compile test). Detects the family from package.json imports (chart.js / d3 / vega-lite), then applies the matching recipe; full per-library depth with citations in references/chartjs.md, references/d3.md, references/vega.md. Use when a dashboard or data product renders charts and their output needs regression coverage - before a chart-library major upgrade, after a theming change, or when runtime-generated Vega specs must be proven valid before render.
72
91%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Passed
No findings from the security scan
Chart.js reference for chart-render-tests: render via headless
Chromium / jsdom + canvas mock, capture canvas pixels via
canvas.toDataURL() + image-diff, disable animations
(options.animation = false) for stable snapshots, test tooltip +
legend interactions.
Per the Chart.js docs, Chart.js renders to <canvas>, testable via
canvas.toDataURL() snapshot diff.
Per the Chart.js docs, the basic config object accepts options:
new Chart(ctx, {
type: 'bar',
data: {...},
options: {
animation: false, // disable for snapshots
responsive: false, // fix the canvas dimensions
plugins: {
legend: { display: true },
},
scales: { y: { beginAtZero: true } },
},
});Without animation: false, snapshots capture mid-animation frames
randomly.
import { test, expect } from '@playwright/test';
test('revenue bar chart matches snapshot', async ({ page }) => {
await page.goto('https://localhost:3000/dashboard');
// Wait for chart to render (no animation, just initial draw)
await page.waitForFunction(() => {
const canvas = document.querySelector('canvas#revenue-chart');
return canvas && canvas.toDataURL().length > 1000;
});
const canvas = page.locator('canvas#revenue-chart');
await expect(canvas).toHaveScreenshot('revenue-chart.png', {
maxDiffPixels: 50,
});
});maxDiffPixels allows for sub-pixel anti-aliasing variance across
runs.
For alternatives to the Playwright screenshot helper - a programmatic
toDataURL() diff, jsdom + canvas-mock unit tests, and non-visual
data-driven assertions - see
chartjs-alternative-approaches.md.
test('tooltip shows data point value on hover', async ({ page }) => {
await page.goto('https://localhost:3000/dashboard');
await waitForChartReady(page);
// Hover over a known data point coordinate
await page.mouse.move(150, 200);
await page.waitForSelector('.chartjs-tooltip', { state: 'visible' });
const tooltipText = await page.locator('.chartjs-tooltip').textContent();
expect(tooltipText).toContain('Q1: 10');
});
test('legend click toggles dataset visibility', async ({ page }) => {
await page.goto('https://localhost:3000/dashboard');
await waitForChartReady(page);
await page.click('.chartjs-legend-item:has-text("Revenue")');
// Re-snapshot; revenue dataset should be hidden
await expect(page.locator('canvas#revenue-chart')).toHaveScreenshot(
'revenue-chart-revenue-hidden.png'
);
});Pin device pixel ratio so CI and dev machines produce identical snapshots:
// playwright.config.ts
use: {
deviceScaleFactor: 1, // pin to 1× for snapshot stability
}| Anti-pattern | Why it fails | Fix |
|---|---|---|
Skip animation: false | Snapshots flaky | Step 1 mandatory |
| Snapshot whole page | Layout shifts unrelated to chart break tests | Snapshot the canvas locator only (Step 2) |
maxDiffPixels: 0 | Anti-aliasing flake | Allow ~50 pixels (Step 2) |
| Test only static data | Dynamic data behavior untested | Snapshot per scenario (filter, range) |
| Skip DPR pinning | CI machines vs dev machines render differently | Step 4 |