Snapshot-test D3.js charts - D3 generates SVG (not Canvas, per d3js.org getting-started); use `outerHTML` snapshot for static structure, `toHaveScreenshot` for rendered SVG; jsdom for headless render in unit tests; disable transitions for stable snapshots; per-element data-binding correctness tests. Use when a project renders charts with `d3.create('svg')` or D3 selections and the emitted SVG structure or update join needs regression coverage - including before a D3 major-version upgrade.
76
96%
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
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-snapshot-tests).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
references/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 | references/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.chartjs-snapshot-tests -
Canvas-based alternativevega-spec-validator -
declarative-spec testing alternativeqa-accessibility -
cross-cutting a11y plugin