Update tests for Story 5-1 Full AV Replacement

- Mark Story 5-1 as done in sprint-status.yaml
- Update FoundryAdapter.test.js:
  - Updated probeCapability tests to expect 'stream-access' (was 'css-fallback')
  - Added tests for all 11 new buildWebRTCSurface methods
  - Added input validation tests for userId-taking methods
- Update ScryingPoolStrip.test.js:
  - Added tests for hasStreamAccess flag in buildParticipantList
  - Added tests for hasStreamAccess in getData()
  - Added tests for _attachVideoStream() method
  - Added tests for _cleanupVideoStreams() method

All 892 unit tests passing + 7 integration tests passing

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
2026-05-24 12:36:52 +02:00
parent f8cbb75773
commit 25dd427a59
3 changed files with 377 additions and 6 deletions
+207 -5
View File
@@ -4,10 +4,13 @@
*
* Story 1.2 — WebRTC spike: FoundryAdapter probe tests.
* Story 1.3 — Data layer: constructor(game), surface delegation tests.
* Story 5.1 — Full AV Replacement: probeCapability returns 'stream-access', buildWebRTCSurface with full API
*
* OQ-1 spike result: css-fallback (FoundryVTT v14, 2026-05-21)
* OQ-1 spike result: css-fallback (FoundryVTT v14, 2026-05-21) - SUPERSEDED
* track.enabled = false does NOT stop inbound WebRTC bandwidth.
* Probe returns 'css-fallback' when AVClient is available, 'unsupported' otherwise.
*
* NEW: probeCapability returns 'stream-access' when getMediaStreamForUser is available
* Full AV replacement architecture: hide Foundry dock, show VVM dock with actual video
*/
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
@@ -72,15 +75,36 @@ describe('FoundryAdapter.probeCapability', () => {
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
it('returns "stream-access" when client has getMediaStreamForUser (Story 5.1 - Full AV Replacement)', () => {
// getMediaStreamForUser is available → full AV replacement is possible
const gameWebrtc = makeGameWebrtc();
expect(FoundryAdapter.probeCapability(gameWebrtc)).toBe('css-fallback');
expect(FoundryAdapter.probeCapability(gameWebrtc)).toBe('stream-access');
});
});
// ─── buildWebRTCSurface ───────────────────────────────────────────────────────
// Helper to create a full mock WebRTC client with all methods
function makeFullWebRTCClient(stream = null) {
return {
getMediaStreamForUser: vi.fn().mockReturnValue(stream),
getConnectedUsers: vi.fn().mockReturnValue(['user-1', 'user-2']),
getLevelsStreamForUser: vi.fn().mockReturnValue(stream),
isAudioEnabled: vi.fn().mockReturnValue(true),
isVideoEnabled: vi.fn().mockReturnValue(true),
toggleAudio: vi.fn(),
toggleVideo: vi.fn(),
toggleBroadcast: vi.fn(),
setUserVideo: vi.fn().mockResolvedValue(undefined),
};
}
function makeFullGameWebrtc(stream = null) {
return {
client: makeFullWebRTCClient(stream),
};
}
describe('FoundryAdapter.buildWebRTCSurface', () => {
let consoleWarnSpy;
let consoleErrorSpy;
@@ -94,6 +118,184 @@ describe('FoundryAdapter.buildWebRTCSurface', () => {
vi.restoreAllMocks();
});
describe('returns complete WebRTC surface with 11 methods (Story 5.1)', () => {
it('returns object with all 11 WebRTC methods', () => {
const gameWebrtc = makeFullGameWebrtc();
const surface = FoundryAdapter.buildWebRTCSurface(gameWebrtc);
expect(surface).toHaveProperty('getMediaStreamForUser');
expect(surface).toHaveProperty('getConnectedUsers');
expect(surface).toHaveProperty('getLevelsStreamForUser');
expect(surface).toHaveProperty('isAudioEnabled');
expect(surface).toHaveProperty('isVideoEnabled');
expect(surface).toHaveProperty('toggleAudio');
expect(surface).toHaveProperty('toggleVideo');
expect(surface).toHaveProperty('toggleBroadcast');
expect(surface).toHaveProperty('setUserVideo');
expect(surface).toHaveProperty('disableTrack');
expect(surface).toHaveProperty('enableTrack');
});
it('getMediaStreamForUser returns stream from client', () => {
const mockStream = new MediaStream();
const gameWebrtc = makeFullGameWebrtc(mockStream);
const surface = FoundryAdapter.buildWebRTCSurface(gameWebrtc);
const result = surface.getMediaStreamForUser('user-1');
expect(result).toBe(mockStream);
expect(gameWebrtc.client.getMediaStreamForUser).toHaveBeenCalledWith('user-1');
});
it('getMediaStreamForUser returns null for invalid userId', () => {
const gameWebrtc = makeFullGameWebrtc();
const surface = FoundryAdapter.buildWebRTCSurface(gameWebrtc);
const result = surface.getMediaStreamForUser('');
expect(result).toBeNull();
expect(consoleWarnSpy).toHaveBeenCalled();
});
it('getMediaStreamForUser returns null when client method throws', () => {
const gameWebrtc = {
client: {
getMediaStreamForUser: vi.fn().mockImplementation(() => {
throw new Error('AV error');
}),
},
};
const surface = FoundryAdapter.buildWebRTCSurface(gameWebrtc);
const result = surface.getMediaStreamForUser('user-1');
expect(result).toBeNull();
expect(consoleErrorSpy).toHaveBeenCalled();
});
it('getConnectedUsers returns array from client', () => {
const gameWebrtc = makeFullGameWebrtc();
const surface = FoundryAdapter.buildWebRTCSurface(gameWebrtc);
const result = surface.getConnectedUsers();
expect(result).toEqual(['user-1', 'user-2']);
});
it('getConnectedUsers returns empty array on error', () => {
const gameWebrtc = {
client: {
getConnectedUsers: vi.fn().mockImplementation(() => {
throw new Error('Connection error');
}),
},
};
const surface = FoundryAdapter.buildWebRTCSurface(gameWebrtc);
const result = surface.getConnectedUsers();
expect(result).toEqual([]);
});
it('isAudioEnabled delegates to client', () => {
const gameWebrtc = makeFullGameWebrtc();
const surface = FoundryAdapter.buildWebRTCSurface(gameWebrtc);
const result = surface.isAudioEnabled();
expect(result).toBe(true);
expect(gameWebrtc.client.isAudioEnabled).toHaveBeenCalled();
});
it('isVideoEnabled delegates to client', () => {
const gameWebrtc = makeFullGameWebrtc();
const surface = FoundryAdapter.buildWebRTCSurface(gameWebrtc);
const result = surface.isVideoEnabled();
expect(result).toBe(true);
expect(gameWebrtc.client.isVideoEnabled).toHaveBeenCalled();
});
it('toggleAudio calls client method with boolean', () => {
const gameWebrtc = makeFullGameWebrtc();
const surface = FoundryAdapter.buildWebRTCSurface(gameWebrtc);
surface.toggleAudio(true);
expect(gameWebrtc.client.toggleAudio).toHaveBeenCalledWith(true);
surface.toggleAudio(false);
expect(gameWebrtc.client.toggleAudio).toHaveBeenCalledWith(false);
});
it('toggleVideo calls client method with boolean', () => {
const gameWebrtc = makeFullGameWebrtc();
const surface = FoundryAdapter.buildWebRTCSurface(gameWebrtc);
surface.toggleVideo(true);
expect(gameWebrtc.client.toggleVideo).toHaveBeenCalledWith(true);
});
it('toggleBroadcast calls client method with boolean', () => {
const gameWebrtc = makeFullGameWebrtc();
const surface = FoundryAdapter.buildWebRTCSurface(gameWebrtc);
surface.toggleBroadcast(false);
expect(gameWebrtc.client.toggleBroadcast).toHaveBeenCalledWith(false);
});
it('setUserVideo calls client method with userId and videoElement', async () => {
const gameWebrtc = makeFullGameWebrtc();
const surface = FoundryAdapter.buildWebRTCSurface(gameWebrtc);
const videoElement = document.createElement('video');
await surface.setUserVideo('user-1', videoElement);
expect(gameWebrtc.client.setUserVideo).toHaveBeenCalledWith('user-1', videoElement);
});
it('setUserVideo validates userId is string', async () => {
const gameWebrtc = makeFullGameWebrtc();
const surface = FoundryAdapter.buildWebRTCSurface(gameWebrtc);
const videoElement = document.createElement('video');
await surface.setUserVideo(null, videoElement);
expect(consoleWarnSpy).toHaveBeenCalled();
expect(gameWebrtc.client.setUserVideo).not.toHaveBeenCalled();
});
it('setUserVideo validates videoElement is HTMLVideoElement', async () => {
const gameWebrtc = makeFullGameWebrtc();
const surface = FoundryAdapter.buildWebRTCSurface(gameWebrtc);
await surface.setUserVideo('user-1', {});
expect(consoleWarnSpy).toHaveBeenCalled();
expect(gameWebrtc.client.setUserVideo).not.toHaveBeenCalled();
});
});
describe('input validation for userId-taking methods', () => {
it('getLevelsStreamForUser warns on empty userId', () => {
const gameWebrtc = makeFullGameWebrtc();
const surface = FoundryAdapter.buildWebRTCSurface(gameWebrtc);
const result = surface.getLevelsStreamForUser('');
expect(result).toBeNull();
expect(consoleWarnSpy).toHaveBeenCalledWith(
'[ScryingPool] getLevelsStreamForUser: invalid userId:',
''
);
});
it('disableTrack warns on invalid userId', () => {
const gameWebrtc = makeFullGameWebrtc();
const surface = FoundryAdapter.buildWebRTCSurface(gameWebrtc);
surface.disableTrack(null);
expect(consoleWarnSpy).toHaveBeenCalled();
});
it('enableTrack warns on invalid userId', () => {
const gameWebrtc = makeFullGameWebrtc();
const surface = FoundryAdapter.buildWebRTCSurface(gameWebrtc);
surface.enableTrack(123);
expect(consoleWarnSpy).toHaveBeenCalled();
});
});
describe('disableTrack', () => {
it('sets track.enabled = false on all video tracks for the user', () => {
const stream = makeStream(true);