Migrate more jest tests to vitest (#33922)

* Migrate more jest tests to vitest

* Fix jest config

* Fix jest config

* Make remaining jest tests type-happy

* Iterate

* Fix Notifier import cycle

* Fix tests

* Delint

* Iterate

* Handle SDKContextClass `client` initialisation internally

Rather than via MatrixChat - this is predominantly for Lifecycle tests as they don't use a MatrixChat and it doesn't make much sense for this component to own this state.

* Fix tests

* Iterate

* Simplify diff

* Improve coverage

* Improve coverage

* Iterate
This commit is contained in:
Michael Telatynski
2026-07-07 09:50:40 +00:00
committed by GitHub
parent 30c02ab5d7
commit 6fda0ad60f
10 changed files with 120 additions and 88 deletions
@@ -6,6 +6,10 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
// @vitest-environment happy-dom
import { vi, describe, it, expect, beforeAll, afterAll } from "vitest";
import {
formatSeconds,
formatRelativeTime,
@@ -25,9 +29,11 @@ import {
HOUR_MS,
MINUTE_MS,
DAY_MS,
} from "../../../src/DateUtils";
import { REPEATABLE_DATE, mockIntlDateTimeFormat, unmockIntlDateTimeFormat } from "../../test-utils";
import * as languageSettings from "../../../src/i18n/settings";
} from "./DateUtils";
import { REPEATABLE_DATE, mockIntlDateTimeFormat, unmockIntlDateTimeFormat } from "../test/test-utils";
import * as languageSettings from "./i18n/settings";
vi.mock("./TimezoneHandler", () => ({ getUserTimezone: () => "UTC" }));
describe("getDaysArray", () => {
it("should return Sunday-Saturday in long mode", () => {
@@ -166,13 +172,13 @@ describe("getMonthsArray", () => {
describe("formatDate", () => {
beforeAll(() => {
jest.useFakeTimers();
jest.setSystemTime(REPEATABLE_DATE);
vi.useFakeTimers();
vi.setSystemTime(REPEATABLE_DATE);
});
afterAll(() => {
jest.setSystemTime(jest.getRealSystemTime());
jest.useRealTimers();
vi.setSystemTime(vi.getRealSystemTime());
vi.useRealTimers();
});
it("should return time string if date is within same day", () => {
@@ -255,14 +261,14 @@ describe("formatSeconds", () => {
describe("formatRelativeTime", () => {
beforeAll(() => {
jest.useFakeTimers();
vi.useFakeTimers();
// Tuesday, 2 November 2021 11:18:03 UTC
jest.setSystemTime(1635851883000);
vi.setSystemTime(1635851883000);
});
afterAll(() => {
jest.setSystemTime(jest.getRealSystemTime());
jest.useRealTimers();
vi.setSystemTime(vi.getRealSystemTime());
vi.useRealTimers();
});
it("returns hour format for events created in the same day", () => {
@@ -369,7 +375,7 @@ describe("formatLocalDateShort()", () => {
});
const timestamp = new Date("Fri Dec 17 2021 09:09:00 GMT+0100 (Central European Standard Time)").getTime();
it("formats date correctly by locale", () => {
const locale = jest.spyOn(languageSettings, "getUserLanguage");
const locale = vi.spyOn(languageSettings, "getUserLanguage");
mockIntlDateTimeFormat();
// format is DD/MM/YY
+4
View File
@@ -8,6 +8,7 @@ Please see LICENSE files in the repository root for full details.
import { vi } from "vitest";
import { mocks } from "../../test/setup/mocks.ts";
import SdkConfig, { DEFAULTS } from "../SdkConfig";
// set up AudioContext API mock
vi.stubGlobal("AudioContext", function () {
@@ -28,3 +29,6 @@ if (globalThis.window === undefined) {
setTimeout: globalThis.setTimeout,
});
}
// uninitialised SdkConfig causes lots of warnings in console, init with defaults
SdkConfig.put(DEFAULTS);
+3 -1
View File
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { beforeEach, afterEach } from "vitest";
import { vi, beforeEach, afterEach } from "vitest";
import fetchMock, { manageFetchMockGlobally } from "@fetch-mock/vitest";
import SdkConfig, { DEFAULTS } from "../SdkConfig";
@@ -15,6 +15,8 @@ import { setupLanguageMock } from "./setupLanguage.ts";
manageFetchMockGlobally();
beforeEach(() => {
vi.stubEnv("TZ", "UTC");
// set up fetch API mock
fetchMock.hardReset();
fetchMock.catch(404);
@@ -6,8 +6,11 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
// @vitest-environment happy-dom
import { type ReactElement } from "react";
import { render } from "jest-matrix-react";
import { render } from "test-utils-rtl";
import { describe, it, expect } from "vitest";
import { MatrixError, ConnectionError } from "matrix-js-sdk/src/matrix";
import {
@@ -17,7 +20,7 @@ import {
messageForResourceLimitError,
messageForSyncError,
resourceLimitStrings,
} from "../../../src/utils/ErrorUtils";
} from "./ErrorUtils";
describe("messageForResourceLimitError", () => {
it("should match snapshot for monthly_active_user", () => {
@@ -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.
*/
// @vitest-environment happy-dom
import {
M_LOCATION,
EventStatus,
@@ -19,8 +21,9 @@ import {
Room,
Thread,
} from "matrix-js-sdk/src/matrix";
import { vi, describe, it, expect, beforeEach, afterAll } from "vitest";
import { MatrixClientPeg } from "../../../src/MatrixClientPeg";
import { MatrixClientPeg } from "../MatrixClientPeg";
import {
canCancel,
canEditContent,
@@ -31,25 +34,30 @@ import {
isContentActionable,
isLocationEvent,
isVoiceMessage,
} from "../../../src/utils/EventUtils";
import { getMockClientWithEventEmitter, makeBeaconInfoEvent, makePollStartEvent, stubClient } from "../../test-utils";
import dis from "../../../src/dispatcher/dispatcher";
import { Action } from "../../../src/dispatcher/actions";
} from "./EventUtils";
import {
getMockClientWithEventEmitter,
makeBeaconInfoEvent,
makePollStartEvent,
stubClient,
} from "../../test/test-utils";
import dis from "../dispatcher/dispatcher";
import { Action } from "../dispatcher/actions";
jest.mock("../../../src/dispatcher/dispatcher");
vi.mock("../dispatcher/dispatcher");
describe("EventUtils", () => {
const userId = "@user:server";
const roomId = "!room:server";
const mockClient = getMockClientWithEventEmitter({
getUserId: jest.fn().mockReturnValue(userId),
getUserId: vi.fn().mockReturnValue(userId),
});
beforeEach(() => {
mockClient.getUserId.mockClear().mockReturnValue(userId);
});
afterAll(() => {
jest.spyOn(MatrixClientPeg, "get").mockRestore();
vi.spyOn(MatrixClientPeg, "get").mockRestore();
});
// setup events
@@ -404,7 +412,7 @@ describe("EventUtils", () => {
};
beforeEach(() => {
jest.clearAllMocks();
vi.clearAllMocks();
stubClient();
client = MatrixClientPeg.safeGet();
@@ -413,9 +421,9 @@ describe("EventUtils", () => {
pendingEventOrdering: PendingEventOrdering.Detached,
});
jest.spyOn(client, "supportsThreads").mockReturnValue(true);
jest.spyOn(client, "getRoom").mockReturnValue(room);
jest.spyOn(client, "fetchRoomEvent").mockImplementation(async (roomId, eventId) => {
vi.spyOn(client, "supportsThreads").mockReturnValue(true);
vi.spyOn(client, "getRoom").mockReturnValue(room);
vi.spyOn(client, "fetchRoomEvent").mockImplementation(async (roomId, eventId) => {
return events[eventId] ?? Promise.reject();
});
});
@@ -6,8 +6,9 @@ Please see LICENSE files in the repository root for full details.
*/
import { type MediaEventContent } from "matrix-js-sdk/src/types";
import { describe, it, expect } from "vitest";
import { downloadLabelForFile } from "../../../src/utils/FileUtils.ts";
import { downloadLabelForFile } from "./FileUtils.ts";
describe("FileUtils", () => {
describe("downloadLabelForFile", () => {
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`messageForConnectionError should match snapshot for ConnectionError 1`] = `
exports[`messageForConnectionError > should match snapshot for ConnectionError 1`] = `
<DocumentFragment>
<span>
<span>
@@ -34,13 +34,13 @@ exports[`messageForConnectionError should match snapshot for ConnectionError 1`]
</DocumentFragment>
`;
exports[`messageForConnectionError should match snapshot for MatrixError M_NOT_FOUND 1`] = `
exports[`messageForConnectionError > should match snapshot for MatrixError M_NOT_FOUND 1`] = `
<DocumentFragment>
There was a problem communicating with the homeserver, please try again later.(M_NOT_FOUND)
</DocumentFragment>
`;
exports[`messageForConnectionError should match snapshot for mixed content error 1`] = `
exports[`messageForConnectionError > should match snapshot for mixed content error 1`] = `
<DocumentFragment>
<span>
<span>
@@ -58,19 +58,19 @@ exports[`messageForConnectionError should match snapshot for mixed content error
</DocumentFragment>
`;
exports[`messageForConnectionError should match snapshot for unknown error 1`] = `
exports[`messageForConnectionError > should match snapshot for unknown error 1`] = `
<DocumentFragment>
There was a problem communicating with the homeserver, please try again later.
</DocumentFragment>
`;
exports[`messageForLoginError should match snapshot for 401 1`] = `
exports[`messageForLoginError > should match snapshot for 401 1`] = `
<DocumentFragment>
Incorrect username and/or password.
</DocumentFragment>
`;
exports[`messageForLoginError should match snapshot for M_RESOURCE_LIMIT_EXCEEDED 1`] = `
exports[`messageForLoginError > should match snapshot for M_RESOURCE_LIMIT_EXCEEDED 1`] = `
<DocumentFragment>
<div>
<div>
@@ -85,19 +85,19 @@ exports[`messageForLoginError should match snapshot for M_RESOURCE_LIMIT_EXCEEDE
</DocumentFragment>
`;
exports[`messageForLoginError should match snapshot for M_USER_DEACTIVATED 1`] = `
exports[`messageForLoginError > should match snapshot for M_USER_DEACTIVATED 1`] = `
<DocumentFragment>
This account has been deactivated.
</DocumentFragment>
`;
exports[`messageForLoginError should match snapshot for unknown error 1`] = `
exports[`messageForLoginError > should match snapshot for unknown error 1`] = `
<DocumentFragment>
There was a problem communicating with the homeserver, please try again later. (HTTP 400)
</DocumentFragment>
`;
exports[`messageForResourceLimitError should match snapshot for admin contact links 1`] = `
exports[`messageForResourceLimitError > should match snapshot for admin contact links 1`] = `
<DocumentFragment>
<span>
Please
@@ -113,13 +113,13 @@ exports[`messageForResourceLimitError should match snapshot for admin contact li
</DocumentFragment>
`;
exports[`messageForResourceLimitError should match snapshot for monthly_active_user 1`] = `
exports[`messageForResourceLimitError > should match snapshot for monthly_active_user 1`] = `
<DocumentFragment>
This homeserver has hit its Monthly Active User limit.
</DocumentFragment>
`;
exports[`messageForSyncError should match snapshot for M_RESOURCE_LIMIT_EXCEEDED 1`] = `
exports[`messageForSyncError > should match snapshot for M_RESOURCE_LIMIT_EXCEEDED 1`] = `
<DocumentFragment>
<div>
<div>
@@ -132,7 +132,7 @@ exports[`messageForSyncError should match snapshot for M_RESOURCE_LIMIT_EXCEEDED
</DocumentFragment>
`;
exports[`messageForSyncError should match snapshot for other errors 1`] = `
exports[`messageForSyncError > should match snapshot for other errors 1`] = `
<DocumentFragment>
<div>
Unable to connect to Homeserver. Retrying…
@@ -6,25 +6,29 @@ 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 { mocked, type Mocked } from "jest-mock-vitest-adapter";
// @vitest-environment happy-dom
import { vi, describe, it, expect, beforeEach, afterEach, type Mocked } from "vitest";
import { type MatrixClient, type Room } from "matrix-js-sdk/src/matrix";
import { sleep } from "matrix-js-sdk/src/utils";
import { mkRoom, resetAsyncStoreWithClient, setupAsyncStoreWithClient, stubClient } from "test-utils/test-utils";
import { MatrixClientPeg } from "../../../src/MatrixClientPeg";
import { mkRoom, resetAsyncStoreWithClient, setupAsyncStoreWithClient, stubClient } from "../../test-utils";
import defaultDispatcher from "../../../src/dispatcher/dispatcher";
import { type ViewRoomPayload } from "../../../src/dispatcher/payloads/ViewRoomPayload";
import { Action } from "../../../src/dispatcher/actions";
import { leaveRoomBehaviour } from "../../../src/utils/leave-behaviour";
import { SDKContextClass } from "../../../src/contexts/SDKContextClass";
import DMRoomMap from "../../../src/utils/DMRoomMap";
import SpaceStore from "../../../src/stores/spaces/SpaceStore";
import { MetaSpace } from "../../../src/stores/spaces";
import { type ActionPayload } from "../../../src/dispatcher/payloads";
import SettingsStore from "../../../src/settings/SettingsStore";
import { CallStore } from "../../../src/stores/CallStore";
import { type Call } from "../../../src/models/Call";
import LegacyCallHandler from "../../../src/LegacyCallHandler";
import { MatrixClientPeg } from "../MatrixClientPeg";
import defaultDispatcher from "../dispatcher/dispatcher";
import { type ViewRoomPayload } from "../dispatcher/payloads/ViewRoomPayload";
import { Action } from "../dispatcher/actions";
import { leaveRoomBehaviour } from "./leave-behaviour";
import { SDKContextClass } from "../contexts/SDKContextClass";
import DMRoomMap from "../utils/DMRoomMap";
import SpaceStore from "../stores/spaces/SpaceStore";
import { MetaSpace } from "../stores/spaces";
import { type ActionPayload } from "../dispatcher/payloads";
import SettingsStore from "../settings/SettingsStore";
import { CallStore } from "../stores/CallStore";
import { type Call } from "../models/Call";
import LegacyCallHandler from "../LegacyCallHandler";
vi.mock("../Modal.tsx");
describe("leaveRoomBehaviour", () => {
SDKContextClass.instance.constructEagerStores(); // Initialize RoomViewStore
@@ -35,7 +39,7 @@ describe("leaveRoomBehaviour", () => {
beforeEach(async () => {
stubClient();
client = mocked(MatrixClientPeg.safeGet());
client = vi.mocked(MatrixClientPeg.safeGet());
DMRoomMap.makeShared(client);
room = mkRoom(client, "!1:example.org");
@@ -58,7 +62,7 @@ describe("leaveRoomBehaviour", () => {
afterEach(async () => {
SpaceStore.instance.setActiveSpace(MetaSpace.Home);
await resetAsyncStoreWithClient(SpaceStore.instance);
jest.restoreAllMocks();
vi.restoreAllMocks();
});
const viewRoom = (room: Room) =>
@@ -72,7 +76,7 @@ describe("leaveRoomBehaviour", () => {
);
const expectDispatch = async <T extends ActionPayload>(payload: T) => {
const dispatcherSpy = jest.fn();
const dispatcherSpy = vi.fn();
const dispatcherRef = defaultDispatcher.register(dispatcherSpy);
await sleep(0);
expect(dispatcherSpy).toHaveBeenCalledWith(payload);
@@ -80,7 +84,7 @@ describe("leaveRoomBehaviour", () => {
};
it("hangs up legacy calls when leaving a room", async () => {
const hangupSpy = jest.spyOn(LegacyCallHandler.instance, "hangupOrReject").mockImplementation(() => {});
const hangupSpy = vi.spyOn(LegacyCallHandler.instance, "hangupOrReject").mockImplementation(() => {});
viewRoom(room);
await leaveRoomBehaviour(client, room.roomId);
@@ -90,10 +94,10 @@ describe("leaveRoomBehaviour", () => {
it("disconnects widget-based calls when leaving a room", async () => {
const mockCall = {
disconnect: jest.fn().mockResolvedValue(undefined),
disconnect: vi.fn().mockResolvedValue(undefined),
} as unknown as Call;
jest.spyOn(CallStore.instance, "getActiveCall").mockReturnValue(mockCall);
vi.spyOn(CallStore.instance, "getActiveCall").mockReturnValue(mockCall);
viewRoom(room);
await leaveRoomBehaviour(client, room.roomId);
@@ -109,7 +113,7 @@ describe("leaveRoomBehaviour", () => {
});
it("returns to the parent space after leaving a room inside of a space that was being viewed", async () => {
jest.spyOn(SpaceStore.instance, "getCanonicalParent").mockImplementation((roomId) =>
vi.spyOn(SpaceStore.instance, "getCanonicalParent").mockImplementation((roomId) =>
roomId === room.roomId ? space : null,
);
viewRoom(room);
@@ -133,7 +137,7 @@ describe("leaveRoomBehaviour", () => {
it("returns to the parent space after leaving a subspace that was being viewed", async () => {
room.isSpaceRoom.mockReturnValue(true);
jest.spyOn(SpaceStore.instance, "getCanonicalParent").mockImplementation((roomId) =>
vi.spyOn(SpaceStore.instance, "getCanonicalParent").mockImplementation((roomId) =>
roomId === room.roomId ? space : null,
);
viewRoom(room);
@@ -149,7 +153,7 @@ describe("leaveRoomBehaviour", () => {
describe("If the feature_dynamic_room_predecessors is not enabled", () => {
beforeEach(() => {
jest.spyOn(SettingsStore, "getValue").mockReturnValue(false);
vi.spyOn(SettingsStore, "getValue").mockReturnValue(false);
});
it("Passes through the dynamic predecessor setting", async () => {
@@ -161,7 +165,7 @@ describe("leaveRoomBehaviour", () => {
describe("If the feature_dynamic_room_predecessors is enabled", () => {
beforeEach(() => {
// Turn on feature_dynamic_room_predecessors setting
jest.spyOn(SettingsStore, "getValue").mockImplementation(
vi.spyOn(SettingsStore, "getValue").mockImplementation(
(settingName) => settingName === "feature_dynamic_room_predecessors",
);
});
@@ -5,18 +5,20 @@
* Please see LICENSE files in the repository root for full details.
*/
import { type MatrixClient, type Room, RoomEvent } from "matrix-js-sdk/src/matrix";
import { type MockedObject } from "jest-mock-vitest-adapter";
import { createRef } from "react";
// @vitest-environment happy-dom
import { mkRoom, stubClient } from "../../test-utils";
import { WidgetPipViewModel } from "../../../src/viewmodels/room/WidgetPipViewModel";
import WidgetStore, { type IApp } from "../../../src/stores/WidgetStore";
import defaultDispatcher from "../../../src/dispatcher/dispatcher";
import { Action } from "../../../src/dispatcher/actions";
import { WidgetLayoutStore } from "../../../src/stores/widgets/WidgetLayoutStore";
import { CallStore, CallStoreEvent } from "../../../src/stores/CallStore";
import { type Call } from "../../../src/models/Call";
import { type MatrixClient, type Room, RoomEvent } from "matrix-js-sdk/src/matrix";
import { vi, describe, it, expect, beforeEach, afterEach, type MockedObject } from "vitest";
import { createRef } from "react";
import { mkRoom, stubClient } from "test-utils";
import { WidgetPipViewModel } from "./WidgetPipViewModel";
import WidgetStore, { type IApp } from "../../stores/WidgetStore";
import defaultDispatcher from "../../dispatcher/dispatcher";
import { Action } from "../../dispatcher/actions";
import { WidgetLayoutStore } from "../../stores/widgets/WidgetLayoutStore";
import { CallStore, CallStoreEvent } from "../../stores/CallStore";
import { type Call } from "../../models/Call";
const userId = "@example:example.org";
const widgetId = "test-widget-id";
@@ -25,8 +27,8 @@ type BackClickEvent = Parameters<WidgetPipViewModel["onBackClick"]>[0];
const createBackClickEvent = (): BackClickEvent =>
({
preventDefault: jest.fn(),
stopPropagation: jest.fn(),
preventDefault: vi.fn(),
stopPropagation: vi.fn(),
}) as unknown as BackClickEvent;
describe("WidgetPipViewModel", () => {
@@ -46,7 +48,7 @@ describe("WidgetPipViewModel", () => {
name: "Test Widget",
data: {},
} as unknown as IApp;
jest.spyOn(WidgetStore.instance, "getApps").mockReturnValue([widget]);
vi.spyOn(WidgetStore.instance, "getApps").mockReturnValue([widget]);
vm = new WidgetPipViewModel({
room,
@@ -58,7 +60,7 @@ describe("WidgetPipViewModel", () => {
afterEach(() => {
vm.dispose();
jest.restoreAllMocks();
vi.restoreAllMocks();
});
it("updates room name", () => {
@@ -68,7 +70,7 @@ describe("WidgetPipViewModel", () => {
});
it("updates onBackClick if call changes", () => {
const dispatchSpy = jest.spyOn(defaultDispatcher, "dispatch").mockImplementation(() => {});
const dispatchSpy = vi.spyOn(defaultDispatcher, "dispatch").mockImplementation(() => {});
vm.onBackClick(createBackClickEvent());
expect(dispatchSpy).toHaveBeenCalledWith({
@@ -91,8 +93,8 @@ describe("WidgetPipViewModel", () => {
});
it("updates onBackClick if viewingRoom changes", () => {
const dispatchSpy = jest.spyOn(defaultDispatcher, "dispatch").mockImplementation(() => {});
const moveSpy = jest.spyOn(WidgetLayoutStore.instance, "moveToContainer").mockImplementation(() => {});
const dispatchSpy = vi.spyOn(defaultDispatcher, "dispatch").mockImplementation(() => {});
const moveSpy = vi.spyOn(WidgetLayoutStore.instance, "moveToContainer").mockImplementation(() => {});
vm.setViewingRoom(true);
vm.onBackClick(createBackClickEvent());
@@ -10,7 +10,9 @@ import React, { type ReactElement } from "react";
// eslint-disable-next-line no-restricted-imports
import { render, type RenderOptions } from "@testing-library/react";
import { TooltipProvider } from "@vector-im/compound-web";
import { I18nContext } from "@element-hq/web-shared-components";
import { I18nApi, I18nContext } from "@element-hq/web-shared-components";
const i18nApi = new I18nApi();
/**
* Wraps the provided components in:
@@ -26,7 +28,7 @@ const wrapWithStandardContexts = (Wrapper: RenderOptions["wrapper"]) => {
if (Wrapper) {
return (
<Wrapper>
<I18nContext.Provider value={window.mxModuleApi.i18n}>
<I18nContext.Provider value={i18nApi}>
<TooltipProvider>{children}</TooltipProvider>
</I18nContext.Provider>
</Wrapper>
@@ -34,7 +36,7 @@ const wrapWithStandardContexts = (Wrapper: RenderOptions["wrapper"]) => {
} else {
return (
<TooltipProvider>
<I18nContext.Provider value={window.mxModuleApi.i18n}>{children}</I18nContext.Provider>
<I18nContext.Provider value={i18nApi}>{children}</I18nContext.Provider>
</TooltipProvider>
);
}