diff --git a/src/components/settings/__tests__/sharing-section-error-state.test.tsx b/src/components/settings/__tests__/sharing-section-error-state.test.tsx new file mode 100644 index 000000000..a1e110d50 --- /dev/null +++ b/src/components/settings/__tests__/sharing-section-error-state.test.tsx @@ -0,0 +1,54 @@ +/** + * A read failure on the owner share-link surface must not read as "no active + * links". `useQuery` is forced into its error state; the card has to render the + * `query-error-card` slot with a retry affordance and must never fall through + * to the empty-state copy — that fall-through is the §6-forbidden case a + * clinical sharing surface cannot afford (a failed read is indistinguishable + * from "you have shared nothing"). + * + * SSR-static render, matching the sibling sharing-section test. + */ +import { describe, expect, it, vi } from "vitest"; +import { renderToStaticMarkup } from "react-dom/server"; + +vi.mock("@/hooks/use-auth", () => ({ + useAuth: () => ({ + user: { id: "u1", role: "USER" }, + isAuthenticated: true, + isLoading: false, + refetch: vi.fn(), + }), +})); + +vi.mock("@tanstack/react-query", () => ({ + useQuery: () => ({ + data: undefined, + isError: true, + isLoading: false, + refetch: vi.fn(), + }), + useMutation: () => ({ mutate: vi.fn(), isPending: false }), + useQueryClient: () => ({ invalidateQueries: vi.fn() }), +})); + +import { I18nProvider } from "@/lib/i18n/context"; +import { SharingSection } from "../sharing-section"; + +function render() { + return renderToStaticMarkup( + + + , + ); +} + +describe(" — a read failure is honest, not empty", () => { + it("renders the query-error card with retry and never the empty state", () => { + const html = render(); + expect(html).toContain('data-slot="query-error-card"'); + expect(html).toContain('data-slot="query-error-retry"'); + // The empty-state must not appear: a failed read is not "no active links". + expect(html).not.toContain('data-testid="share-active-empty"'); + expect(html).not.toContain("No active share links"); + }); +}); diff --git a/src/components/settings/sharing-section.tsx b/src/components/settings/sharing-section.tsx index 6bbc8ba2a..88cb483ca 100644 --- a/src/components/settings/sharing-section.tsx +++ b/src/components/settings/sharing-section.tsx @@ -39,6 +39,8 @@ import { import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { EmptyState } from "@/components/ui/empty-state"; +import { QueryErrorCard } from "@/components/ui/query-error-card"; +import { Skeleton } from "@/components/ui/skeleton"; import { SettingsCard } from "@/components/settings/settings-card"; import { SettingsCardHeader } from "@/components/settings/_card-header"; import { @@ -106,7 +108,12 @@ function ShareLinksCard() { expiryDays: number; } | null>(null); - const { data: links } = useQuery({ + const { + data: links, + isError, + isLoading, + refetch, + } = useQuery({ queryKey: queryKeys.shareLinks(), queryFn: () => apiGet<{ shareLinks: ShareLinkSummary[] }>("/api/share-links").then( @@ -147,180 +154,193 @@ function ShareLinksCard() { initialExpiryDays={prefill?.expiryDays} /> -
-

- {t("settings.sharing.activeTitle")} -

- {activeLinks.length === 0 ? ( - - ) : ( -
    - {activeLinks.map((link) => ( -
  • -
    -

    - {link.label} -

    -
    - - {t("settings.sharing.statusActive")} - - {link.protected && ( - - - {t("settings.sharing.protected")} - - )} - {link.documentCount > 0 && ( - - - {link.documentCount} - - )} -
    -
    -

    - - {t("settings.sharing.created")}: - {" "} - {formatDate(link.createdAt)} -

    -

    - - {t("settings.sharing.expires")}: - {" "} - {formatDateTime(link.expiresAt)} -

    -

    - - {t("settings.sharing.accessCount")}: - {" "} - {link.accessCount} - {link.lastAccessAt - ? ` · ${formatDateTime(link.lastAccessAt)}` - : ""} -

    - - - - - - - - {t("settings.sharing.revoke")} - - - {t("settings.sharing.revokeDescription")} - - - - - {t("common.cancel")} - - revokeMutation.mutate(link.id)} - > - {t("settings.sharing.revoke")} - - - - -
  • - ))} -
- )} -
- - {inactiveLinks.length > 0 && ( -
- - {showRevoked && ( - + )} +
+ + {inactiveLinks.length > 0 && ( +
+ + {showRevoked && ( + + + ))} + + )} +
)} - + )} );