601 lines
		
	
	
		
			22 KiB
		
	
	
	
		
			TypeScript
		
	
	
			
		
		
	
	
			601 lines
		
	
	
		
			22 KiB
		
	
	
	
		
			TypeScript
		
	
	
| /*
 | |
| Copyright 2022 The Matrix.org Foundation C.I.C.
 | |
| 
 | |
| Licensed under the Apache License, Version 2.0 (the "License");
 | |
| you may not use this file except in compliance with the License.
 | |
| You may obtain a copy of the License at
 | |
| 
 | |
|     http://www.apache.org/licenses/LICENSE-2.0
 | |
| 
 | |
| Unless required by applicable law or agreed to in writing, software
 | |
| distributed under the License is distributed on an "AS IS" BASIS,
 | |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | |
| See the License for the specific language governing permissions and
 | |
| limitations under the License.
 | |
| */
 | |
| 
 | |
| import { mocked } from "jest-mock";
 | |
| import {
 | |
|     ClientEvent,
 | |
|     EventTimelineSet,
 | |
|     EventType,
 | |
|     MatrixClient,
 | |
|     MatrixEvent,
 | |
|     MatrixEventEvent,
 | |
|     MsgType,
 | |
|     RelationType,
 | |
|     Room,
 | |
| } from "matrix-js-sdk/src/matrix";
 | |
| import { Relations } from "matrix-js-sdk/src/models/relations";
 | |
| import { SyncState } from "matrix-js-sdk/src/sync";
 | |
| 
 | |
| import { uploadFile } from "../../../src/ContentMessages";
 | |
| import { IEncryptedFile } from "../../../src/customisations/models/IMediaEventContent";
 | |
| import { createVoiceMessageContent } from "../../../src/utils/createVoiceMessageContent";
 | |
| import {
 | |
|     createVoiceBroadcastRecorder,
 | |
|     getChunkLength,
 | |
|     getMaxBroadcastLength,
 | |
|     VoiceBroadcastInfoEventContent,
 | |
|     VoiceBroadcastInfoEventType,
 | |
|     VoiceBroadcastInfoState,
 | |
|     VoiceBroadcastRecorder,
 | |
|     VoiceBroadcastRecorderEvent,
 | |
|     VoiceBroadcastRecording,
 | |
|     VoiceBroadcastRecordingEvent,
 | |
|     VoiceBroadcastRecordingState,
 | |
| } from "../../../src/voice-broadcast";
 | |
| import { mkEvent, mkStubRoom, stubClient } from "../../test-utils";
 | |
| import dis from "../../../src/dispatcher/dispatcher";
 | |
| import { VoiceRecording } from "../../../src/audio/VoiceRecording";
 | |
| 
 | |
| jest.mock("../../../src/voice-broadcast/audio/VoiceBroadcastRecorder", () => ({
 | |
|     ...(jest.requireActual("../../../src/voice-broadcast/audio/VoiceBroadcastRecorder") as object),
 | |
|     createVoiceBroadcastRecorder: jest.fn(),
 | |
| }));
 | |
| 
 | |
| // mock VoiceRecording because it contains all the audio APIs
 | |
| jest.mock("../../../src/audio/VoiceRecording", () => ({
 | |
|     VoiceRecording: jest.fn().mockReturnValue({
 | |
|         disableMaxLength: jest.fn(),
 | |
|         liveData: {
 | |
|             onUpdate: jest.fn(),
 | |
|         },
 | |
|         off: jest.fn(),
 | |
|         on: jest.fn(),
 | |
|         start: jest.fn(),
 | |
|         stop: jest.fn(),
 | |
|         destroy: jest.fn(),
 | |
|         contentType: "audio/ogg",
 | |
|     }),
 | |
| }));
 | |
| 
 | |
| jest.mock("../../../src/ContentMessages", () => ({
 | |
|     uploadFile: jest.fn(),
 | |
| }));
 | |
| 
 | |
| jest.mock("../../../src/utils/createVoiceMessageContent", () => ({
 | |
|     createVoiceMessageContent: jest.fn(),
 | |
| }));
 | |
| 
 | |
