@applicaster/zapp-react-native-utils 13.0.0-alpha.6944415045 → 13.0.0-alpha.7019824418

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 (57) hide show
  1. package/README.md +0 -10
  2. package/appUtils/HooksManager/Hook.ts +2 -2
  3. package/appUtils/HooksManager/types.ts +2 -2
  4. package/appUtils/contextKeysManager/contextResolver.ts +6 -2
  5. package/appUtils/contextKeysManager/index.ts +2 -2
  6. package/appUtils/contextKeysManager/utils/index.ts +2 -1
  7. package/appUtils/focusManager/__tests__/__snapshots__/focusManager.test.js.snap +2 -0
  8. package/appUtils/focusManager/__tests__/focusManager.test.js +82 -0
  9. package/appUtils/focusManager/__tests__/index.test.ts +1 -0
  10. package/appUtils/focusManager/index.ts +27 -6
  11. package/appUtils/focusManager/utils/index.ts +14 -0
  12. package/appUtils/focusManagerAux/index.ts +2 -2
  13. package/appUtils/platform/platformUtils.ts +30 -3
  14. package/appUtils/playerManager/player.ts +2 -2
  15. package/equalUtils/__tests__/isEqual/combination.test.ts +263 -0
  16. package/equalUtils/index.ts +49 -30
  17. package/focusManager/FocusManager.ts +11 -1
  18. package/focusManager/index.ts +41 -4
  19. package/idleUtils/__tests__/withTimeout$.test.ts +37 -0
  20. package/idleUtils/index.ts +1 -1
  21. package/idleUtils/withTimeout.ts +15 -2
  22. package/manifestUtils/__tests__/createConfig.test.js +0 -2
  23. package/navigationUtils/index.ts +3 -3
  24. package/package.json +4 -6
  25. package/reactHooks/layout/useDimensions/helpers/__tests__/getInitialDimensions.test.js +1 -1
  26. package/reactHooks/layout/useDimensions/useDimensions.ts +8 -0
  27. package/reactHooks/navigation/__mocks__/useNavigation.ts +3 -0
  28. package/reactHooks/navigation/index.ts +7 -19
  29. package/reactHooks/navigation/useGetTabBarHeight.ts +1 -3
  30. package/reactHooks/navigation/useProfilerLogging.ts +15 -0
  31. package/reactHooks/state/useRefWithInitialValue.ts +14 -0
  32. package/reactHooks/state/useStateFromSubscribe.ts +23 -0
  33. package/reactHooks/usePluginConfiguration.ts +1 -1
  34. package/reactHooks/useSubscriberFor.ts +4 -3
  35. package/reactUtils/__tests__/__snapshots__/reactUtilsHelpers.test.js.snap +16 -4
  36. package/reactUtils/__tests__/reactUtils.test.js +1 -19
  37. package/reactUtils/__tests__/reactUtilsHelpers.test.js +72 -71
  38. package/reactUtils/createContext/__tests__/__snapshots__/helpers.test.js.snap +6 -72
  39. package/reactUtils/createContext/__tests__/__snapshots__/provider.test.js.snap +3 -28
  40. package/reactUtils/createContext/__tests__/__snapshots__/withConsumer.test.js.snap +3 -4
  41. package/reactUtils/createContext/__tests__/__snapshots__/withProvider.test.js.snap +7 -3
  42. package/reactUtils/createContext/__tests__/helpers.test.js +107 -121
  43. package/reactUtils/createContext/__tests__/{index.test.js → index.test.ts} +2 -2
  44. package/reactUtils/createContext/__tests__/provider.test.js +4 -6
  45. package/reactUtils/createContext/__tests__/withConsumer.test.js +12 -12
  46. package/reactUtils/createContext/__tests__/withProvider.test.js +4 -4
  47. package/reactUtils/createContext/helpers.ts +6 -1
  48. package/reactUtils/createContext/index.ts +11 -25
  49. package/reactUtils/index.ts +0 -11
  50. package/screenState/index.ts +24 -10
  51. package/testUtils/index.tsx +0 -10
  52. package/theme/index.ts +1 -1
  53. package/time/Timer.ts +4 -1
  54. package/types.ts +1 -0
  55. package/zappFrameworkUtils/presentPluginOneTime.ts +2 -3
  56. package/reactUtils/createContext/__tests__/index-no-react.test.js +0 -27
  57. /package/reactUtils/createContext/__tests__/__snapshots__/{index.test.js.snap → index.test.ts.snap} +0 -0
