@applicaster/zapp-react-native-ui-components 16.0.0-rc.12 → 16.0.0-rc.120

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 (166) hide show
  1. package/Components/AudioPlayer/tv/__tests__/audioPlayer.test.js +19 -0
  2. package/Components/AudioPlayer/tv/__tests__/index.test.tsx +120 -0
  3. package/Components/AudioPlayer/tv/index.tsx +5 -1
  4. package/Components/Cell/Cell.tsx +3 -6
  5. package/Components/Cell/TvOSCellComponent.tsx +0 -1
  6. package/Components/CellRendererResolver/index.ts +1 -1
  7. package/Components/FocusableGroup/FocusableTvOS.tsx +11 -7
  8. package/Components/GeneralContentScreen/GeneralContentScreenHookAdapter.tsx +54 -0
  9. package/Components/GeneralContentScreen/__tests__/GeneralContentScreenHookAdapter.test.tsx +64 -0
  10. package/Components/GeneralContentScreen/__tests__/HookContentFocusGroup.web.test.tsx +91 -0
  11. package/Components/GeneralContentScreen/hookAdapter/__tests__/networkService.test.ts +74 -0
  12. package/Components/GeneralContentScreen/hookAdapter/__tests__/runInBackground.test.ts +139 -0
  13. package/Components/GeneralContentScreen/hookAdapter/__tests__/skipExpression.test.ts +294 -0
  14. package/Components/GeneralContentScreen/hookAdapter/__tests__/validationHelper.test.ts +188 -0
  15. package/Components/GeneralContentScreen/hookAdapter/logger.ts +6 -0
  16. package/Components/GeneralContentScreen/hookAdapter/networkService.ts +53 -0
  17. package/Components/GeneralContentScreen/hookAdapter/runInBackground.ts +48 -0
  18. package/Components/GeneralContentScreen/hookAdapter/skipExpression.ts +252 -0
  19. package/Components/GeneralContentScreen/hookAdapter/validationHelper.ts +90 -0
  20. package/Components/GeneralContentScreen/hookFocus/index.tsx +13 -0
  21. package/Components/GeneralContentScreen/hookFocus/index.web.tsx +69 -0
  22. package/Components/GeneralContentScreen/index.ts +2 -0
  23. package/Components/Layout/FullWidthRow.tsx +38 -0
  24. package/Components/LinkHandler/LinkHandler.tsx +2 -7
  25. package/Components/MasterCell/DefaultComponents/ActionButton.tsx +5 -1
  26. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/__tests__/placement.test.ts +47 -21
  27. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/components/ActionButtonController.tsx +19 -4
  28. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/components/__tests__/ActionButtonController.test.tsx +43 -1
  29. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/placement.ts +34 -6
  30. package/Components/MasterCell/DefaultComponents/Button.tsx +1 -0
  31. package/Components/MasterCell/DefaultComponents/Image/hoc/utils/__tests__/withDimensions.test.ts +420 -125
  32. package/Components/MasterCell/DefaultComponents/Image/hoc/utils/index.ts +114 -19
  33. package/Components/MasterCell/DefaultComponents/Image/hoc/withDimensions.tsx +19 -8
  34. package/Components/MasterCell/DefaultComponents/ImageContainer/index.tsx +5 -3
  35. package/Components/MasterCell/DefaultComponents/LiveImage/executePreloadHooks.ts +2 -4
  36. package/Components/MasterCell/DefaultComponents/LiveImage/index.tsx +14 -6
  37. package/Components/MasterCell/DefaultComponents/PressableView.tsx +29 -9
  38. package/Components/MasterCell/DefaultComponents/Text/hooks/useText.ts +4 -0
  39. package/Components/MasterCell/DefaultComponents/Toggle.tsx +36 -14
  40. package/Components/MasterCell/DefaultComponents/__tests__/Toggle.test.tsx +167 -0
  41. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/ActionButton.tsx +154 -100
  42. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/AssetComponent.tsx +15 -4
  43. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/Button.ts +56 -22
  44. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/Spacer.ts +6 -4
  45. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/TextLabelsContainer.ts +3 -1
  46. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/PressableView.test.tsx +34 -21
  47. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/index.test.ts +15 -4
  48. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/index.ts +23 -16
  49. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/utils/__tests__/insertButtons.test.ts +8 -3
  50. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/utils/index.ts +2 -2
  51. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/utils/__tests__/insertButtonsBetweenLabels.test.ts +45 -13
  52. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/utils/index.ts +2 -2
  53. package/Components/MasterCell/SharedUI/CollapsibleTextContainer/__tests__/factory.test.ts +32 -0
  54. package/Components/MasterCell/__tests__/masterCell.test.js +60 -0
  55. package/Components/MasterCell/contexts/PressedStateContext.ts +3 -0
  56. package/Components/MasterCell/dataAdapter.ts +15 -5
  57. package/Components/MasterCell/hooks/index.ts +2 -0
  58. package/Components/MasterCell/hooks/usePressedState.ts +4 -0
  59. package/Components/MasterCell/index.tsx +57 -8
  60. package/Components/MasterCell/utils/__tests__/resolveColor.test.js +169 -132
  61. package/Components/MasterCell/utils/__tests__/resolveColorForProp.test.js +92 -0
  62. package/Components/MasterCell/utils/__tests__/withoutFocusedState.test.js +20 -0
  63. package/Components/MasterCell/utils/index.ts +103 -46
  64. package/Components/ModalComponent/ActionsBottomSheet/ActionButton.tsx +56 -0
  65. package/Components/ModalComponent/ActionsBottomSheet/__tests__/openActionsBottomSheet.test.ts +38 -0
  66. package/Components/ModalComponent/ActionsBottomSheet/boundActionButton.tsx +24 -0
  67. package/Components/ModalComponent/ActionsBottomSheet/index.ts +9 -0
  68. package/Components/ModalComponent/ActionsBottomSheet/openActionsBottomSheet.ts +61 -0
  69. package/Components/ModalComponent/AudioPlayer/Components/Button.tsx +392 -0
  70. package/Components/ModalComponent/AudioPlayer/Components/Header.tsx +772 -0
  71. package/Components/ModalComponent/AudioPlayer/Components/Input.tsx +728 -0
  72. package/Components/ModalComponent/AudioPlayer/Components/Item.tsx +1028 -0
  73. package/Components/ModalComponent/AudioPlayer/Components/NowPlayingHeaderSection.tsx +205 -0
  74. package/Components/ModalComponent/AudioPlayer/Components/__tests__/Item.test.tsx +164 -0
  75. package/Components/ModalComponent/AudioPlayer/Components/__tests__/NowPlayingHeaderSection.test.tsx +36 -0
  76. package/Components/ModalComponent/AudioPlayer/Components/index.ts +9 -0
  77. package/Components/ModalComponent/AudioPlayer/utils/__tests__/mergeStyles.test.ts +433 -0
  78. package/Components/ModalComponent/AudioPlayer/utils/mergeStyles.ts +563 -0
  79. package/Components/ModalComponent/BottomSheetDragArea.tsx +33 -0
  80. package/Components/ModalComponent/BottomSheetModalContent.tsx +440 -50
  81. package/Components/ModalComponent/Button/index.tsx +5 -3
  82. package/Components/ModalComponent/Header/CloseButton.tsx +12 -0
  83. package/Components/ModalComponent/Header/__tests__/CloseButton.test.tsx +72 -0
  84. package/Components/ModalComponent/Header/index.tsx +20 -7
  85. package/Components/ModalComponent/Header/utils.ts +1 -1
  86. package/Components/ModalComponent/ModalBlocksStyleContext.tsx +10 -0
  87. package/Components/ModalComponent/SortableList.tsx +156 -0
  88. package/Components/ModalComponent/SortableList.web.tsx +34 -0
  89. package/Components/ModalComponent/TextInputContent.tsx +135 -0
  90. package/Components/ModalComponent/__tests__/BottomSheetDragArea.test.tsx +74 -0
  91. package/Components/ModalComponent/__tests__/BottomSheetModalContent.test.tsx +343 -0
  92. package/Components/ModalComponent/__tests__/SortableList.web.test.tsx +16 -0
  93. package/Components/ModalComponent/__tests__/TextInputContent.test.tsx +169 -0
  94. package/Components/ModalComponent/__tests__/showTextInput.test.ts +120 -0
  95. package/Components/ModalComponent/index.tsx +18 -9
  96. package/Components/ModalComponent/presets/__tests__/getCell.test.ts +90 -0
  97. package/Components/ModalComponent/presets/dynamicCollectionCell.tsx +78 -0
  98. package/Components/ModalComponent/presets/index.ts +27 -0
  99. package/Components/ModalComponent/presets/selectableCell.tsx +28 -0
  100. package/Components/ModalComponent/presets/standardCell.tsx +41 -0
  101. package/Components/ModalComponent/presets/types.ts +35 -0
  102. package/Components/ModalComponent/showTextInput.ts +54 -0
  103. package/Components/ModalComponent/utils.ts +82 -21
  104. package/Components/OfflineHandler/__tests__/__snapshots__/index.test.tsx.snap +4 -85
  105. package/Components/OfflineHandler/__tests__/index.test.tsx +186 -17
  106. package/Components/OfflineHandler/hooks.ts +111 -0
  107. package/Components/OfflineHandler/index.tsx +91 -75
  108. package/Components/OfflineHandler/utils/index.ts +1 -13
  109. package/Components/PlayerContainer/PlayerContainer.tsx +28 -11
  110. package/Components/PlayerContainer/ProgramInfo/index.tsx +3 -4
  111. package/Components/PlayerContainer/__tests__/PlayerContainer.test.tsx +337 -0
  112. package/Components/PreloaderWrapper/index.tsx +1 -1
  113. package/Components/River/ComponentsMap/ComponentsMap.tsx +91 -62
  114. package/Components/River/River.tsx +4 -0
  115. package/Components/River/__tests__/__snapshots__/componentsMap.test.js.snap +142 -89
  116. package/Components/River/__tests__/componentsMap.test.js +8 -3
  117. package/Components/River/__tests__/resultCallback.test.tsx +47 -0
  118. package/Components/Screen/__tests__/Screen.test.tsx +24 -0
  119. package/Components/Screen/__tests__/__snapshots__/Screen.test.tsx.snap +2 -0
  120. package/Components/Screen/index.tsx +13 -4
  121. package/Components/Screen/navigationHandler.ts +8 -8
  122. package/Components/ScreenFeedLoader/ScreenFeedLoader.tsx +5 -3
  123. package/Components/ScreenResolverFeedProvider/ScreenResolverFeedProvider.tsx +1 -1
  124. package/Components/ScreenRevealManager/ScreenRevealManager.ts +18 -2
  125. package/Components/ScreenRevealManager/__tests__/ScreenRevealManager.test.ts +36 -0
  126. package/Components/ScreenRevealManager/__tests__/withScreenRevealManager.test.tsx +56 -0
  127. package/Components/ScreenRevealManager/withScreenRevealManager.tsx +21 -4
  128. package/Components/TopCutoffOverlay/index.tsx +2 -2
  129. package/Components/Transitioner/AnimationManager.js +1 -1
  130. package/Components/Transitioner/Scene.tsx +4 -3
  131. package/Components/Transitioner/Transitioner.tsx +98 -49
  132. package/Components/Transitioner/__tests__/transitioner.test.js +372 -3
  133. package/Components/VideoLive/LiveImageManager.ts +9 -3
  134. package/Components/VideoLive/__tests__/LiveImageManager.test.ts +59 -0
  135. package/Components/VideoModal/PlayerWrapper.tsx +4 -1
  136. package/Components/VideoModal/__tests__/PlayerWrapper.test.tsx +1 -0
  137. package/Components/VideoModal/__tests__/playerWrapperStyle.test.ts +58 -0
  138. package/Components/VideoModal/__tests__/playerWrapperUtils.test.ts +16 -0
  139. package/Components/VideoModal/playerWrapperStyle.ts +21 -3
  140. package/Components/VideoModal/playerWrapperUtils.ts +7 -1
  141. package/Components/ZappFrameworkComponents/BarView/BarView.tsx +12 -5
  142. package/Components/ZappFrameworkComponents/BarView/__tests__/BarView.test.tsx +2 -2
  143. package/Contexts/CachedDimensionsContext/__tests__/index.test.ts +154 -0
  144. package/Contexts/CachedDimensionsContext/index.ts +17 -2
  145. package/Contexts/ModalNavigationContext/__tests__/index.test.tsx +15 -0
  146. package/Contexts/ModalNavigationContext/index.tsx +2 -2
  147. package/Decorators/ZappPipesDataConnector/__tests__/screenToolbox.test.ts +347 -0
  148. package/Decorators/ZappPipesDataConnector/__tests__/screenToolboxUnavailable.test.ts +53 -0
  149. package/Decorators/ZappPipesDataConnector/index.tsx +19 -4
  150. package/Decorators/ZappPipesDataConnector/screenToolbox.ts +384 -0
  151. package/Helpers/ComponentCellSelectionHelper/index.js +0 -6
  152. package/Helpers/index.js +0 -39
  153. package/Hooks/useEntryActionsExpander.ts +63 -0
  154. package/assets/empty-Info.png +0 -0
  155. package/package.json +6 -5
  156. package/Components/OfflineHandler/NotificationView/NotificationView.lg.tsx +0 -112
  157. package/Components/OfflineHandler/NotificationView/NotificationView.samsung.tsx +0 -107
  158. package/Components/OfflineHandler/NotificationView/NotificationView.tsx +0 -153
  159. package/Components/OfflineHandler/NotificationView/__tests__/index.test.tsx +0 -66
  160. package/Components/OfflineHandler/NotificationView/utils.ts +0 -34
  161. package/Components/Screen/utils.ts +0 -16
  162. package/Helpers/Analytics/index.js +0 -95
  163. /package/Components/BackgroundImage/{BackgroundImage.tv.android.tsx → BackgroundImage.android.tv.tsx} +0 -0
  164. /package/Components/BackgroundImage/{BackgroundImage.tv.ios.tsx → BackgroundImage.ios.tv.tsx} +0 -0
  165. /package/Components/MasterCell/DefaultComponents/ButtonContainerView/{index.tv.android.tsx → index.android.tv.tsx} +0 -0
  166. /package/Decorators/ZappPipesDataConnector/__tests__/{Hero.js → Hero.tsx} +0 -0
