Add frontend testing - Playwright

This commit is contained in:
Urtzi Alfaro
2025-11-14 07:46:29 +01:00
parent a8d8828935
commit 4215026d61
21 changed files with 3071 additions and 0 deletions

View File

@@ -0,0 +1,78 @@
import { Page, expect } from '@playwright/test';
/**
* Authentication helper functions for Playwright tests
*/
export interface LoginCredentials {
email: string;
password: string;
}
/**
* Logs in a user manually (use this for tests that need fresh login)
* For most tests, use the saved auth state instead
*/
export async function login(page: Page, credentials: LoginCredentials) {
await page.goto('/login');
await page.getByLabel(/email/i).fill(credentials.email);
await page.getByLabel(/password/i).fill(credentials.password);
await page.getByRole('button', { name: /log in|sign in|login/i }).click();
// Wait for navigation to complete
await page.waitForURL(/\/(app|dashboard)/);
// Verify login success
await expect(page.locator('body')).toContainText(/dashboard|panel de control/i);
}
/**
* Logs out the current user
*/
export async function logout(page: Page) {
// Look for user menu or logout button
// Adjust selectors based on your actual app structure
const userMenuButton = page.getByRole('button', { name: /user|account|profile/i });
if (await userMenuButton.isVisible()) {
await userMenuButton.click();
}
// Click logout
await page.getByRole('button', { name: /log out|logout|sign out/i }).click();
// Verify we're logged out
await expect(page).toHaveURL(/\/(login|$)/);
}
/**
* Verifies that the user is authenticated
*/
export async function verifyAuthenticated(page: Page) {
// Check for authenticated state indicators
await expect(page.locator('body')).toContainText(/dashboard|panel de control/i);
}
/**
* Verifies that the user is NOT authenticated
*/
export async function verifyNotAuthenticated(page: Page) {
// Should redirect to login if not authenticated
await expect(page).toHaveURL(/\/login/);
}
/**
* Default test credentials
* Override with environment variables for CI/CD
*/
export const TEST_USER: LoginCredentials = {
email: process.env.TEST_USER_EMAIL || 'test@bakery.com',
password: process.env.TEST_USER_PASSWORD || 'test-password-123',
};
export const ADMIN_USER: LoginCredentials = {
email: process.env.ADMIN_USER_EMAIL || 'admin@bakery.com',
password: process.env.ADMIN_USER_PASSWORD || 'admin-password-123',
};

View File

@@ -0,0 +1,155 @@
import { Page, expect } from '@playwright/test';
/**
* General utility functions for Playwright tests
*/
/**
* Waits for the loading spinner to disappear
*/
export async function waitForLoadingToFinish(page: Page) {
// Adjust selectors based on your loading indicators
await page.waitForSelector('[data-testid="loading"], .loading, .spinner', {
state: 'hidden',
timeout: 10000,
}).catch(() => {
// If no loading indicator found, that's fine
});
}
/**
* Waits for an API call to complete
*/
export async function waitForApiCall(page: Page, urlPattern: string | RegExp) {
return page.waitForResponse(
(response) => {
const url = response.url();
if (typeof urlPattern === 'string') {
return url.includes(urlPattern);
}
return urlPattern.test(url);
},
{ timeout: 15000 }
);
}
/**
* Mocks an API endpoint with a custom response
*/
export async function mockApiResponse(
page: Page,
urlPattern: string | RegExp,
response: any,
statusCode: number = 200
) {
await page.route(urlPattern, async (route) => {
await route.fulfill({
status: statusCode,
contentType: 'application/json',
body: JSON.stringify(response),
});
});
}
/**
* Checks if a toast/notification is visible
*/
export async function expectToastMessage(page: Page, message: string | RegExp) {
// Adjust selector based on your toast implementation
const toast = page.locator('[role="alert"], .toast, .notification');
await expect(toast).toContainText(message, { timeout: 5000 });
}
/**
* Fills a form field by label
*/
export async function fillFormField(page: Page, label: string | RegExp, value: string) {
await page.getByLabel(label).fill(value);
}
/**
* Selects an option from a dropdown by label
*/
export async function selectDropdownOption(page: Page, label: string | RegExp, value: string) {
await page.getByLabel(label).selectOption(value);
}
/**
* Uploads a file to a file input
*/
export async function uploadFile(page: Page, inputSelector: string, filePath: string) {
const fileInput = page.locator(inputSelector);
await fileInput.setInputFiles(filePath);
}
/**
* Scrolls an element into view
*/
export async function scrollIntoView(page: Page, selector: string) {
await page.locator(selector).scrollIntoViewIfNeeded();
}
/**
* Takes a screenshot with a custom name
*/
export async function takeScreenshot(page: Page, name: string) {
await page.screenshot({ path: `test-results/screenshots/${name}.png`, fullPage: true });
}
/**
* Waits for navigation to complete
*/
export async function waitForNavigation(page: Page, urlPattern?: string | RegExp) {
if (urlPattern) {
await page.waitForURL(urlPattern);
} else {
await page.waitForLoadState('networkidle');
}
}
/**
* Generates a unique test identifier
*/
export function generateTestId(prefix: string = 'test'): string {
return `${prefix}-${Date.now()}-${Math.random().toString(36).substring(7)}`;
}
/**
* Waits for a specific amount of time (use sparingly, prefer waitFor* methods)
*/
export async function wait(ms: number) {
await new Promise((resolve) => setTimeout(resolve, ms));
}
/**
* Retries an action until it succeeds or max attempts reached
*/
export async function retryAction<T>(
action: () => Promise<T>,
maxAttempts: number = 3,
delayMs: number = 1000
): Promise<T> {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await action();
} catch (error) {
if (attempt === maxAttempts) {
throw error;
}
await wait(delayMs);
}
}
throw new Error('Retry action failed');
}
/**
* Checks if an element is visible on the page
*/
export async function isVisible(page: Page, selector: string): Promise<boolean> {
try {
await page.locator(selector).waitFor({ state: 'visible', timeout: 2000 });
return true;
} catch {
return false;
}
}