fix(sleep): restore the last-night hypnogram timeline bar

The staged bar is gated on the night carrying more than one distinct stage
boundary. The guard read the segment END instants, so an ordinary multi-stage
night whose segments share a right edge — the shape a summary-derived writer
reconstructs to — collapsed to the breakdown footer with no bar. Gate on the
distinct START instants instead, so a night with timed stages keeps its
timeline and only a session recorded on a single instant falls back to the
breakdown. Covers the chart-vs-breakdown branch the test suite did not.
This commit is contained in:
Marc-André Bombeck committed 2026-06-14 20:21:19 +02:00
1 parent dd8ffffd91
commit 6a99f72384
2 files changed
+59 -6

No files matched your search

@@ -92,3 +92,54 @@ describe("<SleepHypnogram> — lane derivation", () => {
expect(src).toContain("ticks={lanes.map((_, i) => lanes.length - 1 - i)}");
});
});
describe("<SleepHypnogram> — timeline bar visibility", () => {
const sharedEnd = "2026-06-09T06:00:00.000Z";
it("draws the timeline bar for a multi-stage night whose segments share a right edge", () => {
// Distinct STARTS but a shared END instant — the shape a summary-derived
// writer reconstructs to. The earlier end-instant guard hid the bar here;
// gating on the start instant restores it. (Regression: bar vanished,
// leaving only the breakdown numbers.)
const session: SleepHypnogramSession = {
...baseSession,
segments: [
{
stage: "CORE",
start: "2026-06-08T22:30:00.000Z",
end: sharedEnd,
minutes: 200,
},
{
stage: "DEEP",
start: "2026-06-09T01:00:00.000Z",
end: sharedEnd,
minutes: 100,
},
{
stage: "REM",
start: "2026-06-09T03:00:00.000Z",
end: sharedEnd,
minutes: 120,
},
],
};
expect(render(session)).toContain('role="img"');
});
it("collapses to the breakdown footer for a degenerate single-instant session", () => {
// Every segment stamped on ONE instant — no real timeline, so only the
// breakdown footer, no bar.
const instant = "2026-06-09T06:00:00.000Z";
const session: SleepHypnogramSession = {
...baseSession,
segments: [
{ stage: "CORE", start: instant, end: instant, minutes: 200 },
{ stage: "DEEP", start: instant, end: instant, minutes: 100 },
],
};
const html = render(session);
expect(html).not.toContain('role="img"');
expect(html).toContain('data-slot="sleep-hypnogram-breakdown"');
});
});
+8 -6
View File
@@ -209,15 +209,17 @@ export function SleepHypnogram({ session }: SleepHypnogramProps) {
// need any positive per-stage minutes. A legacy stage-summary night (no
// segment spans) still shows its breakdown rather than collapsing the
// whole card. "Unavailable" is reserved for a truly empty session.
// Spans must also carry more than one distinct end instant: a summary-
// shaped writer (per-stage night totals all stamped on the sleep end)
// reconstructs into bars that all touch the night's right edge — that
// is a breakdown wearing a timeline costume, so paint it as the
// breakdown footer instead.
// Spans must also carry more than one distinct START instant: a real
// night with timed stages steps through the clock, so its segments begin
// at different instants and earn the timeline bar. Only a degenerate
// session whose every segment starts on a single instant collapses to the
// breakdown footer. (Gating on the END instant instead hid the bar for an
// ordinary multi-stage night whose segments happened to share a right
// edge — the regression this restores.)
const hasTrack =
spans.length > 0 &&
(spans.length === 1 ||
new Set(spans.map((s) => s.x2)).size > 1);
new Set(spans.map((s) => s.x1)).size > 1);
const hasBreakdown = breakdown.length > 0;
return (