maestro/src/user-folder/recording-to-run.e2e.test.ts
2026-06-03 05:08:00 +00:00

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);
});