@applicaster/zapp-react-native-ui-components 15.0.0-alpha.7122965878 → 15.0.0-alpha.7334279569

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 (74) hide show
  1. package/Components/GeneralContentScreen/GeneralContentScreen.tsx +35 -19
  2. package/Components/GeneralContentScreen/__tests__/GeneralContentScreen.test.tsx +104 -0
  3. package/Components/GeneralContentScreen/utils/__tests__/getScreenDataSource.test.ts +19 -0
  4. package/Components/GeneralContentScreen/utils/getScreenDataSource.ts +9 -0
  5. package/Components/HookRenderer/HookRenderer.tsx +40 -10
  6. package/Components/HookRenderer/__tests__/HookRenderer.test.tsx +60 -0
  7. package/Components/Layout/TV/NavBarContainer.tsx +1 -10
  8. package/Components/Layout/TV/__tests__/__snapshots__/NavBarContainer.test.tsx.snap +7 -12
  9. package/Components/Layout/TV/__tests__/__snapshots__/ScreenContainer.test.tsx.snap +7 -12
  10. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/__tests__/model.test.ts +80 -0
  11. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/__tests__/placement.test.ts +187 -0
  12. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/__tests__/selectors.test.ts +45 -0
  13. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/__tests__/style.test.ts +49 -0
  14. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/model.ts +47 -0
  15. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/placement.ts +170 -0
  16. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/selectors.ts +26 -0
  17. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/style.ts +29 -0
  18. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/types.ts +37 -0
  19. package/Components/MasterCell/DefaultComponents/Button.tsx +0 -15
  20. package/Components/MasterCell/DefaultComponents/LiveImage/__tests__/prepareEntry.test.ts +352 -0
  21. package/Components/MasterCell/DefaultComponents/LiveImage/executePreloadHooks.ts +136 -0
  22. package/Components/MasterCell/DefaultComponents/LiveImage/index.tsx +33 -16
  23. package/Components/MasterCell/DefaultComponents/PressableView.tsx +196 -0
  24. package/Components/MasterCell/DefaultComponents/Text/index.tsx +2 -6
  25. package/Components/MasterCell/DefaultComponents/index.ts +2 -0
  26. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/Asset.ts +46 -0
  27. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/Button.ts +126 -0
  28. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/ButtonContainerView.ts +23 -0
  29. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/Spacer.ts +16 -0
  30. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/TextLabel.ts +67 -0
  31. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/TextLabelsContainer.ts +32 -0
  32. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/PressableView.test.tsx +191 -0
  33. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/builders.test.ts +140 -0
  34. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/index.test.ts +222 -0
  35. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/helpers.ts +105 -0
  36. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/index.ts +104 -0
  37. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/utils/__tests__/insertButtons.test.ts +118 -0
  38. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/utils/index.ts +73 -0
  39. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/__tests__/index.test.ts +86 -0
  40. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/index.ts +35 -52
  41. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/utils/__tests__/getPluginIdentifier.test.ts +35 -171
  42. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/utils/index.ts +36 -145
  43. package/Components/MasterCell/elementMapper.tsx +1 -0
  44. package/Components/PreloaderWrapper/__tests__/index.test.tsx +26 -0
  45. package/Components/PreloaderWrapper/index.tsx +15 -0
  46. package/Components/River/RefreshControl.tsx +30 -13
  47. package/Components/River/RiverItem.tsx +26 -20
  48. package/Components/ScreenFeedLoader/ScreenFeedLoader.tsx +46 -0
  49. package/Components/ScreenFeedLoader/__tests__/ScreenFeedLoader.test.tsx +94 -0
  50. package/Components/ScreenFeedLoader/index.ts +1 -0
  51. package/Components/ScreenResolver/__tests__/screenResolver.test.js +24 -0
  52. package/Components/ScreenResolver/hooks/index.ts +3 -0
  53. package/Components/ScreenResolver/hooks/useGetComponent.ts +15 -0
  54. package/Components/ScreenResolver/hooks/useScreenComponentResolver.tsx +90 -0
  55. package/Components/ScreenResolver/index.tsx +14 -120
  56. package/Components/ScreenResolver/utils/__tests__/getScreenTypeProps.test.ts +45 -0
  57. package/Components/ScreenResolver/utils/getScreenTypeProps.ts +43 -0
  58. package/Components/ScreenResolver/utils/index.ts +1 -0
  59. package/Components/ScreenResolver/withDefaultScreenContext.tsx +16 -0
  60. package/Components/ScreenResolverFeedProvider/ScreenResolverFeedProvider.tsx +25 -0
  61. package/Components/ScreenResolverFeedProvider/__tests__/ScreenResolverFeedProvider.test.tsx +44 -0
  62. package/Components/ScreenResolverFeedProvider/index.ts +1 -0
  63. package/Components/VideoLive/LiveImageManager.ts +199 -54
  64. package/Components/VideoLive/PlayerLiveImageComponent.tsx +31 -33
  65. package/Components/VideoLive/__tests__/PlayerLiveImageComponent.test.tsx +2 -17
  66. package/Components/ZappUIComponent/index.tsx +12 -6
  67. package/Components/default-cell-renderer/viewTrees/mobile/index.ts +0 -3
  68. package/Components/index.js +1 -1
  69. package/Contexts/ScreenContext/__tests__/index.test.tsx +57 -0
  70. package/Contexts/ScreenContext/index.tsx +17 -0
  71. package/package.json +5 -5
  72. package/Components/MasterCell/DefaultComponents/Text/utils/__tests__/withAdjustedLineHeight.test.ts +0 -46
  73. package/Components/MasterCell/DefaultComponents/Text/utils/index.ts +0 -21
  74. /package/Components/HookRenderer/{index.tsx → index.ts} +0 -0
