- 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.
716 lines
26 KiB
JavaScript
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);
|
|
});
|
|
});
|