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.
This commit is contained in:
Eli Bosley
2025-12-19 11:44:19 -05:00
committed by GitHub
parent bb9b539732
commit d099e7521d
5 changed files with 123 additions and 30 deletions

View File

@@ -13,7 +13,9 @@ import { createTestI18n } from '../utils/i18n';
vi.mock('@unraid/ui', () => ({
PageContainer: { template: '<div><slot /></div>' },
BrandLoading: { template: '<div data-testid="brand-loading-mock">Loading...</div>' },
BrandButton: {
template: '<button v-bind="$attrs" @click="$emit(\'click\')"><slot /></button>',
},
}));
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', () => {

View File

@@ -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');

View File

@@ -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);
});
</script>
<template>
<PageContainer>
<div v-show="showLoader">
<BrandLoading class="mx-auto my-12 max-w-[160px]" />
<div
v-if="showRedirectPrompt"
class="mx-auto flex max-w-[720px] flex-col items-center gap-4 py-8 text-center"
>
<h1 class="text-2xl font-semibold">{{ t('updateOs.updateUnraidOs') }}</h1>
<p class="text-base leading-relaxed opacity-75">
{{ t('updateOs.update.receiveTheLatestAndGreatestFor') }}
</p>
<BrandButton
data-testid="update-os-account-button"
:icon-right="ArrowTopRightOnSquareIcon"
@click="openAccountUpdate"
>
{{ t('updateOs.update.viewAvailableUpdates') }}
</BrandButton>
</div>
<div v-show="!showLoader">
<div v-else>
<UpdateOsStatus
:show-update-check="true"
:title="t('updateOs.updateUnraidOs')"

View File

@@ -22,6 +22,7 @@ const UNRAID_NET_SUPPORT = new URL('/support', UNRAID_NET);
const WEBGUI_GRAPHQL = '/graphql';
const WEBGUI_SETTINGS_MANAGMENT_ACCESS = '/Settings/ManagementAccess';
const WEBGUI_CONNECT_SETTINGS = `${WEBGUI_SETTINGS_MANAGMENT_ACCESS}#UnraidNetSettings`;
const WEBGUI_REDIRECT = '/redirect.htm';
const WEBGUI_TOOLS_DOWNGRADE = '/Tools/Downgrade';
const WEBGUI_TOOLS_REGISTRATION = '/Tools/Registration';
const WEBGUI_TOOLS_UPDATE = '/Tools/Update';
@@ -66,6 +67,7 @@ export {
DOCS_REGISTRATION_LICENSING,
DOCS_REGISTRATION_REPLACE_KEY,
WEBGUI_CONNECT_SETTINGS,
WEBGUI_REDIRECT,
WEBGUI_GRAPHQL,
WEBGUI_SETTINGS_MANAGMENT_ACCESS,
WEBGUI_TOOLS_DOWNGRADE,

View File

@@ -1,6 +1,7 @@
import { computed, ref } from 'vue';
import { defineStore } from 'pinia';
import { WEBGUI_REDIRECT } from '~/helpers/urls';
import dayjs, { extend } from 'dayjs';
import customParseFormat from 'dayjs/plugin/customParseFormat';
import relativeTime from 'dayjs/plugin/relativeTime';
@@ -71,8 +72,9 @@ export const useUpdateOsStore = defineStore('updateOs', () => {
// fetchAndConfirmInstall logic
const callbackStore = useCallbackActionsStore();
const fetchAndConfirmInstall = (sha256: string) => {
const redirectUrl = new URL(WEBGUI_REDIRECT, window.location.origin).toString();
callbackStore.send(
window.location.href,
redirectUrl,
[
{
sha256,