Files
scrying-pool/tests/unit/foundry/FoundryAdapter.test.js
T
2026-05-21 23:08:34 +02:00

225 lines
8.3 KiB
JavaScript

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