feat(Production): Complete production deployment infrastructure

- Add comprehensive health check system with multiple endpoints
- Add Prometheus metrics endpoint
- Add production logging configurations (5 strategies)
- Add complete deployment documentation suite:
  * QUICKSTART.md - 30-minute deployment guide
  * DEPLOYMENT_CHECKLIST.md - Printable verification checklist
  * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle
  * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference
  * production-logging.md - Logging configuration guide
  * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation
  * README.md - Navigation hub
  * DEPLOYMENT_SUMMARY.md - Executive summary
- Add deployment scripts and automation
- Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment
- Update README with production-ready features

All production infrastructure is now complete and ready for deployment.
This commit is contained in:
2025-10-25 19:18:37 +02:00
parent caa85db796
commit fc3d7e6357
83016 changed files with 378904 additions and 20919 deletions

View File

@@ -0,0 +1,406 @@
/**
* Accessibility Manager Tests
*
* Tests for ARIA live regions, focus management, and screen reader support.
*/
import { AccessibilityManager } from '../../../../resources/js/modules/livecomponent/AccessibilityManager.js';
describe('AccessibilityManager', () => {
let manager;
let container;
beforeEach(() => {
manager = new AccessibilityManager();
container = document.createElement('div');
container.dataset.liveComponent = 'test-component:1';
document.body.appendChild(container);
});
afterEach(() => {
document.body.innerHTML = '';
});
describe('initialize', () => {
it('creates global live region', () => {
manager.initialize();
expect(manager.liveRegion).not.toBeNull();
expect(manager.liveRegion.id).toBe('livecomponent-announcer');
expect(manager.liveRegion.getAttribute('role')).toBe('status');
expect(manager.liveRegion.getAttribute('aria-live')).toBe('polite');
});
it('applies screen reader only styles', () => {
manager.initialize();
const region = manager.liveRegion;
expect(region.style.position).toBe('absolute');
expect(region.style.left).toBe('-10000px');
expect(region.style.width).toBe('1px');
expect(region.style.height).toBe('1px');
});
});
describe('createComponentLiveRegion', () => {
beforeEach(() => {
manager.initialize();
});
it('creates component-specific live region', () => {
const region = manager.createComponentLiveRegion('test:1', container);
expect(region).not.toBeNull();
expect(region.id).toBe('livecomponent-test:1-announcer');
expect(region.getAttribute('aria-live')).toBe('polite');
expect(container.contains(region)).toBe(true);
});
it('reuses existing component live region', () => {
const region1 = manager.createComponentLiveRegion('test:1', container);
const region2 = manager.createComponentLiveRegion('test:1', container);
expect(region1).toBe(region2);
expect(container.querySelectorAll('[role="status"]')).toHaveLength(1);
});
it('supports assertive politeness level', () => {
const region = manager.createComponentLiveRegion('test:1', container, 'assertive');
expect(region.getAttribute('aria-live')).toBe('assertive');
});
});
describe('announce', () => {
beforeEach(() => {
manager.initialize();
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
});
it('queues announcement', () => {
manager.announce('Test message');
expect(manager.announcementQueue).toHaveLength(1);
expect(manager.announcementQueue[0].message).toBe('Test message');
});
it('throttles announcements', () => {
manager.announce('Message 1');
manager.announce('Message 2');
manager.announce('Message 3');
// Should only keep most recent
jest.advanceTimersByTime(500);
expect(manager.liveRegion.textContent).toBe('Message 3');
expect(manager.announcementQueue).toHaveLength(0);
});
it('uses component-specific live region when provided', () => {
manager.createComponentLiveRegion('test:1', container);
manager.announce('Component message', 'polite', 'test:1');
jest.advanceTimersByTime(600);
const componentRegion = manager.componentLiveRegions.get('test:1');
expect(componentRegion.textContent).toBe('Component message');
});
it('updates politeness for assertive announcements', () => {
manager.announce('Urgent message', 'assertive');
jest.advanceTimersByTime(500);
expect(manager.liveRegion.getAttribute('aria-live')).toBe('assertive');
});
});
describe('captureFocusState', () => {
it('captures focus state for focused element', () => {
const input = document.createElement('input');
input.type = 'text';
input.id = 'test-input';
input.value = 'test value';
container.appendChild(input);
input.focus();
input.selectionStart = 2;
input.selectionEnd = 5;
const focusState = manager.captureFocusState('test:1', container);
expect(focusState).not.toBeNull();
expect(focusState.id).toBe('test-input');
expect(focusState.selectionStart).toBe(2);
expect(focusState.selectionEnd).toBe(5);
});
it('captures data-lc-keep-focus attribute', () => {
const button = document.createElement('button');
button.setAttribute('data-lc-keep-focus', '');
container.appendChild(button);
button.focus();
const focusState = manager.captureFocusState('test:1', container);
expect(focusState.keepFocus).toBe(true);
});
it('returns null when focus is outside container', () => {
const externalInput = document.createElement('input');
document.body.appendChild(externalInput);
externalInput.focus();
const focusState = manager.captureFocusState('test:1', container);
expect(focusState).toBeNull();
});
it('captures scroll position', () => {
const textarea = document.createElement('textarea');
textarea.style.height = '50px';
textarea.style.overflow = 'scroll';
textarea.value = 'Line 1\n'.repeat(20);
container.appendChild(textarea);
textarea.focus();
textarea.scrollTop = 100;
const focusState = manager.captureFocusState('test:1', container);
expect(focusState.scrollTop).toBe(100);
});
});
describe('restoreFocus', () => {
it('restores focus to element with data-lc-keep-focus', () => {
const input1 = document.createElement('input');
input1.id = 'input1';
container.appendChild(input1);
const input2 = document.createElement('input');
input2.id = 'input2';
input2.setAttribute('data-lc-keep-focus', '');
container.appendChild(input2);
// Capture focus on input1
input1.focus();
manager.captureFocusState('test:1', container);
// Restore should prefer data-lc-keep-focus
const restored = manager.restoreFocus('test:1', container);
expect(restored).toBe(true);
expect(document.activeElement).toBe(input2);
});
it('restores focus by ID', () => {
const input = document.createElement('input');
input.id = 'test-input';
container.appendChild(input);
input.focus();
manager.captureFocusState('test:1', container);
// Blur and restore
input.blur();
const restored = manager.restoreFocus('test:1', container);
expect(restored).toBe(true);
expect(document.activeElement).toBe(input);
});
it('restores selection for input elements', () => {
const input = document.createElement('input');
input.type = 'text';
input.id = 'test-input';
input.value = 'test value';
container.appendChild(input);
input.focus();
input.selectionStart = 2;
input.selectionEnd = 5;
manager.captureFocusState('test:1', container);
// Change selection
input.selectionStart = 0;
input.selectionEnd = 0;
manager.restoreFocus('test:1', container);
expect(input.selectionStart).toBe(2);
expect(input.selectionEnd).toBe(5);
});
it('restores scroll position', () => {
const textarea = document.createElement('textarea');
textarea.id = 'test-textarea';
textarea.style.height = '50px';
textarea.style.overflow = 'scroll';
textarea.value = 'Line 1\n'.repeat(20);
container.appendChild(textarea);
textarea.focus();
textarea.scrollTop = 100;
manager.captureFocusState('test:1', container);
textarea.scrollTop = 0;
manager.restoreFocus('test:1', container);
expect(textarea.scrollTop).toBe(100);
});
it('returns false when no focus state captured', () => {
const restored = manager.restoreFocus('test:1', container);
expect(restored).toBe(false);
});
it('clears focus state after restore', () => {
const input = document.createElement('input');
input.id = 'test-input';
container.appendChild(input);
input.focus();
manager.captureFocusState('test:1', container);
manager.restoreFocus('test:1', container);
// Second restore should fail (state cleared)
const restored2 = manager.restoreFocus('test:1', container);
expect(restored2).toBe(false);
});
});
describe('announceUpdate', () => {
beforeEach(() => {
manager.initialize();
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
});
it('announces fragment update', () => {
manager.announceUpdate('test:1', 'fragment', { fragmentName: 'header' });
jest.advanceTimersByTime(500);
expect(manager.liveRegion.textContent).toBe('Updated header');
});
it('announces full component update', () => {
manager.announceUpdate('test:1', 'full');
jest.advanceTimersByTime(500);
expect(manager.liveRegion.textContent).toBe('Component updated');
});
it('announces action completion', () => {
manager.announceUpdate('test:1', 'action', { actionMessage: 'Form submitted' });
jest.advanceTimersByTime(500);
expect(manager.liveRegion.textContent).toBe('Form submitted');
});
});
describe('cleanup', () => {
beforeEach(() => {
manager.initialize();
});
it('removes component live region', () => {
manager.createComponentLiveRegion('test:1', container);
const region = manager.componentLiveRegions.get('test:1');
expect(container.contains(region)).toBe(true);
manager.cleanup('test:1');
expect(container.contains(region)).toBe(false);
expect(manager.componentLiveRegions.has('test:1')).toBe(false);
});
it('clears focus state', () => {
const input = document.createElement('input');
input.id = 'test-input';
container.appendChild(input);
input.focus();
manager.captureFocusState('test:1', container);
manager.cleanup('test:1');
expect(manager.focusStates.has('test:1')).toBe(false);
});
});
describe('getStats', () => {
beforeEach(() => {
manager.initialize();
});
it('returns accurate statistics', () => {
manager.createComponentLiveRegion('test:1', container);
manager.createComponentLiveRegion('test:2', container);
const input = document.createElement('input');
container.appendChild(input);
input.focus();
manager.captureFocusState('test:1', container);
manager.announce('Test message');
const stats = manager.getStats();
expect(stats.has_global_live_region).toBe(true);
expect(stats.component_live_regions).toBe(2);
expect(stats.tracked_focus_states).toBe(1);
expect(stats.pending_announcements).toBe(1);
});
});
describe('shouldPreserveKeyboardNav', () => {
it('returns true for interactive elements', () => {
const button = document.createElement('button');
expect(manager.shouldPreserveKeyboardNav(button)).toBe(true);
const input = document.createElement('input');
expect(manager.shouldPreserveKeyboardNav(input)).toBe(true);
const link = document.createElement('a');
expect(manager.shouldPreserveKeyboardNav(link)).toBe(true);
});
it('returns true for elements with tabindex', () => {
const div = document.createElement('div');
div.setAttribute('tabindex', '0');
expect(manager.shouldPreserveKeyboardNav(div)).toBe(true);
});
it('returns true for elements with interactive roles', () => {
const div = document.createElement('div');
div.setAttribute('role', 'button');
expect(manager.shouldPreserveKeyboardNav(div)).toBe(true);
});
it('returns false for non-interactive elements', () => {
const div = document.createElement('div');
expect(manager.shouldPreserveKeyboardNav(div)).toBe(false);
const span = document.createElement('span');
expect(manager.shouldPreserveKeyboardNav(span)).toBe(false);
});
});
});