Files
siftlode/frontend/src/lib/navPosition.wiring.contract.test.ts
T
peter d44d8d6dda fix(gate): knip sees the watch entry, the build must emit watch.html, and two hand copies are held
- The raw-source contract tests read files from disk (rawSources.testkit)
  instead of import.meta.glob, which knip counted as importing every src file —
  its unused-files lane could never name an orphan. knip now names
  src/watchMain.tsx as an entry (registered in docs/gates.md).
- vite build fails when index.html or watch.html is not emitted.
- The watch-entry import walk follows dynamic import() too.
- audit_metrics.py's plan-code patterns are held equal to the guard's.
- CrashBoundary is domain-free: it moves into components/ui.
2026-10-06 03:19:33 +02:00

55 lines
2.3 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { readSources } from "./rawSources.testkit";
// Every push that opens a new view over the current one remembers the list item it was opened from
// (`lib/history.rememberNavPosition`), so Back hands focus back to that item. A push that
// forgets strands keyboard focus on <body> again with no test noticing — the flow only shows in a real
// browser, after a round trip. So the source is the check: each `history.pushState(` in the app has a
// `rememberNavPosition(` right before it or inside its arguments. The overlay stack's own push is the
// one exception — an overlay entry opens no view, and its list stays mounted underneath.
//
// The assertion pins the SET of offending sites and checks the scan found pushes at all: a guard whose
// glob stops matching is green over nothing.
const files = readSources(__dirname, ["../**/*.ts", "../**/*.tsx", "!../**/*.test.*"]) as Record<
string,
string
>;
const EXEMPT: Record<string, string> = {
"lib/history.ts#_ov": "the overlay stack: an overlay entry opens no view over the page",
};
// Lines before a push, and lines of the push call itself, that may carry the remember call.
const BEFORE = 4;
const WITHIN = 4;
function offenders(): { hits: string[]; pushes: number } {
const hits: string[] = [];
let pushes = 0;
for (const [path, code] of Object.entries(files)) {
// Glob keys are relative to this file: `./x.ts` is lib, `../components/x.tsx` is a sibling folder.
const rel = path.startsWith("./") ? `lib/${path.slice(2)}` : path.replace(/^\.\.\//, "");
const lines = code.split("\n");
lines.forEach((line, i) => {
if (!line.includes("history.pushState(")) return;
pushes++;
const around = lines.slice(Math.max(0, i - BEFORE), i + WITHIN).join("\n");
if (around.includes("rememberNavPosition(")) return;
const exempt = Object.keys(EXEMPT).find(
(id) => id.startsWith(`${rel}#`) && around.includes(id.split("#")[1]!),
);
if (!exempt) hits.push(`${rel}:${i + 1}`);
});
}
return { hits: hits.sort(), pushes };
}
describe("every view push remembers the item it was opened from", () => {
it("finds no pushState without rememberNavPosition", () => {
const { hits, pushes } = offenders();
expect(pushes).toBeGreaterThan(4);
expect(hits).toEqual([]);
});
});