@terreno/ui 0.10.0 → 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 (71) hide show
  1. package/dist/Banner.js +2 -2
  2. package/dist/Banner.js.map +1 -1
  3. package/dist/TextFieldNumberActionSheet.d.ts +1 -1
  4. package/dist/Toast.d.ts +1 -1
  5. package/dist/Toast.js +2 -2
  6. package/dist/Toast.js.map +1 -1
  7. package/dist/index.d.ts +2 -2
  8. package/package.json +2 -1
  9. package/src/ActionSheet.test.tsx +262 -3
  10. package/src/AddressField.test.tsx +50 -0
  11. package/src/Banner.test.tsx +22 -0
  12. package/src/Banner.tsx +2 -2
  13. package/src/Box.test.tsx +218 -0
  14. package/src/Button.test.tsx +71 -0
  15. package/src/ConsentFormScreen.test.tsx +167 -0
  16. package/src/ConsentNavigator.test.tsx +206 -0
  17. package/src/DecimalRangeActionSheet.test.tsx +53 -2
  18. package/src/EmailField.test.tsx +81 -0
  19. package/src/EmojiSelector.test.tsx +262 -1
  20. package/src/HeightActionSheet.test.tsx +57 -2
  21. package/src/InfoModalIcon.test.tsx +16 -0
  22. package/src/InfoTooltipButton.test.tsx +53 -1
  23. package/src/MobileAddressAutoComplete.test.tsx +137 -7
  24. package/src/Modal.test.tsx +188 -0
  25. package/src/NumberPickerActionSheet.test.tsx +59 -2
  26. package/src/Page.test.tsx +162 -1
  27. package/src/Pagination.test.tsx +16 -0
  28. package/src/PhoneNumberField.test.tsx +46 -9
  29. package/src/PickerSelect.test.tsx +230 -0
  30. package/src/SegmentedControl.test.tsx +38 -0
  31. package/src/SelectBadge.test.tsx +52 -1
  32. package/src/SideDrawer.test.tsx +69 -0
  33. package/src/Signature.test.tsx +42 -5
  34. package/src/SignatureField.test.tsx +35 -0
  35. package/src/Slider.test.tsx +59 -0
  36. package/src/Spinner.test.tsx +6 -0
  37. package/src/SplitPage.test.tsx +228 -2
  38. package/src/TapToEdit.test.tsx +171 -1
  39. package/src/TerrenoProvider.test.tsx +42 -2
  40. package/src/TextFieldNumberActionSheet.tsx +1 -1
  41. package/src/Theme.test.tsx +118 -28
  42. package/src/Toast.test.tsx +95 -2
  43. package/src/Toast.tsx +3 -3
  44. package/src/Tooltip.test.tsx +204 -1
  45. package/src/UnifiedAddressAutoComplete.test.tsx +38 -19
  46. package/src/UserInactivity.test.tsx +73 -1
  47. package/src/Utilities.test.tsx +190 -2
  48. package/src/WebAddressAutocomplete.test.tsx +148 -1
  49. package/src/__snapshots__/ActionSheet.test.tsx.snap +1736 -0
  50. package/src/__snapshots__/Button.test.tsx.snap +68 -0
  51. package/src/__snapshots__/EmojiSelector.test.tsx.snap +1363 -0
  52. package/src/__snapshots__/InfoTooltipButton.test.tsx.snap +72 -3
  53. package/src/__snapshots__/MobileAddressAutoComplete.test.tsx.snap +60 -9
  54. package/src/__snapshots__/Modal.test.tsx.snap +181 -0
  55. package/src/__snapshots__/Page.test.tsx.snap +48 -2
  56. package/src/__snapshots__/PhoneNumberField.test.tsx.snap +0 -93
  57. package/src/__snapshots__/PickerSelect.test.tsx.snap +706 -0
  58. package/src/__snapshots__/SideDrawer.test.tsx.snap +533 -1399
  59. package/src/__snapshots__/Signature.test.tsx.snap +0 -3
  60. package/src/__snapshots__/SplitPage.test.tsx.snap +970 -0
  61. package/src/__snapshots__/UnifiedAddressAutoComplete.test.tsx.snap +220 -4
  62. package/src/__snapshots__/WebAddressAutocomplete.test.tsx.snap +93 -0
  63. package/src/bunSetup.ts +204 -121
  64. package/src/index.tsx +2 -2
  65. package/src/table/TableHeaderCell.test.tsx +142 -0
  66. package/src/table/TableRow.test.tsx +33 -0
  67. package/src/table/__snapshots__/TableRow.test.tsx.snap +403 -0
  68. package/src/table/tableContext.test.tsx +96 -0
  69. package/src/test-utils.tsx +1 -1
  70. package/src/useConsentForms.test.ts +130 -0
  71. package/src/useSubmitConsent.test.ts +64 -0
