@applicaster/zapp-react-native-ui-components 15.0.0-rc.144 → 15.0.0-rc.145

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 (53) hide show
  1. package/Components/FocusableGroup/index.tsx +3 -2
  2. package/Components/Layout/TV/__tests__/__snapshots__/index.test.tsx.snap +5 -0
  3. package/Components/MasterCell/CONFIG_BUILDER_TO_REACT_COMPONENT.md +144 -0
  4. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/components/ActionButtonController.tsx +165 -0
  5. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/components/__tests__/ActionButtonController.test.tsx +405 -0
  6. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/components/index.ts +1 -0
  7. package/Components/MasterCell/DefaultComponents/ButtonContainerView/components/HorizontalSeparator.tsx +8 -0
  8. package/Components/MasterCell/DefaultComponents/ButtonContainerView/index.tsx +15 -0
  9. package/Components/MasterCell/DefaultComponents/ButtonContainerView/index.tv.android.tsx +58 -0
  10. package/Components/MasterCell/DefaultComponents/{tv/ButtonContainerView/index.tsx → ButtonContainerView/index.tv.tsx} +3 -11
  11. package/Components/MasterCell/DefaultComponents/ButtonContainerView/index.web.ts +1 -0
  12. package/Components/MasterCell/DefaultComponents/ButtonContainerView/types.ts +40 -0
  13. package/Components/MasterCell/DefaultComponents/DataProvider/index.tsx +163 -0
  14. package/Components/MasterCell/DefaultComponents/FocusableView/index.android.tsx +2 -23
  15. package/Components/MasterCell/DefaultComponents/FocusableView/index.tsx +4 -22
  16. package/Components/MasterCell/DefaultComponents/PressableView.tsx +7 -234
  17. package/Components/MasterCell/DefaultComponents/Text/hooks/useText.ts +11 -0
  18. package/Components/MasterCell/DefaultComponents/__tests__/DataProvider.test.tsx +141 -0
  19. package/Components/MasterCell/DefaultComponents/index.ts +7 -3
  20. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/ActionButton.tsx +135 -0
  21. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/Asset.ts +20 -29
  22. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/AssetComponent.tsx +22 -0
  23. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/Button.ts +67 -69
  24. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/TextLabelsContainer.ts +21 -16
  25. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/PressableView.test.tsx +207 -9
  26. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/builders.test.ts +56 -55
  27. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/index.test.ts +137 -16
  28. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/index.ts +49 -31
  29. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/utils/index.ts +165 -0
  30. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/Asset.ts +4 -18
  31. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/Button.ts +24 -73
  32. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/TextLabelsContainer.ts +37 -18
  33. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/TvActionButton.tsx +27 -0
  34. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/__tests__/index.test.ts +24 -21
  35. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/__tests__/renderedTree.test.tsx +231 -0
  36. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/index.ts +24 -12
  37. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/utils/index.ts +62 -0
  38. package/Components/MasterCell/MappingFunctions/index.js +3 -2
  39. package/Components/MasterCell/README.md +4 -0
  40. package/Components/MasterCell/__tests__/__snapshots__/dataAdapter.test.js.snap +24 -0
  41. package/Components/MasterCell/__tests__/configInflater.test.js +1 -0
  42. package/Components/MasterCell/__tests__/elementMapper.test.js +46 -0
  43. package/Components/MasterCell/dataAdapter.ts +4 -1
  44. package/Components/MasterCell/elementMapper.tsx +51 -7
  45. package/Components/MasterCell/utils/__tests__/cloneChildrenWithIds.test.tsx +43 -0
  46. package/Components/MasterCell/utils/__tests__/useFilterChildren.test.tsx +80 -0
  47. package/Components/MasterCell/utils/index.ts +85 -15
  48. package/Components/Navigator/StackNavigator.tsx +6 -0
  49. package/Components/ScreenRevealManager/withScreenRevealManager.tsx +4 -1
  50. package/package.json +5 -5
  51. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/ButtonContainerView.ts +0 -23
  52. package/Components/MasterCell/DefaultComponents/tv/ButtonContainerView/index.android.tsx +0 -135
  53. package/Components/MasterCell/DefaultComponents/tv/ButtonContainerView/types.ts +0 -25
