mirror of
https://github.com/hanzoai/computer.git
synced 2026-09-30 20:55:46 +00:00
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:
+1
-1
@@ -26,5 +26,5 @@ dist-ssr
|
||||
pnpm-lock.yaml
|
||||
playwright-report/
|
||||
test-results/
|
||||
tests/
|
||||
tests/screenshots/
|
||||
*.png
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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));
|
||||
});
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
Reference in New Issue
Block a user