Chart.js test utils for Vitest browser mode.
v1 drops Karma and Jasmine: Karma was deprecated in 2023, and the pieces of
this package that existed to work around it (the __karma__ file scan, reading
fixture configs back over XMLHttpRequest, jasmine.addMatchers) have no
counterpart in a bundler-driven runner. The rendering rules that make chart
pixels comparable across browsers and platforms are unchanged, and so are the
reference images captured with them.
Consumers still on Karma stay on 0.5.x.
npm install --save-dev chartjs-test-utils vitest @vitest/browser @vitest/browser-playwright playwrightChart is injected rather than read from a global: Karma loaded the UMD bundle
into window, a bundler does not.
// test/setup.js
import {Chart, registerables} from 'chart.js';
import {setup} from 'chartjs-test-utils';
Chart.register(...registerables);
// Registers the matchers, the per-spec chart cleanup and `devicePixelRatio = 1`.
setup({Chart});// vitest.browser.config.ts
import {playwright} from '@vitest/browser-playwright';
import {defineConfig} from 'vitest/config';
export default defineConfig({
test: {
browser: {
enabled: true,
headless: true,
instances: [{browser: 'chromium'}, {browser: 'firefox'}],
// Browser launch options belong to the provider. Vitest accepts a
// `launch` key on an instance and silently ignores it.
provider: playwright({
launchOptions: {
args: ['--disable-accelerated-2d-canvas'],
firefoxUserPrefs: {'gfx.canvas.accelerated': false}
}
})
},
include: ['test/specs/**/*.spec.js'],
setupFiles: ['test/setup.js']
}
});Keep the canvas on the CPU. These are the flags Karma used, and they belong to
the provider: Vitest accepts launch or launchOptions on an instance and
silently ignores both. Forcing 2d acceleration back on makes no difference to a
handful of fixtures, but in the Chart.js plugin suites it fails several of them
reproducibly.
import {acquireChart, releaseChart, triggerMouseEvent} from 'chartjs-test-utils';
const chart = acquireChart(config, {canvas: {height: 256, width: 256}});
await triggerMouseEvent(chart, 'mousemove', chart.getDatasetMeta(0).data[0]);Charts acquired during a spec are released after it. Options that not every
browser supports (useShadowDOM, useOffscreenCanvas) skip the spec instead of
failing it, which needs the test context — Jasmine's global pending() has no
equivalent in Vitest:
it('renders into a shadow root', (ctx) => {
const chart = acquireChart(config, {useShadowDOM: true}, ctx);
});A fixture is a chart config plus a reference PNG of what it should render. The
file lookup has to stay in your repo: import.meta.glob resolves against the
file the literal pattern is written in, so createFixtures takes the resolved
maps instead of globbing itself.
// test/specs/fixtures.spec.js
import {createFixtures} from 'chartjs-test-utils';
const specsFromFixtures = createFixtures({
configs: {
...import.meta.glob('../fixtures/**/*.js', {eager: true, import: 'default'}),
...import.meta.glob('../fixtures/**/*.json', {eager: true, import: 'default'})
},
images: import.meta.glob('../fixtures/**/*.png', {eager: true, import: 'default', query: '?url'}),
prefix: '../fixtures/'
});
describe('basic', specsFromFixtures('basic'));Text rendering differs between browsers and platforms, so a fixture that draws
text should set spriteText: true to blit characters from the bundled sprite
sheet.
Reference images can only be produced by a real browser, so producing them is a
mode of the fixture suite. Register the saveFixtureImage command only in that
mode — its presence is what the suite detects. A flag would have to go through
define, which Vitest re-encodes: JSON.stringify(false) arrives in the
browser as the string "false", which is truthy, and every fixture quietly
rewrites itself while reporting a pass.
// vitest.browser.config.ts
import {createSaveFixtureImage} from 'chartjs-test-utils/node';
const updating = process.env.UPDATE_FIXTURES === '1';
export default defineConfig({
test: {
browser: {
commands: updating ? {saveFixtureImage: createSaveFixtureImage()} : {},
// One browser is the source of truth for the images.
instances: updating ? [{browser: 'chromium'}] : [{browser: 'chromium'}, {browser: 'firefox'}]
}
}
});Only images that actually changed are rewritten, so an update is a reviewable diff rather than every fixture touched. Regenerating one is never routine: it means accepting that the output changed.
setup() registers all of them.
| Matcher | Checks |
|---|---|
toEqualImageData(expected, opts) |
rendered canvas against a reference image |
toEqualOptions(expected) |
resolved options, ignoring _-prefixed properties |
toBeValidChart() |
chart, canvas, context and finite size |
toBeChartOfSize({dh, dw, rh, rw}) |
display and render size |
toBeCloseToPixel(expected) |
within 0.5% or 2px |
toBeCloseToPoint({x, y}) |
rounded to two decimals |
toEqualOneOf([...]) |
value is one of the expected |
toEqualImageData takes threshold (per-pixel color distance) and tolerance
(accepted ratio of differing pixels). It blends transparency against white:
pixelmatch 7.2.0 made checkerboard blending the default, which is a different
measurement rather than a stricter one, and every reference image this package
has ever compared was captured against white. checkerboard: true opts a
fixture in once its image has been re-validated.
The package ships type declarations, generated from the JSDoc in src by
npm run types so they cannot drift from the implementation. Chart instances
are typed as chart.js's own Chart, through a type-only import — chart.js is
not a runtime dependency of this package, it is injected into setup().
The matchers are declared as an augmentation of Vitest's Matchers interface,
so expect(chart).toEqualImageData(...) typechecks once the package is
imported anywhere in the project. That part is hand-written in
types/entry.d.ts, since a declaration emit cannot produce it; the mock
context's recorded methods are declared there too, because the mock assigns
them in a loop. A unit test compares both lists against the implementation, so
they cannot fall behind it silently.
createMockContext() records the calls a chart makes to a 2d context, and works
outside the browser:
import {createMockContext} from 'chartjs-test-utils';
const ctx = createMockContext();
ctx.fillRect(1, 2, 3, 4);
ctx.getCalls(); // [{name: 'fillRect', args: [1, 2, 3, 4]}]npm run lint # biome check
npm run format # biome check --write
npm run typecheck # the Vitest configs, through tsconfig.tooling.json
npm run types # emit the declarations into types/generated
npm test # lint, typecheck, node specs, browser specs
npm run dev # the browser suite in watch mode
npm run fixtures:update # rewrite reference images from a Chromium renderLint and formatting are Biome's, configured in biome.jsonc. src/spriting.js
is the one file with a rule exception, explained in that config: it is a port of
the 0.5.0 sprite sheet and is kept diffable against it.