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:
124
tests/fixtures/db.fixture.ts
vendored
Normal file
124
tests/fixtures/db.fixture.ts
vendored
Normal file
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* Database fixture for E2E tests.
|
||||
*
|
||||
* Provides helpers to seed and clean up test data in Postgres.
|
||||
* Uses the `pg` driver directly (same driver the backend uses)
|
||||
* to avoid coupling tests to the backend's TypeORM setup.
|
||||
*
|
||||
* Usage in tests:
|
||||
* import { testDb } from '../fixtures/db.fixture';
|
||||
* test.beforeAll(async () => { await testDb.connect(); });
|
||||
* test.afterAll(async () => { await testDb.cleanup(); await testDb.disconnect(); });
|
||||
*/
|
||||
|
||||
import { Pool, type PoolClient } from 'pg';
|
||||
import { TEST_PREFIX } from './test-data';
|
||||
|
||||
const TEST_DB_URL =
|
||||
process.env.TEST_DB_URL ||
|
||||
'postgresql://hoafinance:change_me@localhost:5432/hoafinance';
|
||||
|
||||
class TestDatabase {
|
||||
private pool: Pool | null = null;
|
||||
|
||||
/** Connect to the test database */
|
||||
async connect(): Promise<void> {
|
||||
this.pool = new Pool({ connectionString: TEST_DB_URL, max: 5 });
|
||||
// Verify connectivity
|
||||
const client = await this.pool.connect();
|
||||
try {
|
||||
await client.query('SELECT 1');
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
/** Get a client from the pool */
|
||||
async getClient(): Promise<PoolClient> {
|
||||
if (!this.pool) throw new Error('TestDatabase not connected');
|
||||
return this.pool.connect();
|
||||
}
|
||||
|
||||
/** Execute a query */
|
||||
async query(sql: string, params?: unknown[]) {
|
||||
if (!this.pool) throw new Error('TestDatabase not connected');
|
||||
return this.pool.query(sql, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up all test-created data.
|
||||
* Removes rows that match the TEST_PREFIX in name/description/memo fields.
|
||||
* This runs within a transaction so it's atomic.
|
||||
*/
|
||||
async cleanup(): Promise<void> {
|
||||
if (!this.pool) return;
|
||||
const client = await this.pool.connect();
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
|
||||
// Clean up test data from tenant schemas.
|
||||
// We look for the e2e test org schema if it exists.
|
||||
const schemas = await client.query(
|
||||
`SELECT schema_name FROM shared.organizations
|
||||
WHERE name LIKE '${TEST_PREFIX}%' OR schema_name = 'e2e_test_hoa'`,
|
||||
);
|
||||
|
||||
for (const row of schemas.rows) {
|
||||
const schema = row.schema_name;
|
||||
// Delete test journal entries, accounts, etc. in tenant schema
|
||||
await client.query(
|
||||
`DELETE FROM "${schema}".journal_entries WHERE memo LIKE $1`,
|
||||
[`${TEST_PREFIX}%`],
|
||||
).catch(() => {}); // Table may not exist
|
||||
await client.query(
|
||||
`DELETE FROM "${schema}".accounts WHERE name LIKE $1`,
|
||||
[`${TEST_PREFIX}%`],
|
||||
).catch(() => {});
|
||||
}
|
||||
|
||||
// Clean up test users from shared schema
|
||||
await client.query(
|
||||
`DELETE FROM shared.users WHERE email LIKE 'e2e-%@test.hoaledgeriq.com'`,
|
||||
).catch(() => {});
|
||||
|
||||
await client.query('COMMIT');
|
||||
} catch (err) {
|
||||
await client.query('ROLLBACK');
|
||||
console.error('Test cleanup failed:', err);
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Seed the minimum data needed for authenticated test flows:
|
||||
* - A test organization (if not exists)
|
||||
* - A test user with known credentials (if not exists)
|
||||
* - Associates user to org with the given role
|
||||
*
|
||||
* This is idempotent — safe to call multiple times.
|
||||
*/
|
||||
async seedTestUser(user: {
|
||||
email: string;
|
||||
password: string;
|
||||
fullName: string;
|
||||
role: string;
|
||||
}): Promise<void> {
|
||||
if (!this.pool) throw new Error('TestDatabase not connected');
|
||||
|
||||
// This is done via API calls in auth.setup.ts instead of direct SQL,
|
||||
// because the backend handles password hashing, schema creation, etc.
|
||||
// This method is available for advanced scenarios that need direct DB access.
|
||||
console.log(`[TestDB] Seed user ${user.email} — use API-based seeding in auth.setup.ts`);
|
||||
}
|
||||
|
||||
/** Disconnect from the database */
|
||||
async disconnect(): Promise<void> {
|
||||
if (this.pool) {
|
||||
await this.pool.end();
|
||||
this.pool = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const testDb = new TestDatabase();
|
||||
Reference in New Issue
Block a user