test(01-10): add red assessment and records contracts

- pin honest assessment fallback, exhaustion, foreground, and state envelopes
- require consistent save and empty-state add action grammar
This commit is contained in:
Marc-André Bombeck committed 2026-07-29 14:18:35 +02:00
1 parent d8e52b831d
commit 18d0facdfe
7 files changed
+362 -7

No files matched your search

@@ -82,20 +82,30 @@ describe("<InsightStatusCard>", () => {
expect(html).toContain("Pulse stayed inside the band.");
});
it("renders the no-provider state without touching the strip path", () => {
it("renders the no-provider setup state when no terminal text exists", () => {
const html = render(
<InsightStatusCard
{...baseProps}
hasProvider={false}
text="ignored when hasProvider is false"
/>,
<InsightStatusCard {...baseProps} hasProvider={false} text={null} />,
);
// v1.18.6 — the no-provider tile is a guided-setup explainer + a
// Settings link, not a bare "unavailable" line.
expect(html).toContain("Connect an AI provider");
expect(html).toContain("Open AI settings");
expect(html).toContain("/settings/ai");
expect(html).not.toContain("ignored when hasProvider is false");
});
it("renders a screened terminal fallback even when its envelope has no provider", () => {
const html = render(
<InsightStatusCard
{...baseProps}
hasProvider={false}
text="The assessment could not be completed safely. Your recorded measurements remain available."
/>,
);
expect(html).toContain("could not be completed safely");
expect(html).toContain("recorded measurements remain available");
expect(html).not.toContain("Connect an AI provider");
expect(html).not.toContain("because your medication caused");
});
it("renders the empty-text state without crashing on null", () => {
@@ -148,4 +158,36 @@ describe("<InsightStatusCard>", () => {
// we do not double-paint the announcement.
expect(html).not.toContain("animate-spin");
});
it("keeps loading, preparing, no-provider, empty, and populated states distinct with stable compact geometry", () => {
const loading = render(
<InsightStatusCard {...baseProps} text={null} loading />,
);
const preparing = render(
<InsightStatusCard {...baseProps} text={null} preparing />,
);
const noProvider = render(
<InsightStatusCard {...baseProps} hasProvider={false} text={null} />,
);
const empty = render(<InsightStatusCard {...baseProps} text={null} />);
const populated = render(
<InsightStatusCard
{...baseProps}
text="Pulse stayed inside the expected range."
/>,
);
expect(loading).toContain('data-testid="insight-status-card-loading"');
expect(preparing).toContain('data-testid="insight-status-card-preparing"');
expect(noProvider).toContain('data-slot="insight-status-no-provider-cta"');
expect(empty).toContain("No assessment yet.");
expect(populated).toContain('data-slot="insight-assessment"');
for (const html of [loading, preparing, noProvider, empty, populated]) {
expect(html).toContain("gap-2");
expect(html).toContain("py-3");
expect(html).toContain("md:py-4");
}
expect(populated).toContain("text-foreground");
});
});
@@ -0,0 +1,80 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { renderToStaticMarkup } from "react-dom/server";
const mocks = vi.hoisted(() => ({
rows: [] as Array<Record<string, unknown>>,
}));
vi.mock("@tanstack/react-query", () => ({
useQuery: () => ({
data: mocks.rows,
isLoading: false,
isError: false,
refetch: vi.fn(),
}),
useQueryClient: () => ({ invalidateQueries: vi.fn() }),
useMutation: () => ({ mutate: vi.fn(), isPending: false }),
}));
vi.mock("@/lib/api/api-fetch", () => ({
apiDelete: vi.fn(),
apiGet: vi.fn(),
}));
vi.mock("sonner", () => ({ toast: { success: vi.fn(), error: vi.fn() } }));
vi.mock("@/components/ui/responsive-sheet", () => ({
ResponsiveSheet: () => null,
}));
vi.mock("@/components/data-list", () => ({
DeleteButton: (props: { title: string }) => (
<button aria-label={props.title}>Delete</button>
),
}));
import { I18nProvider } from "@/lib/i18n/context";
import { AllergyManager } from "../allergy-manager";
function render() {
return renderToStaticMarkup(
<I18nProvider initialLocale="en">
<AllergyManager />
</I18nProvider>,
);
}
beforeEach(() => {
mocks.rows = [];
});
describe("<AllergyManager> action grammar", () => {
it("shows exactly one first-entry action and no duplicate top add action when empty", () => {
const html = render();
expect(html.match(/Add your first allergy/g)).toHaveLength(1);
expect(html).not.toMatch(/>Add allergy<\/button>/);
});
it("shows one normal top add action plus edit/delete controls when populated", () => {
mocks.rows = [
{
id: "allergy-1",
substance: "Penicillin",
category: "MEDICATION",
type: "ALLERGY",
severity: "SEVERE",
status: "ACTIVE",
onsetAt: null,
reaction: "Hives",
note: null,
createdAt: "2026-07-28T10:00:00.000Z",
updatedAt: "2026-07-28T10:00:00.000Z",
},
];
const html = render();
expect(html.match(/>Add allergy<\/button>/g)).toHaveLength(1);
expect(html).not.toContain("Add your first allergy");
expect(html).toContain('aria-label="Edit"');
expect(html).toContain('aria-label="Delete this allergy?"');
expect(html).toContain("Penicillin");
});
});
@@ -0,0 +1,77 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { renderToStaticMarkup } from "react-dom/server";
const mocks = vi.hoisted(() => ({
rows: [] as Array<Record<string, unknown>>,
}));
vi.mock("@tanstack/react-query", () => ({
useQuery: () => ({
data: mocks.rows,
isLoading: false,
isError: false,
refetch: vi.fn(),
}),
useQueryClient: () => ({ invalidateQueries: vi.fn() }),
useMutation: () => ({ mutate: vi.fn(), isPending: false }),
}));
vi.mock("@/lib/api/api-fetch", () => ({
apiDelete: vi.fn(),
apiGet: vi.fn(),
}));
vi.mock("sonner", () => ({ toast: { success: vi.fn(), error: vi.fn() } }));
vi.mock("@/components/ui/responsive-sheet", () => ({
ResponsiveSheet: () => null,
}));
vi.mock("@/components/data-list", () => ({
DeleteButton: (props: { title: string }) => (
<button aria-label={props.title}>Delete</button>
),
}));
import { I18nProvider } from "@/lib/i18n/context";
import { FamilyHistoryManager } from "../family-history-manager";
function render() {
return renderToStaticMarkup(
<I18nProvider initialLocale="en">
<FamilyHistoryManager />
</I18nProvider>,
);
}
beforeEach(() => {
mocks.rows = [];
});
describe("<FamilyHistoryManager> action grammar", () => {
it("shows exactly one first-entry action and no duplicate top add action when empty", () => {
const html = render();
expect(html.match(/Add your first entry/g)).toHaveLength(1);
expect(html).not.toMatch(/>Add entry<\/button>/);
});
it("shows one normal top add action plus edit/delete controls when populated", () => {
mocks.rows = [
{
id: "family-1",
relationship: "MOTHER",
condition: "Type 2 diabetes",
ageAtOnset: 55,
note: null,
createdAt: "2026-07-28T10:00:00.000Z",
updatedAt: "2026-07-28T10:00:00.000Z",
},
];
const html = render();
expect(html.match(/>Add entry<\/button>/g)).toHaveLength(1);
expect(html).not.toContain("Add your first entry");
expect(html).toContain('aria-label="Edit"');
expect(html).toContain('aria-label="Delete this entry?"');
expect(html).toContain("Type 2 diabetes");
expect(html).toContain("Mother");
});
});
@@ -107,6 +107,27 @@ beforeEach(() => {
});
describe("health-profile fact removal control", () => {
it("uses one Save label for new and existing lifestyle facts", () => {
render();
const actionLabels = renderedButtons
.filter(
(props) =>
props.type === "button" &&
props.variant === "outline" &&
typeof props.onClick === "function",
)
.map((props) => {
const children = Array.isArray(props.children)
? props.children
: [props.children];
return children.filter((child) => typeof child === "string").join("");
});
expect(actionLabels).toEqual(["Save", "Save", "Save"]);
expect(actionLabels).not.toContain("Correct");
});
it("renders a confirmed removal action only for each recorded current fact", () => {
const { html } = render();