vitest-environment-webgl-node
v3.0.1
Published
Vitest environment for real headless WebGL 1/2 tests in Node (ANGLE via @onirenaud/node-webgl): no browser, Playwright or canvas mocks
Maintainers
Readme
vitest-environment-webgl-node
Run real WebGL 1 and WebGL 2 tests in Vitest on Node, without a browser, Playwright or canvas mocks.
Headless GPU testing for CI: canvas.getContext('webgl2') returns a GPU-backed context, so three.js,
regl, PixiJS and plain WebGL code run unchanged. Native GPU testing is up to 2.4x faster than running
the same tests in a browser, see the
blog post.
Powered by @onirenaud/node-webgl (Chrome's ANGLE,
statically linked).
pnpm add -D vitest vitest-environment-webgl-node// vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({ test: { environment: 'webgl-node' } });// gl.test.ts
import { expect, it } from 'vitest';
it('clears to red on a real GPU', () => {
const canvas = document.createElement('canvas');
canvas.width = canvas.height = 4;
const gl = canvas.getContext('webgl2')!;
gl.clearColor(1, 0, 0, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
const pixel = new Uint8Array(4);
gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixel);
expect([...pixel]).toEqual([255, 0, 0, 255]);
});The environment installs window, document, HTMLCanvasElement, Image, requestAnimationFrame,
the WebGL* classes and a file-reading fetch() before each test file, and removes them afterwards.
Using Jest instead? See the equivalent
jest-environment-webgl-node
in jest-gpu.
Stability
As of 1.0.0, the public API (the environment export and its options) is considered stable. Breaking changes will be released as a new major version.
Compared to alternatives
| Approach | Real rendering | Notes |
| --------------------------------------------------------- | -------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| 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. |
| Raw gl | Yes | WebGL 1 only (per node-webgl's docs), runs on the system OpenGL driver, and you wire up the DOM globals yourself. |
| 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. |
| 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. |
Supported versions and platforms
- Vitest 4 and 5 (CI tests both). Vitest 3 is not supported.
- Node 22 or later. CI runs Node 22 and 26 on
macos-latestandubuntu-latest. - macOS uses Metal. Linux uses Mesa (see below). Windows uses Direct3D 11 per the node-webgl docs; this repo's CI does not run on Windows.
Useful for
three.js WebGLRenderer, Babylon.js, regl, PixiJS, deck.gl and plain WebGL code. The demos exercise three.js and
Babylon.js; node-webgl's docs name regl, PixiJS and deck.gl. These are compatible use cases, not endorsements.
Usage
// vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
environment: 'webgl-node',
environmentOptions: { webglNode: { backend: 'swiftshader', baseDir: 'assets' } },
},
});import * as THREE from 'three';
import { expect, it } from 'vitest';
it('renders', () => {
const canvas = document.createElement('canvas');
const renderer = new THREE.WebGLRenderer({ canvas });
renderer.setSize(64, 64, false);
renderer.render(new THREE.Scene(), new THREE.PerspectiveCamera());
expect(canvas.getImageData().width).toBe(64);
});Options are those of node-webgl's init() (backend, api) and installDOM()
(baseDir, fetch, devicePixelRatio, innerWidth, innerHeight, frameInterval).
For a Node-only TypeScript project, opt into types for the installed globals by adding
import 'vitest-environment-webgl-node/globals'; to a .d.ts file your tsconfig includes. Do not include
this entry in projects that load TypeScript's DOM library: the real browser globals and node-webgl's shims
intentionally have different types. DOM projects can use their standard global declarations and cast a
node-webgl canvas at the library boundary when necessary.
On Linux CI install Mesa (apt-get install libegl1 libgles2 libgl1-mesa-dri) and set
LIBGL_ALWAYS_SOFTWARE=1. macOS and Windows use prebuilt binaries.
Image snapshots
Pair with vitest-screenshot to diff the canvas against a committed baseline:
import * as THREE from 'three';
import { expect, it } from 'vitest';
import { extendMatchers } from 'vitest-screenshot';
extendMatchers();
it('renders a cube', async () => {
const canvas = document.createElement('canvas');
const renderer = new THREE.WebGLRenderer({ canvas });
renderer.setSize(256, 256, false);
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(50, 1, 0.1, 100);
camera.position.z = 3;
scene.add(new THREE.Mesh(new THREE.BoxGeometry(), new THREE.MeshNormalMaterial()));
renderer.render(scene, camera);
await expect(canvas).toMatchScreenshot('cube.png');
renderer.dispose();
});See also
vitest-environment-webgpu-nodefor headless WebGPU.vitest-screenshotfortoMatchScreenshot().demo/for WebGL contexts, uploads, readbacks, three.js and Babylon.js tests, and the repository README.
Author
Created by Ben Houston and sponsored by Land of Assets.
License
MIT
Resource cleanup
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.
