mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 13:52:20 +00:00
fix(visits): open an upcoming appointment where it lives
Visits are the second tab of `/checkups`, and the bare path opens the
first one, the preventive-care list, which never shows an appointment.
Every surface that listed an upcoming appointment linked it there: the
dashboard's next-appointment line, the Today rail item, the Today
overview fact and the appointment's push notification. A tap on the
appointment landed on the list it is not on.
Each now opens the visit itself (`/checkups?visit=<id>`), or the visits
tab (`/checkups?view=visits`) for a line naming several appointments.
The checkups page reads `view` to pick a tab. The rail item stops
borrowing the check-up item's action: it carries its own label
("View appointment") and intent `visit.view`. The appointment push names
its visit through the producer's `metadata.url` override; a check-up's
push keeps the checkups list.
The same mis-route sat on three links that lead to visits without
being upcoming: a condition's visit rows, the address book's back link
and the onboarding and timeline prompts to add a visit.
`visit-link-guard.test.ts` keeps the visit paths in the builder and
counts every bare `/checkups` per file, each with the reason it names
the checkups list.
This commit is contained in:
1 parent
78bacb0d86
commit
0dcafeeb70
29 files changed
+591
-23
No files matched your search
@@ -0,0 +1,169 @@
|
||||
/**
|
||||
* A link to a visit is built by `src/lib/encounters/visit-links.ts`, and a
|
||||
* bare `/checkups` is a link to the checkups, never to a visit.
|
||||
*
|
||||
* Visits are the second tab of `/checkups`. The bare path opens the first
|
||||
* one, the preventive-care list, which never lists an appointment. Four
|
||||
* surfaces showed an upcoming appointment and linked it to the bare path —
|
||||
* the dashboard's next-appointment line, the Today rail item, the Today
|
||||
* overview fact and the appointment's push — so a tap on the appointment
|
||||
* landed on the list it is not on. Each had spelled the path by hand.
|
||||
*
|
||||
* Two rules, both over code lines (comments are skipped):
|
||||
*
|
||||
* 1. The visit and tab query forms (`/checkups?visit=`, `/checkups?view=`)
|
||||
* appear only in the builder.
|
||||
* 2. The bare `/checkups` literal appears only where the target IS the
|
||||
* checkups list, counted per file. A new occurrence fails the run until
|
||||
* someone decides whether it names a checkup (add it here, with the
|
||||
* reason) or a visit (use the builder).
|
||||
*
|
||||
* Not matched, stated rather than hidden: a path assembled from parts
|
||||
* (`"/check" + "ups"`), and a `/*` inside a string on the same line as the
|
||||
* literal, which the comment skipper reads as a comment.
|
||||
*/
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { walkSourceFiles } from "./helpers/source-files";
|
||||
|
||||
const SRC = join(__dirname, "..");
|
||||
const BUILDER = "lib/encounters/visit-links.ts";
|
||||
|
||||
/**
|
||||
* Where a bare `/checkups` is right, and how often: each names the
|
||||
* preventive-care list itself, or lists routes rather than linking one.
|
||||
*/
|
||||
const BARE_CHECKUPS: Record<string, { count: number; why: string }> = {
|
||||
"components/layout/nav-model.ts": { count: 1, why: "the page's nav entry" },
|
||||
"components/measurement-reminders/vorsorge-dashboard-card.tsx": {
|
||||
count: 1,
|
||||
why: "the checkups card's empty state opens the checkups",
|
||||
},
|
||||
"components/illness/illness-episode-detail.tsx": {
|
||||
count: 2,
|
||||
why: "the body-site view (`?site=`); the bare fallback never renders",
|
||||
},
|
||||
"lib/navigation/shared-record.ts": {
|
||||
count: 2,
|
||||
why: "route lists for domain visibility, not links",
|
||||
},
|
||||
"lib/daily/digest.ts": { count: 1, why: "the due check-ups rail item" },
|
||||
"lib/day/records.ts": { count: 1, why: "a check-up satisfied that day" },
|
||||
"lib/onboarding/tour-state.ts": { count: 1, why: "the checkups tour step" },
|
||||
"lib/notifications/tap-target.ts": {
|
||||
count: 1,
|
||||
why: "a checkup push; an appointment's names its visit as override",
|
||||
},
|
||||
};
|
||||
|
||||
/** The code on each line, with line and block comments removed. */
|
||||
function codeLines(source: string): string[] {
|
||||
const out: string[] = [];
|
||||
let inBlock = false;
|
||||
for (const raw of source.split("\n")) {
|
||||
let line = raw;
|
||||
let code = "";
|
||||
while (line.length > 0) {
|
||||
if (inBlock) {
|
||||
const end = line.indexOf("*/");
|
||||
if (end === -1) {
|
||||
line = "";
|
||||
break;
|
||||
}
|
||||
inBlock = false;
|
||||
line = line.slice(end + 2);
|
||||
continue;
|
||||
}
|
||||
const start = line.indexOf("/*");
|
||||
const lineComment = line.search(/(^|[^:])\/\//);
|
||||
if (lineComment !== -1 && (start === -1 || lineComment < start)) {
|
||||
code += line.slice(
|
||||
0,
|
||||
lineComment + (line[lineComment] === "/" ? 0 : 1),
|
||||
);
|
||||
line = "";
|
||||
break;
|
||||
}
|
||||
if (start === -1) {
|
||||
code += line;
|
||||
line = "";
|
||||
break;
|
||||
}
|
||||
code += line.slice(0, start);
|
||||
inBlock = true;
|
||||
line = line.slice(start + 2);
|
||||
}
|
||||
out.push(code);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
const QUERY_FORM = /\/checkups\?(visit|view)=/;
|
||||
// The bare page, or the page with a query the builder does not own.
|
||||
const BARE = /["'`]\/checkups(?:["'`]|\?(?!visit=|view=))/g;
|
||||
|
||||
function scan(files: { path: string; source: string }[]) {
|
||||
const queryForms: string[] = [];
|
||||
const bare: Record<string, number> = {};
|
||||
for (const { path, source } of files) {
|
||||
for (const line of codeLines(source)) {
|
||||
if (QUERY_FORM.test(line)) queryForms.push(path);
|
||||
const hits = line.match(BARE)?.length ?? 0;
|
||||
if (hits > 0) bare[path] = (bare[path] ?? 0) + hits;
|
||||
}
|
||||
}
|
||||
return { queryForms: [...new Set(queryForms)].sort(), bare };
|
||||
}
|
||||
|
||||
describe("links to a visit go through the visit-links builder", () => {
|
||||
const files = walkSourceFiles(SRC, { floor: 2500 })
|
||||
.filter((p) => !p.includes("__tests__/") && !p.startsWith("generated/"))
|
||||
.map((path) => ({ path, source: readFileSync(join(SRC, path), "utf8") }));
|
||||
|
||||
it("walks the tree", () => {
|
||||
expect(files.length).toBeGreaterThan(2000);
|
||||
});
|
||||
|
||||
it("spells the visit and tab paths only in the builder", () => {
|
||||
// The builder itself must match, or the matcher proves nothing.
|
||||
expect(
|
||||
scan(files).queryForms,
|
||||
"Build a link to a visit with visitPath() / visitsPath() / VISITS_PATH from src/lib/encounters/visit-links.ts.",
|
||||
).toEqual([BUILDER]);
|
||||
});
|
||||
|
||||
it("links the bare checkups page only where the target is the checkups", () => {
|
||||
const { bare } = scan(files);
|
||||
delete bare[BUILDER];
|
||||
const expected = Object.fromEntries(
|
||||
Object.entries(BARE_CHECKUPS).map(([path, { count }]) => [path, count]),
|
||||
);
|
||||
expect(
|
||||
bare,
|
||||
"A new bare `/checkups`: if it leads to a visit, use the visit-links builder; if it really is the checkups list, add it to BARE_CHECKUPS with the reason.",
|
||||
).toEqual(expected);
|
||||
});
|
||||
|
||||
it("skips comments and keeps code", () => {
|
||||
const { queryForms, bare } = scan([
|
||||
{
|
||||
path: "a.tsx",
|
||||
source: [
|
||||
"/**",
|
||||
" * `/checkups?visit=<id>` in a docblock",
|
||||
" */",
|
||||
'// "/checkups" in a line comment',
|
||||
'{/* a JSX comment naming "/checkups"',
|
||||
' over two lines "/checkups" */}',
|
||||
'const url = "https://example.test/x"; const bad = "/checkups";',
|
||||
].join("\n"),
|
||||
},
|
||||
{ path: "b.ts", source: "const p = `/checkups?visit=${id}`;" },
|
||||
{ path: "c.ts", source: "const q = `/checkups?${params}`;" },
|
||||
]);
|
||||
expect(queryForms).toEqual(["b.ts"]);
|
||||
expect(bare).toEqual({ "a.tsx": 1, "c.ts": 1 });
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user