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