@terreno/ui 0.9.3 → 0.11.0
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/dist/Banner.js +2 -2
- package/dist/Banner.js.map +1 -1
- package/dist/Common.d.ts +6 -10
- package/dist/Common.js.map +1 -1
- package/dist/ConsentHistory.d.ts +2 -1
- package/dist/DataTable.js +4 -2
- package/dist/DataTable.js.map +1 -1
- package/dist/DateUtilities.js +16 -7
- package/dist/DateUtilities.js.map +1 -1
- package/dist/DraggableList.d.ts +5 -4
- package/dist/DraggableList.js.map +1 -1
- package/dist/Icon.js +0 -3
- package/dist/Icon.js.map +1 -1
- package/dist/PasswordField.d.ts +1 -1
- package/dist/PasswordField.js.map +1 -1
- package/dist/TextFieldNumberActionSheet.d.ts +1 -1
- package/dist/Toast.d.ts +1 -1
- package/dist/Toast.js +2 -2
- package/dist/Toast.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/package.json +2 -1
- package/src/ActionSheet.test.tsx +262 -3
- package/src/AddressField.test.tsx +50 -0
- package/src/Banner.test.tsx +22 -0
- package/src/Banner.tsx +2 -2
- package/src/Box.test.tsx +218 -0
- package/src/Button.test.tsx +71 -0
- package/src/Common.ts +11 -9
- package/src/ConsentFormScreen.test.tsx +167 -0
- package/src/ConsentHistory.tsx +1 -1
- package/src/ConsentNavigator.test.tsx +210 -4
- package/src/DataTable.tsx +15 -15
- package/src/DateUtilities.test.ts +34 -16
- package/src/DateUtilities.tsx +24 -13
- package/src/DecimalRangeActionSheet.test.tsx +53 -2
- package/src/DraggableList.tsx +5 -5
- package/src/EmailField.test.tsx +81 -0
- package/src/EmojiSelector.test.tsx +262 -1
- package/src/ErrorBoundary.test.tsx +52 -1
- package/src/HeightActionSheet.test.tsx +57 -2
- package/src/Icon.tsx +0 -3
- package/src/InfoModalIcon.test.tsx +16 -0
- package/src/InfoTooltipButton.test.tsx +53 -1
- package/src/MobileAddressAutoComplete.test.tsx +137 -7
- package/src/Modal.test.tsx +188 -0
- package/src/NumberPickerActionSheet.test.tsx +59 -2
- package/src/OpenAPIContext.test.tsx +184 -3
- package/src/Page.test.tsx +162 -1
- package/src/Pagination.test.tsx +16 -0
- package/src/PasswordField.tsx +1 -1
- package/src/PhoneNumberField.test.tsx +46 -9
- package/src/PickerSelect.test.tsx +230 -0
- package/src/SegmentedControl.test.tsx +38 -0
- package/src/SelectBadge.test.tsx +52 -1
- package/src/SideDrawer.test.tsx +69 -0
- package/src/Signature.test.tsx +42 -5
- package/src/SignatureField.test.tsx +35 -0
- package/src/Slider.test.tsx +59 -0
- package/src/Spinner.test.tsx +6 -0
- package/src/SplitPage.test.tsx +228 -2
- package/src/TapToEdit.test.tsx +171 -1
- package/src/TerrenoProvider.test.tsx +42 -2
- package/src/TextFieldNumberActionSheet.tsx +1 -1
- package/src/Theme.test.tsx +118 -28
- package/src/Toast.test.tsx +95 -2
- package/src/Toast.tsx +3 -3
- package/src/Tooltip.test.tsx +204 -1
- package/src/UnifiedAddressAutoComplete.test.tsx +38 -19
- package/src/UserInactivity.test.tsx +73 -1
- package/src/Utilities.test.tsx +190 -2
- package/src/WebAddressAutocomplete.test.tsx +148 -1
- package/src/__snapshots__/ActionSheet.test.tsx.snap +1736 -0
- package/src/__snapshots__/Button.test.tsx.snap +68 -0
- package/src/__snapshots__/EmojiSelector.test.tsx.snap +1363 -0
- package/src/__snapshots__/InfoTooltipButton.test.tsx.snap +72 -3
- package/src/__snapshots__/MobileAddressAutoComplete.test.tsx.snap +60 -9
- package/src/__snapshots__/Modal.test.tsx.snap +181 -0
- package/src/__snapshots__/Page.test.tsx.snap +48 -2
- package/src/__snapshots__/PhoneNumberField.test.tsx.snap +0 -93
- package/src/__snapshots__/PickerSelect.test.tsx.snap +706 -0
- package/src/__snapshots__/SideDrawer.test.tsx.snap +533 -1399
- package/src/__snapshots__/Signature.test.tsx.snap +0 -3
- package/src/__snapshots__/SplitPage.test.tsx.snap +970 -0
- package/src/__snapshots__/UnifiedAddressAutoComplete.test.tsx.snap +220 -4
- package/src/__snapshots__/WebAddressAutocomplete.test.tsx.snap +93 -0
- package/src/bunSetup.ts +204 -121
- package/src/index.tsx +2 -2
- package/src/table/TableHeaderCell.test.tsx +142 -0
- package/src/table/TableRow.test.tsx +33 -0
- package/src/table/__snapshots__/TableRow.test.tsx.snap +403 -0
- package/src/table/tableContext.test.tsx +96 -0
- package/src/test-utils.tsx +1 -1
- package/src/useConsentForms.test.ts +130 -0
- package/src/useSubmitConsent.test.ts +64 -0
package/src/DateUtilities.tsx
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import {DateTime} from "luxon";
|
|
2
2
|
|
|
3
|
+
const getErrorMessage = (error: unknown): string => {
|
|
4
|
+
if (error instanceof Error) {
|
|
5
|
+
return error.message;
|
|
6
|
+
}
|
|
7
|
+
return String(error);
|
|
8
|
+
};
|
|
9
|
+
|
|
3
10
|
function getDate(date: string, {timezone}: {timezone?: string} = {}): DateTime {
|
|
4
11
|
if (!date) {
|
|
5
12
|
throw new Error("Passed undefined");
|
|
@@ -57,8 +64,8 @@ export function humanDate(
|
|
|
57
64
|
let clonedDate;
|
|
58
65
|
try {
|
|
59
66
|
clonedDate = getDate(date, {timezone});
|
|
60
|
-
} catch (error:
|
|
61
|
-
throw new Error(`humanDate: ${error
|
|
67
|
+
} catch (error: unknown) {
|
|
68
|
+
throw new Error(`humanDate: ${getErrorMessage(error)}`);
|
|
62
69
|
}
|
|
63
70
|
if (isTomorrow(date, {timezone})) {
|
|
64
71
|
return "Tomorrow";
|
|
@@ -91,8 +98,8 @@ export function humanDateAndTime(
|
|
|
91
98
|
let clonedDate;
|
|
92
99
|
try {
|
|
93
100
|
clonedDate = getDate(date, {timezone});
|
|
94
|
-
} catch (error:
|
|
95
|
-
throw new Error(`humanDateAndTime: ${error
|
|
101
|
+
} catch (error: unknown) {
|
|
102
|
+
throw new Error(`humanDateAndTime: ${getErrorMessage(error)}`);
|
|
96
103
|
}
|
|
97
104
|
// This should maybe use printTime()
|
|
98
105
|
let time: string = "";
|
|
@@ -158,8 +165,8 @@ export const printDate = (
|
|
|
158
165
|
let clonedDate;
|
|
159
166
|
try {
|
|
160
167
|
clonedDate = getDate(date, {timezone});
|
|
161
|
-
} catch (error:
|
|
162
|
-
throw new Error(`printDate: ${error
|
|
168
|
+
} catch (error: unknown) {
|
|
169
|
+
throw new Error(`printDate: ${getErrorMessage(error)}`);
|
|
163
170
|
}
|
|
164
171
|
|
|
165
172
|
return clonedDate.toLocaleString(DateTime.DATE_SHORT);
|
|
@@ -213,8 +220,8 @@ export function printTime(
|
|
|
213
220
|
}
|
|
214
221
|
try {
|
|
215
222
|
clonedDate = getDate(date, {timezone});
|
|
216
|
-
} catch (error:
|
|
217
|
-
throw new Error(`printTime: ${error
|
|
223
|
+
} catch (error: unknown) {
|
|
224
|
+
throw new Error(`printTime: ${getErrorMessage(error)}`);
|
|
218
225
|
}
|
|
219
226
|
if (showTimezone) {
|
|
220
227
|
return clonedDate.toLocaleString({
|
|
@@ -246,8 +253,8 @@ export function printDateAndTime(
|
|
|
246
253
|
let clonedDate;
|
|
247
254
|
try {
|
|
248
255
|
clonedDate = getDate(date, {timezone});
|
|
249
|
-
} catch (error:
|
|
250
|
-
throw new Error(`printDateAndTime: ${error
|
|
256
|
+
} catch (error: unknown) {
|
|
257
|
+
throw new Error(`printDateAndTime: ${getErrorMessage(error)}`);
|
|
251
258
|
}
|
|
252
259
|
if (showTimezone) {
|
|
253
260
|
return clonedDate.toLocaleString({
|
|
@@ -304,8 +311,8 @@ export function printSince(
|
|
|
304
311
|
const ago = showAgo ? " ago" : "";
|
|
305
312
|
try {
|
|
306
313
|
clonedDate = getDate(date, {timezone});
|
|
307
|
-
} catch (error:
|
|
308
|
-
throw new Error(`printSince: ${error
|
|
314
|
+
} catch (error: unknown) {
|
|
315
|
+
throw new Error(`printSince: ${getErrorMessage(error)}`);
|
|
309
316
|
}
|
|
310
317
|
const now = timezone ? DateTime.now().setZone(timezone) : DateTime.now();
|
|
311
318
|
const diff = now.diff(clonedDate, "months");
|
|
@@ -349,7 +356,11 @@ export function getTimezoneOptions(location: "USA" | "Worldwide", shortTimezone
|
|
|
349
356
|
if (location === "USA") {
|
|
350
357
|
timezones = usTimezoneOptions.map((tz) => [tz.label, tz.value]);
|
|
351
358
|
} else {
|
|
352
|
-
|
|
359
|
+
const intlWithSupportedValuesOf = Intl as typeof Intl & {
|
|
360
|
+
supportedValuesOf?: (key: "timeZone") => string[];
|
|
361
|
+
};
|
|
362
|
+
const supportedValues = intlWithSupportedValuesOf.supportedValuesOf?.("timeZone") ?? [];
|
|
363
|
+
timezones = supportedValues.map((tz) => {
|
|
353
364
|
return [tz, tz];
|
|
354
365
|
});
|
|
355
366
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {describe, expect, it} from "bun:test";
|
|
2
|
-
import {render} from "@testing-library/react-native";
|
|
1
|
+
import {describe, expect, it, mock} from "bun:test";
|
|
2
|
+
import {act, render} from "@testing-library/react-native";
|
|
3
3
|
import {createRef} from "react";
|
|
4
4
|
|
|
5
5
|
import type {ActionSheet} from "./ActionSheet";
|
|
@@ -43,4 +43,55 @@ describe("DecimalRangeActionSheet", () => {
|
|
|
43
43
|
);
|
|
44
44
|
expect(toJSON()).toMatchSnapshot();
|
|
45
45
|
});
|
|
46
|
+
|
|
47
|
+
it("invokes onChange when whole picker changes", () => {
|
|
48
|
+
const actionSheetRef = createRef<ActionSheet>();
|
|
49
|
+
const handleChange = mock((_val: string) => {});
|
|
50
|
+
const {UNSAFE_getAllByProps} = render(
|
|
51
|
+
<ThemeProvider>
|
|
52
|
+
<DecimalRangeActionSheet
|
|
53
|
+
actionSheetRef={actionSheetRef}
|
|
54
|
+
max={10}
|
|
55
|
+
min={0}
|
|
56
|
+
onChange={handleChange}
|
|
57
|
+
value="5.5"
|
|
58
|
+
/>
|
|
59
|
+
</ThemeProvider>
|
|
60
|
+
);
|
|
61
|
+
// The first Picker is the whole number one (selectedValue: "5")
|
|
62
|
+
const wholePickers = UNSAFE_getAllByProps({selectedValue: "5"});
|
|
63
|
+
const whole = wholePickers.find((p) => typeof p.props.onValueChange === "function");
|
|
64
|
+
act(() => {
|
|
65
|
+
if (whole) {
|
|
66
|
+
whole.props.onValueChange(7);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
expect(handleChange).toHaveBeenCalled();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("invokes onChange when decimal picker changes", () => {
|
|
73
|
+
const actionSheetRef = createRef<ActionSheet>();
|
|
74
|
+
const handleChange = mock((_val: string) => {});
|
|
75
|
+
const {UNSAFE_getAllByProps} = render(
|
|
76
|
+
<ThemeProvider>
|
|
77
|
+
<DecimalRangeActionSheet
|
|
78
|
+
actionSheetRef={actionSheetRef}
|
|
79
|
+
max={10}
|
|
80
|
+
min={0}
|
|
81
|
+
onChange={handleChange}
|
|
82
|
+
value="5.5"
|
|
83
|
+
/>
|
|
84
|
+
</ThemeProvider>
|
|
85
|
+
);
|
|
86
|
+
// The second Picker is the decimal one (selectedValue: "5")
|
|
87
|
+
const decimalPickers = UNSAFE_getAllByProps({selectedValue: "5"});
|
|
88
|
+
// Use the last one (second picker)
|
|
89
|
+
const decimal = decimalPickers[decimalPickers.length - 1];
|
|
90
|
+
act(() => {
|
|
91
|
+
if (decimal && typeof decimal.props.onValueChange === "function") {
|
|
92
|
+
decimal.props.onValueChange(3);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
expect(handleChange).toHaveBeenCalled();
|
|
96
|
+
});
|
|
46
97
|
});
|
package/src/DraggableList.tsx
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// Copyright Patryk Jaworski @gerwld
|
|
7
7
|
|
|
8
8
|
import React, {useMemo, useState} from "react";
|
|
9
|
-
import {Platform, View} from "react-native";
|
|
9
|
+
import {Platform, type StyleProp, View, type ViewStyle} from "react-native";
|
|
10
10
|
import {Gesture, GestureDetector} from "react-native-gesture-handler";
|
|
11
11
|
import Animated, {
|
|
12
12
|
runOnJS,
|
|
@@ -41,7 +41,7 @@ interface DragItemProps {
|
|
|
41
41
|
renderGrip?: React.ReactElement | (() => React.ReactElement); // Optional drag handle
|
|
42
42
|
passVibration?: () => void; // Optional haptic feedback callback
|
|
43
43
|
itemBorderRadius: number; // Border radius for items
|
|
44
|
-
itemContainerStyle?:
|
|
44
|
+
itemContainerStyle?: StyleProp<ViewStyle>; // Additional styling for item container
|
|
45
45
|
callbackNewDataIds?: (newIds: string[]) => void; // Callback when items are reordered
|
|
46
46
|
backgroundOnHold?: string; // Background color when item is being dragged
|
|
47
47
|
plainPosition: number; // Current position in the list
|
|
@@ -52,10 +52,10 @@ interface DragItemProps {
|
|
|
52
52
|
*/
|
|
53
53
|
interface DragListProps {
|
|
54
54
|
data?: string[]; // Array of item IDs (deprecated, use dataIDs)
|
|
55
|
-
style?:
|
|
55
|
+
style?: StyleProp<ViewStyle>; // Style for the list container
|
|
56
56
|
callbackNewDataIds: (newIds: string[]) => void; // Callback when items are reordered
|
|
57
|
-
contentContainerStyle?:
|
|
58
|
-
itemContainerStyle?:
|
|
57
|
+
contentContainerStyle?: StyleProp<ViewStyle>; // Style for the content container
|
|
58
|
+
itemContainerStyle?: StyleProp<ViewStyle>; // Style for each item container
|
|
59
59
|
renderItem: (props: {item: string}) => React.ReactElement; // Function to render item content
|
|
60
60
|
renderGrip?: React.ReactElement | (() => React.ReactElement); // Optional custom drag handle
|
|
61
61
|
passVibration?: () => void; // Optional haptic feedback callback
|
package/src/EmailField.test.tsx
CHANGED
|
@@ -103,4 +103,85 @@ describe("EmailField", () => {
|
|
|
103
103
|
);
|
|
104
104
|
expect(toJSON()).toMatchSnapshot();
|
|
105
105
|
});
|
|
106
|
+
|
|
107
|
+
it("calls onBlur with valid email", async () => {
|
|
108
|
+
const handleBlur = mock((_value: string) => {});
|
|
109
|
+
const {getByDisplayValue} = renderWithTheme(
|
|
110
|
+
<EmailField label="Email" onBlur={handleBlur} onChange={() => {}} value="valid@email.com" />
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
const input = getByDisplayValue("valid@email.com");
|
|
114
|
+
await act(async () => {
|
|
115
|
+
fireEvent(input, "blur", {nativeEvent: {text: "valid@email.com"}});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
await waitFor(() => {
|
|
119
|
+
expect(handleBlur).toHaveBeenCalledWith("valid@email.com");
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("does not call onBlur with invalid email", async () => {
|
|
124
|
+
const handleBlur = mock((_value: string) => {});
|
|
125
|
+
const {getByDisplayValue} = renderWithTheme(
|
|
126
|
+
<EmailField label="Email" onBlur={handleBlur} onChange={() => {}} value="" />
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
const input = getByDisplayValue("");
|
|
130
|
+
await act(async () => {
|
|
131
|
+
fireEvent.changeText(input, "invalid-email");
|
|
132
|
+
});
|
|
133
|
+
await act(async () => {
|
|
134
|
+
fireEvent(input, "blur", {nativeEvent: {text: "invalid-email"}});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
expect(handleBlur).not.toHaveBeenCalled();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("clears local error when typing a valid email after invalid", async () => {
|
|
141
|
+
const handleChange = mock((_value: string) => {});
|
|
142
|
+
const {getByDisplayValue, queryByText} = renderWithTheme(
|
|
143
|
+
<EmailField label="Email" onChange={handleChange} value="" />
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
const input = getByDisplayValue("");
|
|
147
|
+
|
|
148
|
+
// Type invalid email first
|
|
149
|
+
await act(async () => {
|
|
150
|
+
fireEvent.changeText(input, "bad");
|
|
151
|
+
});
|
|
152
|
+
// Trigger blur to set the error
|
|
153
|
+
await act(async () => {
|
|
154
|
+
fireEvent(input, "blur", {nativeEvent: {text: "bad"}});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
await waitFor(() => {
|
|
158
|
+
expect(queryByText("Invalid email address format")).toBeTruthy();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// Now type a valid email to clear the error
|
|
162
|
+
await act(async () => {
|
|
163
|
+
fireEvent.changeText(input, "good@email.com");
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
await waitFor(() => {
|
|
167
|
+
expect(queryByText("Invalid email address format")).toBeFalsy();
|
|
168
|
+
expect(handleChange).toHaveBeenCalledWith("good@email.com");
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("validates empty string as valid on blur", async () => {
|
|
173
|
+
const handleBlur = mock((_value: string) => {});
|
|
174
|
+
const {getByDisplayValue} = renderWithTheme(
|
|
175
|
+
<EmailField label="Email" onBlur={handleBlur} onChange={() => {}} value="" />
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
const input = getByDisplayValue("");
|
|
179
|
+
await act(async () => {
|
|
180
|
+
fireEvent(input, "blur", {nativeEvent: {text: ""}});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
await waitFor(() => {
|
|
184
|
+
expect(handleBlur).toHaveBeenCalledWith("");
|
|
185
|
+
});
|
|
186
|
+
});
|
|
106
187
|
});
|
|
@@ -1,8 +1,21 @@
|
|
|
1
1
|
import {describe, expect, it, mock} from "bun:test";
|
|
2
|
+
import {act, fireEvent} from "@testing-library/react-native";
|
|
2
3
|
|
|
3
|
-
import EmojiSelector, {Categories} from "./EmojiSelector";
|
|
4
|
+
import EmojiSelector, {Categories, charFromEmojiObject} from "./EmojiSelector";
|
|
4
5
|
import {renderWithTheme} from "./test-utils";
|
|
5
6
|
|
|
7
|
+
interface LayoutEvent {
|
|
8
|
+
nativeEvent: {layout: {height: number; width: number; x: number; y: number}};
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface LayoutRoot {
|
|
12
|
+
props: {onLayout?: (event: LayoutEvent) => void};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface RenderItemCell {
|
|
16
|
+
props: {onPress?: () => void};
|
|
17
|
+
}
|
|
18
|
+
|
|
6
19
|
describe("EmojiSelector", () => {
|
|
7
20
|
it("renders search bar when showSearchBar is true", () => {
|
|
8
21
|
const {getByPlaceholderText} = renderWithTheme(
|
|
@@ -58,4 +71,252 @@ describe("EmojiSelector", () => {
|
|
|
58
71
|
|
|
59
72
|
expect(tree.toJSON()).toMatchSnapshot();
|
|
60
73
|
});
|
|
74
|
+
|
|
75
|
+
it("renders without tabs", () => {
|
|
76
|
+
const {toJSON} = renderWithTheme(
|
|
77
|
+
<EmojiSelector
|
|
78
|
+
category={Categories.people}
|
|
79
|
+
columns={6}
|
|
80
|
+
onEmojiSelected={mock(() => {})}
|
|
81
|
+
placeholder="Search emojis"
|
|
82
|
+
showHistory={false}
|
|
83
|
+
showSearchBar
|
|
84
|
+
showSectionTitles={false}
|
|
85
|
+
showTabs={false}
|
|
86
|
+
theme="#007AFF"
|
|
87
|
+
/>
|
|
88
|
+
);
|
|
89
|
+
expect(toJSON()).toMatchSnapshot();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("renders without search bar", () => {
|
|
93
|
+
const {queryByPlaceholderText} = renderWithTheme(
|
|
94
|
+
<EmojiSelector
|
|
95
|
+
category={Categories.people}
|
|
96
|
+
columns={6}
|
|
97
|
+
onEmojiSelected={mock(() => {})}
|
|
98
|
+
placeholder="Search emojis"
|
|
99
|
+
showHistory={false}
|
|
100
|
+
showSearchBar={false}
|
|
101
|
+
showSectionTitles
|
|
102
|
+
showTabs
|
|
103
|
+
theme="#007AFF"
|
|
104
|
+
/>
|
|
105
|
+
);
|
|
106
|
+
expect(queryByPlaceholderText("Search emojis")).toBeNull();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("renders with history enabled", () => {
|
|
110
|
+
const {toJSON} = renderWithTheme(
|
|
111
|
+
<EmojiSelector
|
|
112
|
+
category={Categories.people}
|
|
113
|
+
columns={6}
|
|
114
|
+
onEmojiSelected={mock(() => {})}
|
|
115
|
+
placeholder="Search emojis"
|
|
116
|
+
showHistory
|
|
117
|
+
showSearchBar
|
|
118
|
+
showSectionTitles
|
|
119
|
+
showTabs
|
|
120
|
+
theme="#007AFF"
|
|
121
|
+
/>
|
|
122
|
+
);
|
|
123
|
+
expect(toJSON()).toMatchSnapshot();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("renders with all category and shouldInclude filter", () => {
|
|
127
|
+
const {toJSON} = renderWithTheme(
|
|
128
|
+
<EmojiSelector
|
|
129
|
+
category={Categories.all}
|
|
130
|
+
columns={8}
|
|
131
|
+
onEmojiSelected={mock(() => {})}
|
|
132
|
+
placeholder="Search"
|
|
133
|
+
shouldInclude={(emoji) => emoji.category === "Smileys & Emotion"}
|
|
134
|
+
showHistory={false}
|
|
135
|
+
showSearchBar
|
|
136
|
+
showSectionTitles
|
|
137
|
+
showTabs
|
|
138
|
+
theme="#007AFF"
|
|
139
|
+
/>
|
|
140
|
+
);
|
|
141
|
+
expect(toJSON()).toMatchSnapshot();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("renders with history category", () => {
|
|
145
|
+
const {toJSON} = renderWithTheme(
|
|
146
|
+
<EmojiSelector
|
|
147
|
+
category={Categories.history}
|
|
148
|
+
columns={6}
|
|
149
|
+
onEmojiSelected={mock(() => {})}
|
|
150
|
+
placeholder="Search"
|
|
151
|
+
showHistory
|
|
152
|
+
showSearchBar
|
|
153
|
+
showSectionTitles
|
|
154
|
+
showTabs
|
|
155
|
+
theme="#007AFF"
|
|
156
|
+
/>
|
|
157
|
+
);
|
|
158
|
+
expect(toJSON()).toMatchSnapshot();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("updates search query when user types in search bar", async () => {
|
|
162
|
+
const {getByPlaceholderText} = renderWithTheme(
|
|
163
|
+
<EmojiSelector
|
|
164
|
+
category={Categories.all}
|
|
165
|
+
columns={6}
|
|
166
|
+
onEmojiSelected={mock(() => {})}
|
|
167
|
+
placeholder="Search emojis"
|
|
168
|
+
showHistory={false}
|
|
169
|
+
showSearchBar
|
|
170
|
+
showSectionTitles
|
|
171
|
+
showTabs
|
|
172
|
+
theme="#007AFF"
|
|
173
|
+
/>
|
|
174
|
+
);
|
|
175
|
+
const input = getByPlaceholderText("Search emojis");
|
|
176
|
+
await act(async () => {
|
|
177
|
+
fireEvent.changeText(input, "smile");
|
|
178
|
+
});
|
|
179
|
+
expect(input.props.value).toBe("smile");
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("charFromEmojiObject returns a string for a valid emoji", () => {
|
|
183
|
+
const smiley = {
|
|
184
|
+
category: "Smileys & Emotion",
|
|
185
|
+
short_names: ["smiley"],
|
|
186
|
+
sort_order: 1,
|
|
187
|
+
unified: "1F603",
|
|
188
|
+
};
|
|
189
|
+
expect(charFromEmojiObject(smiley)).toBe("😃");
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("exports Categories object with all expected keys", () => {
|
|
193
|
+
expect(Categories.all).toBeDefined();
|
|
194
|
+
expect(Categories.emotion).toBeDefined();
|
|
195
|
+
expect(Categories.people).toBeDefined();
|
|
196
|
+
expect(Categories.history).toBeDefined();
|
|
197
|
+
expect(Categories.nature).toBeDefined();
|
|
198
|
+
expect(Categories.food).toBeDefined();
|
|
199
|
+
expect(Categories.activities).toBeDefined();
|
|
200
|
+
expect(Categories.places).toBeDefined();
|
|
201
|
+
expect(Categories.objects).toBeDefined();
|
|
202
|
+
expect(Categories.symbols).toBeDefined();
|
|
203
|
+
expect(Categories.flags).toBeDefined();
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("handles layout event to compute emoji list and set ready state", async () => {
|
|
207
|
+
const {toJSON, root} = renderWithTheme(
|
|
208
|
+
<EmojiSelector
|
|
209
|
+
category={Categories.people}
|
|
210
|
+
columns={6}
|
|
211
|
+
onEmojiSelected={mock(() => {})}
|
|
212
|
+
placeholder="Search"
|
|
213
|
+
showHistory={false}
|
|
214
|
+
showSearchBar
|
|
215
|
+
showSectionTitles
|
|
216
|
+
showTabs
|
|
217
|
+
theme="#007AFF"
|
|
218
|
+
/>
|
|
219
|
+
);
|
|
220
|
+
// Trigger the onLayout callback so state moves to ready.
|
|
221
|
+
await act(async () => {
|
|
222
|
+
(root as LayoutRoot).props.onLayout?.({
|
|
223
|
+
nativeEvent: {layout: {height: 600, width: 360, x: 0, y: 0}},
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
expect(toJSON()).toBeTruthy();
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it("switches categories when a tab is pressed after layout", async () => {
|
|
230
|
+
const {root, UNSAFE_getAllByType} = renderWithTheme(
|
|
231
|
+
<EmojiSelector
|
|
232
|
+
category={Categories.people}
|
|
233
|
+
columns={6}
|
|
234
|
+
onEmojiSelected={mock(() => {})}
|
|
235
|
+
placeholder="Search"
|
|
236
|
+
showHistory={false}
|
|
237
|
+
showSearchBar
|
|
238
|
+
showSectionTitles
|
|
239
|
+
showTabs
|
|
240
|
+
theme="#007AFF"
|
|
241
|
+
/>
|
|
242
|
+
);
|
|
243
|
+
await act(async () => {
|
|
244
|
+
(root as LayoutRoot).props.onLayout?.({
|
|
245
|
+
nativeEvent: {layout: {height: 600, width: 360, x: 0, y: 0}},
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
const {TouchableOpacity} = require("react-native");
|
|
249
|
+
const tabs = UNSAFE_getAllByType(TouchableOpacity);
|
|
250
|
+
expect(tabs.length).toBeGreaterThan(0);
|
|
251
|
+
// Press the first tab (e.g., "Smileys & Emotion") to exercise handleTabSelect.
|
|
252
|
+
await act(async () => {
|
|
253
|
+
tabs[0].props.onPress?.();
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it("invokes onEmojiSelected when an emoji cell is pressed", async () => {
|
|
258
|
+
const onEmojiSelected = mock(() => {});
|
|
259
|
+
const {root, UNSAFE_getAllByType} = renderWithTheme(
|
|
260
|
+
<EmojiSelector
|
|
261
|
+
category={Categories.emotion}
|
|
262
|
+
columns={6}
|
|
263
|
+
onEmojiSelected={onEmojiSelected}
|
|
264
|
+
placeholder="Search"
|
|
265
|
+
showHistory
|
|
266
|
+
showSearchBar={false}
|
|
267
|
+
showSectionTitles
|
|
268
|
+
showTabs={false}
|
|
269
|
+
theme="#007AFF"
|
|
270
|
+
/>
|
|
271
|
+
);
|
|
272
|
+
await act(async () => {
|
|
273
|
+
(root as LayoutRoot).props.onLayout?.({
|
|
274
|
+
nativeEvent: {layout: {height: 600, width: 360, x: 0, y: 0}},
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
const {FlatList} = require("react-native");
|
|
279
|
+
const [list] = UNSAFE_getAllByType(FlatList);
|
|
280
|
+
expect(list).toBeTruthy();
|
|
281
|
+
const data = list.props.data ?? [];
|
|
282
|
+
expect(data.length).toBeGreaterThan(0);
|
|
283
|
+
const first = data[0];
|
|
284
|
+
const cell = list.props.renderItem({index: 0, item: first});
|
|
285
|
+
// Invoke the emoji cell onPress to exercise handleEmojiSelect + addToHistoryAsync.
|
|
286
|
+
(cell.props as RenderItemCell["props"]).onPress?.();
|
|
287
|
+
await act(async () => {
|
|
288
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
289
|
+
});
|
|
290
|
+
expect(onEmojiSelected).toHaveBeenCalled();
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it("filters the emoji list when a search query is entered", async () => {
|
|
294
|
+
const {getByPlaceholderText, UNSAFE_getAllByType, root} = renderWithTheme(
|
|
295
|
+
<EmojiSelector
|
|
296
|
+
category={Categories.all}
|
|
297
|
+
columns={6}
|
|
298
|
+
onEmojiSelected={mock(() => {})}
|
|
299
|
+
placeholder="Search emojis"
|
|
300
|
+
showHistory={false}
|
|
301
|
+
showSearchBar
|
|
302
|
+
showSectionTitles
|
|
303
|
+
showTabs
|
|
304
|
+
theme="#007AFF"
|
|
305
|
+
/>
|
|
306
|
+
);
|
|
307
|
+
await act(async () => {
|
|
308
|
+
(root as LayoutRoot).props.onLayout?.({
|
|
309
|
+
nativeEvent: {layout: {height: 600, width: 360, x: 0, y: 0}},
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
const input = getByPlaceholderText("Search emojis");
|
|
314
|
+
await act(async () => {
|
|
315
|
+
fireEvent.changeText(input, "smile");
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
const {FlatList} = require("react-native");
|
|
319
|
+
const [list] = UNSAFE_getAllByType(FlatList);
|
|
320
|
+
expect(list.props.data.length).toBeGreaterThan(0);
|
|
321
|
+
});
|
|
61
322
|
});
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import {afterAll, beforeAll, describe, expect, it, mock} from "bun:test";
|
|
1
|
+
import {afterAll, beforeAll, describe, expect, it, mock, spyOn} from "bun:test";
|
|
2
|
+
import React from "react";
|
|
2
3
|
|
|
3
4
|
import {ErrorBoundary} from "./ErrorBoundary";
|
|
5
|
+
import {ErrorPage} from "./ErrorPage";
|
|
4
6
|
import {Text} from "./Text";
|
|
5
7
|
import {renderWithTheme} from "./test-utils";
|
|
6
8
|
|
|
@@ -41,4 +43,53 @@ describe("ErrorBoundary", () => {
|
|
|
41
43
|
);
|
|
42
44
|
expect(toJSON()).toMatchSnapshot();
|
|
43
45
|
});
|
|
46
|
+
|
|
47
|
+
it("sets state from getDerivedStateFromError", () => {
|
|
48
|
+
const error = new Error("derived");
|
|
49
|
+
|
|
50
|
+
const result = ErrorBoundary.getDerivedStateFromError(error);
|
|
51
|
+
|
|
52
|
+
expect(result).toEqual({error});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("calls onError when componentDidCatch receives an error", () => {
|
|
56
|
+
const onError = mock(() => {});
|
|
57
|
+
const boundary = new ErrorBoundary({children: null, onError});
|
|
58
|
+
const error = new Error("caught");
|
|
59
|
+
|
|
60
|
+
boundary.componentDidCatch(error, {componentStack: "stack trace"});
|
|
61
|
+
|
|
62
|
+
expect(onError).toHaveBeenCalledTimes(1);
|
|
63
|
+
expect((onError as any).mock.calls[0]).toEqual([error, "stack trace"]);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("does not throw when componentDidCatch is called without onError", () => {
|
|
67
|
+
const boundary = new ErrorBoundary({children: null});
|
|
68
|
+
const error = new Error("caught");
|
|
69
|
+
|
|
70
|
+
expect(() => boundary.componentDidCatch(error, {componentStack: "stack trace"})).not.toThrow();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("resets error state when resetError is called", () => {
|
|
74
|
+
const boundary = new ErrorBoundary({children: null});
|
|
75
|
+
const setStateSpy = spyOn(boundary, "setState");
|
|
76
|
+
|
|
77
|
+
boundary.resetError();
|
|
78
|
+
|
|
79
|
+
expect(setStateSpy).toHaveBeenCalledTimes(1);
|
|
80
|
+
expect((setStateSpy as any).mock.calls[0][0]).toEqual({error: undefined});
|
|
81
|
+
setStateSpy.mockRestore();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("renders ErrorPage when state has an error", () => {
|
|
85
|
+
const boundary = new ErrorBoundary({children: <Text>Child content</Text>});
|
|
86
|
+
const error = new Error("render error");
|
|
87
|
+
boundary.state = {error};
|
|
88
|
+
|
|
89
|
+
const renderedElement = boundary.render() as React.ReactElement;
|
|
90
|
+
|
|
91
|
+
expect(renderedElement.type).toBe(ErrorPage);
|
|
92
|
+
expect(renderedElement.props.error).toBe(error);
|
|
93
|
+
expect(typeof renderedElement.props.resetError).toBe("function");
|
|
94
|
+
});
|
|
44
95
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {describe, expect, it} from "bun:test";
|
|
2
|
-
import {render} from "@testing-library/react-native";
|
|
1
|
+
import {describe, expect, it, mock} from "bun:test";
|
|
2
|
+
import {act, render} from "@testing-library/react-native";
|
|
3
3
|
import {createRef} from "react";
|
|
4
4
|
|
|
5
5
|
import type {ActionSheet} from "./ActionSheet";
|
|
@@ -31,4 +31,59 @@ describe("HeightActionSheet", () => {
|
|
|
31
31
|
);
|
|
32
32
|
expect(toJSON()).toMatchSnapshot();
|
|
33
33
|
});
|
|
34
|
+
|
|
35
|
+
it("renders with title and min/max values", () => {
|
|
36
|
+
const actionSheetRef = createRef<ActionSheet>();
|
|
37
|
+
const {getByText} = render(
|
|
38
|
+
<ThemeProvider>
|
|
39
|
+
<HeightActionSheet
|
|
40
|
+
actionSheetRef={actionSheetRef}
|
|
41
|
+
max={84}
|
|
42
|
+
min={36}
|
|
43
|
+
onChange={() => {}}
|
|
44
|
+
title="Select Height"
|
|
45
|
+
value="60"
|
|
46
|
+
/>
|
|
47
|
+
</ThemeProvider>
|
|
48
|
+
);
|
|
49
|
+
expect(getByText("Select Height")).toBeTruthy();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("invokes onChange when feet picker changes", () => {
|
|
53
|
+
const actionSheetRef = createRef<ActionSheet>();
|
|
54
|
+
const handleChange = mock((_val: string) => {});
|
|
55
|
+
const {UNSAFE_getAllByProps} = render(
|
|
56
|
+
<ThemeProvider>
|
|
57
|
+
<HeightActionSheet actionSheetRef={actionSheetRef} onChange={handleChange} value="72" />
|
|
58
|
+
</ThemeProvider>
|
|
59
|
+
);
|
|
60
|
+
// feet picker has selectedValue "6" (72 / 12)
|
|
61
|
+
const feetPickers = UNSAFE_getAllByProps({selectedValue: "6"});
|
|
62
|
+
const feetPicker = feetPickers.find((p) => typeof p.props.onValueChange === "function");
|
|
63
|
+
act(() => {
|
|
64
|
+
if (feetPicker) {
|
|
65
|
+
feetPicker.props.onValueChange(5);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
expect(handleChange).toHaveBeenCalled();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("invokes onChange when inches picker changes", () => {
|
|
72
|
+
const actionSheetRef = createRef<ActionSheet>();
|
|
73
|
+
const handleChange = mock((_val: string) => {});
|
|
74
|
+
const {UNSAFE_getAllByProps} = render(
|
|
75
|
+
<ThemeProvider>
|
|
76
|
+
<HeightActionSheet actionSheetRef={actionSheetRef} onChange={handleChange} value="73" />
|
|
77
|
+
</ThemeProvider>
|
|
78
|
+
);
|
|
79
|
+
// inches picker has selectedValue "1" (73 % 12)
|
|
80
|
+
const inchPickers = UNSAFE_getAllByProps({selectedValue: "1"});
|
|
81
|
+
const inchPicker = inchPickers.find((p) => typeof p.props.onValueChange === "function");
|
|
82
|
+
act(() => {
|
|
83
|
+
if (inchPicker) {
|
|
84
|
+
inchPicker.props.onValueChange(5);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
expect(handleChange).toHaveBeenCalled();
|
|
88
|
+
});
|
|
34
89
|
});
|
package/src/Icon.tsx
CHANGED
|
@@ -4,9 +4,6 @@ import type {FC} from "react";
|
|
|
4
4
|
import {type IconProps, iconSizeToNumber} from "./Common";
|
|
5
5
|
import {useTheme} from "./Theme";
|
|
6
6
|
|
|
7
|
-
// TODO: Update <Icon /> to be closer to Expo's Vector Icon, letting multiple icon packs be used,
|
|
8
|
-
// etc.
|
|
9
|
-
// TODO: Add documentation for adding FA6-Pro icons.
|
|
10
7
|
export const Icon: FC<IconProps> = ({
|
|
11
8
|
color = "primary",
|
|
12
9
|
size = "md",
|