Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d142049216 |
@@ -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: |
|
||||
|
||||
+20
-1
@@ -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é
|
||||
|
||||
+3
-3
@@ -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.
|
||||
|
||||
[]()
|
||||
[]()
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
[](https://www.docker.com/)
|
||||
[](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*
|
||||
|
||||
@@ -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.
|
||||
|
||||
[]()
|
||||
[]()
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
[](https://www.docker.com/)
|
||||
[](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*
|
||||
|
||||
Generated
+1
-1
@@ -2626,7 +2626,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "obsigate-desktop"
|
||||
version = "2.22.0"
|
||||
version = "2.22.1"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"env_logger",
|
||||
|
||||
+1
-1
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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) |
|
||||
|
||||
---
|
||||
|
||||
|
||||
+1
-1
@@ -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)**
|
||||
|
||||
+8
-5
@@ -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;
|
||||
|
||||
+1
-1
@@ -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": {
|
||||
|
||||
@@ -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