@applicaster/zapp-react-native-utils 15.0.0-rc.96 → 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.
@@ -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
+ });