mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
203 lines
5.8 KiB
JavaScript
203 lines
5.8 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { createPlaybackClock } from './clock.js';
|
|
|
|
const scene = { id: 'scene', shots: [{ id: 'shot' }] };
|
|
const shot = scene.shots[0];
|
|
const timing = {
|
|
shotIndex: 0,
|
|
totalSec: 4,
|
|
durationSec: 4,
|
|
startElapsedSec: 0,
|
|
};
|
|
function fixture() {
|
|
const timers = new Map();
|
|
let id = 0,
|
|
now = 0,
|
|
running = true;
|
|
const progress = [];
|
|
const clock = createPlaybackClock({
|
|
isRunning: () => running,
|
|
timingForShot: () => timing,
|
|
onProgress: (value) => progress.push(value),
|
|
now: () => now,
|
|
schedule: (callback) => {
|
|
timers.set(++id, callback);
|
|
return id;
|
|
},
|
|
cancel: (handle) => timers.delete(handle),
|
|
});
|
|
return {
|
|
clock,
|
|
timers,
|
|
progress,
|
|
time: (value) => {
|
|
now = value;
|
|
},
|
|
running: (value) => {
|
|
running = value;
|
|
},
|
|
};
|
|
}
|
|
|
|
test('Stop releases every clock timer and queued callbacks cannot publish later', () => {
|
|
const f = fixture();
|
|
const observed = [];
|
|
f.clock.subscribe((snapshot) => observed.push(snapshot));
|
|
f.clock.startRunProgress(4);
|
|
f.clock.startScene(scene, shot, { cancelled: false });
|
|
f.time(2000);
|
|
for (const tick of f.timers.values()) tick();
|
|
assert.equal(f.clock.snapshot.sceneElapsedSec, 2);
|
|
const stale = [...f.timers.values()];
|
|
f.clock.stop();
|
|
assert.equal(f.clock.activeTimers, 0);
|
|
assert.equal(f.timers.size, 0);
|
|
assert.equal(f.clock.snapshot.stopped, true);
|
|
const count = observed.length,
|
|
progressCount = f.progress.length;
|
|
f.time(3000);
|
|
for (const tick of stale) tick();
|
|
assert.equal(observed.length, count);
|
|
assert.equal(f.progress.length, progressCount);
|
|
});
|
|
|
|
test('replacing a shot clock rejects the old callback without clearing the replacement', () => {
|
|
const f = fixture();
|
|
f.clock.startScene(scene, shot, {});
|
|
const stale = [...f.timers.values()][0];
|
|
f.time(1000);
|
|
f.clock.startScene(scene, shot, {});
|
|
f.time(2000);
|
|
stale();
|
|
assert.equal(f.clock.snapshot.sceneElapsedSec, 0);
|
|
assert.equal(f.clock.activeTimers, 1);
|
|
[...f.timers.values()][0]();
|
|
assert.equal(f.clock.snapshot.sceneElapsedSec, 1);
|
|
f.clock.destroy();
|
|
});
|
|
|
|
test('direct-load progress finishes once and snapshots cannot mutate the clock', () => {
|
|
const f = fixture();
|
|
f.running(false);
|
|
f.clock.startShotProgress({}, 2, 0.25, 0.75, {
|
|
scene,
|
|
shot,
|
|
sceneElapsedFrom: 1,
|
|
sceneElapsedTo: 3,
|
|
});
|
|
const tick = [...f.timers.values()][0];
|
|
f.time(1000);
|
|
tick();
|
|
assert.equal(f.progress.at(-1), 0.5);
|
|
assert.equal(f.clock.snapshot.sceneElapsedSec, 2);
|
|
const copy = f.clock.snapshot;
|
|
copy.sceneElapsedSec = 99;
|
|
assert.equal(f.clock.snapshot.sceneElapsedSec, 2);
|
|
f.time(2000);
|
|
tick();
|
|
assert.equal(f.progress.at(-1), 0.75);
|
|
assert.equal(f.clock.activeTimers, 0);
|
|
const count = f.progress.length;
|
|
tick();
|
|
assert.equal(f.progress.length, count);
|
|
});
|
|
|
|
test('aborted starts and destruction during initial notification leave no timers or listeners', () => {
|
|
const f = fixture();
|
|
const controller = new AbortController();
|
|
controller.abort();
|
|
f.clock.startScene(scene, shot, { signal: controller.signal });
|
|
assert.equal(f.timers.size, 0);
|
|
let calls = 0;
|
|
f.clock.subscribe(() => {
|
|
calls++;
|
|
f.clock.destroy();
|
|
});
|
|
f.clock.startScene(scene, shot, {});
|
|
assert.equal(calls, 1);
|
|
assert.equal(f.clock.activeTimers, 0);
|
|
f.clock.startRunProgress(4);
|
|
f.clock.publish(scene, shot, 1);
|
|
f.clock.subscribe(() => calls++);
|
|
assert.equal(calls, 1);
|
|
assert.equal(f.timers.size, 0);
|
|
});
|
|
|
|
test('Stop, abort and destroy settle long holds immediately and release their deadlines', async () => {
|
|
for (const action of ['stop', 'abort', 'destroy']) {
|
|
const clock = createPlaybackClock({
|
|
isRunning: () => true,
|
|
timingForShot: () => timing,
|
|
onProgress() {},
|
|
});
|
|
const abort = new AbortController();
|
|
const wait = clock.wait(60000, { signal: abort.signal });
|
|
assert.equal(clock.activeTimers, 1);
|
|
if (action === 'abort') abort.abort();
|
|
else clock[action]();
|
|
await wait;
|
|
assert.equal(clock.activeTimers, 0);
|
|
clock.destroy();
|
|
}
|
|
});
|
|
|
|
test('a subscriber can Stop or replace the initial clock without the old start acquiring a timer', () => {
|
|
for (const action of ['stop', 'replace']) {
|
|
const f = fixture();
|
|
let first = true;
|
|
f.clock.subscribe(() => {
|
|
if (!first) return;
|
|
first = false;
|
|
if (action === 'stop') f.clock.stop();
|
|
else f.clock.startScene(scene, { id: 'replacement' }, {});
|
|
});
|
|
f.clock.startScene(scene, shot, {});
|
|
assert.equal(f.timers.size, action === 'stop' ? 0 : 1);
|
|
assert.equal(
|
|
f.clock.snapshot.shotId,
|
|
action === 'stop' ? shot.id : 'replacement',
|
|
);
|
|
f.clock.destroy();
|
|
}
|
|
const f = fixture();
|
|
f.running(false);
|
|
f.clock.subscribe(() => f.clock.stop());
|
|
f.clock.startShotProgress({}, 10, 0, 1, {
|
|
scene,
|
|
shot,
|
|
sceneElapsedFrom: 0,
|
|
sceneElapsedTo: 4,
|
|
});
|
|
assert.equal(f.timers.size, 0);
|
|
});
|
|
|
|
test('a subscriber throwing on immediate snapshot does not crash subscribe and receives unsubscribe handle', () => {
|
|
const f = fixture();
|
|
f.clock.publish(scene, shot, 1);
|
|
let calls = 0;
|
|
const warnings = [];
|
|
const origWarn = console.warn;
|
|
console.warn = (...args) => warnings.push(args);
|
|
try {
|
|
const unsubscribe = f.clock.subscribe(() => {
|
|
calls++;
|
|
throw new Error('listener failure');
|
|
});
|
|
assert.equal(typeof unsubscribe, 'function');
|
|
assert.equal(calls, 1);
|
|
assert.equal(warnings.length, 1);
|
|
assert.equal(warnings[0][0], '[Scenes] Scene clock listener failed:');
|
|
f.clock.publish(scene, shot, 2);
|
|
assert.equal(calls, 2);
|
|
assert.equal(warnings.length, 2);
|
|
unsubscribe();
|
|
f.clock.publish(scene, shot, 3);
|
|
assert.equal(calls, 2);
|
|
assert.equal(warnings.length, 2);
|
|
f.clock.destroy();
|
|
} finally {
|
|
console.warn = origWarn;
|
|
}
|
|
});
|