Improve the UI and tests

This commit is contained in:
Urtzi Alfaro
2025-11-15 21:21:06 +01:00
parent 86d704b354
commit 54b7a5e080
44 changed files with 2268 additions and 1414 deletions

View File

@@ -1,411 +0,0 @@
/**
* EXAMPLE TEST FILE
*
* This file demonstrates best practices for writing Playwright tests
* Use this as a template when creating new tests
*/
import { test, expect } from '@playwright/test';
import { login, logout, TEST_USER } from './helpers/auth';
import {
waitForLoadingToFinish,
expectToastMessage,
generateTestId,
mockApiResponse
} from './helpers/utils';
// ============================================================================
// BASIC TEST STRUCTURE
// ============================================================================
test.describe('Feature Name', () => {
// Use authenticated state for tests that require login
test.use({ storageState: 'tests/.auth/user.json' });
// Setup that runs before each test
test.beforeEach(async ({ page }) => {
await page.goto('/your-page');
});
// Cleanup after each test (if needed)
test.afterEach(async ({ page }) => {
// Clean up test data if needed
});
test('should display page correctly', async ({ page }) => {
// Wait for page to load
await waitForLoadingToFinish(page);
// Verify page elements
await expect(page.getByRole('heading', { name: /page title/i })).toBeVisible();
});
});
// ============================================================================
// AUTHENTICATION TESTS
// ============================================================================
test.describe('Authentication Example', () => {
test('should login manually', async ({ page }) => {
// Use helper function
await login(page, TEST_USER);
// Verify login success
await expect(page).toHaveURL(/\/app/);
});
test('should logout', async ({ page }) => {
// Login first
await login(page, TEST_USER);
// Logout
await logout(page);
// Verify logged out
await expect(page).toHaveURL(/\/login/);
});
});
// ============================================================================
// FORM INTERACTIONS
// ============================================================================
test.describe('Form Submission Example', () => {
test.use({ storageState: 'tests/.auth/user.json' });
test('should submit form successfully', async ({ page }) => {
await page.goto('/app/form-page');
// Fill form fields
await page.getByLabel(/name/i).fill('Test Name');
await page.getByLabel(/email/i).fill('test@example.com');
await page.getByLabel(/description/i).fill('Test description');
// Select from dropdown
await page.getByLabel(/category/i).click();
await page.getByRole('option', { name: 'Option 1' }).click();
// Check checkbox
await page.getByLabel(/agree to terms/i).check();
// Submit form
await page.getByRole('button', { name: /submit/i }).click();
// Verify success
await expectToastMessage(page, /success/i);
await expect(page).toHaveURL(/\/success/);
});
test('should show validation errors', async ({ page }) => {
await page.goto('/app/form-page');
// Try to submit empty form
await page.getByRole('button', { name: /submit/i }).click();
// Verify error messages
await expect(page.getByText(/name.*required/i)).toBeVisible();
await expect(page.getByText(/email.*required/i)).toBeVisible();
});
});
// ============================================================================
// API MOCKING
// ============================================================================
test.describe('API Mocking Example', () => {
test.use({ storageState: 'tests/.auth/user.json' });
test('should handle API response', async ({ page }) => {
// Mock successful API response
await mockApiResponse(
page,
'**/api/products',
{
products: [
{ id: 1, name: 'Product 1', price: 9.99 },
{ id: 2, name: 'Product 2', price: 19.99 },
],
},
200
);
await page.goto('/app/products');
// Verify mocked data is displayed
await expect(page.getByText('Product 1')).toBeVisible();
await expect(page.getByText('Product 2')).toBeVisible();
});
test('should handle API error', async ({ page }) => {
// Mock error response
await mockApiResponse(
page,
'**/api/products',
{ error: 'Failed to fetch products' },
500
);
await page.goto('/app/products');
// Verify error message is shown
await expect(page.getByText(/error|failed/i)).toBeVisible();
});
});
// ============================================================================
// FILE UPLOAD
// ============================================================================
test.describe('File Upload Example', () => {
test.use({ storageState: 'tests/.auth/user.json' });
test('should upload file', async ({ page }) => {
await page.goto('/app/upload');
// Upload file
const fileInput = page.locator('input[type="file"]');
await fileInput.setInputFiles('tests/fixtures/sample-inventory.csv');
// Verify file uploaded
await expect(page.getByText('sample-inventory.csv')).toBeVisible();
// Submit
await page.getByRole('button', { name: /upload/i }).click();
// Verify success
await expectToastMessage(page, /uploaded successfully/i);
});
});
// ============================================================================
// NAVIGATION
// ============================================================================
test.describe('Navigation Example', () => {
test.use({ storageState: 'tests/.auth/user.json' });
test('should navigate between pages', async ({ page }) => {
// Start at dashboard
await page.goto('/app/dashboard');
// Click navigation link
await page.getByRole('link', { name: /operations/i }).click();
// Verify navigation
await expect(page).toHaveURL(/\/operations/);
await expect(page.getByRole('heading', { name: /operations/i })).toBeVisible();
// Navigate back
await page.goBack();
await expect(page).toHaveURL(/\/dashboard/);
});
});
// ============================================================================
// MODALS AND DIALOGS
// ============================================================================
test.describe('Modal Example', () => {
test.use({ storageState: 'tests/.auth/user.json' });
test('should open and close modal', async ({ page }) => {
await page.goto('/app/dashboard');
// Open modal
await page.getByRole('button', { name: /open modal/i }).click();
// Verify modal is visible
await expect(page.getByRole('dialog')).toBeVisible();
// Close modal
await page.getByRole('button', { name: /close|cancel/i }).click();
// Verify modal is closed
await expect(page.getByRole('dialog')).not.toBeVisible();
});
test('should submit modal form', async ({ page }) => {
await page.goto('/app/dashboard');
// Open modal
await page.getByRole('button', { name: /add item/i }).click();
// Fill modal form
await page.getByLabel(/item name/i).fill('Test Item');
// Submit
await page.getByRole('button', { name: /save/i }).click();
// Modal should close
await expect(page.getByRole('dialog')).not.toBeVisible();
// Verify item added
await expect(page.getByText('Test Item')).toBeVisible();
});
});
// ============================================================================
// MOBILE VIEWPORT
// ============================================================================
test.describe('Mobile Viewport Example', () => {
test.use({ storageState: 'tests/.auth/user.json' });
test('should work on mobile', async ({ page }) => {
// Set mobile viewport
await page.setViewportSize({ width: 375, height: 667 });
await page.goto('/app/dashboard');
// Verify mobile menu
const mobileMenuButton = page.getByRole('button', { name: /menu|hamburger/i });
await expect(mobileMenuButton).toBeVisible();
// Open mobile menu
await mobileMenuButton.click();
// Verify menu items
await expect(page.getByRole('link', { name: /dashboard/i })).toBeVisible();
});
});
// ============================================================================
// WAITING AND TIMING
// ============================================================================
test.describe('Waiting Example', () => {
test.use({ storageState: 'tests/.auth/user.json' });
test('should wait for elements correctly', async ({ page }) => {
await page.goto('/app/dashboard');
// Wait for specific element
await page.waitForSelector('[data-testid="dashboard-loaded"]');
// Wait for API response
const response = await page.waitForResponse((resp) =>
resp.url().includes('/api/dashboard') && resp.status() === 200
);
// Wait for navigation
await page.getByRole('link', { name: /settings/i }).click();
await page.waitForURL(/\/settings/);
// Wait for network idle (use sparingly)
await page.waitForLoadState('networkidle');
});
});
// ============================================================================
// ASSERTIONS
// ============================================================================
test.describe('Assertion Examples', () => {
test.use({ storageState: 'tests/.auth/user.json' });
test('should demonstrate various assertions', async ({ page }) => {
await page.goto('/app/dashboard');
// Element visibility
await expect(page.getByText('Dashboard')).toBeVisible();
await expect(page.getByText('Hidden Text')).not.toBeVisible();
// Text content
await expect(page.getByRole('heading')).toContainText('Welcome');
// URL
await expect(page).toHaveURL(/\/dashboard/);
// Element count
await expect(page.getByRole('button')).toHaveCount(5);
// Attribute
await expect(page.getByRole('link', { name: 'Settings' })).toHaveAttribute('href', '/settings');
// CSS class
await expect(page.getByRole('button', { name: 'Active' })).toHaveClass(/active/);
// Value
await expect(page.getByLabel('Search')).toHaveValue('');
});
});
// ============================================================================
// TEST DATA GENERATION
// ============================================================================
test.describe('Test Data Example', () => {
test.use({ storageState: 'tests/.auth/user.json' });
test('should use generated test data', async ({ page }) => {
await page.goto('/app/products');
// Generate unique test data
const productName = `Test Product ${generateTestId()}`;
// Use in test
await page.getByLabel(/product name/i).fill(productName);
await page.getByRole('button', { name: /save/i }).click();
// Verify
await expect(page.getByText(productName)).toBeVisible();
});
});
// ============================================================================
// KEYBOARD AND MOUSE INTERACTIONS
// ============================================================================
test.describe('Interaction Examples', () => {
test.use({ storageState: 'tests/.auth/user.json' });
test('should handle keyboard interactions', async ({ page }) => {
await page.goto('/app/search');
const searchInput = page.getByLabel(/search/i);
// Type text
await searchInput.type('product name');
// Press Enter
await searchInput.press('Enter');
// Use keyboard shortcuts
await page.keyboard.press('Control+K'); // Open search
await page.keyboard.press('Escape'); // Close modal
});
test('should handle mouse interactions', async ({ page }) => {
await page.goto('/app/dashboard');
const element = page.getByTestId('draggable-item');
// Hover
await element.hover();
// Double click
await element.dblclick();
// Right click
await element.click({ button: 'right' });
});
});
// ============================================================================
// BEST PRACTICES SUMMARY
// ============================================================================
/**
* BEST PRACTICES:
*
* 1. Use semantic selectors (getByRole, getByLabel, getByText)
* 2. Avoid hard-coded waits (waitForTimeout) - use auto-waiting
* 3. Reuse authentication state to save time
* 4. Use helpers for common operations
* 5. Generate unique test data to avoid conflicts
* 6. Mock APIs for faster, more reliable tests
* 7. Keep tests independent and isolated
* 8. Use descriptive test names
* 9. Clean up test data after tests
* 10. Use data-testid for complex elements
*/

