From 8264e7ffae88e63076aebe9dc90fd56349103ecd Mon Sep 17 00:00:00 2001 From: Bruno Charest Date: Wed, 23 Sep 2026 13:43:44 -0400 Subject: [PATCH] fix: conserver plein ecran et panneau metadonnees dans la visionneuse d'images (sidebar droite) + lanceur E2E Windows BUG-072 --- AGENTS.md | 6 +- CHANGELOG.md | 24 +++++- README.fr.md | 17 +++- README.md | 16 +++- 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/js/viewer.js | 63 ++++++++++---- frontend/style.css | 15 +++- package.json | 5 +- scripts/run-e2e-local.ps1 | 120 +++++++++++++++++++++++++++ tests/e2e/image-viewer.spec.js | 28 +++++++ tests/frontend/image-viewer.test.mjs | 27 ++++++ 16 files changed, 297 insertions(+), 36 deletions(-) create mode 100644 scripts/run-e2e-local.ps1 diff --git a/AGENTS.md b/AGENTS.md index 559e206..f7daff8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -44,9 +44,13 @@ node tests/frontend/unit.test.mjs # Tests JSDOM : node_modules dans tests/frontend/ (npm install là-bas si absent), ex : node tests/frontend/pane-manager.test.mjs -# E2E (si UI touchée, ~5 min) : reproduit le job CI e2e (port 2029, auth désactivée) +# E2E (si UI touchée, ~10 min) : reproduit le job CI e2e (port 2029, auth désactivée) npm run test:e2e # prérequis : uv, Node >= 20, npx playwright install chromium bash scripts/run-e2e-local.sh -g "nom du test" # filtre / --headed + +# Windows sans bash exploitable (WSL HS, git-bash bloqué par App Control) : +npm run test:e2e:ps # équivalent PowerShell, mêmes conditions que le CI +pwsh -File scripts/run-e2e-local.ps1 -PlaywrightArgs @('-g','nom du test') ``` - Un seul test backend : `.\.venv\Scripts\python.exe -m pytest tests/test_search.py -q`. diff --git a/CHANGELOG.md b/CHANGELOG.md index 70ae52b..b4c2a0c 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.19.1**. +> [Unreleased](#unreleased). La dernière version livrée est **2.19.2**. --- @@ -14,6 +14,28 @@ et [Semantic Versioning](https://semver.org/spec/v2.0.0.html). --- +## [2.19.2] — 2026-09-23 + +### Ajouté + +- **Lanceur E2E Windows/PowerShell** : `scripts/run-e2e-local.ps1` (+ script npm + `test:e2e:ps`) reproduit localement le job CI `e2e` (uvicorn natif, auth + désactivée, fixtures TestVault/TestDir, port 2029, projet `chromium-desktop`) + sans dépendre de `bash` — indispensable sur les postes Windows où WSL ne + démarre pas et où git-bash est bloqué par une politique de contrôle + d'application. Le script `bash` reste la référence pour la CI/Linux. + +### Corrigé + +- **BUG-072 — visionneuse d'images** : le plein écran (lightbox) et le panneau + « Métadonnées » sont désormais **conservés lors de la navigation** entre images + (flèches ←/→ et clic sur la pellicule) ; auparavant chaque changement d'image + recréait la visionneuse et perdait ces états. Le panneau de métadonnées s'affiche + maintenant en **barre latérale à droite de l'image** (au lieu d'une bande sous la + pellicule) et reste visible en plein écran. + +--- + ## [2.19.1] — 2026-09-23 ### Corrigé diff --git a/README.fr.md b/README.fr.md index 5e12fc4..b3b1899 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.19.1-blue.svg)]() +[![Version](https://img.shields.io/badge/Version-2.19.2-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/) @@ -872,7 +872,7 @@ Configurables via l'interface (Settings) ou l'API `/api/config`. | Validation des imports frontend | `node tests/frontend/validate-imports.mjs` | `lint` | | Tests unitaires frontend | `node tests/frontend/unit.test.mjs` | `lint` | | Tests backend | `pytest tests/ -q` | `test` | -| **E2E Playwright** | `npm run test:e2e` (~5 min) | `e2e` | +| **E2E Playwright** | `npm run test:e2e` (~10 min) | `e2e` | #### Tests E2E locaux (`npm run test:e2e`) @@ -895,6 +895,15 @@ bash scripts/run-e2e-local.sh --headed # navigateur visible bash scripts/run-e2e-local.sh -g "reset panes" # filtre sur un test ``` +Sous Windows, si `bash` n'est pas exploitable (WSL indisponible, git-bash +bloqué par une politique de contrôle d'application), utiliser le lanceur +PowerShell équivalent : + +```powershell +npm run test:e2e:ps +pwsh -File scripts/run-e2e-local.ps1 -PlaywrightArgs @('-g','reset panes') +``` + La suite doit se terminer sur **tous les tests passant** (60 actuellement), sans échec ni dépendance aux retries. En cas d'échec : corriger et relancer localement jusqu'à 100 %, puis seulement commiter. @@ -966,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.19.1). +Consultez le [CHANGELOG.md](./CHANGELOG.md) pour l'historique complet de toutes les versions (v1.0.0 → v2.19.2). --- -*Projet : ObsiGate | Version : 2.19.1 | Dernière mise à jour : Septembre 2026* +*Projet : ObsiGate | Version : 2.19.2 | Dernière mise à jour : Septembre 2026* diff --git a/README.md b/README.md index fddcb47..a053a50 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.19.1-blue.svg)]() +[![Version](https://img.shields.io/badge/Version-2.19.2-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/) @@ -1042,7 +1042,7 @@ These parameters are configurable via the interface (Settings) or the `/api/conf | Frontend import validation | `node tests/frontend/validate-imports.mjs` | `lint` | | Frontend unit tests | `node tests/frontend/unit.test.mjs` | `lint` | | Backend tests | `pytest tests/ -q` | `test` | -| **E2E Playwright** | `npm run test:e2e` (~5 min) | `e2e` | +| **E2E Playwright** | `npm run test:e2e` (~10 min) | `e2e` | #### Local E2E Tests (`npm run test:e2e`) @@ -1065,6 +1065,14 @@ bash scripts/run-e2e-local.sh --headed # visible browser bash scripts/run-e2e-local.sh -g "reset panes" # filter on a test ``` +On Windows, when `bash` is unusable (WSL unavailable, git-bash blocked by an +Application Control policy), use the equivalent PowerShell launcher: + +```powershell +npm run test:e2e:ps +pwsh -File scripts/run-e2e-local.ps1 -PlaywrightArgs @('-g','reset panes') +``` + The suite must end with **all tests passing** (60 currently), with no failure or reliance on retries. In case of failure: fix and re-run locally until 100 %, then only commit. @@ -1142,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.19.1). +See [CHANGELOG.md](./CHANGELOG.md) for the complete version history (v1.0.0 → v2.19.2). --- -*Project: ObsiGate | Version: 2.19.1 | Last updated: September 2026* +*Project: ObsiGate | Version: 2.19.2 | Last updated: September 2026* diff --git a/VERSION b/VERSION index b8e248f..17bdb70 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.19.1 +2.19.2 diff --git a/desktop/Cargo.lock b/desktop/Cargo.lock index a83c8aa..308eafe 100644 --- a/desktop/Cargo.lock +++ b/desktop/Cargo.lock @@ -2626,7 +2626,7 @@ dependencies = [ [[package]] name = "obsigate-desktop" -version = "2.19.1" +version = "2.19.2" dependencies = [ "chrono", "env_logger", diff --git a/desktop/Cargo.toml b/desktop/Cargo.toml index 839642d..4b8f32d 100644 --- a/desktop/Cargo.toml +++ b/desktop/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "obsigate-desktop" -version = "2.19.1" +version = "2.19.2" 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 b484aa8..2e4a5cf 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.19.1", + "version": "2.19.2", "identifier": "com.obsigate.desktop", "build": { "frontendDist": "../frontend", diff --git a/docs/ISSUES_TODOLIST.md b/docs/ISSUES_TODOLIST.md index 764f329..5770db7 100644 --- a/docs/ISSUES_TODOLIST.md +++ b/docs/ISSUES_TODOLIST.md @@ -180,6 +180,7 @@ Avant de corriger quoi que ce soit, un agent IA doit : | *BUG-069* | Login 2FA bloqué sans erreur : après user+pwd corrects, la page de login reste affichée et le challenge MFA n'apparaît jamais | 🟢 corrigé | P0 | 📱 frontend | IA | `frontend/js/auth.js`, `frontend/index.html` | Activer 2FA → logout → login (bon user+pwd) | `frontend/js/auth.js` (`showMfaChallenge` → `.login-card` + erreur `mfa.challenge_unavailable` si montage impossible), locales FR/EN ; tests `tests/frontend/mfa-settings.test.mjs` (+2) | Reproduit au navigateur avant correctif (challenge jamais affiché), vérifié après : challenge affiché, code erroné → erreur, code valide (200) → app ; frontend mfa-settings 11/11, unit + validate-imports verts | | *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 | | | | | | | | | | | | | ### TODOs techniques (améliorations / nouvelles tâches) @@ -258,6 +259,7 @@ Avant de corriger quoi que ce soit, un agent IA doit : | 2026-09-23 | BUG-070 | Correction | `backend/auth/webauthn_mfa.py`, `backend/auth/router.py`, `.env.example`, `tests/test_webauthn.py`, `CHANGELOG.md`, `docs/ISSUES_TODOLIST.md` | **BUG-070** : activation WebAuthn rejetée en 400. (1) Défauts `localhost` sans port → `resolve_relying_party()` dérive rp_id/origines de la requête (config explicite prioritaire, forwarded sous TRUST_PROXY), appliqué aux endpoints register + login. (2) Challenge single-use → 5 derniers conservés, vérification contre le challenge de la cérémonie en cours. **Vérifié au navigateur** (authentificateur virtuel CDP, instance Docker) : register 200, clé listée, clé de test retirée. Tests : `test_webauthn.py` 19/19 (+8), suite complète 1249 passed / 6 skipped, ruff/mypy 0. | 🟢 corrigé (en attente vérif utilisateur) | | 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) | --- diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 7aa270a..72671e0 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -1,6 +1,6 @@ # ObsiGate — Roadmap -> **Version :** 2.19.1 | **Dernière mise à jour :** 2026-09-23 +> **Version :** 2.19.2 | **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/js/viewer.js b/frontend/js/viewer.js index 046ce33..be6c173 100644 --- a/frontend/js/viewer.js +++ b/frontend/js/viewer.js @@ -548,6 +548,13 @@ const IMAGE_EXTS = new Set([".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp", ". const IMAGE_ZOOM_MIN = 0.1; const IMAGE_ZOOM_MAX = 8; let _imageViewerCleanup = null; +// BUG-072 — the lightbox and metadata panel states must survive the re-render +// triggered by ←/→ navigation (openFile → renderFile → renderImageViewer). +// Reset as soon as a non-image file is rendered. +const _imageViewerState = { lightbox: false, meta: false }; +// Set right before an image→image navigation (←/→ / filmstrip click) so +// renderFile knows the state above must be carried over instead of reset. +let _imageViewerNavPending = false; /** Clamp a zoom factor into the supported [0.1, 8] range. */ export function clampImageZoom(value) { @@ -614,27 +621,29 @@ export function renderImageViewer(area, data) { const nextBtn = mkBtn("chevron-right", t("viewer.image_next")); const originalBtn = mkBtn("external-link", t("viewer.image_open_original")); const downloadBtn = mkBtn("download", t("viewer.download")); - const metaBtn = mkBtn("info", t("viewer.image_metadata")); - const lightboxBtn = mkBtn("maximize", t("viewer.image_fullscreen")); + const metaBtn = mkBtn("info", t("viewer.image_metadata"), "image-btn-metadata"); + const lightboxBtn = mkBtn("maximize", t("viewer.image_fullscreen"), "image-btn-lightbox"); [prevBtn, nextBtn, zoomOutBtn, zoomInBtn, zoomResetBtn, metaBtn, originalBtn, downloadBtn, lightboxBtn] .forEach((b) => toolbar.appendChild(b)); container.appendChild(toolbar); - // ── Stage (image + pan/zoom) ─────────────────────────────────────────── + // ── Body: image stage + metadata sidebar (right) ─────────────────────── + const body = el("div", { class: "image-viewer-body" }); const stage = el("div", { class: "image-stage" }); const img = el("img", { class: "image-main", src: imgUrl, alt: data.title || fileName, draggable: "false" }); stage.appendChild(img); - container.appendChild(stage); + body.appendChild(stage); + + // ── Metadata sidebar (kept open across ←/→ navigation) ───────────────── + const metaPanel = el("div", { class: "image-meta-panel" }); + metaPanel.hidden = !_imageViewerState.meta; + body.appendChild(metaPanel); + container.appendChild(body); // ── Thumbnail filmstrip (navigation) ─────────────────────────────────── const strip = el("div", { class: "image-nav-strip", hidden: true }); container.appendChild(strip); - // ── Metadata panel ───────────────────────────────────────────────────── - const metaPanel = el("div", { class: "image-meta-panel" }); - metaPanel.hidden = true; - container.appendChild(metaPanel); - let scale = 1; let tx = 0; let ty = 0; @@ -674,6 +683,7 @@ export function renderImageViewer(area, data) { const go = (delta) => { if (siblings.length < 2 || currentIndex < 0) return; const next = (currentIndex + delta + siblings.length) % siblings.length; + _imageViewerNavPending = true; openFile(vault, siblings[next].path); }; @@ -689,7 +699,11 @@ export function renderImageViewer(area, data) { title: s.name, loading: "lazy", }); - thumb.addEventListener("click", () => { if (s.path !== path) openFile(vault, s.path); }); + thumb.addEventListener("click", () => { + if (s.path === path) return; + _imageViewerNavPending = true; + openFile(vault, s.path); + }); strip.appendChild(thumb); }); }; @@ -751,13 +765,21 @@ export function renderImageViewer(area, data) { const dlUrl = `/api/file/${encodeURIComponent(vault)}/download?path=${encodeURIComponent(path)}`; window.open(dlUrl, "_blank"); }); + metaBtn.setAttribute("aria-pressed", _imageViewerState.meta ? "true" : "false"); metaBtn.addEventListener("click", () => { - metaPanel.hidden = !metaPanel.hidden; - if (!metaPanel.hidden) renderMeta(); + _imageViewerState.meta = !_imageViewerState.meta; + metaPanel.hidden = !_imageViewerState.meta; + metaBtn.setAttribute("aria-pressed", _imageViewerState.meta ? "true" : "false"); + if (_imageViewerState.meta) renderMeta(); }); + const syncLightbox = () => { + container.classList.toggle("lightbox", _imageViewerState.lightbox); + lightboxBtn.setAttribute("aria-pressed", _imageViewerState.lightbox ? "true" : "false"); + }; + syncLightbox(); lightboxBtn.addEventListener("click", () => { - container.classList.toggle("lightbox"); - lightboxBtn.setAttribute("aria-pressed", container.classList.contains("lightbox") ? "true" : "false"); + _imageViewerState.lightbox = !_imageViewerState.lightbox; + syncLightbox(); }); img.addEventListener("load", () => { if (!metaPanel.hidden) renderMeta(); }); @@ -772,7 +794,10 @@ export function renderImageViewer(area, data) { else if (e.key === "+" || e.key === "=") { e.preventDefault(); setZoom(scale * 1.25); } else if (e.key === "-") { e.preventDefault(); setZoom(scale / 1.25); } else if (e.key === "0") { e.preventDefault(); resetView(); } - else if (e.key === "Escape") { container.classList.remove("lightbox"); } + else if (e.key === "Escape") { + _imageViewerState.lightbox = false; + syncLightbox(); + } }; document.addEventListener("keydown", onKey); _imageViewerCleanup = () => document.removeEventListener("keydown", onKey); @@ -780,6 +805,7 @@ export function renderImageViewer(area, data) { area.appendChild(container); safeCreateIcons(); applyTransform(); + if (_imageViewerState.meta) renderMeta(); try { container.focus({ preventScroll: true }); } catch (_) { /* non-fatal */ } } @@ -810,6 +836,13 @@ export function renderFile(data) { // #108 — release the image viewer's document-level shortcuts before swapping // the content area (otherwise they leak on every re-render). if (_imageViewerCleanup) { _imageViewerCleanup(); _imageViewerCleanup = null; } + // BUG-072 — carry the lightbox / metadata-panel state over an image→image + // navigation; any other render starts from a clean viewer. + if (!data.is_image || !_imageViewerNavPending) { + _imageViewerState.lightbox = false; + _imageViewerState.meta = false; + } + _imageViewerNavPending = false; const area = getContentArea(); // #110 — if this render is about to replace the surface that currently hosts // the shared media element, hand it back to the persistent dock first. diff --git a/frontend/style.css b/frontend/style.css index baf2ff2..e156ad5 100644 --- a/frontend/style.css +++ b/frontend/style.css @@ -9673,6 +9673,11 @@ body.desktop-mode .editor-container { min-width: 46px; text-align: center; } +.image-viewer-body { + flex: 1; + display: flex; + min-height: 0; +} .image-stage { flex: 1; position: relative; @@ -9680,7 +9685,7 @@ body.desktop-mode .editor-container { display: flex; align-items: center; justify-content: center; - min-height: 60vh; + min-width: 0; background: var(--surface2); cursor: grab; touch-action: none; @@ -9731,9 +9736,12 @@ body.desktop-mode .editor-container { opacity: 1; } .image-meta-panel { + width: 280px; + max-width: 40%; + overflow-y: auto; padding: 10px 16px; background: var(--surface); - border-top: 1px solid var(--border); + border-left: 1px solid var(--border); font-size: 0.8rem; color: var(--text-secondary); flex-shrink: 0; @@ -9761,8 +9769,7 @@ body.desktop-mode .editor-container { z-index: 2000; background: rgba(0, 0, 0, 0.9); } -.image-viewer-container.lightbox .image-nav-strip, -.image-viewer-container.lightbox .image-meta-panel { +.image-viewer-container.lightbox .image-nav-strip { display: none; } diff --git a/package.json b/package.json index c91e173..4745484 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "obsigate", - "version": "2.19.1", + "version": "2.19.2", "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": { @@ -9,7 +9,8 @@ }, "scripts": { "test": "echo \"Error: no test specified\" && exit 1", - "test:e2e": "bash scripts/run-e2e-local.sh" + "test:e2e": "bash scripts/run-e2e-local.sh", + "test:e2e:ps": "pwsh -NoProfile -File scripts/run-e2e-local.ps1" }, "repository": { "type": "git", diff --git a/scripts/run-e2e-local.ps1 b/scripts/run-e2e-local.ps1 new file mode 100644 index 0000000..701dba0 --- /dev/null +++ b/scripts/run-e2e-local.ps1 @@ -0,0 +1,120 @@ +<# +.SYNOPSIS + ObsiGate — E2E locaux (Playwright) sous Windows/PowerShell. + +.DESCRIPTION + Équivalent PowerShell de `scripts/run-e2e-local.sh`, pour les postes Windows + où `bash` n'est pas utilisable (WSL indisponible, git-bash bloqué par une + politique de contrôle d'application). Démarre le backend nativement via + uvicorn (auth désactivée, fixtures TestVault/TestDir, port 2029 — mêmes + conditions que le job CI `e2e`), lance la suite Playwright puis nettoie. + +.PARAMETER PlaywrightArgs + Arguments transmis à `npx playwright test`, ex. `-g "image viewer"`, + `--headed`. + +.EXAMPLE + ./scripts/run-e2e-local.ps1 + ./scripts/run-e2e-local.ps1 -g "BUG-072" + ./scripts/run-e2e-local.ps1 --headed +#> +[CmdletBinding()] +param( + [Parameter(ValueFromRemainingArguments = $true)] + [string[]]$PlaywrightArgs +) + +$ErrorActionPreference = "Stop" + +$Root = Split-Path -Parent $PSScriptRoot +Set-Location -LiteralPath $Root + +$Port = if ($env:E2E_PORT) { $env:E2E_PORT } else { "2029" } +$BaseUrl = "http://127.0.0.1:$Port" +$ServerLog = "data/e2e-server.log" +$ServerErrLog = "data/e2e-server.err.log" + +function Assert-Command([string]$Name, [string]$Hint) { + if (-not (Get-Command $Name -ErrorAction SilentlyContinue)) { + throw "[ERR] $Name introuvable. $Hint" + } +} + +Assert-Command "uv" "Installez-le : https://docs.astral.sh/uv/" +Assert-Command "npx" "Installez Node.js (>= 20)." + +# ----- Venv Python 3.11 (créé une seule fois) ----- +$Python = ".venv-e2e/Scripts/python.exe" +if (-not (Test-Path -LiteralPath $Python)) { + Write-Host "[INFO] Création du venv .venv-e2e (Python 3.11)..." + uv venv .venv-e2e --python 3.11 + uv pip install --python $Python -r backend/requirements.txt +} + +# ----- Port déjà occupé ? ----- +try { + Invoke-WebRequest -Uri "$BaseUrl/api/health" -TimeoutSec 2 -UseBasicParsing | Out-Null + Write-Host "[ERR] Quelque chose répond déjà sur $BaseUrl." + Write-Host " Arrêtez-le ou choisissez un autre port : `$env:E2E_PORT=2030; ./scripts/run-e2e-local.ps1" + exit 1 +} catch { + # port libre +} + +# ----- Démarrage du serveur (mêmes conditions que le CI e2e) ----- +Write-Host "[INFO] Démarrage d'ObsiGate sur $BaseUrl (auth désactivée)..." +New-Item -ItemType Directory -Force -Path "data" | Out-Null + +$env:OBSIGATE_AUTH_ENABLED = "false" +$env:VAULT_1_NAME = "TestVault" +$env:VAULT_1_PATH = (Resolve-Path -LiteralPath "test_vault").Path +$env:DIR_1_NAME = "TestDir" +$env:DIR_1_PATH = (Resolve-Path -LiteralPath "test_dir").Path + +$server = Start-Process -FilePath $Python ` + -ArgumentList "-m", "uvicorn", "backend.main:app", "--host", "127.0.0.1", "--port", $Port ` + -RedirectStandardOutput $ServerLog -RedirectStandardError $ServerErrLog ` + -PassThru -WindowStyle Hidden + +$exitCode = 1 +try { + # ----- Attente du health check (30 s max, comme le CI) ----- + $ready = $false + for ($i = 0; $i -lt 30; $i++) { + try { + Invoke-WebRequest -Uri "$BaseUrl/api/health" -TimeoutSec 2 -UseBasicParsing | Out-Null + $ready = $true + break + } catch { + if ($server.HasExited) { + Write-Host "[ERR] Le serveur a quitté prématurément. Log :" + Get-Content -LiteralPath $ServerErrLog -Tail 30 -ErrorAction SilentlyContinue + exit 1 + } + Start-Sleep -Seconds 1 + } + } + if (-not $ready) { + Write-Host "[ERR] Serveur injoignable sur $BaseUrl. Log :" + Get-Content -LiteralPath $ServerErrLog -Tail 30 -ErrorAction SilentlyContinue + exit 1 + } + Write-Host "[OK] Serveur prêt." + + # ----- Browsers Playwright (no-op s'ils sont déjà installés) ----- + npx playwright install chromium + + # ----- Exécution de la suite (projet CI : chromium-desktop) ----- + Write-Host "[INFO] BASE_URL=$BaseUrl npx playwright test --project=chromium-desktop $($PlaywrightArgs -join ' ')" + $env:BASE_URL = $BaseUrl + & npx playwright test --project=chromium-desktop @PlaywrightArgs + $exitCode = $LASTEXITCODE +} finally { + Write-Host "[INFO] Arrêt du serveur (PID $($server.Id))..." + if (-not $server.HasExited) { Stop-Process -Id $server.Id -Force -ErrorAction SilentlyContinue } + # uvicorn (via uv) peut lancer un interpréteur enfant : tuer le groupe resté sur le port. + Get-NetTCPConnection -LocalPort $Port -State Listen -ErrorAction SilentlyContinue | + ForEach-Object { Stop-Process -Id $_.OwningProcess -Force -ErrorAction SilentlyContinue } +} + +exit $exitCode diff --git a/tests/e2e/image-viewer.spec.js b/tests/e2e/image-viewer.spec.js index 86739b4..cb8515e 100644 --- a/tests/e2e/image-viewer.spec.js +++ b/tests/e2e/image-viewer.spec.js @@ -95,4 +95,32 @@ test.describe('Image viewer — zoom / pan / navigation (#108)', () => { await page.locator('#content-area .image-nav-strip img.image-thumb').first().click(); await expect(page.locator('#content-area .image-title')).toBeVisible(); }); + + test('conserve le plein écran et le panneau métadonnées à la navigation (#BUG-072)', async ({ page }) => { + await login(page); + await openFile(page, 'TestVault', 'sample-image.png'); + + const container = page.locator('#content-area .image-viewer-container'); + const metaPanel = page.locator('#content-area .image-meta-panel'); + const title = page.locator('#content-area .image-title'); + await expect(container).toBeVisible({ timeout: 10000 }); + + // Métadonnées : barre latérale à droite de l'image (pas sous la pellicule). + await page.locator('#content-area .image-btn-metadata').click(); + await expect(metaPanel).toBeVisible(); + const stageBox = await page.locator('#content-area .image-stage').boundingBox(); + const metaBox = await metaPanel.boundingBox(); + expect(metaBox.x).toBeGreaterThanOrEqual(stageBox.x + stageBox.width - 1); + + // Plein écran activé. + await page.locator('#content-area .image-btn-lightbox').click(); + await expect(container).toHaveClass(/lightbox/); + + // Naviguer (flèche droite) : les deux états doivent survivre au re-render. + const titleBefore = await title.innerText(); + await page.keyboard.press('ArrowRight'); + await expect(container).toHaveClass(/lightbox/); + await expect(metaPanel).toBeVisible(); + await expect(title).not.toHaveText(titleBefore); + }); }); diff --git a/tests/frontend/image-viewer.test.mjs b/tests/frontend/image-viewer.test.mjs index 86612e1..7c90482 100644 --- a/tests/frontend/image-viewer.test.mjs +++ b/tests/frontend/image-viewer.test.mjs @@ -93,6 +93,20 @@ test("image viewer exposes thumbnail navigation via /api/media thumb", () => { assert.match(viewer, /siblings = \(res\.items \|\| \[\]\)/); }); +// BUG-072 — lightbox + metadata panel must survive ←/→ (and filmstrip) navigation. +test("image viewer persists lightbox + metadata state across navigation", () => { + assert.match(viewer, /const _imageViewerState = \{ lightbox: false, meta: false \}/); + assert.match(viewer, /let _imageViewerNavPending = false/); + // Both navigation paths flag the next render as an image→image navigation. + assert.match(viewer, /_imageViewerNavPending = true;\s*\n\s*openFile\(vault, siblings\[next\]\.path\)/); + assert.match(viewer, /_imageViewerNavPending = true;\s*\n\s*openFile\(vault, s\.path\)/); + // renderFile resets the state only when the next render is not a carried-over image. + assert.match(viewer, /if \(!data\.is_image \|\| !_imageViewerNavPending\)/); + // The persisted state drives the container class and the panel visibility. + assert.match(viewer, /classList\.toggle\("lightbox", _imageViewerState\.lightbox\)/); + assert.match(viewer, /metaPanel\.hidden = !_imageViewerState\.meta/); +}); + // ── Static checks: CSS ───────────────────────────────────────────────────── test("style.css defines the image viewer layout + lightbox", () => { assert.match(css, /\.image-stage\s*\{/); @@ -101,6 +115,19 @@ test("style.css defines the image viewer layout + lightbox", () => { assert.match(css, /\.image-viewer-container\.lightbox\s*\{/); }); +// BUG-072 — metadata is a right-hand sidebar, not a strip under the filmstrip, +// and it stays visible in lightbox mode. +test("style.css lays the metadata panel out as a right sidebar", () => { + assert.match(css, /\.image-viewer-body\s*\{[\s\S]*?display:\s*flex/); + const meta = /\.image-meta-panel\s*\{([^}]*)\}/.exec(css); + assert.ok(meta, ".image-meta-panel rule not found"); + assert.match(meta[1], /border-left:/); + assert.doesNotMatch(meta[1], /border-top:/); + // The lightbox hide rule targets the filmstrip only — not the metadata sidebar. + assert.match(css, /\.image-viewer-container\.lightbox \.image-nav-strip\s*\{/); + assert.doesNotMatch(css, /\.image-viewer-container\.lightbox[^{]*\.image-meta-panel/); +}); + // ── Static checks: icon mapping + backend ────────────────────────────────── test("utils.js maps image extensions to the Lucide 'image' icon", () => { assert.match(utils, /"\.png": "image"/);