/** * E2E tests for the mobile bottom toolbar clearance (BUG-073). * * The fixed bottom bar (#mobile-toolbar, 64px + safe-area) used to cover the * last pixels of every document/page: the clearance rule targeted the dead * `.main-layout` selector instead of `.main-body`, so #content-area reached * behind the bar and the end of the content was unreachable. * * Runs only under the `chromium-mobile` Playwright project (viewport ≤ 768px); * skipped on the desktop project that the CI job executes — same convention * as mobile-editor.spec.js / config-mobile.spec.js. * * Run: * npx playwright test tests/e2e/mobile-toolbar.spec.js --project=chromium-mobile * pwsh scripts/run-e2e-local.ps1 --project=chromium-mobile -g "BUG-073" * (ObsiGate listening on http://localhost:2029, auth disabled) */ import { test, expect } from '@playwright/test'; const MOBILE_MAX_WIDTH = 768; const LONG_DOC = 'ANALYSE_REVIEW.md'; async function boot(page) { await page.goto('/'); await page .waitForSelector('#app:not(.hidden)', { timeout: 15000 }) .catch(() => {}); } /** Wait for the async markdown render, then confirm the area actually scrolls. */ async function openLongDocument(page) { await page.evaluate((p) => window.TabManager.open('TestVault', p), LONG_DOC); await expect(page.locator('#content-area .md-content')).toBeAttached({ timeout: 15000 }); await expect .poll( () => page.evaluate(() => { const area = document.getElementById('content-area'); return area.scrollHeight > area.clientHeight; }), { timeout: 15000 }, ) .toBe(true); } /** Geometry of #content-area vs the fixed #mobile-toolbar bar. */ async function measure(page) { return page.evaluate(() => { const area = document.getElementById('content-area').getBoundingClientRect(); const bar = document.getElementById('mobile-toolbar').getBoundingClientRect(); return { areaBottom: area.bottom, barTop: bar.top, barVisible: bar.height > 0 }; }); } test.describe('Mobile bottom toolbar clearance (BUG-073)', () => { test('content area ends above the fixed bottom toolbar', async ({ page, viewport }) => { test.skip((viewport?.width ?? 0) > MOBILE_MAX_WIDTH, 'Mobile viewport required'); await boot(page); const toolbar = page.locator('#mobile-toolbar'); await expect(toolbar).toBeVisible(); // Open a long document so the layout is in its real document-view state. await openLongDocument(page); await expect(page.locator('#content-area')).toBeVisible(); // The scroll container itself must stop at (or above) the bar — 1px // tolerance for subpixel rounding. await expect .poll(async () => { const m = await measure(page); return m.areaBottom - m.barTop; }) .toBeLessThanOrEqual(1); }); test('the end of a long document scrolls clear of the toolbar', async ({ page, viewport }) => { test.skip((viewport?.width ?? 0) > MOBILE_MAX_WIDTH, 'Mobile viewport required'); await boot(page); await openLongDocument(page); // Scroll to the very end: the last rendered element must sit above the bar. await page.evaluate(() => { const area = document.getElementById('content-area'); area.scrollTop = area.scrollHeight; }); const result = await page.evaluate(() => { const area = document.getElementById('content-area'); const barTop = document.getElementById('mobile-toolbar').getBoundingClientRect().top; // Last element of the rendered document (markdown root or content child). const root = area.querySelector('.md-content') || area.lastElementChild; let last = root; if (root && root.lastElementChild) last = root.lastElementChild; const lastBottom = last ? last.getBoundingClientRect().bottom : area.getBoundingClientRect().bottom; return { lastBottom, barTop }; }); expect(result.lastBottom).toBeLessThanOrEqual(result.barTop + 1); }); });