Files
api/web/__test__/components/Wrapper/component-registry.test.ts
T
Eli Bosley c855caa9b2 fix: improve vue mount speed by 10x
- Refactored teleport container management to be lazily created,
improving performance by avoiding unnecessary DOM manipulations.
- Updated `useTeleport` to dynamically determine the correct teleport
target based on mounted components.
- Removed the `ensureTeleportContainer` import from various components,
streamlining the mounting process.
- Adjusted the dropdown menu component to utilize a computed property
for teleport target management.
- Enhanced the component registry to support a unified app architecture,
replacing legacy mounting functions with a more efficient approach.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
  * Support bundles may include the GraphQL API log when present.
  * OS version data loads lazily when the header dropdown first opens.
* Many UI components now load on demand via a unified mounting approach.

* **Bug Fixes**
  * Dropdowns and modals consistently stack above other UI elements.
  * Server status layout fixes improve small-screen alignment.

* **Breaking Changes**
* Teleport/mounting APIs and public teleport helper were
consolidated/removed; integrations may need update.

* **Tests**
* Extensive new unit tests added for mounting, teleport, modals, and
REST log handling.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2025-09-15 11:24:31 -04:00

185 lines
7.7 KiB
TypeScript

import { describe, expect, it, vi } from 'vitest';
// Mock Vue's defineAsyncComponent
vi.mock('vue', () => ({
defineAsyncComponent: vi.fn((loader) => ({ loader, __asyncComponent: true })),
}));
// Mock CSS imports
vi.mock('~/assets/main.css', () => ({}));
vi.mock('@unraid/ui/styles', () => ({}));
// Mock all component imports
vi.mock('@/components/HeaderOsVersion.standalone.vue', () => ({ default: 'HeaderOsVersion' }));
vi.mock('@/components/UserProfile.standalone.vue', () => ({ default: 'UserProfile' }));
vi.mock('../Auth.standalone.vue', () => ({ default: 'Auth' }));
vi.mock('../ConnectSettings/ConnectSettings.standalone.vue', () => ({ default: 'ConnectSettings' }));
vi.mock('../DownloadApiLogs.standalone.vue', () => ({ default: 'DownloadApiLogs' }));
vi.mock('@/components/Modals.standalone.vue', () => ({ default: 'Modals' }));
vi.mock('../Registration.standalone.vue', () => ({ default: 'Registration' }));
vi.mock('../WanIpCheck.standalone.vue', () => ({ default: 'WanIpCheck' }));
vi.mock('../CallbackHandler.standalone.vue', () => ({ default: 'CallbackHandler' }));
vi.mock('../Logs/LogViewer.standalone.vue', () => ({ default: 'LogViewer' }));
vi.mock('../SsoButton.standalone.vue', () => ({ default: 'SsoButton' }));
vi.mock('../Activation/WelcomeModal.standalone.vue', () => ({ default: 'WelcomeModal' }));
vi.mock('../UpdateOs.standalone.vue', () => ({ default: 'UpdateOs' }));
vi.mock('../DowngradeOs.standalone.vue', () => ({ default: 'DowngradeOs' }));
vi.mock('../DevSettings.vue', () => ({ default: 'DevSettings' }));
vi.mock('../ApiKeyPage.standalone.vue', () => ({ default: 'ApiKeyPage' }));
vi.mock('../ApiKeyAuthorize.standalone.vue', () => ({ default: 'ApiKeyAuthorize' }));
vi.mock('../DevModalTest.standalone.vue', () => ({ default: 'DevModalTest' }));
vi.mock('../LayoutViews/Detail/DetailTest.standalone.vue', () => ({ default: 'DetailTest' }));
vi.mock('@/components/ThemeSwitcher.standalone.vue', () => ({ default: 'ThemeSwitcher' }));
vi.mock('../ColorSwitcher.standalone.vue', () => ({ default: 'ColorSwitcher' }));
vi.mock('@/components/UnraidToaster.vue', () => ({ default: 'UnraidToaster' }));
vi.mock('../UpdateOs/TestUpdateModal.standalone.vue', () => ({ default: 'TestUpdateModal' }));
vi.mock('../TestThemeSwitcher.standalone.vue', () => ({ default: 'TestThemeSwitcher' }));
describe('component-registry', () => {
it('should export ComponentMapping type', async () => {
const module = await import('~/components/Wrapper/component-registry');
expect(module).toBeDefined();
});
it('should export componentMappings array', async () => {
const { componentMappings } = await import('~/components/Wrapper/component-registry');
expect(Array.isArray(componentMappings)).toBe(true);
expect(componentMappings.length).toBeGreaterThan(0);
});
it('should have required properties for each component mapping', async () => {
const { componentMappings } = await import('~/components/Wrapper/component-registry');
componentMappings.forEach((mapping) => {
expect(mapping).toHaveProperty('selector');
expect(mapping).toHaveProperty('appId');
expect(mapping).toHaveProperty('component');
// Check selector is string or array
expect(typeof mapping.selector === 'string' || Array.isArray(mapping.selector)).toBe(true);
// Check appId is string
expect(typeof mapping.appId).toBe('string');
// Check component exists and is an object
expect(mapping.component).toBeDefined();
expect(typeof mapping.component).toBe('object');
});
});
it('should have priority components listed first', async () => {
const { componentMappings } = await import('~/components/Wrapper/component-registry');
// Priority components should be first
expect(componentMappings[0].appId).toBe('header-os-version');
expect(componentMappings[1].appId).toBe('user-profile');
});
it('should support multiple selectors for modals', async () => {
const { componentMappings } = await import('~/components/Wrapper/component-registry');
const modalsMapping = componentMappings.find((m) => m.appId === 'modals');
expect(Array.isArray(modalsMapping?.selector)).toBe(true);
expect(modalsMapping?.selector).toContain('unraid-modals');
expect(modalsMapping?.selector).toContain('#modals');
expect(modalsMapping?.selector).toContain('modals-direct');
});
it('should support multiple selectors for api key components', async () => {
const { componentMappings } = await import('~/components/Wrapper/component-registry');
const apiKeyMapping = componentMappings.find((m) => m.appId === 'apikey-page');
expect(Array.isArray(apiKeyMapping?.selector)).toBe(true);
expect(apiKeyMapping?.selector).toContain('unraid-apikey-page');
expect(apiKeyMapping?.selector).toContain('unraid-api-key-manager');
});
it('should support multiple selectors for toaster', async () => {
const { componentMappings } = await import('~/components/Wrapper/component-registry');
const toasterMapping = componentMappings.find((m) => m.appId === 'toaster');
expect(Array.isArray(toasterMapping?.selector)).toBe(true);
expect(toasterMapping?.selector).toContain('unraid-toaster');
expect(toasterMapping?.selector).toContain('uui-toaster');
});
it('should have unique appIds', async () => {
const { componentMappings } = await import('~/components/Wrapper/component-registry');
const appIds = componentMappings.map((m) => m.appId);
const uniqueAppIds = new Set(appIds);
expect(appIds.length).toBe(uniqueAppIds.size);
});
it('should define all components as async components', async () => {
const { componentMappings } = await import('~/components/Wrapper/component-registry');
componentMappings.forEach((mapping) => {
expect(mapping.component).toBeDefined();
expect(typeof mapping.component).toBe('object');
});
});
it('should have at least the core component mappings', async () => {
const { componentMappings } = await import('~/components/Wrapper/component-registry');
// Just ensure we have a reasonable number of components, not an exact count
expect(componentMappings.length).toBeGreaterThan(10);
});
it('should include all expected components', async () => {
const { componentMappings } = await import('~/components/Wrapper/component-registry');
const expectedAppIds = [
'header-os-version',
'user-profile',
'auth',
'connect-settings',
'download-api-logs',
'modals',
'registration',
'wan-ip-check',
'callback-handler',
'log-viewer',
'sso-button',
'welcome-modal',
'update-os',
'downgrade-os',
'dev-settings',
'apikey-page',
'apikey-authorize',
'dev-modal-test',
'detail-test',
'theme-switcher',
'color-switcher',
'toaster',
'test-update-modal',
'test-theme-switcher',
];
const actualAppIds = componentMappings.map((m) => m.appId);
expectedAppIds.forEach((appId) => {
expect(actualAppIds).toContain(appId);
});
});
it('should properly format selectors', async () => {
const { componentMappings } = await import('~/components/Wrapper/component-registry');
componentMappings.forEach((mapping) => {
if (typeof mapping.selector === 'string') {
// Single selectors should be non-empty strings
expect(mapping.selector.length).toBeGreaterThan(0);
} else if (Array.isArray(mapping.selector)) {
// Array selectors should have at least one item
expect(mapping.selector.length).toBeGreaterThan(0);
// Each selector in array should be non-empty string
mapping.selector.forEach((sel) => {
expect(typeof sel).toBe('string');
expect(sel.length).toBeGreaterThan(0);
});
}
});
});
});