@@ -13,19 +13,37 @@ import StatCard from "@/components/StatCard";
1313import SLOStatusPanel from "@/components/SLOStatusPanel" ;
1414import type { SLOData } from "@/lib/transparencyHooks" ;
1515
16- // ── Mock fetch globally ───────────────────────────────────────────────────
17- const mockFetch = jest . fn ( ) ;
18- global . fetch = mockFetch ;
16+ // ── jsdom polyfills ────────────────────────────────────────────────────────
17+ // jsdom does not implement AbortSignal.timeout(), which HealthBanner uses
18+ // for the readiness fetch call. Polyfill it so the mock fetch is reached.
19+ const ORIGINAL_ABORT_TIMEOUT = AbortSignal . timeout ;
20+ beforeAll ( ( ) => {
21+ AbortSignal . timeout = ( _ms : number ) => {
22+ const controller = new AbortController ( ) ;
23+ return controller . signal ;
24+ } ;
25+ } ) ;
26+ afterAll ( ( ) => {
27+ AbortSignal . timeout = ORIGINAL_ABORT_TIMEOUT ;
28+ } ) ;
29+
30+ // ── Mock fetch globally (follow adminAuth.test.ts pattern) ────────────────
31+
32+ const ORIGINAL_FETCH = global . fetch ;
1933
2034beforeEach ( ( ) => {
21- jest . clearAllMocks ( ) ;
35+ global . fetch = jest . fn ( ) ;
36+ } ) ;
37+
38+ afterAll ( ( ) => {
39+ global . fetch = ORIGINAL_FETCH ;
2240} ) ;
2341
2442// ── HealthBanner Tests ────────────────────────────────────────────────────
2543
2644describe ( "HealthBanner" , ( ) => {
27- it ( 'shows "All Systems Operational" when readyz returns 200 and all checks OK' , async ( ) => {
28- mockFetch . mockResolvedValueOnce ( {
45+ it ( 'shows "All Systems Operational" when readyz returns 200 with all checks OK' , async ( ) => {
46+ ( global . fetch as jest . Mock ) . mockResolvedValue ( {
2947 ok : true ,
3048 json : async ( ) => ( {
3149 status : "ready" ,
@@ -39,14 +57,13 @@ describe("HealthBanner", () => {
3957
4058 render ( < HealthBanner /> ) ;
4159
42- // Wait for the fetch to resolve and state to update
4360 await screen . findByText ( / A l l S y s t e m s O p e r a t i o n a l / i) ;
4461 expect ( screen . getByRole ( "status" ) ) . toBeInTheDocument ( ) ;
4562 expect ( screen . getByText ( / 🟢 / ) ) . toBeInTheDocument ( ) ;
4663 } ) ;
4764
48- it ( 'shows "Degraded Performance " when some subsystems are degraded ' , async ( ) => {
49- mockFetch . mockResolvedValueOnce ( {
65+ it ( 'shows "Service Disruption " when subsystems are unreachable ' , async ( ) => {
66+ ( global . fetch as jest . Mock ) . mockResolvedValue ( {
5067 ok : true ,
5168 json : async ( ) => ( {
5269 status : "not ready" ,
@@ -66,22 +83,24 @@ describe("HealthBanner", () => {
6683 } ) ;
6784
6885 it ( 'shows "Service Disruption" when fetch fails' , async ( ) => {
69- mockFetch . mockRejectedValueOnce ( new Error ( "Network error" ) ) ;
86+ ( global . fetch as jest . Mock ) . mockRejectedValue ( new Error ( "Network error" ) ) ;
7087
7188 render ( < HealthBanner /> ) ;
7289
7390 await screen . findByText ( / S e r v i c e D i s r u p t i o n / i) ;
7491 expect ( screen . getByText ( / 🔴 / ) ) . toBeInTheDocument ( ) ;
7592 } ) ;
7693
77- it ( "shows loading skeleton initially" , ( ) => {
78- // Don't resolve the fetch so it stays in loading state
79- mockFetch . mockImplementationOnce ( ( ) => new Promise ( ( ) => { } ) ) ;
94+ it ( "shows loading skeleton initially before fetch resolves" , async ( ) => {
95+ // Return a promise that never resolves so the component stays in loading state
96+ ( global . fetch as jest . Mock ) . mockImplementation ( ( ) => new Promise ( ( ) => { } ) ) ;
8097
81- render ( < HealthBanner /> ) ;
98+ const { container } = render ( < HealthBanner /> ) ;
8299
83- // Should show skeleton (checking for the container with bg-slate-50)
84- expect ( screen . getByText ( / C h e c k i n g s t a t u s / i) ) . toBeInTheDocument ( ) ;
100+ // The initial render shows a skeleton (animated pulse box) before the fetch resolves
101+ const skeletonBox = container . querySelector ( ".animate-pulse" ) ;
102+ expect ( skeletonBox ) . toBeInTheDocument ( ) ;
103+ expect ( skeletonBox ) . toHaveClass ( "rounded" ) ;
85104 } ) ;
86105} ) ;
87106
@@ -176,7 +195,7 @@ describe("SLOStatusPanel", () => {
176195 const { container } = render ( < SLOStatusPanel sloData = { null } isLoading = { true } /> ) ;
177196
178197 // Skeleton should render a card container with animated pulse elements
179- const skeletonCard = container . querySelector ( ' .card' ) ;
198+ const skeletonCard = container . querySelector ( " .card" ) ;
180199 expect ( skeletonCard ) . toBeInTheDocument ( ) ;
181200 } ) ;
182201
@@ -206,9 +225,6 @@ describe("SLOStatusPanel", () => {
206225
207226describe ( "Transparency Page Integration (Recent Donations)" , ( ) => {
208227 it ( "renders waiting state when no donations" , ( ) => {
209- // We test the inline RecentDonationsFeed by rendering the full page
210- // with zero donations. Since the full page has complex deps (fetch,
211- // socket), we verify the loading indicator is present.
212228 render (
213229 < div >
214230 < p className = "text-[#94A3B8] dark:text-[#64748B] text-sm font-body" >
0 commit comments