Files
michaelschiemer/tests/e2e/livecomponents-partial-rendering.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

362 lines
13 KiB
JavaScript

/**
* LiveComponents E2E Tests - Partial Rendering & Fragments
*
* Tests for fragment-based partial rendering functionality:
* - Fragment extraction and rendering
* - DOM patching with focus preservation
* - Multiple fragment updates in single request
* - Error handling and fallback to full render
*
* @see src/Framework/LiveComponents/Services/FragmentExtractor.php
* @see src/Framework/LiveComponents/Services/FragmentRenderer.php
* @see resources/js/modules/DomPatcher.js
*/
import { test, expect } from '@playwright/test';
test.describe('LiveComponents - Partial Rendering & Fragments', () => {
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 render initial component with fragments', async ({ page }) => {
// Check that component has fragment markers
const headerFragment = await page.locator('[data-lc-fragment="header"]');
const contentFragment = await page.locator('[data-lc-fragment="content"]');
const footerFragment = await page.locator('[data-lc-fragment="footer"]');
await expect(headerFragment).toBeVisible();
await expect(contentFragment).toBeVisible();
await expect(footerFragment).toBeVisible();
});
test('should update single fragment without full re-render', async ({ page }) => {
// Get initial content
const contentFragment = page.locator('[data-lc-fragment="content"]');
const initialContent = await contentFragment.textContent();
// Get component ID
const componentId = await page.locator('[data-component-id]').getAttribute('data-component-id');
// Trigger action that updates only content fragment
await page.click('[data-lc-action="updateContent"]');
// Wait for fragment update
await page.waitForResponse(response =>
response.url().includes('/livecomponent') &&
response.status() === 200
);
// Content should have changed
const updatedContent = await contentFragment.textContent();
expect(updatedContent).not.toBe(initialContent);
// Header and footer should remain unchanged (no re-render)
const headerElement = page.locator('[data-lc-fragment="header"]');
const footerElement = page.locator('[data-lc-fragment="footer"]');
// Elements should still have same references (not re-created)
await expect(headerElement).toBeVisible();
await expect(footerElement).toBeVisible();
});
test('should update multiple fragments in single request', async ({ page }) => {
const headerFragment = page.locator('[data-lc-fragment="header"]');
const contentFragment = page.locator('[data-lc-fragment="content"]');
const initialHeader = await headerFragment.textContent();
const initialContent = await contentFragment.textContent();
// Trigger action that updates both header and content
await page.click('[data-lc-action="updateHeaderAndContent"]');
await page.waitForResponse(response =>
response.url().includes('/livecomponent') &&
response.status() === 200
);
// Both fragments should be updated
const updatedHeader = await headerFragment.textContent();
const updatedContent = await contentFragment.textContent();
expect(updatedHeader).not.toBe(initialHeader);
expect(updatedContent).not.toBe(initialContent);
});
test('should preserve focus during fragment updates', async ({ page }) => {
// Find input in content fragment
const input = page.locator('[data-lc-fragment="content"] input[name="search"]');
await input.fill('test query');
await input.focus();
// Verify focus
await expect(input).toBeFocused();
// Trigger fragment update
await page.click('[data-lc-action="updateContent"]');
await page.waitForResponse(response =>
response.url().includes('/livecomponent') &&
response.status() === 200
);
// Focus should be preserved
await expect(input).toBeFocused();
// Value should be preserved
await expect(input).toHaveValue('test query');
});
test('should preserve selection state during fragment updates', async ({ page }) => {
const textarea = page.locator('[data-lc-fragment="content"] textarea');
await textarea.fill('Hello World');
// Select portion of text
await textarea.evaluate((el) => {
el.setSelectionRange(0, 5); // Select "Hello"
});
// Trigger fragment update
await page.click('[data-lc-action="updateContent"]');
await page.waitForResponse(response =>
response.url().includes('/livecomponent') &&
response.status() === 200
);
// Selection should be preserved
const selectionStart = await textarea.evaluate((el) => el.selectionStart);
const selectionEnd = await textarea.evaluate((el) => el.selectionEnd);
expect(selectionStart).toBe(0);
expect(selectionEnd).toBe(5);
});
test('should fall back to full render if fragment not found', async ({ page }) => {
const component = page.locator('[data-component-id]');
const initialHtml = await component.innerHTML();
// Trigger action with non-existent fragment request
await page.evaluate(() => {
window.LiveComponent.executeAction(
document.querySelector('[data-component-id]').dataset.componentId,
'updateContent',
{},
['non-existent-fragment']
);
});
await page.waitForResponse(response =>
response.url().includes('/livecomponent') &&
response.status() === 200
);
// Should fall back to full render
const updatedHtml = await component.innerHTML();
expect(updatedHtml).not.toBe(initialHtml);
// All fragments should still be present
await expect(page.locator('[data-lc-fragment="header"]')).toBeVisible();
await expect(page.locator('[data-lc-fragment="content"]')).toBeVisible();
await expect(page.locator('[data-lc-fragment="footer"]')).toBeVisible();
});
test('should handle nested fragments correctly', async ({ page }) => {
// Component with nested structure:
// <div data-lc-fragment="parent">
// <div data-lc-fragment="child">...</div>
// </div>
const parentFragment = page.locator('[data-lc-fragment="parent"]');
const childFragment = page.locator('[data-lc-fragment="child"]');
const initialParent = await parentFragment.textContent();
const initialChild = await childFragment.textContent();
// Update only child fragment
await page.click('[data-lc-action="updateChild"]');
await page.waitForResponse(response =>
response.url().includes('/livecomponent') &&
response.status() === 200
);
// Child should be updated
const updatedChild = await childFragment.textContent();
expect(updatedChild).not.toBe(initialChild);
// Parent container should remain (DOM patching, not replacement)
await expect(parentFragment).toBeVisible();
});
test('should update attributes during fragment patch', async ({ page }) => {
const contentFragment = page.locator('[data-lc-fragment="content"]');
// Check initial class
const initialClass = await contentFragment.getAttribute('class');
// Trigger action that changes fragment attributes
await page.click('[data-lc-action="toggleActive"]');
await page.waitForResponse(response =>
response.url().includes('/livecomponent') &&
response.status() === 200
);
// Class should be updated
const updatedClass = await contentFragment.getAttribute('class');
expect(updatedClass).not.toBe(initialClass);
expect(updatedClass).toContain('active');
});
test('should handle fragment updates with different element counts', async ({ page }) => {
const listFragment = page.locator('[data-lc-fragment="list"]');
// Initial list with 3 items
let items = await listFragment.locator('li').count();
expect(items).toBe(3);
// Add item
await page.click('[data-lc-action="addItem"]');
await page.waitForResponse(response =>
response.url().includes('/livecomponent') &&
response.status() === 200
);
// Should have 4 items now
items = await listFragment.locator('li').count();
expect(items).toBe(4);
// Remove item
await page.click('[data-lc-action="removeItem"]');
await page.waitForResponse(response =>
response.url().includes('/livecomponent') &&
response.status() === 200
);
// Should have 3 items again
items = await listFragment.locator('li').count();
expect(items).toBe(3);
});
test('should preserve event listeners during fragment updates', async ({ page }) => {
// Click counter in fragment
const button = page.locator('[data-lc-fragment="content"] button[data-click-counter]');
const counter = page.locator('[data-lc-fragment="content"] .click-count');
// Click button
await button.click();
await expect(counter).toHaveText('1');
// Trigger fragment update
await page.click('[data-lc-action="updateContent"]');
await page.waitForResponse(response =>
response.url().includes('/livecomponent') &&
response.status() === 200
);
// Click button again - event listener should still work
await button.click();
await expect(counter).toHaveText('2');
});
test('should handle rapid fragment updates without conflicts', async ({ page }) => {
const contentFragment = page.locator('[data-lc-fragment="content"]');
// Trigger multiple updates rapidly
await page.click('[data-lc-action="updateContent"]');
await page.click('[data-lc-action="updateContent"]');
await page.click('[data-lc-action="updateContent"]');
// Wait for all responses
await page.waitForLoadState('networkidle');
// Fragment should be stable and visible
await expect(contentFragment).toBeVisible();
// Should have processed all updates
const updateCount = await contentFragment.locator('.update-count').textContent();
expect(parseInt(updateCount)).toBeGreaterThanOrEqual(3);
});
test('should emit fragment update events', async ({ page }) => {
let fragmentUpdateEvents = [];
// Listen for fragment update events
await page.exposeFunction('onFragmentUpdate', (event) => {
fragmentUpdateEvents.push(event);
});
await page.evaluate(() => {
window.addEventListener('livecomponent:fragment-updated', (e) => {
window.onFragmentUpdate(e.detail);
});
});
// Trigger fragment update
await page.click('[data-lc-action="updateContent"]');
await page.waitForResponse(response =>
response.url().includes('/livecomponent') &&
response.status() === 200
);
// Wait for event
await page.waitForTimeout(100);
// Should have received fragment update event
expect(fragmentUpdateEvents.length).toBeGreaterThan(0);
expect(fragmentUpdateEvents[0]).toHaveProperty('fragmentName');
expect(fragmentUpdateEvents[0].fragmentName).toBe('content');
});
test('should work with fragments containing special characters', async ({ page }) => {
// Fragment with special characters in content
const fragment = page.locator('[data-lc-fragment="special-chars"]');
const initialContent = await fragment.innerHTML();
// Update fragment
await page.click('[data-lc-action="updateSpecialChars"]');
await page.waitForResponse(response =>
response.url().includes('/livecomponent') &&
response.status() === 200
);
const updatedContent = await fragment.innerHTML();
// Should properly handle HTML entities and special characters
expect(updatedContent).not.toBe(initialContent);
expect(updatedContent).toContain('&lt;'); // Escaped <
expect(updatedContent).toContain('&amp;'); // Escaped &
});
test('should handle fragments with dynamic data-attributes', async ({ page }) => {
const fragment = page.locator('[data-lc-fragment="dynamic"]');
const initialDataValue = await fragment.getAttribute('data-custom-value');
// Update fragment with new data attributes
await page.click('[data-lc-action="updateDynamic"]');
await page.waitForResponse(response =>
response.url().includes('/livecomponent') &&
response.status() === 200
);
const updatedDataValue = await fragment.getAttribute('data-custom-value');
expect(updatedDataValue).not.toBe(initialDataValue);
});
});