@applicaster/zapp-react-native-utils 16.0.0-alpha.1833861900 → 16.0.0-alpha.1852433906

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 (245) hide show
  1. package/actionUtils/index.ts +3 -1
  2. package/actionsExecutor/ActionExecutor.ts +57 -12
  3. package/actionsExecutor/ActionExecutorContext.tsx +113 -314
  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/appRestart.ts +24 -0
  19. package/actionsExecutor/actions/confirmDialog.ts +53 -0
  20. package/actionsExecutor/actions/dismissBottomSheet.ts +23 -0
  21. package/actionsExecutor/actions/goBack.ts +60 -0
  22. package/actionsExecutor/actions/goHome.ts +31 -0
  23. package/actionsExecutor/actions/index.ts +42 -0
  24. package/actionsExecutor/actions/localStorageRemove.ts +27 -0
  25. package/actionsExecutor/actions/localStorageSet.ts +28 -0
  26. package/actionsExecutor/actions/localStorageToggleFlag.ts +28 -0
  27. package/actionsExecutor/actions/navigateToScreen.ts +121 -0
  28. package/actionsExecutor/actions/openBottomSheet.ts +340 -0
  29. package/actionsExecutor/actions/presentScreen.ts +78 -0
  30. package/actionsExecutor/actions/refreshComponent.ts +85 -0
  31. package/actionsExecutor/actions/screenSetVariable.ts +35 -0
  32. package/actionsExecutor/actions/screenToggleFlag.ts +36 -0
  33. package/actionsExecutor/actions/sendCloudEvent.ts +93 -0
  34. package/actionsExecutor/actions/sessionStorageRemove.ts +19 -0
  35. package/actionsExecutor/actions/sessionStorageSet.ts +20 -0
  36. package/actionsExecutor/actions/sessionStorageToggleFlag.ts +15 -0
  37. package/actionsExecutor/actions/showAlert.ts +51 -0
  38. package/actionsExecutor/actions/showToast.ts +87 -0
  39. package/actionsExecutor/actions/switchLayout.ts +40 -0
  40. package/actionsExecutor/consts.ts +27 -0
  41. package/actionsExecutor/feedDecorator.ts +12 -6
  42. package/actionsExecutor/types.ts +59 -0
  43. package/analyticsUtils/AnalyticsEvents/sendMenuClickEvent.ts +2 -2
  44. package/analyticsUtils/PlayerAnalyticsManager.ts +1 -1
  45. package/analyticsUtils/__tests__/analyticsMapper.test.ts +35 -0
  46. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testACP_events.json +1 -1
  47. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testBlockUnlistedParams_rules.json +1 -1
  48. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testEmptyRenameKeepsName_events.json +4 -0
  49. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testEmptyRenameKeepsName_rules.json +6 -0
  50. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testEventRegex_events.json +3 -9
  51. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testIgnoreOrdering_events.json +18 -0
  52. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testIgnoreOrdering_rules.json +16 -0
  53. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testRegexEventNoFallback_events.json +4 -0
  54. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testRegexEventNoFallback_rules.json +6 -0
  55. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testRegexMultiGroupRename_events.json +4 -0
  56. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testRegexMultiGroupRename_rules.json +6 -0
  57. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testRegexNonNamedFullMatch_events.json +4 -0
  58. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testRegexNonNamedFullMatch_rules.json +6 -0
  59. package/analyticsUtils/__tests__/fixtures/index.js +20 -0
  60. package/analyticsUtils/analyticsMapper.ts +4 -1
  61. package/appUtils/HooksManager/index.ts +9 -0
  62. package/appUtils/contextKeysManager/__tests__/getKey/failure.test.ts +2 -23
  63. package/appUtils/contextKeysManager/__tests__/getKey/success/fullSearch.test.ts +1 -25
  64. package/appUtils/contextKeysManager/__tests__/getKey/success/searchInSecure.test.ts +1 -7
  65. package/appUtils/contextKeysManager/__tests__/removeKey/failure.test.ts +1 -26
  66. package/appUtils/contextKeysManager/__tests__/removeKey/success.test.ts +0 -6
  67. package/appUtils/contextKeysManager/__tests__/setKey/failure/failedToSaveInStorage.test.ts +0 -40
  68. package/appUtils/contextKeysManager/__tests__/setKey/failure/invalidKey.test.ts +4 -4
  69. package/appUtils/contextKeysManager/__tests__/setKey/{success/saveItemAndReference.test.ts → success.test.ts} +2 -42
  70. package/appUtils/contextKeysManager/index.ts +29 -130
  71. package/appUtils/playerManager/OverlayObserver/OverlaysObserver.ts +142 -59
  72. package/appUtils/playerManager/OverlayObserver/__tests__/getUpcomingQueue.test.ts +67 -0
  73. package/appUtils/playerManager/OverlayObserver/__tests__/liveContent.test.ts +205 -0
  74. package/appUtils/playerManager/OverlayObserver/__tests__/utils.test.ts +56 -0
  75. package/appUtils/playerManager/OverlayObserver/getUpcomingQueue.android.tv.ts +4 -0
  76. package/appUtils/playerManager/OverlayObserver/getUpcomingQueue.ios.tv.ts +4 -0
  77. package/appUtils/playerManager/OverlayObserver/getUpcomingQueue.ts +24 -0
  78. package/appUtils/playerManager/OverlayObserver/getUpcomingQueue.web.ts +4 -0
  79. package/appUtils/playerManager/OverlayObserver/utils.ts +51 -20
  80. package/appUtils/playerManager/__tests__/playerContent.test.ts +403 -0
  81. package/appUtils/playerManager/__tests__/playerDiagnostics.test.ts +62 -0
  82. package/appUtils/playerManager/__tests__/playerFactory.test.ts +1 -1
  83. package/appUtils/playerManager/__tests__/playerNative.test.ts +114 -0
  84. package/appUtils/playerManager/__tests__/playerNativeCommand.test.ts +62 -0
  85. package/appUtils/playerManager/__tests__/playerToolbox.test.ts +281 -0
  86. package/appUtils/playerManager/__tests__/usePlayerContent.test.tsx +64 -0
  87. package/appUtils/playerManager/__tests__/usePlayerControllerSetup.test.tsx +62 -0
  88. package/appUtils/playerManager/{conts.ts → const.ts} +33 -0
  89. package/appUtils/playerManager/index.ts +8 -1
  90. package/appUtils/playerManager/player.ts +140 -3
  91. package/appUtils/playerManager/playerFactory.ts +1 -1
  92. package/appUtils/playerManager/playerNative.ts +28 -4
  93. package/appUtils/playerManager/playerNativeCommand.ts +3 -6
  94. package/appUtils/playerManager/playerToolbox.ts +196 -0
  95. package/appUtils/playerManager/usePlayerContent.tsx +43 -0
  96. package/appUtils/playerManager/usePlayerControllerSetup.tsx +7 -1
  97. package/appUtils/playerManager/userLanguagePreference.ts +1 -1
  98. package/arrayUtils/__tests__/{anyThruthy.test.ts → anyTruthy.test.ts} +8 -0
  99. package/arrayUtils/__tests__/arrayUtils.test.ts +165 -108
  100. package/arrayUtils/__tests__/isEmptyArray.test.ts +11 -0
  101. package/arrayUtils/__tests__/isFilledArray.test.ts +12 -0
  102. package/arrayUtils/__tests__/isFirst.test.ts +17 -0
  103. package/arrayUtils/__tests__/isIndexInRange.test.ts +14 -0
  104. package/arrayUtils/__tests__/isLast.test.ts +31 -0
  105. package/arrayUtils/__tests__/makeListOf.test.ts +31 -0
  106. package/arrayUtils/__tests__/makeListOfIndexes.test.ts +20 -0
  107. package/arrayUtils/__tests__/reorderByIds.test.ts +63 -0
  108. package/arrayUtils/__tests__/sample.test.ts +61 -0
  109. package/arrayUtils/index.ts +119 -20
  110. package/bottomSheet/__tests__/scrollContext.test.tsx +35 -0
  111. package/bottomSheet/index.ts +17 -0
  112. package/cellUtils/__tests__/getButtonsEnabled.test.ts +28 -0
  113. package/cellUtils/__tests__/getCellNavigationEnabled.test.ts +36 -0
  114. package/cellUtils/__tests__/getIsCellNavigationEnabled.test.ts +35 -0
  115. package/cellUtils/__tests__/isCellNavigationEnabled.test.ts +21 -0
  116. package/cellUtils/index.ts +73 -8
  117. package/colorUtils/__tests__/isTransparentColor.test.ts +76 -0
  118. package/colorUtils/__tests__/isValidColor.test.ts +70 -0
  119. package/colorUtils/index.ts +50 -0
  120. package/configurationUtils/__tests__/manifestKeyParser.test.ts +21 -0
  121. package/configurationUtils/__tests__/modalBlocksParser.test.ts +403 -0
  122. package/configurationUtils/manifestKeyParser.ts +50 -10
  123. package/configurationUtils/modalBlocksParser.ts +252 -0
  124. package/entryActions/__tests__/buildEntryActions.test.ts +235 -0
  125. package/entryActions/aliasPresentation.ts +108 -0
  126. package/entryActions/buildEntryActions.ts +167 -0
  127. package/entryActions/index.ts +15 -0
  128. package/entryActions/types.ts +39 -0
  129. package/focusManager/FocusManager.ts +26 -0
  130. package/focusManager/__tests__/FocusManager.test.ts +9 -0
  131. package/localizationUtils/index.ts +12 -6
  132. package/logger/__tests__/xrayToolbox.test.ts +69 -0
  133. package/logger/xrayToolbox.ts +67 -0
  134. package/manifestUtils/__tests__/player.audioControls.test.js +158 -0
  135. package/manifestUtils/_internals/index.js +14 -3
  136. package/manifestUtils/defaultManifestConfigurations/generalContent.js +41 -0
  137. package/manifestUtils/defaultManifestConfigurations/player.js +357 -20
  138. package/manifestUtils/fieldUtils/__tests__/fieldUtils.test.js +84 -0
  139. package/manifestUtils/fieldUtils/index.js +125 -0
  140. package/manifestUtils/index.js +3 -0
  141. package/manifestUtils/keys.js +16 -2
  142. package/manifestUtils/mobileAction/button/index.js +16 -0
  143. package/manifestUtils/mobileAction/container/__tests__/mobileActionButtonsContainer.test.js +21 -0
  144. package/manifestUtils/mobileAction/container/index.js +3 -1
  145. package/manifestUtils/mobileAction/groups/__tests__/buildMobileActionButtonGroups.test.js +1 -1
  146. package/manifestUtils/mobileAction/groups/defaults.js +5 -2
  147. package/manifestUtils/modalBlocks.js +304 -0
  148. package/manifestUtils/platformIsTV.js +1 -0
  149. package/modalState/ContentViewModel.ts +113 -0
  150. package/modalState/EditableCollection.ts +17 -0
  151. package/modalState/EditableCollectionRegistry.ts +51 -0
  152. package/modalState/ModalOrchestrator.ts +213 -0
  153. package/modalState/RemoteEditableCollection.ts +235 -0
  154. package/modalState/__tests__/ContentViewModel.editable.test.ts +69 -0
  155. package/modalState/__tests__/ContentViewModel.test.ts +165 -0
  156. package/modalState/__tests__/EditableCollectionRegistry.test.ts +112 -0
  157. package/modalState/__tests__/ModalOrchestrator.phase3.test.ts +86 -0
  158. package/modalState/__tests__/RemoteEditableCollection.test.ts +415 -0
  159. package/modalState/__tests__/index.test.ts +30 -1
  160. package/modalState/__tests__/useBottomSheetContent.test.ts +349 -0
  161. package/modalState/__tests__/useModalStoreState.test.ts +142 -0
  162. package/modalState/index.ts +37 -83
  163. package/modalState/store.ts +109 -0
  164. package/modalState/types.ts +173 -0
  165. package/modalState/useBottomSheetContent.ts +109 -0
  166. package/navigationUtils/xrayNavigator.ts +12 -0
  167. package/numberUtils/__tests__/isMinusZero.test.ts +20 -0
  168. package/numberUtils/__tests__/isZero.test.ts +27 -0
  169. package/numberUtils/__tests__/requireNumber.test.ts +50 -0
  170. package/numberUtils/__tests__/toNumber.test.ts +27 -0
  171. package/numberUtils/__tests__/toNumberWithDefault.test.ts +64 -0
  172. package/numberUtils/__tests__/toNumberWithDefaultZero.test.ts +48 -0
  173. package/numberUtils/index.ts +27 -8
  174. package/package.json +3 -2
  175. package/pipesUtils/__tests__/buildUrlWithQuery.test.ts +33 -0
  176. package/pipesUtils/__tests__/withPipesEndpoint.test.tsx +56 -0
  177. package/pipesUtils/index.ts +1 -0
  178. package/pipesUtils/withPipesEndpoint.tsx +54 -0
  179. package/playerUtils/__tests__/contentDataKeys.test.ts +297 -0
  180. package/playerUtils/__tests__/resolvePlayerActions.test.ts +138 -0
  181. package/playerUtils/__tests__/resolvePlayerTitleSubtitle.test.ts +180 -0
  182. package/playerUtils/contentDataKeys.ts +134 -0
  183. package/playerUtils/index.ts +37 -16
  184. package/playerUtils/isAudioItem.ts +26 -0
  185. package/playerUtils/resolvePlayerActions.ts +71 -0
  186. package/playerUtils/resolvePlayerTitleSubtitle.ts +76 -0
  187. package/pluginUtils/__tests__/pluginUtils.test.js +32 -0
  188. package/pluginUtils/index.ts +29 -0
  189. package/quickBrickAssets/__tests__/index.test.ts +18 -0
  190. package/quickBrickAssets/index.ts +35 -0
  191. package/reactHooks/__tests__/getPluginConfiguration.test.ts +82 -0
  192. package/reactHooks/actions/index.ts +124 -3
  193. package/reactHooks/cell-click/__tests__/index.test.js +64 -0
  194. package/reactHooks/cell-click/index.ts +39 -24
  195. package/reactHooks/connection/index.tsx +8 -5
  196. package/reactHooks/feed/__mocks__/useMarkPipesDataStale.ts +4 -0
  197. package/reactHooks/feed/__tests__/feedInvalidation.test.tsx +338 -0
  198. package/reactHooks/feed/__tests__/useBatchLoading.test.tsx +8 -2
  199. package/reactHooks/feed/__tests__/useInflatedUrl.test.tsx +25 -0
  200. package/reactHooks/feed/__tests__/useMarkPipesDataStale.test.tsx +243 -0
  201. package/reactHooks/feed/index.ts +5 -1
  202. package/reactHooks/feed/useBatchLoading.ts +9 -1
  203. package/reactHooks/feed/useFeedLoader.tsx +28 -1
  204. package/reactHooks/feed/useLoadPipesDataDispatch.ts +12 -11
  205. package/reactHooks/feed/useMarkPipesDataStale.ts +97 -0
  206. package/reactHooks/index.ts +2 -0
  207. package/reactHooks/layout/index.ts +1 -1
  208. package/reactHooks/navigation/__mocks__/index.ts +4 -0
  209. package/reactHooks/navigation/__tests__/index.test.tsx +176 -1
  210. package/reactHooks/navigation/__tests__/useIsScreenActive.test.ts +42 -0
  211. package/reactHooks/navigation/__tests__/usePresentScreen.test.ts +154 -0
  212. package/reactHooks/navigation/index.ts +3 -1
  213. package/reactHooks/navigation/useIsScreenActive.ts +29 -8
  214. package/reactHooks/navigation/usePresentScreen.ts +157 -0
  215. package/reactHooks/navigation/useRoute.ts +22 -5
  216. package/reactHooks/state/useComponentScreenState.ts +1 -1
  217. package/reactHooks/usePluginConfiguration.ts +88 -47
  218. package/reactHooks/utils/__tests__/index.test.js +22 -2
  219. package/reactHooks/utils/index.ts +13 -2
  220. package/riverComponetsMeasurementProvider/index.tsx +15 -9
  221. package/stringUtils/__tests__/stringUtils.test.js +42 -0
  222. package/stringUtils/index.ts +2 -2
  223. package/uiActionsRegistrator/__tests__/resolveActionIdentifiers.test.ts +93 -0
  224. package/uiActionsRegistrator/__tests__/subscribe.test.ts +133 -0
  225. package/uiActionsRegistrator/__tests__/usableActionItems.test.ts +52 -0
  226. package/uiActionsRegistrator/index.ts +25 -0
  227. package/uiActionsRegistrator/registry.ts +351 -0
  228. package/uiActionsRegistrator/resolveActionIdentifiers.ts +100 -0
  229. package/uiActionsRegistrator/usableActionItems.ts +66 -0
  230. package/zappFrameworkUtils/__tests__/localStorageHelper.test.ts +146 -0
  231. package/zappFrameworkUtils/localStorageHelper.ts +19 -15
  232. package/appUtils/contextKeysManager/__tests__/getKey/success/byReference.test.ts +0 -56
  233. package/appUtils/contextKeysManager/__tests__/getKeyByReference/failure.test.ts +0 -265
  234. package/appUtils/contextKeysManager/__tests__/getKeyByReference/success.test.ts +0 -303
  235. package/appUtils/contextKeysManager/__tests__/removeReferenceForKey/failure.test.ts +0 -82
  236. package/appUtils/contextKeysManager/__tests__/removeReferenceForKey/success.test.ts +0 -41
  237. package/appUtils/contextKeysManager/__tests__/setKey/success/saveItemButNotReference.test.ts +0 -208
  238. package/appUtils/contextKeysManager/__tests__/setReferenceForKey/failure.test.ts +0 -93
  239. package/appUtils/contextKeysManager/__tests__/setReferenceForKey/success.test.ts +0 -45
  240. package/appUtils/playerManager/usePlayerTitleSummaryOverlay.tsx +0 -56
  241. package/playerUtils/__tests__/getPlayerActionButtons.test.ts +0 -54
  242. package/playerUtils/getPlayerActionButtons.ts +0 -17
  243. package/reactHooks/feed/__mocks__/usePipesCacheReset.ts +0 -1
  244. package/reactHooks/feed/usePipesCacheReset.ts +0 -44
  245. /package/reactHooks/actions/__tests__/{index.test.js → index.test.tsx} +0 -0
