Compare commits

..

8 Commits

Author SHA1 Message Date
Half-Shot
bcf9854a4c Fix so that we still use notificationsMuted / unread even if a room isn't in view. 2025-01-17 12:14:46 +00:00
Half-Shot
b589757c34 Replace with module API. 2025-01-17 11:48:53 +00:00
Half-Shot
0d576b217b lint 2025-01-17 11:48:53 +00:00
Half-Shot
80cc0a928f dollars 2025-01-17 11:48:53 +00:00
Half-Shot
9cccbeb799 clear current room in two more contexts. 2025-01-17 11:48:53 +00:00
Half-Shot
a8c170f8be Add tests 2025-01-17 11:48:53 +00:00
Half-Shot
f5402b4ec4 Add ability to customize the title template in branding. 2025-01-17 11:48:53 +00:00
Half-Shot
131b28ede8 New config options. 2025-01-17 11:48:53 +00:00
1496 changed files with 7130 additions and 12805 deletions

View File

@@ -7,4 +7,3 @@ test/end-to-end-tests/lib/
src/component-index.js
# Auto-generated file
src/modules.ts
src/modules.js

View File

@@ -200,13 +200,8 @@ module.exports = {
"@typescript-eslint/ban-ts-comment": "off",
// We're okay with assertion errors when we ask for them
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-empty-object-type": [
"error",
{
// We do this sometimes to brand interfaces
allowInterfaces: "with-single-extends",
},
],
// We do this sometimes to brand interfaces
"@typescript-eslint/no-empty-object-type": "off",
},
},
// temporary override for offending icon require files
@@ -252,7 +247,6 @@ module.exports = {
// We don't need super strict typing in test utilities
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/explicit-member-accessibility": "off",
"@typescript-eslint/no-empty-object-type": "off",
// Jest/Playwright specific

5
.github/CODEOWNERS vendored
View File

@@ -10,11 +10,10 @@
/test/components/views/dialogs/security/ @element-hq/element-crypto-web-reviewers
/src/stores/SetupEncryptionStore.ts @element-hq/element-crypto-web-reviewers
/test/stores/SetupEncryptionStore-test.ts @element-hq/element-crypto-web-reviewers
/src/components/views/settings/tabs/user/EncryptionUserSettingsTab.tsx @element-hq/element-crypto-web-reviewers
/src/components/views/settings/tabs/user/EncryptionUserSettingsTab.tsx @element-hq/element-crypto-web-reviewers
/src/src/components/views/settings/encryption/ @element-hq/element-crypto-web-reviewers
/test/unit-tests/components/views/settings/encryption/ @element-hq/element-crypto-web-reviewers
/playwright/e2e/settings/encryption-user-tab/ @element-hq/element-crypto-web-reviewers
/src/components/views/dialogs/devtools/Crypto.tsx @element-hq/element-crypto-web-reviewers
/playwright/e2e/settings/encryption-user-tab/ @element-hq/element-crypto-web-reviewers
# Ignore translations as those will be updated by GHA for Localazy download
/src/i18n/strings

View File

@@ -26,12 +26,6 @@ jobs:
R2_URL: ${{ vars.CF_R2_S3_API }}
R2_PUBLIC_URL: "https://element-web-develop.element.io"
steps:
# Workaround for https://www.cloudflarestatus.com/incidents/t5nrjmpxc1cj
- uses: unfor19/install-aws-cli-action@v1
with:
version: 2.22.35
verbose: false
arch: amd64
- uses: actions/checkout@v4
- uses: actions/setup-node@v4

View File

@@ -96,4 +96,3 @@ jobs:
projectName: ${{ env.SITE == 'staging.element.io' && 'element-web-staging' || 'element-web' }}
directory: _deploy
gitHubToken: ${{ secrets.GITHUB_TOKEN }}
branch: main

View File

@@ -51,7 +51,7 @@ jobs:
- name: Build and push
id: build-and-push
uses: docker/build-push-action@ca877d9245402d1537745e0e356eab47c3520991 # v6
uses: docker/build-push-action@b32b51a8eda65d6793cd0494a773d4f6bcef32dc # v6
with:
context: .
push: true

View File

@@ -104,7 +104,7 @@ jobs:
- name: Skip SonarCloud in merge queue
if: github.event_name == 'merge_group' || inputs.disable_coverage == 'true'
uses: guibranco/github-status-action-v2@119b3320db3f04d89e91df840844b92d57ce3468
uses: guibranco/github-status-action-v2@56cd38caf0615dd03f49d42ed301f1469911ac61
with:
authToken: ${{ secrets.GITHUB_TOKEN }}
state: success

1
.gitignore vendored
View File

@@ -26,7 +26,6 @@ electron/pub
/coverage
# Auto-generated file
/src/modules.ts
/src/modules.js
/build_config.yaml
/book
/index.html

View File

@@ -17,7 +17,6 @@ electron/pub
/coverage
# Auto-generated file
/src/modules.ts
/src/modules.js
/src/i18n/strings
/build_config.yaml
# Raises an error because it contains a template var breaking the script tag

View File

@@ -33,15 +33,19 @@ module.exports = {
"import-notation": null,
"value-keyword-case": null,
"declaration-block-no-redundant-longhand-properties": null,
"declaration-block-no-duplicate-properties": [
true,
// useful for fallbacks
{ ignore: ["consecutive-duplicates-with-different-values"] },
],
"shorthand-property-no-redundant-values": null,
"property-no-vendor-prefix": null,
"value-no-vendor-prefix": null,
"selector-no-vendor-prefix": null,
"media-feature-name-no-vendor-prefix": null,
"number-max-precision": null,
"no-invalid-double-slash-comments": true,
"media-feature-range-notation": null,
"declaration-property-value-no-unknown": null,
"declaration-property-value-keyword-no-deprecated": null,
"csstools/value-no-unknown-custom-properties": [
true,
{

View File

@@ -1,21 +1,3 @@
Changes in [1.11.91](https://github.com/element-hq/element-web/releases/tag/v1.11.91) (2025-01-28)
==================================================================================================
## ✨ Features
* Implement changes to memberlist from feedback ([#29029](https://github.com/element-hq/element-web/pull/29029)). Contributed by @MidhunSureshR.
* Add toast for recovery keys being out of sync ([#28946](https://github.com/element-hq/element-web/pull/28946)). Contributed by @dbkr.
* Refactor LegacyCallHandler event emitter to use TypedEventEmitter ([#29008](https://github.com/element-hq/element-web/pull/29008)). Contributed by @t3chguy.
* Add `Recovery` section in the new user settings `Encryption` tab ([#28673](https://github.com/element-hq/element-web/pull/28673)). Contributed by @florianduros.
* Retry loading chunks to make the app more resilient ([#29001](https://github.com/element-hq/element-web/pull/29001)). Contributed by @t3chguy.
* Clear account idb table on logout ([#28996](https://github.com/element-hq/element-web/pull/28996)). Contributed by @t3chguy.
* Implement new memberlist design with MVVM architecture ([#28874](https://github.com/element-hq/element-web/pull/28874)). Contributed by @MidhunSureshR.
## 🐛 Bug Fixes
* [Backport staging] Switch to secure random strings ([#29035](https://github.com/element-hq/element-web/pull/29035)). Contributed by @RiotRobot.
* React to MatrixEvent sender/target being updated for rendering state events ([#28947](https://github.com/element-hq/element-web/pull/28947)). Contributed by @t3chguy.
Changes in [1.11.90](https://github.com/element-hq/element-web/releases/tag/v1.11.90) (2025-01-14)
==================================================================================================
## ✨ Features

2
debian/control vendored
View File

@@ -8,6 +8,6 @@ Package: element-web
Architecture: all
Recommends: httpd, element-io-archive-keyring
Description:
Element: the future of secure communication
A feature-rich client for Matrix.org
This package contains the web-based client that can be served through a web
server.

View File

@@ -8,13 +8,11 @@
#### develop
The develop branch holds the very latest and greatest code we have to offer, as such it may be less stable.
It is auto-deployed on every commit to element-web or matrix-js-sdk to develop.element.io via GitHub Actions `build_develop.yml`.
The develop branch holds the very latest and greatest code we have to offer, as such it may be less stable. It corresponds to the develop.element.io CD platform.
#### staging
The staging branch corresponds to the very latest release regardless of whether it is an RC or not. Deployed to staging.element.io manually.
It is auto-deployed on every release of element-web to staging.element.io via GitHub Actions `deploy.yml`.
#### master
@@ -217,7 +215,7 @@ We ship Element Web to dockerhub, `*.element.io`, and packages.element.io.
We ship Element Desktop to packages.element.io.
- [ ] Check that element-web has shipped to dockerhub
- [ ] Check that the staging [deployment](https://github.com/element-hq/element-web/actions/workflows/deploy.yml) has completed successfully
- [ ] Deploy staging.element.io. [See docs.](https://handbook.element.io/books/element-web-team/page/deploying-appstagingelementio)
- [ ] Test staging.element.io
For final releases additionally do these steps:
@@ -227,9 +225,6 @@ For final releases additionally do these steps:
- [ ] Ensure Element Web package has shipped to packages.element.io
- [ ] Ensure Element Desktop packages have shipped to packages.element.io
If you need to roll back a deployment to staging.element.io,
you can run the `deploy.yml` automation choosing an older tag which you wish to deploy.
# Housekeeping
We have some manual housekeeping to do in order to prepare for the next release.

View File

@@ -9,7 +9,7 @@ import * as fs from "fs";
import * as childProcess from "child_process";
import * as semver from "semver";
import { type BuildConfig } from "./BuildConfig";
import { BuildConfig } from "./BuildConfig";
// This expects to be run from ./scripts/install.ts
@@ -23,9 +23,10 @@ const MODULES_TS_HEADER = `
* You are not a salmon.
*/
import { RuntimeModule } from "@matrix-org/react-sdk-module-api/lib/RuntimeModule";
`;
const MODULES_TS_DEFINITIONS = `
export const INSTALLED_MODULES = [];
export const INSTALLED_MODULES: RuntimeModule[] = [];
`;
export function installer(config: BuildConfig): void {
@@ -77,8 +78,8 @@ export function installer(config: BuildConfig): void {
return; // hit the finally{} block before exiting
}
// If we reach here, everything seems fine. Write modules.js and log some output
// Note: we compile modules.js in two parts for developer friendliness if they
// If we reach here, everything seems fine. Write modules.ts and log some output
// Note: we compile modules.ts in two parts for developer friendliness if they
// happen to look at it.
console.log("The following modules have been installed: ", installedModules);
let modulesTsHeader = MODULES_TS_HEADER;
@@ -192,5 +193,5 @@ function isModuleVersionCompatible(ourApiVersion: string, moduleApiVersion: stri
}
function writeModulesTs(content: string): void {
fs.writeFileSync("./src/modules.js", content, "utf-8");
fs.writeFileSync("./src/modules.ts", content, "utf-8");
}

View File

@@ -1,7 +1,7 @@
{
"name": "element-web",
"version": "1.11.91",
"description": "Element: the future of secure communication",
"version": "1.11.90",
"description": "A feature-rich client for Matrix.org",
"author": "New Vector Ltd.",
"repository": {
"type": "git",
@@ -74,7 +74,7 @@
"@types/react-dom": "18.3.5",
"oidc-client-ts": "3.1.0",
"jwt-decode": "4.0.0",
"caniuse-lite": "1.0.30001697",
"caniuse-lite": "1.0.30001690",
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0",
"wrap-ansi": "npm:wrap-ansi@^7.0.0"
},
@@ -84,14 +84,14 @@
"@fontsource/inter": "^5",
"@formatjs/intl-segmenter": "^11.5.7",
"@matrix-org/analytics-events": "^0.29.0",
"@matrix-org/emojibase-bindings": "^1.3.4",
"@matrix-org/emojibase-bindings": "^1.3.3",
"@matrix-org/react-sdk-module-api": "^2.4.0",
"@matrix-org/spec": "^1.7.0",
"@sentry/browser": "^8.0.0",
"@types/png-chunks-extract": "^1.0.2",
"@types/react-virtualized": "^9.21.30",
"@vector-im/compound-design-tokens": "^3.0.0",
"@vector-im/compound-web": "^7.6.1",
"@vector-im/compound-design-tokens": "^2.1.0",
"@vector-im/compound-web": "^7.5.0",
"@vector-im/matrix-wysiwyg": "2.38.0",
"@zxcvbn-ts/core": "^3.0.4",
"@zxcvbn-ts/language-common": "^3.0.4",
@@ -178,8 +178,8 @@
"@peculiar/webcrypto": "^1.4.3",
"@playwright/test": "^1.40.1",
"@principalstudio/html-webpack-inject-preload": "^1.2.7",
"@sentry/webpack-plugin": "^3.0.0",
"@stylistic/eslint-plugin": "^3.0.0",
"@sentry/webpack-plugin": "^2.7.1",
"@stylistic/eslint-plugin": "^2.9.0",
"@svgr/webpack": "^8.0.0",
"@testcontainers/postgresql": "^10.16.0",
"@testing-library/dom": "^10.4.0",
@@ -230,7 +230,7 @@
"dotenv": "^16.0.2",
"eslint": "8.57.1",
"eslint-config-google": "^0.14.0",
"eslint-config-prettier": "^10.0.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-deprecate": "0.8.5",
"eslint-plugin-import": "^2.25.4",
"eslint-plugin-jest": "^28.0.0",
@@ -256,7 +256,7 @@
"jsqr": "^1.4.0",
"knip": "^5.36.2",
"lint-staged": "^15.0.2",
"mailpit-api": "^1.0.5",
"mailhog": "^4.16.0",
"matrix-web-i18n": "^3.2.1",
"mini-css-extract-plugin": "2.9.2",
"minimist": "^1.2.6",
@@ -280,14 +280,14 @@
"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": "^16.1.0",
"stylelint-config-standard": "^36.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",
"ts-node": "^10.9.1",
"typescript": "5.7.3",
"typescript": "5.7.2",
"util": "^0.12.5",
"web-streams-polyfill": "^4.0.0",
"webpack": "^5.89.0",

View File

@@ -11,7 +11,7 @@ import type { Locator, Page } from "@playwright/test";
import { test, expect } from "../../element-web-test";
import { SettingLevel } from "../../../src/settings/SettingLevel";
import { Layout } from "../../../src/settings/enums/Layout";
import { type ElementAppPage } from "../../pages/ElementAppPage";
import { ElementAppPage } from "../../pages/ElementAppPage";
// Find and click "Reply" button
const clickButtonReply = async (tile: Locator) => {

View File

@@ -0,0 +1,43 @@
/*
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 { expect, test } from "../../element-web-test";
/*
* Tests for branding configuration
**/
test.describe("Test without branding config", () => {
test("Shows standard branding when showing the home page", async ({ pageWithCredentials: page }) => {
await page.goto("/");
await page.waitForSelector(".mx_MatrixChat", { timeout: 30000 });
expect(page.title()).toEqual("Element *");
});
test("Shows standard branding when showing a room", async ({ app, pageWithCredentials: page }) => {
await app.client.createRoom({ name: "Test Room" });
await app.viewRoomByName("Test Room");
expect(page.title()).toEqual("Element * | Test Room");
});
});
test.describe("Test with custom branding", () => {
test.use({
config: {
brand: "TestBrand",
},
});
test("Shows custom branding when showing the home page", async ({ pageWithCredentials: page }) => {
await page.goto("/");
await page.waitForSelector(".mx_MatrixChat", { timeout: 30000 });
expect(page.title()).toEqual("TestingApp TestBrand * $ignoredParameter");
});
test("Shows custom branding when showing a room", async ({ app, pageWithCredentials: page }) => {
await app.client.createRoom({ name: "Test Room" });
await app.viewRoomByName("Test Room");
expect(page.title()).toEqual("TestingApp TestBrand * Test Room $ignoredParameter");
});
});

View File

@@ -19,19 +19,19 @@ test.use(masHomeserver);
test.describe("Encryption state after registration", () => {
test.skip(isDendrite, "does not yet support MAS");
test("Key backup is enabled by default", async ({ page, mailpitClient, app }, testInfo) => {
test("Key backup is enabled by default", async ({ page, mailhogClient, app }, testInfo) => {
await page.goto("/#/login");
await page.getByRole("button", { name: "Continue" }).click();
await registerAccountMas(page, mailpitClient, `alice_${testInfo.testId}`, "alice@email.com", "Pa$sW0rD!");
await registerAccountMas(page, mailhogClient, `alice_${testInfo.testId}`, "alice@email.com", "Pa$sW0rD!");
await app.settings.openUserSettings("Security & Privacy");
await expect(page.getByText("This session is backing up your keys.")).toBeVisible();
});
test("user is prompted to set up recovery", async ({ page, mailpitClient, app }, testInfo) => {
test("user is prompted to set up recovery", async ({ page, mailhogClient, app }, testInfo) => {
await page.goto("/#/login");
await page.getByRole("button", { name: "Continue" }).click();
await registerAccountMas(page, mailpitClient, `alice_${testInfo.testId}`, "alice@email.com", "Pa$sW0rD!");
await registerAccountMas(page, mailhogClient, `alice_${testInfo.testId}`, "alice@email.com", "Pa$sW0rD!");
await page.getByRole("button", { name: "Add room" }).click();
await page.getByRole("menuitem", { name: "New room" }).click();
@@ -47,7 +47,7 @@ test.describe("Key backup reset from elsewhere", () => {
test("Key backup is disabled when reset from elsewhere", async ({
page,
mailpitClient,
mailhogClient,
request,
homeserver,
}, testInfo) => {
@@ -60,7 +60,7 @@ test.describe("Key backup reset from elsewhere", () => {
await page.goto("/#/login");
await page.getByRole("button", { name: "Continue" }).click();
await registerAccountMas(page, mailpitClient, testUsername, "alice@email.com", testPassword);
await registerAccountMas(page, mailhogClient, testUsername, "alice@email.com", testPassword);
await page.getByRole("button", { name: "Add room" }).click();
await page.getByRole("menuitem", { name: "New room" }).click();

View File

@@ -10,7 +10,6 @@ import { type Page } from "@playwright/test";
import { test, expect } from "../../element-web-test";
import { isDendrite } from "../../plugins/homeserver/dendrite";
import { completeCreateSecretStorageDialog } from "./utils.ts";
async function expectBackupVersionToBe(page: Page, version: string) {
await expect(page.locator(".mx_SecureBackupPanel_statusList tr:nth-child(5) td")).toHaveText(
@@ -36,7 +35,19 @@ test.describe("Backups", () => {
await expect(securityTab.getByRole("heading", { name: "Secure Backup" })).toBeVisible();
await securityTab.getByRole("button", { name: "Set up", exact: true }).click();
const securityKey = await completeCreateSecretStorageDialog(page);
const currentDialogLocator = page.locator(".mx_Dialog");
// It's the first time and secure storage is not set up, so it will create one
await expect(currentDialogLocator.getByRole("heading", { name: "Set up Secure Backup" })).toBeVisible();
await currentDialogLocator.getByRole("button", { name: "Continue", exact: true }).click();
await expect(currentDialogLocator.getByRole("heading", { name: "Save your Security Key" })).toBeVisible();
await currentDialogLocator.getByRole("button", { name: "Copy", exact: true }).click();
// copy the recovery key to use it later
const securityKey = await app.getClipboard();
await currentDialogLocator.getByRole("button", { name: "Continue", exact: true }).click();
await expect(currentDialogLocator.getByRole("heading", { name: "Secure Backup successful" })).toBeVisible();
await currentDialogLocator.getByRole("button", { name: "Done", exact: true }).click();
// Open the settings again
await app.settings.openUserSettings("Security & Privacy");
@@ -51,7 +62,6 @@ test.describe("Backups", () => {
await expectBackupVersionToBe(page, "1");
await securityTab.getByRole("button", { name: "Delete Backup", exact: true }).click();
const currentDialogLocator = page.locator(".mx_Dialog");
await expect(currentDialogLocator.getByRole("heading", { name: "Delete Backup" })).toBeVisible();
// Delete it
await currentDialogLocator.getByTestId("dialog-primary-button").click(); // Click "Delete Backup"

View File

@@ -8,16 +8,9 @@ Please see LICENSE files in the repository root for full details.
import type { Page } from "@playwright/test";
import { expect, test } from "../../element-web-test";
import {
autoJoin,
completeCreateSecretStorageDialog,
copyAndContinue,
createSharedRoomWithUser,
enableKeyBackup,
verify,
} from "./utils";
import { type Bot } from "../../pages/bot";
import { type ElementAppPage } from "../../pages/ElementAppPage";
import { autoJoin, copyAndContinue, createSharedRoomWithUser, enableKeyBackup, verify } from "./utils";
import { Bot } from "../../pages/bot";
import { ElementAppPage } from "../../pages/ElementAppPage";
import { isDendrite } from "../../plugins/homeserver/dendrite";
const checkDMRoom = async (page: Page) => {
@@ -118,7 +111,18 @@ test.describe("Cryptography", function () {
await app.settings.openUserSettings("Security & Privacy");
await page.getByRole("button", { name: "Set up Secure Backup" }).click();
await completeCreateSecretStorageDialog(page);
const dialog = page.locator(".mx_Dialog");
// Recovery key is selected by default
await dialog.getByRole("button", { name: "Continue" }).click();
await copyAndContinue(page);
// If the device is unverified, there should be a "Setting up keys" step; however, it
// can be quite quick, and playwright can miss it, so we can't test for it.
// Either way, we end up at a success dialog:
await expect(dialog.getByText("Secure Backup successful")).toBeVisible();
await dialog.getByRole("button", { name: "Done" }).click();
await expect(dialog.getByText("Secure Backup successful")).not.toBeVisible();
// Verify that the SSSS keys are in the account data stored in the server
await verifyKey(app, "master");

View File

@@ -6,13 +6,11 @@ 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 Locator, type Page } from "@playwright/test";
import { Locator, type Page } from "@playwright/test";
import { test, expect } from "../../element-web-test";
import { viewRoomSummaryByName } from "../right-panel/utils";
import { isDendrite } from "../../plugins/homeserver/dendrite";
import { completeCreateSecretStorageDialog, createBot, logIntoElement } from "./utils.ts";
import { type Client } from "../../pages/client.ts";
const ROOM_NAME = "Test room";
const NAME = "Alice";
@@ -46,7 +44,7 @@ test.use({
test.describe("Dehydration", () => {
test.skip(isDendrite, "does not yet support dehydration v2");
test("'Set up secure backup' creates dehydrated device", async ({ page, user, app }, workerInfo) => {
test("Create dehydrated device", async ({ page, user, app }, workerInfo) => {
// Create a backup (which will create SSSS, and dehydrated device)
const securityTab = await app.settings.openUserSettings("Security & Privacy");
@@ -55,7 +53,17 @@ test.describe("Dehydration", () => {
await expect(securityTab.getByText("Offline device enabled")).not.toBeVisible();
await securityTab.getByRole("button", { name: "Set up", exact: true }).click();
await completeCreateSecretStorageDialog(page);
const currentDialogLocator = page.locator(".mx_Dialog");
// It's the first time and secure storage is not set up, so it will create one
await expect(currentDialogLocator.getByRole("heading", { name: "Set up Secure Backup" })).toBeVisible();
await currentDialogLocator.getByRole("button", { name: "Continue", exact: true }).click();
await expect(currentDialogLocator.getByRole("heading", { name: "Save your Security Key" })).toBeVisible();
await currentDialogLocator.getByRole("button", { name: "Copy", exact: true }).click();
await currentDialogLocator.getByRole("button", { name: "Continue", exact: true }).click();
await expect(currentDialogLocator.getByRole("heading", { name: "Secure Backup successful" })).toBeVisible();
await currentDialogLocator.getByRole("button", { name: "Done", exact: true }).click();
// Open the settings again
await app.settings.openUserSettings("Security & Privacy");
@@ -88,49 +96,4 @@ test.describe("Dehydration", () => {
await expect(page.locator(".mx_UserInfo_devices").getByText("Offline device enabled")).toBeVisible();
await expect(page.locator(".mx_UserInfo_devices").getByText("Dehydrated device")).not.toBeVisible();
});
test("Reset recovery key during login re-creates dehydrated device", async ({
page,
homeserver,
app,
credentials,
}) => {
// Set up cross-signing and recovery
const { botClient } = await createBot(page, homeserver, credentials);
// ... and dehydration
await botClient.evaluate(async (client) => await client.getCrypto().startDehydration());
const initialDehydratedDeviceIds = await getDehydratedDeviceIds(botClient);
expect(initialDehydratedDeviceIds.length).toBe(1);
await botClient.evaluate(async (client) => client.stopClient());
// Log in our client
await logIntoElement(page, credentials);
// Oh no, we forgot our recovery key
await page.locator(".mx_AuthPage").getByRole("button", { name: "Reset all" }).click();
await page.locator(".mx_AuthPage").getByRole("button", { name: "Proceed with reset" }).click();
await completeCreateSecretStorageDialog(page, { accountPassword: credentials.password });
// There should be a brand new dehydrated device
const dehydratedDeviceIds = await getDehydratedDeviceIds(app.client);
expect(dehydratedDeviceIds.length).toBe(1);
expect(dehydratedDeviceIds[0]).not.toEqual(initialDehydratedDeviceIds[0]);
});
});
async function getDehydratedDeviceIds(client: Client): Promise<string[]> {
return await client.evaluate(async (client) => {
const userId = client.getUserId();
const devices = await client.getCrypto().getUserDeviceInfo([userId]);
return Array.from(
devices
.get(userId)
.values()
.filter((d) => d.dehydrated)
.map((d) => d.deviceId),
);
});
}

View File

@@ -20,7 +20,7 @@ import {
logIntoElement,
waitForVerificationRequest,
} from "./utils";
import { type Bot } from "../../pages/bot";
import { Bot } from "../../pages/bot";
test.describe("Device verification", { tag: "@no-webkit" }, () => {
let aliceBotClient: Bot;
@@ -68,8 +68,8 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
// Check that the current device is connected to key backup
// For now we don't check that the backup key is in cache because it's a bit flaky,
// as we need to wait for the secret gossiping to happen.
await checkDeviceIsConnectedKeyBackup(app, expectedBackupVersion, false);
// as we need to wait for the secret gossiping to happen and the settings dialog doesn't refresh automatically.
await checkDeviceIsConnectedKeyBackup(page, expectedBackupVersion, false);
});
test("Verify device with QR code during login", async ({ page, app, credentials, homeserver }) => {
@@ -112,7 +112,9 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
await checkDeviceIsCrossSigned(app);
// Check that the current device is connected to key backup
await checkDeviceIsConnectedKeyBackup(app, expectedBackupVersion, true);
// For now we don't check that the backup key is in cache because it's a bit flaky,
// as we need to wait for the secret gossiping to happen and the settings dialog doesn't refresh automatically.
await checkDeviceIsConnectedKeyBackup(page, expectedBackupVersion, false);
});
test("Verify device with Security Phrase during login", async ({ page, app, credentials, homeserver }) => {
@@ -133,7 +135,7 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
// Check that the current device is connected to key backup
// The backup decryption key should be in cache also, as we got it directly from the 4S
await checkDeviceIsConnectedKeyBackup(app, expectedBackupVersion, true);
await checkDeviceIsConnectedKeyBackup(page, expectedBackupVersion, true);
});
test("Verify device with Security Key during login", async ({ page, app, credentials, homeserver }) => {
@@ -156,7 +158,7 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
// Check that the current device is connected to key backup
// The backup decryption key should be in cache also, as we got it directly from the 4S
await checkDeviceIsConnectedKeyBackup(app, expectedBackupVersion, true);
await checkDeviceIsConnectedKeyBackup(page, expectedBackupVersion, true);
});
test("Handle incoming verification request with SAS", async ({ page, credentials, homeserver, toasts }) => {

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.
*/
import { type Locator } from "@playwright/test";
import { Locator } from "@playwright/test";
import { expect, test } from "../../element-web-test";
import {
@@ -19,7 +19,7 @@ import {
verify,
} from "./utils";
import { bootstrapCrossSigningForClient } from "../../pages/client.ts";
import { type ElementAppPage } from "../../pages/ElementAppPage.ts";
import { ElementAppPage } from "../../pages/ElementAppPage.ts";
test.describe("Cryptography", function () {
test.use({

View File

@@ -1,54 +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 GeneratedSecretStorageKey } from "matrix-js-sdk/src/crypto-api";
import { test, expect } from "../../element-web-test";
import { createBot, deleteCachedSecrets, logIntoElement } from "./utils";
test.describe("Key storage out of sync toast", () => {
let recoveryKey: GeneratedSecretStorageKey;
test.beforeEach(async ({ page, homeserver, credentials }) => {
const res = await createBot(page, homeserver, credentials);
recoveryKey = res.recoveryKey;
await logIntoElement(page, credentials, recoveryKey.encodedPrivateKey);
await deleteCachedSecrets(page);
// We won't be prompted for crypto setup unless we have an e2e room, so make one
await page.getByRole("button", { name: "Add room" }).click();
await page.getByRole("menuitem", { name: "New room" }).click();
await page.getByRole("textbox", { name: "Name" }).fill("Test room");
await page.getByRole("button", { name: "Create room" }).click();
});
test("should prompt for recovery key if 'enter recovery key' pressed", { tag: "@screenshot" }, async ({ page }) => {
// Need to wait for 2 to appear since playwright only evaluates 'first()' initially, so the waiting won't work
await expect(page.getByRole("alert")).toHaveCount(2);
await expect(page.getByRole("alert").first()).toMatchScreenshot("key-storage-out-of-sync-toast.png");
await page.getByRole("button", { name: "Enter recovery key" }).click();
await page.locator(".mx_Dialog").getByRole("button", { name: "use your Security Key" }).click();
await page.getByRole("textbox", { name: "Security key" }).fill(recoveryKey.encodedPrivateKey);
await page.getByRole("button", { name: "Continue" }).click();
await expect(page.getByRole("button", { name: "Enter recovery key" })).not.toBeVisible();
});
test("should open settings to reset flow if 'forgot recovery key' pressed", async ({ page, app, credentials }) => {
await expect(page.getByRole("button", { name: "Enter recovery key" })).toBeVisible();
await page.getByRole("button", { name: "Forgot recovery key?" }).click();
await expect(
page.getByRole("heading", { name: "Forgot your recovery key? Youll need to reset your identity." }),
).toBeVisible();
});
});

View File

@@ -11,7 +11,7 @@ import { type Preset, type Visibility } from "matrix-js-sdk/src/matrix";
import type { Page } from "@playwright/test";
import { test, expect } from "../../element-web-test";
import { doTwoWaySasVerification, awaitVerifier } from "./utils";
import { type Client } from "../../pages/client";
import { Client } from "../../pages/client";
test.describe("User verification", () => {
// note that there are other tests that check user verification works in `crypto.spec.ts`.

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.
*/
import { expect, type JSHandle, type Page } from "@playwright/test";
import { expect, JSHandle, type Page } from "@playwright/test";
import type { ICreateRoomOpts, MatrixClient } from "matrix-js-sdk/src/matrix";
import type {
@@ -18,9 +18,9 @@ import type {
Verifier,
VerifierEvent,
} from "matrix-js-sdk/src/crypto-api";
import { type Credentials, type HomeserverInstance } from "../../plugins/homeserver";
import { type Client } from "../../pages/client";
import { type ElementAppPage } from "../../pages/ElementAppPage";
import { Credentials, HomeserverInstance } from "../../plugins/homeserver";
import { Client } from "../../pages/client";
import { ElementAppPage } from "../../pages/ElementAppPage";
import { Bot } from "../../pages/bot";
/**
@@ -139,14 +139,14 @@ export async function checkDeviceIsCrossSigned(app: ElementAppPage): Promise<voi
* Check that the current device is connected to the expected key backup.
* Also checks that the decryption key is known and cached locally.
*
* @param app -` ElementAppPage` wrapper for the playwright `Page`.
* @param page - the page to check
* @param expectedBackupVersion - the version of the backup we expect to be connected to.
* @param checkBackupPrivateKeyInCache - whether to check that the backup decryption key is cached locally
* @param checkBackupKeyInCache - whether to check that the backup key is cached locally.
*/
export async function checkDeviceIsConnectedKeyBackup(
app: ElementAppPage,
page: Page,
expectedBackupVersion: string,
checkBackupPrivateKeyInCache: boolean,
checkBackupKeyInCache: boolean,
): Promise<void> {
// Sanity check the given backup version: if it's null, something went wrong earlier in the test.
if (!expectedBackupVersion) {
@@ -155,48 +155,23 @@ export async function checkDeviceIsConnectedKeyBackup(
);
}
const backupData = await app.client.evaluate(async (client: MatrixClient) => {
const crypto = client.getCrypto();
if (!crypto) return;
await page.getByRole("button", { name: "User menu" }).click();
await page.locator(".mx_UserMenu_contextMenu").getByRole("menuitem", { name: "Security & Privacy" }).click();
await expect(page.locator(".mx_Dialog").getByRole("button", { name: "Restore from Backup" })).toBeVisible();
const backupInfo = await crypto.getKeyBackupInfo();
const backupKeyIn4S = Boolean(await client.isKeyBackupKeyStored());
const backupPrivateKeyFromCache = await crypto.getSessionBackupPrivateKey();
const hasBackupPrivateKeyFromCache = Boolean(backupPrivateKeyFromCache);
const backupPrivateKeyWellFormed = backupPrivateKeyFromCache instanceof Uint8Array;
const activeBackupVersion = await crypto.getActiveSessionBackupVersion();
// expand the advanced section to see the active version in the reports
await page.locator(".mx_SecureBackupPanel_advanced").locator("..").click();
return {
backupInfo,
hasBackupPrivateKeyFromCache,
backupPrivateKeyWellFormed,
backupKeyIn4S,
activeBackupVersion,
};
});
if (!backupData) {
throw new Error("Crypto module is not available");
if (checkBackupKeyInCache) {
const cacheDecryptionKeyStatusElement = page.locator(".mx_SecureBackupPanel_statusList tr:nth-child(2) td");
await expect(cacheDecryptionKeyStatusElement).toHaveText("cached locally, well formed");
}
const { backupInfo, backupKeyIn4S, hasBackupPrivateKeyFromCache, backupPrivateKeyWellFormed, activeBackupVersion } =
backupData;
await expect(page.locator(".mx_SecureBackupPanel_statusList tr:nth-child(5) td")).toHaveText(
expectedBackupVersion + " (Algorithm: m.megolm_backup.v1.curve25519-aes-sha2)",
);
// We have a key backup
expect(backupInfo).toBeDefined();
// The key backup version is as expected
expect(backupInfo.version).toBe(expectedBackupVersion);
// The active backup version is as expected
expect(activeBackupVersion).toBe(expectedBackupVersion);
// The backup key is stored in 4S
expect(backupKeyIn4S).toBe(true);
if (checkBackupPrivateKeyInCache) {
// The backup key is available locally
expect(hasBackupPrivateKeyFromCache).toBe(true);
// The backup key is well-formed
expect(backupPrivateKeyWellFormed).toBe(true);
}
await expect(page.locator(".mx_SecureBackupPanel_statusList tr:nth-child(6) td")).toHaveText(expectedBackupVersion);
}
/**
@@ -214,11 +189,6 @@ export async function logIntoElement(page: Page, credentials: Credentials, secur
// if a securityKey was given, verify the new device
if (securityKey !== undefined) {
await page.locator(".mx_AuthPage").getByRole("button", { name: "Verify with Security Key" }).click();
const useSecurityKey = page.locator(".mx_Dialog").getByRole("button", { name: "use your Security Key" });
if (await useSecurityKey.isVisible()) {
await useSecurityKey.click();
}
// Fill in the security key
await page.locator(".mx_Dialog").locator('input[type="password"]').fill(securityKey);
await page.locator(".mx_Dialog_primary:not([disabled])", { hasText: "Continue" }).click();
@@ -246,19 +216,18 @@ export async function logOutOfElement(page: Page, discardKeys: boolean = false)
}
/**
* Open the encryption settings, and verify the current session using the security key.
* Open the security settings, and verify the current session using the security key.
*
* @param app - `ElementAppPage` wrapper for the playwright `Page`.
* @param securityKey - The security key (i.e., 4S key), set up during a previous session.
*/
export async function verifySession(app: ElementAppPage, securityKey: string) {
const settings = await app.settings.openUserSettings("Encryption");
await settings.getByRole("button", { name: "Verify this device" }).click();
const settings = await app.settings.openUserSettings("Security & Privacy");
await settings.getByRole("button", { name: "Verify this session" }).click();
await app.page.getByRole("button", { name: "Verify with Security Key" }).click();
await app.page.locator(".mx_Dialog").locator('input[type="password"]').fill(securityKey);
await app.page.getByRole("button", { name: "Continue", disabled: false }).click();
await app.page.getByRole("button", { name: "Done" }).click();
await app.settings.closeDialog();
}
/**
@@ -293,52 +262,19 @@ export async function doTwoWaySasVerification(page: Page, verifier: JSHandle<Ver
export async function enableKeyBackup(app: ElementAppPage): Promise<string> {
await app.settings.openUserSettings("Security & Privacy");
await app.page.getByRole("button", { name: "Set up Secure Backup" }).click();
const dialog = app.page.locator(".mx_Dialog");
// Recovery key is selected by default
await dialog.getByRole("button", { name: "Continue" }).click({ timeout: 60000 });
return await completeCreateSecretStorageDialog(app.page);
}
// copy the text ourselves
const securityKey = await dialog.locator(".mx_CreateSecretStorageDialog_recoveryKey code").textContent();
await copyAndContinue(app.page);
/**
* Go through the "Set up Secure Backup" dialog (aka the `CreateSecretStorageDialog`).
*
* Assumes the dialog is already open for some reason (see also {@link enableKeyBackup}).
*
* @param page - The playwright `Page` fixture.
* @param opts - Options object
* @param opts.accountPassword - The user's account password. If we are also resetting cross-signing, then we will need
* to upload the public cross-signing keys, which will cause the app to prompt for the password.
*
* @returns the new recovery key.
*/
export async function completeCreateSecretStorageDialog(
page: Page,
opts?: { accountPassword?: string },
): Promise<string> {
const currentDialogLocator = page.locator(".mx_Dialog");
await expect(dialog.getByText("Secure Backup successful")).toBeVisible();
await dialog.getByRole("button", { name: "Done" }).click();
await expect(dialog.getByText("Secure Backup successful")).not.toBeVisible();
await expect(currentDialogLocator.getByRole("heading", { name: "Set up Secure Backup" })).toBeVisible();
// "Generate a Security Key" is selected by default
await currentDialogLocator.getByRole("button", { name: "Continue", exact: true }).click();
await expect(currentDialogLocator.getByRole("heading", { name: "Save your Security Key" })).toBeVisible();
await currentDialogLocator.getByRole("button", { name: "Copy", exact: true }).click();
// copy the recovery key to use it later
const recoveryKey = await page.evaluate(() => navigator.clipboard.readText());
await currentDialogLocator.getByRole("button", { name: "Continue", exact: true }).click();
// If the device is unverified, there should be a "Setting up keys" step.
// If this is not the first time we are setting up cross-signing, the app will prompt for our password; otherwise
// the step is quite quick, and playwright can miss it, so we can't test for it.
if (opts && Object.hasOwn(opts, "accountPassword")) {
await expect(currentDialogLocator.getByRole("heading", { name: "Setting up keys" })).toBeVisible();
await page.getByPlaceholder("Password").fill(opts!.accountPassword);
await currentDialogLocator.getByRole("button", { name: "Continue" }).click();
}
// Either way, we end up at a success dialog:
await expect(currentDialogLocator.getByRole("heading", { name: "Secure Backup successful" })).toBeVisible();
await currentDialogLocator.getByRole("button", { name: "Done", exact: true }).click();
await expect(currentDialogLocator.getByText("Secure Backup successful")).not.toBeVisible();
return recoveryKey;
return securityKey;
}
/**

View File

@@ -5,10 +5,10 @@ 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 KeyBackupInfo } from "matrix-js-sdk/src/crypto-api";
import { APIRequestContext } from "playwright-core";
import { KeyBackupInfo } from "matrix-js-sdk/src/crypto-api";
import { type HomeserverInstance } from "../plugins/homeserver";
import { HomeserverInstance } from "../plugins/homeserver";
import { ClientServerApi } from "../plugins/utils/api.ts";
/**

View File

@@ -6,11 +6,11 @@ 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 Locator, type Page } from "@playwright/test";
import { Locator, Page } from "@playwright/test";
import type { EventType, IContent, ISendEventResponse, MsgType, Visibility } from "matrix-js-sdk/src/matrix";
import { expect, test } from "../../element-web-test";
import { type ElementAppPage } from "../../pages/ElementAppPage";
import { ElementAppPage } from "../../pages/ElementAppPage";
import { SettingLevel } from "../../../src/settings/SettingLevel";
import { isDendrite } from "../../plugins/homeserver/dendrite";

View File

@@ -10,7 +10,7 @@ import { expect, test as base } from "../../element-web-test";
import { selectHomeserver } from "../utils";
import { emailHomeserver } from "../../plugins/homeserver/synapse/emailHomeserver.ts";
import { isDendrite } from "../../plugins/homeserver/dendrite";
import { type Credentials } from "../../plugins/homeserver";
import { Credentials } from "../../plugins/homeserver";
const email = "user@nowhere.dummy";

View File

@@ -10,7 +10,7 @@ import { Bot } from "../../pages/bot";
import type { Locator, Page } from "@playwright/test";
import type { ElementAppPage } from "../../pages/ElementAppPage";
import { test, expect } from "../../element-web-test";
import { type Credentials } from "../../plugins/homeserver";
import { Credentials } from "../../plugins/homeserver";
import { isDendrite } from "../../plugins/homeserver/dendrite";
test.describe("Lazy Loading", () => {

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

View File

@@ -6,11 +6,11 @@ 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 { Page } from "playwright-core";
import { expect, test } from "../../element-web-test";
import { selectHomeserver } from "../utils";
import { type Credentials, type HomeserverInstance } from "../../plugins/homeserver";
import { Credentials, HomeserverInstance } from "../../plugins/homeserver";
import { consentHomeserver } from "../../plugins/homeserver/synapse/consentHomeserver.ts";
import { isDendrite } from "../../plugins/homeserver/dendrite";

View File

@@ -6,9 +6,9 @@ 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, expect, type TestInfo } from "@playwright/test";
import { Page, expect, TestInfo } from "@playwright/test";
import { type Credentials, type HomeserverInstance } from "../../plugins/homeserver";
import { Credentials, HomeserverInstance } from "../../plugins/homeserver";
/** Visit the login page, choose to log in with "OAuth test", register a new account, and redirect back to Element
*/

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

View File

@@ -6,14 +6,14 @@ 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 Page } from "@playwright/test";
import { API, Messages } from "mailhog";
import { Page } from "@playwright/test";
import { expect } from "../../element-web-test";
export async function registerAccountMas(
page: Page,
mailpit: MailpitClient,
mailhog: API,
username: string,
email: string,
password: string,
@@ -27,13 +27,13 @@ export async function registerAccountMas(
await page.getByRole("textbox", { name: "Confirm Password" }).fill(password);
await page.getByRole("button", { name: "Continue" }).click();
let code: string;
let messages: Messages;
await expect(async () => {
const messages = await mailpit.listMessages();
expect(messages.messages[0].To[0].Address).toEqual(email);
const text = await mailpit.renderMessageText(messages.messages[0].ID);
[, code] = text.match(/Your verification code to confirm this email address is: (\d{6})/);
messages = await mailhog.messages();
expect(messages.items).toHaveLength(1);
}).toPass();
expect(messages.items[0].to).toEqual(`${username} <${email}>`);
const [, code] = messages.items[0].text.match(/Your verification code to confirm this email address is: (\d{6})/);
await page.getByRole("textbox", { name: "6-digit code" }).fill(code);
await page.getByRole("button", { name: "Continue" }).click();

View File

@@ -19,7 +19,7 @@ test.describe("OIDC Native", { tag: ["@no-firefox", "@no-webkit"] }, () => {
context,
page,
homeserver,
mailpitClient,
mailhogClient,
mas,
}, testInfo) => {
await page.clock.install();
@@ -33,7 +33,7 @@ test.describe("OIDC Native", { tag: ["@no-firefox", "@no-webkit"] }, () => {
await page.getByRole("button", { name: "Continue" }).click();
const userId = `alice_${testInfo.testId}`;
await registerAccountMas(page, mailpitClient, userId, "alice@email.com", "Pa$sW0rD!");
await registerAccountMas(page, mailhogClient, userId, "alice@email.com", "Pa$sW0rD!");
// Eventually, we should end up at the home screen.
await expect(page).toHaveURL(/\/#\/home$/, { timeout: 10000 });

View File

@@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details.
*/
import { test as base, expect } from "../../element-web-test";
import { type Credentials } from "../../plugins/homeserver";
import { Credentials } from "../../plugins/homeserver";
import { isDendrite } from "../../plugins/homeserver/dendrite";
const test = base.extend<{

View File

@@ -6,12 +6,12 @@
* Please see LICENSE files in the repository root for full details.
*/
import { type Page } from "@playwright/test";
import { Page } from "@playwright/test";
import { test as base, expect } from "../../element-web-test";
import { type Client } from "../../pages/client";
import { type ElementAppPage } from "../../pages/ElementAppPage";
import { type Bot } from "../../pages/bot";
import { Client } from "../../pages/client";
import { ElementAppPage } from "../../pages/ElementAppPage";
import { Bot } from "../../pages/bot";
type RoomRef = { name: string; roomId: string };

View File

@@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details.
import { test, expect } from "../../element-web-test";
import type { Bot } from "../../pages/bot";
import type { Client } from "../../pages/client";
import { type ElementAppPage } from "../../pages/ElementAppPage";
import { ElementAppPage } from "../../pages/ElementAppPage";
test.describe("Poll history", () => {
type CreatePollOptions = {

View File

@@ -9,9 +9,9 @@ Please see LICENSE files in the repository root for full details.
import type { JSHandle, Page } from "@playwright/test";
import type { MatrixEvent, Room, IndexedDBStore, ReceiptType } from "matrix-js-sdk/src/matrix";
import { test as base, expect } from "../../element-web-test";
import { type Bot } from "../../pages/bot";
import { type Client } from "../../pages/client";
import { type ElementAppPage } from "../../pages/ElementAppPage";
import { Bot } from "../../pages/bot";
import { Client } from "../../pages/client";
import { ElementAppPage } from "../../pages/ElementAppPage";
type RoomRef = { name: string; roomId: string };
@@ -526,10 +526,9 @@ class Helpers {
await expect(threadPanel).toBeVisible();
await threadPanel.evaluate(($panel) => {
const $button = $panel.querySelector<HTMLElement>('[data-testid="base-card-back-button"]');
const title = $panel.querySelector<HTMLElement>(".mx_BaseCard_header_title")?.textContent;
// If the Threads back button is present then click it - the
// threads button can open either threads list or thread panel
if ($button && title !== "Threads") {
if ($button) {
$button.click();
}
});

View File

@@ -9,8 +9,8 @@ Please see LICENSE files in the repository root for full details.
import type { JSHandle } from "@playwright/test";
import type { MatrixEvent, ISendEventResponse, ReceiptType } from "matrix-js-sdk/src/matrix";
import { expect } from "../../element-web-test";
import { type ElementAppPage } from "../../pages/ElementAppPage";
import { type Bot } from "../../pages/bot";
import { ElementAppPage } from "../../pages/ElementAppPage";
import { Bot } from "../../pages/bot";
import { test } from ".";
import { isDendrite } from "../../plugins/homeserver/dendrite";

View File

@@ -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, mailhogClient, request, checkA11y }) => {
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")] };
@@ -51,11 +51,10 @@ test.describe("Email Registration", async () => {
await expect(page.getByText("An error was encountered when sending the email")).not.toBeVisible();
const messages = await mailpitClient.listMessages();
expect(messages.messages).toHaveLength(1);
expect(messages.messages[0].To[0].Address).toEqual("alice@email.com");
const text = await mailpitClient.renderMessageText(messages.messages[0].ID);
const [emailLink] = text.match(/http.+/);
const messages = await mailhogClient.messages();
expect(messages.items).toHaveLength(1);
expect(messages.items[0].to).toEqual("alice@email.com");
const [emailLink] = messages.items[0].text.match(/http.+/);
await request.get(emailLink); // "Click" the link in the email
await expect(page.getByText("Welcome alice")).toBeVisible();

View File

@@ -6,7 +6,7 @@
* Please see LICENSE files in the repository root for full details.
*/
import { type Page } from "@playwright/test";
import { Page } from "@playwright/test";
import { test as base, 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.
*/
import { type Download, type Page } from "@playwright/test";
import { Download, type Page } from "@playwright/test";
import { test, expect } from "../../element-web-test";
import { viewRoomSummaryByName } from "./utils";

View File

@@ -42,7 +42,7 @@ test.describe("Memberlist", () => {
await app.viewRoomByName(ROOM_NAME);
const memberlist = await app.toggleMemberlistPanel();
await expect(memberlist.locator(".mx_MemberTileView")).toHaveCount(4);
await expect(memberlist.getByText("Invited")).toHaveCount(1);
await expect(memberlist.getByText("(Invited)")).toHaveCount(1);
await expect(page.locator(".mx_MemberListView")).toMatchScreenshot("with-four-members.png");
});
});

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

View File

@@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details.
import { type Page, expect } from "@playwright/test";
import { type ElementAppPage } from "../../pages/ElementAppPage";
import { ElementAppPage } from "../../pages/ElementAppPage";
export async function viewRoomSummaryByName(page: Page, app: ElementAppPage, name: string): Promise<void> {
await app.viewRoomByName(name);

View File

@@ -6,10 +6,10 @@ 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/test";
import { Page } from "@playwright/test";
import { test, expect } from "../../element-web-test";
import { type ElementAppPage } from "../../pages/ElementAppPage";
import { ElementAppPage } from "../../pages/ElementAppPage";
test.describe("Room Header", () => {
test.use({

View File

@@ -6,9 +6,9 @@
* Please see LICENSE files in the repository root for full details.
*/
import { type Locator, type Page } from "@playwright/test";
import { Locator, Page } from "@playwright/test";
import { type ElementAppPage } from "../../../pages/ElementAppPage";
import { ElementAppPage } from "../../../pages/ElementAppPage";
import { test as base, expect } from "../../../element-web-test";
import { SettingLevel } from "../../../../src/settings/SettingLevel";
import { Layout } from "../../../../src/settings/enums/Layout";

View File

@@ -1,73 +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 { test, expect } from "./index";
import { checkDeviceIsCrossSigned } from "../../crypto/utils";
import { bootstrapCrossSigningForClient } from "../../../pages/client";
test.describe("Advanced section in Encryption tab", () => {
test.beforeEach(async ({ page, app, homeserver, credentials, util }) => {
const clientHandle = await app.client.prepareClient();
// Reset cross signing in order to have a verified session
await bootstrapCrossSigningForClient(clientHandle, credentials, true);
});
test("should show the encryption details", { tag: "@screenshot" }, async ({ page, app, util }) => {
await util.openEncryptionTab();
const section = util.getEncryptionDetailsSection();
const deviceId = await page.evaluate(() => window.mxMatrixClientPeg.get().getDeviceId());
await expect(section.getByText(deviceId)).toBeVisible();
await expect(section).toMatchScreenshot("encryption-details.png", {
mask: [section.getByTestId("deviceId"), section.getByTestId("sessionKey")],
});
});
test("should show the import room keys dialog", async ({ page, app, util }) => {
await util.openEncryptionTab();
const section = util.getEncryptionDetailsSection();
await section.getByRole("button", { name: "Import keys" }).click();
await expect(page.getByRole("heading", { name: "Import room keys" })).toBeVisible();
});
test("should show the export room keys dialog", async ({ page, app, util }) => {
await util.openEncryptionTab();
const section = util.getEncryptionDetailsSection();
await section.getByRole("button", { name: "Export keys" }).click();
await expect(page.getByRole("heading", { name: "Export room keys" })).toBeVisible();
});
test(
"should reset the cryptographic identity",
{ tag: "@screenshot" },
async ({ page, app, credentials, util }) => {
const tab = await util.openEncryptionTab();
const section = util.getEncryptionDetailsSection();
await section.getByRole("button", { name: "Reset cryptographic identity" }).click();
await expect(util.getEncryptionTabContent()).toMatchScreenshot("reset-cryptographic-identity.png");
await tab.getByRole("button", { name: "Continue" }).click();
// Fill password dialog and validate
const dialog = page.locator(".mx_InteractiveAuthDialog");
await dialog.getByRole("textbox", { name: "Password" }).fill(credentials.password);
await dialog.getByRole("button", { name: "Continue" }).click();
await expect(section.getByRole("button", { name: "Reset cryptographic identity" })).toBeVisible();
// After resetting the identity, the user should set up a new recovery key
await expect(
util.getEncryptionRecoverySection().getByRole("button", { name: "Set up recovery" }),
).toBeVisible();
await checkDeviceIsCrossSigned(app);
},
);
});

View File

@@ -1,114 +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 GeneratedSecretStorageKey } from "matrix-js-sdk/src/crypto-api";
import { test, expect } from ".";
import {
checkDeviceIsConnectedKeyBackup,
checkDeviceIsCrossSigned,
createBot,
deleteCachedSecrets,
verifySession,
} from "../../crypto/utils";
test.describe("Encryption tab", () => {
test.use({
displayName: "Alice",
});
let recoveryKey: GeneratedSecretStorageKey;
let expectedBackupVersion: string;
test.beforeEach(async ({ page, homeserver, credentials }) => {
// The bot bootstraps cross-signing, creates a key backup and sets up a recovery key
const res = await createBot(page, homeserver, credentials);
recoveryKey = res.recoveryKey;
expectedBackupVersion = res.expectedBackupVersion;
});
test(
"should show a 'Verify this device' button if the device is unverified",
{ tag: "@screenshot" },
async ({ page, app, util }) => {
const dialog = await util.openEncryptionTab();
const content = util.getEncryptionTabContent();
// The user's device is in an unverified state, therefore the only option available to them here is to verify it
const verifyButton = dialog.getByRole("button", { name: "Verify this device" });
await expect(verifyButton).toBeVisible();
await expect(content).toMatchScreenshot("verify-device-encryption-tab.png");
await verifyButton.click();
await util.verifyDevice(recoveryKey);
await expect(content).toMatchScreenshot("default-tab.png", {
mask: [content.getByTestId("deviceId"), content.getByTestId("sessionKey")],
});
// Check that our device is now cross-signed
await checkDeviceIsCrossSigned(app);
// Check that the current device is connected to key backup
// The backup decryption key should be in cache also, as we got it directly from the 4S
await checkDeviceIsConnectedKeyBackup(app, expectedBackupVersion, true);
},
);
// Test what happens if the cross-signing secrets are in secret storage but are not cached in the local DB.
//
// This can happen if we verified another device and secret-gossiping failed, or the other device itself lacked the secrets.
// We simulate this case by deleting the cached secrets in the indexedDB.
test(
"should prompt to enter the recovery key when the secrets are not cached locally",
{ tag: "@screenshot" },
async ({ page, app, util }) => {
await verifySession(app, "new passphrase");
// We need to delete the cached secrets
await deleteCachedSecrets(page);
await util.openEncryptionTab();
// We ask the user to enter the recovery key
const dialog = util.getEncryptionTabContent();
const enterKeyButton = dialog.getByRole("button", { name: "Enter recovery key" });
await expect(enterKeyButton).toBeVisible();
await expect(dialog).toMatchScreenshot("out-of-sync-recovery.png");
await enterKeyButton.click();
// Fill the recovery key
await util.enterRecoveryKey(recoveryKey);
await expect(dialog).toMatchScreenshot("default-tab.png", {
mask: [dialog.getByTestId("deviceId"), dialog.getByTestId("sessionKey")],
});
// Check that our device is now cross-signed
await checkDeviceIsCrossSigned(app);
// Check that the current device is connected to key backup
// The backup decryption key should be in cache also, as we got it directly from the 4S
await checkDeviceIsConnectedKeyBackup(app, expectedBackupVersion, true);
},
);
test("should display the reset identity panel when the user clicks on 'Forgot recovery key?'", async ({
page,
app,
util,
}) => {
await verifySession(app, "new passphrase");
// We need to delete the cached secrets
await deleteCachedSecrets(page);
// The "Key storage is out sync" section is displayed and the user click on the "Forgot recovery key?" button
await util.openEncryptionTab();
const dialog = util.getEncryptionTabContent();
await dialog.getByRole("button", { name: "Forgot recovery key?" }).click();
// The user is prompted to reset their identity
await expect(dialog.getByText("Forgot your recovery key? Youll need to reset your identity.")).toBeVisible();
});
});

View File

@@ -5,10 +5,10 @@
* Please see LICENSE files in the repository root for full details.
*/
import { type Page } from "@playwright/test";
import { type GeneratedSecretStorageKey } from "matrix-js-sdk/src/crypto-api";
import { Page } from "@playwright/test";
import { GeneratedSecretStorageKey } from "matrix-js-sdk/src/crypto-api";
import { type ElementAppPage } from "../../../pages/ElementAppPage";
import { ElementAppPage } from "../../../pages/ElementAppPage";
import { test as base, expect } from "../../../element-web-test";
export { expect };
@@ -18,8 +18,6 @@ export { expect };
export const test = base.extend<{
util: Helpers;
}>({
displayName: "Alice",
util: async ({ page, app, bot }, use) => {
await use(new Helpers(page, app));
},
@@ -69,20 +67,6 @@ class Helpers {
return this.page.getByTestId("encryptionTab");
}
/**
* Get the recovery section
*/
getEncryptionRecoverySection() {
return this.page.getByTestId("recoveryPanel");
}
/**
* Get the encryption details section
*/
getEncryptionDetailsSection() {
return this.page.getByTestId("encryptionDetails");
}
/**
* Set the default key id of the secret storage to `null`
*/
@@ -108,6 +92,6 @@ class Helpers {
const clipboardContent = await this.app.getClipboard();
await dialog.getByRole("textbox").fill(clipboardContent);
await dialog.getByRole("button", { name: confirmButtonLabel }).click();
await expect(this.getEncryptionRecoverySection()).toMatchScreenshot("default-recovery.png");
await expect(dialog).toMatchScreenshot("default-recovery.png");
}
}

View File

@@ -5,17 +5,50 @@
* Please see LICENSE files in the repository root for full details.
*/
import { GeneratedSecretStorageKey } from "matrix-js-sdk/src/crypto-api";
import { test, expect } from ".";
import { checkDeviceIsConnectedKeyBackup, createBot, verifySession } from "../../crypto/utils";
import {
checkDeviceIsConnectedKeyBackup,
checkDeviceIsCrossSigned,
createBot,
deleteCachedSecrets,
verifySession,
} from "../../crypto/utils";
test.describe("Recovery section in Encryption tab", () => {
test.use({
displayName: "Alice",
});
let recoveryKey: GeneratedSecretStorageKey;
let expectedBackupVersion: string;
test.beforeEach(async ({ page, homeserver, credentials }) => {
// The bot bootstraps cross-signing, creates a key backup and sets up a recovery key
await createBot(page, homeserver, credentials);
const res = await createBot(page, homeserver, credentials);
recoveryKey = res.recoveryKey;
expectedBackupVersion = res.expectedBackupVersion;
});
test("should verify the device", { tag: "@screenshot" }, async ({ page, app, util }) => {
const dialog = await util.openEncryptionTab();
// The user's device is in an unverified state, therefore the only option available to them here is to verify it
const verifyButton = dialog.getByRole("button", { name: "Verify this device" });
await expect(verifyButton).toBeVisible();
await expect(util.getEncryptionTabContent()).toMatchScreenshot("verify-device-encryption-tab.png");
await verifyButton.click();
await util.verifyDevice(recoveryKey);
await expect(util.getEncryptionTabContent()).toMatchScreenshot("default-recovery.png");
// Check that our device is now cross-signed
await checkDeviceIsCrossSigned(app);
// Check that the current device is connected to key backup
// The backup decryption key should be in cache also, as we got it directly from the 4S
await app.closeDialog();
await checkDeviceIsConnectedKeyBackup(page, expectedBackupVersion, true);
});
test(
@@ -28,7 +61,7 @@ test.describe("Recovery section in Encryption tab", () => {
// The user can only change the recovery key
const changeButton = dialog.getByRole("button", { name: "Change recovery key" });
await expect(changeButton).toBeVisible();
await expect(util.getEncryptionRecoverySection()).toMatchScreenshot("default-recovery.png");
await expect(util.getEncryptionTabContent()).toMatchScreenshot("default-recovery.png");
await changeButton.click();
// Display the new recovery key and click on the copy button
@@ -56,7 +89,7 @@ test.describe("Recovery section in Encryption tab", () => {
const dialog = await util.openEncryptionTab();
const setupButton = dialog.getByRole("button", { name: "Set up recovery" });
await expect(setupButton).toBeVisible();
await expect(util.getEncryptionRecoverySection()).toMatchScreenshot("set-up-recovery.png");
await expect(util.getEncryptionTabContent()).toMatchScreenshot("set-up-recovery.png");
await setupButton.click();
// Display an informative panel about the recovery key
@@ -82,7 +115,42 @@ test.describe("Recovery section in Encryption tab", () => {
// The recovery key is now set up and the user can change it
await expect(dialog.getByRole("button", { name: "Change recovery key" })).toBeVisible();
await app.closeDialog();
// Check that the current device is connected to key backup and the backup version is the expected one
await checkDeviceIsConnectedKeyBackup(app, "1", true);
await checkDeviceIsConnectedKeyBackup(page, "1", true);
});
// Test what happens if the cross-signing secrets are in secret storage but are not cached in the local DB.
//
// This can happen if we verified another device and secret-gossiping failed, or the other device itself lacked the secrets.
// We simulate this case by deleting the cached secrets in the indexedDB.
test(
"should enter the recovery key when the secrets are not cached",
{ tag: "@screenshot" },
async ({ page, app, util }) => {
await verifySession(app, "new passphrase");
// We need to delete the cached secrets
await deleteCachedSecrets(page);
await util.openEncryptionTab();
// We ask the user to enter the recovery key
const dialog = util.getEncryptionTabContent();
const enterKeyButton = dialog.getByRole("button", { name: "Enter recovery key" });
await expect(enterKeyButton).toBeVisible();
await expect(dialog).toMatchScreenshot("out-of-sync-recovery.png");
await enterKeyButton.click();
// Fill the recovery key
await util.enterRecoveryKey(recoveryKey);
await expect(dialog).toMatchScreenshot("default-recovery.png");
// Check that our device is now cross-signed
await checkDeviceIsCrossSigned(app);
// Check that the current device is connected to key backup
// The backup decryption key should be in cache also, as we got it directly from the 4S
await app.closeDialog();
await checkDeviceIsConnectedKeyBackup(page, expectedBackupVersion, true);
},
);
});

View File

@@ -6,7 +6,7 @@
* Please see LICENSE files in the repository root for full details.
*/
import { type Locator } from "@playwright/test";
import { Locator } from "@playwright/test";
import { test, expect } from "../../element-web-test";

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.
*/
import { type Page, type Request } from "@playwright/test";
import { GenericContainer, type StartedTestContainer, Wait } from "testcontainers";
import { Page, Request } from "@playwright/test";
import { GenericContainer, StartedTestContainer, Wait } from "testcontainers";
import { test as base, expect } from "../../element-web-test";
import type { ElementAppPage } from "../../pages/ElementAppPage";
@@ -69,6 +69,11 @@ const test = base.extend<{
});
test.describe("Sliding Sync", () => {
test.skip(
({ homeserverType }) => homeserverType === "pinecone",
"due to a bug in Pinecone https://github.com/element-hq/dendrite/issues/3490",
);
const checkOrder = async (wantOrder: string[], page: Page) => {
await expect(page.getByRole("group", { name: "Rooms" }).locator(".mx_RoomTile_title")).toHaveText(wantOrder);
};

View File

@@ -9,7 +9,7 @@ Please see LICENSE files in the repository root for full details.
import type { Locator, Page } from "@playwright/test";
import { test, expect } from "../../element-web-test";
import type { Preset, ICreateRoomOpts } from "matrix-js-sdk/src/matrix";
import { type ElementAppPage } from "../../pages/ElementAppPage";
import { ElementAppPage } from "../../pages/ElementAppPage";
import { isDendrite } from "../../plugins/homeserver/dendrite";
async function openSpaceCreateMenu(page: Page): Promise<Locator> {

View File

@@ -6,14 +6,14 @@ 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 JSHandle, type Locator, type Page } from "@playwright/test";
import { JSHandle, Locator, Page } from "@playwright/test";
import type { MatrixEvent, IContent, Room } from "matrix-js-sdk/src/matrix";
import { test as base, expect } from "../../../element-web-test";
import { type Bot } from "../../../pages/bot";
import { type Client } from "../../../pages/client";
import { type ElementAppPage } from "../../../pages/ElementAppPage";
import { type Credentials } from "../../../plugins/homeserver";
import { Bot } from "../../../pages/bot";
import { Client } from "../../../pages/client";
import { ElementAppPage } from "../../../pages/ElementAppPage";
import { Credentials } from "../../../plugins/homeserver";
type RoomRef = { name: string; roomId: string };

View File

@@ -13,8 +13,8 @@ import type { ISendEventResponse, EventType, MsgType } from "matrix-js-sdk/src/m
import { test, expect } from "../../element-web-test";
import { SettingLevel } from "../../../src/settings/SettingLevel";
import { Layout } from "../../../src/settings/enums/Layout";
import { type Client } from "../../pages/client";
import { type ElementAppPage } from "../../pages/ElementAppPage";
import { Client } from "../../pages/client";
import { ElementAppPage } from "../../pages/ElementAppPage";
import { Bot } from "../../pages/bot";
// The avatar size used in the timeline

View File

@@ -12,7 +12,7 @@ import { uniqueId } from "lodash";
import { expect, type Page } from "@playwright/test";
import type { ClientEvent, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
import { type Client } from "../pages/client";
import { Client } from "../pages/client";
/**
* Resolves when room state matches predicate.

View File

@@ -10,8 +10,8 @@ import * as fs from "node:fs";
import type { Page } from "@playwright/test";
import { test, expect } from "../../element-web-test";
import { type ElementAppPage } from "../../pages/ElementAppPage";
import { type Credentials } from "../../plugins/homeserver";
import { ElementAppPage } from "../../pages/ElementAppPage";
import { Credentials } from "../../plugins/homeserver";
import type { UserWidget } from "../../../src/utils/WidgetUtils-types.ts";
const STICKER_PICKER_WIDGET_ID = "fake-sticker-picker";

View File

@@ -11,7 +11,7 @@ Please see LICENSE files in the repository root for full details.
import type { IWidget } from "matrix-widget-api/src/interfaces/IWidget";
import type { MatrixEvent, RoomStateEvent } from "matrix-js-sdk/src/matrix";
import { test, expect } from "../../element-web-test";
import { type Client } from "../../pages/client";
import { Client } from "../../pages/client";
const DEMO_WIDGET_ID = "demo-widget-id";
const DEMO_WIDGET_NAME = "Demo Widget";

View File

@@ -8,12 +8,12 @@ Please see LICENSE files in the repository root for full details.
import {
expect as baseExpect,
type Locator,
type Page,
type ExpectMatcherState,
type ElementHandle,
type PlaywrightTestArgs,
type Fixtures as _Fixtures,
Locator,
Page,
ExpectMatcherState,
ElementHandle,
PlaywrightTestArgs,
Fixtures as _Fixtures,
} from "@playwright/test";
import { sanitizeForFilePath } from "playwright-core/lib/utils";
import AxeBuilder from "@axe-core/playwright";
@@ -21,13 +21,13 @@ import _ from "lodash";
import { extname } from "node:path";
import type { IConfigOptions } from "../src/IConfigOptions";
import { type Credentials } from "./plugins/homeserver";
import { Credentials } from "./plugins/homeserver";
import { ElementAppPage } from "./pages/ElementAppPage";
import { Crypto } from "./pages/crypto";
import { Toasts } from "./pages/toasts";
import { Bot, type CreateBotOpts } from "./pages/bot";
import { Bot, CreateBotOpts } from "./pages/bot";
import { Webserver } from "./plugins/webserver";
import { type Options, type Services, test as base } from "./services.ts";
import { Options, Services, test as base } from "./services.ts";
// Enable experimental service worker support
// See https://playwright.dev/docs/service-workers-experimental#how-to-enable

View File

@@ -5,8 +5,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 BrowserContext, type Page, type TestInfo } from "@playwright/test";
import { type Readable } from "stream";
import { BrowserContext, Page, TestInfo } from "@playwright/test";
import { Readable } from "stream";
import stripAnsi from "strip-ansi";
export class Logger {

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.
*/
import { type JSHandle, type Page } from "@playwright/test";
import { JSHandle, Page } from "@playwright/test";
import { uniqueId } from "lodash";
import { type MatrixClient } from "matrix-js-sdk/src/matrix";
@@ -192,6 +192,7 @@ export class Bot extends Client {
await clientHandle.evaluate(async (cli) => {
await cli.initRustCrypto({ useIndexedDB: false });
cli.setGlobalErrorOnUnknownDevices(false);
await cli.startClient();
});

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.
*/
import { type JSHandle, type Page } from "@playwright/test";
import { type PageFunctionOn } from "playwright-core/types/structs";
import { JSHandle, Page } from "@playwright/test";
import { PageFunctionOn } from "playwright-core/types/structs";
import { Network } from "./network";
import type {
@@ -25,10 +25,9 @@ import type {
StateEvents,
TimelineEvents,
AccountDataEvents,
EmptyObject,
} from "matrix-js-sdk/src/matrix";
import type { RoomMessageEventContent } from "matrix-js-sdk/src/types";
import { type Credentials } from "../plugins/homeserver";
import { Credentials } from "../plugins/homeserver";
export class Client {
public network: Network;
@@ -364,7 +363,7 @@ export class Client {
event: JSHandle<MatrixEvent>,
receiptType?: ReceiptType,
unthreaded?: boolean,
): Promise<EmptyObject> {
): Promise<{}> {
const client = await this.prepareClient();
return client.evaluate(
(client, { event, receiptType, unthreaded }) => {
@@ -387,7 +386,7 @@ export class Client {
* @return {Promise} Resolves: {} an empty object.
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
public async setDisplayName(name: string): Promise<EmptyObject> {
public async setDisplayName(name: string): Promise<{}> {
const client = await this.prepareClient();
return client.evaluate(async (cli: MatrixClient, name) => cli.setDisplayName(name), name);
}
@@ -398,7 +397,7 @@ export class Client {
* @return {Promise} Resolves: {} an empty object.
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
public async setAvatarUrl(url: string): Promise<EmptyObject> {
public async setAvatarUrl(url: string): Promise<{}> {
const client = await this.prepareClient();
return client.evaluate(async (cli: MatrixClient, url) => cli.setAvatarUrl(url), url);
}

View File

@@ -6,9 +6,9 @@ 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 APIRequestContext, type Page, expect } from "@playwright/test";
import { APIRequestContext, Page, expect } from "@playwright/test";
import { type HomeserverInstance } from "../plugins/homeserver";
import { HomeserverInstance } from "../plugins/homeserver";
export class Crypto {
public constructor(

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.
*/
import { type Locator, type Page } from "@playwright/test";
import { Locator, Page } from "@playwright/test";
import type { SettingLevel } from "../../src/settings/SettingLevel";

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.
*/
import { type Page, expect, type Locator } from "@playwright/test";
import { Page, expect, Locator } from "@playwright/test";
export class Toasts {
public constructor(private readonly page: Page) {}

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.
*/
import { type Options } from "../../../services.ts";
import { Options } from "../../../services.ts";
export const isDendrite = ({ homeserverType }: Options): boolean => {
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.
*/
import { type ClientServerApi } from "../utils/api.ts";
import { ClientServerApi } from "../utils/api.ts";
export interface HomeserverInstance {
readonly baseUrl: string;

View File

@@ -6,11 +6,11 @@ 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 Fixtures } from "../../../element-web-test.ts";
import { Fixtures } from "../../../element-web-test.ts";
export const consentHomeserver: Fixtures = {
_homeserver: [
async ({ _homeserver: container, mailpit }, use) => {
async ({ _homeserver: container, mailhog }, use) => {
container
.withCopyDirectoriesToContainer([
{ source: "playwright/plugins/homeserver/synapse/res", target: "/data/res" },
@@ -18,7 +18,7 @@ export const consentHomeserver: Fixtures = {
.withConfig({
email: {
enable_notifs: false,
smtp_host: "mailpit",
smtp_host: "mailhog",
smtp_port: 1025,
smtp_user: "username",
smtp_pass: "password",

View File

@@ -6,17 +6,17 @@ 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 Fixtures } from "../../../element-web-test.ts";
import { Fixtures } from "../../../element-web-test.ts";
export const emailHomeserver: Fixtures = {
_homeserver: [
async ({ _homeserver: container, mailpit }, use) => {
async ({ _homeserver: container, mailhog }, use) => {
container.withConfig({
enable_registration_without_verification: undefined,
disable_msisdn_registration: undefined,
registrations_require_3pid: ["email"],
email: {
smtp_host: "mailpit",
smtp_host: "mailhog",
smtp_port: 1025,
notif_from: "Your Friendly %(app)s homeserver <noreply@example.com>",
app_name: "my_branded_matrix_server",

View File

@@ -9,7 +9,7 @@ Please see LICENSE files in the repository root for full details.
import { TestContainers } from "testcontainers";
import { OAuthServer } from "../../oauth_server";
import { type Fixtures } from "../../../element-web-test.ts";
import { Fixtures } from "../../../element-web-test.ts";
export const legacyOAuthHomeserver: Fixtures = {
oAuthServer: [

View File

@@ -7,11 +7,11 @@ Please see LICENSE files in the repository root for full details.
*/
import { MatrixAuthenticationServiceContainer } from "../../../testcontainers/mas.ts";
import { type Fixtures } from "../../../element-web-test.ts";
import { Fixtures } from "../../../element-web-test.ts";
export const masHomeserver: Fixtures = {
mas: [
async ({ _homeserver: homeserver, logger, network, postgres, mailpit }, use) => {
async ({ _homeserver: homeserver, logger, network, postgres, mailhog }, use) => {
const config = {
clients: [
{

View File

@@ -5,7 +5,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 Fixtures } from "../../../element-web-test.ts";
import { Fixtures } from "../../../element-web-test.ts";
export const uiaLongSessionTimeoutHomeserver: Fixtures = {
synapseConfig: [

View File

@@ -8,8 +8,8 @@ Please see LICENSE files in the repository root for full details.
import http from "http";
import express from "express";
import { type AddressInfo } from "net";
import { type TestInfo } from "@playwright/test";
import { AddressInfo } from "net";
import { TestInfo } from "@playwright/test";
import { randB64Bytes } from "../utils/rand.ts";

View File

@@ -5,9 +5,9 @@ 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 APIRequestContext } from "@playwright/test";
import { APIRequestContext } from "@playwright/test";
import { type Credentials } from "../homeserver";
import { Credentials } from "../homeserver";
export type Verb = "GET" | "POST" | "PUT" | "DELETE";

View File

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

View File

@@ -6,21 +6,21 @@ 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 mailhog from "mailhog";
import { Network, StartedNetwork } from "testcontainers";
import { PostgreSqlContainer, StartedPostgreSqlContainer } from "@testcontainers/postgresql";
import { type SynapseConfig, SynapseContainer } from "./testcontainers/synapse.ts";
import { 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 { StartedMatrixAuthenticationServiceContainer } from "./testcontainers/mas.ts";
import { HomeserverContainer, StartedHomeserverContainer } from "./testcontainers/HomeserverContainer.ts";
import { MailhogContainer, StartedMailhogContainer } from "./testcontainers/mailhog.ts";
import { OAuthServer } from "./plugins/oauth_server";
import { DendriteContainer, PineconeContainer } from "./testcontainers/dendrite.ts";
import { type HomeserverType } from "./plugins/homeserver";
import { HomeserverType } from "./plugins/homeserver";
export interface TestFixtures {
mailpitClient: MailpitClient;
mailhogClient: mailhog.API;
}
export interface Services {
@@ -28,7 +28,7 @@ export interface Services {
network: StartedNetwork;
postgres: StartedPostgreSqlContainer;
mailpit: StartedMailhogContainer;
mailhog: StartedMailhogContainer;
synapseConfig: SynapseConfig;
_homeserver: HomeserverContainer<any>;
@@ -90,20 +90,20 @@ export const test = base.extend<TestFixtures, Services & Options>({
{ scope: "worker" },
],
mailpit: [
mailhog: [
async ({ logger, network }, use) => {
const container = await new MailhogContainer()
.withNetwork(network)
.withNetworkAliases("mailpit")
.withLogConsumer(logger.getConsumer("mailpit"))
.withNetworkAliases("mailhog")
.withLogConsumer(logger.getConsumer("mailhog"))
.start();
await use(container);
await container.stop();
},
{ scope: "worker" },
],
mailpitClient: async ({ mailpit: container }, use) => {
await container.client.deleteMessages();
mailhogClient: async ({ mailhog: container }, use) => {
await container.client.deleteAll();
await use(container.client);
},

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -5,11 +5,11 @@ 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 AbstractStartedContainer, type GenericContainer } from "testcontainers";
import { type APIRequestContext, type TestInfo } from "@playwright/test";
import { AbstractStartedContainer, GenericContainer } from "testcontainers";
import { APIRequestContext, TestInfo } from "@playwright/test";
import { type HomeserverInstance } from "../plugins/homeserver";
import { type StartedMatrixAuthenticationServiceContainer } from "./mas.ts";
import { HomeserverInstance } from "../plugins/homeserver";
import { StartedMatrixAuthenticationServiceContainer } from "./mas.ts";
export interface HomeserverContainer<Config> extends GenericContainer {
withConfigField(key: string, value: any): this;

View File

@@ -12,8 +12,8 @@ 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 { HomeserverContainer } from "./HomeserverContainer.ts";
import { StartedMatrixAuthenticationServiceContainer } from "./mas.ts";
const DEFAULT_CONFIG = {
version: 2,

View File

@@ -5,17 +5,14 @@ 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 { AbstractStartedContainer, GenericContainer, type StartedTestContainer, Wait } from "testcontainers";
import { MailpitClient } from "mailpit-api";
import { AbstractStartedContainer, GenericContainer, StartedTestContainer, Wait } from "testcontainers";
import mailhog from "mailhog";
export class MailhogContainer extends GenericContainer {
constructor() {
super("axllent/mailpit:latest");
super("mailhog/mailhog:latest");
this.withExposedPorts(8025).withWaitStrategy(Wait.forListeningPorts()).withEnvironment({
MP_SMTP_AUTH_ALLOW_INSECURE: "true",
MP_SMTP_AUTH_ACCEPT_ANY: "true",
});
this.withExposedPorts(8025).withWaitStrategy(Wait.forListeningPorts());
}
public override async start(): Promise<StartedMailhogContainer> {
@@ -24,10 +21,10 @@ export class MailhogContainer extends GenericContainer {
}
export class StartedMailhogContainer extends AbstractStartedContainer {
public readonly client: MailpitClient;
public readonly client: mailhog.API;
constructor(container: StartedTestContainer) {
super(container);
this.client = new MailpitClient(`http://${container.getHost()}:${container.getMappedPort(8025)}`);
this.client = mailhog({ host: container.getHost(), port: container.getMappedPort(8025) });
}
}

View File

@@ -5,19 +5,13 @@ 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 {
AbstractStartedContainer,
GenericContainer,
type StartedTestContainer,
Wait,
type ExecResult,
} from "testcontainers";
import { type StartedPostgreSqlContainer } from "@testcontainers/postgresql";
import { AbstractStartedContainer, GenericContainer, StartedTestContainer, Wait, ExecResult } from "testcontainers";
import { 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";
import { Credentials } from "../plugins/homeserver";
const DEFAULT_CONFIG = {
http: {
@@ -98,7 +92,7 @@ const DEFAULT_CONFIG = {
reply_to: '"Authentication Service" <root@localhost>',
transport: "smtp",
mode: "plain",
hostname: "mailpit",
hostname: "mailhog",
port: 1025,
username: "username",
password: "password",

View File

@@ -5,27 +5,21 @@ 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 {
AbstractStartedContainer,
GenericContainer,
type RestartOptions,
type StartedTestContainer,
Wait,
} from "testcontainers";
import { type APIRequestContext, type TestInfo } from "@playwright/test";
import { AbstractStartedContainer, GenericContainer, RestartOptions, StartedTestContainer, Wait } from "testcontainers";
import { APIRequestContext, 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 { 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 { HomeserverContainer, StartedHomeserverContainer } from "./HomeserverContainer.ts";
import { StartedMatrixAuthenticationServiceContainer } from "./mas.ts";
import { Api, ClientServerApi, Verb } from "../plugins/utils/api.ts";
const TAG = "develop@sha256:73803f5af0ff70926d54cccabdb4a6e3abf6a7f96297955e72a2652ceb5c00c3";
const TAG = "develop@sha256:e48308d68dec00af6ce43a05785d475de21a37bc2afaabb440d3a575bcc3d57d";
const DEFAULT_CONFIG = {
server_name: "localhost",

View File

@@ -134,7 +134,6 @@
@import "./views/dialogs/_ConfirmUserActionDialog.pcss";
@import "./views/dialogs/_CreateRoomDialog.pcss";
@import "./views/dialogs/_CreateSubspaceDialog.pcss";
@import "./views/dialogs/_Crypto.pcss";
@import "./views/dialogs/_DeactivateAccountDialog.pcss";
@import "./views/dialogs/_DevtoolsDialog.pcss";
@import "./views/dialogs/_ExportDialog.pcss";
@@ -284,7 +283,6 @@
@import "./views/rooms/_EventTile.pcss";
@import "./views/rooms/_HistoryTile.pcss";
@import "./views/rooms/_IRCLayout.pcss";
@import "./views/rooms/_InvitedIconView.pcss";
@import "./views/rooms/_JumpToBottomButton.pcss";
@import "./views/rooms/_LinkPreviewGroup.pcss";
@import "./views/rooms/_LinkPreviewWidget.pcss";
@@ -355,11 +353,8 @@
@import "./views/settings/_ThemeChoicePanel.pcss";
@import "./views/settings/_UpdateCheckButton.pcss";
@import "./views/settings/_UserProfileSettings.pcss";
@import "./views/settings/encryption/_AdvancedPanel.pcss";
@import "./views/settings/encryption/_ChangeRecoveryKey.pcss";
@import "./views/settings/encryption/_EncryptionCard.pcss";
@import "./views/settings/encryption/_RecoveryPanelOutOfSync.pcss";
@import "./views/settings/encryption/_ResetIdentityPanel.pcss";
@import "./views/settings/tabs/_SettingsBanner.pcss";
@import "./views/settings/tabs/_SettingsIndent.pcss";
@import "./views/settings/tabs/_SettingsSection.pcss";

View File

@@ -37,6 +37,27 @@ Please see LICENSE files in the repository root for full details.
line-height: $font-24px;
margin-left: 10px;
}
.mx_UserMenu_dndBadge {
position: absolute;
bottom: -2px;
right: -7px;
width: 16px;
height: 16px;
border-radius: 50%;
&::before {
content: "";
width: 16px;
height: 16px;
position: absolute;
mask-position: center;
mask-size: contain;
mask-repeat: no-repeat;
background-color: $alert;
mask-image: url("$(res)/img/element-icons/roomlist/dnd.svg");
}
}
}
.mx_IconizedContextMenu {
@@ -137,6 +158,14 @@ Please see LICENSE files in the repository root for full details.
mask-image: url("@vector-im/compound-design-tokens/icons/home-solid.svg");
}
.mx_UserMenu_iconDnd::before {
mask-image: url("$(res)/img/element-icons/roomlist/dnd.svg");
}
.mx_UserMenu_iconDndOff::before {
mask-image: url("$(res)/img/element-icons/roomlist/dnd-cross.svg");
}
.mx_UserMenu_iconBell::before {
mask-image: url("$(res)/img/element-icons/notifications.svg");
}

View File

@@ -1,18 +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.
*/
.mx_Crypto {
table {
margin: var(--cpd-space-4x) 0;
text-align: left;
border-spacing: var(--cpd-space-2x) 0;
thead {
font: var(--cpd-font-heading-sm-semibold);
}
}
}

View File

@@ -26,8 +26,7 @@ Please see LICENSE files in the repository root for full details.
}
&.mx_UserPill_me,
&.mx_AtRoomPill,
&.mx_KeywordPill {
&.mx_AtRoomPill {
background-color: var(--cpd-color-bg-critical-primary) !important; /* To override .markdown-body */
}
@@ -46,8 +45,7 @@ Please see LICENSE files in the repository root for full details.
}
/* We don't want to indicate clickability */
&.mx_AtRoomPill:hover,
&.mx_KeywordPill:hover {
&.mx_AtRoomPill:hover {
background-color: var(--cpd-color-bg-critical-primary) !important; /* To override .markdown-body */
cursor: unset;
}

View File

@@ -35,8 +35,6 @@ Please see LICENSE files in the repository root for full details.
.mx_DisambiguatedProfile_mxid {
margin-inline-start: 0;
font: var(--cpd-font-body-sm-regular);
text-overflow: ellipsis;
overflow: hidden;
}
span:not(.mx_DisambiguatedProfile_mxid) {

Some files were not shown because too many files have changed in this diff Show More