@applicaster/zapp-react-native-utils 16.0.0-alpha.4380012274 → 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 (33) hide show
  1. package/actionsExecutor/__tests__/StorageActions.test.ts +98 -0
  2. package/actionsExecutor/__tests__/feedDecorator.test.ts +30 -0
  3. package/actionsExecutor/__tests__/screenToggleFlag.test.ts +90 -0
  4. package/actionsExecutor/actions/__tests__/openBottomSheet.inlineItems.test.ts +72 -0
  5. package/actionsExecutor/actions/openBottomSheet.ts +1 -0
  6. package/actionsExecutor/actions/screenToggleFlag.ts +1 -3
  7. package/actionsExecutor/feedDecorator.ts +6 -0
  8. package/appUtils/contextKeysManager/__tests__/getKey/failure.test.ts +1 -22
  9. package/appUtils/contextKeysManager/__tests__/getKey/success/fullSearch.test.ts +1 -25
  10. package/appUtils/contextKeysManager/__tests__/getKey/success/searchInSecure.test.ts +1 -7
  11. package/appUtils/contextKeysManager/__tests__/removeKey/failure.test.ts +0 -25
  12. package/appUtils/contextKeysManager/__tests__/removeKey/success.test.ts +0 -6
  13. package/appUtils/contextKeysManager/__tests__/setKey/failure/failedToSaveInStorage.test.ts +0 -40
  14. package/appUtils/contextKeysManager/__tests__/setKey/{success/saveItemAndReference.test.ts → success.test.ts} +2 -42
  15. package/appUtils/contextKeysManager/index.ts +28 -129
  16. package/configurationUtils/__tests__/modalBlocksParser.test.ts +11 -0
  17. package/modalState/ModalOrchestrator.ts +1 -0
  18. package/modalState/__tests__/ModalOrchestrator.phase3.test.ts +23 -0
  19. package/modalState/index.ts +2 -0
  20. package/modalState/types.ts +11 -0
  21. package/package.json +2 -2
  22. package/reactHooks/feed/__tests__/feedInvalidation.test.tsx +338 -0
  23. package/reactHooks/feed/__tests__/useBatchLoading.test.tsx +8 -2
  24. package/reactHooks/feed/useFeedLoader.tsx +28 -1
  25. package/reactHooks/feed/useLoadPipesDataDispatch.ts +12 -11
  26. package/appUtils/contextKeysManager/__tests__/getKey/success/byReference.test.ts +0 -56
  27. package/appUtils/contextKeysManager/__tests__/getKeyByReference/failure.test.ts +0 -265
  28. package/appUtils/contextKeysManager/__tests__/getKeyByReference/success.test.ts +0 -303
  29. package/appUtils/contextKeysManager/__tests__/removeReferenceForKey/failure.test.ts +0 -82
  30. package/appUtils/contextKeysManager/__tests__/removeReferenceForKey/success.test.ts +0 -41
  31. package/appUtils/contextKeysManager/__tests__/setKey/success/saveItemButNotReference.test.ts +0 -208
  32. package/appUtils/contextKeysManager/__tests__/setReferenceForKey/failure.test.ts +0 -93
  33. package/appUtils/contextKeysManager/__tests__/setReferenceForKey/success.test.ts +0 -45
@@ -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
 
