import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' import { RtcManagerAdapter } from '../src/managers/rtc-manager-adapter' import { SttError } from '../src/core/stt-error' // 模拟 AgoraRTC SDK vi.mock('agora-rtc-sdk-ng', () => ({ default: { createClient: vi.fn(() => ({ join: vi.fn(), leave: vi.fn(), publish: vi.fn(), subscribe: vi.fn(), unsubscribe: vi.fn(), on: vi.fn(), })), createMicrophoneAndCameraTracks: vi.fn(() => Promise.resolve([{ close: vi.fn(), isPlaying: false, play: vi.fn() }, { close: vi.fn() }]) ), createMicrophoneAudioTrack: vi.fn(() => Promise.resolve({ close: vi.fn(), isPlaying: false, play: vi.fn() }) ), setPlaybackDevice: vi.fn(() => Promise.resolve()), }, })) // 模拟 MediaDevices API Object.defineProperty(global, 'navigator', { value: { mediaDevices: { getUserMedia: vi.fn(() => Promise.resolve({})), enumerateDevices: vi.fn(() => Promise.resolve([ { deviceId: 'mic1', kind: 'audioinput', label: '麦克风 1' }, { deviceId: 'mic2', kind: 'audioinput', label: '麦克风 2' }, { deviceId: 'speaker1', kind: 'audiooutput', label: '扬声器 1' }, { deviceId: 'speaker2', kind: 'audiooutput', label: '扬声器 2' }, ]) ), }, }, }) // 模拟 token 工具 vi.mock('../src/utils/token-utils', () => ({ generateAgoraToken: vi.fn(() => Promise.resolve('mock-token')), })) describe('RtcManagerAdapter', () => { let manager: RtcManagerAdapter beforeEach(() => { manager = new RtcManagerAdapter('test-app-id', 'test-certificate') }) afterEach(async () => { await manager.destroy() }) describe('constructor', () => { it('should create instance with appId and certificate', () => { expect(manager).toBeInstanceOf(RtcManagerAdapter) expect(manager.isJoined).toBe(false) expect(manager.config).toBeUndefined() }) it('should create instance without parameters', () => { const managerWithoutParams = new RtcManagerAdapter() expect(managerWithoutParams).toBeInstanceOf(RtcManagerAdapter) }) }) describe('join', () => { it('should join channel successfully', async () => { const config = { channel: 'test-channel', userId: 'test-user' } await manager.join(config) expect(manager.isJoined).toBe(true) expect(manager.config).toEqual(config) expect(manager.userId).toBe('test-user') expect(manager.channel).toBe('test-channel') }) it('should throw error when missing required parameters', async () => { await expect(manager.join({} as any)).rejects.toThrow(SttError) await expect(manager.join({ channel: 'test' } as any)).rejects.toThrow(SttError) await expect(manager.join({ userId: 'test' } as any)).rejects.toThrow(SttError) }) it('should throw error when already joined', async () => { const config = { channel: 'test-channel', userId: 'test-user' } await manager.join(config) await expect(manager.join(config)).rejects.toThrow(SttError) }) it('should throw error when missing appId', async () => { const managerWithoutAppId = new RtcManagerAdapter() const config = { channel: 'test-channel', userId: 'test-user' } await expect(managerWithoutAppId.join(config)).rejects.toThrow(SttError) }) it('should throw error when missing certificate', async () => { const managerWithoutCert = new RtcManagerAdapter('test-app-id') const config = { channel: 'test-channel', userId: 'test-user' } await expect(managerWithoutCert.join(config)).rejects.toThrow(SttError) }) it('should emit connecting and connected events', async () => { const config = { channel: 'test-channel', userId: 'test-user' } const connectingSpy = vi.fn() const connectedSpy = vi.fn() manager.on('connecting', connectingSpy) manager.on('connected', connectedSpy) await manager.join(config) expect(connectingSpy).toHaveBeenCalledWith({ channel: 'test-channel', userId: 'test-user', }) expect(connectedSpy).toHaveBeenCalledWith({ channel: 'test-channel', userId: 'test-user', }) }) }) describe('createTracks', () => { it('should create tracks successfully', async () => { const config = { channel: 'test-channel', userId: 'test-user' } await manager.join(config) // 先设置事件监听器 const localUserChangedSpy = vi.fn() manager.on('localUserChanged', localUserChangedSpy) await manager.createTracks() // 验证事件被触发 expect(localUserChangedSpy).toHaveBeenCalled() }) it('should throw error when not joined', async () => { await expect(manager.createTracks()).rejects.toThrow(SttError) }) }) describe('publish', () => { it('should publish tracks successfully', async () => { const config = { channel: 'test-channel', userId: 'test-user' } await manager.join(config) await manager.createTracks() await expect(manager.publish()).resolves.not.toThrow() }) it('should throw error when not joined', async () => { await expect(manager.publish()).rejects.toThrow(SttError) }) it('should throw error when tracks not created', async () => { const config = { channel: 'test-channel', userId: 'test-user' } await manager.join(config) await expect(manager.publish()).rejects.toThrow(SttError) }) }) describe('destroy', () => { it('should destroy manager successfully', async () => { const config = { channel: 'test-channel', userId: 'test-user' } await manager.join(config) await manager.destroy() expect(manager.isJoined).toBe(false) expect(manager.config).toBeUndefined() expect(manager.userId).toBe('') expect(manager.channel).toBe('') }) it('should emit destroying and destroyed events', async () => { const config = { channel: 'test-channel', userId: 'test-user' } await manager.join(config) const destroyingSpy = vi.fn() const destroyedSpy = vi.fn() manager.on('destroying', destroyingSpy) manager.on('destroyed', destroyedSpy) await manager.destroy() expect(destroyingSpy).toHaveBeenCalled() expect(destroyedSpy).toHaveBeenCalled() }) it('should handle destroy when not joined', async () => { await expect(manager.destroy()).resolves.not.toThrow() }) }) describe('event handling', () => { it('should handle network quality events', async () => { const config = { channel: 'test-channel', userId: 'test-user' } await manager.join(config) const networkQualitySpy = vi.fn() manager.on('networkQuality', networkQualitySpy) // 模拟网络质量事件 const mockClient = await import('agora-rtc-sdk-ng') const client = mockClient.default.createClient() const networkQualityHandler = client.on.mock.calls.find( (call: any) => call[0] === 'network-quality' )?.[1] if (networkQualityHandler) { networkQualityHandler({ uplink: 5, downlink: 5 }) expect(networkQualitySpy).toHaveBeenCalledWith({ uplink: 5, downlink: 5 }) } }) it('should handle user published events', async () => { const config = { channel: 'test-channel', userId: 'test-user' } await manager.join(config) const remoteUserChangedSpy = vi.fn() manager.on('remoteUserChanged', remoteUserChangedSpy) // 模拟用户发布事件 const mockClient = await import('agora-rtc-sdk-ng') const client = mockClient.default.createClient() const userPublishedHandler = client.on.mock.calls.find( (call: any) => call[0] === 'user-published' )?.[1] if (userPublishedHandler) { const mockUser = { uid: 'remote-user', audioTrack: {}, videoTrack: {} } await userPublishedHandler(mockUser, 'audio') expect(remoteUserChangedSpy).toHaveBeenCalledWith({ userId: 'remote-user', audioTrack: {}, videoTrack: {}, }) } }) }) describe('audio device management', () => { describe('getAudioDevices', () => { it('should get audio devices successfully', async () => { const devices = await manager.getAudioDevices() expect(devices).toEqual({ microphones: [ { deviceId: 'mic1', kind: 'audioinput', label: '麦克风 1' }, { deviceId: 'mic2', kind: 'audioinput', label: '麦克风 2' }, ], speakers: [ { deviceId: 'speaker1', kind: 'audiooutput', label: '扬声器 1' }, { deviceId: 'speaker2', kind: 'audiooutput', label: '扬声器 2' }, ], }) }) it('should handle errors when getting devices', async () => { // 模拟 getUserMedia 失败 const mockNavigator = global.navigator as any mockNavigator.mediaDevices.getUserMedia = vi.fn(() => Promise.reject(new Error('Permission denied')) ) await expect(manager.getAudioDevices()).rejects.toThrow('Permission denied') }) }) describe('switchMicrophone', () => { it('should switch microphone successfully', async () => { const config = { channel: 'test-channel', userId: 'test-user' } await manager.join(config) await manager.createTracks() const audioDeviceChangedSpy = vi.fn() manager.on('audioDeviceChanged', audioDeviceChangedSpy) await manager.switchMicrophone('mic2') expect(audioDeviceChangedSpy).toHaveBeenCalledWith({ deviceId: 'mic2', deviceType: 'microphone', }) }) it('should throw error when no audio track available', async () => { await expect(manager.switchMicrophone('mic1')).rejects.toThrow(SttError) }) it('should throw error when not joined', async () => { const config = { channel: 'test-channel', userId: 'test-user' } await manager.join(config) await manager.createTracks() // 模拟 createMicrophoneAudioTrack 失败 const mockAgoraRTC = await import('agora-rtc-sdk-ng') mockAgoraRTC.default.createMicrophoneAudioTrack = vi.fn(() => Promise.reject(new Error('Device not found')) ) await expect(manager.switchMicrophone('invalid-device')).rejects.toThrow('Device not found') }) }) describe('switchSpeaker', () => { it('should switch speaker successfully', async () => { const audioDeviceChangedSpy = vi.fn() manager.on('audioDeviceChanged', audioDeviceChangedSpy) await manager.switchSpeaker('speaker2') expect(audioDeviceChangedSpy).toHaveBeenCalledWith({ deviceId: 'speaker2', deviceType: 'speaker', }) }) it('should handle errors when switching speaker', async () => { // 模拟 setPlaybackDevice 失败 const mockAgoraRTC = await import('agora-rtc-sdk-ng') mockAgoraRTC.default.setPlaybackDevice = vi.fn(() => Promise.reject(new Error('Device not found')) ) await expect(manager.switchSpeaker('invalid-device')).rejects.toThrow('Device not found') }) }) describe('createTracks with device selection', () => { it('should create tracks with specific microphone device', async () => { const config = { channel: 'test-channel', userId: 'test-user' } await manager.join(config) const localUserChangedSpy = vi.fn() manager.on('localUserChanged', localUserChangedSpy) await manager.createTracks('mic2') expect(localUserChangedSpy).toHaveBeenCalled() // 验证 createMicrophoneAndCameraTracks 被调用时传入了正确的参数 const mockAgoraRTC = await import('agora-rtc-sdk-ng') expect(mockAgoraRTC.default.createMicrophoneAndCameraTracks).toHaveBeenCalledWith({ AGC: false, microphoneId: 'mic2', }) }) }) }) })