@terreno/rtk 0.18.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -61,5 +61,27 @@ describe("AUTH_DEBUG enabled path", () => {
61
61
  mock.module("expo-constants", () => ({ default: { expoConfig: { extra: {} } } }));
62
62
  }
63
63
  });
64
+ it("emits AUTH_DEBUG enabled and WEBSOCKETS_DEBUG enabled messages on module load", async () => {
65
+ const debugCalls = [];
66
+ const originalDebug = console.debug;
67
+ console.debug = (...args) => {
68
+ debugCalls.push(args);
69
+ };
70
+ mock.module("expo-constants", () => ({
71
+ default: { expoConfig: { extra: { AUTH_DEBUG: "true", WEBSOCKETS_DEBUG: "true" } } },
72
+ }));
73
+ try {
74
+ const testId = `${Date.now()}-${Math.random()}`;
75
+ await import(`../constants?debug=${testId}`);
76
+ const authMsg = debugCalls.find((args) => args.some((v) => typeof v === "string" && v.includes("AUTH_DEBUG is enabled")));
77
+ const wsMsg = debugCalls.find((args) => args.some((v) => typeof v === "string" && v.includes("WEBSOCKETS_DEBUG is enabled")));
78
+ expect(authMsg).toBeDefined();
79
+ expect(wsMsg).toBeDefined();
80
+ }
81
+ finally {
82
+ console.debug = originalDebug;
83
+ mock.module("expo-constants", () => ({ default: { expoConfig: { extra: {} } } }));
84
+ }
85
+ });
64
86
  });
65
87
  //# sourceMappingURL=constants.isolated.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"constants.isolated.js","sourceRoot":"","sources":["../../src/isolated/constants.isolated.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"constants.isolated.js","sourceRoot":"","sources":["../../src/isolated/constants.isolated.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=useServerStatus.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useServerStatus.test.d.ts","sourceRoot":"","sources":["../src/useServerStatus.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,182 @@
