-
Notifications
You must be signed in to change notification settings - Fork 2
feat: Add Playwright e2e tests for demo and fix scroll settle race #31
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
85faee1
d2089fb
7a38b60
c3da0da
2a283e6
2ae12f7
753ddba
54c1583
970b481
7b77844
6b2275e
5072055
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| name: E2E Tests | ||
|
|
||
| on: | ||
| push: | ||
| branches: [main] | ||
| pull_request: | ||
| branches: [main] | ||
|
|
||
| jobs: | ||
| e2e: | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 15 | ||
| defaults: | ||
| run: | ||
| working-directory: demo | ||
|
|
||
| steps: | ||
| - uses: actions/checkout@v6 | ||
| - uses: pnpm/action-setup@v5 | ||
| - uses: actions/setup-node@v6 | ||
| with: | ||
| node-version: 24 | ||
| cache: 'pnpm' | ||
|
|
||
| - run: pnpm install | ||
| working-directory: . | ||
|
|
||
| - name: Install Playwright browsers | ||
| run: pnpm exec playwright install --with-deps chromium | ||
|
|
||
| - name: Run e2e tests | ||
| run: pnpm test:e2e | ||
|
|
||
| - uses: actions/upload-artifact@v7 | ||
| if: failure() | ||
| with: | ||
| name: playwright-report | ||
| path: demo/playwright-report/ | ||
| retention-days: 30 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,3 +7,4 @@ dist/ | |
| *.db-wal2 | ||
| rocicorp-zero-virtual-*.tgz | ||
| docs | ||
| .last-run.json | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,180 @@ | ||
| import {spawn} from 'node:child_process'; | ||
| import { | ||
| existsSync, | ||
| mkdirSync, | ||
| readFileSync, | ||
| rmSync, | ||
| writeFileSync, | ||
| } from 'node:fs'; | ||
| import * as net from 'node:net'; | ||
| import {join} from 'node:path'; | ||
| import {fileURLToPath} from 'node:url'; | ||
| import pg from 'pg'; | ||
| import {seedTestDb} from './seed-test.ts'; | ||
|
|
||
| const DEMO_DIR = fileURLToPath(new URL('..', import.meta.url)); | ||
|
|
||
| // Replica dir is wiped on each run so zero-cache starts with clean data. | ||
| const REPLICA_DIR = '/tmp/zero-playwright-replica'; | ||
| export const REPLICA_FILE = join(REPLICA_DIR, 'replica'); | ||
|
|
||
| // PID file lets globalTeardown kill the zero-cache process. | ||
| export const PID_FILE = '/tmp/zero-playwright.pid'; | ||
|
|
||
| export default async function globalSetup(): Promise<void> { | ||
| console.log('\n[setup] Starting postgres...'); | ||
| await startPostgres(); | ||
|
|
||
| console.log('[setup] Waiting for postgres...'); | ||
| await waitForPort(5430); | ||
| await waitForPostgres(); | ||
|
|
||
| console.log('[setup] Seeding test data...'); | ||
| await seedTestDb(process.env['ZERO_UPSTREAM_DB']!); | ||
|
|
||
| console.log('[setup] Clearing zero-cache replica...'); | ||
| killExistingZeroCache(); | ||
| if (existsSync(REPLICA_DIR)) { | ||
| rmSync(REPLICA_DIR, {recursive: true, force: true}); | ||
| } | ||
| mkdirSync(REPLICA_DIR, {recursive: true}); | ||
|
|
||
| const port = Number(process.env['VITE_PUBLIC_CACHE_PORT'] ?? 5858); | ||
| console.log('[setup] Starting zero-cache...'); | ||
| const zeroCacheProc = spawnZeroCache(port); | ||
| writeFileSync(PID_FILE, String(zeroCacheProc.pid)); | ||
|
|
||
| console.log(`[setup] Waiting for zero-cache on port ${port}...`); | ||
| await waitForPort(port, 60_000); | ||
| console.log('[setup] Ready.\n'); | ||
| } | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Helpers | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| async function startPostgres(): Promise<void> { | ||
| await new Promise<void>((resolve, reject) => { | ||
| const proc = spawn( | ||
| 'docker', | ||
| [ | ||
| 'compose', | ||
| '--env-file', | ||
| '.env', | ||
| '-f', | ||
| './docker/docker-compose.yml', | ||
| 'up', | ||
| '-d', | ||
| ], | ||
| {cwd: DEMO_DIR, stdio: 'inherit'}, | ||
| ); | ||
| proc.on('exit', code => { | ||
| if (code === 0) resolve(); | ||
| else reject(new Error(`docker compose up exited with code ${code}`)); | ||
| }); | ||
| proc.on('error', reject); | ||
| }); | ||
| } | ||
|
|
||
| async function waitForPostgres(timeoutMs = 30_000): Promise<void> { | ||
| const connStr = process.env['ZERO_UPSTREAM_DB']!; | ||
| const deadline = Date.now() + timeoutMs; | ||
| while (Date.now() < deadline) { | ||
| try { | ||
| const pool = new pg.Pool({connectionString: connStr, max: 1}); | ||
| const client = await pool.connect(); | ||
| client.release(); | ||
| await pool.end(); | ||
| return; | ||
| } catch { | ||
| await sleep(500); | ||
| } | ||
| } | ||
| throw new Error('Postgres not ready within timeout'); | ||
| } | ||
|
|
||
| function waitForPort(port: number, timeoutMs = 30_000): Promise<void> { | ||
| return new Promise((resolve, reject) => { | ||
| const deadline = Date.now() + timeoutMs; | ||
|
|
||
| function tryConnect() { | ||
| const socket = new net.Socket(); | ||
| socket.setTimeout(1_000); | ||
|
|
||
| socket.on('connect', () => { | ||
| socket.destroy(); | ||
| resolve(); | ||
| }); | ||
|
|
||
| const retry = () => { | ||
| socket.destroy(); | ||
| if (Date.now() >= deadline) { | ||
| reject(new Error(`Port ${port} not available after ${timeoutMs}ms`)); | ||
| return; | ||
| } | ||
| setTimeout(tryConnect, 500); | ||
| }; | ||
|
|
||
| socket.on('timeout', retry); | ||
| socket.on('error', retry); | ||
| socket.connect(port, '127.0.0.1'); | ||
| } | ||
|
|
||
| tryConnect(); | ||
| }); | ||
| } | ||
|
|
||
| function spawnZeroCache(port: number) { | ||
| const binDir = join(DEMO_DIR, 'node_modules', '.bin'); | ||
| const env: NodeJS.ProcessEnv = { | ||
| ...process.env, | ||
| // Ensure node_modules/.bin is on PATH so zero-cache-dev can find zero-cache. | ||
| PATH: `${binDir}:${process.env['PATH'] ?? ''}`, | ||
| ZERO_REPLICA_FILE: REPLICA_FILE, | ||
| ZERO_LOG_LEVEL: 'error', | ||
|
Comment on lines
+129
to
+134
|
||
| }; | ||
|
|
||
| // Prefer the local bin so we use the exact version pinned in demo/package.json. | ||
| const bin = join(DEMO_DIR, 'node_modules', '.bin', 'zero-cache-dev'); | ||
| const command = existsSync(bin) ? bin : 'zero-cache-dev'; | ||
|
|
||
| const proc = spawn(command, ['--port', String(port)], { | ||
| cwd: DEMO_DIR, | ||
| env, | ||
| stdio: ['ignore', 'pipe', 'pipe'], | ||
| detached: true, | ||
| }); | ||
|
Comment on lines
+141
to
+146
|
||
|
|
||
| proc.stdout?.on('data', (d: Buffer) => | ||
| process.stdout.write(`[zero-cache] ${d}`), | ||
| ); | ||
| proc.stderr?.on('data', (d: Buffer) => | ||
| process.stderr.write(`[zero-cache] ${d}`), | ||
| ); | ||
|
|
||
| proc.on('exit', code => { | ||
| if (code !== null && code !== 0) { | ||
| console.error(`[zero-cache] exited with code ${code}`); | ||
| } | ||
| }); | ||
|
|
||
| proc.unref(); | ||
| return proc; | ||
| } | ||
|
|
||
| function killExistingZeroCache(): void { | ||
| if (!existsSync(PID_FILE)) return; | ||
| const pid = parseInt(readFileSync(PID_FILE, 'utf-8').trim(), 10); | ||
| if (!isNaN(pid)) { | ||
| try { | ||
| process.kill(pid, 'SIGTERM'); | ||
| } catch { | ||
| // Process may already be gone — that's fine. | ||
| } | ||
| } | ||
| rmSync(PID_FILE, {force: true}); | ||
| } | ||
|
|
||
| function sleep(ms: number): Promise<void> { | ||
| return new Promise(resolve => setTimeout(resolve, ms)); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| import {existsSync, readFileSync, rmSync} from 'node:fs'; | ||
| import {PID_FILE} from './global-setup.ts'; | ||
|
|
||
| export default async function globalTeardown(): Promise<void> { | ||
| if (!existsSync(PID_FILE)) return; | ||
|
|
||
| const pid = parseInt(readFileSync(PID_FILE, 'utf-8').trim(), 10); | ||
| if (!isNaN(pid)) { | ||
| try { | ||
| process.kill(pid, 'SIGTERM'); | ||
| console.log(`[teardown] Stopped zero-cache (pid ${pid})`); | ||
| } catch { | ||
| // Already gone — that's fine. | ||
| } | ||
| } | ||
| rmSync(PID_FILE, {force: true}); | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.