@@ -1,4 +1,5 @@
1
1
  import {describe, expect, it, mock} from "bun:test";
2
+ import {act, fireEvent} from "@testing-library/react-native";
2
3
 
3
4
  import {RNPickerSelect} from "./PickerSelect";
4
5
  import {renderWithTheme} from "./test-utils";
@@ -41,6 +42,50 @@ describe("PickerSelect", () => {
41
42
  expect(toJSON()).toMatchSnapshot();
42
43
  });
43
44
 
45
+ it("matches items by itemKey", () => {
46
+ const items = [
47
+ {key: "k1", label: "Option 1", value: "1"},
48
+ {key: "k2", label: "Option 2", value: "2"},
49
+ ];
50
+ const {toJSON} = renderWithTheme(
51
+ <RNPickerSelect
52
+ {...defaultProps}
53
+ itemKey="k2"
54
+ items={items}
55
+ placeholder={{label: "Select", value: ""}}
56
+ />
57
+ );
58
+ expect(toJSON()).toMatchSnapshot();
59
+ });
60
+
61
+ it("renders children when provided", () => {
62
+ const {getByTestId} = renderWithTheme(
63
+ <RNPickerSelect {...defaultProps}>
64
+ <>Custom child content</>
65
+ </RNPickerSelect>
66
+ );
67
+ expect(getByTestId).toBeDefined();
68
+ });
69
+
70
+ it("renders custom InputAccessoryView", () => {
71
+ const CustomAccessory = () => null;
72
+ const {toJSON} = renderWithTheme(
73
+ <RNPickerSelect {...defaultProps} InputAccessoryView={CustomAccessory} />
74
+ );
75
+ expect(toJSON()).toMatchSnapshot();
76
+ });
77
+
78
+ it("passes textInputProps to TextInput", () => {
79
+ const {toJSON} = renderWithTheme(
80
+ <RNPickerSelect
81
+ {...defaultProps}
82
+ textInputProps={{placeholder: "Custom placeholder"}}
83
+ value="1"
84
+ />
85
+ );
86
+ expect(toJSON()).toMatchSnapshot();
87
+ });
88
+
44
89
  it("calls onValueChange when value changes", () => {
45
90
  const mockOnValueChange = mock(() => {});
46
91
  renderWithTheme(
@@ -49,4 +94,189 @@ describe("PickerSelect", () => {
49
94
  // The component is rendered, onValueChange would be called on user interaction
50
95
  expect(mockOnValueChange).toBeDefined();
51
96
  });
97
+
98
+ it("does not crash when value does not match any item", () => {
99
+ const {toJSON} = renderWithTheme(<RNPickerSelect {...defaultProps} value="nonexistent" />);
100
+ expect(toJSON()).toBeTruthy();
101
+ });
102
+
103
+ it("accepts onOpen, onClose, onDonePress callbacks", () => {
104
+ const onOpen = mock(() => {});
105
+ const onClose = mock(() => {});
106
+ const onDonePress = mock(() => {});
107
+ const {toJSON} = renderWithTheme(
108
+ <RNPickerSelect
109
+ {...defaultProps}
110
+ onClose={onClose}
111
+ onDonePress={onDonePress}
112
+ onOpen={onOpen}
113
+ />
114
+ );
115
+ expect(toJSON()).toBeTruthy();
116
+ });
117
+
118
+ it("accepts onUpArrow and onDownArrow callbacks", () => {
119
+ const onUpArrow = mock(() => {});
120
+ const onDownArrow = mock(() => {});
121
+ const {toJSON} = renderWithTheme(
122
+ <RNPickerSelect {...defaultProps} onDownArrow={onDownArrow} onUpArrow={onUpArrow} />
123
+ );
124
+ expect(toJSON()).toBeTruthy();
125
+ });
126
+
127
+ it("forwards touchableDoneProps and touchableWrapperProps", () => {
128
+ const {toJSON} = renderWithTheme(
129
+ <RNPickerSelect
130
+ {...defaultProps}
131
+ touchableDoneProps={{testID: "custom_done"}}
132
+ touchableWrapperProps={{testID: "custom_wrapper"}}
133
+ />
134
+ );
135
+ expect(toJSON()).toBeTruthy();
136
+ });
137
+
138
+ it("renders without crashing when fixAndroidTouchableBug is true", () => {
139
+ const {toJSON} = renderWithTheme(<RNPickerSelect {...defaultProps} fixAndroidTouchableBug />);
140
+ expect(toJSON()).toBeTruthy();
141
+ });
142
+
143
+ it("renders without crashing when useNativeAndroidPickerStyle is false", () => {
144
+ const {toJSON} = renderWithTheme(
145
+ <RNPickerSelect {...defaultProps} useNativeAndroidPickerStyle={false} />
146
+ );
147
+ expect(toJSON()).toBeTruthy();
148
+ });
149
+
150
+ it("renders with modalProps", () => {
151
+ const {toJSON} = renderWithTheme(
152
+ <RNPickerSelect {...defaultProps} modalProps={{animationType: "fade"}} />
153
+ );
154
+ expect(toJSON()).toBeTruthy();
155
+ });
156
+
157
+ it("updates selected item when value prop changes", () => {
158
+ const {rerender, toJSON} = renderWithTheme(<RNPickerSelect {...defaultProps} value="1" />);
159
+ rerender(<RNPickerSelect {...defaultProps} value="3" />);
160
+ expect(toJSON()).toBeTruthy();
161
+ });
162
+
163
+ describe("interactions on iOS", () => {
164
+ it("fires onOpen when the iOS wrapper is pressed and onClose when Done is pressed", async () => {
165
+ const onOpen = mock(() => {});
166
+ const onClose = mock(() => {});
167
+ const onDonePress = mock(() => {});
168
+ const {getByTestId} = renderWithTheme(
169
+ <RNPickerSelect
170
+ {...defaultProps}
171
+ onClose={onClose}
172
+ onDonePress={onDonePress}
173
+ onOpen={onOpen}
174
+ />
175
+ );
176
+
177
+ await act(async () => {
178
+ fireEvent.press(getByTestId("ios_touchable_wrapper"));
179
+ });
180
+ expect(onOpen).toHaveBeenCalled();
181
+
182
+ await act(async () => {
183
+ fireEvent.press(getByTestId("done_button"));
184
+ });
185
+ expect(onClose).toHaveBeenCalled();
186
+ expect(onDonePress).toHaveBeenCalled();
187
+ });
188
+
189
+ it("invokes up/down arrow callbacks when their touchables are pressed", async () => {
190
+ const onUpArrow = mock(() => {});
191
+ const onDownArrow = mock(() => {});
192
+ const {getByTestId, UNSAFE_getAllByType} = renderWithTheme(
193
+ <RNPickerSelect {...defaultProps} onDownArrow={onDownArrow} onUpArrow={onUpArrow} />
194
+ );
195
+ // Open the modal so the accessory view is rendered.
196
+ await act(async () => {
197
+ fireEvent.press(getByTestId("ios_touchable_wrapper"));
198
+ });
199
+ const {TouchableOpacity} = require("react-native");
200
+ interface TouchableTestInstance {
201
+ props: {onPress?: () => void};
202
+ }
203
+ const touchables: TouchableTestInstance[] = UNSAFE_getAllByType(TouchableOpacity).filter(
204
+ (t: TouchableTestInstance) => typeof t.props.onPress === "function"
205
+ );
206
+ // The accessory view renders an up-arrow TouchableOpacity then a down-arrow one.
207
+ expect(touchables.length).toBeGreaterThanOrEqual(2);
208
+ await act(async () => {
209
+ touchables[0].props.onPress?.();
210
+ });
211
+ await act(async () => {
212
+ touchables[1].props.onPress?.();
213
+ });
214
+ expect(onUpArrow).toHaveBeenCalled();
215
+ expect(onDownArrow).toHaveBeenCalled();
216
+ });
217
+
218
+ it("toggles the done button press state when pressed in/out", async () => {
219
+ const {getByTestId} = renderWithTheme(<RNPickerSelect {...defaultProps} />);
220
+ await act(async () => {
221
+ fireEvent.press(getByTestId("ios_touchable_wrapper"));
222
+ });
223
+ const doneButton = getByTestId("done_button");
224
+ await act(async () => {
225
+ fireEvent(doneButton, "pressIn");
226
+ });
227
+ await act(async () => {
228
+ fireEvent(doneButton, "pressOut");
229
+ });
230
+ });
231
+
232
+ it("does not open when disabled", async () => {
233
+ const onOpen = mock(() => {});
234
+ const {getByTestId} = renderWithTheme(
235
+ <RNPickerSelect {...defaultProps} disabled onOpen={onOpen} />
236
+ );
237
+ await act(async () => {
238
+ fireEvent.press(getByTestId("ios_touchable_wrapper"));
239
+ });
240
+ expect(onOpen).not.toHaveBeenCalled();
241
+ });
242
+
243
+ it("calls onValueChange and updates selected item when the Picker emits a change", async () => {
244
+ const mockOnValueChange = mock(() => {});
245
+ const {getByTestId} = renderWithTheme(
246
+ <RNPickerSelect {...defaultProps} onValueChange={mockOnValueChange} />
247
+ );
248
+ await act(async () => {
249
+ fireEvent.press(getByTestId("ios_touchable_wrapper"));
250
+ });
251
+ const picker = getByTestId("ios_picker");
252
+ await act(async () => {
253
+ picker.props.onValueChange?.("2", 1);
254
+ });
255
+ expect(mockOnValueChange).toHaveBeenCalledWith("2", 1);
256
+ });
257
+
258
+ it("updates orientation state when the iOS modal rotates", async () => {
259
+ const {getByTestId, toJSON} = renderWithTheme(<RNPickerSelect {...defaultProps} />);
260
+ await act(async () => {
261
+ fireEvent.press(getByTestId("ios_touchable_wrapper"));
262
+ });
263
+ const modal = getByTestId("ios_modal");
264
+ await act(async () => {
265
+ modal.props.onOrientationChange?.({nativeEvent: {orientation: "landscape"}});
266
+ });
267
+ expect(toJSON()).toBeTruthy();
268
+ });
269
+
270
+ it("closes the modal when the top overlay is pressed", async () => {
271
+ const onClose = mock(() => {});
272
+ const {getByTestId} = renderWithTheme(<RNPickerSelect {...defaultProps} onClose={onClose} />);
273
+ await act(async () => {
274
+ fireEvent.press(getByTestId("ios_touchable_wrapper"));
275
+ });
276
+ await act(async () => {
277
+ fireEvent.press(getByTestId("ios_modal_top"));
278
+ });
279
+ expect(onClose).toHaveBeenCalled();
280
+ });
281
+ });
52
282
  });
@@ -81,4 +81,42 @@ describe("SegmentedControl", () => {
81
81
  expect(getByText("Tab 3")).toBeTruthy();
82
82
  expect(toJSON()).toMatchSnapshot();
83
83
  });
84
+
85
+ it("invokes handleNext when next scroll button is pressed", () => {
86
+ const manyItems = ["Tab 1", "Tab 2", "Tab 3", "Tab 4", "Tab 5", "Tab 6"];
87
+ const {UNSAFE_getAllByProps, queryByText} = renderWithTheme(
88
+ <SegmentedControl items={manyItems} maxItems={3} selectedIndex={0} />
89
+ );
90
+ // Find the right chevron (next button) by its icon prop
91
+ const nextIcons = UNSAFE_getAllByProps({iconName: "chevron-right"});
92
+ expect(nextIcons.length).toBeGreaterThan(0);
93
+ const pressable = nextIcons[0].parent;
94
+ expect(pressable).toBeTruthy();
95
+ if (pressable) {
96
+ fireEvent.press(pressable);
97
+ }
98
+ // After pressing next, should display items starting at Tab 4
99
+ expect(queryByText("Tab 4")).toBeTruthy();
100
+ expect(queryByText("Tab 1")).toBeNull();
101
+ });
102
+
103
+ it("invokes handlePrevious when previous scroll button is pressed", () => {
104
+ const manyItems = ["Tab 1", "Tab 2", "Tab 3", "Tab 4", "Tab 5", "Tab 6"];
105
+ const {UNSAFE_getAllByProps, queryByText} = renderWithTheme(
106
+ <SegmentedControl items={manyItems} maxItems={3} selectedIndex={0} />
107
+ );
108
+ const nextIcons = UNSAFE_getAllByProps({iconName: "chevron-right"});
109
+ const nextPressable = nextIcons[0].parent;
110
+ if (nextPressable) {
111
+ fireEvent.press(nextPressable);
112
+ }
113
+ // Now scrolled forward; press previous to go back
114
+ const prevIcons = UNSAFE_getAllByProps({iconName: "chevron-left"});
115
+ const prevPressable = prevIcons[0].parent;
116
+ if (prevPressable) {
117
+ fireEvent.press(prevPressable);
118
+ }
119
+ // Should be back to Tab 1
120
+ expect(queryByText("Tab 1")).toBeTruthy();
121
+ });
84
122
  });
