@applicaster/zapp-react-native-utils 15.0.0-alpha.5412418016 → 15.0.0-alpha.5485312433

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 (125) hide show
  1. package/actionsExecutor/ActionExecutorContext.tsx +3 -6
  2. package/actionsExecutor/feedDecorator.ts +6 -6
  3. package/adsUtils/__tests__/createVMAP.test.ts +419 -0
  4. package/adsUtils/index.ts +2 -2
  5. package/analyticsUtils/README.md +1 -1
  6. package/analyticsUtils/analyticsMapper.ts +10 -2
  7. package/appDataUtils/__tests__/urlScheme.test.ts +678 -0
  8. package/appUtils/HooksManager/__tests__/__snapshots__/hooksManager.test.js.snap +0 -188
  9. package/appUtils/HooksManager/__tests__/hooksManager.test.js +16 -2
  10. package/appUtils/HooksManager/index.ts +10 -10
  11. package/appUtils/RiverFocusManager/{index.js → index.ts} +25 -18
  12. package/appUtils/accessibilityManager/__tests__/utils.test.ts +360 -0
  13. package/appUtils/accessibilityManager/const.ts +4 -0
  14. package/appUtils/accessibilityManager/hooks.ts +20 -13
  15. package/appUtils/accessibilityManager/index.ts +28 -1
  16. package/appUtils/accessibilityManager/utils.ts +59 -8
  17. package/appUtils/contextKeysManager/__tests__/getKeys/failure.test.ts +7 -2
  18. package/appUtils/contextKeysManager/__tests__/getKeys/success.test.ts +48 -0
  19. package/appUtils/contextKeysManager/contextResolver.ts +51 -22
  20. package/appUtils/contextKeysManager/index.ts +65 -10
  21. package/appUtils/focusManager/__tests__/__snapshots__/focusManager.test.js.snap +3 -0
  22. package/appUtils/focusManager/index.ios.ts +43 -4
  23. package/appUtils/focusManagerAux/utils/index.ios.ts +122 -0
  24. package/appUtils/focusManagerAux/utils/index.ts +1 -1
  25. package/appUtils/focusManagerAux/utils/utils.ios.ts +199 -3
  26. package/appUtils/keyCodes/keys/keys.web.ts +1 -4
  27. package/appUtils/orientationHelper.ts +2 -4
  28. package/appUtils/platform/platformUtils.ts +117 -18
  29. package/appUtils/playerManager/OverlayObserver/OverlaysObserver.ts +94 -4
  30. package/appUtils/playerManager/OverlayObserver/utils.ts +32 -20
  31. package/appUtils/playerManager/player.ts +4 -0
  32. package/appUtils/playerManager/playerNative.ts +29 -16
  33. package/appUtils/playerManager/usePlayerState.tsx +14 -2
  34. package/arrayUtils/__tests__/allTruthy.test.ts +24 -0
  35. package/arrayUtils/__tests__/anyThruthy.test.ts +24 -0
  36. package/arrayUtils/index.ts +5 -0
  37. package/cellUtils/index.ts +32 -0
  38. package/cloudEventsUtils/__tests__/index.test.ts +529 -0
  39. package/cloudEventsUtils/index.ts +65 -1
  40. package/configurationUtils/__tests__/imageSrcFromMediaItem.test.ts +38 -0
  41. package/configurationUtils/__tests__/manifestKeyParser.test.ts +26 -26
  42. package/configurationUtils/index.ts +17 -11
  43. package/dateUtils/__tests__/dayjs.test.ts +330 -0
  44. package/enumUtils/__tests__/getEnumKeyByEnumValue.test.ts +207 -0
  45. package/errorUtils/__tests__/GeneralError.test.ts +97 -0
  46. package/errorUtils/__tests__/HttpStatusCode.test.ts +344 -0
  47. package/errorUtils/__tests__/MissingPluginError.test.ts +113 -0
  48. package/errorUtils/__tests__/NetworkError.test.ts +202 -0
  49. package/errorUtils/__tests__/getParsedResponse.test.ts +188 -0
  50. package/errorUtils/__tests__/invariant.test.ts +112 -0
  51. package/focusManager/aux/index.ts +1 -1
  52. package/headersUtils/__tests__/headersUtils.test.js +11 -1
  53. package/headersUtils/index.ts +2 -1
  54. package/manifestUtils/defaultManifestConfigurations/player.js +125 -11
  55. package/manifestUtils/keys.js +21 -0
  56. package/manifestUtils/platformIsTV.js +11 -0
  57. package/manifestUtils/sharedConfiguration/screenPicker/utils.js +1 -0
  58. package/manifestUtils/tvAction/container/index.js +1 -1
  59. package/navigationUtils/index.ts +15 -5
  60. package/numberUtils/__tests__/toNumber.test.ts +12 -0
  61. package/numberUtils/__tests__/toPositiveNumber.test.ts +165 -0
  62. package/numberUtils/index.ts +19 -1
  63. package/package.json +4 -4
  64. package/playerUtils/usePlayerTTS.ts +8 -3
  65. package/pluginUtils/index.ts +4 -0
  66. package/reactHooks/advertising/index.ts +2 -2
  67. package/reactHooks/app/__tests__/useAppState.test.ts +1 -1
  68. package/reactHooks/autoscrolling/__tests__/useTrackCurrentAutoScrollingElement.test.ts +1 -1
  69. package/reactHooks/autoscrolling/__tests__/useTrackedView.test.tsx +1 -2
  70. package/reactHooks/cell-click/__tests__/index.test.js +1 -3
  71. package/reactHooks/configuration/__tests__/index.test.tsx +1 -1
  72. package/reactHooks/connection/__tests__/index.test.js +1 -1
  73. package/reactHooks/debugging/__tests__/index.test.js +4 -4
  74. package/reactHooks/device/useMemoizedIsTablet.ts +3 -3
  75. package/reactHooks/feed/__tests__/useBatchLoading.test.tsx +32 -23
  76. package/reactHooks/feed/__tests__/useBuildPipesUrl.test.tsx +19 -19
  77. package/reactHooks/feed/__tests__/useEntryScreenId.test.tsx +4 -1
  78. package/reactHooks/feed/__tests__/useFeedLoader.test.tsx +42 -30
  79. package/reactHooks/feed/__tests__/useFeedRefresh.test.tsx +1 -1
  80. package/reactHooks/feed/__tests__/{useInflatedUrl.test.ts → useInflatedUrl.test.tsx} +62 -7
  81. package/reactHooks/feed/useEntryScreenId.ts +2 -2
  82. package/reactHooks/feed/useInflatedUrl.ts +43 -17
  83. package/reactHooks/flatList/useLoadNextPageIfNeeded.ts +13 -16
  84. package/reactHooks/hookModal/hooks/useHookModalScreenData.ts +12 -8
  85. package/reactHooks/layout/__tests__/index.test.tsx +1 -1
  86. package/reactHooks/layout/__tests__/useLayoutVersion.test.tsx +1 -1
  87. package/reactHooks/layout/index.ts +1 -1
  88. package/reactHooks/layout/useDimensions/__tests__/{useDimensions.test.ts → useDimensions.test.tsx} +105 -25
  89. package/reactHooks/layout/useDimensions/useDimensions.ts +2 -2
  90. package/reactHooks/navigation/__tests__/index.test.tsx +40 -9
  91. package/reactHooks/navigation/index.ts +27 -11
  92. package/reactHooks/navigation/useRoute.ts +11 -7
  93. package/reactHooks/player/TVSeekControlller/TVSeekController.ts +27 -10
  94. package/reactHooks/player/__tests__/useAutoSeek._test.tsx +1 -1
  95. package/reactHooks/player/__tests__/useTapSeek._test.ts +1 -1
  96. package/reactHooks/resolvers/__tests__/useCellResolver.test.tsx +1 -1
  97. package/reactHooks/resolvers/__tests__/useComponentResolver.test.tsx +1 -1
  98. package/reactHooks/resolvers/useCellResolver.ts +6 -2
  99. package/reactHooks/resolvers/useComponentResolver.ts +8 -2
  100. package/reactHooks/screen/__tests__/useCurrentScreenData.test.tsx +2 -2
  101. package/reactHooks/screen/__tests__/useScreenBackgroundColor.test.tsx +1 -1
  102. package/reactHooks/screen/__tests__/useScreenData.test.tsx +1 -1
  103. package/reactHooks/screen/__tests__/useTargetScreenData.test.tsx +12 -4
  104. package/reactHooks/screen/useTargetScreenData.ts +4 -2
  105. package/reactHooks/state/useRefWithInitialValue.ts +10 -0
  106. package/reactHooks/state/useRivers.ts +1 -1
  107. package/reactHooks/usePluginConfiguration.ts +2 -2
  108. package/reactHooks/utils/__tests__/index.test.js +1 -1
  109. package/screenState/__tests__/index.test.ts +1 -1
  110. package/searchUtils/const.ts +7 -0
  111. package/searchUtils/index.ts +3 -0
  112. package/services/storageServiceSync.web.ts +1 -1
  113. package/stringUtils/index.ts +1 -1
  114. package/testUtils/index.tsx +30 -21
  115. package/utils/__tests__/mapAccum.test.ts +73 -0
  116. package/utils/__tests__/mergeRight.test.ts +48 -0
  117. package/utils/__tests__/selectors.test.ts +124 -0
  118. package/utils/index.ts +17 -0
  119. package/utils/mapAccum.ts +23 -0
  120. package/utils/mergeRight.ts +5 -0
  121. package/utils/path.ts +6 -3
  122. package/utils/pathOr.ts +5 -1
  123. package/utils/selectors.ts +46 -0
  124. package/zappFrameworkUtils/HookCallback/callbackNavigationAction.ts +49 -12
  125. package/zappFrameworkUtils/HookCallback/hookCallbackManifestExtensions.config.js +1 -1
