const { test, expect } = require('@playwright/test'); /** * v7.63 — menus mobiles façon Notion (viewport 390×844) : * - « / » → barre horizontale défilante sous le curseur (15 boutons, ordre du doc) * - « + » → feuille plein écran « Insert block » (titre centré + Cancel, groupes, * raccourcis) → choix d'un type = applySlash * - « Turn into » / « Color » → feuilles réutilisant les sous-menus de bloc * - « More » → menu contextuel de bloc (desktop) * 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', viewport: { width: 390, height: 844 }, hasTouch: true, isMobile: true }); 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) { 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=Mobile menus ${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 }); return id; } test('barre mobile au / + feuilles Insert block / Turn into / More', async ({ page }) => { await login(page); await newPage(page); await page.click('#_blocksCt [data-bid]'); await page.keyboard.type('/'); // ── 1. la barre apparaît, 15 boutons dans l'ordre de la description ── const bar = page.locator('#fdMobileBar'); await expect(bar).toBeVisible({ timeout: 5000 }); const keys = await page.evaluate(() => Array.from(document.querySelectorAll('#fdMobileBar [data-mtb]')).map((b) => b.dataset.mtb)); expect(keys).toEqual(['plus', 'turn', 'at', 'comment', 'image', 'trash', 'left', 'right', 'up', 'down', 'undo', 'redo', 'color', 'more', 'kbd']); // défilement latéral const overflowX = await bar.evaluate((el) => getComputedStyle(el).overflowX); expect(overflowX).toBe('auto'); // pas de popup desktop en parallèle await expect(page.locator('#_slashMenu')).toBeHidden(); await page.screenshot({ path: 'shots/v763-bar.png' }); // ── 2. « + » → feuille plein écran « Insert block » ── await bar.locator('[data-mtb="plus"]').click(); const sheet = page.locator('.fd-msheet'); await expect(sheet).toBeVisible(); await expect(sheet.locator('.fd-msheet-title')).toHaveText('Insert block'); await expect(sheet.locator('.fd-msheet-cancel')).toHaveText('Cancel'); await expect(sheet.locator('.sm-group').first()).toHaveText('Basic blocks'); await expect(sheet.locator('.slash-item', { hasText: 'Heading 1' })).toHaveCount(1); await page.screenshot({ path: 'shots/v763-insert.png' }); // ── 3. choix d'un type → applySlash (le bloc devient un titre) ── await sheet.locator('.slash-item', { hasText: 'Heading 1' }).click(); await expect(sheet).toHaveCount(0); await expect(bar).toBeHidden(); expect(await page.evaluate(() => window.E.blocks[0].type)).toBe('heading_1'); // ── 4. « Turn into » → feuille réutilisant les items du sous-menu de bloc ── await page.keyboard.type('/'); await expect(bar).toBeVisible(); await bar.locator('[data-mtb="turn"]').click(); await expect(sheet.locator('.fd-msheet-title')).toHaveText('Turn into'); await expect(sheet.locator('.bm-item', { hasText: 'Bulleted list' })).toHaveCount(1); await expect(sheet.locator('.bm-check')).toHaveCount(1); // type courant coché await page.screenshot({ path: 'shots/v763-turn.png' }); await sheet.locator('.fd-msheet-cancel').click(); await expect(sheet).toHaveCount(0); expect(await page.evaluate(() => window.E.blocks[0].type)).toBe('heading_1'); // ── 5. « Color » → feuille avec les 3 groupes ── await page.evaluate(() => localStorage.setItem('fdLastBlockColor', 'color|#E5484D')); await bar.locator('[data-mtb="color"]').click(); for (const g of ['Last used', 'Text color', 'Background color']) { await expect(sheet.locator('.bm-group', { hasText: g })).toHaveCount(1); } await sheet.locator('.fd-msheet-cancel').click(); // ── 6. « More » → menu contextuel de bloc (recherche + pied) ── await bar.locator('[data-mtb="more"]').click(); const menu = page.locator('#blockMenu'); await expect(menu).toBeVisible(); await expect(menu.locator('.bm-search')).toHaveAttribute('placeholder', 'Search actions...'); await expect(menu.locator('.bm-foot')).toContainText('Last edited by'); await page.keyboard.press('Escape'); await expect(menu).toBeHidden(); // ── 7. ↑ déplace réellement le bloc ── (bloc 0 = titre, bloc 1 = paragraphe) await page.click('#_blocksCt [data-bid]'); // bloc 0 (contient '/') await page.keyboard.press('Enter'); await page.waitForTimeout(150); // le focus du nouveau bloc est différé (60 ms) await page.keyboard.type('/'); // bloc neuf et vide → slash exact = ouverture await expect(bar).toBeVisible(); // SM._i = 1 (bloc du bas) await bar.locator('[data-mtb="up"]').click(); expect(await page.evaluate(() => window.E.blocks.map((b) => b.type).join(','))) .toBe('paragraph,heading_1'); await page.screenshot({ path: 'shots/v763-moved.png' }); });