const { test, expect } = require('@playwright/test'); /** * Aperçu CSP strict (A20 phase 3) : charge la page avec le build CSP * d'Alpine (fichier officiel `@alpinejs/csp`, 0 eval) servi à la place de * alpine.min.js via interception — SANS déployer. Toute expression que le * parseur maison ne digère pas = pageerror « CSP Parser Error » (filet) ; * les x-html restants = directive interdite du build (console error). * Quand toutes les surfaces passent ici → bascule réelle + retrait * d'unsafe-eval (ROADMAP A20 phase 3). */ const FD_BASE = process.env.FD_BASE_URL || 'http://localhost:8080'; const USER = process.env.FD_USER || 'e2e@flowdeck.local'; const PASS = process.env.FD_PASS || 'e2e-secret-123'; test.use({ serviceWorkers: 'block' }); const errors = []; test.beforeEach(async ({ page }) => { errors.length = 0; await page.route('**/static/js/alpine.min.js', (route) => route.fulfill({ path: require('path').join(__dirname, 'fixtures', 'alpine.csp.js'), contentType: 'application/javascript', }) ); page.on('console', (m) => { if (m.type() !== 'error') return; if (/Failed to load resource/.test(m.text())) return; errors.push(m.text()); }); page.on('pageerror', (e) => errors.push('pageerror: ' + e.message)); }); test.afterEach(() => expect(errors).toEqual([])); async function login(page) { await page.goto(`${FD_BASE}/auth/login?provider=local`, { waitUntil: 'domcontentloaded' }); await page.fill('#email', USER); await page.fill('#password', PASS); await page.click('.btn-primary'); const ok = await page .waitForURL('**/workspaces', { timeout: 10000 }) .then(() => true) .catch(() => false); if (!ok) { const resp = await page.request.post(`${FD_BASE}/auth/register`, { data: { email: USER, password: PASS, name: 'E2E' }, }); if (resp.status() === 409) throw new Error('compte e2e existant — FD_USER/FD_PASS incorrects'); if (!resp.ok()) throw new Error(`register ${resp.status()}: ${await resp.text()}`); await page.goto(`${FD_BASE}/workspaces`, { waitUntil: 'domcontentloaded' }); await page.waitForURL('**/workspaces', { timeout: 15000 }); } } test('A20-ph3 : library sous build Alpine CSP', async ({ page }) => { await login(page); await page.goto(`${FD_BASE}/library`, { waitUntil: 'domcontentloaded' }); await page.waitForTimeout(600); // le composant est lié par le registre Alpine.data (scope CSP) const bound = await page.evaluate(() => { const el = document.querySelector('[x-data]'); if (!el || !window.Alpine) return 'absent'; try { const d = window.Alpine.$data(el); return d && typeof d === 'object' ? 'ok' : 'vide'; } catch (e) { return 'throw:' + e.message; } }); expect(bound).toBe('ok'); // icône du empty-state : x-html remplacé par x-init + Alpine.effect await expect(page.locator('#lib-empty .empty-icon')).toBeVisible({ timeout: 8000 }); const svg = await page.evaluate( () => document.querySelector('#lib-empty .empty-icon').innerHTML ); expect(svg).toContain(' document.activeElement && document.activeElement.id === 'lib-search-input' ); expect(focused).toBe(true); });