@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/dist/Common.d.ts +6 -10
- package/dist/Common.js.map +1 -1
- package/dist/ConsentHistory.d.ts +2 -1
- package/dist/DataTable.js +4 -2
- package/dist/DataTable.js.map +1 -1
- package/dist/DateUtilities.js +16 -7
- package/dist/DateUtilities.js.map +1 -1
- package/dist/DraggableList.d.ts +5 -4
- package/dist/DraggableList.js.map +1 -1
- package/dist/Icon.js +0 -3
- package/dist/Icon.js.map +1 -1
- package/dist/PasswordField.d.ts +1 -1
- package/dist/PasswordField.js.map +1 -1
- package/dist/table/TableHeaderCell.js.map +1 -1
- package/dist/useConsentForms.d.ts +38 -4
- package/dist/useConsentForms.js +1 -1
- package/dist/useConsentForms.js.map +1 -1
- package/dist/useConsentHistory.d.ts +30 -4
- package/dist/useConsentHistory.js.map +1 -1
- package/dist/useSubmitConsent.d.ts +40 -4
- package/dist/useSubmitConsent.js.map +1 -1
- package/package.json +4 -1
- package/src/Common.ts +11 -9
- package/src/ConsentHistory.tsx +1 -1
- package/src/ConsentNavigator.test.tsx +4 -4
- package/src/DataTable.tsx +15 -15
- package/src/DateUtilities.test.ts +34 -16
- package/src/DateUtilities.tsx +24 -13
- package/src/DraggableList.tsx +5 -5
- package/src/ErrorBoundary.test.tsx +52 -1
- package/src/Icon.tsx +0 -3
- package/src/OpenAPIContext.test.tsx +184 -3
- package/src/PasswordField.tsx +1 -1
- package/src/table/TableBadge.test.tsx +36 -0
- package/src/table/TableHeaderCell.tsx +1 -1
- package/src/useConsentForms.ts +39 -4
- package/src/useConsentHistory.ts +26 -2
- package/src/useSubmitConsent.ts +38 -2
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:
|
|
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:
|
|
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
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
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?:
|
|
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:
|
|
2117
|
+
value: DataTableCellData;
|
|
2116
2118
|
columnDef: DataTableColumn;
|
|
2117
2119
|
colIndex: number;
|
|
2118
2120
|
isPinnedHorizontal: boolean;
|
package/src/ConsentHistory.tsx
CHANGED
|
@@ -17,7 +17,7 @@ import type {ConsentHistoryEntry} from "./useConsentHistory";
|
|
|
17
17
|
import {useConsentHistory} from "./useConsentHistory";
|
|
18
18
|
|
|
19
19
|
interface ConsentHistoryProps {
|
|
20
|
-
api:
|
|
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:
|
|
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:
|
|
51
|
+
enhanceEndpoints: mock((_config: unknown) => innerApi),
|
|
52
52
|
};
|
|
53
53
|
};
|
|
54
54
|
|
|
55
55
|
const createLoadingMockApi = () => {
|
|
56
56
|
const innerApi = {
|
|
57
|
-
injectEndpoints: mock((_config:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
413
|
-
|
|
414
|
-
|
|
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?:
|
|
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(() =>
|
|
29
|
-
|
|
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(() =>
|
|
99
|
-
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
291
|
-
|
|
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(
|
|
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(() =>
|
|
353
|
-
"
|
|
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", () => {
|
package/src/DateUtilities.tsx
CHANGED
|
@@ -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:
|
|
61
|
-
throw new Error(`humanDate: ${error
|
|
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:
|
|
95
|
-
throw new Error(`humanDateAndTime: ${error
|
|
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:
|
|
162
|
-
throw new Error(`printDate: ${error
|
|
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:
|
|
217
|
-
throw new Error(`printTime: ${error
|
|
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:
|
|
250
|
-
throw new Error(`printDateAndTime: ${error
|
|
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:
|
|
308
|
-
throw new Error(`printSince: ${error
|
|
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
|
-
|
|
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
|
}
|
package/src/DraggableList.tsx
CHANGED
|
@@ -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?:
|
|
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?:
|
|
55
|
+
style?: StyleProp<ViewStyle>; // Style for the list container
|
|
56
56
|
callbackNewDataIds: (newIds: string[]) => void; // Callback when items are reordered
|
|
57
|
-
contentContainerStyle?:
|
|
58
|
-
itemContainerStyle?:
|
|
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 {
|
|
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
|
});
|