mirror of
https://github.com/jamiepine/voicebox.git
synced 2026-10-02 16:45:15 -07:00
test: Playwright E2E with real backend + fake TTS engine
e2e/ drives the web build (vite preview) against a per-worker uvicorn: each Playwright worker boots its own backend on its own port with a temp cwd, so SQLite and audio files are fully isolated and parallel- safe. The page fixture seeds the persisted voicebox-server store with the worker's URL before any script runs. VOICEBOX_FAKE_TTS=1 short-circuits get_tts_backend_for_engine to a backend that synthesizes a sine tone sized to the text — the real task queue, SSE progress, history rows, and audio serving all run, only inference is fake. backend/requirements-ci.txt is the slim dependency set validated to boot the app on a CPU-only runner. Specs: startup, settings layout, seeded profile in /voices, and generate-to-completed-audio through the full pipeline.
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
import { type ChildProcess, spawn } from 'node:child_process';
|
||||
import { mkdtempSync, rmSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { test as base } from '@playwright/test';
|
||||
|
||||
const REPO_ROOT = path.resolve(__dirname, '..');
|
||||
const BASE_PORT = 18100;
|
||||
|
||||
export interface BackendFixture {
|
||||
url: string;
|
||||
dataDir: string;
|
||||
}
|
||||
|
||||
async function waitForHealth(url: string, timeoutMs = 60_000): Promise<void> {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
while (Date.now() < deadline) {
|
||||
try {
|
||||
const res = await fetch(`${url}/health`);
|
||||
if (res.ok) return;
|
||||
} catch {
|
||||
// not up yet
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
}
|
||||
throw new Error(`backend at ${url} did not become healthy within ${timeoutMs}ms`);
|
||||
}
|
||||
|
||||
export const test = base.extend<Record<never, never>, { backend: BackendFixture }>({
|
||||
backend: [
|
||||
async ({}, use, workerInfo) => {
|
||||
const port = BASE_PORT + workerInfo.workerIndex;
|
||||
const url = `http://127.0.0.1:${port}`;
|
||||
// uvicorn resolves the data dir from cwd, so a temp cwd isolates
|
||||
// each worker's SQLite and audio files completely.
|
||||
const dataDir = mkdtempSync(path.join(tmpdir(), `voicebox-e2e-${workerInfo.workerIndex}-`));
|
||||
const python = process.env.VOICEBOX_PYTHON ?? path.join(REPO_ROOT, '.venv-ci/bin/python');
|
||||
|
||||
const proc: ChildProcess = spawn(
|
||||
python,
|
||||
['-m', 'uvicorn', 'backend.main:app', '--port', String(port), '--log-level', 'warning'],
|
||||
{
|
||||
cwd: dataDir,
|
||||
env: {
|
||||
...process.env,
|
||||
PYTHONPATH: REPO_ROOT,
|
||||
VOICEBOX_FAKE_TTS: '1',
|
||||
VOICEBOX_CORS_ORIGINS:
|
||||
'http://localhost:4173,http://127.0.0.1:4173,http://localhost:5173',
|
||||
},
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
},
|
||||
);
|
||||
const logs: Buffer[] = [];
|
||||
proc.stdout?.on('data', (chunk) => logs.push(chunk));
|
||||
proc.stderr?.on('data', (chunk) => logs.push(chunk));
|
||||
|
||||
try {
|
||||
await waitForHealth(url);
|
||||
} catch (error) {
|
||||
proc.kill('SIGKILL');
|
||||
throw new Error(`${(error as Error).message}\nbackend log:\n${Buffer.concat(logs)}`);
|
||||
}
|
||||
|
||||
await use({ url, dataDir });
|
||||
|
||||
proc.kill('SIGTERM');
|
||||
await new Promise((resolve) => {
|
||||
proc.once('exit', resolve);
|
||||
setTimeout(resolve, 5000);
|
||||
});
|
||||
rmSync(dataDir, { recursive: true, force: true });
|
||||
},
|
||||
{ scope: 'worker' },
|
||||
],
|
||||
|
||||
// Point the app's persisted server store at this worker's backend before
|
||||
// any page script runs.
|
||||
page: async ({ page, backend }, use) => {
|
||||
await page.addInitScript((serverUrl) => {
|
||||
window.localStorage.setItem(
|
||||
'voicebox-server',
|
||||
JSON.stringify({
|
||||
state: {
|
||||
serverUrl,
|
||||
isConnected: false,
|
||||
mode: 'local',
|
||||
keepServerRunningOnClose: false,
|
||||
customModelsDir: null,
|
||||
},
|
||||
version: 0,
|
||||
}),
|
||||
);
|
||||
}, backend.url);
|
||||
await use(page);
|
||||
},
|
||||
});
|
||||
|
||||
export { expect } from '@playwright/test';
|
||||
Reference in New Issue
Block a user