@terreno/ui 0.14.1 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/dist/ActionSheet.js +15 -27
  2. package/dist/ActionSheet.js.map +1 -1
  3. package/dist/Badge.js +1 -0
  4. package/dist/Badge.js.map +1 -1
  5. package/dist/Banner.d.ts +8 -0
  6. package/dist/Banner.js +2 -2
  7. package/dist/Banner.js.map +1 -1
  8. package/dist/MarkdownView.js +20 -7
  9. package/dist/MarkdownView.js.map +1 -1
  10. package/dist/PickerSelect.js +6 -2
  11. package/dist/PickerSelect.js.map +1 -1
  12. package/dist/Signature.d.ts +8 -1
  13. package/dist/Signature.js +93 -18
  14. package/dist/Signature.js.map +1 -1
  15. package/dist/Signature.native.d.ts +15 -0
  16. package/dist/Signature.native.js +116 -21
  17. package/dist/Signature.native.js.map +1 -1
  18. package/dist/TapToEdit.js +1 -1
  19. package/dist/TapToEdit.js.map +1 -1
  20. package/dist/index.d.ts +1 -1
  21. package/dist/index.js +1 -1
  22. package/dist/index.js.map +1 -1
  23. package/dist/useConsentHistory.d.ts +6 -1
  24. package/dist/useConsentHistory.js +2 -1
  25. package/dist/useConsentHistory.js.map +1 -1
  26. package/package.json +2 -4
  27. package/src/ActionSheet.test.tsx +554 -0
  28. package/src/ActionSheet.tsx +24 -37
  29. package/src/Badge.test.tsx +7 -0
  30. package/src/Badge.tsx +1 -0
  31. package/src/Banner.test.tsx +58 -3
  32. package/src/Banner.tsx +3 -3
  33. package/src/DataTable.test.tsx +176 -1
  34. package/src/DateTimeField.test.tsx +942 -2
  35. package/src/Field.test.tsx +23 -0
  36. package/src/HeightActionSheet.test.tsx +1 -1
  37. package/src/HeightField.test.tsx +35 -0
  38. package/src/HeightFieldDesktop.test.tsx +19 -0
  39. package/src/MarkdownView.test.tsx +28 -0
  40. package/src/MarkdownView.tsx +69 -7
  41. package/src/MobileAddressAutoComplete.test.tsx +6 -2
  42. package/src/PickerSelect.test.tsx +265 -0
  43. package/src/PickerSelect.tsx +24 -8
  44. package/src/Signature.native.tsx +147 -30
  45. package/src/Signature.test.tsx +2 -49
  46. package/src/Signature.tsx +128 -22
  47. package/src/SignatureField.test.tsx +0 -9
  48. package/src/SplitPage.test.tsx +299 -43
  49. package/src/TapToEdit.test.tsx +46 -0
  50. package/src/TapToEdit.tsx +1 -1
  51. package/src/ToastNotifications.test.tsx +748 -1
  52. package/src/Tooltip.test.tsx +707 -1
  53. package/src/WebAddressAutocomplete.test.tsx +99 -0
  54. package/src/WebDropdownMenu.test.tsx +28 -2
  55. package/src/__snapshots__/Banner.test.tsx.snap +125 -0
  56. package/src/__snapshots__/CustomSelectField.test.tsx.snap +5 -4
  57. package/src/__snapshots__/DataTable.test.tsx.snap +366 -0
  58. package/src/__snapshots__/Field.test.tsx.snap +377 -0
  59. package/src/__snapshots__/MarkdownView.test.tsx.snap +284 -74
  60. package/src/__snapshots__/PickerSelect.test.tsx.snap +5 -4
  61. package/src/__snapshots__/SegmentedControl.test.tsx.snap +9 -0
  62. package/src/__snapshots__/SelectField.test.tsx.snap +5 -4
  63. package/src/__snapshots__/Signature.test.tsx.snap +13 -3
  64. package/src/__snapshots__/SignatureField.test.tsx.snap +10 -3
  65. package/src/__snapshots__/SplitPage.test.tsx.snap +698 -46
  66. package/src/bunSetup.ts +0 -19
  67. package/src/index.tsx +1 -1
  68. package/src/login/LoginScreen.test.tsx +12 -0
  69. package/src/useConsentHistory.test.ts +20 -13
  70. package/src/useConsentHistory.ts +7 -2
@@ -1,20 +1,62 @@
1
1
  // biome-ignore-all lint/suspicious/noExplicitAny: test mock typing
2
- import {afterEach, beforeEach, describe, expect, it, type mock} from "bun:test";
2
+ import {
3
+ afterAll,
4
+ afterEach,
5
+ beforeEach,
6
+ describe,
7
+ expect,
8
+ it,
9
+ type mock as MockType,
10
+ mock,
11
+ } from "bun:test";
3
12
  import {act, userEvent} from "@testing-library/react-native";
4
13
  import {DateTime} from "luxon";
5
14
 
6
15
  import {DateTimeField} from "./DateTimeField";
7
16
  import {renderWithTheme, setupComponentTest, teardownComponentTest} from "./test-utils";
8
17
 