@@ -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
 
@@ -242,4 +242,29 @@ describe("useGetUrlInflater", () => {
242
242
  const url = result.current(source, mapping);
243
243
  expect(url).toBe("https://foo.com/shows/A123?q=user%20query");
244
244
  });
245
+
246
+ it("handles search queries with apostrophes without HTML entity escaping or URL truncation", () => {
247
+ const source =
248
+ "https://api.pc-cms.tele.quebec/api/applicaster/v1/mobile/search?q={{q}}";
249
+
250
+ const mapping = {
251
+ q: { source: "search", property: "q" },
252
+ };
253
+
254
+ const searchContext = getSearchContext("L'aventure", mapping);
255
+
256
+ const result = getInflatedDataSourceUrl({
257
+ source,
258
+ contexts: {
259
+ search: searchContext,
260
+ },
261
+ mapping,
262
+ });
263
+
264
+ expect(result).toBe(
265
+ "https://api.pc-cms.tele.quebec/api/applicaster/v1/mobile/search?q=L'aventure"
266
+ );
267
+
268
+ expect(result).not.toContain("&#x27;");
269
+ });
245
270
  });
@@ -0,0 +1,243 @@
1
+ import { renderHook } from "@testing-library/react-native";
2
+ import * as zappPipesModule from "@applicaster/zapp-react-native-redux/ZappPipes";
3
+ import * as useRouteHook from "@applicaster/zapp-react-native-utils/reactHooks/navigation/useRoute";
4
+ import * as useNavigationHooks from "@applicaster/zapp-react-native-utils/reactHooks/navigation/useNavigation";
5
+ import * as useIsScreenActiveHook from "@applicaster/zapp-react-native-utils/reactHooks/navigation/useIsScreenActive";
6
+ import { refreshCoordinator } from "../../../refreshUtils/RefreshCoordinator";
7
+ import { useMarkPipesDataStale } from "../useMarkPipesDataStale";
8
+
9
+ const mockDispatch = jest.fn();
10
+
11
+ jest.mock("@applicaster/zapp-react-native-redux", () => ({
12
+ ...jest.requireActual("@applicaster/zapp-react-native-redux"),
13
+ useAppDispatch: () => mockDispatch,
14
+ }));
15
+
16
+ jest.mock(
17
+ "@applicaster/zapp-react-native-ui-components/Decorators/RiverFeedLoader/utils/getDatasourceUrl",
18
+ () => ({
19
+ getDatasourceUrl: jest.fn(
20
+ (_ctx, component) => component?.data?.source || null
21
+ ),
22
+ })
23
+ );
24
+
25
+ jest.mock(
26
+ "@applicaster/zapp-react-native-ui-components/Decorators/RiverFeedLoader/utils/usePipesContexts",
27
+ () => ({
28
+ usePipesContexts: jest.fn(() => ({})),
29
+ })
30
+ );
31
+
32
+ describe("useMarkPipesDataStale", () => {
33
+ let mockNavigator: any;
34
+ let mockRoute: any;
35
+ let mockIsActive: boolean;
36
+
37
+ const riverId = "river-123";
38
+
39
+ const refreshableComponent: any = {
40
+ id: "comp-1",
41
+ rules: { clear_cache_on_reload: true },
42
+ data: { source: "https://feed.example.com/data1" },
43
+ };
44
+
45
+ const stringBooleanComponent: any = {
46
+ id: "comp-2",
47
+ rules: { clear_cache_on_reload: "true" },
48
+ data: { source: "https://feed.example.com/data2" },
49
+ };
50
+
51
+ const nonRefreshableComponent: any = {
52
+ id: "comp-3",
53
+ rules: { clear_cache_on_reload: false },
54
+ data: { source: "https://feed.example.com/data3" },
55
+ };
56
+
57
+ const noSourceComponent: any = {
58
+ id: "comp-4",
59
+ rules: { clear_cache_on_reload: true },
60
+ };
61
+
62
+ const allComponents = [
63
+ refreshableComponent,
64
+ stringBooleanComponent,
65
+ nonRefreshableComponent,
66
+ noSourceComponent,
67
+ ];
68
+
69
+ beforeEach(() => {
70
+ jest.clearAllMocks();
71
+
72
+ mockIsActive = true;
73
+
74
+ mockNavigator = {
75
+ currentRoute: "river/river-123",
76
+ };
77
+
78
+ mockRoute = {
79
+ pathname: "river/river-123",
80
+ screenData: { id: riverId },
81
+ };
82
+
83
+ jest
84
+ .spyOn(useNavigationHooks, "useNavigation")
85
+ .mockImplementation(() => mockNavigator);
86
+
87
+ jest.spyOn(useRouteHook, "useRoute").mockImplementation(() => mockRoute);
88
+
89
+ jest
90
+ .spyOn(useIsScreenActiveHook, "useIsScreenActive")
91
+ .mockImplementation(() => mockIsActive);
92
+
93
+ jest
94
+ .spyOn(refreshCoordinator, "triggerRefresh")
95
+ .mockImplementation(jest.fn());
96
+
97
+ jest
98
+ .spyOn(zappPipesModule, "markPipesDataStale")
99
+ .mockImplementation(
100
+ (url, opts) => ({ type: "MARK_STALE", url, opts }) as any
101
+ );
102
+ });
103
+
104
+ describe("on unmount", () => {
105
+ it("marks clear_cache_on_reload feeds stale on unmount", () => {
106
+ const { unmount } = renderHook(() =>
107
+ useMarkPipesDataStale(riverId, allComponents)
108
+ );
109
+
110
+ expect(mockDispatch).not.toHaveBeenCalled();
111
+
112
+ unmount();
113
+
114
+ expect(zappPipesModule.markPipesDataStale).toHaveBeenCalledWith(
115
+ "https://feed.example.com/data1",
116
+ { riverId }
117
+ );
118
+
119
+ expect(zappPipesModule.markPipesDataStale).toHaveBeenCalledWith(
120
+ "https://feed.example.com/data2",
121
+ { riverId }
122
+ );
123
+
124
+ expect(zappPipesModule.markPipesDataStale).not.toHaveBeenCalledWith(
125
+ "https://feed.example.com/data3",
126
+ expect.anything()
127
+ );
128
+
129
+ expect(mockDispatch).toHaveBeenCalledTimes(2);
130
+ });
131
+
132
+ it("does not mark stale on unmount if pathname includes videoModal", () => {
133
+ mockRoute.pathname = "videoModal/item-123";
134
+
135
+ const { unmount } = renderHook(() =>
136
+ useMarkPipesDataStale(riverId, allComponents)
137
+ );
138
+
139
+ unmount();
140
+
141
+ expect(mockDispatch).not.toHaveBeenCalled();
142
+ });
143
+ });
144
+
145
+ describe("on screen return (screen not unmounted in Transitioner)", () => {
146
+ it("does not trigger refresh on initial mount", () => {
147
+ renderHook(() => useMarkPipesDataStale(riverId, allComponents));
148
+
149
+ expect(refreshCoordinator.triggerRefresh).not.toHaveBeenCalled();
150
+ expect(mockDispatch).not.toHaveBeenCalled();
151
+ });
152
+
153
+ it("triggers refresh and marks stale when returning from a deeper screen", () => {
154
+ const { rerender } = renderHook(() =>
155
+ useMarkPipesDataStale(riverId, allComponents)
156
+ );
157
+
158
+ // 1. User navigates away to a deeper screen
159
+ mockNavigator.currentRoute = "river/deeper-screen";
160
+ mockIsActive = false;
161
+ rerender({});
162
+
163
+ expect(refreshCoordinator.triggerRefresh).not.toHaveBeenCalled();
164
+ expect(mockDispatch).not.toHaveBeenCalled();
165
+
166
+ // 2. User presses back and returns to original screen
167
+ mockNavigator.currentRoute = "river/river-123";
168
+ mockIsActive = true;
169
+ rerender({});
170
+
171
+ expect(refreshCoordinator.triggerRefresh).toHaveBeenCalledTimes(1);
172
+
173
+ expect(refreshCoordinator.triggerRefresh).toHaveBeenCalledWith(
174
+ [refreshableComponent, stringBooleanComponent],
175
+ riverId
176
+ );
177
+
178
+ expect(zappPipesModule.markPipesDataStale).toHaveBeenCalledWith(
179
+ "https://feed.example.com/data1",
180
+ { riverId }
181
+ );
182
+
183
+ expect(zappPipesModule.markPipesDataStale).toHaveBeenCalledWith(
184
+ "https://feed.example.com/data2",
185
+ { riverId }
186
+ );
187
+
188
+ expect(mockDispatch).toHaveBeenCalledTimes(2);
189
+ });
190
+
191
+ it("does not trigger duplicate refreshes on subsequent re-renders on the same screen", () => {
192
+ const { rerender } = renderHook(() =>
193
+ useMarkPipesDataStale(riverId, allComponents)
194
+ );
195
+
196
+ // Navigate away
197
+ mockNavigator.currentRoute = "river/deeper-screen";
198
+ mockIsActive = false;
199
+ rerender({});
200
+
201
+ // Return
202
+ mockNavigator.currentRoute = "river/river-123";
203
+ mockIsActive = true;
204
+ rerender({});
205
+
206
+ expect(refreshCoordinator.triggerRefresh).toHaveBeenCalledTimes(1);
207
+
208
+ // Subsequent re-render without navigation
209
+ rerender({});
210
+ rerender({});
211
+
212
+ expect(refreshCoordinator.triggerRefresh).toHaveBeenCalledTimes(1);
213
+ });
214
+
215
+ it("triggers refresh again if user navigates away a second time and returns", () => {
216
+ const { rerender } = renderHook(() =>
217
+ useMarkPipesDataStale(riverId, allComponents)
218
+ );
219
+
220
+ // First trip: navigate away and return
221
+ mockNavigator.currentRoute = "river/screen-2";
222
+ mockIsActive = false;
223
+ rerender({});
224
+
225
+ mockNavigator.currentRoute = "river/river-123";
226
+ mockIsActive = true;
227
+ rerender({});
228
+
229
+ expect(refreshCoordinator.triggerRefresh).toHaveBeenCalledTimes(1);
230
+
231
+ // Second trip: navigate away to another screen and return
232
+ mockNavigator.currentRoute = "river/screen-3";
233
+ mockIsActive = false;
234
+ rerender({});
235
+
236
+ mockNavigator.currentRoute = "river/river-123";
237
+ mockIsActive = true;
238
+ rerender({});
239
+
240
+ expect(refreshCoordinator.triggerRefresh).toHaveBeenCalledTimes(2);
241
+ });
242
+ });
243
+ });
@@ -6,7 +6,11 @@ export { useEntryScreenId } from "./useEntryScreenId";
6
6
 
