import assert from "node:assert/strict"; import { fileURLToPath } from "node:url"; import { test } from "node:test"; import { build } from "esbuild"; import { parseHTML } from "linkedom"; const { window, document } = parseHTML(""); Object.assign(globalThis, { window, document }); globalThis.requestAnimationFrame = (cb) => setTimeout(() => cb(performance.now()), 4); const compiled = await build({ stdin: { contents: `export * from "./python"; export * from "./atl"; export * from "./colormatrix"; export * from "./pymodules";`, resolveDir: fileURLToPath(new URL("../src/player/", import.meta.url)), loader: "ts" }, bundle: true, write: false, format: "esm", platform: "node", logLevel: "silent", }); const m = await import(`data:text/javascript;base64,${Buffer.from(compiled.outputFiles[0].text).toString("base64")}`); const { Python, AtlRunner, ColorMatrix, colorMatrices, legacyMatrices, matrixFilter, pythonModules, PyDict } = m; const wait = (ms) => new Promise((r) => setTimeout(r, ms)); const name = (id) => ({ k: "name", id }); const attr = (o, a) => ({ k: "attr", o, a }); const call = (f, ...a) => ({ k: "call", f, a: a.map((v) => (v && v.k ? v : { k: "const", v })), kw: {} }); function python(extra = {}) { const store = {}; const builtins = { ...pythonModules(), ...colorMatrices, ...extra }; const py = new Python({ get: (n) => (n in store ? store[n] : builtins[n]), has: (n) => n in store || n in builtins, set: (n, v) => { store[n] = v; }, del: (n) => { delete store[n]; }, }); return { py, store }; } test("colour matrices compose like Ren'Py's: the right one applies first", () => { const brighter = colorMatrices.BrightnessMatrix(0.2); const half = colorMatrices.TintMatrix("#808080"); const both = half.times(brighter); // brighten, then tint assert.ok(Math.abs(both.rows[0][4] - 0.2 * (128 / 255)) < 1e-6); assert.deepEqual(ColorMatrix.identity().times(brighter).rows, brighter.rows); const gray = colorMatrices.SaturationMatrix(0); assert.ok(Math.abs(gray.rows[0][0] - 0.2126) < 1e-9 && Math.abs(gray.rows[1][2] - 0.0722) < 1e-9); assert.equal(legacyMatrices.tint(1, 0.5, 0).rows[1][1], 0.5); assert.match(matrixFilter(both), /^url\(#pgm-\d+\)$/); assert.equal(matrixFilter(both), matrixFilter(both), "equal matrices share one SVG filter"); assert.equal(document.querySelectorAll("feColorMatrix").length, 1); }); test("the sandbox multiplies matrices with `*` and refuses other arithmetic on them", () => { const { py, store } = python(); py.exec([{ op: "set", t: name("m"), v: { k: "bin", op: "*", l: call(name("TintMatrix"), "#ff0000"), r: call(name("BrightnessMatrix"), 0.1) } }]); assert.ok(store.m instanceof ColorMatrix); assert.throws(() => py.eval({ k: "bin", op: "+", l: name("m"), r: name("m") }), /перемножать/); }); test("re, copy, defaultdict, datetime and string helpers behave like their Python versions", () => { const { py, store } = python(); const re = (fn, ...args) => py.eval(call(attr(name("re"), fn), ...args)); assert.equal(re("sub", "(\\d+)", "<\\1>", "a12b3"), "a<12>b<3>"); assert.equal(re("match", "^ab", "abc").group(), "ab"); assert.equal(re("search", "(?P\\d+)", "x42").group("n"), "42"); assert.deepEqual(re("findall", "\\d", "a1b2"), ["1", "2"]); const original = new PyDict([["k", [1, 2]]]); store.original = original; const copied = py.eval(call(attr(name("copy"), "deepcopy"), name("original"))); copied.get("k").push(3); assert.deepEqual(original.get("k"), [1, 2]); store.counts = py.eval(call(attr(name("collections"), "defaultdict"), { k: "name", id: "list" })); py.scope.set("list", () => []); store.counts = py.eval(call(attr(name("collections"), "defaultdict"), name("list"))); py.eval(call(attr({ k: "sub", o: name("counts"), i: { k: "const", v: "a" } }, "append"), 1)); assert.deepEqual(store.counts.get("a"), [1]); assert.equal(py.eval(call(attr({ k: "const", v: "7" }, "zfill"), 3)), "007"); assert.deepEqual(py.eval(call(attr({ k: "const", v: "a=b" }, "partition"), "=")), ["a", "=", "b"]); const hour = py.eval(attr(call(attr(name("datetime"), "now")), "hour")); assert.ok(hour >= 0 && hour < 24); }); test("ATL image steps, finite repeat, time and function work in a timeline", async () => { const shown = []; const props = {}; const { py } = python(); py.scope.set("drift", (trans) => { trans.xpos = 0.75; return null; }); const host = { block: () => null, props: () => null, apply: () => {}, eval: (expr) => py.eval(expr), image: (img, transition) => shown.push([img.file, transition?.d]), }; const runner = new AtlRunner(host, props); const started = performance.now(); const completed = await runner.start({ repeat: 2, steps: [ { op: "image", img: { file: "a.webp" }, with: { t: "dissolve", d: 0.2 } }, { op: "pause", t: 0.02 }, { op: "image", img: { file: "b.webp" }, with: null }, { op: "time", t: 0.05 }, ] }); assert.equal(completed, true); assert.deepEqual(shown, [["a.webp", 0.2], ["b.webp", undefined], ["a.webp", 0.2], ["b.webp", undefined]]); assert.ok(performance.now() - started >= 95, "`time 0.05` waits for the block clock, twice"); await new AtlRunner(host, props).start({ repeat: null, steps: [{ op: "function", f: { k: "name", id: "drift" } }] }); assert.equal(props.xpos, 0.75, "function f mutates the transform properties"); }); test("an animation stops by itself when its element leaves the page", async () => { const box = document.createElement("div"); document.body.append(box); let frames = 0; const runner = new AtlRunner({ block: () => null, props: () => null, apply: () => { frames++; } }, {}); runner.watch = box; const running = runner.start({ repeat: true, steps: [{ op: "move", warp: "linear", t: 0.02, props: { alpha: 0 } }] }); await wait(40); box.remove(); assert.equal(await running, false, "stopped, not finished"); const seen = frames; await wait(40); assert.equal(frames, seen); }); test("on hover handlers run when the button reports the event", async () => { const props = { alpha: 1 }; const runner = new AtlRunner({ block: () => null, props: () => null, apply: () => {} }, props); const block = { repeat: null, steps: [ { op: "on", events: ["hover"], block: { repeat: null, steps: [{ op: "set", props: { alpha: 0.5 } }] } }, { op: "on", events: ["idle"], block: { repeat: null, steps: [{ op: "set", props: { alpha: 1 } }] } }, ] }; runner.event(block, "hover"); await wait(20); assert.equal(props.alpha, 0.5); runner.event(block, "idle"); await wait(20); assert.equal(props.alpha, 1); });