@@ -10,6 +10,8 @@ import { describe, it, expect } from "vitest";
1010import { render } from "@testing-library/react" ;
1111import { LiveIndicator , computeColumnTally , FeatureGrid } from "./feature-grid" ;
1212import type { CellContext , CellRenderer } from "./feature-grid" ;
13+ import { OverlayColumnHeader } from "./overlay-column-header" ;
14+ import type { Overlay } from "@/lib/overlay-types" ;
1315import { urlsFor } from "./cell-pieces" ;
1416import { getIntegrations } from "@/lib/registry" ;
1517import { starterIsSupported , STARTER_LEVELS } from "@/lib/live-status" ;
@@ -145,7 +147,13 @@ describe("computeColumnTally", () => {
145147 live . set ( "e2e:i1/f2" , row ( "e2e:i1/f2" , "e2e" , "green" ) ) ;
146148 const t = computeColumnTally ( integration , features , live ) ;
147149 // D6-ceiling: D3-only green with no D5/D6 → gray → not counted
148- expect ( t ) . toEqual ( { green : 0 , amber : 0 , red : 0 , unknown : false } ) ;
150+ expect ( t ) . toEqual ( {
151+ green : 0 ,
152+ amber : 0 ,
153+ red : 0 ,
154+ unknown : false ,
155+ loading : false ,
156+ } ) ;
149157 } ) ;
150158
151159 it ( "red D3 → red chip, green D3 without D5/D6 → gray" , ( ) => {
@@ -156,15 +164,27 @@ describe("computeColumnTally", () => {
156164 live . set ( "e2e:i1/f2" , row ( "e2e:i1/f2" , "e2e" , "green" ) ) ;
157165 const t = computeColumnTally ( integration , features , live ) ;
158166 // f1 red (gate fail), f2 gray (no D5/D6)
159- expect ( t ) . toEqual ( { green : 0 , amber : 0 , red : 1 , unknown : false } ) ;
167+ expect ( t ) . toEqual ( {
168+ green : 0 ,
169+ amber : 0 ,
170+ red : 1 ,
171+ unknown : false ,
172+ loading : false ,
173+ } ) ;
160174 } ) ;
161175
162176 it ( "health row alone does not contribute to tally" , ( ) => {
163177 const live : LiveStatusMap = new Map ( ) ;
164178 live . set ( "health:i1" , row ( "health:i1" , "health" , "red" ) ) ;
165179 const t = computeColumnTally ( integration , features , live ) ;
166180 // No D3 rows → all cells gray → nothing counted
167- expect ( t ) . toEqual ( { green : 0 , amber : 0 , red : 0 , unknown : false } ) ;
181+ expect ( t ) . toEqual ( {
182+ green : 0 ,
183+ amber : 0 ,
184+ red : 0 ,
185+ unknown : false ,
186+ loading : false ,
187+ } ) ;
168188 } ) ;
169189
170190 it ( "features without demos are gray (unwired), not counted" , ( ) => {
@@ -178,7 +198,13 @@ describe("computeColumnTally", () => {
178198 } ;
179199 const t = computeColumnTally ( partialInt , features , live ) ;
180200 // f1: wired + D3=green but no D5/D6 → gray; f2: unwired → gray
181- expect ( t ) . toEqual ( { green : 0 , amber : 0 , red : 0 , unknown : false } ) ;
201+ expect ( t ) . toEqual ( {
202+ green : 0 ,
203+ amber : 0 ,
204+ red : 0 ,
205+ unknown : false ,
206+ loading : false ,
207+ } ) ;
182208 } ) ;
183209
184210 it ( "not_supported_features are gray, not counted" , ( ) => {
@@ -191,7 +217,13 @@ describe("computeColumnTally", () => {
191217 } ;
192218 const t = computeColumnTally ( unsupportedInt , features , live ) ;
193219 // f1: D3=green but no D5/D6 → gray; f2: unsupported → gray
194- expect ( t ) . toEqual ( { green : 0 , amber : 0 , red : 0 , unknown : false } ) ;
220+ expect ( t ) . toEqual ( {
221+ green : 0 ,
222+ amber : 0 ,
223+ red : 0 ,
224+ unknown : false ,
225+ loading : false ,
226+ } ) ;
195227 } ) ;
196228
197229 it ( "returns unknown=true when connection is error" , ( ) => {
@@ -205,7 +237,49 @@ describe("computeColumnTally", () => {
205237
206238 it ( "returns zeros with unknown=false when no rows" , ( ) => {
207239 const t = computeColumnTally ( integration , features , new Map ( ) ) ;
208- expect ( t ) . toEqual ( { green : 0 , amber : 0 , red : 0 , unknown : false } ) ;
240+ expect ( t ) . toEqual ( {
241+ green : 0 ,
242+ amber : 0 ,
243+ red : 0 ,
244+ unknown : false ,
245+ loading : false ,
246+ } ) ;
247+ } ) ;
248+
249+ // Regression: while the initial PocketBase fetch is still in flight the
250+ // live-status map is empty AND the connection is "connecting". Returning
251+ // authoritative ✓0 ~0 ✗0 in that window reads as "everything is at depth 0",
252+ // which is a lie — the data simply hasn't arrived yet. The header must show a
253+ // loading/unknown state instead, NEVER zeros, until the first rows land.
254+ it ( "returns loading=true (unknown, not authoritative zeros) while connecting with no rows" , ( ) => {
255+ const t = computeColumnTally (
256+ integration ,
257+ features ,
258+ new Map ( ) ,
259+ "connecting" ,
260+ ) ;
261+ expect ( t . loading ) . toBe ( true ) ;
262+ expect ( t . unknown ) . toBe ( true ) ;
263+ expect ( t . green ) . toBe ( 0 ) ;
264+ expect ( t . amber ) . toBe ( 0 ) ;
265+ expect ( t . red ) . toBe ( 0 ) ;
266+ } ) ;
267+
268+ it ( "does NOT treat connecting-with-rows as loading (data already arrived)" , ( ) => {
269+ const live : LiveStatusMap = new Map ( ) ;
270+ live . set ( "e2e:i1/f1" , row ( "e2e:i1/f1" , "e2e" , "red" ) ) ;
271+ // A delta arrived during a transient reconnect: rows are present, so the
272+ // tally is authoritative even though the connection is mid-reconnect.
273+ const t = computeColumnTally ( integration , features , live , "connecting" ) ;
274+ expect ( t . loading ) . toBe ( false ) ;
275+ expect ( t . unknown ) . toBe ( false ) ;
276+ expect ( t . red ) . toBe ( 1 ) ;
277+ } ) ;
278+
279+ it ( "live with no rows is NOT loading — it is an authoritative empty result" , ( ) => {
280+ const t = computeColumnTally ( integration , features , new Map ( ) , "live" ) ;
281+ expect ( t . loading ) . toBe ( false ) ;
282+ expect ( t . unknown ) . toBe ( false ) ;
209283 } ) ;
210284
211285 it ( "amber chip when D5=green but D6 absent" , ( ) => {
@@ -236,7 +310,13 @@ describe("computeColumnTally", () => {
236310 ) ;
237311 const t = computeColumnTally ( mappedInt , mappedFeatures , mappedLive ) ;
238312 // D3=green, D4=green, D5=green → amber (D6 not yet green)
239- expect ( t ) . toEqual ( { green : 0 , amber : 1 , red : 0 , unknown : false } ) ;
313+ expect ( t ) . toEqual ( {
314+ green : 0 ,
315+ amber : 1 ,
316+ red : 0 ,
317+ unknown : false ,
318+ loading : false ,
319+ } ) ;
240320 } ) ;
241321} ) ;
242322
@@ -340,3 +420,75 @@ describe("FeatureGrid — Starter row-group", () => {
340420 expect ( cell . textContent ) . toContain ( "~" ) ;
341421 } ) ;
342422} ) ;
423+
424+ /* ------------------------------------------------------------------ */
425+ /* OverlayColumnHeader — loading / offline rendering (spec §5.3, A5) */
426+ /* */
427+ /* The core §5.3 guarantee: during the initial-load window the header */
428+ /* must NOT render authoritative ✓0 ~0 ✗0 (which reads as "every cell */
429+ /* at depth 0" — a lie). It shows a "… loading" affordance while the */
430+ /* first signal is in flight, and "? offline" when the stream is down. */
431+ /* ------------------------------------------------------------------ */
432+
433+ describe ( "OverlayColumnHeader — loading / offline rendering (§5.3)" , ( ) => {
434+ const integration : Integration = {
435+ slug : "i1" ,
436+ name : "Integration One" ,
437+ category : "c" ,
438+ language : "ts" ,
439+ description : "" ,
440+ repo : "" ,
441+ backend_url : "https://x" ,
442+ deployed : true ,
443+ features : [ "f1" ] ,
444+ demos : [ { id : "f1" , name : "f1" , description : "" , tags : [ ] } ] ,
445+ } ;
446+
447+ const HEALTH : Set < Overlay > = new Set < Overlay > ( [ "health" ] ) ;
448+
449+ it ( "loading tally → '… loading' affordance, NOT authoritative zero counts" , ( ) => {
450+ const { getByText, queryByText } = render (
451+ < OverlayColumnHeader
452+ integration = { integration }
453+ tally = { { green : 0 , amber : 0 , red : 0 , unknown : true , loading : true } }
454+ overlays = { HEALTH }
455+ /> ,
456+ ) ;
457+ // The loading affordance renders.
458+ expect ( getByText ( / l o a d i n g / ) ) . toBeDefined ( ) ;
459+ // The authoritative zero glyphs must NOT render during loading.
460+ expect ( queryByText ( / ✓ \s * 0 / ) ) . toBeNull ( ) ; // ✓ 0
461+ expect ( queryByText ( / ✗ \s * 0 / ) ) . toBeNull ( ) ; // ✗ 0
462+ expect ( queryByText ( / ^ ~ \s * 0 $ / ) ) . toBeNull ( ) ; // ~ 0
463+ } ) ;
464+
465+ it ( "unknown (offline) tally → '? offline' affordance, NOT authoritative zero counts" , ( ) => {
466+ const { getByText, queryByText } = render (
467+ < OverlayColumnHeader
468+ integration = { integration }
469+ tally = { { green : 0 , amber : 0 , red : 0 , unknown : true , loading : false } }
470+ overlays = { HEALTH }
471+ /> ,
472+ ) ;
473+ // The offline affordance renders.
474+ expect ( getByText ( / o f f l i n e / ) ) . toBeDefined ( ) ;
475+ // The authoritative zero glyphs must NOT render while offline.
476+ expect ( queryByText ( / ✓ \s * 0 / ) ) . toBeNull ( ) ; // ✓ 0
477+ expect ( queryByText ( / ✗ \s * 0 / ) ) . toBeNull ( ) ; // ✗ 0
478+ expect ( queryByText ( / ^ ~ \s * 0 $ / ) ) . toBeNull ( ) ; // ~ 0
479+ } ) ;
480+
481+ it ( "authoritative (not loading, not unknown) tally → renders the count glyphs" , ( ) => {
482+ const { getByText, queryByText } = render (
483+ < OverlayColumnHeader
484+ integration = { integration }
485+ tally = { { green : 2 , amber : 1 , red : 3 , unknown : false , loading : false } }
486+ overlays = { HEALTH }
487+ /> ,
488+ ) ;
489+ // Counts render; neither loading nor offline affordance is shown.
490+ expect ( getByText ( / ✓ / ) ) . toBeDefined ( ) ; // ✓
491+ expect ( queryByText ( / l o a d i n g / ) ) . toBeNull ( ) ;
492+ expect ( queryByText ( / o f f l i n e / ) ) . toBeNull ( ) ;
493+ } ) ;
494+ } ) ;
0 commit comments