Files
element-call-Github/playwright/component/component-call.spec.ts
T
2026-09-04 13:51:47 +02:00

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,
});
});