- Barre flottante défilante sous le curseur (mobile ≤ 768 px) au lieu de la popup desktop, 15 boutons dans l'ordre de la description : +▾, Turn into▾, @, 💬, 🖼, 🗑, ←, →, ↑, ↓, ↩, ↪, Color▾, More▾, ⌨ - +▾ → feuille plein écran « Insert block » (titre centré, Cancel bleu, groupes Basic blocks / AI writing / Media / Data / Synced / Actions, raccourcis markdown à droite) ; items partagés avec la popup desktop (SM._slashItemHtml) - Turn into ▾ / Color ▾ → feuilles réutilisant les générateurs extraits _blockTurnItems / _blockColorItems (extraits de _blockTurnMenuOpen / _blockColorMenuOpen, comportement desktop inchangé) - More ▾ → menu contextuel de bloc existant ; @ insère + ouvre les mentions ; 💬 commentaire ; 🖼 bloc image ; 🗑 suppression ; ⌨ masque le clavier ; ↩ ↪ annuler/rétablir - moveBlock(bid, dir) : ↑↓ réordonne (racine + enfants toggle/columns), → indent dans le frère précédent, ← outdent, garde-fous avec toast - Fermeture auto de la barre quand le « / » n'est plus en tête de bloc ; taps dans la barre/les feuilles exclus du handler « clic hors-zone » ; Cancel ne referme que la feuille (la barre reste tant que le / est tapé) - Raccourcis slash alignés sur les préfixes markdown (-, [], >, ---), groupe SYNCED libellé « Synced » - Tests : tests/test_v763_mobile_menus.py (ordre, câblage, structure, styles + moveBlock exécuté sous node), e2e/v763_mobile_menus.spec.js (390×844, 1 passed) ; test_v761 adapté aux extractions ; suite complète 1375 passed / 0 failed, ruff OK, OpenAPI 7.63.0 (525 chemins)
126 lines
6.1 KiB
JavaScript
126 lines
6.1 KiB
JavaScript
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 || '[email protected]';
|
||
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' });
|
||
});
|