@@ -1,4 +1,5 @@
1
- import {describe, expect, it} from "bun:test";
1
+ import {describe, expect, it, mock} from "bun:test";
2
+ import {act, fireEvent} from "@testing-library/react-native";
2
3
 
3
4
  import {SelectBadge} from "./SelectBadge";
4
5
  import {renderWithTheme} from "./test-utils";
@@ -100,4 +101,54 @@ describe("SelectBadge", () => {
100
101
  );
101
102
  expect(toJSON()).toMatchSnapshot();
102
103
  });
104
+
105
+ it("toggles picker visibility when badge is pressed", () => {
106
+ const {getByLabelText} = renderWithTheme(
107
+ <SelectBadge onChange={() => {}} options={defaultOptions} value="a" />
108
+ );
109
+ const touchable = getByLabelText("Open select badge options");
110
+ expect(() => fireEvent.press(touchable)).not.toThrow();
111
+ });
112
+
113
+ it("invokes onChange when iOS picker value is changed and Save is pressed", () => {
114
+ const handleChange = mock((_val: string) => {});
115
+ const {getByLabelText, UNSAFE_getAllByProps} = renderWithTheme(
116
+ <SelectBadge onChange={handleChange} options={defaultOptions} value="a" />
117
+ );
118
+ // Open the iOS picker modal
119
+ act(() => {
120
+ fireEvent.press(getByLabelText("Open select badge options"));
121
+ });
122
+
123
+ // Change the picker's value via onValueChange
124
+ const pickers = UNSAFE_getAllByProps({selectedValue: "a"});
125
+ const picker = pickers.find((p) => typeof p.props.onValueChange === "function");
126
+ act(() => {
127
+ if (picker) {
128
+ picker.props.onValueChange("b");
129
+ }
130
+ });
131
+
132
+ // Tap Save to commit the change
133
+ act(() => {
134
+ fireEvent.press(getByLabelText("Save selected value"));
135
+ });
136
+ expect(handleChange).toHaveBeenCalledWith("b");
137
+ });
138
+
139
+ it("does not call onChange when iOS picker is dismissed", () => {
140
+ const handleChange = mock((_val: string) => {});
141
+ const {getByLabelText} = renderWithTheme(
142
+ <SelectBadge onChange={handleChange} options={defaultOptions} value="a" />
143
+ );
144
+ // Open the iOS picker modal
145
+ act(() => {
146
+ fireEvent.press(getByLabelText("Open select badge options"));
147
+ });
148
+ // Dismiss the picker by pressing the backdrop
149
+ act(() => {
150
+ fireEvent.press(getByLabelText("Dismiss picker modal"));
151
+ });
152
+ expect(handleChange).not.toHaveBeenCalled();
153
+ });
103
154
  });
