Prepare for repo merge
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
parent
0f670b8dc0
commit
b084ff2313
807 changed files with 0 additions and 0 deletions
|
@ -0,0 +1,251 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { mocked } from "jest-mock";
|
||||
import { Optional } from "matrix-events-sdk";
|
||||
|
||||
import { VoiceRecording } from "../../../src/audio/VoiceRecording";
|
||||
import SdkConfig from "../../../src/SdkConfig";
|
||||
import { concat } from "../../../src/utils/arrays";
|
||||
import {
|
||||
ChunkRecordedPayload,
|
||||
createVoiceBroadcastRecorder,
|
||||
VoiceBroadcastRecorder,
|
||||
VoiceBroadcastRecorderEvent,
|
||||
} from "../../../src/voice-broadcast";
|
||||
|
||||
// mock VoiceRecording because it contains all the audio APIs
|
||||
jest.mock("../../../src/audio/VoiceRecording", () => ({
|
||||
VoiceRecording: jest.fn().mockReturnValue({
|
||||
disableMaxLength: jest.fn(),
|
||||
emit: jest.fn(),
|
||||
liveData: {
|
||||
onUpdate: jest.fn(),
|
||||
},
|
||||
start: jest.fn(),
|
||||
stop: jest.fn(),
|
||||
destroy: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock("../../../src/settings/SettingsStore");
|
||||
|
||||
describe("VoiceBroadcastRecorder", () => {
|
||||
describe("createVoiceBroadcastRecorder", () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(SdkConfig, "get").mockImplementation((key: string) => {
|
||||
if (key === "voice_broadcast") {
|
||||
return {
|
||||
chunk_length: 1337,
|
||||
};
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mocked(SdkConfig.get).mockRestore();
|
||||
});
|
||||
|
||||
it("should return a VoiceBroadcastRecorder instance with targetChunkLength from config", () => {
|
||||
const voiceBroadcastRecorder = createVoiceBroadcastRecorder();
|
||||
expect(voiceBroadcastRecorder).toBeInstanceOf(VoiceBroadcastRecorder);
|
||||
expect(voiceBroadcastRecorder.targetChunkLength).toBe(1337);
|
||||
});
|
||||
});
|
||||
|
||||
describe("instance", () => {
|
||||
const chunkLength = 30;
|
||||
// 0... OpusHead
|
||||
const headers1 = new Uint8Array([...Array(28).fill(0), 79, 112, 117, 115, 72, 101, 97, 100]);
|
||||
// 0... OpusTags
|
||||
const headers2 = new Uint8Array([...Array(28).fill(0), 79, 112, 117, 115, 84, 97, 103, 115]);
|
||||
const chunk1 = new Uint8Array([5, 6]);
|
||||
const chunk2a = new Uint8Array([7, 8]);
|
||||
const chunk2b = new Uint8Array([9, 10]);
|
||||
const contentType = "test content type";
|
||||
|
||||
let voiceRecording: VoiceRecording;
|
||||
let voiceBroadcastRecorder: VoiceBroadcastRecorder;
|
||||
let onChunkRecorded: (chunk: ChunkRecordedPayload) => void;
|
||||
|
||||
const simulateFirstChunk = (): void => {
|
||||
// send headers in wrong order and multiple times to test robustness for that
|
||||
voiceRecording.onDataAvailable!(headers2);
|
||||
voiceRecording.onDataAvailable!(headers1);
|
||||
voiceRecording.onDataAvailable!(headers1);
|
||||
voiceRecording.onDataAvailable!(headers2);
|
||||
// set recorder seconds to something greater than the test chunk length of 30
|
||||
// @ts-ignore
|
||||
voiceRecording.recorderSeconds = 42;
|
||||
voiceRecording.onDataAvailable!(chunk1);
|
||||
voiceRecording.onDataAvailable!(headers1);
|
||||
};
|
||||
|
||||
const expectOnFirstChunkRecorded = (): void => {
|
||||
expect(onChunkRecorded).toHaveBeenNthCalledWith(1, {
|
||||
buffer: concat(headers1, headers2, chunk1),
|
||||
length: 42,
|
||||
});
|
||||
};
|
||||
|
||||
const itShouldNotEmitAChunkRecordedEvent = (): void => {
|
||||
it("should not emit a ChunkRecorded event", (): void => {
|
||||
expect(voiceRecording.emit).not.toHaveBeenCalledWith(
|
||||
VoiceBroadcastRecorderEvent.ChunkRecorded,
|
||||
expect.anything(),
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
voiceRecording = new VoiceRecording();
|
||||
// @ts-ignore
|
||||
voiceRecording.recorderSeconds = 23;
|
||||
// @ts-ignore
|
||||
voiceRecording.contentType = contentType;
|
||||
|
||||
voiceBroadcastRecorder = new VoiceBroadcastRecorder(voiceRecording, chunkLength);
|
||||
jest.spyOn(voiceBroadcastRecorder, "removeAllListeners");
|
||||
onChunkRecorded = jest.fn();
|
||||
voiceBroadcastRecorder.on(VoiceBroadcastRecorderEvent.ChunkRecorded, onChunkRecorded);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
voiceBroadcastRecorder.destroy();
|
||||
});
|
||||
|
||||
it("start should forward the call to VoiceRecording.start", async () => {
|
||||
await voiceBroadcastRecorder.start();
|
||||
expect(voiceRecording.start).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe("stop", () => {
|
||||
beforeEach(async () => {
|
||||
await voiceBroadcastRecorder.stop();
|
||||
});
|
||||
|
||||
it("should forward the call to VoiceRecording.stop", async () => {
|
||||
expect(voiceRecording.stop).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
itShouldNotEmitAChunkRecordedEvent();
|
||||
});
|
||||
|
||||
describe("when calling destroy", () => {
|
||||
beforeEach(() => {
|
||||
voiceBroadcastRecorder.destroy();
|
||||
});
|
||||
|
||||
it("should call VoiceRecording.destroy", () => {
|
||||
expect(voiceRecording.destroy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should remove all listeners", () => {
|
||||
expect(voiceBroadcastRecorder.removeAllListeners).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it("contentType should return the value from VoiceRecording", () => {
|
||||
expect(voiceBroadcastRecorder.contentType).toBe(contentType);
|
||||
});
|
||||
|
||||
describe("when the first header from recorder has been received", () => {
|
||||
beforeEach(() => {
|
||||
voiceRecording.onDataAvailable!(headers1);
|
||||
});
|
||||
|
||||
itShouldNotEmitAChunkRecordedEvent();
|
||||
});
|
||||
|
||||
describe("when the second header from recorder has been received", () => {
|
||||
beforeEach(() => {
|
||||
voiceRecording.onDataAvailable!(headers1);
|
||||
voiceRecording.onDataAvailable!(headers2);
|
||||
});
|
||||
|
||||
itShouldNotEmitAChunkRecordedEvent();
|
||||
});
|
||||
|
||||
describe("when a third page from recorder has been received", () => {
|
||||
beforeEach(() => {
|
||||
voiceRecording.onDataAvailable!(headers1);
|
||||
voiceRecording.onDataAvailable!(headers2);
|
||||
voiceRecording.onDataAvailable!(chunk1);
|
||||
});
|
||||
|
||||
itShouldNotEmitAChunkRecordedEvent();
|
||||
|
||||
describe("and calling stop", () => {
|
||||
let stopPayload: Optional<ChunkRecordedPayload>;
|
||||
|
||||
beforeEach(async () => {
|
||||
stopPayload = await voiceBroadcastRecorder.stop();
|
||||
});
|
||||
|
||||
it("should return the remaining chunk", () => {
|
||||
expect(stopPayload).toEqual({
|
||||
buffer: concat(headers1, headers2, chunk1),
|
||||
length: 23,
|
||||
});
|
||||
});
|
||||
|
||||
describe("and calling start again and receiving some data", () => {
|
||||
beforeEach(() => {
|
||||
simulateFirstChunk();
|
||||
});
|
||||
|
||||
it("should emit the ChunkRecorded event for the first chunk", () => {
|
||||
expectOnFirstChunkRecorded();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("and calling stop() with recording.stop error)", () => {
|
||||
let stopPayload: Optional<ChunkRecordedPayload>;
|
||||
|
||||
beforeEach(async () => {
|
||||
mocked(voiceRecording.stop).mockRejectedValue("Error");
|
||||
stopPayload = await voiceBroadcastRecorder.stop();
|
||||
});
|
||||
|
||||
it("should return the remaining chunk", () => {
|
||||
expect(stopPayload).toEqual({
|
||||
buffer: concat(headers1, headers2, chunk1),
|
||||
length: 23,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when some chunks have been received", () => {
|
||||
beforeEach(() => {
|
||||
simulateFirstChunk();
|
||||
|
||||
// simulate a second chunk
|
||||
voiceRecording.onDataAvailable!(chunk2a);
|
||||
|
||||
// send headers again to test robustness for that
|
||||
voiceRecording.onDataAvailable!(headers2);
|
||||
|
||||
// add another 30 seconds for the next chunk
|
||||
// @ts-ignore
|
||||
voiceRecording.recorderSeconds = 72;
|
||||
voiceRecording.onDataAvailable!(chunk2b);
|
||||
});
|
||||
|
||||
it("should emit ChunkRecorded events", () => {
|
||||
expectOnFirstChunkRecorded();
|
||||
|
||||
expect(onChunkRecorded).toHaveBeenNthCalledWith(2, {
|
||||
buffer: concat(headers1, headers2, chunk2a, chunk2b),
|
||||
length: 72 - 42, // 72 (position at second chunk) - 42 (position of first chunk)
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,171 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React, { ReactElement } from "react";
|
||||
import { act, render, screen } from "jest-matrix-react";
|
||||
import { mocked } from "jest-mock";
|
||||
import { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import {
|
||||
VoiceBroadcastBody as UnwrappedVoiceBroadcastBody,
|
||||
VoiceBroadcastInfoState,
|
||||
VoiceBroadcastRecordingBody,
|
||||
VoiceBroadcastRecording,
|
||||
VoiceBroadcastPlaybackBody,
|
||||
VoiceBroadcastPlayback,
|
||||
VoiceBroadcastRecordingsStore,
|
||||
} from "../../../src/voice-broadcast";
|
||||
import { withClientContextRenderOptions, stubClient, wrapInSdkContext } from "../../test-utils";
|
||||
import { mkVoiceBroadcastInfoStateEvent } from "../utils/test-utils";
|
||||
import { MediaEventHelper } from "../../../src/utils/MediaEventHelper";
|
||||
import { RoomPermalinkCreator } from "../../../src/utils/permalinks/Permalinks";
|
||||
import { SdkContextClass } from "../../../src/contexts/SDKContext";
|
||||
|
||||
jest.mock("../../../src/voice-broadcast/components/molecules/VoiceBroadcastRecordingBody", () => ({
|
||||
VoiceBroadcastRecordingBody: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("../../../src/voice-broadcast/components/molecules/VoiceBroadcastPlaybackBody", () => ({
|
||||
VoiceBroadcastPlaybackBody: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("../../../src/utils/permalinks/Permalinks");
|
||||
jest.mock("../../../src/utils/MediaEventHelper");
|
||||
jest.mock("../../../src/stores/WidgetStore");
|
||||
jest.mock("../../../src/stores/widgets/WidgetLayoutStore");
|
||||
|
||||
describe("VoiceBroadcastBody", () => {
|
||||
const roomId = "!room:example.com";
|
||||
let userId: string;
|
||||
let deviceId: string;
|
||||
let client: MatrixClient;
|
||||
let room: Room;
|
||||
let infoEvent: MatrixEvent;
|
||||
let stoppedEvent: MatrixEvent;
|
||||
let testRecording: VoiceBroadcastRecording;
|
||||
let testPlayback: VoiceBroadcastPlayback;
|
||||
|
||||
const renderVoiceBroadcast = () => {
|
||||
const VoiceBroadcastBody = wrapInSdkContext(UnwrappedVoiceBroadcastBody, SdkContextClass.instance);
|
||||
render(
|
||||
<VoiceBroadcastBody
|
||||
mxEvent={infoEvent}
|
||||
mediaEventHelper={new MediaEventHelper(infoEvent)}
|
||||
onHeightChanged={() => {}}
|
||||
onMessageAllowed={() => {}}
|
||||
permalinkCreator={new RoomPermalinkCreator(room)}
|
||||
/>,
|
||||
withClientContextRenderOptions(client),
|
||||
);
|
||||
testRecording = SdkContextClass.instance.voiceBroadcastRecordingsStore.getByInfoEvent(infoEvent, client);
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
client = stubClient();
|
||||
userId = client.getUserId() || "";
|
||||
deviceId = client.getDeviceId() || "";
|
||||
mocked(client.relations).mockClear();
|
||||
mocked(client.relations).mockResolvedValue({ events: [] });
|
||||
room = new Room(roomId, client, userId);
|
||||
mocked(client.getRoom).mockImplementation((getRoomId?: string) => {
|
||||
if (getRoomId === roomId) return room;
|
||||
return null;
|
||||
});
|
||||
|
||||
infoEvent = mkVoiceBroadcastInfoStateEvent(roomId, VoiceBroadcastInfoState.Started, userId, deviceId);
|
||||
stoppedEvent = mkVoiceBroadcastInfoStateEvent(
|
||||
roomId,
|
||||
VoiceBroadcastInfoState.Stopped,
|
||||
userId,
|
||||
deviceId,
|
||||
infoEvent,
|
||||
);
|
||||
room.addEventsToTimeline([infoEvent], true, room.getLiveTimeline());
|
||||
testRecording = new VoiceBroadcastRecording(infoEvent, client);
|
||||
testPlayback = new VoiceBroadcastPlayback(infoEvent, client, new VoiceBroadcastRecordingsStore());
|
||||
mocked(VoiceBroadcastRecordingBody).mockImplementation(({ recording }): ReactElement | null => {
|
||||
if (testRecording === recording) {
|
||||
return <div data-testid="voice-broadcast-recording-body" />;
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
mocked(VoiceBroadcastPlaybackBody).mockImplementation(({ playback }): ReactElement | null => {
|
||||
if (testPlayback === playback) {
|
||||
return <div data-testid="voice-broadcast-playback-body" />;
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
jest.spyOn(SdkContextClass.instance.voiceBroadcastRecordingsStore, "getByInfoEvent").mockImplementation(
|
||||
(getEvent: MatrixEvent, getClient: MatrixClient): VoiceBroadcastRecording => {
|
||||
if (getEvent === infoEvent && getClient === client) {
|
||||
return testRecording;
|
||||
}
|
||||
|
||||
throw new Error("unexpected event");
|
||||
},
|
||||
);
|
||||
|
||||
jest.spyOn(SdkContextClass.instance.voiceBroadcastPlaybacksStore, "getByInfoEvent").mockImplementation(
|
||||
(getEvent: MatrixEvent): VoiceBroadcastPlayback => {
|
||||
if (getEvent === infoEvent) {
|
||||
return testPlayback;
|
||||
}
|
||||
|
||||
throw new Error("unexpected event");
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("when there is a stopped voice broadcast", () => {
|
||||
beforeEach(() => {
|
||||
room.addEventsToTimeline([stoppedEvent], true, room.getLiveTimeline());
|
||||
renderVoiceBroadcast();
|
||||
});
|
||||
|
||||
it("should render a voice broadcast playback body", () => {
|
||||
screen.getByTestId("voice-broadcast-playback-body");
|
||||
});
|
||||
});
|
||||
|
||||
describe("when there is a started voice broadcast from the current user", () => {
|
||||
beforeEach(() => {
|
||||
renderVoiceBroadcast();
|
||||
});
|
||||
|
||||
it("should render a voice broadcast recording body", () => {
|
||||
screen.getByTestId("voice-broadcast-recording-body");
|
||||
});
|
||||
|
||||
describe("and the recordings ends", () => {
|
||||
beforeEach(() => {
|
||||
act(() => {
|
||||
room.addEventsToTimeline([stoppedEvent], true, room.getLiveTimeline());
|
||||
});
|
||||
});
|
||||
|
||||
it("should render a voice broadcast playback body", () => {
|
||||
screen.getByTestId("voice-broadcast-playback-body");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when displaying a voice broadcast playback", () => {
|
||||
beforeEach(() => {
|
||||
mocked(client).getUserId.mockReturnValue("@other:example.com");
|
||||
renderVoiceBroadcast();
|
||||
});
|
||||
|
||||
it("should render a voice broadcast playback body", () => {
|
||||
screen.getByTestId("voice-broadcast-playback-body");
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { render } from "jest-matrix-react";
|
||||
|
||||
import { LiveBadge } from "../../../../src/voice-broadcast";
|
||||
|
||||
describe("LiveBadge", () => {
|
||||
it("should render as expected with default props", () => {
|
||||
const { container } = render(<LiveBadge />);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should render in grey as expected", () => {
|
||||
const { container } = render(<LiveBadge grey={true} />);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { render, RenderResult, screen } from "jest-matrix-react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
|
||||
import { VoiceBroadcastControl } from "../../../../src/voice-broadcast";
|
||||
import { Icon as StopIcon } from "../../../../res/img/compound/stop-16.svg";
|
||||
|
||||
describe("VoiceBroadcastControl", () => {
|
||||
let result: RenderResult;
|
||||
let onClick: () => void;
|
||||
|
||||
beforeEach(() => {
|
||||
onClick = jest.fn();
|
||||
});
|
||||
|
||||
describe("when rendering it", () => {
|
||||
beforeEach(() => {
|
||||
const stopIcon = <StopIcon className="mx_Icon mx_Icon_16" />;
|
||||
result = render(<VoiceBroadcastControl onClick={onClick} label="test label" icon={stopIcon} />);
|
||||
});
|
||||
|
||||
it("should render as expected", () => {
|
||||
expect(result.container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe("when clicking it", () => {
|
||||
beforeEach(async () => {
|
||||
await userEvent.click(screen.getByLabelText("test label"));
|
||||
});
|
||||
|
||||
it("should call onClick", () => {
|
||||
expect(onClick).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { MatrixClient, Room, RoomMember } from "matrix-js-sdk/src/matrix";
|
||||
import { render, RenderResult } from "jest-matrix-react";
|
||||
|
||||
import { VoiceBroadcastHeader, VoiceBroadcastLiveness } from "../../../../src/voice-broadcast";
|
||||
import { mkRoom, stubClient } from "../../../test-utils";
|
||||
|
||||
// mock RoomAvatar, because it is doing too much fancy stuff
|
||||
jest.mock("../../../../src/components/views/avatars/RoomAvatar", () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn().mockImplementation(({ room }) => {
|
||||
return <div data-testid="room-avatar">room avatar: {room.name}</div>;
|
||||
}),
|
||||
}));
|
||||
|
||||
describe("VoiceBroadcastHeader", () => {
|
||||
const userId = "@user:example.com";
|
||||
const roomId = "!room:example.com";
|
||||
let client: MatrixClient;
|
||||
let room: Room;
|
||||
const sender = new RoomMember(roomId, userId);
|
||||
let container: RenderResult["container"];
|
||||
|
||||
const renderHeader = (live: VoiceBroadcastLiveness, showBroadcast?: boolean, buffering?: boolean): RenderResult => {
|
||||
return render(
|
||||
<VoiceBroadcastHeader
|
||||
live={live}
|
||||
microphoneLabel={sender.name}
|
||||
room={room}
|
||||
showBroadcast={showBroadcast}
|
||||
showBuffering={buffering}
|
||||
/>,
|
||||
);
|
||||
};
|
||||
|
||||
beforeAll(() => {
|
||||
client = stubClient();
|
||||
room = mkRoom(client, roomId);
|
||||
sender.name = "test user";
|
||||
});
|
||||
|
||||
describe("when rendering a live broadcast header with broadcast info", () => {
|
||||
beforeEach(() => {
|
||||
container = renderHeader("live", true, true).container;
|
||||
});
|
||||
|
||||
it("should render the header with a red live badge", () => {
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe("when rendering a buffering live broadcast header with broadcast info", () => {
|
||||
beforeEach(() => {
|
||||
container = renderHeader("live", true).container;
|
||||
});
|
||||
|
||||
it("should render the header with a red live badge", () => {
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe("when rendering a live (grey) broadcast header with broadcast info", () => {
|
||||
beforeEach(() => {
|
||||
container = renderHeader("grey", true).container;
|
||||
});
|
||||
|
||||
it("should render the header with a grey live badge", () => {
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe("when rendering a non-live broadcast header", () => {
|
||||
beforeEach(() => {
|
||||
container = renderHeader("not-live").container;
|
||||
});
|
||||
|
||||
it("should render the header without a live badge", () => {
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022, 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { render, RenderResult, screen } from "jest-matrix-react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
|
||||
import { VoiceBroadcastPlaybackControl, VoiceBroadcastPlaybackState } from "../../../../src/voice-broadcast";
|
||||
|
||||
describe("<VoiceBroadcastPlaybackControl />", () => {
|
||||
const renderControl = (state: VoiceBroadcastPlaybackState): { result: RenderResult; onClick: () => void } => {
|
||||
const onClick = jest.fn();
|
||||
return {
|
||||
onClick,
|
||||
result: render(<VoiceBroadcastPlaybackControl state={state} onClick={onClick} />),
|
||||
};
|
||||
};
|
||||
|
||||
it.each([
|
||||
VoiceBroadcastPlaybackState.Stopped,
|
||||
VoiceBroadcastPlaybackState.Paused,
|
||||
VoiceBroadcastPlaybackState.Buffering,
|
||||
VoiceBroadcastPlaybackState.Playing,
|
||||
])("should render state %s as expected", (state: VoiceBroadcastPlaybackState) => {
|
||||
expect(renderControl(state).result.container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should not render for error state", () => {
|
||||
expect(renderControl(VoiceBroadcastPlaybackState.Error).result.asFragment()).toMatchInlineSnapshot(
|
||||
`<DocumentFragment />`,
|
||||
);
|
||||
});
|
||||
|
||||
describe("when clicking the control", () => {
|
||||
let onClick: () => void;
|
||||
|
||||
beforeEach(async () => {
|
||||
onClick = renderControl(VoiceBroadcastPlaybackState.Playing).onClick;
|
||||
await userEvent.click(screen.getByLabelText("pause voice broadcast"));
|
||||
});
|
||||
|
||||
it("should invoke the onClick callback", () => {
|
||||
expect(onClick).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,27 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`LiveBadge should render as expected with default props 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_LiveBadge"
|
||||
>
|
||||
<div
|
||||
class="mx_Icon mx_Icon_16"
|
||||
/>
|
||||
Live
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`LiveBadge should render in grey as expected 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_LiveBadge mx_LiveBadge--grey"
|
||||
>
|
||||
<div
|
||||
class="mx_Icon mx_Icon_16"
|
||||
/>
|
||||
Live
|
||||
</div>
|
||||
</div>
|
||||
`;
|
|
@ -0,0 +1,16 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`VoiceBroadcastControl when rendering it should render as expected 1`] = `
|
||||
<div>
|
||||
<div
|
||||
aria-label="test label"
|
||||
class="mx_AccessibleButton mx_VoiceBroadcastControl"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="mx_Icon mx_Icon_16"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
|
@ -0,0 +1,277 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`VoiceBroadcastHeader when rendering a buffering live broadcast header with broadcast info should render the header with a red live badge 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader"
|
||||
>
|
||||
<div
|
||||
data-testid="room-avatar"
|
||||
>
|
||||
room avatar:
|
||||
!room:example.com
|
||||
</div>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader_content"
|
||||
>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader_room_wrapper"
|
||||
>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader_room"
|
||||
>
|
||||
!room:example.com
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Change input device"
|
||||
class="mx_AccessibleButton mx_VoiceBroadcastHeader_line"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
class="mx_Icon mx_Icon_16"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M8 6a4 4 0 1 1 8 0v6a4 4 0 0 1-8 0V6Z"
|
||||
/>
|
||||
<path
|
||||
d="M5 11a1 1 0 0 1 1 1 6 6 0 0 0 12 0 1 1 0 1 1 2 0 8.001 8.001 0 0 1-7 7.938V21a1 1 0 1 1-2 0v-1.062A8.001 8.001 0 0 1 4 12a1 1 0 0 1 1-1Z"
|
||||
/>
|
||||
</svg>
|
||||
<span>
|
||||
test user
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader_line"
|
||||
>
|
||||
<div
|
||||
class="mx_Icon mx_Icon_16"
|
||||
/>
|
||||
Voice broadcast
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_LiveBadge"
|
||||
>
|
||||
<div
|
||||
class="mx_Icon mx_Icon_16"
|
||||
/>
|
||||
Live
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`VoiceBroadcastHeader when rendering a live (grey) broadcast header with broadcast info should render the header with a grey live badge 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader"
|
||||
>
|
||||
<div
|
||||
data-testid="room-avatar"
|
||||
>
|
||||
room avatar:
|
||||
!room:example.com
|
||||
</div>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader_content"
|
||||
>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader_room_wrapper"
|
||||
>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader_room"
|
||||
>
|
||||
!room:example.com
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Change input device"
|
||||
class="mx_AccessibleButton mx_VoiceBroadcastHeader_line"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
class="mx_Icon mx_Icon_16"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M8 6a4 4 0 1 1 8 0v6a4 4 0 0 1-8 0V6Z"
|
||||
/>
|
||||
<path
|
||||
d="M5 11a1 1 0 0 1 1 1 6 6 0 0 0 12 0 1 1 0 1 1 2 0 8.001 8.001 0 0 1-7 7.938V21a1 1 0 1 1-2 0v-1.062A8.001 8.001 0 0 1 4 12a1 1 0 0 1 1-1Z"
|
||||
/>
|
||||
</svg>
|
||||
<span>
|
||||
test user
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader_line"
|
||||
>
|
||||
<div
|
||||
class="mx_Icon mx_Icon_16"
|
||||
/>
|
||||
Voice broadcast
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_LiveBadge mx_LiveBadge--grey"
|
||||
>
|
||||
<div
|
||||
class="mx_Icon mx_Icon_16"
|
||||
/>
|
||||
Live
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`VoiceBroadcastHeader when rendering a live broadcast header with broadcast info should render the header with a red live badge 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader"
|
||||
>
|
||||
<div
|
||||
data-testid="room-avatar"
|
||||
>
|
||||
room avatar:
|
||||
!room:example.com
|
||||
</div>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader_content"
|
||||
>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader_room_wrapper"
|
||||
>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader_room"
|
||||
>
|
||||
!room:example.com
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Change input device"
|
||||
class="mx_AccessibleButton mx_VoiceBroadcastHeader_line"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
class="mx_Icon mx_Icon_16"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M8 6a4 4 0 1 1 8 0v6a4 4 0 0 1-8 0V6Z"
|
||||
/>
|
||||
<path
|
||||
d="M5 11a1 1 0 0 1 1 1 6 6 0 0 0 12 0 1 1 0 1 1 2 0 8.001 8.001 0 0 1-7 7.938V21a1 1 0 1 1-2 0v-1.062A8.001 8.001 0 0 1 4 12a1 1 0 0 1 1-1Z"
|
||||
/>
|
||||
</svg>
|
||||
<span>
|
||||
test user
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader_line"
|
||||
>
|
||||
<div
|
||||
class="mx_Icon mx_Icon_16"
|
||||
/>
|
||||
Voice broadcast
|
||||
</div>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader_line"
|
||||
>
|
||||
<div
|
||||
class="mx_Spinner"
|
||||
>
|
||||
<div
|
||||
aria-label="Loading…"
|
||||
class="mx_Spinner_icon"
|
||||
data-testid="spinner"
|
||||
role="progressbar"
|
||||
style="width: 14px; height: 14px;"
|
||||
/>
|
||||
</div>
|
||||
Buffering…
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_LiveBadge"
|
||||
>
|
||||
<div
|
||||
class="mx_Icon mx_Icon_16"
|
||||
/>
|
||||
Live
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`VoiceBroadcastHeader when rendering a non-live broadcast header should render the header without a live badge 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader"
|
||||
>
|
||||
<div
|
||||
data-testid="room-avatar"
|
||||
>
|
||||
room avatar:
|
||||
!room:example.com
|
||||
</div>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader_content"
|
||||
>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader_room_wrapper"
|
||||
>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader_room"
|
||||
>
|
||||
!room:example.com
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Change input device"
|
||||
class="mx_AccessibleButton mx_VoiceBroadcastHeader_line"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
class="mx_Icon mx_Icon_16"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M8 6a4 4 0 1 1 8 0v6a4 4 0 0 1-8 0V6Z"
|
||||
/>
|
||||
<path
|
||||
d="M5 11a1 1 0 0 1 1 1 6 6 0 0 0 12 0 1 1 0 1 1 2 0 8.001 8.001 0 0 1-7 7.938V21a1 1 0 1 1-2 0v-1.062A8.001 8.001 0 0 1 4 12a1 1 0 0 1 1-1Z"
|
||||
/>
|
||||
</svg>
|
||||
<span>
|
||||
test user
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
|
@ -0,0 +1,97 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<VoiceBroadcastPlaybackControl /> should render state buffering as expected 1`] = `
|
||||
<div>
|
||||
<div
|
||||
aria-label="pause voice broadcast"
|
||||
class="mx_AccessibleButton mx_VoiceBroadcastControl"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
class="mx_Icon mx_Icon_12"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M8 4a2 2 0 0 0-2 2v12a2 2 0 1 0 4 0V6a2 2 0 0 0-2-2Zm8 0a2 2 0 0 0-2 2v12a2 2 0 1 0 4 0V6a2 2 0 0 0-2-2Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<VoiceBroadcastPlaybackControl /> should render state pause as expected 1`] = `
|
||||
<div>
|
||||
<div
|
||||
aria-label="resume voice broadcast"
|
||||
class="mx_AccessibleButton mx_VoiceBroadcastControl mx_VoiceBroadcastControl-play"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
class="mx_Icon mx_Icon_16"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="m8.98 4.677 9.921 5.58c1.36.764 1.36 2.722 0 3.486l-9.92 5.58C7.647 20.073 6 19.11 6 17.58V6.42c0-1.53 1.647-2.493 2.98-1.743Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<VoiceBroadcastPlaybackControl /> should render state playing as expected 1`] = `
|
||||
<div>
|
||||
<div
|
||||
aria-label="pause voice broadcast"
|
||||
class="mx_AccessibleButton mx_VoiceBroadcastControl"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
class="mx_Icon mx_Icon_12"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M8 4a2 2 0 0 0-2 2v12a2 2 0 1 0 4 0V6a2 2 0 0 0-2-2Zm8 0a2 2 0 0 0-2 2v12a2 2 0 1 0 4 0V6a2 2 0 0 0-2-2Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<VoiceBroadcastPlaybackControl /> should render state stopped as expected 1`] = `
|
||||
<div>
|
||||
<div
|
||||
aria-label="play voice broadcast"
|
||||
class="mx_AccessibleButton mx_VoiceBroadcastControl mx_VoiceBroadcastControl-play"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
class="mx_Icon mx_Icon_16"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="m8.98 4.677 9.921 5.58c1.36.764 1.36 2.722 0 3.486l-9.92 5.58C7.647 20.073 6 19.11 6 17.58V6.42c0-1.53 1.647-2.493 2.98-1.743Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
|
@ -0,0 +1,249 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022, 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||
import { act, render, RenderResult, screen } from "jest-matrix-react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { mocked } from "jest-mock";
|
||||
|
||||
import {
|
||||
VoiceBroadcastInfoState,
|
||||
VoiceBroadcastLiveness,
|
||||
VoiceBroadcastPlayback,
|
||||
VoiceBroadcastPlaybackBody,
|
||||
VoiceBroadcastPlaybackEvent,
|
||||
VoiceBroadcastPlaybackState,
|
||||
} from "../../../../src/voice-broadcast";
|
||||
import { filterConsole, stubClient } from "../../../test-utils";
|
||||
import { mkVoiceBroadcastInfoStateEvent } from "../../utils/test-utils";
|
||||
import dis from "../../../../src/dispatcher/dispatcher";
|
||||
import { Action } from "../../../../src/dispatcher/actions";
|
||||
import { SdkContextClass } from "../../../../src/contexts/SDKContext";
|
||||
|
||||
jest.mock("../../../../src/dispatcher/dispatcher");
|
||||
|
||||
// mock RoomAvatar, because it is doing too much fancy stuff
|
||||
jest.mock("../../../../src/components/views/avatars/RoomAvatar", () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn().mockImplementation(({ room }) => {
|
||||
return <div data-testid="room-avatar">room avatar: {room.name}</div>;
|
||||
}),
|
||||
}));
|
||||
|
||||
describe("VoiceBroadcastPlaybackBody", () => {
|
||||
const userId = "@user:example.com";
|
||||
const roomId = "!room:example.com";
|
||||
const duration = 23 * 60 + 42; // 23:42
|
||||
let client: MatrixClient;
|
||||
let infoEvent: MatrixEvent;
|
||||
let playback: VoiceBroadcastPlayback;
|
||||
let renderResult: RenderResult;
|
||||
|
||||
filterConsole(
|
||||
// expected for some tests
|
||||
"voice broadcast chunk event to skip to not found",
|
||||
);
|
||||
|
||||
beforeAll(() => {
|
||||
client = stubClient();
|
||||
mocked(client.relations).mockClear();
|
||||
mocked(client.relations).mockResolvedValue({ events: [] });
|
||||
|
||||
infoEvent = mkVoiceBroadcastInfoStateEvent(
|
||||
roomId,
|
||||
VoiceBroadcastInfoState.Stopped,
|
||||
userId,
|
||||
client.getDeviceId(),
|
||||
);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
playback = new VoiceBroadcastPlayback(
|
||||
infoEvent,
|
||||
client,
|
||||
SdkContextClass.instance.voiceBroadcastRecordingsStore,
|
||||
);
|
||||
jest.spyOn(playback, "toggle").mockImplementation(() => Promise.resolve());
|
||||
jest.spyOn(playback, "getLiveness");
|
||||
jest.spyOn(playback, "getState");
|
||||
jest.spyOn(playback, "skipTo");
|
||||
jest.spyOn(playback, "durationSeconds", "get").mockReturnValue(duration);
|
||||
});
|
||||
|
||||
describe("when rendering a buffering voice broadcast", () => {
|
||||
beforeEach(() => {
|
||||
mocked(playback.getState).mockReturnValue(VoiceBroadcastPlaybackState.Buffering);
|
||||
mocked(playback.getLiveness).mockReturnValue("live");
|
||||
renderResult = render(<VoiceBroadcastPlaybackBody playback={playback} />);
|
||||
});
|
||||
|
||||
it("should render as expected", () => {
|
||||
expect(renderResult.container).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe("when rendering a playing broadcast", () => {
|
||||
beforeEach(() => {
|
||||
mocked(playback.getState).mockReturnValue(VoiceBroadcastPlaybackState.Playing);
|
||||
mocked(playback.getLiveness).mockReturnValue("not-live");
|
||||
renderResult = render(<VoiceBroadcastPlaybackBody playback={playback} />);
|
||||
});
|
||||
|
||||
it("should render as expected", () => {
|
||||
expect(renderResult.container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe("and being in the middle of the playback", () => {
|
||||
beforeEach(() => {
|
||||
act(() => {
|
||||
playback.emit(VoiceBroadcastPlaybackEvent.TimesChanged, {
|
||||
duration,
|
||||
position: 10 * 60,
|
||||
timeLeft: duration - 10 * 60,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("and clicking 30s backward", () => {
|
||||
beforeEach(async () => {
|
||||
await act(async () => {
|
||||
await userEvent.click(screen.getByLabelText("30s backward"));
|
||||
});
|
||||
});
|
||||
|
||||
it("should seek 30s backward", () => {
|
||||
expect(playback.skipTo).toHaveBeenCalledWith(9 * 60 + 30);
|
||||
});
|
||||
});
|
||||
|
||||
describe("and clicking 30s forward", () => {
|
||||
beforeEach(async () => {
|
||||
await act(async () => {
|
||||
await userEvent.click(screen.getByLabelText("30s forward"));
|
||||
});
|
||||
});
|
||||
|
||||
it("should seek 30s forward", () => {
|
||||
expect(playback.skipTo).toHaveBeenCalledWith(10 * 60 + 30);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("and clicking the room name", () => {
|
||||
beforeEach(async () => {
|
||||
await userEvent.click(screen.getByText("My room"));
|
||||
});
|
||||
|
||||
it("should not view the room", () => {
|
||||
expect(dis.dispatch).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when rendering a playing broadcast in pip mode", () => {
|
||||
beforeEach(() => {
|
||||
mocked(playback.getState).mockReturnValue(VoiceBroadcastPlaybackState.Playing);
|
||||
mocked(playback.getLiveness).mockReturnValue("not-live");
|
||||
renderResult = render(<VoiceBroadcastPlaybackBody pip={true} playback={playback} />);
|
||||
});
|
||||
|
||||
it("should render as expected", () => {
|
||||
expect(renderResult.container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe("and clicking the room name", () => {
|
||||
beforeEach(async () => {
|
||||
await userEvent.click(screen.getByText("My room"));
|
||||
});
|
||||
|
||||
it("should view the room", () => {
|
||||
expect(dis.dispatch).toHaveBeenCalledWith({
|
||||
action: Action.ViewRoom,
|
||||
room_id: roomId,
|
||||
metricsTrigger: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe(`when rendering a stopped broadcast`, () => {
|
||||
beforeEach(() => {
|
||||
mocked(playback.getState).mockReturnValue(VoiceBroadcastPlaybackState.Stopped);
|
||||
mocked(playback.getLiveness).mockReturnValue("not-live");
|
||||
renderResult = render(<VoiceBroadcastPlaybackBody playback={playback} />);
|
||||
});
|
||||
|
||||
it("should render as expected", () => {
|
||||
expect(renderResult.container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe("and clicking the play button", () => {
|
||||
beforeEach(async () => {
|
||||
await userEvent.click(renderResult.getByLabelText("play voice broadcast"));
|
||||
});
|
||||
|
||||
it("should toggle the recording", () => {
|
||||
expect(playback.toggle).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("and the times update", () => {
|
||||
beforeEach(() => {
|
||||
act(() => {
|
||||
playback.emit(VoiceBroadcastPlaybackEvent.TimesChanged, {
|
||||
duration,
|
||||
position: 5 * 60 + 13,
|
||||
timeLeft: 7 * 60 + 5,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should render the times", async () => {
|
||||
expect(await screen.findByText("05:13")).toBeInTheDocument();
|
||||
expect(await screen.findByText("-07:05")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when rendering an error broadcast", () => {
|
||||
beforeEach(() => {
|
||||
mocked(playback.getState).mockReturnValue(VoiceBroadcastPlaybackState.Error);
|
||||
renderResult = render(<VoiceBroadcastPlaybackBody playback={playback} />);
|
||||
});
|
||||
|
||||
it("should render as expected", () => {
|
||||
expect(renderResult.container).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe.each([
|
||||
[VoiceBroadcastPlaybackState.Paused, "not-live"],
|
||||
[VoiceBroadcastPlaybackState.Playing, "live"],
|
||||
] satisfies [VoiceBroadcastPlaybackState, VoiceBroadcastLiveness][])(
|
||||
"when rendering a %s/%s broadcast",
|
||||
(state: VoiceBroadcastPlaybackState, liveness: VoiceBroadcastLiveness) => {
|
||||
beforeEach(() => {
|
||||
mocked(playback.getState).mockReturnValue(state);
|
||||
mocked(playback.getLiveness).mockReturnValue(liveness);
|
||||
renderResult = render(<VoiceBroadcastPlaybackBody playback={playback} />);
|
||||
});
|
||||
|
||||
it("should render as expected", () => {
|
||||
expect(renderResult.container).toMatchSnapshot();
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("when there is a broadcast without sender, it should raise an error", () => {
|
||||
infoEvent.sender = null;
|
||||
expect(() => {
|
||||
render(<VoiceBroadcastPlaybackBody playback={playback} />);
|
||||
}).toThrow(`Voice Broadcast sender not found (event ${playback.infoEvent.getId()})`);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,165 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { mocked } from "jest-mock";
|
||||
import { MatrixClient, Room, RoomMember } from "matrix-js-sdk/src/matrix";
|
||||
import { act, render, RenderResult, screen } from "jest-matrix-react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
|
||||
import {
|
||||
VoiceBroadcastPlaybacksStore,
|
||||
VoiceBroadcastPreRecording,
|
||||
VoiceBroadcastPreRecordingPip,
|
||||
VoiceBroadcastRecordingsStore,
|
||||
} from "../../../../src/voice-broadcast";
|
||||
import { flushPromises, stubClient } from "../../../test-utils";
|
||||
import { requestMediaPermissions } from "../../../../src/utils/media/requestMediaPermissions";
|
||||
import MediaDeviceHandler, { MediaDeviceKindEnum } from "../../../../src/MediaDeviceHandler";
|
||||
import dis from "../../../../src/dispatcher/dispatcher";
|
||||
import { Action } from "../../../../src/dispatcher/actions";
|
||||
|
||||
jest.mock("../../../../src/dispatcher/dispatcher");
|
||||
jest.mock("../../../../src/utils/media/requestMediaPermissions");
|
||||
|
||||
// mock RoomAvatar, because it is doing too much fancy stuff
|
||||
jest.mock("../../../../src/components/views/avatars/RoomAvatar", () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn().mockImplementation(({ room }) => {
|
||||
return <div data-testid="room-avatar">room avatar: {room.name}</div>;
|
||||
}),
|
||||
}));
|
||||
|
||||
describe("VoiceBroadcastPreRecordingPip", () => {
|
||||
let renderResult: RenderResult;
|
||||
let preRecording: VoiceBroadcastPreRecording;
|
||||
let playbacksStore: VoiceBroadcastPlaybacksStore;
|
||||
let recordingsStore: VoiceBroadcastRecordingsStore;
|
||||
let client: MatrixClient;
|
||||
let room: Room;
|
||||
let sender: RoomMember;
|
||||
|
||||
const itShouldShowTheBroadcastRoom = () => {
|
||||
it("should show the broadcast room", () => {
|
||||
expect(dis.dispatch).toHaveBeenCalledWith({
|
||||
action: Action.ViewRoom,
|
||||
room_id: room.roomId,
|
||||
metricsTrigger: undefined,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
client = stubClient();
|
||||
room = new Room("!room@example.com", client, client.getUserId() || "");
|
||||
sender = new RoomMember(room.roomId, client.getUserId() || "");
|
||||
recordingsStore = new VoiceBroadcastRecordingsStore();
|
||||
playbacksStore = new VoiceBroadcastPlaybacksStore(recordingsStore);
|
||||
mocked(requestMediaPermissions).mockResolvedValue({
|
||||
getTracks: (): Array<MediaStreamTrack> => [],
|
||||
} as unknown as MediaStream);
|
||||
jest.spyOn(MediaDeviceHandler, "getDevices").mockResolvedValue({
|
||||
[MediaDeviceKindEnum.AudioInput]: [
|
||||
{
|
||||
deviceId: "d1",
|
||||
label: "Device 1",
|
||||
} as MediaDeviceInfo,
|
||||
{
|
||||
deviceId: "d2",
|
||||
label: "Device 2",
|
||||
} as MediaDeviceInfo,
|
||||
],
|
||||
[MediaDeviceKindEnum.AudioOutput]: [],
|
||||
[MediaDeviceKindEnum.VideoInput]: [],
|
||||
});
|
||||
jest.spyOn(MediaDeviceHandler.instance, "setDevice").mockImplementation();
|
||||
preRecording = new VoiceBroadcastPreRecording(room, sender, client, playbacksStore, recordingsStore);
|
||||
jest.spyOn(preRecording, "start").mockResolvedValue();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
describe("when rendered", () => {
|
||||
beforeEach(async () => {
|
||||
renderResult = render(<VoiceBroadcastPreRecordingPip voiceBroadcastPreRecording={preRecording} />);
|
||||
|
||||
await act(async () => {
|
||||
flushPromises();
|
||||
});
|
||||
});
|
||||
|
||||
it("should match the snapshot", () => {
|
||||
expect(renderResult.container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe("and double clicking »Go live«", () => {
|
||||
beforeEach(async () => {
|
||||
await userEvent.click(screen.getByText("Go live"));
|
||||
await userEvent.click(screen.getByText("Go live"));
|
||||
});
|
||||
|
||||
it("should call start once", () => {
|
||||
expect(preRecording.start).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("and clicking the room name", () => {
|
||||
beforeEach(async () => {
|
||||
await userEvent.click(screen.getByText(room.name));
|
||||
});
|
||||
|
||||
itShouldShowTheBroadcastRoom();
|
||||
});
|
||||
|
||||
describe("and clicking the room avatar", () => {
|
||||
beforeEach(async () => {
|
||||
await userEvent.click(screen.getByText(`room avatar: ${room.name}`));
|
||||
});
|
||||
|
||||
itShouldShowTheBroadcastRoom();
|
||||
});
|
||||
|
||||
describe("and clicking the device label", () => {
|
||||
beforeEach(async () => {
|
||||
await act(async () => {
|
||||
await userEvent.click(screen.getByText("Default Device"));
|
||||
});
|
||||
});
|
||||
|
||||
it("should display the device selection", () => {
|
||||
expect(screen.queryAllByText("Default Device").length).toBe(2);
|
||||
expect(screen.queryByText("Device 1")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Device 2")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe("and selecting a device", () => {
|
||||
beforeEach(async () => {
|
||||
await act(async () => {
|
||||
await userEvent.click(screen.getByText("Device 1"));
|
||||
});
|
||||
});
|
||||
|
||||
it("should set it as current device", () => {
|
||||
expect(MediaDeviceHandler.instance.setDevice).toHaveBeenCalledWith(
|
||||
"d1",
|
||||
MediaDeviceKindEnum.AudioInput,
|
||||
);
|
||||
});
|
||||
|
||||
it("should not show the device selection", () => {
|
||||
expect(screen.queryByText("Default Device")).not.toBeInTheDocument();
|
||||
// expected to be one in the document, displayed in the pip directly
|
||||
expect(screen.queryByText("Device 1")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Device 2")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022, 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { render, RenderResult } from "jest-matrix-react";
|
||||
import { MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import {
|
||||
VoiceBroadcastInfoEventType,
|
||||
VoiceBroadcastInfoState,
|
||||
VoiceBroadcastRecording,
|
||||
VoiceBroadcastRecordingBody,
|
||||
} from "../../../../src/voice-broadcast";
|
||||
import { mkEvent, stubClient } from "../../../test-utils";
|
||||
|
||||
// mock RoomAvatar, because it is doing too much fancy stuff
|
||||
jest.mock("../../../../src/components/views/avatars/RoomAvatar", () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn().mockImplementation(({ room }) => {
|
||||
return <div data-testid="room-avatar">room avatar: {room.name}</div>;
|
||||
}),
|
||||
}));
|
||||
|
||||
describe("VoiceBroadcastRecordingBody", () => {
|
||||
const userId = "@user:example.com";
|
||||
const roomId = "!room:example.com";
|
||||
let client: MatrixClient;
|
||||
let infoEvent: MatrixEvent;
|
||||
let recording: VoiceBroadcastRecording;
|
||||
|
||||
beforeAll(() => {
|
||||
client = stubClient();
|
||||
infoEvent = mkEvent({
|
||||
event: true,
|
||||
type: VoiceBroadcastInfoEventType,
|
||||
content: {},
|
||||
room: roomId,
|
||||
user: userId,
|
||||
});
|
||||
recording = new VoiceBroadcastRecording(infoEvent, client, VoiceBroadcastInfoState.Resumed);
|
||||
});
|
||||
|
||||
describe("when rendering a live broadcast", () => {
|
||||
let renderResult: RenderResult;
|
||||
|
||||
beforeEach(() => {
|
||||
renderResult = render(<VoiceBroadcastRecordingBody recording={recording} />);
|
||||
});
|
||||
|
||||
it("should render with a red live badge", () => {
|
||||
expect(renderResult.container).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe("when rendering a paused broadcast", () => {
|
||||
let renderResult: RenderResult;
|
||||
|
||||
beforeEach(async () => {
|
||||
await recording.pause();
|
||||
renderResult = render(<VoiceBroadcastRecordingBody recording={recording} />);
|
||||
});
|
||||
|
||||
it("should render with a grey live badge", () => {
|
||||
expect(renderResult.container).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
it("when there is a broadcast without sender, it should raise an error", () => {
|
||||
infoEvent.sender = null;
|
||||
expect(() => {
|
||||
render(<VoiceBroadcastRecordingBody recording={recording} />);
|
||||
}).toThrow(`Voice Broadcast sender not found (event ${recording.infoEvent.getId()})`);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,218 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
//
|
||||
|
||||
import React from "react";
|
||||
import { act, render, RenderResult, screen } from "jest-matrix-react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { ClientEvent, MatrixClient, MatrixEvent, SyncState } from "matrix-js-sdk/src/matrix";
|
||||
import { sleep } from "matrix-js-sdk/src/utils";
|
||||
import { mocked } from "jest-mock";
|
||||
|
||||
import {
|
||||
VoiceBroadcastInfoState,
|
||||
VoiceBroadcastRecording,
|
||||
VoiceBroadcastRecordingPip,
|
||||
} from "../../../../src/voice-broadcast";
|
||||
import { flushPromises, stubClient } from "../../../test-utils";
|
||||
import { mkVoiceBroadcastInfoStateEvent } from "../../utils/test-utils";
|
||||
import { requestMediaPermissions } from "../../../../src/utils/media/requestMediaPermissions";
|
||||
import MediaDeviceHandler, { MediaDeviceKindEnum } from "../../../../src/MediaDeviceHandler";
|
||||
import dis from "../../../../src/dispatcher/dispatcher";
|
||||
import { Action } from "../../../../src/dispatcher/actions";
|
||||
|
||||
jest.mock("../../../../src/dispatcher/dispatcher");
|
||||
jest.mock("../../../../src/utils/media/requestMediaPermissions");
|
||||
|
||||
// mock RoomAvatar, because it is doing too much fancy stuff
|
||||
jest.mock("../../../../src/components/views/avatars/RoomAvatar", () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn().mockImplementation(({ room }) => {
|
||||
return <div data-testid="room-avatar">room avatar: {room.name}</div>;
|
||||
}),
|
||||
}));
|
||||
|
||||
// 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(),
|
||||
},
|
||||
start: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
describe("VoiceBroadcastRecordingPip", () => {
|
||||
const roomId = "!room:example.com";
|
||||
let client: MatrixClient;
|
||||
let infoEvent: MatrixEvent;
|
||||
let recording: VoiceBroadcastRecording;
|
||||
let renderResult: RenderResult;
|
||||
|
||||
const renderPip = async (state: VoiceBroadcastInfoState) => {
|
||||
infoEvent = mkVoiceBroadcastInfoStateEvent(roomId, state, client.getUserId() || "", client.getDeviceId() || "");
|
||||
recording = new VoiceBroadcastRecording(infoEvent, client, state);
|
||||
jest.spyOn(recording, "pause");
|
||||
jest.spyOn(recording, "resume");
|
||||
renderResult = render(<VoiceBroadcastRecordingPip recording={recording} />);
|
||||
await act(async () => {
|
||||
flushPromises();
|
||||
});
|
||||
};
|
||||
|
||||
const itShouldShowTheBroadcastRoom = () => {
|
||||
it("should show the broadcast room", () => {
|
||||
expect(dis.dispatch).toHaveBeenCalledWith({
|
||||
action: Action.ViewRoom,
|
||||
room_id: roomId,
|
||||
metricsTrigger: undefined,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
beforeAll(() => {
|
||||
client = stubClient();
|
||||
mocked(requestMediaPermissions).mockResolvedValue({
|
||||
getTracks: (): Array<MediaStreamTrack> => [],
|
||||
} as unknown as MediaStream);
|
||||
jest.spyOn(MediaDeviceHandler, "getDevices").mockResolvedValue({
|
||||
[MediaDeviceKindEnum.AudioInput]: [
|
||||
{
|
||||
deviceId: "d1",
|
||||
label: "Device 1",
|
||||
} as MediaDeviceInfo,
|
||||
{
|
||||
deviceId: "d2",
|
||||
label: "Device 2",
|
||||
} as MediaDeviceInfo,
|
||||
],
|
||||
[MediaDeviceKindEnum.AudioOutput]: [],
|
||||
[MediaDeviceKindEnum.VideoInput]: [],
|
||||
});
|
||||
jest.spyOn(MediaDeviceHandler.instance, "setDevice").mockImplementation();
|
||||
});
|
||||
|
||||
describe("when rendering a started recording", () => {
|
||||
beforeEach(async () => {
|
||||
await renderPip(VoiceBroadcastInfoState.Started);
|
||||
});
|
||||
|
||||
it("should render as expected", () => {
|
||||
expect(renderResult.container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe("and selecting another input device", () => {
|
||||
beforeEach(async () => {
|
||||
await userEvent.click(screen.getByLabelText("Change input device"));
|
||||
await userEvent.click(screen.getByText("Device 1"));
|
||||
});
|
||||
|
||||
it("should select the device and pause and resume the broadcast", () => {
|
||||
expect(MediaDeviceHandler.instance.setDevice).toHaveBeenCalledWith(
|
||||
"d1",
|
||||
MediaDeviceKindEnum.AudioInput,
|
||||
);
|
||||
expect(recording.pause).toHaveBeenCalled();
|
||||
expect(recording.resume).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("and clicking the room name", () => {
|
||||
beforeEach(async () => {
|
||||
await userEvent.click(screen.getByText("My room"));
|
||||
});
|
||||
|
||||
itShouldShowTheBroadcastRoom();
|
||||
});
|
||||
|
||||
describe("and clicking the room avatar", () => {
|
||||
beforeEach(async () => {
|
||||
await userEvent.click(screen.getByText("room avatar: My room"));
|
||||
});
|
||||
|
||||
itShouldShowTheBroadcastRoom();
|
||||
});
|
||||
|
||||
describe("and clicking the pause button", () => {
|
||||
beforeEach(async () => {
|
||||
await userEvent.click(screen.getByLabelText("pause voice broadcast"));
|
||||
});
|
||||
|
||||
it("should pause the recording", () => {
|
||||
expect(recording.getState()).toBe(VoiceBroadcastInfoState.Paused);
|
||||
});
|
||||
});
|
||||
|
||||
describe("and clicking the stop button", () => {
|
||||
beforeEach(async () => {
|
||||
await userEvent.click(screen.getByLabelText("Stop Recording"));
|
||||
// modal rendering has some weird sleeps
|
||||
await sleep(100);
|
||||
});
|
||||
|
||||
it("should display the confirm end dialog", () => {
|
||||
screen.getByText("Stop live broadcasting?");
|
||||
});
|
||||
|
||||
describe("and confirming the dialog", () => {
|
||||
beforeEach(async () => {
|
||||
await userEvent.click(screen.getByText("Yes, stop broadcast"));
|
||||
});
|
||||
|
||||
it("should end the recording", () => {
|
||||
expect(recording.getState()).toBe(VoiceBroadcastInfoState.Stopped);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("and there is no connection and clicking the pause button", () => {
|
||||
beforeEach(async () => {
|
||||
mocked(client.sendStateEvent).mockImplementation(() => {
|
||||
throw new Error();
|
||||
});
|
||||
await userEvent.click(screen.getByLabelText("pause voice broadcast"));
|
||||
});
|
||||
|
||||
it("should show a connection error info", () => {
|
||||
expect(screen.getByText("Connection error - Recording paused")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe("and the connection is back", () => {
|
||||
beforeEach(() => {
|
||||
mocked(client.sendStateEvent).mockResolvedValue({ event_id: "e1" });
|
||||
client.emit(ClientEvent.Sync, SyncState.Catchup, SyncState.Error);
|
||||
});
|
||||
|
||||
it("should render a paused recording", async () => {
|
||||
await expect(screen.findByLabelText("resume voice broadcast")).resolves.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when rendering a paused recording", () => {
|
||||
beforeEach(async () => {
|
||||
await renderPip(VoiceBroadcastInfoState.Paused);
|
||||
});
|
||||
|
||||
it("should render as expected", () => {
|
||||
expect(renderResult.container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe("and clicking the resume button", () => {
|
||||
beforeEach(async () => {
|
||||
await userEvent.click(screen.getByLabelText("resume voice broadcast"));
|
||||
});
|
||||
|
||||
it("should resume the recording", () => {
|
||||
expect(recording.getState()).toBe(VoiceBroadcastInfoState.Resumed);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,129 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||
import { render, RenderResult } from "jest-matrix-react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { mocked } from "jest-mock";
|
||||
|
||||
import {
|
||||
VoiceBroadcastInfoState,
|
||||
VoiceBroadcastLiveness,
|
||||
VoiceBroadcastPlayback,
|
||||
VoiceBroadcastSmallPlaybackBody,
|
||||
VoiceBroadcastPlaybackState,
|
||||
} from "../../../../src/voice-broadcast";
|
||||
import { stubClient } from "../../../test-utils";
|
||||
import { mkVoiceBroadcastInfoStateEvent } from "../../utils/test-utils";
|
||||
import { SdkContextClass } from "../../../../src/contexts/SDKContext";
|
||||
|
||||
// mock RoomAvatar, because it is doing too much fancy stuff
|
||||
jest.mock("../../../../src/components/views/avatars/RoomAvatar", () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn().mockImplementation(({ room }) => {
|
||||
return <div data-testid="room-avatar">room avatar: {room.name}</div>;
|
||||
}),
|
||||
}));
|
||||
|
||||
describe("<VoiceBroadcastSmallPlaybackBody />", () => {
|
||||
const userId = "@user:example.com";
|
||||
const roomId = "!room:example.com";
|
||||
let client: MatrixClient;
|
||||
let infoEvent: MatrixEvent;
|
||||
let playback: VoiceBroadcastPlayback;
|
||||
let renderResult: RenderResult;
|
||||
|
||||
beforeAll(() => {
|
||||
client = stubClient();
|
||||
mocked(client.relations).mockClear();
|
||||
mocked(client.relations).mockResolvedValue({ events: [] });
|
||||
|
||||
infoEvent = mkVoiceBroadcastInfoStateEvent(
|
||||
roomId,
|
||||
VoiceBroadcastInfoState.Stopped,
|
||||
userId,
|
||||
client.getDeviceId()!,
|
||||
);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
playback = new VoiceBroadcastPlayback(
|
||||
infoEvent,
|
||||
client,
|
||||
SdkContextClass.instance.voiceBroadcastRecordingsStore,
|
||||
);
|
||||
jest.spyOn(playback, "toggle").mockImplementation(() => Promise.resolve());
|
||||
jest.spyOn(playback, "getLiveness");
|
||||
jest.spyOn(playback, "getState");
|
||||
});
|
||||
|
||||
describe("when rendering a buffering broadcast", () => {
|
||||
beforeEach(() => {
|
||||
mocked(playback.getState).mockReturnValue(VoiceBroadcastPlaybackState.Buffering);
|
||||
mocked(playback.getLiveness).mockReturnValue("live");
|
||||
renderResult = render(<VoiceBroadcastSmallPlaybackBody playback={playback} />);
|
||||
});
|
||||
|
||||
it("should render as expected", () => {
|
||||
expect(renderResult.container).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe("when rendering a playing broadcast", () => {
|
||||
beforeEach(() => {
|
||||
mocked(playback.getState).mockReturnValue(VoiceBroadcastPlaybackState.Playing);
|
||||
mocked(playback.getLiveness).mockReturnValue("not-live");
|
||||
renderResult = render(<VoiceBroadcastSmallPlaybackBody playback={playback} />);
|
||||
});
|
||||
|
||||
it("should render as expected", () => {
|
||||
expect(renderResult.container).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe(`when rendering a stopped broadcast`, () => {
|
||||
beforeEach(() => {
|
||||
mocked(playback.getState).mockReturnValue(VoiceBroadcastPlaybackState.Stopped);
|
||||
mocked(playback.getLiveness).mockReturnValue("not-live");
|
||||
renderResult = render(<VoiceBroadcastSmallPlaybackBody playback={playback} />);
|
||||
});
|
||||
|
||||
it("should render as expected", () => {
|
||||
expect(renderResult.container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe("and clicking the play button", () => {
|
||||
beforeEach(async () => {
|
||||
await userEvent.click(renderResult.getByLabelText("play voice broadcast"));
|
||||
});
|
||||
|
||||
it("should toggle the playback", () => {
|
||||
expect(playback.toggle).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe.each([
|
||||
{ state: VoiceBroadcastPlaybackState.Paused, liveness: "not-live" },
|
||||
{ state: VoiceBroadcastPlaybackState.Playing, liveness: "live" },
|
||||
] as Array<{ state: VoiceBroadcastPlaybackState; liveness: VoiceBroadcastLiveness }>)(
|
||||
"when rendering a %s/%s broadcast",
|
||||
({ state, liveness }) => {
|
||||
beforeEach(() => {
|
||||
mocked(playback.getState).mockReturnValue(state);
|
||||
mocked(playback.getLiveness).mockReturnValue(liveness);
|
||||
renderResult = render(<VoiceBroadcastSmallPlaybackBody playback={playback} />);
|
||||
});
|
||||
|
||||
it("should render as expected", () => {
|
||||
expect(renderResult.container).toMatchSnapshot();
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
|
@ -0,0 +1,914 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`VoiceBroadcastPlaybackBody when rendering a buffering voice broadcast should render as expected 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_VoiceBroadcastBody"
|
||||
>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader"
|
||||
>
|
||||
<div
|
||||
data-testid="room-avatar"
|
||||
>
|
||||
room avatar:
|
||||
My room
|
||||
</div>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader_content"
|
||||
>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader_room_wrapper"
|
||||
>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader_room"
|
||||
>
|
||||
My room
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Change input device"
|
||||
class="mx_AccessibleButton mx_VoiceBroadcastHeader_line"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
class="mx_Icon mx_Icon_16"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M8 6a4 4 0 1 1 8 0v6a4 4 0 0 1-8 0V6Z"
|
||||
/>
|
||||
<path
|
||||
d="M5 11a1 1 0 0 1 1 1 6 6 0 0 0 12 0 1 1 0 1 1 2 0 8.001 8.001 0 0 1-7 7.938V21a1 1 0 1 1-2 0v-1.062A8.001 8.001 0 0 1 4 12a1 1 0 0 1 1-1Z"
|
||||
/>
|
||||
</svg>
|
||||
<span>
|
||||
@user:example.com
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader_line"
|
||||
>
|
||||
<div
|
||||
class="mx_Spinner"
|
||||
>
|
||||
<div
|
||||
aria-label="Loading…"
|
||||
class="mx_Spinner_icon"
|
||||
data-testid="spinner"
|
||||
role="progressbar"
|
||||
style="width: 14px; height: 14px;"
|
||||
/>
|
||||
</div>
|
||||
Buffering…
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_LiveBadge"
|
||||
>
|
||||
<div
|
||||
class="mx_Icon mx_Icon_16"
|
||||
/>
|
||||
Live
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_VoiceBroadcastBody_controls"
|
||||
>
|
||||
<div
|
||||
aria-label="30s backward"
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_secondary_content"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="mx_Icon mx_Icon_24"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
aria-label="pause voice broadcast"
|
||||
class="mx_AccessibleButton mx_VoiceBroadcastControl"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
class="mx_Icon mx_Icon_12"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M8 4a2 2 0 0 0-2 2v12a2 2 0 1 0 4 0V6a2 2 0 0 0-2-2Zm8 0a2 2 0 0 0-2 2v12a2 2 0 1 0 4 0V6a2 2 0 0 0-2-2Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
aria-label="30s forward"
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_secondary_content"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="mx_Icon mx_Icon_24"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
aria-label="Audio seek bar"
|
||||
class="mx_SeekBar"
|
||||
max="1"
|
||||
min="0"
|
||||
step="0.001"
|
||||
style="--fillTo: 0;"
|
||||
tabindex="0"
|
||||
type="range"
|
||||
value="0"
|
||||
/>
|
||||
<div
|
||||
class="mx_VoiceBroadcastBody_timerow"
|
||||
>
|
||||
<time
|
||||
class="mx_Clock"
|
||||
datetime="PT0S"
|
||||
>
|
||||
00:00
|
||||
</time>
|
||||
<time
|
||||
class="mx_Clock"
|
||||
datetime="-PT23M42S"
|
||||
>
|
||||
-23:42
|
||||
</time>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`VoiceBroadcastPlaybackBody when rendering a pause/not-live broadcast should render as expected 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_VoiceBroadcastBody"
|
||||
>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader"
|
||||
>
|
||||
<div
|
||||
data-testid="room-avatar"
|
||||
>
|
||||
room avatar:
|
||||
My room
|
||||
</div>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader_content"
|
||||
>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader_room_wrapper"
|
||||
>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader_room"
|
||||
>
|
||||
My room
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Change input device"
|
||||
class="mx_AccessibleButton mx_VoiceBroadcastHeader_line"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
class="mx_Icon mx_Icon_16"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M8 6a4 4 0 1 1 8 0v6a4 4 0 0 1-8 0V6Z"
|
||||
/>
|
||||
<path
|
||||
d="M5 11a1 1 0 0 1 1 1 6 6 0 0 0 12 0 1 1 0 1 1 2 0 8.001 8.001 0 0 1-7 7.938V21a1 1 0 1 1-2 0v-1.062A8.001 8.001 0 0 1 4 12a1 1 0 0 1 1-1Z"
|
||||
/>
|
||||
</svg>
|
||||
<span>
|
||||
@user:example.com
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader_line"
|
||||
>
|
||||
<div
|
||||
class="mx_Icon mx_Icon_16"
|
||||
/>
|
||||
Voice broadcast
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_VoiceBroadcastBody_controls"
|
||||
>
|
||||
<div
|
||||
aria-label="30s backward"
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_secondary_content"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="mx_Icon mx_Icon_24"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
aria-label="resume voice broadcast"
|
||||
class="mx_AccessibleButton mx_VoiceBroadcastControl mx_VoiceBroadcastControl-play"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
class="mx_Icon mx_Icon_16"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="m8.98 4.677 9.921 5.58c1.36.764 1.36 2.722 0 3.486l-9.92 5.58C7.647 20.073 6 19.11 6 17.58V6.42c0-1.53 1.647-2.493 2.98-1.743Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
aria-label="30s forward"
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_secondary_content"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="mx_Icon mx_Icon_24"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
aria-label="Audio seek bar"
|
||||
class="mx_SeekBar"
|
||||
max="1"
|
||||
min="0"
|
||||
step="0.001"
|
||||
style="--fillTo: 0;"
|
||||
tabindex="0"
|
||||
type="range"
|
||||
value="0"
|
||||
/>
|
||||
<div
|
||||
class="mx_VoiceBroadcastBody_timerow"
|
||||
>
|
||||
<time
|
||||
class="mx_Clock"
|
||||
datetime="PT0S"
|
||||
>
|
||||
00:00
|
||||
</time>
|
||||
<time
|
||||
class="mx_Clock"
|
||||
datetime="-PT23M42S"
|
||||
>
|
||||
-23:42
|
||||
</time>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`VoiceBroadcastPlaybackBody when rendering a playing broadcast in pip mode should render as expected 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_VoiceBroadcastBody mx_VoiceBroadcastBody--pip"
|
||||
>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader"
|
||||
>
|
||||
<div
|
||||
class="mx_AccessibleButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
data-testid="room-avatar"
|
||||
>
|
||||
room avatar:
|
||||
My room
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader_content"
|
||||
>
|
||||
<div
|
||||
class="mx_AccessibleButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader_room_wrapper"
|
||||
>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader_room"
|
||||
>
|
||||
My room
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Change input device"
|
||||
class="mx_AccessibleButton mx_VoiceBroadcastHeader_line"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
class="mx_Icon mx_Icon_16"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M8 6a4 4 0 1 1 8 0v6a4 4 0 0 1-8 0V6Z"
|
||||
/>
|
||||
<path
|
||||
d="M5 11a1 1 0 0 1 1 1 6 6 0 0 0 12 0 1 1 0 1 1 2 0 8.001 8.001 0 0 1-7 7.938V21a1 1 0 1 1-2 0v-1.062A8.001 8.001 0 0 1 4 12a1 1 0 0 1 1-1Z"
|
||||
/>
|
||||
</svg>
|
||||
<span>
|
||||
@user:example.com
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader_line"
|
||||
>
|
||||
<div
|
||||
class="mx_Icon mx_Icon_16"
|
||||
/>
|
||||
Voice broadcast
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_VoiceBroadcastBody_controls"
|
||||
>
|
||||
<div
|
||||
aria-label="30s backward"
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_secondary_content"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="mx_Icon mx_Icon_24"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
aria-label="pause voice broadcast"
|
||||
class="mx_AccessibleButton mx_VoiceBroadcastControl"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
class="mx_Icon mx_Icon_12"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M8 4a2 2 0 0 0-2 2v12a2 2 0 1 0 4 0V6a2 2 0 0 0-2-2Zm8 0a2 2 0 0 0-2 2v12a2 2 0 1 0 4 0V6a2 2 0 0 0-2-2Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
aria-label="30s forward"
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_secondary_content"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="mx_Icon mx_Icon_24"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
aria-label="Audio seek bar"
|
||||
class="mx_SeekBar"
|
||||
max="1"
|
||||
min="0"
|
||||
step="0.001"
|
||||
style="--fillTo: 0;"
|
||||
tabindex="0"
|
||||
type="range"
|
||||
value="0"
|
||||
/>
|
||||
<div
|
||||
class="mx_VoiceBroadcastBody_timerow"
|
||||
>
|
||||
<time
|
||||
class="mx_Clock"
|
||||
datetime="PT0S"
|
||||
>
|
||||
00:00
|
||||
</time>
|
||||
<time
|
||||
class="mx_Clock"
|
||||
datetime="-PT23M42S"
|
||||
>
|
||||
-23:42
|
||||
</time>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`VoiceBroadcastPlaybackBody when rendering a playing broadcast should render as expected 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_VoiceBroadcastBody"
|
||||
>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader"
|
||||
>
|
||||
<div
|
||||
data-testid="room-avatar"
|
||||
>
|
||||
room avatar:
|
||||
My room
|
||||
</div>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader_content"
|
||||
>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader_room_wrapper"
|
||||
>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader_room"
|
||||
>
|
||||
My room
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Change input device"
|
||||
class="mx_AccessibleButton mx_VoiceBroadcastHeader_line"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
class="mx_Icon mx_Icon_16"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M8 6a4 4 0 1 1 8 0v6a4 4 0 0 1-8 0V6Z"
|
||||
/>
|
||||
<path
|
||||
d="M5 11a1 1 0 0 1 1 1 6 6 0 0 0 12 0 1 1 0 1 1 2 0 8.001 8.001 0 0 1-7 7.938V21a1 1 0 1 1-2 0v-1.062A8.001 8.001 0 0 1 4 12a1 1 0 0 1 1-1Z"
|
||||
/>
|
||||
</svg>
|
||||
<span>
|
||||
@user:example.com
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader_line"
|
||||
>
|
||||
<div
|
||||
class="mx_Icon mx_Icon_16"
|
||||
/>
|
||||
Voice broadcast
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_VoiceBroadcastBody_controls"
|
||||
>
|
||||
<div
|
||||
aria-label="30s backward"
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_secondary_content"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="mx_Icon mx_Icon_24"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
aria-label="pause voice broadcast"
|
||||
class="mx_AccessibleButton mx_VoiceBroadcastControl"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
class="mx_Icon mx_Icon_12"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M8 4a2 2 0 0 0-2 2v12a2 2 0 1 0 4 0V6a2 2 0 0 0-2-2Zm8 0a2 2 0 0 0-2 2v12a2 2 0 1 0 4 0V6a2 2 0 0 0-2-2Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
aria-label="30s forward"
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_secondary_content"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="mx_Icon mx_Icon_24"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
aria-label="Audio seek bar"
|
||||
class="mx_SeekBar"
|
||||
max="1"
|
||||
min="0"
|
||||
step="0.001"
|
||||
style="--fillTo: 0;"
|
||||
tabindex="0"
|
||||
type="range"
|
||||
value="0"
|
||||
/>
|
||||
<div
|
||||
class="mx_VoiceBroadcastBody_timerow"
|
||||
>
|
||||
<time
|
||||
class="mx_Clock"
|
||||
datetime="PT0S"
|
||||
>
|
||||
00:00
|
||||
</time>
|
||||
<time
|
||||
class="mx_Clock"
|
||||
datetime="-PT23M42S"
|
||||
>
|
||||
-23:42
|
||||
</time>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`VoiceBroadcastPlaybackBody when rendering a playing/live broadcast should render as expected 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_VoiceBroadcastBody"
|
||||
>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader"
|
||||
>
|
||||
<div
|
||||
data-testid="room-avatar"
|
||||
>
|
||||
room avatar:
|
||||
My room
|
||||
</div>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader_content"
|
||||
>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader_room_wrapper"
|
||||
>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader_room"
|
||||
>
|
||||
My room
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Change input device"
|
||||
class="mx_AccessibleButton mx_VoiceBroadcastHeader_line"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
class="mx_Icon mx_Icon_16"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M8 6a4 4 0 1 1 8 0v6a4 4 0 0 1-8 0V6Z"
|
||||
/>
|
||||
<path
|
||||
d="M5 11a1 1 0 0 1 1 1 6 6 0 0 0 12 0 1 1 0 1 1 2 0 8.001 8.001 0 0 1-7 7.938V21a1 1 0 1 1-2 0v-1.062A8.001 8.001 0 0 1 4 12a1 1 0 0 1 1-1Z"
|
||||
/>
|
||||
</svg>
|
||||
<span>
|
||||
@user:example.com
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader_line"
|
||||
>
|
||||
<div
|
||||
class="mx_Icon mx_Icon_16"
|
||||
/>
|
||||
Voice broadcast
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_LiveBadge"
|
||||
>
|
||||
<div
|
||||
class="mx_Icon mx_Icon_16"
|
||||
/>
|
||||
Live
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_VoiceBroadcastBody_controls"
|
||||
>
|
||||
<div
|
||||
aria-label="30s backward"
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_secondary_content"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="mx_Icon mx_Icon_24"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
aria-label="pause voice broadcast"
|
||||
class="mx_AccessibleButton mx_VoiceBroadcastControl"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
class="mx_Icon mx_Icon_12"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M8 4a2 2 0 0 0-2 2v12a2 2 0 1 0 4 0V6a2 2 0 0 0-2-2Zm8 0a2 2 0 0 0-2 2v12a2 2 0 1 0 4 0V6a2 2 0 0 0-2-2Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
aria-label="30s forward"
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_secondary_content"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="mx_Icon mx_Icon_24"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
aria-label="Audio seek bar"
|
||||
class="mx_SeekBar"
|
||||
max="1"
|
||||
min="0"
|
||||
step="0.001"
|
||||
style="--fillTo: 0;"
|
||||
tabindex="0"
|
||||
type="range"
|
||||
value="0"
|
||||
/>
|
||||
<div
|
||||
class="mx_VoiceBroadcastBody_timerow"
|
||||
>
|
||||
<time
|
||||
class="mx_Clock"
|
||||
datetime="PT0S"
|
||||
>
|
||||
00:00
|
||||
</time>
|
||||
<time
|
||||
class="mx_Clock"
|
||||
datetime="-PT23M42S"
|
||||
>
|
||||
-23:42
|
||||
</time>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`VoiceBroadcastPlaybackBody when rendering a stopped broadcast should render as expected 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_VoiceBroadcastBody"
|
||||
>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader"
|
||||
>
|
||||
<div
|
||||
data-testid="room-avatar"
|
||||
>
|
||||
room avatar:
|
||||
My room
|
||||
</div>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader_content"
|
||||
>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader_room_wrapper"
|
||||
>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader_room"
|
||||
>
|
||||
My room
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Change input device"
|
||||
class="mx_AccessibleButton mx_VoiceBroadcastHeader_line"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
class="mx_Icon mx_Icon_16"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M8 6a4 4 0 1 1 8 0v6a4 4 0 0 1-8 0V6Z"
|
||||
/>
|
||||
<path
|
||||
d="M5 11a1 1 0 0 1 1 1 6 6 0 0 0 12 0 1 1 0 1 1 2 0 8.001 8.001 0 0 1-7 7.938V21a1 1 0 1 1-2 0v-1.062A8.001 8.001 0 0 1 4 12a1 1 0 0 1 1-1Z"
|
||||
/>
|
||||
</svg>
|
||||
<span>
|
||||
@user:example.com
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader_line"
|
||||
>
|
||||
<div
|
||||
class="mx_Icon mx_Icon_16"
|
||||
/>
|
||||
Voice broadcast
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_VoiceBroadcastBody_controls"
|
||||
>
|
||||
<div
|
||||
aria-label="play voice broadcast"
|
||||
class="mx_AccessibleButton mx_VoiceBroadcastControl mx_VoiceBroadcastControl-play"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
class="mx_Icon mx_Icon_16"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="m8.98 4.677 9.921 5.58c1.36.764 1.36 2.722 0 3.486l-9.92 5.58C7.647 20.073 6 19.11 6 17.58V6.42c0-1.53 1.647-2.493 2.98-1.743Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
aria-label="Audio seek bar"
|
||||
class="mx_SeekBar"
|
||||
max="1"
|
||||
min="0"
|
||||
step="0.001"
|
||||
style="--fillTo: 0;"
|
||||
tabindex="0"
|
||||
type="range"
|
||||
value="0"
|
||||
/>
|
||||
<div
|
||||
class="mx_VoiceBroadcastBody_timerow"
|
||||
>
|
||||
<time
|
||||
class="mx_Clock"
|
||||
datetime="PT0S"
|
||||
>
|
||||
00:00
|
||||
</time>
|
||||
<time
|
||||
class="mx_Clock"
|
||||
datetime="-PT23M42S"
|
||||
>
|
||||
-23:42
|
||||
</time>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`VoiceBroadcastPlaybackBody when rendering an error broadcast should render as expected 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_VoiceBroadcastBody"
|
||||
>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader"
|
||||
>
|
||||
<div
|
||||
data-testid="room-avatar"
|
||||
>
|
||||
room avatar:
|
||||
My room
|
||||
</div>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader_content"
|
||||
>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader_room_wrapper"
|
||||
>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader_room"
|
||||
>
|
||||
My room
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Change input device"
|
||||
class="mx_AccessibleButton mx_VoiceBroadcastHeader_line"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
class="mx_Icon mx_Icon_16"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M8 6a4 4 0 1 1 8 0v6a4 4 0 0 1-8 0V6Z"
|
||||
/>
|
||||
<path
|
||||
d="M5 11a1 1 0 0 1 1 1 6 6 0 0 0 12 0 1 1 0 1 1 2 0 8.001 8.001 0 0 1-7 7.938V21a1 1 0 1 1-2 0v-1.062A8.001 8.001 0 0 1 4 12a1 1 0 0 1 1-1Z"
|
||||
/>
|
||||
</svg>
|
||||
<span>
|
||||
@user:example.com
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader_line"
|
||||
>
|
||||
<div
|
||||
class="mx_Icon mx_Icon_16"
|
||||
/>
|
||||
Voice broadcast
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_VoiceBroadcastRecordingConnectionError"
|
||||
>
|
||||
<svg
|
||||
class="mx_Icon mx_Icon_16"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12.713 17.713A.968.968 0 0 1 12 18a.968.968 0 0 1-.713-.287A.967.967 0 0 1 11 17a.97.97 0 0 1 .287-.712A.968.968 0 0 1 12 16a.97.97 0 0 1 .713.288A.968.968 0 0 1 13 17a.97.97 0 0 1-.287.713Zm0-4A.968.968 0 0 1 12 14a.968.968 0 0 1-.713-.287A.967.967 0 0 1 11 13V9a.97.97 0 0 1 .287-.712A.968.968 0 0 1 12 8a.97.97 0 0 1 .713.288A.968.968 0 0 1 13 9v4a.97.97 0 0 1-.287.713Z"
|
||||
/>
|
||||
<path
|
||||
clip-rule="evenodd"
|
||||
d="M10.264 3.039c.767-1.344 2.705-1.344 3.472 0l8.554 14.969c.762 1.333-.2 2.992-1.736 2.992H3.446c-1.535 0-2.498-1.659-1.736-2.992l8.553-14.969ZM3.446 19 12 4.031l8.554 14.97H3.446Z"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
Unable to play this voice broadcast
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
|
@ -0,0 +1,98 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`VoiceBroadcastPreRecordingPip when rendered should match the snapshot 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_VoiceBroadcastBody mx_VoiceBroadcastBody--pip"
|
||||
>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader"
|
||||
>
|
||||
<div
|
||||
class="mx_AccessibleButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
data-testid="room-avatar"
|
||||
>
|
||||
room avatar:
|
||||
!room@example.com
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader_content"
|
||||
>
|
||||
<div
|
||||
class="mx_AccessibleButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader_room_wrapper"
|
||||
>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader_room"
|
||||
>
|
||||
!room@example.com
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Change input device"
|
||||
class="mx_AccessibleButton mx_VoiceBroadcastHeader_line mx_VoiceBroadcastHeader_mic--clickable"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
class="mx_Icon mx_Icon_16"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M8 6a4 4 0 1 1 8 0v6a4 4 0 0 1-8 0V6Z"
|
||||
/>
|
||||
<path
|
||||
d="M5 11a1 1 0 0 1 1 1 6 6 0 0 0 12 0 1 1 0 1 1 2 0 8.001 8.001 0 0 1-7 7.938V21a1 1 0 1 1-2 0v-1.062A8.001 8.001 0 0 1 4 12a1 1 0 0 1 1-1Z"
|
||||
/>
|
||||
</svg>
|
||||
<span>
|
||||
Default Device
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_AccessibleButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
class="mx_Icon mx_Icon_16"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6.293 6.293a1 1 0 0 1 1.414 0L12 10.586l4.293-4.293a1 1 0 1 1 1.414 1.414L13.414 12l4.293 4.293a1 1 0 0 1-1.414 1.414L12 13.414l-4.293 4.293a1 1 0 0 1-1.414-1.414L10.586 12 6.293 7.707a1 1 0 0 1 0-1.414Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_VoiceBroadcastBody_blockButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_danger"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="mx_Icon mx_Icon_16"
|
||||
/>
|
||||
Go live
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
|
@ -0,0 +1,131 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`VoiceBroadcastRecordingBody when rendering a live broadcast should render with a red live badge 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_VoiceBroadcastBody"
|
||||
>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader"
|
||||
>
|
||||
<div
|
||||
data-testid="room-avatar"
|
||||
>
|
||||
room avatar:
|
||||
My room
|
||||
</div>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader_content"
|
||||
>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader_room_wrapper"
|
||||
>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader_room"
|
||||
>
|
||||
My room
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Change input device"
|
||||
class="mx_AccessibleButton mx_VoiceBroadcastHeader_line"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
class="mx_Icon mx_Icon_16"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M8 6a4 4 0 1 1 8 0v6a4 4 0 0 1-8 0V6Z"
|
||||
/>
|
||||
<path
|
||||
d="M5 11a1 1 0 0 1 1 1 6 6 0 0 0 12 0 1 1 0 1 1 2 0 8.001 8.001 0 0 1-7 7.938V21a1 1 0 1 1-2 0v-1.062A8.001 8.001 0 0 1 4 12a1 1 0 0 1 1-1Z"
|
||||
/>
|
||||
</svg>
|
||||
<span>
|
||||
@user:example.com
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_LiveBadge"
|
||||
>
|
||||
<div
|
||||
class="mx_Icon mx_Icon_16"
|
||||
/>
|
||||
Live
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`VoiceBroadcastRecordingBody when rendering a paused broadcast should render with a grey live badge 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_VoiceBroadcastBody"
|
||||
>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader"
|
||||
>
|
||||
<div
|
||||
data-testid="room-avatar"
|
||||
>
|
||||
room avatar:
|
||||
My room
|
||||
</div>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader_content"
|
||||
>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader_room_wrapper"
|
||||
>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader_room"
|
||||
>
|
||||
My room
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Change input device"
|
||||
class="mx_AccessibleButton mx_VoiceBroadcastHeader_line"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
class="mx_Icon mx_Icon_16"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M8 6a4 4 0 1 1 8 0v6a4 4 0 0 1-8 0V6Z"
|
||||
/>
|
||||
<path
|
||||
d="M5 11a1 1 0 0 1 1 1 6 6 0 0 0 12 0 1 1 0 1 1 2 0 8.001 8.001 0 0 1-7 7.938V21a1 1 0 1 1-2 0v-1.062A8.001 8.001 0 0 1 4 12a1 1 0 0 1 1-1Z"
|
||||
/>
|
||||
</svg>
|
||||
<span>
|
||||
@user:example.com
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_LiveBadge mx_LiveBadge--grey"
|
||||
>
|
||||
<div
|
||||
class="mx_Icon mx_Icon_16"
|
||||
/>
|
||||
Live
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
|
@ -0,0 +1,238 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`VoiceBroadcastRecordingPip when rendering a paused recording should render as expected 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_VoiceBroadcastBody mx_VoiceBroadcastBody--pip"
|
||||
>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader"
|
||||
>
|
||||
<div
|
||||
class="mx_AccessibleButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
data-testid="room-avatar"
|
||||
>
|
||||
room avatar:
|
||||
My room
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader_content"
|
||||
>
|
||||
<div
|
||||
class="mx_AccessibleButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader_room_wrapper"
|
||||
>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader_room"
|
||||
>
|
||||
My room
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader_line"
|
||||
>
|
||||
<div
|
||||
class="mx_Icon mx_Icon_16"
|
||||
/>
|
||||
<time
|
||||
class="mx_Clock"
|
||||
datetime="PT4H"
|
||||
>
|
||||
4h 0m 0s left
|
||||
</time>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_LiveBadge mx_LiveBadge--grey"
|
||||
>
|
||||
<div
|
||||
class="mx_Icon mx_Icon_16"
|
||||
/>
|
||||
Live
|
||||
</div>
|
||||
</div>
|
||||
<hr
|
||||
class="mx_VoiceBroadcastBody_divider"
|
||||
/>
|
||||
<div
|
||||
class="mx_VoiceBroadcastBody_controls"
|
||||
>
|
||||
<div
|
||||
aria-label="resume voice broadcast"
|
||||
class="mx_AccessibleButton mx_VoiceBroadcastControl mx_VoiceBroadcastControl-recording"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="mx_Icon mx_Icon_12"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Change input device"
|
||||
class="mx_AccessibleButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
class="mx_Icon mx_Icon_16 mx_Icon_alert"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M8 6a4 4 0 1 1 8 0v6a4 4 0 0 1-8 0V6Z"
|
||||
/>
|
||||
<path
|
||||
d="M5 11a1 1 0 0 1 1 1 6 6 0 0 0 12 0 1 1 0 1 1 2 0 8.001 8.001 0 0 1-7 7.938V21a1 1 0 1 1-2 0v-1.062A8.001 8.001 0 0 1 4 12a1 1 0 0 1 1-1Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Stop Recording"
|
||||
class="mx_AccessibleButton mx_VoiceBroadcastControl"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="mx_Icon mx_Icon_16"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`VoiceBroadcastRecordingPip when rendering a started recording should render as expected 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_VoiceBroadcastBody mx_VoiceBroadcastBody--pip"
|
||||
>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader"
|
||||
>
|
||||
<div
|
||||
class="mx_AccessibleButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
data-testid="room-avatar"
|
||||
>
|
||||
room avatar:
|
||||
My room
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader_content"
|
||||
>
|
||||
<div
|
||||
class="mx_AccessibleButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader_room_wrapper"
|
||||
>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader_room"
|
||||
>
|
||||
My room
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader_line"
|
||||
>
|
||||
<div
|
||||
class="mx_Icon mx_Icon_16"
|
||||
/>
|
||||
<time
|
||||
class="mx_Clock"
|
||||
datetime="PT4H"
|
||||
>
|
||||
4h 0m 0s left
|
||||
</time>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_LiveBadge"
|
||||
>
|
||||
<div
|
||||
class="mx_Icon mx_Icon_16"
|
||||
/>
|
||||
Live
|
||||
</div>
|
||||
</div>
|
||||
<hr
|
||||
class="mx_VoiceBroadcastBody_divider"
|
||||
/>
|
||||
<div
|
||||
class="mx_VoiceBroadcastBody_controls"
|
||||
>
|
||||
<div
|
||||
aria-label="pause voice broadcast"
|
||||
class="mx_AccessibleButton mx_VoiceBroadcastControl"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
class="mx_Icon mx_Icon_12"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M8 4a2 2 0 0 0-2 2v12a2 2 0 1 0 4 0V6a2 2 0 0 0-2-2Zm8 0a2 2 0 0 0-2 2v12a2 2 0 1 0 4 0V6a2 2 0 0 0-2-2Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Change input device"
|
||||
class="mx_AccessibleButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
class="mx_Icon mx_Icon_16 mx_Icon_alert"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M8 6a4 4 0 1 1 8 0v6a4 4 0 0 1-8 0V6Z"
|
||||
/>
|
||||
<path
|
||||
d="M5 11a1 1 0 0 1 1 1 6 6 0 0 0 12 0 1 1 0 1 1 2 0 8.001 8.001 0 0 1-7 7.938V21a1 1 0 1 1-2 0v-1.062A8.001 8.001 0 0 1 4 12a1 1 0 0 1 1-1Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Stop Recording"
|
||||
class="mx_AccessibleButton mx_VoiceBroadcastControl"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="mx_Icon mx_Icon_16"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
|
@ -0,0 +1,558 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<VoiceBroadcastSmallPlaybackBody /> when rendering a { state: 'pause', liveness: 'not-live' }/%s broadcast should render as expected 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_VoiceBroadcastBody mx_VoiceBroadcastBody--pip mx_VoiceBroadcastBody--small"
|
||||
>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader"
|
||||
>
|
||||
<div
|
||||
class="mx_AccessibleButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
data-testid="room-avatar"
|
||||
>
|
||||
room avatar:
|
||||
My room
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader_content"
|
||||
>
|
||||
<div
|
||||
class="mx_AccessibleButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader_room_wrapper"
|
||||
>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader_room"
|
||||
>
|
||||
My room
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Change input device"
|
||||
class="mx_AccessibleButton mx_VoiceBroadcastHeader_line"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
class="mx_Icon mx_Icon_16"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M8 6a4 4 0 1 1 8 0v6a4 4 0 0 1-8 0V6Z"
|
||||
/>
|
||||
<path
|
||||
d="M5 11a1 1 0 0 1 1 1 6 6 0 0 0 12 0 1 1 0 1 1 2 0 8.001 8.001 0 0 1-7 7.938V21a1 1 0 1 1-2 0v-1.062A8.001 8.001 0 0 1 4 12a1 1 0 0 1 1-1Z"
|
||||
/>
|
||||
</svg>
|
||||
<span>
|
||||
@user:example.com
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-label="resume voice broadcast"
|
||||
class="mx_AccessibleButton mx_VoiceBroadcastControl mx_VoiceBroadcastControl-play"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
class="mx_Icon mx_Icon_16"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="m8.98 4.677 9.921 5.58c1.36.764 1.36 2.722 0 3.486l-9.92 5.58C7.647 20.073 6 19.11 6 17.58V6.42c0-1.53 1.647-2.493 2.98-1.743Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
class="mx_AccessibleButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
class="mx_Icon mx_Icon_8 mx_VoiceBroadcastBody__small-close"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6.293 6.293a1 1 0 0 1 1.414 0L12 10.586l4.293-4.293a1 1 0 1 1 1.414 1.414L13.414 12l4.293 4.293a1 1 0 0 1-1.414 1.414L12 13.414l-4.293 4.293a1 1 0 0 1-1.414-1.414L10.586 12 6.293 7.707a1 1 0 0 1 0-1.414Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<VoiceBroadcastSmallPlaybackBody /> when rendering a { state: 'playing', liveness: 'live' }/%s broadcast should render as expected 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_VoiceBroadcastBody mx_VoiceBroadcastBody--pip mx_VoiceBroadcastBody--small"
|
||||
>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader"
|
||||
>
|
||||
<div
|
||||
class="mx_AccessibleButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
data-testid="room-avatar"
|
||||
>
|
||||
room avatar:
|
||||
My room
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader_content"
|
||||
>
|
||||
<div
|
||||
class="mx_AccessibleButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader_room_wrapper"
|
||||
>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader_room"
|
||||
>
|
||||
My room
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Change input device"
|
||||
class="mx_AccessibleButton mx_VoiceBroadcastHeader_line"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
class="mx_Icon mx_Icon_16"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M8 6a4 4 0 1 1 8 0v6a4 4 0 0 1-8 0V6Z"
|
||||
/>
|
||||
<path
|
||||
d="M5 11a1 1 0 0 1 1 1 6 6 0 0 0 12 0 1 1 0 1 1 2 0 8.001 8.001 0 0 1-7 7.938V21a1 1 0 1 1-2 0v-1.062A8.001 8.001 0 0 1 4 12a1 1 0 0 1 1-1Z"
|
||||
/>
|
||||
</svg>
|
||||
<span>
|
||||
@user:example.com
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mx_LiveBadge"
|
||||
>
|
||||
<div
|
||||
class="mx_Icon mx_Icon_16"
|
||||
/>
|
||||
Live
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-label="pause voice broadcast"
|
||||
class="mx_AccessibleButton mx_VoiceBroadcastControl"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
class="mx_Icon mx_Icon_12"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M8 4a2 2 0 0 0-2 2v12a2 2 0 1 0 4 0V6a2 2 0 0 0-2-2Zm8 0a2 2 0 0 0-2 2v12a2 2 0 1 0 4 0V6a2 2 0 0 0-2-2Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
class="mx_AccessibleButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
class="mx_Icon mx_Icon_8 mx_VoiceBroadcastBody__small-close"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6.293 6.293a1 1 0 0 1 1.414 0L12 10.586l4.293-4.293a1 1 0 1 1 1.414 1.414L13.414 12l4.293 4.293a1 1 0 0 1-1.414 1.414L12 13.414l-4.293 4.293a1 1 0 0 1-1.414-1.414L10.586 12 6.293 7.707a1 1 0 0 1 0-1.414Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<VoiceBroadcastSmallPlaybackBody /> when rendering a buffering broadcast should render as expected 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_VoiceBroadcastBody mx_VoiceBroadcastBody--pip mx_VoiceBroadcastBody--small"
|
||||
>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader"
|
||||
>
|
||||
<div
|
||||
class="mx_AccessibleButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
data-testid="room-avatar"
|
||||
>
|
||||
room avatar:
|
||||
My room
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader_content"
|
||||
>
|
||||
<div
|
||||
class="mx_AccessibleButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader_room_wrapper"
|
||||
>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader_room"
|
||||
>
|
||||
My room
|
||||
</div>
|
||||
<div
|
||||
class="mx_Spinner"
|
||||
>
|
||||
<div
|
||||
aria-label="Loading…"
|
||||
class="mx_Spinner_icon"
|
||||
data-testid="spinner"
|
||||
role="progressbar"
|
||||
style="width: 12px; height: 12px;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Change input device"
|
||||
class="mx_AccessibleButton mx_VoiceBroadcastHeader_line"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
class="mx_Icon mx_Icon_16"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M8 6a4 4 0 1 1 8 0v6a4 4 0 0 1-8 0V6Z"
|
||||
/>
|
||||
<path
|
||||
d="M5 11a1 1 0 0 1 1 1 6 6 0 0 0 12 0 1 1 0 1 1 2 0 8.001 8.001 0 0 1-7 7.938V21a1 1 0 1 1-2 0v-1.062A8.001 8.001 0 0 1 4 12a1 1 0 0 1 1-1Z"
|
||||
/>
|
||||
</svg>
|
||||
<span>
|
||||
@user:example.com
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mx_LiveBadge"
|
||||
>
|
||||
<div
|
||||
class="mx_Icon mx_Icon_16"
|
||||
/>
|
||||
Live
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-label="pause voice broadcast"
|
||||
class="mx_AccessibleButton mx_VoiceBroadcastControl"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
class="mx_Icon mx_Icon_12"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M8 4a2 2 0 0 0-2 2v12a2 2 0 1 0 4 0V6a2 2 0 0 0-2-2Zm8 0a2 2 0 0 0-2 2v12a2 2 0 1 0 4 0V6a2 2 0 0 0-2-2Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
class="mx_AccessibleButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
class="mx_Icon mx_Icon_8 mx_VoiceBroadcastBody__small-close"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6.293 6.293a1 1 0 0 1 1.414 0L12 10.586l4.293-4.293a1 1 0 1 1 1.414 1.414L13.414 12l4.293 4.293a1 1 0 0 1-1.414 1.414L12 13.414l-4.293 4.293a1 1 0 0 1-1.414-1.414L10.586 12 6.293 7.707a1 1 0 0 1 0-1.414Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<VoiceBroadcastSmallPlaybackBody /> when rendering a playing broadcast should render as expected 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_VoiceBroadcastBody mx_VoiceBroadcastBody--pip mx_VoiceBroadcastBody--small"
|
||||
>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader"
|
||||
>
|
||||
<div
|
||||
class="mx_AccessibleButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
data-testid="room-avatar"
|
||||
>
|
||||
room avatar:
|
||||
My room
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader_content"
|
||||
>
|
||||
<div
|
||||
class="mx_AccessibleButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader_room_wrapper"
|
||||
>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader_room"
|
||||
>
|
||||
My room
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Change input device"
|
||||
class="mx_AccessibleButton mx_VoiceBroadcastHeader_line"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
class="mx_Icon mx_Icon_16"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M8 6a4 4 0 1 1 8 0v6a4 4 0 0 1-8 0V6Z"
|
||||
/>
|
||||
<path
|
||||
d="M5 11a1 1 0 0 1 1 1 6 6 0 0 0 12 0 1 1 0 1 1 2 0 8.001 8.001 0 0 1-7 7.938V21a1 1 0 1 1-2 0v-1.062A8.001 8.001 0 0 1 4 12a1 1 0 0 1 1-1Z"
|
||||
/>
|
||||
</svg>
|
||||
<span>
|
||||
@user:example.com
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-label="pause voice broadcast"
|
||||
class="mx_AccessibleButton mx_VoiceBroadcastControl"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
class="mx_Icon mx_Icon_12"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M8 4a2 2 0 0 0-2 2v12a2 2 0 1 0 4 0V6a2 2 0 0 0-2-2Zm8 0a2 2 0 0 0-2 2v12a2 2 0 1 0 4 0V6a2 2 0 0 0-2-2Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
class="mx_AccessibleButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
class="mx_Icon mx_Icon_8 mx_VoiceBroadcastBody__small-close"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6.293 6.293a1 1 0 0 1 1.414 0L12 10.586l4.293-4.293a1 1 0 1 1 1.414 1.414L13.414 12l4.293 4.293a1 1 0 0 1-1.414 1.414L12 13.414l-4.293 4.293a1 1 0 0 1-1.414-1.414L10.586 12 6.293 7.707a1 1 0 0 1 0-1.414Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<VoiceBroadcastSmallPlaybackBody /> when rendering a stopped broadcast should render as expected 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_VoiceBroadcastBody mx_VoiceBroadcastBody--pip mx_VoiceBroadcastBody--small"
|
||||
>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader"
|
||||
>
|
||||
<div
|
||||
class="mx_AccessibleButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
data-testid="room-avatar"
|
||||
>
|
||||
room avatar:
|
||||
My room
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader_content"
|
||||
>
|
||||
<div
|
||||
class="mx_AccessibleButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader_room_wrapper"
|
||||
>
|
||||
<div
|
||||
class="mx_VoiceBroadcastHeader_room"
|
||||
>
|
||||
My room
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Change input device"
|
||||
class="mx_AccessibleButton mx_VoiceBroadcastHeader_line"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
class="mx_Icon mx_Icon_16"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M8 6a4 4 0 1 1 8 0v6a4 4 0 0 1-8 0V6Z"
|
||||
/>
|
||||
<path
|
||||
d="M5 11a1 1 0 0 1 1 1 6 6 0 0 0 12 0 1 1 0 1 1 2 0 8.001 8.001 0 0 1-7 7.938V21a1 1 0 1 1-2 0v-1.062A8.001 8.001 0 0 1 4 12a1 1 0 0 1 1-1Z"
|
||||
/>
|
||||
</svg>
|
||||
<span>
|
||||
@user:example.com
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-label="play voice broadcast"
|
||||
class="mx_AccessibleButton mx_VoiceBroadcastControl mx_VoiceBroadcastControl-play"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
class="mx_Icon mx_Icon_16"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="m8.98 4.677 9.921 5.58c1.36.764 1.36 2.722 0 3.486l-9.92 5.58C7.647 20.073 6 19.11 6 17.58V6.42c0-1.53 1.647-2.493 2.98-1.743Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
class="mx_AccessibleButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
class="mx_Icon mx_Icon_8 mx_VoiceBroadcastBody__small-close"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6.293 6.293a1 1 0 0 1 1.414 0L12 10.586l4.293-4.293a1 1 0 1 1 1.414 1.414L13.414 12l4.293 4.293a1 1 0 0 1-1.414 1.414L12 13.414l-4.293 4.293a1 1 0 0 1-1.414-1.414L10.586 12 6.293 7.707a1 1 0 0 1 0-1.414Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
|
@ -0,0 +1,743 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { mocked } from "jest-mock";
|
||||
import { screen } from "jest-matrix-react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { MatrixClient, MatrixEvent, MatrixEventEvent, Room } from "matrix-js-sdk/src/matrix";
|
||||
import { defer } from "matrix-js-sdk/src/utils";
|
||||
|
||||
import { Playback, PlaybackState } from "../../../src/audio/Playback";
|
||||
import { PlaybackManager } from "../../../src/audio/PlaybackManager";
|
||||
import { SdkContextClass } from "../../../src/contexts/SDKContext";
|
||||
import { MediaEventHelper } from "../../../src/utils/MediaEventHelper";
|
||||
import {
|
||||
VoiceBroadcastInfoState,
|
||||
VoiceBroadcastLiveness,
|
||||
VoiceBroadcastPlayback,
|
||||
VoiceBroadcastPlaybackEvent,
|
||||
VoiceBroadcastPlaybackState,
|
||||
VoiceBroadcastRecording,
|
||||
} from "../../../src/voice-broadcast";
|
||||
import {
|
||||
filterConsole,
|
||||
flushPromises,
|
||||
flushPromisesWithFakeTimers,
|
||||
stubClient,
|
||||
waitEnoughCyclesForModal,
|
||||
} from "../../test-utils";
|
||||
import { createTestPlayback } from "../../test-utils/audio";
|
||||
import { mkVoiceBroadcastChunkEvent, mkVoiceBroadcastInfoStateEvent } from "../utils/test-utils";
|
||||
import { LazyValue } from "../../../src/utils/LazyValue";
|
||||
|
||||
jest.mock("../../../src/utils/MediaEventHelper", () => ({
|
||||
MediaEventHelper: jest.fn(),
|
||||
}));
|
||||
|
||||
describe("VoiceBroadcastPlayback", () => {
|
||||
const userId = "@user:example.com";
|
||||
let deviceId: string;
|
||||
const roomId = "!room:example.com";
|
||||
let room: Room;
|
||||
let client: MatrixClient;
|
||||
let infoEvent: MatrixEvent;
|
||||
let playback: VoiceBroadcastPlayback;
|
||||
let onStateChanged: (state: VoiceBroadcastPlaybackState) => void;
|
||||
let chunk1Event: MatrixEvent;
|
||||
let deplayedChunk1Event: MatrixEvent;
|
||||
let chunk2Event: MatrixEvent;
|
||||
let chunk2BEvent: MatrixEvent;
|
||||
let chunk3Event: MatrixEvent;
|
||||
const chunk1Length = 2300;
|
||||
const chunk2Length = 4200;
|
||||
const chunk3Length = 6900;
|
||||
const chunk1Data = new ArrayBuffer(2);
|
||||
const chunk2Data = new ArrayBuffer(3);
|
||||
const chunk3Data = new ArrayBuffer(3);
|
||||
let delayedChunk1Helper: MediaEventHelper;
|
||||
let chunk1Helper: MediaEventHelper;
|
||||
let chunk2Helper: MediaEventHelper;
|
||||
let chunk3Helper: MediaEventHelper;
|
||||
let chunk1Playback: Playback;
|
||||
let chunk2Playback: Playback;
|
||||
let chunk3Playback: Playback;
|
||||
let middleOfSecondChunk!: number;
|
||||
let middleOfThirdChunk!: number;
|
||||
|
||||
const queryConfirmListeningDialog = () => {
|
||||
return screen.queryByText(
|
||||
"If you start listening to this live broadcast, your current live broadcast recording will be ended.",
|
||||
);
|
||||
};
|
||||
|
||||
const itShouldSetTheStateTo = (state: VoiceBroadcastPlaybackState) => {
|
||||
it(`should set the state to ${state}`, () => {
|
||||
expect(playback.getState()).toBe(state);
|
||||
});
|
||||
};
|
||||
|
||||
const itShouldEmitAStateChangedEvent = (state: VoiceBroadcastPlaybackState) => {
|
||||
it(`should emit a ${state} state changed event`, () => {
|
||||
expect(mocked(onStateChanged)).toHaveBeenCalledWith(state, playback);
|
||||
});
|
||||
};
|
||||
|
||||
const itShouldHaveLiveness = (liveness: VoiceBroadcastLiveness): void => {
|
||||
it(`should have liveness ${liveness}`, () => {
|
||||
expect(playback.getLiveness()).toBe(liveness);
|
||||
});
|
||||
};
|
||||
|
||||
const startPlayback = () => {
|
||||
beforeEach(() => {
|
||||
playback.start();
|
||||
});
|
||||
};
|
||||
|
||||
const pausePlayback = () => {
|
||||
beforeEach(() => {
|
||||
playback.pause();
|
||||
});
|
||||
};
|
||||
|
||||
const stopPlayback = () => {
|
||||
beforeEach(() => {
|
||||
playback.stop();
|
||||
});
|
||||
};
|
||||
|
||||
const mkChunkHelper = (data: ArrayBuffer): MediaEventHelper => {
|
||||
return {
|
||||
sourceBlob: {
|
||||
cachedValue: new Blob(),
|
||||
done: false,
|
||||
value: {
|
||||
// @ts-ignore
|
||||
arrayBuffer: jest.fn().mockResolvedValue(data),
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const mkDeplayedChunkHelper = (data: ArrayBuffer): MediaEventHelper => {
|
||||
const deferred = defer<LazyValue<Blob>>();
|
||||
|
||||
setTimeout(() => {
|
||||
deferred.resolve({
|
||||
// @ts-ignore
|
||||
arrayBuffer: jest.fn().mockResolvedValue(data),
|
||||
});
|
||||
}, 7500);
|
||||
|
||||
return {
|
||||
sourceBlob: {
|
||||
cachedValue: new Blob(),
|
||||
done: false,
|
||||
// @ts-ignore
|
||||
value: deferred.promise,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const simulateFirstChunkArrived = async (): Promise<void> => {
|
||||
jest.advanceTimersByTime(10000);
|
||||
await flushPromisesWithFakeTimers();
|
||||
};
|
||||
|
||||
const mkInfoEvent = (state: VoiceBroadcastInfoState) => {
|
||||
return mkVoiceBroadcastInfoStateEvent(roomId, state, userId, deviceId);
|
||||
};
|
||||
|
||||
const mkPlayback = async (fakeTimers = false): Promise<VoiceBroadcastPlayback> => {
|
||||
const playback = new VoiceBroadcastPlayback(
|
||||
infoEvent,
|
||||
client,
|
||||
SdkContextClass.instance.voiceBroadcastRecordingsStore,
|
||||
);
|
||||
jest.spyOn(playback, "removeAllListeners");
|
||||
jest.spyOn(playback, "destroy");
|
||||
playback.on(VoiceBroadcastPlaybackEvent.StateChanged, onStateChanged);
|
||||
fakeTimers ? await flushPromisesWithFakeTimers() : await flushPromises();
|
||||
return playback;
|
||||
};
|
||||
|
||||
const setUpChunkEvents = (chunkEvents: MatrixEvent[]) => {
|
||||
mocked(client.relations).mockResolvedValueOnce({
|
||||
events: chunkEvents,
|
||||
});
|
||||
};
|
||||
|
||||
const createChunkEvents = () => {
|
||||
chunk1Event = mkVoiceBroadcastChunkEvent(infoEvent.getId()!, userId, roomId, chunk1Length, 1);
|
||||
deplayedChunk1Event = mkVoiceBroadcastChunkEvent(infoEvent.getId()!, userId, roomId, chunk1Length, 1);
|
||||
chunk2Event = mkVoiceBroadcastChunkEvent(infoEvent.getId()!, userId, roomId, chunk2Length, 2);
|
||||
chunk2Event.setTxnId("tx-id-1");
|
||||
chunk2BEvent = mkVoiceBroadcastChunkEvent(infoEvent.getId()!, userId, roomId, chunk2Length, 2);
|
||||
chunk2BEvent.setTxnId("tx-id-1");
|
||||
chunk3Event = mkVoiceBroadcastChunkEvent(infoEvent.getId()!, userId, roomId, chunk3Length, 3);
|
||||
|
||||
chunk1Helper = mkChunkHelper(chunk1Data);
|
||||
delayedChunk1Helper = mkDeplayedChunkHelper(chunk1Data);
|
||||
chunk2Helper = mkChunkHelper(chunk2Data);
|
||||
chunk3Helper = mkChunkHelper(chunk3Data);
|
||||
|
||||
chunk1Playback = createTestPlayback();
|
||||
chunk2Playback = createTestPlayback();
|
||||
chunk3Playback = createTestPlayback();
|
||||
|
||||
middleOfSecondChunk = (chunk1Length + chunk2Length / 2) / 1000;
|
||||
middleOfThirdChunk = (chunk1Length + chunk2Length + chunk3Length / 2) / 1000;
|
||||
|
||||
jest.spyOn(PlaybackManager.instance, "createPlaybackInstance").mockImplementation(
|
||||
(buffer: ArrayBuffer, _waveForm?: number[]) => {
|
||||
if (buffer === chunk1Data) return chunk1Playback;
|
||||
if (buffer === chunk2Data) return chunk2Playback;
|
||||
if (buffer === chunk3Data) return chunk3Playback;
|
||||
|
||||
throw new Error("unexpected buffer");
|
||||
},
|
||||
);
|
||||
|
||||
mocked(MediaEventHelper).mockImplementation((event: MatrixEvent): any => {
|
||||
if (event === chunk1Event) return chunk1Helper;
|
||||
if (event === deplayedChunk1Event) return delayedChunk1Helper;
|
||||
if (event === chunk2Event) return chunk2Helper;
|
||||
if (event === chunk3Event) return chunk3Helper;
|
||||
});
|
||||
};
|
||||
|
||||
filterConsole(
|
||||
// expected for some tests
|
||||
"Unable to load broadcast playback",
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
client = stubClient();
|
||||
deviceId = client.getDeviceId() || "";
|
||||
room = new Room(roomId, client, client.getSafeUserId());
|
||||
mocked(client.getRoom).mockImplementation((roomId: string): Room | null => {
|
||||
if (roomId === room.roomId) return room;
|
||||
return null;
|
||||
});
|
||||
onStateChanged = jest.fn();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
SdkContextClass.instance.voiceBroadcastPlaybacksStore.getCurrent()?.stop();
|
||||
SdkContextClass.instance.voiceBroadcastPlaybacksStore.clearCurrent();
|
||||
await SdkContextClass.instance.voiceBroadcastRecordingsStore.getCurrent()?.stop();
|
||||
SdkContextClass.instance.voiceBroadcastRecordingsStore.clearCurrent();
|
||||
playback.destroy();
|
||||
});
|
||||
|
||||
describe(`when there is a ${VoiceBroadcastInfoState.Resumed} broadcast without chunks yet`, () => {
|
||||
beforeEach(async () => {
|
||||
infoEvent = mkInfoEvent(VoiceBroadcastInfoState.Resumed);
|
||||
createChunkEvents();
|
||||
room.addLiveEvents([infoEvent]);
|
||||
playback = await mkPlayback();
|
||||
});
|
||||
|
||||
describe("and calling start", () => {
|
||||
startPlayback();
|
||||
|
||||
itShouldHaveLiveness("live");
|
||||
|
||||
it("should be in buffering state", () => {
|
||||
expect(playback.getState()).toBe(VoiceBroadcastPlaybackState.Buffering);
|
||||
});
|
||||
|
||||
it("should have duration 0", () => {
|
||||
expect(playback.durationSeconds).toBe(0);
|
||||
});
|
||||
|
||||
it("should be at time 0", () => {
|
||||
expect(playback.timeSeconds).toBe(0);
|
||||
});
|
||||
|
||||
describe("and calling stop", () => {
|
||||
stopPlayback();
|
||||
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Stopped);
|
||||
|
||||
describe("and calling pause", () => {
|
||||
pausePlayback();
|
||||
// stopped voice broadcasts cannot be paused
|
||||
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Stopped);
|
||||
});
|
||||
});
|
||||
|
||||
describe("and calling pause", () => {
|
||||
pausePlayback();
|
||||
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Paused);
|
||||
});
|
||||
|
||||
describe("and receiving the first chunk", () => {
|
||||
beforeEach(() => {
|
||||
room.relations.aggregateChildEvent(chunk1Event);
|
||||
});
|
||||
|
||||
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Playing);
|
||||
itShouldHaveLiveness("live");
|
||||
|
||||
it("should update the duration", () => {
|
||||
expect(playback.durationSeconds).toBe(2.3);
|
||||
});
|
||||
|
||||
it("should play the first chunk", () => {
|
||||
expect(chunk1Playback.play).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("and receiving the first undecryptable chunk", () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(chunk1Event, "isDecryptionFailure").mockReturnValue(true);
|
||||
room.relations.aggregateChildEvent(chunk1Event);
|
||||
});
|
||||
|
||||
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Error);
|
||||
|
||||
it("should not update the duration", () => {
|
||||
expect(playback.durationSeconds).toBe(0);
|
||||
});
|
||||
|
||||
describe("and the chunk is decrypted", () => {
|
||||
beforeEach(() => {
|
||||
mocked(chunk1Event.isDecryptionFailure).mockReturnValue(false);
|
||||
chunk1Event.emit(MatrixEventEvent.Decrypted, chunk1Event);
|
||||
});
|
||||
|
||||
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Paused);
|
||||
|
||||
it("should not update the duration", () => {
|
||||
expect(playback.durationSeconds).toBe(2.3);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe(`when there is a ${VoiceBroadcastInfoState.Resumed} voice broadcast with some chunks`, () => {
|
||||
beforeEach(async () => {
|
||||
mocked(client.relations).mockResolvedValueOnce({ events: [] });
|
||||
infoEvent = mkInfoEvent(VoiceBroadcastInfoState.Resumed);
|
||||
createChunkEvents();
|
||||
setUpChunkEvents([chunk2Event, chunk1Event]);
|
||||
room.addLiveEvents([infoEvent, chunk1Event, chunk2Event]);
|
||||
room.relations.aggregateChildEvent(chunk2Event);
|
||||
room.relations.aggregateChildEvent(chunk1Event);
|
||||
playback = await mkPlayback();
|
||||
});
|
||||
|
||||
it("durationSeconds should have the length of the known chunks", () => {
|
||||
expect(playback.durationSeconds).toEqual(6.5);
|
||||
});
|
||||
|
||||
describe("and starting a playback with a broken chunk", () => {
|
||||
beforeEach(async () => {
|
||||
mocked(chunk2Playback.prepare).mockRejectedValue("Error decoding chunk");
|
||||
await playback.start();
|
||||
});
|
||||
|
||||
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Error);
|
||||
|
||||
it("start() should keep it in the error state)", async () => {
|
||||
await playback.start();
|
||||
expect(playback.getState()).toBe(VoiceBroadcastPlaybackState.Error);
|
||||
});
|
||||
|
||||
it("stop() should keep it in the error state)", () => {
|
||||
playback.stop();
|
||||
expect(playback.getState()).toBe(VoiceBroadcastPlaybackState.Error);
|
||||
});
|
||||
|
||||
it("toggle() should keep it in the error state)", async () => {
|
||||
await playback.toggle();
|
||||
expect(playback.getState()).toBe(VoiceBroadcastPlaybackState.Error);
|
||||
});
|
||||
|
||||
it("pause() should keep it in the error state)", () => {
|
||||
playback.pause();
|
||||
expect(playback.getState()).toBe(VoiceBroadcastPlaybackState.Error);
|
||||
});
|
||||
});
|
||||
|
||||
describe("and an event with the same transaction Id occurs", () => {
|
||||
beforeEach(() => {
|
||||
room.addLiveEvents([chunk2BEvent]);
|
||||
room.relations.aggregateChildEvent(chunk2BEvent);
|
||||
});
|
||||
|
||||
it("durationSeconds should not change", () => {
|
||||
expect(playback.durationSeconds).toEqual(6.5);
|
||||
});
|
||||
});
|
||||
|
||||
describe("and calling start", () => {
|
||||
startPlayback();
|
||||
|
||||
it("should play the last chunk", () => {
|
||||
expect(playback.getState()).toBe(VoiceBroadcastPlaybackState.Playing);
|
||||
// assert that the last chunk is played first
|
||||
expect(chunk2Playback.play).toHaveBeenCalled();
|
||||
expect(chunk1Playback.play).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe(
|
||||
"and receiving a stop info event with last_chunk_sequence = 2 and " +
|
||||
"the playback of the last available chunk ends",
|
||||
() => {
|
||||
beforeEach(() => {
|
||||
const stoppedEvent = mkVoiceBroadcastInfoStateEvent(
|
||||
roomId,
|
||||
VoiceBroadcastInfoState.Stopped,
|
||||
client.getSafeUserId(),
|
||||
client.deviceId!,
|
||||
infoEvent,
|
||||
2,
|
||||
);
|
||||
room.addLiveEvents([stoppedEvent]);
|
||||
room.relations.aggregateChildEvent(stoppedEvent);
|
||||
chunk2Playback.emit(PlaybackState.Stopped);
|
||||
});
|
||||
|
||||
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Stopped);
|
||||
},
|
||||
);
|
||||
|
||||
describe(
|
||||
"and receiving a stop info event with last_chunk_sequence = 3 and " +
|
||||
"the playback of the last available chunk ends",
|
||||
() => {
|
||||
beforeEach(() => {
|
||||
const stoppedEvent = mkVoiceBroadcastInfoStateEvent(
|
||||
roomId,
|
||||
VoiceBroadcastInfoState.Stopped,
|
||||
client.getSafeUserId(),
|
||||
client.deviceId!,
|
||||
infoEvent,
|
||||
3,
|
||||
);
|
||||
room.addLiveEvents([stoppedEvent]);
|
||||
room.relations.aggregateChildEvent(stoppedEvent);
|
||||
chunk2Playback.emit(PlaybackState.Stopped);
|
||||
});
|
||||
|
||||
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Buffering);
|
||||
|
||||
describe("and the next chunk arrives", () => {
|
||||
beforeEach(() => {
|
||||
room.addLiveEvents([chunk3Event]);
|
||||
room.relations.aggregateChildEvent(chunk3Event);
|
||||
});
|
||||
|
||||
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Playing);
|
||||
|
||||
it("should play the next chunk", () => {
|
||||
expect(chunk3Playback.play).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
describe("and the info event is deleted", () => {
|
||||
beforeEach(() => {
|
||||
infoEvent.makeRedacted(new MatrixEvent({}), room);
|
||||
});
|
||||
|
||||
it("should stop and destroy the playback", () => {
|
||||
expect(playback.getState()).toBe(VoiceBroadcastPlaybackState.Stopped);
|
||||
expect(playback.destroy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("and currently recording a broadcast", () => {
|
||||
let recording: VoiceBroadcastRecording;
|
||||
|
||||
beforeEach(async () => {
|
||||
recording = new VoiceBroadcastRecording(
|
||||
mkVoiceBroadcastInfoStateEvent(
|
||||
roomId,
|
||||
VoiceBroadcastInfoState.Started,
|
||||
client.getSafeUserId(),
|
||||
client.deviceId,
|
||||
),
|
||||
client,
|
||||
);
|
||||
jest.spyOn(recording, "stop");
|
||||
SdkContextClass.instance.voiceBroadcastRecordingsStore.setCurrent(recording);
|
||||
playback.start();
|
||||
await waitEnoughCyclesForModal();
|
||||
});
|
||||
|
||||
it("should display a confirm modal", () => {
|
||||
expect(queryConfirmListeningDialog()).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe("when confirming the dialog", () => {
|
||||
beforeEach(async () => {
|
||||
await userEvent.click(screen.getByText("Yes, end my recording"));
|
||||
});
|
||||
|
||||
it("should stop the recording", () => {
|
||||
expect(recording.stop).toHaveBeenCalled();
|
||||
expect(SdkContextClass.instance.voiceBroadcastRecordingsStore.getCurrent()).toBeNull();
|
||||
});
|
||||
|
||||
it("should not start the playback", () => {
|
||||
expect(playback.getState()).toBe(VoiceBroadcastPlaybackState.Playing);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when not confirming the dialog", () => {
|
||||
beforeEach(async () => {
|
||||
await userEvent.click(screen.getByText("No"));
|
||||
});
|
||||
|
||||
it("should not stop the recording", () => {
|
||||
expect(recording.stop).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should start the playback", () => {
|
||||
expect(playback.getState()).toBe(VoiceBroadcastPlaybackState.Stopped);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when there is a stopped voice broadcast", () => {
|
||||
beforeEach(async () => {
|
||||
jest.useFakeTimers();
|
||||
infoEvent = mkInfoEvent(VoiceBroadcastInfoState.Stopped);
|
||||
createChunkEvents();
|
||||
// use delayed first chunk here to simulate loading time
|
||||
setUpChunkEvents([chunk2Event, deplayedChunk1Event, chunk3Event]);
|
||||
room.addLiveEvents([infoEvent, deplayedChunk1Event, chunk2Event, chunk3Event]);
|
||||
playback = await mkPlayback(true);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it("should expose the info event", () => {
|
||||
expect(playback.infoEvent).toBe(infoEvent);
|
||||
});
|
||||
|
||||
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Stopped);
|
||||
|
||||
describe("and calling start", () => {
|
||||
startPlayback();
|
||||
|
||||
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Buffering);
|
||||
|
||||
describe("and the first chunk data has been loaded", () => {
|
||||
beforeEach(async () => {
|
||||
await simulateFirstChunkArrived();
|
||||
});
|
||||
|
||||
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Playing);
|
||||
|
||||
it("should play the chunks beginning with the first one", () => {
|
||||
// assert that the first chunk is being played
|
||||
expect(chunk1Playback.play).toHaveBeenCalled();
|
||||
expect(chunk2Playback.play).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe("and calling start again", () => {
|
||||
it("should not play the first chunk a second time", () => {
|
||||
expect(chunk1Playback.play).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("and the chunk playback progresses", () => {
|
||||
beforeEach(() => {
|
||||
chunk1Playback.clockInfo.liveData.update([11]);
|
||||
});
|
||||
|
||||
it("should update the time", () => {
|
||||
expect(playback.timeSeconds).toBe(11);
|
||||
});
|
||||
});
|
||||
|
||||
describe("and the chunk playback progresses across the actual time", () => {
|
||||
// This can be the case if the meta data is out of sync with the actual audio data.
|
||||
|
||||
beforeEach(() => {
|
||||
chunk1Playback.clockInfo.liveData.update([15]);
|
||||
});
|
||||
|
||||
it("should update the time", () => {
|
||||
expect(playback.timeSeconds).toBe(15);
|
||||
expect(playback.timeLeftSeconds).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("and skipping to the middle of the second chunk", () => {
|
||||
const middleOfSecondChunk = (chunk1Length + chunk2Length / 2) / 1000;
|
||||
|
||||
beforeEach(async () => {
|
||||
await playback.skipTo(middleOfSecondChunk);
|
||||
});
|
||||
|
||||
it("should play the second chunk", () => {
|
||||
expect(chunk1Playback.stop).toHaveBeenCalled();
|
||||
expect(chunk1Playback.destroy).toHaveBeenCalled();
|
||||
expect(chunk2Playback.play).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should update the time", () => {
|
||||
expect(playback.timeSeconds).toBe(middleOfSecondChunk);
|
||||
});
|
||||
|
||||
describe("and skipping to the start", () => {
|
||||
beforeEach(async () => {
|
||||
await playback.skipTo(0);
|
||||
});
|
||||
|
||||
it("should play the first chunk", () => {
|
||||
expect(chunk2Playback.stop).toHaveBeenCalled();
|
||||
expect(chunk2Playback.destroy).toHaveBeenCalled();
|
||||
expect(chunk1Playback.play).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should update the time", () => {
|
||||
expect(playback.timeSeconds).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("and skipping multiple times", () => {
|
||||
beforeEach(async () => {
|
||||
return Promise.all([
|
||||
playback.skipTo(middleOfSecondChunk),
|
||||
playback.skipTo(middleOfThirdChunk),
|
||||
playback.skipTo(0),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should only skip to the first and last position", () => {
|
||||
expect(chunk1Playback.stop).toHaveBeenCalled();
|
||||
expect(chunk1Playback.destroy).toHaveBeenCalled();
|
||||
expect(chunk2Playback.play).toHaveBeenCalled();
|
||||
|
||||
expect(chunk3Playback.play).not.toHaveBeenCalled();
|
||||
|
||||
expect(chunk2Playback.stop).toHaveBeenCalled();
|
||||
expect(chunk2Playback.destroy).toHaveBeenCalled();
|
||||
expect(chunk1Playback.play).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("and the first chunk ends", () => {
|
||||
beforeEach(() => {
|
||||
chunk1Playback.emit(PlaybackState.Stopped);
|
||||
});
|
||||
|
||||
it("should play until the end", () => {
|
||||
// assert first chunk was unloaded
|
||||
expect(chunk1Playback.destroy).toHaveBeenCalled();
|
||||
|
||||
// assert that the second chunk is being played
|
||||
expect(chunk2Playback.play).toHaveBeenCalled();
|
||||
|
||||
// simulate end of second and third chunk
|
||||
chunk2Playback.emit(PlaybackState.Stopped);
|
||||
chunk3Playback.emit(PlaybackState.Stopped);
|
||||
|
||||
// assert that the entire playback is now in stopped state
|
||||
expect(playback.getState()).toBe(VoiceBroadcastPlaybackState.Stopped);
|
||||
});
|
||||
});
|
||||
|
||||
describe("and calling pause", () => {
|
||||
pausePlayback();
|
||||
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Paused);
|
||||
itShouldEmitAStateChangedEvent(VoiceBroadcastPlaybackState.Paused);
|
||||
});
|
||||
|
||||
describe("and calling stop", () => {
|
||||
stopPlayback();
|
||||
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Stopped);
|
||||
|
||||
it("should stop the playback", () => {
|
||||
expect(chunk1Playback.stop).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe("and skipping to somewhere in the middle of the first chunk", () => {
|
||||
beforeEach(async () => {
|
||||
mocked(chunk1Playback.play).mockClear();
|
||||
await playback.skipTo(1);
|
||||
});
|
||||
|
||||
it("should not start the playback", () => {
|
||||
expect(chunk1Playback.play).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("and calling destroy", () => {
|
||||
beforeEach(() => {
|
||||
playback.destroy();
|
||||
});
|
||||
|
||||
it("should call removeAllListeners", () => {
|
||||
expect(playback.removeAllListeners).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should call destroy on the playbacks", () => {
|
||||
expect(chunk1Playback.destroy).toHaveBeenCalled();
|
||||
expect(chunk2Playback.destroy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("and calling toggle for the first time", () => {
|
||||
beforeEach(async () => {
|
||||
playback.toggle();
|
||||
await simulateFirstChunkArrived();
|
||||
});
|
||||
|
||||
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Playing);
|
||||
|
||||
describe("and calling toggle a second time", () => {
|
||||
beforeEach(async () => {
|
||||
await playback.toggle();
|
||||
});
|
||||
|
||||
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Paused);
|
||||
|
||||
describe("and calling toggle a third time", () => {
|
||||
beforeEach(async () => {
|
||||
await playback.toggle();
|
||||
});
|
||||
|
||||
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Playing);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("and calling stop", () => {
|
||||
stopPlayback();
|
||||
|
||||
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Stopped);
|
||||
|
||||
describe("and calling toggle", () => {
|
||||
beforeEach(async () => {
|
||||
mocked(onStateChanged).mockReset();
|
||||
playback.toggle();
|
||||
await simulateFirstChunkArrived();
|
||||
});
|
||||
|
||||
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Playing);
|
||||
itShouldEmitAStateChangedEvent(VoiceBroadcastPlaybackState.Playing);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { MatrixClient, Room, RoomMember } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import {
|
||||
startNewVoiceBroadcastRecording,
|
||||
VoiceBroadcastPlaybacksStore,
|
||||
VoiceBroadcastPreRecording,
|
||||
VoiceBroadcastRecordingsStore,
|
||||
} from "../../../src/voice-broadcast";
|
||||
import { stubClient } from "../../test-utils";
|
||||
|
||||
jest.mock("../../../src/voice-broadcast/utils/startNewVoiceBroadcastRecording");
|
||||
|
||||
describe("VoiceBroadcastPreRecording", () => {
|
||||
const roomId = "!room:example.com";
|
||||
let client: MatrixClient;
|
||||
let room: Room;
|
||||
let sender: RoomMember;
|
||||
let playbacksStore: VoiceBroadcastPlaybacksStore;
|
||||
let recordingsStore: VoiceBroadcastRecordingsStore;
|
||||
let preRecording: VoiceBroadcastPreRecording;
|
||||
let onDismiss: (voiceBroadcastPreRecording: VoiceBroadcastPreRecording) => void;
|
||||
|
||||
beforeAll(() => {
|
||||
client = stubClient();
|
||||
room = new Room(roomId, client, client.getUserId() || "");
|
||||
sender = new RoomMember(roomId, client.getUserId() || "");
|
||||
recordingsStore = new VoiceBroadcastRecordingsStore();
|
||||
playbacksStore = new VoiceBroadcastPlaybacksStore(recordingsStore);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
onDismiss = jest.fn();
|
||||
preRecording = new VoiceBroadcastPreRecording(room, sender, client, playbacksStore, recordingsStore);
|
||||
preRecording.on("dismiss", onDismiss);
|
||||
});
|
||||
|
||||
describe("start", () => {
|
||||
beforeEach(() => {
|
||||
preRecording.start();
|
||||
});
|
||||
|
||||
it("should start a new voice broadcast recording", () => {
|
||||
expect(startNewVoiceBroadcastRecording).toHaveBeenCalledWith(room, client, playbacksStore, recordingsStore);
|
||||
});
|
||||
|
||||
it("should emit a dismiss event", () => {
|
||||
expect(onDismiss).toHaveBeenCalledWith(preRecording);
|
||||
});
|
||||
});
|
||||
|
||||
describe("cancel", () => {
|
||||
beforeEach(() => {
|
||||
preRecording.cancel();
|
||||
});
|
||||
|
||||
it("should emit a dismiss event", () => {
|
||||
expect(onDismiss).toHaveBeenCalledWith(preRecording);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,660 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { mocked } from "jest-mock";
|
||||
import {
|
||||
ClientEvent,
|
||||
EventTimelineSet,
|
||||
EventType,
|
||||
LOCAL_NOTIFICATION_SETTINGS_PREFIX,
|
||||
MatrixClient,
|
||||
MatrixEvent,
|
||||
MatrixEventEvent,
|
||||
MsgType,
|
||||
RelationType,
|
||||
Room,
|
||||
Relations,
|
||||
SyncState,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import { EncryptedFile } from "matrix-js-sdk/src/types";
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
|
||||
import { uploadFile } from "../../../src/ContentMessages";
|
||||
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";
|
||||
import { createAudioContext } from "../../../src/audio/compat";
|
||||
|
||||
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(),
|
||||
}));
|
||||
|
||||
jest.mock("../../../src/audio/compat", () => ({
|
||||
...jest.requireActual("../../../src/audio/compat"),
|
||||
createAudioContext: jest.fn(),
|
||||
}));
|
||||
|
||||
describe("VoiceBroadcastRecording", () => {
|
||||
const roomId = "!room:example.com";
|
||||
const uploadedUrl = "mxc://example.com/vb";
|
||||
const uploadedFile = { file: true } as unknown as EncryptedFile;
|
||||
const maxLength = getMaxBroadcastLength();
|
||||
let room: Room;
|
||||
let client: MatrixClient;
|
||||
let infoEvent: MatrixEvent;
|
||||
let voiceBroadcastRecording: VoiceBroadcastRecording;
|
||||
let onStateChanged: (state: VoiceBroadcastRecordingState) => void;
|
||||
let voiceBroadcastRecorder: VoiceBroadcastRecorder;
|
||||
let audioElement: HTMLAudioElement;
|
||||
|
||||
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,
|
||||
});
|
||||
};
|
||||
|
||||
const mockAudioBufferSourceNode = {
|
||||
addEventListener: jest.fn(),
|
||||
connect: jest.fn(),
|
||||
start: jest.fn(),
|
||||
};
|
||||
const mockAudioContext = {
|
||||
decodeAudioData: jest.fn(),
|
||||
suspend: jest.fn(),
|
||||
resume: jest.fn(),
|
||||
createBufferSource: jest.fn().mockReturnValue(mockAudioBufferSourceNode),
|
||||
currentTime: 1337,
|
||||
};
|
||||
|
||||
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?: EncryptedFile,
|
||||
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
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
audioElement = {
|
||||
play: jest.fn(),
|
||||
} as any as HTMLAudioElement;
|
||||
|
||||
jest.spyOn(document, "querySelector").mockImplementation((selector: string) => {
|
||||
if (selector === "audio#errorAudio") {
|
||||
return audioElement;
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
mocked(createAudioContext).mockReturnValue(mockAudioContext as unknown as AudioContext);
|
||||
});
|
||||
|
||||
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 audible notifications are disabled", () => {
|
||||
beforeEach(() => {
|
||||
const notificationSettings = mkEvent({
|
||||
event: true,
|
||||
type: `${LOCAL_NOTIFICATION_SETTINGS_PREFIX.name}.${client.getDeviceId()}`,
|
||||
user: client.getSafeUserId(),
|
||||
content: {
|
||||
is_silenced: true,
|
||||
},
|
||||
});
|
||||
mocked(client.getAccountData).mockReturnValue(notificationSettings);
|
||||
});
|
||||
|
||||
describe("and a chunk has been recorded and sending the voice message fails", () => {
|
||||
beforeEach(() => {
|
||||
mocked(client.sendMessage).mockRejectedValue("Error");
|
||||
emitFirsChunkRecorded();
|
||||
});
|
||||
|
||||
itShouldBeInState("connection_error");
|
||||
|
||||
it("should not play a notification", () => {
|
||||
expect(audioElement.play).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("and a chunk has been recorded and sending the voice message fails", () => {
|
||||
beforeEach(() => {
|
||||
mocked(client.sendMessage).mockRejectedValue("Error");
|
||||
emitFirsChunkRecorded();
|
||||
fetchMock.get("media/error.mp3", 200);
|
||||
});
|
||||
|
||||
itShouldBeInState("connection_error");
|
||||
|
||||
it("should play a notification", () => {
|
||||
expect(mockAudioBufferSourceNode.start).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,162 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { mocked } from "jest-mock";
|
||||
import { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import {
|
||||
VoiceBroadcastInfoState,
|
||||
VoiceBroadcastPlayback,
|
||||
VoiceBroadcastPlaybackEvent,
|
||||
VoiceBroadcastPlaybacksStore,
|
||||
VoiceBroadcastPlaybacksStoreEvent,
|
||||
VoiceBroadcastPlaybackState,
|
||||
VoiceBroadcastRecordingsStore,
|
||||
} from "../../../src/voice-broadcast";
|
||||
import { mkStubRoom, stubClient } from "../../test-utils";
|
||||
import { mkVoiceBroadcastInfoStateEvent } from "../utils/test-utils";
|
||||
|
||||
describe("VoiceBroadcastPlaybacksStore", () => {
|
||||
const roomId = "!room:example.com";
|
||||
let client: MatrixClient;
|
||||
let userId: string;
|
||||
let deviceId: string;
|
||||
let room: Room;
|
||||
let infoEvent1: MatrixEvent;
|
||||
let infoEvent2: MatrixEvent;
|
||||
let playback1: VoiceBroadcastPlayback;
|
||||
let playback2: VoiceBroadcastPlayback;
|
||||
let playbacks: VoiceBroadcastPlaybacksStore;
|
||||
let onCurrentChanged: (playback: VoiceBroadcastPlayback | null) => void;
|
||||
|
||||
beforeEach(() => {
|
||||
client = stubClient();
|
||||
userId = client.getUserId() || "";
|
||||
deviceId = client.getDeviceId() || "";
|
||||
mocked(client.relations).mockClear();
|
||||
mocked(client.relations).mockResolvedValue({ events: [] });
|
||||
|
||||
room = mkStubRoom(roomId, "test room", client);
|
||||
mocked(client.getRoom).mockImplementation((roomId: string): Room | null => {
|
||||
if (roomId === room.roomId) {
|
||||
return room;
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
infoEvent1 = mkVoiceBroadcastInfoStateEvent(roomId, VoiceBroadcastInfoState.Started, userId, deviceId);
|
||||
infoEvent2 = mkVoiceBroadcastInfoStateEvent(roomId, VoiceBroadcastInfoState.Started, userId, deviceId);
|
||||
const recordings = new VoiceBroadcastRecordingsStore();
|
||||
playback1 = new VoiceBroadcastPlayback(infoEvent1, client, recordings);
|
||||
jest.spyOn(playback1, "off");
|
||||
playback2 = new VoiceBroadcastPlayback(infoEvent2, client, recordings);
|
||||
jest.spyOn(playback2, "off");
|
||||
|
||||
playbacks = new VoiceBroadcastPlaybacksStore(recordings);
|
||||
jest.spyOn(playbacks, "removeAllListeners");
|
||||
onCurrentChanged = jest.fn();
|
||||
playbacks.on(VoiceBroadcastPlaybacksStoreEvent.CurrentChanged, onCurrentChanged);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
playbacks.off(VoiceBroadcastPlaybacksStoreEvent.CurrentChanged, onCurrentChanged);
|
||||
});
|
||||
|
||||
describe("when setting a current Voice Broadcast playback", () => {
|
||||
beforeEach(() => {
|
||||
playbacks.setCurrent(playback1);
|
||||
});
|
||||
|
||||
it("should return it as current", () => {
|
||||
expect(playbacks.getCurrent()).toBe(playback1);
|
||||
});
|
||||
|
||||
it("should return it by id", () => {
|
||||
expect(playbacks.getByInfoEvent(infoEvent1, client)).toBe(playback1);
|
||||
});
|
||||
|
||||
it("should emit a CurrentChanged event", () => {
|
||||
expect(onCurrentChanged).toHaveBeenCalledWith(playback1);
|
||||
});
|
||||
|
||||
describe("and setting the same again", () => {
|
||||
beforeEach(() => {
|
||||
mocked(onCurrentChanged).mockClear();
|
||||
playbacks.setCurrent(playback1);
|
||||
});
|
||||
|
||||
it("should not emit a CurrentChanged event", () => {
|
||||
expect(onCurrentChanged).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("and setting another playback and start both", () => {
|
||||
beforeEach(() => {
|
||||
playbacks.setCurrent(playback2);
|
||||
playback1.start();
|
||||
playback2.start();
|
||||
});
|
||||
|
||||
it("should set playback1 to paused", () => {
|
||||
expect(playback1.getState()).toBe(VoiceBroadcastPlaybackState.Paused);
|
||||
});
|
||||
|
||||
it("should set playback2 to buffering", () => {
|
||||
// buffering because there are no chunks, yet
|
||||
expect(playback2.getState()).toBe(VoiceBroadcastPlaybackState.Buffering);
|
||||
});
|
||||
|
||||
describe("and calling destroy", () => {
|
||||
beforeEach(() => {
|
||||
playbacks.destroy();
|
||||
});
|
||||
|
||||
it("should remove all listeners", () => {
|
||||
expect(playbacks.removeAllListeners).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should deregister the listeners on the playbacks", () => {
|
||||
expect(playback1.off).toHaveBeenCalledWith(
|
||||
VoiceBroadcastPlaybackEvent.StateChanged,
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(playback2.off).toHaveBeenCalledWith(
|
||||
VoiceBroadcastPlaybackEvent.StateChanged,
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("getByInfoEventId", () => {
|
||||
let returnedPlayback: VoiceBroadcastPlayback;
|
||||
|
||||
describe("when retrieving a known playback", () => {
|
||||
beforeEach(() => {
|
||||
playbacks.setCurrent(playback1);
|
||||
returnedPlayback = playbacks.getByInfoEvent(infoEvent1, client);
|
||||
});
|
||||
|
||||
it("should return the playback", () => {
|
||||
expect(returnedPlayback).toBe(playback1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when retrieving an unknown playback", () => {
|
||||
beforeEach(() => {
|
||||
returnedPlayback = playbacks.getByInfoEvent(infoEvent1, client);
|
||||
});
|
||||
|
||||
it("should return the playback", () => {
|
||||
expect(returnedPlayback.infoEvent).toBe(infoEvent1);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,130 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { mocked } from "jest-mock";
|
||||
import { MatrixClient, Room, RoomMember } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import {
|
||||
VoiceBroadcastPlaybacksStore,
|
||||
VoiceBroadcastPreRecording,
|
||||
VoiceBroadcastPreRecordingStore,
|
||||
VoiceBroadcastRecordingsStore,
|
||||
} from "../../../src/voice-broadcast";
|
||||
import { stubClient } from "../../test-utils";
|
||||
|
||||
describe("VoiceBroadcastPreRecordingStore", () => {
|
||||
const roomId = "!room:example.com";
|
||||
let client: MatrixClient;
|
||||
let room: Room;
|
||||
let sender: RoomMember;
|
||||
let playbacksStore: VoiceBroadcastPlaybacksStore;
|
||||
let recordingsStore: VoiceBroadcastRecordingsStore;
|
||||
let store: VoiceBroadcastPreRecordingStore;
|
||||
let preRecording1: VoiceBroadcastPreRecording;
|
||||
|
||||
beforeAll(() => {
|
||||
client = stubClient();
|
||||
room = new Room(roomId, client, client.getUserId() || "");
|
||||
sender = new RoomMember(roomId, client.getUserId() || "");
|
||||
recordingsStore = new VoiceBroadcastRecordingsStore();
|
||||
playbacksStore = new VoiceBroadcastPlaybacksStore(recordingsStore);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
store = new VoiceBroadcastPreRecordingStore();
|
||||
jest.spyOn(store, "emit");
|
||||
jest.spyOn(store, "removeAllListeners");
|
||||
preRecording1 = new VoiceBroadcastPreRecording(room, sender, client, playbacksStore, recordingsStore);
|
||||
jest.spyOn(preRecording1, "off");
|
||||
});
|
||||
|
||||
it("getCurrent() should return null", () => {
|
||||
expect(store.getCurrent()).toBeNull();
|
||||
});
|
||||
|
||||
it("clearCurrent() should work", () => {
|
||||
store.clearCurrent();
|
||||
expect(store.getCurrent()).toBeNull();
|
||||
});
|
||||
|
||||
describe("when setting a current recording", () => {
|
||||
beforeEach(() => {
|
||||
store.setCurrent(preRecording1);
|
||||
});
|
||||
|
||||
it("getCurrent() should return the recording", () => {
|
||||
expect(store.getCurrent()).toBe(preRecording1);
|
||||
});
|
||||
|
||||
it("should emit a changed event with the recording", () => {
|
||||
expect(store.emit).toHaveBeenCalledWith("changed", preRecording1);
|
||||
});
|
||||
|
||||
describe("and calling destroy()", () => {
|
||||
beforeEach(() => {
|
||||
store.destroy();
|
||||
});
|
||||
|
||||
it("should remove all listeners", () => {
|
||||
expect(store.removeAllListeners).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should deregister from the pre-recordings", () => {
|
||||
expect(preRecording1.off).toHaveBeenCalledWith("dismiss", expect.any(Function));
|
||||
});
|
||||
});
|
||||
|
||||
describe("and cancelling the pre-recording", () => {
|
||||
beforeEach(() => {
|
||||
preRecording1.cancel();
|
||||
});
|
||||
|
||||
it("should clear the current recording", () => {
|
||||
expect(store.getCurrent()).toBeNull();
|
||||
});
|
||||
|
||||
it("should emit a changed event with null", () => {
|
||||
expect(store.emit).toHaveBeenCalledWith("changed", null);
|
||||
});
|
||||
});
|
||||
|
||||
describe("and setting the same pre-recording again", () => {
|
||||
beforeEach(() => {
|
||||
mocked(store.emit).mockClear();
|
||||
store.setCurrent(preRecording1);
|
||||
});
|
||||
|
||||
it("should not emit a changed event", () => {
|
||||
expect(store.emit).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("and setting another pre-recording", () => {
|
||||
let preRecording2: VoiceBroadcastPreRecording;
|
||||
|
||||
beforeEach(() => {
|
||||
mocked(store.emit).mockClear();
|
||||
mocked(preRecording1.off).mockClear();
|
||||
preRecording2 = new VoiceBroadcastPreRecording(room, sender, client, playbacksStore, recordingsStore);
|
||||
store.setCurrent(preRecording2);
|
||||
});
|
||||
|
||||
it("should deregister from the current pre-recording", () => {
|
||||
expect(preRecording1.off).toHaveBeenCalledWith("dismiss", expect.any(Function));
|
||||
});
|
||||
|
||||
it("getCurrent() should return the new recording", () => {
|
||||
expect(store.getCurrent()).toBe(preRecording2);
|
||||
});
|
||||
|
||||
it("should emit a changed event with the new recording", () => {
|
||||
expect(store.emit).toHaveBeenCalledWith("changed", preRecording2);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,167 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { mocked } from "jest-mock";
|
||||
import { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import {
|
||||
VoiceBroadcastRecordingsStore,
|
||||
VoiceBroadcastRecordingsStoreEvent,
|
||||
VoiceBroadcastRecording,
|
||||
VoiceBroadcastInfoState,
|
||||
} from "../../../src/voice-broadcast";
|
||||
import { mkStubRoom, stubClient } from "../../test-utils";
|
||||
import { mkVoiceBroadcastInfoStateEvent } from "../utils/test-utils";
|
||||
|
||||
describe("VoiceBroadcastRecordingsStore", () => {
|
||||
const roomId = "!room:example.com";
|
||||
let client: MatrixClient;
|
||||
let room: Room;
|
||||
let infoEvent: MatrixEvent;
|
||||
let otherInfoEvent: MatrixEvent;
|
||||
let recording: VoiceBroadcastRecording;
|
||||
let otherRecording: VoiceBroadcastRecording;
|
||||
let recordings: VoiceBroadcastRecordingsStore;
|
||||
let onCurrentChanged: (recording: VoiceBroadcastRecording | null) => void;
|
||||
|
||||
beforeEach(() => {
|
||||
client = stubClient();
|
||||
room = mkStubRoom(roomId, "test room", client);
|
||||
mocked(client.getRoom).mockImplementation((roomId: string) => {
|
||||
if (roomId === room.roomId) {
|
||||
return room;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
infoEvent = mkVoiceBroadcastInfoStateEvent(
|
||||
roomId,
|
||||
VoiceBroadcastInfoState.Started,
|
||||
client.getUserId()!,
|
||||
client.getDeviceId()!,
|
||||
);
|
||||
otherInfoEvent = mkVoiceBroadcastInfoStateEvent(
|
||||
roomId,
|
||||
VoiceBroadcastInfoState.Started,
|
||||
client.getUserId()!,
|
||||
client.getDeviceId()!,
|
||||
);
|
||||
recording = new VoiceBroadcastRecording(infoEvent, client);
|
||||
otherRecording = new VoiceBroadcastRecording(otherInfoEvent, client);
|
||||
recordings = new VoiceBroadcastRecordingsStore();
|
||||
onCurrentChanged = jest.fn();
|
||||
recordings.on(VoiceBroadcastRecordingsStoreEvent.CurrentChanged, onCurrentChanged);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
recording.destroy();
|
||||
recordings.off(VoiceBroadcastRecordingsStoreEvent.CurrentChanged, onCurrentChanged);
|
||||
});
|
||||
|
||||
it("when setting a recording without info event Id, it should raise an error", () => {
|
||||
infoEvent.event.event_id = undefined;
|
||||
expect(() => {
|
||||
recordings.setCurrent(recording);
|
||||
}).toThrow("Got broadcast info event without Id");
|
||||
});
|
||||
|
||||
describe("when setting a current Voice Broadcast recording", () => {
|
||||
beforeEach(() => {
|
||||
recordings.setCurrent(recording);
|
||||
});
|
||||
|
||||
it("should return it as current", () => {
|
||||
expect(recordings.hasCurrent()).toBe(true);
|
||||
expect(recordings.getCurrent()).toBe(recording);
|
||||
});
|
||||
|
||||
it("should return it by id", () => {
|
||||
expect(recordings.getByInfoEvent(infoEvent, client)).toBe(recording);
|
||||
});
|
||||
|
||||
it("should emit a CurrentChanged event", () => {
|
||||
expect(onCurrentChanged).toHaveBeenCalledWith(recording);
|
||||
});
|
||||
|
||||
describe("and setting the same again", () => {
|
||||
beforeEach(() => {
|
||||
mocked(onCurrentChanged).mockClear();
|
||||
recordings.setCurrent(recording);
|
||||
});
|
||||
|
||||
it("should not emit a CurrentChanged event", () => {
|
||||
expect(onCurrentChanged).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("and calling clearCurrent()", () => {
|
||||
beforeEach(() => {
|
||||
recordings.clearCurrent();
|
||||
});
|
||||
|
||||
it("should clear the current recording", () => {
|
||||
expect(recordings.hasCurrent()).toBe(false);
|
||||
expect(recordings.getCurrent()).toBeNull();
|
||||
});
|
||||
|
||||
it("should emit a current changed event", () => {
|
||||
expect(onCurrentChanged).toHaveBeenCalledWith(null);
|
||||
});
|
||||
|
||||
it("and calling it again should work", () => {
|
||||
recordings.clearCurrent();
|
||||
expect(recordings.getCurrent()).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("and setting another recording and stopping the previous recording", () => {
|
||||
beforeEach(() => {
|
||||
recordings.setCurrent(otherRecording);
|
||||
recording.stop();
|
||||
});
|
||||
|
||||
it("should keep the current recording", () => {
|
||||
expect(recordings.getCurrent()).toBe(otherRecording);
|
||||
});
|
||||
});
|
||||
|
||||
describe("and the recording stops", () => {
|
||||
beforeEach(() => {
|
||||
recording.stop();
|
||||
});
|
||||
|
||||
it("should clear the current recording", () => {
|
||||
expect(recordings.getCurrent()).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("getByInfoEventId", () => {
|
||||
let returnedRecording: VoiceBroadcastRecording;
|
||||
|
||||
describe("when retrieving a known recording", () => {
|
||||
beforeEach(() => {
|
||||
recordings.setCurrent(recording);
|
||||
returnedRecording = recordings.getByInfoEvent(infoEvent, client);
|
||||
});
|
||||
|
||||
it("should return the recording", () => {
|
||||
expect(returnedRecording).toBe(recording);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when retrieving an unknown recording", () => {
|
||||
beforeEach(() => {
|
||||
returnedRecording = recordings.getByInfoEvent(infoEvent, client);
|
||||
});
|
||||
|
||||
it("should return the recording", () => {
|
||||
expect(returnedRecording.infoEvent).toBe(infoEvent);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,142 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { VoiceBroadcastChunkEvents } from "../../../src/voice-broadcast/utils/VoiceBroadcastChunkEvents";
|
||||
import { mkVoiceBroadcastChunkEvent } from "./test-utils";
|
||||
|
||||
describe("VoiceBroadcastChunkEvents", () => {
|
||||
const userId = "@user:example.com";
|
||||
const roomId = "!room:example.com";
|
||||
const txnId = "txn-id";
|
||||
let eventSeq1Time1: MatrixEvent;
|
||||
let eventSeq2Time4: MatrixEvent;
|
||||
let eventSeq3Time2: MatrixEvent;
|
||||
let eventSeq3Time2T: MatrixEvent;
|
||||
let eventSeq4Time1: MatrixEvent;
|
||||
let eventSeqUTime3: MatrixEvent;
|
||||
let eventSeq2Time4Dup: MatrixEvent;
|
||||
let chunkEvents: VoiceBroadcastChunkEvents;
|
||||
|
||||
beforeEach(() => {
|
||||
eventSeq1Time1 = mkVoiceBroadcastChunkEvent("info1", userId, roomId, 7, 1, 1);
|
||||
eventSeq2Time4 = mkVoiceBroadcastChunkEvent("info1", userId, roomId, 23, 2, 4);
|
||||
eventSeq2Time4Dup = mkVoiceBroadcastChunkEvent("info1", userId, roomId, 3141, 2, 4);
|
||||
jest.spyOn(eventSeq2Time4Dup, "getId").mockReturnValue(eventSeq2Time4.getId());
|
||||
eventSeq3Time2 = mkVoiceBroadcastChunkEvent("info1", userId, roomId, 42, 3, 2);
|
||||
eventSeq3Time2.setTxnId(txnId);
|
||||
eventSeq3Time2T = mkVoiceBroadcastChunkEvent("info1", userId, roomId, 42, 3, 2);
|
||||
eventSeq3Time2T.setTxnId(txnId);
|
||||
eventSeq4Time1 = mkVoiceBroadcastChunkEvent("info1", userId, roomId, 69, 4, 1);
|
||||
eventSeqUTime3 = mkVoiceBroadcastChunkEvent("info1", userId, roomId, 314, undefined, 3);
|
||||
chunkEvents = new VoiceBroadcastChunkEvents();
|
||||
});
|
||||
|
||||
describe("when adding events that all have a sequence", () => {
|
||||
beforeEach(() => {
|
||||
chunkEvents.addEvent(eventSeq2Time4);
|
||||
chunkEvents.addEvent(eventSeq1Time1);
|
||||
chunkEvents.addEvents([eventSeq4Time1, eventSeq2Time4Dup, eventSeq3Time2]);
|
||||
});
|
||||
|
||||
it("should provide the events sort by sequence", () => {
|
||||
expect(chunkEvents.getEvents()).toEqual([
|
||||
eventSeq1Time1,
|
||||
eventSeq2Time4Dup,
|
||||
eventSeq3Time2,
|
||||
eventSeq4Time1,
|
||||
]);
|
||||
});
|
||||
|
||||
it("getNumberOfEvents should return 4", () => {
|
||||
expect(chunkEvents.getNumberOfEvents()).toBe(4);
|
||||
});
|
||||
|
||||
it("getLength should return the total length of all chunks", () => {
|
||||
expect(chunkEvents.getLength()).toBe(3259);
|
||||
});
|
||||
|
||||
it("getLengthTo(first event) should return 0", () => {
|
||||
expect(chunkEvents.getLengthTo(eventSeq1Time1)).toBe(0);
|
||||
});
|
||||
|
||||
it("getLengthTo(some event) should return the time excl. that event", () => {
|
||||
expect(chunkEvents.getLengthTo(eventSeq3Time2)).toBe(7 + 3141);
|
||||
});
|
||||
|
||||
it("getLengthTo(last event) should return the time excl. that event", () => {
|
||||
expect(chunkEvents.getLengthTo(eventSeq4Time1)).toBe(7 + 3141 + 42);
|
||||
});
|
||||
|
||||
it("should return the expected next chunk", () => {
|
||||
expect(chunkEvents.getNext(eventSeq2Time4Dup)).toBe(eventSeq3Time2);
|
||||
});
|
||||
|
||||
it("should return undefined for next last chunk", () => {
|
||||
expect(chunkEvents.getNext(eventSeq4Time1)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("findByTime(0) should return the first chunk", () => {
|
||||
expect(chunkEvents.findByTime(0)).toBe(eventSeq1Time1);
|
||||
});
|
||||
|
||||
it("findByTime(some time) should return the chunk with this time", () => {
|
||||
expect(chunkEvents.findByTime(7 + 3141 + 21)).toBe(eventSeq3Time2);
|
||||
});
|
||||
|
||||
it("findByTime(entire duration) should return the last chunk", () => {
|
||||
expect(chunkEvents.findByTime(7 + 3141 + 42 + 69)).toBe(eventSeq4Time1);
|
||||
});
|
||||
|
||||
describe("and adding an event with a known transaction Id", () => {
|
||||
beforeEach(() => {
|
||||
chunkEvents.addEvent(eventSeq3Time2T);
|
||||
});
|
||||
|
||||
it("should replace the previous event", () => {
|
||||
expect(chunkEvents.getEvents()).toEqual([
|
||||
eventSeq1Time1,
|
||||
eventSeq2Time4Dup,
|
||||
eventSeq3Time2T,
|
||||
eventSeq4Time1,
|
||||
]);
|
||||
expect(chunkEvents.getNumberOfEvents()).toBe(4);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when adding events where at least one does not have a sequence", () => {
|
||||
beforeEach(() => {
|
||||
chunkEvents.addEvent(eventSeq2Time4);
|
||||
chunkEvents.addEvent(eventSeq1Time1);
|
||||
chunkEvents.addEvents([eventSeq4Time1, eventSeqUTime3, eventSeq2Time4Dup, eventSeq3Time2]);
|
||||
});
|
||||
|
||||
it("should provide the events sort by timestamp without duplicates", () => {
|
||||
expect(chunkEvents.getEvents()).toEqual([
|
||||
eventSeq1Time1,
|
||||
eventSeq4Time1,
|
||||
eventSeq3Time2,
|
||||
eventSeqUTime3,
|
||||
eventSeq2Time4Dup,
|
||||
]);
|
||||
expect(chunkEvents.getNumberOfEvents()).toBe(5);
|
||||
});
|
||||
|
||||
describe("getSequenceForEvent", () => {
|
||||
it("should return the sequence if provided by the event", () => {
|
||||
expect(chunkEvents.getSequenceForEvent(eventSeq3Time2)).toBe(3);
|
||||
});
|
||||
|
||||
it("should return the index if no sequence provided by event", () => {
|
||||
expect(chunkEvents.getSequenceForEvent(eventSeqUTime3)).toBe(4);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,179 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { mocked } from "jest-mock";
|
||||
import { ClientEvent, MatrixClient, MatrixEvent, RelationType, Room, SyncState } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import {
|
||||
VoiceBroadcastInfoEventContent,
|
||||
VoiceBroadcastInfoEventType,
|
||||
VoiceBroadcastInfoState,
|
||||
VoiceBroadcastResumer,
|
||||
} from "../../../src/voice-broadcast";
|
||||
import { stubClient } from "../../test-utils";
|
||||
import { mkVoiceBroadcastInfoStateEvent } from "./test-utils";
|
||||
|
||||
describe("VoiceBroadcastResumer", () => {
|
||||
const roomId = "!room:example.com";
|
||||
let client: MatrixClient;
|
||||
let room: Room;
|
||||
let resumer: VoiceBroadcastResumer;
|
||||
let startedInfoEvent: MatrixEvent;
|
||||
let pausedInfoEvent: MatrixEvent;
|
||||
|
||||
const itShouldNotSendAStateEvent = (): void => {
|
||||
it("should not send a state event", () => {
|
||||
expect(client.sendStateEvent).not.toHaveBeenCalled();
|
||||
});
|
||||
};
|
||||
|
||||
const itShouldSendAStoppedStateEvent = (): void => {
|
||||
it("should send a stopped state event", () => {
|
||||
expect(client.sendStateEvent).toHaveBeenCalledWith(
|
||||
startedInfoEvent.getRoomId(),
|
||||
VoiceBroadcastInfoEventType,
|
||||
{
|
||||
"device_id": client.getDeviceId(),
|
||||
"state": VoiceBroadcastInfoState.Stopped,
|
||||
"m.relates_to": {
|
||||
rel_type: RelationType.Reference,
|
||||
event_id: startedInfoEvent.getId(),
|
||||
},
|
||||
} as VoiceBroadcastInfoEventContent,
|
||||
client.getUserId()!,
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const itShouldDeregisterFromTheClient = () => {
|
||||
it("should deregister from the client", () => {
|
||||
expect(client.off).toHaveBeenCalledWith(ClientEvent.Sync, expect.any(Function));
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
client = stubClient();
|
||||
jest.spyOn(client, "off");
|
||||
room = new Room(roomId, client, client.getUserId()!);
|
||||
mocked(client.getRoom).mockImplementation((getRoomId: string | undefined) => {
|
||||
if (getRoomId === roomId) return room;
|
||||
|
||||
return null;
|
||||
});
|
||||
mocked(client.getRooms).mockReturnValue([room]);
|
||||
startedInfoEvent = mkVoiceBroadcastInfoStateEvent(
|
||||
roomId,
|
||||
VoiceBroadcastInfoState.Started,
|
||||
client.getUserId()!,
|
||||
client.getDeviceId()!,
|
||||
);
|
||||
pausedInfoEvent = mkVoiceBroadcastInfoStateEvent(
|
||||
roomId,
|
||||
VoiceBroadcastInfoState.Paused,
|
||||
client.getUserId()!,
|
||||
client.getDeviceId()!,
|
||||
startedInfoEvent,
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("when the initial sync is completed", () => {
|
||||
beforeEach(() => {
|
||||
mocked(client.isInitialSyncComplete).mockReturnValue(true);
|
||||
});
|
||||
|
||||
describe("and there is no info event", () => {
|
||||
beforeEach(() => {
|
||||
resumer = new VoiceBroadcastResumer(client);
|
||||
});
|
||||
|
||||
itShouldNotSendAStateEvent();
|
||||
|
||||
describe("and calling destroy", () => {
|
||||
beforeEach(() => {
|
||||
resumer.destroy();
|
||||
});
|
||||
|
||||
itShouldDeregisterFromTheClient();
|
||||
});
|
||||
});
|
||||
|
||||
describe("and there is a started info event", () => {
|
||||
beforeEach(() => {
|
||||
room.currentState.setStateEvents([startedInfoEvent]);
|
||||
});
|
||||
|
||||
describe("and the client knows about the user and device", () => {
|
||||
beforeEach(() => {
|
||||
resumer = new VoiceBroadcastResumer(client);
|
||||
});
|
||||
|
||||
itShouldSendAStoppedStateEvent();
|
||||
});
|
||||
|
||||
describe("and the client doesn't know about the user", () => {
|
||||
beforeEach(() => {
|
||||
mocked(client.getUserId).mockReturnValue(null);
|
||||
resumer = new VoiceBroadcastResumer(client);
|
||||
});
|
||||
|
||||
itShouldNotSendAStateEvent();
|
||||
});
|
||||
|
||||
describe("and the client doesn't know about the device", () => {
|
||||
beforeEach(() => {
|
||||
mocked(client.getDeviceId).mockReturnValue(null);
|
||||
resumer = new VoiceBroadcastResumer(client);
|
||||
});
|
||||
|
||||
itShouldNotSendAStateEvent();
|
||||
});
|
||||
});
|
||||
|
||||
describe("and there is a paused info event", () => {
|
||||
beforeEach(() => {
|
||||
room.currentState.setStateEvents([pausedInfoEvent]);
|
||||
resumer = new VoiceBroadcastResumer(client);
|
||||
});
|
||||
|
||||
itShouldSendAStoppedStateEvent();
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the initial sync is not completed", () => {
|
||||
beforeEach(() => {
|
||||
room.currentState.setStateEvents([pausedInfoEvent]);
|
||||
mocked(client.isInitialSyncComplete).mockReturnValue(false);
|
||||
mocked(client.getSyncState).mockReturnValue(SyncState.Prepared);
|
||||
resumer = new VoiceBroadcastResumer(client);
|
||||
});
|
||||
|
||||
itShouldNotSendAStateEvent();
|
||||
|
||||
describe("and a sync event appears", () => {
|
||||
beforeEach(() => {
|
||||
client.emit(ClientEvent.Sync, SyncState.Prepared, SyncState.Stopped);
|
||||
});
|
||||
|
||||
itShouldNotSendAStateEvent();
|
||||
|
||||
describe("and the initial sync completed and a sync event appears", () => {
|
||||
beforeEach(() => {
|
||||
mocked(client.getSyncState).mockReturnValue(SyncState.Syncing);
|
||||
client.emit(ClientEvent.Sync, SyncState.Syncing, SyncState.Prepared);
|
||||
});
|
||||
|
||||
itShouldSendAStoppedStateEvent();
|
||||
itShouldDeregisterFromTheClient();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,24 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`setUpVoiceBroadcastPreRecording when trying to start a broadcast if there is no connection should show an info dialog and not set up a pre-recording 1`] = `
|
||||
[MockFunction] {
|
||||
"calls": [
|
||||
[
|
||||
[Function],
|
||||
{
|
||||
"description": <p>
|
||||
Unfortunately we're unable to start a recording right now. Please try again later.
|
||||
</p>,
|
||||
"hasCloseButton": true,
|
||||
"title": "Connection error",
|
||||
},
|
||||
],
|
||||
],
|
||||
"results": [
|
||||
{
|
||||
"type": "return",
|
||||
"value": undefined,
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
|
@ -0,0 +1,116 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`startNewVoiceBroadcastRecording when the current user is allowed to send voice broadcast info state events when there already is a live broadcast of another user should show an info dialog 1`] = `
|
||||
[MockFunction] {
|
||||
"calls": [
|
||||
[
|
||||
[Function],
|
||||
{
|
||||
"description": <p>
|
||||
Someone else is already recording a voice broadcast. Wait for their voice broadcast to end to start a new one.
|
||||
</p>,
|
||||
"hasCloseButton": true,
|
||||
"title": "Can't start a new voice broadcast",
|
||||
},
|
||||
],
|
||||
],
|
||||
"results": [
|
||||
{
|
||||
"type": "return",
|
||||
"value": undefined,
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`startNewVoiceBroadcastRecording when the current user is allowed to send voice broadcast info state events when there already is a live broadcast of the current user in the room should show an info dialog 1`] = `
|
||||
[MockFunction] {
|
||||
"calls": [
|
||||
[
|
||||
[Function],
|
||||
{
|
||||
"description": <p>
|
||||
You are already recording a voice broadcast. Please end your current voice broadcast to start a new one.
|
||||
</p>,
|
||||
"hasCloseButton": true,
|
||||
"title": "Can't start a new voice broadcast",
|
||||
},
|
||||
],
|
||||
],
|
||||
"results": [
|
||||
{
|
||||
"type": "return",
|
||||
"value": undefined,
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`startNewVoiceBroadcastRecording when the current user is allowed to send voice broadcast info state events when there is already a current voice broadcast should show an info dialog 1`] = `
|
||||
[MockFunction] {
|
||||
"calls": [
|
||||
[
|
||||
[Function],
|
||||
{
|
||||
"description": <p>
|
||||
You are already recording a voice broadcast. Please end your current voice broadcast to start a new one.
|
||||
</p>,
|
||||
"hasCloseButton": true,
|
||||
"title": "Can't start a new voice broadcast",
|
||||
},
|
||||
],
|
||||
],
|
||||
"results": [
|
||||
{
|
||||
"type": "return",
|
||||
"value": undefined,
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`startNewVoiceBroadcastRecording when the current user is not allowed to send voice broadcast info state events should show an info dialog 1`] = `
|
||||
[MockFunction] {
|
||||
"calls": [
|
||||
[
|
||||
[Function],
|
||||
{
|
||||
"description": <p>
|
||||
You don't have the required permissions to start a voice broadcast in this room. Contact a room administrator to upgrade your permissions.
|
||||
</p>,
|
||||
"hasCloseButton": true,
|
||||
"title": "Can't start a new voice broadcast",
|
||||
},
|
||||
],
|
||||
],
|
||||
"results": [
|
||||
{
|
||||
"type": "return",
|
||||
"value": undefined,
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`startNewVoiceBroadcastRecording when trying to start a broadcast if there is no connection should show an info dialog and not start a recording 1`] = `
|
||||
[MockFunction] {
|
||||
"calls": [
|
||||
[
|
||||
[Function],
|
||||
{
|
||||
"description": <p>
|
||||
Unfortunately we're unable to start a recording right now. Please try again later.
|
||||
</p>,
|
||||
"hasCloseButton": true,
|
||||
"title": "Connection error",
|
||||
},
|
||||
],
|
||||
],
|
||||
"results": [
|
||||
{
|
||||
"type": "return",
|
||||
"value": undefined,
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
|
@ -0,0 +1,42 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`textForVoiceBroadcastStoppedEvent should render other users broadcast as expected 1`] = `
|
||||
<div>
|
||||
<div>
|
||||
@other:example.com ended a voice broadcast
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`textForVoiceBroadcastStoppedEvent should render own broadcast as expected 1`] = `
|
||||
<div>
|
||||
<div>
|
||||
You ended a voice broadcast
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`textForVoiceBroadcastStoppedEvent should render without login as expected 1`] = `
|
||||
<div>
|
||||
<div>
|
||||
@other:example.com ended a voice broadcast
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`textForVoiceBroadcastStoppedEvent when rendering an event with relation to the start event should render events with relation to the start event 1`] = `
|
||||
<div>
|
||||
<div>
|
||||
<span>
|
||||
You ended a
|
||||
<div
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
voice broadcast
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import {
|
||||
cleanUpBroadcasts,
|
||||
VoiceBroadcastPlayback,
|
||||
VoiceBroadcastPreRecording,
|
||||
VoiceBroadcastRecording,
|
||||
} from "../../../src/voice-broadcast";
|
||||
import { stubClient } from "../../test-utils";
|
||||
import { TestSdkContext } from "../../TestSdkContext";
|
||||
import { mkVoiceBroadcastPlayback, mkVoiceBroadcastPreRecording, mkVoiceBroadcastRecording } from "./test-utils";
|
||||
|
||||
describe("cleanUpBroadcasts", () => {
|
||||
let playback: VoiceBroadcastPlayback;
|
||||
let recording: VoiceBroadcastRecording;
|
||||
let preRecording: VoiceBroadcastPreRecording;
|
||||
let stores: TestSdkContext;
|
||||
|
||||
beforeEach(() => {
|
||||
stores = new TestSdkContext();
|
||||
stores.client = stubClient();
|
||||
|
||||
playback = mkVoiceBroadcastPlayback(stores);
|
||||
jest.spyOn(playback, "stop").mockReturnValue();
|
||||
stores.voiceBroadcastPlaybacksStore.setCurrent(playback);
|
||||
|
||||
recording = mkVoiceBroadcastRecording(stores);
|
||||
jest.spyOn(recording, "stop").mockResolvedValue();
|
||||
stores.voiceBroadcastRecordingsStore.setCurrent(recording);
|
||||
|
||||
preRecording = mkVoiceBroadcastPreRecording(stores);
|
||||
jest.spyOn(preRecording, "cancel").mockReturnValue();
|
||||
stores.voiceBroadcastPreRecordingStore.setCurrent(preRecording);
|
||||
});
|
||||
|
||||
it("should stop and clear all broadcast related stuff", async () => {
|
||||
await cleanUpBroadcasts(stores);
|
||||
expect(playback.stop).toHaveBeenCalled();
|
||||
expect(stores.voiceBroadcastPlaybacksStore.getCurrent()).toBeNull();
|
||||
expect(recording.stop).toHaveBeenCalled();
|
||||
expect(stores.voiceBroadcastRecordingsStore.getCurrent()).toBeNull();
|
||||
expect(preRecording.cancel).toHaveBeenCalled();
|
||||
expect(stores.voiceBroadcastPreRecordingStore.getCurrent()).toBeNull();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { VoiceBroadcastInfoState, VoiceBroadcastLiveness } from "../../../src/voice-broadcast";
|
||||
import { determineVoiceBroadcastLiveness } from "../../../src/voice-broadcast/utils/determineVoiceBroadcastLiveness";
|
||||
|
||||
const testData: Array<{ state: VoiceBroadcastInfoState; expected: VoiceBroadcastLiveness }> = [
|
||||
{ state: VoiceBroadcastInfoState.Started, expected: "live" },
|
||||
{ state: VoiceBroadcastInfoState.Resumed, expected: "live" },
|
||||
{ state: VoiceBroadcastInfoState.Paused, expected: "grey" },
|
||||
{ state: VoiceBroadcastInfoState.Stopped, expected: "not-live" },
|
||||
];
|
||||
|
||||
describe("determineVoiceBroadcastLiveness", () => {
|
||||
it.each(testData)("should return the expected value for a %s broadcast", ({ state, expected }) => {
|
||||
expect(determineVoiceBroadcastLiveness(state)).toBe(expected);
|
||||
});
|
||||
|
||||
it("should return »non-live« for an undefined state", () => {
|
||||
// @ts-ignore
|
||||
expect(determineVoiceBroadcastLiveness(undefined)).toBe("not-live");
|
||||
});
|
||||
|
||||
it("should return »non-live« for an unknown state", () => {
|
||||
// @ts-ignore
|
||||
expect(determineVoiceBroadcastLiveness("unknown test state")).toBe("not-live");
|
||||
});
|
||||
});
|
|
@ -0,0 +1,116 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { mocked } from "jest-mock";
|
||||
import { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import {
|
||||
findRoomLiveVoiceBroadcastFromUserAndDevice,
|
||||
VoiceBroadcastInfoEventType,
|
||||
VoiceBroadcastInfoState,
|
||||
} from "../../../src/voice-broadcast";
|
||||
import { mkEvent, stubClient } from "../../test-utils";
|
||||
import { mkVoiceBroadcastInfoStateEvent } from "./test-utils";
|
||||
|
||||
describe("findRoomLiveVoiceBroadcastFromUserAndDevice", () => {
|
||||
const roomId = "!room:example.com";
|
||||
let client: MatrixClient;
|
||||
let room: Room;
|
||||
|
||||
const itShouldReturnNull = () => {
|
||||
it("should return null", () => {
|
||||
expect(
|
||||
findRoomLiveVoiceBroadcastFromUserAndDevice(room, client.getUserId()!, client.getDeviceId()!),
|
||||
).toBeNull();
|
||||
});
|
||||
};
|
||||
|
||||
beforeAll(() => {
|
||||
client = stubClient();
|
||||
room = new Room(roomId, client, client.getUserId()!);
|
||||
jest.spyOn(room.currentState, "getStateEvents");
|
||||
mocked(client.getRoom).mockImplementation((getRoomId: string) => {
|
||||
if (getRoomId === roomId) return room;
|
||||
return null;
|
||||
});
|
||||
});
|
||||
|
||||
describe("when there is no info event", () => {
|
||||
itShouldReturnNull();
|
||||
});
|
||||
|
||||
describe("when there is an info event without content", () => {
|
||||
beforeEach(() => {
|
||||
room.currentState.setStateEvents([
|
||||
mkEvent({
|
||||
event: true,
|
||||
type: VoiceBroadcastInfoEventType,
|
||||
room: roomId,
|
||||
user: client.getUserId()!,
|
||||
content: {},
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
itShouldReturnNull();
|
||||
});
|
||||
|
||||
describe("when there is a stopped info event", () => {
|
||||
beforeEach(() => {
|
||||
room.currentState.setStateEvents([
|
||||
mkVoiceBroadcastInfoStateEvent(
|
||||
roomId,
|
||||
VoiceBroadcastInfoState.Stopped,
|
||||
client.getUserId()!,
|
||||
client.getDeviceId(),
|
||||
),
|
||||
]);
|
||||
});
|
||||
|
||||
itShouldReturnNull();
|
||||
});
|
||||
|
||||
describe("when there is a started info event from another device", () => {
|
||||
beforeEach(() => {
|
||||
const event = mkVoiceBroadcastInfoStateEvent(
|
||||
roomId,
|
||||
VoiceBroadcastInfoState.Stopped,
|
||||
client.getUserId()!,
|
||||
"JKL123",
|
||||
);
|
||||
room.currentState.setStateEvents([event]);
|
||||
});
|
||||
|
||||
itShouldReturnNull();
|
||||
});
|
||||
|
||||
describe("when there is a started info event", () => {
|
||||
let event: MatrixEvent;
|
||||
|
||||
beforeEach(() => {
|
||||
event = mkVoiceBroadcastInfoStateEvent(
|
||||
roomId,
|
||||
VoiceBroadcastInfoState.Started,
|
||||
client.getUserId()!,
|
||||
client.getDeviceId(),
|
||||
);
|
||||
room.currentState.setStateEvents([event]);
|
||||
});
|
||||
|
||||
it("should return this event", () => {
|
||||
expect(room.currentState.getStateEvents).toHaveBeenCalledWith(
|
||||
VoiceBroadcastInfoEventType,
|
||||
client.getUserId()!,
|
||||
);
|
||||
|
||||
expect(findRoomLiveVoiceBroadcastFromUserAndDevice(room, client.getUserId()!, client.getDeviceId()!)).toBe(
|
||||
event,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
63
test/unit-tests/voice-broadcast/utils/getChunkLength-test.ts
Normal file
63
test/unit-tests/voice-broadcast/utils/getChunkLength-test.ts
Normal file
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import SdkConfig from "../../../src/SdkConfig";
|
||||
import { SettingLevel } from "../../../src/settings/SettingLevel";
|
||||
import { Features } from "../../../src/settings/Settings";
|
||||
import SettingsStore from "../../../src/settings/SettingsStore";
|
||||
import { getChunkLength } from "../../../src/voice-broadcast/utils/getChunkLength";
|
||||
|
||||
describe("getChunkLength", () => {
|
||||
afterEach(() => {
|
||||
SdkConfig.reset();
|
||||
});
|
||||
|
||||
describe("when there is a value provided by Sdk config", () => {
|
||||
beforeEach(() => {
|
||||
SdkConfig.add({
|
||||
voice_broadcast: {
|
||||
chunk_length: 42,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should return this value", () => {
|
||||
expect(getChunkLength()).toBe(42);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when Sdk config does not provide a value", () => {
|
||||
beforeEach(() => {
|
||||
SdkConfig.add({
|
||||
voice_broadcast: {
|
||||
chunk_length: 23,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should return this value", () => {
|
||||
expect(getChunkLength()).toBe(23);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when there are no defaults", () => {
|
||||
it("should return the fallback value", () => {
|
||||
expect(getChunkLength()).toBe(120);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the Features.VoiceBroadcastForceSmallChunks is enabled", () => {
|
||||
beforeEach(async () => {
|
||||
await SettingsStore.setValue(Features.VoiceBroadcastForceSmallChunks, null, SettingLevel.DEVICE, true);
|
||||
});
|
||||
|
||||
it("should return a chunk length of 15 seconds", () => {
|
||||
expect(getChunkLength()).toBe(15);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import SdkConfig, { DEFAULTS } from "../../../src/SdkConfig";
|
||||
import { getMaxBroadcastLength } from "../../../src/voice-broadcast";
|
||||
|
||||
describe("getMaxBroadcastLength", () => {
|
||||
afterEach(() => {
|
||||
SdkConfig.reset();
|
||||
});
|
||||
|
||||
describe("when there is a value provided by Sdk config", () => {
|
||||
beforeEach(() => {
|
||||
SdkConfig.put({
|
||||
voice_broadcast: {
|
||||
max_length: 42,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should return this value", () => {
|
||||
expect(getMaxBroadcastLength()).toBe(42);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when Sdk config does not provide a value", () => {
|
||||
it("should return this value", () => {
|
||||
expect(getMaxBroadcastLength()).toBe(DEFAULTS.voice_broadcast!.max_length);
|
||||
});
|
||||
});
|
||||
|
||||
describe("if there are no defaults", () => {
|
||||
it("should return the fallback value", () => {
|
||||
expect(DEFAULTS.voice_broadcast!.max_length).toBe(4 * 60 * 60);
|
||||
expect(getMaxBroadcastLength()).toBe(4 * 60 * 60);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,195 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { mocked } from "jest-mock";
|
||||
import { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import {
|
||||
hasRoomLiveVoiceBroadcast,
|
||||
VoiceBroadcastInfoEventType,
|
||||
VoiceBroadcastInfoState,
|
||||
} from "../../../src/voice-broadcast";
|
||||
import { mkEvent, stubClient } from "../../test-utils";
|
||||
import { mkVoiceBroadcastInfoStateEvent } from "./test-utils";
|
||||
|
||||
describe("hasRoomLiveVoiceBroadcast", () => {
|
||||
const otherUserId = "@other:example.com";
|
||||
const otherDeviceId = "ASD123";
|
||||
const roomId = "!room:example.com";
|
||||
let client: MatrixClient;
|
||||
let room: Room;
|
||||
let expectedEvent: MatrixEvent | null = null;
|
||||
|
||||
const addVoiceBroadcastInfoEvent = (
|
||||
state: VoiceBroadcastInfoState,
|
||||
userId: string,
|
||||
deviceId: string,
|
||||
startedEvent?: MatrixEvent,
|
||||
): MatrixEvent => {
|
||||
const infoEvent = mkVoiceBroadcastInfoStateEvent(room.roomId, state, userId, deviceId, startedEvent);
|
||||
room.addLiveEvents([infoEvent]);
|
||||
room.currentState.setStateEvents([infoEvent]);
|
||||
room.relations.aggregateChildEvent(infoEvent);
|
||||
return infoEvent;
|
||||
};
|
||||
|
||||
const itShouldReturnTrueTrue = () => {
|
||||
it("should return true/true", async () => {
|
||||
expect(await hasRoomLiveVoiceBroadcast(client, room, client.getSafeUserId())).toEqual({
|
||||
hasBroadcast: true,
|
||||
infoEvent: expectedEvent,
|
||||
startedByUser: true,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const itShouldReturnTrueFalse = () => {
|
||||
it("should return true/false", async () => {
|
||||
expect(await hasRoomLiveVoiceBroadcast(client, room, client.getSafeUserId())).toEqual({
|
||||
hasBroadcast: true,
|
||||
infoEvent: expectedEvent,
|
||||
startedByUser: false,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const itShouldReturnFalseFalse = () => {
|
||||
it("should return false/false", async () => {
|
||||
expect(await hasRoomLiveVoiceBroadcast(client, room, client.getSafeUserId())).toEqual({
|
||||
hasBroadcast: false,
|
||||
infoEvent: null,
|
||||
startedByUser: false,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
client = stubClient();
|
||||
room = new Room(roomId, client, client.getSafeUserId());
|
||||
mocked(client.getRoom).mockImplementation((roomId: string): Room | null => {
|
||||
return roomId === room.roomId ? room : null;
|
||||
});
|
||||
expectedEvent = null;
|
||||
});
|
||||
|
||||
describe("when there is no voice broadcast info at all", () => {
|
||||
itShouldReturnFalseFalse();
|
||||
});
|
||||
|
||||
describe("when the »state« prop is missing", () => {
|
||||
beforeEach(() => {
|
||||
room.currentState.setStateEvents([
|
||||
mkEvent({
|
||||
event: true,
|
||||
room: room.roomId,
|
||||
user: client.getSafeUserId(),
|
||||
type: VoiceBroadcastInfoEventType,
|
||||
skey: client.getSafeUserId(),
|
||||
content: {},
|
||||
}),
|
||||
]);
|
||||
});
|
||||
itShouldReturnFalseFalse();
|
||||
});
|
||||
|
||||
describe("when there is a live broadcast from the current and another user", () => {
|
||||
beforeEach(() => {
|
||||
expectedEvent = addVoiceBroadcastInfoEvent(
|
||||
VoiceBroadcastInfoState.Started,
|
||||
client.getSafeUserId(),
|
||||
client.getDeviceId()!,
|
||||
);
|
||||
addVoiceBroadcastInfoEvent(VoiceBroadcastInfoState.Started, otherUserId, otherDeviceId);
|
||||
});
|
||||
|
||||
itShouldReturnTrueTrue();
|
||||
});
|
||||
|
||||
describe("when there are only stopped info events", () => {
|
||||
beforeEach(() => {
|
||||
addVoiceBroadcastInfoEvent(VoiceBroadcastInfoState.Stopped, client.getSafeUserId(), client.getDeviceId()!);
|
||||
addVoiceBroadcastInfoEvent(VoiceBroadcastInfoState.Stopped, otherUserId, otherDeviceId);
|
||||
});
|
||||
|
||||
itShouldReturnFalseFalse();
|
||||
});
|
||||
|
||||
describe("when there is a live, started broadcast from the current user", () => {
|
||||
beforeEach(() => {
|
||||
expectedEvent = addVoiceBroadcastInfoEvent(
|
||||
VoiceBroadcastInfoState.Started,
|
||||
client.getSafeUserId(),
|
||||
client.getDeviceId()!,
|
||||
);
|
||||
});
|
||||
|
||||
itShouldReturnTrueTrue();
|
||||
});
|
||||
|
||||
describe("when there is a live, paused broadcast from the current user", () => {
|
||||
beforeEach(() => {
|
||||
expectedEvent = addVoiceBroadcastInfoEvent(
|
||||
VoiceBroadcastInfoState.Started,
|
||||
client.getSafeUserId(),
|
||||
client.getDeviceId()!,
|
||||
);
|
||||
addVoiceBroadcastInfoEvent(
|
||||
VoiceBroadcastInfoState.Paused,
|
||||
client.getSafeUserId(),
|
||||
client.getDeviceId()!,
|
||||
expectedEvent,
|
||||
);
|
||||
});
|
||||
|
||||
itShouldReturnTrueTrue();
|
||||
});
|
||||
|
||||
describe("when there is a live, resumed broadcast from the current user", () => {
|
||||
beforeEach(() => {
|
||||
expectedEvent = addVoiceBroadcastInfoEvent(
|
||||
VoiceBroadcastInfoState.Started,
|
||||
client.getSafeUserId(),
|
||||
client.getDeviceId()!,
|
||||
);
|
||||
addVoiceBroadcastInfoEvent(
|
||||
VoiceBroadcastInfoState.Resumed,
|
||||
client.getSafeUserId(),
|
||||
client.getDeviceId()!,
|
||||
expectedEvent,
|
||||
);
|
||||
});
|
||||
|
||||
itShouldReturnTrueTrue();
|
||||
});
|
||||
|
||||
describe("when there was a live broadcast, that has been stopped", () => {
|
||||
beforeEach(() => {
|
||||
const startedEvent = addVoiceBroadcastInfoEvent(
|
||||
VoiceBroadcastInfoState.Started,
|
||||
client.getSafeUserId(),
|
||||
client.getDeviceId()!,
|
||||
);
|
||||
addVoiceBroadcastInfoEvent(
|
||||
VoiceBroadcastInfoState.Stopped,
|
||||
client.getSafeUserId(),
|
||||
client.getDeviceId()!,
|
||||
startedEvent,
|
||||
);
|
||||
});
|
||||
|
||||
itShouldReturnFalseFalse();
|
||||
});
|
||||
|
||||
describe("when there is a live broadcast from another user", () => {
|
||||
beforeEach(() => {
|
||||
expectedEvent = addVoiceBroadcastInfoEvent(VoiceBroadcastInfoState.Started, otherUserId, otherDeviceId);
|
||||
});
|
||||
|
||||
itShouldReturnTrueFalse();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,109 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { EventType, MatrixEvent, RelationType, Room, MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import { mocked } from "jest-mock";
|
||||
|
||||
import { isRelatedToVoiceBroadcast, VoiceBroadcastInfoState } from "../../../src/voice-broadcast";
|
||||
import { mkEvent, stubClient } from "../../test-utils";
|
||||
import { mkVoiceBroadcastInfoStateEvent } from "./test-utils";
|
||||
|
||||
const mkRelatedEvent = (
|
||||
room: Room,
|
||||
relationType: RelationType,
|
||||
relatesTo: MatrixEvent | undefined,
|
||||
client: MatrixClient,
|
||||
): MatrixEvent => {
|
||||
const event = mkEvent({
|
||||
event: true,
|
||||
type: EventType.RoomMessage,
|
||||
room: room.roomId,
|
||||
content: {
|
||||
"m.relates_to": {
|
||||
rel_type: relationType,
|
||||
event_id: relatesTo?.getId(),
|
||||
},
|
||||
},
|
||||
user: client.getSafeUserId(),
|
||||
});
|
||||
room.addLiveEvents([event]);
|
||||
return event;
|
||||
};
|
||||
|
||||
describe("isRelatedToVoiceBroadcast", () => {
|
||||
const roomId = "!room:example.com";
|
||||
let client: MatrixClient;
|
||||
let room: Room;
|
||||
let broadcastEvent: MatrixEvent;
|
||||
let nonBroadcastEvent: MatrixEvent;
|
||||
|
||||
beforeAll(() => {
|
||||
client = stubClient();
|
||||
room = new Room(roomId, client, client.getSafeUserId());
|
||||
|
||||
mocked(client.getRoom).mockImplementation((getRoomId: string): Room | null => {
|
||||
if (getRoomId === roomId) return room;
|
||||
return null;
|
||||
});
|
||||
|
||||
broadcastEvent = mkVoiceBroadcastInfoStateEvent(
|
||||
roomId,
|
||||
VoiceBroadcastInfoState.Started,
|
||||
client.getSafeUserId(),
|
||||
"ABC123",
|
||||
);
|
||||
nonBroadcastEvent = mkEvent({
|
||||
event: true,
|
||||
type: EventType.RoomMessage,
|
||||
room: roomId,
|
||||
content: {},
|
||||
user: client.getSafeUserId(),
|
||||
});
|
||||
|
||||
room.addLiveEvents([broadcastEvent, nonBroadcastEvent]);
|
||||
});
|
||||
|
||||
it("should return true if related (reference) to a broadcast event", () => {
|
||||
expect(
|
||||
isRelatedToVoiceBroadcast(mkRelatedEvent(room, RelationType.Reference, broadcastEvent, client), client),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false if related (reference) is undefeind", () => {
|
||||
expect(isRelatedToVoiceBroadcast(mkRelatedEvent(room, RelationType.Reference, undefined, client), client)).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it("should return false if related (referenireplace) to a broadcast event", () => {
|
||||
expect(
|
||||
isRelatedToVoiceBroadcast(mkRelatedEvent(room, RelationType.Replace, broadcastEvent, client), client),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false if the event has no relation", () => {
|
||||
const noRelationEvent = mkEvent({
|
||||
event: true,
|
||||
type: EventType.RoomMessage,
|
||||
room: room.roomId,
|
||||
content: {},
|
||||
user: client.getSafeUserId(),
|
||||
});
|
||||
expect(isRelatedToVoiceBroadcast(noRelationEvent, client)).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false for an unknown room", () => {
|
||||
const otherRoom = new Room("!other:example.com", client, client.getSafeUserId());
|
||||
expect(
|
||||
isRelatedToVoiceBroadcast(
|
||||
mkRelatedEvent(otherRoom, RelationType.Reference, broadcastEvent, client),
|
||||
client,
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { MatrixClient, Room } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import {
|
||||
VoiceBroadcastInfoState,
|
||||
VoiceBroadcastPlayback,
|
||||
VoiceBroadcastPlaybacksStore,
|
||||
VoiceBroadcastRecordingsStore,
|
||||
} from "../../../src/voice-broadcast";
|
||||
import { pauseNonLiveBroadcastFromOtherRoom } from "../../../src/voice-broadcast/utils/pauseNonLiveBroadcastFromOtherRoom";
|
||||
import { stubClient } from "../../test-utils";
|
||||
import { mkVoiceBroadcastInfoStateEvent } from "./test-utils";
|
||||
|
||||
describe("pauseNonLiveBroadcastFromOtherRoom", () => {
|
||||
const roomId = "!room:example.com";
|
||||
const roomId2 = "!room2@example.com";
|
||||
let room: Room;
|
||||
let client: MatrixClient;
|
||||
let playback: VoiceBroadcastPlayback;
|
||||
let playbacks: VoiceBroadcastPlaybacksStore;
|
||||
let recordings: VoiceBroadcastRecordingsStore;
|
||||
|
||||
const mkPlayback = (infoState: VoiceBroadcastInfoState, roomId: string): VoiceBroadcastPlayback => {
|
||||
const infoEvent = mkVoiceBroadcastInfoStateEvent(
|
||||
roomId,
|
||||
infoState,
|
||||
client.getSafeUserId(),
|
||||
client.getDeviceId()!,
|
||||
);
|
||||
const playback = new VoiceBroadcastPlayback(infoEvent, client, recordings);
|
||||
jest.spyOn(playback, "pause");
|
||||
playbacks.setCurrent(playback);
|
||||
return playback;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
client = stubClient();
|
||||
room = new Room(roomId, client, client.getSafeUserId());
|
||||
recordings = new VoiceBroadcastRecordingsStore();
|
||||
playbacks = new VoiceBroadcastPlaybacksStore(recordings);
|
||||
jest.spyOn(playbacks, "clearCurrent");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
playback?.destroy();
|
||||
playbacks.destroy();
|
||||
});
|
||||
|
||||
describe("when there is no current playback", () => {
|
||||
it("should not clear the current playback", () => {
|
||||
pauseNonLiveBroadcastFromOtherRoom(room, playbacks);
|
||||
expect(playbacks.clearCurrent).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("when listening to a live broadcast in another room", () => {
|
||||
beforeEach(() => {
|
||||
playback = mkPlayback(VoiceBroadcastInfoState.Started, roomId2);
|
||||
});
|
||||
|
||||
it("should not clear current / pause the playback", () => {
|
||||
pauseNonLiveBroadcastFromOtherRoom(room, playbacks);
|
||||
expect(playbacks.clearCurrent).not.toHaveBeenCalled();
|
||||
expect(playback.pause).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("when listening to a non-live broadcast in the same room", () => {
|
||||
beforeEach(() => {
|
||||
playback = mkPlayback(VoiceBroadcastInfoState.Stopped, roomId);
|
||||
});
|
||||
|
||||
it("should not clear current / pause the playback", () => {
|
||||
pauseNonLiveBroadcastFromOtherRoom(room, playbacks);
|
||||
expect(playbacks.clearCurrent).not.toHaveBeenCalled();
|
||||
expect(playback.pause).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("when listening to a non-live broadcast in another room", () => {
|
||||
beforeEach(() => {
|
||||
playback = mkPlayback(VoiceBroadcastInfoState.Stopped, roomId2);
|
||||
});
|
||||
|
||||
it("should clear current and pause the playback", () => {
|
||||
pauseNonLiveBroadcastFromOtherRoom(room, playbacks);
|
||||
expect(playbacks.getCurrent()).toBeNull();
|
||||
expect(playback.pause).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { mocked } from "jest-mock";
|
||||
import { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import {
|
||||
retrieveStartedInfoEvent,
|
||||
VoiceBroadcastInfoEventType,
|
||||
VoiceBroadcastInfoState,
|
||||
} from "../../../src/voice-broadcast";
|
||||
import { mkEvent, stubClient } from "../../test-utils";
|
||||
import { mkVoiceBroadcastInfoStateEvent } from "./test-utils";
|
||||
|
||||
describe("retrieveStartedInfoEvent", () => {
|
||||
let client: MatrixClient;
|
||||
let room: Room;
|
||||
|
||||
const mkStartEvent = () => {
|
||||
return mkVoiceBroadcastInfoStateEvent(
|
||||
room.roomId,
|
||||
VoiceBroadcastInfoState.Started,
|
||||
client.getUserId()!,
|
||||
client.deviceId!,
|
||||
);
|
||||
};
|
||||
|
||||
const mkStopEvent = (startEvent: MatrixEvent) => {
|
||||
return mkVoiceBroadcastInfoStateEvent(
|
||||
room.roomId,
|
||||
VoiceBroadcastInfoState.Stopped,
|
||||
client.getUserId()!,
|
||||
client.deviceId!,
|
||||
startEvent,
|
||||
);
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
client = stubClient();
|
||||
room = new Room("!room:example.com", client, client.getUserId()!);
|
||||
mocked(client.getRoom).mockImplementation((roomId: string): Room | null => {
|
||||
if (roomId === room.roomId) return room;
|
||||
return null;
|
||||
});
|
||||
});
|
||||
|
||||
it("when passing a started event, it should return the event", async () => {
|
||||
const event = mkStartEvent();
|
||||
expect(await retrieveStartedInfoEvent(event, client)).toBe(event);
|
||||
});
|
||||
|
||||
it("when passing an event without relation, it should return null", async () => {
|
||||
const event = mkEvent({
|
||||
event: true,
|
||||
type: VoiceBroadcastInfoEventType,
|
||||
user: client.getUserId()!,
|
||||
content: {},
|
||||
});
|
||||
expect(await retrieveStartedInfoEvent(event, client)).toBeNull();
|
||||
});
|
||||
|
||||
it("when the room contains the event, it should return it", async () => {
|
||||
const startEvent = mkStartEvent();
|
||||
const stopEvent = mkStopEvent(startEvent);
|
||||
room.addLiveEvents([startEvent]);
|
||||
expect(await retrieveStartedInfoEvent(stopEvent, client)).toBe(startEvent);
|
||||
});
|
||||
|
||||
it("when the room not contains the event, it should fetch it", async () => {
|
||||
const startEvent = mkStartEvent();
|
||||
const stopEvent = mkStopEvent(startEvent);
|
||||
mocked(client.fetchRoomEvent).mockResolvedValue(startEvent.event);
|
||||
expect((await retrieveStartedInfoEvent(stopEvent, client))?.getId()).toBe(startEvent.getId());
|
||||
expect(client.fetchRoomEvent).toHaveBeenCalledWith(room.roomId, startEvent.getId());
|
||||
});
|
||||
});
|
|
@ -0,0 +1,123 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { mocked } from "jest-mock";
|
||||
import { MatrixClient, MatrixEvent, Room, SyncState } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import Modal from "../../../src/Modal";
|
||||
import {
|
||||
VoiceBroadcastInfoState,
|
||||
VoiceBroadcastPlayback,
|
||||
VoiceBroadcastPlaybacksStore,
|
||||
VoiceBroadcastPreRecording,
|
||||
VoiceBroadcastPreRecordingStore,
|
||||
VoiceBroadcastRecordingsStore,
|
||||
} from "../../../src/voice-broadcast";
|
||||
import { setUpVoiceBroadcastPreRecording } from "../../../src/voice-broadcast/utils/setUpVoiceBroadcastPreRecording";
|
||||
import { mkRoomMemberJoinEvent, stubClient } from "../../test-utils";
|
||||
import { mkVoiceBroadcastInfoStateEvent } from "./test-utils";
|
||||
|
||||
jest.mock("../../../src/Modal");
|
||||
|
||||
describe("setUpVoiceBroadcastPreRecording", () => {
|
||||
const roomId = "!room:example.com";
|
||||
let client: MatrixClient;
|
||||
let userId: string;
|
||||
let room: Room;
|
||||
let preRecordingStore: VoiceBroadcastPreRecordingStore;
|
||||
let infoEvent: MatrixEvent;
|
||||
let playback: VoiceBroadcastPlayback;
|
||||
let playbacksStore: VoiceBroadcastPlaybacksStore;
|
||||
let recordingsStore: VoiceBroadcastRecordingsStore;
|
||||
let preRecording: VoiceBroadcastPreRecording | null;
|
||||
|
||||
const itShouldNotCreateAPreRecording = () => {
|
||||
it("should return null", () => {
|
||||
expect(preRecording).toBeNull();
|
||||
});
|
||||
|
||||
it("should not create a broadcast pre recording", () => {
|
||||
expect(preRecordingStore.getCurrent()).toBeNull();
|
||||
});
|
||||
};
|
||||
|
||||
const setUpPreRecording = async () => {
|
||||
preRecording = await setUpVoiceBroadcastPreRecording(
|
||||
room,
|
||||
client,
|
||||
playbacksStore,
|
||||
recordingsStore,
|
||||
preRecordingStore,
|
||||
);
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
client = stubClient();
|
||||
userId = client.getSafeUserId();
|
||||
room = new Room(roomId, client, userId);
|
||||
infoEvent = mkVoiceBroadcastInfoStateEvent(
|
||||
roomId,
|
||||
VoiceBroadcastInfoState.Started,
|
||||
client.getUserId()!,
|
||||
client.getDeviceId()!,
|
||||
);
|
||||
preRecording = null;
|
||||
preRecordingStore = new VoiceBroadcastPreRecordingStore();
|
||||
recordingsStore = new VoiceBroadcastRecordingsStore();
|
||||
playback = new VoiceBroadcastPlayback(infoEvent, client, recordingsStore);
|
||||
jest.spyOn(playback, "pause");
|
||||
playbacksStore = new VoiceBroadcastPlaybacksStore(recordingsStore);
|
||||
});
|
||||
|
||||
describe("when trying to start a broadcast if there is no connection", () => {
|
||||
beforeEach(async () => {
|
||||
mocked(client.getSyncState).mockReturnValue(SyncState.Error);
|
||||
await setUpPreRecording();
|
||||
});
|
||||
|
||||
it("should show an info dialog and not set up a pre-recording", () => {
|
||||
expect(preRecordingStore.getCurrent()).toBeNull();
|
||||
expect(Modal.createDialog).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe("when setting up a pre-recording", () => {
|
||||
describe("and there is no user id", () => {
|
||||
beforeEach(async () => {
|
||||
mocked(client.getUserId).mockReturnValue(null);
|
||||
await setUpPreRecording();
|
||||
});
|
||||
|
||||
itShouldNotCreateAPreRecording();
|
||||
});
|
||||
|
||||
describe("and there is no room member", () => {
|
||||
beforeEach(async () => {
|
||||
// check test precondition
|
||||
expect(room.getMember(userId)).toBeNull();
|
||||
await setUpPreRecording();
|
||||
});
|
||||
|
||||
itShouldNotCreateAPreRecording();
|
||||
});
|
||||
|
||||
describe("and there is a room member and listening to another broadcast", () => {
|
||||
beforeEach(async () => {
|
||||
playbacksStore.setCurrent(playback);
|
||||
room.currentState.setStateEvents([mkRoomMemberJoinEvent(userId, roomId)]);
|
||||
await setUpPreRecording();
|
||||
});
|
||||
|
||||
it("should pause the current playback and create a voice broadcast pre-recording", () => {
|
||||
expect(playback.pause).toHaveBeenCalled();
|
||||
expect(playbacksStore.getCurrent()).toBeNull();
|
||||
expect(preRecording).toBeInstanceOf(VoiceBroadcastPreRecording);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { mocked } from "jest-mock";
|
||||
import { MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { shouldDisplayAsVoiceBroadcastRecordingTile, VoiceBroadcastInfoState } from "../../../src/voice-broadcast";
|
||||
import { createTestClient } from "../../test-utils";
|
||||
import { mkVoiceBroadcastInfoStateEvent } from "./test-utils";
|
||||
|
||||
type TestTuple = [string | null, string, string, string, VoiceBroadcastInfoState, boolean];
|
||||
|
||||
const testCases: TestTuple[] = [
|
||||
[
|
||||
"@user1:example.com", // own MXID
|
||||
"@user1:example.com", // sender MXID
|
||||
"ABC123", // own device ID
|
||||
"ABC123", // sender device ID
|
||||
VoiceBroadcastInfoState.Started,
|
||||
true, // expected return value
|
||||
],
|
||||
["@user1:example.com", "@user1:example.com", "ABC123", "ABC123", VoiceBroadcastInfoState.Paused, true],
|
||||
["@user1:example.com", "@user1:example.com", "ABC123", "ABC123", VoiceBroadcastInfoState.Resumed, true],
|
||||
["@user1:example.com", "@user1:example.com", "ABC123", "ABC123", VoiceBroadcastInfoState.Stopped, false],
|
||||
["@user2:example.com", "@user1:example.com", "ABC123", "ABC123", VoiceBroadcastInfoState.Started, false],
|
||||
[null, "@user1:example.com", "ABC123", "ABC123", VoiceBroadcastInfoState.Started, false],
|
||||
// other device
|
||||
["@user1:example.com", "@user1:example.com", "ABC123", "JKL123", VoiceBroadcastInfoState.Started, false],
|
||||
["@user1:example.com", "@user1:example.com", "ABC123", "JKL123", VoiceBroadcastInfoState.Paused, false],
|
||||
["@user1:example.com", "@user1:example.com", "ABC123", "JKL123", VoiceBroadcastInfoState.Resumed, false],
|
||||
];
|
||||
|
||||
describe("shouldDisplayAsVoiceBroadcastRecordingTile", () => {
|
||||
let event: MatrixEvent;
|
||||
let client: MatrixClient;
|
||||
|
||||
beforeAll(() => {
|
||||
client = createTestClient();
|
||||
});
|
||||
|
||||
describe.each<TestTuple>(testCases)(
|
||||
"when called with user »%s«, sender »%s«, device »%s«, sender device »%s« state »%s«",
|
||||
(userId, senderId, deviceId, senderDeviceId, state, expected) => {
|
||||
beforeEach(() => {
|
||||
event = mkVoiceBroadcastInfoStateEvent("!room:example.com", state, senderId, senderDeviceId);
|
||||
mocked(client.getUserId).mockReturnValue(userId);
|
||||
mocked(client.getDeviceId).mockReturnValue(deviceId);
|
||||
});
|
||||
|
||||
it(`should return ${expected}`, () => {
|
||||
expect(shouldDisplayAsVoiceBroadcastRecordingTile(state, client, event)).toBe(expected);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("should return false, when all params are null", () => {
|
||||
event = mkVoiceBroadcastInfoStateEvent("!room:example.com", null, null, null);
|
||||
// @ts-ignore Simulate null state received for any reason.
|
||||
expect(shouldDisplayAsVoiceBroadcastRecordingTile(null, client, event)).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false, when all params are undefined", () => {
|
||||
event = mkVoiceBroadcastInfoStateEvent("!room:example.com", undefined, undefined, undefined);
|
||||
// @ts-ignore Simulate undefined state received for any reason.
|
||||
expect(shouldDisplayAsVoiceBroadcastRecordingTile(undefined, client, event)).toBe(false);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,138 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { EventType, IEvent, MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import {
|
||||
shouldDisplayAsVoiceBroadcastTile,
|
||||
VoiceBroadcastInfoEventType,
|
||||
VoiceBroadcastInfoState,
|
||||
} from "../../../src/voice-broadcast";
|
||||
import { mkEvent } from "../../test-utils";
|
||||
|
||||
describe("shouldDisplayAsVoiceBroadcastTile", () => {
|
||||
let event: MatrixEvent;
|
||||
const roomId = "!room:example.com";
|
||||
const senderId = "@user:example.com";
|
||||
|
||||
const itShouldReturnFalse = () => {
|
||||
it("should return false", () => {
|
||||
expect(shouldDisplayAsVoiceBroadcastTile(event)).toBe(false);
|
||||
});
|
||||
};
|
||||
|
||||
const itShouldReturnTrue = () => {
|
||||
it("should return true", () => {
|
||||
expect(shouldDisplayAsVoiceBroadcastTile(event)).toBe(true);
|
||||
});
|
||||
};
|
||||
|
||||
describe("when a broken event occurs", () => {
|
||||
beforeEach(() => {
|
||||
event = 23 as unknown as MatrixEvent;
|
||||
});
|
||||
|
||||
itShouldReturnFalse();
|
||||
});
|
||||
|
||||
describe("when a non-voice broadcast info event occurs", () => {
|
||||
beforeEach(() => {
|
||||
event = mkEvent({
|
||||
event: true,
|
||||
type: EventType.RoomMessage,
|
||||
room: roomId,
|
||||
user: senderId,
|
||||
content: {},
|
||||
});
|
||||
});
|
||||
|
||||
itShouldReturnFalse();
|
||||
});
|
||||
|
||||
describe("when a voice broadcast info event with empty content occurs", () => {
|
||||
beforeEach(() => {
|
||||
event = mkEvent({
|
||||
event: true,
|
||||
type: VoiceBroadcastInfoEventType,
|
||||
room: roomId,
|
||||
user: senderId,
|
||||
content: {},
|
||||
});
|
||||
});
|
||||
|
||||
itShouldReturnFalse();
|
||||
});
|
||||
|
||||
describe("when a voice broadcast info event with undefined content occurs", () => {
|
||||
beforeEach(() => {
|
||||
event = mkEvent({
|
||||
event: true,
|
||||
type: VoiceBroadcastInfoEventType,
|
||||
room: roomId,
|
||||
user: senderId,
|
||||
content: {},
|
||||
});
|
||||
event.getContent = () => ({}) as any;
|
||||
});
|
||||
|
||||
itShouldReturnFalse();
|
||||
});
|
||||
|
||||
describe("when a voice broadcast info event in state started occurs", () => {
|
||||
beforeEach(() => {
|
||||
event = mkEvent({
|
||||
event: true,
|
||||
type: VoiceBroadcastInfoEventType,
|
||||
room: roomId,
|
||||
user: senderId,
|
||||
content: {
|
||||
state: VoiceBroadcastInfoState.Started,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
itShouldReturnTrue();
|
||||
});
|
||||
|
||||
describe("when a redacted event occurs", () => {
|
||||
beforeEach(() => {
|
||||
event = mkEvent({
|
||||
event: true,
|
||||
type: VoiceBroadcastInfoEventType,
|
||||
room: roomId,
|
||||
user: senderId,
|
||||
content: {},
|
||||
unsigned: {
|
||||
redacted_because: {} as unknown as IEvent,
|
||||
},
|
||||
});
|
||||
event.getContent = () => ({}) as any;
|
||||
});
|
||||
|
||||
itShouldReturnTrue();
|
||||
});
|
||||
|
||||
describe.each([VoiceBroadcastInfoState.Paused, VoiceBroadcastInfoState.Resumed, VoiceBroadcastInfoState.Stopped])(
|
||||
"when a voice broadcast info event in state %s occurs",
|
||||
(state: VoiceBroadcastInfoState) => {
|
||||
beforeEach(() => {
|
||||
event = mkEvent({
|
||||
event: true,
|
||||
type: VoiceBroadcastInfoEventType,
|
||||
room: roomId,
|
||||
user: senderId,
|
||||
content: {
|
||||
state,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
itShouldReturnFalse();
|
||||
},
|
||||
);
|
||||
});
|
|
@ -0,0 +1,234 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { mocked } from "jest-mock";
|
||||
import { EventType, ISendEventResponse, MatrixClient, MatrixEvent, Room, SyncState } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import Modal from "../../../src/Modal";
|
||||
import {
|
||||
startNewVoiceBroadcastRecording,
|
||||
VoiceBroadcastInfoEventType,
|
||||
VoiceBroadcastInfoState,
|
||||
VoiceBroadcastRecordingsStore,
|
||||
VoiceBroadcastRecording,
|
||||
VoiceBroadcastPlaybacksStore,
|
||||
VoiceBroadcastPlayback,
|
||||
} from "../../../src/voice-broadcast";
|
||||
import { mkEvent, stubClient } from "../../test-utils";
|
||||
import { mkVoiceBroadcastInfoStateEvent } from "./test-utils";
|
||||
|
||||
jest.mock("../../../src/voice-broadcast/models/VoiceBroadcastRecording", () => ({
|
||||
VoiceBroadcastRecording: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("../../../src/Modal");
|
||||
|
||||
describe("startNewVoiceBroadcastRecording", () => {
|
||||
const roomId = "!room:example.com";
|
||||
const otherUserId = "@other:example.com";
|
||||
let client: MatrixClient;
|
||||
let playbacksStore: VoiceBroadcastPlaybacksStore;
|
||||
let recordingsStore: VoiceBroadcastRecordingsStore;
|
||||
let room: Room;
|
||||
let infoEvent: MatrixEvent;
|
||||
let otherEvent: MatrixEvent;
|
||||
let result: VoiceBroadcastRecording | null;
|
||||
|
||||
beforeEach(() => {
|
||||
client = stubClient();
|
||||
room = new Room(roomId, client, client.getUserId()!);
|
||||
jest.spyOn(room.currentState, "maySendStateEvent");
|
||||
|
||||
mocked(client.getRoom).mockImplementation((getRoomId: string) => {
|
||||
if (getRoomId === roomId) {
|
||||
return room;
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
mocked(client.sendStateEvent).mockImplementation(
|
||||
(sendRoomId: string, eventType: string, content: any, stateKey: string): Promise<ISendEventResponse> => {
|
||||
if (sendRoomId === roomId && eventType === VoiceBroadcastInfoEventType) {
|
||||
return Promise.resolve({ event_id: infoEvent.getId()! });
|
||||
}
|
||||
|
||||
throw new Error("Unexpected sendStateEvent call");
|
||||
},
|
||||
);
|
||||
|
||||
infoEvent = mkVoiceBroadcastInfoStateEvent(
|
||||
roomId,
|
||||
VoiceBroadcastInfoState.Started,
|
||||
client.getUserId()!,
|
||||
client.getDeviceId()!,
|
||||
);
|
||||
otherEvent = mkEvent({
|
||||
event: true,
|
||||
type: EventType.RoomMember,
|
||||
content: {},
|
||||
user: client.getUserId()!,
|
||||
room: roomId,
|
||||
skey: "",
|
||||
});
|
||||
|
||||
playbacksStore = new VoiceBroadcastPlaybacksStore(recordingsStore);
|
||||
recordingsStore = {
|
||||
setCurrent: jest.fn(),
|
||||
getCurrent: jest.fn(),
|
||||
} as unknown as VoiceBroadcastRecordingsStore;
|
||||
|
||||
mocked(VoiceBroadcastRecording).mockImplementation((infoEvent: MatrixEvent, client: MatrixClient): any => {
|
||||
return {
|
||||
infoEvent,
|
||||
client,
|
||||
start: jest.fn(),
|
||||
} as unknown as VoiceBroadcastRecording;
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("when trying to start a broadcast if there is no connection", () => {
|
||||
beforeEach(async () => {
|
||||
mocked(client.getSyncState).mockReturnValue(SyncState.Error);
|
||||
result = await startNewVoiceBroadcastRecording(room, client, playbacksStore, recordingsStore);
|
||||
});
|
||||
|
||||
it("should show an info dialog and not start a recording", () => {
|
||||
expect(result).toBeNull();
|
||||
expect(Modal.createDialog).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the current user is allowed to send voice broadcast info state events", () => {
|
||||
beforeEach(() => {
|
||||
mocked(room.currentState.maySendStateEvent).mockReturnValue(true);
|
||||
});
|
||||
|
||||
describe("when currently listening to a broadcast and there is no recording", () => {
|
||||
let playback: VoiceBroadcastPlayback;
|
||||
|
||||
beforeEach(() => {
|
||||
playback = new VoiceBroadcastPlayback(infoEvent, client, recordingsStore);
|
||||
jest.spyOn(playback, "pause");
|
||||
playbacksStore.setCurrent(playback);
|
||||
});
|
||||
|
||||
it("should stop listen to the current broadcast and create a new recording", async () => {
|
||||
mocked(client.sendStateEvent).mockImplementation(
|
||||
async (
|
||||
_roomId: string,
|
||||
_eventType: string,
|
||||
_content: any,
|
||||
_stateKey = "",
|
||||
): Promise<ISendEventResponse> => {
|
||||
window.setTimeout(() => {
|
||||
// emit state events after resolving the promise
|
||||
room.currentState.setStateEvents([otherEvent]);
|
||||
room.currentState.setStateEvents([infoEvent]);
|
||||
}, 0);
|
||||
return { event_id: infoEvent.getId()! };
|
||||
},
|
||||
);
|
||||
const recording = await startNewVoiceBroadcastRecording(room, client, playbacksStore, recordingsStore);
|
||||
expect(recording).not.toBeNull();
|
||||
|
||||
// expect to stop and clear the current playback
|
||||
expect(playback.pause).toHaveBeenCalled();
|
||||
expect(playbacksStore.getCurrent()).toBeNull();
|
||||
|
||||
expect(client.sendStateEvent).toHaveBeenCalledWith(
|
||||
roomId,
|
||||
VoiceBroadcastInfoEventType,
|
||||
{
|
||||
chunk_length: 120,
|
||||
device_id: client.getDeviceId(),
|
||||
state: VoiceBroadcastInfoState.Started,
|
||||
},
|
||||
client.getUserId()!,
|
||||
);
|
||||
expect(recording!.infoEvent).toBe(infoEvent);
|
||||
expect(recording!.start).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("when there is already a current voice broadcast", () => {
|
||||
beforeEach(async () => {
|
||||
mocked(recordingsStore.getCurrent).mockReturnValue(new VoiceBroadcastRecording(infoEvent, client));
|
||||
|
||||
result = await startNewVoiceBroadcastRecording(room, client, playbacksStore, recordingsStore);
|
||||
});
|
||||
|
||||
it("should not start a voice broadcast", () => {
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("should show an info dialog", () => {
|
||||
expect(Modal.createDialog).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe("when there already is a live broadcast of the current user in the room", () => {
|
||||
beforeEach(async () => {
|
||||
room.currentState.setStateEvents([
|
||||
mkVoiceBroadcastInfoStateEvent(
|
||||
roomId,
|
||||
VoiceBroadcastInfoState.Resumed,
|
||||
client.getUserId()!,
|
||||
client.getDeviceId()!,
|
||||
),
|
||||
]);
|
||||
|
||||
result = await startNewVoiceBroadcastRecording(room, client, playbacksStore, recordingsStore);
|
||||
});
|
||||
|
||||
it("should not start a voice broadcast", () => {
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("should show an info dialog", () => {
|
||||
expect(Modal.createDialog).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe("when there already is a live broadcast of another user", () => {
|
||||
beforeEach(async () => {
|
||||
room.currentState.setStateEvents([
|
||||
mkVoiceBroadcastInfoStateEvent(roomId, VoiceBroadcastInfoState.Resumed, otherUserId, "ASD123"),
|
||||
]);
|
||||
|
||||
result = await startNewVoiceBroadcastRecording(room, client, playbacksStore, recordingsStore);
|
||||
});
|
||||
|
||||
it("should not start a voice broadcast", () => {
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("should show an info dialog", () => {
|
||||
expect(Modal.createDialog).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the current user is not allowed to send voice broadcast info state events", () => {
|
||||
beforeEach(async () => {
|
||||
mocked(room.currentState.maySendStateEvent).mockReturnValue(false);
|
||||
result = await startNewVoiceBroadcastRecording(room, client, playbacksStore, recordingsStore);
|
||||
});
|
||||
|
||||
it("should not start a voice broadcast", () => {
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("should show an info dialog", () => {
|
||||
expect(Modal.createDialog).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
131
test/unit-tests/voice-broadcast/utils/test-utils.ts
Normal file
131
test/unit-tests/voice-broadcast/utils/test-utils.ts
Normal file
|
@ -0,0 +1,131 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { Optional } from "matrix-events-sdk";
|
||||
import { EventType, IContent, MatrixEvent, MsgType, RelationType, Room, RoomMember } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { SdkContextClass } from "../../../src/contexts/SDKContext";
|
||||
import {
|
||||
VoiceBroadcastPlayback,
|
||||
VoiceBroadcastPreRecording,
|
||||
VoiceBroadcastRecording,
|
||||
} from "../../../src/voice-broadcast";
|
||||
import {
|
||||
VoiceBroadcastChunkEventType,
|
||||
VoiceBroadcastInfoEventType,
|
||||
VoiceBroadcastInfoState,
|
||||
} from "../../../src/voice-broadcast/types";
|
||||
import { mkEvent } from "../../test-utils";
|
||||
|
||||
// timestamp incremented on each call to prevent duplicate timestamp
|
||||
let timestamp = new Date().getTime();
|
||||
|
||||
export const mkVoiceBroadcastInfoStateEvent = (
|
||||
roomId: Optional<string>,
|
||||
state: Optional<VoiceBroadcastInfoState>,
|
||||
senderId: Optional<string>,
|
||||
senderDeviceId: Optional<string>,
|
||||
startedInfoEvent?: MatrixEvent,
|
||||
lastChunkSequence?: number,
|
||||
): MatrixEvent => {
|
||||
const relationContent: IContent = {};
|
||||
|
||||
if (startedInfoEvent) {
|
||||
relationContent["m.relates_to"] = {
|
||||
event_id: startedInfoEvent.getId(),
|
||||
rel_type: "m.reference",
|
||||
};
|
||||
}
|
||||
|
||||
const lastChunkSequenceContent = lastChunkSequence ? { last_chunk_sequence: lastChunkSequence } : {};
|
||||
|
||||
return mkEvent({
|
||||
event: true,
|
||||
// @ts-ignore allow everything here for edge test cases
|
||||
room: roomId,
|
||||
// @ts-ignore allow everything here for edge test cases
|
||||
user: senderId,
|
||||
type: VoiceBroadcastInfoEventType,
|
||||
// @ts-ignore allow everything here for edge test cases
|
||||
skey: senderId,
|
||||
content: {
|
||||
state,
|
||||
device_id: senderDeviceId,
|
||||
...relationContent,
|
||||
...lastChunkSequenceContent,
|
||||
},
|
||||
ts: timestamp++,
|
||||
});
|
||||
};
|
||||
|
||||
export const mkVoiceBroadcastChunkEvent = (
|
||||
infoEventId: string,
|
||||
userId: string,
|
||||
roomId: string,
|
||||
duration: number,
|
||||
sequence?: number,
|
||||
timestamp?: number,
|
||||
): MatrixEvent => {
|
||||
return mkEvent({
|
||||
event: true,
|
||||
user: userId,
|
||||
room: roomId,
|
||||
type: EventType.RoomMessage,
|
||||
content: {
|
||||
msgtype: MsgType.Audio,
|
||||
["org.matrix.msc1767.audio"]: {
|
||||
duration,
|
||||
},
|
||||
info: {
|
||||
duration,
|
||||
},
|
||||
[VoiceBroadcastChunkEventType]: {
|
||||
...(sequence ? { sequence } : {}),
|
||||
},
|
||||
["m.relates_to"]: {
|
||||
rel_type: RelationType.Reference,
|
||||
event_id: infoEventId,
|
||||
},
|
||||
},
|
||||
ts: timestamp,
|
||||
});
|
||||
};
|
||||
|
||||
export const mkVoiceBroadcastPlayback = (stores: SdkContextClass): VoiceBroadcastPlayback => {
|
||||
const infoEvent = mkVoiceBroadcastInfoStateEvent(
|
||||
"!room:example.com",
|
||||
VoiceBroadcastInfoState.Started,
|
||||
"@user:example.com",
|
||||
"ASD123",
|
||||
);
|
||||
return new VoiceBroadcastPlayback(infoEvent, stores.client!, stores.voiceBroadcastRecordingsStore);
|
||||
};
|
||||
|
||||
export const mkVoiceBroadcastRecording = (stores: SdkContextClass): VoiceBroadcastRecording => {
|
||||
const infoEvent = mkVoiceBroadcastInfoStateEvent(
|
||||
"!room:example.com",
|
||||
VoiceBroadcastInfoState.Started,
|
||||
"@user:example.com",
|
||||
"ASD123",
|
||||
);
|
||||
return new VoiceBroadcastRecording(infoEvent, stores.client!);
|
||||
};
|
||||
|
||||
export const mkVoiceBroadcastPreRecording = (stores: SdkContextClass): VoiceBroadcastPreRecording => {
|
||||
const roomId = "!room:example.com";
|
||||
const userId = "@user:example.com";
|
||||
const room = new Room(roomId, stores.client!, userId);
|
||||
const roomMember = new RoomMember(roomId, userId);
|
||||
return new VoiceBroadcastPreRecording(
|
||||
room,
|
||||
roomMember,
|
||||
stores.client!,
|
||||
stores.voiceBroadcastPlaybacksStore,
|
||||
stores.voiceBroadcastRecordingsStore,
|
||||
);
|
||||
};
|
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { render, RenderResult, screen } from "jest-matrix-react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { mocked } from "jest-mock";
|
||||
import { MatrixClient, RelationType } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { textForVoiceBroadcastStoppedEvent, VoiceBroadcastInfoState } from "../../../src/voice-broadcast";
|
||||
import { stubClient } from "../../test-utils";
|
||||
import { mkVoiceBroadcastInfoStateEvent } from "./test-utils";
|
||||
import dis from "../../../src/dispatcher/dispatcher";
|
||||
import { Action } from "../../../src/dispatcher/actions";
|
||||
|
||||
jest.mock("../../../src/dispatcher/dispatcher");
|
||||
|
||||
describe("textForVoiceBroadcastStoppedEvent", () => {
|
||||
const otherUserId = "@other:example.com";
|
||||
const roomId = "!room:example.com";
|
||||
let client: MatrixClient;
|
||||
|
||||
const renderText = (senderId: string, startEventId?: string) => {
|
||||
const event = mkVoiceBroadcastInfoStateEvent(
|
||||
roomId,
|
||||
VoiceBroadcastInfoState.Stopped,
|
||||
senderId,
|
||||
client.deviceId!,
|
||||
);
|
||||
|
||||
if (startEventId) {
|
||||
event.getContent()["m.relates_to"] = {
|
||||
rel_type: RelationType.Reference,
|
||||
event_id: startEventId,
|
||||
};
|
||||
}
|
||||
|
||||
return render(<div>{textForVoiceBroadcastStoppedEvent(event, client)()}</div>);
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
client = stubClient();
|
||||
});
|
||||
|
||||
it("should render own broadcast as expected", () => {
|
||||
expect(renderText(client.getUserId()!).container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should render other users broadcast as expected", () => {
|
||||
expect(renderText(otherUserId).container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should render without login as expected", () => {
|
||||
mocked(client.getUserId).mockReturnValue(null);
|
||||
expect(renderText(otherUserId).container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe("when rendering an event with relation to the start event", () => {
|
||||
let result: RenderResult;
|
||||
|
||||
beforeEach(() => {
|
||||
result = renderText(client.getUserId()!, "$start-id");
|
||||
});
|
||||
|
||||
it("should render events with relation to the start event", () => {
|
||||
expect(result.container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe("and clicking the link", () => {
|
||||
beforeEach(async () => {
|
||||
await userEvent.click(screen.getByRole("button"));
|
||||
});
|
||||
|
||||
it("should dispatch an action to highlight the event", () => {
|
||||
expect(dis.dispatch).toHaveBeenCalledWith({
|
||||
action: Action.ViewRoom,
|
||||
event_id: "$start-id",
|
||||
highlighted: true,
|
||||
room_id: roomId,
|
||||
metricsTrigger: undefined, // room doesn't change
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { mocked } from "jest-mock";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { textForVoiceBroadcastStoppedEventWithoutLink, VoiceBroadcastInfoState } from "../../../src/voice-broadcast";
|
||||
import { stubClient } from "../../test-utils";
|
||||
import { mkVoiceBroadcastInfoStateEvent } from "./test-utils";
|
||||
|
||||
describe("textForVoiceBroadcastStoppedEventWithoutLink", () => {
|
||||
const otherUserId = "@other:example.com";
|
||||
const roomId = "!room:example.com";
|
||||
let client: MatrixClient;
|
||||
|
||||
beforeAll(() => {
|
||||
client = stubClient();
|
||||
});
|
||||
|
||||
const getText = (senderId: string, startEventId?: string) => {
|
||||
const event = mkVoiceBroadcastInfoStateEvent(
|
||||
roomId,
|
||||
VoiceBroadcastInfoState.Stopped,
|
||||
senderId,
|
||||
client.deviceId!,
|
||||
);
|
||||
return textForVoiceBroadcastStoppedEventWithoutLink(event);
|
||||
};
|
||||
|
||||
it("when called for an own broadcast it should return the expected text", () => {
|
||||
expect(getText(client.getUserId()!)).toBe("You ended a voice broadcast");
|
||||
});
|
||||
|
||||
it("when called for other ones broadcast it should return the expected text", () => {
|
||||
expect(getText(otherUserId)).toBe(`${otherUserId} ended a voice broadcast`);
|
||||
});
|
||||
|
||||
it("when not logged in it should return the exptected text", () => {
|
||||
mocked(client.getUserId).mockReturnValue(null);
|
||||
expect(getText(otherUserId)).toBe(`${otherUserId} ended a voice broadcast`);
|
||||
});
|
||||
});
|
Loading…
Add table
Add a link
Reference in a new issue