mirror of
https://github.com/unraid/api.git
synced 2025-12-31 05:29:48 -06:00
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:
@@ -13,7 +13,9 @@ import { createTestI18n } from '../utils/i18n';
|
|||||||
|
|
||||||
vi.mock('@unraid/ui', () => ({
|
vi.mock('@unraid/ui', () => ({
|
||||||
PageContainer: { template: '<div><slot /></div>' },
|
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 = {
|
const mockAccountStore = {
|
||||||
@@ -97,7 +99,7 @@ describe('UpdateOs.standalone.vue', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('Initial Rendering and onBeforeMount Logic', () => {
|
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';
|
window.location.pathname = '/Tools/Update';
|
||||||
mockRebootType.value = '';
|
mockRebootType.value = '';
|
||||||
|
|
||||||
@@ -105,7 +107,7 @@ describe('UpdateOs.standalone.vue', () => {
|
|||||||
global: {
|
global: {
|
||||||
plugins: [createTestingPinia({ createSpy: vi.fn }), createTestI18n()],
|
plugins: [createTestingPinia({ createSpy: vi.fn }), createTestI18n()],
|
||||||
stubs: {
|
stubs: {
|
||||||
// Rely on @unraid/ui mock for PageContainer & BrandLoading
|
// Rely on @unraid/ui mock for PageContainer & BrandButton
|
||||||
UpdateOsStatus: UpdateOsStatusStub,
|
UpdateOsStatus: UpdateOsStatusStub,
|
||||||
UpdateOsThirdPartyDrivers: UpdateOsThirdPartyDriversStub,
|
UpdateOsThirdPartyDrivers: UpdateOsThirdPartyDriversStub,
|
||||||
},
|
},
|
||||||
@@ -114,17 +116,9 @@ describe('UpdateOs.standalone.vue', () => {
|
|||||||
|
|
||||||
await nextTick();
|
await nextTick();
|
||||||
|
|
||||||
// When path matches and rebootType is empty, updateOs should be called
|
expect(mockAccountStore.updateOs).not.toHaveBeenCalled();
|
||||||
expect(mockAccountStore.updateOs).toHaveBeenCalledWith(true);
|
expect(wrapper.find('[data-testid="update-os-account-button"]').exists()).toBe(true);
|
||||||
// Since v-show is used, both elements exist in DOM
|
expect(wrapper.find('[data-testid="update-os-status"]').exists()).toBe(false);
|
||||||
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');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows status and does not call updateOs when path does not match', async () => {
|
it('shows status and does not call updateOs when path does not match', async () => {
|
||||||
@@ -145,8 +139,7 @@ describe('UpdateOs.standalone.vue', () => {
|
|||||||
await nextTick();
|
await nextTick();
|
||||||
|
|
||||||
expect(mockAccountStore.updateOs).not.toHaveBeenCalled();
|
expect(mockAccountStore.updateOs).not.toHaveBeenCalled();
|
||||||
// Since v-show is used, both elements exist in DOM
|
expect(wrapper.find('[data-testid="update-os-account-button"]').exists()).toBe(false);
|
||||||
expect(wrapper.find('[data-testid="brand-loading-mock"]').exists()).toBe(true);
|
|
||||||
expect(wrapper.find('[data-testid="update-os-status"]').exists()).toBe(true);
|
expect(wrapper.find('[data-testid="update-os-status"]').exists()).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -168,10 +161,30 @@ describe('UpdateOs.standalone.vue', () => {
|
|||||||
await nextTick();
|
await nextTick();
|
||||||
|
|
||||||
expect(mockAccountStore.updateOs).not.toHaveBeenCalled();
|
expect(mockAccountStore.updateOs).not.toHaveBeenCalled();
|
||||||
// Since v-show is used, both elements exist in DOM
|
expect(wrapper.find('[data-testid="update-os-account-button"]').exists()).toBe(false);
|
||||||
expect(wrapper.find('[data-testid="brand-loading-mock"]').exists()).toBe(true);
|
|
||||||
expect(wrapper.find('[data-testid="update-os-status"]').exists()).toBe(true);
|
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', () => {
|
describe('Rendering based on rebootType', () => {
|
||||||
|
|||||||
@@ -4,17 +4,41 @@
|
|||||||
|
|
||||||
import { createPinia, setActivePinia } from 'pinia';
|
import { createPinia, setActivePinia } from 'pinia';
|
||||||
|
|
||||||
|
import { WEBGUI_REDIRECT } from '~/helpers/urls';
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
import { useUpdateOsStore } from '~/store/updateOs';
|
import { useUpdateOsStore } from '~/store/updateOs';
|
||||||
|
|
||||||
|
const mockSend = vi.fn();
|
||||||
|
|
||||||
vi.mock('@unraid/shared-callbacks', () => ({
|
vi.mock('@unraid/shared-callbacks', () => ({
|
||||||
useCallback: vi.fn(() => ({
|
useCallback: vi.fn(() => ({
|
||||||
send: vi.fn(),
|
send: mockSend,
|
||||||
watcher: vi.fn(),
|
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', () => {
|
vi.mock('~/composables/services/webgui', () => {
|
||||||
return {
|
return {
|
||||||
WebguiCheckForUpdate: vi.fn().mockResolvedValue({
|
WebguiCheckForUpdate: vi.fn().mockResolvedValue({
|
||||||
@@ -104,6 +128,40 @@ describe('UpdateOs Store', () => {
|
|||||||
expect(store.updateOsModalVisible).toBe(false);
|
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 () => {
|
it('should handle errors when checking for updates', async () => {
|
||||||
const { WebguiCheckForUpdate } = await import('~/composables/services/webgui');
|
const { WebguiCheckForUpdate } = await import('~/composables/services/webgui');
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ import { computed, onBeforeMount } from 'vue';
|
|||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { storeToRefs } from 'pinia';
|
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 { WEBGUI_TOOLS_UPDATE } from '~/helpers/urls';
|
||||||
|
|
||||||
import UpdateOsStatus from '~/components/UpdateOs/Status.vue';
|
import UpdateOsStatus from '~/components/UpdateOs/Status.vue';
|
||||||
@@ -47,25 +48,42 @@ const subtitle = computed(() => {
|
|||||||
return '';
|
return '';
|
||||||
});
|
});
|
||||||
|
|
||||||
/** when we're not prompting for reboot /Tools/Update will automatically send the user to account.unraid.net/server/update-os */
|
// Show a prompt to continue in the Account app when no reboot is pending.
|
||||||
const showLoader = computed(
|
const showRedirectPrompt = computed(
|
||||||
() => window.location.pathname === WEBGUI_TOOLS_UPDATE && rebootType.value === ''
|
() =>
|
||||||
|
typeof window !== 'undefined' &&
|
||||||
|
window.location.pathname === WEBGUI_TOOLS_UPDATE &&
|
||||||
|
rebootType.value === ''
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const openAccountUpdate = () => {
|
||||||
|
accountStore.updateOs(true);
|
||||||
|
};
|
||||||
|
|
||||||
onBeforeMount(() => {
|
onBeforeMount(() => {
|
||||||
if (showLoader.value) {
|
|
||||||
accountStore.updateOs(true);
|
|
||||||
}
|
|
||||||
serverStore.setRebootVersion(props.rebootVersion);
|
serverStore.setRebootVersion(props.rebootVersion);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<PageContainer>
|
<PageContainer>
|
||||||
<div v-show="showLoader">
|
<div
|
||||||
<BrandLoading class="mx-auto my-12 max-w-[160px]" />
|
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>
|
||||||
<div v-show="!showLoader">
|
<div v-else>
|
||||||
<UpdateOsStatus
|
<UpdateOsStatus
|
||||||
:show-update-check="true"
|
:show-update-check="true"
|
||||||
:title="t('updateOs.updateUnraidOs')"
|
:title="t('updateOs.updateUnraidOs')"
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ const UNRAID_NET_SUPPORT = new URL('/support', UNRAID_NET);
|
|||||||
const WEBGUI_GRAPHQL = '/graphql';
|
const WEBGUI_GRAPHQL = '/graphql';
|
||||||
const WEBGUI_SETTINGS_MANAGMENT_ACCESS = '/Settings/ManagementAccess';
|
const WEBGUI_SETTINGS_MANAGMENT_ACCESS = '/Settings/ManagementAccess';
|
||||||
const WEBGUI_CONNECT_SETTINGS = `${WEBGUI_SETTINGS_MANAGMENT_ACCESS}#UnraidNetSettings`;
|
const WEBGUI_CONNECT_SETTINGS = `${WEBGUI_SETTINGS_MANAGMENT_ACCESS}#UnraidNetSettings`;
|
||||||
|
const WEBGUI_REDIRECT = '/redirect.htm';
|
||||||
const WEBGUI_TOOLS_DOWNGRADE = '/Tools/Downgrade';
|
const WEBGUI_TOOLS_DOWNGRADE = '/Tools/Downgrade';
|
||||||
const WEBGUI_TOOLS_REGISTRATION = '/Tools/Registration';
|
const WEBGUI_TOOLS_REGISTRATION = '/Tools/Registration';
|
||||||
const WEBGUI_TOOLS_UPDATE = '/Tools/Update';
|
const WEBGUI_TOOLS_UPDATE = '/Tools/Update';
|
||||||
@@ -66,6 +67,7 @@ export {
|
|||||||
DOCS_REGISTRATION_LICENSING,
|
DOCS_REGISTRATION_LICENSING,
|
||||||
DOCS_REGISTRATION_REPLACE_KEY,
|
DOCS_REGISTRATION_REPLACE_KEY,
|
||||||
WEBGUI_CONNECT_SETTINGS,
|
WEBGUI_CONNECT_SETTINGS,
|
||||||
|
WEBGUI_REDIRECT,
|
||||||
WEBGUI_GRAPHQL,
|
WEBGUI_GRAPHQL,
|
||||||
WEBGUI_SETTINGS_MANAGMENT_ACCESS,
|
WEBGUI_SETTINGS_MANAGMENT_ACCESS,
|
||||||
WEBGUI_TOOLS_DOWNGRADE,
|
WEBGUI_TOOLS_DOWNGRADE,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
|
|
||||||
|
import { WEBGUI_REDIRECT } from '~/helpers/urls';
|
||||||
import dayjs, { extend } from 'dayjs';
|
import dayjs, { extend } from 'dayjs';
|
||||||
import customParseFormat from 'dayjs/plugin/customParseFormat';
|
import customParseFormat from 'dayjs/plugin/customParseFormat';
|
||||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||||
@@ -71,8 +72,9 @@ export const useUpdateOsStore = defineStore('updateOs', () => {
|
|||||||
// fetchAndConfirmInstall logic
|
// fetchAndConfirmInstall logic
|
||||||
const callbackStore = useCallbackActionsStore();
|
const callbackStore = useCallbackActionsStore();
|
||||||
const fetchAndConfirmInstall = (sha256: string) => {
|
const fetchAndConfirmInstall = (sha256: string) => {
|
||||||
|
const redirectUrl = new URL(WEBGUI_REDIRECT, window.location.origin).toString();
|
||||||
callbackStore.send(
|
callbackStore.send(
|
||||||
window.location.href,
|
redirectUrl,
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
sha256,
|
sha256,
|
||||||
|
|||||||
Reference in New Issue
Block a user