113 lines
3.6 KiB
TypeScript
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');
|
|
});
|
|
});
|