From ff1da50dd99e02d1548976357718e1608b795488 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 17 Mar 2025 09:16:45 +0000 Subject: [PATCH] Move a bunch of shared playwright code into @element-hq/element-web-playwright-common (#29477) * Move a bunch of shared playwright code into @element-hq/element-web-playwright-common Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Remove stale devDep Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Update playwright-common Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Update screenshot Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Fix testcontainers version Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --------- Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- package.json | 17 +- playwright.config.ts | 5 +- playwright/@types/playwright-core.d.ts | 12 - playwright/Dockerfile | 9 - playwright/docker-entrypoint.sh | 5 - playwright/e2e/csAPI.ts | 4 +- playwright/e2e/editing/editing.spec.ts | 6 +- playwright/e2e/login/login-consent.spec.ts | 6 +- playwright/e2e/messages/messages.spec.ts | 2 +- playwright/e2e/oidc/index.ts | 2 +- playwright/e2e/register/email.spec.ts | 4 +- playwright/e2e/register/register.spec.ts | 10 +- playwright/e2e/spaces/spaces.spec.ts | 6 +- playwright/e2e/timeline/timeline.spec.ts | 104 +++-- playwright/element-web-test.ts | 231 ++--------- playwright/logger.ts | 63 --- .../plugins/homeserver/dendrite/index.ts | 4 +- playwright/plugins/homeserver/index.ts | 2 +- .../homeserver/synapse/consentHomeserver.ts | 19 +- .../homeserver/synapse/masHomeserver.ts | 3 +- playwright/plugins/oauth_server/index.ts | 3 +- playwright/plugins/utils/api.ts | 76 ---- playwright/plugins/utils/object.ts | 16 - playwright/plugins/utils/port.ts | 19 - playwright/plugins/utils/rand.ts | 13 - playwright/plugins/webserver/index.ts | 4 +- playwright/services.ts | 141 +------ .../with-four-members-linux.png | Bin 18392 -> 19391 bytes .../testcontainers/HomeserverContainer.ts | 24 -- playwright/testcontainers/dendrite.ts | 20 +- playwright/testcontainers/mailpit.ts | 33 -- playwright/testcontainers/mas.ts | 346 ---------------- playwright/testcontainers/synapse.ts | 391 +----------------- yarn.lock | 86 +++- 34 files changed, 226 insertions(+), 1460 deletions(-) delete mode 100644 playwright/@types/playwright-core.d.ts delete mode 100644 playwright/Dockerfile delete mode 100644 playwright/docker-entrypoint.sh delete mode 100644 playwright/logger.ts delete mode 100644 playwright/plugins/utils/api.ts delete mode 100644 playwright/plugins/utils/object.ts delete mode 100644 playwright/plugins/utils/port.ts delete mode 100644 playwright/plugins/utils/rand.ts delete mode 100644 playwright/testcontainers/HomeserverContainer.ts delete mode 100644 playwright/testcontainers/mailpit.ts delete mode 100644 playwright/testcontainers/mas.ts diff --git a/package.json b/package.json index 6581f0c2e0..7843a43c27 100644 --- a/package.json +++ b/package.json @@ -62,19 +62,19 @@ "test": "jest", "test:playwright": "playwright test", "test:playwright:open": "yarn test:playwright --ui", - "test:playwright:screenshots": "yarn test:playwright:screenshots:build && yarn test:playwright:screenshots:run", - "test:playwright:screenshots:build": "docker build playwright -t element-web-playwright", - "test:playwright:screenshots:run": "docker run --rm --network host -e BASE_URL -e CI -v $(pwd):/work/ -v $(node -e 'console.log(require(`path`).dirname(require.resolve(`matrix-js-sdk/package.json`)))'):/work/node_modules/matrix-js-sdk -v /var/run/docker.sock:/var/run/docker.sock -v /tmp/:/tmp/ -it element-web-playwright --grep @screenshot --project=Chrome", + "test:playwright:screenshots": "playwright-screenshots --project=Chrome", "coverage": "yarn test --coverage", "analyse:webpack-bundles": "webpack-bundle-analyzer webpack-stats.json webapp", "update:jitsi": "curl -s https://meet.element.io/libs/external_api.min.js > ./res/jitsi_external_api.min.js" }, "resolutions": { + "@playwright/test": "1.50.1", "@types/react": "18.3.18", "@types/react-dom": "18.3.5", "oidc-client-ts": "3.1.0", "jwt-decode": "4.0.0", "caniuse-lite": "1.0.30001701", + "testcontainers": "10.20.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0", "wrap-ansi": "npm:wrap-ansi@^7.0.0" }, @@ -158,7 +158,6 @@ "devDependencies": { "@action-validator/cli": "^0.6.0", "@action-validator/core": "^0.6.0", - "@axe-core/playwright": "^4.8.1", "@babel/core": "^7.12.10", "@babel/eslint-parser": "^7.12.10", "@babel/eslint-plugin": "^7.12.10", @@ -178,13 +177,13 @@ "@babel/preset-typescript": "^7.12.7", "@babel/runtime": "^7.12.5", "@casualbot/jest-sonar-reporter": "2.2.7", + "@element-hq/element-web-playwright-common": "^1.1.5", "@peculiar/webcrypto": "^1.4.3", - "@playwright/test": "^1.40.1", + "@playwright/test": "^1.50.1", "@principalstudio/html-webpack-inject-preload": "^1.2.7", "@sentry/webpack-plugin": "^3.0.0", "@stylistic/eslint-plugin": "^3.0.0", "@svgr/webpack": "^8.0.0", - "@testcontainers/postgresql": "^10.16.0", "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^6.4.8", "@testing-library/react": "^16.0.0", @@ -259,13 +258,12 @@ "jsqr": "^1.4.0", "knip": "^5.36.2", "lint-staged": "^15.0.2", - "mailpit-api": "^1.0.5", "matrix-web-i18n": "^3.2.1", "mini-css-extract-plugin": "2.9.2", "minimist": "^1.2.6", "modernizr": "^3.12.0", "node-fetch": "^2.6.7", - "playwright-core": "^1.45.1", + "playwright-core": "^1.51.0", "postcss": "8.4.46", "postcss-easings": "^4.0.0", "postcss-hexrgba": "2.1.0", @@ -282,13 +280,12 @@ "rimraf": "^6.0.0", "semver": "^7.5.2", "source-map-loader": "^5.0.0", - "strip-ansi": "^7.1.0", "stylelint": "^16.13.0", "stylelint-config-standard": "^37.0.0", "stylelint-scss": "^6.0.0", "stylelint-value-no-unknown-custom-properties": "^6.0.1", "terser-webpack-plugin": "^5.3.9", - "testcontainers": "^10.16.0", + "testcontainers": "^10.20.0", "ts-node": "^10.9.1", "typescript": "5.8.2", "util": "^0.12.5", diff --git a/playwright.config.ts b/playwright.config.ts index 09bd07bb3b..519e2d9c3b 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details. import { defineConfig, devices } from "@playwright/test"; -import { Options } from "./playwright/services"; +import { type WorkerOptions } from "./playwright/services"; const baseURL = process.env["BASE_URL"] ?? "http://localhost:8080"; @@ -21,7 +21,7 @@ const chromeProject = { }, }; -export default defineConfig({ +export default defineConfig({ projects: [ { name: "Chrome", @@ -83,6 +83,7 @@ export default defineConfig({ url: `${baseURL}/config.json`, reuseExistingServer: true, timeout: (process.env.CI ? 30 : 120) * 1000, + stdout: "pipe", }, testDir: "playwright/e2e", outputDir: "playwright/test-results", diff --git a/playwright/@types/playwright-core.d.ts b/playwright/@types/playwright-core.d.ts deleted file mode 100644 index 244f3c91d4..0000000000 --- a/playwright/@types/playwright-core.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2024 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial -Please see LICENSE files in the repository root for full details. -*/ - -declare module "playwright-core/lib/utils" { - // This type is not public in playwright-core utils - export function sanitizeForFilePath(filePath: string): string; -} diff --git a/playwright/Dockerfile b/playwright/Dockerfile deleted file mode 100644 index 6d812037de..0000000000 --- a/playwright/Dockerfile +++ /dev/null @@ -1,9 +0,0 @@ -FROM mcr.microsoft.com/playwright:v1.51.0-noble - -WORKDIR /work - -# fonts-dejavu is needed for the same RTL rendering as on CI -RUN apt-get update && apt-get -y install docker.io fonts-dejavu - -COPY docker-entrypoint.sh /opt/docker-entrypoint.sh -ENTRYPOINT ["bash", "/opt/docker-entrypoint.sh"] diff --git a/playwright/docker-entrypoint.sh b/playwright/docker-entrypoint.sh deleted file mode 100644 index 241528a29a..0000000000 --- a/playwright/docker-entrypoint.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash - -set -e - -npx playwright test --update-snapshots --reporter line $@ diff --git a/playwright/e2e/csAPI.ts b/playwright/e2e/csAPI.ts index 4f12076139..c622ac99ce 100644 --- a/playwright/e2e/csAPI.ts +++ b/playwright/e2e/csAPI.ts @@ -5,11 +5,11 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import { type APIRequestContext } from "playwright-core"; +import { type APIRequestContext } from "@playwright/test"; import { type KeyBackupInfo } from "matrix-js-sdk/src/crypto-api"; +import { ClientServerApi } from "@element-hq/element-web-playwright-common/lib/utils/api.js"; import { type HomeserverInstance } from "../plugins/homeserver"; -import { ClientServerApi } from "../plugins/utils/api.ts"; /** * A small subset of the Client-Server API used to manipulate the state of the diff --git a/playwright/e2e/editing/editing.spec.ts b/playwright/e2e/editing/editing.spec.ts index 528bcc5962..6f8e68bbc3 100644 --- a/playwright/e2e/editing/editing.spec.ts +++ b/playwright/e2e/editing/editing.spec.ts @@ -267,7 +267,6 @@ test.describe("Editing", () => { app, room, axe, - checkA11y, }) => { axe.disableRules("color-contrast"); // XXX: We have some known contrast issues here @@ -282,7 +281,7 @@ test.describe("Editing", () => { const line = tile.locator(".mx_EventTile_line"); await line.hover(); await line.getByRole("button", { name: "Edit" }).click(); - await checkA11y(); + await expect(axe).toHaveNoViolations(); const editComposer = page.getByRole("textbox", { name: "Edit message" }); await editComposer.pressSequentially("Foo"); await editComposer.press("Backspace"); @@ -290,7 +289,7 @@ test.describe("Editing", () => { await editComposer.press("Backspace"); await editComposer.press("Enter"); await app.getComposerField().hover(); // XXX: move the hover to get rid of the "Edit" tooltip - await checkA11y(); + await expect(axe).toHaveNoViolations(); } await expect( page.locator(".mx_RoomView_body .mx_EventTile[data-scroll-tokens]", { hasText: "Message" }), @@ -305,7 +304,6 @@ test.describe("Editing", () => { user, app, axe, - checkA11y, bot: bob, }) => { // This tests the behaviour when a message has been edited some time after it has been sent, and we diff --git a/playwright/e2e/login/login-consent.spec.ts b/playwright/e2e/login/login-consent.spec.ts index ec6dfbf7c2..28948dc3b9 100644 --- a/playwright/e2e/login/login-consent.spec.ts +++ b/playwright/e2e/login/login-consent.spec.ts @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import { type Page } from "playwright-core"; +import { type Page } from "@playwright/test"; import { expect, test } from "../../element-web-test"; import { selectHomeserver } from "../utils"; @@ -120,7 +120,7 @@ test.describe("Login", () => { credentials, page, homeserver, - checkA11y, + axe, }) => { await page.goto("/"); @@ -149,7 +149,7 @@ test.describe("Login", () => { await expect(page.getByRole("textbox", { name: "Username" })).toBeVisible(); // Disabled because flaky - see https://github.com/vector-im/element-web/issues/24688 // cy.percySnapshot("Login"); - await checkA11y(); + await expect(axe).toHaveNoViolations(); await page.getByRole("textbox", { name: "Username" }).fill(credentials.username); await page.getByPlaceholder("Password").fill(credentials.password); diff --git a/playwright/e2e/messages/messages.spec.ts b/playwright/e2e/messages/messages.spec.ts index e5d65caff5..f430d6b18b 100644 --- a/playwright/e2e/messages/messages.spec.ts +++ b/playwright/e2e/messages/messages.spec.ts @@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details. /* See readme.md for tips on writing these tests. */ -import { type Locator, type Page } from "playwright-core"; +import { type Locator, type Page } from "@playwright/test"; import { test, expect } from "../../element-web-test"; diff --git a/playwright/e2e/oidc/index.ts b/playwright/e2e/oidc/index.ts index b1635fbd78..1989e8764f 100644 --- a/playwright/e2e/oidc/index.ts +++ b/playwright/e2e/oidc/index.ts @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import { type MailpitClient } from "mailpit-api"; +import { type MailpitClient } from "@element-hq/element-web-playwright-common/lib/testcontainers"; import { type Page } from "@playwright/test"; import { expect } from "../../element-web-test"; diff --git a/playwright/e2e/register/email.spec.ts b/playwright/e2e/register/email.spec.ts index d351893f8b..bf8a2157f5 100644 --- a/playwright/e2e/register/email.spec.ts +++ b/playwright/e2e/register/email.spec.ts @@ -34,7 +34,7 @@ test.describe("Email Registration", async () => { test( "registers an account and lands on the home page", { tag: "@screenshot" }, - async ({ page, mailpitClient, request, checkA11y }) => { + async ({ page, mailpitClient, request, axe }) => { await expect(page.getByRole("textbox", { name: "Username" })).toBeVisible(); // Hide the server text as it contains the randomly allocated Homeserver port const screenshotOptions = { mask: [page.locator(".mx_ServerPicker_server")] }; @@ -47,7 +47,7 @@ test.describe("Email Registration", async () => { await expect(page.getByText("Check your email to continue")).toBeVisible(); await expect(page).toMatchScreenshot("registration_check_your_email.png", screenshotOptions); - await checkA11y(); + await expect(axe).toHaveNoViolations(); await expect(page.getByText("An error was encountered when sending the email")).not.toBeVisible(); diff --git a/playwright/e2e/register/register.spec.ts b/playwright/e2e/register/register.spec.ts index 3df1d01678..c481b6ac43 100644 --- a/playwright/e2e/register/register.spec.ts +++ b/playwright/e2e/register/register.spec.ts @@ -33,12 +33,12 @@ test.describe("Registration", () => { test( "registers an account and lands on the home screen", { tag: "@screenshot" }, - async ({ homeserver, page, checkA11y, crypto }) => { + async ({ homeserver, page, axe, crypto }) => { await page.getByRole("button", { name: "Edit", exact: true }).click(); await expect(page.getByRole("button", { name: "Continue", exact: true })).toBeVisible(); await expect(page.locator(".mx_Dialog")).toMatchScreenshot("server-picker.png"); - await checkA11y(); + await expect(axe).toHaveNoViolations(); await page.getByRole("textbox", { name: "Other homeserver" }).fill(homeserver.baseUrl); await page.getByRole("button", { name: "Continue", exact: true }).click(); @@ -52,7 +52,7 @@ test.describe("Registration", () => { includeDialogBackground: true, }; await expect(page).toMatchScreenshot("registration.png", screenshotOptions); - await checkA11y(); + await expect(axe).toHaveNoViolations(); await page.getByRole("textbox", { name: "Username", exact: true }).fill("alice"); await page.getByPlaceholder("Password", { exact: true }).fill("totally a great password"); @@ -62,12 +62,12 @@ test.describe("Registration", () => { const dialog = page.getByRole("dialog"); await expect(dialog).toBeVisible(); await expect(page).toMatchScreenshot("email-prompt.png", screenshotOptions); - await checkA11y(); + await expect(axe).toHaveNoViolations(); await dialog.getByRole("button", { name: "Continue", exact: true }).click(); await expect(page.locator(".mx_InteractiveAuthEntryComponents_termsPolicy")).toBeVisible(); await expect(page).toMatchScreenshot("terms-prompt.png", screenshotOptions); - await checkA11y(); + await expect(axe).toHaveNoViolations(); const termsPolicy = page.locator(".mx_InteractiveAuthEntryComponents_termsPolicy"); await termsPolicy.getByRole("checkbox").click(); // Click the checkbox before terms of service anchor link diff --git a/playwright/e2e/spaces/spaces.spec.ts b/playwright/e2e/spaces/spaces.spec.ts index 01a385c9df..dce7515ef0 100644 --- a/playwright/e2e/spaces/spaces.spec.ts +++ b/playwright/e2e/spaces/spaces.spec.ts @@ -227,7 +227,7 @@ test.describe("Spaces", () => { test( "should render subspaces in the space panel only when expanded", { tag: "@screenshot" }, - async ({ page, app, user, axe, checkA11y }) => { + async ({ page, app, user, axe }) => { axe.disableRules([ // Disable this check as it triggers on nested roving tab index elements which are in practice fine "nested-interactive", @@ -249,7 +249,7 @@ test.describe("Spaces", () => { await expect(spaceTree.getByRole("button", { name: "Root Space" })).toBeVisible(); await expect(spaceTree.getByRole("button", { name: "Child Space" })).not.toBeVisible(); - await checkA11y(); + await expect(axe).toHaveNoViolations(); await expect(page.locator(".mx_SpacePanel")).toMatchScreenshot("space-panel-collapsed.png"); // This finds the expand button with the class name "mx_SpaceButton_toggleCollapse". Note there is another @@ -261,7 +261,7 @@ test.describe("Spaces", () => { await expect(item).toBeVisible(); await expect(item.locator(".mx_SpaceItem", { hasText: "Child Space" })).toBeVisible(); - await checkA11y(); + await expect(axe).toHaveNoViolations(); await expect(page.locator(".mx_SpacePanel")).toMatchScreenshot("space-panel-expanded.png"); }, ); diff --git a/playwright/e2e/timeline/timeline.spec.ts b/playwright/e2e/timeline/timeline.spec.ts index 5c1f99511e..7b13d1ccb1 100644 --- a/playwright/e2e/timeline/timeline.spec.ts +++ b/playwright/e2e/timeline/timeline.spec.ts @@ -277,7 +277,7 @@ test.describe("Timeline", () => { test( "should add inline start margin to an event line on IRC layout", { tag: "@screenshot" }, - async ({ page, app, room, axe, checkA11y }) => { + async ({ page, app, room, axe }) => { axe.disableRules("color-contrast"); await page.goto(`/#/room/${room.roomId}`); @@ -318,7 +318,7 @@ test.describe("Timeline", () => { `, }, ); - await checkA11y(); + await expect(axe).toHaveNoViolations(); }, ); }); @@ -743,68 +743,64 @@ test.describe("Timeline", () => { ).toBeVisible(); }); - test( - "should render url previews", - { tag: "@screenshot" }, - async ({ page, app, room, axe, checkA11y, context }) => { - axe.disableRules("color-contrast"); + test("should render url previews", { tag: "@screenshot" }, async ({ page, app, room, axe, context }) => { + axe.disableRules("color-contrast"); - // Element Web uses a Service Worker to rewrite unauthenticated media requests to authenticated ones, but - // the page can't see this happening. We intercept the route at the BrowserContext to ensure we get it - // post-worker, but we can't waitForResponse on that, so the page context is still used there. Because - // the page doesn't see the rewrite, it waits for the unauthenticated route. This is only confusing until - // the js-sdk (and thus the app as a whole) switches to using authenticated endpoints by default, hopefully. - await context.route( - "**/_matrix/client/v1/media/thumbnail/matrix.org/2022-08-16_yaiSVSRIsNFfxDnV?*", - async (route) => { - await route.fulfill({ - path: "playwright/sample-files/riot.png", - }); - }, - ); - await page.route( - "**/_matrix/media/v3/preview_url?url=https%3A%2F%2Fcall.element.io%2F&ts=*", - async (route) => { - await route.fulfill({ - json: { - "og:title": "Element Call", - "og:description": null, - "og:image:width": 48, - "og:image:height": 48, - "og:image": "mxc://matrix.org/2022-08-16_yaiSVSRIsNFfxDnV", - "og:image:type": "image/png", - "matrix:image:size": 2121, - }, - }); - }, - ); + // Element Web uses a Service Worker to rewrite unauthenticated media requests to authenticated ones, but + // the page can't see this happening. We intercept the route at the BrowserContext to ensure we get it + // post-worker, but we can't waitForResponse on that, so the page context is still used there. Because + // the page doesn't see the rewrite, it waits for the unauthenticated route. This is only confusing until + // the js-sdk (and thus the app as a whole) switches to using authenticated endpoints by default, hopefully. + await context.route( + "**/_matrix/client/v1/media/thumbnail/matrix.org/2022-08-16_yaiSVSRIsNFfxDnV?*", + async (route) => { + await route.fulfill({ + path: "playwright/sample-files/riot.png", + }); + }, + ); + await page.route( + "**/_matrix/media/v3/preview_url?url=https%3A%2F%2Fcall.element.io%2F&ts=*", + async (route) => { + await route.fulfill({ + json: { + "og:title": "Element Call", + "og:description": null, + "og:image:width": 48, + "og:image:height": 48, + "og:image": "mxc://matrix.org/2022-08-16_yaiSVSRIsNFfxDnV", + "og:image:type": "image/png", + "matrix:image:size": 2121, + }, + }); + }, + ); - const requestPromises: Promise[] = [ - page.waitForResponse("**/_matrix/media/v3/preview_url?url=https%3A%2F%2Fcall.element.io%2F&ts=*"), - // see context.route above for why we listen for the unauthenticated endpoint - page.waitForResponse("**/_matrix/media/v3/thumbnail/matrix.org/2022-08-16_yaiSVSRIsNFfxDnV?*"), - ]; + const requestPromises: Promise[] = [ + page.waitForResponse("**/_matrix/media/v3/preview_url?url=https%3A%2F%2Fcall.element.io%2F&ts=*"), + // see context.route above for why we listen for the unauthenticated endpoint + page.waitForResponse("**/_matrix/media/v3/thumbnail/matrix.org/2022-08-16_yaiSVSRIsNFfxDnV?*"), + ]; - await app.client.sendMessage(room.roomId, "https://call.element.io/"); - await page.goto(`/#/room/${room.roomId}`); + await app.client.sendMessage(room.roomId, "https://call.element.io/"); + await page.goto(`/#/room/${room.roomId}`); - await expect(page.locator(".mx_LinkPreviewWidget").getByText("Element Call")).toBeVisible(); - await Promise.all(requestPromises); + await expect(page.locator(".mx_LinkPreviewWidget").getByText("Element Call")).toBeVisible(); + await Promise.all(requestPromises); - await checkA11y(); + await expect(axe).toHaveNoViolations(); - await app.timeline.scrollToBottom(); - await expect(page.locator(".mx_EventTile_last")).toMatchScreenshot("url-preview.png", { - // Exclude timestamp and read marker from snapshot - mask: [page.locator(".mx_MessageTimestamp")], - css: ` + await app.timeline.scrollToBottom(); + await expect(page.locator(".mx_EventTile_last")).toMatchScreenshot("url-preview.png", { + // Exclude timestamp and read marker from snapshot + mask: [page.locator(".mx_MessageTimestamp")], + css: ` .mx_TopUnreadMessagesBar, .mx_MessagePanel_myReadMarker { display: none !important; } `, - }); - }, - ); + }); + }); test.describe("on search results panel", () => { test( diff --git a/playwright/element-web-test.ts b/playwright/element-web-test.ts index 24124d5474..8520533461 100644 --- a/playwright/element-web-test.ts +++ b/playwright/element-web-test.ts @@ -7,18 +7,18 @@ Please see LICENSE files in the repository root for full details. */ import { - expect as baseExpect, - type Locator, - type Page, type ExpectMatcherState, - type ElementHandle, + type MatcherReturnType, + type Page, + type Locator, type PlaywrightTestArgs, type Fixtures as _Fixtures, } from "@playwright/test"; -import { sanitizeForFilePath } from "playwright-core/lib/utils"; -import AxeBuilder from "@axe-core/playwright"; -import _ from "lodash"; -import { extname } from "node:path"; +import { + type TestFixtures as BaseTestFixtures, + expect as baseExpect, + type ToMatchScreenshotOptions, +} from "@element-hq/element-web-playwright-common"; import type { IConfigOptions } from "../src/IConfigOptions"; import { type Credentials } from "./plugins/homeserver"; @@ -27,71 +27,22 @@ import { Crypto } from "./pages/crypto"; import { Toasts } from "./pages/toasts"; import { Bot, type CreateBotOpts } from "./pages/bot"; import { Webserver } from "./plugins/webserver"; -import { type Options, type Services, test as base } from "./services.ts"; +import { type WorkerOptions, type Services, test as base } from "./services"; // Enable experimental service worker support // See https://playwright.dev/docs/service-workers-experimental#how-to-enable process.env["PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS"] = "1"; -// This is deliberately quite a minimal config.json, so that we can test that the default settings actually work. -const CONFIG_JSON: Partial = { - // The default language is set here for test consistency - setting_defaults: { - language: "en-GB", - }, - - // the location tests want a map style url. - map_style_url: "https://api.maptiler.com/maps/streets/style.json?key=fU3vlMsMn4Jb6dnEIFsx", - - features: { - // We don't want to go through the feature announcement during the e2e test - feature_release_announcement: false, - }, -}; +declare module "@element-hq/element-web-playwright-common" { + // Improve the type for the config fixture based on the real type + export interface Config extends Omit {} +} export interface CredentialsWithDisplayName extends Credentials { displayName: string; } -export interface TestFixtures { - axe: AxeBuilder; - checkA11y: () => Promise; - - /** - * The contents of the config.json to send when the client requests it. - */ - config: typeof CONFIG_JSON; - - /** - * The displayname to use for the user registered in {@link #credentials}. - * - * To set it, call `test.use({ displayName: "myDisplayName" })` in the test file or `describe` block. - * See {@link https://playwright.dev/docs/api/class-test#test-use}. - */ - displayName?: string; - - /** - * A test fixture which registers a test user on the {@link #homeserver} and supplies the details - * of the registered user. - */ - credentials: CredentialsWithDisplayName; - - /** - * The same as {@link https://playwright.dev/docs/api/class-fixtures#fixtures-page|`page`}, - * but adds an initScript which will populate localStorage with the user's details from - * {@link #credentials} and {@link #homeserver}. - * - * Similar to {@link #user}, but doesn't load the app. - */ - pageWithCredentials: Page; - - /** - * A (rather poorly-named) test fixture which registers a user per {@link #credentials}, stores - * the credentials into localStorage per {@link #homeserver}, and then loads the front page of the - * app. - */ - user: CredentialsWithDisplayName; - +export interface TestFixtures extends BaseTestFixtures { /** * The same as {@link https://playwright.dev/docs/api/class-fixtures#fixtures-page|`page`}, * but wraps the returned `Page` in a class of utilities for interacting with the Element-Web UI, @@ -105,13 +56,11 @@ export interface TestFixtures { uut?: Locator; // Unit Under Test, useful place to refer a prepared locator botCreateOpts: CreateBotOpts; bot: Bot; - labsFlags: string[]; webserver: Webserver; - disablePresence: boolean; } type CombinedTestFixtures = PlaywrightTestArgs & TestFixtures; -export type Fixtures = _Fixtures; +export type Fixtures = _Fixtures; export const test = base.extend({ context: async ({ context }, use, testInfo) => { // We skip tests instead of using grep-invert to still surface the counts in the html report @@ -121,102 +70,12 @@ export const test = base.extend({ ); await use(context); }, - disablePresence: false, - config: {}, // We merge this atop the default CONFIG_JSON in the page fixture to make extending it easier - page: async ({ homeserver, context, page, config, labsFlags, disablePresence }, use) => { - await context.route(`http://localhost:8080/config.json*`, async (route) => { - const json = { - ...CONFIG_JSON, - ...config, - default_server_config: { - "m.homeserver": { - base_url: homeserver.baseUrl, - }, - ...config.default_server_config, - }, - }; - json["features"] = { - ...json["features"], - // Enable the lab features - ...labsFlags.reduce((obj, flag) => { - obj[flag] = true; - return obj; - }, {}), - }; - if (disablePresence) { - json["enable_presence_by_hs_url"] = { - [homeserver.baseUrl]: false, - }; - } - await route.fulfill({ json }); - }); - await use(page); + + axe: async ({ axe }, use) => { + // Exclude floating UI for now + await use(axe.exclude("[data-floating-ui-portal]")); }, - displayName: undefined, - credentials: async ({ context, homeserver, displayName: testDisplayName }, use, testInfo) => { - const names = ["Alice", "Bob", "Charlie", "Daniel", "Eve", "Frank", "Grace", "Hannah", "Isaac", "Judy"]; - const password = _.uniqueId("password_"); - const displayName = testDisplayName ?? _.sample(names)!; - - const credentials = await homeserver.registerUser(`user_${testInfo.testId}`, password, displayName); - console.log(`Registered test user ${credentials.userId} with displayname ${displayName}`); - - await use({ - ...credentials, - displayName, - }); - }, - labsFlags: [], - - pageWithCredentials: async ({ page, homeserver, credentials }, use) => { - await page.addInitScript( - ({ baseUrl, credentials }) => { - // Seed the localStorage with the required credentials - window.localStorage.setItem("mx_hs_url", baseUrl); - window.localStorage.setItem("mx_user_id", credentials.userId); - window.localStorage.setItem("mx_access_token", credentials.accessToken); - window.localStorage.setItem("mx_device_id", credentials.deviceId); - window.localStorage.setItem("mx_is_guest", "false"); - window.localStorage.setItem("mx_has_pickle_key", "false"); - window.localStorage.setItem("mx_has_access_token", "true"); - - window.localStorage.setItem( - "mx_local_settings", - JSON.stringify({ - // Retain any other settings which may have already been set - ...JSON.parse(window.localStorage.getItem("mx_local_settings") || "{}"), - // Ensure the language is set to a consistent value - language: "en", - }), - ); - }, - { baseUrl: homeserver.baseUrl, credentials }, - ); - await use(page); - }, - - user: async ({ pageWithCredentials: page, credentials }, use) => { - await page.goto("/"); - await page.waitForSelector(".mx_MatrixChat", { timeout: 30000 }); - await use(credentials); - }, - - axe: async ({ page }, use) => { - await use(new AxeBuilder({ page }).exclude("[data-floating-ui-portal]")); - }, - checkA11y: async ({ axe }, use, testInfo) => - use(async () => { - const results = await axe.analyze(); - - await testInfo.attach("accessibility-scan-results", { - body: JSON.stringify(results, null, 2), - contentType: "application/json", - }); - - expect(results.violations).toEqual([]); - }), - app: async ({ page }, use) => { const app = new ElementAppPage(page); await use(app); @@ -244,35 +103,23 @@ export const test = base.extend({ }, }); -// Based on https://github.com/microsoft/playwright/blob/2b77ed4d7aafa85a600caa0b0d101b72c8437eeb/packages/playwright/src/util.ts#L206C8-L210C2 -function sanitizeFilePathBeforeExtension(filePath: string): string { - const ext = extname(filePath); - const base = filePath.substring(0, filePath.length - ext.length); - return sanitizeForFilePath(base) + ext; +interface ExtendedToMatchScreenshotOptions extends ToMatchScreenshotOptions { + includeDialogBackground?: boolean; + showTooltips?: boolean; + timeout?: number; } -export const expect = baseExpect.extend({ - async toMatchScreenshot( +type Expectations = { + toMatchScreenshot: ( this: ExpectMatcherState, receiver: Page | Locator, name: `${string}.png`, - options?: { - mask?: Array; - includeDialogBackground?: boolean; - showTooltips?: boolean; - timeout?: number; - css?: string; - }, - ) { - const testInfo = test.info(); - if (!testInfo) throw new Error(`toMatchScreenshot() must be called during the test`); - - if (!testInfo.tags.includes("@screenshot")) { - throw new Error("toMatchScreenshot() must be used in a test tagged with @screenshot"); - } - - const page = "page" in receiver ? receiver.page() : receiver; + options?: ExtendedToMatchScreenshotOptions, + ) => Promise; +}; +export const expect = baseExpect.extend({ + async toMatchScreenshot(receiver, name, options) { let css = ` .mx_MessagePanel_myReadMarker { display: none !important; @@ -322,21 +169,9 @@ export const expect = baseExpect.extend({ css += options.css; } - // We add a custom style tag before taking screenshots - const style = (await page.addStyleTag({ - content: css, - })) as ElementHandle; - - const screenshotName = sanitizeFilePathBeforeExtension(name); - await baseExpect(receiver).toHaveScreenshot(screenshotName, options); - - await style.evaluate((tag) => tag.remove()); - - testInfo.annotations.push({ - // `_` prefix hides it from the HTML reporter - type: "_screenshot", - // include a path relative to `playwright/snapshots/` - description: testInfo.snapshotPath(screenshotName).split("/playwright/snapshots/", 2)[1], + await baseExpect(receiver).toMatchScreenshot(name, { + ...options, + css, }); return { pass: true, message: () => "", name: "toMatchScreenshot" }; diff --git a/playwright/logger.ts b/playwright/logger.ts deleted file mode 100644 index da70582c38..0000000000 --- a/playwright/logger.ts +++ /dev/null @@ -1,63 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial -Please see LICENSE files in the repository root for full details. -*/ - -import { type BrowserContext, type Page, type TestInfo } from "@playwright/test"; -import { type Readable } from "stream"; -import stripAnsi from "strip-ansi"; - -export class Logger { - private pages: Page[] = []; - private logs: Record = {}; - - public getConsumer(container: string) { - this.logs[container] = ""; - return (stream: Readable) => { - stream.on("data", (chunk) => { - this.logs[container] += chunk.toString(); - }); - stream.on("err", (chunk) => { - this.logs[container] += "ERR " + chunk.toString(); - }); - }; - } - - public async onTestStarted(context: BrowserContext) { - this.pages = []; - for (const id in this.logs) { - if (id.startsWith("page-")) { - delete this.logs[id]; - } else { - this.logs[id] = ""; - } - } - - context.on("console", (msg) => { - const page = msg.page(); - let pageIdx = this.pages.indexOf(page); - if (pageIdx === -1) { - this.pages.push(page); - pageIdx = this.pages.length - 1; - this.logs[`page-${pageIdx}`] = `Console logs for page with URL: ${page.url()}\n\n`; - } - const type = msg.type(); - const text = msg.text(); - this.logs[`page-${pageIdx}`] += `${type}: ${text}\n`; - }); - } - - public async onTestFinished(testInfo: TestInfo) { - if (testInfo.status !== "passed") { - for (const id in this.logs) { - if (!this.logs[id]) continue; - await testInfo.attach(id, { - body: stripAnsi(this.logs[id]), - contentType: "text/plain", - }); - } - } - } -} diff --git a/playwright/plugins/homeserver/dendrite/index.ts b/playwright/plugins/homeserver/dendrite/index.ts index bad52a869d..c139650405 100644 --- a/playwright/plugins/homeserver/dendrite/index.ts +++ b/playwright/plugins/homeserver/dendrite/index.ts @@ -6,8 +6,8 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import { type Options } from "../../../services.ts"; +import { type WorkerOptions } from "../../../services.ts"; -export const isDendrite = ({ homeserverType }: Options): boolean => { +export const isDendrite = ({ homeserverType }: WorkerOptions): boolean => { return homeserverType === "dendrite" || homeserverType === "pinecone"; }; diff --git a/playwright/plugins/homeserver/index.ts b/playwright/plugins/homeserver/index.ts index 04b1ad77f3..0571cd9615 100644 --- a/playwright/plugins/homeserver/index.ts +++ b/playwright/plugins/homeserver/index.ts @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import { type ClientServerApi } from "../utils/api.ts"; +import { type ClientServerApi } from "@element-hq/element-web-playwright-common/lib/utils/api.js"; export interface HomeserverInstance { readonly baseUrl: string; diff --git a/playwright/plugins/homeserver/synapse/consentHomeserver.ts b/playwright/plugins/homeserver/synapse/consentHomeserver.ts index 83d47512cf..9b3316bf57 100644 --- a/playwright/plugins/homeserver/synapse/consentHomeserver.ts +++ b/playwright/plugins/homeserver/synapse/consentHomeserver.ts @@ -6,30 +6,19 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ +import { type SynapseContainer } from "@element-hq/element-web-playwright-common/lib/testcontainers"; + import { type Fixtures } from "../../../element-web-test.ts"; export const consentHomeserver: Fixtures = { _homeserver: [ async ({ _homeserver: container, mailpit }, use) => { - container + (container as SynapseContainer) .withCopyDirectoriesToContainer([ { source: "playwright/plugins/homeserver/synapse/res", target: "/data/res" }, ]) + .withSmtpServer(mailpit) .withConfig({ - email: { - enable_notifs: false, - smtp_host: "mailpit", - smtp_port: 1025, - smtp_user: "username", - smtp_pass: "password", - require_transport_security: false, - notif_from: "Your Friendly %(app)s homeserver ", - app_name: "Matrix", - notif_template_html: "notif_mail.html", - notif_template_text: "notif_mail.txt", - notif_for_new_users: true, - client_base_url: "http://localhost/element", - }, user_consent: { template_dir: "/data/res/templates/privacy", version: "1.0", diff --git a/playwright/plugins/homeserver/synapse/masHomeserver.ts b/playwright/plugins/homeserver/synapse/masHomeserver.ts index 96fcc80bdd..342737d80d 100644 --- a/playwright/plugins/homeserver/synapse/masHomeserver.ts +++ b/playwright/plugins/homeserver/synapse/masHomeserver.ts @@ -6,7 +6,8 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import { MatrixAuthenticationServiceContainer } from "../../../testcontainers/mas.ts"; +import { MatrixAuthenticationServiceContainer } from "@element-hq/element-web-playwright-common/lib/testcontainers"; + import { type Fixtures } from "../../../element-web-test.ts"; export const masHomeserver: Fixtures = { diff --git a/playwright/plugins/oauth_server/index.ts b/playwright/plugins/oauth_server/index.ts index 8a7e9cd8ba..446426a9c1 100644 --- a/playwright/plugins/oauth_server/index.ts +++ b/playwright/plugins/oauth_server/index.ts @@ -10,8 +10,7 @@ import http from "http"; import express from "express"; import { type AddressInfo } from "net"; import { type TestInfo } from "@playwright/test"; - -import { randB64Bytes } from "../utils/rand.ts"; +import { randB64Bytes } from "@element-hq/element-web-playwright-common/lib/utils/rand.js"; export class OAuthServer { private server?: http.Server; diff --git a/playwright/plugins/utils/api.ts b/playwright/plugins/utils/api.ts deleted file mode 100644 index 90c0e2739f..0000000000 --- a/playwright/plugins/utils/api.ts +++ /dev/null @@ -1,76 +0,0 @@ -/* -Copyright 2025 New Vector Ltd. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial -Please see LICENSE files in the repository root for full details. -*/ - -import { type APIRequestContext } from "@playwright/test"; - -import { type Credentials } from "../homeserver"; - -export type Verb = "GET" | "POST" | "PUT" | "DELETE"; - -export class Api { - private _request?: APIRequestContext; - - constructor(private readonly baseUrl: string) {} - - public setRequest(request: APIRequestContext): void { - this._request = request; - } - - public async request(verb: "GET", path: string, token?: string, data?: never): Promise; - public async request(verb: Verb, path: string, token?: string, data?: object): Promise; - public async request(verb: Verb, path: string, token?: string, data?: object): Promise { - const url = `${this.baseUrl}${path}`; - const res = await this._request.fetch(url, { - data, - method: verb, - headers: token - ? { - Authorization: `Bearer ${token}`, - } - : undefined, - }); - - if (!res.ok()) { - throw new Error( - `Request to ${url} failed with status ${res.status()}: ${JSON.stringify(await res.json())}`, - ); - } - - return res.json(); - } -} - -export class ClientServerApi extends Api { - constructor(baseUrl: string) { - super(`${baseUrl}/_matrix/client`); - } - - public async loginUser(userId: string, password: string): Promise { - const json = await this.request<{ - access_token: string; - user_id: string; - device_id: string; - home_server: string; - }>("POST", "/v3/login", undefined, { - type: "m.login.password", - identifier: { - type: "m.id.user", - user: userId, - }, - password: password, - }); - - return { - password, - accessToken: json.access_token, - userId: json.user_id, - deviceId: json.device_id, - homeServer: json.home_server || json.user_id.split(":").slice(1).join(":"), - username: userId.slice(1).split(":")[0], - }; - } -} diff --git a/playwright/plugins/utils/object.ts b/playwright/plugins/utils/object.ts deleted file mode 100644 index bfb92fecec..0000000000 --- a/playwright/plugins/utils/object.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -/** - * Deep copy the given object. The object MUST NOT have circular references and - * MUST NOT have functions. - * @param obj - The object to deep copy. - * @returns A copy of the object without any references to the original. - */ -export function deepCopy(obj: T): T { - return JSON.parse(JSON.stringify(obj)); -} diff --git a/playwright/plugins/utils/port.ts b/playwright/plugins/utils/port.ts deleted file mode 100644 index b54e251f2f..0000000000 --- a/playwright/plugins/utils/port.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial -Please see LICENSE files in the repository root for full details. -*/ - -import * as net from "net"; - -export async function getFreePort(): Promise { - return new Promise((resolve) => { - const srv = net.createServer(); - srv.listen(0, () => { - const port = (srv.address()).port; - srv.close(() => resolve(port)); - }); - }); -} diff --git a/playwright/plugins/utils/rand.ts b/playwright/plugins/utils/rand.ts deleted file mode 100644 index 94f723f0a6..0000000000 --- a/playwright/plugins/utils/rand.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2023 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial -Please see LICENSE files in the repository root for full details. -*/ - -import crypto from "node:crypto"; - -export function randB64Bytes(numBytes: number): string { - return crypto.randomBytes(numBytes).toString("base64").replace(/=*$/, ""); -} diff --git a/playwright/plugins/webserver/index.ts b/playwright/plugins/webserver/index.ts index ba9b9e9706..fe236116bc 100644 --- a/playwright/plugins/webserver/index.ts +++ b/playwright/plugins/webserver/index.ts @@ -6,8 +6,8 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import * as http from "http"; -import { type AddressInfo } from "net"; +import * as http from "node:http"; +import { type AddressInfo } from "node:net"; export class Webserver { private server?: http.Server; diff --git a/playwright/services.ts b/playwright/services.ts index 162f4e9fbb..8ecf10e20e 100644 --- a/playwright/services.ts +++ b/playwright/services.ts @@ -5,113 +5,32 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import { test as base } from "@playwright/test"; -import { type MailpitClient } from "mailpit-api"; -import { Network, type StartedNetwork } from "testcontainers"; -import { PostgreSqlContainer, type StartedPostgreSqlContainer } from "@testcontainers/postgresql"; +import { test as base } from "@element-hq/element-web-playwright-common"; +import { + type Services as BaseServices, + type WorkerOptions as BaseWorkerOptions, +} from "@element-hq/element-web-playwright-common/lib/fixtures"; +import { type HomeserverContainer } from "@element-hq/element-web-playwright-common/lib/testcontainers"; -import { type SynapseConfig, SynapseContainer } from "./testcontainers/synapse.ts"; -import { Logger } from "./logger.ts"; -import { type StartedMatrixAuthenticationServiceContainer } from "./testcontainers/mas.ts"; -import { type HomeserverContainer, type StartedHomeserverContainer } from "./testcontainers/HomeserverContainer.ts"; -import { MailhogContainer, type StartedMailhogContainer } from "./testcontainers/mailpit.ts"; import { type OAuthServer } from "./plugins/oauth_server"; -import { DendriteContainer, PineconeContainer } from "./testcontainers/dendrite.ts"; +import { DendriteContainer, PineconeContainer } from "./testcontainers/dendrite"; import { type HomeserverType } from "./plugins/homeserver"; +import { SynapseContainer } from "./testcontainers/synapse"; -export interface TestFixtures { - mailpitClient: MailpitClient; -} - -export interface Services { - logger: Logger; - - network: StartedNetwork; - postgres: StartedPostgreSqlContainer; - mailpit: StartedMailhogContainer; - - synapseConfig: SynapseConfig; - _homeserver: HomeserverContainer; - homeserver: StartedHomeserverContainer; - // Set in masHomeserver only - mas?: StartedMatrixAuthenticationServiceContainer; +export interface Services extends BaseServices { // Set in legacyOAuthHomeserver only oAuthServer?: OAuthServer; } -export interface Options { +export interface WorkerOptions extends BaseWorkerOptions { homeserverType: HomeserverType; } -export const test = base.extend({ - logger: [ - // eslint-disable-next-line no-empty-pattern - async ({}, use) => { - const logger = new Logger(); - await use(logger); - }, - { scope: "worker" }, - ], - network: [ - // eslint-disable-next-line no-empty-pattern - async ({}, use) => { - const network = await new Network().start(); - await use(network); - await network.stop(); - }, - { scope: "worker" }, - ], - postgres: [ - async ({ logger, network }, use) => { - const container = await new PostgreSqlContainer() - .withNetwork(network) - .withNetworkAliases("postgres") - .withLogConsumer(logger.getConsumer("postgres")) - .withTmpFs({ - "/dev/shm/pgdata/data": "", - }) - .withEnvironment({ - PG_DATA: "/dev/shm/pgdata/data", - }) - .withCommand([ - "-c", - "shared_buffers=128MB", - "-c", - `fsync=off`, - "-c", - `synchronous_commit=off`, - "-c", - "full_page_writes=off", - ]) - .start(); - await use(container); - await container.stop(); - }, - { scope: "worker" }, - ], - - mailpit: [ - async ({ logger, network }, use) => { - const container = await new MailhogContainer() - .withNetwork(network) - .withNetworkAliases("mailpit") - .withLogConsumer(logger.getConsumer("mailpit")) - .start(); - await use(container); - await container.stop(); - }, - { scope: "worker" }, - ], - mailpitClient: async ({ mailpit: container }, use) => { - await container.client.deleteMessages(); - await use(container.client); - }, - - synapseConfig: [{}, { scope: "worker" }], +export const test = base.extend<{}, Services & WorkerOptions>({ homeserverType: ["synapse", { option: true, scope: "worker" }], _homeserver: [ async ({ homeserverType }, use) => { - let container: HomeserverContainer; + let container: HomeserverContainer; switch (homeserverType) { case "synapse": container = new SynapseContainer(); @@ -128,46 +47,12 @@ export const test = base.extend({ }, { scope: "worker" }, ], - homeserver: [ - async ({ homeserverType, logger, network, _homeserver: homeserver, synapseConfig, mas }, use) => { - if (homeserver instanceof SynapseContainer) { - homeserver.withConfig(synapseConfig); - } - const container = await homeserver - .withNetwork(network) - .withNetworkAliases("homeserver") - .withLogConsumer(logger.getConsumer(homeserverType)) - .withMatrixAuthenticationService(mas) - .start(); - await use(container); - await container.stop(); - }, - { scope: "worker" }, - ], - mas: [ - // eslint-disable-next-line no-empty-pattern - async ({}, use) => { - // we stub the mas fixture to allow `homeserver` to depend on it to ensure - // when it is specified by `masHomeserver` it is started before the homeserver - await use(undefined); - }, - { scope: "worker" }, - ], - - context: async ( - { homeserverType, synapseConfig, logger, context, request, _homeserver, homeserver }, - use, - testInfo, - ) => { + context: async ({ homeserverType, synapseConfig, context, _homeserver }, use, testInfo) => { testInfo.skip( !(_homeserver instanceof SynapseContainer) && Object.keys(synapseConfig).length > 0, `Test specifies Synapse config options so is unsupported with ${homeserverType}`, ); - homeserver.setRequest(request); - await logger.onTestStarted(context); await use(context); - await logger.onTestFinished(testInfo); - await homeserver.onTestFinished(testInfo); }, }); diff --git a/playwright/snapshots/right-panel/memberlist.spec.ts/with-four-members-linux.png b/playwright/snapshots/right-panel/memberlist.spec.ts/with-four-members-linux.png index d8bab27faa947e5558dfcc85f4e24e382b87b1cf..a5db88aae638325eee39eefd8b087d0b27d1e5eb 100644 GIT binary patch literal 19391 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­i!|}kqqWPqqVzOs(Z{c#x?yu6>M_POi z(IfTG{5+NhckfCjt3#B``wY2!0%kJ#MqtGJ_$U}FI*e=quIjHjw0nchUX9GBrF_TIT0Z+dqjviaiFvH)d_3`Hjd!P( zjrGDMpNdjf*VO38j@8i6{9f`<|NO)>?c(%sYe}j3XeVsq_SG6!MH0K9hDTOOj=Ic7 zhXHHS0YeZ$qrPIOq)uNo?;IN#OXXQ_*>&4JzeZ+otqp0a9mGyn#{3g;AzSf}_&C-y zi=OJe5H@Dw!p9rVjc$iD<%IT0L$CT24nR{9bR5QPaM1;c9R@PP zF3!R!QwZbcTddW8kW9sB3WIH)erLg|tll8{(ijl){oIr^+PU~%q=Uq@&nE`j~G!fRFIS%Mn@7prj%>`^*#`WRY!XicjGNIJ#wpP_N@GEpq#&b?2dcC zL0yHI9m>!v>uuaBJLu>R5vu;=g8x$IQE&}&OT&o?@j^$b!`~n-IWU%(;Gm6S#x<~$h$!(c^oXwADy?6OM)$D6O_yQ zHy~O2P@c`}+^$3@`tWFX{g2tY*5d%0oddSuRA;BttAV6CCYhoszqsV*%UqsS&xi+8%+be#A_C19Xz}gr=QA4$-l7Ssr8gTsL^hkW-W`md28-q zW1C2VaHrbg^_#$=Z~etV1hT4hEvjEp>C5}%^K`sctZjL^^Z1r}{Xn?1h6dwj*Z3VM zt|Dh^J55M%uaB@|OO;W4fg27{B<>A@Y*WV)VA)eQDVjcqT zo0;H^a#)w2?|KZH(M1IXw!;CSAL9*%D!Cgr8wZS}h<63-3kAqX{W_Y&Vd{f4eFl4_ z-vz4U8zxB{J&8YlXzS?E2)pJ~np06z!x!}hsodkUF-GOqz12*4(p-X=9dq#MUT+(I z(f|E2$d!C^VS~lF@D(p#bzfH$RTv@G1?_NFQc6lfvg>y60GsZ`5i(<;QQsaI<6Vw2 z%QmNO<9_j!O=3!$mHBkM1@)=m3u2h{3Eu4x(u-my&6fk#XYOesbpVEA)AeoBn!h$9 z0>6(w=H(jgTrsJK6^?tDi%TDTo9YToPruUKP|fUqkHi*i9>Idlwf0bQ^@vW zr`mF4hSU8HJQ(wfUPKty?2bCy)LPcm)Z`VB^vs4U_%tuwYN2CdE@P!I7}n|RgOVlQ6WM7?y8AJkHbV~ZeP6_ui!-r&vsF9@sm@O zrGvxrFY)Vam7XN%9eMcFPPX!!7x8&58Oh0~t2d3q!^1vk>SR&Vt*8U&Dc-Y2&)s97 z6c0Lmx>VsmG||#Rt6AQhk?gwDP7}Cwllp~!GNFMIBV%3*JGgSj-m{^(!2XA$$iFN7CUy3<0IL{ zV3>(iVH)}IBXMeadZ{_9+qk+XnqqHeOd36BbF-+Y^TyUWWtiSi-KPV;T;!7|TE@*Z ziTl8f_U3Tx4PFS*bUich6kc#9Wul<#$eOx|9^}StCqYgJ%B$cx`QPV@kX!D};4`vOW%rPB!rp>RpERFN0zR^&0&H7n~n`JGk~*5Y&u%##h(RK3;J9L z6-mw0)Y9VH`R)EWn8QGLVfNw5kUIkh-Dq2OsRDf0J`dAhdfC0wiN8CNf^c#U)gDW9 zvXnN=ZH5wYjp*|<;K0j15>EF$m|R@&GJ_j^!($T@8Mqp_y3e?0&m`#JGr4?boey78 zQR#s|AR&0~G%quIkD&y380YFe;L@9gI-pkaBRaa4g+i=e7EI z6M?VL_zUMfs9c6zTK}3AgoCCV9T}zl5-{~>W#y=l&hh6-BFg7%Nnp!o5K6!FNC7w|nJ=>O2REH(VN>7(2-0`otDbblHC$E}j3$i@(Wa4TK%mwSqSJ=f`gw?M@5KkoSNgYRC(U;lRy zOthJz`A>CmWql=5xT7aO`bk+g-cM3*A-P|9`c$yHL$mwC=&xQDQr>eM zAHs7(yf;-P2NMxYGX`aa1pF?qwpepYcPe^ zcUJkV+Ak)RV%lM}DmUB`hAz8ez}WeHdbpw_n7iCx4fMKru3!1_g;pkUFj4X zc|DVgIPo-0?a5U)uq6Jlm#(?qNZ5O{;7c#7R)aJ1Rd*B24%W$ zUT_%Hb#T7y?VJ$uJv@vbgYxTZGsbfoRt;uHDupP1G4ef&lo;R16a%rpE91X5Y|Sfv z7)iz47f-u#m>p2_Fw6PLzGAI% zLac{8%V~c5vtd`n{0?(0FEq=qkchk6EBmFEvk3;dfBx}u%1CrND^Bo`&fsF^z2qVl z7;AYx+0y2RAJhHCUbu&n*rd~8zEYOAr+0DdmZJb`BInt;y);%>qbD=n>elaIS}2zT z9-Y~T`-&yMCnY-d#yg`;ouBPs^Ean$XF)OC+0rD$q_o1Go&e)mT6#po`7^nD1U6KX z^v7h@TYKJH`&d2_iw=S*TLLn_J(I7btVb4(7kBz%YttWIW2 zk!MUA5UN2l1f`2XKe{!`yl$fK7@C?29@#FoZ*&!`6P*Wg4FwrljrDrrSRKfapF#}% zZ$RLkL3m%0P;`=|h5C7o>d?wwPhnZv9Cc$7!K+vq`(?$O>(dlbn7Eg)A!>>4?#>@F zZLj?1hoWNCH9png@bKK}ME4c3i;IipNX9Z}32Lg?2@sjcc)W8WBb&rTAGF-PE1T(? z;Xr8G$A?!6%FlqFfUy=r52mu++8rIH`r3n2_7rU$k89P$Y`wfLe=CX62XT?%QbG{S z@$@9nrWk?_v$r0%wRTfzNZoBCtg32!V&bIU2udSt_4)R@i^YhnoX^GXTCP3^I)i*7 ziOn#lFYvJ^wY9Im!&={yHbr>8#h2|R=DE?xqx#s5hE0Fh9Qt_Vo+j%>Mv(RJ3q-_TkXF+=3$}LT3h*V z#h{m6B#;WJTkKAGv1dRbbU5Otyc8dBewo9hd^;Osi2$hBG}1mlrq&Yh$^M*~5=)}T&BLSD zYKu*mBl~8t+HyI)kyck%`+R@SKnWi+fYG}^i$mkDZKJ<4d^7c_Q#?+1@oP)IE`vEY zpHU_l*zzH;&m!y0Z3ij&?IZROA6Fk>271#(zW?U^%Y6LIt-L|@xb3JJuS8%X-4!vV z9*WN<2J`C1a7vIsDBwKA1Y<>Od0^%!ztL~94L14^6O!)%BrK-vd;5k8c}AlavlMB{ zEW8ZNwu?Q=Fo~iwcML@7r+p3OR2LiaFWlEU+gHlh9f(Yiw#d zy!4%72iTfvX`j^&NkGTpvlBkM!UEkvfRUv9JU*(zceoeKNtawsYfJAnnYA<%CIKen zyCL*)H}H4&G~vJaqgT^-nr*v#d#UhPA8b0pu#NMBzimocyAwh`de+a7)N3eIy~V-f zVDRq7M#FvkA7I%P#Oz9pxcT`#&KEClJFX026duT%U?6SI&uT;NMllTODQIg6HDpO@ z3`)1m_gv1Vy6}MMOllhcZMwN87K^x_*~k$2Bz}Pw;5AoEx*HBeknMV}H>}XFtI^Y#M7= z`Wuh$pV6c=gjwXx`ZOr*)hlc|{Vysgk7id~^s6BKD;{HXN4(8+My;QI{zp6BRD1wu z9wC-CTy}?aKx6bjSitIO1oX_40fUMLU&SAa^TG$i|AJ3%1FCKWD ziVFPGQKh&aZ0th9Q@;&;&L2Y1QO~!PLSeq#C)Rqk2A{zHSKpJxg~4_*wr`~&xuqct zBa@*7Yb$mWBERFq%!wQd0H4lK{`QFvBEd|H?QFT=ecYnC{B6xvAPU1rNiPMm`762REhE6Ye-U1|;t1&J=f;O=a{H-{tsD;*9!I zpag|3=gr~$cFx>K-4XLPu{?#1`m6t*y{do1z3Y&Rx2{eW6{=` z`%7tGe!hjUhv{SyS&B@VMX6$7%e#z!rpXCrC}N;Pm8V~1b;rWHxwh|fxebk0`mU%^ zO%^|N_|$#MRTKc`-Ddzl0bkn7+1f=TmfEcO6OcqV?_h&dZIT(nYU=uLP5bqR^zTge zdGS3+pInooDtnj~quzVf{r)|kTe}`q*@+;9?4Oq~3Hsnkm~V^L)^O1Zn*B7A&ia-e z))j|OQVN)sFYMMmM(nJHqT3!lx_ab+0K0156OEoqA30n;l0>V~o+EySxflK^^m3Z7e=?Y;mbqa0ax zHqu7jPD(eUsr^s~SToa#mZl~%_cfb{5mE|raz6LwiYANB5hyOrtmWOcL%vA=Y>UV- z(L_oahvL}-fU5dRU@{0*}D;)UVp*=;0vr&U(z7W&1 z0l)-qzt9Lc@*FQ#?hG^S*+}GA5*Wl}7V02Ij%W_!Uv5ljRtUrcDMICOn-^B;bl zx?=wicj9z$Q3#oF_>?nVmx(0miLBt_*E>3BYPsncTA=6r+s&RThG-nhXmhPqlJP** zWd_CkS~adGM8O*}UM!Z%nbsRMI~8lNF8IR&vFWH6e9$`-o_FI3 z7ZOFK7c}c<$}@_&vb}%TkY*Q%bK2qvjP@HpHF;)!vF7K$bT=7QodZ1K4THVIvL> zW`on!YhK>p#LHzyEhbY&K6n|T9=j9x%WaN8oi|N1Wl|rHoLiW;)vi50=vznA3OX@e zV$f|IR;u+eTZfBfR$mq8GBS2kLg$LqS=ui4O?x!YQHmLdD8OhMowZSON+yxA>A7-blLFOA6=5jz%X#Rii6q zdiV3J34Ig`f3r_ZvY8Y|GI;H7Jq8T)gbW%J2>?Rr)`M|jUi-FYi#6xkd%Vh!zIAo6 zH_YPP7jF6az(&+^<{@8w7%Tv&B__5xBPj)QDjGbOSJwd&>bz?_+Dn)|MSpnR>G}+> z)%UE)=93U*TI(;7*?&=yGSOvP>qLO~!)XvnE96{JoA4+J^3M9EcYORfWLH7|soHD8 z6N>%K;|Bd$nUXtq4s@1)m#}@LOwN_}@~?H#gZYaZ$b_S>8V2-iMo#Fe4stuQV|=0VoA2LcUp(DB{Bs-fY%l*Vl~ z@D0L-HsOweIcSyIY2&_6Kc5IewIRQP-fsz*;)xMqty;C)4tRP6oh z?j^fl*BXABZ$#wKM2xAGy@f7R5Ti^5jtU*@nY+fL=45BBuX$(5og%w=l5caOuxA(n zaGL*OP$?E9WaRaSQ;!~WE%C#LEr**jykCze@*1}6t^EqgTE?BP@CV=GhRXVotv*c))7m9kp= zt>SyqDc1Ck7)z<&LhoOjhHudXY&u02K_r4=>I`Xs*n}NC^)&Z*K5~JW7&t<5tRkfe zVR|wZCn?Eb$)T_L=7n|iTTX*86v>OvpFfXg>uukLDE&y#lF1o|Uq`2=JQZFvz^0SZ z?~JQ*?g}q^JJ+m*b|Ed#5%zpjQWo~!E3c|ZRzt?L-UH`!#4i=Pb+fsbn)O{ z052IP;&n5V#i`xud3W4(Lf;(;WE$W_YeI0NJma#@j&^4RWmhl-a$Hr_ckzz^0-VJf++pTUT~)W+W=(eJy(d&GdQFm`Rb@H;~Kfx zEXLMiH1k0WEVICe43=bQM$?9gy8KD*NfWgD%^fX`#m&1(D|m$N8ko8IcJ65(KCPLU zhxzAEL41!=M#2CwPrg#kJAN$}p^#2%#O@vXGWJJSdXhaWa+$w1s zWW-*2_O^394x}q7TXK2XKU7Q-;(=jZXed2QK~DV|8OrHFT~dy5E2lDQsH^`!fs zP^-0f4@c>H?)~F#U6)F%0pKQ2%YIk2TF=d0pC*hLf;>mn>(K^2O(DtN#5q0#LwEyxybS`MW|GsczRhCX`S&WTm=1v@Y{>5< zJ`T{5@q_)V*j?Jj{lF@W#vFZm_P^F-ua!W4|4fMQ>ex^=vsdAWD0bJ%9Mi9w|H|uz zOlJjQf}ZXEkCM8%4GGU0%EPap0Jn-V6hG(8s zpy)2~d-|9z3T&(T`gXe*oy4)1o=g%LSlui-v%K`)I*+T}(-W14&eG;oZlI@E> z?$&(%-_=L!OuwPaH`22Ge!x+HLbV?Jts&$XER6&oW98Y;BW_ETT(gf0Q#OWq6kNDV z%iOJ<0emBHj>5!J+FE*m!ZKy85*VC9lR7?%yu+}bL5gH?81Bs!uD5@=g+Ti59z5aU zsdh&#E>&B4MA86W{-1Sqd~Fr8;~f#57Hv5SRxR7<6lsNbSZbrm=S8krqHnGcou+B8 z&TnqliF1EXk0y&8g?zNp{P8c|*M?;FxQ7o=V(CNBBPk*fWV()YG*v|f*K+-RKpsBE z5-zZBdBn8!RdjO4bU&eyoQ9RMjPV0z#|810uL<3JVn_a6mq+$_=~k@HP^Wq9->>$F zt=Ye}dDpo$(eO{+iw6|IMOM+=TdOQv_HSw%8ZcmjTDL-dhz)!%2 z-M{HPbr2FQ@r>#nKr=@}x61(AD^UNQA?%kcPdcMXJZll7t)|;3sCHnnVH@|Nu23Y^ z`+y;fzKyE&{NXT$xTMO20=4bpps6BgjipIh;*Tkq#UOA^){vamy5B$r2vjqBqy;Qx zLKhoiH3s&wU-q*6CX12*YX3c+sJZjs?+Hf1=j2>Y9St!th8geOfx|v`2QD`fr`rQ< z$%oSo4sCaDcZcB`%jH`~4XBY+@W!*QYM(Z z>(5>X^==+3CHmhkonkS@dmUQUNl4aFYCpmfFz0bPwKEW$uf3vTa*u*5+)X9B0a!x#9G05wZ=)eIb~Tli znL^vQ_4!H~Vivmd;$Rtxzd?%BLJyyy$zF)IkBxmHcne&k`@y{X-t5QP*+v&q4uiYx z58m3^S;07>uYz0=BpU|^kKJ+e$?Mecwv>z2eR9m!zH|7KbE=3dt6?F7_a5Jy{pBU z%bFnw*pq0HzF~vL4SI3$VWZfP)O(48-KDFg+mNDiFMawD&ALXfM+@$>nk)%HF`QZs zRb2cT*NQ<=_&Vz%+l&bIp~8!@<*gjGLfJ;u&yaC9-=4yI%GUShfAoqwo<*j4UM_8{ zJh(y&Rfrwit9C?F&IrC2U^6<}97*%nXzLGM6=!E-lZvFF<+h%`dM2i+rR5?3h!$x> zn!TH1C5HTn+s4(vkRGG9u|tL!ElQ;+QbtBb%r76bBik%s$fn0vX(><1aHY^chZ~hn zu<1maA1$@ID4k{TBCkqm41_4j$XXoX2!0|FxW2dR!JygO1urjG>>bxzi{Gk+5lRgr z7JonExbT?LINa>~8iHc*RwyZD)zMJlEmS7JFJf1z_!j!dea9Pq(ZL^J(*clO)Y&l^ ziLV2A5sF@zC*HpGDz6lrVV#{S&X?PONHm#fjnZs;Z?7M~vU}Sb=N@NeRi&jNHp90SvqXuYx81F`PiD#%8cwT~ma}+I zdhYG6jL+r)DI2{rW;eK_vf!~V-{4j3U)p`g(+L@vO)kFrI%L88LKYkfU@$#Ogx)9g zypK<=uuPhOyQe84F)?ZRlRp=9Om?dNRb@l9(UbpbY+%>mPfbgy(jGbp9^ie7v%A#Q z7N}ZSQ&saah|F1lLlKXc*906I9v?W2pJb&55G^_H^1(xxb;!iO0ii{&6CQQwsW8|7 zRz2su_UU%WzWG*z7;THpF1H(oVe$s@yK7nN=+x8Di9ZgxzZy^~()(*tSs`m>ZT<<1 zd+P{`PKicrclH;+CUSEZ`gfh4Mhp&?RGLahQw!<-QVt3_4_~ZC-Rkh54}?~LTyQQ{ zUqEtge5R`aeQ|h&nBzUX8j>hfpYQG*>R2qP>;mGaP%S$17Wv~FKTCovUYdcjc8P9f zO-ak0XFi4Xu8tEwVYSPNhEN}iX5!Wzb&HV4v73imDz|mbFn)2=9pd)2O=^4h#V6Gz zk6XTC_jenrT@z0v{l(`ChxawaJWW03=H{cwME85g3Y^yI!QihD8wtRXaDwv`&{F~k z2wzMF_nG-0ZSqTYb!lj5Ji(_fG;%_IGsSNG1#t%d#Z~ogpt54XF5ffO8o&HH&}$HpqSG*_i;M zLl|nT*5#A+WmfY!{@(J!%mQmc_3{^brY#tMhH3AT&kk92VRnv(+Bja8z%^|Gx0nZMco!9S!~KYBg5MeeT7mz6TnnJwb|z-TvoAp{qQge5Bt8e4 zrfA*Nv>`9c-!%OBLqZIsb$awkfX~PDFF&qN8>9!r5b#d0i6>UnFs^%_2eZ%6c8jP~ zlf)0rCf7ck>BsP9PUzoXywN>g0}f9PDnxjBFQ)U&OwXS6g$)SV?>P~&{87V96_&#Q z-0lBO@DdZR!cFNU3EA_I2izF{PLY)?d!1&?B{pY`4vfOSGv-n%1qqou|&3#5C!PxJv+^5C1#_4(OfrU1;H;7r7$ zfP36bz$W^?a?6LVqJpA-4rB(9@@y6hol&Fyk-EO?{AWoD!j53*4#)^hr`tq73}`>wf{B(00H8 diff --git a/playwright/testcontainers/HomeserverContainer.ts b/playwright/testcontainers/HomeserverContainer.ts deleted file mode 100644 index d5be8c5301..0000000000 --- a/playwright/testcontainers/HomeserverContainer.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial -Please see LICENSE files in the repository root for full details. -*/ - -import { type AbstractStartedContainer, type GenericContainer } from "testcontainers"; -import { type APIRequestContext, type TestInfo } from "@playwright/test"; - -import { type HomeserverInstance } from "../plugins/homeserver"; -import { type StartedMatrixAuthenticationServiceContainer } from "./mas.ts"; - -export interface HomeserverContainer extends GenericContainer { - withConfigField(key: string, value: any): this; - withConfig(config: Partial): this; - withMatrixAuthenticationService(mas?: StartedMatrixAuthenticationServiceContainer): this; - start(): Promise; -} - -export interface StartedHomeserverContainer extends AbstractStartedContainer, HomeserverInstance { - setRequest(request: APIRequestContext): void; - onTestFinished(testInfo: TestInfo): Promise; -} diff --git a/playwright/testcontainers/dendrite.ts b/playwright/testcontainers/dendrite.ts index edf9dba91d..55938778cd 100644 --- a/playwright/testcontainers/dendrite.ts +++ b/playwright/testcontainers/dendrite.ts @@ -8,12 +8,13 @@ Please see LICENSE files in the repository root for full details. import { GenericContainer, Wait } from "testcontainers"; import * as YAML from "yaml"; import { set } from "lodash"; - -import { randB64Bytes } from "../plugins/utils/rand.ts"; -import { StartedSynapseContainer } from "./synapse.ts"; -import { deepCopy } from "../plugins/utils/object.ts"; -import { type HomeserverContainer } from "./HomeserverContainer.ts"; -import { type StartedMatrixAuthenticationServiceContainer } from "./mas.ts"; +import { randB64Bytes } from "@element-hq/element-web-playwright-common/lib/utils/rand.js"; +import { deepCopy } from "@element-hq/element-web-playwright-common/lib/utils/object.js"; +import { + StartedSynapseContainer, + type HomeserverContainer, + type StartedMatrixAuthenticationServiceContainer, +} from "@element-hq/element-web-playwright-common/lib/testcontainers"; const DEFAULT_CONFIG = { version: 2, @@ -223,7 +224,7 @@ export class DendriteContainer extends GenericContainer implements HomeserverCon .withWaitStrategy(Wait.forHttp("/_matrix/client/versions", 8008)); } - public withConfigField(key: string, value: any): this { + public withConfigField(key: string, value: unknown): this { set(this.config, key, value); return this; } @@ -236,6 +237,11 @@ export class DendriteContainer extends GenericContainer implements HomeserverCon return this; } + // Dendrite does not support SMTP at this time - https://github.com/element-hq/dendrite/issues/1298 + public withSmtpServer(): this { + return this; + } + // Dendrite does not support MAS at this time public withMatrixAuthenticationService(mas?: StartedMatrixAuthenticationServiceContainer): this { return this; diff --git a/playwright/testcontainers/mailpit.ts b/playwright/testcontainers/mailpit.ts deleted file mode 100644 index 6c47f9ab37..0000000000 --- a/playwright/testcontainers/mailpit.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial -Please see LICENSE files in the repository root for full details. -*/ - -import { AbstractStartedContainer, GenericContainer, type StartedTestContainer, Wait } from "testcontainers"; -import { MailpitClient } from "mailpit-api"; - -export class MailhogContainer extends GenericContainer { - constructor() { - super("axllent/mailpit:latest"); - - this.withExposedPorts(8025).withWaitStrategy(Wait.forListeningPorts()).withEnvironment({ - MP_SMTP_AUTH_ALLOW_INSECURE: "true", - MP_SMTP_AUTH_ACCEPT_ANY: "true", - }); - } - - public override async start(): Promise { - return new StartedMailhogContainer(await super.start()); - } -} - -export class StartedMailhogContainer extends AbstractStartedContainer { - public readonly client: MailpitClient; - - constructor(container: StartedTestContainer) { - super(container); - this.client = new MailpitClient(`http://${container.getHost()}:${container.getMappedPort(8025)}`); - } -} diff --git a/playwright/testcontainers/mas.ts b/playwright/testcontainers/mas.ts deleted file mode 100644 index bdd071ebc2..0000000000 --- a/playwright/testcontainers/mas.ts +++ /dev/null @@ -1,346 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial -Please see LICENSE files in the repository root for full details. -*/ - -import { - AbstractStartedContainer, - GenericContainer, - type StartedTestContainer, - Wait, - type ExecResult, -} from "testcontainers"; -import { type StartedPostgreSqlContainer } from "@testcontainers/postgresql"; -import * as YAML from "yaml"; - -import { getFreePort } from "../plugins/utils/port.ts"; -import { deepCopy } from "../plugins/utils/object.ts"; -import { type Credentials } from "../plugins/homeserver"; - -const DEFAULT_CONFIG = { - http: { - listeners: [ - { - name: "web", - resources: [ - { name: "discovery" }, - { name: "human" }, - { name: "oauth" }, - { name: "compat" }, - { - name: "graphql", - playground: true, - }, - { - name: "assets", - path: "/usr/local/share/mas-cli/assets/", - }, - ], - binds: [ - { - address: "[::]:8080", - }, - ], - proxy_protocol: false, - }, - { - name: "internal", - resources: [ - { - name: "health", - }, - ], - binds: [ - { - address: "[::]:8081", - }, - ], - proxy_protocol: false, - }, - ], - trusted_proxies: ["192.128.0.0/16", "172.16.0.0/12", "10.0.0.0/10", "127.0.0.1/8", "fd00::/8", "::1/128"], - public_base: "", // Needs to be set - issuer: "", // Needs to be set - }, - database: { - host: "postgres", - port: 5432, - database: "postgres", - username: "postgres", - password: "p4S5w0rD", - max_connections: 10, - min_connections: 0, - connect_timeout: 30, - idle_timeout: 600, - max_lifetime: 1800, - }, - telemetry: { - tracing: { - exporter: "none", - propagators: [], - }, - metrics: { - exporter: "none", - }, - sentry: { - dsn: null, - }, - }, - templates: { - path: "/usr/local/share/mas-cli/templates/", - assets_manifest: "/usr/local/share/mas-cli/manifest.json", - translations_path: "/usr/local/share/mas-cli/translations/", - }, - email: { - from: '"Authentication Service" ', - reply_to: '"Authentication Service" ', - transport: "smtp", - mode: "plain", - hostname: "mailpit", - port: 1025, - username: "username", - password: "password", - }, - secrets: { - encryption: "984b18e207c55ad5fbb2a49b217481a722917ee87b2308d4cf314c83fed8e3b5", - keys: [ - { - kid: "YEAhzrKipJ", - key: "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAuIV+AW5vx52I4CuumgSxp6yvKfIAnRdALeZZCoFkIGxUli1B\nS79NJ3ls46oLh1pSD9RrhaMp6HTNoi4K3hnP9Q9v77pD7KwdFKG3UdG1zksIB0s/\n+/Ey/DmX4LPluwBBS7r/LkQ1jk745lENA++oiDqZf2D/uP8jCHlvaSNyVKTqi1ki\nOXPd4T4xBUjzuas9ze5jQVSYtfOidgnv1EzUipbIxgvH1jNt4raRlmP8mOq7xEnW\nR+cF5x6n/g17PdSEfrwO4kz6aKGZuMP5lVlDEEnMHKabFSQDBl7+Mpok6jXutbtA\nuiBnsKEahF9eoj4na4fpbRNPdIVyoaN5eGvm5wIDAQABAoIBAApyFCYEmHNWaa83\nCdVSOrRhRDE9r+c0r79pcNT1ajOjrk4qFa4yEC4R46YntCtfY5Hd1pBkIjU0l4d8\nz8Su9WTMEOwjQUEepS7L0NLi6kXZXYT8L40VpGs+32grBvBFHW0qEtQNrHJ36gMv\nx2rXoFTF7HaXiSJx3wvVxAbRqOE9tBXLsmNHaWaAdWQG5o77V9+zvMri3cAeEg2w\nVkKokb0dza7es7xG3tqS26k69SrwGeeuKo7qCHPH2cfyWmY5Yhv8iOoA59JzzbiK\nUdxyzCHskrPSpRKVkVVwmY3RBt282TmSRG7td7e5ESSj50P2e5BI5uu1Hp/dvU4F\nvYjV7kECgYEA6WqYoUpVsgQiqhvJwJIc/8gRm0mUy8TenI36z4Iim01Nt7fibWH7\nXnsFqLGjXtYNVWvBcCrUl9doEnRbJeG2eRGbGKYAWVrOeFvwM4fYvw9GoOiJdDj4\ncgWDe7eHbHE+UTqR7Nnr/UBfipoNWDh6X68HRBuXowh0Q6tOfxsrRFECgYEAyl/V\n4b8bFp3pKZZCb+KPSYsQf793cRmrBexPcLWcDPYbMZQADEZ/VLjbrNrpTOWxUWJT\nhr8MrWswnHO+l5AFu5CNO+QgV2dHLk+2w8qpdpFRPJCfXfo2D3wZ0c4cv3VCwv1V\n5y7f6XWVjDWZYV4wj6c3shxZJjZ+9Hbhf3/twbcCgYA6fuRRR3fCbRbi2qPtBrEN\nyO3gpMgNaQEA6vP4HPzfPrhDWmn8T5nXS61XYW03zxz4U1De81zj0K/cMBzHmZFJ\nNghQXQmpWwBzWVcREvJWr1Vb7erEnaJlsMwKrSvbGWYspSj82oAxr3hCG+lMOpsw\nb4S6pM+TpAK/EqdRY1WsgQKBgQCGoMaaTRXqL9bC0bEU2XVVCWxKb8c3uEmrwQ7/\n/fD4NmjUzI5TnDps1CVfkqoNe+hAKddDFqmKXHqUOfOaxDbsFje+lf5l5tDVoDYH\nfjTKKdYPIm7CiAeauYY7qpA5Vfq52Opixy4yEwUPp0CII67OggFtPaqY3zwJyWQt\n+57hdQKBgGCXM/KKt7ceUDcNJxSGjvu0zD9D5Sv2ihYlEBT/JLaTCCJdvzREevaJ\n1d+mpUAt0Lq6A8NWOMq8HPaxAik3rMQ0WtM5iG+XgsUqvTSb7NcshArDLuWGnW3m\nMC4rM0UBYAS4QweduUSH1imrwH/1Gu5+PxbiecceRMMggWpzu0Bq\n-----END RSA PRIVATE KEY-----\n", - }, - { - kid: "8J1AxrlNZT", - key: "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIF1cjfIOEdy3BXJ72x6fKpEB8WP1ddZAUJAaqqr/6CpToAoGCCqGSM49\nAwEHoUQDQgAEfHdNuI1Yeh3/uOq2PlnW2vymloOVpwBYebbw4VVsna9xhnutIdQW\ndE8hkX8Yb0pIDasrDiwllVLzSvsWJAI0Kw==\n-----END EC PRIVATE KEY-----\n", - }, - { - kid: "3BW6un1EBi", - key: "-----BEGIN EC PRIVATE KEY-----\nMIGkAgEBBDA+3ZV17r8TsiMdw1cpbTSNbyEd5SMy3VS1Mk/kz6O2Ev/3QZut8GE2\nq3eGtLBoVQigBwYFK4EEACKhZANiAASs8Wxjk/uRimRKXnPr2/wDaXkN9wMDjYQK\nmZULb+0ZP1/cXmuXuri8hUGhQvIU8KWY9PkpV+LMPEdpE54mHPKSLjq5CDXoSZ/P\n9f7cdRaOZ000KQPZfIFR9ujJTtDN7Vs=\n-----END EC PRIVATE KEY-----\n", - }, - { - kid: "pkZ0pTKK0X", - key: "-----BEGIN EC PRIVATE KEY-----\nMHQCAQEEIHenfsXYPc5yzjZKUfvmydDR1YRwdsfZYvwHf/2wsYxooAcGBSuBBAAK\noUQDQgAEON1x7Vlu+nA0KvC5vYSOHhDUkfLYNZwYSLPFVT02h9E13yFFMIJegIBl\nAer+6PMZpPc8ycyeH9N+U9NAyliBhQ==\n-----END EC PRIVATE KEY-----\n", - }, - ], - }, - passwords: { - enabled: true, - schemes: [ - { - version: 1, - algorithm: "argon2id", - }, - ], - minimum_complexity: 0, - }, - policy: { - wasm_module: "/usr/local/share/mas-cli/policy.wasm", - client_registration_entrypoint: "client_registration/violation", - register_entrypoint: "register/violation", - authorization_grant_entrypoint: "authorization_grant/violation", - password_entrypoint: "password/violation", - email_entrypoint: "email/violation", - data: { - client_registration: { - // allow non-SSL and localhost URIs - allow_insecure_uris: true, - // EW doesn't have contacts at this time - allow_missing_contacts: true, - }, - }, - }, - upstream_oauth2: { - providers: [], - }, - branding: { - service_name: null, - policy_uri: null, - tos_uri: null, - imprint: null, - logo_uri: null, - }, - account: { - password_registration_enabled: true, - }, - experimental: { - access_token_ttl: 300, - compat_token_ttl: 300, - }, - rate_limiting: { - login: { - burst: 10, - per_second: 1, - }, - registration: { - burst: 10, - per_second: 1, - }, - }, -}; - -export class MatrixAuthenticationServiceContainer extends GenericContainer { - private config: typeof DEFAULT_CONFIG; - private readonly args = ["-c", "/config/config.yaml"]; - - constructor(db: StartedPostgreSqlContainer) { - // We rely on `mas-cli manage add-email` which isn't in a release yet - // https://github.com/element-hq/matrix-authentication-service/pull/3235 - super("ghcr.io/element-hq/matrix-authentication-service:sha-0b90c33"); - - this.config = deepCopy(DEFAULT_CONFIG); - this.config.database.username = db.getUsername(); - this.config.database.password = db.getPassword(); - - this.withExposedPorts(8080, 8081) - .withWaitStrategy(Wait.forHttp("/health", 8081)) - .withCommand(["server", ...this.args]); - } - - public withConfig(config: object): this { - this.config = { - ...this.config, - ...config, - }; - return this; - } - - public override async start(): Promise { - // MAS config issuer needs to know what URL it'll be accessed from, so we have to map the port manually - const port = await getFreePort(); - - this.config.http.public_base = `http://localhost:${port}/`; - this.config.http.issuer = `http://localhost:${port}/`; - - this.withExposedPorts({ - container: 8080, - host: port, - }).withCopyContentToContainer([ - { - target: "/config/config.yaml", - content: YAML.stringify(this.config), - }, - ]); - - return new StartedMatrixAuthenticationServiceContainer( - await super.start(), - `http://localhost:${port}`, - this.args, - ); - } -} - -export class StartedMatrixAuthenticationServiceContainer extends AbstractStartedContainer { - private adminTokenPromise?: Promise; - - constructor( - container: StartedTestContainer, - public readonly baseUrl: string, - private readonly args: string[], - ) { - super(container); - } - - public async getAdminToken(): Promise { - if (this.adminTokenPromise === undefined) { - this.adminTokenPromise = this.registerUserInternal( - "admin", - "totalyinsecureadminpassword", - undefined, - true, - ).then((res) => res.accessToken); - } - return this.adminTokenPromise; - } - - private async manage(cmd: string, ...args: string[]): Promise { - const result = await this.exec(["mas-cli", "manage", cmd, ...this.args, ...args]); - if (result.exitCode !== 0) { - throw new Error(`Failed mas-cli manage ${cmd}: ${result.output}`); - } - return result; - } - - private async manageRegisterUser( - username: string, - password: string, - displayName?: string, - admin = false, - ): Promise { - const args: string[] = []; - if (admin) args.push("-a"); - const result = await this.manage( - "register-user", - ...args, - "-y", - "-p", - password, - "-d", - displayName ?? "", - username, - ); - - const registerLines = result.output.trim().split("\n"); - const userId = registerLines - .find((line) => line.includes("Matrix ID: ")) - ?.split(": ") - .pop(); - - if (!userId) { - throw new Error(`Failed to register user: ${result.output}`); - } - - return userId; - } - - private async manageIssueCompatibilityToken( - username: string, - admin = false, - ): Promise<{ accessToken: string; deviceId: string }> { - const args: string[] = []; - if (admin) args.push("--yes-i-want-to-grant-synapse-admin-privileges"); - const result = await this.manage("issue-compatibility-token", ...args, username); - - const parts = result.output.trim().split(/\s+/); - const accessToken = parts.find((part) => part.startsWith("mct_")); - const deviceId = parts.find((part) => part.startsWith("compat_session.device="))?.split("=")[1]; - - if (!accessToken || !deviceId) { - throw new Error(`Failed to issue compatibility token: ${result.output}`); - } - - return { accessToken, deviceId }; - } - - private async registerUserInternal( - username: string, - password: string, - displayName?: string, - admin = false, - ): Promise { - const userId = await this.manageRegisterUser(username, password, displayName, admin); - const { deviceId, accessToken } = await this.manageIssueCompatibilityToken(username, admin); - - return { - userId, - accessToken, - deviceId, - homeServer: userId.slice(1).split(":").slice(1).join(":"), - displayName, - username, - password, - }; - } - - public async registerUser(username: string, password: string, displayName?: string): Promise { - return this.registerUserInternal(username, password, displayName, false); - } - - public async setThreepid(username: string, medium: string, address: string): Promise { - if (medium !== "email") { - throw new Error("Only email threepids are supported by MAS"); - } - - await this.manage("add-email", username, address); - } -} diff --git a/playwright/testcontainers/synapse.ts b/playwright/testcontainers/synapse.ts index 47c2e708e2..2e841604be 100644 --- a/playwright/testcontainers/synapse.ts +++ b/playwright/testcontainers/synapse.ts @@ -1,395 +1,20 @@ /* -Copyright 2024 New Vector Ltd. +Copyright 2024-2025 New Vector Ltd. SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial Please see LICENSE files in the repository root for full details. */ -import { - AbstractStartedContainer, - GenericContainer, - type RestartOptions, - type StartedTestContainer, - Wait, -} from "testcontainers"; -import { type APIRequestContext, type TestInfo } from "@playwright/test"; -import crypto from "node:crypto"; -import * as YAML from "yaml"; -import { set } from "lodash"; - -import { getFreePort } from "../plugins/utils/port.ts"; -import { randB64Bytes } from "../plugins/utils/rand.ts"; -import { type Credentials } from "../plugins/homeserver"; -import { deepCopy } from "../plugins/utils/object.ts"; -import { type HomeserverContainer, type StartedHomeserverContainer } from "./HomeserverContainer.ts"; -import { type StartedMatrixAuthenticationServiceContainer } from "./mas.ts"; -import { Api, ClientServerApi, type Verb } from "../plugins/utils/api.ts"; +import { SynapseContainer as BaseSynapseContainer } from "@element-hq/element-web-playwright-common/lib/testcontainers"; const TAG = "develop@sha256:2ea87d45fc7ff3327c671b3b4447e6b2032d4f5ca07d62d8aef0d900e105c2f4"; -const DEFAULT_CONFIG = { - server_name: "localhost", - public_baseurl: "", // set by start method - pid_file: "/homeserver.pid", - web_client: false, - soft_file_limit: 0, - // Needs to be configured to log to the console like a good docker process - log_config: "/data/log.config", - listeners: [ - { - // Listener is always port 8008 (configured in the container) - port: 8008, - tls: false, - bind_addresses: ["::"], - type: "http", - x_forwarded: true, - resources: [ - { - names: ["client"], - compress: false, - }, - ], - }, - ], - database: { - // An sqlite in-memory database is fast & automatically wipes each time - name: "sqlite3", - args: { - database: ":memory:", - }, - }, - rc_messages_per_second: 10000, - rc_message_burst_count: 10000, - rc_registration: { - per_second: 10000, - burst_count: 10000, - }, - rc_joins: { - local: { - per_second: 9999, - burst_count: 9999, - }, - remote: { - per_second: 9999, - burst_count: 9999, - }, - }, - rc_joins_per_room: { - per_second: 9999, - burst_count: 9999, - }, - rc_3pid_validation: { - per_second: 1000, - burst_count: 1000, - }, - rc_invites: { - per_room: { - per_second: 1000, - burst_count: 1000, - }, - per_user: { - per_second: 1000, - burst_count: 1000, - }, - }, - rc_login: { - address: { - per_second: 10000, - burst_count: 10000, - }, - account: { - per_second: 10000, - burst_count: 10000, - }, - failed_attempts: { - per_second: 10000, - burst_count: 10000, - }, - }, - media_store_path: "/tmp/media_store", - max_upload_size: "50M", - max_image_pixels: "32M", - dynamic_thumbnails: false, - enable_registration: true, - enable_registration_without_verification: true, - disable_msisdn_registration: false, - registrations_require_3pid: [], - enable_metrics: false, - report_stats: false, - // These placeholders will be replaced with values generated at start - registration_shared_secret: "secret", - macaroon_secret_key: "secret", - form_secret: "secret", - // Signing key must be here: it will be generated to this file - signing_key_path: "/data/localhost.signing.key", - trusted_key_servers: [], - password_config: { - enabled: true, - }, - ui_auth: {}, - background_updates: { - // Inhibit background updates as this Synapse isn't long-lived - min_batch_size: 100000, - sleep_duration_ms: 100000, - }, - enable_authenticated_media: true, - email: undefined, - user_consent: undefined, - server_notices: undefined, - allow_guest_access: false, - experimental_features: {}, - oidc_providers: [], - serve_server_wellknown: true, - presence: { - enabled: true, - include_offline_users_on_sync: true, - }, - room_list_publication_rules: [{ action: "allow" }], -}; - -export type SynapseConfig = Partial; - -export class SynapseContainer extends GenericContainer implements HomeserverContainer { - private config: typeof DEFAULT_CONFIG; - private mas?: StartedMatrixAuthenticationServiceContainer; - - constructor() { +/** + * SynapseContainer which freezes the docker digest to stabilise tests, + * updated periodically by the `playwright-image-updates.yaml` workflow. + */ +export class SynapseContainer extends BaseSynapseContainer { + public constructor() { super(`ghcr.io/element-hq/synapse:${TAG}`); - - this.config = deepCopy(DEFAULT_CONFIG); - this.config.registration_shared_secret = randB64Bytes(16); - this.config.macaroon_secret_key = randB64Bytes(16); - this.config.form_secret = randB64Bytes(16); - - const signingKey = randB64Bytes(32); - this.withWaitStrategy(Wait.forHttp("/health", 8008)).withCopyContentToContainer([ - { target: this.config.signing_key_path, content: `ed25519 x ${signingKey}` }, - { - target: this.config.log_config, - content: YAML.stringify({ - version: 1, - formatters: { - precise: { - format: "%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s - %(message)s", - }, - }, - handlers: { - console: { - class: "logging.StreamHandler", - formatter: "precise", - }, - }, - loggers: { - "synapse.storage.SQL": { - level: "DEBUG", - }, - "twisted": { - handlers: ["console"], - propagate: false, - }, - }, - root: { - level: "DEBUG", - handlers: ["console"], - }, - disable_existing_loggers: false, - }), - }, - ]); - } - - public withConfigField(key: string, value: any): this { - set(this.config, key, value); - return this; - } - - public withConfig(config: Partial): this { - this.config = { - ...this.config, - ...config, - }; - return this; - } - - public withMatrixAuthenticationService(mas?: StartedMatrixAuthenticationServiceContainer): this { - this.mas = mas; - return this; - } - - public override async start(): Promise { - // Synapse config public_baseurl needs to know what URL it'll be accessed from, so we have to map the port manually - const port = await getFreePort(); - - this.withExposedPorts({ - container: 8008, - host: port, - }) - .withConfig({ - public_baseurl: `http://localhost:${port}`, - }) - .withCopyContentToContainer([ - { - target: "/data/homeserver.yaml", - content: YAML.stringify(this.config), - }, - ]); - - const container = await super.start(); - const baseUrl = `http://localhost:${port}`; - if (this.mas) { - return new StartedSynapseWithMasContainer( - container, - baseUrl, - this.config.registration_shared_secret, - this.mas, - ); - } - - return new StartedSynapseContainer(container, baseUrl, this.config.registration_shared_secret); - } -} - -export class StartedSynapseContainer extends AbstractStartedContainer implements StartedHomeserverContainer { - protected adminTokenPromise?: Promise; - protected readonly adminApi: Api; - public readonly csApi: ClientServerApi; - - constructor( - container: StartedTestContainer, - public readonly baseUrl: string, - private readonly registrationSharedSecret: string, - ) { - super(container); - this.adminApi = new Api(`${this.baseUrl}/_synapse/admin`); - this.csApi = new ClientServerApi(this.baseUrl); - } - - public restart(options?: Partial): Promise { - this.adminTokenPromise = undefined; - return super.restart(options); - } - - public setRequest(request: APIRequestContext): void { - this.csApi.setRequest(request); - this.adminApi.setRequest(request); - } - - public async onTestFinished(testInfo: TestInfo): Promise { - // Clean up the server to prevent rooms leaking between tests - await this.deletePublicRooms(); - } - - protected async deletePublicRooms(): Promise { - const token = await this.getAdminToken(); - // We hide the rooms from the room directory to save time between tests and for portability between homeservers - const { chunk: rooms } = await this.csApi.request<{ - chunk: { room_id: string }[]; - }>("GET", "/v3/publicRooms", token, {}); - await Promise.all( - rooms.map((room) => - this.csApi.request("PUT", `/v3/directory/list/room/${room.room_id}`, token, { visibility: "private" }), - ), - ); - } - - private async registerUserInternal( - username: string, - password: string, - displayName?: string, - admin = false, - ): Promise { - const path = "/v1/register"; - const { nonce } = await this.adminApi.request<{ nonce: string }>("GET", path, undefined, {}); - const mac = crypto - .createHmac("sha1", this.registrationSharedSecret) - .update(`${nonce}\0${username}\0${password}\0${admin ? "" : "not"}admin`) - .digest("hex"); - const data = await this.adminApi.request<{ - home_server: string; - access_token: string; - user_id: string; - device_id: string; - }>("POST", path, undefined, { - nonce, - username, - password, - mac, - admin, - displayname: displayName, - }); - - return { - homeServer: data.home_server || data.user_id.split(":").slice(1).join(":"), - accessToken: data.access_token, - userId: data.user_id, - deviceId: data.device_id, - password, - displayName, - username, - }; - } - - protected async getAdminToken(): Promise { - if (this.adminTokenPromise === undefined) { - this.adminTokenPromise = this.registerUserInternal( - "admin", - "totalyinsecureadminpassword", - undefined, - true, - ).then((res) => res.accessToken); - } - return this.adminTokenPromise; - } - - private async adminRequest(verb: "GET", path: string, data?: never): Promise; - private async adminRequest(verb: Verb, path: string, data?: object): Promise; - private async adminRequest(verb: Verb, path: string, data?: object): Promise { - const adminToken = await this.getAdminToken(); - return this.adminApi.request(verb, path, adminToken, data); - } - - public registerUser(username: string, password: string, displayName?: string): Promise { - return this.registerUserInternal(username, password, displayName, false); - } - - public async loginUser(userId: string, password: string): Promise { - return this.csApi.loginUser(userId, password); - } - - public async setThreepid(userId: string, medium: string, address: string): Promise { - await this.adminRequest("PUT", `/v2/users/${userId}`, { - threepids: [ - { - medium, - address, - }, - ], - }); - } -} - -export class StartedSynapseWithMasContainer extends StartedSynapseContainer { - constructor( - container: StartedTestContainer, - baseUrl: string, - registrationSharedSecret: string, - private readonly mas: StartedMatrixAuthenticationServiceContainer, - ) { - super(container, baseUrl, registrationSharedSecret); - } - - protected async getAdminToken(): Promise { - if (this.adminTokenPromise === undefined) { - this.adminTokenPromise = this.mas.getAdminToken(); - } - return this.adminTokenPromise; - } - - public registerUser(username: string, password: string, displayName?: string): Promise { - return this.mas.registerUser(username, password, displayName); - } - - public async setThreepid(userId: string, medium: string, address: string): Promise { - return this.mas.setThreepid(userId, medium, address); } } diff --git a/yarn.lock b/yarn.lock index bab0fd1c35..600521f2b9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -27,7 +27,7 @@ "@jridgewell/gen-mapping" "^0.3.5" "@jridgewell/trace-mapping" "^0.3.24" -"@axe-core/playwright@^4.8.1": +"@axe-core/playwright@^4.10.1": version "4.10.1" resolved "https://registry.yarnpkg.com/@axe-core/playwright/-/playwright-4.10.1.tgz#c811ba8bfa244833cce422c4131e0043828c42cc" integrity sha512-EV5t39VV68kuAfMKqb/RL+YjYKhfuGim9rgIaQ6Vntb2HgaCaau0h98Y3WEUqW1+PbdzxDtDNjFAipbtZuBmEA== @@ -1551,6 +1551,19 @@ resolved "https://registry.yarnpkg.com/@element-hq/element-web-module-api/-/element-web-module-api-0.1.1.tgz#e2b24aa38aa9f7b6af3c4993e6402a8b7e2f3cb5" integrity sha512-qtEQD5nFaRJ+vfAis7uhKB66SyCjrz7O+qGz/hKJjgNhBLT/6C5DK90waKINXSw0J3stFR43IWzEk5GBOrTMow== +"@element-hq/element-web-playwright-common@^1.1.5": + version "1.1.5" + resolved "https://registry.yarnpkg.com/@element-hq/element-web-playwright-common/-/element-web-playwright-common-1.1.5.tgz#e9d4e24f0f284d16b7dc8cb9cd893521e1645be0" + integrity sha512-Cw2PqaU9YUx/qHq8nybjfxMhSGshkvZiRBPMhYnamjMTjUiSEEF74061gjIecxLF/3hgV6I0dGp5Km3FhRbTPQ== + dependencies: + "@axe-core/playwright" "^4.10.1" + "@testcontainers/postgresql" "^10.18.0" + lodash-es "^4.17.21" + mailpit-api "^1.2.0" + strip-ansi "^7.1.0" + testcontainers "^10.18.0" + yaml "^2.7.0" + "@eslint-community/eslint-utils@^4.2.0": version "4.4.0" resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" @@ -2167,7 +2180,14 @@ resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== -"@playwright/test@^1.40.1": +"@playwright/test@1.50.1": + version "1.50.1" + resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.50.1.tgz#027d00ca77ec79688764eb765cfe9a688807bf0b" + integrity sha512-Jii3aBg+CEDpgnuDxEp/h7BimHcUTDlpEtce89xEumlJ5ef2hqepZ+PWp1DDpYC/VO9fmWVI1IlEaoI5fK9FXQ== + dependencies: + playwright "1.50.1" + +"@playwright/test@^1.50.1": version "1.51.0" resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.51.0.tgz#8d5c8400b465a0bfdbcf993e390ceecb903ea6d2" integrity sha512-dJ0dMbZeHhI+wb77+ljx/FeC8VBP6j/rj9OAojO08JI80wTZy6vRk9KvHKiDCUh4iMpEiseMgqRBIeW+eKX6RA== @@ -2743,7 +2763,7 @@ "@svgr/plugin-jsx" "8.1.0" "@svgr/plugin-svgo" "8.1.0" -"@testcontainers/postgresql@^10.16.0": +"@testcontainers/postgresql@^10.18.0": version "10.19.0" resolved "https://registry.yarnpkg.com/@testcontainers/postgresql/-/postgresql-10.19.0.tgz#e1ff9fbfee76c23bc899865524ee8e2ee297bdf2" integrity sha512-3+yQJHCWEtp4hylfZgRxCWN1P6dGqKhFM7Bypg22NpJqq1x/dcmamVCvD+4eTdm1uHV1Ta0BkHRWejxGOyTnrw== @@ -3592,15 +3612,16 @@ classnames "^2.5.1" vaul "^1.0.0" -"@vector-im/matrix-wysiwyg-wasm@link:../../../.cache/yarn/v6/npm-@vector-im-matrix-wysiwyg-2.38.2-3fa19a2a17fd12d955ef1e14fd63aecbcf3b95e8-integrity/node_modules/bindings/wysiwyg-wasm": +"@vector-im/matrix-wysiwyg-wasm@link:../../Library/Caches/Yarn/v6/npm-@vector-im-matrix-wysiwyg-2.38.2-3fa19a2a17fd12d955ef1e14fd63aecbcf3b95e8-integrity/node_modules/bindings/wysiwyg-wasm": version "0.0.0" + uid "" "@vector-im/matrix-wysiwyg@2.38.2": version "2.38.2" resolved "https://registry.yarnpkg.com/@vector-im/matrix-wysiwyg/-/matrix-wysiwyg-2.38.2.tgz#3fa19a2a17fd12d955ef1e14fd63aecbcf3b95e8" integrity sha512-TUnLPgZ8/zGUccQZxjIP3MVHjqybgV4u0r6kXibs35wlXgomXjwcN5gchl3FpgGkiHbi8g3D2ao0oHaqi2GaIw== dependencies: - "@vector-im/matrix-wysiwyg-wasm" "link:../../../.cache/yarn/v6/npm-@vector-im-matrix-wysiwyg-2.38.2-3fa19a2a17fd12d955ef1e14fd63aecbcf3b95e8-integrity/node_modules/bindings/wysiwyg-wasm" + "@vector-im/matrix-wysiwyg-wasm" "link:../../Library/Caches/Yarn/v6/npm-@vector-im-matrix-wysiwyg-2.38.2-3fa19a2a17fd12d955ef1e14fd63aecbcf3b95e8-integrity/node_modules/bindings/wysiwyg-wasm" "@webassemblyjs/ast@1.14.1", "@webassemblyjs/ast@^1.14.1": version "1.14.1" @@ -3805,7 +3826,7 @@ acorn-walk@^8.0.0, acorn-walk@^8.0.2, acorn-walk@^8.1.1: dependencies: acorn "^8.11.0" -acorn@^8.0.4, acorn@^8.1.0, acorn@^8.11.0, acorn@^8.4.1, acorn@^8.9.0: +acorn@^8.0.4, acorn@^8.1.0, acorn@^8.11.0, acorn@^8.9.0: version "8.13.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.13.0.tgz#2a30d670818ad16ddd6a35d3842dacec9e5d7ca3" integrity sha512-8zSiw54Oxrdym50NlZ9sUusyO1Z1ZchgRLWRaK6c86XJFClyCgFKetdowBg5bKxyp/u+CDBJG4Mpp0m3HLZl9w== @@ -3815,6 +3836,11 @@ acorn@^8.14.0, acorn@^8.8.1, acorn@^8.8.2: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.14.0.tgz#063e2c70cac5fb4f6467f0b11152e04c682795b0" integrity sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA== +acorn@^8.4.1: + version "8.14.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.14.1.tgz#721d5dc10f7d5b5609a891773d47731796935dfb" + integrity sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg== + agent-base@6: version "6.0.2" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" @@ -4185,15 +4211,20 @@ await-lock@^2.1.0: resolved "https://registry.yarnpkg.com/await-lock/-/await-lock-2.2.2.tgz#a95a9b269bfd2f69d22b17a321686f551152bcef" integrity sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw== -axe-core@^4.10.0, axe-core@~4.10.2: +axe-core@^4.10.0: version "4.10.2" resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.10.2.tgz#85228e3e1d8b8532a27659b332e39b7fa0e022df" integrity sha512-RE3mdQ7P3FRSe7eqCWoeQ/Z9QXrtniSjp1wUjt5nRC3WIpz5rSCve6o3fsZ2aCpJtrZjSZgjwXAoTO5k4tEI0w== +axe-core@~4.10.2: + version "4.10.3" + resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.10.3.tgz#04145965ac7894faddbac30861e5d8f11bfd14fc" + integrity sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg== + axios@^1.8.1: - version "1.8.2" - resolved "https://registry.yarnpkg.com/axios/-/axios-1.8.2.tgz#fabe06e241dfe83071d4edfbcaa7b1c3a40f7979" - integrity sha512-ls4GYBm5aig9vWx8AWDSGLpnpDQRtWAfrjU+EuytuODrFBkqesN2RkOQCBzrA1RQNHw1SmRMSDDDSwzNAYQ6Rg== + version "1.8.1" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.8.1.tgz#7c118d2146e9ebac512b7d1128771cdd738d11e3" + integrity sha512-NN+fvwH/kV01dYUQ3PTOZns4LWtWhOFCAhQ/pHb88WQ1hNe5V/dvFwc4VJcDL11LT9xSX0QtsR8sWUuyOuOq7g== dependencies: follow-redirects "^1.15.6" form-data "^4.0.0" @@ -8568,6 +8599,11 @@ locate-path@^6.0.0: dependencies: p-locate "^5.0.0" +lodash-es@^4.17.21: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee" + integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw== + lodash.debounce@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" @@ -8672,7 +8708,7 @@ magic-string@0.30.8: dependencies: "@jridgewell/sourcemap-codec" "^1.4.15" -mailpit-api@^1.0.5: +mailpit-api@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/mailpit-api/-/mailpit-api-1.2.0.tgz#6cbd7c5c091fd74b000385790a1fe0c9f2a83fba" integrity sha512-oni/IwQhtbwk3ERwJ6IarKIFgz2U5684SK6Bbkau2GBo2FLoiT14UGkL3CXleYPBH5SCsnymHap1eevEOLwqaA== @@ -8762,8 +8798,8 @@ matrix-events-sdk@0.0.1: integrity sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA== "matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop": - version "37.1.0" - resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/f552370c2625e20a921a5dbf5284491bb6c22861" + version "37.0.0" + resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/d81929de4c9526e7d68ab7226804726cdef6387f" dependencies: "@babel/runtime" "^7.12.5" "@matrix-org/matrix-sdk-crypto-wasm" "^14.0.1" @@ -9559,11 +9595,25 @@ pkg-dir@^4.2.0: dependencies: find-up "^4.0.0" -playwright-core@1.51.0, playwright-core@^1.45.1: +playwright-core@1.50.1: + version "1.50.1" + resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.50.1.tgz#6a0484f1f1c939168f40f0ab3828c4a1592c4504" + integrity sha512-ra9fsNWayuYumt+NiM069M6OkcRb1FZSK8bgi66AtpFoWkg2+y0bJSNmkFrWhMbEBbVKC/EruAHH3g0zmtwGmQ== + +playwright-core@1.51.0, playwright-core@^1.51.0: version "1.51.0" resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.51.0.tgz#bb23ea6bb6298242d088ae5e966ffcf8dc9827e8" integrity sha512-x47yPE3Zwhlil7wlNU/iktF7t2r/URR3VLbH6EknJd/04Qc/PSJ0EY3CMXipmglLG+zyRxW6HNo2EGbKLHPWMg== +playwright@1.50.1: + version "1.50.1" + resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.50.1.tgz#2f93216511d65404f676395bfb97b41aa052b180" + integrity sha512-G8rwsOQJ63XG6BbKj2w5rHeavFjy5zynBA9zsJMMtBoe/Uf757oG12NXz6e6OirF7RCrTVAKFXbLmn1RbL7Qaw== + dependencies: + playwright-core "1.50.1" + optionalDependencies: + fsevents "2.3.2" + playwright@1.51.0: version "1.51.0" resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.51.0.tgz#9ba154497ba62bc6dc199c58ee19295eb35a4707" @@ -11951,10 +12001,10 @@ test-exclude@^6.0.0: glob "^7.1.4" minimatch "^3.0.4" -testcontainers@^10.16.0, testcontainers@^10.19.0: - version "10.19.0" - resolved "https://registry.yarnpkg.com/testcontainers/-/testcontainers-10.19.0.tgz#007138559c6de68c80334232a259f4e94fa19955" - integrity sha512-/mbcCOaj6jj2IPMMmt+YrBi71MZ4BqEzqicjAInsfEox4pVVMnYIW4CkWOdCLiuZ9nVUkoBtxFSJDTqggJNB5A== +testcontainers@10.20.0, testcontainers@^10.18.0, testcontainers@^10.19.0, testcontainers@^10.20.0: + version "10.20.0" + resolved "https://registry.yarnpkg.com/testcontainers/-/testcontainers-10.20.0.tgz#45c524ae4be9b1ffe2fb42b701f6c0a04ee2d90a" + integrity sha512-pOPm/OUIT41aMijAZ9RsYg5xOq9ciy93+pCf2D9qDI0oV8rwk91XpPoUlizll4qwxmmHsLmfZFHJTpeB+BIfmw== dependencies: "@balena/dockerignore" "^1.0.2" "@types/dockerode" "^3.3.29"