@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
|
@@ -1,10 +1,76 @@
|
|
|
1
|
-
import {describe, expect, it} from "bun:test";
|
|
2
|
-
import {render} from "@testing-library/react-native";
|
|
1
|
+
import {afterEach, beforeEach, describe, expect, it, mock} from "bun:test";
|
|
2
|
+
import {render, waitFor} from "@testing-library/react-native";
|
|
3
3
|
import {Text} from "react-native";
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import type {OpenAPIContextType, OpenAPISpec} from "./Common";
|
|
6
|
+
import {OpenAPIProvider, useOpenAPISpec} from "./OpenAPIContext";
|
|
7
|
+
|
|
8
|
+
const TEST_SPEC: OpenAPISpec = {
|
|
9
|
+
paths: {
|
|
10
|
+
"/todoItems/": {
|
|
11
|
+
get: {
|
|
12
|
+
responses: {
|
|
13
|
+
"200": {
|
|
14
|
+
content: {
|
|
15
|
+
"application/json": {
|
|
16
|
+
schema: {
|
|
17
|
+
properties: {
|
|
18
|
+
data: {
|
|
19
|
+
items: {
|
|
20
|
+
properties: {
|
|
21
|
+
metadata: {
|
|
22
|
+
properties: {
|
|
23
|
+
color: {description: "Color metadata", type: "string"},
|
|
24
|
+
},
|
|
25
|
+
type: "object",
|
|
26
|
+
},
|
|
27
|
+
title: {description: "Title for the todo", type: "string"},
|
|
28
|
+
},
|
|
29
|
+
required: ["title"],
|
|
30
|
+
type: "object",
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const ContextReader = ({onContext}: {onContext: (context: OpenAPIContextType) => void}) => {
|
|
45
|
+
const context = useOpenAPISpec();
|
|
46
|
+
onContext(context);
|
|
47
|
+
return <Text>Context reader</Text>;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const HookOutsideProvider = () => {
|
|
51
|
+
useOpenAPISpec();
|
|
52
|
+
return <Text>unreachable</Text>;
|
|
53
|
+
};
|
|
6
54
|
|
|
7
55
|
describe("OpenAPIContext", () => {
|
|
56
|
+
const originalFetch = globalThis.fetch;
|
|
57
|
+
const originalWarn = console.warn;
|
|
58
|
+
const originalError = console.error;
|
|
59
|
+
|
|
60
|
+
beforeEach(() => {
|
|
61
|
+
globalThis.fetch = mock(async () => ({
|
|
62
|
+
json: async () => TEST_SPEC,
|
|
63
|
+
})) as unknown as typeof globalThis.fetch;
|
|
64
|
+
console.warn = mock(() => {});
|
|
65
|
+
console.error = mock(() => {});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
afterEach(() => {
|
|
69
|
+
globalThis.fetch = originalFetch;
|
|
70
|
+
console.warn = originalWarn;
|
|
71
|
+
console.error = originalError;
|
|
72
|
+
});
|
|
73
|
+
|
|
8
74
|
describe("OpenAPIProvider", () => {
|
|
9
75
|
it("renders children", () => {
|
|
10
76
|
const {getByText} = render(
|
|
@@ -32,5 +98,120 @@ describe("OpenAPIContext", () => {
|
|
|
32
98
|
);
|
|
33
99
|
expect(toJSON()).toMatchSnapshot();
|
|
34
100
|
});
|
|
101
|
+
|
|
102
|
+
it("fetches spec and resolves model fields", async () => {
|
|
103
|
+
let capturedContext: OpenAPIContextType | null = null;
|
|
104
|
+
render(
|
|
105
|
+
<OpenAPIProvider specUrl="https://api.example.com/openapi.json">
|
|
106
|
+
<ContextReader
|
|
107
|
+
onContext={(context) => {
|
|
108
|
+
capturedContext = context;
|
|
109
|
+
}}
|
|
110
|
+
/>
|
|
111
|
+
</OpenAPIProvider>
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
await waitFor(() => {
|
|
115
|
+
expect(capturedContext?.spec).toEqual(TEST_SPEC);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const modelFields = capturedContext?.getModelFields("Todo Items");
|
|
119
|
+
expect(modelFields?.type).toBe("object");
|
|
120
|
+
expect(modelFields?.required).toEqual(["title"]);
|
|
121
|
+
expect(modelFields?.properties?.title).toEqual({
|
|
122
|
+
description: "Title for the todo",
|
|
123
|
+
type: "string",
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("resolves nested model fields using dot notation", async () => {
|
|
128
|
+
let capturedContext: OpenAPIContextType | null = null;
|
|
129
|
+
render(
|
|
130
|
+
<OpenAPIProvider specUrl="https://api.example.com/openapi.json">
|
|
131
|
+
<ContextReader
|
|
132
|
+
onContext={(context) => {
|
|
133
|
+
capturedContext = context;
|
|
134
|
+
}}
|
|
135
|
+
/>
|
|
136
|
+
</OpenAPIProvider>
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
await waitFor(() => {
|
|
140
|
+
expect(capturedContext?.spec).toEqual(TEST_SPEC);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
expect(capturedContext?.getModelField("Todo Items", "metadata.color")).toEqual({
|
|
144
|
+
description: "Color metadata",
|
|
145
|
+
type: "string",
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("warns when model path is missing", async () => {
|
|
150
|
+
let capturedContext: OpenAPIContextType | null = null;
|
|
151
|
+
render(
|
|
152
|
+
<OpenAPIProvider specUrl="https://api.example.com/openapi.json">
|
|
153
|
+
<ContextReader
|
|
154
|
+
onContext={(context) => {
|
|
155
|
+
capturedContext = context;
|
|
156
|
+
}}
|
|
157
|
+
/>
|
|
158
|
+
</OpenAPIProvider>
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
await waitFor(() => {
|
|
162
|
+
expect(capturedContext?.spec).toEqual(TEST_SPEC);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
expect(capturedContext?.getModelFields("Unknown Model")).toBeNull();
|
|
166
|
+
expect(console.warn).toHaveBeenCalledWith("No OpenAPI model found for Unknown Model");
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("warns when model field is missing", async () => {
|
|
170
|
+
let capturedContext: OpenAPIContextType | null = null;
|
|
171
|
+
render(
|
|
172
|
+
<OpenAPIProvider specUrl="https://api.example.com/openapi.json">
|
|
173
|
+
<ContextReader
|
|
174
|
+
onContext={(context) => {
|
|
175
|
+
capturedContext = context;
|
|
176
|
+
}}
|
|
177
|
+
/>
|
|
178
|
+
</OpenAPIProvider>
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
await waitFor(() => {
|
|
182
|
+
expect(capturedContext?.spec).toEqual(TEST_SPEC);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
expect(capturedContext?.getModelField("Todo Items", "missingField")).toBeUndefined();
|
|
186
|
+
expect(console.warn).toHaveBeenCalledWith(
|
|
187
|
+
"No OpenAPI field found for Todo Items:missingField"
|
|
188
|
+
);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("logs an error when spec fetch fails", async () => {
|
|
192
|
+
globalThis.fetch = mock(async () => {
|
|
193
|
+
throw new Error("network down");
|
|
194
|
+
}) as unknown as typeof globalThis.fetch;
|
|
195
|
+
|
|
196
|
+
render(
|
|
197
|
+
<OpenAPIProvider specUrl="https://api.example.com/openapi.json">
|
|
198
|
+
<Text>Fetch failing</Text>
|
|
199
|
+
</OpenAPIProvider>
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
await waitFor(() => {
|
|
203
|
+
expect(console.error).toHaveBeenCalledWith(
|
|
204
|
+
"Error fetching OpenAPI spec: Error: network down"
|
|
205
|
+
);
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
describe("useOpenAPISpec", () => {
|
|
211
|
+
it("throws when used outside OpenAPIProvider", () => {
|
|
212
|
+
expect(() => render(<HookOutsideProvider />)).toThrow(
|
|
213
|
+
"useOpenAPISpec must be used within an OpenAPIProvider"
|
|
214
|
+
);
|
|
215
|
+
});
|
|
35
216
|
});
|
|
36
217
|
});
|
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
|
});
|
package/src/PasswordField.tsx
CHANGED
|
@@ -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
|
});
|