1
2
3
4
5
14import { describe, expect, it } from "vitest";
15import {
16 RECONNECT_BANNER_THRESHOLD_MS,
17 computeOfflineStatus,
18 normalizePhase,
19 type OfflineStatusInput,
20} from "../offline-status";
21
22const base: OfflineStatusInput = {
23 navigatorOnline: true,
24 phase: "online",
25 phaseSinceMs: 0,
26 nowMs: 0,
27 crdtPending: 0,
28 outboxPending: 0,
29 outboxFailed: 0,
30};
31
32describe("normalizePhase", () => {
33 it("maps the legacy JSON client + the Yjs provider statuses onto the 3 phases", () => {
34 expect(normalizePhase("connected")).toBe("online");
35 expect(normalizePhase("syncing")).toBe("online");
36 expect(normalizePhase("reconnecting")).toBe("reconnecting");
37 expect(normalizePhase("connecting")).toBe("reconnecting");
38 expect(normalizePhase("offline")).toBe("offline");
39 expect(normalizePhase("closed")).toBe("offline");
40 expect(normalizePhase("disconnected")).toBe("offline");
41 expect(normalizePhase(null)).toBe("offline");
42 });
43});
44
45describe("the banner count = the CRDT + outbox SUM (the spec's 'one number')", () => {
46 it("sums the provider's CRDT-pending edits + the outbox pending into ONE number", () => {
47 const s = computeOfflineStatus({ ...base, crdtPending: 7, outboxPending: 5 });
48 expect(s.pendingCount).toBe(12);
49 });
50 it("is 0 when both are empty", () => {
51 expect(computeOfflineStatus(base).pendingCount).toBe(0);
52 });
53 it("outbox-only (today, the provider is not wired) = the outbox count", () => {
54 const s = computeOfflineStatus({ ...base, outboxPending: 3 });
55 expect(s.pendingCount).toBe(3);
56 });
57});
58
59describe("banner visibility (the 3 triggers + the 5 s blip threshold)", () => {
60 it("is VISIBLE when navigator.onLine is false", () => {
61 const s = computeOfflineStatus({ ...base, navigatorOnline: false });
62 expect(s.bannerVisible).toBe(true);
63 expect(s.chip).toBe("offline");
64 });
65 it("is VISIBLE when the provider is offline", () => {
66 const s = computeOfflineStatus({ ...base, phase: "offline" });
67 expect(s.bannerVisible).toBe(true);
68 expect(s.chip).toBe("offline");
69 });
70 it("is HIDDEN for a brief reconnecting blip (< 5 s) — the chip flashes instead", () => {
71 const s = computeOfflineStatus({
72 ...base,
73 phase: "reconnecting",
74 phaseSinceMs: 0,
75 nowMs: RECONNECT_BANNER_THRESHOLD_MS - 1,
76 });
77 expect(s.bannerVisible).toBe(false);
78 expect(s.chip).toBe("reconnecting");
79 expect(s.reconnectFlash).toBe(true);
80 });
81 it("is VISIBLE once reconnecting passes the 5 s threshold (chip stays amber)", () => {
82 const s = computeOfflineStatus({
83 ...base,
84 phase: "reconnecting",
85 phaseSinceMs: 0,
86 nowMs: RECONNECT_BANNER_THRESHOLD_MS,
87 });
88 expect(s.bannerVisible).toBe(true);
89 expect(s.chip).toBe("reconnecting");
90 expect(s.reconnectFlash).toBe(true);
91 });
92 it("is HIDDEN when online + connected + no pending (steady state)", () => {
93 const s = computeOfflineStatus({ ...base });
94 expect(s.bannerVisible).toBe(false);
95 expect(s.chip).toBe("online");
96 });
97 it("a browser-level offline forces the gray chip even if the provider says reconnecting", () => {
98 const s = computeOfflineStatus({ ...base, navigatorOnline: false, phase: "reconnecting" });
99 expect(s.chip).toBe("offline");
100 });
101});
102
103describe("the failed list + the 'Review failed changes' link", () => {
104 it("shows the review link only when the failed list is non-empty", () => {
105 expect(computeOfflineStatus({ ...base, outboxFailed: 0 }).showReviewLink).toBe(false);
106 expect(computeOfflineStatus({ ...base, outboxFailed: 2 }).showReviewLink).toBe(true);
107 expect(computeOfflineStatus({ ...base, outboxFailed: 2 }).failedCount).toBe(2);
108 });
109});
110
111describe("the a11y label ('Connection: offline, 12 changes pending')", () => {
112 it("describes the connection phase + the ONE pending number", () => {
113 const offline = computeOfflineStatus({
114 ...base,
115 navigatorOnline: false,
116 crdtPending: 7,
117 outboxPending: 5,
118 });
119 expect(offline.ariaLabel).toBe("Connection: Offline, 12 changes pending");
120 const live = computeOfflineStatus(base);
121 expect(live.ariaLabel).toBe("Connection: Live, 0 changes pending");
122 });
123});
124