From 2add24a9c1490e375b32a0c97d958be80e9269f0 Mon Sep 17 00:00:00 2001 From: Bruno Charest Date: Fri, 2 Oct 2026 14:04:58 -0400 Subject: [PATCH] feat: onglet Accueil, menus contextuels de navigation et racine vault #158 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - clic sur le titre: la page d'accueil s'ouvre dans un onglet Accueil (icone Maison) epingle en tete de barre, reactivable d'un clic (onglet propre au panneau actif en split) - page de navigation: menus contextuels des repertoires et des fichiers identiques a ceux de la sidebar, clic sur la racine d'un vault qui ouvre la vue, icone dossier a la couleur de l'arbre de navigation - BUG-101: bloc « Keyboard shortcuts for tabs » duplique dans ui.js (un seul Ctrl+W fermait deux onglets) - BUG-102: le menu contextuel se refermait 10 ms apres son ouverture (scroll de reflow des icones lucide) --- CHANGELOG.md | 31 +++++++- 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 | 5 ++ docs/ROADMAP.md | 11 ++- docs/features/navigation-tab-158.md | 23 ++++-- frontend/js/legacy.js | 10 ++- frontend/js/pane-manager.js | 23 ++++++ frontend/js/sidebar.js | 4 +- frontend/js/ui.js | 78 ++++++++++++-------- frontend/js/vaulthome.js | 17 ++++- frontend/locales/en.json | 1 + frontend/locales/fr.json | 1 + frontend/style.css | 5 ++ package.json | 2 +- tests/e2e/nav-tab.spec.js | 106 ++++++++++++++++++++++++++++ tests/e2e/split-view-matrix.spec.js | 28 ++++---- tests/e2e/split-view.spec.js | 17 ++--- 22 files changed, 307 insertions(+), 75 deletions(-) create mode 100644 tests/e2e/nav-tab.spec.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 0041021..79c21ae 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.48.0**. +> [Unreleased](#unreleased). La dernière version livrée est **2.49.0**. --- @@ -14,6 +14,35 @@ et [Semantic Versioning](https://semver.org/spec/v2.0.0.html). --- +## [2.49.0] — 2026-10-02 + +### Ajouté + +- **#158 — onglet Accueil.** Le clic sur le titre ouvre désormais la page d'accueil **dans un + onglet** (icône Maison) systématiquement épinglé **en tête de barre**, réactivable d'un clic + comme un onglet normal (le mode split a son propre onglet Accueil dans le panneau actif). +- **#158 — menus contextuels dans la page de navigation.** Clic droit sur les répertoires et + les fichiers de la vue : même menu que celui de la sidebar (`ContextMenuManager`), et la + ligne d'un vault dans l'arbre ouvre à nouveau la page de navigation (racine du vault). +- **#158 — icône des répertoires alignée sur la sidebar.** La couleur du dossier dans la + section Répertoires est celle de l'arbre (`var(--accent)`). + +### Corrigé + +- **BUG-102 — menu contextuel refermé par son propre affichage.** L'auto-scroll déclenché par + la reflow des icônes (lucide) à l'ouverture du menu arrivait dans le listener `scroll` + (capture) et refermait le menu ~10 ms après son apparition : un décalage de 250 ms est + ignoré, les vrais défilements ferment toujours le menu. +- **BUG-101 — raccourcis d'onglets déclarés deux fois.** Le bloc « Keyboard shortcuts for + tabs » existed en double dans `ui.js` (fin de fichier) : un seul `Ctrl+W` fermait **deux** + onglets (le second refermait celui qui venait d'être ré-activé) et `Ctrl+Tab` sautait un + onglet. Bloc dupliqué supprimé. +- **#158 — clic titre ne repliait plus l'arborescence.** L'effacement du filtre de la sidebar + n'est déclenché que s'il contenait quelque chose (sinon `restoreSidebarTree()` repliait tout + l'arbre à chaque retour à l'accueil). + +--- + ## [2.48.0] — 2026-10-02 ### Ajouté diff --git a/README.fr.md b/README.fr.md index b660f30..2cf06a7 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.48.0-blue.svg)]() +[![Version](https://img.shields.io/badge/Version-2.49.0-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/) @@ -976,8 +976,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.48.0). +Consultez le [CHANGELOG.md](./CHANGELOG.md) pour l'historique complet de toutes les versions (v1.0.0 → v2.49.0). --- -*Projet : ObsiGate | Version : 2.48.0 | Dernière mise à jour : Septembre 2026* +*Projet : ObsiGate | Version : 2.49.0 | Dernière mise à jour : Septembre 2026* diff --git a/README.md b/README.md index f98b197..9bb3d54 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.48.0-blue.svg)]() +[![Version](https://img.shields.io/badge/Version-2.49.0-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/) @@ -1151,8 +1151,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.48.0). +See [CHANGELOG.md](./CHANGELOG.md) for the complete version history (v1.0.0 → v2.49.0). --- -*Project: ObsiGate | Version: 2.48.0 | Last updated: September 2026* +*Project: ObsiGate | Version: 2.49.0 | Last updated: September 2026* diff --git a/VERSION b/VERSION index 9a9feb0..132775c 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.48.0 +2.49.0 diff --git a/desktop/Cargo.lock b/desktop/Cargo.lock index 21054a1..e8ae733 100644 --- a/desktop/Cargo.lock +++ b/desktop/Cargo.lock @@ -2626,7 +2626,7 @@ dependencies = [ [[package]] name = "obsigate-desktop" -version = "2.48.0" +version = "2.49.0" dependencies = [ "chrono", "env_logger", diff --git a/desktop/Cargo.toml b/desktop/Cargo.toml index f5ac513..d7a80dc 100644 --- a/desktop/Cargo.toml +++ b/desktop/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "obsigate-desktop" -version = "2.48.0" +version = "2.49.0" 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 9e5e119..7c08ca9 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.48.0", + "version": "2.49.0", "identifier": "com.obsigate.desktop", "build": { "frontendDist": "../frontend", diff --git a/docs/ISSUES_TODOLIST.md b/docs/ISSUES_TODOLIST.md index ae10cc7..54b8920 100644 --- a/docs/ISSUES_TODOLIST.md +++ b/docs/ISSUES_TODOLIST.md @@ -208,6 +208,8 @@ Avant de corriger quoi que ce soit, un agent IA doit : | *BUG-098* | [🟡 IMPORTANT] Délimiteur CSV figé `,` : un `.csv` français (`;`) s'affiche en une seule colonne et se réécrit dans un autre format | 🟢 corrigé | P2 | ⚙️ backend | IA | `backend/xlsx_reader.py` (`render_csv_table`, `delimiter=","` par défaut), `backend/routers/files_read.py:551`, `backend/services/mutations.py` (`save_csv_cells`) | Ouvrir un CSV `;` (export Excel FR) dans ObsiGate : une seule colonne ; éditer une cellule puis enregistrer : le fichier est réécrit en `,` | `backend/xlsx_reader.py` (`sniff_csv_delimiter`) + `backend/services/mutations.py::save_csv_cells` : délimiteur détecté et **réutilisé**. Tests `TestCsvDelimiter` + lecture/écriture/guillemets point-virgule | Défaut #156 A3. `render_csv_table(raw)` est appelé sans délimiteur et `save_csv_cells()` re-parse en `,`, alors que l'export CSV de la visionneuse écrit en `;`. Traitement prévu : détection `;`/`,`/tab partagée lecture/écriture/export. Analyse : `docs/features/xlsx-editor-completeness.md` | | *BUG-099* | [🔵 MINEUR] Sonde de perte plafonnée (8 Mo, budget global) : risque de perte **silencieuse** des valeurs calculées en cache au-delà du budget | 🟢 corrigé | P2 | ⚙️ backend | IA | `backend/xlsx_reader.py` (`_MAX_PROBE_BYTES`, `_has_cached_formulas`, `inspect_workbook`) | Constituer un classeur dont le XML de feuille dépasse 8 Mo **avant** la première formule cachée, puis l'éditer : la garde 409 `xlsx_lossy_content` ne se déclenche pas | `backend/xlsx_reader.py` : budget **par feuille** (4 Mo) + plafond global (32 Mo) ; `_scan_cached_formulas()` renvoie `(found, unverified)` et `inspect_workbook()` ajoute `cached_values_unverified` (libellé i18n FR/EN). Contre-preuve : budget épuisé → signalé au lieu de `[]`. Tests `TestXlsxCachedValueProbe` (3) | Défaut #156 A4, **analyse statique (non reproduit)**. Le budget est partagé entre toutes les feuilles : au-delà, `cached_values` n'est pas détecté et l'écriture détruit ces valeurs sans avertissement (risque n°1 de #153). Traitement prévu : budget par feuille + signal d'incertitude pour que la garde reste prudente. Analyse : `docs/features/xlsx-editor-completeness.md` | | *BUG-100* | Retour à l'accueil incomplet : le clic sur le titre perd la section « Raccourcis & Astuces » et laisse la recherche globale ainsi que le filtre de la sidebar actifs | 🟢 corrigé | P1 | 📱 frontend | IA | `frontend/js/viewer.js` (`showWelcome`), `frontend/js/legacy.js` (`goHome`) | Ouvrir un fichier (le dashboard est détruit), puis cliquer le titre ObsiGate | `viewer.js` : `#quick-help` capturé au chargement du module et réinjecté dans le template reconstruit de `showWelcome()` ; `legacy.js::goHome` clique `#search-clear-btn` (chips + barre de résultats) et `#sidebar-filter-clear-btn` avant d'afficher l'accueil | **Cause racine :** `showWelcome()` reconstruit `#dashboard-home` depuis un template qui n'embarque pas le bloc `#quick-help` présent dans `index.html` — il ne disparaissait qu'après la première reconstruction (ouverture de fichier, puis retour). Livré avec #158 A3 | +| *BUG-101* | Raccourcis d'onglets déclarés deux fois : un seul `Ctrl+W` fermait deux onglets (le second fermait celui ré-activé par la fermeture précédente) et `Ctrl+Tab` sautait un onglet | 🟢 corrigé | P1 | 📱 frontend | IA | `frontend/js/ui.js` (bloc « Keyboard shortcuts for tabs » dupliqué en fin de fichier) | Ouvrir deux onglets puis `Ctrl+W` une fois | Suppression du second bloc `document.addEventListener("keydown", …)` dupliqué (fin de `ui.js`) — un seul listener reste | Exposé par #158 : le second onglet n'était plus jamais vide auparavant (la 2ᵉ fermeture tombait sur `_activeTabId === null` et devenait sans effet). Vérifié : repro Playwright (`closes: ["fichier", "nav"]` avant → `["fichier"]` après) + suite E2E | +| *BUG-102* | Menu contextuel qui se referme ~10 ms après son ouverture : la reflow des icônes (remplacement `` → `` par lucide) déclenche un événement `scroll` capturé par `ContextMenuManager` qui referme le menu | 🟢 corrigé | P2 | 📱 frontend | IA | `frontend/js/ui.js` (`ContextMenuManager.init/show`) | Clic droit sur un fichier de la page de navigation (ou de l'arbre) juste après un rendu d'icônes | `show()` mémorise `_shownAt` ; le listener `scroll` (capture) ignore un déclenchement dans les 250 ms suivant l'ouverture | Exposé par #158 A5 (test E2E `nav-tab.spec.js` : menu résolu « hidden » alors que 6 items étaient construits). Vérifié : instrumentation Playwright (display block conservé après le scroll) + suite E2E | | | | | | | | | | | | ### TODOs techniques (améliorations / nouvelles tâches) @@ -311,6 +313,9 @@ Avant de corriger quoi que ce soit, un agent IA doit : | 2026-09-30 | #156 A8-A14 (P2 + P3) — clôture du backlog | Fonctionnalité (sans nouveau défaut) | `backend/services/mutations.py`, `backend/routers/files_read.py`, `backend/routers/files_write.py`, `backend/schemas.py`, `backend/xlsx_reader.py`, `backend/openapi_docs.py`, `backend/tools/{spreadsheets,schemas,labels}.py`, `frontend/js/viewer.js`, `frontend/js/xlsx/command-bar.js`, `frontend/locales/{fr,en}.json`, `frontend/style.css`, `tests/test_xlsx_styles.py`, `tests/test_xlsx_viewer.py`, `tests/test_spreadsheet_tools.py`, `tests/test_xlsx_formats.py`, `tests/frontend/xlsx-viewer.test.mjs`, `tests/e2e/xlsx-viewer.spec.js`, `CHANGELOG.md`, `docs/ROADMAP.md`, `docs/features/xlsx-editor-completeness.md`, `docs/GUIDES/RECHERCHE_PDF_EXCALIDRAW.md`, `README.md`, `README.fr.md` | **P2 + P3 livrés, backlog #156 clôturé** — (A8) **mise en forme en écriture** : bouton Mise en forme (gras/italique/souligné, alignements, couleurs via sélecteur natif, formats de nombre, fusion/défusion, volets figés, largeur/hauteur) et nouvelle route `PUT …/xlsx/style` (`mutate_xlsx_style` : verrou, backup, swap atomique, garde de perte, `If-Match`) ; (A9) décision « pas de moteur de formule » **annoncée dans l'UI** ; (A10) undo/redo unifié avec piles par fichier conservées au re-rendu ; (A11) export de la sélection + Markdown + HTML + impression et recherche sur **toutes** les feuilles (`n/m · k feuilles`) ; (A12) concurrence optimiste `If-Match` → **409** `conflict`/`stale_revision`, retry qui relit ; (A13) cache de `read_workbook_meta()` par `(chemin, mtime_ns, taille)` (LRU 8) ; (A14) outils IA `.xlsm`/`.csv` + `search_workbook`, `analyze_range`, `edit_xlsx_structure` (confirmation conservée). Vérifié : suite **1525 passed / 6 skipped**, ruff 0, mypy 0 (102 fichiers), JSDOM `xlsx-viewer.test.mjs` 107/107, validate-imports + unit, i18n parity 2355/2355, E2E `xlsx` 10/10 + `Split View` 37 + `XSS` 2. | ✅ livré | | 2026-09-29 | #156 (audit), BUG-096 → BUG-099 | Enregistrement (audit statique + 1 repro JSDOM) | `docs/ROADMAP.md`, `docs/features/xlsx-editor-completeness.md` (nouveau), `docs/ISSUES_TODOLIST.md`, `CHANGELOG.md` | **Audit de complétude de l'éditeur Excel → ouverture de l'item #156** (P0 défauts · P1 presse-papiers/clavier · P2 mise en forme/calcul/undo · P3 sortie/robustesse/perf) avec fiche dédiée. **4 défauts** enregistrés : BUG-096 (enregistrement `.csv` en `TypeError`, **reproduit en JSDOM** — `Cannot read properties of undefined (reading 'name')`, aucun `PUT …/csv/save`), BUG-097 (lazy-load `.xlsm` → 415), BUG-098 (délimiteur CSV `,` figé vs export `;`), BUG-099 (sonde de perte plafonnée à 8 Mo, risque théorique). Manques fonctionnels recensés : presse-papiers de plage, clavier complet, zone Nom éditable, mise en forme en écriture, calcul, undo/redo unifié, export/impression, concurrence optimiste, cache des métadonnées, outils IA `.xlsm`/`.csv`. **Aucun code modifié** (documentation seule). | 🔴 ouvert (à traiter) | | 2026-10-02 | #158 (A1-A3), BUG-100 | Fonctionnalité + correction | `frontend/js/navfacets.js` (nouveau), `frontend/js/vaulthome.js`, `frontend/js/ui.js`, `frontend/js/sidebar.js`, `frontend/js/viewer.js`, `frontend/js/legacy.js`, `frontend/style.css`, `frontend/locales/{fr,en}.json`, `backend/services/vaults.py`, `backend/schemas.py`, `tests/test_nav_files.py` (nouveau), `tests/frontend/navfacets.test.mjs` (nouveau), `tests/frontend/unit.test.mjs`, `CHANGELOG.md`, `docs/ROADMAP.md`, `docs/ISSUES_TODOLIST.md` | **A1** : `TabManager.openNav(vault, dir)` — un onglet de navigation par vault, ouvert à chaque clic répertoire de l'arbre (mode focus et mode All) sans fermer les onglets de fichiers ; la ligne vault reste un expandeur. **A2** : la vue liste les sous-répertoires cliquables et embarque le panneau de facettes **Vaults · Tags · Extensions** (mêmes classes que la page de recherche), les tris **Pertinence / Date** et le bouton **Sauver** ; `GET /api/vault/{v}/files` expose désormais les `tags` indexés (facettes calculées côté client). **A3 / BUG-100** : `showWelcome()` réinjecte `#quick-help` et `goHome()` efface recherche globale + filtre sidebar. Tests : `test_nav_files.py` 3 passed, `navfacets.test.mjs` 10/10, `unit.test.mjs` 12/12, `validate-imports` 41 modules, ruff/mypy 0. | 🟢 corrigé (en attente vérif utilisateur) | +| 2026-10-02 | #158 (A4-A6) | Fonctionnalité | `frontend/js/vaulthome.js`, `frontend/js/ui.js`, `frontend/js/pane-manager.js`, `frontend/js/legacy.js`, `frontend/js/sidebar.js`, `frontend/style.css`, `frontend/locales/{fr,en}.json`, `tests/e2e/nav-tab.spec.js` (nouveau), `CHANGELOG.md`, `docs/ROADMAP.md`, `docs/features/navigation-tab-158.md` | **A4** : `TabManager.openHome()` — le clic sur le titre ouvre la page d'accueil dans un **onglet Accueil** (icône Maison, `common.home` FR/EN) remonté en position 0 à chaque fois ; `deactivate()` conserve cet onglet actif, `pane-manager.js` a son propre `openHome` pour le split. **A5** : menus contextuels répertoires **et** fichiers de la page de navigation via `ContextMenuManager.show()` (avec `stopPropagation()`, le handler global referme sinon le menu) — listes vérifiées identiques à la sidebar ; la ligne d'un vault rouvre la page de navigation. **A6** : icône des répertoires à `var(--accent)` (couleur de l'arbre). **Correctif annexé** : l'effacement du filtre sidebar au retour à l'accueil n'est déclenché que s'il était actif (sinon `restoreSidebarTree()` repliait l'arbre). Vérifié : Playwright jetable 16/16, `nav-tab.spec.js` 4 tests, validate-imports/unit/i18n parity verts. | 🟢 corrigé (en attente vérif utilisateur) | +| 2026-10-02 | BUG-101 | Correction | `frontend/js/ui.js` | **BUG-101 — le bloc « Keyboard shortcuts for tabs » existait en double dans `ui.js`** (fin de fichier) : un seul `Ctrl+W` déclenchait **deux** `close()`, le second refermant l'onglet ré-activé par le premier — observé avec #158 quand un onglet navigation survit à la fermeture du fichier ; `Ctrl+Tab` sautait également un onglet. Bloc dupliqué supprimé (un seul listener). Vérifié : repro Playwright (`close()` appelé 2× puis 1×) et suite E2E. | 🟢 corrigé (en attente vérif utilisateur) | +| 2026-10-02 | BUG-102 | Correction | `frontend/js/ui.js` | **BUG-102 — menu contextuel refermé par sa propre ouverture** : la reflow des icônes lucide à l'affichage déclenche un `scroll` capturé qui refermait le menu ~10 ms après son apparition (constaté sur les fichiers de la page de navigation, #158 A5). `show()` mémorise `_shownAt` et le listener `scroll` ignore un déclenchement dans les 250 ms. Vérifié : instrumentation Playwright + suite E2E (`nav-tab.spec.js`). | 🟢 corrigé (en attente vérif utilisateur) | --- diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 086f116..507d1ae 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -1,6 +1,6 @@ # ObsiGate — Roadmap -> **Version :** 2.48.0 | **Dernière mise à jour :** 2026-10-02 +> **Version :** 2.49.0 | **Dernière mise à jour :** 2026-10-02 > **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)** @@ -161,6 +161,13 @@ - [x] **A3 — retour Home complet.** Clic sur le titre = page d'accueil d'un refresh (section **Raccourcis & Astuces** restaurée) + effacement de la recherche globale et de la barre de filtre de la sidebar gauche. + - [x] **A4 — onglet Accueil.** Le clic sur le titre affiche la page d'accueil dans un onglet + (icône Maison) épinglé **en tête de barre**, réactivable à tout moment. + - [x] **A5 — menus contextuels de la page de navigation.** Clic droit identique à la sidebar + sur les répertoires **et** les fichiers de la vue ; la ligne d'un vault dans l'arbre + ouvre la page de navigation (racine du vault). + - [x] **A6 — icône des répertoires** de la section Répertoires à la couleur de l'arbre + (`var(--accent)`). --- @@ -332,7 +339,7 @@ | 117 | Configuration — avatars prédéfinis dans le profil utilisateur (12 images) | 2.25.0 | [features/viewer-toolbar-highlight-avatars.md](./features/viewer-toolbar-highlight-avatars.md) | | 85 | Refonte architecturale — découpage du monolithe (14 routers, `main.py` 4 827 → ~750 lignes), stores JSON verrouillés, rate-limit SQLite optionnel | 2.27.2→2.27.13 | [features/archi-refonte-85.md](./features/archi-refonte-85.md) | | 157 | Recherche — facette « Extensions » dans les résultats (3ᵉ filtre avec Vaults et Tags) + panneau repliable | 2.46.0→2.47.0 | [archive](./archive/COMPLETED_v1-v2.md) | -| 158 | Navigation — clic répertoire → **onglet de navigation** (sous-répertoires, facettes Vaults/Tags/Extensions, tri Pertinence/Date, Sauver) + retour Home complet (**BUG-100**) | 2.48.0 | [features/navigation-tab-158.md](./features/navigation-tab-158.md) | +| 158 | Navigation — clic répertoire → **onglet de navigation** (sous-répertoires, facettes Vaults/Tags/Extensions, tri Pertinence/Date, Sauver), **onglet Accueil**, menus contextuels + retour Home complet (**BUG-100** → **BUG-102**) | 2.48.0→2.49.0 | [features/navigation-tab-158.md](./features/navigation-tab-158.md) | --- diff --git a/docs/features/navigation-tab-158.md b/docs/features/navigation-tab-158.md index 89f1f79..1a3786f 100644 --- a/docs/features/navigation-tab-158.md +++ b/docs/features/navigation-tab-158.md @@ -23,11 +23,20 @@ ne reste « actif », donc un clic suivant ré-éffectue réellement l'onglet (a et vue figée). Points d'entrée des clics (`frontend/js/sidebar.js`) : clic **répertoire** dans l'arbre (2 sites, -mode focus et mode All), changement de contexte vault (sélecteur), facettes/filtres et fil -d'Ariane de la vue (`vaulthome.js` route tout par `openNav`, plus aucune délégation sur -l'arbre). La **ligne d'un vault** garde son rôle d'expansion seul : un onglet de navigation à -l'ouverture d'un vault casserait les compteurs d'onglets et « fermer le dernier onglet → -dashboard » (13 tests E2E `split-view*`). +mode focus et mode All), clic sur la **racine d'un vault** (2 sites, expansion + navigation), +changement de contexte vault (sélecteur), facettes/filtres et fil d'Ariane de la vue +(`vaulthome.js` route tout par `openNav`, plus aucune délégation sur l'arbre). + +**Menus contextuels** : les répertoires et les fichiers de la vue appellent le même +`ContextMenuManager.show()` que la sidebar, avec `stopPropagation()` — le handler global de +`ContextMenuManager` referme le menu sur tout clic droit hors `.tree-item`. + +**Onglet Accueil** (`ui.js::openHome` + `pane-manager.js::openHome`) : le clic sur le titre +crée (ou remonte en position 0) l'onglet `home` — icône Maison, libellé i18n `common.home` — +et l'active ; `activate()` rend la page d'accueil via `showWelcome()` et `deactivate()` +conserve l'onglet Accueil actif (il *est* la page d'accueil). En mode split, l'onglet vit dans +le panneau actif (délégation `getActiveTabManager()`, libellé transmis par l'appelant pour ne +pas d'importer `t` dans `pane-manager.js`, qui déclare déjà des variables locales `t`). **Contenu de la vue** (`vaulthome.js`) : fil d'Ariane · barre **Pertinence / Date** + **Sauver** · panneau de facettes · **Répertoires** cliquables (`GET /api/browse`) · Récents (racine) · @@ -57,3 +66,7 @@ repli `obsigate_facets_collapsed`, filtrage **local** (pas de nouvelle requête) - `tests/frontend/navfacets.test.mjs` (10) : comptages, filtres tag/ext (insensible au point), combinaison ET, tris, non-mutation. - `tests/frontend/unit.test.mjs` : `navfacets.js` ajouté à la liste des modules purs. +- `tests/e2e/nav-tab.spec.js` (4) : racine vault → page de navigation, icône + menus + contextuels identiques à la sidebar, onglet Accueil en tête de barre (avec `#quick-help`). +- Vérification manuelle Playwright (jetable) : 22/22 puis 16/16 checks sur les lots A1-A3 et + A4-A6. diff --git a/frontend/js/legacy.js b/frontend/js/legacy.js index 6ee47df..37f4dba 100644 --- a/frontend/js/legacy.js +++ b/frontend/js/legacy.js @@ -292,8 +292,9 @@ function goHome() { // bar, filters) and the sidebar filter bar before showing the dashboard. const searchClear = document.getElementById("search-clear-btn"); if (searchClear) searchClear.click(); + const sideInput = document.getElementById("sidebar-filter-input"); const sideClear = document.getElementById("sidebar-filter-clear-btn"); - if (sideClear) sideClear.click(); + if (sideClear && sideInput && sideInput.value) sideClear.click(); const searchInput = document.getElementById("search-input"); if (searchInput) searchInput.value = ""; @@ -306,7 +307,12 @@ function goHome() { state.cachedRawSource = null; closeMobileSidebar(); - showWelcome(); + // #158 — la page d'accueil s'ouvre dans un onglet épinglé en tête de barre + if (window.TabManager && window.TabManager.openHome) { + window.TabManager.openHome(); + } else { + showWelcome(); + } } // ========================================================================= diff --git a/frontend/js/pane-manager.js b/frontend/js/pane-manager.js index ef9bea7..96638e8 100644 --- a/frontend/js/pane-manager.js +++ b/frontend/js/pane-manager.js @@ -105,6 +105,20 @@ function createPaneTabManager(paneId) { this.activate(tabId); }, + async openHome(name = "Home") { + const tabId = "home"; + const idx = this._tabs.findIndex((x) => x.id === tabId); + if (idx > 0) { + const [tab] = this._tabs.splice(idx, 1); + this._tabs.unshift(tab); + } else if (idx === -1) { + this._tabs.unshift({ id: tabId, vault: null, path: "", name, icon: "home", home: true }); + this._tabCache[tabId] = { home: true, vault: null, path: "", title: name, data: null, rawSource: null, sourceView: false, scrollTop: 0, icon: "home" }; + } + this._renderTabs(); + this.activate(tabId); + }, + async openPersistent(vault, path) { const tabId = `${vault}::${path}`; const previewTab = this._tabs.find(t => t.id === tabId && t.preview); @@ -166,6 +180,15 @@ function createPaneTabManager(paneId) { const cache = this._tabCache[tabId]; if (!cache) return; + // Onglet Accueil (#158) — la page d'accueil vit dans l'onglet + if (cache.home) { + state.currentVault = null; + state.currentPath = null; + this._showDashboard(); + if (history.pushState) history.pushState(null, '', '#'); + return; + } + state.currentVault = cache.vault; state.currentPath = cache.path; // syncActiveFileTreeItem is imported below to avoid circular deps diff --git a/frontend/js/sidebar.js b/frontend/js/sidebar.js index e34fc65..7033bee 100644 --- a/frontend/js/sidebar.js +++ b/frontend/js/sidebar.js @@ -144,7 +144,7 @@ async function refreshSidebarForContext() { vaultsToShow.forEach((v) => { const vaultItem = el("div", { class: "tree-item vault-item", "data-vault": v.name }, [icon("chevron-right", 14), getVaultIcon(v.name, 16), el("span", { class: "tree-item-text" }, [document.createTextNode(v.name)]), smallBadge(v.file_count)]); - vaultItem.addEventListener("click", () => toggleVault(vaultItem, v.name)); + vaultItem.addEventListener("click", () => { toggleVault(vaultItem, v.name); TabManager.openNav(v.name, ""); }); vaultItem.addEventListener("contextmenu", (e) => { e.preventDefault(); @@ -241,7 +241,7 @@ async function loadVaults() { vaults.forEach((v) => { // Sidebar tree entry const vaultItem = el("div", { class: "tree-item vault-item", "data-vault": v.name }, [icon("chevron-right", 14), getVaultIcon(v.name, 16), el("span", { class: "tree-item-text" }, [document.createTextNode(v.name)]), smallBadge(v.file_count)]); - vaultItem.addEventListener("click", () => toggleVault(vaultItem, v.name)); + vaultItem.addEventListener("click", () => { toggleVault(vaultItem, v.name); TabManager.openNav(v.name, ""); }); vaultItem.addEventListener("contextmenu", (e) => { e.preventDefault(); diff --git a/frontend/js/ui.js b/frontend/js/ui.js index 35b3183..023166e 100644 --- a/frontend/js/ui.js +++ b/frontend/js/ui.js @@ -1776,7 +1776,12 @@ export const ContextMenuManager = { if (e.key === 'Escape') this.hide(); }); - document.addEventListener('scroll', () => this.hide(), true); + document.addEventListener('scroll', () => { + // Un scroll déclenché juste après l'ouverture vient de la reflow des + // icônes (lucide) : on ne referme pas le menu qui vient d'apparaître. + if (performance.now() - (this._shownAt || 0) < 250) return; + this.hide(); + }, true); }, show(x, y, vault, path, type, isReadonly) { @@ -1822,6 +1827,7 @@ export const ContextMenuManager = { } this._menu.classList.add('active'); + this._shownAt = performance.now(); const rect = this._menu.getBoundingClientRect(); const viewportWidth = window.innerWidth; @@ -2133,9 +2139,38 @@ export const TabManager = { this.activate(tabId); }, + /** Onglet Accueil (#158) : la page d'accueil vit dans un onglet épinglé tout + * à gauche de la barre. Recréé (et remonté en position 0) à chaque clic sur + * le titre. */ + async openHome() { + // En split, l'onglet appartient au panneau actif + if (window.getActiveTabManager && window.PaneManager && window.PaneManager.isSplit()) { + return window.getActiveTabManager().openHome(t("common.home")); + } + const tabId = "home"; + const idx = this._tabs.findIndex(t => t.id === tabId); + if (idx > 0) { + const [tab] = this._tabs.splice(idx, 1); + this._tabs.unshift(tab); // toujours en tête de barre + } else if (idx === -1) { + const name = t("common.home"); + this._tabs.unshift({ id: tabId, vault: null, path: "", name, icon: "home", home: true }); + this._tabCache[tabId] = { home: true, vault: null, path: "", title: name, data: null, rawSource: null, sourceView: false, scrollTop: 0, icon: "home" }; + } + this._renderTabs(); + this.activate(tabId); + }, + /** Drop the active-tab marker — the dashboard/home page is showing. - * Keeps the tabs open; clicking one re-activates (and re-renders) it. */ + * Keeps the tabs open; clicking one re-activates (and re-renders) it. + * L'onglet Accueil, lui, reste l'onglet actif quand la page d'accueil + * s'affiche (il EST la page d'accueil). */ deactivate() { + const active = this._activeTabId && this._tabCache[this._activeTabId]; + if (active && active.home) { + this._renderTabs(); + return; + } this._activeTabId = null; this._renderTabs(); }, @@ -2160,6 +2195,17 @@ export const TabManager = { const cache = this._tabCache[tabId]; if (!cache) return; + // Onglet Accueil (#158) — la page d'accueil vit dans l'onglet + if (cache.home) { + state.currentVault = null; + state.currentPath = null; + showWelcome(); // conserve l'onglet Accueil actif (deactivate()) + if (history.pushState) { + history.pushState(null, "", "#"); + } + return; + } + // Navigation tab — render the directory view instead of fetching a file if (cache.nav) { state.currentVault = cache.vault; @@ -2556,32 +2602,4 @@ export const TabManager = { -// ---- Keyboard shortcuts for tabs ---- -document.addEventListener("keydown", (e) => { - if (e.ctrlKey || e.metaKey) { - if (e.key === "w" || e.key === "W") { - e.preventDefault(); - if (TabManager._activeTabId) { - TabManager.close(TabManager._activeTabId); - } - } else if (e.key === "Tab" && !e.shiftKey) { - e.preventDefault(); - const tabs = TabManager._tabs; - const currentIdx = tabs.findIndex(t => t.id === TabManager._activeTabId); - if (currentIdx >= 0 && tabs.length > 1) { - const nextIdx = (currentIdx + 1) % tabs.length; - TabManager.activate(tabs[nextIdx].id); - } - } else if (e.key === "Tab" && e.shiftKey) { - e.preventDefault(); - const tabs = TabManager._tabs; - const currentIdx = tabs.findIndex(t => t.id === TabManager._activeTabId); - if (currentIdx >= 0 && tabs.length > 1) { - const prevIdx = (currentIdx - 1 + tabs.length) % tabs.length; - TabManager.activate(tabs[prevIdx].id); - } - } - } -}); - // ---- Modify init to include TabManager ---- diff --git a/frontend/js/vaulthome.js b/frontend/js/vaulthome.js index 198a7df..a37bcdb 100644 --- a/frontend/js/vaulthome.js +++ b/frontend/js/vaulthome.js @@ -19,7 +19,7 @@ import { api } from './auth.js'; import { el } from './viewer.js'; import { getFileIcon, safeCreateIcons } from './utils.js'; -import { TabManager } from './ui.js'; +import { TabManager, ContextMenuManager } from './ui.js'; import { t, getLocale } from './i18n.js'; import { buildNavFacets, filterNavFiles, sortNavFiles, navDisplayName } from './navfacets.js'; @@ -394,11 +394,18 @@ function renderResults() { const list = el('div', { class: 'vault-home-recent-list' }); state.dirs.forEach((d) => { const item = el('div', { class: 'vault-home-recent-item', 'data-path': d.path }); + // Même couleur d'icône que l'arbre de la sidebar (.tree-item .icon) item.innerHTML = ` - + ${escapeHtml(d.name)} (${d.children_count})`; item.addEventListener('click', () => TabManager.openNav(state.currentVault, d.path)); + // Menu contextuel identique à celui de la sidebar (#158) + item.addEventListener('contextmenu', (e) => { + e.preventDefault(); + e.stopPropagation(); // sinon le handler global de ContextMenuManager referme le menu + ContextMenuManager.show(e.clientX, e.clientY, state.currentVault, d.path, 'directory', false); + }); list.appendChild(item); }); section.appendChild(list); @@ -474,6 +481,12 @@ function buildFileItem(f, locale) { e.preventDefault(); TabManager.openPersistent(f.vault, f.path); }); + // Menu contextuel identique à celui de la sidebar (#158) + item.addEventListener('contextmenu', (e) => { + e.preventDefault(); + e.stopPropagation(); + ContextMenuManager.show(e.clientX, e.clientY, f.vault, f.path, 'file', false); + }); return item; } diff --git a/frontend/locales/en.json b/frontend/locales/en.json index 9e51ed2..ee2777f 100644 --- a/frontend/locales/en.json +++ b/frontend/locales/en.json @@ -344,6 +344,7 @@ "common.previous": "Previous", "common.refresh": "Refresh", "common.rename": "Rename", + "common.home": "Home", "common.root": "(root)", "common.save": "Save", "common.search": "Search", diff --git a/frontend/locales/fr.json b/frontend/locales/fr.json index b8f9cc5..d47e77f 100644 --- a/frontend/locales/fr.json +++ b/frontend/locales/fr.json @@ -344,6 +344,7 @@ "common.previous": "Précédent", "common.refresh": "Rafraîchir", "common.rename": "Renommer", + "common.home": "Accueil", "common.root": "(racine)", "common.save": "Enregistrer", "common.search": "Rechercher", diff --git a/frontend/style.css b/frontend/style.css index 75e869f..4e96eb2 100644 --- a/frontend/style.css +++ b/frontend/style.css @@ -5905,6 +5905,11 @@ body.resizing-v { cursor: pointer; } +/* Icône des répertoires : même couleur que l'arbre de la sidebar (.tree-item .icon) */ +.vault-home-dirs .icon { + color: var(--accent); +} + .vault-home-item:hover { background: var(--bg-secondary); } diff --git a/package.json b/package.json index 1695f55..73d1130 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "obsigate", - "version": "2.48.0", + "version": "2.49.0", "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/nav-tab.spec.js b/tests/e2e/nav-tab.spec.js new file mode 100644 index 0000000..2cb3957 --- /dev/null +++ b/tests/e2e/nav-tab.spec.js @@ -0,0 +1,106 @@ +/** + * E2E — Page de navigation & onglet Accueil (#158). + * + * Couvre : + * - clic sur la racine d'un vault -> page de navigation (onglet dédié) + * - icône des répertoires à la couleur de l'arbre de la sidebar + * - menu contextuel des répertoires et des fichiers identique à la sidebar + * - clic sur le titre -> onglet Accueil (icône Maison) épinglé en tête de barre + */ + +import { test, expect } from '@playwright/test'; + +async function boot(page) { + await page.goto('/'); + await page.waitForFunction(() => window.__OBSIGATE_BOOTED === true, { timeout: 20000 }); + await page.waitForSelector('#dashboard-home, #search-input', { timeout: 15000 }); +} + +const menuItems = (page) => + page.$$eval('#context-menu .context-menu-item', (els) => els.map((e) => e.textContent.trim())) + .catch(() => []); + +test.describe('Page de navigation (#158)', () => { + test.beforeEach(async ({ page }) => { + await boot(page); + }); + + test("clic sur la racine d'un vault ouvre la page de navigation", async ({ page }) => { + const vault = await page.locator('.vault-item').first().getAttribute('data-vault'); + await page.locator('.vault-item').first().click(); + + await page.waitForSelector('#vault-home .vault-home-breadcrumb', { timeout: 15000 }); + await expect(page.locator(`#tab-list .tab-item[data-tab-id="nav::${vault}"]`)).toHaveCount(1); + // Le fil d'Ariane est à la racine du vault + await expect(page.locator('#vault-home .vault-home-breadcrumb-item')).toHaveCount(1); + }); + + test("répertoires de la vue : icône à la couleur de la sidebar + menu contextuel", async ({ page }) => { + await page.locator('.vault-item').first().click(); + await page.waitForSelector('#vault-home .vault-home-dirs .vault-home-recent-item', { timeout: 15000 }); + + // Couleur identique à l'icône de l'arbre (.tree-item .icon -> var(--accent)) + const colors = await page.evaluate(() => { + const cs = (el) => (el ? getComputedStyle(el).color : null); + return { + nav: cs(document.querySelector('#vault-home .vault-home-dirs .vault-home-recent-item svg')), + side: cs(document.querySelector('.tree-item[data-type="directory"] svg')), + }; + }); + expect(colors.nav).toBe(colors.side); + expect(colors.nav).toBeTruthy(); + + // Menu contextuel : page de navigation vs sidebar + await page.locator('#vault-home .vault-home-dirs .vault-home-recent-item').first().click({ button: 'right' }); + await page.waitForSelector('#context-menu', { timeout: 5000 }); + const navMenu = await menuItems(page); + expect(navMenu.length).toBeGreaterThan(0); + await page.keyboard.press('Escape'); + + await page.locator('.tree-item[data-type="directory"]').first().click({ button: 'right' }); + await page.waitForSelector('#context-menu', { timeout: 5000 }); + const sideMenu = await menuItems(page); + await page.keyboard.press('Escape'); + expect(navMenu).toEqual(sideMenu); + }); + + test('fichiers de la vue : menu contextuel identique à la sidebar', async ({ page }) => { + await page.locator('.vault-item').first().click(); + await page.waitForSelector('#vault-home .search-result-item', { timeout: 15000 }); + + await page.locator('#vault-home .search-result-item').first().click({ button: 'right' }); + await page.waitForSelector('#context-menu', { timeout: 5000 }); + const navMenu = await menuItems(page); + expect(navMenu.length).toBeGreaterThan(0); + await page.keyboard.press('Escape'); + + await page.locator('.tree-item[data-type="file"]').first().click({ button: 'right' }); + await page.waitForSelector('#context-menu', { timeout: 5000 }); + const sideMenu = await menuItems(page); + await page.keyboard.press('Escape'); + expect(navMenu).toEqual(sideMenu); + }); + + test('titre -> onglet Accueil (Maison) épinglé en tête de barre', async ({ page }) => { + // Un fichier ouvert pour vérifier que l'onglet Accueil passe devant + await page.locator('.vault-item').first().click(); + await page.waitForSelector('.tree-item[data-type="file"]', { timeout: 15000 }); + await page.locator('.tree-item[data-type="file"]').first().dblclick(); + await page.waitForTimeout(500); + + await page.locator('#header-logo').click(); + await page.waitForSelector('#dashboard-home', { timeout: 10000 }); + + const ids = await page.$$eval('#tab-list .tab-item', (els) => els.map((e) => e.dataset.tabId)); + expect(ids[0]).toBe('home'); + expect(ids.length).toBeGreaterThan(1); + await expect(page.locator('#tab-list .tab-item.active')).toHaveAttribute('data-tab-id', 'home'); + // Icône Maison + libellé traduit + const iconClass = await page.locator('#tab-list .tab-item').first().locator('.tab-icon').getAttribute('class'); + expect(iconClass).toContain('home'); + const label = (await page.locator('#tab-list .tab-item').first().locator('.tab-name').textContent()) || ''; + expect(['Accueil', 'Home']).toContain(label.trim()); + // La page d'accueil est affichée (avec sa section Raccourcis & Astuces) + await expect(page.locator('#dashboard-home #quick-help')).toHaveCount(1); + }); +}); diff --git a/tests/e2e/split-view-matrix.spec.js b/tests/e2e/split-view-matrix.spec.js index 3fb7f0c..238af60 100644 --- a/tests/e2e/split-view-matrix.spec.js +++ b/tests/e2e/split-view-matrix.spec.js @@ -160,8 +160,9 @@ test.describe('Split View Matrix — Boutons & menu contextuel', () => { await flyout.click(); await page.waitForTimeout(600); // Pane 0 (single) holds both files now; empty source pane auto-collapsed + // (+ onglet navigation ouvert au clic sur la racine du vault — #158) await expect(page.locator('.pane-container')).toHaveCount(0); - await expect(page.locator('#tab-list .tab-item')).toHaveCount(2); + await expect(page.locator('#tab-list .tab-item')).toHaveCount(3); }); }); @@ -205,10 +206,10 @@ test.describe('Split View Matrix — Fermeture', () => { test('X button on tab closes it', async ({ page }) => { await openFile(page, 'TestVault', 'note1.md'); await openFile(page, 'TestVault', 'note2.md'); - await expect(page.locator('.tab-item')).toHaveCount(2); + await expect(page.locator('.tab-item')).toHaveCount(3); // + onglet navigation (#158) await page.locator('.tab-item').first().locator('.tab-close').click(); await page.waitForTimeout(300); - await expect(page.locator('.tab-item')).toHaveCount(1); + await expect(page.locator('.tab-item')).toHaveCount(2); }); test('double-click on tab closes it', async ({ page }) => { @@ -216,12 +217,15 @@ test.describe('Split View Matrix — Fermeture', () => { await openFile(page, 'TestVault', 'note2.md'); await page.locator('.tab-item').first().dblclick(); await page.waitForTimeout(300); - await expect(page.locator('.tab-item')).toHaveCount(1); + await expect(page.locator('.tab-item')).toHaveCount(2); // reste : onglet navigation (#158) }); test('Ctrl+W closes active tab (single pane)', async ({ page }) => { - await openFile(page, 'TestVault', 'note1.md'); - await page.keyboard.press('Control+w'); + await openFile(page, 'TestVault', 'note1.md'); // ouvre aussi l'onglet navigation (#158) + await page.keyboard.press('Control+w'); // ferme l'onglet fichier + await page.waitForTimeout(400); + await expect(page.locator('.tab-item')).toHaveCount(1); + await page.keyboard.press('Control+w'); // ferme l'onglet navigation await page.waitForTimeout(400); await expect(page.locator('.tab-item')).toHaveCount(0); await expect(page.locator('#dashboard-home')).toBeVisible(); @@ -233,7 +237,7 @@ test.describe('Split View Matrix — Fermeture', () => { await page.locator('.tab-item').first().click({ button: 'right' }); await page.locator('#tab-context-menu [data-action="close"]').click(); await page.waitForTimeout(300); - await expect(page.locator('.tab-item')).toHaveCount(1); + await expect(page.locator('.tab-item')).toHaveCount(2); // reste navigation + fichier (#158) }); test('context menu Fermer les autres keeps one', async ({ page }) => { @@ -243,7 +247,7 @@ test.describe('Split View Matrix — Fermeture', () => { await openFile(page, 'TestVault', 'Accueil.md'); await page.waitForTimeout(300); const tabs = page.locator('.tab-item'); - await expect(tabs).toHaveCount(3); + await expect(tabs).toHaveCount(5); // accueil + navigation + 3 fichiers (#158) await tabs.nth(1).click({ button: 'right' }); await page.locator('#tab-context-menu [data-action="closeOthers"]').click(); await page.waitForTimeout(300); @@ -256,7 +260,7 @@ test.describe('Split View Matrix — Fermeture', () => { await page.locator('#header-logo').click(); await page.waitForTimeout(300); await openFile(page, 'TestVault', 'Accueil.md'); await page.waitForTimeout(300); - await expect(page.locator('.tab-item')).toHaveCount(3); + await expect(page.locator('.tab-item')).toHaveCount(5); // accueil + navigation + 3 fichiers (#158) await page.locator('.tab-item').first().click({ button: 'right' }); await page.locator('#tab-context-menu [data-action="closeRight"]').click(); await page.waitForTimeout(300); @@ -286,8 +290,8 @@ test.describe('Split View Matrix — Fermeture', () => { await pane1.locator('.tab-item').first().locator('.tab-close').click({ force: true }); await page.waitForTimeout(700); await expect(page.locator('.pane-container')).toHaveCount(0); - // pane 0 file is still open in single mode - await expect(page.locator('#tab-list .tab-item')).toHaveCount(1); + // pane 0 file + onglet navigation still open in single mode (#158) + await expect(page.locator('#tab-list .tab-item')).toHaveCount(2); }); test('closing pane 0 of 2 focuses neighbour then collapses correctly', async ({ page }) => { @@ -376,7 +380,7 @@ test.describe('Split View Matrix — Edge cases', () => { // dblclick note1 again await page.locator('.tree-item[data-path="note1.md"]').dblclick(); await page.waitForTimeout(400); - await expect(page.locator('.tab-item')).toHaveCount(2); + await expect(page.locator('.tab-item')).toHaveCount(3); // + onglet navigation (#158) // it should be the active one const active = page.locator('.tab-item.active'); await expect(active).toHaveCount(1); diff --git a/tests/e2e/split-view.spec.js b/tests/e2e/split-view.spec.js index c6b7437..ee0ecce 100644 --- a/tests/e2e/split-view.spec.js +++ b/tests/e2e/split-view.spec.js @@ -79,7 +79,7 @@ test.describe('Split View — Basic Operations', () => { const tabBar = page.locator('#tab-bar'); await expect(tabBar).toBeVisible(); const tabs = page.locator('.tab-item'); - await expect(tabs).toHaveCount(1); + await expect(tabs).toHaveCount(2); // navigation (racine vault) + fichier (#158) // Content area should show the file const content = page.locator('#content-area'); await expect(content).not.toBeEmpty(); @@ -91,7 +91,7 @@ test.describe('Split View — Basic Operations', () => { await page.waitForTimeout(300); await openFile(page, 'TestVault', 'note2.md'); const tabs = page.locator('.tab-item'); - await expect(tabs).toHaveCount(2); + await expect(tabs).toHaveCount(4); // accueil + navigation + 2 fichiers (#158) }); test('split right via keyboard shortcut', async ({ page }) => { @@ -192,7 +192,7 @@ test.describe('Split View — Tab Drag & Drop', () => { // Source pane should have 1 tab, target should have 1 tab const pane0Tabs = page.locator('.pane-container').nth(0).locator('.tab-item'); const pane1Tabs = page.locator('.pane-container').nth(1).locator('.tab-item'); - await expect(pane0Tabs).toHaveCount(1); + await expect(pane0Tabs).toHaveCount(2); // navigation + fichier (#158) await expect(pane1Tabs).toHaveCount(1); }); @@ -343,12 +343,13 @@ test.describe('Split View — Regression', () => { }); test('closing last tab shows dashboard', async ({ page }) => { - await openFile(page, 'TestVault', 'note1.md'); + await openFile(page, 'TestVault', 'note1.md'); // ouvre aussi l'onglet navigation (#158) - // Close tab via middle-click - const tab = page.locator('.tab-item').first(); - await tab.click({ button: 'middle' }); - await page.waitForTimeout(500); + // Close tabs via middle-click until none remains (dernier onglet -> dashboard) + while (await page.locator('.tab-item').count()) { + await page.locator('.tab-item').first().click({ button: 'middle' }); + await page.waitForTimeout(300); + } // Dashboard should be visible const dashboard = page.locator('#dashboard-home');