/* Importing and reading a PDF. * * The interesting parts are not that a canvas appears: it is that the library * stays out of the main bundle until a PDF is opened, that the pages come out at * the right proportions, and that a document read once survives without the * network — the offline promise is the whole reason the bytes are cached. */ import { writeFileSync } from "node:fs"; import { check, createNotebook, launch, report, withFreshUser } from "./lib.mjs"; const PDF_PATH = "/tmp/e2e-test.pdf"; /** A two-page PDF built here, so the suite depends on no fixture file. */ function writeTestPdf() { const objs = [ "<< /Type /Catalog /Pages 2 0 R >>", "<< /Type /Pages /Kids [3 0 R 5 0 R] /Count 2 >>", "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Contents 4 0 R /Resources << /Font << /F1 7 0 R >> >> >>", null, // stream, filled below "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 842 595] /Contents 6 0 R /Resources << /Font << /F1 7 0 R >> >> >>", null, "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>", ]; const s1 = "BT /F1 24 Tf 72 760 Td (Seite eins Zebrastreifen) Tj ET"; const s2 = "BT /F1 24 Tf 72 520 Td (Seite zwei Dichtung) Tj ET"; objs[3] = `<< /Length ${s1.length} >>\nstream\n${s1}\nendstream`; objs[5] = `<< /Length ${s2.length} >>\nstream\n${s2}\nendstream`; let out = "%PDF-1.4\n"; const offsets = []; objs.forEach((o, i) => { offsets.push(out.length); out += `${i + 1} 0 obj\n${o}\nendobj\n`; }); const xref = out.length; out += `xref\n0 ${objs.length + 1}\n0000000000 65535 f \n`; for (const off of offsets) out += `${String(off).padStart(10, "0")} 00000 n \n`; out += `trailer\n<< /Size ${objs.length + 1} /Root 1 0 R >>\nstartxref\n${xref}\n%%EOF\n`; writeFileSync(PDF_PATH, out, "latin1"); } const browser = await launch(); let session; try { writeTestPdf(); session = await withFreshUser(browser, "pdf"); const p = session.page; /* The library must not be in the bundle everyone downloads. */ const scriptsBefore = await p.evaluate(() => performance.getEntriesByType("resource").map((r) => r.name) ); check( !scriptsBefore.some((u) => /pdf[.-]/i.test(u)), "pdf.js is not loaded before a PDF is opened", scriptsBefore.filter((u) => /pdf/i.test(u)).join(", ") || "none" ); await createNotebook(p, "PDF Buch"); /* ── import ── */ const chooser = p.waitForEvent("filechooser"); await p.getByTitle("Import PDF").click(); await (await chooser).setFiles(PDF_PATH); await p.waitForTimeout(4000); check((await p.locator("[data-pdf-page]").count()) > 0, "the document is shown after importing"); check( (await p.locator("[data-pdf-position]").innerText()).includes("/ 2"), "both pages are counted", await p.locator("[data-pdf-position]").innerText() ); await p.screenshot({ path: "/out/pdf-imported.png" }); /* ── proportions ── */ const boxes = await p.$$eval("[data-pdf-page]", (els) => els.map((e) => ({ w: e.offsetWidth, h: e.offsetHeight })) ); check(boxes.length === 2, "both pages are laid out", JSON.stringify(boxes)); // Page one is A4 portrait (595×842), page two the same turned on its side. const ratio1 = boxes[0].h / boxes[0].w; const ratio2 = boxes[1].h / boxes[1].w; check(Math.abs(ratio1 - 842 / 595) < 0.05, "the portrait page keeps its proportions", ratio1.toFixed(2)); check(Math.abs(ratio2 - 595 / 842) < 0.05, "the landscape page is not stretched to match", ratio2.toFixed(2)); /* ── something was actually painted ── */ const inkedPixels = await p.evaluate(() => { const c = document.querySelector("[data-pdf-page] canvas"); if (!c) return -1; const ctx = c.getContext("2d"); const d = ctx.getImageData(0, 0, c.width, Math.min(c.height, 400)).data; let dark = 0; for (let i = 0; i < d.length; i += 4) if (d[i] < 200) dark++; return dark; }); check(inkedPixels > 50, "the page has actual content drawn on it", `${inkedPixels} dark pixels`); const loadedAfter = await p.evaluate(() => performance.getEntriesByType("resource").map((r) => r.name)); check( loadedAfter.some((u) => /pdf.*\.js|pdf\.worker/i.test(u)), "pdf.js was fetched only once it was needed" ); /* ── it stays readable offline ── */ // A reload lands on the dashboard, which has no note list; the notebook has // to be opened first before a row exists to click. const reopen = async () => { await p.locator("button").filter({ hasText: "PDF Buch" }).first().click(); await p.waitForTimeout(1200); await p.locator("[data-note-row]").first().click(); await p.waitForTimeout(4000); }; await p.reload({ waitUntil: "networkidle" }); await p.waitForTimeout(2500); await reopen(); check((await p.locator("[data-pdf-page]").count()) > 0, "the document reopens after a reload"); await p.context().setOffline(true); await p.reload({ waitUntil: "domcontentloaded" }); await p.waitForTimeout(3000); await reopen(); check( (await p.locator("[data-pdf-page]").count()) > 0, "and still opens with the network switched off" ); await p.screenshot({ path: "/out/pdf-offline.png" }); await p.context().setOffline(false); } catch (e) { check(false, "suite ran to the end", e.message); } finally { await session?.cleanup(); const code = report(); await browser.close(); process.exit(code); }