From fadead9c801b36c13012553886b759699455e653 Mon Sep 17 00:00:00 2001 From: pa Date: Mon, 16 Mar 2026 16:01:07 +0900 Subject: [PATCH] fix test --- src/components/StatusBar.vue | 1 + src/components/__tests__/StatusBar.test.js | 4 +-- src/components/__tests__/Timer.test.js | 35 ++++++++---------- .../__tests__/useAvatarDialogCommands.test.js | 4 +++ .../__tests__/UserActionDropdown.test.js | 5 +++ .../__tests__/UserDialogAvatarsTab.test.js | 4 +-- .../__tests__/useUserDialogCommands.test.js | 7 ++++ .../__tests__/useWorldDialogCommands.test.js | 4 +++ .../__tests__/NavMenuFolderItem.test.js | 6 ++-- .../__tests__/useNavLayout.test.js | 4 ++- src/stores/user.js | 4 +-- .../__tests__/FavoritesAvatarItem.test.js | 9 ++--- .../FavoritesAvatarLocalHistoryItem.test.js | 9 ++--- .../__tests__/FavoritesFriendItem.test.js | 3 +- .../__tests__/FavoritesWorldItem.test.js | 1 + .../__tests__/FriendsLocationsCard.test.js | 10 ++++-- src/views/Layout/__tests__/MainLayout.test.js | 10 ++++-- .../PlayerList/__tests__/PlayerList.test.js | 35 ++++++++++++++---- .../Tabs/__tests__/DiscordPresenceTab.test.js | 36 ++++++++++++------- .../__tests__/WristOverlaySettings.test.js | 25 ++++++++----- .../__tests__/FriendsSidebar.test.js | 1 + src/views/Tools/__tests__/Tools.test.js | 34 +++++++++++------- .../components/__tests__/ToolItem.test.js | 11 ++---- vitest.setup.js | 13 +++++++ 24 files changed, 183 insertions(+), 92 deletions(-) diff --git a/src/components/StatusBar.vue b/src/components/StatusBar.vue index f622aac3..1078e5d0 100644 --- a/src/components/StatusBar.vue +++ b/src/components/StatusBar.vue @@ -211,6 +211,7 @@ :step="1" :format-options="{ maximumFractionDigits: 0 }" class="w-20" + @click.stop @update:modelValue="setZoomLevel"> diff --git a/src/components/__tests__/StatusBar.test.js b/src/components/__tests__/StatusBar.test.js index c1b0c06f..8a8da442 100644 --- a/src/components/__tests__/StatusBar.test.js +++ b/src/components/__tests__/StatusBar.test.js @@ -236,7 +236,7 @@ describe('StatusBar.vue - Servers indicator', () => { expect(wrapper.text()).toContain('Servers'); const serversDots = wrapper.findAll('.bg-status-online'); expect(serversDots.length).toBeGreaterThan(0); - expect(wrapper.find('.bg-\\[\\#e6a23c\\]').exists()).toBe(false); + expect(wrapper.find('.bg-status-askme').exists()).toBe(false); }); test('shows Servers indicator with yellow dot when there is an issue', () => { @@ -246,7 +246,7 @@ describe('StatusBar.vue - Servers indicator', () => { } }); expect(wrapper.text()).toContain('Servers'); - expect(wrapper.find('.bg-\\[\\#e6a23c\\]').exists()).toBe(true); + expect(wrapper.find('.bg-status-askme').exists()).toBe(true); }); test('shows HoverCard content with status text when there is an issue', () => { diff --git a/src/components/__tests__/Timer.test.js b/src/components/__tests__/Timer.test.js index d53193b5..739a6f41 100644 --- a/src/components/__tests__/Timer.test.js +++ b/src/components/__tests__/Timer.test.js @@ -1,30 +1,26 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { mount } from '@vue/test-utils'; -import { nextTick } from 'vue'; +import { nextTick, ref } from 'vue'; const mocks = vi.hoisted(() => ({ - timeToText: vi.fn((ms) => `${ms}ms`) + timeToText: vi.fn((ms) => `${ms}ms`), + nowRef: null })); vi.mock('../../shared/utils', () => ({ timeToText: (...args) => mocks.timeToText(...args) })); +vi.mock('@vueuse/core', () => ({ + useNow: () => mocks.nowRef +})); + import Timer from '../Timer.vue'; describe('Timer.vue', () => { - let intervalCallback; - beforeEach(() => { - intervalCallback = null; mocks.timeToText.mockClear(); - - vi.spyOn(globalThis, 'setInterval').mockImplementation((cb) => { - intervalCallback = cb; - return 99; - }); - vi.spyOn(globalThis, 'clearInterval').mockImplementation(() => {}); - vi.spyOn(Date, 'now').mockReturnValue(10000); + mocks.nowRef = ref(10000); }); afterEach(() => { @@ -42,15 +38,14 @@ describe('Timer.vue', () => { expect(mocks.timeToText).toHaveBeenCalledWith(6000); }); - it('updates text when interval callback runs', async () => { + it('updates text when now value changes', async () => { const wrapper = mount(Timer, { props: { epoch: 4000 } }); - vi.mocked(Date.now).mockReturnValue(13000); - intervalCallback?.(); + mocks.nowRef.value = 13000; await nextTick(); expect(wrapper.text()).toBe('9000ms'); @@ -66,15 +61,15 @@ describe('Timer.vue', () => { expect(wrapper.text()).toBe('-'); }); - it('clears interval on unmount', () => { + it('computes correct elapsed time', () => { + mocks.nowRef.value = 20000; const wrapper = mount(Timer, { props: { - epoch: 1 + epoch: 5000 } }); - wrapper.unmount(); - - expect(clearInterval).toHaveBeenCalledWith(99); + expect(wrapper.text()).toBe('15000ms'); + expect(mocks.timeToText).toHaveBeenCalledWith(15000); }); }); diff --git a/src/components/dialogs/AvatarDialog/__tests__/useAvatarDialogCommands.test.js b/src/components/dialogs/AvatarDialog/__tests__/useAvatarDialogCommands.test.js index c39c516d..3385983e 100644 --- a/src/components/dialogs/AvatarDialog/__tests__/useAvatarDialogCommands.test.js +++ b/src/components/dialogs/AvatarDialog/__tests__/useAvatarDialogCommands.test.js @@ -38,6 +38,10 @@ vi.mock('../../../../coordinators/imageUploadCoordinator', () => ({ uploadImageLegacy: vi.fn() })); +vi.mock('../../../../coordinators/avatarCoordinator', () => ({ + removeAvatarFromCache: vi.fn() +})); + const { copyToClipboard, openExternalLink } = await import('../../../../shared/utils'); const { favoriteRequest, avatarRequest, avatarModerationRequest } = diff --git a/src/components/dialogs/UserDialog/__tests__/UserActionDropdown.test.js b/src/components/dialogs/UserDialog/__tests__/UserActionDropdown.test.js index 9ed92041..1046479b 100644 --- a/src/components/dialogs/UserDialog/__tests__/UserActionDropdown.test.js +++ b/src/components/dialogs/UserDialog/__tests__/UserActionDropdown.test.js @@ -27,11 +27,15 @@ vi.mock('../../../../stores', () => ({ vi.mock('../../../../composables/useInviteChecks', () => ({ useInviteChecks: () => ({ checkCanInvite: () => true }) })); +vi.mock('../../../../composables/useRecentActions', () => ({ + isActionRecent: () => false +})); vi.mock('../../../ui/dropdown-menu', () => ({ DropdownMenu: { template: '
' }, DropdownMenuTrigger: { template: '
' }, DropdownMenuContent: { template: '
' }, DropdownMenuSeparator: { template: '
' }, + DropdownMenuShortcut: { template: '' }, DropdownMenuItem: { emits: ['click'], template: @@ -51,6 +55,7 @@ vi.mock('../../../ui/tooltip', () => ({ vi.mock('lucide-vue-next', () => ({ Check: { template: '' }, CheckCircle: { template: '' }, + Clock: { template: '' }, Flag: { template: '' }, LineChart: { template: '' }, Mail: { template: '' }, diff --git a/src/components/dialogs/UserDialog/__tests__/UserDialogAvatarsTab.test.js b/src/components/dialogs/UserDialog/__tests__/UserDialogAvatarsTab.test.js index 49b62417..a062ba10 100644 --- a/src/components/dialogs/UserDialog/__tests__/UserDialogAvatarsTab.test.js +++ b/src/components/dialogs/UserDialog/__tests__/UserDialogAvatarsTab.test.js @@ -223,13 +223,13 @@ describe('UserDialogAvatarsTab.vue', () => { expect(input.exists()).toBe(true); }); - test('does not render search input for other users', () => { + test('renders search input for other users too', () => { const wrapper = mountComponent({ id: 'usr_other', ref: { id: 'usr_other' } }); const input = wrapper.find('input'); - expect(input.exists()).toBe(false); + expect(input.exists()).toBe(true); }); test('filters avatars by search query', async () => { diff --git a/src/components/dialogs/UserDialog/__tests__/useUserDialogCommands.test.js b/src/components/dialogs/UserDialog/__tests__/useUserDialogCommands.test.js index 8c492ea4..2108830e 100644 --- a/src/components/dialogs/UserDialog/__tests__/useUserDialogCommands.test.js +++ b/src/components/dialogs/UserDialog/__tests__/useUserDialogCommands.test.js @@ -39,6 +39,13 @@ vi.mock('../../../../services/database', () => ({ } })); +vi.mock('../../../../composables/useRecentActions', () => ({ + recordRecentAction: vi.fn(), + useRecentActions: () => ({ + isRecentAction: vi.fn(() => false) + }) +})); + // Import mocks after vi.mock const { copyToClipboard } = await import('../../../../shared/utils'); const { diff --git a/src/components/dialogs/WorldDialog/__tests__/useWorldDialogCommands.test.js b/src/components/dialogs/WorldDialog/__tests__/useWorldDialogCommands.test.js index e4b3f9a7..74dc18d5 100644 --- a/src/components/dialogs/WorldDialog/__tests__/useWorldDialogCommands.test.js +++ b/src/components/dialogs/WorldDialog/__tests__/useWorldDialogCommands.test.js @@ -36,6 +36,10 @@ vi.mock('../../../../coordinators/imageUploadCoordinator', () => ({ uploadImageLegacy: vi.fn() })); +vi.mock('../../../../coordinators/worldCoordinator', () => ({ + removeWorldFromCache: vi.fn() +})); + const { favoriteRequest, miscRequest, userRequest, worldRequest } = await import('../../../../api'); const { openExternalLink } = await import('../../../../shared/utils'); diff --git a/src/components/nav-menu/__tests__/NavMenuFolderItem.test.js b/src/components/nav-menu/__tests__/NavMenuFolderItem.test.js index aedd7261..e4740597 100644 --- a/src/components/nav-menu/__tests__/NavMenuFolderItem.test.js +++ b/src/components/nav-menu/__tests__/NavMenuFolderItem.test.js @@ -84,7 +84,8 @@ describe('NavMenuFolderItem', () => { hasNotifications: false, isEntryNotified: () => false, isNavItemNotified: () => false, - isDashboardItem: () => false + isDashboardItem: () => false, + isToolItem: () => false } }); @@ -103,7 +104,8 @@ describe('NavMenuFolderItem', () => { hasNotifications: false, isEntryNotified: () => false, isNavItemNotified: () => false, - isDashboardItem: () => false + isDashboardItem: () => false, + isToolItem: () => false } }); diff --git a/src/components/nav-menu/composables/__tests__/useNavLayout.test.js b/src/components/nav-menu/composables/__tests__/useNavLayout.test.js index 740483f3..e241f977 100644 --- a/src/components/nav-menu/composables/__tests__/useNavLayout.test.js +++ b/src/components/nav-menu/composables/__tests__/useNavLayout.test.js @@ -92,7 +92,9 @@ describe('useNavLayout', () => { await applyCustomNavLayout(layout, []); await nextTick(); - expect(navLayout.value).toEqual(layout); + expect(navLayout.value).toEqual( + expect.arrayContaining(layout) + ); expect(mocks.setString).toHaveBeenCalled(); }); }); diff --git a/src/stores/user.js b/src/stores/user.js index 7d681ddb..435b0215 100644 --- a/src/stores/user.js +++ b/src/stores/user.js @@ -1,6 +1,6 @@ import { computed, reactive, ref, shallowReactive, watch } from 'vue'; import { defineStore } from 'pinia'; -import { useI18n } from 'vue-i18n'; + import { compareByCreatedAt, @@ -33,7 +33,7 @@ export const useUserStore = defineStore('User', () => { const locationStore = useLocationStore(); const instanceStore = useInstanceStore(); const uiStore = useUiStore(); - const { t } = useI18n(); + const currentUser = ref({ acceptedPrivacyVersion: 0, diff --git a/src/views/Favorites/components/__tests__/FavoritesAvatarItem.test.js b/src/views/Favorites/components/__tests__/FavoritesAvatarItem.test.js index 53ebdfac..682b5f83 100644 --- a/src/views/Favorites/components/__tests__/FavoritesAvatarItem.test.js +++ b/src/views/Favorites/components/__tests__/FavoritesAvatarItem.test.js @@ -119,6 +119,7 @@ vi.mock('@/components/ui/dropdown-menu', () => ({ vi.mock('lucide-vue-next', () => ({ AlertTriangle: { template: '' }, + Image: { template: '' }, Lock: { template: '' }, MoreHorizontal: { template: '' }, Trash2: { template: '' } @@ -207,7 +208,7 @@ describe('FavoritesAvatarItem.vue', () => { ).toContain('rounded-sm'); }); - it('shows fallback text when thumbnail is missing', () => { + it('shows fallback icon when thumbnail is missing', () => { const wrapper = mountItem({ favorite: { id: 'avtr_no_thumb', @@ -223,9 +224,9 @@ describe('FavoritesAvatarItem.vue', () => { expect(wrapper.find('[data-testid="avatar-image"]').exists()).toBe( false ); - expect(wrapper.get('[data-testid="avatar-fallback"]').text()).toContain( - 'B' - ); + expect( + wrapper.get('[data-testid="avatar-fallback"]').find('i').exists() + ).toBe(true); }); it('uses local delete flow for local favorites', async () => { diff --git a/src/views/Favorites/components/__tests__/FavoritesAvatarLocalHistoryItem.test.js b/src/views/Favorites/components/__tests__/FavoritesAvatarLocalHistoryItem.test.js index 69f658b7..fe462a2a 100644 --- a/src/views/Favorites/components/__tests__/FavoritesAvatarLocalHistoryItem.test.js +++ b/src/views/Favorites/components/__tests__/FavoritesAvatarLocalHistoryItem.test.js @@ -96,6 +96,7 @@ vi.mock('@/components/ui/dropdown-menu', () => ({ })); vi.mock('lucide-vue-next', () => ({ + Image: { template: '' }, MoreHorizontal: { template: '' } })); @@ -173,7 +174,7 @@ describe('FavoritesAvatarLocalHistoryItem.vue', () => { ).toContain('rounded-sm'); }); - it('shows fallback text when thumbnail is missing', () => { + it('shows fallback icon when thumbnail is missing', () => { const wrapper = mountItem({ favorite: { id: 'avtr_hist_no_thumb', @@ -185,9 +186,9 @@ describe('FavoritesAvatarLocalHistoryItem.vue', () => { expect(wrapper.find('[data-testid="avatar-image"]').exists()).toBe( false ); - expect(wrapper.get('[data-testid="avatar-fallback"]').text()).toContain( - 'C' - ); + expect( + wrapper.get('[data-testid="avatar-fallback"]').find('i').exists() + ).toBe(true); }); it('runs select-avatar action from menu', async () => { diff --git a/src/views/Favorites/components/__tests__/FavoritesFriendItem.test.js b/src/views/Favorites/components/__tests__/FavoritesFriendItem.test.js index 1ef46c26..079f86da 100644 --- a/src/views/Favorites/components/__tests__/FavoritesFriendItem.test.js +++ b/src/views/Favorites/components/__tests__/FavoritesFriendItem.test.js @@ -177,7 +177,8 @@ vi.mock('@/components/ui/dropdown-menu', () => ({ vi.mock('lucide-vue-next', () => ({ MoreHorizontal: { template: '' }, - Trash2: { template: '' } + Trash2: { template: '' }, + User: { template: '' } })); import FavoritesFriendItem from '../FavoritesFriendItem.vue'; diff --git a/src/views/Favorites/components/__tests__/FavoritesWorldItem.test.js b/src/views/Favorites/components/__tests__/FavoritesWorldItem.test.js index 603467d6..f61b4eaf 100644 --- a/src/views/Favorites/components/__tests__/FavoritesWorldItem.test.js +++ b/src/views/Favorites/components/__tests__/FavoritesWorldItem.test.js @@ -109,6 +109,7 @@ vi.mock('@/components/ui/checkbox', () => ({ vi.mock('lucide-vue-next', () => ({ AlertTriangle: { template: '' }, + Image: { template: '' }, Lock: { template: '' }, MoreHorizontal: { template: '' } })); diff --git a/src/views/FriendsLocations/components/__tests__/FriendsLocationsCard.test.js b/src/views/FriendsLocations/components/__tests__/FriendsLocationsCard.test.js index 6b22a95a..cfecb384 100644 --- a/src/views/FriendsLocations/components/__tests__/FriendsLocationsCard.test.js +++ b/src/views/FriendsLocations/components/__tests__/FriendsLocationsCard.test.js @@ -196,6 +196,11 @@ const i18n = createI18n({ messages: { en } }); +vi.mock('lucide-vue-next', () => ({ + Pencil: { template: '' }, + User: { template: '' } +})); + // Stub all complex UI components — render slots transparently const stubs = { ContextMenu: { template: '
' }, @@ -229,6 +234,7 @@ const stubs = { props: ['location', 'traveling', 'link', 'class'] }, Pencil: { template: '', props: ['class'] }, + User: { template: '', props: ['class', 'size'] }, TooltipWrapper: { template: '', props: ['content', 'disabled', 'delayDuration', 'side'] @@ -356,11 +362,11 @@ describe('FriendsLocationsCard.vue', () => { expect(wrapper.text()).toContain('A'); }); - test('shows ? as avatar fallback when name is empty', () => { + test('shows user icon as avatar fallback when name is empty', () => { const wrapper = mountCard({ friend: makeFriend({ name: undefined }) }); - expect(wrapper.text()).toContain('?'); + expect(wrapper.find('.user-icon').exists()).toBe(true); }); test('hides location when displayInstanceInfo is false', () => { diff --git a/src/views/Layout/__tests__/MainLayout.test.js b/src/views/Layout/__tests__/MainLayout.test.js index 84c687d5..5498a12d 100644 --- a/src/views/Layout/__tests__/MainLayout.test.js +++ b/src/views/Layout/__tests__/MainLayout.test.js @@ -9,9 +9,13 @@ const mocks = vi.hoisted(() => ({ })); vi.mock('pinia', async (i) => ({ ...(await i()), storeToRefs: (s) => s })); -vi.mock('vue-router', () => ({ - useRouter: () => ({ replace: (...a) => mocks.replace(...a) }) -})); +vi.mock('vue-router', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + useRouter: () => ({ replace: (...a) => mocks.replace(...a) }) + }; +}); vi.mock('../../../services/watchState', () => ({ watchState: { isLoggedIn: false } })); diff --git a/src/views/PlayerList/__tests__/PlayerList.test.js b/src/views/PlayerList/__tests__/PlayerList.test.js index 0ef9478c..86794aaf 100644 --- a/src/views/PlayerList/__tests__/PlayerList.test.js +++ b/src/views/PlayerList/__tests__/PlayerList.test.js @@ -44,7 +44,14 @@ vi.mock('../../../stores', () => ({ photonLoggingEnabled: mocks.photonLoggingEnabled, chatboxUserBlacklist: mocks.chatboxUserBlacklist, saveChatboxUserBlacklist: (...args) => - mocks.saveChatboxUserBlacklist(...args) + mocks.saveChatboxUserBlacklist(...args), + photonEventTable: ref({ data: [], pageSize: 10 }), + photonEventTablePrevious: ref({ data: [], pageSize: 10 }), + photonEventTableTypeFilter: ref([]), + photonEventTableFilter: ref(''), + photonEventIcon: ref(false), + photonEventTableFilterChange: vi.fn(), + showUserFromPhotonId: vi.fn() }), useUserStore: () => ({ currentUser: mocks.currentUser @@ -65,6 +72,18 @@ vi.mock('../../../stores', () => ({ useGalleryStore: () => ({ showFullscreenImageDialog: (...args) => mocks.showFullscreenImageDialog(...args) + }), + useSearchStore: () => ({ + stringComparer: { value: (a, b) => a.localeCompare(b) } + }), + useAvatarStore: () => ({ + showAvatarDialog: vi.fn() + }), + useGroupStore: () => ({ + showGroupDialog: vi.fn() + }), + useVrcxStore: () => ({ + ipcEnabled: ref(false) }) })); @@ -142,12 +161,14 @@ vi.mock('../dialogs/ChatboxBlacklistDialog.vue', () => ({ } })); -vi.mock('lucide-vue-next', () => ({ - Apple: { template: '' }, - Home: { template: '' }, - Monitor: { template: '' }, - Smartphone: { template: '' } -})); +vi.mock('lucide-vue-next', async (importOriginal) => { + const actual = await importOriginal(); + const stubs = {}; + for (const key of Object.keys(actual)) { + stubs[key] = { template: '' }; + } + return stubs; +}); import PlayerList from '../PlayerList.vue'; diff --git a/src/views/Settings/components/Tabs/__tests__/DiscordPresenceTab.test.js b/src/views/Settings/components/Tabs/__tests__/DiscordPresenceTab.test.js index 6377b199..3adf9dc3 100644 --- a/src/views/Settings/components/Tabs/__tests__/DiscordPresenceTab.test.js +++ b/src/views/Settings/components/Tabs/__tests__/DiscordPresenceTab.test.js @@ -33,12 +33,23 @@ vi.mock('../../../../../stores', () => ({ }) })); -vi.mock('../../SimpleSwitch.vue', () => ({ - default: { - props: ['label', 'disabled'], - emits: ['change'], +vi.mock('@/components/ui/switch', () => ({ + Switch: { + props: ['modelValue', 'disabled'], + emits: ['update:modelValue'], template: - '
' + '' + } +})); + +vi.mock('../../SettingsGroup.vue', () => ({ + default: { template: '
' } +})); + +vi.mock('../../SettingsItem.vue', () => ({ + default: { + props: ['label', 'description'], + template: '
' } })); @@ -57,7 +68,7 @@ describe('DiscordPresenceTab.vue', () => { const wrapper = mount(DiscordPresenceTab); const tooltipRow = wrapper - .findAll('div.options-container-item') + .findAll('p') .find((node) => node .text() @@ -65,11 +76,13 @@ describe('DiscordPresenceTab.vue', () => { 'view.settings.discord_presence.discord_presence.enable_tooltip' ) ); + expect(tooltipRow).toBeTruthy(); await tooltipRow.trigger('click'); expect(mocks.showVRChatConfig).toHaveBeenCalledTimes(1); - await wrapper.findAll('.emit-change')[0].trigger('click'); + const switches = wrapper.findAll('[data-testid="switch"]'); + await switches[0].trigger('click'); expect(mocks.discordStore.setDiscordActive).toHaveBeenCalledTimes(1); expect(mocks.discordStore.saveDiscordOption).toHaveBeenCalled(); }); @@ -78,12 +91,9 @@ describe('DiscordPresenceTab.vue', () => { mocks.discordStore.discordActive.value = false; const wrapper = mount(DiscordPresenceTab); - const worldIntegration = wrapper - .findAll('[data-testid="simple-switch"]') - .find((node) => - node.attributes('data-label')?.includes('world_integration') - ); + const switches = wrapper.findAll('[data-testid="switch"]'); + const worldIntegrationSwitch = switches[1]; - expect(worldIntegration?.attributes('data-disabled')).toBe('true'); + expect(worldIntegrationSwitch?.attributes('data-disabled')).toBe('true'); }); }); diff --git a/src/views/Settings/components/__tests__/WristOverlaySettings.test.js b/src/views/Settings/components/__tests__/WristOverlaySettings.test.js index c03138c1..5a526ada 100644 --- a/src/views/Settings/components/__tests__/WristOverlaySettings.test.js +++ b/src/views/Settings/components/__tests__/WristOverlaySettings.test.js @@ -53,6 +53,15 @@ vi.mock('@/components/ui/button', () => ({ } })); +vi.mock('@/components/ui/switch', () => ({ + Switch: { + props: ['modelValue', 'disabled'], + emits: ['update:modelValue'], + template: + '' + } +})); + vi.mock('../../../../components/ui/radio-group', () => ({ RadioGroup: { props: ['modelValue', 'disabled'], @@ -72,13 +81,12 @@ vi.mock('../../../../components/ui/toggle-group', () => ({ ToggleGroupItem: { template: '
' } })); -vi.mock('../SimpleSwitch.vue', () => ({ - default: { - props: ['label'], - emits: ['change'], - template: - '
' - } +vi.mock('../SettingsGroup.vue', () => ({ + default: { template: '
' } +})); + +vi.mock('../SettingsItem.vue', () => ({ + default: { template: '
' } })); import WristOverlaySettings from '../WristOverlaySettings.vue'; @@ -102,7 +110,8 @@ describe('WristOverlaySettings.vue', () => { await wrapper.get('[data-testid="filters-btn"]').trigger('click'); expect(wrapper.emitted('open-feed-filters')).toBeTruthy(); - await wrapper.findAll('.emit-change')[0].trigger('click'); + const switches = wrapper.findAll('[data-testid="switch"]'); + await switches[0].trigger('click'); expect(mocks.notificationsStore.setOpenVR).toHaveBeenCalledTimes(1); expect(mocks.saveOpenVROption).toHaveBeenCalled(); diff --git a/src/views/Sidebar/components/__tests__/FriendsSidebar.test.js b/src/views/Sidebar/components/__tests__/FriendsSidebar.test.js index 5e0b390c..adfa88dc 100644 --- a/src/views/Sidebar/components/__tests__/FriendsSidebar.test.js +++ b/src/views/Sidebar/components/__tests__/FriendsSidebar.test.js @@ -14,6 +14,7 @@ const mocks = vi.hoisted(() => ({ appearanceStore: { isSidebarGroupByInstance: { value: false }, isHideFriendsInSameInstance: { value: false }, + isSameInstanceAboveFavorites: { value: false }, isSidebarDivideByFriendGroup: { value: false }, sidebarFavoriteGroups: { value: [] }, sidebarFavoriteGroupOrder: { value: [] }, diff --git a/src/views/Tools/__tests__/Tools.test.js b/src/views/Tools/__tests__/Tools.test.js index 295e8b74..ef143174 100644 --- a/src/views/Tools/__tests__/Tools.test.js +++ b/src/views/Tools/__tests__/Tools.test.js @@ -39,21 +39,22 @@ vi.mock('pinia', async (importOriginal) => { vi.mock('../../../stores', () => ({ useFriendStore: () => ({ friends }), - useGalleryStore: () => ({ showGalleryPage }) -})); - -vi.mock('../../../stores/settings/advanced', () => ({ - useAdvancedSettingsStore: () => ({ showVRChatConfig }) -})); - -vi.mock('../../../stores/launch', () => ({ - useLaunchStore: () => ({ showLaunchOptions }) -})); - -vi.mock('../../../stores/vrcx', () => ({ + useGalleryStore: () => ({ showGalleryPage }), + useToolsStore: () => ({ openDialog: vi.fn() }), + useAdvancedSettingsStore: () => ({ showVRChatConfig }), + useLaunchStore: () => ({ showLaunchOptions }), useVrcxStore: () => ({ showRegistryBackupDialog }) })); +vi.mock('../../../composables/useToolNavPinning', () => ({ + useToolNavPinning: () => ({ + pinToolToNav: vi.fn(), + pinnedToolKeys: new Set(), + refreshPinnedState: vi.fn().mockResolvedValue(undefined), + unpinToolFromNav: vi.fn() + }) +})); + vi.mock('../../../services/config.js', () => ({ default: { getString: (...args) => getString(...args), @@ -65,6 +66,13 @@ vi.mock('../dialogs/AutoChangeStatusDialog.vue', () => ({ default: { template: '
' } })); +vi.mock('../../../components/ui/tooltip', () => ({ + TooltipWrapper: { + template: '
', + props: ['content', 'disabled', 'side'] + } +})); + import Tools from '../Tools.vue'; function findToolItemByTitle(wrapper, titleKey) { @@ -113,7 +121,7 @@ describe('Tools.vue', () => { expect(galleryItem).toBeTruthy(); await galleryItem.trigger('click'); - expect(showGalleryPage).toHaveBeenCalled(); + expect(push).toHaveBeenCalledWith({ name: 'gallery' }); }); test('toggle category persists collapsed state', async () => { diff --git a/src/views/Tools/components/__tests__/ToolItem.test.js b/src/views/Tools/components/__tests__/ToolItem.test.js index 4b237f08..e0d1694e 100644 --- a/src/views/Tools/components/__tests__/ToolItem.test.js +++ b/src/views/Tools/components/__tests__/ToolItem.test.js @@ -1,24 +1,19 @@ import { describe, expect, test, vi } from 'vitest'; -import { defineComponent, markRaw } from 'vue'; import { mount } from '@vue/test-utils'; import ToolItem from '../ToolItem.vue'; describe('ToolItem.vue', () => { test('renders icon, title and description', () => { - const MockIcon = defineComponent({ - template: '' - }); - const wrapper = mount(ToolItem, { props: { - icon: markRaw(MockIcon), + icon: 'ri-screenshot-line', title: 'Test title', description: 'Test description' } }); - expect(wrapper.find('[data-test="mock-icon"]').exists()).toBe(true); + expect(wrapper.find('i.ri-screenshot-line').exists()).toBe(true); expect(wrapper.text()).toContain('Test title'); expect(wrapper.text()).toContain('Test description'); }); @@ -28,7 +23,7 @@ describe('ToolItem.vue', () => { const wrapper = mount(ToolItem, { props: { - icon: markRaw(defineComponent({ template: '' })), + icon: 'ri-screenshot-line', title: 'Clickable title', description: 'Clickable description' }, diff --git a/vitest.setup.js b/vitest.setup.js index be80d49b..388ebfb4 100644 --- a/vitest.setup.js +++ b/vitest.setup.js @@ -49,6 +49,19 @@ Object.defineProperty(window, 'matchMedia', { })) }); +// localStorage polyfill (jsdom may not provide a full implementation) +if (typeof globalThis.localStorage === 'undefined' || typeof globalThis.localStorage.clear !== 'function') { + const store = new Map(); + globalThis.localStorage = { + getItem: (key) => store.get(key) ?? null, + setItem: (key, value) => store.set(key, String(value)), + removeItem: (key) => store.delete(key), + clear: () => store.clear(), + get length() { return store.size; }, + key: (index) => [...store.keys()][index] ?? null + }; +} + // Notification API stub globalThis.Notification = class { static permission = 'denied';