288 lines
8.6 KiB
TypeScript
288 lines
8.6 KiB
TypeScript
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;
|
|
});
|
|
}); |