// The compositor's drawing, without a browser: it spends only on what changed (UI budget). Run with // `node --test e2e/compose.test.mjs`. import test from "node:test"; import assert from "node:assert/strict"; const refreshes = []; globalThis.requestAnimationFrame = (f) => refreshes.push(f); const refresh = () => refreshes.splice(0).forEach((f) => f()); const { Compositor } = await import("../web/room/compose.js"); const params = { width: 640, height: 360, fps: 30, sample_rate: 48000 }; const fakeWai = { composeDescriptor: () => JSON.stringify({ outcome: "read", params, background: "101218" }) }; function make() { const calls = { images: [], fills: 0 }; const ctx = { fillRect() { calls.fills++; }, drawImage(vf, ...r) { calls.images.push({ who: vf.who, dst: r.slice(4) }); }, set fillStyle(_) {}, }; const canvas = { getContext: (kind, opts) => { calls.opts = opts; return ctx; }, width: 0, height: 0 }; let t = 0; const c = new Compositor(fakeWai, canvas, new Uint8Array(), { now: () => t }); return { c, calls, tick: (ms) => { t += ms; } }; } const frame = (who) => ({ who, displayWidth: 16, displayHeight: 9, closed: false, close() { this.closed = true; } }); const tile = (principal, dst) => ({ principal, tile: dst, src: [0, 0, 16, 9], dst }); const room = (tiles) => ({ frame: () => JSON.stringify({ outcome: "compose", frames: [{ t: 0, set_digest: tiles.map((x) => x.principal).join(), tiles }] }) }); const A = tile("a", [0, 0, 320, 360]); const B = tile("b", [320, 0, 320, 360]); const PIP = tile("pip", [260, 200, 120, 90]); // over both A and B, last in drawing order test("nothing new draws nothing", () => { const { c, calls } = make(); c.relayout(room([A, B]), 0); c.start(); refresh(); const { draws } = c.stats(); assert.equal(draws, 1, "the first draw, of the layout"); assert.equal(calls.fills, 1); for (let i = 0; i < 10; i++) refresh(); assert.equal(c.stats().draws, draws, "no refresh is even asked for"); assert.equal(refreshes.length, 0); }); test("nothing is drawn before it starts", () => { const { c } = make(); c.relayout(room([A]), 0); c.setVideo("a", frame("a")); assert.equal(refreshes.length, 0); c.start(); assert.equal(refreshes.length, 1); refresh(); assert.equal(c.stats().tiles, 1); }); test("a new frame draws its tile alone, not the canvas", () => { const { c, calls } = make(); c.relayout(room([A, B]), 0); c.start(); c.setVideo("a", frame("a")); c.setVideo("b", frame("b")); refresh(); calls.images.length = 0; const fills = calls.fills; c.setVideo("b", frame("b")); refresh(); assert.deepEqual(calls.images.map((d) => d.who), ["b"]); assert.equal(calls.fills, fills, "the background is not painted again"); }); test("frames between refreshes draw once, the newest, and the others are released", () => { const { c, calls } = make(); c.relayout(room([A]), 0); c.start(); const first = frame("a"); c.setVideo("a", first); c.setVideo("a", frame("a")); assert.equal(refreshes.length, 1, "one draw asked for"); assert.equal(first.closed, true); refresh(); assert.equal(calls.images.length, 1); const s = c.stats(); assert.equal(s.frames, 2); assert.ok(s.tiles <= s.frames, `${s.tiles} tiles from ${s.frames} frames`); }); test("a tile drawn again brings back what lay over it", () => { const { c, calls } = make(); c.relayout(room([A, B, PIP]), 0); c.start(); for (const who of ["a", "b", "pip"]) c.setVideo(who, frame(who)); refresh(); calls.images.length = 0; c.setVideo("a", frame("a")); refresh(); assert.deepEqual(calls.images.map((d) => d.who), ["a", "pip"], "the window over it, in drawing order; not the tile it doesn't touch"); calls.images.length = 0; c.setVideo("pip", frame("pip")); refresh(); assert.deepEqual(calls.images.map((d) => d.who), ["pip"], "nothing lies over the window"); }); test("tiles that share a column or a row but don't touch are left alone", () => { const { c, calls } = make(); const [TL, BL, TR] = [tile("tl", [0, 0, 320, 180]), tile("bl", [0, 180, 320, 180]), tile("tr", [320, 0, 320, 180])]; c.relayout(room([TL, BL, TR]), 0); c.start(); for (const who of ["tl", "bl", "tr"]) c.setVideo(who, frame(who)); refresh(); calls.images.length = 0; c.setVideo("tl", frame("tl")); refresh(); assert.deepEqual(calls.images.map((d) => d.who), ["tl"]); }); test("a re-layout draws the whole canvas", () => { const { c, calls } = make(); c.relayout(room([A, B]), 0); c.start(); c.setVideo("a", frame("a")); c.setVideo("b", frame("b")); refresh(); calls.images.length = 0; const fills = calls.fills; c.relayout(room([tile("a", [0, 0, 640, 360])]), 1); refresh(); assert.equal(calls.fills, fills + 1); assert.deepEqual(calls.images.map((d) => d.who), ["a"]); }); test("the canvas asks for no software rendering", () => { const { calls } = make(); assert.ok(!calls.opts || !calls.opts.willReadFrequently, "willReadFrequently is a software canvas, for a test's readback alone"); }); test("arrival to draw is reported from when the frame arrived", () => { const { c, tick } = make(); c.relayout(room([A]), 0); c.start(); c.setVideo("a", frame("a")); tick(16); refresh(); assert.equal(c.stats().arrival_to_draw_p95_ms, 16); });