@applicaster/zapp-react-native-utils 15.0.0-rc.97 → 15.0.0-rc.98

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@applicaster/zapp-react-native-utils",
3
- "version": "15.0.0-rc.97",
3
+ "version": "15.0.0-rc.98",
4
4
  "description": "Applicaster Zapp React Native utilities package",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -27,7 +27,7 @@
27
27
  },
28
28
  "homepage": "https://github.com/applicaster/quickbrick#readme",
29
29
  "dependencies": {
30
- "@applicaster/applicaster-types": "15.0.0-rc.97",
30
+ "@applicaster/applicaster-types": "15.0.0-rc.98",
31
31
  "buffer": "^5.2.1",
32
32
  "camelize": "^1.0.0",
33
33
  "dayjs": "^1.11.10",
@@ -0,0 +1,537 @@
1
+ import { renderHook, act } from "@testing-library/react-native";
2
+ import { useSendAnalyticsOnPress } from "../index";
3
+
4
+ // Mock dependencies
5
+ jest.mock("../../../analyticsUtils", () => ({
6
+ useAnalytics: jest.fn(),
7
+ }));
8
+
9
+ jest.mock(
10
+ "@applicaster/zapp-react-native-utils/analyticsUtils/helpers/hooks",
11
+ () => ({
12
+ useSendAnalyticsEventWithFunction: jest.fn(),
13
+ })
14
+ );
15
+
16
+ import { useAnalytics } from "../../../analyticsUtils";
17
+ import { useSendAnalyticsEventWithFunction } from "@applicaster/zapp-react-native-utils/analyticsUtils/helpers/hooks";
18
+
19
+ describe("useSendAnalyticsOnPress", () => {
20
+ const mockUseAnalytics = useAnalytics as jest.MockedFunction<
21
+ typeof useAnalytics
22
+ >;
23
+
24
+ const mockUseSendAnalyticsEventWithFunction =
25
+ useSendAnalyticsEventWithFunction as jest.MockedFunction<
26
+ typeof useSendAnalyticsEventWithFunction
27
+ >;
28
+
29
+ let mockSendOnClickEvent: jest.Mock;
30
+ let mockCallbackSendAnalyticsEvent: jest.Mock;
31
+
32
+ beforeEach(() => {
33
+ jest.clearAllMocks();
34
+
35
+ mockSendOnClickEvent = jest.fn();
36
+ mockCallbackSendAnalyticsEvent = jest.fn();
37
+
38
+ mockUseAnalytics.mockReturnValue({
39
+ sendOnClickEvent: mockSendOnClickEvent,
40
+ } as any);
41
+
42
+ mockUseSendAnalyticsEventWithFunction.mockReturnValue(
43
+ mockCallbackSendAnalyticsEvent
44
+ );
45
+ });
46
+
47
+ describe("initialization", () => {
48
+ it("should return a callback function", () => {
49
+ const item = { id: "item-1" };
50
+ const component = { type: "button" };
51
+ const zappPipesData = { key: "value" };
52
+
53
+ const { result } = renderHook(() =>
54
+ useSendAnalyticsOnPress(item, component, zappPipesData)
55
+ );
56
+
57
+ expect(typeof result.current).toBe("function");
58
+ });
59
+
60
+ it("should call useAnalytics with correct parameters", () => {
61
+ const item = { id: "item-1", title: "Item 1" };
62
+ const component = { type: "cell", name: "MasterCell" };
63
+ const zappPipesData = { feeds: [], screens: [] };
64
+
65
+ renderHook(() => useSendAnalyticsOnPress(item, component, zappPipesData));
66
+
67
+ expect(mockUseAnalytics).toHaveBeenCalledWith({
68
+ item,
69
+ component,
70
+ zappPipesData,
71
+ });
72
+ });
73
+
74
+ it("should call useSendAnalyticsEventWithFunction", () => {
75
+ const item = { id: "item-2" };
76
+ const component = { type: "list" };
77
+ const zappPipesData = {};
78
+
79
+ renderHook(() => useSendAnalyticsOnPress(item, component, zappPipesData));
80
+
81
+ expect(mockUseSendAnalyticsEventWithFunction).toHaveBeenCalled();
82
+ });
83
+ });
84
+
85
+ describe("callback execution", () => {
86
+ it("should call analytics event when callback is invoked", () => {
87
+ const item = { id: "item-1" };
88
+ const component = { type: "button" };
89
+ const zappPipesData = { key: "value" };
90
+
91
+ const { result } = renderHook(() =>
92
+ useSendAnalyticsOnPress(item, component, zappPipesData)
93
+ );
94
+
95
+ const pressedItem = { id: "pressed-item" };
96
+ const index = 5;
97
+
98
+ act(() => {
99
+ result.current(pressedItem, index);
100
+ });
101
+
102
+ expect(mockCallbackSendAnalyticsEvent).toHaveBeenCalledWith({
103
+ sendAnalyticsFunction: mockSendOnClickEvent,
104
+ extraProps: {
105
+ item: pressedItem,
106
+ index,
107
+ component,
108
+ zappPipesData,
109
+ },
110
+ });
111
+ });
112
+
113
+ it("should handle different items on each press", () => {
114
+ const item = { id: "item-1" };
115
+ const component = { type: "list" };
116
+ const zappPipesData = {};
117
+
118
+ const { result } = renderHook(() =>
119
+ useSendAnalyticsOnPress(item, component, zappPipesData)
120
+ );
121
+
122
+ const item1 = { id: "item-a", title: "Item A" };
123
+ const item2 = { id: "item-b", title: "Item B" };
124
+
125
+ act(() => {
126
+ result.current(item1, 0);
127
+ });
128
+
129
+ expect(mockCallbackSendAnalyticsEvent).toHaveBeenLastCalledWith(
130
+ expect.objectContaining({
131
+ extraProps: expect.objectContaining({
132
+ item: item1,
133
+ index: 0,
134
+ }),
135
+ })
136
+ );
137
+
138
+ act(() => {
139
+ result.current(item2, 1);
140
+ });
141
+
142
+ expect(mockCallbackSendAnalyticsEvent).toHaveBeenLastCalledWith(
143
+ expect.objectContaining({
144
+ extraProps: expect.objectContaining({
145
+ item: item2,
146
+ index: 1,
147
+ }),
148
+ })
149
+ );
150
+ });
151
+
152
+ it("should handle different indices", () => {
153
+ const item = { id: "item-1" };
154
+ const component = { type: "grid" };
155
+ const zappPipesData = {};
156
+
157
+ const { result } = renderHook(() =>
158
+ useSendAnalyticsOnPress(item, component, zappPipesData)
159
+ );
160
+
161
+ const pressedItem = { id: "pressed" };
162
+
163
+ for (let i = 0; i < 5; i++) {
164
+ act(() => {
165
+ result.current(pressedItem, i);
166
+ });
167
+
168
+ expect(mockCallbackSendAnalyticsEvent).toHaveBeenLastCalledWith(
169
+ expect.objectContaining({
170
+ extraProps: expect.objectContaining({
171
+ index: i,
172
+ }),
173
+ })
174
+ );
175
+ }
176
+ });
177
+
178
+ it("should always use the current component from closure", () => {
179
+ const item = { id: "item-1" };
180
+ const component = { type: "carousel", name: "MainCarousel" };
181
+ const zappPipesData = { feeds: [] };
182
+
183
+ const { result } = renderHook(() =>
184
+ useSendAnalyticsOnPress(item, component, zappPipesData)
185
+ );
186
+
187
+ act(() => {
188
+ result.current({ id: "test" }, 0);
189
+ });
190
+
191
+ expect(mockCallbackSendAnalyticsEvent).toHaveBeenCalledWith(
192
+ expect.objectContaining({
193
+ extraProps: expect.objectContaining({
194
+ component,
195
+ }),
196
+ })
197
+ );
198
+ });
199
+
200
+ it("should always use the current zappPipesData from closure", () => {
201
+ const item = { id: "item-1" };
202
+ const component = { type: "button" };
203
+ const zappPipesData = { feeds: ["feed1"], screens: ["screen1"] };
204
+
205
+ const { result } = renderHook(() =>
206
+ useSendAnalyticsOnPress(item, component, zappPipesData)
207
+ );
208
+
209
+ act(() => {
210
+ result.current({ id: "test" }, 0);
211
+ });
212
+
213
+ expect(mockCallbackSendAnalyticsEvent).toHaveBeenCalledWith(
214
+ expect.objectContaining({
215
+ extraProps: expect.objectContaining({
216
+ zappPipesData,
217
+ }),
218
+ })
219
+ );
220
+ });
221
+ });
222
+
223
+ describe("memoization", () => {
224
+ it("should memoize callback based on dependencies", () => {
225
+ const item = { id: "item-1" };
226
+ const component = { type: "button" };
227
+ const zappPipesData = { key: "value" };
228
+
229
+ const { result, rerender } = renderHook(
230
+ ({ comp, data }) => useSendAnalyticsOnPress(item, comp, data),
231
+ {
232
+ initialProps: {
233
+ comp: component,
234
+ data: zappPipesData,
235
+ },
236
+ }
237
+ );
238
+
239
+ const firstCallback = result.current;
240
+
241
+ // Rerender without changing dependencies
242
+ rerender({
243
+ comp: component,
244
+ data: zappPipesData,
245
+ });
246
+
247
+ const secondCallback = result.current;
248
+
249
+ // Callback should be memoized
250
+ expect(firstCallback).toBe(secondCallback);
251
+ });
252
+
253
+ it("should create new callback when component changes", () => {
254
+ const item = { id: "item-1" };
255
+ const component1 = { type: "button" };
256
+ const component2 = { type: "list" };
257
+ const zappPipesData = {};
258
+
259
+ const { result, rerender } = renderHook(
260
+ ({ comp }) => useSendAnalyticsOnPress(item, comp, zappPipesData),
261
+ {
262
+ initialProps: { comp: component1 },
263
+ }
264
+ );
265
+
266
+ const firstCallback = result.current;
267
+
268
+ rerender({ comp: component2 });
269
+
270
+ const secondCallback = result.current;
271
+
272
+ // Callback should be different when component changes
273
+ expect(firstCallback).not.toBe(secondCallback);
274
+ });
275
+
276
+ it("should create new callback when zappPipesData changes", () => {
277
+ const item = { id: "item-1" };
278
+ const component = { type: "button" };
279
+ const zappPipesData1 = { feeds: [] as string[] };
280
+ const zappPipesData2 = { feeds: ["feed1"] };
281
+
282
+ const { result, rerender } = renderHook(
283
+ ({ data }) => useSendAnalyticsOnPress(item, component, data),
284
+ {
285
+ initialProps: { data: zappPipesData1 },
286
+ }
287
+ );
288
+
289
+ const firstCallback = result.current;
290
+
291
+ rerender({ data: zappPipesData2 });
292
+
293
+ const secondCallback = result.current;
294
+
295
+ expect(firstCallback).not.toBe(secondCallback);
296
+ });
297
+
298
+ it("should create new callback when callbackSendAnalyticsEvent changes", () => {
299
+ const item = { id: "item-1" };
300
+ const component = { type: "button" };
301
+ const zappPipesData = {};
302
+
303
+ const callback1 = jest.fn();
304
+ const callback2 = jest.fn();
305
+
306
+ mockUseSendAnalyticsEventWithFunction
307
+ .mockReturnValueOnce(callback1)
308
+ .mockReturnValueOnce(callback2);
309
+
310
+ const { result, rerender } = renderHook(
311
+ ({ item, component, zappPipesData }) =>
312
+ useSendAnalyticsOnPress(item, component, zappPipesData),
313
+ {
314
+ initialProps: {
315
+ item,
316
+ component,
317
+ zappPipesData,
318
+ },
319
+ }
320
+ );
321
+
322
+ const firstCallback = result.current;
323
+
324
+ rerender({ item, component, zappPipesData });
325
+
326
+ const secondCallback = result.current;
327
+
328
+ // Even though we didn't change inputs, if the underlying callback changes,
329
+ // our callback should change too
330
+ expect(firstCallback).not.toBe(secondCallback);
331
+ });
332
+ });
333
+
334
+ describe("edge cases", () => {
335
+ it("should handle null item", () => {
336
+ const component = { type: "button" };
337
+ const zappPipesData = {};
338
+
339
+ const { result } = renderHook(() =>
340
+ useSendAnalyticsOnPress(null, component, zappPipesData)
341
+ );
342
+
343
+ expect(() => {
344
+ act(() => {
345
+ result.current({ id: "test" }, 0);
346
+ });
347
+ }).not.toThrow();
348
+ });
349
+
350
+ it("should handle undefined item", () => {
351
+ const component = { type: "button" };
352
+ const zappPipesData = {};
353
+
354
+ const { result } = renderHook(() =>
355
+ useSendAnalyticsOnPress(undefined, component, zappPipesData)
356
+ );
357
+
358
+ expect(() => {
359
+ act(() => {
360
+ result.current({ id: "test" }, 0);
361
+ });
362
+ }).not.toThrow();
363
+ });
364
+
365
+ it("should handle null component", () => {
366
+ const item = { id: "item-1" };
367
+ const zappPipesData = {};
368
+
369
+ const { result } = renderHook(() =>
370
+ useSendAnalyticsOnPress(item, null, zappPipesData)
371
+ );
372
+
373
+ act(() => {
374
+ result.current({ id: "test" }, 0);
375
+ });
376
+
377
+ expect(mockCallbackSendAnalyticsEvent).toHaveBeenCalledWith(
378
+ expect.objectContaining({
379
+ extraProps: expect.objectContaining({
380
+ component: null,
381
+ }),
382
+ })
383
+ );
384
+ });
385
+
386
+ it("should handle null zappPipesData", () => {
387
+ const item = { id: "item-1" };
388
+ const component = { type: "button" };
389
+
390
+ const { result } = renderHook(() =>
391
+ useSendAnalyticsOnPress(item, component, null)
392
+ );
393
+
394
+ act(() => {
395
+ result.current({ id: "test" }, 0);
396
+ });
397
+
398
+ expect(mockCallbackSendAnalyticsEvent).toHaveBeenCalledWith(
399
+ expect.objectContaining({
400
+ extraProps: expect.objectContaining({
401
+ zappPipesData: null,
402
+ }),
403
+ })
404
+ );
405
+ });
406
+
407
+ it("should handle zero index", () => {
408
+ const item = { id: "item-1" };
409
+ const component = { type: "list" };
410
+ const zappPipesData = {};
411
+
412
+ const { result } = renderHook(() =>
413
+ useSendAnalyticsOnPress(item, component, zappPipesData)
414
+ );
415
+
416
+ act(() => {
417
+ result.current({ id: "test" }, 0);
418
+ });
419
+
420
+ expect(mockCallbackSendAnalyticsEvent).toHaveBeenCalledWith(
421
+ expect.objectContaining({
422
+ extraProps: expect.objectContaining({
423
+ index: 0,
424
+ }),
425
+ })
426
+ );
427
+ });
428
+
429
+ it("should handle negative index", () => {
430
+ const item = { id: "item-1" };
431
+ const component = { type: "list" };
432
+ const zappPipesData = {};
433
+
434
+ const { result } = renderHook(() =>
435
+ useSendAnalyticsOnPress(item, component, zappPipesData)
436
+ );
437
+
438
+ act(() => {
439
+ result.current({ id: "test" }, -1);
440
+ });
441
+
442
+ expect(mockCallbackSendAnalyticsEvent).toHaveBeenCalledWith(
443
+ expect.objectContaining({
444
+ extraProps: expect.objectContaining({
445
+ index: -1,
446
+ }),
447
+ })
448
+ );
449
+ });
450
+
451
+ it("should handle complex item structures", () => {
452
+ const item = { id: "item-1" };
453
+ const component = { type: "cell" };
454
+ const zappPipesData = {};
455
+
456
+ const { result } = renderHook(() =>
457
+ useSendAnalyticsOnPress(item, component, zappPipesData)
458
+ );
459
+
460
+ const complexItem = {
461
+ id: "complex",
462
+ nested: {
463
+ deep: {
464
+ structure: true,
465
+ },
466
+ },
467
+ array: [1, 2, 3],
468
+ };
469
+
470
+ act(() => {
471
+ result.current(complexItem, 5);
472
+ });
473
+
474
+ expect(mockCallbackSendAnalyticsEvent).toHaveBeenCalledWith(
475
+ expect.objectContaining({
476
+ extraProps: expect.objectContaining({
477
+ item: complexItem,
478
+ }),
479
+ })
480
+ );
481
+ });
482
+ });
483
+
484
+ describe("sendOnClickEvent integration", () => {
485
+ it("should pass sendOnClickEvent from useAnalytics", () => {
486
+ const item = { id: "item-1" };
487
+ const component = { type: "button" };
488
+ const zappPipesData = {};
489
+
490
+ const customSendOnClickEvent = jest.fn();
491
+
492
+ mockUseAnalytics.mockReturnValue({
493
+ sendOnClickEvent: customSendOnClickEvent,
494
+ } as any);
495
+
496
+ const { result } = renderHook(() =>
497
+ useSendAnalyticsOnPress(item, component, zappPipesData)
498
+ );
499
+
500
+ act(() => {
501
+ result.current({ id: "test" }, 0);
502
+ });
503
+
504
+ expect(mockCallbackSendAnalyticsEvent).toHaveBeenCalledWith(
505
+ expect.objectContaining({
506
+ sendAnalyticsFunction: customSendOnClickEvent,
507
+ })
508
+ );
509
+ });
510
+
511
+ it("should handle missing sendOnClickEvent gracefully", () => {
512
+ const item = { id: "item-1" };
513
+ const component = { type: "button" };
514
+ const zappPipesData = {};
515
+
516
+ mockUseAnalytics.mockReturnValue({
517
+ sendOnClickEvent: undefined,
518
+ } as any);
519
+
520
+ const { result } = renderHook(() =>
521
+ useSendAnalyticsOnPress(item, component, zappPipesData)
522
+ );
523
+
524
+ expect(() => {
525
+ act(() => {
526
+ result.current({ id: "test" }, 0);
527
+ });
528
+ }).not.toThrow();
529
+
530
+ expect(mockCallbackSendAnalyticsEvent).toHaveBeenCalledWith(
531
+ expect.objectContaining({
532
+ sendAnalyticsFunction: undefined,
533
+ })
534
+ );
535
+ });
536
+ });
537
+ });
@@ -0,0 +1,188 @@
1
+ import { renderHook } from "@testing-library/react-native";
2
+ import { useReRenderLog } from "../index";
3
+
4
+ describe("useReRenderLog", () => {
5
+ let consoleLogSpy: jest.SpyInstance;
6
+
7
+ beforeEach(() => {
8
+ consoleLogSpy = jest.spyOn(console, "log").mockImplementation();
9
+ });
10
+
11
+ afterEach(() => {
12
+ consoleLogSpy.mockRestore();
13
+ });
14
+
15
+ it("should log on initial render", () => {
16
+ const message = "Test Component";
17
+
18
+ renderHook(() => useReRenderLog(message));
19
+
20
+ expect(consoleLogSpy).toHaveBeenCalledWith(
21
+ expect.stringContaining("<<RE-RENDERED>>")
22
+ );
23
+
24
+ expect(consoleLogSpy).toHaveBeenCalledWith(
25
+ expect.stringContaining("<<COUNT: 1>>")
26
+ );
27
+
28
+ expect(consoleLogSpy).toHaveBeenCalledWith(
29
+ expect.stringContaining(message)
30
+ );
31
+ });
32
+
33
+ it("should increment count on each re-render", () => {
34
+ const message = "Counter Test";
35
+
36
+ const { rerender } = renderHook(() => useReRenderLog(message));
37
+
38
+ expect(consoleLogSpy).toHaveBeenCalledWith(
39
+ expect.stringContaining("<<COUNT: 1>>")
40
+ );
41
+
42
+ rerender();
43
+
44
+ expect(consoleLogSpy).toHaveBeenCalledWith(
45
+ expect.stringContaining("<<COUNT: 2>>")
46
+ );
47
+
48
+ rerender();
49
+
50
+ expect(consoleLogSpy).toHaveBeenCalledWith(
51
+ expect.stringContaining("<<COUNT: 3>>")
52
+ );
53
+ });
54
+
55
+ it("should log correct message format", () => {
56
+ const message = "MyComponent";
57
+
58
+ renderHook(() => useReRenderLog(message));
59
+
60
+ expect(consoleLogSpy).toHaveBeenCalledWith(
61
+ "<<RE-RENDERED>><<COUNT: 1>>: MyComponent"
62
+ );
63
+ });
64
+
65
+ it("should track renders independently for different hook instances", () => {
66
+ const message1 = "Component1";
67
+ const message2 = "Component2";
68
+
69
+ const { rerender: rerender1 } = renderHook(() => useReRenderLog(message1));
70
+
71
+ const { rerender: rerender2 } = renderHook(() => useReRenderLog(message2));
72
+
73
+ consoleLogSpy.mockClear();
74
+
75
+ rerender1();
76
+
77
+ expect(consoleLogSpy).toHaveBeenLastCalledWith(
78
+ expect.stringContaining("Component1")
79
+ );
80
+
81
+ rerender2();
82
+
83
+ expect(consoleLogSpy).toHaveBeenLastCalledWith(
84
+ expect.stringContaining("Component2")
85
+ );
86
+ });
87
+
88
+ it("should handle empty string message", () => {
89
+ renderHook(() => useReRenderLog(""));
90
+
91
+ expect(consoleLogSpy).toHaveBeenCalledWith("<<RE-RENDERED>><<COUNT: 1>>: ");
92
+ });
93
+
94
+ it("should handle message with special characters", () => {
95
+ const message = "Test!@#$%^&*()";
96
+
97
+ renderHook(() => useReRenderLog(message));
98
+
99
+ expect(consoleLogSpy).toHaveBeenCalledWith(
100
+ expect.stringContaining(message)
101
+ );
102
+ });
103
+
104
+ it("should handle long messages", () => {
105
+ const message = "A".repeat(1000);
106
+
107
+ renderHook(() => useReRenderLog(message));
108
+
109
+ expect(consoleLogSpy).toHaveBeenCalledWith(
110
+ expect.stringContaining(message)
111
+ );
112
+ });
113
+
114
+ it("should persist count across multiple re-renders", () => {
115
+ const message = "Persistence Test";
116
+
117
+ const { rerender } = renderHook(() => useReRenderLog(message));
118
+
119
+ for (let i = 2; i <= 10; i++) {
120
+ rerender();
121
+
122
+ expect(consoleLogSpy).toHaveBeenLastCalledWith(
123
+ expect.stringContaining(`<<COUNT: ${i}>>`)
124
+ );
125
+ }
126
+ });
127
+
128
+ it("should log on every render without dependencies", () => {
129
+ const message = "Every Render";
130
+
131
+ const { rerender } = renderHook(() => useReRenderLog(message));
132
+
133
+ const initialCallCount = consoleLogSpy.mock.calls.length;
134
+
135
+ rerender();
136
+ rerender();
137
+ rerender();
138
+
139
+ // Should have 4 total calls (initial + 3 rerenders)
140
+ expect(consoleLogSpy).toHaveBeenCalledTimes(initialCallCount + 3);
141
+ });
142
+
143
+ it("should handle numeric messages", () => {
144
+ renderHook(() => useReRenderLog(123 as any));
145
+
146
+ expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining("123"));
147
+ });
148
+
149
+ it("should handle object messages", () => {
150
+ const obj = { key: "value" };
151
+
152
+ renderHook(() => useReRenderLog(obj as any));
153
+
154
+ expect(consoleLogSpy).toHaveBeenCalled();
155
+ });
156
+
157
+ it("should maintain separate refs for each instance", () => {
158
+ const { rerender: rerender1 } = renderHook(() =>
159
+ useReRenderLog("Instance1")
160
+ );
161
+
162
+ const { rerender: rerender2 } = renderHook(() =>
163
+ useReRenderLog("Instance2")
164
+ );
165
+
166
+ consoleLogSpy.mockClear();
167
+
168
+ // Render instance 1 twice more
169
+ rerender1();
170
+ rerender1();
171
+
172
+ // Render instance 2 once more
173
+ rerender2();
174
+
175
+ const calls = consoleLogSpy.mock.calls;
176
+
177
+ // Instance 1 should be at count 2 and 3
178
+ expect(calls[0][0]).toContain("Instance1");
179
+ expect(calls[0][0]).toContain("<<COUNT: 2>>");
180
+
181
+ expect(calls[1][0]).toContain("Instance1");
182
+ expect(calls[1][0]).toContain("<<COUNT: 3>>");
183
+
184
+ // Instance 2 should be at count 2
185
+ expect(calls[2][0]).toContain("Instance2");
186
+ expect(calls[2][0]).toContain("<<COUNT: 2>>");
187
+ });
188
+ });
@@ -1,28 +1,23 @@
1
1
  /* eslint react-native/split-platform-components: off */
