fix(ci): fix E2E port mismatch (3003→3690) and track test files

Playwright was waiting for port 3003 but Vite serves on 3690.
Also .gitignore excluded tests/ so CI had zero test files.

- Fix baseURL and webServer.url to port 3690 in playwright.config.ts
- Increase webServer timeout to 180s for CI
- Track test spec files (ignore only tests/screenshots/)
- Fix hardcoded port 3003 URLs in all test files
This commit is contained in:
2026-03-04 14:08:23 -08:00
parent a925350a51
commit 05628bb0af
7 changed files with 433 additions and 4 deletions
+1 -1
View File
@@ -26,5 +26,5 @@ dist-ssr
pnpm-lock.yaml
playwright-report/
test-results/
tests/
tests/screenshots/
*.png
+3 -3
View File
@@ -9,7 +9,7 @@ export default defineConfig({
reporter: 'html',
use: {
baseURL: 'http://localhost:3003',
baseURL: 'http://localhost:3690',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},
@@ -39,8 +39,8 @@ export default defineConfig({
webServer: {
command: 'npm run dev',
url: 'http://localhost:3003',
url: 'http://localhost:3690',
reuseExistingServer: true,
timeout: 120000,
timeout: 180000,
},
});
+59
View File
@@ -0,0 +1,59 @@
import { test, expect } from '@playwright/test';
test.describe('Authentication Pages', () => {
test('sign in page loads correctly', async ({ page }) => {
await page.goto('http://localhost:3690/signin');
// Check page title
await expect(page.locator('h1')).toContainText('Welcome back');
// Check form elements exist
await expect(page.locator('input[type="email"]')).toBeVisible();
await expect(page.locator('input[type="password"]')).toBeVisible();
await expect(page.locator('button[type="submit"]')).toContainText('Sign in');
// Check Google sign in button
await expect(page.locator('text=Sign in with Google')).toBeVisible();
// Check sign up link (there are two, check the one at the bottom)
await expect(page.getByRole('link', { name: 'Sign up' })).toBeVisible();
// Take screenshot
await page.screenshot({ path: 'tests/screenshots/signin-page.png' });
});
test('sign up page loads correctly', async ({ page }) => {
await page.goto('http://localhost:3690/signup');
// Check page title
await expect(page.locator('h1')).toContainText('Create your account');
// Check form elements exist
await expect(page.locator('input[name="email"]')).toBeVisible();
await expect(page.locator('input[name="password"]')).toBeVisible();
await expect(page.locator('input[name="confirmPassword"]')).toBeVisible();
await expect(page.locator('input[name="companyName"]')).toBeVisible();
await expect(page.locator('input[name="phone"]')).toBeVisible();
await expect(page.locator('button[type="submit"]')).toContainText('Create account');
// Check Google sign up button
await expect(page.locator('text=Sign up with Google')).toBeVisible();
// Check sign in link (use the one at the bottom)
await expect(page.getByRole('link', { name: 'Sign in', exact: true })).toBeVisible();
// Take screenshot
await page.screenshot({ path: 'tests/screenshots/signup-page.png' });
});
test('header shows sign in button when not authenticated', async ({ page }) => {
await page.goto('http://localhost:3690/');
// Check for Sign In button in header (use the first one - the button)
await expect(page.locator('header').getByRole('link', { name: 'Sign In' }).first()).toBeVisible();
// Click Sign In button should navigate to signin page
await page.locator('header').getByRole('link', { name: 'Sign In' }).first().click();
await expect(page).toHaveURL('http://localhost:3690/signin');
});
});
+57
View File
@@ -0,0 +1,57 @@
import { test } from '@playwright/test';
test('check console errors', async ({ page }) => {
// Collect all console messages
const consoleMessages: string[] = [];
page.on('console', msg => {
const type = msg.type();
const text = msg.text();
consoleMessages.push(`[${type}] ${text}`);
console.log(`Console [${type}]: ${text}`);
});
page.on('pageerror', error => {
console.log('Page error:', error.message);
consoleMessages.push(`[ERROR] ${error.message}`);
});
// Navigate to the page
console.log('Navigating to http://localhost:3690/signin');
const response = await page.goto('http://localhost:3690/signin', {
waitUntil: 'domcontentloaded'
});
console.log('Response status:', response?.status());
// Wait a bit for any async errors
await page.waitForTimeout(2000);
// Check if React root exists
const hasRoot = await page.evaluate(() => {
return document.getElementById('root') !== null;
});
console.log('Has root element:', hasRoot);
// Check if React is loaded
const hasReact = await page.evaluate(() => {
return typeof (window as any).React !== 'undefined';
});
console.log('React loaded:', hasReact);
// Get the HTML content
const html = await page.content();
console.log('HTML length:', html.length);
// Check for any script errors in the page
const errors = await page.evaluate(() => {
return (window as any).__errors || [];
});
if (errors.length > 0) {
console.log('Script errors:', errors);
}
console.log('\n=== All Console Messages ===');
consoleMessages.forEach(msg => console.log(msg));
});
+63
View File
@@ -0,0 +1,63 @@
import { test, expect } from '@playwright/test';
test.describe('Invoice Generation', () => {
test('should be able to access invoice tab in dashboard', async ({ page }) => {
// Navigate to dashboard (assuming user is already logged in)
await page.goto('/dashboard');
// Check if Invoice tab is visible
const invoiceTab = page.getByRole('button', { name: /Invoices/i });
await expect(invoiceTab).toBeVisible();
// Click on Invoice tab
await invoiceTab.click();
// Verify invoice section is displayed
await expect(page.getByText(/Orders & Invoices/i)).toBeVisible();
});
test('should display download button for paid orders', async ({ page }) => {
// This test assumes there are paid orders in the system
await page.goto('/dashboard');
// Click on Invoice tab
await page.getByRole('button', { name: /Invoices/i }).click();
// Check for presence of either Download or Generate Invoice button
const invoiceButtons = page.locator('button').filter({
hasText: /Download|Generate Invoice/i
});
const count = await invoiceButtons.count();
if (count > 0) {
// If there are orders, at least one button should be visible
await expect(invoiceButtons.first()).toBeVisible();
}
});
test('should show invoice preview modal when Preview button is clicked', async ({ page }) => {
await page.goto('/dashboard');
// Click on Invoice tab
await page.getByRole('button', { name: /Invoices/i }).click();
// Look for Preview button
const previewButton = page.getByRole('button', { name: /Preview/i }).first();
const buttonCount = await previewButton.count();
if (buttonCount > 0) {
// Click preview button
await previewButton.click();
// Check if modal appears
await expect(page.getByText(/Invoice Preview/i)).toBeVisible();
// Close modal
const closeButton = page.locator('button').filter({ has: page.locator('svg') }).last();
await closeButton.click();
// Verify modal is closed
await expect(page.getByText(/Invoice Preview/i)).not.toBeVisible();
}
});
});
+219
View File
@@ -0,0 +1,219 @@
import { test, expect } from '@playwright/test';
test.describe('Rate Limiting Tests', () => {
test('should enforce rate limits on API endpoints', async ({ request }) => {
const endpoint = '/api/send-email';
const payload = {
to: '[email protected]',
subject: 'Test Email',
body: 'This is a test email'
};
// Make requests up to the limit
const responses = [];
for (let i = 0; i < 12; i++) {
const response = await request.post(endpoint, {
data: payload,
headers: {
'Content-Type': 'application/json'
}
});
responses.push(response);
}
// First 10 should succeed (anonymous limit)
for (let i = 0; i < 10; i++) {
expect(responses[i].status()).toBeLessThan(429);
}
// 11th and 12th should be rate limited
expect(responses[10].status()).toBe(429);
expect(responses[11].status()).toBe(429);
// Check rate limit headers
const headers = responses[10].headers();
expect(headers['x-ratelimit-limit']).toBeDefined();
expect(headers['x-ratelimit-remaining']).toBe('0');
expect(headers['retry-after']).toBeDefined();
});
test('should have different limits for authenticated users', async ({ request }) => {
const endpoint = '/api/generate-invoice';
const authToken = 'test-auth-token'; // This would be a real token in production
// Make requests with authentication
const responses = [];
for (let i = 0; i < 25; i++) {
const response = await request.post(endpoint, {
data: { test: 'data' },
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`,
'X-User-Id': 'test-user-123',
'X-User-Role': 'authenticated'
}
});
responses.push(response);
}
// First 20 should succeed (authenticated limit for this endpoint)
for (let i = 0; i < 20; i++) {
expect(responses[i].status()).toBeLessThan(429);
}
// 21st onwards should be rate limited
for (let i = 20; i < 25; i++) {
expect(responses[i].status()).toBe(429);
}
});
test('should reset rate limits after window expires', async ({ request, page }) => {
const endpoint = '/api/send-email';
const payload = {
to: '[email protected]',
subject: 'Test Email',
body: 'This is a test email'
};
// Hit rate limit
for (let i = 0; i < 11; i++) {
await request.post(endpoint, { data: payload });
}
// Last request should be rate limited
const limitedResponse = await request.post(endpoint, { data: payload });
expect(limitedResponse.status()).toBe(429);
// Get retry-after value
const retryAfter = parseInt(limitedResponse.headers()['retry-after'] || '60');
// Wait for rate limit window to expire (in real tests, you'd mock time)
// For testing purposes, we'll skip waiting and just verify the headers exist
expect(retryAfter).toBeGreaterThan(0);
expect(retryAfter).toBeLessThanOrEqual(60);
});
test('should display rate limit message in UI', async ({ page }) => {
// Navigate to a page with API calls
await page.goto('/');
// Mock rate limit response
await page.route('/api/**', route => {
route.fulfill({
status: 429,
headers: {
'Content-Type': 'application/json',
'X-RateLimit-Limit': '10',
'X-RateLimit-Remaining': '0',
'X-RateLimit-Reset': String(Date.now() + 60000),
'Retry-After': '60'
},
body: JSON.stringify({
error: 'Too Many Requests',
message: 'Rate limit exceeded. Please try again in 60 seconds.',
retryAfter: 60
})
});
});
// Trigger an API call (this would be a real action in your app)
await page.evaluate(() => {
fetch('/api/send-email', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ to: '[email protected]', subject: 'Test', body: 'Test' })
});
});
// Check for rate limit message
await expect(page.locator('text=/Rate Limit Exceeded/i')).toBeVisible({ timeout: 5000 });
await expect(page.locator('text=/Please wait/i')).toBeVisible();
});
test('admin should be able to clear rate limits', async ({ request }) => {
const adminKey = process.env.ADMIN_API_KEY || 'test-admin-key';
// Clear rate limit for a user
const response = await request.post('/api/admin/clear-rate-limit', {
data: {
userId: 'test-user-123',
endpoint: 'api/send-email'
},
headers: {
'Content-Type': 'application/json',
'X-Admin-Key': adminKey
}
});
// Should succeed with proper admin key
if (adminKey !== 'test-admin-key') {
expect(response.status()).toBe(200);
const body = await response.json();
expect(body.success).toBe(true);
expect(body.message).toContain('Rate limit cleared');
} else {
// With test key, should be unauthorized
expect(response.status()).toBe(401);
}
});
test('should track rate limit violations in database', async ({ page }) => {
// This test would require database access
// In a real scenario, you'd check the rate_limit_violations table
// Navigate to admin dashboard
await page.goto('/admin/rate-limits');
// Check if violations tab exists
const violationsTab = page.locator('button:has-text("Violations")');
if (await violationsTab.isVisible()) {
await violationsTab.click();
// Check for violations table
await expect(page.locator('text=/Recent Violations/i')).toBeVisible();
}
});
});
test.describe('Rate Limit Dashboard Tests', () => {
test('should display rate limit statistics', async ({ page }) => {
// Navigate to admin dashboard
await page.goto('/admin/rate-limits');
// Check for main sections
await expect(page.locator('h1:has-text("Rate Limit Dashboard")')).toBeVisible();
// Check for summary cards
await expect(page.locator('text=/Total Requests/i')).toBeVisible();
await expect(page.locator('text=/Blocked Requests/i')).toBeVisible();
await expect(page.locator('text=/Active Users/i')).toBeVisible();
await expect(page.locator('text=/Avg Response Time/i')).toBeVisible();
});
test('should allow tab navigation', async ({ page }) => {
await page.goto('/admin/rate-limits');
// Test tab navigation
const tabs = ['overview', 'users', 'violations', 'rules'];
for (const tab of tabs) {
const tabButton = page.locator(`button:has-text("${tab}")`);
await tabButton.click();
// Verify tab is active
await expect(tabButton).toHaveClass(/border-blue-600/);
}
});
test('should refresh data on demand', async ({ page }) => {
await page.goto('/admin/rate-limits');
// Click refresh button
const refreshButton = page.locator('button:has-text("Refresh")');
await refreshButton.click();
// Check for loading state or updated data
// In a real test, you'd verify the data actually refreshed
await expect(refreshButton).toBeEnabled();
});
});
+31
View File
@@ -0,0 +1,31 @@
import { test, expect } from '@playwright/test';
test('sign in page renders', async ({ page }) => {
// Navigate to sign in page
await page.goto('http://localhost:3690/signin');
// Wait for network idle
await page.waitForLoadState('networkidle');
// Check console for errors
page.on('console', msg => {
if (msg.type() === 'error') {
console.log('Console error:', msg.text());
}
});
// Take a screenshot
await page.screenshot({ path: 'signin-debug.png', fullPage: true });
// Try to wait for any h1 element
const h1Count = await page.locator('h1').count();
console.log('Number of h1 elements found:', h1Count);
// Check if page has any content
const bodyText = await page.locator('body').textContent();
console.log('Body text length:', bodyText?.length);
// Check for any visible text
const hasContent = bodyText && bodyText.trim().length > 0;
expect(hasContent).toBeTruthy();
});