Skip to content

Add support for Module API 1.4.0 #30185

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 13 commits into
base: develop
Choose a base branch
from
2 changes: 0 additions & 2 deletions jest.config.ts
Original file line number Diff line number Diff line change
@@ -40,8 +40,6 @@ const config: Config = {
"^!!raw-loader!.*": "jest-raw-loader",
"recorderWorkletFactory": "<rootDir>/__mocks__/empty.js",
"^fetch-mock$": "<rootDir>/node_modules/fetch-mock",
// Requires ESM which is incompatible with our current Jest setup
"^@element-hq/element-web-module-api$": "<rootDir>/__mocks__/empty.js",
},
transformIgnorePatterns: ["/node_modules/(?!(mime|matrix-js-sdk)).+$"],
collectCoverageFrom: [
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -86,7 +86,7 @@
},
"dependencies": {
"@babel/runtime": "^7.12.5",
"@element-hq/element-web-module-api": "1.3.0",
"@element-hq/element-web-module-api": "1.4.1",
"@fontsource/inconsolata": "^5",
"@fontsource/inter": "^5",
"@formatjs/intl-segmenter": "^11.5.7",
21 changes: 20 additions & 1 deletion src/components/views/rooms/RoomPreviewBar.tsx
Original file line number Diff line number Diff line change
@@ -31,6 +31,7 @@ import { UIFeature } from "../../../settings/UIFeature";
import { ModuleRunner } from "../../../modules/ModuleRunner";
import { Icon as AskToJoinIcon } from "../../../../res/img/element-icons/ask-to-join.svg";
import Field from "../elements/Field";
import ModuleApi from "../../../modules/Api.ts";

const MemberEventHtmlReasonField = "io.element.html_reason";

@@ -116,7 +117,7 @@ interface IState {
reason?: string;
}

export default class RoomPreviewBar extends React.Component<IProps, IState> {
class RoomPreviewBar extends React.Component<IProps, IState> {
public static defaultProps = {
onJoinClick() {},
};
@@ -747,3 +748,21 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
);
}
}

const WrappedRoomPreviewBar = (props: IProps): JSX.Element => {
const moduleRenderer = ModuleApi.customComponents.roomPreviewBarRenderer;
if (moduleRenderer) {
return moduleRenderer(
{
...props,
roomId: props.room?.roomId ?? props.roomId,
roomAlias: props.room?.getCanonicalAlias() ?? props.roomAlias,
},
(props) => <RoomPreviewBar {...props} />,
);
}

return <RoomPreviewBar {...props} />;
};

export default WrappedRoomPreviewBar;
11 changes: 10 additions & 1 deletion src/modules/Api.ts
Original file line number Diff line number Diff line change
@@ -21,7 +21,11 @@ import { WidgetPermissionCustomisations } from "../customisations/WidgetPermissi
import { WidgetVariableCustomisations } from "../customisations/WidgetVariables.ts";
import { ConfigApi } from "./ConfigApi.ts";
import { I18nApi } from "./I18nApi.ts";
import { CustomComponentsApi } from "./customComponentApi.ts";
import { CustomComponentsApi } from "./customComponentApi";
import { WatchableProfile } from "./Profile.ts";
import { NavigationApi } from "./Navigation.ts";
import { openDialog } from "./Dialog.tsx";
import { overwriteAccountAuth } from "./Auth.ts";

