@terreno/ui 0.14.0 → 0.14.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/dist/ActionSheet.d.ts +1 -1
  2. package/dist/ActionSheet.js +17 -29
  3. package/dist/ActionSheet.js.map +1 -1
  4. package/dist/Common.d.ts +8 -2
  5. package/dist/Common.js +4 -4
  6. package/dist/Common.js.map +1 -1
  7. package/dist/ConsentFormScreen.js +3 -3
  8. package/dist/ConsentFormScreen.js.map +1 -1
  9. package/dist/DateUtilities.d.ts +25 -25
  10. package/dist/DateUtilities.js +31 -32
  11. package/dist/DateUtilities.js.map +1 -1
  12. package/dist/MarkdownView.js +20 -7
  13. package/dist/MarkdownView.js.map +1 -1
  14. package/dist/MediaQuery.d.ts +4 -4
  15. package/dist/MediaQuery.js +8 -8
  16. package/dist/MediaQuery.js.map +1 -1
  17. package/dist/Page.d.ts +1 -0
  18. package/dist/Page.js +6 -2
  19. package/dist/Page.js.map +1 -1
  20. package/dist/PickerSelect.d.ts +1 -1
  21. package/dist/PickerSelect.js +2 -2
  22. package/dist/PickerSelect.js.map +1 -1
  23. package/dist/TapToEdit.d.ts +1 -1
  24. package/dist/TapToEdit.js +2 -3
  25. package/dist/TapToEdit.js.map +1 -1
  26. package/dist/ToastNotifications.js +2 -2
  27. package/dist/ToastNotifications.js.map +1 -1
  28. package/dist/Tooltip.d.ts +24 -1
  29. package/dist/Tooltip.js +2 -2
  30. package/dist/Tooltip.js.map +1 -1
  31. package/dist/Unifier.d.ts +1 -1
  32. package/dist/Unifier.js +14 -11
  33. package/dist/Unifier.js.map +1 -1
  34. package/dist/Utilities.d.ts +8 -8
  35. package/dist/Utilities.js +12 -14
  36. package/dist/Utilities.js.map +1 -1
  37. package/dist/index.d.ts +1 -1
  38. package/dist/index.js +1 -1
  39. package/dist/index.js.map +1 -1
  40. package/dist/signUp/PasswordRequirements.js +3 -3
  41. package/dist/signUp/PasswordRequirements.js.map +1 -1
  42. package/dist/table/TableHeaderCell.js +1 -9
  43. package/dist/table/TableHeaderCell.js.map +1 -1
  44. package/dist/table/tableContext.d.ts +1 -1
  45. package/dist/table/tableContext.js +2 -2
  46. package/dist/table/tableContext.js.map +1 -1
  47. package/dist/useConsentHistory.d.ts +6 -1
  48. package/dist/useConsentHistory.js +2 -1
  49. package/dist/useConsentHistory.js.map +1 -1
  50. package/package.json +1 -1
  51. package/src/ActionSheet.test.tsx +554 -0
  52. package/src/ActionSheet.tsx +26 -39
  53. package/src/Banner.test.tsx +107 -1
  54. package/src/Common.ts +10 -4
  55. package/src/ConsentFormScreen.test.tsx +22 -0
  56. package/src/ConsentFormScreen.tsx +9 -3
  57. package/src/DataTable.test.tsx +393 -1
  58. package/src/DateTimeField.test.tsx +716 -2
  59. package/src/DateUtilities.tsx +37 -38
  60. package/src/HeightActionSheet.test.tsx +17 -1
  61. package/src/HeightField.test.tsx +141 -1
  62. package/src/HeightFieldDesktop.test.tsx +19 -0
  63. package/src/MarkdownView.test.tsx +28 -0
  64. package/src/MarkdownView.tsx +69 -7
  65. package/src/MediaQuery.ts +8 -8
  66. package/src/MobileAddressAutoComplete.test.tsx +26 -3
  67. package/src/Page.test.tsx +28 -0
  68. package/src/Page.tsx +17 -2
  69. package/src/PickerSelect.test.tsx +243 -0
  70. package/src/PickerSelect.tsx +3 -3
  71. package/src/SplitPage.test.tsx +299 -43
  72. package/src/TapToEdit.test.tsx +44 -0
  73. package/src/TapToEdit.tsx +2 -3
  74. package/src/ToastNotifications.test.tsx +1412 -0
  75. package/src/ToastNotifications.tsx +2 -2
  76. package/src/Tooltip.test.tsx +1294 -3
  77. package/src/Tooltip.tsx +2 -2
  78. package/src/Unifier.ts +14 -11
  79. package/src/Utilities.tsx +14 -16
  80. package/src/WebAddressAutocomplete.test.tsx +237 -0
  81. package/src/WebDropdownMenu.test.tsx +51 -2
  82. package/src/__snapshots__/Banner.test.tsx.snap +125 -0
  83. package/src/__snapshots__/DataTable.test.tsx.snap +366 -0
  84. package/src/__snapshots__/MarkdownView.test.tsx.snap +284 -74
  85. package/src/__snapshots__/SplitPage.test.tsx.snap +698 -46
  86. package/src/bunSetup.ts +0 -4
  87. package/src/index.tsx +1 -1
  88. package/src/login/LoginScreen.test.tsx +35 -1
  89. package/src/signUp/PasswordRequirements.tsx +9 -6
  90. package/src/signUp/__snapshots__/PasswordRequirements.test.tsx.snap +50 -2
  91. package/src/signUp/__snapshots__/SignUpScreen.test.tsx.snap +25 -1
  92. package/src/table/TableHeaderCell.tsx +8 -11
  93. package/src/table/TableRow.test.tsx +31 -1
  94. package/src/table/__snapshots__/TableHeaderCell.test.tsx.snap +2 -0
  95. package/src/table/tableContext.tsx +2 -2
  96. package/src/useConsentHistory.test.ts +20 -13
  97. package/src/useConsentHistory.ts +7 -2
  98. package/src/useStoredState.test.tsx +47 -0
@@ -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,676 @@ 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
+ });
394
1108
  });