2
2
 
3
3
  import { Platform, PlatformIOSStatic, NativeModules } from "react-native";
4
- import { platformSelect } from "@applicaster/zapp-react-native-utils/reactUtils";
5
- import { F as alwaysFalse } from "ramda";
6
4
  import { toBooleanWithDefaultFalse } from "@applicaster/zapp-react-native-utils/booleanUtils";
7
5
 
8
- const useIsTabletIos = (): boolean => {
9
- const { isPad } = Platform as PlatformIOSStatic;
6
+ export const useIsTablet = (): boolean => {
7
+ const platform = Platform.OS;
10
8
 
11
- return isPad;
12
- };
9
+ if (platform === "ios") {
10
+ const { isPad } = Platform as PlatformIOSStatic;
13
11
 
14
- const useIsTabletAndroid = (): boolean => {
15
- const { initialProps } = NativeModules.QuickBrickCommunicationModule;
12
+ return Boolean(isPad);
13
+ }
16
14
 
17
- return toBooleanWithDefaultFalse(initialProps?.is_tablet);
18
- };
15
+ if (platform === "android") {
16
+ const initialProps =
17
+ NativeModules.QuickBrickCommunicationModule?.initialProps;
19
18
 
20
- export const useIsTablet = platformSelect({
21
- ios: useIsTabletIos,
22
- android: useIsTabletAndroid,
23
- lg_tv: alwaysFalse,
24
- samsung_tv: alwaysFalse,
25
- android_tv: alwaysFalse,
26
- tvos: alwaysFalse,
27
- default: alwaysFalse,
28
- });
19
+ return toBooleanWithDefaultFalse(initialProps?.is_tablet);
20
+ }
21
+
22
+ return false;
23
+ };
@@ -0,0 +1,580 @@
1
+ import { renderHook, act } from "@testing-library/react-native";
2
+ import { useFadeOutWhenBlurred } from "../index";
3
+ import { Animated } from "react-native";
4
+
5
+ // Mock React Native Animated
6
+ jest.mock("react-native", () => ({
7
+ Animated: {
8
+ Value: jest.fn((value) => ({
9
+ setValue: jest.fn(),
10
+ _value: value,
11
+ })),
12
+ timing: jest.fn((_value, _config) => ({
13
+ start: jest.fn(),
14
+ })),
15
+ },
16
+ Easing: {
17
+ in: jest.fn((fn) => fn),
18
+ bezier: jest.fn(() => jest.fn()),
19
+ },
20
+ }));
21
+
22
+ describe("useFadeOutWhenBlurred", () => {
23
+ let mockAnimatedValue: any;
24
+ let mockTiming: jest.Mock;
25
+
26
+ beforeEach(() => {
27
+ jest.clearAllMocks();
28
+
29
+ mockAnimatedValue = {
30
+ setValue: jest.fn(),
31
+ _value: 1,
32
+ };
33
+
34
+ (Animated as any).Value = jest.fn().mockReturnValue(mockAnimatedValue);
35
+
36
+ mockTiming = jest.fn(() => ({
37
+ start: jest.fn(),
38
+ }));
39
+
40
+ (Animated as any).timing = mockTiming;
41
+ });
42
+
43
+ describe("initialization", () => {
44
+ it("should return opacity, willReceiveFocus, and hasLostFocus", () => {
45
+ const component = {
46
+ rules: { fade_out_when_blurred: true },
47
+ } as any;
48
+
49
+ const { result } = renderHook(() => useFadeOutWhenBlurred(component));
50
+
51
+ expect(result.current).toHaveProperty("opacity");
52
+ expect(result.current).toHaveProperty("willReceiveFocus");
53
+ expect(result.current).toHaveProperty("hasLostFocus");
54
+ });
55
+
56
+ it("should initialize with opacity 1", () => {
57
+ const component = {
58
+ rules: { fade_out_when_blurred: true },
59
+ } as any;
60
+
61
+ const { result } = renderHook(() => useFadeOutWhenBlurred(component));
62
+
63
+ expect(Animated.Value).toHaveBeenCalledWith(1);
64
+ expect(result.current.opacity).toBe(mockAnimatedValue);
65
+ });
66
+
67
+ it("should return callback functions", () => {
68
+ const component = {
69
+ rules: { fade_out_when_blurred: true },
70
+ } as any;
71
+
72
+ const { result } = renderHook(() => useFadeOutWhenBlurred(component));
73
+
74
+ expect(typeof result.current.willReceiveFocus).toBe("function");
75
+ expect(typeof result.current.hasLostFocus).toBe("function");
76
+ });
77
+
78
+ it("should handle component without fade_out_when_blurred rule", () => {
79
+ const component = {
80
+ rules: {},
81
+ } as any;
82
+
83
+ const { result } = renderHook(() => useFadeOutWhenBlurred(component));
84
+
85
+ expect(result.current.opacity).toBeDefined();
86
+ expect(result.current.willReceiveFocus).toBeDefined();
87
+ expect(result.current.hasLostFocus).toBeDefined();
88
+ });
89
+ });
90
+
91
+ describe("willReceiveFocus callback", () => {
92
+ it("should set visible to true when called while not visible", () => {
93
+ const component = {
94
+ rules: { fade_out_when_blurred: true },
95
+ } as any;
96
+
97
+ const { result } = renderHook(() => useFadeOutWhenBlurred(component));
98
+
99
+ // First lose focus
100
+ act(() => {
101
+ result.current.hasLostFocus(null, { value: "down" });
102
+ });
103
+
104
+ // Then receive focus
105
+ act(() => {
106
+ result.current.willReceiveFocus();
107
+ });
108
+
109
+ // Animation should be called to fade in
110
+ expect(mockTiming).toHaveBeenCalledWith(
111
+ mockAnimatedValue,
112
+ expect.objectContaining({
113
+ toValue: 1,
114
+ })
115
+ );
116
+ });
117
+
118
+ it("should not change visible state when already visible", () => {
119
+ const component = {
120
+ rules: { fade_out_when_blurred: true },
121
+ } as any;
122
+
123
+ const { result } = renderHook(() => useFadeOutWhenBlurred(component));
124
+
125
+ const initialCallCount = mockTiming.mock.calls.length;
126
+
127
+ act(() => {
128
+ result.current.willReceiveFocus();
129
+ });
130
+
131
+ // Should not trigger animation if already visible
132
+ expect(mockTiming.mock.calls.length).toBe(initialCallCount);
133
+ });
134
+
135
+ it("should be memoized", () => {
136
+ const component = {
137
+ rules: { fade_out_when_blurred: true },
138
+ } as any;
139
+
140
+ const { result, rerender } = renderHook(
141
+ ({ component }) => useFadeOutWhenBlurred(component),
142
+ {
143
+ initialProps: { component },
144
+ }
145
+ );
146
+
147
+ const firstCallback = result.current.willReceiveFocus;
148
+
149
+ rerender({ component });
150
+
151
+ const secondCallback = result.current.willReceiveFocus;
152
+
153
+ expect(firstCallback).toBe(secondCallback);
154
+ });
155
+ });
156
+
157
+ describe("hasLostFocus callback", () => {
158
+ it("should set visible to false when direction is down", () => {
159
+ const component = {
160
+ rules: { fade_out_when_blurred: true },
161
+ } as any;
162
+
163
+ const { result } = renderHook(() => useFadeOutWhenBlurred(component));
164
+
165
+ act(() => {
166
+ result.current.hasLostFocus(null, { value: "down" });
167
+ });
168
+
169
+ // Animation should be called to fade out
170
+ expect(mockTiming).toHaveBeenCalledWith(
171
+ mockAnimatedValue,
172
+ expect.objectContaining({
173
+ toValue: 0,
174
+ })
175
+ );
176
+ });
177
+
178
+ it("should not change visible state when direction is not down", () => {
179
+ const component = {
180
+ rules: { fade_out_when_blurred: true },
181
+ } as any;
182
+
183
+ const { result } = renderHook(() => useFadeOutWhenBlurred(component));
184
+
185
+ const initialCallCount = mockTiming.mock.calls.length;
186
+
187
+ act(() => {
188
+ result.current.hasLostFocus(null, { value: "up" });
189
+ });
190
+
191
+ expect(mockTiming.mock.calls.length).toBe(initialCallCount);
192
+
193
+ act(() => {
194
+ result.current.hasLostFocus(null, { value: "left" });
195
+ });
196
+
197
+ expect(mockTiming.mock.calls.length).toBe(initialCallCount);
198
+
199
+ act(() => {
200
+ result.current.hasLostFocus(null, { value: "right" });
201
+ });
202
+
203
+ expect(mockTiming.mock.calls.length).toBe(initialCallCount);
204
+ });
205
+
206
+ it("should handle missing direction gracefully", () => {
207
+ const component = {
208
+ rules: { fade_out_when_blurred: true },
209
+ } as any;
210
+
211
+ const { result } = renderHook(() => useFadeOutWhenBlurred(component));
212
+
213
+ expect(() => {
214
+ act(() => {
215
+ result.current.hasLostFocus(null, "left" as any);
216
+ });
217
+ }).not.toThrow();
218
+ });
219
+
220
+ it("should handle direction without value property", () => {
221
+ const component = {
222
+ rules: { fade_out_when_blurred: true },
223
+ } as any;
224
+
225
+ const { result } = renderHook(() => useFadeOutWhenBlurred(component));
226
+
227
+ expect(() => {
228
+ act(() => {
229
+ result.current.hasLostFocus(null, {} as any);
230
+ });
231
+ }).not.toThrow();
232
+ });
233
+
234
+ it("should be memoized", () => {
235
+ const component = {
236
+ rules: { fade_out_when_blurred: true },
237
+ } as any;
238
+
239
+ const { result, rerender } = renderHook(
240
+ ({ component }) => useFadeOutWhenBlurred(component),
241
+ {
242
+ initialProps: { component },
243
+ }
244
+ );
245
+
246
+ const firstCallback = result.current.hasLostFocus;
247
+
248
+ rerender({ component });
249
+
250
+ const secondCallback = result.current.hasLostFocus;
251
+
252
+ expect(firstCallback).toBe(secondCallback);
253
+ });
254
+ });
255
+
256
+ describe("animation behavior", () => {
257
+ it("should animate to opacity 0 when losing focus with down direction", () => {
258
+ const component = {
259
+ rules: { fade_out_when_blurred: true },
260
+ } as any;
261
+
262
+ const { result } = renderHook(() => useFadeOutWhenBlurred(component));
263
+
264
+ act(() => {
265
+ result.current.hasLostFocus(null, { value: "down" });
266
+ });
267
+
268
+ expect(mockTiming).toHaveBeenCalledWith(
269
+ mockAnimatedValue,
270
+ expect.objectContaining({
271
+ toValue: 0,
272
+ duration: 300,
273
+ useNativeDriver: true,
274
+ })
275
+ );
276
+ });
277
+
278
+ it("should animate to opacity 1 when receiving focus", () => {
279
+ const component = {
280
+ rules: { fade_out_when_blurred: true },
281
+ } as any;
282
+
283
+ const { result } = renderHook(() => useFadeOutWhenBlurred(component));
284
+
285
+ // First lose focus
286
+ act(() => {
287
+ result.current.hasLostFocus(null, { value: "down" });
288
+ });
289
+
290
+ // Then receive focus
291
+ act(() => {
292
+ result.current.willReceiveFocus();
293
+ });
294
+
295
+ expect(mockTiming).toHaveBeenLastCalledWith(
296
+ mockAnimatedValue,
297
+ expect.objectContaining({
298
+ toValue: 1,
299
+ duration: 300,
300
+ useNativeDriver: true,
301
+ })
302
+ );
303
+ });
304
+
305
+ it("should use custom animation options when provided", () => {
306
+ const component = {
307
+ rules: { fade_out_when_blurred: true },
308
+ } as any;
309
+
310
+ const animatedOptions = {
311
+ duration: 500,
312
+ };
313
+
314
+ const { result } = renderHook(() =>
315
+ useFadeOutWhenBlurred(component, animatedOptions)
316
+ );
317
+
318
+ act(() => {
319
+ result.current.hasLostFocus(null, { value: "down" });
320
+ });
321
+
322
+ expect(mockTiming).toHaveBeenCalledWith(
323
+ mockAnimatedValue,
324
+ expect.objectContaining({
325
+ duration: 500,
326
+ })
327
+ );
328
+ });
329
+
330
+ it("should not animate when fade_out_when_blurred is false", () => {
331
+ const component = {
332
+ rules: { fade_out_when_blurred: false },
333
+ } as any;
334
+
335
+ const { result } = renderHook(() => useFadeOutWhenBlurred(component));
336
+
337
+ const initialCallCount = mockTiming.mock.calls.length;
338
+
339
+ act(() => {
340
+ result.current.hasLostFocus(null, { value: "down" });
341
+ });
342
+
343
+ expect(mockTiming.mock.calls.length).toBe(initialCallCount);
344
+ });
345
+
346
+ it("should not animate when fade_out_when_blurred is undefined", () => {
347
+ const component = {
348
+ rules: {},
349
+ } as any;
350
+
351
+ const { result } = renderHook(() => useFadeOutWhenBlurred(component));
352
+
353
+ const initialCallCount = mockTiming.mock.calls.length;
354
+
355
+ act(() => {
356
+ result.current.hasLostFocus(null, { value: "down" });
357
+ });
358
+
359
+ expect(mockTiming.mock.calls.length).toBe(initialCallCount);
360
+ });
361
+
362
+ it("should start animation", () => {
363
+ const component = {
364
+ rules: { fade_out_when_blurred: true },
365
+ } as any;
366
+
367
+ const mockStart = jest.fn();
368
+
369
+ mockTiming.mockReturnValue({
370
+ start: mockStart,
371
+ });
372
+
373
+ const { result } = renderHook(() => useFadeOutWhenBlurred(component));
374
+
375
+ act(() => {
376
+ result.current.hasLostFocus(null, { value: "down" });
377
+ });
378
+
379
+ expect(mockStart).toHaveBeenCalled();
380
+ });
381
+ });
382
+
383
+ describe("focus state transitions", () => {
384
+ it("should handle multiple focus/blur cycles", () => {
385
+ const component = {
386
+ rules: { fade_out_when_blurred: true },
387
+ } as any;
388
+
389
+ const { result } = renderHook(() => useFadeOutWhenBlurred(component));
390
+
391
+ // Blur
392
+ act(() => {
393
+ result.current.hasLostFocus(null, { value: "down" });
394
+ });
395
+
396
+ expect(mockTiming).toHaveBeenLastCalledWith(
397
+ mockAnimatedValue,
398
+ expect.objectContaining({ toValue: 0 })
399
+ );
400
+
401
+ // Focus
402
+ act(() => {
403
+ result.current.willReceiveFocus();
404
+ });
405
+
406
+ expect(mockTiming).toHaveBeenLastCalledWith(
407
+ mockAnimatedValue,
408
+ expect.objectContaining({ toValue: 1 })
409
+ );
410
+
411
+ // Blur again
412
+ act(() => {
413
+ result.current.hasLostFocus(null, { value: "down" });
414
+ });
415
+
416
+ expect(mockTiming).toHaveBeenLastCalledWith(
417
+ mockAnimatedValue,
418
+ expect.objectContaining({ toValue: 0 })
419
+ );
420
+ });
421
+
422
+ it("should handle rapid focus changes", () => {
423
+ const component = {
424
+ rules: { fade_out_when_blurred: true },
425
+ } as any;
426
+
427
+ const { result } = renderHook(() => useFadeOutWhenBlurred(component));
428
+
429
+ act(() => {
430
+ result.current.hasLostFocus(null, { value: "down" });
431
+ result.current.willReceiveFocus();
432
+ result.current.hasLostFocus(null, { value: "down" });
433
+ result.current.willReceiveFocus();
434
+ });
435
+
436
+ // Should handle all transitions
437
+ expect(mockTiming.mock.calls.length).toBeGreaterThan(0);
438
+ });
439
+ });
440
+
441
+ describe("edge cases", () => {
442
+ it("should handle null animatedOptions", () => {
443
+ const component = {
444
+ rules: { fade_out_when_blurred: true },
445
+ } as any;
446
+
447
+ const { result } = renderHook(() =>
448
+ useFadeOutWhenBlurred(component, null as any)
449
+ );
450
+
451
+ act(() => {
452
+ result.current.hasLostFocus(null, { value: "down" });
453
+ });
454
+
455
+ expect(mockTiming).toHaveBeenCalled();
456
+ });
457
+
458
+ it("should handle empty animatedOptions", () => {
459
+ const component = {
460
+ rules: { fade_out_when_blurred: true },
461
+ } as any;
462
+
463
+ const { result } = renderHook(() => useFadeOutWhenBlurred(component, {}));
464
+
465
+ act(() => {
466
+ result.current.hasLostFocus(null, { value: "down" });
467
+ });
468
+
469
+ expect(mockTiming).toHaveBeenCalled();
470
+ });
471
+ });
472
+
473
+ describe("return value memoization", () => {
474
+ it("should memoize return object", () => {
475
+ const component = {
476
+ rules: { fade_out_when_blurred: true },
477
+ } as any;
478
+
479
+ const { result, rerender } = renderHook(
480
+ ({ component }) => useFadeOutWhenBlurred(component),
481
+ {
482
+ initialProps: { component },
483
+ }
484
+ );
485
+
486
+ const firstResult = result.current;
487
+
488
+ rerender({ component });
489
+
490
+ const secondResult = result.current;
491
+
492
+ // The return object should be memoized
493
+ expect(firstResult).toBe(secondResult);
494
+ });
495
+
496
+ it("should keep same opacity reference across rerenders", () => {
497
+ const component = {
498
+ rules: { fade_out_when_blurred: true },
499
+ } as any;
500
+
501
+ const { result, rerender } = renderHook(
502
+ ({ component }) => useFadeOutWhenBlurred(component),
503
+ {
504
+ initialProps: { component },
505
+ }
506
+ );
507
+
508
+ const firstOpacity = result.current.opacity;
509
+
510
+ rerender({ component });
511
+
512
+ const secondOpacity = result.current.opacity;
513
+
514
+ expect(firstOpacity).toBe(secondOpacity);
515
+ });
516
+ });
517
+
518
+ describe("animation configuration", () => {
519
+ it("should use bezier easing", () => {
520
+ const component = {
521
+ rules: { fade_out_when_blurred: true },
522
+ } as any;
523
+
524
+ const { result } = renderHook(() => useFadeOutWhenBlurred(component));
525
+
526
+ act(() => {
527
+ result.current.hasLostFocus(null, { value: "down" });
528
+ });
529
+
530
+ const animationConfig = mockTiming.mock.calls[0][1];
531
+ expect(animationConfig.easing).toBeDefined();
532
+ });
533
+
534
+ it("should use native driver", () => {
535
+ const component = {
536
+ rules: { fade_out_when_blurred: true },
537
+ } as any;
538
+
539
+ const { result } = renderHook(() => useFadeOutWhenBlurred(component));
540
+
541
+ act(() => {
542
+ result.current.hasLostFocus(null, { value: "down" });
543
+ });
544
+
545
+ expect(mockTiming).toHaveBeenCalledWith(
546
+ mockAnimatedValue,
547
+ expect.objectContaining({
548
+ useNativeDriver: true,
549
+ })
550
+ );
551
+ });
552
+
553
+ it("should override animation config with custom options", () => {
554
+ const component = {
555
+ rules: { fade_out_when_blurred: true },
556
+ } as any;
557
+
558
+ const customOptions = {
559
+ duration: 1000,
560
+ useNativeDriver: false,
561
+ };
562
+
563
+ const { result } = renderHook(() =>
564
+ useFadeOutWhenBlurred(component, customOptions)
565
+ );
566
+
567
+ act(() => {
568
+ result.current.hasLostFocus(null, { value: "down" });
569
+ });
570
+
571
+ expect(mockTiming).toHaveBeenCalledWith(
572
+ mockAnimatedValue,
573
+ expect.objectContaining({
574
+ duration: 1000,
575
+ useNativeDriver: false,
576
+ })
577
+ );
578
+ });
579
+ });
580
+ });
@@ -1,55 +0,0 @@
1
- import * as React from "react";
2
-
3
- const layoutReducer = (state, { payload }) => {
4
- return state.map((item, index, _state) => ({
5
- height: index === payload.index ? payload.height : item.height,
6
- y:
7
- index > 0
8
- ? _state.slice(0, index).reduce((acc, value) => acc + value.height, 0)
9
- : 0,
10
- }));
11
- };
12
-
13
- export const useComponentsLayout = (count, onFinishLoadingVisible) => {
14
- const [itemsLayout, dispatchItemLayout] = React.useReducer(
15
- layoutReducer,
16
- Array.from({ length: count }, () => ({ height: 0, y: 0 }))
17
- );
18
-
19
- const notified = React.useRef(false);
20
- const [listHeight, setListHeight] = React.useState<number>(0);
21
-
22
- const onItemLayout = React.useCallback(
23
- (index) => (event) => {
24
- const { height } = event.nativeEvent.layout;
25
- dispatchItemLayout({ payload: { index, height } });
26
- },
27
- []
28
- );
29
-
30
- const onListLayout = React.useCallback((event) => {
31
- const { height } = event.nativeEvent.layout;
32
- setListHeight(height);
33
- }, []);
34
-
35
- React.useEffect(() => {
36
- if (!notified.current) {
37
- const finishLoadingVisible = !!itemsLayout.find(
38
- (item) => item.y > listHeight
39
- );
40
-
41
- if (finishLoadingVisible) {
42
- notified.current = true;
43
- onFinishLoadingVisible?.();
44
- }
45
- }
46
- }, [itemsLayout, listHeight]);
47
-
48
- return React.useMemo(
49
- () => ({
50
- onItemLayout,
51
- onListLayout,
52
- }),
53
- []
54
- );
55
- };