CtrlK
BlogDocsLog inGet started
Tessl Logo

testland/d3-snapshot-tests

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

Quality

96%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide

SecuritybySnyk

Passed

No findings from the security scan

Overview
Quality
Evals
Security
Files
name:
d3-snapshot-tests
description:
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.
metadata:
{"keywords":"d3, svg-snapshot, jsdom, data-binding, chart-regression"}

d3-snapshot-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.

When to use

  • Custom D3 viz library where DOM structure is the contract.
  • Dashboards using Observable Plot / D3-based React libs (Visx, Nivo, Recharts).
  • Pre-deploy gate before D3 major upgrade (D3 v6 → v7 changed module imports).

How to use

  1. Confirm the chart renders SVG via d3.create('svg') or D3 selections (Canvas belongs to chartjs-snapshot-tests).
  2. Pick the snapshot mode: outerHTML for the structural contract (Step 1) or toHaveScreenshot for rendered pixels (Step 2).
  3. Disable D3 transition() in test mode so snapshots are stable (Step 3).
  4. Add a data-binding test: one element per data point, per-element attributes track the data (Step 4).
  5. For fast headless runs, render the generator under jsdom in a unit test (Step 5).
  6. Cover the update join and SVG a11y metadata via references/advanced-tests.md.
  7. Normalize generated IDs before diffing; run the whole suite as a gate before any D3 major-version upgrade.

Worked example

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.

Step 1 - outerHTML structural snapshot

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();
}

Step 2 - Rendered-image snapshot (Playwright)

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.

Step 3 - Disable transitions

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.

Step 4 - Per-element data-binding test

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
});

Step 5 - jsdom unit test (fast)

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.

Advanced correctness tests

Update join (enter / update / exit) and SVG accessibility metadata are the deepest D3 test patterns - see references/advanced-tests.md.

Anti-patterns

Anti-patternWhy it failsFix
outerHTML diff with all generated IDsFalse positives every runNormalize (Step 1)
Skip transition disableSnapshot flakeStep 3
No update-join testenter/exit bugs shipreferences/advanced-tests.md
Test only happy data shapeEmpty / single-element / overflow data shapes breakBoundary value testing
Mix Chart.js + D3 in same chartCanvas + SVG mix: snapshots inconsistentOne library per chart

Limitations

  • jsdom doesn't compute SVG layout (no getBBox()). Tests that require measured positions need a real browser.
  • D3's modular structure means d3 (full bundle) ≠ individual modules (d3-selection, d3-array); pin import strategy.
  • D3 generates <title> tooltip natively; some libs override - test the actual rendering.

References

  • D3 getting-started docs - install, ESM imports, SVG output, framework integration
  • references/advanced-tests.md - update join + SVG a11y metadata tests
  • chartjs-snapshot-tests - Canvas-based alternative
  • vega-spec-validator - declarative-spec testing alternative
  • qa-accessibility - cross-cutting a11y plugin
Workspace
testland
Visibility
Public
Created
Last updated
Publish Source
GitHub
Badge
testland/d3-snapshot-tests badge