Move more tests from Jest to Vitest (#34181)

* Move more tests from Jest to Vitest

* Iterate
This commit is contained in:
Michael Telatynski
2026-07-08 12:15:28 +00:00
committed by GitHub
parent d5932851cb
commit 67a20c57a3
22 changed files with 262 additions and 195 deletions
@@ -1,72 +0,0 @@
/*
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.
*/
import { renderHook } from "jest-matrix-react";
import { type MatrixClient, NotificationCountType, Room } from "matrix-js-sdk/src/matrix";
import { useRoomThreadNotifications } from "../../../../src/hooks/room/useRoomThreadNotifications";
import { stubClient } from "../../../test-utils";
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
import { NotificationLevel } from "../../../../src/stores/notifications/NotificationLevel";
import { populateThread } from "../../../test-utils/threads";
function render(room: Room) {
return renderHook(() => useRoomThreadNotifications(room));
}
describe("useRoomThreadNotifications", () => {
let cli: MatrixClient;
let room: Room;
beforeEach(() => {
stubClient();
cli = MatrixClientPeg.safeGet();
cli.supportsThreads = () => true;
room = new Room("!room:server", cli, cli.getSafeUserId());
});
it("returns none if no thread in the room has notifications", async () => {
const { result } = render(room);
expect(result.current).toBe(NotificationLevel.None);
});
it("returns none if the thread hasn't a notification anymore", async () => {
room.setThreadUnreadNotificationCount("flooble", NotificationCountType.Highlight, 0);
const { result } = render(room);
expect(result.current).toBe(NotificationLevel.None);
});
it("returns red if a thread in the room has a highlight notification", async () => {
room.setThreadUnreadNotificationCount("flooble", NotificationCountType.Highlight, 1);
const { result } = render(room);
expect(result.current).toBe(NotificationLevel.Highlight);
});
it("returns grey if a thread in the room has a normal notification", async () => {
room.setThreadUnreadNotificationCount("flooble", NotificationCountType.Total, 1);
const { result } = render(room);
expect(result.current).toBe(NotificationLevel.Notification);
});
it("returns activity if a thread in the room unread messages", async () => {
await populateThread({
room,
client: cli,
authorId: cli.getSafeUserId(),
participantUserIds: ["@alice:server.org"],
});
const { result } = render(room);
expect(result.current).toBe(NotificationLevel.Activity);
});
});
@@ -1,129 +0,0 @@
/*
Copyright 2025 New Vector 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.
*/
import { act, renderHook, waitFor } from "jest-matrix-react";
import { JoinRule, MatrixEvent, type MatrixClient, type Room } from "matrix-js-sdk/src/matrix";
import { useMediaVisible } from "../../../src/hooks/useMediaVisible";
import { createTestClient, mkStubRoom, withClientContextRenderOptions } from "../../test-utils";
import { type MediaPreviewConfig, MediaPreviewValue } from "../../../src/@types/media_preview";
import MediaPreviewConfigController from "../../../src/settings/controllers/MediaPreviewConfigController";
import SettingsStore from "../../../src/settings/SettingsStore";
const EVENT_ID = "$fibble:example.org";
const ROOM_ID = "!foobar:example.org";
describe("useMediaVisible", () => {
let matrixClient: MatrixClient;
let room: Room;
const mediaPreviewConfig: MediaPreviewConfig = MediaPreviewConfigController.default;
function render({ sender }: { sender?: string } = {}) {
return renderHook(
() =>
useMediaVisible(
new MatrixEvent({
event_id: EVENT_ID,
room_id: ROOM_ID,
sender,
}),
),
withClientContextRenderOptions(matrixClient),
);
}
function renderWithoutEvent() {
return renderHook(() => useMediaVisible(), withClientContextRenderOptions(matrixClient));
}
beforeEach(() => {
matrixClient = createTestClient();
room = mkStubRoom(ROOM_ID, undefined, matrixClient);
matrixClient.getRoom = jest.fn().mockReturnValue(room);
const origFn = SettingsStore.getValue;
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting, ...args) => {
if (setting === "mediaPreviewConfig") {
return mediaPreviewConfig;
}
return origFn(setting, ...args);
});
});
afterEach(() => {
jest.restoreAllMocks();
});
it("should display media by default", () => {
const [visible] = render().result.current;
expect(visible).toEqual(true);
});
it("should use the global rule when no event is provided", () => {
mediaPreviewConfig.media_previews = MediaPreviewValue.Off;
expect(renderWithoutEvent().result.current[0]).toEqual(false);
mediaPreviewConfig.media_previews = MediaPreviewValue.On;
expect(renderWithoutEvent().result.current[0]).toEqual(true);
});
it("should hide media when media previews are Off", () => {
mediaPreviewConfig.media_previews = MediaPreviewValue.Off;
const [visible] = render().result.current;
expect(visible).toEqual(false);
});
it("should always show media sent by us", () => {
mediaPreviewConfig.media_previews = MediaPreviewValue.Off;
const [visible] = render({ sender: matrixClient.getUserId()! }).result.current;
expect(visible).toEqual(true);
});
it.each([[JoinRule.Invite], [JoinRule.Knock], [JoinRule.Restricted]])(
"should display media when media previews are Private and the join rule is %s",
(rule) => {
mediaPreviewConfig.media_previews = MediaPreviewValue.Private;
room.currentState.getJoinRule = jest.fn().mockReturnValue(rule);
const [visible] = render().result.current;
expect(visible).toEqual(true);
},
);
it.each([[JoinRule.Public], ["anything_else"]])(
"should hide media when media previews are Private and the join rule is %s",
(rule) => {
mediaPreviewConfig.media_previews = MediaPreviewValue.Private;
room.currentState.getJoinRule = jest.fn().mockReturnValue(rule);
const [visible] = render().result.current;
expect(visible).toEqual(false);
},
);
it("should hide media after function is called", async () => {
const { result } = render();
expect(result.current[0]).toEqual(true);
expect(result.current[1]).toBeDefined();
act(() => {
result.current[1]!(false);
});
await waitFor(() => {
expect(result.current[0]).toEqual(false);
});
});
it("should show media after function is called", async () => {
mediaPreviewConfig.media_previews = MediaPreviewValue.Off;
const { result } = render();
expect(result.current[0]).toEqual(false);
expect(result.current[1]).toBeDefined();
act(() => {
result.current[1]!(true);
});
await waitFor(() => {
expect(result.current[0]).toEqual(true);
});
});
});
@@ -1,145 +0,0 @@
/*
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.
*/
import { waitFor, renderHook } from "jest-matrix-react";
import { type IPushRules, type MatrixClient, PushRuleKind, RuleId } from "matrix-js-sdk/src/matrix";
import { useNotificationSettings } from "../../../src/hooks/useNotificationSettings";
import { MatrixClientPeg } from "../../../src/MatrixClientPeg";
import {
DefaultNotificationSettings,
type NotificationSettings,
} from "../../../src/models/notificationsettings/NotificationSettings";
import { StandardActions } from "../../../src/notifications/StandardActions";
import { RoomNotifState } from "../../../src/RoomNotifs";
import { stubClient } from "../../test-utils";
const expectedModel: NotificationSettings = {
globalMute: false,
defaultLevels: {
dm: RoomNotifState.AllMessages,
room: RoomNotifState.MentionsOnly,
},
sound: {
calls: "ring",
mentions: "default",
people: undefined,
},
activity: {
bot_notices: false,
invite: true,
status_event: false,
},
mentions: {
user: true,
room: true,
keywords: true,
},
keywords: ["justjann3", "justj4nn3", "justj4nne", "Janne", "J4nne", "Jann3", "jann3", "j4nne", "janne"],
};
describe("useNotificationSettings", () => {
let cli: MatrixClient;
let pushRules: IPushRules;
beforeAll(async () => {
pushRules = (await import("../models/notificationsettings/pushrules_sample.json")) as IPushRules;
});
beforeEach(() => {
stubClient();
cli = MatrixClientPeg.safeGet();
cli.getPushRules = jest.fn(cli.getPushRules).mockResolvedValue(pushRules);
cli.supportsIntentionalMentions = jest.fn(cli.supportsIntentionalMentions).mockReturnValue(false);
});
it("correctly parses model", async () => {
const { result } = renderHook(() => useNotificationSettings(cli));
expect(result.current.model).toEqual(null);
await waitFor(() => expect(result.current.model).toEqual(expectedModel));
expect(result.current.hasPendingChanges).toBeFalsy();
});
it("correctly generates change calls", async () => {
const addPushRule = jest.fn(cli.addPushRule);
cli.addPushRule = addPushRule;
const deletePushRule = jest.fn(cli.deletePushRule);
cli.deletePushRule = deletePushRule;
const setPushRuleEnabled = jest.fn(cli.setPushRuleEnabled);
cli.setPushRuleEnabled = setPushRuleEnabled;
const setPushRuleActions = jest.fn(cli.setPushRuleActions);
cli.setPushRuleActions = setPushRuleActions;
const { result } = renderHook(() => useNotificationSettings(cli));
expect(result.current.model).toEqual(null);
await waitFor(() => expect(result.current.model).toEqual(expectedModel));
expect(result.current.hasPendingChanges).toBeFalsy();
await result.current.reconcile(DefaultNotificationSettings);
await waitFor(() => expect(result.current.hasPendingChanges).toBeFalsy());
expect(addPushRule).toHaveBeenCalledTimes(0);
expect(deletePushRule).toHaveBeenCalledTimes(9);
expect(deletePushRule).toHaveBeenCalledWith("global", PushRuleKind.ContentSpecific, "justjann3");
expect(deletePushRule).toHaveBeenCalledWith("global", PushRuleKind.ContentSpecific, "justj4nn3");
expect(deletePushRule).toHaveBeenCalledWith("global", PushRuleKind.ContentSpecific, "justj4nne");
expect(deletePushRule).toHaveBeenCalledWith("global", PushRuleKind.ContentSpecific, "Janne");
expect(deletePushRule).toHaveBeenCalledWith("global", PushRuleKind.ContentSpecific, "J4nne");
expect(deletePushRule).toHaveBeenCalledWith("global", PushRuleKind.ContentSpecific, "Jann3");
expect(deletePushRule).toHaveBeenCalledWith("global", PushRuleKind.ContentSpecific, "jann3");
expect(deletePushRule).toHaveBeenCalledWith("global", PushRuleKind.ContentSpecific, "j4nne");
expect(deletePushRule).toHaveBeenCalledWith("global", PushRuleKind.ContentSpecific, "janne");
expect(setPushRuleEnabled).toHaveBeenCalledTimes(6);
expect(setPushRuleEnabled).toHaveBeenCalledWith(
"global",
PushRuleKind.Underride,
RuleId.EncryptedMessage,
true,
);
expect(setPushRuleEnabled).toHaveBeenCalledWith("global", PushRuleKind.Underride, RuleId.Message, true);
expect(setPushRuleEnabled).toHaveBeenCalledWith("global", PushRuleKind.Underride, RuleId.EncryptedDM, true);
expect(setPushRuleEnabled).toHaveBeenCalledWith("global", PushRuleKind.Underride, RuleId.DM, true);
expect(setPushRuleEnabled).toHaveBeenCalledWith("global", PushRuleKind.Override, RuleId.SuppressNotices, false);
expect(setPushRuleEnabled).toHaveBeenCalledWith("global", PushRuleKind.Override, RuleId.InviteToSelf, true);
expect(setPushRuleActions).toHaveBeenCalledTimes(6);
expect(setPushRuleActions).toHaveBeenCalledWith(
"global",
PushRuleKind.Underride,
RuleId.EncryptedMessage,
StandardActions.ACTION_NOTIFY,
);
expect(setPushRuleActions).toHaveBeenCalledWith(
"global",
PushRuleKind.Underride,
RuleId.Message,
StandardActions.ACTION_NOTIFY,
);
expect(setPushRuleActions).toHaveBeenCalledWith(
"global",
PushRuleKind.Underride,
RuleId.EncryptedDM,
StandardActions.ACTION_NOTIFY_DEFAULT_SOUND,
);
expect(setPushRuleActions).toHaveBeenCalledWith(
"global",
PushRuleKind.Underride,
RuleId.DM,
StandardActions.ACTION_NOTIFY_DEFAULT_SOUND,
);
expect(setPushRuleActions).toHaveBeenCalledWith(
"global",
PushRuleKind.Override,
RuleId.SuppressNotices,
StandardActions.ACTION_DONT_NOTIFY,
);
expect(setPushRuleActions).toHaveBeenCalledWith(
"global",
PushRuleKind.Override,
RuleId.InviteToSelf,
StandardActions.ACTION_NOTIFY_DEFAULT_SOUND,
);
});
});
@@ -1,110 +0,0 @@
/*
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 { waitFor, renderHook, act } from "jest-matrix-react";
import { type EmptyObject, type MatrixClient } from "matrix-js-sdk/src/matrix";
import { useProfileInfo } from "../../../src/hooks/useProfileInfo";
import { MatrixClientPeg } from "../../../src/MatrixClientPeg";
import { stubClient } from "../../test-utils/test-utils";
function render() {
return renderHook(() => useProfileInfo());
}
describe("useProfileInfo", () => {
let cli: MatrixClient;
beforeEach(() => {
stubClient();
cli = MatrixClientPeg.safeGet();
cli.getProfileInfo = (query) => {
return Promise.resolve({
avatar_url: undefined,
displayname: query,
});
};
});
it("should display user profile when searching", async () => {
const query = "@user:home.server";
const { result } = render();
act(() => {
result.current.search({ query });
});
await waitFor(() => {
expect(result.current.ready).toBe(true);
expect(result.current.profile?.display_name).toBe(query);
});
});
it("should work with empty queries", async () => {
const query = "";
const { result } = render();
act(() => {
result.current.search({ query });
});
await waitFor(() => expect(result.current.ready).toBe(true));
expect(result.current.profile).toBeNull();
});
it("should treat invalid mxids as empty queries", async () => {
const queries = ["@user", "user@home.server"];
for (const query of queries) {
const { result } = render();
act(() => {
result.current.search({ query });
});
await waitFor(() => expect(result.current.ready).toBe(true));
expect(result.current.profile).toBeNull();
}
});
it("should recover from a server exception", async () => {
cli.getProfileInfo = () => {
throw new Error("Oops");
};
const query = "@user:home.server";
const { result } = render();
act(() => {
result.current.search({ query });
});
await waitFor(() => expect(result.current.ready).toBe(true));
expect(result.current.profile).toBeNull();
});
it("should be able to handle an empty result", async () => {
cli.getProfileInfo = () => null as unknown as Promise<EmptyObject>;
const query = "@user:home.server";
const { result } = render();
act(() => {
result.current.search({ query });
});
await waitFor(() => expect(result.current.ready).toBe(true));
expect(result.current.profile?.display_name).toBeUndefined();
});
});
@@ -1,105 +0,0 @@
/*
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 { waitFor, renderHook, act } from "jest-matrix-react";
import { type IRoomDirectoryOptions, type MatrixClient } from "matrix-js-sdk/src/matrix";
import { usePublicRoomDirectory } from "../../../src/hooks/usePublicRoomDirectory";
import { MatrixClientPeg } from "../../../src/MatrixClientPeg";
import { stubClient } from "../../test-utils/test-utils";
function render() {
return renderHook(() => usePublicRoomDirectory());
}
describe("usePublicRoomDirectory", () => {
let cli: MatrixClient;
beforeEach(() => {
stubClient();
cli = MatrixClientPeg.safeGet();
cli.getDomain = () => "matrix.org";
cli.getThirdpartyProtocols = () => Promise.resolve({});
cli.publicRooms = ({ filter }: IRoomDirectoryOptions) => {
const chunk = [
{
room_id: "hello world!",
name: filter?.generic_search_term ?? "", // If the query is "" no filter is applied(an is undefined here), in keeping with the pattern let's call the room ""
world_readable: true,
guest_can_join: true,
num_joined_members: 1,
},
];
return Promise.resolve({
chunk,
total_room_count_estimate: 1,
});
};
});
it("should display public rooms when searching", async () => {
const query = "ROOM NAME";
const { result } = render();
expect(result.current.ready).toBe(false);
expect(result.current.loading).toBe(false);
act(() => {
result.current.search({
limit: 1,
query,
});
});
await waitFor(() => {
expect(result.current.ready).toBe(true);
});
expect(result.current.publicRooms[0].name).toBe(query);
});
it("should work with empty queries", async () => {
const query = "";
const { result } = render();
act(() => {
result.current.search({
limit: 1,
query,
});
});
await waitFor(() => {
expect(result.current.ready).toBe(true);
expect(result.current.publicRooms[0]?.name).toEqual(query);
});
});
it("should recover from a server exception", async () => {
cli.publicRooms = () => {
throw new Error("Oops");
};
const query = "ROOM NAME";
const { result } = render();
act(() => {
result.current.search({
limit: 1,
query,
});
});
await waitFor(() => {
expect(result.current.ready).toBe(true);
});
expect(result.current.publicRooms).toEqual([]);
});
});
@@ -1,147 +0,0 @@
/*
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.
*/
import { renderHook, waitFor } from "jest-matrix-react";
import React from "react";
import { PlatformCallType, useRoomCall } from "../../../src/hooks/room/useRoomCall";
import {
getMockClientWithEventEmitter,
mkRoom,
mockClientMethodsRooms,
mockClientMethodsServer,
mockClientMethodsUser,
MockEventEmitter,
setupAsyncStoreWithClient,
} from "../../test-utils";
import { ScopedRoomContextProvider } from "../../../src/contexts/ScopedRoomContext";
import RoomContext, { type RoomContextType } from "../../../src/contexts/RoomContext";
import { MatrixClientContextProvider } from "../../../src/components/structures/MatrixClientContextProvider";
import type LegacyCallHandler from "../../../src/LegacyCallHandler";
import { CallStore } from "../../../src/stores/CallStore";
import { SDKContextClass } from "../../../src/contexts/SDKContextClass";
describe("useRoomCall", () => {
const client = getMockClientWithEventEmitter({
...mockClientMethodsUser(),
...mockClientMethodsServer(),
...mockClientMethodsRooms(),
matrixRTC: new MockEventEmitter(),
_unstable_getRTCTransports: jest.fn().mockResolvedValue([]),
getCrypto: () => null,
});
const room = mkRoom(client, "!test-room");
// Create a stable room context for this test
const mockRoomViewStore = {
isViewingCall: jest.fn().mockReturnValue(false),
on: jest.fn(),
off: jest.fn(),
emit: jest.fn(),
};
const roomContext = {
...RoomContext,
roomId: room.roomId,
roomViewStore: mockRoomViewStore,
} as unknown as RoomContextType;
beforeEach(() => {
const callHandler = {
getCallForRoom: jest.fn().mockReturnValue(null),
isCallSidebarShown: jest.fn().mockReturnValue(true),
addListener: jest.fn(),
removeListener: jest.fn(),
on: jest.fn(),
off: jest.fn(),
};
jest.spyOn(SDKContextClass.instance, "legacyCallHandler", "get").mockReturnValue(
callHandler as unknown as LegacyCallHandler,
);
});
afterEach(() => {
jest.restoreAllMocks();
});
function render() {
return renderHook(() => useRoomCall(room), {
wrapper: ({ children }) => (
<MatrixClientContextProvider client={client}>
<ScopedRoomContextProvider {...roomContext}>{children}</ScopedRoomContextProvider>
</MatrixClientContextProvider>
),
});
}
describe("Element Call focus detection", () => {
it("Blocks Element Call if required foci are not configured", async () => {
await setupAsyncStoreWithClient(CallStore.instance, client);
const { result } = render();
await waitFor(() => expect(result.current.callOptions).toEqual([PlatformCallType.LegacyCall]));
});
it("Blocks Element Call if transport foci are the wrong type", async () => {
client._unstable_getRTCTransports.mockResolvedValue([{ type: "anything-else" }]);
await setupAsyncStoreWithClient(CallStore.instance, client);
const { result } = render();
await waitFor(() => expect(result.current.callOptions).toEqual([PlatformCallType.LegacyCall]));
});
it("Blocks Element Call if well-known foci are the wrong type", async () => {
client.getClientWellKnown.mockReturnValue({
"org.matrix.msc4143.rtc_foci": {
type: "anything-else",
},
});
await setupAsyncStoreWithClient(CallStore.instance, client);
const { result } = render();
await waitFor(() => expect(result.current.callOptions).toEqual([PlatformCallType.LegacyCall]));
});
it("Allows Element Call if foci is provided via getRTCTransports", async () => {
client._unstable_getRTCTransports.mockResolvedValue([
{ type: "livekit", livekit_service_url: "https://example.org" },
]);
await setupAsyncStoreWithClient(CallStore.instance, client);
const { result } = render();
await waitFor(() =>
expect(result.current.callOptions).toEqual([PlatformCallType.ElementCall, PlatformCallType.LegacyCall]),
);
});
it("Allows Element Call if foci is provided via .well-known", async () => {
client.getClientWellKnown.mockReturnValue({
"org.matrix.msc4143.rtc_foci": {
type: "livekit",
livekit_service_url: "https://example.org",
},
});
await setupAsyncStoreWithClient(CallStore.instance, client);
const { result } = render();
await waitFor(() =>
expect(result.current.callOptions).toEqual([PlatformCallType.ElementCall, PlatformCallType.LegacyCall]),
);
});
it("Ensure handler reacts to transport changes", async () => {
// Clear all transports
client._unstable_getRTCTransports.mockResolvedValue([]);
client.getClientWellKnown.mockReturnValue({});
await setupAsyncStoreWithClient(CallStore.instance, client);
const { result } = render();
// Ensure Element Call is not a call option.
expect(result.current.callOptions).toEqual([PlatformCallType.LegacyCall]);
// Now enable a transport and ensure that useRoomCall picks it up reactively.
client._unstable_getRTCTransports.mockResolvedValue([
{ type: "livekit", livekit_service_url: "https://example.org" },
]);
await setupAsyncStoreWithClient(CallStore.instance, client);
await waitFor(() =>
expect(result.current.callOptions).toEqual([PlatformCallType.ElementCall, PlatformCallType.LegacyCall]),
);
});
});
});
@@ -1,114 +0,0 @@
/*
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.
*/
import { waitFor, renderHook, act } from "jest-matrix-react";
import { type MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
import { KnownMembership } from "matrix-js-sdk/src/types";
import { MatrixClientPeg } from "../../../src/MatrixClientPeg";
import { stubClient } from "../../test-utils";
import { useMyRoomMembership, useRoomMemberCount, useRoomMembers } from "../../../src/hooks/useRoomMembers";
describe("useRoomMembers", () => {
function render(room: Room) {
return renderHook(() => useRoomMembers(room));
}
let cli: MatrixClient;
let room: Room;
beforeEach(() => {
stubClient();
cli = MatrixClientPeg.safeGet();
room = new Room("!room:server", cli, cli.getSafeUserId());
});
it("should update on RoomState.Members events", async () => {
const { result } = render(room);
expect(result.current).toHaveLength(0);
act(() => {
room.currentState.markOutOfBandMembersStarted();
room.currentState.setOutOfBandMembers([
new MatrixEvent({
type: "m.room.member",
state_key: "!user:server",
room_id: room.roomId,
content: {
membership: KnownMembership.Join,
},
}),
]);
});
await waitFor(() => expect(result.current).toHaveLength(1));
});
});
describe("useRoomMemberCount", () => {
function render(room: Room) {
return renderHook(() => useRoomMemberCount(room));
}
let cli: MatrixClient;
let room: Room;
beforeEach(() => {
stubClient();
cli = MatrixClientPeg.safeGet();
room = new Room("!room:server", cli, cli.getSafeUserId());
});
it("should update on RoomState.Members events", async () => {
const { result } = render(room);
expect(result.current).toBe(0);
act(() => {
room.currentState.markOutOfBandMembersStarted();
room.currentState.setOutOfBandMembers([
new MatrixEvent({
type: "m.room.member",
state_key: "!user:server",
room_id: room.roomId,
content: {
membership: KnownMembership.Join,
},
}),
]);
});
await waitFor(() => expect(result.current).toBe(1));
});
});
describe("useMyRoomMembership", () => {
function render(room: Room) {
return renderHook(() => useMyRoomMembership(room));
}
let cli: MatrixClient;
let room: Room;
beforeEach(() => {
stubClient();
cli = MatrixClientPeg.safeGet();
room = new Room("!room:server", cli, cli.getSafeUserId());
});
it("should update on RoomState.Members events", async () => {
room.updateMyMembership(KnownMembership.Join);
const { result } = render(room);
expect(result.current).toBe(KnownMembership.Join);
act(() => {
room.updateMyMembership(KnownMembership.Leave);
});
await waitFor(() => expect(result.current).toBe(KnownMembership.Leave));
});
});
@@ -1,102 +0,0 @@
/*
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.
*/
import { renderHook } from "jest-matrix-react";
import { EventStatus, NotificationCountType, PendingEventOrdering, Room } from "matrix-js-sdk/src/matrix";
import { KnownMembership } from "matrix-js-sdk/src/types";
import type { MatrixClient } from "matrix-js-sdk/src/matrix";
import { useUnreadNotifications } from "../../../src/hooks/useUnreadNotifications";
import { NotificationLevel } from "../../../src/stores/notifications/NotificationLevel";
import { mkEvent, muteRoom, stubClient } from "../../test-utils";
describe("useUnreadNotifications", () => {
let client: MatrixClient;
let room: Room;
beforeEach(() => {
client = stubClient();
room = new Room("!room:example.org", client, "@user:example.org", {
pendingEventOrdering: PendingEventOrdering.Detached,
});
});
function setUnreads(greys: number, reds: number): void {
room.setUnreadNotificationCount(NotificationCountType.Highlight, reds);
room.setUnreadNotificationCount(NotificationCountType.Total, greys);
}
it("shows nothing by default", async () => {
const { result } = renderHook(() => useUnreadNotifications(room));
const { level, symbol, count } = result.current;
expect(symbol).toBe(null);
expect(level).toBe(NotificationLevel.None);
expect(count).toBe(0);
});
it("indicates if there are unsent messages", async () => {
const event = mkEvent({
event: true,
type: "m.message",
user: "@user:example.org",
content: {},
});
event.status = EventStatus.NOT_SENT;
room.addPendingEvent(event, "txn");
const { result } = renderHook(() => useUnreadNotifications(room));
const { level, symbol, count } = result.current;
expect(symbol).toBe("!");
expect(level).toBe(NotificationLevel.Unsent);
expect(count).toBeGreaterThan(0);
});
it("indicates the user has been invited to a channel", async () => {
room.updateMyMembership(KnownMembership.Invite);
const { result } = renderHook(() => useUnreadNotifications(room));
const { level, symbol, count } = result.current;
expect(symbol).toBe("!");
expect(level).toBe(NotificationLevel.Highlight);
expect(count).toBeGreaterThan(0);
});
it("shows nothing for muted channels", async () => {
setUnreads(999, 999);
muteRoom(room);
const { result } = renderHook(() => useUnreadNotifications(room));
const { level, count } = result.current;
expect(level).toBe(NotificationLevel.None);
expect(count).toBe(0);
});
it("uses the correct number of unreads", async () => {
setUnreads(999, 0);
const { result } = renderHook(() => useUnreadNotifications(room));
const { level, count } = result.current;
expect(level).toBe(NotificationLevel.Notification);
expect(count).toBe(999);
});
it("uses the correct number of highlights", async () => {
setUnreads(0, 888);
const { result } = renderHook(() => useUnreadNotifications(room));
const { level, count } = result.current;
expect(level).toBe(NotificationLevel.Highlight);
expect(count).toBe(888);
});
});
@@ -1,85 +0,0 @@
/*
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 { waitFor, renderHook, act } from "jest-matrix-react";
import { type MatrixClient } from "matrix-js-sdk/src/matrix";
import { useUserDirectory } from "../../../src/hooks/useUserDirectory";
import { MatrixClientPeg } from "../../../src/MatrixClientPeg";
import { stubClient } from "../../test-utils";
function render() {
return renderHook(() => useUserDirectory());
}
describe("useUserDirectory", () => {
let cli: MatrixClient;
beforeEach(() => {
stubClient();
cli = MatrixClientPeg.safeGet();
cli.getDomain = () => "matrix.org";
cli.getThirdpartyProtocols = () => Promise.resolve({});
cli.searchUserDirectory = ({ term: query }) =>
Promise.resolve({
results: [
{
user_id: "@bob:matrix.org",
display_name: query,
},
],
limited: false,
});
});
it("search for users in the identity server", async () => {
const query = "Bob";
const { result } = render();
act(() => {
result.current.search({ limit: 1, query });
});
await waitFor(() => {
expect(result.current.ready).toBe(true);
expect(result.current.loading).toBe(false);
});
expect(result.current.users[0].name).toBe(query);
});
it("should work with empty queries", async () => {
const query = "";
const { result } = render();
act(() => {
result.current.search({ limit: 1, query });
});
await waitFor(() => expect(result.current.ready).toBe(true));
expect(result.current.loading).toBe(false);
expect(result.current.users).toEqual([]);
});
it("should recover from a server exception", async () => {
cli.searchUserDirectory = () => {
throw new Error("Oops");
};
const query = "Bob";
const { result } = render();
act(() => {
result.current.search({ limit: 1, query });
});
await waitFor(() => expect(result.current.ready).toBe(true));
expect(result.current.loading).toBe(false);
expect(result.current.users).toEqual([]);
});
});
@@ -1,156 +0,0 @@
/*
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.
*/
import React from "react";
import { renderHook, waitFor } from "jest-matrix-react";
import { ClientEvent } from "matrix-js-sdk/src/matrix";
import { useUserStatus } from "../../../src/hooks/useUserStatus";
import { getMockClientWithEventEmitter, mockClientMethodsUser, mockClientMethodsServer } from "../../test-utils";
import { MatrixClientContextProvider } from "../../../src/components/structures/MatrixClientContextProvider";
import SettingsStore from "../../../src/settings/SettingsStore";
import { userStatusTextWithinMaxLength } from "../../../src/utils/userStatus";
const userId = "@alice:example.com";
const client = getMockClientWithEventEmitter({
...mockClientMethodsUser(),
...mockClientMethodsServer(),
getCrypto: jest.fn().mockReturnValue(null),
doesServerSupportExtendedProfiles: jest.fn().mockResolvedValue(true),
getExtendedProfileProperty: jest.fn().mockResolvedValue(undefined),
});
function render(uid: string | undefined = userId) {
return renderHook(() => useUserStatus(uid), {
wrapper: ({ children }) => (
<MatrixClientContextProvider client={client}>{children}</MatrixClientContextProvider>
),
});
}
describe("userStatusTextWithinMaxLength", () => {
it("returns true for short text", () => {
expect(userStatusTextWithinMaxLength("on a horse")).toBe(true);
});
it("returns false for text exceeding 256 bytes", () => {
expect(userStatusTextWithinMaxLength("a".repeat(257))).toBe(false);
});
it("returns true for text exactly 256 bytes", () => {
expect(userStatusTextWithinMaxLength("a".repeat(256))).toBe(true);
});
});
describe("useUserStatus", () => {
beforeEach(() => {
jest.spyOn(SettingsStore, "getValue").mockImplementation((name): any => {
if (name === "feature_user_status") return true;
});
client.doesServerSupportExtendedProfiles.mockResolvedValue(true);
client.getExtendedProfileProperty.mockResolvedValue(undefined);
});
afterEach(() => {
jest.restoreAllMocks();
});
it("returns undefined when feature is disabled", async () => {
jest.spyOn(SettingsStore, "getValue").mockReturnValue(false);
const { result } = render();
expect(result.current).toBeUndefined();
});
it("returns undefined when userId is undefined", async () => {
const { result } = render(undefined);
expect(result.current).toBeUndefined();
});
it("returns undefined when server does not support extended profiles", async () => {
client.doesServerSupportExtendedProfiles.mockResolvedValue(false);
const { result } = render();
expect(result.current).toBeUndefined();
});
it("returns undefined when status property is not set", async () => {
client.getExtendedProfileProperty.mockResolvedValue(undefined);
const { result } = render();
await waitFor(() =>
expect(client.getExtendedProfileProperty).toHaveBeenCalledWith(userId, "org.matrix.msc4426.status"),
);
expect(result.current).toBeUndefined();
});
it("returns undefined when status is not an object", async () => {
client.getExtendedProfileProperty.mockResolvedValue("not an object");
const { result } = render();
await waitFor(() => expect(client.getExtendedProfileProperty).toHaveBeenCalled());
expect(result.current).toBeUndefined();
});
it("returns undefined when emoji is missing", async () => {
client.getExtendedProfileProperty.mockResolvedValue({ text: "on a horse" });
const { result } = render();
await waitFor(() => expect(client.getExtendedProfileProperty).toHaveBeenCalled());
expect(result.current).toBeUndefined();
});
it("returns undefined when text is missing", async () => {
client.getExtendedProfileProperty.mockResolvedValue({ emoji: "🐎" });
const { result } = render();
await waitFor(() => expect(client.getExtendedProfileProperty).toHaveBeenCalled());
expect(result.current).toBeUndefined();
});
it("returns the user status when valid", async () => {
client.getExtendedProfileProperty.mockResolvedValue({ emoji: "🐎", text: "on a horse" });
const { result } = render();
await waitFor(() => expect(result.current).toEqual({ emoji: "🐎", text: "on a horse" }));
});
it("truncates text that exceeds 256 bytes", async () => {
const longText = "a".repeat(257);
client.getExtendedProfileProperty.mockResolvedValue({ emoji: "🐎", text: longText });
const { result } = render();
await waitFor(() => expect(result.current).toEqual({ emoji: "🐎", text: `${"a".repeat(256)}` }));
});
it("returns undefined when M_NOT_FOUND error is thrown", async () => {
const error = new Error();
client.getExtendedProfileProperty.mockRejectedValue(error);
const { result } = render();
await waitFor(() => expect(client.getExtendedProfileProperty).toHaveBeenCalled());
expect(result.current).toBeUndefined();
});
it("updates status when UserProfileUpdate event is emitted", async () => {
client.getExtendedProfileProperty.mockResolvedValue({ emoji: "🐎", text: "on a horse" });
const { result } = render();
await waitFor(() => expect(result.current).toEqual({ emoji: "🐎", text: "on a horse" }));
// Simulate a profile update event
client.emit(ClientEvent.UserProfileUpdate, userId, {
"org.matrix.msc4426.status": { emoji: "😵", text: "off a horse" },
});
await waitFor(() => expect(result.current).toEqual({ emoji: "😵", text: "off a horse" }));
});
it("ignores UserProfileUpdate events for different users", async () => {
client.getExtendedProfileProperty.mockResolvedValue({ emoji: "🐎", text: "on a horse" });
const { result } = render();
await waitFor(() => expect(result.current).toEqual({ emoji: "🐎", text: "on a horse" }));
client.emit(ClientEvent.UserProfileUpdate, "@bob:example.com", {
"org.matrix.msc4426.status": { emoji: "🤷", text: "unrelated status" },
});
// Should still have original status
expect(result.current).toEqual({ emoji: "🐎", text: "on a horse" });
});
});