- 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.
149 lines
3.6 KiB
TypeScript
149 lines
3.6 KiB
TypeScript
import { Page, expect } from '@playwright/test';
|
|
|
|
/**
|
|
* Helper utilities for Playwright E2E tests
|
|
*/
|
|
|
|
/**
|
|
* Wait for LiveComponent to be ready
|
|
*/
|
|
export async function waitForLiveComponent(page: Page, componentId: string) {
|
|
await page.waitForSelector(`[data-live-component="${componentId}"]`, {
|
|
state: 'visible',
|
|
timeout: 5000
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Wait for HTMX request to complete
|
|
*/
|
|
export async function waitForHtmxRequest(page: Page) {
|
|
await page.waitForEvent('htmx:afterRequest', { timeout: 5000 });
|
|
}
|
|
|
|
/**
|
|
* Fill form and submit with LiveComponent validation
|
|
*/
|
|
export async function fillAndSubmitForm(
|
|
page: Page,
|
|
formSelector: string,
|
|
data: Record<string, string>
|
|
) {
|
|
const form = page.locator(formSelector);
|
|
|
|
for (const [name, value] of Object.entries(data)) {
|
|
await form.locator(`[name="${name}"]`).fill(value);
|
|
}
|
|
|
|
await form.locator('button[type="submit"]').click();
|
|
}
|
|
|
|
/**
|
|
* Assert validation error is shown
|
|
*/
|
|
export async function assertValidationError(
|
|
page: Page,
|
|
fieldName: string,
|
|
expectedError?: string
|
|
) {
|
|
const errorSelector = `[data-field="${fieldName}"] .error-message, .error-${fieldName}`;
|
|
await expect(page.locator(errorSelector)).toBeVisible();
|
|
|
|
if (expectedError) {
|
|
await expect(page.locator(errorSelector)).toContainText(expectedError);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Assert no validation errors
|
|
*/
|
|
export async function assertNoValidationErrors(page: Page) {
|
|
const errors = page.locator('.error-message, [class*="error-"]');
|
|
await expect(errors).toHaveCount(0);
|
|
}
|
|
|
|
/**
|
|
* Wait for success message
|
|
*/
|
|
export async function waitForSuccessMessage(page: Page, message?: string) {
|
|
const successSelector = '.flash-success, .alert-success, [role="alert"][data-type="success"]';
|
|
await expect(page.locator(successSelector)).toBeVisible();
|
|
|
|
if (message) {
|
|
await expect(page.locator(successSelector)).toContainText(message);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Login helper for authenticated tests
|
|
*/
|
|
export async function login(page: Page, email: string, password: string) {
|
|
await page.goto('/login');
|
|
await page.fill('[name="email"]', email);
|
|
await page.fill('[name="password"]', password);
|
|
await page.click('button[type="submit"]');
|
|
|
|
// Wait for redirect after login
|
|
await page.waitForURL('**/dashboard', { timeout: 5000 });
|
|
}
|
|
|
|
/**
|
|
* Logout helper
|
|
*/
|
|
export async function logout(page: Page) {
|
|
await page.click('[data-action="logout"]');
|
|
await page.waitForURL('**/login', { timeout: 5000 });
|
|
}
|
|
|
|
/**
|
|
* Check if element is visible in viewport
|
|
*/
|
|
export async function isInViewport(page: Page, selector: string): Promise<boolean> {
|
|
return await page.locator(selector).evaluate((element) => {
|
|
const rect = element.getBoundingClientRect();
|
|
return (
|
|
rect.top >= 0 &&
|
|
rect.left >= 0 &&
|
|
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
|
|
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
|
|
);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Scroll element into view
|
|
*/
|
|
export async function scrollIntoView(page: Page, selector: string) {
|
|
await page.locator(selector).scrollIntoViewIfNeeded();
|
|
}
|
|
|
|
/**
|
|
* Upload file helper
|
|
*/
|
|
export async function uploadFile(
|
|
page: Page,
|
|
inputSelector: string,
|
|
filePath: string
|
|
) {
|
|
const fileInput = page.locator(inputSelector);
|
|
await fileInput.setInputFiles(filePath);
|
|
}
|
|
|
|
/**
|
|
* Wait for network idle
|
|
*/
|
|
export async function waitForNetworkIdle(page: Page) {
|
|
await page.waitForLoadState('networkidle', { timeout: 10000 });
|
|
}
|
|
|
|
/**
|
|
* Clear browser storage
|
|
*/
|
|
export async function clearStorage(page: Page) {
|
|
await page.context().clearCookies();
|
|
await page.evaluate(() => {
|
|
localStorage.clear();
|
|
sessionStorage.clear();
|
|
});
|
|
}
|