vitest-environment-webgl-node 3.0.0 → 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,18 +3,46 @@
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 WebGL 1 and WebGL 2 inside Vitest, with no browser. Native GPU testing is up to
9
- 2.4x faster 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
12
- [`@onirenaud/node-webgl`](https://github.com/RenaudRohlinger/node-webgl) (Chrome's ANGLE, statically linked).
9
+ Run real WebGL 1 and WebGL 2 tests in Vitest on Node, without a browser, Playwright or canvas mocks.
10
+ Headless GPU testing for CI: `canvas.getContext('webgl2')` returns a GPU-backed context, so three.js,
11
+ regl, PixiJS and plain WebGL code run unchanged. Native GPU testing is up to 2.4x faster than running
12
+ the same tests in a browser, see the
13
+ [blog post](https://ben3d.ca/blog/native-gpu-testing-for-vitest-and-jest).
14
+ Powered by [`@onirenaud/node-webgl`](https://github.com/RenaudRohlinger/node-webgl) (Chrome's ANGLE,
15
+ statically linked).
16
+
17
+ ```sh
18
+ pnpm add -D vitest vitest-environment-webgl-node
19
+ ```
20
+
21
+ ```ts
22
+ // vitest.config.ts
23
+ import { defineConfig } from 'vitest/config';
24
+
25
+ export default defineConfig({ test: { environment: 'webgl-node' } });
26
+ ```
27
+
28
+ ```ts
29
+ // gl.test.ts
30
+ import { expect, it } from 'vitest';
31
+
32
+ it('clears to red on a real GPU', () => {
33
+ const canvas = document.createElement('canvas');
34
+ canvas.width = canvas.height = 4;
35
+ const gl = canvas.getContext('webgl2')!;
36
+ gl.clearColor(1, 0, 0, 1);
37
+ gl.clear(gl.COLOR_BUFFER_BIT);
38
+ const pixel = new Uint8Array(4);
39
+ gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixel);
40
+ expect([...pixel]).toEqual([255, 0, 0, 255]);
41
+ });
42
+ ```
13
43
 
14
44
  The environment installs `window`, `document`, `HTMLCanvasElement`, `Image`, `requestAnimationFrame`,
15
45
  the `WebGL*` classes and a file-reading `fetch()` before each test file, and removes them afterwards.
16
- `document.createElement('canvas').getContext('webgl2')` returns a GPU-backed context, so three.js,
17
- regl, pixi and plain WebGL code run unchanged.
18
46
 
19
47
  Using Jest instead? See the equivalent
20
48
  [`jest-environment-webgl-node`](https://github.com/bhouston/jest-gpu/tree/main/packages/jest-environment-webgl-node)
@@ -25,13 +53,26 @@ in [jest-gpu](https://github.com/bhouston/jest-gpu).
25
53
  As of 1.0.0, the public API (the environment export and its options) is considered stable.
26
54
  Breaking changes will be released as a new major version.
27
55
 
28
- ## Install
56
+ ## Compared to alternatives
29
57
 
30
- ```sh
31
- pnpm add -D vitest vitest-environment-webgl-node
32
- ```
58
+ | Approach | Real rendering | Notes |
59
+ | --------------------------------------------------------- | -------------- | -------------------------------------------------------------------------------------------------------------------------------- |
60
+ | jsdom plus a canvas mock (`jest-webgl-canvas-mock`, etc.) | No | Fast and light, but `getContext()` returns stubs: you can assert calls, not pixels or shader results. |
61
+ | Raw [`gl`](https://github.com/stackgl/headless-gl) | Yes | WebGL 1 only (per node-webgl's docs), runs on the system OpenGL driver, and you wire up the DOM globals yourself. |
62
+ | Vitest browser mode or Playwright | Yes | A real browser: heavier to install and slower to start, and the right choice when you need real browser APIs. |
63
+ | vitest-environment-webgl-node | Yes | ANGLE in the Node process, WebGL 1 and 2. Not a browser: only canvas, `Image`, `fetch` and `requestAnimationFrame` are provided. |
33
64
 
34
- Requires Vitest 4 or 5. Vitest 3 is not supported.
65
+ ## Supported versions and platforms
66
+
67
+ - Vitest 4 and 5 (CI tests both). Vitest 3 is not supported.
68
+ - Node 22 or later. CI runs Node 22 and 26 on `macos-latest` and `ubuntu-latest`.
69
+ - macOS uses Metal. Linux uses Mesa (see below). Windows uses Direct3D 11 per the
70
+ [node-webgl docs](https://github.com/RenaudRohlinger/node-webgl#platforms); this repo's CI does not run on Windows.
71
+
72
+ ## Useful for
73
+
74
+ three.js `WebGLRenderer`, Babylon.js, regl, PixiJS, deck.gl and plain WebGL code. The demos exercise three.js and
75
+ Babylon.js; node-webgl's docs name regl, PixiJS and deck.gl. These are compatible use cases, not endorsements.
35
76
 
36
77
  ## Usage
37
78
 
@@ -62,7 +103,6 @@ it('renders', () => {
62
103
 
63
104
  Options are those of node-webgl's `init()` (`backend`, `api`) and `installDOM()`
64
105
  (`baseDir`, `fetch`, `devicePixelRatio`, `innerWidth`, `innerHeight`, `frameInterval`).
65
- Pair with [`vitest-screenshot`](../vitest-screenshot) for pixel-diff assertions.
66
106
 
67
107
  For a Node-only TypeScript project, opt into types for the installed globals by adding
68
108
  `import 'vitest-environment-webgl-node/globals';` to a `.d.ts` file your tsconfig includes. Do not include
@@ -73,6 +113,39 @@ node-webgl canvas at the library boundary when necessary.
73
113
  On Linux CI install Mesa (`apt-get install libegl1 libgles2 libgl1-mesa-dri`) and set
74
114
  `LIBGL_ALWAYS_SOFTWARE=1`. macOS and Windows use prebuilt binaries.
75
115
 
116
+ ## Image snapshots
117
+
118
+ Pair with [`vitest-screenshot`](https://github.com/bhouston/vitest-gpu/tree/main/packages/vitest-screenshot) to diff the canvas against a committed baseline:
119
+
120
+ ```ts
121
+ import * as THREE from 'three';
122
+ import { expect, it } from 'vitest';
123
+ import { extendMatchers } from 'vitest-screenshot';
124
+
125
+ extendMatchers();
126
+
127
+ it('renders a cube', async () => {
128
+ const canvas = document.createElement('canvas');
129
+ const renderer = new THREE.WebGLRenderer({ canvas });
130
+ renderer.setSize(256, 256, false);
131
+ const scene = new THREE.Scene();
132
+ const camera = new THREE.PerspectiveCamera(50, 1, 0.1, 100);
133
+ camera.position.z = 3;
134
+ scene.add(new THREE.Mesh(new THREE.BoxGeometry(), new THREE.MeshNormalMaterial()));
135
+
136
+ renderer.render(scene, camera);
137
+ await expect(canvas).toMatchScreenshot('cube.png');
138
+ renderer.dispose();
139
+ });
140
+ ```
141
+
142
+ ## See also
143
+
144
+ - [`vitest-environment-webgpu-node`](https://github.com/bhouston/vitest-gpu/tree/main/packages/vitest-environment-webgpu-node) for headless WebGPU.
145
+ - [`vitest-screenshot`](https://github.com/bhouston/vitest-gpu/tree/main/packages/vitest-screenshot) for `toMatchScreenshot()`.
146
+ - [`demo/`](https://github.com/bhouston/vitest-gpu/tree/main/demo/test/webgl) for WebGL contexts, uploads, readbacks, three.js and Babylon.js tests, and the
147
+ [repository README](https://github.com/bhouston/vitest-gpu#readme).
148
+
76
149
  ## Author
77
150
 
78
151
  Created by [Ben Houston](https://ben3d.ca) and sponsored by [Land of Assets](https://landofassets.com).
@@ -88,3 +161,9 @@ MIT
88
161
  [tests-url]: https://github.com/bhouston/vitest-gpu/actions/workflows/ci.yml
89
162
  [discord-badge]: https://img.shields.io/badge/Discord-Join%20Chat-5865F2?logo=discord&logoColor=white
90
163
  [discord-url]: https://discord.gg/fwupDN493R
164
+ [coverage-badge]: https://codecov.io/gh/bhouston/vitest-gpu/branch/main/graph/badge.svg
165
+ [coverage-url]: https://codecov.io/gh/bhouston/vitest-gpu
166
+
167
+ ## Resource cleanup
168
+
169
+ Environment teardown destroys contexts first created through `Canvas.getContext()` during the test file, including DOM canvases, `OffscreenCanvas` and imported canvas factories. It also cancels pending callbacks scheduled through its animation-frame shim. Contexts created directly with the WebGL context constructors, or before setup, remain the caller's responsibility: call `destroy()` on them. Dispose renderers and canvases during a long-running file to release resources sooner.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAoC,KAAK,WAAW,EAAE,KAAK,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAMnH,2EAA2E;AAC3E,MAAM,MAAM,gBAAgB,GAAG,WAAW,GAAG,iBAAiB,CAAC;;;;kBAkF/C,OAAO,UAAU,iBAAsB;QAAE,SAAS,CAAC,EAAE,gBAAgB,CAAA;KAAE;oBAwCrE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,WAAW,EAChB,KAAK,iBAAiB,EACvB,MAAM,uBAAuB,CAAC;AAM/B,2EAA2E;AAC3E,MAAM,MAAM,gBAAgB,GAAG,WAAW,GAAG,iBAAiB,CAAC;;;;kBAkF/C,OAAO,UAAU,iBAAsB;QAAE,SAAS,CAAC,EAAE,gBAAgB,CAAA;KAAE;oBAyFrE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC"}
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { getDisplayInfo, init, installDOM } from '@onirenaud/node-webgl';
1
+ import { Canvas, getDisplayInfo, init, installDOM, } from '@onirenaud/node-webgl';
2
2
  import { readFile } from 'node:fs/promises';
3
3
  import { extname, resolve } from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
@@ -90,11 +90,64 @@ export default {
90
90
  }
91
91
  init({ backend, api });
92
92
  }
93
+ const contexts = new Set();
94
+ const frames = new Set();
95
+ let disposed = false;
96
+ const dispose = () => {
97
+ disposed = true;
98
+ for (const frame of frames)
99
+ clearTimeout(frame);
100
+ frames.clear();
101
+ for (const context of contexts)
102
+ context.destroy();
103
+ contexts.clear();
104
+ };
105
+ const hasFrame = global.requestAnimationFrame !== undefined;
93
106
  const before = snapshot(global);
94
107
  const nested = [];
95
108
  try {
96
109
  // node-webgl remembers fetch installation at module scope, which cannot represent separate Vitest environments.
97
110
  installDOM({ ...dom, fetch: false });
111
+ // Intercept the shared canvas API so imported factories and OffscreenCanvas are covered too.
112
+ const canvasBefore = snapshot(Canvas.prototype);
113
+ nested.push({ target: Canvas.prototype, before: canvasBefore });
114
+ const getContext = Canvas.prototype.getContext;
115
+ Canvas.prototype.getContext = function (...args) {
116
+ if (disposed)
117
+ throw new Error('WebGL environment has been torn down');
118
+ // node-webgl exposes the cached context at runtime but strips it from its declarations.
119
+ const previous = this._ctx;
120
+ const context = getContext.apply(this, args);
121
+ if (context && context !== previous)
122
+ contexts.add(context);
123
+ return context;
124
+ };
125
+ nested.at(-1).changed = changedProperties(canvasBefore, Canvas.prototype);
126
+ // Own only the rAF shim installed by this setup; preserve host implementations.
127
+ if (!hasFrame) {
128
+ Object.defineProperty(global, 'requestAnimationFrame', {
129
+ configurable: true,
130
+ writable: true,
131
+ value: (callback) => {
132
+ if (disposed)
133
+ return 0;
134
+ const frame = setTimeout(() => {
135
+ frames.delete(frame);
136
+ callback(performance.now());
137
+ }, dom.frameInterval ?? 16).unref();
138
+ frames.add(frame);
139
+ return frame;
140
+ },
141
+ });
142
+ Object.defineProperty(global, 'cancelAnimationFrame', {
143
+ configurable: true,
144
+ writable: true,
145
+ value: (frame) => {
146
+ frames.delete(frame);
147
+ clearTimeout(frame);
148
+ },
149
+ });
150
+ }
98
151
  if (dom.fetch !== false)
99
152
  installFetch(global, dom.baseDir ?? process.cwd());
100
153
  // node-webgl's window/document have no event methods; engines like Babylon.js register resize/blur listeners.
@@ -110,6 +163,7 @@ export default {
110
163
  }
111
164
  }
112
165
  catch (error) {
166
+ dispose();
113
167
  for (const item of nested.toReversed())
114
168
  restore(item.target, item.before, item.changed ?? changedProperties(item.before, item.target));
115
169
  restore(global, before, changedProperties(before, global));
@@ -118,9 +172,18 @@ export default {
118
172
  const changed = changedProperties(before, global);
119
173
  return {
120
174
  teardown(g) {
121
- for (const item of nested.toReversed())
122
- restore(item.target, item.before, item.changed);
123
- restore(g, before, changed);
175
+ if (disposed)
176
+ return;
177
+ try {
178
+ dispose();
179
+ }
180
+ finally {
181
+ for (const item of nested.toReversed())
182
+ restore(item.target, item.before, item.changed);
183
+ restore(g, before, changed);
184
+ nested.length = 0;
185
+ before.clear();
186
+ }
124
187
  },
125
188
  };
126
189
  },
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,24 @@
1
1
  {
2
2
  "name": "vitest-environment-webgl-node",
3
- "version": "3.0.0",
4
- "description": "Vitest environment that provides real headless WebGL 1/2 (ANGLE via @onirenaud/node-webgl) in Node",
3
+ "version": "3.0.1",
4
+ "description": "Vitest environment for real headless WebGL 1/2 tests in Node (ANGLE via @onirenaud/node-webgl): no browser, Playwright or canvas mocks",
5
5
  "keywords": [
6
+ "babylonjs",
7
+ "canvas",
8
+ "ci",
9
+ "gpu",
10
+ "gpu-testing",
6
11
  "headless",
12
+ "headless-gl",
13
+ "node",
14
+ "pixijs",
15
+ "regl",
7
16
  "testing",
8
17
  "three.js",
9
18
  "vitest",
10
19
  "vitest-environment",
11
20
  "webgl",
21
+ "webgl-testing",
12
22
  "webgl2"
13
23
  ],
14
24
  "homepage": "https://github.com/bhouston/vitest-gpu#readme",
package/src/index.ts CHANGED
@@ -1,4 +1,11 @@
1
- import { getDisplayInfo, init, installDOM, type InitOptions, type InstallDOMOptions } from '@onirenaud/node-webgl';
1
+ import {
2
+ Canvas,
3
+ getDisplayInfo,
4
+ init,
5
+ installDOM,
6
+ type InitOptions,
7
+ type InstallDOMOptions,
8
+ } from '@onirenaud/node-webgl';
2
9
  import { readFile } from 'node:fs/promises';
3
10
  import { extname, resolve } from 'node:path';
4
11
  import { fileURLToPath } from 'node:url';
@@ -103,11 +110,59 @@ export default {
103
110
  }
104
111
  init({ backend, api });
105
112
  }
113
+ const contexts = new Set<NonNullable<ReturnType<Canvas['getContext']>>>();
114
+ const frames = new Set<ReturnType<typeof setTimeout>>();
115
+ let disposed = false;
116
+ const dispose = () => {
117
+ disposed = true;
118
+ for (const frame of frames) clearTimeout(frame);
119
+ frames.clear();
120
+ for (const context of contexts) context.destroy();
121
+ contexts.clear();
122
+ };
123
+ const hasFrame = (global as unknown as Record<string, unknown>).requestAnimationFrame !== undefined;
106
124
  const before = snapshot(global);
107
125
  const nested: { target: object; before: PropertySnapshot; changed?: Set<PropertyKey> }[] = [];
108
126
  try {
109
127
  // node-webgl remembers fetch installation at module scope, which cannot represent separate Vitest environments.
110
128
  installDOM({ ...dom, fetch: false });
129
+ // Intercept the shared canvas API so imported factories and OffscreenCanvas are covered too.
130
+ const canvasBefore = snapshot(Canvas.prototype);
131
+ nested.push({ target: Canvas.prototype, before: canvasBefore });
132
+ const getContext = Canvas.prototype.getContext;
133
+ Canvas.prototype.getContext = function (this: Canvas, ...args: Parameters<Canvas['getContext']>) {
134
+ if (disposed) throw new Error('WebGL environment has been torn down');
135
+ // node-webgl exposes the cached context at runtime but strips it from its declarations.
136
+ const previous = (this as Canvas & { _ctx?: ReturnType<Canvas['getContext']> })._ctx;
137
+ const context = getContext.apply(this, args);
138
+ if (context && context !== previous) contexts.add(context);
139
+ return context;
140
+ } as Canvas['getContext'];
141
+ nested.at(-1)!.changed = changedProperties(canvasBefore, Canvas.prototype);
142
+ // Own only the rAF shim installed by this setup; preserve host implementations.
143
+ if (!hasFrame) {
144
+ Object.defineProperty(global, 'requestAnimationFrame', {
145
+ configurable: true,
146
+ writable: true,
147
+ value: (callback: (time: number) => void) => {
148
+ if (disposed) return 0;
149
+ const frame = setTimeout(() => {
150
+ frames.delete(frame);
151
+ callback(performance.now());
152
+ }, dom.frameInterval ?? 16).unref();
153
+ frames.add(frame);
154
+ return frame;
155
+ },
156
+ });
157
+ Object.defineProperty(global, 'cancelAnimationFrame', {
158
+ configurable: true,
159
+ writable: true,
160
+ value: (frame: ReturnType<typeof setTimeout>) => {
161
+ frames.delete(frame);
162
+ clearTimeout(frame);
163
+ },
164
+ });
165
+ }
111
166
  if (dom.fetch !== false) installFetch(global, dom.baseDir ?? process.cwd());
112
167
  // node-webgl's window/document have no event methods; engines like Babylon.js register resize/blur listeners.
113
168
  const { window, document } = global as unknown as Record<string, Record<string, unknown>>;
@@ -120,6 +175,7 @@ export default {
120
175
  nested.at(-1)!.changed = changedProperties(targetBefore, target);
121
176
  }
122
177
  } catch (error) {
178
+ dispose();
123
179
  for (const item of nested.toReversed())
124
180
  restore(item.target, item.before, item.changed ?? changedProperties(item.before, item.target));
125
181
  restore(global, before, changedProperties(before, global));
@@ -128,8 +184,15 @@ export default {
128
184
  const changed = changedProperties(before, global);
129
185
  return {
130
186
  teardown(g: Record<string, unknown>) {
131
- for (const item of nested.toReversed()) restore(item.target, item.before, item.changed!);
132
- restore(g, before, changed);
187
+ if (disposed) return;
188
+ try {
189
+ dispose();
190
+ } finally {
191
+ for (const item of nested.toReversed()) restore(item.target, item.before, item.changed!);
192
+ restore(g, before, changed);
193
+ nested.length = 0;
194
+ before.clear();
195
+ }
133
196
  },
134
197
  };
135
198
  },