@terreno/ui 0.9.2 → 0.10.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/src/Common.ts CHANGED
@@ -2066,7 +2066,7 @@ export interface PaginationProps {
2066
2066
  * Data Table
2067
2067
  */
2068
2068
  export interface DataTableCellData {
2069
- value: any;
2069
+ value: unknown;
2070
2070
  highlight?: SurfaceColor;
2071
2071
  textSize?: "sm" | "md" | "lg";
2072
2072
  }
@@ -2085,7 +2085,7 @@ export interface DataTableColumn {
2085
2085
  }
2086
2086
 
2087
2087
  export interface DataTableProps {
2088
- data: {value: any; highlight?: SurfaceColor; textSize?: "sm" | "md" | "lg"}[][];
2088
+ data: DataTableCellData[][];
2089
2089
  columns: DataTableColumn[];
2090
2090
  alternateRowBackground?: boolean;
2091
2091
  totalPages?: number;
@@ -2100,19 +2100,21 @@ export interface DataTableProps {
2100
2100
  /**
2101
2101
  * When tapping the eye icon, a modal is shown with more info about the row.
2102
2102
  */
2103
- moreContentComponent?: React.ComponentType<{
2104
- column: DataTableColumn;
2105
- rowData: any[];
2106
- rowIndex: number;
2107
- }>;
2103
+ moreContentComponent?: React.ComponentType<
2104
+ {
2105
+ column: DataTableColumn;
2106
+ rowData: DataTableCellData[];
2107
+ rowIndex: number;
2108
+ } & Record<string, unknown>
2109
+ >;
2108
2110
  // Extra data to pass to the more modal.
2109
- moreContentExtraData?: any[];
2111
+ moreContentExtraData?: Record<string, unknown>[];
2110
2112
  // Allows handling of custom column types.
2111
2113
  customColumnComponentMap?: DataTableCustomComponentMap;
2112
2114
  }
2113
2115
 
2114
2116
  export interface DataTableCellProps {
2115
- value: any;
2117
+ value: DataTableCellData;
2116
2118
  columnDef: DataTableColumn;
2117
2119
  colIndex: number;
2118
2120
  isPinnedHorizontal: boolean;
@@ -17,7 +17,7 @@ import type {ConsentHistoryEntry} from "./useConsentHistory";
17
17
  import {useConsentHistory} from "./useConsentHistory";
18
18
 
19
19
  interface ConsentHistoryProps {
20
- api: any;
20
+ api: Parameters<typeof useConsentHistory>[0];
21
21
  baseUrl?: string;
22
22
  title?: string;
23
23
  }
@@ -33,7 +33,7 @@ const createMockApi = (forms: ConsentFormPublic[]) => {
33
33
  mockSubmitMutation.mockReturnValue({unwrap: mockUnwrap});
34
34
 
35
35
  const innerApi = {
36
- injectEndpoints: mock((_config: any) => ({
36
+ injectEndpoints: mock((_config: unknown) => ({
37
37
  useGetPendingConsentsQuery: mock(() => ({
38
38
  data: {data: forms},
39
39
  error: undefined,
@@ -48,13 +48,13 @@ const createMockApi = (forms: ConsentFormPublic[]) => {
48
48
  };
49
49
 
50
50
  return {
51
- enhanceEndpoints: mock((_config: any) => innerApi),
51
+ enhanceEndpoints: mock((_config: unknown) => innerApi),
52
52
  };
53
53
  };
54
54
 
55
55
  const createLoadingMockApi = () => {
56
56
  const innerApi = {
57
- injectEndpoints: mock((_config: any) => ({
57
+ injectEndpoints: mock((_config: unknown) => ({
58
58
  useGetPendingConsentsQuery: mock(() => ({
59
59
  data: undefined,
60
60
  error: undefined,
@@ -69,7 +69,7 @@ const createLoadingMockApi = () => {
69
69
  };
70
70
 
71
71
  return {
72
- enhanceEndpoints: mock((_config: any) => innerApi),
72
+ enhanceEndpoints: mock((_config: unknown) => innerApi),
73
73
  };
74
74
  };
75
75
 
package/src/DataTable.tsx CHANGED
@@ -32,23 +32,21 @@ import {TableTitle} from "./table/TableTitle";
32
32
  // easily.
33
33
 
34
34
  const TextCell: FC<{
35
- cellData: {value: string; textSize?: "sm" | "md" | "lg"};
35
+ cellData: DataTableCellData;
36
36
  column: DataTableColumn;
37
37
  }> = ({cellData}) => {
38
38
  return (
39
39
  <Box flex="grow" justifyContent="center">
40
- <Text size={cellData.textSize || "md"}>{cellData.value}</Text>
40
+ <Text size={cellData.textSize || "md"}>{String(cellData.value ?? "")}</Text>
41
41
  </Box>
42
42
  );
43
43
  };
44
44
 
45
- const CheckedCell: FC<{cellData: {value: boolean}; column: DataTableColumn}> = ({cellData}) => {
45
+ const CheckedCell: FC<{cellData: DataTableCellData; column: DataTableColumn}> = ({cellData}) => {
46
+ const isChecked = Boolean(cellData.value);
46
47
  return (
47
48
  <Box flex="grow" justifyContent="center" width="100%">
48
- <Icon
49
- color={cellData.value ? "success" : "secondaryDark"}
50
- iconName={cellData.value ? "check" : "x"}
51
- />
49
+ <Icon color={isChecked ? "success" : "secondaryDark"} iconName={isChecked ? "check" : "x"} />
52
50
  </Box>
53
51
  );
54
52
  };
@@ -71,7 +69,7 @@ const DataTableCell: FC<DataTableCellProps> = ({
71
69
  // Default to TextCell
72
70
  let Component: React.ComponentType<{
73
71
  column: DataTableColumn;
74
- cellData: {value: any; highlight?: SurfaceColor};
72
+ cellData: DataTableCellData;
75
73
  }> = TextCell;
76
74
  if (customColumnComponentMap?.[columnDef.columnType]) {
77
75
  Component = customColumnComponentMap[columnDef.columnType];
@@ -401,20 +399,22 @@ const DataTableHeader: FC<DataTableHeaderProps> = ({
401
399
  };
402
400
 
403
401
  interface DataTableContentProps {
404
- data: any[][];
402
+ data: DataTableCellData[][];
405
403
  columns: DataTableColumn[];
406
404
  pinnedColumns: number;
407
405
  alternateRowBackground: boolean;
408
406
  columnWidths: number[];
409
407
  bodyScrollRef: React.RefObject<ScrollView | null>;
410
408
  onScroll: (event: NativeSyntheticEvent<NativeScrollEvent>, isHeader: boolean) => void;
411
- moreContentComponent?: React.ComponentType<{
412
- column: DataTableColumn;
413
- rowData: any[];
414
- rowIndex: number;
415
- }>;
409
+ moreContentComponent?: React.ComponentType<
410
+ {
411
+ column: DataTableColumn;
412
+ rowData: DataTableCellData[];
413
+ rowIndex: number;
414
+ } & Record<string, unknown>
415
+ >;
416
416
  // Extra props to pass to the more modal, one per row.
417
- moreContentExtraData?: any[];
417
+ moreContentExtraData?: Record<string, unknown>[];
418
418
  moreContentSize?: "sm" | "md" | "lg";
419
419
  customColumnComponentMap?: DataTableCustomComponentMap;
420
420
  rowHeight: number;
@@ -25,9 +25,11 @@ describe("DateUtilities", () => {
25
25
 
26
26
  describe("humanDate", () => {
27
27
  it("should return invalid date if date is undefined", () => {
28
- expect(() => humanDate(undefined as any, {timezone: "America/New_York"})).toThrow(
29
- "humanDate: Passed undefined"
30
- );
28
+ expect(() =>
29
+ humanDate(undefined as unknown as Parameters<typeof humanDate>[0], {
30
+ timezone: "America/New_York",
31
+ })
32
+ ).toThrow("humanDate: Passed undefined");
31
33
  });
32
34
 
33
35
  it("should throw an error if date is invalid", () => {
@@ -95,9 +97,11 @@ describe("DateUtilities", () => {
95
97
 
96
98
  describe("humanDateTime", () => {
97
99
  it("should return invalid date if date is undefined", () => {
98
- expect(() => humanDateAndTime(undefined as any, {timezone: "America/New_York"})).toThrow(
99
- "humanDateAndTime: Passed undefined"
100
- );
100
+ expect(() =>
101
+ humanDateAndTime(undefined as unknown as Parameters<typeof humanDateAndTime>[0], {
102
+ timezone: "America/New_York",
103
+ })
104
+ ).toThrow("humanDateAndTime: Passed undefined");
101
105
  });
102
106
 
103
107
  it("should throw an error if date is invalid", () => {
@@ -232,7 +236,9 @@ describe("DateUtilities", () => {
232
236
 
233
237
  describe("printDate", () => {
234
238
  it("should throw an error if date is undefined", () => {
235
- expect(printDate(undefined as any)).toBe("Invalid Date");
239
+ expect(printDate(undefined as unknown as Parameters<typeof printDate>[0])).toBe(
240
+ "Invalid Date"
241
+ );
236
242
  });
237
243
 
238
244
  it("should throw an error if date is invalid", () => {
@@ -270,11 +276,17 @@ describe("DateUtilities", () => {
270
276
 
271
277
  describe("printOnlyDate", () => {
272
278
  it("should print invalid if no date and no default", () => {
273
- expect(printOnlyDate(undefined as any)).toBe("Invalid Date");
279
+ expect(printOnlyDate(undefined as unknown as Parameters<typeof printOnlyDate>[0])).toBe(
280
+ "Invalid Date"
281
+ );
274
282
  });
275
283
 
276
284
  it("should print default if no date and default", () => {
277
- expect(printOnlyDate(undefined as any, {defaultValue: "default"})).toBe("default");
285
+ expect(
286
+ printOnlyDate(undefined as unknown as Parameters<typeof printOnlyDate>[0], {
287
+ defaultValue: "default",
288
+ })
289
+ ).toBe("default");
278
290
  });
279
291
 
280
292
  it("should print the date in the default format", () => {
@@ -287,9 +299,11 @@ describe("DateUtilities", () => {
287
299
 
288
300
  describe("printDateTime", () => {
289
301
  it("should return invalid date if date is undefined", () => {
290
- expect(printDateAndTime(undefined as any, {timezone: "America/New_York"})).toBe(
291
- "Invalid Datetime"
292
- );
302
+ expect(
303
+ printDateAndTime(undefined as unknown as Parameters<typeof printDateAndTime>[0], {
304
+ timezone: "America/New_York",
305
+ })
306
+ ).toBe("Invalid Datetime");
293
307
  });
294
308
 
295
309
  it("should throw an error if date is invalid", () => {
@@ -339,7 +353,11 @@ describe("DateUtilities", () => {
339
353
 
340
354
  describe("printTime", () => {
341
355
  it("should return invalid date if date is undefined", () => {
342
- expect(printTime(undefined as any, {timezone: "America/New_York"})).toBe("Invalid Date");
356
+ expect(
357
+ printTime(undefined as unknown as Parameters<typeof printTime>[0], {
358
+ timezone: "America/New_York",
359
+ })
360
+ ).toBe("Invalid Date");
343
361
  });
344
362
 
345
363
  it("should throw an error if date is invalid", () => {
@@ -349,9 +367,9 @@ describe("DateUtilities", () => {
349
367
  });
350
368
 
351
369
  it("should throw an error with no timezone", () => {
352
- expect(() => printTime("2022-12-24T12:00:00.000Z", {} as any)).toThrow(
353
- "printTime: timezone is required"
354
- );
370
+ expect(() =>
371
+ printTime("2022-12-24T12:00:00.000Z", {} as unknown as Parameters<typeof printTime>[1])
372
+ ).toThrow("printTime: timezone is required");
355
373
  });
356
374
 
357
375
  it("should return the time in the default format", () => {
@@ -1,5 +1,12 @@
1
1
  import {DateTime} from "luxon";
2
2
 
3
+ const getErrorMessage = (error: unknown): string => {
4
+ if (error instanceof Error) {
5
+ return error.message;
6
+ }
7
+ return String(error);
8
+ };
9
+
3
10
  function getDate(date: string, {timezone}: {timezone?: string} = {}): DateTime {
4
11
  if (!date) {
5
12
  throw new Error("Passed undefined");
@@ -57,8 +64,8 @@ export function humanDate(
57
64
  let clonedDate;
58
65
  try {
59
66
  clonedDate = getDate(date, {timezone});
60
- } catch (error: any) {
61
- throw new Error(`humanDate: ${error.message}`);
67
+ } catch (error: unknown) {
68
+ throw new Error(`humanDate: ${getErrorMessage(error)}`);
62
69
  }
63
70
  if (isTomorrow(date, {timezone})) {
64
71
  return "Tomorrow";
@@ -91,8 +98,8 @@ export function humanDateAndTime(
91
98
  let clonedDate;
92
99
  try {
93
100
  clonedDate = getDate(date, {timezone});
94
- } catch (error: any) {
95
- throw new Error(`humanDateAndTime: ${error.message}`);
101
+ } catch (error: unknown) {
102
+ throw new Error(`humanDateAndTime: ${getErrorMessage(error)}`);
96
103
  }
97
104
  // This should maybe use printTime()
98
105
  let time: string = "";
@@ -158,8 +165,8 @@ export const printDate = (
158
165
  let clonedDate;
159
166
  try {
160
167
  clonedDate = getDate(date, {timezone});
161
- } catch (error: any) {
162
- throw new Error(`printDate: ${error.message}`);
168
+ } catch (error: unknown) {
169
+ throw new Error(`printDate: ${getErrorMessage(error)}`);
163
170
  }
164
171
 
165
172
  return clonedDate.toLocaleString(DateTime.DATE_SHORT);
@@ -213,8 +220,8 @@ export function printTime(
213
220
  }
214
221
  try {
215
222
  clonedDate = getDate(date, {timezone});
216
- } catch (error: any) {
217
- throw new Error(`printTime: ${error.message}`);
223
+ } catch (error: unknown) {
224
+ throw new Error(`printTime: ${getErrorMessage(error)}`);
218
225
  }
219
226
  if (showTimezone) {
220
227
  return clonedDate.toLocaleString({
@@ -246,8 +253,8 @@ export function printDateAndTime(
246
253
  let clonedDate;
247
254
  try {
248
255
  clonedDate = getDate(date, {timezone});
249
- } catch (error: any) {
250
- throw new Error(`printDateAndTime: ${error.message}`);
256
+ } catch (error: unknown) {
257
+ throw new Error(`printDateAndTime: ${getErrorMessage(error)}`);
251
258
  }
252
259
  if (showTimezone) {
253
260
  return clonedDate.toLocaleString({
@@ -304,8 +311,8 @@ export function printSince(
304
311
  const ago = showAgo ? " ago" : "";
305
312
  try {
306
313
  clonedDate = getDate(date, {timezone});
307
- } catch (error: any) {
308
- throw new Error(`printSince: ${error.message}`);
314
+ } catch (error: unknown) {
315
+ throw new Error(`printSince: ${getErrorMessage(error)}`);
309
316
  }
310
317
  const now = timezone ? DateTime.now().setZone(timezone) : DateTime.now();
311
318
  const diff = now.diff(clonedDate, "months");
@@ -349,7 +356,11 @@ export function getTimezoneOptions(location: "USA" | "Worldwide", shortTimezone
349
356
  if (location === "USA") {
350
357
  timezones = usTimezoneOptions.map((tz) => [tz.label, tz.value]);
351
358
  } else {
352
- timezones = (Intl as any).supportedValuesOf("timeZone").map((tz: any) => {
359
+ const intlWithSupportedValuesOf = Intl as typeof Intl & {
360
+ supportedValuesOf?: (key: "timeZone") => string[];
361
+ };
362
+ const supportedValues = intlWithSupportedValuesOf.supportedValuesOf?.("timeZone") ?? [];
363
+ timezones = supportedValues.map((tz) => {
353
364
  return [tz, tz];
354
365
  });
355
366
  }
@@ -6,7 +6,7 @@
6
6
  // Copyright Patryk Jaworski @gerwld
7
7
 
8
8
  import React, {useMemo, useState} from "react";
9
- import {Platform, View} from "react-native";
9
+ import {Platform, type StyleProp, View, type ViewStyle} from "react-native";
10
10
  import {Gesture, GestureDetector} from "react-native-gesture-handler";
11
11
  import Animated, {
12
12
  runOnJS,
@@ -41,7 +41,7 @@ interface DragItemProps {
41
41
  renderGrip?: React.ReactElement | (() => React.ReactElement); // Optional drag handle
42
42
  passVibration?: () => void; // Optional haptic feedback callback
43
43
  itemBorderRadius: number; // Border radius for items
44
- itemContainerStyle?: any; // Additional styling for item container
44
+ itemContainerStyle?: StyleProp<ViewStyle>; // Additional styling for item container
45
45
  callbackNewDataIds?: (newIds: string[]) => void; // Callback when items are reordered
46
46
  backgroundOnHold?: string; // Background color when item is being dragged
47
47
  plainPosition: number; // Current position in the list
@@ -52,10 +52,10 @@ interface DragItemProps {
52
52
  */
53
53
  interface DragListProps {
54
54
  data?: string[]; // Array of item IDs (deprecated, use dataIDs)
55
- style?: any; // Style for the list container
55
+ style?: StyleProp<ViewStyle>; // Style for the list container
56
56
  callbackNewDataIds: (newIds: string[]) => void; // Callback when items are reordered
57
- contentContainerStyle?: any; // Style for the content container
58
- itemContainerStyle?: any; // Style for each item container
57
+ contentContainerStyle?: StyleProp<ViewStyle>; // Style for the content container
58
+ itemContainerStyle?: StyleProp<ViewStyle>; // Style for each item container
59
59
  renderItem: (props: {item: string}) => React.ReactElement; // Function to render item content
60
60
  renderGrip?: React.ReactElement | (() => React.ReactElement); // Optional custom drag handle
61
61
  passVibration?: () => void; // Optional haptic feedback callback
@@ -1,6 +1,8 @@
1
- import {afterAll, beforeAll, describe, expect, it, mock} from "bun:test";
1
+ import {afterAll, beforeAll, describe, expect, it, mock, spyOn} from "bun:test";
2
+ import React from "react";
2
3
 
3
4
  import {ErrorBoundary} from "./ErrorBoundary";
5
+ import {ErrorPage} from "./ErrorPage";
4
6
  import {Text} from "./Text";
5
7
  import {renderWithTheme} from "./test-utils";
6
8
 
@@ -41,4 +43,53 @@ describe("ErrorBoundary", () => {
41
43
  );
42
44
  expect(toJSON()).toMatchSnapshot();
43
45
  });
46
+
47
+ it("sets state from getDerivedStateFromError", () => {
48
+ const error = new Error("derived");
49
+
50
+ const result = ErrorBoundary.getDerivedStateFromError(error);
51
+
52
+ expect(result).toEqual({error});
53
+ });
54
+
55
+ it("calls onError when componentDidCatch receives an error", () => {
56
+ const onError = mock(() => {});
57
+ const boundary = new ErrorBoundary({children: null, onError});
58
+ const error = new Error("caught");
59
+
60
+ boundary.componentDidCatch(error, {componentStack: "stack trace"});
61
+
62
+ expect(onError).toHaveBeenCalledTimes(1);
63
+ expect((onError as any).mock.calls[0]).toEqual([error, "stack trace"]);
64
+ });
65
+
66
+ it("does not throw when componentDidCatch is called without onError", () => {
67
+ const boundary = new ErrorBoundary({children: null});
68
+ const error = new Error("caught");
69
+
70
+ expect(() => boundary.componentDidCatch(error, {componentStack: "stack trace"})).not.toThrow();
71
+ });
72
+
73
+ it("resets error state when resetError is called", () => {
74
+ const boundary = new ErrorBoundary({children: null});
75
+ const setStateSpy = spyOn(boundary, "setState");
76
+
77
+ boundary.resetError();
78
+
79
+ expect(setStateSpy).toHaveBeenCalledTimes(1);
80
+ expect((setStateSpy as any).mock.calls[0][0]).toEqual({error: undefined});
81
+ setStateSpy.mockRestore();
82
+ });
83
+
84
+ it("renders ErrorPage when state has an error", () => {
85
+ const boundary = new ErrorBoundary({children: <Text>Child content</Text>});
86
+ const error = new Error("render error");
87
+ boundary.state = {error};
88
+
89
+ const renderedElement = boundary.render() as React.ReactElement;
90
+
91
+ expect(renderedElement.type).toBe(ErrorPage);
92
+ expect(renderedElement.props.error).toBe(error);
93
+ expect(typeof renderedElement.props.resetError).toBe("function");
94
+ });
44
95
  });
package/src/Icon.tsx CHANGED
@@ -4,9 +4,6 @@ import type {FC} from "react";
4
4
  import {type IconProps, iconSizeToNumber} from "./Common";
5
5
  import {useTheme} from "./Theme";
6
6
 
7
- // TODO: Update <Icon /> to be closer to Expo's Vector Icon, letting multiple icon packs be used,
8
- // etc.
9
- // TODO: Add documentation for adding FA6-Pro icons.
10
7
  export const Icon: FC<IconProps> = ({
11
8
  color = "primary",
12
9
  size = "md",
@@ -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
  });
@@ -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
  };