Files
ai-agent/frontend/src/nav.test.ts
Gabriel Vidal d3ed893d85 feat(nav): the bottom bar is now @gabvdl/ui's BottomNav, with sub-links and swipe
The bar's markup moved into the design system (BottomNav 0.35.4) and
BottomBar became what only this app can know: which sections exist, what
each one routes to, and that tapping Conversation on home brings the
tucked-away composer back instead of navigating nowhere.

What is new for the user:

- a second level. Tap the section you are already in and its pages rise
  from behind the bar — Dashboards (activity, analytics, environment,
  graph), Files (memories, plans, goals, skills, agents, notifs) and
  Settings (shortcuts, tags, cron). Five slots per drawer; the rest wait
  in the hold-to-rearrange stash, and both orders persist server-side
  under the new "nav-sub" list.
- swipe navigation. A left/right swipe on the content area walks the bar
  one section at a time, with the page following the finger; inside an
  expanded section it walks that section's pages and steps out past
  either end. Swipes that start in a horizontal scroller or on the bar
  itself are left alone.

nav.ts grew SUB_SECTIONS (the second level, with the route prefixes each
entry owns) and navKeyOf(), which resolves a path to the finest-grained
bar destination — sectionOf() is unchanged and still owns the sections.

Also the frontend's first test setup: vitest + testing-library + jsdom,
60 tests covering the bar (sections, active route, taps, the composer
tap, drawers, swipes) and the route→section/sub-section mapping.
2026-08-11 21:26:32 +02:00

65 lines
2.2 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { navKeyOf, sectionOf, SUB_SECTIONS } from "./nav";
describe("sectionOf", () => {
it.each([
["/", "conversation"],
["/conversation", "conversation"],
["/conversation/abc.jsonl", "conversation"],
["/conversations", "conversation"],
["/dashboards", "dashboards"],
["/dashboards/activity", "dashboards"],
["/graph", "dashboards"],
["/conversation/dashboard", "dashboards"],
["/files", "files"],
["/_/CLAUDE.md", "files"],
["/memories", "files"],
["/plans/2026-08-11", "files"],
["/goals", "files"],
["/skills", "files"],
["/agents/radar", "files"],
["/notifications", "files"],
["/projects", "projects"],
["/services/traefik", "projects"],
["/templates", "projects"],
["/settings", "settings"],
["/settings/cron", "settings"],
])("puts %s in %s", (path, section) => {
expect(sectionOf(path)).toBe(section);
});
});
describe("navKeyOf", () => {
it("falls back to the section when no sub-link owns the route", () => {
expect(navKeyOf("/")).toBe("conversation");
expect(navKeyOf("/files")).toBe("files");
expect(navKeyOf("/projects")).toBe("projects");
expect(navKeyOf("/dashboards")).toBe("dashboards");
});
it.each([
["/memories", "files/memories"],
["/memories/some-slug", "files/memories"],
["/plans", "files/plans"],
["/goals", "files/goals"],
["/skills", "files/skills"],
["/agents/radar", "files/agents"],
["/notifications", "files/notifs"],
["/dashboards/activity", "dashboards/activity"],
["/dashboards/conversations", "dashboards/conversations"],
["/dashboards/environnement", "dashboards/environnement"],
["/graph", "dashboards/graph"],
["/settings/shortcuts", "settings/shortcuts"],
["/settings/tags", "settings/tags"],
["/settings/cron", "settings/cron"],
])("resolves %s to %s", (path, key) => {
expect(navKeyOf(path)).toBe(key);
});
it("keeps every sub-link key namespaced under its own section", () => {
for (const [section, entries] of Object.entries(SUB_SECTIONS))
for (const e of entries) expect(e.key.startsWith(`${section}/`)).toBe(true);
});
});