@applicaster/zapp-react-native-utils 16.0.0-alpha.4416921324 → 16.0.0-alpha.4548770949

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 (173) hide show
  1. package/actionUtils/index.ts +3 -1
  2. package/actionsExecutor/ActionExecutor.ts +50 -3
  3. package/actionsExecutor/ActionExecutorContext.tsx +99 -17
  4. package/actionsExecutor/__tests__/ActionExecutor.test.ts +71 -0
  5. package/actionsExecutor/__tests__/StorageActions.test.ts +98 -0
  6. package/actionsExecutor/__tests__/feedDecorator.test.ts +91 -0
  7. package/actionsExecutor/__tests__/screenToggleFlag.test.ts +90 -0
  8. package/actionsExecutor/actions/__tests__/dismissBottomSheet.test.ts +25 -0
  9. package/actionsExecutor/actions/__tests__/goBack.test.ts +62 -0
  10. package/actionsExecutor/actions/__tests__/goHome.test.ts +19 -0
  11. package/actionsExecutor/actions/__tests__/loadItemsFromSource.test.ts +88 -0
  12. package/actionsExecutor/actions/__tests__/navigateToScreen.test.ts +267 -0
  13. package/actionsExecutor/actions/__tests__/openBottomSheet.customContent.test.ts +100 -0
  14. package/actionsExecutor/actions/__tests__/openBottomSheet.inlineItems.test.ts +372 -0
  15. package/actionsExecutor/actions/__tests__/presentScreen.test.ts +127 -0
  16. package/actionsExecutor/actions/__tests__/showAlert.test.ts +49 -0
  17. package/actionsExecutor/actions/__tests__/showToast.test.ts +88 -0
  18. package/actionsExecutor/actions/dismissBottomSheet.ts +23 -0
  19. package/actionsExecutor/actions/goBack.ts +60 -0
  20. package/actionsExecutor/actions/goHome.ts +31 -0
  21. package/actionsExecutor/actions/index.ts +12 -0
  22. package/actionsExecutor/actions/navigateToScreen.ts +43 -45
  23. package/actionsExecutor/actions/openBottomSheet.ts +340 -0
  24. package/actionsExecutor/actions/presentScreen.ts +78 -0
  25. package/actionsExecutor/actions/refreshComponent.ts +6 -0
  26. package/actionsExecutor/actions/screenToggleFlag.ts +1 -3
  27. package/actionsExecutor/actions/sendCloudEvent.ts +6 -1
  28. package/actionsExecutor/actions/showAlert.ts +51 -0
  29. package/actionsExecutor/actions/showToast.ts +64 -8
  30. package/actionsExecutor/consts.ts +27 -0
  31. package/actionsExecutor/feedDecorator.ts +12 -6
  32. package/analyticsUtils/PlayerAnalyticsManager.ts +1 -1
  33. package/appUtils/HooksManager/index.ts +9 -0
  34. package/appUtils/contextKeysManager/__tests__/getKey/failure.test.ts +1 -22
  35. package/appUtils/contextKeysManager/__tests__/getKey/success/fullSearch.test.ts +1 -25
  36. package/appUtils/contextKeysManager/__tests__/getKey/success/searchInSecure.test.ts +1 -7
  37. package/appUtils/contextKeysManager/__tests__/removeKey/failure.test.ts +0 -25
  38. package/appUtils/contextKeysManager/__tests__/removeKey/success.test.ts +0 -6
  39. package/appUtils/contextKeysManager/__tests__/setKey/failure/failedToSaveInStorage.test.ts +0 -40
  40. package/appUtils/contextKeysManager/__tests__/setKey/{success/saveItemAndReference.test.ts → success.test.ts} +2 -42
  41. package/appUtils/contextKeysManager/index.ts +28 -129
  42. package/appUtils/playerManager/OverlayObserver/OverlaysObserver.ts +142 -59
  43. package/appUtils/playerManager/OverlayObserver/__tests__/getUpcomingQueue.test.ts +67 -0
  44. package/appUtils/playerManager/OverlayObserver/__tests__/liveContent.test.ts +205 -0
  45. package/appUtils/playerManager/OverlayObserver/__tests__/utils.test.ts +56 -0
  46. package/appUtils/playerManager/OverlayObserver/getUpcomingQueue.android.tv.ts +4 -0
  47. package/appUtils/playerManager/OverlayObserver/getUpcomingQueue.ios.tv.ts +4 -0
  48. package/appUtils/playerManager/OverlayObserver/getUpcomingQueue.ts +24 -0
  49. package/appUtils/playerManager/OverlayObserver/getUpcomingQueue.web.ts +4 -0
  50. package/appUtils/playerManager/OverlayObserver/utils.ts +51 -20
  51. package/appUtils/playerManager/__tests__/playerContent.test.ts +403 -0
  52. package/appUtils/playerManager/__tests__/playerDiagnostics.test.ts +62 -0
  53. package/appUtils/playerManager/__tests__/playerFactory.test.ts +1 -1
  54. package/appUtils/playerManager/__tests__/playerNative.test.ts +114 -0
  55. package/appUtils/playerManager/__tests__/playerNativeCommand.test.ts +62 -0
  56. package/appUtils/playerManager/__tests__/playerToolbox.test.ts +281 -0
  57. package/appUtils/playerManager/__tests__/usePlayerContent.test.tsx +64 -0
  58. package/appUtils/playerManager/__tests__/usePlayerControllerSetup.test.tsx +62 -0
  59. package/appUtils/playerManager/{conts.ts → const.ts} +33 -0
  60. package/appUtils/playerManager/index.ts +8 -1
  61. package/appUtils/playerManager/player.ts +140 -3
  62. package/appUtils/playerManager/playerFactory.ts +1 -1
  63. package/appUtils/playerManager/playerNative.ts +28 -4
  64. package/appUtils/playerManager/playerNativeCommand.ts +3 -6
  65. package/appUtils/playerManager/playerToolbox.ts +196 -0
  66. package/appUtils/playerManager/usePlayerContent.tsx +43 -0
  67. package/appUtils/playerManager/usePlayerControllerSetup.tsx +7 -1
  68. package/appUtils/playerManager/userLanguagePreference.ts +1 -1
  69. package/arrayUtils/__tests__/reorderByIds.test.ts +63 -0
  70. package/arrayUtils/index.ts +62 -0
  71. package/cellUtils/__tests__/getButtonsEnabled.test.ts +28 -0
  72. package/cellUtils/__tests__/getCellNavigationEnabled.test.ts +36 -0
  73. package/cellUtils/__tests__/getIsCellNavigationEnabled.test.ts +35 -0
  74. package/cellUtils/__tests__/isCellNavigationEnabled.test.ts +21 -0
  75. package/cellUtils/index.ts +70 -3
  76. package/configurationUtils/__tests__/manifestKeyParser.test.ts +21 -0
  77. package/configurationUtils/__tests__/modalBlocksParser.test.ts +403 -0
  78. package/configurationUtils/manifestKeyParser.ts +50 -10
  79. package/configurationUtils/modalBlocksParser.ts +252 -0
  80. package/entryActions/__tests__/buildEntryActions.test.ts +235 -0
  81. package/entryActions/aliasPresentation.ts +108 -0
  82. package/entryActions/buildEntryActions.ts +167 -0
  83. package/entryActions/index.ts +15 -0
  84. package/entryActions/types.ts +39 -0
  85. package/focusManager/FocusManager.ts +26 -0
  86. package/focusManager/__tests__/FocusManager.test.ts +9 -0
  87. package/localizationUtils/index.ts +12 -6
  88. package/logger/__tests__/xrayToolbox.test.ts +69 -0
  89. package/logger/xrayToolbox.ts +67 -0
  90. package/manifestUtils/__tests__/player.audioControls.test.js +158 -0
  91. package/manifestUtils/_internals/index.js +8 -3
  92. package/manifestUtils/defaultManifestConfigurations/generalContent.js +6 -0
  93. package/manifestUtils/defaultManifestConfigurations/player.js +338 -1
  94. package/manifestUtils/index.js +3 -0
  95. package/manifestUtils/keys.js +7 -2
  96. package/manifestUtils/mobileAction/container/__tests__/mobileActionButtonsContainer.test.js +21 -0
  97. package/manifestUtils/mobileAction/groups/__tests__/buildMobileActionButtonGroups.test.js +1 -1
  98. package/manifestUtils/mobileAction/groups/defaults.js +2 -1
  99. package/manifestUtils/modalBlocks.js +304 -0
  100. package/modalState/ContentViewModel.ts +113 -0
  101. package/modalState/EditableCollection.ts +17 -0
  102. package/modalState/EditableCollectionRegistry.ts +51 -0
  103. package/modalState/ModalOrchestrator.ts +213 -0
  104. package/modalState/RemoteEditableCollection.ts +235 -0
  105. package/modalState/__tests__/ContentViewModel.editable.test.ts +69 -0
  106. package/modalState/__tests__/ContentViewModel.test.ts +165 -0
  107. package/modalState/__tests__/EditableCollectionRegistry.test.ts +112 -0
  108. package/modalState/__tests__/ModalOrchestrator.phase3.test.ts +86 -0
  109. package/modalState/__tests__/RemoteEditableCollection.test.ts +415 -0
  110. package/modalState/__tests__/index.test.ts +30 -1
  111. package/modalState/__tests__/useBottomSheetContent.test.ts +349 -0
  112. package/modalState/__tests__/useModalStoreState.test.ts +142 -0
  113. package/modalState/index.ts +37 -83
  114. package/modalState/store.ts +109 -0
  115. package/modalState/types.ts +173 -0
  116. package/modalState/useBottomSheetContent.ts +109 -0
  117. package/navigationUtils/xrayNavigator.ts +12 -0
  118. package/package.json +3 -2
  119. package/playerUtils/__tests__/contentDataKeys.test.ts +297 -0
  120. package/playerUtils/__tests__/resolvePlayerActions.test.ts +138 -0
  121. package/playerUtils/__tests__/resolvePlayerTitleSubtitle.test.ts +180 -0
  122. package/playerUtils/contentDataKeys.ts +134 -0
  123. package/playerUtils/index.ts +37 -16
  124. package/playerUtils/isAudioItem.ts +26 -0
  125. package/playerUtils/resolvePlayerActions.ts +71 -0
  126. package/playerUtils/resolvePlayerTitleSubtitle.ts +76 -0
  127. package/pluginUtils/__tests__/pluginUtils.test.js +32 -0
  128. package/pluginUtils/index.ts +29 -0
  129. package/quickBrickAssets/__tests__/index.test.ts +18 -0
  130. package/quickBrickAssets/index.ts +35 -0
  131. package/reactHooks/__tests__/getPluginConfiguration.test.ts +82 -0
  132. package/reactHooks/actions/index.ts +74 -3
  133. package/reactHooks/cell-click/__tests__/index.test.js +64 -0
  134. package/reactHooks/cell-click/index.ts +21 -14
  135. package/reactHooks/connection/index.tsx +8 -5
  136. package/reactHooks/feed/__tests__/feedInvalidation.test.tsx +338 -0
  137. package/reactHooks/feed/__tests__/useBatchLoading.test.tsx +8 -2
  138. package/reactHooks/feed/__tests__/useMarkPipesDataStale.test.tsx +243 -0
  139. package/reactHooks/feed/useFeedLoader.tsx +28 -1
  140. package/reactHooks/feed/useLoadPipesDataDispatch.ts +12 -11
  141. package/reactHooks/feed/useMarkPipesDataStale.ts +68 -23
  142. package/reactHooks/index.ts +2 -0
  143. package/reactHooks/layout/index.ts +1 -1
  144. package/reactHooks/navigation/__tests__/index.test.tsx +176 -1
  145. package/reactHooks/navigation/__tests__/usePresentScreen.test.ts +154 -0
  146. package/reactHooks/navigation/index.ts +2 -0
  147. package/reactHooks/navigation/usePresentScreen.ts +157 -0
  148. package/reactHooks/navigation/useRoute.ts +22 -5
  149. package/reactHooks/usePluginConfiguration.ts +88 -47
  150. package/reactHooks/utils/__tests__/index.test.js +22 -2
  151. package/reactHooks/utils/index.ts +11 -1
  152. package/riverComponetsMeasurementProvider/index.tsx +3 -1
  153. package/uiActionsRegistrator/__tests__/resolveActionIdentifiers.test.ts +93 -0
  154. package/uiActionsRegistrator/__tests__/subscribe.test.ts +133 -0
  155. package/uiActionsRegistrator/__tests__/usableActionItems.test.ts +52 -0
  156. package/uiActionsRegistrator/index.ts +25 -203
  157. package/uiActionsRegistrator/registry.ts +351 -0
  158. package/uiActionsRegistrator/resolveActionIdentifiers.ts +100 -0
  159. package/uiActionsRegistrator/usableActionItems.ts +66 -0
  160. package/zappFrameworkUtils/__tests__/localStorageHelper.test.ts +146 -0
  161. package/zappFrameworkUtils/localStorageHelper.ts +19 -15
  162. package/appUtils/contextKeysManager/__tests__/getKey/success/byReference.test.ts +0 -56
  163. package/appUtils/contextKeysManager/__tests__/getKeyByReference/failure.test.ts +0 -265
  164. package/appUtils/contextKeysManager/__tests__/getKeyByReference/success.test.ts +0 -303
  165. package/appUtils/contextKeysManager/__tests__/removeReferenceForKey/failure.test.ts +0 -82
  166. package/appUtils/contextKeysManager/__tests__/removeReferenceForKey/success.test.ts +0 -41
  167. package/appUtils/contextKeysManager/__tests__/setKey/success/saveItemButNotReference.test.ts +0 -208
  168. package/appUtils/contextKeysManager/__tests__/setReferenceForKey/failure.test.ts +0 -93
  169. package/appUtils/contextKeysManager/__tests__/setReferenceForKey/success.test.ts +0 -45
  170. package/appUtils/playerManager/usePlayerTitleSummaryOverlay.tsx +0 -56
  171. package/playerUtils/__tests__/getPlayerActionButtons.test.ts +0 -54
  172. package/playerUtils/getPlayerActionButtons.ts +0 -17
  173. /package/reactHooks/actions/__tests__/{index.test.js → index.test.tsx} +0 -0
