UNPKG

@spearwolf/twopoint5d

Version:

Create 2.5D realtime graphics and pixelart with WebGL and three.js

176 lines 6.65 kB
import { describe, expect, it } from 'vitest'; import { FrameLoop } from './FrameLoop.js'; function makeFakeRenderer() { return { callback: null, setAnimationLoop(cb) { this.callback = cb; }, tick(now) { this.callback(now); }, }; } function subscribe(loop) { const events = []; const target = { [FrameLoop.OnFrame](props) { events.push(props); }, }; loop.start(target); return { events, target }; } describe('FrameLoop', () => { it('first emitted frame has deltaTime === 0, not NaN', () => { const renderer = makeFakeRenderer(); const loop = new FrameLoop(0, renderer); const { events } = subscribe(loop); renderer.tick(1000); expect(events).toHaveLength(1); expect(Number.isNaN(events[0].deltaTime)).toBe(false); expect(events[0].deltaTime).toBe(0); }); it('emits a single OnFrame per rAF tick with monotonically increasing frameNo', () => { const renderer = makeFakeRenderer(); const loop = new FrameLoop(0, renderer); const { events } = subscribe(loop); renderer.tick(1000); renderer.tick(1016); renderer.tick(1032); expect(events.map((e) => e.frameNo)).toEqual([1, 2, 3]); }); it('lastNow in emitted props reflects the previous frame timestamp (not the current one)', () => { const renderer = makeFakeRenderer(); const loop = new FrameLoop(0, renderer); const { events } = subscribe(loop); renderer.tick(1000); renderer.tick(1016); expect(events[0].now).toBe(1); expect(events[0].lastNow).toBe(1); expect(events[1].now).toBeCloseTo(1.016); expect(events[1].lastNow).toBe(1); expect(events[1].deltaTime).toBeCloseTo(0.016); }); it('measuredFps is 0 until the first measurement window completes', () => { const renderer = makeFakeRenderer(); const loop = new FrameLoop(0, renderer); const { events } = subscribe(loop); for (let i = 0; i < 10; i++) { renderer.tick(1000 + i * 16); } for (const evt of events) { expect(evt.measuredFps).toBe(0); } }); it('measuredFps produces a plausible value once the first window completes', () => { const renderer = makeFakeRenderer(); const loop = new FrameLoop(0, renderer); const { events } = subscribe(loop); for (let i = 0; i < 31; i++) { renderer.tick(1000 + i * (1000 / 60)); } expect(events).toHaveLength(31); expect(events[30].measuredFps).toBe(60); }); it('maxFps throttles emissions to the target rate', () => { const renderer = makeFakeRenderer(); const loop = new FrameLoop(30, renderer); const { events } = subscribe(loop); renderer.tick(0); renderer.tick(16); renderer.tick(33); renderer.tick(50); renderer.tick(66); expect(events).toHaveLength(3); }); it('maxFps grid stays stable across many frames (no drift)', () => { const renderer = makeFakeRenderer(); const loop = new FrameLoop(60, renderer); const { events } = subscribe(loop); const VSYNC = 1000 / 240; for (let i = 0; i < 41; i++) { renderer.tick(i * VSYNC); } expect(events.length).toBeGreaterThanOrEqual(10); expect(events.length).toBeLessThanOrEqual(12); const targetDelta = 1 / 60; for (let i = 1; i < events.length; i++) { expect(events[i].deltaTime).toBeGreaterThan(targetDelta * 0.95); expect(events[i].deltaTime).toBeLessThan(targetDelta * 1.1); } }); it('maxFps tolerates rAF ticks arriving slightly early (jitter tolerance)', () => { const renderer = makeFakeRenderer(); const loop = new FrameLoop(60, renderer); const { events } = subscribe(loop); renderer.tick(0); renderer.tick(16.5); renderer.tick(33.0); expect(events).toHaveLength(3); }); it('after a long pause the schedule snaps forward — no catch-up burst', () => { const renderer = makeFakeRenderer(); const loop = new FrameLoop(30, renderer); const { events } = subscribe(loop); renderer.tick(0); renderer.tick(1000); expect(events).toHaveLength(2); renderer.tick(1001); expect(events).toHaveLength(2); renderer.tick(1034); expect(events).toHaveLength(3); }); it('setFps() resets the schedule mid-loop', () => { const renderer = makeFakeRenderer(); const loop = new FrameLoop(30, renderer); const { events } = subscribe(loop); renderer.tick(0); renderer.tick(33); expect(events).toHaveLength(2); loop.setFps(120); renderer.tick(40); renderer.tick(45); renderer.tick(49); expect(events).toHaveLength(4); }); it('subscriptionCount tracks start()/stop() idempotently', () => { const renderer = makeFakeRenderer(); const loop = new FrameLoop(0, renderer); const a = { [FrameLoop.OnFrame]() { } }; const b = { [FrameLoop.OnFrame]() { } }; expect(loop.subscriptionCount).toBe(0); loop.start(a); expect(loop.subscriptionCount).toBe(1); loop.start(a); expect(loop.subscriptionCount).toBe(1); loop.start(b); expect(loop.subscriptionCount).toBe(2); loop.stop(a); expect(loop.subscriptionCount).toBe(1); loop.stop(a); expect(loop.subscriptionCount).toBe(1); loop.stop(b); expect(loop.subscriptionCount).toBe(0); }); it('start() returns an unsubscribe function', () => { const renderer = makeFakeRenderer(); const loop = new FrameLoop(0, renderer); const target = { [FrameLoop.OnFrame]() { } }; const unsubscribe = loop.start(target); expect(loop.subscriptionCount).toBe(1); expect(typeof unsubscribe).toBe('function'); unsubscribe(); expect(loop.subscriptionCount).toBe(0); }); it('clear() removes all subscribers', () => { const renderer = makeFakeRenderer(); const loop = new FrameLoop(0, renderer); loop.start({ [FrameLoop.OnFrame]() { } }); loop.start({ [FrameLoop.OnFrame]() { } }); expect(loop.subscriptionCount).toBe(2); loop.clear(); expect(loop.subscriptionCount).toBe(0); }); }); //# sourceMappingURL=FrameLoop.spec.js.map