7
7
  export { useBuildPipesUrl } from "./useBuildPipesUrl";
8
8
 
9
- export { usePipesCacheReset } from "./usePipesCacheReset";
9
+ export {
10
+ useMarkPipesDataStale,
11
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
12
+ usePipesCacheReset,
13
+ } from "./useMarkPipesDataStale";
10
14
 
11
15
  export { useBatchLoading } from "./useBatchLoading";
12
16
 
@@ -159,9 +159,17 @@ export const useBatchLoading = (
159
159
  options.riverId,
160
160
  ]);
161
161
 
162
+ // Initial preload only. Batch loading warms the first batch of component
163
+ // feeds and signals readiness; the feed set is frozen at mount on purpose.
164
+ // Per-component loads and reloads (including stale revalidation) are owned by
165
+ // useFeedLoader in ZappPipesDataConnector. Re-running on `feeds`/input changes
166
+ // would re-fire on every store update and cause redundant dispatch storms, so
167
+ // this intentionally runs once on mount.
168
+ /* eslint-disable @wogns3623/better-exhaustive-deps/exhaustive-deps */
162
169
  React.useEffect(() => {
163
170
  runBatchLoading();
164
- }, [runBatchLoading]); // Adding runBatchLoading as a dependency to ensure that it reloads feeds when clearPipesData is called
171
+ }, []);
172
+ /* eslint-enable @wogns3623/better-exhaustive-deps/exhaustive-deps */
165
173
 
166
174
  React.useEffect(() => {
167
175
  // check if all feeds are ready and set hasEverBeenReady to true