@@ -6,6 +6,7 @@ import { ActionsContext } from "@applicaster/zapp-react-native-ui-components/Con
6
6
  import {
7
7
  observeEntryState,
8
8
  RegisteredActionValue,
9
+ uiActionsRegistry,
9
10
  } from "../../uiActionsRegistrator";
10
11
  import { reactHooksLogger } from "../logger";
11
12
 
@@ -20,21 +21,55 @@ type ActionContexts<T = unknown> = {
20
21
  };
21
22
 
22
23
  /**
23
- * React Hook to subscribe to the custom action context
24
+ * A value that changes whenever a provider is added to or removed from
25
+ * `uiActionsRegistry`.
26
+ *
27
+ * Providers can register after the UI that wants them has rendered - the audio
28
+ * player registers playback speed only once its controller has state, and
29
+ * plugin-backed actions are bridged in from an effect. Put this in the
30
+ * dependency list of anything that resolves actions, so the list is re-read
31
+ * instead of being frozen at first paint.
32
+ */
33
+ export function useRegistryRevision(): number {
34
+ const [revision, setRevision] = useState(0);
35
+
36
+ useEffect(
37
+ () =>
38
+ uiActionsRegistry.subscribe(() => setRevision((current) => current + 1)),
39
+ []
40
+ );
41
+
42
+ return revision;
43
+ }
44
+
45
+ /**
46
+ * Resolves a legacy action context or falls back to the UI action registry.
24
47
  * @param {*} plugId - subscribes to the context of that action plugin
25
48
  * @return {*} - global action provider context or context for specific action plugin
26
49
  */
