// @ts-nocheck /** * tests/unit/foundry/FoundryAdapter.test.js * * Story 1.2 — WebRTC spike: FoundryAdapter probe tests. * * OQ-1 spike result: css-fallback (FoundryVTT v14, 2026-05-21) * track.enabled = false does NOT stop inbound WebRTC bandwidth. * Probe returns 'css-fallback' when AVClient is available, 'unsupported' otherwise. */ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { FoundryAdapter } from '../../../src/foundry/FoundryAdapter.js'; import { createFoundryAdapterMock } from '../../helpers/foundryAdapterMock.js'; // ─── Helpers ───────────────────────────────────────────────────────────────── /** * Returns a minimal mock game.webrtc object whose client exposes getMediaStreamForUser. * @param {MediaStream|null} [stream=null] * @returns {{ client: { getMediaStreamForUser: import('vitest').MockedFunction<() => MediaStream|null> } }} */ function makeGameWebrtc(stream = null) { return { client: { getMediaStreamForUser: vi.fn().mockReturnValue(stream), }, }; } /** * Returns a minimal MediaStream mock with one video track. * @param {boolean} [enabled=true] * @returns {{ getVideoTracks: import('vitest').MockedFunction<() => object[]>, _track: { enabled: boolean, kind: string } }} */ function makeStream(enabled = true) { const track = { enabled, kind: 'video' }; return { getVideoTracks: vi.fn().mockReturnValue([track]), _track: track, }; } // ─── probeCapability ───────────────────────────────────────────────────────── describe('FoundryAdapter.probeCapability', () => { it('returns "unsupported" when gameWebrtc is null', () => { expect(FoundryAdapter.probeCapability(null)).toBe('unsupported'); }); it('returns "unsupported" when gameWebrtc is undefined', () => { expect(FoundryAdapter.probeCapability(undefined)).toBe('unsupported'); }); it('returns "unsupported" when gameWebrtc has no client', () => { expect(FoundryAdapter.probeCapability({})).toBe('unsupported'); }); it('returns "unsupported" when client lacks getMediaStreamForUser', () => { expect(FoundryAdapter.probeCapability({ client: {} })).toBe('unsupported'); }); it('returns "css-fallback" when client has getMediaStreamForUser (OQ-1 spike result)', () => { // track.enabled = false does NOT stop bandwidth → probe returns css-fallback, not track-disable const gameWebrtc = makeGameWebrtc(); expect(FoundryAdapter.probeCapability(gameWebrtc)).toBe('css-fallback'); }); }); // ─── buildWebRTCSurface ─────────────────────────────────────────────────────── describe('FoundryAdapter.buildWebRTCSurface', () => { let consoleWarnSpy; let consoleErrorSpy; beforeEach(() => { consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); }); afterEach(() => { vi.restoreAllMocks(); }); describe('disableTrack', () => { it('sets track.enabled = false on all video tracks for the user', () => { const stream = makeStream(true); const gameWebrtc = makeGameWebrtc(stream); const surface = FoundryAdapter.buildWebRTCSurface(gameWebrtc); surface.disableTrack('user-123'); expect(gameWebrtc.client.getMediaStreamForUser).toHaveBeenCalledWith('user-123'); expect(stream._track.enabled).toBe(false); }); it('logs [ScryingPool] warning when no video tracks found (stream has none)', () => { const emptyStream = { getVideoTracks: vi.fn().mockReturnValue([]) }; const gameWebrtc = makeGameWebrtc(emptyStream); const surface = FoundryAdapter.buildWebRTCSurface(gameWebrtc); surface.disableTrack('user-456'); expect(consoleWarnSpy).toHaveBeenCalledWith( '[ScryingPool] disableTrack: no video tracks found for', 'user-456', ); }); it('logs [ScryingPool] warning when getMediaStreamForUser returns null', () => { const gameWebrtc = makeGameWebrtc(null); const surface = FoundryAdapter.buildWebRTCSurface(gameWebrtc); surface.disableTrack('user-789'); expect(consoleWarnSpy).toHaveBeenCalledWith( '[ScryingPool] disableTrack: no video tracks found for', 'user-789', ); }); it('catches errors and logs [ScryingPool] error without throwing', () => { const gameWebrtc = { client: { getMediaStreamForUser: vi.fn().mockImplementation(() => { throw new Error('AV backend unavailable'); }), }, }; const surface = FoundryAdapter.buildWebRTCSurface(gameWebrtc); expect(() => surface.disableTrack('user-err')).not.toThrow(); expect(consoleErrorSpy).toHaveBeenCalledWith( '[ScryingPool] disableTrack failed:', expect.any(Error), ); }); }); describe('enableTrack', () => { it('sets track.enabled = true on all video tracks for the user', () => { const stream = makeStream(false); const gameWebrtc = makeGameWebrtc(stream); const surface = FoundryAdapter.buildWebRTCSurface(gameWebrtc); surface.enableTrack('user-123'); expect(gameWebrtc.client.getMediaStreamForUser).toHaveBeenCalledWith('user-123'); expect(stream._track.enabled).toBe(true); }); it('logs [ScryingPool] warning when no video tracks found', () => { const emptyStream = { getVideoTracks: vi.fn().mockReturnValue([]) }; const gameWebrtc = makeGameWebrtc(emptyStream); const surface = FoundryAdapter.buildWebRTCSurface(gameWebrtc); surface.enableTrack('user-456'); expect(consoleWarnSpy).toHaveBeenCalledWith( '[ScryingPool] enableTrack: no video tracks found for', 'user-456', ); }); it('catches errors and logs [ScryingPool] error without throwing', () => { const gameWebrtc = { client: { getMediaStreamForUser: vi.fn().mockImplementation(() => { throw new Error('AV backend unavailable'); }), }, }; const surface = FoundryAdapter.buildWebRTCSurface(gameWebrtc); expect(() => surface.enableTrack('user-err')).not.toThrow(); expect(consoleErrorSpy).toHaveBeenCalledWith( '[ScryingPool] enableTrack failed:', expect.any(Error), ); }); }); }); // ─── Constructor + interface shape parity ───────────────────────────────────── describe('FoundryAdapter constructor', () => { it('is side-effect-free — instantiates without accessing game.*', () => { expect(() => new FoundryAdapter()).not.toThrow(); }); it('has webrtc = null by default (css-fallback path)', () => { const adapter = new FoundryAdapter(); expect(adapter.webrtc).toBeNull(); }); it('exposes SETTINGS_NS and SETTING_WEBRTC_MODE static constants', () => { expect(typeof FoundryAdapter.SETTINGS_NS).toBe('string'); expect(FoundryAdapter.SETTINGS_NS).toBe('scrying-pool'); expect(typeof FoundryAdapter.SETTING_WEBRTC_MODE).toBe('string'); expect(FoundryAdapter.SETTING_WEBRTC_MODE).toBe('webrtcMode'); }); }); describe('Interface shape parity with canonical mock', () => { it('canonical mock webrtc default matches FoundryAdapter default (null)', () => { const mock = createFoundryAdapterMock(); const adapter = new FoundryAdapter(); expect(mock.webrtc).toBe(adapter.webrtc); // both null }); it('canonical mock can override webrtc to simulate track-disable surface', () => { const mockSurface = { disableTrack: vi.fn(), enableTrack: vi.fn() }; const mock = createFoundryAdapterMock({ webrtc: mockSurface }); expect(mock.webrtc).toBe(mockSurface); expect(typeof mock.webrtc.disableTrack).toBe('function'); expect(typeof mock.webrtc.enableTrack).toBe('function'); }); it('buildWebRTCSurface returns object with disableTrack and enableTrack', () => { const surface = FoundryAdapter.buildWebRTCSurface(makeGameWebrtc()); expect(typeof surface.disableTrack).toBe('function'); expect(typeof surface.enableTrack).toBe('function'); }); });