/* Copyright 2023 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ import React from "react"; import { TooltipProvider } from "@vector-im/compound-web"; import { fireEvent, getByLabelText, getByText, render, screen, waitFor } from "@testing-library/react"; import { EventTimeline, JoinRule, Room } from "matrix-js-sdk/src/matrix"; import { KnownMembership } from "matrix-js-sdk/src/types"; import { SDKContext, SdkContextClass } from "../../../../../src/contexts/SDKContext"; import { getMockClientWithEventEmitter, mockClientMethodsUser } from "../../../../test-utils"; import { CallGuestLinkButton, JoinRuleDialog, } from "../../../../../src/components/views/rooms/RoomHeader/CallGuestLinkButton"; import Modal from "../../../../../src/Modal"; import SdkConfig from "../../../../../src/SdkConfig"; import ShareDialog from "../../../../../src/components/views/dialogs/ShareDialog"; import { _t } from "../../../../../src/languageHandler"; import SettingsStore from "../../../../../src/settings/SettingsStore"; describe("", () => { const roomId = "!room:server.org"; let sdkContext!: SdkContextClass; let modalSpy: jest.SpyInstance; let modalResolve: (value: unknown[] | PromiseLike) => void; let room: Room; const targetUnencrypted = "https://guest_spa_url.com/room/#/!room:server.org?roomId=%21room%3Aserver.org&viaServers=example.org"; const targetEncrypted = "https://guest_spa_url.com/room/#/!room:server.org?roomId=%21room%3Aserver.org&perParticipantE2EE=true&viaServers=example.org"; const expectedShareDialogProps = { target: targetEncrypted, customTitle: "Conference invite link", subtitle: "Link for external users to join the call without a matrix account:", }; /** * Create a room using mocked client * And mock isElementVideoRoom */ const makeRoom = (isVideoRoom = true): Room => { const room = new Room(roomId, sdkContext.client!, sdkContext.client!.getSafeUserId()); jest.spyOn(room, "isElementVideoRoom").mockReturnValue(isVideoRoom); // stub jest.spyOn(room, "getPendingEvents").mockReturnValue([]); return room; }; function mockRoomMembers(room: Room, count: number) { const members = Array(count) .fill(0) .map((_, index) => ({ userId: `@user-${index}:example.org`, roomId: room.roomId, membership: KnownMembership.Join, })); room.currentState.setJoinedMemberCount(members.length); room.getJoinedMembers = jest.fn().mockReturnValue(members); } const getComponent = (room: Room) => render(, { wrapper: ({ children }) => ( {children} ), }); const oldGet = SdkConfig.get; beforeEach(() => { const client = getMockClientWithEventEmitter({ ...mockClientMethodsUser(), sendStateEvent: jest.fn(), }); sdkContext = new SdkContextClass(); sdkContext.client = client; const modalPromise = new Promise((resolve) => { modalResolve = resolve; }); modalSpy = jest.spyOn(Modal, "createDialog").mockReturnValue({ finished: modalPromise, close: jest.fn() }); room = makeRoom(); mockRoomMembers(room, 3); jest.spyOn(SdkConfig, "get").mockImplementation((key) => { if (key === "element_call") { return { guest_spa_url: "https://guest_spa_url.com", url: "https://spa_url.com" }; } return oldGet(key); }); jest.spyOn(room, "hasEncryptionStateEvent").mockReturnValue(true); jest.spyOn(SdkContextClass.instance.roomViewStore, "isViewingCall").mockReturnValue(true); }); afterEach(() => { jest.restoreAllMocks(); }); it("shows the JoinRuleDialog on click with private join rules", async () => { getComponent(room); fireEvent.click(screen.getByLabelText("Share call link")); expect(modalSpy).toHaveBeenCalledWith(JoinRuleDialog, { room, canInvite: false }); // pretend public was selected jest.spyOn(room, "getJoinRule").mockReturnValue(JoinRule.Public); modalResolve([]); await new Promise(process.nextTick); const callParams = modalSpy.mock.calls[1]; expect(callParams[0]).toEqual(ShareDialog); expect(callParams[1].target.toString()).toEqual(expectedShareDialogProps.target); expect(callParams[1].subtitle).toEqual(expectedShareDialogProps.subtitle); expect(callParams[1].customTitle).toEqual(expectedShareDialogProps.customTitle); }); it("shows the ShareDialog on click with public join rules", () => { jest.spyOn(room, "getJoinRule").mockReturnValue(JoinRule.Public); getComponent(room); fireEvent.click(screen.getByLabelText("Share call link")); const callParams = modalSpy.mock.calls[0]; expect(callParams[0]).toEqual(ShareDialog); expect(callParams[1].target.toString()).toEqual(expectedShareDialogProps.target); expect(callParams[1].subtitle).toEqual(expectedShareDialogProps.subtitle); expect(callParams[1].customTitle).toEqual(expectedShareDialogProps.customTitle); }); it("shows the ShareDialog on click with knock join rules", () => { jest.spyOn(room, "getJoinRule").mockReturnValue(JoinRule.Knock); jest.spyOn(room, "canInvite").mockReturnValue(true); getComponent(room); fireEvent.click(screen.getByLabelText("Share call link")); const callParams = modalSpy.mock.calls[0]; expect(callParams[0]).toEqual(ShareDialog); expect(callParams[1].target.toString()).toEqual(expectedShareDialogProps.target); expect(callParams[1].subtitle).toEqual(expectedShareDialogProps.subtitle); expect(callParams[1].customTitle).toEqual(expectedShareDialogProps.customTitle); }); it("don't show external conference button if room not public nor knock and the user cannot change join rules", () => { // preparation for if we refactor the related code to not use currentState. jest.spyOn(room, "getLiveTimeline").mockReturnValue({ getState: jest.fn().mockReturnValue({ maySendStateEvent: jest.fn().mockReturnValue(false), }), } as unknown as EventTimeline); jest.spyOn(room.currentState, "maySendStateEvent").mockReturnValue(false); getComponent(room); expect(screen.queryByLabelText("Share call link")).not.toBeInTheDocument(); }); it("don't show external conference button if now guest spa link is configured", () => { jest.spyOn(room, "getJoinRule").mockReturnValue(JoinRule.Public); jest.spyOn(SdkContextClass.instance.roomViewStore, "isViewingCall").mockReturnValue(true); jest.spyOn(SdkConfig, "get").mockImplementation((key) => { if (key === "element_call") { return { url: "https://example2.com" }; } return oldGet(key); }); getComponent(room); // We only change the SdkConfig and show that this everything else is // configured so that the call link button is shown. expect(screen.queryByLabelText("Share call link")).not.toBeInTheDocument(); jest.spyOn(SdkConfig, "get").mockImplementation((key) => { if (key === "element_call") { return { guest_spa_url: "https://guest_spa_url.com", url: "https://example2.com" }; } return oldGet(key); }); const { container } = getComponent(room); expect(getByLabelText(container, "Share call link")).toBeInTheDocument(); }); it("opens the share dialog with the correct share link in an encrypted room", () => { jest.spyOn(room, "getJoinRule").mockReturnValue(JoinRule.Public); jest.spyOn(SdkContextClass.instance.roomViewStore, "isViewingCall").mockReturnValue(true); const { container } = getComponent(room); const modalSpy = jest.spyOn(Modal, "createDialog"); fireEvent.click(getByLabelText(container, _t("voip|get_call_link"))); // const target = // "https://guest_spa_url.com/room/#/!room:server.org?roomId=%21room%3Aserver.org&perParticipantE2EE=true&viaServers=example.org"; expect(modalSpy).toHaveBeenCalled(); const arg0 = modalSpy.mock.calls[0][0]; const arg1 = modalSpy.mock.calls[0][1] as any; expect(arg0).toEqual(ShareDialog); const { customTitle, subtitle } = arg1; expect({ customTitle, subtitle }).toEqual({ customTitle: "Conference invite link", subtitle: _t("share|share_call_subtitle"), }); expect(arg1.target.toString()).toEqual(targetEncrypted); }); it("share dialog has correct link in an unencrypted room", () => { jest.spyOn(room, "getJoinRule").mockReturnValue(JoinRule.Public); jest.spyOn(room, "hasEncryptionStateEvent").mockReturnValue(false); jest.spyOn(SdkContextClass.instance.roomViewStore, "isViewingCall").mockReturnValue(true); const { container } = getComponent(room); const modalSpy = jest.spyOn(Modal, "createDialog"); fireEvent.click(getByLabelText(container, _t("voip|get_call_link"))); const arg1 = modalSpy.mock.calls[0][1] as any; expect(arg1.target.toString()).toEqual(targetUnencrypted); }); describe("", () => { const onFinished = jest.fn(); const getComponent = (room: Room, canInvite: boolean = true) => render(, { wrapper: ({ children }) => ( {children} ), }); beforeEach(() => { // feature_ask_to_join enabled jest.spyOn(SettingsStore, "getValue").mockReturnValue(true); }); it("shows ask to join if feature is enabled", () => { const { container } = getComponent(room); expect(getByText(container, "Ask to join")).toBeInTheDocument(); }); it("font show ask to join if feature is enabled but cannot invite", () => { getComponent(room, false); expect(screen.queryByText("Ask to join")).not.toBeInTheDocument(); }); it("doesn't show ask to join if feature is disabled", () => { jest.spyOn(SettingsStore, "getValue").mockReturnValue(false); getComponent(room); expect(screen.queryByText("Ask to join")).not.toBeInTheDocument(); }); it("sends correct state event on click", async () => { const sendStateSpy = jest.spyOn(sdkContext.client!, "sendStateEvent"); let container; container = getComponent(room).container; fireEvent.click(getByText(container, "Ask to join")); expect(sendStateSpy).toHaveBeenCalledWith( "!room:server.org", "m.room.join_rules", { join_rule: "knock" }, "", ); expect(sendStateSpy).toHaveBeenCalledTimes(1); await waitFor(() => expect(onFinished).toHaveBeenCalledTimes(1)); onFinished.mockClear(); sendStateSpy.mockClear(); container = getComponent(room).container; fireEvent.click(getByText(container, "Public")); expect(sendStateSpy).toHaveBeenLastCalledWith( "!room:server.org", "m.room.join_rules", { join_rule: "public" }, "", ); expect(sendStateSpy).toHaveBeenCalledTimes(1); container = getComponent(room).container; await waitFor(() => expect(onFinished).toHaveBeenCalledTimes(1)); onFinished.mockClear(); sendStateSpy.mockClear(); fireEvent.click(getByText(container, _t("update_room_access_modal|no_change"))); await waitFor(() => expect(onFinished).toHaveBeenCalledTimes(1)); // Don't call sendStateEvent if no change is clicked. expect(sendStateSpy).toHaveBeenCalledTimes(0); }); }); });