const { test, expect } = require('@playwright/test'); /** * v7.68 — colonnes rendues (slash + Turn into), tables IA sans pipe de tête, * copier/couper/coller d'une sélection multi-blocs (Ctrl+C/X/V + clic droit). * 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', permissions: ['clipboard-read', 'clipboard-write'] }); 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=v768 ${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: 'v768', 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; } // sélection multi-blocs (du milieu du 1er au milieu du 3e) + fin de mouseup async function selectAcross(page) { await page.evaluate(() => { const el1 = document.querySelector('#_blocksCt [data-bid]'); const blocks = document.querySelectorAll('#_blocksCt [data-bid]'); const el3 = blocks[blocks.length - 1]; const r = document.createRange(); r.setStart(el1.firstChild, 0); r.setEnd(el3.firstChild, el3.firstChild.nodeValue.length); const s = getSelection(); s.removeAllRanges(); s.addRange(r); }); } test('colonnes : slash + Turn into 2 colonnes', async ({ page }) => { await login(page); await newPage(page, [{ id: 'b1', type: 'paragraph', content: '' }]); await page.click('#_blocksCt [data-bid]'); await page.keyboard.type('/'); await expect(page.locator('#_slashMenu')).toBeVisible({ timeout: 5000 }); await page.keyboard.type('columns'); await page.locator('#_slashMenu .slash-item[data-sid="columns"]').click(); // le bloc colonnes est RENDU (il tombait dans le rendu générique = vide) await expect(page.locator('#_blocksCt .block-columns')).toHaveCount(1); await expect(page.locator('#_blocksCt .block-column')).toHaveCount(1); await page.locator('#_blocksCt .columns-add-btn').click(); await expect(page.locator('#_blocksCt .block-column')).toHaveCount(2); await page.screenshot({ path: 'shots/v768-columns-slash.png' }); // Turn into → 2 colonnes await newPage(page, [{ id: 'b1', type: 'paragraph', content: 'Pour le menu' }]); await page.locator('#_blocksCt [data-bid]').first().click({ button: 'right' }); const menu = page.locator('#blockMenu'); await expect(menu).toBeVisible(); await menu.locator('[data-sub="turn"]').hover(); const sub = page.locator('#blockMenuSub'); await expect(sub).toBeVisible(); await sub.locator('[data-bm-act="turn-type"][data-cols="2"]').click(); await expect(page.locator('#_blocksCt .block-column')).toHaveCount(2); }); test('table IA sans pipe de tête devient un vrai tableau', async ({ page }) => { await login(page); await newPage(page, [{ id: 'b1', type: 'paragraph', content: '' }]); await page.evaluate(() => { window.E.applyAIBlocks('Nom | Rôle\n--- | ---\nAlice | Dev\nBob | Test'); }); await expect(page.locator('#_blocksCt .ftable-editor')).toHaveCount(1); await expect(page.locator('#_blocksCt .ftable-cell').first()).toHaveText('Nom'); await expect(page.locator('#_blocksCt .ftable-cell').last()).toHaveText('Test'); // la ligne « texte » suivie d'« --- » ne doit PAS devenir un tableau await page.evaluate(() => { window.E.applyAIBlocks('juste du texte\n---\nune phrase'); }); await expect(page.locator('#_blocksCt .ftable-editor')).toHaveCount(1); await page.screenshot({ path: 'shots/v768-ai-table.png' }); }); test('Ctrl+C / Ctrl+X / Ctrl+V sur une sélection multi-blocs', async ({ page }) => { await login(page); await newPage(page, [ { id: 'b1', type: 'paragraph', content: 'Premier bloc' }, { id: 'b2', type: 'heading_2', content: 'Titre deux' }, { id: 'b3', type: 'paragraph', content: 'Troisieme bloc' }, ]); // le raccourci clavier nécessite un éditeur focalisé (sinon Chromium // n'émet pas l'événement copy) await page.locator('#_blocksCt [data-bid]').first().click(); await selectAcross(page); // headless Chromium n'émet PAS le raccourci clavier (pas de presse-papier // OS) : execCommand('copy') déclenche le MÊME événement copy → notre handler await page.evaluate(() => document.execCommand('copy')); const clip = await page.evaluate(() => navigator.clipboard.readText()); expect(clip).toContain('Premier bloc'); expect(clip).toContain('## Titre deux'); // le titre sort en markdown // coupe : les 3 blocs deviennent 2 blocs vides (tête + queue) await selectAcross(page); await page.evaluate(() => document.execCommand('cut')); await expect(page.locator('#_blocksCt [data-bid]')).toHaveCount(2); await expect(page.locator('#_blocksCt [data-bid]').first()).toHaveText(''); // colle : les 3 blocs reviennent APRÈS le bloc du curseur (comme l'entrée // Paste du menu) → 2 blocs restants + 3 collés = 5 await page.evaluate(() => window.E.pasteBlocks()); await expect(page.locator('#_blocksCt [data-bid]')).toHaveCount(5, { timeout: 5000 }); await expect(page.locator('#_blocksCt [data-bid]').nth(1)).toContainText('Premier bloc'); await expect(page.locator('#_blocksCt [data-bid]').nth(2)).toContainText('Titre deux'); }); test('clic droit avec une sélection → groupe Clipboard', async ({ page }) => { await login(page); await newPage(page, [ { id: 'b1', type: 'paragraph', content: 'Alpha bloc' }, { id: 'b2', type: 'paragraph', content: 'Beta bloc' }, ]); await selectAcross(page); await page.locator('#_blocksCt [data-bid]').first().click({ button: 'right', position: { x: 4, y: 4 } }); const menu = page.locator('#blockMenu'); await expect(menu).toBeVisible(); await expect(menu.locator('[data-bm-act="clip-copy"]')).toBeVisible(); await expect(menu.locator('[data-bm-act="clip-cut"]')).toBeVisible(); await expect(menu.locator('[data-bm-act="clip-paste"]')).toBeVisible(); await page.screenshot({ path: 'shots/v768-ctx-clipboard.png' }); await menu.locator('[data-bm-act="clip-copy"]').click(); const clip = await page.evaluate(() => navigator.clipboard.readText()); expect(clip).toContain('Alpha bloc'); expect(clip).toContain('Beta bloc'); });