@@ -1,9 +1,13 @@
1
1
  import React from "react";
2
+ import { Text as RNText, TouchableOpacity, View } from "react-native";
2
3
  import { render, fireEvent } from "@testing-library/react-native";
3
4
  import { useActions } from "@applicaster/zapp-react-native-utils/reactHooks/actions";
4
5
 
6
+ import { configInflater } from "../../../../dataAdapter";
5
7
  import { elementMapper } from "../../../../elementMapper";
6
8
  import { defaultComponents } from "../../../index";
9
+ import { MobileActionButtons } from "..";
10
+ import { AssetComponent } from "../AssetComponent";
7
11
 
8
12
  jest.mock("@applicaster/zapp-react-native-utils/reactHooks/actions", () => ({
9
13
  useActions: jest.fn(),
@@ -47,7 +51,7 @@ jest.mock(
47
51
 
48
52
  const mockUseActions = useActions as jest.Mock;
49
53
 
50
- const item = {
54
+ const entry = {
51
55
  id: "entry-1",
52
56
  } as ZappEntry;
53
57
 
@@ -71,15 +75,59 @@ const buildActionContext = (entryState = {}) => ({
71
75
  isActionAvailable: jest.fn(() => true),
72
76
  });
73
77
 
78
+ const mobileConfiguration = {
79
+ mobile_buttons_container_buttons_enabled: true,
80
+ mobile_buttons_container_position: "over_image",
81
+ mobile_buttons_container_align: "left",
82
+ mobile_buttons_container_margin_top: 0,
83
+ mobile_buttons_container_margin_right: 0,
84
+ mobile_buttons_container_margin_bottom: 0,
85
+ mobile_buttons_container_margin_left: 0,
86
+ mobile_buttons_container_stacking: "horizontal",
87
+ mobile_buttons_container_horizontal_gutter: 8,
88
+ mobile_buttons_container_vertical_gutter: 8,
89
+ mobile_buttons_container_independent_styles: true,
90
+ mobile_buttons_container_over_image_position: "top_right",
91
+ mobile_button_1_button_enabled: true,
92
+ mobile_button_1_assign_action: "navigation_action",
93
+ mobile_button_1_display_mode: "dynamic",
94
+ mobile_button_1_contents_alignment: "center",
95
+ mobile_button_1_background_color: "rgba(1,1,1,1)",
96
+ mobile_button_1_border_color: "rgba(2,2,2,1)",
97
+ mobile_button_1_border_size: 1,
98
+ mobile_button_1_corner_radius: 8,
99
+ mobile_button_1_padding_top: 10,
100
+ mobile_button_1_padding_right: 10,
101
+ mobile_button_1_padding_bottom: 10,
102
+ mobile_button_1_padding_left: 10,
103
+ mobile_button_1_asset_width: 24,
104
+ mobile_button_1_asset_height: 24,
105
+ mobile_button_1_asset_margin_top: 0,
106
+ mobile_button_1_asset_margin_right: 0,
107
+ mobile_button_1_asset_margin_bottom: 0,
108
+ mobile_button_1_asset_margin_left: 0,
109
+ mobile_button_1_asset_enabled: true,
110
+ mobile_button_1_label_enabled: true,
111
+ mobile_button_1_font_color: "rgba(10,10,10,1)",
112
+ mobile_button_1_focused_font_color: "rgba(20,20,20,1)",
113
+ mobile_button_1_font_size: 15,
114
+ mobile_button_1_line_height: 24,
115
+ mobile_button_1_ios_font_family: "Ubuntu-Bold",
116
+ mobile_button_1_android_font_family: "Ubuntu-Bold",
117
+ mobile_button_1_ios_letter_spacing: -0.2,
118
+ mobile_button_1_android_letter_spacing: -0.2,
119
+ mobile_button_1_text_transform: "default",
120
+ };
121
+
74
122
  const baseNode = {
75
- type: "PressableView",
123
+ type: "MobileActionButton",
76
124
  style: {
77
125
  backgroundColor: "rgba(1,1,1,1)",
78
126
  borderColor: "rgba(2,2,2,1)",
79
127
  borderWidth: 1,
80
128
  },
81
129
  props: {
82
- item,
130
+ entry,
83
131
  action: {
84
132
  identifier: "navigation_action",
85
133
  },
@@ -91,16 +139,16 @@ const baseNode = {
91
139
  },
92
140
  elements: [
93
141
  {
94
- type: "Image",
142
+ type: "ReactComponent",
95
143
  style: {
96
144
  width: 24,
97
145
  height: 24,
98
146
  },
99
147
  props: {
148
+ component: AssetComponent,
149
+ renderChildren: false,
150
+ requiresCellUUID: true,
100
151
  testID: "mobile-action-button-asset",
101
- source: {
102
- context: "navigation_action",
103
- },
104
152
  mobileActionRole: "asset",
105
153
  },
106
154
  },
@@ -139,7 +187,7 @@ const baseNode = {
139
187
  ],
140
188
  };
141
189
 
142
- describe("PressableView", () => {
190
+ describe("MobileActionButton", () => {
143
191
  beforeEach(() => {
144
192
  jest.clearAllMocks();
145
193
  });
@@ -186,10 +234,160 @@ describe("PressableView", () => {
186
234
  fireEvent.press(getByTestId("mobile-action-button"));
187
235
 
188
236
  expect(actionContext.invokeAction).toHaveBeenCalledWith(
189
- item,
237
+ entry,
190
238
  expect.objectContaining({
191
239
  updateState: expect.any(Function),
192
240
  })
193
241
  );
194
242
  });
243
+
244
+ it("preserves the rendered primitive tree when the mobile builder is wrapped with DataProvider", () => {
245
+ mockUseActions.mockReturnValue(buildActionContext());
246
+
247
+ const node = configInflater(
248
+ entry,
249
+ MobileActionButtons({
250
+ value: (key) => mobileConfiguration[key],
251
+ configuration: mobileConfiguration,
252
+ placement: "over_image",
253
+ })
254
+ );
255
+
256
+ const { UNSAFE_getAllByType, UNSAFE_getByType, getByTestId, getByText } =
257
+ render(
258
+ <React.Fragment>
259
+ {elementMapper(defaultComponents)(node as never)}
260
+ </React.Fragment>
261
+ );
262
+
263
+ const pressable = UNSAFE_getByType(TouchableOpacity);
264
+ const label = getByText("Play");
265
+ const asset = getByTestId("mobile_action_button_1-asset");
266
+
267
+ expect(pressable.props.testID).toBe("mobile_action_button_1");
268
+ expect(pressable.props.accessibilityLabel).toBe("entry-1");
269
+
270
+ expect(pressable.props.style).toMatchObject({
271
+ backgroundColor: "rgba(1,1,1,1)",
272
+ borderColor: "rgba(2,2,2,1)",
273
+ borderWidth: 1,
274
+ });
275
+
276
+ expect(asset.props.style).toMatchObject({
277
+ width: 24,
278
+ height: 24,
279
+ });
280
+
281
+ expect(label.props.style).toEqual(
282
+ expect.arrayContaining([
283
+ expect.objectContaining({
284
+ color: "rgba(10,10,10,1)",
285
+ fontSize: 15,
286
+ lineHeight: 24,
287
+ }),
288
+ ])
289
+ );
290
+
291
+ expect(UNSAFE_getAllByType(View).length).toBeGreaterThan(0);
292
+ expect(UNSAFE_getAllByType(RNText).length).toBeGreaterThan(0);
293
+ });
294
+
295
+ describe("component asset via mobileButtonAssets", () => {
296
+ const nodeWithFlavour1 = {
297
+ ...baseNode,
298
+ props: {
299
+ ...baseNode.props,
300
+ action: {
301
+ identifier: "navigation_action",
302
+ flavour: "flavour_1" as const,
303
+ },
304
+ },
305
+ };
306
+
307
+ const nodeWithFlavour2 = {
308
+ ...baseNode,
309
+ props: {
310
+ ...baseNode.props,
311
+ action: {
312
+ identifier: "navigation_action",
313
+ flavour: "flavour_2" as const,
314
+ },
315
+ },
316
+ };
317
+
318
+ it("renders mobileButtonAssets component instead of Image when it is a function", () => {
319
+ const MockAssetComponent = jest.fn(({ testID }) => (
320
+ <View testID={testID} accessibilityLabel="mock-asset" />
321
+ ));
322
+
323
+ mockUseActions.mockReturnValue(
324
+ buildActionContext({ mobileButtonAssets: MockAssetComponent })
325
+ );
326
+
327
+ const { getByTestId } = renderNode(nodeWithFlavour1);
328
+
329
+ expect(MockAssetComponent).toHaveBeenCalled();
330
+ expect(getByTestId("mobile-action-button-asset")).toBeTruthy();
331
+ });
332
+
333
+ it("passes flavour_1 to the mobileButtonAssets component", () => {
334
+ const MockAssetComponent = jest.fn(({ testID }) => (
335
+ <View testID={testID} />
336
+ ));
337
+
338
+ mockUseActions.mockReturnValue(
339
+ buildActionContext({ mobileButtonAssets: MockAssetComponent })
340
+ );
341
+
342
+ renderNode(nodeWithFlavour1);
343
+
344
+ expect(MockAssetComponent).toHaveBeenCalledWith(
345
+ expect.objectContaining({ flavour: "flavour_1" }),
346
+ expect.anything()
347
+ );
348
+ });
349
+
350
+ it("passes flavour_2 to the mobileButtonAssets component", () => {
351
+ const MockAssetComponent = jest.fn(({ testID }) => (
352
+ <View testID={testID} />
353
+ ));
354
+
355
+ mockUseActions.mockReturnValue(
356
+ buildActionContext({ mobileButtonAssets: MockAssetComponent })
357
+ );
358
+
359
+ renderNode(nodeWithFlavour2);
360
+
361
+ expect(MockAssetComponent).toHaveBeenCalledWith(
362
+ expect.objectContaining({ flavour: "flavour_2" }),
363
+ expect.anything()
364
+ );
365
+ });
366
+
367
+ it("does not render asset when mobileButtonAssets is absent", () => {
368
+ mockUseActions.mockReturnValue(
369
+ buildActionContext({ mobileButtonAssets: undefined })
370
+ );
371
+
372
+ const { queryByTestId } = renderNode(nodeWithFlavour1);
373
+
374
+ expect(queryByTestId("mobile-action-button-asset")).toBeNull();
375
+ });
376
+
377
+ it("renders asset when mobileButtonAssets exists even if asset is absent", () => {
378
+ mockUseActions.mockReturnValue(
379
+ buildActionContext({
380
+ asset: undefined,
381
+ mobileButtonAssets: [
382
+ "https://example.com/image-inactive.png",
383
+ "https://example.com/image-active.png",
384
+ ],
385
+ })
386
+ );
387
+
388
+ const { getByTestId } = renderNode(nodeWithFlavour1);
389
+
390
+ expect(getByTestId("mobile-action-button-asset")).toBeTruthy();
391
+ });
392
+ });
195
393
  });
@@ -1,8 +1,7 @@
1
+ import { MobileActionButtons } from "..";
1
2
  import { Asset } from "../Asset";
3
+ import { AssetComponent } from "../AssetComponent";
2
4
  import { Button } from "../Button";
3
- import { ButtonContainerView } from "../ButtonContainerView";
4
- import { Spacer } from "../Spacer";
5
- import { TextLabel } from "../TextLabel";
6
5
  import { TextLabelsContainer } from "../TextLabelsContainer";
7
6
 
8
7
  describe("mobile action button builders", () => {
@@ -48,30 +47,6 @@ describe("mobile action button builders", () => {
48
47
 
49
48
  const value = (key) => configuration[key];
50
49
 
51
- it("builds the container node with content style in additionalProps", () => {
52
- const result = ButtonContainerView({
53
- style: { position: "absolute" },
54
- contentStyle: {
55
- flexDirection: "row",
56
- },
57
- elements: [{ type: "PressableView" }],
58
- });
59
-
60
- expect(result).toEqual({
61
- type: "View",
62
- style: { position: "absolute" },
63
- elements: [
64
- {
65
- type: "View",
66
- style: {
67
- flexDirection: "row",
68
- },
69
- elements: [{ type: "PressableView" }],
70
- },
71
- ],
72
- });
73
- });
74
-
75
50
  it("builds the button node with asset and text label children", () => {
76
51
  const result = Button({
77
52
  index: 0,
@@ -81,35 +56,80 @@ describe("mobile action button builders", () => {
81
56
  spacingStyle: { marginRight: 8 },
82
57
  });
83
58
 
84
- expect(result.type).toBe("PressableView");
59
+ expect(result.type).toBe("MobileActionButton");
85
60
  expect(result.additionalProps.action.identifier).toBe("navigation_action");
86
61
 
87
62
  expect(result.elements).toEqual(
88
63
  expect.arrayContaining([
89
- expect.objectContaining({ type: "Image" }),
64
+ expect.objectContaining({ type: "ReactComponent" }),
90
65
  expect.objectContaining({ type: "View" }),
91
66
  ])
92
67
  );
93
68
  });
94
69
 
95
- it("builds the asset, label container, label and spacer nodes", () => {
70
+ it("wraps each mobile button with DataProvider", () => {
71
+ const dataProviderConfiguration = {
72
+ ...configuration,
73
+ mobile_buttons_container_buttons_enabled: true,
74
+ mobile_buttons_container_position: "over_image",
75
+ mobile_buttons_container_align: "left",
76
+ mobile_buttons_container_margin_top: 0,
77
+ mobile_buttons_container_margin_right: 12,
78
+ mobile_buttons_container_margin_bottom: 0,
79
+ mobile_buttons_container_margin_left: 6,
80
+ mobile_buttons_container_stacking: "horizontal",
81
+ mobile_buttons_container_horizontal_gutter: 8,
82
+ mobile_buttons_container_vertical_gutter: 8,
83
+ mobile_buttons_container_independent_styles: true,
84
+ mobile_buttons_container_over_image_position: "top_right",
85
+ };
86
+
87
+ const result = MobileActionButtons({
88
+ value: (key) => dataProviderConfiguration[key],
89
+ configuration: dataProviderConfiguration,
90
+ placement: "over_image",
91
+ });
92
+
93
+ // Top-level is ButtonContainerView (View)
94
+ expect(result.type).toBe("ButtonContainerView");
95
+
96
+ // Content view contains DataProvider-wrapped buttons
97
+ const dataProvider = result.elements[0];
98
+ expect(dataProvider.type).toBe("DataProvider");
99
+
100
+ expect(dataProvider).toMatchObject({
101
+ type: "DataProvider",
102
+ data: [
103
+ {
104
+ func: "identity",
105
+ args: [],
106
+ propName: "entry",
107
+ },
108
+ ],
109
+ elements: [expect.objectContaining({ type: "MobileActionButton" })],
110
+ });
111
+ });
112
+
113
+ it("builds the asset, label container, and label nodes", () => {
96
114
  expect(
97
115
  Asset({
98
- prefix: "mobile_button_1",
99
- value,
100
- actionIdentifier: "navigation_action",
116
+ style: { width: 24, height: 24 },
117
+ testID: "mobile_action_button_1",
101
118
  })
102
119
  ).toEqual(
103
120
  expect.objectContaining({
104
- type: "Image",
121
+ type: "ReactComponent",
122
+ additionalProps: expect.objectContaining({
123
+ component: AssetComponent,
124
+ }),
105
125
  })
106
126
  );
107
127
 
108
128
  expect(
109
129
  TextLabelsContainer({
110
- prefix: "mobile_button_1",
111
- value,
112
130
  actionIdentifier: "navigation_action",
131
+ testID: "mobile_action_button_1",
132
+ style: { color: "rgba(255,255,255,1)" },
113
133
  })
114
134
  ).toEqual(
115
135
  expect.objectContaining({
@@ -117,24 +137,5 @@ describe("mobile action button builders", () => {
117
137
  elements: [expect.objectContaining({ type: "Text" })],
118
138
  })
119
139
  );
120
-
121
- expect(
122
- TextLabel({
123
- prefix: "mobile_button_1",
124
- value,
125
- actionIdentifier: "navigation_action",
126
- })
127
- ).toEqual(
128
- expect.objectContaining({
129
- type: "Text",
130
- })
131
- );
132
-
133
- expect(Spacer({ enabled: true })).toEqual({
134
- type: "View",
135
- style: {
136
- flex: 1,
137
- },
138
- });
139
140
  });
140
141
  });
@@ -48,18 +48,32 @@ describe("MobileActionButtons", () => {
48
48
  });
49
49
 
50
50
  expect(result).toBeTruthy();
51
- expect(result.type).toBe("View");
51
+ expect(result.type).toBe("ButtonContainerView");
52
52
  expect(result.style.position).toBe("absolute");
53
53
  expect(result.style.top).toBe(0);
54
54
  expect(result.style.right).toBe(0);
55
- expect(result.elements).toHaveLength(1);
56
- expect(result.elements[0].type).toBe("View");
57
- expect(result.elements[0].style.flexDirection).toBe("row");
58
- expect(result.elements[0].elements[0].type).toBe("PressableView");
59
55
 
60
- expect(result.elements[0].elements[0].elements).toEqual(
56
+ // Content view
57
+ expect(result.additionalProps.contentStyle.flexDirection).toBe("row");
58
+
59
+ // First button wrapped in DataProvider
60
+ const dataProvider = result.elements[0];
61
+ expect(dataProvider.type).toBe("DataProvider");
62
+
63
+ expect(dataProvider.data).toEqual([
64
+ {
65
+ func: "identity",
66
+ args: [],
67
+ propName: "entry",
68
+ },
69
+ ]);
70
+
71
+ const button = dataProvider.elements[0];
72
+ expect(button.type).toBe("MobileActionButton");
73
+
74
+ expect(button.elements).toEqual(
61
75
  expect.arrayContaining([
62
- expect.objectContaining({ type: "Image" }),
76
+ expect.objectContaining({ type: "ReactComponent" }),
63
77
  expect.objectContaining({ type: "View" }),
64
78
  ])
65
79
  );
@@ -92,7 +106,12 @@ describe("MobileActionButtons", () => {
92
106
  });
93
107
 
94
108
  expect(result?.elements?.[0]?.elements).toHaveLength(1);
95
- expect(result?.elements?.[0]?.elements?.[0]?.type).toBe("PressableView");
109
+
110
+ expect(result?.elements?.[0]?.type).toBe("DataProvider");
111
+
112
+ expect(result?.elements?.[0]?.elements?.[0]?.type).toBe(
113
+ "MobileActionButton"
114
+ );
96
115
  });
97
116
 
98
117
  it("renders button 2 when only button 2 is enabled", () => {
@@ -100,6 +119,7 @@ describe("MobileActionButtons", () => {
100
119
  ...configuration,
101
120
  mobile_button_1_button_enabled: false,
102
121
  mobile_button_2_button_enabled: true,
122
+ mobile_button_2_asset_enabled: true,
103
123
  mobile_button_3_button_enabled: false,
104
124
  };
105
125
 
@@ -112,10 +132,14 @@ describe("MobileActionButtons", () => {
112
132
  });
113
133
 
114
134
  expect(result?.elements?.[0]?.elements).toHaveLength(1);
115
- expect(result?.elements?.[0]?.elements?.[0]?.type).toBe("PressableView");
135
+
136
+ expect(result?.elements?.[0]?.elements?.[0]?.type).toBe(
137
+ "MobileActionButton"
138
+ );
116
139
 
117
140
  expect(
118
- result?.elements?.[0]?.elements?.[0]?.additionalProps?.action?.identifier
141
+ result?.elements?.[0]?.elements?.[0]?.elements?.[0]?.additionalProps
142
+ ?.action?.identifier
119
143
  ).toBe(configurationWithSingleButton.mobile_button_2_assign_action);
120
144
  });
121
145
 
@@ -126,6 +150,7 @@ describe("MobileActionButtons", () => {
126
150
  mobile_button_2_button_enabled: false,
127
151
  mobile_button_3_button_enabled: true,
128
152
  mobile_button_3_assign_action: "local_storage_favourites_action",
153
+ mobile_button_3_asset_enabled: true,
129
154
  };
130
155
 
131
156
  const singleButtonValue = (key) => configurationWithSingleButton[key];
@@ -137,7 +162,10 @@ describe("MobileActionButtons", () => {
137
162
  });
138
163
 
139
164
  expect(result?.elements?.[0]?.elements).toHaveLength(1);
140
- expect(result?.elements?.[0]?.elements?.[0]?.type).toBe("PressableView");
165
+
166
+ expect(result?.elements?.[0]?.elements?.[0]?.type).toBe(
167
+ "MobileActionButton"
168
+ );
141
169
 
142
170
  expect(
143
171
  result?.elements?.[0]?.elements?.[0]?.additionalProps?.action?.identifier
@@ -152,6 +180,7 @@ describe("MobileActionButtons", () => {
152
180
  mobile_button_2_button_enabled: false,
153
181
  mobile_button_3_button_enabled: true,
154
182
  mobile_button_3_assign_action: "action_3",
183
+ mobile_button_3_asset_enabled: true,
155
184
  };
156
185
 
157
186
  const sparseValue = (key) => configurationWithSparseButtons[key];
@@ -162,21 +191,22 @@ describe("MobileActionButtons", () => {
162
191
  placement: "over_image",
163
192
  });
164
193
 
165
- expect(result?.elements?.[0]?.elements).toHaveLength(2);
194
+ // Content view has 2 DataProvider children (one per enabled button)
195
+ expect(result?.elements).toHaveLength(2);
166
196
 
167
197
  expect(
168
198
  result?.elements?.[0]?.elements?.[0]?.additionalProps?.action?.identifier
169
199
  ).toBe("action_1");
170
200
 
171
201
  expect(
172
- result?.elements?.[0]?.elements?.[1]?.additionalProps?.action?.identifier
202
+ result?.elements?.[1]?.elements?.[0]?.additionalProps?.action?.identifier
173
203
  ).toBe("action_3");
174
204
 
175
205
  expect(result?.elements?.[0]?.elements?.[0]?.additionalProps?.testID).toBe(
176
206
  "mobile_action_button_1"
177
207
  );
178
208
 
179
- expect(result?.elements?.[0]?.elements?.[1]?.additionalProps?.testID).toBe(
209
+ expect(result?.elements?.[1]?.elements?.[0]?.additionalProps?.testID).toBe(
180
210
  "mobile_action_button_2"
181
211
  );
182
212
  });
@@ -208,7 +238,7 @@ describe("MobileActionButtons", () => {
208
238
  placement: "over_image",
209
239
  });
210
240
 
211
- expect(overImageResult?.elements?.[0]?.style).toMatchObject({
241
+ expect(overImageResult?.additionalProps?.contentStyle).toMatchObject({
212
242
  flexDirection: "column",
213
243
  alignItems: "center",
214
244
  marginTop: 5,
@@ -217,6 +247,97 @@ describe("MobileActionButtons", () => {
217
247
  marginLeft: 8,
218
248
  });
219
249
 
220
- expect(overImageResult?.elements?.[0]?.style.alignSelf).toBeUndefined();
250
+ expect(
251
+ overImageResult?.additionalProps?.contentStyle?.alignSelf
252
+ ).toBeUndefined();
253
+ });
254
+
255
+ describe("button content visibility", () => {
256
+ const makeConfig = (overrides) => ({
257
+ ...configuration,
258
+ ...overrides,
259
+ });
260
+
261
+ it("renders only an asset when only asset is enabled", () => {
262
+ const config = makeConfig({
263
+ mobile_button_1_asset_enabled: true,
264
+ mobile_button_1_label_enabled: false,
265
+ });
266
+
267
+ const result = MobileActionButtons({
268
+ value: (key) => config[key],
269
+ configuration: config,
270
+ placement: "over_image",
271
+ });
272
+
273
+ const buttonElements =
274
+ result?.elements?.[0]?.elements?.[0]?.elements ?? [];
275
+
276
+ expect(buttonElements.some((el) => el.type === "ReactComponent")).toBe(
277
+ true
278
+ );
279
+
280
+ expect(buttonElements.some((el) => el.type === "View")).toBe(false);
281
+ });
282
+
283
+ it("renders only a label container when only label is enabled", () => {
284
+ const config = makeConfig({
285
+ mobile_button_1_asset_enabled: false,
286
+ mobile_button_1_label_enabled: true,
287
+ });
288
+
289
+ const result = MobileActionButtons({
290
+ value: (key) => config[key],
291
+ configuration: config,
292
+ placement: "over_image",
293
+ });
294
+
295
+ const buttonElements =
296
+ result?.elements?.[0]?.elements?.[0]?.elements ?? [];
297
+
298
+ expect(buttonElements.some((el) => el.type === "ReactComponent")).toBe(
299
+ false
300
+ );
301
+
302
+ expect(buttonElements.some((el) => el.type === "View")).toBe(true);
303
+ });
304
+
305
+ it("renders both asset and label container when both are enabled", () => {
306
+ const config = makeConfig({
307
+ mobile_button_1_asset_enabled: true,
308
+ mobile_button_1_label_enabled: true,
309
+ });
310
+
311
+ const result = MobileActionButtons({
312
+ value: (key) => config[key],
313
+ configuration: config,
314
+ placement: "over_image",
315
+ });
316
+
317
+ const buttonElements =
318
+ result?.elements?.[0]?.elements?.[0]?.elements ?? [];
319
+
320
+ expect(buttonElements.some((el) => el.type === "ReactComponent")).toBe(
321
+ true
322
+ );
323
+
324
+ expect(buttonElements.some((el) => el.type === "View")).toBe(true);
325
+ });
326
+
327
+ it("omits the button entirely when neither asset nor label is enabled", () => {
328
+ const config = makeConfig({
329
+ mobile_button_1_asset_enabled: false,
330
+ mobile_button_1_label_enabled: false,
331
+ });
332
+
333
+ const result = MobileActionButtons({
334
+ value: (key) => config[key],
335
+ configuration: config,
336
+ placement: "over_image",
337
+ });
338
+
339
+ // Button() returns null so compact filters the DataProvider wrapper
340
+ expect(result?.elements).toHaveLength(0);
341
+ });
221
342
  });
222
343
  });