@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,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 {OpenAPIProvider} from "./OpenAPIContext";
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
  });
@@ -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
  });
@@ -2,6 +2,6 @@ import type React from "react";
2
2
 
3
3
  import {Box} from "./Box";
4
4
 
5
- export const PasswordField = (): React.ReactElement => {
5
+ export const PasswordField: React.FC = () => {
6
6
  return <Box />;
7
7
  };
@@ -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
  });