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,467 @@
/**
* LiveComponents E2E Tests - Request Batching
*
* Tests for automatic request batching functionality:
* - Multiple actions batched into single HTTP request
* - Batch payload structure and response handling
* - Error handling for failed actions in batch
* - Batch size limits and automatic debouncing
* - Network efficiency validation
*
* @see src/Framework/LiveComponents/Services/BatchRequestHandler.php
* @see resources/js/modules/LiveComponent.js (batch handling)
*/
import { test, expect } from '@playwright/test';
test.describe('LiveComponents - Request Batching', () => {
test.beforeEach(async ({ page }) => {
// Navigate to test page with LiveComponent
await page.goto('/livecomponents/test');
// Wait for LiveComponent to be initialized
await page.waitForFunction(() => window.LiveComponent !== undefined);
});
test('should batch multiple rapid actions into single request', async ({ page }) => {
// Track network requests
const requests = [];
page.on('request', request => {
if (request.url().includes('/livecomponent')) {
requests.push(request);
}
});
// Trigger multiple actions rapidly (should be batched)
await page.click('[data-lc-action="increment"]');
await page.click('[data-lc-action="increment"]');
await page.click('[data-lc-action="increment"]');
// Wait for batch to be sent (debounced)
await page.waitForTimeout(100);
// Should only have 1 request (batched)
expect(requests.length).toBe(1);
// Verify batch request structure
const request = requests[0];
const postData = request.postData();
expect(postData).toBeTruthy();
const payload = JSON.parse(postData);
expect(payload).toHaveProperty('batch');
expect(payload.batch).toBeInstanceOf(Array);
expect(payload.batch.length).toBe(3);
});
test('should handle batch response with multiple action results', async ({ page }) => {
// Trigger batched actions
await page.click('[data-lc-action="increment"]');
await page.click('[data-lc-action="updateTitle"]');
await page.click('[data-lc-action="toggleFlag"]');
// Wait for batch response
const response = await page.waitForResponse(response =>
response.url().includes('/livecomponent') &&
response.status() === 200
);
const responseData = await response.json();
// Verify batch response structure
expect(responseData).toHaveProperty('batch_results');
expect(responseData.batch_results).toBeInstanceOf(Array);
expect(responseData.batch_results.length).toBe(3);
// Verify each result has required fields
responseData.batch_results.forEach(result => {
expect(result).toHaveProperty('success');
expect(result).toHaveProperty('html');
expect(result).toHaveProperty('action_index');
});
// Verify component state was updated correctly
const counter = await page.locator('.counter-value').textContent();
expect(parseInt(counter)).toBeGreaterThan(0);
});
test('should handle partial batch failure gracefully', async ({ page }) => {
// Trigger batch with one failing action
await page.click('[data-lc-action="increment"]');
await page.click('[data-lc-action="failingAction"]'); // This will fail
await page.click('[data-lc-action="updateTitle"]');
await page.waitForTimeout(100);
const response = await page.waitForResponse(response =>
response.url().includes('/livecomponent')
);
const responseData = await response.json();
// First action should succeed
expect(responseData.batch_results[0].success).toBe(true);
// Second action should fail
expect(responseData.batch_results[1].success).toBe(false);
expect(responseData.batch_results[1]).toHaveProperty('error');
// Third action should still execute
expect(responseData.batch_results[2].success).toBe(true);
// Verify error notification was shown
const errorNotification = page.locator('.livecomponent-error');
await expect(errorNotification).toBeVisible();
});
test('should respect batch size limits', async ({ page }) => {
const requests = [];
page.on('request', request => {
if (request.url().includes('/livecomponent')) {
requests.push(request);
}
});
// Trigger more actions than batch limit (default: 10)
for (let i = 0; i < 15; i++) {
await page.click('[data-lc-action="increment"]');
}
await page.waitForTimeout(200);
// Should have 2 requests (batch size limit: 10 + 5)
expect(requests.length).toBeGreaterThanOrEqual(2);
// First batch should have 10 actions
const firstBatchData = JSON.parse(requests[0].postData());
expect(firstBatchData.batch.length).toBeLessThanOrEqual(10);
// Second batch should have remaining actions
const secondBatchData = JSON.parse(requests[1].postData());
expect(secondBatchData.batch.length).toBeLessThanOrEqual(5);
});
test('should debounce rapid actions before batching', async ({ page }) => {
const requests = [];
page.on('request', request => {
if (request.url().includes('/livecomponent')) {
requests.push(request);
}
});
// Click rapidly within debounce window (default: 50ms)
await page.click('[data-lc-action="increment"]');
await page.waitForTimeout(10);
await page.click('[data-lc-action="increment"]');
await page.waitForTimeout(10);
await page.click('[data-lc-action="increment"]');
// Wait for debounce + network
await page.waitForTimeout(150);
// Should only have 1 batched request
expect(requests.length).toBe(1);
// Click after debounce window expires
await page.waitForTimeout(100);
await page.click('[data-lc-action="increment"]');
await page.waitForTimeout(150);
// Should have 2 requests total
expect(requests.length).toBe(2);
});
test('should send immediate request when batch is manually flushed', async ({ page }) => {
const requests = [];
page.on('request', request => {
if (request.url().includes('/livecomponent')) {
requests.push(request);
}
});
// Trigger action
await page.click('[data-lc-action="increment"]');
// Manually flush batch before debounce expires
await page.evaluate(() => {
window.LiveComponent.flushBatch();
});
// Wait for immediate request
await page.waitForTimeout(50);
// Should have sent request immediately
expect(requests.length).toBe(1);
});
test('should validate network efficiency with batching', async ({ page }) => {
// Count requests WITHOUT batching
await page.evaluate(() => {
window.LiveComponent.disableBatching();
});
const unbatchedRequests = [];
page.on('request', request => {
if (request.url().includes('/livecomponent')) {
unbatchedRequests.push(request);
}
});
// Trigger 5 actions
for (let i = 0; i < 5; i++) {
await page.click('[data-lc-action="increment"]');
await page.waitForTimeout(20);
}
await page.waitForTimeout(200);
const unbatchedCount = unbatchedRequests.length;
expect(unbatchedCount).toBe(5); // Should be 5 separate requests
// Reload and test WITH batching
await page.reload();
await page.waitForFunction(() => window.LiveComponent !== undefined);
const batchedRequests = [];
page.on('request', request => {
if (request.url().includes('/livecomponent')) {
batchedRequests.push(request);
}
});
// Trigger same 5 actions
for (let i = 0; i < 5; i++) {
await page.click('[data-lc-action="increment"]');
}
await page.waitForTimeout(200);
const batchedCount = batchedRequests.length;
expect(batchedCount).toBe(1); // Should be 1 batched request
// Network efficiency: 80% reduction in requests
const efficiency = ((unbatchedCount - batchedCount) / unbatchedCount) * 100;
expect(efficiency).toBeGreaterThanOrEqual(80);
});
test('should preserve action order in batch', async ({ page }) => {
// Trigger actions in specific order
await page.click('[data-lc-action="setValueA"]');
await page.click('[data-lc-action="setValueB"]');
await page.click('[data-lc-action="setValueC"]');
const response = await page.waitForResponse(response =>
response.url().includes('/livecomponent') &&
response.status() === 200
);
const responseData = await response.json();
// Verify action_index preserves order
expect(responseData.batch_results[0].action_index).toBe(0);
expect(responseData.batch_results[1].action_index).toBe(1);
expect(responseData.batch_results[2].action_index).toBe(2);
// Verify final state reflects correct order
const finalValue = await page.locator('.value-display').textContent();
expect(finalValue).toBe('C'); // Last action wins
});
test('should include component state in batch request', async ({ page }) => {
const requests = [];
page.on('request', request => {
if (request.url().includes('/livecomponent')) {
requests.push(request);
}
});
// Update state and trigger batch
await page.fill('[data-lc-model="searchTerm"]', 'test query');
await page.click('[data-lc-action="search"]');
await page.click('[data-lc-action="filter"]');
await page.waitForTimeout(100);
const request = requests[0];
const payload = JSON.parse(request.postData());
// Verify state is included
expect(payload).toHaveProperty('state');
expect(payload.state.searchTerm).toBe('test query');
// Verify batch actions
expect(payload.batch.length).toBe(2);
expect(payload.batch[0].action).toBe('search');
expect(payload.batch[1].action).toBe('filter');
});
test('should handle batch with different action parameters', async ({ page }) => {
// Trigger actions with different params
await page.evaluate(() => {
const componentId = document.querySelector('[data-component-id]').dataset.componentId;
window.LiveComponent.executeAction(componentId, 'updateField', { field: 'name', value: 'John' });
window.LiveComponent.executeAction(componentId, 'updateField', { field: 'email', value: 'john@example.com' });
window.LiveComponent.executeAction(componentId, 'updateField', { field: 'age', value: 30 });
});
const response = await page.waitForResponse(response =>
response.url().includes('/livecomponent') &&
response.status() === 200
);
const responseData = await response.json();
// Verify all 3 actions were batched
expect(responseData.batch_results.length).toBe(3);
// Verify each action maintained its parameters
expect(responseData.batch_results[0].success).toBe(true);
expect(responseData.batch_results[1].success).toBe(true);
expect(responseData.batch_results[2].success).toBe(true);
// Verify final component state
const nameField = await page.locator('[data-field="name"]').textContent();
const emailField = await page.locator('[data-field="email"]').textContent();
const ageField = await page.locator('[data-field="age"]').textContent();
expect(nameField).toBe('John');
expect(emailField).toBe('john@example.com');
expect(ageField).toBe('30');
});
test('should emit batch events for monitoring', async ({ page }) => {
let batchStartEvent = null;
let batchCompleteEvent = null;
// Listen for batch events
await page.exposeFunction('onBatchStart', (event) => {
batchStartEvent = event;
});
await page.exposeFunction('onBatchComplete', (event) => {
batchCompleteEvent = event;
});
await page.evaluate(() => {
window.addEventListener('livecomponent:batch-start', (e) => {
window.onBatchStart(e.detail);
});
window.addEventListener('livecomponent:batch-complete', (e) => {
window.onBatchComplete(e.detail);
});
});
// Trigger batch
await page.click('[data-lc-action="increment"]');
await page.click('[data-lc-action="increment"]');
await page.click('[data-lc-action="increment"]');
await page.waitForResponse(response =>
response.url().includes('/livecomponent') &&
response.status() === 200
);
// Wait for events
await page.waitForTimeout(100);
// Verify batch-start event
expect(batchStartEvent).not.toBeNull();
expect(batchStartEvent).toHaveProperty('actionCount');
expect(batchStartEvent.actionCount).toBe(3);
// Verify batch-complete event
expect(batchCompleteEvent).not.toBeNull();
expect(batchCompleteEvent).toHaveProperty('results');
expect(batchCompleteEvent.results.length).toBe(3);
expect(batchCompleteEvent).toHaveProperty('duration');
});
test('should handle concurrent batches correctly', async ({ page }) => {
const requests = [];
page.on('request', request => {
if (request.url().includes('/livecomponent')) {
requests.push(request);
}
});
// Trigger first batch
await page.click('[data-lc-action="increment"]');
await page.click('[data-lc-action="increment"]');
// Wait for partial debounce
await page.waitForTimeout(30);
// Trigger second batch (while first is still debouncing)
await page.click('[data-lc-action="updateTitle"]');
await page.click('[data-lc-action="updateTitle"]');
// Wait for all batches to complete
await page.waitForTimeout(200);
// Should have sent batches separately
expect(requests.length).toBeGreaterThanOrEqual(1);
// Verify final state is consistent
const counter = await page.locator('.counter-value').textContent();
expect(parseInt(counter)).toBe(2); // Both increments applied
});
test('should handle batch timeout gracefully', async ({ page }) => {
// Mock slow server response
await page.route('**/livecomponent/**', async route => {
await new Promise(resolve => setTimeout(resolve, 5000)); // 5 second delay
route.continue();
});
// Trigger batch
await page.click('[data-lc-action="increment"]');
// Should show loading state
const loadingIndicator = page.locator('.livecomponent-loading');
await expect(loadingIndicator).toBeVisible();
// Wait for timeout (should be less than 5 seconds)
await page.waitForTimeout(3000);
// Should show timeout error
const errorNotification = page.locator('.livecomponent-error');
await expect(errorNotification).toBeVisible();
await expect(errorNotification).toContainText('timeout');
});
test('should allow disabling batching per action', async ({ page }) => {
const requests = [];
page.on('request', request => {
if (request.url().includes('/livecomponent')) {
requests.push(request);
}
});
// Trigger batchable actions
await page.click('[data-lc-action="increment"]');
await page.click('[data-lc-action="increment"]');
// Trigger non-batchable action (critical/immediate)
await page.click('[data-lc-action="criticalAction"]');
await page.waitForTimeout(200);
// Should have 2 requests:
// 1. Immediate critical action
// 2. Batched increments
expect(requests.length).toBeGreaterThanOrEqual(2);
// First request should be the critical action (immediate)
const firstRequest = JSON.parse(requests[0].postData());
expect(firstRequest.action).toBe('criticalAction');
expect(firstRequest).not.toHaveProperty('batch');
// Second request should be the batched increments
const secondRequest = JSON.parse(requests[1].postData());
expect(secondRequest).toHaveProperty('batch');
expect(secondRequest.batch.length).toBe(2);
});
});