mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-14 06:40:50 +00:00
test(healthkit): cover device-event ingest + the awareness surface
Add mapping tests for the five event classes (verdict resolution, severity grading, the value-pinned-to-1 contract, unknown-codepoint fallback, and that a continuous measurement never gets a classification), the awareness-card gating + device-verdict framing + disclaimer presence, and the events route narrowing + empty-state contract. Extend the enum, chart-token and Coach-gate drift guards for the new event types.
This commit is contained in:
1 parent
135141a750
commit
95064b405a
5 files changed
+280
-11
No files matched your search
@@ -0,0 +1,115 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
|
||||
import { I18nProvider } from "@/lib/i18n/context";
|
||||
|
||||
/**
|
||||
* v1.10.0 — categorical events (WX-B). `<RhythmEventsCard>` unit tests.
|
||||
*
|
||||
* The card is the device-flagged event awareness surface. The load-bearing
|
||||
* behaviour under test:
|
||||
* - data-availability gating: the card un-mounts entirely (renders
|
||||
* nothing) when the user has no events or while the payload is in
|
||||
* flight — never an empty / alarming card.
|
||||
* - the device-verdict framing ("Your device flagged …") and the
|
||||
* permanent regulatory disclaimer that states HealthLog does not make a
|
||||
* medical assessment.
|
||||
*
|
||||
* The component depends on `useAuth` (gate) + TanStack Query (`useQuery`)
|
||||
* for the payload, so both are mocked and the assertions run through SSR.
|
||||
*/
|
||||
|
||||
vi.mock("@/hooks/use-auth", () => ({
|
||||
useAuth: vi.fn(() => ({ isAuthenticated: true, user: null })),
|
||||
}));
|
||||
|
||||
const useQueryMock = vi.fn();
|
||||
vi.mock("@tanstack/react-query", () => ({
|
||||
useQuery: (opts: unknown) => useQueryMock(opts),
|
||||
}));
|
||||
|
||||
const { RhythmEventsCard } = await import("../rhythm-events-card");
|
||||
|
||||
interface RhythmEvent {
|
||||
id: string;
|
||||
type: string;
|
||||
classification: string | null;
|
||||
occurredAt: string;
|
||||
source: string;
|
||||
deviceType: string | null;
|
||||
}
|
||||
|
||||
function renderWith(
|
||||
data: { events: RhythmEvent[]; hasEvents: boolean } | undefined,
|
||||
) {
|
||||
useQueryMock.mockReturnValue({ data, isLoading: false });
|
||||
return renderToStaticMarkup(
|
||||
<I18nProvider initialLocale="en">
|
||||
<RhythmEventsCard />
|
||||
</I18nProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
const IRREGULAR_EVENT: RhythmEvent = {
|
||||
id: "evt_1",
|
||||
type: "IRREGULAR_RHYTHM_NOTIFICATION",
|
||||
classification: "IRREGULAR",
|
||||
occurredAt: "2026-06-01T09:15:00.000Z",
|
||||
source: "APPLE_HEALTH",
|
||||
deviceType: "watch",
|
||||
};
|
||||
|
||||
const STEADINESS_EVENT: RhythmEvent = {
|
||||
id: "evt_2",
|
||||
type: "WALKING_STEADINESS_EVENT",
|
||||
classification: "VERY_LOW",
|
||||
occurredAt: "2026-05-20T17:00:00.000Z",
|
||||
source: "APPLE_HEALTH",
|
||||
deviceType: "phone",
|
||||
};
|
||||
|
||||
describe("<RhythmEventsCard>", () => {
|
||||
it("renders nothing before the payload resolves", () => {
|
||||
useQueryMock.mockReturnValue({ data: undefined, isLoading: true });
|
||||
const html = renderToStaticMarkup(
|
||||
<I18nProvider initialLocale="en">
|
||||
<RhythmEventsCard />
|
||||
</I18nProvider>,
|
||||
);
|
||||
expect(html).toBe("");
|
||||
});
|
||||
|
||||
it("renders nothing when the user has no events (data-availability gate)", () => {
|
||||
const html = renderWith({ events: [], hasEvents: false });
|
||||
expect(html).toBe("");
|
||||
});
|
||||
|
||||
it("renders the timeline + each event row when events exist", () => {
|
||||
const html = renderWith({
|
||||
events: [IRREGULAR_EVENT, STEADINESS_EVENT],
|
||||
hasEvents: true,
|
||||
});
|
||||
expect(html).toContain('data-slot="rhythm-events-card"');
|
||||
expect(html).toContain('data-slot="rhythm-events-timeline"');
|
||||
const rows = (html.match(/data-slot="rhythm-event-row"/g) ?? []).length;
|
||||
expect(rows).toBe(2);
|
||||
expect(html).toContain('data-event-type="IRREGULAR_RHYTHM_NOTIFICATION"');
|
||||
expect(html).toContain('data-event-type="WALKING_STEADINESS_EVENT"');
|
||||
});
|
||||
|
||||
it("frames the verdict as the DEVICE's decision, never HealthLog's", () => {
|
||||
const html = renderWith({ events: [IRREGULAR_EVENT], hasEvents: true });
|
||||
expect(html).toContain('data-slot="rhythm-event-verdict"');
|
||||
// Verbatim device-decision framing — load-bearing regulatory copy.
|
||||
expect(html).toContain("Your device flagged a possible irregular rhythm.");
|
||||
});
|
||||
|
||||
it("renders the permanent regulatory disclaimer", () => {
|
||||
const html = renderWith({ events: [IRREGULAR_EVENT], hasEvents: true });
|
||||
expect(html).toContain('data-slot="rhythm-events-disclaimer"');
|
||||
// The disclaimer must state HealthLog does not make a medical
|
||||
// assessment and does not diagnose.
|
||||
expect(html).toContain("not a medical assessment by HealthLog");
|
||||
expect(html).toContain("does not diagnose");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user