From ab4f0f0f776c17989ef444ac4f2dc5746e534e5e Mon Sep 17 00:00:00 2001 From: Johannes Marbach Date: Mon, 9 Nov 2026 10:02:00 +0100 Subject: [PATCH 1/3] Make one site use the embedded build in playwright tests --- .github/workflows/test.yaml | 3 + docker-compose-playwright.yml | 5 ++ docs/agents/testing.md | 4 ++ playwright.config.ts | 8 +++ playwright/fixtures/widget-user.ts | 5 +- playwright/widget/embedded/build.setup.ts | 20 +++++++ .../widget/embedded/config-json.spec.ts | 57 +++++++++++++++++++ playwright/widget/test-helpers.ts | 6 +- src/config/Config.test.ts | 16 ++++++ src/config/Config.ts | 7 ++- 10 files changed, 127 insertions(+), 4 deletions(-) create mode 100644 playwright/widget/embedded/build.setup.ts create mode 100644 playwright/widget/embedded/config-json.spec.ts diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 5e2703f67..53e6a3129 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -107,6 +107,9 @@ jobs: run: pnpm playwright install --with-deps - name: Run backend components run: | + # Make sure the directory for the embedded build exists as Docker might + # create it with wrong access rights otherwise. + mkdir -p embedded/web/dist docker compose -f docker-compose-dev.yml -f docker-compose-playwright.yml pull docker compose -f docker-compose-dev.yml -f docker-compose-playwright.yml up -d docker ps diff --git a/docker-compose-playwright.yml b/docker-compose-playwright.yml index fe44f9b36..52bda51a3 100644 --- a/docker-compose-playwright.yml +++ b/docker-compose-playwright.yml @@ -23,3 +23,8 @@ services: element-web-1: # Pin to a SHA so that upstream cannot break our tests. Renovate handles regular updates. image: ghcr.io/element-hq/element-web:develop@sha256:f30be05ae9893a5f8acab96207056d1144ce436b8550b95fb60cdfa838f50699 + volumes: + # The other site opens the embedded package under test in place of the + # copy its image bundles (the first site opens the dev server's app. + # Built by the Playwright run (see build.setup.ts). + - ./embedded/web/dist:/app/widgets/element-call:ro,Z diff --git a/docs/agents/testing.md b/docs/agents/testing.md index eb8792d67..ba203fd11 100644 --- a/docs/agents/testing.md +++ b/docs/agents/testing.md @@ -34,6 +34,10 @@ translations. The browser is Playwright's, so a fresh clone needs ## End-to-end +The Element Web instance `HOST1` opens the app the dev server serves. The instance +`HOST2` (the other site) opens the embedded package as a host ships it. This package +is built by the `build-embedded` setup project and mounted in `docker-compose-playwright.yml`. + - `playwright/*.spec.ts` — standalone, on Chromium and Firefox. - `playwright/widget/` — widget mode, where cross-context bugs surface. - `playwright/component/` — the component in a host page, via the harness on port diff --git a/playwright.config.ts b/playwright.config.ts index 2de044194..26db26a82 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -63,8 +63,15 @@ export default defineConfig({ }, /* Configure projects for major browsers */ projects: [ + { + // Builds the embedded package into the directory the other site's + // Element Web container serves it from (see docker-compose-playwright.yml). + name: "build-embedded", + testMatch: "**/widget/embedded/build.setup.ts", + }, { name: "chromium", + dependencies: ["build-embedded"], testIgnore: "**/mobile/**", use: { ...devices["Desktop Chrome"], @@ -86,6 +93,7 @@ export default defineConfig({ }, { name: "firefox", + dependencies: ["build-embedded"], testIgnore: "**/mobile/**", use: { ...devices["Desktop Firefox"], diff --git a/playwright/fixtures/widget-user.ts b/playwright/fixtures/widget-user.ts index b21732594..f069db1a1 100644 --- a/playwright/fixtures/widget-user.ts +++ b/playwright/fixtures/widget-user.ts @@ -127,13 +127,14 @@ export const widgetTest = test.extend({ * Provide a way to add additional users within a test. * The returned user will be registered on the default homeserver, the name will be made unique by appending a timestamp. */ - addUser: async ({ browser }, use) => { + addUser: async ({ browser }, use, testInfo) => { await use( async ( username: string, host: string = HOST1, ): Promise => { - const uniqueSuffix = Date.now(); + // Tests in parallel workers can reach this in the same millisecond. + const uniqueSuffix = `${Date.now()}_${testInfo.parallelIndex}`; const { page, clientHandle, mxId } = await TestHelpers.registerUser( browser, `${username.toLowerCase()}_${uniqueSuffix}`, diff --git a/playwright/widget/embedded/build.setup.ts b/playwright/widget/embedded/build.setup.ts new file mode 100644 index 000000000..efc1e3d73 --- /dev/null +++ b/playwright/widget/embedded/build.setup.ts @@ -0,0 +1,20 @@ +/* +Copyright 2026 Element Creations Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE in the repository root for full details. +*/ + +import { test as setup } from "@playwright/test"; +import { execFileSync } from "node:child_process"; +import { mkdirSync } from "node:fs"; + +const EMBEDDED_DIST = "embedded/web/dist"; + +setup("build the embedded package", () => { + setup.setTimeout(10 * 60 * 1000); + mkdirSync(EMBEDDED_DIST, { recursive: true }); + execFileSync("pnpm", ["build:embedded", "--outDir", EMBEDDED_DIST], { + stdio: "inherit", + }); +}); diff --git a/playwright/widget/embedded/config-json.spec.ts b/playwright/widget/embedded/config-json.spec.ts new file mode 100644 index 000000000..fa045eb4f --- /dev/null +++ b/playwright/widget/embedded/config-json.spec.ts @@ -0,0 +1,57 @@ +/* +Copyright 2026 Element Creations Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE in the repository root for full details. +*/ + +import { expect, type Route, test } from "@playwright/test"; + +import { widgetTest } from "../../fixtures/widget-user.ts"; +import { HOST2, TestHelpers } from "../test-helpers.ts"; + +/** Where the other site's Element Web serves the embedded package from */ +const CONFIG_JSON = "**/widgets/element-call/config.json"; + +const HOST_ANSWERS: Record Promise> = { + "a 404": async (route) => route.fulfill({ status: 404 }), + "a failed fetch": async (route) => route.abort("failed"), + "an HTML page": async (route) => + route.fulfill({ + status: 200, + contentType: "text/html", + body: "Element", + }), +}; + +widgetTest.skip( + ({ browserName }) => browserName === "firefox", + "The widget suite does not run on Firefox, see the other specs here", +); + +for (const [answer, respond] of Object.entries(HOST_ANSWERS)) { + widgetTest( + `reaches the lobby when config.json gets ${answer}`, + async ({ addUser }) => { + test.slow(); + const { page } = await addUser("Brooks", HOST2); + await TestHelpers.createRoom("Config Room", page); + + let answered = 0; + await page.route(CONFIG_JSON, async (route) => { + answered++; + await respond(route); + }); + + await TestHelpers.startCallInCurrentRoom(page); + + await expect( + page + .locator('iframe[title="Element Call"]') + .contentFrame() + .getByTestId("lobby_joinCall"), + ).toBeVisible({ timeout: 15000 }); + expect(answered).toBeGreaterThan(0); + }, + ); +} diff --git a/playwright/widget/test-helpers.ts b/playwright/widget/test-helpers.ts index 4e255f869..38e61f49c 100644 --- a/playwright/widget/test-helpers.ts +++ b/playwright/widget/test-helpers.ts @@ -144,7 +144,11 @@ export class TestHelpers { await this.dismissStartupToasts(page); - await TestHelpers.setDevToolElementCallDevUrl(page); + // The other site's Element Web opens the embedded package it serves itself + // (see docker-compose-playwright.yml), not the dev server's app. + if (host !== HOST2) { + await TestHelpers.setDevToolElementCallDevUrl(page); + } const clientHandle = await page.evaluateHandle(() => window.mxMatrixClientPeg.get(), diff --git a/src/config/Config.test.ts b/src/config/Config.test.ts index 6df037483..0a9fe76cf 100644 --- a/src/config/Config.test.ts +++ b/src/config/Config.test.ts @@ -108,6 +108,22 @@ describe("Config.initWith", () => { expect(Config.get().media_quality).toEqual(DEFAULT_CONFIG.media_quality); }); + it("falls back to the defaults when config.json is not JSON", async () => { + vi.spyOn(globalThis, "fetch").mockResolvedValue( + new Response("Element", { + status: 200, + headers: { "content-type": "text/html" }, + }), + ); + const warnSpy = vi.spyOn(logger, "warn").mockImplementation(() => {}); + Reflect.set(Config, "internalInstance", undefined); + + await expect(Config.init()).resolves.toBeUndefined(); + + expect(Config.get().media_quality).toEqual(DEFAULT_CONFIG.media_quality); + expect(warnSpy).toHaveBeenCalledTimes(1); + }); + it("replaces a config initialized earlier", () => { Config.initWith({ ssla: "https://first.invalid/ssla" }); Config.initWith({ ssla: "https://second.invalid/ssla" }); diff --git a/src/config/Config.ts b/src/config/Config.ts index 3c0f17bc9..76a25f75a 100644 --- a/src/config/Config.ts +++ b/src/config/Config.ts @@ -156,5 +156,10 @@ async function downloadConfig(fetchTarget: string): Promise { return {}; } - return response.json(); + try { + return await response.json(); + } catch (e) { + logger.warn(`Config at ${fetchTarget} is not JSON, using defaults`, e); + return {}; + } } From ceeb26b27130588d6f3a148d6e83a64245fc966b Mon Sep 17 00:00:00 2001 From: Johannes Marbach Date: Tue, 10 Nov 2026 07:30:57 +0100 Subject: [PATCH 2/3] Remove test.slow() --- playwright/widget/embedded/config-json.spec.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/playwright/widget/embedded/config-json.spec.ts b/playwright/widget/embedded/config-json.spec.ts index fa045eb4f..a9d7c590f 100644 --- a/playwright/widget/embedded/config-json.spec.ts +++ b/playwright/widget/embedded/config-json.spec.ts @@ -33,7 +33,6 @@ for (const [answer, respond] of Object.entries(HOST_ANSWERS)) { widgetTest( `reaches the lobby when config.json gets ${answer}`, async ({ addUser }) => { - test.slow(); const { page } = await addUser("Brooks", HOST2); await TestHelpers.createRoom("Config Room", page); From 60d60291844608209ea92e642ba8583c7a39cfa4 Mon Sep 17 00:00:00 2001 From: Johannes Marbach Date: Tue, 10 Nov 2026 07:38:23 +0100 Subject: [PATCH 3/3] Delint --- playwright/widget/embedded/config-json.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playwright/widget/embedded/config-json.spec.ts b/playwright/widget/embedded/config-json.spec.ts index a9d7c590f..b8bba8b24 100644 --- a/playwright/widget/embedded/config-json.spec.ts +++ b/playwright/widget/embedded/config-json.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, type Route, test } from "@playwright/test"; +import { expect, type Route } from "@playwright/test"; import { widgetTest } from "../../fixtures/widget-user.ts"; import { HOST2, TestHelpers } from "../test-helpers.ts";