2
0

rtc-manager-adapter.test.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363
  1. import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
  2. import { RtcManagerAdapter } from '../src/managers/rtc-manager-adapter'
  3. import { SttError } from '../src/core/stt-error'
  4. // 模拟 AgoraRTC SDK
  5. vi.mock('agora-rtc-sdk-ng', () => ({
  6. default: {
  7. createClient: vi.fn(() => ({
  8. join: vi.fn(),
  9. leave: vi.fn(),
  10. publish: vi.fn(),
  11. subscribe: vi.fn(),
  12. unsubscribe: vi.fn(),
  13. on: vi.fn(),
  14. })),
  15. createMicrophoneAndCameraTracks: vi.fn(() =>
  16. Promise.resolve([{ close: vi.fn(), isPlaying: false, play: vi.fn() }, { close: vi.fn() }])
  17. ),
  18. createMicrophoneAudioTrack: vi.fn(() =>
  19. Promise.resolve({ close: vi.fn(), isPlaying: false, play: vi.fn() })
  20. ),
  21. setPlaybackDevice: vi.fn(() => Promise.resolve()),
  22. },
  23. }))
  24. // 模拟 MediaDevices API
  25. Object.defineProperty(global, 'navigator', {
  26. value: {
  27. mediaDevices: {
  28. getUserMedia: vi.fn(() => Promise.resolve({})),
  29. enumerateDevices: vi.fn(() =>
  30. Promise.resolve([
  31. { deviceId: 'mic1', kind: 'audioinput', label: '麦克风 1' },
  32. { deviceId: 'mic2', kind: 'audioinput', label: '麦克风 2' },
  33. { deviceId: 'speaker1', kind: 'audiooutput', label: '扬声器 1' },
  34. { deviceId: 'speaker2', kind: 'audiooutput', label: '扬声器 2' },
  35. ])
  36. ),
  37. },
  38. },
  39. })
  40. // 模拟 token 工具
  41. vi.mock('../src/utils/token-utils', () => ({
  42. generateAgoraToken: vi.fn(() => Promise.resolve('mock-token')),
  43. }))
  44. describe('RtcManagerAdapter', () => {
  45. let manager: RtcManagerAdapter
  46. beforeEach(() => {
  47. manager = new RtcManagerAdapter('test-app-id', 'test-certificate')
  48. })
  49. afterEach(async () => {
  50. await manager.destroy()
  51. })
  52. describe('constructor', () => {
  53. it('should create instance with appId and certificate', () => {
  54. expect(manager).toBeInstanceOf(RtcManagerAdapter)
  55. expect(manager.isJoined).toBe(false)
  56. expect(manager.config).toBeUndefined()
  57. })
  58. it('should create instance without parameters', () => {
  59. const managerWithoutParams = new RtcManagerAdapter()
  60. expect(managerWithoutParams).toBeInstanceOf(RtcManagerAdapter)
  61. })
  62. })
  63. describe('join', () => {
  64. it('should join channel successfully', async () => {
  65. const config = { channel: 'test-channel', userId: 'test-user' }
  66. await manager.join(config)
  67. expect(manager.isJoined).toBe(true)
  68. expect(manager.config).toEqual(config)
  69. expect(manager.userId).toBe('test-user')
  70. expect(manager.channel).toBe('test-channel')
  71. })
  72. it('should throw error when missing required parameters', async () => {
  73. await expect(manager.join({} as any)).rejects.toThrow(SttError)
  74. await expect(manager.join({ channel: 'test' } as any)).rejects.toThrow(SttError)
  75. await expect(manager.join({ userId: 'test' } as any)).rejects.toThrow(SttError)
  76. })
  77. it('should throw error when already joined', async () => {
  78. const config = { channel: 'test-channel', userId: 'test-user' }
  79. await manager.join(config)
  80. await expect(manager.join(config)).rejects.toThrow(SttError)
  81. })
  82. it('should throw error when missing appId', async () => {
  83. const managerWithoutAppId = new RtcManagerAdapter()
  84. const config = { channel: 'test-channel', userId: 'test-user' }
  85. await expect(managerWithoutAppId.join(config)).rejects.toThrow(SttError)
  86. })
  87. it('should throw error when missing certificate', async () => {
  88. const managerWithoutCert = new RtcManagerAdapter('test-app-id')
  89. const config = { channel: 'test-channel', userId: 'test-user' }
  90. await expect(managerWithoutCert.join(config)).rejects.toThrow(SttError)
  91. })
  92. it('should emit connecting and connected events', async () => {
  93. const config = { channel: 'test-channel', userId: 'test-user' }
  94. const connectingSpy = vi.fn()
  95. const connectedSpy = vi.fn()
  96. manager.on('connecting', connectingSpy)
  97. manager.on('connected', connectedSpy)
  98. await manager.join(config)
  99. expect(connectingSpy).toHaveBeenCalledWith({
  100. channel: 'test-channel',
  101. userId: 'test-user',
  102. })
  103. expect(connectedSpy).toHaveBeenCalledWith({
  104. channel: 'test-channel',
  105. userId: 'test-user',
  106. })
  107. })
  108. })
  109. describe('createTracks', () => {
  110. it('should create tracks successfully', async () => {
  111. const config = { channel: 'test-channel', userId: 'test-user' }
  112. await manager.join(config)
  113. // 先设置事件监听器
  114. const localUserChangedSpy = vi.fn()
  115. manager.on('localUserChanged', localUserChangedSpy)
  116. await manager.createTracks()
  117. // 验证事件被触发
  118. expect(localUserChangedSpy).toHaveBeenCalled()
  119. })
  120. it('should throw error when not joined', async () => {
  121. await expect(manager.createTracks()).rejects.toThrow(SttError)
  122. })
  123. })
  124. describe('publish', () => {
  125. it('should publish tracks successfully', async () => {
  126. const config = { channel: 'test-channel', userId: 'test-user' }
  127. await manager.join(config)
  128. await manager.createTracks()
  129. await expect(manager.publish()).resolves.not.toThrow()
  130. })
  131. it('should throw error when not joined', async () => {
  132. await expect(manager.publish()).rejects.toThrow(SttError)
  133. })
  134. it('should throw error when tracks not created', async () => {
  135. const config = { channel: 'test-channel', userId: 'test-user' }
  136. await manager.join(config)
  137. await expect(manager.publish()).rejects.toThrow(SttError)
  138. })
  139. })
  140. describe('destroy', () => {
  141. it('should destroy manager successfully', async () => {
  142. const config = { channel: 'test-channel', userId: 'test-user' }
  143. await manager.join(config)
  144. await manager.destroy()
  145. expect(manager.isJoined).toBe(false)
  146. expect(manager.config).toBeUndefined()
  147. expect(manager.userId).toBe('')
  148. expect(manager.channel).toBe('')
  149. })
  150. it('should emit destroying and destroyed events', async () => {
  151. const config = { channel: 'test-channel', userId: 'test-user' }
  152. await manager.join(config)
  153. const destroyingSpy = vi.fn()
  154. const destroyedSpy = vi.fn()
  155. manager.on('destroying', destroyingSpy)
  156. manager.on('destroyed', destroyedSpy)
  157. await manager.destroy()
  158. expect(destroyingSpy).toHaveBeenCalled()
  159. expect(destroyedSpy).toHaveBeenCalled()
  160. })
  161. it('should handle destroy when not joined', async () => {
  162. await expect(manager.destroy()).resolves.not.toThrow()
  163. })
  164. })
  165. describe('event handling', () => {
  166. it('should handle network quality events', async () => {
  167. const config = { channel: 'test-channel', userId: 'test-user' }
  168. await manager.join(config)
  169. const networkQualitySpy = vi.fn()
  170. manager.on('networkQuality', networkQualitySpy)
  171. // 模拟网络质量事件
  172. const mockClient = await import('agora-rtc-sdk-ng')
  173. const client = mockClient.default.createClient()
  174. const networkQualityHandler = client.on.mock.calls.find(
  175. (call: any) => call[0] === 'network-quality'
  176. )?.[1]
  177. if (networkQualityHandler) {
  178. networkQualityHandler({ uplink: 5, downlink: 5 })
  179. expect(networkQualitySpy).toHaveBeenCalledWith({ uplink: 5, downlink: 5 })
  180. }
  181. })
  182. it('should handle user published events', async () => {
  183. const config = { channel: 'test-channel', userId: 'test-user' }
  184. await manager.join(config)
  185. const remoteUserChangedSpy = vi.fn()
  186. manager.on('remoteUserChanged', remoteUserChangedSpy)
  187. // 模拟用户发布事件
  188. const mockClient = await import('agora-rtc-sdk-ng')
  189. const client = mockClient.default.createClient()
  190. const userPublishedHandler = client.on.mock.calls.find(
  191. (call: any) => call[0] === 'user-published'
  192. )?.[1]
  193. if (userPublishedHandler) {
  194. const mockUser = { uid: 'remote-user', audioTrack: {}, videoTrack: {} }
  195. await userPublishedHandler(mockUser, 'audio')
  196. expect(remoteUserChangedSpy).toHaveBeenCalledWith({
  197. userId: 'remote-user',
  198. audioTrack: {},
  199. videoTrack: {},
  200. })
  201. }
  202. })
  203. })
  204. describe('audio device management', () => {
  205. describe('getAudioDevices', () => {
  206. it('should get audio devices successfully', async () => {
  207. const devices = await manager.getAudioDevices()
  208. expect(devices).toEqual({
  209. microphones: [
  210. { deviceId: 'mic1', kind: 'audioinput', label: '麦克风 1' },
  211. { deviceId: 'mic2', kind: 'audioinput', label: '麦克风 2' },
  212. ],
  213. speakers: [
  214. { deviceId: 'speaker1', kind: 'audiooutput', label: '扬声器 1' },
  215. { deviceId: 'speaker2', kind: 'audiooutput', label: '扬声器 2' },
  216. ],
  217. })
  218. })
  219. it('should handle errors when getting devices', async () => {
  220. // 模拟 getUserMedia 失败
  221. const mockNavigator = global.navigator as any
  222. mockNavigator.mediaDevices.getUserMedia = vi.fn(() =>
  223. Promise.reject(new Error('Permission denied'))
  224. )
  225. await expect(manager.getAudioDevices()).rejects.toThrow('Permission denied')
  226. })
  227. })
  228. describe('switchMicrophone', () => {
  229. it('should switch microphone successfully', async () => {
  230. const config = { channel: 'test-channel', userId: 'test-user' }
  231. await manager.join(config)
  232. await manager.createTracks()
  233. const audioDeviceChangedSpy = vi.fn()
  234. manager.on('audioDeviceChanged', audioDeviceChangedSpy)
  235. await manager.switchMicrophone('mic2')
  236. expect(audioDeviceChangedSpy).toHaveBeenCalledWith({
  237. deviceId: 'mic2',
  238. deviceType: 'microphone',
  239. })
  240. })
  241. it('should throw error when no audio track available', async () => {
  242. await expect(manager.switchMicrophone('mic1')).rejects.toThrow(SttError)
  243. })
  244. it('should throw error when not joined', async () => {
  245. const config = { channel: 'test-channel', userId: 'test-user' }
  246. await manager.join(config)
  247. await manager.createTracks()
  248. // 模拟 createMicrophoneAudioTrack 失败
  249. const mockAgoraRTC = await import('agora-rtc-sdk-ng')
  250. mockAgoraRTC.default.createMicrophoneAudioTrack = vi.fn(() =>
  251. Promise.reject(new Error('Device not found'))
  252. )
  253. await expect(manager.switchMicrophone('invalid-device')).rejects.toThrow('Device not found')
  254. })
  255. })
  256. describe('switchSpeaker', () => {
  257. it('should switch speaker successfully', async () => {
  258. const audioDeviceChangedSpy = vi.fn()
  259. manager.on('audioDeviceChanged', audioDeviceChangedSpy)
  260. await manager.switchSpeaker('speaker2')
  261. expect(audioDeviceChangedSpy).toHaveBeenCalledWith({
  262. deviceId: 'speaker2',
  263. deviceType: 'speaker',
  264. })
  265. })
  266. it('should handle errors when switching speaker', async () => {
  267. // 模拟 setPlaybackDevice 失败
  268. const mockAgoraRTC = await import('agora-rtc-sdk-ng')
  269. mockAgoraRTC.default.setPlaybackDevice = vi.fn(() =>
  270. Promise.reject(new Error('Device not found'))
  271. )
  272. await expect(manager.switchSpeaker('invalid-device')).rejects.toThrow('Device not found')
  273. })
  274. })
  275. describe('createTracks with device selection', () => {
  276. it('should create tracks with specific microphone device', async () => {
  277. const config = { channel: 'test-channel', userId: 'test-user' }
  278. await manager.join(config)
  279. const localUserChangedSpy = vi.fn()
  280. manager.on('localUserChanged', localUserChangedSpy)
  281. await manager.createTracks('mic2')
  282. expect(localUserChangedSpy).toHaveBeenCalled()
  283. // 验证 createMicrophoneAndCameraTracks 被调用时传入了正确的参数
  284. const mockAgoraRTC = await import('agora-rtc-sdk-ng')
  285. expect(mockAgoraRTC.default.createMicrophoneAndCameraTracks).toHaveBeenCalledWith({
  286. AGC: false,
  287. microphoneId: 'mic2',
  288. })
  289. })
  290. })
  291. })
  292. })