Add frontend testing - Playwright
This commit is contained in:
78
frontend/tests/helpers/auth.ts
Normal file
78
frontend/tests/helpers/auth.ts
Normal 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',
|
||||
};
|
||||
155
frontend/tests/helpers/utils.ts
Normal file
155
frontend/tests/helpers/utils.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user