import assert from "node:assert/strict"; import { fileURLToPath } from "node:url"; import { test } from "node:test"; import { build } from "esbuild"; globalThis.requestAnimationFrame = (cb) => setTimeout(() => cb(performance.now()), 4); const compiled = await build({ entryPoints: [fileURLToPath(new URL("../src/player/atl.ts", import.meta.url))], bundle: true, write: false, format: "esm", platform: "node", logLevel: "silent" }); const { AtlRunner } = await import(`data:text/javascript;base64,${Buffer.from(compiled.outputFiles[0].text).toString("base64")}`); const wait = (ms) => new Promise((r) => setTimeout(r, ms)); const block = (steps, repeat = null) => ({ steps, repeat }); const set = (props) => ({ op: "set", props }); const move = (t, props, warp = "linear") => ({ op: "move", warp, t, props }); function runner(named = {}) { const seen = []; const host = { block: (n) => named[n] ?? null, props: () => null, apply: (p) => seen.push({ ...p }), }; const props = { xpos: 0.5, xanchor: 0.5 }; return { run: new AtlRunner(host, props), props, seen }; } test("a linear move interpolates from the current value to the target", async () => { const { run, props, seen } = runner(); await run.start(block([set({ xalign: 0 }), move(0.12, { xalign: 1 })])); assert.equal(props.xalign, 1); const values = seen.map((p) => p.xalign); assert.ok(values.some((v) => v > 0 && v < 1), "has intermediate frames"); assert.deepEqual(values, [...values].sort((a, b) => a - b), "monotonic"); }); test("a move starts from the value implied by xpos when xalign was never set", async () => { const { run, seen } = runner(); await run.start(block([move(0.05, { xalign: 1 })])); assert.ok(seen[0].xalign > 0.4, `first frame ${seen[0].xalign} starts near xpos 0.5`); }); test("tuples move component-wise and parallel blocks run together", async () => { const { run, props } = runner(); const started = performance.now(); await run.start(block([{ op: "parallel", blocks: [ block([move(0.1, { pos: [1, 1] })]), block([move(0.1, { alpha: 0 })]), ] }])); assert.deepEqual(props.pos, [1, 1]); assert.equal(props.alpha, 0); assert.ok(performance.now() - started < 250, "blocks overlapped instead of running one after another"); }); test("repeat loops until stopped, even when the body takes no time", async () => { const { run, seen } = runner(); const done = run.start(block([set({ alpha: 0.5 })], true)); // would freeze the page without the frame yield await wait(60); run.stop(); await done; assert.ok(seen.length > 1 && seen.length < 200); const counted = runner(); const finished = counted.run.start(block([{ op: "pause", t: 0.01 }, set({ zoom: 2 })], 3)); await finished; assert.equal(counted.seen.length, 3); }); test("named transforms, choice and block steps are followed", async () => { const named = { grow: block([move(0.03, { zoom: 2 })]) }; const { run, props } = runner(named); await run.start(block([{ op: "use", name: "grow" }, { op: "choice", blocks: [{ w: 1, block: block([set({ alpha: 0.25 })]) }] }, { op: "block", block: block([set({ yoffset: 5 })]) }])); assert.equal(props.zoom, 2); assert.equal(props.alpha, 0.25); assert.equal(props.yoffset, 5); }); test("on show runs at once and `on hide` runs only when the image leaves", async () => { const { run, props } = runner(); const timeline = block([ { op: "on", events: ["show"], block: block([set({ alpha: 1 })]) }, { op: "on", events: ["hide"], block: block([move(0.05, { alpha: 0 })]) }, ]); void run.start(timeline); await wait(30); assert.equal(props.alpha, 1); assert.equal(run.hasHideHandler(timeline), true); await run.hide(timeline); assert.equal(props.alpha, 0); }); test("excessive nesting is refused instead of overflowing the stack", async () => { const { run } = runner({ loop: { steps: [{ op: "use", name: "loop" }], repeat: null } }); await assert.rejects(run.start(block([{ op: "use", name: "loop" }])), /вложенность/); });