1
+ import { afterEach, beforeEach, describe, expect, it, mock } from "bun:test";
2
+ import { configureStore } from "@reduxjs/toolkit";
3
+ import { act, renderHook } from "@testing-library/react-native";
4
+ import React from "react";
5
+ import { Provider } from "react-redux";
6
+ import { offlineReducer, setOnlineStatus } from "./offlineSlice";
7
+ import { useServerStatus } from "./useServerStatus";
8
+ const createTestStore = () => configureStore({
9
+ reducer: { offline: offlineReducer },
10
+ });
11
+ const createWrapper = (store) => {
12
+ const Wrapper = ({ children }) => React.createElement(Provider, { children, store });
13
+ return Wrapper;
14
+ };
15
+ const createMockWindow = () => {
16
+ const listeners = new Map();
17
+ return {
18
+ _dispatch: (event) => {
19
+ for (const handler of listeners.get(event) ?? []) {
20
+ handler();
21
+ }
22
+ },
23
+ _listeners: listeners,
24
+ addEventListener: (event, handler) => {
25
+ if (!listeners.has(event)) {
26
+ listeners.set(event, new Set());
27
+ }
28
+ listeners.get(event)?.add(handler);
29
+ },
30
+ removeEventListener: (event, handler) => {
31
+ listeners.get(event)?.delete(handler);
32
+ },
33
+ };
34
+ };
35
+ describe("useServerStatus", () => {
36
+ let store;
37
+ let originalFetch;
38
+ let mockWindow;
39
+ beforeEach(() => {
40
+ store = createTestStore();
41
+ originalFetch = globalThis.fetch;
42
+ mockWindow = createMockWindow();
43
+ globalThis.window = mockWindow;
44
+ });
45
+ afterEach(() => {
46
+ globalThis.fetch = originalFetch;
47
+ });
48
+ it("returns initial online state from offlineSlice", () => {
49
+ globalThis.fetch = mock(async () => new Response("ok", { status: 200 }));
50
+ const { result, unmount } = renderHook(() => useServerStatus({ skip: true }), {
51
+ wrapper: createWrapper(store),
52
+ });
53
+ expect(result.current.isOnline).toBe(true);
54
+ expect(result.current.queueLength).toBe(0);
55
+ expect(result.current.isSyncing).toBe(false);
56
+ unmount();
57
+ });
58
+ it("skips polling when skip is true", async () => {
59
+ const fetchFn = mock(async () => new Response("ok", { status: 200 }));
60
+ globalThis.fetch = fetchFn;
61
+ const { unmount } = renderHook(() => useServerStatus({ skip: true }), {
62
+ wrapper: createWrapper(store),
63
+ });
64
+ await act(async () => {
65
+ await new Promise((resolve) => setTimeout(resolve, 50));
66
+ });
67
+ expect(fetchFn).not.toHaveBeenCalled();
68
+ unmount();
69
+ });
70
+ it("dispatches online status when health check succeeds", async () => {
71
+ store.dispatch(setOnlineStatus(false));
72
+ const fetchFn = mock(async () => new Response("ok", { status: 200 }));
73
+ globalThis.fetch = fetchFn;
74
+ const { unmount } = renderHook(() => useServerStatus({
75
+ healthUrl: "http://localhost:4000/health",
76
+ pollIntervalMs: 60000,
77
+ }), { wrapper: createWrapper(store) });
78
+ await act(async () => {
79
+ await new Promise((resolve) => setTimeout(resolve, 100));
80
+ });
81
+ expect(fetchFn).toHaveBeenCalled();
82
+ expect(store.getState().offline.isOnline).toBe(true);
83
+ unmount();
84
+ });
85
+ it("dispatches offline status when health check returns non-ok", async () => {
86
+ const fetchFn = mock(async () => new Response("error", { status: 500 }));
87
+ globalThis.fetch = fetchFn;
88
+ const { unmount } = renderHook(() => useServerStatus({
89
+ healthUrl: "http://localhost:4000/health",
90
+ pollIntervalMs: 60000,
91
+ }), { wrapper: createWrapper(store) });
92
+ await act(async () => {
93
+ await new Promise((resolve) => setTimeout(resolve, 100));
94
+ });
95
+ expect(fetchFn).toHaveBeenCalled();
96
+ expect(store.getState().offline.isOnline).toBe(false);
97
+ unmount();
98
+ });
99
+ it("dispatches offline status when health check throws", async () => {
100
+ const fetchFn = mock(async () => {
101
+ throw new Error("Network error");
102
+ });
103
+ globalThis.fetch = fetchFn;
104
+ const { unmount } = renderHook(() => useServerStatus({
105
+ healthUrl: "http://localhost:4000/health",
106
+ pollIntervalMs: 60000,
107
+ }), { wrapper: createWrapper(store) });
108
+ await act(async () => {
109
+ await new Promise((resolve) => setTimeout(resolve, 100));
110
+ });
111
+ expect(fetchFn).toHaveBeenCalled();
112
+ expect(store.getState().offline.isOnline).toBe(false);
113
+ unmount();
114
+ });
115
+ it("does not change status when health succeeds and already online", async () => {
116
+ const fetchFn = mock(async () => new Response("ok", { status: 200 }));
117
+ globalThis.fetch = fetchFn;
118
+ const { unmount } = renderHook(() => useServerStatus({
119
+ healthUrl: "http://localhost:4000/health",
120
+ pollIntervalMs: 60000,
121
+ }), { wrapper: createWrapper(store) });
122
+ await act(async () => {
123
+ await new Promise((resolve) => setTimeout(resolve, 100));
124
+ });
125
+ expect(fetchFn).toHaveBeenCalled();
126
+ expect(store.getState().offline.isOnline).toBe(true);
127
+ unmount();
128
+ });
129
+ it("accepts custom health URL", async () => {
130
+ const fetchFn = mock(async () => new Response("ok", { status: 200 }));
131
+ globalThis.fetch = fetchFn;
132
+ const { unmount } = renderHook(() => useServerStatus({
133
+ healthUrl: "http://custom-api:3000/ping",
134
+ pollIntervalMs: 60000,
135
+ skip: false,
136
+ }), { wrapper: createWrapper(store) });
137
+ await act(async () => {
138
+ await new Promise((resolve) => setTimeout(resolve, 100));
139
+ });
140
+ expect(fetchFn).toHaveBeenCalled();
141
+ const callUrl = fetchFn.mock.calls[0][0];
142
+ expect(callUrl).toBe("http://custom-api:3000/ping");
143
+ unmount();
144
+ });
145
+ it("dispatches offline when browser fires offline event", async () => {
146
+ const fetchFn = mock(async () => new Response("ok", { status: 200 }));
147
+ globalThis.fetch = fetchFn;
148
+ const { unmount } = renderHook(() => useServerStatus({
149
+ healthUrl: "http://localhost:4000/health",
150
+ pollIntervalMs: 60000,
151
+ }), { wrapper: createWrapper(store) });
152
+ await act(async () => {
153
+ await new Promise((resolve) => setTimeout(resolve, 50));
154
+ });
155
+ expect(store.getState().offline.isOnline).toBe(true);
156
+ act(() => {
157
+ mockWindow._dispatch("offline");
158
+ });
159
+ expect(store.getState().offline.isOnline).toBe(false);
160
+ unmount();
161
+ });
162
+ it("triggers health check when browser fires online event", async () => {
163
+ store.dispatch(setOnlineStatus(false));
164
+ const fetchFn = mock(async () => new Response("ok", { status: 200 }));
165
+ globalThis.fetch = fetchFn;
166
+ const { unmount } = renderHook(() => useServerStatus({
167
+ healthUrl: "http://localhost:4000/health",
168
+ pollIntervalMs: 60000,
169
+ }), { wrapper: createWrapper(store) });
170
+ await act(async () => {
171
+ await new Promise((resolve) => setTimeout(resolve, 100));
172
+ });
173
+ fetchFn.mockClear();
174
+ await act(async () => {
175
+ mockWindow._dispatch("online");
176
+ await new Promise((resolve) => setTimeout(resolve, 100));
177
+ });
178
+ expect(fetchFn).toHaveBeenCalled();
179
+ unmount();
180
+ });
181
+ });
182
+ //# sourceMappingURL=useServerStatus.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useServerStatus.test.js","sourceRoot":"","sources":["../src/useServerStatus.test.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "@better-auth/expo": "^1.2.8",
4
4
  "@react-native-async-storage/async-storage": "2.2.0",
