@terreno/ui 0.10.0 → 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Banner.js +7 -5
- package/dist/Banner.js.map +1 -1
- package/dist/Common.d.ts +3 -1
- package/dist/Common.js.map +1 -1
- package/dist/TextFieldNumberActionSheet.d.ts +1 -1
- package/dist/Toast.d.ts +1 -1
- package/dist/Toast.js +2 -2
- package/dist/Toast.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/package.json +2 -1
- package/src/ActionSheet.test.tsx +262 -3
- package/src/AddressField.test.tsx +50 -0
- package/src/Banner.test.tsx +65 -0
- package/src/Banner.tsx +7 -5
- package/src/Box.test.tsx +218 -0
- package/src/Button.test.tsx +71 -0
- package/src/Common.ts +3 -1
- package/src/ConsentFormScreen.test.tsx +167 -0
- package/src/ConsentNavigator.test.tsx +206 -0
- package/src/DecimalRangeActionSheet.test.tsx +53 -2
- package/src/EmailField.test.tsx +81 -0
- package/src/EmojiSelector.test.tsx +262 -1
- package/src/HeightActionSheet.test.tsx +57 -2
- package/src/InfoModalIcon.test.tsx +16 -0
- package/src/InfoTooltipButton.test.tsx +53 -1
- package/src/MobileAddressAutoComplete.test.tsx +137 -7
- package/src/Modal.test.tsx +188 -0
- package/src/NumberPickerActionSheet.test.tsx +59 -2
- package/src/Page.test.tsx +162 -1
- package/src/Pagination.test.tsx +16 -0
- package/src/PhoneNumberField.test.tsx +46 -9
- package/src/PickerSelect.test.tsx +230 -0
- package/src/SegmentedControl.test.tsx +38 -0
- package/src/SelectBadge.test.tsx +52 -1
- package/src/SideDrawer.test.tsx +69 -0
- package/src/Signature.test.tsx +42 -5
- package/src/SignatureField.test.tsx +35 -0
- package/src/Slider.test.tsx +59 -0
- package/src/Spinner.test.tsx +6 -0
- package/src/SplitPage.test.tsx +228 -2
- package/src/TapToEdit.test.tsx +171 -1
- package/src/TerrenoProvider.test.tsx +42 -2
- package/src/TextFieldNumberActionSheet.tsx +1 -1
- package/src/Theme.test.tsx +118 -28
- package/src/Toast.test.tsx +95 -2
- package/src/Toast.tsx +3 -3
- package/src/Tooltip.test.tsx +204 -1
- package/src/UnifiedAddressAutoComplete.test.tsx +38 -19
- package/src/UserInactivity.test.tsx +73 -1
- package/src/Utilities.test.tsx +190 -2
- package/src/WebAddressAutocomplete.test.tsx +148 -1
- package/src/__snapshots__/ActionSheet.test.tsx.snap +1736 -0
- package/src/__snapshots__/Button.test.tsx.snap +68 -0
- package/src/__snapshots__/EmojiSelector.test.tsx.snap +1363 -0
- package/src/__snapshots__/InfoTooltipButton.test.tsx.snap +72 -3
- package/src/__snapshots__/MobileAddressAutoComplete.test.tsx.snap +60 -9
- package/src/__snapshots__/Modal.test.tsx.snap +181 -0
- package/src/__snapshots__/Page.test.tsx.snap +48 -2
- package/src/__snapshots__/PhoneNumberField.test.tsx.snap +0 -93
- package/src/__snapshots__/PickerSelect.test.tsx.snap +706 -0
- package/src/__snapshots__/SideDrawer.test.tsx.snap +533 -1399
- package/src/__snapshots__/Signature.test.tsx.snap +0 -3
- package/src/__snapshots__/SplitPage.test.tsx.snap +970 -0
- package/src/__snapshots__/UnifiedAddressAutoComplete.test.tsx.snap +220 -4
- package/src/__snapshots__/WebAddressAutocomplete.test.tsx.snap +93 -0
- package/src/bunSetup.ts +204 -121
- package/src/index.tsx +2 -2
- package/src/table/TableHeaderCell.test.tsx +142 -0
- package/src/table/TableRow.test.tsx +33 -0
- package/src/table/__snapshots__/TableRow.test.tsx.snap +403 -0
- package/src/table/tableContext.test.tsx +96 -0
- package/src/test-utils.tsx +1 -1
- package/src/useConsentForms.test.ts +130 -0
- package/src/useSubmitConsent.test.ts +64 -0
package/src/Box.test.tsx
CHANGED
|
@@ -659,4 +659,222 @@ describe("Box", () => {
|
|
|
659
659
|
expect(component.toJSON()).toMatchSnapshot();
|
|
660
660
|
});
|
|
661
661
|
});
|
|
662
|
+
|
|
663
|
+
describe("edge case warnings and fallbacks", () => {
|
|
664
|
+
it("returns empty style when border prop is falsy", () => {
|
|
665
|
+
const {root} = renderWithTheme(<Box border={undefined as any} />);
|
|
666
|
+
expect(root).toBeTruthy();
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
it("returns empty style when borderBottom prop is falsy", () => {
|
|
670
|
+
const {root} = renderWithTheme(<Box borderBottom={undefined as any} />);
|
|
671
|
+
expect(root).toBeTruthy();
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
it("returns empty style when borderLeft prop is falsy", () => {
|
|
675
|
+
const {root} = renderWithTheme(<Box borderLeft={undefined as any} />);
|
|
676
|
+
expect(root).toBeTruthy();
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
it("returns empty style when borderRight prop is falsy", () => {
|
|
680
|
+
const {root} = renderWithTheme(<Box borderRight={undefined as any} />);
|
|
681
|
+
expect(root).toBeTruthy();
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
it("returns empty style when borderTop prop is falsy", () => {
|
|
685
|
+
const {root} = renderWithTheme(<Box borderTop={undefined as any} />);
|
|
686
|
+
expect(root).toBeTruthy();
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
it("warns when invalid height value is provided", () => {
|
|
690
|
+
const warnSpy = spyOn(console, "warn").mockImplementation(() => {});
|
|
691
|
+
renderWithTheme(<Box height={"abc" as any} />);
|
|
692
|
+
expect(warnSpy).toHaveBeenCalled();
|
|
693
|
+
warnSpy.mockRestore();
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
it("warns when invalid maxHeight value is provided", () => {
|
|
697
|
+
const warnSpy = spyOn(console, "warn").mockImplementation(() => {});
|
|
698
|
+
renderWithTheme(<Box maxHeight={"xyz" as any} />);
|
|
699
|
+
expect(warnSpy).toHaveBeenCalled();
|
|
700
|
+
warnSpy.mockRestore();
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
it("warns when invalid maxWidth value is provided", () => {
|
|
704
|
+
const warnSpy = spyOn(console, "warn").mockImplementation(() => {});
|
|
705
|
+
renderWithTheme(<Box maxWidth={"abc" as any} />);
|
|
706
|
+
expect(warnSpy).toHaveBeenCalled();
|
|
707
|
+
warnSpy.mockRestore();
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
it("warns when invalid minHeight value is provided", () => {
|
|
711
|
+
const warnSpy = spyOn(console, "warn").mockImplementation(() => {});
|
|
712
|
+
renderWithTheme(<Box minHeight={"abc" as any} />);
|
|
713
|
+
expect(warnSpy).toHaveBeenCalled();
|
|
714
|
+
warnSpy.mockRestore();
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
it("warns when invalid minWidth value is provided", () => {
|
|
718
|
+
const warnSpy = spyOn(console, "warn").mockImplementation(() => {});
|
|
719
|
+
renderWithTheme(<Box minWidth={"abc" as any} />);
|
|
720
|
+
expect(warnSpy).toHaveBeenCalled();
|
|
721
|
+
warnSpy.mockRestore();
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
it("warns when invalid width value is provided", () => {
|
|
725
|
+
const warnSpy = spyOn(console, "warn").mockImplementation(() => {});
|
|
726
|
+
renderWithTheme(<Box width={"abc" as any} />);
|
|
727
|
+
expect(warnSpy).toHaveBeenCalled();
|
|
728
|
+
warnSpy.mockRestore();
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
it("applies mdDirection only when mediaQuery is md+", () => {
|
|
732
|
+
const {root} = renderWithTheme(<Box mdDirection="row" />);
|
|
733
|
+
expect(root).toBeTruthy();
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
it("applies lgDirection only when mediaQuery is lg", () => {
|
|
737
|
+
const {root} = renderWithTheme(<Box lgDirection="column" />);
|
|
738
|
+
expect(root).toBeTruthy();
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
it("applies width with border adds 4 pixels", () => {
|
|
742
|
+
const {root} = renderWithTheme(<Box border="default" width={100} />);
|
|
743
|
+
const view = root.findByType("View");
|
|
744
|
+
expect(view.props.style.width).toBe(104);
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
it("applies height with border adds 4 pixels", () => {
|
|
748
|
+
const {root} = renderWithTheme(<Box border="default" height={80} />);
|
|
749
|
+
const view = root.findByType("View");
|
|
750
|
+
expect(view.props.style.height).toBe(84);
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
it("warns when wrap is combined with alignItems on native", () => {
|
|
754
|
+
const warnSpy = spyOn(console, "warn").mockImplementation(() => {});
|
|
755
|
+
renderWithTheme(<Box alignItems="center" wrap />);
|
|
756
|
+
expect(warnSpy).toHaveBeenCalled();
|
|
757
|
+
warnSpy.mockRestore();
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
it("applies dangerouslySetInlineStyle overrides", () => {
|
|
761
|
+
const {root} = renderWithTheme(
|
|
762
|
+
<Box dangerouslySetInlineStyle={{__style: {backgroundColor: "red"}}} />
|
|
763
|
+
);
|
|
764
|
+
const view = root.findByType("View");
|
|
765
|
+
expect(view.props.style.backgroundColor).toBe("red");
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
it("handles rounding='circle' with width/height", () => {
|
|
769
|
+
const {root} = renderWithTheme(<Box height={40} rounding="circle" width={40} />);
|
|
770
|
+
const view = root.findByType("View");
|
|
771
|
+
expect(view.props.style.borderRadius).toBe(40);
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
it("warns when rounding='circle' without width/height", () => {
|
|
775
|
+
const warnSpy = spyOn(console, "warn").mockImplementation(() => {});
|
|
776
|
+
renderWithTheme(<Box rounding="circle" />);
|
|
777
|
+
expect(warnSpy).toHaveBeenCalled();
|
|
778
|
+
warnSpy.mockRestore();
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
it("applies shadow on ios/web", () => {
|
|
782
|
+
const {root} = renderWithTheme(<Box shadow />);
|
|
783
|
+
const view = root.findByType("View");
|
|
784
|
+
expect(view.props.style.boxShadow).toBeDefined();
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
it("applies overflow='scroll'", () => {
|
|
788
|
+
const {root} = renderWithTheme(<Box overflow="scroll" />);
|
|
789
|
+
const view = root.findByType("View");
|
|
790
|
+
expect(view.props.style.overflow).toBe("scroll");
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
it("applies overflow='scrollY'", () => {
|
|
794
|
+
const {root} = renderWithTheme(<Box overflow="scrollY" />);
|
|
795
|
+
const view = root.findByType("View");
|
|
796
|
+
expect(view.props.style.overflow).toBe("scroll");
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
it("applies position='absolute'", () => {
|
|
800
|
+
const {root} = renderWithTheme(<Box position="absolute" />);
|
|
801
|
+
const view = root.findByType("View");
|
|
802
|
+
expect(view.props.style.position).toBe("absolute");
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
it("applies zIndex", () => {
|
|
806
|
+
const {root} = renderWithTheme(<Box zIndex={10} />);
|
|
807
|
+
const view = root.findByType("View");
|
|
808
|
+
expect(view.props.style.zIndex).toBe(10);
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
it("applies top/left/right/bottom offsets", () => {
|
|
812
|
+
const {root} = renderWithTheme(<Box bottom left position="absolute" right top />);
|
|
813
|
+
const view = root.findByType("View");
|
|
814
|
+
expect(view.props.style.top).toBe(0);
|
|
815
|
+
expect(view.props.style.left).toBe(0);
|
|
816
|
+
expect(view.props.style.right).toBe(0);
|
|
817
|
+
expect(view.props.style.bottom).toBe(0);
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
it("fires onScroll when scroll is enabled", async () => {
|
|
821
|
+
const onScroll = mock(() => {});
|
|
822
|
+
const {root} = renderWithTheme(
|
|
823
|
+
<Box onScroll={onScroll} scroll>
|
|
824
|
+
<Text>Scrollable</Text>
|
|
825
|
+
</Box>
|
|
826
|
+
);
|
|
827
|
+
const scrollView = root.findByType("ScrollView" as any);
|
|
828
|
+
await act(async () => {
|
|
829
|
+
scrollView.props.onScroll?.({nativeEvent: {contentOffset: {y: 100}}});
|
|
830
|
+
});
|
|
831
|
+
expect(onScroll).toHaveBeenCalledWith(100);
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
it("invokes onHoverStart/onHoverEnd callbacks", async () => {
|
|
835
|
+
const onHoverStart = mock(() => {});
|
|
836
|
+
const onHoverEnd = mock(() => {});
|
|
837
|
+
const {root} = renderWithTheme(
|
|
838
|
+
<Box onHoverEnd={onHoverEnd} onHoverStart={onHoverStart}>
|
|
839
|
+
<Text>Hover</Text>
|
|
840
|
+
</Box>
|
|
841
|
+
);
|
|
842
|
+
const view = root.findByType("View");
|
|
843
|
+
await act(async () => {
|
|
844
|
+
await view.props.onPointerEnter?.();
|
|
845
|
+
});
|
|
846
|
+
await act(async () => {
|
|
847
|
+
await view.props.onPointerLeave?.();
|
|
848
|
+
});
|
|
849
|
+
expect(onHoverStart).toHaveBeenCalled();
|
|
850
|
+
expect(onHoverEnd).toHaveBeenCalled();
|
|
851
|
+
});
|
|
852
|
+
|
|
853
|
+
it("exposes scrollTo and scrollToEnd through ref", () => {
|
|
854
|
+
const ref = React.createRef<any>();
|
|
855
|
+
renderWithTheme(
|
|
856
|
+
<Box ref={ref} scroll>
|
|
857
|
+
<Text>Content</Text>
|
|
858
|
+
</Box>
|
|
859
|
+
);
|
|
860
|
+
expect(typeof ref.current?.scrollTo).toBe("function");
|
|
861
|
+
expect(typeof ref.current?.scrollToEnd).toBe("function");
|
|
862
|
+
// Call them to cover the function bodies
|
|
863
|
+
ref.current?.scrollTo(100);
|
|
864
|
+
ref.current?.scrollToEnd();
|
|
865
|
+
});
|
|
866
|
+
|
|
867
|
+
it("invokes onClick with haptic feedback", async () => {
|
|
868
|
+
const onClick = mock(() => Promise.resolve());
|
|
869
|
+
const {getByLabelText} = renderWithTheme(
|
|
870
|
+
<Box accessibilityHint="" accessibilityLabel="Press" onClick={onClick}>
|
|
871
|
+
<Text>Click</Text>
|
|
872
|
+
</Box>
|
|
873
|
+
);
|
|
874
|
+
await act(async () => {
|
|
875
|
+
fireEvent.press(getByLabelText("Press"));
|
|
876
|
+
});
|
|
877
|
+
expect(onClick).toHaveBeenCalled();
|
|
878
|
+
});
|
|
879
|
+
});
|
|
662
880
|
});
|
package/src/Button.test.tsx
CHANGED
|
@@ -157,4 +157,75 @@ describe("Button", () => {
|
|
|
157
157
|
const {getByLabelText} = renderWithTheme(<Button onClick={() => {}} text="Accessible" />);
|
|
158
158
|
expect(getByLabelText("Accessible")).toBeTruthy();
|
|
159
159
|
});
|
|
160
|
+
|
|
161
|
+
it("invokes onClick when confirmation primary button is pressed", async () => {
|
|
162
|
+
const handleClick = mock(() => Promise.resolve());
|
|
163
|
+
const {getByText, queryByText} = renderWithTheme(
|
|
164
|
+
<Button
|
|
165
|
+
confirmationText="Confirm action?"
|
|
166
|
+
modalTitle="Confirm Title"
|
|
167
|
+
onClick={handleClick}
|
|
168
|
+
text="Press Me"
|
|
169
|
+
withConfirmation
|
|
170
|
+
/>
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
await act(async () => {
|
|
174
|
+
fireEvent.press(getByText("Press Me"));
|
|
175
|
+
await new Promise((resolve) => setTimeout(resolve, 600));
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// Wait for confirmation modal
|
|
179
|
+
await waitFor(
|
|
180
|
+
() => {
|
|
181
|
+
expect(queryByText("Confirm Title")).toBeTruthy();
|
|
182
|
+
},
|
|
183
|
+
{timeout: 2000}
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
await act(async () => {
|
|
187
|
+
fireEvent.press(getByText("Confirm"));
|
|
188
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
await waitFor(() => {
|
|
192
|
+
expect(handleClick).toHaveBeenCalled();
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("dismisses confirmation modal when secondary button is pressed", async () => {
|
|
197
|
+
const handleClick = mock(() => Promise.resolve());
|
|
198
|
+
const {getByText, queryByText} = renderWithTheme(
|
|
199
|
+
<Button
|
|
200
|
+
confirmationText="Confirm action?"
|
|
201
|
+
modalTitle="Confirm Title"
|
|
202
|
+
onClick={handleClick}
|
|
203
|
+
text="Press Me"
|
|
204
|
+
withConfirmation
|
|
205
|
+
/>
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
await act(async () => {
|
|
209
|
+
fireEvent.press(getByText("Press Me"));
|
|
210
|
+
await new Promise((resolve) => setTimeout(resolve, 600));
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
await waitFor(
|
|
214
|
+
() => {
|
|
215
|
+
expect(queryByText("Cancel")).toBeTruthy();
|
|
216
|
+
},
|
|
217
|
+
{timeout: 2000}
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
// Cancel does not throw and does not invoke onClick
|
|
221
|
+
expect(() => fireEvent.press(getByText("Cancel"))).not.toThrow();
|
|
222
|
+
expect(handleClick).not.toHaveBeenCalled();
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it("renders with tooltip on desktop (wrapped in Tooltip)", () => {
|
|
226
|
+
const {toJSON} = renderWithTheme(
|
|
227
|
+
<Button onClick={() => {}} text="Hover me" tooltipText="Tooltip text" />
|
|
228
|
+
);
|
|
229
|
+
expect(toJSON()).toMatchSnapshot();
|
|
230
|
+
});
|
|
160
231
|
});
|
package/src/Common.ts
CHANGED
|
@@ -1447,8 +1447,10 @@ export interface BannerButtonProps {
|
|
|
1447
1447
|
export interface BannerPropsBase {
|
|
1448
1448
|
/**
|
|
1449
1449
|
* Used to identify if banner has been dismissed by the user.
|
|
1450
|
+
* When provided, dismissal state is persisted to AsyncStorage.
|
|
1451
|
+
* When omitted, dismissal is ephemeral (resets on remount).
|
|
1450
1452
|
*/
|
|
1451
|
-
id
|
|
1453
|
+
id?: string;
|
|
1452
1454
|
/**
|
|
1453
1455
|
* The text to display in the main body of the banner.
|
|
1454
1456
|
*/
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import {afterAll, describe, expect, it, mock} from "bun:test";
|
|
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
|
+
|
|
18
|
+
import {ConsentFormScreen} from "./ConsentFormScreen";
|
|
19
|
+
import {renderWithTheme} from "./test-utils";
|
|
20
|
+
import type {ConsentFormPublic} from "./useConsentForms";
|
|
21
|
+
|
|
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
|
+
const baseForm: ConsentFormPublic = {
|
|
29
|
+
active: true,
|
|
30
|
+
agreeButtonText: "I agree",
|
|
31
|
+
allowDecline: true,
|
|
32
|
+
captureSignature: false,
|
|
33
|
+
checkboxes: [],
|
|
34
|
+
content: {en: "Consent body", es: "Cuerpo de consentimiento"},
|
|
35
|
+
declineButtonText: "Decline",
|
|
36
|
+
defaultLocale: "en",
|
|
37
|
+
id: "consent-1",
|
|
38
|
+
order: 0,
|
|
39
|
+
required: true,
|
|
40
|
+
requireScrollToBottom: false,
|
|
41
|
+
slug: "consent",
|
|
42
|
+
title: "Consent",
|
|
43
|
+
type: "tos",
|
|
44
|
+
version: 1,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
describe("ConsentFormScreen", () => {
|
|
48
|
+
it("renders with default state and a locale fallback", () => {
|
|
49
|
+
const onAgree = mock(() => {});
|
|
50
|
+
const {getByTestId, getByText} = renderWithTheme(
|
|
51
|
+
<ConsentFormScreen
|
|
52
|
+
form={{...baseForm, content: {en: "Hello"}}}
|
|
53
|
+
locale="fr"
|
|
54
|
+
onAgree={onAgree}
|
|
55
|
+
/>
|
|
56
|
+
);
|
|
57
|
+
expect(getByTestId("consent-form-scroll-view")).toBeTruthy();
|
|
58
|
+
expect(getByText("I agree")).toBeTruthy();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("substitutes variables in content and preserves unknown placeholders", () => {
|
|
62
|
+
const {getByTestId} = renderWithTheme(
|
|
63
|
+
<ConsentFormScreen
|
|
64
|
+
form={{...baseForm, content: {en: "Hello {{name}}, {{missing}} stays"}}}
|
|
65
|
+
locale="en"
|
|
66
|
+
onAgree={() => {}}
|
|
67
|
+
variables={{name: "Ada"}}
|
|
68
|
+
/>
|
|
69
|
+
);
|
|
70
|
+
expect(getByTestId("markdown-view").props.children).toBe("Hello Ada, {{missing}} stays");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("invokes onAgree with signature and checkbox values", () => {
|
|
74
|
+
const onAgree = mock(() => {});
|
|
75
|
+
const form: ConsentFormPublic = {
|
|
76
|
+
...baseForm,
|
|
77
|
+
captureSignature: false,
|
|
78
|
+
checkboxes: [
|
|
79
|
+
{label: "Required box", required: true},
|
|
80
|
+
{label: "Optional box", required: false},
|
|
81
|
+
],
|
|
82
|
+
};
|
|
83
|
+
const {getByTestId} = renderWithTheme(
|
|
84
|
+
<ConsentFormScreen form={form} locale="en" onAgree={onAgree} />
|
|
85
|
+
);
|
|
86
|
+
// Toggle the required checkbox on and the optional one off/on
|
|
87
|
+
act(() => {
|
|
88
|
+
fireEvent.press(getByTestId("consent-form-checkbox-0"));
|
|
89
|
+
fireEvent.press(getByTestId("consent-form-checkbox-1"));
|
|
90
|
+
fireEvent.press(getByTestId("consent-form-checkbox-1"));
|
|
91
|
+
});
|
|
92
|
+
act(() => {
|
|
93
|
+
fireEvent.press(getByTestId("consent-form-agree-button"));
|
|
94
|
+
});
|
|
95
|
+
// Button has a debounce; wait briefly.
|
|
96
|
+
return new Promise<void>((resolve) => {
|
|
97
|
+
setTimeout(() => {
|
|
98
|
+
expect(onAgree).toHaveBeenCalledTimes(1);
|
|
99
|
+
const call = onAgree.mock.calls[0]?.[0] as {
|
|
100
|
+
checkboxValues: Record<string, boolean>;
|
|
101
|
+
signature?: string;
|
|
102
|
+
};
|
|
103
|
+
expect(call.checkboxValues["0"]).toBe(true);
|
|
104
|
+
expect(call.signature).toBeUndefined();
|
|
105
|
+
resolve();
|
|
106
|
+
}, 600);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("opens and confirms the confirmation modal before toggling checkbox on", () => {
|
|
111
|
+
const form: ConsentFormPublic = {
|
|
112
|
+
...baseForm,
|
|
113
|
+
checkboxes: [{confirmationPrompt: "Really?", label: "Tricky", required: true}],
|
|
114
|
+
};
|
|
115
|
+
const {getByTestId, queryByText} = renderWithTheme(
|
|
116
|
+
<ConsentFormScreen form={form} locale="en" onAgree={() => {}} />
|
|
117
|
+
);
|
|
118
|
+
act(() => {
|
|
119
|
+
fireEvent.press(getByTestId("consent-form-checkbox-0"));
|
|
120
|
+
});
|
|
121
|
+
expect(queryByText("Really?")).toBeTruthy();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("fires onDecline when decline is pressed", () => {
|
|
125
|
+
const onDecline = mock(() => {});
|
|
126
|
+
const {getByTestId} = renderWithTheme(
|
|
127
|
+
<ConsentFormScreen form={baseForm} locale="en" onAgree={() => {}} onDecline={onDecline} />
|
|
128
|
+
);
|
|
129
|
+
act(() => {
|
|
130
|
+
fireEvent.press(getByTestId("consent-form-decline-button"));
|
|
131
|
+
});
|
|
132
|
+
return new Promise<void>((resolve) => {
|
|
133
|
+
setTimeout(() => {
|
|
134
|
+
expect(onDecline).toHaveBeenCalled();
|
|
135
|
+
resolve();
|
|
136
|
+
}, 600);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("fires scroll handlers without crashing", () => {
|
|
141
|
+
const form = {...baseForm, requireScrollToBottom: true};
|
|
142
|
+
const {getByTestId} = renderWithTheme(
|
|
143
|
+
<ConsentFormScreen form={form} locale="en" onAgree={() => {}} />
|
|
144
|
+
);
|
|
145
|
+
const scroll = getByTestId("consent-form-scroll-view");
|
|
146
|
+
act(() => {
|
|
147
|
+
fireEvent(scroll, "contentSizeChange", 0, 300);
|
|
148
|
+
fireEvent(scroll, "layout", {nativeEvent: {layout: {height: 400}}});
|
|
149
|
+
fireEvent(scroll, "scroll", {
|
|
150
|
+
nativeEvent: {
|
|
151
|
+
contentOffset: {y: 100},
|
|
152
|
+
contentSize: {height: 300},
|
|
153
|
+
layoutMeasurement: {height: 210},
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
expect(scroll).toBeTruthy();
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("shows the scroll hint when content is still scrollable", () => {
|
|
161
|
+
const form = {...baseForm, requireScrollToBottom: true};
|
|
162
|
+
const {getByTestId} = renderWithTheme(
|
|
163
|
+
<ConsentFormScreen form={form} locale="en" onAgree={() => {}} />
|
|
164
|
+
);
|
|
165
|
+
expect(getByTestId("consent-form-scroll-hint")).toBeTruthy();
|
|
166
|
+
});
|
|
167
|
+
});
|
|
@@ -164,4 +164,210 @@ describe("ConsentNavigator", () => {
|
|
|
164
164
|
// The extra screen should have a working next button (onNext was injected)
|
|
165
165
|
expect(getByTestId("extra-screen-next")).toBeTruthy();
|
|
166
166
|
});
|
|
167
|
+
|
|
168
|
+
it("renders children when error is auth (401)", () => {
|
|
169
|
+
const api = {
|
|
170
|
+
enhanceEndpoints: mock(() => ({
|
|
171
|
+
injectEndpoints: mock(() => ({
|
|
172
|
+
useGetPendingConsentsQuery: mock(() => ({
|
|
173
|
+
data: undefined,
|
|
174
|
+
error: {status: 401},
|
|
175
|
+
isLoading: false,
|
|
176
|
+
refetch: mock(() => {}),
|
|
177
|
+
})),
|
|
178
|
+
useSubmitConsentResponseMutation: mock(() => [
|
|
179
|
+
mock(() => ({unwrap: mock(() => Promise.resolve({}))})),
|
|
180
|
+
{error: undefined, isLoading: false},
|
|
181
|
+
]),
|
|
182
|
+
})),
|
|
183
|
+
})),
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const {getByText} = renderWithTheme(
|
|
187
|
+
<ConsentNavigator api={api as any}>
|
|
188
|
+
<Text>App Content</Text>
|
|
189
|
+
</ConsentNavigator>
|
|
190
|
+
);
|
|
191
|
+
expect(getByText("App Content")).toBeTruthy();
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("renders children when error is auth (403)", () => {
|
|
195
|
+
const api = {
|
|
196
|
+
enhanceEndpoints: mock(() => ({
|
|
197
|
+
injectEndpoints: mock(() => ({
|
|
198
|
+
useGetPendingConsentsQuery: mock(() => ({
|
|
199
|
+
data: undefined,
|
|
200
|
+
error: {originalStatus: 403},
|
|
201
|
+
isLoading: false,
|
|
202
|
+
refetch: mock(() => {}),
|
|
203
|
+
})),
|
|
204
|
+
useSubmitConsentResponseMutation: mock(() => [
|
|
205
|
+
mock(() => ({unwrap: mock(() => Promise.resolve({}))})),
|
|
206
|
+
{error: undefined, isLoading: false},
|
|
207
|
+
]),
|
|
208
|
+
})),
|
|
209
|
+
})),
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const {getByText} = renderWithTheme(
|
|
213
|
+
<ConsentNavigator api={api as any}>
|
|
214
|
+
<Text>App Content</Text>
|
|
215
|
+
</ConsentNavigator>
|
|
216
|
+
);
|
|
217
|
+
expect(getByText("App Content")).toBeTruthy();
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("renders error screen with retry button for non-auth errors", () => {
|
|
221
|
+
const onError = mock(() => {});
|
|
222
|
+
const api = {
|
|
223
|
+
enhanceEndpoints: mock(() => ({
|
|
224
|
+
injectEndpoints: mock(() => ({
|
|
225
|
+
useGetPendingConsentsQuery: mock(() => ({
|
|
226
|
+
data: undefined,
|
|
227
|
+
error: {message: "Server error", status: 500},
|
|
228
|
+
isLoading: false,
|
|
229
|
+
refetch: mock(() => {}),
|
|
230
|
+
})),
|
|
231
|
+
useSubmitConsentResponseMutation: mock(() => [
|
|
232
|
+
mock(() => ({unwrap: mock(() => Promise.resolve({}))})),
|
|
233
|
+
{error: undefined, isLoading: false},
|
|
234
|
+
]),
|
|
235
|
+
})),
|
|
236
|
+
})),
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
const {getByTestId, getByText} = renderWithTheme(
|
|
240
|
+
<ConsentNavigator api={api as any} onError={onError}>
|
|
241
|
+
<Text>App Content</Text>
|
|
242
|
+
</ConsentNavigator>
|
|
243
|
+
);
|
|
244
|
+
expect(getByTestId("consent-navigator-error")).toBeTruthy();
|
|
245
|
+
expect(getByText("Failed to load consent forms")).toBeTruthy();
|
|
246
|
+
expect(onError).toHaveBeenCalled();
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it("renders without error when no extra screens provided", () => {
|
|
250
|
+
const api = createMockApi([]);
|
|
251
|
+
|
|
252
|
+
const {getByText} = renderWithTheme(
|
|
253
|
+
<ConsentNavigator api={api}>
|
|
254
|
+
<Text>App Content</Text>
|
|
255
|
+
</ConsentNavigator>
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
expect(getByText("App Content")).toBeTruthy();
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it("calls submit and refetch when user agrees to a consent form", async () => {
|
|
262
|
+
const {act, fireEvent} = await import("@testing-library/react-native");
|
|
263
|
+
const form = makeForm();
|
|
264
|
+
const unwrap = mock(() => Promise.resolve({agreed: true, id: "r-1"}));
|
|
265
|
+
const submitMutation = mock(() => ({unwrap}));
|
|
266
|
+
const refetch = mock(() => Promise.resolve());
|
|
267
|
+
const api = {
|
|
268
|
+
enhanceEndpoints: mock(() => ({
|
|
269
|
+
injectEndpoints: mock(() => ({
|
|
270
|
+
useGetPendingConsentsQuery: mock(() => ({
|
|
271
|
+
data: {data: [form]},
|
|
272
|
+
error: undefined,
|
|
273
|
+
isLoading: false,
|
|
274
|
+
refetch,
|
|
275
|
+
})),
|
|
276
|
+
useSubmitConsentResponseMutation: mock(() => [
|
|
277
|
+
submitMutation,
|
|
278
|
+
{error: undefined, isLoading: false},
|
|
279
|
+
]),
|
|
280
|
+
})),
|
|
281
|
+
})),
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
const {getByTestId} = renderWithTheme(
|
|
285
|
+
<ConsentNavigator api={api as any}>
|
|
286
|
+
<Text>App Content</Text>
|
|
287
|
+
</ConsentNavigator>
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
await act(async () => {
|
|
291
|
+
fireEvent.press(getByTestId("consent-form-agree-button"));
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
expect(submitMutation).toHaveBeenCalled();
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it("invokes onError when consent submission fails on agree", async () => {
|
|
298
|
+
const {act, fireEvent} = await import("@testing-library/react-native");
|
|
299
|
+
const form = makeForm();
|
|
300
|
+
const onError = mock(() => {});
|
|
301
|
+
const unwrap = mock(() => Promise.reject(new Error("submit failed")));
|
|
302
|
+
const submitMutation = mock(() => ({unwrap}));
|
|
303
|
+
const refetch = mock(() => Promise.resolve());
|
|
304
|
+
const api = {
|
|
305
|
+
enhanceEndpoints: mock(() => ({
|
|
306
|
+
injectEndpoints: mock(() => ({
|
|
307
|
+
useGetPendingConsentsQuery: mock(() => ({
|
|
308
|
+
data: {data: [form]},
|
|
309
|
+
error: undefined,
|
|
310
|
+
isLoading: false,
|
|
311
|
+
refetch,
|
|
312
|
+
})),
|
|
313
|
+
useSubmitConsentResponseMutation: mock(() => [
|
|
314
|
+
submitMutation,
|
|
315
|
+
{error: undefined, isLoading: false},
|
|
316
|
+
]),
|
|
317
|
+
})),
|
|
318
|
+
})),
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
const {getByTestId} = renderWithTheme(
|
|
322
|
+
<ConsentNavigator api={api as any} onError={onError}>
|
|
323
|
+
<Text>App Content</Text>
|
|
324
|
+
</ConsentNavigator>
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
await act(async () => {
|
|
328
|
+
fireEvent.press(getByTestId("consent-form-agree-button"));
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
await act(async () => {
|
|
332
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
expect(onError).toHaveBeenCalled();
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it("calls submit with agreed=false when user declines an optional consent form", async () => {
|
|
339
|
+
const {act, fireEvent} = await import("@testing-library/react-native");
|
|
340
|
+
const form = makeForm({allowDecline: true, required: false});
|
|
341
|
+
const unwrap = mock(() => Promise.resolve({agreed: false, id: "r-2"}));
|
|
342
|
+
const submitMutation = mock(() => ({unwrap}));
|
|
343
|
+
const refetch = mock(() => Promise.resolve());
|
|
344
|
+
const api = {
|
|
345
|
+
enhanceEndpoints: mock(() => ({
|
|
346
|
+
injectEndpoints: mock(() => ({
|
|
347
|
+
useGetPendingConsentsQuery: mock(() => ({
|
|
348
|
+
data: {data: [form]},
|
|
349
|
+
error: undefined,
|
|
350
|
+
isLoading: false,
|
|
351
|
+
refetch,
|
|
352
|
+
})),
|
|
353
|
+
useSubmitConsentResponseMutation: mock(() => [
|
|
354
|
+
submitMutation,
|
|
355
|
+
{error: undefined, isLoading: false},
|
|
356
|
+
]),
|
|
357
|
+
})),
|
|
358
|
+
})),
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
const {getByTestId} = renderWithTheme(
|
|
362
|
+
<ConsentNavigator api={api as any}>
|
|
363
|
+
<Text>App Content</Text>
|
|
364
|
+
</ConsentNavigator>
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
const declineBtn = getByTestId("consent-form-decline-button");
|
|
368
|
+
await act(async () => {
|
|
369
|
+
fireEvent.press(declineBtn);
|
|
370
|
+
});
|
|
371
|
+
expect(submitMutation).toHaveBeenCalled();
|
|
372
|
+
});
|
|
167
373
|
});
|