Production-ready test infrastructure with Page Object Model pattern, reusable fixtures for auth/DB/test-data, and example tests covering login flow, dashboard, accounts CRUD API, and visual regression. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
98 lines
3.1 KiB
TypeScript
98 lines
3.1 KiB
TypeScript
/**
|
|
* E2E tests for the authentication flow.
|
|
*
|
|
* Tests login via UI (not reusing stored auth state).
|
|
* These tests use a fresh browser context per test.
|
|
*/
|
|
|
|
import { test, expect } from '@playwright/test';
|
|
import { LoginPage } from '../page-objects';
|
|
import { TEST_USERS } from '../fixtures/test-data';
|
|
|
|
// Don't use stored auth state — we're testing the login flow itself
|
|
test.use({ storageState: { cookies: [], origins: [] } });
|
|
|
|
test.describe('Login Page', () => {
|
|
let loginPage: LoginPage;
|
|
|
|
test.beforeEach(async ({ page }) => {
|
|
loginPage = new LoginPage(page);
|
|
await loginPage.goto();
|
|
});
|
|
|
|
test('should display login form', async () => {
|
|
await expect(loginPage.emailInput).toBeVisible();
|
|
await expect(loginPage.passwordInput).toBeVisible();
|
|
await expect(loginPage.signInButton).toBeVisible();
|
|
});
|
|
|
|
test('should show error for invalid credentials', async () => {
|
|
await loginPage.login('invalid@example.com', 'WrongPassword!');
|
|
|
|
// Wait for the error alert to appear
|
|
await loginPage.assertError(/invalid|unauthorized|failed/i);
|
|
await loginPage.assertStillOnLogin();
|
|
});
|
|
|
|
test('should show validation error for empty fields', async ({ page }) => {
|
|
// Try to submit with empty password
|
|
await loginPage.emailInput.fill('test@example.com');
|
|
await loginPage.signInButton.click();
|
|
|
|
// Should remain on login page (Mantine form validation)
|
|
await loginPage.assertStillOnLogin();
|
|
});
|
|
|
|
test('should login successfully with valid credentials', async () => {
|
|
await loginPage.loginAndWaitForRedirect(
|
|
TEST_USERS.treasurer.email,
|
|
TEST_USERS.treasurer.password,
|
|
);
|
|
|
|
// Should be redirected away from login
|
|
await expect(loginPage.page).not.toHaveURL(/\/login/);
|
|
});
|
|
|
|
test('should show register link', async () => {
|
|
await expect(loginPage.registerLink).toBeVisible();
|
|
});
|
|
|
|
test('should redirect unauthenticated users to login', async ({ page }) => {
|
|
// Try accessing a protected route directly
|
|
await page.goto('/dashboard');
|
|
|
|
// Should be redirected to login
|
|
await expect(page).toHaveURL(/\/login/);
|
|
});
|
|
});
|
|
|
|
test.describe('Logout', () => {
|
|
test('should clear auth state on logout', async ({ page }) => {
|
|
// First login
|
|
const loginPage = new LoginPage(page);
|
|
await loginPage.goto();
|
|
await loginPage.loginAndWaitForRedirect(
|
|
TEST_USERS.treasurer.email,
|
|
TEST_USERS.treasurer.password,
|
|
);
|
|
|
|
// Handle org selection if needed
|
|
if (page.url().includes('/select-org')) {
|
|
await page.getByRole('button').first().click();
|
|
await page.waitForURL(/\/dashboard/, { timeout: 10_000 });
|
|
}
|
|
|
|
// Look for logout in user menu/settings
|
|
const userMenu = page.locator('[class*="avatar"], [class*="Avatar"]').first();
|
|
if (await userMenu.isVisible({ timeout: 3_000 }).catch(() => false)) {
|
|
await userMenu.click();
|
|
}
|
|
|
|
const logoutButton = page.getByRole('button', { name: /logout|sign out/i }).first();
|
|
if (await logoutButton.isVisible({ timeout: 3_000 }).catch(() => false)) {
|
|
await logoutButton.click();
|
|
await expect(page).toHaveURL(/\/login/);
|
|
}
|
|
});
|
|
});
|