18
+ const setDesktop = () => {
19
+ mock.module("./MediaQuery", () => ({
20
+ isMobileDevice: () => false,
21
+ mediaQuery: () => "lg" as const,
22
+ mediaQueryLargerThan: () => true,
23
+ mediaQuerySmallerThan: () => false,
24
+ }));
25
+ };
26
+
27
+ const setMobile = () => {
28
+ mock.module("./MediaQuery", () => ({
29
+ isMobileDevice: () => true,
30
+ mediaQuery: () => "xs" as const,
31
+ mediaQueryLargerThan: () => false,
32
+ mediaQuerySmallerThan: () => true,
33
+ }));
34
+ };
35
+
36
+ // Restore MediaQuery to bunSetup defaults after all tests to prevent cross-file pollution.
37
+ // bunSetup mocks: isMobileDevice → false, mediaQueryLargerThan → false.
38
+ const restoreDefault = () => {
39
+ mock.module("./MediaQuery", () => ({
40
+ isMobileDevice: mock(() => false),
41
+ mediaQueryLargerThan: mock(() => false),
42
+ }));
43
+ };
44
+
45
+ afterAll(() => {
46
+ restoreDefault();
47
+ });
48
+
9
49
  describe("DateTimeField", () => {
10
- let mockOnChange: ReturnType<typeof mock>;
50
+ let mockOnChange: ReturnType<MockType>;
11
51
 
12
52
  beforeEach(() => {
53
+ setDesktop();
13
54
  const mocks = setupComponentTest();
14
55
  mockOnChange = mocks.onChange;
15
56
  });
16
57
 
17
58
  afterEach(() => {
59
+ setDesktop();
18
60
  teardownComponentTest();
19
61
  });
20
62
 
@@ -391,4 +433,902 @@ describe("DateTimeField", () => {
391
433
  expect(getByPlaceholderText("YYYY").props.value).toBe("2023");
392
434
  });
393
435
  });
