Files
element-call/src/settings/DeveloperSettingsTab.test.tsx
T
RobinandJohannes Marbach 289260531b Let developers override the whole transport
The developer setting that overrode the LiveKit service URL becomes a
JSON transport, so that a `url`-only or combined transport can be tested
against a homeserver that does not advertise one yet. The settings modal
shows each LiveKit room's role instead of a never-populated isLocal flag.

Folds "Avoid null in transport dev setting" from the same PR. The setting feeds the SDK's transport option; the config fallback becomes a transport the host builds.

Co-authored-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2026-10-14 10:23:29 +02:00

497 lines
15 KiB
TypeScript

/*
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 {
DeveloperSettingsTab,
type DeveloperSettingsSnapshot,
type TransportInfo,
} from "./DeveloperSettingsTab";
import { outOfCallDeveloperSettingsTabViewModel } from "./DeveloperSettingsTabViewModel";
import { createStaticViewModel } from "../state/ViewModel";
import {
customTransport as customTransportSetting,
enableExtendedLivekitLogs as enableExtendedLivekitLogsSetting,
matrixRTCMode as matrixRTCModeSetting,
} from "./settings";
import {
authenticateWithTransport,
constant,
MatrixRTCMode,
} from "@element-hq/matrixrtc-sdk";
import type * as MatrixRTCSdk from "@element-hq/matrixrtc-sdk";
import { exampleTransport, mockConfig } from "../utils/test";
// 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("@element-hq/matrixrtc-sdk", async (importOriginal) => ({
...(await importOriginal<typeof MatrixRTCSdk>()),
authenticateWithTransport: vi.fn().mockResolvedValue({
url: "mock-url",
jwt: "mock-jwt",
}),
}));
/** A transport the panel shows, as the client would hand it out. */
function mockTransport(
url: string,
local: boolean,
resolved?: { url: string; roomAlias: string; identity: string },
): TransportInfo {
return {
local,
transport: {
type: "livekit",
id: url,
raw: { type: "livekit", livekit_service_url: url },
resolved$: constant(
resolved && { type: "livekit", token: "secret", ...resolved },
),
},
};
}
// 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<void> {
// `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 transports = [
mockTransport("https://local-sfu.example.org/jwt", true, {
url: "wss://local-sfu.example.org",
roomAlias: "TestAlias",
identity: "localParticipantIdentity",
}),
mockTransport("https://remote-sfu.example.org/jwt", false),
];
const { container } = render(
<DeveloperSettingsTab
client={client}
roomId={"#room:example.org"}
transports={transports}
env={{ MY_MOCK_ENV: 10, ENV: "test" } as unknown as ImportMetaEnv}
vm={outOfCallDeveloperSettingsTabViewModel}
/>,
);
// 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(
<TooltipProvider>
<DeveloperSettingsTab
client={client}
env={{} as unknown as ImportMetaEnv}
vm={outOfCallDeveloperSettingsTabViewModel}
/>
</TooltipProvider>,
);
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(authenticateWithTransport).not.toHaveBeenCalled();
expect(customTransportSetting.getValue()).toBe(null);
});
it("will not update custom transport without text in input", async () => {
const user = userEvent.setup();
render(
<TooltipProvider>
<DeveloperSettingsTab
client={client}
roomId="#testRoom"
env={{} as unknown as ImportMetaEnv}
vm={outOfCallDeveloperSettingsTabViewModel}
/>
</TooltipProvider>,
);
const input = screen.getByLabelText("Custom transport JSON");
await user.clear(input);
const saveButton = screen.getByRole("button", { name: "Save" });
await user.click(saveButton);
expect(authenticateWithTransport).not.toHaveBeenCalled();
expect(customTransportSetting.getValue()).toBe(null);
});
it("will not update custom transport when pressing cancel", async () => {
const user = userEvent.setup();
render(
<TooltipProvider>
<DeveloperSettingsTab
client={client}
roomId="#testRoom"
env={{} as unknown as ImportMetaEnv}
vm={outOfCallDeveloperSettingsTabViewModel}
/>
</TooltipProvider>,
);
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(authenticateWithTransport).not.toHaveBeenCalled();
expect(customTransportSetting.getValue()).toBe(null);
});
it("will update custom transport", async () => {
const user = userEvent.setup();
render(
<TooltipProvider>
<DeveloperSettingsTab
client={client}
roomId="#testRoom"
env={{} as unknown as ImportMetaEnv}
vm={outOfCallDeveloperSettingsTabViewModel}
/>
</TooltipProvider>,
);
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(authenticateWithTransport).toHaveBeenCalledWith(
expect.objectContaining({
transport: { type: "livekit", url: "wss://example.livekit.valid" },
serverName: "hs",
roomId: "#testRoom",
role: "publisher",
}),
);
expect(customTransportSetting.getValue()).toEqual({
type: "livekit",
url: "wss://example.livekit.valid",
});
});
it("will show error on unreachable transport", async () => {
const user = userEvent.setup();
render(
<TooltipProvider>
<DeveloperSettingsTab
client={client}
roomId="#testRoom"
env={{} as unknown as ImportMetaEnv}
vm={outOfCallDeveloperSettingsTabViewModel}
/>
</TooltipProvider>,
);
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" });
(authenticateWithTransport 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);
});
});
describe("enable extended livekit logs", () => {
afterEach(() => {
enableExtendedLivekitLogsSetting.setValue(false);
});
it("toggles extended livekit logs setting", async () => {
const user = userEvent.setup();
const client = createMockMatrixClient();
render(
<TooltipProvider>
<DeveloperSettingsTab
client={client}
env={{} as unknown as ImportMetaEnv}
vm={outOfCallDeveloperSettingsTabViewModel}
/>
</TooltipProvider>,
);
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(
<TooltipProvider>
<DeveloperSettingsTab
client={client}
env={{} as unknown as ImportMetaEnv}
vm={outOfCallDeveloperSettingsTabViewModel}
/>
</TooltipProvider>,
);
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(
<TooltipProvider>
<DeveloperSettingsTab
client={client}
env={{} as unknown as ImportMetaEnv}
vm={outOfCallDeveloperSettingsTabViewModel}
/>
</TooltipProvider>,
);
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(
<TooltipProvider>
<DeveloperSettingsTab
client={client}
env={{} as unknown as ImportMetaEnv}
vm={outOfCallDeveloperSettingsTabViewModel}
/>
</TooltipProvider>,
);
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<DeveloperSettingsSnapshot>({
keyRotation: { suppressed: false, participantCount: 5 },
});
render(
<TooltipProvider>
<DeveloperSettingsTab
client={client}
env={{} as unknown as ImportMetaEnv}
vm={vm}
/>
</TooltipProvider>,
);
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<DeveloperSettingsSnapshot>({
keyRotation: { suppressed: true, participantCount: 50 },
});
render(
<TooltipProvider>
<DeveloperSettingsTab
client={client}
env={{} as unknown as ImportMetaEnv}
vm={vm}
/>
</TooltipProvider>,
);
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(
<TooltipProvider>
<DeveloperSettingsTab
client={client}
env={{} as unknown as ImportMetaEnv}
vm={outOfCallDeveloperSettingsTabViewModel}
/>
</TooltipProvider>,
);
await waitFor(() =>
expect(
screen.queryByText(/Media key rotation:/),
).not.toBeInTheDocument(),
);
});
});
});