Add Playwright e2e test suite

This commit is contained in:
2026-03-06 21:08:20 -05:00
parent 8b1fff4130
commit 7dd403e814
8 changed files with 304 additions and 0 deletions

49
scripts/e2e-prepare.ts Normal file
View File

@@ -0,0 +1,49 @@
import { mkdirSync, readFileSync, rmSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { Database } from 'bun:sqlite';
const MIGRATION_FILES = [
'0000_cold_silver_centurion.sql',
'0001_glossy_statement_snapshots.sql',
'0002_workflow_task_projection_metadata.sql',
'0003_task_stage_event_timeline.sql',
'0004_watchlist_company_taxonomy.sql',
'0005_financial_taxonomy_v3.sql'
] as const;
export const E2E_DATABASE_PATH = join(process.cwd(), 'data', 'e2e.sqlite');
export const E2E_WORKFLOW_DATA_DIR = join(process.cwd(), '.workflow-data', 'e2e');
function removeFileIfPresent(path: string) {
rmSync(path, { force: true });
}
function applyMigrations(database: Database) {
for (const file of MIGRATION_FILES) {
const sql = readFileSync(join(process.cwd(), 'drizzle', file), 'utf8');
database.exec(sql);
}
}
export function prepareE2eDatabase() {
mkdirSync(dirname(E2E_DATABASE_PATH), { recursive: true });
removeFileIfPresent(E2E_DATABASE_PATH);
removeFileIfPresent(`${E2E_DATABASE_PATH}-shm`);
removeFileIfPresent(`${E2E_DATABASE_PATH}-wal`);
rmSync(E2E_WORKFLOW_DATA_DIR, { force: true, recursive: true });
const database = new Database(E2E_DATABASE_PATH, { create: true });
try {
database.exec('PRAGMA foreign_keys = ON;');
applyMigrations(database);
} finally {
database.close();
}
}
if (import.meta.main) {
prepareE2eDatabase();
console.info(`[e2e] prepared SQLite database at ${E2E_DATABASE_PATH}`);
}

81
scripts/e2e-run.ts Normal file
View File

@@ -0,0 +1,81 @@
import { spawn } from 'node:child_process';
import { Socket } from 'node:net';
const host = process.env.PLAYWRIGHT_HOST ?? '127.0.0.1';
const requestedPort = Number(process.env.PLAYWRIGHT_PORT ?? '3400');
const candidatePorts = [
requestedPort,
3401,
3402,
3410,
3500,
3600,
3700,
3800
];
async function isPortAvailable(port: number) {
return await new Promise<boolean>((resolve) => {
const socket = new Socket();
const finish = (available: boolean) => {
socket.destroy();
resolve(available);
};
socket.setTimeout(250);
socket.once('connect', () => finish(false));
socket.once('timeout', () => finish(false));
socket.once('error', (error: NodeJS.ErrnoException) => {
if (error.code === 'ECONNREFUSED' || error.code === 'EHOSTUNREACH') {
finish(true);
return;
}
finish(false);
});
try {
socket.connect(port, host);
} catch {
finish(false);
}
});
}
async function pickPort() {
for (const port of candidatePorts) {
if (await isPortAvailable(port)) {
return port;
}
}
throw new Error(`Unable to find an open Playwright port from: ${candidatePorts.join(', ')}`);
}
const port = await pickPort();
const baseURL = `http://${host}:${port}`;
console.info(`[e2e] using ${baseURL}`);
const child = spawn(
'bun',
['x', 'playwright', 'test', ...process.argv.slice(2)],
{
stdio: 'inherit',
env: {
...process.env,
PLAYWRIGHT_BASE_URL: baseURL,
PLAYWRIGHT_HOST: host,
PLAYWRIGHT_PORT: String(port)
}
}
);
child.on('exit', (code, signal) => {
if (signal) {
process.exit(signal === 'SIGINT' || signal === 'SIGTERM' ? 0 : 1);
return;
}
process.exit(code ?? 0);
});

53
scripts/e2e-webserver.ts Normal file
View File

@@ -0,0 +1,53 @@
import { spawn } from 'node:child_process';
import { mkdirSync } from 'node:fs';
import { prepareE2eDatabase, E2E_DATABASE_PATH, E2E_WORKFLOW_DATA_DIR } from './e2e-prepare';
const host = process.env.PLAYWRIGHT_HOST ?? '127.0.0.1';
const port = process.env.PLAYWRIGHT_PORT ?? '3400';
const baseURL = process.env.PLAYWRIGHT_BASE_URL ?? `http://${host}:${port}`;
const env: NodeJS.ProcessEnv = {
...process.env,
BETTER_AUTH_BASE_URL: baseURL,
BETTER_AUTH_SECRET: 'playwright-e2e-secret-playwright-e2e-secret',
BETTER_AUTH_TRUSTED_ORIGINS: baseURL,
DATABASE_URL: `file:${E2E_DATABASE_PATH}`,
HOSTNAME: host,
NEXT_PUBLIC_API_URL: '',
PORT: port,
SEC_USER_AGENT: 'Fiscal Clone Playwright <support@fiscal.local>',
WORKFLOW_LOCAL_DATA_DIR: E2E_WORKFLOW_DATA_DIR,
WORKFLOW_LOCAL_QUEUE_CONCURRENCY: '1',
WORKFLOW_TARGET_WORLD: 'local'
};
delete env.NO_COLOR;
prepareE2eDatabase();
mkdirSync(E2E_WORKFLOW_DATA_DIR, { recursive: true });
const child = spawn(
'bun',
['--bun', 'next', 'dev', '--turbopack', '--hostname', host, '--port', port],
{
stdio: 'inherit',
env
}
);
function forwardSignal(signal: NodeJS.Signals) {
if (!child.killed) {
child.kill(signal);
}
}
process.on('SIGINT', () => forwardSignal('SIGINT'));
process.on('SIGTERM', () => forwardSignal('SIGTERM'));
child.on('exit', (code, signal) => {
if (signal) {
process.exit(signal === 'SIGINT' || signal === 'SIGTERM' ? 0 : 1);
return;
}
process.exit(code ?? 0);
});