@@ -1,4 +1,33 @@
1
1
  import {describe, expect, it, mock} from "bun:test";
2
+ import type {ReactNode} from "react";
3
+ import {Pressable, Text as RNText, View} from "react-native";
4
+
5
+ // Capture the props passed to Drawer so we can exercise the render callbacks.
6
+ interface CapturedDrawerProps {
7
+ onOpen?: () => void;
8
+ onClose?: () => void;
9
+ renderDrawerContent?: () => ReactNode;
10
+ children?: ReactNode;
11
+ }
12
+
13
+ let lastDrawerProps: CapturedDrawerProps | null = null;
14
+ mock.module("react-native-drawer-layout", () => ({
15
+ Drawer: (props: CapturedDrawerProps) => {
16
+ lastDrawerProps = props;
17
+ return (
18
+ <View testID="mock-drawer">
19
+ {props.renderDrawerContent ? props.renderDrawerContent() : null}
20
+ <Pressable onPress={props.onOpen} testID="mock-drawer-open">
21
+ <RNText>open</RNText>
22
+ </Pressable>
23
+ <Pressable onPress={props.onClose} testID="mock-drawer-close">
24
+ <RNText>close</RNText>
25
+ </Pressable>
26
+ {props.children}
27
+ </View>
28
+ );
29
+ },
30
+ }));
2
31
 
