Files
gstack/make-pdf/test/browseClient.test.ts
T
Garry Tan 3af86348f6 feat(make-pdf): new /make-pdf skill + orchestrator binary
Turn markdown into publication-quality PDFs. $P generate input.md out.pdf
produces a PDF with 1in margins, intelligent page breaks, page numbers,
running header, CONFIDENTIAL footer, and curly quotes/em dashes — all on
Helvetica so copy-paste extraction works ("S ai li ng" bug avoided).

Architecture (per Codex round 2):
  markdown → render.ts (marked + sanitize + smartypants) → orchestrator
    → $B newtab --json → $B load-html --tab-id → $B js (poll Paged.js)
    → $B pdf --tab-id → $B closetab

browseClient.ts shells out to the compiled browse CLI rather than
duplicating Playwright. --tab-id isolation per render means parallel
$P generate calls don't race on the active tab. try/finally tab cleanup
survives Paged.js timeouts, browser crashes, and output-path failures.

Features in v1:
  --cover              left-aligned cover page (eyebrow + title + hairline rule)
  --toc                clickable static TOC (Paged.js page numbers deferred)
  --watermark <text>   diagonal DRAFT/CONFIDENTIAL layer
  --no-chapter-breaks  opt out of H1-starts-new-page
  --page-numbers       "N of M" footer (default on)
  --tagged --outline   accessible PDF + bookmark outline (default on)
  --allow-network      opt in to external image loading (default off for privacy)
  --quiet --verbose    stderr control

Design decisions locked from the /plan-design-review pass:
  - Helvetica everywhere (Chromium emits single-word Tj operators for
    system fonts; bundled webfonts emit per-glyph and break extraction).
  - Left-aligned body, flush-left paragraphs, no text-indent, 12pt gap.
  - Cover shares 1in margins with body pages; no flexbox-center, no
    inset padding.
  - The reference HTMLs at .context/designs/*.html are the implementation
    source of truth for print-css.ts.

Tests (56 unit + 1 E2E combined-features gate):
  - smartypants: code/URL-safe, verified against 10 fixtures
  - sanitizer: strips <script>/<iframe>/on*/javascript: URLs
  - render: HTML assembly, CJK fallback, cover/TOC/chapter wrap
  - print-css: all @page rules, margin variants, watermark
  - pdftotext: normalize()+copyPasteGate() cross-OS tolerance
  - browseClient: binary resolution + typed error propagation
  - combined-features gate (P0): 2-chapter fixture with smartypants +
    hyphens + ligatures + bold/italic + inline code + lists + blockquote
    passes through PDF → pdftotext → expected.txt diff

Deferred to Phase 4 (future PR): Paged.js vendored for accurate TOC page
numbers, highlight.js for syntax highlighting, drop caps, pull quotes,
two-column, CMYK, watermark visual-diff acceptance.

Plan: .context/ceo-plans/2026-04-19-perfect-pdf-generator.md
References: .context/designs/make-pdf-*.html

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 05:34:05 +08:00

73 lines
2.3 KiB
TypeScript

/**
* browseClient unit tests — binary resolution and error mapping.
*
* These are pure unit tests; they do NOT require a running browse daemon.
*/
import { describe, expect, test } from "bun:test";
import { BrowseClientError } from "../src/types";
import { resolveBrowseBin } from "../src/browseClient";
describe("resolveBrowseBin", () => {
test("throws BrowseClientError with setup hint when nothing is found", () => {
// Point every candidate path to a non-existent location.
const originalEnv = process.env.BROWSE_BIN;
process.env.BROWSE_BIN = "/nonexistent/browse-does-not-exist";
// We can't easily mock the sibling and global paths without touching
// the filesystem, so in a typical dev environment this will usually
// find the real browse. That's fine — on CI it will throw, and the
// error message shape is what we're actually asserting.
let thrown: any = null;
try {
resolveBrowseBin();
} catch (err) {
thrown = err;
}
if (thrown) {
expect(thrown).toBeInstanceOf(BrowseClientError);
expect(thrown.message).toContain("browse binary not found");
expect(thrown.message).toContain("./setup");
expect(thrown.message).toContain("BROWSE_BIN");
}
// Restore env
if (originalEnv === undefined) {
delete process.env.BROWSE_BIN;
} else {
process.env.BROWSE_BIN = originalEnv;
}
});
test("honors BROWSE_BIN when it points at a real executable", () => {
const originalEnv = process.env.BROWSE_BIN;
// `/bin/sh` exists on every POSIX system and is executable.
process.env.BROWSE_BIN = "/bin/sh";
try {
const resolved = resolveBrowseBin();
expect(resolved).toBe("/bin/sh");
} finally {
if (originalEnv === undefined) {
delete process.env.BROWSE_BIN;
} else {
process.env.BROWSE_BIN = originalEnv;
}
}
});
});
describe("BrowseClientError", () => {
test("captures exit code, command, and stderr", () => {
const err = new BrowseClientError(127, "pdf", "Chromium not found");
expect(err.exitCode).toBe(127);
expect(err.command).toBe("pdf");
expect(err.stderr).toBe("Chromium not found");
expect(err.message).toContain("browse pdf exited 127");
expect(err.message).toContain("Chromium not found");
expect(err.name).toBe("BrowseClientError");
});
});