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', () => ({
|
||||
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', () => {
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -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')"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user