fix(pdf): affichage des pages du viewer PDF via iframe (BUG-060)
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* E2E tests for ObsiGate PDF inline viewer (BUG-060).
|
||||
*
|
||||
* Regression : la CSP posée par BUG-034 (`object-src 'none'`) bloque l'élément
|
||||
* `<embed>` qui servait le PDF. Le viewer doit rendre le stream dans une
|
||||
* `<iframe>` (autorisée par `frame-src 'self'`).
|
||||
*
|
||||
* Fixtures : `test_vault/sample-pdf.pdf` (3 pages, texte simple).
|
||||
*
|
||||
* Run (local):
|
||||
* BASE_URL=http://localhost:2029 npx playwright test tests/e2e/pdf-viewer.spec.js
|
||||
* BASE_URL=http://localhost:2029 npx playwright test tests/e2e/pdf-viewer.spec.js --headed
|
||||
*/
|
||||
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
const BASE = process.env.BASE_URL || 'http://localhost:2029';
|
||||
|
||||
const CREDS = {
|
||||
username: process.env.OBSIGATE_USER || 'admin',
|
||||
password: process.env.OBSIGATE_PASS || 'test123',
|
||||
};
|
||||
|
||||
async function login(page) {
|
||||
await page.goto(BASE);
|
||||
const loginForm = page.locator('#login-screen');
|
||||
await expect(loginForm).toBeVisible({ timeout: 5000 }).catch(() => {});
|
||||
if (await loginForm.isVisible()) {
|
||||
await page.fill('#login-username', CREDS.username);
|
||||
await page.fill('#login-password', CREDS.password);
|
||||
await page.click('#login-btn');
|
||||
}
|
||||
await page.waitForFunction(() => window.__OBSIGATE_BOOTED === true, { timeout: 20000 });
|
||||
}
|
||||
|
||||
async function openFile(page, vault, filePath) {
|
||||
const treeItem = page.locator(`.tree-item[data-vault="${vault}"][data-path="${filePath}"]`);
|
||||
if (!(await treeItem.isVisible().catch(() => false))) {
|
||||
const vaultItem = page.locator(`.vault-item[data-vault="${vault}"]`).first();
|
||||
if (await vaultItem.count() > 0) {
|
||||
await vaultItem.click();
|
||||
await page.waitForTimeout(800);
|
||||
}
|
||||
}
|
||||
await treeItem.dblclick({ timeout: 5000 });
|
||||
await page.waitForTimeout(500);
|
||||
}
|
||||
|
||||
test.describe('PDF viewer — affichage inline (BUG-060)', () => {
|
||||
|
||||
test('ouvre un PDF dans une iframe (pas d\'<embed>) et le stream charge sans violation CSP', async ({ page }) => {
|
||||
const cspViolations = [];
|
||||
page.on('console', (msg) => {
|
||||
const text = msg.text();
|
||||
if (text.includes('Content Security Policy') && (text.includes('object-src') || text.includes('Refused'))) {
|
||||
cspViolations.push(text);
|
||||
}
|
||||
});
|
||||
|
||||
await login(page);
|
||||
|
||||
// Le stream est demandé au moment où le viewer monte l'iframe : enregistrer
|
||||
// l'écoute AVANT d'ouvrir le fichier (sinon la réponse est déjà passée).
|
||||
const streamResponsePromise = page.waitForResponse(
|
||||
(r) => r.url().includes('/pdf/stream') && (r.status() === 200 || r.status() === 206),
|
||||
{ timeout: 15000 },
|
||||
);
|
||||
|
||||
await openFile(page, 'TestVault', 'sample-pdf.pdf');
|
||||
|
||||
const iframe = page.locator('#content-area .pdf-iframe');
|
||||
await expect(iframe).toBeVisible({ timeout: 15000 });
|
||||
await expect(iframe).toHaveAttribute('src', /\/api\/file\/TestVault\/pdf\/stream\?path=/);
|
||||
|
||||
// La balise doit être une iframe (le <embed>/<object> serait bloqué par CSP)
|
||||
const tagName = await iframe.evaluate((el) => el.tagName);
|
||||
expect(tagName).toBe('IFRAME');
|
||||
expect(await page.locator('#content-area embed, #content-area object').count()).toBe(0);
|
||||
|
||||
// La barre d'outils indique le nombre de pages du PDF
|
||||
await expect(page.locator('.pdf-info')).toContainText('3 pages');
|
||||
|
||||
// Le stream est bien servi en application/pdf (200 ou 206 Range)
|
||||
const streamResp = await streamResponsePromise;
|
||||
expect(streamResp.headers()['content-type'] || '').toContain('application/pdf');
|
||||
|
||||
// Le cadre embarque réellement le document PDF (navigateur natif)
|
||||
const pdfFrame = await iframe.contentFrame();
|
||||
expect(pdfFrame).not.toBeNull();
|
||||
|
||||
// Aucune violation CSP liée à object-src pendant l'ouverture
|
||||
expect(cspViolations).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,95 @@
|
||||
#!/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\}" class="pdf-iframe"/,
|
||||
"PDF must use <iframe>, not <embed>/<object> (CSP object-src 'none' otherwise blocks it)",
|
||||
);
|
||||
assert.match(
|
||||
block[1],
|
||||
/\.pdf-iframe[\s\S]*?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 still targets the pdf-iframe via contentWindow", () => {
|
||||
assert.match(
|
||||
viewer,
|
||||
/document\.querySelector\('\.pdf-iframe'\)\.contentWindow\.location\.hash='page=\$\{item\.page\}'/,
|
||||
"TOC links must keep navigating the iframe",
|
||||
);
|
||||
});
|
||||
|
||||
// ── 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.");
|
||||
}
|
||||
Reference in New Issue
Block a user