/** * /local-workspace — le chemin du header suit la navigation dans les * dossiers, et une navigation partielle vers un sous-dossier remonte la * page Alpine (avant : « Cannot redefine property: $nextTick » → tous les * composants restés visibles à la fois). * * Parcours : sidebar Docs → sidebar Sub → clic dans la vue sur Leaf → * breadcrumb interne Docs → crumb workspace du header. */ const { test, expect } = require('@playwright/test'); const FD_BASE = process.env.FD_BASE_URL || 'http://localhost:8080'; const FD_PASS = process.env.FD_PASS || 'e2e-secret-123'; test('local-workspace: chemin header + navigation dossiers (partielle et interne)', async ({ page }) => { test.setTimeout(180000); const errors = []; page.on('console', m => { if (m.type() === 'warning' && m.text().includes('Cannot redefine')) errors.push(m.text()); }); page.on('pageerror', e => errors.push('PAGEERROR: ' + e.message)); // ── 1. Utilisateur + workspace + arborescence Docs/Sub/Leaf ── await page.goto(`${FD_BASE}/auth/login?provider=local`); await page.click('button:has-text("Register")'); await page.fill('#name', 'LWBC'); await page.fill('#email', 'lwbc' + Date.now() + '@flowdeck.local'); await page.fill('#password', FD_PASS); await page.click('.btn-primary'); await page.waitForURL('**/workspaces', { timeout: 20000 }); await page.waitForTimeout(800); await page.keyboard.press('Escape').catch(() => {}); const ids = await page.evaluate(async () => { const csrf = decodeURIComponent((document.cookie.match(/csrf_token=([^;]+)/) || [])[1] || ''); const H = { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf }; const r = await fetch('/api/workspaces', { method: 'POST', headers: H, body: JSON.stringify({ name: 'LWBC WS ' + Date.now() }) }); const d = await r.json(); await fetch('/api/workspaces/' + d.id + '/select', { method: 'POST', headers: H }); const mk = (name, type, parent) => fetch('/api/local-workspace/items', { method: 'POST', headers: H, body: JSON.stringify({ name, type, parent_id: parent || null }) }).then(x => x.json()); const docs = await mk('Docs', 'folder'); const sub = await mk('Sub', 'folder', docs.id); const leaf = await mk('Leaf', 'folder', sub.id); await mk('Page In Leaf', 'page', leaf.id); return { wsName: '', docs: docs.id, sub: sub.id, leaf: leaf.id, ws: d.id }; }); await page.goto(`${FD_BASE}/local-workspace`); await page.waitForFunction(() => typeof window._wsData !== 'undefined', { timeout: 15000 }); const wsName = await page.evaluate(() => { const el = document.querySelector('.ws-main .unified-breadcrumb .bc-root'); return el ? el.textContent.trim() : ''; }); expect(wsName).toBeTruthy(); const headerPath = () => page.evaluate(() => { const el = document.querySelector('.header-breadcrumb'); return el ? el.innerText.replace(/\s+/g, ' ').replace(/…/g, '...').trim() : null; }); expect(await headerPath()).toContain(wsName); // Composants Alpine peints à l'état brut (x-show non évalué) — hors pages // légitimement visibles (arbre, ligne « parent », barre de tags). // checkVisibility() prend en compte les ancêtres (display:contents sur // .bc-edit-wrap ferme l'input sans que son propre display change). const rawPainted = () => page.evaluate(() => { const sels = ['.ws-dropdown', '.ws-select-menu', '.upload-progress', '.undo-toast', '.move-modal-overlay', '.ws-search-bar', '.ws-select-bar', '.more-menu', '.filter-row', '.bc-edit-wrap']; return sels.filter(s => [...document.querySelectorAll(s)] .some(e => e.checkVisibility({ opacityProperty: true, visibilityProperty: true }))); }); await page.goto(`${FD_BASE}/local-workspace`); await page.waitForFunction(() => typeof window._wsData !== 'undefined', { timeout: 15000 }); await expect.poll(headerPath, { timeout: 10000 }).toContain(wsName); // ── 2. Sidebar : clic sur Docs puis sur Sub (navigation partielle) ── const clickSidebarFolder = async (id) => { await page.evaluate((fid) => { const el = document.querySelector('.sidebar-item.ws-tree-item[data-id="' + fid + '"] .tree-folder-link'); if (el) el.click(); }, id); await page.waitForURL('**folder=' + id, { timeout: 15000 }); await page.waitForTimeout(1200); }; await clickSidebarFolder(ids.docs); expect(await headerPath()).toBe(`Home / ${wsName} / Docs`); expect(await rawPainted()).toEqual([]); await clickSidebarFolder(ids.sub); expect(await headerPath()).toBe(`Home / ${wsName} / Docs / Sub`); expect(await rawPainted()).toEqual([]); // ── 3. Clic sur un sous-dossier DANS la vue (sans rechargement) ── await page.evaluate((fid) => { const el = document.querySelector('.ws-main [data-folder-id="' + fid + '"] .name.folder'); if (el) el.click(); }, ids.leaf); await expect.poll(headerPath, { timeout: 10000 }).toContain('/ Sub / Leaf'); expect(await headerPath()).toContain(wsName); expect(await rawPainted()).toEqual([]); // ── 4. Breadcrumb interne : clic sur Docs → les deux chemins suivent ── const inPage = () => page.evaluate(() => [...document.querySelectorAll('.ws-main .unified-breadcrumb a')].map(a => a.textContent.trim())); expect(await inPage()).toEqual([expect.stringContaining(wsName), 'Docs', 'Sub', 'Leaf']); await page.click('.ws-main .unified-breadcrumb a:has-text("Docs")'); await expect.poll(headerPath, { timeout: 10000 }).toBe(`Home / ${wsName} / Docs`); expect(await inPage()).toEqual([expect.stringContaining(wsName), 'Docs']); // ── 5. Crumb workspace du header → retour à la racine ── await page.click('.header-breadcrumb a:has-text("' + wsName + '")'); await page.waitForURL('**/local-workspace', { timeout: 15000 }); await expect.poll(headerPath, { timeout: 10000 }).toBe(`Home / ${wsName}`); expect(await rawPainted()).toEqual([]); expect(errors).toEqual([]); });