feat: onglet de navigation avec facettes, tris et retour Home complet #158
CI / lint (push) Successful in 2m46s
CI / security (push) Successful in 1m57s
CI / build (push) Canceled after 0s
CI / e2e (push) Canceled after 0s
CI / test (push) Canceled after 1h50m30s

- clic sur un repertoire de l'arbre (modes focus vault et All) ouvre un onglet de navigation dedie qui coexiste avec les onglets de fichiers ouverts
- vue: sous-repertoires cliquables, facettes Vaults/Tags/Extensions (meme mecanisme que la recherche), tris Pertinence/Date, bouton Sauver; tags indexes exposes par GET /api/vault/{v}/files
- BUG-100: showWelcome re-injecte la section Raccourcis & Astuces et goHome efface la recherche globale + le filtre de la sidebar
This commit is contained in:
2026-10-02 12:27:27 -04:00
parent 5ffc9d851a
commit a50227849d
26 changed files with 870 additions and 232 deletions
+86
View File
@@ -0,0 +1,86 @@
#!/usr/bin/env node
/**
* ObsiGate — pure tests for the navigation page facets/filters (#158):
* `frontend/js/navfacets.js` (no DOM, real module import).
*
* Usage: node tests/frontend/navfacets.test.mjs
*/
import { strict as assert } from "node:assert";
import { buildNavFacets, filterNavFiles, sortNavFiles, navDisplayName } from "../../frontend/js/navfacets.js";
const files = [
{ name: "note2.md", path: "note2.md", vault: "V1", extension: "md", modified: 100, tags: ["docker"] },
{ name: "note1.md", path: "note1.md", vault: "V1", extension: "md", modified: 300, tags: ["python", "docker"] },
{ name: "photo.png", path: "sub/photo.png", vault: "V1", extension: ".PNG", modified: 200, tags: [] },
{ name: "sans-titre.md", path: "sans-titre.md", vault: "V1", extension: "md", modified: 400 },
];
let pass = 0;
function test(name, fn) {
try {
fn();
pass++;
console.log(` ✓ ${name}`);
} catch (err) {
console.error(` ✗ ${name}\n ${err.message}`);
process.exitCode = 1;
}
}
test("buildNavFacets counts vaults/tags/extensions, sorted desc", () => {
const f = buildNavFacets(files);
assert.deepStrictEqual(f.vaults, { V1: 4 });
assert.deepStrictEqual(f.tags, { docker: 2, python: 1 });
// leading dot stripped + lowercased, untagged file contributes nothing
assert.deepStrictEqual(f.extensions, { md: 3, png: 1 });
});
test("buildNavFacets tolerates empty/missing input", () => {
assert.deepStrictEqual(buildNavFacets([]), { vaults: {}, tags: {}, extensions: {} });
assert.deepStrictEqual(buildNavFacets(undefined), { vaults: {}, tags: {}, extensions: {} });
});
test("filterNavFiles: no filter returns the input untouched", () => {
assert.strictEqual(filterNavFiles(files, {}), files);
assert.strictEqual(filterNavFiles(files, { tag: "", ext: "" }), files);
});
test("filterNavFiles: tag filter keeps files carrying the tag", () => {
const out = filterNavFiles(files, { tag: "docker" });
assert.deepStrictEqual(out.map((f) => f.name).sort(), ["note1.md", "note2.md"]);
});
test("filterNavFiles: extension filter is dot-insensitive", () => {
assert.deepStrictEqual(filterNavFiles(files, { ext: "png" }).map((f) => f.name), ["photo.png"]);
assert.deepStrictEqual(filterNavFiles(files, { ext: ".png" }).map((f) => f.name), ["photo.png"]);
});
test("filterNavFiles: tag + ext combine (AND) and can empty the list", () => {
assert.deepStrictEqual(filterNavFiles(files, { tag: "docker", ext: "md" }).map((f) => f.name).sort(), ["note1.md", "note2.md"]);
assert.deepStrictEqual(filterNavFiles(files, { tag: "python", ext: "png" }), []);
});
test("sortNavFiles 'modified' = newest first", () => {
assert.deepStrictEqual(sortNavFiles(files, "modified").map((f) => f.modified), [400, 300, 200, 100]);
});
test("sortNavFiles 'relevance' = alphabetical, .md stripped, case-insensitive", () => {
const names = sortNavFiles(files, "relevance").map(navDisplayName);
assert.deepStrictEqual(names, ["note1", "note2", "photo.png", "sans-titre"]);
});
test("sortNavFiles does not mutate its input", () => {
const before = files.map((f) => f.name);
sortNavFiles(files, "relevance");
sortNavFiles(files, "modified");
assert.deepStrictEqual(files.map((f) => f.name), before);
});
test("navDisplayName strips only a trailing .md", () => {
assert.strictEqual(navDisplayName({ name: "a.md" }), "a");
assert.strictEqual(navDisplayName({ name: "a.markdown" }), "a.markdown");
assert.strictEqual(navDisplayName({ path: "dir/b.md" }), "b");
});
console.log(`\n${pass} passed`);
+1
View File
@@ -109,6 +109,7 @@ function testModulesHaveImports() {
{ file: 'pane-manager.js', reason: 'standalone — no deps' },
{ file: 'editor-inline.js', reason: 'standalone — DOM-only helpers for the inline editor host (#93)' },
{ file: 'themes.js', reason: 'standalone — no deps' },
{ file: 'navfacets.js', reason: 'pure helpers — no deps, imported by vaulthome.js' },
{ file: 'sidebar_raw.js', reason: 'legacy file — excluded' },
{ file: 'desktop.js', reason: 'standalone — reads window.__TAURI__ global, no imports needed' },
];
+49
View File
@@ -0,0 +1,49 @@
# tests/test_nav_files.py — Navigation page listing (#158)
#
# The vault home / navigation page lists a directory with its indexed tags so
# the Vaults · Tags · Extensions facets can filter the listing client-side.
# The tags must survive the endpoint's response_model.
from __future__ import annotations
def test_vault_files_expose_indexed_tags(client):
"""Every entry carries a `tags` list; markdown tags come from the index."""
resp = client.get("/api/vault/TestVault/files", params={"recursive": False})
assert resp.status_code == 200
data = resp.json()
files = data["files"]
assert files, "expected a non-empty listing"
# The field is always present (response_model must not strip it)
assert all("tags" in f for f in files)
by_name = {f["name"]: f for f in files}
assert "note1.md" in by_name
assert sorted(by_name["note1.md"]["tags"]) == ["python", "tutorial"]
# Unindexed / tagless files keep an empty list, never null
assert sorted(by_name["note2.md"]["tags"]) == ["devops", "docker"]
for f in files:
assert isinstance(f["tags"], list)
def test_vault_files_tags_in_subdirectory(client):
"""Tags are resolved per directory listing, not only at the vault root."""
resp = client.get(
"/api/vault/TestVault/files",
params={"dir": "Projets", "recursive": False},
)
assert resp.status_code == 200
files = resp.json()["files"]
by_name = {f["name"]: f for f in files}
assert "projet.md" in by_name
assert sorted(by_name["projet.md"]["tags"]) == ["projet", "python"]
def test_indexed_tags_unknown_vault_returns_empty():
"""Index miss (unknown vault/file) degrades to an empty tag list."""
from backend.services.vaults import _indexed_tags
assert _indexed_tags("NoSuchVault", "ghost.md") == []