fix: DockerSecretsResolver - don't normalize absolute paths like /var/www/html/...
Some checks failed
Deploy Application / deploy (push) Has been cancelled

This commit is contained in:
2025-11-24 21:28:25 +01:00
parent 4eb7134853
commit 77abc65cd7
1327 changed files with 91915 additions and 9909 deletions

View File

@@ -0,0 +1,448 @@
/**
* Tests for ActionHandler Module
*/
import { ActionHandler } from '../../resources/js/modules/common/ActionHandler.js';
import { dockerContainerHandler, genericApiHandler } from '../../resources/js/modules/common/ActionHandlers.js';
describe('ActionHandler', () => {
let container;
let handler;
let mockToast;
let mockConfirm;
let mockRefresh;
beforeEach(() => {
// Setup DOM
document.body.innerHTML = '';
container = document.createElement('div');
container.className = 'test-container';
document.body.appendChild(container);
// Mock functions
mockToast = jest.fn();
mockConfirm = jest.fn(() => true);
mockRefresh = jest.fn();
// Create handler
handler = new ActionHandler('.test-container', {
toastHandler: mockToast,
confirmationHandler: mockConfirm,
refreshHandler: mockRefresh,
autoRefresh: true
});
});
afterEach(() => {
document.body.innerHTML = '';
});
describe('Event Delegation', () => {
test('should handle click events on buttons with data-action', () => {
const button = document.createElement('button');
button.setAttribute('data-action', 'test');
button.setAttribute('data-action-url', '/api/test');
container.appendChild(button);
// Mock fetch
global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve({ success: true })
})
);
button.click();
expect(global.fetch).toHaveBeenCalled();
});
test('should not handle clicks outside buttons', () => {
const div = document.createElement('div');
container.appendChild(div);
global.fetch = jest.fn();
div.click();
expect(global.fetch).not.toHaveBeenCalled();
});
});
describe('Handler Registration', () => {
test('should register handler', () => {
handler.registerHandler('test-handler', {
urlTemplate: '/api/test/{id}/{action}'
});
expect(handler.handlers.has('test-handler')).toBe(true);
});
test('should use registered handler for actions', () => {
handler.registerHandler('docker-container', dockerContainerHandler);
const button = document.createElement('button');
button.setAttribute('data-action', 'start');
button.setAttribute('data-action-handler', 'docker-container');
button.setAttribute('data-action-param-id', '123');
container.appendChild(button);
global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve({ success: true })
})
);
button.click();
expect(global.fetch).toHaveBeenCalledWith(
expect.stringContaining('/admin/infrastructure/docker/api/containers/123/start'),
expect.any(Object)
);
});
});
describe('URL Template Processing', () => {
test('should replace {action} placeholder', () => {
handler.registerHandler('test', {
urlTemplate: '/api/{action}'
});
const button = document.createElement('button');
button.setAttribute('data-action', 'delete');
button.setAttribute('data-action-handler', 'test');
container.appendChild(button);
global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve({ success: true })
})
);
button.click();
expect(global.fetch).toHaveBeenCalledWith(
expect.stringContaining('/api/delete'),
expect.any(Object)
);
});
test('should replace {id} placeholder', () => {
handler.registerHandler('test', {
urlTemplate: '/api/users/{id}/{action}'
});
const button = document.createElement('button');
button.setAttribute('data-action', 'delete');
button.setAttribute('data-action-handler', 'test');
button.setAttribute('data-action-param-id', '123');
container.appendChild(button);
global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve({ success: true })
})
);
button.click();
expect(global.fetch).toHaveBeenCalledWith(
expect.stringContaining('/api/users/123/delete'),
expect.any(Object)
);
});
test('should replace {param:name} placeholders', () => {
handler.registerHandler('test', {
urlTemplate: '/api/{param:entity}/{param:id}/{action}'
});
const button = document.createElement('button');
button.setAttribute('data-action', 'update');
button.setAttribute('data-action-handler', 'test');
button.setAttribute('data-action-param-entity', 'users');
button.setAttribute('data-action-param-id', '123');
container.appendChild(button);
global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve({ success: true })
})
);
button.click();
expect(global.fetch).toHaveBeenCalledWith(
expect.stringContaining('/api/users/123/update'),
expect.any(Object)
);
});
});
describe('CSRF Token Handling', () => {
test('should extract token from data-live-component', () => {
const component = document.createElement('div');
component.setAttribute('data-live-component', 'test-component');
component.setAttribute('data-csrf-token', 'test-token');
document.body.appendChild(component);
container.appendChild(component);
const button = document.createElement('button');
button.setAttribute('data-action', 'test');
button.setAttribute('data-action-url', '/api/test');
container.appendChild(button);
global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve({ success: true })
})
);
button.click();
expect(global.fetch).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({
headers: expect.objectContaining({
'X-CSRF-Token': 'test-token'
}),
body: expect.stringContaining('"token":"test-token"')
})
);
});
test('should extract token from meta tag', () => {
const meta = document.createElement('meta');
meta.setAttribute('name', 'csrf-token');
meta.setAttribute('content', 'meta-token');
document.head.appendChild(meta);
const button = document.createElement('button');
button.setAttribute('data-action', 'test');
button.setAttribute('data-action-url', '/api/test');
container.appendChild(button);
global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve({ success: true })
})
);
button.click();
expect(global.fetch).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({
headers: expect.objectContaining({
'X-CSRF-Token': 'meta-token'
})
})
);
document.head.removeChild(meta);
});
});
describe('Loading States', () => {
test('should set loading state on button', async () => {
const button = document.createElement('button');
button.textContent = 'Click Me';
button.setAttribute('data-action', 'test');
button.setAttribute('data-action-url', '/api/test');
container.appendChild(button);
global.fetch = jest.fn(() =>
new Promise(resolve => setTimeout(() => {
resolve({
json: () => Promise.resolve({ success: true })
});
}, 100))
);
button.click();
expect(button.disabled).toBe(true);
expect(button.classList.contains('action-loading')).toBe(true);
});
test('should reset loading state after request', async () => {
const button = document.createElement('button');
button.textContent = 'Click Me';
button.setAttribute('data-action', 'test');
button.setAttribute('data-action-url', '/api/test');
container.appendChild(button);
global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve({ success: true })
})
);
await button.click();
// Wait for async operations
await new Promise(resolve => setTimeout(resolve, 50));
expect(button.disabled).toBe(false);
expect(button.classList.contains('action-loading')).toBe(false);
});
});
describe('Confirmations', () => {
test('should show confirmation before action', () => {
handler.registerHandler('test', {
urlTemplate: '/api/{action}',
confirmations: {
delete: 'Are you sure?'
}
});
const button = document.createElement('button');
button.setAttribute('data-action', 'delete');
button.setAttribute('data-action-handler', 'test');
container.appendChild(button);
global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve({ success: true })
})
);
button.click();
expect(mockConfirm).toHaveBeenCalledWith('Are you sure?');
});
test('should not execute action if confirmation cancelled', () => {
mockConfirm.mockReturnValue(false);
handler.registerHandler('test', {
urlTemplate: '/api/{action}',
confirmations: {
delete: 'Are you sure?'
}
});
const button = document.createElement('button');
button.setAttribute('data-action', 'delete');
button.setAttribute('data-action-handler', 'test');
container.appendChild(button);
global.fetch = jest.fn();
button.click();
expect(global.fetch).not.toHaveBeenCalled();
});
});
describe('Toast Integration', () => {
test('should show success toast on success', async () => {
const button = document.createElement('button');
button.setAttribute('data-action', 'test');
button.setAttribute('data-action-url', '/api/test');
container.appendChild(button);
global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve({ success: true })
})
);
await button.click();
await new Promise(resolve => setTimeout(resolve, 50));
expect(mockToast).toHaveBeenCalledWith(
expect.stringContaining('successfully'),
'success'
);
});
test('should show error toast on error', async () => {
handler.registerHandler('test', {
urlTemplate: '/api/{action}',
errorMessages: {
test: 'Test failed'
}
});
const button = document.createElement('button');
button.setAttribute('data-action', 'test');
button.setAttribute('data-action-handler', 'test');
container.appendChild(button);
global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve({ success: false, message: 'Error' })
})
);
await button.click();
await new Promise(resolve => setTimeout(resolve, 50));
expect(mockToast).toHaveBeenCalledWith(
expect.stringContaining('Test failed'),
'error'
);
});
});
describe('Auto Refresh', () => {
test('should refresh after successful action', async () => {
const button = document.createElement('button');
button.setAttribute('data-action', 'test');
button.setAttribute('data-action-url', '/api/test');
container.appendChild(button);
global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve({ success: true })
})
);
await button.click();
await new Promise(resolve => setTimeout(resolve, 50));
expect(mockRefresh).toHaveBeenCalled();
});
test('should not refresh on error', async () => {
const button = document.createElement('button');
button.setAttribute('data-action', 'test');
button.setAttribute('data-action-url', '/api/test');
container.appendChild(button);
global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve({ success: false })
})
);
await button.click();
await new Promise(resolve => setTimeout(resolve, 50));
expect(mockRefresh).not.toHaveBeenCalled();
});
});
describe('Error Handling', () => {
test('should handle network errors', async () => {
const button = document.createElement('button');
button.setAttribute('data-action', 'test');
button.setAttribute('data-action-url', '/api/test');
container.appendChild(button);
global.fetch = jest.fn(() =>
Promise.reject(new Error('Network error'))
);
await button.click();
await new Promise(resolve => setTimeout(resolve, 50));
expect(mockToast).toHaveBeenCalledWith(
expect.stringContaining('Network error'),
'error'
);
});
});
});

