mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 13:22:14 +00:00
Maximizing the Coach drawer hands the conversation to /coach and carries ?doc= with it, so browser-Back can rebuild the sheet. That is the one close-shaped transition whose whole point is to LEAVE the selection where it is, and the close path has always returned early for it. The dropped-traversal check added beside that close did not know. It saw a closed sheet and a selection still in the address bar, which is exactly the state it exists to repair, and it repaired away the return URL a quarter of a second after the close had deliberately preserved it. Whether it won that race was a question about how fast the machine navigated, so it surfaced as one flaky run on the narrow viewport rather than as a broken feature. Both sides read one named predicate now, and the check is skipped when the close kept the selection on purpose. Proved by driving the grace window to zero, which is the worst case the timer can have: before, the maximize never even reached /coach, three attempts out of three; after, it passes there and the whole documents family passes with it. Twenty consecutive runs of that family on the narrow viewport are clean.
298 lines
9.1 KiB
TypeScript
298 lines
9.1 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import {
|
|
closeDocumentSelectionHistoryEntry,
|
|
closeDocumentSelectionAfterCoachHandoff,
|
|
coachHandoffRetainsSelection,
|
|
documentSelectionHistoryState,
|
|
documentSelectionHref,
|
|
documentSelectionSurvivedClose,
|
|
withoutDocumentSelectionHref,
|
|
} from "../documents-view";
|
|
|
|
class MemoryHistory {
|
|
private entries: { href: string; state: unknown }[];
|
|
private index: number;
|
|
|
|
constructor(entries: string[]) {
|
|
this.entries = entries.map((href) => ({ href, state: null }));
|
|
this.index = entries.length - 1;
|
|
}
|
|
|
|
get href() {
|
|
return this.entries[this.index]?.href;
|
|
}
|
|
|
|
get state() {
|
|
return this.entries[this.index]?.state;
|
|
}
|
|
|
|
pushState(state: unknown, _unused: string, href: string | URL) {
|
|
this.entries.splice(this.index + 1, Infinity, {
|
|
href: href.toString(),
|
|
state,
|
|
});
|
|
this.index += 1;
|
|
}
|
|
|
|
replaceState(state: unknown, _unused: string, href?: string | URL | null) {
|
|
this.entries[this.index] = {
|
|
href: href?.toString() ?? this.href!,
|
|
state,
|
|
};
|
|
}
|
|
|
|
back() {
|
|
if (this.index > 0) this.index -= 1;
|
|
}
|
|
}
|
|
|
|
describe("document vault URL selection", () => {
|
|
it("adds an ordinary card selection without dropping unrelated query state", () => {
|
|
expect(
|
|
documentSelectionHref(
|
|
"/documents",
|
|
"episode=episode-1&view=compact&q=MRT",
|
|
"doc_123",
|
|
),
|
|
).toBe("/documents?episode=episode-1&view=compact&q=MRT&doc=doc_123");
|
|
});
|
|
|
|
it("replaces an existing selection instead of appending a duplicate", () => {
|
|
expect(
|
|
documentSelectionHref(
|
|
"/documents",
|
|
"episode=episode-1&doc=old-document",
|
|
"new-document",
|
|
),
|
|
).toBe("/documents?episode=episode-1&doc=new-document");
|
|
});
|
|
|
|
it.each(["", "../coach", "doc?id=other", "doc id", "doc\n", "a".repeat(41)])(
|
|
"refuses to write an unsafe document id: %j",
|
|
(documentId) => {
|
|
expect(
|
|
documentSelectionHref(
|
|
"/documents",
|
|
"episode=episode-1&view=compact",
|
|
documentId,
|
|
),
|
|
).toBeNull();
|
|
},
|
|
);
|
|
|
|
it("removes only the document selection on close", () => {
|
|
expect(
|
|
withoutDocumentSelectionHref(
|
|
"/documents",
|
|
"episode=episode-1&doc=doc_123&view=compact&q=MRT",
|
|
),
|
|
).toBe("/documents?episode=episode-1&view=compact&q=MRT");
|
|
});
|
|
|
|
it("returns the pathname when doc was the only query parameter", () => {
|
|
expect(withoutDocumentSelectionHref("/documents", "doc=doc_123")).toBe(
|
|
"/documents",
|
|
);
|
|
});
|
|
|
|
it("consumes a card-open entry so the next Back reaches the prior distinct page", () => {
|
|
const history = new MemoryHistory(["/today", "/documents?episode=ep-1"]);
|
|
const openHref = documentSelectionHref(
|
|
"/documents",
|
|
"episode=ep-1",
|
|
"doc-1",
|
|
);
|
|
expect(openHref).not.toBeNull();
|
|
history.pushState(documentSelectionHistoryState("doc-1"), "", openHref!);
|
|
|
|
closeDocumentSelectionHistoryEntry(
|
|
history,
|
|
"/documents",
|
|
"episode=ep-1&doc=doc-1",
|
|
"doc-1",
|
|
);
|
|
|
|
expect(history.href).toBe("/documents?episode=ep-1");
|
|
history.back();
|
|
expect(history.href).toBe("/today");
|
|
});
|
|
|
|
it("consumes a dismissed Coach handoff and closes its card-owned detail entry", () => {
|
|
const history = new MemoryHistory(["/today", "/documents?episode=ep-1"]);
|
|
history.pushState(
|
|
documentSelectionHistoryState("doc-1"),
|
|
"",
|
|
"/documents?episode=ep-1&doc=doc-1",
|
|
);
|
|
|
|
const retainedHandoff = closeDocumentSelectionAfterCoachHandoff(
|
|
history,
|
|
"/documents",
|
|
"episode=ep-1&doc=doc-1",
|
|
"doc-1",
|
|
"doc-1",
|
|
"dismiss",
|
|
);
|
|
|
|
expect(retainedHandoff).toBeNull();
|
|
expect(history.href).toBe("/documents?episode=ep-1");
|
|
});
|
|
|
|
it("retains a navigating Coach handoff for maximize and browser Back", () => {
|
|
const history = new MemoryHistory(["/today", "/documents?episode=ep-1"]);
|
|
history.pushState(
|
|
documentSelectionHistoryState("doc-1"),
|
|
"",
|
|
"/documents?episode=ep-1&doc=doc-1",
|
|
);
|
|
|
|
const retainedHandoff = closeDocumentSelectionAfterCoachHandoff(
|
|
history,
|
|
"/documents",
|
|
"episode=ep-1&doc=doc-1",
|
|
"doc-1",
|
|
"doc-1",
|
|
"navigate",
|
|
);
|
|
|
|
expect(retainedHandoff).toBe("doc-1");
|
|
expect(history.href).toBe("/documents?episode=ep-1&doc=doc-1");
|
|
});
|
|
|
|
it("replaces a direct deep-link selection without navigating away", () => {
|
|
const history = new MemoryHistory([
|
|
"/today",
|
|
"/documents?episode=ep-1&doc=doc-1",
|
|
]);
|
|
history.replaceState({ __NA: true }, "", history.href);
|
|
closeDocumentSelectionHistoryEntry(
|
|
history,
|
|
"/documents",
|
|
"episode=ep-1&doc=doc-1",
|
|
"doc-1",
|
|
);
|
|
|
|
expect(history.href).toBe("/documents?episode=ep-1");
|
|
});
|
|
|
|
// ── The dropped traversal ────────────────────────────────────────────────
|
|
//
|
|
// `history.back()` is a request. A traversal asked for while another is
|
|
// still settling can be dropped, and the browser says nothing: the sheet
|
|
// closes, React stays consistent, and the address bar keeps naming a
|
|
// document nobody has open. Reproduced in the browser suite as a closed
|
|
// sheet whose `?doc=` never cleared; this is the predicate the component
|
|
// uses to notice it.
|
|
|
|
it("reports a selection that survived its own close", () => {
|
|
expect(
|
|
documentSelectionSurvivedClose("?episode=ep-1&doc=doc-1", "doc-1"),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("reports nothing to do once the traversal landed", () => {
|
|
expect(documentSelectionSurvivedClose("?episode=ep-1", "doc-1")).toBe(
|
|
false,
|
|
);
|
|
});
|
|
|
|
it("leaves a DIFFERENT selection alone", () => {
|
|
// The traversal landed somewhere that carries its own selection. Stripping
|
|
// that would close a sheet this close had nothing to do with.
|
|
expect(
|
|
documentSelectionSurvivedClose("?episode=ep-1&doc=doc-2", "doc-1"),
|
|
).toBe(false);
|
|
});
|
|
|
|
it("strips only the selection when the fallback runs", () => {
|
|
// What the component does with a true verdict, on the URL shape that
|
|
// produced it: every unrelated parameter survives.
|
|
expect(
|
|
withoutDocumentSelectionHref(
|
|
"/documents",
|
|
"episode=ep-1&view=compact&q=MRT&doc=doc-1",
|
|
),
|
|
).toBe("/documents?episode=ep-1&view=compact&q=MRT");
|
|
});
|
|
|
|
// ── The close and the check that follows it must agree ───────────────────
|
|
//
|
|
// Maximizing the Coach drawer is the one close-shaped transition that keeps
|
|
// `?doc=`: it is the return URL that lets browser-Back reconstruct the
|
|
// sheet. The dropped-traversal check added beside the close does not know
|
|
// that on its own — it sees a closed sheet and a selection still in the
|
|
// address bar, which is exactly the state it was written to repair, and it
|
|
// would repair away the way back a few hundred milliseconds later.
|
|
//
|
|
// Whether it won that race was a question about how fast the machine
|
|
// navigated to `/coach`, which is why it surfaced as one flaky mobile run.
|
|
// Both sides read the same predicate now, and these cases are what stop them
|
|
// drifting apart again.
|
|
|
|
it("keeps the selection when the drawer is being maximized", () => {
|
|
expect(coachHandoffRetainsSelection("doc-1", "doc-1", "navigate")).toBe(
|
|
true,
|
|
);
|
|
});
|
|
|
|
it("does not keep it for a dismissed drawer", () => {
|
|
expect(coachHandoffRetainsSelection("doc-1", "doc-1", "dismiss")).toBe(
|
|
false,
|
|
);
|
|
});
|
|
|
|
it("does not keep it for an ordinary close with no handoff", () => {
|
|
expect(coachHandoffRetainsSelection("doc-1", null, undefined)).toBe(false);
|
|
});
|
|
|
|
it("does not keep it when the handoff names a different document", () => {
|
|
expect(coachHandoffRetainsSelection("doc-1", "doc-2", "navigate")).toBe(
|
|
false,
|
|
);
|
|
});
|
|
|
|
it("agrees with what the close actually did, in both directions", () => {
|
|
// The property that matters, stated against the close itself rather than
|
|
// against the predicate alone: retained means the URL still carries the
|
|
// selection, and not-retained means it does not.
|
|
const retained = new MemoryHistory(["/today", "/documents?episode=ep-1"]);
|
|
retained.pushState(
|
|
documentSelectionHistoryState("doc-1"),
|
|
"",
|
|
"/documents?episode=ep-1&doc=doc-1",
|
|
);
|
|
closeDocumentSelectionAfterCoachHandoff(
|
|
retained,
|
|
"/documents",
|
|
"episode=ep-1&doc=doc-1",
|
|
"doc-1",
|
|
"doc-1",
|
|
"navigate",
|
|
);
|
|
expect(coachHandoffRetainsSelection("doc-1", "doc-1", "navigate")).toBe(
|
|
true,
|
|
);
|
|
expect(retained.href).toBe("/documents?episode=ep-1&doc=doc-1");
|
|
|
|
const closed = new MemoryHistory(["/today", "/documents?episode=ep-1"]);
|
|
closed.pushState(
|
|
documentSelectionHistoryState("doc-1"),
|
|
"",
|
|
"/documents?episode=ep-1&doc=doc-1",
|
|
);
|
|
closeDocumentSelectionAfterCoachHandoff(
|
|
closed,
|
|
"/documents",
|
|
"episode=ep-1&doc=doc-1",
|
|
"doc-1",
|
|
"doc-1",
|
|
"dismiss",
|
|
);
|
|
expect(coachHandoffRetainsSelection("doc-1", "doc-1", "dismiss")).toBe(
|
|
false,
|
|
);
|
|
expect(closed.href).toBe("/documents?episode=ep-1");
|
|
});
|
|
});
|