27
50
  export function useActions<T = unknown>(plugId: string): any {
28
51
  const [logged, setLogged] = useState(false);
29
52
  const context = useContext<ActionContexts<T>>(ActionsContext);
53
+ useRegistryRevision();
54
+
30
55
  const actionContext = context?.actions?.[plugId]?.module?.context;
31
56
 
32
57
  if (plugId) {
33
58
  if (!actionContext) {
59
+ const registeredAction = uiActionsRegistry.getAction(plugId);
60
+
61
+ if (registeredAction) return registeredAction;
62
+
34
63
  if (!logged) {
35
64
  logger.warning({
36
65
  message: `useActions: Couldn't find an action for ${plugId} plugin`,
37
- data: { actions: context?.actions },
66
+ // Identifiers only. The action map holds every plugin's module and
67
+ // live React context, which is far too large for a log line and can
68
+ // carry whatever state those plugins hold.
69
+ data: {
70
+ plugId,
71
+ availableActions: Object.keys(context?.actions ?? {}),
72
+ },
38
73
  });
39
74
 
40
75
  setLogged(true);
@@ -80,7 +115,31 @@ export function useEntryActionState(
80
115
  useEffect(() => {
81
116
  if (!action) return undefined;
82
117
 
83
- const subscription = observeEntryState(action, entry).subscribe(setState);
118
+ // An action's stream is third-party code. Subscribing with only a `next`
119
+ // handler lets RxJS rethrow an error notification asynchronously, where no
120
+ // error boundary catches it - one plugin faulting would take the app down
121
+ // rather than this one button. The button keeps its last known state.
122
+ let subscription;
123
+
124
+ try {
125
+ subscription = observeEntryState(action, entry).subscribe({
126
+ next: setState,
127
+ error: (error) =>
128
+ logger.warning({
129
+ message:
130
+ "useEntryActionState: the action's state stream failed - the button keeps its last state",
131
+ data: { entryId, error },
132
+ }),
133
+ });
134
+ } catch (error) {
135
+ logger.warning({
136
+ message:
137
+ "useEntryActionState: could not observe the action - the button keeps its initial state",
138
+ data: { entryId, error },
139
+ });
140
+
141
+ return undefined;
142
+ }
84
143
 
85
144
  return () => subscription.unsubscribe();
86
145
  // eslint-disable-next-line @wogns3623/better-exhaustive-deps/exhaustive-deps
@@ -88,6 +147,18 @@ export function useEntryActionState(
88
147
 
89
148
  const invokeAction = useCallback(
90
149
  (options: InvokeArgsOptions = {}) => {
150
+ if (typeof action?.invokeAction !== "function") {
151
+ // A button was rendered and pressed, and the press goes nowhere. Only
152
+ // reachable on a user interaction, so it costs nothing on render. The
153
+ // action value carries no identifier, so the label is what locates the
154
+ // button that misbehaved.
155
+ logger.warning({
156
+ message:
157
+ "useEntryActionState: the pressed action has no invokeAction - the press did nothing",
158
+ data: { entryId, label: state?.label },
159
+ });
160
+ }
161
+
91
162
  action?.invokeAction?.(entry, { updateState: setState, ...options });
92
163
  },
93
164
  // eslint-disable-next-line @wogns3623/better-exhaustive-deps/exhaustive-deps
@@ -5,6 +5,7 @@ import { ActionExecutorContext } from "@applicaster/zapp-react-native-utils/acti
5
5
 
6
6
  const mockNavigator = {
7
7
  getPathname: () => "pathnameMock",
8
+ currentRoute: "pathnameMock",
8
9
  push: jest.fn(),
9
10
  data: {},
10
11
  };
@@ -29,6 +30,7 @@ jest.mock("@applicaster/zapp-react-native-utils/reactHooks/screen", () => ({
29
30
  ),
30
31
  useTargetScreenData: jest.fn(() => ({})),
31
32
  useCurrentScreenData: jest.fn(() => ({})),
33
+ useScreenContext: jest.fn(() => ({})),
32
34
  }));
33
35
 
34
36
  jest.mock("@applicaster/zapp-react-native-ui-components/Contexts", () => ({
@@ -66,6 +68,12 @@ const wrapper = ({ children }) => (
66
68
  </CellTapContext.Provider>
67
69
  );
68
70
 
71
+ const actionExecutorWrapper = ({ children }) => (
72
+ <ActionExecutorContext.Provider value={actionExecutor}>
73
+ {children}
74
+ </ActionExecutorContext.Provider>
75
+ );
76
+
69
77
  describe("useCellClick", () => {
70
78
  it("returns a function", () => {
71
79
  const { result } = renderHook(() => useCellClick(mockProps));
@@ -95,4 +103,60 @@ describe("useCellClick", () => {
95
103
 
96
104
  cleanup();
97
105
  });
106
+
107
+ it("does not push when the cell finishes a hook", async () => {
108
+ mockNavigator.push.mockClear();
109
+
110
+ const item = {
111
+ id: "profile-1",
112
+ type: { value: "feed" },
113
+ extensions: {
114
+ tap_actions: {
115
+ actions: [
116
+ { type: "sessionStorageSet" },
117
+ { type: "finishHook", options: { success: true } },
118
+ ],
119
+ },
120
+ },
121
+ };
122
+
123
+ const { result } = renderHook(() => useCellClick({ item }), {
124
+ wrapper: actionExecutorWrapper,
125
+ });
126
+
127
+ await result.current();
128
+
129
+ await waitFor(() => {
130
+ expect(actionExecutor.handleEntryActions).toHaveBeenCalled();
131
+ });
132
+
133
+ expect(mockNavigator.push).not.toHaveBeenCalled();
134
+ cleanup();
135
+ });
136
+
137
+ it("pushes after tap actions that do not finish a hook", async () => {
138
+ mockNavigator.push.mockClear();
139
+
140
+ const item = {
141
+ id: "episode-1",
142
+ type: { value: "feed" },
143
+ extensions: {
144
+ tap_actions: {
145
+ actions: [{ type: "sessionStorageSet" }],
146
+ },
147
+ },
148
+ };
149
+
150
+ const { result } = renderHook(() => useCellClick({ item }), {
151
+ wrapper: actionExecutorWrapper,
152
+ });
153
+
154
+ await result.current();
155
+
156
+ await waitFor(() => {
157
+ expect(mockNavigator.push).toHaveBeenCalled();
158
+ });
159
+
160
+ cleanup();
161
+ });
98
162
  });
@@ -83,6 +83,12 @@ export const useCellClick = ({
83
83
 
84
84
  logOnPress(selectedItem, pathname, component, onCellTap);
85
85
 
86
+ const tapActions = selectedItem?.extensions?.tap_actions?.actions;
87
+
88
+ const finishesHook =
89
+ Array.isArray(tapActions) &&
90
+ tapActions.some((action) => action?.type === "finishHook");
91
+
86
92
  if (selectedItem) {
87
93
  await actionExecutor?.handleEntryActions(selectedItem, {
88
94
  component,
@@ -101,20 +107,21 @@ export const useCellClick = ({
101
107
  url: selectedItem.link.href,
102
108
  });
103
109
  }
104
- } else {
105
- if (isFunction(onCellTap)) {
106
- onCellTap?.(selectedItem, _index);
107
- } else {
108
- if (currentRoute === pathname || pathname.includes("video-modal")) {
109
- const targetScreen = component?.data?.target;
110
-
111
- push(
112
- R.when(
113
- () => targetScreen,
114
- R.mergeLeft({ screen_type: targetScreen })
115
- )(selectedItem)
116
- );
117
- }
110
+ } else if (isFunction(onCellTap)) {
111
+ onCellTap?.(selectedItem, _index);
112
+ } else if (!finishesHook) {
113
+ // finishHook already completes the hook and navigates to the original
114
+ // target. Pushing the cell as well stacks a second copy of that screen
115
+ // (e.g. home/home after a profile selector hook).
116
+ if (currentRoute === pathname || pathname.includes("video-modal")) {
117
+ const targetScreen = component?.data?.target;
118
+
119
+ push(
120
+ R.when(
121
+ () => targetScreen,
122
+ R.mergeLeft({ screen_type: targetScreen })
123
+ )(selectedItem)
124
+ );
118
125
  }
119
126
  }
120
127
  },
@@ -4,6 +4,11 @@ import { NetInfoState } from "@react-native-community/netinfo";
4
4
  import { NetworkStatusContext } from "@applicaster/zapp-react-native-ui-components/Contexts/NetworkStatusContext";
5
5
  import { isOnline } from "@applicaster/quick-brick-core/App/NetworkStatusProvider/utils";
6
6
 
7
+ type ConnectionInfo = NetInfoState & {
8
+ deviceStatus: NetStatus;
9
+ stillOfflineAfterResumeCount?: number;
10
+ };
11
+
7
12
  /**
8
13
  * This hook returns connection type from RN NetInfo component or boolean for iConnected if justOnlineStatus flag is set
9
14
  * @param {boolean} justOnlineStatus - flag if to return boolean for isConnected
@@ -12,16 +17,14 @@ import { isOnline } from "@applicaster/quick-brick-core/App/NetworkStatusProvide
12
17
 
13
18
  export function useConnectionInfo(justOnlineStatus: true): boolean;
14
19
 
15
- export function useConnectionInfo(
16
- justOnlineStatus: false
17
- ): NetInfoState & { deviceStatus: NetStatus };
20
+ export function useConnectionInfo(justOnlineStatus: false): ConnectionInfo;
18
21
 
19
22
  export function useConnectionInfo(
20
23
  justOnlineStatus: boolean
21
- ): boolean | (NetInfoState & { deviceStatus: NetStatus }) {
24
+ ): boolean | ConnectionInfo {
22
25
  const connectionInfo = React.useContext(
23
26
  NetworkStatusContext
24
- ) as NetInfoState & { deviceStatus: NetStatus };
27
+ ) as ConnectionInfo;
25
28
 
26
29
  return justOnlineStatus ? isOnline(connectionInfo) === true : connectionInfo;
27
30
  }
@@ -0,0 +1,338 @@
1
+ import React from "react";
2
+ import { Provider } from "react-redux";
3
+ import { configureStore } from "@reduxjs/toolkit";
4
+ import { act, renderHook } from "@testing-library/react-native";
5
+ import reducer, {
6
+ loadPipesData,
7
+ unsubscribeFromDataSource,
8
+ zappPipesClear,
9
+ } from "@applicaster/zapp-react-native-redux/ZappPipes";
10
+ import { zappPipesMiddleware } from "../../../../zapp-react-native-redux/ZappPipes/middleware";
11
+ import { useFeedLoader } from "../useFeedLoader";
12
+
13
+ const mockGet = jest.fn();
14
+ const mockPost = jest.fn();
15
+ const mockSubscribe = jest.fn();
16
+ const mockRelease = jest.fn();
17
+ let mockChange: () => void;
18
+ let mockLoad: ReturnType<typeof jest.fn>;
19
+
20
+ jest.mock("@applicaster/zapp-pipes-v2-client", () => ({
21
+ __esModule: true,
22
+ default: jest.fn(() => ({
23
+ get: (...args) => mockGet(...args),
24
+ post: (...args) => mockPost(...args),
25
+ })),
26
+ RequestBuilder: jest.fn(() => ({
27
+ setResolvers() {
28
+ return this;
29
+ },
30
+ build: async () => ({}),
31
+ })),
32
+ subscribeForUrlContextKeyChanges: (...args) => mockSubscribe(...args),
33
+ }));
34
+
35
+ jest.mock("../useInflatedUrl", () => ({
36
+ useInflatedUrl: ({ feedUrl }) => feedUrl,
37
+ }));
38
+
39
+ jest.mock("../useLoadPipesDataDispatch", () => ({
40
+ useLoadPipesDataDispatch: () => mockLoad,
41
+ }));
42
+
43
+ const url = "https://example.test/feed";
44
+ const data = { id: "feed", type: { value: "feed" }, entry: [] };
45
+
46
+ const settle = async () => {
47
+ for (let index = 0; index < 30; index++) await Promise.resolve();
48
+ };
49
+
50
+ const createHarness = (plugins: QuickBrickPlugin[] = []) => {
51
+ const store = configureStore({
52
+ reducer: {
53
+ zappPipes: reducer,
54
+ appData: () => ({ layoutVersion: "v2" }),
55
+ plugins: () => plugins,
56
+ },
57
+ middleware: (defaults) =>
58
+ defaults({ serializableCheck: false }).concat(zappPipesMiddleware),
59
+ });
60
+
61
+ mockLoad = jest.fn((feedUrl, options) =>
62
+ store.dispatch(loadPipesData(feedUrl, options))
63
+ );
64
+
65
+ const wrapper = ({ children }) => (
66
+ <Provider store={store}>{children}</Provider>
67
+ );
68
+
69
+ return { store, wrapper };
70
+ };
71
+
72
+ describe("dependency-driven feed invalidation", () => {
73
+ beforeEach(() => {
74
+ jest.clearAllMocks();
75
+ mockGet.mockResolvedValue({ response: data, code: 200 });
76
+ mockPost.mockResolvedValue({ response: data, code: 200 });
77
+
78
+ mockSubscribe.mockImplementation((_url, _options, onChange) => {
79
+ mockChange = onChange;
80
+
81
+ return mockRelease;
82
+ });
83
+ });
84
+
85
+ afterEach(() => unsubscribeFromDataSource());
86
+
87
+ it("silently revalidates a shared mounted feed once and keeps its content", async () => {
88
+ const { store, wrapper } = createHarness();
89
+
90
+ const first = renderHook(() => useFeedLoader({ feedUrl: url }), {
91
+ wrapper,
92
+ });
93
+
94
+ const second = renderHook(() => useFeedLoader({ feedUrl: url }), {
95
+ wrapper,
96
+ });
97
+
98
+ await act(settle);
99
+
100
+ expect(mockGet).toHaveBeenCalledTimes(1);
101
+ expect(mockSubscribe).toHaveBeenCalledTimes(1);
102
+ let finish;
103
+
104
+ mockGet.mockImplementationOnce(
105
+ () =>
106
+ new Promise((resolve) => {
107
+ finish = resolve;
108
+ })
109
+ );
110
+
111
+ await act(async () => {
112
+ mockChange();
113
+ await settle();
114
+ });
115
+
116
+ expect(mockGet).toHaveBeenCalledTimes(2);
117
+ expect(first.result.current.data).toEqual(data);
118
+ expect(first.result.current.loading).toBe(false);
119
+ expect(store.getState().zappPipes[url].refreshing).toBe(true);
120
+
121
+ await act(async () => {
122
+ finish({ response: data, code: 200 });
123
+ await settle();
124
+ });
125
+
126
+ expect(store.getState().zappPipes[url].stale).toBeUndefined();
127
+ first.unmount();
128
+ second.unmount();
129
+ expect(mockRelease).not.toHaveBeenCalled();
130
+ });
131
+
132
+ it("invalidates an unmounted cache without fetching and revalidates on remount", async () => {
133
+ const { store, wrapper } = createHarness();
134
+
135
+ const first = renderHook(() => useFeedLoader({ feedUrl: url }), {
136
+ wrapper,
137
+ });
138
+
139
+ await act(settle);
140
+ first.unmount();
141
+
142
+ mockChange();
143
+ mockChange();
144
+ await settle();
145
+
146
+ expect(mockGet).toHaveBeenCalledTimes(1);
147
+
148
+ expect(store.getState().zappPipes[url]).toMatchObject({
149
+ stale: true,
150
+ invalidationVersion: 2,
151
+ data,
152
+ });
153
+
154
+ const second = renderHook(() => useFeedLoader({ feedUrl: url }), {
155
+ wrapper,
156
+ });
157
+
158
+ await act(settle);
159
+
160
+ expect(mockGet).toHaveBeenCalledTimes(2);
161
+ expect(store.getState().zappPipes[url].stale).toBeUndefined();
162
+ second.unmount();
163
+ });
164
+
165
+ it("runs one follow-up when invalidated repeatedly during a request", async () => {
166
+ const { store, wrapper } = createHarness();
167
+ let finish;
168
+
169
+ mockGet.mockImplementationOnce(
170
+ () =>
171
+ new Promise((resolve) => {
172
+ finish = resolve;
173
+ })
174
+ );
175
+
176
+ const hook = renderHook(() => useFeedLoader({ feedUrl: url }), { wrapper });
177
+ await act(settle);
178
+
179
+ await act(async () => {
180
+ mockChange();
181
+ mockChange();
182
+ await settle();
183
+ });
184
+
185
+ expect(mockGet).toHaveBeenCalledTimes(1);
186
+
187
+ await act(async () => {
188
+ finish({ response: data, code: 200 });
189
+ await settle();
190
+ });
191
+
192
+ expect(mockGet).toHaveBeenCalledTimes(2);
193
+ expect(store.getState().zappPipes[url].stale).toBeUndefined();
194
+ hook.unmount();
195
+ });
196
+
197
+ it("stops a failed revalidation until another dependency change", async () => {
198
+ const { wrapper } = createHarness();
199
+ const hook = renderHook(() => useFeedLoader({ feedUrl: url }), { wrapper });
200
+ await act(settle);
201
+ mockGet.mockRejectedValueOnce(new Error("network unavailable"));
202
+
203
+ await act(async () => {
204
+ mockChange();
205
+ await settle();
206
+ });
207
+
208
+ expect(mockGet).toHaveBeenCalledTimes(2);
209
+ expect(hook.result.current.error).toBeDefined();
210
+
211
+ await act(async () => {
212
+ mockChange();
213
+ await settle();
214
+ });
215
+
216
+ expect(mockGet).toHaveBeenCalledTimes(3);
217
+ expect(hook.result.current.error).toBeUndefined();
218
+ hook.unmount();
219
+ });
220
+
221
+ it("releases cache subscriptions for direct clear actions", async () => {
222
+ const { store } = createHarness();
223
+ await store.dispatch(loadPipesData(url));
224
+ await store.dispatch(loadPipesData(`${url}/two`));
225
+ store.dispatch(zappPipesClear(url));
226
+ expect(mockRelease).toHaveBeenCalledTimes(1);
227
+ store.dispatch(zappPipesClear());
228
+ expect(mockRelease).toHaveBeenCalledTimes(2);
229
+ });
230
+
231
+ it("preserves POST method and body when revalidating", async () => {
232
+ const { wrapper } = createHarness();
233
+ const bodyParams = { filter: "recent" };
234
+
235
+ const hook = renderHook(
236
+ () =>
237
+ useFeedLoader({
238
+ feedUrl: url,
239
+ pipesOptions: { method: "post", bodyParams },
240
+ }),
241
+ { wrapper }
242
+ );
243
+
244
+ await act(settle);
245
+
246
+ await act(async () => {
247
+ mockChange();
248
+ await settle();
249
+ });
250
+
251
+ expect(mockSubscribe).toHaveBeenCalledWith(
252
+ url,
253
+ { method: "post" },
254
+ expect.any(Function)
255
+ );
256
+
257
+ expect(mockPost).toHaveBeenCalledTimes(2);
258
+ expect(mockPost).toHaveBeenLastCalledWith(url, bodyParams);
259
+ expect(mockGet).not.toHaveBeenCalled();
260
+ hook.unmount();
261
+ });
262
+
263
+ it("does not revalidate a skipped mounted feed until loading is enabled", async () => {
264
+ const { store, wrapper } = createHarness();
265
+ await store.dispatch(loadPipesData(url));
266
+
267
+ const hook = renderHook(
268
+ ({ skipLoading }) =>
269
+ useFeedLoader({
270
+ feedUrl: url,
271
+ pipesOptions: { skipLoading },
272
+ }),
273
+ { wrapper, initialProps: { skipLoading: true } }
274
+ );
275
+
276
+ await act(async () => {
277
+ mockChange();
278
+ await settle();
279
+ });
280
+
281
+ expect(mockGet).toHaveBeenCalledTimes(1);
282
+ expect(store.getState().zappPipes[url].stale).toBe(true);
283
+
284
+ hook.rerender({ skipLoading: false });
285
+ await act(settle);
286
+ expect(mockGet).toHaveBeenCalledTimes(2);
287
+ expect(store.getState().zappPipes[url].stale).toBeUndefined();
288
+ hook.unmount();
289
+ });
290
+
291
+ it("shares GET revalidation with an omitted or empty unused request body", async () => {
292
+ const { store } = createHarness();
293
+ await store.dispatch(loadPipesData(url));
294
+ mockChange();
295
+ mockGet.mockClear();
296
+
297
+ await Promise.all([
298
+ store.dispatch(loadPipesData(url)),
299
+ store.dispatch(loadPipesData(url, { method: "get", bodyParams: {} })),
300
+ ]);
301
+
302
+ expect(mockGet).toHaveBeenCalledTimes(1);
303
+ });
304
+
305
+ it("keeps plugin observation until its cache is cleared", async () => {
306
+ const release = jest.fn();
307
+ let changed;
308
+
309
+ const addDataSourceListener = jest.fn((callback) => {
310
+ changed = callback;
311
+
312
+ return release;
313
+ });
314
+
315
+ const { store, wrapper } = createHarness([
316
+ {
317
+ identifier: "generic-provider",
318
+ module: { addDataSourceListener },
319
+ } as QuickBrickPlugin,
320
+ ]);
321
+
322
+ const pluginUrl = "pipesv2://generic-provider/feed";
323
+
324
+ const hook = renderHook(() => useFeedLoader({ feedUrl: pluginUrl }), {
325
+ wrapper,
326
+ });
327
+
328
+ await act(settle);
329
+ hook.unmount();
330
+ changed();
331
+ await settle();
332
+ expect(mockGet).toHaveBeenCalledTimes(1);
333
+ expect(store.getState().zappPipes[pluginUrl].stale).toBe(true);
334
+ expect(release).not.toHaveBeenCalled();
335
+ store.dispatch(zappPipesClear(pluginUrl));
336
+ expect(release).toHaveBeenCalledTimes(1);
337
+ });
338
+ });
@@ -2,6 +2,7 @@ import { renderHook, waitFor } from "@testing-library/react-native";
2
2
  import { allFeedsIsReady, useBatchLoading } from "../useBatchLoading";
3
3
  import { WrappedWithProviders } from "@applicaster/zapp-react-native-utils/testUtils";
4
4
  import { appStore } from "@applicaster/zapp-react-native-redux/AppStore";
5
+ import * as ZappPipes from "@applicaster/zapp-react-native-redux/ZappPipes";
5
6
 
6
7
  jest.mock("../../navigation");
7
8
 
@@ -32,11 +33,16 @@ describe("useBatchLoading", () => {
32
33
  // ... more items
33
34
  ];
34
35
 
35
- beforeAll(() => {
36
- jest.useFakeTimers();
36
+ beforeEach(() => {
37
+ jest
38
+ .spyOn(ZappPipes, "loadPipesData")
39
+ .mockImplementation((url, options) => async (dispatch) => {
40
+ dispatch(ZappPipes.zappPipesRequestStart(url, options));
41
+ });
37
42
  });
38
43
 
39
44
  afterEach(() => {
45
+ jest.restoreAllMocks();
40
46
  jest.clearAllMocks();
41
47
  });
42
48