- Toolbar de sélection sous le surlignage : ligne de style (T + type de bloc dynamique → sous-menu Turn into), A/B/I/U/T barré/Tx, lien/surlignage/ code/équation/…, Comment + Ask AI, section grise Skills (4 actions) et pied « Edit with AI » (Alt+⇧+E) ; positionnée sous la sélection (au-dessus si manque de place), fermée au clic hors sélection - « … » → menu contextuel de bloc existant (recherche « Search actions… », groupe Text, 10 actions + raccourcis, pied auteur/heure/taille) - Info-bulle claire à droite des éléments du menu : data-tooltip-html + classe .tooltip-light dans le tooltip global (app.js) - Persistance du formatage inline : sérialiseur gtMd() (DOM formaté → source markdown) branché sur sync() ; mdEsc() rend __…__ (u), ==…== (mark) et $$…$$ (équation inline KaTeX, data-tex conservé) - Fixes racine : askAI(idx) écrasé par askAI() → askAIBlock(idx) (menu de bloc, Ctrl+J, Alt+⇧+E) ; ed.__x (Alpine v2) inexistant sous v3 → _xed() via _x_dataStack[0] (showFmt/hideCommentBtn/mentions ne s'exécutaient jamais) ; ancre de sélection nœud élément gérée ; checkMd() appelé avec un id au lieu de l'index (exception à chaque barre d'espace) - Tests : tests/test_v762_selection_toolbar.py (roundtrip gtMd/mdEsc sous node) + e2e/v762_selection_toolbar.spec.js (structure, sous-menus, géométrie, persistance bold/highlight après reload) ; suite complète 1368 passed / 0 failed, ruff OK, OpenAPI régénérée 7.62.0 (525 chemins)
128 lines
5.6 KiB
JavaScript
128 lines
5.6 KiB
JavaScript
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 || '[email protected]';
|
|
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');
|
|
// 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();
|
|
});
|