I’m very disappointed because I had an amazing Jest unit testing setup configured with headless-gl: GitHub - stackgl/headless-gl: 🎃 Windowless WebGL for node.js. I could render a frame using WebGLRenderer.render() just like a browser environment and examine object positions and properties for the test.
However, since THREE deprecated WebGL1 in 0.163.0, I can’t use this anymore since the package doesn’t support WebGL2.
Does anyone have unit testing suggestions? Using Pupeteer is not an option. I have 1,000+ tests that I would need to migrate and I would not be interested in testing with a headless browser rendering a page anyway.
I know that I could also mock WebGLRenderer.render() to at least do the essential things like update matrices. This would work well for me because many of my tests are not dependent on actual visual output, but this is less than ideal.
1 Like
I made some progress testing THREE with headless-gl and WebGL 2 and wanted to share. Hopefully it is helpful to some people. I thought this would be a daunting task, but I was able to mock some WebGL 2 methods with empty functions so that WebGLRenderer can initialize successfully and render() can be called without issue. This fixed 99% of my tests.
In a Jest environment using JSDom, this is the code snippet I am using. I am mocking HTMLCanvasElement.getContext() and injecting headless-gl. Perhaps there is a better way, but this is how I’ve always done it. I then mocked only 4 WebGL 2 methods to get things working. These methods are called when WebGLRenderer initializes for various reasons.
// This is how we set up headless GL. Without this, there will be an error when the WebGLRenderer tries to create a context.
const gl = require("gl")(window.innerWidth, window.innerHeight);
const getContextOriginal = HTMLCanvasElement.prototype.getContext;
HTMLCanvasElement.prototype.getContext = function(contextId, options) {
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.
gl.texImage3D = () => {};
gl.createVertexArray = () => {};
gl.bindVertexArray = () => {};
gl.deleteVertexArray = () => {};
return gl;
}
// It's important that we allow other contexts to be created (ex. '2d'). Otherwise, this can cause THREE errors and warnings.
return getContextOriginal.call(this, contextId, options);
}
This is far from perfect, but allows you to invoke a render loop like a real app and test app logic, object positions, matrices, etc. Depending on the type of project you are developing, this might be very useful. If you need to test shader compilation, visual output, etc, you will still need to use something like Puppeteer.
1 Like
TYSM for this! I lost the code I had written 9 years ago where I figured this out. Thank you!
For anyone coming from a search engine, here’s my full Node script:
import fs from 'fs';
import * as THREE from 'three';
import { Canvas } from 'canvas';
import { default as createGL } from 'gl';
// Create a headless WebGL context
const width = 512;
const height = 512;
const gl = createGL(width, height);
// stub for Three.js compatibility
let HTMLCanvasElement = new Canvas(width, height, 'webgl2');
HTMLCanvasElement.removeEventListener = () => {};
HTMLCanvasElement.addEventListener = () => {};
HTMLCanvasElement.style = { width, height }
const getContextOriginal = Canvas.prototype.getContext;
HTMLCanvasElement.getContext = function(contextId, options) {
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.
gl.texImage3D = () => {};
gl.createVertexArray = () => {};
gl.bindVertexArray = () => {};
gl.deleteVertexArray = () => {};
return gl;
}
// It's important that we allow other contexts to be created (ex. '2d'). Otherwise, this can cause THREE errors and warnings.
return getContextOriginal.call(this, contextId, options);
}
const ctx = HTMLCanvasElement.getContext('2d');
// Three Setup
var renderer = new THREE.WebGLRenderer({
context: gl,
canvas: HTMLCanvasElement
});
renderer.setSize(width, height);
renderer.setClearColor(0xFFFFFF, 1); // sets bg color
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000);
camera.position.z = 5;
scene.add(camera);
// Create a green cube
const geometry = new THREE.BoxGeometry();
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);
renderer.render(scene, camera);
// 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);
// Save output as a PNG file
fs.writeFileSync('output.png', HTMLCanvasElement.toBuffer('image/png'));