Some checks failed
Deploy Application / deploy (push) Has been cancelled
449 lines
15 KiB
JavaScript
449 lines
15 KiB
JavaScript
/**
|
|
* 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'
|
|
);
|
|
});
|
|
});
|
|
});
|
|
|