Files
api/web/__test__/components/Activation/WelcomeModal.test.ts
Eli Bosley 3b00fec5fd chore: Remove legacy store modules and add new API key and reporting services (#1536)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added developer CLI tools for toggling GraphQL sandbox and modal
testing utilities.
* Introduced a "Show Activation Modal" developer component for UI
testing.
  * Added system initial setup detection and related GraphQL queries.
* Enhanced login and welcome pages with dynamic server info and initial
setup state.
  * Improved SSO button with internationalization and error handling.
* Added internal CLI admin API key management service and internal
GraphQL client service.
* Introduced comprehensive API report generation service for system and
service status.
* Added CLI commands and GraphQL mutations/queries for plugin and SSO
user management.
* Added new modal target components and improved teleport target
detection.

* **Enhancements**
* Refined modal dialog targeting and teleportation for flexible UI
placement.
* Updated modal components and stores for improved activation/welcome
modal control.
  * Improved plugin and SSO user management via CLI through GraphQL API.
* Refactored partner logo components to use props instead of store
dependencies.
  * Enhanced styling and accessibility for buttons and modals.
* Streamlined Tailwind CSS integration with shared styles and updated
theme variables.
* Improved GraphQL module configuration to avoid directive conflicts in
tests.
  * Adjusted Vite config for better dependency handling in test mode.
  * Improved error handling and logging in CLI commands and services.
* Reordered imports and refined component class bindings for UI
consistency.

* **Bug Fixes**
* Resolved issues with duplicate script tags and component registration
in the web UI.
* Fixed modal close button visibility and activation modal state
handling.
* Added error handling and logging improvements across CLI commands and
services.
  * Fixed newline issues in last-download-time fixture files.

* **Chores**
* Added and updated numerous tests for CLI commands, services, and UI
components.
* Updated translation files and localization resources for new UI
messages.
* Adjusted environment, configuration, and dependency files for improved
development and test workflows.
  * Cleaned up unused imports and mocks in tests.
  * Reorganized exports and barrel files in shared and UI modules.
  * Added integration and dependency resolution tests for core modules.

* **Removals & Refactoring**
* Removed legacy Redux state management, configuration, and UPnP logic
from the backend.
* Eliminated deprecated GraphQL subscriptions and client code related to
registration and mothership.
* Removed direct store manipulation and replaced with service-based
approaches in CLI commands.
  * Deleted unused or redundant test files and configuration listeners.
* Refactored SSO user service to consolidate add/remove operations into
a single update method.
* Simplified API key services with new methods for automatic key
management.
* Replaced direct plugin and SSO user service calls with GraphQL client
interactions in CLI commands.
* Removed complex theme fallback and dark mode CSS rules, replacing with
streamlined static theme variables.
* Cleaned up Tailwind CSS configuration and removed deprecated local
styles.
* Removed multiple internal utility files and replaced with simplified
or centralized implementations.
* Removed deprecated local configuration and synchronization files and
listeners.
  * Removed UPnP helper functions and job management classes.
* Refactored server resolver to dynamically construct local server data
internally.
* Removed CORS handler and replaced with simplified or externalized
logic.
* Removed store synchronization and registration event pubsub handling.
* Removed GraphQL client creation utilities for internal API
communication.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-25 15:07:37 -04:00

326 lines
9.6 KiB
TypeScript

/**
* WelcomeModal Component Test Coverage
*/
import { ref } from 'vue';
import { mount } from '@vue/test-utils';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import type { ComposerTranslation } from 'vue-i18n';
import WelcomeModal from '~/components/Activation/WelcomeModal.ce.vue';
vi.mock('@unraid/ui', async (importOriginal) => {
const actual = await importOriginal<typeof import('@unraid/ui')>();
return {
...actual,
Dialog: {
name: 'Dialog',
props: ['modelValue', 'title', 'description', 'showFooter', 'size', 'showCloseButton'],
emits: ['update:modelValue'],
template: `
<div v-if="modelValue" role="dialog" aria-modal="true">
<div v-if="$slots.header" class="dialog-header"><slot name="header" /></div>
<div class="dialog-body"><slot /></div>
<div v-if="$slots.footer" class="dialog-footer"><slot name="footer" /></div>
</div>
`,
},
};
});
const mockT = (key: string, args?: unknown[]) => (args ? `${key} ${JSON.stringify(args)}` : key);
const mockComponents = {
ActivationPartnerLogo: {
template: '<div data-testid="partner-logo"></div>',
props: ['partnerInfo'],
},
ActivationSteps: {
template: '<div data-testid="activation-steps" :active-step="activeStep"></div>',
props: ['activeStep'],
},
};
const mockWelcomeModalDataStore = {
partnerInfo: ref({
hasPartnerLogo: false,
partnerName: null as string | null,
}),
loading: ref(false),
isInitialSetup: ref(true), // Default to true for testing
};
const mockThemeStore = {
setTheme: vi.fn(),
};
vi.mock('vue-i18n', () => ({
useI18n: () => ({
t: mockT,
}),
}));
vi.mock('~/components/Activation/store/welcomeModalData', () => ({
useWelcomeModalDataStore: () => mockWelcomeModalDataStore,
}));
vi.mock('~/store/theme', () => ({
useThemeStore: () => mockThemeStore,
}));
describe('Activation/WelcomeModal.ce.vue', () => {
let mockSetProperty: ReturnType<typeof vi.fn>;
let mockQuerySelector: ReturnType<typeof vi.fn>;
beforeEach(() => {
vi.clearAllMocks();
vi.useFakeTimers();
mockWelcomeModalDataStore.partnerInfo.value = {
hasPartnerLogo: false,
partnerName: null,
};
mockWelcomeModalDataStore.loading.value = false;
mockWelcomeModalDataStore.isInitialSetup.value = true;
// Mock document methods
mockSetProperty = vi.fn();
mockQuerySelector = vi.fn();
Object.defineProperty(window.document, 'querySelector', {
value: mockQuerySelector,
writable: true,
});
Object.defineProperty(window.document.documentElement.style, 'setProperty', {
value: mockSetProperty,
writable: true,
});
// Mock window.location.pathname to simulate being on /login page
Object.defineProperty(window, 'location', {
value: {
pathname: '/login',
},
writable: true,
});
});
afterEach(() => {
vi.useRealTimers();
});
const mountComponent = async () => {
const wrapper = mount(WelcomeModal, {
props: { t: mockT as unknown as ComposerTranslation },
global: {
stubs: mockComponents,
},
});
await wrapper.vm.$nextTick();
return wrapper;
};
it('uses the correct title text when no partner name is provided', () => {
mountComponent();
expect(mockT('Welcome to Unraid!')).toBe('Welcome to Unraid!');
});
it('uses the correct title text when partner name is provided', () => {
mockWelcomeModalDataStore.partnerInfo.value = {
hasPartnerLogo: true,
partnerName: 'Test Partner',
};
mountComponent();
expect(mockT('Welcome to your new {0} system, powered by Unraid!', ['Test Partner'])).toBe(
'Welcome to your new {0} system, powered by Unraid! ["Test Partner"]'
);
});
it('uses the correct description text', () => {
mountComponent();
const descriptionText = mockT(
`First, you'll create your device's login credentials, then you'll activate your Unraid license—your device's operating system (OS).`
);
expect(descriptionText).toBe(
"First, you'll create your device's login credentials, then you'll activate your Unraid license—your device's operating system (OS)."
);
});
it('displays the partner logo when available', async () => {
mockWelcomeModalDataStore.partnerInfo.value = {
hasPartnerLogo: true,
partnerName: 'Test Partner',
};
const wrapper = await mountComponent();
const partnerLogo = wrapper.find('[data-testid="partner-logo"]');
expect(partnerLogo.exists()).toBe(true);
});
it('hides modal when Create a password button is clicked', async () => {
const wrapper = await mountComponent();
const button = wrapper.find('button');
expect(button.exists()).toBe(true);
// Initially dialog should be visible
let dialog = wrapper.findComponent({ name: 'Dialog' });
expect(dialog.exists()).toBe(true);
expect(dialog.props('modelValue')).toBe(true);
await button.trigger('click');
await wrapper.vm.$nextTick();
// After click, the dialog should be hidden (modelValue should be false)
dialog = wrapper.findComponent({ name: 'Dialog' });
expect(dialog.exists()).toBe(true);
expect(dialog.props('modelValue')).toBe(false);
});
it('disables the Create a password button when loading', async () => {
mockWelcomeModalDataStore.loading.value = true;
const wrapper = await mountComponent();
const button = wrapper.find('button');
expect(button.exists()).toBe(true);
expect(button.attributes('disabled')).toBe('');
});
it('renders activation steps with correct active step', async () => {
const wrapper = await mountComponent();
const activationSteps = wrapper.find('[data-testid="activation-steps"]');
expect(activationSteps.exists()).toBe(true);
expect(activationSteps.attributes('active-step')).toBe('1');
});
it('calls setTheme on mount', () => {
mountComponent();
expect(mockThemeStore.setTheme).toHaveBeenCalled();
});
it('handles theme setting error gracefully', async () => {
vi.useFakeTimers();
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
mockThemeStore.setTheme.mockRejectedValueOnce(new Error('Theme error'));
mountComponent();
await vi.runAllTimersAsync();
expect(consoleErrorSpy).toHaveBeenCalledWith('Error setting theme:', expect.any(Error));
consoleErrorSpy.mockRestore();
vi.useRealTimers();
});
it('shows modal on login page even when isInitialSetup is false', async () => {
Object.defineProperty(window, 'location', {
value: { pathname: '/login' },
writable: true,
});
mockWelcomeModalDataStore.isInitialSetup.value = false;
const wrapper = await mountComponent();
const dialog = wrapper.findComponent({ name: 'Dialog' });
expect(dialog.exists()).toBe(true);
});
it('shows modal on non-login page when isInitialSetup is true', async () => {
Object.defineProperty(window, 'location', {
value: { pathname: '/Dashboard' },
writable: true,
});
mockWelcomeModalDataStore.isInitialSetup.value = true;
const wrapper = await mountComponent();
const dialog = wrapper.findComponent({ name: 'Dialog' });
expect(dialog.exists()).toBe(true);
});
it('does not show modal on non-login page when isInitialSetup is false', async () => {
Object.defineProperty(window, 'location', {
value: { pathname: '/Dashboard' },
writable: true,
});
mockWelcomeModalDataStore.isInitialSetup.value = false;
const wrapper = await mountComponent();
const dialog = wrapper.findComponent({ name: 'Dialog' });
expect(dialog.exists()).toBe(true);
expect(dialog.props('modelValue')).toBe(false);
});
describe('Modal properties', () => {
it('shows close button when on /login page', async () => {
Object.defineProperty(window, 'location', {
value: { pathname: '/login' },
writable: true,
});
const wrapper = await mountComponent();
const dialog = wrapper.findComponent({ name: 'Dialog' });
expect(dialog.exists()).toBe(true);
expect(dialog.props('showCloseButton')).toBe(true);
});
it('hides close button when NOT on /login page', async () => {
// Set location to a non-login page
Object.defineProperty(window, 'location', {
value: { pathname: '/Dashboard' },
writable: true,
});
const wrapper = mount(WelcomeModal, {
props: { t: mockT as unknown as ComposerTranslation },
global: {
stubs: mockComponents,
},
});
// Manually show the modal since it won't auto-show on non-login pages
wrapper.vm.showWelcomeModal();
await wrapper.vm.$nextTick();
const dialog = wrapper.findComponent({ name: 'Dialog' });
expect(dialog.exists()).toBe(true);
expect(dialog.props('showCloseButton')).toBe(false);
});
it('passes correct props to Dialog component', async () => {
const wrapper = await mountComponent();
const dialog = wrapper.findComponent({ name: 'Dialog' });
expect(dialog.exists()).toBe(true);
expect(dialog.props()).toMatchObject({
modelValue: true,
showFooter: false,
showCloseButton: true,
size: 'full',
});
});
it('renders modal with correct content', async () => {
const wrapper = await mountComponent();
// Check that the modal is rendered
const dialog = wrapper.findComponent({ name: 'Dialog' });
expect(dialog.exists()).toBe(true);
expect(wrapper.text()).toContain('Welcome to Unraid!');
expect(wrapper.text()).toContain('Create a password');
});
});
});