3
32
  import {SideDrawer} from "./SideDrawer";
4
33
  import {Text} from "./Text";
@@ -96,4 +125,44 @@ describe("SideDrawer", () => {
96
125
  );
97
126
  expect(toJSON()).toMatchSnapshot();
98
127
  });
128
+
129
+ it("invokes onOpen and onClose when Drawer triggers the callbacks", () => {
130
+ const handleOpen = mock(() => {});
131
+ const handleClose = mock(() => {});
132
+ renderWithTheme(
133
+ <SideDrawer
134
+ isOpen
135
+ onClose={handleClose}
136
+ onOpen={handleOpen}
137
+ renderContent={() => <Text>Drawer body</Text>}
138
+ >
139
+ <Text>Content</Text>
140
+ </SideDrawer>
141
+ );
142
+ lastDrawerProps?.onOpen?.();
143
+ lastDrawerProps?.onClose?.();
144
+ expect(handleOpen).toHaveBeenCalledTimes(1);
145
+ expect(handleClose).toHaveBeenCalledTimes(1);
146
+ });
147
+
148
+ it("renders the drawer content via the render callback", () => {
149
+ const {getByText} = renderWithTheme(
150
+ <SideDrawer isOpen renderContent={() => <Text>Rendered drawer body</Text>}>
151
+ <Text>Main</Text>
152
+ </SideDrawer>
153
+ );
154
+ expect(getByText("Rendered drawer body")).toBeTruthy();
155
+ });
156
+
157
+ it("exercises the default no-op callbacks when onOpen/onClose are omitted", () => {
158
+ renderWithTheme(
159
+ <SideDrawer isOpen renderContent={() => <Text>Default callbacks</Text>}>
160
+ <Text>Content</Text>
161
+ </SideDrawer>
162
+ );
163
+ expect(() => {
164
+ lastDrawerProps?.onOpen?.();
165
+ lastDrawerProps?.onClose?.();
166
+ }).not.toThrow();
167
+ });
99
168
  });
