@applicaster/zapp-react-native-utils 13.0.0-alpha.1212224153 → 13.0.0-alpha.1291347678

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 (68) hide show
  1. package/README.md +0 -10
  2. package/analyticsUtils/currentScreenProperities.ts +6 -4
  3. package/appUtils/HooksManager/Hook.ts +2 -2
  4. package/appUtils/HooksManager/index.ts +1 -1
  5. package/appUtils/HooksManager/types.ts +2 -2
  6. package/appUtils/contextKeysManager/contextResolver.ts +6 -2
  7. package/appUtils/contextKeysManager/index.ts +2 -2
  8. package/appUtils/contextKeysManager/utils/index.ts +2 -1
  9. package/appUtils/focusManager/__tests__/__snapshots__/focusManager.test.js.snap +1 -0
  10. package/appUtils/focusManager/index.ts +11 -12
  11. package/appUtils/keyCodes/index.ts +11 -7
  12. package/appUtils/orientationHelper.ts +11 -13
  13. package/appUtils/platform/platformUtils.ts +30 -3
  14. package/appUtils/playerManager/index.ts +1 -1
  15. package/appUtils/playerManager/player.ts +6 -2
  16. package/appUtils/playerManager/playerNative.ts +6 -2
  17. package/equalUtils/__tests__/isEqual/combination.test.ts +263 -0
  18. package/equalUtils/index.ts +49 -30
  19. package/focusManager/FocusManager.ts +11 -1
  20. package/focusManager/index.ts +41 -4
  21. package/functionUtils/index.ts +21 -8
  22. package/idleUtils/__tests__/withTimeout$.test.ts +37 -0
  23. package/idleUtils/index.ts +1 -1
  24. package/idleUtils/withTimeout.ts +15 -2
  25. package/manifestUtils/__tests__/createConfig.test.js +0 -2
  26. package/manifestUtils/fonts/index.js +5 -6
  27. package/manifestUtils/sharedConfiguration/screenPicker/stylesFields.js +1 -1
  28. package/navigationUtils/index.ts +3 -3
  29. package/numberUtils/index.ts +7 -5
  30. package/package.json +4 -9
  31. package/reactHooks/autoscrolling/__tests__/useTrackedView.test.tsx +5 -7
  32. package/reactHooks/autoscrolling/useTrackedView.ts +12 -1
  33. package/reactHooks/layout/useDimensions/helpers/__tests__/getInitialDimensions.test.js +1 -1
  34. package/reactHooks/layout/useDimensions/useDimensions.ts +8 -0
  35. package/reactHooks/navigation/__mocks__/useNavigation.ts +3 -0
  36. package/reactHooks/navigation/__tests__/index.test.tsx +6 -17
  37. package/reactHooks/navigation/index.ts +10 -22
  38. package/reactHooks/navigation/useGetTabBarHeight.ts +1 -3
  39. package/reactHooks/navigation/useProfilerLogging.ts +15 -0
  40. package/reactHooks/state/useRefWithInitialValue.ts +14 -0
  41. package/reactHooks/state/useStateFromSubscribe.ts +23 -0
  42. package/reactHooks/usePluginConfiguration.ts +1 -1
  43. package/reactHooks/useSubscriberFor.ts +4 -3
  44. package/reactUtils/__tests__/__snapshots__/reactUtilsHelpers.test.js.snap +16 -4
  45. package/reactUtils/__tests__/reactUtils.test.js +1 -19
  46. package/reactUtils/__tests__/reactUtilsHelpers.test.js +72 -71
  47. package/reactUtils/createContext/__tests__/__snapshots__/helpers.test.js.snap +6 -72
  48. package/reactUtils/createContext/__tests__/__snapshots__/provider.test.js.snap +3 -28
  49. package/reactUtils/createContext/__tests__/__snapshots__/withConsumer.test.js.snap +3 -4
  50. package/reactUtils/createContext/__tests__/__snapshots__/withProvider.test.js.snap +7 -3
  51. package/reactUtils/createContext/__tests__/helpers.test.js +107 -121
  52. package/reactUtils/createContext/__tests__/{index.test.js → index.test.ts} +2 -2
  53. package/reactUtils/createContext/__tests__/provider.test.js +4 -6
  54. package/reactUtils/createContext/__tests__/withConsumer.test.js +12 -12
  55. package/reactUtils/createContext/__tests__/withProvider.test.js +4 -4
  56. package/reactUtils/createContext/helpers.ts +6 -1
  57. package/reactUtils/createContext/index.ts +11 -25
  58. package/reactUtils/index.ts +0 -11
  59. package/screenState/index.ts +24 -10
  60. package/services/js2native.ts +673 -0
  61. package/testUtils/index.tsx +0 -10
  62. package/theme/index.ts +1 -1
  63. package/time/BackgroundTimer.ts +55 -0
  64. package/time/Timer.ts +9 -6
  65. package/types.ts +1 -0
  66. package/zappFrameworkUtils/presentPluginOneTime.ts +2 -3
  67. package/reactUtils/createContext/__tests__/index-no-react.test.js +0 -27
  68. /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`
@@ -72,12 +72,14 @@ const mappedScreenPropertiesScreen = (
72
72
  screenData
73
73
  ): AnalyticsScreenProperties => {
74
74
  return {
75
- screen_name: screenData?.targetScreen?.name,
75
+ screen_name: screenData?.targetScreen?.name || screenData?.name,
76
76
  screen_entry_title: screenData?.title,
77
77
  screen_entry_id: R.isNil(screenData?.type) ? null : screenData?.id,
78
- screen_layout_id: screenData?.targetScreen?.id,
79
- screen_type: screenData?.targetScreen?.type,
80
- home_screen: screenData?.targetScreen?.home,
78
+ screen_layout_id: screenData?.targetScreen?.id || screenData?.id,
79
+ screen_type: screenData?.targetScreen?.type || screenData?.type,
80
+ home_screen: !R.isNil(screenData?.targetScreen?.home)
81
+ ? screenData?.targetScreen?.home
82
+ : screenData?.home,
81
83
  };
82
84
  };
83
85
 
@@ -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);
@@ -84,7 +84,7 @@ export function HooksManager({
84
84
  plugins,
85
85
  startUpHooksData = {},
86
86
  }: HookManagerArgs): HookManager {
87
- hooksManagerLogger.addContext({ targetScreen });
87
+ hooksManagerLogger.addContext({ targetScreenId: targetScreen.id });
88
88
 
89
89
  function logHookEvent(func, message, data) {
90
90
  func({
@@ -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,6 +28,7 @@ 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],
@@ -13,6 +13,7 @@ import {
13
13
  import { subscriber } from "../../functionUtils";
14
14
  import { coreLogger } from "../../logger";
15
15
  import { ACTION } from "./utils/enums";
16
+
16
17
  const logger = coreLogger.addSubsystem("focusManager");
17
18
 
18
19
  const isFocusEnabled = (focusableItem): boolean => {
@@ -325,8 +326,6 @@ export const focusManager = (function () {
325
326
  },
326
327
  });
327
328
 
328
- currentFocusNode = focusCandidate;
329
-
330
329
  focusableItem && setFocus(focusCandidate.id, null);
331
330
 
332
331
  return { success: true };
@@ -335,15 +334,14 @@ export const focusManager = (function () {
335
334
  return { success: false };
336
335
  }
337
336
 
338
- function treeLoaded(val, node) {
339
- logger.log({
340
- message: "treeLoaded",
341
- data: {
342
- val,
343
- node,
344
- currentFocusNode,
345
- },
346
- });
337
+ function treeLoaded(_val, _node) {}
338
+
339
+ function pressIn() {
340
+ const currentFocusable = getCurrentFocus();
341
+
342
+ if (currentFocusable) {
343
+ currentFocusable.pressIn(null);
344
+ }
347
345
  }
348
346
 
349
347
  /**
@@ -477,7 +475,7 @@ export const focusManager = (function () {
477
475
  function isCurrentFocusOnTheTopScreen() {
478
476
  const routes = R.pathOr([], ["root", "children"], focusableTree);
479
477
 
480
- if (routes.length <= 1) {
478
+ if (currentFocusNode && routes.length <= 1) {
481
479
  // there is only one or zero routes
482
480
  return true;
483
481
  }
@@ -553,6 +551,7 @@ export const focusManager = (function () {
553
551
  invokeHandler,
554
552
  removeHandler,
555
553
  longPress,
554
+ pressIn,
556
555
  pressOut,
557
556
  enableFocus,
558
557
  setInitialFocus,
@@ -1,5 +1,3 @@
1
- import * as R from "ramda";
2
-
3
1
  // more keys can be added here. Simply make sure
4
2
  // the key matches the code value
5
3
 
@@ -34,12 +32,18 @@ export function keyCode(key) {
34
32
  keyCode(keyEvent).matchesAny("Enter", "Backspace", KEYS.ArrowLeft, ...)
35
33
  */
36
34
 
37
- const matches = R.compose(
38
- R.either(R.eqProps("code", key), R.eqProps("keyCode", key)),
39
- R.when(R.is(String), R.prop(R.__, KEYS))
40
- );
35
+ const matches = (compareKey) => {
36
+ // If compareKey is a string, look it up in KEYS object
37
+ const keyToCompare =
38
+ typeof compareKey === "string" ? KEYS[compareKey] : compareKey;
39
+
40
+ // Compare either code or keyCode properties
41
+ return (
42
+ keyToCompare?.code === key.code || keyToCompare?.keyCode === key.keyCode
43
+ );
44
+ };
41
45
 
42
- const matchesAny = (...args) => R.any(matches, args);
46
+ const matchesAny = (...args) => args.some(matches);
43
47
 
44
48
  const direction = () =>
45
49
  matchesAny(...ARROW_KEYS) ? directionHelper(key) : null;
@@ -1,8 +1,8 @@
1
- /// <reference types="@types/react-native" />
2
1
  import * as ReactNative from "react-native";
3
- import { isTV, platformSelect } from "../reactUtils";
4
2
  import { usePickFromState } from "@applicaster/zapp-react-native-redux/hooks";
5
- import { useIsTablet } from "@applicaster/zapp-react-native-utils/reactHooks/device/useIsTablet";
3
+
4
+ import { isTV, platformSelect } from "../reactUtils";
5
+ import { useIsTablet } from "../reactHooks";
6
6
 
7
7
  import {
8
8
  QUICK_BRICK_EVENTS,
@@ -121,8 +121,15 @@ export const removeOrientationChangeListener = (listener: any): void => {
121
121
  */
122
122
  export const isOrientationCompatible = ({
123
123
  orientation,
124
- layoutData: { width, height },
124
+ layoutData: { width, height, isTablet, isTabletPortrait },
125
125
  }) => {
126
+ // We count portrait as compatible, because if default in zapp for mobile layout
127
+ // We count landscape as compatible, because if isTabletPortrait is false,
128
+ // it defaults for device
129
+ if (isTablet && !isTabletPortrait) {
130
+ return true;
131
+ }
132
+
126
133
  const isLandscape = width > height;
127
134
 
128
135
  if (
@@ -146,15 +153,6 @@ export const isOrientationCompatible = ({
146
153
  return true;
147
154
  };
148
155
 
149
- export const useIsOrientationCompatible = (orientation) => {
150
- const { height, width } = ReactNative.useWindowDimensions();
151
-
152
- return isOrientationCompatible({
153
- orientation,
154
- layoutData: { width, height },
155
- });
156
- };
157
-
158
156
  /**
159
157
  * Determine final screen orientation based on device type,
160
158
  * settings and orientation value provided by screen data settings
@@ -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
 
@@ -452,7 +452,7 @@ export class PlayerManager {
452
452
  if (this.castingReceiver) {
453
453
  this.castingReceiver.closeNativePlayer();
454
454
  } else {
455
- this.getActivePlayer().closeNativePlayer();
455
+ this.getActivePlayer()?.closeNativePlayer();
456
456
  this.invokeHandler("close");
457
457
  }
458
458
  }
@@ -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
 
@@ -736,6 +736,10 @@ export class Player {
736
736
  return null;
737
737
  };
738
738
 
739
+ public hasResumePosition() {
740
+ return false;
741
+ }
742
+
739
743
  public getConfig = () => this.config;
740
744
 
741
745
  // TODO: replace with enum of supported orientations
@@ -54,6 +54,10 @@ export class PlayerNative extends Player {
54
54
  });
55
55
  }
56
56
 
57
+ public hasResumePosition() {
58
+ return true;
59
+ }
60
+
57
61
  getContentPosition = () => {
58
62
  return this.playerState.contentPosition;
59
63
  };
@@ -222,10 +226,10 @@ export class PlayerNative extends Player {
222
226
 
223
227
  onVideoLoad = (event) => {
224
228
  if (this.startPosition) {
229
+ super.onVideoLoad({ ...event, ignoreContinueWatching: true });
230
+
225
231
  this.seekTo(this.startPosition);
226
232
  this.startPosition = null;
227
-
228
- super.onVideoLoad({ ...event, ignoreContinueWatching: true });
229
233
  } else {
230
234
  super.onVideoLoad(event);
231
235
  }
@@ -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
+ });