Skip to content

feat!: replace Karma and Jasmine with Vitest (v1.0.0) - #34

Merged
kurkle merged 2 commits into
chartjs:masterfrom
kurkle:vitest-v1
Sep 12, 2026
Merged

kurkle merged 2 commits into
chartjs:masterfrom
kurkle:vitest-v1

Conversation

@kurkle

@kurkle kurkle commented Sep 12, 2026

Copy link
Copy Markdown
Member

@etimberg — this is a proposal, and the question underneath it is yours to answer before the code matters: is Chart.js heading to Vitest? If the answer is no, this PR should be closed rather than merged; a major nobody adopts is worse than a dormant 0.5.0. If the answer is yes, or maybe, here is a worked path with the traps already paid for.

Why a major rather than a Vitest entry beside the Karma one

Karma was deprecated in 2023. Most of what this package does exists to work around it: scanning __karma__.files to discover fixtures, reading every fixture config back over a synchronous XMLHttpRequest, registering matchers through jasmine.addMatchers, and calling Jasmine's global pending() from inside a helper. None of that has a counterpart in a bundler-driven runner.

My first draft kept the Karma entry and added Vitest as a subpath. That optimizes for a case that does not exist: a major breaks nobody precisely because it is a major. The seven repos that depend on this package (Chart.js, annotation, datalabels, zoom, and the date-fns, luxon and moment adapters) pin ^0.5.0 and will not see 1.0.0 until they choose to. Two entries in one package would instead keep the Karma core alive forever with nobody benefiting.

So: 1.0.0 is Vitest-only, and 0.5.x stays available for Karma consumers.

What does not change

The rules that make chart pixels comparable across browsers and platforms are untouched, and so are the reference images captured with them: the text sprite sheet, the wrapper CSS, devicePixelRatio = 1, and the pixelmatch comparison. pixelmatch moves ^5^7, where 7.2.0 made checkerboard blending the default — that is a different measurement rather than a stricter one (each goes blind where the ink colour meets the background it is blended against), so the matcher keeps blending against white and a fixture can opt into the checkerboard once its reference image has been re-validated against it.

What changes for a consumer

// test/setup.js
import {Chart, registerables} from 'chart.js';
import {setup} from 'chartjs-test-utils';

Chart.register(...registerables);
setup({Chart});   // matchers, per-spec chart cleanup, devicePixelRatio = 1

Chart is injected because Karma loaded the UMD bundle into window and a bundler does not.

// test/specs/fixtures.spec.js — the glob must live in the consumer
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'));

import.meta.glob (like require.context) resolves against the file the literal pattern is written in, so fixture discovery cannot move into the package — only the resolved maps can be passed in. This shapes the whole API and is the one constraint worth knowing before reading the diff.

Jasmine's pending() has no global equivalent, so options that not every browser supports take the test context:

it('renders into a shadow root', (ctx) => acquireChart(config, {useShadowDOM: true}, ctx));

Traps this already accounts for

  • Browser launch flags belong to the provider. Vitest accepts launch or launchOptions on a browser.instances[] entry and silently ignores both. Verified by pointing executablePath at a file that does not exist and watching the run pass anyway.
  • Fixture update mode is detected from the command, not a flag. A define flag is re-encoded by Vitest: JSON.stringify(false) arrives in the browser as the string "false", which is truthy — every fixture then rewrites itself while reporting a pass. So saveFixtureImage (chartjs-test-utils/node) is registered only when updating, and its presence is the switch.
  • Config files need their own tsconfig. rootDir: src keeps the Vitest configs out of the existing projects, so a typo'd or ignored key goes unnoticed. tsconfig.tooling.json is in the typecheck chain, and it is what caught the launch trap above.
  • Coverage in a multi-browser run must be istanbul. Vitest refuses v8 as soon as a non-Chromium instance is configured, because v8 coverage comes over the Chrome DevTools Protocol.

Evidence

A green check and "not measured" look the same from outside, so:

  • The package now tests itself: node specs for the option matcher and mock context, plus a browser suite rendering two fixtures (one of them text, through the sprite sheet) in Chromium and Firefox — 24 passing, 2 skipped, the skips being the ctx.skip() path exercised deliberately.
  • The suite was run against a real consumer: chartjs-chart-treemap's browser tests — 63 pixel fixtures plus controller specs, 142 tests across both browsers — pass with its local test/utils replaced by this package, with no reference image regenerated.
  • Tampering with a fixture colour fails it in both browsers with an identical pixel count, so the comparison is really comparing.
  • That treemap run is also what caught a bug I had introduced while making the sprite sheet lazy: an Image that has not finished decoding draws nothing, so text vanished from the first fixture that drew any — and the reference captured under the same flaw compared clean. The sheet decodes at import again, guarded so a node import does not touch Image.
  • npm pack + install into a scratch project: the entry and chartjs-test-utils/node both resolve through the package name, deep imports are blocked by the exports map, and publint is clean.

