129 lines
4.8 KiB
TypeScript
129 lines
4.8 KiB
TypeScript
/**
|
|
* 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<Omit<RecordedAction, 'type' | 'ts'>> = {},
|
|
): 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 = `<!doctype html><html><body>
|
|
<button id="go" onclick="document.getElementById('out').textContent='clicked-result'">Go</button>
|
|
<span id="out"></span>
|
|
</body></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 = `<!doctype html><html><body>
|
|
<input id="search" />
|
|
<button id="submit" onclick="document.getElementById('out').textContent='searched:' + document.getElementById('search').value">Submit</button>
|
|
<span id="out"></span>
|
|
</body></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);
|
|
});
|