|
|
@@ -0,0 +1,232 @@
|
|
|
+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() }])
|
|
|
+ ),
|
|
|
+ },
|
|
|
+}))
|
|
|
+
|
|
|
+// 模拟 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: {},
|
|
|
+ })
|
|
|
+ }
|
|
|
+ })
|
|
|
+ })
|
|
|
+})
|