Files
michaelschiemer/tests/JavaScript/ActionHandler.test.js
2025-11-24 21:28:25 +01:00

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'
);
});
});
});