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