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
D3 reference for chart-render-tests: use outerHTML snapshot for
static structure, toHaveScreenshot for rendered SVG, jsdom for
headless render in unit tests, disabled transitions for stable
snapshots, and per-element data-binding correctness tests.
Per the D3 getting-started docs, D3 "generates SVG output (not
Canvas). Code examples show creation of SVG elements with
d3.create('svg') and DOM manipulation via selections." SVG is
text-DOM, so snapshots can be the rendered SVG markup OR a rendered
image - different test patterns for each.
d3.create('svg') or D3
selections (Canvas belongs to chartjs.md).outerHTML for the structural contract
(Step 1) or toHaveScreenshot for rendered pixels (Step 2).transition() in test mode so snapshots are stable
(Step 3).A revenue bar chart renders svg.bar-chart from [10, 20, 30, 40].
Capture its outerHTML, pass it through normalizeSvg (Step 1) to
strip generated IDs, and store bar-chart.svg.txt. Add a
data-binding test (Step 4) asserting 4 rects and
heights[3] > heights[0] (data 40 > 10). The 750 ms enter
transition is swapped for the identity function in test mode
(Step 3) so the snapshot is deterministic. Suite runs green. Later a
D3 v6 → v7 upgrade drops a <g> wrapper import; the outerHTML
snapshot diff flags the missing group before it ships.
For tests where SVG structure should match exactly:
import { test, expect } from '@playwright/test';
test('bar chart SVG has expected structure', async ({ page }) => {
await page.goto('https://localhost:3000/d3-bar');
await page.waitForSelector('svg.bar-chart');
const svgHtml = await page.locator('svg.bar-chart').evaluate(el => el.outerHTML);
// Compare to stored fixture
expect(normalizeSvg(svgHtml)).toMatchSnapshot('bar-chart.svg.txt');
});normalizeSvg strips dynamically-generated IDs (__id__123) +
whitespace differences:
function normalizeSvg(svg: string): string {
return svg
.replace(/id="[^"]*-\d+"/g, 'id="ID"')
.replace(/\s+/g, ' ')
.trim();
}For visual-regression-style:
test('scatter plot renders correctly', async ({ page }) => {
await page.goto('https://localhost:3000/d3-scatter');
await page.waitForSelector('svg.scatter');
await expect(page.locator('svg.scatter')).toHaveScreenshot('scatter.png', {
maxDiffPixels: 50,
});
});Per Chart.js docs equivalent works for D3 too - snapshot the locator, not the page.
D3 transition() calls animate. Disable for tests:
// Instead of d3.select(...).transition().duration(750).attr(...)
// In test mode:
const transition = process.env.NODE_ENV === 'test'
? (sel) => sel // identity
: (sel) => sel.transition().duration(750);
transition(d3.select('.bars').selectAll('rect'))
.attr('width', d => x(d.value));Or use d3.transition().duration(0) if API can't be conditional.
D3's strength is data-driven DOM. Test the binding holds:
test('one rect per data point', async ({ page }) => {
const data = [10, 20, 30, 40];
await page.goto(`https://localhost:3000/d3-bar?data=${JSON.stringify(data)}`);
await page.waitForSelector('svg.bar-chart rect');
const rects = await page.locator('svg.bar-chart rect').count();
expect(rects).toBe(data.length);
// Per-rect height matches data
const heights = await page.locator('svg.bar-chart rect').evaluateAll(els =>
els.map(el => parseFloat(el.getAttribute('height')!))
);
expect(heights[3]).toBeGreaterThan(heights[0]); // data[3]=40 > data[0]=10
});import { JSDOM } from 'jsdom';
import * as d3 from 'd3';
test('bar generator emits N rects for N data points', () => {
const dom = new JSDOM('<svg id="chart"></svg>');
global.document = dom.window.document;
const data = [1, 2, 3];
d3.select(dom.window.document.body)
.select('svg')
.selectAll('rect')
.data(data)
.join('rect')
.attr('height', d => d);
const rects = dom.window.document.querySelectorAll('rect');
expect(rects).toHaveLength(3);
});Per the D3 getting-started docs, D3 imports cleanly under ESM - jsdom + native ESM works.
Update join (enter / update / exit) and SVG accessibility
metadata are the deepest D3 test patterns - see
d3-advanced-tests.md.
| Anti-pattern | Why it fails | Fix |
|---|---|---|
| outerHTML diff with all generated IDs | False positives every run | Normalize (Step 1) |
| Skip transition disable | Snapshot flake | Step 3 |
| No update-join test | enter/exit bugs ship | d3-advanced-tests.md |
| Test only happy data shape | Empty / single-element / overflow data shapes break | Boundary value testing |
| Mix Chart.js + D3 in same chart | Canvas + SVG mix: snapshots inconsistent | One library per chart |
getBBox()). Tests that
require measured positions need a real browser.d3 (full bundle) ≠ individual
modules (d3-selection, d3-array); pin import strategy.<title> tooltip natively; some libs override -
test the actual rendering.qa-accessibility -
cross-cutting a11y plugin