@@ -1,8 +1,8 @@
1
1
  import * as React from "react";
2
2
  import { Provider } from "react-redux";
3
- import { renderHook } from "@testing-library/react-hooks";
3
+ import { renderHook } from "@testing-library/react-native";
4
4
  import configureStore from "redux-mock-store";
5
- import thunk from "redux-thunk";
5
+ import { thunk } from "redux-thunk";
6
6
 
7
7
  const mockStore = configureStore([thunk]);
8
8
 
@@ -1,4 +1,4 @@
1
- import { renderHook } from "@testing-library/react-hooks";
1
+ import { renderHook } from "@testing-library/react-native";
2
2
 
3
3
  jest.mock(
4
4
  "@applicaster/zapp-react-native-ui-components/Components/River/useScreenConfiguration",
@@ -1,4 +1,4 @@
1
- import { renderHook } from "@testing-library/react-hooks";
1
+ import { renderHook } from "@testing-library/react-native";
2
2
  import configureStore from "redux-mock-store";
3
3
 
4
4
  const mockStore = configureStore();
@@ -1,16 +1,24 @@
1
1
  import * as React from "react";
2
2
  import { Provider } from "react-redux";
3
- import { renderHook } from "@testing-library/react-hooks";
3
+ import { renderHook } from "@testing-library/react-native";
4
4
  import configureStore from "redux-mock-store";
5
- import thunk from "redux-thunk";
5
+ import { thunk } from "redux-thunk";
6
6
 
7
7
  const mockStore = configureStore([thunk]);
8
8
 
9
9
  import { useTargetScreenData } from "../useTargetScreenData";
10
10
 
11
11
  describe("useTargetScreenData", function () {
12
- const river_id_2 = {};
13
- const river_id_1 = {};
12
+ const river_id_2 = {
13
+ id: "river_id_2",
14
+ type: "any",
15
+ };
16
+
17
+ const river_id_1 = {
18
+ id: "river_id_1",
19
+ type: "any",
20
+ };
21
+
14
22
  const screenId = "river_id_2";
15
23
  const entry = { id: "test", type: { value: "video" } };
16
24
 
@@ -1,7 +1,8 @@
1
1
  import React from "react";
2
- import { usePickFromState } from "@applicaster/zapp-react-native-redux/hooks";
2
+ import { useContentTypes } from "@applicaster/zapp-react-native-redux/hooks";
3
3
  import * as R from "ramda";
4
4
  import { appStore } from "@applicaster/zapp-react-native-redux/AppStore";
5
+ import { useRivers } from "../state";
5
6
 
6
7
  export function getTargetScreenData(
7
8
  entry: ZappEntry,
@@ -43,7 +44,8 @@ export function getTargetScreenDataFromEntry(entry: ZappEntry): ZappRiver {
43
44
  }
44
45
 
45
46
  export const useTargetScreenData = (entry: ZappEntry) => {
46
- const { rivers, contentTypes } = usePickFromState(["rivers", "contentTypes"]);
47
+ const rivers = useRivers();
48
+ const contentTypes = useContentTypes();
47
49
 
48
50
  return React.useMemo(
49
51
  () => getTargetScreenData(entry, rivers, contentTypes),
@@ -1,5 +1,9 @@
1
1
  import React from "react";
2
2
 
3
+ type WithOptionalCleanup = {
4
+ cleanup?: () => void;
5
+ };
6
+
3
7
  /**
4
8
  * returns a ref with an initial value that is lazily evaluated once
5
9
  * */
@@ -10,5 +14,11 @@ export const useRefWithInitialValue = <T>(initialValueGetter: () => T) => {
10
14
  ref.current = initialValueGetter();
11
15
  }
12
16
 
17
+ React.useEffect(() => {
18
+ return () => {
19
+ (ref.current as unknown as WithOptionalCleanup)?.cleanup?.();
20
+ };
21
+ }, []);
22
+
13
23
  return ref;
14
24
  };
@@ -3,6 +3,6 @@ import {
3
3
  selectRivers,
4
4
  } from "@applicaster/zapp-react-native-redux";
5
5
 
6
- export function useRivers(): Record<string, ZappRiver> {
6
+ export function useRivers() {
7
7
  return useAppSelector(selectRivers);
8
8
  }
@@ -1,7 +1,7 @@
1
1
  import * as R from "ramda";
2
2
  import { useMemo } from "react";
3
3
 
4
- import { usePickFromState } from "@applicaster/zapp-react-native-redux/hooks";
4
+ import { usePlugins } from "@applicaster/zapp-react-native-redux/hooks";
5
5
 
6
6
  // importing this way prevents typescript packages importing from this
7
7
  // file to go crazy reporting errors on JS
@@ -25,7 +25,7 @@ export function parsePluginConfiguration(plugin, manifest) {
25
25
  }
26
26
 
27
27
  export function usePluginConfiguration<T, S>(identifier, manifest) {
28
- const { plugins } = usePickFromState(["plugins"]);
28
+ const plugins = usePlugins();
29
29
 
30
30
  const plugin = useMemo(
31
31
  () => R.find(R.propEq("identifier", identifier), plugins),
@@ -1,5 +1,5 @@
1
1
  import { usePrevious, useIsInitialRender } from "../";
2
- import { renderHook, cleanup } from "@testing-library/react-hooks";
2
+ import { renderHook, cleanup } from "@testing-library/react-native";
3
3
 
4
4
  describe("usePrevious", () => {
5
5
  it("renders", () => {
@@ -1,4 +1,4 @@
1
- import { act, renderHook } from "@testing-library/react-hooks";
1
+ import { act, renderHook } from "@testing-library/react-native";
2
2
  import { useScreenState } from "../index";
3
3
 
4
4
  describe("useScreenState", () => {
@@ -0,0 +1,7 @@
1
+ export const FOCUSABLE_GROUP_ID = "search-screen";
2
+
3
+ export const FOCUSABLE_RESULTS_ID = "search-screen-results";
4
+
5
+ export const SEARCH_BOX_GROUP_ID = "search_box_group_id";
6
+
7
+ export const SEARCH_INPUT_FOCUSABLE_ID = "search_input_group_id";
@@ -0,0 +1,3 @@
1
+ import { SEARCH_INPUT_FOCUSABLE_ID } from "./const";
2
+
3
+ export const isSearchInputId = (id) => id === SEARCH_INPUT_FOCUSABLE_ID;
@@ -17,7 +17,7 @@ export const resolveStorageContextKey = (storageKey: string) => {
17
17
  const key = splitKey[1];
18
18
 
19
19
  if (!key) {
20
- return getFromSessionOrLocalStorage(key);
20
+ return getFromSessionOrLocalStorage(namespaceOrKey);
21
21
  } else {
22
22
  return getFromSessionOrLocalStorage(key, namespaceOrKey);
23
23
  }
@@ -4,7 +4,7 @@ declare const global;
4
4
  import * as R from "ramda";
5
5
  import camelize from "camelize";
6
6
  // @ts-ignore
7
- import Handlebars from "react-native-handlebars";
7
+ import Handlebars from "handlebars";
8
8
 
9
9
  global.Buffer = global.Buffer || require("buffer").Buffer;
10
10
 
@@ -4,7 +4,7 @@ import React, { PropsWithChildren } from "react";
4
4
  import configureStore from "redux-mock-store";
5
5
  import { Provider } from "react-redux";
6
6
  import { View } from "react-native";
7
- import thunk from "redux-thunk";
7
+ import { thunk } from "redux-thunk";
8
8
  import * as R from "ramda";
9
9
 
10
10
  import { appStore } from "@applicaster/zapp-react-native-redux/AppStore";
@@ -56,9 +56,29 @@ const initialState = {
56
56
  contentTypes: null,
57
57
  };
58
58
 
59
+ const themeObjDefault = {
60
+ component_margin_top: 0,
61
+ component_margin_bottom: 0,
62
+ component_margin_left: 0,
63
+ component_margin_right: 0,
64
+ component_padding_top: 0,
65
+ component_padding_bottom: 0,
66
+ component_padding_left: 0,
67
+ component_padding_right: 0,
68
+ component_gutter_vertical: 0,
69
+ component_gutter_horizontal: 0,
70
+ component_corner_radius: 0,
71
+ app_background_color: "rgba(0,0,0,0)",
72
+ screen_margin_top: 0,
73
+ screen_margin_bottom: 0,
74
+ component_anchor_point_y: 0,
75
+ assets: "",
76
+ };
77
+
59
78
  export const WrappedWithProviders = ({
60
79
  children,
61
80
  store: storeObj = {},
81
+ theme: themeObj = themeObjDefault,
62
82
  }: any) => {
63
83
  const _store = configureStore([thunk])(
64
84
  R.mergeDeepRight(initialState, { ...storeObj })
@@ -70,24 +90,7 @@ export const WrappedWithProviders = ({
70
90
  <ThemeContext.Provider
71
91
  value={{
72
92
  themes: {
73
- light: {
74
- component_margin_top: 0,
75
- component_margin_bottom: 0,
76
- component_margin_left: 0,
77
- component_margin_right: 0,
78
- component_padding_top: 0,
79
- component_padding_bottom: 0,
80
- component_padding_left: 0,
81
- component_padding_right: 0,
82
- component_gutter_vertical: 0,
83
- component_gutter_horizontal: 0,
84
- component_corner_radius: 0,
85
- app_background_color: "rgba(0,0,0,0)",
86
- screen_margin_top: 0,
87
- screen_margin_bottom: 0,
88
- component_anchor_point_y: 0,
89
- assets: "",
90
- },
93
+ light: themeObj,
91
94
  },
92
95
  selectedThemeId: "light",
93
96
  setSelectedThemeId: () => {},
@@ -113,11 +116,17 @@ export const WrappedWithProviders = ({
113
116
  * @param store optional store to pass to the provider
114
117
  * @returns
115
118
  */
116
- export const renderWithProviders = (component, storeObj?: unknown) => {
119
+ export const renderWithProviders = (
120
+ component,
121
+ storeObj?: unknown,
122
+ themeObj?: unknown
123
+ ) => {
117
124
  return render(component, {
118
125
  wrapper: function TestWrapper({ children }: PropsWithChildren<any>) {
119
126
  return (
120
- <WrappedWithProviders store={storeObj}>{children}</WrappedWithProviders>
127
+ <WrappedWithProviders store={storeObj} theme={themeObj}>
128
+ {children}
129
+ </WrappedWithProviders>
121
130
  );
122
131
  },
123
132
  });
@@ -0,0 +1,73 @@
1
+ import { mapAccum } from "../mapAccum";
2
+
3
+ describe("mapAccum", () => {
4
+ it("using standard ramda test", () => {
5
+ const digits = ["1", "2", "3", "4"];
6
+ const appender = (a, b) => [a + b, a + b];
7
+
8
+ const [acc, result] = mapAccum(appender, 0, digits); //= > ['01234', ['01', '012', '0123', '01234']]
9
+ expect(acc).toBe("01234");
10
+ expect(result).toEqual(["01", "012", "0123", "01234"]);
11
+ });
12
+
13
+ it("maps and accumulates over an array", () => {
14
+ const fn = (acc, x) => [acc + x, x * 2];
15
+ const [acc, result] = mapAccum(fn, 0, [1, 2, 3]);
16
+
17
+ expect(acc).toBe(6); // final accumulator (0 + 1 + 2 + 3)
18
+ expect(result).toEqual([2, 4, 6]); // mapped values (acc + x*2 at each step)
19
+ });
20
+
21
+ it("returns initial accumulator for empty array", () => {
22
+ const fn = (acc, x) => [acc + x, acc * x];
23
+ const [acc, result] = mapAccum(fn, 10, []);
24
+
25
+ expect(acc).toBe(10);
26
+ expect(result).toEqual([]);
27
+ });
28
+
29
+ it("accumulates strings correctly", () => {
30
+ const fn = (acc, x) => [acc + x, acc + x];
31
+ const [acc, result] = mapAccum(fn, "A", ["B", "C", "D"]);
32
+
33
+ expect(acc).toBe("ABCD");
34
+ expect(result).toEqual(["AB", "ABC", "ABCD"]);
35
+ });
36
+
37
+ it("works with objects as accumulator", () => {
38
+ const fn = (acc, x) => {
39
+ const newAcc = { sum: acc.sum + x };
40
+
41
+ return [newAcc, newAcc.sum];
42
+ };
43
+
44
+ const [acc, result] = mapAccum(fn, { sum: 0 }, [1, 2, 3]);
45
+
46
+ expect(acc).toEqual({ sum: 6 });
47
+ expect(result).toEqual([1, 3, 6]);
48
+ });
49
+
50
+ it("is curried", () => {
51
+ const fn = (acc, x) => [acc + x, x * 2];
52
+ const mapWithFn = mapAccum(fn);
53
+ const withInit = mapWithFn(2);
54
+ const [acc, result] = withInit([1, 2, 3]);
55
+
56
+ expect(acc).toBe(8);
57
+ expect(result).toEqual([2, 4, 6]);
58
+ });
59
+
60
+ it("does not mutate the original array", () => {
61
+ const arr = [1, 2, 3];
62
+ mapAccum((acc, x) => [acc + x, acc + x], 0, arr);
63
+ expect(arr).toEqual([1, 2, 3]);
64
+ });
65
+
66
+ it("handles mixed types in accumulator and result", () => {
67
+ const fn = (acc, x) => [acc + x.length, acc + "-" + x];
68
+ const [acc, result] = mapAccum(fn, 0, ["a", "bb", "ccc"]);
69
+
70
+ expect(acc).toBe(6);
71
+ expect(result).toEqual(["0-a", "1-bb", "3-ccc"]);
72
+ });
73
+ });
@@ -0,0 +1,48 @@
1
+ import { mergeRight } from "../mergeRight";
2
+
3
+ describe("mergeRight", () => {
4
+ test("merges two objects with no overlapping keys", () => {
5
+ const a = { x: 1, y: 2 };
6
+ const b = { z: 3 };
7
+
8
+ const result = mergeRight(a, b);
9
+
10
+ expect(result).toEqual({ x: 1, y: 2, z: 3 });
11
+ });
12
+
13
+ test("overwrites keys from the second object", () => {
14
+ const a = { x: 1, y: 2 };
15
+ const b = { y: 10, z: 3 };
16
+
17
+ const result = mergeRight(a, b);
18
+
19
+ expect(result).toEqual({ x: 1, y: 10, z: 3 });
20
+ });
21
+
22
+ test("does not mutate the original objects", () => {
23
+ const a = { x: 1 };
24
+ const b = { y: 2 };
25
+
26
+ const result = mergeRight(a, b);
27
+
28
+ expect(result).not.toBe(a);
29
+ expect(result).not.toBe(b);
30
+ expect(a).toEqual({ x: 1 });
31
+ expect(b).toEqual({ y: 2 });
32
+ });
33
+
34
+ test("works with empty objects", () => {
35
+ expect(mergeRight({}, { a: 1 })).toEqual({ a: 1 });
36
+ expect(mergeRight({ a: 1 }, {})).toEqual({ a: 1 });
37
+ expect(mergeRight({}, {})).toEqual({});
38
+ });
39
+
40
+ test("works with nested objects (shallow merge only)", () => {
41
+ const a = { x: { nested: 1 }, y: 2 };
42
+ const b = { x: { nested: 10 }, z: 3 };
43
+
44
+ const result = mergeRight(a, b);
45
+
46
+ expect(result).toEqual({ x: { nested: 10 }, y: 2, z: 3 });
47
+ });
48
+ });
@@ -0,0 +1,124 @@
1
+ import {
2
+ createPluginsByPathSelector,
3
+ createPluginsByModuleSelector,
4
+ combinePluginSelectors,
5
+ } from "../selectors";
6
+
7
+ describe("Plugin Selectors", () => {
8
+ const mockPlugins = [
9
+ {
10
+ identifier: "plugin1",
11
+ module: {
12
+ urlScheme: { host: "test" },
13
+ player: { type: "default" },
14
+ },
15
+ customField: true,
16
+ },
17
+ {
18
+ identifier: "plugin2",
19
+ module: {
20
+ urlScheme: { host: "other" },
21
+ },
22
+ },
23
+ {
24
+ identifier: "plugin3",
25
+ module: {
26
+ player: { type: "custom" },
27
+ },
28
+ },
29
+ ];
30
+
31
+ const mockState = {
32
+ plugins: mockPlugins,
33
+ };
34
+
35
+ describe("createPluginsByPathSelector", () => {
36
+ it("filters plugins by string path", () => {
37
+ const selector = createPluginsByPathSelector("customField");
38
+ const result = selector(mockState);
39
+
40
+ expect(result).toHaveLength(1);
41
+ expect(result[0].identifier).toBe("plugin1");
42
+ });
43
+
44
+ it("filters plugins by array path", () => {
45
+ const selector = createPluginsByPathSelector(["module", "urlScheme"]);
46
+ const result = selector(mockState);
47
+
48
+ expect(result).toHaveLength(2);
49
+ expect(result.map((p) => p.identifier)).toEqual(["plugin1", "plugin2"]);
50
+ });
51
+
52
+ it("handles missing plugins array", () => {
53
+ const selector = createPluginsByPathSelector("customField");
54
+ const result = selector({});
55
+
56
+ expect(result).toEqual([]);
57
+ });
58
+
59
+ it("handles non-existent path", () => {
60
+ const selector = createPluginsByPathSelector("nonexistent");
61
+ const result = selector(mockState);
62
+
63
+ expect(result).toEqual([]);
64
+ });
65
+ });
66
+
67
+ describe("createPluginsByModuleSelector", () => {
68
+ it("filters plugins by module path", () => {
69
+ const selector = createPluginsByModuleSelector("player");
70
+ const result = selector(mockState);
71
+
72
+ expect(result).toHaveLength(2);
73
+ expect(result.map((p) => p.identifier)).toEqual(["plugin1", "plugin3"]);
74
+ });
75
+
76
+ it("handles non-existent module", () => {
77
+ const selector = createPluginsByModuleSelector("nonexistent");
78
+ const result = selector(mockState);
79
+
80
+ expect(result).toEqual([]);
81
+ });
82
+ });
83
+
84
+ describe("combinePluginSelectors", () => {
85
+ it("combines multiple selectors with AND logic", () => {
86
+ const urlSchemeSelector = createPluginsByModuleSelector("urlScheme");
87
+ const playerSelector = createPluginsByModuleSelector("player");
88
+
89
+ const combinedSelector = combinePluginSelectors(
90
+ urlSchemeSelector,
91
+ playerSelector
92
+ );
93
+
94
+ const result = combinedSelector(mockState);
95
+
96
+ expect(result).toHaveLength(1);
97
+ expect(result[0].identifier).toBe("plugin1");
98
+ });
99
+
100
+ it("returns empty array when no plugins match all conditions", () => {
101
+ const urlSchemeSelector = createPluginsByModuleSelector("urlScheme");
102
+ const customSelector = createPluginsByPathSelector("nonexistent");
103
+
104
+ const combinedSelector = combinePluginSelectors(
105
+ urlSchemeSelector,
106
+ customSelector
107
+ );
108
+
109
+ const result = combinedSelector(mockState);
110
+
111
+ expect(result).toEqual([]);
112
+ });
113
+
114
+ it("handles empty state", () => {
115
+ const selector = combinePluginSelectors(
116
+ createPluginsByModuleSelector("urlScheme")
117
+ );
118
+
119
+ const result = selector({});
120
+
121
+ expect(result).toEqual([]);
122
+ });
123
+ });
124
+ });
package/utils/index.ts CHANGED
@@ -16,6 +16,10 @@ export { endsWith } from "./endsWith";
16
16
 
17
17
  export { take } from "./take";
18
18
 
19
+ export { mapAccum } from "./mapAccum";
20
+
21
+ export { mergeRight } from "./mergeRight";
22
+
19
23
  export {
20
24
  cloneDeep as clone,
21
25
  flatten,
@@ -37,4 +41,17 @@ export {
37
41
  uniq,
38
42
  uniqWith,
39
43
  flowRight as compose,
44
+ partial,
45
+ clamp,
46
+ reverse,
47
+ takeRight,
48
+ fromPairs,
49
+ sortBy,
50
+ merge,
51
+ values,
52
+ head,
53
+ findIndex,
54
+ set,
55
+ compact,
56
+ identity,
40
57
  } from "lodash";
@@ -0,0 +1,23 @@
1
+ import { curry } from "lodash";
2
+
3
+ /**
4
+ * A native reimplementation of Ramda's mapAccum.
5
+ *
6
+ * @template A, B, C
7
+ * @param {(acc: A, value: B) => [A, C]} fn - Function returning [newAcc, mappedValue]
8
+ * @param {A} acc - Initial accumulator
9
+ * @param {B[]} list - List to process
10
+ * @returns {[A, C[]]} - Tuple of [final accumulator, mapped array]
11
+ */
12
+ export const mapAccum = curry((fn, acc, list) => {
13
+ const result = [];
14
+ let currentAcc = acc;
15
+
16
+ for (let i = 0; i < list.length; i++) {
17
+ const [nextAcc, mapped] = fn(currentAcc, list[i]);
18
+ currentAcc = nextAcc;
19
+ result.push(mapped);
20
+ }
21
+
22
+ return [currentAcc, result];
23
+ });
@@ -0,0 +1,5 @@
1
+ import { assign } from "lodash";
2
+
3
+ export const mergeRight = (a, b) => {
4
+ return assign({}, a, b);
5
+ };
package/utils/path.ts CHANGED
@@ -1,5 +1,8 @@
1
- import { get } from "lodash";
1
+ import { pathOr } from "./pathOr";
2
2
 
3
- export const path = (route, record) => {
4
- return get(record, route, undefined);
3
+ export const path = <T = any>(
4
+ route: (string | number) | (string | number)[],
5
+ record: any
6
+ ) => {
7
+ return pathOr<T>(undefined, route, record);
5
8
  };
package/utils/pathOr.ts CHANGED
@@ -1,5 +1,9 @@
1
1
  import { get } from "lodash";
2
2
 
3
- export const pathOr = (defaultValue, path, record) => {
3
+ export const pathOr = <T = any>(
4
+ defaultValue: T,
5
+ path: (number | string) | (number | string)[],
6
+ record: any
7
+ ): T => {
4
8
  return get(record, path, defaultValue);
5
9
  };
@@ -0,0 +1,46 @@
1
+ import { get } from "lodash";
2
+
3
+ export type Selector<T> = (state: any) => T;
4
+
5
+ /**
6
+ * Creates a selector for filtering plugins by path
7
+ * @param path - Path to check in plugin. Can be a dot notation string or array
8
+ * @returns A selector function that returns matching plugins
9
+ */
10
+ export const createPluginsByPathSelector = (
11
+ path: string | string[]
12
+ ): Selector<any[]> => {
13
+ return (state: any) => {
14
+ const plugins = state.plugins || [];
15
+
16
+ return plugins.filter((plugin: any) => get(plugin, path));
17
+ };
18
+ };
19
+
20
+ /**
21
+ * Creates a selector for filtering plugins by module path
22
+ * @param modulePath - Module path to check in plugin (e.g., "urlScheme", "player")
23
+ * @returns A selector function that returns plugins with specified module
24
+ */
25
+ export const createPluginsByModuleSelector = (
26
+ modulePath: string
27
+ ): Selector<any[]> => {
28
+ return createPluginsByPathSelector(["module", modulePath]);
29
+ };
30
+
31
+ /**
32
+ * Creates a selector that combines multiple plugin selectors
33
+ * @param selectors - Array of plugin selectors to combine
34
+ * @returns A selector function that returns plugins matching all conditions
35
+ */
36
+ export const combinePluginSelectors = (
37
+ ...selectors: Selector<any[]>[]
38
+ ): Selector<any[]> => {
39
+ return (state: any) => {
40
+ return selectors.reduce((filtered, selector) => {
41
+ const selected = selector(state);
42
+
43
+ return filtered.filter((plugin) => selected.includes(plugin));
44
+ }, state.plugins || []);
45
+ };
46
+ };