import { beforeEach, describe, expect, test, vi } from 'vitest'; import { mount } from '@vue/test-utils'; import { nextTick } from 'vue'; const mocks = vi.hoisted(() => ({ makeRef: (value) => ({ value, __v_isRef: true }), route: { path: '/friend-list' }, routerPush: vi.fn(), friends: null, allFavoriteFriendIds: null, randomUserColours: null, stringComparer: null, friendsListSearch: null, getAllUserStats: vi.fn(), getAllUserMutualCount: vi.fn(), confirmDeleteFriend: vi.fn(), handleFriendDelete: vi.fn(), showUserDialog: vi.fn(), modalConfirm: vi.fn().mockResolvedValue({ ok: true }), modalAlert: vi.fn(), userGetUser: vi.fn().mockResolvedValue({}), friendDeleteFriend: vi.fn().mockResolvedValue({}), toastSuccess: vi.fn(), setOptions: vi.fn(), setPageIndex: vi.fn(), setSorting: vi.fn(), toggleBulkColumnVisibility: vi.fn(), pagination: null, sorting: null })); mocks.friends = mocks.makeRef(new Map()); mocks.allFavoriteFriendIds = mocks.makeRef(new Set()); mocks.randomUserColours = mocks.makeRef(false); mocks.stringComparer = mocks.makeRef(null); mocks.friendsListSearch = mocks.makeRef(''); mocks.pagination = mocks.makeRef({ pageIndex: 3, pageSize: 10 }); mocks.sorting = mocks.makeRef([]); vi.mock('pinia', async (importOriginal) => { const actual = await importOriginal(); return { ...actual, storeToRefs: (store) => store }; }); vi.mock('vue-i18n', () => ({ useI18n: () => ({ t: (key) => key, locale: require('vue').ref('en') }) })); vi.mock('vue-router', () => ({ useRoute: () => mocks.route })); vi.mock('vue-sonner', () => ({ toast: { success: (...args) => mocks.toastSuccess(...args) } })); vi.mock('../../../stores', () => ({ useFriendStore: () => ({ friends: mocks.friends, allFavoriteFriendIds: mocks.allFavoriteFriendIds, getAllUserStats: mocks.getAllUserStats, getAllUserMutualCount: mocks.getAllUserMutualCount }), useModalStore: () => ({ confirm: (...args) => mocks.modalConfirm(...args), alert: (...args) => mocks.modalAlert(...args) }), useSearchStore: () => ({ stringComparer: mocks.stringComparer, friendsListSearch: mocks.friendsListSearch }), useUserStore: () => ({}), useAppearanceSettingsStore: () => ({ tablePageSizes: [10, 25, 50], tablePageSize: 25, randomUserColours: mocks.randomUserColours }), useVrcxStore: () => ({ maxTableSize: 100 }) })); vi.mock('../../../coordinators/userCoordinator', () => ({ showUserDialog: (...args) => mocks.showUserDialog(...args) })); vi.mock('../../../coordinators/friendRelationshipCoordinator', () => ({ confirmDeleteFriend: (...args) => mocks.confirmDeleteFriend(...args), handleFriendDelete: (...args) => mocks.handleFriendDelete(...args) })); vi.mock('../../../plugins/router', () => ({ router: { push: (...args) => mocks.routerPush(...args) } })); vi.mock('../../../api', () => ({ userRequest: { getUser: (...args) => mocks.userGetUser(...args) }, friendRequest: { deleteFriend: (...args) => mocks.friendDeleteFriend(...args) } })); vi.mock('../../../services/confusables', () => ({ default: (value) => value, removeWhitespace: (value) => String(value ?? '').replace(/\s+/g, '') })); vi.mock('../../../shared/utils', () => ({ localeIncludes: (source, query) => String(source ?? '').toLowerCase().includes(String(query ?? '').toLowerCase()) })); vi.mock('../../../composables/useDataTableScrollHeight', () => ({ useDataTableScrollHeight: () => ({ tableStyle: {} }) })); vi.mock('../../../lib/table/useVrcxVueTable', () => ({ useVrcxVueTable: (options) => ({ table: { setOptions: (...args) => mocks.setOptions(...args), setPageIndex: (...args) => mocks.setPageIndex(...args), setSorting: (...args) => mocks.setSorting(...args), getFilteredRowModel: () => ({ rows: options.data }), getColumn: (id) => id === 'bulkSelect' ? { toggleVisibility: (...args) => mocks.toggleBulkColumnVisibility(...args) } : null }, sorting: mocks.sorting, pagination: mocks.pagination }) })); vi.mock('../columns.jsx', () => ({ createColumns: () => [{ id: 'bulkSelect' }] })); vi.mock('@/components/ui/data-table', () => ({ DataTableLayout: { props: ['totalItems', 'onPageSizeChange', 'onRowClick'], template: '
' + '' + '' + '' + '{{ totalItems }}' + '
' } })); vi.mock('@/components/ui/button', () => ({ Button: { emits: ['click'], template: '' } })); vi.mock('@/components/ui/input-group', () => ({ InputGroupField: { props: ['modelValue'], emits: ['update:modelValue', 'change'], template: '' } })); vi.mock('@/components/ui/progress', () => ({ Progress: { template: '
' } })); vi.mock('@/components/ui/select', () => ({ Select: { emits: ['update:modelValue'], template: '
' + '' + '' + '
' }, SelectContent: { template: '
' }, SelectGroup: { template: '
' }, SelectItem: { template: '
' }, SelectTrigger: { template: '
' }, SelectValue: { template: '
' } })); vi.mock('@/components/ui/dialog', () => ({ Dialog: { props: ['open'], emits: ['update:open'], template: '
' }, DialogContent: { template: '
' }, DialogFooter: { template: '
' }, DialogHeader: { template: '
' }, DialogTitle: { template: '
' } })); vi.mock('@/components/ui/switch', () => ({ Switch: { props: ['modelValue'], emits: ['update:modelValue'], template: '' } })); vi.mock('@/components/ui/toggle', () => ({ Toggle: { props: ['modelValue'], emits: ['update:modelValue'], template: '' } })); vi.mock('@/components/ui/tooltip', () => ({ TooltipWrapper: { template: '
' } })); vi.mock('lucide-vue-next', () => ({ Star: { template: '' } })); import FriendList from '../FriendList.vue'; function makeFriendCtx({ id, displayName, memo = '', dateJoined = null }) { return { id, memo, ref: { id, displayName, statusDescription: '', note: '', bio: '', $trustLevel: 'trusted', date_joined: dateJoined } }; } function clickButtonByText(wrapper, text) { const button = wrapper.findAll('button').find((node) => node.text().trim() === text); if (!button) { throw new Error(`Cannot find button with text: ${text}`); } return button.trigger('click'); } async function flushAsync() { await Promise.resolve(); await Promise.resolve(); await nextTick(); } describe('FriendList.vue', () => { beforeEach(() => { mocks.route.path = '/friend-list'; mocks.friends.value = new Map(); mocks.allFavoriteFriendIds.value = new Set(); mocks.friendsListSearch.value = ''; mocks.pagination.value = { pageIndex: 3, pageSize: 10 }; mocks.sorting.value = []; mocks.routerPush.mockReset(); mocks.getAllUserStats.mockReset(); mocks.getAllUserMutualCount.mockReset(); mocks.showUserDialog.mockReset(); mocks.modalConfirm.mockClear(); mocks.modalAlert.mockReset(); mocks.userGetUser.mockReset(); mocks.friendDeleteFriend.mockReset(); mocks.toastSuccess.mockReset(); mocks.setOptions.mockReset(); mocks.setPageIndex.mockReset(); mocks.setSorting.mockReset(); mocks.toggleBulkColumnVisibility.mockReset(); }); test('filters friend list by search text and VIP toggle', async () => { mocks.friends.value = new Map([ ['usr_1', makeFriendCtx({ id: 'usr_1', displayName: 'Alice' })], ['usr_2', makeFriendCtx({ id: 'usr_2', displayName: 'Bob' })] ]); mocks.allFavoriteFriendIds.value = new Set(['usr_1']); mocks.friendsListSearch.value = 'alice'; const wrapper = mount(FriendList); await flushAsync(); wrapper.vm.friendsListSearchFilterVIP = true; wrapper.vm.friendsListSearchChange(); await nextTick(); expect(wrapper.vm.friendsListDisplayData.map((item) => item.id)).toEqual(['usr_1']); expect(mocks.getAllUserStats).toHaveBeenCalled(); expect(mocks.getAllUserMutualCount).toHaveBeenCalled(); }); test('opens charts tab from toolbar button', async () => { const wrapper = mount(FriendList); await clickButtonByText(wrapper, 'view.friend_list.load_mutual_friends'); expect(mocks.routerPush).toHaveBeenCalledWith({ name: 'charts' }); }); test('loads missing user profiles and shows completion toast', async () => { mocks.friends.value = new Map([ ['usr_1', makeFriendCtx({ id: 'usr_1', displayName: 'Alice', dateJoined: null })], ['usr_2', makeFriendCtx({ id: 'usr_2', displayName: 'Bob', dateJoined: '2020-01-01' })] ]); const wrapper = mount(FriendList); await flushAsync(); await clickButtonByText(wrapper, 'view.friend_list.load'); await flushAsync(); expect(mocks.userGetUser).toHaveBeenCalledTimes(1); expect(mocks.userGetUser).toHaveBeenCalledWith({ userId: 'usr_1' }); expect(mocks.toastSuccess).toHaveBeenCalledWith('view.friend_list.load_complete'); }); test('select row emits lookup-user for id-less value and opens user dialog for id', () => { const wrapper = mount(FriendList); wrapper.vm.selectFriendsListRow({ displayName: 'Unknown' }); wrapper.vm.selectFriendsListRow({ id: 'usr_99', displayName: 'Known' }); expect(wrapper.emitted('lookup-user')?.[0]?.[0]).toEqual({ displayName: 'Unknown' }); expect(mocks.showUserDialog).toHaveBeenCalledWith('usr_99'); }); test('toggles bulk mode column visibility and resets page size', async () => { const wrapper = mount(FriendList); mocks.toggleBulkColumnVisibility.mockReset(); await wrapper.get('[data-testid="bulk-switch"]').trigger('click'); await nextTick(); expect(mocks.toggleBulkColumnVisibility).toHaveBeenCalledWith(true); await wrapper.get('[data-testid="set-page-size"]').trigger('click'); expect(mocks.pagination.value).toEqual({ pageIndex: 0, pageSize: 50 }); }); });