diff --git a/backend/dev_homeserver-othersite.yaml b/backend/dev_homeserver-othersite.yaml index 81e775ca..7eb8f294 100644 --- a/backend/dev_homeserver-othersite.yaml +++ b/backend/dev_homeserver-othersite.yaml @@ -50,6 +50,9 @@ max_event_delay_duration: 24h enable_registration: true enable_registration_without_verification: true +# Shared secret for admin user registration via API (for testing only!) +registration_shared_secret: "test_shared_secret_for_local_dev_only" + report_stats: false serve_server_wellknown: true diff --git a/backend/dev_homeserver.yaml b/backend/dev_homeserver.yaml index dc7b42c8..0aea2ece 100644 --- a/backend/dev_homeserver.yaml +++ b/backend/dev_homeserver.yaml @@ -50,6 +50,9 @@ max_event_delay_duration: 24h enable_registration: true enable_registration_without_verification: true +# Shared secret for admin user registration via API (for testing only!) +registration_shared_secret: "test_shared_secret_for_local_dev_only" + report_stats: false serve_server_wellknown: true diff --git a/backend/dev_nginx.conf b/backend/dev_nginx.conf index bfd79fa2..834adaeb 100644 --- a/backend/dev_nginx.conf +++ b/backend/dev_nginx.conf @@ -34,6 +34,13 @@ server { proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header Host $host; } + location ~ ^(/_matrix|/_synapse/admin) { + proxy_pass "http://homeserver:8008"; + proxy_http_version 1.1; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Host $host; + } error_page 500 502 503 504 /50x.html; @@ -76,6 +83,14 @@ server { proxy_set_header Host $host; } + location ~ ^(/_matrix|/_synapse/admin) { + proxy_pass "http://homeserver-1:18008"; + proxy_http_version 1.1; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Host $host; + } + error_page 500 502 503 504 /50x.html; } diff --git a/backend/playwright_homeserver-othersite.yaml b/backend/playwright_homeserver-othersite.yaml index 35640ae9..86c77b35 100644 --- a/backend/playwright_homeserver-othersite.yaml +++ b/backend/playwright_homeserver-othersite.yaml @@ -50,6 +50,9 @@ max_event_delay_duration: 24h enable_registration: true enable_registration_without_verification: true +# Shared secret for admin user registration via API (for testing only!) +registration_shared_secret: "test_shared_secret_for_local_dev_only" + report_stats: false serve_server_wellknown: true diff --git a/backend/playwright_homeserver.yaml b/backend/playwright_homeserver.yaml index a83247cd..8f437524 100644 --- a/backend/playwright_homeserver.yaml +++ b/backend/playwright_homeserver.yaml @@ -50,6 +50,9 @@ max_event_delay_duration: 24h enable_registration: true enable_registration_without_verification: true +# Shared secret for admin user registration via API (for testing only!) +registration_shared_secret: "test_shared_secret_for_local_dev_only" + report_stats: false serve_server_wellknown: true diff --git a/playwright.config.ts b/playwright.config.ts index 4fb86b95..84afed64 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -7,11 +7,22 @@ Please see LICENSE in the repository root for full details. */ import { defineConfig, devices } from "@playwright/test"; +import { join } from "path"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; const baseURL = process.env.USE_DOCKER ? "http://localhost:8080" : "https://localhost:3000"; +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +// Needed by the synapse admin API called in fixtures +process.env.NODE_EXTRA_CA_CERTS = join( + __dirname, + "backend/dev_tls_local-ca.crt", +); + /** * See https://playwright.dev/docs/test-configuration. */ diff --git a/playwright/fixtures/widget-user.ts b/playwright/fixtures/widget-user.ts index cdb683ee..68aef8d9 100644 --- a/playwright/fixtures/widget-user.ts +++ b/playwright/fixtures/widget-user.ts @@ -91,7 +91,9 @@ export const widgetTest = test.extend({ await ewPage1 .getByRole("button", { name: "Invite to this room", exact: true }) - .click(); + .click({ + timeout: 10000, + }); await expect( ewPage1.getByRole("heading", { name: "Invite to Welcome Room" }), ).toBeVisible(); diff --git a/playwright/mobile/create-call-mobile.spec.ts b/playwright/mobile/create-call-mobile.spec.ts index b66ad6c4..1d9d3af0 100644 --- a/playwright/mobile/create-call-mobile.spec.ts +++ b/playwright/mobile/create-call-mobile.spec.ts @@ -53,7 +53,6 @@ test("@mobile Start a new call then leave and show the feedback screen", async ( mobileTest( "Test earpiece overlay in controlledAudioDevices mode", async ({ asMobile, browser }) => { - test.slow(); // Triples the timeout const { creatorPage, inviteLink } = asMobile; // ======== diff --git a/playwright/sfu-reconnect-bug.spec.ts b/playwright/sfu-reconnect-bug.spec.ts index 9f666f0f..9be4a3ac 100644 --- a/playwright/sfu-reconnect-bug.spec.ts +++ b/playwright/sfu-reconnect-bug.spec.ts @@ -9,7 +9,9 @@ import { expect, test } from "@playwright/test"; test("When creator left, avoid reconnect to the same SFU", async ({ browser, + browserName, }) => { + test.skip(browserName === "firefox", "Browser independent"); // Use reduce motion to disable animations that are making the tests a bit flaky const creatorContext = await browser.newContext({ reducedMotion: "reduce" }); const creatorPage = await creatorContext.newPage(); @@ -91,8 +93,10 @@ test("When creator left, avoid reconnect to the same SFU", async ({ // the creator leaves the call await creatorPage.getByTestId("incall_leave").click(); - await guestCPage.waitForTimeout(2000); // https://github.com/element-hq/element-call/issues/3344 // The app used to request a new jwt token then to reconnect to the SFU expect(wsConnectionCount).toBe(1); + // Wait a bit to be sure that if there was a reconnect, it would have happened by now + await guestCPage.waitForTimeout(6000); + expect(wsConnectionCount).toBe(1); }); diff --git a/playwright/spa-call-sticky.spec.ts b/playwright/spa-call-sticky.spec.ts index 328a65da..1dbda735 100644 --- a/playwright/spa-call-sticky.spec.ts +++ b/playwright/spa-call-sticky.spec.ts @@ -115,8 +115,12 @@ test("One to One rejoin after improper leave does not crash EC", async ({ await guestPage.getByTestId("lobby_joinCall").click(); // We cannot use the `expectVideoTilesCount` helper here since one of them is expected to show waiting for media - await expect(page.getByTestId("videoTile")).toHaveCount(3); - await expect(guestPage.getByTestId("videoTile")).toHaveCount(2); + await expect(page.getByTestId("videoTile")).toHaveCount(3, { + timeout: 10000, + }); + await expect(guestPage.getByTestId("videoTile")).toHaveCount(2, { + timeout: 10000, + }); }); function isStickySend(url: string): boolean { diff --git a/playwright/spa-helpers.ts b/playwright/spa-helpers.ts index 648f6e6e..46c414c9 100644 --- a/playwright/spa-helpers.ts +++ b/playwright/spa-helpers.ts @@ -119,25 +119,27 @@ async function setRtcModeFromSettings( 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(); + await expect(page.getByText("Waiting for media...")).not.toBeVisible({ + timeout: 10000, + }); - // There should be 5 video elements, visible and autoplaying - const videoElements = await page.locator("video").all(); - expect(videoElements.length).toBe(count); + // There should be `count` video elements, visible and autoplaying + await expect(page.locator("video")).toHaveCount(count); - const blockDisplayCount = await page - .locator("video") - .evaluateAll( - (videos: Element[]) => - videos.filter( - (v: Element) => window.getComputedStyle(v).display === "block", - ).length, - ); - expect(blockDisplayCount).toBe(count); + await expect(async () => { + const videoBlockCount = await page + .locator("video") + .evaluateAll( + (videos: Element[]) => + videos.filter( + (v: Element) => window.getComputedStyle(v).display === "block", + ).length, + ); + expect(videoBlockCount).toBe(count); + }).toPass({ + timeout: 10000, + }); } export const SpaHelpers = { diff --git a/playwright/utils/synapse-admin.ts b/playwright/utils/synapse-admin.ts new file mode 100644 index 00000000..b1d0039c --- /dev/null +++ b/playwright/utils/synapse-admin.ts @@ -0,0 +1,142 @@ +/* +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 { createHmac } from "crypto"; + +/** + * Response from Synapse registration API + */ +export interface SynapseRegistrationResponse { + access_token: string; + user_id: string; + home_server: string; + device_id: string; +} + +/** + * Utility class for interacting with Synapse Admin API + * This provides fast user registration without going through the UI + * + * @see https://matrix-org.github.io/synapse/latest/admin_api/register_api.html + */ +export class SynapseAdmin { + public constructor( + private baseUrl: string = "https://synapse.m.localhost", + private sharedSecret: string = "test_shared_secret_for_local_dev_only", + ) {} + + /** + * Register a user using the Synapse Admin API + * This is much faster than going through the UI registration flow + * + * @param username - The username (localpart) for the new user + * @param password - The password for the new user + * @param displayName - Optional display name (defaults to username) + * @param admin - Whether the user should be an admin (defaults to false) + * @returns Registration response containing access token and user ID + */ + public async registerUser( + username: string, + password: string, + displayName?: string, + admin: boolean = false, + ): Promise { + // Get a nonce first + const nonce = await this.getNonce(); + + // Generate the HMAC + const mac = this.generateMac(username, password, admin, nonce); + + // Make the registration request + const response = await fetch(`${this.baseUrl}/_synapse/admin/v1/register`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + nonce, + username, + password, + displayname: displayName || username, + admin, + mac, + }), + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error( + `Failed to register user ${username}: ${response.status} ${error}`, + ); + } + + return response.json(); + } + + /** + * Get a nonce for registration + * The nonce is required for the HMAC calculation + * + * @returns A nonce string + */ + private async getNonce(): Promise { + const response = await fetch(`${this.baseUrl}/_synapse/admin/v1/register`, { + method: "GET", + }); + + if (!response.ok) { + throw new Error( + `Failed to get nonce: ${response.status} ${await response.text()}`, + ); + } + + const data = await response.json(); + return data.nonce; + } + + /** + * Generate HMAC for shared secret registration + * This is the authentication mechanism for the admin API + * + * @param username - The username + * @param password - The password + * @param admin - Whether the user is an admin + * @param nonce - The nonce from the server + * @returns The HMAC hex string + */ + private generateMac( + username: string, + password: string, + admin: boolean, + nonce: string, + ): string { + const mac = createHmac("sha1", this.sharedSecret); + mac.update(nonce); + mac.update("\x00"); + mac.update(username); + mac.update("\x00"); + mac.update(password); + mac.update("\x00"); + mac.update(admin ? "admin" : "notadmin"); + + return mac.digest("hex"); + } + + /** + * Create a new SynapseAdmin instance for a different homeserver + * + * @param baseUrl - The base URL of the homeserver + * @param sharedSecret - The shared secret (defaults to test secret) + * @returns A new SynapseAdmin instance + */ + public static forHomeserver( + baseUrl: string, + sharedSecret: string = "test_shared_secret_for_local_dev_only", + ): SynapseAdmin { + return new SynapseAdmin(baseUrl, sharedSecret); + } +} diff --git a/playwright/widget/federated-call.test.ts b/playwright/widget/federated-call.test.ts index fda58250..560636a5 100644 --- a/playwright/widget/federated-call.test.ts +++ b/playwright/widget/federated-call.test.ts @@ -26,8 +26,12 @@ modePairs.forEach(([rtcMode1, rtcMode2]) => { "The is test is not working on firefox CI environment. No mic/audio device inputs so cam/mic are disabled", ); - const florian = await addUser("floriant", HOST1); - const timo = await addUser("timo", HOST2); + test.slow(); + + const [florian, timo] = await Promise.all([ + addUser("florian", HOST1), + addUser("timo", HOST2), + ]); const roomName = "Call Room"; @@ -57,27 +61,20 @@ modePairs.forEach(([rtcMode1, rtcMode2]) => { const frame = user.page .locator('iframe[title="Element Call"]') .contentFrame(); - await expect(frame.getByTestId("videoTile")).toHaveCount(2); + await expect(frame.getByTestId("videoTile")).toHaveCount(2, { + timeout: 10000, + }); - // There are no other options than to wait for all media to be ready? - // Or it is too flaky :/ - await user.page.waitForTimeout(3000); // No one should be waiting for media - await expect(frame.getByText("Waiting for media...")).not.toBeVisible(); + await expect(frame.getByText("Waiting for media...")).not.toBeVisible({ + timeout: 10000, + }); // There should be 2 video elements, visible and autoplaying const videoElements = await frame.locator("video").all(); expect(videoElements.length).toBe(2); - const blockDisplayCount = await frame - .locator("video") - .evaluateAll( - (videos: Element[]) => - videos.filter( - (v: Element) => window.getComputedStyle(v).display === "block", - ).length, - ); - expect(blockDisplayCount).toBe(2); + await TestHelpers.expectVisibleVideoCount(frame, 2); } // await florian.page.pause(); diff --git a/playwright/widget/federation-oldest-membership-bug.spec.ts b/playwright/widget/federation-oldest-membership-bug.spec.ts index 70442e05..ab5c70fc 100644 --- a/playwright/widget/federation-oldest-membership-bug.spec.ts +++ b/playwright/widget/federation-oldest-membership-bug.spec.ts @@ -75,18 +75,11 @@ widgetTest( await expect(frame.getByText("Waiting for media...")).not.toBeVisible(); // There should be 2 video elements, visible and autoplaying - const videoElements = await frame.locator("video").all(); - expect(videoElements.length).toBe(2); + await expect(frame.locator("video")).toHaveCount(2, { + timeout: 10000, + }); - const blockDisplayCount = await frame - .locator("video") - .evaluateAll( - (videos: Element[]) => - videos.filter( - (v: Element) => window.getComputedStyle(v).display === "block", - ).length, - ); - expect(blockDisplayCount).toBe(2); + await TestHelpers.expectVisibleVideoCount(frame, 2); } }, ); diff --git a/playwright/widget/hotswap-legacy-compat.test.ts b/playwright/widget/hotswap-legacy-compat.test.ts index e4695624..ed6f1508 100644 --- a/playwright/widget/hotswap-legacy-compat.test.ts +++ b/playwright/widget/hotswap-legacy-compat.test.ts @@ -27,6 +27,7 @@ import { HOST1, HOST2, TestHelpers } from "./test-helpers"; widgetTest( `Test swapping publisher from ${HOST1} to ${HOST2}`, async ({ addUser, browserName }) => { + test.slow(); test.skip( browserName === "firefox", "The is test is not working on firefox CI environment. No mic/audio device inputs so cam/mic are disabled", @@ -65,40 +66,26 @@ widgetTest( .contentFrame(); await expect(frame.getByTestId("videoTile")).toHaveCount(2); - // There are no other options than to wait for all media to be ready? - // Or it is too flaky :/ - await user.page.waitForTimeout(3000); - // No one should be waiting for media - await expect(frame.getByText("Waiting for media...")).not.toBeVisible(); + // Wait for "Waiting for media..." to disappear (with timeout) + await expect(frame.getByText("Waiting for media...")).not.toBeVisible({ + timeout: 10000, // Maximum time to wait + }); // There should be 2 video elements, visible and autoplaying - const videoElements = await frame.locator("video").all(); - expect(videoElements.length).toBe(2); + await expect(frame.locator("video")).toHaveCount(2, { + timeout: 10000, + }); - const blockDisplayCount = await frame - .locator("video") - .evaluateAll( - (videos: Element[]) => - videos.filter( - (v: Element) => window.getComputedStyle(v).display === "block", - ).length, - ); - expect(blockDisplayCount).toBe(2); + await TestHelpers.expectVisibleVideoCount(frame, 2); } // now we switch the mode for timo (second joiner on multi-sfu HOST2 but currently HOST1) await TestHelpers.setEmbeddedElementCallRtcMode(timo.page, "compat"); await timo.page.waitForTimeout(3000); - const blockDisplayCount = await timo.page - .locator('iframe[title="Element Call"]') - .contentFrame() - .locator("video") - .evaluateAll( - (videos: Element[]) => - videos.filter( - (v: Element) => window.getComputedStyle(v).display === "block", - ).length, - ); - expect(blockDisplayCount).toBe(2); + + await TestHelpers.expectVisibleVideoCount( + timo.page.locator('iframe[title="Element Call"]').contentFrame(), + 2, + ); }, ); diff --git a/playwright/widget/huddle-call.test.ts b/playwright/widget/huddle-call.test.ts index 9c7cfc80..68e1ba54 100644 --- a/playwright/widget/huddle-call.test.ts +++ b/playwright/widget/huddle-call.test.ts @@ -11,18 +11,21 @@ import { widgetTest } from "../fixtures/widget-user.ts"; import { HOST1, TestHelpers } from "./test-helpers.ts"; widgetTest("Create and join a group call", async ({ addUser, browserName }) => { + // increase the timeouts, it is a long test and it is annoying to retry from the beginning for a single timeout. + test.slow(); + test.skip( browserName === "firefox", "The is test is not working on firefox CI environment. No mic/audio device inputs so cam/mic are disabled", ); - test.slow(); // We are registering multiple users here, give it more time - - const valere = await addUser("Valere", HOST1); - const timo = await addUser("Timo", HOST1); - const robin = await addUser("Robin", HOST1); - const halfshot = await addUser("Halfshot", HOST1); - const florian = await addUser("florian", HOST1); + const [valere, timo, robin, halfshot, florian] = await Promise.all([ + addUser("Valere", HOST1), + addUser("Timo", HOST1), + addUser("Robin", HOST1), + addUser("Halfshot", HOST1), + addUser("florian", HOST1), + ]); const roomName = "Group Call Room"; await TestHelpers.createRoom(roomName, valere.page, [ @@ -47,52 +50,55 @@ widgetTest("Create and join a group call", async ({ addUser, browserName }) => { await TestHelpers.joinCallFromLobby(valere.page); - for (const user of [timo, robin, halfshot, florian]) { - await TestHelpers.joinCallInCurrentRoom(user.page); - } + await Promise.all( + [timo, robin, halfshot, florian].map(async (user) => { + await TestHelpers.joinCallInCurrentRoom(user.page); + }), + ); - for (const user of [timo, robin, halfshot, florian]) { - const frame = user.page - .locator('iframe[title="Element Call"]') - .contentFrame(); - // No lobby, should start with video on - await expect( - frame.getByRole("switch", { name: "Stop video", checked: true }), - ).toBeVisible(); - } + await Promise.all( + [timo, robin, halfshot, florian].map(async (user) => { + const frame = user.page + .locator('iframe[title="Element Call"]') + .contentFrame(); + await expect( + frame.getByRole("switch", { name: "Stop video", checked: true }), + ).toBeVisible({ + timeout: 10000, + }); + }), + ); // We should see 5 video tiles everywhere now - for (const user of [valere, timo, robin, halfshot, florian]) { - const frame = user.page - .locator('iframe[title="Element Call"]') - .contentFrame(); - await expect(frame.getByTestId("videoTile")).toHaveCount(5); - for (const participant of [valere, timo, robin, halfshot, florian]) { - // Check the names are correct - await expect(frame.getByText(participant.displayName)).toBeVisible(); - } + await Promise.all( + [valere, timo, robin, halfshot, florian].map(async (user) => { + const frame = user.page + .locator('iframe[title="Element Call"]') + .contentFrame(); + await expect(frame.getByTestId("videoTile")).toHaveCount(5, { + timeout: 15000, + }); - // There is no other options than to wait for all media to be ready? - // Or it is too flaky :/ - await user.page.waitForTimeout(5000); - // No one should be waiting for media - await expect(frame.getByText("Waiting for media...")).not.toBeVisible(); - - // There should be 5 video elements, visible and autoplaying - const videoElements = await frame.locator("video").all(); - expect(videoElements.length).toBe(5); - await expect(frame.locator("video[autoplay]")).toHaveCount(5); - - const blockDisplayCount = await frame - .locator("video") - .evaluateAll( - (videos: Element[]) => - videos.filter( - (v: Element) => window.getComputedStyle(v).display === "block", - ).length, + await Promise.all( + [valere, timo, robin, halfshot, florian].map(async (user) => { + // Check the names are correct + await expect(frame.getByText(user.displayName)).toBeVisible(); + }), ); - expect(blockDisplayCount).toBe(5); - } + + // No one should be waiting for media + await expect(frame.getByText("Waiting for media...")).not.toBeVisible({ + // Use a bigger timeout here + timeout: 10000, + }); + + // There should be 5 video elements, visible and autoplaying + await expect(frame.locator("video")).toHaveCount(5); + await expect(frame.locator("video[autoplay]")).toHaveCount(5); + + await TestHelpers.expectVisibleVideoCount(frame, 5); + }), + ); // Quickly test muting one participant to see it reflects and that our asserts works const florianFrame = florian.page @@ -108,28 +114,16 @@ widgetTest("Create and join a group call", async ({ addUser, browserName }) => { await expect(florianVideoButton).toHaveAccessibleName("Start video"); await expect(florianVideoButton).not.toBeChecked(); - // wait a bit for the state to propagate - await valere.page.waitForTimeout(3000); { const frame = valere.page .locator('iframe[title="Element Call"]') .contentFrame(); - const videoElements = await frame.locator("video").all(); - expect(videoElements.length).toBe(5); - - const blockDisplayCount = await frame - .locator("video") - .evaluateAll( - (videos: Element[]) => - videos.filter( - (v: Element) => window.getComputedStyle(v).display === "block", - ).length, - ); + await expect(frame.locator("video")).toHaveCount(5, { + timeout: 10000, + }); // out of 5 ONLY 4 are visible (display:block) !! - // XXX we need to be better at our HTML markup and accessibility, it would make - // this kind of stuff way easier to test if we could look out for aria attributes. - expect(blockDisplayCount).toBe(4); + await TestHelpers.expectVisibleVideoCount(frame, 4); } }); diff --git a/playwright/widget/pip-call-button-interaction.test.ts b/playwright/widget/pip-call-button-interaction.test.ts index 5e59f822..95aa4196 100644 --- a/playwright/widget/pip-call-button-interaction.test.ts +++ b/playwright/widget/pip-call-button-interaction.test.ts @@ -16,8 +16,6 @@ widgetTest("Footer interaction in PiP", async ({ addUser, browserName }) => { "The is test is not working on firefox CI environment. No mic/audio device inputs so cam/mic are disabled", ); - test.slow(); - const valere = await addUser("Valere", HOST1); const callRoom = "CallRoom"; diff --git a/playwright/widget/pip-call.test.ts b/playwright/widget/pip-call.test.ts index 63ba2050..b18252c1 100644 --- a/playwright/widget/pip-call.test.ts +++ b/playwright/widget/pip-call.test.ts @@ -47,7 +47,10 @@ widgetTest("Put call in PIP", async ({ addUser, browserName }) => { // check that the video is on await expect( frame.getByRole("switch", { name: "Stop video", checked: true }), - ).toBeVisible(); + ).toBeVisible({ + // Increase timeout, as this expect was flaky + timeout: 15000, + }); // Switch to the other room, the call should go to PIP await TestHelpers.switchToRoomNamed(valere.page, "DoubleTask"); @@ -63,8 +66,10 @@ widgetTest("Put call in PIP", async ({ addUser, browserName }) => { const frame = valere.page .locator('iframe[title="Element Call"]') .contentFrame(); + + await expect(frame.locator("video")).toHaveCount(1, { timeout: 10000 }); + const videoElements = await frame.locator("video").all(); - expect(videoElements.length).toBe(1); const pipVideo = videoElements[0]; await expect(pipVideo).toHaveCSS("object-fit", "cover"); diff --git a/playwright/widget/screen-share.test.ts b/playwright/widget/screen-share.test.ts index c60ef697..c6b03c3f 100644 --- a/playwright/widget/screen-share.test.ts +++ b/playwright/widget/screen-share.test.ts @@ -18,9 +18,11 @@ widgetTest("Sharing screen in group call", async ({ addUser, browserName }) => { test.slow(); // We are registering multiple users here, give it more time - const alice = await addUser("Alice", HOST1); - const bob = await addUser("Bob", HOST1); - const carol = await addUser("Carol", HOST1); + const [alice, bob, carol] = await Promise.all([ + addUser("Alice", HOST1), + addUser("Bob", HOST1), + addUser("Carol", HOST1), + ]); const roomName = "Meeting Room"; await TestHelpers.createRoom(roomName, alice.page, [bob.mxId, carol.mxId]); @@ -50,7 +52,7 @@ widgetTest("Sharing screen in group call", async ({ addUser, browserName }) => { // Expect 3 video tiles await expect(frame.locator("video")).toHaveCount(3, { - timeout: 5000, + timeout: 10000, }); } diff --git a/playwright/widget/simple-create.spec.ts b/playwright/widget/simple-create.spec.ts index 4686e99d..31afb31e 100644 --- a/playwright/widget/simple-create.spec.ts +++ b/playwright/widget/simple-create.spec.ts @@ -17,7 +17,7 @@ widgetTest.skip( ); widgetTest("Start a new call as widget", async ({ asWidget, browserName }) => { - test.slow(); // Triples the timeout + test.slow(); const { brooks, whistler } = asWidget; @@ -83,8 +83,12 @@ widgetTest("Start a new call as widget", async ({ asWidget, browserName }) => { .locator('iframe[title="Element Call"]') .contentFrame() .getByTestId("incall_leave") - .click(); + .click({ timeout: 15000 }); - await expect(whistler.page.locator(".mx_BasicMessageComposer")).toBeVisible(); - await expect(brooks.page.locator(".mx_BasicMessageComposer")).toBeVisible(); + await expect(whistler.page.locator(".mx_BasicMessageComposer")).toBeVisible({ + timeout: 10000, + }); + await expect(brooks.page.locator(".mx_BasicMessageComposer")).toBeVisible({ + timeout: 10000, + }); }); diff --git a/playwright/widget/test-helpers.ts b/playwright/widget/test-helpers.ts index 37cb2583..b47588ce 100644 --- a/playwright/widget/test-helpers.ts +++ b/playwright/widget/test-helpers.ts @@ -10,9 +10,12 @@ import { expect, type JSHandle, type Page, + type FrameLocator, } from "@playwright/test"; import { type MatrixClient } from "matrix-js-sdk"; +import { SynapseAdmin } from "../utils/synapse-admin.ts"; + const PASSWORD = "foobarbaz1!"; export const HOST1 = "https://app.m.localhost/#/welcome"; @@ -26,14 +29,14 @@ export class TestHelpers { voice: boolean = false, ): Promise { const buttonName = voice ? "Voice call" : "Video call"; - await expect(page.getByRole("button", { name: buttonName })).toBeVisible(); - await page.getByRole("button", { name: buttonName }).click(); - await expect( - page.getByRole("menuitem", { name: "Element Call" }), - ).toBeVisible(); + await page.getByRole("button", { name: buttonName }).click({ + timeout: 5000, + }); - await page.getByRole("menuitem", { name: "Element Call" }).click(); + await page.getByRole("menuitem", { name: "Element Call" }).click({ + timeout: 10000, + }); } public static async joinCallFromLobby(page: Page): Promise { @@ -57,9 +60,12 @@ export class TestHelpers { ): Promise { // This is the header button that notifies about an ongoing call const label = audioOnly ? "Voice call started" : "Video call started"; - await expect(page.getByText(label)).toBeVisible(); - await expect(page.getByRole("button", { name: "Join" })).toBeVisible(); - await page.getByRole("button", { name: "Join" }).click(); + await expect(page.getByText(label)).toBeVisible({ + timeout: 10000, + }); + await page.getByRole("button", { name: "Join" }).click({ + timeout: 5000, + }); } /** @@ -74,28 +80,44 @@ export class TestHelpers { clientHandle: JSHandle; mxId: string; }> { + // Determine which homeserver to use based on the host + const synapseBaseUrl = + host === HOST2 + ? "https://synapse.othersite.m.localhost" + : "https://synapse.m.localhost"; + + // Register user via Synapse Admin API to speed things up + const synapseAdmin = SynapseAdmin.forHomeserver(synapseBaseUrl); + const credentials = await synapseAdmin.registerUser( + username, + PASSWORD, + username, + ); + + // STEP 2: Open browser and login const userContext = await browser.newContext({ reducedMotion: "reduce", }); const page = await userContext.newPage(); await page.goto(host); - await page.getByRole("link", { name: "Create Account" }).click(); - await page.getByRole("textbox", { name: "Username" }).fill(username); - await page.getByRole("textbox", { name: "Password", exact: true }).click(); - await page - .getByRole("textbox", { name: "Password", exact: true }) - .fill(PASSWORD); - await page.getByRole("textbox", { name: "Confirm password" }).click(); - await page - .getByRole("textbox", { name: "Confirm password" }) - .fill(PASSWORD); - await page.getByRole("button", { name: "Register" }).click(); + + await page.getByRole("link", { name: "Sign in" }).click({ + timeout: 10000, + }); + + await page.getByRole("textbox", { name: "Username" }).fill(username, { + timeout: 10000, + }); + await page.getByRole("textbox", { name: "Password" }).fill(PASSWORD, { + timeout: 10000, + }); + await page.getByRole("button", { name: "Sign in" }).click(); await expect( page.getByRole("heading", { name: `Welcome ${username}` }), ).toBeVisible({ - // Increase timeout as registration can be slow :/ - timeout: 15_000, + // Increase timeout here :/ flaky + timeout: 15000, }); await this.maybeDismissBrowserNotSupportedToast(page); @@ -106,11 +128,7 @@ export class TestHelpers { const clientHandle = await page.evaluateHandle(() => window.mxMatrixClientPeg.get(), ); - const mxId = (await clientHandle.evaluate( - (cli: MatrixClient) => cli.getUserId(), - clientHandle, - ))!; - + const mxId = credentials.user_id; return { page, clientHandle, mxId }; } @@ -178,10 +196,14 @@ export class TestHelpers { .getByRole("button", { name: "New conversation" }) .click(); - await page.getByRole("menuitem", { name: "New Room" }).click(); + await page.getByRole("menuitem", { name: "New Room" }).click({ + timeout: 5000, + }); await page.getByRole("textbox", { name: "Name" }).fill(name); await page.getByRole("button", { name: "Create room" }).click(); - await expect(page.getByText("You created this room.")).toBeVisible(); + await expect(page.getByText("You created this room.")).toBeVisible({ + timeout: 10000, + }); await expect(page.getByText("Encryption enabled")).toBeVisible(); await TestHelpers.maybeDismissKeyBackupToast(page); @@ -212,9 +234,12 @@ export class TestHelpers { roomName: string, page: Page, ): Promise { - await expect(page.getByRole("option", { name: roomName })).toBeVisible(); - await page.getByRole("option", { name: roomName }).click(); - await page.getByRole("button", { name: "Accept" }).click(); + await page.getByRole("option", { name: roomName }).click({ + timeout: 10000, + }); + await page.getByRole("button", { name: "Accept" }).click({ + timeout: 5000, + }); await expect( page.getByRole("main").getByRole("heading", { name: roomName }), @@ -234,8 +259,12 @@ export class TestHelpers { page: Page, mode: RtcMode, ): Promise { - await page.getByRole("button", { name: "Video call" }).click(); - await page.getByRole("menuitem", { name: "Element Call" }).click(); + await page.getByRole("button", { name: "Video call" }).click({ + timeout: 5000, + }); + await page.getByRole("menuitem", { name: "Element Call" }).click({ + timeout: 10000, + }); await TestHelpers.setEmbeddedElementCallRtcMode(page, mode); await page.getByRole("button", { name: "Close lobby" }).click(); @@ -331,4 +360,30 @@ export class TestHelpers { timeout: 5000, }); } + + public static async expectVisibleVideoCount( + frame: FrameLocator, + count: number, + ): Promise { + // XXX we need to be better at our HTML markup and accessibility, it would make + // this kind of stuff way easier to test if we could look out for aria attributes. + await expect + .poll( + async () => { + return await frame + .locator("video") + .evaluateAll( + (videos: Element[]) => + videos.filter( + (v: Element) => + window.getComputedStyle(v).display === "block", + ).length, + ); + }, + { + timeout: 10000, + }, + ) + .toBe(count); + } } diff --git a/playwright/widget/voice-call-dm.spec.ts b/playwright/widget/voice-call-dm.spec.ts index a4e6255b..7a13c58b 100644 --- a/playwright/widget/voice-call-dm.spec.ts +++ b/playwright/widget/voice-call-dm.spec.ts @@ -20,8 +20,6 @@ widgetTest( "The is test is not working on firefox CI environment. No mic/audio device inputs so cam/mic are disabled", ); - test.slow(); // Triples the timeout - const { brooks, whistler } = asWidget; await TestHelpers.startCallInCurrentRoom(brooks.page, true); @@ -113,8 +111,6 @@ widgetTest( "The is test is not working on firefox CI environment. No mic/audio device inputs so cam/mic are disabled", ); - test.slow(); // Triples the timeout - const { brooks, whistler } = asWidget; await TestHelpers.startCallInCurrentRoom(brooks.page, false); @@ -200,8 +196,6 @@ widgetTest( "The is test is not working on firefox CI environment. No mic/audio device inputs so cam/mic are disabled", ); - test.slow(); // Triples the timeout - const { brooks, whistler } = asWidget; await TestHelpers.startCallInCurrentRoom(brooks.page, false);