@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.
Files changed (74) hide show
  1. package/dist/Banner.js +7 -5
  2. package/dist/Banner.js.map +1 -1
  3. package/dist/Common.d.ts +3 -1
  4. package/dist/Common.js.map +1 -1
  5. package/dist/TextFieldNumberActionSheet.d.ts +1 -1
  6. package/dist/Toast.d.ts +1 -1
  7. package/dist/Toast.js +2 -2
  8. package/dist/Toast.js.map +1 -1
  9. package/dist/index.d.ts +2 -2
  10. package/package.json +2 -1
  11. package/src/ActionSheet.test.tsx +262 -3
  12. package/src/AddressField.test.tsx +50 -0
  13. package/src/Banner.test.tsx +65 -0
  14. package/src/Banner.tsx +7 -5
  15. package/src/Box.test.tsx +218 -0
  16. package/src/Button.test.tsx +71 -0
  17. package/src/Common.ts +3 -1
  18. package/src/ConsentFormScreen.test.tsx +167 -0
  19. package/src/ConsentNavigator.test.tsx +206 -0
  20. package/src/DecimalRangeActionSheet.test.tsx +53 -2
  21. package/src/EmailField.test.tsx +81 -0
  22. package/src/EmojiSelector.test.tsx +262 -1
  23. package/src/HeightActionSheet.test.tsx +57 -2
  24. package/src/InfoModalIcon.test.tsx +16 -0
  25. package/src/InfoTooltipButton.test.tsx +53 -1
  26. package/src/MobileAddressAutoComplete.test.tsx +137 -7
  27. package/src/Modal.test.tsx +188 -0
  28. package/src/NumberPickerActionSheet.test.tsx +59 -2
  29. package/src/Page.test.tsx +162 -1
  30. package/src/Pagination.test.tsx +16 -0
  31. package/src/PhoneNumberField.test.tsx +46 -9
  32. package/src/PickerSelect.test.tsx +230 -0
  33. package/src/SegmentedControl.test.tsx +38 -0
  34. package/src/SelectBadge.test.tsx +52 -1
  35. package/src/SideDrawer.test.tsx +69 -0
  36. package/src/Signature.test.tsx +42 -5
  37. package/src/SignatureField.test.tsx +35 -0
  38. package/src/Slider.test.tsx +59 -0
  39. package/src/Spinner.test.tsx +6 -0
  40. package/src/SplitPage.test.tsx +228 -2
  41. package/src/TapToEdit.test.tsx +171 -1
  42. package/src/TerrenoProvider.test.tsx +42 -2
  43. package/src/TextFieldNumberActionSheet.tsx +1 -1
  44. package/src/Theme.test.tsx +118 -28
  45. package/src/Toast.test.tsx +95 -2
  46. package/src/Toast.tsx +3 -3
  47. package/src/Tooltip.test.tsx +204 -1
  48. package/src/UnifiedAddressAutoComplete.test.tsx +38 -19
  49. package/src/UserInactivity.test.tsx +73 -1
  50. package/src/Utilities.test.tsx +190 -2
  51. package/src/WebAddressAutocomplete.test.tsx +148 -1
  52. package/src/__snapshots__/ActionSheet.test.tsx.snap +1736 -0
  53. package/src/__snapshots__/Button.test.tsx.snap +68 -0
  54. package/src/__snapshots__/EmojiSelector.test.tsx.snap +1363 -0
  55. package/src/__snapshots__/InfoTooltipButton.test.tsx.snap +72 -3
  56. package/src/__snapshots__/MobileAddressAutoComplete.test.tsx.snap +60 -9
  57. package/src/__snapshots__/Modal.test.tsx.snap +181 -0
  58. package/src/__snapshots__/Page.test.tsx.snap +48 -2
  59. package/src/__snapshots__/PhoneNumberField.test.tsx.snap +0 -93
  60. package/src/__snapshots__/PickerSelect.test.tsx.snap +706 -0
  61. package/src/__snapshots__/SideDrawer.test.tsx.snap +533 -1399
  62. package/src/__snapshots__/Signature.test.tsx.snap +0 -3
  63. package/src/__snapshots__/SplitPage.test.tsx.snap +970 -0
  64. package/src/__snapshots__/UnifiedAddressAutoComplete.test.tsx.snap +220 -4
  65. package/src/__snapshots__/WebAddressAutocomplete.test.tsx.snap +93 -0
  66. package/src/bunSetup.ts +204 -121
  67. package/src/index.tsx +2 -2
  68. package/src/table/TableHeaderCell.test.tsx +142 -0
  69. package/src/table/TableRow.test.tsx +33 -0
  70. package/src/table/__snapshots__/TableRow.test.tsx.snap +403 -0
  71. package/src/table/tableContext.test.tsx +96 -0
  72. package/src/test-utils.tsx +1 -1
  73. package/src/useConsentForms.test.ts +130 -0
  74. package/src/useSubmitConsent.test.ts +64 -0