const legacyCustomisationsFactory = <T extends object>(baseCustomisations: T) => {
let used = false;
@@ -57,6 +61,11 @@ class ModuleApi implements Api {
legacyCustomisationsFactory(WidgetVariableCustomisations);
/* eslint-enable @typescript-eslint/naming-convention */

public readonly navigation = new NavigationApi();
public readonly openDialog = openDialog;
public readonly overwriteAccountAuth = overwriteAccountAuth;
public readonly profile = new WatchableProfile();

public readonly config = new ConfigApi();
public readonly i18n = new I18nApi();
public readonly customComponents = new CustomComponentsApi();
43 changes: 43 additions & 0 deletions src/modules/Auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
Copyright 2025 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/

import { type AccountAuthInfo } from "@element-hq/element-web-module-api";
import { sleep } from "matrix-js-sdk/src/utils";

import type { OverwriteLoginPayload } from "../dispatcher/payloads/OverwriteLoginPayload.ts";
import { Action } from "../dispatcher/actions.ts";
import defaultDispatcher from "../dispatcher/dispatcher.ts";
import type { ActionPayload } from "../dispatcher/payloads.ts";

export async function overwriteAccountAuth(accountInfo: AccountAuthInfo): Promise<void> {
const { promise, resolve } = Promise.withResolvers<void>();

const onAction = (payload: ActionPayload): void => {
if (payload.action === Action.OnLoggedIn) {
// We want to wait for the new login to complete before returning.
// See `Action.OnLoggedIn` in dispatcher.
resolve();
}
};
const dispatcherRef = defaultDispatcher.register(onAction);

defaultDispatcher.dispatch<OverwriteLoginPayload>(
{
action: Action.OverwriteLogin,
credentials: {
...accountInfo,
guest: false,
},
},
true,
); // require to be sync to match inherited interface behaviour

// wait for login to complete
await promise;
defaultDispatcher.unregister(dispatcherRef);
await sleep(0); // wait for the next tick to ensure the login is fully processed
}
52 changes: 52 additions & 0 deletions src/modules/Dialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
Copyright 2025 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/

import React, { type ComponentType, type JSX, useCallback } from "react";
import { type DialogProps, type DialogOptions, type DialogHandle } from "@element-hq/element-web-module-api";

import Modal from "../Modal";
import BaseDialog from "../components/views/dialogs/BaseDialog.tsx";

const OuterDialog = <M, P extends object>({
title,
Dialog,
props,
onFinished,
}: {
title: string;
Dialog: ComponentType<DialogProps<M> & P>;
props: P;
onFinished(ok: boolean, model: M | null): void;
}): JSX.Element => {
const close = useCallback(() => onFinished(false, null), [onFinished]);
const submit = useCallback((model: M) => onFinished(true, model), [onFinished]);
return (
<BaseDialog onFinished={close} title={title}>
<Dialog {...props} onSubmit={submit} onCancel={close} />
</BaseDialog>
);
};

export function openDialog<M, P extends object>(
initialOptions: DialogOptions,
Dialog: ComponentType<P & DialogProps<M>>,
props: P,
): DialogHandle<M> {
const { close, finished } = Modal.createDialog(OuterDialog<M, P>, {
title: initialOptions.title,
Dialog,
props,
});

return {
finished: finished.then(([ok, model]) => ({
ok: ok ?? false,
model: model ?? null,
})),
close: () => close(false, null),
};
}
43 changes: 43 additions & 0 deletions src/modules/Navigation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
Copyright 2025 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/

import { type NavigationApi as INavigationApi } from "@element-hq/element-web-module-api";

import { navigateToPermalink } from "../utils/permalinks/navigator.ts";
import { parsePermalink } from "../utils/permalinks/Permalinks.ts";
import { getCachedRoomIDForAlias } from "../RoomAliasCache.ts";
import { MatrixClientPeg } from "../MatrixClientPeg.ts";
import dispatcher from "../dispatcher/dispatcher.ts";
import { Action } from "../dispatcher/actions.ts";
import SettingsStore from "../settings/SettingsStore.ts";

export class NavigationApi implements INavigationApi {
public async toMatrixToLink(link: string, join = false): Promise<void> {
navigateToPermalink(link);

const parts = parsePermalink(link);
if (parts?.roomIdOrAlias && join) {
let roomId: string | undefined = parts.roomIdOrAlias;
if (roomId.startsWith("#")) {
roomId = getCachedRoomIDForAlias(parts.roomIdOrAlias);
if (!roomId) {
// alias resolution failed
const result = await MatrixClientPeg.safeGet().getRoomIdForAlias(parts.roomIdOrAlias);
roomId = result.room_id;
}
}

if (roomId) {
dispatcher.dispatch({
action: Action.JoinRoom,
canAskToJoin: SettingsStore.getValue("feature_ask_to_join"),
roomId,
});
}
}
}
}
32 changes: 32 additions & 0 deletions src/modules/Profile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
Copyright 2025 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/

import { type Profile, Watchable } from "@element-hq/element-web-module-api";

import { OwnProfileStore } from "../stores/OwnProfileStore.ts";
import { UPDATE_EVENT } from "../stores/AsyncStore.ts";

export class WatchableProfile extends Watchable<Profile> {
public constructor() {
super({});
this.value = this.profile;

OwnProfileStore.instance.on(UPDATE_EVENT, this.onProfileChange);
}

private get profile(): Profile {
return {
isGuest: OwnProfileStore.instance.matrixClient?.isGuest() ?? false,
userId: OwnProfileStore.instance.matrixClient?.getUserId() ?? undefined,
displayName: OwnProfileStore.instance.displayName ?? undefined,
};
}

private readonly onProfileChange = (): void => {
this.value = this.profile;
};
}
23 changes: 21 additions & 2 deletions src/modules/customComponentApi.ts
Original file line number Diff line number Diff line change
@@ -12,9 +12,10 @@ import type {
CustomComponentsApi as ICustomComponentsApi,
CustomMessageRenderFunction,
CustomMessageComponentProps as ModuleCustomMessageComponentProps,
OriginalComponentProps,
OriginalMessageComponentProps,
CustomMessageRenderHints as ModuleCustomCustomMessageRenderHints,
MatrixEvent as ModuleMatrixEvent,
CustomRoomPreviewBarRenderFunction,
} from "@element-hq/element-web-module-api";
import type React from "react";

@@ -72,6 +73,7 @@ export class CustomComponentsApi implements ICustomComponentsApi {
): void {
this.registeredMessageRenderers.push({ eventTypeOrFilter: eventTypeOrFilter, renderer, hints });
}

/**
* Select the correct renderer based on the event information.
* @param mxEvent The message event being rendered.
@@ -100,7 +102,7 @@ export class CustomComponentsApi implements ICustomComponentsApi {
*/
public renderMessage(
props: CustomMessageComponentProps,
originalComponent?: (props?: OriginalComponentProps) => React.JSX.Element,
originalComponent?: (props?: OriginalMessageComponentProps) => React.JSX.Element,
): React.JSX.Element | null {
const moduleEv = CustomComponentsApi.getModuleMatrixEvent(props.mxEvent);
const renderer = moduleEv && this.selectRenderer(moduleEv);
@@ -134,4 +136,21 @@ export class CustomComponentsApi implements ICustomComponentsApi {
}
return null;
}

private _roomPreviewBarRenderer?: CustomRoomPreviewBarRenderFunction;

/**
* Get the custom room preview bar renderer, if any has been registered.
*/
public get roomPreviewBarRenderer(): CustomRoomPreviewBarRenderFunction | undefined {
return this._roomPreviewBarRenderer;
}

/**
* Register a custom room preview bar renderer.
* @param renderer - the function that will render the custom room preview bar.
*/
public registerRoomPreviewBar(renderer: CustomRoomPreviewBarRenderFunction): void {
this._roomPreviewBarRenderer = renderer;
}
}
4 changes: 2 additions & 2 deletions src/stores/RoomViewStore.tsx
Original file line number Diff line number Diff line change
@@ -510,8 +510,8 @@ export class RoomViewStore extends EventEmitter {
});

