fix(workspace): compteurs de tags dynamiques + counts scoped par workspace
FlowDeck CI / lint (push) Successful in 53s
FlowDeck CI / test (push) Successful in 5m47s
FlowDeck CI / docker (push) Successful in 48s

This commit is contained in:
2026-09-14 20:26:27 -04:00
parent 98af112ba1
commit 0d475c3d2d
3 changed files with 149 additions and 8 deletions
+113
View File
@@ -0,0 +1,113 @@
const { test } = 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 tags: chip counts update dynamically on add/remove', async ({ page }) => {
test.setTimeout(120000);
const errors = [];
page.on('console', m => { if (m.type() === 'error') errors.push('CONSOLE: ' + m.text()); });
page.on('pageerror', e => errors.push('PAGEERROR: ' + e.message));
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(1000);
await page.keyboard.press('Escape').catch(() => {});
await page.waitForTimeout(300);
const ws = await page.evaluate(async () => {
const r = await fetch('/api/workspaces', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'Tag WS ' + Date.now() })
});
const d = await r.json();
await fetch('/api/workspaces/' + d.id + '/select', { method: 'POST' });
return d;
});
console.log('WS:', JSON.stringify(ws));
await page.waitForTimeout(600);
// Seed 2 files
const seeds = await page.evaluate(async () => {
const mk = (name) => fetch('/api/local-workspace/items', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, type: 'page' })
}).then(r => r.json());
const a = await mk('Doc A');
const b = await mk('Doc B');
return { a: a.id, b: b.id };
});
console.log('SEEDED:', JSON.stringify(seeds));
// Tag Doc A via API to set up the scenario
const tagRes = await page.evaluate(async (id) => {
const r = await fetch('/api/local-workspace/items/' + id + '/tags', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'important', color: '#E03E3E' })
});
return r.status;
}, seeds.a);
console.log('TAG A status:', tagRes);
// Load local-workspace (full load first for simplicity)
await page.goto(`${FD_BASE}/local-workspace`);
await page.waitForFunction(() => typeof window._wsData !== 'undefined', { timeout: 10000 });
await page.waitForTimeout(2500);
const readChip = async (label) => {
return page.evaluate((tagName) => {
const ws = window._wsData;
const t = (ws.workspaceTags || []).find(x => String(x.id) === String(tagName));
const chip = [...document.querySelectorAll('.tag-filter-bar .tag-chip')]
.find(b => b.textContent.includes(tagName));
return {
dataCount: t ? t.count : null,
chipText: chip ? chip.textContent.trim() : null,
wsTags: (ws.workspaceTags || []).map(x => x.id + ':' + x.count),
};
}, 'important');
};
console.log('INIT:', JSON.stringify(await readChip()));
// Add SAME tag to Doc B via CONTEXT MENU handler (ctxAddNewTag) → 1 → 2
const ctxAdd = await page.evaluate(async (id) => {
const ws = window._wsData;
const walk = (arr) => { if (!arr) return null; for (const n of arr) { if (n.id === id) return n; const c = walk(n.children); if (c) return c; } return null; };
const node = walk(ws.displayTree) || walk(ws.tree);
if (!node) return { err: 'node not found' };
const store = window.Alpine && Alpine.store('fdCtx');
if (!store) return { err: 'no fdCtx store' };
store.node = node;
await ws.ctxAddNewTag('important', '#E03E3E');
return { id: node.id, name: node.name };
}, seeds.b);
console.log('CTX ADD B:', JSON.stringify(ctxAdd));
await page.waitForTimeout(1000);
console.log('AFTER CTX ADD B:', JSON.stringify(await readChip()));
// Remove tag from Doc A via CONTEXT MENU handler (ctxRemoveTag) → 2 → 1
const ctxRm = await page.evaluate(async (id) => {
const ws = window._wsData;
const walk = (arr) => { if (!arr) return null; for (const n of arr) { if (n.id === id) return n; const c = walk(n.children); if (c) return c; } return null; };
const node = walk(ws.displayTree) || walk(ws.tree);
if (!node) return { err: 'node not found' };
const tag = (node.tags || []).find(t => t.name === 'important');
if (!tag) return { err: 'tag not on node' };
const store = window.Alpine && Alpine.store('fdCtx');
store.node = node;
ws.ctxRemoveTag(tag.id);
return { id: node.id, removedTagId: tag.id };
}, seeds.a);
console.log('CTX RM A:', JSON.stringify(ctxRm));
await page.waitForTimeout(1000);
console.log('AFTER CTX RM A:', JSON.stringify(await readChip()));
console.log('ERRORS:');
errors.slice(0, 20).forEach(e => console.log(e));
});