const assert = require("node:assert/strict"); const fs = require("node:fs"); const path = require("node:path"); const vm = require("node:vm"); const mainPath = process.argv[2] ? path.resolve(process.argv[2]) : path.join(__dirname, "midctrl-homepage-optimized", "assets", "js", "main.js"); const source = fs.readFileSync(mainPath, "utf8"); const events = []; const consoleErrors = []; const windowListeners = new Map(); const documentListeners = new Map(); const canvasListeners = new Map(); const timers = new Map(); let nextTimerId = 1; let rafQueue = []; let rafTime = 0; let nextObjectId = 1; let reducedMotionChangeListener = null; let documentHidden = true; let documentReadyState = "loading"; function addListener(store, type, callback) { if (!store.has(type)) store.set(type, []); store.get(type).push(callback); } function emit(store, type, event = {}) { for (const callback of store.get(type) || []) callback(event); } function flushTimers() { const pending = [...timers.entries()]; timers.clear(); for (const [, callback] of pending) callback(); } function flushAnimationFrames() { const pending = rafQueue; rafQueue = []; rafTime += 16.667; for (const callback of pending) callback(rafTime); } function makeObject(type) { return { type, id: nextObjectId++ }; } const constants = { NO_ERROR: 0, FRAMEBUFFER_COMPLETE: 0x8cd5, MAX_TEXTURE_SIZE: 0x0d33, MAX_RENDERBUFFER_SIZE: 0x84e8, ALIASED_POINT_SIZE_RANGE: 0x846d, COMPILE_STATUS: 0x8b81, LINK_STATUS: 0x8b82, VERTEX_SHADER: 0x8b31, FRAGMENT_SHADER: 0x8b30, HIGH_FLOAT: 0x8df2, TEXTURE0: 0x84c0, TEXTURE1: 0x84c1, }; const glTarget = { ...constants, failTexturePixelsAbove: Infinity, createFramebuffer() { events.push("createFramebuffer"); return makeObject("framebuffer"); }, deleteFramebuffer() { events.push("deleteFramebuffer"); }, createTexture() { return makeObject("texture"); }, deleteTexture() {}, createRenderbuffer() { return makeObject("renderbuffer"); }, deleteRenderbuffer() {}, createShader() { return makeObject("shader"); }, createProgram() { return makeObject("program"); }, createBuffer() { return makeObject("buffer"); }, texImage2D(target, level, internalFormat, width, height) { if (width * height > this.failTexturePixelsAbove) { events.push("textureAllocationFailed"); throw new Error("Synthetic texture allocation failure."); } }, getParameter(parameter) { if (parameter === this.MAX_TEXTURE_SIZE || parameter === this.MAX_RENDERBUFFER_SIZE) return 16384; if (parameter === this.ALIASED_POINT_SIZE_RANGE) return [1, 64]; return null; }, getShaderPrecisionFormat() { return { precision: 23 }; }, getShaderParameter() { return true; }, getProgramParameter() { return true; }, getShaderInfoLog() { return ""; }, getProgramInfoLog() { return ""; }, getUniformLocation() { return makeObject("uniform"); }, getAttribLocation() { return 0; }, checkFramebufferStatus() { return this.FRAMEBUFFER_COMPLETE; }, getError() { return this.NO_ERROR; }, isContextLost() { return false; }, finish() { events.push("finish"); }, }; let generatedConstant = 0x10000; const gl = new Proxy(glTarget, { get(target, property) { if (property in target) return target[property]; if (typeof property === "string" && /^[A-Z0-9_]+$/.test(property)) { target[property] = generatedConstant++; return target[property]; } if (typeof property === "string") { target[property] = function noop() {}; return target[property]; } return undefined; }, }); const canvas = { width: 300, height: 150, hidden: false, getContext() { return gl; }, addEventListener(type, callback) { addListener(canvasListeners, type, callback); }, }; gl.canvas = canvas; const year = { textContent: "" }; const mediaQuery = { matches: false, addEventListener(type, callback) { if (type === "change") reducedMotionChangeListener = callback; }, addListener(callback) { reducedMotionChangeListener = callback; }, }; const context = { console: { log() {}, warn() {}, error(...args) { consoleErrors.push(args.map(String).join(" ")); }, }, document: { get hidden() { return documentHidden; }, get visibilityState() { return documentHidden ? "hidden" : "visible"; }, get readyState() { return documentReadyState; }, documentElement: { clientWidth: 1280, clientHeight: 720 }, querySelector(selector) { if (selector === "#year") return year; return null; }, getElementById(id) { return id === "sakura" ? canvas : null; }, addEventListener(type, callback) { addListener(documentListeners, type, callback); }, }, navigator: { deviceMemory: 8 }, innerWidth: 1280, innerHeight: 720, devicePixelRatio: 1, performance: { now: () => rafTime }, matchMedia() { return mediaQuery; }, addEventListener(type, callback) { addListener(windowListeners, type, callback); }, setTimeout(callback) { const id = nextTimerId++; timers.set(id, callback); return id; }, clearTimeout(id) { timers.delete(id); }, requestAnimationFrame(callback) { rafQueue.push(callback); return rafQueue.length; }, cancelAnimationFrame() { // Deliberately leave the callback queued to verify generation guards in // browsers where cancellation races with a frame already being dispatched. events.push("cancelAnimationFrame"); }, Date, Math, Number, Object, Array, Float32Array, Uint8Array, isFinite, }; context.window = context; context.globalThis = context; vm.runInNewContext(source, context, { filename: "main.js" }); assert.equal(events.filter((event) => event === "createFramebuffer").length, 0, "WebGL must wait only for the DOM"); documentHidden = false; documentReadyState = "interactive"; emit(documentListeners, "DOMContentLoaded"); assert.equal(consoleErrors.length, 0, "initialization must not log errors"); assert.deepEqual([canvas.width, canvas.height], [1280, 720]); assert.equal(events.filter((event) => event === "createFramebuffer").length, 3); assert.equal(rafQueue.length, 1, "a missed pre-DOM visibility transition must still start animation"); function resize(width, height, dpr, burstCount = 1) { context.innerWidth = width; context.innerHeight = height; context.devicePixelRatio = dpr; context.document.documentElement.clientWidth = width; context.document.documentElement.clientHeight = height; for (let i = 0; i < burstCount; i++) emit(windowListeners, "resize"); assert.equal(timers.size, 1, "resize bursts must collapse to one timer"); flushTimers(); flushAnimationFrames(); } events.length = 0; resize(1366, 768, 1, 25); assert.equal(events.filter((event) => event === "createFramebuffer").length, 3); assert.equal(events.filter((event) => event === "deleteFramebuffer").length, 3); assert.ok(events.indexOf("createFramebuffer") < events.indexOf("deleteFramebuffer")); events.length = 0; resize(3000, 1800, 2); assert.ok(canvas.width * canvas.height <= 10000000, "normal pixel budget must be enforced"); events.length = 0; resize(3200, 1800, 2); assert.equal(events.filter((event) => event === "finish").length, 1); assert.equal(events.filter((event) => event === "deleteFramebuffer").length, 3); assert.equal(events.filter((event) => event === "createFramebuffer").length, 3); assert.ok(events.indexOf("deleteFramebuffer") < events.indexOf("createFramebuffer")); events.length = 0; gl.failTexturePixelsAbove = 5000000; resize(3600, 2000, 2); assert.ok(events.filter((event) => event === "textureAllocationFailed").length >= 2); assert.ok(canvas.width * canvas.height <= 5000000, "failed allocations must fall back to a smaller target"); assert.equal(consoleErrors.length, 0, "successful fallback must not log a terminal error"); events.length = 0; gl.failTexturePixelsAbove = Infinity; let contextLossPrevented = false; documentHidden = true; emit(windowListeners, "pagehide", { persisted: true }); assert.equal(events.filter((event) => event === "cancelAnimationFrame").length, 1); assert.equal(rafQueue.length, 1, "the mock retains the stale frame after pagehide"); documentHidden = false; emit(windowListeners, "pageshow", { persisted: true }); assert.equal(rafQueue.length, 2, "pageshow must queue a replacement frame"); emit(windowListeners, "pageshow", { persisted: true }); assert.equal(rafQueue.length, 3, "a repeated pageshow replaces the pending frame in the mock queue"); flushAnimationFrames(); assert.equal(rafQueue.length, 1, "only the newest pageshow loop may continue"); events.length = 0; mediaQuery.matches = true; reducedMotionChangeListener({ matches: true }); assert.equal(events.filter((event) => event === "cancelAnimationFrame").length, 1); flushAnimationFrames(); assert.equal(rafQueue.length, 0, "reduced motion must leave no live animation callback"); mediaQuery.matches = false; reducedMotionChangeListener({ matches: false }); assert.equal(rafQueue.length, 1, "animation must have one pending frame before context loss"); events.length = 0; emit(canvasListeners, "webglcontextlost", { preventDefault() { contextLossPrevented = true; }, }); emit(canvasListeners, "webglcontextrestored"); assert.equal(contextLossPrevented, true); assert.equal(events.filter((event) => event === "createFramebuffer").length, 3); assert.equal(events.filter((event) => event === "cancelAnimationFrame").length, 1); assert.equal(rafQueue.length, 2, "the mock keeps both the stale and restored callbacks queued"); flushAnimationFrames(); assert.equal(rafQueue.length, 1, "only the restored animation loop may schedule the next frame"); assert.equal(consoleErrors.length, 0, "context restoration must not log errors"); console.log("PASS: cold start, page recovery, Render Target resize, fallback, and context restoration");