@terreno/ui 0.14.0 → 0.14.2
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/ActionSheet.d.ts +1 -1
- package/dist/ActionSheet.js +17 -29
- package/dist/ActionSheet.js.map +1 -1
- package/dist/Common.d.ts +8 -2
- package/dist/Common.js +4 -4
- package/dist/Common.js.map +1 -1
- package/dist/ConsentFormScreen.js +3 -3
- package/dist/ConsentFormScreen.js.map +1 -1
- package/dist/DateUtilities.d.ts +25 -25
- package/dist/DateUtilities.js +31 -32
- package/dist/DateUtilities.js.map +1 -1
- package/dist/MarkdownView.js +20 -7
- package/dist/MarkdownView.js.map +1 -1
- package/dist/MediaQuery.d.ts +4 -4
- package/dist/MediaQuery.js +8 -8
- package/dist/MediaQuery.js.map +1 -1
- package/dist/Page.d.ts +1 -0
- package/dist/Page.js +6 -2
- package/dist/Page.js.map +1 -1
- package/dist/PickerSelect.d.ts +1 -1
- package/dist/PickerSelect.js +2 -2
- package/dist/PickerSelect.js.map +1 -1
- package/dist/TapToEdit.d.ts +1 -1
- package/dist/TapToEdit.js +2 -3
- package/dist/TapToEdit.js.map +1 -1
- package/dist/ToastNotifications.js +2 -2
- package/dist/ToastNotifications.js.map +1 -1
- package/dist/Tooltip.d.ts +24 -1
- package/dist/Tooltip.js +2 -2
- package/dist/Tooltip.js.map +1 -1
- package/dist/Unifier.d.ts +1 -1
- package/dist/Unifier.js +14 -11
- package/dist/Unifier.js.map +1 -1
- package/dist/Utilities.d.ts +8 -8
- package/dist/Utilities.js +12 -14
- package/dist/Utilities.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/signUp/PasswordRequirements.js +3 -3
- package/dist/signUp/PasswordRequirements.js.map +1 -1
- package/dist/table/TableHeaderCell.js +1 -9
- package/dist/table/TableHeaderCell.js.map +1 -1
- package/dist/table/tableContext.d.ts +1 -1
- package/dist/table/tableContext.js +2 -2
- package/dist/table/tableContext.js.map +1 -1
- package/dist/useConsentHistory.d.ts +6 -1
- package/dist/useConsentHistory.js +2 -1
- package/dist/useConsentHistory.js.map +1 -1
- package/package.json +1 -1
- package/src/ActionSheet.test.tsx +554 -0
- package/src/ActionSheet.tsx +26 -39
- package/src/Banner.test.tsx +107 -1
- package/src/Common.ts +10 -4
- package/src/ConsentFormScreen.test.tsx +22 -0
- package/src/ConsentFormScreen.tsx +9 -3
- package/src/DataTable.test.tsx +393 -1
- package/src/DateTimeField.test.tsx +716 -2
- package/src/DateUtilities.tsx +37 -38
- package/src/HeightActionSheet.test.tsx +17 -1
- package/src/HeightField.test.tsx +141 -1
- package/src/HeightFieldDesktop.test.tsx +19 -0
- package/src/MarkdownView.test.tsx +28 -0
- package/src/MarkdownView.tsx +69 -7
- package/src/MediaQuery.ts +8 -8
- package/src/MobileAddressAutoComplete.test.tsx +26 -3
- package/src/Page.test.tsx +28 -0
- package/src/Page.tsx +17 -2
- package/src/PickerSelect.test.tsx +243 -0
- package/src/PickerSelect.tsx +3 -3
- package/src/SplitPage.test.tsx +299 -43
- package/src/TapToEdit.test.tsx +44 -0
- package/src/TapToEdit.tsx +2 -3
- package/src/ToastNotifications.test.tsx +1412 -0
- package/src/ToastNotifications.tsx +2 -2
- package/src/Tooltip.test.tsx +1294 -3
- package/src/Tooltip.tsx +2 -2
- package/src/Unifier.ts +14 -11
- package/src/Utilities.tsx +14 -16
- package/src/WebAddressAutocomplete.test.tsx +237 -0
- package/src/WebDropdownMenu.test.tsx +51 -2
- package/src/__snapshots__/Banner.test.tsx.snap +125 -0
- package/src/__snapshots__/DataTable.test.tsx.snap +366 -0
- package/src/__snapshots__/MarkdownView.test.tsx.snap +284 -74
- package/src/__snapshots__/SplitPage.test.tsx.snap +698 -46
- package/src/bunSetup.ts +0 -4
- package/src/index.tsx +1 -1
- package/src/login/LoginScreen.test.tsx +35 -1
- package/src/signUp/PasswordRequirements.tsx +9 -6
- package/src/signUp/__snapshots__/PasswordRequirements.test.tsx.snap +50 -2
- package/src/signUp/__snapshots__/SignUpScreen.test.tsx.snap +25 -1
- package/src/table/TableHeaderCell.tsx +8 -11
- package/src/table/TableRow.test.tsx +31 -1
- package/src/table/__snapshots__/TableHeaderCell.test.tsx.snap +2 -0
- package/src/table/tableContext.tsx +2 -2
- package/src/useConsentHistory.test.ts +20 -13
- package/src/useConsentHistory.ts +7 -2
- package/src/useStoredState.test.tsx +47 -0
package/src/Banner.test.tsx
CHANGED
|
@@ -2,7 +2,7 @@ import {describe, expect, it, mock} from "bun:test";
|
|
|
2
2
|
import {act, fireEvent, waitFor} from "@testing-library/react-native";
|
|
3
3
|
import React from "react";
|
|
4
4
|
|
|
5
|
-
import {Banner} from "./Banner";
|
|
5
|
+
import {Banner, hideBanner} from "./Banner";
|
|
6
6
|
import {renderWithTheme} from "./test-utils";
|
|
7
7
|
import {Unifier} from "./Unifier";
|
|
8
8
|
|
|
@@ -168,4 +168,110 @@ describe("Banner", () => {
|
|
|
168
168
|
expect(setItemMock).not.toHaveBeenCalled();
|
|
169
169
|
});
|
|
170
170
|
});
|
|
171
|
+
|
|
172
|
+
it("hides banner when storage already has the dismissed flag", async () => {
|
|
173
|
+
const getItemMock = Unifier.storage.getItem as ReturnType<typeof mock>;
|
|
174
|
+
getItemMock.mockReturnValueOnce(Promise.resolve("true"));
|
|
175
|
+
|
|
176
|
+
const {queryByText} = renderWithTheme(
|
|
177
|
+
<Banner dismissible id="stored-banner" text="Previously dismissed" />
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
await waitFor(() => {
|
|
181
|
+
expect(queryByText("Previously dismissed")).toBeNull();
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("exercises the async .then path in useEffect", async () => {
|
|
186
|
+
const getItemMock = Unifier.storage.getItem as ReturnType<typeof mock>;
|
|
187
|
+
getItemMock.mockReturnValueOnce(Promise.resolve(null));
|
|
188
|
+
|
|
189
|
+
const {queryByText} = renderWithTheme(
|
|
190
|
+
<Banner dismissible id="flush-banner" text="Flush banner" />
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
await act(async () => {
|
|
194
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
expect(queryByText("Flush banner")).toBeTruthy();
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("renders button without icon name (text-only button path)", async () => {
|
|
201
|
+
const handleClick = mock(() => Promise.resolve());
|
|
202
|
+
const {getByText} = renderWithTheme(
|
|
203
|
+
<Banner
|
|
204
|
+
buttonOnClick={handleClick}
|
|
205
|
+
buttonText="TextOnly"
|
|
206
|
+
id="textonly-banner"
|
|
207
|
+
text="Banner"
|
|
208
|
+
/>
|
|
209
|
+
);
|
|
210
|
+
expect(getByText("TextOnly")).toBeTruthy();
|
|
211
|
+
|
|
212
|
+
await act(async () => {
|
|
213
|
+
fireEvent.press(getByText("TextOnly"));
|
|
214
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
await waitFor(() => {
|
|
218
|
+
expect(handleClick).toHaveBeenCalled();
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("covers catch block when buttonOnClick rejects", async () => {
|
|
223
|
+
const handleClick = mock(() => Promise.reject(new Error("boom")));
|
|
224
|
+
const {UNSAFE_root} = renderWithTheme(
|
|
225
|
+
<Banner buttonOnClick={handleClick} buttonText="Fail" id="catch-banner" text="Banner" />
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
const pressable = UNSAFE_root.findAll(
|
|
229
|
+
(node) => node.props?.["aria-label"] === "Fail" && typeof node.props?.onPress === "function"
|
|
230
|
+
)[0];
|
|
231
|
+
|
|
232
|
+
try {
|
|
233
|
+
await act(async () => {
|
|
234
|
+
await pressable.props.onPress();
|
|
235
|
+
});
|
|
236
|
+
} catch (_e) {
|
|
237
|
+
// Expected: catch block in BannerButton re-throws
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
expect(handleClick).toHaveBeenCalled();
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it("hideBanner persists the banner id to storage", async () => {
|
|
244
|
+
const setItemMock = Unifier.storage.setItem as ReturnType<typeof mock>;
|
|
245
|
+
setItemMock.mockClear();
|
|
246
|
+
|
|
247
|
+
await hideBanner("my-banner");
|
|
248
|
+
expect(setItemMock).toHaveBeenCalledWith("@TerrenoUI:my-banner", "true");
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it("renders with button loading state", () => {
|
|
252
|
+
const handleClick = mock(() => Promise.resolve());
|
|
253
|
+
const {toJSON} = renderWithTheme(
|
|
254
|
+
<Banner
|
|
255
|
+
buttonOnClick={handleClick}
|
|
256
|
+
buttonText="Loading"
|
|
257
|
+
id="test-banner"
|
|
258
|
+
loading
|
|
259
|
+
text="Banner loading"
|
|
260
|
+
/>
|
|
261
|
+
);
|
|
262
|
+
expect(toJSON()).toMatchSnapshot();
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it("renders banner with dismissible=true and no id (non-persistent dismiss)", async () => {
|
|
266
|
+
const {getByLabelText, queryByText} = renderWithTheme(
|
|
267
|
+
<Banner dismissible text="Non persistent" />
|
|
268
|
+
);
|
|
269
|
+
expect(queryByText("Non persistent")).toBeTruthy();
|
|
270
|
+
await act(async () => {
|
|
271
|
+
fireEvent.press(getByLabelText("Dismiss"));
|
|
272
|
+
});
|
|
273
|
+
await waitFor(() => {
|
|
274
|
+
expect(queryByText("Non persistent")).toBeNull();
|
|
275
|
+
});
|
|
276
|
+
});
|
|
171
277
|
});
|
package/src/Common.ts
CHANGED
|
@@ -395,12 +395,12 @@ export const SPACING_MAP = {
|
|
|
395
395
|
12: 80,
|
|
396
396
|
};
|
|
397
397
|
|
|
398
|
-
export
|
|
398
|
+
export const getSpacing = (spacing: SignedUpTo12) => {
|
|
399
399
|
if (spacing < 0) {
|
|
400
400
|
return SPACING_MAP[Math.abs(spacing) as UnsignedUpTo12] * -1;
|
|
401
401
|
}
|
|
402
402
|
return SPACING_MAP[spacing as UnsignedUpTo12];
|
|
403
|
-
}
|
|
403
|
+
};
|
|
404
404
|
|
|
405
405
|
export type TextFieldType =
|
|
406
406
|
| "date"
|
|
@@ -753,9 +753,9 @@ const ROUNDING_MAP = {
|
|
|
753
753
|
xl: 32,
|
|
754
754
|
};
|
|
755
755
|
|
|
756
|
-
export
|
|
756
|
+
export const getRounding = (rounding: Rounding) => {
|
|
757
757
|
return ROUNDING_MAP[rounding];
|
|
758
|
-
}
|
|
758
|
+
};
|
|
759
759
|
|
|
760
760
|
export interface HeadingProps {
|
|
761
761
|
align?: "left" | "right" | "center" | "justify"; // default "left"
|
|
@@ -1965,6 +1965,12 @@ export interface PageProps {
|
|
|
1965
1965
|
rightButtonOnClick?: () => void;
|
|
1966
1966
|
children?: ReactChildren;
|
|
1967
1967
|
onError?: (error: Error, stack: string) => void;
|
|
1968
|
+
/**
|
|
1969
|
+
* When true, wraps content in SafeAreaView so it respects top/bottom device
|
|
1970
|
+
* insets (camera notches, home indicator, status bar). Opt-in to avoid
|
|
1971
|
+
* regressing existing screens that handle insets at the navigation level.
|
|
1972
|
+
*/
|
|
1973
|
+
safeArea?: boolean;
|
|
1968
1974
|
}
|
|
1969
1975
|
|
|
1970
1976
|
export interface ProgressBarProps {
|
|
@@ -2,6 +2,7 @@ import {describe, expect, it, mock} from "bun:test";
|
|
|
2
2
|
import {act, fireEvent} from "@testing-library/react-native";
|
|
3
3
|
|
|
4
4
|
import {ConsentFormScreen} from "./ConsentFormScreen";
|
|
5
|
+
import {SignatureField} from "./SignatureField";
|
|
5
6
|
import {renderWithTheme} from "./test-utils";
|
|
6
7
|
import type {ConsentFormPublic} from "./useConsentForms";
|
|
7
8
|
|
|
@@ -334,4 +335,25 @@ describe("ConsentFormScreen", () => {
|
|
|
334
335
|
});
|
|
335
336
|
expect(queryByTestId("consent-footer-checkboxes-hint")).toBeNull();
|
|
336
337
|
});
|
|
338
|
+
|
|
339
|
+
it("exercises SignatureField onChange, onStart, and onEnd callbacks", () => {
|
|
340
|
+
const form: ConsentFormPublic = {
|
|
341
|
+
...baseForm,
|
|
342
|
+
captureSignature: true,
|
|
343
|
+
};
|
|
344
|
+
const {UNSAFE_getByType} = renderWithTheme(
|
|
345
|
+
<ConsentFormScreen form={form} locale="en" onAgree={() => {}} />
|
|
346
|
+
);
|
|
347
|
+
const sig = UNSAFE_getByType(SignatureField);
|
|
348
|
+
act(() => {
|
|
349
|
+
sig.props.onChange("data:image/png;base64,abc");
|
|
350
|
+
});
|
|
351
|
+
act(() => {
|
|
352
|
+
sig.props.onStart();
|
|
353
|
+
});
|
|
354
|
+
act(() => {
|
|
355
|
+
sig.props.onEnd();
|
|
356
|
+
});
|
|
357
|
+
expect(sig).toBeTruthy();
|
|
358
|
+
});
|
|
337
359
|
});
|
|
@@ -3,8 +3,10 @@ import {
|
|
|
3
3
|
type LayoutChangeEvent,
|
|
4
4
|
type NativeScrollEvent,
|
|
5
5
|
type NativeSyntheticEvent,
|
|
6
|
+
Platform,
|
|
6
7
|
Pressable,
|
|
7
8
|
ScrollView,
|
|
9
|
+
View,
|
|
8
10
|
} from "react-native";
|
|
9
11
|
|
|
10
12
|
import {Box} from "./Box";
|
|
@@ -166,7 +168,7 @@ export const ConsentFormScreen: React.FC<ConsentFormScreenProps> = ({
|
|
|
166
168
|
);
|
|
167
169
|
|
|
168
170
|
return (
|
|
169
|
-
<Page color="base" footer={footer} maxWidth="100%" scroll={false} title={form.title}>
|
|
171
|
+
<Page color="base" footer={footer} maxWidth="100%" safeArea scroll={false} title={form.title}>
|
|
170
172
|
<ScrollView
|
|
171
173
|
onContentSizeChange={handleContentSizeChange}
|
|
172
174
|
onLayout={handleLayout}
|
|
@@ -212,7 +214,11 @@ export const ConsentFormScreen: React.FC<ConsentFormScreenProps> = ({
|
|
|
212
214
|
)}
|
|
213
215
|
|
|
214
216
|
{Boolean(form.captureSignature) && (
|
|
215
|
-
<
|
|
217
|
+
<View
|
|
218
|
+
onTouchEnd={Platform.OS === "ios" ? () => setScrollEnabled(true) : undefined}
|
|
219
|
+
onTouchStart={Platform.OS === "ios" ? () => setScrollEnabled(false) : undefined}
|
|
220
|
+
testID="consent-form-signature"
|
|
221
|
+
>
|
|
216
222
|
<SignatureField
|
|
217
223
|
onChange={(value) => setSignatureValue(value)}
|
|
218
224
|
onEnd={() => setScrollEnabled(true)}
|
|
@@ -220,7 +226,7 @@ export const ConsentFormScreen: React.FC<ConsentFormScreenProps> = ({
|
|
|
220
226
|
title="Signature"
|
|
221
227
|
value={signatureValue}
|
|
222
228
|
/>
|
|
223
|
-
</
|
|
229
|
+
</View>
|
|
224
230
|
)}
|
|
225
231
|
|
|
226
232
|
{Boolean(form.requireScrollToBottom && !hasScrolledToBottom) && (
|
package/src/DataTable.test.tsx
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// biome-ignore-all lint/suspicious/noExplicitAny: test mock typing
|
|
2
2
|
import {describe, expect, it, mock} from "bun:test";
|
|
3
|
+
import {act} from "@testing-library/react-native";
|
|
3
4
|
|
|
5
|
+
import type {DataTableCustomComponentMap, DataTableProps} from "./Common";
|
|
4
6
|
import {DataTable} from "./DataTable";
|
|
5
7
|
import {Text} from "./Text";
|
|
6
8
|
import {renderWithTheme} from "./test-utils";
|
|
@@ -114,7 +116,7 @@ describe("DataTable", () => {
|
|
|
114
116
|
<DataTable
|
|
115
117
|
columns={sampleColumns}
|
|
116
118
|
data={sampleData}
|
|
117
|
-
moreContentComponent={MoreContent as
|
|
119
|
+
moreContentComponent={MoreContent as unknown as DataTableProps["moreContentComponent"]}
|
|
118
120
|
/>
|
|
119
121
|
);
|
|
120
122
|
expect(toJSON()).toMatchSnapshot();
|
|
@@ -131,4 +133,394 @@ describe("DataTable", () => {
|
|
|
131
133
|
const {toJSON} = renderWithTheme(<DataTable columns={sampleColumns} data={[]} />);
|
|
132
134
|
expect(toJSON()).toMatchSnapshot();
|
|
133
135
|
});
|
|
136
|
+
|
|
137
|
+
it("handles sort cycling: none -> asc -> desc -> none", () => {
|
|
138
|
+
const sortableColumns = [
|
|
139
|
+
{columnType: "text", sortable: true, title: "Name", width: 150},
|
|
140
|
+
{columnType: "text", sortable: false, title: "Age", width: 100},
|
|
141
|
+
];
|
|
142
|
+
const setSortColumn = mock((_sort?: {column: number; direction: string}) => {});
|
|
143
|
+
const {UNSAFE_getAllByType} = renderWithTheme(
|
|
144
|
+
<DataTable
|
|
145
|
+
columns={sortableColumns}
|
|
146
|
+
data={[[{value: "Alice"}, {value: "28"}]]}
|
|
147
|
+
setSortColumn={setSortColumn}
|
|
148
|
+
/>
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
const {Pressable: PressableComp} = require("react-native");
|
|
152
|
+
const pressables = UNSAFE_getAllByType(PressableComp);
|
|
153
|
+
// Find the sort pressable (has hitSlop=16)
|
|
154
|
+
const sortPressable = pressables.find(
|
|
155
|
+
(p: {props: {hitSlop?: number}}) => p.props.hitSlop === 16
|
|
156
|
+
);
|
|
157
|
+
expect(sortPressable).toBeTruthy();
|
|
158
|
+
|
|
159
|
+
const {fireEvent} = require("@testing-library/react-native");
|
|
160
|
+
// First click: none -> asc
|
|
161
|
+
fireEvent.press(sortPressable!);
|
|
162
|
+
expect(setSortColumn).toHaveBeenCalledWith({column: 0, direction: "asc"});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("handles sort from asc to desc", () => {
|
|
166
|
+
const sortableColumns = [{columnType: "text", sortable: true, title: "Name", width: 150}];
|
|
167
|
+
const setSortColumn = mock((_sort?: {column: number; direction: string}) => {});
|
|
168
|
+
const {UNSAFE_getAllByType} = renderWithTheme(
|
|
169
|
+
<DataTable
|
|
170
|
+
columns={sortableColumns}
|
|
171
|
+
data={[[{value: "Alice"}]]}
|
|
172
|
+
setSortColumn={setSortColumn}
|
|
173
|
+
sortColumn={{column: 0, direction: "asc"}}
|
|
174
|
+
/>
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
const {Pressable: PressableComp} = require("react-native");
|
|
178
|
+
const pressables = UNSAFE_getAllByType(PressableComp);
|
|
179
|
+
const sortPressable = pressables.find(
|
|
180
|
+
(p: {props: {hitSlop?: number}}) => p.props.hitSlop === 16
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
const {fireEvent} = require("@testing-library/react-native");
|
|
184
|
+
fireEvent.press(sortPressable!);
|
|
185
|
+
expect(setSortColumn).toHaveBeenCalledWith({column: 0, direction: "desc"});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("handles sort from desc to none", () => {
|
|
189
|
+
const sortableColumns = [{columnType: "text", sortable: true, title: "Name", width: 150}];
|
|
190
|
+
const setSortColumn = mock((_sort?: {column: number; direction: string}) => {});
|
|
191
|
+
const {UNSAFE_getAllByType} = renderWithTheme(
|
|
192
|
+
<DataTable
|
|
193
|
+
columns={sortableColumns}
|
|
194
|
+
data={[[{value: "Alice"}]]}
|
|
195
|
+
setSortColumn={setSortColumn}
|
|
196
|
+
sortColumn={{column: 0, direction: "desc"}}
|
|
197
|
+
/>
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
const {Pressable: PressableComp} = require("react-native");
|
|
201
|
+
const pressables = UNSAFE_getAllByType(PressableComp);
|
|
202
|
+
const sortPressable = pressables.find(
|
|
203
|
+
(p: {props: {hitSlop?: number}}) => p.props.hitSlop === 16
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
const {fireEvent} = require("@testing-library/react-native");
|
|
207
|
+
fireEvent.press(sortPressable!);
|
|
208
|
+
expect(setSortColumn).toHaveBeenCalledWith(undefined);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("handles sort on non-sortable column (no-op)", () => {
|
|
212
|
+
const columns = [{columnType: "text", sortable: false, title: "Name", width: 150}];
|
|
213
|
+
const setSortColumn = mock(() => {});
|
|
214
|
+
renderWithTheme(
|
|
215
|
+
<DataTable columns={columns} data={[[{value: "Alice"}]]} setSortColumn={setSortColumn} />
|
|
216
|
+
);
|
|
217
|
+
// No sort pressable rendered for non-sortable columns, so no action needed
|
|
218
|
+
expect(setSortColumn).not.toHaveBeenCalled();
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("syncs scroll between header and body via refs", () => {
|
|
222
|
+
const {UNSAFE_getAllByType} = renderWithTheme(
|
|
223
|
+
<DataTable columns={sampleColumns} data={sampleData} pinnedColumns={1} />
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
const {ScrollView: ScrollViewComp} = require("react-native");
|
|
227
|
+
const scrollViews = UNSAFE_getAllByType(ScrollViewComp);
|
|
228
|
+
expect(scrollViews.length).toBeGreaterThan(0);
|
|
229
|
+
|
|
230
|
+
// Inject mock scrollTo on the refs so handleScroll branches execute
|
|
231
|
+
const mockScrollTo = mock((_opts: {animated: boolean; x: number}) => {});
|
|
232
|
+
for (const sv of scrollViews) {
|
|
233
|
+
if (sv.props.horizontal) {
|
|
234
|
+
const fiber = (sv as unknown as {_fiber?: {ref?: {current: unknown}}})._fiber;
|
|
235
|
+
if (fiber?.ref && typeof fiber.ref === "object") {
|
|
236
|
+
fiber.ref.current = {scrollTo: mockScrollTo};
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const {fireEvent} = require("@testing-library/react-native");
|
|
242
|
+
// Find header scroll (onScroll passes isHeader=true)
|
|
243
|
+
const headerScroll = scrollViews.find(
|
|
244
|
+
(sv: {props: {horizontal?: boolean; showsHorizontalScrollIndicator?: boolean}}) =>
|
|
245
|
+
sv.props.horizontal && sv.props.showsHorizontalScrollIndicator === false
|
|
246
|
+
);
|
|
247
|
+
// Find body scroll (onScroll passes isHeader=false)
|
|
248
|
+
const bodyScroll = scrollViews.find(
|
|
249
|
+
(sv: {props: {horizontal?: boolean; showsHorizontalScrollIndicator?: boolean}}) =>
|
|
250
|
+
sv.props.horizontal && sv.props.showsHorizontalScrollIndicator === true
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
if (headerScroll) {
|
|
254
|
+
fireEvent.scroll(headerScroll, {
|
|
255
|
+
nativeEvent: {contentOffset: {x: 50, y: 0}},
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
if (bodyScroll) {
|
|
259
|
+
fireEvent.scroll(bodyScroll, {
|
|
260
|
+
nativeEvent: {contentOffset: {x: 75, y: 0}},
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
expect(mockScrollTo).toHaveBeenCalled();
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("renders with custom column component map", () => {
|
|
268
|
+
const CustomComponent = ({cellData}: {cellData: {value: unknown}}) => (
|
|
269
|
+
<Text>Custom: {String(cellData.value)}</Text>
|
|
270
|
+
);
|
|
271
|
+
const customColumns = [{columnType: "custom", title: "Custom Col", width: 150}];
|
|
272
|
+
const customData = [[{value: "test"}]];
|
|
273
|
+
const {getByText} = renderWithTheme(
|
|
274
|
+
<DataTable
|
|
275
|
+
columns={customColumns}
|
|
276
|
+
customColumnComponentMap={{custom: CustomComponent as any}}
|
|
277
|
+
data={customData}
|
|
278
|
+
/>
|
|
279
|
+
);
|
|
280
|
+
expect(getByText("Custom: test")).toBeTruthy();
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it("renders with infoModalText on column header", () => {
|
|
284
|
+
const columnsWithInfo = [
|
|
285
|
+
{columnType: "text", infoModalText: "**Help text**", title: "Name", width: 150},
|
|
286
|
+
];
|
|
287
|
+
const {toJSON} = renderWithTheme(
|
|
288
|
+
<DataTable columns={columnsWithInfo} data={[[{value: "Alice"}]]} />
|
|
289
|
+
);
|
|
290
|
+
expect(toJSON()).toBeTruthy();
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it("renders with cell highlight", () => {
|
|
294
|
+
const highlightData = [[{highlight: "primary", value: "Highlighted"}]];
|
|
295
|
+
const {toJSON} = renderWithTheme(
|
|
296
|
+
<DataTable columns={[{columnType: "text", title: "Name", width: 150}]} data={highlightData} />
|
|
297
|
+
);
|
|
298
|
+
expect(toJSON()).toBeTruthy();
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it("renders with moreContentExtraData", () => {
|
|
302
|
+
const MoreContent = ({rowIndex, extraInfo}: {rowIndex: number; extraInfo?: string}) => (
|
|
303
|
+
<Text>
|
|
304
|
+
Row {rowIndex}: {extraInfo}
|
|
305
|
+
</Text>
|
|
306
|
+
);
|
|
307
|
+
const {toJSON} = renderWithTheme(
|
|
308
|
+
<DataTable
|
|
309
|
+
columns={sampleColumns}
|
|
310
|
+
data={sampleData}
|
|
311
|
+
moreContentComponent={MoreContent as any}
|
|
312
|
+
moreContentExtraData={[{extraInfo: "info1"}, {extraInfo: "info2"}, {extraInfo: "info3"}]}
|
|
313
|
+
/>
|
|
314
|
+
);
|
|
315
|
+
expect(toJSON()).toBeTruthy();
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it("opens and dismisses more content modal via MoreButtonCell press", async () => {
|
|
319
|
+
const MoreContent = ({rowIndex}: {rowIndex: number}) => <Text>Detail for row {rowIndex}</Text>;
|
|
320
|
+
const {UNSAFE_getAllByType} = renderWithTheme(
|
|
321
|
+
<DataTable
|
|
322
|
+
columns={sampleColumns}
|
|
323
|
+
data={sampleData}
|
|
324
|
+
moreContentComponent={MoreContent as any}
|
|
325
|
+
/>
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
const {Pressable: PressableComp} = require("react-native");
|
|
329
|
+
const {fireEvent} = require("@testing-library/react-native");
|
|
330
|
+
const pressables = UNSAFE_getAllByType(PressableComp);
|
|
331
|
+
|
|
332
|
+
// Find the info/eye icon pressable (MoreButtonCell has accessibilityHint="View details")
|
|
333
|
+
const moreBtn = pressables.find(
|
|
334
|
+
(p: {props: {accessibilityHint?: string}}) => p.props.accessibilityHint === "View details"
|
|
335
|
+
);
|
|
336
|
+
expect(moreBtn).toBeTruthy();
|
|
337
|
+
|
|
338
|
+
// Press to open modal
|
|
339
|
+
await act(async () => {
|
|
340
|
+
fireEvent.press(moreBtn!);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
// Find the modal dismiss pressable and press it
|
|
344
|
+
const {Modal: ModalComp} = require("./Modal");
|
|
345
|
+
const modals = UNSAFE_getAllByType(ModalComp);
|
|
346
|
+
if (modals.length > 0 && modals[0].props.onDismiss) {
|
|
347
|
+
await act(async () => {
|
|
348
|
+
modals[0].props.onDismiss();
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it("renders with customColumnComponentMap", () => {
|
|
354
|
+
const CustomCell = ({cellData}: {cellData: {value: unknown}; column: unknown}) => (
|
|
355
|
+
<Text>Custom: {String(cellData.value)}</Text>
|
|
356
|
+
);
|
|
357
|
+
const customColumns = [
|
|
358
|
+
{columnType: "custom", title: "Custom Col", width: 150},
|
|
359
|
+
{columnType: "text", title: "Name", width: 100},
|
|
360
|
+
];
|
|
361
|
+
const customData = [[{value: "A"}, {value: "Bob"}]];
|
|
362
|
+
const {getByText} = renderWithTheme(
|
|
363
|
+
<DataTable
|
|
364
|
+
columns={customColumns}
|
|
365
|
+
customColumnComponentMap={{custom: CustomCell} as DataTableCustomComponentMap}
|
|
366
|
+
data={customData}
|
|
367
|
+
/>
|
|
368
|
+
);
|
|
369
|
+
expect(getByText("Custom: A")).toBeTruthy();
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it("handleSort cycles through asc, desc, and clear", () => {
|
|
373
|
+
const sortableColumns = [
|
|
374
|
+
{columnType: "text", sortable: true, title: "Name", width: 150},
|
|
375
|
+
{columnType: "text", sortable: false, title: "Age", width: 100},
|
|
376
|
+
];
|
|
377
|
+
const setSortColumn = mock((_val: unknown) => {});
|
|
378
|
+
const {UNSAFE_getAllByType} = renderWithTheme(
|
|
379
|
+
<DataTable
|
|
380
|
+
columns={sortableColumns}
|
|
381
|
+
data={[[{value: "Alice"}, {value: "28"}]]}
|
|
382
|
+
setSortColumn={setSortColumn}
|
|
383
|
+
/>
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
const {Pressable: PressableComp} = require("react-native");
|
|
387
|
+
const pressables = UNSAFE_getAllByType(PressableComp);
|
|
388
|
+
const sortButton = pressables.find((p: {props: {hitSlop?: number}}) => p.props.hitSlop === 16);
|
|
389
|
+
expect(sortButton).toBeTruthy();
|
|
390
|
+
|
|
391
|
+
// First press: asc
|
|
392
|
+
act(() => {
|
|
393
|
+
sortButton!.props.onPress();
|
|
394
|
+
});
|
|
395
|
+
expect(setSortColumn).toHaveBeenCalledWith({column: 0, direction: "asc"});
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
it("handleSort from asc to desc", () => {
|
|
399
|
+
const sortableColumns = [{columnType: "text", sortable: true, title: "Name", width: 150}];
|
|
400
|
+
const setSortColumn = mock((_val: unknown) => {});
|
|
401
|
+
const {UNSAFE_getAllByType} = renderWithTheme(
|
|
402
|
+
<DataTable
|
|
403
|
+
columns={sortableColumns}
|
|
404
|
+
data={[[{value: "Alice"}]]}
|
|
405
|
+
setSortColumn={setSortColumn}
|
|
406
|
+
sortColumn={{column: 0, direction: "asc"}}
|
|
407
|
+
/>
|
|
408
|
+
);
|
|
409
|
+
|
|
410
|
+
const {Pressable: PressableComp} = require("react-native");
|
|
411
|
+
const pressables = UNSAFE_getAllByType(PressableComp);
|
|
412
|
+
const sortButton = pressables.find((p: {props: {hitSlop?: number}}) => p.props.hitSlop === 16);
|
|
413
|
+
|
|
414
|
+
act(() => {
|
|
415
|
+
sortButton!.props.onPress();
|
|
416
|
+
});
|
|
417
|
+
expect(setSortColumn).toHaveBeenCalledWith({column: 0, direction: "desc"});
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
it("handleSort from desc clears sort", () => {
|
|
421
|
+
const sortableColumns = [{columnType: "text", sortable: true, title: "Name", width: 150}];
|
|
422
|
+
const setSortColumn = mock((_val: unknown) => {});
|
|
423
|
+
const {UNSAFE_getAllByType} = renderWithTheme(
|
|
424
|
+
<DataTable
|
|
425
|
+
columns={sortableColumns}
|
|
426
|
+
data={[[{value: "Alice"}]]}
|
|
427
|
+
setSortColumn={setSortColumn}
|
|
428
|
+
sortColumn={{column: 0, direction: "desc"}}
|
|
429
|
+
/>
|
|
430
|
+
);
|
|
431
|
+
|
|
432
|
+
const {Pressable: PressableComp} = require("react-native");
|
|
433
|
+
const pressables = UNSAFE_getAllByType(PressableComp);
|
|
434
|
+
const sortButton = pressables.find((p: {props: {hitSlop?: number}}) => p.props.hitSlop === 16);
|
|
435
|
+
|
|
436
|
+
act(() => {
|
|
437
|
+
sortButton!.props.onPress();
|
|
438
|
+
});
|
|
439
|
+
expect(setSortColumn).toHaveBeenCalledWith(undefined);
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
it("handleSort does nothing for non-sortable column", () => {
|
|
443
|
+
const columns = [{columnType: "text", sortable: false, title: "Name", width: 150}];
|
|
444
|
+
const setSortColumn = mock((_val: unknown) => {});
|
|
445
|
+
const {toJSON} = renderWithTheme(
|
|
446
|
+
<DataTable columns={columns} data={[[{value: "Alice"}]]} setSortColumn={setSortColumn} />
|
|
447
|
+
);
|
|
448
|
+
expect(toJSON()).toBeTruthy();
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
it("handleScroll syncs header and body scroll positions", () => {
|
|
452
|
+
const {UNSAFE_getAllByType} = renderWithTheme(
|
|
453
|
+
<DataTable columns={sampleColumns} data={sampleData} pinnedColumns={1} />
|
|
454
|
+
);
|
|
455
|
+
|
|
456
|
+
const {ScrollView: ScrollViewComp} = require("react-native");
|
|
457
|
+
const scrollViews = UNSAFE_getAllByType(ScrollViewComp);
|
|
458
|
+
const horizontalScrollViews = scrollViews.filter(
|
|
459
|
+
(sv: {props: {horizontal?: boolean}}) => sv.props.horizontal
|
|
460
|
+
);
|
|
461
|
+
|
|
462
|
+
if (horizontalScrollViews.length >= 2) {
|
|
463
|
+
// Trigger scroll on the body scroll view
|
|
464
|
+
act(() => {
|
|
465
|
+
horizontalScrollViews[1].props.onScroll({
|
|
466
|
+
nativeEvent: {contentOffset: {x: 50, y: 0}},
|
|
467
|
+
});
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
// Trigger scroll on the header scroll view
|
|
471
|
+
act(() => {
|
|
472
|
+
horizontalScrollViews[0].props.onScroll({
|
|
473
|
+
nativeEvent: {contentOffset: {x: 100, y: 0}},
|
|
474
|
+
});
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
it("renders with cell highlight color", () => {
|
|
480
|
+
const highlightData = [[{highlight: "primary", value: "Highlighted"}]];
|
|
481
|
+
const highlightColumns = [{columnType: "text", title: "Col", width: 150}];
|
|
482
|
+
const {toJSON} = renderWithTheme(<DataTable columns={highlightColumns} data={highlightData} />);
|
|
483
|
+
expect(toJSON()).toBeTruthy();
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
it("opens and closes more content modal", () => {
|
|
487
|
+
const MoreContent = ({rowIndex}: {rowIndex: number}) => <Text>Row {rowIndex} details</Text>;
|
|
488
|
+
const {UNSAFE_getAllByType} = renderWithTheme(
|
|
489
|
+
<DataTable
|
|
490
|
+
columns={sampleColumns}
|
|
491
|
+
data={sampleData}
|
|
492
|
+
moreContentComponent={MoreContent as unknown as DataTableProps["moreContentComponent"]}
|
|
493
|
+
/>
|
|
494
|
+
);
|
|
495
|
+
|
|
496
|
+
const {Pressable: PressableComp} = require("react-native");
|
|
497
|
+
const pressables = UNSAFE_getAllByType(PressableComp);
|
|
498
|
+
// Find the "Open modal" button (MoreButtonCell)
|
|
499
|
+
const moreButton = pressables.find(
|
|
500
|
+
(p: {props: {accessibilityLabel?: string}}) => p.props.accessibilityLabel === "Open modal"
|
|
501
|
+
);
|
|
502
|
+
|
|
503
|
+
if (moreButton) {
|
|
504
|
+
act(() => {
|
|
505
|
+
moreButton.props.onPress();
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
it("handleSort with no setSortColumn is a no-op", () => {
|
|
511
|
+
const sortableColumns = [{columnType: "text", sortable: true, title: "Name", width: 150}];
|
|
512
|
+
const {UNSAFE_getAllByType} = renderWithTheme(
|
|
513
|
+
<DataTable columns={sortableColumns} data={[[{value: "Alice"}]]} />
|
|
514
|
+
);
|
|
515
|
+
|
|
516
|
+
const {Pressable: PressableComp} = require("react-native");
|
|
517
|
+
const pressables = UNSAFE_getAllByType(PressableComp);
|
|
518
|
+
const sortButton = pressables.find((p: {props: {hitSlop?: number}}) => p.props.hitSlop === 16);
|
|
519
|
+
|
|
520
|
+
if (sortButton) {
|
|
521
|
+
act(() => {
|
|
522
|
+
sortButton.props.onPress();
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
});
|
|
134
526
|
});
|