/** * Sonde des gestes de redimensionnement — instance Docker locale. * 1. presser dans le CORPS près d'une limite de colonne / ligne * → standards : ça doit SÉLECTIONNER une cellule, pas redimensionner ; * 2. double-clic sur un bord d'en-tête * → standards (Excel / Sheets) : ajustement automatique au contenu ; * 3. glisser un bord d'en-tête → doit toujours redimensionner (inchangé). */ import { chromium } from '@playwright/test'; const BASE = process.env.BASE_URL || 'http://localhost:2020'; const results = []; const check = (name, ok, detail = '') => { results.push({ name, ok, detail }); console.log(`${ok ? 'PASS' : 'FAIL'} — ${name}${detail ? ` (${detail})` : ''}`); }; const browser = await chromium.launch(); const context = await browser.newContext({ viewport: { width: 1600, height: 1000 } }); const page = await context.newPage(); const styleCalls = []; page.on('request', (r) => { if (r.url().includes('/xlsx/style') && r.method() === 'PUT') { styleCalls.push(r.postDataJSON ? r.postDataJSON() : null); } }); try { await page.goto(BASE, { waitUntil: 'domcontentloaded' }); const login = page.locator('#login-screen'); await login.waitFor({ state: 'visible', timeout: 20000 }).catch(() => {}); if (await login.isVisible().catch(() => false)) { await page.fill('#login-username', 'admin'); await page.fill('#login-password', 'test123'); await page.click('#login-btn'); } await page.waitForFunction(() => window.__OBSIGATE_BOOTED === true, { timeout: 30000 }); const treeItem = page.locator('.tree-item[data-vault="TestVault"][data-path="budget.xlsx"]'); if (!(await treeItem.count())) { await page.locator('.tree-item.vault-item[data-vault="TestVault"]').first().click(); await treeItem.waitFor({ state: 'attached', timeout: 10000 }); } await treeItem.dblclick({ timeout: 10000 }); await page.locator('#content-area .xlsx-panel:visible .xlsx-table').waitFor({ state: 'visible', timeout: 20000 }); await page.waitForTimeout(600); const ghostVisible = () => page.evaluate(() => { const g = document.querySelector('.xlsx-resize-ghost'); return !!g && !g.hidden; }); const selectedCount = () => page.evaluate(() => { const p = [...document.querySelectorAll('.xlsx-panel')].find((x) => getComputedStyle(x).display !== 'none'); return p ? p.querySelectorAll('td.xlsx-selected').length : 0; }); const colWidthPx = (letter) => page.evaluate((L) => { const p = [...document.querySelectorAll('.xlsx-panel')].find((x) => getComputedStyle(x).display !== 'none'); const th = [...p.querySelectorAll('thead th')].find((n) => n.textContent.trim() === L); return th ? Math.round(th.getBoundingClientRect().width) : -1; }, letter); // ── 1. presser dans le CORPS, à 3 px de la limite A|B ── const abEdge = await page.evaluate(() => { const p = [...document.querySelectorAll('.xlsx-panel')].find((x) => getComputedStyle(x).display !== 'none'); const ths = [...p.querySelectorAll('thead th')]; const a = ths.find((t) => t.textContent.trim() === 'A'); const r = a.getBoundingClientRect(); return { x: r.right - 3, y: r.bottom + 60 }; // 60 px sous l'en-tête : dans le corps }); await page.mouse.move(abEdge.x, abEdge.y); await page.mouse.down(); await page.waitForTimeout(120); const bodyGhost = await ghostVisible(); await page.mouse.up(); await page.waitForTimeout(200); const bodySel = await selectedCount(); check( 'pression dans le corps près d\'une limite : SÉLECTION (pas de redimensionnement)', !bodyGhost, `ghost pendant la pression=${bodyGhost}, cellules sélectionnées=${bodySel}`, ); // ── 2. glisser le bord d'en-tête A|B : doit redimensionner ── styleCalls.length = 0; const headerEdge = await page.evaluate(() => { const p = [...document.querySelectorAll('.xlsx-panel')].find((x) => getComputedStyle(x).display !== 'none'); const ths = [...p.querySelectorAll('thead th')]; const a = ths.find((t) => t.textContent.trim() === 'A'); const r = a.getBoundingClientRect(); return { x: r.right, y: r.top + r.height / 2, w: Math.round(r.width) }; }); await page.mouse.move(headerEdge.x, headerEdge.y); await page.mouse.down(); await page.mouse.move(headerEdge.x + 40, headerEdge.y, { steps: 5 }); const ghostDuringDrag = await ghostVisible(); await page.mouse.up(); await page.waitForTimeout(500); const wAfterDrag = await colWidthPx('A'); check( 'glisser un bord d\'en-tête : ghost + écriture de la largeur', ghostDuringDrag && wAfterDrag > headerEdge.w, `ghost=${ghostDuringDrag}, A: ${headerEdge.w}px → ${wAfterDrag}px, PUT=${styleCalls.length}`, ); // ── 3. double-clic sur le bord d'en-tête A|B → ajustement automatique ── styleCalls.length = 0; const beforeDbl = await colWidthPx('A'); await page.mouse.dblclick(headerEdge.x, headerEdge.y, { delay: 80 }); await page.waitForTimeout(700); const afterDbl = await colWidthPx('A'); const fitOp = styleCalls.some((c) => c && Array.isArray(c.ops) && c.ops.some((o) => o.op === 'col_width')); check( 'double-clic sur un bord d\'en-tête : ajustement automatique (PUT col_width)', fitOp, `A: ${beforeDbl}px → ${afterDbl}px, PUT=${JSON.stringify(styleCalls)}`, ); // ── 4. double-clic sur le bord d'une ligne (gouttière des numéros) ── styleCalls.length = 0; const rowEdge = await page.evaluate(() => { const p = [...document.querySelectorAll('.xlsx-panel')].find((x) => getComputedStyle(x).display !== 'none'); const th = p.querySelector('th.xlsx-rownum'); const r = th.getBoundingClientRect(); return { x: r.left + 10, y: r.bottom }; }); await page.mouse.dblclick(rowEdge.x, rowEdge.y, { delay: 80 }); await page.waitForTimeout(700); const rowFitOp = styleCalls.some((c) => c && Array.isArray(c.ops) && c.ops.some((o) => o.op === 'row_height')); check( 'double-clic sur un bord de ligne : ajustement automatique (PUT row_height)', rowFitOp, `PUT=${JSON.stringify(styleCalls)}`, ); } catch (err) { check('exécution sans erreur', false, String(err && err.message ? err.message : err)); } finally { await browser.close(); } const failed = results.filter((r) => !r.ok); console.log(`\n${results.length - failed.length}/${results.length} vérifications OK`); process.exit(failed.length ? 1 : 0);