mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-04-22 08:13:52 +02:00
add test
This commit is contained in:
170
src/views/Sidebar/components/__tests__/FriendItem.test.js
Normal file
170
src/views/Sidebar/components/__tests__/FriendItem.test.js
Normal file
@@ -0,0 +1,170 @@
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
import { mount } from '@vue/test-utils';
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
appearanceStore: {
|
||||
hideNicknames: false
|
||||
},
|
||||
friendStore: {
|
||||
isRefreshFriendsLoading: false,
|
||||
allFavoriteFriendIds: new Set(),
|
||||
confirmDeleteFriend: vi.fn()
|
||||
},
|
||||
userStore: {
|
||||
showUserDialog: vi.fn()
|
||||
}
|
||||
}));
|
||||
|
||||
vi.mock('pinia', () => ({
|
||||
storeToRefs: (store) => store
|
||||
}));
|
||||
|
||||
vi.mock('../../../../stores', () => ({
|
||||
useAppearanceSettingsStore: () => mocks.appearanceStore,
|
||||
useFriendStore: () => mocks.friendStore,
|
||||
useUserStore: () => mocks.userStore
|
||||
}));
|
||||
|
||||
vi.mock('../../../../shared/utils', () => ({
|
||||
userImage: vi.fn(() => 'https://example.com/avatar.png'),
|
||||
userStatusClass: vi.fn(() => 'status-online')
|
||||
}));
|
||||
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({
|
||||
t: (key) => key
|
||||
})
|
||||
}));
|
||||
|
||||
vi.mock('@/components/ui/avatar', () => ({
|
||||
Avatar: {
|
||||
template: '<div data-testid="avatar"><slot /></div>'
|
||||
},
|
||||
AvatarImage: {
|
||||
props: ['src'],
|
||||
template: '<img data-testid="avatar-image" :src="src" />'
|
||||
},
|
||||
AvatarFallback: {
|
||||
template: '<span data-testid="avatar-fallback"><slot /></span>'
|
||||
}
|
||||
}));
|
||||
|
||||
vi.mock('@/components/ui/button', () => ({
|
||||
Button: {
|
||||
emits: ['click'],
|
||||
template:
|
||||
'<button data-testid="delete-button" @click="$emit(\'click\', $event)"><slot /></button>'
|
||||
}
|
||||
}));
|
||||
|
||||
vi.mock('@/components/ui/spinner', () => ({
|
||||
Spinner: {
|
||||
template: '<span data-testid="spinner" />'
|
||||
}
|
||||
}));
|
||||
|
||||
vi.mock('@/components/Location.vue', () => ({
|
||||
default: {
|
||||
props: ['location', 'traveling', 'link'],
|
||||
template:
|
||||
'<span data-testid="location">{{ location }}|{{ traveling }}</span>'
|
||||
}
|
||||
}));
|
||||
|
||||
vi.mock('@/components/Timer.vue', () => ({
|
||||
default: {
|
||||
props: ['epoch'],
|
||||
template: '<span data-testid="timer">{{ epoch }}</span>'
|
||||
}
|
||||
}));
|
||||
|
||||
vi.mock('lucide-vue-next', () => ({
|
||||
User: {
|
||||
template: '<span data-testid="icon-user" />'
|
||||
},
|
||||
Trash2: {
|
||||
template: '<span data-testid="icon-trash" />'
|
||||
}
|
||||
}));
|
||||
|
||||
import FriendItem from '../FriendItem.vue';
|
||||
|
||||
function makeFriend(overrides = {}) {
|
||||
return {
|
||||
id: 'usr_1',
|
||||
name: 'Alice',
|
||||
state: 'active',
|
||||
pendingOffline: false,
|
||||
$nickName: 'Ali',
|
||||
ref: {
|
||||
displayName: 'Alice',
|
||||
$userColour: '#fff',
|
||||
statusDescription: 'Online',
|
||||
location: 'wrld_abc:123',
|
||||
travelingToLocation: '',
|
||||
$location_at: 123
|
||||
},
|
||||
...overrides
|
||||
};
|
||||
}
|
||||
|
||||
function mountItem(props = {}) {
|
||||
return mount(FriendItem, {
|
||||
props: {
|
||||
friend: makeFriend(),
|
||||
isGroupByInstance: false,
|
||||
...props
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
describe('FriendItem.vue', () => {
|
||||
beforeEach(() => {
|
||||
mocks.appearanceStore.hideNicknames = false;
|
||||
mocks.friendStore.isRefreshFriendsLoading = false;
|
||||
mocks.friendStore.allFavoriteFriendIds = new Set();
|
||||
mocks.friendStore.confirmDeleteFriend.mockReset();
|
||||
mocks.userStore.showUserDialog.mockReset();
|
||||
});
|
||||
|
||||
test('renders nickname when hideNicknames is false', () => {
|
||||
const wrapper = mountItem();
|
||||
expect(wrapper.text()).toContain('Alice (Ali)');
|
||||
});
|
||||
|
||||
test('renders favorite star when grouped by instance and friend is favorite', () => {
|
||||
mocks.appearanceStore.hideNicknames = true;
|
||||
mocks.friendStore.allFavoriteFriendIds = new Set(['usr_1']);
|
||||
|
||||
const wrapper = mountItem({
|
||||
friend: makeFriend({ $nickName: '' }),
|
||||
isGroupByInstance: true
|
||||
});
|
||||
|
||||
expect(wrapper.text()).toContain('Alice ⭐');
|
||||
});
|
||||
|
||||
test('clicking row opens user dialog', async () => {
|
||||
const wrapper = mountItem();
|
||||
await wrapper.get('div').trigger('click');
|
||||
expect(mocks.userStore.showUserDialog).toHaveBeenCalledWith('usr_1');
|
||||
});
|
||||
|
||||
test('renders delete action for orphan friend and triggers confirmDeleteFriend', async () => {
|
||||
const wrapper = mountItem({
|
||||
friend: makeFriend({
|
||||
id: 'usr_orphan',
|
||||
name: 'Ghost',
|
||||
ref: null
|
||||
})
|
||||
});
|
||||
|
||||
expect(wrapper.text()).toContain('Ghost');
|
||||
const button = wrapper.get('[data-testid="delete-button"]');
|
||||
await button.trigger('click');
|
||||
expect(mocks.friendStore.confirmDeleteFriend).toHaveBeenCalledWith(
|
||||
'usr_orphan'
|
||||
);
|
||||
expect(mocks.userStore.showUserDialog).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
290
src/views/Sidebar/components/__tests__/FriendsSidebar.test.js
Normal file
290
src/views/Sidebar/components/__tests__/FriendsSidebar.test.js
Normal file
@@ -0,0 +1,290 @@
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import { nextTick } from 'vue';
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
friendStore: {
|
||||
allFavoriteOnlineFriends: { value: [] },
|
||||
allFavoriteFriendIds: { value: new Set() },
|
||||
onlineFriends: { value: [] },
|
||||
activeFriends: { value: [] },
|
||||
offlineFriends: { value: [] },
|
||||
friendsInSameInstance: { value: [] }
|
||||
},
|
||||
appearanceStore: {
|
||||
isSidebarGroupByInstance: { value: false },
|
||||
isHideFriendsInSameInstance: { value: false },
|
||||
isSidebarDivideByFriendGroup: { value: false },
|
||||
sidebarFavoriteGroups: { value: [] },
|
||||
sidebarFavoriteGroupOrder: { value: [] },
|
||||
sidebarSortMethods: { value: [] }
|
||||
},
|
||||
advancedStore: {
|
||||
gameLogDisabled: { value: false }
|
||||
},
|
||||
userStore: {
|
||||
showUserDialog: vi.fn(),
|
||||
showSendBoopDialog: vi.fn(),
|
||||
currentUser: {
|
||||
value: {
|
||||
id: 'usr_me',
|
||||
displayName: 'Me',
|
||||
$userColour: '#fff',
|
||||
statusDescription: 'Ready',
|
||||
status: 'active',
|
||||
statusHistory: [],
|
||||
isBoopingEnabled: true,
|
||||
$locationTag: 'wrld_me:123',
|
||||
$travelingToLocation: ''
|
||||
}
|
||||
}
|
||||
},
|
||||
launchStore: {
|
||||
showLaunchDialog: vi.fn()
|
||||
},
|
||||
favoriteStore: {
|
||||
favoriteFriendGroups: { value: [] },
|
||||
groupedByGroupKeyFavoriteFriends: { value: {} },
|
||||
localFriendFavorites: { value: {} }
|
||||
},
|
||||
locationStore: {
|
||||
lastLocation: { value: { location: 'wrld_home:123', friendList: new Map() } },
|
||||
lastLocationDestination: { value: '' }
|
||||
},
|
||||
gameStore: {
|
||||
isGameRunning: { value: true }
|
||||
},
|
||||
configRepository: {
|
||||
getBool: vi.fn(),
|
||||
setBool: vi.fn()
|
||||
},
|
||||
notificationRequest: {
|
||||
sendRequestInvite: vi.fn().mockResolvedValue({}),
|
||||
sendInvite: vi.fn().mockResolvedValue({})
|
||||
},
|
||||
worldRequest: {
|
||||
getCachedWorld: vi.fn().mockResolvedValue({ ref: { name: 'World' } })
|
||||
},
|
||||
instanceRequest: {
|
||||
selfInvite: vi.fn().mockResolvedValue({})
|
||||
},
|
||||
userRequest: {
|
||||
saveCurrentUser: vi.fn().mockResolvedValue({})
|
||||
},
|
||||
toast: {
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
warning: vi.fn()
|
||||
}
|
||||
}));
|
||||
|
||||
vi.mock('pinia', () => ({
|
||||
storeToRefs: (store) => store
|
||||
}));
|
||||
|
||||
vi.mock('@tanstack/vue-virtual', () => ({
|
||||
useVirtualizer: (optionsRef) => ({
|
||||
value: {
|
||||
getVirtualItems: () => {
|
||||
const options = optionsRef.value;
|
||||
return Array.from({ length: options.count }, (_, index) => ({
|
||||
index,
|
||||
key: options.getItemKey?.(index) ?? index,
|
||||
start: index * 52
|
||||
}));
|
||||
},
|
||||
getTotalSize: () => optionsRef.value.count * 52,
|
||||
measure: vi.fn(),
|
||||
measureElement: vi.fn()
|
||||
}
|
||||
})
|
||||
}));
|
||||
|
||||
vi.mock('../../../../stores', () => ({
|
||||
useFriendStore: () => mocks.friendStore,
|
||||
useAppearanceSettingsStore: () => mocks.appearanceStore,
|
||||
useAdvancedSettingsStore: () => mocks.advancedStore,
|
||||
useFavoriteStore: () => mocks.favoriteStore,
|
||||
useGameStore: () => mocks.gameStore,
|
||||
useLaunchStore: () => mocks.launchStore,
|
||||
useLocationStore: () => mocks.locationStore,
|
||||
useUserStore: () => mocks.userStore
|
||||
}));
|
||||
|
||||
vi.mock('../../../../shared/utils', () => ({
|
||||
getFriendsSortFunction: () => (a, b) => a.id.localeCompare(b.id),
|
||||
isRealInstance: (location) =>
|
||||
typeof location === 'string' && location.startsWith('wrld_'),
|
||||
userImage: vi.fn(() => 'https://example.com/avatar.png'),
|
||||
userStatusClass: vi.fn(() => ''),
|
||||
parseLocation: vi.fn((location) => ({
|
||||
worldId: location?.split(':')[0] ?? '',
|
||||
instanceId: location?.split(':')[1] ?? '',
|
||||
tag: location ?? ''
|
||||
}))
|
||||
}));
|
||||
|
||||
vi.mock('../../../../shared/utils/invite.js', () => ({
|
||||
checkCanInvite: vi.fn(() => true),
|
||||
checkCanInviteSelf: vi.fn(() => true)
|
||||
}));
|
||||
|
||||
vi.mock('../../../../shared/utils/location.js', () => ({
|
||||
getFriendsLocations: vi.fn(() => 'wrld_same:1')
|
||||
}));
|
||||
|
||||
vi.mock('../../../../service/config', () => ({
|
||||
default: mocks.configRepository
|
||||
}));
|
||||
|
||||
vi.mock('../../../../api', () => ({
|
||||
notificationRequest: mocks.notificationRequest,
|
||||
worldRequest: mocks.worldRequest,
|
||||
instanceRequest: mocks.instanceRequest,
|
||||
userRequest: mocks.userRequest
|
||||
}));
|
||||
|
||||
vi.mock('vue-sonner', () => ({
|
||||
toast: mocks.toast
|
||||
}));
|
||||
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({
|
||||
t: (key) => key
|
||||
})
|
||||
}));
|
||||
|
||||
vi.mock('../../../../components/ui/context-menu', () => ({
|
||||
ContextMenu: { template: '<div><slot /></div>' },
|
||||
ContextMenuTrigger: { template: '<div><slot /></div>' },
|
||||
ContextMenuContent: { template: '<div><slot /></div>' },
|
||||
ContextMenuItem: {
|
||||
emits: ['click'],
|
||||
props: ['disabled'],
|
||||
template: '<button :disabled="disabled" @click="$emit(\'click\')"><slot /></button>'
|
||||
},
|
||||
ContextMenuSeparator: { template: '<hr />' },
|
||||
ContextMenuSub: { template: '<div><slot /></div>' },
|
||||
ContextMenuSubContent: { template: '<div><slot /></div>' },
|
||||
ContextMenuSubTrigger: { template: '<div><slot /></div>' },
|
||||
ContextMenuCheckboxItem: {
|
||||
emits: ['click'],
|
||||
props: ['modelValue'],
|
||||
template: '<button @click="$emit(\'click\')"><slot /></button>'
|
||||
}
|
||||
}));
|
||||
|
||||
vi.mock('../../../../components/BackToTop.vue', () => ({
|
||||
default: { template: '<div data-testid="back-to-top" />' }
|
||||
}));
|
||||
|
||||
vi.mock('../../../../components/Location.vue', () => ({
|
||||
default: {
|
||||
props: ['location', 'traveling', 'link'],
|
||||
template: '<span data-testid="location">{{ location }}</span>'
|
||||
}
|
||||
}));
|
||||
|
||||
vi.mock('../FriendItem.vue', () => ({
|
||||
default: {
|
||||
props: ['friend'],
|
||||
template: '<div data-testid="friend-item">{{ friend.id }}</div>'
|
||||
}
|
||||
}));
|
||||
|
||||
vi.mock('lucide-vue-next', () => ({
|
||||
ChevronDown: { template: '<span data-testid="chevron" />' }
|
||||
}));
|
||||
|
||||
import FriendsSidebar from '../FriendsSidebar.vue';
|
||||
|
||||
function flushPromises() {
|
||||
return new Promise((resolve) => setTimeout(resolve, 0));
|
||||
}
|
||||
|
||||
function makeFriend(id, location = 'wrld_online:1') {
|
||||
return {
|
||||
id,
|
||||
state: 'online',
|
||||
pendingOffline: false,
|
||||
ref: {
|
||||
location,
|
||||
$location: {
|
||||
tag: location
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
describe('FriendsSidebar.vue', () => {
|
||||
beforeEach(() => {
|
||||
mocks.friendStore.allFavoriteOnlineFriends.value = [];
|
||||
mocks.friendStore.allFavoriteFriendIds.value = new Set();
|
||||
mocks.friendStore.onlineFriends.value = [];
|
||||
mocks.friendStore.activeFriends.value = [];
|
||||
mocks.friendStore.offlineFriends.value = [];
|
||||
mocks.friendStore.friendsInSameInstance.value = [];
|
||||
|
||||
mocks.appearanceStore.isSidebarGroupByInstance.value = false;
|
||||
mocks.appearanceStore.isHideFriendsInSameInstance.value = false;
|
||||
mocks.appearanceStore.isSidebarDivideByFriendGroup.value = false;
|
||||
mocks.appearanceStore.sidebarFavoriteGroups.value = [];
|
||||
mocks.appearanceStore.sidebarFavoriteGroupOrder.value = [];
|
||||
mocks.appearanceStore.sidebarSortMethods.value = [];
|
||||
|
||||
mocks.configRepository.getBool.mockImplementation(
|
||||
(_key, defaultValue) => Promise.resolve(defaultValue ?? false)
|
||||
);
|
||||
mocks.configRepository.setBool.mockResolvedValue(undefined);
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test('renders online section and friend rows', async () => {
|
||||
mocks.friendStore.onlineFriends.value = [makeFriend('usr_online')];
|
||||
|
||||
const wrapper = mount(FriendsSidebar);
|
||||
await flushPromises();
|
||||
await nextTick();
|
||||
|
||||
expect(wrapper.text()).toContain('side_panel.online');
|
||||
expect(wrapper.findAll('[data-testid="friend-item"]').length).toBe(1);
|
||||
expect(wrapper.text()).toContain('usr_online');
|
||||
});
|
||||
|
||||
test('clicking online header collapses online rows and persists state', async () => {
|
||||
mocks.friendStore.onlineFriends.value = [makeFriend('usr_online')];
|
||||
const wrapper = mount(FriendsSidebar);
|
||||
await flushPromises();
|
||||
await nextTick();
|
||||
|
||||
const onlineHeader = wrapper
|
||||
.findAll('div.cursor-pointer')
|
||||
.find((node) => node.text().includes('side_panel.online'));
|
||||
expect(onlineHeader).toBeTruthy();
|
||||
|
||||
await onlineHeader.trigger('click');
|
||||
await flushPromises();
|
||||
await nextTick();
|
||||
|
||||
expect(wrapper.findAll('[data-testid="friend-item"]').length).toBe(0);
|
||||
expect(mocks.configRepository.setBool).toHaveBeenCalledWith(
|
||||
'VRCX_isFriendsGroupOnline',
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
test('renders same-instance section when grouping is enabled', async () => {
|
||||
mocks.appearanceStore.isSidebarGroupByInstance.value = true;
|
||||
mocks.friendStore.friendsInSameInstance.value = [
|
||||
[makeFriend('usr_a', 'wrld_same:1'), makeFriend('usr_b', 'wrld_same:1')]
|
||||
];
|
||||
|
||||
const wrapper = mount(FriendsSidebar);
|
||||
await flushPromises();
|
||||
await nextTick();
|
||||
|
||||
expect(wrapper.text()).toContain('side_panel.same_instance');
|
||||
expect(wrapper.findAll('[data-testid="friend-item"]').length).toBe(2);
|
||||
expect(wrapper.text()).toContain('(2)');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,182 @@
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import { ref } from 'vue';
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
router: {
|
||||
push: vi.fn()
|
||||
},
|
||||
inviteStore: {
|
||||
refreshInviteMessageTableData: vi.fn()
|
||||
},
|
||||
galleryStore: {
|
||||
clearInviteImageUpload: vi.fn()
|
||||
},
|
||||
notificationStore: null
|
||||
}));
|
||||
|
||||
vi.mock('pinia', () => ({
|
||||
storeToRefs: (store) => store
|
||||
}));
|
||||
|
||||
vi.mock('vue-router', () => ({
|
||||
useRouter: () => mocks.router
|
||||
}));
|
||||
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({
|
||||
t: (key) => key
|
||||
})
|
||||
}));
|
||||
|
||||
vi.mock('../../../../stores', () => ({
|
||||
useInviteStore: () => mocks.inviteStore,
|
||||
useGalleryStore: () => mocks.galleryStore,
|
||||
useNotificationStore: () => mocks.notificationStore
|
||||
}));
|
||||
|
||||
vi.mock('@/components/ui/sheet', () => ({
|
||||
Sheet: {
|
||||
props: ['open'],
|
||||
emits: ['update:open'],
|
||||
template: '<div data-testid="sheet"><slot /></div>'
|
||||
},
|
||||
SheetContent: {
|
||||
template: '<div data-testid="sheet-content"><slot /></div>'
|
||||
},
|
||||
SheetHeader: {
|
||||
template: '<div data-testid="sheet-header"><slot /></div>'
|
||||
},
|
||||
SheetTitle: {
|
||||
template: '<div data-testid="sheet-title"><slot /></div>'
|
||||
}
|
||||
}));
|
||||
|
||||
vi.mock('@/components/ui/tabs', () => ({
|
||||
Tabs: {
|
||||
props: ['modelValue'],
|
||||
emits: ['update:modelValue'],
|
||||
template:
|
||||
'<div data-testid="tabs" :data-model-value="modelValue"><slot /></div>'
|
||||
},
|
||||
TabsList: { template: '<div data-testid="tabs-list"><slot /></div>' },
|
||||
TabsTrigger: {
|
||||
props: ['value'],
|
||||
template:
|
||||
'<button data-testid="tabs-trigger" :data-value="value"><slot /></button>'
|
||||
},
|
||||
TabsContent: {
|
||||
props: ['value'],
|
||||
template:
|
||||
'<div data-testid="tabs-content" :data-value="value"><slot /></div>'
|
||||
}
|
||||
}));
|
||||
|
||||
vi.mock('../NotificationList.vue', () => ({
|
||||
default: {
|
||||
props: ['notifications', 'recentNotifications'],
|
||||
emits: [
|
||||
'show-invite-response',
|
||||
'show-invite-request-response',
|
||||
'navigate-to-table'
|
||||
],
|
||||
template:
|
||||
'<div data-testid="notification-list">' +
|
||||
'<button data-testid="emit-show-invite-response" @click="$emit(\'show-invite-response\', { id: \'invite_1\' })">invite-response</button>' +
|
||||
'<button data-testid="emit-show-invite-request-response" @click="$emit(\'show-invite-request-response\', { id: \'invite_2\' })">invite-request-response</button>' +
|
||||
'<button data-testid="emit-navigate" @click="$emit(\'navigate-to-table\')">navigate</button>' +
|
||||
'</div>'
|
||||
}
|
||||
}));
|
||||
|
||||
vi.mock('../../../Notifications/dialogs/SendInviteResponseDialog.vue', () => ({
|
||||
default: {
|
||||
props: ['sendInviteResponseDialogVisible'],
|
||||
template:
|
||||
'<div data-testid="dialog-response" :data-visible="String(sendInviteResponseDialogVisible)" />'
|
||||
}
|
||||
}));
|
||||
|
||||
vi.mock(
|
||||
'../../../Notifications/dialogs/SendInviteRequestResponseDialog.vue',
|
||||
() => ({
|
||||
default: {
|
||||
props: ['sendInviteRequestResponseDialogVisible'],
|
||||
template:
|
||||
'<div data-testid="dialog-request-response" :data-visible="String(sendInviteRequestResponseDialogVisible)" />'
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
import NotificationCenterSheet from '../NotificationCenterSheet.vue';
|
||||
|
||||
describe('NotificationCenterSheet.vue', () => {
|
||||
beforeEach(() => {
|
||||
mocks.router.push.mockReset();
|
||||
mocks.inviteStore.refreshInviteMessageTableData.mockReset();
|
||||
mocks.galleryStore.clearInviteImageUpload.mockReset();
|
||||
|
||||
mocks.notificationStore = {
|
||||
isNotificationCenterOpen: ref(false),
|
||||
unseenFriendNotifications: ref([]),
|
||||
unseenGroupNotifications: ref([]),
|
||||
unseenOtherNotifications: ref([]),
|
||||
recentFriendNotifications: ref([]),
|
||||
recentGroupNotifications: ref([]),
|
||||
recentOtherNotifications: ref([])
|
||||
};
|
||||
});
|
||||
|
||||
test('selects group tab when opening and only group unseen notifications exist', async () => {
|
||||
mocks.notificationStore.unseenGroupNotifications.value = [{ id: 'g1' }];
|
||||
const wrapper = mount(NotificationCenterSheet);
|
||||
|
||||
mocks.notificationStore.isNotificationCenterOpen.value = true;
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.get('[data-testid="tabs"]').attributes('data-model-value')).toBe(
|
||||
'group'
|
||||
);
|
||||
});
|
||||
|
||||
test('navigate-to-table closes center and routes to notification page', async () => {
|
||||
mocks.notificationStore.isNotificationCenterOpen.value = true;
|
||||
const wrapper = mount(NotificationCenterSheet);
|
||||
|
||||
await wrapper.get('[data-testid="emit-navigate"]').trigger('click');
|
||||
|
||||
expect(mocks.notificationStore.isNotificationCenterOpen.value).toBe(
|
||||
false
|
||||
);
|
||||
expect(mocks.router.push).toHaveBeenCalledWith({ name: 'notification' });
|
||||
});
|
||||
|
||||
test('show invite response/request dialogs trigger side effects', async () => {
|
||||
const wrapper = mount(NotificationCenterSheet);
|
||||
|
||||
await wrapper
|
||||
.get('[data-testid="emit-show-invite-response"]')
|
||||
.trigger('click');
|
||||
|
||||
expect(mocks.inviteStore.refreshInviteMessageTableData).toHaveBeenCalledWith(
|
||||
'response'
|
||||
);
|
||||
expect(mocks.galleryStore.clearInviteImageUpload).toHaveBeenCalled();
|
||||
expect(
|
||||
wrapper.get('[data-testid="dialog-response"]').attributes('data-visible')
|
||||
).toBe('true');
|
||||
|
||||
await wrapper
|
||||
.get('[data-testid="emit-show-invite-request-response"]')
|
||||
.trigger('click');
|
||||
|
||||
expect(mocks.inviteStore.refreshInviteMessageTableData).toHaveBeenCalledWith(
|
||||
'requestResponse'
|
||||
);
|
||||
expect(
|
||||
wrapper
|
||||
.get('[data-testid="dialog-request-response"]')
|
||||
.attributes('data-visible')
|
||||
).toBe('true');
|
||||
});
|
||||
});
|
||||
221
src/views/Sidebar/components/__tests__/NotificationItem.test.js
Normal file
221
src/views/Sidebar/components/__tests__/NotificationItem.test.js
Normal file
@@ -0,0 +1,221 @@
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import dayjs from 'dayjs';
|
||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
notificationStore: {
|
||||
acceptFriendRequestNotification: vi.fn(),
|
||||
acceptRequestInvite: vi.fn(),
|
||||
hideNotificationPrompt: vi.fn(),
|
||||
deleteNotificationLogPrompt: vi.fn(),
|
||||
sendNotificationResponse: vi.fn(),
|
||||
queueMarkAsSeen: vi.fn(),
|
||||
openNotificationLink: vi.fn(),
|
||||
isNotificationExpired: vi.fn(() => false)
|
||||
},
|
||||
userStore: {
|
||||
cachedUsers: new Map(),
|
||||
showUserDialog: vi.fn(),
|
||||
showSendBoopDialog: vi.fn()
|
||||
},
|
||||
groupStore: {
|
||||
showGroupDialog: vi.fn()
|
||||
},
|
||||
locationStore: {
|
||||
lastLocation: { value: { location: 'wrld_home:123' } }
|
||||
},
|
||||
gameStore: {
|
||||
isGameRunning: { value: true }
|
||||
}
|
||||
}));
|
||||
|
||||
vi.mock('pinia', () => ({
|
||||
storeToRefs: (store) => store
|
||||
}));
|
||||
|
||||
vi.mock('../../../../stores', () => ({
|
||||
useNotificationStore: () => mocks.notificationStore,
|
||||
useUserStore: () => mocks.userStore,
|
||||
useGroupStore: () => mocks.groupStore,
|
||||
useLocationStore: () => mocks.locationStore,
|
||||
useGameStore: () => mocks.gameStore
|
||||
}));
|
||||
|
||||
vi.mock('../../../../shared/utils', () => ({
|
||||
checkCanInvite: vi.fn(() => true),
|
||||
userImage: vi.fn(() => 'https://example.com/avatar.png')
|
||||
}));
|
||||
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({
|
||||
t: (key) => key,
|
||||
te: () => false
|
||||
})
|
||||
}));
|
||||
|
||||
vi.mock('@/components/ui/item', () => ({
|
||||
Item: { template: '<div data-testid="item"><slot /></div>' },
|
||||
ItemMedia: { template: '<div data-testid="item-media"><slot /></div>' },
|
||||
ItemContent: { template: '<div data-testid="item-content"><slot /></div>' },
|
||||
ItemTitle: { template: '<div data-testid="item-title"><slot /></div>' },
|
||||
ItemDescription: {
|
||||
template: '<div data-testid="item-description"><slot /></div>'
|
||||
}
|
||||
}));
|
||||
|
||||
vi.mock('@/components/ui/avatar', () => ({
|
||||
Avatar: { template: '<div data-testid="avatar"><slot /></div>' },
|
||||
AvatarImage: {
|
||||
props: ['src'],
|
||||
template: '<img data-testid="avatar-image" :src="src" />'
|
||||
},
|
||||
AvatarFallback: { template: '<span data-testid="avatar-fallback"><slot /></span>' }
|
||||
}));
|
||||
|
||||
vi.mock('@/components/ui/hover-card', () => ({
|
||||
HoverCard: { template: '<div data-testid="hover-card"><slot /></div>' },
|
||||
HoverCardTrigger: { template: '<div data-testid="hover-trigger"><slot /></div>' },
|
||||
HoverCardContent: { template: '<div data-testid="hover-content"><slot /></div>' }
|
||||
}));
|
||||
|
||||
vi.mock('@/components/ui/badge', () => ({
|
||||
Badge: { template: '<span data-testid="badge"><slot /></span>' }
|
||||
}));
|
||||
|
||||
vi.mock('@/components/ui/separator', () => ({
|
||||
Separator: { template: '<hr data-testid="separator" />' }
|
||||
}));
|
||||
|
||||
vi.mock('@/components/ui/tooltip', () => ({
|
||||
TooltipWrapper: { template: '<span data-testid="tooltip"><slot /></span>' }
|
||||
}));
|
||||
|
||||
vi.mock('../../../../components/Location.vue', () => ({
|
||||
default: {
|
||||
props: ['location'],
|
||||
template: '<span data-testid="location">{{ location }}</span>'
|
||||
}
|
||||
}));
|
||||
|
||||
vi.mock('lucide-vue-next', () => {
|
||||
function icon(name) {
|
||||
return { template: `<span data-icon="${name}" />` };
|
||||
}
|
||||
return {
|
||||
Ban: icon('Ban'),
|
||||
Bell: icon('Bell'),
|
||||
BellOff: icon('BellOff'),
|
||||
CalendarDays: icon('CalendarDays'),
|
||||
Check: icon('Check'),
|
||||
Link: icon('Link'),
|
||||
Mail: icon('Mail'),
|
||||
MessageCircle: icon('MessageCircle'),
|
||||
Reply: icon('Reply'),
|
||||
Send: icon('Send'),
|
||||
Tag: icon('Tag'),
|
||||
Trash2: icon('Trash2'),
|
||||
UserPlus: icon('UserPlus'),
|
||||
Users: icon('Users'),
|
||||
X: icon('X')
|
||||
};
|
||||
});
|
||||
|
||||
import NotificationItem from '../NotificationItem.vue';
|
||||
|
||||
function makeNotification(overrides = {}) {
|
||||
return {
|
||||
id: 'noty_1',
|
||||
type: 'friendRequest',
|
||||
senderUserId: 'usr_123',
|
||||
senderUsername: 'Alice',
|
||||
created_at: '2026-03-09T00:00:00.000Z',
|
||||
seen: false,
|
||||
details: {},
|
||||
...overrides
|
||||
};
|
||||
}
|
||||
|
||||
describe('NotificationItem.vue', () => {
|
||||
beforeEach(() => {
|
||||
mocks.notificationStore.acceptFriendRequestNotification.mockReset();
|
||||
mocks.notificationStore.acceptRequestInvite.mockReset();
|
||||
mocks.notificationStore.hideNotificationPrompt.mockReset();
|
||||
mocks.notificationStore.deleteNotificationLogPrompt.mockReset();
|
||||
mocks.notificationStore.sendNotificationResponse.mockReset();
|
||||
mocks.notificationStore.queueMarkAsSeen.mockReset();
|
||||
mocks.notificationStore.openNotificationLink.mockReset();
|
||||
mocks.notificationStore.isNotificationExpired.mockReturnValue(false);
|
||||
mocks.userStore.showUserDialog.mockReset();
|
||||
mocks.userStore.showSendBoopDialog.mockReset();
|
||||
mocks.groupStore.showGroupDialog.mockReset();
|
||||
mocks.userStore.cachedUsers = new Map();
|
||||
});
|
||||
|
||||
test('renders sender and opens user dialog on sender click', async () => {
|
||||
const wrapper = mount(NotificationItem, {
|
||||
props: {
|
||||
notification: makeNotification()
|
||||
}
|
||||
});
|
||||
|
||||
expect(wrapper.text()).toContain('Alice');
|
||||
await wrapper.get('span.truncate.cursor-pointer').trigger('click');
|
||||
expect(mocks.userStore.showUserDialog).toHaveBeenCalledWith('usr_123');
|
||||
});
|
||||
|
||||
test('clicking accept icon calls acceptFriendRequestNotification', async () => {
|
||||
const wrapper = mount(NotificationItem, {
|
||||
props: {
|
||||
notification: makeNotification()
|
||||
}
|
||||
});
|
||||
|
||||
await wrapper.get('[data-icon="Check"]').trigger('click');
|
||||
expect(
|
||||
mocks.notificationStore.acceptFriendRequestNotification
|
||||
).toHaveBeenCalledWith(expect.objectContaining({ id: 'noty_1' }));
|
||||
});
|
||||
|
||||
test('link response calls openNotificationLink', async () => {
|
||||
const wrapper = mount(NotificationItem, {
|
||||
props: {
|
||||
notification: makeNotification({
|
||||
type: 'message',
|
||||
responses: [
|
||||
{
|
||||
type: 'link',
|
||||
icon: 'reply',
|
||||
text: 'Open',
|
||||
data: 'group:grp_123'
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
await wrapper.get('[data-icon="Link"]').trigger('click');
|
||||
expect(mocks.notificationStore.openNotificationLink).toHaveBeenCalledWith(
|
||||
'group:grp_123'
|
||||
);
|
||||
});
|
||||
|
||||
test('unmount queues mark-as-seen for unseen notification', () => {
|
||||
const wrapper = mount(NotificationItem, {
|
||||
props: {
|
||||
notification: makeNotification({
|
||||
version: 2
|
||||
}),
|
||||
isUnseen: true
|
||||
}
|
||||
});
|
||||
|
||||
wrapper.unmount();
|
||||
expect(mocks.notificationStore.queueMarkAsSeen).toHaveBeenCalledWith(
|
||||
'noty_1',
|
||||
2
|
||||
);
|
||||
});
|
||||
});
|
||||
127
src/views/Sidebar/components/__tests__/NotificationList.test.js
Normal file
127
src/views/Sidebar/components/__tests__/NotificationList.test.js
Normal file
@@ -0,0 +1,127 @@
|
||||
import { describe, expect, test, vi } from 'vitest';
|
||||
import { mount } from '@vue/test-utils';
|
||||
|
||||
vi.mock('@tanstack/vue-virtual', () => ({
|
||||
useVirtualizer: (optionsRef) => ({
|
||||
value: {
|
||||
getVirtualItems: () => {
|
||||
const options = optionsRef.value;
|
||||
return Array.from({ length: options.count }, (_, index) => ({
|
||||
index,
|
||||
key: options.getItemKey?.(index) ?? index,
|
||||
start: index * 56
|
||||
}));
|
||||
},
|
||||
getTotalSize: () => optionsRef.value.count * 56,
|
||||
measure: vi.fn(),
|
||||
measureElement: vi.fn()
|
||||
}
|
||||
})
|
||||
}));
|
||||
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({
|
||||
t: (key) => key
|
||||
})
|
||||
}));
|
||||
|
||||
vi.mock('@/components/ui/button', () => ({
|
||||
Button: {
|
||||
emits: ['click'],
|
||||
template:
|
||||
'<button data-testid="view-more" @click="$emit(\'click\')"><slot /></button>'
|
||||
}
|
||||
}));
|
||||
|
||||
vi.mock('@/components/ui/separator', () => ({
|
||||
Separator: { template: '<hr data-testid="separator" />' }
|
||||
}));
|
||||
|
||||
vi.mock('../NotificationItem.vue', () => ({
|
||||
default: {
|
||||
props: ['notification', 'isUnseen'],
|
||||
emits: ['show-invite-response', 'show-invite-request-response'],
|
||||
template:
|
||||
'<div data-testid="notification-item" :data-id="notification.id" :data-unseen="String(isUnseen)">' +
|
||||
'{{ notification.id }}' +
|
||||
'<button data-testid="emit-invite-response" @click="$emit(\'show-invite-response\', notification)">invite-response</button>' +
|
||||
'<button data-testid="emit-invite-request-response" @click="$emit(\'show-invite-request-response\', notification)">invite-request-response</button>' +
|
||||
'</div>'
|
||||
}
|
||||
}));
|
||||
|
||||
import NotificationList from '../NotificationList.vue';
|
||||
|
||||
function makeNoty(id, createdAt) {
|
||||
return {
|
||||
id,
|
||||
created_at: createdAt,
|
||||
type: 'friendRequest'
|
||||
};
|
||||
}
|
||||
|
||||
describe('NotificationList.vue', () => {
|
||||
test('renders empty state when there are no rows', () => {
|
||||
const wrapper = mount(NotificationList, {
|
||||
props: {
|
||||
notifications: [],
|
||||
recentNotifications: []
|
||||
}
|
||||
});
|
||||
|
||||
expect(wrapper.text()).toContain(
|
||||
'side_panel.notification_center.no_new_notifications'
|
||||
);
|
||||
});
|
||||
|
||||
test('sorts unseen notifications desc and renders recent section header', () => {
|
||||
const wrapper = mount(NotificationList, {
|
||||
props: {
|
||||
notifications: [
|
||||
makeNoty('old', '2026-03-08T00:00:00.000Z'),
|
||||
makeNoty('new', '2026-03-09T00:00:00.000Z')
|
||||
],
|
||||
recentNotifications: [makeNoty('recent1', '2026-03-07T00:00:00.000Z')]
|
||||
}
|
||||
});
|
||||
|
||||
const items = wrapper.findAll('[data-testid="notification-item"]');
|
||||
expect(items.map((x) => x.attributes('data-id'))).toEqual([
|
||||
'new',
|
||||
'old',
|
||||
'recent1'
|
||||
]);
|
||||
expect(wrapper.text()).toContain(
|
||||
'side_panel.notification_center.past_notifications'
|
||||
);
|
||||
});
|
||||
|
||||
test('emits navigate-to-table when view-more button is clicked', async () => {
|
||||
const wrapper = mount(NotificationList, {
|
||||
props: {
|
||||
notifications: [makeNoty('n1', '2026-03-09T00:00:00.000Z')],
|
||||
recentNotifications: []
|
||||
}
|
||||
});
|
||||
|
||||
await wrapper.get('[data-testid="view-more"]').trigger('click');
|
||||
expect(wrapper.emitted('navigate-to-table')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('re-emits invite-related events from NotificationItem', async () => {
|
||||
const wrapper = mount(NotificationList, {
|
||||
props: {
|
||||
notifications: [makeNoty('n1', '2026-03-09T00:00:00.000Z')],
|
||||
recentNotifications: []
|
||||
}
|
||||
});
|
||||
|
||||
await wrapper.get('[data-testid="emit-invite-response"]').trigger('click');
|
||||
await wrapper
|
||||
.get('[data-testid="emit-invite-request-response"]')
|
||||
.trigger('click');
|
||||
|
||||
expect(wrapper.emitted('show-invite-response')).toBeTruthy();
|
||||
expect(wrapper.emitted('show-invite-request-response')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user