// v7.69.0 — la sélection multi-blocs SURVIT aux gestes qui l'effaçaient : // 1) glisser la souris d'un bloc à l'autre // 2) Ctrl+C sur cette sélection // 3) clic droit → le menu n'efface plus le surlignage const { test, expect } = require('@playwright/test'); 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) { await page.request.post(`${FD_BASE}/auth/register`, { data: { email: USER, password: PASS, name: 'E2E' } }); 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=sel ${Date.now()}`, { headers: { 'X-CSRF-Token': csrf }, }); const { id } = await created.json(); if (body) { await page.request.post(`${FD_BASE}/board/api/pages/${id}/blocks`, { headers: { 'X-CSRF-Token': csrf }, data: { title: 'sel', blocks: body }, }); } await page.goto(`${FD_BASE}/pages/${id}`, { waitUntil: 'domcontentloaded' }); await page.waitForSelector('#_blocksCt [data-bid]', { timeout: 15000 }); } const BLOCKS = [ { id: 'b1', type: 'paragraph', content: 'Premier bloc' }, { id: 'b2', type: 'heading_2', content: 'Titre deux' }, { id: 'b3', type: 'paragraph', content: 'Troisieme bloc' }, ]; // état natif de la sélection const nativeSel = (page) => page.evaluate(() => { const s = window.getSelection(); return { rc: s.rangeCount, coll: s.isCollapsed, txt: s.toString().slice(0, 40) }; }); const clipState = (page) => page.evaluate(() => window.E._clipState()); // VRAI glisser de souris : le navigateur seul ne franchit jamais deux // contenteditable — c'est _buildSelAt qui reconstruit la plage. async function dragAcross(page, last = 2) { const b1 = await page.locator('#_blocksCt [data-bid]').first().boundingBox(); const bEnd = await page.locator('#_blocksCt [data-bid]').nth(last).boundingBox(); // on part du tout début du 1er bloc et on va jusqu'à la fin du dernier await page.mouse.move(b1.x + 2, b1.y + b1.height / 2); await page.mouse.down(); for (let i = 1; i <= 8; i++) { await page.mouse.move(b1.x + 2, b1.y + b1.height / 2 + ((bEnd.y - b1.y) * i) / 8); } await page.mouse.move(bEnd.x + bEnd.width - 2, bEnd.y + bEnd.height / 2); await page.mouse.up(); await page.waitForTimeout(80); } test('glisser la souris sur 3 blocs construit une sélection multi-blocs qui survit', async ({ page }) => { await login(page); await newPage(page, BLOCKS); await dragAcross(page); // la plage reconstruite ne doit PAS être effacée par le « click » suivant const s = await nativeSel(page); expect(s.coll).toBe(false); expect(s.rc).toBe(1); const st = await clipState(page); expect(st.si).toBe(0); expect(st.ei).toBe(2); await page.screenshot({ path: 'shots/v769-drag.png' }); }); // VRAI raccourci clavier (pas execCommand) : c'est ce qui était cassé. // Maj+↓ depuis la fin du bloc 1 → la sélection traverse les 3 blocs. async function keyboardExtend(page) { const b1 = await page.locator('#_blocksCt [data-bid]').first().boundingBox(); await page.mouse.click(b1.x + 2, b1.y + b1.height / 2); await page.keyboard.press('End'); await page.keyboard.press('Shift+ArrowDown'); await page.keyboard.press('Shift+ArrowDown'); await page.waitForTimeout(80); } test('Ctrl+C copie la sélection multi-blocs en markdown', async ({ page }) => { await login(page); await newPage(page, BLOCKS); await keyboardExtend(page); expect((await clipState(page)).ei).toBe(2); await page.keyboard.press('Control+c'); // le vrai raccourci await page.waitForTimeout(250); const clip = await page.evaluate(() => navigator.clipboard.readText()); expect(clip).toContain('## Titre deux'); // le titre sort en markdown expect(clip).toContain('Troisieme bloc'); // Ctrl+X retire les blocs traversés (tête + queue restent) await page.keyboard.press('Control+x'); await page.waitForTimeout(300); await expect(page.locator('#_blocksCt [data-bid]')).toHaveCount(2); }); test("clic droit : le menu s'ouvre SANS effacer le surlignage", async ({ page }) => { await login(page); await newPage(page, BLOCKS); await dragAcross(page); expect((await nativeSel(page)).coll).toBe(false); await page.locator('#_blocksCt [data-bid]').nth(1).click({ button: 'right', position: { x: 30, y: 6 } }); await page.waitForTimeout(150); // le surlignage reste visible const s = await nativeSel(page); expect(s.coll).toBe(false); expect(s.txt.length).toBeGreaterThan(0); // …et le groupe Clipboard est bien proposé const menu = page.locator('#blockMenu'); await expect(menu).toBeVisible(); await expect(menu).toContainText('Clipboard'); await expect(menu.locator('[data-bm-act="clip-copy"]')).toBeVisible(); await page.screenshot({ path: 'shots/v769-rightclick.png' }); }); test('clic droit → Copy via le menu colle bien le markdown figé', async ({ page }) => { await login(page); await newPage(page, BLOCKS); await dragAcross(page); await page.locator('#_blocksCt [data-bid]').nth(1).click({ button: 'right', position: { x: 30, y: 6 } }); const menu = page.locator('#blockMenu'); await expect(menu.locator('[data-bm-act="clip-copy"]')).toBeVisible(); await menu.locator('[data-bm-act="clip-copy"]').click(); await page.waitForTimeout(200); const clip = await page.evaluate(() => navigator.clipboard.readText()); expect(clip).toContain('Premier bloc'); expect(clip).toContain('## Titre deux'); }); test('un clic simple dans un bloc place quand même le curseur (pas de sélection fantôme)', async ({ page }) => { await login(page); await newPage(page, BLOCKS); await dragAcross(page); // le clic suivant est un NOUVEL geste : il doit effacer la sélection const b1 = await page.locator('#_blocksCt [data-bid]').first().boundingBox(); await page.mouse.click(b1.x + 15, b1.y + b1.height / 2); await page.waitForTimeout(120); const st = await clipState(page); expect(st).toBeNull(); });