@terreno/ui 0.12.2 → 0.13.3
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/ConsentFormScreen.js +8 -7
- package/dist/ConsentFormScreen.js.map +1 -1
- package/dist/Field.js.map +1 -1
- package/dist/PickerSelect.d.ts +22 -10
- package/dist/PickerSelect.js +11 -9
- package/dist/PickerSelect.js.map +1 -1
- package/dist/SelectBadge.js +11 -1
- package/dist/SelectBadge.js.map +1 -1
- package/dist/SelectField.js +2 -2
- package/dist/SelectField.js.map +1 -1
- package/dist/SidebarNavigation.native.js.map +1 -1
- package/dist/Signature.js +4 -0
- package/dist/Signature.js.map +1 -1
- package/dist/Signature.native.js +4 -0
- package/dist/Signature.native.js.map +1 -1
- package/dist/Theme.d.ts +1 -1
- package/dist/Theme.js.map +1 -1
- package/dist/useConsentForms.d.ts +4 -3
- package/dist/useConsentForms.js +26 -4
- package/dist/useConsentForms.js.map +1 -1
- package/dist/useConsentHistory.d.ts +4 -3
- package/dist/useConsentHistory.js +26 -4
- package/dist/useConsentHistory.js.map +1 -1
- package/dist/useSubmitConsent.d.ts +7 -6
- package/dist/useSubmitConsent.js +25 -3
- package/dist/useSubmitConsent.js.map +1 -1
- package/package.json +1 -1
- package/src/ConsentFormScreen.test.tsx +91 -23
- package/src/ConsentFormScreen.tsx +21 -0
- package/src/Field.tsx +1 -1
- package/src/HeightField.test.tsx +68 -0
- package/src/Modal.tsx +2 -2
- package/src/NumberField.test.tsx +13 -19
- package/src/PickerSelect.tsx +48 -34
- package/src/SelectBadge.tsx +7 -3
- package/src/SelectField.tsx +2 -2
- package/src/SidebarNavigation.native.tsx +6 -2
- package/src/Signature.native.tsx +4 -0
- package/src/Signature.test.tsx +10 -0
- package/src/Signature.tsx +4 -0
- package/src/Theme.tsx +17 -14
- package/src/Tooltip.test.tsx +41 -22
- package/src/__snapshots__/AddressField.test.tsx.snap +0 -1
- package/src/__snapshots__/CustomSelectField.test.tsx.snap +0 -7
- package/src/__snapshots__/Field.test.tsx.snap +0 -6
- package/src/__snapshots__/PickerSelect.test.tsx.snap +0 -7
- package/src/__snapshots__/SelectField.test.tsx.snap +0 -6
- package/src/__snapshots__/TerrenoProvider.test.tsx.snap +4 -18
- package/src/__snapshots__/TimezonePicker.test.tsx.snap +0 -6
- package/src/bunSetup.ts +22 -0
- package/src/polyfill.d.ts +1 -1
- package/src/table/__snapshots__/TableBadge.test.tsx.snap +0 -1
- package/src/useConsentForms.test.ts +25 -0
- package/src/useConsentForms.ts +32 -7
- package/src/useConsentHistory.test.ts +99 -0
- package/src/useConsentHistory.ts +31 -6
- package/src/useSubmitConsent.test.ts +24 -0
- package/src/useSubmitConsent.ts +35 -10
|
@@ -38,15 +38,16 @@ interface ConsentHistoryHookState {
|
|
|
38
38
|
isLoading: boolean;
|
|
39
39
|
refetch: () => void | Promise<void>;
|
|
40
40
|
}
|
|
41
|
+
interface ConsentHistoryEnhancedApi {
|
|
42
|
+
useGetMyConsentsQuery: () => ConsentHistoryHookState;
|
|
43
|
+
}
|
|
41
44
|
interface ConsentHistoryApi {
|
|
42
45
|
injectEndpoints: (options: {
|
|
43
46
|
endpoints: (build: ConsentHistoryQueryBuilder) => {
|
|
44
47
|
getMyConsents: unknown;
|
|
45
48
|
};
|
|
46
49
|
overrideExisting: boolean;
|
|
47
|
-
}) =>
|
|
48
|
-
useGetMyConsentsQuery: () => ConsentHistoryHookState;
|
|
49
|
-
};
|
|
50
|
+
}) => ConsentHistoryEnhancedApi;
|
|
50
51
|
}
|
|
51
52
|
export declare const useConsentHistory: (api: ConsentHistoryApi, baseUrl?: string) => {
|
|
52
53
|
entries: ConsentHistoryEntry[];
|
|
@@ -1,7 +1,22 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Cache the enhanced api per (api, baseUrl). `injectEndpoints` logs a console
|
|
3
|
+
* error in development whenever an endpoint with the same name is re-injected
|
|
4
|
+
* (with `overrideExisting: false`), so calling it on every render of every
|
|
5
|
+
* consumer would flood the console. WeakMap-by-api lets the GC reclaim entries
|
|
6
|
+
* when the api object is unreachable.
|
|
7
|
+
*/
|
|
8
|
+
const enhancedApiCache = new WeakMap();
|
|
9
|
+
const getEnhancedApi = (api, base) => {
|
|
10
|
+
let byBase = enhancedApiCache.get(api);
|
|
11
|
+
if (!byBase) {
|
|
12
|
+
byBase = new Map();
|
|
13
|
+
enhancedApiCache.set(api, byBase);
|
|
14
|
+
}
|
|
15
|
+
const cached = byBase.get(base);
|
|
16
|
+
if (cached) {
|
|
17
|
+
return cached;
|
|
18
|
+
}
|
|
19
|
+
const enhanced = api.injectEndpoints({
|
|
5
20
|
endpoints: (build) => ({
|
|
6
21
|
getMyConsents: build.query({
|
|
7
22
|
providesTags: ["MyConsents"],
|
|
@@ -10,6 +25,13 @@ export const useConsentHistory = (api, baseUrl) => {
|
|
|
10
25
|
}),
|
|
11
26
|
overrideExisting: false,
|
|
12
27
|
});
|
|
28
|
+
byBase.set(base, enhanced);
|
|
29
|
+
return enhanced;
|
|
30
|
+
};
|
|
31
|
+
export const useConsentHistory = (api, baseUrl) => {
|
|
32
|
+
var _a;
|
|
33
|
+
const base = baseUrl || "";
|
|
34
|
+
const enhancedApi = getEnhancedApi(api, base);
|
|
13
35
|
const { data, isLoading, error, refetch } = enhancedApi.useGetMyConsentsQuery();
|
|
14
36
|
const entries = Array.isArray(data) ? data : ((_a = data === null || data === void 0 ? void 0 : data.data) !== null && _a !== void 0 ? _a : []);
|
|
15
37
|
return { entries, error, isLoading, refetch };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useConsentHistory.js","sourceRoot":"","sources":["../src/useConsentHistory.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"useConsentHistory.js","sourceRoot":"","sources":["../src/useConsentHistory.ts"],"names":[],"mappings":"AAgDA;;;;;;GAMG;AACH,MAAM,gBAAgB,GAAG,IAAI,OAAO,EAA6D,CAAC;AAElG,MAAM,cAAc,GAAG,CAAC,GAAsB,EAAE,IAAY,EAA6B,EAAE;IACzF,IAAI,MAAM,GAAG,gBAAgB,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACvC,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,MAAM,GAAG,IAAI,GAAG,EAAE,CAAC;QACnB,gBAAgB,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;IACpC,CAAC;IACD,MAAM,MAAM,GAAG,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAChC,IAAI,MAAM,EAAE,CAAC;QACX,OAAO,MAAM,CAAC;IAChB,CAAC;IACD,MAAM,QAAQ,GAAG,GAAG,CAAC,eAAe,CAAC;QACnC,SAAS,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;YACrB,aAAa,EAAE,KAAK,CAAC,KAAK,CAAC;gBACzB,YAAY,EAAE,CAAC,YAAY,CAAC;gBAC5B,KAAK,EAAE,GAAG,EAAE,CAAC,GAAG,IAAI,cAAc;aACnC,CAAC;SACH,CAAC;QACF,gBAAgB,EAAE,KAAK;KACxB,CAAC,CAAC;IACH,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;IAC3B,OAAO,QAAQ,CAAC;AAClB,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,GAAsB,EAAE,OAAgB,EAAE,EAAE;;IAC5E,MAAM,IAAI,GAAG,OAAO,IAAI,EAAE,CAAC;IAC3B,MAAM,WAAW,GAAG,cAAc,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;IAE9C,MAAM,EAAC,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,OAAO,EAAC,GAAG,WAAW,CAAC,qBAAqB,EAAE,CAAC;IAC9E,MAAM,OAAO,GAA0B,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAA,IAAI,aAAJ,IAAI,uBAAJ,IAAI,CAAE,IAAI,mCAAI,EAAE,CAAC,CAAC;IAEvF,OAAO,EAAC,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,OAAO,EAAC,CAAC;AAC9C,CAAC,CAAC"}
|
|
@@ -22,18 +22,19 @@ interface SubmitConsentMutationBuilder {
|
|
|
22
22
|
};
|
|
23
23
|
}) => unknown;
|
|
24
24
|
}
|
|
25
|
+
interface SubmitConsentEnhancedApi {
|
|
26
|
+
useSubmitConsentResponseMutation: () => [
|
|
27
|
+
(body: SubmitConsentBody) => SubmitConsentMutationResult,
|
|
28
|
+
SubmitConsentMutationHookState
|
|
29
|
+
];
|
|
30
|
+
}
|
|
25
31
|
interface SubmitConsentApiWithTags {
|
|
26
32
|
injectEndpoints: (options: {
|
|
27
33
|
endpoints: (build: SubmitConsentMutationBuilder) => {
|
|
28
34
|
submitConsentResponse: unknown;
|
|
29
35
|
};
|
|
30
36
|
overrideExisting: boolean;
|
|
31
|
-
}) =>
|
|
32
|
-
useSubmitConsentResponseMutation: () => [
|
|
33
|
-
(body: SubmitConsentBody) => SubmitConsentMutationResult,
|
|
34
|
-
SubmitConsentMutationHookState
|
|
35
|
-
];
|
|
36
|
-
};
|
|
37
|
+
}) => SubmitConsentEnhancedApi;
|
|
37
38
|
}
|
|
38
39
|
interface SubmitConsentApi {
|
|
39
40
|
enhanceEndpoints: (options: {
|
package/dist/useSubmitConsent.js
CHANGED
|
@@ -1,7 +1,23 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Cache the enhanced api per (api, baseUrl). `injectEndpoints` logs a console
|
|
3
|
+
* error in development whenever an endpoint with the same name is re-injected
|
|
4
|
+
* (with `overrideExisting: false`), so calling it on every render of every
|
|
5
|
+
* consumer would flood the console. WeakMap-by-api lets the GC reclaim entries
|
|
6
|
+
* when the api object is unreachable.
|
|
7
|
+
*/
|
|
8
|
+
const enhancedApiCache = new WeakMap();
|
|
9
|
+
const getEnhancedApi = (api, base) => {
|
|
10
|
+
let byBase = enhancedApiCache.get(api);
|
|
11
|
+
if (!byBase) {
|
|
12
|
+
byBase = new Map();
|
|
13
|
+
enhancedApiCache.set(api, byBase);
|
|
14
|
+
}
|
|
15
|
+
const cached = byBase.get(base);
|
|
16
|
+
if (cached) {
|
|
17
|
+
return cached;
|
|
18
|
+
}
|
|
3
19
|
const apiWithConsentTags = api.enhanceEndpoints({ addTagTypes: ["PendingConsents"] });
|
|
4
|
-
const
|
|
20
|
+
const enhanced = apiWithConsentTags.injectEndpoints({
|
|
5
21
|
endpoints: (build) => ({
|
|
6
22
|
submitConsentResponse: build.mutation({
|
|
7
23
|
invalidatesTags: ["PendingConsents"],
|
|
@@ -14,6 +30,12 @@ export const useSubmitConsent = (api, baseUrl) => {
|
|
|
14
30
|
}),
|
|
15
31
|
overrideExisting: false,
|
|
16
32
|
});
|
|
33
|
+
byBase.set(base, enhanced);
|
|
34
|
+
return enhanced;
|
|
35
|
+
};
|
|
36
|
+
export const useSubmitConsent = (api, baseUrl) => {
|
|
37
|
+
const base = baseUrl || "";
|
|
38
|
+
const enhancedApi = getEnhancedApi(api, base);
|
|
17
39
|
const [submitMutation, { isLoading: isSubmitting, error }] = enhancedApi.useSubmitConsentResponseMutation();
|
|
18
40
|
const submit = async (body) => {
|
|
19
41
|
return submitMutation(body).unwrap();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useSubmitConsent.js","sourceRoot":"","sources":["../src/useSubmitConsent.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"useSubmitConsent.js","sourceRoot":"","sources":["../src/useSubmitConsent.ts"],"names":[],"mappings":"AA8CA;;;;;;GAMG;AACH,MAAM,gBAAgB,GAAG,IAAI,OAAO,EAA2D,CAAC;AAEhG,MAAM,cAAc,GAAG,CAAC,GAAqB,EAAE,IAAY,EAA4B,EAAE;IACvF,IAAI,MAAM,GAAG,gBAAgB,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACvC,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,MAAM,GAAG,IAAI,GAAG,EAAE,CAAC;QACnB,gBAAgB,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;IACpC,CAAC;IACD,MAAM,MAAM,GAAG,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAChC,IAAI,MAAM,EAAE,CAAC;QACX,OAAO,MAAM,CAAC;IAChB,CAAC;IACD,MAAM,kBAAkB,GAAG,GAAG,CAAC,gBAAgB,CAAC,EAAC,WAAW,EAAE,CAAC,iBAAiB,CAAC,EAAC,CAAC,CAAC;IACpF,MAAM,QAAQ,GAAG,kBAAkB,CAAC,eAAe,CAAC;QAClD,SAAS,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;YACrB,qBAAqB,EAAE,KAAK,CAAC,QAAQ,CAAC;gBACpC,eAAe,EAAE,CAAC,iBAAiB,CAAC;gBACpC,KAAK,EAAE,CAAC,IAAuB,EAAE,EAAE,CAAC,CAAC;oBACnC,IAAI;oBACJ,MAAM,EAAE,MAAM;oBACd,GAAG,EAAE,GAAG,IAAI,mBAAmB;iBAChC,CAAC;aACH,CAAC;SACH,CAAC;QACF,gBAAgB,EAAE,KAAK;KACxB,CAAC,CAAC;IACH,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;IAC3B,OAAO,QAAQ,CAAC;AAClB,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,GAAqB,EAAE,OAAgB,EAAE,EAAE;IAC1E,MAAM,IAAI,GAAG,OAAO,IAAI,EAAE,CAAC;IAC3B,MAAM,WAAW,GAAG,cAAc,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;IAE9C,MAAM,CAAC,cAAc,EAAE,EAAC,SAAS,EAAE,YAAY,EAAE,KAAK,EAAC,CAAC,GACtD,WAAW,CAAC,gCAAgC,EAAE,CAAC;IAEjD,MAAM,MAAM,GAAG,KAAK,EAAE,IAAuB,EAAE,EAAE;QAC/C,OAAO,cAAc,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,CAAC;IACvC,CAAC,CAAC;IAEF,OAAO,EAAC,KAAK,EAAE,YAAY,EAAE,MAAM,EAAC,CAAC;AACvC,CAAC,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,30 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {describe, expect, it, mock} from "bun:test";
|
|
2
2
|
import {act, fireEvent} from "@testing-library/react-native";
|
|
3
|
-
import {Text as RNText} from "react-native";
|
|
4
|
-
|
|
5
|
-
// Capture the real MarkdownView export before mocking so we can restore it in
|
|
6
|
-
// afterAll and avoid leaking a test-only mock to sibling test files.
|
|
7
|
-
const RealMarkdownViewModule = require("./MarkdownView");
|
|
8
|
-
|
|
9
|
-
// Mock MarkdownView with a simple Text passthrough so we can assert on the
|
|
10
|
-
// rendered (variable-substituted) content without depending on
|
|
11
|
-
// react-native-markdown-display's tokenization.
|
|
12
|
-
mock.module("./MarkdownView", () => ({
|
|
13
|
-
MarkdownView: ({children}: {children: string}) => (
|
|
14
|
-
<RNText testID="markdown-view">{children}</RNText>
|
|
15
|
-
),
|
|
16
|
-
}));
|
|
17
3
|
|
|
18
4
|
import {ConsentFormScreen} from "./ConsentFormScreen";
|
|
19
5
|
import {renderWithTheme} from "./test-utils";
|
|
20
6
|
import type {ConsentFormPublic} from "./useConsentForms";
|
|
21
7
|
|
|
22
|
-
// Restore the real MarkdownView so downstream test files (e.g.
|
|
23
|
-
// MarkdownView.test.tsx) see the un-mocked module regardless of test order.
|
|
24
|
-
afterAll(() => {
|
|
25
|
-
mock.module("./MarkdownView", () => RealMarkdownViewModule);
|
|
26
|
-
});
|
|
27
|
-
|
|
28
8
|
const baseForm: ConsentFormPublic = {
|
|
29
9
|
active: true,
|
|
30
10
|
agreeButtonText: "I agree",
|
|
@@ -59,7 +39,7 @@ describe("ConsentFormScreen", () => {
|
|
|
59
39
|
});
|
|
60
40
|
|
|
61
41
|
it("substitutes variables in content and preserves unknown placeholders", () => {
|
|
62
|
-
const {
|
|
42
|
+
const {getByText} = renderWithTheme(
|
|
63
43
|
<ConsentFormScreen
|
|
64
44
|
form={{...baseForm, content: {en: "Hello {{name}}, {{missing}} stays"}}}
|
|
65
45
|
locale="en"
|
|
@@ -67,7 +47,7 @@ describe("ConsentFormScreen", () => {
|
|
|
67
47
|
variables={{name: "Ada"}}
|
|
68
48
|
/>
|
|
69
49
|
);
|
|
70
|
-
expect(
|
|
50
|
+
expect(getByText("Hello Ada, {{missing}} stays")).toBeTruthy();
|
|
71
51
|
});
|
|
72
52
|
|
|
73
53
|
it("invokes onAgree with signature and checkbox values", () => {
|
|
@@ -164,4 +144,92 @@ describe("ConsentFormScreen", () => {
|
|
|
164
144
|
);
|
|
165
145
|
expect(getByTestId("consent-form-scroll-hint")).toBeTruthy();
|
|
166
146
|
});
|
|
147
|
+
|
|
148
|
+
it("shows footer scroll hint when scroll to bottom is required but not done", () => {
|
|
149
|
+
const form = {...baseForm, requireScrollToBottom: true};
|
|
150
|
+
const {getByTestId, queryByTestId} = renderWithTheme(
|
|
151
|
+
<ConsentFormScreen form={form} locale="en" onAgree={() => {}} />
|
|
152
|
+
);
|
|
153
|
+
expect(getByTestId("consent-footer-scroll-hint")).toBeTruthy();
|
|
154
|
+
expect(queryByTestId("consent-footer-signature-hint")).toBeNull();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("hides footer scroll hint when scroll to bottom is not required", () => {
|
|
158
|
+
const {queryByTestId} = renderWithTheme(
|
|
159
|
+
<ConsentFormScreen form={baseForm} locale="en" onAgree={() => {}} />
|
|
160
|
+
);
|
|
161
|
+
expect(queryByTestId("consent-footer-scroll-hint")).toBeNull();
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("shows footer signature hint when signature is required but not provided", () => {
|
|
165
|
+
const form = {...baseForm, captureSignature: true};
|
|
166
|
+
const {getByTestId, queryByTestId} = renderWithTheme(
|
|
167
|
+
<ConsentFormScreen form={form} locale="en" onAgree={() => {}} />
|
|
168
|
+
);
|
|
169
|
+
expect(getByTestId("consent-footer-signature-hint")).toBeTruthy();
|
|
170
|
+
expect(queryByTestId("consent-footer-scroll-hint")).toBeNull();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("hides footer signature hint when signature is not required", () => {
|
|
174
|
+
const {queryByTestId} = renderWithTheme(
|
|
175
|
+
<ConsentFormScreen form={baseForm} locale="en" onAgree={() => {}} />
|
|
176
|
+
);
|
|
177
|
+
expect(queryByTestId("consent-footer-signature-hint")).toBeNull();
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("shows both footer hints when scroll and signature are both required", () => {
|
|
181
|
+
const form = {...baseForm, captureSignature: true, requireScrollToBottom: true};
|
|
182
|
+
const {getByTestId} = renderWithTheme(
|
|
183
|
+
<ConsentFormScreen form={form} locale="en" onAgree={() => {}} />
|
|
184
|
+
);
|
|
185
|
+
expect(getByTestId("consent-footer-scroll-hint")).toBeTruthy();
|
|
186
|
+
expect(getByTestId("consent-footer-signature-hint")).toBeTruthy();
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("shows the required-items legend when any checkbox is required", () => {
|
|
190
|
+
const form: ConsentFormPublic = {
|
|
191
|
+
...baseForm,
|
|
192
|
+
checkboxes: [{label: "Required box", required: true}],
|
|
193
|
+
};
|
|
194
|
+
const {getByTestId} = renderWithTheme(
|
|
195
|
+
<ConsentFormScreen form={form} locale="en" onAgree={() => {}} />
|
|
196
|
+
);
|
|
197
|
+
expect(getByTestId("consent-form-required-legend")).toBeTruthy();
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("hides the required-items legend when no checkbox is required", () => {
|
|
201
|
+
const form: ConsentFormPublic = {
|
|
202
|
+
...baseForm,
|
|
203
|
+
checkboxes: [{label: "Optional box", required: false}],
|
|
204
|
+
};
|
|
205
|
+
const {queryByTestId} = renderWithTheme(
|
|
206
|
+
<ConsentFormScreen form={form} locale="en" onAgree={() => {}} />
|
|
207
|
+
);
|
|
208
|
+
expect(queryByTestId("consent-form-required-legend")).toBeNull();
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("shows the checkbox footer hint when a required checkbox is unchecked", () => {
|
|
212
|
+
const form: ConsentFormPublic = {
|
|
213
|
+
...baseForm,
|
|
214
|
+
checkboxes: [{label: "Required box", required: true}],
|
|
215
|
+
};
|
|
216
|
+
const {getByTestId} = renderWithTheme(
|
|
217
|
+
<ConsentFormScreen form={form} locale="en" onAgree={() => {}} />
|
|
218
|
+
);
|
|
219
|
+
expect(getByTestId("consent-footer-checkboxes-hint")).toBeTruthy();
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("hides the checkbox footer hint once required checkboxes are checked", () => {
|
|
223
|
+
const form: ConsentFormPublic = {
|
|
224
|
+
...baseForm,
|
|
225
|
+
checkboxes: [{label: "Required box", required: true}],
|
|
226
|
+
};
|
|
227
|
+
const {getByTestId, queryByTestId} = renderWithTheme(
|
|
228
|
+
<ConsentFormScreen form={form} locale="en" onAgree={() => {}} />
|
|
229
|
+
);
|
|
230
|
+
act(() => {
|
|
231
|
+
fireEvent.press(getByTestId("consent-form-checkbox-0"));
|
|
232
|
+
});
|
|
233
|
+
expect(queryByTestId("consent-footer-checkboxes-hint")).toBeNull();
|
|
234
|
+
});
|
|
167
235
|
});
|
|
@@ -50,6 +50,7 @@ export const ConsentFormScreen: React.FC<ConsentFormScreenProps> = ({
|
|
|
50
50
|
});
|
|
51
51
|
|
|
52
52
|
const signatureProvided = !form.captureSignature || Boolean(signatureValue);
|
|
53
|
+
const hasRequiredCheckboxes = form.checkboxes.some((checkbox) => checkbox.required);
|
|
53
54
|
|
|
54
55
|
const canAgree = hasScrolledToBottom && allRequiredCheckboxesChecked && signatureProvided;
|
|
55
56
|
|
|
@@ -140,6 +141,21 @@ export const ConsentFormScreen: React.FC<ConsentFormScreenProps> = ({
|
|
|
140
141
|
/>
|
|
141
142
|
</Box>
|
|
142
143
|
</Box>
|
|
144
|
+
{Boolean(form.requireScrollToBottom && !hasScrolledToBottom) && (
|
|
145
|
+
<Text align="center" color="error" size="sm" testID="consent-footer-scroll-hint">
|
|
146
|
+
Please scroll to the bottom to continue
|
|
147
|
+
</Text>
|
|
148
|
+
)}
|
|
149
|
+
{Boolean(hasRequiredCheckboxes && !allRequiredCheckboxesChecked) && (
|
|
150
|
+
<Text align="center" color="error" size="sm" testID="consent-footer-checkboxes-hint">
|
|
151
|
+
Please check all required items marked with *
|
|
152
|
+
</Text>
|
|
153
|
+
)}
|
|
154
|
+
{Boolean(form.captureSignature && !signatureValue) && (
|
|
155
|
+
<Text align="center" color="error" size="sm" testID="consent-footer-signature-hint">
|
|
156
|
+
Please provide your signature to continue
|
|
157
|
+
</Text>
|
|
158
|
+
)}
|
|
143
159
|
</Box>
|
|
144
160
|
);
|
|
145
161
|
|
|
@@ -159,6 +175,11 @@ export const ConsentFormScreen: React.FC<ConsentFormScreenProps> = ({
|
|
|
159
175
|
|
|
160
176
|
{form.checkboxes.length > 0 && (
|
|
161
177
|
<Box direction="column" gap={2} testID="consent-form-checkboxes">
|
|
178
|
+
{hasRequiredCheckboxes && (
|
|
179
|
+
<Text color="secondaryDark" size="sm" testID="consent-form-required-legend">
|
|
180
|
+
* indicates a required item
|
|
181
|
+
</Text>
|
|
182
|
+
)}
|
|
162
183
|
{form.checkboxes.map((checkbox, index) => {
|
|
163
184
|
const key = index.toString();
|
|
164
185
|
const isChecked = checkboxValues[key] ?? false;
|
package/src/Field.tsx
CHANGED
|
@@ -50,7 +50,7 @@ export const Field: FC<FieldProps> = ({type, ...rest}) => {
|
|
|
50
50
|
} else if (type && ["date", "time", "datetime"].includes(type)) {
|
|
51
51
|
return (
|
|
52
52
|
<DateTimeField
|
|
53
|
-
{...(rest as DateTimeFieldProps
|
|
53
|
+
{...(rest as Omit<DateTimeFieldProps, "type">)}
|
|
54
54
|
type={type as "date" | "time" | "datetime"}
|
|
55
55
|
/>
|
|
56
56
|
);
|
package/src/HeightField.test.tsx
CHANGED
|
@@ -206,3 +206,71 @@ describe("HeightField", () => {
|
|
|
206
206
|
});
|
|
207
207
|
});
|
|
208
208
|
});
|
|
209
|
+
|
|
210
|
+
describe("HeightField - Android platform", () => {
|
|
211
|
+
// Toggle Platform.OS to "android" to exercise the Android rendering branch
|
|
212
|
+
// that uses SelectField pickers instead of the Pressable+ActionSheet path.
|
|
213
|
+
const {Platform} = require("react-native") as {Platform: {OS: string}};
|
|
214
|
+
const originalOS = Platform.OS;
|
|
215
|
+
|
|
216
|
+
beforeEach(() => {
|
|
217
|
+
Platform.OS = "android";
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
afterEach(() => {
|
|
221
|
+
Platform.OS = originalOS;
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("renders Android pickers with title, helperText, and errorText", () => {
|
|
225
|
+
const onChange = mock(() => {});
|
|
226
|
+
const {getByText, queryByLabelText} = renderWithTheme(
|
|
227
|
+
<HeightField
|
|
228
|
+
errorText="Required"
|
|
229
|
+
helperText="Enter height"
|
|
230
|
+
onChange={onChange}
|
|
231
|
+
title="Height"
|
|
232
|
+
value="70"
|
|
233
|
+
/>
|
|
234
|
+
);
|
|
235
|
+
// Title and helper/error rendered
|
|
236
|
+
expect(getByText("Height")).toBeTruthy();
|
|
237
|
+
expect(getByText("Enter height")).toBeTruthy();
|
|
238
|
+
expect(getByText("Required")).toBeTruthy();
|
|
239
|
+
// The Pressable from the iOS branch should NOT be present.
|
|
240
|
+
expect(queryByLabelText("Height selector")).toBeNull();
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it("renders Android pickers in disabled state", () => {
|
|
244
|
+
const onChange = mock(() => {});
|
|
245
|
+
const {root} = renderWithTheme(
|
|
246
|
+
<HeightField disabled onChange={onChange} title="Height" value="60" />
|
|
247
|
+
);
|
|
248
|
+
expect(root).toBeTruthy();
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it("forwards feet picker changes to onChange (Android)", () => {
|
|
252
|
+
const onChange = mock(() => {});
|
|
253
|
+
const {SelectField} = require("./SelectField") as {
|
|
254
|
+
SelectField: React.ComponentType<{onChange?: (v: string) => void}>;
|
|
255
|
+
};
|
|
256
|
+
const {UNSAFE_getAllByType} = renderWithTheme(<HeightField onChange={onChange} value="70" />);
|
|
257
|
+
const selects = UNSAFE_getAllByType(SelectField);
|
|
258
|
+
expect(selects.length).toBe(2);
|
|
259
|
+
// First SelectField is feet. value="70" → 5ft 10in. Bumping feet to 6 yields 6*12+10=82.
|
|
260
|
+
selects[0].props.onChange?.("6");
|
|
261
|
+
expect(onChange).toHaveBeenCalledWith("82");
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it("forwards inches picker changes to onChange (Android)", () => {
|
|
265
|
+
const onChange = mock(() => {});
|
|
266
|
+
const {SelectField} = require("./SelectField") as {
|
|
267
|
+
SelectField: React.ComponentType<{onChange?: (v: string) => void}>;
|
|
268
|
+
};
|
|
269
|
+
const {UNSAFE_getAllByType} = renderWithTheme(<HeightField onChange={onChange} value="70" />);
|
|
270
|
+
const selects = UNSAFE_getAllByType(SelectField);
|
|
271
|
+
expect(selects.length).toBe(2);
|
|
272
|
+
// Second SelectField is inches. value="70" → 5ft 10in. Changing inches to 3 yields 5*12+3=63.
|
|
273
|
+
selects[1].props.onChange?.("3");
|
|
274
|
+
expect(onChange).toHaveBeenCalledWith("63");
|
|
275
|
+
});
|
|
276
|
+
});
|
package/src/Modal.tsx
CHANGED
|
@@ -12,7 +12,7 @@ import {Gesture, GestureDetector} from "react-native-gesture-handler";
|
|
|
12
12
|
import {runOnJS} from "react-native-reanimated";
|
|
13
13
|
|
|
14
14
|
import {Button} from "./Button";
|
|
15
|
-
import type {ModalProps} from "./Common";
|
|
15
|
+
import type {ModalProps, TerrenoTheme} from "./Common";
|
|
16
16
|
import {Heading} from "./Heading";
|
|
17
17
|
import {Icon} from "./Icon";
|
|
18
18
|
import {isMobileDevice} from "./MediaQuery";
|
|
@@ -45,7 +45,7 @@ const ModalContent: FC<{
|
|
|
45
45
|
secondaryButtonOnClick?: ModalProps["secondaryButtonOnClick"];
|
|
46
46
|
onDismiss: ModalProps["onDismiss"];
|
|
47
47
|
sizePx: DimensionValue;
|
|
48
|
-
theme:
|
|
48
|
+
theme: TerrenoTheme;
|
|
49
49
|
isMobile: boolean;
|
|
50
50
|
}> = ({
|
|
51
51
|
children,
|
package/src/NumberField.test.tsx
CHANGED
|
@@ -4,23 +4,23 @@ import {act, fireEvent, waitFor} from "@testing-library/react-native";
|
|
|
4
4
|
import {NumberField} from "./NumberField";
|
|
5
5
|
import {renderWithTheme} from "./test-utils";
|
|
6
6
|
|
|
7
|
+
const noOp = (): void => {};
|
|
8
|
+
|
|
7
9
|
describe("NumberField", () => {
|
|
8
10
|
it("renders correctly with default props", () => {
|
|
9
|
-
const {toJSON} = renderWithTheme(
|
|
10
|
-
<NumberField label="Number" onChange={() => {}} type="number" />
|
|
11
|
-
);
|
|
11
|
+
const {toJSON} = renderWithTheme(<NumberField label="Number" onChange={noOp} type="number" />);
|
|
12
12
|
expect(toJSON()).toMatchSnapshot();
|
|
13
13
|
});
|
|
14
14
|
|
|
15
15
|
it("renders with initial value", () => {
|
|
16
16
|
const {getByDisplayValue} = renderWithTheme(
|
|
17
|
-
<NumberField label="Number" onChange={
|
|
17
|
+
<NumberField label="Number" onChange={noOp} type="number" value="42" />
|
|
18
18
|
);
|
|
19
19
|
expect(getByDisplayValue("42")).toBeTruthy();
|
|
20
20
|
});
|
|
21
21
|
|
|
22
22
|
it("calls onChange with valid integer", async () => {
|
|
23
|
-
const handleChange = mock((_value: string) => {});
|
|
23
|
+
const handleChange = mock((_value: string): void => {});
|
|
24
24
|
const {getByDisplayValue} = renderWithTheme(
|
|
25
25
|
<NumberField label="Number" onChange={handleChange} type="number" value="" />
|
|
26
26
|
);
|
|
@@ -36,7 +36,7 @@ describe("NumberField", () => {
|
|
|
36
36
|
});
|
|
37
37
|
|
|
38
38
|
it("does not call onChange with non-integer for number type", async () => {
|
|
39
|
-
const handleChange = mock((_value: string) => {});
|
|
39
|
+
const handleChange = mock((_value: string): void => {});
|
|
40
40
|
const {getByDisplayValue} = renderWithTheme(
|
|
41
41
|
<NumberField label="Number" onChange={handleChange} type="number" value="" />
|
|
42
42
|
);
|
|
@@ -51,7 +51,7 @@ describe("NumberField", () => {
|
|
|
51
51
|
});
|
|
52
52
|
|
|
53
53
|
it("calls onChange with valid decimal", async () => {
|
|
54
|
-
const handleChange = mock((_value: string) => {});
|
|
54
|
+
const handleChange = mock((_value: string): void => {});
|
|
55
55
|
const {getByDisplayValue} = renderWithTheme(
|
|
56
56
|
<NumberField label="Decimal" onChange={handleChange} type="decimal" value="" />
|
|
57
57
|
);
|
|
@@ -67,7 +67,7 @@ describe("NumberField", () => {
|
|
|
67
67
|
});
|
|
68
68
|
|
|
69
69
|
it("handles leading dot for decimal type", async () => {
|
|
70
|
-
const handleChange = mock((_value: string) => {});
|
|
70
|
+
const handleChange = mock((_value: string): void => {});
|
|
71
71
|
const {getByDisplayValue} = renderWithTheme(
|
|
72
72
|
<NumberField label="Decimal" onChange={handleChange} type="decimal" value="" />
|
|
73
73
|
);
|
|
@@ -84,40 +84,34 @@ describe("NumberField", () => {
|
|
|
84
84
|
|
|
85
85
|
it("validates max value", () => {
|
|
86
86
|
const {getByText} = renderWithTheme(
|
|
87
|
-
<NumberField label="Number" max={100} onChange={
|
|
87
|
+
<NumberField label="Number" max={100} onChange={noOp} type="number" value="150" />
|
|
88
88
|
);
|
|
89
89
|
expect(getByText("Value must be less than or equal to 100")).toBeTruthy();
|
|
90
90
|
});
|
|
91
91
|
|
|
92
92
|
it("validates min value", () => {
|
|
93
93
|
const {getByText} = renderWithTheme(
|
|
94
|
-
<NumberField label="Number" min={10} onChange={
|
|
94
|
+
<NumberField label="Number" min={10} onChange={noOp} type="number" value="5" />
|
|
95
95
|
);
|
|
96
96
|
expect(getByText("Value must be greater than or equal to 10")).toBeTruthy();
|
|
97
97
|
});
|
|
98
98
|
|
|
99
99
|
it("shows custom errorText", () => {
|
|
100
100
|
const {getByText} = renderWithTheme(
|
|
101
|
-
<NumberField
|
|
102
|
-
errorText="Custom error"
|
|
103
|
-
label="Number"
|
|
104
|
-
onChange={() => {}}
|
|
105
|
-
type="number"
|
|
106
|
-
value=""
|
|
107
|
-
/>
|
|
101
|
+
<NumberField errorText="Custom error" label="Number" onChange={noOp} type="number" value="" />
|
|
108
102
|
);
|
|
109
103
|
expect(getByText("Custom error")).toBeTruthy();
|
|
110
104
|
});
|
|
111
105
|
|
|
112
106
|
it("does not show error for valid number within range", () => {
|
|
113
107
|
const {queryByText} = renderWithTheme(
|
|
114
|
-
<NumberField label="Number" max={100} min={0} onChange={
|
|
108
|
+
<NumberField label="Number" max={100} min={0} onChange={noOp} type="number" value="50" />
|
|
115
109
|
);
|
|
116
110
|
expect(queryByText(/must be/)).toBeNull();
|
|
117
111
|
});
|
|
118
112
|
|
|
119
113
|
it("syncs value when prop changes", async () => {
|
|
120
|
-
const handleChange = mock((_value: string) => {});
|
|
114
|
+
const handleChange = mock((_value: string): void => {});
|
|
121
115
|
const {getByDisplayValue, unmount} = renderWithTheme(
|
|
122
116
|
<NumberField label="Number" onChange={handleChange} type="number" value="10" />
|
|
123
117
|
);
|