/** * Activation Modal Component Test Coverage */ import { ref } from 'vue'; import { flushPromises, mount } from '@vue/test-utils'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import ActivationModal from '~/components/Activation/ActivationModal.vue'; import { createTestI18n, testTranslate } from '../../utils/i18n'; vi.mock('@unraid/ui', async (importOriginal) => { const actual = (await importOriginal()) as Record; return { ...actual, Dialog: { name: 'Dialog', props: ['modelValue', 'title', 'description', 'showFooter', 'size', 'showCloseButton'], emits: ['update:modelValue'], template: `
`, }, BrandButton: { template: '', props: ['text', 'iconRight', 'variant', 'external', 'href', 'size', 'type'], emits: ['click'], }, }; }); const mockT = testTranslate; const mockComponents = { ActivationPartnerLogo: { template: '
', props: ['partnerInfo'], }, ActivationSteps: { template: '
', props: ['steps', 'activeStepIndex', 'onStepClick'], }, ActivationPluginsStep: { template: '
', props: ['t', 'onComplete', 'onSkip', 'onBack', 'showSkip', 'showBack'], }, ActivationTimezoneStep: { template: '
', props: ['t', 'onComplete', 'onSkip', 'onBack', 'showSkip', 'showBack'], }, ActivationWelcomeStep: { template: '
', props: [ 'currentVersion', 'previousVersion', 'partnerName', 'onComplete', 'onSkip', 'onBack', 'showSkip', 'showBack', 'redirectToLogin', ], }, ActivationLicenseStep: { template: '
', props: [ 'modalTitle', 'modalDescription', 'docsButtons', 'canGoBack', 'purchaseStore', 'onComplete', 'onBack', 'showBack', ], }, }; const mockActivationCodeDataStore = { partnerInfo: ref({ hasPartnerLogo: false, partnerName: null as string | null, }), activationCode: ref({ code: 'TEST-CODE-123' }), isFreshInstall: ref(true), }; let handleKeydown: ((e: KeyboardEvent) => void) | null = null; const mockActivationCodeModalStore = { isVisible: ref(true), setIsHidden: vi.fn((value: boolean) => { if (value === true) { window.location.href = '/Tools/Registration'; } }), // This gets defined after we mock the store _store: null as unknown, }; const mockPurchaseStore = { activate: vi.fn(), }; const mockStepDefinitions = [ { id: 'TIMEZONE', required: true, completed: false, introducedIn: '7.0.0', title: 'Set Time Zone', description: 'Configure system time', icon: 'i-heroicons-clock', }, { id: 'PLUGINS', required: false, completed: false, introducedIn: '7.0.0', title: 'Install Essential Plugins', description: 'Add helpful plugins', icon: 'i-heroicons-puzzle-piece', }, { id: 'ACTIVATION', required: true, completed: false, introducedIn: '7.0.0', title: 'Activate License', description: 'Create an Unraid.net account and activate your key', icon: 'i-heroicons-key', }, ]; const mockUpgradeOnboardingStore = { shouldShowUpgradeOnboarding: ref(false), upgradeSteps: ref(mockStepDefinitions), allUpgradeSteps: ref(mockStepDefinitions), currentVersion: ref('7.0.0'), previousVersion: ref('6.12.0'), refetchActivationOnboarding: vi.fn().mockResolvedValue(undefined), }; const mutateMock = vi.fn().mockResolvedValue(undefined); vi.mock('@vue/apollo-composable', () => ({ useMutation: () => ({ mutate: mutateMock, onDone: vi.fn(), onError: vi.fn(), }), useLazyQuery: () => ({ load: vi.fn(), refetch: vi.fn().mockResolvedValue(undefined), onResult: vi.fn(), onError: vi.fn(), }), })); // Mock all imports vi.mock('vue-i18n', async (importOriginal) => { const actual = (await importOriginal()) as typeof import('vue-i18n'); return { ...(actual as Record), useI18n: () => ({ t: mockT, }), } as typeof import('vue-i18n'); }); vi.mock('~/components/Activation/store/activationCodeModal', () => { const store = { useActivationCodeModalStore: () => { mockActivationCodeModalStore._store = mockActivationCodeModalStore; return mockActivationCodeModalStore; }, }; return store; }); vi.mock('~/components/Activation/store/activationCodeData', () => ({ useActivationCodeDataStore: () => mockActivationCodeDataStore, })); vi.mock('~/components/Activation/store/upgradeOnboarding', () => ({ useUpgradeOnboardingStore: () => mockUpgradeOnboardingStore, })); vi.mock('~/store/purchase', () => ({ usePurchaseStore: () => mockPurchaseStore, })); vi.mock('~/store/theme', () => ({ useThemeStore: () => ({ fetchTheme: vi.fn().mockResolvedValue(undefined), }), })); vi.mock('@heroicons/vue/24/solid', () => ({ ArrowTopRightOnSquareIcon: {}, ArrowPathIcon: {}, ArrowRightOnRectangleIcon: {}, CogIcon: {}, GlobeAltIcon: {}, InformationCircleIcon: {}, KeyIcon: {}, QuestionMarkCircleIcon: {}, })); vi.mock('@nuxt/ui', async (importOriginal) => { const actual = (await importOriginal()) as Record; return { ...actual, UStepper: { name: 'UStepper', props: ['modelValue', 'items', 'orientation'], template: '
', }, }; }); const originalAddEventListener = window.addEventListener; window.addEventListener = vi.fn((event: string, handler: EventListenerOrEventListenerObject) => { if (event === 'keydown') { handleKeydown = handler as unknown as (e: KeyboardEvent) => void; } return originalAddEventListener(event, handler); }); describe('Activation/ActivationModal.vue', () => { beforeEach(() => { vi.clearAllMocks(); mutateMock.mockClear(); mockUpgradeOnboardingStore.refetchActivationOnboarding.mockClear(); mockActivationCodeDataStore.partnerInfo.value = { hasPartnerLogo: false, partnerName: null, }; mockActivationCodeModalStore.isVisible.value = true; // Reset window.location Object.defineProperty(window, 'location', { writable: true, value: { href: '', hostname: 'localhost' }, }); handleKeydown = null; mockUpgradeOnboardingStore.shouldShowUpgradeOnboarding.value = false; mockUpgradeOnboardingStore.upgradeSteps.value = mockStepDefinitions.map((step) => ({ ...step })); mockUpgradeOnboardingStore.allUpgradeSteps.value = mockStepDefinitions.map((step) => ({ ...step, })); }); const mountComponent = () => { return mount(ActivationModal, { global: { plugins: [createTestI18n()], stubs: mockComponents, }, }); }; it('uses the correct title text', () => { mountComponent(); expect(mockT('activation.activationModal.letSActivateYourUnraidOs')).toBe( "Let's activate your Unraid OS License" ); }); it('uses the correct description text', () => { mountComponent(); const descriptionText = mockT('activation.activationModal.onTheFollowingScreenYourLicense'); expect(descriptionText).toBe( "On the following screen, your license will be activated. You'll then create an Unraid.net Account to manage your license going forward." ); }); it('provides documentation links with correct URLs', () => { mountComponent(); const licensingText = mockT('activation.activationModal.moreAboutLicensing'); const accountsText = mockT('activation.activationModal.moreAboutUnraidNetAccounts'); expect(licensingText).toBe('More about Licensing'); expect(accountsText).toBe('More about Unraid.net Accounts'); }); it('displays the partner logo when available', () => { mockActivationCodeDataStore.partnerInfo.value = { hasPartnerLogo: true, partnerName: 'partner-name', }; const wrapper = mountComponent(); expect(wrapper.html()).toContain('data-testid="partner-logo"'); }); it('renders timezone step initially when activation code is present', async () => { const wrapper = mountComponent(); // The component now renders steps dynamically based on the step registry // Check that the activation steps component is rendered expect(wrapper.html()).toContain('data-testid="activation-steps"'); }); it('handles Konami code sequence to close modal and redirect', async () => { mountComponent(); if (!handleKeydown) { return; } const konamiCode = [ 'ArrowUp', 'ArrowUp', 'ArrowDown', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'ArrowLeft', 'ArrowRight', 'b', 'a', ]; for (const key of konamiCode) { handleKeydown(new KeyboardEvent('keydown', { key })); } expect(mockActivationCodeModalStore.setIsHidden).toHaveBeenCalledWith(true); expect(window.location.href).toBe('/Tools/Registration'); }); it('does not trigger konami code action for incorrect sequence', async () => { mountComponent(); if (!handleKeydown) { return; } const incorrectSequence = ['ArrowUp', 'ArrowDown', 'b', 'a']; for (const key of incorrectSequence) { handleKeydown(new KeyboardEvent('keydown', { key })); } expect(mockActivationCodeModalStore.setIsHidden).not.toHaveBeenCalled(); expect(window.location.href).toBe(''); }); it('does not render when isVisible is false', () => { mockActivationCodeModalStore.isVisible.value = false; const wrapper = mountComponent(); expect(wrapper.find('[role="dialog"]').exists()).toBe(false); }); it('marks pending upgrade steps complete when the modal is closed', async () => { mockUpgradeOnboardingStore.shouldShowUpgradeOnboarding.value = true; mockUpgradeOnboardingStore.upgradeSteps.value = [ { id: 'TIMEZONE', required: true, completed: false, introducedIn: '7.0.0', title: 'Set Time Zone', description: 'Configure system time', icon: 'i-heroicons-clock', }, { id: 'PLUGINS', required: false, completed: false, introducedIn: '7.0.0', title: 'Install Essential Plugins', description: 'Add helpful plugins', icon: 'i-heroicons-puzzle-piece', }, ]; mockUpgradeOnboardingStore.allUpgradeSteps.value = mockUpgradeOnboardingStore.upgradeSteps.value; const wrapper = mountComponent(); const dialog = wrapper.findComponent({ name: 'Dialog' }); expect(dialog.exists()).toBe(true); dialog.vm.$emit('update:modelValue', false); await flushPromises(); expect(mutateMock).toHaveBeenCalledTimes(2); expect(mutateMock).toHaveBeenNthCalledWith(1, { input: { stepId: 'TIMEZONE' } }); expect(mutateMock).toHaveBeenNthCalledWith(2, { input: { stepId: 'PLUGINS' } }); expect(mockUpgradeOnboardingStore.refetchActivationOnboarding).toHaveBeenCalledTimes(1); }); it('renders activation steps with correct active step', () => { const wrapper = mountComponent(); expect(wrapper.html()).toContain('data-testid="activation-steps"'); // The component now uses activeStepIndex prop instead of active-step attribute const activationSteps = wrapper.find('[data-testid="activation-steps"]'); expect(activationSteps.exists()).toBe(true); }); });