View File

@@ -0,0 +1,254 @@
/**
* Tests for DrawerManager
*/
import { DrawerManager } from '../../resources/js/modules/livecomponent/DrawerManager.js';
describe('DrawerManager', () => {
let manager;
let container;
beforeEach(() => {
// Create container for testing
container = document.createElement('div');
document.body.appendChild(container);
manager = new DrawerManager();
});
afterEach(() => {
manager.destroy();
if (container && container.parentNode) {
container.parentNode.removeChild(container);
}
// Clean up any remaining drawers
document.querySelectorAll('.drawer').forEach(el => el.remove());
document.querySelectorAll('.drawer-overlay').forEach(el => el.remove());
});
describe('open', () => {
it('should create and show drawer', (done) => {
const drawer = manager.open('test-drawer', {
title: 'Test Drawer',
content: '<p>Test content</p>',
position: 'left',
width: '400px'
});
expect(drawer).toBeDefined();
expect(drawer.drawer).toBeDefined();
const drawerElement = document.querySelector('.drawer');
expect(drawerElement).toBeTruthy();
expect(drawerElement.classList.contains('drawer--left')).toBe(true);
// Wait for animation frame
requestAnimationFrame(() => {
expect(drawer.isOpen()).toBe(true);
done();
});
});
it('should create overlay when showOverlay is true', () => {
manager.open('test-drawer', {
showOverlay: true
});
const overlay = document.querySelector('.drawer-overlay');
expect(overlay).toBeTruthy();
});
it('should not create overlay when showOverlay is false', () => {
manager.open('test-drawer', {
showOverlay: false
});
const overlay = document.querySelector('.drawer-overlay');
expect(overlay).toBeFalsy();
});
it('should add drawer to stack', () => {
manager.open('drawer-1', {});
manager.open('drawer-2', {});
expect(manager.drawerStack.length).toBe(2);
expect(manager.isOpen('drawer-1')).toBe(true);
expect(manager.isOpen('drawer-2')).toBe(true);
});
it('should set correct z-index for stacked drawers', () => {
const drawer1 = manager.open('drawer-1', {});
const drawer2 = manager.open('drawer-2', {});
const zIndex1 = parseInt(drawer1.drawer.style.zIndex);
const zIndex2 = parseInt(drawer2.drawer.style.zIndex);
expect(zIndex2).toBeGreaterThan(zIndex1);
});
});
describe('close', () => {
it('should close drawer', () => {
const drawer = manager.open('test-drawer', {});
expect(manager.isOpen('test-drawer')).toBe(true);
manager.close('test-drawer');
// Wait for animation
setTimeout(() => {
expect(manager.isOpen('test-drawer')).toBe(false);
}, 350);
});
it('should remove drawer from stack', () => {
manager.open('drawer-1', {});
manager.open('drawer-2', {});
manager.close('drawer-1');
expect(manager.drawerStack.length).toBe(1);
expect(manager.isOpen('drawer-1')).toBe(false);
expect(manager.isOpen('drawer-2')).toBe(true);
});
it('should handle closing non-existent drawer gracefully', () => {
expect(() => {
manager.close('non-existent');
}).not.toThrow();
});
});
describe('closeAll', () => {
it('should close all drawers', () => {
manager.open('drawer-1', {});
manager.open('drawer-2', {});
manager.open('drawer-3', {});
expect(manager.drawerStack.length).toBe(3);
manager.closeAll();
expect(manager.drawerStack.length).toBe(0);
});
});
describe('isOpen', () => {
it('should return true for open drawer', () => {
manager.open('test-drawer', {});
expect(manager.isOpen('test-drawer')).toBe(true);
});
it('should return false for closed drawer', () => {
expect(manager.isOpen('test-drawer')).toBe(false);
});
});
describe('getTopDrawer', () => {
it('should return topmost drawer', () => {
manager.open('drawer-1', {});
manager.open('drawer-2', {});
const topDrawer = manager.getTopDrawer();
expect(topDrawer).toBeDefined();
expect(topDrawer.componentId).toBe('drawer-2');
});
it('should return null when no drawers are open', () => {
expect(manager.getTopDrawer()).toBeNull();
});
});
describe('ESC key handling', () => {
it('should close drawer on ESC when closeOnEscape is true', () => {
manager.open('test-drawer', {
closeOnEscape: true
});
const escapeEvent = new KeyboardEvent('keydown', {
key: 'Escape',
bubbles: true
});
document.dispatchEvent(escapeEvent);
setTimeout(() => {
expect(manager.isOpen('test-drawer')).toBe(false);
}, 100);
});
it('should not close drawer on ESC when closeOnEscape is false', () => {
manager.open('test-drawer', {
closeOnEscape: false
});
const escapeEvent = new KeyboardEvent('keydown', {
key: 'Escape',
bubbles: true
});
document.dispatchEvent(escapeEvent);
expect(manager.isOpen('test-drawer')).toBe(true);
});
it('should only close topmost drawer on ESC', () => {
manager.open('drawer-1', { closeOnEscape: true });
manager.open('drawer-2', { closeOnEscape: true });
const escapeEvent = new KeyboardEvent('keydown', {
key: 'Escape',
bubbles: true
});
document.dispatchEvent(escapeEvent);
setTimeout(() => {
expect(manager.isOpen('drawer-2')).toBe(false);
expect(manager.isOpen('drawer-1')).toBe(true);
}, 100);
});
});
describe('overlay click handling', () => {
it('should close drawer on overlay click when closeOnOverlay is true', () => {
manager.open('test-drawer', {
showOverlay: true,
closeOnOverlay: true
});
const overlay = document.querySelector('.drawer-overlay');
overlay.click();
setTimeout(() => {
expect(manager.isOpen('test-drawer')).toBe(false);
}, 100);
});
});
describe('focus management', () => {
it('should focus drawer when opened', () => {
const drawer = manager.open('test-drawer', {
content: '<button id="test-btn">Test</button>'
});
// Wait for focus
setTimeout(() => {
const focusedElement = document.activeElement;
expect(focusedElement.closest('.drawer')).toBeTruthy();
}, 50);
});
});
describe('destroy', () => {
it('should cleanup all drawers and handlers', () => {
manager.open('drawer-1', {});
manager.open('drawer-2', {});
manager.destroy();
expect(manager.drawerStack.length).toBe(0);
expect(manager.escapeHandler).toBeNull();
});
});
});

