@applicaster/zapp-react-native-utils 15.0.0-rc.99 → 16.0.0-rc.1

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 (63) hide show
  1. package/README.md +0 -6
  2. package/actionUtils/index.ts +7 -0
  3. package/actionsExecutor/ActionExecutorContext.tsx +83 -6
  4. package/appUtils/HooksManager/index.ts +35 -0
  5. package/appUtils/focusManager/treeDataStructure/Tree/__tests__/Tree.test.js +46 -0
  6. package/appUtils/focusManager/treeDataStructure/Tree/index.js +18 -18
  7. package/appUtils/focusManagerAux/utils/index.ts +12 -6
  8. package/appUtils/focusManagerAux/utils/utils.ios.ts +6 -3
  9. package/appUtils/localizationsHelper.ts +4 -0
  10. package/appUtils/playerManager/index.ts +9 -0
  11. package/appUtils/playerManager/player.ts +1 -1
  12. package/appUtils/playerManager/playerNative.ts +2 -1
  13. package/appUtils/playerManager/usePlayer.tsx +5 -3
  14. package/cellUtils/__tests__/cellUtils.test.ts +39 -0
  15. package/cellUtils/index.ts +11 -1
  16. package/componentsUtils/index.ts +8 -0
  17. package/dateUtils/__tests__/dayjs.test.ts +0 -3
  18. package/dateUtils/index.ts +2 -0
  19. package/manifestUtils/_internals/__tests__/index.test.js +41 -0
  20. package/manifestUtils/_internals/index.js +33 -0
  21. package/manifestUtils/defaultManifestConfigurations/player.js +6 -16
  22. package/manifestUtils/fieldUtils/__tests__/fieldUtils.test.js +49 -0
  23. package/manifestUtils/fieldUtils/index.js +54 -0
  24. package/manifestUtils/index.js +2 -0
  25. package/manifestUtils/keys.js +228 -0
  26. package/manifestUtils/mobileAction/button/__tests__/mobileActionButton.test.js +168 -0
  27. package/manifestUtils/mobileAction/button/index.js +140 -0
  28. package/manifestUtils/mobileAction/container/__tests__/mobileActionButtonsContainer.test.js +102 -0
  29. package/manifestUtils/mobileAction/container/index.js +73 -0
  30. package/manifestUtils/mobileAction/groups/__tests__/buildMobileActionButtonGroups.test.js +127 -0
  31. package/manifestUtils/mobileAction/groups/defaults.js +76 -0
  32. package/manifestUtils/mobileAction/groups/index.js +80 -0
  33. package/numberUtils/__tests__/toNumber.test.ts +27 -12
  34. package/numberUtils/__tests__/toPositiveNumber.test.ts +32 -4
  35. package/numberUtils/index.ts +5 -1
  36. package/package.json +3 -3
  37. package/pluginUtils/index.ts +4 -5
  38. package/reactHooks/casting/index.ts +1 -0
  39. package/reactHooks/casting/useIsCasting.tsx +57 -0
  40. package/reactHooks/cell-click/index.ts +2 -1
  41. package/reactHooks/feed/index.ts +0 -2
  42. package/reactHooks/feed/useInflatedUrl.ts +1 -1
  43. package/reactHooks/resolvers/useComponentResolver.ts +13 -3
  44. package/reactHooks/screen/__tests__/useCurrentScreenIsHook.test.ts +103 -0
  45. package/reactHooks/screen/__tests__/useCurrentScreenIsStartupHook.test.ts +94 -0
  46. package/reactHooks/screen/index.ts +4 -0
  47. package/reactHooks/screen/useCurrentScreenIsHook.ts +9 -0
  48. package/reactHooks/screen/useCurrentScreenIsStartupHook.ts +8 -0
  49. package/reactHooks/state/__tests__/useComponentScreenState.test.ts +246 -0
  50. package/reactHooks/state/index.ts +2 -0
  51. package/reactHooks/state/useComponentScreenState.ts +45 -0
  52. package/refreshUtils/RefreshCoordinator/__tests__/refreshCoordinator.test.ts +206 -0
  53. package/refreshUtils/RefreshCoordinator/index.ts +245 -0
  54. package/refreshUtils/RefreshCoordinator/utils/__tests__/getDataRefreshConfig.test.ts +104 -0
  55. package/refreshUtils/RefreshCoordinator/utils/index.ts +29 -0
  56. package/screenPickerUtils/index.ts +5 -0
  57. package/screenUtils/index.ts +3 -0
  58. package/utils/__tests__/clone.test.ts +158 -0
  59. package/utils/__tests__/path.test.ts +7 -0
  60. package/utils/clone.ts +7 -0
  61. package/utils/index.ts +2 -1
  62. package/reactHooks/feed/__tests__/useFeedRefresh.test.tsx +0 -75
  63. package/reactHooks/feed/useFeedRefresh.tsx +0 -65
