vitest-environment-webgpu-node 2.0.1 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,13 +3,37 @@
3
3
  [![npm version][npm-badge]][npm-url]
4
4
  [![npm downloads][downloads-badge]][npm-url]
5
5
  [![Tests][tests-badge]][tests-url]
6
+ [![Coverage][coverage-badge]][coverage-url]
6
7
  [![Discord][discord-badge]][discord-url]
7
8
 
8
- Real, headless WebGPU inside Vitest, with no browser. Native GPU testing is up to 2.4x faster
9
- than running the same tests in a browser — see the
10
- [blog post](https://ben3d.ca/blog/native-gpu-testing-for-vitest-and-jest) for details.
11
- Powered by Google's Dawn through the
12
- [`webgpu`](https://github.com/dawn-gpu/node-webgpu) npm package.
9
+ Run real WebGPU tests in Vitest on Node, without a browser, Playwright or mocks. Headless GPU testing
10
+ for CI: `navigator.gpu` is backed by Google's Dawn through the
11
+ [`webgpu`](https://github.com/dawn-gpu/node-webgpu) npm package. Native GPU testing is up to 2.4x faster
12
+ than running the same tests in a browser, see the
13
+ [blog post](https://ben3d.ca/blog/native-gpu-testing-for-vitest-and-jest).
14
+
15
+ ```sh
16
+ pnpm add -D vitest vitest-environment-webgpu-node
17
+ ```
18
+
19
+ ```ts
20
+ // vitest.config.ts
21
+ import { defineConfig } from 'vitest/config';
22
+
23
+ export default defineConfig({ test: { environment: 'webgpu-node' } });
24
+ ```
25
+
26
+ ```ts
27
+ // gpu.test.ts
28
+ import { expect, it } from 'vitest';
29
+
30
+ it('has a real GPU device', async () => {
31
+ const adapter = await navigator.gpu.requestAdapter();
32
+ const device = await adapter!.requestDevice();
33
+ const buffer = device.createBuffer({ size: 16, usage: GPUBufferUsage.COPY_DST });
34
+ expect(buffer.size).toBe(16);
35
+ });
36
+ ```
13
37
 
14
38
  The environment puts `navigator.gpu` and every `GPU*` class and constant (`GPUBufferUsage`,
15
39
  `GPUShaderStage`, ...) on the global object before each test file and removes them afterwards.
@@ -18,13 +42,30 @@ Using Jest instead? See the equivalent
18
42
  [`jest-environment-webgpu-node`](https://github.com/bhouston/jest-gpu/tree/main/packages/jest-environment-webgpu-node)
19
43
  in [jest-gpu](https://github.com/bhouston/jest-gpu).
20
44
 
21
- ## Install
45
+ ## Compared to alternatives
22
46
 
23
- ```sh
24
- pnpm add -D vitest vitest-environment-webgpu-node
25
- ```
47
+ WebGPU has no mock equivalent of a canvas stub that runs shaders, so the realistic options are a browser or
48
+ Dawn in Node.
49
+
50
+ | Approach | Real rendering | Notes |
51
+ | --------------------------------- | -------------- | ------------------------------------------------------------------------------------------------------------- |
52
+ | Hand-written `navigator.gpu` mock | No | You can assert calls, not shader output or readbacks. |
53
+ | Vitest browser mode or Playwright | Yes | A real browser with WebGPU enabled: heavier to install and slower to start; right when you need browser APIs. |
54
+ | vitest-environment-webgpu-node | Yes | Dawn in the Node process (Metal, Vulkan, D3D12 and others via `dawnOptions`). Not a browser. |
55
+
56
+ ## Supported versions and platforms
57
+
58
+ - Vitest 4 and 5 (CI tests both). Vitest 3 is not supported.
59
+ - Node 22 or later. CI runs Node 22 and 26 on `macos-latest` and `ubuntu-latest`.
60
+ - Type checking needs TypeScript 6 or later (see [TypeScript types](#typescript-types)).
61
+ - Linux without a GPU needs Mesa's software Vulkan driver, see [Linux and CI](#linux-and-ci). This repo's CI does
62
+ not run on Windows.
26
63
 
27
- Type checking needs TypeScript 6 or later (see [TypeScript types](#typescript-types)).
64
+ ## Useful for
65
+
66
+ three.js `WebGPURenderer`, Babylon Lite, Vercel's vgpu and raw WebGPU compute or render code. The demos exercise
67
+ those three; other WebGPU libraries such as TypeGPU or luma.gl use the same API but are not covered by this
68
+ repo's tests. These are compatible use cases, not endorsements.
28
69
 
29
70
  ## Usage
30
71
 
@@ -77,7 +118,7 @@ sudo apt-get update && sudo apt-get install -y libegl1 libgles2 libgl1-mesa-dri
77
118
  export LIBGL_ALWAYS_SOFTWARE=1
78
119
  ```
79
120
 
80
- This repo's own [CI workflow](../../.github/workflows/ci.yml) does exactly this, so WebGPU tests run
121
+ This repo's own [CI workflow](https://github.com/bhouston/vitest-gpu/blob/main/.github/workflows/ci.yml) does exactly this, so WebGPU tests run
81
122
  for real (not mocked) on `ubuntu-latest` on every PR. macOS runners use Dawn's Metal backend and need no
82
123
  extra setup.
83
124
 
@@ -100,12 +141,24 @@ await expect(canvas).toMatchScreenshot('scene.png');
100
141
  ```
101
142
 
102
143
  `readPixels()` returns `{ width, height, data }` RGBA8 pixels, ready for
103
- [`vitest-screenshot`](../vitest-screenshot). Readback supports `rgba8unorm`, `rgba8unorm-srgb`,
144
+ [`vitest-screenshot`](https://github.com/bhouston/vitest-gpu/tree/main/packages/vitest-screenshot). Readback supports `rgba8unorm`, `rgba8unorm-srgb`,
104
145
  `bgra8unorm`, and `bgra8unorm-srgb`; other canvas formats remain valid for rendering but
105
146
  `readPixels()` rejects them with an unsupported-format error. `asElement()` returns the same object and
106
147
  defaults to `HTMLCanvasElement` when DOM types are available. You can also request a library-specific type
107
148
  explicitly with `canvas.asElement<HTMLCanvasElement>()`.
108
149
 
150
+ ## Image snapshots
151
+
152
+ `await expect(canvas).toMatchScreenshot('scene.png')` comes from [`vitest-screenshot`](https://github.com/bhouston/vitest-gpu/tree/main/packages/vitest-screenshot).
153
+ Call `extendMatchers()` from it once, then pass the canvas as in the example above.
154
+
155
+ ## See also
156
+
157
+ - [`vitest-environment-webgl-node`](https://github.com/bhouston/vitest-gpu/tree/main/packages/vitest-environment-webgl-node) for headless WebGL.
158
+ - [`vitest-screenshot`](https://github.com/bhouston/vitest-gpu/tree/main/packages/vitest-screenshot) for `toMatchScreenshot()`.
159
+ - [`demo/`](https://github.com/bhouston/vitest-gpu/tree/main/demo/test/webgpu) for device, upload, readback, triangle, three.js, Babylon Lite and vgpu tests,
160
+ and the [repository README](https://github.com/bhouston/vitest-gpu#readme).
161
+
109
162
  ## Author
110
163
 
111
164
  Created by [Ben Houston](https://ben3d.ca) and sponsored by [Land of Assets](https://landofassets.com).
@@ -121,3 +174,9 @@ MIT
121
174
  [tests-url]: https://github.com/bhouston/vitest-gpu/actions/workflows/ci.yml
122
175
  [discord-badge]: https://img.shields.io/badge/Discord-Join%20Chat-5865F2?logo=discord&logoColor=white
123
176
  [discord-url]: https://discord.gg/fwupDN493R
177
+ [coverage-badge]: https://codecov.io/gh/bhouston/vitest-gpu/branch/main/graph/badge.svg
178
+ [coverage-url]: https://codecov.io/gh/bhouston/vitest-gpu
179
+
180
+ ## Resource cleanup
181
+
182
+ Environment teardown destroys devices requested through its `navigator.gpu` and cancels pending callbacks scheduled through its animation-frame shim. Device requests that finish after teardown are destroyed and rejected. Devices created through another GPU instance remain the caller's responsibility. Keep disposing renderers, buffers, textures and devices during long-running files to release resources sooner.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,qBAAqB,EAAE,KAAK,SAAS,EAAE,MAAM,aAAa,CAAC;AAElG,4EAA4E;AAC5E,MAAM,MAAM,iBAAiB,GAAG;IAC9B,yFAAyF;IACzF,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;CACxB,CAAC;;;;kBAyCc,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,kBAAuB;QAAE,UAAU,CAAC,EAAE,iBAAiB,CAAA;KAAE;2BA8BvE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,qBAAqB,EAAE,KAAK,SAAS,EAAE,MAAM,aAAa,CAAC;AAElG,4EAA4E;AAC5E,MAAM,MAAM,iBAAiB,GAAG;IAC9B,yFAAyF;IACzF,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;CACxB,CAAC;;;;kBA2Dc,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,kBAAuB;QAAE,UAAU,CAAC,EAAE,iBAAiB,CAAA;KAAE;2BA4DvE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC"}
package/dist/index.js CHANGED
@@ -1,17 +1,38 @@
1
1
  import { create, globals } from 'webgpu';
2
2
  import { HeadlessCanvas } from './canvas.js';
3
3
  export { createCanvas, HeadlessCanvas, HeadlessCanvasContext } from './canvas.js';
4
- /** Just enough DOM for libraries that make their own canvas and drive a frame loop. Only installed when missing. */
5
- const requestAnimationFrame = (callback) => setTimeout(() => callback(performance.now()), 16).unref();
4
+ /** Each environment owns its frame callbacks; unref alone does not release them. */
6
5
  const domShims = () => {
6
+ const frames = new Set();
7
+ let disposed = false;
8
+ const requestAnimationFrame = (callback) => {
9
+ if (disposed)
10
+ return 0;
11
+ const frame = setTimeout(() => {
12
+ frames.delete(frame);
13
+ callback(performance.now());
14
+ }, 16).unref();
15
+ frames.add(frame);
16
+ return frame;
17
+ };
18
+ const cancelAnimationFrame = (frame) => {
19
+ frames.delete(frame);
20
+ clearTimeout(frame);
21
+ };
22
+ const dispose = () => {
23
+ disposed = true;
24
+ for (const frame of frames)
25
+ clearTimeout(frame);
26
+ frames.clear();
27
+ };
7
28
  const window = {
8
29
  devicePixelRatio: 1,
9
30
  requestAnimationFrame,
10
- cancelAnimationFrame: clearTimeout,
31
+ cancelAnimationFrame,
11
32
  addEventListener() { },
12
33
  removeEventListener() { },
13
34
  };
14
- return {
35
+ const shims = {
15
36
  HTMLCanvasElement: HeadlessCanvas,
16
37
  document: {
17
38
  createElement: (tag) => (tag === 'canvas' ? new HeadlessCanvas() : {}),
@@ -22,8 +43,9 @@ const domShims = () => {
22
43
  window,
23
44
  self: window,
24
45
  requestAnimationFrame,
25
- cancelAnimationFrame: clearTimeout,
46
+ cancelAnimationFrame,
26
47
  };
48
+ return { shims, dispose };
27
49
  };
28
50
  const restore = (properties) => {
29
51
  for (const { target, key, descriptor } of properties.toReversed()) {
@@ -37,7 +59,17 @@ export default {
37
59
  name: 'webgpu-node',
38
60
  viteEnvironment: 'ssr',
39
61
  setup(global, { webgpuNode = {} }) {
40
- const shims = { ...globals, ...domShims() };
62
+ const dom = domShims();
63
+ const shims = { ...globals, ...dom.shims };
64
+ const devices = new Set();
65
+ let disposed = false;
66
+ const dispose = () => {
67
+ disposed = true;
68
+ dom.dispose();
69
+ for (const device of devices)
70
+ device.destroy();
71
+ devices.clear();
72
+ };
41
73
  const modified = [];
42
74
  try {
43
75
  for (const key of Object.keys(shims)) {
@@ -56,20 +88,51 @@ export default {
56
88
  }
57
89
  const navigator = global.navigator;
58
90
  modified.push({ target: navigator, key: 'gpu', descriptor: Object.getOwnPropertyDescriptor(navigator, 'gpu') });
91
+ const gpu = create(webgpuNode.dawnOptions ?? []);
92
+ const requestAdapter = gpu.requestAdapter.bind(gpu);
93
+ gpu.requestAdapter = async (options) => {
94
+ if (disposed)
95
+ throw new Error('WebGPU environment has been torn down');
96
+ const adapter = await requestAdapter(options);
97
+ if (adapter) {
98
+ const requestDevice = adapter.requestDevice.bind(adapter);
99
+ adapter.requestDevice = async (descriptor) => {
100
+ if (disposed)
101
+ throw new Error('WebGPU environment has been torn down');
102
+ const device = await requestDevice(descriptor);
103
+ if (disposed) {
104
+ device.destroy();
105
+ throw new Error('WebGPU environment has been torn down');
106
+ }
107
+ devices.add(device);
108
+ return device;
109
+ };
110
+ }
111
+ return adapter;
112
+ };
59
113
  Object.defineProperty(navigator, 'gpu', {
60
- value: create(webgpuNode.dawnOptions ?? []),
114
+ value: gpu,
61
115
  writable: false,
62
116
  enumerable: false,
63
117
  configurable: true,
64
118
  });
65
119
  }
66
120
  catch (error) {
121
+ dispose();
67
122
  restore(modified);
68
123
  throw error;
69
124
  }
70
125
  return {
71
126
  teardown(_global) {
72
- restore(modified);
127
+ if (disposed)
128
+ return;
129
+ try {
130
+ dispose();
131
+ }
132
+ finally {
133
+ restore(modified);
134
+ modified.length = 0;
135
+ }
73
136
  },
74
137
  };
75
138
  },
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,14 +1,22 @@
1
1
  {
2
2
  "name": "vitest-environment-webgpu-node",
3
- "version": "2.0.1",
4
- "description": "Vitest environment that provides real headless WebGPU (Dawn via the webgpu package) in Node",
3
+ "version": "3.0.1",
4
+ "description": "Vitest environment for real headless WebGPU tests in Node (Dawn via the webgpu package): no browser, Playwright or mocks",
5
5
  "keywords": [
6
+ "ci",
7
+ "compute-shader",
6
8
  "dawn",
9
+ "gpu",
10
+ "gpu-testing",
7
11
  "headless",
12
+ "node",
8
13
  "testing",
14
+ "three.js",
9
15
  "vitest",
10
16
  "vitest-environment",
11
- "webgpu"
17
+ "webgpu",
18
+ "webgpu-testing",
19
+ "wgsl"
12
20
  ],
13
21
  "homepage": "https://github.com/bhouston/vitest-gpu#readme",
14
22
  "bugs": {
@@ -27,8 +35,7 @@
27
35
  "dist/**/*.d.ts",
28
36
  "dist/**/*.d.ts.map",
29
37
  "src/**/*.ts",
30
- "!src/**/*.test.ts",
31
- "CHANGELOG.md"
38
+ "!src/**/*.test.ts"
32
39
  ],
33
40
  "type": "module",
34
41
  "main": "./dist/index.js",
@@ -42,16 +49,12 @@
42
49
  "publishConfig": {
43
50
  "access": "public"
44
51
  },
45
- "scripts": {
46
- "build": "tsc",
47
- "tsc": "tsc"
48
- },
49
52
  "dependencies": {
50
53
  "webgpu": "^0.6.1"
51
54
  },
52
55
  "peerDependencies": {
53
56
  "typescript": ">=6",
54
- "vitest": ">=3 <5"
57
+ "vitest": ">=4 <6"
55
58
  },
56
59
  "peerDependenciesMeta": {
57
60
  "typescript": {
@@ -59,6 +62,10 @@
59
62
  }
60
63
  },
61
64
  "engines": {
62
- "node": ">=20.0.0"
65
+ "node": ">=22"
66
+ },
67
+ "scripts": {
68
+ "build": "tsc",
69
+ "tsc": "tsc"
63
70
  }
64
- }
71
+ }
package/src/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { Environment } from 'vitest/environments';
1
+ import type { Environment } from 'vitest/runtime';
2
2
  import { create, globals } from 'webgpu';
3
3
  import { HeadlessCanvas } from './canvas.js';
4
4
 
@@ -10,19 +10,36 @@ export type WebgpuNodeOptions = {
10
10
  dawnOptions?: string[];
11
11
  };
12
12
 
13
- /** Just enough DOM for libraries that make their own canvas and drive a frame loop. Only installed when missing. */
14
- const requestAnimationFrame = (callback: (time: number) => void): NodeJS.Timeout =>
15
- setTimeout(() => callback(performance.now()), 16).unref();
16
-
17
- const domShims = (): Record<string, unknown> => {
13
+ /** Each environment owns its frame callbacks; unref alone does not release them. */
14
+ const domShims = () => {
15
+ const frames = new Set<NodeJS.Timeout>();
16
+ let disposed = false;
17
+ const requestAnimationFrame = (callback: (time: number) => void): NodeJS.Timeout | number => {
18
+ if (disposed) return 0;
19
+ const frame = setTimeout(() => {
20
+ frames.delete(frame);
21
+ callback(performance.now());
22
+ }, 16).unref();
23
+ frames.add(frame);
24
+ return frame;
25
+ };
26
+ const cancelAnimationFrame = (frame: NodeJS.Timeout) => {
27
+ frames.delete(frame);
28
+ clearTimeout(frame);
29
+ };
30
+ const dispose = () => {
31
+ disposed = true;
32
+ for (const frame of frames) clearTimeout(frame);
33
+ frames.clear();
34
+ };
18
35
  const window = {
19
36
  devicePixelRatio: 1,
20
37
  requestAnimationFrame,
21
- cancelAnimationFrame: clearTimeout,
38
+ cancelAnimationFrame,
22
39
  addEventListener() {},
23
40
  removeEventListener() {},
24
41
  };
25
- return {
42
+ const shims = {
26
43
  HTMLCanvasElement: HeadlessCanvas,
27
44
  document: {
28
45
  createElement: (tag: string) => (tag === 'canvas' ? new HeadlessCanvas() : {}),
@@ -33,8 +50,9 @@ const domShims = (): Record<string, unknown> => {
33
50
  window,
34
51
  self: window,
35
52
  requestAnimationFrame,
36
- cancelAnimationFrame: clearTimeout,
53
+ cancelAnimationFrame,
37
54
  };
55
+ return { shims, dispose };
38
56
  };
39
57
 
40
58
  type SavedProperty = { target: object; key: PropertyKey; descriptor?: PropertyDescriptor };
@@ -50,7 +68,16 @@ export default {
50
68
  name: 'webgpu-node',
51
69
  viteEnvironment: 'ssr',
52
70
  setup(global: Record<string, unknown>, { webgpuNode = {} }: { webgpuNode?: WebgpuNodeOptions }) {
53
- const shims = { ...globals, ...domShims() } as Record<string, unknown>;
71
+ const dom = domShims();
72
+ const shims = { ...globals, ...dom.shims } as Record<string, unknown>;
73
+ const devices = new Set<GPUDevice>();
74
+ let disposed = false;
75
+ const dispose = () => {
76
+ disposed = true;
77
+ dom.dispose();
78
+ for (const device of devices) device.destroy();
79
+ devices.clear();
80
+ };
54
81
  const modified: SavedProperty[] = [];
55
82
  try {
56
83
  for (const key of Object.keys(shims)) {
@@ -68,19 +95,46 @@ export default {
68
95
  }
69
96
  const navigator = global.navigator as object;
70
97
  modified.push({ target: navigator, key: 'gpu', descriptor: Object.getOwnPropertyDescriptor(navigator, 'gpu') });
98
+ const gpu = create(webgpuNode.dawnOptions ?? []);
99
+ const requestAdapter = gpu.requestAdapter.bind(gpu);
100
+ gpu.requestAdapter = async (options) => {
101
+ if (disposed) throw new Error('WebGPU environment has been torn down');
102
+ const adapter = await requestAdapter(options);
103
+ if (adapter) {
104
+ const requestDevice = adapter.requestDevice.bind(adapter);
105
+ adapter.requestDevice = async (descriptor) => {
106
+ if (disposed) throw new Error('WebGPU environment has been torn down');
107
+ const device = await requestDevice(descriptor);
108
+ if (disposed) {
109
+ device.destroy();
110
+ throw new Error('WebGPU environment has been torn down');
111
+ }
112
+ devices.add(device);
113
+ return device;
114
+ };
115
+ }
116
+ return adapter;
117
+ };
71
118
  Object.defineProperty(navigator, 'gpu', {
72
- value: create(webgpuNode.dawnOptions ?? []),
119
+ value: gpu,
73
120
  writable: false,
74
121
  enumerable: false,
75
122
  configurable: true,
76
123
  });
77
124
  } catch (error) {
125
+ dispose();
78
126
  restore(modified);
79
127
  throw error;
80
128
  }
81
129
  return {
82
130
  teardown(_global?: Record<string, unknown>) {
83
- restore(modified);
131
+ if (disposed) return;
132
+ try {
133
+ dispose();
134
+ } finally {
135
+ restore(modified);
136
+ modified.length = 0;
137
+ }
84
138
  },
85
139
  };
86
140
  },
package/CHANGELOG.md DELETED
@@ -1,5 +0,0 @@
1
- ## [vitest-environment-webgpu-node-v2.0.1](https://github.com/bhouston/vitest-gpu/compare/vitest-environment-webgpu-node-v2.0.0...vitest-environment-webgpu-node-v2.0.1) (2026-09-25)
2
-
3
- ### Bug Fixes
4
-
5
- * drop deprecated transformMode from GPU environments ([28f736a](https://github.com/bhouston/vitest-gpu/commit/28f736ad629a2a3ca023839cadce460d0d5c6937)), closes [#54](https://github.com/bhouston/vitest-gpu/issues/54)