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('diag v5: partial-nav ctx menu + rename sidebar/header sync', async ({ page }) => { test.setTimeout(180000); const errors = []; page.on('console', m => { if (m.type() === 'error') errors.push('CONSOLE: ' + m.text()); }); page.on('pageerror', e => errors.push('PAGEERROR: ' + e.message)); // ── 1. Fresh user + workspace ── await page.goto(`${FD_BASE}/auth/login?provider=local`); await page.click('button:has-text("Register")'); await page.fill('#name', 'Diag'); await page.fill('#email', 'diag' + Date.now() + '@flowdeck.local'); await page.fill('#password', FD_PASS); await page.click('.btn-primary'); await page.waitForURL('**/workspaces', { timeout: 20000 }); await page.waitForTimeout(1200); // Dismiss any onboarding agent dialog that may cover the page await page.keyboard.press('Escape').catch(() => {}); await page.waitForTimeout(300); // Create + select a workspace via API (CSRF-exempt), no fragile UI dialogs. const ws = await page.evaluate(async () => { const r = await fetch('/api/workspaces', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: 'Diag WS ' + Date.now() }) }); const d = await r.json(); const s = await fetch('/api/workspaces/' + d.id + '/select', { method: 'POST' }); return { id: d.id, ok: s.ok }; }); console.log('WS:', JSON.stringify(ws)); await page.waitForTimeout(800); // Seed items via API. await page.evaluate(async () => { const mk = (name, type) => fetch('/api/local-workspace/items', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name, type }) }).then(r => r.json()); const p = await mk('RenameMe Doc', 'page'); await mk('Folder A', 'folder'); window.__seedPageId = p.id; }); console.log('STEP1 ok — user + workspace + seeded (all via API)'); // ── 2. PARTIAL nav to /local-workspace (exactly the user's bug path) ── await page.evaluate(() => window.fdNavigate('/local-workspace')); await page.waitForURL('**/local-workspace**', { timeout: 10000 }); await page.waitForFunction(() => document.querySelectorAll('[data-ws-id]').length >= 1, { timeout: 10000 }); await page.waitForTimeout(1500); console.log('STEP2 ok — partial-nav LW tree items:', await page.locator('[data-ws-id]').count()); // ── 3. PARTIAL-nav ctx menu (local-workspace) ── const rows1 = page.locator('[data-ws-id]'); if (await rows1.count()) { await rows1.first().click({ button: 'right' }); await page.waitForTimeout(700); const st = await page.evaluate(() => { const s = window.Alpine && Alpine.store('fdCtx'); return s ? { open: s.open, node: s.node && s.node.name } : { undefined: true }; }); const vis = await page.locator('.fd-ctx-menu').isVisible().catch(() => false); console.log('STEP3 LW ctx (partial-nav): visible=' + vis + ' fdCtx=' + JSON.stringify(st)); await page.screenshot({ path: 'shots/diag_v5_lw_ctx.png' }).catch(() => {}); await page.evaluate(() => { const s = window.Alpine && Alpine.store('fdCtx'); if (s) s.close(); }); } // ── 4. PARTIAL-nav ctx menu (library) ── await page.evaluate(() => window.fdNavigate('/library')); await page.waitForFunction(() => typeof window._libData !== 'undefined', { timeout: 10000 }); await page.waitForTimeout(1500); const libRows = page.locator('.lib-row'); console.log('STEP4 partial /library rows:', await libRows.count(), 'libData:', await page.evaluate(() => typeof window._libData + ':' + typeof (window._libData && window._libData.onRowContextMenu))); if (await libRows.count()) { await libRows.first().click({ button: 'right' }); await page.waitForTimeout(700); const vis = await page.locator('.fd-ctx-menu').isVisible().catch(() => false); const st = await page.evaluate(() => { const s = window.Alpine && Alpine.store('fdCtx'); return s ? { open: s.open, node: s.node && s.node.name } : { undefined: true }; }); console.log('STEP5 LIB ctx (partial-nav): visible=' + vis + ' fdCtx=' + JSON.stringify(st)); await page.screenshot({ path: 'shots/diag_v5_lib_ctx.png' }).catch(() => {}); await page.evaluate(() => { const s = window.Alpine && Alpine.store('fdCtx'); if (s) s.close(); }); } // ── 5. Rename a page via local-workspace tree (partial-nav) ── await page.evaluate(() => window.fdNavigate('/local-workspace')); await page.waitForFunction(() => typeof window._wsData !== 'undefined' && typeof window._wsData.commitRename === 'function', { timeout: 10000 }); await page.waitForFunction(() => document.querySelectorAll('[data-ws-id]').length >= 1, { timeout: 10000 }); await page.waitForTimeout(1200); const rn = await page.evaluate(async () => { const ws = window._wsData; const find = (name) => { const walk = (arr) => { if (!arr) return null; for (const n of arr) { if (n.name === name) return n; const c = walk(n.children); if (c) return c; } return null; }; return walk(ws.displayTree) || walk(ws.tree); }; const target = find('RenameMe Doc'); if (!target) return { err: 'node RenameMe Doc not found' }; ws.renamingId = target.id; ws.renamingName = 'Renamed Via LW'; await ws.commitRename(target); return { id: target.id, name: target.name }; }); console.log('STEP6 rename page:', JSON.stringify(rn)); await page.waitForTimeout(2000); const chk1 = await page.evaluate((pid) => { const row = [...document.querySelectorAll('[data-ws-id]')].find(n => String(n.getAttribute('data-ws-id')) === String(pid)); const sb = document.querySelector('.sidebar-item.ws-tree-item[data-id="' + pid + '"] .page-name'); const head = document.querySelector('.header-breadcrumb'); const crumbs = head ? head.innerText.trim().split(/\s/).filter(Boolean) : []; return { inPageRow: row ? row.innerText.trim() : null, sidebarWsName: sb ? sb.textContent : null, headerCrumbs: crumbs, }; }, rn.id); console.log('STEP7 after rename:', JSON.stringify(chk1, null, 2)); // ── 6. Rename the CURRENT folder → in-page breadcrumb must update (no reload) ── const fld = await page.evaluate(async () => { const ws = window._wsData; const walk = (arr) => { if (!arr) return null; for (const n of arr) { if (n.is_folder && n.name === 'Folder A') return n; const c = walk(n.children); if (c) return c; } return null; }; const folder = walk(ws.displayTree) || walk(ws.tree); if (!folder) return { err: 'folder not found' }; return { id: folder.id, name: folder.name }; }); // Partial-nav INTO the folder so the server renders the breadcrumb (folderStack) await page.evaluate((fid) => window.fdNavigate('/local-workspace?folder=' + fid), fld.id); await page.waitForFunction(() => { const ws = window._wsData; return ws && ws.folderStack && ws.folderStack.length >= 1; }, { timeout: 10000 }); await page.waitForTimeout(1200); const beforeCrumb = await page.evaluate(() => { const c = document.querySelector('.unified-breadcrumb .bc-current'); return c ? c.textContent.trim() : null; }); console.log('STEP8 crumb before rename:', beforeCrumb); const rnf = await page.evaluate(async (fid) => { const ws = window._wsData; const cur = ws.folderStack && ws.folderStack[ws.folderStack.length - 1]; if (!cur) return { err: 'no folderStack' }; ws.renamingId = cur.id; ws.renamingName = 'Folder Renamed'; await ws.commitRename(cur); return { id: cur.id, name: cur.name, bc: ws.breadcrumb.map(b => b.name) }; }, fld.id); console.log('STEP9 rename folder:', JSON.stringify(rnf)); await page.waitForTimeout(2000); const chk2 = await page.evaluate((fid) => { const crumb = document.querySelector('.unified-breadcrumb .bc-current'); const sb = document.querySelector('.sidebar-item.ws-tree-item[data-id="' + fid + '"] .page-name'); return { crumbLabel: crumb ? crumb.textContent.trim() : null, sidebarWsName: sb ? sb.textContent : null, }; }, fld.id); console.log('STEP10 after folder rename:', JSON.stringify(chk2, null, 2)); await page.screenshot({ path: 'shots/diag_v5_folder_rename.png' }).catch(() => {}); console.log('ERRORS:'); errors.slice(0, 40).forEach(e => console.log(e)); });