/** * Régression — 2 bugs rapportés (post v7.45.0) : * 1. Logout : le SW servait sa page « hors ligne » sur TOUTE navigation * redirigée (fetch event en redirect:'manual' → opaqueredirect → rejet). * 2. Drag & drop de fichiers : POST /api/local-workspace/upload sans * X-CSRF-Token → 403 (A19 a retiré l'exemption sans équiper l'appel). * * Instance attendue sur FD_BASE_URL (défaut 8080), compte e2e documenté. */ const { test, expect } = require('@playwright/test'); 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'; 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: 8000 }) .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.ok() && resp.status() !== 409) { throw new Error(`register ${resp.status()}: ${await resp.text()}`); } await page.goto(`${FD_BASE}/workspaces`, { waitUntil: 'domcontentloaded' }); await page.waitForURL('**/workspaces', { timeout: 15000 }); } // workspace actif requis par /api/local-workspace/upload (A22) const ws = await page.evaluate(async () => (await fetch('/api/workspaces')).json()); if (!ws.workspaces || ws.workspaces.length === 0) { await page.evaluate(async () => { const csrf = (document.cookie.match(/csrf_token=([^;]+)/) || [])[1] || ''; const r = await fetch('/api/workspaces', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf }, body: JSON.stringify({ name: 'E2E workspace' }), }); const w = await r.json(); await fetch(`/api/workspaces/${w.id}/select`, { method: 'POST', headers: { 'X-CSRF-Token': csrf }, }); }); } } test('logout avec SW : atterrit sur la page login, pas hors ligne', async ({ page }) => { await login(page); await page.evaluate(async () => { if ('serviceWorker' in navigator) await navigator.serviceWorker.ready; }); await page.waitForTimeout(1000); expect(await page.evaluate(() => !!navigator.serviceWorker.controller)).toBe(true); await page.goto(`${FD_BASE}/auth/logout`, { waitUntil: 'domcontentloaded' }); await page.waitForURL(/\/auth\/login/, { timeout: 10000 }); const body = await page.evaluate(() => document.body.innerText); expect(body).not.toContain('hors ligne'); expect(page.url()).toContain('/auth/login'); }); test('drop de fichier : upload accepté (CSRF), 200 et item créé', async ({ page }) => { test.setTimeout(60000); await login(page); await page.goto(`${FD_BASE}/local-workspace`, { waitUntil: 'domcontentloaded' }); await page.waitForTimeout(1000); const uploadResp = []; page.on('response', (r) => { if (r.url().includes('/api/local-workspace/upload')) { uploadResp.push( r.text().then((body) => ({ status: r.status(), body })).catch(() => ({ status: r.status(), body: '' })) ); } }); await page.evaluate(() => { const zone = document.querySelector('[x-data*="wsInitData"]') || document.body; const dt = new DataTransfer(); dt.items.add(new File(['hello world'], 'e2e-drop.txt', { type: 'text/plain' })); zone.dispatchEvent(new DragEvent('dragover', { dataTransfer: dt, bubbles: true, cancelable: true })); zone.dispatchEvent(new DragEvent('drop', { dataTransfer: dt, bubbles: true, cancelable: true })); }); await expect.poll(() => uploadResp.length, { timeout: 15000 }).toBeGreaterThan(0); const resp = await uploadResp[0]; expect(resp.status, resp.body).toBe(200); // Nettoyage : l'upload crée une page fichier → on la supprime. const parsed = JSON.parse(resp.body || '{}'); for (const item of parsed.items || []) { if (!item.id) continue; await page.evaluate(async (pid) => { const csrf = (document.cookie.match(/csrf_token=([^;]+)/) || [])[1] || ''; await fetch('/api/local-workspace/items/' + pid, { method: 'DELETE', headers: { 'X-CSRF-Token': csrf }, }); }, item.id); } });