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>
This commit is contained in:
Michael Telatynski 2025-03-17 09:16:45 +00:00 committed by GitHub
parent 4af5d4ac80
commit ff1da50dd9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
34 changed files with 226 additions and 1460 deletions

View File

@ -62,19 +62,19 @@
"test": "jest", "test": "jest",
"test:playwright": "playwright test", "test:playwright": "playwright test",
"test:playwright:open": "yarn test:playwright --ui", "test:playwright:open": "yarn test:playwright --ui",
"test:playwright:screenshots": "yarn test:playwright:screenshots:build && yarn test:playwright:screenshots:run", "test:playwright:screenshots": "playwright-screenshots --project=Chrome",
"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",
"coverage": "yarn test --coverage", "coverage": "yarn test --coverage",
"analyse:webpack-bundles": "webpack-bundle-analyzer webpack-stats.json webapp", "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" "update:jitsi": "curl -s https://meet.element.io/libs/external_api.min.js > ./res/jitsi_external_api.min.js"
}, },
"resolutions": { "resolutions": {
"@playwright/test": "1.50.1",
"@types/react": "18.3.18", "@types/react": "18.3.18",
"@types/react-dom": "18.3.5", "@types/react-dom": "18.3.5",
"oidc-client-ts": "3.1.0", "oidc-client-ts": "3.1.0",
"jwt-decode": "4.0.0", "jwt-decode": "4.0.0",
"caniuse-lite": "1.0.30001701", "caniuse-lite": "1.0.30001701",
"testcontainers": "10.20.0",
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0",
"wrap-ansi": "npm:wrap-ansi@^7.0.0" "wrap-ansi": "npm:wrap-ansi@^7.0.0"
}, },
@ -158,7 +158,6 @@
"devDependencies": { "devDependencies": {
"@action-validator/cli": "^0.6.0", "@action-validator/cli": "^0.6.0",
"@action-validator/core": "^0.6.0", "@action-validator/core": "^0.6.0",
"@axe-core/playwright": "^4.8.1",
"@babel/core": "^7.12.10", "@babel/core": "^7.12.10",
"@babel/eslint-parser": "^7.12.10", "@babel/eslint-parser": "^7.12.10",
"@babel/eslint-plugin": "^7.12.10", "@babel/eslint-plugin": "^7.12.10",
@ -178,13 +177,13 @@
"@babel/preset-typescript": "^7.12.7", "@babel/preset-typescript": "^7.12.7",
"@babel/runtime": "^7.12.5", "@babel/runtime": "^7.12.5",
"@casualbot/jest-sonar-reporter": "2.2.7", "@casualbot/jest-sonar-reporter": "2.2.7",
"@element-hq/element-web-playwright-common": "^1.1.5",
"@peculiar/webcrypto": "^1.4.3", "@peculiar/webcrypto": "^1.4.3",
"@playwright/test": "^1.40.1", "@playwright/test": "^1.50.1",
"@principalstudio/html-webpack-inject-preload": "^1.2.7", "@principalstudio/html-webpack-inject-preload": "^1.2.7",
"@sentry/webpack-plugin": "^3.0.0", "@sentry/webpack-plugin": "^3.0.0",
"@stylistic/eslint-plugin": "^3.0.0", "@stylistic/eslint-plugin": "^3.0.0",
"@svgr/webpack": "^8.0.0", "@svgr/webpack": "^8.0.0",
"@testcontainers/postgresql": "^10.16.0",
"@testing-library/dom": "^10.4.0", "@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.4.8", "@testing-library/jest-dom": "^6.4.8",
"@testing-library/react": "^16.0.0", "@testing-library/react": "^16.0.0",
@ -259,13 +258,12 @@
"jsqr": "^1.4.0", "jsqr": "^1.4.0",
"knip": "^5.36.2", "knip": "^5.36.2",
"lint-staged": "^15.0.2", "lint-staged": "^15.0.2",
"mailpit-api": "^1.0.5",
"matrix-web-i18n": "^3.2.1", "matrix-web-i18n": "^3.2.1",
"mini-css-extract-plugin": "2.9.2", "mini-css-extract-plugin": "2.9.2",
"minimist": "^1.2.6", "minimist": "^1.2.6",
"modernizr": "^3.12.0", "modernizr": "^3.12.0",
"node-fetch": "^2.6.7", "node-fetch": "^2.6.7",
"playwright-core": "^1.45.1", "playwright-core": "^1.51.0",
"postcss": "8.4.46", "postcss": "8.4.46",
"postcss-easings": "^4.0.0", "postcss-easings": "^4.0.0",
"postcss-hexrgba": "2.1.0", "postcss-hexrgba": "2.1.0",
@ -282,13 +280,12 @@
"rimraf": "^6.0.0", "rimraf": "^6.0.0",
"semver": "^7.5.2", "semver": "^7.5.2",
"source-map-loader": "^5.0.0", "source-map-loader": "^5.0.0",
"strip-ansi": "^7.1.0",
"stylelint": "^16.13.0", "stylelint": "^16.13.0",
"stylelint-config-standard": "^37.0.0", "stylelint-config-standard": "^37.0.0",
"stylelint-scss": "^6.0.0", "stylelint-scss": "^6.0.0",
"stylelint-value-no-unknown-custom-properties": "^6.0.1", "stylelint-value-no-unknown-custom-properties": "^6.0.1",
"terser-webpack-plugin": "^5.3.9", "terser-webpack-plugin": "^5.3.9",
"testcontainers": "^10.16.0", "testcontainers": "^10.20.0",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
"typescript": "5.8.2", "typescript": "5.8.2",
"util": "^0.12.5", "util": "^0.12.5",

View File

@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details.
import { defineConfig, devices } from "@playwright/test"; 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"; const baseURL = process.env["BASE_URL"] ?? "http://localhost:8080";
@ -21,7 +21,7 @@ const chromeProject = {
}, },
}; };
export default defineConfig<Options>({ export default defineConfig<WorkerOptions>({
projects: [ projects: [
{ {
name: "Chrome", name: "Chrome",
@ -83,6 +83,7 @@ export default defineConfig<Options>({
url: `${baseURL}/config.json`, url: `${baseURL}/config.json`,
reuseExistingServer: true, reuseExistingServer: true,
timeout: (process.env.CI ? 30 : 120) * 1000, timeout: (process.env.CI ? 30 : 120) * 1000,
stdout: "pipe",
}, },
testDir: "playwright/e2e", testDir: "playwright/e2e",
outputDir: "playwright/test-results", outputDir: "playwright/test-results",

View File

@ -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;
}

View File

@ -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"]

View File

@ -1,5 +0,0 @@
#!/bin/bash
set -e
npx playwright test --update-snapshots --reporter line $@

View File

@ -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. 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 { 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 { 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 * A small subset of the Client-Server API used to manipulate the state of the

View File

@ -267,7 +267,6 @@ test.describe("Editing", () => {
app, app,
room, room,
axe, axe,
checkA11y,
}) => { }) => {
axe.disableRules("color-contrast"); // XXX: We have some known contrast issues here 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"); const line = tile.locator(".mx_EventTile_line");
await line.hover(); await line.hover();
await line.getByRole("button", { name: "Edit" }).click(); await line.getByRole("button", { name: "Edit" }).click();
await checkA11y(); await expect(axe).toHaveNoViolations();
const editComposer = page.getByRole("textbox", { name: "Edit message" }); const editComposer = page.getByRole("textbox", { name: "Edit message" });
await editComposer.pressSequentially("Foo"); await editComposer.pressSequentially("Foo");
await editComposer.press("Backspace"); await editComposer.press("Backspace");
@ -290,7 +289,7 @@ test.describe("Editing", () => {
await editComposer.press("Backspace"); await editComposer.press("Backspace");
await editComposer.press("Enter"); await editComposer.press("Enter");
await app.getComposerField().hover(); // XXX: move the hover to get rid of the "Edit" tooltip await app.getComposerField().hover(); // XXX: move the hover to get rid of the "Edit" tooltip
await checkA11y(); await expect(axe).toHaveNoViolations();
} }
await expect( await expect(
page.locator(".mx_RoomView_body .mx_EventTile[data-scroll-tokens]", { hasText: "Message" }), page.locator(".mx_RoomView_body .mx_EventTile[data-scroll-tokens]", { hasText: "Message" }),
@ -305,7 +304,6 @@ test.describe("Editing", () => {
user, user,
app, app,
axe, axe,
checkA11y,
bot: bob, bot: bob,
}) => { }) => {
// This tests the behaviour when a message has been edited some time after it has been sent, and we // This tests the behaviour when a message has been edited some time after it has been sent, and we

View File

@ -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. 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 { expect, test } from "../../element-web-test";
import { selectHomeserver } from "../utils"; import { selectHomeserver } from "../utils";
@ -120,7 +120,7 @@ test.describe("Login", () => {
credentials, credentials,
page, page,
homeserver, homeserver,
checkA11y, axe,
}) => { }) => {
await page.goto("/"); await page.goto("/");
@ -149,7 +149,7 @@ test.describe("Login", () => {
await expect(page.getByRole("textbox", { name: "Username" })).toBeVisible(); await expect(page.getByRole("textbox", { name: "Username" })).toBeVisible();
// Disabled because flaky - see https://github.com/vector-im/element-web/issues/24688 // Disabled because flaky - see https://github.com/vector-im/element-web/issues/24688
// cy.percySnapshot("Login"); // cy.percySnapshot("Login");
await checkA11y(); await expect(axe).toHaveNoViolations();
await page.getByRole("textbox", { name: "Username" }).fill(credentials.username); await page.getByRole("textbox", { name: "Username" }).fill(credentials.username);
await page.getByPlaceholder("Password").fill(credentials.password); await page.getByPlaceholder("Password").fill(credentials.password);

View File

@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details.
/* See readme.md for tips on writing these tests. */ /* 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"; import { test, expect } from "../../element-web-test";

View File

@ -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. 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 { type Page } from "@playwright/test";
import { expect } from "../../element-web-test"; import { expect } from "../../element-web-test";

View File

@ -34,7 +34,7 @@ test.describe("Email Registration", async () => {
test( test(
"registers an account and lands on the home page", "registers an account and lands on the home page",
{ tag: "@screenshot" }, { tag: "@screenshot" },
async ({ page, mailpitClient, request, checkA11y }) => { async ({ page, mailpitClient, request, axe }) => {
await expect(page.getByRole("textbox", { name: "Username" })).toBeVisible(); await expect(page.getByRole("textbox", { name: "Username" })).toBeVisible();
// Hide the server text as it contains the randomly allocated Homeserver port // Hide the server text as it contains the randomly allocated Homeserver port
const screenshotOptions = { mask: [page.locator(".mx_ServerPicker_server")] }; 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.getByText("Check your email to continue")).toBeVisible();
await expect(page).toMatchScreenshot("registration_check_your_email.png", screenshotOptions); 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(); await expect(page.getByText("An error was encountered when sending the email")).not.toBeVisible();

View File

@ -33,12 +33,12 @@ test.describe("Registration", () => {
test( test(
"registers an account and lands on the home screen", "registers an account and lands on the home screen",
{ tag: "@screenshot" }, { tag: "@screenshot" },
async ({ homeserver, page, checkA11y, crypto }) => { async ({ homeserver, page, axe, crypto }) => {
await page.getByRole("button", { name: "Edit", exact: true }).click(); await page.getByRole("button", { name: "Edit", exact: true }).click();
await expect(page.getByRole("button", { name: "Continue", exact: true })).toBeVisible(); await expect(page.getByRole("button", { name: "Continue", exact: true })).toBeVisible();
await expect(page.locator(".mx_Dialog")).toMatchScreenshot("server-picker.png"); 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("textbox", { name: "Other homeserver" }).fill(homeserver.baseUrl);
await page.getByRole("button", { name: "Continue", exact: true }).click(); await page.getByRole("button", { name: "Continue", exact: true }).click();
@ -52,7 +52,7 @@ test.describe("Registration", () => {
includeDialogBackground: true, includeDialogBackground: true,
}; };
await expect(page).toMatchScreenshot("registration.png", screenshotOptions); 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.getByRole("textbox", { name: "Username", exact: true }).fill("alice");
await page.getByPlaceholder("Password", { exact: true }).fill("totally a great password"); await page.getByPlaceholder("Password", { exact: true }).fill("totally a great password");
@ -62,12 +62,12 @@ test.describe("Registration", () => {
const dialog = page.getByRole("dialog"); const dialog = page.getByRole("dialog");
await expect(dialog).toBeVisible(); await expect(dialog).toBeVisible();
await expect(page).toMatchScreenshot("email-prompt.png", screenshotOptions); await expect(page).toMatchScreenshot("email-prompt.png", screenshotOptions);
await checkA11y(); await expect(axe).toHaveNoViolations();
await dialog.getByRole("button", { name: "Continue", exact: true }).click(); await dialog.getByRole("button", { name: "Continue", exact: true }).click();
await expect(page.locator(".mx_InteractiveAuthEntryComponents_termsPolicy")).toBeVisible(); await expect(page.locator(".mx_InteractiveAuthEntryComponents_termsPolicy")).toBeVisible();
await expect(page).toMatchScreenshot("terms-prompt.png", screenshotOptions); await expect(page).toMatchScreenshot("terms-prompt.png", screenshotOptions);
await checkA11y(); await expect(axe).toHaveNoViolations();
const termsPolicy = page.locator(".mx_InteractiveAuthEntryComponents_termsPolicy"); const termsPolicy = page.locator(".mx_InteractiveAuthEntryComponents_termsPolicy");
await termsPolicy.getByRole("checkbox").click(); // Click the checkbox before terms of service anchor link await termsPolicy.getByRole("checkbox").click(); // Click the checkbox before terms of service anchor link

View File

@ -227,7 +227,7 @@ test.describe("Spaces", () => {
test( test(
"should render subspaces in the space panel only when expanded", "should render subspaces in the space panel only when expanded",
{ tag: "@screenshot" }, { tag: "@screenshot" },
async ({ page, app, user, axe, checkA11y }) => { async ({ page, app, user, axe }) => {
axe.disableRules([ axe.disableRules([
// Disable this check as it triggers on nested roving tab index elements which are in practice fine // Disable this check as it triggers on nested roving tab index elements which are in practice fine
"nested-interactive", "nested-interactive",
@ -249,7 +249,7 @@ test.describe("Spaces", () => {
await expect(spaceTree.getByRole("button", { name: "Root Space" })).toBeVisible(); await expect(spaceTree.getByRole("button", { name: "Root Space" })).toBeVisible();
await expect(spaceTree.getByRole("button", { name: "Child Space" })).not.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"); 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 // 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).toBeVisible();
await expect(item.locator(".mx_SpaceItem", { hasText: "Child Space" })).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"); await expect(page.locator(".mx_SpacePanel")).toMatchScreenshot("space-panel-expanded.png");
}, },
); );

View File

@ -277,7 +277,7 @@ test.describe("Timeline", () => {
test( test(
"should add inline start margin to an event line on IRC layout", "should add inline start margin to an event line on IRC layout",
{ tag: "@screenshot" }, { tag: "@screenshot" },
async ({ page, app, room, axe, checkA11y }) => { async ({ page, app, room, axe }) => {
axe.disableRules("color-contrast"); axe.disableRules("color-contrast");
await page.goto(`/#/room/${room.roomId}`); 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(); ).toBeVisible();
}); });
test( test("should render url previews", { tag: "@screenshot" }, async ({ page, app, room, axe, context }) => {
"should render url previews", axe.disableRules("color-contrast");
{ tag: "@screenshot" },
async ({ page, app, room, axe, checkA11y, context }) => {
axe.disableRules("color-contrast");
// Element Web uses a Service Worker to rewrite unauthenticated media requests to authenticated ones, but // 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 // 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 // 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 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. // the js-sdk (and thus the app as a whole) switches to using authenticated endpoints by default, hopefully.
await context.route( await context.route(
"**/_matrix/client/v1/media/thumbnail/matrix.org/2022-08-16_yaiSVSRIsNFfxDnV?*", "**/_matrix/client/v1/media/thumbnail/matrix.org/2022-08-16_yaiSVSRIsNFfxDnV?*",
async (route) => { async (route) => {
await route.fulfill({ await route.fulfill({
path: "playwright/sample-files/riot.png", path: "playwright/sample-files/riot.png",
}); });
}, },
); );
await page.route( await page.route(
"**/_matrix/media/v3/preview_url?url=https%3A%2F%2Fcall.element.io%2F&ts=*", "**/_matrix/media/v3/preview_url?url=https%3A%2F%2Fcall.element.io%2F&ts=*",
async (route) => { async (route) => {
await route.fulfill({ await route.fulfill({
json: { json: {
"og:title": "Element Call", "og:title": "Element Call",
"og:description": null, "og:description": null,
"og:image:width": 48, "og:image:width": 48,
"og:image:height": 48, "og:image:height": 48,
"og:image": "mxc://matrix.org/2022-08-16_yaiSVSRIsNFfxDnV", "og:image": "mxc://matrix.org/2022-08-16_yaiSVSRIsNFfxDnV",
"og:image:type": "image/png", "og:image:type": "image/png",
"matrix:image:size": 2121, "matrix:image:size": 2121,
}, },
}); });
}, },
); );
const requestPromises: Promise<any>[] = [ const requestPromises: Promise<any>[] = [
page.waitForResponse("**/_matrix/media/v3/preview_url?url=https%3A%2F%2Fcall.element.io%2F&ts=*"), 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 // see context.route above for why we listen for the unauthenticated endpoint
page.waitForResponse("**/_matrix/media/v3/thumbnail/matrix.org/2022-08-16_yaiSVSRIsNFfxDnV?*"), page.waitForResponse("**/_matrix/media/v3/thumbnail/matrix.org/2022-08-16_yaiSVSRIsNFfxDnV?*"),
]; ];
await app.client.sendMessage(room.roomId, "https://call.element.io/"); await app.client.sendMessage(room.roomId, "https://call.element.io/");
await page.goto(`/#/room/${room.roomId}`); await page.goto(`/#/room/${room.roomId}`);
await expect(page.locator(".mx_LinkPreviewWidget").getByText("Element Call")).toBeVisible(); await expect(page.locator(".mx_LinkPreviewWidget").getByText("Element Call")).toBeVisible();
await Promise.all(requestPromises); await Promise.all(requestPromises);
await checkA11y(); await expect(axe).toHaveNoViolations();
await app.timeline.scrollToBottom(); await app.timeline.scrollToBottom();
await expect(page.locator(".mx_EventTile_last")).toMatchScreenshot("url-preview.png", { await expect(page.locator(".mx_EventTile_last")).toMatchScreenshot("url-preview.png", {
// Exclude timestamp and read marker from snapshot // Exclude timestamp and read marker from snapshot
mask: [page.locator(".mx_MessageTimestamp")], mask: [page.locator(".mx_MessageTimestamp")],
css: ` css: `
.mx_TopUnreadMessagesBar, .mx_MessagePanel_myReadMarker { .mx_TopUnreadMessagesBar, .mx_MessagePanel_myReadMarker {
display: none !important; display: none !important;
} }
`, `,
}); });
}, });
);
test.describe("on search results panel", () => { test.describe("on search results panel", () => {
test( test(

View File

@ -7,18 +7,18 @@ Please see LICENSE files in the repository root for full details.
*/ */
import { import {
expect as baseExpect,
type Locator,
type Page,
type ExpectMatcherState, type ExpectMatcherState,
type ElementHandle, type MatcherReturnType,
type Page,
type Locator,
type PlaywrightTestArgs, type PlaywrightTestArgs,
type Fixtures as _Fixtures, type Fixtures as _Fixtures,
} from "@playwright/test"; } from "@playwright/test";
import { sanitizeForFilePath } from "playwright-core/lib/utils"; import {
import AxeBuilder from "@axe-core/playwright"; type TestFixtures as BaseTestFixtures,
import _ from "lodash"; expect as baseExpect,
import { extname } from "node:path"; type ToMatchScreenshotOptions,
} from "@element-hq/element-web-playwright-common";
import type { IConfigOptions } from "../src/IConfigOptions"; import type { IConfigOptions } from "../src/IConfigOptions";
import { type Credentials } from "./plugins/homeserver"; import { type Credentials } from "./plugins/homeserver";
@ -27,71 +27,22 @@ import { Crypto } from "./pages/crypto";
import { Toasts } from "./pages/toasts"; import { Toasts } from "./pages/toasts";
import { Bot, type CreateBotOpts } from "./pages/bot"; import { Bot, type CreateBotOpts } from "./pages/bot";
import { Webserver } from "./plugins/webserver"; 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 // Enable experimental service worker support
// See https://playwright.dev/docs/service-workers-experimental#how-to-enable // See https://playwright.dev/docs/service-workers-experimental#how-to-enable
process.env["PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS"] = "1"; 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. declare module "@element-hq/element-web-playwright-common" {
const CONFIG_JSON: Partial<IConfigOptions> = { // Improve the type for the config fixture based on the real type
// The default language is set here for test consistency export interface Config extends Omit<IConfigOptions, "default_server_config"> {}
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,
},
};
export interface CredentialsWithDisplayName extends Credentials { export interface CredentialsWithDisplayName extends Credentials {
displayName: string; displayName: string;
} }
export interface TestFixtures { export interface TestFixtures extends BaseTestFixtures {
axe: AxeBuilder;
checkA11y: () => Promise<void>;
/**
* 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;
/** /**
* The same as {@link https://playwright.dev/docs/api/class-fixtures#fixtures-page|`page`}, * 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, * 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 uut?: Locator; // Unit Under Test, useful place to refer a prepared locator
botCreateOpts: CreateBotOpts; botCreateOpts: CreateBotOpts;
bot: Bot; bot: Bot;
labsFlags: string[];
webserver: Webserver; webserver: Webserver;
disablePresence: boolean;
} }
type CombinedTestFixtures = PlaywrightTestArgs & TestFixtures; type CombinedTestFixtures = PlaywrightTestArgs & TestFixtures;
export type Fixtures = _Fixtures<CombinedTestFixtures, Services & Options, CombinedTestFixtures>; export type Fixtures = _Fixtures<CombinedTestFixtures, Services & WorkerOptions, CombinedTestFixtures>;
export const test = base.extend<TestFixtures>({ export const test = base.extend<TestFixtures>({
context: async ({ context }, use, testInfo) => { context: async ({ context }, use, testInfo) => {
// We skip tests instead of using grep-invert to still surface the counts in the html report // 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<TestFixtures>({
); );
await use(context); await use(context);
}, },
disablePresence: false,
config: {}, // We merge this atop the default CONFIG_JSON in the page fixture to make extending it easier axe: async ({ axe }, use) => {
page: async ({ homeserver, context, page, config, labsFlags, disablePresence }, use) => { // Exclude floating UI for now
await context.route(`http://localhost:8080/config.json*`, async (route) => { await use(axe.exclude("[data-floating-ui-portal]"));
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);
}, },
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) => { app: async ({ page }, use) => {
const app = new ElementAppPage(page); const app = new ElementAppPage(page);
await use(app); await use(app);
@ -244,35 +103,23 @@ export const test = base.extend<TestFixtures>({
}, },
}); });
// Based on https://github.com/microsoft/playwright/blob/2b77ed4d7aafa85a600caa0b0d101b72c8437eeb/packages/playwright/src/util.ts#L206C8-L210C2 interface ExtendedToMatchScreenshotOptions extends ToMatchScreenshotOptions {
function sanitizeFilePathBeforeExtension(filePath: string): string { includeDialogBackground?: boolean;
const ext = extname(filePath); showTooltips?: boolean;
const base = filePath.substring(0, filePath.length - ext.length); timeout?: number;
return sanitizeForFilePath(base) + ext;
} }
export const expect = baseExpect.extend({ type Expectations = {
async toMatchScreenshot( toMatchScreenshot: (
this: ExpectMatcherState, this: ExpectMatcherState,
receiver: Page | Locator, receiver: Page | Locator,
name: `${string}.png`, name: `${string}.png`,
options?: { options?: ExtendedToMatchScreenshotOptions,
mask?: Array<Locator>; ) => Promise<MatcherReturnType>;
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;
export const expect = baseExpect.extend<Expectations>({
async toMatchScreenshot(receiver, name, options) {
let css = ` let css = `
.mx_MessagePanel_myReadMarker { .mx_MessagePanel_myReadMarker {
display: none !important; display: none !important;
@ -322,21 +169,9 @@ export const expect = baseExpect.extend({
css += options.css; css += options.css;
} }
// We add a custom style tag before taking screenshots await baseExpect(receiver).toMatchScreenshot(name, {
const style = (await page.addStyleTag({ ...options,
content: css, css,
})) as ElementHandle<Element>;
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],
}); });
return { pass: true, message: () => "", name: "toMatchScreenshot" }; return { pass: true, message: () => "", name: "toMatchScreenshot" };

View File

@ -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<string, string> = {};
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",
});
}
}
}
}

View File

@ -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. 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"; return homeserverType === "dendrite" || homeserverType === "pinecone";
}; };

View File

@ -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. 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 { export interface HomeserverInstance {
readonly baseUrl: string; readonly baseUrl: string;

View File

@ -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. 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"; import { type Fixtures } from "../../../element-web-test.ts";
export const consentHomeserver: Fixtures = { export const consentHomeserver: Fixtures = {
_homeserver: [ _homeserver: [
async ({ _homeserver: container, mailpit }, use) => { async ({ _homeserver: container, mailpit }, use) => {
container (container as SynapseContainer)
.withCopyDirectoriesToContainer([ .withCopyDirectoriesToContainer([
{ source: "playwright/plugins/homeserver/synapse/res", target: "/data/res" }, { source: "playwright/plugins/homeserver/synapse/res", target: "/data/res" },
]) ])
.withSmtpServer(mailpit)
.withConfig({ .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 <noreply@example.com>",
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: { user_consent: {
template_dir: "/data/res/templates/privacy", template_dir: "/data/res/templates/privacy",
version: "1.0", version: "1.0",

View File

@ -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. 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"; import { type Fixtures } from "../../../element-web-test.ts";
export const masHomeserver: Fixtures = { export const masHomeserver: Fixtures = {

View File

@ -10,8 +10,7 @@ import http from "http";
import express from "express"; import express from "express";
import { type AddressInfo } from "net"; import { type AddressInfo } from "net";
import { type TestInfo } from "@playwright/test"; import { type TestInfo } from "@playwright/test";
import { randB64Bytes } from "@element-hq/element-web-playwright-common/lib/utils/rand.js";
import { randB64Bytes } from "../utils/rand.ts";
export class OAuthServer { export class OAuthServer {
private server?: http.Server; private server?: http.Server;

View File

@ -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<R extends {}>(verb: "GET", path: string, token?: string, data?: never): Promise<R>;
public async request<R extends {}>(verb: Verb, path: string, token?: string, data?: object): Promise<R>;
public async request<R extends {}>(verb: Verb, path: string, token?: string, data?: object): Promise<R> {
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<Credentials> {
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],
};
}
}

View File

@ -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<T>(obj: T): T {
return JSON.parse(JSON.stringify(obj));
}

View File

@ -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<number> {
return new Promise<number>((resolve) => {
const srv = net.createServer();
srv.listen(0, () => {
const port = (<net.AddressInfo>srv.address()).port;
srv.close(() => resolve(port));
});
});
}

View File

@ -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(/=*$/, "");
}

View File

@ -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. Please see LICENSE files in the repository root for full details.
*/ */
import * as http from "http"; import * as http from "node:http";
import { type AddressInfo } from "net"; import { type AddressInfo } from "node:net";
export class Webserver { export class Webserver {
private server?: http.Server; private server?: http.Server;

View File

@ -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. Please see LICENSE files in the repository root for full details.
*/ */
import { test as base } from "@playwright/test"; import { test as base } from "@element-hq/element-web-playwright-common";
import { type MailpitClient } from "mailpit-api"; import {
import { Network, type StartedNetwork } from "testcontainers"; type Services as BaseServices,
import { PostgreSqlContainer, type StartedPostgreSqlContainer } from "@testcontainers/postgresql"; 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 { 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 { type HomeserverType } from "./plugins/homeserver";
import { SynapseContainer } from "./testcontainers/synapse";
export interface TestFixtures { export interface Services extends BaseServices {
mailpitClient: MailpitClient;
}
export interface Services {
logger: Logger;
network: StartedNetwork;
postgres: StartedPostgreSqlContainer;
mailpit: StartedMailhogContainer;
synapseConfig: SynapseConfig;
_homeserver: HomeserverContainer<any>;
homeserver: StartedHomeserverContainer;
// Set in masHomeserver only
mas?: StartedMatrixAuthenticationServiceContainer;
// Set in legacyOAuthHomeserver only // Set in legacyOAuthHomeserver only
oAuthServer?: OAuthServer; oAuthServer?: OAuthServer;
} }
export interface Options { export interface WorkerOptions extends BaseWorkerOptions {
homeserverType: HomeserverType; homeserverType: HomeserverType;
} }
export const test = base.extend<TestFixtures, Services & Options>({ export const test = base.extend<{}, Services & WorkerOptions>({
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" }],
homeserverType: ["synapse", { option: true, scope: "worker" }], homeserverType: ["synapse", { option: true, scope: "worker" }],
_homeserver: [ _homeserver: [
async ({ homeserverType }, use) => { async ({ homeserverType }, use) => {
let container: HomeserverContainer<any>; let container: HomeserverContainer<unknown>;
switch (homeserverType) { switch (homeserverType) {
case "synapse": case "synapse":
container = new SynapseContainer(); container = new SynapseContainer();
@ -128,46 +47,12 @@ export const test = base.extend<TestFixtures, Services & Options>({
}, },
{ scope: "worker" }, { 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); context: async ({ homeserverType, synapseConfig, context, _homeserver }, use, testInfo) => {
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,
) => {
testInfo.skip( testInfo.skip(
!(_homeserver instanceof SynapseContainer) && Object.keys(synapseConfig).length > 0, !(_homeserver instanceof SynapseContainer) && Object.keys(synapseConfig).length > 0,
`Test specifies Synapse config options so is unsupported with ${homeserverType}`, `Test specifies Synapse config options so is unsupported with ${homeserverType}`,
); );
homeserver.setRequest(request);
await logger.onTestStarted(context);
await use(context); await use(context);
await logger.onTestFinished(testInfo);
await homeserver.onTestFinished(testInfo);
}, },
}); });

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View File

@ -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<Config> extends GenericContainer {
withConfigField(key: string, value: any): this;
withConfig(config: Partial<Config>): this;
withMatrixAuthenticationService(mas?: StartedMatrixAuthenticationServiceContainer): this;
start(): Promise<StartedHomeserverContainer>;
}
export interface StartedHomeserverContainer extends AbstractStartedContainer, HomeserverInstance {
setRequest(request: APIRequestContext): void;
onTestFinished(testInfo: TestInfo): Promise<void>;
}

View File

@ -8,12 +8,13 @@ Please see LICENSE files in the repository root for full details.
import { GenericContainer, Wait } from "testcontainers"; import { GenericContainer, Wait } from "testcontainers";
import * as YAML from "yaml"; import * as YAML from "yaml";
import { set } from "lodash"; import { set } from "lodash";
import { randB64Bytes } from "@element-hq/element-web-playwright-common/lib/utils/rand.js";
import { randB64Bytes } from "../plugins/utils/rand.ts"; import { deepCopy } from "@element-hq/element-web-playwright-common/lib/utils/object.js";
import { StartedSynapseContainer } from "./synapse.ts"; import {
import { deepCopy } from "../plugins/utils/object.ts"; StartedSynapseContainer,
import { type HomeserverContainer } from "./HomeserverContainer.ts"; type HomeserverContainer,
import { type StartedMatrixAuthenticationServiceContainer } from "./mas.ts"; type StartedMatrixAuthenticationServiceContainer,
} from "@element-hq/element-web-playwright-common/lib/testcontainers";
const DEFAULT_CONFIG = { const DEFAULT_CONFIG = {
version: 2, version: 2,
@ -223,7 +224,7 @@ export class DendriteContainer extends GenericContainer implements HomeserverCon
.withWaitStrategy(Wait.forHttp("/_matrix/client/versions", 8008)); .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); set(this.config, key, value);
return this; return this;
} }
@ -236,6 +237,11 @@ export class DendriteContainer extends GenericContainer implements HomeserverCon
return this; 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 // Dendrite does not support MAS at this time
public withMatrixAuthenticationService(mas?: StartedMatrixAuthenticationServiceContainer): this { public withMatrixAuthenticationService(mas?: StartedMatrixAuthenticationServiceContainer): this {
return this; return this;

View File

@ -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<StartedMailhogContainer> {
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)}`);
}
}

View File

@ -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" <root@localhost>',
reply_to: '"Authentication Service" <root@localhost>',
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<StartedMatrixAuthenticationServiceContainer> {
// 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<string>;
constructor(
container: StartedTestContainer,
public readonly baseUrl: string,
private readonly args: string[],
) {
super(container);
}
public async getAdminToken(): Promise<string> {
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<ExecResult> {
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<string> {
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<Credentials> {
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<Credentials> {
return this.registerUserInternal(username, password, displayName, false);
}
public async setThreepid(username: string, medium: string, address: string): Promise<void> {
if (medium !== "email") {
throw new Error("Only email threepids are supported by MAS");
}
await this.manage("add-email", username, address);
}
}

View File

@ -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 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. Please see LICENSE files in the repository root for full details.
*/ */
import { import { SynapseContainer as BaseSynapseContainer } from "@element-hq/element-web-playwright-common/lib/testcontainers";
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";
const TAG = "develop@sha256:2ea87d45fc7ff3327c671b3b4447e6b2032d4f5ca07d62d8aef0d900e105c2f4"; const TAG = "develop@sha256:2ea87d45fc7ff3327c671b3b4447e6b2032d4f5ca07d62d8aef0d900e105c2f4";
const DEFAULT_CONFIG = { /**
server_name: "localhost", * SynapseContainer which freezes the docker digest to stabilise tests,
public_baseurl: "", // set by start method * updated periodically by the `playwright-image-updates.yaml` workflow.
pid_file: "/homeserver.pid", */
web_client: false, export class SynapseContainer extends BaseSynapseContainer {
soft_file_limit: 0, public constructor() {
// 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<typeof DEFAULT_CONFIG>;
export class SynapseContainer extends GenericContainer implements HomeserverContainer<typeof DEFAULT_CONFIG> {
private config: typeof DEFAULT_CONFIG;
private mas?: StartedMatrixAuthenticationServiceContainer;
constructor() {
super(`ghcr.io/element-hq/synapse:${TAG}`); 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<typeof DEFAULT_CONFIG>): this {
this.config = {
...this.config,
...config,
};
return this;
}
public withMatrixAuthenticationService(mas?: StartedMatrixAuthenticationServiceContainer): this {
this.mas = mas;
return this;
}
public override async start(): Promise<StartedSynapseContainer> {
// 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<string>;
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<RestartOptions>): Promise<void> {
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<void> {
// Clean up the server to prevent rooms leaking between tests
await this.deletePublicRooms();
}
protected async deletePublicRooms(): Promise<void> {
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<Credentials> {
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<string> {
if (this.adminTokenPromise === undefined) {
this.adminTokenPromise = this.registerUserInternal(
"admin",
"totalyinsecureadminpassword",
undefined,
true,
).then((res) => res.accessToken);
}
return this.adminTokenPromise;
}
private async adminRequest<R extends {}>(verb: "GET", path: string, data?: never): Promise<R>;
private async adminRequest<R extends {}>(verb: Verb, path: string, data?: object): Promise<R>;
private async adminRequest<R extends {}>(verb: Verb, path: string, data?: object): Promise<R> {
const adminToken = await this.getAdminToken();
return this.adminApi.request(verb, path, adminToken, data);
}
public registerUser(username: string, password: string, displayName?: string): Promise<Credentials> {
return this.registerUserInternal(username, password, displayName, false);
}
public async loginUser(userId: string, password: string): Promise<Credentials> {
return this.csApi.loginUser(userId, password);
}
public async setThreepid(userId: string, medium: string, address: string): Promise<void> {
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<string> {
if (this.adminTokenPromise === undefined) {
this.adminTokenPromise = this.mas.getAdminToken();
}
return this.adminTokenPromise;
}
public registerUser(username: string, password: string, displayName?: string): Promise<Credentials> {
return this.mas.registerUser(username, password, displayName);
}
public async setThreepid(userId: string, medium: string, address: string): Promise<void> {
return this.mas.setThreepid(userId, medium, address);
} }
} }

View File

@ -27,7 +27,7 @@
"@jridgewell/gen-mapping" "^0.3.5" "@jridgewell/gen-mapping" "^0.3.5"
"@jridgewell/trace-mapping" "^0.3.24" "@jridgewell/trace-mapping" "^0.3.24"
"@axe-core/playwright@^4.8.1": "@axe-core/playwright@^4.10.1":
version "4.10.1" version "4.10.1"
resolved "https://registry.yarnpkg.com/@axe-core/playwright/-/playwright-4.10.1.tgz#c811ba8bfa244833cce422c4131e0043828c42cc" resolved "https://registry.yarnpkg.com/@axe-core/playwright/-/playwright-4.10.1.tgz#c811ba8bfa244833cce422c4131e0043828c42cc"
integrity sha512-EV5t39VV68kuAfMKqb/RL+YjYKhfuGim9rgIaQ6Vntb2HgaCaau0h98Y3WEUqW1+PbdzxDtDNjFAipbtZuBmEA== 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" 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== 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": "@eslint-community/eslint-utils@^4.2.0":
version "4.4.0" version "4.4.0"
resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" 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" resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33"
integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== 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" version "1.51.0"
resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.51.0.tgz#8d5c8400b465a0bfdbcf993e390ceecb903ea6d2" resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.51.0.tgz#8d5c8400b465a0bfdbcf993e390ceecb903ea6d2"
integrity sha512-dJ0dMbZeHhI+wb77+ljx/FeC8VBP6j/rj9OAojO08JI80wTZy6vRk9KvHKiDCUh4iMpEiseMgqRBIeW+eKX6RA== integrity sha512-dJ0dMbZeHhI+wb77+ljx/FeC8VBP6j/rj9OAojO08JI80wTZy6vRk9KvHKiDCUh4iMpEiseMgqRBIeW+eKX6RA==
@ -2743,7 +2763,7 @@
"@svgr/plugin-jsx" "8.1.0" "@svgr/plugin-jsx" "8.1.0"
"@svgr/plugin-svgo" "8.1.0" "@svgr/plugin-svgo" "8.1.0"
"@testcontainers/postgresql@^10.16.0": "@testcontainers/postgresql@^10.18.0":
version "10.19.0" version "10.19.0"
resolved "https://registry.yarnpkg.com/@testcontainers/postgresql/-/postgresql-10.19.0.tgz#e1ff9fbfee76c23bc899865524ee8e2ee297bdf2" resolved "https://registry.yarnpkg.com/@testcontainers/postgresql/-/postgresql-10.19.0.tgz#e1ff9fbfee76c23bc899865524ee8e2ee297bdf2"
integrity sha512-3+yQJHCWEtp4hylfZgRxCWN1P6dGqKhFM7Bypg22NpJqq1x/dcmamVCvD+4eTdm1uHV1Ta0BkHRWejxGOyTnrw== integrity sha512-3+yQJHCWEtp4hylfZgRxCWN1P6dGqKhFM7Bypg22NpJqq1x/dcmamVCvD+4eTdm1uHV1Ta0BkHRWejxGOyTnrw==
@ -3592,15 +3612,16 @@
classnames "^2.5.1" classnames "^2.5.1"
vaul "^1.0.0" 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" version "0.0.0"
uid ""
"@vector-im/matrix-wysiwyg@2.38.2": "@vector-im/matrix-wysiwyg@2.38.2":
version "2.38.2" version "2.38.2"
resolved "https://registry.yarnpkg.com/@vector-im/matrix-wysiwyg/-/matrix-wysiwyg-2.38.2.tgz#3fa19a2a17fd12d955ef1e14fd63aecbcf3b95e8" resolved "https://registry.yarnpkg.com/@vector-im/matrix-wysiwyg/-/matrix-wysiwyg-2.38.2.tgz#3fa19a2a17fd12d955ef1e14fd63aecbcf3b95e8"
integrity sha512-TUnLPgZ8/zGUccQZxjIP3MVHjqybgV4u0r6kXibs35wlXgomXjwcN5gchl3FpgGkiHbi8g3D2ao0oHaqi2GaIw== integrity sha512-TUnLPgZ8/zGUccQZxjIP3MVHjqybgV4u0r6kXibs35wlXgomXjwcN5gchl3FpgGkiHbi8g3D2ao0oHaqi2GaIw==
dependencies: 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": "@webassemblyjs/ast@1.14.1", "@webassemblyjs/ast@^1.14.1":
version "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: dependencies:
acorn "^8.11.0" 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" version "8.13.0"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.13.0.tgz#2a30d670818ad16ddd6a35d3842dacec9e5d7ca3" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.13.0.tgz#2a30d670818ad16ddd6a35d3842dacec9e5d7ca3"
integrity sha512-8zSiw54Oxrdym50NlZ9sUusyO1Z1ZchgRLWRaK6c86XJFClyCgFKetdowBg5bKxyp/u+CDBJG4Mpp0m3HLZl9w== 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" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.14.0.tgz#063e2c70cac5fb4f6467f0b11152e04c682795b0"
integrity sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA== 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: agent-base@6:
version "6.0.2" version "6.0.2"
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" 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" resolved "https://registry.yarnpkg.com/await-lock/-/await-lock-2.2.2.tgz#a95a9b269bfd2f69d22b17a321686f551152bcef"
integrity sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw== integrity sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw==
axe-core@^4.10.0, axe-core@~4.10.2: axe-core@^4.10.0:
version "4.10.2" version "4.10.2"
resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.10.2.tgz#85228e3e1d8b8532a27659b332e39b7fa0e022df" resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.10.2.tgz#85228e3e1d8b8532a27659b332e39b7fa0e022df"
integrity sha512-RE3mdQ7P3FRSe7eqCWoeQ/Z9QXrtniSjp1wUjt5nRC3WIpz5rSCve6o3fsZ2aCpJtrZjSZgjwXAoTO5k4tEI0w== 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: axios@^1.8.1:
version "1.8.2" version "1.8.1"
resolved "https://registry.yarnpkg.com/axios/-/axios-1.8.2.tgz#fabe06e241dfe83071d4edfbcaa7b1c3a40f7979" resolved "https://registry.yarnpkg.com/axios/-/axios-1.8.1.tgz#7c118d2146e9ebac512b7d1128771cdd738d11e3"
integrity sha512-ls4GYBm5aig9vWx8AWDSGLpnpDQRtWAfrjU+EuytuODrFBkqesN2RkOQCBzrA1RQNHw1SmRMSDDDSwzNAYQ6Rg== integrity sha512-NN+fvwH/kV01dYUQ3PTOZns4LWtWhOFCAhQ/pHb88WQ1hNe5V/dvFwc4VJcDL11LT9xSX0QtsR8sWUuyOuOq7g==
dependencies: dependencies:
follow-redirects "^1.15.6" follow-redirects "^1.15.6"
form-data "^4.0.0" form-data "^4.0.0"
@ -8568,6 +8599,11 @@ locate-path@^6.0.0:
dependencies: dependencies:
p-locate "^5.0.0" 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: lodash.debounce@^4.0.8:
version "4.0.8" version "4.0.8"
resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
@ -8672,7 +8708,7 @@ magic-string@0.30.8:
dependencies: dependencies:
"@jridgewell/sourcemap-codec" "^1.4.15" "@jridgewell/sourcemap-codec" "^1.4.15"
mailpit-api@^1.0.5: mailpit-api@^1.2.0:
version "1.2.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/mailpit-api/-/mailpit-api-1.2.0.tgz#6cbd7c5c091fd74b000385790a1fe0c9f2a83fba" resolved "https://registry.yarnpkg.com/mailpit-api/-/mailpit-api-1.2.0.tgz#6cbd7c5c091fd74b000385790a1fe0c9f2a83fba"
integrity sha512-oni/IwQhtbwk3ERwJ6IarKIFgz2U5684SK6Bbkau2GBo2FLoiT14UGkL3CXleYPBH5SCsnymHap1eevEOLwqaA== integrity sha512-oni/IwQhtbwk3ERwJ6IarKIFgz2U5684SK6Bbkau2GBo2FLoiT14UGkL3CXleYPBH5SCsnymHap1eevEOLwqaA==
@ -8762,8 +8798,8 @@ matrix-events-sdk@0.0.1:
integrity sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA== integrity sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA==
"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop": "matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop":
version "37.1.0" version "37.0.0"
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/f552370c2625e20a921a5dbf5284491bb6c22861" resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/d81929de4c9526e7d68ab7226804726cdef6387f"
dependencies: dependencies:
"@babel/runtime" "^7.12.5" "@babel/runtime" "^7.12.5"
"@matrix-org/matrix-sdk-crypto-wasm" "^14.0.1" "@matrix-org/matrix-sdk-crypto-wasm" "^14.0.1"
@ -9559,11 +9595,25 @@ pkg-dir@^4.2.0:
dependencies: dependencies:
find-up "^4.0.0" 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" version "1.51.0"
resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.51.0.tgz#bb23ea6bb6298242d088ae5e966ffcf8dc9827e8" resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.51.0.tgz#bb23ea6bb6298242d088ae5e966ffcf8dc9827e8"
integrity sha512-x47yPE3Zwhlil7wlNU/iktF7t2r/URR3VLbH6EknJd/04Qc/PSJ0EY3CMXipmglLG+zyRxW6HNo2EGbKLHPWMg== 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: playwright@1.51.0:
version "1.51.0" version "1.51.0"
resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.51.0.tgz#9ba154497ba62bc6dc199c58ee19295eb35a4707" 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" glob "^7.1.4"
minimatch "^3.0.4" minimatch "^3.0.4"
testcontainers@^10.16.0, testcontainers@^10.19.0: testcontainers@10.20.0, testcontainers@^10.18.0, testcontainers@^10.19.0, testcontainers@^10.20.0:
version "10.19.0" version "10.20.0"
resolved "https://registry.yarnpkg.com/testcontainers/-/testcontainers-10.19.0.tgz#007138559c6de68c80334232a259f4e94fa19955" resolved "https://registry.yarnpkg.com/testcontainers/-/testcontainers-10.20.0.tgz#45c524ae4be9b1ffe2fb42b701f6c0a04ee2d90a"
integrity sha512-/mbcCOaj6jj2IPMMmt+YrBi71MZ4BqEzqicjAInsfEox4pVVMnYIW4CkWOdCLiuZ9nVUkoBtxFSJDTqggJNB5A== integrity sha512-pOPm/OUIT41aMijAZ9RsYg5xOq9ciy93+pCf2D9qDI0oV8rwk91XpPoUlizll4qwxmmHsLmfZFHJTpeB+BIfmw==
dependencies: dependencies:
"@balena/dockerignore" "^1.0.2" "@balena/dockerignore" "^1.0.2"
"@types/dockerode" "^3.3.29" "@types/dockerode" "^3.3.29"