datajunction-ui 0.0.55 → 0.0.56

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 (29) hide show
  1. package/package.json +1 -1
  2. package/src/app/components/NamespaceHeader.jsx +431 -11
  3. package/src/app/components/NodeListActions.jsx +10 -7
  4. package/src/app/components/__tests__/GitModals.test.jsx +1293 -0
  5. package/src/app/components/__tests__/NamespaceHeader.test.jsx +764 -0
  6. package/src/app/components/__tests__/NodeListActions.test.jsx +5 -3
  7. package/src/app/components/__tests__/__snapshots__/NamespaceHeader.test.jsx.snap +95 -0
  8. package/src/app/components/git/CreateBranchModal.jsx +229 -0
  9. package/src/app/components/git/CreatePRModal.jsx +270 -0
  10. package/src/app/components/git/DeleteBranchModal.jsx +173 -0
  11. package/src/app/components/git/GitSettingsModal.jsx +292 -0
  12. package/src/app/components/git/SyncToGitModal.jsx +219 -0
  13. package/src/app/components/git/index.js +5 -0
  14. package/src/app/icons/DeleteIcon.jsx +3 -3
  15. package/src/app/icons/EditIcon.jsx +3 -3
  16. package/src/app/icons/EyeIcon.jsx +3 -4
  17. package/src/app/icons/JupyterExportIcon.jsx +3 -7
  18. package/src/app/icons/PythonIcon.jsx +3 -3
  19. package/src/app/pages/AddEditNodePage/index.jsx +8 -5
  20. package/src/app/pages/NamespacePage/index.jsx +34 -21
  21. package/src/app/pages/NodePage/ClientCodePopover.jsx +3 -7
  22. package/src/app/pages/NodePage/NodeInfoTab.jsx +10 -3
  23. package/src/app/pages/NodePage/NotebookDownload.jsx +4 -10
  24. package/src/app/pages/NodePage/WatchNodeButton.jsx +7 -12
  25. package/src/app/pages/NodePage/index.jsx +42 -13
  26. package/src/app/services/DJService.js +198 -1
  27. package/src/styles/index.css +3 -0
  28. package/src/styles/node-creation.scss +22 -0
  29. package/src/styles/settings.css +1 -1
@@ -508,4 +508,768 @@ describe('<NamespaceHeader />', () => {
508
508
  expect(screen.getByText(/Local\/adhoc deployments/)).toBeInTheDocument();
509
509
  });
510
510
  });
