From d099e7521d2062bb9cf84f340e46b169dd2492c5 Mon Sep 17 00:00:00 2001 From: Eli Bosley Date: Fri, 19 Dec 2025 11:44:19 -0500 Subject: [PATCH] fix: resolve issue with "Continue" button when updating (#1852) - Replaced BrandLoading with BrandButton in UpdateOs component for better user interaction. - Updated test cases to reflect changes in rendering logic, ensuring the account button is displayed when no reboot is pending. - Added functionality to navigate to account update when the button is clicked. - Introduced WEBGUI_REDIRECT URL for handling update installations in the store logic. --- web/__test__/components/UpdateOs.test.ts | 49 +++++++++++------- web/__test__/store/updateOs.test.ts | 60 +++++++++++++++++++++- web/src/components/UpdateOs.standalone.vue | 38 ++++++++++---- web/src/helpers/urls.ts | 2 + web/src/store/updateOs.ts | 4 +- 5 files changed, 123 insertions(+), 30 deletions(-) diff --git a/web/__test__/components/UpdateOs.test.ts b/web/__test__/components/UpdateOs.test.ts index a5505a711..83e15aa4c 100644 --- a/web/__test__/components/UpdateOs.test.ts +++ b/web/__test__/components/UpdateOs.test.ts @@ -13,7 +13,9 @@ import { createTestI18n } from '../utils/i18n'; vi.mock('@unraid/ui', () => ({ PageContainer: { template: '
' }, - BrandLoading: { template: '
Loading...
' }, + BrandButton: { + template: '', + }, })); const mockAccountStore = { @@ -97,7 +99,7 @@ describe('UpdateOs.standalone.vue', () => { }); describe('Initial Rendering and onBeforeMount Logic', () => { - it('shows loader and calls updateOs when path matches and rebootType is empty', async () => { + it('shows account button and does not auto-redirect when path matches and rebootType is empty', async () => { window.location.pathname = '/Tools/Update'; mockRebootType.value = ''; @@ -105,7 +107,7 @@ describe('UpdateOs.standalone.vue', () => { global: { plugins: [createTestingPinia({ createSpy: vi.fn }), createTestI18n()], stubs: { - // Rely on @unraid/ui mock for PageContainer & BrandLoading + // Rely on @unraid/ui mock for PageContainer & BrandButton UpdateOsStatus: UpdateOsStatusStub, UpdateOsThirdPartyDrivers: UpdateOsThirdPartyDriversStub, }, @@ -114,17 +116,9 @@ describe('UpdateOs.standalone.vue', () => { await nextTick(); - // When path matches and rebootType is empty, updateOs should be called - expect(mockAccountStore.updateOs).toHaveBeenCalledWith(true); - // Since v-show is used, both elements exist in DOM - expect(wrapper.find('[data-testid="brand-loading-mock"]').exists()).toBe(true); - expect(wrapper.find('[data-testid="update-os-status"]').exists()).toBe(true); - // The loader should be visible when showLoader is true - const loaderWrapper = wrapper.find('[data-testid="brand-loading-mock"]').element.parentElement; - expect(loaderWrapper?.style.display).not.toBe('none'); - // The status should be hidden when showLoader is true - const statusWrapper = wrapper.find('[data-testid="update-os-status"]').element.parentElement; - expect(statusWrapper?.style.display).toBe('none'); + expect(mockAccountStore.updateOs).not.toHaveBeenCalled(); + expect(wrapper.find('[data-testid="update-os-account-button"]').exists()).toBe(true); + expect(wrapper.find('[data-testid="update-os-status"]').exists()).toBe(false); }); it('shows status and does not call updateOs when path does not match', async () => { @@ -145,8 +139,7 @@ describe('UpdateOs.standalone.vue', () => { await nextTick(); expect(mockAccountStore.updateOs).not.toHaveBeenCalled(); - // Since v-show is used, both elements exist in DOM - expect(wrapper.find('[data-testid="brand-loading-mock"]').exists()).toBe(true); + expect(wrapper.find('[data-testid="update-os-account-button"]').exists()).toBe(false); expect(wrapper.find('[data-testid="update-os-status"]').exists()).toBe(true); }); @@ -168,10 +161,30 @@ describe('UpdateOs.standalone.vue', () => { await nextTick(); expect(mockAccountStore.updateOs).not.toHaveBeenCalled(); - // Since v-show is used, both elements exist in DOM - expect(wrapper.find('[data-testid="brand-loading-mock"]').exists()).toBe(true); + expect(wrapper.find('[data-testid="update-os-account-button"]').exists()).toBe(false); expect(wrapper.find('[data-testid="update-os-status"]').exists()).toBe(true); }); + + it('navigates to account update when the button is clicked', async () => { + window.location.pathname = '/Tools/Update'; + mockRebootType.value = ''; + + const wrapper = mount(UpdateOs, { + global: { + plugins: [createTestingPinia({ createSpy: vi.fn }), createTestI18n()], + stubs: { + UpdateOsStatus: UpdateOsStatusStub, + UpdateOsThirdPartyDrivers: UpdateOsThirdPartyDriversStub, + }, + }, + }); + + await nextTick(); + + await wrapper.find('[data-testid="update-os-account-button"]').trigger('click'); + + expect(mockAccountStore.updateOs).toHaveBeenCalledWith(true); + }); }); describe('Rendering based on rebootType', () => { diff --git a/web/__test__/store/updateOs.test.ts b/web/__test__/store/updateOs.test.ts index 56318fcd0..940787e26 100644 --- a/web/__test__/store/updateOs.test.ts +++ b/web/__test__/store/updateOs.test.ts @@ -4,17 +4,41 @@ import { createPinia, setActivePinia } from 'pinia'; +import { WEBGUI_REDIRECT } from '~/helpers/urls'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { useUpdateOsStore } from '~/store/updateOs'; +const mockSend = vi.fn(); + vi.mock('@unraid/shared-callbacks', () => ({ useCallback: vi.fn(() => ({ - send: vi.fn(), + send: mockSend, watcher: vi.fn(), })), })); +vi.mock('~/composables/preventClose', () => ({ + addPreventClose: vi.fn(), + removePreventClose: vi.fn(), +})); + +vi.mock('~/store/account', () => ({ + useAccountStore: () => ({ + accountActionStatus: 'ready', + }), +})); + +vi.mock('~/store/installKey', () => ({ + useInstallKeyStore: () => ({ + keyInstallStatus: 'ready', + }), +})); + +vi.mock('~/store/updateOsActions', () => ({ + useUpdateOsActionsStore: () => ({}), +})); + vi.mock('~/composables/services/webgui', () => { return { WebguiCheckForUpdate: vi.fn().mockResolvedValue({ @@ -104,6 +128,40 @@ describe('UpdateOs Store', () => { expect(store.updateOsModalVisible).toBe(false); }); + it('should send update install through redirect.htm', () => { + const originalLocation = window.location; + + Object.defineProperty(window, 'location', { + configurable: true, + value: { + ...originalLocation, + origin: 'https://littlebox.tail45affd.ts.net', + href: 'https://littlebox.tail45affd.ts.net/Plugins', + }, + }); + + store.fetchAndConfirmInstall('test-sha256'); + + const expectedUrl = new URL(WEBGUI_REDIRECT, window.location.origin).toString(); + + expect(mockSend).toHaveBeenCalledWith( + expectedUrl, + [ + { + sha256: 'test-sha256', + type: 'updateOs', + }, + ], + undefined, + 'forUpc' + ); + + Object.defineProperty(window, 'location', { + configurable: true, + value: originalLocation, + }); + }); + it('should handle errors when checking for updates', async () => { const { WebguiCheckForUpdate } = await import('~/composables/services/webgui'); diff --git a/web/src/components/UpdateOs.standalone.vue b/web/src/components/UpdateOs.standalone.vue index 194dd01e2..7fd17ab35 100644 --- a/web/src/components/UpdateOs.standalone.vue +++ b/web/src/components/UpdateOs.standalone.vue @@ -19,7 +19,8 @@ import { computed, onBeforeMount } from 'vue'; import { useI18n } from 'vue-i18n'; import { storeToRefs } from 'pinia'; -import { BrandLoading, PageContainer } from '@unraid/ui'; +import { ArrowTopRightOnSquareIcon } from '@heroicons/vue/24/solid'; +import { BrandButton, PageContainer } from '@unraid/ui'; import { WEBGUI_TOOLS_UPDATE } from '~/helpers/urls'; import UpdateOsStatus from '~/components/UpdateOs/Status.vue'; @@ -47,25 +48,42 @@ const subtitle = computed(() => { return ''; }); -/** when we're not prompting for reboot /Tools/Update will automatically send the user to account.unraid.net/server/update-os */ -const showLoader = computed( - () => window.location.pathname === WEBGUI_TOOLS_UPDATE && rebootType.value === '' +// Show a prompt to continue in the Account app when no reboot is pending. +const showRedirectPrompt = computed( + () => + typeof window !== 'undefined' && + window.location.pathname === WEBGUI_TOOLS_UPDATE && + rebootType.value === '' ); +const openAccountUpdate = () => { + accountStore.updateOs(true); +}; + onBeforeMount(() => { - if (showLoader.value) { - accountStore.updateOs(true); - } serverStore.setRebootVersion(props.rebootVersion); });