|
1 | 1 | # chartjs-test-utils |
2 | 2 |
|
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. |
4 | 4 |
|
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. |
8 | 11 |
|
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 |
10 | 89 |
|
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 |
13 | 180 |
|
14 | 181 | ```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 |
17 | 188 | ``` |
18 | 189 |
|
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. |
0 commit comments