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.
74
93%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
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 |