View File

@@ -0,0 +1,255 @@
/**
* Tests for UIEventHandler
*/
import { UIEventHandler } from '../../resources/js/modules/livecomponent/UIEventHandler.js';
import { LiveComponentUIHelper } from '../../resources/js/modules/livecomponent/LiveComponentUIHelper.js';
describe('UIEventHandler', () => {
let uiEventHandler;
let mockManager;
let mockUIHelper;
beforeEach(() => {
// Create mock LiveComponentManager
mockManager = {
uiHelper: null
};
// Create mock UIHelper
mockUIHelper = {
showNotification: jest.fn(),
hideNotification: jest.fn(),
showDialog: jest.fn(),
closeDialog: jest.fn(),
showConfirm: jest.fn().mockResolvedValue(true),
showAlert: jest.fn()
};
mockManager.uiHelper = mockUIHelper;
// Create UIEventHandler instance
uiEventHandler = new UIEventHandler(mockManager);
});
afterEach(() => {
// Cleanup event listeners
if (uiEventHandler) {
uiEventHandler.destroy();
}
});
describe('Initialization', () => {
test('should initialize event listeners', () => {
expect(uiEventHandler.isInitialized).toBe(false);
uiEventHandler.init();
expect(uiEventHandler.isInitialized).toBe(true);
});
test('should not initialize twice', () => {
uiEventHandler.init();
const listenerCount = uiEventHandler.eventListeners.size;
uiEventHandler.init();
expect(uiEventHandler.eventListeners.size).toBe(listenerCount);
});
});
describe('Toast Events', () => {
beforeEach(() => {
uiEventHandler.init();
});
test('should handle toast:show event', () => {
const event = new CustomEvent('toast:show', {
detail: {
message: 'Test message',
type: 'success',
duration: 5000,
position: 'top-right',
componentId: 'test-component'
}
});
document.dispatchEvent(event);
expect(mockUIHelper.showNotification).toHaveBeenCalledWith('test-component', {
message: 'Test message',
type: 'success',
duration: 5000,
position: 'top-right'
});
});
test('should handle toast:show with defaults', () => {
const event = new CustomEvent('toast:show', {
detail: {
message: 'Test message'
}
});
document.dispatchEvent(event);
expect(mockUIHelper.showNotification).toHaveBeenCalledWith('global', {
message: 'Test message',
type: 'info',
duration: 5000,
position: 'top-right'
});
});
test('should handle toast:hide event', () => {
const event = new CustomEvent('toast:hide', {
detail: {
componentId: 'test-component'
}
});
document.dispatchEvent(event);
expect(mockUIHelper.hideNotification).toHaveBeenCalledWith('test-component');
});
test('should handle livecomponent:toast:show event', () => {
const event = new CustomEvent('livecomponent:toast:show', {
detail: {
message: 'Test message',
type: 'info'
}
});
document.dispatchEvent(event);
expect(mockUIHelper.showNotification).toHaveBeenCalled();
});
});
describe('Modal Events', () => {
beforeEach(() => {
uiEventHandler.init();
});
test('should handle modal:show event', () => {
const event = new CustomEvent('modal:show', {
detail: {
componentId: 'test-component',
title: 'Test Modal',
content: '<p>Test content</p>',
size: 'medium',
buttons: []
}
});
document.dispatchEvent(event);
expect(mockUIHelper.showDialog).toHaveBeenCalledWith('test-component', {
title: 'Test Modal',
content: '<p>Test content</p>',
size: 'medium',
buttons: [],
closeOnBackdrop: true,
closeOnEscape: true,
onClose: null,
onConfirm: null
});
});
test('should handle modal:close event', () => {
const event = new CustomEvent('modal:close', {
detail: {
componentId: 'test-component'
}
});
document.dispatchEvent(event);
expect(mockUIHelper.closeDialog).toHaveBeenCalledWith('test-component');
});
test('should handle modal:confirm event', async () => {
const event = new CustomEvent('modal:confirm', {
detail: {
componentId: 'test-component',
title: 'Confirm',
message: 'Are you sure?',
confirmText: 'Yes',
cancelText: 'No'
}
});
document.dispatchEvent(event);
// Wait for promise to resolve
await new Promise(resolve => setTimeout(resolve, 100));
expect(mockUIHelper.showConfirm).toHaveBeenCalledWith('test-component', {
title: 'Confirm',
message: 'Are you sure?',
confirmText: 'Yes',
cancelText: 'No',
confirmClass: 'btn-primary',
cancelClass: 'btn-secondary'
});
});
test('should handle modal:alert event', () => {
const event = new CustomEvent('modal:alert', {
detail: {
componentId: 'test-component',
title: 'Alert',
message: 'Alert message',
type: 'info',
buttonText: 'OK'
}
});
document.dispatchEvent(event);
expect(mockUIHelper.showAlert).toHaveBeenCalledWith('test-component', {
title: 'Alert',
message: 'Alert message',
buttonText: 'OK',
type: 'info'
});
});
});
describe('Error Handling', () => {
beforeEach(() => {
uiEventHandler.init();
});
test('should handle errors gracefully', () => {
// Mock showNotification to throw error
mockUIHelper.showNotification = jest.fn(() => {
throw new Error('Test error');
});
const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
const event = new CustomEvent('toast:show', {
detail: {
message: 'Test message'
}
});
document.dispatchEvent(event);
expect(consoleSpy).toHaveBeenCalled();
consoleSpy.mockRestore();
});
});
describe('Cleanup', () => {
test('should cleanup event listeners on destroy', () => {
uiEventHandler.init();
const listenerCount = uiEventHandler.eventListeners.size;
expect(listenerCount).toBeGreaterThan(0);
uiEventHandler.destroy();
expect(uiEventHandler.eventListeners.size).toBe(0);
expect(uiEventHandler.isInitialized).toBe(false);
});
});
});

