Move some of the slowest tests from jest to vitest (#34277)
* Remove doubled-up I18n Provider. ModalManager already provides this. * Reconfigure svgr into a vite-friendly `?react` resource query * Move InviteDialog test to vitest * Move RoomHeader tests to Vitest * Share serializer between Jest & Vitest * Attempt to stabilise InviteDialog test * Fix async leaks * Iterate based on copilot review * Fix InviteDialog throttle * Iterate * Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Fix lockfile * Iterate --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
co-authored by
Copilot Autofix powered by AI
parent
5b0fd416fd
commit
2430e0ab8b
@@ -0,0 +1,8 @@
|
||||
/*
|
||||
Copyright 2026 Element Creations Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
export default "div";
|
||||
@@ -5,5 +5,4 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
export const Icon = "div";
|
||||
export default "image-file-stub";
|
||||
|
||||
@@ -34,6 +34,7 @@ const config: Config = {
|
||||
"\\.(css|scss|pcss)(\\?raw)?$": "<rootDir>/__mocks__/cssMock.js",
|
||||
"\\.(gif|png|ttf|woff2)$": "<rootDir>/__mocks__/imageMock.js",
|
||||
"\\.svg$": "<rootDir>/__mocks__/svg.js",
|
||||
"\\.svg\\?react$": "<rootDir>/__mocks__/svg-react.js",
|
||||
"^matrix-js-sdk(.*)$": "<rootDir>/node_modules/matrix-js-sdk$1",
|
||||
"^react$": "<rootDir>/node_modules/react",
|
||||
"^react-dom$": "<rootDir>/node_modules/react-dom",
|
||||
@@ -69,6 +70,7 @@ const config: Config = {
|
||||
prettierPath: null,
|
||||
moduleDirectories: ["node_modules", "test/test-utils"],
|
||||
workerIdleMemoryLimit: "512MB",
|
||||
snapshotSerializers: ["<rootDir>/src/test/react-use-id-serializer.ts"],
|
||||
};
|
||||
|
||||
// if we're running under GHA, enable relevant reporters
|
||||
|
||||
@@ -209,6 +209,7 @@
|
||||
"testcontainers": "^12.0.0",
|
||||
"typescript": "catalog:ts6",
|
||||
"util": "^0.12.5",
|
||||
"vite-plugin-svgr": "catalog:",
|
||||
"vitest": "catalog:",
|
||||
"vitest-canvas-mock": "^1.1.4",
|
||||
"web-streams-polyfill": "^4.0.0",
|
||||
|
||||
Vendored
+5
-1
@@ -9,6 +9,10 @@ Please see LICENSE files in the repository root for full details.
|
||||
|
||||
declare module "*.svg" {
|
||||
const path: string;
|
||||
export const Icon: React.FC<React.SVGProps<SVGSVGElement>>;
|
||||
export default path;
|
||||
}
|
||||
|
||||
declare module "*.svg?react" {
|
||||
const Icon: React.FC<React.SVGProps<SVGSVGElement>>;
|
||||
export default Icon;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import { IconButton, Tooltip } from "@vector-im/compound-web";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { ThreadsIcon, CheckIcon, ChevronDownIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
|
||||
import { Icon as MarkAllThreadsReadIcon } from "../../../res/img/element-icons/check-all.svg";
|
||||
import MarkAllThreadsReadIcon from "../../../res/img/element-icons/check-all.svg?react";
|
||||
import BaseCard from "../views/right_panel/BaseCard";
|
||||
import type ResizeNotifier from "../../utils/ResizeNotifier";
|
||||
import MatrixClientContext, { useMatrixClientContext } from "../../contexts/MatrixClientContext";
|
||||
|
||||
@@ -11,7 +11,7 @@ import { Button, Tooltip } from "@vector-im/compound-web";
|
||||
import { RestartIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
|
||||
import AccessibleButton from "../../../views/elements/AccessibleButton";
|
||||
import { Icon as EMailPromptIcon } from "../../../../../res/img/element-icons/email-prompt.svg";
|
||||
import EMailPromptIcon from "../../../../../res/img/element-icons/email-prompt.svg?react";
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import { useTimeoutToggle } from "../../../../hooks/useTimeoutToggle";
|
||||
import { ErrorMessage } from "../../ErrorMessage";
|
||||
|
||||
@@ -12,7 +12,7 @@ import { CloseIcon, RestartIcon } from "@vector-im/compound-design-tokens/assets
|
||||
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import AccessibleButton from "../../../views/elements/AccessibleButton";
|
||||
import { Icon as EmailPromptIcon } from "../../../../../res/img/element-icons/email-prompt.svg";
|
||||
import EmailPromptIcon from "../../../../../res/img/element-icons/email-prompt.svg?react";
|
||||
import { useTimeoutToggle } from "../../../../hooks/useTimeoutToggle";
|
||||
import { ErrorMessage } from "../../ErrorMessage";
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import React, { useState, useEffect } from "react";
|
||||
import { type MatrixClient, type Beacon, type Room } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import type * as maplibregl from "maplibre-gl";
|
||||
import { Icon as LiveLocationIcon } from "../../../../res/img/location/live-location.svg";
|
||||
import LiveLocationIcon from "../../../../res/img/location/live-location.svg?react";
|
||||
import { useLiveBeacons } from "../../../utils/beacon/useLiveBeacons";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import BaseDialog from "../dialogs/BaseDialog";
|
||||
|
||||
@@ -13,7 +13,7 @@ import { type Beacon, type BeaconIdentifier } from "matrix-js-sdk/src/matrix";
|
||||
import { useEventEmitterState } from "../../../hooks/useEventEmitter";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { OwnBeaconStore, OwnBeaconStoreEvent } from "../../../stores/OwnBeaconStore";
|
||||
import { Icon as LiveLocationIcon } from "../../../../res/img/location/live-location.svg";
|
||||
import LiveLocationIcon from "../../../../res/img/location/live-location.svg?react";
|
||||
import { type ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import dispatcher from "../../../dispatcher/dispatcher";
|
||||
|
||||
@@ -9,7 +9,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
import React from "react";
|
||||
import classNames from "classnames";
|
||||
|
||||
import { Icon as LiveLocationIcon } from "../../../../res/img/location/live-location.svg";
|
||||
import LiveLocationIcon from "../../../../res/img/location/live-location.svg?react";
|
||||
|
||||
interface Props extends React.SVGProps<SVGSVGElement> {
|
||||
// use error styling when true
|
||||
|
||||
@@ -12,7 +12,6 @@ import React, { type JSX } from "react";
|
||||
import FocusLock from "react-focus-lock";
|
||||
import classNames from "classnames";
|
||||
import { type MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import { I18nContext } from "@element-hq/web-shared-components";
|
||||
import { CloseIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
@@ -172,42 +171,38 @@ export default class BaseDialog extends React.Component<IProps> {
|
||||
}
|
||||
|
||||
return (
|
||||
// XXX: We can't import ModuleAPI here because it causes a dependency cycle - hack and
|
||||
// use the copy on the window object :(
|
||||
<I18nContext.Provider value={window.mxModuleApi.i18n}>
|
||||
<MatrixClientContext.Provider value={this.matrixClient}>
|
||||
{this.props.screenName && <PosthogScreenTracker screenName={this.props.screenName} />}
|
||||
<FocusLock
|
||||
returnFocus={true}
|
||||
lockProps={lockProps}
|
||||
className={classNames(this.props.className, {
|
||||
mx_Dialog_fixedWidth: this.props.fixedWidth,
|
||||
<MatrixClientContext.Provider value={this.matrixClient}>
|
||||
{this.props.screenName && <PosthogScreenTracker screenName={this.props.screenName} />}
|
||||
<FocusLock
|
||||
returnFocus={true}
|
||||
lockProps={lockProps}
|
||||
className={classNames(this.props.className, {
|
||||
mx_Dialog_fixedWidth: this.props.fixedWidth,
|
||||
})}
|
||||
>
|
||||
{this.props.top}
|
||||
<div
|
||||
className={classNames("mx_Dialog_header", {
|
||||
mx_Dialog_headerWithButton: !!this.props.headerButton,
|
||||
})}
|
||||
>
|
||||
{this.props.top}
|
||||
<div
|
||||
className={classNames("mx_Dialog_header", {
|
||||
mx_Dialog_headerWithButton: !!this.props.headerButton,
|
||||
})}
|
||||
>
|
||||
{!!(this.props.title || headerImage) && (
|
||||
<Heading
|
||||
size="3"
|
||||
as="h1"
|
||||
className={classNames("mx_Dialog_title", this.props.titleClass)}
|
||||
id="mx_BaseDialog_title"
|
||||
>
|
||||
{headerImage}
|
||||
{this.props.title}
|
||||
</Heading>
|
||||
)}
|
||||
{this.props.headerButton}
|
||||
</div>
|
||||
{this.props.children}
|
||||
{cancelButton}
|
||||
</FocusLock>
|
||||
</MatrixClientContext.Provider>
|
||||
</I18nContext.Provider>
|
||||
{!!(this.props.title || headerImage) && (
|
||||
<Heading
|
||||
size="3"
|
||||
as="h1"
|
||||
className={classNames("mx_Dialog_title", this.props.titleClass)}
|
||||
id="mx_BaseDialog_title"
|
||||
>
|
||||
{headerImage}
|
||||
{this.props.title}
|
||||
</Heading>
|
||||
)}
|
||||
{this.props.headerButton}
|
||||
</div>
|
||||
{this.props.children}
|
||||
{cancelButton}
|
||||
</FocusLock>
|
||||
</MatrixClientContext.Provider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+98
-81
@@ -6,17 +6,17 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { vi, describe, it, expect, afterAll, beforeEach, afterEach, type Mocked } from "vitest";
|
||||
|
||||
import React from "react";
|
||||
import { findByText, fireEvent, render, screen } from "jest-matrix-react";
|
||||
import { findByText, fireEvent, render, screen } from "test-utils-rtl";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { type MatrixClient, MatrixError, Room, RoomType } from "matrix-js-sdk/src/matrix";
|
||||
import { KnownMembership } from "matrix-js-sdk/src/types";
|
||||
import { sleep } from "matrix-js-sdk/src/utils";
|
||||
import { mocked, type Mocked } from "jest-mock-vitest-adapter";
|
||||
import { UserVerificationStatus } from "matrix-js-sdk/src/crypto-api";
|
||||
|
||||
import InviteDialog from "../../../../../src/components/views/dialogs/InviteDialog";
|
||||
import { InviteKind } from "../../../../../src/components/views/dialogs/InviteDialogTypes";
|
||||
import {
|
||||
clearAllModals,
|
||||
filterConsole,
|
||||
@@ -25,27 +25,43 @@ import {
|
||||
mkMembership,
|
||||
mkMessage,
|
||||
mkRoomCreateEvent,
|
||||
} from "../../../../test-utils";
|
||||
import DMRoomMap from "../../../../../src/utils/DMRoomMap";
|
||||
import SdkConfig from "../../../../../src/SdkConfig";
|
||||
import { type ValidatedServerConfig } from "../../../../../src/utils/ValidatedServerConfig";
|
||||
import { type IConfigOptions } from "../../../../../src/IConfigOptions";
|
||||
import { SDKContextClass } from "../../../../../src/contexts/SDKContextClass";
|
||||
import { type IProfileInfo } from "../../../../../src/hooks/useProfileInfo";
|
||||
import { DirectoryMember, startDmOnFirstMessage } from "../../../../../src/utils/direct-messages";
|
||||
import { TestSDKContext } from "../../../TestSDKContext.ts";
|
||||
TestSDKContext,
|
||||
} from "test-utils";
|
||||
|
||||
const mockGetAccessToken = jest.fn().mockResolvedValue("getAccessToken");
|
||||
jest.mock("../../../../../src/IdentityAuthClient", () =>
|
||||
jest.fn().mockImplementation(() => ({
|
||||
getAccessToken: mockGetAccessToken,
|
||||
})),
|
||||
);
|
||||
import InviteDialog from "./InviteDialog";
|
||||
import { InviteKind } from "./InviteDialogTypes";
|
||||
import DMRoomMap from "../../../utils/DMRoomMap";
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
import { type ValidatedServerConfig } from "../../../utils/ValidatedServerConfig";
|
||||
import { type IConfigOptions } from "../../../IConfigOptions";
|
||||
import { SDKContextClass } from "../../../contexts/SDKContextClass";
|
||||
import { type IProfileInfo } from "../../../hooks/useProfileInfo";
|
||||
import { DirectoryMember, startDmOnFirstMessage } from "../../../utils/direct-messages";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg.ts";
|
||||
|
||||
jest.mock("../../../../../src/utils/direct-messages", () => ({
|
||||
...jest.requireActual("../../../../../src/utils/direct-messages"),
|
||||
__esModule: true,
|
||||
startDmOnFirstMessage: jest.fn(),
|
||||
const mockGetAccessToken = vi.fn().mockResolvedValue("getAccessToken");
|
||||
vi.mock("../../../IdentityAuthClient", () => ({
|
||||
default: vi.fn().mockImplementation(function () {
|
||||
return {
|
||||
getAccessToken: mockGetAccessToken,
|
||||
};
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("../../../utils/direct-messages", async () => ({
|
||||
...(await vi.importActual("../../../utils/direct-messages")),
|
||||
startDmOnFirstMessage: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../../../dispatcher/dispatcher");
|
||||
|
||||
vi.mock("lodash", async () => ({
|
||||
...(await vi.importActual("lodash")),
|
||||
// Stub out the debounce to prevent async leaks
|
||||
debounce: vi.fn((fn) => {
|
||||
fn.cancel = vi.fn();
|
||||
return fn;
|
||||
}),
|
||||
}));
|
||||
|
||||
const getSearchField = () => screen.getByTestId("invite-dialog-input");
|
||||
@@ -106,23 +122,23 @@ describe("InviteDialog", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
mockClient = getMockClientWithEventEmitter({
|
||||
getCrypto: jest.fn().mockReturnValue({
|
||||
getUserVerificationStatus: jest
|
||||
getCrypto: vi.fn().mockReturnValue({
|
||||
getUserVerificationStatus: vi
|
||||
.fn()
|
||||
.mockResolvedValue(new UserVerificationStatus(false, false, true, false)),
|
||||
}),
|
||||
getDomain: jest.fn().mockReturnValue(serverDomain),
|
||||
getUserId: jest.fn().mockReturnValue(bobId),
|
||||
getSafeUserId: jest.fn().mockReturnValue(bobId),
|
||||
isGuest: jest.fn().mockReturnValue(false),
|
||||
getVisibleRooms: jest.fn().mockReturnValue([]),
|
||||
getRoom: jest.fn(),
|
||||
getRooms: jest.fn(),
|
||||
getAccountData: jest.fn(),
|
||||
getPushActionsForEvent: jest.fn(),
|
||||
mxcUrlToHttp: jest.fn().mockReturnValue(""),
|
||||
isRoomEncrypted: jest.fn().mockReturnValue(false),
|
||||
getProfileInfo: jest.fn().mockImplementation(async (userId: string) => {
|
||||
getDomain: vi.fn().mockReturnValue(serverDomain),
|
||||
getUserId: vi.fn().mockReturnValue(bobId),
|
||||
getSafeUserId: vi.fn().mockReturnValue(bobId),
|
||||
isGuest: vi.fn().mockReturnValue(false),
|
||||
getVisibleRooms: vi.fn().mockReturnValue([]),
|
||||
getRoom: vi.fn(),
|
||||
getRooms: vi.fn(),
|
||||
getAccountData: vi.fn(),
|
||||
getPushActionsForEvent: vi.fn(),
|
||||
mxcUrlToHttp: vi.fn().mockReturnValue(""),
|
||||
isRoomEncrypted: vi.fn().mockReturnValue(false),
|
||||
getProfileInfo: vi.fn().mockImplementation(async (userId: string) => {
|
||||
if (userId === aliceId) return aliceProfileInfo;
|
||||
if (userId === bobId) return bobProfileInfo;
|
||||
|
||||
@@ -131,24 +147,25 @@ describe("InviteDialog", () => {
|
||||
error: "Profile not found",
|
||||
});
|
||||
}),
|
||||
getIdentityServerUrl: jest.fn(),
|
||||
searchUserDirectory: jest.fn().mockResolvedValue({}),
|
||||
lookupThreePid: jest.fn(),
|
||||
registerWithIdentityServer: jest.fn().mockResolvedValue({
|
||||
getIdentityServerUrl: vi.fn(),
|
||||
searchUserDirectory: vi.fn().mockResolvedValue({}),
|
||||
lookupThreePid: vi.fn(),
|
||||
registerWithIdentityServer: vi.fn().mockResolvedValue({
|
||||
access_token: "access_token",
|
||||
token: "token",
|
||||
}),
|
||||
getOpenIdToken: jest.fn().mockResolvedValue({}),
|
||||
getIdentityAccount: jest.fn().mockResolvedValue({}),
|
||||
getTerms: jest.fn().mockResolvedValue({ policies: [] }),
|
||||
supportsThreads: jest.fn().mockReturnValue(false),
|
||||
isInitialSyncComplete: jest.fn().mockReturnValue(true),
|
||||
getClientWellKnown: jest.fn().mockResolvedValue({}),
|
||||
invite: jest.fn(),
|
||||
getOpenIdToken: vi.fn().mockResolvedValue({}),
|
||||
getIdentityAccount: vi.fn().mockResolvedValue({}),
|
||||
getTerms: vi.fn().mockResolvedValue({ policies: [] }),
|
||||
supportsThreads: vi.fn().mockReturnValue(false),
|
||||
isInitialSyncComplete: vi.fn().mockReturnValue(true),
|
||||
getClientWellKnown: vi.fn().mockResolvedValue({}),
|
||||
invite: vi.fn(),
|
||||
});
|
||||
SdkConfig.put({ validated_server_config: {} as ValidatedServerConfig } as IConfigOptions);
|
||||
DMRoomMap.makeShared(mockClient);
|
||||
jest.clearAllMocks();
|
||||
vi.clearAllMocks();
|
||||
vi.spyOn(MatrixClientPeg, "safeGet").mockReturnValue(mockClient);
|
||||
|
||||
room = new Room(roomId, mockClient, mockClient.getSafeUserId());
|
||||
room.addLiveEvents(
|
||||
@@ -174,7 +191,7 @@ describe("InviteDialog", () => {
|
||||
skey: aliceId,
|
||||
}),
|
||||
]);
|
||||
jest.spyOn(DMRoomMap.shared(), "getUniqueRoomsWithIndividuals").mockReturnValue({
|
||||
vi.spyOn(DMRoomMap.shared(), "getUniqueRoomsWithIndividuals").mockReturnValue({
|
||||
[aliceId]: room,
|
||||
});
|
||||
mockClient.getRooms.mockReturnValue([room]);
|
||||
@@ -192,20 +209,20 @@ describe("InviteDialog", () => {
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.restoreAllMocks();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("should label with space name", () => {
|
||||
room.isSpaceRoom = jest.fn().mockReturnValue(true);
|
||||
room.getType = jest.fn().mockReturnValue(RoomType.Space);
|
||||
room.isSpaceRoom = vi.fn().mockReturnValue(true);
|
||||
room.getType = vi.fn().mockReturnValue(RoomType.Space);
|
||||
room.name = "Space";
|
||||
render(<InviteDialog kind={InviteKind.Invite} roomId={roomId} onFinished={jest.fn()} />);
|
||||
render(<InviteDialog kind={InviteKind.Invite} roomId={roomId} onFinished={vi.fn()} />);
|
||||
|
||||
expect(screen.queryByText("Invite to Space")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should label with room name", () => {
|
||||
render(<InviteDialog kind={InviteKind.Invite} roomId={roomId} onFinished={jest.fn()} />);
|
||||
render(<InviteDialog kind={InviteKind.Invite} roomId={roomId} onFinished={vi.fn()} />);
|
||||
expect(screen.getByText(`Invite to ${roomId}`)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -214,7 +231,7 @@ describe("InviteDialog", () => {
|
||||
<InviteDialog
|
||||
kind={InviteKind.Invite}
|
||||
roomId={roomId}
|
||||
onFinished={jest.fn()}
|
||||
onFinished={vi.fn()}
|
||||
initialText="@localpart:server.tld"
|
||||
/>,
|
||||
);
|
||||
@@ -227,7 +244,7 @@ describe("InviteDialog", () => {
|
||||
<InviteDialog
|
||||
kind={InviteKind.Invite}
|
||||
roomId={roomId}
|
||||
onFinished={jest.fn()}
|
||||
onFinished={vi.fn()}
|
||||
initialText="@localpart:server:tld"
|
||||
/>,
|
||||
);
|
||||
@@ -253,7 +270,7 @@ describe("InviteDialog", () => {
|
||||
<InviteDialog
|
||||
kind={kind}
|
||||
roomId={kind === InviteKind.Invite ? roomId : ""}
|
||||
onFinished={jest.fn()}
|
||||
onFinished={vi.fn()}
|
||||
initialText={aliceEmail}
|
||||
/>,
|
||||
);
|
||||
@@ -275,20 +292,20 @@ describe("InviteDialog", () => {
|
||||
<InviteDialog
|
||||
kind={InviteKind.Invite}
|
||||
roomId={roomId}
|
||||
onFinished={jest.fn()}
|
||||
onFinished={vi.fn()}
|
||||
initialText="foobar@email.com"
|
||||
/>,
|
||||
);
|
||||
|
||||
await screen.findByText("foobar@email.com");
|
||||
await screen.findByText("Invite by email");
|
||||
await expect(screen.findByText("foobar@email.com")).resolves.toBeVisible();
|
||||
await expect(screen.findByText("Invite by email")).resolves.toBeVisible();
|
||||
});
|
||||
|
||||
it("should add pasted values", async () => {
|
||||
mockClient.getIdentityServerUrl.mockReturnValue("https://identity-server");
|
||||
mockClient.lookupThreePid.mockResolvedValue({});
|
||||
|
||||
render(<InviteDialog kind={InviteKind.Invite} roomId={roomId} onFinished={jest.fn()} />);
|
||||
render(<InviteDialog kind={InviteKind.Invite} roomId={roomId} onFinished={vi.fn()} />);
|
||||
|
||||
const input = screen.getByTestId("invite-dialog-input");
|
||||
input.focus();
|
||||
@@ -302,7 +319,7 @@ describe("InviteDialog", () => {
|
||||
mockClient.getIdentityServerUrl.mockReturnValue("https://identity-server");
|
||||
mockClient.lookupThreePid.mockResolvedValue({});
|
||||
|
||||
render(<InviteDialog kind={InviteKind.Invite} roomId={roomId} onFinished={jest.fn()} />);
|
||||
render(<InviteDialog kind={InviteKind.Invite} roomId={roomId} onFinished={vi.fn()} />);
|
||||
|
||||
const input = screen.getByTestId("invite-dialog-input");
|
||||
input.focus();
|
||||
@@ -313,7 +330,7 @@ describe("InviteDialog", () => {
|
||||
});
|
||||
|
||||
it("should allow to invite multiple emails to a room", async () => {
|
||||
render(<InviteDialog kind={InviteKind.Invite} roomId={roomId} onFinished={jest.fn()} />);
|
||||
render(<InviteDialog kind={InviteKind.Invite} roomId={roomId} onFinished={vi.fn()} />);
|
||||
|
||||
await enterIntoSearchField(aliceEmail);
|
||||
expectPill(aliceEmail);
|
||||
@@ -332,7 +349,7 @@ describe("InviteDialog", () => {
|
||||
});
|
||||
|
||||
it("should allow to invite more than one email to a DM", async () => {
|
||||
render(<InviteDialog kind={InviteKind.Dm} onFinished={jest.fn()} />);
|
||||
render(<InviteDialog kind={InviteKind.Dm} onFinished={vi.fn()} />);
|
||||
|
||||
await enterIntoSearchField(aliceEmail);
|
||||
expectPill(aliceEmail);
|
||||
@@ -343,7 +360,7 @@ describe("InviteDialog", () => {
|
||||
});
|
||||
|
||||
it("should not allow to invite more than one email to a DM", async () => {
|
||||
render(<InviteDialog kind={InviteKind.Dm} onFinished={jest.fn()} />);
|
||||
render(<InviteDialog kind={InviteKind.Dm} onFinished={vi.fn()} />);
|
||||
|
||||
// Start with an email → should convert to a pill
|
||||
await enterIntoSearchField(aliceEmail);
|
||||
@@ -363,7 +380,7 @@ describe("InviteDialog", () => {
|
||||
});
|
||||
|
||||
it("should not allow to invite a MXID and an email to a DM", async () => {
|
||||
render(<InviteDialog kind={InviteKind.Dm} onFinished={jest.fn()} />);
|
||||
render(<InviteDialog kind={InviteKind.Dm} onFinished={vi.fn()} />);
|
||||
|
||||
// Start with a MXID → should convert to a pill
|
||||
await enterIntoSearchField(carolId);
|
||||
@@ -377,7 +394,7 @@ describe("InviteDialog", () => {
|
||||
});
|
||||
|
||||
it("should start a DM if the profile is available", async () => {
|
||||
render(<InviteDialog kind={InviteKind.Dm} onFinished={jest.fn()} />);
|
||||
render(<InviteDialog kind={InviteKind.Dm} onFinished={vi.fn()} />);
|
||||
await enterIntoSearchField(aliceId);
|
||||
await userEvent.click(screen.getByRole("button", { name: "Go" }));
|
||||
expect(startDmOnFirstMessage).toHaveBeenCalledWith(mockClient, [
|
||||
@@ -388,7 +405,7 @@ describe("InviteDialog", () => {
|
||||
});
|
||||
|
||||
it("should not allow pasting the same user multiple times", async () => {
|
||||
render(<InviteDialog kind={InviteKind.Invite} roomId={roomId} onFinished={jest.fn()} />);
|
||||
render(<InviteDialog kind={InviteKind.Invite} roomId={roomId} onFinished={vi.fn()} />);
|
||||
|
||||
const input = screen.getByTestId("invite-dialog-input");
|
||||
input.focus();
|
||||
@@ -401,7 +418,7 @@ describe("InviteDialog", () => {
|
||||
});
|
||||
|
||||
it("should add to selection on click of user tile", async () => {
|
||||
render(<InviteDialog kind={InviteKind.Invite} roomId={roomId} onFinished={jest.fn()} />);
|
||||
render(<InviteDialog kind={InviteKind.Invite} roomId={roomId} onFinished={vi.fn()} />);
|
||||
|
||||
const input = screen.getByTestId("invite-dialog-input");
|
||||
input.focus();
|
||||
@@ -418,18 +435,18 @@ describe("InviteDialog", () => {
|
||||
it("should show a spinner", async () => {
|
||||
mockClient.invite.mockReturnValue(new Promise(() => {}));
|
||||
|
||||
render(<InviteDialog kind={InviteKind.Invite} roomId={roomId} onFinished={jest.fn()} />);
|
||||
render(<InviteDialog kind={InviteKind.Invite} roomId={roomId} onFinished={vi.fn()} />);
|
||||
await enterIntoSearchField(bobId);
|
||||
await userEvent.click(screen.getByRole("button", { name: "Invite" }));
|
||||
|
||||
await screen.findByText("Preparing invitations...");
|
||||
await expect(screen.findByText("Preparing invitations...")).resolves.toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
describe("when inviting a user with an unknown profile", () => {
|
||||
beforeEach(async () => {
|
||||
mocked(startDmOnFirstMessage).mockClear();
|
||||
render(<InviteDialog kind={InviteKind.Dm} onFinished={jest.fn()} />);
|
||||
vi.mocked(startDmOnFirstMessage).mockClear();
|
||||
render(<InviteDialog kind={InviteKind.Dm} onFinished={vi.fn()} />);
|
||||
await enterIntoSearchField(carolId);
|
||||
await userEvent.click(screen.getByRole("button", { name: "Go" }));
|
||||
// modal rendering has some weird sleeps - fake timers will mess up the entire test
|
||||
@@ -452,7 +469,7 @@ describe("InviteDialog", () => {
|
||||
<InviteDialog
|
||||
kind={InviteKind.Invite}
|
||||
roomId={roomId}
|
||||
onFinished={jest.fn()}
|
||||
onFinished={vi.fn()}
|
||||
initialText="@localpart:server.tld"
|
||||
/>,
|
||||
);
|
||||
@@ -462,7 +479,7 @@ describe("InviteDialog", () => {
|
||||
|
||||
describe("when inviting a user whose cryptographic identity we do not know", () => {
|
||||
beforeEach(() => {
|
||||
mocked(mockClient.getCrypto()!.getUserVerificationStatus).mockImplementation(async (u) => {
|
||||
vi.mocked(mockClient.getCrypto()!.getUserVerificationStatus).mockImplementation(async (u) => {
|
||||
return new UserVerificationStatus(false, false, false, false);
|
||||
});
|
||||
});
|
||||
@@ -475,7 +492,7 @@ describe("InviteDialog", () => {
|
||||
<InviteDialog
|
||||
kind={kind as InviteKind.Invite | InviteKind.Dm}
|
||||
roomId={roomId}
|
||||
onFinished={jest.fn()}
|
||||
onFinished={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
@@ -485,8 +502,8 @@ describe("InviteDialog", () => {
|
||||
await userEvent.click(screen.getByRole("button", { name: goButtonName }));
|
||||
await screen.findByText("Confirm inviting them", { exact: false });
|
||||
|
||||
expect(mocked(mockClient.getCrypto()!.getUserVerificationStatus)).toHaveBeenCalledTimes(1);
|
||||
expect(mocked(mockClient.getCrypto()!.getUserVerificationStatus)).toHaveBeenCalledWith(aliceId);
|
||||
expect(vi.mocked(mockClient.getCrypto()!.getUserVerificationStatus)).toHaveBeenCalledTimes(1);
|
||||
expect(vi.mocked(mockClient.getCrypto()!.getUserVerificationStatus)).toHaveBeenCalledWith(aliceId);
|
||||
});
|
||||
|
||||
it("should show a warning when inviting by email address", async () => {
|
||||
@@ -495,7 +512,7 @@ describe("InviteDialog", () => {
|
||||
await screen.findByText("Confirm inviting them", { exact: false });
|
||||
|
||||
// We shouldn't call getUserVerificationStatus on an email address
|
||||
expect(mocked(mockClient.getCrypto()!.getUserVerificationStatus)).not.toHaveBeenCalled();
|
||||
expect(vi.mocked(mockClient.getCrypto()!.getUserVerificationStatus)).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -11,7 +11,7 @@ import { EventType, type Room, RoomMember } from "matrix-js-sdk/src/matrix";
|
||||
import { KnownMembership } from "matrix-js-sdk/src/types";
|
||||
import { type MatrixCall } from "matrix-js-sdk/src/webrtc/call";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { uniqBy } from "lodash";
|
||||
import { debounce, uniqBy } from "lodash";
|
||||
import { Pill, PillInput, RichList } from "@element-hq/web-shared-components";
|
||||
import { DialPadIcon, UserProfileSolidIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
|
||||
@@ -187,7 +187,6 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
|
||||
initialText: "",
|
||||
};
|
||||
|
||||
private debounceTimer: number | null = null; // actually number because we're in the browser
|
||||
private editorRef = createRef<HTMLInputElement>();
|
||||
private numberEntryFieldRef = createRef<Field>();
|
||||
private unmounted = false;
|
||||
@@ -256,6 +255,7 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
|
||||
|
||||
public componentWillUnmount(): void {
|
||||
this.unmounted = true;
|
||||
this.updateSuggestions.cancel();
|
||||
}
|
||||
|
||||
private onConsultFirstChange = (ev: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
@@ -530,125 +530,127 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
|
||||
this.props.onFinished(false);
|
||||
};
|
||||
|
||||
private updateSuggestions = async (term: string): Promise<void> => {
|
||||
MatrixClientPeg.safeGet()
|
||||
.searchUserDirectory({ term })
|
||||
.then(async (r): Promise<void> => {
|
||||
if (term !== this.state.filterText) {
|
||||
// Discard the results - we were probably too slow on the server-side to make
|
||||
// these results useful. This is a race we want to avoid because we could overwrite
|
||||
// more accurate results.
|
||||
return;
|
||||
}
|
||||
|
||||
if (!r.results) r.results = [];
|
||||
|
||||
// While we're here, try and autocomplete a search result for the mxid itself
|
||||
// if there's no matches (and the input looks like a mxid).
|
||||
if (term[0] === "@" && term.indexOf(":") > 1) {
|
||||
try {
|
||||
const profile = await this.profilesStore.getOrFetchProfile(term, { shouldThrow: true });
|
||||
|
||||
if (profile) {
|
||||
// If we have a profile, we have enough information to assume that
|
||||
// the mxid can be invited - add it to the list. We stick it at the
|
||||
// top so it is most obviously presented to the user.
|
||||
r.results.splice(0, 0, {
|
||||
user_id: term,
|
||||
display_name: profile["displayname"],
|
||||
avatar_url: profile["avatar_url"],
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
logger.warn("Non-fatal error trying to make an invite for a user ID", e);
|
||||
private updateSuggestions = debounce(
|
||||
async (term: string): Promise<void> => {
|
||||
MatrixClientPeg.safeGet()
|
||||
.searchUserDirectory({ term })
|
||||
.then(async (r): Promise<void> => {
|
||||
if (term !== this.state.filterText) {
|
||||
// Discard the results - we were probably too slow on the server-side to make
|
||||
// these results useful. This is a race we want to avoid because we could overwrite
|
||||
// more accurate results.
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({
|
||||
serverResultsMixin: r.results.map((u) => ({
|
||||
userId: u.user_id,
|
||||
user: new DirectoryMember(u),
|
||||
})),
|
||||
if (!r.results) r.results = [];
|
||||
|
||||
// While we're here, try and autocomplete a search result for the mxid itself
|
||||
// if there's no matches (and the input looks like a mxid).
|
||||
if (term[0] === "@" && term.indexOf(":") > 1) {
|
||||
try {
|
||||
const profile = await this.profilesStore.getOrFetchProfile(term, { shouldThrow: true });
|
||||
|
||||
if (profile) {
|
||||
// If we have a profile, we have enough information to assume that
|
||||
// the mxid can be invited - add it to the list. We stick it at the
|
||||
// top so it is most obviously presented to the user.
|
||||
r.results.splice(0, 0, {
|
||||
user_id: term,
|
||||
display_name: profile["displayname"],
|
||||
avatar_url: profile["avatar_url"],
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
logger.warn("Non-fatal error trying to make an invite for a user ID", e);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.unmounted) return;
|
||||
this.setState({
|
||||
serverResultsMixin: r.results.map((u) => ({
|
||||
userId: u.user_id,
|
||||
user: new DirectoryMember(u),
|
||||
})),
|
||||
});
|
||||
})
|
||||
.catch((e) => {
|
||||
logger.error("Error searching user directory:");
|
||||
logger.error(e);
|
||||
if (this.unmounted) return;
|
||||
this.setState({ serverResultsMixin: [] }); // clear results because it's moderately fatal
|
||||
});
|
||||
})
|
||||
.catch((e) => {
|
||||
logger.error("Error searching user directory:");
|
||||
logger.error(e);
|
||||
this.setState({ serverResultsMixin: [] }); // clear results because it's moderately fatal
|
||||
});
|
||||
|
||||
// Whenever we search the directory, also try to search the identity server. It's
|
||||
// all debounced the same anyways.
|
||||
if (!this.state.canUseIdentityServer) {
|
||||
// The user doesn't have an identity server set - warn them of that.
|
||||
this.setState({ tryingIdentityServer: true });
|
||||
return;
|
||||
}
|
||||
if (Email.looksValid(term) && this.canInviteThirdParty() && SettingsStore.getValue(UIFeature.IdentityServer)) {
|
||||
// Start off by suggesting the plain email while we try and resolve it
|
||||
// to a real account.
|
||||
this.setState({
|
||||
// per above: the userId is a lie here - it's just a regular identifier
|
||||
threepidResultsMixin: [{ user: new ThreepidMember(term), userId: term }],
|
||||
});
|
||||
try {
|
||||
const authClient = new IdentityAuthClient();
|
||||
const token = await authClient.getAccessToken();
|
||||
// No token → unable to try a lookup
|
||||
if (!token) return;
|
||||
|
||||
if (term !== this.state.filterText) return; // abandon hope
|
||||
|
||||
const lookup = await MatrixClientPeg.safeGet().lookupThreePid("email", term, token);
|
||||
if (term !== this.state.filterText) return; // abandon hope
|
||||
|
||||
if (!lookup || !("mxid" in lookup)) {
|
||||
// We weren't able to find anyone - we're already suggesting the plain email
|
||||
// as an alternative, so do nothing.
|
||||
return;
|
||||
}
|
||||
|
||||
// We append the user suggestion to give the user an option to click
|
||||
// the email anyways, and so we don't cause things to jump around. In
|
||||
// theory, the user would see the user pop up and think "ah yes, that
|
||||
// person!"
|
||||
const profile = await this.profilesStore.getOrFetchProfile(lookup.mxid);
|
||||
if (term !== this.state.filterText || !profile) return; // abandon hope
|
||||
this.setState({
|
||||
threepidResultsMixin: [
|
||||
...this.state.threepidResultsMixin,
|
||||
{
|
||||
user: new DirectoryMember({
|
||||
user_id: lookup.mxid,
|
||||
display_name: profile.displayname,
|
||||
avatar_url: profile.avatar_url,
|
||||
}),
|
||||
// Use the search term as identifier, so that it shows up in suggestions.
|
||||
userId: term,
|
||||
},
|
||||
],
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error("Error searching identity server:");
|
||||
logger.error(e);
|
||||
this.setState({ threepidResultsMixin: [] }); // clear results because it's moderately fatal
|
||||
// Whenever we search the directory, also try to search the identity server. It's
|
||||
// all debounced the same anyways.
|
||||
if (!this.state.canUseIdentityServer) {
|
||||
// The user doesn't have an identity server set - warn them of that.
|
||||
this.setState({ tryingIdentityServer: true });
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
if (
|
||||
Email.looksValid(term) &&
|
||||
this.canInviteThirdParty() &&
|
||||
SettingsStore.getValue(UIFeature.IdentityServer)
|
||||
) {
|
||||
// Start off by suggesting the plain email while we try and resolve it
|
||||
// to a real account.
|
||||
this.setState({
|
||||
// per above: the userId is a lie here - it's just a regular identifier
|
||||
threepidResultsMixin: [{ user: new ThreepidMember(term), userId: term }],
|
||||
});
|
||||
try {
|
||||
const authClient = new IdentityAuthClient();
|
||||
const token = await authClient.getAccessToken();
|
||||
// No token → unable to try a lookup
|
||||
if (!token) return;
|
||||
|
||||
if (term !== this.state.filterText) return; // abandon hope
|
||||
|
||||
const lookup = await MatrixClientPeg.safeGet().lookupThreePid("email", term, token);
|
||||
if (term !== this.state.filterText) return; // abandon hope
|
||||
|
||||
if (!lookup || !("mxid" in lookup)) {
|
||||
// We weren't able to find anyone - we're already suggesting the plain email
|
||||
// as an alternative, so do nothing.
|
||||
return;
|
||||
}
|
||||
|
||||
// We append the user suggestion to give the user an option to click
|
||||
// the email anyways, and so we don't cause things to jump around. In
|
||||
// theory, the user would see the user pop up and think "ah yes, that
|
||||
// person!"
|
||||
const profile = await this.profilesStore.getOrFetchProfile(lookup.mxid);
|
||||
if (term !== this.state.filterText || !profile) return; // abandon hope
|
||||
if (this.unmounted) return;
|
||||
this.setState({
|
||||
threepidResultsMixin: [
|
||||
...this.state.threepidResultsMixin,
|
||||
{
|
||||
user: new DirectoryMember({
|
||||
user_id: lookup.mxid,
|
||||
display_name: profile.displayname,
|
||||
avatar_url: profile.avatar_url,
|
||||
}),
|
||||
// Use the search term as identifier, so that it shows up in suggestions.
|
||||
userId: term,
|
||||
},
|
||||
],
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error("Error searching identity server:");
|
||||
logger.error(e);
|
||||
if (this.unmounted) return;
|
||||
this.setState({ threepidResultsMixin: [] }); // clear results because it's moderately fatal
|
||||
}
|
||||
}
|
||||
},
|
||||
150, // 150ms debounce (human reaction time + some)
|
||||
);
|
||||
|
||||
private updateFilter = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
const term = e.target.value;
|
||||
this.setState({ filterText: term });
|
||||
|
||||
// Debounce server lookups to reduce spam. We don't clear the existing server
|
||||
// results because they might still be vaguely accurate, likewise for races which
|
||||
// could happen here.
|
||||
if (this.debounceTimer) {
|
||||
clearTimeout(this.debounceTimer);
|
||||
}
|
||||
this.debounceTimer = window.setTimeout(() => {
|
||||
this.updateSuggestions(term);
|
||||
}, 150); // 150ms debounce (human reaction time + some)
|
||||
this.updateSuggestions(term);
|
||||
};
|
||||
|
||||
private showMoreRecents = (): void => {
|
||||
@@ -898,20 +900,18 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
|
||||
);
|
||||
}
|
||||
|
||||
const tiles = toRender.map((r) => (
|
||||
<DMRoomTile
|
||||
member={r.user}
|
||||
lastActiveTs={lastActive(r)}
|
||||
key={r.user.userId}
|
||||
onToggle={this.toggleMember}
|
||||
isSelected={this.state.targets.some((t) => t.userId === r.userId)}
|
||||
/>
|
||||
));
|
||||
|
||||
return (
|
||||
<div className="mx_InviteDialog_section">
|
||||
<RichList title={sectionName} titleAttributes={{ "role": "heading", "aria-level": 3 }}>
|
||||
{tiles}
|
||||
{toRender.map((r) => (
|
||||
<DMRoomTile
|
||||
member={r.user}
|
||||
lastActiveTs={lastActive(r)}
|
||||
key={r.user.userId}
|
||||
onToggle={this.toggleMember}
|
||||
isSelected={this.state.targets.some((t) => t.userId === r.userId)}
|
||||
/>
|
||||
))}
|
||||
</RichList>
|
||||
{showMore}
|
||||
</div>
|
||||
@@ -919,10 +919,6 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
|
||||
}
|
||||
|
||||
private renderEditor(): JSX.Element {
|
||||
const targets = this.state.targets.map((t) => (
|
||||
<DMUserTile member={t} onRemove={this.state.busy ? undefined : this.removeMember} key={t.userId} />
|
||||
));
|
||||
|
||||
return (
|
||||
<PillInput
|
||||
data-testid="invite-dialog-input-wrapper"
|
||||
@@ -944,7 +940,9 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
|
||||
!this.state.busy && this.removeMember(this.state.targets[this.state.targets.length - 1])
|
||||
}
|
||||
>
|
||||
{targets}
|
||||
{this.state.targets.map((t) => (
|
||||
<DMUserTile member={t} onRemove={this.state.busy ? undefined : this.removeMember} key={t.userId} />
|
||||
))}
|
||||
</PillInput>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import BaseAvatar from "../../avatars/BaseAvatar.tsx";
|
||||
import { mediaFromMxc } from "../../../../customisations/Media.ts";
|
||||
import UserIdentifierCustomisations from "../../../../customisations/UserIdentifier.ts";
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import { Icon as EmailPillAvatarIcon } from "../../../../../res/img/icon-email-pill-avatar.svg";
|
||||
import EmailPillAvatarIcon from "../../../../../res/img/icon-email-pill-avatar.svg?react";
|
||||
|
||||
interface IDMRoomTileProps {
|
||||
member: Member;
|
||||
|
||||
@@ -25,11 +25,11 @@ import PlatformPeg from "../../../PlatformPeg";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { mediaFromMxc } from "../../../customisations/Media";
|
||||
import { PosthogAnalytics } from "../../../PosthogAnalytics";
|
||||
import { Icon as FacebookIcon } from "../../../../res/img/element-icons/brands/facebook.svg";
|
||||
import { Icon as GithubIcon } from "../../../../res/img/element-icons/brands/github.svg";
|
||||
import { Icon as GitlabIcon } from "../../../../res/img/element-icons/brands/gitlab.svg";
|
||||
import { Icon as GoogleIcon } from "../../../../res/img/element-icons/brands/google.svg";
|
||||
import { Icon as TwitterIcon } from "../../../../res/img/element-icons/brands/twitter.svg";
|
||||
import FacebookIcon from "../../../../res/img/element-icons/brands/facebook.svg?react";
|
||||
import GithubIcon from "../../../../res/img/element-icons/brands/github.svg?react";
|
||||
import GitlabIcon from "../../../../res/img/element-icons/brands/gitlab.svg?react";
|
||||
import GoogleIcon from "../../../../res/img/element-icons/brands/google.svg?react";
|
||||
import TwitterIcon from "../../../../res/img/element-icons/brands/twitter.svg?react";
|
||||
|
||||
interface ISSOButtonProps extends IProps {
|
||||
idp?: IIdentityProvider;
|
||||
|
||||
@@ -10,7 +10,7 @@ import React from "react";
|
||||
import classNames from "classnames";
|
||||
import LocationMarkerIcon from "@vector-im/compound-design-tokens/assets/web/icons/location-pin-solid";
|
||||
|
||||
import { Icon as MapFallbackImage } from "../../../../res/img/location/map.svg";
|
||||
import MapFallbackImage from "../../../../res/img/location/map.svg?react";
|
||||
import Spinner from "../elements/Spinner";
|
||||
|
||||
interface Props extends React.HTMLAttributes<HTMLDivElement> {
|
||||
|
||||
@@ -12,7 +12,7 @@ import { type PollAnswerSubevent } from "matrix-js-sdk/src/extensible_events_v1/
|
||||
import { CheckIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { Icon as TrophyIcon } from "../../../../res/img/element-icons/trophy.svg";
|
||||
import TrophyIcon from "../../../../res/img/element-icons/trophy.svg?react";
|
||||
import StyledRadioButton from "../elements/StyledRadioButton";
|
||||
|
||||
type PollOptionContentProps = {
|
||||
|
||||
@@ -10,7 +10,7 @@ import { MessageTimestampView } from "@element-hq/web-shared-components";
|
||||
|
||||
import { type EventTileViewModel } from "../../../../viewmodels/room/timeline/event-tile/EventTileViewModel";
|
||||
import { type MessageTimestampViewModelProps } from "../../../../viewmodels/room/timeline/event-tile/timestamp/MessageTimestampViewModel.ts";
|
||||
import { Icon as LateIcon } from "../../../../../res/img/sensor.svg";
|
||||
import LateIcon from "../../../../../res/img/sensor.svg?react";
|
||||
|
||||
/**
|
||||
* Props for the {@link MessageTimestampAdapter} component.
|
||||
|
||||
+50
-52
@@ -6,28 +6,27 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { vi, describe, it, expect, beforeEach, afterEach, type Mocked } from "vitest";
|
||||
import React from "react";
|
||||
import { fireEvent, getByLabelText, getByText, render, screen, waitFor } from "jest-matrix-react";
|
||||
import { fireEvent, getByLabelText, getByText, render, screen, waitFor } from "test-utils-rtl";
|
||||
import { type EventTimeline, JoinRule, Room } from "matrix-js-sdk/src/matrix";
|
||||
import { KnownMembership } from "matrix-js-sdk/src/types";
|
||||
import { getMockClientWithEventEmitter, mockClientMethodsUser, TestSDKContext } from "test-utils";
|
||||
|
||||
import { SDKContext } from "../../../../../../src/contexts/SDKContext";
|
||||
import { TestSDKContext } from "../../../../TestSDKContext.ts";
|
||||
import { getMockClientWithEventEmitter, mockClientMethodsUser } from "../../../../../test-utils";
|
||||
import {
|
||||
CallGuestLinkButton,
|
||||
JoinRuleDialog,
|
||||
} from "../../../../../../src/components/views/rooms/RoomHeader/CallGuestLinkButton";
|
||||
import Modal from "../../../../../../src/Modal";
|
||||
import SdkConfig from "../../../../../../src/SdkConfig";
|
||||
import { ShareDialog } from "../../../../../../src/components/views/dialogs/ShareDialog";
|
||||
import { _t } from "../../../../../../src/languageHandler";
|
||||
import SettingsStore from "../../../../../../src/settings/SettingsStore";
|
||||
import { SDKContext } from "../../../../contexts/SDKContext";
|
||||
import { CallGuestLinkButton, JoinRuleDialog } from "./CallGuestLinkButton";
|
||||
import Modal from "../../../../Modal";
|
||||
import SdkConfig from "../../../../SdkConfig";
|
||||
import { ShareDialog } from "../../dialogs/ShareDialog";
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import SettingsStore from "../../../../settings/SettingsStore";
|
||||
|
||||
describe("<CallGuestLinkButton />", () => {
|
||||
const roomId = "!room:server.org";
|
||||
let sdkContext!: TestSDKContext;
|
||||
let modalSpy: jest.SpyInstance;
|
||||
let modalSpy: Mocked<any>;
|
||||
let modalResolve: (value: unknown[] | PromiseLike<unknown[]>) => void;
|
||||
let room: Room;
|
||||
|
||||
@@ -47,11 +46,11 @@ describe("<CallGuestLinkButton />", () => {
|
||||
*/
|
||||
const makeRoom = (isVideoRoom = true): Room => {
|
||||
const room = new Room(roomId, sdkContext.client!, sdkContext.client!.getSafeUserId());
|
||||
sdkContext.client!.getRoomDirectoryVisibility = jest.fn().mockResolvedValue("public");
|
||||
jest.spyOn(room, "isElementVideoRoom").mockReturnValue(isVideoRoom);
|
||||
sdkContext.client!.getRoomDirectoryVisibility = vi.fn().mockResolvedValue("public");
|
||||
vi.spyOn(room, "isElementVideoRoom").mockReturnValue(isVideoRoom);
|
||||
// stub
|
||||
jest.spyOn(room, "getPendingEvents").mockReturnValue([]);
|
||||
jest.spyOn(room, "getVersion").mockReturnValue("9");
|
||||
vi.spyOn(room, "getPendingEvents").mockReturnValue([]);
|
||||
vi.spyOn(room, "getVersion").mockReturnValue("9");
|
||||
return room;
|
||||
};
|
||||
function mockRoomMembers(room: Room, count: number) {
|
||||
@@ -64,7 +63,7 @@ describe("<CallGuestLinkButton />", () => {
|
||||
}));
|
||||
|
||||
room.currentState.setJoinedMemberCount(members.length);
|
||||
room.getJoinedMembers = jest.fn().mockReturnValue(members);
|
||||
room.getJoinedMembers = vi.fn().mockReturnValue(members);
|
||||
}
|
||||
|
||||
const getComponent = (room: Room) =>
|
||||
@@ -76,29 +75,29 @@ describe("<CallGuestLinkButton />", () => {
|
||||
beforeEach(() => {
|
||||
const client = getMockClientWithEventEmitter({
|
||||
...mockClientMethodsUser(),
|
||||
sendStateEvent: jest.fn(),
|
||||
getVisibleRooms: jest.fn().mockReturnValue([]),
|
||||
sendStateEvent: vi.fn(),
|
||||
getVisibleRooms: vi.fn().mockReturnValue([]),
|
||||
});
|
||||
sdkContext = new TestSDKContext();
|
||||
sdkContext._client = client;
|
||||
const modalPromise = new Promise<unknown[]>((resolve) => {
|
||||
modalResolve = resolve;
|
||||
});
|
||||
modalSpy = jest.spyOn(Modal, "createDialog").mockReturnValue({ finished: modalPromise, close: jest.fn() });
|
||||
modalSpy = vi.spyOn(Modal, "createDialog").mockReturnValue({ finished: modalPromise, close: vi.fn() });
|
||||
room = makeRoom();
|
||||
mockRoomMembers(room, 3);
|
||||
|
||||
jest.spyOn(SdkConfig, "get").mockImplementation((key) => {
|
||||
vi.spyOn(SdkConfig, "get").mockImplementation((key) => {
|
||||
if (key === "element_call") {
|
||||
return { guest_spa_url: "https://guest_spa_url.com", url: "https://spa_url.com" };
|
||||
}
|
||||
return oldGet(key);
|
||||
});
|
||||
jest.spyOn(room, "hasEncryptionStateEvent").mockReturnValue(true);
|
||||
jest.spyOn(sdkContext.roomViewStore, "isViewingCall").mockReturnValue(true);
|
||||
vi.spyOn(room, "hasEncryptionStateEvent").mockReturnValue(true);
|
||||
vi.spyOn(sdkContext.roomViewStore, "isViewingCall").mockReturnValue(true);
|
||||
});
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("shows the JoinRuleDialog on click with private join rules", async () => {
|
||||
@@ -106,7 +105,7 @@ describe("<CallGuestLinkButton />", () => {
|
||||
fireEvent.click(screen.getByRole("button", { name: "Share call link" }));
|
||||
expect(modalSpy).toHaveBeenCalledWith(JoinRuleDialog, { room, canInvite: false });
|
||||
// pretend public was selected
|
||||
jest.spyOn(room, "getJoinRule").mockReturnValue(JoinRule.Public);
|
||||
vi.spyOn(room, "getJoinRule").mockReturnValue(JoinRule.Public);
|
||||
modalResolve([]);
|
||||
await new Promise(process.nextTick);
|
||||
const callParams = modalSpy.mock.calls[1];
|
||||
@@ -117,7 +116,7 @@ describe("<CallGuestLinkButton />", () => {
|
||||
});
|
||||
|
||||
it("shows the ShareDialog on click with public join rules", () => {
|
||||
jest.spyOn(room, "getJoinRule").mockReturnValue(JoinRule.Public);
|
||||
vi.spyOn(room, "getJoinRule").mockReturnValue(JoinRule.Public);
|
||||
getComponent(room);
|
||||
fireEvent.click(screen.getByRole("button", { name: "Share call link" }));
|
||||
const callParams = modalSpy.mock.calls[0];
|
||||
@@ -128,8 +127,8 @@ describe("<CallGuestLinkButton />", () => {
|
||||
});
|
||||
|
||||
it("shows the ShareDialog on click with knock join rules", () => {
|
||||
jest.spyOn(room, "getJoinRule").mockReturnValue(JoinRule.Knock);
|
||||
jest.spyOn(room, "canInvite").mockReturnValue(true);
|
||||
vi.spyOn(room, "getJoinRule").mockReturnValue(JoinRule.Knock);
|
||||
vi.spyOn(room, "canInvite").mockReturnValue(true);
|
||||
getComponent(room);
|
||||
fireEvent.click(screen.getByRole("button", { name: "Share call link" }));
|
||||
const callParams = modalSpy.mock.calls[0];
|
||||
@@ -141,21 +140,21 @@ describe("<CallGuestLinkButton />", () => {
|
||||
|
||||
it("don't show external conference button if room not public nor knock and the user cannot change join rules", () => {
|
||||
// preparation for if we refactor the related code to not use currentState.
|
||||
jest.spyOn(room, "getLiveTimeline").mockReturnValue({
|
||||
getState: jest.fn().mockReturnValue({
|
||||
maySendStateEvent: jest.fn().mockReturnValue(false),
|
||||
vi.spyOn(room, "getLiveTimeline").mockReturnValue({
|
||||
getState: vi.fn().mockReturnValue({
|
||||
maySendStateEvent: vi.fn().mockReturnValue(false),
|
||||
}),
|
||||
} as unknown as EventTimeline);
|
||||
jest.spyOn(room.currentState, "maySendStateEvent").mockReturnValue(false);
|
||||
vi.spyOn(room.currentState, "maySendStateEvent").mockReturnValue(false);
|
||||
getComponent(room);
|
||||
expect(screen.queryByLabelText("Share call link")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("don't show external conference button if now guest spa link is configured", () => {
|
||||
jest.spyOn(room, "getJoinRule").mockReturnValue(JoinRule.Public);
|
||||
jest.spyOn(sdkContext.roomViewStore, "isViewingCall").mockReturnValue(true);
|
||||
vi.spyOn(room, "getJoinRule").mockReturnValue(JoinRule.Public);
|
||||
vi.spyOn(sdkContext.roomViewStore, "isViewingCall").mockReturnValue(true);
|
||||
|
||||
jest.spyOn(SdkConfig, "get").mockImplementation((key) => {
|
||||
vi.spyOn(SdkConfig, "get").mockImplementation((key) => {
|
||||
if (key === "element_call") {
|
||||
return { url: "https://example2.com" };
|
||||
}
|
||||
@@ -167,7 +166,7 @@ describe("<CallGuestLinkButton />", () => {
|
||||
// configured so that the call link button is shown.
|
||||
expect(screen.queryByLabelText("Share call link")).not.toBeInTheDocument();
|
||||
|
||||
jest.spyOn(SdkConfig, "get").mockImplementation((key) => {
|
||||
vi.spyOn(SdkConfig, "get").mockImplementation((key) => {
|
||||
if (key === "element_call") {
|
||||
return { guest_spa_url: "https://guest_spa_url.com", url: "https://example2.com" };
|
||||
}
|
||||
@@ -179,11 +178,11 @@ describe("<CallGuestLinkButton />", () => {
|
||||
});
|
||||
|
||||
it("opens the share dialog with the correct share link in an encrypted room", () => {
|
||||
jest.spyOn(room, "getJoinRule").mockReturnValue(JoinRule.Public);
|
||||
jest.spyOn(sdkContext.roomViewStore, "isViewingCall").mockReturnValue(true);
|
||||
vi.spyOn(room, "getJoinRule").mockReturnValue(JoinRule.Public);
|
||||
vi.spyOn(sdkContext.roomViewStore, "isViewingCall").mockReturnValue(true);
|
||||
|
||||
getComponent(room);
|
||||
const modalSpy = jest.spyOn(Modal, "createDialog");
|
||||
const modalSpy = vi.spyOn(Modal, "createDialog");
|
||||
fireEvent.click(getByLabelText(document.body, _t("voip|get_call_link")));
|
||||
// const target =
|
||||
// "https://guest_spa_url.com/room/#/!room:server.org?roomId=%21room%3Aserver.org&perParticipantE2EE=true&viaServers=example.org";
|
||||
@@ -200,19 +199,19 @@ describe("<CallGuestLinkButton />", () => {
|
||||
});
|
||||
|
||||
it("share dialog has correct link in an unencrypted room", () => {
|
||||
jest.spyOn(room, "getJoinRule").mockReturnValue(JoinRule.Public);
|
||||
jest.spyOn(room, "hasEncryptionStateEvent").mockReturnValue(false);
|
||||
jest.spyOn(sdkContext.roomViewStore, "isViewingCall").mockReturnValue(true);
|
||||
vi.spyOn(room, "getJoinRule").mockReturnValue(JoinRule.Public);
|
||||
vi.spyOn(room, "hasEncryptionStateEvent").mockReturnValue(false);
|
||||
vi.spyOn(sdkContext.roomViewStore, "isViewingCall").mockReturnValue(true);
|
||||
|
||||
getComponent(room);
|
||||
const modalSpy = jest.spyOn(Modal, "createDialog");
|
||||
const modalSpy = vi.spyOn(Modal, "createDialog");
|
||||
fireEvent.click(getByLabelText(document.body, _t("voip|get_call_link")));
|
||||
const arg1 = modalSpy.mock.calls[0][1] as any;
|
||||
expect(arg1.target.toString()).toEqual(targetUnencrypted);
|
||||
});
|
||||
|
||||
describe("<JoinRuleDialog />", () => {
|
||||
const onFinished = jest.fn();
|
||||
const onFinished = vi.fn();
|
||||
|
||||
const getComponent = (room: Room, canInvite: boolean = true) =>
|
||||
render(<JoinRuleDialog room={room} canInvite={canInvite} onFinished={onFinished} />, {
|
||||
@@ -221,7 +220,7 @@ describe("<CallGuestLinkButton />", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
// feature_ask_to_join enabled
|
||||
jest.spyOn(SettingsStore, "getValue").mockReturnValue(true);
|
||||
vi.spyOn(SettingsStore, "getValue").mockReturnValue(true);
|
||||
});
|
||||
|
||||
it("shows ask to join if feature is enabled", () => {
|
||||
@@ -233,16 +232,15 @@ describe("<CallGuestLinkButton />", () => {
|
||||
expect(screen.queryByRole("radio", { name: "Ask to join ( Recommended )" })).not.toBeInTheDocument();
|
||||
});
|
||||
it("doesn't show ask to join if feature is disabled", () => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockReturnValue(false);
|
||||
vi.spyOn(SettingsStore, "getValue").mockReturnValue(false);
|
||||
getComponent(room);
|
||||
expect(screen.queryByRole("radio", { name: "Ask to join ( Recommended )" })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("sends correct state event on click", async () => {
|
||||
const sendStateSpy = jest.spyOn(sdkContext.client!, "sendStateEvent");
|
||||
const sendStateSpy = vi.spyOn(sdkContext.client!, "sendStateEvent");
|
||||
|
||||
let container;
|
||||
container = getComponent(room).container;
|
||||
getComponent(room);
|
||||
fireEvent.click(screen.getByRole("radio", { name: "Ask to join ( Recommended )" }));
|
||||
expect(sendStateSpy).toHaveBeenCalledWith(
|
||||
"!room:server.org",
|
||||
@@ -255,7 +253,7 @@ describe("<CallGuestLinkButton />", () => {
|
||||
onFinished.mockClear();
|
||||
sendStateSpy.mockClear();
|
||||
|
||||
container = getComponent(room).container;
|
||||
let container = getComponent(room).container;
|
||||
fireEvent.click(getByText(container, "Anyone"));
|
||||
expect(sendStateSpy).toHaveBeenLastCalledWith(
|
||||
"!room:server.org",
|
||||
+121
-117
@@ -7,6 +7,10 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { vi, describe, it, expect, beforeEach, afterEach, type Mocked } from "vitest";
|
||||
|
||||
import React from "react";
|
||||
import { CallType, type MatrixCall } from "matrix-js-sdk/src/webrtc/call";
|
||||
import {
|
||||
@@ -34,41 +38,41 @@ import {
|
||||
type RenderOptions,
|
||||
screen,
|
||||
waitFor,
|
||||
} from "jest-matrix-react";
|
||||
} from "test-utils-rtl";
|
||||
import { type ViewRoomOpts } from "@matrix-org/react-sdk-module-api/lib/lifecycles/RoomViewLifecycle";
|
||||
import { mocked } from "jest-mock";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { filterConsole, setupAsyncStoreWithClient, stubClient } from "test-utils";
|
||||
|
||||
import { filterConsole, setupAsyncStoreWithClient, stubClient } from "../../../../../test-utils";
|
||||
import RoomHeader from "../../../../../../src/components/views/rooms/RoomHeader/RoomHeader";
|
||||
import DMRoomMap from "../../../../../../src/utils/DMRoomMap";
|
||||
import { MatrixClientPeg } from "../../../../../../src/MatrixClientPeg";
|
||||
import { ScopedRoomContextProvider } from "../../../../../../src/contexts/ScopedRoomContext";
|
||||
import RoomContext, { type RoomContextType } from "../../../../../../src/contexts/RoomContext";
|
||||
import RightPanelStore from "../../../../../../src/stores/right-panel/RightPanelStore";
|
||||
import { RightPanelPhases } from "../../../../../../src/stores/right-panel/RightPanelStorePhases";
|
||||
import SettingsStore from "../../../../../../src/settings/SettingsStore";
|
||||
import SdkConfig from "../../../../../../src/SdkConfig";
|
||||
import dispatcher from "../../../../../../src/dispatcher/dispatcher";
|
||||
import { CallStore } from "../../../../../../src/stores/CallStore";
|
||||
import { type Call } from "../../../../../../src/models/Call";
|
||||
import * as ShieldUtils from "../../../../../../src/utils/ShieldUtils";
|
||||
import { WidgetLayoutStore } from "../../../../../../src/stores/widgets/WidgetLayoutStore";
|
||||
import MatrixClientContext from "../../../../../../src/contexts/MatrixClientContext";
|
||||
import { _t } from "../../../../../../src/languageHandler";
|
||||
import WidgetStore, { type IApp } from "../../../../../../src/stores/WidgetStore";
|
||||
import { UIFeature } from "../../../../../../src/settings/UIFeature";
|
||||
import { SettingLevel } from "../../../../../../src/settings/SettingLevel";
|
||||
import { ElementCallMemberEventType } from "../../../../../../src/call-types";
|
||||
import { SDKContext } from "../../../../../../src/contexts/SDKContext";
|
||||
import { SDKContextClass } from "../../../../../../src/contexts/SDKContextClass.ts";
|
||||
import RoomHeader from "./RoomHeader";
|
||||
import DMRoomMap from "../../../../utils/DMRoomMap";
|
||||
import { MatrixClientPeg } from "../../../../MatrixClientPeg";
|
||||
import { ScopedRoomContextProvider } from "../../../../contexts/ScopedRoomContext";
|
||||
import RoomContext, { type RoomContextType } from "../../../../contexts/RoomContext";
|
||||
import RightPanelStore from "../../../../stores/right-panel/RightPanelStore";
|
||||
import { RightPanelPhases } from "../../../../stores/right-panel/RightPanelStorePhases";
|
||||
import SettingsStore from "../../../../settings/SettingsStore";
|
||||
import SdkConfig from "../../../../SdkConfig";
|
||||
import dispatcher from "../../../../dispatcher/dispatcher";
|
||||
import { CallStore } from "../../../../stores/CallStore";
|
||||
import { type Call } from "../../../../models/Call";
|
||||
import * as ShieldUtils from "../../../../utils/ShieldUtils";
|
||||
import { WidgetLayoutStore } from "../../../../stores/widgets/WidgetLayoutStore";
|
||||
import MatrixClientContext from "../../../../contexts/MatrixClientContext";
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import WidgetStore, { type IApp } from "../../../../stores/WidgetStore";
|
||||
import { UIFeature } from "../../../../settings/UIFeature";
|
||||
import { SettingLevel } from "../../../../settings/SettingLevel";
|
||||
import { ElementCallMemberEventType } from "../../../../call-types";
|
||||
import { SDKContext } from "../../../../contexts/SDKContext";
|
||||
import { SDKContextClass } from "../../../../contexts/SDKContextClass.ts";
|
||||
|
||||
jest.mock("../../../../../../src/utils/ShieldUtils");
|
||||
jest.mock("../../../../../../src/hooks/right-panel/useCurrentPhase", () => ({
|
||||
vi.mock("../../../../utils/ShieldUtils");
|
||||
vi.mock("../../../../hooks/right-panel/useCurrentPhase", () => ({
|
||||
useCurrentPhase: () => {
|
||||
return { currentPhase: "foo", isOpen: false };
|
||||
},
|
||||
}));
|
||||
vi.mock("../../../../Modal");
|
||||
|
||||
describe("RoomHeader", () => {
|
||||
filterConsole(
|
||||
@@ -79,13 +83,13 @@ describe("RoomHeader", () => {
|
||||
let room: Room;
|
||||
const ROOM_ID = "!1:example.org";
|
||||
|
||||
let setCardSpy: jest.SpyInstance | undefined;
|
||||
let setCardSpy: Mocked<RightPanelStore["setCard"]> | undefined;
|
||||
|
||||
const mockRoomViewStore = {
|
||||
isViewingCall: jest.fn().mockReturnValue(false),
|
||||
on: jest.fn(),
|
||||
off: jest.fn(),
|
||||
emit: jest.fn(),
|
||||
isViewingCall: vi.fn().mockReturnValue(false),
|
||||
on: vi.fn(),
|
||||
off: vi.fn(),
|
||||
emit: vi.fn(),
|
||||
};
|
||||
|
||||
let client: MatrixClient;
|
||||
@@ -110,15 +114,15 @@ describe("RoomHeader", () => {
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
});
|
||||
DMRoomMap.setShared({
|
||||
getUserIdForRoomId: jest.fn(),
|
||||
getUserIdForRoomId: vi.fn(),
|
||||
} as unknown as DMRoomMap);
|
||||
|
||||
setCardSpy = jest.spyOn(RightPanelStore.instance, "setCard");
|
||||
jest.spyOn(ShieldUtils, "shieldStatusForRoom").mockResolvedValue(ShieldUtils.E2EStatus.Normal);
|
||||
setCardSpy = vi.spyOn(RightPanelStore.instance, "setCard");
|
||||
vi.spyOn(ShieldUtils, "shieldStatusForRoom").mockResolvedValue(ShieldUtils.E2EStatus.Normal);
|
||||
|
||||
// Mock CallStore.instance.getCall to return null by default
|
||||
// Individual tests can override this when they need a specific Call object
|
||||
jest.spyOn(CallStore.instance, "getCall").mockReturnValue(null);
|
||||
vi.spyOn(CallStore.instance, "getCall").mockReturnValue(null);
|
||||
|
||||
// Reset the mock RoomViewStore
|
||||
mockRoomViewStore.isViewingCall.mockReturnValue(false);
|
||||
@@ -132,7 +136,7 @@ describe("RoomHeader", () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
vi.resetAllMocks();
|
||||
SettingsStore.reset();
|
||||
});
|
||||
|
||||
@@ -190,7 +194,7 @@ describe("RoomHeader", () => {
|
||||
},
|
||||
];
|
||||
room.currentState.setJoinedMemberCount(members.length);
|
||||
room.getJoinedMembers = jest.fn().mockReturnValue(members);
|
||||
room.getJoinedMembers = vi.fn().mockReturnValue(members);
|
||||
|
||||
const { container } = render(<RoomHeader room={room} />, getWrapper());
|
||||
|
||||
@@ -222,7 +226,7 @@ describe("RoomHeader", () => {
|
||||
|
||||
it("opens the notifications panel", async () => {
|
||||
const user = userEvent.setup();
|
||||
SettingsStore.setValue("feature_notifications", null, SettingLevel.DEVICE, true);
|
||||
await SettingsStore.setValue("feature_notifications", null, SettingLevel.DEVICE, true);
|
||||
|
||||
render(<RoomHeader room={room} />, getWrapper());
|
||||
|
||||
@@ -242,7 +246,7 @@ describe("RoomHeader", () => {
|
||||
|
||||
it("should not show voice call button in managed hybrid environments", async () => {
|
||||
mockRoomMembers(room, 2);
|
||||
jest.spyOn(SdkConfig, "get").mockReturnValue({ widget_build_url: "https://widget.build.url" });
|
||||
vi.spyOn(SdkConfig, "get").mockReturnValue({ widget_build_url: "https://widget.build.url" });
|
||||
render(<RoomHeader room={room} />, getWrapper());
|
||||
|
||||
const videoButton = screen.getByRole("button", { name: "Video call" });
|
||||
@@ -270,7 +274,7 @@ describe("RoomHeader", () => {
|
||||
|
||||
afterEach(() => {
|
||||
SdkConfig.reset();
|
||||
jest.restoreAllMocks();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("should not show call buttons in rooms smaller than 3 members", async () => {
|
||||
@@ -369,7 +373,7 @@ describe("RoomHeader", () => {
|
||||
expect(voiceButton).not.toHaveAttribute("aria-disabled", "true");
|
||||
expect(videoButton).not.toHaveAttribute("aria-disabled", "true");
|
||||
|
||||
const placeCallSpy = jest.spyOn(SDKContextClass.instance.legacyCallHandler, "placeCall");
|
||||
const placeCallSpy = vi.spyOn(SDKContextClass.instance.legacyCallHandler, "placeCall");
|
||||
|
||||
await user.click(voiceButton);
|
||||
expect(placeCallSpy).toHaveBeenLastCalledWith(room.roomId, CallType.Voice);
|
||||
@@ -380,7 +384,7 @@ describe("RoomHeader", () => {
|
||||
|
||||
it("you can't call if there's already a call", () => {
|
||||
mockRoomMembers(room, 2);
|
||||
jest.spyOn(SDKContextClass.instance.legacyCallHandler, "getCallForRoom").mockReturnValue(
|
||||
vi.spyOn(SDKContextClass.instance.legacyCallHandler, "getCallForRoom").mockReturnValue(
|
||||
// The JS-SDK does not export the class `MatrixCall` only the type
|
||||
{} as MatrixCall,
|
||||
);
|
||||
@@ -392,7 +396,7 @@ describe("RoomHeader", () => {
|
||||
|
||||
it("can call in large rooms if able to edit widgets", () => {
|
||||
mockRoomMembers(room, 10);
|
||||
jest.spyOn(room.currentState, "mayClientSendStateEvent").mockReturnValue(true);
|
||||
vi.spyOn(room.currentState, "mayClientSendStateEvent").mockReturnValue(true);
|
||||
render(<RoomHeader room={room} />, getWrapper());
|
||||
|
||||
const videoCallButton = screen.getByRole("button", { name: "Video call" });
|
||||
@@ -401,7 +405,7 @@ describe("RoomHeader", () => {
|
||||
|
||||
it("disable calls in large rooms by default", () => {
|
||||
mockRoomMembers(room, 10);
|
||||
jest.spyOn(room.currentState, "mayClientSendStateEvent").mockReturnValue(false);
|
||||
vi.spyOn(room.currentState, "mayClientSendStateEvent").mockReturnValue(false);
|
||||
render(<RoomHeader room={room} />, getWrapper());
|
||||
expect(
|
||||
getByLabelText(document.body, "You do not have permission to start video calls", {
|
||||
@@ -415,7 +419,7 @@ describe("RoomHeader", () => {
|
||||
beforeEach(async () => {
|
||||
SdkConfig.put({});
|
||||
// Enable Element Call
|
||||
client._unstable_getRTCTransports = jest
|
||||
client._unstable_getRTCTransports = vi
|
||||
.fn()
|
||||
.mockResolvedValue([{ type: "livekit", livekit_service_url: "https://example.org" }]);
|
||||
// And ensure the CallStore has the transports configured.
|
||||
@@ -424,7 +428,7 @@ describe("RoomHeader", () => {
|
||||
|
||||
afterEach(() => {
|
||||
SdkConfig.reset();
|
||||
jest.restoreAllMocks();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("renders only the video call element", async () => {
|
||||
@@ -436,7 +440,7 @@ describe("RoomHeader", () => {
|
||||
},
|
||||
});
|
||||
// allow element calls
|
||||
jest.spyOn(room.currentState, "mayClientSendStateEvent").mockReturnValue(true);
|
||||
vi.spyOn(room.currentState, "mayClientSendStateEvent").mockReturnValue(true);
|
||||
|
||||
render(<RoomHeader room={room} />, getWrapper());
|
||||
|
||||
@@ -445,7 +449,7 @@ describe("RoomHeader", () => {
|
||||
const videoCallButton = screen.getByRole("button", { name: "Video call" });
|
||||
expect(videoCallButton).not.toHaveAttribute("aria-disabled", "true");
|
||||
|
||||
const dispatcherSpy = jest.spyOn(dispatcher, "dispatch").mockImplementation();
|
||||
const dispatcherSpy = vi.spyOn(dispatcher, "dispatch").mockImplementation(() => {});
|
||||
|
||||
await user.click(videoCallButton);
|
||||
expect(dispatcherSpy).toHaveBeenCalledWith(expect.objectContaining({ view_call: true }));
|
||||
@@ -458,15 +462,15 @@ describe("RoomHeader", () => {
|
||||
},
|
||||
});
|
||||
// allow element calls
|
||||
jest.spyOn(room.currentState, "mayClientSendStateEvent").mockReturnValue(true);
|
||||
jest.spyOn(WidgetLayoutStore.instance, "isInContainer").mockReturnValue(true);
|
||||
vi.spyOn(room.currentState, "mayClientSendStateEvent").mockReturnValue(true);
|
||||
vi.spyOn(WidgetLayoutStore.instance, "isInContainer").mockReturnValue(true);
|
||||
const widget = { type: "m.jitsi" } as IApp;
|
||||
jest.spyOn(CallStore.instance, "getCall").mockReturnValue({
|
||||
vi.spyOn(CallStore.instance, "getCall").mockReturnValue({
|
||||
widget,
|
||||
on: () => {},
|
||||
off: () => {},
|
||||
} as unknown as Call);
|
||||
jest.spyOn(WidgetStore.instance, "getApps").mockReturnValue([widget]);
|
||||
vi.spyOn(WidgetStore.instance, "getApps").mockReturnValue([widget]);
|
||||
render(<RoomHeader room={room} />, getWrapper());
|
||||
// Voice and video
|
||||
for (const button of screen.getAllByRole("button", { name: "Ongoing call" })) {
|
||||
@@ -484,17 +488,17 @@ describe("RoomHeader", () => {
|
||||
},
|
||||
});
|
||||
// allow calls
|
||||
jest.spyOn(room.currentState, "mayClientSendStateEvent").mockReturnValue(true);
|
||||
jest.spyOn(WidgetLayoutStore.instance, "isInContainer").mockReturnValue(false);
|
||||
const spy = jest.spyOn(WidgetLayoutStore.instance, "moveToContainer");
|
||||
vi.spyOn(room.currentState, "mayClientSendStateEvent").mockReturnValue(true);
|
||||
vi.spyOn(WidgetLayoutStore.instance, "isInContainer").mockReturnValue(false);
|
||||
const spy = vi.spyOn(WidgetLayoutStore.instance, "moveToContainer");
|
||||
|
||||
const widget = { type: "m.jitsi" } as IApp;
|
||||
jest.spyOn(CallStore.instance, "getCall").mockReturnValue({
|
||||
vi.spyOn(CallStore.instance, "getCall").mockReturnValue({
|
||||
widget,
|
||||
on: () => {},
|
||||
off: () => {},
|
||||
} as unknown as Call);
|
||||
jest.spyOn(WidgetStore.instance, "getApps").mockReturnValue([widget]);
|
||||
vi.spyOn(WidgetStore.instance, "getApps").mockReturnValue([widget]);
|
||||
|
||||
render(<RoomHeader room={room} />, getWrapper());
|
||||
|
||||
@@ -506,7 +510,7 @@ describe("RoomHeader", () => {
|
||||
|
||||
it("disables calling if there's a jitsi call", () => {
|
||||
mockRoomMembers(room, 2);
|
||||
jest.spyOn(SDKContextClass.instance.legacyCallHandler, "getCallForRoom").mockReturnValue(
|
||||
vi.spyOn(SDKContextClass.instance.legacyCallHandler, "getCallForRoom").mockReturnValue(
|
||||
// The JS-SDK does not export the class `MatrixCall` only the type
|
||||
{} as MatrixCall,
|
||||
);
|
||||
@@ -519,7 +523,7 @@ describe("RoomHeader", () => {
|
||||
it("calls using legacy or jitsi", async () => {
|
||||
const user = userEvent.setup();
|
||||
mockRoomMembers(room, 2);
|
||||
jest.spyOn(room.currentState, "mayClientSendStateEvent").mockImplementation((key) => {
|
||||
vi.spyOn(room.currentState, "mayClientSendStateEvent").mockImplementation((key) => {
|
||||
if (key === "im.vector.modular.widgets") return true;
|
||||
return false;
|
||||
});
|
||||
@@ -530,7 +534,7 @@ describe("RoomHeader", () => {
|
||||
expect(voiceButton).not.toHaveAttribute("aria-disabled", "true");
|
||||
expect(videoButton).not.toHaveAttribute("aria-disabled", "true");
|
||||
|
||||
const placeCallSpy = jest.spyOn(SDKContextClass.instance.legacyCallHandler, "placeCall");
|
||||
const placeCallSpy = vi.spyOn(SDKContextClass.instance.legacyCallHandler, "placeCall");
|
||||
await user.click(voiceButton);
|
||||
expect(placeCallSpy).toHaveBeenLastCalledWith(room.roomId, CallType.Voice);
|
||||
|
||||
@@ -542,7 +546,7 @@ describe("RoomHeader", () => {
|
||||
const user = userEvent.setup();
|
||||
mockRoomMembers(room, 3);
|
||||
|
||||
jest.spyOn(room.currentState, "mayClientSendStateEvent").mockImplementation((key) => {
|
||||
vi.spyOn(room.currentState, "mayClientSendStateEvent").mockImplementation((key) => {
|
||||
if (key === "im.vector.modular.widgets") return true;
|
||||
return false;
|
||||
});
|
||||
@@ -552,7 +556,7 @@ describe("RoomHeader", () => {
|
||||
const videoButton = screen.getByRole("button", { name: "Video call" });
|
||||
expect(videoButton).not.toHaveAttribute("aria-disabled", "true");
|
||||
|
||||
const placeCallSpy = jest.spyOn(SDKContextClass.instance.legacyCallHandler, "placeCall");
|
||||
const placeCallSpy = vi.spyOn(SDKContextClass.instance.legacyCallHandler, "placeCall");
|
||||
await user.click(videoButton);
|
||||
expect(placeCallSpy).toHaveBeenLastCalledWith(room.roomId, CallType.Video);
|
||||
});
|
||||
@@ -561,7 +565,7 @@ describe("RoomHeader", () => {
|
||||
const user = userEvent.setup();
|
||||
mockRoomMembers(room, 3);
|
||||
|
||||
jest.spyOn(room.currentState, "mayClientSendStateEvent").mockImplementation((key) => {
|
||||
vi.spyOn(room.currentState, "mayClientSendStateEvent").mockImplementation((key) => {
|
||||
if (key === ElementCallMemberEventType.name) return true;
|
||||
return false;
|
||||
});
|
||||
@@ -571,7 +575,7 @@ describe("RoomHeader", () => {
|
||||
const videoButton = screen.getByRole("button", { name: "Video call" });
|
||||
expect(videoButton).not.toHaveAttribute("aria-disabled", "true");
|
||||
|
||||
const dispatcherSpy = jest.spyOn(dispatcher, "dispatch").mockImplementation();
|
||||
const dispatcherSpy = vi.spyOn(dispatcher, "dispatch").mockImplementation(() => {});
|
||||
await user.click(videoButton);
|
||||
expect(dispatcherSpy).toHaveBeenCalledWith(expect.objectContaining({ view_call: true }));
|
||||
});
|
||||
@@ -579,7 +583,7 @@ describe("RoomHeader", () => {
|
||||
it("buttons are disabled if there is an ongoing call", async () => {
|
||||
mockRoomMembers(room, 3);
|
||||
|
||||
jest.spyOn(CallStore.prototype, "connectedCalls", "get").mockReturnValue(
|
||||
vi.spyOn(CallStore.prototype, "connectedCalls", "get").mockReturnValue(
|
||||
new Set([{ roomId: "some_other_room" } as Call]),
|
||||
);
|
||||
const { container } = render(<RoomHeader room={room} />, getWrapper());
|
||||
@@ -592,7 +596,7 @@ describe("RoomHeader", () => {
|
||||
it("join video call button is shown if there is an ongoing call", async () => {
|
||||
mockRoomMembers(room, 3);
|
||||
// Mock CallStore to return a call with 3 participants
|
||||
jest.spyOn(CallStore.instance, "getCall").mockReturnValue(createMockCall(ROOM_ID, 3));
|
||||
vi.spyOn(CallStore.instance, "getCall").mockReturnValue(createMockCall(ROOM_ID, 3));
|
||||
render(<RoomHeader room={room} />, getWrapper());
|
||||
const joinButton = getByLabelText(document.body, "Join video call");
|
||||
expect(joinButton).not.toHaveAttribute("aria-disabled", "true");
|
||||
@@ -601,7 +605,7 @@ describe("RoomHeader", () => {
|
||||
it("join voice call button is shown if there is an ongoing call", async () => {
|
||||
mockRoomMembers(room, 3);
|
||||
// Mock CallStore to return a call with 3 participants
|
||||
jest.spyOn(CallStore.instance, "getCall").mockReturnValue(createMockCall(ROOM_ID, 3, CallType.Voice));
|
||||
vi.spyOn(CallStore.instance, "getCall").mockReturnValue(createMockCall(ROOM_ID, 3, CallType.Voice));
|
||||
render(<RoomHeader room={room} />, getWrapper());
|
||||
const joinButton = getByLabelText(document.body, "Join voice call");
|
||||
expect(joinButton).not.toHaveAttribute("aria-disabled", "true");
|
||||
@@ -610,10 +614,10 @@ describe("RoomHeader", () => {
|
||||
it("clicking the join button of an ongoing video call joins as a video call", async () => {
|
||||
const user = userEvent.setup();
|
||||
mockRoomMembers(room, 3);
|
||||
jest.spyOn(CallStore.instance, "getCall").mockReturnValue(createMockCall(ROOM_ID, 3, CallType.Video, true));
|
||||
vi.spyOn(CallStore.instance, "getCall").mockReturnValue(createMockCall(ROOM_ID, 3, CallType.Video, true));
|
||||
render(<RoomHeader room={room} />, getWrapper());
|
||||
|
||||
const dispatcherSpy = jest.spyOn(dispatcher, "dispatch").mockImplementation();
|
||||
const dispatcherSpy = vi.spyOn(dispatcher, "dispatch").mockImplementation(() => {});
|
||||
await user.click(getByLabelText(document.body, "Join video call"));
|
||||
|
||||
expect(dispatcherSpy).toHaveBeenCalledWith(expect.objectContaining({ view_call: true, voiceOnly: false }));
|
||||
@@ -622,10 +626,10 @@ describe("RoomHeader", () => {
|
||||
it("clicking the join button of an ongoing voice call joins as a voice call", async () => {
|
||||
const user = userEvent.setup();
|
||||
mockRoomMembers(room, 3);
|
||||
jest.spyOn(CallStore.instance, "getCall").mockReturnValue(createMockCall(ROOM_ID, 3, CallType.Voice, true));
|
||||
vi.spyOn(CallStore.instance, "getCall").mockReturnValue(createMockCall(ROOM_ID, 3, CallType.Voice, true));
|
||||
render(<RoomHeader room={room} />, getWrapper());
|
||||
|
||||
const dispatcherSpy = jest.spyOn(dispatcher, "dispatch").mockImplementation();
|
||||
const dispatcherSpy = vi.spyOn(dispatcher, "dispatch").mockImplementation(() => {});
|
||||
await user.click(getByLabelText(document.body, "Join voice call"));
|
||||
|
||||
expect(dispatcherSpy).toHaveBeenCalledWith(expect.objectContaining({ view_call: true, voiceOnly: true }));
|
||||
@@ -634,8 +638,8 @@ describe("RoomHeader", () => {
|
||||
it("join button is disabled if there is an other ongoing call", async () => {
|
||||
mockRoomMembers(room, 3);
|
||||
// Mock CallStore to return a call with 3 participants
|
||||
jest.spyOn(CallStore.instance, "getCall").mockReturnValue(createMockCall(ROOM_ID, 3));
|
||||
jest.spyOn(CallStore.prototype, "connectedCalls", "get").mockReturnValue(
|
||||
vi.spyOn(CallStore.instance, "getCall").mockReturnValue(createMockCall(ROOM_ID, 3));
|
||||
vi.spyOn(CallStore.prototype, "connectedCalls", "get").mockReturnValue(
|
||||
new Set([{ roomId: "some_other_room" } as Call]),
|
||||
);
|
||||
render(<RoomHeader room={room} />, getWrapper());
|
||||
@@ -649,22 +653,22 @@ describe("RoomHeader", () => {
|
||||
|
||||
mockRoomViewStore.isViewingCall.mockReturnValue(true);
|
||||
render(<RoomHeader room={room} />, getWrapper());
|
||||
getByLabelText(document.body, "Close lobby");
|
||||
expect(getByLabelText(document.body, "Close lobby")).toBeVisible();
|
||||
});
|
||||
|
||||
it("close lobby button is shown if there is an ongoing call but we are viewing the lobby", async () => {
|
||||
mockRoomMembers(room, 3);
|
||||
// Mock CallStore to return a call with 3 participants
|
||||
jest.spyOn(CallStore.instance, "getCall").mockReturnValue(createMockCall(ROOM_ID, 3));
|
||||
vi.spyOn(CallStore.instance, "getCall").mockReturnValue(createMockCall(ROOM_ID, 3));
|
||||
mockRoomViewStore.isViewingCall.mockReturnValue(true);
|
||||
|
||||
render(<RoomHeader room={room} />, getWrapper());
|
||||
getByLabelText(document.body, "Close lobby");
|
||||
expect(getByLabelText(document.body, "Close lobby")).toBeVisible();
|
||||
});
|
||||
|
||||
it("don't show external conference button if the call is not shown", () => {
|
||||
mockRoomViewStore.isViewingCall.mockReturnValue(false);
|
||||
jest.spyOn(SdkConfig, "get").mockImplementation((key) => {
|
||||
vi.spyOn(SdkConfig, "get").mockImplementation((key) => {
|
||||
return { guest_spa_url: "https://guest_spa_url.com", url: "https://spa_url.com" };
|
||||
});
|
||||
render(<RoomHeader room={room} />, getWrapper());
|
||||
@@ -680,7 +684,7 @@ describe("RoomHeader", () => {
|
||||
it("gives the option of element call or legacy calling for video", async () => {
|
||||
const user = userEvent.setup();
|
||||
mockRoomMembers(room, 2);
|
||||
jest.spyOn(room.currentState, "mayClientSendStateEvent").mockImplementation((key) => {
|
||||
vi.spyOn(room.currentState, "mayClientSendStateEvent").mockImplementation((key) => {
|
||||
if (key === ElementCallMemberEventType.name) return true;
|
||||
return false;
|
||||
});
|
||||
@@ -697,7 +701,7 @@ describe("RoomHeader", () => {
|
||||
it("gives the option of element call or legacy calling for voice in DM rooms", async () => {
|
||||
const user = userEvent.setup();
|
||||
mockRoomMembers(room, 2);
|
||||
jest.spyOn(room.currentState, "mayClientSendStateEvent").mockImplementation((key) => {
|
||||
vi.spyOn(room.currentState, "mayClientSendStateEvent").mockImplementation((key) => {
|
||||
if (key === ElementCallMemberEventType.name) return true;
|
||||
return false;
|
||||
});
|
||||
@@ -731,9 +735,9 @@ describe("RoomHeader", () => {
|
||||
});
|
||||
|
||||
it("does not show a user status for non-DM rooms", async () => {
|
||||
SettingsStore.setValue("feature_user_status", null, SettingLevel.DEVICE, true);
|
||||
client.doesServerSupportExtendedProfiles = jest.fn().mockResolvedValue(true);
|
||||
mocked(client.getExtendedProfileProperty).mockResolvedValue({ emoji: "🐎", text: "on a horse" });
|
||||
await SettingsStore.setValue("feature_user_status", null, SettingLevel.DEVICE, true);
|
||||
vi.mocked(client.doesServerSupportExtendedProfiles).mockResolvedValue(true);
|
||||
vi.mocked(client.getExtendedProfileProperty).mockResolvedValue({ emoji: "🐎", text: "on a horse" });
|
||||
|
||||
render(<RoomHeader room={room} />, getWrapper());
|
||||
|
||||
@@ -742,7 +746,7 @@ describe("RoomHeader", () => {
|
||||
});
|
||||
|
||||
it("shows a history icon if the room is encrypted and has shared history", async () => {
|
||||
mocked(client.getCrypto()!).isEncryptionEnabledInRoom.mockResolvedValue(true);
|
||||
vi.mocked(client.getCrypto()!).isEncryptionEnabledInRoom.mockResolvedValue(true);
|
||||
await room.addLiveEvents(
|
||||
[
|
||||
new MatrixEvent({
|
||||
@@ -757,11 +761,11 @@ describe("RoomHeader", () => {
|
||||
);
|
||||
|
||||
render(<RoomHeader room={room} />, getWrapper());
|
||||
await waitFor(() => getByLabelText(document.body, "New members see history"));
|
||||
await waitFor(() => expect(getByLabelText(document.body, "New members see history")).toBeVisible());
|
||||
});
|
||||
|
||||
it("shows a user icon if the room is encrypted and has world readable history", async () => {
|
||||
mocked(client.getCrypto()!).isEncryptionEnabledInRoom.mockResolvedValue(true);
|
||||
vi.mocked(client.getCrypto()!).isEncryptionEnabledInRoom.mockResolvedValue(true);
|
||||
await room.addLiveEvents(
|
||||
[
|
||||
new MatrixEvent({
|
||||
@@ -776,17 +780,17 @@ describe("RoomHeader", () => {
|
||||
);
|
||||
|
||||
render(<RoomHeader room={room} />, getWrapper());
|
||||
await waitFor(() => getByLabelText(document.body, "Anyone can see history"));
|
||||
await waitFor(() => expect(getByLabelText(document.body, "Anyone can see history")).toBeVisible());
|
||||
});
|
||||
|
||||
describe("dm", () => {
|
||||
beforeEach(() => {
|
||||
// Make the mocked room a DM
|
||||
mocked(DMRoomMap.shared().getUserIdForRoomId).mockImplementation((roomId) => {
|
||||
vi.mocked(DMRoomMap.shared().getUserIdForRoomId).mockImplementation((roomId) => {
|
||||
if (roomId === room.roomId) return "@user:example.com";
|
||||
});
|
||||
room.getMember = jest.fn((userId) => new RoomMember(room.roomId, userId));
|
||||
room.getJoinedMembers = jest.fn().mockReturnValue([
|
||||
room.getMember = vi.fn((userId) => new RoomMember(room.roomId, userId));
|
||||
room.getJoinedMembers = vi.fn().mockReturnValue([
|
||||
{
|
||||
userId: "@me:example.org",
|
||||
name: "Member",
|
||||
@@ -816,7 +820,7 @@ describe("RoomHeader", () => {
|
||||
[ShieldUtils.E2EStatus.Verified, "Verified"],
|
||||
[ShieldUtils.E2EStatus.Warning, "Untrusted"],
|
||||
])("shows the %s icon", async (value: ShieldUtils.E2EStatus, expectedLabel: string) => {
|
||||
jest.spyOn(ShieldUtils, "shieldStatusForRoom").mockResolvedValue(value);
|
||||
vi.spyOn(ShieldUtils, "shieldStatusForRoom").mockResolvedValue(value);
|
||||
|
||||
render(<RoomHeader room={room} />, getWrapper());
|
||||
|
||||
@@ -824,9 +828,9 @@ describe("RoomHeader", () => {
|
||||
});
|
||||
|
||||
it("shows the user status", async () => {
|
||||
SettingsStore.setValue("feature_user_status", null, SettingLevel.DEVICE, true);
|
||||
client.doesServerSupportExtendedProfiles = jest.fn().mockResolvedValue(true);
|
||||
mocked(client.getExtendedProfileProperty).mockResolvedValue({ emoji: "🐎", text: "on a horse" });
|
||||
await SettingsStore.setValue("feature_user_status", null, SettingLevel.DEVICE, true);
|
||||
vi.mocked(client.doesServerSupportExtendedProfiles).mockResolvedValue(true);
|
||||
vi.mocked(client.getExtendedProfileProperty).mockResolvedValue({ emoji: "🐎", text: "on a horse" });
|
||||
|
||||
render(<RoomHeader room={room} />, getWrapper());
|
||||
|
||||
@@ -835,13 +839,13 @@ describe("RoomHeader", () => {
|
||||
});
|
||||
|
||||
it("updates user status when it changes", async () => {
|
||||
SettingsStore.setValue("feature_user_status", null, SettingLevel.DEVICE, true);
|
||||
client.doesServerSupportExtendedProfiles = jest.fn().mockResolvedValue(true);
|
||||
mocked(client.getExtendedProfileProperty).mockResolvedValue({ emoji: "🐎", text: "on a horse" });
|
||||
await SettingsStore.setValue("feature_user_status", null, SettingLevel.DEVICE, true);
|
||||
vi.mocked(client.doesServerSupportExtendedProfiles).mockResolvedValue(true);
|
||||
vi.mocked(client.getExtendedProfileProperty).mockResolvedValue({ emoji: "🐎", text: "on a horse" });
|
||||
|
||||
render(<RoomHeader room={room} />, getWrapper());
|
||||
|
||||
mocked(client.getExtendedProfileProperty).mockResolvedValue({ emoji: "🐴", text: "is a horse" });
|
||||
vi.mocked(client.getExtendedProfileProperty).mockResolvedValue({ emoji: "🐴", text: "is a horse" });
|
||||
client.emit(ClientEvent.UserProfileUpdate, "@bob:example.org", { emoji: "🐴", text: "is a horse" });
|
||||
|
||||
await waitFor(() => expect(screen.getByText("is a horse")).toBeInTheDocument());
|
||||
@@ -849,9 +853,9 @@ describe("RoomHeader", () => {
|
||||
});
|
||||
|
||||
it("does not show the user status when the feature is disabled", async () => {
|
||||
SettingsStore.setValue("feature_user_status", null, SettingLevel.DEVICE, false);
|
||||
client.doesServerSupportExtendedProfiles = jest.fn().mockResolvedValue(true);
|
||||
mocked(client.getExtendedProfileProperty).mockResolvedValue({ emoji: "🐎", text: "on a horse" });
|
||||
await SettingsStore.setValue("feature_user_status", null, SettingLevel.DEVICE, false);
|
||||
vi.mocked(client.doesServerSupportExtendedProfiles).mockResolvedValue(true);
|
||||
vi.mocked(client.getExtendedProfileProperty).mockResolvedValue({ emoji: "🐎", text: "on a horse" });
|
||||
|
||||
render(<RoomHeader room={room} />, getWrapper());
|
||||
|
||||
@@ -872,12 +876,12 @@ describe("RoomHeader", () => {
|
||||
|
||||
it("updates the icon when the encryption status changes", async () => {
|
||||
// The room starts verified
|
||||
jest.spyOn(ShieldUtils, "shieldStatusForRoom").mockResolvedValue(ShieldUtils.E2EStatus.Verified);
|
||||
vi.spyOn(ShieldUtils, "shieldStatusForRoom").mockResolvedValue(ShieldUtils.E2EStatus.Verified);
|
||||
render(<RoomHeader room={room} />, getWrapper());
|
||||
await waitFor(() => expect(getByLabelText(document.body, "Verified")).toBeInTheDocument());
|
||||
|
||||
// A new member joins, and the room becomes unverified
|
||||
jest.spyOn(ShieldUtils, "shieldStatusForRoom").mockResolvedValue(ShieldUtils.E2EStatus.Warning);
|
||||
vi.spyOn(ShieldUtils, "shieldStatusForRoom").mockResolvedValue(ShieldUtils.E2EStatus.Warning);
|
||||
act(() => {
|
||||
room.emit(
|
||||
RoomStateEvent.Members,
|
||||
@@ -898,7 +902,7 @@ describe("RoomHeader", () => {
|
||||
await waitFor(() => expect(getByLabelText(document.body, "Untrusted")).toBeInTheDocument());
|
||||
|
||||
// The user becomes verified
|
||||
jest.spyOn(ShieldUtils, "shieldStatusForRoom").mockResolvedValue(ShieldUtils.E2EStatus.Verified);
|
||||
vi.spyOn(ShieldUtils, "shieldStatusForRoom").mockResolvedValue(ShieldUtils.E2EStatus.Verified);
|
||||
act(() => {
|
||||
MatrixClientPeg.get()!.emit(
|
||||
CryptoEvent.UserTrustStatusChanged,
|
||||
@@ -909,7 +913,7 @@ describe("RoomHeader", () => {
|
||||
await waitFor(() => expect(getByLabelText(document.body, "Verified")).toBeInTheDocument());
|
||||
|
||||
// An unverified device is added
|
||||
jest.spyOn(ShieldUtils, "shieldStatusForRoom").mockResolvedValue(ShieldUtils.E2EStatus.Warning);
|
||||
vi.spyOn(ShieldUtils, "shieldStatusForRoom").mockResolvedValue(ShieldUtils.E2EStatus.Warning);
|
||||
act(() => {
|
||||
MatrixClientPeg.get()!.emit(CryptoEvent.DevicesUpdated, ["@alice:example.org"], false);
|
||||
});
|
||||
@@ -931,7 +935,7 @@ describe("RoomHeader", () => {
|
||||
});
|
||||
|
||||
it("calls onClick-callback on legacyAdditionalButtons", () => {
|
||||
const callback = jest.fn();
|
||||
const callback = vi.fn();
|
||||
const additionalButtons: ViewRoomOpts["buttons"] = [
|
||||
{
|
||||
icon: () => <>test-icon</>,
|
||||
@@ -945,7 +949,7 @@ describe("RoomHeader", () => {
|
||||
|
||||
const button = screen.getByRole("button", { name: "test-label" });
|
||||
const event = createEvent.click(button);
|
||||
event.stopPropagation = jest.fn();
|
||||
event.stopPropagation = vi.fn();
|
||||
fireEvent(button, event);
|
||||
|
||||
expect(callback).toHaveBeenCalled();
|
||||
@@ -960,11 +964,11 @@ describe("RoomHeader", () => {
|
||||
});
|
||||
|
||||
describe("ask to join enabled", () => {
|
||||
it("does render the RoomKnocksBar", () => {
|
||||
SettingsStore.setValue("feature_ask_to_join", null, SettingLevel.DEVICE, true);
|
||||
jest.spyOn(room, "canInvite").mockReturnValue(true);
|
||||
jest.spyOn(room, "getJoinRule").mockReturnValue(JoinRule.Knock);
|
||||
jest.spyOn(room, "getMembersWithMembership").mockReturnValue([new RoomMember(room.roomId, "@foo")]);
|
||||
it("does render the RoomKnocksBar", async () => {
|
||||
await SettingsStore.setValue("feature_ask_to_join", null, SettingLevel.DEVICE, true);
|
||||
vi.spyOn(room, "canInvite").mockReturnValue(true);
|
||||
vi.spyOn(room, "getJoinRule").mockReturnValue(JoinRule.Knock);
|
||||
vi.spyOn(room, "getMembersWithMembership").mockReturnValue([new RoomMember(room.roomId, "@foo")]);
|
||||
|
||||
render(<RoomHeader room={room} />, getWrapper());
|
||||
expect(screen.getByRole("heading", { name: "Asking to join" })).toBeInTheDocument();
|
||||
@@ -975,7 +979,7 @@ describe("RoomHeader", () => {
|
||||
const user = userEvent.setup();
|
||||
render(<RoomHeader room={room} />, getWrapper());
|
||||
|
||||
const dispatcherSpy = jest.spyOn(dispatcher, "dispatch");
|
||||
const dispatcherSpy = vi.spyOn(dispatcher, "dispatch");
|
||||
await user.click(getByLabelText(document.body, "Open room settings"));
|
||||
expect(dispatcherSpy).toHaveBeenCalledWith(expect.objectContaining({ action: "open_room_settings" }));
|
||||
});
|
||||
@@ -1009,9 +1013,9 @@ function createMockCall(
|
||||
widget: { id: "test-widget", type: isElementCall ? "m.call" : undefined },
|
||||
connectionState: "disconnected",
|
||||
callType,
|
||||
on: jest.fn(),
|
||||
off: jest.fn(),
|
||||
emit: jest.fn(),
|
||||
on: vi.fn(),
|
||||
off: vi.fn(),
|
||||
emit: vi.fn(),
|
||||
} as unknown as Call;
|
||||
}
|
||||
|
||||
@@ -1033,5 +1037,5 @@ function mockRoomMembers(room: Room, count: number) {
|
||||
}));
|
||||
|
||||
room.currentState.setJoinedMemberCount(members.length);
|
||||
room.getJoinedMembers = jest.fn().mockReturnValue(members);
|
||||
room.getJoinedMembers = vi.fn().mockReturnValue(members);
|
||||
}
|
||||
+20
-19
@@ -6,20 +6,21 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { type MockedObject } from "jest-mock";
|
||||
import { Room } from "matrix-js-sdk/src/matrix";
|
||||
import { fireEvent, render, screen, waitFor } from "jest-matrix-react";
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { VideoRoomChatButton } from "../../../../../../src/components/views/rooms/RoomHeader/VideoRoomChatButton";
|
||||
import { SDKContext } from "../../../../../../src/contexts/SDKContext";
|
||||
import { TestSDKContext } from "../../../../TestSDKContext.ts";
|
||||
import type RightPanelStore from "../../../../../../src/stores/right-panel/RightPanelStore";
|
||||
import { getMockClientWithEventEmitter, mockClientMethodsUser } from "../../../../../test-utils";
|
||||
import { RoomNotificationState } from "../../../../../../src/stores/notifications/RoomNotificationState";
|
||||
import { NotificationLevel } from "../../../../../../src/stores/notifications/NotificationLevel";
|
||||
import { NotificationStateEvents } from "../../../../../../src/stores/notifications/NotificationState";
|
||||
import { RightPanelPhases } from "../../../../../../src/stores/right-panel/RightPanelStorePhases";
|
||||
import { vi, describe, it, expect, beforeEach, afterEach, type MockedObject } from "vitest";
|
||||
import React from "react";
|
||||
import { Room } from "matrix-js-sdk/src/matrix";
|
||||
import { fireEvent, render, screen, waitFor } from "test-utils-rtl";
|
||||
import { getMockClientWithEventEmitter, mockClientMethodsUser, TestSDKContext } from "test-utils";
|
||||
|
||||
import { VideoRoomChatButton } from "./VideoRoomChatButton";
|
||||
import { SDKContext } from "../../../../contexts/SDKContext";
|
||||
import type RightPanelStore from "../../../../stores/right-panel/RightPanelStore";
|
||||
import { RoomNotificationState } from "../../../../stores/notifications/RoomNotificationState";
|
||||
import { NotificationLevel } from "../../../../stores/notifications/NotificationLevel";
|
||||
import { NotificationStateEvents } from "../../../../stores/notifications/NotificationState";
|
||||
import { RightPanelPhases } from "../../../../stores/right-panel/RightPanelStorePhases";
|
||||
|
||||
describe("<VideoRoomChatButton />", () => {
|
||||
const roomId = "!room:server.org";
|
||||
@@ -32,9 +33,9 @@ describe("<VideoRoomChatButton />", () => {
|
||||
*/
|
||||
const makeRoom = (isVideoRoom = true): Room => {
|
||||
const room = new Room(roomId, sdkContext.client!, sdkContext.client!.getSafeUserId());
|
||||
jest.spyOn(room, "isElementVideoRoom").mockReturnValue(isVideoRoom);
|
||||
vi.spyOn(room, "isElementVideoRoom").mockReturnValue(isVideoRoom);
|
||||
// stub
|
||||
jest.spyOn(room, "getPendingEvents").mockReturnValue([]);
|
||||
vi.spyOn(room, "getPendingEvents").mockReturnValue([]);
|
||||
return room;
|
||||
};
|
||||
|
||||
@@ -43,7 +44,7 @@ describe("<VideoRoomChatButton />", () => {
|
||||
|
||||
// @ts-ignore ugly mocking
|
||||
roomNotificationState._level = level;
|
||||
jest.spyOn(sdkContext.roomNotificationStateStore, "getRoomState").mockReturnValue(roomNotificationState);
|
||||
vi.spyOn(sdkContext.roomNotificationStateStore, "getRoomState").mockReturnValue(roomNotificationState);
|
||||
return roomNotificationState;
|
||||
};
|
||||
|
||||
@@ -57,15 +58,15 @@ describe("<VideoRoomChatButton />", () => {
|
||||
...mockClientMethodsUser(),
|
||||
});
|
||||
rightPanelStore = {
|
||||
showOrHidePhase: jest.fn(),
|
||||
showOrHidePhase: vi.fn(),
|
||||
} as unknown as MockedObject<RightPanelStore>;
|
||||
sdkContext = new TestSDKContext();
|
||||
sdkContext._client = client;
|
||||
jest.spyOn(sdkContext, "rightPanelStore", "get").mockReturnValue(rightPanelStore);
|
||||
vi.spyOn(sdkContext, "rightPanelStore", "get").mockReturnValue(rightPanelStore);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("toggles timeline in right panel on click", () => {
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`RoomHeader dm does not show the face pile for DMs 1`] = `
|
||||
exports[`RoomHeader > dm > does not show the face pile for DMs 1`] = `
|
||||
<DocumentFragment>
|
||||
<header
|
||||
class="_flex_4dswl_9 mx_RoomHeader light-panel"
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`<VideoRoomChatButton /> renders button with an unread marker when room is unread 1`] = `
|
||||
exports[`<VideoRoomChatButton /> > renders button with an unread marker when room is unread 1`] = `
|
||||
<button
|
||||
aria-label="Chat"
|
||||
aria-labelledby="react-use-id-1"
|
||||
@@ -15,8 +15,8 @@ import { _t } from "../../../languageHandler";
|
||||
import { SettingLevel } from "../../../settings/SettingLevel";
|
||||
import { ImageSize } from "../../../settings/enums/ImageSize";
|
||||
import { SettingsSubsection } from "./shared/SettingsSubsection";
|
||||
import { Icon as ImgSizeNormalIcon } from "../../../../res/img/element-icons/settings/img-size-normal.svg";
|
||||
import { Icon as ImgSizeLargeIcon } from "../../../../res/img/element-icons/settings/img-size-large.svg";
|
||||
import ImgSizeNormalIcon from "../../../../res/img/element-icons/settings/img-size-normal.svg?react";
|
||||
import ImgSizeLargeIcon from "../../../../res/img/element-icons/settings/img-size-large.svg?react";
|
||||
|
||||
interface IState {
|
||||
size: ImageSize;
|
||||
|
||||
@@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
|
||||
import React, { Fragment } from "react";
|
||||
|
||||
import { Icon as InactiveIcon } from "../../../../../res/img/element-icons/settings/inactive.svg";
|
||||
import InactiveIcon from "../../../../../res/img/element-icons/settings/inactive.svg?react";
|
||||
import { INACTIVE_DEVICE_AGE_DAYS, isDeviceInactive } from "../../../../components/views/settings/devices/filter";
|
||||
import { type ExtendedDevice } from "../../../../components/views/settings/devices/types";
|
||||
import { formatDate, formatRelativeTime } from "../../../../DateUtils";
|
||||
|
||||
@@ -10,7 +10,7 @@ import classNames from "classnames";
|
||||
import React from "react";
|
||||
import { ShieldIcon, ErrorSolidIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
|
||||
import { Icon as InactiveIcon } from "../../../../../res/img/element-icons/settings/inactive.svg";
|
||||
import InactiveIcon from "../../../../../res/img/element-icons/settings/inactive.svg?react";
|
||||
import { DeviceSecurityVariation } from "./types";
|
||||
|
||||
interface Props {
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
Copyright 2026 Element Creations Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { type SnapshotSerializer } from "vitest";
|
||||
|
||||
const REACT_USE_ID = /_r_[a-z0-9]+_/g;
|
||||
|
||||
function normaliseReactUseIds(snapshot: string): string {
|
||||
// React useId values can vary between runs and make snapshots flaky:
|
||||
// https://github.com/element-hq/element-web/issues/31765
|
||||
// Avoid running the regex for DOM snapshots without React useId output.
|
||||
if (!snapshot.includes("_r_")) return snapshot;
|
||||
|
||||
const ids = new Map<string, string>();
|
||||
let nextId = 1;
|
||||
|
||||
return snapshot.replace(REACT_USE_ID, (id) => {
|
||||
let replacement = ids.get(id);
|
||||
if (!replacement) {
|
||||
replacement = `react-use-id-${nextId++}`;
|
||||
ids.set(id, replacement);
|
||||
}
|
||||
return replacement;
|
||||
});
|
||||
}
|
||||
|
||||
// Prevent this serializer from recursively matching the same DOM node when it calls serialize().
|
||||
let isSerializingDomSnapshot = false;
|
||||
|
||||
const plugin = {
|
||||
test: (value: unknown): value is Element | DocumentFragment =>
|
||||
!isSerializingDomSnapshot &&
|
||||
globalThis.Element &&
|
||||
(value instanceof Element || value instanceof DocumentFragment),
|
||||
print: (value: unknown, serialize: (value: unknown) => string): string => {
|
||||
isSerializingDomSnapshot = true;
|
||||
try {
|
||||
return normaliseReactUseIds(serialize(value));
|
||||
} finally {
|
||||
isSerializingDomSnapshot = false;
|
||||
}
|
||||
},
|
||||
} satisfies SnapshotSerializer;
|
||||
|
||||
export default plugin;
|
||||
// Jest compatibility exports
|
||||
export const test = plugin.test;
|
||||
export const print = plugin.print;
|
||||
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { vi, expect as viExpect } from "vitest";
|
||||
import { vi, expect as viExpect, beforeAll as viBeforeAll, afterAll as viAfterAll } from "vitest";
|
||||
import { mocked as jestMocked } from "jest-mock";
|
||||
|
||||
export const isJest = typeof jest !== "undefined";
|
||||
@@ -26,6 +26,8 @@ const mocked = adapter.mocked;
|
||||
export { adapter as vi, mocked };
|
||||
|
||||
const _expect = isJest ? (expect as unknown as typeof viExpect) : viExpect;
|
||||
export { _expect as expect };
|
||||
const _beforeAll = isJest ? (beforeAll as unknown as typeof viBeforeAll) : viBeforeAll;
|
||||
const _afterAll = isJest ? (afterAll as unknown as typeof viAfterAll) : viAfterAll;
|
||||
export { _expect as expect, _beforeAll as beforeAll, _afterAll as afterAll };
|
||||
|
||||
export { type Mocked, type MockedObject } from "vitest";
|
||||
|
||||
@@ -21,43 +21,6 @@ declare global {
|
||||
|
||||
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
|
||||
|
||||
const REACT_USE_ID = /_r_[a-z0-9]+_/g;
|
||||
|
||||
function normaliseReactUseIds(snapshot: string): string {
|
||||
// React useId values can vary between runs and make snapshots flaky:
|
||||
// https://github.com/element-hq/element-web/issues/31765
|
||||
// Avoid running the regex for DOM snapshots without React useId output.
|
||||
if (!snapshot.includes("_r_")) return snapshot;
|
||||
|
||||
const ids = new Map<string, string>();
|
||||
let nextId = 1;
|
||||
|
||||
return snapshot.replace(REACT_USE_ID, (id) => {
|
||||
let replacement = ids.get(id);
|
||||
if (!replacement) {
|
||||
replacement = `react-use-id-${nextId++}`;
|
||||
ids.set(id, replacement);
|
||||
}
|
||||
return replacement;
|
||||
});
|
||||
}
|
||||
|
||||
// Prevent this serializer from recursively matching the same DOM node when it calls serialize().
|
||||
let isSerializingDomSnapshot = false;
|
||||
|
||||
expect.addSnapshotSerializer({
|
||||
test: (value: unknown): value is Element | DocumentFragment =>
|
||||
!isSerializingDomSnapshot && (value instanceof Element || value instanceof DocumentFragment),
|
||||
print: (value: unknown, serialize: (value: unknown) => string): string => {
|
||||
isSerializingDomSnapshot = true;
|
||||
try {
|
||||
return normaliseReactUseIds(serialize(value));
|
||||
} finally {
|
||||
isSerializingDomSnapshot = false;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// Fake random strings to give a predictable snapshot for IDs
|
||||
jest.mock("matrix-js-sdk/src/randomstring");
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -6,6 +6,8 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { beforeAll, afterAll } from "../setup/adapter.ts";
|
||||
|
||||
type FilteredConsole = Pick<Console, "log" | "error" | "info" | "debug" | "warn">;
|
||||
|
||||
/**
|
||||
|
||||
@@ -366,6 +366,7 @@ export function createTestClient(): MatrixClient {
|
||||
setRoomTag: vi.fn().mockResolvedValue({}),
|
||||
getExtendedProfileProperty: vi.fn(),
|
||||
setExtendedProfileProperty: vi.fn().mockResolvedValue(undefined),
|
||||
doesServerSupportExtendedProfiles: vi.fn(),
|
||||
} as unknown as MatrixClient;
|
||||
|
||||
client.reEmitter = new ReEmitter(client);
|
||||
|
||||
@@ -6,6 +6,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { defineProject } from "vitest/config";
|
||||
import svgr from "vite-plugin-svgr";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
export default defineProject({
|
||||
@@ -53,5 +54,15 @@ export default defineProject({
|
||||
url: "http://localhost/",
|
||||
},
|
||||
},
|
||||
snapshotSerializers: [resolve(__dirname, "./src/test/react-use-id-serializer.ts")],
|
||||
},
|
||||
plugins: [
|
||||
svgr({
|
||||
svgrOptions: {
|
||||
ref: true,
|
||||
svgProps: { "role": "presentation", "aria-hidden": "true" },
|
||||
expandProps: "end",
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
+44
-52
@@ -506,62 +506,54 @@ export default (env: string, argv: Record<string, any>): webpack.Configuration =
|
||||
{
|
||||
test: /\.svg$/,
|
||||
issuer: /\.(js|ts|jsx|tsx|html)$/,
|
||||
resourceQuery: { not: [/raw/] },
|
||||
use: [
|
||||
{
|
||||
loader: "@svgr/webpack",
|
||||
options: {
|
||||
namedExport: "Icon",
|
||||
svgProps: {
|
||||
"role": "presentation",
|
||||
"aria-hidden": true,
|
||||
},
|
||||
// props set on the svg will override defaults
|
||||
expandProps: "end",
|
||||
svgoConfig: {
|
||||
plugins: [
|
||||
{
|
||||
name: "preset-default",
|
||||
params: {
|
||||
overrides: {
|
||||
removeViewBox: false,
|
||||
},
|
||||
},
|
||||
resourceQuery: /react/,
|
||||
loader: "@svgr/webpack",
|
||||
options: {
|
||||
svgProps: {
|
||||
"role": "presentation",
|
||||
"aria-hidden": true,
|
||||
},
|
||||
// props set on the svg will override defaults
|
||||
expandProps: "end",
|
||||
svgoConfig: {
|
||||
plugins: [
|
||||
{
|
||||
name: "preset-default",
|
||||
params: {
|
||||
overrides: {
|
||||
removeViewBox: false,
|
||||
},
|
||||
// generates a viewbox if missing
|
||||
{ name: "removeDimensions" },
|
||||
// https://github.com/facebook/docusaurus/issues/8297
|
||||
{ name: "prefixIds" },
|
||||
],
|
||||
},
|
||||
},
|
||||
/**
|
||||
* Forwards the React ref to the root SVG element
|
||||
* Useful when using things like `asChild` in
|
||||
* radix-ui
|
||||
*/
|
||||
ref: true,
|
||||
esModule: false,
|
||||
name: "[name].[hash:7].[ext]",
|
||||
outputPath: getAssetOutputPath,
|
||||
publicPath: function (url: string, resourcePath: string) {
|
||||
const outputPath = getAssetOutputPath(url, resourcePath);
|
||||
return toPublicPath(outputPath);
|
||||
},
|
||||
},
|
||||
// generates a viewbox if missing
|
||||
{ name: "removeDimensions" },
|
||||
// https://github.com/facebook/docusaurus/issues/8297
|
||||
{ name: "prefixIds" },
|
||||
],
|
||||
},
|
||||
{
|
||||
loader: "file-loader",
|
||||
options: {
|
||||
esModule: false,
|
||||
name: "[name].[hash:7].[ext]",
|
||||
outputPath: getAssetOutputPath,
|
||||
publicPath: function (url: string, resourcePath: string) {
|
||||
const outputPath = getAssetOutputPath(url, resourcePath);
|
||||
return toPublicPath(outputPath);
|
||||
},
|
||||
},
|
||||
/**
|
||||
* Forwards the React ref to the root SVG element
|
||||
* Useful when using things like `asChild` in
|
||||
* radix-ui
|
||||
*/
|
||||
ref: true,
|
||||
esModule: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
test: /\.svg$/,
|
||||
issuer: /\.(js|ts|jsx|tsx|html)$/,
|
||||
resourceQuery: { not: [/raw/, /react/] },
|
||||
loader: "file-loader",
|
||||
options: {
|
||||
esModule: false,
|
||||
name: "[name].[hash:7].[ext]",
|
||||
outputPath: getAssetOutputPath,
|
||||
publicPath: function (url: string, resourcePath: string) {
|
||||
const outputPath = getAssetOutputPath(url, resourcePath);
|
||||
return toPublicPath(outputPath);
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
test: /\.svg$/,
|
||||
|
||||
Generated
+14
@@ -1018,6 +1018,9 @@ importers:
|
||||
util:
|
||||
specifier: ^0.12.5
|
||||
version: 0.12.5
|
||||
vite-plugin-svgr:
|
||||
specifier: 'catalog:'
|
||||
version: 5.2.0(@typescript/typescript6@6.0.2)(rollup@4.60.1)(vite@8.1.5(@types/node@25.9.3)(esbuild@0.27.4)(jiti@2.7.0)(sugarss@5.0.1(postcss@8.5.19))(terser@5.48.0)(yaml@2.9.0))
|
||||
vitest:
|
||||
specifier: 'catalog:'
|
||||
version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@25.9.3)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(happy-dom@20.11.0)(jsdom@26.1.0(patch_hash=040623e87b1c8b676c2a705513c0276c0704dd1b23fc3a1bb77cde8128b64b5f))(vite@8.1.5(@types/node@25.9.3)(esbuild@0.27.4)(jiti@2.7.0)(sugarss@5.0.1(postcss@8.5.19))(terser@5.48.0)(yaml@2.9.0))
|
||||
@@ -26613,6 +26616,17 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- rollup
|
||||
|
||||
vite-plugin-svgr@5.2.0(@typescript/typescript6@6.0.2)(rollup@4.60.1)(vite@8.1.5(@types/node@25.9.3)(esbuild@0.27.4)(jiti@2.7.0)(sugarss@5.0.1(postcss@8.5.19))(terser@5.48.0)(yaml@2.9.0)):
|
||||
dependencies:
|
||||
'@rollup/pluginutils': 5.4.0(rollup@4.60.1)
|
||||
'@svgr/core': 8.1.0(@typescript/typescript6@6.0.2)
|
||||
'@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(@typescript/typescript6@6.0.2))
|
||||
vite: 8.1.5(@types/node@25.9.3)(esbuild@0.27.4)(jiti@2.7.0)(sugarss@5.0.1(postcss@8.5.19))(terser@5.48.0)(yaml@2.9.0)
|
||||
transitivePeerDependencies:
|
||||
- rollup
|
||||
- supports-color
|
||||
- typescript
|
||||
|
||||
vite-plugin-svgr@5.2.0(rollup@4.60.1)(typescript@7.0.2)(vite@8.1.5(@types/node@25.9.3)(esbuild@0.27.4)(jiti@2.7.0)(sugarss@5.0.1(postcss@8.5.19))(terser@5.48.0)(yaml@2.9.0)):
|
||||
dependencies:
|
||||
'@rollup/pluginutils': 5.4.0(rollup@4.60.1)
|
||||
|
||||
Reference in New Issue
Block a user