mirror of
https://github.com/element-hq/element-web.git
synced 2025-12-27 23:11:21 +00:00
Add tests to cover joining and starting an Element call (#30843)
* Add tests * Add test IDs * Revert to pre-new-widget-refactors state * Update codeowners * Remove one of the test IDs * Update snapshots as DMs don't have room names :) * Remove only * fix a import * fix docstring * update snaps * remove a line * update snaps
This commit is contained in:
parent
b89de61e12
commit
479b451916
7
.github/CODEOWNERS
vendored
7
.github/CODEOWNERS
vendored
@ -18,9 +18,10 @@
|
||||
/playwright/e2e/settings/encryption-user-tab/ @element-hq/element-crypto-web-reviewers
|
||||
|
||||
|
||||
/src/models/Call.ts @element-hq/element-call-reviewers
|
||||
/src/call-types.ts @element-hq/element-call-reviewers
|
||||
/src/components/views/voip @element-hq/element-call-reviewers
|
||||
/src/models/Call.ts @element-hq/element-call-reviewers
|
||||
/src/call-types.ts @element-hq/element-call-reviewers
|
||||
/src/components/views/voip @element-hq/element-call-reviewers
|
||||
/playwright/e2e/voip/element-call.spec.ts @element-hq/element-call-reviewers
|
||||
|
||||
# Ignore translations as those will be updated by GHA for Localazy download
|
||||
/src/i18n/strings
|
||||
|
||||
312
playwright/e2e/voip/element-call.spec.ts
Normal file
312
playwright/e2e/voip/element-call.spec.ts
Normal file
@ -0,0 +1,312 @@
|
||||
/*
|
||||
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";
|
||||
import type { Bot } from "../../pages/bot";
|
||||
|
||||
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.get("returnToLobby")).toEqual("false");
|
||||
}
|
||||
|
||||
async function sendRTCState(bot: Bot, roomId: string, notification?: "ring" | "notification") {
|
||||
const resp = await bot.sendStateEvent(
|
||||
roomId,
|
||||
"org.matrix.msc3401.call.member",
|
||||
{
|
||||
application: "m.call",
|
||||
call_id: "",
|
||||
device_id: "OiDFxsZrjz",
|
||||
expires: 180000000,
|
||||
foci_preferred: [
|
||||
{
|
||||
livekit_alias: roomId,
|
||||
livekit_service_url: "https://example.org",
|
||||
type: "livekit",
|
||||
},
|
||||
],
|
||||
focus_active: {
|
||||
focus_selection: "oldest_membership",
|
||||
type: "livekit",
|
||||
},
|
||||
scope: "m.room",
|
||||
},
|
||||
`_@${bot.credentials.userId}_OiDFxsZrjz_m.call`,
|
||||
);
|
||||
if (!notification) {
|
||||
return;
|
||||
}
|
||||
await bot.sendEvent(roomId, null, "org.matrix.msc4075.rtc.notification", {
|
||||
"lifetime": 30000,
|
||||
"m.mentions": {
|
||||
room: true,
|
||||
user_ids: [],
|
||||
},
|
||||
"m.relates_to": {
|
||||
event_id: resp.event_id,
|
||||
rel_type: "org.matrix.msc4075.rtc.notification.parent",
|
||||
},
|
||||
"notification_type": notification,
|
||||
"sender_ts": 1758611895996,
|
||||
});
|
||||
}
|
||||
|
||||
test.describe("Element Call", () => {
|
||||
test.use({
|
||||
config: {
|
||||
element_call: {
|
||||
use_exclusively: false,
|
||||
},
|
||||
features: {
|
||||
feature_group_calls: true,
|
||||
},
|
||||
},
|
||||
displayName: "Alice",
|
||||
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();
|
||||
|
||||
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("intent")).toEqual("start_call");
|
||||
expect(hash.get("skipLobby")).toEqual("false");
|
||||
});
|
||||
|
||||
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();
|
||||
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("intent")).toEqual("start_call");
|
||||
expect(hash.get("skipLobby")).toEqual("true");
|
||||
});
|
||||
|
||||
test("should be able to join a call in progress", async ({ page, user, bot, room, app }) => {
|
||||
await app.viewRoomById(room.roomId);
|
||||
// Allow bob to create a call
|
||||
await app.client.setPowerLevel(room.roomId, bot.credentials.userId, 50);
|
||||
await expect(page.getByText("Bob joined the room")).toBeVisible();
|
||||
// Fake a start of a call
|
||||
await sendRTCState(bot, room.roomId);
|
||||
const button = page.getByTestId("join-call-button");
|
||||
await expect(button).toBeInViewport({ timeout: 5000 });
|
||||
// And test joining
|
||||
await button.click();
|
||||
const frameUrlStr = await page.locator("iframe").getAttribute("src");
|
||||
console.log(frameUrlStr);
|
||||
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("intent")).toEqual("join_existing");
|
||||
expect(hash.get("skipLobby")).toEqual("false");
|
||||
});
|
||||
|
||||
[true, false].forEach((skipLobbyToggle) => {
|
||||
test(
|
||||
`should be able to join a call via incoming call toast (skipLobby=${skipLobbyToggle})`,
|
||||
{ tag: ["@screenshot"] },
|
||||
async ({ page, user, bot, room, app }) => {
|
||||
await app.viewRoomById(room.roomId);
|
||||
// Allow bob to create a call
|
||||
await app.client.setPowerLevel(room.roomId, bot.credentials.userId, 50);
|
||||
await expect(page.getByText("Bob joined the room")).toBeVisible();
|
||||
// Fake a start of a call
|
||||
await sendRTCState(bot, room.roomId, "notification");
|
||||
const toast = page.locator(".mx_Toast_toast");
|
||||
const button = toast.getByRole("button", { name: "Join" });
|
||||
if (skipLobbyToggle) {
|
||||
await toast.getByRole("switch").check();
|
||||
await expect(toast).toMatchScreenshot("incoming-call-group-video-toast-checked.png");
|
||||
} else {
|
||||
await toast.getByRole("switch").uncheck();
|
||||
await expect(toast).toMatchScreenshot("incoming-call-group-video-toast-unchecked.png");
|
||||
}
|
||||
|
||||
// And test joining
|
||||
await button.click();
|
||||
const frameUrlStr = await page.locator("iframe").getAttribute("src");
|
||||
console.log(frameUrlStr);
|
||||
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("intent")).toEqual("join_existing");
|
||||
expect(hash.get("skipLobby")).toEqual(skipLobbyToggle.toString());
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("DMs", () => {
|
||||
test.use({
|
||||
room: async ({ page, app, user, bot }, use) => {
|
||||
const roomId = await app.client.createRoom({
|
||||
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("intent")).toEqual("start_call_dm");
|
||||
expect(hash.get("skipLobby")).toEqual("false");
|
||||
});
|
||||
|
||||
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("intent")).toEqual("start_call_dm");
|
||||
expect(hash.get("skipLobby")).toEqual("true");
|
||||
});
|
||||
|
||||
test("should be able to join a call in progress", async ({ page, user, bot, room, app }) => {
|
||||
await app.viewRoomById(room.roomId);
|
||||
// Allow bob to create a call
|
||||
await expect(page.getByText("Bob joined the room")).toBeVisible();
|
||||
// Fake a start of a call
|
||||
await sendRTCState(bot, room.roomId);
|
||||
const button = page.getByTestId("join-call-button");
|
||||
await expect(button).toBeInViewport({ timeout: 5000 });
|
||||
// And test joining
|
||||
await button.click();
|
||||
const frameUrlStr = await page.locator("iframe").getAttribute("src");
|
||||
console.log(frameUrlStr);
|
||||
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("intent")).toEqual("join_existing_dm");
|
||||
expect(hash.get("skipLobby")).toEqual("false");
|
||||
});
|
||||
|
||||
[true, false].forEach((skipLobbyToggle) => {
|
||||
test(
|
||||
`should be able to join a call via incoming call toast (skipLobby=${skipLobbyToggle})`,
|
||||
{ tag: ["@screenshot"] },
|
||||
async ({ page, user, bot, room, app }) => {
|
||||
await app.viewRoomById(room.roomId);
|
||||
// Allow bob to create a call
|
||||
await expect(page.getByText("Bob joined the room")).toBeVisible();
|
||||
// Fake a start of a call
|
||||
await sendRTCState(bot, room.roomId, "ring");
|
||||
const toast = page.locator(".mx_Toast_toast");
|
||||
const button = toast.getByRole("button", { name: "Join" });
|
||||
if (skipLobbyToggle) {
|
||||
await toast.getByRole("switch").check();
|
||||
await expect(toast).toMatchScreenshot("incoming-call-dm-video-toast-checked.png");
|
||||
} else {
|
||||
await toast.getByRole("switch").uncheck();
|
||||
await expect(toast).toMatchScreenshot("incoming-call-dm-video-toast-unchecked.png");
|
||||
}
|
||||
|
||||
// And test joining
|
||||
await button.click();
|
||||
const frameUrlStr = await page.locator("iframe").getAttribute("src");
|
||||
console.log(frameUrlStr);
|
||||
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("intent")).toEqual("join_existing_dm");
|
||||
expect(hash.get("skipLobby")).toEqual(skipLobbyToggle.toString());
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -469,6 +469,27 @@ export class Client {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a power level to one or multiple users.
|
||||
* Will apply changes atop of current power level event.
|
||||
* @param roomId - the room to update power levels in
|
||||
* @param userId - the ID of the user or users to update power levels of
|
||||
* @param powerLevel - the numeric power level to update given users to
|
||||
*/
|
||||
public async setPowerLevel(
|
||||
roomId: string,
|
||||
userId: string | string[],
|
||||
powerLevel: number,
|
||||
): Promise<ISendEventResponse> {
|
||||
const client = await this.prepareClient();
|
||||
return client.evaluate(
|
||||
async (client, { roomId, userId, powerLevel }) => {
|
||||
return client.setPowerLevel(roomId, userId, powerLevel);
|
||||
},
|
||||
{ roomId, userId, powerLevel },
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Leaves the given room.
|
||||
* @param roomId ID of the room to leave
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
@ -129,6 +129,7 @@ export default function RoomHeader({
|
||||
disabled={!!videoCallDisabledReason}
|
||||
color="primary"
|
||||
aria-label={videoCallDisabledReason ?? _t("action|join")}
|
||||
data-testId="join-call-button"
|
||||
>
|
||||
{_t("action|join")}
|
||||
</Button>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user