You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
403 lines
11 KiB
403 lines
11 KiB
import { UserStatus } from '@rocket.chat/core-typings';
|
|
import { mockAppRoot } from '@rocket.chat/mock-providers';
|
|
import { useStream } from '@rocket.chat/ui-contexts';
|
|
import { renderHook, act, waitFor } from '@testing-library/react';
|
|
|
|
import type { RoomMember } from './useMembersList';
|
|
import { useMembersList } from './useMembersList';
|
|
|
|
type MembersPage = {
|
|
offset: number;
|
|
count: number;
|
|
total: number;
|
|
members: Array<{
|
|
_id: string;
|
|
username: string;
|
|
roles: string[];
|
|
status?: UserStatus;
|
|
}>;
|
|
};
|
|
|
|
jest.mock('@rocket.chat/ui-contexts', () => {
|
|
const originalModule = jest.requireActual('@rocket.chat/ui-contexts');
|
|
|
|
return {
|
|
__esModule: true,
|
|
...originalModule,
|
|
useStream: jest.fn(),
|
|
};
|
|
});
|
|
|
|
const mockUseStream = useStream as jest.MockedFunction<typeof useStream>;
|
|
const mockDMMembersEndpoint = jest.fn();
|
|
const mockRoomMembersEndpoint = jest.fn();
|
|
|
|
describe('useMembersList', () => {
|
|
let fakeMembersPage1: MembersPage;
|
|
let fakeMembersPage2: MembersPage;
|
|
|
|
const wrapper = mockAppRoot()
|
|
.withJohnDoe()
|
|
.withSetting('UI_Use_Real_Name', false)
|
|
.withEndpoint('GET', '/v1/im.members', (_params) => {
|
|
mockDMMembersEndpoint();
|
|
return fakeMembersPage1 as any;
|
|
})
|
|
.withEndpoint('GET', '/v1/rooms.membersOrderedByRole', (_params) => {
|
|
mockRoomMembersEndpoint();
|
|
return fakeMembersPage1 as any;
|
|
});
|
|
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
|
|
fakeMembersPage1 = {
|
|
offset: 0,
|
|
count: 3,
|
|
total: 5,
|
|
members: [
|
|
{ _id: 'user1', username: 'alex', roles: ['owner'], status: UserStatus.ONLINE },
|
|
{ _id: 'user2', username: 'bob', roles: ['leader'], status: UserStatus.OFFLINE },
|
|
{ _id: 'user3', username: 'john', roles: ['moderator'], status: UserStatus.OFFLINE },
|
|
],
|
|
};
|
|
|
|
fakeMembersPage2 = {
|
|
offset: 3,
|
|
count: 2,
|
|
total: 5,
|
|
members: [
|
|
{ _id: 'user4', username: 'chris', roles: [], status: UserStatus.ONLINE },
|
|
{ _id: 'user5', username: 'zoe', roles: [], status: UserStatus.OFFLINE },
|
|
],
|
|
};
|
|
|
|
mockUseStream.mockReturnValue(() => () => undefined);
|
|
});
|
|
|
|
it('fetches members using the correct endpoint for roomType c', async () => {
|
|
const { result } = renderHook(
|
|
() =>
|
|
useMembersList({
|
|
rid: 'room123',
|
|
type: 'all',
|
|
limit: 3,
|
|
debouncedText: '',
|
|
roomType: 'c',
|
|
}),
|
|
{ wrapper: wrapper.build() },
|
|
);
|
|
|
|
await expect(result.current.isLoading).toBe(true);
|
|
|
|
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
expect(mockDMMembersEndpoint).not.toHaveBeenCalled();
|
|
expect(mockRoomMembersEndpoint).toHaveBeenCalled();
|
|
|
|
expect(result.current.data?.pages[0].members).toHaveLength(fakeMembersPage1.members.length);
|
|
});
|
|
|
|
it('fetches members using the correct endpoint for roomType p', async () => {
|
|
const { result } = renderHook(
|
|
() =>
|
|
useMembersList({
|
|
rid: 'room123',
|
|
type: 'all',
|
|
limit: 3,
|
|
debouncedText: '',
|
|
roomType: 'p',
|
|
}),
|
|
{ wrapper: wrapper.build() },
|
|
);
|
|
|
|
expect(result.current.isLoading).toBe(true);
|
|
|
|
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
|
|
expect(mockRoomMembersEndpoint).toHaveBeenCalled();
|
|
expect(mockDMMembersEndpoint).not.toHaveBeenCalled();
|
|
|
|
expect(result.current.data?.pages[0].members).toHaveLength(fakeMembersPage1.members.length);
|
|
});
|
|
|
|
it('fetches from /v1/im.members if roomType is d', async () => {
|
|
const { result } = renderHook(
|
|
() =>
|
|
useMembersList({
|
|
rid: 'directRoomId',
|
|
type: 'all',
|
|
limit: 3,
|
|
debouncedText: '',
|
|
roomType: 'd',
|
|
}),
|
|
{ wrapper: wrapper.build() },
|
|
);
|
|
|
|
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
|
|
expect(mockDMMembersEndpoint).toHaveBeenCalled();
|
|
expect(mockRoomMembersEndpoint).not.toHaveBeenCalled();
|
|
|
|
expect(result.current.data?.pages[0].members).toHaveLength(fakeMembersPage1.members.length);
|
|
});
|
|
|
|
it('applies pagination with fetchNextPage', async () => {
|
|
const { result } = renderHook(
|
|
() =>
|
|
useMembersList({
|
|
rid: 'room123',
|
|
type: 'all',
|
|
limit: 3,
|
|
debouncedText: '',
|
|
roomType: 'c',
|
|
}),
|
|
{
|
|
wrapper: wrapper
|
|
.withEndpoint('GET', '/v1/rooms.membersOrderedByRole', ({ offset }) => {
|
|
if (offset === 0) {
|
|
return fakeMembersPage1 as any;
|
|
}
|
|
if (offset === 3) {
|
|
return fakeMembersPage2 as any;
|
|
}
|
|
return {
|
|
members: [],
|
|
offset,
|
|
count: 0,
|
|
total: 5,
|
|
};
|
|
})
|
|
.build(),
|
|
},
|
|
);
|
|
|
|
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
expect(result.current.data?.pages[0].members).toHaveLength(3);
|
|
|
|
await act(async () => {
|
|
await result.current.fetchNextPage();
|
|
});
|
|
|
|
await waitFor(() => expect(result.current.isFetchingNextPage).toBe(false));
|
|
await waitFor(() => expect(result.current.data?.pages).toHaveLength(2));
|
|
expect(result.current.data?.pages[1].members).toHaveLength(2);
|
|
});
|
|
|
|
it('subscribes to "roles-change" and unsubscribes on unmount', async () => {
|
|
const unsubscribeMock = jest.fn();
|
|
const subscribeMock = jest.fn().mockReturnValue(unsubscribeMock);
|
|
|
|
mockUseStream.mockReturnValue(subscribeMock);
|
|
|
|
const { unmount } = renderHook(
|
|
() =>
|
|
useMembersList({
|
|
rid: 'room123',
|
|
type: 'all',
|
|
limit: 3,
|
|
debouncedText: '',
|
|
roomType: 'c',
|
|
}),
|
|
{ wrapper: wrapper.build() },
|
|
);
|
|
|
|
await waitFor(() => expect(subscribeMock).toHaveBeenCalledWith('roles-change', expect.any(Function)));
|
|
|
|
unmount();
|
|
expect(unsubscribeMock).toHaveBeenCalled();
|
|
});
|
|
|
|
it('updates member roles in the cache on roles-change', async () => {
|
|
const subscribeMock = jest.fn();
|
|
let rolesChangeCallback: ((payload: any) => void) | undefined = undefined;
|
|
|
|
subscribeMock.mockImplementation((eventName, cb) => {
|
|
if (eventName === 'roles-change') {
|
|
rolesChangeCallback = cb;
|
|
}
|
|
return () => undefined; // unsubscribe mock
|
|
});
|
|
mockUseStream.mockReturnValue(subscribeMock);
|
|
|
|
const { result } = renderHook(
|
|
() =>
|
|
useMembersList({
|
|
rid: 'room123',
|
|
type: 'all',
|
|
limit: 3,
|
|
debouncedText: '',
|
|
roomType: 'c',
|
|
}),
|
|
{ wrapper: wrapper.build() },
|
|
);
|
|
|
|
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
let user2 = result.current.data?.pages[0].members.find((m) => m._id === 'user2') as RoomMember;
|
|
expect(user2?.roles).toEqual(['leader']);
|
|
|
|
// Simulate a roles-change event "added" for user2 -> 'owner'
|
|
await act(async () => {
|
|
rolesChangeCallback?.({
|
|
type: 'added',
|
|
scope: 'room123',
|
|
u: { _id: 'user2' },
|
|
_id: 'owner',
|
|
});
|
|
});
|
|
|
|
user2 = result.current.data?.pages[0].members.find((m) => m._id === 'user2') as RoomMember;
|
|
|
|
await waitFor(() => expect(user2?.roles).toContain('owner'));
|
|
await waitFor(() => expect(user2?.roles).toContain('leader'));
|
|
});
|
|
|
|
it('sorts members list cache by "roles > status > username/name" logic on roles-change', async () => {
|
|
const customPage = {
|
|
offset: 0,
|
|
count: 6,
|
|
total: 6,
|
|
members: [
|
|
{ _id: 'u1', username: 'mark', roles: ['owner'], status: UserStatus.OFFLINE },
|
|
{ _id: 'u2', username: 'michael', roles: ['leader'], status: UserStatus.OFFLINE },
|
|
{ _id: 'u3', username: 'karl', roles: ['moderator'], status: UserStatus.ONLINE },
|
|
{ _id: 'u4', username: 'bob', roles: ['moderator'], status: UserStatus.OFFLINE },
|
|
{ _id: 'u5', username: 'alex', roles: [], status: UserStatus.OFFLINE },
|
|
{ _id: 'u6', username: 'john', roles: [], status: UserStatus.ONLINE },
|
|
],
|
|
};
|
|
|
|
let rolesChangeCallback: ((payload: any) => void) | undefined;
|
|
mockUseStream.mockReturnValue((eventName, cb) => {
|
|
if (eventName === 'roles-change') {
|
|
rolesChangeCallback = cb as any;
|
|
}
|
|
return () => undefined;
|
|
});
|
|
|
|
const { result } = renderHook(
|
|
() =>
|
|
useMembersList({
|
|
rid: 'room123',
|
|
type: 'all',
|
|
limit: 5,
|
|
debouncedText: '',
|
|
roomType: 'c',
|
|
}),
|
|
{
|
|
wrapper: wrapper.withEndpoint('GET', '/v1/rooms.membersOrderedByRole', (_params) => customPage as any).build(),
|
|
},
|
|
);
|
|
|
|
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
|
|
let memberIds = result.current.data?.pages[0].members.map((m) => m._id);
|
|
expect(memberIds).toEqual(['u1', 'u2', 'u3', 'u4', 'u5', 'u6']);
|
|
|
|
// Simulate giving user 'alex/u5' the "moderator" role.
|
|
// That should push 'alex/u5' to the third of the sorted list -
|
|
// after mark/u1 and michael/u2 (since owner), after karl/u3 (since online) and before bob/u4 (since sort by username).
|
|
act(() => {
|
|
rolesChangeCallback?.({
|
|
type: 'added',
|
|
scope: 'room123',
|
|
u: { _id: 'u5' },
|
|
_id: 'moderator',
|
|
});
|
|
});
|
|
|
|
await waitFor(() => {
|
|
memberIds = result.current.data?.pages[0].members.map((m) => m._id);
|
|
expect(memberIds).toEqual(['u1', 'u2', 'u3', 'u5', 'u4', 'u6']);
|
|
});
|
|
});
|
|
|
|
it('re-sorts members globally across multiple pages on roles-change and verifies exact array match', async () => {
|
|
const firstPage = {
|
|
offset: 0,
|
|
count: 2,
|
|
total: 4,
|
|
members: [
|
|
{ _id: 'userA', username: 'adam', roles: ['owner'], status: UserStatus.OFFLINE },
|
|
{ _id: 'userB', username: 'bea', roles: ['moderator'], status: UserStatus.OFFLINE },
|
|
],
|
|
};
|
|
|
|
const secondPage = {
|
|
offset: 2,
|
|
count: 2,
|
|
total: 4,
|
|
members: [
|
|
{ _id: 'userC', username: 'charlie', roles: [], status: UserStatus.ONLINE },
|
|
{ _id: 'userD', username: 'david', roles: [], status: UserStatus.OFFLINE },
|
|
],
|
|
};
|
|
|
|
let rolesChangeCallback: ((payload: any) => void) | undefined;
|
|
|
|
mockUseStream.mockReturnValue((eventName, cb) => {
|
|
if (eventName === 'roles-change') {
|
|
rolesChangeCallback = cb as any;
|
|
}
|
|
return () => undefined;
|
|
});
|
|
|
|
const testWrapper = wrapper.withEndpoint('GET', '/v1/rooms.membersOrderedByRole', ({ offset }) => {
|
|
if (offset === 0) {
|
|
return firstPage as any;
|
|
}
|
|
if (offset === 2) {
|
|
return secondPage as any;
|
|
}
|
|
return { offset, count: 2, total: 4, members: [] } as any;
|
|
});
|
|
|
|
const { result } = renderHook(
|
|
() =>
|
|
useMembersList({
|
|
rid: 'room123',
|
|
type: 'all',
|
|
limit: 2,
|
|
debouncedText: '',
|
|
roomType: 'c',
|
|
}),
|
|
{ wrapper: testWrapper.build() },
|
|
);
|
|
|
|
// Page 1
|
|
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
expect(result.current.data?.pages).toHaveLength(1);
|
|
expect(result.current.data?.pages[0].members.map((m) => m._id)).toStrictEqual(['userA', 'userB']);
|
|
|
|
// Page 2
|
|
await act(async () => {
|
|
await result.current.fetchNextPage();
|
|
});
|
|
|
|
await waitFor(() => expect(result.current.isFetchingNextPage).toBe(false));
|
|
await waitFor(() => expect(result.current.data?.pages).toHaveLength(2));
|
|
expect(result.current.data?.pages[1].members.map((m) => m._id)).toStrictEqual(['userC', 'userD']);
|
|
|
|
// Give userC the "owner" role
|
|
await act(async () => {
|
|
await rolesChangeCallback?.({
|
|
type: 'added',
|
|
scope: 'room123',
|
|
u: { _id: 'userC' },
|
|
_id: 'owner',
|
|
});
|
|
});
|
|
|
|
expect(result.current.data?.pages).toHaveLength(2);
|
|
|
|
const userC = result.current.data?.pages.flatMap((page) => page.members).find((m) => m._id === 'userC') as RoomMember | null;
|
|
await waitFor(() => expect(userC?.roles).toContain('owner'));
|
|
|
|
// userC now has 'owner' + ONLINE => sits at the top after sorting
|
|
// then userA (owner), then userB (moderator), then userD (member)
|
|
expect(result.current.data?.pages[0].members).toHaveLength(2);
|
|
expect(result.current.data?.pages[1].members).toHaveLength(2);
|
|
|
|
await waitFor(() => expect(result.current.data?.pages[0].members.map((m) => m._id)).toStrictEqual(['userC', 'userA']));
|
|
await waitFor(() => expect(result.current.data?.pages[1].members.map((m) => m._id)).toStrictEqual(['userB', 'userD']));
|
|
});
|
|
});
|
|
|