/// import type { ReactNode } from "react"; import { Suspense, StrictMode, Component, useState } from "react"; import { act, fireEvent, render, screen, waitFor } from "@testing-library/react"; import { createHttpMockingServer } from "@hyper-fetch/testing"; import { useFetch } from "hooks/use-fetch"; import { client, createRequest } from "../../utils"; const { resetMocks, startServer, stopServer, mockRequest } = createHttpMockingServer(); // --------------------------------------------------------------------------- // Shared components // --------------------------------------------------------------------------- function DataView({ request, testId = "result" }: { request: any; testId?: string }) { const { data, error, loading, status } = useFetch(request, { suspense: true, dependencyTracking: false, }); return (
{error ? `error:${JSON.stringify(error)}` : `data:${JSON.stringify(data)}`}
); } interface ErrorBoundaryProps { children: ReactNode; fallback?: ReactNode; } interface ErrorBoundaryState { error: Error | null; } class ErrorBoundary extends Component { constructor(props: ErrorBoundaryProps) { super(props); this.state = { error: null }; } static getDerivedStateFromError(error: Error) { return { error }; } render() { const { error } = this.state; const { fallback, children } = this.props; if (error) { return fallback ??
{error.message}
; } return children; } } // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- describe("useFetch [ Suspense ]", () => { let request = createRequest(); beforeAll(() => { startServer(); }); afterEach(() => { resetMocks(); }); afterAll(() => { stopServer(); }); beforeEach(() => { vi.resetModules(); request = createRequest(); client.clear(); }); // ----------------------------------------------------------------------- // Fallback rendering // ----------------------------------------------------------------------- describe("when rendering with a Suspense boundary", () => { it("should show fallback while loading and render data once resolved", async () => { const mock = mockRequest(request); render( Loading...}> , ); expect(screen.getByTestId("fallback")).toBeInTheDocument(); await waitFor(() => { const el = screen.getByTestId("result"); expect(el.textContent).toContain(JSON.stringify(mock)); expect(el).toHaveAttribute("data-loading", "false"); }); }); it("should render error response without crashing", async () => { mockRequest(request, { status: 400 }); render( Loading...}> , ); expect(screen.getByTestId("fallback")).toBeInTheDocument(); await waitFor(() => { const el = screen.getByTestId("result"); expect(el.textContent).toContain("error:"); }); }); it("should set correct status code on success", async () => { mockRequest(request); render( Loading...}> , ); await waitFor(() => { expect(screen.getByTestId("result")).toHaveAttribute("data-status", "200"); }); }); it("should set correct status code on error", async () => { mockRequest(request, { status: 500 }); render( Loading...}> , ); await waitFor(() => { expect(screen.getByTestId("result")).toHaveAttribute("data-status", "500"); }); }); }); // ----------------------------------------------------------------------- // Cached data // ----------------------------------------------------------------------- describe("when cache already has data", () => { it("should render immediately without suspending", async () => { const mock = mockRequest(request); function CachedView({ testId }: { testId: string }) { const { data, loading } = useFetch(request, { suspense: true, revalidate: false, dependencyTracking: false, }); return (
data:{JSON.stringify(data)},loading:{String(loading)}
); } // First render — suspends, then resolves const first = render( Loading...}> , ); await waitFor(() => { expect(screen.getByTestId("first").textContent).toContain(JSON.stringify(mock)); }); first.unmount(); // Second render — should NOT suspend since data is cached render( Loading...}> , ); // Synchronous assertion — no fallback, data rendered immediately expect(screen.getByTestId("second").textContent).toContain(JSON.stringify(mock)); }); }); // ----------------------------------------------------------------------- // Disabled // ----------------------------------------------------------------------- describe("when disabled", () => { it("should not suspend and render with null data", () => { function DisabledView() { const { data, loading } = useFetch(request, { suspense: true, disabled: true, dependencyTracking: false, }); return (
data:{String(data)},loading:{String(loading)}
); } render( Loading...}> , ); expect(screen.getByTestId("disabled").textContent).toContain("data:null"); expect(screen.queryByTestId("fallback")).not.toBeInTheDocument(); }); }); // ----------------------------------------------------------------------- // Multiple components // ----------------------------------------------------------------------- describe("when multiple components suspend under the same boundary", () => { it("should show single fallback until all resolve", async () => { const requestA = createRequest({ endpoint: "/suspense-a" }); const requestB = createRequest({ endpoint: "/suspense-b" }); const mockA = mockRequest(requestA); const mockB = mockRequest(requestB, { delay: 50 }); render( Loading all...}> , ); expect(screen.getByTestId("fallback")).toBeInTheDocument(); await waitFor(() => { expect(screen.getByTestId("a").textContent).toContain(JSON.stringify(mockA)); expect(screen.getByTestId("b").textContent).toContain(JSON.stringify(mockB)); }); }); it("should support independent Suspense boundaries per component", async () => { const requestA = createRequest({ endpoint: "/suspense-ind-a" }); const requestB = createRequest({ endpoint: "/suspense-ind-b" }); const mockA = mockRequest(requestA); mockRequest(requestB, { delay: 100 }); render(
Loading A...
}> Loading B...}> , ); // Both should initially show fallbacks expect(screen.getByTestId("fallback-a")).toBeInTheDocument(); expect(screen.getByTestId("fallback-b")).toBeInTheDocument(); // A resolves first (no delay) await waitFor(() => { expect(screen.getByTestId("a").textContent).toContain(JSON.stringify(mockA)); }); }); }); // ----------------------------------------------------------------------- // Nested Suspense boundaries // ----------------------------------------------------------------------- describe("when using nested Suspense boundaries", () => { it("should allow outer fallback to wrap inner components", async () => { const outerRequest = createRequest({ endpoint: "/nested-outer" }); const innerRequest = createRequest({ endpoint: "/nested-inner" }); const outerMock = mockRequest(outerRequest); const innerMock = mockRequest(innerRequest, { delay: 50 }); function InnerView() { const { data } = useFetch(innerRequest, { suspense: true, dependencyTracking: false }); return
inner:{JSON.stringify(data)}
; } function OuterView() { const { data } = useFetch(outerRequest, { suspense: true, dependencyTracking: false }); return (
outer:{JSON.stringify(data)} Inner loading...
}> ); } render( Outer loading...}> , ); expect(screen.getByTestId("outer-fallback")).toBeInTheDocument(); await waitFor(() => { expect(screen.getByTestId("outer").textContent).toContain(JSON.stringify(outerMock)); }); await waitFor(() => { expect(screen.getByTestId("inner").textContent).toContain(JSON.stringify(innerMock)); }); }); }); // ----------------------------------------------------------------------- // StrictMode compatibility // ----------------------------------------------------------------------- describe("when rendering in StrictMode", () => { it("should work correctly with StrictMode double-render", async () => { const mock = mockRequest(request); render( Loading...}> , ); await waitFor(() => { const el = screen.getByTestId("result"); expect(el.textContent).toContain(JSON.stringify(mock)); }); }); }); // ----------------------------------------------------------------------- // Dynamic parameter changes // ----------------------------------------------------------------------- describe("when request parameters change", () => { it("should re-suspend when cache key changes", async () => { const request1 = createRequest({ endpoint: "/suspense-param-1" }); const request2 = createRequest({ endpoint: "/suspense-param-2" }); const mock1 = mockRequest(request1); const mock2 = mockRequest(request2, { delay: 50 }); function ParamView({ req }: { req: any }) { const { data } = useFetch(req, { suspense: true, dependencyTracking: false }); return
{JSON.stringify(data)}
; } function Wrapper() { const [req, setReq] = useState(request1); return ( <> Loading...}> ); } render(); await waitFor(() => { expect(screen.getByTestId("param-result").textContent).toContain(JSON.stringify(mock1)); }); act(() => { fireEvent.click(screen.getByTestId("switch-btn")); }); await waitFor(() => { expect(screen.getByTestId("param-result").textContent).toContain(JSON.stringify(mock2)); }); }); }); // ----------------------------------------------------------------------- // ErrorBoundary integration // ----------------------------------------------------------------------- describe("when combined with ErrorBoundary", () => { it("should not trigger ErrorBoundary on normal error responses", async () => { mockRequest(request, { status: 400 }); render( Caught!}> Loading...}> , ); await waitFor(() => { const el = screen.getByTestId("result"); expect(el.textContent).toContain("error:"); }); expect(screen.queryByTestId("error-boundary")).not.toBeInTheDocument(); }); }); // ----------------------------------------------------------------------- // Refetch after suspense // ----------------------------------------------------------------------- describe("when refetching after initial suspense", () => { it("should not re-suspend on refetch (data is already present)", async () => { const mock = mockRequest(request); const spy = vi.fn(); function RefetchView() { const { data, refetch } = useFetch(request, { suspense: true, dependencyTracking: false, }); spy(data); return (
{JSON.stringify(data)}
); } render( Loading...}> , ); await waitFor(() => { expect(screen.getByTestId("refetch-data").textContent).toContain(JSON.stringify(mock)); }); const newMock = mockRequest(request, { data: { updated: true } }); act(() => { fireEvent.click(screen.getByTestId("refetch-btn")); }); // Should NOT show fallback during refetch expect(screen.queryByTestId("fallback")).not.toBeInTheDocument(); await waitFor(() => { expect(screen.getByTestId("refetch-data").textContent).toContain(JSON.stringify(newMock)); }); }); }); // ----------------------------------------------------------------------- // Non-suspense mode (default) // ----------------------------------------------------------------------- describe("when suspense option is not set", () => { it("should behave normally without suspending", async () => { const mock = mockRequest(request); function NormalView() { const { data, loading } = useFetch(request, { dependencyTracking: false }); if (loading) return
Loading...
; return
{JSON.stringify(data)}
; } render( Suspense fallback}> , ); // Should show component's own loading state, not Suspense fallback expect(screen.getByTestId("normal-loading")).toBeInTheDocument(); expect(screen.queryByTestId("fallback")).not.toBeInTheDocument(); await waitFor(() => { expect(screen.getByTestId("normal-data").textContent).toContain(JSON.stringify(mock)); }); }); }); });