// take a copy of roomAlias & roomId as they may change by the time the join is complete
const { roomAlias, roomId = payload.roomId } = this.state;
const address = roomAlias || roomId!;
const { roomAlias, roomId } = this.state;
const address = roomAlias || payload.roomId || roomId!;
const viaServers = this.state.viaServers || [];
try {
const cli = MatrixClientPeg.safeGet();
Original file line number Diff line number Diff line change
@@ -53,6 +53,7 @@ import { ElementWidgetActions } from "../../../../src/stores/widgets/ElementWidg
jest.mock("../../../../src/stores/OwnProfileStore", () => ({
OwnProfileStore: {
instance: {
on: jest.fn(),
isProfileInfoFetched: true,
removeListener: jest.fn(),
getHttpAvatarUrl: jest.fn().mockReturnValue("http://avatar_url"),
Original file line number Diff line number Diff line change
@@ -43,6 +43,7 @@ import { RoomPermalinkCreator } from "../../../../../src/utils/permalinks/Permal
jest.mock("../../../../../src/stores/OwnProfileStore", () => ({
OwnProfileStore: {
instance: {
on: jest.fn(),
isProfileInfoFetched: true,
removeListener: jest.fn(),
getHttpAvatarUrl: jest.fn().mockReturnValue("http://avatar_url"),
Original file line number Diff line number Diff line change
@@ -48,6 +48,7 @@ jest.mock("../../../../../src/settings/SettingsStore", () => ({
jest.mock("../../../../../src/stores/OwnProfileStore", () => ({
OwnProfileStore: {
instance: {
on: jest.fn(),
displayName: "Ernie",
getHttpAvatarUrl: jest.fn().mockReturnValue("image.com/img"),
},
10 changes: 5 additions & 5 deletions yarn.lock
Original file line number Diff line number Diff line change
@@ -1682,10 +1682,10 @@
resolved "https://registry.yarnpkg.com/@element-hq/element-call-embedded/-/element-call-embedded-0.13.1.tgz#9161f657f7bebdb5339847b7a8f0a3149a36f95c"
integrity sha512-6RGZPdx+gOCzpJNe+dbftEyiWuNx+2H+uXiZp7QN8SOZ3dl/yjg0JcK60wsC48i7gXy/6ERdbwTgaL9ez8mvhA==

"@element-hq/element-web-module-api@1.3.0":
version "1.3.0"
resolved "https://registry.yarnpkg.com/@element-hq/element-web-module-api/-/element-web-module-api-1.3.0.tgz#6067fa654174d1dd0953447bb036e38f9dfa51a5"
integrity sha512-rEV0xnT/tNYPIdqHWWiz2KZo96UeZR0YChfoVLiPT46ZlEYyxqkjxT5bOm1eL2/CiYRe8t1yka3UDkIjq481/g==
"@element-hq/element-web-module-api@1.4.1":
version "1.4.1"
resolved "https://registry.yarnpkg.com/@element-hq/element-web-module-api/-/element-web-module-api-1.4.1.tgz#a46526d58985190f9989bf1686ea872687d3c6e1"
integrity sha512-A8yaQtX7QoKThzzZVU+VYOFhpiNyppEMuIQijK48RvhVp1nwmy0cTD6u/6Yn64saNwJjtna+Oy+Qzo/TfwwhxQ==

"@element-hq/element-web-playwright-common@^1.4.3":
version "1.4.3"
@@ -4543,7 +4543,7 @@
classnames "^2.5.1"
vaul "^1.0.0"

"@vector-im/matrix-wysiwyg-wasm@link:../../bindings/wysiwyg-wasm":
"@vector-im/matrix-wysiwyg-wasm@link:../../Library/Caches/Yarn/v6/npm-@vector-im-matrix-wysiwyg-2.39.0-a6238e517f23a2f3025d9c65445914771c63b163-integrity/node_modules/bindings/wysiwyg-wasm":
version "0.0.0"
uid ""


Unchanged files with check annotations Beta

*/
public async getToast(expectedTitle: string): Promise<Locator> {
const toast = this.page.locator(".mx_Toast_toast", { hasText: expectedTitle }).first();
await expect(toast).toBeVisible();

Check failure on line 22 in playwright/pages/toasts.ts

GitHub Actions / Run Tests [Chrome] 1/6

[Chrome] › playwright/e2e/crypto/user-verification.spec.ts:34:9 › User verification › can receive a verification request when there is no existing DM

7) [Chrome] › playwright/e2e/crypto/user-verification.spec.ts:34:9 › User verification › can receive a verification request when there is no existing DM Error: Timed out 5000ms waiting for expect(locator).toBeVisible() Locator: locator('.mx_Toast_toast').filter({ hasText: 'Verification requested' }).first() Expected: visible Received: <element(s) not found> Call log: - Expect "toBeVisible" with timeout 5000ms - waiting for locator('.mx_Toast_toast').filter({ hasText: 'Verification requested' }).first() at ../pages/toasts.ts:22 20 | public async getToast(expectedTitle: string): Promise<Locator> { 21 | const toast = this.page.locator(".mx_Toast_toast", { hasText: expectedTitle }).first(); > 22 | await expect(toast).toBeVisible(); | ^ 23 | return toast; 24 | } 25 | at Toasts.getToast (/home/runner/work/element-web/element-web/playwright/pages/toasts.ts:22:29) at /home/runner/work/element-web/element-web/playwright/e2e/crypto/user-verification.spec.ts:63:36
return toast;
}
/* go back to the test room and find Bob's message again */
await app.viewRoomById(testRoomId);
await expect(lastTile).toContainText("test encrypted 1");

Check failure on line 202 in playwright/e2e/crypto/event-shields.spec.ts

GitHub Actions / Run Tests [Chrome] 1/6

[Chrome] › playwright/e2e/crypto/event-shields.spec.ts:164:13 › Cryptography › event shields › Should show a grey padlock for a key restored from backup

6) [Chrome] › playwright/e2e/crypto/event-shields.spec.ts:164:13 › Cryptography › event shields › Should show a grey padlock for a key restored from backup Error: Timed out 5000ms waiting for expect(locator).toContainText(expected) Locator: locator('.mx_EventTile_last') Expected string: "test encrypted 1" Received: <element(s) not found> Call log: - Expect "toContainText" with timeout 5000ms - waiting for locator('.mx_EventTile_last') 200 | /* go back to the test room and find Bob's message again */ 201 | await app.viewRoomById(testRoomId); > 202 | await expect(lastTile).toContainText("test encrypted 1"); | ^ 203 | // The gray shield would be a mx_EventTile_e2eIcon_normal. The red shield would be a mx_EventTile_e2eIcon_warning. 204 | // No shield would have no div mx_EventTile_e2eIcon at all. 205 | await expect(lastTileE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_normal/); at /home/runner/work/element-web/element-web/playwright/e2e/crypto/event-shields.spec.ts:202:36
// The gray shield would be a mx_EventTile_e2eIcon_normal. The red shield would be a mx_EventTile_e2eIcon_warning.
// No shield would have no div mx_EventTile_e2eIcon at all.
await expect(lastTileE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_normal/);
/*
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.
await page.route("**/_matrix/client/v3/sendToDevice/m.secret.request/**", async (route) => {
await route.fulfill({ json: {} });
await new Promise((f) => setTimeout(f, 1000));
await route.fetch();

Check failure on line 104 in playwright/e2e/crypto/device-verification.spec.ts

GitHub Actions / Run Tests [Chrome] 1/6

[Chrome] › playwright/e2e/crypto/device-verification.spec.ts:129:9 › Device verification › Verify device with QR code during login @no-webkit

5) [Chrome] › playwright/e2e/crypto/device-verification.spec.ts:129:9 › Device verification › Verify device with QR code during login @no-webkit Error: "route.fetch: Test ended." while running route callback. Consider awaiting `await page.unrouteAll({ behavior: 'ignoreErrors' })` before the end of the test to ignore remaining routes in flight. 102 | await route.fulfill({ json: {} }); 103 | await new Promise((f) => setTimeout(f, 1000)); > 104 | await route.fetch(); | ^ 105 | }); 106 | 107 | await logIntoElement(page, credentials); at /home/runner/work/element-web/element-web/playwright/e2e/crypto/device-verification.spec.ts:104:25
});
await logIntoElement(page, credentials);
// Send message
await page.locator("div[contenteditable=true]").press("Enter");
// It was sent
await expect(page.locator(".mx_EventTile_last .mx_EventTile_body").getByText("my message 1")).toBeVisible();

Check failure on line 170 in playwright/e2e/composer/RTE.spec.ts

GitHub Actions / Run Tests [Chrome] 1/6

[Chrome] › playwright/e2e/composer/RTE.spec.ts:154:13 › Composer › Rich text editor › sends a message when you click send or press Enter

4) [Chrome] › playwright/e2e/composer/RTE.spec.ts:154:13 › Composer › Rich text editor › sends a message when you click send or press Enter Error: Timed out 5000ms waiting for expect(locator).toBeVisible() Locator: locator('.mx_EventTile_last .mx_EventTile_body').getByText('my message 1') Expected: visible Received: <element(s) not found> Call log: - Expect "toBeVisible" with timeout 5000ms - waiting for locator('.mx_EventTile_last .mx_EventTile_body').getByText('my message 1') 168 | await page.locator("div[contenteditable=true]").press("Enter"); 169 | // It was sent > 170 | await expect(page.locator(".mx_EventTile_last .mx_EventTile_body").getByText("my message 1")).toBeVisible(); | ^ 171 | }); 172 | 173 | test("sends only one message when you press Enter multiple times", async ({ page }) => { at /home/runner/work/element-web/element-web/playwright/e2e/composer/RTE.spec.ts:170:107
});
test("sends only one message when you press Enter multiple times", async ({ page }) => {
// Assert that replied audio file is rendered as file button inside ReplyChain
const button = tile.locator(".mx_ReplyChain_wrapper .mx_MFileBody_info[role='button']");
// Assert that the file button has file name
await expect(button.locator(".mx_MFileBody_info_filename")).toBeVisible();

Check failure on line 244 in playwright/e2e/audio-player/audio-player.spec.ts

GitHub Actions / Run Tests [Chrome] 1/6

[Chrome] › playwright/e2e/audio-player/audio-player.spec.ts:222:9 › Audio player › should support replying to audio file with another audio file @no-firefox @no-webkit @screenshot

2) [Chrome] › playwright/e2e/audio-player/audio-player.spec.ts:222:9 › Audio player › should support replying to audio file with another audio file @no-firefox @no-webkit @screenshot Error: Timed out 5000ms waiting for expect(locator).toBeVisible() Locator: locator('.mx_EventTile_last').locator('.mx_ReplyChain_wrapper .mx_MFileBody_info[role=\'button\']').locator('.mx_MFileBody_info_filename') Expected: visible Received: <element(s) not found> Call log: - Expect "toBeVisible" with timeout 5000ms - waiting for locator('.mx_EventTile_last').locator('.mx_ReplyChain_wrapper .mx_MFileBody_info[role=\'button\']').locator('.mx_MFileBody_info_filename') 242 | const button = tile.locator(".mx_ReplyChain_wrapper .mx_MFileBody_info[role='button']"); 243 | // Assert that the file button has file name > 244 | await expect(button.locator(".mx_MFileBody_info_filename")).toBeVisible(); | ^ 245 | 246 | await takeSnapshots(page, app, "Selected EventTile of audio player with a reply"); 247 | }, at /home/runner/work/element-web/element-web/playwright/e2e/audio-player/audio-player.spec.ts:244:73
await takeSnapshots(page, app, "Selected EventTile of audio player with a reply");
},
await expect(tile.locator(".mx_AudioPlayer_container")).toBeVisible();
// Assert that there are two "mx_ReplyChain" elements
await expect(tile.locator(".mx_ReplyChain")).toHaveCount(2);

Check failure on line 283 in playwright/e2e/audio-player/audio-player.spec.ts

GitHub Actions / Run Tests [Chrome] 1/6

[Chrome] › playwright/e2e/audio-player/audio-player.spec.ts:250:9 › Audio player › should support creating a reply chain with multiple audio files @no-firefox @no-webkit @screenshot

3) [Chrome] › playwright/e2e/audio-player/audio-player.spec.ts:250:9 › Audio player › should support creating a reply chain with multiple audio files @no-firefox @no-webkit @screenshot Error: Timed out 5000ms waiting for expect(locator).toHaveCount(expected) Locator: locator('.mx_EventTile_last').locator('.mx_ReplyChain') Expected: 2 Received: 1 Call log: - Expect "toHaveCount" with timeout 5000ms - waiting for locator('.mx_EventTile_last').locator('.mx_ReplyChain') 9 × locator resolved to 1 element - unexpected value "1" 281 | 282 | // Assert that there are two "mx_ReplyChain" elements > 283 | await expect(tile.locator(".mx_ReplyChain")).toHaveCount(2); | ^ 284 | 285 | // Assert that one line contains the user name 286 | await expect(tile.locator(".mx_ReplyChain .mx_ReplyTile_sender").getByText(user.displayName)).toBeVisible(); at /home/runner/work/element-web/element-web/playwright/e2e/audio-player/audio-player.spec.ts:283:58
// Assert that one line contains the user name
await expect(tile.locator(".mx_ReplyChain .mx_ReplyTile_sender").getByText(user.displayName)).toBeVisible();
await page.waitForSelector(".mx_MatrixChat", { timeout: 30000 });
// Check that the room reloaded
await expect(page).toHaveURL(/\/#\/room\//);

Check failure on line 42 in playwright/e2e/app-loading/stored-credentials.spec.ts

GitHub Actions / Run Tests [Chrome] 1/6

[Chrome] › playwright/e2e/app-loading/stored-credentials.spec.ts:26:5 › Shows the last known page on reload

1) [Chrome] › playwright/e2e/app-loading/stored-credentials.spec.ts:26:5 › Shows the last known page on reload Error: Timed out 5000ms waiting for expect(page).toHaveURL(expected) Expected pattern: /\/#\/room\// Received string: "http://localhost:8080/#/home" Call log: - Expect "toHaveURL" with timeout 5000ms 9 × unexpected value "http://localhost:8080/#/home" 40 | 41 | // Check that the room reloaded > 42 | await expect(page).toHaveURL(/\/#\/room\//); | ^ 43 | await expect(page.locator(".mx_RoomHeader")).toContainText("Test Room"); 44 | }); 45 | at /home/runner/work/element-web/element-web/playwright/e2e/app-loading/stored-credentials.spec.ts:42:24
await expect(page.locator(".mx_RoomHeader")).toContainText("Test Room");
});