@applicaster/zapp-react-native-ui-components 16.0.0-rc.57 → 16.0.0-rc.58
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.
- package/Components/ModalComponent/AudioPlayer/Components/Action.tsx +0 -18
- package/Components/ModalComponent/AudioPlayer/Components/Button.tsx +17 -24
- package/Components/ModalComponent/AudioPlayer/Components/Header.tsx +25 -73
- package/Components/ModalComponent/AudioPlayer/Components/Input.tsx +0 -18
- package/Components/ModalComponent/AudioPlayer/Components/Item.tsx +75 -79
- package/Components/ModalComponent/AudioPlayer/Components/NowPlayingHeaderSection.tsx +89 -0
- package/Components/ModalComponent/AudioPlayer/Components/index.ts +3 -1
- package/Components/ModalComponent/AudioPlayer/utils/mergeStyles.ts +199 -0
- package/Components/ModalComponent/BottomSheetModalContent.tsx +382 -61
- package/Components/ModalComponent/Button/index.tsx +5 -3
- package/Components/ModalComponent/Header/index.tsx +20 -7
- package/Components/ModalComponent/Header/utils.ts +1 -1
- package/Components/ModalComponent/ModalBlocksStyleContext.tsx +10 -0
- package/Components/ModalComponent/SortableList.tsx +142 -0
- package/Components/ModalComponent/TextInputContent.tsx +100 -0
- package/Components/ModalComponent/__tests__/BottomSheetModalContent.test.tsx +238 -0
- package/Components/ModalComponent/__tests__/TextInputContent.test.tsx +120 -0
- package/Components/ModalComponent/__tests__/showTextInput.test.ts +95 -0
- package/Components/ModalComponent/index.tsx +11 -0
- package/Components/ModalComponent/presets/__tests__/getCell.test.ts +66 -0
- package/Components/ModalComponent/presets/dynamicCollectionCell.tsx +78 -0
- package/Components/ModalComponent/presets/index.ts +27 -0
- package/Components/ModalComponent/presets/selectableCell.tsx +28 -0
- package/Components/ModalComponent/presets/standardCell.tsx +41 -0
- package/Components/ModalComponent/presets/types.ts +35 -0
- package/Components/ModalComponent/showTextInput.ts +43 -0
- package/Components/ModalComponent/utils.ts +76 -20
- package/package.json +6 -5
- /package/Decorators/ZappPipesDataConnector/__tests__/{Hero.js → Hero.tsx} +0 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useState } from "react";
|
|
2
|
+
import Sortable from "react-native-sortables";
|
|
3
|
+
import { Text, View, ViewStyle } from "react-native";
|
|
4
|
+
import Animated, { useAnimatedRef } from "react-native-reanimated";
|
|
5
|
+
|
|
6
|
+
const DEFAULT_HEADERS: any[] = [];
|
|
7
|
+
const DEFAULT_SORTABLE_DATA: any[] = [];
|
|
8
|
+
|
|
9
|
+
const renderHeaderComponent = (component: any) => {
|
|
10
|
+
if (!component) return null;
|
|
11
|
+
if (React.isValidElement(component)) return component;
|
|
12
|
+
|
|
13
|
+
if (typeof component === "function") {
|
|
14
|
+
const HeaderComponent = component;
|
|
15
|
+
|
|
16
|
+
return <HeaderComponent />;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return null;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const SortableListHeader = ({
|
|
23
|
+
title,
|
|
24
|
+
style,
|
|
25
|
+
}: {
|
|
26
|
+
title?: string;
|
|
27
|
+
style?: ViewStyle;
|
|
28
|
+
}) => {
|
|
29
|
+
const displayTitle = typeof title === "string" ? title : String(title ?? "");
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<View style={style}>
|
|
33
|
+
{displayTitle ? <Text>{displayTitle}</Text> : null}
|
|
34
|
+
</View>
|
|
35
|
+
);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export type SortableListProps = {
|
|
39
|
+
scrollViewStyle?: ViewStyle;
|
|
40
|
+
contentContainerStyle?: ViewStyle;
|
|
41
|
+
itemStyle?: ViewStyle;
|
|
42
|
+
headerStyle?: ViewStyle;
|
|
43
|
+
columns?: number;
|
|
44
|
+
rowGap?: number;
|
|
45
|
+
columnGap?: number;
|
|
46
|
+
spacing?: number;
|
|
47
|
+
keyExtractor?: (item: any, index?: number) => string;
|
|
48
|
+
renderItem: ({
|
|
49
|
+
item,
|
|
50
|
+
index,
|
|
51
|
+
renderHandle,
|
|
52
|
+
}: {
|
|
53
|
+
item: any;
|
|
54
|
+
index: number;
|
|
55
|
+
renderHandle: (children: React.ReactElement) => React.ReactElement;
|
|
56
|
+
}) => React.ReactNode;
|
|
57
|
+
sortableData?: any[];
|
|
58
|
+
headers?: { index: number; title?: string; component: any }[];
|
|
59
|
+
onDragEnd?: (params: {
|
|
60
|
+
fromIndex?: number;
|
|
61
|
+
toIndex?: number;
|
|
62
|
+
from?: number;
|
|
63
|
+
to?: number;
|
|
64
|
+
data: any[];
|
|
65
|
+
}) => void;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export const SortableList = ({
|
|
69
|
+
scrollViewStyle,
|
|
70
|
+
contentContainerStyle,
|
|
71
|
+
itemStyle,
|
|
72
|
+
headerStyle,
|
|
73
|
+
columns = 1,
|
|
74
|
+
rowGap = 0,
|
|
75
|
+
columnGap = 0,
|
|
76
|
+
keyExtractor = (item, index) =>
|
|
77
|
+
`sortable-item-${item?.id ?? item?.title ?? index}`,
|
|
78
|
+
renderItem,
|
|
79
|
+
sortableData = DEFAULT_SORTABLE_DATA,
|
|
80
|
+
headers = DEFAULT_HEADERS,
|
|
81
|
+
onDragEnd,
|
|
82
|
+
}: SortableListProps) => {
|
|
83
|
+
const scrollableRef = useAnimatedRef<Animated.ScrollView>();
|
|
84
|
+
|
|
85
|
+
const [localData, setLocalData] = useState(sortableData);
|
|
86
|
+
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
setLocalData(sortableData);
|
|
89
|
+
}, [sortableData]);
|
|
90
|
+
|
|
91
|
+
const handleDragEnd = useCallback(
|
|
92
|
+
(params: any) => {
|
|
93
|
+
if (params?.data) {
|
|
94
|
+
setLocalData(params.data);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (onDragEnd) {
|
|
98
|
+
onDragEnd(params);
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
[onDragEnd]
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<Animated.ScrollView
|
|
106
|
+
ref={scrollableRef}
|
|
107
|
+
style={scrollViewStyle}
|
|
108
|
+
contentContainerStyle={contentContainerStyle}
|
|
109
|
+
>
|
|
110
|
+
{(headers || []).map((header, idx) => (
|
|
111
|
+
<View key={`static-header-${idx}`} style={headerStyle}>
|
|
112
|
+
{header.title ? (
|
|
113
|
+
<SortableListHeader title={header.title} />
|
|
114
|
+
) : (
|
|
115
|
+
renderHeaderComponent(header.component)
|
|
116
|
+
)}
|
|
117
|
+
</View>
|
|
118
|
+
))}
|
|
119
|
+
<Sortable.Grid
|
|
120
|
+
scrollableRef={scrollableRef}
|
|
121
|
+
customHandle
|
|
122
|
+
columns={columns}
|
|
123
|
+
rowGap={rowGap}
|
|
124
|
+
columnGap={columnGap}
|
|
125
|
+
data={localData}
|
|
126
|
+
keyExtractor={keyExtractor}
|
|
127
|
+
onDragEnd={handleDragEnd}
|
|
128
|
+
renderItem={({ item, index }) => (
|
|
129
|
+
<View key={keyExtractor(item, index)} style={itemStyle}>
|
|
130
|
+
{renderItem({
|
|
131
|
+
item,
|
|
132
|
+
index,
|
|
133
|
+
renderHandle: (children) => (
|
|
134
|
+
<Sortable.Handle>{children}</Sortable.Handle>
|
|
135
|
+
),
|
|
136
|
+
})}
|
|
137
|
+
</View>
|
|
138
|
+
)}
|
|
139
|
+
/>
|
|
140
|
+
</Animated.ScrollView>
|
|
141
|
+
);
|
|
142
|
+
};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import React, { useCallback, useState } from "react";
|
|
2
|
+
import { StyleSheet, View } from "react-native";
|
|
3
|
+
import { actionExecutor } from "@applicaster/zapp-react-native-utils/actionsExecutor/ActionExecutor";
|
|
4
|
+
import {
|
|
5
|
+
AudioPlayerInput,
|
|
6
|
+
VARIANT_NAME_PLAYLIST_INPUT,
|
|
7
|
+
} from "./AudioPlayer/Components/Input";
|
|
8
|
+
import {
|
|
9
|
+
AudioPlayerButton,
|
|
10
|
+
VARIANT_TEXT_ONLY,
|
|
11
|
+
} from "./AudioPlayer/Components/Button";
|
|
12
|
+
|
|
13
|
+
export type TextInputContentProps = {
|
|
14
|
+
/** Injected by the sheet host; unused for layout (header is owned by ModalComponent). */
|
|
15
|
+
dismiss?: () => void;
|
|
16
|
+
width?: number;
|
|
17
|
+
maxHeight?: number;
|
|
18
|
+
currentRoute?: boolean;
|
|
19
|
+
inputLabel?: string;
|
|
20
|
+
defaultValue?: string;
|
|
21
|
+
buttonLabel: string;
|
|
22
|
+
actions: ActionType[];
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const styles = StyleSheet.create({
|
|
26
|
+
container: {
|
|
27
|
+
paddingHorizontal: 20,
|
|
28
|
+
paddingBottom: 8,
|
|
29
|
+
},
|
|
30
|
+
inputWrap: {
|
|
31
|
+
marginTop: 8,
|
|
32
|
+
marginBottom: 24,
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Modal body only — input + submit.
|
|
38
|
+
* Standard dismiss header comes from BottomSheetModalContent / ModalHeader.
|
|
39
|
+
*/
|
|
40
|
+
export function TextInputContent(props: TextInputContentProps) {
|
|
41
|
+
const { inputLabel, defaultValue = "", buttonLabel, actions } = props;
|
|
42
|
+
|
|
43
|
+
const [textValue, setTextValue] = useState(defaultValue);
|
|
44
|
+
const [submitting, setSubmitting] = useState(false);
|
|
45
|
+
|
|
46
|
+
const onSubmit = useCallback(async () => {
|
|
47
|
+
if (submitting) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
setSubmitting(true);
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const updatedActions = (actions || []).map((item) => {
|
|
55
|
+
if (item.type === "sendCloudEvent" || item.options?.data) {
|
|
56
|
+
return {
|
|
57
|
+
...item,
|
|
58
|
+
options: {
|
|
59
|
+
...item.options,
|
|
60
|
+
data: {
|
|
61
|
+
...(item.options?.data || {}),
|
|
62
|
+
name: textValue,
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return item;
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
await actionExecutor.handleActions(updatedActions);
|
|
72
|
+
await actionExecutor.handleAction({ type: "dismissBottomSheet" });
|
|
73
|
+
} finally {
|
|
74
|
+
setSubmitting(false);
|
|
75
|
+
}
|
|
76
|
+
}, [submitting, actions, textValue]);
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<View style={styles.container}>
|
|
80
|
+
<View style={styles.inputWrap}>
|
|
81
|
+
<AudioPlayerInput
|
|
82
|
+
configuration={VARIANT_NAME_PLAYLIST_INPUT}
|
|
83
|
+
data={{
|
|
84
|
+
placeholder: inputLabel || "",
|
|
85
|
+
label: inputLabel,
|
|
86
|
+
}}
|
|
87
|
+
value={textValue}
|
|
88
|
+
onChangeText={setTextValue}
|
|
89
|
+
onClearPress={() => setTextValue("")}
|
|
90
|
+
/>
|
|
91
|
+
</View>
|
|
92
|
+
<AudioPlayerButton
|
|
93
|
+
configuration={VARIANT_TEXT_ONLY}
|
|
94
|
+
data={{ title: buttonLabel }}
|
|
95
|
+
onPress={onSubmit}
|
|
96
|
+
disabled={submitting}
|
|
97
|
+
/>
|
|
98
|
+
</View>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { render, screen } from "@testing-library/react-native";
|
|
3
|
+
import { BottomSheetModalContent } from "../BottomSheetModalContent";
|
|
4
|
+
import { ContentViewModel } from "@applicaster/zapp-react-native-utils/modalState";
|
|
5
|
+
|
|
6
|
+
jest.mock("react-native-sortables", () => {
|
|
7
|
+
const { View } = require("react-native");
|
|
8
|
+
|
|
9
|
+
const Grid = ({ data, renderItem }: any) => (
|
|
10
|
+
<View testID="sortable-grid">
|
|
11
|
+
{(data || []).map((item: any, index: number) =>
|
|
12
|
+
renderItem({
|
|
13
|
+
item,
|
|
14
|
+
index,
|
|
15
|
+
renderHandle: (children: any) => (
|
|
16
|
+
<View testID={`sortable-handle-${index}`}>{children}</View>
|
|
17
|
+
),
|
|
18
|
+
})
|
|
19
|
+
)}
|
|
20
|
+
</View>
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
const Handle = ({ children }: any) => (
|
|
24
|
+
<View testID="sortable-handle-wrapper">{children}</View>
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
__esModule: true,
|
|
29
|
+
default: {
|
|
30
|
+
Grid,
|
|
31
|
+
Handle,
|
|
32
|
+
},
|
|
33
|
+
Grid,
|
|
34
|
+
Handle,
|
|
35
|
+
};
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
jest.mock("@applicaster/zapp-react-native-utils/theme", () => ({
|
|
39
|
+
useTheme: () => ({
|
|
40
|
+
modal_bottom_sheet_padding_top: 0,
|
|
41
|
+
modal_bottom_sheet_padding_bottom: 0,
|
|
42
|
+
modal_bottom_sheet_background_color: "#000",
|
|
43
|
+
modal_bottom_sheet_item_selected_icon: null,
|
|
44
|
+
}),
|
|
45
|
+
}));
|
|
46
|
+
|
|
47
|
+
jest.mock("@applicaster/zapp-react-native-redux/hooks", () => ({
|
|
48
|
+
usePlugins: () => [],
|
|
49
|
+
}));
|
|
50
|
+
|
|
51
|
+
jest.mock("@applicaster/zapp-react-native-utils/localizationUtils", () => ({
|
|
52
|
+
useLocalizedStrings: () => ({}),
|
|
53
|
+
}));
|
|
54
|
+
|
|
55
|
+
jest.mock("react-native-safe-area-context", () => ({
|
|
56
|
+
useSafeAreaInsets: () => ({ top: 0, bottom: 0, left: 0, right: 0 }),
|
|
57
|
+
}));
|
|
58
|
+
|
|
59
|
+
jest.mock("../Header", () => ({
|
|
60
|
+
ModalHeader: ({ title }: any) => {
|
|
61
|
+
const { Text } = require("react-native");
|
|
62
|
+
|
|
63
|
+
return <Text testID="default-header">{title}</Text>;
|
|
64
|
+
},
|
|
65
|
+
}));
|
|
66
|
+
|
|
67
|
+
jest.mock("../Button", () => ({
|
|
68
|
+
Button: ({ label }: any) => {
|
|
69
|
+
const { Text } = require("react-native");
|
|
70
|
+
|
|
71
|
+
return <Text testID="generic-button">{label}</Text>;
|
|
72
|
+
},
|
|
73
|
+
}));
|
|
74
|
+
|
|
75
|
+
jest.mock("../AudioPlayer/Components/Item", () => ({
|
|
76
|
+
AudioPlayerTrackItem: ({ data }: any) => {
|
|
77
|
+
const { Text } = require("react-native");
|
|
78
|
+
|
|
79
|
+
return <Text testID="selectable-row">{data.textLabel1}</Text>;
|
|
80
|
+
},
|
|
81
|
+
mapMenuItemToPlaylistData: (item: any) => ({ textLabel1: item.title }),
|
|
82
|
+
VARIANT_PLAYLIST_ITEM: {},
|
|
83
|
+
}));
|
|
84
|
+
|
|
85
|
+
jest.mock("../AudioPlayer/Components", () => ({
|
|
86
|
+
NowPlayingHeaderSection: () => null,
|
|
87
|
+
AudioPlayerButton: ({ data, onPress }: any) => {
|
|
88
|
+
const { Text } = require("react-native");
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<Text testID="create-playlist-button" onPress={onPress}>
|
|
92
|
+
{data.title}
|
|
93
|
+
</Text>
|
|
94
|
+
);
|
|
95
|
+
},
|
|
96
|
+
VARIANT_WITH_ICON: {},
|
|
97
|
+
}));
|
|
98
|
+
|
|
99
|
+
const baseProps = {
|
|
100
|
+
items: [{ id: "track-a", title: "Track A", label: "Track A" }],
|
|
101
|
+
onPress: jest.fn(),
|
|
102
|
+
width: 320,
|
|
103
|
+
currentRoute: true,
|
|
104
|
+
dismiss: jest.fn(),
|
|
105
|
+
title: "Sheet Title",
|
|
106
|
+
maxHeight: 400,
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
describe("BottomSheetModalContent role/behavior cells", () => {
|
|
110
|
+
it("renders standard Button rows and default header when no viewModel/role", () => {
|
|
111
|
+
render(<BottomSheetModalContent {...baseProps} />);
|
|
112
|
+
|
|
113
|
+
expect(screen.getByTestId("default-header")).toBeTruthy();
|
|
114
|
+
expect(screen.getByTestId("generic-button")).toBeTruthy();
|
|
115
|
+
expect(screen.queryByTestId("selectable-row")).toBeNull();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("renders selectable rows when viewModel has collection_selector role", () => {
|
|
119
|
+
const viewModel = new ContentViewModel({
|
|
120
|
+
title: "Playlists",
|
|
121
|
+
items: [{ id: "fav-1", title: "Favorites" }],
|
|
122
|
+
role: "collection_selector",
|
|
123
|
+
behavior: { selectMode: "multi", currentSelection: [] },
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
render(<BottomSheetModalContent {...baseProps} viewModel={viewModel} />);
|
|
127
|
+
|
|
128
|
+
expect(screen.getByTestId("default-header")).toBeTruthy();
|
|
129
|
+
expect(screen.getByTestId("selectable-row")).toBeTruthy();
|
|
130
|
+
expect(screen.queryByTestId("generic-button")).toBeNull();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("does not accept styleVariant as a rendering input", () => {
|
|
134
|
+
render(
|
|
135
|
+
<BottomSheetModalContent
|
|
136
|
+
{...baseProps}
|
|
137
|
+
{...({ styleVariant: "audio_player" } as any)}
|
|
138
|
+
/>
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
expect(screen.getByTestId("generic-button")).toBeTruthy();
|
|
142
|
+
expect(screen.queryByTestId("selectable-row")).toBeNull();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("renders SortableList when viewModel has dynamic_collection role with reorder operation", () => {
|
|
146
|
+
const moveMock = jest.fn();
|
|
147
|
+
const { BehaviorSubject } = require("rxjs");
|
|
148
|
+
|
|
149
|
+
const viewModel = new ContentViewModel({
|
|
150
|
+
title: "Queue",
|
|
151
|
+
items: [
|
|
152
|
+
{ id: "t1", title: "Track 1" },
|
|
153
|
+
{ id: "t2", title: "Track 2" },
|
|
154
|
+
],
|
|
155
|
+
role: "dynamic_collection",
|
|
156
|
+
dynamicCollection: { operations: ["remove", "reorder"] },
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
viewModel.editableCollection = {
|
|
160
|
+
items$: new BehaviorSubject([
|
|
161
|
+
{ id: "t1", title: "Track 1" },
|
|
162
|
+
{ id: "t2", title: "Track 2" },
|
|
163
|
+
]),
|
|
164
|
+
operations: ["remove", "reorder"],
|
|
165
|
+
move: moveMock,
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
render(<BottomSheetModalContent {...baseProps} viewModel={viewModel} />);
|
|
169
|
+
|
|
170
|
+
expect(screen.getByTestId("sortable-grid")).toBeTruthy();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("renders create playlist button when operations include add and calls editableCollection.add on press", () => {
|
|
174
|
+
const addMock = jest.fn();
|
|
175
|
+
const { BehaviorSubject } = require("rxjs");
|
|
176
|
+
|
|
177
|
+
const viewModel = new ContentViewModel({
|
|
178
|
+
title: "Playlists",
|
|
179
|
+
items: [{ id: "p1", title: "Playlist 1" }],
|
|
180
|
+
role: "collection_selector",
|
|
181
|
+
dynamicCollection: { operations: ["add"] },
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
viewModel.editableCollection = {
|
|
185
|
+
items$: new BehaviorSubject([{ id: "p1", title: "Playlist 1" }]),
|
|
186
|
+
operations: ["add"],
|
|
187
|
+
add: addMock,
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
render(<BottomSheetModalContent {...baseProps} viewModel={viewModel} />);
|
|
191
|
+
|
|
192
|
+
const createBtn = screen.getByTestId("create-playlist-button");
|
|
193
|
+
expect(createBtn).toBeTruthy();
|
|
194
|
+
|
|
195
|
+
const { fireEvent } = require("@testing-library/react-native");
|
|
196
|
+
fireEvent.press(createBtn);
|
|
197
|
+
|
|
198
|
+
expect(addMock).toHaveBeenCalledWith({
|
|
199
|
+
id: "create_new_playlist",
|
|
200
|
+
title: "Create New Playlist",
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("honors disableScrollViewWrap in contentComponentProps to bypass ScrollView wrapping", () => {
|
|
205
|
+
const CustomComponent = () => (
|
|
206
|
+
<React.Fragment>Custom Content</React.Fragment>
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
const { UNSAFE_getByType } = render(
|
|
210
|
+
<BottomSheetModalContent
|
|
211
|
+
{...baseProps}
|
|
212
|
+
contentComponent={CustomComponent}
|
|
213
|
+
contentComponentProps={{ disableScrollViewWrap: true }}
|
|
214
|
+
/>
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
const { ScrollView } = require("react-native");
|
|
218
|
+
expect(() => UNSAFE_getByType(ScrollView)).toThrow();
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("honors static disableScrollViewWrap property on ContentComponent", () => {
|
|
222
|
+
const CustomSelfScrollingComponent: any = () => (
|
|
223
|
+
<React.Fragment>Self Scrolling</React.Fragment>
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
CustomSelfScrollingComponent.disableScrollViewWrap = true;
|
|
227
|
+
|
|
228
|
+
const { UNSAFE_getByType } = render(
|
|
229
|
+
<BottomSheetModalContent
|
|
230
|
+
{...baseProps}
|
|
231
|
+
contentComponent={CustomSelfScrollingComponent}
|
|
232
|
+
/>
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
const { ScrollView } = require("react-native");
|
|
236
|
+
expect(() => UNSAFE_getByType(ScrollView)).toThrow();
|
|
237
|
+
});
|
|
238
|
+
});
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import {
|
|
3
|
+
fireEvent,
|
|
4
|
+
render,
|
|
5
|
+
screen,
|
|
6
|
+
waitFor,
|
|
7
|
+
} from "@testing-library/react-native";
|
|
8
|
+
import { actionExecutor } from "@applicaster/zapp-react-native-utils/actionsExecutor/ActionExecutor";
|
|
9
|
+
|
|
10
|
+
jest.mock(
|
|
11
|
+
"@applicaster/zapp-react-native-utils/actionsExecutor/ActionExecutor",
|
|
12
|
+
() => ({
|
|
13
|
+
actionExecutor: {
|
|
14
|
+
handleAction: jest.fn().mockResolvedValue(undefined),
|
|
15
|
+
handleActions: jest.fn().mockResolvedValue(undefined),
|
|
16
|
+
},
|
|
17
|
+
ActionResult: { Success: "success", Error: "error" },
|
|
18
|
+
})
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
jest.mock("../AudioPlayer/Components/Input", () => ({
|
|
22
|
+
AudioPlayerInput: ({ value, onChangeText, data }: any) => {
|
|
23
|
+
const { TextInput, Text } = require("react-native");
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<>
|
|
27
|
+
<Text testID="input-label">{data?.label}</Text>
|
|
28
|
+
<TextInput
|
|
29
|
+
testID="playlist-input"
|
|
30
|
+
value={value}
|
|
31
|
+
onChangeText={onChangeText}
|
|
32
|
+
placeholder={data?.placeholder}
|
|
33
|
+
/>
|
|
34
|
+
</>
|
|
35
|
+
);
|
|
36
|
+
},
|
|
37
|
+
VARIANT_NAME_PLAYLIST_INPUT: {},
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
jest.mock("../AudioPlayer/Components/Button", () => ({
|
|
41
|
+
AudioPlayerButton: ({ data, onPress }: any) => {
|
|
42
|
+
const { Pressable, Text } = require("react-native");
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<Pressable testID="submit-button" onPress={onPress}>
|
|
46
|
+
<Text>{data?.title}</Text>
|
|
47
|
+
</Pressable>
|
|
48
|
+
);
|
|
49
|
+
},
|
|
50
|
+
VARIANT_TEXT_ONLY: {},
|
|
51
|
+
}));
|
|
52
|
+
|
|
53
|
+
import { TextInputContent } from "../TextInputContent";
|
|
54
|
+
|
|
55
|
+
const submitAction = {
|
|
56
|
+
type: "sendCloudEvent",
|
|
57
|
+
options: {
|
|
58
|
+
url: "https://server.com/cloud-events",
|
|
59
|
+
type: "com.applicaster.collection.create.v1",
|
|
60
|
+
subject: "edit_collection",
|
|
61
|
+
data: { collectionId: "playlist-123" },
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const baseProps = {
|
|
66
|
+
inputLabel: "Name your playlist",
|
|
67
|
+
defaultValue: "Summer Hits",
|
|
68
|
+
buttonLabel: "Update",
|
|
69
|
+
actions: [submitAction],
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
describe("TextInputContent", () => {
|
|
73
|
+
beforeEach(() => {
|
|
74
|
+
jest.clearAllMocks();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("renders default value and button label without a modal header", () => {
|
|
78
|
+
render(<TextInputContent {...baseProps} />);
|
|
79
|
+
|
|
80
|
+
expect(screen.queryByTestId("sheet-header")).toBeNull();
|
|
81
|
+
|
|
82
|
+
expect(screen.getByTestId("playlist-input").props.value).toBe(
|
|
83
|
+
"Summer Hits"
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
expect(screen.getByTestId("submit-button")).toBeTruthy();
|
|
87
|
+
expect(screen.getByText("Update")).toBeTruthy();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("on submit merges name into sendCloudEvent data then dismisses", async () => {
|
|
91
|
+
render(<TextInputContent {...baseProps} />);
|
|
92
|
+
|
|
93
|
+
fireEvent.changeText(screen.getByTestId("playlist-input"), "New Name");
|
|
94
|
+
fireEvent.press(screen.getByTestId("submit-button"));
|
|
95
|
+
|
|
96
|
+
await waitFor(() => {
|
|
97
|
+
expect(actionExecutor.handleActions).toHaveBeenCalledTimes(1);
|
|
98
|
+
expect(actionExecutor.handleAction).toHaveBeenCalledTimes(1);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
expect(actionExecutor.handleActions).toHaveBeenCalledWith([
|
|
102
|
+
{
|
|
103
|
+
type: "sendCloudEvent",
|
|
104
|
+
options: {
|
|
105
|
+
url: "https://server.com/cloud-events",
|
|
106
|
+
type: "com.applicaster.collection.create.v1",
|
|
107
|
+
subject: "edit_collection",
|
|
108
|
+
data: {
|
|
109
|
+
collectionId: "playlist-123",
|
|
110
|
+
name: "New Name",
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
]);
|
|
115
|
+
|
|
116
|
+
expect(actionExecutor.handleAction).toHaveBeenCalledWith({
|
|
117
|
+
type: "dismissBottomSheet",
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
});
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import {
|
|
2
|
+
actionExecutor,
|
|
3
|
+
ActionResult,
|
|
4
|
+
} from "@applicaster/zapp-react-native-utils/actionsExecutor/ActionExecutor";
|
|
5
|
+
import { TextInputContent } from "../TextInputContent";
|
|
6
|
+
import { showTextInputAction } from "../showTextInput";
|
|
7
|
+
|
|
8
|
+
jest.mock(
|
|
9
|
+
"@applicaster/zapp-react-native-utils/actionsExecutor/ActionExecutor",
|
|
10
|
+
() => ({
|
|
11
|
+
actionExecutor: {
|
|
12
|
+
handleAction: jest.fn().mockResolvedValue("success"),
|
|
13
|
+
},
|
|
14
|
+
ActionResult: { Success: "success", Error: "error" },
|
|
15
|
+
})
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
jest.mock("../TextInputContent", () => ({
|
|
19
|
+
TextInputContent: function MockTextInputContent() {
|
|
20
|
+
return null;
|
|
21
|
+
},
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
describe("showTextInputAction", () => {
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
jest.clearAllMocks();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("delegates to openBottomSheet with TextInputContent as body-only component", async () => {
|
|
30
|
+
const action = {
|
|
31
|
+
type: "showTextInput",
|
|
32
|
+
options: {
|
|
33
|
+
headerTitle: "Create Playlist",
|
|
34
|
+
inputLabel: "Name your playlist",
|
|
35
|
+
defaultValue: "",
|
|
36
|
+
buttonLabel: "Create",
|
|
37
|
+
actions: [
|
|
38
|
+
{
|
|
39
|
+
type: "sendCloudEvent",
|
|
40
|
+
options: {
|
|
41
|
+
url: "https://example.com/events",
|
|
42
|
+
type: "com.applicaster.collection.create.v1",
|
|
43
|
+
data: {},
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
type: "showToast",
|
|
48
|
+
options: {
|
|
49
|
+
message: "Playlist created.",
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const result = await showTextInputAction(action as any);
|
|
57
|
+
|
|
58
|
+
expect(result).toBe(ActionResult.Success);
|
|
59
|
+
expect(actionExecutor.handleAction).toHaveBeenCalledTimes(1);
|
|
60
|
+
|
|
61
|
+
expect(actionExecutor.handleAction).toHaveBeenCalledWith(
|
|
62
|
+
{
|
|
63
|
+
type: "openBottomSheet",
|
|
64
|
+
options: {
|
|
65
|
+
header: {
|
|
66
|
+
title: "Create Playlist",
|
|
67
|
+
},
|
|
68
|
+
content: {
|
|
69
|
+
component: TextInputContent,
|
|
70
|
+
props: {
|
|
71
|
+
inputLabel: "Name your playlist",
|
|
72
|
+
defaultValue: "",
|
|
73
|
+
buttonLabel: "Create",
|
|
74
|
+
actions: action.options.actions,
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
undefined
|
|
80
|
+
);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("returns Error when required options are missing", async () => {
|
|
84
|
+
const action = {
|
|
85
|
+
type: "showTextInput",
|
|
86
|
+
options: {
|
|
87
|
+
headerTitle: "Create Playlist",
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const result = await showTextInputAction(action as any);
|
|
92
|
+
expect(result).toBe(ActionResult.Error);
|
|
93
|
+
expect(actionExecutor.handleAction).not.toHaveBeenCalled();
|
|
94
|
+
});
|
|
95
|
+
});
|