@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,5 +1,6 @@
1
- import {describe, expect, it, mock} from "bun:test";
2
- import {View} from "react-native";
1
+ import {afterAll, beforeAll, describe, expect, it, mock} from "bun:test";
2
+ import {act} from "@testing-library/react-native";
3
+ import {Dimensions, View} from "react-native";
3
4
 
4
5
  import {SplitPage} from "./SplitPage";
5
6
  import {renderWithTheme} from "./test-utils";
@@ -79,4 +80,229 @@ describe("SplitPage", () => {
79
80
  );
80
81
  expect(toJSON()).toMatchSnapshot();
81
82
  });
83
+
84
+ it("returns null when no children and no renderContent", () => {
85
+ const {toJSON} = renderWithTheme(
86
+ <SplitPage listViewData={[]} renderListViewItem={() => null} />
87
+ );
88
+ expect(toJSON()).toBeNull();
89
+ });
90
+
91
+ it("returns null when tabs count does not match children count", () => {
92
+ const {toJSON} = renderWithTheme(
93
+ <SplitPage {...defaultProps} tabs={["Tab 1"]}>
94
+ <View testID="child-1" />
95
+ <View testID="child-2" />
96
+ <View testID="child-3" />
97
+ </SplitPage>
98
+ );
99
+ expect(toJSON()).toBeNull();
100
+ });
101
+
102
+ it("renders with list view header", () => {
103
+ const {toJSON} = renderWithTheme(
104
+ <SplitPage
105
+ {...defaultProps}
106
+ renderContent={(selectedId) => <View testID={`content-${selectedId}`} />}
107
+ renderListViewHeader={() => <View testID="list-header" />}
108
+ />
109
+ );
110
+ expect(toJSON()).toMatchSnapshot();
111
+ });
112
+
113
+ it("renders with custom list view width and max width", () => {
114
+ const {toJSON} = renderWithTheme(
115
+ <SplitPage
116
+ {...defaultProps}
117
+ listViewMaxWidth={500}
118
+ listViewWidth={400}
119
+ renderContent={(selectedId) => <View testID={`content-${selectedId}`} />}
120
+ />
121
+ );
122
+ expect(toJSON()).toMatchSnapshot();
123
+ });
124
+
125
+ it("renders with listViewExtraData", () => {
126
+ const {toJSON} = renderWithTheme(
127
+ <SplitPage
128
+ {...defaultProps}
129
+ listViewExtraData={{counter: 1}}
130
+ renderContent={(selectedId) => <View testID={`content-${selectedId}`} />}
131
+ />
132
+ );
133
+ expect(toJSON()).toMatchSnapshot();
134
+ });
135
+
136
+ it("renders with keyboard offset", () => {
137
+ const {toJSON} = renderWithTheme(
138
+ <SplitPage
139
+ {...defaultProps}
140
+ keyboardOffset={100}
141
+ renderContent={(selectedId) => <View testID={`content-${selectedId}`} />}
142
+ />
143
+ );
144
+ expect(toJSON()).toMatchSnapshot();
145
+ });
146
+
147
+ it("filters out null children", () => {
148
+ const {toJSON} = renderWithTheme(
149
+ <SplitPage {...defaultProps}>
150
+ <View testID="child-1" />
151
+ {null}
152
+ <View testID="child-2" />
153
+ </SplitPage>
154
+ );
155
+ expect(toJSON()).toMatchSnapshot();
156
+ });
157
+
158
+ it("renders with showItemList true to reset selection", () => {
159
+ const onSelectionChange = mock(() => {});
160
+ const {toJSON} = renderWithTheme(
161
+ <SplitPage
162
+ {...defaultProps}
163
+ onSelectionChange={onSelectionChange}
164
+ renderContent={(selectedId) => <View testID={`content-${selectedId}`} />}
165
+ showItemList
166
+ />
167
+ );
168
+ expect(toJSON()).toMatchSnapshot();
169
+ });
170
+
171
+ it("renders with bottomNavBarHeight", () => {
172
+ const {toJSON} = renderWithTheme(
173
+ <SplitPage {...defaultProps} bottomNavBarHeight={60}>
174
+ <View testID="child-1" />
175
+ </SplitPage>
176
+ );
177
+ expect(toJSON()).toMatchSnapshot();
178
+ });
179
+
180
+ describe("desktop viewport (mediaQueryLargerThan('sm') true)", () => {
181
+ const desktopImpl = () => ({fontScale: 1, height: 1000, scale: 2, width: 1400}) as any;
182
+ const mobileImpl = () => ({fontScale: 1, height: 812, scale: 2, width: 375}) as any;
183
+ let originalGet: typeof Dimensions.get;
184
+ beforeAll(() => {
185
+ originalGet = Dimensions.get;
186
+ if (typeof (Dimensions.get as any).mockImplementation === "function") {
187
+ (Dimensions.get as any).mockImplementation(desktopImpl);
188
+ } else {
189
+ (Dimensions.get as any) = desktopImpl;
190
+ }
191
+ });
192
+ afterAll(() => {
193
+ if (typeof (Dimensions.get as any).mockImplementation === "function") {
194
+ (Dimensions.get as any).mockImplementation(mobileImpl);
195
+ } else {
196
+ (Dimensions.get as any) = originalGet;
197
+ }
198
+ });
199
+
200
+ it("verifies Dimensions mock is overridden", () => {
201
+ expect(Dimensions.get("window").width).toBe(1400);
202
+ });
203
+
204
+ it("renders renderList/renderContent on desktop", () => {
205
+ const {toJSON} = renderWithTheme(
206
+ <SplitPage
207
+ {...defaultProps}
208
+ renderContent={(selectedId) => <View testID={`content-${selectedId}`} />}
209
+ />
210
+ );
211
+ expect(toJSON()).toBeTruthy();
212
+ });
213
+
214
+ it("renders renderList/renderChildrenContent on desktop with 2 children", () => {
215
+ const {toJSON} = renderWithTheme(
216
+ <SplitPage {...defaultProps}>
217
+ <View testID="child-1" />
218
+ <View testID="child-2" />
219
+ </SplitPage>
220
+ );
221
+ expect(toJSON()).toBeTruthy();
222
+ });
223
+
224
+ it("renders renderChildrenContent with >2 children and tabs on desktop", () => {
225
+ const {toJSON} = renderWithTheme(
226
+ <SplitPage {...defaultProps} tabs={["A", "B", "C"]}>
227
+ <View testID="child-1" />
228
+ <View testID="child-2" />
229
+ <View testID="child-3" />
230
+ </SplitPage>
231
+ );
232
+ expect(toJSON()).toBeTruthy();
233
+ });
234
+
235
+ it("renders with listViewWidth/listViewMaxWidth applied", () => {
236
+ const {toJSON} = renderWithTheme(
237
+ <SplitPage
238
+ {...defaultProps}
239
+ listViewMaxWidth={400}
240
+ listViewWidth={350}
241
+ renderContent={(id) => <View testID={`content-${id}`} />}
242
+ />
243
+ );
244
+ expect(toJSON()).toBeTruthy();
245
+ });
246
+ });
247
+
248
+ describe("item selection callbacks", () => {
249
+ it("onItemSelect runs onSelectionChange when item clicked via Box press", async () => {
250
+ const {fireEvent} = await import("@testing-library/react-native");
251
+ const onSelectionChange = mock(async (_arg: unknown) => {});
252
+ const {getAllByLabelText} = renderWithTheme(
253
+ <SplitPage
254
+ {...defaultProps}
255
+ onSelectionChange={onSelectionChange}
256
+ renderContent={(id) => <View testID={`content-${id}`} />}
257
+ />
258
+ );
259
+
260
+ const boxes = getAllByLabelText("Select");
261
+ expect(boxes.length).toBeGreaterThan(0);
262
+ await act(async () => {
263
+ fireEvent.press(boxes[0]);
264
+ });
265
+ expect(onSelectionChange).toHaveBeenCalled();
266
+ });
267
+
268
+ it("selecting an item shows mobile children content when no renderContent", async () => {
269
+ const {fireEvent} = await import("@testing-library/react-native");
270
+ const {getAllByLabelText, queryByTestId} = renderWithTheme(
271
+ <SplitPage {...defaultProps}>
272
+ <View testID="child-1" />
273
+ <View testID="child-2" />
274
+ </SplitPage>
275
+ );
276
+ const boxes = getAllByLabelText("Select");
277
+ await act(async () => {
278
+ fireEvent.press(boxes[0]);
279
+ });
280
+ expect(queryByTestId("swiper-flatlist")).toBeTruthy();
281
+ });
282
+
283
+ it("selection deselect when showItemList becomes true", async () => {
284
+ const onSelectionChange = mock(async () => {});
285
+ const {rerender} = renderWithTheme(
286
+ <SplitPage
287
+ {...defaultProps}
288
+ onSelectionChange={onSelectionChange}
289
+ renderContent={(id) => <View testID={`content-${id}`} />}
290
+ />
291
+ );
292
+
293
+ await act(async () => {
294
+ rerender(
295
+ <SplitPage
296
+ {...defaultProps}
297
+ onSelectionChange={onSelectionChange}
298
+ renderContent={(id) => <View testID={`content-${id}`} />}
299
+ showItemList
300
+ />
301
+ );
302
+ });
303
+
304
+ // showItemList=true triggers onItemDeselect -> onSelectionChange(undefined)
305
+ expect(onSelectionChange).toHaveBeenCalled();
306
+ });
307
+ });
82
308
  });