@@ -15,6 +15,7 @@ type Props = {
15
15
  pipesOptions?: {
16
16
  clearCache?: boolean;
17
17
  loadLocalFavourites?: boolean;
18
+ loadLocalFavorites?: boolean;
18
19
  silentRefresh?: boolean;
19
20
  method?: "get" | "post";
20
21
  bodyParams?: Record<string, unknown>;
@@ -45,11 +46,36 @@ export const useFeedLoader = ({
45
46
 
46
47
  const loadPipesDataDispatcher = useLoadPipesDataDispatch();
47
48
 
49
+ useEffect(() => {
50
+ if (
51
+ currentFeed?.stale &&
52
+ !currentFeed.refreshing &&
53
+ (!currentFeed.error || isInitialRender) &&
54
+ !pipesOptions.skipLoading
55
+ ) {
56
+ loadPipesDataDispatcher(
57
+ callableFeedUrl,
58
+ { ...pipesOptions, clearCache: false, silentRefresh: true },
59
+ { withResolvers: true }
60
+ );
61
+ }
62
+ }, [
63
+ callableFeedUrl,
64
+ currentFeed?.stale,
65
+ currentFeed?.invalidationVersion,
66
+ currentFeed?.refreshing,
67
+ currentFeed?.error,
68
+ pipesOptions,
69
+ loadPipesDataDispatcher,
70
+ isInitialRender,
71
+ ]);
72
+
48
73
  const reloadData = React.useCallback<ReloadDataFunction>(
49
74
  (silentRefresh = true, callback) => {
50
75
  loadPipesDataDispatcher(
51
76
  callableFeedUrl,
52
77
  {
78
+ ...pipesOptions,
53
79
  clearCache: true,
54
80
  silentRefresh,
55
81
  callback,
@@ -59,7 +85,7 @@ export const useFeedLoader = ({
59
85
  }
60
86
  );
61
87
  },
62
- [callableFeedUrl]
88
+ [callableFeedUrl, pipesOptions, loadPipesDataDispatcher]
63
89
  );
64
90
 
65
91
  const loadNext: FeedLoaderResponse["loadNext"] = React.useCallback(() => {
@@ -81,6 +107,7 @@ export const useFeedLoader = ({
81
107
 
82
108
  useEffect(() => {
83
109
  if (
110
+ !currentFeed?.stale &&
84
111
  shouldDispatchData(callableFeedUrl, currentFeed, pipesOptions.clearCache)
85
112
  ) {
86
113
  if (callableFeedUrl && !pipesOptions.skipLoading) {
@@ -19,14 +19,6 @@ export const useLoadPipesDataDispatch = () => {
19
19
  (screenData as LegacyNavigationScreenData)?.targetScreen?.id ??
20
20
  screenData?.id;
21
21
 
22
- const onLoadCB = (options) => (data, _err) => {
23
- options?.callback?.();
24
-
25
- if (data) {
26
- applyScreenRouteDefaults(data, screenStateStore, pathname);
27
- }
28
- };
29
-
30
22
  return React.useCallback(
31
23
  (
32
24
  url: string,
@@ -36,6 +28,9 @@ export const useLoadPipesDataDispatch = () => {
36
28
  clearCache?: boolean;
37
29
  silentRefresh?: boolean;
38
30
  parentFeed?: string;
31
+ method?: "get" | "post";
32
+ bodyParams?: Record<string, unknown>;
33
+ loadLocalFavorites?: boolean;
39
34
  } = {},
40
35
  {
41
36
  withResolvers = false,
@@ -46,18 +41,24 @@ export const useLoadPipesDataDispatch = () => {
46
41
  } = {}
47
42
  ) => {
48
43
  if (url) {
49
- dispatch(
44
+ return dispatch(
50
45
  ZappPipes.loadPipesData(url, {
51
46
  riverId,
52
47
  resolvers: withResolvers ? resolvers : undefined,
53
48
  ...options,
54
49
  callback: withScreenRouteMapping
55
- ? onLoadCB(options)
50
+ ? (data, error) => {
51
+ options.callback?.(data, error);
52
+
53
+ if (data) {
54
+ applyScreenRouteDefaults(data, screenStateStore, pathname);
55
+ }
56
+ }
56
57
  : options?.callback,
57
58
  })
58
59
  );
59
60
  }
60
61
  },
61
- []
62
+ [dispatch, riverId, resolvers, screenStateStore, pathname]
62
63
  );
63
64
  };
@@ -1,56 +0,0 @@
1
- import { ContextKeysManager } from "../../..";
2
-
3
- describe("Context Keys Manager - getKey", () => {
4
- it("returns value if found by reference", async () => {
5
- // setup
6
- const key = "namespace.key";
7
- const value = "value";
8
-
9
- const mockedLogger = {
10
- warn: jest.fn(),
11
- };
12
-
13
- const mockedSessionStorage = {
14
- getItem: jest.fn(),
15
- setItem: jest.fn(),
16
- removeItem: jest.fn(),
17
- };
18
-
19
- const mockedLocalStorage = {
20
- getItem: jest.fn(),
21
- setItem: jest.fn(),
22
- removeItem: jest.fn(),
23
- };
24
-
25
- const mockedSecureStorage = {
26
- getItem: jest.fn(),
27
- setItem: jest.fn(),
28
- removeItem: jest.fn(),
29
- };
30
-
31
- const contextManager = new ContextKeysManager({
32
- logger: mockedLogger,
33
- sessionStorage: mockedSessionStorage,
34
- localStorage: mockedLocalStorage,
35
- secureStorage: mockedSecureStorage,
36
- });
37
-
38
- const getKeyByReference = jest
39
- .spyOn(contextManager, "getKeyByReference")
40
- .mockResolvedValueOnce(value);
41
-
42
- // run
43
- const result = await contextManager.getKey(key);
44
-
45
- // verify
46
- expect(result).toEqual(value);
47
-
48
- expect(mockedLogger.warn).not.toHaveBeenCalled();
49
-
50
- expect(getKeyByReference).toHaveBeenCalledWith("key", "namespace");
51
-
52
- expect(mockedSessionStorage.getItem).not.toHaveBeenCalled();
53
- expect(mockedLocalStorage.getItem).not.toHaveBeenCalled();
54
- expect(mockedSecureStorage.getItem).not.toHaveBeenCalled();
55
- });
56
- });