import { test, expect, describe, beforeEach, afterEach } from "bun:test"; // Mock MediaStream and related APIs class MockMediaStreamTrack { kind: string; id: string; label: string = ''; enabled: boolean = true; muted: boolean = false; readyState: 'live' | 'ended' = 'live'; constructor(kind: string) { this.kind = kind; this.id = Math.random().toString(36); } stop() { this.readyState = 'ended'; } clone() { return new MockMediaStreamTrack(this.kind); } } class MockMediaStream { id: string; active: boolean = true; private tracks: MockMediaStreamTrack[] = []; constructor(tracks?: MockMediaStreamTrack[]) { this.id = Math.random().toString(36); if (tracks) { this.tracks = [...tracks]; } } getTracks() { return [...this.tracks]; } getVideoTracks() { return this.tracks.filter(track => track.kind === 'video'); } getAudioTracks() { return this.tracks.filter(track => track.kind === 'audio'); } addTrack(track: MockMediaStreamTrack) { this.tracks.push(track); } removeTrack(track: MockMediaStreamTrack) { const index = this.tracks.indexOf(track); if (index > -1) { this.tracks.splice(index, 1); } } } class MockHTMLVideoElement { srcObject: MockMediaStream | null = null; autoplay: boolean = false; muted: boolean = false; play() { return Promise.resolve(); } pause() {} } // Mock navigator.mediaDevices let mockGetUserMediaCalls: any[] = []; const mockGetUserMedia = (constraints: any) => { mockGetUserMediaCalls.push(constraints); const tracks = []; if (constraints.video) tracks.push(new MockMediaStreamTrack('video')); if (constraints.audio) tracks.push(new MockMediaStreamTrack('audio')); return Promise.resolve(new MockMediaStream(tracks)); }; (global as any).navigator = { mediaDevices: { getUserMedia: mockGetUserMedia } }; // Mock document.getElementById let getElementByIdReturn: any = null; (global as any).document = { getElementById: () => getElementByIdReturn }; // Import after mocking import { MediaHandler } from "../../public/js/services/MediaHandler.ts"; describe("MediaHandler", () => { let mediaHandler: MediaHandler; let mockVideoElement: MockHTMLVideoElement; beforeEach(() => { mockVideoElement = new MockHTMLVideoElement(); getElementByIdReturn = mockVideoElement; mockGetUserMediaCalls.length = 0; // Clear calls array }); test("should initialize with video element", () => { mediaHandler = new MediaHandler('testVideo'); // The MediaHandler should have found and stored the video element expect(mediaHandler.getLocalVideo()).toBe(mockVideoElement); }); test("should initialize without video element", () => { mediaHandler = new MediaHandler(); expect(mediaHandler.getLocalVideo()).toBeNull(); }); test("should get local stream successfully", async () => { mediaHandler = new MediaHandler('testVideo'); const mockTracks = [ new MockMediaStreamTrack('video'), new MockMediaStreamTrack('audio') ]; const mockStream = new MockMediaStream(mockTracks); // Override mock to return this specific stream and track calls const originalGetUserMedia = (global as any).navigator.mediaDevices.getUserMedia; const getUserMediaCalls: any[] = []; (global as any).navigator.mediaDevices.getUserMedia = (constraints: any) => { getUserMediaCalls.push(constraints); return Promise.resolve(mockStream); }; const stream = await mediaHandler.getLocalStream(); expect(getUserMediaCalls).toHaveLength(1); expect(getUserMediaCalls[0]).toEqual({ video: { width: { ideal: 1280 }, height: { ideal: 720 }, frameRate: { ideal: 30 } }, audio: true }); expect(stream).toBe(mockStream); expect(mockVideoElement.srcObject).toBe(mockStream); // Restore original (global as any).navigator.mediaDevices.getUserMedia = originalGetUserMedia; }); test("should handle getUserMedia error", async () => { mediaHandler = new MediaHandler('testVideo'); const error = new Error('Camera access denied'); // Override mock to reject with error const originalGetUserMedia = (global as any).navigator.mediaDevices.getUserMedia; const originalConsoleError = console.error; const errorLogs: any[] = []; console.error = (...args: any[]) => errorLogs.push(args); (global as any).navigator.mediaDevices.getUserMedia = () => Promise.reject(error); await expect(mediaHandler.getLocalStream()).rejects.toThrow('Camera access denied'); expect(errorLogs.length).toBeGreaterThan(0); expect(errorLogs[0][0]).toBe('Error accessing media devices:'); expect(errorLogs[0][1]).toBe(error); // Restore originals console.error = originalConsoleError; (global as any).navigator.mediaDevices.getUserMedia = originalGetUserMedia; }); test("should stop local stream", async () => { mediaHandler = new MediaHandler('testVideo'); const mockTracks = [ new MockMediaStreamTrack('video'), new MockMediaStreamTrack('audio') ]; const mockStream = new MockMediaStream(mockTracks); // Override mock to return this specific stream const originalGetUserMedia = (global as any).navigator.mediaDevices.getUserMedia; (global as any).navigator.mediaDevices.getUserMedia = () => Promise.resolve(mockStream); await mediaHandler.getLocalStream(); // Track the initial state of the tracks const initialStates = mockTracks.map(track => track.readyState); mediaHandler.stopLocalStream(); // Check that all tracks were stopped (readyState should be 'ended') mockTracks.forEach(track => { expect(track.readyState).toBe('ended'); }); expect(mockVideoElement.srcObject).toBeNull(); expect(mediaHandler.getCurrentStream()).toBeNull(); // Restore original (global as any).navigator.mediaDevices.getUserMedia = originalGetUserMedia; }); test("should handle stopping stream when no stream exists", () => { mediaHandler = new MediaHandler('testVideo'); expect(() => { mediaHandler.stopLocalStream(); }).not.toThrow(); expect(mockVideoElement.srcObject).toBeNull(); }); test("should get current stream", async () => { mediaHandler = new MediaHandler('testVideo'); expect(mediaHandler.getCurrentStream()).toBeNull(); const mockStream = new MockMediaStream([new MockMediaStreamTrack('video')]); // Override mock to return this specific stream const originalGetUserMedia = (global as any).navigator.mediaDevices.getUserMedia; (global as any).navigator.mediaDevices.getUserMedia = () => Promise.resolve(mockStream); await mediaHandler.getLocalStream(); // Restore original (global as any).navigator.mediaDevices.getUserMedia = originalGetUserMedia; expect(mediaHandler.getCurrentStream()).toBe(mockStream); }); test("should work without video element", async () => { getElementByIdReturn = null; mediaHandler = new MediaHandler('nonExistentVideo'); const mockStream = new MockMediaStream([new MockMediaStreamTrack('video')]); // Override mock to return this specific stream const originalGetUserMedia = (global as any).navigator.mediaDevices.getUserMedia; (global as any).navigator.mediaDevices.getUserMedia = () => Promise.resolve(mockStream); const stream = await mediaHandler.getLocalStream(); expect(stream).toBe(mockStream); expect(mediaHandler.getLocalVideo()).toBeNull(); // Restore original (global as any).navigator.mediaDevices.getUserMedia = originalGetUserMedia; }); test("should handle multiple calls to getLocalStream", async () => { mediaHandler = new MediaHandler('testVideo'); const mockStream1 = new MockMediaStream([new MockMediaStreamTrack('video')]); const mockStream2 = new MockMediaStream([new MockMediaStreamTrack('video')]); let callCount = 0; const originalGetUserMedia = (global as any).navigator.mediaDevices.getUserMedia; (global as any).navigator.mediaDevices.getUserMedia = () => { callCount++; return Promise.resolve(callCount === 1 ? mockStream1 : mockStream2); }; const stream1 = await mediaHandler.getLocalStream(); const stream2 = await mediaHandler.getLocalStream(); expect(stream1).toBe(mockStream1); expect(stream2).toBe(mockStream2); expect(callCount).toBe(2); // The video element should have the latest stream expect(mockVideoElement.srcObject).toBe(mockStream2); // Restore original (global as any).navigator.mediaDevices.getUserMedia = originalGetUserMedia; }); });