diff --git a/src/components/StatusBar.vue b/src/components/StatusBar.vue index 88c9581a..8e515351 100644 --- a/src/components/StatusBar.vue +++ b/src/components/StatusBar.vue @@ -79,7 +79,8 @@
+ class="flex items-center gap-1 px-2 h-[22px] whitespace-nowrap border-r border-border cursor-pointer hover:bg-accent" + @click="vrcStatusStore.openStatusPage()"> {{ t('status_bar.servers') }}
@@ -349,9 +350,6 @@ function toggleVisibility(key) { visibility[key] = !visibility[key]; configRepository.setString(VISIBILITY_KEY, JSON.stringify(visibility)); - if (key === 'servers') { - vrcStatusStore.setStatusBarServersVisible(visibility.servers); - } } // --- WebSocket message rate + sparkline --- @@ -543,12 +541,10 @@ } drawSparkline(); - vrcStatusStore.setStatusBarServersVisible(visibility.servers); }); onBeforeUnmount(() => { clearTimeout(serversHoverTimer); - vrcStatusStore.setStatusBarServersVisible(false); }); watch( diff --git a/src/stores/__tests__/vrcStatus.test.js b/src/stores/__tests__/vrcStatus.test.js index 9a96e60e..5dd15dad 100644 --- a/src/stores/__tests__/vrcStatus.test.js +++ b/src/stores/__tests__/vrcStatus.test.js @@ -3,11 +3,7 @@ import { createPinia, setActivePinia } from 'pinia'; const mocks = vi.hoisted(() => ({ execute: vi.fn(), - formatDateFilter: vi.fn(() => 'formatted-time'), - openExternalLink: vi.fn(), - toastWarning: vi.fn(() => 'toast-id-1'), - toastSuccess: vi.fn(() => 'toast-id-2'), - toastDismiss: vi.fn() + openExternalLink: vi.fn() })); vi.mock('../../services/webapi', () => ({ @@ -23,27 +19,10 @@ vi.mock('worker-timers', () => ({ clearTimeout: vi.fn() })); -vi.mock('vue-sonner', () => ({ - toast: { - warning: (...args) => mocks.toastWarning(...args), - success: (...args) => mocks.toastSuccess(...args), - dismiss: (...args) => mocks.toastDismiss(...args) - } -})); - vi.mock('../../shared/utils', () => ({ - formatDateFilter: (...args) => mocks.formatDateFilter(...args), openExternalLink: (...args) => mocks.openExternalLink(...args) })); -vi.mock('vue-i18n', () => ({ - useI18n: () => ({ - t: (key) => key - , - locale: require('vue').ref('en') - }) -})); - /** * */ @@ -137,130 +116,3 @@ describe('useVrcStatusStore.getVrcStatus', () => { expect(store.statusText).toBe(''); }); }); - -describe('useVrcStatusStore dual-mode notification', () => { - beforeEach(async () => { - mocks.execute.mockResolvedValue({ - status: 200, - data: JSON.stringify({ - page: { updated_at: '2026-01-01T00:00:00.000Z' }, - status: { description: 'All Systems Operational' } - }) - }); - - setActivePinia(createPinia()); - useVrcStatusStore(); - await flushPromises(); - vi.clearAllMocks(); - }); - - test('does not show toast before initialized (startup race prevention)', async () => { - const store = useVrcStatusStore(); - // Do NOT call setStatusBarServersVisible — initialized remains false - - mocks.execute - .mockResolvedValueOnce({ - status: 200, - data: JSON.stringify({ - page: { updated_at: '2026-01-02T00:00:00.000Z' }, - status: { description: 'Partial System Outage' } - }) - }) - .mockResolvedValueOnce({ - status: 200, - data: JSON.stringify({ - components: [{ name: 'API', status: 'major_outage' }] - }) - }); - - await store.getVrcStatus(); - await flushPromises(); - - expect(mocks.toastWarning).not.toHaveBeenCalled(); - }); - - test('shows toast when statusBarServersVisible is false and initialized', async () => { - const store = useVrcStatusStore(); - // Initialize via action (simulates StatusBar onMounted with servers=false) - store.setStatusBarServersVisible(false); - - mocks.execute - .mockResolvedValueOnce({ - status: 200, - data: JSON.stringify({ - page: { updated_at: '2026-01-02T00:00:00.000Z' }, - status: { description: 'Partial System Outage' } - }) - }) - .mockResolvedValueOnce({ - status: 200, - data: JSON.stringify({ - components: [{ name: 'API', status: 'major_outage' }] - }) - }); - - await store.getVrcStatus(); - await flushPromises(); - - expect(mocks.toastWarning).toHaveBeenCalled(); - expect(mocks.toastWarning.mock.calls[0][0]).toBe( - 'status_bar.servers_issue' - ); - }); - - test('does NOT show toast when statusBarServersVisible is true', async () => { - const store = useVrcStatusStore(); - store.setStatusBarServersVisible(true); - - mocks.execute - .mockResolvedValueOnce({ - status: 200, - data: JSON.stringify({ - page: { updated_at: '2026-01-02T00:00:00.000Z' }, - status: { description: 'Partial System Outage' } - }) - }) - .mockResolvedValueOnce({ - status: 200, - data: JSON.stringify({ - components: [{ name: 'API', status: 'major_outage' }] - }) - }); - - await store.getVrcStatus(); - await flushPromises(); - - expect(mocks.toastWarning).not.toHaveBeenCalled(); - }); - - test('triggers toast when switching from StatusBar mode to toast mode with active issue', async () => { - const store = useVrcStatusStore(); - store.setStatusBarServersVisible(true); - - // Create an issue while in StatusBar mode - mocks.execute - .mockResolvedValueOnce({ - status: 200, - data: JSON.stringify({ - page: { updated_at: '2026-01-02T00:00:00.000Z' }, - status: { description: 'Major Outage' } - }) - }) - .mockResolvedValueOnce({ - status: 200, - data: JSON.stringify({ - components: [{ name: 'API', status: 'major_outage' }] - }) - }); - - await store.getVrcStatus(); - await flushPromises(); - expect(mocks.toastWarning).not.toHaveBeenCalled(); - - // Switch to toast mode - should trigger notification - store.setStatusBarServersVisible(false); - await flushPromises(); - - expect(mocks.toastWarning).toHaveBeenCalledTimes(1); - }); -}); diff --git a/src/stores/settings/appearance.js b/src/stores/settings/appearance.js index 2724a1ae..a9c1ee6d 100644 --- a/src/stores/settings/appearance.js +++ b/src/stores/settings/appearance.js @@ -109,7 +109,6 @@ export const useAppearanceSettingsStore = defineStore( const isDataTableStriped = ref(false); const showPointerOnHover = ref(false); - const showStatusBar = ref(true); const tableLimitsDialog = ref({ visible: false, maxTableSize: 500, @@ -168,7 +167,6 @@ export const useAppearanceSettingsStore = defineStore( navIsCollapsedConfig, dataTableStripedConfig, showPointerOnHoverConfig, - showStatusBarConfig, appFontFamilyConfig, lastDarkThemeConfig ] = await Promise.all([ @@ -232,7 +230,6 @@ export const useAppearanceSettingsStore = defineStore( configRepository.getBool('VRCX_navIsCollapsed', false), configRepository.getBool('VRCX_dataTableStriped', false), configRepository.getBool('VRCX_showPointerOnHover', false), - configRepository.getBool('VRCX_showStatusBar', true), configRepository.getString( 'VRCX_fontFamily', APP_FONT_DEFAULT_KEY @@ -333,7 +330,6 @@ export const useAppearanceSettingsStore = defineStore( isNavCollapsed.value = navIsCollapsedConfig; isDataTableStriped.value = dataTableStripedConfig; showPointerOnHover.value = showPointerOnHoverConfig; - showStatusBar.value = showStatusBarConfig; applyPointerHoverClass(); @@ -567,13 +563,6 @@ export const useAppearanceSettingsStore = defineStore( showInstanceIdInLocation.value ); } - /** - * - */ - function setShowStatusBar() { - showStatusBar.value = !showStatusBar.value; - configRepository.setBool('VRCX_showStatusBar', showStatusBar.value); - } /** * */ @@ -1105,7 +1094,6 @@ export const useAppearanceSettingsStore = defineStore( isNavCollapsed, isDataTableStriped, showPointerOnHover, - showStatusBar, tableLimitsDialog, TABLE_MAX_SIZE_MIN, TABLE_MAX_SIZE_MAX, @@ -1116,7 +1104,6 @@ export const useAppearanceSettingsStore = defineStore( setDisplayVRCPlusIconsAsAvatar, setHideNicknames, setShowInstanceIdInLocation, - setShowStatusBar, setIsAgeGatedInstancesVisible, setSortFavorites, setInstanceUsersSortAlphabetical, diff --git a/src/stores/vrcStatus.js b/src/stores/vrcStatus.js index 76591119..e3b46830 100644 --- a/src/stores/vrcStatus.js +++ b/src/stores/vrcStatus.js @@ -1,9 +1,7 @@ -import { computed, ref, watch } from 'vue'; +import { computed, ref } from 'vue'; import { defineStore } from 'pinia'; -import { toast } from 'vue-sonner'; -import { useI18n } from 'vue-i18n'; -import { formatDateFilter, openExternalLink } from '../shared/utils'; +import { openExternalLink } from '../shared/utils'; import webApiService from '../services/webapi'; @@ -18,14 +16,6 @@ export const useVrcStatusStore = defineStore('VrcStatus', () => { const lastTimeFetched = ref(0); const pollingInterval = ref(0); - const statusBarServersVisible = ref(false); - const initialized = ref(false); - - const alertRef = ref(null); - const lastStatusText = ref(''); - - const { t } = useI18n(); - const statusText = computed(() => { if (lastStatus.value && lastStatusSummary.value) { return `${lastStatus.value}: ${lastStatusSummary.value}`; @@ -35,17 +25,6 @@ export const useVrcStatusStore = defineStore('VrcStatus', () => { const hasIssue = computed(() => !!lastStatus.value); - /** - * @returns {void} - */ - function dismissAlert() { - if (!alertRef.value) { - return; - } - toast.dismiss(alertRef.value); - alertRef.value = null; - } - /** * @returns {void} */ @@ -53,75 +32,6 @@ export const useVrcStatusStore = defineStore('VrcStatus', () => { openExternalLink('https://status.vrchat.com'); } - /** - * @param {boolean} visible - * @returns {void} - */ - function setStatusBarServersVisible(visible) { - statusBarServersVisible.value = visible; - if (!initialized.value) { - initialized.value = true; - } - } - - /** - * @param {string} text - * @returns {void} - */ - function showWarningToast(text) { - dismissAlert(); - alertRef.value = toast.warning(t('status_bar.servers_issue'), { - description: `${formatDateFilter(lastStatusTime.value, 'short')}: ${text}`, - duration: Infinity, - closeButton: true, - position: 'bottom-right', - action: { - label: 'Open', - onClick: () => openStatusPage() - } - }); - } - - watch(statusText, (newVal) => { - if (statusBarServersVisible.value || !initialized.value) { - return; - } - - if (lastStatusText.value === newVal) { - return; - } - lastStatusText.value = newVal; - - if (!newVal) { - if (alertRef.value) { - dismissAlert(); - alertRef.value = toast.success(t('status_bar.servers_issue'), { - description: `${formatDateFilter(lastStatusTime.value, 'short')}: ${t('status_bar.servers_ok')}`, - position: 'bottom-right', - action: { - label: 'Open', - onClick: () => openStatusPage() - } - }); - } - return; - } - - showWarningToast(newVal); - }); - - watch(statusBarServersVisible, (visible) => { - if (!visible && hasIssue.value && statusText.value) { - lastStatusText.value = ''; - showWarningToast(statusText.value); - lastStatusText.value = statusText.value; - } - if (visible) { - dismissAlert(); - lastStatusText.value = ''; - } - }); - /** * @returns {Promise} */ @@ -210,8 +120,6 @@ export const useVrcStatusStore = defineStore('VrcStatus', () => { lastStatusSummary, statusText, hasIssue, - statusBarServersVisible, - setStatusBarServersVisible, openStatusPage, onBrowserFocus, getVrcStatus diff --git a/src/views/Layout/MainLayout.vue b/src/views/Layout/MainLayout.vue index 3f374e77..2b6931fa 100644 --- a/src/views/Layout/MainLayout.vue +++ b/src/views/Layout/MainLayout.vue @@ -53,7 +53,7 @@ - + @@ -119,7 +119,7 @@ const router = useRouter(); const appearanceSettingsStore = useAppearanceSettingsStore(); - const { navWidth, isNavCollapsed, showStatusBar } = storeToRefs(appearanceSettingsStore); + const { navWidth, isNavCollapsed } = storeToRefs(appearanceSettingsStore); const sidebarOpen = computed(() => !isNavCollapsed.value); diff --git a/src/views/Layout/__tests__/MainLayout.test.js b/src/views/Layout/__tests__/MainLayout.test.js index 9539a360..84c687d5 100644 --- a/src/views/Layout/__tests__/MainLayout.test.js +++ b/src/views/Layout/__tests__/MainLayout.test.js @@ -2,38 +2,113 @@ import { describe, expect, it, vi } from 'vitest'; import { mount } from '@vue/test-utils'; import { ref } from 'vue'; -const mocks = vi.hoisted(() => ({ replace: vi.fn(), setNavCollapsed: vi.fn(), setNavWidth: vi.fn() })); +const mocks = vi.hoisted(() => ({ + replace: vi.fn(), + setNavCollapsed: vi.fn(), + setNavWidth: vi.fn() +})); vi.mock('pinia', async (i) => ({ ...(await i()), storeToRefs: (s) => s })); -vi.mock('vue-router', () => ({ useRouter: () => ({ replace: (...a) => mocks.replace(...a) }) })); -vi.mock('../../../services/watchState', () => ({ watchState: { isLoggedIn: false } })); -vi.mock('../../../stores', () => ({ useAppearanceSettingsStore: () => ({ navWidth: ref(240), isNavCollapsed: ref(false), showStatusBar: ref(false), setNavCollapsed: (...a) => mocks.setNavCollapsed(...a), setNavWidth: (...a) => mocks.setNavWidth(...a) }) })); -vi.mock('../../../composables/useMainLayoutResizable', () => ({ useMainLayoutResizable: () => ({ asideDefaultSize: 30, asideMinSize: 0, asideMaxPx: 480, mainDefaultSize: 70, handleLayout: vi.fn(), isAsideCollapsed: () => false, isAsideCollapsedStatic: false, isSideBarTabShow: ref(true) }) })); -vi.mock('../../../components/ui/resizable', () => ({ ResizablePanelGroup: { template: '
' }, ResizablePanel: { template: '
' }, ResizableHandle: { template: '
' } })); -vi.mock('../../../components/ui/sidebar', () => ({ SidebarProvider: { template: '
' }, SidebarInset: { template: '
' } })); -vi.mock('../../../components/nav-menu/NavMenu.vue', () => ({ default: { template: '
' } })); -vi.mock('../../Sidebar/Sidebar.vue', () => ({ default: { template: '
' } })); -vi.mock('../../../components/StatusBar.vue', () => ({ default: { template: '
' } })); -vi.mock('../../../components/dialogs/MainDialogContainer.vue', () => ({ default: { template: '
' } })); -vi.mock('../../../components/FullscreenImagePreview.vue', () => ({ default: { template: '
' } })); -vi.mock('../../../components/dialogs/ChooseFavoriteGroupDialog.vue', () => ({ default: { template: '
' } })); -vi.mock('../../../components/dialogs/LaunchDialog.vue', () => ({ default: { template: '
' } })); -vi.mock('../../Settings/dialogs/LaunchOptionsDialog.vue', () => ({ default: { template: '
' } })); -vi.mock('../../Favorites/dialogs/FriendImportDialog.vue', () => ({ default: { template: '
' } })); -vi.mock('../../Favorites/dialogs/WorldImportDialog.vue', () => ({ default: { template: '
' } })); -vi.mock('../../Favorites/dialogs/AvatarImportDialog.vue', () => ({ default: { template: '
' } })); -vi.mock('../../../components/dialogs/GroupDialog/GroupMemberModerationDialog.vue', () => ({ default: { template: '
' } })); -vi.mock('../../../components/dialogs/InviteGroupDialog.vue', () => ({ default: { template: '
' } })); -vi.mock('../../Settings/dialogs/VRChatConfigDialog.vue', () => ({ default: { template: '
' } })); -vi.mock('../../Settings/dialogs/PrimaryPasswordDialog.vue', () => ({ default: { template: '
' } })); -vi.mock('../../../components/dialogs/SendBoopDialog.vue', () => ({ default: { template: '
' } })); -vi.mock('../../Settings/dialogs/ChangelogDialog.vue', () => ({ default: { template: '
' } })); +vi.mock('vue-router', () => ({ + useRouter: () => ({ replace: (...a) => mocks.replace(...a) }) +})); +vi.mock('../../../services/watchState', () => ({ + watchState: { isLoggedIn: false } +})); +vi.mock('../../../stores', () => ({ + useAppearanceSettingsStore: () => ({ + navWidth: ref(240), + isNavCollapsed: ref(false), + setNavCollapsed: (...a) => mocks.setNavCollapsed(...a), + setNavWidth: (...a) => mocks.setNavWidth(...a) + }) +})); +vi.mock('../../../composables/useMainLayoutResizable', () => ({ + useMainLayoutResizable: () => ({ + asideDefaultSize: 30, + asideMinSize: 0, + asideMaxPx: 480, + mainDefaultSize: 70, + handleLayout: vi.fn(), + isAsideCollapsed: () => false, + isAsideCollapsedStatic: false, + isSideBarTabShow: ref(true) + }) +})); +vi.mock('../../../components/ui/resizable', () => ({ + ResizablePanelGroup: { template: '
' }, + ResizablePanel: { template: '
' }, + ResizableHandle: { template: '
' } +})); +vi.mock('../../../components/ui/sidebar', () => ({ + SidebarProvider: { template: '
' }, + SidebarInset: { template: '
' } +})); +vi.mock('../../../components/nav-menu/NavMenu.vue', () => ({ + default: { template: '
' } +})); +vi.mock('../../Sidebar/Sidebar.vue', () => ({ + default: { template: '
' } +})); +vi.mock('../../../components/StatusBar.vue', () => ({ + default: { template: '
' } +})); +vi.mock('../../../components/dialogs/MainDialogContainer.vue', () => ({ + default: { template: '
' } +})); +vi.mock('../../../components/FullscreenImagePreview.vue', () => ({ + default: { template: '
' } +})); +vi.mock('../../../components/dialogs/ChooseFavoriteGroupDialog.vue', () => ({ + default: { template: '
' } +})); +vi.mock('../../../components/dialogs/LaunchDialog.vue', () => ({ + default: { template: '
' } +})); +vi.mock('../../Settings/dialogs/LaunchOptionsDialog.vue', () => ({ + default: { template: '
' } +})); +vi.mock('../../Favorites/dialogs/FriendImportDialog.vue', () => ({ + default: { template: '
' } +})); +vi.mock('../../Favorites/dialogs/WorldImportDialog.vue', () => ({ + default: { template: '
' } +})); +vi.mock('../../Favorites/dialogs/AvatarImportDialog.vue', () => ({ + default: { template: '
' } +})); +vi.mock( + '../../../components/dialogs/GroupDialog/GroupMemberModerationDialog.vue', + () => ({ default: { template: '
' } }) +); +vi.mock('../../../components/dialogs/InviteGroupDialog.vue', () => ({ + default: { template: '
' } +})); +vi.mock('../../Settings/dialogs/VRChatConfigDialog.vue', () => ({ + default: { template: '
' } +})); +vi.mock('../../Settings/dialogs/PrimaryPasswordDialog.vue', () => ({ + default: { template: '
' } +})); +vi.mock('../../../components/dialogs/SendBoopDialog.vue', () => ({ + default: { template: '
' } +})); +vi.mock('../../Settings/dialogs/ChangelogDialog.vue', () => ({ + default: { template: '
' } +})); import MainLayout from '../MainLayout.vue'; describe('MainLayout.vue', () => { it('redirects to login when not logged in', () => { - mount(MainLayout, { global: { stubs: { RouterView: { template: '
' }, KeepAlive: { template: '
' } } } }); + mount(MainLayout, { + global: { + stubs: { + RouterView: { template: '
' }, + KeepAlive: { template: '
' } + } + } + }); expect(mocks.replace).toHaveBeenCalledWith({ name: 'login' }); }); }); diff --git a/src/views/Settings/components/Tabs/AppearanceTab.vue b/src/views/Settings/components/Tabs/AppearanceTab.vue index 801777a8..601ab567 100644 --- a/src/views/Settings/components/Tabs/AppearanceTab.vue +++ b/src/views/Settings/components/Tabs/AppearanceTab.vue @@ -82,10 +82,6 @@ :label="t('view.settings.appearance.appearance.show_instance_id')" :value="showInstanceIdInLocation" @change="setShowInstanceIdInLocation" /> - getLanguageName(String(appLanguage.value))); @@ -403,7 +398,6 @@ setDisplayVRCPlusIconsAsAvatar, setHideNicknames, setShowInstanceIdInLocation, - setShowStatusBar, setIsAgeGatedInstancesVisible, setInstanceUsersSortAlphabetical, setDtHour12,