package/README.md CHANGED
@@ -376,16 +376,6 @@ useEffect(() => {
376
376
  }, []);
377
377
  ```
378
378
 
379
- ### testUtils
380
-
381
- `@applicaster/zapp-react-native-utils/testUtils`
382
-
383
- - `testIDEquals: (testId: any) => boolean | { width: number, height: number }` - Function to be used in tandem with enzyme findWhere
384
-
385
- ```javascript
386
- wrapper.findWhere(testIDEquals("text"));
387
- ```
388
-
389
379
  ### appUtils - contextKeysManager
390
380
 
391
381
  `@applicaster/zapp-react-native-utils/appUtils/contextKeyManager`
@@ -16,7 +16,7 @@ const BASE_HOOK_PROPERTIES = ["screen_id", "identifier", "weight"];
16
16
  * @returns {number} index
17
17
  */
18
18
  const hookState: (
19
- arg0: typeof HOOKS_EVENTS[keyof typeof HOOKS_EVENTS]
19
+ arg0: (typeof HOOKS_EVENTS)[keyof typeof HOOKS_EVENTS]
20
20
  ) => number = R.indexOf(R.__, R.values(HOOKS_EVENTS));
21
21
 
22
22
  /**
@@ -62,7 +62,7 @@ export class Hook implements HookInterface {
62
62
  * @param {Array<Any>} args variadic arguments to invoke the handler with
63
63
  */
64
64
  setStateAndNotify(
65
- event: typeof HOOKS_EVENTS[keyof typeof HOOKS_EVENTS],
65
+ event: (typeof HOOKS_EVENTS)[keyof typeof HOOKS_EVENTS],
66
66
  ...args
67
67
  ) {
68
68
  this.state = hookState(event);
@@ -103,7 +103,7 @@ export interface Hook<T extends HookModule = HookModule> {
103
103
  identifier: string;
104
104
  configuration: T["configuration"];
105
105
  setStateAndNotify(
106
- event: typeof HOOKS_EVENTS[keyof typeof HOOKS_EVENTS],
106
+ event: (typeof HOOKS_EVENTS)[keyof typeof HOOKS_EVENTS],
107
107
  opts: {
108
108
  error?: typeof Error;
109
109
  route?: string;
@@ -112,7 +112,7 @@ export interface Hook<T extends HookModule = HookModule> {
112
112
  }
113
113
  );
114
114
  general?: {
115
- transition_type: typeof HOOKS_EVENTS[keyof typeof HOOKS_EVENTS];
115
+ transition_type: (typeof HOOKS_EVENTS)[keyof typeof HOOKS_EVENTS];
116
116
  allow_screen_plugin_presentation: boolean;
117
117
  is_flow_blocker: boolean;
118
118
  };
@@ -42,12 +42,16 @@ export const resolveObjectValues = async (
42
42
 
43
43
  const resolvedEntries = await Promise.all(
44
44
  entries.map(async ([key, value]) => {
45
- const regex = /@{([^/]+)\/([^}]*)}/;
46
-
47
45
  if (typeof value !== "string") {
48
46
  return [key, value];
49
47
  }
50
48
 
49
+ if (!value.startsWith("@")) {
50
+ return [key, value];
51
+ }
52
+
53
+ const regex = /@{([^/]+)\/([^}]*)}/;
54
+
51
55
  const match = (value as string).match(regex);
52
56
 
53
57
  if (match) {
@@ -16,12 +16,12 @@ import {
16
16
 
17
17
  export { StorageLevel };
18
18
 
19
- type KeyNameObj = {
19
+ export type KeyNameObj = {
20
20
  namespace: string;
21
21
  key: string;
22
22
  };
23
23
 
24
- type KeyName = string | KeyNameObj;
24
+ export type KeyName = string | KeyNameObj;
25
25
 
26
26
  type ParsedKeyAndNamespace = {
27
27
  namespace?: string;
@@ -1,10 +1,11 @@
1
1
  import * as R from "ramda";
2
2
  import { DEFAULT_NAMESPACE } from "../consts";
3
+ import { KeyName, KeyNameObj } from "../index";
3
4
 
4
5
  const lastDotRegex = /\.([^.]+)$/;
5
6
  const splitByLastDot = R.compose(R.init, R.split(lastDotRegex));
6
7
 
7
- export function getNamespaceAndKey(namespacedKey) {
8
+ export function getNamespaceAndKey(namespacedKey: KeyName): KeyNameObj {
8
9
  if (typeof namespacedKey !== "string") return namespacedKey;
9
10
 
10
11
  const namespacedKeyProps = namespacedKey.includes(".")
@@ -28,10 +28,12 @@ exports[`focusManager should be defined 1`] = `
28
28
  "moveFocus": [Function],
