fix: degagement mobile sous la barre de navigation du bas (clearance retargetee .main-body) BUG-073
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* 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);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,132 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* ObsiGate — Mobile bottom toolbar clearance non-regression tests (BUG-073).
|
||||
*
|
||||
* Static checks (no jsdom — runs in the "Frontend unit tests" CI step):
|
||||
* - BUG-073: the fixed bottom toolbar (#mobile-toolbar, 64px + safe-area)
|
||||
* covered the last pixels of every document/page because the clearance
|
||||
* rule in the ≤768px media block targeted `.main-layout` — a class that
|
||||
* does not exist in index.html (the real layout container is
|
||||
* `.main-body`). The dead selector reserved no space, so #content-area
|
||||
* extended behind the bar and the end of the content could never be
|
||||
* scrolled clear of it.
|
||||
* - The clearance must live on `.main-body` inside the mobile media block
|
||||
* (desktop keeps no padding), the dead `.main-layout` rules must be gone,
|
||||
* reading mode (toolbar hidden) must reset the padding, and the
|
||||
* now-playing rule must not double-count the toolbar zone.
|
||||
*
|
||||
* Usage: node tests/frontend/mobile-toolbar.test.mjs
|
||||
*/
|
||||
|
||||
import { strict as assert } from "node:assert";
|
||||
import { readFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = path.join(__dirname, "..", "..");
|
||||
|
||||
const css = readFileSync(path.join(ROOT, "frontend", "style.css"), "utf8");
|
||||
const indexHtml = readFileSync(path.join(ROOT, "frontend", "index.html"), "utf8");
|
||||
|
||||
function test(label, fn) {
|
||||
try {
|
||||
fn();
|
||||
console.log(" \u2713 " + label);
|
||||
} catch (err) {
|
||||
console.error(" \u2717 " + label + "\n " + err.message);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
/** Body of the first @media (max-width: 768px) block whose body contains `needle`. */
|
||||
function mobileBlockContaining(needle) {
|
||||
const marker = "@media (max-width: 768px)";
|
||||
let idx = 0;
|
||||
while ((idx = css.indexOf(marker, idx)) !== -1) {
|
||||
const open = css.indexOf("{", idx);
|
||||
let depth = 0;
|
||||
let end = open;
|
||||
for (; end < css.length; end++) {
|
||||
if (css[end] === "{") depth++;
|
||||
else if (css[end] === "}") {
|
||||
depth--;
|
||||
if (depth === 0) break;
|
||||
}
|
||||
}
|
||||
const body = css.slice(open + 1, end);
|
||||
if (body.includes(needle)) return body;
|
||||
idx = end + 1;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ── The layout container the clearance must target exists in the DOM ───────
|
||||
test("index.html — layout container .main-body and #mobile-toolbar both exist", () => {
|
||||
assert.match(indexHtml, /class="main-body"/, "index.html must define the .main-body layout container");
|
||||
assert.match(indexHtml, /class="mobile-toolbar"/, "index.html must define the .mobile-toolbar bar");
|
||||
});
|
||||
|
||||
// ── BUG-073: clearance inside the mobile media block ────────────────────────
|
||||
const toolbarBlock = mobileBlockContaining(".mobile-toolbar");
|
||||
|
||||
test("style.css — a ≤768px media block styles .mobile-toolbar", () => {
|
||||
assert.ok(toolbarBlock, "no @media (max-width: 768px) block contains .mobile-toolbar");
|
||||
});
|
||||
|
||||
test("style.css — mobile clearance targets .main-body (64px + safe-area)", () => {
|
||||
assert.ok(toolbarBlock, "toolbar media block not found");
|
||||
assert.match(
|
||||
toolbarBlock,
|
||||
/\.main-body \{[^}]*padding-bottom:[^}]*calc\(64px \+ env\(safe-area-inset-bottom/,
|
||||
"the toolbar clearance must pad .main-body inside the mobile block (BUG-073: .main-layout does not exist in index.html)",
|
||||
);
|
||||
});
|
||||
|
||||
test("style.css — dead .main-layout clearance rules are gone", () => {
|
||||
assert.ok(toolbarBlock, "toolbar media block not found");
|
||||
assert.doesNotMatch(
|
||||
toolbarBlock,
|
||||
/\.main-layout \{[^}]*padding-bottom/,
|
||||
".main-layout clearance is a dead selector in index.html (BUG-073)",
|
||||
);
|
||||
assert.doesNotMatch(
|
||||
toolbarBlock,
|
||||
/\.editor-modal\.active ~ \.main-layout/,
|
||||
"the sibling clearance reset targeted a selector that never matches (BUG-073)",
|
||||
);
|
||||
});
|
||||
|
||||
test("style.css — desktop .main-body carries no toolbar padding", () => {
|
||||
const base = css.match(/\.main-body \{[^}]*\}/);
|
||||
assert.ok(base, ".main-body base rule not found");
|
||||
assert.doesNotMatch(base[0], /padding-bottom/, "toolbar clearance must stay mobile-only");
|
||||
});
|
||||
|
||||
// ── Companion rules ─────────────────────────────────────────────────────────
|
||||
test("style.css — reading mode resets the clearance (toolbar hidden)", () => {
|
||||
assert.match(
|
||||
css,
|
||||
/body\.reading-mode \.main-body \{[^}]*padding-bottom: 0/,
|
||||
"reading mode hides #mobile-toolbar: .main-body must release the padding",
|
||||
);
|
||||
});
|
||||
|
||||
test("style.css — np-active keeps only the dock zone (no toolbar double-count)", () => {
|
||||
assert.match(
|
||||
css,
|
||||
/body\.np-active \.content-area \{[^}]*padding-bottom: 76px/,
|
||||
"toolbar clearance now comes from .main-body: np-active must pad the dock zone only (76px)",
|
||||
);
|
||||
assert.doesNotMatch(
|
||||
css,
|
||||
/body\.np-active \.content-area \{[^}]*calc\(64px \+ env\(safe-area-inset-bottom[^}]*\+ 76px/,
|
||||
"the old formula double-counted the toolbar zone already cleared by .main-body",
|
||||
);
|
||||
});
|
||||
|
||||
if (process.exitCode) {
|
||||
console.error("\nMobile toolbar tests FAILED");
|
||||
} else {
|
||||
console.log("\nAll mobile toolbar tests passed.");
|
||||
}
|
||||
Reference in New Issue
Block a user