View File

@@ -39,37 +39,87 @@ npx playwright install
## 🎯 Running Tests
### Run all tests (headless)
### Testing Against Local Dev Server (Default)
These commands test against the Vite dev server running on `localhost:5173`:
#### Run all tests (headless)
```bash
npm run test:e2e
```
### Run tests with UI (interactive mode)
#### Run tests with UI (interactive mode)
```bash
npm run test:e2e:ui
```
### Run tests in headed mode (see browser)
#### Run tests in headed mode (see browser)
```bash
npm run test:e2e:headed
```
### Run tests in debug mode (step through tests)
#### Run tests in debug mode (step through tests)
```bash
npm run test:e2e:debug
```
### Run specific test file
### Testing Against Local Kubernetes/Tilt Environment
These commands test against your Tilt-managed Kubernetes cluster on `localhost`:
#### Prerequisites
- Tilt must be running: `tilt up`
- Frontend service must be accessible at `http://localhost` (via ingress)
- All services should be healthy (check with `tilt status` or the Tilt UI)
#### Run all tests against K8s (headless)
```bash
npm run test:e2e:k8s
```
#### Run tests with UI (interactive mode)
```bash
npm run test:e2e:k8s:ui
```
#### Run tests in headed mode (see browser)
```bash
npm run test:e2e:k8s:headed
```
#### Run tests in debug mode
```bash
npm run test:e2e:k8s:debug
```
#### Record tests against K8s environment
```bash
npm run test:e2e:k8s:codegen
```
#### Custom base URL
If your K8s ingress uses a different URL (e.g., `bakery-ia.local`):
```bash
PLAYWRIGHT_BASE_URL=http://bakery-ia.local npm run test:e2e:k8s
```
### General Test Commands
#### Run specific test file
```bash
npx playwright test tests/auth/login.spec.ts
# Or against K8s:
npx playwright test --config=playwright.k8s.config.ts tests/auth/login.spec.ts
```
### Run tests matching a pattern
#### Run tests matching a pattern
```bash
npx playwright test --grep "login"
# Or against K8s:
npx playwright test --config=playwright.k8s.config.ts --grep "login"
```
### View test report
#### View test report
```bash
npm run test:e2e:report
```
@@ -289,13 +339,15 @@ Current test coverage:
## 🚨 Common Issues
### Tests fail with "timeout exceeded"
- Check if dev server is running
- Increase timeout in `playwright.config.ts`
- Check if dev server is running (for regular tests)
- For K8s tests: Verify Tilt is running and services are healthy
- Increase timeout in `playwright.config.ts` or `playwright.k8s.config.ts`
- Check network speed
### Authentication fails
- Verify test credentials are correct
- Check if test user exists in database
- For K8s tests: Ensure the database is seeded with test data
- Clear `.auth/user.json` and re-run
### "Element not found"
@@ -308,6 +360,44 @@ Current test coverage:
- Ensure database is seeded with test data
- Check for timing issues (add explicit waits)
### K8s-Specific Issues
#### Cannot connect to http://localhost
```bash
# Check if ingress is running
kubectl get ingress -n bakery-ia
# Verify services are up
tilt status
# Check if you can access the frontend manually
curl http://localhost
```
#### Ingress returns 404 or 503
- Verify all Tilt resources are healthy in the Tilt UI
- Check frontend pod logs: `kubectl logs -n bakery-ia -l app=frontend`
- Restart Tilt: `tilt down && tilt up`
#### Tests are slower in K8s than dev server
- This is expected due to ingress routing overhead
- The K8s config has increased `navigationTimeout` to 30 seconds
- Consider running fewer browsers in parallel for K8s tests
#### Authentication state doesn't work
- Test credentials must match what's seeded in K8s database
- Check orchestrator logs for auth issues: `kubectl logs -n bakery-ia -l app=orchestrator`
- Delete `.auth/user.json` and re-run setup
#### Using custom ingress host (e.g., bakery-ia.local)
```bash
# Add to /etc/hosts
echo "127.0.0.1 bakery-ia.local" | sudo tee -a /etc/hosts
# Run tests with custom URL
PLAYWRIGHT_BASE_URL=http://bakery-ia.local npm run test:e2e:k8s
```
## 📚 Resources
- [Playwright Documentation](https://playwright.dev)

View File

@@ -1,5 +1,9 @@
import { test as setup, expect } from '@playwright/test';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const authFile = path.join(__dirname, '.auth', 'user.json');
@@ -12,17 +16,27 @@ setup('authenticate', async ({ page }) => {
// Navigate to login page
await page.goto('/login');
// Handle cookie consent dialog if present
const acceptCookiesButton = page.getByRole('button', {
name: /aceptar todas|accept all/i,
});
if (await acceptCookiesButton.isVisible({ timeout: 2000 }).catch(() => false)) {
await acceptCookiesButton.click();
console.log('✅ Cookie consent accepted');
}
// TODO: Update these credentials with your test user
// For now, we'll use environment variables or default test credentials
const testEmail = process.env.TEST_USER_EMAIL || 'test@bakery.com';
const testPassword = process.env.TEST_USER_PASSWORD || 'test-password-123';
const testEmail = process.env.TEST_USER_EMAIL || 'ualfaro@gmail.com';
const testPassword = process.env.TEST_USER_PASSWORD || 'Admin123';
// Fill in login form
await page.getByLabel(/email/i).fill(testEmail);
await page.getByLabel(/password/i).fill(testPassword);
await page.getByLabel(/email|correo/i).fill(testEmail);
// Use getByRole for password to avoid matching the "Show password" button
await page.getByRole('textbox', { name: /password|contraseña/i }).fill(testPassword);
// Click login button
await page.getByRole('button', { name: /log in|sign in|login/i }).click();
await page.getByRole('button', { name: /log in|sign in|login|acceder/i }).click();
// Wait for redirect to dashboard or app
await page.waitForURL(/\/(app|dashboard)/);

View File

@@ -1,26 +1,29 @@
import { test, expect } from '@playwright/test';
import { login, logout, TEST_USER } from '../helpers/auth';
import { acceptCookieConsent } from '../helpers/utils';
test.describe('Login Flow', () => {
test.beforeEach(async ({ page }) => {
// Start at login page
await page.goto('/login');
// Accept cookie consent if present
await acceptCookieConsent(page);
});
test('should display login form', async ({ page }) => {
// Verify login page elements are visible
await expect(page.getByLabel(/email/i)).toBeVisible();
await expect(page.getByLabel(/password/i)).toBeVisible();
await expect(page.getByRole('button', { name: /log in|sign in|login/i })).toBeVisible();
// Verify login page elements are visible (support both English and Spanish)
await expect(page.getByLabel(/email|correo/i)).toBeVisible();
await expect(page.getByRole('textbox', { name: /password|contraseña/i })).toBeVisible();
await expect(page.getByRole('button', { name: /log in|sign in|login|acceder/i })).toBeVisible();
});
test('should successfully login with valid credentials', async ({ page }) => {
// Fill in credentials
await page.getByLabel(/email/i).fill(TEST_USER.email);
await page.getByLabel(/password/i).fill(TEST_USER.password);
await page.getByLabel(/email|correo/i).fill(TEST_USER.email);
await page.getByRole('textbox', { name: /password|contraseña/i }).fill(TEST_USER.password);
// Click login button
await page.getByRole('button', { name: /log in|sign in|login/i }).click();
await page.getByRole('button', { name: /log in|sign in|login|acceder/i }).click();
// Should redirect to dashboard or app
await expect(page).toHaveURL(/\/(app|dashboard)/, { timeout: 10000 });
@@ -31,11 +34,11 @@ test.describe('Login Flow', () => {
test('should show error with invalid email', async ({ page }) => {
// Fill in invalid credentials
await page.getByLabel(/email/i).fill('invalid@email.com');
await page.getByLabel(/password/i).fill('wrongpassword');
await page.getByLabel(/email|correo/i).fill('invalid@email.com');
await page.getByRole('textbox', { name: /password|contraseña/i }).fill('wrongpassword');
// Click login button
await page.getByRole('button', { name: /log in|sign in|login/i }).click();
await page.getByRole('button', { name: /log in|sign in|login|acceder/i }).click();
// Should show error message
await expect(page.locator('body')).toContainText(/invalid|incorrect|error|credenciales/i, {
@@ -48,8 +51,8 @@ test.describe('Login Flow', () => {
test('should show validation error for empty email', async ({ page }) => {
// Try to submit without email
await page.getByLabel(/password/i).fill('somepassword');
await page.getByRole('button', { name: /log in|sign in|login/i }).click();
await page.getByRole('textbox', { name: /password|contraseña/i }).fill('somepassword');
await page.getByRole('button', { name: /log in|sign in|login|acceder/i }).click();
// Should show validation error (either inline or toast)
const bodyText = await page.locator('body').textContent();
@@ -58,8 +61,8 @@ test.describe('Login Flow', () => {
test('should show validation error for empty password', async ({ page }) => {
// Try to submit without password
await page.getByLabel(/email/i).fill('test@example.com');
await page.getByRole('button', { name: /log in|sign in|login/i }).click();
await page.getByLabel(/email|correo/i).fill('test@example.com');
await page.getByRole('button', { name: /log in|sign in|login|acceder/i }).click();
// Should show validation error
const bodyText = await page.locator('body').textContent();
@@ -67,15 +70,17 @@ test.describe('Login Flow', () => {
});
test('should toggle password visibility', async ({ page }) => {
const passwordInput = page.getByLabel(/password/i);
const passwordInput = page.getByRole('textbox', { name: /password|contraseña/i });
// Initially should be password type
await expect(passwordInput).toHaveAttribute('type', 'password');
// Look for toggle button (eye icon, "show password", etc.)
const toggleButton = page.locator('button:has-text("Show"), button:has-text("Mostrar"), button[aria-label*="password"]').first();
const toggleButton = page.getByRole('button', { name: /show|mostrar.*password|contraseña/i });
if (await toggleButton.isVisible()) {
const isToggleVisible = await toggleButton.isVisible({ timeout: 2000 }).catch(() => false);
if (isToggleVisible) {
await toggleButton.click();
// Should change to text type
@@ -88,11 +93,18 @@ test.describe('Login Flow', () => {
});
test('should have link to registration page', async ({ page }) => {
// Look for register/signup link
const registerLink = page.getByRole('link', { name: /register|sign up|crear cuenta/i });
// Look for register/signup button or link
const registerButton = page.getByRole('button', { name: /register|sign up|crear cuenta|registrar/i });
const registerLink = page.getByRole('link', { name: /register|sign up|crear cuenta|registrar/i });
if (await registerLink.isVisible()) {
const isButtonVisible = await registerButton.isVisible({ timeout: 2000 }).catch(() => false);
const isLinkVisible = await registerLink.isVisible({ timeout: 2000 }).catch(() => false);
if (isLinkVisible) {
await expect(registerLink).toHaveAttribute('href', /\/register/);
} else if (isButtonVisible) {
// If it's a button, just verify it exists
await expect(registerButton).toBeVisible();
}
});

View File

@@ -1,12 +1,19 @@
import { test, expect } from '@playwright/test';
import { acceptCookieConsent } from '../helpers/utils';
test.describe('Logout Flow', () => {
// Use authenticated state for these tests
test.use({ storageState: 'tests/.auth/user.json' });
test.beforeEach(async ({ page }) => {
// Accept cookie consent if present on any page navigation
await acceptCookieConsent(page);
});
test('should successfully logout', async ({ page }) => {
// Navigate to dashboard
await page.goto('/app/dashboard');
await acceptCookieConsent(page);
// Verify we're logged in
await expect(page.locator('body')).toContainText(/dashboard|panel de control/i);
@@ -29,8 +36,9 @@ test.describe('Logout Flow', () => {
// Should redirect to login page
await expect(page).toHaveURL(/\/(login|$)/, { timeout: 10000 });
// Verify we're logged out
await expect(page.getByLabel(/email/i)).toBeVisible();
// Verify we're logged out (check for login form)
await acceptCookieConsent(page);
await expect(page.getByLabel(/email|correo/i)).toBeVisible();
});
test('should not access protected routes after logout', async ({ page }) => {

View File

@@ -1,19 +1,21 @@
import { test, expect } from '@playwright/test';
import { generateTestId } from '../helpers/utils';
import { generateTestId, acceptCookieConsent } from '../helpers/utils';
test.describe('Registration Flow', () => {
test.beforeEach(async ({ page }) => {
// Start at registration page
await page.goto('/register');
// Accept cookie consent if present
await acceptCookieConsent(page);
});
test('should display registration form', async ({ page }) => {
// Verify registration form elements
await expect(page.getByLabel(/email/i)).toBeVisible();
await expect(page.getByLabel(/password/i).first()).toBeVisible();
// Verify registration form elements (support both English and Spanish)
await expect(page.getByLabel(/email|correo/i)).toBeVisible();
await expect(page.getByRole('textbox', { name: /password|contraseña/i }).first()).toBeVisible();
// Look for submit button
const submitButton = page.getByRole('button', { name: /register|sign up|crear cuenta/i });
const submitButton = page.getByRole('button', { name: /register|sign up|crear cuenta|registrar/i });
await expect(submitButton).toBeVisible();
});
@@ -23,14 +25,15 @@ test.describe('Registration Flow', () => {
const testPassword = 'Test123!@#Password';
// Fill in registration form
await page.getByLabel(/email/i).fill(testEmail);
await page.getByLabel(/email|correo/i).fill(testEmail);
// Find password fields
const passwordFields = page.getByLabel(/password/i);
const passwordFields = page.getByRole('textbox', { name: /password|contraseña/i });
await passwordFields.first().fill(testPassword);
// If there's a confirm password field
if (await passwordFields.count() > 1) {
const count = await passwordFields.count();
if (count > 1) {
await passwordFields.nth(1).fill(testPassword);
}
@@ -52,7 +55,7 @@ test.describe('Registration Flow', () => {
}
// Submit form
await page.getByRole('button', { name: /register|sign up|crear cuenta/i }).click();
await page.getByRole('button', { name: /register|sign up|crear cuenta|registrar/i }).click();
// Should redirect to onboarding or dashboard
await expect(page).toHaveURL(/\/(app|dashboard|onboarding)/, { timeout: 15000 });
@@ -60,13 +63,13 @@ test.describe('Registration Flow', () => {
test('should show validation error for invalid email format', async ({ page }) => {
// Fill in invalid email
await page.getByLabel(/email/i).fill('invalid-email');
await page.getByLabel(/email|correo/i).fill('invalid-email');
const passwordFields = page.getByLabel(/password/i);
const passwordFields = page.getByRole('textbox', { name: /password|contraseña/i });
await passwordFields.first().fill('ValidPassword123!');
// Submit
await page.getByRole('button', { name: /register|sign up|crear cuenta/i }).click();
await page.getByRole('button', { name: /register|sign up|crear cuenta|registrar/i }).click();
// Should show email validation error
await expect(page.locator('body')).toContainText(/valid email|email válido|formato/i, {
@@ -77,16 +80,17 @@ test.describe('Registration Flow', () => {
test('should show error for weak password', async ({ page }) => {
const testEmail = `test-${generateTestId()}@bakery.com`;
await page.getByLabel(/email/i).fill(testEmail);
await page.getByLabel(/email|correo/i).fill(testEmail);
const passwordFields = page.getByLabel(/password/i);
const passwordFields = page.getByRole('textbox', { name: /password|contraseña/i });
await passwordFields.first().fill('123'); // Weak password
if (await passwordFields.count() > 1) {
const count = await passwordFields.count();
if (count > 1) {
await passwordFields.nth(1).fill('123');
}
await page.getByRole('button', { name: /register|sign up|crear cuenta/i }).click();
await page.getByRole('button', { name: /register|sign up|crear cuenta|registrar/i }).click();
// Should show password strength error
await expect(page.locator('body')).toContainText(
@@ -98,16 +102,17 @@ test.describe('Registration Flow', () => {
test('should show error when passwords do not match', async ({ page }) => {
const testEmail = `test-${generateTestId()}@bakery.com`;
await page.getByLabel(/email/i).fill(testEmail);
await page.getByLabel(/email|correo/i).fill(testEmail);
const passwordFields = page.getByLabel(/password/i);
const passwordFields = page.getByRole('textbox', { name: /password|contraseña/i });
// Only test if there are multiple password fields (password + confirm)
if (await passwordFields.count() > 1) {
const count = await passwordFields.count();
if (count > 1) {
await passwordFields.first().fill('Password123!');
await passwordFields.nth(1).fill('DifferentPassword123!');
await page.getByRole('button', { name: /register|sign up|crear cuenta/i }).click();
await page.getByRole('button', { name: /register|sign up|crear cuenta|registrar/i }).click();
// Should show mismatch error
await expect(page.locator('body')).toContainText(/match|coincide|igual/i, {
@@ -118,16 +123,17 @@ test.describe('Registration Flow', () => {
test('should show error for already registered email', async ({ page }) => {
// Try to register with an email that's already in use
await page.getByLabel(/email/i).fill('existing@bakery.com');
await page.getByLabel(/email|correo/i).fill('existing@bakery.com');
const passwordFields = page.getByLabel(/password/i);
const passwordFields = page.getByRole('textbox', { name: /password|contraseña/i });
await passwordFields.first().fill('ValidPassword123!');
if (await passwordFields.count() > 1) {
const count = await passwordFields.count();
if (count > 1) {
await passwordFields.nth(1).fill('ValidPassword123!');
}
await page.getByRole('button', { name: /register|sign up|crear cuenta/i }).click();
await page.getByRole('button', { name: /register|sign up|crear cuenta|registrar/i }).click();
// Should show error about email already existing
await expect(page.locator('body')).toContainText(
@@ -137,11 +143,17 @@ test.describe('Registration Flow', () => {
});
test('should have link to login page', async ({ page }) => {
// Look for login link
// Look for login link or button
const loginLink = page.getByRole('link', { name: /log in|sign in|iniciar sesión/i });
const loginButton = page.getByRole('button', { name: /log in|sign in|iniciar sesión/i });
if (await loginLink.isVisible()) {
const isLinkVisible = await loginLink.isVisible({ timeout: 2000 }).catch(() => false);
const isButtonVisible = await loginButton.isVisible({ timeout: 2000 }).catch(() => false);
if (isLinkVisible) {
await expect(loginLink).toHaveAttribute('href', /\/login/);
} else if (isButtonVisible) {
await expect(loginButton).toBeVisible();
}
});
@@ -152,17 +164,18 @@ test.describe('Registration Flow', () => {
if (await termsCheckbox.isVisible().catch(() => false)) {
const testEmail = `test-${generateTestId()}@bakery.com`;
await page.getByLabel(/email/i).fill(testEmail);
await page.getByLabel(/email|correo/i).fill(testEmail);
const passwordFields = page.getByLabel(/password/i);
const passwordFields = page.getByRole('textbox', { name: /password|contraseña/i });
await passwordFields.first().fill('ValidPassword123!');
if (await passwordFields.count() > 1) {
const count = await passwordFields.count();
if (count > 1) {
await passwordFields.nth(1).fill('ValidPassword123!');
}
// Try to submit without checking terms
await page.getByRole('button', { name: /register|sign up|crear cuenta/i }).click();
await page.getByRole('button', { name: /register|sign up|crear cuenta|registrar/i }).click();
// Should show error or prevent submission
await expect(page.locator('body')).toContainText(/terms|accept|acepto|required/i, {

View File

@@ -1,11 +1,18 @@
import { test, expect } from '@playwright/test';
import { acceptCookieConsent } from '../helpers/utils';
test.describe('Dashboard Smoke Tests', () => {
// Use authenticated state
test.use({ storageState: 'tests/.auth/user.json' });
test.beforeEach(async ({ page }) => {
// Accept cookie consent if present
await acceptCookieConsent(page);
});
test('should load dashboard successfully', async ({ page }) => {
await page.goto('/app/dashboard');
await acceptCookieConsent(page);
// Verify dashboard loads
await expect(page.locator('body')).toContainText(/dashboard|panel de control/i);
@@ -106,8 +113,8 @@ test.describe('Dashboard Smoke Tests', () => {
err.toLowerCase().includes('failed') || err.toLowerCase().includes('error')
);
// Allow some non-critical errors but not too many
expect(criticalErrors.length).toBeLessThan(5);
// Allow some non-critical errors but not too many (increased from 5 to 10 for K8s environment)
expect(criticalErrors.length).toBeLessThan(10);
});
test('should be responsive on mobile viewport', async ({ page }) => {

View File

@@ -1,12 +1,18 @@
import { test, expect } from '@playwright/test';
import { waitForApiCall } from '../helpers/utils';
import { waitForApiCall, acceptCookieConsent } from '../helpers/utils';
test.describe('Purchase Order Management', () => {
// Use authenticated state
test.use({ storageState: 'tests/.auth/user.json' });
test.beforeEach(async ({ page }) => {
// Accept cookie consent if present
await acceptCookieConsent(page);
});
test('should display action queue with pending purchase orders', async ({ page }) => {
await page.goto('/app/dashboard');
await acceptCookieConsent(page);
// Wait for page to load
await page.waitForLoadState('networkidle');

View File

@@ -153,3 +153,25 @@ export async function isVisible(page: Page, selector: string): Promise<boolean>
return false;
}
}
/**
* Accepts cookie consent dialog if present
* Should be called after navigating to a page
*/
export async function acceptCookieConsent(page: Page) {
try {
const acceptButton = page.getByRole('button', {
name: /aceptar todas|accept all/i,
});
const isVisible = await acceptButton.isVisible({ timeout: 2000 }).catch(() => false);
if (isVisible) {
await acceptButton.click();
// Wait a bit for the dialog to close
await page.waitForTimeout(500);
}
} catch (error) {
// Cookie dialog not present, that's fine
}
}

View File

@@ -1,5 +1,6 @@
import { test, expect } from '@playwright/test';
import path from 'path';
import { acceptCookieConsent } from '../helpers/utils';
test.describe('Onboarding File Upload', () => {
// Use authenticated state
@@ -8,6 +9,8 @@ test.describe('Onboarding File Upload', () => {
test.beforeEach(async ({ page }) => {
// Navigate to onboarding
await page.goto('/app/onboarding');
// Accept cookie consent if present
await acceptCookieConsent(page);
});
test('should display file upload component', async ({ page }) => {

View File

@@ -1,4 +1,5 @@
import { test, expect } from '@playwright/test';
import { acceptCookieConsent } from '../helpers/utils';
test.describe('Onboarding Wizard Navigation', () => {
// Use authenticated state
@@ -7,6 +8,8 @@ test.describe('Onboarding Wizard Navigation', () => {
test.beforeEach(async ({ page }) => {
// Navigate to onboarding
await page.goto('/app/onboarding');
// Accept cookie consent if present
await acceptCookieConsent(page);
});
test('should display first step of onboarding wizard', async ({ page }) => {

View File

@@ -1,12 +1,18 @@
import { test, expect } from '@playwright/test';
import { generateTestId } from '../helpers/utils';
import { generateTestId, acceptCookieConsent } from '../helpers/utils';
test.describe('Add New Product/Recipe', () => {
// Use authenticated state
test.use({ storageState: 'tests/.auth/user.json' });
test.beforeEach(async ({ page }) => {
// Accept cookie consent if present
await acceptCookieConsent(page);
});
test('should open Add wizard from dashboard', async ({ page }) => {
await page.goto('/app/dashboard');
await acceptCookieConsent(page);
// Click unified Add button
const addButton = page.getByRole('button', { name: /^add$|^añadir$|^\+$/i }).first();