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'); }); });