@@ -0,0 +1,245 @@
1
+ import { noop } from "@applicaster/zapp-react-native-utils/functionUtils";
2
+ import { interval, Subject, Subscription } from "rxjs";
3
+ import { map, filter, throttleTime } from "rxjs/operators";
4
+
5
+ import { getDataRefreshConfig } from "./utils";
6
+
7
+ export { getDataRefreshConfig, type RefreshConfig } from "./utils";
8
+
9
+ const THROTTLE_TIMEOUT = 500; // ms
10
+
11
+ type ComponentToRefresh = {
12
+ component: ZappUIComponent;
13
+ screenId: string;
14
+ };
15
+
16
+ /**
17
+ * Public contract for refresh coordination.
18
+ *
19
+ * The coordinator serves as an event bus for component refresh events.
20
+ * It supports two types of producers and any number of consumers:
21
+ *
22
+ * Producers (push events into refresh$):
23
+ * register() → starts a periodic timer that emits refresh events
24
+ * triggerRefresh() → imperatively emits refresh events (e.g. pull-to-refresh)
25
+ *
26
+ * Consumers (react to refresh events):
27
+ * subscribeTo() → listen to refresh events for a specific component/screen
28
+ * subscribeToAny() → listen to all refresh events with throttling
29
+ *
30
+ * Note: subscribeTo() works independently of register(). A component can
31
+ * subscribe to refresh events without having auto-refresh enabled.
32
+ * This is how pull-to-refresh works — triggerRefresh() pushes events that
33
+ * are picked up by existing subscribeTo() listeners in UrlFeedResolver,
34
+ * even for components that don't have periodic auto-refresh timers.
35
+ */
36
+ interface IRefreshCoordinator {
37
+ register(component: ZappUIComponent, screenId: string): () => void;
38
+ triggerRefresh(components: ZappUIComponent[], screenId: string): void;
39
+ subscribeTo(
40
+ component: ZappUIComponent,
41
+ screenId: string,
42
+ listener: () => void
43
+ ): Subscription;
44
+ subscribeToAny(
45
+ listener: (componentToRefresh: ComponentToRefresh) => void
46
+ ): Subscription;
47
+ }
48
+
49
+ /**
50
+ * Singleton implementation of RefreshCoordinator.
51
+ *
52
+ * Design goals:
53
+ * - One shared coordinator across the app
54
+ * - Central refresh$ event bus for all refresh events
55
+ * - Multiple producers: periodic timers (register) and imperative triggers (triggerRefresh)
56
+ * - Multiple consumers: per-component (subscribeTo) and global (subscribeToAny)
57
+ * - Producers and consumers are independent — subscribeTo works without register
58
+ * - Safe cleanup of timers and subscriptions
59
+ */
60
+ class RefreshCoordinator implements IRefreshCoordinator {
61
+ private static instance: RefreshCoordinator;
62
+
63
+ /**
64
+ * Active timers per component/screen pair ((componentId, screenId) → RxJS Subscription).
65
+ * Each entry is keyed by a composite of componentId and screenId (via getKey),
66
+ * and gets its own interval stream for periodic refresh.
67
+ */
68
+ private timers: Map<string, Subscription> = new Map();
69
+
70
+ /**
71
+ * Central event stream emitting components that need refresh.
72
+ * All component interval streams push into this Subject.
73
+ * Multiple listeners can subscribe to receive refresh events for specific components.
74
+ * This is the core stream that coordinates all refresh operations.
75
+ */
76
+ private refresh$ = new Subject<ComponentToRefresh>();
77
+
78
+ // Store the current single subscriber for subscribeToAny
79
+ private anyListenerSubscription?: Subscription;
80
+
81
+ private constructor() {}
82
+
83
+ public static getInstance(): RefreshCoordinator {
84
+ if (!RefreshCoordinator.instance) {
85
+ RefreshCoordinator.instance = new RefreshCoordinator();
86
+ }
87
+
88
+ return RefreshCoordinator.instance;
89
+ }
90
+
91
+ /**
92
+ * Registers a component for periodic refresh.
93
+ *
94
+ * Behavior:
95
+ * - If the component was previously registered, its timer is replaced.
96
+ * - If refreshing is disabled, nothing is scheduled.
97
+ * - Returns an unregister function for manual cleanup.
98
+ * - Each component gets its own interval stream for periodic refresh.
99
+ */
100
+ public register(component: ZappUIComponent, screenId: string): () => void {
101
+ const componentId: ZappUIComponent["id"] = component.id;
102
+
103
+ // Ensure no duplicate timer exists for this component
104
+ this.unregister(componentId, screenId);
105
+
106
+ const refreshConfig = getDataRefreshConfig(component);
107
+
108
+ // If refreshing disabled → do nothing
109
+ if (!refreshConfig.isRefreshingEnabled) {
110
+ return noop;
111
+ }
112
+
113
+ /**
114
+ * Create interval stream:
115
+ * - Emits every refreshIntervalMs
116
+ * - Maps emission to the ComponentToRefresh object
117
+ * - Pushes component into central refresh$ stream
118
+ */
119
+ const subscription: Subscription = interval(refreshConfig.refreshIntervalMs)
120
+ .pipe(
121
+ map(() => ({
122
+ component,
123
+ screenId,
124
+ }))
125
+ )
126
+ .subscribe(this.refresh$);
127
+
128
+ this.timers.set(this.getKey(componentId, screenId), subscription);
129
+
130
+ return (): void => this.unregister(componentId, screenId);
131
+ }
132
+
133
+ /**
134
+ * Imperatively triggers refresh for the given components on a screen.
135
+ *
136
+ * Pushes each component into the central refresh$ stream so that
137
+ * existing subscribers (e.g. UrlFeedResolver's reloadData) react
138
+ * to the event. Used by pull-to-refresh and programmatic refresh.
139
+ */
140
+ public triggerRefresh(components: ZappUIComponent[], screenId: string): void {
141
+ components.forEach((component) => {
142
+ this.refresh$.next({ component, screenId });
143
+ });
144
+ }
145
+
146
+ /**
147
+ * Stops and removes timer for a specific component.
148
+ * Cleans up the RxJS subscription for that component's interval stream.
149
+ */
150
+ private unregister(
151
+ componentId: ZappUIComponent["id"],
152
+ screenId: string
153
+ ): void {
154
+ const key = this.getKey(componentId, screenId);
155
+ const subscription: Subscription | undefined = this.timers.get(key);
156
+
157
+ if (subscription) {
158
+ subscription.unsubscribe();
159
+ this.timers.delete(key);
160
+ }
161
+ }
162
+
163
+ private getKey(componentId: ZappUIComponent["id"], screenId: string): string {
164
+ return `componentId:${componentId}___screenId:${screenId}`;
165
+ }
166
+
167
+ /**
168
+ * Subscribes to refresh events for a specific component.
169
+ *
170
+ * Each subscription is independent - multiple listeners can subscribe
171
+ * to the same component's refresh events.
172
+ */
173
+ public subscribeTo(
174
+ component: ZappUIComponent,
175
+ screenId: string,
176
+ listener: () => void
177
+ ): Subscription {
178
+ return this.refresh$
179
+ .pipe(
180
+ filter(
181
+ (componentToRefresh: ComponentToRefresh) =>
182
+ componentToRefresh.component.id === component.id &&
183
+ componentToRefresh.screenId === screenId
184
+ )
185
+ )
186
+ .subscribe(listener);
187
+ }
188
+
189
+ /**
190
+ * Subscribes a single global listener to all refresh events.
191
+ *
192
+ * Behavior:
193
+ * - Only one active listener exists at a time.
194
+ * - If a previous listener was already subscribed, it is automatically unsubscribed.
195
+ * - Throttles refresh events to emit at most once per THROTTLE_TIMEOUT interval.
196
+ * - Useful for scenarios where you want a single handler to react
197
+ * to any component refresh without having multiple active callbacks.
198
+ *
199
+ * @param listener - Callback to invoke whenever any component triggers a refresh
200
+ * @returns The RxJS Subscription object for this listener
201
+ */
202
+ public subscribeToAny(
203
+ listener: (componentToRefresh: ComponentToRefresh) => void
204
+ ): Subscription {
205
+ // Unsubscribe previous listener if present to enforce "single active listener" policy
206
+ if (this.anyListenerSubscription) {
207
+ this.anyListenerSubscription.unsubscribe();
208
+ }
209
+
210
+ // Create a new throttled subscription
211
+ this.anyListenerSubscription = this.refresh$
212
+ .pipe(throttleTime(THROTTLE_TIMEOUT))
213
+ .subscribe(listener);
214
+
215
+ return this.anyListenerSubscription;
216
+ }
217
+
218
+ /**
219
+ * Fully resets coordinator state.
220
+ *
221
+ * Clears:
222
+ * - All component timers
223
+ * - Any active "subscribeToAny" listener
224
+ * - Recreates internal Subject to allow fresh subscriptions
225
+ */
226
+ public clear(): void {
227
+ // Stop and remove all active timers
228
+ this.timers.forEach((sub: Subscription) => sub.unsubscribe());
229
+ this.timers.clear();
230
+
231
+ // Stop any active "subscribeToAny" subscription
232
+ if (this.anyListenerSubscription) {
233
+ this.anyListenerSubscription.unsubscribe();
234
+ this.anyListenerSubscription = undefined;
235
+ }
236
+
237
+ // Complete old Subject to release subscribers
238
+ this.refresh$.complete();
239
+
240
+ // Recreate Subject so coordinator can be reused
241
+ this.refresh$ = new Subject<ComponentToRefresh>();
242
+ }
243
+ }
244
+
245
+ export const refreshCoordinator = RefreshCoordinator.getInstance();
@@ -0,0 +1,104 @@
1
+ import {
2
+ getDataRefreshConfig,
3
+ MINIMUM_REFRESHING_INTERVAL_IN_SECONDS,
4
+ DEFAULT_REFRESHING_INTERVAL_IN_SECONDS,
5
+ } from "..";
6
+
7
+ describe("getDataRefreshConfig", () => {
8
+ it("returns correct values when rules are defined", () => {
9
+ const component = {
10
+ rules: {
11
+ enable_data_refreshing: true,
12
+ refreshing_interval: 30,
13
+ },
14
+ } as any;
15
+
16
+ const result = getDataRefreshConfig(component);
17
+
18
+ expect(result).toEqual({
19
+ isRefreshingEnabled: true,
20
+ refreshIntervalMs: 30 * 1000,
21
+ });
22
+ });
23
+
24
+ it("returns false for isRefreshingEnabled if rules flag is false", () => {
25
+ const component = {
26
+ rules: {
27
+ enable_data_refreshing: false,
28
+ refreshing_interval: 45,
29
+ },
30
+ } as any;
31
+
32
+ const result = getDataRefreshConfig(component);
33
+
34
+ expect(result).toEqual({
35
+ isRefreshingEnabled: false,
36
+ refreshIntervalMs: 45 * 1000,
37
+ });
38
+ });
39
+
40
+ it("applies minimum interval if given interval is too low", () => {
41
+ const component = {
42
+ rules: {
43
+ enable_data_refreshing: true,
44
+ refreshing_interval: 2,
45
+ },
46
+ } as any;
47
+
48
+ const result = getDataRefreshConfig(component);
49
+
50
+ expect(result.refreshIntervalMs).toBe(
51
+ MINIMUM_REFRESHING_INTERVAL_IN_SECONDS * 1000
52
+ );
53
+ });
54
+
55
+ it("uses default interval when rules.refreshing_interval is undefined", () => {
56
+ const component = {
57
+ rules: {
58
+ enable_data_refreshing: true,
59
+ },
60
+ } as any;
61
+
62
+ const result = getDataRefreshConfig(component);
63
+
64
+ expect(result.refreshIntervalMs).toBe(
65
+ DEFAULT_REFRESHING_INTERVAL_IN_SECONDS * 1000
66
+ );
67
+ });
68
+
69
+ it("handles missing rules gracefully", () => {
70
+ const component = {} as any;
71
+
72
+ const result = getDataRefreshConfig(component);
73
+
74
+ expect(result).toEqual({
75
+ isRefreshingEnabled: false,
76
+ refreshIntervalMs: DEFAULT_REFRESHING_INTERVAL_IN_SECONDS * 1000,
77
+ });
78
+ });
79
+
80
+ it("handles null component gracefully", () => {
81
+ const result = getDataRefreshConfig(null as any);
82
+
83
+ expect(result).toEqual({
84
+ isRefreshingEnabled: false,
85
+ refreshIntervalMs: DEFAULT_REFRESHING_INTERVAL_IN_SECONDS * 1000,
86
+ });
87
+ });
88
+
89
+ it("handles string values for rules correctly", () => {
90
+ const component = {
91
+ rules: {
92
+ enable_data_refreshing: "true",
93
+ refreshing_interval: "25",
94
+ },
95
+ } as any;
96
+
97
+ const result = getDataRefreshConfig(component);
98
+
99
+ expect(result).toEqual({
100
+ isRefreshingEnabled: true,
101
+ refreshIntervalMs: 25 * 1000,
102
+ });
103
+ });
104
+ });
@@ -0,0 +1,29 @@
1
+ import { isTrue } from "@applicaster/zapp-react-native-utils/booleanUtils";
2
+ import { toNumberWithDefault } from "@applicaster/zapp-react-native-utils/numberUtils";
3
+
4
+ export const MINIMUM_REFRESHING_INTERVAL_IN_SECONDS = 5; // 5 seconds
5
+
6
+ export const DEFAULT_REFRESHING_INTERVAL_IN_SECONDS = 60; // 1 minute
7
+
8
+ export type RefreshConfig = {
9
+ isRefreshingEnabled: boolean;
10
+ refreshIntervalMs: number;
11
+ };
12
+
13
+ export const getDataRefreshConfig = (
14
+ component: ZappUIComponent
15
+ ): RefreshConfig => {
16
+ const isRefreshingEnabled = isTrue(component?.rules?.enable_data_refreshing);
17
+
18
+ const refreshing_interval = toNumberWithDefault(
19
+ DEFAULT_REFRESHING_INTERVAL_IN_SECONDS,
20
+ component?.rules?.refreshing_interval
21
+ );
22
+
23
+ return {
24
+ isRefreshingEnabled,
25
+ refreshIntervalMs:
26
+ Math.max(refreshing_interval, MINIMUM_REFRESHING_INTERVAL_IN_SECONDS) *
27
+ 1000,
28
+ };
29
+ };
@@ -1,3 +1,5 @@
1
+ import { endsWith } from "@applicaster/zapp-react-native-utils/utils";
2
+
1
3
  export const getFocusableId = (id) => `PickerItem.${id}`;
