@applicaster/zapp-react-native-ui-components 15.0.0-alpha.5197372201 → 15.0.0-alpha.5262500595

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 (124) hide show
  1. package/Components/Cell/TvOSCellComponent.tsx +1 -3
  2. package/Components/GeneralContentScreen/GeneralContentScreen.tsx +35 -19
  3. package/Components/GeneralContentScreen/__tests__/GeneralContentScreen.test.tsx +104 -0
  4. package/Components/GeneralContentScreen/utils/__tests__/getScreenDataSource.test.ts +19 -0
  5. package/Components/GeneralContentScreen/utils/getScreenDataSource.ts +9 -0
  6. package/Components/HookRenderer/HookRenderer.tsx +40 -10
  7. package/Components/HookRenderer/__tests__/HookRenderer.test.tsx +60 -0
  8. package/Components/Layout/TV/NavBarContainer.tsx +1 -10
  9. package/Components/Layout/TV/__tests__/__snapshots__/NavBarContainer.test.tsx.snap +7 -12
  10. package/Components/Layout/TV/__tests__/__snapshots__/ScreenContainer.test.tsx.snap +7 -12
  11. package/Components/Layout/TV/__tests__/__snapshots__/index.test.tsx.snap +5 -0
  12. package/Components/MasterCell/CONFIG_BUILDER_TO_REACT_COMPONENT.md +144 -0
  13. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/__tests__/model.test.ts +80 -0
  14. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/__tests__/placement.test.ts +187 -0
  15. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/__tests__/selectors.test.ts +45 -0
  16. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/__tests__/style.test.ts +49 -0
  17. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/components/ActionButtonController.tsx +165 -0
  18. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/components/__tests__/ActionButtonController.test.tsx +405 -0
  19. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/components/index.ts +1 -0
  20. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/model.ts +47 -0
  21. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/placement.ts +170 -0
  22. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/selectors.ts +26 -0
  23. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/style.ts +29 -0
  24. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/types.ts +37 -0
  25. package/Components/MasterCell/DefaultComponents/Button.tsx +0 -15
  26. package/Components/MasterCell/DefaultComponents/ButtonContainerView/components/HorizontalSeparator.tsx +8 -0
  27. package/Components/MasterCell/DefaultComponents/ButtonContainerView/index.tsx +15 -0
  28. package/Components/MasterCell/DefaultComponents/ButtonContainerView/index.tv.android.tsx +58 -0
  29. package/Components/MasterCell/DefaultComponents/{tv/ButtonContainerView/index.tsx → ButtonContainerView/index.tv.tsx} +3 -11
  30. package/Components/MasterCell/DefaultComponents/ButtonContainerView/index.web.ts +1 -0
  31. package/Components/MasterCell/DefaultComponents/ButtonContainerView/types.ts +40 -0
  32. package/Components/MasterCell/DefaultComponents/DataProvider/index.tsx +163 -0
  33. package/Components/MasterCell/DefaultComponents/FocusableView/index.android.tsx +2 -23
  34. package/Components/MasterCell/DefaultComponents/FocusableView/index.tsx +4 -22
  35. package/Components/MasterCell/DefaultComponents/Image/Image.android.tsx +3 -1
  36. package/Components/MasterCell/DefaultComponents/LiveImage/__tests__/prepareEntry.test.ts +352 -0
  37. package/Components/MasterCell/DefaultComponents/LiveImage/executePreloadHooks.ts +136 -0
  38. package/Components/MasterCell/DefaultComponents/LiveImage/index.tsx +33 -16
  39. package/Components/MasterCell/DefaultComponents/PressableView.tsx +34 -0
  40. package/Components/MasterCell/DefaultComponents/Text/hooks/useText.ts +11 -0
  41. package/Components/MasterCell/DefaultComponents/Text/index.tsx +2 -6
  42. package/Components/MasterCell/DefaultComponents/__tests__/DataProvider.test.tsx +141 -0
  43. package/Components/MasterCell/DefaultComponents/index.ts +9 -3
  44. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/ActionButton.tsx +135 -0
  45. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/Asset.ts +33 -0
  46. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/AssetComponent.tsx +22 -0
  47. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/Button.ts +125 -0
  48. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/Spacer.ts +16 -0
  49. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/TextLabel.ts +67 -0
  50. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/TextLabelsContainer.ts +37 -0
  51. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/PressableView.test.tsx +393 -0
  52. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/builders.test.ts +141 -0
  53. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/index.test.ts +343 -0
  54. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/helpers.ts +105 -0
  55. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/index.ts +122 -0
  56. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/utils/__tests__/insertButtons.test.ts +118 -0
  57. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/utils/index.ts +238 -0
  58. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/Asset.ts +4 -18
  59. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/Button.ts +24 -73
  60. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/TextLabelsContainer.ts +37 -18
  61. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/TvActionButton.tsx +27 -0
  62. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/__tests__/index.test.ts +89 -0
  63. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/__tests__/renderedTree.test.tsx +231 -0
  64. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/index.ts +47 -52
  65. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/utils/__tests__/getPluginIdentifier.test.ts +35 -171
  66. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/utils/index.ts +98 -145
  67. package/Components/MasterCell/MappingFunctions/index.js +3 -2
  68. package/Components/MasterCell/README.md +4 -0
  69. package/Components/MasterCell/__tests__/__snapshots__/dataAdapter.test.js.snap +24 -0
  70. package/Components/MasterCell/__tests__/configInflater.test.js +1 -0
  71. package/Components/MasterCell/__tests__/elementMapper.test.js +46 -0
  72. package/Components/MasterCell/dataAdapter.ts +4 -1
  73. package/Components/MasterCell/elementMapper.tsx +52 -7
  74. package/Components/MasterCell/utils/__tests__/cloneChildrenWithIds.test.tsx +43 -0
  75. package/Components/MasterCell/utils/__tests__/useFilterChildren.test.tsx +80 -0
  76. package/Components/MasterCell/utils/index.ts +85 -15
  77. package/Components/Navigator/StackNavigator.tsx +6 -0
  78. package/Components/PlayerContainer/PlayerContainer.tsx +1 -10
  79. package/Components/PreloaderWrapper/__tests__/index.test.tsx +26 -0
  80. package/Components/PreloaderWrapper/index.tsx +15 -0
  81. package/Components/River/ComponentsMap/ComponentsMap.tsx +2 -16
  82. package/Components/River/RefreshControl.tsx +19 -88
  83. package/Components/River/River.tsx +9 -82
  84. package/Components/River/RiverItem.tsx +26 -20
  85. package/Components/River/hooks/__tests__/usePullToRefresh.test.ts +132 -0
  86. package/Components/River/hooks/index.ts +1 -0
  87. package/Components/River/hooks/usePullToRefresh.ts +51 -0
  88. package/Components/ScreenFeedLoader/ScreenFeedLoader.tsx +46 -0
  89. package/Components/ScreenFeedLoader/__tests__/ScreenFeedLoader.test.tsx +94 -0
  90. package/Components/ScreenFeedLoader/index.ts +1 -0
  91. package/Components/ScreenResolver/__tests__/screenResolver.test.js +24 -0
  92. package/Components/ScreenResolver/hooks/index.ts +3 -0
  93. package/Components/ScreenResolver/hooks/useGetComponent.ts +15 -0
  94. package/Components/ScreenResolver/hooks/useScreenComponentResolver.tsx +90 -0
  95. package/Components/ScreenResolver/index.tsx +14 -120
  96. package/Components/ScreenResolver/utils/__tests__/getScreenTypeProps.test.ts +45 -0
  97. package/Components/ScreenResolver/utils/getScreenTypeProps.ts +43 -0
  98. package/Components/ScreenResolver/utils/index.ts +1 -0
  99. package/Components/ScreenResolver/withDefaultScreenContext.tsx +16 -0
  100. package/Components/ScreenResolverFeedProvider/ScreenResolverFeedProvider.tsx +25 -0
  101. package/Components/ScreenResolverFeedProvider/__tests__/ScreenResolverFeedProvider.test.tsx +44 -0
  102. package/Components/ScreenResolverFeedProvider/index.ts +1 -0
  103. package/Components/ScreenRevealManager/withScreenRevealManager.tsx +4 -1
  104. package/Components/TopCutoffOverlay/__tests__/TopCutoffOverlay.test.tsx +201 -0
  105. package/Components/TopCutoffOverlay/hooks/__tests__/useMarginTop.test.ts +130 -0
  106. package/Components/TopCutoffOverlay/hooks/index.ts +1 -0
  107. package/Components/TopCutoffOverlay/hooks/useMarginTop.ts +59 -0
  108. package/Components/TopCutoffOverlay/index.tsx +55 -0
  109. package/Components/VideoLive/LiveImageManager.ts +199 -54
  110. package/Components/VideoLive/PlayerLiveImageComponent.tsx +31 -33
  111. package/Components/VideoLive/__tests__/PlayerLiveImageComponent.test.tsx +2 -17
  112. package/Components/ZappUIComponent/index.tsx +12 -6
  113. package/Components/default-cell-renderer/viewTrees/mobile/index.ts +0 -3
  114. package/Components/index.js +1 -1
  115. package/Contexts/ScreenContext/__tests__/index.test.tsx +57 -0
  116. package/Contexts/ScreenContext/index.tsx +17 -0
  117. package/package.json +5 -5
  118. package/Components/MasterCell/DefaultComponents/Text/utils/__tests__/withAdjustedLineHeight.test.ts +0 -46
  119. package/Components/MasterCell/DefaultComponents/Text/utils/index.ts +0 -21
  120. package/Components/MasterCell/DefaultComponents/tv/ButtonContainerView/index.android.tsx +0 -135
  121. package/Components/MasterCell/DefaultComponents/tv/ButtonContainerView/types.ts +0 -25
  122. package/Components/PlayerContainer/ErrorDisplay/ErrorDisplay.tsx +0 -57
  123. package/Components/PlayerContainer/ErrorDisplay/index.ts +0 -9
  124. /package/Components/HookRenderer/{index.tsx → index.ts} +0 -0