Packaging note

The package publishes its sources instead of a rollup bundle, so pixelmatch resolves as a normal dependency rather than being inlined, and the exports map can keep the node-only fixture writer (node:fs) out of the browser entry. CI runs lint, typecheck and both suites on Node 24 with Chromium and Firefox installed.

ESLint stays at 8 here because eslint-config-chartjs is eslintrc-only; #33 proposes Biome instead, which removes that constraint. The two PRs are independent and will conflict textually in src/ and package.json — whichever lands first, I will rebase the other.

Not in this PR

Migrating the Chart.js suite itself. It is an order of magnitude larger than any plugin repo I have migrated, and that migration needs a Chart.js maintainer driving it. This offers a path, not a commitment to walk it for you.

🤖 Generated with Claude Code

@kurkle

kurkle commented Sep 12, 2026

Copy link
Copy Markdown
Member Author

Two things this touches that are already open here:

  • Bump ua-parser-js from 0.7.31 to 0.7.33 #32 (dependabot, ua-parser-js): that advisory reaches this repo only through karma@6.4.1, which npm installs because karma is a peer dependency. Dropping the Karma peers removes the whole chain — ua-parser-js and karma both disappear from the lockfile on this branch (0 occurrences of either). The same thing happened in the plugin repos I migrated: npm audit went to zero without a single dependency being upgraded.
  • Fixture (re)generation #30 (Fixture (re)generation, from 2022): this supersedes it. Fixture regeneration is here as a mode of the suite rather than a separate path — npm run fixtures:update registers a saveFixtureImage browser command, and only images that actually changed are rewritten, so an update stays a reviewable diff. Happy to close Fixture (re)generation #30 if this direction is the one you want.

🤖 Generated with Claude Code

etimberg
etimberg previously approved these changes Sep 12, 2026

@etimberg etimberg left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Happy to go this direction. I generally use vitest professionally these days but haven't thought about switching chart.js to use it. Maybe claude can help there too

Karma was deprecated in 2023, and most of what this package did existed to
work around it: scanning `__karma__.files` to find fixtures, reading every
fixture config back over `XMLHttpRequest`, and registering matchers through
`jasmine.addMatchers`. None of it has a counterpart in a bundler-driven
runner, so v1 drops the Karma and Jasmine peers instead of keeping a second
entry point alive beside them. Consumers still on Karma stay on 0.5.x.

What the rendering rules do is unchanged, and so are the reference images
captured with them: the sprite sheet, the wrapper CSS, `devicePixelRatio = 1`
and the pixelmatch comparison all behave as before. pixelmatch moves 5 -> 7,
where `checkerboard` blending became the default in 7.2.0; that is a different
measurement rather than a stricter one, so the matcher keeps blending against
white and a fixture opts into the checkerboard per comparison.

Notable changes:

- `setup({Chart})` takes the Chart.js constructor instead of reading a global.
  Karma loaded the UMD bundle into `window`, a bundler does not.
- `createFixtures({configs, images})` takes the resolved file maps, because
  `import.meta.glob` resolves against the file the literal pattern is written
  in. The glob has to stay in the consumer; only the map can move here.
- `pending()` becomes `ctx.skip()`, so `useShadowDOM` and `useOffscreenCanvas`
  need the test context passed to `acquireChart`.
- Fixture images are rewritten by a `saveFixtureImage` browser command
  (`chartjs-test-utils/node`), registered only when updating. The suite detects
  the mode from the command's presence rather than a `define` flag, which
  Vitest re-encodes: `JSON.stringify(false)` arrives as the truthy string
  "false" and every fixture quietly rewrites itself while reporting a pass.
- The package publishes its sources instead of a rollup bundle, so pixelmatch
  resolves as a normal dependency rather than being inlined.
- The package now tests itself: node specs for the option matcher and the mock
  context, and a browser suite that renders two fixtures in Chromium and
  Firefox. CI installs both browsers and runs lint, typecheck and both suites.

Verified against a real suite: chartjs-chart-treemap's browser tests (63 pixel
fixtures plus the controller specs, 142 tests across both browsers) pass with
its `test/utils` replaced by this package and no reference image regenerated.
That run is what caught the sprite sheet being decoded lazily, which silently
dropped text from the first fixture that drew any.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

BREAKING CHANGE: Karma and Jasmine are no longer supported. The package
requires Vitest, `setup({Chart})` must be called from a setup file, and
`specsFromFixtures` is now built by `createFixtures`.
@kurkle

