220 lines · 8.8 KB
/** Minimal Chrome DevTools Protocol driver for the editor e2e: headless profiles, OOPIF frames, screenshots. */
import { spawn } from "node:child_process";
import type { ChildProcess } from "node:child_process";
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";

const CHROME = process.env.CHROME_PATH ?? "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
const POLL_MS = 250;
const DEFAULT_WAIT_MS = 45_000;
const START_ATTEMPTS = 80;

type Reply = { result?: unknown; error?: { message: string } };

export const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

export async function waitFor<T>(
  label: string,
  probe: () => Promise<T | null | undefined | false>,
  timeoutMs = DEFAULT_WAIT_MS,
): Promise<T> {
  const deadline = Date.now() + timeoutMs;
  let lastError: unknown = null;
  while (Date.now() < deadline) {
    try {
      const value = await probe();
      if (value) return value;
    } catch (error) {
      lastError = error;
    }
    await sleep(POLL_MS);
  }
  throw new Error(`timed out waiting for ${label}${lastError ? ` (${String(lastError)})` : ""}`);
}

/** One headless Chrome with its own profile, so two instances are two independent browsers (no shared cookies). */
export class Browser {
  private socket!: WebSocket;
  private nextId = 1;
  private pending = new Map<number, (reply: Reply) => void>();