@@ -195,7 +195,6 @@ class TvOSCell extends React.Component<Props, State> {
195
195
  groupId,
196
196
  component,
197
197
  index,
198
- componentsMapOffset,
199
198
  } = this.props;
200
199
 
201
200
  this.setScreenLayout(componentAnchorPointY, screenLayout);
@@ -222,8 +221,7 @@ class TvOSCell extends React.Component<Props, State> {
222
221
  const totalOffset =
223
222
  headerOffset +
224
223
  toNumberWithDefaultZero(componentAnchorPointY) +
225
- extraAnchorPointYOffset -
226
- toNumberWithDefaultZero(componentsMapOffset) +
224
+ extraAnchorPointYOffset +
227
225
  componentMarginTop +
228
226
  componentPaddingTop;
229
227
 
@@ -12,12 +12,26 @@ import { createLogger } from "@applicaster/zapp-react-native-utils/logger";
12
12
  import { isNilOrEmpty } from "@applicaster/zapp-react-native-utils/reactUtils/helpers";
13
13
  import { ScreenTrackedViewPositionsContext } from "@applicaster/zapp-react-native-ui-components/Contexts/ScreenTrackedViewPositionsContext";
14
14
  import { useEventAlerts } from "./utils/useEventAlerts";
15
+ import {
16
+ selectRiverById,
17
+ useAppSelector,
18
+ } from "@applicaster/zapp-react-native-redux";
19
+ import { getScreenDataSource } from "./utils/getScreenDataSource";
20
+ import { ScreenResolverFeedProvider } from "../ScreenResolverFeedProvider/ScreenResolverFeedProvider";
15
21
 
16
22
  const { log_debug } = createLogger({
17
23
  category: "ScreenContainer",
18
24
  subsystem: "General",
19
25
  });
20
26
 
27
+ /** Provides screen-feed from general-screen configuration (if defined) */
28
+ const useFeedData = (id) => {
29
+ const river = useAppSelector((state) => selectRiverById(state, id));
30
+ const feedData = getScreenDataSource(river);
31
+
32
+ return feedData;
33
+ };
34
+
21
35
  export const GeneralContentScreen = ({
22
36
  feed,
23
37
  screenId,
@@ -103,24 +117,26 @@ export const GeneralContentScreen = ({
103
117
  if (!isReady || isNilOrEmpty(components || uiComponents)) return null;
104
118
 
105
119
  return (
106
- <ScreenTrackedViewPositionsContext.Provider>
107
- <CellTapContext.Provider value={contextValue}>
108
- <ComponentsMap
109
- feed={feed}
110
- riverId={screenId}
111
- groupId={groupId || `general-content-screen-${screenId}`}
112
- riverComponents={components || uiComponents}
113
- scrollViewExtraProps={scrollViewExtraProps}
114
- getStaticComponentFeed={getStaticComponentFeed}
115
- extraAnchorPointYOffset={extraAnchorPointYOffset}
116
- isScreenWrappedInContainer={isScreenWrappedInContainer}
117
- parentFocus={parentFocus}
118
- focused={focused}
119
- containerHeight={containerHeight}
120
- preferredFocus={preferredFocus}
121
- {...componentsMapExtraProps}
122
- />
123
- </CellTapContext.Provider>
124
- </ScreenTrackedViewPositionsContext.Provider>
120
+ <ScreenResolverFeedProvider id={screenId} useFeedData={useFeedData}>
121
+ <ScreenTrackedViewPositionsContext.Provider>
122
+ <CellTapContext.Provider value={contextValue}>
123
+ <ComponentsMap
124
+ feed={feed}
125
+ riverId={screenId}
126
+ groupId={groupId || `general-content-screen-${screenId}`}
127
+ riverComponents={components || uiComponents}
128
+ scrollViewExtraProps={scrollViewExtraProps}
129
+ getStaticComponentFeed={getStaticComponentFeed}
130
+ extraAnchorPointYOffset={extraAnchorPointYOffset}
131
+ isScreenWrappedInContainer={isScreenWrappedInContainer}
132
+ parentFocus={parentFocus}
133
+ focused={focused}
134
+ containerHeight={containerHeight}
135
+ preferredFocus={preferredFocus}
136
+ {...componentsMapExtraProps}
137
+ />
138
+ </CellTapContext.Provider>
139
+ </ScreenTrackedViewPositionsContext.Provider>
140
+ </ScreenResolverFeedProvider>
125
141
  );
126
142
  };
@@ -0,0 +1,104 @@
1
+ import React from "react";
2
+ import { render } from "@testing-library/react-native";
3
+ import { GeneralContentScreen } from "../GeneralContentScreen";
4
+
5
+ const mockUseAppSelector = jest.fn();
6
+ const mockSelectRiverById = jest.fn();
7
+ const mockProviderSpy = jest.fn();
8
+
9
+ jest.mock("../../River/ComponentsMap", () => ({
10
+ ComponentsMap: () => {
11
+ const React = require("react");
12
+ const { View } = require("react-native");
13
+
14
+ return <View testID="components-map" />;
15
+ },
16
+ }));
17
+
18
+ jest.mock("@applicaster/zapp-react-native-utils/reactHooks/actions", () => ({
19
+ useActions: jest.fn(() => jest.fn()),
20
+ }));
21
+
22
+ jest.mock("../utils", () => ({
23
+ logger: { warn: jest.fn() },
24
+ whenMatchingType: jest.fn((_type, value) => value),
25
+ }));
26
+
27
+ jest.mock("@applicaster/zapp-react-native-utils/reactHooks/layout", () => ({
28
+ useLayoutVersion: jest.fn(() => false),
29
+ }));
30
+
31
+ jest.mock(
32
+ "@applicaster/zapp-react-native-utils/reactHooks/screen/useScreenData",
33
+ () => ({
34
+ useScreenData: jest.fn(() => ({
35
+ ui_components: [{ id: "ui-component" }],
36
+ general: {},
37
+ })),
38
+ })
39
+ );
40
+
41
+ jest.mock("../utils/useCurationAPI", () => ({
42
+ useCurationAPI: jest.fn(() => [{ id: "curation-component" }]),
43
+ }));
44
+
45
+ jest.mock("@applicaster/quick-brick-core/App/ActionSetters", () => ({
46
+ useRiverInitialState: jest.fn(() => []),
47
+ }));
48
+
49
+ jest.mock("../utils/useEventAlerts", () => ({
50
+ useEventAlerts: jest.fn(),
51
+ }));
52
+
53
+ jest.mock("@applicaster/zapp-react-native-redux", () => ({
54
+ useAppSelector: (...args) => mockUseAppSelector(...args),
55
+ selectRiverById: (...args) => mockSelectRiverById(...args),
56
+ }));
57
+
58
+ jest.mock("../utils/getScreenDataSource", () => ({
59
+ getScreenDataSource: jest.fn(() => ({
60
+ source: "https://feed",
61
+ mapping: {},
62
+ })),
63
+ }));
64
+
65
+ jest.mock(
66
+ "../../ScreenResolverFeedProvider/ScreenResolverFeedProvider",
67
+ () => ({
68
+ ScreenResolverFeedProvider: ({ id, useFeedData, children }) => {
69
+ const React = require("react");
70
+ const { View } = require("react-native");
71
+
72
+ mockProviderSpy(id, useFeedData);
73
+ useFeedData(id);
74
+
75
+ return <View testID="screen-resolver-feed-provider">{children}</View>;
76
+ },
77
+ })
78
+ );
79
+
80
+ describe("GeneralContentScreen", () => {
81
+ beforeEach(() => {
82
+ jest.clearAllMocks();
83
+ mockUseAppSelector.mockImplementation((selector) => selector({}));
84
+ mockSelectRiverById.mockReturnValue({ id: "screen-1" });
85
+ });
86
+
87
+ it("wraps content with ScreenResolverFeedProvider and renders ComponentsMap", () => {
88
+ const { getByTestId } = render(
89
+ <GeneralContentScreen
90
+ screenId="screen-1"
91
+ feed={null}
92
+ components={[{ id: "component-1" }]}
93
+ />
94
+ );
95
+
96
+ expect(getByTestId("screen-resolver-feed-provider")).toBeDefined();
97
+ expect(getByTestId("components-map")).toBeDefined();
98
+
99
+ expect(mockProviderSpy).toHaveBeenCalledWith(
100
+ "screen-1",
101
+ expect.any(Function)
102
+ );
103
+ });
104
+ });
@@ -0,0 +1,19 @@
1
+ import { getScreenDataSource } from "../getScreenDataSource";
2
+
3
+ describe("getScreenDataSource", () => {
4
+ it("returns screen_feed data when present", () => {
5
+ const result = getScreenDataSource({
6
+ data: {
7
+ screen_feed: {
8
+ source: "https://feed",
9
+ },
10
+ },
11
+ });
12
+
13
+ expect(result).toEqual({ source: "https://feed" });
14
+ });
15
+
16
+ it("returns undefined when screen_feed is missing", () => {
17
+ expect(getScreenDataSource({ data: {} })).toBeUndefined();
18
+ });
19
+ });
@@ -0,0 +1,9 @@
1
+ import { get } from "@applicaster/zapp-react-native-utils/utils";
2
+
3
+ const lookupPath = ["data", "screen_feed"];
4
+
5
+ export const getScreenDataSource = (
6
+ screenData: any
7
+ ): Option<ZappDataSource> => {
8
+ return get(screenData, lookupPath) as ZappDataSource | undefined;
9
+ };
@@ -6,7 +6,12 @@ import {
6
6
  } from "@applicaster/zapp-react-native-utils/reactHooks/navigation";
7
7
  import { useHookAnalytics } from "@applicaster/zapp-react-native-utils/analyticsUtils/helpers/hooks";
8
8
  import { useSetNavbarState } from "@applicaster/zapp-react-native-utils/reactHooks";
9
- import { PresentationType } from "../ScreenResolver";
9
+
10
+ import { componentsLogger } from "../../Helpers/logger";
11
+
12
+ const logger = componentsLogger.addSubsystem("HookRenderer");
13
+
14
+ const HOOK_PRESENTATION_TYPE = "Hook";
10
15
 
11
16
  type Props = {
12
17
  focused?: boolean;
@@ -15,20 +20,17 @@ type Props = {
15
20
  callback: hookCallback;
16
21
  };
17
22
 
18
- export const HookRenderer = (props: Props) => {
19
- const {
20
- focused,
21
- screenData: { payload, hookPlugin },
22
- callback,
23
- } = props;
24
-
25
- const { setVisible: showNavBar } = useSetNavbarState();
23
+ const HookRenderer = (props: Props) => {
24
+ const { focused, screenData, callback } = props;
25
+ const { payload, hookPlugin } = screenData;
26
26
 
27
27
  const {
28
28
  module: { Component: HookComponent, presentFullScreen },
29
29
  configuration,
30
30
  } = hookPlugin;
31
31
 
32
+ const { setVisible: showNavBar } = useSetNavbarState();
33
+
32
34
  useHookAnalytics(props);
33
35
 
34
36
  const isNavBarVisible = useIsNavBarVisible();
@@ -63,8 +65,36 @@ export const HookRenderer = (props: Props) => {
63
65
  hookPlugin,
64
66
  focused,
65
67
  parentFocus,
66
- presentationType: PresentationType.Hook,
68
+ presentationType: HOOK_PRESENTATION_TYPE,
67
69
  }}
68
70
  />
69
71
  );
70
72
  };
73
+
74
+ /**
75
+ * Guard component to prevent rendering HookRenderer when screenData or hookPlugin is missing. This is to avoid potential crashes due to missing data.
76
+ */
77
+ const HookRendererGuard = (props: Props) => {
78
+ React.useEffect(() => {
79
+ if (!props.screenData) {
80
+ logger.error(
81
+ "HookRenderer received no screenData, screen cannot be rendered"
82
+ );
83
+ } else if (!props.screenData.hookPlugin) {
84
+ logger.error(
85
+ "HookRenderer received screenData with no hookPlugin, screen cannot be rendered",
86
+ {
87
+ screenData: props.screenData,
88
+ }
89
+ );
90
+ }
91
+ }, [props.screenData]);
92
+
93
+ if (!props.screenData || !props.screenData.hookPlugin) {
94
+ return null;
95
+ }
96
+
97
+ return <HookRenderer {...props} />;
98
+ };
99
+
100
+ export { HookRendererGuard as HookRenderer };
@@ -0,0 +1,60 @@
1
+ import React from "react";
2
+ import { View } from "react-native";
3
+ import { render } from "@testing-library/react-native";
4
+ import { HookRenderer } from "..";
5
+
6
+ jest.mock("@applicaster/zapp-react-native-utils/reactUtils", () => ({
7
+ isTV: jest.fn(() => false),
8
+ }));
9
+
10
+ jest.mock("@applicaster/zapp-react-native-utils/reactHooks/navigation", () => ({
11
+ useBackHandler: jest.fn(),
12
+ useIsNavBarVisible: jest.fn(() => true),
13
+ }));
14
+
15
+ jest.mock(
16
+ "@applicaster/zapp-react-native-utils/analyticsUtils/helpers/hooks",
17
+ () => ({
18
+ useHookAnalytics: jest.fn(),
19
+ })
20
+ );
21
+
22
+ jest.mock("@applicaster/zapp-react-native-utils/reactHooks", () => ({
23
+ useSetNavbarState: jest.fn(() => ({
24
+ setVisible: jest.fn(),
25
+ })),
26
+ }));
27
+
28
+ describe("HookRenderer", () => {
29
+ it("returns null when hookPlugin is missing", () => {
30
+ const { toJSON } = render(
31
+ <HookRenderer callback={jest.fn()} screenData={{ payload: {} } as any} />
32
+ );
33
+
34
+ expect(toJSON()).toBeNull();
35
+ });
36
+
37
+ it("passes Hook presentationType to rendered hook component", () => {
38
+ const HookComponent = (props) => (
39
+ <View testID="hook-component" {...props} />
40
+ );
41
+
42
+ const { getByTestId } = render(
43
+ <HookRenderer
44
+ callback={jest.fn()}
45
+ screenData={{
46
+ payload: { foo: "bar" },
47
+ hookPlugin: {
48
+ module: {
49
+ Component: HookComponent,
50
+ presentFullScreen: false,
51
+ },
52
+ configuration: {},
53
+ },
54
+ }}
55
+ />
56
+ );
57
+
58
+ expect(getByTestId("hook-component").props.presentationType).toBe("Hook");
59
+ });
60
+ });
@@ -15,12 +15,6 @@ const styles = StyleSheet.create({
15
15
  flex: 1,
16
16
  width: "100%",
17
17
  },
18
- themeStyles: {
19
- // limits the height of the focusable container of the TopMenuBarTV component
20
- // to prevent it from being overlapped by the screen content,
21
- // as it makes TopMenuBarTV unfocusable on tvOS
22
- maxHeight: 1,
23
- },
24
18
  });
25
19
 
26
20
  export const NavBarContainer = ({ children, isVisible, onReady }: Props) => {
@@ -31,10 +25,7 @@ export const NavBarContainer = ({ children, isVisible, onReady }: Props) => {
31
25
  }, [onReady]);
32
26
 
33
27
  return isVisible ? (
34
- <View
35
- testID="nav-bar-container"
36
- style={[styles.container, styles.themeStyles]}
37
- >
28
+ <View testID="nav-bar-container" style={styles.container}>
38
29
  {children}
39
30
  </View>
40
31
  ) : null;
@@ -3,18 +3,13 @@
3
3
  exports[`NavBarContainer renders 1`] = `
4
4
  <View
5
5
  style={
6
- [
7
- {
8
- "flex": 1,
9
- "position": "absolute",
10
- "top": 0,
11
- "width": "100%",
12
- "zIndex": 10,
13
- },
14
- {
15
- "maxHeight": 1,
16
- },
17
- ]
6
+ {
7
+ "flex": 1,
8
+ "position": "absolute",
9
+ "top": 0,
10
+ "width": "100%",
11
+ "zIndex": 10,
12
+ }
18
13
  }
19
14
  testID="nav-bar-container"
20
15
  >
@@ -14,18 +14,13 @@ exports[`ScreenContainer renders 1`] = `
14
14
  >
15
15
  <View
16
16
  style={
17
- [
18
- {
19
- "flex": 1,
20
- "position": "absolute",
21
- "top": 0,
22
- "width": "100%",
23
- "zIndex": 10,
24
- },
25
- {
26
- "maxHeight": 1,
27
- },
28
- ]
17
+ {
18
+ "flex": 1,
19
+ "position": "absolute",
20
+ "top": 0,
21
+ "width": "100%",
22
+ "zIndex": 10,
23
+ }
29
24
  }
30
25
  testID="nav-bar-container"
31
26
  >
@@ -9,6 +9,11 @@ exports[`Layout TV renders 1`] = `
9
9
  excludeFromFocusSearching={true}
10
10
  id="/river/A1234"
11
11
  preferredFocus={true}
12
+ style={
13
+ {
14
+ "flex": 1,
15
+ }
16
+ }
12
17
  >
13
18
  <View
14
19
  Components={
@@ -0,0 +1,144 @@
1
+ # Config Builder -> React Component Migration Guide
2
+
3
+ This guide explains how to migrate MasterCell config-builders to React components incrementally, without breaking existing config-node rendering.
4
+
5
+ ## Why This Exists
6
+
7
+ MasterCell currently supports config-node builders such as:
8
+
9
+ ```ts
10
+ {
11
+ type: "View",
12
+ style: {},
13
+ additionalProps: {},
14
+ data: [],
15
+ elements: []
16
+ }
17
+ ```
18
+
19
+ This is still valid and should continue to work.
20
+
21
+ For incremental migration, MasterCell also supports inline React nodes:
22
+
23
+ ```ts
24
+ {
25
+ type: "ReactComponent",
26
+ style: {},
27
+ additionalProps: {
28
+ component: MyComponent
29
+ }
30
+ }
31
+ ```
32
+
33
+ This lets you migrate one builder at a time.
34
+
35
+ ## Rendering Pipeline
36
+
37
+ 1. Builder returns node config (`type`, `style`, `additionalProps`, `data`, `elements`).
38
+ 2. `configInflater` converts `additionalProps` into runtime `props` and resolves `data`.
39
+ 3. `elementMapper` renders:
40
+ 4. String `type` nodes via component registry (`components[type]`).
41
+ 5. `type: "ReactComponent"` nodes via `props.component`.
42
+
43
+ ## Migration Pattern
44
+
45
+ 1. Keep existing builder API unchanged.
46
+ 2. Add a dedicated React component for the node being migrated.
47
+ 3. Change builder output from string `type` to `type: "ReactComponent"`.
48
+ 4. Pass component reference through `additionalProps.component`.
49
+ 5. Keep behavioral props unchanged (`testID`, roles, accessibility props).
50
+ 6. Set `renderChildren` explicitly:
51
+ 7. `false` when React component renders everything itself.
52
+ 8. `true` (or omit) when builder still provides `elements` children.
53
+ 9. Set `requiresCellUUID: true` only if the component needs `cellUUID`.
54
+ 10. Update builder-shape tests and rendering tests together.
55
+
56
+ ## Example Using `TextLabelsContainer.ts`
57
+
58
+ Current builder (config-node only):
59
+
60
+ ```ts
61
+ export const TextLabelsContainer = ({ actionIdentifier, testID, style, extraProps }) => ({
62
+ type: "View",
63
+ additionalProps: {
64
+ mobileActionRole: "label_container",
65
+ },
66
+ elements: [
67
+ {
68
+ type: "Text",
69
+ style,
70
+ additionalProps: {
71
+ label: { context: actionIdentifier, name: "label_1" },
72
+ state: "default",
73
+ mobileActionRole: "label",
74
+ testID: testID ? `${testID}-label` : undefined,
75
+ ...extraProps,
76
+ },
77
+ },
78
+ ],
79
+ });
80
+ ```
81
+
82
+ Incremental migration target:
83
+
84
+ ```ts
85
+ import { TextLabelsContainerView } from "./TextLabelsContainerView";
86
+
87
+ export const TextLabelsContainer = ({ actionIdentifier, testID, style, extraProps }) => ({
88
+ type: "ReactComponent",
89
+ additionalProps: {
90
+ component: TextLabelsContainerView,
91
+ renderChildren: false,
92
+ mobileActionRole: "label_container",
93
+ actionIdentifier,
94
+ testID,
95
+ textStyle: style,
96
+ textProps: extraProps,
97
+ },
98
+ });
99
+ ```
100
+
101
+ And React component:
102
+
103
+ ```tsx
104
+ export const TextLabelsContainerView = ({
105
+ actionIdentifier,
106
+ testID,
107
+ textStyle,
108
+ textProps,
109
+ mobileActionRole,
110
+ }) => (
111
+ <View mobileActionRole={mobileActionRole}>
112
+ <Text
113
+ style={textStyle}
114
+ label={{ context: actionIdentifier, name: "label_1" }}
115
+ state="default"
116
+ mobileActionRole="label"
117
+ testID={testID ? `${testID}-label` : undefined}
118
+ {...textProps}
119
+ />
120
+ </View>
121
+ );
122
+ ```
123
+
124
+ This keeps external behavior and props contract intact while moving implementation to React.
125
+
126
+ ## Rules for Safe Incremental Migration
127
+
128
+ 1. Do not change role markers used by parent logic (for example `mobileActionRole`).
129
+ 2. Do not rename `testID` contracts during migration.
130
+ 3. Keep output shape stable for parent builders (`null` handling, `elements` presence).
131
+ 4. Keep data-mapping semantics identical (`label`, `state`, `entry`, etc.).
132
+ 5. Migrate one node per PR where possible.
133
+
134
+ ## Testing Checklist
135
+
136
+ 1. Builder test: node type and required props (`component`, role, `testID`) are correct.
137
+ 2. Rendering test: `configInflater + elementMapper` still renders expected UI.
138
+ 3. Behavior test: parent container logic that clones/filters children still works.
139
+ 4. Regression test: hidden/empty states still return `null` as before.
140
+
141
+ ## Important Limitation
142
+
143
+ `additionalProps.component` is a function reference and is suitable for local JS-built trees.
144
+ If a tree must be fully serialized JSON, use a string `componentKey` and resolve it in a registry instead of storing a function directly.
@@ -0,0 +1,80 @@
1
+ import { buildActionButtonsModel } from "../model";
2
+
3
+ describe("buildActionButtonsModel", () => {
4
+ it("returns null when the container is disabled", () => {
5
+ const configuration = {
6
+ mobile_buttons_container_buttons_enabled: false,
7
+ mobile_button_1_button_enabled: true,
8
+ };
9
+
10
+ const value = (key) => configuration[key];
11
+
12
+ expect(
13
+ buildActionButtonsModel({
14
+ configuration,
15
+ value,
16
+ containerPrefix: "mobile_buttons_container",
17
+ buttonPrefix: "mobile_button",
18
+ })
19
+ ).toBeNull();
20
+ });
21
+
22
+ it("returns explicit enabled slots and semantic container data", () => {
23
+ const configuration = {
24
+ mobile_buttons_container_buttons_enabled: true,
25
+ mobile_buttons_container_align: "right",
26
+ mobile_buttons_container_margin_top: 1,
27
+ mobile_buttons_container_margin_right: 2,
28
+ mobile_buttons_container_margin_bottom: 3,
29
+ mobile_buttons_container_margin_left: 4,
30
+ mobile_buttons_container_stacking: "vertical",
31
+ mobile_buttons_container_horizontal_gutter: 8,
32
+ mobile_buttons_container_vertical_gutter: 12,
33
+ mobile_buttons_container_independent_styles: false,
34
+ mobile_button_1_button_enabled: true,
35
+ mobile_button_2_button_enabled: false,
36
+ mobile_button_3_button_enabled: true,
37
+ };
38
+
39
+ const value = (key) => configuration[key];
40
+
41
+ expect(
42
+ buildActionButtonsModel({
43
+ configuration,
44
+ value,
45
+ containerPrefix: "mobile_buttons_container",
46
+ buttonPrefix: "mobile_button",
47
+ })
48
+ ).toEqual({
49
+ enabledSlots: [1, 3],
50
+ buttonsCount: 2,
51
+ container: {
52
+ horizontalAlign: "flex-end",
53
+ margins: {
54
+ top: 1,
55
+ right: 2,
56
+ bottom: 3,
57
+ left: 4,
58
+ },
59
+ stacking: "vertical",
60
+ horizontalGutter: 8,
61
+ verticalGutter: 12,
62
+ independentStyles: false,
63
+ },
64
+ buttons: [
65
+ {
66
+ slot: 1,
67
+ renderIndex: 0,
68
+ specificPrefix: "mobile_button_1",
69
+ stylePrefix: "mobile_button_1",
70
+ },
71
+ {
72
+ slot: 3,
73
+ renderIndex: 1,
74
+ specificPrefix: "mobile_button_3",
75
+ stylePrefix: "mobile_button_1",
76
+ },
77
+ ],
78
+ });
79
+ });
80
+ });