139 lines
5.6 KiB
JavaScript
139 lines
5.6 KiB
JavaScript
/**
|
|
* 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}"]`);
|
|
// Le vault peut être replié (auth activée) : l'étendre avant de chercher le fichier.
|
|
if (!(await treeItem.count())) {
|
|
await page.locator(`.tree-item.vault-item[data-vault="${vault}"]`).first().click();
|
|
await treeItem.waitFor({ state: 'attached', timeout: 8000 });
|
|
}
|
|
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([]);
|
|
});
|
|
});
|
|
|
|
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();
|
|
// Le changement de query force un rechargement (un simple changement de
|
|
// fragment est ignoré par le lecteur PDF natif), puis #page=3 est appliqué.
|
|
await expect(page.locator('#content-area .pdf-iframe')).toHaveAttribute(
|
|
'src',
|
|
/\/pdf\/stream\?path=.*&_pdfpage=\d+#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);
|
|
});
|
|
}); |