@applicaster/zapp-react-native-ui-components 16.0.0-rc.9 → 16.0.0-rc.90

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 (144) 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 +2 -2
  7. package/Components/ComponentResolver/__tests__/componentResolver.test.js +1 -1
  8. package/Components/FocusableGroup/FocusableTvOS.tsx +11 -7
  9. package/Components/GeneralContentScreen/GeneralContentScreenHookAdapter.tsx +54 -0
  10. package/Components/GeneralContentScreen/__tests__/GeneralContentScreenHookAdapter.test.tsx +64 -0
  11. package/Components/GeneralContentScreen/__tests__/HookContentFocusGroup.web.test.tsx +91 -0
  12. package/Components/GeneralContentScreen/hookAdapter/__tests__/networkService.test.ts +74 -0
  13. package/Components/GeneralContentScreen/hookAdapter/__tests__/runInBackground.test.ts +139 -0
  14. package/Components/GeneralContentScreen/hookAdapter/__tests__/skipExpression.test.ts +294 -0
  15. package/Components/GeneralContentScreen/hookAdapter/__tests__/validationHelper.test.ts +188 -0
  16. package/Components/GeneralContentScreen/hookAdapter/logger.ts +6 -0
  17. package/Components/GeneralContentScreen/hookAdapter/networkService.ts +53 -0
  18. package/Components/GeneralContentScreen/hookAdapter/runInBackground.ts +48 -0
  19. package/Components/GeneralContentScreen/hookAdapter/skipExpression.ts +252 -0
  20. package/Components/GeneralContentScreen/hookAdapter/validationHelper.ts +90 -0
  21. package/Components/GeneralContentScreen/hookFocus/index.tsx +13 -0
  22. package/Components/GeneralContentScreen/hookFocus/index.web.tsx +69 -0
  23. package/Components/GeneralContentScreen/index.ts +2 -0
  24. package/Components/Layout/FullWidthRow.tsx +38 -0
  25. package/Components/MasterCell/DefaultComponents/ActionButton.tsx +21 -6
  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/PressableView.tsx +29 -9
  36. package/Components/MasterCell/DefaultComponents/Text/hooks/useText.ts +4 -0
  37. package/Components/MasterCell/DefaultComponents/Toggle.tsx +36 -14
  38. package/Components/MasterCell/DefaultComponents/__tests__/Toggle.test.tsx +167 -0
  39. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/ActionButton.tsx +154 -100
  40. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/AssetComponent.tsx +15 -4
  41. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/Button.ts +56 -22
  42. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/Spacer.ts +6 -4
  43. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/TextLabelsContainer.ts +3 -1
  44. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/PressableView.test.tsx +32 -21
  45. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/index.test.ts +15 -4
  46. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/index.ts +23 -16
  47. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/utils/__tests__/insertButtons.test.ts +8 -3
  48. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/utils/index.ts +2 -2
  49. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/utils/__tests__/insertButtonsBetweenLabels.test.ts +45 -13
  50. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/utils/index.ts +2 -2
  51. package/Components/MasterCell/contexts/PressedStateContext.ts +3 -0
  52. package/Components/MasterCell/dataAdapter.ts +15 -5
  53. package/Components/MasterCell/hooks/index.ts +2 -0
  54. package/Components/MasterCell/hooks/usePressedState.ts +4 -0
  55. package/Components/MasterCell/index.tsx +30 -5
  56. package/Components/MasterCell/utils/__tests__/resolveColor.test.js +169 -132
  57. package/Components/MasterCell/utils/__tests__/resolveColorForProp.test.js +92 -0
  58. package/Components/MasterCell/utils/index.ts +85 -46
  59. package/Components/ModalComponent/ActionsBottomSheet/ActionButton.tsx +56 -0
  60. package/Components/ModalComponent/ActionsBottomSheet/__tests__/openActionsBottomSheet.test.ts +38 -0
  61. package/Components/ModalComponent/ActionsBottomSheet/boundActionButton.tsx +24 -0
  62. package/Components/ModalComponent/ActionsBottomSheet/index.ts +9 -0
  63. package/Components/ModalComponent/ActionsBottomSheet/openActionsBottomSheet.ts +61 -0
  64. package/Components/ModalComponent/AudioPlayer/Components/Button.tsx +385 -0
  65. package/Components/ModalComponent/AudioPlayer/Components/Header.tsx +728 -0
  66. package/Components/ModalComponent/AudioPlayer/Components/Input.tsx +628 -0
  67. package/Components/ModalComponent/AudioPlayer/Components/Item.tsx +982 -0
  68. package/Components/ModalComponent/AudioPlayer/Components/NowPlayingHeaderSection.tsx +94 -0
  69. package/Components/ModalComponent/AudioPlayer/Components/__tests__/Item.test.tsx +113 -0
  70. package/Components/ModalComponent/AudioPlayer/Components/index.ts +9 -0
  71. package/Components/ModalComponent/AudioPlayer/utils/__tests__/mergeStyles.test.ts +248 -0
  72. package/Components/ModalComponent/AudioPlayer/utils/mergeStyles.ts +386 -0
  73. package/Components/ModalComponent/BottomSheetDragArea.tsx +33 -0
  74. package/Components/ModalComponent/BottomSheetModalContent.tsx +431 -49
  75. package/Components/ModalComponent/Button/index.tsx +5 -3
  76. package/Components/ModalComponent/Header/CloseButton.tsx +12 -0
  77. package/Components/ModalComponent/Header/__tests__/CloseButton.test.tsx +72 -0
  78. package/Components/ModalComponent/Header/index.tsx +20 -7
  79. package/Components/ModalComponent/Header/utils.ts +1 -1
  80. package/Components/ModalComponent/ModalBlocksStyleContext.tsx +10 -0
  81. package/Components/ModalComponent/SortableList.tsx +157 -0
  82. package/Components/ModalComponent/SortableList.web.tsx +34 -0
  83. package/Components/ModalComponent/TextInputContent.tsx +112 -0
  84. package/Components/ModalComponent/__tests__/BottomSheetDragArea.test.tsx +74 -0
  85. package/Components/ModalComponent/__tests__/BottomSheetModalContent.test.tsx +255 -0
  86. package/Components/ModalComponent/__tests__/SortableList.web.test.tsx +16 -0
  87. package/Components/ModalComponent/__tests__/TextInputContent.test.tsx +156 -0
  88. package/Components/ModalComponent/__tests__/showTextInput.test.ts +95 -0
  89. package/Components/ModalComponent/index.tsx +18 -9
  90. package/Components/ModalComponent/presets/__tests__/getCell.test.ts +90 -0
  91. package/Components/ModalComponent/presets/dynamicCollectionCell.tsx +78 -0
  92. package/Components/ModalComponent/presets/index.ts +27 -0
  93. package/Components/ModalComponent/presets/selectableCell.tsx +28 -0
  94. package/Components/ModalComponent/presets/standardCell.tsx +41 -0
  95. package/Components/ModalComponent/presets/types.ts +35 -0
  96. package/Components/ModalComponent/showTextInput.ts +43 -0
  97. package/Components/ModalComponent/utils.ts +82 -21
  98. package/Components/OfflineHandler/__tests__/__snapshots__/index.test.tsx.snap +4 -85
  99. package/Components/OfflineHandler/__tests__/index.test.tsx +78 -18
  100. package/Components/OfflineHandler/hooks.ts +111 -0
  101. package/Components/OfflineHandler/index.tsx +51 -73
  102. package/Components/OfflineHandler/utils/index.ts +1 -13
  103. package/Components/PlayerContainer/PlayerContainer.tsx +25 -17
  104. package/Components/PlayerContainer/ProgramInfo/index.tsx +3 -4
  105. package/Components/PlayerContainer/__tests__/PlayerContainer.test.tsx +337 -0
  106. package/Components/PreloaderWrapper/index.tsx +1 -1
  107. package/Components/River/ComponentsMap/ComponentsMap.tsx +87 -61
  108. package/Components/River/River.tsx +4 -0
  109. package/Components/River/__tests__/__snapshots__/componentsMap.test.js.snap +140 -89
  110. package/Components/River/__tests__/componentsMap.test.js +8 -3
  111. package/Components/River/__tests__/resultCallback.test.tsx +47 -0
  112. package/Components/Screen/index.tsx +8 -4
  113. package/Components/Screen/navigationHandler.ts +8 -8
  114. package/Components/ScreenFeedLoader/ScreenFeedLoader.tsx +5 -3
  115. package/Components/ScreenResolverFeedProvider/ScreenResolverFeedProvider.tsx +1 -1
  116. package/Components/ScreenRevealManager/ScreenRevealManager.ts +18 -2
  117. package/Components/ScreenRevealManager/__tests__/ScreenRevealManager.test.ts +36 -0
  118. package/Components/ScreenRevealManager/__tests__/withScreenRevealManager.test.tsx +56 -0
  119. package/Components/ScreenRevealManager/withScreenRevealManager.tsx +21 -4
  120. package/Components/TopCutoffOverlay/index.tsx +2 -2
  121. package/Components/Transitioner/Transitioner.tsx +28 -17
  122. package/Components/VideoLive/LiveImageManager.ts +1 -1
  123. package/Components/VideoModal/utils.ts +6 -1
  124. package/Components/ZappFrameworkComponents/BarView/BarView.tsx +12 -5
  125. package/Components/ZappFrameworkComponents/BarView/__tests__/BarView.test.tsx +2 -2
  126. package/Contexts/CachedDimensionsContext/__tests__/index.test.ts +154 -0
  127. package/Contexts/CachedDimensionsContext/index.ts +17 -2
  128. package/Contexts/ModalNavigationContext/__tests__/index.test.tsx +15 -0
  129. package/Contexts/ModalNavigationContext/index.tsx +2 -2
  130. package/Helpers/ComponentCellSelectionHelper/index.js +0 -6
  131. package/Helpers/index.js +0 -39
  132. package/Hooks/useEntryActionsExpander.ts +63 -0
  133. package/package.json +6 -5
  134. package/Components/OfflineHandler/NotificationView/NotificationView.lg.tsx +0 -112
  135. package/Components/OfflineHandler/NotificationView/NotificationView.samsung.tsx +0 -107
  136. package/Components/OfflineHandler/NotificationView/NotificationView.tsx +0 -153
  137. package/Components/OfflineHandler/NotificationView/__tests__/index.test.tsx +0 -66
  138. package/Components/OfflineHandler/NotificationView/utils.ts +0 -34
  139. package/Components/Screen/utils.ts +0 -16
  140. package/Helpers/Analytics/index.js +0 -95
  141. /package/Components/BackgroundImage/{BackgroundImage.tv.android.tsx → BackgroundImage.android.tv.tsx} +0 -0
  142. /package/Components/BackgroundImage/{BackgroundImage.tv.ios.tsx → BackgroundImage.ios.tv.tsx} +0 -0
  143. /package/Components/MasterCell/DefaultComponents/ButtonContainerView/{index.tv.android.tsx → index.android.tv.tsx} +0 -0
  144. /package/Decorators/ZappPipesDataConnector/__tests__/{Hero.js → Hero.tsx} +0 -0
