From d3a4234f4a33e8d0c26b95b9c8aeb7c529c15c7f Mon Sep 17 00:00:00 2001 From: fkwp Date: Fri, 6 Feb 2026 11:54:22 +0100 Subject: [PATCH 01/65] setup webhooks for SFUs --- backend/dev_livekit-othersite.yaml | 4 ++++ backend/dev_livekit.yaml | 4 ++++ 2 files changed, 8 insertions(+) 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 From 7b1eb03e2a8ebd4ffa81338ee9ad7a6628205b5e Mon Sep 17 00:00:00 2001 From: fkwp Date: Fri, 6 Feb 2026 11:54:39 +0100 Subject: [PATCH 02/65] add ca certs to SFU containers such that webhooks are accepted --- dev-backend-docker-compose.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/dev-backend-docker-compose.yml b/dev-backend-docker-compose.yml index 28682a33..31694c01 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: From 4211405e7b419cec745076ff137244150b9b6057 Mon Sep 17 00:00:00 2001 From: Valere Date: Fri, 3 Apr 2026 16:09:30 +0200 Subject: [PATCH 03/65] Use synapse API to register instead doing via UI --- backend/dev_homeserver-othersite.yaml | 3 + backend/dev_homeserver.yaml | 3 + backend/dev_nginx.conf | 21 ++- backend/playwright_homeserver-othersite.yaml | 3 + backend/playwright_homeserver.yaml | 3 + playwright.config.ts | 11 ++ playwright/utils/synapse-admin.ts | 142 +++++++++++++++++++ playwright/widget/test-helpers.ts | 50 ++++--- 8 files changed, 213 insertions(+), 23 deletions(-) create mode 100644 playwright/utils/synapse-admin.ts 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 d3ddbc53..fdefa3bd 100644 --- a/backend/dev_nginx.conf +++ b/backend/dev_nginx.conf @@ -28,11 +28,18 @@ server { # Reason: the lk-jwt-service uses the federation API for the openid token # verification, which requires TLS location ~ ^(/_matrix|/_synapse/client) { - proxy_pass "http://homeserver:8008"; + 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; + 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; @@ -73,7 +80,15 @@ server { 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; + 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/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/test-helpers.ts b/playwright/widget/test-helpers.ts index ff5fa7e2..d0674f8a 100644 --- a/playwright/widget/test-helpers.ts +++ b/playwright/widget/test-helpers.ts @@ -13,6 +13,8 @@ import { } 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"; @@ -74,29 +76,41 @@ 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.goto(host + "/#/login"); + + await page.getByRole("link", { name: "Sign in" }).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("textbox", { name: "Password" }).fill(PASSWORD); + await page.getByRole("button", { name: "Sign in" }).click(); + + // 😤For reasons web is staying on an infinite loading page after login, so we reload the page + // Super annoying to have to wait... + await page.waitForTimeout(2000); + await page.reload(); await expect( page.getByRole("heading", { name: `Welcome ${username}` }), - ).toBeVisible({ - // Increase timeout as registration can be slow :/ - timeout: 15_000, - }); + ).toBeVisible(); await this.maybeDismissBrowserNotSupportedToast(page); await this.maybeDismissServiceWorkerWarningToast(page); @@ -106,11 +120,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 }; } From 24e721f572625ffd55da82d765194e79639918f6 Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 7 Apr 2026 13:43:19 +0200 Subject: [PATCH 04/65] fix web stuck problem! go to / and not to #/login --- playwright/widget/test-helpers.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/playwright/widget/test-helpers.ts b/playwright/widget/test-helpers.ts index d0674f8a..2279e20e 100644 --- a/playwright/widget/test-helpers.ts +++ b/playwright/widget/test-helpers.ts @@ -95,7 +95,7 @@ export class TestHelpers { reducedMotion: "reduce", }); const page = await userContext.newPage(); - await page.goto(host + "/#/login"); + await page.goto(host); await page.getByRole("link", { name: "Sign in" }).click(); @@ -103,11 +103,6 @@ export class TestHelpers { await page.getByRole("textbox", { name: "Password" }).fill(PASSWORD); await page.getByRole("button", { name: "Sign in" }).click(); - // 😤For reasons web is staying on an infinite loading page after login, so we reload the page - // Super annoying to have to wait... - await page.waitForTimeout(2000); - await page.reload(); - await expect( page.getByRole("heading", { name: `Welcome ${username}` }), ).toBeVisible(); From 0127040a8c88f60e6ad1009804994f8fc3cb316a Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 7 Apr 2026 15:43:55 +0200 Subject: [PATCH 05/65] Remove slow? no registration anymore --- playwright/mobile/create-call-mobile.spec.ts | 1 - playwright/widget/huddle-call.test.ts | 2 -- playwright/widget/pip-call-button-interaction.test.ts | 2 -- playwright/widget/pip-call.test.ts | 2 -- playwright/widget/simple-create.spec.ts | 2 -- playwright/widget/voice-call-dm.spec.ts | 6 ------ 6 files changed, 15 deletions(-) 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/widget/huddle-call.test.ts b/playwright/widget/huddle-call.test.ts index 9c7cfc80..ea5d64b6 100644 --- a/playwright/widget/huddle-call.test.ts +++ b/playwright/widget/huddle-call.test.ts @@ -16,8 +16,6 @@ widgetTest("Create and join a group call", 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(); // 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); 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..ef19a36e 100644 --- a/playwright/widget/pip-call.test.ts +++ b/playwright/widget/pip-call.test.ts @@ -16,8 +16,6 @@ widgetTest("Put call 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 timo = await addUser("Timo", HOST1); diff --git a/playwright/widget/simple-create.spec.ts b/playwright/widget/simple-create.spec.ts index 4686e99d..6b5d17bb 100644 --- a/playwright/widget/simple-create.spec.ts +++ b/playwright/widget/simple-create.spec.ts @@ -17,8 +17,6 @@ widgetTest.skip( ); widgetTest("Start a new call as widget", async ({ asWidget, browserName }) => { - test.slow(); // Triples the timeout - const { brooks, whistler } = asWidget; await TestHelpers.startCallInCurrentRoom(brooks.page, false); 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); From 701edd952273de7ba77bd3309cdcb8227eb793f2 Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 21 Apr 2026 17:54:10 +0200 Subject: [PATCH 06/65] fixup lint --- playwright/widget/simple-create.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playwright/widget/simple-create.spec.ts b/playwright/widget/simple-create.spec.ts index 6b5d17bb..c8fb8013 100644 --- a/playwright/widget/simple-create.spec.ts +++ b/playwright/widget/simple-create.spec.ts @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial Please see LICENSE in the repository root for full details. */ -import { expect, test } from "@playwright/test"; +import { expect } from "@playwright/test"; import { widgetTest } from "../fixtures/widget-user.ts"; import { TestHelpers } from "./test-helpers.ts"; From 754a42ffd1ff2f9eff8f7999b07457386ba588e8 Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 21 Apr 2026 18:14:51 +0200 Subject: [PATCH 07/65] increase default timeout on huddle test --- playwright/widget/huddle-call.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/playwright/widget/huddle-call.test.ts b/playwright/widget/huddle-call.test.ts index ea5d64b6..7bc94f81 100644 --- a/playwright/widget/huddle-call.test.ts +++ b/playwright/widget/huddle-call.test.ts @@ -11,6 +11,9 @@ 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", From 0e2d2c6f72e8953a3d0be2c26093bc66e1070ad6 Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 21 Apr 2026 18:36:40 +0200 Subject: [PATCH 08/65] try paralelize registrations? --- playwright/widget/federated-call.test.ts | 6 ++++-- playwright/widget/huddle-call.test.ts | 14 ++++++++------ playwright/widget/screen-share.test.ts | 8 +++++--- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/playwright/widget/federated-call.test.ts b/playwright/widget/federated-call.test.ts index fda58250..2b17a706 100644 --- a/playwright/widget/federated-call.test.ts +++ b/playwright/widget/federated-call.test.ts @@ -26,8 +26,10 @@ 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); + const [florian, timo] = await Promise.all([ + addUser("florian", HOST1), + addUser("timo", HOST2), + ]); const roomName = "Call Room"; diff --git a/playwright/widget/huddle-call.test.ts b/playwright/widget/huddle-call.test.ts index 7bc94f81..07f12d8a 100644 --- a/playwright/widget/huddle-call.test.ts +++ b/playwright/widget/huddle-call.test.ts @@ -8,7 +8,7 @@ Please see LICENSE in the repository root for full details. import { expect, test } from "@playwright/test"; import { widgetTest } from "../fixtures/widget-user.ts"; -import { HOST1, TestHelpers } from "./test-helpers.ts"; +import { HOST1, HOST2, 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. @@ -19,11 +19,13 @@ widgetTest("Create and join a group call", async ({ addUser, browserName }) => { "The is test is not working on firefox CI environment. No mic/audio device inputs so cam/mic are disabled", ); - 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, [ diff --git a/playwright/widget/screen-share.test.ts b/playwright/widget/screen-share.test.ts index c60ef697..f8437e1f 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]); From 55de3fd216ef84c0501754c6f704a9f8f492063b Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 22 Apr 2026 09:03:57 +0200 Subject: [PATCH 09/65] tests optimizations --- .../widget/hotswap-legacy-compat.test.ts | 12 +-- playwright/widget/huddle-call.test.ts | 93 ++++++++++--------- playwright/widget/test-helpers.ts | 5 +- 3 files changed, 57 insertions(+), 53 deletions(-) diff --git a/playwright/widget/hotswap-legacy-compat.test.ts b/playwright/widget/hotswap-legacy-compat.test.ts index e4695624..f58a562d 100644 --- a/playwright/widget/hotswap-legacy-compat.test.ts +++ b/playwright/widget/hotswap-legacy-compat.test.ts @@ -65,15 +65,13 @@ 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); const blockDisplayCount = await frame .locator("video") diff --git a/playwright/widget/huddle-call.test.ts b/playwright/widget/huddle-call.test.ts index 07f12d8a..262b66db 100644 --- a/playwright/widget/huddle-call.test.ts +++ b/playwright/widget/huddle-call.test.ts @@ -8,7 +8,7 @@ Please see LICENSE in the repository root for full details. import { expect, test } from "@playwright/test"; import { widgetTest } from "../fixtures/widget-user.ts"; -import { HOST1, HOST2, TestHelpers } from "./test-helpers.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. @@ -50,52 +50,56 @@ 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(); + }), + ); // 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); + for (const participant of [valere, timo, robin, halfshot, florian]) { + // Check the names are correct + await expect(frame.getByText(participant.displayName)).toBeVisible(); + } - // 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(); + // 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 - const videoElements = await frame.locator("video").all(); - expect(videoElements.length).toBe(5); - await expect(frame.locator("video[autoplay]")).toHaveCount(5); + // There should be 5 video elements, visible and autoplaying + await expect(frame.locator("video")).toHaveCount(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, - ); - expect(blockDisplayCount).toBe(5); - } + const blockDisplayCount = await frame + .locator("video") + .evaluateAll( + (videos: Element[]) => + videos.filter( + (v: Element) => window.getComputedStyle(v).display === "block", + ).length, + ); + expect(blockDisplayCount).toBe(5); + }), + ); // Quickly test muting one participant to see it reflects and that our asserts works const florianFrame = florian.page @@ -111,15 +115,14 @@ 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); + await expect(frame.locator("video")).toHaveCount(5, { + timeout: 10000, + }); const blockDisplayCount = await frame .locator("video") diff --git a/playwright/widget/test-helpers.ts b/playwright/widget/test-helpers.ts index 2279e20e..e37e4b5b 100644 --- a/playwright/widget/test-helpers.ts +++ b/playwright/widget/test-helpers.ts @@ -105,7 +105,10 @@ export class TestHelpers { await expect( page.getByRole("heading", { name: `Welcome ${username}` }), - ).toBeVisible(); + ).toBeVisible({ + // Increase timeout here + timeout: 10000, + }); await this.maybeDismissBrowserNotSupportedToast(page); await this.maybeDismissServiceWorkerWarningToast(page); From 33172837e7490eb53b86ce86cb740eb9672fbe64 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 22 Apr 2026 07:23:47 +0000 Subject: [PATCH 10/65] Update dependency livekit-client to v2.18.4 --- pnpm-lock.yaml | 134 ++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 111 insertions(+), 23 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fa9ebebb..2d3e891d 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.4(@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.4(@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.4(@types/dom-mediacapture-record@1.0.22)) '@mediapipe/tasks-vision': specifier: ^0.10.18 version: 0.10.34 @@ -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.4(@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/fef093747e1c35a60b0fdc687a7166a7d7eb8a28 matrix-widget-api: specifier: ^1.16.1 version: 1.17.0 @@ -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==} @@ -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.4: + resolution: {integrity: sha512-kjbH9WdA85gZNqFiAMY9jKJ3HArk8F+AQvAvSE5N6PnY+wNCV9OYe9yeb9BVezZiD6ltGyB3CkTxGyuX6BXYAw==} 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/fef093747e1c35a60b0fdc687a7166a7d7eb8a28: + resolution: {tarball: https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/fef093747e1c35a60b0fdc687a7166a7d7eb8a28} version: 41.3.0 engines: {node: '>=22.0.0'} @@ -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.4(@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.4(@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.4(@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.4(@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.4(@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,11 +8178,11 @@ 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.4(@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.4(@types/dom-mediacapture-record@1.0.22) '@matrix-org/matrix-sdk-crypto-wasm@18.1.0': {} @@ -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': {} @@ -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.4(@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,7 +11874,7 @@ 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/fef093747e1c35a60b0fdc687a7166a7d7eb8a28: dependencies: '@babel/runtime': 7.29.2 '@matrix-org/matrix-sdk-crypto-wasm': 18.1.0 @@ -13643,7 +13731,7 @@ snapshots: webpack-virtual-modules@0.6.2: {} - webrtc-adapter@9.0.4: + webrtc-adapter@9.0.5: dependencies: sdp: 3.2.2 From 2b46925e77a2b9685953d8a8e5a448e7e4ee6d5b Mon Sep 17 00:00:00 2001 From: Timo K Date: Wed, 22 Apr 2026 11:51:33 +0200 Subject: [PATCH 11/65] Call window.controls.onBackButtonPressed if esc is pressed without any focus. --- src/useCallViewKeyboardShortcuts.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/useCallViewKeyboardShortcuts.ts b/src/useCallViewKeyboardShortcuts.ts index 728a2614..3d9654be 100644 --- a/src/useCallViewKeyboardShortcuts.ts +++ b/src/useCallViewKeyboardShortcuts.ts @@ -68,6 +68,8 @@ export function useCallViewKeyboardShortcuts( } else if (KeyToReactionMap[event.key]) { event.preventDefault(); sendReaction(KeyToReactionMap[event.key]); + } else if (event.key === "Escape") { + window.controls.onBackButtonPressed?.(); } }, [ From 56bab70534d52c368c5d2b05e2c61cc5eb34e5aa Mon Sep 17 00:00:00 2001 From: Timo K Date: Wed, 22 Apr 2026 12:26:03 +0200 Subject: [PATCH 12/65] add tests --- src/useCallViewKeyboardShortcuts.test.tsx | 72 ++++++++++++++++++++++- 1 file changed, 71 insertions(+), 1 deletion(-) diff --git a/src/useCallViewKeyboardShortcuts.test.tsx b/src/useCallViewKeyboardShortcuts.test.tsx index e22380d1..5a327f83 100644 --- a/src/useCallViewKeyboardShortcuts.test.tsx +++ b/src/useCallViewKeyboardShortcuts.test.tsx @@ -6,7 +6,7 @@ Please see LICENSE in the repository root for full details. */ import { render } from "@testing-library/react"; -import { type FC, useRef } from "react"; +import { type FC, useRef, useState } from "react"; import { expect, test, vi } from "vitest"; import { Button } from "@vector-im/compound-web"; import userEvent from "@testing-library/user-event"; @@ -17,6 +17,7 @@ import { ReactionSet, ReactionsRowSize, } from "./reactions"; +import { type Controls } from "./controls"; // Test Explanation: // - The main objective is to test `useCallViewKeyboardShortcuts`. @@ -27,6 +28,7 @@ interface TestComponentProps { onButtonClick?: () => void; sendReaction?: () => void; toggleHandRaised?: () => void; + initialModalOpen?: boolean; } const TestComponent: FC = ({ @@ -34,7 +36,9 @@ const TestComponent: FC = ({ onButtonClick = (): void => {}, sendReaction = (reaction: ReactionOption): void => {}, toggleHandRaised = (): void => {}, + initialModalOpen = false, }) => { + const [modalOpen, setModalOpen] = useState(initialModalOpen); const ref = useRef(null); useCallViewKeyboardShortcuts( ref, @@ -47,6 +51,19 @@ const TestComponent: FC = ({ return (
+ {modalOpen && ( + { + if (e.key === "Escape") { + e.preventDefault(); + setModalOpen(false); + } + }} + > + + + )}
); }; @@ -118,6 +135,27 @@ test("raised hand can be sent via keyboard presses", async () => { expect(toggleHandRaised).toHaveBeenCalledOnce(); }); +test("raised hand cannot be sent via keyboard presses if modal open and focussed", async () => { + const user = userEvent.setup(); + const toggleHandRaised = vi.fn(); + const { getByRole } = render( + , + ); + getByRole("button", { name: "InModalButton" }).focus(); + await user.keyboard("h"); + + expect(toggleHandRaised).not.toHaveBeenCalledOnce(); + + // once we press esc... + await user.keyboard("[Escape]"); + // we can toggle the hand raise... + await user.keyboard("h"); + expect(toggleHandRaised).toHaveBeenCalledOnce(); +}); + test("unmuting happens in place of the default action", async () => { const user = userEvent.setup(); const defaultPrevented = vi.fn(); @@ -138,3 +176,35 @@ test("unmuting happens in place of the default action", async () => { await user.keyboard("[Space]"); expect(defaultPrevented).toBeCalledWith(true); }); + +test("escape button triggers the controls back action", async () => { + const user = userEvent.setup(); + + window.controls = { onBackButtonPressed: vi.fn() } as unknown as Controls; + // In the real application, we mostly just want the spacebar shortcut to avoid + // scrolling the page. But to test that here in JSDOM, we need some kind of + // container element that can be interactive and receive focus / keydown + // events.