Corrige deux régressions de /local-workspace :
- le chemin du header restait bloqué sur « Home / <workspace> » quel que
soit le dossier affiché : la route rend désormais breadcrumb_items
(Home / <workspace> / <dossier> / <sous-dossier>, niveaux cliquables,
collapse « … » au-delà de 4) et la navigation sans rechargement recalcule
le chemin via l'event flowdeck:breadcrumb-changed ;
- le clic sur un dossier du sidebar affichait TOUS les composants à la
fois : Alpine.data('wsInitData') retournait le même objet singleton, le
2e montage (navigation partielle) levait « Cannot redefine property:
\ » et initTree abandonnait, laissant tout le contenu au state
brut. La factory retourne désormais une enveloppe fraîche par montage
qui délègue à l'état réactif partagé. #lw-config est aussi relu à chaque
exécution (le 2e montage gardait le folder_id du 1er chargement).
Inclus également le travail en cours de l'arbre : Library (colonnes Last
visited/Source, ordre d'en-tête, favoris à icônes Workspace), Meeting
Notes (bloc, CSS, routes, docs), coloration de code hljs, badges
favori/publié dans l'arbre local-workspace, docs (DATA_MODEL,
architectures) et tests associés.
122 lines
5.9 KiB
JavaScript
122 lines
5.9 KiB
JavaScript
/**
|
|
* /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([]);
|
|
});
|