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(); 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) => { 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 }; }