From d1420492168652849ad37812464c2834f4d0e986 Mon Sep 17 00:00:00 2001 From: Bruno Charest Date: Wed, 23 Sep 2026 23:36:31 -0400 Subject: [PATCH] fix: degagement mobile sous la barre de navigation du bas (clearance retargetee .main-body) BUG-073 --- .gitea/workflows/ci.yml | 1 + CHANGELOG.md | 21 +++- README.fr.md | 6 +- README.md | 6 +- VERSION | 2 +- desktop/Cargo.lock | 2 +- desktop/Cargo.toml | 2 +- desktop/tauri.conf.json | 2 +- docs/ISSUES_TODOLIST.md | 2 + docs/ROADMAP.md | 2 +- frontend/style.css | 13 ++- package.json | 2 +- tests/e2e/mobile-toolbar.spec.js | 103 +++++++++++++++++++ tests/frontend/mobile-toolbar.test.mjs | 132 +++++++++++++++++++++++++ 14 files changed, 278 insertions(+), 18 deletions(-) create mode 100644 tests/e2e/mobile-toolbar.spec.js create mode 100644 tests/frontend/mobile-toolbar.test.mjs diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 583c760..cb20046 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -43,6 +43,7 @@ jobs: node tests/frontend/forge-completion.test.mjs node tests/frontend/config-mobile.test.mjs node tests/frontend/settings-order-avatar.test.mjs + node tests/frontend/mobile-toolbar.test.mjs - name: Frontend JSDOM tests (PaneManager + Excalidraw + Plugins + AI + SW + Collab + Mobile + Semantic + Desktop + Inline edition) run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index 80f7a98..9213f7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ Format basé sur [Keep a Changelog](https://keepachangelog.com/fr/1.1.0/), et [Semantic Versioning](https://semver.org/spec/v2.0.0.html). > **En cours de développement** : les changements à venir sont listés dans la section -> [Unreleased](#unreleased). La dernière version livrée est **2.22.0**. +> [Unreleased](#unreleased). La dernière version livrée est **2.22.1**. --- @@ -14,6 +14,25 @@ et [Semantic Versioning](https://semver.org/spec/v2.0.0.html). --- +## [2.22.1] — 2026-09-23 + +### Corrigé + +- **BUG-073 — mobile, fin de contenu masquée.** La barre de navigation fixe du bas + (`#mobile-toolbar`, 64 px + safe-area) recouvrait le bas de tous les documents et + pages en vue mobile (≤ 768 px) : les dernières lignes restaient définitivement sous + la barre. Cause : la règle de clearance du bloc mobile ciblait `.main-layout`, + classe absente de `index.html` (le vrai conteneur est `.main-body`) — sélecteur + mort, aucun dégagement réservé. Correctif : cible `.main-body` + (`padding-bottom: calc(64px + env(safe-area-inset-bottom, 0))`), suppression de la + règle sœur morte `.editor-modal.active ~ .main-layout`, reset du dégagement en mode + lecture (barre masquée) et `body.np-active .content-area` ramené à `76 px` (le + dégagement dock, sans double comptage avec `.main-body`). Tests : + `tests/frontend/mobile-toolbar.test.mjs` (7, au CI) et E2E + `tests/e2e/mobile-toolbar.spec.js` (2, projet `chromium-mobile`). + +--- + ## [2.22.0] — 2026-09-23 ### Ajouté diff --git a/README.fr.md b/README.fr.md index 97858ef..2397435 100644 --- a/README.fr.md +++ b/README.fr.md @@ -4,7 +4,7 @@ **Porte d'entrée web ultra-léger pour vos vaults Obsidian** — Accédez, naviguez et recherchez dans toutes vos notes Obsidian depuis n'importe quel appareil via une interface web moderne et responsive. -[![Version](https://img.shields.io/badge/Version-2.22.0-blue.svg)]() +[![Version](https://img.shields.io/badge/Version-2.22.1-blue.svg)]() [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Docker](https://img.shields.io/badge/Docker-Ready-blue.svg)](https://www.docker.com/) [![Python](https://img.shields.io/badge/Python-3.11+-green.svg)](https://www.python.org/) @@ -975,8 +975,8 @@ Ce projet est sous licence **MIT** — voir le fichier [LICENSE](LICENSE) pour l ## 📝 Changelog -Consultez le [CHANGELOG.md](./CHANGELOG.md) pour l'historique complet de toutes les versions (v1.0.0 → v2.22.0). +Consultez le [CHANGELOG.md](./CHANGELOG.md) pour l'historique complet de toutes les versions (v1.0.0 → v2.22.1). --- -*Projet : ObsiGate | Version : 2.22.0 | Dernière mise à jour : Septembre 2026* +*Projet : ObsiGate | Version : 2.22.1 | Dernière mise à jour : Septembre 2026* diff --git a/README.md b/README.md index 6e81d7d..56e75c1 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ **Ultra-light web gateway for your Obsidian vaults** — Access, browse, and search all your Obsidian notes from any device via a modern, responsive web interface. -[![Version](https://img.shields.io/badge/Version-2.22.0-blue.svg)]() +[![Version](https://img.shields.io/badge/Version-2.22.1-blue.svg)]() [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Docker](https://img.shields.io/badge/Docker-Ready-blue.svg)](https://www.docker.com/) [![Python](https://img.shields.io/badge/Python-3.11+-green.svg)](https://www.python.org/) @@ -1150,8 +1150,8 @@ This project is licensed under the **MIT License** - see the [LICENSE](LICENSE) ## 📝 Changelog -See [CHANGELOG.md](./CHANGELOG.md) for the complete version history (v1.0.0 → v2.22.0). +See [CHANGELOG.md](./CHANGELOG.md) for the complete version history (v1.0.0 → v2.22.1). --- -*Project: ObsiGate | Version: 2.22.0 | Last updated: September 2026* +*Project: ObsiGate | Version: 2.22.1 | Last updated: September 2026* diff --git a/VERSION b/VERSION index f48f82f..d93847f 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.22.0 +2.22.1 diff --git a/desktop/Cargo.lock b/desktop/Cargo.lock index a9acc01..e277934 100644 --- a/desktop/Cargo.lock +++ b/desktop/Cargo.lock @@ -2626,7 +2626,7 @@ dependencies = [ [[package]] name = "obsigate-desktop" -version = "2.22.0" +version = "2.22.1" dependencies = [ "chrono", "env_logger", diff --git a/desktop/Cargo.toml b/desktop/Cargo.toml index 4ef8ea3..0bab0a6 100644 --- a/desktop/Cargo.toml +++ b/desktop/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "obsigate-desktop" -version = "2.22.0" +version = "2.22.1" description = "ObsiGate Desktop — Porte d'entrée native pour vos vaults Obsidian" authors = ["Bruno Charest"] edition = "2021" diff --git a/desktop/tauri.conf.json b/desktop/tauri.conf.json index d79d03b..816103b 100644 --- a/desktop/tauri.conf.json +++ b/desktop/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://raw.githubusercontent.com/nicedoc/obsigate/main/desktop/tauri.conf.schema.json", "productName": "ObsiGate", - "version": "2.22.0", + "version": "2.22.1", "identifier": "com.obsigate.desktop", "build": { "frontendDist": "../frontend", diff --git a/docs/ISSUES_TODOLIST.md b/docs/ISSUES_TODOLIST.md index 5770db7..f686467 100644 --- a/docs/ISSUES_TODOLIST.md +++ b/docs/ISSUES_TODOLIST.md @@ -181,6 +181,7 @@ Avant de corriger quoi que ce soit, un agent IA doit : | *BUG-070* | Activation clé physique WebAuthn impossible : « Validation du credential WebAuthn échouée » à chaque tentative | 🟢 corrigé | P0 | ⚙️ backend | IA | `backend/auth/webauthn_mfa.py`, `backend/auth/router.py` | Config → Sécurité → Ajouter une clé → cérémonie navigateur → 400 | `resolve_relying_party()` (rp_id/origines dérivés de la requête, config explicite prioritaire, forwarded si TRUST_PROXY) sur les 4 endpoints ; challenges multiples (5 derniers) acceptés ; `.env.example` ; tests `tests/test_webauthn.py` (+8) | Logs : origin `http://localhost:2020` rejetée + challenge mismatch au retry. Vérifié navigateur (authentificateur virtuel CDP) : register 200 + clé listée, clé de test retirée (admin de nouveau TOTP seul) ; pytest 1249 passed, ruff/mypy 0 | | *BUG-071* | Configuration « Configurations » inutilisable en mode mobile : sommaire masqué sans bouton d'accès, navigation par ancre sans JS, grilles 2 colonnes et rangées d'ajout qui débordent (≤768px) | 🟢 corrigé | P1 | 📱 frontend | IA | `frontend/index.html`, `frontend/js/config.js`, `frontend/js/i18n.js`, `frontend/style.css`, `frontend/locales/{fr,en}.json` | Mobile (≤768px) : ouvrir Configurations → aucun sommaire ni moyen d'atteindre une section ; champs « Clés IA » / jetons / webhooks débordent | `index.html` (+`#config-hamburger` `.help-hamburger`, `config.toc_toggle` FR/EN) ; `config.js` (toggle, scroll doux + actif + repli auto mobile, reset à l'ouverture) ; `i18n.js` (`data-i18n-attr` multi-paires `;`) ; `style.css` (bloc mobile `#config-modal` : sommaire haut 46vh, grilles 1fr, add-rows wrap + `!important`, items wrap, 44px) ; tests `tests/frontend/config-mobile.test.mjs` (nouveau, 11) + CI ; E2E `tests/e2e/config-mobile.spec.js` (nouveau, 3/3 projet chromium-mobile, ignoré en desktop) | pytest 1249 passed / 6 skipped, ruff 0, mypy 0, validate-imports 39 modules, unit 10/10, JSDOM ai 93/93 + sidebar 6/6 + mobile 35/35 + ai-keys 7/7 | | *BUG-072* | Visionneuse d'images : le plein écran et le panneau « Métadonnées » ne sont pas conservés lors de la navigation ←/→, et le panneau s'affiche sous la pellicule au lieu d'une barre latérale | 🟢 corrigé | P2 | 📱 frontend | IA | `frontend/js/viewer.js`, `frontend/style.css` | Ouvrir une image, activer le plein écran (ou Métadonnées), puis naviguer avec les flèches précédent/suivant | État persistant `_imageViewerState { lightbox, meta }` + drapeau `_imageViewerNavPending` posé par `go()`/pellicule : `renderFile` ne réinitialise que hors navigation image→image. Panneau reconstruit dans `.image-viewer-body` (sidebar droite, `border-left`, `width:280px; max-width:40%`) ; la règle lightbox ne masque plus que la pellicule. Boutons `image-btn-lightbox`/`image-btn-metadata` (+ `aria-pressed`), `Escape` resynchronisé. Tests : `tests/frontend/image-viewer.test.mjs` (+2), E2E `tests/e2e/image-viewer.spec.js` (+1). | Navigation → `openFile` → `renderImageViewer` recréait le conteneur : les états `lightbox`/`metaPanel` étaient perdus. Le panneau était rendu en bas (colonne) au lieu d'une sidebar droite | +| *BUG-073* | Mobile : la barre de navigation fixe du bas masque le bas de tous les documents et pages affichés (les dernières lignes restent définitivement sous la barre) | 🟢 corrigé | P1 | 📱 frontend | IA | `frontend/style.css` | Mobile (≤768px) : ouvrir un document long, défiler jusqu'au bas → la fin du contenu passe sous la barre `#mobile-toolbar` et n'est jamais atteignable | Clearance mobile retargetée de `.main-layout` (sélecteur mort, absent de `index.html`) vers `.main-body` (`calc(64px + env(safe-area-inset-bottom, 0))`) ; règle sœur morte `.editor-modal.active ~ .main-layout` supprimée ; reset `body.reading-mode .main-body { padding-bottom: 0 }` (barre masquée en mode lecture) ; `body.np-active .content-area` ramené à `76px` (dégagement dock seul, géométrie totale inchangée). Tests : `tests/frontend/mobile-toolbar.test.mjs` (7, au CI), E2E `tests/e2e/mobile-toolbar.spec.js` (2, `chromium-mobile`, skip desktop) | La règle de clearance du bloc mobile cible `.main-layout`, classe absente de `index.html` (vrai conteneur : `.main-body`) → sélecteur mort, aucun dégagement réservé | | | | | | | | | | | | | ### TODOs techniques (améliorations / nouvelles tâches) @@ -260,6 +261,7 @@ Avant de corriger quoi que ce soit, un agent IA doit : | 2026-09-23 | BUG-071 | Correction | `frontend/index.html`, `frontend/js/config.js`, `frontend/js/i18n.js`, `frontend/style.css`, `frontend/locales/{fr,en}.json`, `tests/frontend/config-mobile.test.mjs` (nouveau), `.gitea/workflows/ci.yml`, `CHANGELOG.md`, `docs/ISSUES_TODOLIST.md` | **BUG-071** : page « Configurations » inutilisable en mobile. (1) `#config-nav` masquée sous 768px sans toggle → hamburger `#config-hamburger` ajouté à l'en-tête (`.help-hamburger`, libellé `config.toc_toggle` FR/EN). (2) Ancres brutes sans JS → interception en `config.js` (scroll doux, lien actif, repli auto mobile, reset à l'ouverture). (3) Débordements 360px → bloc CSS mobile `#config-modal` (sommaire haut 46vh, grilles 1fr, add-rows wrap + largeurs inline neutralisées, items wrap, cibles 44px). `data-i18n-attr` multi-paires (`;`). Vérifié : `config-mobile.test.mjs` 11/11 (nouveau, au CI), pytest 1249 passed / 6 skipped, ruff/mypy 0, validate-imports 39 modules, unit 10/10, JSDOM ai 93/93 + ai-sidebar 6/6 + sidebar-filters 8/8 + mobile-editor 35/35 + config-ai-keys 7/7. | 🟢 corrigé (en attente vérif utilisateur) | | 2026-09-23 | BUG-071 (complément E2E) | Test | `tests/e2e/config-mobile.spec.js` (nouveau), `CHANGELOG.md`, `docs/ISSUES_TODOLIST.md` | **BUG-071 (complément E2E)** : spec Playwright mobile (convention `mobile-editor.spec.js` : `test.skip` hors viewport ≤768px, donc inactive sur le projet `chromium-desktop` du CI). Vérifié en local sur l'instance de test (port 2029, auth désactivée) : hamburger → sommaire, sélection → scroll + actif + repli, 0 débordement horizontal à 393px (3/3 `chromium-mobile`, 3 ignorés en desktop) ; suite `mobile-editor.spec.js` intacte (3/3). | 🟢 corrigé (en attente vérif utilisateur) | | 2026-09-23 | BUG-072 | Correction | `frontend/js/viewer.js`, `frontend/style.css`, `tests/frontend/image-viewer.test.mjs`, `tests/e2e/image-viewer.spec.js`, `scripts/run-e2e-local.ps1` (nouveau), `package.json`, `AGENTS.md`, `README.md`, `README.fr.md`, `CHANGELOG.md`, `docs/ISSUES_TODOLIST.md` | **BUG-072** : dans la visionneuse d'images (#108-D), le plein écran (lightbox) et le panneau « Métadonnées » étaient perdus dès qu'on changeait d'image avec ←/→ (ou la pellicule), car `openFile` → `renderFile` recrée entièrement `renderImageViewer`. (1) **Persistance** : état module `_imageViewerState { lightbox, meta }` restauré à chaque rendu ; un drapeau `_imageViewerNavPending` posé par `go()` et le clic de vignette indique à `renderFile` que le rendu suivant est une navigation image→image (pas de réinitialisation) — toute autre ouverture repart à zéro. (2) **Panneau latéral** : `.image-meta-panel` déplacé dans un nouveau `.image-viewer-body` en flex row, à droite de `.image-stage` (`border-left`, `width:280px; max-width:40%`, défilement vertical) au lieu d'une bande sous la pellicule ; la règle lightbox ne masque plus que la pellicule. Boutons stables `image-btn-lightbox`/`image-btn-metadata` + `aria-pressed`, `Escape` resynchronise l'état. Tests statiques `image-viewer.test.mjs` (+2) et E2E Playwright (+1). **Diagnostic E2E** : `npm run test:e2e` bloquait car `bash` résout vers WSL (HS, Ubuntu `Stopped`, `HCS_E_CONNECTION_TIMEOUT`) et git-bash est bloqué par App Control → lanceur PowerShell ajouté. Vérifié : `image-viewer.spec.js` 4/4, **suite `chromium-desktop` complète 103 passed / 6 skipped (10,3 min)** via `scripts/run-e2e-local.ps1`, `image-viewer.test.mjs` 12/12, unit 10/10, validate-imports 40 modules. | 🟢 corrigé (en attente vérif utilisateur) | +| 2026-09-23 | BUG-073 | Correction | `frontend/style.css`, `tests/frontend/mobile-toolbar.test.mjs` (nouveau), `tests/e2e/mobile-toolbar.spec.js` (nouveau), `.gitea/workflows/ci.yml`, `CHANGELOG.md`, `docs/ISSUES_TODOLIST.md` | **BUG-073** : en mobile (≤768px), la barre fixe `#mobile-toolbar` (64px + safe-area) recouvrait le bas de tous les documents/pages — fin de contenu inaccessible. (1) **Cause** : la règle de clearance du bloc `@media (max-width: 768px)` ciblait `.main-layout`, classe absente de `index.html` (vrai conteneur : `.main-body`) → sélecteur mort, zéro dégagement ; règle sœur morte `.editor-modal.active ~ .main-layout { padding-bottom: 0 }` supprimée (overlay plein écran / nécessaire en édition inline). (2) **Correctif** : `.main-body { padding-bottom: calc(64px + env(safe-area-inset-bottom, 0)) }`, reset `body.reading-mode .main-body { padding-bottom: 0 }` (barre masquée en mode lecture), `body.np-active .content-area` ramené de `calc(64px+safe+76px)` à `76px` (dégagement dock seul — géométrie totale identique, pas de double comptage avec `.main-body`). Tests : `mobile-toolbar.test.mjs` (7 statiques, ajouté au CI), E2E `mobile-toolbar.spec.js` (géométrie + scroll fin de `ANALYSE_REVIEW.md`, skip hors viewport ≤768). Vérifié : `mobile-toolbar` 7/7, JSDOM 14 suites 0 échec, **E2E `chromium-mobile` BUG-073 2/2 + régressions mobile-editor/config-mobile 6/6**, **suite `chromium-desktop` complète 106 passed / 9 skipped (11,4 min)**, pytest 1302 passed / 6 skipped, ruff/mypy 0, validate-imports 40 modules, unit 11/11. | 🟢 corrigé (en attente vérif utilisateur) | --- diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 4b5bcae..ec0b8de 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -1,6 +1,6 @@ # ObsiGate — Roadmap -> **Version :** 2.22.0 | **Dernière mise à jour :** 2026-09-23 +> **Version :** 2.22.1 | **Dernière mise à jour :** 2026-09-23 > **Ce fichier ne contient que le travail à venir** (🔵 En cours + ⚪ Backlog) et un index compact > vers les fonctionnalités livrées. > - **Méthode de livraison à appliquer pour toute tâche : [DELIVERY_WORKFLOW.md](./DELIVERY_WORKFLOW.md)** diff --git a/frontend/style.css b/frontend/style.css index ca935c4..83b6e4c 100644 --- a/frontend/style.css +++ b/frontend/style.css @@ -8617,13 +8617,10 @@ body.desktop-mode .editor-container { } /* Push content up to avoid toolbar overlap */ - .main-layout { + .main-body { padding-bottom: 64px; padding-bottom: calc(64px + env(safe-area-inset-bottom, 0)); } - .editor-modal.active ~ .main-layout { - padding-bottom: 0; - } /* Enable pull-to-refresh on mobile — unblock the overscroll chain */ html { @@ -10567,7 +10564,9 @@ body.desktop-mode .editor-container { bottom: calc(64px + env(safe-area-inset-bottom, 0px) + 12px); } body.np-active .content-area { - padding-bottom: calc(64px + env(safe-area-inset-bottom, 0px) + 76px); + /* Toolbar clearance is carried by .main-body (BUG-073): only the dock + zone (76px above the toolbar) remains here — same total as before. */ + padding-bottom: 76px; } } @@ -11600,6 +11599,10 @@ body.reading-mode .content-area { margin: 0 auto; padding: clamp(20px, 6vw, 48px) clamp(18px, 6vw, 40px) 96px; } +/* The bottom toolbar is hidden in reading mode: no clearance needed. */ +body.reading-mode .main-body { + padding-bottom: 0; +} body.reading-mode .breadcrumb, body.reading-mode .file-actions { display: none; diff --git a/package.json b/package.json index daadecc..90df877 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "obsigate", - "version": "2.22.0", + "version": "2.22.1", "description": "**Porte d'entrée web ultra-léger pour vos vaults Obsidian** — Accédez, naviguez et recherchez dans toutes vos notes Obsidian depuis n'importe quel appareil via une interface web moderne et responsive.", "main": "patch.js", "directories": { diff --git a/tests/e2e/mobile-toolbar.spec.js b/tests/e2e/mobile-toolbar.spec.js new file mode 100644 index 0000000..395e73d --- /dev/null +++ b/tests/e2e/mobile-toolbar.spec.js @@ -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); + }); +}); diff --git a/tests/frontend/mobile-toolbar.test.mjs b/tests/frontend/mobile-toolbar.test.mjs new file mode 100644 index 0000000..f6d3175 --- /dev/null +++ b/tests/frontend/mobile-toolbar.test.mjs @@ -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."); +}