@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.
Files changed (94) hide show
  1. package/dist/Banner.js +2 -2
  2. package/dist/Banner.js.map +1 -1
  3. package/dist/Common.d.ts +6 -10
  4. package/dist/Common.js.map +1 -1
  5. package/dist/ConsentHistory.d.ts +2 -1
  6. package/dist/DataTable.js +4 -2
  7. package/dist/DataTable.js.map +1 -1
  8. package/dist/DateUtilities.js +16 -7
  9. package/dist/DateUtilities.js.map +1 -1
  10. package/dist/DraggableList.d.ts +5 -4
  11. package/dist/DraggableList.js.map +1 -1
  12. package/dist/Icon.js +0 -3
  13. package/dist/Icon.js.map +1 -1
  14. package/dist/PasswordField.d.ts +1 -1
  15. package/dist/PasswordField.js.map +1 -1
  16. package/dist/TextFieldNumberActionSheet.d.ts +1 -1
  17. package/dist/Toast.d.ts +1 -1
  18. package/dist/Toast.js +2 -2
  19. package/dist/Toast.js.map +1 -1
  20. package/dist/index.d.ts +2 -2
  21. package/package.json +2 -1
  22. package/src/ActionSheet.test.tsx +262 -3
  23. package/src/AddressField.test.tsx +50 -0
  24. package/src/Banner.test.tsx +22 -0
  25. package/src/Banner.tsx +2 -2
  26. package/src/Box.test.tsx +218 -0
  27. package/src/Button.test.tsx +71 -0
  28. package/src/Common.ts +11 -9
  29. package/src/ConsentFormScreen.test.tsx +167 -0
  30. package/src/ConsentHistory.tsx +1 -1
  31. package/src/ConsentNavigator.test.tsx +210 -4
  32. package/src/DataTable.tsx +15 -15
  33. package/src/DateUtilities.test.ts +34 -16
  34. package/src/DateUtilities.tsx +24 -13
  35. package/src/DecimalRangeActionSheet.test.tsx +53 -2
  36. package/src/DraggableList.tsx +5 -5
  37. package/src/EmailField.test.tsx +81 -0
  38. package/src/EmojiSelector.test.tsx +262 -1
  39. package/src/ErrorBoundary.test.tsx +52 -1
  40. package/src/HeightActionSheet.test.tsx +57 -2
  41. package/src/Icon.tsx +0 -3
  42. package/src/InfoModalIcon.test.tsx +16 -0
  43. package/src/InfoTooltipButton.test.tsx +53 -1
  44. package/src/MobileAddressAutoComplete.test.tsx +137 -7
  45. package/src/Modal.test.tsx +188 -0
  46. package/src/NumberPickerActionSheet.test.tsx +59 -2
  47. package/src/OpenAPIContext.test.tsx +184 -3
  48. package/src/Page.test.tsx +162 -1
  49. package/src/Pagination.test.tsx +16 -0
  50. package/src/PasswordField.tsx +1 -1
  51. package/src/PhoneNumberField.test.tsx +46 -9
  52. package/src/PickerSelect.test.tsx +230 -0
  53. package/src/SegmentedControl.test.tsx +38 -0
  54. package/src/SelectBadge.test.tsx +52 -1
  55. package/src/SideDrawer.test.tsx +69 -0
  56. package/src/Signature.test.tsx +42 -5
  57. package/src/SignatureField.test.tsx +35 -0
  58. package/src/Slider.test.tsx +59 -0
  59. package/src/Spinner.test.tsx +6 -0
  60. package/src/SplitPage.test.tsx +228 -2
  61. package/src/TapToEdit.test.tsx +171 -1
  62. package/src/TerrenoProvider.test.tsx +42 -2
  63. package/src/TextFieldNumberActionSheet.tsx +1 -1
  64. package/src/Theme.test.tsx +118 -28
  65. package/src/Toast.test.tsx +95 -2
  66. package/src/Toast.tsx +3 -3
  67. package/src/Tooltip.test.tsx +204 -1
  68. package/src/UnifiedAddressAutoComplete.test.tsx +38 -19
  69. package/src/UserInactivity.test.tsx +73 -1
  70. package/src/Utilities.test.tsx +190 -2
  71. package/src/WebAddressAutocomplete.test.tsx +148 -1
  72. package/src/__snapshots__/ActionSheet.test.tsx.snap +1736 -0
  73. package/src/__snapshots__/Button.test.tsx.snap +68 -0
  74. package/src/__snapshots__/EmojiSelector.test.tsx.snap +1363 -0
  75. package/src/__snapshots__/InfoTooltipButton.test.tsx.snap +72 -3
  76. package/src/__snapshots__/MobileAddressAutoComplete.test.tsx.snap +60 -9
  77. package/src/__snapshots__/Modal.test.tsx.snap +181 -0
  78. package/src/__snapshots__/Page.test.tsx.snap +48 -2
  79. package/src/__snapshots__/PhoneNumberField.test.tsx.snap +0 -93
  80. package/src/__snapshots__/PickerSelect.test.tsx.snap +706 -0
  81. package/src/__snapshots__/SideDrawer.test.tsx.snap +533 -1399
  82. package/src/__snapshots__/Signature.test.tsx.snap +0 -3
  83. package/src/__snapshots__/SplitPage.test.tsx.snap +970 -0
  84. package/src/__snapshots__/UnifiedAddressAutoComplete.test.tsx.snap +220 -4
  85. package/src/__snapshots__/WebAddressAutocomplete.test.tsx.snap +93 -0
  86. package/src/bunSetup.ts +204 -121
  87. package/src/index.tsx +2 -2
  88. package/src/table/TableHeaderCell.test.tsx +142 -0
  89. package/src/table/TableRow.test.tsx +33 -0
  90. package/src/table/__snapshots__/TableRow.test.tsx.snap +403 -0
  91. package/src/table/tableContext.test.tsx +96 -0
  92. package/src/test-utils.tsx +1 -1
  93. package/src/useConsentForms.test.ts +130 -0
  94. package/src/useSubmitConsent.test.ts +64 -0
@@ -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: any) {
61
- throw new Error(`humanDate: ${error.message}`);
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: any) {
95
- throw new Error(`humanDateAndTime: ${error.message}`);
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: any) {
162
- throw new Error(`printDate: ${error.message}`);
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: any) {
217
- throw new Error(`printTime: ${error.message}`);
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: any) {
250
- throw new Error(`printDateAndTime: ${error.message}`);
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: any) {
308
- throw new Error(`printSince: ${error.message}`);
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
- timezones = (Intl as any).supportedValuesOf("timeZone").map((tz: any) => {
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
  });
@@ -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?: any; // Additional styling for item container
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?: any; // Style for the list container
55
+ style?: StyleProp<ViewStyle>; // Style for the list container
56
56
  callbackNewDataIds: (newIds: string[]) => void; // Callback when items are reordered
57
- contentContainerStyle?: any; // Style for the content container
58
- itemContainerStyle?: any; // Style for each item container
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
@@ -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",