|
|
|
@@ -0,0 +1,171 @@
|
|
|
|
|
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));
|
|
|
|
|
});
|