Both headers carried their own literal list of {label, path}. That made the
route contract and the header two sources for the same three facts — which
routes are navigable, what they are called, and in what order — with nothing
keeping them in step: a renamed route or a reordered menu could be right in one
place and stale in the other.
techLogNavigation(layoutGroup) derives the menu from TECH_LOG_ROUTE_REGISTRY,
where navigationOrder is what makes a route navigable. Output is byte-identical
to the previous literal lists, pinned by a new test.
Derived from TechLog's own route contract rather than from the composed
registries: .dependency-cruiser.json freezes an exact allowlist of files that
may read src/features/installed-*, explicitly so that coupling cannot spread,
and a feature header is not on it.
The studio header keeps its active-state rules — "작업본" stays highlighted
across /studio/documents/* except on the new-document screen — because that is
presentation behaviour the contract has no opinion about.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
61 lines
2.1 KiB
TypeScript
61 lines
2.1 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import { techLogNavigation } from "../../../src/features/tech-log/presentation/tech-log-navigation.ts";
|
|
import { TECH_LOG_ROUTE_REGISTRY } from "../../../src/features/tech-log/contracts/tech-log-route-contract.ts";
|
|
|
|
/**
|
|
* The headers used to repeat the route contract's navigation facts in their own
|
|
* literal arrays, so a renamed route or a reordered menu could be right in one
|
|
* place and stale in the other. These pin the derived result against the shape
|
|
* the headers rendered before the change, and against the contract itself.
|
|
*/
|
|
describe("TechLog navigation derivation", () => {
|
|
it("derives the public menu in contract order", () => {
|
|
expect(
|
|
techLogNavigation("PUBLIC").map((entry) => [entry.label, entry.path]),
|
|
).toEqual([
|
|
["탐색", "/explore"],
|
|
["프로젝트", "/projects"],
|
|
["변경 기록", "/releases"],
|
|
["프로필", "/profile"],
|
|
]);
|
|
});
|
|
|
|
it("derives the studio menu in contract order", () => {
|
|
expect(
|
|
techLogNavigation("STUDIO").map((entry) => [entry.label, entry.path]),
|
|
).toEqual([
|
|
["작업본", "/studio/documents"],
|
|
["게시 기록", "/studio/publications"],
|
|
["새 문서", "/studio/documents/new"],
|
|
]);
|
|
});
|
|
|
|
it("advertises exactly the routes the contract marks navigable", () => {
|
|
const navigable = Object.values(TECH_LOG_ROUTE_REGISTRY)
|
|
.filter((definition) => definition.navigationOrder !== null)
|
|
.map((definition) => definition.routeId)
|
|
.sort();
|
|
const derived = [
|
|
...techLogNavigation("PUBLIC"),
|
|
...techLogNavigation("STUDIO"),
|
|
]
|
|
.map((entry) => entry.routeId)
|
|
.sort();
|
|
expect(derived).toEqual(navigable);
|
|
});
|
|
|
|
it("keeps each shell's entries inside its own layout group", () => {
|
|
for (const group of ["PUBLIC", "STUDIO"] as const) {
|
|
for (const entry of techLogNavigation(group)) {
|
|
expect(
|
|
TECH_LOG_ROUTE_REGISTRY[
|
|
entry.routeId as keyof typeof TECH_LOG_ROUTE_REGISTRY
|
|
]?.layoutGroup,
|
|
entry.routeId,
|
|
).toBe(group);
|
|
}
|
|
}
|
|
});
|
|
});
|