From d5164acb501da28dfce8ae08a606cfb7a0578e6d Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 6 Jul 2026 15:10:50 +0100 Subject: [PATCH] Migrate batch of tests to vitest (#34111) * Migrate batch of tests tro vitest * Iterate * Migrate another test * Fix lockfile --- apps/web/package.json | 1 + .../Avatar-test.ts => src/Avatar.test.ts} | 52 ++--- .../KeyBindingsManager.test.ts} | 4 +- .../Lifecycle.test.ts} | 199 ++++++++---------- .../Markdown-test.ts => src/Markdown.test.ts} | 4 +- .../PosthogAnalytics.test.ts} | 105 +++++---- .../PosthogTrackers.test.ts} | 12 +- .../RoomNotifs.test.ts} | 30 +-- .../Rooms-test.ts => src/Rooms.test.ts} | 22 +- .../ScalarAuthClient.test.ts} | 32 +-- .../SdkConfig.test.ts} | 4 +- .../SupportedBrowser.test.ts} | 21 +- .../TimezoneHandler.test.ts} | 6 +- .../__snapshots__/favicon.test.ts.snap} | 6 +- .../favicon-test.ts => src/favicon.test.ts} | 25 ++- apps/web/test/setup/adapter.ts | 5 +- apps/web/test/test-utils/threads.ts | 1 + pnpm-lock.yaml | 14 ++ 18 files changed, 285 insertions(+), 258 deletions(-) rename apps/web/{test/unit-tests/Avatar-test.ts => src/Avatar.test.ts} (60%) rename apps/web/{test/unit-tests/KeyBindingsManager-test.ts => src/KeyBindingsManager.test.ts} (98%) rename apps/web/{test/unit-tests/Lifecycle-test.ts => src/Lifecycle.test.ts} (86%) rename apps/web/{test/unit-tests/Markdown-test.ts => src/Markdown.test.ts} (99%) rename apps/web/{test/unit-tests/PosthogAnalytics-test.ts => src/PosthogAnalytics.test.ts} (79%) rename apps/web/{test/unit-tests/PosthogTrackers-test.ts => src/PosthogTrackers.test.ts} (76%) rename apps/web/{test/unit-tests/RoomNotifs-test.ts => src/RoomNotifs.test.ts} (94%) rename apps/web/{test/unit-tests/Rooms-test.ts => src/Rooms.test.ts} (86%) rename apps/web/{test/unit-tests/ScalarAuthClient-test.ts => src/ScalarAuthClient.test.ts} (89%) rename apps/web/{test/unit-tests/SdkConfig-test.ts => src/SdkConfig.test.ts} (92%) rename apps/web/{test/unit-tests/SupportedBrowser-test.ts => src/SupportedBrowser.test.ts} (90%) rename apps/web/{test/unit-tests/TimezoneHandler-test.ts => src/TimezoneHandler.test.ts} (85%) rename apps/web/{test/unit-tests/__snapshots__/favicon-test.ts.snap => src/__snapshots__/favicon.test.ts.snap} (96%) rename apps/web/{test/unit-tests/favicon-test.ts => src/favicon.test.ts} (84%) diff --git a/apps/web/package.json b/apps/web/package.json index 9b8b51e64a..3b0347223b 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -228,6 +228,7 @@ "typescript": "catalog:", "util": "^0.12.5", "vitest": "catalog:", + "vitest-canvas-mock": "^1.1.4", "web-streams-polyfill": "^4.0.0", "webpack": "^5.89.0", "webpack-bundle-analyzer": "^5.0.0", diff --git a/apps/web/test/unit-tests/Avatar-test.ts b/apps/web/src/Avatar.test.ts similarity index 60% rename from apps/web/test/unit-tests/Avatar-test.ts rename to apps/web/src/Avatar.test.ts index 72e313db28..4ee7d894c8 100644 --- a/apps/web/test/unit-tests/Avatar-test.ts +++ b/apps/web/src/Avatar.test.ts @@ -6,15 +6,15 @@ 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 } from "jest-mock"; +import { vi, describe, it, expect, beforeEach, type Mock } from "vitest"; import { type Room, type RoomMember, RoomType } from "matrix-js-sdk/src/matrix"; -import { avatarUrlForRoom } from "../../src/Avatar"; -import { type Media, mediaFromMxc } from "../../src/customisations/Media"; -import DMRoomMap from "../../src/utils/DMRoomMap"; +import { avatarUrlForRoom } from "./Avatar"; +import { type Media, mediaFromMxc } from "./customisations/Media"; +import DMRoomMap from "./utils/DMRoomMap"; -jest.mock("../../src/customisations/Media", () => ({ - mediaFromMxc: jest.fn(), +vi.mock("./customisations/Media", () => ({ + mediaFromMxc: vi.fn(), })); const roomId = "!room:example.com"; @@ -22,31 +22,31 @@ const avatarUrl1 = "https://example.com/avatar1"; const avatarUrl2 = "https://example.com/avatar2"; describe("avatarUrlForRoom", () => { - let getThumbnailOfSourceHttp: jest.Mock; + let getThumbnailOfSourceHttp: Mock; let room: Room; let roomMember: RoomMember; let dmRoomMap: DMRoomMap; beforeEach(() => { - getThumbnailOfSourceHttp = jest.fn(); - mocked(mediaFromMxc).mockImplementation((): Media => { + getThumbnailOfSourceHttp = vi.fn(); + vi.mocked(mediaFromMxc).mockImplementation((): Media => { return { getThumbnailOfSourceHttp, } as unknown as Media; }); room = { roomId, - getMxcAvatarUrl: jest.fn(), - isSpaceRoom: jest.fn(), - getType: jest.fn(), - getAvatarFallbackMember: jest.fn(), + getMxcAvatarUrl: vi.fn(), + isSpaceRoom: vi.fn(), + getType: vi.fn(), + getAvatarFallbackMember: vi.fn(), } as unknown as Room; dmRoomMap = { - getUserIdForRoomId: jest.fn(), + getUserIdForRoomId: vi.fn(), } as unknown as DMRoomMap; DMRoomMap.setShared(dmRoomMap); roomMember = { - getMxcAvatarUrl: jest.fn(), + getMxcAvatarUrl: vi.fn(), } as unknown as RoomMember; }); @@ -55,40 +55,40 @@ describe("avatarUrlForRoom", () => { }); it("should return the HTTP source if the room provides a MXC url", () => { - mocked(room.getMxcAvatarUrl).mockReturnValue(avatarUrl1); + vi.mocked(room.getMxcAvatarUrl).mockReturnValue(avatarUrl1); getThumbnailOfSourceHttp.mockReturnValue(avatarUrl2); expect(avatarUrlForRoom(room, 128, 256, "crop")).toEqual(avatarUrl2); expect(getThumbnailOfSourceHttp).toHaveBeenCalledWith(128, 256, "crop"); }); it("should return null for a space room", () => { - mocked(room.isSpaceRoom).mockReturnValue(true); - mocked(room.getType).mockReturnValue(RoomType.Space); + vi.mocked(room.isSpaceRoom).mockReturnValue(true); + vi.mocked(room.getType).mockReturnValue(RoomType.Space); expect(avatarUrlForRoom(room, 128, 128)).toBeNull(); }); it("should return null if the room is not a DM", () => { - mocked(dmRoomMap).getUserIdForRoomId.mockReturnValue(undefined); + vi.mocked(dmRoomMap).getUserIdForRoomId.mockReturnValue(undefined); expect(avatarUrlForRoom(room, 128, 128)).toBeNull(); expect(dmRoomMap.getUserIdForRoomId).toHaveBeenCalledWith(roomId); }); it("should return null if there is no other member in the room", () => { - mocked(dmRoomMap).getUserIdForRoomId.mockReturnValue("@user:example.com"); - mocked(room.getAvatarFallbackMember).mockReturnValue(undefined); + vi.mocked(dmRoomMap).getUserIdForRoomId.mockReturnValue("@user:example.com"); + vi.mocked(room.getAvatarFallbackMember).mockReturnValue(undefined); expect(avatarUrlForRoom(room, 128, 128)).toBeNull(); }); it("should return null if the other member has no avatar URL", () => { - mocked(dmRoomMap).getUserIdForRoomId.mockReturnValue("@user:example.com"); - mocked(room.getAvatarFallbackMember).mockReturnValue(roomMember); + vi.mocked(dmRoomMap).getUserIdForRoomId.mockReturnValue("@user:example.com"); + vi.mocked(room.getAvatarFallbackMember).mockReturnValue(roomMember); expect(avatarUrlForRoom(room, 128, 128)).toBeNull(); }); it("should return the other member's avatar URL", () => { - mocked(dmRoomMap).getUserIdForRoomId.mockReturnValue("@user:example.com"); - mocked(room.getAvatarFallbackMember).mockReturnValue(roomMember); - mocked(roomMember.getMxcAvatarUrl).mockReturnValue(avatarUrl2); + vi.mocked(dmRoomMap).getUserIdForRoomId.mockReturnValue("@user:example.com"); + vi.mocked(room.getAvatarFallbackMember).mockReturnValue(roomMember); + vi.mocked(roomMember.getMxcAvatarUrl).mockReturnValue(avatarUrl2); getThumbnailOfSourceHttp.mockReturnValue(avatarUrl2); expect(avatarUrlForRoom(room, 128, 256, "crop")).toEqual(avatarUrl2); expect(getThumbnailOfSourceHttp).toHaveBeenCalledWith(128, 256, "crop"); diff --git a/apps/web/test/unit-tests/KeyBindingsManager-test.ts b/apps/web/src/KeyBindingsManager.test.ts similarity index 98% rename from apps/web/test/unit-tests/KeyBindingsManager-test.ts rename to apps/web/src/KeyBindingsManager.test.ts index ed0be01fe2..02c93d430e 100644 --- a/apps/web/test/unit-tests/KeyBindingsManager-test.ts +++ b/apps/web/src/KeyBindingsManager.test.ts @@ -6,7 +6,9 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import { isKeyComboMatch, type KeyCombo } from "../../src/KeyBindingsManager"; +import { describe, it, expect } from "vitest"; + +import { isKeyComboMatch, type KeyCombo } from "./KeyBindingsManager"; function mockKeyEvent( key: string, diff --git a/apps/web/test/unit-tests/Lifecycle-test.ts b/apps/web/src/Lifecycle.test.ts similarity index 86% rename from apps/web/test/unit-tests/Lifecycle-test.ts rename to apps/web/src/Lifecycle.test.ts index 3b15100410..ab0d05154e 100644 --- a/apps/web/test/unit-tests/Lifecycle-test.ts +++ b/apps/web/src/Lifecycle.test.ts @@ -6,37 +6,34 @@ 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 { Crypto } from "@peculiar/webcrypto"; +// @vitest-environment happy-dom + +import { vi, describe, it, expect, beforeEach, afterEach, type MockedObject } from "vitest"; import { logger } from "matrix-js-sdk/src/logger"; import * as MatrixJs from "matrix-js-sdk/src/matrix"; import { decodeBase64, encodeUnpaddedBase64 } from "matrix-js-sdk/src/matrix"; import * as encryptAESSecretStorageItemModule from "matrix-js-sdk/src/utils/encryptAESSecretStorageItem"; -import { mocked, type MockedObject } from "jest-mock-vitest-adapter"; -import fetchMock from "@fetch-mock/jest"; +import fetchMock from "@fetch-mock/vitest"; +import { flushPromises, getMockClientWithEventEmitter, mockClientMethodsUser, mockPlatformPeg } from "test-utils"; +import { makeDelegatedAuthConfig } from "test-utils/oidc"; -import StorageEvictedDialog from "../../src/components/views/dialogs/StorageEvictedDialog"; -import * as Lifecycle from "../../src/Lifecycle"; -import { MatrixClientPeg } from "../../src/MatrixClientPeg"; -import Modal from "../../src/Modal"; -import * as StorageAccess from "../../src/utils/StorageAccess"; -import { idbSave } from "../../src/utils/StorageAccess"; -import { flushPromises, getMockClientWithEventEmitter, mockClientMethodsUser, mockPlatformPeg } from "../test-utils"; -import { OidcClientStore } from "../../src/stores/oidc/OidcClientStore"; -import { makeDelegatedAuthConfig } from "../test-utils/oidc"; -import { Action } from "../../src/dispatcher/actions"; -import PlatformPeg from "../../src/PlatformPeg"; -import { persistAccessTokenInStorage, persistRefreshTokenInStorage } from "../../src/utils/tokens/tokens"; -import { encryptPickleKey } from "../../src/utils/tokens/pickling"; -import * as StorageManager from "../../src/utils/StorageManager.ts"; -import type BasePlatform from "../../src/BasePlatform.ts"; -import * as createMatrixClientModule from "../../src/utils/createMatrixClient"; +import StorageEvictedDialog from "./components/views/dialogs/StorageEvictedDialog"; +import * as Lifecycle from "./Lifecycle"; +import { MatrixClientPeg } from "./MatrixClientPeg"; +import Modal from "./Modal"; +import * as StorageAccess from "./utils/StorageAccess"; +import { idbSave } from "./utils/StorageAccess"; +import { OidcClientStore } from "./stores/oidc/OidcClientStore"; +import { Action } from "./dispatcher/actions"; +import PlatformPeg from "./PlatformPeg"; +import { persistAccessTokenInStorage, persistRefreshTokenInStorage } from "./utils/tokens/tokens"; +import { encryptPickleKey } from "./utils/tokens/pickling"; +import * as StorageManager from "./utils/StorageManager.ts"; +import type BasePlatform from "./BasePlatform.ts"; +import * as createMatrixClientModule from "./utils/createMatrixClient"; const { logout, restoreSessionFromStorage, setLoggedIn } = Lifecycle; -const webCrypto = new Crypto(); - -const windowCrypto = window.crypto; - describe("Lifecycle", () => { const homeserverUrl = "https://domain"; const identityServerUrl = "https://is.org"; @@ -46,65 +43,55 @@ describe("Lifecycle", () => { let mockPlatform: MockedObject; - const realLocalStorage = global.localStorage; - let mockClient!: MockedObject; beforeEach(() => { - jest.restoreAllMocks(); mockPlatform = mockPlatformPeg(); mockClient = getMockClientWithEventEmitter({ ...mockClientMethodsUser(), - stopClient: jest.fn(), - removeAllListeners: jest.fn(), - clearStores: jest.fn(), - getAccountData: jest.fn(), - getDeviceId: jest.fn().mockReturnValue(deviceId), - isVersionSupported: jest.fn().mockResolvedValue(true), - getCrypto: jest.fn(), - getClientWellKnown: jest.fn(), - waitForClientWellKnown: jest.fn(), - getThirdpartyProtocols: jest.fn(), + stopClient: vi.fn(), + removeAllListeners: vi.fn(), + clearStores: vi.fn(), + getAccountData: vi.fn(), + getDeviceId: vi.fn().mockReturnValue(deviceId), + isVersionSupported: vi.fn().mockResolvedValue(true), + getCrypto: vi.fn(), + getClientWellKnown: vi.fn(), + waitForClientWellKnown: vi.fn(), + getThirdpartyProtocols: vi.fn(), store: { - destroy: jest.fn(), + destroy: vi.fn(), }, - getVersions: jest.fn().mockResolvedValue({ versions: ["v1.1"] }), - logout: jest.fn().mockResolvedValue(undefined), - getAccessToken: jest.fn(), - getRefreshToken: jest.fn(), - setGuest: jest.fn(), - setNotifTimelineSet: jest.fn(), + getVersions: vi.fn().mockResolvedValue({ versions: ["v1.1"] }), + logout: vi.fn().mockResolvedValue(undefined), + getAccessToken: vi.fn(), + getRefreshToken: vi.fn(), + isInitialSyncComplete: vi.fn(), + setGuest: vi.fn(), + setNotifTimelineSet: vi.fn(), }); // stub this - jest.spyOn(MatrixClientPeg, "set").mockImplementation(() => {}); - jest.spyOn(MatrixClientPeg, "start").mockResolvedValue(undefined); + vi.spyOn(MatrixClientPeg, "set").mockImplementation(() => {}); + vi.spyOn(MatrixClientPeg, "start").mockResolvedValue(undefined); - // reset any mocking - // @ts-ignore mocking - delete global.localStorage; - global.localStorage = realLocalStorage; + vi.spyOn(encryptAESSecretStorageItemModule, "default").mockRestore(); - // @ts-ignore mocking - delete window.crypto; - window.crypto = webCrypto; - - jest.spyOn(encryptAESSecretStorageItemModule, "default").mockRestore(); + localStorage.clear(); + sessionStorage.clear(); }); - afterAll(() => { - // @ts-ignore unmocking - delete window.crypto; - window.crypto = windowCrypto; + afterEach(() => { + vi.resetAllMocks(); }); const initIdbMock = (mockStore: Record> = {}): void => { - jest.spyOn(StorageAccess, "idbLoad") + vi.spyOn(StorageAccess, "idbLoad") .mockClear() .mockImplementation( // @ts-ignore mock type async (table: string, key: string) => mockStore[table]?.[key] ?? null, ); - jest.spyOn(StorageAccess, "idbSave") + vi.spyOn(StorageAccess, "idbSave") .mockClear() .mockImplementation( // @ts-ignore mock type @@ -114,13 +101,13 @@ describe("Lifecycle", () => { mockStore[tableKey] = table; }, ); - jest.spyOn(StorageAccess, "idbDelete") + vi.spyOn(StorageAccess, "idbDelete") .mockClear() .mockImplementation(async (tableKey: string, key: string | string[]) => { const table = mockStore[tableKey]; delete table?.[key as string]; }); - jest.spyOn(StorageAccess, "idbClear") + vi.spyOn(StorageAccess, "idbClear") .mockClear() .mockImplementation(async (tableKey: string) => { mockStore[tableKey] = {}; @@ -159,14 +146,14 @@ describe("Lifecycle", () => { describe("loadSession", () => { beforeEach(() => { // stub this out - jest.spyOn(Modal, "createDialog").mockReturnValue( + vi.spyOn(Modal, "createDialog").mockReturnValue( // @ts-ignore allow bad mock { finished: Promise.resolve([true]) }, ); }); it("should not show any error dialog when checkConsistency throws but abortSignal has triggered", async () => { - jest.spyOn(StorageManager, "checkConsistency").mockRejectedValue(new Error("test error")); + vi.spyOn(StorageManager, "checkConsistency").mockRejectedValue(new Error("test error")); const abortController = new AbortController(); const prom = Lifecycle.loadSession({ @@ -183,27 +170,29 @@ describe("Lifecycle", () => { }); describe("restoreSessionFromStorage()", () => { + const realLocalStorage = localStorage; + beforeEach(() => { initIdbMock(); - jest.clearAllMocks(); - jest.spyOn(logger, "log").mockClear(); + vi.spyOn(logger, "log").mockClear(); - jest.spyOn(MatrixJs, "createClient").mockReturnValue(mockClient); - jest.spyOn(createMatrixClientModule, "createClientWithCreds").mockReturnValue(mockClient); + vi.spyOn(MatrixJs, "createClient").mockReturnValue(mockClient); + vi.spyOn(createMatrixClientModule, "createClientWithCreds").mockReturnValue(mockClient); // stub this out - jest.spyOn(Modal, "createDialog").mockReturnValue( + vi.spyOn(Modal, "createDialog").mockReturnValue( // @ts-ignore allow bad mock { finished: Promise.resolve([true]) }, ); }); + afterEach(() => { + vi.stubGlobal("localStorage", realLocalStorage); + }); + it("should return false when localStorage is not available", async () => { - // @ts-ignore dirty mocking - delete global.localStorage; - // @ts-ignore dirty mocking - global.localStorage = undefined; + vi.stubGlobal("localStorage", undefined); expect(await restoreSessionFromStorage()).toEqual(false); }); @@ -272,7 +261,7 @@ describe("Lifecycle", () => { }); it("should persist access token when idb is not available", async () => { - jest.spyOn(StorageAccess, "idbSave").mockRejectedValue("oups"); + vi.spyOn(StorageAccess, "idbSave").mockRejectedValue("oups"); expect(await restoreSessionFromStorage()).toEqual(true); expect(StorageAccess.idbSave).toHaveBeenCalledWith("account", "mx_access_token", accessToken); @@ -384,7 +373,7 @@ describe("Lifecycle", () => { it("should persist access token when idb is not available", async () => { // dont fail for pickle key persist - jest.spyOn(StorageAccess, "idbSave").mockImplementation( + vi.spyOn(StorageAccess, "idbSave").mockImplementation( async (table: string, key: string | string[]) => { if (table === "account" && key === "mx_access_token") { throw new Error("oups"); @@ -406,7 +395,7 @@ describe("Lifecycle", () => { it("should create and start new matrix client with credentials", async () => { // Check that the rust crypto key is as expected. We have to do this during the call, as // the buffer is cleared afterwards. - mocked(MatrixClientPeg.start).mockImplementation(async (opts) => { + vi.mocked(MatrixClientPeg.start).mockImplementation(async (opts) => { expect(opts?.rustCryptoStoreKey).toEqual(decodeBase64(pickleKey)); }); @@ -559,14 +548,14 @@ describe("Lifecycle", () => { beforeEach(() => { initIdbMock(); - jest.clearAllMocks(); - jest.spyOn(logger, "log").mockClear(); + vi.clearAllMocks(); + vi.spyOn(logger, "log").mockClear(); - jest.spyOn(MatrixJs, "createClient").mockReturnValue(mockClient); + vi.spyOn(MatrixJs, "createClient").mockReturnValue(mockClient); // remove any mock implementations - jest.spyOn(mockPlatform, "createPickleKey").mockRestore(); + vi.spyOn(mockPlatform, "createPickleKey").mockRestore(); // but still spy and call through - jest.spyOn(mockPlatform, "createPickleKey"); + vi.spyOn(mockPlatform, "createPickleKey"); }); const refreshToken = "test-refresh-token"; @@ -617,8 +606,8 @@ describe("Lifecycle", () => { describe("without a pickle key", () => { beforeEach(() => { - jest.spyOn(mockPlatform, "createPickleKey").mockResolvedValue(null); - jest.spyOn(createMatrixClientModule, "createClientWithCreds").mockReturnValue(mockClient); + vi.spyOn(mockPlatform, "createPickleKey").mockResolvedValue(null); + vi.spyOn(createMatrixClientModule, "createClientWithCreds").mockReturnValue(mockClient); }); it("should persist credentials", async () => { @@ -650,7 +639,7 @@ describe("Lifecycle", () => { }); it("should remove any access token from storage when there is none in credentials and idb save fails", async () => { - jest.spyOn(StorageAccess, "idbSave").mockRejectedValue("oups"); + vi.spyOn(StorageAccess, "idbSave").mockRejectedValue("oups"); await setLoggedIn({ ...credentials, // @ts-ignore @@ -726,7 +715,7 @@ describe("Lifecycle", () => { }); it("should persist token when encrypting the token fails", async () => { - jest.spyOn(encryptAESSecretStorageItemModule, "default").mockRejectedValue("MOCK REJECT ENCRYPTAES"); + vi.spyOn(encryptAESSecretStorageItemModule, "default").mockRejectedValue("MOCK REJECT ENCRYPTAES"); await setLoggedIn(credentials); // persist the unencrypted token @@ -735,13 +724,11 @@ describe("Lifecycle", () => { it("should persist token in localStorage when idb fails to save token", async () => { // dont fail for pickle key persist - jest.spyOn(StorageAccess, "idbSave").mockImplementation( - async (table: string, key: string | string[]) => { - if (table === "account" && key === "mx_access_token") { - throw new Error("oups"); - } - }, - ); + vi.spyOn(StorageAccess, "idbSave").mockImplementation(async (table: string, key: string | string[]) => { + if (table === "account" && key === "mx_access_token") { + throw new Error("oups"); + } + }); await setLoggedIn(credentials); // put plain accessToken in localstorage when we dont have idb @@ -750,13 +737,11 @@ describe("Lifecycle", () => { it("should remove any access token from storage when there is none in credentials and idb save fails", async () => { // dont fail for pickle key persist - jest.spyOn(StorageAccess, "idbSave").mockImplementation( - async (table: string, key: string | string[]) => { - if (table === "account" && key === "mx_access_token") { - throw new Error("oups"); - } - }, - ); + vi.spyOn(StorageAccess, "idbSave").mockImplementation(async (table: string, key: string | string[]) => { + if (table === "account" && key === "mx_access_token") { + throw new Error("oups"); + } + }); await setLoggedIn({ ...credentials, // @ts-ignore @@ -768,7 +753,7 @@ describe("Lifecycle", () => { }); it("should create new matrix client with credentials", async () => { - jest.spyOn(createMatrixClientModule, "createClientWithCreds").mockReturnValue(mockClient); + vi.spyOn(createMatrixClientModule, "createClientWithCreds").mockReturnValue(mockClient); expect(await setLoggedIn(credentials)).toEqual(mockClient); expect(createMatrixClientModule.createClientWithCreds).toHaveBeenCalledWith( @@ -903,7 +888,7 @@ describe("Lifecycle", () => { beforeEach(() => { oidcClientStore = new OidcClientStore(mockClient); // stub - jest.spyOn(oidcClientStore, "revokeTokens").mockResolvedValue(undefined); + vi.spyOn(oidcClientStore, "revokeTokens").mockResolvedValue(undefined); mockClient.getAccessToken.mockReturnValue(accessToken); mockClient.getRefreshToken.mockReturnValue(refreshToken); @@ -918,7 +903,7 @@ describe("Lifecycle", () => { }); it("should call logout on the client when oidcClientStore.isUserAuthenticatedWithOidc is falsy", async () => { - jest.spyOn(oidcClientStore, "isUserAuthenticatedWithOidc", "get").mockReturnValue(false); + vi.spyOn(oidcClientStore, "isUserAuthenticatedWithOidc", "get").mockReturnValue(false); logout(oidcClientStore); await flushPromises(); @@ -928,7 +913,7 @@ describe("Lifecycle", () => { }); it("should revoke tokens when user is authenticated with oidc", async () => { - jest.spyOn(oidcClientStore, "isUserAuthenticatedWithOidc", "get").mockReturnValue(true); + vi.spyOn(oidcClientStore, "isUserAuthenticatedWithOidc", "get").mockReturnValue(true); logout(oidcClientStore); await flushPromises(); @@ -940,12 +925,12 @@ describe("Lifecycle", () => { describe("overwritelogin", () => { beforeEach(async () => { - jest.spyOn(MatrixJs, "createClient").mockReturnValue(mockClient); + vi.spyOn(MatrixJs, "createClient").mockReturnValue(mockClient); }); it("should replace the current login with a new one", async () => { - const stopSpy = jest.spyOn(mockClient, "stopClient").mockReturnValue(undefined); - jest.spyOn(createMatrixClientModule, "createClientWithCreds").mockReturnValue(mockClient); + const stopSpy = vi.spyOn(mockClient, "stopClient").mockReturnValue(undefined); + vi.spyOn(createMatrixClientModule, "createClientWithCreds").mockReturnValue(mockClient); const dis = window.mxDispatcher; const firstLoginEvent: Promise = new Promise((resolve) => { @@ -963,7 +948,7 @@ describe("Lifecycle", () => { expect(stopSpy).toHaveBeenCalledTimes(1); // important the overwrite action should not call unset before replacing. // So spy on it and make sure it's not called. - jest.spyOn(MatrixClientPeg, "unset").mockReturnValue(undefined); + vi.spyOn(MatrixClientPeg, "unset").mockReturnValue(undefined); expect(createMatrixClientModule.createClientWithCreds).toHaveBeenCalledWith( expect.objectContaining({ diff --git a/apps/web/test/unit-tests/Markdown-test.ts b/apps/web/src/Markdown.test.ts similarity index 99% rename from apps/web/test/unit-tests/Markdown-test.ts rename to apps/web/src/Markdown.test.ts index 4cfb165ea9..68dc00f9ba 100644 --- a/apps/web/test/unit-tests/Markdown-test.ts +++ b/apps/web/src/Markdown.test.ts @@ -6,7 +6,9 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import Markdown from "../../src/Markdown"; +import { describe, it, expect } from "vitest"; + +import Markdown from "./Markdown"; describe("Markdown parser test", () => { describe("fixing HTML links", () => { diff --git a/apps/web/test/unit-tests/PosthogAnalytics-test.ts b/apps/web/src/PosthogAnalytics.test.ts similarity index 79% rename from apps/web/test/unit-tests/PosthogAnalytics-test.ts rename to apps/web/src/PosthogAnalytics.test.ts index 08d62e1373..a70c91bbee 100644 --- a/apps/web/test/unit-tests/PosthogAnalytics-test.ts +++ b/apps/web/src/PosthogAnalytics.test.ts @@ -7,41 +7,38 @@ 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 } from "jest-mock"; +// @vitest-environment happy-dom + +import { vi, describe, it, expect, beforeEach, afterEach } from "vitest"; import { type PostHog } from "posthog-js"; import { type MatrixClient } from "matrix-js-sdk/src/matrix"; import { type CryptoApi } from "matrix-js-sdk/src/crypto-api"; +import { getMockClientWithEventEmitter } from "test-utils"; -import { - Anonymity, - getRedactedCurrentLocation, - type IPosthogEvent, - PosthogAnalytics, -} from "../../src/PosthogAnalytics"; -import SdkConfig from "../../src/SdkConfig"; -import { getMockClientWithEventEmitter } from "../test-utils"; -import SettingsStore from "../../src/settings/SettingsStore"; -import { Layout } from "../../src/settings/enums/Layout"; -import defaultDispatcher from "../../src/dispatcher/dispatcher"; -import { Action } from "../../src/dispatcher/actions"; -import { SettingLevel } from "../../src/settings/SettingLevel"; +import { Anonymity, getRedactedCurrentLocation, type IPosthogEvent, PosthogAnalytics } from "./PosthogAnalytics"; +import SdkConfig from "./SdkConfig"; +import SettingsStore from "./settings/SettingsStore"; +import { Layout } from "./settings/enums/Layout"; +import defaultDispatcher from "./dispatcher/dispatcher"; +import { Action } from "./dispatcher/actions"; +import { SettingLevel } from "./settings/SettingLevel"; const getFakePosthog = (): PostHog => ({ - capture: jest.fn(), - init: jest.fn(), - identify: jest.fn(), - reset: jest.fn(), - register: jest.fn(), - get_distinct_id: jest.fn(), + capture: vi.fn(), + init: vi.fn(), + identify: vi.fn(), + reset: vi.fn(), + register: vi.fn(), + get_distinct_id: vi.fn(), persistence: { - get_property: jest.fn(), + get_property: vi.fn(), }, - identifyUser: jest.fn(), + identifyUser: vi.fn(), }) as unknown as PostHog; interface ITestEvent extends IPosthogEvent { - eventName: "JestTestEvents"; + eventName: "TestEvents"; foo?: string; } @@ -121,17 +118,17 @@ describe("PosthogAnalytics", () => { it("Should pass event to posthog", () => { analytics.setAnonymity(Anonymity.Pseudonymous); analytics.trackEvent({ - eventName: "JestTestEvents", + eventName: "TestEvents", foo: "bar", }); - expect(mocked(fakePosthog).capture.mock.calls[0][0]).toBe("JestTestEvents"); - expect(mocked(fakePosthog).capture.mock.calls[0][1]!["foo"]).toEqual("bar"); + expect(vi.mocked(fakePosthog).capture.mock.calls[0][0]).toBe("TestEvents"); + expect(vi.mocked(fakePosthog).capture.mock.calls[0][1]!["foo"]).toEqual("bar"); }); it("Should not track events if anonymous", async () => { analytics.setAnonymity(Anonymity.Anonymous); await analytics.trackEvent({ - eventName: "JestTestEvents", + eventName: "TestEvents", foo: "bar", }); expect(fakePosthog.capture).not.toHaveBeenCalled(); @@ -140,7 +137,7 @@ describe("PosthogAnalytics", () => { it("Should not track any events if disabled", async () => { analytics.setAnonymity(Anonymity.Disabled); analytics.trackEvent({ - eventName: "JestTestEvents", + eventName: "TestEvents", foo: "bar", }); expect(fakePosthog.capture).not.toHaveBeenCalled(); @@ -164,29 +161,29 @@ describe("PosthogAnalytics", () => { it("Should identify the user to posthog if pseudonymous", async () => { analytics.setAnonymity(Anonymity.Pseudonymous); const client = getMockClientWithEventEmitter({ - getAccountDataFromServer: jest.fn().mockResolvedValue(null), - setAccountData: jest.fn().mockResolvedValue({}), + getAccountDataFromServer: vi.fn().mockResolvedValue(null), + setAccountData: vi.fn().mockResolvedValue({}), }); await analytics.identifyUser(client, () => "analytics_id"); - expect(mocked(fakePosthog).identify.mock.calls[0][0]).toBe("analytics_id"); + expect(vi.mocked(fakePosthog).identify.mock.calls[0][0]).toBe("analytics_id"); }); it("Should not identify the user to posthog if anonymous", async () => { analytics.setAnonymity(Anonymity.Anonymous); const client = getMockClientWithEventEmitter({}); await analytics.identifyUser(client, () => "analytics_id"); - expect(mocked(fakePosthog).identify.mock.calls.length).toBe(0); + expect(vi.mocked(fakePosthog).identify.mock.calls.length).toBe(0); }); it("Should identify using the server's analytics id if present", async () => { analytics.setAnonymity(Anonymity.Pseudonymous); const client = getMockClientWithEventEmitter({ - getAccountDataFromServer: jest.fn().mockResolvedValue({ id: "existing_analytics_id" }), - setAccountData: jest.fn().mockResolvedValue({}), + getAccountDataFromServer: vi.fn().mockResolvedValue({ id: "existing_analytics_id" }), + setAccountData: vi.fn().mockResolvedValue({}), }); await analytics.identifyUser(client, () => "new_analytics_id"); - expect(mocked(fakePosthog).identify.mock.calls[0][0]).toBe("existing_analytics_id"); + expect(vi.mocked(fakePosthog).identify.mock.calls[0][0]).toBe("existing_analytics_id"); }); }); @@ -220,9 +217,9 @@ describe("PosthogAnalytics", () => { true, ); analytics.trackEvent({ - eventName: "JestTestEvents", + eventName: "TestEvents", }); - expect(mocked(fakePosthog).capture.mock.calls[0][1]!["$set"]).toMatchObject({ + expect(vi.mocked(fakePosthog).capture.mock.calls[0][1]!["$set"]).toMatchObject({ WebLayout: "IRC", }); }); @@ -237,9 +234,9 @@ describe("PosthogAnalytics", () => { true, ); analytics.trackEvent({ - eventName: "JestTestEvents", + eventName: "TestEvents", }); - expect(mocked(fakePosthog).capture.mock.calls[0][1]!["$set"]).toMatchObject({ + expect(vi.mocked(fakePosthog).capture.mock.calls[0][1]!["$set"]).toMatchObject({ WebLayout: "Bubble", }); }); @@ -254,9 +251,9 @@ describe("PosthogAnalytics", () => { true, ); analytics.trackEvent({ - eventName: "JestTestEvents", + eventName: "TestEvents", }); - expect(mocked(fakePosthog).capture.mock.calls[0][1]!["$set"]).toMatchObject({ + expect(vi.mocked(fakePosthog).capture.mock.calls[0][1]!["$set"]).toMatchObject({ WebLayout: "Group", }); }); @@ -272,10 +269,10 @@ describe("PosthogAnalytics", () => { true, ); analytics.trackEvent({ - eventName: "JestTestEvents", + eventName: "TestEvents", }); - console.log(mocked(fakePosthog).capture.mock.calls[0]); - expect(mocked(fakePosthog).capture.mock.calls[0][1]!["$set"]).toMatchObject({ + console.log(vi.mocked(fakePosthog).capture.mock.calls[0]); + expect(vi.mocked(fakePosthog).capture.mock.calls[0][1]!["$set"]).toMatchObject({ WebLayout: "Compact", }); }); @@ -311,9 +308,9 @@ describe("PosthogAnalytics", () => { true, ); analytics.trackEvent({ - eventName: "JestTestEvents", + eventName: "TestEvents", }); - expect(mocked(fakePosthog).capture.mock.calls[0][1]!["$set"]).toMatchObject({ + expect(vi.mocked(fakePosthog).capture.mock.calls[0][1]!["$set"]).toMatchObject({ URLPreviewsEnabled: true, }); }); @@ -323,10 +320,10 @@ describe("PosthogAnalytics", () => { let analytics: PosthogAnalytics; const getFakeClient = (): MatrixClient => ({ - getCrypto: jest.fn(), - setAccountData: jest.fn(), + getCrypto: vi.fn(), + setAccountData: vi.fn(), // just fake return an `im.vector.analytics` content - getAccountDataFromServer: jest.fn().mockReturnValue({ + getAccountDataFromServer: vi.fn().mockReturnValue({ id: "0000000", pseudonymousAnalyticsOptIn: true, }), @@ -350,7 +347,7 @@ describe("PosthogAnalytics", () => { // To simulate a switch we call updateAnonymityFromSettings. // As per documentation this function is called On login. const mockClient = getFakeClient(); - mocked(mockClient.getCrypto).mockReturnValue({ + vi.mocked(mockClient.getCrypto).mockReturnValue({ getVersion: () => { return rustBackend ? "Rust SDK 0.6.0 (9c6b550), Vodozemac 0.5.0" : "Olm 3.2.0"; }, @@ -363,7 +360,7 @@ describe("PosthogAnalytics", () => { await simulateLogin(false); - expect(mocked(fakePosthog).register.mock.lastCall![0]["cryptoSDK"]).toStrictEqual("Legacy"); + expect(vi.mocked(fakePosthog).register.mock.lastCall![0]["cryptoSDK"]).toStrictEqual("Legacy"); }); it("should send Legacy cryptoSDK superProperty correctly", async () => { @@ -373,7 +370,7 @@ describe("PosthogAnalytics", () => { // Super Properties are properties associated with events that are set once and then sent with every capture call. // They are set using posthog.register - expect(mocked(fakePosthog).register.mock.lastCall![0]["cryptoSDK"]).toStrictEqual("Legacy"); + expect(vi.mocked(fakePosthog).register.mock.lastCall![0]["cryptoSDK"]).toStrictEqual("Legacy"); }); it("should send cryptoSDK superProperty when enabling analytics", async () => { @@ -383,13 +380,13 @@ describe("PosthogAnalytics", () => { // This initial call is due to the call to register platformSuperProperties // The important thing is that the cryptoSDK superProperty is not set. - expect(mocked(fakePosthog).register.mock.lastCall![0]).toStrictEqual({}); + expect(vi.mocked(fakePosthog).register.mock.lastCall![0]).toStrictEqual({}); // switching to pseudonymous should ensure that the cryptoSDK superProperty is set correctly analytics.setAnonymity(Anonymity.Pseudonymous); // Super Properties are properties associated with events that are set once and then sent with every capture call. // They are set using posthog.register - expect(mocked(fakePosthog).register.mock.lastCall![0]["cryptoSDK"]).toStrictEqual("Rust"); + expect(vi.mocked(fakePosthog).register.mock.lastCall![0]["cryptoSDK"]).toStrictEqual("Rust"); }); }); }); diff --git a/apps/web/test/unit-tests/PosthogTrackers-test.ts b/apps/web/src/PosthogTrackers.test.ts similarity index 76% rename from apps/web/test/unit-tests/PosthogTrackers-test.ts rename to apps/web/src/PosthogTrackers.test.ts index a8904e4080..846fc57084 100644 --- a/apps/web/test/unit-tests/PosthogTrackers-test.ts +++ b/apps/web/src/PosthogTrackers.test.ts @@ -5,16 +5,20 @@ 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 { PosthogAnalytics } from "../../src/PosthogAnalytics"; -import PosthogTrackers from "../../src/PosthogTrackers"; +// @vitest-environment happy-dom + +import { vi, describe, it, expect, afterEach } from "vitest"; + +import { PosthogAnalytics } from "./PosthogAnalytics"; +import PosthogTrackers from "./PosthogTrackers"; describe("PosthogTrackers", () => { afterEach(() => { - jest.resetAllMocks(); + vi.resetAllMocks(); }); it("tracks URL Previews", () => { - jest.spyOn(PosthogAnalytics.instance, "trackEvent"); + vi.spyOn(PosthogAnalytics.instance, "trackEvent"); const tracker = new PosthogTrackers(); tracker.trackUrlPreview("$123456", false, [ { diff --git a/apps/web/test/unit-tests/RoomNotifs-test.ts b/apps/web/src/RoomNotifs.test.ts similarity index 94% rename from apps/web/test/unit-tests/RoomNotifs-test.ts rename to apps/web/src/RoomNotifs.test.ts index e514288a6c..a94f8435d4 100644 --- a/apps/web/test/unit-tests/RoomNotifs-test.ts +++ b/apps/web/src/RoomNotifs.test.ts @@ -6,7 +6,9 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import { mocked } from "jest-mock"; +// @vitest-environment happy-dom + +import { vi, describe, it, expect, beforeEach, type Mocked } from "vitest"; import { PushRuleActionName, TweakName, @@ -19,19 +21,19 @@ import { type MatrixClient, } from "matrix-js-sdk/src/matrix"; import { KnownMembership } from "matrix-js-sdk/src/types"; +import { mkEvent, mkRoom, mkRoomMember, muteRoom, stubClient, upsertRoomStateEvents } from "test-utils"; +import { mkThread } from "test-utils/threads"; -import { mkEvent, mkRoom, mkRoomMember, muteRoom, stubClient, upsertRoomStateEvents } from "../test-utils"; import { getRoomNotifsState, RoomNotifState, getUnreadNotificationCount, determineUnreadState, getUnsentMessages, -} from "../../src/RoomNotifs"; -import { NotificationLevel } from "../../src/stores/notifications/NotificationLevel"; -import SettingsStore from "../../src/settings/SettingsStore"; -import { MatrixClientPeg } from "../../src/MatrixClientPeg"; -import { mkThread } from "../test-utils/threads"; +} from "./RoomNotifs"; +import { NotificationLevel } from "./stores/notifications/NotificationLevel"; +import SettingsStore from "./settings/SettingsStore"; +import { MatrixClientPeg } from "./MatrixClientPeg"; describe("getUnsentMessages", () => { const ROOM_ID = "!roomId"; @@ -95,14 +97,14 @@ describe("getUnsentMessages", () => { }); describe("RoomNotifs test", () => { - let client: jest.Mocked; + let client: Mocked; beforeEach(() => { - client = stubClient() as jest.Mocked; + client = stubClient() as Mocked; }); it("getRoomNotifsState handles rules with no conditions", () => { - mocked(client).pushRules = { + vi.mocked(client).pushRules = { global: { override: [ { @@ -118,7 +120,7 @@ describe("RoomNotifs test", () => { }); it("getRoomNotifsState handles guest users", () => { - mocked(client).isGuest.mockReturnValue(true); + vi.mocked(client).isGuest.mockReturnValue(true); expect(getRoomNotifsState(client, "!roomId:server")).toBe(RoomNotifState.AllMessages); }); @@ -258,7 +260,7 @@ describe("RoomNotifs test", () => { describe("and dynamic room predecessors are enabled", () => { beforeEach(() => { - jest.spyOn(SettingsStore, "getValue").mockImplementation( + vi.spyOn(SettingsStore, "getValue").mockImplementation( (settingName) => settingName === "feature_dynamic_room_predecessors", ); }); @@ -343,7 +345,7 @@ describe("RoomNotifs test", () => { }); it("indicates the user knock has been denied", async () => { - jest.spyOn(SettingsStore, "getValue").mockImplementation((name) => { + vi.spyOn(SettingsStore, "getValue").mockImplementation((name) => { return name === "feature_ask_to_join"; }); const roomMember = mkRoomMember( @@ -355,7 +357,7 @@ describe("RoomNotifs test", () => { membership: KnownMembership.Knock, }, ); - jest.spyOn(room, "getMember").mockReturnValue(roomMember); + vi.spyOn(room, "getMember").mockReturnValue(roomMember); const { level, symbol, count } = determineUnreadState(room); expect(symbol).toBe("!"); diff --git a/apps/web/test/unit-tests/Rooms-test.ts b/apps/web/src/Rooms.test.ts similarity index 86% rename from apps/web/test/unit-tests/Rooms-test.ts rename to apps/web/src/Rooms.test.ts index 841b6f3722..54984858d7 100644 --- a/apps/web/test/unit-tests/Rooms-test.ts +++ b/apps/web/src/Rooms.test.ts @@ -6,11 +6,13 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import { mocked } from "jest-mock"; -import { EventType, type MatrixClient, type MatrixEvent } from "matrix-js-sdk/src/matrix"; +// @vitest-environment happy-dom -import { setDMRoom } from "../../src/Rooms"; -import { mkEvent, stubClient } from "../test-utils"; +import { vi, describe, it, expect, beforeEach } from "vitest"; +import { EventType, type MatrixClient, type MatrixEvent } from "matrix-js-sdk/src/matrix"; +import { mkEvent, stubClient } from "test-utils"; + +import { setDMRoom } from "./Rooms"; describe("setDMRoom", () => { const userId1 = "@user1:example.com"; @@ -23,8 +25,8 @@ describe("setDMRoom", () => { let client: MatrixClient; beforeEach(() => { - client = mocked(stubClient()); - client.getAccountData = jest.fn().mockImplementation((eventType: string): MatrixEvent | undefined => { + client = vi.mocked(stubClient()); + client.getAccountData = vi.fn().mockImplementation((eventType: string): MatrixEvent | undefined => { if (eventType === EventType.Direct) { return mkEvent({ event: true, @@ -43,7 +45,7 @@ describe("setDMRoom", () => { describe("when logged in as a guest and marking a room as DM", () => { beforeEach(() => { - mocked(client.isGuest).mockReturnValue(true); + vi.mocked(client.isGuest).mockReturnValue(true); setDMRoom(client, roomId1, userId1); }); @@ -94,7 +96,7 @@ describe("setDMRoom", () => { describe("when the direct event is undefined", () => { beforeEach(() => { - mocked(client.getAccountData).mockReturnValue(undefined); + vi.mocked(client.getAccountData).mockReturnValue(undefined); setDMRoom(client, roomId1, userId1); }); @@ -108,8 +110,8 @@ describe("setDMRoom", () => { describe("when the current content is undefined", () => { beforeEach(() => { // @ts-ignore - mocked(client.getAccountData).mockReturnValue({ - getContent: jest.fn(), + vi.mocked(client.getAccountData).mockReturnValue({ + getContent: vi.fn(), }); setDMRoom(client, roomId1, userId1); }); diff --git a/apps/web/test/unit-tests/ScalarAuthClient-test.ts b/apps/web/src/ScalarAuthClient.test.ts similarity index 89% rename from apps/web/test/unit-tests/ScalarAuthClient-test.ts rename to apps/web/src/ScalarAuthClient.test.ts index 0ed4184b55..9d7bb0b7fe 100644 --- a/apps/web/test/unit-tests/ScalarAuthClient-test.ts +++ b/apps/web/src/ScalarAuthClient.test.ts @@ -6,14 +6,16 @@ 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 } from "jest-mock"; -import fetchMock from "@fetch-mock/jest"; -import { type MatrixClient } from "matrix-js-sdk/src/matrix"; +// @vitest-environment happy-dom -import ScalarAuthClient from "../../src/ScalarAuthClient"; -import { stubClient } from "../test-utils"; -import SdkConfig from "../../src/SdkConfig"; -import { WidgetType } from "../../src/widgets/WidgetType"; +import { vi, describe, it, expect, beforeEach } from "vitest"; +import fetchMock from "@fetch-mock/vitest"; +import { type MatrixClient } from "matrix-js-sdk/src/matrix"; +import { stubClient } from "test-utils"; + +import ScalarAuthClient from "./ScalarAuthClient"; +import SdkConfig from "./SdkConfig"; +import { WidgetType } from "./widgets/WidgetType"; describe("ScalarAuthClient", function () { const apiUrl = "https://test.com/api"; @@ -27,7 +29,7 @@ describe("ScalarAuthClient", function () { let client: MatrixClient; beforeEach(function () { - jest.clearAllMocks(); + vi.clearAllMocks(); client = stubClient(); }); @@ -42,9 +44,9 @@ describe("ScalarAuthClient", function () { body: { user_id: client.getUserId() }, }); - client.getOpenIdToken = jest.fn().mockResolvedValue(tokenObject); + client.getOpenIdToken = vi.fn().mockResolvedValue(tokenObject); - sac.exchangeForScalarToken = jest.fn((arg) => { + sac.exchangeForScalarToken = vi.fn((arg) => { return Promise.resolve(arg === tokenObject ? "wokentoken" : "othertoken"); }); @@ -91,13 +93,13 @@ describe("ScalarAuthClient", function () { describe("registerForToken", () => { it("should call `termsInteractionCallback` upon M_TERMS_NOT_SIGNED error", async () => { const sac = new ScalarAuthClient(apiUrl + 4, uiUrl); - const termsInteractionCallback = jest.fn(); + const termsInteractionCallback = vi.fn(); sac.setTermsInteractionCallback(termsInteractionCallback); fetchMock.get("https://test.com/api4/account?scalar_token=testtoken1&v=1.1", { body: { errcode: "M_TERMS_NOT_SIGNED" }, }); - sac.exchangeForScalarToken = jest.fn(() => Promise.resolve("testtoken1")); - mocked(client.getTerms).mockResolvedValue({ policies: {} }); + sac.exchangeForScalarToken = vi.fn(() => Promise.resolve("testtoken1")); + vi.mocked(client.getTerms).mockResolvedValue({ policies: {} }); await expect(sac.registerForToken()).resolves.toBe("testtoken1"); }); @@ -108,7 +110,7 @@ describe("ScalarAuthClient", function () { body: { errcode: "SERVER_IS_SAD" }, status: 500, }); - sac.exchangeForScalarToken = jest.fn(() => Promise.resolve("testtoken2")); + sac.exchangeForScalarToken = vi.fn(() => Promise.resolve("testtoken2")); await expect(sac.registerForToken()).rejects.toBeTruthy(); }); @@ -118,7 +120,7 @@ describe("ScalarAuthClient", function () { fetchMock.get("https://test.com/api6/account?scalar_token=testtoken3&v=1.1", { body: {}, }); - sac.exchangeForScalarToken = jest.fn(() => Promise.resolve("testtoken3")); + sac.exchangeForScalarToken = vi.fn(() => Promise.resolve("testtoken3")); await expect(sac.registerForToken()).rejects.toThrow("Missing user_id in response"); }); diff --git a/apps/web/test/unit-tests/SdkConfig-test.ts b/apps/web/src/SdkConfig.test.ts similarity index 92% rename from apps/web/test/unit-tests/SdkConfig-test.ts rename to apps/web/src/SdkConfig.test.ts index 5595a7d584..91a7168b78 100644 --- a/apps/web/test/unit-tests/SdkConfig-test.ts +++ b/apps/web/src/SdkConfig.test.ts @@ -6,7 +6,9 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import SdkConfig, { DEFAULTS } from "../../src/SdkConfig"; +import { describe, it, expect, beforeEach } from "vitest"; + +import SdkConfig, { DEFAULTS } from "./SdkConfig"; describe("SdkConfig", () => { describe("with default values", () => { diff --git a/apps/web/test/unit-tests/SupportedBrowser-test.ts b/apps/web/src/SupportedBrowser.test.ts similarity index 90% rename from apps/web/test/unit-tests/SupportedBrowser-test.ts rename to apps/web/src/SupportedBrowser.test.ts index 40b8b9c6a1..f8b8a9c024 100644 --- a/apps/web/test/unit-tests/SupportedBrowser-test.ts +++ b/apps/web/src/SupportedBrowser.test.ts @@ -6,17 +6,20 @@ 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 } from "vitest"; import { logger } from "matrix-js-sdk/src/logger"; -import { getBrowserSupport, checkBrowserSupport, LOCAL_STORAGE_KEY } from "../../src/SupportedBrowser"; -import ToastStore from "../../src/stores/ToastStore"; -import GenericToast from "../../src/components/views/toasts/GenericToast"; +import { getBrowserSupport, checkBrowserSupport, LOCAL_STORAGE_KEY } from "./SupportedBrowser"; +import ToastStore from "./stores/ToastStore"; +import GenericToast from "./components/views/toasts/GenericToast"; -jest.mock("matrix-js-sdk/src/logger"); +vi.mock("matrix-js-sdk/src/logger"); describe("SupportedBrowser", () => { beforeEach(() => { - jest.resetAllMocks(); + vi.resetAllMocks(); localStorage.clear(); getBrowserSupport.clear(); }); @@ -24,8 +27,8 @@ describe("SupportedBrowser", () => { const testUserAgentFactory = (expectedWarning?: string) => async (userAgent: string): Promise => { - const toastSpy = jest.spyOn(ToastStore.sharedInstance(), "addOrReplaceToast"); - const warnLogSpy = jest.spyOn(logger, "warn"); + const toastSpy = vi.spyOn(ToastStore.sharedInstance(), "addOrReplaceToast"); + const warnLogSpy = vi.spyOn(logger, "warn"); Object.defineProperty(window, "navigator", { value: { userAgent: userAgent }, writable: true }); checkBrowserSupport(); if (expectedWarning) { @@ -91,8 +94,8 @@ describe("SupportedBrowser", () => { ); it("should not warn for unsupported browser if user accepted already", async () => { - const toastSpy = jest.spyOn(ToastStore.sharedInstance(), "addOrReplaceToast"); - const warnLogSpy = jest.spyOn(logger, "warn"); + const toastSpy = vi.spyOn(ToastStore.sharedInstance(), "addOrReplaceToast"); + const warnLogSpy = vi.spyOn(logger, "warn"); const userAgent = "Mozilla/5.0 (X11; CrOS x86_64 15633.69.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.6045.212 Safari/537.36"; Object.defineProperty(window, "navigator", { value: { userAgent: userAgent }, writable: true }); diff --git a/apps/web/test/unit-tests/TimezoneHandler-test.ts b/apps/web/src/TimezoneHandler.test.ts similarity index 85% rename from apps/web/test/unit-tests/TimezoneHandler-test.ts rename to apps/web/src/TimezoneHandler.test.ts index be7628712a..dcce6ff11b 100644 --- a/apps/web/test/unit-tests/TimezoneHandler-test.ts +++ b/apps/web/src/TimezoneHandler.test.ts @@ -8,7 +8,11 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import * as tzh from "../../src/TimezoneHandler"; +// @vitest-environment happy-dom + +import { describe, it, expect } from "vitest"; + +import * as tzh from "./TimezoneHandler"; describe("TimezoneHandler", () => { it("should support setting a user timezone", async () => { diff --git a/apps/web/test/unit-tests/__snapshots__/favicon-test.ts.snap b/apps/web/src/__snapshots__/favicon.test.ts.snap similarity index 96% rename from apps/web/test/unit-tests/__snapshots__/favicon-test.ts.snap rename to apps/web/src/__snapshots__/favicon.test.ts.snap index f4c22fe3e9..827a17cbfa 100644 --- a/apps/web/test/unit-tests/__snapshots__/favicon-test.ts.snap +++ b/apps/web/src/__snapshots__/favicon.test.ts.snap @@ -1,6 +1,6 @@ -// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`Favicon should clear a badge if called with a zero value 1`] = ` +exports[`Favicon > should clear a badge if called with a zero value 1`] = ` [ { "props": { @@ -236,7 +236,7 @@ exports[`Favicon should clear a badge if called with a zero value 1`] = ` ] `; -exports[`Favicon should draw a badge if called with a non-zero value 1`] = ` +exports[`Favicon > should draw a badge if called with a non-zero value 1`] = ` [ { "props": { diff --git a/apps/web/test/unit-tests/favicon-test.ts b/apps/web/src/favicon.test.ts similarity index 84% rename from apps/web/test/unit-tests/favicon-test.ts rename to apps/web/src/favicon.test.ts index 32c1ea9966..7208e4debb 100644 --- a/apps/web/test/unit-tests/favicon-test.ts +++ b/apps/web/src/favicon.test.ts @@ -6,15 +6,18 @@ 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 "jest-canvas-mock"; +// @vitest-environment happy-dom -import Favicon from "../../src/favicon"; +import { vi, describe, it, expect, beforeEach } from "vitest"; +import "vitest-canvas-mock"; -jest.useFakeTimers(); +import Favicon from "./favicon"; + +vi.useFakeTimers(); describe("Favicon", () => { beforeEach(() => { - jest.restoreAllMocks(); + vi.restoreAllMocks(); document.getElementsByTagName("head")[0]?.remove(); const head = document.createElement("head"); window.document.documentElement.prepend(head); @@ -30,14 +33,14 @@ describe("Favicon", () => { it("should draw a badge if called with a non-zero value", () => { const favicon = new Favicon(); favicon.badge(123); - jest.runAllTimers(); + vi.runAllTimers(); expect(favicon["context"].__getDrawCalls()).toMatchSnapshot(); }); it("should clear a badge if called with a zero value", () => { const favicon = new Favicon(); favicon.badge(123); - jest.runAllTimers(); + vi.runAllTimers(); favicon.badge(0); expect(favicon["context"].__getDrawCalls()).toMatchSnapshot(); }); @@ -48,7 +51,7 @@ describe("Favicon", () => { const favicon = new Favicon(); const originalLink = window.document.querySelector("link"); favicon.badge(123); - jest.runAllTimers(); + vi.runAllTimers(); const newLink = window.document.querySelector("link"); expect(originalLink).not.toStrictEqual(newLink); }); @@ -60,9 +63,9 @@ describe("Favicon", () => { link.href = "favicon.png"; head.appendChild(link); - const spy = jest.spyOn(document, "createElement"); + const spy = vi.spyOn(document, "createElement"); const favicon = new Favicon(); - jest.runAllTimers(); + vi.runAllTimers(); const img = spy.mock.results[0].value; img.onload(); @@ -78,9 +81,9 @@ describe("Favicon", () => { link.href = "favicon.png"; head.appendChild(link); - const spy = jest.spyOn(document, "createElement"); + const spy = vi.spyOn(document, "createElement"); const favicon = new Favicon(); - jest.runAllTimers(); + vi.runAllTimers(); const img = spy.mock.results[0].value; img.height = 512; diff --git a/apps/web/test/setup/adapter.ts b/apps/web/test/setup/adapter.ts index 40b6ea9328..4ec437a5af 100644 --- a/apps/web/test/setup/adapter.ts +++ b/apps/web/test/setup/adapter.ts @@ -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 } from "vitest"; +import { vi, expect as viExpect } from "vitest"; import { mocked as jestMocked } from "jest-mock"; export const isJest = typeof jest !== "undefined"; @@ -22,4 +22,7 @@ const adapter = { const mocked = adapter.mocked; export { adapter as vi, mocked }; +const _expect = isJest ? (expect as unknown as typeof viExpect) : viExpect; +export { _expect as expect }; + export { type Mocked, type MockedObject } from "vitest"; diff --git a/apps/web/test/test-utils/threads.ts b/apps/web/test/test-utils/threads.ts index 21d3543325..5767318924 100644 --- a/apps/web/test/test-utils/threads.ts +++ b/apps/web/test/test-utils/threads.ts @@ -16,6 +16,7 @@ import { } from "matrix-js-sdk/src/matrix"; import { mkMessage, type MessageEventProps } from "./test-utils"; +import { expect } from "../setup/adapter.ts"; export const makeThreadEvent = ({ rootEventId, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f0fa71a38a..0f19af20ed 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1067,6 +1067,9 @@ importers: vitest: specifier: 'catalog:' version: 4.1.9(@opentelemetry/api@1.9.1)(@types/node@25.9.3)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(happy-dom@20.10.6)(jsdom@26.1.0(patch_hash=040623e87b1c8b676c2a705513c0276c0704dd1b23fc3a1bb77cde8128b64b5f))(vite@8.1.3(@types/node@25.9.3)(esbuild@0.27.4)(jiti@2.7.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.48.0)(yaml@2.8.4)) + vitest-canvas-mock: + specifier: ^1.1.4 + version: 1.1.4(vitest@4.1.9) web-streams-polyfill: specifier: ^4.0.0 version: 4.3.0 @@ -13675,6 +13678,11 @@ packages: postcss: optional: true + vitest-canvas-mock@1.1.4: + resolution: {integrity: sha512-4boWHY+STwAxGl1+uwakNNoQky5EjPLC8HuponXNoAscYyT1h/F7RUvTkl4IyF/MiWr3V8Q626je3Iel3eArqA==} + peerDependencies: + vitest: ^3.0.0 || ^4.0.0 + vitest-plugin-vis@5.1.1: resolution: {integrity: sha512-A/MEvQhDpNAj/5b53GiN139glgWUe0Qh5cDupm25G+06FA/2K85bd3tK+Hi4fTaolNbL5WiLNgnQ8dOL+kC7Pw==} peerDependencies: @@ -27960,6 +27968,12 @@ snapshots: - typescript - universal-cookie + vitest-canvas-mock@1.1.4(vitest@4.1.9): + dependencies: + cssfontparser: 1.2.1 + moo-color: 1.0.3 + vitest: 4.1.9(@opentelemetry/api@1.9.1)(@types/node@25.9.3)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(happy-dom@20.10.6)(jsdom@26.1.0(patch_hash=040623e87b1c8b676c2a705513c0276c0704dd1b23fc3a1bb77cde8128b64b5f))(vite@8.1.3(@types/node@25.9.3)(esbuild@0.27.4)(jiti@2.7.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.48.0)(yaml@2.8.4)) + vitest-plugin-vis@5.1.1(@vitest/browser-playwright@4.1.9)(@vitest/browser@4.1.9)(babel-plugin-macros@3.1.0)(typescript@6.0.3)(vitest@4.1.9): dependencies: '@vitest/browser': 4.1.9(vite@8.1.3(@types/node@25.9.3)(esbuild@0.27.4)(jiti@2.7.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.48.0)(yaml@2.8.4))(vitest@4.1.9)