| describe("VoiceBroadcastRecording", () => {
 | |
|     const roomId = "!room:example.com";
 | |
|     const uploadedUrl = "mxc://example.com/vb";
 | |
|     const uploadedFile = { file: true } as unknown as IEncryptedFile;
 | |
|     const maxLength = getMaxBroadcastLength();
 | |
|     let room: Room;
 | |
|     let client: MatrixClient;
 | |
|     let infoEvent: MatrixEvent;
 | |
|     let voiceBroadcastRecording: VoiceBroadcastRecording;
 | |
|     let onStateChanged: (state: VoiceBroadcastRecordingState) => void;
 | |
|     let voiceBroadcastRecorder: VoiceBroadcastRecorder;
 | |
| 
 | |
|     const mkVoiceBroadcastInfoEvent = (content: VoiceBroadcastInfoEventContent) => {
 | |
|         return mkEvent({
 | |
|             event: true,
 | |
|             type: VoiceBroadcastInfoEventType,
 | |
|             user: client.getSafeUserId(),
 | |
|             room: roomId,
 | |
|             content,
 | |
|         });
 | |
|     };
 | |
| 
 | |
|     const setUpVoiceBroadcastRecording = () => {
 | |
|         voiceBroadcastRecording = new VoiceBroadcastRecording(infoEvent, client);
 | |
|         voiceBroadcastRecording.on(VoiceBroadcastRecordingEvent.StateChanged, onStateChanged);
 | |
|         jest.spyOn(voiceBroadcastRecording, "destroy");
 | |
|         jest.spyOn(voiceBroadcastRecording, "emit");
 | |
|         jest.spyOn(voiceBroadcastRecording, "removeAllListeners");
 | |
|     };
 | |
| 
 | |
|     const itShouldBeInState = (state: VoiceBroadcastRecordingState) => {
 | |
|         it(`should be in state stopped ${state}`, () => {
 | |
|             expect(voiceBroadcastRecording.getState()).toBe(state);
 | |
|         });
 | |
|     };
 | |
| 
 | |
|     const emitFirsChunkRecorded = () => {
 | |
|         voiceBroadcastRecorder.emit(VoiceBroadcastRecorderEvent.ChunkRecorded, {
 | |
|             buffer: new Uint8Array([1, 2, 3]),
 | |
|             length: 23,
 | |
|         });
 | |
|     };
 | |
| 
 | |
|     const itShouldSendAnInfoEvent = (state: VoiceBroadcastInfoState, lastChunkSequence: number) => {
 | |
|         it(`should send a ${state} info event`, () => {
 | |
|             expect(client.sendStateEvent).toHaveBeenCalledWith(
 | |
|                 roomId,
 | |
|                 VoiceBroadcastInfoEventType,
 | |
|                 {
 | |
|                     device_id: client.getDeviceId(),
 | |
|                     state,
 | |
|                     last_chunk_sequence: lastChunkSequence,
 | |
|                     ["m.relates_to"]: {
 | |
|                         rel_type: RelationType.Reference,
 | |
|                         event_id: infoEvent.getId(),
 | |
|                     },
 | |
|                 } as VoiceBroadcastInfoEventContent,
 | |
|                 client.getUserId()!,
 | |
|             );
 | |
|         });
 | |
|     };
 | |
| 
 | |
|     const itShouldSendAVoiceMessage = (data: number[], size: number, duration: number, sequence: number) => {
 | |
|         // events contain milliseconds
 | |
|         duration *= 1000;
 | |
| 
 | |
|         it("should send a voice message", () => {
 | |
|             expect(uploadFile).toHaveBeenCalledWith(
 | |
|                 client,
 | |
|                 roomId,
 | |
|                 new Blob([new Uint8Array(data)], { type: voiceBroadcastRecorder.contentType }),
 | |
|             );
 | |
| 
 | |
|             expect(mocked(client.sendMessage)).toHaveBeenCalledWith(roomId, {
 | |
|                 body: "Voice message",
 | |
|                 file: {
 | |
|                     file: true,
 | |
|                 },
 | |
|                 info: {
 | |
|                     duration,
 | |
|                     mimetype: "audio/ogg",
 | |
|                     size,
 | |
|                 },
 | |
|                 ["m.relates_to"]: {
 | |
|                     event_id: infoEvent.getId(),
 | |
|                     rel_type: "m.reference",
 | |
|                 },
 | |
|                 msgtype: "m.audio",
 | |
|                 ["org.matrix.msc1767.audio"]: {
 | |
|                     duration,
 | |
|                     waveform: undefined,
 | |
|                 },
 | |
|                 ["org.matrix.msc1767.file"]: {
 | |
|                     file: {
 | |
|                         file: true,
 | |
|                     },
 | |
|                     mimetype: "audio/ogg",
 | |
|                     name: "Voice message.ogg",
 | |
|                     size,
 | |
|                     url: "mxc://example.com/vb",
 | |
|                 },
 | |
|                 ["org.matrix.msc1767.text"]: "Voice message",
 | |
|                 ["org.matrix.msc3245.voice"]: {},
 | |
|                 url: "mxc://example.com/vb",
 | |
|                 ["io.element.voice_broadcast_chunk"]: {
 | |
|                     sequence,
 | |
|                 },
 | |
|             });
 | |
|         });
 | |
|     };
 | |
| 
 | |
|     const setUpUploadFileMock = () => {
 | |
|         mocked(uploadFile).mockResolvedValue({
 | |
|             url: uploadedUrl,
 | |
|             file: uploadedFile,
 | |
|         });
 | |
|     };
 | |
| 
 | |
|     beforeEach(() => {
 | |
|         client = stubClient();
 | |
|         room = mkStubRoom(roomId, "Test Room", client);
 | |
|         mocked(client.getRoom).mockImplementation((getRoomId: string | undefined): Room | null => {
 | |
|             if (getRoomId === roomId) {
 | |
|                 return room;
 | |
|             }
 | |
| 
 | |
|             return null;
 | |
|         });
 | |
|         onStateChanged = jest.fn();
 | |
|         voiceBroadcastRecorder = new VoiceBroadcastRecorder(new VoiceRecording(), getChunkLength());
 | |
|         jest.spyOn(voiceBroadcastRecorder, "start");
 | |
|         jest.spyOn(voiceBroadcastRecorder, "stop");
 | |
|         jest.spyOn(voiceBroadcastRecorder, "destroy");
 | |
|         mocked(createVoiceBroadcastRecorder).mockReturnValue(voiceBroadcastRecorder);
 | |
| 
 | |
|         setUpUploadFileMock();
 | |
| 
 | |
|         mocked(createVoiceMessageContent).mockImplementation(
 | |
|             (
 | |
|                 mxc: string | undefined,
 | |
|                 mimetype: string,
 | |
|                 duration: number,
 | |
|                 size: number,
 | |
|                 file?: IEncryptedFile,
 | |
|                 waveform?: number[],
 | |
|             ) => {
 | |
|                 return {
 | |
|                     body: "Voice message",
 | |
|                     msgtype: MsgType.Audio,
 | |
|                     url: mxc,
 | |
|                     file,
 | |
|                     info: {
 | |
|                         duration,
 | |
|                         mimetype,
 | |
|                         size,
 | |
|                     },
 | |
|                     ["org.matrix.msc1767.text"]: "Voice message",
 | |
|                     ["org.matrix.msc1767.file"]: {
 | |
|                         url: mxc,
 | |
|                         file,
 | |
|                         name: "Voice message.ogg",
 | |
|                         mimetype,
 | |
|                         size,
 | |
|                     },
 | |
|                     ["org.matrix.msc1767.audio"]: {
 | |
|                         duration,
 | |
|                         // https://github.com/matrix-org/matrix-doc/pull/3246
 | |
|                         waveform,
 | |
|                     },
 | |
|                     ["org.matrix.msc3245.voice"]: {}, // No content, this is a rendering hint
 | |
|                 };
 | |
|             },
 | |
|         );
 | |
|     });
 | |
| 
 | |
|     afterEach(() => {
 | |
|         voiceBroadcastRecording?.off(VoiceBroadcastRecordingEvent.StateChanged, onStateChanged);
 | |
|     });
 | |
| 
 | |
|     describe("when there is an info event without id", () => {
 | |
|         beforeEach(() => {
 | |
|             infoEvent = mkVoiceBroadcastInfoEvent({
 | |
|                 device_id: client.getDeviceId()!,
 | |
|                 state: VoiceBroadcastInfoState.Started,
 | |
|             });
 | |
|             jest.spyOn(infoEvent, "getId").mockReturnValue(undefined);
 | |
|         });
 | |
| 
 | |
|         it("should raise an error when creating a broadcast", () => {
 | |
|             expect(() => {
 | |
|                 setUpVoiceBroadcastRecording();
 | |
|             }).toThrow("Cannot create broadcast for info event without Id.");
 | |
|         });
 | |
|     });
 | |
| 
 | |
|     describe("when there is an info event without room", () => {
 | |
|         beforeEach(() => {
 | |
|             infoEvent = mkVoiceBroadcastInfoEvent({
 | |
|                 device_id: client.getDeviceId()!,
 | |
|                 state: VoiceBroadcastInfoState.Started,
 | |
|             });
 | |
|             jest.spyOn(infoEvent, "getRoomId").mockReturnValue(undefined);
 | |
|         });
 | |
| 
 | |
|         it("should raise an error when creating a broadcast", () => {
 | |
|             expect(() => {
 | |
|                 setUpVoiceBroadcastRecording();
 | |
|             }).toThrow(`Cannot create broadcast for unknown room (info event ${infoEvent.getId()})`);
 | |
|         });
 | |
|     });
 | |
| 
 | |
|     describe("when created for a Voice Broadcast Info without relations", () => {
 | |
|         beforeEach(() => {
 | |
|             infoEvent = mkVoiceBroadcastInfoEvent({
 | |
|                 device_id: client.getDeviceId()!,
 | |
|                 state: VoiceBroadcastInfoState.Started,
 | |
|             });
 | |
|             setUpVoiceBroadcastRecording();
 | |
|         });
 | |
| 
 | |
|         it("should be in Started state", () => {
 | |
|             expect(voiceBroadcastRecording.getState()).toBe(VoiceBroadcastInfoState.Started);
 | |
|         });
 | |
| 
 | |
|         describe("and calling stop", () => {
 | |
|             beforeEach(() => {
 | |
|                 voiceBroadcastRecording.stop();
 | |
|             });
 | |
| 
 | |
|             itShouldSendAnInfoEvent(VoiceBroadcastInfoState.Stopped, 0);
 | |
|             itShouldBeInState(VoiceBroadcastInfoState.Stopped);
 | |
| 
 | |
|             it("should emit a stopped state changed event", () => {
 | |
|                 expect(onStateChanged).toHaveBeenCalledWith(VoiceBroadcastInfoState.Stopped);
 | |
|             });
 | |
|         });
 | |
| 
 | |
|         describe("and calling start", () => {
 | |
|             beforeEach(async () => {
 | |
|                 await voiceBroadcastRecording.start();
 | |
|             });
 | |
| 
 | |
|             it("should start the recorder", () => {
 | |
|                 expect(voiceBroadcastRecorder.start).toHaveBeenCalled();
 | |
|             });
 | |
| 
 | |
|             describe("and the info event is redacted", () => {
 | |
|                 beforeEach(() => {
 | |
|                     infoEvent.emit(
 | |
|                         MatrixEventEvent.BeforeRedaction,
 | |
|                         infoEvent,
 | |
|                         mkEvent({
 | |
|                             event: true,
 | |
|                             type: EventType.RoomRedaction,
 | |
|                             user: client.getSafeUserId(),
 | |
|                             content: {},
 | |
|                         }),
 | |
|                     );
 | |
|                 });
 | |
| 
 | |
|                 itShouldBeInState(VoiceBroadcastInfoState.Stopped);
 | |
| 
 | |
|                 it("should destroy the recording", () => {
 | |
|                     expect(voiceBroadcastRecording.destroy).toHaveBeenCalled();
 | |
|                 });
 | |
|             });
 | |
| 
 | |
|             describe("and receiving a call action", () => {
 | |
|                 beforeEach(() => {
 | |
|                     dis.dispatch(
 | |
|                         {
 | |
|                             action: "call_state",
 | |
|                         },
 | |
|                         true,
 | |
|                     );
 | |
|                 });
 | |
| 
 | |
|                 itShouldBeInState(VoiceBroadcastInfoState.Paused);
 | |
|             });
 | |
| 
 | |
|             describe("and a chunk time update occurs", () => {
 | |
|                 beforeEach(() => {
 | |
|                     voiceBroadcastRecorder.emit(VoiceBroadcastRecorderEvent.CurrentChunkLengthUpdated, 10);
 | |
|                 });
 | |
| 
 | |
|                 it("should update time left", () => {
 | |
|                     expect(voiceBroadcastRecording.getTimeLeft()).toBe(maxLength - 10);
 | |
|                     expect(voiceBroadcastRecording.emit).toHaveBeenCalledWith(
 | |
|                         VoiceBroadcastRecordingEvent.TimeLeftChanged,
 | |
|                         maxLength - 10,
 | |
|                     );
 | |
|                 });
 | |
| 
 | |
|                 describe("and a chunk time update occurs, that would increase time left", () => {
 | |
|                     beforeEach(() => {
 | |
|                         mocked(voiceBroadcastRecording.emit).mockClear();
 | |
|                         voiceBroadcastRecorder.emit(VoiceBroadcastRecorderEvent.CurrentChunkLengthUpdated, 5);
 | |
|                     });
 | |
| 
 | |
|                     it("should not change time left", () => {
 | |
|                         expect(voiceBroadcastRecording.getTimeLeft()).toBe(maxLength - 10);
 | |
|                         expect(voiceBroadcastRecording.emit).not.toHaveBeenCalled();
 | |
|                     });
 | |
|                 });
 | |
|             });
 | |
| 
 | |
|             describe("and a chunk has been recorded", () => {
 | |
|                 beforeEach(async () => {
 | |
|                     emitFirsChunkRecorded();
 | |
|                 });
 | |
| 
 | |
|                 itShouldSendAVoiceMessage([1, 2, 3], 3, 23, 1);
 | |
| 
 | |
|                 describe("and another chunk has been recorded, that exceeds the max time", () => {
 | |
|                     beforeEach(() => {
 | |
|                         mocked(voiceBroadcastRecorder.stop).mockResolvedValue({
 | |
|                             buffer: new Uint8Array([23, 24, 25]),
 | |
|                             length: getMaxBroadcastLength(),
 | |
|                         });
 | |
|                         voiceBroadcastRecorder.emit(
 | |
|                             VoiceBroadcastRecorderEvent.CurrentChunkLengthUpdated,
 | |
|                             getMaxBroadcastLength(),
 | |
|                         );
 | |
|                     });
 | |
| 
 | |
|                     itShouldBeInState(VoiceBroadcastInfoState.Stopped);
 | |
|                     itShouldSendAVoiceMessage([23, 24, 25], 3, getMaxBroadcastLength(), 2);
 | |
|                     itShouldSendAnInfoEvent(VoiceBroadcastInfoState.Stopped, 2);
 | |
|                 });
 | |
|             });
 | |
| 
 | |
|             describe("and calling stop", () => {
 | |
|                 beforeEach(async () => {
 | |
|                     mocked(voiceBroadcastRecorder.stop).mockResolvedValue({
 | |
|                         buffer: new Uint8Array([4, 5, 6]),
 | |
|                         length: 42,
 | |
|                     });
 | |
|                     await voiceBroadcastRecording.stop();
 | |
|                 });
 | |
| 
 | |
|                 itShouldSendAVoiceMessage([4, 5, 6], 3, 42, 1);
 | |
|                 itShouldSendAnInfoEvent(VoiceBroadcastInfoState.Stopped, 1);
 | |
|             });
 | |
| 
 | |
|             describe.each([
 | |
|                 ["pause", async () => voiceBroadcastRecording.pause()],
 | |
|                 ["toggle", async () => voiceBroadcastRecording.toggle()],
 | |
|             ])("and calling %s", (_case: string, action: Function) => {
 | |
|                 beforeEach(async () => {
 | |
|                     await action();
 | |
|                 });
 | |
| 
 | |
|                 itShouldBeInState(VoiceBroadcastInfoState.Paused);
 | |
|                 itShouldSendAnInfoEvent(VoiceBroadcastInfoState.Paused, 0);
 | |
| 
 | |
|                 it("should stop the recorder", () => {
 | |
|                     expect(mocked(voiceBroadcastRecorder.stop)).toHaveBeenCalled();
 | |
|                 });
 | |
| 
 | |
|                 it("should emit a paused state changed event", () => {
 | |
|                     expect(onStateChanged).toHaveBeenCalledWith(VoiceBroadcastInfoState.Paused);
 | |
|                 });
 | |
|             });
 | |
| 
 | |
|             describe("and there is no connection", () => {
 | |
|                 beforeEach(() => {
 | |
|                     mocked(client.sendStateEvent).mockImplementation(() => {
 | |
|                         throw new Error();
 | |
|                     });
 | |
|                 });
 | |
| 
 | |
|                 describe.each([
 | |
|                     ["pause", async () => voiceBroadcastRecording.pause()],
 | |
|                     ["toggle", async () => voiceBroadcastRecording.toggle()],
 | |
|                 ])("and calling %s", (_case: string, action: Function) => {
 | |
|                     beforeEach(async () => {
 | |
|                         await action();
 | |
|                     });
 | |
| 
 | |
|                     itShouldBeInState("connection_error");
 | |
| 
 | |
|                     describe("and the connection is back", () => {
 | |
|                         beforeEach(() => {
 | |
|                             mocked(client.sendStateEvent).mockResolvedValue({ event_id: "e1" });
 | |
|                             client.emit(ClientEvent.Sync, SyncState.Catchup, SyncState.Error);
 | |
|                         });
 | |
| 
 | |
|                         itShouldBeInState(VoiceBroadcastInfoState.Paused);
 | |
|                     });
 | |
|                 });
 | |
|             });
 | |
| 
 | |
|             describe("and calling destroy", () => {
 | |
|                 beforeEach(() => {
 | |
|                     voiceBroadcastRecording.destroy();
 | |
|                 });
 | |
| 
 | |
|                 it("should stop the recorder and remove all listeners", () => {
 | |
|                     expect(mocked(voiceBroadcastRecorder.stop)).toHaveBeenCalled();
 | |
|                     expect(mocked(voiceBroadcastRecorder.destroy)).toHaveBeenCalled();
 | |
|                     expect(mocked(voiceBroadcastRecording.removeAllListeners)).toHaveBeenCalled();
 | |
|                 });
 | |
|             });
 | |
| 
 | |
|             describe("and a chunk has been recorded and the upload fails", () => {
 | |
|                 beforeEach(() => {
 | |
|                     mocked(uploadFile).mockRejectedValue("Error");
 | |
|                     emitFirsChunkRecorded();
 | |
|                 });
 | |
| 
 | |
|                 itShouldBeInState("connection_error");
 | |
| 
 | |
|                 describe("and the connection is back", () => {
 | |
|                     beforeEach(() => {
 | |
|                         setUpUploadFileMock();
 | |
|                         client.emit(ClientEvent.Sync, SyncState.Catchup, SyncState.Error);
 | |
|                     });
 | |
| 
 | |
|                     itShouldBeInState(VoiceBroadcastInfoState.Paused);
 | |
|                     itShouldSendAVoiceMessage([1, 2, 3], 3, 23, 1);
 | |
|                 });
 | |
|             });
 | |
| 
 | |
|             describe("and a chunk has been recorded and sending the voice message fails", () => {
 | |
|                 beforeEach(() => {
 | |
|                     mocked(client.sendMessage).mockRejectedValue("Error");
 | |
|                     emitFirsChunkRecorded();
 | |
|                 });
 | |
| 
 | |
|                 itShouldBeInState("connection_error");
 | |
| 
 | |
|                 describe("and the connection is back", () => {
 | |
|                     beforeEach(() => {
 | |
|                         mocked(client.sendMessage).mockClear();
 | |
|                         mocked(client.sendMessage).mockResolvedValue({ event_id: "e23" });
 | |
|                         client.emit(ClientEvent.Sync, SyncState.Catchup, SyncState.Error);
 | |
|                     });
 | |
| 
 | |
|                     itShouldBeInState(VoiceBroadcastInfoState.Paused);
 | |
|                     itShouldSendAVoiceMessage([1, 2, 3], 3, 23, 1);
 | |
|                 });
 | |
|             });
 | |
|         });
 | |
| 
 | |
|         describe("and it is in paused state", () => {
 | |
|             beforeEach(async () => {
 | |
|                 await voiceBroadcastRecording.pause();
 | |
|             });
 | |
| 
 | |
|             describe.each([
 | |
|                 ["resume", async () => voiceBroadcastRecording.resume()],
 | |
|                 ["toggle", async () => voiceBroadcastRecording.toggle()],
 | |
|             ])("and calling %s", (_case: string, action: Function) => {
 | |
|                 beforeEach(async () => {
 | |
|                     await action();
 | |
|                 });
 | |
| 
 | |
|                 itShouldBeInState(VoiceBroadcastInfoState.Resumed);
 | |
|                 itShouldSendAnInfoEvent(VoiceBroadcastInfoState.Resumed, 0);
 | |
| 
 | |
|                 it("should start the recorder", () => {
 | |
|                     expect(mocked(voiceBroadcastRecorder.start)).toHaveBeenCalled();
 | |
|                 });
 | |
| 
 | |
|                 it(`should emit a ${VoiceBroadcastInfoState.Resumed} state changed event`, () => {
 | |
|                     expect(onStateChanged).toHaveBeenCalledWith(VoiceBroadcastInfoState.Resumed);
 | |
|                 });
 | |
|             });
 | |
|         });
 | |
|     });
 | |
| 
 | |
|     describe("when created for a Voice Broadcast Info with a Stopped relation", () => {
 | |
|         beforeEach(() => {
 | |
|             infoEvent = mkVoiceBroadcastInfoEvent({
 | |
|                 device_id: client.getDeviceId()!,
 | |
|                 state: VoiceBroadcastInfoState.Started,
 | |
|                 chunk_length: 120,
 | |
|             });
 | |
| 
 | |
|             const relationsContainer = {
 | |
|                 getRelations: jest.fn(),
 | |
|             } as unknown as Relations;
 | |
|             mocked(relationsContainer.getRelations).mockReturnValue([
 | |
|                 mkVoiceBroadcastInfoEvent({
 | |
|                     device_id: client.getDeviceId()!,
 | |
|                     state: VoiceBroadcastInfoState.Stopped,
 | |
|                     ["m.relates_to"]: {
 | |
|                         rel_type: RelationType.Reference,
 | |
|                         event_id: infoEvent.getId()!,
 | |
|                     },
 | |
|                 }),
 | |
|             ]);
 | |
| 
 | |
|             const timelineSet = {
 | |
|                 relations: {
 | |
|                     getChildEventsForEvent: jest
 | |
|                         .fn()
 | |
|                         .mockImplementation(
 | |
|                             (eventId: string, relationType: RelationType | string, eventType: EventType | string) => {
 | |
|                                 if (
 | |
|                                     eventId === infoEvent.getId() &&
 | |
|                                     relationType === RelationType.Reference &&
 | |
|                                     eventType === VoiceBroadcastInfoEventType
 | |
|                                 ) {
 | |
|                                     return relationsContainer;
 | |
|                                 }
 | |
|                             },
 | |
|                         ),
 | |
|                 },
 | |
|             } as unknown as EventTimelineSet;
 | |
|             mocked(room.getUnfilteredTimelineSet).mockReturnValue(timelineSet);
 | |
| 
 | |
|             setUpVoiceBroadcastRecording();
 | |
|         });
 | |
| 
 | |
|         it("should be in Stopped state", () => {
 | |
|             expect(voiceBroadcastRecording.getState()).toBe(VoiceBroadcastInfoState.Stopped);
 | |
|         });
 | |
|     });
 | |
| });
 |