mirror of
https://github.com/vector-im/element-call.git
synced 2026-09-10 21:55:19 +00:00
more tests
This commit is contained in:
@@ -339,6 +339,50 @@ describe("UrlParams", () => {
|
|||||||
callIntent: "audio",
|
callIntent: "audio",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("accepts start_call_dm_voice", () => {
|
||||||
|
expect(
|
||||||
|
computeUrlParams(
|
||||||
|
"?intent=start_call_dm_voice&widgetId=1234&parentUrl=parent.org",
|
||||||
|
),
|
||||||
|
).toMatchObject({
|
||||||
|
...startNewCallDefaults("desktop"),
|
||||||
|
// A DM rings the other side and waits for them, whichever platform
|
||||||
|
sendNotificationType: "ring",
|
||||||
|
autoLeaveWhenOthersLeft: true,
|
||||||
|
waitForCallPickup: true,
|
||||||
|
callIntent: "audio",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("accepts join_existing_dm", () => {
|
||||||
|
expect(
|
||||||
|
computeUrlParams(
|
||||||
|
"?intent=join_existing_dm&widgetId=1234&parentUrl=parent.org",
|
||||||
|
),
|
||||||
|
).toMatchObject({
|
||||||
|
...joinExistingCallDefaults("desktop"),
|
||||||
|
// Straight in: the other side is already waiting
|
||||||
|
skipLobby: true,
|
||||||
|
autoLeaveWhenOthersLeft: true,
|
||||||
|
waitForCallPickup: false,
|
||||||
|
callIntent: "video",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("accepts join_existing_dm_voice", () => {
|
||||||
|
expect(
|
||||||
|
computeUrlParams(
|
||||||
|
"?intent=join_existing_dm_voice&widgetId=1234&parentUrl=parent.org",
|
||||||
|
),
|
||||||
|
).toMatchObject({
|
||||||
|
...joinExistingCallDefaults("desktop"),
|
||||||
|
skipLobby: true,
|
||||||
|
autoLeaveWhenOthersLeft: true,
|
||||||
|
waitForCallPickup: false,
|
||||||
|
callIntent: "audio",
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("skipLobby", () => {
|
describe("skipLobby", () => {
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ import userEvent, {
|
|||||||
import { type RelationsContainer } from "matrix-js-sdk/lib/models/relations-container";
|
import { type RelationsContainer } from "matrix-js-sdk/lib/models/relations-container";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { TooltipProvider } from "@vector-im/compound-web";
|
import { TooltipProvider } from "@vector-im/compound-web";
|
||||||
|
import { Subject } from "rxjs";
|
||||||
|
import { Room as LivekitRoom } from "livekit-client";
|
||||||
|
|
||||||
import { prefetchSounds } from "../soundUtils";
|
import { prefetchSounds } from "../soundUtils";
|
||||||
import { useAudioContext } from "../useAudioContext";
|
import { useAudioContext } from "../useAudioContext";
|
||||||
@@ -54,8 +56,10 @@ import { GroupCallErrorBoundary } from "./GroupCallErrorBoundary";
|
|||||||
import {
|
import {
|
||||||
type HostBridge,
|
type HostBridge,
|
||||||
HostBridgeProvider,
|
HostBridgeProvider,
|
||||||
|
type HostRequest,
|
||||||
nullHostBridge,
|
nullHostBridge,
|
||||||
} from "../HostBridge";
|
} from "../HostBridge";
|
||||||
|
import { type JoinCallData } from "../widget";
|
||||||
import { MatrixRTCTransportMissingError } from "../utils/errors";
|
import { MatrixRTCTransportMissingError } from "../utils/errors";
|
||||||
import { ProcessorProvider } from "../livekit/TrackProcessorContext";
|
import { ProcessorProvider } from "../livekit/TrackProcessorContext";
|
||||||
import { MediaDevicesContext } from "../MediaDevicesContext";
|
import { MediaDevicesContext } from "../MediaDevicesContext";
|
||||||
@@ -139,6 +143,8 @@ function createCallView(
|
|||||||
joined = true,
|
joined = true,
|
||||||
options: {
|
options: {
|
||||||
withErrorBoundary?: boolean;
|
withErrorBoundary?: boolean;
|
||||||
|
/** Wait for the host to say when to join, rather than joining at once. */
|
||||||
|
preload?: boolean;
|
||||||
} = {},
|
} = {},
|
||||||
): {
|
): {
|
||||||
rtcSession: MatrixRTCSession;
|
rtcSession: MatrixRTCSession;
|
||||||
@@ -176,7 +182,7 @@ function createCallView(
|
|||||||
client={client}
|
client={client}
|
||||||
isPasswordlessUser={false}
|
isPasswordlessUser={false}
|
||||||
confineToRoom={false}
|
confineToRoom={false}
|
||||||
preload={false}
|
preload={options.preload ?? false}
|
||||||
// Straight into the (mocked) call, past the lobby
|
// Straight into the (mocked) call, past the lobby
|
||||||
skipLobby
|
skipLobby
|
||||||
rtcSession={rtcSession.asMockedSession()}
|
rtcSession={rtcSession.asMockedSession()}
|
||||||
@@ -283,6 +289,26 @@ test("Should ask the host to close when all other left and play a sound", async
|
|||||||
await waitFor(() => expect(close).toHaveBeenCalledOnce());
|
await waitFor(() => expect(close).toHaveBeenCalledOnce());
|
||||||
}, 80000);
|
}, 80000);
|
||||||
|
|
||||||
|
test("Waits for the host to say when to join, when preloaded", async () => {
|
||||||
|
// Nothing to match device names against; the host names none anyway
|
||||||
|
vi.spyOn(LivekitRoom, "getLocalDevices").mockResolvedValue([]);
|
||||||
|
const join$ = new Subject<HostRequest<JoinCallData>>();
|
||||||
|
const hostBridge: HostBridge = { ...nullHostBridge, join$ };
|
||||||
|
|
||||||
|
createCallView(hostBridge, false, { preload: true });
|
||||||
|
await flushPromises();
|
||||||
|
// Past the lobby, but not in the call: the host has not asked yet
|
||||||
|
expect(screen.queryByText("Leave")).toBeNull();
|
||||||
|
|
||||||
|
const reply = vi.fn();
|
||||||
|
act(() =>
|
||||||
|
join$.next({ data: { audioInput: null, videoInput: null }, reply }),
|
||||||
|
);
|
||||||
|
// Then in the call, and the host told so
|
||||||
|
await waitFor(() => expect(reply).toHaveBeenCalledOnce());
|
||||||
|
expect(screen.getByText("Leave")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
test("Should not ask the host to close when auto leave due to error", async () => {
|
test("Should not ask the host to close when auto leave due to error", async () => {
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2026 Element Creations Ltd.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
import { render, screen } from "@testing-library/react";
|
||||||
|
import userEvent from "@testing-library/user-event";
|
||||||
|
import { TooltipProvider } from "@vector-im/compound-web";
|
||||||
|
import { type MatrixClient, type RoomSummary } from "matrix-js-sdk";
|
||||||
|
|
||||||
|
import { KnockLobbyView } from "./KnockLobbyView";
|
||||||
|
import { LeaveToHomeProvider } from "../LeaveToHomeContext";
|
||||||
|
import { MediaDevicesContext } from "../MediaDevicesContext";
|
||||||
|
import { type ProcessorState } from "../livekit/TrackProcessorContext";
|
||||||
|
import { mockMediaDevices } from "../utils/test";
|
||||||
|
|
||||||
|
vi.mock("@livekit/components-react", () => ({
|
||||||
|
usePreviewTracks: (): unknown[] => [],
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../livekit/TrackProcessorContext", () => ({
|
||||||
|
useTrackProcessor: (): ProcessorState => ({
|
||||||
|
supported: false,
|
||||||
|
processor: undefined,
|
||||||
|
}),
|
||||||
|
useTrackProcessorSync: (): void => {},
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("react-use-measure", () => ({
|
||||||
|
default: (): [() => void, object] => [(): void => {}, {}],
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../settings/SettingsModal", () => ({
|
||||||
|
SettingsModal: (): null => null,
|
||||||
|
defaultSettingsTab: "general",
|
||||||
|
}));
|
||||||
|
|
||||||
|
const client = {
|
||||||
|
getUserId: () => "@user:example.org",
|
||||||
|
getDeviceId: () => "DEVICE",
|
||||||
|
} as Partial<MatrixClient> as MatrixClient;
|
||||||
|
|
||||||
|
// What peeking at a room we are not in tells us about it
|
||||||
|
const roomSummary = {
|
||||||
|
room_id: "!room:example.org",
|
||||||
|
name: "Knock Room",
|
||||||
|
"im.nheko.summary.encryption": "m.megolm.v1.aes-sha2",
|
||||||
|
} as Partial<RoomSummary> as RoomSummary;
|
||||||
|
|
||||||
|
function renderKnockLobby(knock: (() => void) | null): void {
|
||||||
|
render(
|
||||||
|
<LeaveToHomeProvider value={vi.fn()}>
|
||||||
|
<MediaDevicesContext value={mockMediaDevices({})}>
|
||||||
|
<TooltipProvider>
|
||||||
|
<KnockLobbyView
|
||||||
|
client={client}
|
||||||
|
roomSummary={roomSummary}
|
||||||
|
profile={{ displayName: "Test User", avatarUrl: "" }}
|
||||||
|
knock={knock}
|
||||||
|
confineToRoom={false}
|
||||||
|
hideHeader={false}
|
||||||
|
/>
|
||||||
|
</TooltipProvider>
|
||||||
|
</MediaDevicesContext>
|
||||||
|
</LeaveToHomeProvider>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("KnockLobbyView", () => {
|
||||||
|
it("offers to ask to join, with what it knows of the room", async () => {
|
||||||
|
const knock = vi.fn();
|
||||||
|
renderKnockLobby(knock);
|
||||||
|
|
||||||
|
// The mute state arrives asynchronously, and the lobby with it
|
||||||
|
const button = await screen.findByTestId("lobby_joinCall");
|
||||||
|
expect(button).toHaveTextContent("Request to join call");
|
||||||
|
expect(button).toBeEnabled();
|
||||||
|
expect(screen.getByText("Knock Room")).toBeInTheDocument();
|
||||||
|
|
||||||
|
await userEvent.setup().click(button);
|
||||||
|
expect(knock).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("waits once it has asked", async () => {
|
||||||
|
renderKnockLobby(null);
|
||||||
|
|
||||||
|
const button = await screen.findByTestId("lobby_joinCall");
|
||||||
|
expect(button).toHaveTextContent("Request sent!");
|
||||||
|
// Compound's button keeps focusable, saying so through ARIA instead
|
||||||
|
expect(button).toHaveAttribute("aria-disabled", "true");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -6,15 +6,22 @@ Please see LICENSE in the repository root for full details.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||||
import { BehaviorSubject } from "rxjs";
|
import { BehaviorSubject, NEVER, Subject } from "rxjs";
|
||||||
import { logger } from "matrix-js-sdk/lib/logger";
|
import { logger } from "matrix-js-sdk/lib/logger";
|
||||||
|
|
||||||
import { nullHostBridge } from "../HostBridge";
|
import {
|
||||||
|
type DeviceMuteRequest,
|
||||||
|
type DeviceMuteState,
|
||||||
|
type HostBridge,
|
||||||
|
type HostRequest,
|
||||||
|
nullHostBridge,
|
||||||
|
} from "../HostBridge";
|
||||||
import { MuteStates, MuteState } from "./MuteStates";
|
import { MuteStates, MuteState } from "./MuteStates";
|
||||||
import {
|
import {
|
||||||
type AudioOutputDeviceLabel,
|
type AudioOutputDeviceLabel,
|
||||||
type DeviceLabel,
|
type DeviceLabel,
|
||||||
type MediaDevice,
|
type MediaDevice,
|
||||||
|
type SelectedAudioInputDevice,
|
||||||
type SelectedAudioOutputDevice,
|
type SelectedAudioOutputDevice,
|
||||||
type SelectedDevice,
|
type SelectedDevice,
|
||||||
} from "./MediaDevices";
|
} from "./MediaDevices";
|
||||||
@@ -221,6 +228,80 @@ describe("MuteStates", () => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function aAudioInput(): MediaDevice<DeviceLabel, SelectedAudioInputDevice> {
|
||||||
|
return {
|
||||||
|
available$: constant(
|
||||||
|
new Map<string, DeviceLabel>([
|
||||||
|
["mic0", { type: "name", name: "Built-in Microphone" }],
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
selected$: constant({ id: "mic0", hardwareDeviceChange$: NEVER }),
|
||||||
|
select(): void {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test("keeps the host informed and applies its mute requests", async () => {
|
||||||
|
const deviceMute$ = new Subject<
|
||||||
|
HostRequest<DeviceMuteRequest, DeviceMuteState>
|
||||||
|
>();
|
||||||
|
const notifyDeviceMute = vi.fn(async (): Promise<void> => {});
|
||||||
|
const hostBridge: HostBridge = {
|
||||||
|
...nullHostBridge,
|
||||||
|
notifyDeviceMute,
|
||||||
|
deviceMute$,
|
||||||
|
};
|
||||||
|
const muteStates = new MuteStates(
|
||||||
|
testScope,
|
||||||
|
mockMediaDevices({
|
||||||
|
audioInput: aAudioInput(),
|
||||||
|
videoInput: aVideoInput(),
|
||||||
|
}),
|
||||||
|
{ audioEnabled: true, videoEnabled: false },
|
||||||
|
hostBridge,
|
||||||
|
);
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
// The host hears the state we started in
|
||||||
|
expect(notifyDeviceMute).toHaveBeenLastCalledWith({
|
||||||
|
audio_enabled: true,
|
||||||
|
video_enabled: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// The host asks for the camera on, saying nothing about the microphone,
|
||||||
|
// which is left as it is
|
||||||
|
const reply = vi.fn();
|
||||||
|
deviceMute$.next({ data: { video_enabled: true }, reply });
|
||||||
|
await flushPromises();
|
||||||
|
expect(reply).toHaveBeenCalledExactlyOnceWith({
|
||||||
|
audio_enabled: true,
|
||||||
|
video_enabled: true,
|
||||||
|
});
|
||||||
|
expect(muteStates.audio.enabled$.value).toBe(true);
|
||||||
|
expect(muteStates.video.enabled$.value).toBe(true);
|
||||||
|
expect(notifyDeviceMute).toHaveBeenLastCalledWith({
|
||||||
|
audio_enabled: true,
|
||||||
|
video_enabled: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Then for everything off
|
||||||
|
const replyAgain = vi.fn();
|
||||||
|
deviceMute$.next({
|
||||||
|
data: { audio_enabled: false, video_enabled: false },
|
||||||
|
reply: replyAgain,
|
||||||
|
});
|
||||||
|
await flushPromises();
|
||||||
|
expect(replyAgain).toHaveBeenCalledExactlyOnceWith({
|
||||||
|
audio_enabled: false,
|
||||||
|
video_enabled: false,
|
||||||
|
});
|
||||||
|
expect(muteStates.audio.enabled$.value).toBe(false);
|
||||||
|
expect(muteStates.video.enabled$.value).toBe(false);
|
||||||
|
expect(notifyDeviceMute).toHaveBeenLastCalledWith({
|
||||||
|
audio_enabled: false,
|
||||||
|
video_enabled: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test("should mute camera when in earpiece mode", async () => {
|
test("should mute camera when in earpiece mode", async () => {
|
||||||
const audioOutputDevice = aAudioOutputDevices();
|
const audioOutputDevice = aAudioOutputDevices();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user