mirror of
https://github.com/unraid/api.git
synced 2026-05-01 20:54:27 -05:00
c855caa9b2
- 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>
185 lines
7.7 KiB
TypeScript
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);
|
|
});
|
|
}
|
|
});
|
|
});
|
|
});
|