@@ -0,0 +1,94 @@
1
+ import React from "react";
2
+ import { StyleSheet, View } from "react-native";
3
+ import {
4
+ AudioPlayerTrackItem,
5
+ VARIANT_NOW_PLAYING,
6
+ mapMenuItemToNowPlayingData,
7
+ } from "./Item";
8
+ import { AudioPlayerHeader, VARIANT_SECTION_HEADER } from "./Header";
9
+ import type { MenuItem } from "@applicaster/zapp-react-native-utils/modalState/types";
10
+ import { usePlayer } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/usePlayer";
11
+ import { usePlayerState } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/usePlayerState";
12
+
13
+ const styles = StyleSheet.create({
14
+ divider: {
15
+ height: StyleSheet.hairlineWidth,
16
+ backgroundColor: "rgba(255, 255, 255, 0.1)",
17
+ marginHorizontal: 20,
18
+ marginVertical: 8,
19
+ },
20
+ });
21
+
22
+ type NowPlayingHeaderSectionProps = {
23
+ onClear?: () => void;
24
+ nextUpTitle?: string;
25
+ nowPlayingTitle?: string;
26
+ };
27
+
28
+ export function NowPlayingHeaderSection({
29
+ onClear,
30
+ nextUpTitle = "Next Up",
31
+ nowPlayingTitle = "Now Playing",
32
+ }: NowPlayingHeaderSectionProps) {
33
+ const player = usePlayer();
34
+ const playerState = usePlayerState("queue-now-playing-section");
35
+
36
+ const renderNowPlayingTrack = () => {
37
+ if (!player?.entry) {
38
+ return null;
39
+ }
40
+
41
+ const mediaGroup = player.entry.media_group;
42
+
43
+ const imageGroup = Array.isArray(mediaGroup)
44
+ ? mediaGroup.find((g: any) => g.type === "image")
45
+ : null;
46
+
47
+ const firstImage = Array.isArray(imageGroup?.media_item)
48
+ ? imageGroup.media_item[0]?.src
49
+ : imageGroup?.media_item?.src;
50
+
51
+ const nowPlayingItem: MenuItem = {
52
+ id: String(player.entry.id ?? "now_playing"),
53
+ title: (player.entry.title as string) || String(player.entry.id),
54
+ summary: (player.entry.summary as string) ?? "",
55
+ icon: firstImage,
56
+ };
57
+
58
+ const data = mapMenuItemToNowPlayingData(
59
+ nowPlayingItem,
60
+ playerState?.isPaused,
61
+ nowPlayingTitle
62
+ );
63
+
64
+ const handlePlayPause = () => {
65
+ if (playerState?.isPaused) {
66
+ player?.play();
67
+ } else {
68
+ player?.pause();
69
+ }
70
+ };
71
+
72
+ return (
73
+ <View>
74
+ <AudioPlayerTrackItem
75
+ configuration={VARIANT_NOW_PLAYING}
76
+ data={data}
77
+ onTrailingButtonPress={handlePlayPause}
78
+ />
79
+ <View style={styles.divider} />
80
+ </View>
81
+ );
82
+ };
83
+
84
+ return (
85
+ <View>
86
+ {renderNowPlayingTrack()}
87
+ <AudioPlayerHeader
88
+ configuration={VARIANT_SECTION_HEADER}
89
+ data={{ title: nextUpTitle }}
90
+ onClear={onClear}
91
+ />
92
+ </View>
93
+ );
94
+ }
@@ -0,0 +1,113 @@
1
+ import React from "react";
2
+ import { Pressable } from "react-native";
3
+ import { fireEvent, render, screen } from "@testing-library/react-native";
4
+ import {
5
+ AudioPlayerTrackItem,
6
+ VARIANT_PLAYLIST_ITEM,
7
+ mapMenuItemToNowPlayingData,
8
+ } from "../Item";
9
+
10
+ const playlistItemData = {
11
+ textLabel1: "Queue",
12
+ textLabel2: "",
13
+ trailingButton: "multiSelect" as const,
14
+ isSelected: false,
15
+ };
16
+
17
+ describe("AudioPlayerTrackItem multiSelect checkbox", () => {
18
+ it("invokes onPress when the checkbox is pressed even if onTrailingButtonPress is omitted", () => {
19
+ const onPress = jest.fn();
20
+
21
+ render(
22
+ <AudioPlayerTrackItem
23
+ configuration={VARIANT_PLAYLIST_ITEM}
24
+ data={playlistItemData}
25
+ onPress={onPress}
26
+ />
27
+ );
28
+
29
+ const pressables = screen.UNSAFE_getAllByType(Pressable);
30
+ fireEvent.press(pressables[pressables.length - 1]);
31
+
32
+ expect(onPress).toHaveBeenCalledTimes(1);
33
+ });
34
+
35
+ it("invokes onTrailingButtonPress instead of onPress when a dedicated trailing handler is provided", () => {
36
+ const onPress = jest.fn();
37
+ const onTrailingButtonPress = jest.fn();
38
+
39
+ render(
40
+ <AudioPlayerTrackItem
41
+ configuration={VARIANT_PLAYLIST_ITEM}
42
+ data={playlistItemData}
43
+ onPress={onPress}
44
+ onTrailingButtonPress={onTrailingButtonPress}
45
+ />
46
+ );
47
+
48
+ const pressables = screen.UNSAFE_getAllByType(Pressable);
49
+ fireEvent.press(pressables[pressables.length - 1]);
50
+
51
+ expect(onTrailingButtonPress).toHaveBeenCalledTimes(1);
52
+ expect(onPress).not.toHaveBeenCalled();
53
+ });
54
+
55
+ it("invokes onPress when the row is pressed", () => {
56
+ const onPress = jest.fn();
57
+
58
+ render(
59
+ <AudioPlayerTrackItem
60
+ configuration={VARIANT_PLAYLIST_ITEM}
61
+ data={playlistItemData}
62
+ onPress={onPress}
63
+ />
64
+ );
65
+
66
+ fireEvent.press(screen.UNSAFE_getAllByType(Pressable)[0]);
67
+
68
+ expect(onPress).toHaveBeenCalledTimes(1);
69
+ });
70
+
71
+ it("renders only textLabel1 Text element when textLabel2 is missing or empty", () => {
72
+ const { UNSAFE_getAllByType, queryByText } = render(
73
+ <AudioPlayerTrackItem
74
+ configuration={VARIANT_PLAYLIST_ITEM}
75
+ data={{
76
+ textLabel1: "Only Title",
77
+ textLabel2: "",
78
+ }}
79
+ />
80
+ );
81
+
82
+ const { Text } = require("react-native");
83
+ const textNodes = UNSAFE_getAllByType(Text);
84
+ expect(textNodes).toHaveLength(1);
85
+ expect(queryByText("Only Title")).toBeTruthy();
86
+ });
87
+ });
88
+
89
+ describe("mapMenuItemToNowPlayingData", () => {
90
+ const item = {
91
+ id: "item_1",
92
+ title: "Track Title",
93
+ summary: "Artist Name",
94
+ icon: "https://example.com/cover.png",
95
+ };
96
+
97
+ it("uses default 'Now Playing' label when omitted", () => {
98
+ const data = mapMenuItemToNowPlayingData(item as any);
99
+ expect(data.nowPlayingLabel).toBe("Now Playing");
100
+ expect(data.textLabel1).toBe("Track Title");
101
+ expect(data.textLabel2).toBe("Artist Name");
102
+ });
103
+
104
+ it("uses custom nowPlayingLabel when provided", () => {
105
+ const data = mapMenuItemToNowPlayingData(
106
+ item as any,
107
+ false,
108
+ "En cours de lecture"
109
+ );
110
+
111
+ expect(data.nowPlayingLabel).toBe("En cours de lecture");
112
+ });
113
+ });
@@ -0,0 +1,9 @@
1
+ export { AudioPlayerButton, VARIANT_WITH_ICON } from "./Button";
2
+
3
+ export { AudioPlayerHeader } from "./Header";
4
+
5
+ export { AudioPlayerTrackItem } from "./Item";
6
+
7
+ export { NowPlayingHeaderSection } from "./NowPlayingHeaderSection";
8
+
9
+ export { AudioPlayerInput } from "./Input";
@@ -0,0 +1,248 @@
1
+ import {
2
+ mergeItemConfiguration,
3
+ mergeButtonConfiguration,
4
+ } from "../mergeStyles";
5
+
6
+ const base = {
7
+ background: {
8
+ defaultColor: "transparent",
9
+ focusedColor: "rgba(62, 62, 62, 1)",
10
+ cornerRadius: 12,
11
+ },
12
+ image: {
13
+ width: 48,
14
+ height: 48,
15
+ borderWidth: 1,
16
+ borderColor: "rgba(255, 255, 255, 0.1)",
17
+ cornerRadius: 6,
18
+ },
19
+ nowPlayingAsset: {},
20
+ nowPlayingLabel: { fontColor: "#fff", fontSize: 12 },
21
+ textLabel1: { fontColor: "#fff", fontSize: 15 },
22
+ textLabel2: { fontColor: "#999", fontSize: 13 },
23
+ };
24
+
25
+ describe("mergeItemConfiguration background", () => {
26
+ it("applies Studio focus background to focusedColor", () => {
27
+ const merged = mergeItemConfiguration(base as any, {
28
+ background: {
29
+ default: { backgroundColor: "transparent" },
30
+ focused: { backgroundColor: "#FF3B30" },
31
+ },
32
+ });
33
+
34
+ expect(merged.background.defaultColor).toBe("transparent");
35
+ expect(merged.background.focusedColor).toBe("#FF3B30");
36
+ });
37
+
38
+ it("accepts focus alias bucket from parser output", () => {
39
+ const merged = mergeItemConfiguration(base as any, {
40
+ background: {
41
+ default: { backgroundColor: "transparent" },
42
+ focus: { backgroundColor: "#00FF66" },
43
+ },
44
+ });
45
+
46
+ expect(merged.background.focusedColor).toBe("#00FF66");
47
+ });
48
+
49
+ it("merges text label lineHeight from Studio keys", () => {
50
+ const merged = mergeItemConfiguration(base as any, {
51
+ textLabel1: {
52
+ default: { fontSize: 28, lineHeight: 40 },
53
+ },
54
+ });
55
+
56
+ expect(merged.textLabel1.fontSize).toBe(28);
57
+ expect(merged.textLabel1.lineHeight).toBe(40);
58
+ });
59
+
60
+ it("merges background padding from Studio keys", () => {
61
+ const merged = mergeItemConfiguration(base as any, {
62
+ background: {
63
+ default: {
64
+ backgroundColor: "#2E7D32",
65
+ paddingTop: 32,
66
+ paddingBottom: 32,
67
+ paddingLeft: 40,
68
+ paddingRight: 40,
69
+ },
70
+ },
71
+ });
72
+
73
+ expect(merged.background.paddingTop).toBe(32);
74
+ expect(merged.background.paddingBottom).toBe(32);
75
+ expect(merged.background.paddingLeft).toBe(40);
76
+ expect(merged.background.paddingRight).toBe(40);
77
+ // Default-only override must not steal the base focused highlight color.
78
+ expect(merged.background.focusedColor).toBe("rgba(62, 62, 62, 1)");
79
+ });
80
+
81
+ it("preserves base focusedColor when only default background is set", () => {
82
+ const merged = mergeItemConfiguration(base as any, {
83
+ background: {
84
+ default: { backgroundColor: "#2E7D32" },
85
+ },
86
+ });
87
+
88
+ expect(merged.background.defaultColor).toBe("#2E7D32");
89
+ expect(merged.background.focusedColor).toBe("rgba(62, 62, 62, 1)");
90
+ });
91
+
92
+ it("applies Studio checkbox assets onto checkboxAssets", () => {
93
+ const merged = mergeItemConfiguration(base as any, {
94
+ checkbox: {
95
+ default: {
96
+ asset: "https://placehold.co/24x24/FF3B30/FFFFFF/png?text=CB",
97
+ checkedAsset: "https://placehold.co/24x24/FFD60A/000000/png?text=CK",
98
+ },
99
+ },
100
+ });
101
+
102
+ expect(merged.checkboxAssets?.asset).toBe(
103
+ "https://placehold.co/24x24/FF3B30/FFFFFF/png?text=CB"
104
+ );
105
+
106
+ expect(merged.checkboxAssets?.checkedAsset).toBe(
107
+ "https://placehold.co/24x24/FFD60A/000000/png?text=CK"
108
+ );
109
+ });
110
+
111
+ it("applies Studio reorder/remove assets onto actionButtonAssets", () => {
112
+ const merged = mergeItemConfiguration(base as any, {
113
+ actionButtons: {
114
+ default: {
115
+ removeAsset: "https://placehold.co/24x24/00FF00/000000/png?text=RM",
116
+ dragAsset: "https://placehold.co/24x24/FF3B30/FFFFFF/png?text=RH",
117
+ },
118
+ },
119
+ });
120
+
121
+ expect(merged.actionButtonAssets?.removeAsset).toBe(
122
+ "https://placehold.co/24x24/00FF00/000000/png?text=RM"
123
+ );
124
+
125
+ expect(merged.actionButtonAssets?.dragAsset).toBe(
126
+ "https://placehold.co/24x24/FF3B30/FFFFFF/png?text=RH"
127
+ );
128
+ });
129
+ });
130
+
131
+ describe("mergeButtonConfiguration background", () => {
132
+ const buttonBase = {
133
+ background: {
134
+ defaultColor: "#FFFFFF",
135
+ focusedColor: "#E0E0E0",
136
+ inactiveColor: "#CCCCCC",
137
+ cornerRadius: 12,
138
+ paddingTop: 12,
139
+ paddingRight: 12,
140
+ paddingBottom: 12,
141
+ paddingLeft: 12,
142
+ horizontalGutter: 8,
143
+ },
144
+ title: {
145
+ fontColor: "#000000",
146
+ focusedFontColor: "#000000",
147
+ inactiveFontColor: "#666666",
148
+ iosFontFamily: "SFProText-Semibold",
149
+ androidFontFamily: "Roboto-Medium",
150
+ fontSize: 15,
151
+ lineHeight: 24,
152
+ iosLetterSpacing: 0,
153
+ androidLetterSpacing: 0,
154
+ textTransform: "default" as const,
155
+ },
156
+ icons: {
157
+ width: 24,
158
+ height: 24,
159
+ leadingIcon: { enabled: true },
160
+ },
161
+ };
162
+
163
+ it("applies Studio button focus background to focusedColor", () => {
164
+ const merged = mergeButtonConfiguration(buttonBase as any, {
165
+ background: {
166
+ default: { backgroundColor: "#FFFFFF" },
167
+ focused: { backgroundColor: "#00FF00" },
168
+ },
169
+ });
170
+
171
+ expect(merged.background.defaultColor).toBe("#FFFFFF");
172
+ expect(merged.background.focusedColor).toBe("#00FF00");
173
+ });
174
+
175
+ it("applies Studio button title lineHeight", () => {
176
+ const merged = mergeButtonConfiguration(buttonBase as any, {
177
+ title: {
178
+ default: { fontColor: "#000000", fontSize: 15, lineHeight: 40 },
179
+ },
180
+ });
181
+
182
+ expect(merged.title.lineHeight).toBe(40);
183
+ expect(merged.title.fontSize).toBe(15);
184
+ });
185
+
186
+ it("applies Studio button background padding", () => {
187
+ const merged = mergeButtonConfiguration(buttonBase as any, {
188
+ background: {
189
+ default: {
190
+ backgroundColor: "#FFFFFF",
191
+ paddingTop: 32,
192
+ paddingRight: 20,
193
+ paddingBottom: 8,
194
+ paddingLeft: 4,
195
+ },
196
+ },
197
+ });
198
+
199
+ expect(merged.background.paddingTop).toBe(32);
200
+ expect(merged.background.paddingRight).toBe(20);
201
+ expect(merged.background.paddingBottom).toBe(8);
202
+ expect(merged.background.paddingLeft).toBe(4);
203
+ expect(merged.background.focusedColor).toBe("#E0E0E0");
204
+ });
205
+
206
+ it("preserves base button focusedColor when only default background is set", () => {
207
+ const merged = mergeButtonConfiguration(buttonBase as any, {
208
+ background: {
209
+ default: { backgroundColor: "#ABCDEF" },
210
+ },
211
+ });
212
+
213
+ expect(merged.background.defaultColor).toBe("#ABCDEF");
214
+ expect(merged.background.focusedColor).toBe("#E0E0E0");
215
+ });
216
+
217
+ it("applies Studio create-new leading icon asset", () => {
218
+ const merged = mergeButtonConfiguration(buttonBase as any, {
219
+ leadingIcon: {
220
+ default: {
221
+ asset: "https://placehold.co/24x24/FF00FF/FFFFFF/png?text=CN",
222
+ },
223
+ },
224
+ });
225
+
226
+ expect(merged.icons.leadingIcon.asset).toBe(
227
+ "https://placehold.co/24x24/FF00FF/FFFFFF/png?text=CN"
228
+ );
229
+ });
230
+
231
+ it("applies Studio button title typography (fontFamily, letterSpacing, textTransform)", () => {
232
+ const merged = mergeButtonConfiguration(buttonBase as any, {
233
+ title: {
234
+ default: {
235
+ fontFamily: "CustomFont",
236
+ letterSpacing: -0.5,
237
+ textTransform: "uppercase",
238
+ },
239
+ },
240
+ });
241
+
242
+ expect(merged.title.iosFontFamily).toBe("CustomFont");
243
+ expect(merged.title.androidFontFamily).toBe("CustomFont");
244
+ expect(merged.title.iosLetterSpacing).toBe(-0.5);
245
+ expect(merged.title.androidLetterSpacing).toBe(-0.5);
246
+ expect(merged.title.textTransform).toBe("uppercase");
247
+ });
248
+ });