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 }), onlineFriends: null, allFavoriteOnlineFriends: null, allFavoriteFriendIds: null, activeFriends: null, offlineFriends: null, friendsInSameInstance: null, isSidebarDivideByFriendGroup: null, sidebarFavoriteGroups: null, sidebarFavoriteGroupOrder: null, sidebarSortMethods: null, favoriteFriendGroups: null, groupedByGroupKeyFavoriteFriends: null, localFriendFavorites: null, configGetString: vi.fn(), configGetBool: vi.fn(), configSetString: vi.fn(), configSetBool: vi.fn(), virtualMeasure: vi.fn() })); mocks.onlineFriends = mocks.makeRef([]); mocks.allFavoriteOnlineFriends = mocks.makeRef([]); mocks.allFavoriteFriendIds = mocks.makeRef(new Set()); mocks.activeFriends = mocks.makeRef([]); mocks.offlineFriends = mocks.makeRef([]); mocks.friendsInSameInstance = mocks.makeRef([]); mocks.isSidebarDivideByFriendGroup = mocks.makeRef(false); mocks.sidebarFavoriteGroups = mocks.makeRef([]); mocks.sidebarFavoriteGroupOrder = mocks.makeRef([]); mocks.sidebarSortMethods = mocks.makeRef('status'); mocks.favoriteFriendGroups = mocks.makeRef([]); mocks.groupedByGroupKeyFavoriteFriends = mocks.makeRef({}); mocks.localFriendFavorites = mocks.makeRef({}); vi.mock('pinia', () => ({ storeToRefs: (store) => store })); vi.mock('vue-i18n', () => ({ useI18n: () => ({ t: (key) => key, locale: require('vue').ref('en') }) })); vi.mock('../../../stores', () => ({ useFriendStore: () => ({ onlineFriends: mocks.onlineFriends, allFavoriteOnlineFriends: mocks.allFavoriteOnlineFriends, allFavoriteFriendIds: mocks.allFavoriteFriendIds, activeFriends: mocks.activeFriends, offlineFriends: mocks.offlineFriends, friendsInSameInstance: mocks.friendsInSameInstance }), useAppearanceSettingsStore: () => ({ isSidebarDivideByFriendGroup: mocks.isSidebarDivideByFriendGroup, sidebarFavoriteGroups: mocks.sidebarFavoriteGroups, sidebarFavoriteGroupOrder: mocks.sidebarFavoriteGroupOrder, sidebarSortMethods: mocks.sidebarSortMethods }), useFavoriteStore: () => ({ favoriteFriendGroups: mocks.favoriteFriendGroups, groupedByGroupKeyFavoriteFriends: mocks.groupedByGroupKeyFavoriteFriends, localFriendFavorites: mocks.localFriendFavorites }) })); vi.mock('../../../service/config.js', () => ({ default: { getString: (...args) => mocks.configGetString(...args), getBool: (...args) => mocks.configGetBool(...args), setString: (...args) => mocks.configSetString(...args), setBool: (...args) => mocks.configSetBool(...args) } })); vi.mock('../../../shared/utils/location.js', () => ({ getFriendsLocations: (friends) => friends?.[0]?.ref?.location ?? '' })); vi.mock('../../../shared/utils', () => ({ getFriendsSortFunction: () => (a, b) => String(a?.displayName ?? '').localeCompare(String(b?.displayName ?? '')) })); vi.mock('@tanstack/vue-virtual', () => ({ useVirtualizer: (optionsRef) => ({ value: { getVirtualItems: () => Array.from({ length: optionsRef.value.count }, (_, index) => ({ index, key: index, start: index * 64 })), getTotalSize: () => optionsRef.value.count * 64, measure: (...args) => mocks.virtualMeasure(...args), measureElement: vi.fn() } }) })); vi.mock('lucide-vue-next', () => ({ ChevronDown: { template: '' }, Loader2: { template: '' }, Settings: { template: '' } })); vi.mock('@/components/ui/field', () => ({ Field: { template: '
' }, FieldContent: { template: '
' }, FieldLabel: { template: '
' } })); vi.mock('@/components/ui/tabs', () => ({ Tabs: { props: ['modelValue'], emits: ['update:modelValue'], template: '
' + '' + '' + '' + '
' }, TabsList: { template: '
' }, TabsTrigger: { props: ['value'], template: '' } })); vi.mock('@/components/ui/button', () => ({ Button: { emits: ['click'], template: '' } })); vi.mock('@/components/ui/data-table', () => ({ DataTableEmpty: { props: ['type'], template: '
{{ type }}
' } })); vi.mock('@/components/ui/input-group', () => ({ InputGroupSearch: { props: ['modelValue'], emits: ['update:modelValue'], template: '' } })); vi.mock('../../../components/ui/popover', () => ({ Popover: { template: '
' }, PopoverContent: { template: '
' }, PopoverTrigger: { template: '
' } })); vi.mock('../../../components/ui/slider', () => ({ Slider: { props: ['modelValue'], emits: ['update:modelValue'], template: '' } })); vi.mock('../../../components/ui/switch', () => ({ Switch: { props: ['modelValue'], emits: ['update:modelValue'], template: '' } })); vi.mock('../components/FriendsLocationsCard.vue', () => ({ default: { props: ['friend'], template: '
{{ friend.displayName }}
' } })); import FriendsLocations from '../FriendsLocations.vue'; function makeFriend(id, displayName, location = 'wrld_1:instance') { return { id, displayName, signature: '', worldName: '', ref: { location } }; } async function flushSettings() { await Promise.resolve(); await Promise.resolve(); await nextTick(); await nextTick(); } describe('FriendsLocations.vue', () => { beforeEach(() => { mocks.onlineFriends.value = []; mocks.allFavoriteOnlineFriends.value = []; mocks.allFavoriteFriendIds.value = new Set(); mocks.activeFriends.value = []; mocks.offlineFriends.value = []; mocks.friendsInSameInstance.value = []; mocks.isSidebarDivideByFriendGroup.value = false; mocks.sidebarFavoriteGroups.value = []; mocks.sidebarFavoriteGroupOrder.value = []; mocks.sidebarSortMethods.value = 'status'; mocks.favoriteFriendGroups.value = []; mocks.groupedByGroupKeyFavoriteFriends.value = {}; mocks.localFriendFavorites.value = {}; mocks.configGetString.mockReset(); mocks.configGetBool.mockReset(); mocks.configSetString.mockReset(); mocks.configSetBool.mockReset(); mocks.virtualMeasure.mockReset(); mocks.configGetString.mockImplementation((_key, defaultValue) => Promise.resolve(defaultValue ?? '1')); mocks.configGetBool.mockResolvedValue(false); }); test('renders online friend cards after initial settings load', async () => { mocks.onlineFriends.value = [makeFriend('usr_1', 'Alice'), makeFriend('usr_2', 'Bob')]; const wrapper = mount(FriendsLocations); await flushSettings(); const cards = wrapper.findAll('[data-testid="friend-card"]'); expect(cards.map((node) => node.text())).toEqual(['Alice', 'Bob']); }); test('filters cards by search text in DOM', async () => { mocks.onlineFriends.value = [makeFriend('usr_1', 'Alice'), makeFriend('usr_2', 'Bob')]; const wrapper = mount(FriendsLocations); await flushSettings(); await wrapper.get('[data-testid="friend-locations-search"]').setValue('bob'); await flushSettings(); const cards = wrapper.findAll('[data-testid="friend-card"]'); expect(cards.map((node) => node.text())).toEqual(['Bob']); }); test('switches to offline segment and renders offline cards', async () => { mocks.onlineFriends.value = [makeFriend('usr_1', 'Alice')]; mocks.offlineFriends.value = [makeFriend('usr_3', 'Carol')]; const wrapper = mount(FriendsLocations); await flushSettings(); await wrapper.get('[data-testid="segment-offline"]').trigger('click'); await flushSettings(); const cards = wrapper.findAll('[data-testid="friend-card"]'); expect(cards.map((node) => node.text())).toEqual(['Carol']); }); test('persists card scale and same-instance preferences', async () => { const wrapper = mount(FriendsLocations); await flushSettings(); await wrapper.get('[data-testid="set-scale"]').trigger('click'); await wrapper.get('[data-testid="toggle-same-instance"]').trigger('click'); expect(mocks.configSetString).toHaveBeenCalledWith('VRCX_FriendLocationCardScale', '0.8'); expect(mocks.configSetBool).toHaveBeenCalledWith('VRCX_FriendLocationShowSameInstance', true); }); test('renders empty state when no rows match', async () => { const wrapper = mount(FriendsLocations); await flushSettings(); expect(wrapper.get('[data-testid="empty-state"]').text()).toBe('nomatch'); }); });