436
+
437
+ describe("empty and invalid value handling", () => {
438
+ it("should clear all fields when value is empty", () => {
439
+ const {getByPlaceholderText, rerender} = renderWithTheme(
440
+ <DateTimeField onChange={mockOnChange} type="date" value="2023-05-15T00:00:00.000Z" />
441
+ );
442
+ expect(getByPlaceholderText("MM").props.value).toBe("05");
443
+
444
+ rerender(<DateTimeField onChange={mockOnChange} type="date" value="" />);
445
+ expect(getByPlaceholderText("MM").props.value).toBe("");
446
+ expect(getByPlaceholderText("DD").props.value).toBe("");
447
+ expect(getByPlaceholderText("YYYY").props.value).toBe("");
448
+ });
449
+
450
+ it("should clear all fields when value is undefined", () => {
451
+ const {getByPlaceholderText, rerender} = renderWithTheme(
452
+ <DateTimeField onChange={mockOnChange} type="datetime" value="2023-05-15T15:30:00.000Z" />
453
+ );
454
+ rerender(<DateTimeField onChange={mockOnChange} type="datetime" value={undefined} />);
455
+ expect(getByPlaceholderText("MM").props.value).toBe("");
456
+ expect(getByPlaceholderText("hh").props.value).toBe("");
457
+ expect(getByPlaceholderText("mm").props.value).toBe("");
458
+ });
459
+ });
460
+
461
+ describe("time type field validation", () => {
462
+ it("should validate hour fields for time type", async () => {
463
+ const user = userEvent.setup();
464
+ const {getByPlaceholderText} = renderWithTheme(
465
+ <DateTimeField
466
+ onChange={mockOnChange}
467
+ timezone="America/New_York"
468
+ type="time"
469
+ value="2023-05-15T15:30:00.000Z"
470
+ />
471
+ );
472
+ const hourInput = getByPlaceholderText("hh");
473
+ await user.clear(hourInput);
474
+ await user.type(hourInput, "13");
475
+ await act(async () => {
476
+ await new Promise((resolve) => setTimeout(resolve, 100));
477
+ });
478
+ expect(hourInput).toBeTruthy();
479
+ });
480
+
481
+ it("should validate year field correctly", async () => {
482
+ const user = userEvent.setup();
483
+ const {getByPlaceholderText} = renderWithTheme(
484
+ <DateTimeField onChange={mockOnChange} type="date" value="2023-05-15T00:00:00.000Z" />
485
+ );
486
+ const yearInput = getByPlaceholderText("YYYY");
487
+ await user.clear(yearInput);
488
+ await user.type(yearInput, "1800");
489
+ await act(async () => {
490
+ await new Promise((resolve) => setTimeout(resolve, 100));
491
+ });
492
+ expect(yearInput).toBeTruthy();
493
+ });
494
+
495
+ it("should validate day out of range", async () => {
496
+ const user = userEvent.setup();
497
+ const {getByPlaceholderText} = renderWithTheme(
498
+ <DateTimeField onChange={mockOnChange} type="date" value="2023-05-15T00:00:00.000Z" />
499
+ );
500
+ const dayInput = getByPlaceholderText("DD");
501
+ await user.clear(dayInput);
502
+ await user.type(dayInput, "32");
503
+ await act(async () => {
504
+ await new Promise((resolve) => setTimeout(resolve, 100));
505
+ });
506
+ expect(dayInput).toBeTruthy();
507
+ });
508
+ });
509
+
510
+ describe("datetime type interactions", () => {
511
+ it("should render datetime with all segments on desktop", () => {
512
+ const {getByPlaceholderText} = renderWithTheme(
513
+ <DateTimeField
514
+ onChange={mockOnChange}
515
+ timezone="America/New_York"
516
+ type="datetime"
517
+ value="2023-05-15T15:30:00.000Z"
518
+ />
519
+ );
520
+ expect(getByPlaceholderText("MM").props.value).toBe("05");
521
+ expect(getByPlaceholderText("DD").props.value).toBe("15");
522
+ expect(getByPlaceholderText("YYYY").props.value).toBe("2023");
523
+ expect(getByPlaceholderText("hh").props.value).toBe("11");
524
+ expect(getByPlaceholderText("mm").props.value).toBe("30");
525
+ });
526
+
527
+ it("should handle hour change in datetime mode", async () => {
528
+ const user = userEvent.setup();
529
+ const {getByPlaceholderText} = renderWithTheme(
530
+ <DateTimeField
531
+ onChange={mockOnChange}
532
+ timezone="America/New_York"
533
+ type="datetime"
534
+ value="2023-05-15T15:30:00.000Z"
535
+ />
536
+ );
537
+ const hourInput = getByPlaceholderText("hh");
538
+ await user.clear(hourInput);
539
+ await user.type(hourInput, "3");
540
+ await act(async () => {
541
+ await new Promise((resolve) => setTimeout(resolve, 100));
542
+ });
543
+ expect(hourInput).toBeTruthy();
544
+ });
545
+
546
+ it("should handle minute change in datetime mode", async () => {
547
+ const user = userEvent.setup();
548
+ const {getByPlaceholderText} = renderWithTheme(
549
+ <DateTimeField
550
+ onChange={mockOnChange}
551
+ timezone="America/New_York"
552
+ type="datetime"
553
+ value="2023-05-15T15:30:00.000Z"
554
+ />
555
+ );
556
+ const minuteInput = getByPlaceholderText("mm");
557
+ await user.clear(minuteInput);
558
+ await user.type(minuteInput, "45");
559
+ await act(async () => {
560
+ await new Promise((resolve) => setTimeout(resolve, 100));
561
+ });
562
+ expect(mockOnChange).toHaveBeenCalled();
563
+ });
564
+ });
565
+
566
+ describe("disabled state", () => {
567
+ it("should render disabled date field", () => {
568
+ const {getByPlaceholderText} = renderWithTheme(
569
+ <DateTimeField
570
+ disabled
571
+ onChange={mockOnChange}
572
+ type="date"
573
+ value="2023-05-15T00:00:00.000Z"
574
+ />
575
+ );
576
+ expect(getByPlaceholderText("MM").props.readOnly).toBe(true);
577
+ });
578
+
579
+ it("should not render action sheet when disabled", () => {
580
+ const {queryByAccessibilityHint} = renderWithTheme(
581
+ <DateTimeField
582
+ disabled
583
+ onChange={mockOnChange}
584
+ type="datetime"
585
+ value="2023-05-15T15:30:00.000Z"
586
+ />
587
+ );
588
+ expect(queryByAccessibilityHint("Opens the calendar to select a date and time")).toBeNull();
589
+ });
590
+ });
591
+
592
+ describe("title, error, and helper text", () => {
593
+ it("should render title", () => {
594
+ const {getByText} = renderWithTheme(
595
+ <DateTimeField
596
+ onChange={mockOnChange}
597
+ title="Pick a date"
598
+ type="date"
599
+ value="2023-05-15T00:00:00.000Z"
600
+ />
601
+ );
602
+ expect(getByText("Pick a date")).toBeTruthy();
603
+ });
604
+
605
+ it("should render error text", () => {
606
+ const {getByText} = renderWithTheme(
607
+ <DateTimeField
608
+ errorText="Required field"
609
+ onChange={mockOnChange}
610
+ type="date"
611
+ value="2023-05-15T00:00:00.000Z"
612
+ />
613
+ );
614
+ expect(getByText("Required field")).toBeTruthy();
615
+ });
616
+
617
+ it("should render helper text", () => {
618
+ const {getByText} = renderWithTheme(
619
+ <DateTimeField
620
+ helperText="Select a date"
621
+ onChange={mockOnChange}
622
+ type="date"
623
+ value="2023-05-15T00:00:00.000Z"
624
+ />
625
+ );
626
+ expect(getByText("Select a date")).toBeTruthy();
627
+ });
628
+ });
629
+
630
+ describe("getFieldValue edge cases", () => {
631
+ it("should return empty string for unrecognized index in time mode", () => {
632
+ const {getByPlaceholderText} = renderWithTheme(
633
+ <DateTimeField
634
+ onChange={mockOnChange}
635
+ timezone="America/New_York"
636
+ type="time"
637
+ value="2023-05-15T15:30:00.000Z"
638
+ />
639
+ );
640
+ expect(getByPlaceholderText("hh").props.value).toBe("11");
641
+ expect(getByPlaceholderText("mm").props.value).toBe("30");
642
+ });
643
+ });
644
+
645
+ describe("getISOFromFields edge cases", () => {
646
+ it("should return undefined when time fields are incomplete for time type", () => {
647
+ const {getByPlaceholderText} = renderWithTheme(
648
+ <DateTimeField onChange={mockOnChange} timezone="America/New_York" type="time" value="" />
649
+ );
650
+ expect(getByPlaceholderText("hh").props.value).toBe("");
651
+ });
652
+
653
+ it("should handle 12pm correctly in time type", async () => {
654
+ const user = userEvent.setup();
655
+ const {getByPlaceholderText} = renderWithTheme(
656
+ <DateTimeField
657
+ onChange={mockOnChange}
658
+ timezone="America/New_York"
659
+ type="time"
660
+ value="2023-05-15T12:00:00.000Z"
661
+ />
662
+ );
663
+ const minuteInput = getByPlaceholderText("mm");
664
+ await user.clear(minuteInput);
665
+ await user.type(minuteInput, "15");
666
+ await act(async () => {
667
+ await new Promise((resolve) => setTimeout(resolve, 100));
668
+ });
669
+ expect(mockOnChange).toHaveBeenCalled();
670
+ });
671
+
672
+ it("should handle 12am correctly in time type", () => {
673
+ const {getByPlaceholderText} = renderWithTheme(
674
+ <DateTimeField
675
+ onChange={mockOnChange}
676
+ timezone="America/New_York"
677
+ type="time"
678
+ value="2023-05-15T04:00:00.000Z"
679
+ />
680
+ );
681
+ expect(getByPlaceholderText("hh").props.value).toBe("12");
682
+ });
683
+ });
684
+
685
+ describe("onTimezoneChange callback", () => {
686
+ it("should call onTimezoneChange when provided", () => {
687
+ const mockTzChange = mock(() => {});
688
+ renderWithTheme(
689
+ <DateTimeField
690
+ onChange={mockOnChange}
691
+ onTimezoneChange={mockTzChange}
692
+ timezone="America/New_York"
693
+ type="time"
694
+ value="2023-05-15T15:30:00.000Z"
695
+ />
696
+ );
697
+ expect(mockTzChange).toBeDefined();
698
+ });
699
+ });
700
+
701
+ describe("mobile time display", () => {
702
+ it("should render MobileTimeDisplay on mobile with time type", () => {
703
+ setMobile();
704
+ const {getByAccessibilityHint} = renderWithTheme(
705
+ <DateTimeField
706
+ onChange={mockOnChange}
707
+ timezone="America/New_York"
708
+ type="time"
709
+ value="2023-05-15T15:30:00.000Z"
710
+ />
711
+ );
712
+ expect(getByAccessibilityHint("Tap to select a time")).toBeTruthy();
713
+ });
714
+
715
+ it("should render disabled MobileTimeDisplay", () => {
716
+ setMobile();
717
+ const {getByAccessibilityHint} = renderWithTheme(
718
+ <DateTimeField
719
+ disabled
720
+ onChange={mockOnChange}
721
+ timezone="America/New_York"
722
+ type="time"
723
+ value="2023-05-15T15:30:00.000Z"
724
+ />
725
+ );
726
+ expect(getByAccessibilityHint("Tap to select a time")).toBeTruthy();
727
+ });
728
+
729
+ it("should render MobileTimeDisplay placeholder when no value", () => {
730
+ setMobile();
731
+ const {getByAccessibilityHint} = renderWithTheme(
732
+ <DateTimeField onChange={mockOnChange} timezone="America/New_York" type="time" value="" />
733
+ );
734
+ expect(getByAccessibilityHint("Tap to select a time")).toBeTruthy();
735
+ });
736
+
737
+ it("should render mobile datetime with time display", () => {
738
+ setMobile();
739
+ const {getByAccessibilityHint} = renderWithTheme(
740
+ <DateTimeField
741
+ onChange={mockOnChange}
742
+ timezone="America/New_York"
743
+ type="datetime"
744
+ value="2023-05-15T15:30:00.000Z"
745
+ />
746
+ );
747
+ expect(getByAccessibilityHint("Opens date and time picker")).toBeTruthy();
748
+ });
749
+ });
750
+
751
+ describe("onActionSheetChange", () => {
752
+ it("should handle action sheet date selection", async () => {
753
+ setDesktop();
754
+ const {UNSAFE_root} = renderWithTheme(
755
+ <DateTimeField
756
+ onChange={mockOnChange}
757
+ timezone="America/New_York"
758
+ type="date"
759
+ value="2023-05-15T00:00:00.000Z"
760
+ />
761
+ );
762
+
763
+ const actionSheet = UNSAFE_root.findAll(
764
+ (n: any) => n.props?.onChange && n.props?.visible !== undefined
765
+ );
766
+ expect(actionSheet.length).toBeGreaterThan(0);
767
+ await act(async () => {
768
+ actionSheet[0].props.onChange("2023-06-20T00:00:00.000Z");
769
+ });
770
+ expect(mockOnChange).toHaveBeenCalled();
771
+ });
772
+
773
+ it("should handle action sheet clear (empty string)", async () => {
774
+ setDesktop();
775
+ const {UNSAFE_root} = renderWithTheme(
776
+ <DateTimeField
777
+ onChange={mockOnChange}
778
+ timezone="America/New_York"
779
+ type="date"
780
+ value="2023-05-15T00:00:00.000Z"
781
+ />
782
+ );
783
+
784
+ const actionSheet = UNSAFE_root.findAll(
785
+ (n: any) => n.props?.onChange && n.props?.visible !== undefined
786
+ );
787
+ expect(actionSheet.length).toBeGreaterThan(0);
788
+ await act(async () => {
789
+ actionSheet[0].props.onChange("");
790
+ });
791
+ expect(mockOnChange).toHaveBeenCalledWith("");
792
+ });
793
+
794
+ it("should handle action sheet time selection", async () => {
795
+ setDesktop();
796
+ const {UNSAFE_root} = renderWithTheme(
797
+ <DateTimeField
798
+ onChange={mockOnChange}
799
+ timezone="America/New_York"
800
+ type="time"
801
+ value="2023-05-15T15:30:00.000Z"
802
+ />
803
+ );
804
+
805
+ const actionSheet = UNSAFE_root.findAll(
806
+ (n: any) => n.props?.onChange && n.props?.visible !== undefined
807
+ );
808
+ expect(actionSheet.length).toBeGreaterThan(0);
809
+ await act(async () => {
810
+ actionSheet[0].props.onChange("2023-05-15T18:45:00.000Z");
811
+ });
812
+ expect(mockOnChange).toHaveBeenCalled();
813
+ });
814
+
815
+ it("should handle action sheet datetime selection", async () => {
816
+ setDesktop();
817
+ const {UNSAFE_root} = renderWithTheme(
818
+ <DateTimeField
819
+ onChange={mockOnChange}
820
+ timezone="America/New_York"
821
+ type="datetime"
822
+ value="2023-05-15T15:30:00.000Z"
823
+ />
824
+ );
825
+
826
+ const actionSheet = UNSAFE_root.findAll(
827
+ (n: any) => n.props?.onChange && n.props?.visible !== undefined
828
+ );
829
+ expect(actionSheet.length).toBeGreaterThan(0);
830
+ await act(async () => {
831
+ actionSheet[0].props.onChange("2023-06-20T10:00:00.000Z");
832
+ });
833
+ expect(mockOnChange).toHaveBeenCalled();
834
+ });
835
+ });
836
+
837
+ describe("handleAmPmChange", () => {
838
+ it("should toggle AM to PM and emit new value", async () => {
839
+ setDesktop();
840
+ // 15:30 UTC = 11:30 AM in New York => toggling to PM should change the time
841
+ const {UNSAFE_root} = renderWithTheme(
842
+ <DateTimeField
843
+ onChange={mockOnChange}
844
+ timezone="America/New_York"
845
+ type="time"
846
+ value="2023-05-15T15:30:00.000Z"
847
+ />
848
+ );
849
+
850
+ const amPmSelects = UNSAFE_root.findAll((n: any) => n.props?.onAmPmChange);
851
+ expect(amPmSelects.length).toBeGreaterThan(0);
852
+ await act(async () => {
853
+ amPmSelects[0].props.onAmPmChange("pm");
854
+ });
855
+ expect(mockOnChange).toHaveBeenCalled();
856
+ });
857
+ });
858
+
859
+ describe("handleTimezoneChange", () => {
860
+ it("should call onTimezoneChange callback and emit new value", async () => {
861
+ setDesktop();
862
+ const mockTzChange = mock(() => {});
863
+ const {UNSAFE_root} = renderWithTheme(
864
+ <DateTimeField
865
+ onChange={mockOnChange}
866
+ onTimezoneChange={mockTzChange}
867
+ timezone="America/New_York"
868
+ type="time"
869
+ value="2023-05-15T15:30:00.000Z"
870
+ />
871
+ );
872
+
873
+ const tzPickers = UNSAFE_root.findAll((n: any) => n.props?.onTimezoneChange);
874
+ expect(tzPickers.length).toBeGreaterThan(0);
875
+ await act(async () => {
876
+ tzPickers[0].props.onTimezoneChange("America/Chicago");
877
+ });
878
+ expect(mockTzChange).toHaveBeenCalledWith("America/Chicago");
879
+ });
880
+
881
+ it("should use local timezone state when onTimezoneChange not provided", async () => {
882
+ setDesktop();
883
+ const {UNSAFE_root} = renderWithTheme(
884
+ <DateTimeField
885
+ onChange={mockOnChange}
886
+ timezone="America/New_York"
887
+ type="time"
888
+ value="2023-05-15T15:30:00.000Z"
889
+ />
890
+ );
891
+
892
+ const tzPickers = UNSAFE_root.findAll((n: any) => n.props?.onTimezoneChange);
893
+ expect(tzPickers.length).toBeGreaterThan(0);
894
+ await act(async () => {
895
+ tzPickers[0].props.onTimezoneChange("America/Chicago");
896
+ });
897
+ expect(mockOnChange).toHaveBeenCalled();
898
+ });
899
+ });
900
+
901
+ describe("minute validation", () => {
902
+ it("should validate invalid minute in time mode", async () => {
903
+ const user = userEvent.setup();
904
+ const {getByPlaceholderText} = renderWithTheme(
905
+ <DateTimeField
906
+ onChange={mockOnChange}
907
+ timezone="America/New_York"
908
+ type="time"
909
+ value="2023-05-15T15:30:00.000Z"
910
+ />
911
+ );
912
+ const minuteInput = getByPlaceholderText("mm");
913
+ await user.clear(minuteInput);
914
+ await user.type(minuteInput, "99");
915
+ await act(async () => {
916
+ await new Promise((resolve) => setTimeout(resolve, 100));
917
+ });
918
+ expect(minuteInput).toBeTruthy();
919
+ });
920
+ });
921
+
922
+ describe("getISOFromFields datetime am/pm", () => {
923
+ it("should handle pm in datetime mode", async () => {
924
+ const user = userEvent.setup();
925
+ const {getByPlaceholderText} = renderWithTheme(
926
+ <DateTimeField
927
+ onChange={mockOnChange}
928
+ timezone="America/New_York"
929
+ type="datetime"
930
+ value="2023-05-15T20:30:00.000Z"
931
+ />
932
+ );
933
+ const minuteInput = getByPlaceholderText("mm");
934
+ await user.clear(minuteInput);
935
+ await user.type(minuteInput, "45");
936
+ await act(async () => {
937
+ await new Promise((resolve) => setTimeout(resolve, 100));
938
+ });
939
+ expect(mockOnChange).toHaveBeenCalled();
940
+ });
941
+
942
+ it("should handle 12am in datetime mode", async () => {
943
+ const user = userEvent.setup();
944
+ const {getByPlaceholderText} = renderWithTheme(
945
+ <DateTimeField
946
+ onChange={mockOnChange}
947
+ timezone="America/New_York"
948
+ type="datetime"
949
+ value="2023-05-15T04:30:00.000Z"
950
+ />
951
+ );
952
+ const minuteInput = getByPlaceholderText("mm");
953
+ await user.clear(minuteInput);
954
+ await user.type(minuteInput, "15");
955
+ await act(async () => {
956
+ await new Promise((resolve) => setTimeout(resolve, 100));
957
+ });
958
+ expect(mockOnChange).toHaveBeenCalled();
959
+ });
960
+ });
961
+
962
+ describe("onBlur edge cases", () => {
963
+ it("should handle onBlur with AM/PM override in time mode", async () => {
964
+ setDesktop();
965
+ const user = userEvent.setup();
966
+ const {getByPlaceholderText} = renderWithTheme(
967
+ <DateTimeField
968
+ onChange={mockOnChange}
969
+ timezone="America/New_York"
970
+ type="time"
971
+ value="2023-05-15T15:30:00.000Z"
972
+ />
973
+ );
974
+ const hourInput = getByPlaceholderText("hh");
975
+ await user.clear(hourInput);
976
+ await user.type(hourInput, "5");
977
+ await act(async () => {
978
+ await new Promise((resolve) => setTimeout(resolve, 100));
979
+ });
980
+ expect(hourInput).toBeTruthy();
981
+ });
982
+ });
983
+
984
+ describe("onDismiss and onLayout", () => {
985
+ it("should handle DateTimeActionSheet onDismiss", async () => {
986
+ setDesktop();
987
+ const {UNSAFE_root} = renderWithTheme(
988
+ <DateTimeField
989
+ onChange={mockOnChange}
990
+ timezone="America/New_York"
991
+ type="date"
992
+ value="2023-05-15T00:00:00.000Z"
993
+ />
994
+ );
995
+
996
+ const actionSheet = UNSAFE_root.findAll(
997
+ (n: any) => n.props?.onDismiss && n.props?.visible !== undefined
998
+ );
999
+ expect(actionSheet.length).toBeGreaterThan(0);
1000
+ await act(async () => {
1001
+ actionSheet[0].props.onDismiss();
1002
+ });
1003
+ expect(UNSAFE_root).toBeTruthy();
1004
+ });
1005
+
1006
+ it("should handle Pressable onLayout", async () => {
1007
+ setDesktop();
1008
+ const {UNSAFE_root} = renderWithTheme(
1009
+ <DateTimeField
1010
+ onChange={mockOnChange}
1011
+ timezone="America/New_York"
1012
+ type="date"
1013
+ value="2023-05-15T00:00:00.000Z"
1014
+ />
1015
+ );
1016
+
1017
+ const pressables = UNSAFE_root.findAll((n: any) => n.props?.onLayout);
1018
+ expect(pressables.length).toBeGreaterThan(0);
1019
+ await act(async () => {
1020
+ pressables[0].props.onLayout({nativeEvent: {layout: {width: 500}}});
1021
+ });
1022
+ expect(UNSAFE_root).toBeTruthy();
1023
+ });
1024
+ });
1025
+
1026
+ describe("SelectField AM/PM onChange inline callback", () => {
1027
+ it("should trigger SelectField onChange to call onAmPmChange", async () => {
1028
+ setDesktop();
1029
+ const {UNSAFE_root} = renderWithTheme(
1030
+ <DateTimeField
1031
+ onChange={mockOnChange}
1032
+ timezone="America/New_York"
1033
+ type="time"
1034
+ value="2023-05-15T15:30:00.000Z"
1035
+ />
1036
+ );
1037
+ const selects = UNSAFE_root.findAll((n: any) => {
1038
+ const opts = n.props?.options;
1039
+ return (
1040
+ Array.isArray(opts) &&
1041
+ opts.some((o: {value?: string}) => o?.value === "am" || o?.value === "pm")
1042
+ );
1043
+ });
1044
+ expect(selects.length).toBeGreaterThan(0);
1045
+ expect(selects[0].props.onChange).toBeDefined();
1046
+ await act(async () => {
1047
+ selects[0].props.onChange("pm");
1048
+ });
1049
+ expect(mockOnChange).toHaveBeenCalled();
1050
+ });
1051
+ });
1052
+
1053
+ describe("inputRef onRef callback", () => {
1054
+ it("should set ref when segment renders", () => {
1055
+ setDesktop();
1056
+ const {getByPlaceholderText} = renderWithTheme(
1057
+ <DateTimeField
1058
+ onChange={mockOnChange}
1059
+ timezone="America/New_York"
1060
+ type="time"
1061
+ value="2023-05-15T15:30:00.000Z"
1062
+ />
1063
+ );
1064
+ expect(getByPlaceholderText("hh")).toBeTruthy();
1065
+ expect(getByPlaceholderText("mm")).toBeTruthy();
1066
+ });
1067
+ });
1068
+
1069
+ describe("datetime type date-only change", () => {
1070
+ it("should handle changing date in datetime mode without changing time", async () => {
1071
+ const user = userEvent.setup();
1072
+ const {getByPlaceholderText} = renderWithTheme(
1073
+ <DateTimeField
1074
+ onChange={mockOnChange}
1075
+ timezone="America/New_York"
1076
+ type="datetime"
1077
+ value="2023-05-15T15:30:00.000Z"
1078
+ />
1079
+ );
1080
+ const dayInput = getByPlaceholderText("DD");
1081
+ await user.clear(dayInput);
1082
+ await user.type(dayInput, "20");
1083
+ await act(async () => {
1084
+ await new Promise((resolve) => setTimeout(resolve, 100));
1085
+ });
1086
+ expect(mockOnChange).toHaveBeenCalled();
1087
+ });
1088
+
1089
+ it("should handle changing year in datetime mode", async () => {
1090
+ const user = userEvent.setup();
1091
+ const {getByPlaceholderText} = renderWithTheme(
1092
+ <DateTimeField
1093
+ onChange={mockOnChange}
1094
+ timezone="America/New_York"
1095
+ type="datetime"
1096
+ value="2023-05-15T15:30:00.000Z"
1097
+ />
1098
+ );
1099
+ const yearInput = getByPlaceholderText("YYYY");
1100
+ await user.clear(yearInput);
1101
+ await user.type(yearInput, "2024");
1102
+ await act(async () => {
1103
+ await new Promise((resolve) => setTimeout(resolve, 100));
1104
+ });
1105
+ expect(mockOnChange).toHaveBeenCalled();
1106
+ });
1107
+ });
1108
+
1109
+ describe("12 AM handling in time type (getISOFromFields)", () => {
1110
+ it("should convert hour 12 AM to 0 in time type", async () => {
1111
+ setDesktop();
1112
+ const user = userEvent.setup();
1113
+ // 04:00 UTC = 00:00 (12:00 AM) in America/New_York
1114
+ const {getByPlaceholderText} = renderWithTheme(
1115
+ <DateTimeField
1116
+ onChange={mockOnChange}
1117
+ timezone="America/New_York"
1118
+ type="time"
1119
+ value="2023-05-15T04:00:00.000Z"
1120
+ />
1121
+ );
1122
+ expect(getByPlaceholderText("hh").props.value).toBe("12");
1123
+
1124
+ const minuteInput = getByPlaceholderText("mm");
1125
+ await user.clear(minuteInput);
1126
+ await user.type(minuteInput, "15");
1127
+ await act(async () => {
1128
+ await new Promise((resolve) => setTimeout(resolve, 100));
1129
+ });
1130
+ expect(mockOnChange).toHaveBeenCalled();
1131
+ const lastCall = mockOnChange.mock.calls[mockOnChange.mock.calls.length - 1][0];
1132
+ const parsed = DateTime.fromISO(lastCall).setZone("America/New_York");
1133
+ expect(parsed.hour).toBe(0);
1134
+ expect(parsed.minute).toBe(15);
1135
+ });
1136
+
1137
+ it("should convert hour 12 AM to 0 in datetime type", async () => {
1138
+ setDesktop();
1139
+ const user = userEvent.setup();
1140
+ // 04:30 UTC = 00:30 (12:30 AM) in America/New_York
1141
+ const {getByPlaceholderText} = renderWithTheme(
1142
+ <DateTimeField
1143
+ onChange={mockOnChange}
1144
+ timezone="America/New_York"
1145
+ type="datetime"
1146
+ value="2023-05-15T04:30:00.000Z"
1147
+ />
1148
+ );
1149
+ expect(getByPlaceholderText("hh").props.value).toBe("12");
1150
+
1151
+ const minuteInput = getByPlaceholderText("mm");
1152
+ await user.clear(minuteInput);
1153
+ await user.type(minuteInput, "45");
1154
+ await act(async () => {
1155
+ await new Promise((resolve) => setTimeout(resolve, 100));
1156
+ });
1157
+ expect(mockOnChange).toHaveBeenCalled();
1158
+ const lastCall = mockOnChange.mock.calls[mockOnChange.mock.calls.length - 1][0];
1159
+ const parsed = DateTime.fromISO(lastCall).setZone("America/New_York");
1160
+ expect(parsed.hour).toBe(0);
1161
+ });
1162
+ });
1163
+
1164
+ describe("onActionSheetChange invalid date handling", () => {
1165
+ it("should warn and return early for invalid ISO string", async () => {
1166
+ setDesktop();
1167
+ const warnSpy = mock(() => {});
1168
+ const originalWarn = console.warn;
1169
+ console.warn = warnSpy;
1170
+
1171
+ const {UNSAFE_root} = renderWithTheme(
1172
+ <DateTimeField
1173
+ onChange={mockOnChange}
1174
+ timezone="America/New_York"
1175
+ type="date"
1176
+ value="2023-05-15T00:00:00.000Z"
1177
+ />
1178
+ );
1179
+
1180
+ mockOnChange.mockClear();
1181
+ const actionSheet = UNSAFE_root.findAll(
1182
+ (n: any) => n.props?.onChange && n.props?.visible !== undefined
1183
+ );
1184
+ expect(actionSheet.length).toBeGreaterThan(0);
1185
+ await act(async () => {
1186
+ actionSheet[0].props.onChange("not-a-valid-date");
1187
+ });
1188
+ expect(warnSpy).toHaveBeenCalledWith(
1189
+ "Invalid date passed to DateTimeField",
1190
+ "not-a-valid-date"
1191
+ );
1192
+ expect(mockOnChange).not.toHaveBeenCalled();
1193
+
1194
+ console.warn = originalWarn;
1195
+ });
1196
+ });
1197
+
1198
+ describe("useEffect invalid value handling", () => {
1199
+ it("should warn and return early for invalid non-empty value prop", () => {
1200
+ const warnSpy = mock(() => {});
1201
+ const originalWarn = console.warn;
1202
+ console.warn = warnSpy;
1203
+
1204
+ const {getByPlaceholderText} = renderWithTheme(
1205
+ <DateTimeField onChange={mockOnChange} type="date" value="invalid-date-string" />
1206
+ );
1207
+
1208
+ expect(warnSpy).toHaveBeenCalledWith(
1209
+ "Invalid date passed to DateTimeField",
1210
+ "invalid-date-string"
1211
+ );
1212
+ expect(getByPlaceholderText("MM").props.value).toBe("");
1213
+
1214
+ console.warn = originalWarn;
1215
+ });
1216
+
1217
+ it("should warn for invalid value in time type", () => {
1218
+ const warnSpy = mock(() => {});
1219
+ const originalWarn = console.warn;
1220
+ console.warn = warnSpy;
1221
+
1222
+ renderWithTheme(
1223
+ <DateTimeField
1224
+ onChange={mockOnChange}
1225
+ timezone="America/New_York"
1226
+ type="time"
1227
+ value="not-valid"
1228
+ />
1229
+ );
1230
+
1231
+ expect(warnSpy).toHaveBeenCalledWith("Invalid date passed to DateTimeField", "not-valid");
1232
+
1233
+ console.warn = originalWarn;
1234
+ });
1235
+ });
1236
+
1237
+ describe("getFieldValue datetime hour/minute indices", () => {
1238
+ it("should return hour and minute for datetime indices 3 and 4", () => {
1239
+ setDesktop();
1240
+ // 20:30 UTC = 4:30 PM in America/New_York
1241
+ const {getByPlaceholderText} = renderWithTheme(
1242
+ <DateTimeField
1243
+ onChange={mockOnChange}
1244
+ timezone="America/New_York"
1245
+ type="datetime"
1246
+ value="2023-05-15T20:30:00.000Z"
1247
+ />
1248
+ );
1249
+ // Indices 0-2 are date fields, indices 3-4 are hour/minute
1250
+ expect(getByPlaceholderText("hh").props.value).toBe("04");
1251
+ expect(getByPlaceholderText("mm").props.value).toBe("30");
1252
+ });
1253
+
1254
+ it("should return hour and minute for datetime at midnight", () => {
1255
+ setDesktop();
1256
+ // 04:00 UTC = 00:00 (12:00 AM) in America/New_York
1257
+ const {getByPlaceholderText} = renderWithTheme(
1258
+ <DateTimeField
1259
+ onChange={mockOnChange}
1260
+ timezone="America/New_York"
1261
+ type="datetime"
1262
+ value="2023-05-15T04:00:00.000Z"
1263
+ />
1264
+ );
1265
+ expect(getByPlaceholderText("hh").props.value).toBe("12");
1266
+ expect(getByPlaceholderText("mm").props.value).toBe("00");
1267
+ });
1268
+ });
1269
+
1270
+ describe("handleTimezoneChange branches", () => {
1271
+ it("should call onTimezoneChange when provided for datetime type", async () => {
1272
+ setDesktop();
1273
+ const mockTzChange = mock(() => {});
1274
+ const {UNSAFE_root} = renderWithTheme(
1275
+ <DateTimeField
1276
+ onChange={mockOnChange}
1277
+ onTimezoneChange={mockTzChange}
1278
+ timezone="America/New_York"
1279
+ type="datetime"
1280
+ value="2023-05-15T15:30:00.000Z"
1281
+ />
1282
+ );
1283
+
1284
+ const tzPickers = UNSAFE_root.findAll((n: any) => n.props?.onTimezoneChange);
1285
+ expect(tzPickers.length).toBeGreaterThan(0);
1286
+ await act(async () => {
1287
+ tzPickers[0].props.onTimezoneChange("America/Chicago");
1288
+ });
1289
+ expect(mockTzChange).toHaveBeenCalledWith("America/Chicago");
1290
+ });
1291
+
1292
+ it("should set local timezone when onTimezoneChange not provided for datetime type", async () => {
1293
+ setDesktop();
1294
+ const {UNSAFE_root} = renderWithTheme(
1295
+ <DateTimeField
1296
+ onChange={mockOnChange}
1297
+ timezone="America/New_York"
1298
+ type="datetime"
1299
+ value="2023-05-15T15:30:00.000Z"
1300
+ />
1301
+ );
1302
+
1303
+ const tzPickers = UNSAFE_root.findAll((n: any) => n.props?.onTimezoneChange);
1304
+ expect(tzPickers.length).toBeGreaterThan(0);
1305
+ await act(async () => {
1306
+ tzPickers[0].props.onTimezoneChange("America/Chicago");
1307
+ });
1308
+ expect(mockOnChange).toHaveBeenCalled();
1309
+ });
1310
+ });
1311
+
1312
+ describe("minute validation in validateField", () => {
1313
+ it("should validate minute field for datetime type via hour change triggering revalidation", async () => {
1314
+ setDesktop();
1315
+ const user = userEvent.setup();
1316
+ const {getByPlaceholderText} = renderWithTheme(
1317
+ <DateTimeField
1318
+ onChange={mockOnChange}
1319
+ timezone="America/New_York"
1320
+ type="datetime"
1321
+ value="2023-05-15T15:30:00.000Z"
1322
+ />
1323
+ );
1324
+ // Type an invalid hour (triggers validateField for datetime index 3)
1325
+ const hourInput = getByPlaceholderText("hh");
1326
+ await user.clear(hourInput);
1327
+ await user.type(hourInput, "0");
1328
+ await act(async () => {
1329
+ await new Promise((resolve) => setTimeout(resolve, 100));
1330
+ });
1331
+ expect(hourInput).toBeTruthy();
1332
+ });
1333
+ });
394
1334
  });