import { nextTick, ref } from 'vue'; import { mount } from '@vue/test-utils'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import CheckUpdateResponseModal from '~/components/UpdateOs/CheckUpdateResponseModal.vue'; import { createTestI18n, testTranslate } from '../utils/i18n'; vi.mock('@unraid/ui', () => ({ BrandButton: { name: 'BrandButton', props: { text: { type: String, default: undefined, }, }, emits: ['click'], template: '', }, BrandLoading: { template: '
' }, Button: { template: '' }, cn: (...classes: string[]) => classes.filter(Boolean).join(' '), DialogDescription: { template: '
' }, Label: { template: '' }, ResponsiveModal: { name: 'ResponsiveModal', props: ['open', 'dialogClass', 'sheetClass', 'showCloseButton'], template: '
', }, ResponsiveModalFooter: { template: '' }, ResponsiveModalHeader: { template: '
' }, ResponsiveModalTitle: { template: '
' }, Switch: { name: 'Switch', props: ['modelValue'], template: '
' }, Tooltip: { template: '
' }, TooltipTrigger: { template: '
' }, TooltipContent: { template: '
' }, TooltipProvider: { template: '
' }, })); vi.mock('@heroicons/vue/24/solid', () => ({ ArrowTopRightOnSquareIcon: { template: '' }, CheckCircleIcon: { template: '' }, CogIcon: { template: '' }, EyeIcon: { template: '' }, IdentificationIcon: { template: '' }, KeyIcon: { template: '' }, })); vi.mock('@heroicons/vue/24/outline', () => ({ ArrowDownTrayIcon: { template: '' }, })); vi.mock('~/components/UpdateOs/IgnoredRelease.vue', () => ({ default: { template: '
', props: ['label'] }, })); vi.mock('~/composables/dateTime', () => ({ default: () => ({ outputDateTimeFormatted: ref('2024-01-01'), outputDateTimeReadableDiff: ref('today'), }), })); vi.mock('pinia', async () => { const actual = await vi.importActual('pinia'); const isActualStore = (candidate: unknown): candidate is Parameters[0] => Boolean(candidate && typeof candidate === 'object' && '$id' in candidate); const isRefLike = (input: unknown): input is { value: unknown } => Boolean(input && typeof input === 'object' && 'value' in input); return { ...actual, storeToRefs: (store: unknown) => { if (isActualStore(store)) { return actual.storeToRefs(store); } if (!store || typeof store !== 'object') { return {}; } const refs: Record = {}; for (const [key, value] of Object.entries(store)) { if (isRefLike(value)) { refs[key] = value; } } return refs; }, }; }); const mockAccountUpdateOs = vi.fn(); vi.mock('~/store/account', () => ({ useAccountStore: () => ({ updateOs: mockAccountUpdateOs, }), })); const mockRenew = vi.fn(); vi.mock('~/store/purchase', () => ({ usePurchaseStore: () => ({ renew: mockRenew, }), })); const mockSetReleaseForUpdate = vi.fn(); const mockSetModalOpen = vi.fn(); const mockFetchAndConfirmInstall = vi.fn(); const available = ref(null); const availableWithRenewal = ref(null); const availableReleaseDate = ref(null); const availableRequiresAuth = ref(false); const checkForUpdatesLoading = ref(false); vi.mock('~/store/updateOs', () => ({ useUpdateOsStore: () => ({ available, availableWithRenewal, availableReleaseDate, availableRequiresAuth, checkForUpdatesLoading, setReleaseForUpdate: mockSetReleaseForUpdate, setModalOpen: mockSetModalOpen, fetchAndConfirmInstall: mockFetchAndConfirmInstall, }), })); const regExp = ref(null); const regUpdatesExpired = ref(false); const dateTimeFormat = ref('YYYY-MM-DD'); const osVersion = ref(null); const updateOsIgnoredReleases = ref([]); const updateOsNotificationsEnabled = ref(true); const updateOsResponse = ref<{ changelog?: string | null } | null>(null); const mockUpdateOsIgnoreRelease = vi.fn(); vi.mock('~/store/server', () => ({ useServerStore: () => ({ regExp, regUpdatesExpired, dateTimeFormat, osVersion, updateOsIgnoredReleases, updateOsNotificationsEnabled, updateOsResponse, updateOsIgnoreRelease: mockUpdateOsIgnoreRelease, }), })); const mountModal = () => mount(CheckUpdateResponseModal, { props: { open: true, }, global: { plugins: [createTestI18n()], }, }); describe('CheckUpdateResponseModal', () => { beforeEach(() => { available.value = null; availableWithRenewal.value = null; availableReleaseDate.value = null; availableRequiresAuth.value = false; checkForUpdatesLoading.value = false; regExp.value = null; regUpdatesExpired.value = false; osVersion.value = null; updateOsIgnoredReleases.value = []; updateOsNotificationsEnabled.value = true; updateOsResponse.value = null; mockAccountUpdateOs.mockClear(); mockRenew.mockClear(); mockSetModalOpen.mockClear(); mockSetReleaseForUpdate.mockClear(); mockFetchAndConfirmInstall.mockClear(); mockUpdateOsIgnoreRelease.mockClear(); }); it('renders loading state while checking for updates', () => { expect(testTranslate('updateOs.checkUpdateResponseModal.checkingForOsUpdates')).toBe( 'Checking for OS updates...' ); checkForUpdatesLoading.value = true; const wrapper = mountModal(); expect(wrapper.find('.responsive-modal-title').text()).toBe('Checking for OS updates...'); expect(wrapper.find('.brand-loading').exists()).toBe(true); expect(wrapper.find('.ui-button').text()).toBe('More Options'); }); it('shows up-to-date messaging when no updates are available', async () => { osVersion.value = '6.12.3'; updateOsNotificationsEnabled.value = false; const wrapper = mountModal(); await nextTick(); expect(wrapper.find('.responsive-modal-title').text()).toBe('Unraid OS is up-to-date'); expect(wrapper.text()).toContain('Current Version 6.12.3'); expect(wrapper.text()).toContain( 'Go to Settings > Notifications to enable automatic OS update notifications for future releases.' ); expect(wrapper.find('.ui-button').text()).toBe('More Options'); expect(wrapper.text()).toContain('Enable update notifications'); }); it('displays update actions when a new release is available', async () => { available.value = '6.13.0'; osVersion.value = '6.12.3'; updateOsResponse.value = { changelog: '### New release' }; const wrapper = mountModal(); await nextTick(); const actionButtons = wrapper.findAll('.brand-button'); const viewChangelogButton = actionButtons.find((button) => button.text().includes('View Changelog to Start Update') ); expect(viewChangelogButton).toBeDefined(); await viewChangelogButton!.trigger('click'); expect(mockSetReleaseForUpdate).toHaveBeenCalledWith({ changelog: '### New release' }); }); it('includes renew option when update requires license renewal', async () => { available.value = '6.14.0'; availableWithRenewal.value = '6.14.0'; updateOsResponse.value = { changelog: '### Renewal release' }; const wrapper = mountModal(); await nextTick(); const actionButtons = wrapper.findAll('.brand-button'); const labels = actionButtons.map((button) => button.text()); expect(labels).toContain('View Changelog'); expect(labels).toContain('Extend License'); await actionButtons.find((btn) => btn.text() === 'Extend License')?.trigger('click'); expect(mockRenew).toHaveBeenCalled(); }); });