@@ -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
  >
@@ -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
+ });
@@ -0,0 +1,187 @@
1
+ import {
2
+ insertBetweenLabelContainers,
3
+ insertBetweenLabels,
4
+ } from "../placement";
5
+
6
+ describe("ActionButtonsCore placement", () => {
7
+ const buttons = { type: "View", name: "buttons" };
8
+
9
+ const above_labels = [
10
+ { name: "above_label_1" },
11
+ { name: "above_label_2" },
12
+ { name: "above_label_3" },
13
+ ];
14
+
15
+ const below_labels = [
16
+ { name: "below_label_1" },
17
+ { name: "below_label_2" },
18
+ { name: "below_label_3" },
19
+ ];
20
+
21
+ it("inserts buttons after the matching label", () => {
22
+ expect(
23
+ insertBetweenLabels({ position: "below_label_2" }, buttons, below_labels)
24
+ ).toEqual([below_labels[0], below_labels[1], buttons, below_labels[2]]);
25
+ });
26
+
27
+ it("inserts buttons before the matching label", () => {
28
+ expect(
29
+ insertBetweenLabels({ position: "above_label_2" }, buttons, above_labels)
30
+ ).toEqual([above_labels[0], buttons, above_labels[1], above_labels[2]]);
31
+ });
32
+
33
+ it("prepends buttons only when on_top is allowed", () => {
34
+ expect(
35
+ insertBetweenLabels(
36
+ { position: "on_top", allowOnTop: true },
37
+ buttons,
38
+ below_labels
39
+ )
40
+ ).toEqual([buttons, ...below_labels]);
41
+
42
+ expect(
43
+ insertBetweenLabels(
44
+ { position: "on_top", allowOnTop: false },
45
+ buttons,
46
+ below_labels
47
+ )
48
+ ).toEqual(below_labels);
49
+ });
50
+
51
+ it("appends buttons when appendWhenMissing is enabled", () => {
52
+ expect(
53
+ insertBetweenLabels(
54
+ { position: "unknown", appendWhenMissing: true },
55
+ buttons,
56
+ below_labels
57
+ )
58
+ ).toEqual([...below_labels, buttons]);
59
+ });
60
+
61
+ it("returns labels unchanged when appendWhenMissing is disabled", () => {
62
+ expect(
63
+ insertBetweenLabels(
64
+ { position: "unknown", appendWhenMissing: false },
65
+ buttons,
66
+ below_labels
67
+ )
68
+ ).toEqual(below_labels);
69
+ });
70
+
71
+ const labelContainers = [
72
+ {
73
+ elements: [
74
+ {
75
+ elements: [{ name: "top_label_1" }, { name: "top_label_2" }],
76
+ },
77
+ ],
78
+ },
79
+ {
80
+ elements: [
81
+ {
82
+ elements: [{ name: "bottom_label_1" }],
83
+ },
84
+ ],
85
+ },
86
+ ];
87
+
88
+ it("inserts buttons after the matching label in a nested container", () => {
89
+ expect(
90
+ insertBetweenLabelContainers(
91
+ { position: "below_top_label_2" },
92
+ buttons,
93
+ labelContainers
94
+ )
95
+ ).toEqual([
96
+ {
97
+ elements: [
98
+ {
99
+ elements: [
100
+ { name: "top_label_1" },
101
+ { name: "top_label_2" },
102
+ buttons,
103
+ ],
104
+ },
105
+ ],
106
+ },
107
+ labelContainers[1],
108
+ ]);
109
+ });
110
+
111
+ it("inserts buttons before the matching label in a nested container", () => {
112
+ expect(
113
+ insertBetweenLabelContainers(
114
+ { position: "above_top_label_2" },
115
+ buttons,
116
+ labelContainers
117
+ )
118
+ ).toEqual([
119
+ {
120
+ elements: [
121
+ {
122
+ elements: [
123
+ { name: "top_label_1" },
124
+ buttons,
125
+ { name: "top_label_2" },
126
+ ],
127
+ },
128
+ ],
129
+ },
130
+ labelContainers[1],
131
+ ]);
132
+ });
133
+
134
+ it("prepends buttons into the first container only when on_top is allowed", () => {
135
+ expect(
136
+ insertBetweenLabelContainers(
137
+ { position: "on_top", allowOnTop: true },
138
+ buttons,
139
+ labelContainers
140
+ )
141
+ ).toEqual([
142
+ {
143
+ elements: [buttons, ...labelContainers[0].elements],
144
+ },
145
+ labelContainers[1],
146
+ ]);
147
+ });
148
+
149
+ it("appends buttons into the last container when configured", () => {
150
+ expect(
151
+ insertBetweenLabelContainers(
152
+ { position: "unknown", appendWhenMissing: true },
153
+ buttons,
154
+ labelContainers
155
+ )
156
+ ).toEqual([
157
+ labelContainers[0],
158
+ {
159
+ elements: [...labelContainers[1].elements, buttons],
160
+ },
161
+ ]);
162
+ });
163
+
164
+ it("inserts buttons after a matching label in a direct two-level container", () => {
165
+ const directLabelContainers = [
166
+ {
167
+ elements: [{ name: "top_label_1" }, { name: "top_label_2" }],
168
+ },
169
+ {
170
+ elements: [{ name: "bottom_label_1" }],
171
+ },
172
+ ];
173
+
174
+ expect(
175
+ insertBetweenLabelContainers(
176
+ { position: "below_top_label_2" },
177
+ buttons,
178
+ directLabelContainers
179
+ )
180
+ ).toEqual([
181
+ {
182
+ elements: [{ name: "top_label_1" }, { name: "top_label_2" }, buttons],
183
+ },
184
+ directLabelContainers[1],
185
+ ]);
186
+ });
187
+ });