fix: viewer PDF (TOC, largeur) et plein ecran assistant (BUG-061 a BUG-063)
CI / lint (push) Successful in 1m36s
CI / security (push) Successful in 1m11s
CI / test (push) Successful in 3m33s
CI / build (push) Successful in 1m2s
CI / e2e (push) Successful in 11m23s

This commit is contained in:
2026-09-17 20:45:38 -04:00
parent 2e2a33cef3
commit f7e068baed
16 changed files with 309 additions and 21 deletions
+45
View File
@@ -89,4 +89,49 @@ test.describe('PDF viewer — affichage inline (BUG-060)', () => {
// Aucune violation CSP liée à object-src pendant l'ouverture
expect(cspViolations).toEqual([]);
});
});
test.describe('PDF viewer — TOC & plein largeur', () => {
test('les entrées de la TOC rechargent l\'iframe sur la page ciblée (#page=N)', async ({ page }) => {
await login(page);
await openFile(page, 'TestVault', 'sample-pdf-toc.pdf');
const tocLinks = page.locator('#content-area .pdf-toc a[data-page]');
await expect(tocLinks).toHaveCount(3, { timeout: 10000 });
await page.locator('#content-area .pdf-toc a[data-page="3"]').click();
await expect(page.locator('#content-area .pdf-iframe')).toHaveAttribute(
'src',
/\/pdf\/stream\?path=.*#page=3$/,
{ timeout: 5000 },
);
});
test('le PDF occupe toute la largeur quand la navigation est masquée', async ({ page }) => {
await login(page);
await openFile(page, 'TestVault', 'sample-pdf-toc.pdf');
await expect(page.locator('#content-area .pdf-viewer-container')).toBeVisible({ timeout: 10000 });
// Masquer la barre de navigation (bouton réel).
await page.locator('#sidebar-toggle-btn').click();
await expect(page.locator('#sidebar')).toHaveClass(/hidden/);
const widths = await page.evaluate(() => {
const area = document.getElementById('content-area');
const cs = getComputedStyle(area);
const container = document.querySelector('.pdf-viewer-container');
const contentWidth =
area.clientWidth - parseFloat(cs.paddingLeft) - parseFloat(cs.paddingRight);
return {
container: container.getBoundingClientRect().width,
contentWidth,
maxWidth: cs.maxWidth,
};
});
// Le plafond de lecture (1200px) ne doit plus s'appliquer au viewer PDF.
expect(widths.maxWidth).toBe('none');
expect(Math.abs(widths.container - widths.contentWidth)).toBeLessThan(2);
});
});
+23
View File
@@ -786,6 +786,29 @@ async function main() {
panel.remove();
});
// ── 10b. Fullscreen toggle must beat the persisted inline width ──
await test("fullscreen button toggles the panel and CSS lifts the inline width", async () => {
const { readFileSync } = await import("node:fs");
const css = readFileSync(path.resolve(JS_DIR, "..", "style.css"), "utf-8");
assert.match(
css,
/\.bookslm-panel\.fullscreen\s*\{\s*width:\s*100vw\s*!important;/,
"the fullscreen width must override the inline width written by the resize handle",
);
const b = new BooksLM();
const panel = b._render();
document.body.appendChild(panel);
const btn = panel.querySelector(".bookslm-btn-fullscreen");
btn.click();
assert.ok(panel.classList.contains("fullscreen"), "fullscreen class added on first click");
assert.equal(b._isFullscreen, true);
btn.click();
assert.ok(!panel.classList.contains("fullscreen"), "fullscreen class removed on second click");
assert.equal(b._isFullscreen, false);
panel.remove();
});
// ── 11. Formatted markdown rendering ──
await test("_renderMarkdown renders headings, lists, code and tables", () => {
const b = new BooksLM();
+30 -5
View File
@@ -39,12 +39,12 @@ test("viewer.js — PDF branch renders the stream in an <iframe class=pdf-iframe
assert.ok(block, "PDF render block not found");
assert.match(
block[1],
/<iframe src="\$\{pdfUrl\}" class="pdf-iframe"/,
/<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],
/\.pdf-iframe[\s\S]*?src="\$\{pdfUrl\}"/,
/<iframe src="\$\{pdfUrl\}"/,
"iframe src must come from the /pdf/stream URL",
);
});
@@ -54,12 +54,37 @@ test("viewer.js — no <embed>/<object> left in the source", () => {
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", () => {
test("viewer.js — TOC links carry a data-page and never poke contentWindow", () => {
assert.match(
viewer,
/document\.querySelector\('\.pdf-iframe'\)\.contentWindow\.location\.hash='page=\$\{item\.page\}'/,
"TOC links must keep navigating the iframe",
/<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 reloads the iframe 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], /iframe\.src = `\$\{base\}#page=\$\{page\}`/, "the iframe must be reloaded with #page=N");
});
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 ─────────────────────────