@@ -1,15 +1,26 @@
1
1
  import {describe, expect, it, mock} from "bun:test";
2
- import {forwardRef} from "react";
2
+ import {fireEvent} from "@testing-library/react-native";
3
+ import {forwardRef, useImperativeHandle} from "react";
3
4
  import {View} from "react-native";
4
5
 
5
6
  import {Signature} from "./Signature";
6
7
  import {renderWithTheme} from "./test-utils";
7
8
 
8
- // Mock react-signature-canvas
9
+ const clearMock = mock(() => {});
10
+ let toDataURLReturn: string = "";
11
+ const toDataURLMock = mock(() => toDataURLReturn);
12
+ let lastOnEnd: (() => void) | undefined;
13
+
14
+ // Mock react-signature-canvas so we can exercise the ref methods and onEnd callback.
9
15
  mock.module("react-signature-canvas", () => ({
10
- default: forwardRef(({backgroundColor}: any, ref) => (
11
- <View ref={ref as any} style={{backgroundColor}} testID="signature-canvas" />
12
- )),
16
+ default: forwardRef(({backgroundColor, onEnd}: any, ref) => {
17
+ lastOnEnd = onEnd;
18
+ useImperativeHandle(ref, () => ({
19
+ clear: clearMock,
20
+ toDataURL: toDataURLMock,
21
+ }));
22
+ return <View style={{backgroundColor}} testID="signature-canvas" />;
23
+ }),
13
24
  }));
