124 lines
4.8 KiB
JavaScript
124 lines
4.8 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* ObsiGate — Viewer PDF non-regression tests (BUG-060).
|
|
*
|
|
* Static checks on the source of the PDF inline viewer:
|
|
* - BUG-060 : the CSP set by BUG-034 puts `object-src 'none'`, which blocks
|
|
* `<embed>`/`<object>`. The PDF body was therefore never rendered (blank
|
|
* pages). The viewer must now use an `<iframe>` — permitted by
|
|
* `frame-src 'self'` since the PDF stream URL is same-origin.
|
|
*
|
|
* Usage: node tests/frontend/pdf-viewer.test.mjs
|
|
*/
|
|
|
|
import { strict as assert } from "node:assert";
|
|
import { readFileSync } from "node:fs";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
const ROOT = path.join(__dirname, "..", "..");
|
|
|
|
const viewer = readFileSync(path.join(ROOT, "frontend", "js", "viewer.js"), "utf8");
|
|
const main = readFileSync(path.join(ROOT, "backend", "main.py"), "utf8");
|
|
const css = readFileSync(path.join(ROOT, "frontend", "style.css"), "utf8");
|
|
|
|
function test(label, fn) {
|
|
try {
|
|
fn();
|
|
console.log(" \u2713 " + label);
|
|
} catch (err) {
|
|
console.error(" \u2717 " + label + "\n " + err.message);
|
|
process.exitCode = 1;
|
|
}
|
|
}
|
|
|
|
// ── viewer.js : le rendu PDF passe par une iframe ─────────────────────────
|
|
test("viewer.js — PDF branch renders the stream in an <iframe class=pdf-iframe>", () => {
|
|
const block = viewer.match(/if \(data\.is_pdf\) \{([\s\S]*?)\n \}/);
|
|
assert.ok(block, "PDF render block not found");
|
|
assert.match(
|
|
block[1],
|
|
/<iframe src="\$\{pdfUrl\}" data-pdf-url="\$\{pdfUrl\}" class="pdf-iframe"/,
|
|
"PDF must use <iframe>, not <embed>/<object> (CSP object-src 'none' otherwise blocks it)",
|
|
);
|
|
assert.match(
|
|
block[1],
|
|
/<iframe src="\$\{pdfUrl\}"/,
|
|
"iframe src must come from the /pdf/stream URL",
|
|
);
|
|
});
|
|
|
|
test("viewer.js — no <embed>/<object> left in the source", () => {
|
|
assert.doesNotMatch(viewer, /<embed\b/i, "<embed> is blocked by CSP object-src 'none'");
|
|
assert.doesNotMatch(viewer, /<object\b/i, "<object> is blocked by CSP object-src 'none'");
|
|
});
|
|
|
|
test("viewer.js — TOC links carry a data-page and never poke contentWindow", () => {
|
|
assert.match(
|
|
viewer,
|
|
/<a href="#" data-page="\$\{item\.page\}">/,
|
|
"TOC links must expose the target page via data-page",
|
|
);
|
|
assert.doesNotMatch(
|
|
viewer,
|
|
/contentWindow\.location\.hash\s*=/,
|
|
"contentWindow is about:blank in the native PDF viewer: hash navigation never reaches the document",
|
|
);
|
|
});
|
|
|
|
test("viewer.js — navigatePdfToPage forces a reload with the #page fragment", () => {
|
|
const fn = viewer.match(/export function navigatePdfToPage\(area, page\) \{([\s\S]*?)\n\}/);
|
|
assert.ok(fn, "navigatePdfToPage helper not found");
|
|
assert.match(fn[1], /data-pdf-url/, "the base URL must be preserved without the fragment");
|
|
assert.match(
|
|
fn[1],
|
|
/_pdfpage=\$\{Date\.now\(\)\}#page=\$\{page\}/,
|
|
"the query must change to force a reload (a fragment-only change is ignored by the native viewer)",
|
|
);
|
|
});
|
|
|
|
test("style.css — full-bleed viewers ignore the centered reading width", () => {
|
|
assert.match(
|
|
css,
|
|
/\.sidebar\.hidden ~ \.content-wrapper \.content-area:has\(\.pdf-viewer-container\)/,
|
|
"PDF viewer must fill the width when the navigation sidebar is hidden",
|
|
);
|
|
const rule = css.match(
|
|
/\.content-area:has\(\.pdf-viewer-container\)[\s\S]*?\{([^}]*)\}/,
|
|
);
|
|
assert.ok(rule, "full-bleed rule not found");
|
|
assert.match(rule[1], /max-width:\s*none/, "the 1200px reading cap must be lifted");
|
|
});
|
|
|
|
// ── backend : la CSP autorise le cadre same-origin ─────────────────────────
|
|
test("backend CSP — frame-src 'self' allows same-origin iframes", () => {
|
|
const csp = main.match(/frame-src ([^";]+);/);
|
|
assert.ok(csp, "CSP frame-src directive not found");
|
|
assert.ok(
|
|
csp[1].includes("'self'"),
|
|
`frame-src must allow 'self' (found: ${csp[1]}) — otherwise the PDF iframe is blocked`,
|
|
);
|
|
});
|
|
|
|
test("backend CSP — object-src 'none' stays in place (no defusing)", () => {
|
|
assert.match(
|
|
main,
|
|
/object-src 'none';/,
|
|
"object-src must stay locked to 'none': the fix is moving to <iframe>, not weakening CSP",
|
|
);
|
|
});
|
|
|
|
// ── style.css : l'iframe garde une hauteur utile ───────────────────────────
|
|
test("style.css — .pdf-iframe fills the viewer body", () => {
|
|
const rule = css.match(/\.pdf-iframe\s*\{([^}]*)\}/);
|
|
assert.ok(rule, ".pdf-iframe rule not found");
|
|
assert.match(rule[1], /flex:\s*1/, "iframe must stretch to fill the available height");
|
|
assert.match(rule[1], /min-height/, "iframe must keep its minimum height");
|
|
});
|
|
|
|
if (process.exitCode) {
|
|
console.error("\nPDF viewer tests FAILED");
|
|
} else {
|
|
console.log("\nAll PDF viewer tests passed.");
|
|
} |