29
29
  "on": [Function],
30
30
  "press": [Function],
31
+ "pressIn": [Function],
31
32
  "pressOut": [Function],
32
33
  "recoverFocus": [Function],
33
34
  "register": [Function],
34
35
  "removeHandler": [Function],
36
+ "resetFocus": [Function],
35
37
  "setFocus": [Function],
36
38
  "setInitialFocus": [Function],
37
39
  "unregister": [Function],
@@ -1,5 +1,6 @@
1
1
  import { focusManager } from "../index.ts";
2
2
  import { focusManager as focusManagerIOS } from "../index.ios.ts";
3
+ import { findFirstFocusableItem } from "../utils";
3
4
 
4
5
  describe("focusManagerIOS", () => {
5
6
  it("should be defined", () => {
@@ -8,6 +9,67 @@ describe("focusManagerIOS", () => {
8
9
  });
9
10
  });
10
11
 
12
+ const focusableTree = {
13
+ root: {
14
+ children: [
15
+ {
16
+ children: [
17
+ {
18
+ children: [
19
+ {
20
+ children: [],
21
+ component: {
22
+ isGroup: true,
23
+ },
24
+ id: "route/1/1",
25
+ },
26
+ {
27
+ children: [],
28
+ component: {
29
+ isGroup: true,
30
+ },
31
+ id: "route/1/2",
32
+ },
33
+ ],
34
+ component: {
35
+ isGroup: true,
36
+ },
37
+ id: "route/1",
38
+ },
39
+ {
40
+ children: [
41
+ {
42
+ children: [],
43
+ component: {
44
+ isGroup: true,
45
+ },
46
+ id: "route/2/1",
47
+ },
48
+ {
49
+ children: [],
50
+ component: {
51
+ isGroup: false,
52
+ },
53
+ id: "route/2/2",
54
+ },
55
+ ],
56
+ component: {
57
+ isGroup: true,
58
+ },
59
+ id: "route/2",
60
+ },
61
+ ],
62
+ component: {
63
+ isGroup: true,
64
+ },
65
+ id: "/river/1234",
66
+ },
67
+ ],
68
+ component: null,
69
+ id: "root",
70
+ },
71
+ };
72
+
11
73
  describe("focusManager", () => {
12
74
  it("should be defined", () => {
13
75
  expect(focusManager).toBeDefined();
@@ -45,4 +107,24 @@ describe("focusManager", () => {
45
107
  it("placeholder test", () => {
46
108
  expect(true).toBe(true);
47
109
  });
110
+
111
+ it("findFirstFocusableItem returns focusableItem", () => {
112
+ const expectedItemId =
113
+ focusableTree.root.children[0].children[1].children[1].id;
114
+
115
+ const focusableItem = findFirstFocusableItem(focusableTree.root.children);
116
+
117
+ expect(
118
+ focusableItem !== undefined && expectedItemId === focusableItem.id
119
+ ).toBe(true);
120
+ });
121
+
122
+ it("findFirstFocusableItem does not return focusableItem", () => {
123
+ focusableTree.root.children[0].children[1].children[1].component.isGroup =
124
+ true;
125
+
126
+ const focusableItem = findFirstFocusableItem(focusableTree.root.children);
127
+
128
+ expect(focusableItem === undefined).toBe(true);
129
+ });
48
130
  });
@@ -30,6 +30,7 @@ describe("focusManager", () => {
30
30
  "isGroupItemFocused",
31
31
  "recoverFocus",
32
32
  "isCurrentFocusOnTheTopScreen",
33
+ "resetFocus",
33
34
  ];
34
35
 
35
36
  expectedMethods.forEach((method) => {
@@ -13,6 +13,8 @@ import {
13
13
  import { subscriber } from "../../functionUtils";
14
14
  import { coreLogger } from "../../logger";
15
15
  import { ACTION } from "./utils/enums";
16
+ import { findFirstFocusableItem } from "./utils";
17
+
16
18
  const logger = coreLogger.addSubsystem("focusManager");
17
19
 
18
20
  const isFocusEnabled = (focusableItem): boolean => {
@@ -325,8 +327,6 @@ export const focusManager = (function () {
325
327
  },
326
328
  });
327
329
 
328
- currentFocusNode = focusCandidate;
329
-
330
330
  focusableItem && setFocus(focusCandidate.id, null);
331
331
 
332
332
  return { success: true };
@@ -346,6 +346,14 @@ export const focusManager = (function () {
346
346
  });
347
347
  }
348
348
 
349
+ function pressIn() {
350
+ const currentFocusable = getCurrentFocus();
351
+
352
+ if (currentFocusable) {
353
+ currentFocusable.pressIn(null);
354
+ }
355
+ }
356
+
349
357
  /**
350
358
  * calls the `press` method on the current focus component
351
359
  * @public
@@ -477,7 +485,7 @@ export const focusManager = (function () {
477
485
  function isCurrentFocusOnTheTopScreen() {
478
486
  const routes = R.pathOr([], ["root", "children"], focusableTree);
479
487
 
480
- if (routes.length <= 1) {
488
+ if (currentFocusNode && routes.length <= 1) {
481
489
  // there is only one or zero routes
482
490
  return true;
483
491
  }
@@ -485,20 +493,29 @@ export const focusManager = (function () {
485
493
  return haveSameParentBeforeRoot(currentFocusNode, R.last(routes));
486
494
  }
487
495
 
488
- function recoverFocus() {
496
+ function resetFocus() {
489
497
  if (!isCurrentFocusOnTheTopScreen()) {
490
- // We've failed to set focused node on the new screen => run focus recovery
498
+ // We've failed to set focused node on the new screen => run focus reset
491
499
  setInitialFocus();
492
500
  }
493
501
  }
494
502
 
503
+ /**
504
+ * sets the focus on first focusable item
505
+ * @public
506
+ */
507
+ function recoverFocus() {
508
+ const focusableItem = findFirstFocusableItem(focusableTree.root.children);
509
+ focusableItem && setFocus(focusableItem.id);
510
+ }
511
+
495
512
  /**
496
513
  * moves the focus in a provided direction
497
514
  * @public
498
515
  * @param {Object} direction of the scroll
499
516
  */
500
517
  function moveFocus(direction) {
501
- if (!isCurrentFocusOnTheTopScreen()) {
518
+ if (!isCurrentFocusOnTheTopScreen() && currentFocusNode) {
502
519
  // We are opening new screen. We need to ignore movement of focus.
503
520
  return false;
504
521
  }
@@ -521,6 +538,8 @@ export const focusManager = (function () {
521
538
  } else {
522
539
  currentFocusNode.component.failedLostFocus(currentFocusNode, direction);
523
540
  }
541
+ } else {
542
+ recoverFocus();
524
543
  }
525
544
  }
526
545
 
@@ -553,6 +572,7 @@ export const focusManager = (function () {
553
572
  invokeHandler,
554
573
  removeHandler,
555
574
  longPress,
575
+ pressIn,
556
576
  pressOut,
557
577
  enableFocus,
558
578
  setInitialFocus,
@@ -564,5 +584,6 @@ export const focusManager = (function () {
564
584
  isGroupItemFocused,
565
585
  recoverFocus,
566
586
  isCurrentFocusOnTheTopScreen,
587
+ resetFocus,
567
588
  };
568
589
  })();
@@ -98,3 +98,17 @@ export const waitForContent = (focusableTree) => {
98
98
  conditionFn: contentHasAnyChildren,
99
99
  });
100
100
  };
101
+
102
+ export const findFirstFocusableItem = (items) => {
103
+ for (const item of items) {
104
+ if (!item.component.isGroup) {
105
+ return item;
106
+ } else {
107
+ if (item.children) {
108
+ const focusableItem = findFirstFocusableItem(item.children);
109
+
110
+ if (focusableItem) return focusableItem;
111
+ }
112
+ }
113
+ }
114
+ };
@@ -54,7 +54,7 @@ export async function setFocusOnContent(currentRoute: string) {
54
54
  // For doing it we have to wait registration of first focusable node into focusableTree
55
55
  // we wait until content node starts having children
56
56
 
57
- const { setInitialFocus, focusableTree } = focusManager;
57
+ const { setInitialFocus, focusableTree, getCurrentFocus } = focusManager;
58
58
 
59
59
  try {
60
60
  await waitForActiveScreen(currentRoute, focusableTree);
@@ -71,6 +71,6 @@ export async function setFocusOnContent(currentRoute: string) {
71
71
  );
72
72
  }
73
73
  } catch (error) {
74
- setFocusOnContent(currentRoute);
74
+ !getCurrentFocus() && setFocusOnContent(currentRoute);
75
75
  }
76
76
  }
@@ -77,13 +77,22 @@ export class ClosedCaptioningManager {
77
77
 
78
78
  private constructor() {
79
79
  this.initialize();
80
+
81
+ window["vizioDebug"] = {
82
+ setCCEnabled: (isCCEnabled: boolean) => {
83
+ this.ccState$.next(isCCEnabled);
84
+ },
85
+ };
80
86
  }
81
87
 
82
88
  static getInstance(): ClosedCaptioningManager {
83
89
  if (!ClosedCaptioningManager.ccManagerInstance) {
90
+ log_debug("ClosedCaptioningManager: Creating new instance");
84
91
  ClosedCaptioningManager.ccManagerInstance = new ClosedCaptioningManager();
85
92
  }
86
93
 
94
+ log_debug("ClosedCaptioningManager: Returning existing instance");
95
+
87
96
  return ClosedCaptioningManager.ccManagerInstance;
88
97
  }
89
98
 
@@ -92,23 +101,41 @@ export class ClosedCaptioningManager {
92
101
  PLATFORM_KEYS.deviceClosedCaptioningEnabled
93
102
  );
94
103
 
95
- log_debug("Initializing closed captioning manager", { existingState });
96
-
97
104
  if (existingState !== null) {
105
+ log_debug(
106
+ `ClosedCaptioningManager: Setting initial state to ${existingState}`
107
+ );
108
+
98
109
  this.ccState$.next(existingState === "true" || existingState === true);
110
+ } else {
111
+ log_debug("ClosedCaptioningManager: No existing state found");
99
112
  }
100
113
 
101
114
  if (hasVizioAPIs()) {
115
+ log_debug("ClosedCaptioningManager: Setting closed captioning handler");
116
+
102
117
  window.VIZIO!.setClosedCaptionHandler((isCCEnabled) => {
103
- log_debug("CC state changed", { isCCEnabled });
118
+ log_debug(
119
+ "ClosedCaptioningManager: There was a change in the CC state",
120
+ { isCCEnabled }
121
+ );
104
122
 
105
123
  sessionStorage.setItem(
106
124
  PLATFORM_KEYS.deviceClosedCaptioningEnabled,
107
125
  isCCEnabled
108
126
  );
109
127
 
128
+ log_debug(
129
+ "ClosedCaptioningManager: Saved CC state to session storage",
130
+ { isCCEnabled }
131
+ );
132
+
110
133
  this.ccState$.next(isCCEnabled);
111
134
  });
135
+ } else {
136
+ log_debug(
137
+ "ClosedCaptioningManager: Vizio APIs are not available, skipping setup"
138
+ );
112
139
  }
113
140
  }
114
141
 
@@ -154,8 +154,8 @@ export class Player {
154
154
  this.isSeeking()
155
155
  ? this.playerState.seekPosition
156
156
  : this.playerState.adState
157
- ? this.playerState.adState.adPosition
158
- : this.playerState.contentPosition;
157
+ ? this.playerState.adState.adPosition
158
+ : this.playerState.contentPosition;
159
159
 
160
160
  getEntry = (): ZappEntry | null => this.entry;
161
161
 
@@ -0,0 +1,263 @@
1
+ import { isEqual } from "../..";
2
+
3
+ const array = [1, 2, 3];
4
+ const sameProp = "sameProp";
5
+
6
+ describe("isEqual - comprehensive tests", () => {
7
+ describe("all conditions together", () => {
8
+ it("all conditions checked and not equal", () => {
9
+ const prev = {
10
+ byValueProp: { a: 1, b: 2 },
11
+ byRefProp: array,
12
+ byComparatorProp: "test",
13
+ extraProp1: 123,
14
+ extraProp2: sameProp,
15
+ };
16
+
17
+ const next = {
18
+ byValueProp: { a: 1, b: 2 },
19
+ byRefProp: array,
20
+ byComparatorProp: "different",
21
+ extraProp1: 456,
22
+ extraProp2: sameProp,
23
+ };
24
+
25
+ const customComparator = jest.fn().mockReturnValue(true);
26
+
27
+ const result = isEqual({
28
+ byValue: ["byValueProp"],
29
+ byReference: ["byRefProp"],
30
+ byComparator: {
31
+ comparator: customComparator,
32
+ keys: ["byComparatorProp"],
33
+ },
34
+ compareRemainingKeysBySameValue: true,
35
+ })(prev, next);
36
+
37
+ expect(result).toBe(false);
38
+
39
+ expect(customComparator).toHaveBeenCalledWith(
40
+ prev.byComparatorProp,
41
+ next.byComparatorProp
42
+ );
43
+ });
44
+
45
+ it("all conditions pass with exact remaining keys check", () => {
46
+ const prev = {
47
+ byValueProp: { a: 1, b: 2 },
48
+ byRefProp: array,
49
+ byComparatorProp: "test",
50
+ extraProp: sameProp,
51
+ };
52
+
53
+ const next = {
54
+ byValueProp: { a: 1, b: 2 },
55
+ byRefProp: array,
56
+ byComparatorProp: "test",
57
+ extraProp: sameProp,
58
+ };
59
+
60
+ const customComparator = jest.fn().mockReturnValue(true);
61
+
62
+ const result = isEqual({
63
+ byValue: ["byValueProp"],
64
+ byReference: ["byRefProp"],
65
+ byComparator: {
66
+ comparator: customComparator,
67
+ keys: ["byComparatorProp"],
68
+ },
69
+ compareRemainingKeysBySameValue: true,
70
+ })(prev, next);
71
+
72
+ expect(result).toBe(true);
73
+
74
+ expect(customComparator).toHaveBeenCalledWith(
75
+ prev.byComparatorProp,
76
+ next.byComparatorProp
77
+ );
78
+ });
79
+
80
+ it("byReference check fails first (early return)", () => {
81
+ const prev = {
82
+ byValueProp: { a: 1, b: 2 },
83
+ byRefProp: array,
84
+ byComparatorProp: sameProp,
85
+ };
86
+
87
+ const next = {
88
+ byValueProp: { a: 1, b: 2 },
89
+ byRefProp: [1, 2, 3],
90
+ byComparatorProp: sameProp,
91
+ };
92
+
93
+ const customComparator = jest.fn().mockReturnValue(true);
94
+
95
+ const result = isEqual({
96
+ byValue: ["byValueProp"],
97
+ byReference: ["byRefProp"],
98
+ byComparator: {
99
+ comparator: customComparator,
100
+ keys: ["byComparatorProp"],
101
+ },
102
+ })(prev, next);
103
+
104
+ expect(result).toBe(false);
105
+ expect(customComparator).not.toHaveBeenCalled();
106
+ });
107
+
108
+ it("byValue check fails after byReference passes", () => {
109
+ const prev = {
110
+ byValueProp: { a: 1, b: 2 },
111
+ byRefProp: array,
112
+ byComparatorProp: sameProp,
113
+ };
114
+
115
+ const next = {
116
+ byValueProp: { a: 1, b: 3 },
117
+ byRefProp: array,
118
+ byComparatorProp: sameProp,
119
+ };
120
+
121
+ const customComparator = jest.fn().mockReturnValue(true);
122
+
123
+ const result = isEqual({
124
+ byValue: ["byValueProp"],
125
+ byReference: ["byRefProp"],
126
+ byComparator: {
127
+ comparator: customComparator,
128
+ keys: ["byComparatorProp"],
129
+ },
130
+ })(prev, next);
131
+
132
+ expect(result).toBe(false);
133
+ expect(customComparator).not.toHaveBeenCalled();
134
+ });
135
+
136
+ it("byComparator check fails after other checks pass", () => {
137
+ const prev = {
138
+ byValueProp: { a: 1, b: 2 },
139
+ byRefProp: array,
140
+ byComparatorProp: "test",
141
+ };
142
+
143
+ const next = {
144
+ byValueProp: { a: 1, b: 2 },
145
+ byRefProp: array,
146
+ byComparatorProp: "different",
147
+ };
148
+
149
+ const customComparator = jest.fn().mockReturnValue(false);
150
+
151
+ const result = isEqual({
152
+ byValue: ["byValueProp"],
153
+ byReference: ["byRefProp"],
154
+ byComparator: {
155
+ comparator: customComparator,
156
+ keys: ["byComparatorProp"],
157
+ },
158
+ })(prev, next);
159
+
160
+ expect(result).toBe(false);
161
+
162
+ expect(customComparator).toHaveBeenCalledWith(
163
+ prev.byComparatorProp,
164
+ next.byComparatorProp
165
+ );
166
+ });
167
+
168
+ it("remaining keys comparison fails", () => {
169
+ const sharedArray = [1, 2, 3];
170
+
171
+ const prev = {
172
+ byValueProp: { a: 1, b: 2 },
173
+ byRefProp: sharedArray,
174
+ byComparatorProp: "test",
175
+ extraProp: "original",
176
+ };
177
+
178
+ const next = {
179
+ byValueProp: { a: 1, b: 2 },
180
+ byRefProp: sharedArray,
181
+ byComparatorProp: "different",
182
+ extraProp: "changed",
183
+ };
184
+
185
+ const customComparator = jest.fn().mockReturnValue(true);
186
+
187
+ const result = isEqual({
188
+ byValue: ["byValueProp"],
189
+ byReference: ["byRefProp"],
190
+ byComparator: {
191
+ comparator: customComparator,
192
+ keys: ["byComparatorProp"],
193
+ },
194
+ compareRemainingKeysBySameValue: true,
195
+ })(prev, next);
196
+
197
+ expect(result).toBe(false);
198
+ expect(customComparator).toHaveBeenCalled();
199
+ });
200
+
201
+ it("with debug flag", () => {
202
+ const consoleSpy = jest.spyOn(console, "log").mockImplementation();
203
+
204
+ const prev = {
205
+ byValueProp: { a: 1, b: 2 },
206
+ byRefProp: array,
207
+ byComparatorProp: "test",
208
+ extraProp: sameProp,
209
+ };
210
+
211
+ const next = {
212
+ byValueProp: { a: 1, b: 2 },
213
+ byRefProp: array,
214
+ byComparatorProp: "different",
215
+ extraProp: sameProp,
216
+ };
217
+
218
+ const customComparator = jest.fn().mockReturnValue(true);
219
+
220
+ const result = isEqual({
221
+ byValue: ["byValueProp"],
222
+ byReference: ["byRefProp"],
223
+ byComparator: {
224
+ comparator: customComparator,
225
+ keys: ["byComparatorProp"],
226
+ },
227
+ compareRemainingKeysBySameValue: true,
228
+ debug: true,
229
+ })(prev, next);
230
+
231
+ expect(result).toBe(true);
232
+ expect(consoleSpy).toHaveBeenCalled();
233
+
234
+ consoleSpy.mockRestore();
235
+ });
236
+
237
+ it("handles missing properties", () => {
238
+ const prev = {
239
+ byValueProp: { a: 1, b: 2 },
240
+ byRefProp: [1, 2, 3],
241
+ };
242
+
243
+ const next = {
244
+ byValueProp: { a: 1, b: 2 },
245
+ byComparatorProp: "test",
246
+ };
247
+
248
+ const customComparator = jest.fn().mockReturnValue(true);
249
+
250
+ const result = isEqual({
251
+ byValue: ["byValueProp"],
252
+ byReference: ["byRefProp"],
253
+ byComparator: {
254
+ comparator: customComparator,
255
+ keys: ["byComparatorProp"],
256
+ },
257
+ })(prev, next);
258
+
259
+ expect(result).toBe(false);
260
+ expect(customComparator).not.toHaveBeenCalled();
261
+ });
262
+ });
263
+ });