Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
258 changes: 258 additions & 0 deletions web/__test__/components/Activation/ActivationModal.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
/**
* Activation Modal Component Test Coverage
*/

import { ref } from 'vue';
import { mount } from '@vue/test-utils';

import { beforeEach, describe, expect, it, vi } from 'vitest';

import type { ComposerTranslation } from 'vue-i18n';

import ActivationModal from '~/components/Activation/ActivationModal.vue';

const mockT = (key: string, args?: unknown[]) => (args ? `${key} ${JSON.stringify(args)}` : key);

const mockComponents = {
Modal: {
template: `
<div data-testid="modal" v-if="open">
<div data-testid="modal-header"><slot name="header" /></div>
<div data-testid="modal-body"><slot /></div>
<div data-testid="modal-footer"><slot name="footer" /></div>
<div data-testid="modal-subfooter"><slot name="subFooter" /></div>
</div>
`,
props: [
't',
'open',
'showCloseX',
'title',
'titleInMain',
'description',
'overlayColor',
'overlayOpacity',
'maxWidth',
'modalVerticalCenter',
'disableShadow',
'disableOverlayClose',
],
},
ActivationPartnerLogo: {
template: '<div data-testid="partner-logo"></div>',
props: ['name'],
},
ActivationSteps: {
template: '<div data-testid="activation-steps" :active-step="activeStep"></div>',
props: ['activeStep'],
},
BrandButton: {
template:
'<button data-testid="brand-button" :type="type" @click="$emit(\'click\')"><slot /></button>',
props: ['text', 'iconRight', 'variant', 'external', 'href', 'size', 'type'],
emits: ['click'],
},
};

const mockActivationCodeDataStore = {
partnerInfo: ref({
hasPartnerLogo: false,
partnerName: null as string | null,
}),
};

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(),
};

// Mock all imports
vi.mock('vue-i18n', () => ({
useI18n: () => ({
t: mockT,
}),
}));

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('~/store/purchase', () => ({
usePurchaseStore: () => mockPurchaseStore,
}));

vi.mock('~/store/theme', () => ({
useThemeStore: vi.fn(),
}));

vi.mock('@heroicons/vue/24/solid', () => ({
ArrowTopRightOnSquareIcon: {},
}));

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();

mockActivationCodeDataStore.partnerInfo.value = {
hasPartnerLogo: false,
partnerName: null,
};

mockActivationCodeModalStore.isVisible.value = true;

// Reset window.location
Object.defineProperty(window, 'location', {
writable: true,
value: { href: '' },
});

handleKeydown = null;
});

const mountComponent = () => {
return mount(ActivationModal, {
props: { t: mockT as unknown as ComposerTranslation },
global: {
stubs: mockComponents,
},
});
};

it('uses the correct title text', () => {
mountComponent();

expect(mockT("Let's activate your Unraid OS License")).toBe("Let's activate your Unraid OS License");
});

it('uses the correct description text', () => {
mountComponent();

const descriptionText = mockT(
`On the following screen, your license will be activated. You'll then create an Unraid.net Account to manage your license going forward.`
);

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('More about Licensing');
const accountsText = mockT('More about Unraid.net Accounts');

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('calls activate method when Activate Now button is clicked', async () => {
const wrapper = mountComponent();
const button = wrapper.find('[data-testid="brand-button"]');

expect(button.exists()).toBe(true);

await button.trigger('click');

expect(mockPurchaseStore.activate).toHaveBeenCalledTimes(1);
});

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.html()).toBe('<!--v-if-->');
});

it('renders activation steps with correct active step', () => {
const wrapper = mountComponent();

expect(wrapper.html()).toContain('data-testid="activation-steps"');
expect(wrapper.html()).toContain('active-step="2"');
});
});
81 changes: 81 additions & 0 deletions web/__test__/components/Activation/ActivationPartnerLogo.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/**
* ActivationPartnerLogo Component Test Coverage
*/

import { ref } from 'vue';
import { mount } from '@vue/test-utils';

import { beforeEach, describe, expect, it, vi } from 'vitest';

import ActivationPartnerLogo from '~/components/Activation/ActivationPartnerLogo.vue';

const mockActivationPartnerLogoImg = {
template: '<div data-testid="partner-logo-img"></div>',
};

const mockActivationCodeDataStore = {
partnerInfo: ref({
partnerUrl: null as string | null,
}),
};

vi.mock('~/components/Activation/store/activationCodeData', () => ({
useActivationCodeDataStore: () => mockActivationCodeDataStore,
}));

describe('ActivationPartnerLogo', () => {
beforeEach(() => {
vi.clearAllMocks();
mockActivationCodeDataStore.partnerInfo.value = {
partnerUrl: null,
};
});

const mountComponent = () => {
return mount(ActivationPartnerLogo, {
global: {
stubs: {
ActivationPartnerLogoImg: mockActivationPartnerLogoImg,
},
},
});
};

it('renders a link with partner logo when partnerUrl exists', () => {
mockActivationCodeDataStore.partnerInfo.value = {
partnerUrl: 'https://example.com',
};

const wrapper = mountComponent();
const link = wrapper.find('a');
const logoImg = wrapper.find('[data-testid="partner-logo-img"]');

expect(link.exists()).toBe(true);
expect(link.attributes('href')).toBe('https://example.com');
expect(link.attributes('target')).toBe('_blank');
expect(link.attributes('rel')).toBe('noopener noreferrer');
expect(logoImg.exists()).toBe(true);
});

it('does not render anything when no partnerUrl exists', () => {
const wrapper = mountComponent();
const link = wrapper.find('a');
const logoImg = wrapper.find('[data-testid="partner-logo-img"]');

expect(link.exists()).toBe(false);
expect(logoImg.exists()).toBe(false);
});

it('applies correct opacity classes for hover and focus states', () => {
mockActivationCodeDataStore.partnerInfo.value = {
partnerUrl: 'https://example.com',
};

const wrapper = mountComponent();
const link = wrapper.find('a');

expect(link.classes()).toContain('opacity-100');
expect(link.classes()).toContain('hover:opacity-75');
expect(link.classes()).toContain('focus:opacity-75');
});
});
Loading
Loading