From e97896840af263efa570b93bce5901c286b49c5a Mon Sep 17 00:00:00 2001 From: nsemets Date: Fri, 30 Jan 2026 18:04:09 +0200 Subject: [PATCH] test(unit-tests): added unit tests for skipped tests --- .../institutions-list.component.spec.ts | 46 +-- .../add-component-dialog.component.spec.ts | 160 +++++++++- .../delete-component-dialog.component.spec.ts | 294 +++++++++++++++++- .../duplicate-dialog.component.spec.ts | 77 ++++- .../overview-collections.component.spec.ts | 75 ++++- ...registration-custom-step.component.spec.ts | 121 +++++-- .../mocks/collections-submissions.mock.ts | 44 ++- 7 files changed, 758 insertions(+), 59 deletions(-) diff --git a/src/app/features/institutions/pages/institutions-list/institutions-list.component.spec.ts b/src/app/features/institutions/pages/institutions-list/institutions-list.component.spec.ts index 2c90dcfd1..a83e876a8 100644 --- a/src/app/features/institutions/pages/institutions-list/institutions-list.component.spec.ts +++ b/src/app/features/institutions/pages/institutions-list/institutions-list.component.spec.ts @@ -1,38 +1,30 @@ -import { MockComponents, MockProvider } from 'ng-mocks'; +import { Store } from '@ngxs/store'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MockComponents } from 'ng-mocks'; + +import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; import { FormControl } from '@angular/forms'; -import { ActivatedRoute, Router } from '@angular/router'; import { ScheduledBannerComponent } from '@core/components/osf-banners/scheduled-banner/scheduled-banner.component'; import { LoadingSpinnerComponent } from '@osf/shared/components/loading-spinner/loading-spinner.component'; import { SearchInputComponent } from '@osf/shared/components/search-input/search-input.component'; import { SubHeaderComponent } from '@osf/shared/components/sub-header/sub-header.component'; -import { InstitutionsSelectors } from '@osf/shared/stores/institutions'; +import { FetchInstitutions, InstitutionsSelectors } from '@osf/shared/stores/institutions'; import { InstitutionsListComponent } from './institutions-list.component'; import { MOCK_INSTITUTION } from '@testing/mocks/institution.mock'; import { OSFTestingModule } from '@testing/osf.testing.module'; -import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock'; -import { RouterMockBuilder } from '@testing/providers/router-provider.mock'; import { provideMockStore } from '@testing/providers/store-provider.mock'; -describe.skip('Component: Institutions List', () => { +describe('InstitutionsListComponent', () => { let component: InstitutionsListComponent; let fixture: ComponentFixture; - let routerMock: ReturnType; - let activatedRouteMock: ReturnType; + let store: Store; const mockInstitutions = [MOCK_INSTITUTION]; - const mockTotalCount = 2; beforeEach(async () => { - routerMock = RouterMockBuilder.create().build(); - activatedRouteMock = ActivatedRouteMockBuilder.create() - .withQueryParams({ page: '1', size: '10', search: '' }) - .build(); - await TestBed.configureTestingModule({ imports: [ InstitutionsListComponent, @@ -43,17 +35,15 @@ describe.skip('Component: Institutions List', () => { provideMockStore({ signals: [ { selector: InstitutionsSelectors.getInstitutions, value: mockInstitutions }, - { selector: InstitutionsSelectors.getInstitutionsTotalCount, value: mockTotalCount }, { selector: InstitutionsSelectors.isInstitutionsLoading, value: false }, ], }), - MockProvider(Router, routerMock), - MockProvider(ActivatedRoute, activatedRouteMock), ], }).compileComponents(); fixture = TestBed.createComponent(InstitutionsListComponent); component = fixture.componentInstance; + store = TestBed.inject(Store); fixture.detectChanges(); }); @@ -61,6 +51,26 @@ describe.skip('Component: Institutions List', () => { expect(component).toBeTruthy(); }); + it('should dispatch FetchInstitutions on init', () => { + expect(store.dispatch).toHaveBeenCalledWith(expect.any(FetchInstitutions)); + const action = (store.dispatch as jest.Mock).mock.calls[0][0] as FetchInstitutions; + expect(action.searchValue).toBeUndefined(); + }); + + it('should dispatch FetchInstitutions with search value after debounce', fakeAsync(() => { + (store.dispatch as jest.Mock).mockClear(); + component.searchControl.setValue('test search'); + tick(300); + expect(store.dispatch).toHaveBeenCalledWith(new FetchInstitutions('test search')); + })); + + it('should dispatch FetchInstitutions with empty string when search is null', fakeAsync(() => { + (store.dispatch as jest.Mock).mockClear(); + component.searchControl.setValue(null); + tick(300); + expect(store.dispatch).toHaveBeenCalledWith(new FetchInstitutions('')); + })); + it('should initialize with correct default values', () => { expect(component.classes).toBe('flex-1 flex flex-column w-full'); expect(component.searchControl).toBeInstanceOf(FormControl); diff --git a/src/app/features/project/overview/components/add-component-dialog/add-component-dialog.component.spec.ts b/src/app/features/project/overview/components/add-component-dialog/add-component-dialog.component.spec.ts index 5538fd4f3..5fceb0708 100644 --- a/src/app/features/project/overview/components/add-component-dialog/add-component-dialog.component.spec.ts +++ b/src/app/features/project/overview/components/add-component-dialog/add-component-dialog.component.spec.ts @@ -1,26 +1,182 @@ +import { Store } from '@ngxs/store'; + import { MockComponent } from 'ng-mocks'; +import { of } from 'rxjs'; + import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { UserSelectors } from '@core/store/user'; import { AffiliatedInstitutionSelectComponent } from '@osf/shared/components/affiliated-institution-select/affiliated-institution-select.component'; +import { ComponentFormControls } from '@osf/shared/enums/create-component-form-controls.enum'; +import { ToastService } from '@osf/shared/services/toast.service'; +import { FetchUserInstitutions, InstitutionsSelectors } from '@osf/shared/stores/institutions'; +import { FetchRegions, RegionsSelectors } from '@osf/shared/stores/regions'; + +import { CreateComponent, GetComponents, ProjectOverviewSelectors } from '../../store'; import { AddComponentDialogComponent } from './add-component-dialog.component'; -describe.skip('AddComponentComponent', () => { +import { MOCK_INSTITUTION } from '@testing/mocks/institution.mock'; +import { MOCK_PROJECT } from '@testing/mocks/project.mock'; +import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideMockStore } from '@testing/providers/store-provider.mock'; + +describe('AddComponentDialogComponent', () => { let component: AddComponentDialogComponent; let fixture: ComponentFixture; + let store: Store; + + const mockRegions = [{ id: 'region-1', name: 'Region 1' }]; + const mockUser = { id: 'user-1', defaultRegionId: 'user-region' } as any; + const mockProject = { ...MOCK_PROJECT, id: 'proj-1', title: 'Project', tags: ['tag1'] }; + const mockInstitutions = [MOCK_INSTITUTION]; + const mockUserInstitutions = [MOCK_INSTITUTION, { ...MOCK_INSTITUTION, id: 'inst-2', name: 'Inst 2' }]; beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [AddComponentDialogComponent, MockComponent(AffiliatedInstitutionSelectComponent)], + imports: [AddComponentDialogComponent, OSFTestingModule, MockComponent(AffiliatedInstitutionSelectComponent)], + providers: [ + provideMockStore({ + signals: [ + { selector: RegionsSelectors.getRegions, value: mockRegions }, + { selector: UserSelectors.getCurrentUser, value: mockUser }, + { selector: ProjectOverviewSelectors.getProject, value: mockProject }, + { selector: ProjectOverviewSelectors.getInstitutions, value: mockInstitutions }, + { selector: RegionsSelectors.areRegionsLoading, value: false }, + { selector: ProjectOverviewSelectors.getComponentsSubmitting, value: false }, + { selector: InstitutionsSelectors.getUserInstitutions, value: mockUserInstitutions }, + { selector: InstitutionsSelectors.areUserInstitutionsLoading, value: false }, + ], + }), + ], }).compileComponents(); fixture = TestBed.createComponent(AddComponentDialogComponent); component = fixture.componentInstance; + store = TestBed.inject(Store); + (store.dispatch as jest.Mock).mockReturnValue(of(void 0)); fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); + + it('should initialize form with default values', () => { + expect(component.componentForm.get(ComponentFormControls.Title)?.value).toBe(''); + expect(Array.isArray(component.componentForm.get(ComponentFormControls.Affiliations)?.value)).toBe(true); + expect(component.componentForm.get(ComponentFormControls.Description)?.value).toBe(''); + expect(component.componentForm.get(ComponentFormControls.AddContributors)?.value).toBe(false); + expect(component.componentForm.get(ComponentFormControls.AddTags)?.value).toBe(false); + expect(['', 'user-region']).toContain(component.componentForm.get(ComponentFormControls.StorageLocation)?.value); + }); + + it('should dispatch FetchRegions and FetchUserInstitutions on init', () => { + expect(store.dispatch).toHaveBeenCalledWith(expect.any(FetchRegions)); + expect(store.dispatch).toHaveBeenCalledWith(expect.any(FetchUserInstitutions)); + }); + + it('should return store values from selectors', () => { + expect(component.storageLocations()).toEqual(mockRegions); + expect(component.currentUser()).toEqual(mockUser); + expect(component.currentProject()).toEqual(mockProject); + expect(component.institutions()).toEqual(mockInstitutions); + expect(component.areRegionsLoading()).toBe(false); + expect(component.isSubmitting()).toBe(false); + expect(component.userInstitutions()).toEqual(mockUserInstitutions); + expect(component.areUserInstitutionsLoading()).toBe(false); + }); + + it('should set affiliations form control from selected institutions', () => { + const institutions = [MOCK_INSTITUTION]; + component.setSelectedInstitutions(institutions); + expect(component.componentForm.get(ComponentFormControls.Affiliations)?.value).toEqual([MOCK_INSTITUTION.id]); + }); + + it('should mark form as touched and not dispatch when submitForm with invalid form', () => { + (store.dispatch as jest.Mock).mockClear(); + component.componentForm.get(ComponentFormControls.Title)?.setValue(''); + component.submitForm(); + expect(component.componentForm.touched).toBe(true); + const createCalls = (store.dispatch as jest.Mock).mock.calls.filter((c) => c[0] instanceof CreateComponent); + expect(createCalls.length).toBe(0); + }); + + it('should dispatch CreateComponent and on success close dialog, getComponents, showSuccess', () => { + component.componentForm.get(ComponentFormControls.Title)?.setValue('New Component'); + component.componentForm.get(ComponentFormControls.StorageLocation)?.setValue('region-1'); + component.componentForm.get(ComponentFormControls.Affiliations)?.setValue([MOCK_INSTITUTION.id]); + (store.dispatch as jest.Mock).mockClear(); + + component.submitForm(); + + expect(store.dispatch).toHaveBeenCalledWith( + new CreateComponent(mockProject.id, 'New Component', '', [], 'region-1', [MOCK_INSTITUTION.id], false) + ); + expect(component.dialogRef.close).toHaveBeenCalled(); + expect(store.dispatch).toHaveBeenCalledWith(expect.any(GetComponents)); + expect(TestBed.inject(ToastService).showSuccess).toHaveBeenCalledWith( + 'project.overview.dialog.toast.addComponent.success' + ); + }); + + it('should pass project tags when addTags is true', () => { + component.componentForm.get(ComponentFormControls.Title)?.setValue('With Tags'); + component.componentForm.get(ComponentFormControls.StorageLocation)?.setValue('region-1'); + component.componentForm.get(ComponentFormControls.Affiliations)?.setValue([]); + component.componentForm.get(ComponentFormControls.AddTags)?.setValue(true); + (store.dispatch as jest.Mock).mockClear(); + + component.submitForm(); + + expect(store.dispatch).toHaveBeenCalledWith( + new CreateComponent(mockProject.id, 'With Tags', '', mockProject.tags, 'region-1', [], false) + ); + }); + + it('should set storage location to user default region when control empty and regions loaded', () => { + fixture = TestBed.createComponent(AddComponentDialogComponent); + component = fixture.componentInstance; + component.componentForm.get(ComponentFormControls.StorageLocation)?.setValue(''); + fixture.detectChanges(); + expect(component.componentForm.get(ComponentFormControls.StorageLocation)?.value).toBe('user-region'); + }); +}); + +describe('AddComponentDialogComponent when user has no default region', () => { + let component: AddComponentDialogComponent; + let fixture: ComponentFixture; + + const mockRegions = [{ id: 'region-1', name: 'Region 1' }]; + const mockProject = { ...MOCK_PROJECT, id: 'proj-1', title: 'Project', tags: ['tag1'] }; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [AddComponentDialogComponent, OSFTestingModule, MockComponent(AffiliatedInstitutionSelectComponent)], + providers: [ + provideMockStore({ + signals: [ + { selector: RegionsSelectors.getRegions, value: mockRegions }, + { selector: UserSelectors.getCurrentUser, value: null }, + { selector: ProjectOverviewSelectors.getProject, value: mockProject }, + { selector: ProjectOverviewSelectors.getInstitutions, value: [] }, + { selector: RegionsSelectors.areRegionsLoading, value: false }, + { selector: ProjectOverviewSelectors.getComponentsSubmitting, value: false }, + { selector: InstitutionsSelectors.getUserInstitutions, value: [] }, + { selector: InstitutionsSelectors.areUserInstitutionsLoading, value: false }, + ], + }), + ], + }).compileComponents(); + + fixture = TestBed.createComponent(AddComponentDialogComponent); + component = fixture.componentInstance; + component.componentForm.get(ComponentFormControls.StorageLocation)?.setValue(''); + fixture.detectChanges(); + }); + + it('should set storage location to first region when control empty', () => { + expect(component.componentForm.get(ComponentFormControls.StorageLocation)?.value).toBe('region-1'); + }); }); diff --git a/src/app/features/project/overview/components/delete-component-dialog/delete-component-dialog.component.spec.ts b/src/app/features/project/overview/components/delete-component-dialog/delete-component-dialog.component.spec.ts index 73c88f51e..b2954f518 100644 --- a/src/app/features/project/overview/components/delete-component-dialog/delete-component-dialog.component.spec.ts +++ b/src/app/features/project/overview/components/delete-component-dialog/delete-component-dialog.component.spec.ts @@ -1,22 +1,312 @@ +import { Store } from '@ngxs/store'; + +import { DynamicDialogConfig } from 'primeng/dynamicdialog'; + +import { of } from 'rxjs'; + import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { DeleteProject, SettingsSelectors } from '@osf/features/project/settings/store'; +import { RegistrySelectors } from '@osf/features/registry/store/registry'; +import { ScientistsNames } from '@osf/shared/constants/scientists.const'; +import { ResourceType } from '@osf/shared/enums/resource-type.enum'; +import { UserPermissions } from '@osf/shared/enums/user-permissions.enum'; +import { ToastService } from '@osf/shared/services/toast.service'; +import { CurrentResourceSelectors } from '@osf/shared/stores/current-resource'; + +import { GetComponents, ProjectOverviewSelectors } from '../../store'; + import { DeleteComponentDialogComponent } from './delete-component-dialog.component'; -describe.skip('DeleteComponentDialogComponent', () => { +import { MOCK_NODE_WITH_ADMIN } from '@testing/mocks/node.mock'; +import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideMockStore } from '@testing/providers/store-provider.mock'; + +const mockComponentsWithAdmin = [ + { id: 'comp-1', title: 'Component 1', isPublic: true, permissions: [UserPermissions.Admin] }, + { id: 'comp-2', title: 'Component 2', isPublic: false, permissions: [UserPermissions.Admin] }, +]; + +const mockComponentsWithoutAdmin = [ + { id: 'comp-1', title: 'Component 1', isPublic: true, permissions: [UserPermissions.Read] }, +]; + +describe('DeleteComponentDialogComponent', () => { let component: DeleteComponentDialogComponent; let fixture: ComponentFixture; + let store: Store; + let dialogConfig: DynamicDialogConfig; + + const mockProject = { ...MOCK_NODE_WITH_ADMIN, id: 'proj-1' }; beforeEach(async () => { + dialogConfig = { data: { resourceType: ResourceType.Project } }; + await TestBed.configureTestingModule({ - imports: [DeleteComponentDialogComponent], + imports: [DeleteComponentDialogComponent, OSFTestingModule], + providers: [ + provideMockStore({ + signals: [ + { selector: ProjectOverviewSelectors.getProject, value: mockProject }, + { selector: RegistrySelectors.getRegistry, value: null }, + { selector: SettingsSelectors.isSettingsSubmitting, value: false }, + { selector: CurrentResourceSelectors.isResourceWithChildrenLoading, value: false }, + { selector: CurrentResourceSelectors.getResourceWithChildren, value: mockComponentsWithAdmin }, + ], + }), + { provide: DynamicDialogConfig, useValue: dialogConfig }, + ], }).compileComponents(); fixture = TestBed.createComponent(DeleteComponentDialogComponent); component = fixture.componentInstance; + store = TestBed.inject(Store); + (store.dispatch as jest.Mock).mockReturnValue(of(void 0)); fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); + + it('should return store values from selectors', () => { + expect(component.project()).toEqual(mockProject); + expect(component.registration()).toBeNull(); + expect(component.isSubmitting()).toBe(false); + expect(component.isLoading()).toBe(false); + expect(component.components()).toEqual(mockComponentsWithAdmin); + }); + + it('should have selectedScientist as one of ScientistsNames', () => { + expect(ScientistsNames).toContain(component.selectedScientist()); + }); + + it('should compute currentResource as project when resourceType is Project', () => { + expect(component.currentResource()).toEqual(mockProject); + }); + + it('should compute hasAdminAccessForAllComponents true when all components have Admin', () => { + expect(component.hasAdminAccessForAllComponents()).toBe(true); + }); + + it('should compute hasSubComponents true when more than one component', () => { + expect(component.hasSubComponents()).toBe(true); + }); + + it('should return isInputValid true when userInput matches selectedScientist', () => { + const scientist = component.selectedScientist(); + component.onInputChange(scientist); + expect(component.isInputValid()).toBe(true); + }); + + it('should return isInputValid false when userInput does not match', () => { + component.onInputChange('wrong'); + expect(component.isInputValid()).toBe(false); + }); + + it('should set userInput on onInputChange', () => { + component.onInputChange('test'); + expect(component.userInput()).toBe('test'); + }); + + it('should dispatch DeleteProject with components and on success close, getComponents, showSuccess', () => { + const scientist = component.selectedScientist(); + component.onInputChange(scientist); + (store.dispatch as jest.Mock).mockClear(); + + component.handleDeleteComponent(); + + expect(store.dispatch).toHaveBeenCalledWith(expect.any(DeleteProject)); + const deleteCall = (store.dispatch as jest.Mock).mock.calls.find((c) => c[0] instanceof DeleteProject); + expect(deleteCall[0].projects).toEqual(mockComponentsWithAdmin); + expect(component.dialogRef.close).toHaveBeenCalledWith({ success: true }); + expect(store.dispatch).toHaveBeenCalledWith(expect.any(GetComponents)); + expect(TestBed.inject(ToastService).showSuccess).toHaveBeenCalledWith( + 'project.overview.dialog.toast.deleteComponent.success' + ); + }); +}); + +describe('DeleteComponentDialogComponent when not all components have Admin', () => { + let component: DeleteComponentDialogComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [DeleteComponentDialogComponent, OSFTestingModule], + providers: [ + provideMockStore({ + signals: [ + { selector: ProjectOverviewSelectors.getProject, value: { ...MOCK_NODE_WITH_ADMIN, id: 'proj-1' } }, + { selector: RegistrySelectors.getRegistry, value: null }, + { selector: SettingsSelectors.isSettingsSubmitting, value: false }, + { selector: CurrentResourceSelectors.isResourceWithChildrenLoading, value: false }, + { selector: CurrentResourceSelectors.getResourceWithChildren, value: mockComponentsWithoutAdmin }, + ], + }), + { provide: DynamicDialogConfig, useValue: { data: { resourceType: ResourceType.Project } } }, + ], + }).compileComponents(); + fixture = TestBed.createComponent(DeleteComponentDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should compute hasAdminAccessForAllComponents false', () => { + expect(component.hasAdminAccessForAllComponents()).toBe(false); + }); +}); + +describe('DeleteComponentDialogComponent when single component', () => { + let component: DeleteComponentDialogComponent; + let fixture: ComponentFixture; + + const singleComponent = [ + { id: 'comp-1', title: 'Component 1', isPublic: true, permissions: [UserPermissions.Admin] }, + ]; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [DeleteComponentDialogComponent, OSFTestingModule], + providers: [ + provideMockStore({ + signals: [ + { selector: ProjectOverviewSelectors.getProject, value: { ...MOCK_NODE_WITH_ADMIN, id: 'proj-1' } }, + { selector: RegistrySelectors.getRegistry, value: null }, + { selector: SettingsSelectors.isSettingsSubmitting, value: false }, + { selector: CurrentResourceSelectors.isResourceWithChildrenLoading, value: false }, + { selector: CurrentResourceSelectors.getResourceWithChildren, value: singleComponent }, + ], + }), + { provide: DynamicDialogConfig, useValue: { data: { resourceType: ResourceType.Project } } }, + ], + }).compileComponents(); + fixture = TestBed.createComponent(DeleteComponentDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should compute hasSubComponents false', () => { + expect(component.hasSubComponents()).toBe(false); + }); +}); + +describe('DeleteComponentDialogComponent when no components', () => { + let component: DeleteComponentDialogComponent; + let fixture: ComponentFixture; + let store: Store; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [DeleteComponentDialogComponent, OSFTestingModule], + providers: [ + provideMockStore({ + signals: [ + { selector: ProjectOverviewSelectors.getProject, value: { ...MOCK_NODE_WITH_ADMIN, id: 'proj-1' } }, + { selector: RegistrySelectors.getRegistry, value: null }, + { selector: SettingsSelectors.isSettingsSubmitting, value: false }, + { selector: CurrentResourceSelectors.isResourceWithChildrenLoading, value: false }, + { selector: CurrentResourceSelectors.getResourceWithChildren, value: [] }, + ], + }), + { provide: DynamicDialogConfig, useValue: { data: { resourceType: ResourceType.Project } } }, + ], + }).compileComponents(); + fixture = TestBed.createComponent(DeleteComponentDialogComponent); + component = fixture.componentInstance; + store = TestBed.inject(Store); + (store.dispatch as jest.Mock).mockClear(); + fixture.detectChanges(); + }); + + it('should not dispatch when handleDeleteComponent', () => { + component.handleDeleteComponent(); + expect(store.dispatch).not.toHaveBeenCalled(); + }); +}); + +describe('DeleteComponentDialogComponent when resourceType is Registration', () => { + let component: DeleteComponentDialogComponent; + let fixture: ComponentFixture; + + const mockRegistration = { ...MOCK_NODE_WITH_ADMIN, id: 'reg-1' }; + const mockComponentsWithAdmin = [ + { id: 'comp-1', title: 'Component 1', isPublic: true, permissions: [UserPermissions.Admin] }, + ]; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [DeleteComponentDialogComponent, OSFTestingModule], + providers: [ + provideMockStore({ + signals: [ + { selector: ProjectOverviewSelectors.getProject, value: null }, + { selector: RegistrySelectors.getRegistry, value: mockRegistration }, + { selector: SettingsSelectors.isSettingsSubmitting, value: false }, + { selector: CurrentResourceSelectors.isResourceWithChildrenLoading, value: false }, + { selector: CurrentResourceSelectors.getResourceWithChildren, value: mockComponentsWithAdmin }, + ], + }), + { provide: DynamicDialogConfig, useValue: { data: { resourceType: ResourceType.Registration } } }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(DeleteComponentDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should compute currentResource as registration', () => { + expect(component.currentResource()).toEqual(mockRegistration); + }); +}); + +describe('DeleteComponentDialogComponent isForksContext', () => { + let component: DeleteComponentDialogComponent; + let fixture: ComponentFixture; + let store: Store; + + const mockProject = { ...MOCK_NODE_WITH_ADMIN, id: 'proj-1' }; + const mockComponentsWithAdmin = [ + { id: 'comp-1', title: 'Component 1', isPublic: true, permissions: [UserPermissions.Admin] }, + ]; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [DeleteComponentDialogComponent, OSFTestingModule], + providers: [ + provideMockStore({ + signals: [ + { selector: ProjectOverviewSelectors.getProject, value: mockProject }, + { selector: RegistrySelectors.getRegistry, value: null }, + { selector: SettingsSelectors.isSettingsSubmitting, value: false }, + { selector: CurrentResourceSelectors.isResourceWithChildrenLoading, value: false }, + { selector: CurrentResourceSelectors.getResourceWithChildren, value: mockComponentsWithAdmin }, + ], + }), + { + provide: DynamicDialogConfig, + useValue: { data: { resourceType: ResourceType.Project, isForksContext: true } }, + }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(DeleteComponentDialogComponent); + component = fixture.componentInstance; + store = TestBed.inject(Store); + (store.dispatch as jest.Mock).mockReturnValue(of(void 0)); + fixture.detectChanges(); + }); + + it('should not dispatch GetComponents when isForksContext', () => { + const scientist = component.selectedScientist(); + component.onInputChange(scientist); + (store.dispatch as jest.Mock).mockClear(); + + component.handleDeleteComponent(); + + expect(store.dispatch).toHaveBeenCalledWith(expect.any(DeleteProject)); + const getComponentsCalls = (store.dispatch as jest.Mock).mock.calls.filter((c) => c[0] instanceof GetComponents); + expect(getComponentsCalls.length).toBe(0); + }); }); diff --git a/src/app/features/project/overview/components/duplicate-dialog/duplicate-dialog.component.spec.ts b/src/app/features/project/overview/components/duplicate-dialog/duplicate-dialog.component.spec.ts index 5d78cf850..62f5c009a 100644 --- a/src/app/features/project/overview/components/duplicate-dialog/duplicate-dialog.component.spec.ts +++ b/src/app/features/project/overview/components/duplicate-dialog/duplicate-dialog.component.spec.ts @@ -1,22 +1,95 @@ +import { Store } from '@ngxs/store'; + +import { of } from 'rxjs'; + import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ToastService } from '@osf/shared/services/toast.service'; + +import { DuplicateProject, ProjectOverviewSelectors } from '../../store'; + import { DuplicateDialogComponent } from './duplicate-dialog.component'; -describe.skip('DuplicateDialogComponent', () => { +import { MOCK_NODE_WITH_ADMIN } from '@testing/mocks/node.mock'; +import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideMockStore } from '@testing/providers/store-provider.mock'; + +describe('DuplicateDialogComponent', () => { let component: DuplicateDialogComponent; let fixture: ComponentFixture; + let store: Store; + + const mockProject = { ...MOCK_NODE_WITH_ADMIN, id: 'proj-1', title: 'Test Project' }; beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [DuplicateDialogComponent], + imports: [DuplicateDialogComponent, OSFTestingModule], + providers: [ + provideMockStore({ + signals: [ + { selector: ProjectOverviewSelectors.getProject, value: mockProject }, + { selector: ProjectOverviewSelectors.getDuplicateProjectSubmitting, value: false }, + ], + }), + ], }).compileComponents(); fixture = TestBed.createComponent(DuplicateDialogComponent); component = fixture.componentInstance; + store = TestBed.inject(Store); + (store.dispatch as jest.Mock).mockReturnValue(of(void 0)); fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); + + it('should return project and isSubmitting from store', () => { + expect(component.project()).toEqual(mockProject); + expect(component.isSubmitting()).toBe(false); + }); + + it('should dispatch DuplicateProject and on success close dialog and showSuccess', () => { + (store.dispatch as jest.Mock).mockClear(); + + component.handleDuplicateConfirm(); + + expect(store.dispatch).toHaveBeenCalledWith(new DuplicateProject(mockProject.id, mockProject.title)); + expect(component.dialogRef.close).toHaveBeenCalled(); + expect(TestBed.inject(ToastService).showSuccess).toHaveBeenCalledWith( + 'project.overview.dialog.toast.duplicate.success' + ); + }); +}); + +describe('DuplicateDialogComponent when no project', () => { + let component: DuplicateDialogComponent; + let fixture: ComponentFixture; + let store: Store; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [DuplicateDialogComponent, OSFTestingModule], + providers: [ + provideMockStore({ + signals: [ + { selector: ProjectOverviewSelectors.getProject, value: null }, + { selector: ProjectOverviewSelectors.getDuplicateProjectSubmitting, value: false }, + ], + }), + ], + }).compileComponents(); + + fixture = TestBed.createComponent(DuplicateDialogComponent); + component = fixture.componentInstance; + store = TestBed.inject(Store); + (store.dispatch as jest.Mock).mockClear(); + fixture.detectChanges(); + }); + + it('should not dispatch when handleDuplicateConfirm', () => { + component.handleDuplicateConfirm(); + expect(store.dispatch).not.toHaveBeenCalled(); + }); }); diff --git a/src/app/features/project/overview/components/overview-collections/overview-collections.component.spec.ts b/src/app/features/project/overview/components/overview-collections/overview-collections.component.spec.ts index cbc3f5cf2..4809b1a72 100644 --- a/src/app/features/project/overview/components/overview-collections/overview-collections.component.spec.ts +++ b/src/app/features/project/overview/components/overview-collections/overview-collections.component.spec.ts @@ -1,14 +1,26 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { collectionFilterNames } from '@osf/features/collections/constants'; +import { CollectionSubmission } from '@osf/shared/models/collections/collections.models'; + import { OverviewCollectionsComponent } from './overview-collections.component'; -describe.skip('OverviewCollectionsComponent', () => { +import { + MOCK_COLLECTION_SUBMISSION_EMPTY_FILTERS, + MOCK_COLLECTION_SUBMISSION_SINGLE_FILTER, + MOCK_COLLECTION_SUBMISSION_STRINGIFY, + MOCK_COLLECTION_SUBMISSION_WITH_FILTERS, + MOCK_COLLECTION_SUBMISSIONS, +} from '@testing/mocks/collections-submissions.mock'; +import { OSFTestingModule } from '@testing/osf.testing.module'; + +describe('OverviewCollectionsComponent', () => { let component: OverviewCollectionsComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [OverviewCollectionsComponent], + imports: [OverviewCollectionsComponent, OSFTestingModule], }).compileComponents(); fixture = TestBed.createComponent(OverviewCollectionsComponent); @@ -19,4 +31,63 @@ describe.skip('OverviewCollectionsComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + it('should have default input values', () => { + expect(component.projectSubmissions()).toBeNull(); + expect(component.isProjectSubmissionsLoading()).toBe(false); + }); + + it('should accept projectSubmissions and isProjectSubmissionsLoading via setInput', () => { + const submissions: CollectionSubmission[] = MOCK_COLLECTION_SUBMISSIONS.map((s) => ({ + ...s, + collectionTitle: s.title, + collectionId: `col-${s.id}`, + })) as CollectionSubmission[]; + fixture.componentRef.setInput('projectSubmissions', submissions); + fixture.componentRef.setInput('isProjectSubmissionsLoading', true); + fixture.detectChanges(); + expect(component.projectSubmissions()).toEqual(submissions); + expect(component.isProjectSubmissionsLoading()).toBe(true); + }); + + it('should return empty array from getSubmissionAttributes when submission has no filter values', () => { + expect(component.getSubmissionAttributes(MOCK_COLLECTION_SUBMISSION_EMPTY_FILTERS)).toEqual([]); + }); + + it('should return attributes for truthy filter keys from getSubmissionAttributes', () => { + const result = component.getSubmissionAttributes(MOCK_COLLECTION_SUBMISSION_WITH_FILTERS); + const programAreaFilter = collectionFilterNames.find((f) => f.key === 'programArea'); + const collectedTypeFilter = collectionFilterNames.find((f) => f.key === 'collectedType'); + const statusFilter = collectionFilterNames.find((f) => f.key === 'status'); + expect(result).toContainEqual({ + key: 'programArea', + label: programAreaFilter?.label, + value: 'Health', + }); + expect(result).toContainEqual({ + key: 'collectedType', + label: collectedTypeFilter?.label, + value: 'Article', + }); + expect(result).toContainEqual({ + key: 'status', + label: statusFilter?.label, + value: 'Published', + }); + expect(result.length).toBe(3); + }); + + it('should exclude falsy values from getSubmissionAttributes', () => { + const result = component.getSubmissionAttributes(MOCK_COLLECTION_SUBMISSION_SINGLE_FILTER); + expect(result).toHaveLength(1); + expect(result[0].key).toBe('collectedType'); + expect(result[0].value).toBe('Article'); + }); + + it('should stringify numeric-like values in getSubmissionAttributes', () => { + const result = component.getSubmissionAttributes(MOCK_COLLECTION_SUBMISSION_STRINGIFY); + const statusAttr = result.find((a) => a.key === 'status'); + expect(statusAttr?.value).toBe('1'); + expect(typeof statusAttr?.value).toBe('string'); + }); }); diff --git a/src/app/features/registries/pages/draft-registration-custom-step/draft-registration-custom-step.component.spec.ts b/src/app/features/registries/pages/draft-registration-custom-step/draft-registration-custom-step.component.spec.ts index b446b84b5..0534020da 100644 --- a/src/app/features/registries/pages/draft-registration-custom-step/draft-registration-custom-step.component.spec.ts +++ b/src/app/features/registries/pages/draft-registration-custom-step/draft-registration-custom-step.component.spec.ts @@ -1,44 +1,51 @@ -import { MockComponent } from 'ng-mocks'; +import { Store } from '@ngxs/store'; + +import { MockComponent, MockProvider } from 'ng-mocks'; + +import { of } from 'rxjs'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ActivatedRoute, Router } from '@angular/router'; -import { RegistriesSelectors } from '@osf/features/registries/store'; +import { DraftRegistrationAttributesJsonApi } from '@osf/shared/models/registration/registration-json-api.model'; import { CustomStepComponent } from '../../components/custom-step/custom-step.component'; +import { RegistriesSelectors, UpdateDraft } from '../../store'; import { DraftRegistrationCustomStepComponent } from './draft-registration-custom-step.component'; -import { MOCK_REGISTRIES_PAGE } from '@testing/mocks/registries.mock'; import { OSFTestingModule } from '@testing/osf.testing.module'; import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock'; import { RouterMockBuilder } from '@testing/providers/router-provider.mock'; import { provideMockStore } from '@testing/providers/store-provider.mock'; -describe.skip('DraftRegistrationCustomStepComponent', () => { +describe('DraftRegistrationCustomStepComponent', () => { let component: DraftRegistrationCustomStepComponent; let fixture: ComponentFixture; - let mockActivatedRoute: ReturnType; + let store: Store; let mockRouter: ReturnType; + let mockActivatedRoute: ReturnType; + + const mockStepsData = { stepKey: { field: 'value' } }; + const mockDraftRegistration = { + id: 'draft-1', + providerId: 'prov-1', + branchedFrom: { id: 'proj-1', filesLink: '/project/proj-1/files/' }, + }; beforeEach(async () => { - mockActivatedRoute = ActivatedRouteMockBuilder.create().withParams({ id: 'draft-1', step: '1' }).build(); - mockRouter = RouterMockBuilder.create().withUrl('/registries/prov-1/draft/draft-1/custom').build(); + mockRouter = RouterMockBuilder.create().build(); + mockActivatedRoute = ActivatedRouteMockBuilder.create().withParams({ id: 'draft-1' }).build(); await TestBed.configureTestingModule({ imports: [DraftRegistrationCustomStepComponent, OSFTestingModule, MockComponent(CustomStepComponent)], providers: [ - { provide: ActivatedRoute, useValue: mockActivatedRoute }, - { provide: Router, useValue: mockRouter }, + MockProvider(Router, mockRouter), + MockProvider(ActivatedRoute, mockActivatedRoute), provideMockStore({ signals: [ - { selector: RegistriesSelectors.getStepsData, value: {} }, - { - selector: RegistriesSelectors.getDraftRegistration, - value: { id: 'draft-1', providerId: 'prov-1', branchedFrom: { id: 'node-1', filesLink: '/files' } }, - }, - { selector: RegistriesSelectors.getPagesSchema, value: [MOCK_REGISTRIES_PAGE] }, - { selector: RegistriesSelectors.getStepsState, value: { 1: { invalid: false } } }, + { selector: RegistriesSelectors.getStepsData, value: mockStepsData }, + { selector: RegistriesSelectors.getDraftRegistration, value: mockDraftRegistration }, ], }), ], @@ -46,6 +53,8 @@ describe.skip('DraftRegistrationCustomStepComponent', () => { fixture = TestBed.createComponent(DraftRegistrationCustomStepComponent); component = fixture.componentInstance; + store = TestBed.inject(Store); + (store.dispatch as jest.Mock).mockReturnValue(of(void 0)); fixture.detectChanges(); }); @@ -53,29 +62,81 @@ describe.skip('DraftRegistrationCustomStepComponent', () => { expect(component).toBeTruthy(); }); - it('should compute inputs from draft registration', () => { - expect(component.filesLink()).toBe('/files'); + it('should return stepsData and draftRegistration from store', () => { + expect(component.stepsData()).toEqual(mockStepsData); + expect(component.draftRegistration()).toEqual(mockDraftRegistration); + }); + + it('should compute filesLink from draftRegistration branchedFrom', () => { + expect(component.filesLink()).toBe('/project/proj-1/files/'); + }); + + it('should compute provider from draftRegistration providerId', () => { expect(component.provider()).toBe('prov-1'); - expect(component.projectId()).toBe('node-1'); }); - it('should dispatch updateDraft on onUpdateAction', () => { - const actionsMock = { updateDraft: jest.fn() } as any; - Object.defineProperty(component, 'actions', { value: actionsMock }); + it('should compute projectId from draftRegistration branchedFrom id', () => { + expect(component.projectId()).toBe('proj-1'); + }); + + it('should dispatch UpdateDraft with id and registration_responses payload on onUpdateAction', () => { + const attributes: Partial = { + registration_responses: { field1: 'value1' }, + }; + (store.dispatch as jest.Mock).mockClear(); - component.onUpdateAction({ a: 1 } as any); - expect(actionsMock.updateDraft).toHaveBeenCalledWith('draft-1', { registration_responses: { a: 1 } }); + component.onUpdateAction(attributes); + + expect(store.dispatch).toHaveBeenCalledWith(expect.any(UpdateDraft)); + const call = (store.dispatch as jest.Mock).mock.calls.find((c) => c[0] instanceof UpdateDraft); + expect(call[0].draftId).toBe('draft-1'); + expect(call[0].attributes).toEqual({ registration_responses: { registration_responses: { field1: 'value1' } } }); }); - it('should navigate back to metadata on onBack', () => { - const navigateSpy = jest.spyOn(TestBed.inject(Router), 'navigate'); + it('should navigate to ../metadata on onBack', () => { component.onBack(); - expect(navigateSpy).toHaveBeenCalledWith(['../', 'metadata'], { relativeTo: TestBed.inject(ActivatedRoute) }); + expect(mockRouter.navigate).toHaveBeenCalledWith( + ['../', 'metadata'], + expect.objectContaining({ relativeTo: expect.anything() }) + ); }); - it('should navigate to review on onNext', () => { - const navigateSpy = jest.spyOn(TestBed.inject(Router), 'navigate'); + it('should navigate to ../review on onNext', () => { component.onNext(); - expect(navigateSpy).toHaveBeenCalledWith(['../', 'review'], { relativeTo: TestBed.inject(ActivatedRoute) }); + expect(mockRouter.navigate).toHaveBeenCalledWith( + ['../', 'review'], + expect.objectContaining({ relativeTo: expect.anything() }) + ); + }); +}); + +describe('DraftRegistrationCustomStepComponent when no draft registration', () => { + let component: DraftRegistrationCustomStepComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [DraftRegistrationCustomStepComponent, OSFTestingModule, MockComponent(CustomStepComponent)], + providers: [ + MockProvider(Router, RouterMockBuilder.create().build()), + MockProvider(ActivatedRoute, ActivatedRouteMockBuilder.create().withParams({ id: 'draft-1' }).build()), + provideMockStore({ + signals: [ + { selector: RegistriesSelectors.getStepsData, value: {} }, + { selector: RegistriesSelectors.getDraftRegistration, value: null }, + ], + }), + ], + }).compileComponents(); + + fixture = TestBed.createComponent(DraftRegistrationCustomStepComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should compute empty filesLink provider and projectId', () => { + expect(component.filesLink()).toBe(''); + expect(component.provider()).toBe(''); + expect(component.projectId()).toBe(''); }); }); diff --git a/src/testing/mocks/collections-submissions.mock.ts b/src/testing/mocks/collections-submissions.mock.ts index db217ecb6..36214be05 100644 --- a/src/testing/mocks/collections-submissions.mock.ts +++ b/src/testing/mocks/collections-submissions.mock.ts @@ -1,4 +1,5 @@ -import { CollectionSubmissionWithGuid } from '@osf/shared/models/collections/collections.models'; +import { CollectionSubmissionReviewState } from '@osf/shared/enums/collection-submission-review-state.enum'; +import { CollectionSubmission, CollectionSubmissionWithGuid } from '@osf/shared/models/collections/collections.models'; export const MOCK_COLLECTION_SUBMISSION_1: CollectionSubmissionWithGuid = { id: '1', @@ -11,7 +12,7 @@ export const MOCK_COLLECTION_SUBMISSION_1: CollectionSubmissionWithGuid = { dateCreated: '2024-01-01T00:00:00Z', dateModified: '2024-01-02T00:00:00Z', public: false, - reviewsState: 'pending', + reviewsState: CollectionSubmissionReviewState.Pending, collectedType: 'preprint', status: 'pending', volume: '1', @@ -54,7 +55,7 @@ export const MOCK_COLLECTION_SUBMISSION_2: CollectionSubmissionWithGuid = { dateCreated: '2024-01-02T00:00:00Z', dateModified: '2024-01-03T00:00:00Z', public: true, - reviewsState: 'approved', + reviewsState: CollectionSubmissionReviewState.Accepted, collectedType: 'preprint', status: 'approved', volume: '2', @@ -87,3 +88,40 @@ export const MOCK_COLLECTION_SUBMISSION_2: CollectionSubmissionWithGuid = { }; export const MOCK_COLLECTION_SUBMISSIONS = [MOCK_COLLECTION_SUBMISSION_1, MOCK_COLLECTION_SUBMISSION_2]; + +export const MOCK_COLLECTION_SUBMISSION_EMPTY_FILTERS: CollectionSubmission = { + id: 'sub-1', + type: 'collection-submissions', + collectionTitle: 'Collection', + collectionId: 'col-1', + reviewsState: CollectionSubmissionReviewState.Pending, + collectedType: '', + status: '', + volume: '', + issue: '', + programArea: '', + schoolType: '', + studyDesign: '', + dataType: '', + disease: '', + gradeLevels: '', +}; + +export const MOCK_COLLECTION_SUBMISSION_WITH_FILTERS: CollectionSubmission = { + ...MOCK_COLLECTION_SUBMISSION_EMPTY_FILTERS, + reviewsState: CollectionSubmissionReviewState.Accepted, + collectedType: 'Article', + status: 'Published', + programArea: 'Health', +}; + +export const MOCK_COLLECTION_SUBMISSION_SINGLE_FILTER: CollectionSubmission = { + ...MOCK_COLLECTION_SUBMISSION_EMPTY_FILTERS, + collectedType: 'Article', +}; + +export const MOCK_COLLECTION_SUBMISSION_STRINGIFY: CollectionSubmission = { + ...MOCK_COLLECTION_SUBMISSION_EMPTY_FILTERS, + collectedType: 'Article', + status: '1', +};