/* Copyright 2025 New Vector Ltd. SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial Please see LICENSE in the repository root for full details. */ import { afterEach, describe, expect, it, type Mock, vi } from "vitest"; import { render, waitFor, screen } from "@testing-library/react"; import userEvent, { type UserEvent } from "@testing-library/user-event"; import { TooltipProvider } from "@vector-im/compound-web"; import type { MatrixClient } from "matrix-js-sdk"; import type { Room as LivekitRoom } from "livekit-client"; import { DeveloperSettingsTab, type DeveloperSettingsSnapshot, } from "./DeveloperSettingsTab"; import { outOfCallDeveloperSettingsTabViewModel } from "./DeveloperSettingsTabViewModel"; import { createStaticViewModel } from "../state/ViewModel"; import { customTransport as customTransportSetting, enableExtendedLivekitLogs as enableExtendedLivekitLogsSetting, matrixRTCMode as matrixRTCModeSetting, } from "./settings"; import { MatrixRTCMode } from "../config/ConfigOptions"; import { exampleTransport, mockConfig } from "../utils/test"; import { getSFUConfig } from "../livekit/auth"; // Mock url params hook to avoid environment-dependent snapshot churn. vi.mock("../UrlParams", () => ({ useUrlParams: (): { mocked: boolean; answer: number } => ({ mocked: true, answer: 42, }), })); // IMPORTANT: mock the same specifier used by DeveloperSettingsTab vi.mock("../livekit/auth", () => ({ getSFUConfig: vi.fn().mockResolvedValue({ url: "mock-url", jwt: "mock-jwt", }), })); // Provide a minimal mock of a Livekit Room structure used by the component. function createMockLivekitRoom( wsUrl: string, serverInfo: object, metadata: string, ): { isLocal: boolean; url: string; room: LivekitRoom; livekitAlias: string } { const mockRoom = { serverInfo, metadata, engine: { client: { ws: { url: wsUrl } } }, localParticipant: { identity: "localParticipantIdentity" }, remoteParticipants: new Map(), } as unknown as LivekitRoom; return { isLocal: true, url: wsUrl, room: mockRoom, livekitAlias: "TestAlias", }; } // Minimal MatrixClient mock with only the methods used by the component. function createMockMatrixClient(): MatrixClient { return { doesServerSupportUnstableFeature: vi.fn().mockResolvedValue(true), // ensure stickyEventsSupported eventually becomes true getCrypto: (): { getVersion: () => string } | undefined => ({ getVersion: () => "crypto-1.0.0", }), getUserId: () => "@alice:example.org", getDeviceId: () => "DEVICE123", } as unknown as MatrixClient; } /** * Types the JSON representation of an object into the given element. */ async function typeJson( user: UserEvent, element: Element, object: object, ): Promise { // `user.type` requires us to escape brackets by doubling them await user.type( element, JSON.stringify(object).replace(/([{[\]])/g, (c) => c + c), ); } describe("DeveloperSettingsTab", () => { it("renders and matches snapshot", async () => { const client = createMockMatrixClient(); const livekitRooms: { room: LivekitRoom; url: string; isLocal?: boolean; livekitAlias: string; }[] = [ createMockLivekitRoom( "wss://local-sfu.example.org", { region: "local", version: "1.2.3" }, "local-metadata", ), { isLocal: false, livekitAlias: "TestAlias2", url: "wss://remote-sfu.example.org", room: { localParticipant: { identity: "localParticipantIdentity" }, remoteParticipants: new Map(), serverInfo: { region: "remote", version: "4.5.6" }, metadata: "remote-metadata", engine: { client: { ws: { url: "wss://remote-sfu.example.org" } } }, } as unknown as LivekitRoom, }, ]; const { container } = render( , ); // Wait for the async sticky events feature check to resolve so the final UI // (e.g. enabled Matrix_2_0 radio button) appears deterministically. await waitFor(() => expect(client.doesServerSupportUnstableFeature).toHaveBeenCalled(), ); expect(container).toMatchSnapshot(); }); describe("custom transport", () => { afterEach(() => { customTransportSetting.setValue(null); }); const client = { doesServerSupportUnstableFeature: vi.fn().mockResolvedValue(true), getCrypto: () => ({ getVersion: (): string => "x" }), getUserId: () => "@u:hs", getDeviceId: () => "DEVICE", getDomain: () => "hs", } as unknown as MatrixClient; it("will not update custom transport without roomId", async () => { const user = userEvent.setup(); render( , ); const input = screen.getByLabelText("Custom transport JSON"); await user.clear(input); await typeJson(user, input, exampleTransport); const saveButton = screen.getByRole("button", { name: "Save" }); await user.click(saveButton); expect(getSFUConfig).not.toHaveBeenCalled(); expect(customTransportSetting.getValue()).toBe(null); }); it("will not update custom transport without text in input", async () => { const user = userEvent.setup(); render( , ); const input = screen.getByLabelText("Custom transport JSON"); await user.clear(input); const saveButton = screen.getByRole("button", { name: "Save" }); await user.click(saveButton); expect(getSFUConfig).not.toHaveBeenCalled(); expect(customTransportSetting.getValue()).toBe(null); }); it("will not update custom transport when pressing cancel", async () => { const user = userEvent.setup(); render( , ); const input = screen.getByLabelText("Custom transport JSON"); await user.clear(input); await user.type(input, "wss://example.livekit.invalid"); const cancelButton = screen.getByRole("button", { name: "Reset overwrite", }); await user.click(cancelButton); expect(getSFUConfig).not.toHaveBeenCalled(); expect(customTransportSetting.getValue()).toBe(null); }); it("will update custom transport", async () => { const user = userEvent.setup(); render( , ); const input = screen.getByLabelText("Custom transport JSON"); await user.clear(input); await typeJson(user, input, { type: "livekit", url: "wss://example.livekit.valid", }); const saveButton = screen.getByRole("button", { name: "Save" }); await user.click(saveButton); expect(getSFUConfig).toHaveBeenCalledWith( expect.objectContaining({ transport: { type: "livekit", url: "wss://example.livekit.valid" }, roomId: "#testRoom", }), ); expect(customTransportSetting.getValue()).toEqual({ type: "livekit", url: "wss://example.livekit.valid", }); }); it("will show error on unreachable transport", async () => { const user = userEvent.setup(); render( , ); const input = screen.getByLabelText("Custom transport JSON"); await user.clear(input); await typeJson(user, input, { type: "livekit", url: "wss://example.livekit.valid", }); const saveButton = screen.getByRole("button", { name: "Save" }); (getSFUConfig as Mock).mockImplementation(() => { throw new Error("Failed to get SFU config"); }); await user.click(saveButton); expect( screen.getByText("invalid transport (did not update)"), ).toBeInTheDocument(); expect(customTransportSetting.getValue()).toBe(null); }); }); // Add this test inside the describe("DeveloperSettingsTab", () => { block, // after the custom transport tests: describe("enable extended livekit logs", () => { afterEach(() => { enableExtendedLivekitLogsSetting.setValue(false); }); it("toggles extended livekit logs setting", async () => { const user = userEvent.setup(); const client = createMockMatrixClient(); render( , ); const checkbox = screen.getByLabelText("Enable extended livekit logs"); // Initial state should be unchecked (default false) expect(checkbox).not.toBeChecked(); expect(enableExtendedLivekitLogsSetting.getValue()).toBe(false); // Click to enable await user.click(checkbox); expect(checkbox).toBeChecked(); expect(enableExtendedLivekitLogsSetting.getValue()).toBe(true); // Click to disable await user.click(checkbox); expect(checkbox).not.toBeChecked(); expect(enableExtendedLivekitLogsSetting.getValue()).toBe(false); }); it("Use the current setting value on render", () => { const client = createMockMatrixClient(); // Set the value to true before rendering enableExtendedLivekitLogsSetting.setValue(true); render( , ); const checkbox = screen.getByLabelText("Enable extended livekit logs"); expect(checkbox).toBeChecked(); expect(enableExtendedLivekitLogsSetting.getValue()).toBe(true); }); }); describe("matrix rtc mode", () => { afterEach(() => { matrixRTCModeSetting.setValue(MatrixRTCMode.Compatibility); vi.restoreAllMocks(); }); function getModeRadios(): { compatibility: HTMLInputElement; matrix20: HTMLInputElement; } { return { compatibility: screen.getByDisplayValue( MatrixRTCMode.Compatibility, ) as HTMLInputElement, matrix20: screen.getByDisplayValue( MatrixRTCMode.Matrix_2_0, ) as HTMLInputElement, }; } it("radios reflect the localStorage setting when config does not force the mode", async () => { mockConfig({}); matrixRTCModeSetting.setValue(MatrixRTCMode.Compatibility); const client = createMockMatrixClient(); render( , ); await waitFor(() => expect(client.doesServerSupportUnstableFeature).toHaveBeenCalled(), ); const radios = getModeRadios(); expect(radios.compatibility).toBeChecked(); expect(radios.matrix20).not.toBeChecked(); // None are disabled by config; only Matrix_2_0 may be disabled by sticky-events support. expect(radios.compatibility).not.toBeDisabled(); }); it.each([MatrixRTCMode.Compatibility, MatrixRTCMode.Matrix_2_0])( "disables all radios and shows the config value (%s) as checked when matrix_rtc_mode is set", async (configMode) => { mockConfig({ matrix_rtc_mode: configMode }); // Local setting is intentionally different from the config value to // prove config wins. matrixRTCModeSetting.setValue( configMode === MatrixRTCMode.Compatibility ? MatrixRTCMode.Matrix_2_0 : MatrixRTCMode.Compatibility, ); const client = createMockMatrixClient(); render( , ); await waitFor(() => expect(client.doesServerSupportUnstableFeature).toHaveBeenCalled(), ); const radios = getModeRadios(); expect(radios.compatibility).toBeDisabled(); expect(radios.matrix20).toBeDisabled(); const checkedValue = ( { [MatrixRTCMode.Compatibility]: radios.compatibility, [MatrixRTCMode.Matrix_2_0]: radios.matrix20, } as const )[configMode]; expect(checkedValue).toBeChecked(); }, ); }); describe("KeyRotationStatus", () => { it("displays active status when key rotation is not suppressed", async () => { const client = createMockMatrixClient(); const vm = createStaticViewModel({ keyRotation: { suppressed: false, participantCount: 5 }, }); render( , ); await waitFor(() => expect( screen.getByText(/Media key rotation: active \(5 participants\)/), ).toBeInTheDocument(), ); }); it("displays suppressed status when key rotation is suppressed", async () => { const client = createMockMatrixClient(); const vm = createStaticViewModel({ keyRotation: { suppressed: true, participantCount: 50 }, }); render( , ); await waitFor(() => expect( screen.getByText( /Media key rotation: suppressed, participant limit reached \(50 participants\)/, ), ).toBeInTheDocument(), ); }); it("does not render KeyRotationStatus when not in a call", async () => { const client = createMockMatrixClient(); render( , ); await waitFor(() => expect( screen.queryByText(/Media key rotation:/), ).not.toBeInTheDocument(), ); }); }); });