Files
flowdeck/e2e/sidebar_tabs.spec.js
T
bruno 1d1cdbd618
FlowDeck CI / test (push) Failing after 3h13m58s
FlowDeck CI / lint (push) Successful in 2m12s
FlowDeck CI / docker (push) Skipped
fix: side peek des bases repasse en vanilla JS + largeur 1100px standard (v7.49.0)
- 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).
2026-10-05 22:47:36 -04:00

182 lines
7.4 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
});