511
+
512
+ it('should show Configure Git button and open modal', async () => {
513
+ const mockDjClient = {
514
+ namespaceSources: jest.fn().mockResolvedValue({
515
+ total_deployments: 0,
516
+ primary_source: null,
517
+ }),
518
+ listDeployments: jest.fn().mockResolvedValue([]),
519
+ getNamespaceGitConfig: jest.fn().mockResolvedValue(null),
520
+ };
521
+
522
+ render(
523
+ <MemoryRouter>
524
+ <DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
525
+ <NamespaceHeader namespace="test.namespace" />
526
+ </DJClientContext.Provider>
527
+ </MemoryRouter>,
528
+ );
529
+
530
+ await waitFor(() => {
531
+ expect(screen.getByText('Configure Git')).toBeInTheDocument();
532
+ });
533
+
534
+ fireEvent.click(screen.getByText('Configure Git'));
535
+
536
+ await waitFor(() => {
537
+ expect(screen.getByText('Git Configuration')).toBeInTheDocument();
538
+ });
539
+ });
540
+
541
+ it('should show git action buttons when git is configured', async () => {
542
+ const mockDjClient = {
543
+ namespaceSources: jest.fn().mockResolvedValue({
544
+ total_deployments: 1,
545
+ primary_source: {
546
+ type: 'git',
547
+ repository: 'test/repo',
548
+ branch: 'main',
549
+ },
550
+ }),
551
+ listDeployments: jest.fn().mockResolvedValue([]),
552
+ getNamespaceGitConfig: jest.fn().mockResolvedValue({
553
+ github_repo_path: 'test/repo',
554
+ git_branch: 'main',
555
+ git_path: 'nodes/',
556
+ git_only: false,
557
+ }),
558
+ };
559
+
560
+ render(
561
+ <MemoryRouter>
562
+ <DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
563
+ <NamespaceHeader namespace="test.namespace" />
564
+ </DJClientContext.Provider>
565
+ </MemoryRouter>,
566
+ );
567
+
568
+ // For non-branch namespaces, button is labeled "New Branch"
569
+ await waitFor(() => {
570
+ expect(screen.getByText('New Branch')).toBeInTheDocument();
571
+ });
572
+ });
573
+
574
+ it('should show Create PR and Delete Branch for branch namespaces', async () => {
575
+ const mockDjClient = {
576
+ namespaceSources: jest.fn().mockResolvedValue({
577
+ total_deployments: 1,
578
+ primary_source: {
579
+ type: 'git',
580
+ repository: 'test/repo',
581
+ branch: 'feature',
582
+ },
583
+ }),
584
+ listDeployments: jest.fn().mockResolvedValue([]),
585
+ getNamespaceGitConfig: jest
586
+ .fn()
587
+ .mockResolvedValueOnce({
588
+ github_repo_path: 'test/repo',
589
+ git_branch: 'feature',
590
+ git_path: 'nodes/',
591
+ git_only: false,
592
+ parent_namespace: 'test.main',
593
+ })
594
+ .mockResolvedValueOnce({
595
+ github_repo_path: 'test/repo',
596
+ git_branch: 'main',
597
+ git_path: 'nodes/',
598
+ }),
599
+ getPullRequest: jest.fn().mockResolvedValue(null),
600
+ };
601
+
602
+ render(
603
+ <MemoryRouter>
604
+ <DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
605
+ <NamespaceHeader namespace="test.feature" />
606
+ </DJClientContext.Provider>
607
+ </MemoryRouter>,
608
+ );
609
+
610
+ await waitFor(() => {
611
+ expect(screen.getByText('Create PR')).toBeInTheDocument();
612
+ });
613
+ // Delete Branch button only has an icon with title attribute
614
+ expect(screen.getByTitle('Delete Branch')).toBeInTheDocument();
615
+ });
616
+
617
+ it('should open Create Branch modal when button is clicked', async () => {
618
+ const mockDjClient = {
619
+ namespaceSources: jest.fn().mockResolvedValue({
620
+ total_deployments: 1,
621
+ primary_source: {
622
+ type: 'git',
623
+ repository: 'test/repo',
624
+ branch: 'main',
625
+ },
626
+ }),
627
+ listDeployments: jest.fn().mockResolvedValue([]),
628
+ getNamespaceGitConfig: jest.fn().mockResolvedValue({
629
+ github_repo_path: 'test/repo',
630
+ git_branch: 'main',
631
+ git_path: 'nodes/',
632
+ git_only: false,
633
+ }),
634
+ };
635
+
636
+ render(
637
+ <MemoryRouter>
638
+ <DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
639
+ <NamespaceHeader namespace="test.namespace" />
640
+ </DJClientContext.Provider>
641
+ </MemoryRouter>,
642
+ );
643
+
644
+ await waitFor(() => {
645
+ expect(screen.getByText('New Branch')).toBeInTheDocument();
646
+ });
647
+
648
+ fireEvent.click(screen.getByText('New Branch'));
649
+
650
+ await waitFor(() => {
651
+ expect(screen.getByLabelText('Branch Name')).toBeInTheDocument();
652
+ });
653
+ });
654
+
655
+ it('should open Sync to Git modal when button is clicked', async () => {
656
+ // Sync to Git only shows for branch namespaces
657
+ const mockDjClient = {
658
+ namespaceSources: jest.fn().mockResolvedValue({
659
+ total_deployments: 1,
660
+ primary_source: {
661
+ type: 'git',
662
+ repository: 'test/repo',
663
+ branch: 'feature',
664
+ },
665
+ }),
666
+ listDeployments: jest.fn().mockResolvedValue([]),
667
+ getNamespaceGitConfig: jest
668
+ .fn()
669
+ .mockResolvedValueOnce({
670
+ github_repo_path: 'test/repo',
671
+ git_branch: 'feature',
672
+ git_path: 'nodes/',
673
+ git_only: false,
674
+ parent_namespace: 'test.main',
675
+ })
676
+ .mockResolvedValueOnce({
677
+ github_repo_path: 'test/repo',
678
+ git_branch: 'main',
679
+ git_path: 'nodes/',
680
+ }),
681
+ getPullRequest: jest.fn().mockResolvedValue(null),
682
+ };
683
+
684
+ render(
685
+ <MemoryRouter>
686
+ <DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
687
+ <NamespaceHeader namespace="test.feature" />
688
+ </DJClientContext.Provider>
689
+ </MemoryRouter>,
690
+ );
691
+
692
+ await waitFor(() => {
693
+ expect(screen.getByText('Sync to Git')).toBeInTheDocument();
694
+ });
695
+
696
+ fireEvent.click(screen.getByText('Sync to Git'));
697
+
698
+ await waitFor(() => {
699
+ expect(screen.getByText(/Sync all nodes in/)).toBeInTheDocument();
700
+ });
701
+ });
702
+
703
+ it('should call updateNamespaceGitConfig when saving git settings', async () => {
704
+ const mockDjClient = {
705
+ namespaceSources: jest.fn().mockResolvedValue({
706
+ total_deployments: 0,
707
+ primary_source: null,
708
+ }),
709
+ listDeployments: jest.fn().mockResolvedValue([]),
710
+ getNamespaceGitConfig: jest.fn().mockResolvedValue(null),
711
+ updateNamespaceGitConfig: jest.fn().mockResolvedValue({
712
+ github_repo_path: 'myorg/repo',
713
+ git_branch: 'main',
714
+ git_path: 'nodes/',
715
+ git_only: true,
716
+ }),
717
+ };
718
+
719
+ render(
720
+ <MemoryRouter>
721
+ <DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
722
+ <NamespaceHeader namespace="test.namespace" />
723
+ </DJClientContext.Provider>
724
+ </MemoryRouter>,
725
+ );
726
+
727
+ await waitFor(() => {
728
+ expect(screen.getByText('Configure Git')).toBeInTheDocument();
729
+ });
730
+
731
+ fireEvent.click(screen.getByText('Configure Git'));
732
+
733
+ await waitFor(() => {
734
+ expect(screen.getByLabelText('Repository')).toBeInTheDocument();
735
+ });
736
+
737
+ fireEvent.change(screen.getByLabelText('Repository'), {
738
+ target: { value: 'myorg/repo' },
739
+ });
740
+ fireEvent.change(screen.getByLabelText('Branch'), {
741
+ target: { value: 'main' },
742
+ });
743
+
744
+ fireEvent.click(screen.getByText('Save Settings'));
745
+
746
+ await waitFor(() => {
747
+ expect(mockDjClient.updateNamespaceGitConfig).toHaveBeenCalledWith(
748
+ 'test.namespace',
749
+ expect.objectContaining({
750
+ github_repo_path: 'myorg/repo',
751
+ git_branch: 'main',
752
+ }),
753
+ );
754
+ });
755
+ });
756
+
757
+ it('should call createBranch when creating a branch', async () => {
758
+ const mockDjClient = {
759
+ namespaceSources: jest.fn().mockResolvedValue({
760
+ total_deployments: 1,
761
+ primary_source: {
762
+ type: 'git',
763
+ repository: 'test/repo',
764
+ branch: 'main',
765
+ },
766
+ }),
767
+ listDeployments: jest.fn().mockResolvedValue([]),
768
+ getNamespaceGitConfig: jest.fn().mockResolvedValue({
769
+ github_repo_path: 'test/repo',
770
+ git_branch: 'main',
771
+ git_path: 'nodes/',
772
+ git_only: false,
773
+ }),
774
+ createBranch: jest.fn().mockResolvedValue({
775
+ branch: {
776
+ namespace: 'test.feature_xyz',
777
+ git_branch: 'feature-xyz',
778
+ parent_namespace: 'test.namespace',
779
+ },
780
+ deployment_results: [],
781
+ }),
782
+ };
783
+
784
+ render(
785
+ <MemoryRouter>
786
+ <DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
787
+ <NamespaceHeader namespace="test.namespace" />
788
+ </DJClientContext.Provider>
789
+ </MemoryRouter>,
790
+ );
791
+
792
+ await waitFor(() => {
793
+ expect(screen.getByText('New Branch')).toBeInTheDocument();
794
+ });
795
+
796
+ fireEvent.click(screen.getByText('New Branch'));
797
+
798
+ await waitFor(() => {
799
+ expect(screen.getByLabelText('Branch Name')).toBeInTheDocument();
800
+ });
801
+
802
+ fireEvent.change(screen.getByLabelText('Branch Name'), {
803
+ target: { value: 'feature-xyz' },
804
+ });
805
+
806
+ // The button inside the modal is labeled "Create Branch"
807
+ fireEvent.click(screen.getByRole('button', { name: 'Create Branch' }));
808
+
809
+ await waitFor(() => {
810
+ expect(mockDjClient.createBranch).toHaveBeenCalledWith(
811
+ 'test.namespace',
812
+ 'feature-xyz',
813
+ );
814
+ });
815
+ });
816
+
817
+ it('should call syncNamespaceToGit when syncing', async () => {
818
+ // Sync to Git only shows for branch namespaces
819
+ const mockDjClient = {
820
+ namespaceSources: jest.fn().mockResolvedValue({
821
+ total_deployments: 1,
822
+ primary_source: {
823
+ type: 'git',
824
+ repository: 'test/repo',
825
+ branch: 'feature',
826
+ },
827
+ }),
828
+ listDeployments: jest.fn().mockResolvedValue([]),
829
+ getNamespaceGitConfig: jest
830
+ .fn()
831
+ .mockResolvedValueOnce({
832
+ github_repo_path: 'test/repo',
833
+ git_branch: 'feature',
834
+ git_path: 'nodes/',
835
+ git_only: false,
836
+ parent_namespace: 'test.main',
837
+ })
838
+ .mockResolvedValueOnce({
839
+ github_repo_path: 'test/repo',
840
+ git_branch: 'main',
841
+ git_path: 'nodes/',
842
+ }),
843
+ getPullRequest: jest.fn().mockResolvedValue(null),
844
+ syncNamespaceToGit: jest.fn().mockResolvedValue({
845
+ files_synced: 5,
846
+ commit_sha: 'abc123',
847
+ commit_url: 'https://github.com/test/repo/commit/abc123',
848
+ }),
849
+ };
850
+
851
+ render(
852
+ <MemoryRouter>
853
+ <DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
854
+ <NamespaceHeader namespace="test.feature" />
855
+ </DJClientContext.Provider>
856
+ </MemoryRouter>,
857
+ );
858
+
859
+ await waitFor(() => {
860
+ expect(screen.getByText('Sync to Git')).toBeInTheDocument();
861
+ });
862
+
863
+ fireEvent.click(screen.getByText('Sync to Git'));
864
+
865
+ await waitFor(() => {
866
+ expect(screen.getByLabelText(/Commit Message/)).toBeInTheDocument();
867
+ });
868
+
869
+ fireEvent.change(screen.getByLabelText(/Commit Message/), {
870
+ target: { value: 'Test commit' },
871
+ });
872
+
873
+ fireEvent.click(screen.getByRole('button', { name: 'Sync Now' }));
874
+
875
+ await waitFor(() => {
876
+ expect(mockDjClient.syncNamespaceToGit).toHaveBeenCalledWith(
877
+ 'test.feature',
878
+ 'Test commit',
879
+ );
880
+ });
881
+ });
882
+
883
+ it('should show View PR button when PR exists', async () => {
884
+ const mockDjClient = {
885
+ namespaceSources: jest.fn().mockResolvedValue({
886
+ total_deployments: 1,
887
+ primary_source: {
888
+ type: 'git',
889
+ repository: 'test/repo',
890
+ branch: 'feature',
891
+ },
892
+ }),
893
+ listDeployments: jest.fn().mockResolvedValue([]),
894
+ getNamespaceGitConfig: jest.fn().mockResolvedValue({
895
+ github_repo_path: 'test/repo',
896
+ git_branch: 'feature',
897
+ git_path: 'nodes/',
898
+ git_only: false,
899
+ parent_namespace: 'test.main',
900
+ }),
901
+ getPullRequest: jest.fn().mockResolvedValue({
902
+ pr_number: 42,
903
+ pr_url: 'https://github.com/test/repo/pull/42',
904
+ }),
905
+ };
906
+
907
+ render(
908
+ <MemoryRouter>
909
+ <DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
910
+ <NamespaceHeader namespace="test.feature" />
911
+ </DJClientContext.Provider>
912
+ </MemoryRouter>,
913
+ );
914
+
915
+ await waitFor(() => {
916
+ expect(screen.getByText(/View PR #42/)).toBeInTheDocument();
917
+ });
918
+ });
919
+
920
+ it('should call createPullRequest when creating a PR', async () => {
921
+ const mockDjClient = {
922
+ namespaceSources: jest.fn().mockResolvedValue({
923
+ total_deployments: 1,
924
+ primary_source: {
925
+ type: 'git',
926
+ repository: 'test/repo',
927
+ branch: 'feature',
928
+ },
929
+ }),
930
+ listDeployments: jest.fn().mockResolvedValue([]),
931
+ getNamespaceGitConfig: jest
932
+ .fn()
933
+ .mockResolvedValueOnce({
934
+ github_repo_path: 'test/repo',
935
+ git_branch: 'feature',
936
+ git_path: 'nodes/',
937
+ git_only: false,
938
+ parent_namespace: 'test.main',
939
+ })
940
+ .mockResolvedValueOnce({
941
+ github_repo_path: 'test/repo',
942
+ git_branch: 'main',
943
+ git_path: 'nodes/',
944
+ }),
945
+ getPullRequest: jest.fn().mockResolvedValue(null),
946
+ syncNamespaceToGit: jest.fn().mockResolvedValue({
947
+ files_synced: 3,
948
+ commit_sha: 'abc123',
949
+ commit_url: 'https://github.com/test/repo/commit/abc123',
950
+ }),
951
+ createPullRequest: jest.fn().mockResolvedValue({
952
+ pr_number: 99,
953
+ pr_url: 'https://github.com/test/repo/pull/99',
954
+ head_branch: 'feature',
955
+ base_branch: 'main',
956
+ }),
957
+ };
958
+
959
+ render(
960
+ <MemoryRouter>
961
+ <DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
962
+ <NamespaceHeader namespace="test.feature" />
963
+ </DJClientContext.Provider>
964
+ </MemoryRouter>,
965
+ );
966
+
967
+ await waitFor(() => {
968
+ expect(screen.getByText('Create PR')).toBeInTheDocument();
969
+ });
970
+
971
+ fireEvent.click(screen.getByText('Create PR'));
972
+
973
+ await waitFor(() => {
974
+ expect(screen.getByLabelText(/Title/)).toBeInTheDocument();
975
+ });
976
+
977
+ fireEvent.change(screen.getByLabelText(/Title/), {
978
+ target: { value: 'My PR Title' },
979
+ });
980
+ fireEvent.change(screen.getByLabelText(/Description/), {
981
+ target: { value: 'PR description' },
982
+ });
983
+
984
+ // There are two "Create PR" buttons - one in header, one in modal
985
+ // Get all and click the last one (modal's submit button)
986
+ const createPRButtons = screen.getAllByRole('button', {
987
+ name: 'Create PR',
988
+ });
989
+ fireEvent.click(createPRButtons[createPRButtons.length - 1]);
990
+
991
+ await waitFor(() => {
992
+ expect(mockDjClient.syncNamespaceToGit).toHaveBeenCalledWith(
993
+ 'test.feature',
994
+ 'My PR Title',
995
+ );
996
+ });
997
+
998
+ await waitFor(() => {
999
+ expect(mockDjClient.createPullRequest).toHaveBeenCalledWith(
1000
+ 'test.feature',
1001
+ 'My PR Title',
1002
+ 'PR description',
1003
+ );
1004
+ });
1005
+ });
1006
+
1007
+ it('should call deleteBranch when deleting a branch', async () => {
1008
+ const mockDjClient = {
1009
+ namespaceSources: jest.fn().mockResolvedValue({
1010
+ total_deployments: 1,
1011
+ primary_source: {
1012
+ type: 'git',
1013
+ repository: 'test/repo',
1014
+ branch: 'feature',
1015
+ },
1016
+ }),
1017
+ listDeployments: jest.fn().mockResolvedValue([]),
1018
+ getNamespaceGitConfig: jest
1019
+ .fn()
1020
+ .mockResolvedValueOnce({
1021
+ github_repo_path: 'test/repo',
1022
+ git_branch: 'feature',
1023
+ git_path: 'nodes/',
1024
+ git_only: false,
1025
+ parent_namespace: 'test.main',
1026
+ })
1027
+ .mockResolvedValueOnce({
1028
+ github_repo_path: 'test/repo',
1029
+ git_branch: 'main',
1030
+ git_path: 'nodes/',
1031
+ }),
1032
+ getPullRequest: jest.fn().mockResolvedValue(null),
1033
+ deleteBranch: jest.fn().mockResolvedValue({ success: true }),
1034
+ };
1035
+
1036
+ // Mock window.location
1037
+ delete window.location;
1038
+ window.location = { href: '' };
1039
+
1040
+ render(
1041
+ <MemoryRouter>
1042
+ <DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
1043
+ <NamespaceHeader namespace="test.feature" />
1044
+ </DJClientContext.Provider>
1045
+ </MemoryRouter>,
1046
+ );
1047
+
1048
+ await waitFor(() => {
1049
+ // Delete Branch button in header only has icon with title attribute
1050
+ expect(screen.getByTitle('Delete Branch')).toBeInTheDocument();
1051
+ });
1052
+
1053
+ fireEvent.click(screen.getByTitle('Delete Branch'));
1054
+
1055
+ await waitFor(() => {
1056
+ expect(screen.getByRole('checkbox')).toBeInTheDocument();
1057
+ });
1058
+
1059
+ // There are two buttons with "Delete Branch" - header icon and modal button
1060
+ // Get all and click the last one (modal's submit button)
1061
+ const deleteBranchButtons = screen.getAllByRole('button', {
1062
+ name: 'Delete Branch',
1063
+ });
1064
+ fireEvent.click(deleteBranchButtons[deleteBranchButtons.length - 1]);
1065
+
1066
+ await waitFor(() => {
1067
+ expect(mockDjClient.deleteBranch).toHaveBeenCalledWith(
1068
+ 'test.main',
1069
+ 'test.feature',
1070
+ true,
1071
+ );
1072
+ });
1073
+ });
1074
+
1075
+ it('should fetch parent git config for branch namespace', async () => {
1076
+ const mockDjClient = {
1077
+ namespaceSources: jest.fn().mockResolvedValue({
1078
+ total_deployments: 1,
1079
+ primary_source: {
1080
+ type: 'git',
1081
+ repository: 'test/repo',
1082
+ branch: 'feature',
1083
+ },
1084
+ }),
1085
+ listDeployments: jest.fn().mockResolvedValue([]),
1086
+ getNamespaceGitConfig: jest
1087
+ .fn()
1088
+ .mockResolvedValueOnce({
1089
+ github_repo_path: 'test/repo',
1090
+ git_branch: 'feature',
1091
+ git_path: 'nodes/',
1092
+ git_only: false,
1093
+ parent_namespace: 'test.main',
1094
+ })
1095
+ .mockResolvedValueOnce({
1096
+ github_repo_path: 'test/repo',
1097
+ git_branch: 'main',
1098
+ git_path: 'nodes/',
1099
+ }),
1100
+ getPullRequest: jest.fn().mockResolvedValue(null),
1101
+ };
1102
+
1103
+ render(
1104
+ <MemoryRouter>
1105
+ <DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
1106
+ <NamespaceHeader namespace="test.feature" />
1107
+ </DJClientContext.Provider>
1108
+ </MemoryRouter>,
1109
+ );
1110
+
1111
+ await waitFor(() => {
1112
+ expect(mockDjClient.getNamespaceGitConfig).toHaveBeenCalledWith(
1113
+ 'test.feature',
1114
+ );
1115
+ });
1116
+
1117
+ await waitFor(() => {
1118
+ expect(mockDjClient.getNamespaceGitConfig).toHaveBeenCalledWith(
1119
+ 'test.main',
1120
+ );
1121
+ });
1122
+
1123
+ await waitFor(() => {
1124
+ expect(mockDjClient.getPullRequest).toHaveBeenCalledWith('test.feature');
1125
+ });
1126
+ });
1127
+
1128
+ it('should handle error fetching parent git config gracefully', async () => {
1129
+ const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
1130
+
1131
+ const mockDjClient = {
1132
+ namespaceSources: jest.fn().mockResolvedValue({
1133
+ total_deployments: 1,
1134
+ primary_source: {
1135
+ type: 'git',
1136
+ repository: 'test/repo',
1137
+ branch: 'feature',
1138
+ },
1139
+ }),
1140
+ listDeployments: jest.fn().mockResolvedValue([]),
1141
+ getNamespaceGitConfig: jest
1142
+ .fn()
1143
+ .mockResolvedValueOnce({
1144
+ github_repo_path: 'test/repo',
1145
+ git_branch: 'feature',
1146
+ git_path: 'nodes/',
1147
+ git_only: false,
1148
+ parent_namespace: 'test.main',
1149
+ })
1150
+ .mockRejectedValueOnce(new Error('Parent not found')),
1151
+ getPullRequest: jest.fn().mockResolvedValue(null),
1152
+ };
1153
+
1154
+ render(
1155
+ <MemoryRouter>
1156
+ <DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
1157
+ <NamespaceHeader namespace="test.feature" />
1158
+ </DJClientContext.Provider>
1159
+ </MemoryRouter>,
1160
+ );
1161
+
1162
+ await waitFor(() => {
1163
+ expect(consoleSpy).toHaveBeenCalledWith(
1164
+ 'Failed to fetch parent git config:',
1165
+ expect.any(Error),
1166
+ );
1167
+ });
1168
+
1169
+ consoleSpy.mockRestore();
1170
+ });
1171
+
1172
+ it('should handle error fetching PR gracefully', async () => {
1173
+ const mockDjClient = {
1174
+ namespaceSources: jest.fn().mockResolvedValue({
1175
+ total_deployments: 1,
1176
+ primary_source: {
1177
+ type: 'git',
1178
+ repository: 'test/repo',
1179
+ branch: 'feature',
1180
+ },
1181
+ }),
1182
+ listDeployments: jest.fn().mockResolvedValue([]),
1183
+ getNamespaceGitConfig: jest
1184
+ .fn()
1185
+ .mockResolvedValueOnce({
1186
+ github_repo_path: 'test/repo',
1187
+ git_branch: 'feature',
1188
+ git_path: 'nodes/',
1189
+ git_only: false,
1190
+ parent_namespace: 'test.main',
1191
+ })
1192
+ .mockResolvedValueOnce({
1193
+ github_repo_path: 'test/repo',
1194
+ git_branch: 'main',
1195
+ git_path: 'nodes/',
1196
+ }),
1197
+ getPullRequest: jest.fn().mockRejectedValue(new Error('API Error')),
1198
+ };
1199
+
1200
+ render(
1201
+ <MemoryRouter>
1202
+ <DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
1203
+ <NamespaceHeader namespace="test.feature" />
1204
+ </DJClientContext.Provider>
1205
+ </MemoryRouter>,
1206
+ );
1207
+
1208
+ // Should render without crashing and show Create PR button
1209
+ await waitFor(() => {
1210
+ expect(screen.getByText('Create PR')).toBeInTheDocument();
1211
+ });
1212
+ });
1213
+
1214
+ it('should call onGitConfigLoaded callback when config is fetched', async () => {
1215
+ const onGitConfigLoaded = jest.fn();
1216
+ const mockDjClient = {
1217
+ namespaceSources: jest.fn().mockResolvedValue({
1218
+ total_deployments: 0,
1219
+ primary_source: null,
1220
+ }),
1221
+ listDeployments: jest.fn().mockResolvedValue([]),
1222
+ getNamespaceGitConfig: jest.fn().mockResolvedValue({
1223
+ github_repo_path: 'test/repo',
1224
+ git_branch: 'main',
1225
+ }),
1226
+ };
1227
+
1228
+ render(
1229
+ <MemoryRouter>
1230
+ <DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
1231
+ <NamespaceHeader
1232
+ namespace="test.namespace"
1233
+ onGitConfigLoaded={onGitConfigLoaded}
1234
+ />
1235
+ </DJClientContext.Provider>
1236
+ </MemoryRouter>,
1237
+ );
1238
+
1239
+ await waitFor(() => {
1240
+ expect(onGitConfigLoaded).toHaveBeenCalledWith({
1241
+ github_repo_path: 'test/repo',
1242
+ git_branch: 'main',
1243
+ });
1244
+ });
1245
+ });
1246
+
1247
+ it('should call onGitConfigLoaded with null when git config fetch fails', async () => {
1248
+ const onGitConfigLoaded = jest.fn();
1249
+ const mockDjClient = {
1250
+ namespaceSources: jest.fn().mockResolvedValue({
1251
+ total_deployments: 0,
1252
+ primary_source: null,
1253
+ }),
1254
+ listDeployments: jest.fn().mockResolvedValue([]),
1255
+ getNamespaceGitConfig: jest
1256
+ .fn()
1257
+ .mockRejectedValue(new Error('Config not found')),
1258
+ };
1259
+
1260
+ render(
1261
+ <MemoryRouter>
1262
+ <DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
1263
+ <NamespaceHeader
1264
+ namespace="test.namespace"
1265
+ onGitConfigLoaded={onGitConfigLoaded}
1266
+ />
1267
+ </DJClientContext.Provider>
1268
+ </MemoryRouter>,
1269
+ );
1270
+
1271
+ await waitFor(() => {
1272
+ expect(onGitConfigLoaded).toHaveBeenCalledWith(null);
1273
+ });
1274
+ });
511
1275
  });