const { test, expect } = require('@playwright/test'); /** * v7.62 — menu contextuel au highlight de texte (toolbar façon Notion). * Vérifie : les 4 lignes + Skills + pied, le menu « … », le sous-menu * « Turn into », et surtout la PERSISTANCE du formatage après reload * (sync() relit le DOM formaté via gtMd → source markdown). * Instance de test attendue sur FD_BASE_URL (défaut 8081). */ const FD_BASE = process.env.FD_BASE_URL || 'http://localhost:8081'; const USER = process.env.FD_USER || 'e2e@flowdeck.local'; 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'); const ok = await page .waitForURL('**/workspaces', { timeout: 8000 }) .then(() => true) .catch(() => false); if (!ok) { const resp = await page.request.post(`${FD_BASE}/auth/register`, { data: { email: USER, password: PASS, name: 'E2E' }, }); if (resp.status() !== 409) expect(resp.ok()).toBeTruthy(); await page.goto(`${FD_BASE}/workspaces`, { waitUntil: 'domcontentloaded' }); } } // Sélectionne tout le bloc et simule la fin du glisser-souris async function selectBlock(page) { await page.evaluate(() => { const el = document.querySelector('#_blocksCt [data-bid]'); const r = document.createRange(); r.selectNodeContents(el); const s = getSelection(); s.removeAllRanges(); s.addRange(r); el.dispatchEvent(new MouseEvent('mouseup', { bubbles: true })); }); await expect(page.locator('.format-toolbar')).toBeVisible({ timeout: 5000 }); } test('toolbar de sélection : structure, sous-menus, persistance', async ({ page }) => { await login(page); const cookies = await page.context().cookies(); const csrf = (cookies.find((c) => c.name === 'csrf_token') || {}).value || ''; const created = await page.request.post( `${FD_BASE}/board/api/pages?title=Menu highlight ${Date.now()}`, { headers: { 'X-CSRF-Token': csrf } } ); expect(created.status(), await created.text()).toBeLessThan(400); const { id } = await created.json(); await page.goto(`${FD_BASE}/pages/${id}`, { waitUntil: 'domcontentloaded' }); await page.waitForSelector('#_blocksCt [data-bid]', { timeout: 15000 }); await page.click('#_blocksCt [data-bid]'); await page.keyboard.type('Menu lors d un highlight de texte'); // ── 1. structure : 4 lignes + Skills + pied ── await selectBlock(page); const bar = page.locator('.format-toolbar'); await expect(bar.locator('.ft-row')).toHaveCount(4); await expect(bar.locator('.ft-style')).toContainText('Normal Text'); await expect(bar.locator('.ft-group')).toHaveText('Skills'); await expect(bar.locator('.ft-skill')).toHaveCount(4); await expect(bar.locator('.ft-skill').first()).toHaveText('Improve writing'); await expect(bar.locator('.ft-foot')).toContainText('Edit with AI'); // refonte v7.67 : ligne 4 = Comment (libellé) à gauche, réaction à droite ; // entête Skills avec ≡ ; plus de bouton ✨ Ask AI (doublon du pied) await expect(bar.locator('.ft-comment')).toContainText('Comment'); await expect(bar.locator('.ft-row-split .ft-react')).toHaveCount(1); await expect(bar.locator('.ft-skills-head')).toHaveText('Skills'); await expect(bar.locator('[data-tooltip^="Ask AI"]')).toHaveCount(0); // géométrie : sous la sélection, entièrement dans le viewport const barBox = await bar.boundingBox(); const selBox = await page.evaluate(() => { const r = getSelection().getRangeAt(0).getBoundingClientRect(); return { bottom: r.bottom }; }); expect(barBox.y).toBeGreaterThanOrEqual(selBox.bottom); expect(barBox.y + barBox.height).toBeLessThanOrEqual(900); expect(barBox.x).toBeGreaterThanOrEqual(0); expect(barBox.x + barBox.width).toBeLessThanOrEqual(1440); await page.screenshot({ path: 'shots/v762-toolbar.png' }); // ── 2. ligne 1 → sous-menu « Turn into » ── await bar.locator('.ft-style').click(); const sub = page.locator('#blockMenuSub'); await expect(sub).toBeVisible(); await expect(sub).toContainText('Heading 4'); await expect(sub.locator('.bm-check')).toHaveCount(1); // type courant coché await page.screenshot({ path: 'shots/v762-turn-submenu.png' }); await page.keyboard.press('Escape'); // ── 3. « … » → menu contextuel de bloc (recherche + pied) ── await selectBlock(page); await bar.locator('.ft-more').click(); const menu = page.locator('#blockMenu'); await expect(menu).toBeVisible(); await expect(menu.locator('.bm-search')).toHaveAttribute('placeholder', 'Search actions...'); await expect(menu).toContainText('Copy link to block'); await expect(menu.locator('.bm-foot')).toContainText('Last edited by'); await page.screenshot({ path: 'shots/v762-more-menu.png' }); await page.keyboard.press('Escape'); await expect(menu).toBeHidden(); // ── 4. persistance : B (gras) survit au reload ── await selectBlock(page); await page.getByRole('button', { name: 'B', exact: true }).click(); await expect(page.locator('#_blocksCt [data-bid] b, #_blocksCt [data-bid] strong')).toHaveCount(1); await page.waitForTimeout(2500); // autosave 1,5 s await page.reload({ waitUntil: 'domcontentloaded' }); await page.waitForSelector('#_blocksCt [data-bid]', { timeout: 15000 }); await expect(page.locator('#_blocksCt [data-bid] strong')).toHaveCount(1); // rendu mdEsc // ── 5. persistance : S (surlignage) survit au reload ── await selectBlock(page); await bar.locator('.ft-hl').click(); await expect(page.locator('#_blocksCt [data-bid] mark')).toHaveCount(1); await page.waitForTimeout(2500); await page.reload({ waitUntil: 'domcontentloaded' }); await page.waitForSelector('#_blocksCt [data-bid]', { timeout: 15000 }); await expect(page.locator('#_blocksCt [data-bid] mark')).toHaveCount(1); // ── 6. fermeture hors sélection ── await page.mouse.click(10, 400); await expect(page.locator('.format-toolbar')).toBeHidden(); });