fix: side peek des bases repasse en vanilla JS + largeur 1100px standard (v7.49.0)
FlowDeck CI / test (push) Failing after 3h13m58s
FlowDeck CI / lint (push) Successful in 2m12s
FlowDeck CI / docker (push) Skipped

- Panneau peek: les bindings Alpine (x-data absent du conteneur) rendaient
  loovverture et le redimensionnement inoperants -> cblage direct sur le document.
- Helper unique window.fdWirePeekResize (app.js): pointer capture, 300px-90vw,
  clic=fermer, largeur persiste fd_peek_width partagee entre les 4 peeks.
- database-table-container margin:0 (tableau colle a gauche, marge Library).
- .lib-container remonte dans app.css (trash etait pleine largeur), .db-index 1100px.
- ObsiGate verifie sans code: creation .xlsx OK (openpyxl, #186).
This commit is contained in:
2026-10-05 22:47:36 -04:00
parent 894004a1f5
commit 1d1cdbd618
63 changed files with 9473 additions and 393 deletions
+182
View File
@@ -0,0 +1,182 @@
const { test, expect } = require('@playwright/test');
/**
* Sidebar — navigation par onglets compressibles (Home / Chat / Meeting / Inbox).
*
* - ordre exact des onglets ;
* - l'onglet actif est une pilule avec son libellé, les autres sont réduits à
* l'icône (largeur mesurée) ;
* - chaque sélection remplace le contenu de la barre latérale ;
* - les panneaux Chat / Meeting / Inbox interrogent leurs vraies API.
*/
const FD_BASE = process.env.FD_BASE_URL || 'http://localhost:8080';
const USER = process.env.FD_USER || '[email protected]';
const PASS = process.env.FD_PASS || 'e2e-secret-123';
test.use({ serviceWorkers: 'block' });
async function login(page) {
await page.goto(`${FD_BASE}/auth/login?provider=local`, { waitUntil: 'domcontentloaded' });
await page.fill('#email', USER);
await page.fill('#password', PASS);
await page.click('.btn-primary');
try {
await page.waitForURL('**/workspaces', { timeout: 8000 });
} catch (e) {
// Compte absent de l'instance (harnais de test documenté du repo, comme
// smoke.spec.js) : on le crée puis on rejoue la connexion.
const resp = await page.request.post(`${FD_BASE}/auth/register`, {
data: { email: USER, password: PASS, name: 'E2E' },
});
if (!resp.ok()) throw new Error(`register ${resp.status()}: ${await resp.text()}`);
await page.goto(`${FD_BASE}/auth/login?provider=local`, { waitUntil: 'domcontentloaded' });
await page.fill('#email', USER);
await page.fill('#password', PASS);
await page.click('.btn-primary');
await page.waitForURL('**/workspaces', { timeout: 15000 });
}
}
const panelText = (page, tab) =>
page.evaluate(
(t) => {
const el = document.querySelector(`.sidebar-tab-panel[x-show*="'${t}'"]`);
if (!el) return null;
const hidden = el.offsetParent === null;
return { hidden, text: (el.innerText || '').trim().slice(0, 200) };
},
tab
);
test('les quatre onglets se succèdent et le contenu bascule', async ({ page }) => {
await login(page);
await page.goto(`${FD_BASE}/workspaces`, { waitUntil: 'domcontentloaded' });
await page.waitForSelector('.sidebar-nav-row');
// ordre des onglets
const order = await page.$$eval('.sidebar-nav-row [data-tab]', (els) =>
els.map((e) => e.getAttribute('data-tab'))
);
expect(order).toEqual(['home', 'chat', 'meetings', 'inbox']);
// Home actif par défaut : pilule (libellé visible), les autres réduite à l'icône
let labels = await page.$$eval('.sidebar-nav-row [data-tab]', (els) =>
els.map((e) => ({
tab: e.getAttribute('data-tab'),
active: e.classList.contains('active'),
labelVisible: e.querySelector('.nav-tab-label').offsetParent !== null,
width: Math.round(e.getBoundingClientRect().width),
}))
);
expect(labels.find((l) => l.tab === 'home').labelVisible).toBe(true);
labels.filter((l) => l.tab !== 'home').forEach((l) => {
expect(l.active).toBe(false);
expect(l.labelVisible).toBe(false);
});
const homeWidth = labels.find((l) => l.tab === 'home').width;
const chatWidth = labels.find((l) => l.tab === 'chat').width;
expect(homeWidth).toBeGreaterThan(chatWidth + 15);
// Chat : le panneau Home disparaît, celui de Chat est visible
await page.click('.sidebar-nav-row [data-tab="chat"]');
await page.waitForTimeout(400);
expect((await panelText(page, 'home')).hidden).toBe(true);
const chat = await panelText(page, 'chat');
expect(chat.hidden).toBe(false);
expect(chat.text).toMatch(/CHAT/i);
labels = await page.$$eval('.sidebar-nav-row [data-tab]', (els) =>
els.map((e) => ({
tab: e.getAttribute('data-tab'),
active: e.classList.contains('active'),
labelVisible: e.querySelector('.nav-tab-label').offsetParent !== null,
}))
);
expect(labels.find((l) => l.tab === 'chat')).toEqual({
tab: 'chat',
active: true,
labelVisible: true,
});
// Meeting
await page.click('.sidebar-nav-row [data-tab="meetings"]');
await page.waitForTimeout(400);
const meetings = await panelText(page, 'meetings');
expect(meetings.hidden).toBe(false);
expect(meetings.text).toMatch(/MEETING/i);
expect(await page.locator('.sb-quick-btn').count()).toBe(2);
// Inbox
await page.click('.sidebar-nav-row [data-tab="inbox"]');
await page.waitForTimeout(400);
const inbox = await panelText(page, 'inbox');
expect(inbox.hidden).toBe(false);
expect(inbox.text).toMatch(/INBOX/i);
// retour Home : l'arbre du workspace est de nouveau là, et UNIQUEMENT Home
// porte son libellé (régression : clic Home → texte sur tous les boutons)
await page.click('.sidebar-nav-row [data-tab="home"]');
await page.waitForTimeout(300);
expect((await panelText(page, 'home')).hidden).toBe(false);
await expect(page.locator('#sidebar-workspace-items, .sidebar-static-links').first()).toBeVisible();
labels = await page.$$eval('.sidebar-nav-row [data-tab]', (els) =>
els.map((e) => ({
tab: e.getAttribute('data-tab'),
labelVisible: e.querySelector('.nav-tab-label').offsetParent !== null,
}))
);
expect(labels.find((l) => l.tab === 'home').labelVisible).toBe(true);
labels.filter((l) => l.tab !== 'home').forEach((l) => {
expect(l.labelVisible).toBe(false);
});
});
test('le libellé suit l’onglet actif même avec une CSS périmée (cache)', async ({ page }) => {
// ?v={{ asset_version }} = fichier VERSION : une feuille de style peut donc
// être servie depuis le cache du navigateur. On rejoue ce cas en neutralisant
// les règles .nav-tab-label de la feuille courante.
await page.route('**/static/css/app.css*', async (route) => {
const res = await route.fetch();
let body = await res.text();
body = body.replace(/\.nav-tab-btn \.nav-tab-label \{[^}]*\}/g, '');
body = body.replace(/\.nav-tab-btn\.active \.nav-tab-label \{[^}]*\}/g, '');
body = body.replace(/\.nav-tab-btn\.active \.nav-tab-badge \{[^}]*\}/g, '');
await route.fulfill({ response: res, body });
});
await login(page);
await page.goto(`${FD_BASE}/workspaces`, { waitUntil: 'domcontentloaded' });
await page.waitForSelector('.sidebar-nav-row');
const visible = async () =>
page.$$eval('.sidebar-nav-row [data-tab]', (els) =>
els.map((e) => ({
tab: e.getAttribute('data-tab'),
labelVisible: e.querySelector('.nav-tab-label').offsetParent !== null,
}))
);
// CSS sans les règles de libellé → seul l'onglet actif garde son texte
for (const active of ['home', 'chat', 'meetings', 'inbox']) {
await page.click(`.sidebar-nav-row [data-tab="${active}"]`);
await page.waitForTimeout(250);
const state = await visible();
expect(state.filter((s) => s.labelVisible).map((s) => s.tab)).toEqual([active]);
}
});
test('le panneau Meeting appelle /api/v2/meetings/upcoming', async ({ page }) => {
const calls = [];
page.on('request', (r) => {
if (r.url().includes('/api/v2/meetings/upcoming')) calls.push(r.url());
});
await login(page);
await page.goto(`${FD_BASE}/workspaces`, { waitUntil: 'domcontentloaded' });
await page.click('.sidebar-nav-row [data-tab="meetings"]');
await page.waitForResponse((r) => r.url().includes('/api/v2/meetings/upcoming'), { timeout: 10000 });
expect(calls.length).toBeGreaterThan(0);
const status = await page.evaluate(async () => {
const r = await fetch('/api/v2/meetings/upcoming?days=30', { credentials: 'same-origin' });
return r.status;
});
expect(status).toBe(200);
});