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
View guide
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