14
25
 
15
26
  describe("Signature", () => {
@@ -29,4 +40,30 @@ describe("Signature", () => {
29
40
  const {getByText} = renderWithTheme(<Signature onChange={mockOnChange} />);
30
41
  expect(getByText("Clear")).toBeTruthy();
31
42
  });
43
+
44
+ it("calls clear on the signature canvas when Clear is pressed", () => {
45
+ clearMock.mockClear();
46
+ const mockOnChange = mock(() => {});
47
+ const {getByText} = renderWithTheme(<Signature onChange={mockOnChange} />);
48
+ fireEvent.press(getByText("Clear"));
49
+ expect(clearMock).toHaveBeenCalledTimes(1);
50
+ });
51
+
52
+ it("calls onChange with the data URL when a stroke ends", () => {
53
+ toDataURLReturn = "data:image/png;base64,abc";
54
+ const mockOnChange = mock(() => {});
55
+ renderWithTheme(<Signature onChange={mockOnChange} />);
56
+ expect(lastOnEnd).toBeDefined();
57
+ lastOnEnd?.();
58
+ expect(mockOnChange).toHaveBeenCalledWith("data:image/png;base64,abc");
59
+ });
60
+
61
+ it("does not call onChange when toDataURL returns an empty value", () => {
62
+ toDataURLReturn = "";
63
+ const mockOnChange = mock(() => {});
64
+ renderWithTheme(<Signature onChange={mockOnChange} />);
65
+ expect(lastOnEnd).toBeDefined();
66
+ lastOnEnd?.();
67
+ expect(mockOnChange).not.toHaveBeenCalled();
68
+ });
32
69
  });
@@ -1,6 +1,7 @@
1
1
  import {describe, expect, it, mock} from "bun:test";
2
2
  import {forwardRef} from "react";
3
3
  import {View} from "react-native";
4
+ import type {ReactTestInstance} from "react-test-renderer";
4
5
 
5
6
  import {SignatureField} from "./SignatureField";
6
7
  import {renderWithTheme} from "./test-utils";
@@ -57,4 +58,38 @@ describe("SignatureField", () => {
57
58
  );
58
59
  expect(getByText("Signature is required")).toBeTruthy();
59
60
  });
