/** * v1.4.19 Phase A5 — Settings → Integrations status consolidation. * * the maintainer's verbatim concern (paraphrased): * - Withings card had 3 status displays — top-right "connected" * badge, mid-card "connected / last successful / last attempt" * trio container, and the badge inside that container. * - Mood Log card had 4 — same as Withings PLUS a "letzter Sync" * line at the very bottom. * * After A5: each card has EXACTLY ONE status display (the pill, top * right). Error context (the failure message itself, when actionable) * is allowed to surface below the pill since it carries information * the pill can't fit. Timestamp repetition is gone. * * The tests pin: * 1. Healthy Withings + Mood Log: ONE pill per card, no banner. * 2. Withings transient error: ONE pill per card + error message, * NO redundant "{n}/{threshold} consecutive failures" badge, * NO "Last successful sync / Last attempt" trio. * 3. Withings reauth: pill carries the "Error — reconnect" label. * 4. Mood Log disconnected: pill says "Not connected", no other * status display surfaces. * 5. Mood Log card has the visual divider that Withings always had * — consistency the maintainer explicitly called out. */ import { describe, expect, it, vi, beforeEach } from "vitest"; import { renderToStaticMarkup } from "react-dom/server"; vi.mock("next/navigation", () => ({ useRouter: () => ({ push: vi.fn(), replace: vi.fn() }), usePathname: () => "/settings/integrations", })); vi.mock("@/hooks/use-auth", () => ({ useAuth: () => ({ user: { id: "u1", role: "USER" }, isAuthenticated: true, isLoading: false, refetch: vi.fn(), }), })); // v1.12.1 — the four cards now read every field off the single // /api/integrations/status envelope; the per-card /api//status // queries are gone. The test mock only needs the consolidated payload. let integrationStatusPayload: unknown = null; function setIntegrationStatus(payload: unknown) { integrationStatusPayload = payload; } vi.mock("@tanstack/react-query", () => ({ useQuery: ({ queryKey }: { queryKey: unknown[] }) => { const key = Array.isArray(queryKey) ? queryKey.join("/") : ""; if (key === "integrations/status") { return { data: integrationStatusPayload, isLoading: false }; } if (key === "settings/global-services") { return { data: { telegramGlobal: true, ntfyGlobal: true, webPushGlobal: true, apiGlobal: true, moodLogGlobal: true, }, isLoading: false, }; } return { data: null, isLoading: false, refetch: vi.fn() }; }, useMutation: () => ({ mutate: vi.fn(), isPending: false }), useQueryClient: () => ({ invalidateQueries: vi.fn() }), })); import { I18nProvider } from "@/lib/i18n/context"; import { IntegrationsSection } from "../integrations-section"; function render() { return renderToStaticMarkup( , ); } /** Count occurrences of a substring in the rendered HTML. */ function count(html: string, needle: string): number { let n = 0; let i = 0; while ((i = html.indexOf(needle, i)) !== -1) { n++; i += needle.length; } return n; } describe("IntegrationsSection — single-status-display contract (A5)", () => { beforeEach(() => { integrationStatusPayload = null; }); it("renders exactly ONE status pill per card when both integrations are healthy", () => { setIntegrationStatus({ threshold: 3, integrations: [ { integration: "withings", state: "connected", lastSuccessAt: "2026-05-09T18:00:00.000Z", lastAttemptAt: "2026-05-09T18:00:00.000Z", lastError: null, connected: true, configured: true, legacyLastSyncedAt: "2026-05-09T18:00:00.000Z", hasActivityScope: true, }, { integration: "moodlog", state: "connected", lastSuccessAt: "2026-05-09T17:00:00.000Z", lastAttemptAt: "2026-05-09T17:00:00.000Z", lastError: null, configured: true, enabled: true, legacyLastSyncedAt: "2026-05-09T17:00:00.000Z", entryCount: 42, webhookSecret: "secret123", }, ], }); const html = render(); // Exactly one pill per card → 3 pills total (Withings, WHOOP, // Fitbit / Google Health). The moodLog integration was removed. expect(count(html, 'data-testid="integration-status-pill"')).toBe(3); // The redundant banner from v1.4.15 is gone. expect(html).not.toContain('data-testid="integration-status-banner"'); // Card-body "letzter Sync" repetition is gone — no // "Last sync:" label outside the pill. expect(html).not.toContain("Last sync:"); // No "Last successful sync" / "Last attempt" trio. expect(html).not.toContain("Last successful sync"); expect(html).not.toContain("Last attempt"); }); it("Withings transient error keeps the pill + error text but drops the {n}/{threshold} duplicate badge", () => { setIntegrationStatus({ threshold: 3, integrations: [ { integration: "withings", state: "error_transient", lastSuccessAt: "2026-05-09T08:00:00.000Z", lastAttemptAt: "2026-05-09T18:00:00.000Z", lastError: "Withings refresh error: 503 - upstream", connected: true, configured: true, legacyLastSyncedAt: "2026-05-09T08:00:00.000Z", hasActivityScope: true, }, { integration: "moodlog", state: "connected", lastSuccessAt: null, lastAttemptAt: null, lastError: null, }, ], }); const html = render(); // Pill carries the "Error — reconnect" copy via the pill's // `data-state="error"` marker. expect(html).toContain('data-state="error"'); // Actionable error message still surfaces (we kept it because // the pill can't fit "Withings refresh error: 503 - upstream"). expect(html).toContain("Withings refresh error: 503 - upstream"); // …but the consecutive-failure badge that doubled the pill's // signal is gone. expect(html).not.toContain("2/3 consecutive failures"); expect(html).not.toContain("Last successful sync"); expect(html).not.toContain("Last attempt"); }); it("Withings reauth state surfaces 'Error — reconnect' on the pill (the maintainer's wording)", () => { setIntegrationStatus({ threshold: 3, integrations: [ { integration: "withings", state: "error_reauth", lastSuccessAt: "2026-05-08T12:00:00.000Z", lastAttemptAt: "2026-05-09T18:00:00.000Z", lastError: "Withings refresh error: 100 - invalid_grant", connected: true, configured: true, legacyLastSyncedAt: "2026-05-08T12:00:00.000Z", tokenExpired: true, hasActivityScope: true, }, { integration: "moodlog", state: "connected", lastSuccessAt: null, lastAttemptAt: null, lastError: null, }, ], }); const html = render(); expect(html).toContain('data-state="error"'); // Old "Reconnect required" banner is gone — the pill is now // the canonical surface. expect(html).not.toContain('data-testid="integration-status-banner"'); }); it("disconnected state shows 'Not connected' on the pill, no extra status surfaces", () => { setIntegrationStatus({ threshold: 3, integrations: [ { integration: "withings", state: "disconnected", lastSuccessAt: "2026-05-08T10:00:00.000Z", lastAttemptAt: "2026-05-08T10:00:00.000Z", lastError: null, }, ], }); const html = render(); expect(html).toContain("Not connected"); expect(html).not.toContain('data-testid="integration-status-banner"'); // No trailing "letzter Sync" line outside the pill. expect(html).not.toContain("Last sync:"); }); // v1.4.43 W14 — parked-state surface check. // The pill flips to `data-state="parked"` and the resume banner // renders with the reconnect CTA. The legacy `error` reconnect text // (red) must NOT appear because the user-facing copy is the lighter // "Paused — reconnect manually" string, not "Error — reconnect". it("Withings parked state surfaces the parked pill + resume CTA", () => { setIntegrationStatus({ threshold: 3, integrations: [ { integration: "withings", state: "parked", lastSuccessAt: "2026-05-08T12:00:00.000Z", lastAttemptAt: "2026-05-09T18:00:00.000Z", lastError: "Withings activity error: 293", connected: true, configured: true, legacyLastSyncedAt: "2026-05-08T12:00:00.000Z", hasActivityScope: true, }, { integration: "moodlog", state: "connected", lastSuccessAt: null, lastAttemptAt: null, lastError: null, }, ], }); const html = render(); expect(html).toContain('data-state="parked"'); expect(html).toContain("Paused"); expect(html).toContain("reconnect manually"); expect(html).toContain('data-testid="withings-parked-banner"'); expect(html).toContain('data-testid="withings-resume-button"'); // The lastError is surfaced under the pill so the operator sees // the contract-mismatch reason without opening Audit Log. expect(html).toContain("Withings activity error: 293"); }); it("every integration card carries the visual divider (consistency)", () => { setIntegrationStatus({ threshold: 3, integrations: [ { integration: "withings", state: "connected", lastSuccessAt: "2026-05-09T18:00:00.000Z", lastAttemptAt: "2026-05-09T18:00:00.000Z", lastError: null, connected: true, configured: true, legacyLastSyncedAt: "2026-05-09T18:00:00.000Z", hasActivityScope: true, }, ], }); const html = render(); // Every integration card includes the section divider data-testid so the // header → body separation is visually consistent (Withings, WHOOP, // Fitbit / Google Health). The moodLog integration was removed. expect(count(html, 'data-testid="integration-card-divider"')).toBe(3); }); });