@@ -1,10 +1,11 @@
1
1
  import React from "react";
2
- import { StyleSheet, View } from "react-native";
3
-
2
+ import { StyleSheet, SafeAreaView as RNSafeAreaView } from "react-native";
3
+ import { SafeAreaView } from "react-native-safe-area-context";
4
4
  import { useIsRTL } from "@applicaster/zapp-react-native-utils/localizationUtils";
5
5
  import CloseButton from "./Buttons/CloseButton";
6
6
  import BackButton from "./Buttons/BackButton";
7
7
  import BarTitle from "./BarTitle";
8
+ import { platformSelect } from "@applicaster/zapp-react-native-utils/reactUtils";
8
9
 
9
10
  type Props = {
10
11
  screenStyles: {
@@ -23,6 +24,11 @@ const defaultState: NavBarState = {
23
24
  title: "",
24
25
  };
25
26
 
27
+ const SafeAreaViewPlatform = platformSelect({
28
+ ios: RNSafeAreaView,
29
+ android: SafeAreaView,
30
+ });
31
+
26
32
  const BarView = ({ screenStyles, barState = defaultState }: Props) => {
27
33
  const isRTL = useIsRTL();
28
34
 
@@ -44,14 +50,15 @@ const BarView = ({ screenStyles, barState = defaultState }: Props) => {
44
50
  );
45
51
 
46
52
  return (
47
- <View
53
+ <SafeAreaViewPlatform
54
+ edges={["top", "left", "right"]}
48
55
  style={[style.view, isRTL ? style.rtlStyle : {}]}
49
- testID="BarView-Container"
56
+ testID="BarView-safeAreaView"
50
57
  >
51
58
  {backButton}
52
59
  <BarTitle title={barState.title} screenStyles={screenStyles} />
53
60
  {closeButton}
54
- </View>
61
+ </SafeAreaViewPlatform>
55
62
  );
56
63
  };
57
64
 
@@ -46,7 +46,7 @@ describe("BarView", () => {
46
46
  <BarView screenStyles={screenStyles} barState={barState} />
47
47
  );
48
48
 
49
- expect(getByTestId("BarView-Container").props.style).toContainEqual({
49
+ expect(getByTestId("BarView-safeAreaView").props.style).toContainEqual({
50
50
  transform: [{ scaleX: -1 }],
51
51
  });
52
52
  });
@@ -58,7 +58,7 @@ describe("BarView", () => {
58
58
  <BarView screenStyles={screenStyles} barState={barState} />
59
59
  );
60
60
 
61
- expect(getByTestId("BarView-Container").props.style).not.toContainEqual({
61
+ expect(getByTestId("BarView-safeAreaView").props.style).not.toContainEqual({
62
62
  transform: [{ scaleX: -1 }],
63
63
  });
64
64
  });
@@ -0,0 +1,154 @@
1
+ import { renderHook, act } from "@testing-library/react-native";
2
+
3
+ type DimensionsStore = {
4
+ setState: (
5
+ partial: { dimensions: Record<string, unknown>; ids: string[] },
6
+ replace?: boolean
7
+ ) => void;
8
+ };
9
+
10
+ const createdStores: DimensionsStore[] = [];
11
+
12
+ jest.mock("zustand", () => {
13
+ const actual = jest.requireActual<typeof import("zustand")>("zustand");
14
+
15
+ return {
16
+ ...actual,
17
+ create: ((initializer: Parameters<typeof actual.create>[0]) => {
18
+ const store = actual.create(initializer);
19
+ createdStores.push(store);
20
+
21
+ return store;
22
+ }) as typeof actual.create,
23
+ };
24
+ });
25
+
26
+ import { useCachedDimensions } from "../index";
27
+
28
+ const resetStore = () => {
29
+ const store = createdStores[createdStores.length - 1];
30
+
31
+ act(() => {
32
+ store?.setState({ dimensions: {}, ids: [] });
33
+ });
34
+ };
35
+
36
+ describe("useCachedDimensions", () => {
37
+ beforeEach(() => {
38
+ resetStore();
39
+ });
40
+
41
+ describe("getCachedDimensions", () => {
42
+ it("returns default dimensions when id has no cached value", () => {
43
+ const { result } = renderHook(() => useCachedDimensions("component-1"));
44
+
45
+ expect(result.current.getCachedDimensions()).toEqual({
46
+ width: undefined,
47
+ height: undefined,
48
+ });
49
+ });
50
+
51
+ it("returns cached dimensions after setCachedDimensions", () => {
52
+ const { result } = renderHook(() => useCachedDimensions("component-1"));
53
+ const dimensions = { width: 100, height: 200 };
54
+
55
+ act(() => {
56
+ result.current.setCachedDimensions(dimensions);
57
+ });
58
+
59
+ expect(result.current.getCachedDimensions()).toEqual(dimensions);
60
+ });
61
+ });
62
+
63
+ describe("setCachedDimensions", () => {
64
+ it("updates dimensions for the hook id", () => {
65
+ const { result } = renderHook(() => useCachedDimensions("component-1"));
66
+
67
+ act(() => {
68
+ result.current.setCachedDimensions({ width: 50, height: 75 });
69
+ });
70
+
71
+ expect(result.current.getCachedDimensions()).toEqual({
72
+ width: 50,
73
+ height: 75,
74
+ });
75
+ });
76
+
77
+ it("shares cached dimensions between hook instances with the same id", () => {
78
+ const { result: first } = renderHook(() =>
79
+ useCachedDimensions("shared-id")
80
+ );
81
+
82
+ const { result: second } = renderHook(() =>
83
+ useCachedDimensions("shared-id")
84
+ );
85
+
86
+ act(() => {
87
+ first.current.setCachedDimensions({ width: 10, height: 20 });
88
+ });
89
+
90
+ expect(second.current.getCachedDimensions()).toEqual({
91
+ width: 10,
92
+ height: 20,
93
+ });
94
+ });
95
+ });
96
+
97
+ describe("getLastCachedDimensions", () => {
98
+ it("returns undefined when no dimensions have been cached", () => {
99
+ const { result } = renderHook(() => useCachedDimensions("component-1"));
100
+
101
+ expect(result.current.getLastCachedDimensions()).toBeUndefined();
102
+ });
103
+
104
+ it("returns the most recently cached dimensions across ids", () => {
105
+ const { result: first } = renderHook(() => useCachedDimensions("first"));
106
+
107
+ const { result: second } = renderHook(() =>
108
+ useCachedDimensions("second")
109
+ );
110
+
111
+ act(() => {
112
+ first.current.setCachedDimensions({ width: 100, height: 100 });
113
+ });
114
+
115
+ act(() => {
116
+ second.current.setCachedDimensions({ width: 200, height: 200 });
117
+ });
118
+
119
+ expect(first.current.getLastCachedDimensions()).toEqual({
120
+ width: 200,
121
+ height: 200,
122
+ });
123
+ });
124
+
125
+ it("returns the latest dimensions when the same id is updated multiple times", () => {
126
+ const { result } = renderHook(() => useCachedDimensions("component-1"));
127
+
128
+ act(() => {
129
+ result.current.setCachedDimensions({ width: 100, height: 100 });
130
+ });
131
+
132
+ act(() => {
133
+ result.current.setCachedDimensions({ width: 300, height: 400 });
134
+ });
135
+
136
+ expect(result.current.getLastCachedDimensions()).toEqual({
137
+ width: 300,
138
+ height: 400,
139
+ });
140
+ });
141
+ });
142
+
143
+ describe("return value", () => {
144
+ it("exposes getCachedDimensions, setCachedDimensions, and getLastCachedDimensions", () => {
145
+ const { result } = renderHook(() => useCachedDimensions("component-1"));
146
+
147
+ expect(result.current).toEqual({
148
+ getCachedDimensions: expect.any(Function),
149
+ setCachedDimensions: expect.any(Function),
150
+ getLastCachedDimensions: expect.any(Function),
151
+ });
152
+ });
153
+ });
154
+ });
@@ -1,6 +1,7 @@
1
1
  import { create, useStore } from "zustand";
2
2
  import { produce } from "immer";
3
3
  import { useCallback, useMemo } from "react";
4
+ import { last } from "@applicaster/zapp-react-native-utils/utils";
4
5
 
5
6
  type Dimensions = {
6
7
  width: Option<number>;
@@ -10,14 +11,17 @@ type Dimensions = {
10
11
  type DimensionsState = {
11
12
  dimensions: Record<string, Dimensions>;
12
13
  setCachedDimensions: (id: string, dimensions: Dimensions) => void;
14
+ ids: string[];
13
15
  };
14
16
 
15
17
  const dimensionsStore = create<DimensionsState>((set) => ({
16
18
  dimensions: {},
19
+ ids: [],
17
20
  setCachedDimensions: (id, dimensions) =>
18
21
  set(
19
22
  produce((draft) => {
20
23
  draft.dimensions[id] = dimensions;
24
+ draft.ids.push(id);
21
25
  })
22
26
  ),
23
27
  }));
@@ -28,7 +32,7 @@ const initialDimensions = () => ({
28
32
  });
29
33
 
30
34
  export const useCachedDimensions = (id: string) => {
31
- const { dimensions, setCachedDimensions } = useStore(
35
+ const { dimensions, setCachedDimensions, ids } = useStore(
32
36
  dimensionsStore
33
37
  ) as DimensionsState;
34
38
 
@@ -40,11 +44,22 @@ export const useCachedDimensions = (id: string) => {
40
44
  setCachedDimensions(id, newDimensions);
41
45
  }, []);
42
46
 
47
+ const getLastCachedDimensions = useCallback(() => {
48
+ const previousId = last(ids);
49
+
50
+ if (!previousId) {
51
+ return undefined;
52
+ }
53
+
54
+ return dimensions[previousId];
55
+ }, [dimensions, ids]);
56
+
43
57
  return useMemo(
44
58
  () => ({
45
59
  getCachedDimensions,
46
60
  setCachedDimensions: handleSetCachedDimensions,
61
+ getLastCachedDimensions,
47
62
  }),
48
- [getCachedDimensions, handleSetCachedDimensions]
63
+ [getCachedDimensions, handleSetCachedDimensions, getLastCachedDimensions]
49
64
  );
50
65
  };
@@ -0,0 +1,15 @@
1
+ import React from "react";
2
+ import { render } from "@testing-library/react-native";
3
+ import { withModalNavigationContextProvider } from "../";
4
+
5
+ describe("withModalNavigationContextProvider", () => {
6
+ it("does not re-render the wrapped component when props are unchanged", () => {
7
+ const Component = jest.fn(() => null);
8
+ const WrappedComponent = withModalNavigationContextProvider(Component);
9
+ const { rerender } = render(<WrappedComponent title="Modal" />);
10
+
11
+ rerender(<WrappedComponent title="Modal" />);
12
+
13
+ expect(Component).toHaveBeenCalledTimes(1);
14
+ });
15
+ });
@@ -6,11 +6,11 @@ export function useModalNavigationContext() {
6
6
  }
7
7
 
8
8
  export function withModalNavigationContextProvider(Component) {
9
- return function WrappedWithModalNavigationContextProvider(props) {
9
+ return React.memo(function WrappedWithModalNavigationContextProvider(props) {
10
10
  return (
11
11
  <ModalNavigationContext.Provider value={true}>
12
12
  <Component {...props} />
13
13
  </ModalNavigationContext.Provider>
14
14
  );
15
- };
15
+ });
16
16
  }
@@ -0,0 +1,347 @@
1
+ import { renderHook } from "@testing-library/react-native";
2
+
3
+ const mockRegister = jest.fn().mockResolvedValue(undefined);
4
+
5
+ // The X-Ray mock (__mocks__/@applicaster/quick-brick-xray) has no toolbox API: give it one.
6
+ require("@applicaster/quick-brick-xray").registerReactToolbox = mockRegister;
7
+
8
+ // Required after the toolbox API is in place: loading them can register the player toolbox.
9
+ const {
10
+ modalStore,
11
+ } = require("@applicaster/zapp-react-native-utils/modalState");
12
+
13
+ const {
14
+ setXrayNavigator,
15
+ } = require("@applicaster/zapp-react-native-utils/navigationUtils/xrayNavigator");
16
+
17
+ const {
18
+ zappHookModalStore,
19
+ } = require("@applicaster/zapp-react-native-ui-components/Contexts/ZappHookModalContext");
20
+
21
+ const mockStore = {
22
+ rivers: { home: { name: "Home", type: "general_content" } },
23
+ plugins: [
24
+ { identifier: "quick-brick-epg-screen", name: "EPG" },
25
+ { identifier: "login-plugin", name: "Login" },
26
+ ],
27
+ };
28
+
29
+ jest.mock("@applicaster/zapp-react-native-redux/AppStore", () => ({
30
+ appStore: { get: (key) => mockStore[key] },
31
+ }));
32
+
33
+ const { useScreenToolboxRecord, getTopPathname } = require("../screenToolbox");
34
+
35
+ const home = { id: "home", name: "Home", type: "general_content" };
36
+
37
+ const stack = {
38
+ "/river/home": { screen: home },
39
+ "/river/epg": {
40
+ screen: { id: "epg", name: "Guide", type: "quick-brick-epg-screen" },
41
+ },
42
+ "/river/tabs": {
43
+ screen: { id: "tabs", name: "Tabs", type: "quick-brick-tabs" },
44
+ nested: { screen: { id: "t1", name: "Tab 1", type: "general_content" } },
45
+ },
46
+ "/river/home/hooks/login": {
47
+ screen: { identifier: "login-plugin", name: "Login" },
48
+ entry: { payload: { id: "e1", title: "One" } },
49
+ },
50
+ };
51
+
52
+ const navigate = (pathname: string, videoModalState = { visible: false }) =>
53
+ setXrayNavigator({
54
+ getPathname: () => pathname,
55
+ getStackForPathname: (route) =>
56
+ stack[route] ? { route, state: stack[route] } : undefined,
57
+ videoModalState,
58
+ } as unknown as QuickBrickAppNavigator);
59
+
60
+ const mount = (
61
+ component,
62
+ index,
63
+ feed?,
64
+ parentId?,
65
+ pathname = "/river/home"
66
+ ) => {
67
+ const hook = renderHook(() =>
68
+ useScreenToolboxRecord(component, "home", index, parentId, pathname)
69
+ );
70
+
71
+ hook.result.current.feed = feed;
72
+
73
+ return hook;
74
+ };
75
+
76
+ const current = (args = {}) => {
77
+ // Other toolboxes (player) may register first.
78
+ const { tools } = mockRegister.mock.calls.find(
79
+ ([toolbox]) => toolbox.name === "screen"
80
+ )[0];
81
+
82
+ return tools.find((tool) => tool.name === "current").handler(args);
83
+ };
84
+
85
+ const presentHook = (overrides = {}) =>
86
+ zappHookModalStore.setState({
87
+ state: {
88
+ path: "/hooks/login",
89
+ screenData: {
90
+ hookPlugin: { identifier: "login-plugin", name: "Login" },
91
+ payload: { id: "e1", title: "One" },
92
+ },
93
+ } as any,
94
+ isHooksExecutionInProgress: true,
95
+ isRunningInBackground: false,
96
+ ...overrides,
97
+ });
98
+
99
+ describe("screenToolbox", () => {
100
+ // Registers the toolbox; the registration is kept for the whole module, as in the app.
101
+ beforeAll(() => mount({ id: "init" }, 0).unmount());
102
+
103
+ beforeEach(() => navigate("/river/home"));
104
+
105
+ afterEach(() => {
106
+ modalStore.getState().setModalState({ visible: false, screen: null });
107
+ zappHookModalStore.getState().resetState();
108
+ zappHookModalStore.getState().setIsHooksExecutionInProgress(false);
109
+ });
110
+
111
+ it("returns the top screen's mounted components in order, with feed and entries", () => {
112
+ const second = mount({ id: "c2", component_type: "grid" }, 1, {
113
+ url: "https://feed/2",
114
+ loading: true,
115
+ });
116
+
117
+ const first = mount({ id: "c1", component_type: "horizontal_list" }, 0, {
118
+ url: "https://feed/1",
119
+ loading: false,
120
+ data: {
121
+ entry: [
122
+ { id: "e1", title: "One" },
123
+ { id: "e2", title: "Two" },
124
+ ],
125
+ },
126
+ });
127
+
128
+ const third = mount({ id: "c3", component_type: "grid" }, 2, {
129
+ url: "https://feed/3",
130
+ error: new Error("404"),
131
+ });
132
+
133
+ expect(current({ limit: 1 })).toEqual({
134
+ pathname: "/river/home",
135
+ screen: {
136
+ kind: "river",
137
+ id: "home",
138
+ name: "Home",
139
+ type: "general_content",
140
+ },
141
+ entry: null,
142
+ rivers: [
143
+ {
144
+ id: "home",
145
+ name: "Home",
146
+ type: "general_content",
147
+ components: [
148
+ {
149
+ index: 0,
150
+ id: "c1",
151
+ type: "horizontal_list",
152
+ feed: { url: "https://feed/1", state: "loaded", count: 2 },
153
+ entries: [{ index: 0, id: "e1", title: "One" }],
154
+ },
155
+ {
156
+ index: 1,
157
+ id: "c2",
158
+ type: "grid",
159
+ feed: { url: "https://feed/2", state: "loading", count: 0 },
160
+ entries: [],
161
+ },
162
+ {
163
+ index: 2,
164
+ id: "c3",
165
+ type: "grid",
166
+ feed: {
167
+ url: "https://feed/3",
168
+ state: "error",
169
+ error: "404",
170
+ count: 0,
171
+ },
172
+ entries: [],
173
+ },
174
+ ],
175
+ },
176
+ ],
177
+ mounted: ["/river/home"],
178
+ });
179
+
180
+ [first, second, third].forEach((hook) => hook.unmount());
181
+
182
+ expect(current().rivers).toEqual([]);
183
+ });
184
+
185
+ it("nests a group's children under it", () => {
186
+ const group = mount({ id: "g", component_type: "group" }, 1);
187
+
188
+ const child = mount(
189
+ { id: "g0", component_type: "hero" },
190
+ 0,
191
+ undefined,
192
+ "g"
193
+ );
194
+
195
+ const row = mount({ id: "r", component_type: "grid" }, 0);
196
+
197
+ expect(
198
+ current().rivers[0].components.map(({ id, children }) => ({
199
+ id,
200
+ children: children?.map((item) => item.id),
201
+ }))
202
+ ).toEqual([
203
+ { id: "r", children: undefined },
204
+ { id: "g", children: ["g0"] },
205
+ ]);
206
+
207
+ [group, child, row].forEach((hook) => hook.unmount());
208
+ });
209
+
210
+ it("reads another mounted screen by pathname", () => {
211
+ const other = mount({ id: "c" }, 0, undefined, undefined, "/river/epg");
212
+
213
+ expect(current({ pathname: "/river/epg" }).rivers[0].components).toEqual([
214
+ expect.objectContaining({ id: "c" }),
215
+ ]);
216
+
217
+ expect(current().mounted).toEqual(["/river/epg"]);
218
+
219
+ other.unmount();
220
+ });
221
+
222
+ it("names the plugin that renders a screen plugin", () => {
223
+ navigate("/river/epg");
224
+
225
+ expect(current().screen).toEqual({
226
+ kind: "plugin",
227
+ id: "epg",
228
+ name: "Guide",
229
+ type: "quick-brick-epg-screen",
230
+ plugin: { identifier: "quick-brick-epg-screen", name: "EPG" },
231
+ });
232
+ });
233
+
234
+ it("reports the nested tab screen", () => {
235
+ navigate("/river/tabs");
236
+
237
+ expect(current().nested).toEqual({
238
+ id: "t1",
239
+ name: "Tab 1",
240
+ type: "general_content",
241
+ });
242
+ });
243
+
244
+ it("unwraps the entry of a hook screen on the stack", () => {
245
+ navigate("/river/home/hooks/login");
246
+
247
+ expect(current()).toMatchObject({
248
+ screen: { kind: "hook", plugin: { identifier: "login-plugin" } },
249
+ entry: { id: "e1", title: "One" },
250
+ });
251
+ });
252
+
253
+ it("says when there is no screen yet", () => {
254
+ navigate("/");
255
+
256
+ expect(current().screen).toEqual({ kind: "none" });
257
+ });
258
+
259
+ it("reads a modal's screen from the modal store, its pathname as ModalProvider builds it", () => {
260
+ modalStore.getState().openModal({ item: home, props: { screenId: "m1" } });
261
+
262
+ expect(current()).toMatchObject({
263
+ pathname: "modal/m1",
264
+ screen: { kind: "river", id: "home" },
265
+ });
266
+ });
267
+
268
+ it("describes a modal hook by its plugin", () => {
269
+ modalStore.getState().openModal({
270
+ item: {
271
+ hookPlugin: { identifier: "login-plugin", name: "Login" },
272
+ payload: { id: "e1", title: "One" },
273
+ },
274
+ props: { screenType: "hooks" },
275
+ } as any);
276
+
277
+ expect(current()).toMatchObject({
278
+ screen: {
279
+ kind: "hook",
280
+ plugin: { identifier: "login-plugin", name: "Login" },
281
+ },
282
+ entry: { id: "e1", title: "One" },
283
+ });
284
+ });
285
+
286
+ it("describes a bottom sheet", () => {
287
+ modalStore.getState().openModal({
288
+ item: { modalBottomSheetContentProps: {} },
289
+ } as any);
290
+
291
+ expect(current().screen).toEqual({ kind: "bottomSheet" });
292
+ });
293
+
294
+ it("puts a modal over the video modal, and the video modal over the stack unless docked", () => {
295
+ const item = { id: "v1", title: "Video" };
296
+
297
+ navigate("/river/home", {
298
+ visible: true,
299
+ mode: "MAXIMIZED",
300
+ item,
301
+ } as any);
302
+
303
+ expect(current()).toMatchObject({
304
+ pathname: "video-modal/v1",
305
+ screen: { kind: "player" },
306
+ entry: { id: "v1", title: "Video" },
307
+ });
308
+
309
+ modalStore.getState().openModal({ item: home });
310
+
311
+ expect(getTopPathname()).toBe("modal/home");
312
+
313
+ modalStore.getState().setModalState({ visible: false, screen: null });
314
+
315
+ navigate("/river/home", {
316
+ visible: true,
317
+ mode: "MINIMIZED",
318
+ item,
319
+ } as any);
320
+
321
+ expect(getTopPathname()).toBe("/river/home");
322
+ });
323
+
324
+ it("puts a presented hook screen on top, not one running in the background", () => {
325
+ presentHook();
326
+
327
+ expect(current()).toMatchObject({
328
+ pathname: "hooks-modal/Login",
329
+ screen: {
330
+ kind: "hook",
331
+ name: "Login",
332
+ plugin: { identifier: "login-plugin" },
333
+ },
334
+ entry: { id: "e1", title: "One" },
335
+ });
336
+
337
+ presentHook({ isRunningInBackground: true });
338
+
339
+ expect(getTopPathname()).toBe("/river/home");
340
+ });
341
+
342
+ it("describes nothing for a modal pathname that is no longer presented", () => {
343
+ expect(current({ pathname: "modal/gone" }).screen).toEqual({
344
+ kind: "none",
345
+ });
346
+ });
347
+ });