5
5
  "@reduxjs/toolkit": "^2.11.1",
6
- "@terreno/ui": "0.18.0",
6
+ "@terreno/ui": "0.19.0",
7
7
  "async-mutex": "^0.5.0",
8
8
  "axios": "^1.13.2",
9
9
  "axios-retry": "^4.5.0",
@@ -67,5 +67,5 @@
67
67
  "test:coverage": "bun run ../scripts/check-coverage.ts"
68
68
  },
69
69
  "types": "dist/index.d.ts",
70
- "version": "0.18.0"
70
+ "version": "0.19.0"
71
71
  }
@@ -63,4 +63,30 @@ describe("AUTH_DEBUG enabled path", () => {
63
63
  mock.module("expo-constants", () => ({default: {expoConfig: {extra: {}}}}));
64
64
  }
65
65
  });
66
+
67
+ it("emits AUTH_DEBUG enabled and WEBSOCKETS_DEBUG enabled messages on module load", async () => {
68
+ const debugCalls: unknown[][] = [];
69
+ const originalDebug = console.debug;
70
+ console.debug = (...args: unknown[]): void => {
71
+ debugCalls.push(args);
72
+ };
73
+ mock.module("expo-constants", () => ({
74
+ default: {expoConfig: {extra: {AUTH_DEBUG: "true", WEBSOCKETS_DEBUG: "true"}}},
75
+ }));
76
+ try {
77
+ const testId = `${Date.now()}-${Math.random()}`;
78
+ await import(`../constants?debug=${testId}`);
79
+ const authMsg = debugCalls.find((args) =>
80
+ args.some((v) => typeof v === "string" && v.includes("AUTH_DEBUG is enabled"))
81
+ );
82
+ const wsMsg = debugCalls.find((args) =>
83
+ args.some((v) => typeof v === "string" && v.includes("WEBSOCKETS_DEBUG is enabled"))
84
+ );
85
+ expect(authMsg).toBeDefined();
86
+ expect(wsMsg).toBeDefined();
87
+ } finally {
88
+ console.debug = originalDebug;
89
+ mock.module("expo-constants", () => ({default: {expoConfig: {extra: {}}}}));
90
+ }
91
+ });
66
92
  });
