mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 13:01:00 +00:00
Give the today-just-in spec an account that earns a hero
The spec watched the day flip from provisional to final on a hero that only its own subject was keeping on screen: the arrival marker was the last thing in the calm-degrade guard holding the hero up for the bare fixture account, so removing the arrival chip took the hero with it and left the spec nothing to observe. Putting the arrival back into the guard would bring back a worse problem. An arrival-only digest lands in the compact all-clear composition, which would tell someone with no readings at all that everything is clear - a claim about their health made from nothing, and made only on the days something happened to sync. The guard stays as it is; the fixture was what was wrong. So the spec now seeds its own precondition: one integration needing a reconnect, which is an ordinary Today for anyone whose readings come from a device. It is read fresh on every request rather than through the dashboard snapshot cache, so unlike a seeded score or briefing it cannot lose a race with a cached snapshot, and the rail item admits no module gate and no dismiss path. The row is removed again afterwards. While the spec was open: it now pins the state it starts from. It only ever asserted that the pending-sleep note was absent at the end, which a note that never rendered also satisfies. It asserts the note is up and the day reads provisional first, so the two halves are one assertion with the number flipped. The component side is pinned in unit tests, since the spec needs a built app and a database to run.
This commit is contained in:
1 parent
99ceb236de
commit
96c10f07e6
4 files changed
+157
-19
No files matched your search
@@ -114,6 +114,84 @@ describe("TodayHero — an arrival is not a chip", () => {
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* What the `today-just-in` e2e spec stands on.
|
||||
*
|
||||
* That spec watches an open dashboard flip from provisional to final when
|
||||
* last night's sleep lands. It can only watch it on a hero that exists,
|
||||
* and until the chip was removed the ARRIVAL was what kept the hero alive
|
||||
* on the bare fixture account — so the spec was quietly testing the day
|
||||
* flip on a hero that only its own subject was propping up. It now seeds
|
||||
* a rail item instead, which is the same shape as a real account with a
|
||||
* connection needing attention.
|
||||
*
|
||||
* These cases are the local half of that: the browser proves the flip on
|
||||
* a real page, and this proves the composition the fixture relies on is
|
||||
* the one the component actually renders. Without them the e2e change
|
||||
* would be a guess about a file it never touches.
|
||||
*/
|
||||
describe("TodayHero — what earns the hero without a score", () => {
|
||||
const SYNC_ISSUE: DailyDigest["worthALook"][number] = {
|
||||
kind: "sync_issue",
|
||||
title: "A connection needs your attention",
|
||||
body: "One of your integrations stopped syncing.",
|
||||
status: "warning",
|
||||
actions: [
|
||||
{
|
||||
labelKey: "daily.action.reconnect",
|
||||
intent: "sync.reconnect",
|
||||
href: "/settings/integrations",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
/** No score, no briefing, no reaction line: one rail item and a night. */
|
||||
function itemOnly(over: Partial<DailyDigest> = {}): DailyDigest {
|
||||
return digest({
|
||||
score: null,
|
||||
briefingLead: null,
|
||||
reactionLine: null,
|
||||
phase: "provisional",
|
||||
sleepPending: true,
|
||||
worthALook: [SYNC_ISSUE],
|
||||
justIn: { kind: "sleep_night", at: ARRIVED_AT },
|
||||
...over,
|
||||
});
|
||||
}
|
||||
|
||||
it("renders on a rail item alone, and names the day it is reading", () => {
|
||||
const html = render(<TodayHero digest={itemOnly()} />);
|
||||
|
||||
expect(html).toContain('data-slot="today-hero"');
|
||||
expect(html).toContain('data-phase="provisional"');
|
||||
expect(html).toContain('data-slot="today-hero-rail"');
|
||||
// The note the flip is going to clear. Asserting it is HERE first is
|
||||
// what stops the "it is gone afterwards" assertion from passing on a
|
||||
// note that was never rendered at all.
|
||||
expect(html).toContain('data-slot="today-hero-sleep-pending"');
|
||||
expect(html).not.toContain('data-slot="today-hero-just-in"');
|
||||
});
|
||||
|
||||
it("clears the pending note and reads final once the night is in", () => {
|
||||
const html = render(
|
||||
<TodayHero digest={itemOnly({ phase: "final", sleepPending: false })} />,
|
||||
);
|
||||
|
||||
expect(html).toContain('data-phase="final"');
|
||||
expect(html).not.toContain('data-slot="today-hero-sleep-pending"');
|
||||
expect(html).not.toContain('data-slot="today-hero-just-in"');
|
||||
});
|
||||
|
||||
it("has nothing to render once the item goes too", () => {
|
||||
// The mirror, and the reason the e2e fixture has to seed something:
|
||||
// strip the item and the same digest paints nothing, arrival and
|
||||
// pending night included.
|
||||
const html = render(<TodayHero digest={itemOnly({ worthALook: [] })} />);
|
||||
|
||||
expect(html).not.toContain('data-slot="today-hero"');
|
||||
});
|
||||
});
|
||||
|
||||
describe("TodayHero — the reaction line replaces the lead", () => {
|
||||
const REACTION = "A solid night, deeper than your recent stretch.";
|
||||
|
||||
|
||||
@@ -92,11 +92,11 @@ export function TodayHero({
|
||||
renderFilteredAllClear?: boolean;
|
||||
}) {
|
||||
const { t } = useTranslations();
|
||||
// Gates ONLY the chip's time string — never the chip's presence. The hero
|
||||
// itself is deliberately NOT mount-gated (v1.30.9: it is the LCP element and
|
||||
// paints from the server-dehydrated digest on both the SSR and the hydration
|
||||
// render). So the slot renders on both passes and the formatted time fills in
|
||||
// on the first client re-render, keeping those two passes byte-identical.
|
||||
// The hero is deliberately NOT mount-gated (v1.30.9: it is the LCP element
|
||||
// and paints from the server-dehydrated digest on both the SSR and the
|
||||
// hydration render), so nothing in it may differ between those two passes.
|
||||
// The one thing that did — the removed arrival chip's wall-clock time — was
|
||||
// the reason a mount gate lived here at all.
|
||||
const { keep, letGo } = useCoachCheckinAction();
|
||||
const dismissItem = usePriorityItemDismiss();
|
||||
|
||||
@@ -147,13 +147,20 @@ export function TodayHero({
|
||||
const compactAllClear = !lead && !topSignal && !hasItems;
|
||||
|
||||
// Calm degrade (plan §3): a genuinely empty account — no score, no rail
|
||||
// items, no cached briefing lead, and no fresh arrival — surfaces nothing
|
||||
// items, no cached briefing lead and no reaction line — surfaces nothing
|
||||
// here. The tile strip below carries its own "add your first reading" empty
|
||||
// state, so a second alarming empty card on the hero would be noise. The
|
||||
// all-clear state (score present, nothing needs attention) is handled inline
|
||||
// below. A fresh marker counts as content even when line generation was
|
||||
// unavailable: the deterministic digest line still gives the hero an honest
|
||||
// lead, and hiding the marker would lose the arrival moment entirely.
|
||||
// below.
|
||||
//
|
||||
// A fresh arrival used to count as content on its own, back when the
|
||||
// "just in" chip was the thing it kept on screen. With the chip gone it
|
||||
// no longer earns a hero: an arrival-only digest reaches the compact
|
||||
// all-clear composition, which would tell someone with no readings at
|
||||
// all that everything is clear — a claim about their health made from
|
||||
// nothing, and made only on the days something happened to sync. The
|
||||
// arrival still rides the DTO and still feeds the morning line.
|
||||
//
|
||||
// A filtered layout is the one explicit exception: its empty digest means
|
||||
// "the hidden canonical candidates are quiet here", so retain all-clear.
|
||||
if (
|
||||
|
||||
Reference in new issue
Block a user