| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| import { chromium } from "playwright"; |
| import { humanType, humanTypeInto, pause } from "./human-typing.js"; |
| import { PERSONAS, FALLBACK_USER_KEY } from "./personas.js"; |
|
|
| interface CliArgs { |
| url: string; |
| speed: number; |
| headless: boolean; |
| } |
|
|
| function parseArgs(argv: string[]): CliArgs { |
| const args: CliArgs = { |
| url: "http://localhost:5678", |
| speed: 1, |
| headless: false, |
| }; |
| for (let i = 0; i < argv.length; i++) { |
| const arg = argv[i]; |
| if (arg === "--url") args.url = argv[++i]; |
| else if (arg === "--speed") args.speed = Number(argv[++i]); |
| else if (arg === "--headless") args.headless = true; |
| } |
| return args; |
| } |
|
|
| async function run() { |
| const args = parseArgs(process.argv.slice(2)); |
| const persona = PERSONAS.alice; |
|
|
| console.log(`[demo/solo] launching as ${persona.name} (${persona.color})`); |
| console.log(`[demo/solo] target: ${args.url} speed: ${args.speed}x`); |
|
|
| const browser = await chromium.launch({ |
| headless: args.headless, |
| args: ["--window-size=1280,800"], |
| }); |
|
|
| const context = await browser.newContext({ |
| viewport: { width: 1280, height: 800 }, |
| deviceScaleFactor: 2, |
| }); |
|
|
| await context.addInitScript( |
| ({ key, value }) => { |
| try { |
| localStorage.setItem(key, value); |
| } catch { |
| |
| } |
| }, |
| { |
| key: FALLBACK_USER_KEY, |
| value: JSON.stringify({ name: persona.name, color: persona.color }), |
| } |
| ); |
|
|
| const page = await context.newPage(); |
|
|
| console.log(`[demo/solo] goto ${args.url}`); |
| try { |
| await page.goto(args.url, { waitUntil: "domcontentloaded", timeout: 15_000 }); |
| } catch (err) { |
| console.error( |
| `[demo/solo] cannot reach ${args.url}. Is the frontend dev server running?` |
| ); |
| await browser.close(); |
| process.exit(1); |
| } |
|
|
| await page.waitForSelector("[aria-label='Undo']", { timeout: 20_000 }); |
| console.log(`[demo/solo] editor ready, starting scenario`); |
|
|
| await pause(1200, 2000); |
|
|
| const title = page.getByPlaceholder("Article title"); |
| await humanTypeInto(page, title, "Climate signals in early 2026", { |
| speed: args.speed, |
| typoRate: 0.03, |
| }); |
|
|
| await pause(900, 1600); |
|
|
| const body = page.locator(".ProseMirror").first(); |
| await body.click(); |
| await pause(600, 1200); |
|
|
| await humanType( |
| page, |
| "Over the last six months, a handful of monitoring stations picked up anomalies that deserve a closer look. ", |
| { speed: args.speed, typoRate: 0.035, thinkRate: 0.22 } |
| ); |
|
|
| await pause(800, 1400); |
|
|
| await humanType( |
| page, |
| "We cross-checked three independent datasets before drawing any conclusions.", |
| { speed: args.speed, typoRate: 0.025 } |
| ); |
|
|
| await pause(1500, 2500); |
|
|
| console.log(`[demo/solo] scenario complete. Leaving browser open for inspection.`); |
| console.log(`[demo/solo] press Ctrl+C to quit.`); |
|
|
| await new Promise(() => {}); |
| } |
|
|
| run().catch((err) => { |
| console.error("[demo/solo] fatal:", err); |
| process.exit(1); |
| }); |
|
|