diff --git a/apps/web/__mocks__/workerFactoryMock.js b/apps/web/__mocks__/workerFactoryMock.js index 34ff72b12e..e637f835c2 100644 --- a/apps/web/__mocks__/workerFactoryMock.js +++ b/apps/web/__mocks__/workerFactoryMock.js @@ -9,5 +9,7 @@ Please see LICENSE files in the repository root for full details. import { vi } from "vitest"; export default function workerFactory(options) { - return vi.fn; + return { + postMessage: vi.fn(), + }; } diff --git a/apps/web/src/DecryptionFailureTracker.ts b/apps/web/src/DecryptionFailureTracker.ts index 8725a0db0d..279622fe86 100644 --- a/apps/web/src/DecryptionFailureTracker.ts +++ b/apps/web/src/DecryptionFailureTracker.ts @@ -365,7 +365,7 @@ export class DecryptionFailureTracker { /** * Clear state and stop checking for and tracking failures. */ - private stop(): void { + public stop(): void { if (this.checkInterval) clearInterval(this.checkInterval); if (this.trackInterval) clearInterval(this.trackInterval); diff --git a/apps/web/src/Lifecycle.ts b/apps/web/src/Lifecycle.ts index 4a9e5e9bf0..e282483e5f 100644 --- a/apps/web/src/Lifecycle.ts +++ b/apps/web/src/Lifecycle.ts @@ -955,8 +955,8 @@ export async function logout(): Promise { homeserverUrl: client.getHomeserverUrl(), deviceId: client.getDeviceId()!, }); - } catch (e) { - console.error("@@", e); + } catch { + // This is fine } PosthogAnalytics.instance.logout(); diff --git a/apps/web/test/unit-tests/components/structures/MatrixChat-test.tsx b/apps/web/src/components/structures/MatrixChat.test.tsx similarity index 80% rename from apps/web/test/unit-tests/components/structures/MatrixChat-test.tsx rename to apps/web/src/components/structures/MatrixChat.test.tsx index 5dbaef7103..9629f2ab8d 100644 --- a/apps/web/test/unit-tests/components/structures/MatrixChat-test.tsx +++ b/apps/web/src/components/structures/MatrixChat.test.tsx @@ -6,9 +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 { vi, describe, it, expect, beforeEach, afterEach, type Mocked } from "vitest"; import React, { type ComponentProps, createRef, type RefObject } from "react"; -import { fireEvent, render, type RenderResult, screen, waitFor, within, act } from "jest-matrix-react"; -import { type Mocked, mocked } from "jest-mock-vitest-adapter"; +import { cleanup, fireEvent, render, type RenderResult, screen, waitFor, within, act } from "test-utils-rtl"; import { ClientEvent, type MatrixClient, @@ -29,14 +31,8 @@ import { UserVerificationStatus, type CryptoApi, } from "matrix-js-sdk/src/crypto-api"; -import fetchMock from "@fetch-mock/jest"; +import fetchMock from "@fetch-mock/vitest"; import * as qrLogin from "matrix-js-sdk/src/rendezvous"; - -import MatrixChat from "../../../../src/components/structures/MatrixChat"; -import * as StorageAccess from "../../../../src/utils/StorageAccess"; -import defaultDispatcher from "../../../../src/dispatcher/dispatcher"; -import { Action } from "../../../../src/dispatcher/actions"; -import { UserTab } from "../../../../src/components/views/dialogs/UserTab"; import { clearAllModals, createStubMatrixRTC, @@ -49,65 +45,83 @@ import { mockPlatformPeg, resetJsDomAfterEach, unmockClientPeg, -} from "../../../test-utils"; -import * as leaveRoomUtils from "../../../../src/utils/leave-behaviour"; -import { OAuthClientError } from "../../../../src/utils/oauth/error"; -import { CallStore } from "../../../../src/stores/CallStore"; -import { type Call } from "../../../../src/models/Call"; -import { PosthogAnalytics } from "../../../../src/PosthogAnalytics"; -import PlatformPeg from "../../../../src/PlatformPeg"; -import EventIndexPeg from "../../../../src/indexing/EventIndexPeg"; -import * as Lifecycle from "../../../../src/Lifecycle"; -import { SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY } from "../../../../src/BasePlatform"; -import SettingsStore from "../../../../src/settings/SettingsStore"; -import { SettingLevel } from "../../../../src/settings/SettingLevel"; -import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; -import DMRoomMap from "../../../../src/utils/DMRoomMap"; -import { ReleaseAnnouncementStore } from "../../../../src/stores/ReleaseAnnouncementStore"; -import { DRAFT_LAST_CLEANUP_KEY } from "../../../../src/DraftCleaner"; -import { UIFeature } from "../../../../src/settings/UIFeature"; -import AutoDiscoveryUtils from "../../../../src/utils/AutoDiscoveryUtils"; -import { type ValidatedServerConfig } from "../../../../src/utils/ValidatedServerConfig"; -import Modal from "../../../../src/Modal.tsx"; -import { SetupEncryptionStore } from "../../../../src/stores/SetupEncryptionStore.ts"; -import { ShareFormat } from "../../../../src/dispatcher/payloads/SharePayload.ts"; -import { clearStorage } from "../../../../src/Lifecycle"; -import UserSettingsDialog from "../../../../src/components/views/dialogs/UserSettingsDialog.tsx"; -import { SDKContextClass } from "../../../../src/contexts/SDKContextClass"; -import { makeDelegatedAuthMetadata } from "../../../test-utils/auth.ts"; -import { type QrLoginCredentials } from "../../../../src/components/views/auth/LoginWithQR.tsx"; -import { storeAuthContext } from "../../../../src/utils/oauth/persistOAuthSettings.ts"; +} from "test-utils"; +import { makeDelegatedAuthMetadata } from "test-utils/auth.ts"; + +import MatrixChat from "../../components/structures/MatrixChat"; +import * as StorageAccess from "../../utils/StorageAccess"; +import defaultDispatcher from "../../dispatcher/dispatcher"; +import { Action } from "../../dispatcher/actions"; +import { UserTab } from "../../components/views/dialogs/UserTab"; +import * as leaveRoomUtils from "../../utils/leave-behaviour"; +import { OAuthClientError } from "../../utils/oauth/error"; +import { CallStore } from "../../stores/CallStore"; +import { type Call } from "../../models/Call"; +import { PosthogAnalytics } from "../../PosthogAnalytics"; +import PlatformPeg from "../../PlatformPeg"; +import EventIndexPeg from "../../indexing/EventIndexPeg"; +import MediaDeviceHandler from "../../MediaDeviceHandler"; +import * as Lifecycle from "../../Lifecycle"; +import { SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY } from "../../BasePlatform"; +import SettingsStore from "../../settings/SettingsStore"; +import { SettingLevel } from "../../settings/SettingLevel"; +import { MatrixClientPeg } from "../../MatrixClientPeg"; +import DMRoomMap from "../../utils/DMRoomMap"; +import { ReleaseAnnouncementStore } from "../../stores/ReleaseAnnouncementStore"; +import { DRAFT_LAST_CLEANUP_KEY } from "../../DraftCleaner"; +import { UIFeature } from "../../settings/UIFeature"; +import AutoDiscoveryUtils from "../../utils/AutoDiscoveryUtils"; +import { type ValidatedServerConfig } from "../../utils/ValidatedServerConfig"; +import Modal from "../../Modal.tsx"; +import { SetupEncryptionStore } from "../../stores/SetupEncryptionStore.ts"; +import { ShareFormat } from "../../dispatcher/payloads/SharePayload.ts"; +import { clearStorage } from "../../Lifecycle"; +import UserSettingsDialog from "../../components/views/dialogs/UserSettingsDialog.tsx"; +import { SDKContextClass } from "../../contexts/SDKContextClass"; +import { type QrLoginCredentials } from "../../components/views/auth/LoginWithQR.tsx"; +import { storeAuthContext } from "../../utils/oauth/persistOAuthSettings.ts"; // Stub out ThemeWatcher as the necessary bits for themes are done in element-web's index.html and thus are lacking here, // plus JSDOM's implementation of CSSStyleDeclaration has a bunch of differences to real browsers which cause issues. -jest.mock("../../../../src/settings/watchers/ThemeWatcher"); -jest.mock("../../../../src/theme"); +vi.mock("../../settings/watchers/ThemeWatcher"); +vi.mock("../../theme"); + +vi.mock("../../async-components/views/dialogs/security/NewRecoveryMethodDialog", () => ({ + __test: true, + __esModule: true, + default: () => mocked dialog, +})); +vi.mock("../../async-components/views/dialogs/security/RecoveryMethodRemovedDialog", () => ({ + __test: true, + __esModule: true, + default: () => mocked dialog, +})); /** The matrix versions our mock server claims to support */ const SERVER_SUPPORTED_MATRIX_VERSIONS = ["v1.1", "v1.5", "v1.6", "v1.8", "v1.9"]; function createMockCrypto(): CryptoApi { return { - getVersion: jest.fn().mockReturnValue("Version 0"), - getVerificationRequestsToDeviceInProgress: jest.fn().mockReturnValue([]), - getUserDeviceInfo: jest.fn().mockReturnValue({ - get: jest + getVersion: vi.fn().mockReturnValue("Version 0"), + getVerificationRequestsToDeviceInProgress: vi.fn().mockReturnValue([]), + getUserDeviceInfo: vi.fn().mockReturnValue({ + get: vi .fn() .mockReturnValue( new Map([ [ "devid", - { deviceId: "devid", dehydrated: false, getIdentityKey: jest.fn().mockReturnValue("k") }, + { deviceId: "devid", dehydrated: false, getIdentityKey: vi.fn().mockReturnValue("k") }, ], ]), ), }), - getUserVerificationStatus: jest.fn().mockResolvedValue(new UserVerificationStatus(true, true, false)), - setDeviceIsolationMode: jest.fn(), - isDehydrationSupported: jest.fn().mockReturnValue(false), - getDeviceVerificationStatus: jest.fn().mockResolvedValue({ signedByOwner: true } as DeviceVerificationStatus), - isCrossSigningReady: jest.fn().mockReturnValue(false), - requestOwnUserVerification: jest.fn().mockResolvedValue({ cancel: jest.fn(), on: jest.fn() }), + getUserVerificationStatus: vi.fn().mockResolvedValue(new UserVerificationStatus(true, true, false)), + setDeviceIsolationMode: vi.fn(), + isDehydrationSupported: vi.fn().mockReturnValue(false), + getDeviceVerificationStatus: vi.fn().mockResolvedValue({ signedByOwner: true } as DeviceVerificationStatus), + isCrossSigningReady: vi.fn().mockReturnValue(false), + requestOwnUserVerification: vi.fn().mockResolvedValue({ cancel: vi.fn(), on: vi.fn() }), } as any; } @@ -117,11 +131,13 @@ describe("", () => { const accessToken = "abc123"; const refreshToken = "def456"; let bootstrapDeferred: PromiseWithResolvers; + let oauthLoginDeferred: PromiseWithResolvers; + let qrSignInDeferred: PromiseWithResolvers>> | undefined; // reused in createClient mock below const getMockClientMethods = () => ({ ...mockClientMethodsUser(userId), ...mockClientMethodsServer(), - getVersions: jest.fn().mockResolvedValue({ versions: SERVER_SUPPORTED_MATRIX_VERSIONS }), + getVersions: vi.fn().mockResolvedValue({ versions: SERVER_SUPPORTED_MATRIX_VERSIONS }), startClient: async function () { // This `sleep` is a horrible hack, for which I am sorry. // @@ -133,70 +149,75 @@ describe("", () => { // indexedDB, so in some ways this is just a more realistic simulation of the real world 😇 await sleep(1); + // @ts-ignore + this.getSyncState.mockReturnValue(SyncState.Prepared); // @ts-ignore this.emit(ClientEvent.Sync, SyncState.Prepared, null); }, - stopClient: jest.fn(), - setCanResetTimelineCallback: jest.fn(), - isInitialSyncComplete: jest.fn(), - getSyncState: jest.fn(), - getSsoLoginUrl: jest.fn(), - getSyncStateData: jest.fn().mockReturnValue(null), - getThirdpartyProtocols: jest.fn().mockResolvedValue({}), - getClientWellKnown: jest.fn().mockReturnValue({}), - isVersionSupported: jest.fn().mockResolvedValue(false), - initRustCrypto: jest.fn(), - getRoom: jest.fn(), - getMediaHandler: jest.fn().mockReturnValue({ - setVideoInput: jest.fn(), - setAudioInput: jest.fn(), - setAudioSettings: jest.fn(), - stopAllStreams: jest.fn(), + stopClient: vi.fn(), + setCanResetTimelineCallback: vi.fn(), + isInitialSyncComplete: vi.fn(), + getSyncState: vi.fn(), + getSsoLoginUrl: vi.fn(), + getSyncStateData: vi.fn().mockReturnValue(null), + getThirdpartyProtocols: vi.fn().mockResolvedValue({}), + getClientWellKnown: vi.fn().mockReturnValue({}), + _unstable_getRTCTransports: vi.fn().mockResolvedValue([]), + waitForClientWellKnown: vi.fn().mockResolvedValue({}), + isVersionSupported: vi.fn().mockResolvedValue(false), + initRustCrypto: vi.fn(), + getRoom: vi.fn(), + getMediaHandler: vi.fn().mockReturnValue({ + setVideoInput: vi.fn(), + setAudioInput: vi.fn(), + setAudioSettings: vi.fn(), + stopAllStreams: vi.fn(), } as unknown as MediaHandler), - setAccountData: jest.fn(), + setAccountData: vi.fn(), store: { - destroy: jest.fn(), - startup: jest.fn(), + destroy: vi.fn(), + startup: vi.fn(), }, - login: jest.fn(), - loginFlows: jest.fn().mockResolvedValue({ flows: [] }), - isGuest: jest.fn().mockReturnValue(false), - clearStores: jest.fn(), - setGuest: jest.fn(), - setNotifTimelineSet: jest.fn(), - getAccountData: jest.fn(), - doesServerSupportUnstableFeature: jest.fn().mockResolvedValue(false), - getDevices: jest.fn().mockResolvedValue({ devices: [] }), - getProfileInfo: jest.fn().mockResolvedValue({ + login: vi.fn(), + loginFlows: vi.fn().mockResolvedValue({ flows: [] }), + isGuest: vi.fn().mockReturnValue(false), + clearStores: vi.fn(), + setGuest: vi.fn(), + setNotifTimelineSet: vi.fn(), + getAccountData: vi.fn(), + doesServerSupportUnstableFeature: vi.fn().mockResolvedValue(false), + getDevices: vi.fn().mockResolvedValue({ devices: [] }), + getProfileInfo: vi.fn().mockResolvedValue({ displayname: "Ernie", }), - getVisibleRooms: jest.fn().mockReturnValue([]), - getRooms: jest.fn().mockReturnValue([]), - getCrypto: jest.fn().mockReturnValue({ - getVerificationRequestsToDeviceInProgress: jest.fn().mockReturnValue([]), - isCrossSigningReady: jest.fn().mockReturnValue(false), - isDehydrationSupported: jest.fn().mockReturnValue(false), - getUserDeviceInfo: jest.fn().mockReturnValue(new Map()), - getUserVerificationStatus: jest.fn().mockResolvedValue(new UserVerificationStatus(false, false, false)), - getVersion: jest.fn().mockReturnValue("1"), - setDeviceIsolationMode: jest.fn(), - userHasCrossSigningKeys: jest.fn(), - getActiveSessionBackupVersion: jest.fn().mockResolvedValue(null), + getVisibleRooms: vi.fn().mockReturnValue([]), + getRooms: vi.fn().mockReturnValue([]), + getCrypto: vi.fn().mockReturnValue({ + getVerificationRequestsToDeviceInProgress: vi.fn().mockReturnValue([]), + isCrossSigningReady: vi.fn().mockReturnValue(false), + isDehydrationSupported: vi.fn().mockReturnValue(false), + getUserDeviceInfo: vi.fn().mockReturnValue(new Map()), + getUserVerificationStatus: vi.fn().mockResolvedValue(new UserVerificationStatus(false, false, false)), + getVersion: vi.fn().mockReturnValue("1"), + setDeviceIsolationMode: vi.fn(), + userHasCrossSigningKeys: vi.fn(), + getActiveSessionBackupVersion: vi.fn().mockResolvedValue(null), globalBlacklistUnverifiedDevices: false, // This needs to not finish immediately because we need to test the screen appears - bootstrapCrossSigning: jest.fn().mockImplementation(() => bootstrapDeferred.promise), - getKeyBackupInfo: jest.fn().mockResolvedValue(null), + bootstrapCrossSigning: vi.fn().mockImplementation(() => bootstrapDeferred.promise), + getKeyBackupInfo: vi.fn().mockResolvedValue(null), }), secretStorage: { - isStored: jest.fn().mockReturnValue(null), + isStored: vi.fn().mockReturnValue(null), }, matrixRTC: createStubMatrixRTC(), - getDehydratedDevice: jest.fn(), - whoami: jest.fn(), - logout: jest.fn(), - getDeviceId: jest.fn(), + getDehydratedDevice: vi.fn(), + whoami: vi.fn(), + logout: vi.fn(), + getDeviceId: vi.fn(), forget: () => Promise.resolve(), - getAuthMetadata: jest.fn().mockRejectedValue(new Error("Legacy auth")), + getAuthMetadata: vi.fn().mockRejectedValue(new Error("Legacy auth")), + deleteExtendedProfileProperty: vi.fn(), }); let mockClient: Mocked; const serverConfig = { @@ -236,11 +257,16 @@ describe("", () => { } beforeEach(async () => { + vi.restoreAllMocks(); + vi.spyOn(MediaDeviceHandler, "loadDevices").mockResolvedValue(undefined); + vi.doMock("../../utils/SessionLock.ts", () => ({ + getSessionLock: vi.fn().mockResolvedValue(true), + checkSessionLockFree: vi.fn().mockReturnValue(true), + })); + await clearStorage(); Lifecycle.setSessionLockNotStolen(); - localStorage.clear(); - jest.restoreAllMocks(); defaultProps = { config: { brand: "Test", @@ -252,20 +278,20 @@ describe("", () => { }, validated_server_config: serverConfig, }, - onNewScreen: jest.fn(), - onTokenLoginCompleted: jest.fn(), + onNewScreen: vi.fn(), + onTokenLoginCompleted: vi.fn(), urlParams: {}, }; mockClient = getMockClientWithEventEmitter(getMockClientMethods()); - jest.spyOn(MatrixJs, "createClient").mockReturnValue(mockClient); + vi.spyOn(MatrixJs, "createClient").mockReturnValue(mockClient); - jest.spyOn(defaultDispatcher, "dispatch").mockClear(); - jest.spyOn(defaultDispatcher, "fire").mockClear(); + vi.spyOn(defaultDispatcher, "dispatch").mockClear(); + vi.spyOn(defaultDispatcher, "fire").mockClear(); DMRoomMap.makeShared(mockClient); - jest.spyOn(AutoDiscoveryUtils, "validateServerConfigWithStaticUrls").mockResolvedValue( + vi.spyOn(AutoDiscoveryUtils, "validateServerConfigWithStaticUrls").mockResolvedValue( {} as ValidatedServerConfig, ); @@ -273,12 +299,26 @@ describe("", () => { await clearAllModals(); - jest.spyOn(OAuth2.prototype, "completeAuthorizationCodeGrant").mockImplementation( - (code) => new Promise(() => {}), + oauthLoginDeferred = Promise.withResolvers(); + // Guard against an unhandled rejection if we settle this in `afterEach` without a consumer having awaited it. + oauthLoginDeferred.promise.catch(() => {}); + vi.spyOn(OAuth2.prototype, "completeAuthorizationCodeGrant").mockImplementation( + () => oauthLoginDeferred.promise, ); }); afterEach(async () => { + try { + cleanup(); + } catch { + // Allow it to fail without throwing this hook + } + + bootstrapDeferred.resolve(); + oauthLoginDeferred.reject(new Error("Test teardown")); + qrSignInDeferred?.reject(new Error("Test teardown")); + qrSignInDeferred = undefined; + // @ts-ignore DMRoomMap.setShared(null); @@ -287,16 +327,13 @@ describe("", () => { act(() => defaultDispatcher.dispatch({ action: Action.OnLoggedOut }, true)); localStorage.clear(); + vi.clearAllTimers(); - // This is a massive hack, but ... - // - // A lot of these tests end up completing while the login flow is still proceeding. So then, we start the next - // test while stuff is still ongoing from the previous test, which messes up the current test (by changing - // localStorage or opening modals, or whatever). - // - // There is no obvious event we could wait for which indicates that everything has completed, since each test - // does something different. Instead... - await act(() => sleep(200)); + // This is a massive hack, but a lot of these tests end up completing while the login flow is still proceeding. + // So then, we start the next test while stuff is still ongoing from the previous test, which messes up the current test. + // There is no obvious event we could wait for which indicates that everything has completed, + // since each test does something different. Instead, we just let real timers and microtasks drain. + await sleep(200); }); resetJsDomAfterEach(); @@ -326,24 +363,24 @@ describe("", () => { it("should notify resizenotifier when left panel hidden", async () => { getComponent(); - jest.spyOn(SDKContextClass.instance.resizeNotifier, "notifyLeftHandleResized"); + vi.spyOn(SDKContextClass.instance.resizeNotifier, "notifyLeftHandleResized"); defaultDispatcher.dispatch({ action: "hide_left_panel" }); await waitFor(() => - expect(mocked(SDKContextClass.instance.resizeNotifier.notifyLeftHandleResized)).toHaveBeenCalled(), + expect(vi.mocked(SDKContextClass.instance.resizeNotifier.notifyLeftHandleResized)).toHaveBeenCalled(), ); }); it("should notify resizenotifier when left panel shown", async () => { getComponent(); - jest.spyOn(SDKContextClass.instance.resizeNotifier, "notifyLeftHandleResized"); + vi.spyOn(SDKContextClass.instance.resizeNotifier, "notifyLeftHandleResized"); defaultDispatcher.dispatch({ action: "show_left_panel" }); await waitFor(() => - expect(mocked(SDKContextClass.instance.resizeNotifier.notifyLeftHandleResized)).toHaveBeenCalled(), + expect(vi.mocked(SDKContextClass.instance.resizeNotifier.notifyLeftHandleResized)).toHaveBeenCalled(), ); }); @@ -353,7 +390,7 @@ describe("", () => { defaultProps.config.validated_server_config!.delegatedAuthentication = authConfig; fetchMock.post(authConfig.registration_endpoint!, { client_id: "abc123" }); mockPlatformPeg({ - getOAuthClientMetadata: jest.fn().mockReturnValue({ + getOAuthClientMetadata: vi.fn().mockReturnValue({ client_name: "App name", client_uri: "https://company", redirect_uris: ["https://app"], @@ -361,7 +398,10 @@ describe("", () => { application_type: "web", }), }); - jest.spyOn(qrLogin, "signInByGeneratingQR").mockReturnValue(new Promise(() => {})); + qrSignInDeferred = Promise.withResolvers(); + // Guard against an unhandled rejection if we settle this in `afterEach` without a consumer having awaited it. + qrSignInDeferred.promise.catch(() => {}); + vi.spyOn(qrLogin, "signInByGeneratingQR").mockReturnValue(qrSignInDeferred.promise); }); it("should open QrLoginDialog on ViewQrLogin action", async () => { @@ -405,12 +445,12 @@ describe("", () => { mockClient.whoami.mockResolvedValue({ user_id: "@user:homeserver", device_id: qrCreds.deviceId }); mockClient.getCrypto.mockReturnValue({ ...createMockCrypto(), - crossSignDevice: jest.fn().mockResolvedValue(undefined), - importSecretsBundle: jest.fn().mockResolvedValue(undefined), + crossSignDevice: vi.fn().mockResolvedValue(undefined), + importSecretsBundle: vi.fn().mockResolvedValue(undefined), }); getComponent(); - const createDialogSpy = jest.spyOn(Modal, "createDialog").mockReturnValue({} as any); + const createDialogSpy = vi.spyOn(Modal, "createDialog").mockReturnValue({} as any); // Assert welcome screen await screen.findByText("Welcome to Test"); @@ -431,8 +471,8 @@ describe("", () => { onLoggedIn(creds: QrLoginCredentials): Promise; }; - const configureFromCompletedSpy = jest.spyOn(Lifecycle, "configureFromCompletedOAuthLogin"); - const restoreSessionSpy = jest.spyOn(Lifecycle, "restoreSessionFromStorage"); + const configureFromCompletedSpy = vi.spyOn(Lifecycle, "configureFromCompletedOAuthLogin"); + const restoreSessionSpy = vi.spyOn(Lifecycle, "restoreSessionFromStorage"); const prom = onLoggedIn(qrCreds); await waitFor(() => @@ -494,14 +534,14 @@ describe("", () => { }; beforeEach(() => { - mocked(OAuth2.prototype.completeAuthorizationCodeGrant).mockResolvedValue(tokenResponse); + vi.mocked(OAuth2.prototype.completeAuthorizationCodeGrant).mockResolvedValue(tokenResponse); loginClient = getMockClientWithEventEmitter(getMockClientMethods()); // this is used to create a temporary client during login - jest.spyOn(MatrixJs, "createClient").mockReturnValue(loginClient); + vi.spyOn(MatrixJs, "createClient").mockReturnValue(loginClient); - jest.spyOn(logger, "error").mockClear(); - jest.spyOn(logger, "log").mockClear(); + vi.spyOn(logger, "error").mockClear(); + vi.spyOn(logger, "log").mockClear(); loginClient.whoami.mockResolvedValue({ user_id: userId, @@ -577,7 +617,7 @@ describe("", () => { }); it("should call onTokenLoginCompleted", async () => { - const onTokenLoginCompleted = jest.fn(); + const onTokenLoginCompleted = vi.fn(); getComponent({ urlParams, onTokenLoginCompleted }); await waitFor(() => expect(onTokenLoginCompleted).toHaveBeenCalled()); @@ -585,14 +625,14 @@ describe("", () => { describe("when login fails", () => { beforeEach(() => { - mocked(OAuth2.prototype.completeAuthorizationCodeGrant).mockRejectedValue( + vi.mocked(OAuth2.prototype.completeAuthorizationCodeGrant).mockRejectedValue( new Error(OAuth2Error.CodeExchangeFailed), ); }); it("should log and return to welcome page with correct error when login state is not found", async () => { sessionStorage.clear(); - mocked(OAuth2.prototype.completeAuthorizationCodeGrant).mockRejectedValue( + vi.mocked(OAuth2.prototype.completeAuthorizationCodeGrant).mockRejectedValue( new Error(OAuth2Error.MissingOrInvalidStoredState), ); getComponent({ urlParams }); @@ -632,7 +672,7 @@ describe("", () => { }); it("should not store clientId or issuer", async () => { - const sessionStorageSetSpy = jest.spyOn(sessionStorage.__proto__, "setItem"); + const sessionStorageSetSpy = vi.spyOn(sessionStorage, "setItem"); getComponent({ urlParams }); await flushPromises(); @@ -673,14 +713,14 @@ describe("", () => { ); // set up keys screen is rendered - expect(screen.getByText("Setting up keys")).toBeInTheDocument(); + await expect(screen.findByText("Setting up keys")).resolves.toBeInTheDocument(); }); it("should persist device language when available", async () => { await SettingsStore.setValue("language", null, SettingLevel.DEVICE, "en"); const languageBefore = SettingsStore.getValueAt(SettingLevel.DEVICE, "language", null, true, true); - jest.spyOn(Lifecycle, "attemptDelegatedAuthLogin"); + vi.spyOn(Lifecycle, "attemptDelegatedAuthLogin"); getComponent({ urlParams }); await flushPromises(); @@ -694,7 +734,7 @@ describe("", () => { await SettingsStore.setValue("language", null, SettingLevel.DEVICE, null); const languageBefore = SettingsStore.getValueAt(SettingLevel.DEVICE, "language", null, true, true); - jest.spyOn(Lifecycle, "attemptDelegatedAuthLogin"); + vi.spyOn(Lifecycle, "attemptDelegatedAuthLogin"); getComponent({ urlParams }); await flushPromises(); @@ -716,7 +756,7 @@ describe("", () => { beforeEach(async () => { await populateStorageForSession(); - jest.spyOn(StorageAccess, "idbLoad").mockImplementation(async (table, key) => { + vi.spyOn(StorageAccess, "idbLoad").mockImplementation(async (table, key) => { const safeKey = Array.isArray(key) ? key[0] : key; return mockidb[table]?.[safeKey]; }); @@ -746,9 +786,9 @@ describe("", () => { getComponent(); // wait for logged in view to load - await screen.findByLabelText("User menu"); + await expect(screen.findByLabelText("User menu")).resolves.toBeVisible(); - await screen.findByRole("heading", { level: 1, name: "Welcome Ernie" }); + await expect(screen.findByRole("heading", { level: 1, name: "Welcome Ernie" })).resolves.toBeVisible(); }); describe("clean up drafts", () => { @@ -762,7 +802,7 @@ describe("", () => { mockClient.getRoom.mockImplementation((id) => [room].find((room) => room.roomId === id) || null); }); it("should clean up drafts", async () => { - Date.now = jest.fn(() => timestamp); + Date.now = vi.fn(() => timestamp); localStorage.setItem(`mx_cider_state_${roomId}`, "fake_content"); localStorage.setItem(`mx_cider_state_${unknownRoomId}`, "fake_content"); await getComponentAndWaitForReady(); @@ -774,7 +814,7 @@ describe("", () => { }); it("should clean up wysiwyg drafts", async () => { - Date.now = jest.fn(() => timestamp); + Date.now = vi.fn(() => timestamp); localStorage.setItem(`mx_wysiwyg_state_${roomId}`, "fake_content"); localStorage.setItem(`mx_wysiwyg_state_${unknownRoomId}`, "fake_content"); await getComponentAndWaitForReady(); @@ -797,13 +837,13 @@ describe("", () => { describe("onAction()", () => { afterEach(() => { - jest.restoreAllMocks(); + vi.restoreAllMocks(); }); it("ViewUserDeviceSettings should open user device settings", async () => { await getComponentAndWaitForReady(); - const createDialog = jest.spyOn(Modal, "createDialog").mockReturnValue({} as any); + const createDialog = vi.spyOn(Modal, "createDialog").mockReturnValue({} as any); await act(async () => { defaultDispatcher.dispatch({ @@ -832,9 +872,9 @@ describe("", () => { mockClient.getRoom.mockImplementation( (id) => [room, spaceRoom].find((room) => room.roomId === id) || null, ); - jest.spyOn(spaceRoom, "isSpaceRoom").mockReturnValue(true); + vi.spyOn(spaceRoom, "isSpaceRoom").mockReturnValue(true); - jest.spyOn(ReleaseAnnouncementStore.instance, "getReleaseAnnouncement").mockReturnValue(null); + vi.spyOn(ReleaseAnnouncementStore.instance, "getReleaseAnnouncement").mockReturnValue(null); (room as any).client = mockClient; (spaceRoom as any).client = mockClient; }); @@ -845,7 +885,7 @@ describe("", () => { await getComponentAndWaitForReady(); // Register a mock function to the dispatcher - const fn = jest.fn(); + const fn = vi.fn(); defaultDispatcher.register(fn); // Forge the room @@ -869,7 +909,7 @@ describe("", () => { await clearAllModals(); await getComponentAndWaitForReady(); // this is thoroughly unit tested elsewhere - jest.spyOn(leaveRoomUtils, "leaveRoomBehaviour").mockClear().mockResolvedValue(undefined); + vi.spyOn(leaveRoomUtils, "leaveRoomBehaviour").mockClear().mockResolvedValue(undefined); }); const dispatchAction = () => defaultDispatcher.dispatch({ @@ -890,8 +930,8 @@ describe("", () => { }); describe("for a room", () => { beforeEach(() => { - jest.spyOn(room.currentState, "getJoinedMemberCount").mockReturnValue(2); - jest.spyOn(room.currentState, "getStateEvents").mockReturnValue(publicJoinRule); + vi.spyOn(room.currentState, "getJoinedMemberCount").mockReturnValue(2); + vi.spyOn(room.currentState, "getStateEvents").mockReturnValue(publicJoinRule); }); it("should launch a confirmation modal", async () => { dispatchAction(); @@ -899,7 +939,7 @@ describe("", () => { expect(dialog).toMatchSnapshot(); }); it("should warn when room has only one joined member", async () => { - jest.spyOn(room.currentState, "getJoinedMemberCount").mockReturnValue(1); + vi.spyOn(room.currentState, "getJoinedMemberCount").mockReturnValue(1); dispatchAction(); await screen.findByRole("dialog"); expect( @@ -909,7 +949,7 @@ describe("", () => { ).toBeInTheDocument(); }); it("should warn when room is not public", async () => { - jest.spyOn(room.currentState, "getStateEvents").mockReturnValue(inviteJoinRule); + vi.spyOn(room.currentState, "getStateEvents").mockReturnValue(inviteJoinRule); dispatchAction(); await screen.findByRole("dialog"); expect( @@ -919,11 +959,11 @@ describe("", () => { ).toBeInTheDocument(); }); it("should warn when user is the last admin", async () => { - jest.spyOn(room, "getJoinedMembers").mockReturnValue([ + vi.spyOn(room, "getJoinedMembers").mockReturnValue([ { powerLevel: 100 } as unknown as MatrixJs.RoomMember, { powerLevel: 0 } as unknown as MatrixJs.RoomMember, ]); - jest.spyOn(room, "getMember").mockReturnValue({ + vi.spyOn(room, "getMember").mockReturnValue({ powerLevel: 100, } as unknown as MatrixJs.RoomMember); dispatchAction(); @@ -969,7 +1009,7 @@ describe("", () => { room_id: spaceId, }); beforeEach(() => { - jest.spyOn(spaceRoom.currentState, "getStateEvents").mockReturnValue(publicJoinRule); + vi.spyOn(spaceRoom.currentState, "getStateEvents").mockReturnValue(publicJoinRule); }); it("should launch a confirmation modal", async () => { dispatchAction(); @@ -977,7 +1017,7 @@ describe("", () => { expect(dialog).toMatchSnapshot(); }); it("should warn when space is not public", async () => { - jest.spyOn(spaceRoom.currentState, "getStateEvents").mockReturnValue(inviteJoinRule); + vi.spyOn(spaceRoom.currentState, "getStateEvents").mockReturnValue(inviteJoinRule); dispatchAction(); await screen.findByRole("dialog"); expect( @@ -999,9 +1039,9 @@ describe("", () => { permalinkCreator: null, }); }); - const forwardCall = mocked(defaultDispatcher.dispatch).mock.calls.find( - ([call]) => call.action === Action.OpenForwardDialog, - ); + const forwardCall = vi + .mocked(defaultDispatcher.dispatch) + .mock.calls.find(([call]) => call.action === Action.OpenForwardDialog); const payload = forwardCall?.[0]; @@ -1021,9 +1061,9 @@ describe("", () => { permalinkCreator: null, }); }); - const forwardCall = mocked(defaultDispatcher.dispatch).mock.calls.find( - ([call]) => call.action === Action.OpenForwardDialog, - ); + const forwardCall = vi + .mocked(defaultDispatcher.dispatch) + .mock.calls.find(([call]) => call.action === Action.OpenForwardDialog); const payload = forwardCall?.[0]; @@ -1049,9 +1089,9 @@ describe("", () => { permalinkCreator: null, }); }); - const forwardCall = mocked(defaultDispatcher.dispatch).mock.calls.find( - ([call]) => call.action === Action.OpenForwardDialog, - ); + const forwardCall = vi + .mocked(defaultDispatcher.dispatch) + .mock.calls.find(([call]) => call.action === Action.OpenForwardDialog); const payload = forwardCall?.[0]; @@ -1077,9 +1117,9 @@ describe("", () => { permalinkCreator: null, }); }); - const forwardCall = mocked(defaultDispatcher.dispatch).mock.calls.find( - ([call]) => call.action === Action.OpenForwardDialog, - ); + const forwardCall = vi + .mocked(defaultDispatcher.dispatch) + .mock.calls.find(([call]) => call.action === Action.OpenForwardDialog); const payload = forwardCall?.[0]; @@ -1094,8 +1134,8 @@ describe("", () => { describe("logout", () => { let logoutClient!: ReturnType; - const call1 = { disconnect: jest.fn() } as unknown as Call; - const call2 = { disconnect: jest.fn() } as unknown as Call; + const call1 = { disconnect: vi.fn() } as unknown as Call; + const call2 = { disconnect: vi.fn() } as unknown as Call; const dispatchLogoutAndWait = async (): Promise => { defaultDispatcher.dispatch({ @@ -1107,16 +1147,16 @@ describe("", () => { beforeEach(() => { // stub out various cleanup functions - jest.spyOn(SDKContextClass.instance.legacyCallHandler, "hangupAllCalls") + vi.spyOn(SDKContextClass.instance.legacyCallHandler, "hangupAllCalls") .mockClear() .mockImplementation(() => {}); - jest.spyOn(PosthogAnalytics.instance, "logout").mockImplementation(() => {}); - jest.spyOn(EventIndexPeg, "deleteEventIndex").mockImplementation(async () => {}); + vi.spyOn(PosthogAnalytics.instance, "logout").mockImplementation(() => {}); + vi.spyOn(EventIndexPeg, "deleteEventIndex").mockImplementation(async () => {}); - jest.spyOn(CallStore.instance, "connectedCalls", "get").mockReturnValue(new Set([call1, call2])); + vi.spyOn(CallStore.instance, "connectedCalls", "get").mockReturnValue(new Set([call1, call2])); mockPlatformPeg({ - destroyPickleKey: jest.fn(), + destroyPickleKey: vi.fn(), }); logoutClient = getMockClientWithEventEmitter(getMockClientMethods()); @@ -1124,9 +1164,9 @@ describe("", () => { mockClient.logout.mockResolvedValue({}); mockClient.getDeviceId.mockReturnValue(deviceId); // this is used to create a temporary client to cleanup after logout - jest.spyOn(MatrixJs, "createClient").mockClear().mockReturnValue(logoutClient); + vi.spyOn(MatrixJs, "createClient").mockClear().mockReturnValue(logoutClient); - jest.spyOn(logger, "warn").mockClear(); + vi.spyOn(logger, "warn").mockClear(); }); it("should hangup all legacy calls", async () => { @@ -1220,11 +1260,11 @@ describe("", () => { // lostKeys returns false, meaning there are other devices to verify against const realStore = SetupEncryptionStore.sharedInstance(); - jest.spyOn(realStore, "lostKeys").mockReturnValue(false); + vi.spyOn(realStore, "lostKeys").mockReturnValue(false); }); afterEach(() => { - jest.restoreAllMocks(); + vi.restoreAllMocks(); // Reset things back to how they were before we started defaultProps.config.force_verification = false; localStorage.removeItem("must_verify_device"); @@ -1238,11 +1278,16 @@ describe("", () => { getComponent(); // Then we are asked to verify our device - await screen.findByRole("heading", { name: "Confirm your digital identity", level: 2 }); + await expect( + screen.findByRole("heading", { name: "Confirm your digital identity", level: 2 }), + ).resolves.toBeVisible(); // Sanity: we are not racing with another screen update, so this heading stays visible - await screen.findByRole("heading", { name: "Confirm your digital identity", level: 2 }); + await expect( + screen.findByRole("heading", { name: "Confirm your digital identity", level: 2 }), + ).resolves.toBeVisible(); }); + it("should not open app after cancelling device verify if unskippable verification is on", async () => { // See https://github.com/element-hq/element-web/issues/29230 // We used to allow bypassing force verification by choosing "Verify with @@ -1267,7 +1312,9 @@ describe("", () => { act(() => closeButton.click()); // Then we are not allowed in - we are still being asked to verify - await screen.findByRole("heading", { name: "Confirm your digital identity", level: 2 }); + await expect( + screen.findByRole("heading", { name: "Confirm your digital identity", level: 2 }), + ).resolves.toBeVisible(); }); describe("when query params have a loginToken", () => { @@ -1292,7 +1339,7 @@ describe("", () => { localStorage.setItem("mx_sso_is_url", serverConfig.isUrl); loginClient = getMockClientWithEventEmitter(getMockClientMethods()); // this is used to create a temporary client during login - jest.spyOn(MatrixJs, "createClient").mockReturnValue(loginClient); + vi.spyOn(MatrixJs, "createClient").mockReturnValue(loginClient); loginClient.login.mockClear().mockResolvedValue(clientLoginResponse); }); @@ -1301,7 +1348,7 @@ describe("", () => { // Given force_verification is on (outer describe) // And we just logged in via OIDC (inner describe) - mocked(loginClient.getCrypto()!.userHasCrossSigningKeys).mockResolvedValue(true); + vi.mocked(loginClient.getCrypto()!.userHasCrossSigningKeys).mockResolvedValue(true); // When we load the page getComponent({ urlParams }); @@ -1345,7 +1392,7 @@ describe("", () => { mockClient.loginFlows.mockResolvedValue({ flows: [{ type: "m.login.password" }] }); - jest.spyOn(StorageAccess, "idbLoad").mockImplementation(async (table, key) => { + vi.spyOn(StorageAccess, "idbLoad").mockImplementation(async (table, key) => { const safeKey = Array.isArray(key) ? key[0] : key; return mockidb[table]?.[safeKey]; }); @@ -1357,12 +1404,13 @@ describe("", () => { // but as the exception was swallowed, the test was passing (see in `initClientCrypto`). // There are several uses of the peg in the app, so during all these tests you might end-up // with a real client instead of the mocked one. Not sure how reliable all these tests are. - jest.spyOn(MatrixClientPeg, "set"); - jest.spyOn(MatrixClientPeg, "get").mockReturnValue(mockClient); + vi.spyOn(MatrixClientPeg, "set"); + vi.spyOn(MatrixClientPeg, "get").mockReturnValue(mockClient); const result = getComponent(); await result.findByText("You're signed out"); + await result.findByLabelText("Language Dropdown"); expect(result.container).toMatchSnapshot(); }); }); @@ -1385,7 +1433,7 @@ describe("", () => { }), ); - await flushPromises(); + await screen.findByLabelText("Username"); return renderResult; }; @@ -1404,7 +1452,7 @@ describe("", () => { loginClient = getMockClientWithEventEmitter(getMockClientMethods()); // this is used to create a temporary client during login // FIXME: except it is *also* used as the permanent client for the rest of the test. - jest.spyOn(MatrixJs, "createClient").mockClear().mockReturnValue(loginClient); + vi.spyOn(MatrixJs, "createClient").mockClear().mockReturnValue(loginClient); loginClient.login.mockClear().mockResolvedValue({ access_token: "TOKEN", @@ -1423,20 +1471,20 @@ describe("", () => { describe("post login setup", () => { beforeEach(() => { const mockCrypto = { - getVersion: jest.fn().mockReturnValue("Version 0"), - getVerificationRequestsToDeviceInProgress: jest.fn().mockReturnValue([]), - getUserDeviceInfo: jest.fn().mockResolvedValue(new Map()), - getUserVerificationStatus: jest + getVersion: vi.fn().mockReturnValue("Version 0"), + getVerificationRequestsToDeviceInProgress: vi.fn().mockReturnValue([]), + getUserDeviceInfo: vi.fn().mockResolvedValue(new Map()), + getUserVerificationStatus: vi .fn() .mockResolvedValue(new UserVerificationStatus(false, false, false)), - setDeviceIsolationMode: jest.fn(), - userHasCrossSigningKeys: jest.fn().mockResolvedValue(false), + setDeviceIsolationMode: vi.fn(), + userHasCrossSigningKeys: vi.fn().mockResolvedValue(false), // This needs to not finish immediately because we need to test the screen appears - bootstrapCrossSigning: jest.fn().mockImplementation(() => bootstrapDeferred.promise), - resetKeyBackup: jest.fn(), - isEncryptionEnabledInRoom: jest.fn().mockResolvedValue(false), - checkKeyBackupAndEnable: jest.fn().mockResolvedValue(null), - isDehydrationSupported: jest.fn().mockReturnValue(false), + bootstrapCrossSigning: vi.fn().mockImplementation(() => bootstrapDeferred.promise), + resetKeyBackup: vi.fn(), + isEncryptionEnabledInRoom: vi.fn().mockResolvedValue(false), + checkKeyBackupAndEnable: vi.fn().mockResolvedValue(null), + isDehydrationSupported: vi.fn().mockReturnValue(false), }; loginClient.getCrypto.mockReturnValue(mockCrypto as any); }); @@ -1454,7 +1502,7 @@ describe("", () => { describe("when user does not have cross signing set up", () => { beforeEach(() => { - jest.spyOn(loginClient.getCrypto()!, "userHasCrossSigningKeys").mockResolvedValue(false); + vi.spyOn(loginClient.getCrypto()!, "userHasCrossSigningKeys").mockResolvedValue(false); }); describe("when encryption is force disabled", () => { @@ -1471,8 +1519,8 @@ describe("", () => { }, }); - jest.spyOn(loginClient.getCrypto()!, "isEncryptionEnabledInRoom").mockImplementation( - async (roomId) => { + vi.spyOn(loginClient.getCrypto()!, "isEncryptionEnabledInRoom").mockImplementation( + async (roomId: string) => { return roomId === encryptedRoom.roomId; }, ); @@ -1483,14 +1531,14 @@ describe("", () => { await getComponentAndLogin(); // logged in, did not set up keys - await screen.findByLabelText("User menu"); + await expect(screen.findByLabelText("User menu")).resolves.toBeVisible(); }); it("should go to set up e2e screen when user is in encrypted rooms", async () => { loginClient.getRooms.mockReturnValue([unencryptedRoom, encryptedRoom]); await getComponentAndLogin(); // set up keys screen is rendered - await screen.findByText("Setting up keys"); + await expect(screen.findByText("Setting up keys")).resolves.toBeVisible(); }); }); @@ -1505,7 +1553,7 @@ describe("", () => { }); it("should show complete security screen when user has cross signing set up", async () => { - jest.spyOn(loginClient.getCrypto()!, "userHasCrossSigningKeys").mockResolvedValue(true); + vi.spyOn(loginClient.getCrypto()!, "userHasCrossSigningKeys").mockResolvedValue(true); await getComponentAndLogin(); @@ -1548,14 +1596,14 @@ describe("", () => { localStorage.setItem("mx_sso_is_url", serverConfig.isUrl); loginClient = getMockClientWithEventEmitter(getMockClientMethods()); // this is used to create a temporary client during login - jest.spyOn(MatrixJs, "createClient").mockReturnValue(loginClient); + vi.spyOn(MatrixJs, "createClient").mockReturnValue(loginClient); loginClient.login.mockClear().mockResolvedValue(clientLoginResponse); }); it("should show an error dialog when no homeserver is found in local storage", async () => { localStorage.removeItem("mx_sso_hs_url"); - const localStorageGetSpy = jest.spyOn(localStorage.__proto__, "getItem"); + const localStorageGetSpy = vi.spyOn(localStorage, "getItem"); getComponent({ urlParams }); await flushPromises(); @@ -1584,7 +1632,7 @@ describe("", () => { }); it("should call onTokenLoginCompleted", async () => { - const onTokenLoginCompleted = jest.fn(); + const onTokenLoginCompleted = vi.fn(); getComponent({ urlParams, onTokenLoginCompleted }); await waitFor(() => expect(onTokenLoginCompleted).toHaveBeenCalled()); @@ -1620,7 +1668,7 @@ describe("", () => { describe("when login succeeds", () => { beforeEach(() => { - jest.spyOn(StorageAccess, "idbLoad").mockImplementation( + vi.spyOn(StorageAccess, "idbLoad").mockImplementation( async (_table: string, key: string | string[]) => { if (key === "mx_access_token") { return accessToken as any; @@ -1630,7 +1678,7 @@ describe("", () => { }); it("should clear storage", async () => { - const localStorageClearSpy = jest.spyOn(localStorage.__proto__, "clear"); + const localStorageClearSpy = vi.spyOn(localStorage, "clear"); getComponent({ urlParams }); @@ -1650,7 +1698,7 @@ describe("", () => { }); it("should set fresh login flag in session storage", async () => { - const sessionStorageSetSpy = jest.spyOn(sessionStorage.__proto__, "setItem"); + const sessionStorageSetSpy = vi.spyOn(sessionStorage, "setItem"); getComponent({ urlParams }); await waitFor(() => expect(sessionStorageSetSpy).toHaveBeenCalledWith("mx_fresh_login", "true")); @@ -1686,21 +1734,21 @@ describe("", () => { describe("automatic SSO selection", () => { let ssoClient: ReturnType; - let hrefSetter: jest.Mock; + let hrefSetter: Mocked<(href: string) => void>; beforeEach(() => { ssoClient = getMockClientWithEventEmitter({ ...getMockClientMethods(), - getHomeserverUrl: jest.fn().mockReturnValue("matrix.example.com"), - getIdentityServerUrl: jest.fn().mockReturnValue("ident.example.com"), - getSsoLoginUrl: jest.fn().mockReturnValue("http://my-sso-url"), + getHomeserverUrl: vi.fn().mockReturnValue("matrix.example.com"), + getIdentityServerUrl: vi.fn().mockReturnValue("ident.example.com"), + getSsoLoginUrl: vi.fn().mockReturnValue("http://my-sso-url"), }); // this is used to create a temporary client to cleanup after logout - jest.spyOn(MatrixJs, "createClient").mockClear().mockReturnValue(ssoClient); + vi.spyOn(MatrixJs, "createClient").mockClear().mockReturnValue(ssoClient); mockPlatformPeg(); // Ensure we don't have a client peg as we aren't logged in. unmockClientPeg(); - hrefSetter = jest.fn(); + hrefSetter = vi.fn(); const originalHref = window.location.href.toString(); Object.defineProperty(window, "location", { value: { @@ -1751,6 +1799,7 @@ describe("", () => { describe("Multi-tab lockout", () => { beforeEach(() => { mockPlatformPeg(); + vi.doUnmock("../../utils/SessionLock.ts"); }); afterEach(() => { @@ -1759,7 +1808,7 @@ describe("", () => { // Flaky test, see https://github.com/element-hq/element-web/issues/30337 it("waits for other tab to stop during startup", async () => { - jest.spyOn(Lifecycle, "attemptDelegatedAuthLogin"); + vi.spyOn(Lifecycle, "attemptDelegatedAuthLogin"); // simulate an active window localStorage.setItem("react_sdk_session_lock_ping", String(Date.now())); @@ -1790,6 +1839,7 @@ describe("", () => { // should just show the welcome screen await rendered.findByText("Welcome to Test"); + await rendered.findByLabelText("Language Dropdown"); expect(rendered.container).toMatchSnapshot(); }); @@ -1812,7 +1862,7 @@ describe("", () => { await populateStorageForSession(); const client = getMockClientWithEventEmitter(getMockClientMethods()); - jest.spyOn(MatrixJs, "createClient").mockReturnValue(client); + vi.spyOn(MatrixJs, "createClient").mockReturnValue(client); const rendered = getComponent({}); await rendered.findByText("Welcome Ernie"); @@ -1857,7 +1907,7 @@ describe("", () => { const client = new MockClientWithEventEmitter({ ...getMockClientMethods(), }) as unknown as Mocked; - jest.spyOn(MatrixJs, "createClient").mockReturnValue(client); + vi.spyOn(MatrixJs, "createClient").mockReturnValue(client); // intercept initCrypto and have it block until we complete the deferred const initCryptoCompleteDefer = Promise.withResolvers(); @@ -1901,7 +1951,7 @@ describe("", () => { }; const enabledMobileRegistration = (): void => { - jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName): any => { + vi.spyOn(SettingsStore, "getValue").mockImplementation((settingName): any => { if (settingName === "Registration.mobileRegistrationHelper") return true; if (settingName === UIFeature.Registration) return true; }); @@ -1910,7 +1960,7 @@ describe("", () => { it("should render welcome screen if mobile registration is not enabled in settings", async () => { await getComponentAndWaitForReady(); - await screen.findByText("Powered by Matrix"); + await expect(screen.findByText("Powered by Matrix")).resolves.toBeVisible(); }); it("should render mobile registration", async () => { @@ -1925,13 +1975,8 @@ describe("", () => { describe("when key backup failed", () => { it("should show the new recovery method dialog", async () => { - const spy = jest.spyOn(Modal, "createDialog"); - jest.mock("../../../../src/async-components/views/dialogs/security/NewRecoveryMethodDialog", () => ({ - __test: true, - __esModule: true, - default: () => mocked dialog, - })); - jest.spyOn(mockClient.getCrypto()!, "getActiveSessionBackupVersion").mockResolvedValue("version"); + const spy = vi.spyOn(Modal, "createDialog"); + vi.spyOn(mockClient.getCrypto()!, "getActiveSessionBackupVersion").mockResolvedValue("version"); getComponent({}); defaultDispatcher.dispatch({ @@ -1949,13 +1994,7 @@ describe("", () => { }); it("should show the recovery method removed dialog", async () => { - const spy = jest.spyOn(Modal, "createDialog"); - jest.mock("../../../../src/async-components/views/dialogs/security/RecoveryMethodRemovedDialog", () => ({ - __test: true, - __esModule: true, - default: () => mocked dialog, - })); - + const spy = vi.spyOn(Modal, "createDialog"); getComponent({}); defaultDispatcher.dispatch({ action: Action.WillStartClient, diff --git a/apps/web/src/components/structures/MatrixChat.tsx b/apps/web/src/components/structures/MatrixChat.tsx index 7707069fc4..1d58cf35cd 100644 --- a/apps/web/src/components/structures/MatrixChat.tsx +++ b/apps/web/src/components/structures/MatrixChat.tsx @@ -524,6 +524,8 @@ export default class MatrixChat extends React.PureComponent { UIStore.destroy(); this.stores.resizeNotifier.removeListener("middlePanelResized", this.dispatchTimelineResize); window.removeEventListener("resize", this.onWindowResized); + + DecryptionFailureTracker.instance.stop(); } private onWindowResized = (): void => { @@ -1577,6 +1579,15 @@ export default class MatrixChat extends React.PureComponent { this.firstSyncComplete = false; const cli = MatrixClientPeg.safeGet(); + // If the client has already completed its initial sync — e.g. this is a repeat WillStartClient for a client + // that is already running — it won't emit another `Prepared`, so resolve firstSyncPromise straight away + // rather than waiting for an event that will never come. + // This is mostly an issue under test. + if (cli.getSyncState() === SyncState.Prepared) { + this.firstSyncComplete = true; + this.firstSyncPromise.resolve(); + } + // Allow the JS SDK to reap timeline events. This reduces the amount of // memory consumed as the JS SDK stores multiple distinct copies of room // state (each of which can be 10s of MBs) for each DISJOINT timeline. This is diff --git a/apps/web/test/unit-tests/components/structures/__snapshots__/MatrixChat-test.tsx.snap b/apps/web/src/components/structures/__snapshots__/MatrixChat.test.tsx.snap similarity index 89% rename from apps/web/test/unit-tests/components/structures/__snapshots__/MatrixChat-test.tsx.snap rename to apps/web/src/components/structures/__snapshots__/MatrixChat.test.tsx.snap index d101825e06..80b2123a18 100644 --- a/apps/web/test/unit-tests/components/structures/__snapshots__/MatrixChat-test.tsx.snap +++ b/apps/web/src/components/structures/__snapshots__/MatrixChat.test.tsx.snap @@ -1,6 +1,6 @@ -// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[` Multi-tab lockout shows the lockout page when a second tab opens after a session is restored 1`] = ` +exports[` > Multi-tab lockout > shows the lockout page when a second tab opens > after a session is restored 1`] = `
Multi-tab lockout shows the lockout page when a second t
`; -exports[` Multi-tab lockout shows the lockout page when a second tab opens during crypto init 1`] = ` +exports[` > Multi-tab lockout > shows the lockout page when a second tab opens > during crypto init 1`] = ` @@ -34,7 +34,7 @@ exports[` Multi-tab lockout shows the lockout page when a second t `; -exports[` Multi-tab lockout shows the lockout page when a second tab opens while we are checking the sync store 1`] = ` +exports[` > Multi-tab lockout > shows the lockout page when a second tab opens > while we are checking the sync store 1`] = `
Multi-tab lockout shows the lockout page when a second t
`; -exports[` Multi-tab lockout shows the lockout page when a second tab opens while we were waiting for the lock ourselves 1`] = ` +exports[` > Multi-tab lockout > shows the lockout page when a second tab opens > while we were waiting for the lock ourselves 1`] = `
Multi-tab lockout shows the lockout page when a second t
`; -exports[` Multi-tab lockout waits for other tab to stop during startup 1`] = ` +exports[` > Multi-tab lockout > waits for other tab to stop during startup 1`] = `
Multi-tab lockout waits for other tab to stop during sta
`; -exports[` Multi-tab lockout waits for other tab to stop during startup 2`] = ` +exports[` > Multi-tab lockout > waits for other tab to stop during startup 2`] = `
Multi-tab lockout waits for other tab to stop during sta
`; -exports[` Multi-tab lockout waits for other tab to stop during startup 3`] = ` +exports[` > Multi-tab lockout > waits for other tab to stop during startup 3`] = `
Multi-tab lockout waits for other tab to stop during sta
`; -exports[` qr login should fire ViewQrLogin action on 'qr_login' route 1`] = ` +exports[` > qr login > should fire ViewQrLogin action on 'qr_login' route 1`] = `
qr login should fire ViewQrLogin action on 'qr_login' ro
`; -exports[` qr login should open QrLoginDialog on ViewQrLogin action 1`] = ` +exports[` > qr login > should open QrLoginDialog on ViewQrLogin action 1`] = `
qr login should open QrLoginDialog on ViewQrLogin action
`; -exports[` should render spinner while app is loading 1`] = ` +exports[` > should render spinner while app is loading 1`] = `
should render spinner while app is loading 1`] = `
`; -exports[` with a soft-logged-out session should show the soft-logout page 1`] = ` +exports[` > with a soft-logged-out session > should show the soft-logout page 1`] = `
with a soft-logged-out session should show the soft-logo
`; -exports[` with an existing session onAction() room actions leave_room for a room should launch a confirmation modal 1`] = ` +exports[` > with an existing session > onAction() > room actions > leave_room > for a room > should launch a confirmation modal 1`] = `
with an existing session onAction() room actions leave_r
with an existing session onAction() room actions leave_r
`; -exports[` with an existing session onAction() room actions leave_room for a space should launch a confirmation modal 1`] = ` +exports[` > with an existing session > onAction() > room actions > leave_room > for a space > should launch a confirmation modal 1`] = `
with an existing session onAction() room actions leave_r
void = jest.fn(); - public stop: () => void = jest.fn(); + public start: () => void = vi.fn(); + public stop: () => void = vi.fn(); } describe("CompleteSecurity", () => { @@ -35,16 +37,16 @@ describe("CompleteSecurity", () => { }); const userIdToDevices = new Map(); userIdToDevices.set("USER_ID", deviceIdToDevice); - mocked(client.getCrypto()!.getUserDeviceInfo).mockResolvedValue(userIdToDevices); + vi.mocked(client.getCrypto()!.getUserDeviceInfo).mockResolvedValue(userIdToDevices); const mockSetupEncryptionStore = new MockSetupEncryptionStore(); - jest.spyOn(SetupEncryptionStore, "sharedInstance").mockReturnValue( + vi.spyOn(SetupEncryptionStore, "sharedInstance").mockReturnValue( mockSetupEncryptionStore as SetupEncryptionStore, ); }); afterEach(() => { - jest.restoreAllMocks(); + vi.restoreAllMocks(); }); it("Renders with a cancel button by default", () => { @@ -54,7 +56,7 @@ describe("CompleteSecurity", () => { }); it("Renders with a cancel button if forceVerification false", () => { - jest.spyOn(SdkConfig, "get").mockImplementation((key: string) => { + vi.spyOn(SdkConfig, "get").mockImplementation((key: string) => { if (key === "forceVerification") { return false; } @@ -66,7 +68,7 @@ describe("CompleteSecurity", () => { }); it("Renders without a cancel button if forceVerification true", () => { - jest.spyOn(SdkConfig, "get").mockImplementation((key: string) => { + vi.spyOn(SdkConfig, "get").mockImplementation((key: string) => { if (key === "force_verification") { return true; } @@ -80,7 +82,7 @@ describe("CompleteSecurity", () => { it("Renders a warning if user hits Reset", async () => { // Given a store and a dialog based on it const store = new SetupEncryptionStore(); - jest.spyOn(SetupEncryptionStore, "sharedInstance").mockReturnValue(store); + vi.spyOn(SetupEncryptionStore, "sharedInstance").mockReturnValue(store); const panel = await act(() => render( {}} />)); // No recovery methods are available, so only the "Can't confirm?" button should be visible @@ -99,12 +101,12 @@ describe("CompleteSecurity", () => { it("Allows verifying with another device if one is available", async () => { // Given a store and a dialog based on it const store = new SetupEncryptionStore(); - jest.spyOn(store, "fetchKeyInfo").mockImplementation(async () => { + vi.spyOn(store, "fetchKeyInfo").mockImplementation(async () => { store.hasDevicesToVerifyAgainst = true; store.phase = Phase.Intro; store.emit("update"); }); - jest.spyOn(SetupEncryptionStore, "sharedInstance").mockReturnValue(store); + vi.spyOn(SetupEncryptionStore, "sharedInstance").mockReturnValue(store); const panel = await act(() => render( {}} />)); // The snapshot should have "Use another device" and "Can't confirm?" @@ -124,12 +126,12 @@ describe("CompleteSecurity", () => { it("Allows verifying with recovery key if one is available", async () => { // Given a store and a dialog based on it const store = new SetupEncryptionStore(); - jest.spyOn(store, "fetchKeyInfo").mockImplementation(async () => { + vi.spyOn(store, "fetchKeyInfo").mockImplementation(async () => { store.keyInfo = {} as any; store.phase = Phase.Intro; store.emit("update"); }); - jest.spyOn(SetupEncryptionStore, "sharedInstance").mockReturnValue(store); + vi.spyOn(SetupEncryptionStore, "sharedInstance").mockReturnValue(store); const panel = await act(() => render( {}} />)); // The snapshot should have "Use recovery key" and "Can't confirm?" diff --git a/apps/web/test/unit-tests/components/structures/auth/E2eSetup-test.tsx b/apps/web/src/components/structures/auth/E2eSetup.test.tsx similarity index 52% rename from apps/web/test/unit-tests/components/structures/auth/E2eSetup-test.tsx rename to apps/web/src/components/structures/auth/E2eSetup.test.tsx index 4e52a7f7ac..5d98fc3254 100644 --- a/apps/web/test/unit-tests/components/structures/auth/E2eSetup-test.tsx +++ b/apps/web/src/components/structures/auth/E2eSetup.test.tsx @@ -5,28 +5,30 @@ 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, afterEach } from "vitest"; import React from "react"; -import { render } from "jest-matrix-react"; -import { mocked } from "jest-mock"; +import { render } from "test-utils-rtl"; -import E2eSetup from "../../../../../src/components/structures/auth/E2eSetup.tsx"; -import { InitialCryptoSetupStore } from "../../../../../src/stores/InitialCryptoSetupStore.ts"; +import E2eSetup from "./E2eSetup.tsx"; +import { InitialCryptoSetupStore } from "../../../stores/InitialCryptoSetupStore.ts"; -afterEach(() => jest.restoreAllMocks()); +afterEach(() => vi.restoreAllMocks()); describe("LeftPanel", () => { it("should call `onCancelled` when the user clicks the cancel button", () => { const mockInitialCryptoSetupStore = { - getStatus: jest.fn(), - on: jest.fn(), - off: jest.fn(), + getStatus: vi.fn(), + on: vi.fn(), + off: vi.fn(), }; - jest.spyOn(InitialCryptoSetupStore, "sharedInstance").mockReturnValue(mockInitialCryptoSetupStore as any); + vi.spyOn(InitialCryptoSetupStore, "sharedInstance").mockReturnValue(mockInitialCryptoSetupStore as any); // We need the setup process to have failed, for the dialog to present a cancel button. - mocked(mockInitialCryptoSetupStore.getStatus).mockReturnValue("error"); + vi.mocked(mockInitialCryptoSetupStore.getStatus).mockReturnValue("error"); - const onCancelled = jest.fn(); + const onCancelled = vi.fn(); const { getByRole } = render(); getByRole("button", { name: "Cancel" }).click(); diff --git a/apps/web/test/unit-tests/components/structures/auth/ForgotPassword-test.tsx b/apps/web/src/components/structures/auth/ForgotPassword.test.tsx similarity index 88% rename from apps/web/test/unit-tests/components/structures/auth/ForgotPassword-test.tsx rename to apps/web/src/components/structures/auth/ForgotPassword.test.tsx index 9e08a644da..cd03e4e910 100644 --- a/apps/web/test/unit-tests/components/structures/auth/ForgotPassword-test.tsx +++ b/apps/web/src/components/structures/auth/ForgotPassword.test.tsx @@ -6,20 +6,22 @@ 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 } from "vitest"; import React from "react"; -import { mocked } from "jest-mock"; -import { render, type RenderResult, screen, waitFor, cleanup } from "jest-matrix-react"; +import { render, type RenderResult, screen, waitFor, cleanup } from "test-utils-rtl"; import userEvent from "@testing-library/user-event"; import { type MatrixClient, createClient } from "matrix-js-sdk/src/matrix"; +import { clearAllModals, filterConsole, stubClient, waitEnoughCyclesForModal } from "test-utils"; -import ForgotPassword from "../../../../../src/components/structures/auth/ForgotPassword"; -import { type ValidatedServerConfig } from "../../../../../src/utils/ValidatedServerConfig"; -import { clearAllModals, filterConsole, stubClient, waitEnoughCyclesForModal } from "../../../../test-utils"; -import AutoDiscoveryUtils from "../../../../../src/utils/AutoDiscoveryUtils"; +import ForgotPassword from "./ForgotPassword"; +import { type ValidatedServerConfig } from "../../../utils/ValidatedServerConfig"; +import AutoDiscoveryUtils from "../../../utils/AutoDiscoveryUtils"; -jest.mock("matrix-js-sdk/src/matrix", () => ({ - ...jest.requireActual("matrix-js-sdk/src/matrix"), - createClient: jest.fn(), +vi.mock("matrix-js-sdk/src/matrix", async () => ({ + ...(await vi.importActual("matrix-js-sdk/src/matrix")), + createClient: vi.fn(), })); describe("", () => { @@ -54,15 +56,15 @@ describe("", () => { beforeEach(() => { client = stubClient(); - mocked(createClient).mockReturnValue(client); + vi.mocked(createClient).mockReturnValue(client); serverConfig = { hsName: "example.com" } as ValidatedServerConfig; - onComplete = jest.fn(); - onLoginClick = jest.fn(); + onComplete = vi.fn(); + onLoginClick = vi.fn(); - jest.spyOn(AutoDiscoveryUtils, "validateServerConfigWithStaticUrls").mockResolvedValue(serverConfig); - jest.spyOn(AutoDiscoveryUtils, "authComponentStateForError"); + vi.spyOn(AutoDiscoveryUtils, "validateServerConfigWithStaticUrls").mockResolvedValue(serverConfig); + vi.spyOn(AutoDiscoveryUtils, "authComponentStateForError"); }); afterEach(async () => { @@ -120,9 +122,9 @@ describe("", () => { describe("and submitting an unknown email", () => { beforeEach(async () => { - mocked(AutoDiscoveryUtils.validateServerConfigWithStaticUrls).mockResolvedValue(serverConfig); + vi.mocked(AutoDiscoveryUtils.validateServerConfigWithStaticUrls).mockResolvedValue(serverConfig); await typeIntoField("Email address", testEmail); - mocked(client).requestPasswordEmailToken.mockRejectedValue({ + vi.mocked(client).requestPasswordEmailToken.mockRejectedValue({ errcode: "M_THREEPID_NOT_FOUND", }); await click(screen.getByText("Send email")); @@ -136,7 +138,7 @@ describe("", () => { describe("and a connection error occurs", () => { beforeEach(async () => { await typeIntoField("Email address", testEmail); - mocked(client).requestPasswordEmailToken.mockRejectedValue({ + vi.mocked(client).requestPasswordEmailToken.mockRejectedValue({ name: "ConnectionError", }); await click(screen.getByText("Send email")); @@ -154,8 +156,8 @@ describe("", () => { describe("and the server liveness check fails", () => { beforeEach(async () => { await typeIntoField("Email address", testEmail); - mocked(AutoDiscoveryUtils.validateServerConfigWithStaticUrls).mockRejectedValue({}); - mocked(AutoDiscoveryUtils.authComponentStateForError).mockReturnValue({ + vi.mocked(AutoDiscoveryUtils.validateServerConfigWithStaticUrls).mockRejectedValue({}); + vi.mocked(AutoDiscoveryUtils.authComponentStateForError).mockReturnValue({ serverErrorIsFatal: true, serverIsAlive: false, serverDeadError: "server down", @@ -171,7 +173,7 @@ describe("", () => { describe("and submitting an known email", () => { beforeEach(async () => { await typeIntoField("Email address", testEmail); - mocked(client).requestPasswordEmailToken.mockResolvedValue({ + vi.mocked(client).requestPasswordEmailToken.mockResolvedValue({ sid: testSid, }); await click(screen.getByText("Send email")); @@ -238,14 +240,14 @@ describe("", () => { describe("and entering a new password", () => { beforeEach(async () => { - mocked(client.setPassword).mockRejectedValue({ httpStatus: 401 }); + vi.mocked(client.setPassword).mockRejectedValue({ httpStatus: 401 }); await typeIntoField("New Password", testPassword); await typeIntoField("Confirm new password", testPassword); }); describe("and submitting it running into rate limiting", () => { beforeEach(async () => { - mocked(client.setPassword).mockRejectedValue({ + vi.mocked(client.setPassword).mockRejectedValue({ message: "rate limit reached", httpStatus: 429, data: { @@ -265,7 +267,7 @@ describe("", () => { describe("and confirm the email link and submitting the new password", () => { beforeEach(async () => { // fake link confirmed by resolving client.setPassword instead of raising an error - mocked(client.setPassword).mockResolvedValue({}); + vi.mocked(client.setPassword).mockResolvedValue({}); await click(screen.getByText("Reset password")); }); @@ -314,9 +316,7 @@ describe("", () => { describe("and dismissing the dialog by clicking the background", () => { beforeEach(async () => { await userEvent.click(await screen.findByTestId("dialog-background"), { delay: null }); - await waitEnoughCyclesForModal({ - useFakeTimers: true, - }); + await waitEnoughCyclesForModal(); }); itShouldCloseTheDialogAndShowThePasswordInput(); @@ -325,9 +325,7 @@ describe("", () => { describe("and dismissing the dialog", () => { beforeEach(async () => { await click(await screen.findByLabelText("Close dialog")); - await waitEnoughCyclesForModal({ - useFakeTimers: true, - }); + await waitEnoughCyclesForModal(); }); itShouldCloseTheDialogAndShowThePasswordInput(); @@ -336,9 +334,7 @@ describe("", () => { describe("and clicking »Re-enter email address«", () => { beforeEach(async () => { await click(await screen.findByText("Re-enter email address")); - await waitEnoughCyclesForModal({ - useFakeTimers: true, - }); + await waitEnoughCyclesForModal(); }); it("should close the dialog and go back to the email input", async () => { @@ -352,7 +348,7 @@ describe("", () => { describe("and validating the link from the mail", () => { beforeEach(async () => { - mocked(client.setPassword).mockResolvedValue({}); + vi.mocked(client.setPassword).mockResolvedValue({}); await click(screen.getByText("Reset password")); // flush promises for the modal to disappear await waitEnoughCyclesForModal(); diff --git a/apps/web/test/unit-tests/components/structures/auth/Login-test.tsx b/apps/web/src/components/structures/auth/Login.test.tsx similarity index 93% rename from apps/web/test/unit-tests/components/structures/auth/Login-test.tsx rename to apps/web/src/components/structures/auth/Login.test.tsx index 38c8116b6e..3826c9ffe7 100644 --- a/apps/web/test/unit-tests/components/structures/auth/Login-test.tsx +++ b/apps/web/src/components/structures/auth/Login.test.tsx @@ -5,10 +5,12 @@ 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 MockedObject } from "vitest"; import React from "react"; -import { fireEvent, render, screen, waitForElementToBeRemoved } from "jest-matrix-react"; -import { mocked, type MockedObject } from "jest-mock-vitest-adapter"; -import fetchMock from "@fetch-mock/jest"; +import { fireEvent, render, screen, waitForElementToBeRemoved } from "test-utils-rtl"; +import fetchMock from "@fetch-mock/vitest"; import { OAUTH_AWARE_PREFERRED_FLOW_FIELD, IdentityProviderBrand, @@ -17,16 +19,16 @@ import { } from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; import * as Matrix from "matrix-js-sdk/src/matrix"; +import { mkServerConfig, mockPlatformPeg, unmockPlatformPeg } from "test-utils"; +import { makeDelegatedAuthMetadata } from "test-utils/auth"; -import SdkConfig from "../../../../../src/SdkConfig"; -import { mkServerConfig, mockPlatformPeg, unmockPlatformPeg } from "../../../../test-utils"; -import Login from "../../../../../src/components/structures/auth/Login"; -import type BasePlatform from "../../../../../src/BasePlatform"; -import * as registerClientUtils from "../../../../../src/utils/oauth/registerClient"; -import { makeDelegatedAuthMetadata } from "../../../../test-utils/auth"; -import { ModuleApi } from "../../../../../src/modules/Api.ts"; +import SdkConfig from "../../../SdkConfig"; +import Login from "./Login"; +import type BasePlatform from "../../../BasePlatform"; +import * as registerClientUtils from "../../../utils/oauth/registerClient"; +import { ModuleApi } from "../../../modules/Api.ts"; -jest.useRealTimers(); +vi.useRealTimers(); const oauthStaticClientsConfig = { "https://staticallyregisteredissuer.org/": { @@ -37,9 +39,9 @@ const oauthStaticClientsConfig = { describe("Login", function () { let platform: MockedObject; - const mockClient = mocked({ - login: jest.fn().mockResolvedValue({}), - loginFlows: jest.fn(), + const mockClient = vi.mocked({ + login: vi.fn().mockResolvedValue({}), + loginFlows: vi.fn(), } as unknown as Matrix.MatrixClient); beforeEach(function () { @@ -54,7 +56,7 @@ describe("Login", function () { user_id: "@user:server", }); mockClient.loginFlows.mockClear().mockResolvedValue({ flows: [{ type: "m.login.password" }] }); - jest.spyOn(Matrix, "createClient").mockImplementation((opts) => { + vi.spyOn(Matrix, "createClient").mockImplementation((opts) => { mockClient.idBaseUrl = opts.idBaseUrl; mockClient.baseUrl = opts.baseUrl; return mockClient; @@ -64,7 +66,7 @@ describe("Login", function () { versions: ["v1.1"], }); platform = mockPlatformPeg({ - startSingleSignOn: jest.fn(), + startSingleSignOn: vi.fn(), }); }); @@ -106,7 +108,7 @@ describe("Login", function () { }); it("should show register button", async () => { - const onRegisterClick = jest.fn(); + const onRegisterClick = vi.fn(); const { getByText } = render( { - jest.spyOn(logger, "error"); + vi.spyOn(logger, "error"); }); afterEach(() => { - jest.spyOn(logger, "error").mockRestore(); + vi.spyOn(logger, "error").mockRestore(); }); it("should attempt to register oauth client", async () => { // dont mock, spy so we can check config values were correctly passed - jest.spyOn(registerClientUtils, "getOAuthClientId"); + vi.spyOn(registerClientUtils, "getOAuthClientId"); fetchMock.post(delegatedAuth.registration_endpoint!, { status: 500 }); getComponent(hsUrl, isUrl, delegatedAuth); diff --git a/apps/web/test/unit-tests/components/structures/auth/LoginSplashView-test.tsx b/apps/web/src/components/structures/auth/LoginSplashView.test.tsx similarity index 89% rename from apps/web/test/unit-tests/components/structures/auth/LoginSplashView-test.tsx rename to apps/web/src/components/structures/auth/LoginSplashView.test.tsx index c2eed442c5..e81db88031 100644 --- a/apps/web/test/unit-tests/components/structures/auth/LoginSplashView-test.tsx +++ b/apps/web/src/components/structures/auth/LoginSplashView.test.tsx @@ -6,13 +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 { act, render, type RenderResult } from "jest-matrix-react"; +// @vitest-environment happy-dom + +import { vi, describe, it, expect, beforeEach } from "vitest"; +import { act, render, type RenderResult } from "test-utils-rtl"; import React, { type ComponentProps } from "react"; import EventEmitter from "events"; import { CryptoEvent } from "matrix-js-sdk/src/crypto-api"; import { sleep } from "matrix-js-sdk/src/utils"; -import { LoginSplashView } from "../../../../../src/components/structures/auth/LoginSplashView"; +import { LoginSplashView } from "./LoginSplashView"; import type { MatrixClient } from "matrix-js-sdk/src/matrix"; describe("", () => { @@ -42,7 +45,7 @@ describe("", () => { }); it("Calls onLogoutClick", () => { - const onLogoutClick = jest.fn(); + const onLogoutClick = vi.fn(); const rendered = getComponent({ onLogoutClick }); expect(onLogoutClick).not.toHaveBeenCalled(); rendered.getByRole("button", { name: "Logout" }).click(); diff --git a/apps/web/test/unit-tests/components/structures/auth/Registration-test.tsx b/apps/web/src/components/structures/auth/Registration.test.tsx similarity index 85% rename from apps/web/test/unit-tests/components/structures/auth/Registration-test.tsx rename to apps/web/src/components/structures/auth/Registration.test.tsx index 9c498514de..6ddd01cae7 100644 --- a/apps/web/test/unit-tests/components/structures/auth/Registration-test.tsx +++ b/apps/web/src/components/structures/auth/Registration.test.tsx @@ -7,30 +7,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 MockedObject } from "vitest"; import React from "react"; -import { fireEvent, render, screen, waitFor, waitForElementToBeRemoved } from "jest-matrix-react"; +import { fireEvent, render, screen, waitFor, waitForElementToBeRemoved } from "test-utils-rtl"; import { createClient, type MatrixClient, MatrixError, type ValidatedAuthMetadata } from "matrix-js-sdk/src/matrix"; -import { mocked, type MockedObject } from "jest-mock-vitest-adapter"; -import fetchMock from "@fetch-mock/jest"; +import fetchMock from "@fetch-mock/vitest"; +import { getMockClientWithEventEmitter, mkServerConfig, mockPlatformPeg, unmockPlatformPeg } from "test-utils"; +import { makeDelegatedAuthMetadata } from "test-utils/auth"; -import SdkConfig, { DEFAULTS } from "../../../../../src/SdkConfig"; -import { - getMockClientWithEventEmitter, - mkServerConfig, - mockPlatformPeg, - unmockPlatformPeg, -} from "../../../../test-utils"; -import Registration from "../../../../../src/components/structures/auth/Registration"; -import { makeDelegatedAuthMetadata } from "../../../../test-utils/auth"; -import { startOAuthLogin } from "../../../../../src/utils/oauth/authorize"; +import SdkConfig, { DEFAULTS } from "../../../SdkConfig"; +import Registration from "./Registration"; +import { startOAuthLogin } from "../../../utils/oauth/authorize"; -jest.mock("../../../../../src/utils/oauth/authorize", () => ({ - startOAuthLogin: jest.fn(), +vi.mock("../../../utils/oauth/authorize", () => ({ + startOAuthLogin: vi.fn(), })); -jest.mock("matrix-js-sdk/src/matrix", () => ({ - ...jest.requireActual("matrix-js-sdk/src/matrix"), - createClient: jest.fn(), +vi.mock("matrix-js-sdk/src/matrix", async () => ({ + ...(await vi.importActual("matrix-js-sdk/src/matrix")), + createClient: vi.fn(), })); /** The matrix versions our mock server claims to support */ @@ -45,9 +42,9 @@ describe("Registration", function () { disable_custom_urls: true, }); mockClient = getMockClientWithEventEmitter({ - registerRequest: jest.fn(), - loginFlows: jest.fn(), - getVersions: jest.fn().mockResolvedValue({ versions: SERVER_SUPPORTED_MATRIX_VERSIONS }), + registerRequest: vi.fn(), + loginFlows: vi.fn(), + getVersions: vi.fn().mockResolvedValue({ versions: SERVER_SUPPORTED_MATRIX_VERSIONS }), }); mockClient.registerRequest.mockRejectedValueOnce( new MatrixError( @@ -58,7 +55,7 @@ describe("Registration", function () { ), ); mockClient.loginFlows.mockResolvedValue({ flows: [{ type: "m.login.password" }] }); - mocked(createClient).mockImplementation((opts) => { + vi.mocked(createClient).mockImplementation((opts) => { mockClient.idBaseUrl = opts.idBaseUrl; mockClient.baseUrl = opts.baseUrl; return mockClient; @@ -69,21 +66,21 @@ describe("Registration", function () { versions: SERVER_SUPPORTED_MATRIX_VERSIONS, }); mockPlatformPeg({ - startSingleSignOn: jest.fn(), + startSingleSignOn: vi.fn(), }); }); afterEach(function () { - jest.restoreAllMocks(); + vi.restoreAllMocks(); SdkConfig.reset(); // we touch the config, so clean up unmockPlatformPeg(); }); const defaultProps = { defaultDeviceDisplayName: "test-device-display-name", - onLoggedIn: jest.fn(), - onLoginClick: jest.fn(), - onServerConfigChange: jest.fn(), + onLoggedIn: vi.fn(), + onLoginClick: vi.fn(), + onServerConfigChange: vi.fn(), }; const defaultHsUrl = "https://matrix.org"; diff --git a/apps/web/test/unit-tests/components/structures/auth/__snapshots__/CompleteSecurity-test.tsx.snap b/apps/web/src/components/structures/auth/__snapshots__/CompleteSecurity.test.tsx.snap similarity index 96% rename from apps/web/test/unit-tests/components/structures/auth/__snapshots__/CompleteSecurity-test.tsx.snap rename to apps/web/src/components/structures/auth/__snapshots__/CompleteSecurity.test.tsx.snap index 636c0c6b9b..8e3d1c7bd4 100644 --- a/apps/web/test/unit-tests/components/structures/auth/__snapshots__/CompleteSecurity-test.tsx.snap +++ b/apps/web/src/components/structures/auth/__snapshots__/CompleteSecurity.test.tsx.snap @@ -1,13 +1,14 @@ -// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`CompleteSecurity Allows verifying with another device if one is available 1`] = ` +exports[`CompleteSecurity > Allows verifying with another device if one is available 1`] = `
`; -exports[`CompleteSecurity Allows verifying with recovery key if one is available 1`] = ` +exports[`CompleteSecurity > Allows verifying with recovery key if one is available 1`] = `
Renders a spinner 1`] = ` +exports[` > Renders a spinner 1`] = `
Renders a spinner 1`] = ` `; -exports[` Renders an error message 1`] = ` +exports[` > Renders an error message 1`] = `
key.split("|", 2)[1]); + +// Set up a stub module API (so the i18n API exists) +window.mxModuleApi = { i18n: new I18nApi() } as ModuleApiType; diff --git a/apps/web/src/test/setupTests.ts b/apps/web/src/test/setupTests.ts index cbb6a4386d..6368d6e52b 100644 --- a/apps/web/src/test/setupTests.ts +++ b/apps/web/src/test/setupTests.ts @@ -12,6 +12,12 @@ import SdkConfig, { DEFAULTS } from "../SdkConfig"; import "./setupGlobals.ts"; import { setupLanguageMock } from "./setupLanguage.ts"; +declare global { + var IS_REACT_ACT_ENVIRONMENT: boolean; +} + +globalThis.IS_REACT_ACT_ENVIRONMENT = true; + manageFetchMockGlobally(); beforeEach(() => { diff --git a/apps/web/test/setup/adapter.ts b/apps/web/test/setup/adapter.ts index 4d741df47b..9d6c08acff 100644 --- a/apps/web/test/setup/adapter.ts +++ b/apps/web/test/setup/adapter.ts @@ -5,7 +5,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 { vi, expect as viExpect, beforeAll as viBeforeAll, afterAll as viAfterAll } from "vitest"; +import { vi } from "vitest"; +import * as vitest from "vitest"; import { mocked as jestMocked } from "jest-mock"; export const isJest = typeof jest !== "undefined"; @@ -25,9 +26,17 @@ const adapter = { const mocked = adapter.mocked; export { adapter as vi, mocked }; -const _expect = isJest ? (expect as unknown as typeof viExpect) : viExpect; -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 }; +const _expect = isJest ? (expect as unknown as typeof vitest.expect) : vitest.expect; +const _beforeAll = isJest ? (beforeAll as unknown as typeof vitest.beforeAll) : vitest.beforeAll; +const _afterAll = isJest ? (afterAll as unknown as typeof vitest.afterAll) : vitest.afterAll; +const _beforeEach = isJest ? (beforeEach as unknown as typeof vitest.beforeEach) : vitest.beforeEach; +const _afterEach = isJest ? (afterEach as unknown as typeof vitest.afterEach) : vitest.afterEach; +export { + _expect as expect, + _beforeAll as beforeAll, + _afterAll as afterAll, + _beforeEach as beforeEach, + _afterEach as afterEach, +}; export { type Mocked, type MockedObject } from "vitest"; diff --git a/apps/web/test/test-utils/utilities.ts b/apps/web/test/test-utils/utilities.ts index 15b0b9807f..16fd6c5d39 100644 --- a/apps/web/test/test-utils/utilities.ts +++ b/apps/web/test/test-utils/utilities.ts @@ -13,7 +13,7 @@ import { type ActionPayload } from "../../src/dispatcher/payloads"; import defaultDispatcher from "../../src/dispatcher/dispatcher"; import { type DispatcherAction } from "../../src/dispatcher/actions"; import Modal from "../../src/Modal"; -import { vi } from "../setup/adapter.ts"; +import { vi, beforeEach, afterEach } from "../setup/adapter.ts"; export const emitPromise = (e: EventEmitter, k: string | symbol) => new Promise((r) => e.once(k, r));