CtrlK
BlogDocsLog inGet started
Tessl Logo

testland/chart-render-tests

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

Quality

93%

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

d3.mdreferences/

D3 (SVG) structural snapshot tests

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.

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.md).
  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 d3-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 d3-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 shipd3-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

SKILL.md

tile.json