- 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.
190 lines
5.6 KiB
TypeScript
190 lines
5.6 KiB
TypeScript
import { test, expect } from '@playwright/test';
|
|
import {
|
|
waitForLiveComponent,
|
|
fillAndSubmitForm,
|
|
assertValidationError,
|
|
assertNoValidationErrors,
|
|
waitForSuccessMessage
|
|
} from '../support/test-helpers';
|
|
|
|
/**
|
|
* LiveComponents: Form Validation Tests
|
|
*
|
|
* Tests real-time form validation with LiveComponents
|
|
*/
|
|
test.describe('LiveComponents Form Validation', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
// Navigate to a page with LiveComponent form
|
|
// Adjust URL based on your actual routes
|
|
await page.goto('/');
|
|
});
|
|
|
|
test('should show validation errors on invalid input', async ({ page }) => {
|
|
// Skip if no form is present on homepage
|
|
const formCount = await page.locator('form').count();
|
|
if (formCount === 0) {
|
|
test.skip();
|
|
return;
|
|
}
|
|
|
|
const form = page.locator('form').first();
|
|
|
|
// Try to submit empty form
|
|
await form.locator('button[type="submit"]').click();
|
|
|
|
// Wait a bit for validation to trigger
|
|
await page.waitForTimeout(500);
|
|
|
|
// Check if any validation messages appear
|
|
const errorMessages = page.locator('.error-message, [class*="error-"], .invalid-feedback');
|
|
const errorCount = await errorMessages.count();
|
|
|
|
// If there are required fields, there should be error messages
|
|
const requiredFields = await form.locator('[required]').count();
|
|
if (requiredFields > 0) {
|
|
expect(errorCount).toBeGreaterThan(0);
|
|
}
|
|
});
|
|
|
|
test('should validate email format in real-time', async ({ page }) => {
|
|
// Look for email input
|
|
const emailInput = page.locator('input[type="email"]').first();
|
|
const emailCount = await emailInput.count();
|
|
|
|
if (emailCount === 0) {
|
|
test.skip();
|
|
return;
|
|
}
|
|
|
|
// Enter invalid email
|
|
await emailInput.fill('invalid-email');
|
|
await emailInput.blur();
|
|
|
|
// Wait for validation
|
|
await page.waitForTimeout(500);
|
|
|
|
// Check for error message (if client-side validation is present)
|
|
// Note: HTML5 validation or custom validation might trigger
|
|
const validity = await emailInput.evaluate((el: HTMLInputElement) => el.validity.valid);
|
|
expect(validity).toBe(false);
|
|
});
|
|
|
|
test('should clear validation errors on valid input', async ({ page }) => {
|
|
const emailInput = page.locator('input[type="email"]').first();
|
|
const emailCount = await emailInput.count();
|
|
|
|
if (emailCount === 0) {
|
|
test.skip();
|
|
return;
|
|
}
|
|
|
|
// Enter invalid email first
|
|
await emailInput.fill('invalid');
|
|
await emailInput.blur();
|
|
await page.waitForTimeout(300);
|
|
|
|
// Now enter valid email
|
|
await emailInput.fill('valid@example.com');
|
|
await emailInput.blur();
|
|
await page.waitForTimeout(300);
|
|
|
|
// Check validity
|
|
const validity = await emailInput.evaluate((el: HTMLInputElement) => el.validity.valid);
|
|
expect(validity).toBe(true);
|
|
});
|
|
|
|
test('should prevent submission with validation errors', async ({ page }) => {
|
|
const formCount = await page.locator('form').count();
|
|
if (formCount === 0) {
|
|
test.skip();
|
|
return;
|
|
}
|
|
|
|
const form = page.locator('form').first();
|
|
const currentUrl = page.url();
|
|
|
|
// Try to submit form with invalid data
|
|
const submitButton = form.locator('button[type="submit"]');
|
|
await submitButton.click();
|
|
|
|
// Wait a bit
|
|
await page.waitForTimeout(1000);
|
|
|
|
// URL should not change if validation failed
|
|
// (assuming no AJAX submission that stays on page)
|
|
const newUrl = page.url();
|
|
|
|
// Form should still be visible (not submitted)
|
|
await expect(form).toBeVisible();
|
|
});
|
|
|
|
test('should handle required fields', async ({ page }) => {
|
|
const requiredInputs = page.locator('input[required], textarea[required], select[required]');
|
|
const requiredCount = await requiredInputs.count();
|
|
|
|
if (requiredCount === 0) {
|
|
test.skip();
|
|
return;
|
|
}
|
|
|
|
// Check first required field
|
|
const firstRequired = requiredInputs.first();
|
|
|
|
// Should have required attribute
|
|
await expect(firstRequired).toHaveAttribute('required');
|
|
|
|
// Should have appropriate ARIA attributes
|
|
const ariaRequired = await firstRequired.getAttribute('aria-required');
|
|
expect(['true', null]).toContain(ariaRequired);
|
|
});
|
|
|
|
test('should handle max length validation', async ({ page }) => {
|
|
const maxLengthInputs = page.locator('input[maxlength], textarea[maxlength]');
|
|
const count = await maxLengthInputs.count();
|
|
|
|
if (count === 0) {
|
|
test.skip();
|
|
return;
|
|
}
|
|
|
|
const input = maxLengthInputs.first();
|
|
const maxLength = await input.getAttribute('maxlength');
|
|
|
|
if (maxLength) {
|
|
const maxLengthNum = parseInt(maxLength, 10);
|
|
|
|
// Try to enter text longer than maxlength
|
|
const longText = 'a'.repeat(maxLengthNum + 10);
|
|
await input.fill(longText);
|
|
|
|
// Value should be truncated to maxlength
|
|
const value = await input.inputValue();
|
|
expect(value.length).toBeLessThanOrEqual(maxLengthNum);
|
|
}
|
|
});
|
|
|
|
test('should show character counter for textarea (if present)', async ({ page }) => {
|
|
const textarea = page.locator('textarea[maxlength]').first();
|
|
const count = await textarea.count();
|
|
|
|
if (count === 0) {
|
|
test.skip();
|
|
return;
|
|
}
|
|
|
|
// Look for character counter
|
|
const counter = page.locator('[data-char-counter], .char-counter, .character-count');
|
|
const counterCount = await counter.count();
|
|
|
|
// Enter some text
|
|
await textarea.fill('Hello World');
|
|
|
|
// If counter exists, it should update
|
|
if (counterCount > 0) {
|
|
await page.waitForTimeout(500);
|
|
const counterText = await counter.first().textContent();
|
|
expect(counterText).toBeTruthy();
|
|
}
|
|
});
|
|
});
|