Files
image-studio/renderer/checkerboard.test.ts
2026-07-04 15:43:50 +02:00

135 lines
4.5 KiB
TypeScript

import { describe, expect, test } from "bun:test";
import { createCheckerboardRenderer } from "./checkerboard";
import type { WebGlRendererContext } from "./types";
type FakeGlCall = {
name: string;
args: unknown[];
};
type FakeUniformLocation = {
name: string;
};
describe("checkerboard renderer", () => {
test("renders a clipped checkerboard with one draw call", () => {
const fake = createFakeGl();
const renderer = createCheckerboardRenderer(createFakeContext(fake.gl, 100, 80));
renderer.render({ x: -5, y: 3, w: 30, h: 20 }, 12);
expect(fake.calls.filter((call) => call.name === "drawArrays")).toHaveLength(1);
expect(fake.calls.find((call) => call.name === "scissor")?.args).toEqual([0, 57, 25, 20]);
expect(fake.uniforms.get("u_origin")).toEqual([-5, 3]);
expect(fake.uniforms.get("u_squareSize")).toEqual([12]);
expect(Array.from(fake.bufferData.at(-1) ?? [])).toEqual([0, 3, 25, 3, 0, 23, 0, 23, 25, 3, 25, 23]);
});
test("skips fully clipped checkerboards", () => {
const fake = createFakeGl();
const renderer = createCheckerboardRenderer(createFakeContext(fake.gl, 100, 80));
renderer.render({ x: 120, y: 0, w: 20, h: 20 }, 8);
expect(fake.calls.filter((call) => call.name === "drawArrays")).toHaveLength(0);
});
test("disposes WebGL resources", () => {
const fake = createFakeGl();
const renderer = createCheckerboardRenderer(createFakeContext(fake.gl, 100, 80));
renderer.dispose();
expect(fake.calls.filter((call) => call.name === "deleteBuffer")).toHaveLength(1);
expect(fake.calls.filter((call) => call.name === "deleteProgram")).toHaveLength(1);
});
});
function createFakeContext(gl: WebGL2RenderingContext, width: number, height: number): WebGlRendererContext {
return {
gl,
canvas: { width, height } as HTMLCanvasElement,
};
}
function createFakeGl() {
const calls: FakeGlCall[] = [];
const uniforms = new Map<string, number[]>();
const bufferData: Float32Array[] = [];
const shader = {};
const program = {};
const buffer = {};
const noop = () => {};
const gl = {
ARRAY_BUFFER: 0x8892,
BLEND: 0x0be2,
COMPILE_STATUS: 0x8b81,
DYNAMIC_DRAW: 0x88e8,
FLOAT: 0x1406,
FRAGMENT_SHADER: 0x8b30,
LINK_STATUS: 0x8b82,
SCISSOR_TEST: 0x0c11,
TRIANGLES: 0x0004,
VERTEX_SHADER: 0x8b31,
attachShader: noop,
bindBuffer: noop,
bufferData: (...args: [number, BufferSource, number]) => {
const data = args[1];
if (data instanceof Float32Array) bufferData.push(data);
},
compileShader: noop,
createBuffer: () => buffer,
createProgram: () => program,
createShader: () => shader,
deleteBuffer: () => {
calls.push({ name: "deleteBuffer", args: [] });
},
deleteProgram: () => {
calls.push({ name: "deleteProgram", args: [] });
},
deleteShader: noop,
disable: (capability: number) => {
calls.push({ name: "disable", args: [capability] });
},
disableVertexAttribArray: (index: number) => {
calls.push({ name: "disableVertexAttribArray", args: [index] });
},
drawArrays: (mode: number, first: number, count: number) => {
calls.push({ name: "drawArrays", args: [mode, first, count] });
},
enable: (capability: number) => {
calls.push({ name: "enable", args: [capability] });
},
enableVertexAttribArray: (index: number) => {
calls.push({ name: "enableVertexAttribArray", args: [index] });
},
getAttribLocation: () => 0,
getProgramInfoLog: () => null,
getProgramParameter: () => true,
getShaderInfoLog: () => null,
getShaderParameter: () => true,
getUniformLocation: (...args: [unknown, string]) => ({ name: args[1] }),
linkProgram: noop,
scissor: (x: number, y: number, w: number, h: number) => {
calls.push({ name: "scissor", args: [x, y, w, h] });
},
shaderSource: noop,
uniform1f: (location: FakeUniformLocation, value: number) => {
uniforms.set(location.name, [value]);
},
uniform2f: (location: FakeUniformLocation, x: number, y: number) => {
uniforms.set(location.name, [x, y]);
},
uniform4fv: (location: FakeUniformLocation, value: Iterable<number>) => {
uniforms.set(location.name, Array.from(value));
},
useProgram: noop,
vertexAttribPointer: (index: number, size: number, type: number, normalized: boolean, stride: number, offset: number) => {
calls.push({ name: "vertexAttribPointer", args: [index, size, type, normalized, stride, offset] });
},
} as unknown as WebGL2RenderingContext;
return { gl, calls, uniforms, bufferData };
}