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_livekit-othersite.yaml b/backend/dev_livekit-othersite.yaml index 0ae98c24..53fc9ce9 100644 --- a/backend/dev_livekit-othersite.yaml +++ b/backend/dev_livekit-othersite.yaml @@ -18,3 +18,7 @@ keys: devkey: secret room: auto_create: false +webhook: + api_key: devkey + urls: + - https://matrix-rtc.othersite.m.localhost/livekit/jwt/sfu_webhook diff --git a/backend/dev_livekit.yaml b/backend/dev_livekit.yaml index 157e4d04..6cef4241 100644 --- a/backend/dev_livekit.yaml +++ b/backend/dev_livekit.yaml @@ -18,3 +18,7 @@ keys: devkey: secret room: auto_create: false +webhook: + api_key: devkey + urls: + - https://matrix-rtc.m.localhost/livekit/jwt/sfu_webhook 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/dev-backend-docker-compose.yml b/dev-backend-docker-compose.yml index 36fc7f44..702aef39 100644 --- a/dev-backend-docker-compose.yml +++ b/dev-backend-docker-compose.yml @@ -62,7 +62,10 @@ services: - 7882:7882/tcp - 50100-50200:50100-50200/udp volumes: + - ./backend/dev_tls_m.localhost.crt:/local_cert.pem:Z - ./backend/dev_livekit.yaml:/etc/livekit.yaml:Z + environment: + - SSL_CERT_FILE=/local_cert.pem networks: - ecbackend @@ -82,7 +85,10 @@ services: - 17882:17882/tcp - 50300-50400:50300-50400/udp volumes: + - ./backend/dev_tls_m.localhost.crt:/local_cert.pem:Z - ./backend/dev_livekit-othersite.yaml:/etc/livekit.yaml:Z + environment: + - SSL_CERT_FILE=/local_cert.pem networks: - ecbackend @@ -164,6 +170,8 @@ services: - "8448:8448" extra_hosts: - "host.docker.internal:host-gateway" + - "auth-server:127.0.0.1" + - "auth-server-1:127.0.0.1" depends_on: - synapse networks: diff --git a/package.json b/package.json index 87204c66..f2548099 100644 --- a/package.json +++ b/package.json @@ -81,7 +81,7 @@ "@typescript-eslint/parser": "^8.31.0", "@use-gesture/react": "^10.2.11", "@vector-im/compound-design-tokens": "^10.0.0", - "@vector-im/compound-web": "^9.0.0", + "@vector-im/compound-web": "^9.3.0", "@vitejs/plugin-react": "^4.0.1", "@vitest/coverage-v8": "^4.0.18", "babel-plugin-transform-vite-meta-env": "^1.0.3", @@ -132,7 +132,7 @@ "typescript": "^5.8.3", "typescript-eslint-language-service": "^5.0.5", "unique-names-generator": "^4.6.0", - "uuid": "^13.0.0", + "uuid": "^14.0.0", "vaul": "^1.0.0", "vite": "^8.0.0", "vite-plugin-generate-file": "^0.3.0", 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/errors.spec.ts b/playwright/errors.spec.ts index 607384f5..23f6e29f 100644 --- a/playwright/errors.spec.ts +++ b/playwright/errors.spec.ts @@ -75,9 +75,7 @@ test("Should automatically retry non fatal JWT errors", async ({ await expect(page.getByTestId("video").first()).toBeVisible(); }); -// We skip this test for now as it appears the livekit does not let us -// detect and handle NotAllowed errors anymore. https://github.com/livekit/client-sdk-js/issues/1883 -test.skip("Should show error screen if call creation is restricted", async ({ +test("Should show error screen if call creation is restricted", async ({ page, browserName, }) => { diff --git a/playwright/fixtures/widget-user.ts b/playwright/fixtures/widget-user.ts index 31422fcc..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(); @@ -104,6 +106,7 @@ export const widgetTest = test.extend({ await ewPage1.getByRole("dialog").getByRole("textbox").fill(whistlerMxId); await ewPage1.getByRole("dialog").getByRole("textbox").click(); await ewPage1.getByRole("button", { name: "Invite" }).click(); + await TestHelpers.dismissInviteUnknownUserModal(ewPage1); // Accept the invite await expect( @@ -126,6 +129,7 @@ export const widgetTest = test.extend({ await ewPage1.getByRole("textbox", { name: "Search" }).click(); await ewPage1.getByRole("textbox", { name: "Search" }).fill(whistlerMxId); await ewPage1.getByRole("button", { name: "Go" }).click(); + await TestHelpers.dismissInviteUnknownUserModalDM(ewPage1); // Wait and send the first message to create the DM await expect( 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 ff5fa7e2..9167412e 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,43 +80,56 @@ 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); await this.maybeDismissServiceWorkerWarningToast(page); + await this.maybeDismissBackupChat(page); await TestHelpers.setDevToolElementCallDevUrl(page); 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 }; } @@ -152,6 +171,22 @@ export class TestHelpers { } } + private static async maybeDismissBackupChat(page: Page): Promise { + const toast = page + .locator(".mx_Toast_toast") + .getByText("Back up your chats"); + + try { + await expect(toast).toBeVisible({ timeout: 700 }); + await page + .locator(".mx_Toast_toast") + .getByRole("button", { name: "Dismiss" }) + .click(); + } catch { + // toast not visible, continue as normal + } + } + public static async maybeDismissKeyBackupToast(page: Page): Promise { const toast = page .locator(".mx_Toast_toast") @@ -178,10 +213,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); @@ -199,6 +238,7 @@ export class TestHelpers { } await page.getByRole("button", { name: "Invite" }).click(); + await TestHelpers.dismissInviteUnknownUserModal(page); } } @@ -211,9 +251,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 }), @@ -233,8 +276,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(); @@ -308,4 +355,52 @@ export class TestHelpers { ): Promise { await page.getByRole("option", { name: `Open room ${roomName}` }).click(); } + + public static async dismissInviteUnknownUserModal(page: Page): Promise { + await expect( + page.getByRole("heading", { name: "Invite new contacts to this" }), + ).toBeVisible(); + await page.getByRole("button", { name: "Invite" }).click({ + timeout: 5000, + }); + } + + public static async dismissInviteUnknownUserModalDM( + page: Page, + ): Promise { + await expect( + page.getByRole("heading", { + name: "Start a chat with this new contact?", + }), + ).toBeVisible(); + await page.getByRole("button", { name: "Continue" }).click({ + 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); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fa9ebebb..84f0a087 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -47,16 +47,16 @@ importers: version: 11.7.12 '@livekit/components-core': specifier: ^0.12.0 - version: 0.12.13(livekit-client@2.18.3(@types/dom-mediacapture-record@1.0.22))(tslib@2.8.1) + version: 0.12.13(livekit-client@2.18.6(@types/dom-mediacapture-record@1.0.22))(tslib@2.8.1) '@livekit/components-react': specifier: ^2.0.0 - version: 2.9.20(livekit-client@2.18.3(@types/dom-mediacapture-record@1.0.22))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(tslib@2.8.1) + version: 2.9.20(livekit-client@2.18.6(@types/dom-mediacapture-record@1.0.22))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(tslib@2.8.1) '@livekit/protocol': specifier: ^1.42.2 version: 1.45.3 '@livekit/track-processors': specifier: ^0.7.1 - version: 0.7.2(@types/dom-mediacapture-transform@0.1.11)(livekit-client@2.18.3(@types/dom-mediacapture-record@1.0.22)) + version: 0.7.2(@types/dom-mediacapture-transform@0.1.11)(livekit-client@2.18.6(@types/dom-mediacapture-record@1.0.22)) '@mediapipe/tasks-vision': specifier: ^0.10.18 version: 0.10.34 @@ -148,8 +148,8 @@ importers: specifier: ^10.0.0 version: 10.1.0(@types/react@19.2.14)(react@19.2.5) '@vector-im/compound-web': - specifier: ^9.0.0 - version: 9.2.0(@fontsource/inconsolata@5.2.8)(@fontsource/inter@5.2.8)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(@vector-im/compound-design-tokens@10.1.0(@types/react@19.2.14)(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + specifier: ^9.3.0 + version: 9.3.0(@fontsource/inconsolata@5.2.8)(@fontsource/inter@5.2.8)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(@vector-im/compound-design-tokens@10.1.0(@types/react@19.2.14)(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@vitejs/plugin-react': specifier: ^4.0.1 version: 4.7.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.7)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)) @@ -227,7 +227,7 @@ importers: version: 5.88.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@24.12.2)(typescript@5.9.3) livekit-client: specifier: ^2.18.1 - version: 2.18.3(@types/dom-mediacapture-record@1.0.22) + version: 2.18.6(@types/dom-mediacapture-record@1.0.22) lodash-es: specifier: ^4.17.21 version: 4.18.1 @@ -236,7 +236,7 @@ importers: version: 1.9.2 matrix-js-sdk: specifier: matrix-org/matrix-js-sdk#develop - version: https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/4b33892d48017b3733a00a48cf5d30182be4a6fb + version: https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/bec985b783c666de24d6db92a4307a10416742d7 matrix-widget-api: specifier: ^1.16.1 version: 1.17.0 @@ -301,8 +301,8 @@ importers: specifier: ^4.6.0 version: 4.7.1 uuid: - specifier: ^13.0.0 - version: 13.0.0 + specifier: ^14.0.0 + version: 14.0.0 vaul: specifier: ^1.0.0 version: 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) @@ -1576,8 +1576,8 @@ packages: '@types/dom-mediacapture-transform': ^0.1.9 livekit-client: ^1.12.0 || ^2.1.0 - '@matrix-org/matrix-sdk-crypto-wasm@18.1.0': - resolution: {integrity: sha512-GxXK2U39+2qWNvR3fXJY7nxdikvpiT17RaS0/Dktk6R8FMKDk3vm79Hq65yrCWLBmT7pJZoerfILNZqhrcUHrg==} + '@matrix-org/matrix-sdk-crypto-wasm@18.2.0': + resolution: {integrity: sha512-puyZefvq6sHfqlmkri8umhA44724H2JL0YtX8wlvhGuNl8awX/Q1tZyW2Iekm9ZJP7BtuOqlNdg9oQd6iaGbNw==} engines: {node: '>= 18'} '@mdx-js/react@3.1.1': @@ -2981,6 +2981,12 @@ packages: peerDependencies: typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/project-service@8.59.0': + resolution: {integrity: sha512-Lw5ITrR5s5TbC19YSvlr63ZfLaJoU6vtKTHyB0GQOpX0W7d5/Ir6vUahWi/8Sps/nOukZQ0IB3SmlxZnjaKVnw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/scope-manager@5.62.0': resolution: {integrity: sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -2989,12 +2995,22 @@ packages: resolution: {integrity: sha512-SgmyvDPexWETQek+qzZnrG6844IaO02UVyOLhI4wpo82dpZJY9+6YZCKAMFzXb7qhx37mFK1QcPQ18tud+vo6Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/scope-manager@8.59.0': + resolution: {integrity: sha512-UzR16Ut8IpA3Mc4DbgAShlPPkVm8xXMWafXxB0BocaVRHs8ZGakAxGRskF7FId3sdk9lgGD73GSFaWmWFDE4dg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/tsconfig-utils@8.58.2': resolution: {integrity: sha512-3SR+RukipDvkkKp/d0jP0dyzuls3DbGmwDpVEc5wqk5f38KFThakqAAO0XMirWAE+kT00oTauTbzMFGPoAzB0A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/tsconfig-utils@8.59.0': + resolution: {integrity: sha512-91Sbl3s4Kb3SybliIY6muFBmHVv+pYXfybC4Oolp3dvk8BvIE3wOPc+403CWIT7mJNkfQRGtdqghzs2+Z91Tqg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/type-utils@8.58.2': resolution: {integrity: sha512-Z7EloNR/B389FvabdGeTo2XMs4W9TjtPiO9DAsmT0yom0bwlPyRjkJ1uCdW1DvrrrYP50AJZ9Xc3sByZA9+dcg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -3010,6 +3026,10 @@ packages: resolution: {integrity: sha512-9TukXyATBQf/Jq9AMQXfvurk+G5R2MwfqQGDR2GzGz28HvY/lXNKGhkY+6IOubwcquikWk5cjlgPvD2uAA7htQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/types@8.59.0': + resolution: {integrity: sha512-nLzdsT1gdOgFxxxwrlNVUBzSNBEEHJ86bblmk4QAS6stfig7rcJzWKqCyxFy3YRRHXDWEkb2NralA1nOYkkm/A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/typescript-estree@5.62.0': resolution: {integrity: sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -3025,6 +3045,12 @@ packages: peerDependencies: typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/typescript-estree@8.59.0': + resolution: {integrity: sha512-O9Re9P1BmBLFJyikRbQpLku/QA3/AueZNO9WePLBwQrvkixTmDe8u76B6CYUAITRl/rHawggEqUGn5QIkVRLMw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/utils@5.62.0': resolution: {integrity: sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -3038,6 +3064,13 @@ packages: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/utils@8.59.0': + resolution: {integrity: sha512-I1R/K7V07XsMJ12Oaxg/O9GfrysGTmCRhvZJBv0RE0NcULMzjqVpR5kRRQjHsz3J/bElU7HwCO7zkqL+MSUz+g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/visitor-keys@5.62.0': resolution: {integrity: sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -3046,6 +3079,10 @@ packages: resolution: {integrity: sha512-f1WO2Lx8a9t8DARmcWAUPJbu0G20bJlj8L4z72K00TMeJAoyLr/tHhI/pzYBLrR4dXWkcxO1cWYZEOX8DKHTqA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/visitor-keys@8.59.0': + resolution: {integrity: sha512-/uejZt4dSere1bx12WLlPfv8GktzcaDtuJ7s42/HEZ5zGj9oxRaD4bj7qwSunXkf+pbAhFt2zjpHYUiT5lHf0Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} @@ -3068,8 +3105,8 @@ packages: react: optional: true - '@vector-im/compound-web@9.2.0': - resolution: {integrity: sha512-jHbABGEQ2yqNtm5xRIkklQs198VEfSk9AJQolI+e4WSJ0xg8Ozyv9t9KIuKQAmjdSV9aow5G6hDE861XB6DQgw==} + '@vector-im/compound-web@9.3.0': + resolution: {integrity: sha512-Elu4Uw8RbfP6JaudQYkVibALYT6qpwubqfKhteTxIPWBWzSYM+P5T+B1uX+ra+grNcXwXUt2xfMxpqYQsAHgYA==} peerDependencies: '@fontsource/inconsolata': ^5 '@fontsource/inter': ^5 @@ -4936,8 +4973,8 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - livekit-client@2.18.3: - resolution: {integrity: sha512-A8QDaVPo+Ye35bJFyKe6PjMOtY33dmdRXGKP/3+BG48ynEES3YwFzHbsPHJiScgI4OZouNef3Ew/BPazXKwo8Q==} + livekit-client@2.18.6: + resolution: {integrity: sha512-JTOSWkRrFC9KayPvasbnXpAmt+J/ILk5c8f3xUmjqazZk7j9QTyj0qhDHIgdyy/5KFqjqaRmtPu/InMB+WlkPA==} peerDependencies: '@types/dom-mediacapture-record': ^1 @@ -5011,8 +5048,8 @@ packages: matrix-events-sdk@0.0.1: resolution: {integrity: sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA==} - matrix-js-sdk@https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/4b33892d48017b3733a00a48cf5d30182be4a6fb: - resolution: {tarball: https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/4b33892d48017b3733a00a48cf5d30182be4a6fb} + matrix-js-sdk@https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/bec985b783c666de24d6db92a4307a10416742d7: + resolution: {tarball: https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/bec985b783c666de24d6db92a4307a10416742d7} version: 41.3.0 engines: {node: '>=22.0.0'} @@ -6421,8 +6458,8 @@ packages: util@0.12.5: resolution: {integrity: sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==} - uuid@13.0.0: - resolution: {integrity: sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==} + uuid@14.0.0: + resolution: {integrity: sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==} hasBin: true validate-npm-package-license@3.0.4: @@ -6611,8 +6648,8 @@ packages: webpack-virtual-modules@0.6.2: resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} - webrtc-adapter@9.0.4: - resolution: {integrity: sha512-5ZZY1+lGq8LEKuDlg9M2RPJHlH3R7OVwyHqMcUsLKCgd9Wvf+QrFTCItkXXYPmrJn8H6gRLXbSgxLLdexiqHxw==} + webrtc-adapter@9.0.5: + resolution: {integrity: sha512-U9vjByy/sK2OMXu5mmfuZFKTMIUQe34c0JXRO+oDrxJTsntdYT2iIFwYMOV7HhMTuktcZLGf2W1N/OcSf9ssWg==} engines: {node: '>=6.0.0', npm: '>=3.10.0'} whatwg-encoding@3.1.1: @@ -8115,21 +8152,21 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@livekit/components-core@0.12.13(livekit-client@2.18.3(@types/dom-mediacapture-record@1.0.22))(tslib@2.8.1)': + '@livekit/components-core@0.12.13(livekit-client@2.18.6(@types/dom-mediacapture-record@1.0.22))(tslib@2.8.1)': dependencies: '@floating-ui/dom': 1.7.4 - livekit-client: 2.18.3(@types/dom-mediacapture-record@1.0.22) + livekit-client: 2.18.6(@types/dom-mediacapture-record@1.0.22) loglevel: 1.9.1 rxjs: 7.8.2 tslib: 2.8.1 - '@livekit/components-react@2.9.20(livekit-client@2.18.3(@types/dom-mediacapture-record@1.0.22))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(tslib@2.8.1)': + '@livekit/components-react@2.9.20(livekit-client@2.18.6(@types/dom-mediacapture-record@1.0.22))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(tslib@2.8.1)': dependencies: - '@livekit/components-core': 0.12.13(livekit-client@2.18.3(@types/dom-mediacapture-record@1.0.22))(tslib@2.8.1) + '@livekit/components-core': 0.12.13(livekit-client@2.18.6(@types/dom-mediacapture-record@1.0.22))(tslib@2.8.1) clsx: 2.1.1 events: 3.3.0 jose: 6.2.2 - livekit-client: 2.18.3(@types/dom-mediacapture-record@1.0.22) + livekit-client: 2.18.6(@types/dom-mediacapture-record@1.0.22) react: 19.2.5 react-dom: 19.2.5(react@19.2.5) tslib: 2.8.1 @@ -8141,13 +8178,13 @@ snapshots: dependencies: '@bufbuild/protobuf': 1.10.1 - '@livekit/track-processors@0.7.2(@types/dom-mediacapture-transform@0.1.11)(livekit-client@2.18.3(@types/dom-mediacapture-record@1.0.22))': + '@livekit/track-processors@0.7.2(@types/dom-mediacapture-transform@0.1.11)(livekit-client@2.18.6(@types/dom-mediacapture-record@1.0.22))': dependencies: '@mediapipe/tasks-vision': 0.10.34 '@types/dom-mediacapture-transform': 0.1.11 - livekit-client: 2.18.3(@types/dom-mediacapture-record@1.0.22) + livekit-client: 2.18.6(@types/dom-mediacapture-record@1.0.22) - '@matrix-org/matrix-sdk-crypto-wasm@18.1.0': {} + '@matrix-org/matrix-sdk-crypto-wasm@18.2.0': {} '@mdx-js/react@3.1.1(@types/react@19.2.14)(react@19.2.5)': dependencies: @@ -9399,6 +9436,15 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/project-service@8.59.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.59.0(typescript@5.9.3) + '@typescript-eslint/types': 8.59.0 + debug: 4.4.3 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/scope-manager@5.62.0': dependencies: '@typescript-eslint/types': 5.62.0 @@ -9409,10 +9455,19 @@ snapshots: '@typescript-eslint/types': 8.58.2 '@typescript-eslint/visitor-keys': 8.58.2 + '@typescript-eslint/scope-manager@8.59.0': + dependencies: + '@typescript-eslint/types': 8.59.0 + '@typescript-eslint/visitor-keys': 8.59.0 + '@typescript-eslint/tsconfig-utils@8.58.2(typescript@5.9.3)': dependencies: typescript: 5.9.3 + '@typescript-eslint/tsconfig-utils@8.59.0(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + '@typescript-eslint/type-utils@8.58.2(eslint@8.57.1)(typescript@5.9.3)': dependencies: '@typescript-eslint/types': 8.58.2 @@ -9429,6 +9484,8 @@ snapshots: '@typescript-eslint/types@8.58.2': {} + '@typescript-eslint/types@8.59.0': {} + '@typescript-eslint/typescript-estree@5.62.0(typescript@5.9.3)': dependencies: '@typescript-eslint/types': 5.62.0 @@ -9458,6 +9515,21 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/typescript-estree@8.59.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.59.0(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.59.0(typescript@5.9.3) + '@typescript-eslint/types': 8.59.0 + '@typescript-eslint/visitor-keys': 8.59.0 + debug: 4.4.3 + minimatch: 10.2.5 + semver: 7.7.4 + tinyglobby: 0.2.16 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/utils@5.62.0(eslint@8.57.1)(typescript@5.9.3)': dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@8.57.1) @@ -9484,6 +9556,17 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/utils@8.59.0(eslint@8.57.1)(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@8.57.1) + '@typescript-eslint/scope-manager': 8.59.0 + '@typescript-eslint/types': 8.59.0 + '@typescript-eslint/typescript-estree': 8.59.0(typescript@5.9.3) + eslint: 8.57.1 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/visitor-keys@5.62.0': dependencies: '@typescript-eslint/types': 5.62.0 @@ -9494,6 +9577,11 @@ snapshots: '@typescript-eslint/types': 8.58.2 eslint-visitor-keys: 5.0.1 + '@typescript-eslint/visitor-keys@8.59.0': + dependencies: + '@typescript-eslint/types': 8.59.0 + eslint-visitor-keys: 5.0.1 + '@ungap/structured-clone@1.3.0': {} '@use-gesture/core@10.3.1': {} @@ -9508,7 +9596,7 @@ snapshots: '@types/react': 19.2.14 react: 19.2.5 - '@vector-im/compound-web@9.2.0(@fontsource/inconsolata@5.2.8)(@fontsource/inter@5.2.8)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(@vector-im/compound-design-tokens@10.1.0(@types/react@19.2.14)(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + '@vector-im/compound-web@9.3.0(@fontsource/inconsolata@5.2.8)(@fontsource/inter@5.2.8)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(@vector-im/compound-design-tokens@10.1.0(@types/react@19.2.14)(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: '@floating-ui/react': 0.27.19(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@fontsource/inconsolata': 5.2.8 @@ -10652,7 +10740,7 @@ snapshots: eslint-plugin-jest@29.15.2(@typescript-eslint/eslint-plugin@8.58.2(@typescript-eslint/parser@8.58.2(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3): dependencies: - '@typescript-eslint/utils': 8.58.2(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/utils': 8.59.0(eslint@8.57.1)(typescript@5.9.3) eslint: 8.57.1 optionalDependencies: '@typescript-eslint/eslint-plugin': 8.58.2(@typescript-eslint/parser@8.58.2(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) @@ -11710,7 +11798,7 @@ snapshots: lines-and-columns@1.2.4: {} - livekit-client@2.18.3(@types/dom-mediacapture-record@1.0.22): + livekit-client@2.18.6(@types/dom-mediacapture-record@1.0.22): dependencies: '@livekit/mutex': 1.1.1 '@livekit/protocol': 1.45.3 @@ -11721,7 +11809,7 @@ snapshots: sdp-transform: 2.15.0 tslib: 2.8.1 typed-emitter: 2.1.0 - webrtc-adapter: 9.0.4 + webrtc-adapter: 9.0.5 locate-path@5.0.0: dependencies: @@ -11786,10 +11874,10 @@ snapshots: matrix-events-sdk@0.0.1: {} - matrix-js-sdk@https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/4b33892d48017b3733a00a48cf5d30182be4a6fb: + matrix-js-sdk@https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/bec985b783c666de24d6db92a4307a10416742d7: dependencies: '@babel/runtime': 7.29.2 - '@matrix-org/matrix-sdk-crypto-wasm': 18.1.0 + '@matrix-org/matrix-sdk-crypto-wasm': 18.2.0 another-json: 0.2.0 bs58: 6.0.0 content-type: 1.0.5 @@ -11801,7 +11889,6 @@ snapshots: p-retry: 8.0.0 sdp-transform: 3.0.0 unhomoglyph: 1.0.6 - uuid: 13.0.0 matrix-widget-api@1.17.0: dependencies: @@ -13439,7 +13526,7 @@ snapshots: is-typed-array: 1.1.15 which-typed-array: 1.1.20 - uuid@13.0.0: {} + uuid@14.0.0: {} validate-npm-package-license@3.0.4: dependencies: @@ -13643,7 +13730,7 @@ snapshots: webpack-virtual-modules@0.6.2: {} - webrtc-adapter@9.0.4: + webrtc-adapter@9.0.5: dependencies: sdp: 3.2.2 diff --git a/src/RTCConnectionStats.tsx b/src/RTCConnectionStats.tsx index f62a1e4d..ea9df3f5 100644 --- a/src/RTCConnectionStats.tsx +++ b/src/RTCConnectionStats.tsx @@ -87,7 +87,7 @@ export const RTCConnectionStats: FC = ({
); diff --git a/src/button/ReactionToggleButton.tsx b/src/button/ReactionToggleButton.tsx index 39804a5f..c71642e9 100644 --- a/src/button/ReactionToggleButton.tsx +++ b/src/button/ReactionToggleButton.tsx @@ -173,7 +173,7 @@ export interface ReactionData { interface ReactionToggleButtonProps extends ComponentPropsWithoutRef<"button"> { reactionData: ReactionData; identifier: string; - size?: "sm" | "lg"; + size?: "md" | "lg"; /** List of participants raising their hand */ } diff --git a/src/button/__snapshots__/ReactionToggleButton.test.tsx.snap b/src/button/__snapshots__/ReactionToggleButton.test.tsx.snap index 608c1a0f..a1e319d9 100644 --- a/src/button/__snapshots__/ReactionToggleButton.test.tsx.snap +++ b/src/button/__snapshots__/ReactionToggleButton.test.tsx.snap @@ -10,7 +10,7 @@ exports[`Can close reaction dialog 1`] = ` aria-expanded="true" aria-haspopup="true" aria-labelledby="_r_bb_" - class="_button_13vu4_8 _has-icon_13vu4_60 _icon-only_13vu4_53" + class="_button_1nw83_8 _has-icon_1nw83_60 _icon-only_1nw83_53" data-kind="primary" data-size="lg" role="button" @@ -44,7 +44,7 @@ exports[`Can fully expand emoji picker 1`] = ` aria-expanded="true" aria-haspopup="true" aria-labelledby="_r_7m_" - class="_button_13vu4_8 _has-icon_13vu4_60 _icon-only_13vu4_53" + class="_button_1nw83_8 _has-icon_1nw83_60 _icon-only_1nw83_53" data-kind="primary" data-size="lg" role="button" @@ -75,7 +75,7 @@ exports[`Can lower hand 1`] = ` aria-expanded="false" aria-haspopup="true" aria-labelledby="_r_36_" - class="_button_13vu4_8 _has-icon_13vu4_60 _icon-only_13vu4_53" + class="_button_1nw83_8 _has-icon_1nw83_60 _icon-only_1nw83_53" data-kind="secondary" data-size="lg" role="button" @@ -109,7 +109,7 @@ exports[`Can open menu 1`] = ` aria-expanded="true" aria-haspopup="true" aria-labelledby="_r_0_" - class="_button_13vu4_8 _has-icon_13vu4_60 _icon-only_13vu4_53" + class="_button_1nw83_8 _has-icon_1nw83_60 _icon-only_1nw83_53" data-kind="primary" data-size="lg" role="button" @@ -140,7 +140,7 @@ exports[`Can raise hand 1`] = ` aria-expanded="false" aria-haspopup="true" aria-labelledby="_r_1j_" - class="_button_13vu4_8 raisedButton _has-icon_13vu4_60 _icon-only_13vu4_53" + class="_button_1nw83_8 raisedButton _has-icon_1nw83_60 _icon-only_1nw83_53" data-kind="primary" data-size="lg" role="button" diff --git a/src/components/CallFooter.tsx b/src/components/CallFooter.tsx index 4e728d3b..fdedf36c 100644 --- a/src/components/CallFooter.tsx +++ b/src/components/CallFooter.tsx @@ -101,7 +101,7 @@ export const CallFooter: FC = ({ tileStoreGeneration, }) => { const buttons: JSX.Element[] = []; - const buttonSize = asPip ? "sm" : "lg"; + const buttonSize = asPip ? "md" : "lg"; const showSettingsButton = openSettings !== undefined && !asPip && !hideControls; const showLayoutSwitcher = !asPip && !hideControls; diff --git a/src/input/AvatarInputField.tsx b/src/input/AvatarInputField.tsx index 4a3173b4..f9b14707 100644 --- a/src/input/AvatarInputField.tsx +++ b/src/input/AvatarInputField.tsx @@ -113,7 +113,7 @@ export const AvatarInputField: FC = ({ iconOnly Icon={EditIcon} kind="tertiary" - size="sm" + size="md" aria-label={t("action.edit")} /> } @@ -136,7 +136,7 @@ export const AvatarInputField: FC = ({ iconOnly Icon={EditIcon} kind="tertiary" - size="sm" + size="md" aria-label={t("action.edit")} onClick={onSelectUpload} /> diff --git a/src/room/EarpieceOverlay.tsx b/src/room/EarpieceOverlay.tsx index 6835bdd7..574792f0 100644 --- a/src/room/EarpieceOverlay.tsx +++ b/src/room/EarpieceOverlay.tsx @@ -30,7 +30,7 @@ export const EarpieceOverlay: FC = ({ show, onBackToVideoPressed }) => { {t("handset.overlay_description")}