Thanks a ton! Now I’m able to unit test threejs modules. Here’s my setupTests.ts
file:
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable import/no-extraneous-dependencies */
import { GlobalRegistrator } from '@happy-dom/global-registrator';
import { Canvas } from 'canvas';
import { writeFileSync } from 'fs';
import { WebGLRenderer } from 'three';
GlobalRegistrator.register();
export const createHeadlessWebGLRenderer = async (width = 512, height = 512) => {
const { default: createWebGLContext } = await import('gl');
const gl = createWebGLContext(width, height);
// Mock WebGL2 methods that Three.js might use
Object.assign(gl, {
texImage3D: () => {},
createVertexArray: () => {},
bindVertexArray: () => {},
deleteVertexArray: () => {},
});
// @ts-expect-error - Stub for Three.js compatibility
const canvas = new Canvas(width, height, 'webgl2');
const originalGetContext = canvas.getContext.bind(canvas);
Object.assign(canvas, {
removeEventListener: () => {},
addEventListener: () => {},
style: { width, height },
getContext: (contextId: '2d' | 'webgl2', options: any) => {
if (contextId === 'webgl2') {
// headless-gl only supports WebGL 1. However, with a few hacks, we can make THREE work reasonably well with headless-gl.
// This is far from perfect. Even THREE's default shaders won't compile since they use WebGL 2. However, this is irrelevant for 99% of my tests.
// Until headless-gl supports WebGL 2, tests that require shader compilation, visual output, or WebGL data will need to use a browser environment via Puppeteer.
return gl;
}
// It's important that we allow other contexts to be created (ex. '2d'). Otherwise, this can cause THREE errors and warnings.
return originalGetContext(contextId, options);
},
});
const ctx = canvas.getContext('2d');
// Three Setup
const renderer = new WebGLRenderer({
context: gl,
canvas: canvas as unknown as HTMLCanvasElement,
});
renderer.setSize(width, height);
renderer.setClearColor(0xffffff, 1); // sets bg color
return {
renderer,
canvas,
takeSnapshot: (filepath: string, format: 'png' | 'jpeg' = 'png') => {
// Read pixels and save as an image
const pixels = new Uint8Array(width * height * 4);
gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
// Copy pixel data to canvas
const imageData = ctx.createImageData(width, height);
imageData.data.set(pixels);
ctx.putImageData(imageData, 0, 0);
const buffer = canvas.toBuffer(`image/${format}` as any);
writeFileSync(filepath, buffer);
},
};
};