- 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.
201 lines
5.4 KiB
TypeScript
201 lines
5.4 KiB
TypeScript
import { test, expect } from '@playwright/test';
|
|
|
|
/**
|
|
* LiveComponents: Real-time Updates Tests
|
|
*
|
|
* Tests SSE (Server-Sent Events) and real-time component updates
|
|
*/
|
|
test.describe('LiveComponents Real-time Updates', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await page.goto('/');
|
|
});
|
|
|
|
test('should establish SSE connection (if present)', async ({ page }) => {
|
|
// Listen for EventSource connections
|
|
const sseConnections: string[] = [];
|
|
|
|
page.on('request', (request) => {
|
|
if (request.url().includes('/sse') || request.headers()['accept']?.includes('text/event-stream')) {
|
|
sseConnections.push(request.url());
|
|
}
|
|
});
|
|
|
|
// Wait for potential SSE connections
|
|
await page.waitForTimeout(2000);
|
|
|
|
// If SSE is implemented, we should have connections
|
|
// This is informational - not all pages may have SSE
|
|
console.log('SSE Connections:', sseConnections);
|
|
});
|
|
|
|
test('should handle component updates via HTMX', async ({ page }) => {
|
|
// Look for HTMX-enabled elements
|
|
const htmxElements = page.locator('[hx-get], [hx-post], [hx-trigger]');
|
|
const count = await htmxElements.count();
|
|
|
|
if (count === 0) {
|
|
test.skip();
|
|
return;
|
|
}
|
|
|
|
// Track HTMX requests
|
|
let htmxRequestMade = false;
|
|
|
|
page.on('request', (request) => {
|
|
const headers = request.headers();
|
|
if (headers['hx-request'] === 'true') {
|
|
htmxRequestMade = true;
|
|
}
|
|
});
|
|
|
|
// Trigger first HTMX element
|
|
const firstElement = htmxElements.first();
|
|
await firstElement.click();
|
|
|
|
// Wait for request
|
|
await page.waitForTimeout(1000);
|
|
|
|
// HTMX request should have been made
|
|
expect(htmxRequestMade).toBe(true);
|
|
});
|
|
|
|
test('should update DOM without full page reload', async ({ page }) => {
|
|
const htmxElements = page.locator('[hx-get], [hx-post]');
|
|
const count = await htmxElements.count();
|
|
|
|
if (count === 0) {
|
|
test.skip();
|
|
return;
|
|
}
|
|
|
|
// Track page loads
|
|
let pageReloaded = false;
|
|
page.on('load', () => {
|
|
pageReloaded = true;
|
|
});
|
|
|
|
// Get initial page state
|
|
const initialUrl = page.url();
|
|
|
|
// Trigger HTMX action
|
|
await htmxElements.first().click();
|
|
await page.waitForTimeout(1500);
|
|
|
|
// URL should not change (unless it's a navigation)
|
|
const currentUrl = page.url();
|
|
|
|
// Page should not have reloaded for simple updates
|
|
// (this is a soft check as some actions might navigate)
|
|
if (currentUrl === initialUrl) {
|
|
expect(pageReloaded).toBe(false);
|
|
}
|
|
});
|
|
|
|
test('should handle loading states during updates', async ({ page }) => {
|
|
// Look for loading indicators
|
|
const loadingIndicators = page.locator('[hx-indicator], .htmx-request, [data-loading]');
|
|
|
|
// Trigger update if HTMX elements exist
|
|
const htmxElements = page.locator('[hx-get], [hx-post]');
|
|
const count = await htmxElements.count();
|
|
|
|
if (count === 0) {
|
|
test.skip();
|
|
return;
|
|
}
|
|
|
|
// Trigger action and check for loading state
|
|
await htmxElements.first().click();
|
|
|
|
// During request, loading indicator might be visible
|
|
await page.waitForTimeout(100);
|
|
|
|
// After request, loading should be hidden
|
|
await page.waitForTimeout(2000);
|
|
|
|
const loadingCount = await loadingIndicators.count();
|
|
console.log('Loading indicators found:', loadingCount);
|
|
});
|
|
|
|
test('should preserve form data during partial updates', async ({ page }) => {
|
|
const forms = page.locator('form');
|
|
const formCount = await forms.count();
|
|
|
|
if (formCount === 0) {
|
|
test.skip();
|
|
return;
|
|
}
|
|
|
|
const form = forms.first();
|
|
const inputs = form.locator('input[type="text"], input[type="email"]');
|
|
const inputCount = await inputs.count();
|
|
|
|
if (inputCount === 0) {
|
|
test.skip();
|
|
return;
|
|
}
|
|
|
|
// Fill some data
|
|
const testValue = 'test-value-123';
|
|
await inputs.first().fill(testValue);
|
|
|
|
// Wait a bit
|
|
await page.waitForTimeout(500);
|
|
|
|
// Value should still be present
|
|
const currentValue = await inputs.first().inputValue();
|
|
expect(currentValue).toBe(testValue);
|
|
});
|
|
|
|
test('should handle WebSocket connections (if present)', async ({ page }) => {
|
|
// Track WebSocket connections
|
|
const wsConnections: string[] = [];
|
|
|
|
page.on('websocket', (ws) => {
|
|
wsConnections.push(ws.url());
|
|
|
|
ws.on('framesent', (event) => {
|
|
console.log('WebSocket sent:', event.payload);
|
|
});
|
|
|
|
ws.on('framereceived', (event) => {
|
|
console.log('WebSocket received:', event.payload);
|
|
});
|
|
});
|
|
|
|
// Wait for potential WebSocket connections
|
|
await page.waitForTimeout(2000);
|
|
|
|
// Log WebSocket connections (informational)
|
|
console.log('WebSocket Connections:', wsConnections);
|
|
|
|
// If WebSocket is used, we should have connections
|
|
if (wsConnections.length > 0) {
|
|
expect(wsConnections.length).toBeGreaterThan(0);
|
|
}
|
|
});
|
|
|
|
test('should handle connection errors gracefully', async ({ page }) => {
|
|
// Monitor console errors
|
|
const errors: string[] = [];
|
|
|
|
page.on('console', (msg) => {
|
|
if (msg.type() === 'error') {
|
|
errors.push(msg.text());
|
|
}
|
|
});
|
|
|
|
// Navigate and wait
|
|
await page.goto('/');
|
|
await page.waitForTimeout(3000);
|
|
|
|
// Filter out known/expected errors
|
|
const criticalErrors = errors.filter(
|
|
(error) => !error.includes('favicon') && !error.includes('DevTools')
|
|
);
|
|
|
|
// Should not have critical JavaScript errors
|
|
expect(criticalErrors.length).toBe(0);
|
|
});
|
|
});
|