Files
jobtrackingapp/job-tracker-ui/src/sidebar-active-nav.test.ts
T
cesnimda 4f69d395be fix(nav): only the most-specific sidebar item is active
On /career/builder/{id} both "Career Workspace" (/career) and "CV Builder"
(/career/builder) highlighted, because AppShell tested each item with
`pathname === to || pathname.startsWith(to + "/")` — so /career matched
every /career/... child. No "most specific wins" rule.

Add AppShell.activeNavTo(pathname, tos): the longest `to` that the path is
at or under wins, across both nav lists; every other item is inactive. A
child route never lights up a parent nav item. `selected` now compares
against that single computed activeTo. Exported as a pure function so the
ownership rule is unit-tested directly (sidebar-active-nav.test.ts):
exactly one active item for /career, /career/builder and
/career/builder/{id}, and no double-highlight.

Also give the breadcrumb/title in App.tsx explicit /career/builder ->
"CV Builder" ownership (it previously showed "Career Workspace"), and
reframe the Career Workspace header to the "Career Profile" product
framing: "This information powers your CVs, applications, cover letters
and AI assistance."

Frontend only — no change to CareerProfiles, CvVariants, CV generation,
extraction APIs, AI, permissions or tenant isolation. Plan for the deeper
information-architecture work is in docs/career-workspace-ux-refactor.md,
staged so the 1376-line CareerProfilePage and the live CV/extraction
pipeline are refactored incrementally with verification, not in one risky
rewrite.

Verified: tsc clean, frontend build clean, 135 frontend tests pass
(128 + 7 new nav tests). Backend untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 02:29:50 +02:00

44 lines
1.7 KiB
TypeScript

import { activeNavTo } from "./layout/AppShell";
// Route ownership under test (the real sidebar entries):
// /career -> Career Workspace
// /career/builder -> CV Builder
// A child route must activate exactly one nav item — the most specific owner.
const TOS = ["/dashboard", "/jobs", "/career", "/career/builder", "/settings"];
describe("sidebar active nav ownership", () => {
test("/career activates Career Workspace only", () => {
expect(activeNavTo("/career", TOS)).toBe("/career");
});
test("/career/builder activates CV Builder only, not Career Workspace", () => {
expect(activeNavTo("/career/builder", TOS)).toBe("/career/builder");
});
test("/career/builder/{id} activates CV Builder only (the reported bug)", () => {
// Previously /career matched via startsWith('/career/') AND /career/builder matched — both lit up.
expect(activeNavTo("/career/builder/42", TOS)).toBe("/career/builder");
});
test("a plain child of /career (not /builder) still belongs to Career Workspace", () => {
expect(activeNavTo("/career/anything-else", TOS)).toBe("/career");
});
test("unrelated routes are unaffected and exact matches win", () => {
expect(activeNavTo("/jobs", TOS)).toBe("/jobs");
expect(activeNavTo("/settings/connected-accounts", TOS)).toBe("/settings");
});
test("a path owned by no nav item activates nothing", () => {
expect(activeNavTo("/admin/system", TOS)).toBeNull();
});
test("exactly one item is ever active (no double highlight)", () => {
for (const path of ["/career", "/career/builder", "/career/builder/7", "/jobs"]) {
const active = activeNavTo(path, TOS);
const matches = TOS.filter((t) => t === active);
expect(matches).toHaveLength(1);
}
});
});