91 lines
3.1 KiB
JavaScript
91 lines
3.1 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import { mkdtemp, rm } from 'node:fs/promises';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
import { build } from 'esbuild';
|
|
|
|
// Pure scheduler verification. No Electron, DOM interaction, backend, or model.
|
|
const root = fileURLToPath(new URL('../', import.meta.url));
|
|
const directory = await mkdtemp(path.join(os.tmpdir(), 'prospect-particle-loop-'));
|
|
try {
|
|
const output = path.join(directory, 'particle-loop.mjs');
|
|
await build({ entryPoints: [path.join(root, 'renderer/components/particleLoop.ts')],
|
|
platform: 'node', bundle: true, format: 'esm', outfile: output });
|
|
const { createParticleLoop } = await import(pathToFileURL(output).href);
|
|
const queued = new Map();
|
|
let handle = 0;
|
|
let render = true;
|
|
let animate = true;
|
|
const draws = [];
|
|
const loop = createParticleLoop({
|
|
draw: (elapsed, still) => draws.push({ elapsed, still }),
|
|
canRender: () => render,
|
|
canAnimate: () => animate,
|
|
requestFrame: callback => { const id = ++handle; queued.set(id, callback); return id; },
|
|
cancelFrame: id => queued.delete(id),
|
|
});
|
|
const step = time => {
|
|
const entry = queued.entries().next().value;
|
|
assert.ok(entry, 'one frame should be scheduled');
|
|
queued.delete(entry[0]);
|
|
entry[1](time);
|
|
};
|
|
const checks = [];
|
|
|
|
loop.sync();
|
|
assert.equal(queued.size, 1);
|
|
checks.push('active starts exactly one scheduled frame');
|
|
|
|
step(0);
|
|
const first = draws.length;
|
|
step(16); assert.equal(draws.length, first);
|
|
step(34); assert.equal(draws.length, first + 1);
|
|
step(50); assert.equal(draws.length, first + 1);
|
|
step(68); assert.equal(draws.length, first + 2);
|
|
checks.push('draws are capped at 30 fps');
|
|
|
|
for (let index = 0; index < 5; index += 1) loop.sync();
|
|
assert.equal(queued.size, 1);
|
|
checks.push('repeated state sync never duplicates the animation loop');
|
|
|
|
animate = false;
|
|
loop.sync();
|
|
assert.equal(queued.size, 0);
|
|
assert.equal(draws.at(-1).still, true);
|
|
checks.push('paused or reduced motion leaves a static frame and no scheduled work');
|
|
|
|
render = false;
|
|
const before = draws.length;
|
|
loop.sync();
|
|
assert.equal(queued.size, 0);
|
|
assert.equal(draws.length, before);
|
|
checks.push('hidden or offscreen canvas does not draw');
|
|
|
|
render = true;
|
|
loop.sync();
|
|
assert.equal(draws.at(-1).still, true);
|
|
assert.equal(queued.size, 0);
|
|
checks.push('visible reduced-motion frame is restored without starting work');
|
|
|
|
animate = true;
|
|
loop.sync();
|
|
const elapsed = draws.at(-1).elapsed;
|
|
step(100000); assert.equal(draws.at(-1).elapsed, elapsed);
|
|
step(100034); assert.equal(draws.at(-1).elapsed, elapsed + 34);
|
|
checks.push('resume retains elapsed motion without a hidden-time jump');
|
|
|
|
loop.dispose();
|
|
assert.equal(queued.size, 0);
|
|
const end = draws.length;
|
|
loop.sync();
|
|
assert.equal(draws.length, end);
|
|
assert.equal(queued.size, 0);
|
|
checks.push('dispose cancels work permanently');
|
|
|
|
process.stdout.write(JSON.stringify({ kind: 'pure scheduler unit check', passed: checks.length,
|
|
checks, externalActions: 0 }, null, 2) + '\n');
|
|
} finally {
|
|
await rm(directory, { recursive: true, force: true });
|
|
}
|