codex-pdf-restore.spec.ts

6.2 KB · raw

1/**2 * codex-pdf-restore.spec.ts — end-to-end Codex PDF import test.3 *4 * Runs the real importCodexPDF pipeline against the user's actual Codex PDF5 * inside a live Chrome+extension context. This is the only reliable way to6 * exercise the full chain (pdf.js → canvas render → cell crop → Aztec decode7 * → ChunkBuffer → reconstructAndVerify) because every step needs a real DOM.8 *9 * Run: npx playwright test tests/codex-pdf-restore.spec.ts10 * (build first: npm run build && no additional flags needed)11 *12 * FILE INJECTION STRATEGY13 * -----------------------14 * We navigate with ?codex=restore (not ?codex=pdf). Both URLs route to15 * CodexRestoreView, but only ?codex=pdf sets autoPickPdf=true, which fires16 * a 50ms-delayed pdfFileInput.click() that opens the OS file dialog. The OS17 * dialog steals focus, which in Chrome's extension popup context terminates18 * the page. By using ?codex=restore we skip the auto-click entirely.19 *20 * We then use Playwright's CDP-based setInputFiles() on the hidden file input,21 * which bypasses the OS dialog and directly dispatches the change event,22 * triggering handlePDFSelected() inside the extension.23 */2425import { test, expect } from "@playwright/test";26import type { BrowserContext, Page } from "@playwright/test";27import { fileURLToPath } from "url";28import { dirname, join } from "path";29import os from "os";30import fs from "fs/promises";3132const __filename = fileURLToPath(import.meta.url);33const __dirname = dirname(__filename);34const EXTENSION_PATH = join(__dirname, "..", "dist");3536const CODEX_PDF_PATH = "/Users/june/Downloads/peach-codex-2026-05-20 (2).pdf";3738async function makeContext(): Promise<BrowserContext> {39  const { chromium } = await import("playwright");40  const userDataDir = join(os.tmpdir(), `peach-codex-test-${Date.now()}`);41  await fs.mkdir(userDataDir, { recursive: true });42  return chromium.launchPersistentContext(userDataDir, {43    headless: false,44    args: [45      `--load-extension=${EXTENSION_PATH}`,46      `--disable-extensions-except=${EXTENSION_PATH}`,47    ],48  });49}5051async function getExtId(ctx: BrowserContext): Promise<string> {52  const maxWait = 15_000;53  const start = Date.now();54  let sw = ctx.serviceWorkers()[0];55  while (!sw && Date.now() - start < maxWait) {56    await new Promise((r) => setTimeout(r, 500));57    sw = ctx.serviceWorkers()[0];58  }59  if (!sw) throw new Error(`No service worker after ${maxWait}ms`);60  return new URL(sw.url()).host;61}6263async function openCodexRestorePage(ctx: BrowserContext, extId: string): Promise<Page> {64  const page = await ctx.newPage();6566  // Capture extension console output for diagnostics.67  page.on("console", (msg) => {68    const text = msg.text();69    if (msg.type() === "error" || text.startsWith("[codex]")) {70      console.log(`  [browser:${msg.type()}] ${text}`);71    }72  });7374  // ?codex=restore routes to CodexRestoreView without setting autoPickPdf=true.75  // autoPickPdf=true (set only by ?codex=pdf) fires a 50ms click on the hidden76  // file input, opening the OS dialog which steals focus and kills the page.77  await page.goto(`chrome-extension://${extId}/src/popup/index.html?codex=restore`);7879  // The file input lives inside <Show when={phase() === "choose"}> which is80  // true at mount. Wait for it to be attached before handing back the page.81  await page.waitForSelector('input[type="file"]', { state: "attached", timeout: 10_000 });8283  return page;84}8586test.describe("Codex PDF restore", () => {87  let ctx: BrowserContext;8889  test.beforeEach(async () => {90    ctx = await makeContext();91  });9293  test.afterEach(async () => {94    await ctx.close();95  });9697  test("imports all chunks from the real Codex PDF", async () => {98    // The full import of a large Codex (21+ pages) including scale-detection99    // renders can take up to 3 minutes. Set an explicit test timeout.100    test.setTimeout(180_000);101102    const extId = await getExtId(ctx);103    const page = await openCodexRestorePage(ctx, extId);104105    // CDP-based setInputFiles bypasses the OS dialog and dispatches the106    // change event directly, firing handlePDFSelected inside the extension.107    await page.locator('input[type="file"]').setInputFiles(CODEX_PDF_PATH);108109    // After handlePDFSelected runs importCodexPDF:110    //   success  → phase="ready"  → body contains "master password" + "X / Y codes"111    //   missing  → body contains pdfError with "codes" + camera starts112    //   failure  → body contains "failed to read"113    //   empty    → body contains "no Codex codes found"114    await page.waitForFunction(115      () => {116        const body = document.body.textContent ?? "";117        return (118          body.includes("codes") ||119          body.includes("master password") ||120          body.includes("no Codex codes found") ||121          body.includes("failed to read")122        );123      },124      { timeout: 150_000, polling: 1_000 },125    );126127    const bodyText = await page.evaluate(() => document.body.textContent ?? "");128    console.log("Body snippet:", bodyText.replace(/\s+/g, " ").slice(0, 400));129130    expect(bodyText).not.toContain("no Codex codes found");131    expect(bodyText).not.toContain("failed to read");132133    const passphraseVisible = await page.locator("#codex-passphrase").isVisible().catch(() => false);134    if (passphraseVisible) {135      console.log("✓ Buffer is ready — passphrase prompt visible.");136    }137    expect(bodyText).toMatch(/master password|codes/);138  });139140  test("decodes at least one Aztec envelope from page 1", async () => {141    test.setTimeout(120_000);142143    const extId = await getExtId(ctx);144    const page = await openCodexRestorePage(ctx, extId);145146    await page.locator('input[type="file"]').setInputFiles(CODEX_PDF_PATH);147148    await page.waitForFunction(149      () => {150        const body = document.body.textContent ?? "";151        return (152          body.includes("codes") ||153          body.includes("master password") ||154          body.includes("no Codex codes found") ||155          body.includes("failed to read")156        );157      },158      { timeout: 90_000, polling: 500 },159    );160161    const bodyText = await page.evaluate(() => document.body.textContent ?? "");162    console.log("Body snippet:", bodyText.slice(0, 300));163164    expect(bodyText).not.toContain("no Codex codes found");165    expect(bodyText).not.toContain("failed to read");166  });167});