@applicaster/quick-brick-core 16.0.0-rc.99 → 17.0.0-alpha.1984136194

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.
Files changed (50) hide show
  1. package/App/ActionSetters/index.ts +2 -2
  2. package/App/ActionsProvider/ActionsProvider.tsx +1 -1
  3. package/App/ActionsProvider/LegacyActionsRegistryAdapter.tsx +17 -1
  4. package/App/ActionsProvider/__tests__/LegacyActionsRegistryAdapter.test.tsx +64 -0
  5. package/App/AssetCache/logger.ts +1 -1
  6. package/App/DeepLinking/SamsungAppControl/__tests__/samsungAppControl.test.ts +255 -0
  7. package/App/DeepLinking/SamsungAppControl/index.ts +223 -0
  8. package/App/DeepLinking/URLSchemeHandler/SchemeHandlerHooks/usePresentSchemeHandler.ts +3 -0
  9. package/App/DeepLinking/URLSchemeListener/URLSchemeContextProvider.tsx +31 -1
  10. package/App/DeepLinking/logger.ts +1 -1
  11. package/App/ErrorBoundary/index.tsx +5 -2
  12. package/App/NavigationProvider/NavigationProvider.tsx +17 -11
  13. package/App/NavigationProvider/__tests__/navToolbox.test.ts +227 -0
  14. package/App/NavigationProvider/__tests__/navigationProvider.test.tsx +56 -0
  15. package/App/NavigationProvider/navToolbox.ts +224 -0
  16. package/App/NavigationProvider/utils.ts +23 -2
  17. package/App/NetworkStatusProvider/NetworkStatusProvider.tsx +40 -4
  18. package/App/NetworkStatusProvider/ResumeVerificationController.ts +282 -0
  19. package/App/NetworkStatusProvider/__tests__/NetworkStatusProvider.test.tsx +167 -1
  20. package/App/NetworkStatusProvider/__tests__/ResumeVerificationController.test.ts +486 -0
  21. package/App/NetworkStatusProvider/__tests__/resumeVerification.test.ts +126 -0
  22. package/App/NetworkStatusProvider/__tests__/useResumeVerifiedNetInfo.test.ts +204 -0
  23. package/App/NetworkStatusProvider/resumeVerification.ts +54 -0
  24. package/App/NetworkStatusProvider/useResumeVerifiedNetInfo.ts +33 -0
  25. package/App/ThemeManager/index.tsx +6 -1
  26. package/App/appLifeCycleManager/index.tsx +2 -2
  27. package/App/appRemoteDataLoader/index.tsx +2 -2
  28. package/App/appRemoteDataLoader/remoteDataLoader.ts +2 -2
  29. package/App/index.tsx +8 -2
  30. package/App/layoutPreview/LayoutPreviewReminder.tsx +136 -0
  31. package/App/layoutPreview/__tests__/persistence.test.ts +63 -0
  32. package/App/layoutPreview/persistence.ts +228 -0
  33. package/App/logger.js +2 -2
  34. package/App/remoteContextReloader/__tests__/getNativeRemoteContextData.test.ts +148 -0
  35. package/App/remoteContextReloader/__tests__/getPromiseForType.test.js +31 -0
  36. package/App/remoteContextReloader/getRemoteContextData/getNativeRemoteContextData.ts +107 -29
  37. package/App/remoteContextReloader/helpers.ts +2 -1
  38. package/App/uiActions/index.ts +13 -0
  39. package/App/uiActions/muteUnmute/__tests__/assetResolver.test.ts +226 -0
  40. package/App/uiActions/muteUnmute/__tests__/binding.test.ts +51 -0
  41. package/App/uiActions/muteUnmute/__tests__/defaultAssets.test.ts +47 -0
  42. package/App/uiActions/muteUnmute/__tests__/entryState.test.ts +57 -0
  43. package/App/uiActions/muteUnmute/__tests__/labelResolver.test.ts +35 -0
  44. package/App/uiActions/muteUnmute/assetResolver.ts +87 -0
  45. package/App/uiActions/muteUnmute/defaultAssets.ts +44 -0
  46. package/App/uiActions/muteUnmute/entryState.ts +28 -0
  47. package/App/uiActions/muteUnmute/index.ts +79 -0
  48. package/App/uiActions/muteUnmute/labelResolver.ts +15 -0
  49. package/App/uiActions/muteUnmute/types.ts +18 -0
  50. package/package.json +8 -8
@@ -13,6 +13,12 @@ import {
13
13
  } from "./utils";
14
14
  import { usePrevious } from "@applicaster/zapp-react-native-utils/reactHooks/utils";
