Add tests for usePresence

and move the slightly weird test about it working when no member is linked
onto the hook test which is what it's actually trying to test.
This commit is contained in:
David Baker
2026-06-08 19:08:50 +01:00
parent 07974c7747
commit 6a40729137
@@ -6,17 +6,20 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details. Please see LICENSE files in the repository root for full details.
*/ */
import { render, waitFor } from "jest-matrix-react"; import { act, render, renderHook, waitFor } from "jest-matrix-react";
import { mocked } from "jest-mock"; import { mocked } from "jest-mock";
import { type MatrixClient, PendingEventOrdering, Room, RoomMember, User } from "matrix-js-sdk/src/matrix"; import { type MatrixClient, PendingEventOrdering, Room, RoomMember, User, UserEvent } from "matrix-js-sdk/src/matrix";
import React from "react"; import React from "react";
import userEvent from "@testing-library/user-event";
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg"; import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
import { stubClient } from "../../../../test-utils"; import { getMockClientWithEventEmitter, stubClient } from "../../../../test-utils";
import DMRoomMap from "../../../../../src/utils/DMRoomMap"; import DMRoomMap from "../../../../../src/utils/DMRoomMap";
import WithPresenceIndicator from "../../../../../src/components/views/avatars/WithPresenceIndicator"; import WithPresenceIndicator, {
Presence,
usePresence,
} from "../../../../../src/components/views/avatars/WithPresenceIndicator";
import { isPresenceEnabled } from "../../../../../src/utils/presence"; import { isPresenceEnabled } from "../../../../../src/utils/presence";
import { getJoinedNonFunctionalMembers } from "../../../../../src/utils/room/getJoinedNonFunctionalMembers";
jest.mock("../../../../../src/utils/presence"); jest.mock("../../../../../src/utils/presence");
@@ -87,51 +90,136 @@ describe("WithPresenceIndicator", () => {
expect(asFragment()).toMatchSnapshot(); expect(asFragment()).toMatchSnapshot();
}); });
});
describe("usePresence", () => {
const ROOM_ID = "roomId";
const DM_USER_ID = "@bob:foo.bar";
let mockClient: ReturnType<typeof getMockClientWithEventEmitter>;
let room: Room;
let member: RoomMember;
let user: User;
beforeEach(() => {
mockClient = getMockClientWithEventEmitter({
getUserId: jest.fn().mockReturnValue("@alice:foo.bar"),
getUser: jest.fn().mockReturnValue(null),
store: { getPendingEvents: jest.fn().mockResolvedValue([]) },
});
room = new Room(ROOM_ID, mockClient as unknown as MatrixClient, mockClient.getUserId() ?? "");
mocked(isPresenceEnabled).mockReturnValue(true);
mocked(getJoinedNonFunctionalMembers).mockReturnValue([1, 2] as any);
user = new User(DM_USER_ID);
user.presence = "online";
member = new RoomMember(ROOM_ID, DM_USER_ID);
member.user = user;
});
afterEach(() => {
jest.restoreAllMocks();
});
it("returns null when presence is disabled", () => {
mocked(isPresenceEnabled).mockReturnValue(false);
const { result } = renderHook(() => usePresence(room, member));
expect(result.current).toBeNull();
});
it("returns null when room does not have exactly 2 members", () => {
mocked(getJoinedNonFunctionalMembers).mockReturnValue([1] as any);
const { result } = renderHook(() => usePresence(room, member));
expect(result.current).toBeNull();
});
it("returns null when member is null", () => {
const { result } = renderHook(() => usePresence(room, null));
expect(result.current).toBeNull();
});
it.each([ it.each([
["online", "Online"], ["online", Presence.Online],
["offline", "Offline"], ["offline", Presence.Offline],
["unavailable", "Away"], ["unavailable", Presence.Away],
])( ["busy", Presence.Busy],
"renders presence indicator when member.user is not linked but client has user data", ])("returns correct presence for user with '%s' presence state", (presenceStr, expectedPresence) => {
async (presenceStr, renderedStr) => { user.presence = presenceStr;
mocked(isPresenceEnabled).mockReturnValue(true); const { result } = renderHook(() => usePresence(room, member));
expect(result.current).toBe(expectedPresence);
});
const DM_USER_ID = "@bob:foo.bar"; it("returns Online when user.currentlyActive is true regardless of presence string", () => {
const dmRoomMap = { user.presence = "offline";
getUserIdForRoomId: () => { user.currentlyActive = true;
return DM_USER_ID; const { result } = renderHook(() => usePresence(room, member));
}, expect(result.current).toBe(Presence.Online);
} as unknown as DMRoomMap; });
jest.spyOn(DMRoomMap, "shared").mockReturnValue(dmRoomMap); it("updates when UserEvent.Presence fires on member.user", async () => {
user.presence = "online";
const { result } = renderHook(() => usePresence(room, member));
expect(result.current).toBe(Presence.Online);
// member.user is not set: simulates the race condition on fresh login with no cache act(() => {
// where the room list renders before member.user is linked user.presence = "offline";
room.getMember = jest.fn((userId) => { user.emit(UserEvent.Presence, null as any, user);
return new RoomMember(room.roomId, userId); });
});
// But client.getUser() has the presence data await waitFor(() => expect(result.current).toBe(Presence.Offline));
const user = new User(DM_USER_ID); });
user.presence = presenceStr;
mockClient.getUser = jest.fn((userId) => (userId === DM_USER_ID ? user : null));
const { container } = renderComponent(); it("updates when UserEvent.CurrentlyActive fires on member.user", async () => {
user.presence = "offline";
user.currentlyActive = false;
const { result } = renderHook(() => usePresence(room, member));
expect(result.current).toBe(Presence.Offline);
const presence = container.querySelector(".mx_WithPresenceIndicator_icon")!; act(() => {
expect(presence).toBeVisible(); user.currentlyActive = true;
await userEvent.hover(presence!); user.emit(UserEvent.CurrentlyActive, null as any, user);
});
const tooltip = await waitFor(() => { await waitFor(() => expect(result.current).toBe(Presence.Online));
const tooltip = document.getElementById(presence.getAttribute("aria-labelledby")!); });
expect(tooltip).toBeVisible();
return tooltip;
});
// component should fall back to reading client.getUser() which does have the presence data it("returns correct presence when member.user is not linked but client has user data", () => {
// so it should render correctly member.user = undefined;
expect(tooltip).toHaveTextContent(renderedStr); mocked(mockClient.getUser).mockImplementation((userId) => (userId === DM_USER_ID ? user : null));
},
); user.presence = "online";
const { result } = renderHook(() => usePresence(room, member));
expect(result.current).toBe("online");
});
it("updates via client-level UserEvent.Presence when member.user is not yet linked", async () => {
member.user = undefined;
mocked(mockClient.getUser).mockImplementation((userId) => (userId === DM_USER_ID ? user : null));
user.presence = "online";
const { result } = renderHook(() => usePresence(room, member));
expect(result.current).toBe(Presence.Online);
act(() => {
user.presence = "offline";
mockClient.emit(UserEvent.Presence, null as any, user);
});
await waitFor(() => expect(result.current).toBe(Presence.Offline));
});
it("does not update when client emits UserEvent.Presence for a different user", async () => {
user.presence = "online";
const { result } = renderHook(() => usePresence(room, member));
expect(result.current).toBe(Presence.Online);
act(() => {
const otherUser = new User("@other:foo.bar");
otherUser.presence = "offline";
mockClient.emit(UserEvent.Presence, null as any, otherUser);
});
expect(result.current).toBe(Presence.Online);
});
}); });