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