2
4
 
3
5
  export const getPickerSelectorId = (id) => `PickerSelector.${id}`;
@@ -11,3 +13,6 @@ export const getScreenPickerSelectorContainerId = (id) =>
11
13
 
12
14
  export const getScreenPickerContentContainerId = (id) =>
13
15
  `${getScreenPickerId(id)}-screen-container`;
16
+
17
+ export const isTabsScreenContentContainerId = (id) =>
18
+ endsWith("-screen-container", id);
@@ -0,0 +1,3 @@
1
+ export const TV_SCREEN_HEIGHT = 1080;
2
+
3
+ export const TV_SCREEN_WIDTH = 1920;
@@ -0,0 +1,158 @@
1
+ import { clone } from "../clone";
2
+
3
+ describe("clone", () => {
4
+ const originalStructuredClone = global.structuredClone;
5
+
6
+ afterEach(() => {
7
+ // Restore the original structuredClone after each test
8
+ global.structuredClone = originalStructuredClone;
9
+ });
10
+
11
+ describe("when structuredClone is available", () => {
12
+ beforeEach(() => {
13
+ // Mock structuredClone if it's not available
14
+ if (typeof global.structuredClone === "undefined") {
15
+ global.structuredClone = jest.fn((value) =>
16
+ JSON.parse(JSON.stringify(value))
17
+ );
18
+ }
19
+ });
20
+
21
+ test("clones primitive values", () => {
22
+ expect(clone(42)).toBe(42);
23
+ expect(clone("test")).toBe("test");
24
+ expect(clone(true)).toBe(true);
25
+ expect(clone(null)).toBe(null);
26
+ });
27
+
28
+ test("clones simple objects", () => {
29
+ const original = { a: 1, b: 2 };
30
+ const cloned = clone(original);
31
+
32
+ expect(cloned).toEqual(original);
33
+ expect(cloned).not.toBe(original);
34
+ });
35
+
36
+ test("clones nested objects", () => {
37
+ const original = { a: { b: { c: 1 } } };
38
+ const cloned = clone(original);
39
+
40
+ expect(cloned).toEqual(original);
41
+ expect(cloned).not.toBe(original);
42
+ expect(cloned.a).not.toBe(original.a);
43
+ expect(cloned.a.b).not.toBe(original.a.b);
44
+ });
45
+
46
+ test("clones arrays", () => {
47
+ const original = [1, 2, 3];
48
+ const cloned = clone(original);
49
+
50
+ expect(cloned).toEqual(original);
51
+ expect(cloned).not.toBe(original);
52
+ });
53
+
54
+ test("clones arrays with nested objects", () => {
55
+ const original = [{ a: 1 }, { b: 2 }];
56
+ const cloned = clone(original);
57
+
58
+ expect(cloned).toEqual(original);
59
+ expect(cloned).not.toBe(original);
60
+ expect(cloned[0]).not.toBe(original[0]);
61
+ expect(cloned[1]).not.toBe(original[1]);
62
+ });
63
+
64
+ test("does not mutate the original value", () => {
65
+ const original = { a: 1, b: { c: 2 } };
66
+ const cloned = clone(original);
67
+
68
+ cloned.a = 99;
69
+ cloned.b.c = 99;
70
+
71
+ expect(original.a).toBe(1);
72
+ expect(original.b.c).toBe(2);
73
+ });
74
+ });
75
+
76
+ describe("when structuredClone is not available (fallback to cloneDeep)", () => {
77
+ beforeEach(() => {
78
+ // Delete structuredClone to force fallback to cloneDeep
79
+ delete (global as any).structuredClone;
80
+ });
81
+
82
+ test("clones primitive values using cloneDeep", () => {
83
+ expect(clone(42)).toBe(42);
84
+ expect(clone("test")).toBe("test");
85
+ expect(clone(true)).toBe(true);
86
+ expect(clone(null)).toBe(null);
87
+ });
88
+
89
+ test("clones simple objects using cloneDeep", () => {
90
+ const original = { a: 1, b: 2 };
91
+ const cloned = clone(original);
92
+
93
+ expect(cloned).toEqual(original);
94
+ expect(cloned).not.toBe(original);
95
+ });
96
+
97
+ test("clones nested objects using cloneDeep", () => {
98
+ const original = { a: { b: { c: 1 } } };
99
+ const cloned = clone(original);
100
+
101
+ expect(cloned).toEqual(original);
102
+ expect(cloned).not.toBe(original);
103
+ expect(cloned.a).not.toBe(original.a);
104
+ expect(cloned.a.b).not.toBe(original.a.b);
105
+ });
106
+
107
+ test("clones arrays using cloneDeep", () => {
108
+ const original = [1, 2, 3];
109
+ const cloned = clone(original);
110
+
111
+ expect(cloned).toEqual(original);
112
+ expect(cloned).not.toBe(original);
113
+ });
114
+
115
+ test("clones arrays with nested objects using cloneDeep", () => {
116
+ const original = [{ a: 1 }, { b: 2 }];
117
+ const cloned = clone(original);
118
+
119
+ expect(cloned).toEqual(original);
120
+ expect(cloned).not.toBe(original);
121
+ expect(cloned[0]).not.toBe(original[0]);
122
+ expect(cloned[1]).not.toBe(original[1]);
123
+ });
124
+
125
+ test("does not mutate the original value when using cloneDeep", () => {
126
+ const original = { a: 1, b: { c: 2 } };
127
+ const cloned = clone(original);
128
+
129
+ cloned.a = 99;
130
+ cloned.b.c = 99;
131
+
132
+ expect(original.a).toBe(1);
133
+ expect(original.b.c).toBe(2);
134
+ });
135
+
136
+ test("handles complex objects with multiple levels", () => {
137
+ const original = {
138
+ name: "test",
139
+ data: {
140
+ items: [
141
+ { id: 1, value: "a" },
142
+ { id: 2, value: "b" },
143
+ ],
144
+ meta: { count: 2 },
145
+ },
146
+ };
147
+
148
+ const cloned = clone(original);
149
+
150
+ expect(cloned).toEqual(original);
151
+ expect(cloned).not.toBe(original);
152
+ expect(cloned.data).not.toBe(original.data);
153
+ expect(cloned.data.items).not.toBe(original.data.items);
154
+ expect(cloned.data.items[0]).not.toBe(original.data.items[0]);
155
+ expect(cloned.data.meta).not.toBe(original.data.meta);
156
+ });
157
+ });
158
+ });
@@ -31,3 +31,10 @@ test("example 4", () => {
31
31
 
32
32
  expect(path(route, xs)).toBeUndefined();
33
33
  });