61
+
62
+ it("invokes onStart and onEnd callbacks via the Signature component", () => {
63
+ const onStart = mock(() => {});
64
+ const onEnd = mock(() => {});
65
+ const onChange = mock((_v: string) => {});
66
+
67
+ const {UNSAFE_getAllByProps} = renderWithTheme(
68
+ <SignatureField onChange={onChange} onEnd={onEnd} onStart={onStart} />
69
+ );
70
+
71
+ // Find the inner Signature wrapper by looking for elements with onStart
72
+ const elementsWithOnStart = UNSAFE_getAllByProps({}).filter(
73
+ (el: ReactTestInstance) => typeof el.props?.onStart === "function"
74
+ );
75
+ expect(elementsWithOnStart.length).toBeGreaterThan(0);
76
+ const signatureEl = elementsWithOnStart[0];
77
+ signatureEl.props.onStart();
78
+ signatureEl.props.onEnd();
79
+ expect(onStart).toHaveBeenCalled();
80
+ expect(onEnd).toHaveBeenCalled();
81
+ });
82
+
83
+ it("does not crash when onStart/onEnd are not provided", () => {
84
+ const onChange = mock((_v: string) => {});
85
+ const {UNSAFE_getAllByProps} = renderWithTheme(<SignatureField onChange={onChange} />);
86
+
87
+ const elementsWithOnStart = UNSAFE_getAllByProps({}).filter(
88
+ (el: ReactTestInstance) => typeof el.props?.onStart === "function"
89
+ );
90
+ expect(elementsWithOnStart.length).toBeGreaterThan(0);
91
+ const signatureEl = elementsWithOnStart[0];
92
+ expect(() => signatureEl.props.onStart()).not.toThrow();
93
+ expect(() => signatureEl.props.onEnd()).not.toThrow();
94
+ });
60
95
  });
@@ -244,4 +244,63 @@ describe("Slider", () => {
244
244
  expect(toJSON()).toMatchSnapshot();
245
245
  });
246
246
  });
247
+
248
+ describe("advanced branches", () => {
249
+ it("renders icon-based value mapping when useIcons is true", () => {
250
+ const mockOnChange = mock(() => {});
251
+ const {toJSON} = renderWithTheme(
252
+ <Slider
253
+ maximumValue={2}
254
+ minimumValue={0}
255
+ onChange={mockOnChange}
256
+ showSelection
257
+ step={1}
258
+ useIcons
259
+ value={1}
260
+ valueMapping={[
261
+ {label: "face-frown", size: "md", value: 0},
262
+ {label: "face-meh", size: "lg", value: 1},
263
+ {label: "face-smile", size: "xl", value: 2},
264
+ ]}
265
+ />
266
+ );
267
+ expect(toJSON()).toBeTruthy();
268
+ });
269
+
270
+ it("renders custom labels alongside min and max labels", () => {
271
+ const mockOnChange = mock(() => {});
272
+ const {getByText} = renderWithTheme(
273
+ <Slider
274
+ labels={{
275
+ custom: [{label: "Middle", value: 50}],
276
+ max: "High",
277
+ min: "Low",
278
+ }}
279
+ maximumValue={100}
280
+ minimumValue={0}
281
+ onChange={mockOnChange}
282
+ value={50}
283
+ />
284
+ );
285
+ expect(getByText("Low")).toBeTruthy();
286
+ expect(getByText("Middle")).toBeTruthy();
287
+ expect(getByText("High")).toBeTruthy();
288
+ });
289
+
290
+ it("falls back to plain numeric formatter when valueMapping is empty", () => {
291
+ const mockOnChange = mock(() => {});
292
+ const {getByText} = renderWithTheme(
293
+ <Slider
294
+ maximumValue={10}
295
+ minimumValue={0}
296
+ onChange={mockOnChange}
297
+ showSelection
298
+ step={1}
299
+ value={7}
300
+ valueMapping={[]}
301
+ />
302
+ );
303
+ expect(getByText("7")).toBeTruthy();
304
+ });
305
+ });
247
306
  });
@@ -71,4 +71,10 @@ describe("Spinner", () => {
71
71
 
72
72
  expect(toJSON()).toMatchSnapshot();
73
73
  });
74
+
75
+ it("clears the delay timer on unmount", () => {
76
+ const {unmount, toJSON} = renderWithTheme(<Spinner />);
77
+ expect(toJSON()).toBeNull();
78
+ expect(() => unmount()).not.toThrow();
79
+ });
74
80
  });