import { jest } from '@jest/globals'; import { MeetingRecorder } from '../../public/call/media/meeting-recorder.js'; class MediaStreamMock { constructor(tracks = []) { this.tracks = tracks; } getTracks() { return this.tracks; } getAudioTracks() { return this.tracks.filter(track => track.kind === 'audio'); } getVideoTracks() { return this.tracks.filter(track => track.kind === 'video'); } } class MediaRecorderMock { static isTypeSupported() { return true; } constructor(stream, options) { this.stream = stream; this.mimeType = options.mimeType; this.state = 'inactive'; } start() { this.state = 'recording'; } stop() { this.state = 'inactive'; this.ondataavailable({ data: new Blob(['recording'], { type: this.mimeType }) }); this.onstop(); } } function createTrack(kind) { return { kind, readyState: 'live', stop: jest.fn(), clone: jest.fn(() => createTrack(kind)) }; } function createWindowMock({ mediaRecorder = MediaRecorderMock } = {}) { return { MediaRecorder: mediaRecorder, MediaStream: MediaStreamMock, URL: { createObjectURL: jest.fn(() => 'blob:recording'), revokeObjectURL: jest.fn() }, requestAnimationFrame: jest.fn(() => 1), cancelAnimationFrame: jest.fn(), setTimeout: jest.fn((callback) => { callback(); return 1; }) }; } describe('MeetingRecorder', () => { beforeEach(() => { HTMLCanvasElement.prototype.getContext = jest.fn(() => ({ fillStyle: '', font: '', textAlign: '', textBaseline: '', fillRect: jest.fn(), fillText: jest.fn(), drawImage: jest.fn() })); HTMLCanvasElement.prototype.captureStream = jest.fn(() => new MediaStreamMock([createTrack('video')])); }); afterEach(() => { jest.restoreAllMocks(); document.body.innerHTML = ''; }); test('starts and stops recording a meeting file', async () => { const windowRef = createWindowMock(); const recorder = new MeetingRecorder({ documentRef: document, windowRef }); const localVideo = document.createElement('video'); localVideo.id = 'localVideo'; localVideo.srcObject = new MediaStreamMock([createTrack('video')]); Object.defineProperty(localVideo, 'readyState', { value: 2 }); Object.defineProperty(localVideo, 'videoWidth', { value: 640 }); Object.defineProperty(localVideo, 'videoHeight', { value: 360 }); localVideo.getBoundingClientRect = () => ({ width: 320, height: 180 }); document.body.appendChild(localVideo); await recorder.start({ localStream: new MediaStreamMock([createTrack('audio')]), connectionId: '123-456-789' }); expect(recorder.isRecording()).toBe(true); const result = await recorder.stop(); expect(result.filename).toContain('meeting-recording-123-456-789'); expect(result.mimeType.toLowerCase()).toBe('video/mp4;codecs=avc1.42e01e,mp4a.40.2'); expect(result.filename).toMatch(/\.mp4$/); expect(windowRef.URL.createObjectURL).not.toHaveBeenCalled(); expect(recorder.isRecording()).toBe(false); }); test('reports unsupported browsers', async () => { HTMLCanvasElement.prototype.captureStream = undefined; const windowRef = createWindowMock({ mediaRecorder: undefined }); const recorder = new MeetingRecorder({ documentRef: document, windowRef }); await expect(recorder.start()).rejects.toThrow('当前浏览器不支持会议录制'); }); });