@@ -0,0 +1,266 @@
1
+ import {afterEach, beforeEach, describe, expect, it, mock} from "bun:test";
2
+ import {configureStore} from "@reduxjs/toolkit";
3
+ import {act, renderHook} from "@testing-library/react-native";
4
+ import React from "react";
5
+ import {Provider} from "react-redux";
6
+
7
+ import {offlineReducer, setOnlineStatus} from "./offlineSlice";
8
+ import {useServerStatus} from "./useServerStatus";
9
+
10
+ const createTestStore = () =>
11
+ configureStore({
12
+ reducer: {offline: offlineReducer},
13
+ });
14
+
15
+ const createWrapper = (store: ReturnType<typeof createTestStore>) => {
16
+ const Wrapper: React.FC<{children: React.ReactNode}> = ({children}) =>
17
+ React.createElement(Provider, {children, store});
18
+ return Wrapper;
19
+ };
20
+
21
+ interface MockWindow {
22
+ addEventListener: (event: string, handler: () => void) => void;
23
+ removeEventListener: (event: string, handler: () => void) => void;
24
+ _listeners: Map<string, Set<() => void>>;
25
+ _dispatch: (event: string) => void;
26
+ }
27
+
28
+ const createMockWindow = (): MockWindow => {
29
+ const listeners = new Map<string, Set<() => void>>();
30
+ return {
31
+ _dispatch: (event: string) => {
32
+ for (const handler of listeners.get(event) ?? []) {
33
+ handler();
34
+ }
35
+ },
36
+ _listeners: listeners,
37
+ addEventListener: (event: string, handler: () => void) => {
38
+ if (!listeners.has(event)) {
39
+ listeners.set(event, new Set());
40
+ }
41
+ listeners.get(event)?.add(handler);
42
+ },
43
+ removeEventListener: (event: string, handler: () => void) => {
44
+ listeners.get(event)?.delete(handler);
45
+ },
46
+ };
47
+ };
48
+
49
+ describe("useServerStatus", () => {
50
+ let store: ReturnType<typeof createTestStore>;
51
+ let originalFetch: typeof globalThis.fetch;
52
+ let mockWindow: MockWindow;
53
+
54
+ beforeEach(() => {
55
+ store = createTestStore();
56
+ originalFetch = globalThis.fetch;
57
+ mockWindow = createMockWindow();
58
+ (globalThis as Record<string, unknown>).window = mockWindow;
59
+ });
60
+
61
+ afterEach(() => {
62
+ globalThis.fetch = originalFetch;
63
+ });
64
+
65
+ it("returns initial online state from offlineSlice", () => {
66
+ globalThis.fetch = mock(
67
+ async () => new Response("ok", {status: 200})
68
+ ) as unknown as typeof fetch;
69
+
70
+ const {result, unmount} = renderHook(() => useServerStatus({skip: true}), {
71
+ wrapper: createWrapper(store),
72
+ });
73
+
74
+ expect(result.current.isOnline).toBe(true);
75
+ expect(result.current.queueLength).toBe(0);
76
+ expect(result.current.isSyncing).toBe(false);
77
+ unmount();
78
+ });
79
+
80
+ it("skips polling when skip is true", async () => {
81
+ const fetchFn = mock(async () => new Response("ok", {status: 200}));
82
+ globalThis.fetch = fetchFn as unknown as typeof fetch;
83
+
84
+ const {unmount} = renderHook(() => useServerStatus({skip: true}), {
85
+ wrapper: createWrapper(store),
86
+ });
87
+
88
+ await act(async () => {
89
+ await new Promise((resolve) => setTimeout(resolve, 50));
90
+ });
91
+
92
+ expect(fetchFn).not.toHaveBeenCalled();
93
+ unmount();
94
+ });
95
+
96
+ it("dispatches online status when health check succeeds", async () => {
97
+ store.dispatch(setOnlineStatus(false));
98
+ const fetchFn = mock(async () => new Response("ok", {status: 200}));
99
+ globalThis.fetch = fetchFn as unknown as typeof fetch;
100
+
101
+ const {unmount} = renderHook(
102
+ () =>
103
+ useServerStatus({
104
+ healthUrl: "http://localhost:4000/health",
105
+ pollIntervalMs: 60_000,
106
+ }),
107
+ {wrapper: createWrapper(store)}
108
+ );
109
+
110
+ await act(async () => {
111
+ await new Promise((resolve) => setTimeout(resolve, 100));
112
+ });
113
+
114
+ expect(fetchFn).toHaveBeenCalled();
115
+ expect(store.getState().offline.isOnline).toBe(true);
116
+ unmount();
117
+ });
118
+
119
+ it("dispatches offline status when health check returns non-ok", async () => {
120
+ const fetchFn = mock(async () => new Response("error", {status: 500}));
121
+ globalThis.fetch = fetchFn as unknown as typeof fetch;
122
+
123
+ const {unmount} = renderHook(
124
+ () =>
125
+ useServerStatus({
126
+ healthUrl: "http://localhost:4000/health",
127
+ pollIntervalMs: 60_000,
128
+ }),
129
+ {wrapper: createWrapper(store)}
130
+ );
131
+
132
+ await act(async () => {
133
+ await new Promise((resolve) => setTimeout(resolve, 100));
134
+ });
135
+
136
+ expect(fetchFn).toHaveBeenCalled();
137
+ expect(store.getState().offline.isOnline).toBe(false);
138
+ unmount();
139
+ });
140
+
141
+ it("dispatches offline status when health check throws", async () => {
142
+ const fetchFn = mock(async () => {
143
+ throw new Error("Network error");
144
+ });
145
+ globalThis.fetch = fetchFn as unknown as typeof fetch;
146
+
147
+ const {unmount} = renderHook(
148
+ () =>
149
+ useServerStatus({
150
+ healthUrl: "http://localhost:4000/health",
151
+ pollIntervalMs: 60_000,
152
+ }),
153
+ {wrapper: createWrapper(store)}
154
+ );
155
+
156
+ await act(async () => {
157
+ await new Promise((resolve) => setTimeout(resolve, 100));
158
+ });
159
+
160
+ expect(fetchFn).toHaveBeenCalled();
161
+ expect(store.getState().offline.isOnline).toBe(false);
162
+ unmount();
163
+ });
164
+
165
+ it("does not change status when health succeeds and already online", async () => {
166
+ const fetchFn = mock(async () => new Response("ok", {status: 200}));
167
+ globalThis.fetch = fetchFn as unknown as typeof fetch;
168
+
169
+ const {unmount} = renderHook(
170
+ () =>
171
+ useServerStatus({
172
+ healthUrl: "http://localhost:4000/health",
173
+ pollIntervalMs: 60_000,
174
+ }),
175
+ {wrapper: createWrapper(store)}
176
+ );
177
+
178
+ await act(async () => {
179
+ await new Promise((resolve) => setTimeout(resolve, 100));
180
+ });
181
+
182
+ expect(fetchFn).toHaveBeenCalled();
183
+ expect(store.getState().offline.isOnline).toBe(true);
184
+ unmount();
185
+ });
186
+
187
+ it("accepts custom health URL", async () => {
188
+ const fetchFn = mock(async () => new Response("ok", {status: 200}));
189
+ globalThis.fetch = fetchFn as unknown as typeof fetch;
190
+
191
+ const {unmount} = renderHook(
192
+ () =>
193
+ useServerStatus({
194
+ healthUrl: "http://custom-api:3000/ping",
195
+ pollIntervalMs: 60_000,
196
+ skip: false,
197
+ }),
198
+ {wrapper: createWrapper(store)}
199
+ );
200
+
201
+ await act(async () => {
202
+ await new Promise((resolve) => setTimeout(resolve, 100));
203
+ });
204
+
205
+ expect(fetchFn).toHaveBeenCalled();
206
+ const callUrl = (fetchFn.mock.calls[0] as unknown[])[0] as string;
207
+ expect(callUrl).toBe("http://custom-api:3000/ping");
208
+ unmount();
209
+ });
210
+
211
+ it("dispatches offline when browser fires offline event", async () => {
212
+ const fetchFn = mock(async () => new Response("ok", {status: 200}));
213
+ globalThis.fetch = fetchFn as unknown as typeof fetch;
214
+
215
+ const {unmount} = renderHook(
216
+ () =>
217
+ useServerStatus({
218
+ healthUrl: "http://localhost:4000/health",
219
+ pollIntervalMs: 60_000,
220
+ }),
221
+ {wrapper: createWrapper(store)}
222
+ );
223
+
224
+ await act(async () => {
225
+ await new Promise((resolve) => setTimeout(resolve, 50));
226
+ });
227
+
228
+ expect(store.getState().offline.isOnline).toBe(true);
229
+
230
+ act(() => {
231
+ mockWindow._dispatch("offline");
232
+ });
233
+
234
+ expect(store.getState().offline.isOnline).toBe(false);
235
+ unmount();
236
+ });
237
+
238
+ it("triggers health check when browser fires online event", async () => {
239
+ store.dispatch(setOnlineStatus(false));
240
+ const fetchFn = mock(async () => new Response("ok", {status: 200}));
241
+ globalThis.fetch = fetchFn as unknown as typeof fetch;
242
+
243
+ const {unmount} = renderHook(
244
+ () =>
245
+ useServerStatus({
246
+ healthUrl: "http://localhost:4000/health",
247
+ pollIntervalMs: 60_000,
248
+ }),
249
+ {wrapper: createWrapper(store)}
250
+ );
251
+
252
+ await act(async () => {
253
+ await new Promise((resolve) => setTimeout(resolve, 100));
254
+ });
255
+
256
+ fetchFn.mockClear();
257
+
258
+ await act(async () => {
259
+ mockWindow._dispatch("online");
260
+ await new Promise((resolve) => setTimeout(resolve, 100));
261
+ });
262
+
263
+ expect(fetchFn).toHaveBeenCalled();
264
+ unmount();
265
+ });
266
+ });