View File

@@ -0,0 +1,264 @@
/**
* Tests for PopoverManager
*/
import { PopoverManager } from '../../resources/js/modules/livecomponent/PopoverManager.js';
describe('PopoverManager', () => {
let manager;
let anchor;
beforeEach(() => {
// Create anchor element for testing
anchor = document.createElement('button');
anchor.id = 'test-anchor';
anchor.textContent = 'Anchor';
document.body.appendChild(anchor);
manager = new PopoverManager();
});
afterEach(() => {
if (manager) {
manager.destroy();
}
if (anchor && anchor.parentNode) {
anchor.parentNode.removeChild(anchor);
}
// Clean up any remaining popovers
document.querySelectorAll('.livecomponent-popover').forEach(el => el.remove());
});
describe('show', () => {
it('should create and show popover', () => {
const popover = manager.show('test-popover', {
anchorId: 'test-anchor',
content: 'Test content',
position: 'top'
});
expect(popover).toBeDefined();
expect(popover.element).toBeDefined();
const popoverElement = document.querySelector('.livecomponent-popover');
expect(popoverElement).toBeTruthy();
});
it('should return null when anchorId is missing', () => {
const popover = manager.show('test-popover', {
content: 'Test content'
});
expect(popover).toBeNull();
});
it('should return null when anchor element not found', () => {
const popover = manager.show('test-popover', {
anchorId: 'non-existent',
content: 'Test content'
});
expect(popover).toBeNull();
});
it('should position popover relative to anchor', () => {
anchor.style.position = 'absolute';
anchor.style.top = '100px';
anchor.style.left = '200px';
anchor.style.width = '100px';
anchor.style.height = '50px';
const popover = manager.show('test-popover', {
anchorId: 'test-anchor',
content: 'Test',
position: 'top',
offset: 10
});
expect(popover).toBeDefined();
const popoverElement = popover.element;
// Check that popover is positioned
expect(popoverElement.style.position).toBe('fixed');
expect(popoverElement.style.top).toBeTruthy();
expect(popoverElement.style.left).toBeTruthy();
});
it('should include title when provided', () => {
manager.show('test-popover', {
anchorId: 'test-anchor',
title: 'Test Title',
content: 'Test content'
});
const titleElement = document.querySelector('.popover-title');
expect(titleElement).toBeTruthy();
expect(titleElement.textContent).toBe('Test Title');
});
it('should include arrow when showArrow is true', () => {
manager.show('test-popover', {
anchorId: 'test-anchor',
showArrow: true,
content: 'Test'
});
const arrow = document.querySelector('.popover-arrow');
expect(arrow).toBeTruthy();
});
it('should not include arrow when showArrow is false', () => {
manager.show('test-popover', {
anchorId: 'test-anchor',
showArrow: false,
content: 'Test'
});
const arrow = document.querySelector('.popover-arrow');
expect(arrow).toBeFalsy();
});
});
describe('hide', () => {
it('should hide popover', () => {
const popover = manager.show('test-popover', {
anchorId: 'test-anchor',
content: 'Test'
});
expect(manager.popovers.has('test-popover')).toBe(true);
manager.hide('test-popover');
expect(manager.popovers.has('test-popover')).toBe(false);
});
it('should handle hiding non-existent popover gracefully', () => {
expect(() => {
manager.hide('non-existent');
}).not.toThrow();
});
});
describe('position calculation', () => {
beforeEach(() => {
anchor.style.position = 'absolute';
anchor.style.top = '100px';
anchor.style.left = '200px';
anchor.style.width = '100px';
anchor.style.height = '50px';
});
it('should position popover on top', () => {
const popover = manager.show('test-popover', {
anchorId: 'test-anchor',
position: 'top',
offset: 10
});
const rect = popover.element.getBoundingClientRect();
const anchorRect = anchor.getBoundingClientRect();
expect(rect.bottom).toBeLessThan(anchorRect.top);
});
it('should position popover on bottom', () => {
const popover = manager.show('test-popover', {
anchorId: 'test-anchor',
position: 'bottom',
offset: 10
});
const rect = popover.element.getBoundingClientRect();
const anchorRect = anchor.getBoundingClientRect();
expect(rect.top).toBeGreaterThan(anchorRect.bottom);
});
it('should position popover on left', () => {
const popover = manager.show('test-popover', {
anchorId: 'test-anchor',
position: 'left',
offset: 10
});
const rect = popover.element.getBoundingClientRect();
const anchorRect = anchor.getBoundingClientRect();
expect(rect.right).toBeLessThan(anchorRect.left);
});
it('should position popover on right', () => {
const popover = manager.show('test-popover', {
anchorId: 'test-anchor',
position: 'right',
offset: 10
});
const rect = popover.element.getBoundingClientRect();
const anchorRect = anchor.getBoundingClientRect();
expect(rect.left).toBeGreaterThan(anchorRect.right);
});
});
describe('auto positioning', () => {
it('should choose best position when position is auto', () => {
anchor.style.position = 'absolute';
anchor.style.top = '50px';
anchor.style.left = '50px';
const popover = manager.show('test-popover', {
anchorId: 'test-anchor',
position: 'auto',
offset: 10
});
expect(popover).toBeDefined();
// Should position successfully
expect(popover.element.style.position).toBe('fixed');
});
});
describe('viewport boundary detection', () => {
it('should keep popover within viewport', () => {
// Position anchor near top-left corner
anchor.style.position = 'absolute';
anchor.style.top = '10px';
anchor.style.left = '10px';
const popover = manager.show('test-popover', {
anchorId: 'test-anchor',
position: 'top',
offset: 100 // Large offset that would push outside viewport
});
const rect = popover.element.getBoundingClientRect();
expect(rect.top).toBeGreaterThanOrEqual(0);
expect(rect.left).toBeGreaterThanOrEqual(0);
});
});
describe('destroy', () => {
it('should cleanup all popovers', () => {
manager.show('popover-1', { anchorId: 'test-anchor', content: 'Test 1' });
manager.show('popover-2', { anchorId: 'test-anchor', content: 'Test 2' });
expect(manager.popovers.size).toBeGreaterThan(0);
manager.destroy();
expect(manager.popovers.size).toBe(0);
});
});
describe('Popover API detection', () => {
it('should detect Popover API support', () => {
// This test checks if the manager correctly detects API support
// Actual behavior depends on browser environment
expect(manager.usePopoverAPI).toBeDefined();
expect(typeof manager.usePopoverAPI).toBe('boolean');
});
});
});