fix: /local-workspace — chemin du header + clic sur un dossier du sidebar (v7.69.9)
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.
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* /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([]);
|
||||
});
|
||||
Reference in New Issue
Block a user