@@ -1,4 +1,6 @@
1
- import {describe, expect, it} from "bun:test";
1
+ import {describe, expect, it, mock} from "bun:test";
2
+ import {act, fireEvent} from "@testing-library/react-native";
3
+ import {Linking} from "react-native";
2
4
 
3
5
  import {formatAddress, TapToEdit} from "./TapToEdit";
4
6
  import {renderWithTheme} from "./test-utils";
@@ -90,6 +92,174 @@ describe("TapToEdit", () => {
90
92
  );
91
93
  expect(toJSON()).toMatchSnapshot();
92
94
  });
95
+
96
+ it("renders multiselect type as comma-joined string", () => {
97
+ const {getByText} = renderWithTheme(
98
+ <TapToEdit editable={false} title="Tags" type="multiselect" value={["a", "b", "c"]} />
99
+ );
100
+ expect(getByText("a, b, c")).toBeTruthy();
101
+ });
102
+
103
+ it("renders url type showing hostname for a valid URL", () => {
104
+ const {getByText} = renderWithTheme(
105
+ <TapToEdit editable={false} title="Website" type="url" value="https://example.com/foo" />
106
+ );
107
+ expect(getByText("example.com")).toBeTruthy();
108
+ });
109
+
110
+ it("renders url type falling back to raw value for invalid URL", () => {
111
+ const {getByText} = renderWithTheme(
112
+ <TapToEdit editable={false} title="Website" type="url" value="not-a-url" />
113
+ );
114
+ expect(getByText("not-a-url")).toBeTruthy();
115
+ });
116
+
117
+ it("renders url type with empty value without logging error", () => {
118
+ const {toJSON} = renderWithTheme(
119
+ <TapToEdit editable={false} title="Website" type="url" value="" />
120
+ );
121
+ expect(toJSON()).toBeTruthy();
122
+ });
123
+
124
+ it("renders address type using formatAddress", () => {
125
+ const {getByText} = renderWithTheme(
126
+ <TapToEdit
127
+ editable={false}
128
+ title="Home"
129
+ type="address"
130
+ value={{
131
+ address1: "123 Main St",
132
+ city: "Boston",
133
+ state: "MA",
134
+ zipcode: "02101",
135
+ }}
136
+ />
137
+ );
138
+ expect(getByText(/123 Main St/)).toBeTruthy();
139
+ });
140
+
141
+ it("invokes Linking.openURL for url type when clicked", async () => {
142
+ const originalOpen = Linking.openURL;
143
+ const openMock = mock(() => Promise.resolve(true));
144
+ (Linking as any).openURL = openMock;
145
+
146
+ const {getByLabelText} = renderWithTheme(
147
+ <TapToEdit editable={false} title="Site" type="url" value="https://example.com" />
148
+ );
149
+
150
+ await act(async () => {
151
+ fireEvent.press(getByLabelText("Link"));
152
+ });
153
+ expect(openMock).toHaveBeenCalled();
154
+
155
+ (Linking as any).openURL = originalOpen;
156
+ });
157
+
158
+ it("invokes Linking.openURL with google maps for address type when clicked", async () => {
159
+ const originalOpen = Linking.openURL;
160
+ const openMock = mock(() => Promise.resolve(true));
161
+ (Linking as any).openURL = openMock;
162
+
163
+ const {getByLabelText} = renderWithTheme(
164
+ <TapToEdit
165
+ editable={false}
166
+ title="Home"
167
+ type="address"
168
+ value={{address1: "1 Market St", city: "SF", state: "CA", zipcode: "94105"}}
169
+ />
170
+ );
171
+
172
+ await act(async () => {
173
+ fireEvent.press(getByLabelText("Link"));
174
+ });
175
+ expect(openMock).toHaveBeenCalled();
176
+ const arg = openMock.mock.calls[0][0];
177
+ expect(arg).toContain("google.com/maps");
178
+
179
+ (Linking as any).openURL = originalOpen;
180
+ });
181
+
182
+ it("throws when editable is true and setValue is not provided", () => {
183
+ expect(() =>
184
+ renderWithTheme(<TapToEdit editable title="Required Save" value="foo" />)
185
+ ).toThrow();
186
+ });
187
+
188
+ it("enters editing mode when Edit button is pressed", async () => {
189
+ const setValue = mock(() => {});
190
+ const {getByLabelText, queryByText} = renderWithTheme(
191
+ <TapToEdit setValue={setValue} title="Name" value="Jane" />
192
+ );
193
+ await act(async () => {
194
+ fireEvent.press(getByLabelText("Edit"));
195
+ });
196
+ expect(queryByText("Cancel")).toBeTruthy();
197
+ expect(queryByText("Save")).toBeTruthy();
198
+ });
199
+
200
+ it("calls setValue with initial value and exits editing on Cancel", async () => {
201
+ const setValue = mock(() => {});
202
+ const {getByLabelText, getByText} = renderWithTheme(
203
+ <TapToEdit setValue={setValue} title="Name" value="Jane" />
204
+ );
205
+ await act(async () => {
206
+ fireEvent.press(getByLabelText("Edit"));
207
+ });
208
+ await act(async () => {
209
+ fireEvent.press(getByText("Cancel"));
210
+ });
211
+ expect(setValue).toHaveBeenCalled();
212
+ });
213
+
214
+ it("clears value when Clear button is pressed", async () => {
215
+ const setValue = mock(() => {});
216
+ const onSave = mock(() => Promise.resolve());
217
+ const {getByLabelText, getByText} = renderWithTheme(
218
+ <TapToEdit onSave={onSave} setValue={setValue} showClearButton title="Name" value="Jane" />
219
+ );
220
+ await act(async () => {
221
+ fireEvent.press(getByLabelText("Edit"));
222
+ });
223
+ await act(async () => {
224
+ fireEvent.press(getByText("Clear"));
225
+ });
226
+ expect(setValue).toHaveBeenCalledWith("");
227
+ expect(onSave).toHaveBeenCalledWith("");
228
+ });
229
+
230
+ it("calls onSave when Save is pressed", async () => {
231
+ const setValue = mock(() => {});
232
+ const onSave = mock(() => Promise.resolve());
233
+ const {getByLabelText, getByText} = renderWithTheme(
234
+ <TapToEdit onSave={onSave} setValue={setValue} title="Name" value="Jane" />
235
+ );
236
+ await act(async () => {
237
+ fireEvent.press(getByLabelText("Edit"));
238
+ });
239
+ await act(async () => {
240
+ fireEvent.press(getByText("Save"));
241
+ });
242
+ expect(onSave).toHaveBeenCalledWith("Jane");
243
+ });
244
+
245
+ it("logs error when saving without onSave", async () => {
246
+ const setValue = mock(() => {});
247
+ const originalError = console.error;
248
+ const errorMock = mock(() => {});
249
+ console.error = errorMock;
250
+
251
+ const {getByLabelText, getByText} = renderWithTheme(
252
+ <TapToEdit setValue={setValue} title="Name" value="Jane" />
253
+ );
254
+ await act(async () => {
255
+ fireEvent.press(getByLabelText("Edit"));
256
+ });
257
+ await act(async () => {
258
+ fireEvent.press(getByText("Save"));
259
+ });
260
+ expect(errorMock).toHaveBeenCalled();
261
+ console.error = originalError;
262
+ });
93
263
  });
