const { test, expect } = require('@playwright/test'); /** * v7.66 — deux ajustements éditeur : * - « / » ouvre le menu slash → Escape ferme le menu ET retire le « / » * - survol d'un bloc → bouton à droite (bulle + ) qui commente le bloc * 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' }); } } async function newPage(page, body) { 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=v766 ${Date.now()}`, { headers: { 'X-CSRF-Token': csrf } } ); expect(created.status(), await created.text()).toBeLessThan(400); const { id } = await created.json(); if (body) { const bs = await page.request.post(`${FD_BASE}/board/api/pages/${id}/blocks`, { headers: { 'X-CSRF-Token': csrf }, data: { title: 'v766', blocks: body }, }); expect(bs.status(), await bs.text()).toBeLessThan(400); } await page.goto(`${FD_BASE}/pages/${id}`, { waitUntil: 'domcontentloaded' }); await page.waitForSelector('#_blocksCt [data-bid]', { timeout: 15000 }); return id; } test('Escape du menu slash retire le caractère /', async ({ page }) => { await login(page); await newPage(page, [{ id: 'b1', type: 'paragraph', content: '' }]); const blk = page.locator('#_blocksCt [data-bid]').first(); await blk.click(); await page.keyboard.type('/'); await expect(page.locator('#_slashMenu')).toBeVisible({ timeout: 5000 }); await page.keyboard.press('Escape'); await expect(page.locator('#_slashMenu')).toBeHidden(); // le « / » a été retiré — pas de caractère résiduel await expect(blk).toHaveText(''); await page.screenshot({ path: 'shots/v766-slash-esc.png' }); }); test('bouton commenter à droite du bloc', async ({ page }) => { await login(page); // 2 blocs : l'éditeur focalise le 1er au chargement (`:focus-within` le // montre) → on mesure sur le 2e, ni focalisé ni survolé. await newPage(page, [ { id: 'b1', type: 'paragraph', content: 'Premier bloc' }, { id: 'b2', type: 'paragraph', content: 'Un bloc a commenter' }, ]); await expect(page.locator('#_blocksCt .block-comment-btn')).toHaveCount(2); const wrap = page.locator('#_blocksCt .block-wrapper').nth(1); const btn = page.locator('#_blocksCt .block-comment-btn').nth(1); // masqué tant qu'on ne survole pas / ne focalise pas le bloc await expect.poll(() => btn.evaluate((el) => getComputedStyle(el).opacity)).toBe('0'); await wrap.hover(); await expect.poll(() => btn.evaluate((el) => getComputedStyle(el).opacity)).toBe('1'); // bien à droite du bloc (gouttière 64px) const wb = await wrap.boundingBox(); const bb = await btn.boundingBox(); expect(bb.x).toBeGreaterThan(wb.x + wb.width - 1); await page.screenshot({ path: 'shots/v766-comment-btn.png' }); // clic → tiroir → envoi → ancre = le bloc visé (et pas le 1er) await btn.click(); const drawer = page.locator('.comments-drawer'); await expect(drawer).toBeVisible(); await drawer.locator('textarea').fill('Commentaire via le bouton de droite'); await drawer.locator('.ct-send').click(); await expect(page.locator('.comment-thread')).toHaveCount(1); await expect(page.locator('#_blocksCt .fd-anchor[data-cid]')).toHaveText('Un bloc a commenter'); });