15
15
 
16
+ import { useResumeVerifiedNetInfo } from "./useResumeVerifiedNetInfo";
17
+ import { describeNetInfo } from "./resumeVerification";
18
+ import { subsystemLogger } from "@applicaster/zapp-react-native-utils/logger";
19
+
20
+ const logger = subsystemLogger("network/reachability", "NetworkStatusProvider");
21
+
16
22
  // Use default netInfo config for all platforms (important for web platform which otherwise
17
23
  // will use backend root which is not responding for HEAD request)
18
24
  if (["web", "lg_tv", "samsung_tv"].includes(Platform.OS)) {
@@ -36,10 +42,30 @@ export const NetworkStatusProvider = ({
36
42
  // NetInfo library does not have accurate information for Tizen SmartTVs, and possibly WebOS
37
43
  // This combined info helps us gather data from the native connection status APIs
38
44
  // and includes it in our definition of... online.
39
- const combinedNetInfo: NetInfoState & { deviceStatus?: NetStatus } = {
40
- ...currentNetInfo,
41
- deviceStatus,
42
- };
45
+ // stable reference between real changes: every new reference is a network update
46
+ const liveNetInfo = React.useMemo<
47
+ NetInfoState & { deviceStatus?: NetStatus }
48
+ >(
49
+ () => ({ ...currentNetInfo, deviceStatus }),
50
+ [currentNetInfo, deviceStatus]
51
+ );
52
+
53
+ // NetInfo reports stale offline status right after resume/unlock (iOS),
54
+ // publish the network status verified after the app returns to foreground
55
+ const { netInfo: verifiedNetInfo, stillOfflineAfterResumeCount } =
56
+ useResumeVerifiedNetInfo(liveNetInfo);
57
+
58
+ // Memoized so consumers only re-render when the published status changes,
59
+ // not on every render of this provider
60
+ const combinedNetInfo = React.useMemo<
61
+ NetInfoState & {
62
+ deviceStatus?: NetStatus;
63
+ stillOfflineAfterResumeCount: number;
64
+ }
65
+ >(
66
+ () => ({ ...verifiedNetInfo, stillOfflineAfterResumeCount }),
67
+ [verifiedNetInfo, stillOfflineAfterResumeCount]
68
+ );
43
69
 
44
70
  const { onConnectionTypeChanged, onConnectionLost, onConnectionRestored } =
45
71
  React.useMemo<NetworkHooks>(() => getNetworkHooks(plugins), []);
@@ -57,6 +83,16 @@ export const NetworkStatusProvider = ({
57
83
  if (!combinedNetInfo.details) return;
58
84
 
59
85
  if (previousOnline !== undefined && previousOnline !== online) {
86
+ logger.info({
87
+ message: "NetworkStatus: online status changed",
88
+ data: {
89
+ online,
90
+ previousOnline,
91
+ published: describeNetInfo(combinedNetInfo),
92
+ live: describeNetInfo(liveNetInfo),
93
+ },
94
+ });
95
+
60
96
  online
61
97
  ? onConnectionRestored &&
62
98
  callAllWith(onConnectionRestored, combinedNetInfo)
@@ -0,0 +1,282 @@
1
+ import { AppState, AppStateStatus } from "react-native";
2
+ import { NetInfoState } from "@react-native-community/netinfo";
3
+
4
+ import { isOnline } from "./utils";
5
+ import {
6
+ RESUME_VERIFICATION_DELAY_MS,
7
+ describeNetInfo,
8
+ getResumeDecision,
9
+ isBackgroundAppState,
10
+ isResumeVerificationEnabled,
11
+ } from "./resumeVerification";
12
+ import { subsystemLogger } from "@applicaster/zapp-react-native-utils/logger";
13
+
14
+ const logger = subsystemLogger("network/reachability", "NetworkStatusProvider");
15
+
16
+ export type ConnectionInfo = NetInfoState & { deviceStatus?: NetStatus };
17
+
18
+ export type ResumeVerifiedState<T extends ConnectionInfo> = {
19
+ netInfo: T;
20
+ stillOfflineAfterResumeCount: number;
21
+ };
22
+
23
+ type Phase = "live" | "frozen" | "verifying";
24
+
25
+ /**
26
+ * Holds the published network status while the app is in background and for
27
+ * RESUME_VERIFICATION_DELAY_MS after it becomes active again, then publishes the
28
+ * current state. Works around stale iOS NetInfo reachability failures on resume/unlock.
29
+ *
30
+ * start()/stop() manage the AppState subscription and timer (useLayoutEffect),
31
+ * setNetInfo() feeds live NetInfo, subscribe()/getState() back useSyncExternalStore.
32
+ */
33
+ export class ResumeVerificationController<T extends ConnectionInfo> {
34
+ private phase: Phase = "live";
35
+ private appState: AppStateStatus;
36
+ private live: T;
37
+ private state: ResumeVerifiedState<T>;
38
+ private listeners = new Set<() => void>();
39
+ private subscription: { remove: () => void } | null = null;
40
+ private timer: ReturnType<typeof setTimeout> | null = null;
41
+
42
+ /**
43
+ * @param initialNetInfo - live NetInfo at mount, published as the initial state.
44
+ * Starts "frozen" when the app is created in background (e.g. iOS background launch).
45
+ */
46
+ constructor(initialNetInfo: T) {
47
+ this.appState = AppState.currentState;
48
+ this.live = initialNetInfo;
49
+ this.state = { netInfo: initialNetInfo, stillOfflineAfterResumeCount: 0 };
50
+
51
+ if (isResumeVerificationEnabled() && isBackgroundAppState(this.appState)) {
52
+ this.phase = "frozen";
53
+ }
54
+
55
+ this.subscribe = this.subscribe.bind(this);
56
+ this.getState = this.getState.bind(this);
57
+ this.applyAppState = this.applyAppState.bind(this);
58
+ }
59
+
60
+ /**
61
+ * useSyncExternalStore subscribe: listener is called whenever getState() changes.
62
+ * @param listener - called with no arguments after each publish
63
+ * @returns unsubscribe function
64
+ */
65
+ subscribe(listener: () => void): () => void {
66
+ this.listeners.add(listener);
67
+
68
+ return () => {
69
+ this.listeners.delete(listener);
70
+ };
71
+ }
72
+
73
+ /**
74
+ * useSyncExternalStore snapshot: same reference until a new state is published.
75
+ * @returns the verified network status and the still-offline-after-resume counter
76
+ */
77
+ getState(): ResumeVerifiedState<T> {
78
+ return this.state;
79
+ }
80
+
81
+ /**
82
+ * Subscribes to AppState changes and catches up with the current app state.
83
+ * Idempotent; no-op on platforms without resume verification (web, TV).
84
+ * Call from useLayoutEffect, paired with stop().
85
+ */
86
+ start(): void {
87
+ if (!isResumeVerificationEnabled() || this.subscription) return;
88
+
89
+ this.subscription = AppState.addEventListener("change", this.applyAppState);
90
+
91
+ // AppState may have changed since construction (e.g. initial "background"
92
+ // corrected to "active"), or stop() interrupted a window: catch up
93
+ this.applyAppState(AppState.currentState);
94
+ }
95
+
96
+ /**
97
+ * Removes the AppState subscription and cancels a pending verification.
98
+ * An interrupted window falls back to "frozen" and restarts on the next start() while active.
99
+ */
100
+ stop(): void {
101
+ this.subscription?.remove();
102
+ this.subscription = null;
103
+ this.clearTimer();
104
+
105
+ // an interrupted window is restarted by the next start() while active
106
+ if (this.phase === "verifying") {
107
+ this.phase = "frozen";
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Feeds the latest live NetInfo. Published immediately while "live",
113
+ * held (and logged as ignored) while "frozen" or "verifying".
114
+ * @param netInfo - live NetInfo; the same reference is a no-op
115
+ */
116
+ setNetInfo(netInfo: T): void {
117
+ if (netInfo === this.live) return;
118
+
119
+ this.live = netInfo;
120
+
121
+ if (this.phase === "live") {
122
+ this.publish(netInfo);
123
+
124
+ return;
125
+ }
126
+
127
+ logger.info({
128
+ message: "NetworkStatus: network change ignored until verified",
129
+ data: {
130
+ appState: this.appState,
131
+ phase: this.phase,
132
+ published: describeNetInfo(this.state.netInfo),
133
+ live: describeNetInfo(netInfo),
134
+ },
135
+ });
136
+ }
137
+
138
+ /**
139
+ * AppState change handler: "background" freezes, "active" after a freeze starts verification,
140
+ * anything else ("inactive", "unknown") keeps the current phase.
141
+ * @param nextAppState - new AppState value
142
+ */
143
+ private applyAppState(nextAppState: AppStateStatus): void {
144
+ const previousAppState = this.appState;
145
+ const nextPhase = this.getNextPhase(nextAppState);
146
+ this.appState = nextAppState;
147
+
148
+ if (previousAppState === nextAppState && nextPhase === this.phase) return;
149
+
150
+ logger.info({
151
+ message: "NetworkStatus: app state changed",
152
+ data: {
153
+ appState: nextAppState,
154
+ previousAppState,
155
+ phase: this.phase,
156
+ nextPhase,
157
+ published: describeNetInfo(this.state.netInfo),
158
+ live: describeNetInfo(this.live),
159
+ },
160
+ });
161
+
162
+ if (nextPhase === "frozen" && this.phase !== "frozen") {
163
+ this.freeze();
164
+ } else if (nextPhase === "verifying" && this.phase === "frozen") {
165
+ this.startVerification();
166
+ }
167
+ }
168
+
169
+ /**
170
+ * @param appState - new AppState value
171
+ * @returns the phase the controller moves to for this app state
172
+ */
173
+ private getNextPhase(appState: AppStateStatus): Phase {
174
+ if (isBackgroundAppState(appState)) return "frozen";
175
+
176
+ if (appState === "active" && this.phase === "frozen") return "verifying";
177
+
178
+ return this.phase;
179
+ }
180
+
181
+ /**
182
+ * Stops publishing live changes; cancels a running verification window.
183
+ */
184
+ private freeze(): void {
185
+ if (this.phase === "verifying") {
186
+ this.clearTimer();
187
+
188
+ logger.info({
189
+ message: "NetworkStatus: resume verification cancelled",
190
+ data: {
191
+ appState: this.appState,
192
+ snapshot: describeNetInfo(this.state.netInfo),
193
+ live: describeNetInfo(this.live),
194
+ },
195
+ });
196
+ }
197
+
198
+ this.phase = "frozen";
199
+ }
200
+
201
+ /**
202
+ * Opens the RESUME_VERIFICATION_DELAY_MS window. When it ends, compares the state published
203
+ * before background with the current live state, publishes the live state and bumps
204
+ * stillOfflineAfterResumeCount when the app is still offline.
205
+ */
206
+ private startVerification(): void {
207
+ this.phase = "verifying";
208
+
209
+ const snapshot = this.state.netInfo;
210
+ const startedAt = Date.now();
211
+
212
+ logger.info({
213
+ message: "NetworkStatus: resume verification started",
214
+ data: {
215
+ delayMs: RESUME_VERIFICATION_DELAY_MS,
216
+ appState: this.appState,
217
+ snapshot: describeNetInfo(snapshot),
218
+ live: describeNetInfo(this.live),
219
+ },
220
+ });
221
+
222
+ this.timer = setTimeout(() => {
223
+ this.timer = null;
224
+
225
+ if (this.phase !== "verifying") return;
226
+
227
+ const decision = getResumeDecision(
228
+ isOnline(snapshot) === true,
229
+ isOnline(this.live) === true
230
+ );
231
+
232
+ logger.info({
233
+ message: "NetworkStatus: resume verification finished",
234
+ data: {
235
+ decision,
236
+ elapsedMs: Date.now() - startedAt,
237
+ appState: this.appState,
238
+ snapshot: describeNetInfo(snapshot),
239
+ live: describeNetInfo(this.live),
240
+ },
241
+ });
242
+
243
+ this.phase = "live";
244
+
245
+ this.publish(
246
+ this.live,
247
+ this.state.stillOfflineAfterResumeCount +
248
+ (decision === "still_offline" ? 1 : 0)
249
+ );
250
+ }, RESUME_VERIFICATION_DELAY_MS);
251
+ }
252
+
253
+ /**
254
+ * Cancels the pending verification timer, if any.
255
+ */
256
+ private clearTimer(): void {
257
+ if (this.timer !== null) {
258
+ clearTimeout(this.timer);
259
+ this.timer = null;
260
+ }
261
+ }
262
+
263
+ /**
264
+ * Replaces the snapshot and notifies subscribers; no-op when nothing changed.
265
+ * @param netInfo - NetInfo to publish
266
+ * @param stillOfflineAfterResumeCount - counter to publish, defaults to the current one
267
+ */
268
+ private publish(
269
+ netInfo: T,
270
+ stillOfflineAfterResumeCount = this.state.stillOfflineAfterResumeCount
271
+ ): void {
272
+ if (
273
+ netInfo === this.state.netInfo &&
274
+ stillOfflineAfterResumeCount === this.state.stillOfflineAfterResumeCount
275
+ ) {
276
+ return;
277
+ }
278
+
279
+ this.state = { netInfo, stillOfflineAfterResumeCount };
280
+ this.listeners.forEach((listener) => listener());
281
+ }
282
+ }
@@ -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(NetInfoMock);
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
+ });