Tiroir Comments - Section « Reactions » au-dessus des fils : émoji, compte (si >1) et texte ancré lu dans le DOM (reactionList() / _anchorText(), offsets source hors pastilles) ; l'état vide ne s'affiche plus s'il reste des réactions. - Suppression : ✕ sur la ligne du tiroir (mes réactions) ET croix ::after au survol de la pastille sur l'émoji que j'ai posé (aucun nœud ajouté → offsets intacts). Deux entrées → removeReaction() → POST toggle → loadReactions(). Menu de sélection (maquette ASCII) - Ligne 4 = rangée scannée : 💬 Comment (libellé) à gauche, 🙂+ réaction à droite (.ft-row-split) ; la réaction sort de la ligne 3. - Entête Skills avec ≡ à droite (::after), liste Skills scrollable. - ✨ Ask AI retiré de la toolbar : doublon exact de « Edit with AI » du pied. - Écart volontaire à la maquette : le barré (T) de la ligne 2 est conservé. Fix racine (flake de gate) - _is_public_host attrape OSError (le garde SSRF renvoie un booléen, il ne lève plus) : un socket.timeout DNS ne bascule plus fetch_og_metadata dans son except Exception → repli silencieux → AssertionError. - fixture stub_dns sur TestOGParser (même motif que test_agent_web_tools) : zéro vrai DNS dans les tests OG. Tests: pytest tests/ -q -n auto → 1390 passed / 0 failed (flake corrigé) ; ruff check app tests → OK ; e2e v764 (réactions dans le tiroir + suppression A/B), v762 (nouvelle ligne 4), v766, v7641, v763 → 6 passed sur :8081 (image flowdeck:v7.67.0 rebuild + redéployée).
134 lines
6.1 KiB
JavaScript
134 lines
6.1 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');
|
|
// refonte v7.67 : ligne 4 = Comment (libellé) à gauche, réaction à droite ;
|
|
// entête Skills avec ≡ ; plus de bouton ✨ Ask AI (doublon du pied)
|
|
await expect(bar.locator('.ft-comment')).toContainText('Comment');
|
|
await expect(bar.locator('.ft-row-split .ft-react')).toHaveCount(1);
|
|
await expect(bar.locator('.ft-skills-head')).toHaveText('Skills');
|
|
await expect(bar.locator('[data-tooltip^="Ask AI"]')).toHaveCount(0);
|
|
// 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();
|
|
});
|