Files
michaelschiemer/tests/e2e/livecomponents/integration.spec.js
Michael Schiemer fc3d7e6357 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.
2025-10-25 19:18:37 +02:00

716 lines
26 KiB
JavaScript

import { test, expect } from '@playwright/test';
/**
* LiveComponents E2E Integration Tests
*
* Tests für Cross-Cutting Concerns:
* - Partial Rendering (Fragment-basierte Updates)
* - Batch Operations (Mehrere Actions zusammen)
* - Server-Sent Events (Echtzeit-Kommunikation)
*
* @requires Test-Seite unter /livecomponents/test/integration
* @requires LiveComponents Framework initialisiert
*/
test.describe('Partial Rendering', () => {
test.beforeEach(async ({ page }) => {
await page.goto('https://localhost/livecomponents/test/integration');
await page.waitForFunction(() => window.LiveComponents !== undefined, { timeout: 5000 });
});
test('should update only targeted fragment without full component re-render', async ({ page }) => {
// Timestamp vor Update erfassen
const initialTimestamp = await page.locator('#component-timestamp').textContent();
// Fragment-spezifische Action triggern
await page.click('button#update-fragment');
await page.waitForTimeout(500);
// Fragment wurde aktualisiert
const fragmentContent = await page.locator('#target-fragment').textContent();
expect(fragmentContent).toContain('Updated');
// Component timestamp NICHT geändert (kein Full Render)
const finalTimestamp = await page.locator('#component-timestamp').textContent();
expect(finalTimestamp).toBe(initialTimestamp);
// Nur Fragment wurde im DOM aktualisiert
const updateCount = await page.evaluate(() => {
return window.__fragmentUpdateCount || 0;
});
expect(updateCount).toBe(1);
});
test('should update multiple fragments in single request', async ({ page }) => {
await page.click('button#update-multiple-fragments');
await page.waitForTimeout(500);
// Beide Fragments aktualisiert
const fragment1 = await page.locator('#fragment-1').textContent();
const fragment2 = await page.locator('#fragment-2').textContent();
expect(fragment1).toContain('Fragment 1 Updated');
expect(fragment2).toContain('Fragment 2 Updated');
// Nur 1 HTTP Request für beide Updates
const requestCount = await page.evaluate(() => {
return window.__requestCount || 0;
});
expect(requestCount).toBe(1);
});
test('should preserve component state during partial render', async ({ page }) => {
// State setzen
await page.fill('input#state-input', 'Test Value');
await page.click('button#save-state');
await page.waitForTimeout(200);
// Fragment Update triggern
await page.click('button#update-fragment');
await page.waitForTimeout(500);
// State wurde NICHT verloren
const inputValue = await page.inputValue('input#state-input');
expect(inputValue).toBe('Test Value');
// State im Component noch vorhanden
const stateValue = await page.evaluate(() => {
const component = window.LiveComponents.get('integration:test');
return component.state.get('savedValue');
});
expect(stateValue).toBe('Test Value');
});
test('should handle nested fragment updates', async ({ page }) => {
await page.click('button#update-nested-fragment');
await page.waitForTimeout(500);
// Parent fragment aktualisiert
const parentContent = await page.locator('#parent-fragment').textContent();
expect(parentContent).toContain('Parent Updated');
// Child fragment innerhalb von parent auch aktualisiert
const childContent = await page.locator('#child-fragment').textContent();
expect(childContent).toContain('Child Updated');
// Sibling fragment NICHT aktualisiert
const siblingTimestamp = await page.locator('#sibling-fragment').getAttribute('data-timestamp');
const originalTimestamp = await page.evaluate(() => window.__originalSiblingTimestamp);
expect(siblingTimestamp).toBe(originalTimestamp);
});
test('should apply morphing algorithm for minimal DOM changes', async ({ page }) => {
// DOM Nodes vor Update zählen
const nodesBefore = await page.evaluate(() => {
const fragment = document.getElementById('morph-fragment');
return fragment.querySelectorAll('*').length;
});
// Kleines Update im Fragment
await page.click('button#small-update');
await page.waitForTimeout(500);
// DOM Nodes nach Update
const nodesAfter = await page.evaluate(() => {
const fragment = document.getElementById('morph-fragment');
return fragment.querySelectorAll('*').length;
});
// Anzahl Nodes sollte gleich bleiben (nur Content geändert)
expect(nodesAfter).toBe(nodesBefore);
// Morphing Stats prüfen
const morphStats = await page.evaluate(() => window.__morphingStats);
expect(morphStats.nodesAdded).toBe(0);
expect(morphStats.nodesRemoved).toBe(0);
expect(morphStats.nodesUpdated).toBeGreaterThan(0);
});
test('should handle fragment-not-found gracefully', async ({ page }) => {
// Action mit nicht-existierendem Fragment
await page.evaluate(() => {
window.LiveComponents.get('integration:test').call('updateFragment', {
fragmentId: 'non-existent-fragment'
});
});
await page.waitForTimeout(500);
// Error handling
const errorMessage = await page.locator('.fragment-error').textContent();
expect(errorMessage).toContain('Fragment not found');
// Component bleibt funktionsfähig
await page.click('button#update-fragment');
await page.waitForTimeout(500);
const fragmentContent = await page.locator('#target-fragment').textContent();
expect(fragmentContent).toContain('Updated');
});
});
test.describe('Batch Operations', () => {
test.beforeEach(async ({ page }) => {
await page.goto('https://localhost/livecomponents/test/integration');
await page.waitForFunction(() => window.LiveComponents !== undefined, { timeout: 5000 });
});
test('should execute multiple actions in single batch request', async ({ page }) => {
const requestsBefore = await page.evaluate(() => window.__requestCount || 0);
// Batch von 3 Actions triggern
await page.evaluate(() => {
const component = window.LiveComponents.get('integration:test');
component.batch()
.call('incrementCounter')
.call('updateText', { text: 'Batch Updated' })
.call('toggleFlag')
.execute();
});
await page.waitForTimeout(500);
// Nur 1 zusätzlicher Request für alle 3 Actions
const requestsAfter = await page.evaluate(() => window.__requestCount || 0);
expect(requestsAfter - requestsBefore).toBe(1);
// Alle Actions wurden ausgeführt
const counter = await page.locator('#counter-value').textContent();
expect(parseInt(counter)).toBeGreaterThan(0);
const text = await page.locator('#text-value').textContent();
expect(text).toBe('Batch Updated');
const flag = await page.locator('#flag-value').textContent();
expect(flag).toBe('true');
});
test('should maintain action execution order in batch', async ({ page }) => {
const executionLog = [];
await page.exposeFunction('logExecution', (action) => {
executionLog.push(action);
});
await page.evaluate(() => {
const component = window.LiveComponents.get('integration:test');
component.batch()
.call('action1')
.call('action2')
.call('action3')
.execute();
});
await page.waitForTimeout(1000);
// Actions in korrekter Reihenfolge ausgeführt
expect(executionLog).toEqual(['action1', 'action2', 'action3']);
});
test('should rollback batch on action failure', async ({ page }) => {
const initialCounter = await page.locator('#counter-value').textContent();
await page.evaluate(() => {
const component = window.LiveComponents.get('integration:test');
component.batch()
.call('incrementCounter') // Erfolgreich
.call('failingAction') // Fehlschlag
.call('incrementCounter') // Sollte nicht ausgeführt werden
.execute();
});
await page.waitForTimeout(500);
// Counter wurde NICHT inkrementiert (Rollback)
const finalCounter = await page.locator('#counter-value').textContent();
expect(finalCounter).toBe(initialCounter);
// Error angezeigt
const errorVisible = await page.locator('.batch-error').isVisible();
expect(errorVisible).toBe(true);
});
test('should handle partial batch execution with continueOnError flag', async ({ page }) => {
await page.evaluate(() => {
const component = window.LiveComponents.get('integration:test');
component.batch({ continueOnError: true })
.call('incrementCounter') // Erfolgreich
.call('failingAction') // Fehlschlag
.call('incrementCounter') // Sollte trotzdem ausgeführt werden
.execute();
});
await page.waitForTimeout(500);
// Counter wurde 2x inkrementiert (trotz Fehler in der Mitte)
const counter = await page.locator('#counter-value').textContent();
expect(parseInt(counter)).toBe(2);
// Partial success angezeigt
const partialSuccessVisible = await page.locator('.partial-success').isVisible();
expect(partialSuccessVisible).toBe(true);
});
test('should support batch with mixed action types', async ({ page }) => {
await page.evaluate(() => {
const component = window.LiveComponents.get('integration:test');
component.batch()
.call('syncAction') // Synchrone Action
.call('asyncAction') // Asynchrone Action
.call('fragmentAction') // Fragment Update Action
.execute();
});
await page.waitForTimeout(1000);
// Alle Action-Typen erfolgreich
const syncResult = await page.locator('#sync-result').isVisible();
const asyncResult = await page.locator('#async-result').isVisible();
const fragmentResult = await page.locator('#fragment-result').isVisible();
expect(syncResult).toBe(true);
expect(asyncResult).toBe(true);
expect(fragmentResult).toBe(true);
});
test('should batch state updates efficiently', async ({ page }) => {
const stateUpdatesBefore = await page.evaluate(() => {
return window.__stateUpdateCount || 0;
});
await page.evaluate(() => {
const component = window.LiveComponents.get('integration:test');
// 10 State-Updates in Batch
const batch = component.batch();
for (let i = 0; i < 10; i++) {
batch.call('incrementCounter');
}
batch.execute();
});
await page.waitForTimeout(500);
// Counter wurde 10x inkrementiert
const counter = await page.locator('#counter-value').textContent();
expect(parseInt(counter)).toBe(10);
// Nur 1 State-Update Event gefeuert (optimiert)
const stateUpdatesAfter = await page.evaluate(() => {
return window.__stateUpdateCount || 0;
});
expect(stateUpdatesAfter - stateUpdatesBefore).toBe(1);
});
});
test.describe('Server-Sent Events (SSE)', () => {
test.beforeEach(async ({ page }) => {
await page.goto('https://localhost/livecomponents/test/integration');
await page.waitForFunction(() => window.LiveComponents !== undefined, { timeout: 5000 });
});
test('should establish SSE connection for real-time updates', async ({ page }) => {
// SSE aktivieren
await page.click('button#enable-sse');
await page.waitForTimeout(1000);
// Connection Status prüfen
const connectionStatus = await page.evaluate(() => {
const component = window.LiveComponents.get('integration:test');
return component.sse?.readyState;
});
expect(connectionStatus).toBe(1); // OPEN
// SSE-Indikator sichtbar
const sseIndicator = await page.locator('.sse-connected').isVisible();
expect(sseIndicator).toBe(true);
});
test('should receive and apply server-pushed updates', async ({ page }) => {
await page.click('button#enable-sse');
await page.waitForTimeout(1000);
const initialValue = await page.locator('#live-value').textContent();
// Server-Push triggern (z.B. durch andere User/Aktion)
await page.evaluate(() => {
// Simuliere Server-Push Event
const event = new MessageEvent('message', {
data: JSON.stringify({
type: 'update',
payload: { liveValue: 'Server Updated' }
})
});
const component = window.LiveComponents.get('integration:test');
component.sse?.dispatchEvent(event);
});
await page.waitForTimeout(500);
// Wert wurde automatisch aktualisiert
const updatedValue = await page.locator('#live-value').textContent();
expect(updatedValue).toBe('Server Updated');
expect(updatedValue).not.toBe(initialValue);
});
test('should handle SSE reconnection on connection loss', async ({ page }) => {
await page.click('button#enable-sse');
await page.waitForTimeout(1000);
// Connection simuliert unterbrochen
await page.evaluate(() => {
const component = window.LiveComponents.get('integration:test');
component.sse?.close();
});
await page.waitForTimeout(500);
// Reconnection Indikator
const reconnecting = await page.locator('.sse-reconnecting').isVisible();
expect(reconnecting).toBe(true);
// Nach Retry-Periode reconnected
await page.waitForTimeout(3000);
const reconnected = await page.locator('.sse-connected').isVisible();
expect(reconnected).toBe(true);
});
test('should support multiple SSE event types', async ({ page }) => {
await page.click('button#enable-sse');
await page.waitForTimeout(1000);
const events = [];
await page.exposeFunction('logSSEEvent', (eventType) => {
events.push(eventType);
});
// Verschiedene Event-Typen senden
await page.evaluate(() => {
const component = window.LiveComponents.get('integration:test');
const sse = component.sse;
sse.dispatchEvent(new MessageEvent('message', {
data: JSON.stringify({ type: 'update', payload: {} })
}));
sse.dispatchEvent(new MessageEvent('message', {
data: JSON.stringify({ type: 'notification', payload: {} })
}));
sse.dispatchEvent(new MessageEvent('message', {
data: JSON.stringify({ type: 'sync', payload: {} })
}));
});
await page.waitForTimeout(1000);
// Alle Event-Typen wurden verarbeitet
expect(events).toContain('update');
expect(events).toContain('notification');
expect(events).toContain('sync');
});
test('should batch SSE updates for performance', async ({ page }) => {
await page.click('button#enable-sse');
await page.waitForTimeout(1000);
const renderCountBefore = await page.evaluate(() => window.__renderCount || 0);
// Viele schnelle SSE Updates
await page.evaluate(() => {
const component = window.LiveComponents.get('integration:test');
const sse = component.sse;
for (let i = 0; i < 20; i++) {
sse.dispatchEvent(new MessageEvent('message', {
data: JSON.stringify({
type: 'update',
payload: { counter: i }
})
}));
}
});
await page.waitForTimeout(1000);
// Finale Wert korrekt
const finalCounter = await page.locator('#sse-counter').textContent();
expect(parseInt(finalCounter)).toBe(19);
// Deutlich weniger Renders als Updates (Batching)
const renderCountAfter = await page.evaluate(() => window.__renderCount || 0);
const renderDiff = renderCountAfter - renderCountBefore;
expect(renderDiff).toBeLessThan(20);
expect(renderDiff).toBeGreaterThan(0);
});
test('should close SSE connection when component unmounts', async ({ page }) => {
await page.click('button#enable-sse');
await page.waitForTimeout(1000);
// Connection aktiv
let connectionState = await page.evaluate(() => {
const component = window.LiveComponents.get('integration:test');
return component.sse?.readyState;
});
expect(connectionState).toBe(1); // OPEN
// Component unmounten
await page.evaluate(() => {
window.LiveComponents.get('integration:test').unmount();
});
await page.waitForTimeout(500);
// Connection geschlossen
connectionState = await page.evaluate(() => {
const component = window.LiveComponents.get('integration:test');
return component.sse?.readyState;
});
expect(connectionState).toBe(2); // CLOSED
});
test('should handle SSE authentication and authorization', async ({ page }) => {
// SSE mit Auth Token
await page.evaluate(() => {
const component = window.LiveComponents.get('integration:test');
component.enableSSE({ authToken: 'valid-token-123' });
});
await page.waitForTimeout(1000);
// Connection erfolgreich mit Auth
const authenticated = await page.locator('.sse-authenticated').isVisible();
expect(authenticated).toBe(true);
// SSE mit ungültigem Token
await page.evaluate(() => {
const component = window.LiveComponents.get('integration:test');
component.sse?.close();
component.enableSSE({ authToken: 'invalid-token' });
});
await page.waitForTimeout(1000);
// Auth Fehler
const authError = await page.locator('.sse-auth-error').isVisible();
expect(authError).toBe(true);
});
test('should support SSE with custom event filters', async ({ page }) => {
await page.evaluate(() => {
const component = window.LiveComponents.get('integration:test');
// Nur 'important' Events empfangen
component.enableSSE({
filter: (event) => event.priority === 'important'
});
});
await page.waitForTimeout(1000);
const receivedEvents = [];
await page.exposeFunction('trackEvent', (event) => {
receivedEvents.push(event);
});
// Verschiedene Events senden
await page.evaluate(() => {
const component = window.LiveComponents.get('integration:test');
const sse = component.sse;
sse.dispatchEvent(new MessageEvent('message', {
data: JSON.stringify({
type: 'update',
priority: 'important',
payload: { id: 1 }
})
}));
sse.dispatchEvent(new MessageEvent('message', {
data: JSON.stringify({
type: 'update',
priority: 'low',
payload: { id: 2 }
})
}));
sse.dispatchEvent(new MessageEvent('message', {
data: JSON.stringify({
type: 'update',
priority: 'important',
payload: { id: 3 }
})
}));
});
await page.waitForTimeout(1000);
// Nur 'important' Events wurden verarbeitet
expect(receivedEvents.length).toBe(2);
expect(receivedEvents.map(e => e.id)).toEqual([1, 3]);
});
});
test.describe('Integration: Partial Rendering + Batch + SSE', () => {
test.beforeEach(async ({ page }) => {
await page.goto('https://localhost/livecomponents/test/integration');
await page.waitForFunction(() => window.LiveComponents !== undefined, { timeout: 5000 });
});
test('should combine partial rendering with batch operations', async ({ page }) => {
const requestsBefore = await page.evaluate(() => window.__requestCount || 0);
// Batch mit Fragment-Updates
await page.evaluate(() => {
const component = window.LiveComponents.get('integration:test');
component.batch()
.call('updateFragment', { fragmentId: 'fragment-1' })
.call('updateFragment', { fragmentId: 'fragment-2' })
.call('incrementCounter')
.execute();
});
await page.waitForTimeout(500);
// Nur 1 Request für alle Updates
const requestsAfter = await page.evaluate(() => window.__requestCount || 0);
expect(requestsAfter - requestsBefore).toBe(1);
// Beide Fragments aktualisiert
const fragment1Updated = await page.locator('#fragment-1').textContent();
const fragment2Updated = await page.locator('#fragment-2').textContent();
expect(fragment1Updated).toContain('Updated');
expect(fragment2Updated).toContain('Updated');
// Counter auch aktualisiert
const counter = await page.locator('#counter-value').textContent();
expect(parseInt(counter)).toBeGreaterThan(0);
});
test('should push partial updates via SSE', async ({ page }) => {
await page.click('button#enable-sse');
await page.waitForTimeout(1000);
// SSE Event mit Fragment Update
await page.evaluate(() => {
const component = window.LiveComponents.get('integration:test');
component.sse.dispatchEvent(new MessageEvent('message', {
data: JSON.stringify({
type: 'fragment-update',
fragmentId: 'live-fragment',
html: '<div id="live-fragment">SSE Updated</div>'
})
}));
});
await page.waitForTimeout(500);
// Fragment via SSE aktualisiert
const fragmentContent = await page.locator('#live-fragment').textContent();
expect(fragmentContent).toBe('SSE Updated');
// Kein Full Component Render
const componentTimestamp = await page.locator('#component-timestamp').textContent();
const originalTimestamp = await page.evaluate(() => window.__originalComponentTimestamp);
expect(componentTimestamp).toBe(originalTimestamp);
});
test('should batch SSE-triggered actions efficiently', async ({ page }) => {
await page.click('button#enable-sse');
await page.waitForTimeout(1000);
const actionsBefore = await page.evaluate(() => window.__actionCount || 0);
// Mehrere SSE Events die Actions triggern
await page.evaluate(() => {
const component = window.LiveComponents.get('integration:test');
const sse = component.sse;
// 5 Events in kurzer Zeit
for (let i = 0; i < 5; i++) {
sse.dispatchEvent(new MessageEvent('message', {
data: JSON.stringify({
type: 'trigger-action',
action: 'incrementCounter'
})
}));
}
});
await page.waitForTimeout(1000);
// Actions wurden gebatched
const actionsAfter = await page.evaluate(() => window.__actionCount || 0);
const actionDiff = actionsAfter - actionsBefore;
// Weniger Action-Aufrufe als Events (durch Batching)
expect(actionDiff).toBeLessThan(5);
expect(actionDiff).toBeGreaterThan(0);
// Finaler Counter-Wert korrekt
const counter = await page.locator('#counter-value').textContent();
expect(parseInt(counter)).toBe(5);
});
test('should maintain consistency across all integration features', async ({ page }) => {
await page.click('button#enable-sse');
await page.waitForTimeout(1000);
// Komplexes Szenario: Batch + Partial + SSE gleichzeitig
// 1. Batch mit Fragment-Updates starten
const batchPromise = page.evaluate(() => {
const component = window.LiveComponents.get('integration:test');
return component.batch()
.call('updateFragment', { fragmentId: 'fragment-1' })
.call('incrementCounter')
.execute();
});
// 2. Während Batch läuft: SSE Update
await page.waitForTimeout(200);
await page.evaluate(() => {
const component = window.LiveComponents.get('integration:test');
component.sse.dispatchEvent(new MessageEvent('message', {
data: JSON.stringify({
type: 'update',
payload: { liveValue: 'SSE During Batch' }
})
}));
});
await batchPromise;
await page.waitForTimeout(500);
// Alle Updates korrekt angewendet
const fragment1 = await page.locator('#fragment-1').textContent();
expect(fragment1).toContain('Updated');
const counter = await page.locator('#counter-value').textContent();
expect(parseInt(counter)).toBeGreaterThan(0);
const liveValue = await page.locator('#live-value').textContent();
expect(liveValue).toBe('SSE During Batch');
// State konsistent
const stateConsistent = await page.evaluate(() => {
const component = window.LiveComponents.get('integration:test');
return component.validateStateConsistency();
});
expect(stateConsistent).toBe(true);
});
});