mirror of
https://github.com/vector-im/element-call.git
synced 2026-09-10 21:55:19 +00:00
135 lines
5.2 KiB
TypeScript
135 lines
5.2 KiB
TypeScript
/*
|
|
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 { expect, type Locator, test } from "@playwright/test";
|
|
|
|
import { createUserAndRoom, expectWithin, startHarness } from "./harness.ts";
|
|
|
|
/**
|
|
* Element Call embedded as a React component, driven through the development
|
|
* harness in `component/dev`.
|
|
*
|
|
* What these cover that the widget tests cannot is everything that follows from
|
|
* sharing a page with a host: whether Element Call stays inside the space it
|
|
* was given, and whether two of it can exist at once. As a widget, the iframe
|
|
* guaranteed both.
|
|
*/
|
|
|
|
/** The settings button, whichever of the two the footer is currently showing. */
|
|
function settingsButton(pane: Locator): Locator {
|
|
return pane
|
|
.getByTestId("settings-bottom-left")
|
|
.or(pane.getByTestId("settings-bottom-center"))
|
|
.filter({ visible: true })
|
|
.first();
|
|
}
|
|
|
|
test("holds a call between two components on one page", async ({ page }) => {
|
|
const { username, roomId } = await createUserAndRoom("twocomponents");
|
|
const panes = await startHarness(page, username, roomId);
|
|
|
|
// Each component shows a lobby of its own, and neither has joined anything
|
|
// just by being rendered
|
|
for (const index of [0, 1])
|
|
await expect(panes.nth(index).getByTestId("lobby_joinCall")).toBeVisible({
|
|
timeout: 60_000,
|
|
});
|
|
|
|
for (const index of [0, 1])
|
|
await panes.nth(index).getByTestId("lobby_joinCall").click();
|
|
|
|
// Two devices of one account, so each component should see itself and the
|
|
// other. This is the part that proves two Element Calls in one page are two
|
|
// calls, and not one shared thing wearing two hats.
|
|
for (const index of [0, 1])
|
|
await expect(panes.nth(index).getByTestId("videoTile")).toHaveCount(2, {
|
|
timeout: 60_000,
|
|
});
|
|
});
|
|
|
|
test("keeps its modals inside the container it was given", async ({ page }) => {
|
|
const { username, roomId } = await createUserAndRoom("containment");
|
|
const panes = await startHarness(page, username, roomId);
|
|
const pane = panes.first();
|
|
const container = pane.getByTestId("call-container");
|
|
|
|
await pane.getByTestId("lobby_joinCall").click({ timeout: 60_000 });
|
|
await expect(pane.getByTestId("footer-container")).toBeVisible({
|
|
timeout: 60_000,
|
|
});
|
|
|
|
// Both of these are positioned `fixed`, and were centred on the window
|
|
// rather than the container until it was made a containing block. The
|
|
// settings dialog spilled over the host's interface; the reaction picker sat
|
|
// at 82vh, which put it below the container entirely and so out of sight.
|
|
await settingsButton(pane).click();
|
|
await expectWithin(pane.getByRole("dialog"), container);
|
|
await pane.getByTestId("modal_close").click();
|
|
|
|
await pane.getByRole("button", { name: "Reactions" }).click();
|
|
await expectWithin(
|
|
pane.getByRole("dialog", { name: "Pick reaction" }),
|
|
container,
|
|
);
|
|
});
|
|
|
|
test("leaves the host's own page unstyled", async ({ page }) => {
|
|
const { username, roomId } = await createUserAndRoom("hoststyles");
|
|
const panes = await startHarness(page, username, roomId);
|
|
await expect(panes.first().getByTestId("lobby_joinCall")).toBeVisible({
|
|
timeout: 60_000,
|
|
});
|
|
|
|
// Element Call's stylesheet is written for a page of its own: normalize.css
|
|
// gives `html` a line height, Compound gives `body` its font and feature
|
|
// settings, and the design tokens live on `:root`. None of that may reach the
|
|
// host's document — the harness sets none of these itself, so anything other
|
|
// than the browser's defaults here came from us.
|
|
const host = await page.evaluate(() => {
|
|
const html = getComputedStyle(document.documentElement);
|
|
const body = getComputedStyle(document.body);
|
|
return {
|
|
lineHeight: html.lineHeight,
|
|
fontFeatureSettings: body.fontFeatureSettings,
|
|
token: html.getPropertyValue("--cpd-color-text-primary"),
|
|
};
|
|
});
|
|
expect(host).toEqual({
|
|
lineHeight: "normal",
|
|
fontFeatureSettings: "normal",
|
|
token: "",
|
|
});
|
|
|
|
// While inside the container, the same rules do apply
|
|
const root = panes.first().locator("[data-element-call-root]");
|
|
await expect(root).toHaveCSS("font-feature-settings", /"kern"/);
|
|
});
|
|
|
|
test("tells its host what it is doing", async ({ page }) => {
|
|
const { username, roomId } = await createUserAndRoom("hostbridge");
|
|
const panes = await startHarness(page, username, roomId);
|
|
const pane = panes.first();
|
|
const log = page.getByTestId("bridge-log");
|
|
|
|
// Every component reports to its host through the bridge, whether that host
|
|
// is a widget container or an application embedding it directly
|
|
await expect(log).toContainText("contentLoaded", { timeout: 60_000 });
|
|
|
|
await pane.getByTestId("lobby_joinCall").click({ timeout: 60_000 });
|
|
await expect(log).toContainText("notifyJoined", { timeout: 60_000 });
|
|
await expect(log).toContainText("setAlwaysOnScreen(true)", {
|
|
timeout: 60_000,
|
|
});
|
|
|
|
// And takes instructions back: the host asking for a mute should come back
|
|
// as the component reporting the new state
|
|
await pane.getByRole("button", { name: "Mute" }).click();
|
|
await expect(log).toContainText("notifyDeviceMute(audio: false", {
|
|
timeout: 30_000,
|
|
});
|
|
});
|