94 lines
3.0 KiB
TypeScript
94 lines
3.0 KiB
TypeScript
import { fork, type ChildProcess } from 'node:child_process';
|
|
import { once } from 'node:events';
|
|
import { writeFile } from 'node:fs/promises';
|
|
import { test as base } from '@playwright/test';
|
|
import { serverEnvironment } from './environment.ts';
|
|
import { workerDirectory } from './paths.ts';
|
|
import { resetDatabases } from './database.ts';
|
|
|
|
export { expect } from '@playwright/test';
|
|
export type { Page, Locator, Request } from '@playwright/test';
|
|
|
|
async function stopServer(child: ChildProcess) {
|
|
if (child.exitCode !== null || child.signalCode !== null) return;
|
|
const exited = once(child, 'exit');
|
|
child.kill('SIGTERM');
|
|
const kill = setTimeout(() => child.kill('SIGKILL'), 6_000);
|
|
try {
|
|
await exited;
|
|
} finally {
|
|
clearTimeout(kill);
|
|
}
|
|
}
|
|
|
|
type AppServer = { origin: string; output: () => string };
|
|
|
|
export const test = base.extend<{ cleanDatabase: void }, { appServer: AppServer }>({
|
|
appServer: [
|
|
// Playwright requires destructuring even when a fixture has no dependencies.
|
|
// eslint-disable-next-line no-empty-pattern
|
|
async ({}, use) => {
|
|
const child = fork(new URL('../server.mjs', import.meta.url), [], {
|
|
env: serverEnvironment(workerDirectory()),
|
|
execArgv: [],
|
|
stdio: ['ignore', 'pipe', 'pipe', 'ipc']
|
|
});
|
|
let output = '';
|
|
child.stdout!.on('data', (chunk) => (output = (output + chunk).slice(-50_000)));
|
|
child.stderr!.on('data', (chunk) => (output = (output + chunk).slice(-50_000)));
|
|
try {
|
|
const origin = await new Promise<string>((resolve, reject) => {
|
|
const timeout = setTimeout(
|
|
() => reject(new Error('E2E server startup timed out')),
|
|
30_000
|
|
);
|
|
child.once('message', (message: { origin: string }) => {
|
|
clearTimeout(timeout);
|
|
resolve(message.origin);
|
|
});
|
|
child.once('error', (error) => {
|
|
clearTimeout(timeout);
|
|
reject(error);
|
|
});
|
|
child.once('exit', (code, signal) => {
|
|
clearTimeout(timeout);
|
|
reject(new Error(`E2E server exited (${code ?? signal})`));
|
|
});
|
|
});
|
|
const health = await fetch(`${origin}/healthz`, { signal: AbortSignal.timeout(30_000) });
|
|
if (!health.ok) throw new Error(`E2E server health: ${health.status}`);
|
|
await use({ origin, output: () => output });
|
|
} catch (error) {
|
|
console.error(output);
|
|
throw error;
|
|
} finally {
|
|
await stopServer(child);
|
|
}
|
|
},
|
|
{ scope: 'worker', timeout: 60_000 }
|
|
],
|
|
baseURL: async ({ appServer }, use) => {
|
|
await use(appServer.origin);
|
|
},
|
|
cleanDatabase: [
|
|
async ({ appServer }, use, testInfo) => {
|
|
let setupFailed = false;
|
|
try {
|
|
// Depend on the server so the real application has migrated both databases.
|
|
resetDatabases();
|
|
await use();
|
|
} catch (error) {
|
|
setupFailed = true;
|
|
throw error;
|
|
} finally {
|
|
if (setupFailed || testInfo.status !== testInfo.expectedStatus) {
|
|
const path = testInfo.outputPath('server.log');
|
|
await writeFile(path, appServer.output());
|
|
await testInfo.attach('server.log', { path, contentType: 'text/plain' });
|
|
}
|
|
}
|
|
},
|
|
{ auto: true }
|
|
]
|
|
});
|