diff --git a/playwright/access.spec.ts b/playwright/access.spec.ts index da7ec364..01ca700f 100644 --- a/playwright/access.spec.ts +++ b/playwright/access.spec.ts @@ -7,6 +7,8 @@ Please see LICENSE in the repository root for full details. import { expect, test } from "@playwright/test"; +import { SpaHelpers } from "./spa-helpers.ts"; + test("Sign up a new account, then login, then logout", async ({ browser }) => { const userId = `test_user-id_${Date.now()}`; @@ -69,12 +71,7 @@ test("As a guest, create a call, share link and other join", async ({ // ======== // ARRANGE: The first user creates a call as guest, join it, then click the invite button to copy the invite link // ======== - await creatorPage.getByTestId("home_callName").click(); - await creatorPage.getByTestId("home_callName").fill("Welcome"); - await creatorPage.getByTestId("home_displayName").click(); - await creatorPage.getByTestId("home_displayName").fill("Inviter"); - await creatorPage.getByTestId("home_go").click(); - await expect(creatorPage.locator("video")).toBeVisible(); + await SpaHelpers.createCall(creatorPage, "Inviter", "Welcome"); // join await creatorPage.getByTestId("lobby_joinCall").click(); @@ -82,19 +79,7 @@ test("As a guest, create a call, share link and other join", async ({ await creatorPage.getByRole("radio", { name: "Spotlight" }).check(); // Get the invite link - await creatorPage.getByRole("button", { name: "Invite" }).click(); - await expect( - creatorPage.getByRole("heading", { name: "Invite to this call" }), - ).toBeVisible(); - await expect(creatorPage.getByRole("img", { name: "QR Code" })).toBeVisible(); - await expect(creatorPage.getByTestId("modal_inviteLink")).toBeVisible(); - await expect(creatorPage.getByTestId("modal_inviteLink")).toBeVisible(); - await creatorPage.getByTestId("modal_inviteLink").click(); - - const inviteLink = (await creatorPage.evaluate( - "navigator.clipboard.readText()", - )) as string; - expect(inviteLink).toContain("room/#/"); + const inviteLink = await SpaHelpers.getCallInviteLink(creatorPage); // ======== // ACT: The other user use the invite link to join the call as a guest @@ -103,13 +88,7 @@ test("As a guest, create a call, share link and other join", async ({ reducedMotion: "reduce", }); const guestPage = await guestInviteeContext.newPage(); - - await guestPage.goto(inviteLink); - await guestPage.getByTestId("joincall_displayName").fill("Invitee"); - await expect(guestPage.getByTestId("joincall_joincall")).toBeVisible(); - await guestPage.getByTestId("joincall_joincall").click(); - await guestPage.getByTestId("lobby_joinCall").click(); - await guestPage.getByRole("radio", { name: "Spotlight" }).check(); + await SpaHelpers.joinCallFromInviteLink(guestPage, inviteLink); // ======== // ASSERT: check that there are two members in the call diff --git a/playwright/spa-call-sticky.spec.ts b/playwright/spa-call-sticky.spec.ts new file mode 100644 index 00000000..58e9c997 --- /dev/null +++ b/playwright/spa-call-sticky.spec.ts @@ -0,0 +1,40 @@ +/* +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 { test } from "@playwright/test"; + +import { SpaHelpers } from "./spa-helpers"; + +test("One to One call using matrix rtc 2.0 aka sticky events", async ({ + browser, + page, + browserName, +}) => { + test.skip( + browserName === "firefox", + "The is test is not working on firefox CI environment. No mic/audio device inputs so cam/mic are disabled", + ); + + await page.goto("/"); + + await SpaHelpers.createCall(page, "John Doe", "HelloCall", true, "2_0"); + + const inviteLink = await SpaHelpers.getCallInviteLink(page); + + // Other + const guestInviteeContext = await browser.newContext({ + reducedMotion: "reduce", + }); + const guestPage = await guestInviteeContext.newPage(); + + await guestPage.goto("/"); + + await SpaHelpers.joinCallFromInviteLink(guestPage, inviteLink); + + await SpaHelpers.expectVideoTilesCount(page, 2); + await SpaHelpers.expectVideoTilesCount(guestPage, 2); +}); diff --git a/playwright/spa-helpers.ts b/playwright/spa-helpers.ts new file mode 100644 index 00000000..648f6e6e --- /dev/null +++ b/playwright/spa-helpers.ts @@ -0,0 +1,148 @@ +/* +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 Page } from "@playwright/test"; + +import { type RtcMode } from "./widget/test-helpers.ts"; + +/** + * Create and join a call from the SPA home page. + * + * @param page - The Playwright page object + * @param userName - The display name to use for the call + * @param callName - The name of the call to create + * @param autoJoin - Whether to automatically join the call after creating it + * @param mode - The RTC mode to use for the call + */ +async function createCall( + page: Page, + userName: string, + callName: string, + autoJoin: boolean = false, + mode: RtcMode | undefined = undefined, +): Promise { + await page.getByTestId("home_callName").click(); + await page.getByTestId("home_callName").fill(callName); + await page.getByTestId("home_displayName").click(); + await page.getByTestId("home_displayName").fill(userName); + await page.getByTestId("home_go").click(); + + await expect(page.locator("video")).toBeVisible(); + await expect(page.getByTestId("lobby_joinCall")).toBeVisible(); + + if (mode) { + await setRtcModeFromSettings(page, mode); + } + + if (autoJoin) { + // Join the call + await page.getByTestId("lobby_joinCall").click(); + } +} + +/** + * Get the invite link for the current call. + */ +async function getCallInviteLink(page: Page): Promise { + await page.getByRole("button", { name: "Invite" }).click(); + await expect( + page.getByRole("heading", { name: "Invite to this call" }), + ).toBeVisible(); + await expect(page.getByRole("img", { name: "QR Code" })).toBeVisible(); + await expect(page.getByTestId("modal_inviteLink")).toBeVisible(); + await expect(page.getByTestId("modal_inviteLink")).toBeVisible(); + await page.getByTestId("modal_inviteLink").click(); + + const inviteLink = (await page.evaluate( + "navigator.clipboard.readText()", + )) as string; + expect(inviteLink).toContain("room/#/"); + + return inviteLink; +} + +/** + * Join a call from an invitation link. + * @param page - The Playwright page object + * @param inviteLink - The invite link to join + * @param displayName - The display name to use when joining the call + * @param mode - The RTC mode to use for the call + */ +async function joinCallFromInviteLink( + page: Page, + inviteLink: string, + displayName: string = "Invitee", + mode: RtcMode | undefined = undefined, +): Promise { + await page.goto(inviteLink); + await page.getByTestId("joincall_displayName").fill(displayName); + await expect(page.getByTestId("joincall_joincall")).toBeVisible(); + await page.getByTestId("joincall_joincall").click(); + + if (mode) { + await setRtcModeFromSettings(page, mode); + } + + await page.getByTestId("lobby_joinCall").click(); + await page.getByRole("radio", { name: "Spotlight" }).check(); +} + +async function setRtcModeFromSettings( + page: Page, + mode: RtcMode, +): Promise { + await page.getByRole("button", { name: "Settings" }).click(); + await page.getByRole("tab", { name: "Preferences" }).click(); + await page.getByText("Developer mode", { exact: true }).check(); // Idempotent: won't uncheck if already checked + + // Move to Developer tab now + await page.getByRole("tab", { name: "Developer" }).click(); + if (mode == "legacy") { + await page.getByText("Legacy: state events").click(); + } else if (mode == "2_0") { + await page.getByText("Matrix 2.0").click(); + } else { + // compat + await page.getByText("Compatibility: state events").click(); + } + + await page.getByTestId("modal_close").click(); +} + +/** + * Expect a certain number of video tiles to be present and visible. + */ +async function expectVideoTilesCount(page: Page, count: number): Promise { + await expect(page.getByTestId("videoTile")).toHaveCount(2); + + // There are no other options than to wait for all media to be ready? + // Or it is too flaky :/ + await page.waitForTimeout(3000); + // No one should be waiting for media + await expect(page.getByText("Waiting for media...")).not.toBeVisible(); + + // There should be 5 video elements, visible and autoplaying + const videoElements = await page.locator("video").all(); + expect(videoElements.length).toBe(count); + + const blockDisplayCount = await page + .locator("video") + .evaluateAll( + (videos: Element[]) => + videos.filter( + (v: Element) => window.getComputedStyle(v).display === "block", + ).length, + ); + expect(blockDisplayCount).toBe(count); +} + +export const SpaHelpers = { + createCall, + getCallInviteLink, + joinCallFromInviteLink, + expectVideoTilesCount, +};