@applicaster/quick-brick-core 16.0.0-rc.11 → 16.0.0-rc.110
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.
- package/App/ActionsProvider/ActionsProvider.tsx +31 -9
- package/App/ActionsProvider/LegacyActionsRegistryAdapter.tsx +123 -0
- package/App/ActionsProvider/__tests__/LegacyActionsRegistryAdapter.test.tsx +64 -0
- package/App/DeepLinking/URLSchemeHandler/SchemeHandlerHooks/__tests__/useOpenSchemeHandler.test.tsx +25 -15
- package/App/ModalProvider/ModalBottomSheet/DraggableBottomSheet/__tests__/index.test.tsx +88 -0
- package/App/ModalProvider/ModalBottomSheet/DraggableBottomSheet/index.tsx +30 -56
- package/App/ModalProvider/ModalBottomSheet/ModalBottomSheetFrame.tsx +9 -2
- package/App/ModalProvider/ModalBottomSheet/hooks/__tests__/useKeyboardHeight.android.test.ts +105 -0
- package/App/ModalProvider/ModalBottomSheet/hooks/__tests__/useKeyboardHeight.test.ts +102 -0
- package/App/ModalProvider/ModalBottomSheet/hooks/index.ts +1 -0
- package/App/ModalProvider/ModalBottomSheet/hooks/useKeyboardHeight.ts +30 -0
- package/App/ModalProvider/ModalBottomSheet/index.tsx +5 -2
- package/App/ModalProvider/ModalContent.tsx +3 -0
- package/App/ModalProvider/__tests__/ModalContent.test.tsx +39 -0
- package/App/ModalProvider/__tests__/index.test.tsx +99 -0
- package/App/ModalProvider/index.tsx +39 -14
- package/App/NavigationProvider/NavigationProvider.tsx +57 -5
- package/App/NavigationProvider/__tests__/navigationProvider.test.tsx +56 -0
- package/App/NavigationProvider/__tests__/utils.test.ts +31 -1
- package/App/NavigationProvider/utils.ts +14 -0
- package/App/NetworkStatusProvider/NetworkStatusProvider.tsx +32 -4
- package/App/NetworkStatusProvider/ResumeVerificationController.ts +282 -0
- package/App/NetworkStatusProvider/__tests__/NetworkStatusProvider.test.tsx +152 -1
- package/App/NetworkStatusProvider/__tests__/ResumeVerificationController.test.ts +471 -0
- package/App/NetworkStatusProvider/__tests__/resumeVerification.test.ts +126 -0
- package/App/NetworkStatusProvider/__tests__/useResumeVerifiedNetInfo.test.ts +189 -0
- package/App/NetworkStatusProvider/resumeVerification.ts +54 -0
- package/App/NetworkStatusProvider/useResumeVerifiedNetInfo.ts +33 -0
- package/App/NotificationToastRenderer/NotificationToastManager.ts +214 -0
- package/App/NotificationToastRenderer/NotificationToastRenderer.tsx +134 -0
- package/App/NotificationToastRenderer/NotificationToastRenderer.tv.tsx +10 -0
- package/App/NotificationToastRenderer/NotificationToastRenderer.web.tsx +61 -0
- package/App/NotificationToastRenderer/__tests__/NotificationToastManager.test.ts +237 -0
- package/App/NotificationToastRenderer/__tests__/NotificationToastRenderer.test.tsx +233 -0
- package/App/NotificationToastRenderer/__tests__/NotificationToastRenderer.web.test.tsx +90 -0
- package/App/NotificationToastRenderer/__tests__/resolveConfirmationToastStyle.test.ts +118 -0
- package/App/NotificationToastRenderer/__tests__/useNotificationHeight.test.tsx +58 -0
- package/App/NotificationToastRenderer/__tests__/useNotificationToastState.test.ts +112 -0
- package/App/NotificationToastRenderer/index.tsx +9 -0
- package/App/NotificationToastRenderer/resolveConfirmationToastStyle.ts +33 -0
- package/App/NotificationToastRenderer/useNotificationHeight.tsx +14 -0
- package/App/NotificationToastRenderer/useNotificationToastState.tsx +12 -0
- package/App/index.tsx +8 -5
- package/package.json +8 -8
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
import { AppState, Platform } from "react-native";
|
|
2
|
+
|
|
3
|
+
import { ResumeVerificationController } from "../ResumeVerificationController";
|
|
4
|
+
import { RESUME_VERIFICATION_DELAY_MS } from "../resumeVerification";
|
|
5
|
+
import { isOnline } from "../utils";
|
|
6
|
+
|
|
7
|
+
const mockLogger = {
|
|
8
|
+
warning: jest.fn(),
|
|
9
|
+
debug: jest.fn(),
|
|
10
|
+
info: jest.fn(),
|
|
11
|
+
log: jest.fn(),
|
|
12
|
+
error: jest.fn(),
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
// addSubsystem runs at module load, before mockLogger is initialised: delegate lazily
|
|
16
|
+
jest.mock("../../logger", () => ({
|
|
17
|
+
coreAppLogger: {
|
|
18
|
+
addSubsystem: () => ({
|
|
19
|
+
warning: (event) => mockLogger.warning(event),
|
|
20
|
+
debug: (event) => mockLogger.debug(event),
|
|
21
|
+
info: (event) => mockLogger.info(event),
|
|
22
|
+
log: (event) => mockLogger.log(event),
|
|
23
|
+
error: (event) => mockLogger.error(event),
|
|
24
|
+
}),
|
|
25
|
+
},
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
jest.mock("react-native/Libraries/AppState/AppState", () => {
|
|
29
|
+
const { EventEmitter } = require("events");
|
|
30
|
+
const emitter = new EventEmitter();
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
emitter,
|
|
34
|
+
currentState: "active",
|
|
35
|
+
addEventListener: (type, handler) => {
|
|
36
|
+
emitter.addListener(type, handler);
|
|
37
|
+
|
|
38
|
+
return { remove: () => emitter.removeListener(type, handler) };
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const ONLINE = {
|
|
44
|
+
type: "wifi",
|
|
45
|
+
details: { isConnectionExpensive: false },
|
|
46
|
+
isConnected: true,
|
|
47
|
+
isInternetReachable: true,
|
|
48
|
+
deviceStatus: null,
|
|
49
|
+
} as any;
|
|
50
|
+
|
|
51
|
+
const OFFLINE = {
|
|
52
|
+
type: "wifi",
|
|
53
|
+
details: { isConnectionExpensive: false },
|
|
54
|
+
isConnected: false,
|
|
55
|
+
isInternetReachable: false,
|
|
56
|
+
deviceStatus: null,
|
|
57
|
+
} as any;
|
|
58
|
+
|
|
59
|
+
const NOT_READY = {
|
|
60
|
+
type: "unknown",
|
|
61
|
+
details: null,
|
|
62
|
+
isConnected: null,
|
|
63
|
+
isInternetReachable: null,
|
|
64
|
+
deviceStatus: null,
|
|
65
|
+
} as any;
|
|
66
|
+
|
|
67
|
+
const mockAppState = AppState as any;
|
|
68
|
+
|
|
69
|
+
const changeAppState = (state: string) => {
|
|
70
|
+
mockAppState.currentState = state;
|
|
71
|
+
mockAppState.emitter.emit("change", state);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const advance = (ms: number) => jest.advanceTimersByTime(ms);
|
|
75
|
+
|
|
76
|
+
let controller: ResumeVerificationController<any> | undefined;
|
|
77
|
+
|
|
78
|
+
const setup = (initial = ONLINE) => {
|
|
79
|
+
controller = new ResumeVerificationController(initial);
|
|
80
|
+
controller.start();
|
|
81
|
+
|
|
82
|
+
return controller;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const publishedOnline = () => isOnline(controller.getState().netInfo) === true;
|
|
86
|
+
|
|
87
|
+
const infoMessages = () =>
|
|
88
|
+
mockLogger.info.mock.calls.map(([event]) => event.message);
|
|
89
|
+
|
|
90
|
+
describe("ResumeVerificationController", () => {
|
|
91
|
+
const originalOS = Platform.OS;
|
|
92
|
+
|
|
93
|
+
beforeEach(() => {
|
|
94
|
+
jest.useFakeTimers();
|
|
95
|
+
Platform.OS = "ios";
|
|
96
|
+
mockAppState.currentState = "active";
|
|
97
|
+
mockAppState.emitter.removeAllListeners();
|
|
98
|
+
jest.clearAllMocks();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
afterEach(() => {
|
|
102
|
+
controller?.stop();
|
|
103
|
+
controller = undefined;
|
|
104
|
+
jest.useRealTimers();
|
|
105
|
+
Platform.OS = originalOS;
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("passes live changes through immediately while active", () => {
|
|
109
|
+
setup(ONLINE);
|
|
110
|
+
|
|
111
|
+
controller.setNetInfo(OFFLINE);
|
|
112
|
+
expect(controller.getState().netInfo).toBe(OFFLINE);
|
|
113
|
+
|
|
114
|
+
controller.setNetInfo(ONLINE);
|
|
115
|
+
expect(controller.getState().netInfo).toBe(ONLINE);
|
|
116
|
+
expect(controller.getState().stillOfflineAfterResumeCount).toBe(0);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("ignores network changes while in background", () => {
|
|
120
|
+
setup(ONLINE);
|
|
121
|
+
|
|
122
|
+
changeAppState("background");
|
|
123
|
+
controller.setNetInfo(OFFLINE);
|
|
124
|
+
|
|
125
|
+
expect(publishedOnline()).toBe(true);
|
|
126
|
+
|
|
127
|
+
expect(infoMessages()).toContain(
|
|
128
|
+
"NetworkStatus: network change ignored until verified"
|
|
129
|
+
);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("'inactive' (Control Center, Notification Center) passes network changes through immediately", () => {
|
|
133
|
+
setup(ONLINE);
|
|
134
|
+
|
|
135
|
+
changeAppState("inactive");
|
|
136
|
+
controller.setNetInfo(OFFLINE);
|
|
137
|
+
expect(publishedOnline()).toBe(false);
|
|
138
|
+
|
|
139
|
+
changeAppState("active");
|
|
140
|
+
controller.setNetInfo(ONLINE);
|
|
141
|
+
expect(publishedOnline()).toBe(true);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("lock screen (active -> inactive -> background -> active) still verifies for 5s", () => {
|
|
145
|
+
setup(ONLINE);
|
|
146
|
+
|
|
147
|
+
changeAppState("inactive");
|
|
148
|
+
changeAppState("background");
|
|
149
|
+
changeAppState("active");
|
|
150
|
+
controller.setNetInfo(OFFLINE);
|
|
151
|
+
|
|
152
|
+
advance(RESUME_VERIFICATION_DELAY_MS - 1);
|
|
153
|
+
expect(publishedOnline()).toBe(true);
|
|
154
|
+
|
|
155
|
+
advance(1);
|
|
156
|
+
expect(publishedOnline()).toBe(false);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("'inactive' during the verification window keeps the window running", () => {
|
|
160
|
+
setup(ONLINE);
|
|
161
|
+
|
|
162
|
+
changeAppState("background");
|
|
163
|
+
changeAppState("active");
|
|
164
|
+
controller.setNetInfo(OFFLINE);
|
|
165
|
+
advance(2000);
|
|
166
|
+
|
|
167
|
+
changeAppState("inactive");
|
|
168
|
+
expect(publishedOnline()).toBe(true);
|
|
169
|
+
|
|
170
|
+
advance(RESUME_VERIFICATION_DELAY_MS - 2000);
|
|
171
|
+
expect(publishedOnline()).toBe(false);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("online -> resume with late offline blip -> stays online", () => {
|
|
175
|
+
setup(ONLINE);
|
|
176
|
+
|
|
177
|
+
changeAppState("background");
|
|
178
|
+
changeAppState("active");
|
|
179
|
+
|
|
180
|
+
controller.setNetInfo(OFFLINE); // stale reachability failure lands after 'active'
|
|
181
|
+
expect(publishedOnline()).toBe(true);
|
|
182
|
+
|
|
183
|
+
advance(2000);
|
|
184
|
+
controller.setNetInfo(ONLINE); // retry succeeds
|
|
185
|
+
advance(RESUME_VERIFICATION_DELAY_MS);
|
|
186
|
+
|
|
187
|
+
expect(publishedOnline()).toBe(true);
|
|
188
|
+
expect(controller.getState().stillOfflineAfterResumeCount).toBe(0);
|
|
189
|
+
|
|
190
|
+
expect(mockLogger.info).toHaveBeenCalledWith(
|
|
191
|
+
expect.objectContaining({
|
|
192
|
+
message: "NetworkStatus: resume verification finished",
|
|
193
|
+
data: expect.objectContaining({ decision: "still_online" }),
|
|
194
|
+
})
|
|
195
|
+
);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("blip arriving before 'active' (while background) is also ignored", () => {
|
|
199
|
+
setup(ONLINE);
|
|
200
|
+
|
|
201
|
+
changeAppState("background");
|
|
202
|
+
controller.setNetInfo(OFFLINE);
|
|
203
|
+
changeAppState("active");
|
|
204
|
+
advance(1000);
|
|
205
|
+
controller.setNetInfo(ONLINE);
|
|
206
|
+
advance(RESUME_VERIFICATION_DELAY_MS);
|
|
207
|
+
|
|
208
|
+
expect(publishedOnline()).toBe(true);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("online -> resume -> still offline after 5s -> publishes offline exactly at 5s", () => {
|
|
212
|
+
setup(ONLINE);
|
|
213
|
+
|
|
214
|
+
changeAppState("background");
|
|
215
|
+
changeAppState("active");
|
|
216
|
+
controller.setNetInfo(OFFLINE);
|
|
217
|
+
|
|
218
|
+
advance(RESUME_VERIFICATION_DELAY_MS - 1);
|
|
219
|
+
expect(publishedOnline()).toBe(true);
|
|
220
|
+
|
|
221
|
+
advance(1);
|
|
222
|
+
expect(publishedOnline()).toBe(false);
|
|
223
|
+
|
|
224
|
+
expect(mockLogger.info).toHaveBeenCalledWith(
|
|
225
|
+
expect.objectContaining({
|
|
226
|
+
data: expect.objectContaining({ decision: "went_offline" }),
|
|
227
|
+
})
|
|
228
|
+
);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("offline -> resume -> online after 5s -> publishes online only at 5s", () => {
|
|
232
|
+
setup(OFFLINE);
|
|
233
|
+
|
|
234
|
+
changeAppState("background");
|
|
235
|
+
changeAppState("active");
|
|
236
|
+
controller.setNetInfo(ONLINE);
|
|
237
|
+
expect(publishedOnline()).toBe(false);
|
|
238
|
+
|
|
239
|
+
advance(RESUME_VERIFICATION_DELAY_MS);
|
|
240
|
+
expect(publishedOnline()).toBe(true);
|
|
241
|
+
|
|
242
|
+
expect(mockLogger.info).toHaveBeenCalledWith(
|
|
243
|
+
expect.objectContaining({
|
|
244
|
+
data: expect.objectContaining({ decision: "back_online" }),
|
|
245
|
+
})
|
|
246
|
+
);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it("offline -> resume -> still offline -> increments stillOfflineAfterResumeCount", () => {
|
|
250
|
+
setup(OFFLINE);
|
|
251
|
+
|
|
252
|
+
changeAppState("background");
|
|
253
|
+
changeAppState("active");
|
|
254
|
+
advance(RESUME_VERIFICATION_DELAY_MS);
|
|
255
|
+
|
|
256
|
+
expect(publishedOnline()).toBe(false);
|
|
257
|
+
expect(controller.getState().stillOfflineAfterResumeCount).toBe(1);
|
|
258
|
+
|
|
259
|
+
changeAppState("background");
|
|
260
|
+
changeAppState("active");
|
|
261
|
+
advance(RESUME_VERIFICATION_DELAY_MS);
|
|
262
|
+
|
|
263
|
+
expect(controller.getState().stillOfflineAfterResumeCount).toBe(2);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it("going back to background mid-window cancels and restarts the 5s window", () => {
|
|
267
|
+
setup(ONLINE);
|
|
268
|
+
|
|
269
|
+
changeAppState("background");
|
|
270
|
+
changeAppState("active");
|
|
271
|
+
controller.setNetInfo(OFFLINE);
|
|
272
|
+
advance(3000);
|
|
273
|
+
|
|
274
|
+
changeAppState("background");
|
|
275
|
+
|
|
276
|
+
expect(infoMessages()).toContain(
|
|
277
|
+
"NetworkStatus: resume verification cancelled"
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
advance(RESUME_VERIFICATION_DELAY_MS); // old timer must not fire
|
|
281
|
+
expect(publishedOnline()).toBe(true);
|
|
282
|
+
|
|
283
|
+
changeAppState("active");
|
|
284
|
+
advance(RESUME_VERIFICATION_DELAY_MS - 1);
|
|
285
|
+
expect(publishedOnline()).toBe(true);
|
|
286
|
+
|
|
287
|
+
advance(1);
|
|
288
|
+
expect(publishedOnline()).toBe(false);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it("after verification ends, live changes pass through immediately again", () => {
|
|
292
|
+
setup(ONLINE);
|
|
293
|
+
|
|
294
|
+
changeAppState("background");
|
|
295
|
+
changeAppState("active");
|
|
296
|
+
advance(RESUME_VERIFICATION_DELAY_MS);
|
|
297
|
+
|
|
298
|
+
controller.setNetInfo(OFFLINE);
|
|
299
|
+
expect(publishedOnline()).toBe(false);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it("constructed while app is in background starts frozen and verifies on first active", () => {
|
|
303
|
+
mockAppState.currentState = "background";
|
|
304
|
+
setup(ONLINE);
|
|
305
|
+
|
|
306
|
+
controller.setNetInfo(OFFLINE);
|
|
307
|
+
expect(publishedOnline()).toBe(true);
|
|
308
|
+
|
|
309
|
+
changeAppState("active");
|
|
310
|
+
advance(RESUME_VERIFICATION_DELAY_MS);
|
|
311
|
+
expect(publishedOnline()).toBe(false);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it("a not-ready snapshot counts as online; offline at 5s is announced", () => {
|
|
315
|
+
setup(NOT_READY);
|
|
316
|
+
|
|
317
|
+
changeAppState("background");
|
|
318
|
+
changeAppState("active");
|
|
319
|
+
controller.setNetInfo(OFFLINE);
|
|
320
|
+
advance(RESUME_VERIFICATION_DELAY_MS);
|
|
321
|
+
|
|
322
|
+
expect(publishedOnline()).toBe(false);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it("active -> active (no background) does not open a window", () => {
|
|
326
|
+
setup(ONLINE);
|
|
327
|
+
|
|
328
|
+
changeAppState("active");
|
|
329
|
+
controller.setNetInfo(OFFLINE);
|
|
330
|
+
|
|
331
|
+
expect(publishedOnline()).toBe(false);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it("logs app state changes at info level with app state and network summaries", () => {
|
|
335
|
+
setup(ONLINE);
|
|
336
|
+
|
|
337
|
+
changeAppState("background");
|
|
338
|
+
|
|
339
|
+
expect(mockLogger.info).toHaveBeenCalledWith({
|
|
340
|
+
message: "NetworkStatus: app state changed",
|
|
341
|
+
data: expect.objectContaining({
|
|
342
|
+
appState: "background",
|
|
343
|
+
previousAppState: "active",
|
|
344
|
+
phase: "live",
|
|
345
|
+
nextPhase: "frozen",
|
|
346
|
+
published: expect.objectContaining({ online: true }),
|
|
347
|
+
live: expect.objectContaining({ online: true }),
|
|
348
|
+
}),
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it("non-native platforms pass through untouched and never subscribe", () => {
|
|
353
|
+
Platform.OS = "web" as any;
|
|
354
|
+
setup(ONLINE);
|
|
355
|
+
|
|
356
|
+
expect(mockAppState.emitter.listenerCount("change")).toBe(0);
|
|
357
|
+
|
|
358
|
+
changeAppState("background");
|
|
359
|
+
controller.setNetInfo(OFFLINE);
|
|
360
|
+
|
|
361
|
+
expect(publishedOnline()).toBe(false);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it("catches up when 'active' happened between construction and start()", () => {
|
|
365
|
+
mockAppState.currentState = "background";
|
|
366
|
+
controller = new ResumeVerificationController(ONLINE);
|
|
367
|
+
|
|
368
|
+
mockAppState.currentState = "active"; // emitted before start() subscribed
|
|
369
|
+
controller.start();
|
|
370
|
+
|
|
371
|
+
controller.setNetInfo(OFFLINE);
|
|
372
|
+
expect(publishedOnline()).toBe(true);
|
|
373
|
+
|
|
374
|
+
advance(RESUME_VERIFICATION_DELAY_MS);
|
|
375
|
+
expect(publishedOnline()).toBe(false);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it("getState keeps the same reference until something is published", () => {
|
|
379
|
+
setup(ONLINE);
|
|
380
|
+
|
|
381
|
+
const listener = jest.fn();
|
|
382
|
+
controller.subscribe(listener);
|
|
383
|
+
const before = controller.getState();
|
|
384
|
+
|
|
385
|
+
controller.setNetInfo(ONLINE); // same reference
|
|
386
|
+
expect(controller.getState()).toBe(before);
|
|
387
|
+
|
|
388
|
+
changeAppState("background");
|
|
389
|
+
controller.setNetInfo(OFFLINE);
|
|
390
|
+
expect(controller.getState()).toBe(before);
|
|
391
|
+
expect(listener).not.toHaveBeenCalled();
|
|
392
|
+
|
|
393
|
+
changeAppState("active");
|
|
394
|
+
advance(RESUME_VERIFICATION_DELAY_MS);
|
|
395
|
+
|
|
396
|
+
expect(listener).toHaveBeenCalledTimes(1);
|
|
397
|
+
expect(controller.getState()).not.toBe(before);
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
it("unsubscribe stops notifications", () => {
|
|
401
|
+
setup(ONLINE);
|
|
402
|
+
|
|
403
|
+
const listener = jest.fn();
|
|
404
|
+
const unsubscribe = controller.subscribe(listener);
|
|
405
|
+
|
|
406
|
+
unsubscribe();
|
|
407
|
+
controller.setNetInfo(OFFLINE);
|
|
408
|
+
|
|
409
|
+
expect(listener).not.toHaveBeenCalled();
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
it("stop() removes the AppState listener and pending timer; start() while active opens a fresh window", () => {
|
|
413
|
+
setup(ONLINE);
|
|
414
|
+
|
|
415
|
+
changeAppState("background");
|
|
416
|
+
changeAppState("active");
|
|
417
|
+
controller.setNetInfo(OFFLINE);
|
|
418
|
+
advance(3000);
|
|
419
|
+
|
|
420
|
+
controller.stop();
|
|
421
|
+
expect(mockAppState.emitter.listenerCount("change")).toBe(0);
|
|
422
|
+
expect(jest.getTimerCount()).toBe(0);
|
|
423
|
+
|
|
424
|
+
controller.start();
|
|
425
|
+
advance(RESUME_VERIFICATION_DELAY_MS - 1);
|
|
426
|
+
expect(publishedOnline()).toBe(true);
|
|
427
|
+
|
|
428
|
+
advance(1);
|
|
429
|
+
expect(publishedOnline()).toBe(false);
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
it("start() is idempotent (no duplicate AppState listener)", () => {
|
|
433
|
+
setup(ONLINE);
|
|
434
|
+
controller.start();
|
|
435
|
+
|
|
436
|
+
expect(mockAppState.emitter.listenerCount("change")).toBe(1);
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
it("does not log 'ignored' when nothing changed (constructed in background)", () => {
|
|
440
|
+
mockAppState.currentState = "background";
|
|
441
|
+
setup(ONLINE);
|
|
442
|
+
|
|
443
|
+
expect(infoMessages()).not.toContain(
|
|
444
|
+
"NetworkStatus: network change ignored until verified"
|
|
445
|
+
);
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
it("logs only at info level", () => {
|
|
449
|
+
setup(ONLINE);
|
|
450
|
+
|
|
451
|
+
changeAppState("background");
|
|
452
|
+
controller.setNetInfo(OFFLINE);
|
|
453
|
+
changeAppState("active");
|
|
454
|
+
advance(1000);
|
|
455
|
+
changeAppState("background");
|
|
456
|
+
changeAppState("active");
|
|
457
|
+
advance(RESUME_VERIFICATION_DELAY_MS);
|
|
458
|
+
|
|
459
|
+
expect(infoMessages()).toEqual(
|
|
460
|
+
expect.arrayContaining([
|
|
461
|
+
"NetworkStatus: app state changed",
|
|
462
|
+
"NetworkStatus: network change ignored until verified",
|
|
463
|
+
"NetworkStatus: resume verification started",
|
|
464
|
+
"NetworkStatus: resume verification cancelled",
|
|
465
|
+
"NetworkStatus: resume verification finished",
|
|
466
|
+
])
|
|
467
|
+
);
|
|
468
|
+
|
|
469
|
+
expect(mockLogger.warning).not.toHaveBeenCalled();
|
|
470
|
+
});
|
|
471
|
+
});
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { Platform } from "react-native";
|
|
2
|
+
import {
|
|
3
|
+
RESUME_VERIFICATION_DELAY_MS,
|
|
4
|
+
describeNetInfo,
|
|
5
|
+
getResumeDecision,
|
|
6
|
+
isBackgroundAppState,
|
|
7
|
+
isResumeVerificationEnabled,
|
|
8
|
+
} from "../resumeVerification";
|
|
9
|
+
|
|
10
|
+
describe("resumeVerification", () => {
|
|
11
|
+
it("uses a 5s verification delay", () => {
|
|
12
|
+
expect(RESUME_VERIFICATION_DELAY_MS).toBe(5000);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe("isBackgroundAppState", () => {
|
|
16
|
+
it.each([
|
|
17
|
+
["inactive", false],
|
|
18
|
+
["background", true],
|
|
19
|
+
["active", false],
|
|
20
|
+
["unknown", false],
|
|
21
|
+
["extension", false],
|
|
22
|
+
[null, false],
|
|
23
|
+
[undefined, false],
|
|
24
|
+
])("%s -> %s", (state, expected) => {
|
|
25
|
+
expect(isBackgroundAppState(state)).toBe(expected);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe("getResumeDecision", () => {
|
|
30
|
+
it.each([
|
|
31
|
+
[true, true, "still_online"],
|
|
32
|
+
[true, false, "went_offline"],
|
|
33
|
+
[false, true, "back_online"],
|
|
34
|
+
[false, false, "still_offline"],
|
|
35
|
+
])("wasOnline=%s isOnlineNow=%s -> %s", (was, now, expected) => {
|
|
36
|
+
expect(getResumeDecision(was, now)).toBe(expected);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe("isResumeVerificationEnabled", () => {
|
|
41
|
+
const originalOS = Platform.OS;
|
|
42
|
+
|
|
43
|
+
afterEach(() => {
|
|
44
|
+
Platform.OS = originalOS;
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it.each([
|
|
48
|
+
["ios", true],
|
|
49
|
+
["android", true],
|
|
50
|
+
["web", false],
|
|
51
|
+
["samsung_tv", false],
|
|
52
|
+
["lg_tv", false],
|
|
53
|
+
])("%s -> %s", (os, expected) => {
|
|
54
|
+
Platform.OS = os as any;
|
|
55
|
+
expect(isResumeVerificationEnabled()).toBe(expected);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it.each(["ios", "android"])(
|
|
59
|
+
"is disabled on TV platforms (%s isTV)",
|
|
60
|
+
(os) => {
|
|
61
|
+
const isTVDescriptor = Object.getOwnPropertyDescriptor(
|
|
62
|
+
Platform,
|
|
63
|
+
"isTV"
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
Platform.OS = os as any;
|
|
67
|
+
|
|
68
|
+
Object.defineProperty(Platform, "isTV", {
|
|
69
|
+
value: true,
|
|
70
|
+
configurable: true,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
expect(isResumeVerificationEnabled()).toBe(false);
|
|
75
|
+
} finally {
|
|
76
|
+
if (isTVDescriptor) {
|
|
77
|
+
Object.defineProperty(Platform, "isTV", isTVDescriptor);
|
|
78
|
+
} else {
|
|
79
|
+
delete (Platform as any).isTV;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe("describeNetInfo", () => {
|
|
87
|
+
it("summarises the status without leaking details (ip, subnet, carrier)", () => {
|
|
88
|
+
const summary = describeNetInfo({
|
|
89
|
+
type: "wifi",
|
|
90
|
+
isConnected: false,
|
|
91
|
+
isInternetReachable: false,
|
|
92
|
+
details: { ipAddress: "10.0.0.2", subnet: "255.255.255.0" },
|
|
93
|
+
deviceStatus: null,
|
|
94
|
+
} as any);
|
|
95
|
+
|
|
96
|
+
expect(summary).toEqual({
|
|
97
|
+
online: false,
|
|
98
|
+
isReady: true,
|
|
99
|
+
type: "wifi",
|
|
100
|
+
isConnected: false,
|
|
101
|
+
isInternetReachable: false,
|
|
102
|
+
deviceOnline: undefined,
|
|
103
|
+
at: expect.any(String),
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
expect(JSON.stringify(summary)).not.toContain("10.0.0.2");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("reports not-ready info as online (mirrors isOnline)", () => {
|
|
110
|
+
expect(
|
|
111
|
+
describeNetInfo({
|
|
112
|
+
type: "unknown",
|
|
113
|
+
isConnected: null,
|
|
114
|
+
isInternetReachable: null,
|
|
115
|
+
details: null,
|
|
116
|
+
} as any)
|
|
117
|
+
).toEqual(expect.objectContaining({ online: true, isReady: false }));
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("handles null input", () => {
|
|
121
|
+
expect(describeNetInfo(null)).toEqual(
|
|
122
|
+
expect.objectContaining({ online: true, isReady: false })
|
|
123
|
+
);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
});
|