Merge matrix-react-sdk into element-web

Merge remote-tracking branch 'repomerge/t3chguy/repomerge' into t3chguy/repo-merge

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
Michael Telatynski
2024-10-15 15:32:09 +01:00
3265 changed files with 484599 additions and 699 deletions
@@ -0,0 +1,169 @@
/*
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 { zip } from "lodash";
import { render, screen, act, fireEvent, waitFor, cleanup } from "jest-matrix-react";
import { mocked, Mocked } from "jest-mock";
import { MatrixClient, PendingEventOrdering, Room, RoomStateEvent } from "matrix-js-sdk/src/matrix";
import { Widget } from "matrix-widget-api";
import type { RoomMember } from "matrix-js-sdk/src/matrix";
import type { ClientWidgetApi } from "matrix-widget-api";
import {
stubClient,
mkRoomMember,
wrapInMatrixClientContext,
useMockedCalls,
MockedCall,
setupAsyncStoreWithClient,
useMockMediaDevices,
} from "../../../../test-utils";
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
import { CallView as _CallView } from "../../../../../src/components/views/voip/CallView";
import { WidgetMessagingStore } from "../../../../../src/stores/widgets/WidgetMessagingStore";
import { CallStore } from "../../../../../src/stores/CallStore";
import { Call, ConnectionState } from "../../../../../src/models/Call";
const CallView = wrapInMatrixClientContext(_CallView);
describe("CallView", () => {
useMockedCalls();
jest.spyOn(HTMLMediaElement.prototype, "play").mockImplementation(async () => {});
let client: Mocked<MatrixClient>;
let room: Room;
let alice: RoomMember;
beforeEach(() => {
useMockMediaDevices();
stubClient();
client = mocked(MatrixClientPeg.safeGet());
room = new Room("!1:example.org", client, "@alice:example.org", {
pendingEventOrdering: PendingEventOrdering.Detached,
});
alice = mkRoomMember(room.roomId, "@alice:example.org");
jest.spyOn(room, "getMember").mockImplementation((userId) => (userId === alice.userId ? alice : null));
client.getRoom.mockImplementation((roomId) => (roomId === room.roomId ? room : null));
client.getRooms.mockReturnValue([room]);
client.reEmitter.reEmit(room, [RoomStateEvent.Events]);
setupAsyncStoreWithClient(CallStore.instance, client);
setupAsyncStoreWithClient(WidgetMessagingStore.instance, client);
});
afterEach(() => {
client.reEmitter.stopReEmitting(room, [RoomStateEvent.Events]);
});
const renderView = async (skipLobby = false): Promise<void> => {
render(<CallView room={room} resizing={false} waitForCall={false} skipLobby={skipLobby} />);
await act(() => Promise.resolve()); // Let effects settle
};
describe("with an existing call", () => {
let call: MockedCall;
let widget: Widget;
beforeEach(() => {
MockedCall.create(room, "1");
const maybeCall = CallStore.instance.getCall(room.roomId);
if (!(maybeCall instanceof MockedCall)) throw new Error("Failed to create call");
call = maybeCall;
widget = new Widget(call.widget);
WidgetMessagingStore.instance.storeMessaging(widget, room.roomId, {
stop: () => {},
} as unknown as ClientWidgetApi);
});
afterEach(() => {
cleanup(); // Unmount before we do any cleanup that might update the component
call.destroy();
WidgetMessagingStore.instance.stopMessaging(widget, room.roomId);
});
it("calls clean on mount", async () => {
const cleanSpy = jest.spyOn(call, "clean");
await renderView();
expect(cleanSpy).toHaveBeenCalled();
});
/**
* TODO: Fix I do not understand this test
*/
it.skip("tracks participants", async () => {
const bob = mkRoomMember(room.roomId, "@bob:example.org");
const carol = mkRoomMember(room.roomId, "@carol:example.org");
const expectAvatars = (userIds: string[]) => {
const avatars = screen.queryAllByRole("button", { name: "Profile picture" });
expect(userIds.length).toBe(avatars.length);
for (const [userId, avatar] of zip(userIds, avatars)) {
fireEvent.focus(avatar!);
screen.getAllByRole("tooltip", { name: userId });
}
};
await renderView();
expect(screen.queryByLabelText(/joined/)).toBe(null);
expectAvatars([]);
act(() => {
call.participants = new Map([[alice, new Set(["a"])]]);
});
screen.getByText("1 person joined");
expectAvatars([alice.userId]);
act(() => {
call.participants = new Map([
[alice, new Set(["a"])],
[bob, new Set(["b1", "b2"])],
[carol, new Set(["c"])],
]);
});
screen.getByText("4 people joined");
expectAvatars([alice.userId, bob.userId, bob.userId, carol.userId]);
act(() => {
call.participants = new Map();
});
expect(screen.queryByLabelText(/joined/)).toBe(null);
expectAvatars([]);
});
it("automatically connects to the call when skipLobby is true", async () => {
const connectSpy = jest.spyOn(call, "start");
await renderView(true);
await waitFor(() => expect(connectSpy).toHaveBeenCalled(), { interval: 1 });
});
});
describe("without an existing call", () => {
it("creates and connects to a new call when the join button is pressed", async () => {
expect(Call.get(room)).toBeNull();
await renderView(true);
await waitFor(() => expect(CallStore.instance.getCall(room.roomId)).not.toBeNull());
const call = CallStore.instance.getCall(room.roomId)!;
const widget = new Widget(call.widget);
WidgetMessagingStore.instance.storeMessaging(widget, room.roomId, {
stop: () => {},
} as unknown as ClientWidgetApi);
await waitFor(() => expect(call.connectionState).toBe(ConnectionState.Connected));
cleanup(); // Unmount before we do any cleanup that might update the component
call.destroy();
WidgetMessagingStore.instance.stopMessaging(widget, room.roomId);
});
});
});
@@ -0,0 +1,56 @@
/*
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 React from "react";
import { render, screen } from "jest-matrix-react";
import userEvent from "@testing-library/user-event";
import DialPad, { BUTTONS, BUTTON_LETTERS } from "../../../../../src/components/views/voip/DialPad";
it("when hasDial is true, displays all expected numbers and letters", () => {
render(<DialPad onDigitPress={jest.fn()} hasDial={true} onDialPress={jest.fn()} />);
// check that we have the expected number of buttons + 1 for the dial button
expect(screen.getAllByRole("button")).toHaveLength(BUTTONS.length + 1);
// BUTTONS represents the numbers and symbols
BUTTONS.forEach((button) => {
expect(screen.getByText(button)).toBeInTheDocument();
});
// BUTTON_LETTERS represents the `ABC` type strings you see on the keypad, but also contains
// some empty strings, so we filter them out prior to tests
BUTTON_LETTERS.filter(Boolean).forEach((letterSet) => {
expect(screen.getByText(letterSet)).toBeInTheDocument();
});
// check for the dial button
expect(screen.getByRole("button", { name: "Dial" })).toBeInTheDocument();
});
it("clicking a digit button calls the correct function", async () => {
const mockOnDigitPress = jest.fn();
render(<DialPad onDigitPress={mockOnDigitPress} hasDial={true} onDialPress={jest.fn()} />);
// click the `1` button
const buttonText = "1";
await userEvent.click(screen.getByText(buttonText, { exact: false }));
expect(mockOnDigitPress).toHaveBeenCalledTimes(1);
expect(mockOnDigitPress.mock.calls[0][0]).toBe(buttonText);
});
it("clicking the dial button calls the correct function", async () => {
const mockOnDial = jest.fn();
render(<DialPad onDigitPress={jest.fn()} hasDial={true} onDialPress={mockOnDial} />);
// click the `1` button
const buttonText = "Dial";
await userEvent.click(screen.getByRole("button", { name: buttonText }));
expect(mockOnDial).toHaveBeenCalledTimes(1);
expect(mockOnDial).toHaveBeenCalledWith(); // represents no arguments in the call
});
@@ -0,0 +1,57 @@
/*
* Copyright 2024 New Vector Ltd.
* Copyright 2024 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 { MatrixCall } from "matrix-js-sdk/src/webrtc/call";
import LegacyCallViewButtons from "../../../../../../src/components/views/voip/LegacyCallView/LegacyCallViewButtons";
import { createTestClient } from "../../../../../test-utils";
describe("LegacyCallViewButtons", () => {
const matrixClient = createTestClient();
const roomId = "test-room-id";
const renderButtons = () => {
const call = new MatrixCall({
client: matrixClient,
roomId,
});
return render(
<LegacyCallViewButtons
call={call}
handlers={{
onScreenshareClick: jest.fn(),
onToggleSidebarClick: jest.fn(),
onHangupClick: jest.fn(),
onMicMuteClick: jest.fn(),
onVidMuteClick: jest.fn(),
}}
buttonsVisibility={{
vidMute: true,
screensharing: true,
sidebar: true,
contextMenu: true,
dialpad: true,
}}
buttonsState={{
micMuted: false,
vidMuted: false,
sidebarShown: false,
screensharing: false,
}}
/>,
);
};
it("should render the buttons", () => {
const { container } = renderButtons();
expect(container).toMatchSnapshot();
});
});
@@ -0,0 +1,68 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`LegacyCallViewButtons should render the buttons 1`] = `
<div>
<div
class="mx_LegacyCallViewButtons"
>
<div
aria-expanded="false"
aria-haspopup="true"
aria-label="Dialpad"
class="mx_AccessibleButton mx_LegacyCallViewButtons_button mx_LegacyCallViewButtons_dialpad"
role="button"
tabindex="0"
/>
<div
aria-label="Mute microphone"
class="mx_AccessibleButton mx_LegacyCallViewButtons_button mx_LegacyCallViewButtons_button_mic mx_LegacyCallViewButtons_button_on"
role="button"
tabindex="0"
>
<div
class="mx_AccessibleButton mx_LegacyCallViewButtons_button mx_LegacyCallViewButtons_button mx_LegacyCallViewButtons_dropdownButton mx_LegacyCallViewButtons_dropdownButton_collapsed mx_LegacyCallViewButtons_button_on"
role="button"
tabindex="0"
/>
</div>
<div
aria-label="Turn off camera"
class="mx_AccessibleButton mx_LegacyCallViewButtons_button mx_LegacyCallViewButtons_button_vid mx_LegacyCallViewButtons_button_on"
role="button"
tabindex="0"
>
<div
class="mx_AccessibleButton mx_LegacyCallViewButtons_button mx_LegacyCallViewButtons_button mx_LegacyCallViewButtons_dropdownButton mx_LegacyCallViewButtons_dropdownButton_collapsed mx_LegacyCallViewButtons_button_on"
role="button"
tabindex="0"
/>
</div>
<div
aria-label="Start sharing your screen"
class="mx_AccessibleButton mx_LegacyCallViewButtons_button mx_LegacyCallViewButtons_button_screensharing mx_LegacyCallViewButtons_button_off"
role="button"
tabindex="0"
/>
<div
aria-label="Show sidebar"
class="mx_AccessibleButton mx_LegacyCallViewButtons_button mx_LegacyCallViewButtons_button_sidebar mx_LegacyCallViewButtons_button_off"
role="button"
tabindex="0"
/>
<div
aria-expanded="false"
aria-haspopup="true"
aria-label="More"
class="mx_AccessibleButton mx_LegacyCallViewButtons_button mx_LegacyCallViewButtons_button_more"
role="button"
tabindex="0"
/>
<div
aria-label="Hangup"
class="mx_AccessibleButton mx_LegacyCallViewButtons_button mx_LegacyCallViewButtons_button_hangup"
role="button"
tabindex="0"
/>
</div>
</div>
`;
@@ -0,0 +1,60 @@
/*
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 React from "react";
import { render, screen } from "jest-matrix-react";
import { CallFeed } from "matrix-js-sdk/src/webrtc/callFeed";
import { MatrixCall } from "matrix-js-sdk/src/webrtc/call";
import { MatrixClient, Room } from "matrix-js-sdk/src/matrix";
import * as AvatarModule from "../../../../../src/Avatar";
import VideoFeed from "../../../../../src/components/views/voip/VideoFeed";
import { stubClient, useMockedCalls } from "../../../../test-utils";
import LegacyCallHandler from "../../../../../src/LegacyCallHandler";
import DMRoomMap from "../../../../../src/utils/DMRoomMap";
const FAKE_AVATAR_URL = "http://fakeurl.dummy/fake.png";
describe("VideoFeed", () => {
useMockedCalls();
let client: MatrixClient;
beforeAll(() => {
client = stubClient();
(AvatarModule as any).avatarUrlForRoom = jest.fn().mockReturnValue(FAKE_AVATAR_URL);
const dmRoomMap = new DMRoomMap(client);
jest.spyOn(dmRoomMap, "getUserIdForRoomId");
jest.spyOn(DMRoomMap, "shared").mockReturnValue(dmRoomMap);
});
afterAll(() => {
jest.restoreAllMocks();
});
it("Displays the room avatar when no video is available", () => {
window.mxLegacyCallHandler = {
roomIdForCall: jest.fn().mockReturnValue("!this:room.here"),
} as unknown as LegacyCallHandler;
const mockCall = {
room: new Room("!room:example.com", client, client.getSafeUserId()),
};
const feed = {
isAudioMuted: jest.fn().mockReturnValue(false),
isVideoMuted: jest.fn().mockReturnValue(true),
addListener: jest.fn(),
removeListener: jest.fn(),
};
render(<VideoFeed feed={feed as unknown as CallFeed} call={mockCall as unknown as MatrixCall} />);
const avatarImg = screen.getByRole("img");
expect(avatarImg).toHaveAttribute("src", FAKE_AVATAR_URL);
});
});