import { reactive, ref } from 'vue'; import { describe, expect, test, vi, beforeEach } from 'vitest'; vi.mock('vue-sonner', () => ({ toast: { success: vi.fn(), error: vi.fn() } })); vi.mock('vue-i18n', () => ({ useI18n: () => ({ t: (key) => key , locale: require('vue').ref('en') }), createI18n: () => ({ global: { t: (key) => key , locale: require('vue').ref('en') }, install: vi.fn() }) })); vi.mock('../../../../plugin/router', () => { const { ref: vRef } = require('vue'); return { router: { beforeEach: vi.fn(), push: vi.fn(), replace: vi.fn(), currentRoute: vRef({ path: '/', name: '', meta: {} }), isReady: vi.fn().mockResolvedValue(true) }, initRouter: vi.fn() }; }); vi.mock('vue-router', async (importOriginal) => { const actual = await importOriginal(); const { ref: vRef } = require('vue'); return { ...actual, useRouter: vi.fn(() => ({ push: vi.fn(), replace: vi.fn(), currentRoute: vRef({ path: '/', name: '', meta: {} }) })) }; }); vi.mock('../../../../plugin/interopApi', () => ({ initInteropApi: vi.fn() })); vi.mock('../../../../service/database', () => ({ database: new Proxy( {}, { get: (_target, prop) => { if (prop === '__esModule') return false; return vi.fn().mockResolvedValue(null); } } ) })); vi.mock('../../../../service/config', () => ({ default: { init: vi.fn(), getString: vi.fn().mockImplementation((_k, d) => d ?? '{}'), setString: vi.fn(), getBool: vi.fn().mockImplementation((_k, d) => d ?? false), setBool: vi.fn(), getInt: vi.fn().mockImplementation((_k, d) => d ?? 0), setInt: vi.fn(), getFloat: vi.fn().mockImplementation((_k, d) => d ?? 0), setFloat: vi.fn(), getObject: vi.fn().mockReturnValue(null), setObject: vi.fn(), getArray: vi.fn().mockReturnValue([]), setArray: vi.fn(), remove: vi.fn() } })); vi.mock('../../../../service/jsonStorage', () => ({ default: vi.fn() })); vi.mock('../../../../service/watchState', () => ({ watchState: { isLoggedIn: false } })); vi.mock('../../../../service/request', () => ({ request: vi.fn().mockResolvedValue({ json: {} }), processBulk: vi.fn(), buildRequestInit: vi.fn(), parseResponse: vi.fn(), shouldIgnoreError: vi.fn(), $throw: vi.fn(), failedGetRequests: new Map() })); vi.mock('../../../../api', () => ({ groupRequest: {}, userRequest: {} })); import { useGroupModerationData } from '../useGroupModerationData'; function createTables() { return { members: reactive({ data: [], pageSize: 15 }), bans: reactive({ data: [], filters: [{ prop: ['$displayName'], value: '' }], pageSize: 15 }), invites: reactive({ data: [], pageSize: 15 }), joinRequests: reactive({ data: [], pageSize: 15 }), blocked: reactive({ data: [], pageSize: 15 }), logs: reactive({ data: [], filters: [{ prop: ['description'], value: '' }], pageSize: 15 }) }; } function createDeps(overrides = {}) { const tables = createTables(); return { groupMemberModeration: ref({ id: 'grp_test', visible: true, groupRef: { memberCount: 10, roles: [] } }), currentUser: ref({ id: 'usr_self' }), applyGroupMember: vi.fn((json) => json), handleGroupMember: vi.fn(), tables, selection: { selectedUsers: {}, setSelectedUsers: vi.fn() }, groupRequest: { getGroupBans: vi.fn(), getGroupLogs: vi.fn(), getGroupInvites: vi.fn(), getGroupJoinRequests: vi.fn(), getGroupMember: vi.fn(), getGroupMembers: vi.fn(), getGroupMembersSearch: vi.fn() }, userRequest: { getCachedUser: vi.fn() }, ...overrides }; } describe('useGroupModerationData', () => { beforeEach(() => { vi.clearAllMocks(); }); describe('getAllGroupBans', () => { test('populates bans table with fetched data', async () => { const deps = createDeps(); const bans = [ { userId: 'usr_1', user: { displayName: 'Alice' } }, { userId: 'usr_2', user: { displayName: 'Bob' } } ]; deps.groupRequest.getGroupBans.mockResolvedValue({ json: bans, params: { groupId: 'grp_test' } }); const { getAllGroupBans } = useGroupModerationData(deps); await getAllGroupBans('grp_test'); expect(deps.tables.bans.data).toHaveLength(2); expect(deps.groupRequest.getGroupBans).toHaveBeenCalledWith({ groupId: 'grp_test', n: 100, offset: 0 }); }); test('paginates through multiple pages', async () => { const deps = createDeps(); const page1 = Array.from({ length: 100 }, (_, i) => ({ userId: `usr_${i}`, user: { displayName: `User${i}` } })); const page2 = [{ userId: 'usr_100', user: { displayName: 'User100' } }]; deps.groupRequest.getGroupBans .mockResolvedValueOnce({ json: page1, params: { groupId: 'grp_test' } }) .mockResolvedValueOnce({ json: page2, params: { groupId: 'grp_test' } }); const { getAllGroupBans } = useGroupModerationData(deps); await getAllGroupBans('grp_test'); expect(deps.tables.bans.data).toHaveLength(101); expect(deps.groupRequest.getGroupBans).toHaveBeenCalledTimes(2); }); test('skips data from wrong group', async () => { const deps = createDeps(); deps.groupRequest.getGroupBans.mockResolvedValue({ json: [{ userId: 'usr_1' }], params: { groupId: 'grp_other' } }); const { getAllGroupBans } = useGroupModerationData(deps); await getAllGroupBans('grp_test'); // Should have continued past the mismatched group and eventually exhausted pages // The data won't contain the mismatched entry because it was skipped expect(deps.tables.bans.data).toHaveLength(0); }); test('handles API error gracefully', async () => { const { toast } = await import('vue-sonner'); const deps = createDeps(); deps.groupRequest.getGroupBans.mockRejectedValue(new Error('Network error')); const { getAllGroupBans, isGroupMembersLoading } = useGroupModerationData(deps); await getAllGroupBans('grp_test'); expect(toast.error).toHaveBeenCalledWith('Failed to get group bans'); expect(isGroupMembersLoading.value).toBe(false); }); test('stops when dialog is no longer visible', async () => { const deps = createDeps(); const page1 = Array.from({ length: 100 }, (_, i) => ({ userId: `usr_${i}` })); deps.groupRequest.getGroupBans .mockResolvedValueOnce({ json: page1, params: { groupId: 'grp_test' } }) .mockImplementation(() => { deps.groupMemberModeration.value.visible = false; return Promise.resolve({ json: [{ userId: 'usr_extra' }], params: { groupId: 'grp_test' } }); }); const { getAllGroupBans } = useGroupModerationData(deps); await getAllGroupBans('grp_test'); // Should stop after detecting visible=false expect(deps.groupRequest.getGroupBans).toHaveBeenCalledTimes(2); }); }); describe('getAllGroupLogs', () => { test('populates logs table and deduplicates', async () => { const deps = createDeps(); const logs = [ { id: 'log_1', description: 'event 1' }, { id: 'log_2', description: 'event 2' }, { id: 'log_1', description: 'event 1 dup' } ]; deps.groupRequest.getGroupLogs.mockResolvedValue({ json: { results: logs, hasNext: false }, params: { groupId: 'grp_test' } }); const { getAllGroupLogs } = useGroupModerationData(deps); await getAllGroupLogs('grp_test'); expect(deps.tables.logs.data).toHaveLength(2); }); test('passes eventTypes filter when provided', async () => { const deps = createDeps(); deps.groupRequest.getGroupLogs.mockResolvedValue({ json: { results: [], hasNext: false }, params: { groupId: 'grp_test' } }); const { getAllGroupLogs } = useGroupModerationData(deps); await getAllGroupLogs('grp_test', ['group.member.ban', 'group.member.kick']); expect(deps.groupRequest.getGroupLogs).toHaveBeenCalledWith( expect.objectContaining({ eventTypes: ['group.member.ban', 'group.member.kick'] }) ); }); }); describe('getAllGroupInvitesAndJoinRequests', () => { test('fetches invites, join requests, and blocked in parallel', async () => { const deps = createDeps(); deps.groupRequest.getGroupInvites.mockResolvedValue({ json: [{ userId: 'usr_inv' }], params: { groupId: 'grp_test' } }); deps.groupRequest.getGroupJoinRequests .mockResolvedValueOnce({ json: [{ userId: 'usr_join' }], params: { groupId: 'grp_test' } }) .mockResolvedValueOnce({ json: [{ userId: 'usr_block' }], params: { groupId: 'grp_test' } }); const { getAllGroupInvitesAndJoinRequests } = useGroupModerationData(deps); await getAllGroupInvitesAndJoinRequests('grp_test'); expect(deps.tables.invites.data).toHaveLength(1); expect(deps.tables.joinRequests.data).toHaveLength(1); expect(deps.tables.blocked.data).toHaveLength(1); }); }); describe('selectGroupMemberUserId', () => { test('parses multiple user IDs from input', async () => { const deps = createDeps(); deps.groupRequest.getGroupMember.mockResolvedValue({ json: { userId: 'usr_aaaa1111-2222-3333-4444-555566667777', user: { displayName: 'A' } }, params: {} }); const { selectGroupMemberUserId } = useGroupModerationData(deps); await selectGroupMemberUserId( 'usr_aaaa1111-2222-3333-4444-555566667777 usr_bbbb1111-2222-3333-4444-555566667777' ); expect(deps.groupRequest.getGroupMember).toHaveBeenCalledTimes(2); }); test('falls back to raw input when no usr_ pattern found', async () => { const deps = createDeps(); deps.groupRequest.getGroupMember.mockResolvedValue({ json: { userId: 'some_input', user: { displayName: 'Test' } }, params: {} }); const { selectGroupMemberUserId } = useGroupModerationData(deps); await selectGroupMemberUserId('some_input'); expect(deps.groupRequest.getGroupMember).toHaveBeenCalledWith({ groupId: 'grp_test', userId: 'some_input' }); }); test('does nothing with empty input', async () => { const deps = createDeps(); const { selectGroupMemberUserId } = useGroupModerationData(deps); await selectGroupMemberUserId(''); expect(deps.groupRequest.getGroupMember).not.toHaveBeenCalled(); }); }); describe('addGroupMemberToSelection', () => { test('uses group member data when available', async () => { const deps = createDeps(); const member = { userId: 'usr_1', user: { displayName: 'Alice' } }; deps.groupRequest.getGroupMember.mockResolvedValue({ json: member, params: {} }); deps.applyGroupMember.mockReturnValue(member); const { addGroupMemberToSelection } = useGroupModerationData(deps); await addGroupMemberToSelection('usr_1'); expect(deps.selection.setSelectedUsers).toHaveBeenCalledWith('usr_1', member); expect(deps.userRequest.getCachedUser).not.toHaveBeenCalled(); }); test('falls back to user API when member has no user object', async () => { const deps = createDeps(); deps.groupRequest.getGroupMember.mockResolvedValue({ json: { userId: 'usr_1' }, params: {} }); deps.applyGroupMember.mockReturnValue({ userId: 'usr_1' }); deps.userRequest.getCachedUser.mockResolvedValue({ json: { id: 'usr_1', displayName: 'Alice' } }); const { addGroupMemberToSelection } = useGroupModerationData(deps); await addGroupMemberToSelection('usr_1'); expect(deps.userRequest.getCachedUser).toHaveBeenCalledWith({ userId: 'usr_1' }); expect(deps.selection.setSelectedUsers).toHaveBeenCalledWith('usr_1', expect.objectContaining({ userId: 'usr_1', displayName: 'Alice' })); }); }); describe('resetData', () => { test('clears all table data and search state', () => { const deps = createDeps(); deps.tables.members.data = [{ userId: 'usr_1' }]; deps.tables.bans.data = [{ userId: 'usr_2' }]; const { resetData, memberSearch } = useGroupModerationData(deps); memberSearch.value = 'test'; resetData(); expect(deps.tables.members.data).toHaveLength(0); expect(deps.tables.bans.data).toHaveLength(0); expect(deps.tables.invites.data).toHaveLength(0); expect(deps.tables.joinRequests.data).toHaveLength(0); expect(deps.tables.blocked.data).toHaveLength(0); expect(deps.tables.logs.data).toHaveLength(0); expect(memberSearch.value).toBe(''); }); }); describe('member search / sort / filter', () => { test('groupMembersSearch clears table when search is too short', () => { const deps = createDeps(); deps.tables.members.data = [{ userId: 'usr_1' }]; const { groupMembersSearch, memberSearch, isGroupMembersLoading } = useGroupModerationData(deps); memberSearch.value = 'ab'; groupMembersSearch(); expect(deps.tables.members.data).toHaveLength(0); expect(isGroupMembersLoading.value).toBe(false); }); test('setGroupMemberSortOrder does nothing when sort is the same', async () => { const deps = createDeps(); deps.groupRequest.getGroupMember.mockResolvedValue({ json: null, params: {} }); const { setGroupMemberSortOrder, memberSortOrder } = useGroupModerationData(deps); const currentSort = memberSortOrder.value; await setGroupMemberSortOrder(currentSort); // getGroupMember should not have been called since sort didn't change expect(deps.groupRequest.getGroupMember).not.toHaveBeenCalled(); }); test('setGroupMemberFilter does nothing when filter is the same', async () => { const deps = createDeps(); const { setGroupMemberFilter, memberFilter } = useGroupModerationData(deps); const currentFilter = memberFilter.value; await setGroupMemberFilter(currentFilter); expect(deps.groupRequest.getGroupMember).not.toHaveBeenCalled(); }); }); describe('loadAllGroupMembers', () => { test('does nothing when already loading', async () => { const deps = createDeps(); const { loadAllGroupMembers, isGroupMembersLoading } = useGroupModerationData(deps); isGroupMembersLoading.value = true; await loadAllGroupMembers(); expect(deps.groupRequest.getGroupMember).not.toHaveBeenCalled(); }); }); });