/** * recording-to-run.e2e.test.ts * * End-to-end coverage for the path users actually take: * 1. browser-recorder captures a sequence of actions (we synthesize one * directly — recorder unit tests already exercise the capture logic). * 2. compileScript turns the recording into a runnable browser-macro source. * 3. The compiled source is written to disk and executed via runUserScript * against a real headless chromium. * 4. The script's returned value matches what the recorded actions would * have produced if a human ran them. * * Until now each leg had unit tests, but the glue between them was only * verified by hand. This catches regressions where the compiler emits valid * code that the runtime can't actually execute. * * Gated behind SKIP_PLAYWRIGHT_E2E=1 — chromium spin-up is ~2-5s per case. */ import { describe, it, expect, afterEach } from 'vitest'; import { writeFileSync, unlinkSync } from 'node:fs'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; import type { RecordedAction } from '../engine/browser-recorder.js'; import { compileScript } from './script-compiler.js'; import { runUserScript } from './script-runner.js'; const skipPlaywright = process.env['SKIP_PLAYWRIGHT_E2E'] === '1'; const tmpFiles: string[] = []; afterEach(() => { for (const f of tmpFiles) { try { unlinkSync(f); } catch { /* already gone */ } } tmpFiles.length = 0; }); function writeTmpScript(source: string): string { const path = join(tmpdir(), `recording-e2e-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}.js`); writeFileSync(path, source, 'utf-8'); tmpFiles.push(path); return path; } function action( type: RecordedAction['type'], extras: Partial> = {}, ): RecordedAction { return { type, ts: '2026-05-11T00:00:00Z', ...extras }; } describe.skipIf(skipPlaywright)('recording → compileScript → runUserScript (E2E)', () => { it('compiles a recording with goto + click + getText and executes it', async () => { // Recording captured against a synthetic data: URL page so the test // doesn't depend on an external HTTP server. const html = ` `; const dataUrl = 'data:text/html,' + encodeURIComponent(html); const recording: RecordedAction[] = [ action('goto', { url: dataUrl }), action('click', { selector: '#go' }), action('getText', { selector: '#out' }), ]; const { source } = compileScript({ recording, description: 'E2E pipeline smoke test', }); // Sanity check: the compiler emitted something we expect to run. expect(source).toContain('await page.goto('); expect(source).toContain("page.locator('#go').click()"); expect(source).toContain('return __text;'); const scriptPath = writeTmpScript(source); const result = await runUserScript({ scriptPath, params: {}, runtime: 'playwright', }); expect(result.result).toBe('clicked-result'); }, 30_000); it('threads paramHints from recording through to the runtime', async () => { // The recording fills a field with a known sentinel value; paramHints turn // that into a params.{name} reference at compile time. At runtime we pass // a different value for {name} and verify the page reflects the new value. const html = ` `; const dataUrl = 'data:text/html,' + encodeURIComponent(html); const recording: RecordedAction[] = [ action('goto', { url: dataUrl }), action('fill', { selector: '#search', value: 'recorded-keyword' }), action('click', { selector: '#submit' }), action('getText', { selector: '#out' }), ]; const { source, meta } = compileScript({ recording, description: 'paramHint plumbing', paramHints: [ { name: 'keyword', valueToReplace: 'recorded-keyword', type: 'string' }, ], }); // The compiler should have replaced the literal with params.keyword and // surfaced the param spec in the frontmatter. expect(source).toContain('.fill(params.keyword)'); expect(meta.params).toEqual([{ name: 'keyword', type: 'string' }]); const scriptPath = writeTmpScript(source); const result = await runUserScript({ scriptPath, params: { keyword: 'replayed-keyword' }, runtime: 'playwright', }); expect(result.result).toBe('searched:replayed-keyword'); }, 30_000); });