vitest-environment-webgpu-node 3.0.0 → 3.0.2

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
@@ -1,30 +1,83 @@
1
+ <p align="center">
2
+ <a href="https://npmjs.com/package/vitest-environment-webgpu-node"><img src="https://img.shields.io/npm/v/vitest-environment-webgpu-node.svg" alt="npm package"></a>
3
+ <a href="https://www.npmtrends.com/vitest-environment-webgpu-node"><img src="https://img.shields.io/npm/dw/vitest-environment-webgpu-node.svg" alt="npm downloads"></a>
4
+ <a href="https://codecov.io/gh/bhouston/vitest-gpu"><img src="https://codecov.io/gh/bhouston/vitest-gpu/branch/main/graph/badge.svg" alt="coverage"></a>
5
+ <a href="https://discord.gg/fwupDN493R"><img src="https://img.shields.io/badge/chat-discord-blue?style=flat&logo=discord" alt="discord chat"></a>
6
+ <a href="https://vitest-gpu.ben3d.ca/"><img src="https://img.shields.io/badge/docs-read-blue?style=flat" alt="documentation"></a>
7
+ </p>
8
+ <br/>
9
+
1
10
  # vitest-environment-webgpu-node
2
11
 
3
- [![npm version][npm-badge]][npm-url]
4
- [![npm downloads][downloads-badge]][npm-url]
5
- [![Tests][tests-badge]][tests-url]
6
- [![Discord][discord-badge]][discord-url]
12
+ > Real WebGPU tests in Vitest, with no browser.
13
+
14
+ - 🖥️ Real WebGPU via Dawn
15
+ - 🚫 No Browser or Mocks
16
+ - 🧩 Headless Canvas Included
17
+ - 🔧 Dawn Backend Options
18
+ - ⚡ Up to 2.4x Faster Than Browsers
19
+
20
+ Headless GPU testing for CI: `navigator.gpu` is backed by Google's Dawn through the
21
+ [`webgpu`](https://github.com/dawn-gpu/node-webgpu) npm package. Native GPU testing is up to 2.4x faster than
22
+ running the same tests in a browser, see the [blog post](https://ben3d.ca/blog/native-gpu-testing-for-vitest-and-jest).
23
+
24
+ Part of [vitest-gpu](https://github.com/bhouston/vitest-gpu), which also provides WebGL and image snapshot packages.
25
+
26
+ [Read the Docs to Learn More](https://vitest-gpu.ben3d.ca/docs/webgpu/) about the WebGPU environment: Dawn options, TypeScript types, the headless canvas and CI setup.
27
+
28
+ ```sh
29
+ pnpm add -D vitest vitest-environment-webgpu-node
30
+ ```
31
+
32
+ ```ts
33
+ // vitest.config.ts
34
+ import { defineConfig } from 'vitest/config';
35
+
36
+ export default defineConfig({ test: { environment: 'webgpu-node' } });
37
+ ```
38
+
39
+ ```ts
40
+ // gpu.test.ts
41
+ import { expect, it } from 'vitest';
7
42
 
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.
43
+ it('has a real GPU device', async () => {
44
+ const adapter = await navigator.gpu.requestAdapter();
45
+ const device = await adapter!.requestDevice();
46
+ const buffer = device.createBuffer({ size: 16, usage: GPUBufferUsage.COPY_DST });
47
+ expect(buffer.size).toBe(16);
48
+ });
49
+ ```
13
50
 
14
51
  The environment puts `navigator.gpu` and every `GPU*` class and constant (`GPUBufferUsage`,
15
52
  `GPUShaderStage`, ...) on the global object before each test file and removes them afterwards.
16
53
 
17
54
  Using Jest instead? See the equivalent
18
55
  [`jest-environment-webgpu-node`](https://github.com/bhouston/jest-gpu/tree/main/packages/jest-environment-webgpu-node)
19
- in [jest-gpu](https://github.com/bhouston/jest-gpu).
56
+ in [jest-gpu](https://github.com/bhouston/jest-gpu), documented at [jest-gpu.ben3d.ca](https://jest-gpu.ben3d.ca/).
20
57
 
21
- ## Install
58
+ ## Compared to alternatives
22
59
 
23
- ```sh
24
- pnpm add -D vitest vitest-environment-webgpu-node
25
- ```
60
+ WebGPU has no mock equivalent of a canvas stub that runs shaders, so the realistic options are a browser or
61
+ Dawn in Node.
62
+
63
+ | Approach | Real rendering | Notes |
64
+ | --------------------------------- | -------------- | ------------------------------------------------------------------------------------------------------------- |
65
+ | Hand-written `navigator.gpu` mock | No | You can assert calls, not shader output or readbacks. |
66
+ | 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. |
67
+ | vitest-environment-webgpu-node | Yes | Dawn in the Node process (Metal, Vulkan, D3D12 and others via `dawnOptions`). Not a browser. |
68
+
69
+ ## Supported versions and platforms
70
+
71
+ - Vitest 4 and 5 (CI tests both). Vitest 3 is not supported.
72
+ - Node 22 or later. CI runs Node 22 and 26 on `macos-latest`, `ubuntu-latest` and `windows-latest`.
73
+ - Type checking needs TypeScript 6 or later (see [TypeScript types](#typescript-types)).
74
+ - Linux without a GPU needs Mesa's software Vulkan driver, see [Linux and CI](#linux-and-ci).
75
+
76
+ ## Useful for
26
77
 
27
- Requires Vitest 4 or 5 (Vitest 3 is not supported). Type checking needs TypeScript 6 or later (see [TypeScript types](#typescript-types)).
78
+ three.js `WebGPURenderer`, Babylon Lite, Vercel's vgpu and raw WebGPU compute or render code. The demos exercise
79
+ those three; other WebGPU libraries such as TypeGPU or luma.gl use the same API but are not covered by this
80
+ repo's tests. These are compatible use cases, not endorsements.
28
81
 
29
82
  ## Usage
30
83
 
@@ -77,7 +130,7 @@ sudo apt-get update && sudo apt-get install -y libegl1 libgles2 libgl1-mesa-dri
77
130
  export LIBGL_ALWAYS_SOFTWARE=1
78
131
  ```
79
132
 
80
- This repo's own [CI workflow](../../.github/workflows/ci.yml) does exactly this, so WebGPU tests run
133
+ 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
134
  for real (not mocked) on `ubuntu-latest` on every PR. macOS runners use Dawn's Metal backend and need no
82
135
  extra setup.
83
136
 
@@ -100,24 +153,36 @@ await expect(canvas).toMatchScreenshot('scene.png');
100
153
  ```
101
154
 
102
155
  `readPixels()` returns `{ width, height, data }` RGBA8 pixels, ready for
103
- [`vitest-screenshot`](../vitest-screenshot). Readback supports `rgba8unorm`, `rgba8unorm-srgb`,
156
+ [`vitest-screenshot`](https://github.com/bhouston/vitest-gpu/tree/main/packages/vitest-screenshot). Readback supports `rgba8unorm`, `rgba8unorm-srgb`,
104
157
  `bgra8unorm`, and `bgra8unorm-srgb`; other canvas formats remain valid for rendering but
105
158
  `readPixels()` rejects them with an unsupported-format error. `asElement()` returns the same object and
106
159
  defaults to `HTMLCanvasElement` when DOM types are available. You can also request a library-specific type
107
160
  explicitly with `canvas.asElement<HTMLCanvasElement>()`.
108
161
 
109
- ## Author
162
+ ## Image snapshots
163
+
164
+ `await expect(canvas).toMatchScreenshot('scene.png')` comes from [`vitest-screenshot`](https://github.com/bhouston/vitest-gpu/tree/main/packages/vitest-screenshot).
165
+ Call `extendMatchers()` from it once, then pass the canvas as in the example above.
166
+
167
+ ## See also
168
+
169
+ - [`vitest-environment-webgl-node`](https://github.com/bhouston/vitest-gpu/tree/main/packages/vitest-environment-webgl-node) for headless WebGL.
170
+ - [`vitest-screenshot`](https://github.com/bhouston/vitest-gpu/tree/main/packages/vitest-screenshot) for `toMatchScreenshot()`.
171
+ - [`demo/`](https://github.com/bhouston/vitest-gpu/tree/main/demo/test/webgpu) for device, upload, readback, triangle, three.js, Babylon Lite and vgpu tests,
172
+ and the [repository README](https://github.com/bhouston/vitest-gpu#readme).
173
+
174
+ ## Resource cleanup
110
175
 
111
- Created by [Ben Houston](https://ben3d.ca) and sponsored by [Land of Assets](https://landofassets.com).
176
+ 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.
177
+
178
+ ## Contribution
179
+
180
+ See [Contributing Guide](https://github.com/bhouston/vitest-gpu/blob/main/CONTRIBUTING.md).
112
181
 
113
182
  ## License
114
183
 
115
184
  MIT
116
185
 
117
- [npm-badge]: https://img.shields.io/npm/v/vitest-environment-webgpu-node.svg
118
- [npm-url]: https://www.npmjs.com/package/vitest-environment-webgpu-node
119
- [downloads-badge]: https://img.shields.io/npm/dm/vitest-environment-webgpu-node.svg
120
- [tests-badge]: https://github.com/bhouston/vitest-gpu/actions/workflows/ci.yml/badge.svg
121
- [tests-url]: https://github.com/bhouston/vitest-gpu/actions/workflows/ci.yml
122
- [discord-badge]: https://img.shields.io/badge/Discord-Join%20Chat-5865F2?logo=discord&logoColor=white
123
- [discord-url]: https://discord.gg/fwupDN493R
186
+ ## Author
187
+
188
+ [Ben Houston](https://ben3d.ca), Sponsored by [Land of Assets](https://landofassets.com).
package/dist/canvas.d.ts CHANGED
@@ -1,7 +1,10 @@
1
1
  /** RGBA8 pixels, top row first. Feed it to `vitest-screenshot`'s `toMatchScreenshot`. */
2
2
  export type RgbaImage = {
3
+ /** Width in pixels. */
3
4
  width: number;
5
+ /** Height in pixels. */
4
6
  height: number;
7
+ /** RGBA8 bytes, four per pixel, row by row from the top. */
5
8
  data: Uint8Array;
6
9
  };
7
10
  type DefaultCanvasElement = typeof globalThis extends {
@@ -12,11 +15,30 @@ type DefaultCanvasElement = typeof globalThis extends {
12
15
  /** Minimal `GPUCanvasContext`: `getCurrentTexture()` is a texture the size of the canvas, kept until reconfigured or resized. */
13
16
  export declare class HeadlessCanvasContext {
14
17
  #private;
18
+ /** The canvas this context belongs to. */
15
19
  readonly canvas: HeadlessCanvas;
16
- constructor(canvas: HeadlessCanvas);
20
+ /**
21
+ * Creates the context for a canvas. Canvases create their own; you rarely call this.
22
+ *
23
+ * @param canvas - The canvas whose size this context's texture follows.
24
+ */
25
+ constructor(
26
+ /** The canvas this context belongs to. */
27
+ canvas: HeadlessCanvas);
28
+ /**
29
+ * Stores the configuration and discards any texture created under the previous one, as `GPUCanvasContext.configure()` does.
30
+ *
31
+ * @param config - Device, format and usage for the canvas texture.
32
+ */
17
33
  configure(config: GPUCanvasConfiguration): void;
34
+ /** Clears the configuration and discards the current texture. */
18
35
  unconfigure(): void;
36
+ /** The configuration passed to `configure()`, or `null` before it is called or after `unconfigure()`. */
19
37
  getConfiguration(): GPUCanvasConfiguration | null;
38
+ /**
39
+ * The texture the canvas renders into. It is created on first use at the canvas size and reused until the context
40
+ * is reconfigured or the canvas is resized. Throws if `configure()` has not been called.
41
+ */
20
42
  getCurrentTexture(): GPUTexture;
21
43
  /** Copies the current texture to the CPU. BGRA formats are swizzled to RGBA. */
22
44
  readPixels(): Promise<RgbaImage>;
@@ -24,31 +46,92 @@ export declare class HeadlessCanvasContext {
24
46
  /** Enough of `HTMLCanvasElement` for three.js `WebGPURenderer`, Babylon Lite and friends. */
25
47
  export declare class HeadlessCanvas {
26
48
  #private;
49
+ /** Canvas width in pixels. */
27
50
  width: number;
51
+ /** Canvas height in pixels. */
28
52
  height: number;
53
+ /** A plain object libraries can write CSS properties to; it has no effect on rendering. */
29
54
  readonly style: Record<string, string>;
30
- constructor(width?: number, height?: number);
55
+ /**
56
+ * Creates a canvas of the given size. {@link createCanvas} is the usual entry point.
57
+ *
58
+ * @param width - Canvas width in pixels.
59
+ * @param height - Canvas height in pixels.
60
+ */
61
+ constructor(
62
+ /** Canvas width in pixels. */
63
+ width?: number,
64
+ /** Canvas height in pixels. */
65
+ height?: number);
66
+ /** Same as `width`; there is no layout in Node. */
31
67
  get clientWidth(): number;
68
+ /** Same as `height`; there is no layout in Node. */
32
69
  get clientHeight(): number;
70
+ /**
71
+ * Returns the canvas's `GPUCanvasContext` for `'webgpu'`.
72
+ *
73
+ * @param id - The context type. Only `'webgpu'` is supported.
74
+ * @returns The context for `'webgpu'`, or `null` for any other type.
75
+ */
33
76
  getContext(id: 'webgpu'): HeadlessCanvasContext;
77
+ /**
78
+ * Returns the canvas's `GPUCanvasContext` for `'webgpu'`.
79
+ *
80
+ * @param id - The context type. Only `'webgpu'` is supported.
81
+ * @returns The context for `'webgpu'`, or `null` for any other type.
82
+ */
34
83
  getContext(id: string): HeadlessCanvasContext | null;
84
+ /** Reads the current texture back as RGBA8 pixels; see {@link HeadlessCanvasContext.readPixels}. */
35
85
  readPixels(): Promise<RgbaImage>;
36
- /** This same object, cast to the element type a rendering library expects. */
86
+ /**
87
+ * This same object, cast to the element type a rendering library expects.
88
+ *
89
+ * @typeParam TElement - The element type to cast to. Defaults to `HTMLCanvasElement` when DOM types are available.
90
+ */
37
91
  asElement<TElement = DefaultCanvasElement>(): TElement;
92
+ /** A rectangle at the origin with the canvas's width and height. */
38
93
  getBoundingClientRect(): {
94
+ /** Always 0. */
39
95
  x: number;
96
+ /** Always 0. */
40
97
  y: number;
98
+ /** The canvas width. */
41
99
  width: number;
100
+ /** The canvas height. */
42
101
  height: number;
102
+ /** Always 0. */
43
103
  top: number;
104
+ /** Always 0. */
44
105
  left: number;
45
106
  };
107
+ /**
108
+ * Stores an attribute on the canvas.
109
+ *
110
+ * @param name - Attribute name.
111
+ * @param value - Attribute value.
112
+ */
46
113
  setAttribute(name: string, value: string): void;
114
+ /**
115
+ * Reads an attribute stored with `setAttribute()`.
116
+ *
117
+ * @param name - Attribute name.
118
+ * @returns The stored value, or `null` if none was set.
119
+ */
47
120
  getAttribute(name: string): string | null;
121
+ /** Does nothing; present so libraries that register listeners keep working. */
48
122
  addEventListener(): void;
123
+ /** Does nothing; present so libraries that remove listeners keep working. */
49
124
  removeEventListener(): void;
125
+ /** Does nothing and returns `true`. */
50
126
  dispatchEvent(): boolean;
51
127
  }
128
+ /**
129
+ * Creates a headless canvas for WebGPU tests.
130
+ *
131
+ * @param width - Canvas width in pixels.
132
+ * @param height - Canvas height in pixels.
133
+ * @returns A {@link HeadlessCanvas}; pass `canvas.asElement()` to libraries that expect an `HTMLCanvasElement`.
134
+ */
52
135
  export declare const createCanvas: (width: number, height: number) => HeadlessCanvas;
53
136
  export {};
54
137
  //# sourceMappingURL=canvas.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"canvas.d.ts","sourceRoot":"","sources":["../src/canvas.ts"],"names":[],"mappings":"AAAA,yFAAyF;AACzF,MAAM,MAAM,SAAS,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,UAAU,CAAA;CAAE,CAAC;AAE5E,KAAK,oBAAoB,GAAG,OAAO,UAAU,SAAS;IACpD,iBAAiB,EAAE;QAAE,SAAS,EAAE,MAAM,QAAQ,CAAA;KAAE,CAAC;CAClD,GACG,QAAQ,GACR,cAAc,CAAC;AAanB,iIAAiI;AACjI,qBAAa,qBAAqB;;IAIpB,QAAQ,CAAC,MAAM,EAAE,cAAc;IAA3C,YAAqB,MAAM,EAAE,cAAc,EAAI;IAE/C,SAAS,CAAC,MAAM,EAAE,sBAAsB,GAAG,IAAI,CAG9C;IAED,WAAW,IAAI,IAAI,CAGlB;IAED,gBAAgB,IAAI,sBAAsB,GAAG,IAAI,CAEhD;IAED,iBAAiB,IAAI,UAAU,CAY9B;IAED,gFAAgF;IAC1E,UAAU,IAAI,OAAO,CAAC,SAAS,CAAC,CAmCrC;CAMF;AAED,6FAA6F;AAC7F,qBAAa,cAAc;;IAMhB,KAAK;IACL,MAAM;IALf,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAM;IAG5C,YACS,KAAK,SAAM,EACX,MAAM,SAAM,EACjB;IAEJ,IAAI,WAAW,IAAI,MAAM,CAExB;IAED,IAAI,YAAY,IAAI,MAAM,CAEzB;IAED,UAAU,CAAC,EAAE,EAAE,QAAQ,GAAG,qBAAqB,CAAC;IAChD,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,qBAAqB,GAAG,IAAI,CAAC;IAKrD,UAAU,IAAI,OAAO,CAAC,SAAS,CAAC,CAE/B;IAED,8EAA8E;IAC9E,SAAS,CAAC,QAAQ,GAAG,oBAAoB,KAAK,QAAQ,CAErD;IAED,qBAAqB,IAAI;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAE1G;IAED,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAE9C;IAED,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAExC;IAED,gBAAgB,IAAI,IAAI,CAAG;IAC3B,mBAAmB,IAAI,IAAI,CAAG;IAC9B,aAAa,IAAI,OAAO,CAEvB;CACF;AAED,eAAO,MAAM,YAAY,UAAW,MAAM,UAAU,MAAM,KAAG,cAAmD,CAAC"}
1
+ {"version":3,"file":"canvas.d.ts","sources":["../src/canvas.ts"],"names":[],"mappings":"AAAA,yFAAyF;AACzF,MAAM,MAAM,SAAS,GAAG;IACtB,uBAAuB;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,wBAAwB;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,4DAA4D;IAC5D,IAAI,EAAE,UAAU,CAAC;CAClB,CAAC;AAEF,KAAK,oBAAoB,GAAG,OAAO,UAAU,SAAS;IACpD,iBAAiB,EAAE;QAAE,SAAS,EAAE,MAAM,QAAQ,CAAA;KAAE,CAAC;CAClD,GACG,QAAQ,GACR,cAAc,CAAC;AAanB,iIAAiI;AACjI,qBAAa,qBAAqB;;IAU9B,0CAA0C;IAC1C,QAAQ,CAAC,MAAM,EAAE,cAAc;IAPjC;;;;OAIG;IACH;IACE,0CAA0C;IACjC,MAAM,EAAE,cAAc,EAC7B;IAEJ;;;;OAIG;IACH,SAAS,CAAC,MAAM,EAAE,sBAAsB,GAAG,IAAI,CAG9C;IAED,iEAAiE;IACjE,WAAW,IAAI,IAAI,CAGlB;IAED,yGAAyG;IACzG,gBAAgB,IAAI,sBAAsB,GAAG,IAAI,CAEhD;IAED;;;OAGG;IACH,iBAAiB,IAAI,UAAU,CAY9B;IAED,gFAAgF;IAC1E,UAAU,IAAI,OAAO,CAAC,SAAS,CAAC,CAmCrC;CAMF;AAED,6FAA6F;AAC7F,qBAAa,cAAc;;IAavB,8BAA8B;IACvB,KAAK;IACZ,+BAA+B;IACxB,MAAM;IAdf,2FAA2F;IAC3F,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAM;IAG5C;;;;;OAKG;IACH;IACE,8BAA8B;IACvB,KAAK,SAAM;IAClB,+BAA+B;IACxB,MAAM,SAAM,EACjB;IAEJ,mDAAmD;IACnD,IAAI,WAAW,IAAI,MAAM,CAExB;IAED,oDAAoD;IACpD,IAAI,YAAY,IAAI,MAAM,CAEzB;IAED;;;;;OAKG;IACH,UAAU,CAAC,EAAE,EAAE,QAAQ,GAAG,qBAAqB,CAAC;IAChD;;;;;OAKG;IACH,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,qBAAqB,GAAG,IAAI,CAAC;IAKrD,oGAAoG;IACpG,UAAU,IAAI,OAAO,CAAC,SAAS,CAAC,CAE/B;IAED;;;;OAIG;IACH,SAAS,CAAC,QAAQ,GAAG,oBAAoB,KAAK,QAAQ,CAErD;IAED,oEAAoE;IACpE,qBAAqB,IAAI;QACvB,gBAAgB;QAChB,CAAC,EAAE,MAAM,CAAC;QACV,gBAAgB;QAChB,CAAC,EAAE,MAAM,CAAC;QACV,wBAAwB;QACxB,KAAK,EAAE,MAAM,CAAC;QACd,yBAAyB;QACzB,MAAM,EAAE,MAAM,CAAC;QACf,gBAAgB;QAChB,GAAG,EAAE,MAAM,CAAC;QACZ,gBAAgB;QAChB,IAAI,EAAE,MAAM,CAAC;KACd,CAEA;IAED;;;;;OAKG;IACH,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAE9C;IAED;;;;;OAKG;IACH,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAExC;IAED,+EAA+E;IAC/E,gBAAgB,IAAI,IAAI,CAAG;IAC3B,6EAA6E;IAC7E,mBAAmB,IAAI,IAAI,CAAG;IAC9B,uCAAuC;IACvC,aAAa,IAAI,OAAO,CAEvB;CACF;AAED;;;;;;GAMG;AACH,eAAO,MAAM,YAAY,UAAW,MAAM,UAAU,MAAM,KAAG,cAAmD,CAAC"}
package/dist/canvas.js CHANGED
@@ -13,20 +13,38 @@ export class HeadlessCanvasContext {
13
13
  canvas;
14
14
  #config;
15
15
  #texture;
16
- constructor(canvas) {
16
+ /**
17
+ * Creates the context for a canvas. Canvases create their own; you rarely call this.
18
+ *
19
+ * @param canvas - The canvas whose size this context's texture follows.
20
+ */
21
+ constructor(
22
+ /** The canvas this context belongs to. */
23
+ canvas) {
17
24
  this.canvas = canvas;
18
25
  }
26
+ /**
27
+ * Stores the configuration and discards any texture created under the previous one, as `GPUCanvasContext.configure()` does.
28
+ *
29
+ * @param config - Device, format and usage for the canvas texture.
30
+ */
19
31
  configure(config) {
20
32
  this.#config = config;
21
33
  this.#drop();
22
34
  }
35
+ /** Clears the configuration and discards the current texture. */
23
36
  unconfigure() {
24
37
  this.#config = undefined;
25
38
  this.#drop();
26
39
  }
40
+ /** The configuration passed to `configure()`, or `null` before it is called or after `unconfigure()`. */
27
41
  getConfiguration() {
28
42
  return this.#config ?? null;
29
43
  }
44
+ /**
45
+ * The texture the canvas renders into. It is created on first use at the canvas size and reused until the context
46
+ * is reconfigured or the canvas is resized. Throws if `configure()` has not been called.
47
+ */
30
48
  getCurrentTexture() {
31
49
  const config = this.#config;
32
50
  if (!config)
@@ -92,42 +110,83 @@ export class HeadlessCanvas {
92
110
  width;
93
111
  height;
94
112
  #context = new HeadlessCanvasContext(this);
113
+ /** A plain object libraries can write CSS properties to; it has no effect on rendering. */
95
114
  style = {};
96
115
  #attributes = new Map();
97
- constructor(width = 300, height = 150) {
116
+ /**
117
+ * Creates a canvas of the given size. {@link createCanvas} is the usual entry point.
118
+ *
119
+ * @param width - Canvas width in pixels.
120
+ * @param height - Canvas height in pixels.
121
+ */
122
+ constructor(
123
+ /** Canvas width in pixels. */
124
+ width = 300,
125
+ /** Canvas height in pixels. */
126
+ height = 150) {
98
127
  this.width = width;
99
128
  this.height = height;
100
129
  }
130
+ /** Same as `width`; there is no layout in Node. */
101
131
  get clientWidth() {
102
132
  return this.width;
103
133
  }
134
+ /** Same as `height`; there is no layout in Node. */
104
135
  get clientHeight() {
105
136
  return this.height;
106
137
  }
107
138
  getContext(id) {
108
139
  return id === 'webgpu' ? this.#context : null;
109
140
  }
141
+ /** Reads the current texture back as RGBA8 pixels; see {@link HeadlessCanvasContext.readPixels}. */
110
142
  readPixels() {
111
143
  return this.#context.readPixels();
112
144
  }
113
- /** This same object, cast to the element type a rendering library expects. */
145
+ /**
146
+ * This same object, cast to the element type a rendering library expects.
147
+ *
148
+ * @typeParam TElement - The element type to cast to. Defaults to `HTMLCanvasElement` when DOM types are available.
149
+ */
114
150
  asElement() {
115
151
  return this;
116
152
  }
153
+ /** A rectangle at the origin with the canvas's width and height. */
117
154
  getBoundingClientRect() {
118
155
  return { x: 0, y: 0, top: 0, left: 0, width: this.width, height: this.height };
119
156
  }
157
+ /**
158
+ * Stores an attribute on the canvas.
159
+ *
160
+ * @param name - Attribute name.
161
+ * @param value - Attribute value.
162
+ */
120
163
  setAttribute(name, value) {
121
164
  this.#attributes.set(name, value);
122
165
  }
166
+ /**
167
+ * Reads an attribute stored with `setAttribute()`.
168
+ *
169
+ * @param name - Attribute name.
170
+ * @returns The stored value, or `null` if none was set.
171
+ */
123
172
  getAttribute(name) {
124
173
  return this.#attributes.get(name) ?? null;
125
174
  }
175
+ /** Does nothing; present so libraries that register listeners keep working. */
126
176
  addEventListener() { }
177
+ /** Does nothing; present so libraries that remove listeners keep working. */
127
178
  removeEventListener() { }
179
+ /** Does nothing and returns `true`. */
128
180
  dispatchEvent() {
129
181
  return true;
130
182
  }
131
183
  }
184
+ /**
185
+ * Creates a headless canvas for WebGPU tests.
186
+ *
187
+ * @param width - Canvas width in pixels.
188
+ * @param height - Canvas height in pixels.
189
+ * @returns A {@link HeadlessCanvas}; pass `canvas.asElement()` to libraries that expect an `HTMLCanvasElement`.
190
+ */
132
191
  export const createCanvas = (width, height) => new HeadlessCanvas(width, height);
133
192
  //# sourceMappingURL=canvas.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"canvas.js","sourceRoot":"","sources":["../src/canvas.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"canvas.js","sources":["../src/canvas.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -4,13 +4,33 @@ export type WebgpuNodeOptions = {
4
4
  /** Dawn options, e.g. `['backend=vulkan', 'enable-dawn-features=allow_unsafe_apis']`. */
5
5
  dawnOptions?: string[];
6
6
  };
7
+ /**
8
+ * The Vitest environment registered as `webgpu-node`. Before each test file it puts `navigator.gpu`, the `GPU*`
9
+ * classes and constants, and minimal `window`, `document`, `HTMLCanvasElement` and `requestAnimationFrame` shims
10
+ * on the global object (only where missing), and it removes them afterwards.
11
+ */
7
12
  declare const _default: {
13
+ /** The name Vitest uses to select this environment: `test.environment: 'webgpu-node'`. */
8
14
  name: string;
15
+ /** Runs test files through Vite's `ssr` environment, as Node code. */
9
16
  viteEnvironment: "ssr";
17
+ /**
18
+ * Creates the Dawn-backed `navigator.gpu` and installs the DOM shims on the test file's global object.
19
+ *
20
+ * @param global - The global object of the test file's context.
21
+ * @param __namedParameters - The environment options from the Vitest config; `webgpuNode` holds {@link WebgpuNodeOptions}.
22
+ * @returns A `teardown` hook that destroys the devices this environment created and restores the globals.
23
+ */
10
24
  setup(global: Record<string, unknown>, { webgpuNode }: {
11
25
  webgpuNode?: WebgpuNodeOptions;
12
26
  }): {
13
- teardown(_global?: Record<string, unknown>): void;
27
+ /**
28
+ * Destroys the GPU devices requested through this environment, cancels pending animation-frame callbacks and
29
+ * removes the globals that `setup` added.
30
+ *
31
+ * @param _global - Unused; teardown restores the global object it captured in `setup`.
32
+ */
33
+ teardown: (_global?: Record<string, unknown>) => void;
14
34
  };
15
35
  };
16
36
  export default _default;
@@ -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","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;AAwDF;;;;GAIG;;IAED,0FAA0F;;IAE1F,sEAAsE;;IAEtE;;;;;;OAMG;kBACW,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,kBAAuB;QAAE,UAAU,CAAC,EAAE,iBAAiB,CAAA;KAAE;QA6D1F;;;;;WAKG;6BACkB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;;;AA/ElD,wBA0FwB"}
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()) {
@@ -33,11 +55,36 @@ const restore = (properties) => {
33
55
  Reflect.deleteProperty(target, key);
34
56
  }
35
57
  };
58
+ /**
59
+ * The Vitest environment registered as `webgpu-node`. Before each test file it puts `navigator.gpu`, the `GPU*`
60
+ * classes and constants, and minimal `window`, `document`, `HTMLCanvasElement` and `requestAnimationFrame` shims
61
+ * on the global object (only where missing), and it removes them afterwards.
62
+ */
36
63
  export default {
64
+ /** The name Vitest uses to select this environment: `test.environment: 'webgpu-node'`. */
37
65
  name: 'webgpu-node',
66
+ /** Runs test files through Vite's `ssr` environment, as Node code. */
38
67
  viteEnvironment: 'ssr',
68
+ /**
69
+ * Creates the Dawn-backed `navigator.gpu` and installs the DOM shims on the test file's global object.
70
+ *
71
+ * @param global - The global object of the test file's context.
72
+ * @param __namedParameters - The environment options from the Vitest config; `webgpuNode` holds {@link WebgpuNodeOptions}.
73
+ * @returns A `teardown` hook that destroys the devices this environment created and restores the globals.
74
+ */
39
75
  setup(global, { webgpuNode = {} }) {
40
- const shims = { ...globals, ...domShims() };
76
+ const dom = domShims();
77
+ // oxlint-disable-next-line typescript/no-misused-spread -- intentionally copies the own enumerable GPU* globals
78
+ const shims = { ...globals, ...dom.shims };
79
+ const devices = new Set();
80
+ let disposed = false;
81
+ const dispose = () => {
82
+ disposed = true;
83
+ dom.dispose();
84
+ for (const device of devices)
85
+ device.destroy();
86
+ devices.clear();
87
+ };
41
88
  const modified = [];
42
89
  try {
43
90
  for (const key of Object.keys(shims)) {
@@ -56,20 +103,57 @@ export default {
56
103
  }
57
104
  const navigator = global.navigator;
58
105
  modified.push({ target: navigator, key: 'gpu', descriptor: Object.getOwnPropertyDescriptor(navigator, 'gpu') });
106
+ const gpu = create(webgpuNode.dawnOptions ?? []);
107
+ const requestAdapter = gpu.requestAdapter.bind(gpu);
108
+ gpu.requestAdapter = async (options) => {
109
+ if (disposed)
110
+ throw new Error('WebGPU environment has been torn down');
111
+ const adapter = await requestAdapter(options);
112
+ if (adapter) {
113
+ const requestDevice = adapter.requestDevice.bind(adapter);
114
+ adapter.requestDevice = async (descriptor) => {
115
+ if (disposed)
116
+ throw new Error('WebGPU environment has been torn down');
117
+ const device = await requestDevice(descriptor);
118
+ if (disposed) {
119
+ device.destroy();
120
+ throw new Error('WebGPU environment has been torn down');
121
+ }
122
+ devices.add(device);
123
+ return device;
124
+ };
125
+ }
126
+ return adapter;
127
+ };
59
128
  Object.defineProperty(navigator, 'gpu', {
60
- value: create(webgpuNode.dawnOptions ?? []),
129
+ value: gpu,
61
130
  writable: false,
62
131
  enumerable: false,
63
132
  configurable: true,
64
133
  });
65
134
  }
66
135
  catch (error) {
136
+ dispose();
67
137
  restore(modified);
68
138
  throw error;
69
139
  }
70
140
  return {
71
- teardown(_global) {
72
- restore(modified);
141
+ /**
142
+ * Destroys the GPU devices requested through this environment, cancels pending animation-frame callbacks and
143
+ * removes the globals that `setup` added.
144
+ *
145
+ * @param _global - Unused; teardown restores the global object it captured in `setup`.
146
+ */
147
+ teardown: (_global) => {
148
+ if (disposed)
149
+ return;
150
+ try {
151
+ dispose();
152
+ }
153
+ finally {
154
+ restore(modified);
155
+ modified.length = 0;
156
+ }
73
157
  },
74
158
  };
75
159
  },
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","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": "3.0.0",
4
- "description": "Vitest environment that provides real headless WebGPU (Dawn via the webgpu package) in Node",
3
+ "version": "3.0.2",
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": {
@@ -42,7 +50,10 @@
42
50
  "access": "public"
43
51
  },
44
52
  "dependencies": {
45
- "webgpu": "^0.6.1"
53
+ "webgpu": "^0.6.2"
54
+ },
55
+ "devDependencies": {
56
+ "typescript": "7.1.0-beta"
46
57
  },
47
58
  "peerDependencies": {
48
59
  "typescript": ">=6",
@@ -57,7 +68,7 @@
57
68
  "node": ">=22"
58
69
  },
59
70
  "scripts": {
60
- "build": "tsc",
61
- "tsc": "tsc"
71
+ "build": "tsc -p tsconfig.build.json",
72
+ "tsc": "tsc --noEmit"
62
73
  }
63
74
  }
package/src/canvas.ts CHANGED
@@ -1,5 +1,12 @@
1
1
  /** RGBA8 pixels, top row first. Feed it to `vitest-screenshot`'s `toMatchScreenshot`. */
2
- export type RgbaImage = { width: number; height: number; data: Uint8Array };
2
+ export type RgbaImage = {
3
+ /** Width in pixels. */
4
+ width: number;
5
+ /** Height in pixels. */
6
+ height: number;
7
+ /** RGBA8 bytes, four per pixel, row by row from the top. */
8
+ data: Uint8Array;
9
+ };
3
10
 
4
11
  type DefaultCanvasElement = typeof globalThis extends {
5
12
  HTMLCanvasElement: { prototype: infer TElement };
@@ -23,22 +30,41 @@ export class HeadlessCanvasContext {
23
30
  #config: GPUCanvasConfiguration | undefined;
24
31
  #texture: GPUTexture | undefined;
25
32
 
26
- constructor(readonly canvas: HeadlessCanvas) {}
33
+ /**
34
+ * Creates the context for a canvas. Canvases create their own; you rarely call this.
35
+ *
36
+ * @param canvas - The canvas whose size this context's texture follows.
37
+ */
38
+ constructor(
39
+ /** The canvas this context belongs to. */
40
+ readonly canvas: HeadlessCanvas,
41
+ ) {}
27
42
 
43
+ /**
44
+ * Stores the configuration and discards any texture created under the previous one, as `GPUCanvasContext.configure()` does.
45
+ *
46
+ * @param config - Device, format and usage for the canvas texture.
47
+ */
28
48
  configure(config: GPUCanvasConfiguration): void {
29
49
  this.#config = config;
30
50
  this.#drop();
31
51
  }
32
52
 
53
+ /** Clears the configuration and discards the current texture. */
33
54
  unconfigure(): void {
34
55
  this.#config = undefined;
35
56
  this.#drop();
36
57
  }
37
58
 
59
+ /** The configuration passed to `configure()`, or `null` before it is called or after `unconfigure()`. */
38
60
  getConfiguration(): GPUCanvasConfiguration | null {
39
61
  return this.#config ?? null;
40
62
  }
41
63
 
64
+ /**
65
+ * The texture the canvas renders into. It is created on first use at the canvas size and reused until the context
66
+ * is reconfigured or the canvas is resized. Throws if `configure()` has not been called.
67
+ */
42
68
  getCurrentTexture(): GPUTexture {
43
69
  const config = this.#config;
44
70
  if (!config) throw new Error('getCurrentTexture() called before configure()');
@@ -100,54 +126,118 @@ export class HeadlessCanvasContext {
100
126
  /** Enough of `HTMLCanvasElement` for three.js `WebGPURenderer`, Babylon Lite and friends. */
101
127
  export class HeadlessCanvas {
102
128
  readonly #context = new HeadlessCanvasContext(this);
129
+ /** A plain object libraries can write CSS properties to; it has no effect on rendering. */
103
130
  readonly style: Record<string, string> = {};
104
131
  readonly #attributes = new Map<string, string>();
105
132
 
133
+ /**
134
+ * Creates a canvas of the given size. {@link createCanvas} is the usual entry point.
135
+ *
136
+ * @param width - Canvas width in pixels.
137
+ * @param height - Canvas height in pixels.
138
+ */
106
139
  constructor(
140
+ /** Canvas width in pixels. */
107
141
  public width = 300,
142
+ /** Canvas height in pixels. */
108
143
  public height = 150,
109
144
  ) {}
110
145
 
146
+ /** Same as `width`; there is no layout in Node. */
111
147
  get clientWidth(): number {
112
148
  return this.width;
113
149
  }
114
150
 
151
+ /** Same as `height`; there is no layout in Node. */
115
152
  get clientHeight(): number {
116
153
  return this.height;
117
154
  }
118
155
 
156
+ /**
157
+ * Returns the canvas's `GPUCanvasContext` for `'webgpu'`.
158
+ *
159
+ * @param id - The context type. Only `'webgpu'` is supported.
160
+ * @returns The context for `'webgpu'`, or `null` for any other type.
161
+ */
119
162
  getContext(id: 'webgpu'): HeadlessCanvasContext;
163
+ /**
164
+ * Returns the canvas's `GPUCanvasContext` for `'webgpu'`.
165
+ *
166
+ * @param id - The context type. Only `'webgpu'` is supported.
167
+ * @returns The context for `'webgpu'`, or `null` for any other type.
168
+ */
120
169
  getContext(id: string): HeadlessCanvasContext | null;
121
170
  getContext(id: string): HeadlessCanvasContext | null {
122
171
  return id === 'webgpu' ? this.#context : null;
123
172
  }
124
173
 
174
+ /** Reads the current texture back as RGBA8 pixels; see {@link HeadlessCanvasContext.readPixels}. */
125
175
  readPixels(): Promise<RgbaImage> {
126
176
  return this.#context.readPixels();
127
177
  }
128
178
 
129
- /** This same object, cast to the element type a rendering library expects. */
179
+ /**
180
+ * This same object, cast to the element type a rendering library expects.
181
+ *
182
+ * @typeParam TElement - The element type to cast to. Defaults to `HTMLCanvasElement` when DOM types are available.
183
+ */
130
184
  asElement<TElement = DefaultCanvasElement>(): TElement {
131
185
  return this as unknown as TElement;
132
186
  }
133
187
 
134
- getBoundingClientRect(): { x: number; y: number; width: number; height: number; top: number; left: number } {
188
+ /** A rectangle at the origin with the canvas's width and height. */
189
+ getBoundingClientRect(): {
190
+ /** Always 0. */
191
+ x: number;
192
+ /** Always 0. */
193
+ y: number;
194
+ /** The canvas width. */
195
+ width: number;
196
+ /** The canvas height. */
197
+ height: number;
198
+ /** Always 0. */
199
+ top: number;
200
+ /** Always 0. */
201
+ left: number;
202
+ } {
135
203
  return { x: 0, y: 0, top: 0, left: 0, width: this.width, height: this.height };
136
204
  }
137
205
 
206
+ /**
207
+ * Stores an attribute on the canvas.
208
+ *
209
+ * @param name - Attribute name.
210
+ * @param value - Attribute value.
211
+ */
138
212
  setAttribute(name: string, value: string): void {
139
213
  this.#attributes.set(name, value);
140
214
  }
141
215
 
216
+ /**
217
+ * Reads an attribute stored with `setAttribute()`.
218
+ *
219
+ * @param name - Attribute name.
220
+ * @returns The stored value, or `null` if none was set.
221
+ */
142
222
  getAttribute(name: string): string | null {
143
223
  return this.#attributes.get(name) ?? null;
144
224
  }
145
225
 
226
+ /** Does nothing; present so libraries that register listeners keep working. */
146
227
  addEventListener(): void {}
228
+ /** Does nothing; present so libraries that remove listeners keep working. */
147
229
  removeEventListener(): void {}
230
+ /** Does nothing and returns `true`. */
148
231
  dispatchEvent(): boolean {
149
232
  return true;
150
233
  }
151
234
  }
152
235
 
236
+ /**
237
+ * Creates a headless canvas for WebGPU tests.
238
+ *
239
+ * @param width - Canvas width in pixels.
240
+ * @param height - Canvas height in pixels.
241
+ * @returns A {@link HeadlessCanvas}; pass `canvas.asElement()` to libraries that expect an `HTMLCanvasElement`.
242
+ */
153
243
  export const createCanvas = (width: number, height: number): HeadlessCanvas => new HeadlessCanvas(width, height);
package/src/index.ts CHANGED
@@ -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 };
@@ -46,11 +64,35 @@ const restore = (properties: SavedProperty[]): void => {
46
64
  }
47
65
  };
48
66
 
67
+ /**
68
+ * The Vitest environment registered as `webgpu-node`. Before each test file it puts `navigator.gpu`, the `GPU*`
69
+ * classes and constants, and minimal `window`, `document`, `HTMLCanvasElement` and `requestAnimationFrame` shims
70
+ * on the global object (only where missing), and it removes them afterwards.
71
+ */
49
72
  export default {
73
+ /** The name Vitest uses to select this environment: `test.environment: 'webgpu-node'`. */
50
74
  name: 'webgpu-node',
75
+ /** Runs test files through Vite's `ssr` environment, as Node code. */
51
76
  viteEnvironment: 'ssr',
77
+ /**
78
+ * Creates the Dawn-backed `navigator.gpu` and installs the DOM shims on the test file's global object.
79
+ *
80
+ * @param global - The global object of the test file's context.
81
+ * @param __namedParameters - The environment options from the Vitest config; `webgpuNode` holds {@link WebgpuNodeOptions}.
82
+ * @returns A `teardown` hook that destroys the devices this environment created and restores the globals.
83
+ */
52
84
  setup(global: Record<string, unknown>, { webgpuNode = {} }: { webgpuNode?: WebgpuNodeOptions }) {
53
- const shims = { ...globals, ...domShims() } as Record<string, unknown>;
85
+ const dom = domShims();
86
+ // oxlint-disable-next-line typescript/no-misused-spread -- intentionally copies the own enumerable GPU* globals
87
+ const shims = { ...globals, ...dom.shims } as Record<string, unknown>;
88
+ const devices = new Set<GPUDevice>();
89
+ let disposed = false;
90
+ const dispose = () => {
91
+ disposed = true;
92
+ dom.dispose();
93
+ for (const device of devices) device.destroy();
94
+ devices.clear();
95
+ };
54
96
  const modified: SavedProperty[] = [];
55
97
  try {
56
98
  for (const key of Object.keys(shims)) {
@@ -68,19 +110,52 @@ export default {
68
110
  }
69
111
  const navigator = global.navigator as object;
70
112
  modified.push({ target: navigator, key: 'gpu', descriptor: Object.getOwnPropertyDescriptor(navigator, 'gpu') });
113
+ const gpu = create(webgpuNode.dawnOptions ?? []);
114
+ const requestAdapter = gpu.requestAdapter.bind(gpu);
115
+ gpu.requestAdapter = async (options) => {
116
+ if (disposed) throw new Error('WebGPU environment has been torn down');
117
+ const adapter = await requestAdapter(options);
118
+ if (adapter) {
119
+ const requestDevice = adapter.requestDevice.bind(adapter);
120
+ adapter.requestDevice = async (descriptor) => {
121
+ if (disposed) throw new Error('WebGPU environment has been torn down');
122
+ const device = await requestDevice(descriptor);
123
+ if (disposed) {
124
+ device.destroy();
125
+ throw new Error('WebGPU environment has been torn down');
126
+ }
127
+ devices.add(device);
128
+ return device;
129
+ };
130
+ }
131
+ return adapter;
132
+ };
71
133
  Object.defineProperty(navigator, 'gpu', {
72
- value: create(webgpuNode.dawnOptions ?? []),
134
+ value: gpu,
73
135
  writable: false,
74
136
  enumerable: false,
75
137
  configurable: true,
76
138
  });
77
139
  } catch (error) {
140
+ dispose();
78
141
  restore(modified);
79
142
  throw error;
80
143
  }
81
144
  return {
82
- teardown(_global?: Record<string, unknown>) {
83
- restore(modified);
145
+ /**
146
+ * Destroys the GPU devices requested through this environment, cancels pending animation-frame callbacks and
147
+ * removes the globals that `setup` added.
148
+ *
149
+ * @param _global - Unused; teardown restores the global object it captured in `setup`.
150
+ */
151
+ teardown: (_global?: Record<string, unknown>) => {
152
+ if (disposed) return;
153
+ try {
154
+ dispose();
155
+ } finally {
156
+ restore(modified);
157
+ modified.length = 0;
158
+ }
84
159
  },
85
160
  };
86
161
  },