Remove abandoned Voice Broadcasts labs flag (#28548)

* Remove abandoned Voice Broadcasts labs flag

Any existing voice broadcasts will be shown as a series of voice messages which will sequence play as normal

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Remove dead code

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Update snapshots

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
Michael Telatynski 2024-12-02 10:53:27 +00:00 committed by GitHub
parent 5d72735b1f
commit d8ebc68aa8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
174 changed files with 29 additions and 13632 deletions

View file

@ -39,10 +39,6 @@ import { Action } from "../../src/dispatcher/actions";
import { getFunctionalMembers } from "../../src/utils/room/getFunctionalMembers";
import SettingsStore from "../../src/settings/SettingsStore";
import { UIFeature } from "../../src/settings/UIFeature";
import { VoiceBroadcastInfoState, VoiceBroadcastPlayback, VoiceBroadcastRecording } from "../../src/voice-broadcast";
import { mkVoiceBroadcastInfoStateEvent } from "./voice-broadcast/utils/test-utils";
import { SdkContextClass } from "../../src/contexts/SDKContext";
import Modal from "../../src/Modal";
import { createAudioContext } from "../../src/audio/compat";
import * as ManagedHybrid from "../../src/widgets/ManagedHybrid";
@ -403,53 +399,6 @@ describe("LegacyCallHandler", () => {
await callHandler.placeCall(NATIVE_ROOM_ALICE, CallType.Voice);
expect(spy).toHaveBeenCalledWith(MatrixClientPeg.safeGet().getRoom(NATIVE_ROOM_ALICE));
});
describe("when listening to a voice broadcast", () => {
let voiceBroadcastPlayback: VoiceBroadcastPlayback;
beforeEach(() => {
voiceBroadcastPlayback = new VoiceBroadcastPlayback(
mkVoiceBroadcastInfoStateEvent(
"!room:example.com",
VoiceBroadcastInfoState.Started,
MatrixClientPeg.safeGet().getSafeUserId(),
"d42",
),
MatrixClientPeg.safeGet(),
SdkContextClass.instance.voiceBroadcastRecordingsStore,
);
SdkContextClass.instance.voiceBroadcastPlaybacksStore.setCurrent(voiceBroadcastPlayback);
jest.spyOn(voiceBroadcastPlayback, "pause").mockImplementation();
});
it("and placing a call should pause the broadcast", async () => {
callHandler.placeCall(NATIVE_ROOM_ALICE, CallType.Voice);
await untilCallHandlerEvent(callHandler, LegacyCallHandlerEvent.CallState);
expect(voiceBroadcastPlayback.pause).toHaveBeenCalled();
});
});
describe("when recording a voice broadcast", () => {
beforeEach(() => {
SdkContextClass.instance.voiceBroadcastRecordingsStore.setCurrent(
new VoiceBroadcastRecording(
mkVoiceBroadcastInfoStateEvent(
"!room:example.com",
VoiceBroadcastInfoState.Started,
MatrixClientPeg.safeGet().getSafeUserId(),
"d42",
),
MatrixClientPeg.safeGet(),
),
);
});
it("and placing a call should show the info dialog", async () => {
callHandler.placeCall(NATIVE_ROOM_ALICE, CallType.Voice);
expect(Modal.createDialog).toMatchSnapshot();
});
});
});
describe("LegacyCallHandler without third party protocols", () => {
@ -528,9 +477,6 @@ describe("LegacyCallHandler without third party protocols", () => {
audioElement.id = "remoteAudio";
document.body.appendChild(audioElement);
SdkContextClass.instance.voiceBroadcastPlaybacksStore.clearCurrent();
SdkContextClass.instance.voiceBroadcastRecordingsStore.clearCurrent();
fetchMock.get(
"/media/ring.mp3",
{ body: new Blob(["1", "2", "3", "4"], { type: "audio/mpeg" }) },

View file

@ -43,8 +43,6 @@ import { mkThread } from "../test-utils/threads";
import dis from "../../src/dispatcher/dispatcher";
import { ThreadPayload } from "../../src/dispatcher/payloads/ThreadPayload";
import { Action } from "../../src/dispatcher/actions";
import { VoiceBroadcastChunkEventType, VoiceBroadcastInfoState } from "../../src/voice-broadcast";
import { mkVoiceBroadcastInfoStateEvent } from "./voice-broadcast/utils/test-utils";
import { addReplyToMessageContent } from "../../src/utils/Reply";
jest.mock("../../src/utils/notifications", () => ({
@ -85,16 +83,13 @@ describe("Notifier", () => {
});
};
const mkAudioEvent = (broadcastChunkContent?: object): MatrixEvent => {
const chunkContent = broadcastChunkContent ? { [VoiceBroadcastChunkEventType]: broadcastChunkContent } : {};
const mkAudioEvent = (): MatrixEvent => {
return mkEvent({
event: true,
type: EventType.RoomMessage,
user: "@user:example.com",
room: "!room:example.com",
content: {
...chunkContent,
msgtype: MsgType.Audio,
body: "test audio message",
},
@ -320,24 +315,6 @@ describe("Notifier", () => {
);
});
it("should display the expected notification for a broadcast chunk with sequence = 1", () => {
const audioEvent = mkAudioEvent({ sequence: 1 });
Notifier.displayPopupNotification(audioEvent, testRoom);
expect(MockPlatform.displayNotification).toHaveBeenCalledWith(
"@user:example.com (!room1:server)",
"@user:example.com started a voice broadcast",
"data:image/png;base64,00",
testRoom,
audioEvent,
);
});
it("should display the expected notification for a broadcast chunk with sequence = 2", () => {
const audioEvent = mkAudioEvent({ sequence: 2 });
Notifier.displayPopupNotification(audioEvent, testRoom);
expect(MockPlatform.displayNotification).not.toHaveBeenCalled();
});
it("should strip reply fallback", () => {
const event = mkMessage({
msg: "Test",
@ -581,24 +558,6 @@ describe("Notifier", () => {
Notifier.evaluateEvent(mkAudioEvent());
expect(Notifier.displayPopupNotification).toHaveBeenCalledTimes(1);
});
it("should not show a notification for broadcast info events in any case", () => {
// Let client decide to show a notification
mockClient.getPushActionsForEvent.mockReturnValue({
notify: true,
tweaks: {},
});
const broadcastStartedEvent = mkVoiceBroadcastInfoStateEvent(
"!other:example.org",
VoiceBroadcastInfoState.Started,
"@user:example.com",
"ABC123",
);
Notifier.evaluateEvent(broadcastStartedEvent);
expect(Notifier.displayPopupNotification).not.toHaveBeenCalled();
});
});
describe("setPromptHidden", () => {

View file

@ -18,10 +18,6 @@ describe("SdkConfig", () => {
describe("with custom values", () => {
beforeEach(() => {
SdkConfig.put({
voice_broadcast: {
chunk_length: 42,
max_length: 1337,
},
feedback: {
existing_issues_url: "https://existing",
} as any,
@ -30,8 +26,6 @@ describe("SdkConfig", () => {
it("should return the custom config", () => {
const customConfig = JSON.parse(JSON.stringify(DEFAULTS));
customConfig.voice_broadcast.chunk_length = 42;
customConfig.voice_broadcast.max_length = 1337;
customConfig.feedback.existing_issues_url = "https://existing";
expect(SdkConfig.get()).toEqual(customConfig);
});

View file

@ -16,11 +16,6 @@ import { SpaceStoreClass } from "../../src/stores/spaces/SpaceStore";
import { WidgetLayoutStore } from "../../src/stores/widgets/WidgetLayoutStore";
import { WidgetPermissionStore } from "../../src/stores/widgets/WidgetPermissionStore";
import WidgetStore from "../../src/stores/WidgetStore";
import {
VoiceBroadcastPlaybacksStore,
VoiceBroadcastPreRecordingStore,
VoiceBroadcastRecordingsStore,
} from "../../src/voice-broadcast";
/**
* A class which provides the same API as SdkContextClass but adds additional unsafe setters which can
@ -36,9 +31,6 @@ export class TestSdkContext extends SdkContextClass {
declare public _PosthogAnalytics?: PosthogAnalytics;
declare public _SlidingSyncManager?: SlidingSyncManager;
declare public _SpaceStore?: SpaceStoreClass;
declare public _VoiceBroadcastRecordingsStore?: VoiceBroadcastRecordingsStore;
declare public _VoiceBroadcastPreRecordingStore?: VoiceBroadcastPreRecordingStore;
declare public _VoiceBroadcastPlaybacksStore?: VoiceBroadcastPlaybacksStore;
constructor() {
super();

View file

@ -1,24 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`LegacyCallHandler when recording a voice broadcast and placing a call should show the info dialog 1`] = `
[MockFunction] {
"calls": [
[
[Function],
{
"description": <p>
You cant start a call as you are currently recording a live broadcast. Please end your live broadcast in order to start a call.
</p>,
"hasCloseButton": true,
"title": "Cant start a call",
},
],
],
"results": [
{
"type": "return",
"value": undefined,
},
],
}
`;

View file

@ -44,7 +44,6 @@ import {
} from "../../../test-utils";
import * as leaveRoomUtils from "../../../../src/utils/leave-behaviour";
import { OidcClientError } from "../../../../src/utils/oidc/error";
import * as voiceBroadcastUtils from "../../../../src/voice-broadcast/utils/cleanUpBroadcasts";
import LegacyCallHandler from "../../../../src/LegacyCallHandler";
import { CallStore } from "../../../../src/stores/CallStore";
import { Call } from "../../../../src/models/Call";
@ -811,7 +810,6 @@ describe("<MatrixChat />", () => {
jest.spyOn(LegacyCallHandler.instance, "hangupAllCalls")
.mockClear()
.mockImplementation(() => {});
jest.spyOn(voiceBroadcastUtils, "cleanUpBroadcasts").mockImplementation(async () => {});
jest.spyOn(PosthogAnalytics.instance, "logout").mockImplementation(() => {});
jest.spyOn(EventIndexPeg, "deleteEventIndex").mockImplementation(async () => {});
@ -831,22 +829,12 @@ describe("<MatrixChat />", () => {
jest.spyOn(logger, "warn").mockClear();
});
afterAll(() => {
jest.spyOn(voiceBroadcastUtils, "cleanUpBroadcasts").mockRestore();
});
it("should hangup all legacy calls", async () => {
await getComponentAndWaitForReady();
await dispatchLogoutAndWait();
expect(LegacyCallHandler.instance.hangupAllCalls).toHaveBeenCalled();
});
it("should cleanup broadcasts", async () => {
await getComponentAndWaitForReady();
await dispatchLogoutAndWait();
expect(voiceBroadcastUtils.cleanUpBroadcasts).toHaveBeenCalled();
});
it("should disconnect all calls", async () => {
await getComponentAndWaitForReady();
await dispatchLogoutAndWait();

View file

@ -10,7 +10,7 @@ import React from "react";
import { mocked, Mocked } from "jest-mock";
import { screen, render, act, cleanup } from "jest-matrix-react";
import userEvent from "@testing-library/user-event";
import { MatrixClient, PendingEventOrdering, Room, MatrixEvent, RoomStateEvent } from "matrix-js-sdk/src/matrix";
import { MatrixClient, PendingEventOrdering, Room, RoomStateEvent } from "matrix-js-sdk/src/matrix";
import { Widget, ClientWidgetApi } from "matrix-widget-api";
import { UserEvent } from "@testing-library/user-event/dist/types/setup/setup";
@ -26,7 +26,6 @@ import {
wrapInSdkContext,
mkRoomCreateEvent,
mockPlatformPeg,
flushPromises,
useMockMediaDevices,
} from "../../../test-utils";
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
@ -39,17 +38,7 @@ import defaultDispatcher from "../../../../src/dispatcher/dispatcher";
import { Action } from "../../../../src/dispatcher/actions";
import { ViewRoomPayload } from "../../../../src/dispatcher/payloads/ViewRoomPayload";
import { TestSdkContext } from "../../TestSdkContext";
import {
VoiceBroadcastInfoState,
VoiceBroadcastPlaybacksStore,
VoiceBroadcastPreRecording,
VoiceBroadcastPreRecordingStore,
VoiceBroadcastRecording,
VoiceBroadcastRecordingsStore,
} from "../../../../src/voice-broadcast";
import { mkVoiceBroadcastInfoStateEvent } from "../../voice-broadcast/utils/test-utils";
import { RoomViewStore } from "../../../../src/stores/RoomViewStore";
import { IRoomStateEventsActionPayload } from "../../../../src/actions/MatrixActionCreators";
import { Container, WidgetLayoutStore } from "../../../../src/stores/widgets/WidgetLayoutStore";
import WidgetStore from "../../../../src/stores/WidgetStore";
import { WidgetType } from "../../../../src/widgets/WidgetType";
@ -76,13 +65,6 @@ describe("PipContainer", () => {
let room: Room;
let room2: Room;
let alice: RoomMember;
let voiceBroadcastRecordingsStore: VoiceBroadcastRecordingsStore;
let voiceBroadcastPreRecordingStore: VoiceBroadcastPreRecordingStore;
let voiceBroadcastPlaybacksStore: VoiceBroadcastPlaybacksStore;
const actFlushPromises = async () => {
await flushPromises();
};
beforeEach(async () => {
useMockMediaDevices();
@ -125,13 +107,7 @@ describe("PipContainer", () => {
sdkContext = new TestSdkContext();
// @ts-ignore PipContainer uses SDKContext in the constructor
SdkContextClass.instance = sdkContext;
voiceBroadcastRecordingsStore = new VoiceBroadcastRecordingsStore();
voiceBroadcastPreRecordingStore = new VoiceBroadcastPreRecordingStore();
voiceBroadcastPlaybacksStore = new VoiceBroadcastPlaybacksStore(voiceBroadcastRecordingsStore);
sdkContext.client = client;
sdkContext._VoiceBroadcastRecordingsStore = voiceBroadcastRecordingsStore;
sdkContext._VoiceBroadcastPreRecordingStore = voiceBroadcastPreRecordingStore;
sdkContext._VoiceBroadcastPlaybacksStore = voiceBroadcastPlaybacksStore;
});
afterEach(async () => {
@ -190,51 +166,10 @@ describe("PipContainer", () => {
ActiveWidgetStore.instance.destroyPersistentWidget("1", room.roomId);
};
const makeVoiceBroadcastInfoStateEvent = (): MatrixEvent => {
return mkVoiceBroadcastInfoStateEvent(
room.roomId,
VoiceBroadcastInfoState.Started,
alice.userId,
client.getDeviceId() || "",
);
};
const setUpVoiceBroadcastRecording = () => {
const infoEvent = makeVoiceBroadcastInfoStateEvent();
const voiceBroadcastRecording = new VoiceBroadcastRecording(infoEvent, client);
voiceBroadcastRecordingsStore.setCurrent(voiceBroadcastRecording);
};
const setUpVoiceBroadcastPreRecording = () => {
const voiceBroadcastPreRecording = new VoiceBroadcastPreRecording(
room,
alice,
client,
voiceBroadcastPlaybacksStore,
voiceBroadcastRecordingsStore,
);
voiceBroadcastPreRecordingStore.setCurrent(voiceBroadcastPreRecording);
};
const setUpRoomViewStore = () => {
sdkContext._RoomViewStore = new RoomViewStore(defaultDispatcher, sdkContext);
};
const mkVoiceBroadcast = (room: Room): MatrixEvent => {
const infoEvent = makeVoiceBroadcastInfoStateEvent();
room.currentState.setStateEvents([infoEvent]);
defaultDispatcher.dispatch<IRoomStateEventsActionPayload>(
{
action: "MatrixActions.RoomState.events",
event: infoEvent,
state: room.currentState,
lastStateEvent: null,
},
true,
);
return infoEvent;
};
it("hides if there's no content", () => {
renderPip();
expect(screen.queryByRole("complementary")).toBeNull();
@ -339,138 +274,4 @@ describe("PipContainer", () => {
WidgetStore.instance.removeVirtualWidget("1", room.roomId);
});
describe("when there is a voice broadcast recording and pre-recording", () => {
beforeEach(async () => {
setUpVoiceBroadcastPreRecording();
setUpVoiceBroadcastRecording();
renderPip();
await actFlushPromises();
});
it("should render the voice broadcast recording PiP", () => {
// check for the „Live“ badge to be present
expect(screen.queryByText("Live")).toBeInTheDocument();
});
it("and a call it should show both, the call and the recording", async () => {
await withCall(async () => {
// Broadcast: Check for the „Live“ badge to be present
expect(screen.queryByText("Live")).toBeInTheDocument();
// Call: Check for the „Leave“ button to be present
screen.getByRole("button", { name: "Leave" });
});
});
});
describe("when there is a voice broadcast playback and pre-recording", () => {
beforeEach(async () => {
mkVoiceBroadcast(room);
setUpVoiceBroadcastPreRecording();
renderPip();
await actFlushPromises();
});
it("should render the voice broadcast pre-recording PiP", () => {
// check for the „Go live“ button
expect(screen.queryByText("Go live")).toBeInTheDocument();
});
});
describe("when there is a voice broadcast pre-recording", () => {
beforeEach(async () => {
setUpVoiceBroadcastPreRecording();
renderPip();
await actFlushPromises();
});
it("should render the voice broadcast pre-recording PiP", () => {
// check for the „Go live“ button
expect(screen.queryByText("Go live")).toBeInTheDocument();
});
});
describe("when listening to a voice broadcast in a room and then switching to another room", () => {
beforeEach(async () => {
setUpRoomViewStore();
viewRoom(room.roomId);
mkVoiceBroadcast(room);
await actFlushPromises();
expect(voiceBroadcastPlaybacksStore.getCurrent()).toBeTruthy();
await voiceBroadcastPlaybacksStore.getCurrent()?.start();
viewRoom(room2.roomId);
renderPip();
});
it("should render the small voice broadcast playback PiP", () => {
// check for the „pause voice broadcast“ button
expect(screen.getByLabelText("pause voice broadcast")).toBeInTheDocument();
// check for the absence of the „30s forward“ button
expect(screen.queryByLabelText("30s forward")).not.toBeInTheDocument();
});
});
describe("when viewing a room with a live voice broadcast", () => {
let startEvent!: MatrixEvent;
beforeEach(async () => {
setUpRoomViewStore();
viewRoom(room.roomId);
startEvent = mkVoiceBroadcast(room);
renderPip();
await actFlushPromises();
});
it("should render the voice broadcast playback pip", () => {
// check for the „resume voice broadcast“ button
expect(screen.queryByLabelText("play voice broadcast")).toBeInTheDocument();
});
describe("and the broadcast stops", () => {
beforeEach(async () => {
const stopEvent = mkVoiceBroadcastInfoStateEvent(
room.roomId,
VoiceBroadcastInfoState.Stopped,
alice.userId,
client.getDeviceId() || "",
startEvent,
);
await act(async () => {
room.currentState.setStateEvents([stopEvent]);
defaultDispatcher.dispatch<IRoomStateEventsActionPayload>(
{
action: "MatrixActions.RoomState.events",
event: stopEvent,
state: room.currentState,
lastStateEvent: stopEvent,
},
true,
);
await flushPromises();
});
});
it("should not render the voice broadcast playback pip", () => {
// check for the „resume voice broadcast“ button
expect(screen.queryByLabelText("play voice broadcast")).not.toBeInTheDocument();
});
});
describe("and leaving the room", () => {
beforeEach(async () => {
await act(async () => {
viewRoom(room2.roomId);
await flushPromises();
});
});
it("should not render the voice broadcast playback pip", () => {
// check for the „resume voice broadcast“ button
expect(screen.queryByLabelText("play voice broadcast")).not.toBeInTheDocument();
});
});
});
});

View file

@ -7,20 +7,14 @@ Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { act, render, RenderResult, screen, waitFor } from "jest-matrix-react";
import { DEVICE_CODE_SCOPE, MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
import { render, screen, waitFor } from "jest-matrix-react";
import { DEVICE_CODE_SCOPE, MatrixClient, Room } from "matrix-js-sdk/src/matrix";
import { CryptoApi } from "matrix-js-sdk/src/crypto-api";
import { mocked } from "jest-mock";
import fetchMock from "fetch-mock-jest";
import UnwrappedUserMenu from "../../../../src/components/structures/UserMenu";
import { stubClient, wrapInSdkContext } from "../../../test-utils";
import {
VoiceBroadcastInfoState,
VoiceBroadcastRecording,
VoiceBroadcastRecordingsStore,
} from "../../../../src/voice-broadcast";
import { mkVoiceBroadcastInfoStateEvent } from "../../voice-broadcast/utils/test-utils";
import { TestSdkContext } from "../../TestSdkContext";
import defaultDispatcher from "../../../../src/dispatcher/dispatcher";
import LogoutDialog from "../../../../src/components/views/dialogs/LogoutDialog";
@ -34,71 +28,12 @@ import { UserTab } from "../../../../src/components/views/dialogs/UserTab";
describe("<UserMenu>", () => {
let client: MatrixClient;
let renderResult: RenderResult;
let sdkContext: TestSdkContext;
beforeEach(() => {
sdkContext = new TestSdkContext();
});
describe("<UserMenu> when video broadcast", () => {
let voiceBroadcastInfoEvent: MatrixEvent;
let voiceBroadcastRecording: VoiceBroadcastRecording;
let voiceBroadcastRecordingsStore: VoiceBroadcastRecordingsStore;
beforeAll(() => {
client = stubClient();
voiceBroadcastInfoEvent = mkVoiceBroadcastInfoStateEvent(
"!room:example.com",
VoiceBroadcastInfoState.Started,
client.getUserId() || "",
client.getDeviceId() || "",
);
});
beforeEach(() => {
voiceBroadcastRecordingsStore = new VoiceBroadcastRecordingsStore();
sdkContext._VoiceBroadcastRecordingsStore = voiceBroadcastRecordingsStore;
voiceBroadcastRecording = new VoiceBroadcastRecording(voiceBroadcastInfoEvent, client);
});
describe("when rendered", () => {
beforeEach(() => {
const UserMenu = wrapInSdkContext(UnwrappedUserMenu, sdkContext);
renderResult = render(<UserMenu isPanelCollapsed={true} />);
});
it("should render as expected", () => {
expect(renderResult.container).toMatchSnapshot();
});
describe("and a live voice broadcast starts", () => {
beforeEach(() => {
act(() => {
voiceBroadcastRecordingsStore.setCurrent(voiceBroadcastRecording);
});
});
it("should render the live voice broadcast avatar addon", () => {
expect(renderResult.queryByTestId("user-menu-live-vb")).toBeInTheDocument();
});
describe("and the broadcast ends", () => {
beforeEach(() => {
act(() => {
voiceBroadcastRecordingsStore.clearCurrent();
});
});
it("should not render the live voice broadcast avatar addon", () => {
expect(renderResult.queryByTestId("user-menu-live-vb")).not.toBeInTheDocument();
});
});
});
});
});
describe("<UserMenu> logout", () => {
beforeEach(() => {
client = stubClient();
@ -106,7 +41,7 @@ describe("<UserMenu>", () => {
it("should logout directly if no crypto", async () => {
const UserMenu = wrapInSdkContext(UnwrappedUserMenu, sdkContext);
renderResult = render(<UserMenu isPanelCollapsed={true} />);
render(<UserMenu isPanelCollapsed={true} />);
mocked(client.getRooms).mockReturnValue([
{
@ -128,7 +63,7 @@ describe("<UserMenu>", () => {
it("should logout directly if no encrypted rooms", async () => {
const UserMenu = wrapInSdkContext(UnwrappedUserMenu, sdkContext);
renderResult = render(<UserMenu isPanelCollapsed={true} />);
render(<UserMenu isPanelCollapsed={true} />);
mocked(client.getRooms).mockReturnValue([
{
@ -152,7 +87,7 @@ describe("<UserMenu>", () => {
it("should show dialog if some encrypted rooms", async () => {
const UserMenu = wrapInSdkContext(UnwrappedUserMenu, sdkContext);
renderResult = render(<UserMenu isPanelCollapsed={true} />);
render(<UserMenu isPanelCollapsed={true} />);
mocked(client.getRooms).mockReturnValue([
{

View file

@ -1,33 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<UserMenu> <UserMenu> when video broadcast when rendered should render as expected 1`] = `
<div>
<div
class="mx_UserMenu"
>
<div
aria-expanded="false"
aria-haspopup="true"
aria-label="User menu"
class="mx_AccessibleButton mx_UserMenu_contextMenuButton"
role="button"
tabindex="0"
>
<div
class="mx_UserMenu_userAvatar"
>
<span
class="_avatar_mcap2_17 mx_BaseAvatar mx_UserMenu_userAvatar_BaseAvatar _avatar-imageless_mcap2_61"
data-color="2"
data-testid="avatar-img"
data-type="round"
role="presentation"
style="--cpd-avatar-size: 32px;"
>
u
</span>
</div>
</div>
</div>
</div>
`;

View file

@ -37,8 +37,6 @@ import dispatcher from "../../../../../src/dispatcher/dispatcher";
import SettingsStore from "../../../../../src/settings/SettingsStore";
import { ReadPinsEventId } from "../../../../../src/components/views/right_panel/types";
import { Action } from "../../../../../src/dispatcher/actions";
import { mkVoiceBroadcastInfoStateEvent } from "../../../voice-broadcast/utils/test-utils";
import { VoiceBroadcastInfoState } from "../../../../../src/voice-broadcast";
import { createMessageEventContent } from "../../../../test-utils/events";
import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx";
@ -234,17 +232,6 @@ describe("MessageContextMenu", () => {
expect(document.querySelector('li[aria-label="Forward"]')).toBeFalsy();
});
it("should not allow forwarding a voice broadcast", () => {
const broadcastStartEvent = mkVoiceBroadcastInfoStateEvent(
roomId,
VoiceBroadcastInfoState.Started,
"@user:example.com",
"ABC123",
);
createMenu(broadcastStartEvent);
expect(document.querySelector('li[aria-label="Forward"]')).toBeFalsy();
});
describe("forwarding beacons", () => {
const aliceId = "@alice:server.org";

View file

@ -6,14 +6,11 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import { Feature, ServerSupport } from "matrix-js-sdk/src/feature";
import { MatrixClient, MatrixEvent, RelationType } from "matrix-js-sdk/src/matrix";
import { MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix";
import { screen, act } from "jest-matrix-react";
import userEvent from "@testing-library/user-event";
import { flushPromises, mkEvent, stubClient } from "../../../../test-utils";
import { mkVoiceBroadcastInfoStateEvent } from "../../../voice-broadcast/utils/test-utils";
import { VoiceBroadcastInfoState } from "../../../../../src/voice-broadcast";
import { createRedactEventDialog } from "../../../../../src/components/views/dialogs/ConfirmRedactDialog";
describe("ConfirmRedactDialog", () => {
@ -21,15 +18,6 @@ describe("ConfirmRedactDialog", () => {
let client: MatrixClient;
let mxEvent: MatrixEvent;
const setUpVoiceBroadcastStartedEvent = () => {
mxEvent = mkVoiceBroadcastInfoStateEvent(
roomId,
VoiceBroadcastInfoState.Started,
client.getUserId()!,
client.deviceId!,
);
};
const confirmDeleteVoiceBroadcastStartedEvent = async () => {
act(() => createRedactEventDialog({ mxEvent }));
// double-flush promises required for the dialog to show up
@ -68,44 +56,4 @@ describe("ConfirmRedactDialog", () => {
`cannot redact event ${mxEvent.getId()} without room ID`,
);
});
describe("when redacting a voice broadcast started event", () => {
beforeEach(() => {
setUpVoiceBroadcastStartedEvent();
});
describe("and the server does not support relation based redactions", () => {
beforeEach(() => {
client.canSupport.set(Feature.RelationBasedRedactions, ServerSupport.Unsupported);
});
describe("and displaying and confirm the dialog for a voice broadcast", () => {
beforeEach(async () => {
await confirmDeleteVoiceBroadcastStartedEvent();
});
it("should call redact without `with_rel_types`", () => {
expect(client.redactEvent).toHaveBeenCalledWith(roomId, mxEvent.getId(), undefined, {});
});
});
});
describe("and the server supports relation based redactions", () => {
beforeEach(() => {
client.canSupport.set(Feature.RelationBasedRedactions, ServerSupport.Unstable);
});
describe("and displaying and confirm the dialog for a voice broadcast", () => {
beforeEach(async () => {
await confirmDeleteVoiceBroadcastStartedEvent();
});
it("should call redact with `with_rel_types`", () => {
expect(client.redactEvent).toHaveBeenCalledWith(roomId, mxEvent.getId(), undefined, {
with_rel_types: [RelationType.Reference],
});
});
});
});
});
});

View file

@ -185,33 +185,6 @@ exports[`DevtoolsDialog renders the devtools dialog 1`] = `
/>
</div>
</div>
<div
class="mx_SettingsFlag"
>
<label
class="mx_SettingsFlag_label"
for="mx_SettingsFlag_4yVCeEefiPqp"
>
<span
class="mx_SettingsFlag_labelText"
>
Force 15s voice broadcast chunk length
</span>
</label>
<div
aria-checked="false"
aria-disabled="false"
aria-label="Force 15s voice broadcast chunk length"
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_enabled"
id="mx_SettingsFlag_4yVCeEefiPqp"
role="switch"
tabindex="0"
>
<div
class="mx_ToggleSwitch_ball"
/>
</div>
</div>
</div>
</div>
<div

View file

@ -14,7 +14,6 @@ import fs from "fs";
import path from "path";
import SettingsStore from "../../../../../src/settings/SettingsStore";
import { VoiceBroadcastInfoEventType, VoiceBroadcastInfoState } from "../../../../../src/voice-broadcast";
import { mkEvent, mkRoom, stubClient } from "../../../../test-utils";
import MessageEvent from "../../../../../src/components/views/messages/MessageEvent";
import { RoomPermalinkCreator } from "../../../../../src/utils/permalinks/Permalinks";
@ -24,10 +23,6 @@ jest.mock("../../../../../src/components/views/messages/UnknownBody", () => ({
default: () => <div data-testid="unknown-body" />,
}));
jest.mock("../../../../../src/voice-broadcast/components/VoiceBroadcastBody", () => ({
VoiceBroadcastBody: () => <div data-testid="voice-broadcast-body" />,
}));
jest.mock("../../../../../src/components/views/messages/MImageBody", () => ({
__esModule: true,
default: () => <div data-testid="image-body" />,
@ -81,27 +76,6 @@ describe("MessageEvent", () => {
jest.spyOn(SettingsStore, "unwatchSetting").mockImplementation(jest.fn());
});
describe("when a voice broadcast start event occurs", () => {
let result: RenderResult;
beforeEach(() => {
event = mkEvent({
event: true,
type: VoiceBroadcastInfoEventType,
user: client.getUserId()!,
room: room.roomId,
content: {
state: VoiceBroadcastInfoState.Started,
},
});
result = renderMessageEvent();
});
it("should render a VoiceBroadcast component", () => {
result.getByTestId("voice-broadcast-body");
});
});
describe("when an image with a caption is sent", () => {
let result: RenderResult;

View file

@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
*/
import * as React from "react";
import { EventType, MatrixEvent, Room, RoomMember, THREAD_RELATION_TYPE } from "matrix-js-sdk/src/matrix";
import { EventType, MatrixEvent, RoomMember, THREAD_RELATION_TYPE } from "matrix-js-sdk/src/matrix";
import { act, fireEvent, render, screen, waitFor } from "jest-matrix-react";
import userEvent from "@testing-library/user-event";
@ -19,7 +19,6 @@ import {
mkStubRoom,
mockPlatformPeg,
stubClient,
waitEnoughCyclesForModal,
} from "../../../../test-utils";
import MessageComposer from "../../../../../src/components/views/rooms/MessageComposer";
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
@ -28,7 +27,6 @@ import { IRoomState } from "../../../../../src/components/structures/RoomView";
import ResizeNotifier from "../../../../../src/utils/ResizeNotifier";
import { RoomPermalinkCreator } from "../../../../../src/utils/permalinks/Permalinks";
import { LocalRoom } from "../../../../../src/models/LocalRoom";
import { Features } from "../../../../../src/settings/Settings";
import SettingsStore from "../../../../../src/settings/SettingsStore";
import { SettingLevel } from "../../../../../src/settings/SettingLevel";
import dis from "../../../../../src/dispatcher/dispatcher";
@ -36,9 +34,6 @@ import { E2EStatus } from "../../../../../src/utils/ShieldUtils";
import { addTextToComposerRTL } from "../../../../test-utils/composer";
import UIStore, { UI_EVENTS } from "../../../../../src/stores/UIStore";
import { Action } from "../../../../../src/dispatcher/actions";
import { VoiceBroadcastInfoState, VoiceBroadcastRecording } from "../../../../../src/voice-broadcast";
import { mkVoiceBroadcastInfoStateEvent } from "../../../voice-broadcast/utils/test-utils";
import { SdkContextClass } from "../../../../../src/contexts/SDKContext";
import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx";
const openStickerPicker = async (): Promise<void> => {
@ -51,15 +46,6 @@ const startVoiceMessage = async (): Promise<void> => {
await userEvent.click(screen.getByLabelText("Voice Message"));
};
const setCurrentBroadcastRecording = (room: Room, state: VoiceBroadcastInfoState): void => {
const recording = new VoiceBroadcastRecording(
mkVoiceBroadcastInfoStateEvent(room.roomId, state, "@user:example.com", "ABC123"),
MatrixClientPeg.safeGet(),
state,
);
act(() => SdkContextClass.instance.voiceBroadcastRecordingsStore.setCurrent(recording));
};
const expectVoiceMessageRecordingTriggered = (): void => {
// Checking for the voice message dialog text, if no mic can be found.
// By this we know at least that starting a voice message was triggered.
@ -78,14 +64,11 @@ describe("MessageComposer", () => {
await clearAllModals();
jest.useRealTimers();
SdkContextClass.instance.voiceBroadcastRecordingsStore.clearCurrent();
// restore settings
act(() => {
[
"MessageComposerInput.showStickersButton",
"MessageComposerInput.showPollsButton",
Features.VoiceBroadcast,
"feature_wysiwyg_composer",
].forEach((setting: string): void => {
SettingsStore.setValue(setting, null, SettingLevel.DEVICE, SettingsStore.getDefaultValue(setting));
@ -212,10 +195,6 @@ describe("MessageComposer", () => {
setting: "MessageComposerInput.showPollsButton",
buttonLabel: "Poll",
},
{
setting: Features.VoiceBroadcast,
buttonLabel: "Voice broadcast",
},
].forEach(({ setting, buttonLabel }) => {
[true, false].forEach((value: boolean) => {
describe(`when ${setting} = ${value}`, () => {
@ -437,34 +416,6 @@ describe("MessageComposer", () => {
expectVoiceMessageRecordingTriggered();
});
});
describe("when recording a voice broadcast and trying to start a voice message", () => {
beforeEach(async () => {
setCurrentBroadcastRecording(room, VoiceBroadcastInfoState.Started);
wrapAndRender({ room });
await startVoiceMessage();
await waitEnoughCyclesForModal();
});
it("should not start a voice message and display the info dialog", async () => {
expect(screen.queryByLabelText("Stop recording")).not.toBeInTheDocument();
expect(screen.getByText("Can't start voice message")).toBeInTheDocument();
});
});
describe("when there is a stopped voice broadcast recording and trying to start a voice message", () => {
beforeEach(async () => {
setCurrentBroadcastRecording(room, VoiceBroadcastInfoState.Stopped);
wrapAndRender({ room });
await startVoiceMessage();
await waitEnoughCyclesForModal();
});
it("should try to start a voice message and should not display the info dialog", async () => {
expect(screen.queryByText("Can't start voice message")).not.toBeInTheDocument();
expectVoiceMessageRecordingTriggered();
});
});
});
describe("for a LocalRoom", () => {

View file

@ -168,27 +168,4 @@ describe("MessageComposerButtons", () => {
]);
});
});
describe("with showVoiceBroadcastButton = true", () => {
it("should render the »Voice broadcast« button", () => {
wrapAndRender(
<MessageComposerButtons
{...mockProps}
isMenuOpen={true}
showLocationButton={true}
showPollsButton={true}
showStickersButton={true}
showVoiceBroadcastButton={true}
/>,
false,
);
expect(getButtonLabels()).toEqual([
"Emoji",
"Attachment",
"More options",
["Sticker", "Voice Message", "Voice broadcast", "Poll", "Location"],
]);
});
});
});

View file

@ -9,14 +9,7 @@ Please see LICENSE files in the repository root for full details.
import React from "react";
import { render, screen, act, RenderResult } from "jest-matrix-react";
import { mocked, Mocked } from "jest-mock";
import {
MatrixClient,
PendingEventOrdering,
Room,
MatrixEvent,
RoomStateEvent,
Thread,
} from "matrix-js-sdk/src/matrix";
import { MatrixClient, PendingEventOrdering, Room, RoomStateEvent, Thread } from "matrix-js-sdk/src/matrix";
import { KnownMembership } from "matrix-js-sdk/src/types";
import { Widget } from "matrix-widget-api";
@ -40,8 +33,6 @@ import DMRoomMap from "../../../../../src/utils/DMRoomMap";
import PlatformPeg from "../../../../../src/PlatformPeg";
import BasePlatform from "../../../../../src/BasePlatform";
import { WidgetMessagingStore } from "../../../../../src/stores/widgets/WidgetMessagingStore";
import { VoiceBroadcastInfoState } from "../../../../../src/voice-broadcast";
import { mkVoiceBroadcastInfoStateEvent } from "../../../voice-broadcast/utils/test-utils";
import { TestSdkContext } from "../../../TestSdkContext";
import { SDKContext } from "../../../../../src/contexts/SDKContext";
import { shouldShowComponent } from "../../../../../src/customisations/helpers/UIComponents";
@ -61,20 +52,6 @@ describe("RoomTile", () => {
} as unknown as BasePlatform);
useMockedCalls();
const setUpVoiceBroadcast = async (state: VoiceBroadcastInfoState): Promise<void> => {
voiceBroadcastInfoEvent = mkVoiceBroadcastInfoStateEvent(
room.roomId,
state,
client.getSafeUserId(),
client.getDeviceId()!,
);
await act(async () => {
room.currentState.setStateEvents([voiceBroadcastInfoEvent]);
await flushPromises();
});
};
const renderRoomTile = (): RenderResult => {
return render(
<SDKContext.Provider value={sdkContext}>
@ -89,7 +66,6 @@ describe("RoomTile", () => {
};
let client: Mocked<MatrixClient>;
let voiceBroadcastInfoEvent: MatrixEvent;
let room: Room;
let sdkContext: TestSdkContext;
let showMessagePreview = false;
@ -303,49 +279,6 @@ describe("RoomTile", () => {
});
expect(screen.queryByLabelText(/participant/)).toBe(null);
});
describe("and a live broadcast starts", () => {
beforeEach(async () => {
renderRoomTile();
await setUpVoiceBroadcast(VoiceBroadcastInfoState.Started);
});
it("should still render the call subtitle", () => {
expect(screen.queryByText("Video")).toBeInTheDocument();
expect(screen.queryByText("Live")).not.toBeInTheDocument();
});
});
});
describe("when a live voice broadcast starts", () => {
beforeEach(async () => {
renderRoomTile();
await setUpVoiceBroadcast(VoiceBroadcastInfoState.Started);
});
it("should render the »Live« subtitle", () => {
expect(screen.queryByText("Live")).toBeInTheDocument();
});
describe("and the broadcast stops", () => {
beforeEach(async () => {
const stopEvent = mkVoiceBroadcastInfoStateEvent(
room.roomId,
VoiceBroadcastInfoState.Stopped,
client.getSafeUserId(),
client.getDeviceId()!,
voiceBroadcastInfoEvent,
);
await act(async () => {
room.currentState.setStateEvents([stopEvent]);
await flushPromises();
});
});
it("should not render the »Live« subtitle", () => {
expect(screen.queryByText("Live")).not.toBeInTheDocument();
});
});
});
});

View file

@ -17,7 +17,6 @@ import userEvent from "@testing-library/user-event";
import RolesRoomSettingsTab from "../../../../../../../src/components/views/settings/tabs/room/RolesRoomSettingsTab";
import { mkStubRoom, withClientContextRenderOptions, stubClient } from "../../../../../../test-utils";
import { MatrixClientPeg } from "../../../../../../../src/MatrixClientPeg";
import { VoiceBroadcastInfoEventType } from "../../../../../../../src/voice-broadcast";
import SettingsStore from "../../../../../../../src/settings/SettingsStore";
import { ElementCall } from "../../../../../../../src/models/Call";
@ -34,14 +33,6 @@ describe("RolesRoomSettingsTab", () => {
return renderResult;
};
const getVoiceBroadcastsSelect = async (): Promise<Element> => {
return (await renderTab()).container.querySelector("select[label='Voice broadcasts']")!;
};
const getVoiceBroadcastsSelectedOption = async (): Promise<Element> => {
return (await renderTab()).container.querySelector("select[label='Voice broadcasts'] option:checked")!;
};
beforeEach(() => {
stubClient();
cli = MatrixClientPeg.safeGet();
@ -76,26 +67,6 @@ describe("RolesRoomSettingsTab", () => {
expect(container.querySelector(`[placeholder="@admin:server"]`)).toBeDisabled();
});
it("should initially show »Moderator« permission for »Voice broadcasts«", async () => {
expect((await getVoiceBroadcastsSelectedOption()).textContent).toBe("Moderator");
});
describe("when setting »Default« permission for »Voice broadcasts«", () => {
beforeEach(async () => {
fireEvent.change(await getVoiceBroadcastsSelect(), {
target: { value: 0 },
});
});
it("should update the power levels", () => {
expect(cli.sendStateEvent).toHaveBeenCalledWith(roomId, EventType.RoomPowerLevels, {
events: {
[VoiceBroadcastInfoEventType]: 0,
},
});
});
});
describe("Element Call", () => {
const setGroupCallsEnabled = (val: boolean): void => {
jest.spyOn(SettingsStore, "getValue").mockImplementation((name: string) => {

View file

@ -11,11 +11,8 @@ import { MatrixClient } from "matrix-js-sdk/src/matrix";
import { SdkContextClass } from "../../../src/contexts/SDKContext";
import { OidcClientStore } from "../../../src/stores/oidc/OidcClientStore";
import { UserProfilesStore } from "../../../src/stores/UserProfilesStore";
import { VoiceBroadcastPreRecordingStore } from "../../../src/voice-broadcast";
import { createTestClient } from "../../test-utils";
jest.mock("../../../src/voice-broadcast/stores/VoiceBroadcastPreRecordingStore");
describe("SdkContextClass", () => {
let sdkContext = SdkContextClass.instance;
let client: MatrixClient;
@ -33,12 +30,6 @@ describe("SdkContextClass", () => {
expect(SdkContextClass.instance).toBe(globalInstance);
});
it("voiceBroadcastPreRecordingStore should always return the same VoiceBroadcastPreRecordingStore", () => {
const first = sdkContext.voiceBroadcastPreRecordingStore;
expect(first).toBeInstanceOf(VoiceBroadcastPreRecordingStore);
expect(sdkContext.voiceBroadcastPreRecordingStore).toBe(first);
});
it("userProfilesStore should raise an error without a client", () => {
expect(() => sdkContext.userProfilesStore).toThrow("Unable to create UserProfilesStore without a client");
});

View file

@ -7,19 +7,16 @@ Please see LICENSE files in the repository root for full details.
*/
import { mocked } from "jest-mock";
import { EventType, MatrixClient, MatrixEvent, MsgType, RelationType, Room } from "matrix-js-sdk/src/matrix";
import { EventType, MatrixClient, MatrixEvent, MsgType, Room } from "matrix-js-sdk/src/matrix";
import {
JSONEventFactory,
MessageEventFactory,
pickFactory,
RoomCreateEventFactory,
TextualEventFactory,
} from "../../../src/events/EventTileFactory";
import SettingsStore from "../../../src/settings/SettingsStore";
import { VoiceBroadcastChunkEventType, VoiceBroadcastInfoState } from "../../../src/voice-broadcast";
import { createTestClient, mkEvent } from "../../test-utils";
import { mkVoiceBroadcastInfoStateEvent } from "../voice-broadcast/utils/test-utils";
const roomId = "!room:example.com";
@ -31,11 +28,7 @@ describe("pickFactory", () => {
let createEventWithoutPredecessor: MatrixEvent;
let dynamicPredecessorEvent: MatrixEvent;
let voiceBroadcastStartedEvent: MatrixEvent;
let voiceBroadcastStoppedEvent: MatrixEvent;
let voiceBroadcastChunkEvent: MatrixEvent;
let utdEvent: MatrixEvent;
let utdBroadcastChunkEvent: MatrixEvent;
let audioMessageEvent: MatrixEvent;
beforeAll(() => {
@ -82,29 +75,6 @@ describe("pickFactory", () => {
last_known_event_id: null,
},
});
voiceBroadcastStartedEvent = mkVoiceBroadcastInfoStateEvent(
roomId,
VoiceBroadcastInfoState.Started,
client.getUserId()!,
client.deviceId!,
);
room.addLiveEvents([voiceBroadcastStartedEvent], { addToState: true });
voiceBroadcastStoppedEvent = mkVoiceBroadcastInfoStateEvent(
roomId,
VoiceBroadcastInfoState.Stopped,
client.getUserId()!,
client.deviceId!,
);
voiceBroadcastChunkEvent = mkEvent({
event: true,
type: EventType.RoomMessage,
user: client.getUserId()!,
room: roomId,
content: {
msgtype: MsgType.Audio,
[VoiceBroadcastChunkEventType]: {},
},
});
audioMessageEvent = mkEvent({
event: true,
type: EventType.RoomMessage,
@ -123,20 +93,6 @@ describe("pickFactory", () => {
msgtype: "m.bad.encrypted",
},
});
utdBroadcastChunkEvent = mkEvent({
event: true,
type: EventType.RoomMessage,
user: client.getUserId()!,
room: roomId,
content: {
"msgtype": "m.bad.encrypted",
"m.relates_to": {
rel_type: RelationType.Reference,
event_id: voiceBroadcastStartedEvent.getId(),
},
},
});
jest.spyOn(utdBroadcastChunkEvent, "isDecryptionFailure").mockReturnValue(true);
});
it("should return JSONEventFactory for a no-op m.room.power_levels event", () => {
@ -151,10 +107,6 @@ describe("pickFactory", () => {
});
describe("when showing hidden events", () => {
it("should return a JSONEventFactory for a voice broadcast event", () => {
expect(pickFactory(voiceBroadcastChunkEvent, client, true)).toBe(JSONEventFactory);
});
it("should return a JSONEventFactory for a room create event without predecessor", () => {
room.currentState.events.set(
EventType.RoomCreate,
@ -164,17 +116,9 @@ describe("pickFactory", () => {
expect(pickFactory(createEventWithoutPredecessor, client, true)).toBe(JSONEventFactory);
});
it("should return a TextualEventFactory for a voice broadcast stopped event", () => {
expect(pickFactory(voiceBroadcastStoppedEvent, client, true)).toBe(TextualEventFactory);
});
it("should return a MessageEventFactory for an audio message event", () => {
expect(pickFactory(audioMessageEvent, client, true)).toBe(MessageEventFactory);
});
it("should return a MessageEventFactory for a UTD broadcast chunk event", () => {
expect(pickFactory(utdBroadcastChunkEvent, client, true)).toBe(MessageEventFactory);
});
});
describe("when not showing hidden events", () => {
@ -252,14 +196,6 @@ describe("pickFactory", () => {
});
});
it("should return undefined for a voice broadcast event", () => {
expect(pickFactory(voiceBroadcastChunkEvent, client, false)).toBeUndefined();
});
it("should return a TextualEventFactory for a voice broadcast stopped event", () => {
expect(pickFactory(voiceBroadcastStoppedEvent, client, false)).toBe(TextualEventFactory);
});
it("should return a MessageEventFactory for an audio message event", () => {
expect(pickFactory(audioMessageEvent, client, false)).toBe(MessageEventFactory);
});
@ -267,9 +203,5 @@ describe("pickFactory", () => {
it("should return a MessageEventFactory for a UTD event", () => {
expect(pickFactory(utdEvent, client, false)).toBe(MessageEventFactory);
});
it("should return undefined for a UTD broadcast chunk event", () => {
expect(pickFactory(utdBroadcastChunkEvent, client, false)).toBeUndefined();
});
});
});

View file

@ -24,13 +24,6 @@ import { ActiveRoomChangedPayload } from "../../../src/dispatcher/payloads/Activ
import { SpaceStoreClass } from "../../../src/stores/spaces/SpaceStore";
import { TestSdkContext } from "../TestSdkContext";
import { ViewRoomPayload } from "../../../src/dispatcher/payloads/ViewRoomPayload";
import {
VoiceBroadcastInfoState,
VoiceBroadcastPlayback,
VoiceBroadcastPlaybacksStore,
VoiceBroadcastRecording,
} from "../../../src/voice-broadcast";
import { mkVoiceBroadcastInfoStateEvent } from "../voice-broadcast/utils/test-utils";
import Modal from "../../../src/Modal";
import ErrorDialog from "../../../src/components/views/dialogs/ErrorDialog";
import { CancelAskToJoinPayload } from "../../../src/dispatcher/payloads/CancelAskToJoinPayload";
@ -160,7 +153,6 @@ describe("RoomViewStore", function () {
stores._SlidingSyncManager = slidingSyncManager;
stores._PosthogAnalytics = new MockPosthogAnalytics();
stores._SpaceStore = new MockSpaceStore();
stores._VoiceBroadcastPlaybacksStore = new VoiceBroadcastPlaybacksStore(stores.voiceBroadcastRecordingsStore);
roomViewStore = new RoomViewStore(dis, stores);
stores._RoomViewStore = roomViewStore;
});
@ -343,88 +335,6 @@ describe("RoomViewStore", function () {
});
});
describe("when listening to a voice broadcast", () => {
let voiceBroadcastPlayback: VoiceBroadcastPlayback;
beforeEach(() => {
voiceBroadcastPlayback = new VoiceBroadcastPlayback(
mkVoiceBroadcastInfoStateEvent(
roomId,
VoiceBroadcastInfoState.Started,
mockClient.getSafeUserId(),
"d42",
),
mockClient,
stores.voiceBroadcastRecordingsStore,
);
stores.voiceBroadcastPlaybacksStore.setCurrent(voiceBroadcastPlayback);
jest.spyOn(voiceBroadcastPlayback, "pause").mockImplementation();
});
it("and viewing a call it should pause the current broadcast", async () => {
await viewCall();
expect(voiceBroadcastPlayback.pause).toHaveBeenCalled();
expect(roomViewStore.isViewingCall()).toBe(true);
});
});
describe("when recording a voice broadcast", () => {
beforeEach(() => {
stores.voiceBroadcastRecordingsStore.setCurrent(
new VoiceBroadcastRecording(
mkVoiceBroadcastInfoStateEvent(
roomId,
VoiceBroadcastInfoState.Started,
mockClient.getSafeUserId(),
"d42",
),
mockClient,
),
);
});
it("and trying to view a call, it should not actually view it and show the info dialog", async () => {
await viewCall();
expect(Modal.createDialog).toMatchSnapshot();
expect(roomViewStore.isViewingCall()).toBe(false);
});
describe("and viewing a room with a broadcast", () => {
beforeEach(async () => {
const broadcastEvent = mkVoiceBroadcastInfoStateEvent(
roomId2,
VoiceBroadcastInfoState.Started,
mockClient.getSafeUserId(),
"ABC123",
);
room2.addLiveEvents([broadcastEvent], { addToState: true });
stores.voiceBroadcastPlaybacksStore.getByInfoEvent(broadcastEvent, mockClient);
dis.dispatch({ action: Action.ViewRoom, room_id: roomId2 });
await untilDispatch(Action.ActiveRoomChanged, dis);
});
it("should continue recording", () => {
expect(stores.voiceBroadcastPlaybacksStore.getCurrent()).toBeNull();
expect(stores.voiceBroadcastRecordingsStore.getCurrent()?.getState()).toBe(
VoiceBroadcastInfoState.Started,
);
});
describe("and stopping the recording", () => {
beforeEach(async () => {
await stores.voiceBroadcastRecordingsStore.getCurrent()?.stop();
// check test precondition
expect(stores.voiceBroadcastRecordingsStore.getCurrent()).toBeNull();
});
it("should view the broadcast", () => {
expect(stores.voiceBroadcastPlaybacksStore.getCurrent()?.infoEvent.getRoomId()).toBe(roomId2);
});
});
});
});
describe("Sliding Sync", function () {
beforeEach(() => {
jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName, roomId, value) => {

View file

@ -18,26 +18,3 @@ exports[`RoomViewStore should display the generic error message when the roomId
"title": "Failed to join",
}
`;
exports[`RoomViewStore when recording a voice broadcast and trying to view a call, it should not actually view it and show the info dialog 1`] = `
[MockFunction] {
"calls": [
[
[Function],
{
"description": <p>
You cant start a call as you are currently recording a live broadcast. Please end your live broadcast in order to start a call.
</p>,
"hasCloseButton": true,
"title": "Cant start a call",
},
],
],
"results": [
{
"type": "return",
"value": undefined,
},
],
}
`;

View file

@ -71,18 +71,5 @@ describe("MessageEventPreview", () => {
});
expect(preview.getTextFor(event)).toBe(`${userId}: test new content body`);
});
it("when called with a broadcast chunk event it should return null", () => {
const event = mkEvent({
event: true,
content: {
body: "test body",
["io.element.voice_broadcast_chunk"]: {},
},
user: userId,
type: "m.room.message",
});
expect(preview.getTextFor(event)).toBeNull();
});
});
});

View file

@ -1,54 +0,0 @@
/*
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 { Room } from "matrix-js-sdk/src/matrix";
import { VoiceBroadcastPreview } from "../../../../../src/stores/room-list/previews/VoiceBroadcastPreview";
import { VoiceBroadcastInfoState } from "../../../../../src/voice-broadcast";
import { mkEvent, stubClient } from "../../../../test-utils";
import { mkVoiceBroadcastInfoStateEvent } from "../../../voice-broadcast/utils/test-utils";
describe("VoiceBroadcastPreview.getTextFor", () => {
const roomId = "!room:example.com";
const userId = "@user:example.com";
const deviceId = "d42";
let preview: VoiceBroadcastPreview;
beforeAll(() => {
preview = new VoiceBroadcastPreview();
});
it("when passing an event with empty content, it should return null", () => {
const event = mkEvent({
event: true,
content: {},
user: userId,
type: "m.room.message",
});
expect(preview.getTextFor(event)).toBeNull();
});
it("when passing a broadcast started event, it should return null", () => {
const event = mkVoiceBroadcastInfoStateEvent(roomId, VoiceBroadcastInfoState.Started, userId, deviceId);
expect(preview.getTextFor(event)).toBeNull();
});
it("when passing a broadcast stopped event, it should return the expected text", () => {
const event = mkVoiceBroadcastInfoStateEvent(roomId, VoiceBroadcastInfoState.Stopped, userId, deviceId);
expect(preview.getTextFor(event)).toBe("@user:example.com ended a voice broadcast");
});
it("when passing a redacted broadcast stopped event, it should return null", () => {
const event = mkVoiceBroadcastInfoStateEvent(roomId, VoiceBroadcastInfoState.Stopped, userId, deviceId);
event.makeRedacted(
mkEvent({ event: true, content: {}, user: userId, type: "m.room.redaction" }),
new Room(roomId, stubClient(), userId),
);
expect(preview.getTextFor(event)).toBeNull();
});
});

View file

@ -22,9 +22,6 @@ import { waitFor } from "jest-matrix-react";
import { stubClient, mkRoom, mkEvent } from "../../../test-utils";
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
import { StopGapWidget } from "../../../../src/stores/widgets/StopGapWidget";
import { ElementWidgetActions } from "../../../../src/stores/widgets/ElementWidgetActions";
import { VoiceBroadcastInfoEventType, VoiceBroadcastRecording } from "../../../../src/voice-broadcast";
import { SdkContextClass } from "../../../../src/contexts/SDKContext";
import ActiveWidgetStore from "../../../../src/stores/ActiveWidgetStore";
import SettingsStore from "../../../../src/settings/SettingsStore";
@ -225,41 +222,6 @@ describe("StopGapWidget", () => {
expect(messaging.feedEvent).toHaveBeenLastCalledWith(event.getEffectiveEvent(), "!1:example.org");
});
});
describe("when there is a voice broadcast recording", () => {
let voiceBroadcastInfoEvent: MatrixEvent;
let voiceBroadcastRecording: VoiceBroadcastRecording;
beforeEach(() => {
voiceBroadcastInfoEvent = mkEvent({
event: true,
room: client.getRoom("x")?.roomId,
user: client.getUserId()!,
type: VoiceBroadcastInfoEventType,
content: {},
});
voiceBroadcastRecording = new VoiceBroadcastRecording(voiceBroadcastInfoEvent, client);
jest.spyOn(voiceBroadcastRecording, "pause");
jest.spyOn(SdkContextClass.instance.voiceBroadcastRecordingsStore, "getCurrent").mockReturnValue(
voiceBroadcastRecording,
);
});
describe(`and receiving a action:${ElementWidgetActions.JoinCall} message`, () => {
beforeEach(async () => {
messaging.on.mock.calls.find(([event, listener]) => {
if (event === `action:${ElementWidgetActions.JoinCall}`) {
listener();
return true;
}
});
});
it("should pause the current voice broadcast recording", () => {
expect(voiceBroadcastRecording.pause).toHaveBeenCalled();
});
});
});
});
describe("StopGapWidget with stickyPromise", () => {
let client: MockedObject<MatrixClient>;

View file

@ -1,46 +0,0 @@
/*
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 { getEventDisplayInfo } from "../../../src/utils/EventRenderingUtils";
import { VoiceBroadcastInfoState } from "../../../src/voice-broadcast";
import { mkVoiceBroadcastInfoStateEvent } from "../voice-broadcast/utils/test-utils";
import { createTestClient } from "../../test-utils";
describe("getEventDisplayInfo", () => {
const mkBroadcastInfoEvent = (state: VoiceBroadcastInfoState) => {
return mkVoiceBroadcastInfoStateEvent("!room:example.com", state, "@user:example.com", "ASD123");
};
it("should return the expected value for a broadcast started event", () => {
expect(getEventDisplayInfo(createTestClient(), mkBroadcastInfoEvent(VoiceBroadcastInfoState.Started), false))
.toMatchInlineSnapshot(`
{
"hasRenderer": true,
"isBubbleMessage": false,
"isInfoMessage": false,
"isLeftAlignedBubbleMessage": false,
"isSeeingThroughMessageHiddenForModeration": false,
"noBubbleEvent": true,
}
`);
});
it("should return the expected value for a broadcast stopped event", () => {
expect(getEventDisplayInfo(createTestClient(), mkBroadcastInfoEvent(VoiceBroadcastInfoState.Stopped), false))
.toMatchInlineSnapshot(`
{
"hasRenderer": true,
"isBubbleMessage": false,
"isInfoMessage": true,
"isLeftAlignedBubbleMessage": false,
"isSeeingThroughMessageHiddenForModeration": false,
"noBubbleEvent": true,
}
`);
});
});

View file

@ -35,8 +35,6 @@ import {
import { getMockClientWithEventEmitter, makeBeaconInfoEvent, makePollStartEvent, stubClient } from "../../test-utils";
import dis from "../../../src/dispatcher/dispatcher";
import { Action } from "../../../src/dispatcher/actions";
import { mkVoiceBroadcastInfoStateEvent } from "../voice-broadcast/utils/test-utils";
import { VoiceBroadcastInfoState } from "../../../src/voice-broadcast/types";
jest.mock("../../../src/dispatcher/dispatcher");
@ -148,20 +146,6 @@ describe("EventUtils", () => {
},
});
const voiceBroadcastStart = mkVoiceBroadcastInfoStateEvent(
"!room:example.com",
VoiceBroadcastInfoState.Started,
"@user:example.com",
"ABC123",
);
const voiceBroadcastStop = mkVoiceBroadcastInfoStateEvent(
"!room:example.com",
VoiceBroadcastInfoState.Stopped,
"@user:example.com",
"ABC123",
);
describe("isContentActionable()", () => {
type TestCase = [string, MatrixEvent];
it.each<TestCase>([
@ -172,7 +156,6 @@ describe("EventUtils", () => {
["room member event", roomMemberEvent],
["event without msgtype", noMsgType],
["event without content body property", noContentBody],
["broadcast stop event", voiceBroadcastStop],
])("returns false for %s", (_description, event) => {
expect(isContentActionable(event)).toBe(false);
});
@ -183,7 +166,6 @@ describe("EventUtils", () => {
["event with empty content body", emptyContentBody],
["event with a content body", niceTextMessage],
["beacon_info event", beaconInfoEvent],
["broadcast start event", voiceBroadcastStart],
])("returns true for %s", (_description, event) => {
expect(isContentActionable(event)).toBe(true);
});

View file

@ -1,251 +0,0 @@
/*
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)
});
});
});
});
});

View file

@ -1,171 +0,0 @@
/*
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, 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, 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, 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");
});
});
});

View file

@ -1,24 +0,0 @@
/*
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();
});
});

View file

@ -1,44 +0,0 @@
/*
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();
});
});
});
});

View file

@ -1,89 +0,0 @@
/*
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();
});
});
});

View file

@ -1,51 +0,0 @@
/*
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();
});
});
});

View file

@ -1,27 +0,0 @@
// 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>
`;

View file

@ -1,16 +0,0 @@
// 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>
`;

View file

@ -1,277 +0,0 @@
// 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>
`;

View file

@ -1,97 +0,0 @@
// 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>
`;

View file

@ -1,249 +0,0 @@
/*
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()})`);
});
});

View file

@ -1,163 +0,0 @@
/*
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 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();
});
});
});
});
});

View file

@ -1,79 +0,0 @@
/*
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()})`);
});
});

View file

@ -1,217 +0,0 @@
/*
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 { 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 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"));
await screen.findByText("Stop live broadcasting?");
// modal rendering has some weird sleeps
await sleep(200);
});
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);
});
});
});
});

View file

@ -1,129 +0,0 @@
/*
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();
});
},
);
});

View file

@ -1,914 +0,0 @@
// 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>
`;

View file

@ -1,98 +0,0 @@
// 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>
`;

View file

@ -1,131 +0,0 @@
// 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>
`;

View file

@ -1,238 +0,0 @@
// 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>
`;

View file

@ -1,558 +0,0 @@
// 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>
`;

View file

@ -1,747 +0,0 @@
/*
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);
if (fakeTimers) {
await flushPromisesWithFakeTimers();
} else {
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], { addToState: true });
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], { addToState: true });
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], { addToState: true });
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], { addToState: true });
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], { addToState: true });
room.relations.aggregateChildEvent(stoppedEvent);
chunk2Playback.emit(PlaybackState.Stopped);
});
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Buffering);
describe("and the next chunk arrives", () => {
beforeEach(() => {
room.addLiveEvents([chunk3Event], { addToState: true });
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], { addToState: true });
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);
});
});
});
});

View file

@ -1,68 +0,0 @@
/*
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);
});
});
});

View file

@ -1,660 +0,0 @@
/*
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: () => Promise<void>) => {
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: () => Promise<void>) => {
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: () => Promise<void>) => {
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);
});
});
});

View file

@ -1,162 +0,0 @@
/*
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);
});
});
});
});

View file

@ -1,130 +0,0 @@
/*
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);
});
});
});
});

View file

@ -1,167 +0,0 @@
/*
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);
});
});
});
});

View file

@ -1,142 +0,0 @@
/*
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);
});
});
});
});

View file

@ -1,179 +0,0 @@
/*
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();
});
});
});
});

View file

@ -1,24 +0,0 @@
// 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,
},
],
}
`;

View file

@ -1,116 +0,0 @@
// 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,
},
],
}
`;

View file

@ -1,42 +0,0 @@
// 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>
`;

View file

@ -1,51 +0,0 @@
/*
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();
});
});

View file

@ -1,33 +0,0 @@
/*
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");
});
});

View file

@ -1,116 +0,0 @@
/*
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,
);
});
});
});

View file

@ -1,63 +0,0 @@
/*
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);
});
});
});

View file

@ -1,43 +0,0 @@
/*
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);
});
});
});

View file

@ -1,195 +0,0 @@
/*
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], { addToState: true });
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();
});
});

View file

@ -1,109 +0,0 @@
/*
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], { addToState: true });
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], { addToState: true });
});
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);
});
});

View file

@ -1,98 +0,0 @@
/*
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();
});
});
});

View file

@ -1,81 +0,0 @@
/*
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], { addToState: true });
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());
});
});

View file

@ -1,123 +0,0 @@
/*
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);
});
});
});
});

View file

@ -1,72 +0,0 @@
/*
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);
});
});

View file

@ -1,138 +0,0 @@
/*
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();
},
);
});

View file

@ -1,234 +0,0 @@
/*
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();
});
});
});

View file

@ -1,131 +0,0 @@
/*
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,
);
};

View file

@ -1,90 +0,0 @@
/*
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
});
});
});
});
});

View file

@ -1,47 +0,0 @@
/*
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`);
});
});