94
264
 
95
265
  describe("formatAddress", () => {
@@ -1,8 +1,23 @@
1
- import {describe, expect, it} from "bun:test";
2
- import {render} from "@testing-library/react-native";
1
+ import {beforeAll, describe, expect, it, mock} from "bun:test";
2
+ import {act, render} from "@testing-library/react-native";
3
3
  import {Text, View} from "react-native";
4
4
 
5
5
  import {TerrenoProvider} from "./TerrenoProvider";
6
+ import {useToast} from "./Toast";
7
+
8
+ interface RafGlobal {
9
+ requestAnimationFrame?: (callback: FrameRequestCallback) => number;
10
+ cancelAnimationFrame?: (id: number) => void;
11
+ }
12
+
13
+ beforeAll(() => {
14
+ const g = globalThis as RafGlobal;
15
+ if (!g.requestAnimationFrame) {
16
+ g.requestAnimationFrame = (callback) =>
17
+ setTimeout(() => callback(Date.now()), 0) as unknown as number;
18
+ g.cancelAnimationFrame = (id) => clearTimeout(id);
19
+ }
20
+ });
6
21
 
7
22
  describe("TerrenoProvider", () => {
8
23
  it("renders children correctly", () => {
@@ -33,4 +48,29 @@ describe("TerrenoProvider", () => {
33
48
  );
34
49
  expect(toJSON()).toMatchSnapshot();
35
50
  });
51
+
52
+ it("renders a toast via the configured renderToast prop", async () => {
53
+ const dismissSpy = mock(() => {});
54
+ let toastApi: ReturnType<typeof useToast> | null = null;
55
+
56
+ const ToastCaller = () => {
57
+ toastApi = useToast();
58
+ return <Text>App</Text>;
59
+ };
60
+
61
+ const {queryByText} = render(
62
+ <TerrenoProvider>
63
+ <ToastCaller />
64
+ </TerrenoProvider>
65
+ );
66
+
67
+ expect(toastApi).not.toBeNull();
68
+
69
+ await act(async () => {
70
+ toastApi?.info("Hello from toast", {onDismiss: dismissSpy});
71
+ await new Promise((resolve) => setTimeout(resolve, 0));
72
+ });
73
+
74
+ expect(queryByText("Hello from toast")).toBeTruthy();
75
+ });
36
76
  });
@@ -12,7 +12,7 @@ export class NumberPickerActionSheet extends React.Component<
12
12
  TextFieldPickerActionSheetProps,
13
13
  NumberPickerActionSheetState
14
14
  > {
15
- render() {
15
+ render(): React.ReactElement {
16
16
  return (
17
17
  <ActionSheet bounceOnOpen gestureEnabled ref={this.props.actionSheetRef}>
18
18
  <Box marginBottom={8} paddingX={4} width="100%">
@@ -1,9 +1,12 @@
1
1
  import {describe, expect, it} from "bun:test";
2
- import {render} from "@testing-library/react-native";
2
+ import {act, render} from "@testing-library/react-native";
3
3
  import {Text, View} from "react-native";
4
4
 
5
5
  import {ThemeProvider, useTheme} from "./Theme";
6
6
 
7
+ type ThemeContextValue = ReturnType<typeof useTheme>;
8
+ type ThemeValue = ThemeContextValue["theme"];
9
+
7
10
  const ThemeConsumer = () => {
8
11
  const {theme} = useTheme();
9
12
  return (
@@ -40,7 +43,7 @@ describe("Theme", () => {
40
43
 
41
44
  describe("useTheme", () => {
42
45
  it("returns theme object", () => {
43
- let capturedTheme: any;
46
+ let capturedTheme: ThemeContextValue | undefined;
44
47
  const Capture = () => {
45
48
  capturedTheme = useTheme();
46
49
  return null;
@@ -52,14 +55,14 @@ describe("Theme", () => {
52
55
  </ThemeProvider>
53
56
  );
54
57
 
55
- expect(capturedTheme.theme).toBeDefined();
56
- expect(capturedTheme.setTheme).toBeDefined();
57
- expect(capturedTheme.setPrimitives).toBeDefined();
58
- expect(capturedTheme.resetTheme).toBeDefined();
58
+ expect(capturedTheme?.theme).toBeDefined();
59
+ expect(capturedTheme?.setTheme).toBeDefined();
60
+ expect(capturedTheme?.setPrimitives).toBeDefined();
61
+ expect(capturedTheme?.resetTheme).toBeDefined();
59
62
  });
60
63
 
61
64
  it("provides surface colors", () => {
62
- let theme: any;
65
+ let theme: ThemeValue | undefined;
63
66
  const Capture = () => {
64
67
  theme = useTheme().theme;
65
68
  return null;
@@ -71,14 +74,14 @@ describe("Theme", () => {
71
74
  </ThemeProvider>
72
75
  );
73
76
 
74
- expect(theme.surface).toBeDefined();
75
- expect(theme.surface.base).toBeDefined();
76
- expect(theme.surface.primary).toBeDefined();
77
- expect(theme.surface.error).toBeDefined();
77
+ expect(theme?.surface).toBeDefined();
78
+ expect(theme?.surface?.base).toBeDefined();
79
+ expect(theme?.surface?.primary).toBeDefined();
80
+ expect(theme?.surface?.error).toBeDefined();
78
81
  });
79
82
 
80
83
  it("provides text colors", () => {
81
- let theme: any;
84
+ let theme: ThemeValue | undefined;
82
85
  const Capture = () => {
83
86
  theme = useTheme().theme;
84
87
  return null;
@@ -90,14 +93,14 @@ describe("Theme", () => {
90
93
  </ThemeProvider>
91
94
  );
92
95
 
93
- expect(theme.text).toBeDefined();
94
- expect(theme.text.primary).toBeDefined();
95
- expect(theme.text.inverted).toBeDefined();
96
- expect(theme.text.error).toBeDefined();
96
+ expect(theme?.text).toBeDefined();
97
+ expect(theme?.text?.primary).toBeDefined();
98
+ expect(theme?.text?.inverted).toBeDefined();
99
+ expect(theme?.text?.error).toBeDefined();
97
100
  });
98
101
 
99
102
  it("provides border colors", () => {
100
- let theme: any;
103
+ let theme: ThemeValue | undefined;
101
104
  const Capture = () => {
102
105
  theme = useTheme().theme;
103
106
  return null;
@@ -109,12 +112,12 @@ describe("Theme", () => {
109
112
  </ThemeProvider>
110
113
  );
111
114
 
112
- expect(theme.border).toBeDefined();
113
- expect(theme.border.default).toBeDefined();
115
+ expect(theme?.border).toBeDefined();
116
+ expect(theme?.border?.default).toBeDefined();
114
117
  });
115
118
 
116
119
  it("provides spacing values", () => {
117
- let theme: any;
120
+ let theme: ThemeValue | undefined;
118
121
  const Capture = () => {
119
122
  theme = useTheme().theme;
120
123
  return null;
@@ -126,14 +129,14 @@ describe("Theme", () => {
126
129
  </ThemeProvider>
127
130
  );
128
131
 
129
- expect(theme.spacing).toBeDefined();
130
- expect(theme.spacing.sm).toBeDefined();
131
- expect(theme.spacing.md).toBeDefined();
132
- expect(theme.spacing.lg).toBeDefined();
132
+ expect(theme?.spacing).toBeDefined();
133
+ expect(theme?.spacing?.sm).toBeDefined();
134
+ expect(theme?.spacing?.md).toBeDefined();
135
+ expect(theme?.spacing?.lg).toBeDefined();
133
136
  });
134
137
 
135
138
  it("provides radius values", () => {
136
- let theme: any;
139
+ let theme: ThemeValue | undefined;
137
140
  const Capture = () => {
138
141
  theme = useTheme().theme;
139
142
  return null;
@@ -145,9 +148,96 @@ describe("Theme", () => {
145
148
  </ThemeProvider>
146
149
  );
147
150
 
148
- expect(theme.radius).toBeDefined();
149
- expect(theme.radius.default).toBeDefined();
150
- expect(theme.radius.rounded).toBeDefined();
151
+ expect(theme?.radius).toBeDefined();
152
+ expect(theme?.radius?.default).toBeDefined();
153
+ expect(theme?.radius?.rounded).toBeDefined();
154
+ });
155
+
156
+ it("updates theme when setTheme is called", () => {
157
+ let captured: ThemeContextValue | undefined;
158
+ const Capture = () => {
159
+ captured = useTheme();
160
+ return null;
161
+ };
162
+ render(
163
+ <ThemeProvider>
164
+ <Capture />
165
+ </ThemeProvider>
166
+ );
167
+ act(() => {
168
+ captured?.setTheme({surface: {base: "error100"}});
169
+ });
170
+ expect(captured?.theme.surface.base).toBe("#D33232");
171
+ });
172
+
173
+ it("updates primitives when setPrimitives is called", () => {
174
+ let captured: ThemeContextValue | undefined;
175
+ const Capture = () => {
176
+ captured = useTheme();
177
+ return null;
178
+ };
179
+ render(
180
+ <ThemeProvider>
181
+ <Capture />
182
+ </ThemeProvider>
183
+ );
184
+ act(() => {
185
+ captured?.setPrimitives({neutral000: "#AABBCC"});
186
+ });
187
+ expect(captured?.theme.surface.base).toBe("#AABBCC");
188
+ });
189
+
190
+ it("resets theme to default when resetTheme is called", () => {
191
+ let captured: ThemeContextValue | undefined;
192
+ const Capture = () => {
193
+ captured = useTheme();
194
+ return null;
195
+ };
196
+ render(
197
+ <ThemeProvider>
198
+ <Capture />
199
+ </ThemeProvider>
200
+ );
201
+ act(() => {
202
+ captured?.setTheme({surface: {base: "error100"}});
203
+ captured?.setPrimitives({neutral000: "#123456"});
204
+ });
205
+ act(() => {
206
+ captured?.resetTheme();
207
+ });
208
+ expect(captured?.theme.surface.base).toBe("#FFFFFF");
209
+ });
210
+
211
+ it("supports non-object top-level values when setTheme is called", () => {
212
+ let captured: ThemeContextValue | undefined;
213
+ const Capture = () => {
214
+ captured = useTheme();
215
+ return null;
216
+ };
217
+ render(
218
+ <ThemeProvider>
219
+ <Capture />
220
+ </ThemeProvider>
221
+ );
222
+ act(() => {
223
+ captured?.setTheme({primitives: undefined});
224
+ });
225
+ expect(captured?.theme).toBeDefined();
226
+ });
227
+
228
+ it("invokes the no-op default context setters when rendered without a provider", () => {
229
+ let captured: ThemeContextValue | undefined;
230
+ const Capture = () => {
231
+ captured = useTheme();
232
+ return null;
233
+ };
234
+ render(<Capture />);
235
+ // Exercise the default no-op callbacks on the context.
236
+ expect(() => {
237
+ captured?.resetTheme();
238
+ captured?.setPrimitives({neutral000: "#000000"});
239
+ captured?.setTheme({surface: {base: "neutral000"}});
240
+ }).not.toThrow();
151
241
  });
152
242
  });
153
243
  });