@terreno/ui 0.10.0 → 0.11.1
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 +7 -5
- package/dist/Banner.js.map +1 -1
- package/dist/Common.d.ts +3 -1
- package/dist/Common.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 +65 -0
- package/src/Banner.tsx +7 -5
- package/src/Box.test.tsx +218 -0
- package/src/Button.test.tsx +71 -0
- package/src/Common.ts +3 -1
- package/src/ConsentFormScreen.test.tsx +167 -0
- package/src/ConsentNavigator.test.tsx +206 -0
- package/src/DecimalRangeActionSheet.test.tsx +53 -2
- package/src/EmailField.test.tsx +81 -0
- package/src/EmojiSelector.test.tsx +262 -1
- package/src/HeightActionSheet.test.tsx +57 -2
- 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/Page.test.tsx +162 -1
- package/src/Pagination.test.tsx +16 -0
- 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
|
@@ -1,17 +1,79 @@
|
|
|
1
1
|
import {describe, expect, it, mock} from "bun:test";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import {fireEvent} from "@testing-library/react-native";
|
|
3
|
+
import {forwardRef, useImperativeHandle, useRef} from "react";
|
|
4
|
+
import {Pressable, Text, View} from "react-native";
|
|
4
5
|
|
|
5
6
|
import {MobileAddressAutocomplete} from "./MobileAddressAutoComplete";
|
|
6
7
|
import {renderWithTheme} from "./test-utils";
|
|
7
8
|
|
|
9
|
+
// Capture the props passed to GooglePlacesAutocomplete so we can exercise the inline
|
|
10
|
+
// callbacks (onPress, onFocus, onBlur, onChange, textInputProps, etc.)
|
|
11
|
+
interface CapturedGooglePlacesProps {
|
|
12
|
+
placeholder?: string;
|
|
13
|
+
textInputProps?: {
|
|
14
|
+
onFocus?: () => void;
|
|
15
|
+
onBlur?: () => void;
|
|
16
|
+
onChange?: (event: {nativeEvent: {text: string}}) => void;
|
|
17
|
+
};
|
|
18
|
+
onPress?: (
|
|
19
|
+
data: {description: string},
|
|
20
|
+
details: {
|
|
21
|
+
address_components: {
|
|
22
|
+
long_name: string;
|
|
23
|
+
short_name: string;
|
|
24
|
+
types: string[];
|
|
25
|
+
}[];
|
|
26
|
+
}
|
|
27
|
+
) => void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let lastGooglePlacesProps: CapturedGooglePlacesProps | null = null;
|
|
31
|
+
const setAddressTextSpy = mock(() => {});
|
|
32
|
+
|
|
8
33
|
// Mock react-native-google-places-autocomplete
|
|
9
34
|
mock.module("react-native-google-places-autocomplete", () => ({
|
|
10
|
-
GooglePlacesAutocomplete: forwardRef((
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
35
|
+
GooglePlacesAutocomplete: forwardRef((props: CapturedGooglePlacesProps, ref) => {
|
|
36
|
+
lastGooglePlacesProps = props;
|
|
37
|
+
const innerRef = useRef<Record<string, unknown>>({});
|
|
38
|
+
useImperativeHandle(ref, () => ({
|
|
39
|
+
setAddressText: setAddressTextSpy,
|
|
40
|
+
...innerRef.current,
|
|
41
|
+
}));
|
|
42
|
+
return (
|
|
43
|
+
<View testID="google-places-autocomplete">
|
|
44
|
+
<Text>{props.placeholder}</Text>
|
|
45
|
+
<Pressable
|
|
46
|
+
onPress={() =>
|
|
47
|
+
props.onPress?.(
|
|
48
|
+
{description: "123 Main St"},
|
|
49
|
+
{
|
|
50
|
+
address_components: [
|
|
51
|
+
{long_name: "123", short_name: "123", types: ["street_number"]},
|
|
52
|
+
{long_name: "Main St", short_name: "Main St", types: ["route"]},
|
|
53
|
+
{long_name: "San Francisco", short_name: "SF", types: ["locality"]},
|
|
54
|
+
{
|
|
55
|
+
long_name: "California",
|
|
56
|
+
short_name: "CA",
|
|
57
|
+
types: ["administrative_area_level_1"],
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
long_name: "San Francisco County",
|
|
61
|
+
short_name: "SF County",
|
|
62
|
+
types: ["administrative_area_level_2"],
|
|
63
|
+
},
|
|
64
|
+
{long_name: "United States", short_name: "US", types: ["country"]},
|
|
65
|
+
{long_name: "94105", short_name: "94105", types: ["postal_code"]},
|
|
66
|
+
],
|
|
67
|
+
}
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
testID="mock-google-places-select"
|
|
71
|
+
>
|
|
72
|
+
<Text>Select</Text>
|
|
73
|
+
</Pressable>
|
|
74
|
+
</View>
|
|
75
|
+
);
|
|
76
|
+
}),
|
|
15
77
|
}));
|
|
16
78
|
|
|
17
79
|
describe("MobileAddressAutocomplete", () => {
|
|
@@ -55,4 +117,72 @@ describe("MobileAddressAutocomplete", () => {
|
|
|
55
117
|
);
|
|
56
118
|
expect(toJSON()).toMatchSnapshot();
|
|
57
119
|
});
|
|
120
|
+
|
|
121
|
+
it("invokes handleAutoCompleteChange with processed address components", () => {
|
|
122
|
+
const handleAutoCompleteChange = mock(() => {});
|
|
123
|
+
const handleAddressChange = mock(() => {});
|
|
124
|
+
setAddressTextSpy.mockClear();
|
|
125
|
+
const {getByTestId} = renderWithTheme(
|
|
126
|
+
<MobileAddressAutocomplete
|
|
127
|
+
googleMapsApiKey="test-api-key"
|
|
128
|
+
handleAddressChange={handleAddressChange}
|
|
129
|
+
handleAutoCompleteChange={handleAutoCompleteChange}
|
|
130
|
+
includeCounty
|
|
131
|
+
inputValue=""
|
|
132
|
+
/>
|
|
133
|
+
);
|
|
134
|
+
fireEvent.press(getByTestId("mock-google-places-select"));
|
|
135
|
+
expect(handleAutoCompleteChange).toHaveBeenCalled();
|
|
136
|
+
const payload = handleAutoCompleteChange.mock.calls[0][0] as {address1?: string};
|
|
137
|
+
expect(payload.address1).toContain("Main St");
|
|
138
|
+
expect(setAddressTextSpy).toHaveBeenCalled();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("fires onFocus, onBlur and onChange via textInputProps callbacks", () => {
|
|
142
|
+
const handleAddressChange = mock(() => {});
|
|
143
|
+
renderWithTheme(
|
|
144
|
+
<MobileAddressAutocomplete
|
|
145
|
+
googleMapsApiKey="test-api-key"
|
|
146
|
+
handleAddressChange={handleAddressChange}
|
|
147
|
+
handleAutoCompleteChange={() => {}}
|
|
148
|
+
inputValue=""
|
|
149
|
+
/>
|
|
150
|
+
);
|
|
151
|
+
const tip = lastGooglePlacesProps?.textInputProps;
|
|
152
|
+
expect(typeof tip?.onFocus).toBe("function");
|
|
153
|
+
expect(typeof tip?.onBlur).toBe("function");
|
|
154
|
+
expect(typeof tip?.onChange).toBe("function");
|
|
155
|
+
tip?.onFocus?.();
|
|
156
|
+
tip?.onBlur?.();
|
|
157
|
+
tip?.onChange?.({nativeEvent: {text: "456 Oak Ave"}});
|
|
158
|
+
expect(handleAddressChange).toHaveBeenCalledWith("456 Oak Ave");
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("falls back to the TextField and propagates its onChange without an API key", () => {
|
|
162
|
+
const handleAddressChange = mock(() => {});
|
|
163
|
+
const {UNSAFE_root} = renderWithTheme(
|
|
164
|
+
<MobileAddressAutocomplete
|
|
165
|
+
handleAddressChange={handleAddressChange}
|
|
166
|
+
handleAutoCompleteChange={() => {}}
|
|
167
|
+
inputValue=""
|
|
168
|
+
testID="mobile-fallback"
|
|
169
|
+
/>
|
|
170
|
+
);
|
|
171
|
+
expect(UNSAFE_root).toBeTruthy();
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("wrapping TouchableOpacity clears focus when pressed", () => {
|
|
175
|
+
const {UNSAFE_getAllByType} = renderWithTheme(
|
|
176
|
+
<MobileAddressAutocomplete
|
|
177
|
+
googleMapsApiKey="test-api-key"
|
|
178
|
+
handleAddressChange={() => {}}
|
|
179
|
+
handleAutoCompleteChange={() => {}}
|
|
180
|
+
inputValue=""
|
|
181
|
+
/>
|
|
182
|
+
);
|
|
183
|
+
const {TouchableOpacity} = require("react-native");
|
|
184
|
+
const [wrapper] = UNSAFE_getAllByType(TouchableOpacity);
|
|
185
|
+
expect(wrapper).toBeTruthy();
|
|
186
|
+
expect(() => wrapper.props.onPress?.()).not.toThrow();
|
|
187
|
+
});
|
|
58
188
|
});
|
package/src/Modal.test.tsx
CHANGED
|
@@ -5,6 +5,16 @@ import {Modal} from "./Modal";
|
|
|
5
5
|
import {Text} from "./Text";
|
|
6
6
|
import {renderWithTheme} from "./test-utils";
|
|
7
7
|
|
|
8
|
+
// Minimal shape of a test instance returned by UNSAFE_getAllByType that we rely on here.
|
|
9
|
+
interface PressableTestInstance {
|
|
10
|
+
props: {
|
|
11
|
+
style?:
|
|
12
|
+
| {backgroundColor?: string; cursor?: string}
|
|
13
|
+
| {backgroundColor?: string; cursor?: string}[];
|
|
14
|
+
onPress?: (event?: {stopPropagation?: () => void}) => void;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
8
18
|
describe("Modal", () => {
|
|
9
19
|
it("renders correctly when visible", () => {
|
|
10
20
|
const {toJSON} = renderWithTheme(
|
|
@@ -166,4 +176,182 @@ describe("Modal", () => {
|
|
|
166
176
|
);
|
|
167
177
|
expect(toJSON()).toMatchSnapshot();
|
|
168
178
|
});
|
|
179
|
+
|
|
180
|
+
it("renders primary button with click handler", () => {
|
|
181
|
+
const handlePrimary = mock(() => {});
|
|
182
|
+
const {getByText} = renderWithTheme(
|
|
183
|
+
<Modal
|
|
184
|
+
onDismiss={() => {}}
|
|
185
|
+
primaryButtonOnClick={handlePrimary}
|
|
186
|
+
primaryButtonText="Confirm"
|
|
187
|
+
title="Title"
|
|
188
|
+
visible
|
|
189
|
+
>
|
|
190
|
+
<Text>Content</Text>
|
|
191
|
+
</Modal>
|
|
192
|
+
);
|
|
193
|
+
expect(getByText("Confirm")).toBeTruthy();
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("renders secondary button with click handler", () => {
|
|
197
|
+
const handleSecondary = mock(() => {});
|
|
198
|
+
const {getByText} = renderWithTheme(
|
|
199
|
+
<Modal
|
|
200
|
+
onDismiss={() => {}}
|
|
201
|
+
secondaryButtonOnClick={handleSecondary}
|
|
202
|
+
secondaryButtonText="Cancel"
|
|
203
|
+
title="Title"
|
|
204
|
+
visible
|
|
205
|
+
>
|
|
206
|
+
<Text>Content</Text>
|
|
207
|
+
</Modal>
|
|
208
|
+
);
|
|
209
|
+
expect(getByText("Cancel")).toBeTruthy();
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it("does not call primaryButtonOnClick when not visible", () => {
|
|
213
|
+
const handlePrimary = mock(() => {});
|
|
214
|
+
renderWithTheme(
|
|
215
|
+
<Modal
|
|
216
|
+
onDismiss={() => {}}
|
|
217
|
+
primaryButtonOnClick={handlePrimary}
|
|
218
|
+
primaryButtonText="Confirm"
|
|
219
|
+
title="Title"
|
|
220
|
+
visible={false}
|
|
221
|
+
>
|
|
222
|
+
<Text>Content</Text>
|
|
223
|
+
</Modal>
|
|
224
|
+
);
|
|
225
|
+
expect(handlePrimary).not.toHaveBeenCalled();
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("renders with persistOnBackgroundClick", () => {
|
|
229
|
+
const {toJSON} = renderWithTheme(
|
|
230
|
+
<Modal onDismiss={() => {}} persistOnBackgroundClick title="Persistent" visible>
|
|
231
|
+
<Text>Content</Text>
|
|
232
|
+
</Modal>
|
|
233
|
+
);
|
|
234
|
+
expect(toJSON()).toMatchSnapshot();
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it("does not call onDismiss when visible is false and close is pressed", () => {
|
|
238
|
+
const handleDismiss = mock(() => {});
|
|
239
|
+
renderWithTheme(
|
|
240
|
+
<Modal onDismiss={handleDismiss} title="Hidden" visible={false}>
|
|
241
|
+
<Text>Content</Text>
|
|
242
|
+
</Modal>
|
|
243
|
+
);
|
|
244
|
+
expect(handleDismiss).not.toHaveBeenCalled();
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it("renders transitioning from hidden to visible", () => {
|
|
248
|
+
const {rerender, toJSON} = renderWithTheme(
|
|
249
|
+
<Modal onDismiss={() => {}} title="Toggle" visible={false}>
|
|
250
|
+
<Text>Content</Text>
|
|
251
|
+
</Modal>
|
|
252
|
+
);
|
|
253
|
+
rerender(
|
|
254
|
+
<Modal onDismiss={() => {}} title="Toggle" visible>
|
|
255
|
+
<Text>Content</Text>
|
|
256
|
+
</Modal>
|
|
257
|
+
);
|
|
258
|
+
expect(toJSON()).toBeTruthy();
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it("invokes primaryButtonOnClick when primary button pressed while visible", async () => {
|
|
262
|
+
const handlePrimary = mock(() => {});
|
|
263
|
+
const {getByText} = renderWithTheme(
|
|
264
|
+
<Modal
|
|
265
|
+
onDismiss={() => {}}
|
|
266
|
+
primaryButtonOnClick={handlePrimary}
|
|
267
|
+
primaryButtonText="Submit"
|
|
268
|
+
title="Title"
|
|
269
|
+
visible
|
|
270
|
+
>
|
|
271
|
+
<Text>Content</Text>
|
|
272
|
+
</Modal>
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
await new Promise((resolve) => {
|
|
276
|
+
fireEvent.press(getByText("Submit"));
|
|
277
|
+
setTimeout(resolve, 600);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
expect(handlePrimary).toHaveBeenCalled();
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it("invokes secondaryButtonOnClick when secondary button pressed while visible", async () => {
|
|
284
|
+
const handleSecondary = mock(() => {});
|
|
285
|
+
const {getByText} = renderWithTheme(
|
|
286
|
+
<Modal
|
|
287
|
+
onDismiss={() => {}}
|
|
288
|
+
secondaryButtonOnClick={handleSecondary}
|
|
289
|
+
secondaryButtonText="Cancel"
|
|
290
|
+
title="Title"
|
|
291
|
+
visible
|
|
292
|
+
>
|
|
293
|
+
<Text>Content</Text>
|
|
294
|
+
</Modal>
|
|
295
|
+
);
|
|
296
|
+
|
|
297
|
+
await new Promise((resolve) => {
|
|
298
|
+
fireEvent.press(getByText("Cancel"));
|
|
299
|
+
setTimeout(resolve, 600);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
expect(handleSecondary).toHaveBeenCalled();
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it("dismisses when the backdrop is pressed and persistOnBackgroundClick is false", () => {
|
|
306
|
+
const handleDismiss = mock(() => {});
|
|
307
|
+
const {UNSAFE_getAllByType} = renderWithTheme(
|
|
308
|
+
<Modal onDismiss={handleDismiss} title="Title" visible>
|
|
309
|
+
<Text>Content</Text>
|
|
310
|
+
</Modal>
|
|
311
|
+
);
|
|
312
|
+
// Find the backdrop Pressable (first Pressable in tree with a style that includes backgroundColor).
|
|
313
|
+
const {Pressable} = require("react-native");
|
|
314
|
+
const pressables: PressableTestInstance[] = UNSAFE_getAllByType(Pressable);
|
|
315
|
+
const backdrop = pressables.find((node) => {
|
|
316
|
+
const style = node.props.style;
|
|
317
|
+
if (Array.isArray(style)) {
|
|
318
|
+
return style.some((s) => s?.backgroundColor?.includes?.("rgba"));
|
|
319
|
+
}
|
|
320
|
+
return style?.backgroundColor?.includes?.("rgba");
|
|
321
|
+
});
|
|
322
|
+
expect(backdrop).toBeTruthy();
|
|
323
|
+
backdrop?.props.onPress?.();
|
|
324
|
+
expect(handleDismiss).toHaveBeenCalled();
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it("stops propagation on the inner backdrop wrapper press", () => {
|
|
328
|
+
const stopPropagation = mock(() => {});
|
|
329
|
+
const {UNSAFE_getAllByType} = renderWithTheme(
|
|
330
|
+
<Modal onDismiss={() => {}} title="Title" visible>
|
|
331
|
+
<Text>Content</Text>
|
|
332
|
+
</Modal>
|
|
333
|
+
);
|
|
334
|
+
const {Pressable} = require("react-native");
|
|
335
|
+
const pressables: PressableTestInstance[] = UNSAFE_getAllByType(Pressable);
|
|
336
|
+
// Inner wrapper is the pressable with style {cursor: "auto"}.
|
|
337
|
+
const inner = pressables.find((node) => node.props.style?.cursor === "auto");
|
|
338
|
+
expect(inner).toBeTruthy();
|
|
339
|
+
inner?.props.onPress?.({stopPropagation});
|
|
340
|
+
expect(stopPropagation).toHaveBeenCalled();
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it("does not stop propagation on the inner wrapper when persistOnBackgroundClick is true", () => {
|
|
344
|
+
const stopPropagation = mock(() => {});
|
|
345
|
+
const {UNSAFE_getAllByType} = renderWithTheme(
|
|
346
|
+
<Modal onDismiss={() => {}} persistOnBackgroundClick title="Title" visible>
|
|
347
|
+
<Text>Content</Text>
|
|
348
|
+
</Modal>
|
|
349
|
+
);
|
|
350
|
+
const {Pressable} = require("react-native");
|
|
351
|
+
const pressables: PressableTestInstance[] = UNSAFE_getAllByType(Pressable);
|
|
352
|
+
const inner = pressables.find((node) => node.props.style?.cursor === "auto");
|
|
353
|
+
expect(inner).toBeTruthy();
|
|
354
|
+
inner?.props.onPress?.({stopPropagation});
|
|
355
|
+
expect(stopPropagation).not.toHaveBeenCalled();
|
|
356
|
+
});
|
|
169
357
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
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
|
+
import type {ReactTestInstance} from "react-test-renderer";
|
|
4
5
|
|
|
5
6
|
import type {ActionSheet} from "./ActionSheet";
|
|
6
7
|
import {NumberPickerActionSheet} from "./NumberPickerActionSheet";
|
|
@@ -43,4 +44,60 @@ describe("NumberPickerActionSheet", () => {
|
|
|
43
44
|
);
|
|
44
45
|
expect(toJSON()).toMatchSnapshot();
|
|
45
46
|
});
|
|
47
|
+
|
|
48
|
+
it("invokes onChange when picker value changes", () => {
|
|
49
|
+
const actionSheetRef = createRef<ActionSheet>();
|
|
50
|
+
const handleChange = mock((_val: string) => {});
|
|
51
|
+
const {UNSAFE_getAllByProps} = render(
|
|
52
|
+
<ThemeProvider>
|
|
53
|
+
<NumberPickerActionSheet
|
|
54
|
+
actionSheetRef={actionSheetRef}
|
|
55
|
+
max={10}
|
|
56
|
+
min={0}
|
|
57
|
+
onChange={handleChange}
|
|
58
|
+
value="5"
|
|
59
|
+
/>
|
|
60
|
+
</ThemeProvider>
|
|
61
|
+
);
|
|
62
|
+
const pickers = UNSAFE_getAllByProps({selectedValue: "5"});
|
|
63
|
+
const picker = pickers.find(
|
|
64
|
+
(p: ReactTestInstance) => typeof p.props.onValueChange === "function"
|
|
65
|
+
);
|
|
66
|
+
act(() => {
|
|
67
|
+
if (picker) {
|
|
68
|
+
picker.props.onValueChange(7);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
expect(handleChange).toHaveBeenCalledWith("7");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("closes the action sheet when Close button is pressed", () => {
|
|
75
|
+
const setModalVisible = mock((_v: boolean) => {});
|
|
76
|
+
const actionSheetRef = createRef<ActionSheet>();
|
|
77
|
+
const {UNSAFE_getAllByProps} = render(
|
|
78
|
+
<ThemeProvider>
|
|
79
|
+
<NumberPickerActionSheet
|
|
80
|
+
actionSheetRef={actionSheetRef}
|
|
81
|
+
max={10}
|
|
82
|
+
min={0}
|
|
83
|
+
onChange={() => {}}
|
|
84
|
+
value="5"
|
|
85
|
+
/>
|
|
86
|
+
</ThemeProvider>
|
|
87
|
+
);
|
|
88
|
+
// Replace the ref target with a mock after mount so the Button's onClick
|
|
89
|
+
// invokes our spy instead of the real ActionSheet instance.
|
|
90
|
+
(actionSheetRef as {current: {setModalVisible: typeof setModalVisible}}).current = {
|
|
91
|
+
setModalVisible,
|
|
92
|
+
};
|
|
93
|
+
const closeButtons = UNSAFE_getAllByProps({text: "Close"});
|
|
94
|
+
const closeButton = closeButtons.find(
|
|
95
|
+
(b: ReactTestInstance) => typeof b.props.onClick === "function"
|
|
96
|
+
);
|
|
97
|
+
expect(closeButton).toBeDefined();
|
|
98
|
+
act(() => {
|
|
99
|
+
closeButton?.props.onClick();
|
|
100
|
+
});
|
|
101
|
+
expect(setModalVisible).toHaveBeenCalledWith(false);
|
|
102
|
+
});
|
|
46
103
|
});
|
package/src/Page.test.tsx
CHANGED
|
@@ -1,9 +1,97 @@
|
|
|
1
|
-
import {describe, expect, it, mock} from "bun:test";
|
|
1
|
+
import {afterAll, describe, expect, it, mock} from "bun:test";
|
|
2
|
+
import {act, fireEvent, waitFor} from "@testing-library/react-native";
|
|
3
|
+
import React, {type ReactNode} from "react";
|
|
4
|
+
import {Pressable, Text as RNText} from "react-native";
|
|
5
|
+
|
|
6
|
+
// Override the IconButton mock so the inline onClick arrows fire when pressed.
|
|
7
|
+
mock.module("./IconButton", () => ({
|
|
8
|
+
IconButton: ({
|
|
9
|
+
accessibilityLabel,
|
|
10
|
+
accessibilityHint,
|
|
11
|
+
iconName,
|
|
12
|
+
onClick,
|
|
13
|
+
}: {
|
|
14
|
+
accessibilityLabel?: string;
|
|
15
|
+
accessibilityHint?: string;
|
|
16
|
+
iconName: string;
|
|
17
|
+
onClick?: () => void;
|
|
18
|
+
}) => (
|
|
19
|
+
<Pressable
|
|
20
|
+
accessibilityHint={accessibilityHint}
|
|
21
|
+
accessibilityLabel={accessibilityLabel}
|
|
22
|
+
onPress={onClick}
|
|
23
|
+
testID={`icon-button-${iconName}`}
|
|
24
|
+
>
|
|
25
|
+
<RNText>{iconName}</RNText>
|
|
26
|
+
</Pressable>
|
|
27
|
+
),
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
// Override the expo-router mock so we can observe router.back() calls, but
|
|
31
|
+
// preserve the full shape provided by bunSetup.ts (Link, Stack, Tabs, hooks,
|
|
32
|
+
// and the rest of the router object) so other components that import from
|
|
33
|
+
// "expo-router" don't see `undefined` for those exports.
|
|
34
|
+
const routerBack = mock(() => {});
|
|
35
|
+
interface MockChildrenProps {
|
|
36
|
+
children?: ReactNode;
|
|
37
|
+
}
|
|
38
|
+
mock.module("expo-router", () => ({
|
|
39
|
+
Link: ({children, ...props}: MockChildrenProps) => React.createElement("Link", props, children),
|
|
40
|
+
router: {
|
|
41
|
+
back: routerBack,
|
|
42
|
+
canGoBack: mock(() => true),
|
|
43
|
+
navigate: mock(() => {}),
|
|
44
|
+
push: mock(() => {}),
|
|
45
|
+
replace: mock(() => {}),
|
|
46
|
+
},
|
|
47
|
+
Stack: ({children, ...props}: MockChildrenProps) => React.createElement("Stack", props, children),
|
|
48
|
+
Tabs: ({children, ...props}: MockChildrenProps) => React.createElement("Tabs", props, children),
|
|
49
|
+
useLocalSearchParams: mock(() => ({})),
|
|
50
|
+
useRouter: mock(() => ({
|
|
51
|
+
back: mock(() => {}),
|
|
52
|
+
canGoBack: mock(() => true),
|
|
53
|
+
navigate: mock(() => {}),
|
|
54
|
+
push: mock(() => {}),
|
|
55
|
+
replace: mock(() => {}),
|
|
56
|
+
})),
|
|
57
|
+
useSegments: mock(() => []),
|
|
58
|
+
}));
|
|
2
59
|
|
|
3
60
|
import {Page} from "./Page";
|
|
4
61
|
import {Text} from "./Text";
|
|
5
62
|
import {renderWithTheme} from "./test-utils";
|
|
6
63
|
|
|
64
|
+
// Restore the global mocks set up by bunSetup.ts after this file finishes so
|
|
65
|
+
// that other test files (e.g. IconButton.test.tsx, ConsentFormScreen.test.tsx)
|
|
66
|
+
// are not affected by the overrides above.
|
|
67
|
+
afterAll(() => {
|
|
68
|
+
mock.module("./IconButton", () => ({
|
|
69
|
+
IconButton: mock(() => null),
|
|
70
|
+
}));
|
|
71
|
+
mock.module("expo-router", () => ({
|
|
72
|
+
Link: ({children, ...props}: MockChildrenProps) => React.createElement("Link", props, children),
|
|
73
|
+
router: {
|
|
74
|
+
back: mock(() => {}),
|
|
75
|
+
canGoBack: mock(() => true),
|
|
76
|
+
navigate: mock(() => {}),
|
|
77
|
+
push: mock(() => {}),
|
|
78
|
+
replace: mock(() => {}),
|
|
79
|
+
},
|
|
80
|
+
Stack: ({children, ...props}: MockChildrenProps) =>
|
|
81
|
+
React.createElement("Stack", props, children),
|
|
82
|
+
Tabs: ({children, ...props}: MockChildrenProps) => React.createElement("Tabs", props, children),
|
|
83
|
+
useLocalSearchParams: mock(() => ({})),
|
|
84
|
+
useRouter: mock(() => ({
|
|
85
|
+
back: mock(() => {}),
|
|
86
|
+
canGoBack: mock(() => true),
|
|
87
|
+
navigate: mock(() => {}),
|
|
88
|
+
push: mock(() => {}),
|
|
89
|
+
replace: mock(() => {}),
|
|
90
|
+
})),
|
|
91
|
+
useSegments: mock(() => []),
|
|
92
|
+
}));
|
|
93
|
+
});
|
|
94
|
+
|
|
7
95
|
describe("Page", () => {
|
|
8
96
|
const mockNavigation = {
|
|
9
97
|
goBack: mock(() => {}),
|
|
@@ -125,4 +213,77 @@ describe("Page", () => {
|
|
|
125
213
|
);
|
|
126
214
|
expect(toJSON()).toMatchSnapshot();
|
|
127
215
|
});
|
|
216
|
+
|
|
217
|
+
it("invokes rightButtonOnClick when right button is pressed", async () => {
|
|
218
|
+
const handleRightClick = mock(() => {});
|
|
219
|
+
const {getByText} = renderWithTheme(
|
|
220
|
+
<Page
|
|
221
|
+
navigation={mockNavigation}
|
|
222
|
+
rightButton="Save"
|
|
223
|
+
rightButtonOnClick={handleRightClick}
|
|
224
|
+
title="Page"
|
|
225
|
+
>
|
|
226
|
+
<Text>Content</Text>
|
|
227
|
+
</Page>
|
|
228
|
+
);
|
|
229
|
+
await act(async () => {
|
|
230
|
+
fireEvent.press(getByText("Save"));
|
|
231
|
+
await new Promise((resolve) => setTimeout(resolve, 600));
|
|
232
|
+
});
|
|
233
|
+
await waitFor(() => expect(handleRightClick).toHaveBeenCalled());
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it("renders without header when title and backButton are both absent", () => {
|
|
237
|
+
const {queryByText} = renderWithTheme(
|
|
238
|
+
<Page navigation={mockNavigation}>
|
|
239
|
+
<Text>Plain page</Text>
|
|
240
|
+
</Page>
|
|
241
|
+
);
|
|
242
|
+
expect(queryByText("Plain page")).toBeTruthy();
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it("renders loading state with loadingText", () => {
|
|
246
|
+
const {getByText} = renderWithTheme(
|
|
247
|
+
<Page loading loadingText="Loading data..." navigation={mockNavigation}>
|
|
248
|
+
<Text>Content</Text>
|
|
249
|
+
</Page>
|
|
250
|
+
);
|
|
251
|
+
expect(getByText("Loading data...")).toBeTruthy();
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it("invokes router.back when the back button is pressed", () => {
|
|
255
|
+
routerBack.mockClear();
|
|
256
|
+
const {getByTestId} = renderWithTheme(
|
|
257
|
+
<Page backButton navigation={mockNavigation} title="Page">
|
|
258
|
+
<Text>Content</Text>
|
|
259
|
+
</Page>
|
|
260
|
+
);
|
|
261
|
+
fireEvent.press(getByTestId("icon-button-chevron-left"));
|
|
262
|
+
expect(routerBack).toHaveBeenCalled();
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it("invokes router.back when the close button is pressed", () => {
|
|
266
|
+
routerBack.mockClear();
|
|
267
|
+
const {getByTestId} = renderWithTheme(
|
|
268
|
+
<Page closeButton navigation={mockNavigation} title="Page">
|
|
269
|
+
<Text>Content</Text>
|
|
270
|
+
</Page>
|
|
271
|
+
);
|
|
272
|
+
fireEvent.press(getByTestId("icon-button-xmark"));
|
|
273
|
+
expect(routerBack).toHaveBeenCalled();
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it("safely handles a missing rightButtonOnClick callback", async () => {
|
|
277
|
+
const {getByText} = renderWithTheme(
|
|
278
|
+
<Page navigation={mockNavigation} rightButton="Go" title="Page">
|
|
279
|
+
<Text>Content</Text>
|
|
280
|
+
</Page>
|
|
281
|
+
);
|
|
282
|
+
await act(async () => {
|
|
283
|
+
fireEvent.press(getByText("Go"));
|
|
284
|
+
await new Promise((resolve) => setTimeout(resolve, 600));
|
|
285
|
+
});
|
|
286
|
+
// No crash; the optional-chained call handles the missing prop.
|
|
287
|
+
expect(getByText("Go")).toBeTruthy();
|
|
288
|
+
});
|
|
128
289
|
});
|
package/src/Pagination.test.tsx
CHANGED
|
@@ -83,4 +83,20 @@ describe("Pagination", () => {
|
|
|
83
83
|
const {toJSON} = renderWithTheme(<Pagination page={2} setPage={() => {}} totalPages={20} />);
|
|
84
84
|
expect(toJSON()).toMatchSnapshot();
|
|
85
85
|
});
|
|
86
|
+
|
|
87
|
+
it("renders 'more' button for large page sets without throwing when pressed", () => {
|
|
88
|
+
const handleSetPage = mock((_page: number) => {});
|
|
89
|
+
const {UNSAFE_getAllByProps} = renderWithTheme(
|
|
90
|
+
<Pagination page={10} setPage={handleSetPage} totalPages={20} />
|
|
91
|
+
);
|
|
92
|
+
// Find the "more" pagination buttons (they have iconName="ellipsis")
|
|
93
|
+
const moreIcons = UNSAFE_getAllByProps({iconName: "ellipsis"});
|
|
94
|
+
expect(moreIcons.length).toBeGreaterThan(0);
|
|
95
|
+
const morePressable = moreIcons[0].parent;
|
|
96
|
+
if (morePressable) {
|
|
97
|
+
expect(() => fireEvent.press(morePressable)).not.toThrow();
|
|
98
|
+
}
|
|
99
|
+
// Pressing "more" does nothing (onClick is no-op)
|
|
100
|
+
expect(handleSetPage).not.toHaveBeenCalled();
|
|
101
|
+
});
|
|
86
102
|
});
|
|
@@ -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 {PhoneNumberField} from "./PhoneNumberField";
|
|
4
5
|
import {renderWithTheme} from "./test-utils";
|
|
@@ -30,15 +31,6 @@ describe("PhoneNumberField", () => {
|
|
|
30
31
|
expect(getByDisplayValue("(555) 123-4567")).toBeTruthy();
|
|
31
32
|
});
|
|
32
33
|
|
|
33
|
-
it("formats phone number as user types", () => {
|
|
34
|
-
const handleChange = mock((_value: string) => {});
|
|
35
|
-
const {toJSON} = renderWithTheme(
|
|
36
|
-
<PhoneNumberField label="Phone" onChange={handleChange} value="5551234567" />
|
|
37
|
-
);
|
|
38
|
-
// Snapshot captures the formatted phone number display
|
|
39
|
-
expect(toJSON()).toMatchSnapshot();
|
|
40
|
-
});
|
|
41
|
-
|
|
42
34
|
it("renders with custom errorText", () => {
|
|
43
35
|
const {getByText} = renderWithTheme(
|
|
44
36
|
<PhoneNumberField
|
|
@@ -76,4 +68,49 @@ describe("PhoneNumberField", () => {
|
|
|
76
68
|
);
|
|
77
69
|
expect(toJSON()).toMatchSnapshot();
|
|
78
70
|
});
|
|
71
|
+
|
|
72
|
+
it("calls onBlur callback when provided", async () => {
|
|
73
|
+
const handleBlur = mock((_value: string) => {});
|
|
74
|
+
const handleChange = mock((_value: string) => {});
|
|
75
|
+
const {getByDisplayValue} = renderWithTheme(
|
|
76
|
+
<PhoneNumberField
|
|
77
|
+
label="Phone"
|
|
78
|
+
onBlur={handleBlur}
|
|
79
|
+
onChange={handleChange}
|
|
80
|
+
value="(555) 123-4567"
|
|
81
|
+
/>
|
|
82
|
+
);
|
|
83
|
+
const input = getByDisplayValue("(555) 123-4567");
|
|
84
|
+
await act(async () => {
|
|
85
|
+
fireEvent(input, "blur", {nativeEvent: {text: "(555) 123-4567"}});
|
|
86
|
+
});
|
|
87
|
+
expect(handleBlur).toHaveBeenCalled();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("calls onBlur with invalid number and sets an error state", async () => {
|
|
91
|
+
const handleBlur = mock((_value: string) => {});
|
|
92
|
+
const handleChange = mock((_value: string) => {});
|
|
93
|
+
const {getByDisplayValue} = renderWithTheme(
|
|
94
|
+
<PhoneNumberField label="Phone" onBlur={handleBlur} onChange={handleChange} value="" />
|
|
95
|
+
);
|
|
96
|
+
const input = getByDisplayValue("");
|
|
97
|
+
await act(async () => {
|
|
98
|
+
fireEvent.changeText(input, "123");
|
|
99
|
+
fireEvent(input, "blur", {nativeEvent: {text: "123"}});
|
|
100
|
+
});
|
|
101
|
+
expect(handleBlur).toHaveBeenCalled();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("handles empty input on blur without error", async () => {
|
|
105
|
+
const handleBlur = mock((_value: string) => {});
|
|
106
|
+
const handleChange = mock((_value: string) => {});
|
|
107
|
+
const {getByDisplayValue} = renderWithTheme(
|
|
108
|
+
<PhoneNumberField label="Phone" onBlur={handleBlur} onChange={handleChange} value="" />
|
|
109
|
+
);
|
|
110
|
+
const input = getByDisplayValue("");
|
|
111
|
+
await act(async () => {
|
|
112
|
+
fireEvent(input, "blur", {nativeEvent: {text: ""}});
|
|
113
|
+
});
|
|
114
|
+
expect(handleBlur).toHaveBeenCalled();
|
|
115
|
+
});
|
|
79
116
|
});
|