Skip to content

Commit 82e5f92

Browse files
kurkleclaude
andauthored
feat!: replace Karma and Jasmine with Vitest (v1.0.0) (#34)
* feat!: replace Karma and Jasmine with Vitest 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`. * chore: adopt biome for the Vitest sources Rebased on master now that #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 #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> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent e30614a commit 82e5f92

32 files changed

Lines changed: 2727 additions & 4035 deletions

‎.github/workflows/ci.yml‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,9 @@ jobs:
1616
- uses: actions/setup-node@v7
1717
with:
1818
node-version: 24
19-
- name: Setup and build
20-
run: |
21-
npm ci
22-
npm test
23-
npm run build
19+
cache: npm
20+
- run: npm ci
21+
# The fixture suite renders in real browsers, so they have to be there.
22+
- name: Install browsers
23+
run: npx playwright install --with-deps chromium firefox
24+
- run: npm test

‎.github/workflows/publish-npm.yml‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,13 @@ jobs:
1212
- uses: actions/setup-node@v7
1313
with:
1414
node-version: 24
15+
cache: npm
1516
registry-url: https://registry.npmjs.org/
16-
- name: Setup and build
17-
run: |
18-
npm ci
19-
npm test
20-
npm run build
17+
- run: npm ci
18+
# The browser suite runs in CI on every push; this is the smoke test that
19+
# the published sources at least lint and pass the node specs.
20+
- run: npm run lint
21+
- run: npm run test:unit
2122
- run: npm publish
2223
env:
2324
NODE_AUTH_TOKEN: ${{secrets.NPM_AUTH_TOKEN}}

‎.gitignore‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
dist/
2-
31
# Node.js
42
node_modules/
53
npm-debug.log*

‎README.md‎

Lines changed: 183 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,192 @@
11
# chartjs-test-utils
22

3-
Chart.js test utils package. For usage examples, take a look at these repositories:
3+
Chart.js test utils for [Vitest](https://vitest.dev/) browser mode.
44

5-
- [Chart.js](https://github.com/chartjs/Chart.js)
6-
- [chartjs-plugin-annotation](https://github.com/chartjs/chartjs-plugin-annotation)
7-
- [chartjs-plugin-datalabels](https://github.com/chartjs/chartjs-plugin-datalabels)
5+
`v1` drops Karma and Jasmine: Karma was deprecated in 2023, and the pieces of
6+
this package that existed to work around it (the `__karma__` file scan, reading
7+
fixture configs back over `XMLHttpRequest`, `jasmine.addMatchers`) have no
8+
counterpart in a bundler-driven runner. The rendering rules that make chart
9+
pixels comparable across browsers and platforms are unchanged, and so are the
10+
reference images captured with them.
811

9-
## Development
12+
Consumers still on Karma stay on `0.5.x`.
13+
14+
## Install
15+
16+
```sh
17+
npm install --save-dev chartjs-test-utils vitest @vitest/browser @vitest/browser-playwright playwright
18+
```
19+
20+
## Setup
21+
22+
`Chart` is injected rather than read from a global: Karma loaded the UMD bundle
23+
into `window`, a bundler does not.
24+
25+
```js
26+
// test/setup.js
27+
import {Chart, registerables} from 'chart.js';
28+
import {setup} from 'chartjs-test-utils';
29+
30+
Chart.register(...registerables);
31+
32+
// Registers the matchers, the per-spec chart cleanup and `devicePixelRatio = 1`.
33+
setup({Chart});
34+
```
35+
36+
```js
37+
// vitest.browser.config.ts
38+
import {playwright} from '@vitest/browser-playwright';
39+
import {defineConfig} from 'vitest/config';
40+
41+
export default defineConfig({
42+
test: {
43+
browser: {
44+
enabled: true,
45+
headless: true,
46+
instances: [{browser: 'chromium'}, {browser: 'firefox'}],
47+
// Browser launch options belong to the provider. Vitest accepts a
48+
// `launch` key on an instance and silently ignores it.
49+
provider: playwright({
50+
launchOptions: {
51+
args: ['--disable-accelerated-2d-canvas'],
52+
firefoxUserPrefs: {'gfx.canvas.accelerated': false}
53+
}
54+
})
55+
},
56+
include: ['test/specs/**/*.spec.js'],
57+
setupFiles: ['test/setup.js']
58+
}
59+
});
60+
```
61+
62+
Keep the canvas on the CPU. These are the flags Karma used, and they belong to
63+
the provider: Vitest accepts `launch` or `launchOptions` on an instance and
64+
silently ignores both. Forcing 2d acceleration back on makes no difference to a
65+
handful of fixtures, but in the Chart.js plugin suites it fails several of them
66+
reproducibly.
67+
68+
## Charts
69+
70+
```js
71+
import {acquireChart, releaseChart, triggerMouseEvent} from 'chartjs-test-utils';
72+
73+
const chart = acquireChart(config, {canvas: {height: 256, width: 256}});
74+
await triggerMouseEvent(chart, 'mousemove', chart.getDatasetMeta(0).data[0]);
75+
```
76+
77+
Charts acquired during a spec are released after it. Options that not every
78+
browser supports (`useShadowDOM`, `useOffscreenCanvas`) skip the spec instead of
79+
failing it, which needs the test context — Jasmine's global `pending()` has no
80+
equivalent in Vitest:
81+
82+
```js
83+
it('renders into a shadow root', (ctx) => {
84+
const chart = acquireChart(config, {useShadowDOM: true}, ctx);
85+
});
86+
```
87+
88+
## Fixtures
1089

11-
Linting and formatting are done by [Biome](https://biomejs.dev/), configured in
12-
`biome.jsonc`:
90+
A fixture is a chart config plus a reference PNG of what it should render. The
91+
file lookup has to stay in your repo: `import.meta.glob` resolves against the
92+
file the literal pattern is written in, so `createFixtures` takes the resolved
93+
maps instead of globbing itself.
94+
95+
```js
96+
// test/specs/fixtures.spec.js
97+
import {createFixtures} from 'chartjs-test-utils';
98+
99+
const specsFromFixtures = createFixtures({
100+
configs: {
101+
...import.meta.glob('../fixtures/**/*.js', {eager: true, import: 'default'}),
102+
...import.meta.glob('../fixtures/**/*.json', {eager: true, import: 'default'})
103+
},
104+
images: import.meta.glob('../fixtures/**/*.png', {eager: true, import: 'default', query: '?url'}),
105+
prefix: '../fixtures/'
106+
});
107+
108+
describe('basic', specsFromFixtures('basic'));
109+
```
110+
111+
Text rendering differs between browsers and platforms, so a fixture that draws
112+
text should set `spriteText: true` to blit characters from the bundled sprite
113+
sheet.
114+
115+
### Updating reference images
116+
117+
Reference images can only be produced by a real browser, so producing them is a
118+
mode of the fixture suite. Register the `saveFixtureImage` command only in that
119+
mode — its presence is what the suite detects. A flag would have to go through
120+
`define`, which Vitest re-encodes: `JSON.stringify(false)` arrives in the
121+
browser as the string `"false"`, which is truthy, and every fixture quietly
122+
rewrites itself while reporting a pass.
123+
124+
```ts
125+
// vitest.browser.config.ts
126+
import {createSaveFixtureImage} from 'chartjs-test-utils/node';
127+
128+
const updating = process.env.UPDATE_FIXTURES === '1';
129+
130+
export default defineConfig({
131+
test: {
132+
browser: {
133+
commands: updating ? {saveFixtureImage: createSaveFixtureImage()} : {},
134+
// One browser is the source of truth for the images.
135+
instances: updating ? [{browser: 'chromium'}] : [{browser: 'chromium'}, {browser: 'firefox'}]
136+
}
137+
}
138+
});
139+
```
140+
141+
Only images that actually changed are rewritten, so an update is a reviewable
142+
diff rather than every fixture touched. Regenerating one is never routine: it
143+
means accepting that the output changed.
144+
145+
## Matchers
146+
147+
`setup()` registers all of them.
148+
149+
| Matcher | Checks |
150+
| --- | --- |
151+
| `toEqualImageData(expected, opts)` | rendered canvas against a reference image |
152+
| `toEqualOptions(expected)` | resolved options, ignoring `_`-prefixed properties |
153+
| `toBeValidChart()` | chart, canvas, context and finite size |
154+
| `toBeChartOfSize({dh, dw, rh, rw})` | display and render size |
155+
| `toBeCloseToPixel(expected)` | within 0.5% or 2px |
156+
| `toBeCloseToPoint({x, y})` | rounded to two decimals |
157+
| `toEqualOneOf([...])` | value is one of the expected |
158+
159+
`toEqualImageData` takes `threshold` (per-pixel color distance) and `tolerance`
160+
(accepted ratio of differing pixels). It blends transparency against white:
161+
pixelmatch 7.2.0 made checkerboard blending the default, which is a different
162+
measurement rather than a stricter one, and every reference image this package
163+
has ever compared was captured against white. `checkerboard: true` opts a
164+
fixture in once its image has been re-validated.
165+
166+
## Node
167+
168+
`createMockContext()` records the calls a chart makes to a 2d context, and works
169+
outside the browser:
170+
171+
```js
172+
import {createMockContext} from 'chartjs-test-utils';
173+
174+
const ctx = createMockContext();
175+
ctx.fillRect(1, 2, 3, 4);
176+
ctx.getCalls(); // [{name: 'fillRect', args: [1, 2, 3, 4]}]
177+
```
178+
179+
## Development
13180

14181
```sh
15-
npm run lint # check formatting and lint rules
16-
npm run format # apply the safe fixes
182+
npm run lint # biome check
183+
npm run format # biome check --write
184+
npm run typecheck # the Vitest configs, through tsconfig.tooling.json
185+
npm test # lint, typecheck, node specs, browser specs
186+
npm run dev # the browser suite in watch mode
187+
npm run fixtures:update # rewrite reference images from a Chromium render
17188
```
18189

19-
The formatter settings mirror the `eslint-config-chartjs` style rules it
20-
replaced, so the formatter agrees with the existing sources rather than
21-
restyling them.
190+
Lint and formatting are Biome's, configured in `biome.jsonc`. `src/spriting.js`
191+
is the one file with a rule exception, explained in that config: it is a port of
192+
the 0.5.0 sprite sheet and is kept diffable against it.

‎biome.jsonc‎

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
// (2-space indent, single quotes, semicolons, no spacing inside braces),
66
// so that adopting the formatter is not also a restyling of the sources.
77
"files": {
8-
"includes": ["**/*.js", "**/*.json", "**/*.jsonc", "!dist", "!package-lock.json"]
8+
"includes": ["**/*.js", "**/*.mjs", "**/*.ts", "**/*.json", "**/*.jsonc", "!package-lock.json"]
99
},
1010
"formatter": {
1111
"enabled": true,
@@ -35,31 +35,21 @@
3535
"linter": {
3636
"enabled": true,
3737
"rules": {
38-
"recommended": true,
38+
"preset": "recommended",
3939
"complexity": {
4040
// eslint: complexity [2, 10]. Cognitive complexity is a different
4141
// measure, so the threshold is not the same number.
4242
"noExcessiveCognitiveComplexity": {
4343
"level": "warn",
4444
"options": { "maxAllowedComplexity": 15 }
45-
},
46-
// The sources are written in the ES5 style Karma-era Chart.js used:
47-
// `var`, `function () {}` callbacks, `arguments`, string concatenation.
48-
// Modernizing them is a code change, not a tooling change, so these
49-
// stay off until someone makes it deliberately.
50-
"noArguments": "off",
51-
"useArrowFunction": "off",
52-
"useDateNow": "off",
53-
"useOptionalChain": "off"
54-
},
55-
"correctness": {
56-
// Same reason: this reports `var` declared inside a block.
57-
"noInnerDeclarations": "off"
45+
}
46+
// These were off while the sources were ES5 (`var`, `arguments`,
47+
// string concatenation). The Vitest rewrite has no such code left, so
48+
// they are on: recommended defaults, nothing added.
5849
},
5950
"style": {
6051
// eslint: curly [2, all]
61-
"useBlockStatements": "error",
62-
"useTemplate": "off"
52+
"useBlockStatements": "error"
6353
},
6454
"suspicious": {
6555
// eslint: no-console [2, {allow: [warn, error]}]
@@ -77,6 +67,20 @@
7767
}
7868
}
7969
},
70+
"overrides": [
71+
{
72+
// Ported from 0.5.0 with only the Image guard changed, and kept diffable
73+
// against that version. The optional-chain rewrite of `text && text.charCodeAt`
74+
// is equivalent here only because the loops iterate over `text.length`,
75+
// which is not a property of the guard worth relying on in a port.
76+
"includes": ["src/spriting.js"],
77+
"linter": {
78+
"rules": {
79+
"complexity": { "useOptionalChain": "off" }
80+
}
81+
}
82+
}
83+
],
8084
"assist": {
8185
"actions": {
8286
"source": {

0 commit comments

Comments
 (0)