feat: add Playwright E2E and API regression test suite
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>
This commit is contained in:
108
tests/fixtures/auth.fixture.ts
vendored
Normal file
108
tests/fixtures/auth.fixture.ts
vendored
Normal file
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* Authentication fixture for E2E tests.
|
||||
*
|
||||
* Provides helpers to authenticate via the API and manage JWT tokens.
|
||||
* The auth.setup.ts project uses this to create persistent auth state
|
||||
* that other tests reuse (avoiding login in every test).
|
||||
*/
|
||||
|
||||
import { type APIRequestContext, type BrowserContext } from '@playwright/test';
|
||||
import { TEST_USERS } from './test-data';
|
||||
|
||||
const API_BASE = process.env.API_BASE_URL || 'http://localhost/api';
|
||||
|
||||
export interface AuthTokens {
|
||||
accessToken: string;
|
||||
user: Record<string, unknown>;
|
||||
organizations: Array<Record<string, unknown>>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Login via the API and return tokens.
|
||||
* Uses Playwright's request context (no browser needed).
|
||||
*/
|
||||
export async function apiLogin(
|
||||
request: APIRequestContext,
|
||||
credentials: { email: string; password: string } = TEST_USERS.treasurer,
|
||||
): Promise<AuthTokens> {
|
||||
const response = await request.post(`${API_BASE}/auth/login`, {
|
||||
data: {
|
||||
email: credentials.email,
|
||||
password: credentials.password,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok()) {
|
||||
const body = await response.text();
|
||||
throw new Error(`Login failed (${response.status()}): ${body}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch to a specific organization after login.
|
||||
*/
|
||||
export async function apiSwitchOrg(
|
||||
request: APIRequestContext,
|
||||
accessToken: string,
|
||||
organizationId: string,
|
||||
): Promise<AuthTokens> {
|
||||
const response = await request.post(`${API_BASE}/auth/switch-org`, {
|
||||
data: { organizationId },
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
});
|
||||
|
||||
if (!response.ok()) {
|
||||
const body = await response.text();
|
||||
throw new Error(`Switch org failed (${response.status()}): ${body}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an authenticated API request context with a Bearer token.
|
||||
* Useful for API-level tests that don't need a browser.
|
||||
*/
|
||||
export function authHeaders(accessToken: string) {
|
||||
return {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject auth state into a browser context's localStorage.
|
||||
* Matches the frontend's Zustand authStore shape.
|
||||
*/
|
||||
export async function injectAuthState(
|
||||
context: BrowserContext,
|
||||
tokens: AuthTokens,
|
||||
orgId?: string,
|
||||
): Promise<void> {
|
||||
const baseURL = process.env.BASE_URL || 'http://localhost';
|
||||
|
||||
// The frontend uses Zustand with persist middleware in localStorage
|
||||
// under key 'auth-storage'. We inject it so the frontend thinks
|
||||
// the user is already logged in.
|
||||
const selectedOrg = orgId
|
||||
? tokens.organizations.find((o: any) => o.id === orgId)
|
||||
: tokens.organizations[0];
|
||||
|
||||
const authState = {
|
||||
state: {
|
||||
token: tokens.accessToken,
|
||||
user: tokens.user,
|
||||
organizations: tokens.organizations,
|
||||
currentOrg: selectedOrg
|
||||
? { id: (selectedOrg as any).id, name: (selectedOrg as any).name, role: (selectedOrg as any).role }
|
||||
: null,
|
||||
},
|
||||
version: 0,
|
||||
};
|
||||
|
||||
await context.addInitScript((authData) => {
|
||||
window.localStorage.setItem('auth-storage', JSON.stringify(authData));
|
||||
}, authState);
|
||||
}
|
||||
Reference in New Issue
Block a user