@@ -1,8 +1,10 @@
1
1
  import {describe, expect, it} from "bun:test";
2
2
 
3
3
  import {
4
+ bind,
4
5
  concat,
5
6
  findAddressComponent,
7
+ formattedCountyCode,
6
8
  fromClassName,
7
9
  fromInlineStyle,
8
10
  iconNumberToSize,
@@ -11,10 +13,16 @@ import {
11
13
  isNative,
12
14
  isTestUser,
13
15
  isValidGoogleApiKey,
16
+ mapClassName,
17
+ mapping,
14
18
  mergeInlineStyles,
15
19
  printAPIError,
16
20
  processAddressComponents,
21
+ range,
22
+ rangeWithoutZero,
23
+ toggle,
17
24
  toProps,
25
+ union,
18
26
  } from "./Utilities";
19
27
 
20
28
  describe("Utilities", () => {
@@ -30,6 +38,11 @@ describe("Utilities", () => {
30
38
  const result = mergeInlineStyles(undefined, {color: "red"});
31
39
  expect(result.__style).toEqual({color: "red"});
32
40
  });
41
+
42
+ it("handles undefined new style", () => {
43
+ const result = mergeInlineStyles({__style: {color: "red"}}, undefined);
44
+ expect(result.__style).toEqual({color: "red"});
45
+ });
33
46
  });
34
47
 
35
48
  describe("isTestUser", () => {
@@ -48,6 +61,10 @@ describe("Utilities", () => {
48
61
  it("returns falsy for undefined profile", () => {
49
62
  expect(isTestUser(undefined)).toBeFalsy();
50
63
  });
64
+
65
+ it("returns falsy for profile without email", () => {
66
+ expect(isTestUser({} as any)).toBeFalsy();
67
+ });
51
68
  });
52
69
 
53
70
  describe("iconNumberToSize", () => {
@@ -74,6 +91,10 @@ describe("Utilities", () => {
74
91
  it("returns lg for default size (16)", () => {
75
92
  expect(iconNumberToSize()).toBe("lg");
76
93
  });
94
+
95
+ it("returns xs for zero", () => {
96
+ expect(iconNumberToSize(0)).toBe("xs");
97
+ });
77
98
  });
78
99
 
79
100
  describe("Style utilities", () => {
@@ -91,6 +112,11 @@ describe("Utilities", () => {
91
112
  expect(result.className.has("class1")).toBe(true);
92
113
  expect(result.className.has("class2")).toBe(true);
93
114
  });
115
+
116
+ it("supports empty className list", () => {
117
+ const result = fromClassName();
118
+ expect(result.className.size).toBe(0);
119
+ });
94
120
  });
95
121
 
96
122
  describe("fromInlineStyle", () => {
@@ -110,6 +136,29 @@ describe("Utilities", () => {
110
136
  expect(result.className.has("class2")).toBe(true);
111
137
  expect(result.inlineStyle.color).toBe("red");
112
138
  });
139
+
140
+ it("concatenates with identity element", () => {
141
+ const style = fromClassName("a");
142
+ const result = concat([identity(), style]);
143
+ expect(result.className.has("a")).toBe(true);
144
+ });
145
+ });
146
+
147
+ describe("mapClassName", () => {
148
+ it("maps class names through a function", () => {
149
+ const prefix = mapClassName((name: string) => `prefix-${name}`);
150
+ const style = fromClassName("foo", "bar");
151
+ const result = prefix(style);
152
+ expect(result.className.has("prefix-foo")).toBe(true);
153
+ expect(result.className.has("prefix-bar")).toBe(true);
154
+ });
155
+
156
+ it("preserves inline styles", () => {
157
+ const prefix = mapClassName((name: string) => `prefix-${name}`);
158
+ const style = fromInlineStyle({color: "red"});
159
+ const result = prefix(style);
160
+ expect(result.inlineStyle.color).toBe("red");
161
+ });
113
162
  });
114
163
 
115
164
  describe("toProps", () => {
@@ -132,6 +181,88 @@ describe("Utilities", () => {
132
181
  expect(props.style).toBeUndefined();
133
182
  });
134
183
  });
184
+
185
+ describe("toggle", () => {
186
+ it("returns style with classnames when val is true", () => {
187
+ const toggleFn = toggle("active", "enabled");
188
+ const result = toggleFn(true);
189
+ expect(result.className.has("active")).toBe(true);
190
+ expect(result.className.has("enabled")).toBe(true);
191
+ });
192
+
193
+ it("returns identity when val is false", () => {
194
+ const toggleFn = toggle("active");
195
+ const result = toggleFn(false);
196
+ expect(result.className.size).toBe(0);
197
+ });
198
+
199
+ it("returns identity when val is undefined", () => {
200
+ const toggleFn = toggle("active");
201
+ const result = toggleFn();
202
+ expect(result.className.size).toBe(0);
203
+ });
204
+ });
205
+
206
+ describe("mapping", () => {
207
+ it("maps string to classname", () => {
208
+ const map = mapping({large: "size-large", small: "size-small"});
209
+ const result = map("small");
210
+ expect(result.className.has("size-small")).toBe(true);
211
+ });
212
+
213
+ it("returns identity for unknown key", () => {
214
+ const map = mapping({small: "size-small"});
215
+ const result = map("unknown");
216
+ expect(result.className.size).toBe(0);
217
+ });
218
+ });
219
+
220
+ describe("range", () => {
221
+ it("creates classname from positive number", () => {
222
+ const result = range("padding")(3);
223
+ expect(result.className.has("padding3")).toBe(true);
224
+ });
225
+
226
+ it("creates classname from negative number with N prefix", () => {
227
+ const result = range("margin")(-2);
228
+ expect(result.className.has("marginN2")).toBe(true);
229
+ });
230
+
231
+ it("creates classname from zero", () => {
232
+ const result = range("padding")(0);
233
+ expect(result.className.has("padding0")).toBe(true);
234
+ });
235
+ });
236
+
237
+ describe("rangeWithoutZero", () => {
238
+ it("creates classname for non-zero", () => {
239
+ const result = rangeWithoutZero("padding")(2);
240
+ expect(result.className.has("padding2")).toBe(true);
241
+ });
242
+
243
+ it("returns identity for zero", () => {
244
+ const result = rangeWithoutZero("padding")(0);
245
+ expect(result.className.size).toBe(0);
246
+ });
247
+ });
248
+
249
+ describe("bind", () => {
250
+ it("binds a functor to a scope", () => {
251
+ const scope: {[key: string]: string} = {padding2: "scoped-padding"};
252
+ const bound = bind(range("padding"), scope);
253
+ const result = bound(2);
254
+ expect(result.className.has("scoped-padding")).toBe(true);
255
+ });
256
+ });
257
+
258
+ describe("union", () => {
259
+ it("combines multiple functors", () => {
260
+ const combined = union(toggle("a"), toggle("b"));
261
+ const result = combined(true);
262
+ expect(result.className.has("a")).toBe(true);
263
+ expect(result.className.has("b")).toBe(true);
264
+ });
265
+ });
135
266
  });
136
267
 
137
268
  describe("isNative", () => {
@@ -155,6 +286,10 @@ describe("Utilities", () => {
155
286
  it("returns empty string for missing type", () => {
156
287
  expect(findAddressComponent(components, "country")).toBe("");
157
288
  });
289
+
290
+ it("returns empty string when components is empty", () => {
291
+ expect(findAddressComponent([], "locality")).toBe("");
292
+ });
158
293
  });
159
294
 
160
295
  describe("processAddressComponents", () => {
@@ -163,6 +298,7 @@ describe("Utilities", () => {
163
298
  {long_name: "Main Street", short_name: "Main St", types: ["route"]},
164
299
  {long_name: "Boston", short_name: "Boston", types: ["locality"]},
165
300
  {long_name: "Massachusetts", short_name: "MA", types: ["administrative_area_level_1"]},
301
+ {long_name: "Suffolk County", short_name: "Suffolk", types: ["administrative_area_level_2"]},
166
302
  {long_name: "02101", short_name: "02101", types: ["postal_code"]},
167
303
  ];
168
304
 
@@ -183,11 +319,32 @@ describe("Utilities", () => {
183
319
  const result = processAddressComponents(undefined);
184
320
  expect(result.address1).toBe("");
185
321
  });
322
+
323
+ it("handles empty components with includeCounty", () => {
324
+ const result = processAddressComponents([], {includeCounty: true}) as any;
325
+ expect(result.countyName).toBe("");
326
+ expect(result.countyCode).toBe("");
327
+ });
328
+
329
+ it("handles components with includeCounty when county is present", () => {
330
+ const result = processAddressComponents(components, {includeCounty: true}) as any;
331
+ expect(result.countyName).toBe("Suffolk County");
332
+ });
333
+
334
+ it("handles components with includeCounty when county is missing", () => {
335
+ const componentsWithoutCounty = components.filter(
336
+ (c) => !c.types.includes("administrative_area_level_2")
337
+ );
338
+ const result = processAddressComponents(componentsWithoutCounty, {
339
+ includeCounty: true,
340
+ }) as any;
341
+ expect(result.countyName).toBe("");
342
+ });
186
343
  });
187
344
 
188
345
  describe("isValidGoogleApiKey", () => {
189
346
  it("returns true for valid-looking API key", () => {
190
- expect(isValidGoogleApiKey("AIzaSyD-9tSrke72PouQMnMX-a7eZSW0jkFMBWY")).toBe(true);
347
+ expect(isValidGoogleApiKey("test-dummy-key-not-real-0123456789")).toBe(true);
191
348
  });
192
349
 
193
350
  it("returns false for empty string", () => {
@@ -198,8 +355,30 @@ describe("Utilities", () => {
198
355
  expect(isValidGoogleApiKey("short")).toBe(false);
199
356
  });
200
357
 
358
+ it("returns false for too long key", () => {
359
+ expect(isValidGoogleApiKey("a".repeat(51))).toBe(false);
360
+ });
361
+
201
362
  it("returns false for key with invalid characters", () => {
202
- expect(isValidGoogleApiKey("invalid key with spaces!!!")).toBe(false);
363
+ expect(isValidGoogleApiKey("invalid key with spaces and special!!!!")).toBe(false);
364
+ });
365
+
366
+ it("returns false when passed a non-string value", () => {
367
+ expect(isValidGoogleApiKey(123 as any)).toBe(false);
368
+ });
369
+
370
+ it("returns false for whitespace-only key", () => {
371
+ expect(isValidGoogleApiKey(" ")).toBe(false);
372
+ });
373
+ });
374
+
375
+ describe("formattedCountyCode", () => {
376
+ it("returns empty string for unknown state/county", () => {
377
+ expect(formattedCountyCode("Nowhere", "Nothing")).toBe("");
378
+ });
379
+
380
+ it("handles missing county data gracefully", () => {
381
+ expect(formattedCountyCode("Massachusetts", "Fake County")).toBe("");
203
382
  });
204
383
  });
205
384
 
@@ -216,6 +395,10 @@ describe("Utilities", () => {
216
395
  it("returns falsy for null", () => {
217
396
  expect(isAPIError(null)).toBeFalsy();
218
397
  });
398
+
399
+ it("returns falsy for undefined", () => {
400
+ expect(isAPIError(undefined)).toBeFalsy();
401
+ });
219
402
  });
220
403
 
221
404
  describe("printAPIError", () => {
@@ -233,5 +416,10 @@ describe("Utilities", () => {
233
416
  const error = {data: {detail: "Resource does not exist", title: "Not Found"}};
234
417
  expect(printAPIError(error as any, false)).toBe("Not Found");
235
418
  });
419
+
420
+ it("prints title when detail is missing", () => {
421
+ const error = {data: {title: "Not Found"}};
422
+ expect(printAPIError(error as any, true)).toBe("Not Found");
423
+ });
236
424
  });
237
425
  });
@@ -1,4 +1,7 @@
1
- import {describe, expect, it} from "bun:test";
1
+ import {afterEach, beforeEach, describe, expect, it, mock} from "bun:test";
2
+ import {act, fireEvent} from "@testing-library/react-native";
3
+
4
+ import type {AddressInterface} from "./Common";
2
5
  import {renderWithTheme} from "./test-utils";
3
6
  import {WebAddressAutocomplete} from "./WebAddressAutocomplete";
4
7
 
@@ -30,4 +33,148 @@ describe("WebAddressAutocomplete", () => {
30
33
  const {toJSON} = renderWithTheme(<WebAddressAutocomplete {...defaultProps} disabled />);
31
34
  expect(toJSON()).toMatchSnapshot();
32
35
  });
36
+
37
+ it("renders without calling handleAutoCompleteChange until a place is selected", () => {
38
+ const handleAutoCompleteChange = mock((_arg: AddressInterface) => {});
39
+ renderWithTheme(
40
+ <WebAddressAutocomplete
41
+ {...defaultProps}
42
+ handleAutoCompleteChange={handleAutoCompleteChange}
43
+ />
44
+ );
45
+ expect(handleAutoCompleteChange).not.toHaveBeenCalled();
46
+ });
47
+
48
+ it("renders with includeCounty flag", () => {
49
+ const {toJSON} = renderWithTheme(<WebAddressAutocomplete {...defaultProps} includeCounty />);
50
+ expect(toJSON()).toMatchSnapshot();
51
+ });
52
+
53
+ describe("with Google Maps available", () => {
54
+ interface PlaceResult {
55
+ address_components: {
56
+ long_name: string;
57
+ short_name: string;
58
+ types: string[];
59
+ }[];
60
+ }
61
+
62
+ interface GoogleMapsWindow {
63
+ google?: {
64
+ maps?: {
65
+ places?: {
66
+ Autocomplete?: unknown;
67
+ };
68
+ };
69
+ };
70
+ [key: string]: unknown;
71
+ }
72
+
73
+ interface MinimalDocument {
74
+ createElement: (tag: string) => HTMLScriptElement;
75
+ head: {appendChild: (node: unknown) => void};
76
+ }
77
+
78
+ interface TestGlobals {
79
+ window?: GoogleMapsWindow;
80
+ document?: MinimalDocument;
81
+ }
82
+
83
+ const testGlobal = globalThis as TestGlobals;
84
+ const originalWindow = testGlobal.window;
85
+ const originalDocument = testGlobal.document;
86
+
87
+ let listeners: Record<string, () => void>;
88
+ let placeResult: PlaceResult | null;
89
+ let autocompleteConstructor: ReturnType<typeof mock>;
90
+
91
+ beforeEach(() => {
92
+ listeners = {};
93
+ placeResult = null;
94
+
95
+ autocompleteConstructor = mock((_input: unknown, _opts: unknown) => ({
96
+ addListener: (event: string, cb: () => void) => {
97
+ listeners[event] = cb;
98
+ },
99
+ getPlace: () => placeResult,
100
+ }));
101
+
102
+ testGlobal.window = {
103
+ google: {
104
+ maps: {
105
+ places: {
106
+ Autocomplete: autocompleteConstructor,
107
+ },
108
+ },
109
+ },
110
+ };
111
+ testGlobal.document =
112
+ originalDocument ??
113
+ ({
114
+ createElement: () => ({}) as HTMLScriptElement,
115
+ head: {appendChild: () => {}},
116
+ } satisfies MinimalDocument);
117
+ });
118
+
119
+ afterEach(() => {
120
+ // Leave a minimal window so React's effect cleanup (which assigns
121
+ // `window[callbackName] = null`) does not blow up after teardown.
122
+ testGlobal.window = originalWindow ?? {};
123
+ testGlobal.document = originalDocument;
124
+ });
125
+
126
+ it("initializes the Autocomplete and wires up the place_changed listener", async () => {
127
+ const handleAutoCompleteChange = mock((_arg: AddressInterface) => {});
128
+ renderWithTheme(
129
+ <WebAddressAutocomplete
130
+ googleMapsApiKey="test-key"
131
+ handleAddressChange={() => {}}
132
+ handleAutoCompleteChange={handleAutoCompleteChange}
133
+ inputValue=""
134
+ />
135
+ );
136
+
137
+ await act(async () => {
138
+ await new Promise((resolve) => setTimeout(resolve, 0));
139
+ });
140
+
141
+ expect(autocompleteConstructor).toHaveBeenCalled();
142
+ expect(typeof listeners.place_changed).toBe("function");
143
+
144
+ // Simulate a selected place.
145
+ placeResult = {
146
+ address_components: [
147
+ {long_name: "5", short_name: "5", types: ["street_number"]},
148
+ {long_name: "Elm", short_name: "Elm", types: ["route"]},
149
+ {long_name: "Oakland", short_name: "OAK", types: ["locality"]},
150
+ {long_name: "California", short_name: "CA", types: ["administrative_area_level_1"]},
151
+ {long_name: "94601", short_name: "94601", types: ["postal_code"]},
152
+ ],
153
+ };
154
+ listeners.place_changed();
155
+ expect(handleAutoCompleteChange).toHaveBeenCalled();
156
+ });
157
+
158
+ it("invokes handleAddressChange from the fallback TextField's onChange", async () => {
159
+ const handleAddressChange = mock(() => {});
160
+ const {UNSAFE_getAllByType} = renderWithTheme(
161
+ <WebAddressAutocomplete
162
+ googleMapsApiKey="test-key"
163
+ handleAddressChange={handleAddressChange}
164
+ handleAutoCompleteChange={() => {}}
165
+ inputValue=""
166
+ />
167
+ );
168
+
169
+ await act(async () => {
170
+ await new Promise((resolve) => setTimeout(resolve, 0));
171
+ });
172
+
173
+ const {TextInput} = require("react-native");
174
+ const inputs = UNSAFE_getAllByType(TextInput);
175
+ expect(inputs.length).toBeGreaterThan(0);
176
+ fireEvent.changeText(inputs[0], "321 Pine");
177
+ expect(handleAddressChange).toHaveBeenCalledWith("321 Pine");
178
+ });
179
+ });
33
180
  });