kurkle commented Sep 12, 2026

Copy link
Copy Markdown
Member Author

Happy to go this direction. I generally use vitest professionally these days but haven't thought about switching chart.js to use it. Maybe claude can help there too

It very likely can 😄

Rebased on master now that chartjs#33 has landed. The mechanical part of the rebase
kept this branch's files; this commit is the part that is not mechanical.

- eslint and eslint-config-chartjs are gone from `devDependencies`, `lint` and
  `format` are Biome, and the `eslint-disable` pragmas in the new sources are
  gone: two for `callback-return`, a rule Biome does not have, and two for
  `no-console`, now `biome-ignore lint/suspicious/noConsole` with the reason on
  the same line -- a reason wrapped onto the next line suppresses nothing and
  reports itself as an unused suppression.
- `biome.jsonc` lints `.ts` and `.mjs` too, so the Vitest configs and the
  fixture script are covered.
- The rule exceptions chartjs#33 needed for the ES5-era sources are lifted:
  `useArrowFunction`, `noArguments`, `useOptionalChain`, `useTemplate` and
  `noInnerDeclarations` are back on Biome's recommended settings, because the
  rewrite has no `var`-in-block, `arguments` or string concatenation left.
  `src/spriting.js` keeps `useOptionalChain` off in an `overrides` block: it is
  a port of the 0.5.0 sprite sheet, and rewriting `text && text.charCodeAt` as
  `text?.charCodeAt` is equivalent only because the loops iterate over
  `text.length`.
- With those rules on, Biome found four real things in the new code, all fixed
  rather than silenced: three `forEach` callbacks whose concise arrow bodies
  returned a value (now `for...of`), the `chart.$test || {}` guards (now
  optional chaining), a `var me = this` left useless once the mock context's
  method wrappers became arrows, and nine string concatenations in the matcher
  messages -- which the unit specs assert verbatim, so they are covered.
- `recommended: true` is deprecated in Biome 2.5; it is now `preset:
  "recommended"`.

`biome check` is clean with no warnings. The suites still pass: 7 node specs,
24 browser specs plus the 2 deliberate skips, and chartjs-chart-treemap's
browser suite -- 142 tests across Chromium and Firefox -- still passes against
this package with no reference image regenerated.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@kurkle

kurkle commented Sep 12, 2026

Copy link
Copy Markdown
Member Author

Rebased onto master now that #33 is in, and force-pushed. Your approval predates this, so here is what changed — the rebase itself was mechanical (this branch rewrites the files #33 reformatted, so the resolution was "keep the rewrite"), but adopting Biome was not:

  • eslint and eslint-config-chartjs are out of devDependencies; lint and format are Biome. The eslint-disable pragmas are gone: two for callback-return (no such rule in Biome) and two for no-console, now biome-ignore lint/suspicious/noConsole with the reason on the same line — wrapping the reason onto the next line suppresses nothing and reports itself as an unused suppression.
  • biome.jsonc now also covers .ts and .mjs, so the Vitest configs and the fixture script are linted.
  • The ES5-era exceptions chore: replace eslint with biome #33 needed are lifted: useArrowFunction, noArguments, useOptionalChain, useTemplate and noInnerDeclarations are back on the recommended settings, since the rewrite has no var-in-block, arguments or string concatenation left. One overrides entry remains, for src/spriting.js: it is a port of the 0.5.0 sprite sheet, and text && text.charCodeAttext?.charCodeAt is equivalent there only because the loops iterate over text.length.
  • With those rules on, Biome found four real things in the new code, all fixed rather than silenced: three forEach callbacks whose concise arrow bodies returned a value (now for...of), the chart.$test || {} guards (now optional chaining), a var me = this left useless once the mock context wrappers became arrows, and nine concatenations in the matcher messages — which the unit specs assert verbatim, so that change is covered.
  • recommended: true is deprecated in Biome 2.5, so it is preset: "recommended" now.

Re-verified after the rebase, not assumed: biome check clean with zero warnings, typecheck clean, 7 node specs, 24 browser specs plus the 2 deliberate skips in Chromium and Firefox, and chartjs-chart-treemap's browser suite — 142 tests across both browsers — still passes against this package with no reference image regenerated.

If you would rather review the delta than the whole thing again, the second commit (chore: adopt biome for the Vitest sources) is exactly it; the squash merge will collapse both.

🤖 Generated with Claude Code

@kurkle
kurkle merged commit 82e5f92 into chartjs:master Sep 12, 2026
1 check passed
@kurkle kurkle mentioned this pull request Sep 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants