Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 157 additions & 0 deletions playwright/e2e/voip/element-call.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
/*
Copyright 2025 New Vector Ltd.

SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/

import type { EventType, Preset } from "matrix-js-sdk/src/matrix";
import { SettingLevel } from "../../../src/settings/SettingLevel";
import { test, expect } from "../../element-web-test";
import type { Credentials } from "../../plugins/homeserver";

function assertCommonCallParameters(
url: URLSearchParams,
hash: URLSearchParams,
user: Credentials,
room: { roomId: string },
): void {
expect(url.has("widgetId")).toEqual(true);
expect(url.has("parentUrl")).toEqual(true);

expect(hash.get("perParticipantE2EE")).toEqual("false");
expect(hash.get("userId")).toEqual(user.userId);
expect(hash.get("deviceId")).toEqual(user.deviceId);
expect(hash.get("roomId")).toEqual(room.roomId);
expect(hash.get("preload")).toEqual("false");

expect(hash.has("rageshakeSubmitUrl")).toEqual(true);
expect(hash.has("returnToLobby")).toEqual(false);
}

test.describe("Element Call", () => {
test.use({
config: {
element_call: {
use_exclusively: true,
},
},
botCreateOpts: {
autoAcceptInvites: true,
displayName: "Bob",
},
});

test.beforeEach(async ({ page, user, app }) => {
// Mock a widget page. It doesn't need to actually be Element Call.
await page.route("/widget.html", async (route) => {
await route.fulfill({
status: 200,
body: "<p> Hello world </p>",
});
});
await app.settings.setValue(
"Developer.elementCallUrl",
null,
SettingLevel.DEVICE,
new URL("/widget.html#", page.url()).toString(),
);
});

test.describe("Group Chat", () => {
test.use({
room: async ({ page, app, user, bot }, use) => {
const roomId = await app.client.createRoom({ name: "TestRoom", invite: [bot.credentials.userId] });
await use({ roomId });
},
});
test("should be able to start a video call", async ({ page, user, room, app }) => {
await app.viewRoomById(room.roomId);
await expect(page.getByText("Bob joined the room")).toBeVisible();

await page.getByRole("button", { name: "Video call" }).click();
await page.getByRole("menuitem", { name: "Element Call" }).click();

Check failure on line 73 in playwright/e2e/voip/element-call.spec.ts

View workflow job for this annotation

GitHub Actions / Run Tests [Chrome] 6/6

[Chrome] › playwright/e2e/voip/element-call.spec.ts:68:13 › Element Call › Group Chat › should be able to start a video call

1) [Chrome] › playwright/e2e/voip/element-call.spec.ts:68:13 › Element Call › Group Chat › should be able to start a video call Retry #2 ─────────────────────────────────────────────────────────────────────────────────────── Error: locator.click: Test timeout of 30000ms exceeded. Call log: - waiting for getByRole('menuitem', { name: 'Element Call' }) 71 | 72 | await page.getByRole("button", { name: "Video call" }).click(); > 73 | await page.getByRole("menuitem", { name: "Element Call" }).click(); | ^ 74 | 75 | const frameUrlStr = await page.locator("iframe").getAttribute("src"); 76 | await expect(frameUrlStr).toBeDefined(); at /home/runner/work/element-web/element-web/playwright/e2e/voip/element-call.spec.ts:73:72

Check failure on line 73 in playwright/e2e/voip/element-call.spec.ts

View workflow job for this annotation

GitHub Actions / Run Tests [Chrome] 6/6

[Chrome] › playwright/e2e/voip/element-call.spec.ts:68:13 › Element Call › Group Chat › should be able to start a video call

1) [Chrome] › playwright/e2e/voip/element-call.spec.ts:68:13 › Element Call › Group Chat › should be able to start a video call Retry #1 ─────────────────────────────────────────────────────────────────────────────────────── Error: locator.click: Test timeout of 30000ms exceeded. Call log: - waiting for getByRole('menuitem', { name: 'Element Call' }) 71 | 72 | await page.getByRole("button", { name: "Video call" }).click(); > 73 | await page.getByRole("menuitem", { name: "Element Call" }).click(); | ^ 74 | 75 | const frameUrlStr = await page.locator("iframe").getAttribute("src"); 76 | await expect(frameUrlStr).toBeDefined(); at /home/runner/work/element-web/element-web/playwright/e2e/voip/element-call.spec.ts:73:72

Check failure on line 73 in playwright/e2e/voip/element-call.spec.ts

View workflow job for this annotation

GitHub Actions / Run Tests [Chrome] 6/6

[Chrome] › playwright/e2e/voip/element-call.spec.ts:68:13 › Element Call › Group Chat › should be able to start a video call

1) [Chrome] › playwright/e2e/voip/element-call.spec.ts:68:13 › Element Call › Group Chat › should be able to start a video call Error: locator.click: Test timeout of 30000ms exceeded. Call log: - waiting for getByRole('menuitem', { name: 'Element Call' }) 71 | 72 | await page.getByRole("button", { name: "Video call" }).click(); > 73 | await page.getByRole("menuitem", { name: "Element Call" }).click(); | ^ 74 | 75 | const frameUrlStr = await page.locator("iframe").getAttribute("src"); 76 | await expect(frameUrlStr).toBeDefined(); at /home/runner/work/element-web/element-web/playwright/e2e/voip/element-call.spec.ts:73:72

const frameUrlStr = await page.locator("iframe").getAttribute("src");
await expect(frameUrlStr).toBeDefined();
// Ensure we set the correct parameters for ECall.
const url = new URL(frameUrlStr);
const hash = new URLSearchParams(url.hash.slice(1));
assertCommonCallParameters(url.searchParams, hash, user, room);
expect(hash.get("sendNotificationType")).toEqual("notification");
expect(hash.get("intent")).toEqual("start_call");
expect(hash.get("skipLobby")).toEqual(null);
});

test("should be able to skip lobby by holding down shift", async ({ page, user, bot, room, app }) => {
await app.viewRoomById(room.roomId);
await expect(page.getByText("Bob joined the room")).toBeVisible();

await page.getByRole("button", { name: "Video call" }).click();
await page.keyboard.down("Shift");
await page.getByRole("menuitem", { name: "Element Call" }).click();

Check failure on line 92 in playwright/e2e/voip/element-call.spec.ts

View workflow job for this annotation

GitHub Actions / Run Tests [Chrome] 6/6

[Chrome] › playwright/e2e/voip/element-call.spec.ts:86:13 › Element Call › Group Chat › should be able to skip lobby by holding down shift

2) [Chrome] › playwright/e2e/voip/element-call.spec.ts:86:13 › Element Call › Group Chat › should be able to skip lobby by holding down shift Retry #1 ─────────────────────────────────────────────────────────────────────────────────────── Error: locator.click: Test timeout of 30000ms exceeded. Call log: - waiting for getByRole('menuitem', { name: 'Element Call' }) 90 | await page.getByRole("button", { name: "Video call" }).click(); 91 | await page.keyboard.down("Shift"); > 92 | await page.getByRole("menuitem", { name: "Element Call" }).click(); | ^ 93 | await page.keyboard.up("Shift"); 94 | 95 | const frameUrlStr = await page.locator("iframe").getAttribute("src"); at /home/runner/work/element-web/element-web/playwright/e2e/voip/element-call.spec.ts:92:72

Check failure on line 92 in playwright/e2e/voip/element-call.spec.ts

View workflow job for this annotation

GitHub Actions / Run Tests [Chrome] 6/6

[Chrome] › playwright/e2e/voip/element-call.spec.ts:86:13 › Element Call › Group Chat › should be able to skip lobby by holding down shift

2) [Chrome] › playwright/e2e/voip/element-call.spec.ts:86:13 › Element Call › Group Chat › should be able to skip lobby by holding down shift Error: locator.click: Test timeout of 30000ms exceeded. Call log: - waiting for getByRole('menuitem', { name: 'Element Call' }) 90 | await page.getByRole("button", { name: "Video call" }).click(); 91 | await page.keyboard.down("Shift"); > 92 | await page.getByRole("menuitem", { name: "Element Call" }).click(); | ^ 93 | await page.keyboard.up("Shift"); 94 | 95 | const frameUrlStr = await page.locator("iframe").getAttribute("src"); at /home/runner/work/element-web/element-web/playwright/e2e/voip/element-call.spec.ts:92:72
await page.keyboard.up("Shift");

const frameUrlStr = await page.locator("iframe").getAttribute("src");
await expect(frameUrlStr).toBeDefined();
const url = new URL(frameUrlStr);
const hash = new URLSearchParams(url.hash.slice(1));
assertCommonCallParameters(url.searchParams, hash, user, room);
expect(hash.get("sendNotificationType")).toEqual("notification");
expect(hash.get("intent")).toEqual("start_call");
expect(hash.get("skipLobby")).toEqual("true");
});
});

test.describe("DMs", () => {
test.use({
room: async ({ page, app, user, bot }, use) => {
const roomId = await app.client.createRoom({
name: "TestRoom",
preset: "trusted_private_chat" as Preset.TrustedPrivateChat,
invite: [bot.credentials.userId],
});
await app.client.setAccountData("m.direct" as EventType.Direct, {
[bot.credentials.userId]: [roomId],
});
await use({ roomId });
},
});

test("should be able to start a video call", async ({ page, user, room, app }) => {
await app.viewRoomById(room.roomId);
await expect(page.getByText("Bob joined the room")).toBeVisible();

await page.getByRole("button", { name: "Video call" }).click();
await page.getByRole("menuitem", { name: "Element Call" }).click();
const frameUrlStr = await page.locator("iframe").getAttribute("src");

await expect(frameUrlStr).toBeDefined();
const url = new URL(frameUrlStr);
const hash = new URLSearchParams(url.hash.slice(1));
assertCommonCallParameters(url.searchParams, hash, user, room);
expect(hash.get("sendNotificationType")).toEqual("ring");
expect(hash.get("intent")).toEqual("start_call_dm");
expect(hash.get("skipLobby")).toEqual(null);
});

test("should be able to skip lobby by holding down shift", async ({ page, user, room, app }) => {
await app.viewRoomById(room.roomId);
await expect(page.getByText("Bob joined the room")).toBeVisible();

await page.getByRole("button", { name: "Video call" }).click();
await page.keyboard.down("Shift");
await page.getByRole("menuitem", { name: "Element Call" }).click();
await page.keyboard.up("Shift");
const frameUrlStr = await page.locator("iframe").getAttribute("src");

await expect(frameUrlStr).toBeDefined();
const url = new URL(frameUrlStr);
const hash = new URLSearchParams(url.hash.slice(1));
assertCommonCallParameters(url.searchParams, hash, user, room);
expect(hash.get("sendNotificationType")).toEqual("ring");
expect(hash.get("intent")).toEqual("start_call_dm");
expect(hash.get("skipLobby")).toEqual("true");
});
});
});
1 change: 0 additions & 1 deletion src/components/structures/RoomView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2609,7 +2609,6 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
<CallView
room={this.state.room}
resizing={this.state.resizing}
skipLobby={this.context.roomViewStore.skipCallLobby() ?? false}
role="main"
onClose={this.onCallClose}
/>
Expand Down
6 changes: 4 additions & 2 deletions src/components/views/beacon/RoomCallBanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { logger } from "matrix-js-sdk/src/logger";

import { _t } from "../../../languageHandler";
import AccessibleButton, { type ButtonEvent } from "../elements/AccessibleButton";
import defaultDispatcher from "../../../dispatcher/dispatcher";
import defaultDispatcher, { type MatrixDispatcher } from "../../../dispatcher/dispatcher";
import { type ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
import { Action } from "../../../dispatcher/actions";
import { ConnectionState, type ElementCall } from "../../../models/Call";
Expand All @@ -35,7 +35,8 @@ const RoomCallBannerInner: React.FC<RoomCallBannerProps> = ({ roomId, call }) =>
action: Action.ViewRoom,
room_id: roomId,
view_call: true,
skipLobby: "shiftKey" in ev ? ev.shiftKey : false,
// If shift is held down, always skip lobby. Else, use defaults.
skipLobby: ("shiftKey" in ev && ev.shiftKey) || undefined,
metricsTrigger: undefined,
});
},
Expand Down Expand Up @@ -79,6 +80,7 @@ const RoomCallBannerInner: React.FC<RoomCallBannerProps> = ({ roomId, call }) =>

interface Props {
roomId: Room["roomId"];
dispatcher?: MatrixDispatcher;
}

const RoomCallBanner: React.FC<Props> = ({ roomId }) => {
Expand Down
25 changes: 3 additions & 22 deletions src/components/views/voip/CallView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,11 @@ interface JoinCallViewProps {
room: Room;
resizing: boolean;
call: Call;
skipLobby?: boolean;
role?: AriaRole;
onClose: () => void;
}

const JoinCallView: FC<JoinCallViewProps> = ({ room, resizing, call, skipLobby, role, onClose }) => {
const JoinCallView: FC<JoinCallViewProps> = ({ room, resizing, call, role, onClose }) => {
const cli = useContext(MatrixClientContext);
useTypedEventEmitter(call, CallEvent.Close, onClose);

Expand All @@ -35,12 +34,6 @@ const JoinCallView: FC<JoinCallViewProps> = ({ room, resizing, call, skipLobby,
call.clean();
}, [call]);

useEffect(() => {
// Always update the widget data so that we don't ignore "skipLobby" accidentally.
call.widget.data ??= {};
call.widget.data.skipLobby = skipLobby;
}, [call.widget, skipLobby]);

const disconnectAllOtherCalls: () => Promise<void> = useCallback(async () => {
// The stickyPromise has to resolve before the widget actually becomes sticky.
// We only let the widget become sticky after disconnecting all other active calls.
Expand Down Expand Up @@ -69,27 +62,15 @@ const JoinCallView: FC<JoinCallViewProps> = ({ room, resizing, call, skipLobby,
interface CallViewProps {
room: Room;
resizing: boolean;
skipLobby?: boolean;
role?: AriaRole;
/**
* Callback for when the user closes the call.
*/
onClose: () => void;
}

export const CallView: FC<CallViewProps> = ({ room, resizing, skipLobby, role, onClose }) => {
export const CallView: FC<CallViewProps> = ({ room, resizing, role, onClose }) => {
const call = useCall(room.roomId);

return (
call && (
<JoinCallView
room={room}
resizing={resizing}
call={call}
skipLobby={skipLobby}
role={role}
onClose={onClose}
/>
)
);
return call && <JoinCallView room={room} resizing={resizing} call={call} role={role} onClose={onClose} />;
};
4 changes: 2 additions & 2 deletions src/hooks/room/useRoomCall.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ export const useRoomCall = (
if (widget && promptPinWidget) {
WidgetLayoutStore.instance.moveToContainer(room, widget, Container.Top);
} else {
placeCall(room, CallType.Voice, callPlatformType, evt?.shiftKey ?? false);
placeCall(room, CallType.Voice, callPlatformType, evt?.shiftKey || undefined);
}
},
[promptPinWidget, room, widget],
Expand All @@ -240,7 +240,7 @@ export const useRoomCall = (
if (widget && promptPinWidget) {
WidgetLayoutStore.instance.moveToContainer(room, widget, Container.Top);
} else {
placeCall(room, CallType.Video, callPlatformType, evt?.shiftKey ?? false);
placeCall(room, CallType.Video, callPlatformType, evt?.shiftKey || undefined);
}
},
[widget, promptPinWidget, room],
Expand Down
Loading
Loading