From 9099338af8f6228c666fb21945e96a0c9493af42 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Thu, 19 Dec 2024 00:09:12 +0000
Subject: [PATCH 1/6] Update dependency typescript to v5.7.2 (#28565)
* Update dependency typescript to v5.7.2
* Fix types
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
* Fix types
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---------
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
---
package.json | 2 +-
playwright/e2e/crypto/dehydration.spec.ts | 4 ++--
playwright/e2e/crypto/migration.spec.ts | 4 ++--
playwright/element-web-test.ts | 6 ++++--
src/components/views/settings/EventIndexPanel.tsx | 4 ++--
.../views/settings/SetIntegrationManager-test.tsx | 7 +------
yarn.lock | 8 ++++----
7 files changed, 16 insertions(+), 19 deletions(-)
diff --git a/package.json b/package.json
index 910754b5a04..999d1237f94 100644
--- a/package.json
+++ b/package.json
@@ -282,7 +282,7 @@
"terser-webpack-plugin": "^5.3.9",
"ts-node": "^10.9.1",
"ts-prune": "^0.10.3",
- "typescript": "5.6.3",
+ "typescript": "5.7.2",
"util": "^0.12.5",
"web-streams-polyfill": "^4.0.0",
"webpack": "^5.89.0",
diff --git a/playwright/e2e/crypto/dehydration.spec.ts b/playwright/e2e/crypto/dehydration.spec.ts
index 590ab774b50..a247bed1801 100644
--- a/playwright/e2e/crypto/dehydration.spec.ts
+++ b/playwright/e2e/crypto/dehydration.spec.ts
@@ -8,11 +8,11 @@ Please see LICENSE files in the repository root for full details.
import { Locator, type Page } from "@playwright/test";
-import { test as base, expect } from "../../element-web-test";
+import { test as base, expect, Fixtures } from "../../element-web-test";
import { viewRoomSummaryByName } from "../right-panel/utils";
import { isDendrite } from "../../plugins/homeserver/dendrite";
-const test = base.extend({
+const test = base.extend({
// eslint-disable-next-line no-empty-pattern
startHomeserverOpts: async ({}, use) => {
await use("dehydration");
diff --git a/playwright/e2e/crypto/migration.spec.ts b/playwright/e2e/crypto/migration.spec.ts
index 048b39f06a5..03464d12c49 100644
--- a/playwright/e2e/crypto/migration.spec.ts
+++ b/playwright/e2e/crypto/migration.spec.ts
@@ -9,9 +9,9 @@ Please see LICENSE files in the repository root for full details.
import path from "path";
import { readFile } from "node:fs/promises";
-import { expect, test as base } from "../../element-web-test";
+import { expect, Fixtures, test as base } from "../../element-web-test";
-const test = base.extend({
+const test = base.extend({
// Replace the `user` fixture with one which populates the indexeddb data before starting the app.
user: async ({ context, pageWithCredentials: page, credentials }, use) => {
await page.route(`/test_indexeddb_cryptostore_dump/*`, async (route, request) => {
diff --git a/playwright/element-web-test.ts b/playwright/element-web-test.ts
index 76e57e33f70..6ac0b7226ab 100644
--- a/playwright/element-web-test.ts
+++ b/playwright/element-web-test.ts
@@ -60,7 +60,7 @@ interface CredentialsWithDisplayName extends Credentials {
displayName: string;
}
-export const test = base.extend<{
+export interface Fixtures {
axe: AxeBuilder;
checkA11y: () => Promise;
@@ -124,7 +124,9 @@ export const test = base.extend<{
slidingSyncProxy: ProxyInstance;
labsFlags: string[];
webserver: Webserver;
-}>({
+}
+
+export const test = base.extend({
config: CONFIG_JSON,
page: async ({ context, page, config, labsFlags }, use) => {
await context.route(`http://localhost:8080/config.json*`, async (route) => {
diff --git a/src/components/views/settings/EventIndexPanel.tsx b/src/components/views/settings/EventIndexPanel.tsx
index 0051c4dc3a0..8cd67c7ccff 100644
--- a/src/components/views/settings/EventIndexPanel.tsx
+++ b/src/components/views/settings/EventIndexPanel.tsx
@@ -214,7 +214,7 @@ export default class EventIndexPanel extends React.Component<{}, IState> {
{this.state.enabling ? : _t("settings|security|message_search_failed")}
- {EventIndexPeg.error && (
+ {EventIndexPeg.error ? (
{_t("common|advanced")}
@@ -230,7 +230,7 @@ export default class EventIndexPanel extends React.Component<{}, IState> {
- )}
+ ) : undefined}
>
);
}
diff --git a/test/unit-tests/components/views/settings/SetIntegrationManager-test.tsx b/test/unit-tests/components/views/settings/SetIntegrationManager-test.tsx
index 5c77e88d932..54c2aff979b 100644
--- a/test/unit-tests/components/views/settings/SetIntegrationManager-test.tsx
+++ b/test/unit-tests/components/views/settings/SetIntegrationManager-test.tsx
@@ -11,7 +11,6 @@ import { fireEvent, render, screen, waitFor, within } from "jest-matrix-react";
import { logger } from "matrix-js-sdk/src/logger";
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
-import { SDKContext, SdkContextClass } from "../../../../../src/contexts/SDKContext";
import SettingsStore from "../../../../../src/settings/SettingsStore";
import { UIFeature } from "../../../../../src/settings/UIFeature";
import {
@@ -35,13 +34,9 @@ describe("SetIntegrationManager", () => {
deleteThreePid: jest.fn(),
});
- let stores!: SdkContextClass;
-
const getComponent = () => (
-
-
-
+
);
diff --git a/yarn.lock b/yarn.lock
index 55995a78780..35e7ac04b58 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -11539,10 +11539,10 @@ typed-array-length@^1.0.6:
possible-typed-array-names "^1.0.0"
reflect.getprototypeof "^1.0.6"
-typescript@5.6.3:
- version "5.6.3"
- resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.6.3.tgz#5f3449e31c9d94febb17de03cc081dd56d81db5b"
- integrity sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==
+typescript@5.7.2:
+ version "5.7.2"
+ resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.7.2.tgz#3169cf8c4c8a828cde53ba9ecb3d2b1d5dd67be6"
+ integrity sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==
ua-parser-js@^1.0.2:
version "1.0.39"
From 2c4a07915325934fb8d341dca2d3b25ba81c7052 Mon Sep 17 00:00:00 2001
From: ElementRobot
Date: Thu, 19 Dec 2024 09:26:31 +0000
Subject: [PATCH 2/6] [create-pull-request] automated change (#28772)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
---
playwright/plugins/homeserver/synapse/index.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/playwright/plugins/homeserver/synapse/index.ts b/playwright/plugins/homeserver/synapse/index.ts
index 4620edfb029..863e236c695 100644
--- a/playwright/plugins/homeserver/synapse/index.ts
+++ b/playwright/plugins/homeserver/synapse/index.ts
@@ -20,7 +20,7 @@ import { randB64Bytes } from "../../utils/rand";
// Docker tag to use for synapse docker image.
// We target a specific digest as every now and then a Synapse update will break our CI.
// This digest is updated by the playwright-image-updates.yaml workflow periodically.
-const DOCKER_TAG = "develop@sha256:737711309e64119facbc615d703c33d7e57c3d2789a0d6d12955529902276a99";
+const DOCKER_TAG = "develop@sha256:c965896a4865479ab2628807ebf6d9c742586f3b6185a56f10077a408f1c7c3b";
async function cfgDirFromTemplate(opts: StartHomeserverOpts): Promise> {
const templateDir = path.join(__dirname, "templates", opts.template);
From cd7cf86b9614d46c1f508219cd19971bf5239a3e Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Thu, 19 Dec 2024 11:55:05 +0000
Subject: [PATCH 3/6] Remove AccountPasswordStore and related flows (#28750)
* Remove AccountPasswordStore and related flows
As they are no longer needed since MSC3967
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
* Iterate
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
* Iterate
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
* Improve coverage
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
* Update src/CreateCrossSigning.ts
Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
* Update comment
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---------
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
---
src/CreateCrossSigning.ts | 60 ++++---------------
src/SecurityManager.ts | 2 -
src/components/structures/MatrixChat.tsx | 12 +---
src/components/structures/auth/Login.tsx | 7 +--
.../structures/auth/Registration.tsx | 22 +++----
src/contexts/SDKContext.ts | 9 ---
src/stores/AccountPasswordStore.ts | 35 -----------
src/stores/InitialCryptoSetupStore.ts | 32 ++--------
src/stores/SetupEncryptionStore.ts | 2 -
test/CreateCrossSigning-test.ts | 50 ++++++++--------
.../stores/AccountPasswordStore-test.ts | 53 ----------------
.../stores/InitialCryptoSetupStore-test.ts | 36 +++++------
.../stores/SetupEncryptionStore-test.ts | 8 ---
13 files changed, 71 insertions(+), 257 deletions(-)
delete mode 100644 src/stores/AccountPasswordStore.ts
delete mode 100644 test/unit-tests/stores/AccountPasswordStore-test.ts
diff --git a/src/CreateCrossSigning.ts b/src/CreateCrossSigning.ts
index c38f1a3dd57..19b2977a9f9 100644
--- a/src/CreateCrossSigning.ts
+++ b/src/CreateCrossSigning.ts
@@ -7,60 +7,25 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
-import { logger } from "matrix-js-sdk/src/logger";
-import { AuthDict, CrossSigningKeys, MatrixClient, MatrixError, UIAFlow, UIAResponse } from "matrix-js-sdk/src/matrix";
+import { AuthDict, MatrixClient, MatrixError, UIAResponse } from "matrix-js-sdk/src/matrix";
import { SSOAuthEntry } from "./components/views/auth/InteractiveAuthEntryComponents";
import Modal from "./Modal";
import { _t } from "./languageHandler";
import InteractiveAuthDialog from "./components/views/dialogs/InteractiveAuthDialog";
-/**
- * Determine if the homeserver allows uploading device keys with only password auth, or with no auth at
- * all (ie. if the homeserver supports MSC3967).
- * @param cli The Matrix Client to use
- * @returns True if the homeserver allows uploading device keys with only password auth or with no auth
- * at all, otherwise false
- */
-async function canUploadKeysWithPasswordOnly(cli: MatrixClient): Promise {
- try {
- await cli.uploadDeviceSigningKeys(undefined, {} as CrossSigningKeys);
- // If we get here, it's because the server is allowing us to upload keys without
- // auth the first time due to MSC3967. Therefore, yes, we can upload keys
- // (with or without password, technically, but that's fine).
- return true;
- } catch (error) {
- if (!(error instanceof MatrixError) || !error.data || !error.data.flows) {
- logger.log("uploadDeviceSigningKeys advertised no flows!");
- return false;
- }
- const canUploadKeysWithPasswordOnly = error.data.flows.some((f: UIAFlow) => {
- return f.stages.length === 1 && f.stages[0] === "m.login.password";
- });
- return canUploadKeysWithPasswordOnly;
- }
-}
-
/**
* Ensures that cross signing keys are created and uploaded for the user.
* The homeserver may require user-interactive auth to upload the keys, in
- * which case the user will be prompted to authenticate. If the homeserver
- * allows uploading keys with just an account password and one is provided,
- * the keys will be uploaded without user interaction.
+ * which case the user will be prompted to authenticate.
*
* This function does not set up backups of the created cross-signing keys
* (or message keys): the cross-signing keys are stored locally and will be
* lost requiring a crypto reset, if the user logs out or loses their session.
*
* @param cli The Matrix Client to use
- * @param isTokenLogin True if the user logged in via a token login, otherwise false
- * @param accountPassword The password that the user logged in with
*/
-export async function createCrossSigning(
- cli: MatrixClient,
- isTokenLogin: boolean,
- accountPassword?: string,
-): Promise {
+export async function createCrossSigning(cli: MatrixClient): Promise {
const cryptoApi = cli.getCrypto();
if (!cryptoApi) {
throw new Error("No crypto API found!");
@@ -69,19 +34,14 @@ export async function createCrossSigning(
const doBootstrapUIAuth = async (
makeRequest: (authData: AuthDict) => Promise>,
): Promise => {
- if (accountPassword && (await canUploadKeysWithPasswordOnly(cli))) {
- await makeRequest({
- type: "m.login.password",
- identifier: {
- type: "m.id.user",
- user: cli.getUserId(),
- },
- password: accountPassword,
- });
- } else if (isTokenLogin) {
- // We are hoping the grace period is active
+ try {
await makeRequest({});
- } else {
+ } catch (error) {
+ if (!(error instanceof MatrixError) || !error.data || !error.data.flows) {
+ // Not a UIA response
+ throw error;
+ }
+
const dialogAesthetics = {
[SSOAuthEntry.PHASE_PREAUTH]: {
title: _t("auth|uia|sso_title"),
diff --git a/src/SecurityManager.ts b/src/SecurityManager.ts
index e8122b2dbf2..10672917be4 100644
--- a/src/SecurityManager.ts
+++ b/src/SecurityManager.ts
@@ -191,8 +191,6 @@ export interface AccessSecretStorageOpts {
forceReset?: boolean;
/** Create new cross-signing keys. Only applicable if `forceReset` is `true`. */
resetCrossSigning?: boolean;
- /** The cached account password, if available. */
- accountPassword?: string;
}
/**
diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx
index ee120c430ae..1a6abbbadd3 100644
--- a/src/components/structures/MatrixChat.tsx
+++ b/src/components/structures/MatrixChat.tsx
@@ -431,8 +431,6 @@ export default class MatrixChat extends React.PureComponent {
// if cross-signing is not yet set up, do so now if possible.
InitialCryptoSetupStore.sharedInstance().startInitialCryptoSetup(
cli,
- Boolean(this.tokenLogin),
- this.stores,
this.onCompleteSecurityE2eSetupFinished,
);
this.setStateForNewView({ view: Views.E2E_SETUP });
@@ -504,8 +502,6 @@ export default class MatrixChat extends React.PureComponent {
UIStore.destroy();
this.state.resizeNotifier.removeListener("middlePanelResized", this.dispatchTimelineResize);
window.removeEventListener("resize", this.onWindowResized);
-
- this.stores.accountPasswordStore.clearPassword();
}
private onWindowResized = (): void => {
@@ -1935,8 +1931,8 @@ export default class MatrixChat extends React.PureComponent {
this.showScreen("forgot_password");
};
- private onRegisterFlowComplete = (credentials: IMatrixClientCreds, password: string): Promise => {
- return this.onUserCompletedLoginFlow(credentials, password);
+ private onRegisterFlowComplete = (credentials: IMatrixClientCreds): Promise => {
+ return this.onUserCompletedLoginFlow(credentials);
};
// returns a promise which resolves to the new MatrixClient
@@ -2003,9 +1999,7 @@ export default class MatrixChat extends React.PureComponent {
* Note: SSO users (and any others using token login) currently do not pass through
* this, as they instead jump straight into the app after `attemptTokenLogin`.
*/
- private onUserCompletedLoginFlow = async (credentials: IMatrixClientCreds, password: string): Promise => {
- this.stores.accountPasswordStore.setPassword(password);
-
+ private onUserCompletedLoginFlow = async (credentials: IMatrixClientCreds): Promise => {
// Create and start the client
await Lifecycle.setLoggedIn(credentials);
await this.postLoginSetup();
diff --git a/src/components/structures/auth/Login.tsx b/src/components/structures/auth/Login.tsx
index 0a14450e63a..d9c853bc3a5 100644
--- a/src/components/structures/auth/Login.tsx
+++ b/src/components/structures/auth/Login.tsx
@@ -48,10 +48,7 @@ interface IProps {
// Called when the user has logged in. Params:
// - The object returned by the login API
- // - The user's password, if applicable, (may be cached in memory for a
- // short time so the user is not required to re-enter their password
- // for operations like uploading cross-signing keys).
- onLoggedIn(data: IMatrixClientCreds, password: string): void;
+ onLoggedIn(data: IMatrixClientCreds): void;
// login shouldn't know or care how registration, password recovery, etc is done.
onRegisterClick(): void;
@@ -199,7 +196,7 @@ export default class LoginComponent extends React.PureComponent
this.loginLogic.loginViaPassword(username, phoneCountry, phoneNumber, password).then(
(data) => {
this.setState({ serverIsAlive: true }); // it must be, we logged in.
- this.props.onLoggedIn(data, password);
+ this.props.onLoggedIn(data);
},
(error) => {
if (this.unmounted) return;
diff --git a/src/components/structures/auth/Registration.tsx b/src/components/structures/auth/Registration.tsx
index 0ae5c933460..91fe5c5faa0 100644
--- a/src/components/structures/auth/Registration.tsx
+++ b/src/components/structures/auth/Registration.tsx
@@ -72,10 +72,7 @@ interface IProps {
mobileRegister?: boolean;
// Called when the user has logged in. Params:
// - object with userId, deviceId, homeserverUrl, identityServerUrl, accessToken
- // - The user's password, if available and applicable (may be cached in memory
- // for a short time so the user is not required to re-enter their password
- // for operations like uploading cross-signing keys).
- onLoggedIn(params: IMatrixClientCreds, password: string): Promise;
+ onLoggedIn(params: IMatrixClientCreds): Promise;
// registration shouldn't know or care how login is done.
onLoginClick(): void;
onServerConfigChange(config: ValidatedServerConfig): void;
@@ -431,16 +428,13 @@ export default class Registration extends React.Component {
newState.busy = false;
newState.completedNoSignin = true;
} else {
- await this.props.onLoggedIn(
- {
- userId,
- deviceId: (response as RegisterResponse).device_id!,
- homeserverUrl: this.state.matrixClient.getHomeserverUrl(),
- identityServerUrl: this.state.matrixClient.getIdentityServerUrl(),
- accessToken,
- },
- this.state.formVals.password!,
- );
+ await this.props.onLoggedIn({
+ userId,
+ deviceId: (response as RegisterResponse).device_id!,
+ homeserverUrl: this.state.matrixClient.getHomeserverUrl(),
+ identityServerUrl: this.state.matrixClient.getIdentityServerUrl(),
+ accessToken,
+ });
this.setupPushers();
}
diff --git a/src/contexts/SDKContext.ts b/src/contexts/SDKContext.ts
index fe736615545..d77cd1e804e 100644
--- a/src/contexts/SDKContext.ts
+++ b/src/contexts/SDKContext.ts
@@ -13,7 +13,6 @@ import defaultDispatcher from "../dispatcher/dispatcher";
import LegacyCallHandler from "../LegacyCallHandler";
import { PosthogAnalytics } from "../PosthogAnalytics";
import { SlidingSyncManager } from "../SlidingSyncManager";
-import { AccountPasswordStore } from "../stores/AccountPasswordStore";
import { MemberListStore } from "../stores/MemberListStore";
import { RoomNotificationStateStore } from "../stores/notifications/RoomNotificationStateStore";
import RightPanelStore from "../stores/right-panel/RightPanelStore";
@@ -63,7 +62,6 @@ export class SdkContextClass {
protected _SpaceStore?: SpaceStoreClass;
protected _LegacyCallHandler?: LegacyCallHandler;
protected _TypingStore?: TypingStore;
- protected _AccountPasswordStore?: AccountPasswordStore;
protected _UserProfilesStore?: UserProfilesStore;
protected _OidcClientStore?: OidcClientStore;
@@ -149,13 +147,6 @@ export class SdkContextClass {
return this._TypingStore;
}
- public get accountPasswordStore(): AccountPasswordStore {
- if (!this._AccountPasswordStore) {
- this._AccountPasswordStore = new AccountPasswordStore();
- }
- return this._AccountPasswordStore;
- }
-
public get userProfilesStore(): UserProfilesStore {
if (!this.client) {
throw new Error("Unable to create UserProfilesStore without a client");
diff --git a/src/stores/AccountPasswordStore.ts b/src/stores/AccountPasswordStore.ts
deleted file mode 100644
index 85bb7359e19..00000000000
--- a/src/stores/AccountPasswordStore.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
-Copyright 2024 New Vector Ltd.
-Copyright 2023 The Matrix.org Foundation C.I.C.
-
-SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
-Please see LICENSE files in the repository root for full details.
-*/
-
-const PASSWORD_TIMEOUT = 5 * 60 * 1000; // five minutes
-
-/**
- * Store for the account password.
- * This password can be used for a short time after login
- * to avoid requestin the password all the time for instance during e2ee setup.
- */
-export class AccountPasswordStore {
- private password?: string;
- private passwordTimeoutId?: ReturnType;
-
- public setPassword(password: string): void {
- this.password = password;
- clearTimeout(this.passwordTimeoutId);
- this.passwordTimeoutId = setTimeout(this.clearPassword, PASSWORD_TIMEOUT);
- }
-
- public getPassword(): string | undefined {
- return this.password;
- }
-
- public clearPassword = (): void => {
- clearTimeout(this.passwordTimeoutId);
- this.passwordTimeoutId = undefined;
- this.password = undefined;
- };
-}
diff --git a/src/stores/InitialCryptoSetupStore.ts b/src/stores/InitialCryptoSetupStore.ts
index 5554a15d260..46ae784db4b 100644
--- a/src/stores/InitialCryptoSetupStore.ts
+++ b/src/stores/InitialCryptoSetupStore.ts
@@ -11,7 +11,6 @@ import { logger } from "matrix-js-sdk/src/logger";
import { useEffect, useState } from "react";
import { createCrossSigning } from "../CreateCrossSigning";
-import { SdkContextClass } from "../contexts/SDKContext";
type Status = "in_progress" | "complete" | "error" | undefined;
@@ -45,8 +44,6 @@ export class InitialCryptoSetupStore extends EventEmitter {
private status: Status = undefined;
private client?: MatrixClient;
- private isTokenLogin?: boolean;
- private stores?: SdkContextClass;
private onFinished?: (success: boolean) => void;
public static sharedInstance(): InitialCryptoSetupStore {
@@ -62,18 +59,9 @@ export class InitialCryptoSetupStore extends EventEmitter {
* Start the initial crypto setup process.
*
* @param {MatrixClient} client The client to use for the setup
- * @param {boolean} isTokenLogin True if the user logged in via a token login, otherwise false
- * @param {SdkContextClass} stores The stores to use for the setup
*/
- public startInitialCryptoSetup(
- client: MatrixClient,
- isTokenLogin: boolean,
- stores: SdkContextClass,
- onFinished: (success: boolean) => void,
- ): void {
+ public startInitialCryptoSetup(client: MatrixClient, onFinished: (success: boolean) => void): void {
this.client = client;
- this.isTokenLogin = isTokenLogin;
- this.stores = stores;
this.onFinished = onFinished;
// We just start this process: it's progress is tracked by the events rather
@@ -89,7 +77,7 @@ export class InitialCryptoSetupStore extends EventEmitter {
* @returns {boolean} True if a retry was initiated, otherwise false
*/
public retry(): boolean {
- if (this.client === undefined || this.isTokenLogin === undefined || this.stores == undefined) return false;
+ if (this.client === undefined) return false;
this.doSetup().catch(() => logger.error("Initial crypto setup failed"));
@@ -98,12 +86,10 @@ export class InitialCryptoSetupStore extends EventEmitter {
private reset(): void {
this.client = undefined;
- this.isTokenLogin = undefined;
- this.stores = undefined;
}
private async doSetup(): Promise {
- if (this.client === undefined || this.isTokenLogin === undefined || this.stores == undefined) {
+ if (this.client === undefined) {
throw new Error("No setup is in progress");
}
@@ -115,7 +101,7 @@ export class InitialCryptoSetupStore extends EventEmitter {
try {
// Create the user's cross-signing keys
- await createCrossSigning(this.client, this.isTokenLogin, this.stores.accountPasswordStore.getPassword());
+ await createCrossSigning(this.client);
// Check for any existing backup and enable key backup if there isn't one
const currentKeyBackup = await cryptoApi.checkKeyBackupAndEnable();
@@ -129,16 +115,6 @@ export class InitialCryptoSetupStore extends EventEmitter {
this.emit("update");
this.onFinished?.(true);
} catch (e) {
- if (this.isTokenLogin) {
- // ignore any failures, we are relying on grace period here
- this.reset();
-
- this.status = "complete";
- this.emit("update");
- this.onFinished?.(true);
-
- return;
- }
logger.error("Error bootstrapping cross-signing", e);
this.status = "error";
this.emit("update");
diff --git a/src/stores/SetupEncryptionStore.ts b/src/stores/SetupEncryptionStore.ts
index a13ba26f722..bfa28c3cd22 100644
--- a/src/stores/SetupEncryptionStore.ts
+++ b/src/stores/SetupEncryptionStore.ts
@@ -19,7 +19,6 @@ import { Device, SecretStorage } from "matrix-js-sdk/src/matrix";
import { MatrixClientPeg } from "../MatrixClientPeg";
import { AccessCancelledError, accessSecretStorage } from "../SecurityManager";
-import { SdkContextClass } from "../contexts/SDKContext";
import { asyncSome } from "../utils/arrays";
import { initialiseDehydration } from "../utils/device/dehydration";
@@ -239,7 +238,6 @@ export class SetupEncryptionStore extends EventEmitter {
{
forceReset: true,
resetCrossSigning: true,
- accountPassword: SdkContextClass.instance.accountPasswordStore.getPassword(),
},
);
} catch (e) {
diff --git a/test/CreateCrossSigning-test.ts b/test/CreateCrossSigning-test.ts
index e1762bb5040..85341b8bce2 100644
--- a/test/CreateCrossSigning-test.ts
+++ b/test/CreateCrossSigning-test.ts
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
-import { MatrixClient, MatrixError } from "matrix-js-sdk/src/matrix";
+import { HTTPError, MatrixClient, MatrixError } from "matrix-js-sdk/src/matrix";
import { mocked } from "jest-mock";
import { createCrossSigning } from "../src/CreateCrossSigning";
@@ -21,14 +21,14 @@ describe("CreateCrossSigning", () => {
});
it("should call bootstrapCrossSigning with an authUploadDeviceSigningKeys function", async () => {
- await createCrossSigning(client, false, "password");
+ await createCrossSigning(client);
expect(client.getCrypto()?.bootstrapCrossSigning).toHaveBeenCalledWith({
authUploadDeviceSigningKeys: expect.any(Function),
});
});
- it("should upload with password auth if possible", async () => {
+ it("should upload", async () => {
client.uploadDeviceSigningKeys = jest.fn().mockRejectedValueOnce(
new MatrixError({
flows: [
@@ -39,24 +39,7 @@ describe("CreateCrossSigning", () => {
}),
);
- await createCrossSigning(client, false, "password");
-
- const { authUploadDeviceSigningKeys } = mocked(client.getCrypto()!).bootstrapCrossSigning.mock.calls[0][0];
-
- const makeRequest = jest.fn();
- await authUploadDeviceSigningKeys!(makeRequest);
- expect(makeRequest).toHaveBeenCalledWith({
- type: "m.login.password",
- identifier: {
- type: "m.id.user",
- user: client.getUserId(),
- },
- password: "password",
- });
- });
-
- it("should attempt to upload keys without auth if using token login", async () => {
- await createCrossSigning(client, true, undefined);
+ await createCrossSigning(client);
const { authUploadDeviceSigningKeys } = mocked(client.getCrypto()!).bootstrapCrossSigning.mock.calls[0][0];
@@ -65,7 +48,7 @@ describe("CreateCrossSigning", () => {
expect(makeRequest).toHaveBeenCalledWith({});
});
- it("should prompt user if password upload not possible", async () => {
+ it("should prompt user if upload failed with UIA", async () => {
const createDialog = jest.spyOn(Modal, "createDialog").mockReturnValue({
finished: Promise.resolve([true]),
close: jest.fn(),
@@ -81,13 +64,32 @@ describe("CreateCrossSigning", () => {
}),
);
- await createCrossSigning(client, false, "password");
+ await createCrossSigning(client);
const { authUploadDeviceSigningKeys } = mocked(client.getCrypto()!).bootstrapCrossSigning.mock.calls[0][0];
- const makeRequest = jest.fn();
+ const makeRequest = jest.fn().mockRejectedValue(
+ new MatrixError({
+ flows: [
+ {
+ stages: ["dummy.mystery_flow_nobody_knows"],
+ },
+ ],
+ }),
+ );
await authUploadDeviceSigningKeys!(makeRequest);
expect(makeRequest).not.toHaveBeenCalledWith();
expect(createDialog).toHaveBeenCalled();
});
+
+ it("should throw error if server fails with something other than UIA", async () => {
+ await createCrossSigning(client);
+
+ const { authUploadDeviceSigningKeys } = mocked(client.getCrypto()!).bootstrapCrossSigning.mock.calls[0][0];
+
+ const error = new HTTPError("Internal Server Error", 500);
+ const makeRequest = jest.fn().mockRejectedValue(error);
+ await expect(authUploadDeviceSigningKeys!(makeRequest)).rejects.toThrow(error);
+ expect(makeRequest).not.toHaveBeenCalledWith();
+ });
});
diff --git a/test/unit-tests/stores/AccountPasswordStore-test.ts b/test/unit-tests/stores/AccountPasswordStore-test.ts
deleted file mode 100644
index 00fa8e05e66..00000000000
--- a/test/unit-tests/stores/AccountPasswordStore-test.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
-Copyright 2024 New Vector Ltd.
-Copyright 2023 The Matrix.org Foundation C.I.C.
-
-SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
-Please see LICENSE files in the repository root for full details.
-*/
-
-import { AccountPasswordStore } from "../../../src/stores/AccountPasswordStore";
-
-jest.useFakeTimers();
-
-describe("AccountPasswordStore", () => {
- let accountPasswordStore: AccountPasswordStore;
-
- beforeEach(() => {
- accountPasswordStore = new AccountPasswordStore();
- });
-
- it("should not have a password by default", () => {
- expect(accountPasswordStore.getPassword()).toBeUndefined();
- });
-
- describe("when setting a password", () => {
- beforeEach(() => {
- accountPasswordStore.setPassword("pass1");
- });
-
- it("should return the password", () => {
- expect(accountPasswordStore.getPassword()).toBe("pass1");
- });
-
- describe("and the password timeout exceed", () => {
- beforeEach(() => {
- jest.advanceTimersToNextTimer();
- });
-
- it("should clear the password", () => {
- expect(accountPasswordStore.getPassword()).toBeUndefined();
- });
- });
-
- describe("and setting another password", () => {
- beforeEach(() => {
- accountPasswordStore.setPassword("pass2");
- });
-
- it("should return the other password", () => {
- expect(accountPasswordStore.getPassword()).toBe("pass2");
- });
- });
- });
-});
diff --git a/test/unit-tests/stores/InitialCryptoSetupStore-test.ts b/test/unit-tests/stores/InitialCryptoSetupStore-test.ts
index 64b81bade22..8cfae4d6996 100644
--- a/test/unit-tests/stores/InitialCryptoSetupStore-test.ts
+++ b/test/unit-tests/stores/InitialCryptoSetupStore-test.ts
@@ -8,12 +8,11 @@ Please see LICENSE files in the repository root for full details.
import { mocked } from "jest-mock";
import { MatrixClient } from "matrix-js-sdk/src/matrix";
import { waitFor } from "jest-matrix-react";
+import { sleep } from "matrix-js-sdk/src/utils";
import { createCrossSigning } from "../../../src/CreateCrossSigning";
import { InitialCryptoSetupStore } from "../../../src/stores/InitialCryptoSetupStore";
-import { SdkContextClass } from "../../../src/contexts/SDKContext";
import { createTestClient } from "../../test-utils";
-import { AccountPasswordStore } from "../../../src/stores/AccountPasswordStore";
jest.mock("../../../src/CreateCrossSigning", () => ({
createCrossSigning: jest.fn(),
@@ -22,7 +21,6 @@ jest.mock("../../../src/CreateCrossSigning", () => ({
describe("InitialCryptoSetupStore", () => {
let testStore: InitialCryptoSetupStore;
let client: MatrixClient;
- let stores: SdkContextClass;
let createCrossSigningResolve: () => void;
let createCrossSigningReject: (e: Error) => void;
@@ -30,11 +28,6 @@ describe("InitialCryptoSetupStore", () => {
beforeEach(() => {
testStore = new InitialCryptoSetupStore();
client = createTestClient();
- stores = {
- accountPasswordStore: {
- getPassword: jest.fn(),
- } as unknown as AccountPasswordStore,
- } as unknown as SdkContextClass;
mocked(createCrossSigning).mockImplementation(() => {
return new Promise((resolve, reject) => {
@@ -45,7 +38,7 @@ describe("InitialCryptoSetupStore", () => {
});
it("should call createCrossSigning when startInitialCryptoSetup is called", async () => {
- testStore.startInitialCryptoSetup(client, false, stores, jest.fn());
+ testStore.startInitialCryptoSetup(client, jest.fn());
await waitFor(() => expect(createCrossSigning).toHaveBeenCalled());
});
@@ -54,7 +47,7 @@ describe("InitialCryptoSetupStore", () => {
const updateSpy = jest.fn();
testStore.on("update", updateSpy);
- testStore.startInitialCryptoSetup(client, false, stores, jest.fn());
+ testStore.startInitialCryptoSetup(client, jest.fn());
createCrossSigningResolve();
await waitFor(() => expect(updateSpy).toHaveBeenCalled());
@@ -65,21 +58,28 @@ describe("InitialCryptoSetupStore", () => {
const updateSpy = jest.fn();
testStore.on("update", updateSpy);
- testStore.startInitialCryptoSetup(client, false, stores, jest.fn());
+ testStore.startInitialCryptoSetup(client, jest.fn());
createCrossSigningReject(new Error("Test error"));
await waitFor(() => expect(updateSpy).toHaveBeenCalled());
expect(testStore.getStatus()).toBe("error");
});
- it("should ignore failures if tokenLogin is true", async () => {
- const updateSpy = jest.fn();
- testStore.on("update", updateSpy);
+ it("should fail to retry once complete", async () => {
+ testStore.startInitialCryptoSetup(client, jest.fn());
- testStore.startInitialCryptoSetup(client, true, stores, jest.fn());
- createCrossSigningReject(new Error("Test error"));
+ await waitFor(() => expect(createCrossSigning).toHaveBeenCalled());
+ createCrossSigningResolve();
+ await sleep(0); // await the next tick
+ expect(testStore.retry()).toBeFalsy();
+ });
- await waitFor(() => expect(updateSpy).toHaveBeenCalled());
- expect(testStore.getStatus()).toBe("complete");
+ it("should retry if initial attempt failed", async () => {
+ testStore.startInitialCryptoSetup(client, jest.fn());
+
+ await waitFor(() => expect(createCrossSigning).toHaveBeenCalled());
+ createCrossSigningReject(new Error("Test error"));
+ await sleep(0); // await the next tick
+ expect(testStore.retry()).toBeTruthy();
});
});
diff --git a/test/unit-tests/stores/SetupEncryptionStore-test.ts b/test/unit-tests/stores/SetupEncryptionStore-test.ts
index d3d0300a215..b0bc3a73d87 100644
--- a/test/unit-tests/stores/SetupEncryptionStore-test.ts
+++ b/test/unit-tests/stores/SetupEncryptionStore-test.ts
@@ -11,7 +11,6 @@ import { MatrixClient, Device } from "matrix-js-sdk/src/matrix";
import { SecretStorageKeyDescriptionAesV1, ServerSideSecretStorage } from "matrix-js-sdk/src/secret-storage";
import { BootstrapCrossSigningOpts, CryptoApi, DeviceVerificationStatus } from "matrix-js-sdk/src/crypto-api";
-import { SdkContextClass } from "../../../src/contexts/SDKContext";
import { accessSecretStorage } from "../../../src/SecurityManager";
import { SetupEncryptionStore } from "../../../src/stores/SetupEncryptionStore";
import { emitPromise, stubClient } from "../../test-utils";
@@ -21,7 +20,6 @@ jest.mock("../../../src/SecurityManager", () => ({
}));
describe("SetupEncryptionStore", () => {
- const cachedPassword = "p4assword";
let client: Mocked;
let mockCrypto: Mocked;
let mockSecretStorage: Mocked;
@@ -47,11 +45,6 @@ describe("SetupEncryptionStore", () => {
Object.defineProperty(client, "secretStorage", { value: mockSecretStorage });
setupEncryptionStore = new SetupEncryptionStore();
- SdkContextClass.instance.accountPasswordStore.setPassword(cachedPassword);
- });
-
- afterEach(() => {
- SdkContextClass.instance.accountPasswordStore.clearPassword();
});
describe("start", () => {
@@ -172,7 +165,6 @@ describe("SetupEncryptionStore", () => {
await setupEncryptionStore.resetConfirm();
expect(mocked(accessSecretStorage)).toHaveBeenCalledWith(expect.any(Function), {
- accountPassword: cachedPassword,
forceReset: true,
resetCrossSigning: true,
});
From baaed75c4b722a5670a2414d5ab777066523195c Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Thu, 19 Dec 2024 13:36:09 +0000
Subject: [PATCH 4/6] Clean up Playwright test code related to legacy crypto
(#28770)
* Clean up Playwright test code related to legacy crypto
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
* Tidy further
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---------
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
playwright/e2e/crypto/dehydration.spec.ts | 2 --
playwright/e2e/crypto/event-shields.spec.ts | 7 ++-----
playwright/e2e/crypto/migration.spec.ts | 1 -
playwright/e2e/crypto/utils.ts | 6 +-----
4 files changed, 3 insertions(+), 13 deletions(-)
diff --git a/playwright/e2e/crypto/dehydration.spec.ts b/playwright/e2e/crypto/dehydration.spec.ts
index a247bed1801..39629c82622 100644
--- a/playwright/e2e/crypto/dehydration.spec.ts
+++ b/playwright/e2e/crypto/dehydration.spec.ts
@@ -50,8 +50,6 @@ test.describe("Dehydration", () => {
});
test("Create dehydrated device", async ({ page, user, app }, workerInfo) => {
- test.skip(workerInfo.project.name === "Legacy Crypto", "This test only works with Rust crypto.");
-
// Create a backup (which will create SSSS, and dehydrated device)
const securityTab = await app.settings.openUserSettings("Security & Privacy");
diff --git a/playwright/e2e/crypto/event-shields.spec.ts b/playwright/e2e/crypto/event-shields.spec.ts
index 0beb8e36500..da9fe1fd1a3 100644
--- a/playwright/e2e/crypto/event-shields.spec.ts
+++ b/playwright/e2e/crypto/event-shields.spec.ts
@@ -133,8 +133,7 @@ test.describe("Cryptography", function () {
"Encrypted by a device not verified by its owner.",
);
- /* In legacy crypto: should show a grey padlock for a message from a deleted device.
- * In rust crypto: should show a red padlock for a message from an unverified device.
+ /* Should show a red padlock for a message from an unverified device.
* Rust crypto remembers the verification state of the sending device, so it will know that the device was
* unverified, even if it gets deleted. */
// bob deletes his second device
@@ -168,9 +167,7 @@ test.describe("Cryptography", function () {
await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_warning/);
await lastE2eIcon.focus();
await expect(await app.getTooltipForElement(lastE2eIcon)).toContainText(
- workerInfo.project.name === "Legacy Crypto"
- ? "Encrypted by an unknown or deleted device."
- : "Encrypted by a device not verified by its owner.",
+ "Encrypted by a device not verified by its owner.",
);
});
diff --git a/playwright/e2e/crypto/migration.spec.ts b/playwright/e2e/crypto/migration.spec.ts
index 03464d12c49..a9530a288b7 100644
--- a/playwright/e2e/crypto/migration.spec.ts
+++ b/playwright/e2e/crypto/migration.spec.ts
@@ -29,7 +29,6 @@ test.describe("migration", function () {
test.use({ displayName: "Alice" });
test("Should support migration from legacy crypto", async ({ context, user, page }, workerInfo) => {
- test.skip(workerInfo.project.name === "Legacy Crypto", "This test only works with Rust crypto.");
test.slow();
// We should see a migration progress bar
diff --git a/playwright/e2e/crypto/utils.ts b/playwright/e2e/crypto/utils.ts
index 94b1933977e..337ff3d6344 100644
--- a/playwright/e2e/crypto/utils.ts
+++ b/playwright/e2e/crypto/utils.ts
@@ -220,11 +220,7 @@ export async function doTwoWaySasVerification(page: Page, verifier: JSHandle
Date: Thu, 19 Dec 2024 22:53:51 +0000
Subject: [PATCH 5/6] Use mapped types around account data events (#28752)
* Use mapped types around account data events
---------
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
playwright/e2e/crypto/crypto.spec.ts | 2 +-
.../get-openid-token.spec.ts | 3 +-
.../e2e/integration-manager/kick.spec.ts | 3 +-
.../integration-manager/read_events.spec.ts | 3 +-
.../integration-manager/send_event.spec.ts | 3 +-
playwright/e2e/room/room.spec.ts | 4 +--
playwright/e2e/spotlight/spotlight.spec.ts | 5 ++-
playwright/e2e/widgets/stickers.spec.ts | 3 +-
playwright/pages/client.ts | 8 +++--
src/@types/matrix-js-sdk.d.ts | 31 ++++++++++++++++++
src/components/structures/LoggedInView.tsx | 3 +-
.../views/dialogs/devtools/AccountData.tsx | 4 +--
.../views/settings/devices/useOwnDevices.ts | 2 +-
src/hooks/useAccountData.ts | 4 +--
.../handlers/AccountSettingsHandler.ts | 17 +++++-----
src/stores/WidgetStore.ts | 10 ++----
src/utils/IdentityServerUtils.ts | 2 +-
src/utils/WidgetUtils-types.ts | 32 +++++++++++++++++++
src/utils/WidgetUtils.ts | 14 ++------
src/utils/device/clientInformation.ts | 14 ++++----
src/utils/device/types.ts | 13 ++++++++
src/utils/notifications.ts | 4 ++-
test/unit-tests/Notifier-test.ts | 3 +-
.../right_panel/RoomSummaryCard-test.tsx | 25 +++++++--------
test/unit-tests/utils/notifications-test.ts | 11 +++++--
25 files changed, 152 insertions(+), 71 deletions(-)
create mode 100644 src/utils/WidgetUtils-types.ts
create mode 100644 src/utils/device/types.ts
diff --git a/playwright/e2e/crypto/crypto.spec.ts b/playwright/e2e/crypto/crypto.spec.ts
index 668c17d931d..d223138781a 100644
--- a/playwright/e2e/crypto/crypto.spec.ts
+++ b/playwright/e2e/crypto/crypto.spec.ts
@@ -81,7 +81,7 @@ test.describe("Cryptography", function () {
* Verify that the `m.cross_signing.${keyType}` key is available on the account data on the server
* @param keyType
*/
- async function verifyKey(app: ElementAppPage, keyType: string) {
+ async function verifyKey(app: ElementAppPage, keyType: "master" | "self_signing" | "user_signing") {
const accountData: { encrypted: Record> } = await app.client.evaluate(
(cli, keyType) => cli.getAccountDataFromServer(`m.cross_signing.${keyType}`),
keyType,
diff --git a/playwright/e2e/integration-manager/get-openid-token.spec.ts b/playwright/e2e/integration-manager/get-openid-token.spec.ts
index 48d52d35fa4..2bf00817ed6 100644
--- a/playwright/e2e/integration-manager/get-openid-token.spec.ts
+++ b/playwright/e2e/integration-manager/get-openid-token.spec.ts
@@ -9,6 +9,7 @@ Please see LICENSE files in the repository root for full details.
import type { Page } from "@playwright/test";
import { test, expect } from "../../element-web-test";
import { openIntegrationManager } from "./utils";
+import type { UserWidget } from "../../../src/utils/WidgetUtils-types.ts";
const ROOM_NAME = "Integration Manager Test";
@@ -92,7 +93,7 @@ test.describe("Integration Manager: Get OpenID Token", () => {
},
},
id: "integration-manager",
- },
+ } as unknown as UserWidget,
});
// Succeed when checking the token is valid
diff --git a/playwright/e2e/integration-manager/kick.spec.ts b/playwright/e2e/integration-manager/kick.spec.ts
index 59c2703a18d..9d25d049348 100644
--- a/playwright/e2e/integration-manager/kick.spec.ts
+++ b/playwright/e2e/integration-manager/kick.spec.ts
@@ -9,6 +9,7 @@ Please see LICENSE files in the repository root for full details.
import type { Page } from "@playwright/test";
import { test, expect } from "../../element-web-test";
import { openIntegrationManager } from "./utils";
+import type { UserWidget } from "../../../src/utils/WidgetUtils-types.ts";
const ROOM_NAME = "Integration Manager Test";
const USER_DISPLAY_NAME = "Alice";
@@ -136,7 +137,7 @@ test.describe("Integration Manager: Kick", () => {
},
},
id: "integration-manager",
- },
+ } as unknown as UserWidget,
});
// Succeed when checking the token is valid
diff --git a/playwright/e2e/integration-manager/read_events.spec.ts b/playwright/e2e/integration-manager/read_events.spec.ts
index 791d5bd725b..8fc81d766f5 100644
--- a/playwright/e2e/integration-manager/read_events.spec.ts
+++ b/playwright/e2e/integration-manager/read_events.spec.ts
@@ -9,6 +9,7 @@ Please see LICENSE files in the repository root for full details.
import type { Page } from "@playwright/test";
import { test, expect } from "../../element-web-test";
import { openIntegrationManager } from "./utils";
+import type { UserWidget } from "../../../src/utils/WidgetUtils-types.ts";
const ROOM_NAME = "Integration Manager Test";
@@ -107,7 +108,7 @@ test.describe("Integration Manager: Read Events", () => {
},
},
id: "integration-manager",
- },
+ } as unknown as UserWidget,
});
// Succeed when checking the token is valid
diff --git a/playwright/e2e/integration-manager/send_event.spec.ts b/playwright/e2e/integration-manager/send_event.spec.ts
index 363719d8f14..2f6e9039533 100644
--- a/playwright/e2e/integration-manager/send_event.spec.ts
+++ b/playwright/e2e/integration-manager/send_event.spec.ts
@@ -9,6 +9,7 @@ Please see LICENSE files in the repository root for full details.
import type { Page } from "@playwright/test";
import { test, expect } from "../../element-web-test";
import { openIntegrationManager } from "./utils";
+import type { UserWidget } from "../../../src/utils/WidgetUtils-types.ts";
const ROOM_NAME = "Integration Manager Test";
@@ -113,7 +114,7 @@ test.describe("Integration Manager: Send Event", () => {
},
},
id: "integration-manager",
- },
+ } as unknown as UserWidget,
});
// Succeed when checking the token is valid
diff --git a/playwright/e2e/room/room.spec.ts b/playwright/e2e/room/room.spec.ts
index 76fa64a6488..cd8a3ff793d 100644
--- a/playwright/e2e/room/room.spec.ts
+++ b/playwright/e2e/room/room.spec.ts
@@ -6,7 +6,7 @@ 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 { EventType } from "matrix-js-sdk/src/matrix";
+import type { AccountDataEvents } from "matrix-js-sdk/src/matrix";
import { test, expect } from "../../element-web-test";
import { Bot } from "../../pages/bot";
@@ -28,7 +28,7 @@ test.describe("Room Directory", () => {
const charlieRoom = await cli.createRoom({ is_direct: true });
await cli.invite(bobRoom.room_id, bob);
await cli.invite(charlieRoom.room_id, charlie);
- await cli.setAccountData("m.direct" as EventType, {
+ await cli.setAccountData("m.direct" as keyof AccountDataEvents, {
[bob]: [bobRoom.room_id],
[charlie]: [charlieRoom.room_id],
});
diff --git a/playwright/e2e/spotlight/spotlight.spec.ts b/playwright/e2e/spotlight/spotlight.spec.ts
index 22513ca47a7..a6396dcc42a 100644
--- a/playwright/e2e/spotlight/spotlight.spec.ts
+++ b/playwright/e2e/spotlight/spotlight.spec.ts
@@ -6,6 +6,7 @@ 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 { AccountDataEvents } from "matrix-js-sdk/src/matrix";
import { test, expect } from "../../element-web-test";
import { Filter } from "../../pages/Spotlight";
import { Bot } from "../../pages/bot";
@@ -255,7 +256,9 @@ test.describe("Spotlight", () => {
// Invite BotBob into existing DM with ByteBot
const dmRooms = await app.client.evaluate((client, userId) => {
- const map = client.getAccountData("m.direct")?.getContent>();
+ const map = client
+ .getAccountData("m.direct" as keyof AccountDataEvents)
+ ?.getContent>();
return map[userId] ?? [];
}, bot2UserId);
expect(dmRooms).toHaveLength(1);
diff --git a/playwright/e2e/widgets/stickers.spec.ts b/playwright/e2e/widgets/stickers.spec.ts
index 318f7129616..418e104037b 100644
--- a/playwright/e2e/widgets/stickers.spec.ts
+++ b/playwright/e2e/widgets/stickers.spec.ts
@@ -12,6 +12,7 @@ import type { Page } from "@playwright/test";
import { test, expect } from "../../element-web-test";
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";
const STICKER_PICKER_WIDGET_NAME = "Fake Stickers";
@@ -123,7 +124,7 @@ async function setWidgetAccountData(
state_key: STICKER_PICKER_WIDGET_ID,
type: "m.widget",
id: STICKER_PICKER_WIDGET_ID,
- },
+ } as unknown as UserWidget,
});
}
diff --git a/playwright/pages/client.ts b/playwright/pages/client.ts
index 2dfe7484f5d..23b48602e98 100644
--- a/playwright/pages/client.ts
+++ b/playwright/pages/client.ts
@@ -25,6 +25,7 @@ import type {
Upload,
StateEvents,
TimelineEvents,
+ AccountDataEvents,
} from "matrix-js-sdk/src/matrix";
import type { RoomMessageEventContent } from "matrix-js-sdk/src/types";
import { Credentials } from "../plugins/homeserver";
@@ -439,11 +440,14 @@ export class Client {
* @param type The type of account data to set
* @param content The content to set
*/
- public async setAccountData(type: string, content: IContent): Promise {
+ public async setAccountData(
+ type: T,
+ content: AccountDataEvents[T],
+ ): Promise {
const client = await this.prepareClient();
return client.evaluate(
async (client, { type, content }) => {
- await client.setAccountData(type, content);
+ await client.setAccountData(type as T, content as AccountDataEvents[T]);
},
{ type, content },
);
diff --git a/src/@types/matrix-js-sdk.d.ts b/src/@types/matrix-js-sdk.d.ts
index 41ccfcbb3b2..21299cff300 100644
--- a/src/@types/matrix-js-sdk.d.ts
+++ b/src/@types/matrix-js-sdk.d.ts
@@ -11,6 +11,8 @@ import type { BLURHASH_FIELD } from "../utils/image-media";
import type { JitsiCallMemberEventType, JitsiCallMemberContent } from "../call-types";
import type { ILayoutStateEvent, WIDGET_LAYOUT_EVENT_TYPE } from "../stores/widgets/types";
import type { EncryptedFile } from "matrix-js-sdk/src/types";
+import type { DeviceClientInformation } from "../utils/device/types.ts";
+import type { UserWidget } from "../utils/WidgetUtils-types.ts";
// Extend Matrix JS SDK types via Typescript declaration merging to support unspecced event fields and types
declare module "matrix-js-sdk/src/types" {
@@ -57,6 +59,35 @@ declare module "matrix-js-sdk/src/types" {
};
}
+ export interface AccountDataEvents {
+ // Analytics account data event
+ "im.vector.analytics": {
+ id: string;
+ pseudonymousAnalyticsOptIn?: boolean;
+ };
+ // Device client information account data event
+ [key: `io.element.matrix_client_information.${string}`]: DeviceClientInformation;
+ // Element settings account data events
+ "im.vector.setting.breadcrumbs": { recent_rooms: string[] };
+ "io.element.recent_emoji": { recent_emoji: string[] };
+ "im.vector.setting.integration_provisioning": { enabled: boolean };
+ "im.vector.riot.breadcrumb_rooms": { recent_rooms: string[] };
+ "im.vector.web.settings": Record;
+
+ // URL preview account data event
+ "org.matrix.preview_urls": { disable: boolean };
+
+ // This is not yet in the Matrix spec yet is being used as if it was
+ "m.widgets": {
+ [widgetId: string]: UserWidget;
+ };
+
+ // This is not in the Matrix spec yet seems to use an `m.` prefix
+ "m.accepted_terms": {
+ accepted: string[];
+ };
+ }
+
export interface AudioContent {
// MSC1767 + Ideals of MSC2516 as MSC3245
// https://github.com/matrix-org/matrix-doc/pull/3245
diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx
index 019f9cd1a84..88e7e937900 100644
--- a/src/components/structures/LoggedInView.tsx
+++ b/src/components/structures/LoggedInView.tsx
@@ -16,6 +16,7 @@ import {
IUsageLimit,
SyncStateData,
SyncState,
+ EventType,
} from "matrix-js-sdk/src/matrix";
import { MatrixCall } from "matrix-js-sdk/src/webrtc/call";
import classNames from "classnames";
@@ -161,7 +162,7 @@ class LoggedInView extends React.Component {
this._matrixClient.on(ClientEvent.AccountData, this.onAccountData);
// check push rules on start up as well
- monitorSyncedPushRules(this._matrixClient.getAccountData("m.push_rules"), this._matrixClient);
+ monitorSyncedPushRules(this._matrixClient.getAccountData(EventType.PushRules), this._matrixClient);
this._matrixClient.on(ClientEvent.Sync, this.onSync);
// Call `onSync` with the current state as well
this.onSync(this._matrixClient.getSyncState(), null, this._matrixClient.getSyncStateData() ?? undefined);
diff --git a/src/components/views/dialogs/devtools/AccountData.tsx b/src/components/views/dialogs/devtools/AccountData.tsx
index f1fc081b009..920cab88603 100644
--- a/src/components/views/dialogs/devtools/AccountData.tsx
+++ b/src/components/views/dialogs/devtools/AccountData.tsx
@@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details.
*/
import React, { useContext, useMemo, useState } from "react";
-import { IContent, MatrixEvent } from "matrix-js-sdk/src/matrix";
+import { AccountDataEvents, IContent, MatrixEvent } from "matrix-js-sdk/src/matrix";
import BaseTool, { DevtoolsContext, IDevtoolsProps } from "./BaseTool";
import MatrixClientContext from "../../../../contexts/MatrixClientContext";
@@ -21,7 +21,7 @@ export const AccountDataEventEditor: React.FC = ({ mxEvent, onBack
const fields = useMemo(() => [eventTypeField(mxEvent?.getType())], [mxEvent]);
- const onSend = async ([eventType]: string[], content?: IContent): Promise => {
+ const onSend = async ([eventType]: Array, content?: IContent): Promise => {
await cli.setAccountData(eventType, content || {});
};
diff --git a/src/components/views/settings/devices/useOwnDevices.ts b/src/components/views/settings/devices/useOwnDevices.ts
index 51a05f6242d..52b8e0aa631 100644
--- a/src/components/views/settings/devices/useOwnDevices.ts
+++ b/src/components/views/settings/devices/useOwnDevices.ts
@@ -116,7 +116,7 @@ export const useOwnDevices = (): DevicesState => {
const notificationSettings = new Map();
Object.keys(devices).forEach((deviceId) => {
- const eventType = `${LOCAL_NOTIFICATION_SETTINGS_PREFIX.name}.${deviceId}`;
+ const eventType = `${LOCAL_NOTIFICATION_SETTINGS_PREFIX.name}.${deviceId}` as const;
const event = matrixClient.getAccountData(eventType);
if (event) {
notificationSettings.set(deviceId, event.getContent());
diff --git a/src/hooks/useAccountData.ts b/src/hooks/useAccountData.ts
index 0f55969e29a..b2fe464e55d 100644
--- a/src/hooks/useAccountData.ts
+++ b/src/hooks/useAccountData.ts
@@ -7,14 +7,14 @@ Please see LICENSE files in the repository root for full details.
*/
import { useCallback, useState } from "react";
-import { ClientEvent, MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix";
+import { AccountDataEvents, ClientEvent, MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix";
import { useTypedEventEmitter } from "./useEventEmitter";
const tryGetContent = (ev?: MatrixEvent): T | undefined => ev?.getContent();
// Hook to simplify listening to Matrix account data
-export const useAccountData = (cli: MatrixClient, eventType: string): T => {
+export const useAccountData = (cli: MatrixClient, eventType: keyof AccountDataEvents): T => {
const [value, setValue] = useState(() => tryGetContent(cli.getAccountData(eventType)));
const handler = useCallback(
diff --git a/src/settings/handlers/AccountSettingsHandler.ts b/src/settings/handlers/AccountSettingsHandler.ts
index 051e6fc7a61..9187bd0d492 100644
--- a/src/settings/handlers/AccountSettingsHandler.ts
+++ b/src/settings/handlers/AccountSettingsHandler.ts
@@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
-import { ClientEvent, MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix";
+import { AccountDataEvents, ClientEvent, MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix";
import { defer } from "matrix-js-sdk/src/utils";
import { isEqual } from "lodash";
@@ -140,11 +140,11 @@ export default class AccountSettingsHandler extends MatrixClientBackedSettingsHa
}
// helper function to set account data then await it being echoed back
- private async setAccountData(
- eventType: string,
- field: string,
- value: any,
- legacyEventType?: string,
+ private async setAccountData(
+ eventType: K,
+ field: F,
+ value: AccountDataEvents[K][F],
+ legacyEventType?: keyof AccountDataEvents,
): Promise {
let content = this.getSettings(eventType);
if (legacyEventType && !content?.[field]) {
@@ -161,7 +161,8 @@ export default class AccountSettingsHandler extends MatrixClientBackedSettingsHa
// which race between different lines.
const deferred = defer();
const handler = (event: MatrixEvent): void => {
- if (event.getType() !== eventType || !isEqual(event.getContent()[field], value)) return;
+ if (event.getType() !== eventType || !isEqual(event.getContent()[field], value))
+ return;
this.client.off(ClientEvent.AccountData, handler);
deferred.resolve();
};
@@ -212,7 +213,7 @@ export default class AccountSettingsHandler extends MatrixClientBackedSettingsHa
return this.client && !this.client.isGuest();
}
- private getSettings(eventType = "im.vector.web.settings"): any {
+ private getSettings(eventType: keyof AccountDataEvents = "im.vector.web.settings"): any {
// TODO: [TS] Types on return
if (!this.client) return null;
diff --git a/src/stores/WidgetStore.ts b/src/stores/WidgetStore.ts
index cfb92360a0d..62aac7a4298 100644
--- a/src/stores/WidgetStore.ts
+++ b/src/stores/WidgetStore.ts
@@ -17,17 +17,11 @@ import WidgetEchoStore from "../stores/WidgetEchoStore";
import ActiveWidgetStore from "../stores/ActiveWidgetStore";
import WidgetUtils from "../utils/WidgetUtils";
import { UPDATE_EVENT } from "./AsyncStore";
+import { IApp } from "../utils/WidgetUtils-types";
interface IState {}
-export interface IApp extends IWidget {
- "roomId": string;
- "eventId"?: string; // not present on virtual widgets
- // eslint-disable-next-line camelcase
- "avatar_url"?: string; // MSC2765 https://github.com/matrix-org/matrix-doc/pull/2765
- // Whether the widget was created from `widget_build_url` and thus is a call widget of some kind
- "io.element.managed_hybrid"?: boolean;
-}
+export type { IApp };
export function isAppWidget(widget: IWidget | IApp): widget is IApp {
return "roomId" in widget && typeof widget.roomId === "string";
diff --git a/src/utils/IdentityServerUtils.ts b/src/utils/IdentityServerUtils.ts
index 6fb2a100e6d..a7bd3d12be8 100644
--- a/src/utils/IdentityServerUtils.ts
+++ b/src/utils/IdentityServerUtils.ts
@@ -20,7 +20,7 @@ export function setToDefaultIdentityServer(matrixClient: MatrixClient): void {
const url = getDefaultIdentityServerUrl();
// Account data change will update localstorage, client, etc through dispatcher
matrixClient.setAccountData("m.identity_server", {
- base_url: url,
+ base_url: url ?? null,
});
}
diff --git a/src/utils/WidgetUtils-types.ts b/src/utils/WidgetUtils-types.ts
new file mode 100644
index 00000000000..56515eeddb1
--- /dev/null
+++ b/src/utils/WidgetUtils-types.ts
@@ -0,0 +1,32 @@
+/*
+Copyright 2024 New Vector Ltd.
+Copyright 2017-2020 The Matrix.org Foundation C.I.C.
+Copyright 2019 Travis Ralston
+
+SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
+Please see LICENSE files in the repository root for full details.
+*/
+
+import { IWidget } from "matrix-widget-api";
+
+export interface IApp extends IWidget {
+ "roomId": string;
+ "eventId"?: string; // not present on virtual widgets
+ // eslint-disable-next-line camelcase
+ "avatar_url"?: string; // MSC2765 https://github.com/matrix-org/matrix-doc/pull/2765
+ // Whether the widget was created from `widget_build_url` and thus is a call widget of some kind
+ "io.element.managed_hybrid"?: boolean;
+}
+
+export interface IWidgetEvent {
+ id: string;
+ type: string;
+ sender: string;
+ // eslint-disable-next-line camelcase
+ state_key: string;
+ content: IApp;
+}
+
+export interface UserWidget extends Omit {
+ content: IWidget & Partial;
+}
diff --git a/src/utils/WidgetUtils.ts b/src/utils/WidgetUtils.ts
index ad2ed63ba16..e82b8b632d8 100644
--- a/src/utils/WidgetUtils.ts
+++ b/src/utils/WidgetUtils.ts
@@ -29,23 +29,13 @@ import WidgetStore, { IApp, isAppWidget } from "../stores/WidgetStore";
import { parseUrl } from "./UrlUtils";
import { useEventEmitter } from "../hooks/useEventEmitter";
import { WidgetLayoutStore } from "../stores/widgets/WidgetLayoutStore";
+import { IWidgetEvent, UserWidget } from "./WidgetUtils-types";
// How long we wait for the state event echo to come back from the server
// before waitFor[Room/User]Widget rejects its promise
const WIDGET_WAIT_TIME = 20000;
-export interface IWidgetEvent {
- id: string;
- type: string;
- sender: string;
- // eslint-disable-next-line camelcase
- state_key: string;
- content: IApp;
-}
-
-export interface UserWidget extends Omit {
- content: IWidget & Partial;
-}
+export type { IWidgetEvent, UserWidget };
export default class WidgetUtils {
/**
diff --git a/src/utils/device/clientInformation.ts b/src/utils/device/clientInformation.ts
index 500bfdd5508..5e3f23018fd 100644
--- a/src/utils/device/clientInformation.ts
+++ b/src/utils/device/clientInformation.ts
@@ -6,17 +6,14 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
-import { MatrixClient } from "matrix-js-sdk/src/matrix";
+import { AccountDataEvents, MatrixClient } from "matrix-js-sdk/src/matrix";
import BasePlatform from "../../BasePlatform";
import { IConfigOptions } from "../../IConfigOptions";
import { DeepReadonly } from "../../@types/common";
+import { DeviceClientInformation } from "./types";
-export type DeviceClientInformation = {
- name?: string;
- version?: string;
- url?: string;
-};
+export type { DeviceClientInformation };
const formatUrl = (): string | undefined => {
// don't record url for electron clients
@@ -34,7 +31,8 @@ const formatUrl = (): string | undefined => {
};
const clientInformationEventPrefix = "io.element.matrix_client_information.";
-export const getClientInformationEventType = (deviceId: string): string => `${clientInformationEventPrefix}${deviceId}`;
+export const getClientInformationEventType = (deviceId: string): `${typeof clientInformationEventPrefix}${string}` =>
+ `${clientInformationEventPrefix}${deviceId}`;
/**
* Record extra client information for the current device
@@ -70,7 +68,7 @@ export const pruneClientInformation = (validDeviceIds: string[], matrixClient: M
}
const [, deviceId] = event.getType().split(clientInformationEventPrefix);
if (deviceId && !validDeviceIds.includes(deviceId)) {
- matrixClient.deleteAccountData(event.getType());
+ matrixClient.deleteAccountData(event.getType() as keyof AccountDataEvents);
}
});
};
diff --git a/src/utils/device/types.ts b/src/utils/device/types.ts
new file mode 100644
index 00000000000..1b652672a83
--- /dev/null
+++ b/src/utils/device/types.ts
@@ -0,0 +1,13 @@
+/*
+Copyright 2024 New Vector Ltd.
+Copyright 2022 The Matrix.org Foundation C.I.C.
+
+SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
+Please see LICENSE files in the repository root for full details.
+*/
+
+export type DeviceClientInformation = {
+ name?: string;
+ version?: string;
+ url?: string;
+};
diff --git a/src/utils/notifications.ts b/src/utils/notifications.ts
index 30d29483802..164b888d027 100644
--- a/src/utils/notifications.ts
+++ b/src/utils/notifications.ts
@@ -40,7 +40,9 @@ export const deviceNotificationSettingsKeys = [
"audioNotificationsEnabled",
];
-export function getLocalNotificationAccountDataEventType(deviceId: string | null): string {
+export function getLocalNotificationAccountDataEventType(
+ deviceId: string | null,
+): `${typeof LOCAL_NOTIFICATION_SETTINGS_PREFIX.name}.${string}` {
return `${LOCAL_NOTIFICATION_SETTINGS_PREFIX.name}.${deviceId}`;
}
diff --git a/test/unit-tests/Notifier-test.ts b/test/unit-tests/Notifier-test.ts
index f94f50724d0..4c4897dbc38 100644
--- a/test/unit-tests/Notifier-test.ts
+++ b/test/unit-tests/Notifier-test.ts
@@ -16,6 +16,7 @@ import {
IContent,
MatrixEvent,
SyncState,
+ AccountDataEvents,
} from "matrix-js-sdk/src/matrix";
import { waitFor } from "jest-matrix-react";
import { CallMembership, MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc";
@@ -69,7 +70,7 @@ describe("Notifier", () => {
let MockPlatform: MockedObject;
let mockClient: MockedObject;
let testRoom: Room;
- let accountDataEventKey: string;
+ let accountDataEventKey: keyof AccountDataEvents;
let accountDataStore: Record = {};
let mockSettings: Record = {};
diff --git a/test/unit-tests/components/views/right_panel/RoomSummaryCard-test.tsx b/test/unit-tests/components/views/right_panel/RoomSummaryCard-test.tsx
index 80d2609577e..52e7b5a118c 100644
--- a/test/unit-tests/components/views/right_panel/RoomSummaryCard-test.tsx
+++ b/test/unit-tests/components/views/right_panel/RoomSummaryCard-test.tsx
@@ -338,19 +338,18 @@ describe("", () => {
});
it("does not show public room label for a DM", async () => {
- mockClient.getAccountData.mockImplementation(
- (eventType) =>
- ({
- [EventType.Direct]: new MatrixEvent({
- type: EventType.Direct,
- content: {
- "@bob:sesame.st": ["some-room-id"],
- // this room is a DM with ernie
- "@ernie:sesame.st": ["some-other-room-id", room.roomId],
- },
- }),
- })[eventType],
- );
+ mockClient.getAccountData.mockImplementation((eventType) => {
+ if (eventType === EventType.Direct) {
+ return new MatrixEvent({
+ type: EventType.Direct,
+ content: {
+ "@bob:sesame.st": ["some-room-id"],
+ // this room is a DM with ernie
+ "@ernie:sesame.st": ["some-other-room-id", room.roomId],
+ },
+ });
+ }
+ });
getComponent();
await flushPromises();
diff --git a/test/unit-tests/utils/notifications-test.ts b/test/unit-tests/utils/notifications-test.ts
index 6cf6e3496b7..2432d477a97 100644
--- a/test/unit-tests/utils/notifications-test.ts
+++ b/test/unit-tests/utils/notifications-test.ts
@@ -6,7 +6,14 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
-import { MatrixEvent, NotificationCountType, Room, MatrixClient, ReceiptType } from "matrix-js-sdk/src/matrix";
+import {
+ MatrixEvent,
+ NotificationCountType,
+ Room,
+ MatrixClient,
+ ReceiptType,
+ AccountDataEvents,
+} from "matrix-js-sdk/src/matrix";
import { Mocked, mocked } from "jest-mock";
import {
@@ -32,7 +39,7 @@ jest.mock("../../../src/settings/SettingsStore");
describe("notifications", () => {
let accountDataStore: Record = {};
let mockClient: Mocked;
- let accountDataEventKey: string;
+ let accountDataEventKey: keyof AccountDataEvents;
beforeEach(() => {
jest.clearAllMocks();
From 5448de5dd6ff3fbe9f2920a9dc4a71ebe02d9cce Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Thu, 19 Dec 2024 23:42:09 +0000
Subject: [PATCH 6/6] Run Playwright tests on Firefox & "Safari" nightly
(#28757)
* Run Playwright tests on Firefox
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
* Update playwright.config.ts
* Iterate
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
* Iterate
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
* Iterate
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
* Iterate
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
* Iterate
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
* Iterate
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
* Iterate
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
* Iterate
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
* Iterate
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
* Iterate
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
* Iterate
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
* Iterate
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
* Iterate
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
* Update end-to-end-tests.yaml
* Iterate
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
* Finalise
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
* Documentation
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
* typo
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---------
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
.github/workflows/end-to-end-tests.yaml | 49 ++++-
docs/playwright.md | 23 ++-
package.json | 2 +-
playwright.config.ts | 43 ++++-
.../e2e/audio-player/audio-player.spec.ts | 2 +-
playwright/e2e/crypto/backups.spec.ts | 176 +++++++++---------
.../e2e/crypto/device-verification.spec.ts | 2 +-
playwright/e2e/crypto/migration.spec.ts | 2 +-
playwright/e2e/location/location.spec.ts | 3 +-
playwright/e2e/oidc/oidc-aware.spec.ts | 2 +-
playwright/e2e/oidc/oidc-native.spec.ts | 2 +-
playwright/e2e/register/register.spec.ts | 4 +-
playwright/e2e/right-panel/file-panel.spec.ts | 2 +-
.../e2e/room-directory/room-directory.spec.ts | 68 ++++---
.../general-room-settings-tab.spec.ts | 2 +-
.../preferences-user-settings-tab.spec.ts | 2 +-
playwright/e2e/spaces/spaces.spec.ts | 72 +++----
.../threadsActivityCentre.spec.ts | 2 +-
playwright/e2e/threads/threads.spec.ts | 4 +-
playwright/e2e/timeline/timeline.spec.ts | 5 +-
playwright/e2e/widgets/stickers.spec.ts | 2 +-
playwright/element-web-test.ts | 8 +
playwright/plugins/docker/index.ts | 8 +-
23 files changed, 295 insertions(+), 190 deletions(-)
diff --git a/.github/workflows/end-to-end-tests.yaml b/.github/workflows/end-to-end-tests.yaml
index 5a75040866b..6afabdb1fe6 100644
--- a/.github/workflows/end-to-end-tests.yaml
+++ b/.github/workflows/end-to-end-tests.yaml
@@ -3,6 +3,9 @@
# as an artifact and run end-to-end tests.
name: End to End Tests
on:
+ # CRON to run all Projects at 6am UTC
+ schedule:
+ - cron: "0 6 * * *"
pull_request: {}
merge_group:
types: [checks_requested]
@@ -32,6 +35,8 @@ concurrency:
env:
# fetchdep.sh needs to know our PR number
PR_NUMBER: ${{ github.event.pull_request.number }}
+ # Use 6 runners in the default case, but 4 when running on a schedule where we run all 5 projects (20 runners total)
+ NUM_RUNNERS: ${{ github.event_name == 'schedule' && 4 || 6 }}
permissions: {} # No permissions required
@@ -40,6 +45,9 @@ jobs:
name: "Build Element-Web"
runs-on: ubuntu-24.04
if: inputs.skip != true
+ outputs:
+ num-runners: ${{ env.NUM_RUNNERS }}
+ runners-matrix: ${{ steps.runner-vars.outputs.matrix }}
steps:
- name: Checkout code
uses: actions/checkout@v4
@@ -79,8 +87,17 @@ jobs:
path: webapp
retention-days: 1
+ - name: Calculate runner variables
+ id: runner-vars
+ uses: actions/github-script@v7
+ with:
+ script: |
+ const numRunners = parseInt(process.env.NUM_RUNNERS, 10);
+ const matrix = Array.from({ length: numRunners }, (_, i) => i + 1);
+ core.setOutput("matrix", JSON.stringify(matrix));
+
playwright:
- name: "Run Tests ${{ matrix.runner }}/${{ strategy.job-total }}"
+ name: "Run Tests [${{ matrix.project }}] ${{ matrix.runner }}/${{ needs.build.outputs.num-runners }}"
needs: build
if: inputs.skip != true
runs-on: ubuntu-24.04
@@ -92,7 +109,19 @@ jobs:
fail-fast: false
matrix:
# Run multiple instances in parallel to speed up the tests
- runner: [1, 2, 3, 4, 5, 6]
+ runner: ${{ fromJSON(needs.build.outputs.runners-matrix) }}
+ project:
+ - Chrome
+ - Firefox
+ - WebKit
+ isCron:
+ - ${{ github.event_name == 'schedule' }}
+ # Skip the Firefox & Safari runs unless this was a cron trigger
+ exclude:
+ - isCron: false
+ project: Firefox
+ - isCron: false
+ project: WebKit
steps:
- uses: actions/checkout@v4
with:
@@ -124,24 +153,30 @@ jobs:
with:
path: |
~/.cache/ms-playwright
- key: ${{ runner.os }}-playwright-${{ steps.playwright.outputs.version }}-chromium
+ key: ${{ runner.os }}-playwright-${{ steps.playwright.outputs.version }}
- - name: Install Playwright browser
+ - name: Install Playwright browsers
if: steps.playwright-cache.outputs.cache-hit != 'true'
- run: yarn playwright install --with-deps --no-shell chromium
+ run: yarn playwright install --with-deps --no-shell
+
+ - name: Install system dependencies for WebKit
+ # Some WebKit dependencies seem to lay outside the cache and will need to be installed separately
+ if: matrix.project == 'WebKit' && steps.playwright-cache.outputs.cache-hit == 'true'
+ run: yarn playwright install-deps webkit
# We skip tests tagged with @mergequeue when running on PRs, but run them in MQ and everywhere else
- name: Run Playwright tests
run: |
yarn playwright test \
- --shard "${{ matrix.runner }}/${{ strategy.job-total }}" \
+ --shard "${{ matrix.runner }}/${{ needs.build.outputs.num-runners }}" \
+ --project="${{ matrix.project }}" \
${{ github.event_name == 'pull_request' && '--grep-invert @mergequeue' || '' }}
- name: Upload blob report to GitHub Actions Artifacts
if: always()
uses: actions/upload-artifact@v4
with:
- name: all-blob-reports-${{ matrix.runner }}
+ name: all-blob-reports-${{ matrix.project }}-${{ matrix.runner }}
path: blob-report
retention-days: 1
diff --git a/docs/playwright.md b/docs/playwright.md
index 4af3194220a..73ee77228b7 100644
--- a/docs/playwright.md
+++ b/docs/playwright.md
@@ -53,15 +53,11 @@ yarn run test:playwright:open --headed --debug
See more command line options at .
-### Running with Rust cryptography
+## Projects
-`matrix-js-sdk` is currently in the
-[process](https://github.com/vector-im/element-web/issues/21972) of being
-updated to replace its end-to-end encryption implementation to use the [Matrix
-Rust SDK](https://github.com/matrix-org/matrix-rust-sdk). This is not currently
-enabled by default, but it is possible to have Playwright configure Element to use
-the Rust crypto implementation by passing `--project="Rust Crypto"` or using
-the top left options in open mode.
+By default, Playwright will run all "Projects", this means tests will run against Chrome, Firefox and "Safari" (Webkit).
+We only run tests against Chrome in pull request CI, but all projects in the merge queue.
+Some tests are excluded from running on certain browsers due to incompatibilities in the test harness.
## How the Tests Work
@@ -224,3 +220,14 @@ We use test tags to categorise tests for running subsets more efficiently.
- `@mergequeue`: Tests that are slow or flaky and cover areas of the app we update seldom, should not be run on every PR commit but will be run in the Merge Queue.
- `@screenshot`: Tests that use `toMatchScreenshot` to speed up a run of `test:playwright:screenshots`. A test with this tag must not also have the `@mergequeue` tag as this would cause false positives in the stale screenshot detection.
+- `@no-$project`: Tests which are unsupported in $Project. These tests will be skipped when running in $Project.
+
+Anything testing Matrix media will need to have `@no-firefox` and `@no-webkit` as those rely on the service worker which
+has to be disabled in Playwright on Firefox & Webkit to retain routing functionality.
+Anything testing VoIP/microphone will need to have `@no-webkit` as fake microphone functionality is not available
+there at this time.
+
+## Colima
+
+If you are running under Colima, you may need to set the environment variable `TMPDIR` to `/tmp/colima` or a path
+within `$HOME` to allow bind mounting temporary directories into the Docker containers.
diff --git a/package.json b/package.json
index 999d1237f94..f41993a687a 100644
--- a/package.json
+++ b/package.json
@@ -64,7 +64,7 @@
"test:playwright:open": "yarn test:playwright --ui",
"test:playwright:screenshots": "yarn test:playwright:screenshots:build && yarn test:playwright:screenshots:run",
"test:playwright:screenshots:build": "docker build playwright -t element-web-playwright",
- "test:playwright:screenshots:run": "docker run --rm --network host -e BASE_URL -e CI -v $(pwd):/work/ -v $(node -e 'console.log(require(`path`).dirname(require.resolve(`matrix-js-sdk/package.json`)))'):/work/node_modules/matrix-js-sdk -v /var/run/docker.sock:/var/run/docker.sock -v /tmp/:/tmp/ -it element-web-playwright --grep @screenshot",
+ "test:playwright:screenshots:run": "docker run --rm --network host -e BASE_URL -e CI -v $(pwd):/work/ -v $(node -e 'console.log(require(`path`).dirname(require.resolve(`matrix-js-sdk/package.json`)))'):/work/node_modules/matrix-js-sdk -v /var/run/docker.sock:/var/run/docker.sock -v /tmp/:/tmp/ -it element-web-playwright --grep @screenshot --project=Chrome",
"coverage": "yarn test --coverage",
"analyse:unused-exports": "ts-node ./scripts/analyse_unused_exports.ts",
"analyse:webpack-bundles": "webpack-bundle-analyzer webpack-stats.json webapp",
diff --git a/playwright.config.ts b/playwright.config.ts
index 06c1b05322d..0b2bd1bd02d 100644
--- a/playwright.config.ts
+++ b/playwright.config.ts
@@ -11,16 +11,49 @@ import { defineConfig, devices } from "@playwright/test";
const baseURL = process.env["BASE_URL"] ?? "http://localhost:8080";
export default defineConfig({
- projects: [{ name: "Chrome", use: { ...devices["Desktop Chrome"], channel: "chromium" } }],
+ projects: [
+ {
+ name: "Chrome",
+ use: {
+ ...devices["Desktop Chrome"],
+ channel: "chromium",
+ permissions: ["clipboard-write", "clipboard-read", "microphone"],
+ launchOptions: {
+ args: ["--use-fake-ui-for-media-stream", "--use-fake-device-for-media-stream", "--mute-audio"],
+ },
+ },
+ },
+ {
+ name: "Firefox",
+ use: {
+ ...devices["Desktop Firefox"],
+ launchOptions: {
+ firefoxUserPrefs: {
+ "permissions.default.microphone": 1,
+ },
+ },
+ // This is needed to work around an issue between Playwright routes, Firefox, and Service workers
+ // https://github.com/microsoft/playwright/issues/33561#issuecomment-2471642120
+ serviceWorkers: "block",
+ },
+ ignoreSnapshots: true,
+ },
+ {
+ name: "WebKit",
+ use: {
+ ...devices["Desktop Safari"],
+ // Seemingly WebKit has the same issue as Firefox in Playwright routes not working
+ // https://playwright.dev/docs/network#missing-network-events-and-service-workers
+ serviceWorkers: "block",
+ },
+ ignoreSnapshots: true,
+ },
+ ],
use: {
viewport: { width: 1280, height: 720 },
ignoreHTTPSErrors: true,
video: "retain-on-failure",
baseURL,
- permissions: ["clipboard-write", "clipboard-read", "microphone"],
- launchOptions: {
- args: ["--use-fake-ui-for-media-stream", "--use-fake-device-for-media-stream", "--mute-audio"],
- },
trace: "on-first-retry",
},
webServer: {
diff --git a/playwright/e2e/audio-player/audio-player.spec.ts b/playwright/e2e/audio-player/audio-player.spec.ts
index 2bb9ab0be45..2749d7eb1d5 100644
--- a/playwright/e2e/audio-player/audio-player.spec.ts
+++ b/playwright/e2e/audio-player/audio-player.spec.ts
@@ -13,7 +13,7 @@ import { SettingLevel } from "../../../src/settings/SettingLevel";
import { Layout } from "../../../src/settings/enums/Layout";
import { ElementAppPage } from "../../pages/ElementAppPage";
-test.describe("Audio player", () => {
+test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
test.use({
displayName: "Hanako",
});
diff --git a/playwright/e2e/crypto/backups.spec.ts b/playwright/e2e/crypto/backups.spec.ts
index d174cc89e5a..40c7dc0ac6c 100644
--- a/playwright/e2e/crypto/backups.spec.ts
+++ b/playwright/e2e/crypto/backups.spec.ts
@@ -51,90 +51,94 @@ test.describe("Backups", () => {
displayName: "Hanako",
});
- test("Create, delete and recreate a keys backup", async ({ page, user, app }, workerInfo) => {
- // Create a backup
- const securityTab = await app.settings.openUserSettings("Security & Privacy");
-
- await expect(securityTab.getByRole("heading", { name: "Secure Backup" })).toBeVisible();
- await securityTab.getByRole("button", { name: "Set up", exact: true }).click();
-
- 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");
- await expect(securityTab.getByRole("heading", { name: "Secure Backup" })).toBeVisible();
-
- // expand the advanced section to see the active version in the reports
- await page
- .locator(".mx_Dialog .mx_SettingsSubsection_content details .mx_SecureBackupPanel_advanced")
- .locator("..")
- .click();
-
- await expectBackupVersionToBe(page, "1");
-
- await securityTab.getByRole("button", { name: "Delete Backup", exact: true }).click();
- await expect(currentDialogLocator.getByRole("heading", { name: "Delete Backup" })).toBeVisible();
- // Delete it
- await currentDialogLocator.getByTestId("dialog-primary-button").click(); // Click "Delete Backup"
-
- // Create another
- await securityTab.getByRole("button", { name: "Set up", exact: true }).click();
- await expect(currentDialogLocator.getByRole("heading", { name: "Security Key" })).toBeVisible();
- await currentDialogLocator.getByLabel("Security Key").fill(securityKey);
- await currentDialogLocator.getByRole("button", { name: "Continue", exact: true }).click();
-
- // Should be successful
- await expect(currentDialogLocator.getByRole("heading", { name: "Success!" })).toBeVisible();
- await currentDialogLocator.getByRole("button", { name: "OK", exact: true }).click();
-
- // Open the settings again
- await app.settings.openUserSettings("Security & Privacy");
- await expect(securityTab.getByRole("heading", { name: "Secure Backup" })).toBeVisible();
-
- // expand the advanced section to see the active version in the reports
- await page
- .locator(".mx_Dialog .mx_SettingsSubsection_content details .mx_SecureBackupPanel_advanced")
- .locator("..")
- .click();
-
- await expectBackupVersionToBe(page, "2");
-
- // ==
- // Ensure that if you don't have the secret storage passphrase the backup won't be created
- // ==
-
- // First delete version 2
- await securityTab.getByRole("button", { name: "Delete Backup", exact: true }).click();
- await expect(currentDialogLocator.getByRole("heading", { name: "Delete Backup" })).toBeVisible();
- // Click "Delete Backup"
- await currentDialogLocator.getByTestId("dialog-primary-button").click();
-
- // Try to create another
- await securityTab.getByRole("button", { name: "Set up", exact: true }).click();
- await expect(currentDialogLocator.getByRole("heading", { name: "Security Key" })).toBeVisible();
- // But cancel the security key dialog, to simulate not having the secret storage passphrase
- await currentDialogLocator.getByTestId("dialog-cancel-button").click();
-
- await expect(currentDialogLocator.getByRole("heading", { name: "Starting backup…" })).toBeVisible();
- // check that it failed
- await expect(currentDialogLocator.getByText("Unable to create key backup")).toBeVisible();
- // cancel
- await currentDialogLocator.getByTestId("dialog-cancel-button").click();
-
- // go back to the settings to check that no backup was created (the setup button should still be there)
- await app.settings.openUserSettings("Security & Privacy");
- await expect(securityTab.getByRole("button", { name: "Set up", exact: true })).toBeVisible();
- });
+ test(
+ "Create, delete and recreate a keys backup",
+ { tag: "@no-webkit" },
+ async ({ page, user, app }, workerInfo) => {
+ // Create a backup
+ const securityTab = await app.settings.openUserSettings("Security & Privacy");
+
+ await expect(securityTab.getByRole("heading", { name: "Secure Backup" })).toBeVisible();
+ await securityTab.getByRole("button", { name: "Set up", exact: true }).click();
+
+ 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");
+ await expect(securityTab.getByRole("heading", { name: "Secure Backup" })).toBeVisible();
+
+ // expand the advanced section to see the active version in the reports
+ await page
+ .locator(".mx_Dialog .mx_SettingsSubsection_content details .mx_SecureBackupPanel_advanced")
+ .locator("..")
+ .click();
+
+ await expectBackupVersionToBe(page, "1");
+
+ await securityTab.getByRole("button", { name: "Delete Backup", exact: true }).click();
+ await expect(currentDialogLocator.getByRole("heading", { name: "Delete Backup" })).toBeVisible();
+ // Delete it
+ await currentDialogLocator.getByTestId("dialog-primary-button").click(); // Click "Delete Backup"
+
+ // Create another
+ await securityTab.getByRole("button", { name: "Set up", exact: true }).click();
+ await expect(currentDialogLocator.getByRole("heading", { name: "Security Key" })).toBeVisible();
+ await currentDialogLocator.getByLabel("Security Key").fill(securityKey);
+ await currentDialogLocator.getByRole("button", { name: "Continue", exact: true }).click();
+
+ // Should be successful
+ await expect(currentDialogLocator.getByRole("heading", { name: "Success!" })).toBeVisible();
+ await currentDialogLocator.getByRole("button", { name: "OK", exact: true }).click();
+
+ // Open the settings again
+ await app.settings.openUserSettings("Security & Privacy");
+ await expect(securityTab.getByRole("heading", { name: "Secure Backup" })).toBeVisible();
+
+ // expand the advanced section to see the active version in the reports
+ await page
+ .locator(".mx_Dialog .mx_SettingsSubsection_content details .mx_SecureBackupPanel_advanced")
+ .locator("..")
+ .click();
+
+ await expectBackupVersionToBe(page, "2");
+
+ // ==
+ // Ensure that if you don't have the secret storage passphrase the backup won't be created
+ // ==
+
+ // First delete version 2
+ await securityTab.getByRole("button", { name: "Delete Backup", exact: true }).click();
+ await expect(currentDialogLocator.getByRole("heading", { name: "Delete Backup" })).toBeVisible();
+ // Click "Delete Backup"
+ await currentDialogLocator.getByTestId("dialog-primary-button").click();
+
+ // Try to create another
+ await securityTab.getByRole("button", { name: "Set up", exact: true }).click();
+ await expect(currentDialogLocator.getByRole("heading", { name: "Security Key" })).toBeVisible();
+ // But cancel the security key dialog, to simulate not having the secret storage passphrase
+ await currentDialogLocator.getByTestId("dialog-cancel-button").click();
+
+ await expect(currentDialogLocator.getByRole("heading", { name: "Starting backup…" })).toBeVisible();
+ // check that it failed
+ await expect(currentDialogLocator.getByText("Unable to create key backup")).toBeVisible();
+ // cancel
+ await currentDialogLocator.getByTestId("dialog-cancel-button").click();
+
+ // go back to the settings to check that no backup was created (the setup button should still be there)
+ await app.settings.openUserSettings("Security & Privacy");
+ await expect(securityTab.getByRole("button", { name: "Set up", exact: true })).toBeVisible();
+ },
+ );
});
diff --git a/playwright/e2e/crypto/device-verification.spec.ts b/playwright/e2e/crypto/device-verification.spec.ts
index 83a81c260cd..032b649b8d7 100644
--- a/playwright/e2e/crypto/device-verification.spec.ts
+++ b/playwright/e2e/crypto/device-verification.spec.ts
@@ -21,7 +21,7 @@ import {
} from "./utils";
import { Bot } from "../../pages/bot";
-test.describe("Device verification", () => {
+test.describe("Device verification", { tag: "@no-webkit" }, () => {
let aliceBotClient: Bot;
/** The backup version that was set up by the bot client. */
diff --git a/playwright/e2e/crypto/migration.spec.ts b/playwright/e2e/crypto/migration.spec.ts
index a9530a288b7..52df22688bb 100644
--- a/playwright/e2e/crypto/migration.spec.ts
+++ b/playwright/e2e/crypto/migration.spec.ts
@@ -25,7 +25,7 @@ const test = base.extend({
},
});
-test.describe("migration", function () {
+test.describe("migration", { tag: "@no-webkit" }, function () {
test.use({ displayName: "Alice" });
test("Should support migration from legacy crypto", async ({ context, user, page }, workerInfo) => {
diff --git a/playwright/e2e/location/location.spec.ts b/playwright/e2e/location/location.spec.ts
index e0c23d6c22f..2277c16d4fc 100644
--- a/playwright/e2e/location/location.spec.ts
+++ b/playwright/e2e/location/location.spec.ts
@@ -10,7 +10,8 @@ import { Locator, Page } from "@playwright/test";
import { test, expect } from "../../element-web-test";
-test.describe("Location sharing", () => {
+// Firefox headless lacks WebGL support https://bugzilla.mozilla.org/show_bug.cgi?id=1375585
+test.describe("Location sharing", { tag: "@no-firefox" }, () => {
const selectLocationShareTypeOption = (page: Page, shareType: string): Locator => {
return page.getByTestId(`share-location-option-${shareType}`);
};
diff --git a/playwright/e2e/oidc/oidc-aware.spec.ts b/playwright/e2e/oidc/oidc-aware.spec.ts
index a2f1e62714c..7b155f27a4d 100644
--- a/playwright/e2e/oidc/oidc-aware.spec.ts
+++ b/playwright/e2e/oidc/oidc-aware.spec.ts
@@ -9,7 +9,7 @@ Please see LICENSE files in the repository root for full details.
import { test, expect, registerAccountMas } from ".";
import { isDendrite } from "../../plugins/homeserver/dendrite";
-test.describe("OIDC Aware", () => {
+test.describe("OIDC Aware", { tag: ["@no-firefox", "@no-webkit"] }, () => {
test.skip(isDendrite, "does not yet support MAS");
test.slow(); // trace recording takes a while here
diff --git a/playwright/e2e/oidc/oidc-native.spec.ts b/playwright/e2e/oidc/oidc-native.spec.ts
index 3309826b634..2ae5cf83e60 100644
--- a/playwright/e2e/oidc/oidc-native.spec.ts
+++ b/playwright/e2e/oidc/oidc-native.spec.ts
@@ -10,7 +10,7 @@ import { test, expect, registerAccountMas } from ".";
import { isDendrite } from "../../plugins/homeserver/dendrite";
import { ElementAppPage } from "../../pages/ElementAppPage.ts";
-test.describe("OIDC Native", () => {
+test.describe("OIDC Native", { tag: ["@no-firefox", "@no-webkit"] }, () => {
test.skip(isDendrite, "does not yet support MAS");
test.slow(); // trace recording takes a while here
diff --git a/playwright/e2e/register/register.spec.ts b/playwright/e2e/register/register.spec.ts
index 19608ee174d..c1274362668 100644
--- a/playwright/e2e/register/register.spec.ts
+++ b/playwright/e2e/register/register.spec.ts
@@ -9,7 +9,9 @@ Please see LICENSE files in the repository root for full details.
import { test, expect } from "../../element-web-test";
test.describe("Registration", () => {
- test.use({ startHomeserverOpts: "consent" });
+ test.use({
+ startHomeserverOpts: "consent",
+ });
test.beforeEach(async ({ page }) => {
await page.goto("/#/register");
diff --git a/playwright/e2e/right-panel/file-panel.spec.ts b/playwright/e2e/right-panel/file-panel.spec.ts
index c535bcdfbb6..c5a106d8412 100644
--- a/playwright/e2e/right-panel/file-panel.spec.ts
+++ b/playwright/e2e/right-panel/file-panel.spec.ts
@@ -39,7 +39,7 @@ test.describe("FilePanel", () => {
await expect(page.locator(".mx_FilePanel")).toBeVisible();
});
- test.describe("render", () => {
+ test.describe("render", { tag: ["@no-firefox", "@no-webkit"] }, () => {
test("should render empty state", { tag: "@screenshot" }, async ({ page }) => {
// Wait until the information about the empty state is rendered
await expect(page.locator(".mx_EmptyState")).toBeVisible();
diff --git a/playwright/e2e/room-directory/room-directory.spec.ts b/playwright/e2e/room-directory/room-directory.spec.ts
index f299a929bb7..b3d2cf0ee99 100644
--- a/playwright/e2e/room-directory/room-directory.spec.ts
+++ b/playwright/e2e/room-directory/room-directory.spec.ts
@@ -15,37 +15,43 @@ test.describe("Room Directory", () => {
botCreateOpts: { displayName: "Paul" },
});
- test("should allow admin to add alias & publish room to directory", async ({ page, app, user, bot }) => {
- const roomId = await app.client.createRoom({
- name: "Gaming",
- preset: "public_chat" as Preset,
- });
-
- await app.viewRoomByName("Gaming");
- await app.settings.openRoomSettings();
-
- // First add a local address `gaming`
- const localAddresses = page.locator(".mx_SettingsFieldset", { hasText: "Local Addresses" });
- await localAddresses.getByRole("textbox").fill("gaming");
- await localAddresses.getByRole("button", { name: "Add" }).click();
- await expect(localAddresses.getByText("#gaming:localhost")).toHaveClass("mx_EditableItem_item");
-
- // Publish into the public rooms directory
- const publishedAddresses = page.locator(".mx_SettingsFieldset", { hasText: "Published Addresses" });
- await expect(publishedAddresses.locator("#canonicalAlias")).toHaveValue("#gaming:localhost");
- const checkbox = publishedAddresses
- .locator(".mx_SettingsFlag", { hasText: "Publish this room to the public in localhost's room directory?" })
- .getByRole("switch");
- await checkbox.check();
- await expect(checkbox).toBeChecked();
-
- await app.closeDialog();
-
- const resp = await bot.publicRooms({});
- expect(resp.total_room_count_estimate).toEqual(1);
- expect(resp.chunk).toHaveLength(1);
- expect(resp.chunk[0].room_id).toEqual(roomId);
- });
+ test(
+ "should allow admin to add alias & publish room to directory",
+ { tag: "@no-webkit" },
+ async ({ page, app, user, bot }) => {
+ const roomId = await app.client.createRoom({
+ name: "Gaming",
+ preset: "public_chat" as Preset,
+ });
+
+ await app.viewRoomByName("Gaming");
+ await app.settings.openRoomSettings();
+
+ // First add a local address `gaming`
+ const localAddresses = page.locator(".mx_SettingsFieldset", { hasText: "Local Addresses" });
+ await localAddresses.getByRole("textbox").fill("gaming");
+ await localAddresses.getByRole("button", { name: "Add" }).click();
+ await expect(localAddresses.getByText("#gaming:localhost")).toHaveClass("mx_EditableItem_item");
+
+ // Publish into the public rooms directory
+ const publishedAddresses = page.locator(".mx_SettingsFieldset", { hasText: "Published Addresses" });
+ await expect(publishedAddresses.locator("#canonicalAlias")).toHaveValue("#gaming:localhost");
+ const checkbox = publishedAddresses
+ .locator(".mx_SettingsFlag", {
+ hasText: "Publish this room to the public in localhost's room directory?",
+ })
+ .getByRole("switch");
+ await checkbox.check();
+ await expect(checkbox).toBeChecked();
+
+ await app.closeDialog();
+
+ const resp = await bot.publicRooms({});
+ expect(resp.total_room_count_estimate).toEqual(1);
+ expect(resp.chunk).toHaveLength(1);
+ expect(resp.chunk[0].room_id).toEqual(roomId);
+ },
+ );
test(
"should allow finding published rooms in directory",
diff --git a/playwright/e2e/settings/general-room-settings-tab.spec.ts b/playwright/e2e/settings/general-room-settings-tab.spec.ts
index 828ba5285bb..7102a258bca 100644
--- a/playwright/e2e/settings/general-room-settings-tab.spec.ts
+++ b/playwright/e2e/settings/general-room-settings-tab.spec.ts
@@ -36,7 +36,7 @@ test.describe("General room settings tab", () => {
await expect(settings.getByText("Show more")).toBeVisible();
});
- test("long address should not cause dialog to overflow", async ({ page, app }) => {
+ test("long address should not cause dialog to overflow", { tag: "@no-webkit" }, async ({ page, app }) => {
const settings = await app.settings.openRoomSettings("General");
// 1. Set the room-address to be a really long string
const longString = "abcasdhjasjhdaj1jh1asdhasjdhajsdhjavhjksd".repeat(4);
diff --git a/playwright/e2e/settings/preferences-user-settings-tab.spec.ts b/playwright/e2e/settings/preferences-user-settings-tab.spec.ts
index 8dc2570b426..fb2dae4eb07 100644
--- a/playwright/e2e/settings/preferences-user-settings-tab.spec.ts
+++ b/playwright/e2e/settings/preferences-user-settings-tab.spec.ts
@@ -31,7 +31,7 @@ test.describe("Preferences user settings tab", () => {
await expect(tab).toMatchScreenshot("Preferences-user-settings-tab-should-be-rendered-properly-1.png");
});
- test("should be able to change the app language", async ({ uut, user }) => {
+ test("should be able to change the app language", { tag: ["@no-firefox", "@no-webkit"] }, async ({ uut, user }) => {
// Check language and region setting dropdown
const languageInput = uut.getByRole("button", { name: "Language Dropdown" });
await languageInput.scrollIntoViewIfNeeded();
diff --git a/playwright/e2e/spaces/spaces.spec.ts b/playwright/e2e/spaces/spaces.spec.ts
index 233cdee3b4b..374fc6b0687 100644
--- a/playwright/e2e/spaces/spaces.spec.ts
+++ b/playwright/e2e/spaces/spaces.spec.ts
@@ -55,38 +55,44 @@ test.describe("Spaces", () => {
botCreateOpts: { displayName: "BotBob" },
});
- test("should allow user to create public space", { tag: "@screenshot" }, async ({ page, app, user }) => {
- const contextMenu = await openSpaceCreateMenu(page);
- await expect(contextMenu).toMatchScreenshot("space-create-menu.png");
-
- await contextMenu.getByRole("button", { name: /Public/ }).click();
-
- await contextMenu
- .locator('.mx_SpaceBasicSettings_avatarContainer input[type="file"]')
- .setInputFiles("playwright/sample-files/riot.png");
- await contextMenu.getByRole("textbox", { name: "Name" }).fill("Let's have a Riot");
- await expect(contextMenu.getByRole("textbox", { name: "Address" })).toHaveValue("lets-have-a-riot");
- await contextMenu.getByRole("textbox", { name: "Description" }).fill("This is a space to reminisce Riot.im!");
- await contextMenu.getByRole("button", { name: "Create" }).click();
-
- // Create the default General & Random rooms, as well as a custom "Jokes" room
- await expect(page.getByPlaceholder("General")).toBeVisible();
- await expect(page.getByPlaceholder("Random")).toBeVisible();
- await page.getByPlaceholder("Support").fill("Jokes");
- await page.getByRole("button", { name: "Continue" }).click();
-
- // Copy matrix.to link
- await page.getByRole("button", { name: "Share invite link" }).click();
- expect(await app.getClipboardText()).toEqual("https://matrix.to/#/#lets-have-a-riot:localhost");
-
- // Go to space home
- await page.getByRole("button", { name: "Go to my first room" }).click();
-
- // Assert rooms exist in the room list
- await expect(page.getByRole("treeitem", { name: "General" })).toBeVisible();
- await expect(page.getByRole("treeitem", { name: "Random" })).toBeVisible();
- await expect(page.getByRole("treeitem", { name: "Jokes" })).toBeVisible();
- });
+ test(
+ "should allow user to create public space",
+ { tag: ["@screenshot", "@no-webkit"] },
+ async ({ page, app, user }) => {
+ const contextMenu = await openSpaceCreateMenu(page);
+ await expect(contextMenu).toMatchScreenshot("space-create-menu.png");
+
+ await contextMenu.getByRole("button", { name: /Public/ }).click();
+
+ await contextMenu
+ .locator('.mx_SpaceBasicSettings_avatarContainer input[type="file"]')
+ .setInputFiles("playwright/sample-files/riot.png");
+ await contextMenu.getByRole("textbox", { name: "Name" }).fill("Let's have a Riot");
+ await expect(contextMenu.getByRole("textbox", { name: "Address" })).toHaveValue("lets-have-a-riot");
+ await contextMenu
+ .getByRole("textbox", { name: "Description" })
+ .fill("This is a space to reminisce Riot.im!");
+ await contextMenu.getByRole("button", { name: "Create" }).click();
+
+ // Create the default General & Random rooms, as well as a custom "Jokes" room
+ await expect(page.getByPlaceholder("General")).toBeVisible();
+ await expect(page.getByPlaceholder("Random")).toBeVisible();
+ await page.getByPlaceholder("Support").fill("Jokes");
+ await page.getByRole("button", { name: "Continue" }).click();
+
+ // Copy matrix.to link
+ await page.getByRole("button", { name: "Share invite link" }).click();
+ expect(await app.getClipboardText()).toEqual("https://matrix.to/#/#lets-have-a-riot:localhost");
+
+ // Go to space home
+ await page.getByRole("button", { name: "Go to my first room" }).click();
+
+ // Assert rooms exist in the room list
+ await expect(page.getByRole("treeitem", { name: "General" })).toBeVisible();
+ await expect(page.getByRole("treeitem", { name: "Random" })).toBeVisible();
+ await expect(page.getByRole("treeitem", { name: "Jokes" })).toBeVisible();
+ },
+ );
test("should allow user to create private space", { tag: "@screenshot" }, async ({ page, app, user }) => {
const menu = await openSpaceCreateMenu(page);
@@ -157,7 +163,7 @@ test.describe("Spaces", () => {
).toBeVisible();
});
- test("should allow user to invite another to a space", async ({ page, app, user, bot }) => {
+ test("should allow user to invite another to a space", { tag: "@no-webkit" }, async ({ page, app, user, bot }) => {
await app.client.createSpace({
visibility: "public" as any,
room_alias_name: "space",
diff --git a/playwright/e2e/spaces/threads-activity-centre/threadsActivityCentre.spec.ts b/playwright/e2e/spaces/threads-activity-centre/threadsActivityCentre.spec.ts
index ecf458c0600..965047e75ef 100644
--- a/playwright/e2e/spaces/threads-activity-centre/threadsActivityCentre.spec.ts
+++ b/playwright/e2e/spaces/threads-activity-centre/threadsActivityCentre.spec.ts
@@ -9,7 +9,7 @@
import { expect, test } from ".";
import { CommandOrControl } from "../../utils";
-test.describe("Threads Activity Centre", () => {
+test.describe("Threads Activity Centre", { tag: "@no-firefox" }, () => {
test.use({
displayName: "Alice",
botCreateOpts: { displayName: "Other User" },
diff --git a/playwright/e2e/threads/threads.spec.ts b/playwright/e2e/threads/threads.spec.ts
index 06ec57653c7..b6d72da3587 100644
--- a/playwright/e2e/threads/threads.spec.ts
+++ b/playwright/e2e/threads/threads.spec.ts
@@ -324,7 +324,7 @@ test.describe("Threads", () => {
});
});
- test("can send voice messages", async ({ page, app, user }) => {
+ test("can send voice messages", { tag: ["@no-firefox", "@no-webkit"] }, async ({ page, app, user }) => {
// Increase right-panel size, so that voice messages fit
await page.evaluate(() => {
window.localStorage.setItem("mx_rhs_size", "600");
@@ -353,7 +353,7 @@ test.describe("Threads", () => {
test(
"should send location and reply to the location on ThreadView",
- { tag: "@screenshot" },
+ { tag: ["@screenshot", "@no-firefox"] },
async ({ page, app, bot }) => {
const roomId = await app.client.createRoom({});
await app.client.inviteUser(roomId, bot.credentials.userId);
diff --git a/playwright/e2e/timeline/timeline.spec.ts b/playwright/e2e/timeline/timeline.spec.ts
index 7aaabb9759d..4761876de45 100644
--- a/playwright/e2e/timeline/timeline.spec.ts
+++ b/playwright/e2e/timeline/timeline.spec.ts
@@ -90,7 +90,7 @@ test.describe("Timeline", () => {
let oldAvatarUrl: string;
let newAvatarUrl: string;
- test.describe("useOnlyCurrentProfiles", () => {
+ test.describe("useOnlyCurrentProfiles", { tag: ["@no-firefox", "@no-webkit"] }, () => {
test.beforeEach(async ({ app, user }) => {
({ content_uri: oldAvatarUrl } = await app.client.uploadContent(OLD_AVATAR, { type: "image/png" }));
await app.client.setAvatarUrl(oldAvatarUrl);
@@ -876,7 +876,7 @@ test.describe("Timeline", () => {
});
});
- test.describe("message sending", () => {
+ test.describe("message sending", { tag: ["@no-firefox", "@no-webkit"] }, () => {
const MESSAGE = "Hello world";
const reply = "Reply";
const viewRoomSendMessageAndSetupReply = async (page: Page, app: ElementAppPage, roomId: string) => {
@@ -914,7 +914,6 @@ test.describe("Timeline", () => {
});
test("can reply with a voice message", async ({ page, app, room, context }) => {
- await context.grantPermissions(["microphone"]);
await viewRoomSendMessageAndSetupReply(page, app, room.roomId);
const composerOptions = await app.openMessageComposerOptions();
diff --git a/playwright/e2e/widgets/stickers.spec.ts b/playwright/e2e/widgets/stickers.spec.ts
index 418e104037b..bd65100baae 100644
--- a/playwright/e2e/widgets/stickers.spec.ts
+++ b/playwright/e2e/widgets/stickers.spec.ts
@@ -128,7 +128,7 @@ async function setWidgetAccountData(
});
}
-test.describe("Stickers", () => {
+test.describe("Stickers", { tag: ["@no-firefox", "@no-webkit"] }, () => {
test.use({
displayName: "Sally",
room: async ({ app }, use) => {
diff --git a/playwright/element-web-test.ts b/playwright/element-web-test.ts
index 6ac0b7226ab..4fe51d0a8a0 100644
--- a/playwright/element-web-test.ts
+++ b/playwright/element-web-test.ts
@@ -127,6 +127,14 @@ export interface Fixtures {
}
export const test = base.extend({
+ context: async ({ context }, use, testInfo) => {
+ // We skip tests instead of using grep-invert to still surface the counts in the html report
+ test.skip(
+ testInfo.tags.includes(`@no-${testInfo.project.name.toLowerCase()}`),
+ `Test does not work on ${testInfo.project.name}`,
+ );
+ await use(context);
+ },
config: CONFIG_JSON,
page: async ({ context, page, config, labsFlags }, use) => {
await context.route(`http://localhost:8080/config.json*`, async (route) => {
diff --git a/playwright/plugins/docker/index.ts b/playwright/plugins/docker/index.ts
index 895a7d0f123..6cc13860be3 100644
--- a/playwright/plugins/docker/index.ts
+++ b/playwright/plugins/docker/index.ts
@@ -140,8 +140,12 @@ export class Docker {
* Detects whether the docker command is actually podman.
* To do this, it looks for "podman" in the output of "docker --help".
*/
+ static _isPodman?: boolean;
static async isPodman(): Promise {
- const { stdout } = await exec("docker", ["--help"], true);
- return stdout.toLowerCase().includes("podman");
+ if (Docker._isPodman === undefined) {
+ const { stdout } = await exec("docker", ["--help"], true);
+ Docker._isPodman = stdout.toLowerCase().includes("podman");
+ }
+ return Docker._isPodman;
}
}