Migrate more jest tests to vitest (#33898)

* Migrate more jest tests to vitest

* Fix jest config

* Fix jest config

* Make remaining jest tests type-happy
This commit is contained in:
Michael Telatynski
2026-06-19 15:34:18 +00:00
committed by GitHub
parent fab71c80ed
commit 45234b9c94
62 changed files with 625 additions and 488 deletions
@@ -0,0 +1,82 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
// @vitest-environment happy-dom
import { vi, describe, it, expect } from "vitest";
import defaultDispatcher from "./dispatcher";
import { Action } from "./actions";
import { AsyncActionPayload } from "./payloads";
describe("MatrixDispatcher", () => {
it("should throw error if unregistering unknown token", () => {
expect(() => defaultDispatcher.unregister("not-a-real-token")).toThrow(
"Dispatcher.unregister(...): 'not-a-real-token' does not map to a registered callback.",
);
});
it("should execute callbacks in registered order", async () => {
const deferred1 = Promise.withResolvers<number>();
const deferred2 = Promise.withResolvers<number>();
const fn1 = vi.fn(() => deferred1.resolve(1));
const fn2 = vi.fn(() => deferred2.resolve(2));
defaultDispatcher.register(fn1);
defaultDispatcher.register(fn2);
defaultDispatcher.dispatch({ action: Action.OnLoggedIn });
const res = await Promise.race([deferred1.promise, deferred2.promise]);
expect(res).toBe(1);
});
it("should skip the queue for the given callback", async () => {
const deferred1 = Promise.withResolvers<number>();
const deferred2 = Promise.withResolvers<number>();
const fn1 = vi.fn(() => deferred1.resolve(1));
const fn2 = vi.fn(() => deferred2.resolve(2));
defaultDispatcher.register(() => {
defaultDispatcher.waitFor([id2]);
});
defaultDispatcher.register(fn1);
const id2 = defaultDispatcher.register(fn2);
defaultDispatcher.dispatch({ action: Action.OnLoggedIn });
const res = await Promise.race([deferred1.promise, deferred2.promise]);
expect(res).toBe(2);
});
it("should not fire callback which was added during a dispatch", () => {
const fn2 = vi.fn();
defaultDispatcher.register(() => {
defaultDispatcher.register(fn2);
});
defaultDispatcher.dispatch({ action: Action.OnLoggedIn }, true);
expect(fn2).not.toHaveBeenCalled();
});
it("should handle AsyncActionPayload", () => {
const fn = vi.fn();
defaultDispatcher.register(fn);
const readyFn = vi.fn((dispatch) => {
dispatch({ action: "test" });
});
defaultDispatcher.dispatch(new AsyncActionPayload(readyFn), true);
expect(fn).toHaveBeenLastCalledWith(expect.objectContaining({ action: "test" }));
});
});
+193
View File
@@ -0,0 +1,193 @@
/*
Copyright 2025 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// @vitest-environment happy-dom
import { vi, describe, it, expect, afterEach, type Mocked } from "vitest";
import {
Direction,
type MatrixClient,
type IEvent,
MatrixEvent,
type Room,
ClientEvent,
SyncState,
} from "matrix-js-sdk/src/matrix";
import EventIndex from "./EventIndex.ts";
import {
emitPromise,
getMockClientWithEventEmitter,
mockClientMethodsRooms,
mockPlatformPeg,
} from "../../test/test-utils";
import type BaseEventIndexManager from "./BaseEventIndexManager.ts";
import { type ICrawlerCheckpoint } from "./BaseEventIndexManager.ts";
import SettingsStore from "../settings/SettingsStore.ts";
afterEach(() => {
vi.restoreAllMocks();
});
describe("EventIndex", () => {
it("crawls through the loaded checkpoints", async () => {
const mockIndexingManager = {
loadCheckpoints: vi.fn(),
removeCrawlerCheckpoint: vi.fn(),
isEventIndexEmpty: vi.fn().mockResolvedValue(false),
} as any as Mocked<BaseEventIndexManager>;
mockPlatformPeg({ getEventIndexingManager: () => mockIndexingManager });
const room1 = { roomId: "!room1:id" } as any as Room;
const room2 = { roomId: "!room2:id" } as any as Room;
const mockClient = getMockClientWithEventEmitter({
getEventMapper: () => (obj: Partial<IEvent>) => new MatrixEvent(obj),
createMessagesRequest: vi.fn(),
...mockClientMethodsRooms([room1, room2]),
});
vi.spyOn(SettingsStore, "getValueAt").mockImplementation((_level, settingName): any => {
if (settingName === "crawlerSleepTime") return 0;
return undefined;
});
mockIndexingManager.loadCheckpoints.mockResolvedValue([
{ roomId: "!room1:id", token: "token1", direction: Direction.Backward } as ICrawlerCheckpoint,
{ roomId: "!room2:id", token: "token2", direction: Direction.Forward } as ICrawlerCheckpoint,
]);
const indexer = new EventIndex();
await indexer.init();
let changedCheckpointPromise = emitPromise(indexer, "changedCheckpoint") as Promise<Room>;
indexer.startCrawler();
// Mock out the /messags request, and wait for the crawler to hit the first room
const mock1 = mockCreateMessagesRequest(mockClient);
let changedCheckpoint = await changedCheckpointPromise;
expect(changedCheckpoint.roomId).toEqual("!room1:id");
await mock1.called;
expect(mockClient.createMessagesRequest).toHaveBeenCalledWith("!room1:id", "token1", 100, "b");
// Continue, and wait for the crawler to hit the second room
changedCheckpointPromise = emitPromise(indexer, "changedCheckpoint") as Promise<Room>;
mock1.resolve({ chunk: [] });
changedCheckpoint = await changedCheckpointPromise;
expect(changedCheckpoint.roomId).toEqual("!room2:id");
// Mock out the /messages request again, and wait for it to be called
const mock2 = mockCreateMessagesRequest(mockClient);
await mock2.called;
expect(mockClient.createMessagesRequest).toHaveBeenCalledWith("!room2:id", "token2", 100, "f");
});
it("adds checkpoints for the encrypted rooms after the first sync", async () => {
const mockIndexingManager = {
loadCheckpoints: vi.fn().mockResolvedValue([]),
isEventIndexEmpty: vi.fn().mockResolvedValue(true),
addCrawlerCheckpoint: vi.fn(),
removeCrawlerCheckpoint: vi.fn(),
commitLiveEvents: vi.fn(),
} as any as Mocked<BaseEventIndexManager>;
mockPlatformPeg({ getEventIndexingManager: () => mockIndexingManager });
const room1 = {
roomId: "!room1:id",
getLiveTimeline: () => ({
getPaginationToken: () => "token1",
}),
} as any as Room;
const room2 = {
roomId: "!room2:id",
getLiveTimeline: () => ({
getPaginationToken: () => "token2",
}),
} as any as Room;
const mockCrypto = {
isEncryptionEnabledInRoom: vi.fn().mockResolvedValue(true),
};
const mockClient = getMockClientWithEventEmitter({
getEventMapper: () => (obj: Partial<IEvent>) => new MatrixEvent(obj),
createMessagesRequest: vi.fn(),
getCrypto: () => mockCrypto as any,
...mockClientMethodsRooms([room1, room2]),
});
const commitLiveEventsCalled = Promise.withResolvers<void>();
mockIndexingManager.commitLiveEvents.mockImplementation(async () => {
commitLiveEventsCalled.resolve();
});
const indexer = new EventIndex();
await indexer.init();
// During the first sync, some events are added to the index, meaning that `isEventIndexEmpty` will now be false.
mockIndexingManager.isEventIndexEmpty.mockResolvedValue(false);
// The first sync completes:
mockClient.emit(ClientEvent.Sync, SyncState.Syncing, null, {});
// Wait for `commitLiveEvents` to be called, by which time the checkpoints should have been added.
await commitLiveEventsCalled.promise;
expect(mockIndexingManager.addCrawlerCheckpoint).toHaveBeenCalledTimes(4);
expect(mockIndexingManager.addCrawlerCheckpoint).toHaveBeenCalledWith({
roomId: "!room1:id",
token: "token1",
direction: Direction.Backward,
fullCrawl: true,
});
expect(mockIndexingManager.addCrawlerCheckpoint).toHaveBeenCalledWith({
roomId: "!room1:id",
token: "token1",
direction: Direction.Forward,
});
expect(mockIndexingManager.addCrawlerCheckpoint).toHaveBeenCalledWith({
roomId: "!room2:id",
token: "token2",
direction: Direction.Backward,
fullCrawl: true,
});
expect(mockIndexingManager.addCrawlerCheckpoint).toHaveBeenCalledWith({
roomId: "!room2:id",
token: "token2",
direction: Direction.Forward,
});
});
});
/**
* Mock out the `createMessagesRequest` method on the client, with an implementation that will block until a resolver is called.
*
* @returns An object with the following properties:
* * `called`: A promise that resolves when `createMessagesRequest` is called.
* * `resolve`: A function that can be called to allow `createMessagesRequest` to complete.
*/
function mockCreateMessagesRequest(mockClient: Mocked<MatrixClient>): {
called: Promise<void>;
resolve: (result: any) => void;
} {
const messagesCalledPromise = Promise.withResolvers<void>();
const messagesResultPromise = Promise.withResolvers();
mockClient.createMessagesRequest.mockImplementationOnce(() => {
messagesCalledPromise.resolve();
return messagesResultPromise.promise as any;
});
return {
called: messagesCalledPromise.promise,
resolve: messagesResultPromise.resolve,
};
}
+170
View File
@@ -0,0 +1,170 @@
/*
Copyright 2024, 2025 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
// @vitest-environment happy-dom
import { vi, describe, it, expect, beforeAll, beforeEach, afterEach } from "vitest";
import { ClientEvent, type MatrixClient, type Room, SyncState } from "matrix-js-sdk/src/matrix";
import { waitFor } from "test-utils-rtl";
import type BasePlatform from "../BasePlatform";
import SdkConfig from "../SdkConfig";
import { SettingLevel } from "./SettingLevel";
import SettingsStore from "./SettingsStore";
import { mkStubRoom, mockPlatformPeg, stubClient } from "../../test/test-utils";
import { SETTINGS, type SettingKey } from "./Settings.tsx";
import MatrixClientBackedController from "./controllers/MatrixClientBackedController.ts";
const TEST_DATA = [
{
name: "Electron.showTrayIcon" as SettingKey,
level: SettingLevel.PLATFORM,
value: true,
},
];
/**
* An existing setting that has {@link IBaseSetting#supportedLevelsAreOrdered} set to true.
*/
const SETTING_NAME_WITH_CONFIG_OVERRIDE = "feature_msc3531_hide_messages_pending_moderation";
describe("SettingsStore", () => {
let platformSettings: Record<string, any>;
beforeAll(() => {
vi.clearAllMocks();
platformSettings = {};
mockPlatformPeg({
isLevelSupported: vi.fn().mockReturnValue(true),
supportsSetting: vi.fn().mockReturnValue(true),
setSettingValue: vi.fn().mockImplementation((settingName: string, value: any) => {
platformSettings[settingName] = value;
}),
getSettingValue: vi.fn().mockImplementation((settingName: string) => {
return platformSettings[settingName];
}),
reload: vi.fn(),
} as unknown as BasePlatform);
TEST_DATA.forEach((d) => {
SettingsStore.setValue(d.name, null, d.level, d.value);
});
});
beforeEach(() => {
SdkConfig.reset();
SettingsStore.reset();
});
describe("getValueAt", () => {
TEST_DATA.forEach((d) => {
it(`should return the value "${d.level}"."${d.name}"`, () => {
expect(SettingsStore.getValueAt(d.level, d.name)).toBe(d.value);
// regression test #22545
expect(SettingsStore.getValueAt(d.level, d.name)).toBe(d.value);
});
});
it(`supportedLevelsAreOrdered correctly overrides setting`, async () => {
SdkConfig.put({
features: {
[SETTING_NAME_WITH_CONFIG_OVERRIDE]: false,
},
});
await SettingsStore.setValue(SETTING_NAME_WITH_CONFIG_OVERRIDE, null, SettingLevel.DEVICE, true);
expect(SettingsStore.getValue(SETTING_NAME_WITH_CONFIG_OVERRIDE)).toBe(false);
});
it(`supportedLevelsAreOrdered doesn't incorrectly override setting`, async () => {
await SettingsStore.setValue(SETTING_NAME_WITH_CONFIG_OVERRIDE, null, SettingLevel.DEVICE, true);
expect(SettingsStore.getValueAt(SettingLevel.DEVICE, SETTING_NAME_WITH_CONFIG_OVERRIDE)).toBe(true);
});
});
describe("exportForRageshake", () => {
it("should not export settings marked as non-exportable", async () => {
await SettingsStore.setValue("userTimezone", null, SettingLevel.DEVICE, "Europe/London");
const values = JSON.parse(SettingsStore.exportForRageshake()) as Record<SettingKey, unknown>;
for (const exportedKey of Object.keys(values) as SettingKey[]) {
expect(SETTINGS[exportedKey].shouldExportToRageshake).not.toEqual(false);
}
});
});
describe("runMigrations", () => {
let client: MatrixClient;
let room: Room;
beforeEach(() => {
client = stubClient();
room = mkStubRoom("!room:example.org", "Room", client);
client.getRooms = vi.fn().mockReturnValue([room]);
client.getRoom = vi.fn().mockReturnValue(room);
});
afterEach(() => {
vi.restoreAllMocks();
});
describe("Migrate media preview configuration", () => {
beforeEach(() => {
MatrixClientBackedController.matrixClient = client;
client.getAccountData = vi.fn().mockImplementation((type) => {
if (type === "im.vector.web.settings") {
return {
getContent: vi.fn().mockReturnValue({
showImages: false,
showAvatarsOnInvites: false,
}),
};
} else {
return undefined;
}
});
});
it("migrates media preview configuration immediately", async () => {
client.setAccountData = vi.fn();
SettingsStore.runMigrations(false);
expect(client.setAccountData).toHaveBeenCalledWith("io.element.msc4278.media_preview_config", {
invite_avatars: "off",
media_previews: "off",
});
});
it("migrates media preview configuration once client is ready", async () => {
client.setAccountData = vi.fn();
const mockInitialSync = (client.isInitialSyncComplete = vi.fn().mockReturnValue(false));
SettingsStore.runMigrations(false);
mockInitialSync.mockReturnValue(true);
client.emit(ClientEvent.Sync, SyncState.Prepared, null);
// Update is asynchronous
await waitFor(() => {
expect(client.setAccountData).toHaveBeenCalledWith("io.element.msc4278.media_preview_config", {
invite_avatars: "off",
media_previews: "off",
});
});
});
it("does not migrate media preview configuration if the session is fresh", async () => {
client.setAccountData = vi.fn();
SettingsStore.runMigrations(true);
client.emit(ClientEvent.Sync, SyncState.Prepared, null);
expect(client.setAccountData).not.toHaveBeenCalled();
});
it("does not migrate media preview configuration if the account data is already set", async () => {
client.setAccountData = vi.fn();
client.getAccountData = vi.fn().mockReturnValue({});
SettingsStore.runMigrations(false);
client.emit(ClientEvent.Sync, SyncState.Prepared, null);
expect(client.setAccountData).not.toHaveBeenCalled();
});
});
});
});
+2 -1
View File
@@ -10,6 +10,8 @@ Please see LICENSE files in the repository root for full details.
import { logger } from "matrix-js-sdk/src/logger";
import { type ReactNode } from "react";
import { ClientEvent } from "matrix-js-sdk/src/matrix";
// Imports directly from shared-components to avoid an import cycle
import { _t } from "@element-hq/web-shared-components";
import DeviceSettingsHandler from "./handlers/DeviceSettingsHandler";
import RoomDeviceSettingsHandler from "./handlers/RoomDeviceSettingsHandler";
@@ -18,7 +20,6 @@ import RoomAccountSettingsHandler from "./handlers/RoomAccountSettingsHandler";
import AccountSettingsHandler from "./handlers/AccountSettingsHandler";
import RoomSettingsHandler from "./handlers/RoomSettingsHandler";
import ConfigSettingsHandler from "./handlers/ConfigSettingsHandler";
import { _t } from "../languageHandler";
import dis from "../dispatcher/dispatcher";
import {
type IFeature,
@@ -0,0 +1,40 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { describe, it, expect } from "vitest";
import { ImageSize, suggestedSize } from "./ImageSize";
describe("ImageSize", () => {
describe("suggestedSize", () => {
it("constrains width", () => {
const size = suggestedSize(ImageSize.Normal, { w: 648, h: 162 });
expect(size).toStrictEqual({ w: 324, h: 81 });
});
it("constrains height", () => {
const size = suggestedSize(ImageSize.Normal, { w: 162, h: 648 });
expect(size).toStrictEqual({ w: 81, h: 324 });
});
it("constrains width in large mode", () => {
const size = suggestedSize(ImageSize.Large, { w: 2400, h: 1200 });
expect(size).toStrictEqual({ w: 800, h: 400 });
});
it("returns max values if content size is not specified", () => {
const size = suggestedSize(ImageSize.Normal, {});
expect(size).toStrictEqual({ w: 324, h: 324 });
});
it("returns integer values", () => {
const size = suggestedSize(ImageSize.Normal, { w: 642, h: 350 }); // does not divide evenly
expect(size).toStrictEqual({ w: 324, h: 176 });
});
it("returns integer values for portrait images", () => {
const size = suggestedSize(ImageSize.Normal, { w: 720, h: 1280 });
expect(size).toStrictEqual({ w: 182, h: 324 });
});
});
});
@@ -0,0 +1,212 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2021 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
// @vitest-environment happy-dom
import { describe, it, expect } from "vitest";
import SettingsStore from "../SettingsStore";
import ThemeWatcher from "./ThemeWatcher";
import { type SettingLevel } from "../SettingLevel";
import { type SettingKey, type Settings } from "../Settings.tsx";
function makeMatchMedia(values: any) {
class FakeMediaQueryList {
matches: false;
media?: null;
onchange?: null;
addListener() {}
removeListener() {}
addEventListener() {}
removeEventListener() {}
dispatchEvent() {
return true;
}
constructor(query: string) {
this.matches = values[query];
}
}
return function matchMedia(query: string) {
return new FakeMediaQueryList(query) as unknown as MediaQueryList;
};
}
function makeGetValue(values: any): any {
return function getValue<S extends SettingKey>(
settingName: S,
_roomId: string | null = null,
_excludeDefault = false,
): Settings[S] {
return values[settingName];
};
}
function makeGetValueAt(values: any) {
return function getValueAt(
_level: SettingLevel,
settingName: string,
_roomId: string | null = null,
_explicit = false,
_excludeDefault = false,
): any {
return values[settingName];
};
}
describe("ThemeWatcher", function () {
it("should choose a light theme by default", () => {
// Given no system settings
global.matchMedia = makeMatchMedia({});
// Then getEffectiveTheme returns light
const themeWatcher = new ThemeWatcher();
expect(themeWatcher.getEffectiveTheme()).toBe("light");
});
it("should choose default theme if system settings are inconclusive", () => {
// Given no system settings but we asked to use them
global.matchMedia = makeMatchMedia({});
SettingsStore.getValue = makeGetValue({
use_system_theme: true,
theme: "light",
});
// Then getEffectiveTheme returns light
const themeWatcher = new ThemeWatcher();
expect(themeWatcher.getEffectiveTheme()).toBe("light");
});
it("should choose a dark theme if that is selected", () => {
// Given system says light high contrast but theme is set to dark
global.matchMedia = makeMatchMedia({
"(prefers-contrast: more)": true,
"(prefers-color-scheme: light)": true,
});
SettingsStore.getValueAt = makeGetValueAt({ theme: "dark" });
// Then getEffectiveTheme returns dark
const themeWatcher = new ThemeWatcher();
expect(themeWatcher.getEffectiveTheme()).toBe("dark");
});
it("should choose a light theme if that is selected", () => {
// Given system settings say dark high contrast but theme set to light
global.matchMedia = makeMatchMedia({
"(prefers-contrast: more)": true,
"(prefers-color-scheme: dark)": true,
});
SettingsStore.getValueAt = makeGetValueAt({ theme: "light" });
// Then getEffectiveTheme returns light
const themeWatcher = new ThemeWatcher();
expect(themeWatcher.getEffectiveTheme()).toBe("light");
});
it("should choose a light-high-contrast theme if that is selected", () => {
// Given system settings say dark and theme set to light-high-contrast
global.matchMedia = makeMatchMedia({ "(prefers-color-scheme: dark)": true });
SettingsStore.getValueAt = makeGetValueAt({ theme: "light-high-contrast" });
// Then getEffectiveTheme returns light-high-contrast
const themeWatcher = new ThemeWatcher();
expect(themeWatcher.getEffectiveTheme()).toBe("light-high-contrast");
});
it("should choose a light theme if system prefers it (via default)", () => {
// Given system prefers lightness, even though we did not
// click "Use system theme" or choose a theme explicitly
global.matchMedia = makeMatchMedia({ "(prefers-color-scheme: light)": true });
SettingsStore.getValueAt = makeGetValueAt({});
SettingsStore.getValue = makeGetValue({ use_system_theme: true });
// Then getEffectiveTheme returns light
const themeWatcher = new ThemeWatcher();
expect(themeWatcher.getEffectiveTheme()).toBe("light");
});
it("should choose a dark theme if system prefers it (via default)", () => {
// Given system prefers darkness, even though we did not
// click "Use system theme" or choose a theme explicitly
global.matchMedia = makeMatchMedia({ "(prefers-color-scheme: dark)": true });
SettingsStore.getValueAt = makeGetValueAt({});
SettingsStore.getValue = makeGetValue({ use_system_theme: true });
// Then getEffectiveTheme returns dark
const themeWatcher = new ThemeWatcher();
expect(themeWatcher.getEffectiveTheme()).toBe("dark");
});
it("should choose a light theme if system prefers it (explicit)", () => {
// Given system prefers lightness
global.matchMedia = makeMatchMedia({ "(prefers-color-scheme: light)": true });
SettingsStore.getValueAt = makeGetValueAt({ use_system_theme: true });
SettingsStore.getValue = makeGetValue({ use_system_theme: true });
// Then getEffectiveTheme returns light
const themeWatcher = new ThemeWatcher();
expect(themeWatcher.getEffectiveTheme()).toBe("light");
});
it("should choose a dark theme if system prefers it (explicit)", () => {
// Given system prefers darkness
global.matchMedia = makeMatchMedia({ "(prefers-color-scheme: dark)": true });
SettingsStore.getValueAt = makeGetValueAt({ use_system_theme: true });
SettingsStore.getValue = makeGetValue({ use_system_theme: true });
// Then getEffectiveTheme returns dark
const themeWatcher = new ThemeWatcher();
expect(themeWatcher.getEffectiveTheme()).toBe("dark");
});
it("should choose a high-contrast theme if system prefers it", () => {
// Given system prefers high contrast and light
global.matchMedia = makeMatchMedia({
"(prefers-contrast: more)": true,
"(prefers-color-scheme: light)": true,
});
SettingsStore.getValueAt = makeGetValueAt({ use_system_theme: true });
SettingsStore.getValue = makeGetValue({ use_system_theme: true });
// Then getEffectiveTheme returns light-high-contrast
const themeWatcher = new ThemeWatcher();
expect(themeWatcher.getEffectiveTheme()).toBe("light-high-contrast");
});
it("should not choose a high-contrast theme if not available", () => {
// Given system prefers high contrast and dark, but we don't (yet)
// have a high-contrast dark theme
global.matchMedia = makeMatchMedia({
"(prefers-contrast: more)": true,
"(prefers-color-scheme: dark)": true,
});
SettingsStore.getValueAt = makeGetValueAt({ use_system_theme: true });
SettingsStore.getValue = makeGetValue({ use_system_theme: true });
// Then getEffectiveTheme returns dark
const themeWatcher = new ThemeWatcher();
expect(themeWatcher.getEffectiveTheme()).toBe("dark");
});
it("should identify custom dark themes as dark", () => {
SettingsStore.getValueAt = makeGetValueAt({ use_system_theme: false, theme: "custom-darkula" });
SettingsStore.getValue = makeGetValue({
custom_themes: [
{
name: "darkula",
is_dark: true,
},
],
});
const themeWatcher = new ThemeWatcher();
expect(themeWatcher.getEffectiveTheme()).toBe("custom-darkula");
expect(themeWatcher.isUserOnDarkTheme()).toBe(true);
});
});
+17 -1
View File
@@ -5,9 +5,12 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { beforeEach } from "vitest";
import { vi, beforeEach } from "vitest";
import fetchMock, { manageFetchMockGlobally } from "@fetch-mock/vitest";
import { mocks } from "../../test/setup/mocks.ts";
import SdkConfig, { DEFAULTS } from "../SdkConfig";
manageFetchMockGlobally();
beforeEach(() => {
@@ -16,3 +19,16 @@ beforeEach(() => {
fetchMock.catch(404);
fetchMock.mockGlobal();
});
// set up AudioContext API mock
vi.stubGlobal("AudioContext", function () {
return mocks.AudioContext;
});
if (globalThis.window === undefined) {
// We are in a node environment, stub a basic window so singletons work
vi.stubGlobal("window", {});
}
// uninitialised SdkConfig causes lots of warnings in console, init with defaults
SdkConfig.put(DEFAULTS);
+297
View File
@@ -0,0 +1,297 @@
/*
* Copyright 2024 New Vector Ltd.
* Copyright 2024 The Matrix.org Foundation C.I.C.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
// @vitest-environment happy-dom
import { vi, describe, it, test, expect, beforeEach } from "vitest";
import { EventTimeline, EventType, type IEvent, type MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
import { createTestClient } from "../../test/test-utils";
import PinningUtils from "./PinningUtils";
import SettingsStore from "../settings/SettingsStore";
import { isContentActionable } from "./EventUtils";
import { ReadPinsEventId } from "../components/views/right_panel/types";
vi.mock("./EventUtils", () => {
return {
isContentActionable: vi.fn(),
canPinEvent: vi.fn(),
};
});
describe("PinningUtils", () => {
const roomId = "!room:example.org";
const userId = "@alice:example.org";
const mockedIsContentActionable = vi.mocked(isContentActionable);
let matrixClient: MatrixClient;
let room: Room;
/**
* Create a pinned event with the given content.
* @param content
*/
function makePinEvent(content?: Partial<IEvent>) {
return new MatrixEvent({
type: EventType.RoomMessage,
sender: userId,
content: {
body: "First pinned message",
msgtype: "m.text",
},
room_id: roomId,
origin_server_ts: 0,
event_id: "$eventId",
...content,
});
}
beforeEach(() => {
// Enable feature pinning
vi.spyOn(SettingsStore, "getValue").mockReturnValue(true);
mockedIsContentActionable.mockImplementation(() => true);
matrixClient = createTestClient();
room = new Room(roomId, matrixClient, userId);
matrixClient.getRoom = vi.fn().mockReturnValue(room);
vi.spyOn(
matrixClient.getRoom(roomId)!.getLiveTimeline().getState(EventTimeline.FORWARDS)!,
"mayClientSendStateEvent",
).mockReturnValue(true);
});
describe("isUnpinnable", () => {
test.each(PinningUtils.PINNABLE_EVENT_TYPES)("should return true for pinnable event types", (eventType) => {
const event = makePinEvent({ type: eventType });
expect(PinningUtils.isUnpinnable(event)).toBe(true);
});
test("should return false for a non pinnable event type", () => {
const event = makePinEvent({ type: EventType.RoomCreate });
expect(PinningUtils.isUnpinnable(event)).toBe(false);
});
test("should return true for a redacted event", () => {
const event = makePinEvent({ unsigned: { redacted_because: "because" as unknown as IEvent } });
expect(PinningUtils.isUnpinnable(event)).toBe(true);
});
});
describe("isPinnable", () => {
test.each(PinningUtils.PINNABLE_EVENT_TYPES)("should return true for pinnable event types", (eventType) => {
const event = makePinEvent({ type: eventType });
expect(PinningUtils.isPinnable(event)).toBe(true);
});
test("should return false for a redacted event", () => {
const event = makePinEvent({ unsigned: { redacted_because: "because" as unknown as IEvent } });
expect(PinningUtils.isPinnable(event)).toBe(false);
});
});
describe("isPinned", () => {
test("should return false if no room", () => {
matrixClient.getRoom = vi.fn().mockReturnValue(undefined);
const event = makePinEvent();
expect(PinningUtils.isPinned(matrixClient, event)).toBe(false);
});
test("should return false if no pinned event", () => {
vi.spyOn(
matrixClient.getRoom(roomId)!.getLiveTimeline().getState(EventTimeline.FORWARDS)!,
"getStateEvents",
).mockReturnValue(null);
const event = makePinEvent();
expect(PinningUtils.isPinned(matrixClient, event)).toBe(false);
});
test("should return false if pinned events do not contain the event id", () => {
vi.spyOn(
matrixClient.getRoom(roomId)!.getLiveTimeline().getState(EventTimeline.FORWARDS)!,
"getStateEvents",
).mockReturnValue({
// @ts-ignore
getContent: () => ({ pinned: ["$otherEventId"] }),
});
const event = makePinEvent();
expect(PinningUtils.isPinned(matrixClient, event)).toBe(false);
});
test("should return true if pinned events contains the event id", () => {
const event = makePinEvent();
vi.spyOn(
matrixClient.getRoom(roomId)!.getLiveTimeline().getState(EventTimeline.FORWARDS)!,
"getStateEvents",
).mockReturnValue({
// @ts-ignore
getContent: () => ({ pinned: [event.getId()] }),
});
expect(PinningUtils.isPinned(matrixClient, event)).toBe(true);
});
});
describe("canPin & canUnpin", () => {
describe("canPin", () => {
test("should return false if event is not actionable", () => {
mockedIsContentActionable.mockImplementation(() => false);
const event = makePinEvent();
expect(PinningUtils.canPin(matrixClient, event)).toBe(false);
});
test("should return false if no room", () => {
matrixClient.getRoom = vi.fn().mockReturnValue(undefined);
const event = makePinEvent();
expect(PinningUtils.canPin(matrixClient, event)).toBe(false);
});
test("should return false if client cannot send state event", () => {
vi.spyOn(
matrixClient.getRoom(roomId)!.getLiveTimeline().getState(EventTimeline.FORWARDS)!,
"mayClientSendStateEvent",
).mockReturnValue(false);
const event = makePinEvent();
expect(PinningUtils.canPin(matrixClient, event)).toBe(false);
});
test("should return false if event is not pinnable", () => {
const event = makePinEvent({ type: EventType.RoomCreate });
expect(PinningUtils.canPin(matrixClient, event)).toBe(false);
});
test("should return true if all conditions are met", () => {
const event = makePinEvent();
expect(PinningUtils.canPin(matrixClient, event)).toBe(true);
});
});
describe("canUnpin", () => {
test("should return false if event is not unpinnable", () => {
const event = makePinEvent({ type: EventType.RoomCreate });
expect(PinningUtils.canUnpin(matrixClient, event)).toBe(false);
});
test("should return true if all conditions are met", () => {
const event = makePinEvent();
expect(PinningUtils.canUnpin(matrixClient, event)).toBe(true);
});
test("should return true if the event is redacted", () => {
const event = makePinEvent({ unsigned: { redacted_because: "because" as unknown as IEvent } });
expect(PinningUtils.canUnpin(matrixClient, event)).toBe(true);
});
});
});
describe("pinOrUnpinEvent", () => {
test("should do nothing if no room", async () => {
matrixClient.getRoom = vi.fn().mockReturnValue(undefined);
const event = makePinEvent();
await PinningUtils.pinOrUnpinEvent(matrixClient, event);
expect(matrixClient.sendStateEvent).not.toHaveBeenCalled();
});
test("should do nothing if no event id", async () => {
const event = makePinEvent({ event_id: undefined });
await PinningUtils.pinOrUnpinEvent(matrixClient, event);
expect(matrixClient.sendStateEvent).not.toHaveBeenCalled();
});
test("should pin the event if not pinned", async () => {
vi.spyOn(
matrixClient.getRoom(roomId)!.getLiveTimeline().getState(EventTimeline.FORWARDS)!,
"getStateEvents",
).mockReturnValue({
// @ts-ignore
getContent: () => ({ pinned: ["$otherEventId"] }),
});
vi.spyOn(room, "getAccountData").mockReturnValue({
getContent: vi.fn().mockReturnValue({
event_ids: ["$otherEventId"],
}),
} as unknown as MatrixEvent);
const event = makePinEvent();
await PinningUtils.pinOrUnpinEvent(matrixClient, event);
expect(matrixClient.setRoomAccountData).toHaveBeenCalledWith(roomId, ReadPinsEventId, {
event_ids: ["$otherEventId", event.getId()],
});
expect(matrixClient.sendStateEvent).toHaveBeenCalledWith(
roomId,
EventType.RoomPinnedEvents,
{ pinned: ["$otherEventId", event.getId()] },
"",
);
});
test("should unpin the event if already pinned", async () => {
const event = makePinEvent();
vi.spyOn(
matrixClient.getRoom(roomId)!.getLiveTimeline().getState(EventTimeline.FORWARDS)!,
"getStateEvents",
).mockReturnValue({
// @ts-ignore
getContent: () => ({ pinned: [event.getId(), "$otherEventId"] }),
});
await PinningUtils.pinOrUnpinEvent(matrixClient, event);
expect(matrixClient.sendStateEvent).toHaveBeenCalledWith(
roomId,
EventType.RoomPinnedEvents,
{ pinned: ["$otherEventId"] },
"",
);
});
});
describe("userHasPinOrUnpinPermission", () => {
test("should return true if user can pin or unpin", () => {
expect(PinningUtils.userHasPinOrUnpinPermission(matrixClient, room)).toBe(true);
});
test("should return false if client cannot send state event", () => {
vi.spyOn(
matrixClient.getRoom(roomId)!.getLiveTimeline().getState(EventTimeline.FORWARDS)!,
"mayClientSendStateEvent",
).mockReturnValue(false);
expect(PinningUtils.userHasPinOrUnpinPermission(matrixClient, room)).toBe(false);
});
});
describe("unpinAllEvents", () => {
it("should unpin all events in the given room", async () => {
await PinningUtils.unpinAllEvents(matrixClient, roomId);
expect(matrixClient.sendStateEvent).toHaveBeenCalledWith(
roomId,
EventType.RoomPinnedEvents,
{ pinned: [] },
"",
);
});
});
});
+102
View File
@@ -0,0 +1,102 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2021 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { vi, describe, it, expect, afterEach } from "vitest";
import { Singleflight } from "./Singleflight";
describe("Singleflight", () => {
afterEach(() => {
Singleflight.forgetAll();
});
it("should throw for bad context variables", () => {
const permutations: [object | null, string | null][] = [
[null, null],
[{}, null],
[null, "test"],
];
for (const p of permutations) {
expect(() => Singleflight.for(p[0], p[1])).toThrow("An instance and key must be supplied");
}
});
it("should execute the function once", () => {
const instance = {};
const key = "test";
const val = {}; // unique object for reference check
const fn = vi.fn().mockReturnValue(val);
const sf = Singleflight.for(instance, key);
const r1 = sf.do(fn);
expect(r1).toBe(val);
expect(fn.mock.calls.length).toBe(1);
const r2 = sf.do(fn);
expect(r2).toBe(val);
expect(fn.mock.calls.length).toBe(1);
});
it("should execute the function once, even with new contexts", () => {
const instance = {};
const key = "test";
const val = {}; // unique object for reference check
const fn = vi.fn().mockReturnValue(val);
let sf = Singleflight.for(instance, key);
const r1 = sf.do(fn);
expect(r1).toBe(val);
expect(fn.mock.calls.length).toBe(1);
sf = Singleflight.for(instance, key); // RESET FOR TEST
const r2 = sf.do(fn);
expect(r2).toBe(val);
expect(fn.mock.calls.length).toBe(1);
});
it("should execute the function twice if the result was forgotten", () => {
const instance = {};
const key = "test";
const val = {}; // unique object for reference check
const fn = vi.fn().mockReturnValue(val);
const sf = Singleflight.for(instance, key);
const r1 = sf.do(fn);
expect(r1).toBe(val);
expect(fn.mock.calls.length).toBe(1);
sf.forget();
const r2 = sf.do(fn);
expect(r2).toBe(val);
expect(fn.mock.calls.length).toBe(2);
});
it("should execute the function twice if the instance was forgotten", () => {
const instance = {};
const key = "test";
const val = {}; // unique object for reference check
const fn = vi.fn().mockReturnValue(val);
const sf = Singleflight.for(instance, key);
const r1 = sf.do(fn);
expect(r1).toBe(val);
expect(fn.mock.calls.length).toBe(1);
Singleflight.forgetAllFor(instance);
const r2 = sf.do(fn);
expect(r2).toBe(val);
expect(fn.mock.calls.length).toBe(2);
});
it("should execute the function twice if everything was forgotten", () => {
const instance = {};
const key = "test";
const val = {}; // unique object for reference check
const fn = vi.fn().mockReturnValue(val);
const sf = Singleflight.for(instance, key);
const r1 = sf.do(fn);
expect(r1).toBe(val);
expect(fn.mock.calls.length).toBe(1);
Singleflight.forgetAll();
const r2 = sf.do(fn);
expect(r2).toBe(val);
expect(fn.mock.calls.length).toBe(2);
});
});
+22
View File
@@ -0,0 +1,22 @@
/*
Copyright 2026 Element Creations Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
// @vitest-environment happy-dom
import { vi, describe, it, expect } from "vitest";
import { onSubmitPreventDefault } from "./form.ts";
describe("onSubmitPreventDefault", () => {
it("should preventDefault", () => {
const event = new SubmitEvent("submit");
const spy = vi.spyOn(event, "preventDefault");
onSubmitPreventDefault(event);
expect(spy).toHaveBeenCalled();
});
});
+221
View File
@@ -0,0 +1,221 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2021 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { describe, it, expect } from "vitest";
import { EnhancedMap, mapDiff } from "./maps";
describe("maps", () => {
describe("mapDiff", () => {
it("should indicate no differences when the pointers are the same", () => {
const a = new Map([
[1, 1],
[2, 2],
[3, 3],
]);
const result = mapDiff(a, a);
expect(result).toBeDefined();
expect(result.added).toBeDefined();
expect(result.removed).toBeDefined();
expect(result.changed).toBeDefined();
expect(result.added).toHaveLength(0);
expect(result.removed).toHaveLength(0);
expect(result.changed).toHaveLength(0);
});
it("should indicate no differences when there are none", () => {
const a = new Map([
[1, 1],
[2, 2],
[3, 3],
]);
const b = new Map([
[1, 1],
[2, 2],
[3, 3],
]);
const result = mapDiff(a, b);
expect(result).toBeDefined();
expect(result.added).toBeDefined();
expect(result.removed).toBeDefined();
expect(result.changed).toBeDefined();
expect(result.added).toHaveLength(0);
expect(result.removed).toHaveLength(0);
expect(result.changed).toHaveLength(0);
});
it("should indicate added properties", () => {
const a = new Map([
[1, 1],
[2, 2],
[3, 3],
]);
const b = new Map([
[1, 1],
[2, 2],
[3, 3],
[4, 4],
]);
const result = mapDiff(a, b);
expect(result).toBeDefined();
expect(result.added).toBeDefined();
expect(result.removed).toBeDefined();
expect(result.changed).toBeDefined();
expect(result.added).toHaveLength(1);
expect(result.removed).toHaveLength(0);
expect(result.changed).toHaveLength(0);
expect(result.added).toEqual([4]);
});
it("should indicate removed properties", () => {
const a = new Map([
[1, 1],
[2, 2],
[3, 3],
]);
const b = new Map([
[1, 1],
[2, 2],
]);
const result = mapDiff(a, b);
expect(result).toBeDefined();
expect(result.added).toBeDefined();
expect(result.removed).toBeDefined();
expect(result.changed).toBeDefined();
expect(result.added).toHaveLength(0);
expect(result.removed).toHaveLength(1);
expect(result.changed).toHaveLength(0);
expect(result.removed).toEqual([3]);
});
it("should indicate changed properties", () => {
const a = new Map([
[1, 1],
[2, 2],
[3, 3],
]);
const b = new Map([
[1, 1],
[2, 2],
[3, 4],
]); // note change
const result = mapDiff(a, b);
expect(result).toBeDefined();
expect(result.added).toBeDefined();
expect(result.removed).toBeDefined();
expect(result.changed).toBeDefined();
expect(result.added).toHaveLength(0);
expect(result.removed).toHaveLength(0);
expect(result.changed).toHaveLength(1);
expect(result.changed).toEqual([3]);
});
it("should indicate changed, added, and removed properties", () => {
const a = new Map([
[1, 1],
[2, 2],
[3, 3],
]);
const b = new Map([
[1, 1],
[2, 8],
[4, 4],
]); // note change
const result = mapDiff(a, b);
expect(result).toBeDefined();
expect(result.added).toBeDefined();
expect(result.removed).toBeDefined();
expect(result.changed).toBeDefined();
expect(result.added).toHaveLength(1);
expect(result.removed).toHaveLength(1);
expect(result.changed).toHaveLength(1);
expect(result.added).toEqual([4]);
expect(result.removed).toEqual([3]);
expect(result.changed).toEqual([2]);
});
it("should indicate changes for difference in pointers", () => {
const a = new Map([[1, {}]]); // {} always creates a new object
const b = new Map([[1, {}]]);
const result = mapDiff(a, b);
expect(result).toBeDefined();
expect(result.added).toBeDefined();
expect(result.removed).toBeDefined();
expect(result.changed).toBeDefined();
expect(result.added).toHaveLength(0);
expect(result.removed).toHaveLength(0);
expect(result.changed).toHaveLength(1);
expect(result.changed).toEqual([1]);
});
});
describe("EnhancedMap", () => {
// Most of these tests will make sure it implements the Map<K, V> class
it("should be empty by default", () => {
const result = new EnhancedMap();
expect(result.size).toBe(0);
});
it("should use the provided entries", () => {
const obj = { a: 1, b: 2 };
const result = new EnhancedMap(Object.entries(obj));
expect(result.size).toBe(2);
expect(result.get("a")).toBe(1);
expect(result.get("b")).toBe(2);
});
it("should create keys if they do not exist", () => {
const key = "a";
const val = {}; // we'll check pointers
const result = new EnhancedMap<string, any>();
expect(result.size).toBe(0);
let get = result.getOrCreate(key, val);
expect(get).toBeDefined();
expect(get).toBe(val);
expect(result.size).toBe(1);
get = result.getOrCreate(key, 44); // specifically change `val`
expect(get).toBeDefined();
expect(get).toBe(val);
expect(result.size).toBe(1);
get = result.get(key); // use the base class function
expect(get).toBeDefined();
expect(get).toBe(val);
expect(result.size).toBe(1);
});
it("should proxy remove to delete and return it", () => {
const val = {};
const result = new EnhancedMap<string, any>();
result.set("a", val);
expect(result.size).toBe(1);
const removed = result.remove("a");
expect(result.size).toBe(0);
expect(removed).toBeDefined();
expect(removed).toBe(val);
});
it("should support removing unknown keys", () => {
const val = {};
const result = new EnhancedMap<string, any>();
result.set("a", val);
expect(result.size).toBe(1);
const removed = result.remove("not-a");
expect(result.size).toBe(1);
expect(removed).not.toBeDefined();
});
});
});
-2
View File
@@ -6,8 +6,6 @@ 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, afterAll } from "vitest";
import fetchMock from "@fetch-mock/vitest";