Files
Portfolio/tests/unit/scene-resources.test.ts

113 lines
3.6 KiB
TypeScript

import { describe, expect, it, vi } from 'vitest';
import { SceneResourceBag } from '@/scene/resources';
import { renderQualityFor } from '@/scene/render-quality';
import { routeLabelFor } from '@/scene/world/sign';
describe('SceneResourceBag', () => {
it.each([
'geometry',
'material',
'texture',
'target',
'pass',
'renderer',
] as const)('disposes an owned %s exactly once', (kind) => {
const dispose = vi.fn();
const bag = new SceneResourceBag();
bag.own(kind, { dispose });
bag.dispose();
bag.dispose();
expect(dispose).toHaveBeenCalledTimes(1);
expect(bag.snapshot().disposed).toBe(true);
expect(bag.snapshot().owned[kind]).toBe(0);
});
it('deduplicates the same disposable across ownership categories', () => {
const dispose = vi.fn();
const resource = { dispose };
const bag = new SceneResourceBag();
bag.own('geometry', resource);
bag.own('material', resource);
bag.dispose();
expect(dispose).toHaveBeenCalledTimes(1);
});
it('immediately disposes a resource registered after terminal disposal', () => {
const dispose = vi.fn();
const bag = new SceneResourceBag();
bag.dispose();
bag.own('texture', { dispose });
expect(dispose).toHaveBeenCalledTimes(1);
});
it('removes listeners, disconnects observers, and clears timers and RAF exactly once', () => {
const target = new EventTarget();
const listener = vi.fn();
const observer = { disconnect: vi.fn() };
const clearTimer = vi.fn();
const cancelFrame = vi.fn();
const bag = new SceneResourceBag();
bag.listen(target, 'ping', listener);
bag.observe(observer);
bag.timer(7, clearTimer);
bag.animationFrame(9, cancelFrame);
target.dispatchEvent(new Event('ping'));
bag.dispose();
bag.dispose();
target.dispatchEvent(new Event('ping'));
expect(listener).toHaveBeenCalledTimes(1);
expect(observer.disconnect).toHaveBeenCalledTimes(1);
expect(clearTimer).toHaveBeenCalledTimes(1);
expect(clearTimer).toHaveBeenCalledWith(7);
expect(cancelFrame).toHaveBeenCalledTimes(1);
expect(cancelFrame).toHaveBeenCalledWith(9);
});
it('replaces a tracked RAF without allowing more than one outstanding frame', () => {
const cancelFrame = vi.fn();
const bag = new SceneResourceBag();
bag.animationFrame(1, cancelFrame);
bag.animationFrame(2, cancelFrame);
expect(cancelFrame).toHaveBeenCalledOnce();
expect(cancelFrame).toHaveBeenCalledWith(1);
expect(bag.snapshot().rafCount).toBe(1);
bag.releaseAnimationFrame(2);
expect(bag.snapshot().rafCount).toBe(0);
bag.dispose();
expect(cancelFrame).toHaveBeenCalledOnce();
});
});
describe('scene render quality', () => {
it('reduces GPU work monotonically across live tiers', () => {
const high = renderQualityFor('high', 3, false);
const medium = renderQualityFor('medium', 3, false);
const low = renderQualityFor('low', 3, false);
expect(high).toEqual({ pixelRatio: 2, terrainSegments: 96, particleCount: 384, postEffects: true });
expect(medium).toEqual({ pixelRatio: 1.5, terrainSegments: 64, particleCount: 192, postEffects: true });
expect(low).toEqual({ pixelRatio: 1.25, terrainSegments: 32, particleCount: 64, postEffects: false });
});
it('caps mobile and tablet pixel ratio at 1.5 even for High', () => {
expect(renderQualityFor('high', 3, true).pixelRatio).toBe(1.5);
});
it('keeps route sign labels canonical and compact', () => {
expect(routeLabelFor('start')).toBe('00 / START');
expect(routeLabelFor('infrastructure')).toBe('03 / INFRASTRUCTURE');
expect(routeLabelFor('contact')).toBe('07 / DESTINATION');
});
});