fix: DockerSecretsResolver - don't normalize absolute paths like /var/www/html/...
Some checks failed
Deploy Application / deploy (push) Has been cancelled
Some checks failed
Deploy Application / deploy (push) Has been cancelled
This commit is contained in:
448
tests/JavaScript/ActionHandler.test.js
Normal file
448
tests/JavaScript/ActionHandler.test.js
Normal 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'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
254
tests/JavaScript/DrawerManager.test.js
Normal file
254
tests/JavaScript/DrawerManager.test.js
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
255
tests/JavaScript/LiveComponentUIEventHandler.test.js
Normal file
255
tests/JavaScript/LiveComponentUIEventHandler.test.js
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
264
tests/JavaScript/PopoverManager.test.js
Normal file
264
tests/JavaScript/PopoverManager.test.js
Normal 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user