vega-spec-validator
Validate Vega + Vega-Lite specifications against the JSON Schema (vega.github.io/schema), test cross-engine compatibility (Vega-Lite compiles to Vega per the canonical compiler), and verify data-binding correctness. Pair with d3-snapshot-tests when Vega specs render to SVG; pair with chartjs-snapshot-tests when rendered to Canvas. Use when application code generates Vega/Vega-Lite JSON specs at runtime (BI builders, spec templating) and those specs must be proven valid and correctly encoded before render, or before a Vega major-version upgrade.
Install with skills.sh (any agent)
npx skills add testland/qa --skill vega-spec-validatorvega-spec-validator
Vega-Lite specs (Mark, Encoding, Data) compile to Vega, which renders to SVG/Canvas, per the Vega-Lite docs (opens in new window). Tests validate a spec is well-formed AND produces the expected rendered output.
When to use
How to use
Step 1 - JSON Schema validation
Per the Vega-Lite docs (opens in new window), the spec is JSON; it has a published JSON Schema. Validate:
import Ajv from 'ajv';
import schema from 'vega-lite/build/vega-lite-schema.json';
const ajv = new Ajv({ strict: false });
const validate = ajv.compile(schema);
test('generated bar spec is valid Vega-Lite', () => {
const spec = generateBarSpec({ x: 'quarter', y: 'revenue' });
const valid = validate(spec);
if (!valid) {
console.log(validate.errors);
}
expect(valid).toBe(true);
});Schema URL pattern: https://vega.github.io/schema/vega-lite/v5.json (track current version per the Vega-Lite docs (opens in new window)).
Step 2 - Spec structural assertions
Beyond schema validity, assert business-relevant structure:
test('spec uses correct mark + encoding for bar chart', () => {
const spec = generateBarSpec({ x: 'quarter', y: 'revenue' });
expect(spec.mark.type).toBe('bar');
expect(spec.encoding.x.field).toBe('quarter');
expect(spec.encoding.x.type).toBe('nominal');
expect(spec.encoding.y.field).toBe('revenue');
expect(spec.encoding.y.type).toBe('quantitative');
});Step 3 - Compilation test (Vega-Lite → Vega)
Per the Vega-Lite docs (opens in new window): "Vega-Lite compiles a Vega-Lite specification into a lower-level, more detailed Vega specifications and rendered using Vega's compiler."
import * as vl from 'vega-lite';
test('Vega-Lite compiles to valid Vega', () => {
const vlSpec = generateBarSpec(...);
const vegaSpec = vl.compile(vlSpec).spec;
// Validate Vega spec against Vega schema
const vegaValid = validateVegaSchema(vegaSpec);
expect(vegaValid).toBe(true);
});Failed compilation indicates the Vega-Lite spec is well-formed schema-wise but semantically broken (e.g., references a non-existent field).
Worked example
One test that proves a BI builder's bar-spec generator produces a spec that is schema-valid, correctly encoded, AND compiles - the three core gates in a single first run:
import Ajv from 'ajv';
import vlSchema from 'vega-lite/build/vega-lite-schema.json';
import * as vl from 'vega-lite';
const validate = new Ajv({ strict: false }).compile(vlSchema);
test('generated bar spec is valid, correctly encoded, and compiles', () => {
const spec = generateBarSpec({ x: 'quarter', y: 'revenue' });
// Gate 1 - schema-valid
expect(validate(spec)).toBe(true);
// Gate 2 - correct mark + encoding
expect(spec.mark.type).toBe('bar');
expect(spec.encoding.x.field).toBe('quarter');
expect(spec.encoding.y.type).toBe('quantitative');
// Gate 3 - compiles to Vega without throwing
expect(() => vl.compile(spec)).not.toThrow();
});That single test is the minimum a runtime-generated spec must pass before render. Render-time and semantic verification build on it - see the advanced reference below.
Advanced tests
Render-to-SVG assertions, multi-view composition (facet / layer / concat / repeat), data-transform verification, interaction parameters, and spec-snapshot regression each get a full worked test in references/advanced-spec-tests.md.
Anti-patterns
| Anti-pattern | Why it fails | Fix |
|---|---|---|
| Skip JSON Schema validation; compile direct | Compiler errors are cryptic | Step 1 first |
| Test only the rendered output, not the spec | Spec gen bugs hide behind correct render | Step 2 + spec snapshot (references) |
| Hardcoded Vega-Lite v4 schema | Schema upgrades change validity | Pin AND track |
| Skip transform tests | Aggregate / filter bugs ship silently | Transform test (references) |
Use mark: 'bar' shorthand mixed with object form | Schema accepts both; downstream code may not | Pick one form per project |
Limitations
References
Vega/Vega-Lite advanced spec tests
View source (opens in new window)Vega/Vega-Lite advanced spec tests
Deep reference for the vega-spec-validator SKILL. Consult after the core schema + structure + compilation gates pass, when adding render-time, composition, transform, interaction, or spec-snapshot coverage.
Render-to-SVG test
import { Vega } from 'react-vega';
import { create } from 'jsdom';
test('renders SVG with expected mark count', async () => {
const dom = create('<div id="vis"></div>');
global.document = dom.window.document;
const view = new vega.View(vega.parse(vegaSpec))
.renderer('svg')
.initialize(dom.window.document.querySelector('#vis'))
.run();
const svg = await view.toSVG();
// Use a parser to count <path>/<rect> elements
const rectCount = (svg.match(/<rect/g) || []).length;
expect(rectCount).toBe(4); // 4 quarters
});Multi-view composition test
Per the Vega-Lite docs (opens in new window), Vega-Lite supports faceting, layering, concatenation, repeating. Test each composition:
test('layered spec has 2 layers', () => {
const spec = {
layer: [
{ mark: 'line', encoding: {...} },
{ mark: 'point', encoding: {...} },
],
};
expect(validate(spec)).toBe(true);
expect(spec.layer).toHaveLength(2);
});
test('faceted spec creates one view per category', async () => {
const spec = {
facet: { field: 'category', type: 'nominal' },
spec: { mark: 'bar', encoding: {...} },
};
const compiled = vl.compile(spec).spec;
const view = new vega.View(vega.parse(compiled)).renderer('svg').initialize(...).run();
// Inspect view's data tables to verify N facets emerged
});Data transform test
Per the Vega-Lite docs (opens in new window), transforms include "Aggregate, filter, bin, calculate, fold, pivot." Test transform output:
test('aggregate transform produces correct sum', () => {
const spec = {
data: { values: [
{ region: 'NA', revenue: 100 },
{ region: 'NA', revenue: 200 },
{ region: 'EU', revenue: 150 },
]},
transform: [
{ aggregate: [{ op: 'sum', field: 'revenue', as: 'total' }],
groupby: ['region'] },
],
mark: 'bar',
encoding: { x: { field: 'region' }, y: { field: 'total', type: 'quantitative' } },
};
const view = new vega.View(vega.parse(vl.compile(spec).spec));
await view.runAsync();
const data = view.data('source_0');
expect(data.find(d => d.region === 'NA').total).toBe(300);
expect(data.find(d => d.region === 'EU').total).toBe(150);
});Interaction (parameters / selections)
Per the Vega-Lite docs (opens in new window), "Interactive parameters - Selections and value bindings" enable interaction. Test parameters resolve:
test('selection parameter filters data', async () => {
const spec = {
params: [{ name: 'brush', select: 'interval' }],
data: {...},
mark: 'point',
encoding: {...},
transform: [{ filter: { param: 'brush' } }],
};
// Compile, render, inject brush event, verify filtered data
...
});Spec-snapshot regression
For complex spec-generation logic, snapshot the spec output:
test('quarterly-revenue spec snapshot stable', () => {
const spec = generateBarSpec({
x: 'quarter',
y: 'revenue',
title: 'Quarterly Revenue',
});
expect(spec).toMatchSnapshot('quarterly-revenue.spec.json');
});When intentionally changing spec generation, regenerate snapshots: UPDATE_SNAPSHOTS=1 npm test.
Related skills
chartjs-snapshot-tests
Snapshot-test Chart.js charts - render via headless Chromium / jsdom + canvas mock, capture canvas pixels via `canvas.toDataURL()` + image-diff, disable animations (`options.animation = false`) for stable snapshots, test tooltip + legend interactions. Use when a project renders Chart.js charts and needs regression coverage of their rendered output.
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.