34
+
35
+ test("example 5", () => {
36
+ const route = ["a", "b", 0];
37
+ const xs = { a: { b: [1, 2, 3] } };
38
+
39
+ expect(path(route, xs)).toBe(1);
40
+ });
package/utils/clone.ts ADDED
@@ -0,0 +1,7 @@
1
+ import { cloneDeep } from "lodash";
2
+
3
+ export const clone = (value) => {
4
+ return typeof structuredClone !== "undefined"
5
+ ? structuredClone(value) // no support for hermes, fallback to cloneDeep
6
+ : cloneDeep(value);
7
+ };
package/utils/index.ts CHANGED
@@ -20,8 +20,9 @@ export { mapAccum } from "./mapAccum";
20
20
 
21
21
  export { mergeRight } from "./mergeRight";
22
22
 
23
+ export { clone } from "./clone";
24
+
23
25
  export {
24
- cloneDeep as clone,
25
26
  flatten,
26
27
  drop,
27
28
  size,
@@ -1,75 +0,0 @@
1
- import { renderHook } from "@testing-library/react-native";
2
-
3
- jest.useFakeTimers();
4
-
5
- jest.mock("@applicaster/zapp-react-native-utils/reactHooks/navigation", () => ({
6
- useIsScreenActive: () => true,
7
- }));
8
-
9
- const { useFeedRefresh, feedRefreshLogger } = require("..");
10
-
11
- describe("useFeedRefresh", () => {
12
- const reloadData = jest.fn();
13
-
14
- const component = {
15
- id: "foo",
16
- rules: { enable_data_refreshing: true, refreshing_interval: 61 },
17
- };
18
-
19
- it("Calls reloadData after passed time lapses", () => {
20
- renderHook(() => useFeedRefresh({ reloadData, component }));
21
-
22
- expect(reloadData).not.toBeCalled();
23
- jest.runOnlyPendingTimers();
24
- expect(reloadData).toBeCalled();
25
- });
26
-
27
- it("Logs warning message if refresh time set below minimum value", () => {
28
- const loggerSpy = jest.spyOn(feedRefreshLogger, "warning");
29
-
30
- renderHook(() =>
31
- useFeedRefresh({
32
- reloadData,
33
- component: {
34
- ...component,
35
- rules: { enable_data_refreshing: true, refreshing_interval: 2 },
36
- },
37
- })
38
- );
39
-
40
- expect(loggerSpy).toBeCalled();
41
- });
42
-
43
- it("Calls reloadData on a Component that had a timer setup and was re-mounted", () => {
44
- reloadData.mockReset();
45
-
46
- // it is not called before rendering hook"
47
- expect(reloadData).not.toBeCalled();
48
-
49
- // it is not called when rendering new component (Not previously rendered)
50
- renderHook(() =>
51
- useFeedRefresh({ reloadData, component: { ...component, id: "bar" } })
52
- );
53
-
54
- expect(reloadData).not.toBeCalled();
55
-
56
- // it is called when rendering previously rendered component",
57
- renderHook(() => useFeedRefresh({ reloadData, component }));
58
- expect(reloadData).toBeCalled();
59
- });
60
-
61
- it("Doesn't refresh data when enable_data_refreshing if false", () => {
62
- reloadData.mockReset();
63
-
64
- renderHook(() =>
65
- useFeedRefresh({
66
- reloadData,
67
- component: { ...component, rules: { enable_data_refreshing: false } },
68
- })
69
- );
70
-
71
- jest.runOnlyPendingTimers();
72
-
73
- expect(reloadData).not.toBeCalled();
74
- });
75
- });