diff --git a/jest.config.js b/jest.config.js index 7a9d65cbf..7638e2f48 100644 --- a/jest.config.js +++ b/jest.config.js @@ -54,10 +54,10 @@ module.exports = { extensionsToTreatAsEsm: ['.ts'], coverageThreshold: { global: { - branches: 28.0, - functions: 32.0, - lines: 60.28, - statements: 60.77, + branches: 31.0, + functions: 35.0, + lines: 64.0, + statements: 64.0, }, }, watchPathIgnorePatterns: [ @@ -70,19 +70,9 @@ module.exports = { ], testPathIgnorePatterns: [ '/src/environments', - '/src/app/app.config.ts', '/src/app/features/files/pages/file-detail', '/src/app/features/project/addons/', - '/src/app/features/project/registrations', - '/src/app/features/project/wiki', - '/src/app/features/registry/components', - '/src/app/features/registry/pages/registry-wiki/registry-wiki', '/src/app/features/settings/addons/', '/src/app/features/settings/tokens/store/', - '/src/app/shared/components/file-menu/', - '/src/app/shared/components/line-chart/', - '/src/app/shared/components/pie-chart/', - '/src/app/shared/components/reusable-filter/', - '/src/app/shared/components/wiki/edit-section/', ], }; diff --git a/setup-jest.ts b/setup-jest.ts index decf90381..0ec261638 100644 --- a/setup-jest.ts +++ b/setup-jest.ts @@ -38,6 +38,14 @@ Object.defineProperty(window, 'ResizeObserver', { value: ResizeObserver, }); +// eslint-disable-next-line @typescript-eslint/no-explicit-any +(global as any).ace = { + define: jest.fn(), + require: jest.fn().mockReturnValue({ + snippetCompleter: {}, + }), +}; + jest.mock('@newrelic/browser-agent/loaders/browser-agent', () => ({ BrowserAgent: jest.fn().mockImplementation(() => ({ start: jest.fn(), diff --git a/src/app/features/analytics/components/view-duplicates/view-duplicates.component.ts b/src/app/features/analytics/components/view-duplicates/view-duplicates.component.ts index bbf9e934c..7da6c59ee 100644 --- a/src/app/features/analytics/components/view-duplicates/view-duplicates.component.ts +++ b/src/app/features/analytics/components/view-duplicates/view-duplicates.component.ts @@ -19,7 +19,7 @@ import { Signal, signal, } from '@angular/core'; -import { toSignal } from '@angular/core/rxjs-interop'; +import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop'; import { ActivatedRoute, Router, RouterLink } from '@angular/router'; import { UserSelectors } from '@core/store/user'; @@ -75,7 +75,6 @@ export class ViewDuplicatesComponent { isAuthenticated = select(UserSelectors.isAuthenticated); readonly pageSize = 10; - readonly UserPermissions = UserPermissions; currentPage = signal(1); firstIndex = computed(() => (this.currentPage() - 1) * this.pageSize); @@ -180,7 +179,8 @@ export class ViewDuplicatesComponent { resourceType: this.resourceType(), }, }) - .onClose.subscribe((result) => { + .onClose.pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe((result) => { if (result?.success) { this.actions.getDuplicates(currentResource.id, currentResource.type, this.currentPage(), this.pageSize); } @@ -224,7 +224,8 @@ export class ViewDuplicatesComponent { pageSize: this.pageSize, }, }) - .onClose.subscribe((result) => { + .onClose.pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe((result) => { if (result?.success) { const resource = this.currentResource(); if (resource) { diff --git a/src/app/features/collections/components/add-to-collection/add-to-collection.component.ts b/src/app/features/collections/components/add-to-collection/add-to-collection.component.ts index f9be9fb74..4a3193719 100644 --- a/src/app/features/collections/components/add-to-collection/add-to-collection.component.ts +++ b/src/app/features/collections/components/add-to-collection/add-to-collection.component.ts @@ -34,13 +34,11 @@ import { CanDeactivateComponent } from '@shared/models/can-deactivate.interface' import { CollectionsSelectors, GetCollectionProvider } from '@shared/stores/collections'; import { ProjectsSelectors } from '@shared/stores/projects/projects.selectors'; -import { - AddToCollectionConfirmationDialogComponent, - CollectionMetadataStepComponent, - ProjectContributorsStepComponent, - ProjectMetadataStepComponent, - SelectProjectStepComponent, -} from './index'; +import { AddToCollectionConfirmationDialogComponent } from './add-to-collection-confirmation-dialog/add-to-collection-confirmation-dialog.component'; +import { CollectionMetadataStepComponent } from './collection-metadata-step/collection-metadata-step.component'; +import { ProjectContributorsStepComponent } from './project-contributors-step/project-contributors-step.component'; +import { ProjectMetadataStepComponent } from './project-metadata-step/project-metadata-step.component'; +import { SelectProjectStepComponent } from './select-project-step/select-project-step.component'; @Component({ selector: 'osf-add-to-collection-form', diff --git a/src/app/features/project/registrations/registrations.component.spec.ts b/src/app/features/project/registrations/registrations.component.spec.ts index 286e5b545..b00a06e57 100644 --- a/src/app/features/project/registrations/registrations.component.spec.ts +++ b/src/app/features/project/registrations/registrations.component.spec.ts @@ -1,41 +1,145 @@ -import { TranslatePipe } from '@ngx-translate/core'; -import { MockComponents } from 'ng-mocks'; +import { MockComponents, MockProvider } from 'ng-mocks'; +import { signal } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { FormsModule } from '@angular/forms'; +import { ActivatedRoute, Router } from '@angular/router'; +import { ENVIRONMENT } from '@core/provider/environment.provider'; import { CustomPaginatorComponent } from '@osf/shared/components/custom-paginator/custom-paginator.component'; import { LoadingSpinnerComponent } from '@osf/shared/components/loading-spinner/loading-spinner.component'; import { RegistrationCardComponent } from '@osf/shared/components/registration-card/registration-card.component'; import { SubHeaderComponent } from '@osf/shared/components/sub-header/sub-header.component'; +import { CurrentResourceSelectors } from '@shared/stores/current-resource'; import { RegistrationsComponent } from './registrations.component'; +import { GetRegistrations, RegistrationsSelectors } from './store'; + +import { MOCK_REGISTRATION } from '@testing/mocks/registration.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('RegistrationsComponent', () => { let component: RegistrationsComponent; let fixture: ComponentFixture; + let routerMock: ReturnType; + let activatedRouteMock: ReturnType; + let storeDispatchSpy: jest.SpyInstance; + + const mockProjectId = 'project-123'; + const mockRegistrations = [MOCK_REGISTRATION]; + const mockEnvironment = { + defaultProvider: 'test-provider', + }; beforeEach(async () => { + routerMock = RouterMockBuilder.create().build(); + activatedRouteMock = ActivatedRouteMockBuilder.create().withParams({ id: mockProjectId }).build(); + + const mockStore = provideMockStore({ + signals: [ + { selector: CurrentResourceSelectors.hasAdminAccess, value: signal(true) }, + { selector: RegistrationsSelectors.getRegistrations, value: signal(mockRegistrations) }, + { selector: RegistrationsSelectors.getRegistrationsTotalCount, value: signal(10) }, + { selector: RegistrationsSelectors.isRegistrationsLoading, value: signal(false) }, + ], + }); + + storeDispatchSpy = jest.spyOn(mockStore.useValue, 'dispatch'); + await TestBed.configureTestingModule({ imports: [ RegistrationsComponent, + OSFTestingModule, ...MockComponents( RegistrationCardComponent, SubHeaderComponent, - FormsModule, - TranslatePipe, LoadingSpinnerComponent, CustomPaginatorComponent ), ], + providers: [ + MockProvider(Router, routerMock), + MockProvider(ActivatedRoute, activatedRouteMock), + MockProvider(ENVIRONMENT, mockEnvironment), + mockStore, + ], }).compileComponents(); fixture = TestBed.createComponent(RegistrationsComponent); component = fixture.componentInstance; - fixture.detectChanges(); }); - it('should create', () => { - expect(component).toBeTruthy(); + it('should have default values', () => { + expect(component.itemsPerPage).toBe(10); + expect(component.first).toBe(0); + }); + + it('should initialize projectId from route params', () => { + expect(component.projectId()).toBe(mockProjectId); + }); + + it('should dispatch getRegistrations action on ngOnInit', () => { + component.ngOnInit(); + + expect(storeDispatchSpy).toHaveBeenCalledWith( + expect.objectContaining({ + projectId: mockProjectId, + page: 1, + pageSize: 10, + }) + ); + expect(storeDispatchSpy).toHaveBeenCalledWith(expect.any(GetRegistrations)); + }); + + it('should navigate to registries route when addRegistration is called', () => { + const navigateSpy = jest.spyOn(routerMock, 'navigate'); + + component.addRegistration(); + + expect(navigateSpy).toHaveBeenCalledWith([`registries/${mockEnvironment.defaultProvider}/new`], { + queryParams: { projectId: mockProjectId }, + }); + }); + + it('should dispatch getRegistrations and update first on page change', () => { + const mockPaginatorState = { + page: 2, + first: 20, + rows: 10, + pageCount: 5, + } as any; + + component.onPageChange(mockPaginatorState); + + expect(storeDispatchSpy).toHaveBeenCalledWith( + expect.objectContaining({ + projectId: mockProjectId, + page: 3, + pageSize: 10, + }) + ); + expect(component.first).toBe(20); + }); + + it('should handle page change with page 0', () => { + const mockPaginatorState = { + page: 0, + first: 0, + rows: 10, + pageCount: 5, + } as any; + + component.onPageChange(mockPaginatorState); + + expect(storeDispatchSpy).toHaveBeenCalledWith( + expect.objectContaining({ + projectId: mockProjectId, + page: 1, + pageSize: 10, + }) + ); + expect(component.first).toBe(0); }); }); diff --git a/src/app/features/project/wiki/wiki.component.spec.ts b/src/app/features/project/wiki/wiki.component.spec.ts index bb4a2054c..b9478e146 100644 --- a/src/app/features/project/wiki/wiki.component.spec.ts +++ b/src/app/features/project/wiki/wiki.component.spec.ts @@ -1,33 +1,96 @@ -import { provideStore } from '@ngxs/store'; - import { MockComponents, MockProvider } from 'ng-mocks'; -import { of } from 'rxjs'; +import { Subject } from 'rxjs'; -import { provideHttpClient } from '@angular/common/http'; -import { provideHttpClientTesting } from '@angular/common/http/testing'; +import { signal } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ActivatedRoute, Router } from '@angular/router'; import { SubHeaderComponent } from '@osf/shared/components/sub-header/sub-header.component'; -import { ViewOnlyLinkMessageComponent } from '@osf/shared/components/view-only-link-message/view-only-link-message.component'; import { CompareSectionComponent } from '@osf/shared/components/wiki/compare-section/compare-section.component'; import { EditSectionComponent } from '@osf/shared/components/wiki/edit-section/edit-section.component'; import { ViewSectionComponent } from '@osf/shared/components/wiki/view-section/view-section.component'; import { WikiListComponent } from '@osf/shared/components/wiki/wiki-list/wiki-list.component'; +import { ResourceType } from '@osf/shared/enums/resource-type.enum'; +import { WikiModes } from '@osf/shared/models/wiki/wiki.model'; import { ToastService } from '@osf/shared/services/toast.service'; -import { WikiState } from '@osf/shared/stores/wiki'; +import { CurrentResourceSelectors } from '@osf/shared/stores/current-resource'; +import { + ClearWiki, + CreateWikiVersion, + DeleteWiki, + GetCompareVersionContent, + GetComponentsWikiList, + GetWikiList, + GetWikiVersionContent, + GetWikiVersions, + SetCurrentWiki, + ToggleMode, + UpdateWikiPreviewContent, + WikiSelectors, +} from '@osf/shared/stores/wiki'; +import { ViewOnlyLinkMessageComponent } from '@shared/components/view-only-link-message/view-only-link-message.component'; import { WikiComponent } from './wiki.component'; +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'; +import { ToastServiceMockBuilder } from '@testing/providers/toast-provider.mock'; + describe('WikiComponent', () => { let component: WikiComponent; let fixture: ComponentFixture; + let routerMock: ReturnType; + let activatedRouteMock: ReturnType; + let toastServiceMock: ReturnType; + let storeDispatchSpy: jest.SpyInstance; + let queryParamsSubject: Subject; + + const mockProjectId = 'project-123'; + const mockWikiId = 'wiki-123'; + const mockWikiList = [{ id: 'wiki-1', name: 'Wiki 1' }] as any; beforeEach(async () => { + queryParamsSubject = new Subject(); + routerMock = RouterMockBuilder.create().build(); + toastServiceMock = ToastServiceMockBuilder.create().build(); + activatedRouteMock = ActivatedRouteMockBuilder.create() + .withParams({ id: mockProjectId }) + .withQueryParams({ wiki: mockWikiId }) + .build(); + + Object.defineProperty(activatedRouteMock, 'queryParams', { + value: queryParamsSubject.asObservable(), + writable: true, + }); + + const mockStore = provideMockStore({ + signals: [ + { selector: CurrentResourceSelectors.hasWriteAccess, value: signal(true) }, + { selector: WikiSelectors.getWikiModes, value: signal({ view: true, edit: false, compare: false }) }, + { selector: WikiSelectors.getPreviewContent, value: signal('Preview content') }, + { selector: WikiSelectors.getWikiVersionContent, value: signal('Version content') }, + { selector: WikiSelectors.getCompareVersionContent, value: signal('Compare content') }, + { selector: WikiSelectors.getWikiList, value: signal(mockWikiList) }, + { selector: WikiSelectors.getComponentsWikiList, value: signal([]) }, + { selector: WikiSelectors.getCurrentWikiId, value: signal(mockWikiId) }, + { selector: WikiSelectors.getWikiVersions, value: signal([]) }, + { selector: WikiSelectors.getWikiListLoading, value: signal(false) }, + { selector: WikiSelectors.getComponentsWikiListLoading, value: signal(false) }, + { selector: WikiSelectors.getWikiVersionsLoading, value: signal(false) }, + { selector: WikiSelectors.getCompareVersionsLoading, value: signal(false) }, + { selector: WikiSelectors.getWikiVersionSubmitting, value: signal(false) }, + ], + }); + + storeDispatchSpy = jest.spyOn(mockStore.useValue, 'dispatch'); + await TestBed.configureTestingModule({ imports: [ WikiComponent, + OSFTestingModule, ...MockComponents( SubHeaderComponent, WikiListComponent, @@ -36,34 +99,138 @@ describe('WikiComponent', () => { CompareSectionComponent, ViewOnlyLinkMessageComponent ), - provideStore([WikiState]), - provideHttpClient(), - provideHttpClientTesting(), ], providers: [ - { - provide: ActivatedRoute, - useValue: { - parent: { - params: of({ id: '123' }), - }, - snapshot: { - queryParams: { wiki: 'test-wiki' }, - }, - queryParams: of({ wiki: 'test-wiki' }), - }, - }, - MockProvider(Router), - MockProvider(ToastService), + MockProvider(Router, routerMock), + MockProvider(ActivatedRoute, activatedRouteMock), + MockProvider(ToastService, toastServiceMock), + mockStore, ], }).compileComponents(); fixture = TestBed.createComponent(WikiComponent); component = fixture.componentInstance; - fixture.detectChanges(); }); - it('should create', () => { - expect(component).toBeTruthy(); + it('should dispatch getWikiList and getComponentsWikiList on construction', () => { + expect(storeDispatchSpy).toHaveBeenCalledTimes(2); + + const getWikiListCall = storeDispatchSpy.mock.calls.find((call) => call[0] instanceof GetWikiList); + const getComponentsWikiListCall = storeDispatchSpy.mock.calls.find( + (call) => call[0] instanceof GetComponentsWikiList + ); + + expect(getWikiListCall).toBeDefined(); + expect(getWikiListCall[0].resourceType).toBe(ResourceType.Project); + expect(getWikiListCall[0].resourceId).toBe(mockProjectId); + + expect(getComponentsWikiListCall).toBeDefined(); + expect(getComponentsWikiListCall[0].resourceType).toBe(ResourceType.Project); + expect(getComponentsWikiListCall[0].resourceId).toBe(mockProjectId); + }); + + it('should call toggleMode action when toggleMode is called', () => { + storeDispatchSpy.mockClear(); + + component.toggleMode(WikiModes.Edit); + + expect(storeDispatchSpy).toHaveBeenCalledWith(expect.any(ToggleMode)); + const action = storeDispatchSpy.mock.calls[0][0] as ToggleMode; + expect(action.mode).toBe(WikiModes.Edit); + }); + + it('should call updateWikiPreviewContent action when updateWikiPreviewContent is called', () => { + storeDispatchSpy.mockClear(); + const content = 'New preview content'; + + component.updateWikiPreviewContent(content); + + expect(storeDispatchSpy).toHaveBeenCalledWith(expect.any(UpdateWikiPreviewContent)); + const action = storeDispatchSpy.mock.calls[0][0] as UpdateWikiPreviewContent; + expect(action.content).toBe(content); + }); + + it('should dispatch deleteWiki when onDeleteWiki is called', () => { + storeDispatchSpy.mockClear(); + + component.onDeleteWiki(); + + expect(storeDispatchSpy).toHaveBeenCalledWith(expect.any(DeleteWiki)); + const action = storeDispatchSpy.mock.calls[0][0] as DeleteWiki; + expect(action.wikiId).toBe(mockWikiId); + }); + + it('should dispatch createWikiVersion, show toast, and get versions when onSaveContent is called', () => { + storeDispatchSpy.mockClear(); + const content = 'New wiki content'; + + component.onSaveContent(content); + + const createVersionCall = storeDispatchSpy.mock.calls.find((call) => call[0] instanceof CreateWikiVersion); + expect(createVersionCall).toBeDefined(); + expect((createVersionCall[0] as CreateWikiVersion).wikiId).toBe(mockWikiId); + expect((createVersionCall[0] as CreateWikiVersion).content).toBe(content); + + expect(toastServiceMock.showSuccess).toHaveBeenCalledWith('project.wiki.version.successSaved'); + + const getVersionsCall = storeDispatchSpy.mock.calls.find((call) => call[0] instanceof GetWikiVersions); + expect(getVersionsCall).toBeDefined(); + expect((getVersionsCall[0] as GetWikiVersions).wikiId).toBe(mockWikiId); + }); + + it('should dispatch getWikiVersionContent when onSelectVersion is called', () => { + storeDispatchSpy.mockClear(); + const versionId = 'version-123'; + + component.onSelectVersion(versionId); + + expect(storeDispatchSpy).toHaveBeenCalledWith(expect.any(GetWikiVersionContent)); + const action = storeDispatchSpy.mock.calls[0][0] as GetWikiVersionContent; + expect(action.wikiId).toBe(mockWikiId); + expect(action.versionId).toBe(versionId); + }); + + it('should dispatch getCompareVersionContent when onSelectCompareVersion is called', () => { + storeDispatchSpy.mockClear(); + const versionId = 'version-123'; + + component.onSelectCompareVersion(versionId); + + expect(storeDispatchSpy).toHaveBeenCalledWith(expect.any(GetCompareVersionContent)); + const action = storeDispatchSpy.mock.calls[0][0] as GetCompareVersionContent; + expect(action.wikiId).toBe(mockWikiId); + expect(action.versionId).toBe(versionId); + }); + + it('should handle query params changes and dispatch setCurrentWiki and getWikiVersions', () => { + storeDispatchSpy.mockClear(); + const newWikiId = 'new-wiki-123'; + + queryParamsSubject.next({ wiki: newWikiId }); + + const setCurrentWikiCall = storeDispatchSpy.mock.calls.find((call) => call[0] instanceof SetCurrentWiki); + expect(setCurrentWikiCall).toBeDefined(); + expect((setCurrentWikiCall[0] as SetCurrentWiki).wikiId).toBe(newWikiId); + + const getVersionsCall = storeDispatchSpy.mock.calls.find((call) => call[0] instanceof GetWikiVersions); + expect(getVersionsCall).toBeDefined(); + expect((getVersionsCall[0] as GetWikiVersions).wikiId).toBe(newWikiId); + }); + + it('should not process query params when wiki is empty', () => { + storeDispatchSpy.mockClear(); + + queryParamsSubject.next({ wiki: '' }); + + const setCurrentWikiCall = storeDispatchSpy.mock.calls.find((call) => call[0] instanceof SetCurrentWiki); + expect(setCurrentWikiCall).toBeUndefined(); + }); + + it('should dispatch clearWiki on destroy', () => { + storeDispatchSpy.mockClear(); + + fixture.destroy(); + + expect(storeDispatchSpy).toHaveBeenCalledWith(expect.any(ClearWiki)); }); }); diff --git a/src/app/features/project/wiki/wiki.component.ts b/src/app/features/project/wiki/wiki.component.ts index d57a8cd9f..155d01b20 100644 --- a/src/app/features/project/wiki/wiki.component.ts +++ b/src/app/features/project/wiki/wiki.component.ts @@ -1,6 +1,6 @@ import { createDispatchMap, select } from '@ngxs/store'; -import { TranslatePipe, TranslateService } from '@ngx-translate/core'; +import { TranslatePipe } from '@ngx-translate/core'; import { Button } from 'primeng/button'; import { ButtonGroupModule } from 'primeng/buttongroup'; @@ -28,7 +28,6 @@ import { DeleteWiki, GetCompareVersionContent, GetComponentsWikiList, - GetWikiContent, GetWikiList, GetWikiModes, GetWikiVersionContent, @@ -62,7 +61,6 @@ export class WikiComponent { private readonly router = inject(Router); private readonly destroyRef = inject(DestroyRef); private toastService = inject(ToastService); - private readonly translateService = inject(TranslateService); WikiModes = WikiModes; homeWikiName = 'Home'; @@ -80,16 +78,13 @@ export class WikiComponent { isWikiVersionSubmitting = select(WikiSelectors.getWikiVersionSubmitting); isWikiVersionLoading = select(WikiSelectors.getWikiVersionsLoading); isCompareVersionLoading = select(WikiSelectors.getCompareVersionsLoading); - isAnonymous = select(WikiSelectors.isWikiAnonymous); hasViewOnly = computed(() => hasViewOnlyParam(this.router)); hasWriteAccess = select(CurrentResourceSelectors.hasWriteAccess); - hasAdminAccess = select(CurrentResourceSelectors.hasAdminAccess); actions = createDispatchMap({ getWikiModes: GetWikiModes, toggleMode: ToggleMode, - getWikiContent: GetWikiContent, getWikiList: GetWikiList, getComponentsWikiList: GetComponentsWikiList, updateWikiPreviewContent: UpdateWikiPreviewContent, diff --git a/src/app/features/registries/components/index.ts b/src/app/features/registries/components/index.ts deleted file mode 100644 index 91c733b6d..000000000 --- a/src/app/features/registries/components/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './registry-services/registry-services.component'; diff --git a/src/app/features/registries/pages/registries-landing/registries-landing.component.spec.ts b/src/app/features/registries/pages/registries-landing/registries-landing.component.spec.ts index aec8f82ea..cf4780553 100644 --- a/src/app/features/registries/pages/registries-landing/registries-landing.component.spec.ts +++ b/src/app/features/registries/pages/registries-landing/registries-landing.component.spec.ts @@ -4,13 +4,13 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { Router } from '@angular/router'; import { ScheduledBannerComponent } from '@core/components/osf-banners/scheduled-banner/scheduled-banner.component'; -import { RegistryServicesComponent } from '@osf/features/registries/components'; import { LoadingSpinnerComponent } from '@osf/shared/components/loading-spinner/loading-spinner.component'; import { ResourceCardComponent } from '@osf/shared/components/resource-card/resource-card.component'; import { SearchInputComponent } from '@osf/shared/components/search-input/search-input.component'; import { SubHeaderComponent } from '@osf/shared/components/sub-header/sub-header.component'; import { RegistrationProviderSelectors } from '@osf/shared/stores/registration-provider'; +import { RegistryServicesComponent } from '../../components/registry-services/registry-services.component'; import { RegistriesSelectors } from '../../store'; import { RegistriesLandingComponent } from './registries-landing.component'; diff --git a/src/app/features/registries/pages/registries-landing/registries-landing.component.ts b/src/app/features/registries/pages/registries-landing/registries-landing.component.ts index 7bf1f2830..80928853a 100644 --- a/src/app/features/registries/pages/registries-landing/registries-landing.component.ts +++ b/src/app/features/registries/pages/registries-landing/registries-landing.component.ts @@ -23,7 +23,7 @@ import { RegistrationProviderSelectors, } from '@osf/shared/stores/registration-provider'; -import { RegistryServicesComponent } from '../../components'; +import { RegistryServicesComponent } from '../../components/registry-services/registry-services.component'; import { GetRegistries, RegistriesSelectors } from '../../store'; @Component({ diff --git a/src/app/features/registry/components/add-resource-dialog/add-resource-dialog.component.spec.ts b/src/app/features/registry/components/add-resource-dialog/add-resource-dialog.component.spec.ts index 3def8b45c..b934deae5 100644 --- a/src/app/features/registry/components/add-resource-dialog/add-resource-dialog.component.spec.ts +++ b/src/app/features/registry/components/add-resource-dialog/add-resource-dialog.component.spec.ts @@ -1,32 +1,144 @@ -import { MockComponents } from 'ng-mocks'; +import { Store } from '@ngxs/store'; +import { MockComponent, MockProvider } from 'ng-mocks'; + +import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog'; + +import { signal } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { IconComponent } from '@osf/shared/components/icon/icon.component'; import { LoadingSpinnerComponent } from '@osf/shared/components/loading-spinner/loading-spinner.component'; +import { RegistryResourcesSelectors } from '../../store/registry-resources'; import { ResourceFormComponent } from '../resource-form/resource-form.component'; import { AddResourceDialogComponent } from './add-resource-dialog.component'; +import { DynamicDialogRefMock } from '@testing/mocks/dynamic-dialog-ref.mock'; +import { TranslateServiceMock } from '@testing/mocks/translate.service.mock'; +import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideMockStore } from '@testing/providers/store-provider.mock'; + describe('AddResourceDialogComponent', () => { let component: AddResourceDialogComponent; let fixture: ComponentFixture; + let store: Store; + let dialogRef: jest.Mocked; + let mockDialogConfig: jest.Mocked; + + const mockRegistryId = 'registry-123'; beforeEach(async () => { + mockDialogConfig = { + data: { + id: mockRegistryId, + }, + } as jest.Mocked; + await TestBed.configureTestingModule({ imports: [ AddResourceDialogComponent, - ...MockComponents(LoadingSpinnerComponent, ResourceFormComponent, IconComponent), + OSFTestingModule, + MockComponent(LoadingSpinnerComponent), + MockComponent(ResourceFormComponent), + MockComponent(IconComponent), + ], + providers: [ + DynamicDialogRefMock, + TranslateServiceMock, + MockProvider(DynamicDialogConfig, mockDialogConfig), + provideMockStore({ + signals: [ + { selector: RegistryResourcesSelectors.getCurrentResource, value: signal(null) }, + { selector: RegistryResourcesSelectors.isCurrentResourceLoading, value: signal(false) }, + ], + }), ], }).compileComponents(); fixture = TestBed.createComponent(AddResourceDialogComponent); component = fixture.componentInstance; + store = TestBed.inject(Store); + dialogRef = TestBed.inject(DynamicDialogRef) as jest.Mocked; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); + + it('should initialize with default values', () => { + expect(component.doiDomain).toBe('https://doi.org/'); + expect(component.inputLimits).toBeDefined(); + expect(component.isResourceConfirming()).toBe(false); + expect(component.isPreviewMode()).toBe(false); + expect(component.resourceOptions()).toBeDefined(); + }); + + it('should initialize form with empty values', () => { + expect(component.form.get('pid')?.value).toBe(''); + expect(component.form.get('resourceType')?.value).toBe(''); + expect(component.form.get('description')?.value).toBe(''); + }); + + it('should validate pid with DOI validator', () => { + const pidControl = component.form.get('pid'); + pidControl?.setValue('invalid-doi'); + pidControl?.updateValueAndValidity(); + + const hasDoiError = pidControl?.hasError('doi') || pidControl?.hasError('invalidDoi'); + expect(hasDoiError).toBe(true); + }); + + it('should accept valid DOI format', () => { + const pidControl = component.form.get('pid'); + pidControl?.setValue('10.1234/valid.doi'); + + expect(pidControl?.hasError('doi')).toBe(false); + }); + + it('should not preview resource when form is invalid', () => { + const dispatchSpy = jest.spyOn(store, 'dispatch'); + component.form.get('pid')?.setValue(''); + + component.previewResource(); + + expect(dispatchSpy).not.toHaveBeenCalled(); + expect(component.isPreviewMode()).toBe(false); + }); + + it('should throw error when previewing resource without current resource', () => { + component.form.patchValue({ + pid: '10.1234/test', + resourceType: 'dataset', + }); + + expect(() => component.previewResource()).toThrow(); + }); + + it('should set isPreviewMode to false when backToEdit is called', () => { + component.isPreviewMode.set(true); + + component.backToEdit(); + + expect(component.isPreviewMode()).toBe(false); + }); + + it('should throw error when adding resource without current resource', () => { + expect(() => component.onAddResource()).toThrow(); + }); + + it('should close dialog without deleting when closeDialog is called without current resource', () => { + const dispatchSpy = jest.spyOn(store, 'dispatch'); + + component.closeDialog(); + + expect(dialogRef.close).toHaveBeenCalled(); + expect(dispatchSpy).not.toHaveBeenCalled(); + }); + + it('should compute doiLink as undefined when current resource does not exist', () => { + expect(component.doiLink()).toBe('https://doi.org/undefined'); + }); }); diff --git a/src/app/features/registry/components/archiving-message/archiving-message.component.spec.ts b/src/app/features/registry/components/archiving-message/archiving-message.component.spec.ts index a13adc258..6cf0e635c 100644 --- a/src/app/features/registry/components/archiving-message/archiving-message.component.spec.ts +++ b/src/app/features/registry/components/archiving-message/archiving-message.component.spec.ts @@ -2,21 +2,24 @@ import { MockComponents } from 'ng-mocks'; import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ENVIRONMENT } from '@core/provider/environment.provider'; import { IconComponent } from '@osf/shared/components/icon/icon.component'; +import { RegistryStatus } from '@osf/shared/enums/registry-status.enum'; -import { RegistryOverview } from '../../models'; +import { RegistrationOverviewModel } from '../../models'; import { ShortRegistrationInfoComponent } from '../short-registration-info/short-registration-info.component'; import { ArchivingMessageComponent } from './archiving-message.component'; -import { MOCK_REGISTRY_OVERVIEW } from '@testing/mocks/registry-overview.mock'; +import { MOCK_REGISTRATION_OVERVIEW_MODEL } from '@testing/mocks/registration-overview-model.mock'; import { OSFTestingModule } from '@testing/osf.testing.module'; describe('ArchivingMessageComponent', () => { let component: ArchivingMessageComponent; let fixture: ComponentFixture; + let environment: any; - const mockRegistration: RegistryOverview = MOCK_REGISTRY_OVERVIEW; + const mockRegistration: RegistrationOverviewModel = MOCK_REGISTRATION_OVERVIEW_MODEL; beforeEach(async () => { await TestBed.configureTestingModule({ @@ -29,26 +32,34 @@ describe('ArchivingMessageComponent', () => { fixture = TestBed.createComponent(ArchivingMessageComponent); component = fixture.componentInstance; + environment = TestBed.inject(ENVIRONMENT); fixture.componentRef.setInput('registration', mockRegistration); fixture.detectChanges(); }); - it('should create', () => { - expect(component).toBeTruthy(); - }); - it('should have support email from environment', () => { expect(component.supportEmail).toBeDefined(); expect(typeof component.supportEmail).toBe('string'); + expect(component.supportEmail).toBe(environment.supportEmail); }); it('should receive registration input', () => { expect(component.registration()).toEqual(mockRegistration); }); + it('should have registration as a required input', () => { + expect(component.registration()).toBeDefined(); + expect(component.registration()).toEqual(mockRegistration); + }); + it('should handle different registration statuses', () => { - const statuses = ['archived', 'pending', 'approved', 'rejected']; + const statuses = [ + RegistryStatus.Accepted, + RegistryStatus.Pending, + RegistryStatus.Withdrawn, + RegistryStatus.Embargo, + ]; statuses.forEach((status) => { const registrationWithStatus = { ...mockRegistration, status }; @@ -67,4 +78,29 @@ describe('ArchivingMessageComponent', () => { expect(component.registration().title).toBe('Updated Title'); }); + + it('should update when registration properties change', () => { + const updatedRegistration = { + ...mockRegistration, + title: 'New Title', + description: 'New Description', + }; + + fixture.componentRef.setInput('registration', updatedRegistration); + fixture.detectChanges(); + + expect(component.registration().title).toBe('New Title'); + expect(component.registration().description).toBe('New Description'); + }); + + it('should maintain supportEmail reference when registration changes', () => { + const initialSupportEmail = component.supportEmail; + const updatedRegistration = { ...mockRegistration, title: 'Different Title' }; + + fixture.componentRef.setInput('registration', updatedRegistration); + fixture.detectChanges(); + + expect(component.supportEmail).toBe(initialSupportEmail); + expect(component.supportEmail).toBe(environment.supportEmail); + }); }); diff --git a/src/app/features/registry/components/edit-resource-dialog/edit-resource-dialog.component.spec.ts b/src/app/features/registry/components/edit-resource-dialog/edit-resource-dialog.component.spec.ts index 9d1d9e8ef..c4f9447ff 100644 --- a/src/app/features/registry/components/edit-resource-dialog/edit-resource-dialog.component.spec.ts +++ b/src/app/features/registry/components/edit-resource-dialog/edit-resource-dialog.component.spec.ts @@ -1,24 +1,49 @@ +import { Store } from '@ngxs/store'; + import { MockComponents, MockProvider } from 'ng-mocks'; -import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog'; +import { DynamicDialogConfig } from 'primeng/dynamicdialog'; + +import { of } from 'rxjs'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { LoadingSpinnerComponent } from '@osf/shared/components/loading-spinner/loading-spinner.component'; +import { RegistryResourceType } from '@osf/shared/enums/registry-resource.enum'; +import { RegistryResource } from '../../models'; import { RegistryResourcesSelectors } from '../../store/registry-resources'; import { ResourceFormComponent } from '../resource-form/resource-form.component'; import { EditResourceDialogComponent } from './edit-resource-dialog.component'; +import { DynamicDialogRefMock } from '@testing/mocks/dynamic-dialog-ref.mock'; import { OSFTestingModule } from '@testing/osf.testing.module'; import { provideMockStore } from '@testing/providers/store-provider.mock'; describe('EditResourceDialogComponent', () => { let component: EditResourceDialogComponent; let fixture: ComponentFixture; + let store: Store; + let mockDialogConfig: jest.Mocked; + + const mockRegistryId = 'registry-123'; + const mockResource: RegistryResource = { + id: 'resource-123', + pid: '10.1234/test.doi', + type: RegistryResourceType.Data, + description: 'Test resource description', + finalized: false, + }; beforeEach(async () => { + mockDialogConfig = { + data: { + id: mockRegistryId, + resource: mockResource, + }, + } as jest.Mocked; + await TestBed.configureTestingModule({ imports: [ EditResourceDialogComponent, @@ -26,13 +51,8 @@ describe('EditResourceDialogComponent', () => { ...MockComponents(LoadingSpinnerComponent, ResourceFormComponent), ], providers: [ - MockProvider(DynamicDialogRef, { close: jest.fn() }), - MockProvider(DynamicDialogConfig, { - data: { - id: 'reg-1', - resource: { id: 'res-1', pid: '10.1234/test', type: 'dataset', description: 'Test resource description' }, - }, - }), + DynamicDialogRefMock, + MockProvider(DynamicDialogConfig, mockDialogConfig), provideMockStore({ signals: [{ selector: RegistryResourcesSelectors.isCurrentResourceLoading, value: false }], }), @@ -41,24 +61,124 @@ describe('EditResourceDialogComponent', () => { fixture = TestBed.createComponent(EditResourceDialogComponent); component = fixture.componentInstance; + store = TestBed.inject(Store); fixture.detectChanges(); }); - it('should create', () => { - expect(component).toBeTruthy(); - }); - it('should initialize form with resource data', () => { - expect(component.form.get('pid')?.value).toBe('10.1234/test'); - expect(component.form.get('resourceType')?.value).toBe('dataset'); - expect(component.form.get('description')?.value).toBe('Test resource description'); + expect(component.form.get('pid')?.value).toBe(mockResource.pid); + expect(component.form.get('resourceType')?.value).toBe(mockResource.type); + expect(component.form.get('description')?.value).toBe(mockResource.description); }); - it('should have form validators', () => { + it('should have required validators on pid and resourceType', () => { const pidControl = component.form.get('pid'); const resourceTypeControl = component.form.get('resourceType'); expect(pidControl?.hasError('required')).toBe(false); expect(resourceTypeControl?.hasError('required')).toBe(false); }); + + it('should validate pid with DOI validator when invalid format', () => { + const pidControl = component.form.get('pid'); + pidControl?.setValue('invalid-doi'); + pidControl?.updateValueAndValidity(); + + const hasDoiError = pidControl?.hasError('doi') || pidControl?.hasError('invalidDoi'); + expect(hasDoiError).toBe(true); + }); + + it('should accept valid DOI format', () => { + const pidControl = component.form.get('pid'); + pidControl?.setValue('10.1234/valid.doi'); + + expect(pidControl?.hasError('doi')).toBe(false); + }); + + it('should mark form as invalid when pid is empty', () => { + component.form.get('pid')?.setValue(''); + component.form.get('pid')?.markAsTouched(); + + expect(component.form.invalid).toBe(true); + }); + + it('should mark form as invalid when resourceType is empty', () => { + component.form.get('resourceType')?.setValue(''); + component.form.get('resourceType')?.markAsTouched(); + + expect(component.form.invalid).toBe(true); + }); + + it('should mark form as valid when all required fields are filled with valid values', () => { + component.form.patchValue({ + pid: '10.1234/test', + resourceType: 'dataset', + description: 'Test description', + }); + + expect(component.form.valid).toBe(true); + }); + + it('should dispatch UpdateResource action with correct parameters when form is valid', () => { + const dispatchSpy = jest.spyOn(store, 'dispatch').mockReturnValue(of()); + component.form.patchValue({ + pid: '10.1234/updated', + resourceType: 'paper', + description: 'Updated description', + }); + + component.save(); + + expect(dispatchSpy).toHaveBeenCalledWith( + expect.objectContaining({ + registryId: mockRegistryId, + resourceId: mockResource.id, + resource: expect.objectContaining({ + pid: '10.1234/updated', + resource_type: 'paper', + description: 'Updated description', + }), + }) + ); + }); + + it('should handle empty description', () => { + const dispatchSpy = jest.spyOn(store, 'dispatch').mockReturnValue(of()); + component.form.patchValue({ + pid: '10.1234/test', + resourceType: 'dataset', + description: '', + }); + + component.save(); + + expect(dispatchSpy).toHaveBeenCalledWith( + expect.objectContaining({ + resource: expect.objectContaining({ + description: '', + }), + }) + ); + }); + + it('should handle null form values by converting to empty strings', () => { + const dispatchSpy = jest.spyOn(store, 'dispatch').mockReturnValue(of()); + component.form.patchValue({ + pid: '10.1234/test', + resourceType: 'dataset', + description: null, + }); + + component.save(); + + expect(dispatchSpy).toHaveBeenCalledWith( + expect.objectContaining({ + resource: expect.objectContaining({ + pid: '10.1234/test', + resource_type: 'dataset', + description: '', + }), + }) + ); + }); }); diff --git a/src/app/features/registry/components/index.ts b/src/app/features/registry/components/index.ts deleted file mode 100644 index b10f487e5..000000000 --- a/src/app/features/registry/components/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -export * from './add-resource-dialog/add-resource-dialog.component'; -export * from './archiving-message/archiving-message.component'; -export * from './edit-resource-dialog/edit-resource-dialog.component'; -export * from './registration-links-card/registration-links-card.component'; -export * from './registration-withdraw-dialog/registration-withdraw-dialog.component'; -export * from './registry-blocks-section/registry-blocks-section.component'; -export * from './registry-revisions/registry-revisions.component'; -export * from './registry-statuses/registry-statuses.component'; -export * from './resource-form/resource-form.component'; diff --git a/src/app/features/registry/components/registration-links-card/registration-links-card.component.spec.ts b/src/app/features/registry/components/registration-links-card/registration-links-card.component.spec.ts index d7d2d81bb..f10170db1 100644 --- a/src/app/features/registry/components/registration-links-card/registration-links-card.component.spec.ts +++ b/src/app/features/registry/components/registration-links-card/registration-links-card.component.spec.ts @@ -5,10 +5,14 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ContributorsListComponent } from '@osf/shared/components/contributors-list/contributors-list.component'; import { DataResourcesComponent } from '@osf/shared/components/data-resources/data-resources.component'; import { IconComponent } from '@osf/shared/components/icon/icon.component'; -import { TruncatedTextComponent } from '@osf/shared/components/truncated-text/truncated-text.component'; import { RegistrationLinksCardComponent } from './registration-links-card.component'; +import { createMockLinkedNode } from '@testing/mocks/linked-node.mock'; +import { createMockLinkedRegistration } from '@testing/mocks/linked-registration.mock'; +import { createMockRegistryComponent } from '@testing/mocks/registry-component.mock'; +import { OSFTestingModule } from '@testing/osf.testing.module'; + describe('RegistrationLinksCardComponent', () => { let component: RegistrationLinksCardComponent; let fixture: ComponentFixture; @@ -17,16 +21,100 @@ describe('RegistrationLinksCardComponent', () => { await TestBed.configureTestingModule({ imports: [ RegistrationLinksCardComponent, - ...MockComponents(DataResourcesComponent, TruncatedTextComponent, IconComponent, ContributorsListComponent), + OSFTestingModule, + ...MockComponents(DataResourcesComponent, IconComponent, ContributorsListComponent), ], }).compileComponents(); fixture = TestBed.createComponent(RegistrationLinksCardComponent); component = fixture.componentInstance; - fixture.detectChanges(); }); it('should create', () => { + fixture.componentRef.setInput('registrationData', createMockLinkedRegistration()); + fixture.detectChanges(); expect(component).toBeTruthy(); }); + + it('should set registrationData input correctly with LinkedRegistration', () => { + const mockLinkedRegistration = createMockLinkedRegistration(); + fixture.componentRef.setInput('registrationData', mockLinkedRegistration); + fixture.detectChanges(); + + expect(component.registrationData()).toEqual(mockLinkedRegistration); + }); + + it('should set registrationData input correctly with LinkedNode', () => { + const mockLinkedNode = createMockLinkedNode(); + fixture.componentRef.setInput('registrationData', mockLinkedNode); + fixture.detectChanges(); + + expect(component.registrationData()).toEqual(mockLinkedNode); + }); + + it('should set registrationData input correctly with RegistryComponentModel', () => { + const mockRegistryComponent = createMockRegistryComponent(); + fixture.componentRef.setInput('registrationData', mockRegistryComponent); + fixture.detectChanges(); + + expect(component.registrationData()).toEqual(mockRegistryComponent); + }); + + it('should return true when data has reviewsState property', () => { + fixture.componentRef.setInput('registrationData', createMockLinkedRegistration()); + fixture.detectChanges(); + + expect(component.isRegistrationData()).toBe(true); + }); + + it('should return false when data does not have reviewsState property', () => { + fixture.componentRef.setInput('registrationData', createMockLinkedNode()); + fixture.detectChanges(); + + expect(component.isRegistrationData()).toBe(false); + }); + + it('should return true when data has registrationSupplement property', () => { + fixture.componentRef.setInput('registrationData', createMockRegistryComponent()); + fixture.detectChanges(); + + expect(component.isComponentData()).toBe(true); + }); + + it('should return true for LinkedRegistration with registrationSupplement', () => { + fixture.componentRef.setInput('registrationData', createMockLinkedRegistration()); + fixture.detectChanges(); + + expect(component.isComponentData()).toBe(true); + }); + + it('should return false when data does not have registrationSupplement property', () => { + fixture.componentRef.setInput('registrationData', createMockLinkedNode()); + fixture.detectChanges(); + + expect(component.isComponentData()).toBe(false); + }); + + it('should return LinkedRegistration when data has reviewsState', () => { + const mockLinkedRegistration = createMockLinkedRegistration(); + fixture.componentRef.setInput('registrationData', mockLinkedRegistration); + fixture.detectChanges(); + + expect(component.registrationDataTyped()).toEqual(mockLinkedRegistration); + }); + + it('should return null when data does not have reviewsState', () => { + fixture.componentRef.setInput('registrationData', createMockLinkedNode()); + fixture.detectChanges(); + + expect(component.registrationDataTyped()).toBeNull(); + }); + + it('should return RegistryComponentModel when data has registrationSupplement', () => { + const mockRegistryComponent = createMockRegistryComponent(); + fixture.componentRef.setInput('registrationData', mockRegistryComponent); + fixture.detectChanges(); + + expect(component.componentsDataTyped()).toEqual(mockRegistryComponent); + }); }); diff --git a/src/app/features/registry/components/registration-overview-toolbar/registration-overview-toolbar.component.spec.ts b/src/app/features/registry/components/registration-overview-toolbar/registration-overview-toolbar.component.spec.ts index 8d2935b21..56a81a601 100644 --- a/src/app/features/registry/components/registration-overview-toolbar/registration-overview-toolbar.component.spec.ts +++ b/src/app/features/registry/components/registration-overview-toolbar/registration-overview-toolbar.component.spec.ts @@ -1,26 +1,134 @@ -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 { UserSelectors } from '@core/store/user'; import { SocialsShareButtonComponent } from '@osf/shared/components/socials-share-button/socials-share-button.component'; +import { ResourceType } from '@osf/shared/enums/resource-type.enum'; +import { ToastService } from '@osf/shared/services/toast.service'; +import { BookmarksSelectors } from '@osf/shared/stores/bookmarks'; import { RegistrationOverviewToolbarComponent } from './registration-overview-toolbar.component'; -describe('RegistrationRegistrationOverviewToolbarComponent', () => { +import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideMockStore } from '@testing/providers/store-provider.mock'; +import { ToastServiceMockBuilder } from '@testing/providers/toast-provider.mock'; + +describe('RegistrationOverviewToolbarComponent', () => { let component: RegistrationOverviewToolbarComponent; let fixture: ComponentFixture; + let store: jest.Mocked; + let toastService: ReturnType; + + const mockResourceId = 'registration-123'; + const mockResourceTitle = 'Test Registration'; + const mockBookmarksCollectionId = 'bookmarks-123'; beforeEach(async () => { + toastService = ToastServiceMockBuilder.create().build(); + await TestBed.configureTestingModule({ - imports: [RegistrationOverviewToolbarComponent, MockComponent(SocialsShareButtonComponent)], + imports: [RegistrationOverviewToolbarComponent, OSFTestingModule, MockComponent(SocialsShareButtonComponent)], + providers: [ + provideMockStore({ + signals: [ + { selector: BookmarksSelectors.getBookmarksCollectionId, value: mockBookmarksCollectionId }, + { selector: BookmarksSelectors.getBookmarks, value: [] }, + { selector: BookmarksSelectors.areBookmarksLoading, value: false }, + { selector: BookmarksSelectors.getBookmarksCollectionIdSubmitting, value: false }, + { selector: UserSelectors.isAuthenticated, value: true }, + ], + }), + MockProvider(ToastService, toastService), + ], }).compileComponents(); + store = TestBed.inject(Store) as jest.Mocked; + store.dispatch = jest.fn().mockReturnValue(of(true)); + fixture = TestBed.createComponent(RegistrationOverviewToolbarComponent); component = fixture.componentInstance; + + fixture.componentRef.setInput('resourceId', mockResourceId); + fixture.componentRef.setInput('resourceTitle', mockResourceTitle); + fixture.componentRef.setInput('isPublic', true); + }); + + it('should set resourceId input correctly', () => { fixture.detectChanges(); + expect(component.resourceId()).toBe(mockResourceId); }); - it('should create', () => { - expect(component).toBeTruthy(); + it('should set resourceTitle input correctly', () => { + fixture.detectChanges(); + expect(component.resourceTitle()).toBe(mockResourceTitle); + }); + + it('should set isPublic input correctly', () => { + fixture.detectChanges(); + expect(component.isPublic()).toBe(true); + }); + + it('should dispatch GetResourceBookmark when bookmarksCollectionId and resourceId exist', () => { + fixture.detectChanges(); + + expect(store.dispatch).toHaveBeenCalledWith( + expect.objectContaining({ + bookmarkCollectionId: mockBookmarksCollectionId, + resourceId: mockResourceId, + resourceType: ResourceType.Registration, + }) + ); + }); + + it('should set isBookmarked to false when bookmarks array is empty', () => { + fixture.detectChanges(); + expect(component.isBookmarked()).toBe(false); + }); + + it('should not do anything when resourceId is missing', () => { + fixture.componentRef.setInput('resourceId', ''); + fixture.detectChanges(); + + component.toggleBookmark(); + + expect(store.dispatch).not.toHaveBeenCalled(); + expect(toastService.showSuccess).not.toHaveBeenCalled(); + }); + + it('should add bookmark when isBookmarked is false', () => { + fixture.detectChanges(); + component.isBookmarked.set(false); + jest.clearAllMocks(); + + component.toggleBookmark(); + + expect(store.dispatch).toHaveBeenCalledWith( + expect.objectContaining({ + bookmarkCollectionId: mockBookmarksCollectionId, + resourceId: mockResourceId, + resourceType: ResourceType.Registration, + }) + ); + }); + + it('should remove bookmark when isBookmarked is true', () => { + fixture.detectChanges(); + component.isBookmarked.set(true); + jest.clearAllMocks(); + + component.toggleBookmark(); + + expect(store.dispatch).toHaveBeenCalledWith( + expect.objectContaining({ + bookmarkCollectionId: mockBookmarksCollectionId, + resourceId: mockResourceId, + resourceType: ResourceType.Registration, + }) + ); }); }); diff --git a/src/app/features/registry/components/registry-blocks-section/registry-blocks-section.component.spec.ts b/src/app/features/registry/components/registry-blocks-section/registry-blocks-section.component.spec.ts index ad7e78b25..595364d11 100644 --- a/src/app/features/registry/components/registry-blocks-section/registry-blocks-section.component.spec.ts +++ b/src/app/features/registry/components/registry-blocks-section/registry-blocks-section.component.spec.ts @@ -1,11 +1,15 @@ -import { MockComponents } from 'ng-mocks'; +import { MockComponent } from 'ng-mocks'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { RegistrationBlocksDataComponent } from '@osf/shared/components/registration-blocks-data/registration-blocks-data.component'; +import { RevisionReviewStates } from '@osf/shared/enums/revision-review-states.enum'; +import { PageSchema } from '@osf/shared/models/registration/page-schema.model'; import { RegistryBlocksSectionComponent } from './registry-blocks-section.component'; +import { createMockPageSchema } from '@testing/mocks/page-schema.mock'; +import { createMockSchemaResponse } from '@testing/mocks/schema-response.mock'; import { OSFTestingModule } from '@testing/osf.testing.module'; describe('RegistryBlocksSectionComponent', () => { @@ -14,49 +18,101 @@ describe('RegistryBlocksSectionComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [RegistryBlocksSectionComponent, OSFTestingModule, ...MockComponents(RegistrationBlocksDataComponent)], + imports: [RegistryBlocksSectionComponent, OSFTestingModule, MockComponent(RegistrationBlocksDataComponent)], }).compileComponents(); fixture = TestBed.createComponent(RegistryBlocksSectionComponent); component = fixture.componentInstance; + }); + it('should create', () => { fixture.componentRef.setInput('schemaBlocks', []); - fixture.componentRef.setInput('isSchemaBlocksLoading', false); - fixture.componentRef.setInput('isSchemaResponsesLoading', false); fixture.componentRef.setInput('schemaResponse', null); fixture.detectChanges(); - }); - it('should create', () => { expect(component).toBeTruthy(); }); - it('should compute updatedFields from schemaResponse', () => { - const mockSchemaResponse = { - id: 'test-id', - dateCreated: '2024-01-01', - dateSubmitted: null, - dateModified: '2024-01-01', - revisionJustification: 'test', - revisionResponses: {}, - updatedResponseKeys: ['key1', 'key2'], - reviewsState: 'pending' as any, - isPendingCurrentUserApproval: false, - isOriginalResponse: true, - registrationSchemaId: 'schema-id', - registrationId: 'reg-id', - }; + it('should set schemaBlocks input correctly', () => { + const mockBlocks: PageSchema[] = [createMockPageSchema()]; + fixture.componentRef.setInput('schemaBlocks', mockBlocks); + fixture.componentRef.setInput('schemaResponse', null); + fixture.detectChanges(); + + expect(component.schemaBlocks()).toEqual(mockBlocks); + }); + + it('should set schemaResponse input correctly', () => { + const mockResponse = createMockSchemaResponse('response-1', RevisionReviewStates.Approved); + fixture.componentRef.setInput('schemaBlocks', []); + fixture.componentRef.setInput('schemaResponse', mockResponse); + fixture.detectChanges(); + + expect(component.schemaResponse()).toEqual(mockResponse); + }); + + it('should default isLoading to false', () => { + fixture.componentRef.setInput('schemaBlocks', []); + fixture.componentRef.setInput('schemaResponse', null); + fixture.detectChanges(); + + expect(component.isLoading()).toBe(false); + }); - fixture.componentRef.setInput('schemaResponse', mockSchemaResponse); + it('should compute updatedFields from schemaResponse with updatedResponseKeys', () => { + const mockResponse = createMockSchemaResponse('response-1', RevisionReviewStates.Approved); + mockResponse.updatedResponseKeys = ['key1', 'key2', 'key3']; + fixture.componentRef.setInput('schemaBlocks', []); + fixture.componentRef.setInput('schemaResponse', mockResponse); fixture.detectChanges(); - expect(component.updatedFields()).toEqual(['key1', 'key2']); + expect(component.updatedFields()).toEqual(['key1', 'key2', 'key3']); }); it('should return empty array when schemaResponse is null', () => { + fixture.componentRef.setInput('schemaBlocks', []); fixture.componentRef.setInput('schemaResponse', null); fixture.detectChanges(); expect(component.updatedFields()).toEqual([]); }); + + it('should handle single updatedResponseKey', () => { + const mockResponse = createMockSchemaResponse('response-1', RevisionReviewStates.Approved); + mockResponse.updatedResponseKeys = ['single-key']; + fixture.componentRef.setInput('schemaBlocks', []); + fixture.componentRef.setInput('schemaResponse', mockResponse); + fixture.detectChanges(); + + expect(component.updatedFields()).toEqual(['single-key']); + }); + + it('should initialize with all required inputs', () => { + const mockBlocks: PageSchema[] = [createMockPageSchema()]; + const mockResponse = createMockSchemaResponse('response-1', RevisionReviewStates.Approved); + + fixture.componentRef.setInput('schemaBlocks', mockBlocks); + fixture.componentRef.setInput('schemaResponse', mockResponse); + fixture.detectChanges(); + + expect(component.schemaBlocks()).toEqual(mockBlocks); + expect(component.schemaResponse()).toEqual(mockResponse); + expect(component.isLoading()).toBe(false); + }); + + it('should handle all inputs being set together', () => { + const mockBlocks: PageSchema[] = [createMockPageSchema()]; + const mockResponse = createMockSchemaResponse('response-1', RevisionReviewStates.Approved); + mockResponse.updatedResponseKeys = ['test-key']; + + fixture.componentRef.setInput('schemaBlocks', mockBlocks); + fixture.componentRef.setInput('schemaResponse', mockResponse); + fixture.componentRef.setInput('isLoading', true); + fixture.detectChanges(); + + expect(component.schemaBlocks()).toEqual(mockBlocks); + expect(component.schemaResponse()).toEqual(mockResponse); + expect(component.isLoading()).toBe(true); + expect(component.updatedFields()).toEqual(['test-key']); + }); }); diff --git a/src/app/features/registry/components/registry-make-decision/registry-make-decision.component.spec.ts b/src/app/features/registry/components/registry-make-decision/registry-make-decision.component.spec.ts index 4dea4eade..6a637ae08 100644 --- a/src/app/features/registry/components/registry-make-decision/registry-make-decision.component.spec.ts +++ b/src/app/features/registry/components/registry-make-decision/registry-make-decision.component.spec.ts @@ -17,7 +17,7 @@ import { RegistrySelectors } from '../../store/registry'; import { RegistryMakeDecisionComponent } from './registry-make-decision.component'; import { DynamicDialogRefMock } from '@testing/mocks/dynamic-dialog-ref.mock'; -import { MOCK_REGISTRY_OVERVIEW } from '@testing/mocks/registry-overview.mock'; +import { MOCK_REGISTRATION_OVERVIEW_MODEL } from '@testing/mocks/registration-overview-model.mock'; import { OSFTestingModule } from '@testing/osf.testing.module'; import { provideMockStore } from '@testing/providers/store-provider.mock'; @@ -28,9 +28,9 @@ describe('RegistryMakeDecisionComponent', () => { let mockDialogConfig: jest.Mocked; const mockRegistry = { - ...MOCK_REGISTRY_OVERVIEW, + ...MOCK_REGISTRATION_OVERVIEW_MODEL, reviewsState: RegistrationReviewStates.Accepted, - revisionStatus: RevisionReviewStates.Approved, + revisionState: RevisionReviewStates.Approved, }; beforeEach(async () => { @@ -71,20 +71,6 @@ describe('RegistryMakeDecisionComponent', () => { expect(component.requestForm.get(ModerationDecisionFormControls.Comment)?.value).toBe(''); }); - it('should compute isPendingModeration correctly', () => { - expect(component.isPendingModeration).toBe(false); - - mockDialogConfig.data.registry = { - ...mockRegistry, - revisionStatus: RevisionReviewStates.RevisionPendingModeration, - }; - fixture = TestBed.createComponent(RegistryMakeDecisionComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - - expect(component.isPendingModeration).toBe(true); - }); - it('should compute isPendingReview correctly', () => { expect(component.isPendingReview).toBe(false); @@ -123,20 +109,21 @@ describe('RegistryMakeDecisionComponent', () => { }); it('should compute acceptValue correctly for pending review', () => { - expect(component.acceptValue).toBe(SchemaResponseActionTrigger.AcceptRevision); - }); - - it('should compute acceptValue correctly for pending withdrawal', () => { - mockDialogConfig.data.registry = { ...mockRegistry, reviewsState: RegistrationReviewStates.PendingWithdraw }; + mockDialogConfig.data.registry = { ...mockRegistry, reviewsState: RegistrationReviewStates.Pending }; fixture = TestBed.createComponent(RegistryMakeDecisionComponent); component = fixture.componentInstance; fixture.detectChanges(); - expect(component.acceptValue).toBe(ReviewActionTrigger.AcceptWithdrawal); + expect(component.acceptValue).toBe(ReviewActionTrigger.AcceptSubmission); }); it('should compute rejectValue correctly for pending review', () => { - expect(component.rejectValue).toBe(SchemaResponseActionTrigger.RejectRevision); + mockDialogConfig.data.registry = { ...mockRegistry, reviewsState: RegistrationReviewStates.Pending }; + fixture = TestBed.createComponent(RegistryMakeDecisionComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + + expect(component.rejectValue).toBe(ReviewActionTrigger.RejectSubmission); }); it('should compute rejectValue correctly for pending withdrawal', () => { @@ -156,10 +143,12 @@ describe('RegistryMakeDecisionComponent', () => { submitDecision: submitDecisionSpy, }; - component.requestForm.patchValue({ + const formValue = { [ModerationDecisionFormControls.Action]: ReviewActionTrigger.AcceptSubmission, [ModerationDecisionFormControls.Comment]: 'Test comment', - }); + }; + + component.requestForm.patchValue(formValue); component.handleSubmission(); @@ -171,7 +160,7 @@ describe('RegistryMakeDecisionComponent', () => { }, true ); - expect(closeSpy).toHaveBeenCalled(); + expect(closeSpy).toHaveBeenCalledWith(formValue); }); it('should handle form submission without revision ID', () => { @@ -224,9 +213,9 @@ describe('RegistryMakeDecisionComponent', () => { it('should handle different registry states', () => { const states = [ - { reviewsState: RegistrationReviewStates.Pending, revisionStatus: RevisionReviewStates.Approved }, - { reviewsState: RegistrationReviewStates.Accepted, revisionStatus: RevisionReviewStates.Approved }, - { reviewsState: RegistrationReviewStates.PendingWithdraw, revisionStatus: RevisionReviewStates.Approved }, + { reviewsState: RegistrationReviewStates.Pending, revisionState: RevisionReviewStates.Approved }, + { reviewsState: RegistrationReviewStates.Accepted, revisionState: RevisionReviewStates.Approved }, + { reviewsState: RegistrationReviewStates.PendingWithdraw, revisionState: RevisionReviewStates.Approved }, ]; states.forEach((state) => { @@ -237,7 +226,82 @@ describe('RegistryMakeDecisionComponent', () => { fixture.detectChanges(); expect(component.registry.reviewsState).toBe(state.reviewsState); - expect(component.registry.revisionStatus).toBe(state.revisionStatus); + expect(component.registry.revisionState).toBe(state.revisionState); }); }); + + it('should identify comment as required for reject actions', () => { + expect(component.isCommentRequired(ReviewActionTrigger.RejectSubmission)).toBe(true); + expect(component.isCommentRequired(SchemaResponseActionTrigger.RejectRevision)).toBe(true); + expect(component.isCommentRequired(ReviewActionTrigger.RejectWithdrawal)).toBe(true); + expect(component.isCommentRequired(ReviewActionTrigger.ForceWithdraw)).toBe(true); + }); + + it('should compute isCommentInvalid correctly when comment is required and empty', () => { + const commentControl = component.requestForm.get(ModerationDecisionFormControls.Comment); + component.requestForm.patchValue({ + [ModerationDecisionFormControls.Action]: ReviewActionTrigger.RejectSubmission, + [ModerationDecisionFormControls.Comment]: '', + }); + commentControl?.markAsTouched(); + + expect(component.isCommentInvalid).toBe(true); + }); + + it('should compute isCommentInvalid as false when control is not touched or dirty', () => { + component.requestForm.patchValue({ + [ModerationDecisionFormControls.Action]: ReviewActionTrigger.RejectSubmission, + [ModerationDecisionFormControls.Comment]: '', + }); + + expect(component.isCommentInvalid).toBe(false); + }); + + it('should validate comment max length', () => { + const commentControl = component.requestForm.get(ModerationDecisionFormControls.Comment); + const longComment = 'a'.repeat(component.decisionCommentLimit + 1); + commentControl?.setValue(longComment); + commentControl?.updateValueAndValidity(); + + expect(commentControl?.hasError('maxlength')).toBe(true); + }); + + it('should update comment validators when action changes to reject', () => { + const commentControl = component.requestForm.get(ModerationDecisionFormControls.Comment); + component.requestForm.patchValue({ + [ModerationDecisionFormControls.Action]: ReviewActionTrigger.RejectSubmission, + }); + + expect(commentControl?.hasError('required')).toBe(true); + }); + + it('should clear comment validators when action changes to accept', () => { + const commentControl = component.requestForm.get(ModerationDecisionFormControls.Comment); + component.requestForm.patchValue({ + [ModerationDecisionFormControls.Action]: ReviewActionTrigger.RejectSubmission, + }); + component.requestForm.patchValue({ + [ModerationDecisionFormControls.Action]: ReviewActionTrigger.AcceptSubmission, + }); + + expect(commentControl?.hasError('required')).toBe(false); + }); + + it('should initialize with correct constants', () => { + expect(component.decisionCommentLimit).toBeDefined(); + expect(component.INPUT_VALIDATION_MESSAGES).toBeDefined(); + expect(component.ReviewActionTrigger).toBeDefined(); + expect(component.SchemaResponseActionTrigger).toBeDefined(); + expect(component.ModerationDecisionFormControls).toBeDefined(); + }); + + it('should set embargoEndDate from registry', () => { + const testDate = '2024-12-31'; + mockDialogConfig.data.registry = { ...mockRegistry, embargoEndDate: testDate }; + fixture = TestBed.createComponent(RegistryMakeDecisionComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + + expect(component.embargoEndDate).toBe(testDate); + }); }); diff --git a/src/app/features/registry/components/registry-make-decision/registry-make-decision.component.ts b/src/app/features/registry/components/registry-make-decision/registry-make-decision.component.ts index 15db71ed9..2c8c3319f 100644 --- a/src/app/features/registry/components/registry-make-decision/registry-make-decision.component.ts +++ b/src/app/features/registry/components/registry-make-decision/registry-make-decision.component.ts @@ -24,7 +24,7 @@ import { RevisionReviewStates } from '@osf/shared/enums/revision-review-states.e import { ReviewActionTrigger, SchemaResponseActionTrigger } from '@osf/shared/enums/trigger-action.enum'; import { DateAgoPipe } from '@osf/shared/pipes/date-ago.pipe'; -import { RegistryOverview } from '../../models'; +import { RegistrationOverviewModel } from '../../models'; import { RegistrySelectors, SubmitDecision } from '../../store/registry'; @Component({ @@ -62,7 +62,7 @@ export class RegistryMakeDecisionComponent { actions = createDispatchMap({ submitDecision: SubmitDecision }); - registry = this.config.data.registry as RegistryOverview; + registry = this.config.data.registry as RegistrationOverviewModel; embargoEndDate = this.registry.embargoEndDate; RevisionReviewStates = RevisionReviewStates; @@ -72,7 +72,7 @@ export class RegistryMakeDecisionComponent { readonly INPUT_VALIDATION_MESSAGES = INPUT_VALIDATION_MESSAGES; get isPendingModeration(): boolean { - return this.registry.revisionStatus === RevisionReviewStates.RevisionPendingModeration; + return this.registry.revisionState === RevisionReviewStates.RevisionPendingModeration; } get isPendingReview(): boolean { @@ -86,7 +86,7 @@ export class RegistryMakeDecisionComponent { get canWithdraw(): boolean { return ( this.registry.reviewsState === RegistrationReviewStates.Accepted && - this.registry.revisionStatus === RevisionReviewStates.Approved + this.registry.revisionState === RevisionReviewStates.Approved ); } get isCommentInvalid(): boolean { diff --git a/src/app/features/registry/components/registry-overview-metadata/registry-overview-metadata.component.spec.ts b/src/app/features/registry/components/registry-overview-metadata/registry-overview-metadata.component.spec.ts index ba61aaba3..697e0a1de 100644 --- a/src/app/features/registry/components/registry-overview-metadata/registry-overview-metadata.component.spec.ts +++ b/src/app/features/registry/components/registry-overview-metadata/registry-overview-metadata.component.spec.ts @@ -1,22 +1,188 @@ +import { Store } from '@ngxs/store'; + +import { MockComponents } from 'ng-mocks'; + +import { of } from 'rxjs'; + import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { Router } from '@angular/router'; + +import { ENVIRONMENT } from '@core/provider/environment.provider'; +import { AffiliatedInstitutionsViewComponent } from '@osf/shared/components/affiliated-institutions-view/affiliated-institutions-view.component'; +import { ContributorsListComponent } from '@osf/shared/components/contributors-list/contributors-list.component'; +import { ResourceCitationsComponent } from '@osf/shared/components/resource-citations/resource-citations.component'; +import { ResourceDoiComponent } from '@osf/shared/components/resource-doi/resource-doi.component'; +import { ResourceLicenseComponent } from '@osf/shared/components/resource-license/resource-license.component'; +import { SubjectsListComponent } from '@osf/shared/components/subjects-list/subjects-list.component'; +import { TagsListComponent } from '@osf/shared/components/tags-list/tags-list.component'; +import { CurrentResourceType, ResourceType } from '@osf/shared/enums/resource-type.enum'; +import { ContributorsSelectors, LoadMoreBibliographicContributors } from '@osf/shared/stores/contributors'; +import { FetchSelectedSubjects, SubjectsSelectors } from '@osf/shared/stores/subjects'; + +import { + GetRegistryIdentifiers, + GetRegistryInstitutions, + GetRegistryLicense, + RegistrySelectors, + SetRegistryCustomCitation, +} from '../../store/registry'; import { RegistryOverviewMetadataComponent } from './registry-overview-metadata.component'; +import { MOCK_REGISTRATION_OVERVIEW_MODEL } from '@testing/mocks/registration-overview-model.mock'; +import { OSFTestingModule } from '@testing/osf.testing.module'; +import { RouterMockBuilder } from '@testing/providers/router-provider.mock'; +import { provideMockStore } from '@testing/providers/store-provider.mock'; + describe('RegistryOverviewMetadataComponent', () => { let component: RegistryOverviewMetadataComponent; let fixture: ComponentFixture; + let store: jest.Mocked; + let routerMock: ReturnType; + + const mockRegistry = { + ...MOCK_REGISTRATION_OVERVIEW_MODEL, + id: 'registry-123', + licenseId: 'license-123', + }; + + const mockEnvironment = { + webUrl: 'https://test.osf.io', + }; beforeEach(async () => { + routerMock = RouterMockBuilder.create().build(); + await TestBed.configureTestingModule({ - imports: [RegistryOverviewMetadataComponent], + imports: [ + RegistryOverviewMetadataComponent, + OSFTestingModule, + ...MockComponents( + ResourceCitationsComponent, + AffiliatedInstitutionsViewComponent, + ContributorsListComponent, + ResourceDoiComponent, + ResourceLicenseComponent, + SubjectsListComponent, + TagsListComponent + ), + ], + providers: [ + provideMockStore({ + signals: [ + { selector: RegistrySelectors.getRegistry, value: mockRegistry }, + { selector: RegistrySelectors.isRegistryAnonymous, value: false }, + { selector: RegistrySelectors.hasWriteAccess, value: true }, + { selector: RegistrySelectors.getLicense, value: null }, + { selector: RegistrySelectors.isLicenseLoading, value: false }, + { selector: RegistrySelectors.getIdentifiers, value: [] }, + { selector: RegistrySelectors.isIdentifiersLoading, value: false }, + { selector: RegistrySelectors.getInstitutions, value: [] }, + { selector: RegistrySelectors.isInstitutionsLoading, value: false }, + { selector: SubjectsSelectors.getSubjects, value: [] }, + { selector: SubjectsSelectors.getSubjectsLoading, value: false }, + { selector: ContributorsSelectors.getBibliographicContributors, value: [] }, + { selector: ContributorsSelectors.isBibliographicContributorsLoading, value: false }, + { selector: ContributorsSelectors.hasMoreBibliographicContributors, value: false }, + ], + }), + { provide: Router, useValue: routerMock }, + { provide: ENVIRONMENT, useValue: mockEnvironment }, + ], }).compileComponents(); + store = TestBed.inject(Store) as jest.Mocked; + store.dispatch = jest.fn().mockReturnValue(of(true)); fixture = TestBed.createComponent(RegistryOverviewMetadataComponent); component = fixture.componentInstance; + }); + + it('should have currentResourceType set to Registrations', () => { + expect(component.currentResourceType).toBe(CurrentResourceType.Registrations); + }); + + it('should have correct dateFormat', () => { + expect(component.dateFormat).toBe('MMM d, y, h:mm a'); + }); + + it('should have webUrl from environment', () => { + expect(component.webUrl).toBe('https://test.osf.io'); + }); + + it('should dispatch actions when registry exists', () => { fixture.detectChanges(); + + expect(store.dispatch).toHaveBeenCalledWith(expect.any(GetRegistryInstitutions)); + expect(store.dispatch).toHaveBeenCalledWith(expect.any(FetchSelectedSubjects)); + expect(store.dispatch).toHaveBeenCalledWith(expect.any(GetRegistryLicense)); + expect(store.dispatch).toHaveBeenCalledWith(expect.any(GetRegistryIdentifiers)); }); - it('should create', () => { - expect(component).toBeTruthy(); + it('should dispatch GetRegistryInstitutions with correct registryId', () => { + fixture.detectChanges(); + + const call = (store.dispatch as jest.Mock).mock.calls.find((call) => call[0] instanceof GetRegistryInstitutions); + expect(call).toBeDefined(); + const action = call[0] as GetRegistryInstitutions; + expect(action.registryId).toBe('registry-123'); + }); + + it('should dispatch FetchSelectedSubjects with correct parameters', () => { + fixture.detectChanges(); + + const call = (store.dispatch as jest.Mock).mock.calls.find((call) => call[0] instanceof FetchSelectedSubjects); + expect(call).toBeDefined(); + const action = call[0] as FetchSelectedSubjects; + expect(action.resourceId).toBe('registry-123'); + expect(action.resourceType).toBe(ResourceType.Registration); + }); + + it('should dispatch GetRegistryLicense with licenseId from registry', () => { + fixture.detectChanges(); + + const call = (store.dispatch as jest.Mock).mock.calls.find((call) => call[0] instanceof GetRegistryLicense); + expect(call).toBeDefined(); + const action = call[0] as GetRegistryLicense; + expect(action.licenseId).toBe('license-123'); + }); + + it('should dispatch GetRegistryIdentifiers with correct registryId', () => { + fixture.detectChanges(); + + const call = (store.dispatch as jest.Mock).mock.calls.find((call) => call[0] instanceof GetRegistryIdentifiers); + expect(call).toBeDefined(); + const action = call[0] as GetRegistryIdentifiers; + expect(action.registryId).toBe('registry-123'); + }); + + it('should dispatch SetRegistryCustomCitation with citation', () => { + const citation = 'Custom Citation Text'; + component.onCustomCitationUpdated(citation); + + expect(store.dispatch).toHaveBeenCalledWith(expect.any(SetRegistryCustomCitation)); + const call = (store.dispatch as jest.Mock).mock.calls.find((call) => call[0] instanceof SetRegistryCustomCitation); + expect(call).toBeDefined(); + const action = call[0] as SetRegistryCustomCitation; + expect(action.citation).toBe(citation); + }); + + it('should dispatch LoadMoreBibliographicContributors with registry id', () => { + component.handleLoadMoreContributors(); + + expect(store.dispatch).toHaveBeenCalledWith(expect.any(LoadMoreBibliographicContributors)); + const call = (store.dispatch as jest.Mock).mock.calls.find( + (call) => call[0] instanceof LoadMoreBibliographicContributors + ); + expect(call).toBeDefined(); + const action = call[0] as LoadMoreBibliographicContributors; + expect(action.resourceId).toBe('registry-123'); + expect(action.resourceType).toBe(ResourceType.Registration); + }); + + it('should navigate to search page with tag as query param', () => { + const tag = 'test-tag'; + component.tagClicked(tag); + + expect(routerMock.navigate).toHaveBeenCalledWith(['/search'], { queryParams: { search: tag } }); }); }); diff --git a/src/app/features/registry/components/registry-revisions/registry-revisions.component.spec.ts b/src/app/features/registry/components/registry-revisions/registry-revisions.component.spec.ts index cc073e6a8..40458c7ac 100644 --- a/src/app/features/registry/components/registry-revisions/registry-revisions.component.spec.ts +++ b/src/app/features/registry/components/registry-revisions/registry-revisions.component.spec.ts @@ -2,33 +2,24 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { RegistrationReviewStates } from '@osf/shared/enums/registration-review-states.enum'; import { RevisionReviewStates } from '@osf/shared/enums/revision-review-states.enum'; +import { SchemaResponse } from '@osf/shared/models/registration/schema-response.model'; import { RegistryRevisionsComponent } from './registry-revisions.component'; -import { MOCK_REGISTRY_OVERVIEW } from '@testing/mocks/registry-overview.mock'; +import { MOCK_REGISTRATION_OVERVIEW_MODEL } from '@testing/mocks/registration-overview-model.mock'; +import { createMockSchemaResponse } from '@testing/mocks/schema-response.mock'; import { OSFTestingModule } from '@testing/osf.testing.module'; describe('RegistryRevisionsComponent', () => { let component: RegistryRevisionsComponent; let fixture: ComponentFixture; - const mockRegistry = { - ...MOCK_REGISTRY_OVERVIEW, - schemaResponses: [ - { - id: 'response-1', - reviewsState: RevisionReviewStates.Approved, - dateCreated: '2023-01-01T00:00:00Z', - dateModified: '2023-01-01T00:00:00Z', - }, - { - id: 'response-2', - reviewsState: RevisionReviewStates.Approved, - dateCreated: '2023-01-02T00:00:00Z', - dateModified: '2023-01-02T00:00:00Z', - }, - ], - }; + const mockRegistry = MOCK_REGISTRATION_OVERVIEW_MODEL; + + const mockSchemaResponses: SchemaResponse[] = [ + createMockSchemaResponse('response-1', RevisionReviewStates.Approved, false), + createMockSchemaResponse('response-2', RevisionReviewStates.Approved, true), + ]; beforeEach(async () => { await TestBed.configureTestingModule({ @@ -39,197 +30,324 @@ describe('RegistryRevisionsComponent', () => { component = fixture.componentInstance; fixture.componentRef.setInput('registry', mockRegistry); + fixture.componentRef.setInput('schemaResponses', mockSchemaResponses); fixture.componentRef.setInput('selectedRevisionIndex', 0); fixture.detectChanges(); }); - it('should create', () => { - expect(component).toBeTruthy(); - }); - - it('should initialize with default values', () => { + it('should initialize with default input values', () => { expect(component.isSubmitting()).toBe(false); expect(component.isModeration()).toBe(false); expect(component.canEdit()).toBe(false); expect(component.unApprovedRevisionId).toBe(null); }); + it('should expose RevisionReviewStates enum', () => { + expect(component.RevisionReviewStates).toBe(RevisionReviewStates); + }); + it('should receive required inputs', () => { expect(component.registry()).toEqual(mockRegistry); + expect(component.schemaResponses()).toEqual(mockSchemaResponses); expect(component.selectedRevisionIndex()).toBe(0); }); - it('should compute revisions correctly', () => { - const revisions = component.revisions(); + it('should update when registry input changes', () => { + const newRegistry = { ...mockRegistry, id: 'new-registry-id' }; + fixture.componentRef.setInput('registry', newRegistry); + fixture.detectChanges(); - expect(revisions).toHaveLength(2); - expect(revisions[0].index).toBe(0); - expect(revisions[0].isSelected).toBe(true); - expect(revisions[0].label).toBe('registry.overview.latest'); - expect(revisions[1].index).toBe(1); - expect(revisions[1].isSelected).toBe(false); - expect(revisions[1].label).toBe('registry.overview.original'); + expect(component.registry()).toEqual(newRegistry); }); - it('should compute revisions with single revision', () => { - const singleRevisionRegistry = { - ...mockRegistry, - schemaResponses: [mockRegistry.schemaResponses![0]], - }; - - fixture.componentRef.setInput('registry', singleRevisionRegistry); + it('should update when schemaResponses input changes', () => { + const newResponses = [createMockSchemaResponse('new-response', RevisionReviewStates.Approved)]; + fixture.componentRef.setInput('schemaResponses', newResponses); fixture.detectChanges(); - const revisions = component.revisions(); - expect(revisions).toHaveLength(1); - expect(revisions[0].label).toBe('registry.overview.original'); + expect(component.schemaResponses()).toEqual(newResponses); }); - it('should filter revisions in moderation mode', () => { - fixture.componentRef.setInput('isModeration', true); + it('should update when selectedRevisionIndex input changes', () => { + fixture.componentRef.setInput('selectedRevisionIndex', 1); fixture.detectChanges(); - const revisions = component.revisions(); - expect(revisions).toHaveLength(2); + expect(component.selectedRevisionIndex()).toBe(1); }); - it('should filter revisions in non-moderation mode', () => { - fixture.componentRef.setInput('isModeration', false); + it('should update isSubmitting input', () => { + fixture.componentRef.setInput('isSubmitting', true); fixture.detectChanges(); - const revisions = component.revisions(); - expect(revisions).toHaveLength(2); + expect(component.isSubmitting()).toBe(true); }); - it('should compute registryInProgress correctly', () => { - expect(component.registryInProgress).toBe(false); - - const inProgressRegistry = { ...mockRegistry, revisionStatus: RevisionReviewStates.RevisionInProgress }; - fixture.componentRef.setInput('registry', inProgressRegistry); + it('should update isModeration input', () => { + fixture.componentRef.setInput('isModeration', true); fixture.detectChanges(); - expect(component.registryInProgress).toBe(true); + expect(component.isModeration()).toBe(true); }); - it('should compute registryApproved correctly', () => { - expect(component.registryApproved).toBe(true); - - const notApprovedRegistry = { ...mockRegistry, revisionStatus: RevisionReviewStates.RevisionInProgress }; - fixture.componentRef.setInput('registry', notApprovedRegistry); + it('should update canEdit input', () => { + fixture.componentRef.setInput('canEdit', true); fixture.detectChanges(); - expect(component.registryApproved).toBe(false); + expect(component.canEdit()).toBe(true); }); - it('should compute registryAcceptedUnapproved correctly', () => { - expect(component.registryAcceptedUnapproved).toBe(false); + describe('registryInProgress', () => { + it('should return false when revisionState is not RevisionInProgress', () => { + expect(component.registryInProgress).toBe(false); + }); - const unapprovedRegistry = { - ...mockRegistry, - revisionStatus: RevisionReviewStates.Unapproved, - reviewsState: RegistrationReviewStates.Accepted, - }; - fixture.componentRef.setInput('registry', unapprovedRegistry); - fixture.detectChanges(); + it('should return true when revisionState is RevisionInProgress', () => { + const inProgressRegistry = { ...mockRegistry, revisionState: RevisionReviewStates.RevisionInProgress }; + fixture.componentRef.setInput('registry', inProgressRegistry); + fixture.detectChanges(); + + expect(component.registryInProgress).toBe(true); + }); - expect(component.registryAcceptedUnapproved).toBe(true); + it('should return false when registry is null', () => { + fixture.componentRef.setInput('registry', null); + fixture.detectChanges(); + + expect(component.registryInProgress).toBe(false); + }); }); - it('should emit openRevision when emitOpenRevision is called', () => { - const emitSpy = jest.fn(); - component.openRevision.subscribe(emitSpy); + describe('registryApproved', () => { + it('should return true when revisionState is Approved', () => { + expect(component.registryApproved).toBe(true); + }); + + it('should return false when revisionState is not Approved', () => { + const notApprovedRegistry = { ...mockRegistry, revisionState: RevisionReviewStates.RevisionInProgress }; + fixture.componentRef.setInput('registry', notApprovedRegistry); + fixture.detectChanges(); - component.emitOpenRevision(1); + expect(component.registryApproved).toBe(false); + }); - expect(emitSpy).toHaveBeenCalledWith(1); + it('should return false when registry is null', () => { + fixture.componentRef.setInput('registry', null); + fixture.detectChanges(); + + expect(component.registryApproved).toBe(false); + }); }); - it('should emit continueUpdate when continueUpdateHandler is called', () => { - const emitSpy = jest.fn(); - component.continueUpdate.subscribe(emitSpy); + describe('registryAcceptedUnapproved', () => { + it('should return false when conditions are not met', () => { + expect(component.registryAcceptedUnapproved).toBe(false); + }); - component.continueUpdateHandler(); + it('should return true when revisionState is Unapproved and reviewsState is Accepted', () => { + const unapprovedRegistry = { + ...mockRegistry, + revisionState: RevisionReviewStates.Unapproved, + reviewsState: RegistrationReviewStates.Accepted, + }; + fixture.componentRef.setInput('registry', unapprovedRegistry); + fixture.detectChanges(); - expect(emitSpy).toHaveBeenCalled(); - }); + expect(component.registryAcceptedUnapproved).toBe(true); + }); - it('should handle different input configurations', () => { - fixture.componentRef.setInput('isSubmitting', true); - fixture.componentRef.setInput('isModeration', true); - fixture.componentRef.setInput('canEdit', true); - fixture.componentRef.setInput('selectedRevisionIndex', 1); - fixture.detectChanges(); + it('should return false when revisionState is Unapproved but reviewsState is not Accepted', () => { + const unapprovedRegistry = { + ...mockRegistry, + revisionState: RevisionReviewStates.Unapproved, + reviewsState: RegistrationReviewStates.Pending, + }; + fixture.componentRef.setInput('registry', unapprovedRegistry); + fixture.detectChanges(); - expect(component.isSubmitting()).toBe(true); - expect(component.isModeration()).toBe(true); - expect(component.canEdit()).toBe(true); - expect(component.selectedRevisionIndex()).toBe(1); - }); + expect(component.registryAcceptedUnapproved).toBe(false); + }); - it('should handle registry with different revision statuses', () => { - const statuses = [ - RevisionReviewStates.Approved, - RevisionReviewStates.RevisionInProgress, - RevisionReviewStates.Unapproved, - ]; + it('should return false when reviewsState is Accepted but revisionState is not Unapproved', () => { + const approvedRegistry = { + ...mockRegistry, + revisionState: RevisionReviewStates.Approved, + reviewsState: RegistrationReviewStates.Accepted, + }; + fixture.componentRef.setInput('registry', approvedRegistry); + fixture.detectChanges(); - statuses.forEach((status) => { - const registryWithStatus = { ...mockRegistry, revisionStatus: status }; - fixture.componentRef.setInput('registry', registryWithStatus); + expect(component.registryAcceptedUnapproved).toBe(false); + }); + + it('should return false when registry is null', () => { + fixture.componentRef.setInput('registry', null); fixture.detectChanges(); - expect(component.registry()!.revisionStatus).toBe(status); + expect(component.registryAcceptedUnapproved).toBe(false); }); }); - it('should handle registry with different review states', () => { - const reviewStates = [ - RegistrationReviewStates.Pending, - RegistrationReviewStates.Accepted, - RegistrationReviewStates.Rejected, - ]; + describe('revisions computed signal', () => { + it('should return empty array when schemaResponses is empty', () => { + fixture.componentRef.setInput('schemaResponses', []); + fixture.detectChanges(); + + expect(component.revisions()).toHaveLength(0); + }); - reviewStates.forEach((reviewsState) => { - const registryWithReviewState = { ...mockRegistry, reviewsState }; - fixture.componentRef.setInput('registry', registryWithReviewState); + it('should handle null schemaResponses', () => { + fixture.componentRef.setInput('schemaResponses', null as any); fixture.detectChanges(); - expect(component.registry()!.reviewsState).toBe(reviewsState); + expect(component.revisions()).toHaveLength(0); }); - }); - it('should handle registry with mixed schema response states', () => { - const mixedRegistry = { - ...mockRegistry, - schemaResponses: [ - { - id: 'response-1', - reviewsState: RevisionReviewStates.Approved, - created: '2023-01-01T00:00:00Z', - modified: '2023-01-01T00:00:00Z', - }, - { - id: 'response-2', - reviewsState: RevisionReviewStates.Unapproved, - created: '2023-01-02T00:00:00Z', - modified: '2023-01-02T00:00:00Z', - }, - ], - }; - - fixture.componentRef.setInput('registry', mixedRegistry); - fixture.detectChanges(); + it('should assign "original" label when there is only one revision', () => { + const singleResponse = [createMockSchemaResponse('single', RevisionReviewStates.Approved, true)]; + fixture.componentRef.setInput('schemaResponses', singleResponse); + fixture.detectChanges(); - const revisions = component.revisions(); - expect(revisions).toHaveLength(1); - }); + const revisions = component.revisions(); + expect(revisions).toHaveLength(1); + expect(revisions[0].label).toBe('registry.overview.original'); + }); - it('should be reactive to input changes', () => { - fixture.componentRef.setInput('selectedRevisionIndex', 1); - fixture.detectChanges(); + it('should assign "latest" label to first revision when multiple revisions exist', () => { + const revisions = component.revisions(); + expect(revisions[0].label).toBe('registry.overview.latest'); + }); + + it('should assign "original" label to last revision when multiple revisions exist', () => { + const revisions = component.revisions(); + const lastIndex = revisions.length - 1; + expect(revisions[lastIndex].label).toBe('registry.overview.original'); + }); + + it('should mark revision as selected when index matches selectedRevisionIndex', () => { + fixture.componentRef.setInput('selectedRevisionIndex', 0); + fixture.detectChanges(); + + const revisions = component.revisions(); + expect(revisions[0].isSelected).toBe(true); + expect(revisions[1].isSelected).toBe(false); + }); + + it('should update isSelected when selectedRevisionIndex changes', () => { + fixture.componentRef.setInput('selectedRevisionIndex', 1); + fixture.detectChanges(); + + const revisions = component.revisions(); + expect(revisions[0].isSelected).toBe(false); + expect(revisions[1].isSelected).toBe(true); + }); + + it('should show all revisions when isModeration is true', () => { + const responsesWithUnapproved = [ + createMockSchemaResponse('response-1', RevisionReviewStates.Approved, false), + createMockSchemaResponse('response-2', RevisionReviewStates.Unapproved, false), + createMockSchemaResponse('response-3', RevisionReviewStates.Approved, true), + ]; + fixture.componentRef.setInput('schemaResponses', responsesWithUnapproved); + fixture.componentRef.setInput('isModeration', true); + fixture.detectChanges(); + + const revisions = component.revisions(); + expect(revisions).toHaveLength(3); + }); + + it('should filter to only Approved or isOriginalResponse when isModeration is false', () => { + const responsesWithUnapproved = [ + createMockSchemaResponse('response-1', RevisionReviewStates.Approved, false), + createMockSchemaResponse('response-2', RevisionReviewStates.Unapproved, false), + createMockSchemaResponse('response-3', RevisionReviewStates.Approved, true), + ]; + fixture.componentRef.setInput('schemaResponses', responsesWithUnapproved); + fixture.componentRef.setInput('isModeration', false); + fixture.detectChanges(); + + const revisions = component.revisions(); + expect(revisions).toHaveLength(2); + expect(revisions.every((r) => r.reviewsState === RevisionReviewStates.Approved || r.isOriginalResponse)).toBe( + true + ); + }); + + it('should include original response even if not approved when isModeration is false', () => { + const responsesWithUnapprovedOriginal = [ + createMockSchemaResponse('response-1', RevisionReviewStates.Approved, false), + createMockSchemaResponse('response-2', RevisionReviewStates.Unapproved, true), + ]; + fixture.componentRef.setInput('schemaResponses', responsesWithUnapprovedOriginal); + fixture.componentRef.setInput('isModeration', false); + fixture.detectChanges(); + + const revisions = component.revisions(); + expect(revisions).toHaveLength(2); + expect(revisions.some((r) => r.isOriginalResponse)).toBe(true); + }); + + it('should set unApprovedRevisionId when registryAcceptedUnapproved is true', () => { + const unapprovedResponse = createMockSchemaResponse('unapproved-id', RevisionReviewStates.Unapproved, false); + const responses = [ + createMockSchemaResponse('response-1', RevisionReviewStates.Approved, false), + unapprovedResponse, + ]; + const unapprovedRegistry = { + ...mockRegistry, + revisionState: RevisionReviewStates.Unapproved, + reviewsState: RegistrationReviewStates.Accepted, + }; + + fixture.componentRef.setInput('registry', unapprovedRegistry); + fixture.componentRef.setInput('schemaResponses', responses); + fixture.detectChanges(); + + expect(component.unApprovedRevisionId).toBe('unapproved-id'); + }); - const revisions = component.revisions(); - expect(revisions[0].isSelected).toBe(false); - expect(revisions[1].isSelected).toBe(true); + it('should set unApprovedRevisionId to null when no unapproved revision found', () => { + const responses = [ + createMockSchemaResponse('response-1', RevisionReviewStates.Approved, false), + createMockSchemaResponse('response-2', RevisionReviewStates.Approved, true), + ]; + const unapprovedRegistry = { + ...mockRegistry, + revisionState: RevisionReviewStates.Unapproved, + reviewsState: RegistrationReviewStates.Accepted, + }; + + fixture.componentRef.setInput('registry', unapprovedRegistry); + fixture.componentRef.setInput('schemaResponses', responses); + fixture.detectChanges(); + + expect(component.unApprovedRevisionId).toBe(null); + }); + + it('should not set unApprovedRevisionId when registryAcceptedUnapproved is false', () => { + const unapprovedResponse = createMockSchemaResponse('unapproved-id', RevisionReviewStates.Unapproved, false); + const responses = [ + createMockSchemaResponse('response-1', RevisionReviewStates.Approved, false), + unapprovedResponse, + ]; + + fixture.componentRef.setInput('schemaResponses', responses); + fixture.detectChanges(); + + expect(component.unApprovedRevisionId).toBe(null); + }); + + it('should assign correct indices to revisions', () => { + const revisions = component.revisions(); + revisions.forEach((revision, expectedIndex) => { + expect(revision.index).toBe(expectedIndex); + }); + }); + + it('should preserve all response properties in revisions', () => { + const revisions = component.revisions(); + expect(revisions[0].id).toBe(mockSchemaResponses[0].id); + expect(revisions[0].dateCreated).toBe(mockSchemaResponses[0].dateCreated); + expect(revisions[0].reviewsState).toBe(mockSchemaResponses[0].reviewsState); + }); }); }); diff --git a/src/app/features/registry/components/registry-statuses/registry-statuses.component.spec.ts b/src/app/features/registry/components/registry-statuses/registry-statuses.component.spec.ts index 00d18b3d6..d7ce9a8b5 100644 --- a/src/app/features/registry/components/registry-statuses/registry-statuses.component.spec.ts +++ b/src/app/features/registry/components/registry-statuses/registry-statuses.component.spec.ts @@ -1,14 +1,21 @@ +import { Store } from '@ngxs/store'; + import { MockProvider } from 'ng-mocks'; import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ENVIRONMENT } from '@core/provider/environment.provider'; import { RegistrationReviewStates } from '@osf/shared/enums/registration-review-states.enum'; +import { RegistryStatus } from '@osf/shared/enums/registry-status.enum'; +import { RevisionReviewStates } from '@osf/shared/enums/revision-review-states.enum'; import { CustomConfirmationService } from '@osf/shared/services/custom-confirmation.service'; import { CustomDialogService } from '@osf/shared/services/custom-dialog.service'; +import { MakePublic } from '../../store/registry'; + import { RegistryStatusesComponent } from './registry-statuses.component'; -import { MOCK_REGISTRY_OVERVIEW } from '@testing/mocks/registry-overview.mock'; +import { MOCK_REGISTRATION_OVERVIEW_MODEL } from '@testing/mocks/registration-overview-model.mock'; import { OSFTestingModule } from '@testing/osf.testing.module'; import { CustomConfirmationServiceMockBuilder } from '@testing/providers/custom-confirmation-provider.mock'; import { CustomDialogServiceMockBuilder } from '@testing/providers/custom-dialog-provider.mock'; @@ -19,8 +26,10 @@ describe('RegistryStatusesComponent', () => { let fixture: ComponentFixture; let mockCustomDialogService: ReturnType; let mockCustomConfirmationService: ReturnType; + let store: Store; - const mockRegistry = MOCK_REGISTRY_OVERVIEW; + const mockRegistry = { ...MOCK_REGISTRATION_OVERVIEW_MODEL, embargoEndDate: '2024-01-01T00:00:00Z' }; + const mockEnvironment = { supportEmail: 'support@osf.io' }; beforeEach(async () => { mockCustomDialogService = CustomDialogServiceMockBuilder.create().withDefaultOpen().build(); @@ -31,6 +40,7 @@ describe('RegistryStatusesComponent', () => { providers: [ MockProvider(CustomDialogService, mockCustomDialogService), MockProvider(CustomConfirmationService, mockCustomConfirmationService), + MockProvider(ENVIRONMENT, mockEnvironment), provideMockStore({ signals: [], }), @@ -39,103 +49,185 @@ describe('RegistryStatusesComponent', () => { fixture = TestBed.createComponent(RegistryStatusesComponent); component = fixture.componentInstance; + store = TestBed.inject(Store); fixture.componentRef.setInput('registry', mockRegistry); fixture.detectChanges(); }); - it('should create', () => { - expect(component).toBeTruthy(); - }); - - it('should initialize with default values', () => { + it('should initialize with default input values', () => { expect(component.canEdit()).toBe(false); expect(component.isModeration()).toBe(false); }); + it('should initialize supportEmail from environment', () => { + expect(component.supportEmail).toBe('support@osf.io'); + }); + + it('should expose RegistryStatus enum', () => { + expect(component.RegistryStatus).toBe(RegistryStatus); + }); + + it('should expose RevisionReviewStates enum', () => { + expect(component.RevisionReviewStates).toBe(RevisionReviewStates); + }); + it('should receive registry input', () => { expect(component.registry()).toEqual(mockRegistry); }); - it('should compute canWithdraw correctly', () => { - expect(component.canWithdraw()).toBe(true); - - const registryWithPendingReview = { ...mockRegistry, reviewsState: RegistrationReviewStates.Pending }; - fixture.componentRef.setInput('registry', registryWithPendingReview); + it('should update canEdit input', () => { + fixture.componentRef.setInput('canEdit', true); fixture.detectChanges(); - expect(component.canWithdraw()).toBe(false); + expect(component.canEdit()).toBe(true); + }); + + it('should update isModeration input', () => { fixture.componentRef.setInput('isModeration', true); fixture.detectChanges(); - expect(component.canWithdraw()).toBe(false); + + expect(component.isModeration()).toBe(true); }); - it('should compute isAccepted correctly', () => { - expect(component.isAccepted()).toBe(true); + describe('canWithdraw', () => { + it('should return true when reviewsState is Accepted and isModeration is false', () => { + expect(component.canWithdraw()).toBe(true); + }); - const registryWithDifferentStatus = { ...mockRegistry, status: RegistryStatus.Pending }; - fixture.componentRef.setInput('registry', registryWithDifferentStatus); - fixture.detectChanges(); - expect(component.isAccepted()).toBe(false); - }); + it('should return false when reviewsState is not Accepted', () => { + const registryWithPendingReview = { ...mockRegistry, reviewsState: RegistrationReviewStates.Pending }; + fixture.componentRef.setInput('registry', registryWithPendingReview); + fixture.detectChanges(); - it('should compute isEmbargo correctly', () => { - expect(component.isEmbargo()).toBe(false); + expect(component.canWithdraw()).toBe(false); + }); - const embargoRegistry = { ...mockRegistry, status: RegistryStatus.Embargo }; - fixture.componentRef.setInput('registry', embargoRegistry); - fixture.detectChanges(); - expect(component.isEmbargo()).toBe(true); - }); + it('should return false when isModeration is true', () => { + fixture.componentRef.setInput('isModeration', true); + fixture.detectChanges(); + + expect(component.canWithdraw()).toBe(false); + }); + + it('should return false when both conditions are not met', () => { + const registryWithPendingReview = { ...mockRegistry, reviewsState: RegistrationReviewStates.Pending }; + fixture.componentRef.setInput('registry', registryWithPendingReview); + fixture.componentRef.setInput('isModeration', true); + fixture.detectChanges(); - it('should format embargo end date correctly', () => { - expect(component.embargoEndDate).toBe('Mon Jan 01 2024'); + expect(component.canWithdraw()).toBe(false); + }); }); - it('should handle registry without embargo end date', () => { - const registryWithoutEmbargo = { ...mockRegistry, embargoEndDate: undefined }; - fixture.componentRef.setInput('registry', registryWithoutEmbargo); - fixture.detectChanges(); + describe('isAccepted', () => { + it('should return true when status is Accepted', () => { + expect(component.isAccepted()).toBe(true); + }); - expect(component.embargoEndDate).toBe(null); + it('should return false when status is not Accepted', () => { + const registryWithDifferentStatus = { ...mockRegistry, status: RegistryStatus.Pending }; + fixture.componentRef.setInput('registry', registryWithDifferentStatus); + fixture.detectChanges(); + + expect(component.isAccepted()).toBe(false); + }); + + it('should return false when status is Embargo', () => { + const embargoRegistry = { ...mockRegistry, status: RegistryStatus.Embargo }; + fixture.componentRef.setInput('registry', embargoRegistry); + fixture.detectChanges(); + + expect(component.isAccepted()).toBe(false); + }); }); - it('should open withdraw dialog', () => { - component.openWithdrawDialog(); + describe('isEmbargo', () => { + it('should return false when status is not Embargo', () => { + expect(component.isEmbargo()).toBe(false); + }); + + it('should return true when status is Embargo', () => { + const embargoRegistry = { ...mockRegistry, status: RegistryStatus.Embargo }; + fixture.componentRef.setInput('registry', embargoRegistry); + fixture.detectChanges(); - expect(mockCustomDialogService.open).toHaveBeenCalledWith(expect.any(Function), { - header: 'registry.overview.withdrawRegistration', - width: '552px', - data: { - registryId: 'test-registry-id', - }, + expect(component.isEmbargo()).toBe(true); }); }); - it('should not open withdraw dialog when registry is null', () => { - fixture.componentRef.setInput('registry', null); - fixture.detectChanges(); + describe('embargoEndDate getter', () => { + it('should format embargo end date correctly', () => { + const date = new Date('2024-01-01T00:00:00Z').toDateString(); + expect(component.embargoEndDate).toBe(date); + }); - component.openWithdrawDialog(); + it('should return null when registry has no embargo end date', () => { + const registryWithoutEmbargo = { ...mockRegistry, embargoEndDate: undefined }; + fixture.componentRef.setInput('registry', registryWithoutEmbargo); + fixture.detectChanges(); - expect(mockCustomDialogService.open).not.toHaveBeenCalled(); + expect(component.embargoEndDate).toBe(null); + }); }); - it('should not open end embargo dialog when registry is null', () => { - fixture.componentRef.setInput('registry', null); - fixture.detectChanges(); + describe('openWithdrawDialog', () => { + it('should open withdraw dialog with correct parameters', () => { + component.openWithdrawDialog(); + + expect(mockCustomDialogService.open).toHaveBeenCalledWith(expect.any(Function), { + header: 'registry.overview.withdrawRegistration', + width: '552px', + data: { + registryId: mockRegistry.id, + }, + }); + }); - component.openEndEmbargoDialog(); + it('should use correct registryId from registry', () => { + const registryWithDifferentId = { ...mockRegistry, id: 'different-registry-id' }; + fixture.componentRef.setInput('registry', registryWithDifferentId); + fixture.detectChanges(); - expect(mockCustomConfirmationService.confirmDelete).not.toHaveBeenCalled(); + component.openWithdrawDialog(); + + expect(mockCustomDialogService.open).toHaveBeenCalledWith(expect.any(Function), { + header: 'registry.overview.withdrawRegistration', + width: '552px', + data: { + registryId: 'different-registry-id', + }, + }); + }); }); - it('should handle different input configurations', () => { - fixture.componentRef.setInput('canEdit', true); - fixture.componentRef.setInput('isModeration', true); - fixture.detectChanges(); + describe('openEndEmbargoDialog', () => { + it('should call confirmDelete with correct parameters', () => { + component.openEndEmbargoDialog(); - expect(component.canEdit()).toBe(true); - expect(component.isModeration()).toBe(true); + expect(mockCustomConfirmationService.confirmDelete).toHaveBeenCalledWith({ + headerKey: 'registry.overview.endEmbargo', + messageKey: 'registry.overview.endEmbargoMessage', + acceptLabelKey: 'common.buttons.confirm', + onConfirm: expect.any(Function), + }); + }); + + it('should dispatch MakePublic with correct registryId', () => { + const dispatchSpy = jest.spyOn(store, 'dispatch'); + const registryWithDifferentId = { ...mockRegistry, id: 'different-registry-id' }; + fixture.componentRef.setInput('registry', registryWithDifferentId); + fixture.detectChanges(); + + let onConfirmCallback: () => void; + (mockCustomConfirmationService.confirmDelete as jest.Mock).mockImplementation((options) => { + onConfirmCallback = options.onConfirm; + }); + + component.openEndEmbargoDialog(); + onConfirmCallback!(); + + expect(dispatchSpy).toHaveBeenCalledWith(new MakePublic('different-registry-id')); + }); }); }); diff --git a/src/app/features/registry/components/resource-form/resource-form.component.spec.ts b/src/app/features/registry/components/resource-form/resource-form.component.spec.ts index dca4279b0..7bbfcbc13 100644 --- a/src/app/features/registry/components/resource-form/resource-form.component.spec.ts +++ b/src/app/features/registry/components/resource-form/resource-form.component.spec.ts @@ -1,11 +1,15 @@ -import { MockComponents } from 'ng-mocks'; +import { MockComponents, MockDirective } from 'ng-mocks'; + +import { Textarea } from 'primeng/textarea'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { FormControl, FormGroup } from '@angular/forms'; import { FormSelectComponent } from '@osf/shared/components/form-select/form-select.component'; import { TextInputComponent } from '@osf/shared/components/text-input/text-input.component'; +import { InputLimits } from '@osf/shared/constants/input-limits.const'; +import { resourceTypeOptions } from '../../constants'; import { RegistryResourceFormModel } from '../../models'; import { ResourceFormComponent } from './resource-form.component'; @@ -24,7 +28,12 @@ describe('ResourceFormComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [ResourceFormComponent, OSFTestingModule, ...MockComponents(TextInputComponent, FormSelectComponent)], + imports: [ + ResourceFormComponent, + OSFTestingModule, + ...MockComponents(TextInputComponent, FormSelectComponent), + MockDirective(Textarea), + ], }).compileComponents(); fixture = TestBed.createComponent(ResourceFormComponent); @@ -34,105 +43,84 @@ describe('ResourceFormComponent', () => { fixture.detectChanges(); }); - it('should create', () => { - expect(component).toBeTruthy(); + it('should initialize inputLimits with InputLimits constant', () => { + expect(component.inputLimits).toBe(InputLimits); + }); + + it('should initialize resourceOptions signal with resourceTypeOptions', () => { + expect(component.resourceOptions()).toEqual(resourceTypeOptions); }); - it('should initialize with default values', () => { + it('should initialize with default input values', () => { expect(component.showCancelButton()).toBe(true); expect(component.showPreviewButton()).toBe(false); expect(component.cancelButtonLabel()).toBe('common.buttons.cancel'); expect(component.primaryButtonLabel()).toBe('common.buttons.save'); }); - it('should receive formGroup input', () => { + it('should receive and store formGroup input', () => { expect(component.formGroup()).toBe(mockFormGroup); }); - it('should get control by name', () => { - const pidControl = component.getControl('pid'); - const resourceTypeControl = component.getControl('resourceType'); - const descriptionControl = component.getControl('description'); - - expect(pidControl).toBe(mockFormGroup.get('pid')); - expect(resourceTypeControl).toBe(mockFormGroup.get('resourceType')); - expect(descriptionControl).toBe(mockFormGroup.get('description')); - }); - - it('should emit cancelClicked when handleCancel is called', () => { - const cancelSpy = jest.fn(); - component.cancelClicked.subscribe(cancelSpy); + it('should update when formGroup input changes', () => { + const newFormGroup = new FormGroup({ + pid: new FormControl('10.1234/new-test'), + resourceType: new FormControl('software'), + description: new FormControl('New description'), + }); - component.handleCancel(); + fixture.componentRef.setInput('formGroup', newFormGroup); + fixture.detectChanges(); - expect(cancelSpy).toHaveBeenCalled(); + expect(component.formGroup()).toBe(newFormGroup); }); - it('should emit submitClicked when handleSubmit is called', () => { - const submitSpy = jest.fn(); - component.submitClicked.subscribe(submitSpy); + it('should return correct control for pid', () => { + const control = component.getControl('pid'); + expect(control).toBe(mockFormGroup.get('pid')); + expect(control?.value).toBe('10.1234/test'); + }); - component.handleSubmit(); + it('should update showCancelButton input', () => { + fixture.componentRef.setInput('showCancelButton', false); + fixture.detectChanges(); - expect(submitSpy).toHaveBeenCalled(); + expect(component.showCancelButton()).toBe(false); }); - it('should handle different input configurations', () => { - fixture.componentRef.setInput('showCancelButton', false); + it('should update showPreviewButton input', () => { fixture.componentRef.setInput('showPreviewButton', true); - fixture.componentRef.setInput('cancelButtonLabel', 'Custom Cancel'); - fixture.componentRef.setInput('primaryButtonLabel', 'Custom Save'); fixture.detectChanges(); - expect(component.showCancelButton()).toBe(false); expect(component.showPreviewButton()).toBe(true); - expect(component.cancelButtonLabel()).toBe('Custom Cancel'); - expect(component.primaryButtonLabel()).toBe('Custom Save'); }); - it('should handle form with different values', () => { - const newFormGroup = new FormGroup({ - pid: new FormControl('10.1234/new-test'), - resourceType: new FormControl('software'), - description: new FormControl('New description'), - }); - - fixture.componentRef.setInput('formGroup', newFormGroup); + it('should update cancelButtonLabel input', () => { + const customLabel = 'Custom Cancel'; + fixture.componentRef.setInput('cancelButtonLabel', customLabel); fixture.detectChanges(); - expect(component.formGroup()).toBe(newFormGroup); - expect(component.getControl('pid')?.value).toBe('10.1234/new-test'); - expect(component.getControl('resourceType')?.value).toBe('software'); - expect(component.getControl('description')?.value).toBe('New description'); + expect(component.cancelButtonLabel()).toBe(customLabel); }); - it('should handle form with empty values', () => { - const emptyFormGroup = new FormGroup({ - pid: new FormControl(''), - resourceType: new FormControl(''), - description: new FormControl(''), - }); - - fixture.componentRef.setInput('formGroup', emptyFormGroup); + it('should update primaryButtonLabel input', () => { + const customLabel = 'Custom Save'; + fixture.componentRef.setInput('primaryButtonLabel', customLabel); fixture.detectChanges(); - expect(component.getControl('pid')?.value).toBe(''); - expect(component.getControl('resourceType')?.value).toBe(''); - expect(component.getControl('description')?.value).toBe(''); + expect(component.primaryButtonLabel()).toBe(customLabel); }); - it('should handle form with null values', () => { - const nullFormGroup = new FormGroup({ - pid: new FormControl(null), - resourceType: new FormControl(null), - description: new FormControl(null), - }); - - fixture.componentRef.setInput('formGroup', nullFormGroup); + it('should handle multiple input updates simultaneously', () => { + fixture.componentRef.setInput('showCancelButton', false); + fixture.componentRef.setInput('showPreviewButton', true); + fixture.componentRef.setInput('cancelButtonLabel', 'Custom Cancel'); + fixture.componentRef.setInput('primaryButtonLabel', 'Custom Save'); fixture.detectChanges(); - expect(component.getControl('pid')?.value).toBe(null); - expect(component.getControl('resourceType')?.value).toBe(null); - expect(component.getControl('description')?.value).toBe(null); + expect(component.showCancelButton()).toBe(false); + expect(component.showPreviewButton()).toBe(true); + expect(component.cancelButtonLabel()).toBe('Custom Cancel'); + expect(component.primaryButtonLabel()).toBe('Custom Save'); }); }); diff --git a/src/app/features/registry/components/short-registration-info/short-registration-info.component.spec.ts b/src/app/features/registry/components/short-registration-info/short-registration-info.component.spec.ts index 12209c783..cb5c8c3e3 100644 --- a/src/app/features/registry/components/short-registration-info/short-registration-info.component.spec.ts +++ b/src/app/features/registry/components/short-registration-info/short-registration-info.component.spec.ts @@ -1,73 +1,113 @@ +import { Store } from '@ngxs/store'; + import { MockComponent } from 'ng-mocks'; +import { of } from 'rxjs'; + +import { signal } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ENVIRONMENT } from '@core/provider/environment.provider'; import { ContributorsListComponent } from '@osf/shared/components/contributors-list/contributors-list.component'; -import { RegistryStatus } from '@osf/shared/enums/registry-status.enum'; +import { ResourceType } from '@osf/shared/enums/resource-type.enum'; +import { ContributorsSelectors, LoadMoreBibliographicContributors } from '@osf/shared/stores/contributors'; -import { RegistryOverview } from '../../models'; +import { RegistrationOverviewModel } from '../../models'; import { ShortRegistrationInfoComponent } from './short-registration-info.component'; -import { MOCK_REGISTRY_OVERVIEW } from '@testing/mocks/registry-overview.mock'; +import { MOCK_REGISTRATION_OVERVIEW_MODEL } from '@testing/mocks/registration-overview-model.mock'; import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideMockStore } from '@testing/providers/store-provider.mock'; describe('ShortRegistrationInfoComponent', () => { let component: ShortRegistrationInfoComponent; let fixture: ComponentFixture; + let store: Store; - const mockRegistration: RegistryOverview = MOCK_REGISTRY_OVERVIEW; + const mockRegistration: RegistrationOverviewModel = MOCK_REGISTRATION_OVERVIEW_MODEL; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [ShortRegistrationInfoComponent, OSFTestingModule, MockComponent(ContributorsListComponent)], + providers: [ + provideMockStore({ + signals: [ + { selector: ContributorsSelectors.getBibliographicContributors, value: signal([]) }, + { selector: ContributorsSelectors.isBibliographicContributorsLoading, value: signal(false) }, + { selector: ContributorsSelectors.hasMoreBibliographicContributors, value: signal(false) }, + ], + }), + ], }).compileComponents(); fixture = TestBed.createComponent(ShortRegistrationInfoComponent); component = fixture.componentInstance; + store = TestBed.inject(Store); fixture.componentRef.setInput('registration', mockRegistration); fixture.detectChanges(); }); - it('should create', () => { - expect(component).toBeTruthy(); + it('should receive registration input', () => { + expect(component.registration()).toEqual(mockRegistration); }); - it('should handle different registration data', () => { - const differentRegistration: RegistryOverview = { + it('should compute associatedProjectUrl from registration and environment', () => { + const environment = TestBed.inject(ENVIRONMENT); + const expectedUrl = `${environment.webUrl}/${mockRegistration.associatedProjectId}`; + + expect(component.associatedProjectUrl()).toBe(expectedUrl); + }); + + it('should update associatedProjectUrl when registration changes', () => { + const environment = TestBed.inject(ENVIRONMENT); + const newRegistration: RegistrationOverviewModel = { ...mockRegistration, - title: 'Different Title', - status: RegistryStatus.Pending, - associatedProjectId: 'different-project-id', + associatedProjectId: 'new-project-id', }; - fixture.componentRef.setInput('registration', differentRegistration); + fixture.componentRef.setInput('registration', newRegistration); fixture.detectChanges(); - expect(component.registration().title).toBe('Different Title'); - expect(component.registration().status).toBe('pending'); - expect(component.associatedProjectUrl).toContain('different-project-id'); + expect(component.associatedProjectUrl()).toBe(`${environment.webUrl}/new-project-id`); + }); + + it('should have bibliographicContributors signal', () => { + expect(component.bibliographicContributors).toBeDefined(); + expect(typeof component.bibliographicContributors).toBe('function'); }); - it('should handle registration with minimal data', () => { - const minimalRegistration: RegistryOverview = { - id: 'minimal-id', - title: 'Minimal Title', - status: 'pending', - associatedProjectId: 'minimal-project-id', - } as RegistryOverview; + it('should have isBibliographicContributorsLoading signal', () => { + expect(component.isBibliographicContributorsLoading).toBeDefined(); + expect(typeof component.isBibliographicContributorsLoading).toBe('function'); + }); + + it('should have hasMoreBibliographicContributors signal', () => { + expect(component.hasMoreBibliographicContributors).toBeDefined(); + expect(typeof component.hasMoreBibliographicContributors).toBe('function'); + }); - fixture.componentRef.setInput('registration', minimalRegistration); + it('should dispatch LoadMoreBibliographicContributors action with correct parameters', () => { + const dispatchSpy = jest.spyOn(store, 'dispatch').mockReturnValue(of()); + const registrationId = 'test-registration-id'; + const registrationWithId: RegistrationOverviewModel = { + ...mockRegistration, + id: registrationId, + }; + + fixture.componentRef.setInput('registration', registrationWithId); fixture.detectChanges(); - expect(component.registration().id).toBe('minimal-id'); - expect(component.registration().title).toBe('Minimal Title'); - expect(component.associatedProjectUrl).toContain('minimal-project-id'); + component.handleLoadMoreContributors(); + + expect(dispatchSpy).toHaveBeenCalledWith( + new LoadMoreBibliographicContributors(registrationId, ResourceType.Registration) + ); }); it('should be reactive to registration input changes', () => { - const updatedRegistration = { + const updatedRegistration: RegistrationOverviewModel = { ...mockRegistration, title: 'Updated Title', associatedProjectId: 'updated-project-id', @@ -77,6 +117,6 @@ describe('ShortRegistrationInfoComponent', () => { fixture.detectChanges(); expect(component.registration().title).toBe('Updated Title'); - expect(component.associatedProjectUrl).toContain('updated-project-id'); + expect(component.registration().associatedProjectId).toBe('updated-project-id'); }); }); diff --git a/src/app/features/registry/components/withdrawn-message/withdrawn-message.component.spec.ts b/src/app/features/registry/components/withdrawn-message/withdrawn-message.component.spec.ts index fd5edd72f..25454c3f5 100644 --- a/src/app/features/registry/components/withdrawn-message/withdrawn-message.component.spec.ts +++ b/src/app/features/registry/components/withdrawn-message/withdrawn-message.component.spec.ts @@ -4,19 +4,24 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { IconComponent } from '@osf/shared/components/icon/icon.component'; -import { RegistryOverview } from '../../models'; +import { RegistrationOverviewModel } from '../../models'; import { ShortRegistrationInfoComponent } from '../short-registration-info/short-registration-info.component'; import { WithdrawnMessageComponent } from './withdrawn-message.component'; -import { MOCK_REGISTRY_OVERVIEW } from '@testing/mocks/registry-overview.mock'; +import { MOCK_REGISTRATION_OVERVIEW_MODEL } from '@testing/mocks/registration-overview-model.mock'; import { OSFTestingModule } from '@testing/osf.testing.module'; describe('WithdrawnMessageComponent', () => { let component: WithdrawnMessageComponent; let fixture: ComponentFixture; - const mockRegistration: RegistryOverview = MOCK_REGISTRY_OVERVIEW; + const mockRegistration: RegistrationOverviewModel = { + ...MOCK_REGISTRATION_OVERVIEW_MODEL, + dateWithdrawn: '2023-06-15T10:30:00Z', + withdrawalJustification: 'Test withdrawal justification', + withdrawn: true, + }; beforeEach(async () => { await TestBed.configureTestingModule({ @@ -42,57 +47,65 @@ describe('WithdrawnMessageComponent', () => { expect(component.registration()).toEqual(mockRegistration); }); - it('should handle different registration statuses', () => { - const statuses = ['withdrawn', 'pending', 'approved', 'rejected']; + it('should be reactive to registration input changes', () => { + const updatedRegistration: RegistrationOverviewModel = { + ...mockRegistration, + dateWithdrawn: '2024-01-20T12:00:00Z', + withdrawalJustification: 'Updated justification', + }; - statuses.forEach((status) => { - const registrationWithStatus = { ...mockRegistration, status }; - fixture.componentRef.setInput('registration', registrationWithStatus); - fixture.detectChanges(); + fixture.componentRef.setInput('registration', updatedRegistration); + fixture.detectChanges(); - expect(component.registration().status).toBe(status); - }); + expect(component.registration().dateWithdrawn).toBe('2024-01-20T12:00:00Z'); + expect(component.registration().withdrawalJustification).toBe('Updated justification'); }); - it('should handle registration with different properties', () => { - const complexRegistration: RegistryOverview = { + it('should handle registration with null withdrawal date', () => { + const registrationWithNullDate: RegistrationOverviewModel = { ...mockRegistration, - title: 'Complex Registration Title', - description: 'A very detailed description of the registration', - doi: '10.1234/complex-test', - iaUrl: 'https://example.com/complex-test', + dateWithdrawn: null, }; - fixture.componentRef.setInput('registration', complexRegistration); + fixture.componentRef.setInput('registration', registrationWithNullDate); fixture.detectChanges(); - expect(component.registration().title).toBe('Complex Registration Title'); - expect(component.registration().description).toBe('A very detailed description of the registration'); - expect(component.registration().doi).toBe('10.1234/complex-test'); - expect(component.registration().iaUrl).toBe('https://example.com/complex-test'); + expect(component.registration().dateWithdrawn).toBeNull(); }); - it('should handle registration with minimal data', () => { - const minimalRegistration: RegistryOverview = { - id: 'minimal-id', - title: 'Minimal Title', - status: 'withdrawn', - } as RegistryOverview; + it('should handle different withdrawal dates', () => { + const dates = ['2023-01-01T00:00:00Z', '2023-12-31T23:59:59Z', '2024-06-15T10:30:00Z']; - fixture.componentRef.setInput('registration', minimalRegistration); - fixture.detectChanges(); + dates.forEach((date) => { + const registrationWithDate: RegistrationOverviewModel = { + ...mockRegistration, + dateWithdrawn: date, + }; - expect(component.registration().id).toBe('minimal-id'); - expect(component.registration().title).toBe('Minimal Title'); - expect(component.registration().status).toBe('withdrawn'); + fixture.componentRef.setInput('registration', registrationWithDate); + fixture.detectChanges(); + + expect(component.registration().dateWithdrawn).toBe(date); + }); }); - it('should be reactive to registration input changes', () => { - const updatedRegistration = { ...mockRegistration, title: 'Updated Title' }; + it('should handle different withdrawal justifications', () => { + const justifications = [ + 'Short justification', + 'Very long justification that contains multiple sentences and provides detailed explanation of why the registration was withdrawn.', + '', + ]; - fixture.componentRef.setInput('registration', updatedRegistration); - fixture.detectChanges(); + justifications.forEach((justification) => { + const registrationWithJustification: RegistrationOverviewModel = { + ...mockRegistration, + withdrawalJustification: justification, + }; + + fixture.componentRef.setInput('registration', registrationWithJustification); + fixture.detectChanges(); - expect(component.registration().title).toBe('Updated Title'); + expect(component.registration().withdrawalJustification).toBe(justification); + }); }); }); diff --git a/src/app/features/registry/models/index.ts b/src/app/features/registry/models/index.ts index 71efab673..af2646b36 100644 --- a/src/app/features/registry/models/index.ts +++ b/src/app/features/registry/models/index.ts @@ -1,9 +1,9 @@ -export * from './linked-nodes.models'; +export * from './linked-nodes.model'; export * from './linked-nodes-json-api.model'; export * from './linked-registrations-json-api.model'; -export * from './linked-response.models'; +export * from './linked-response.model'; export * from './registration-overview-json-api.model'; -export * from './registry-components.models'; +export * from './registry-components.model'; export * from './registry-components-json-api.model'; -export * from './registry-overview.models'; +export * from './registry-overview.model'; export * from './resources'; diff --git a/src/app/features/registry/models/linked-nodes.models.ts b/src/app/features/registry/models/linked-nodes.model.ts similarity index 100% rename from src/app/features/registry/models/linked-nodes.models.ts rename to src/app/features/registry/models/linked-nodes.model.ts diff --git a/src/app/features/registry/models/linked-response.models.ts b/src/app/features/registry/models/linked-response.model.ts similarity index 98% rename from src/app/features/registry/models/linked-response.models.ts rename to src/app/features/registry/models/linked-response.model.ts index 8a3afd70d..e689f15f5 100644 --- a/src/app/features/registry/models/linked-response.models.ts +++ b/src/app/features/registry/models/linked-response.model.ts @@ -1,6 +1,6 @@ import { MetaJsonApi } from '@osf/shared/models/common/json-api.model'; -import { LinkedNode, LinkedRegistration } from './linked-nodes.models'; +import { LinkedNode, LinkedRegistration } from './linked-nodes.model'; export interface LinkedNodesResponseJsonApi { data: LinkedNode[]; diff --git a/src/app/features/registry/models/registry-components-json-api.model.ts b/src/app/features/registry/models/registry-components-json-api.model.ts index 766b24e35..12f05b6dc 100644 --- a/src/app/features/registry/models/registry-components-json-api.model.ts +++ b/src/app/features/registry/models/registry-components-json-api.model.ts @@ -2,7 +2,7 @@ import { MetaJsonApi } from '@osf/shared/models/common/json-api.model'; import { ContributorDataJsonApi } from '@osf/shared/models/contributors/contributor-response-json-api.model'; import { RegistrationNodeAttributesJsonApi } from '@osf/shared/models/registration/registration-node-json-api.model'; -import { RegistryComponentModel } from './registry-components.models'; +import { RegistryComponentModel } from './registry-components.model'; export interface RegistryComponentJsonApi { id: string; diff --git a/src/app/features/registry/models/registry-components.models.ts b/src/app/features/registry/models/registry-components.model.ts similarity index 100% rename from src/app/features/registry/models/registry-components.models.ts rename to src/app/features/registry/models/registry-components.model.ts diff --git a/src/app/features/registry/models/registry-overview.model.ts b/src/app/features/registry/models/registry-overview.model.ts new file mode 100644 index 000000000..957feafb5 --- /dev/null +++ b/src/app/features/registry/models/registry-overview.model.ts @@ -0,0 +1,18 @@ +import { RegistryStatus } from '@osf/shared/enums/registry-status.enum'; +import { MetaJsonApi } from '@shared/models/common/json-api.model'; +import { RegistrationNodeModel } from '@shared/models/registration/registration-node.model'; + +export interface RegistrationOverviewModel extends RegistrationNodeModel { + registrationSchemaLink: string; + licenseId: string; + associatedProjectId: string; + providerId: string; + status: RegistryStatus; + forksCount: number; + rootParentId?: string; +} + +export interface RegistryOverviewWithMeta { + registry: RegistrationOverviewModel; + meta?: MetaJsonApi; +} diff --git a/src/app/features/registry/models/registry-overview.models.ts b/src/app/features/registry/models/registry-overview.models.ts deleted file mode 100644 index 7b10e892f..000000000 --- a/src/app/features/registry/models/registry-overview.models.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { RegistrationReviewStates } from '@osf/shared/enums/registration-review-states.enum'; -import { RegistryStatus } from '@osf/shared/enums/registry-status.enum'; -import { RevisionReviewStates } from '@osf/shared/enums/revision-review-states.enum'; -import { UserPermissions } from '@osf/shared/enums/user-permissions.enum'; -import { IdTypeModel } from '@shared/models/common/id-type.model'; -import { MetaJsonApi } from '@shared/models/common/json-api.model'; -import { ContributorModel } from '@shared/models/contributors/contributor.model'; -import { IdentifierModel } from '@shared/models/identifiers/identifier.model'; -import { LicenseModel, LicensesOption } from '@shared/models/license/license.model'; -import { ProviderShortInfoModel } from '@shared/models/provider/provider.model'; -import { RegistrationNodeModel, RegistrationResponses } from '@shared/models/registration/registration-node.model'; -import { SchemaResponse } from '@shared/models/registration/schema-response.model'; -import { SubjectModel } from '@shared/models/subject/subject.model'; - -export interface RegistryOverview { - id: string; - type: string; - isPublic: boolean; - forksCount: number; - title: string; - description: string; - dateModified: string; - dateCreated: string; - dateRegistered?: string; - registrationType: string; - doi: string; - tags: string[]; - provider?: ProviderShortInfoModel; - contributors: ContributorModel[]; - citation: string; - category: string; - isFork: boolean; - accessRequestsEnabled: boolean; - nodeLicense?: LicensesOption; - license?: LicenseModel; - licenseUrl?: string; - identifiers?: IdentifierModel[]; - analyticsKey: string; - currentUserCanComment: boolean; - currentUserPermissions: UserPermissions[]; - currentUserIsContributor: boolean; - currentUserIsContributorOrGroupMember: boolean; - wikiEnabled: boolean; - region?: IdTypeModel; - subjects?: SubjectModel[]; - customCitation: string; - hasData: boolean; - hasAnalyticCode: boolean; - hasMaterials: boolean; - hasPapers: boolean; - hasSupplements: boolean; - questions: RegistrationResponses; - registrationSchemaLink: string; - associatedProjectId: string; - schemaResponses: SchemaResponse[]; - status: RegistryStatus; - revisionStatus: RevisionReviewStates; - reviewsState?: RegistrationReviewStates; - archiving: boolean; - embargoEndDate: string; - withdrawn: boolean; - withdrawalJustification?: string; - dateWithdrawn: string | null; - rootParentId: string | null; - iaUrl: string | null; -} - -export interface RegistrationOverviewModel extends RegistrationNodeModel { - registrationSchemaLink: string; - licenseId: string; - associatedProjectId: string; - providerId: string; - status: RegistryStatus; - forksCount: number; - rootParentId?: string; -} - -export interface RegistryOverviewWithMeta { - registry: RegistrationOverviewModel; - meta?: MetaJsonApi; -} diff --git a/src/app/features/registry/pages/registry-components/registry-components.component.ts b/src/app/features/registry/pages/registry-components/registry-components.component.ts index 55c7ec4ce..f4a9c8c31 100644 --- a/src/app/features/registry/pages/registry-components/registry-components.component.ts +++ b/src/app/features/registry/pages/registry-components/registry-components.component.ts @@ -10,7 +10,7 @@ import { SubHeaderComponent } from '@osf/shared/components/sub-header/sub-header import { ViewOnlyLinkMessageComponent } from '@osf/shared/components/view-only-link-message/view-only-link-message.component'; import { hasViewOnlyParam } from '@osf/shared/helpers/view-only.helper'; -import { RegistrationLinksCardComponent } from '../../components'; +import { RegistrationLinksCardComponent } from '../../components/registration-links-card/registration-links-card.component'; import { GetRegistryComponents, RegistryComponentsSelectors } from '../../store/registry-components'; @Component({ diff --git a/src/app/features/registry/pages/registry-links/registry-links.component.spec.ts b/src/app/features/registry/pages/registry-links/registry-links.component.spec.ts index 0be278e44..cbf35df47 100644 --- a/src/app/features/registry/pages/registry-links/registry-links.component.spec.ts +++ b/src/app/features/registry/pages/registry-links/registry-links.component.spec.ts @@ -8,7 +8,7 @@ import { LoadingSpinnerComponent } from '@osf/shared/components/loading-spinner/ import { SubHeaderComponent } from '@osf/shared/components/sub-header/sub-header.component'; import { LoaderService } from '@osf/shared/services/loader.service'; -import { RegistrationLinksCardComponent } from '../../components'; +import { RegistrationLinksCardComponent } from '../../components/registration-links-card/registration-links-card.component'; import { RegistryLinksSelectors } from '../../store/registry-links'; import { RegistryLinksComponent } from './registry-links.component'; diff --git a/src/app/features/registry/pages/registry-links/registry-links.component.ts b/src/app/features/registry/pages/registry-links/registry-links.component.ts index 7f65e86dc..660acd02b 100644 --- a/src/app/features/registry/pages/registry-links/registry-links.component.ts +++ b/src/app/features/registry/pages/registry-links/registry-links.component.ts @@ -12,7 +12,7 @@ import { LoadingSpinnerComponent } from '@osf/shared/components/loading-spinner/ import { SubHeaderComponent } from '@osf/shared/components/sub-header/sub-header.component'; import { LoaderService } from '@osf/shared/services/loader.service'; -import { RegistrationLinksCardComponent } from '../../components'; +import { RegistrationLinksCardComponent } from '../../components/registration-links-card/registration-links-card.component'; import { GetLinkedNodes, GetLinkedRegistrations, RegistryLinksSelectors } from '../../store/registry-links'; @Component({ diff --git a/src/app/features/registry/pages/registry-overview/registry-overview.component.spec.ts b/src/app/features/registry/pages/registry-overview/registry-overview.component.spec.ts index 6176bbe3f..506a9108d 100644 --- a/src/app/features/registry/pages/registry-overview/registry-overview.component.spec.ts +++ b/src/app/features/registry/pages/registry-overview/registry-overview.component.spec.ts @@ -8,14 +8,12 @@ import { SubHeaderComponent } from '@osf/shared/components/sub-header/sub-header import { ViewOnlyLinkMessageComponent } from '@osf/shared/components/view-only-link-message/view-only-link-message.component'; import { CustomDialogService } from '@osf/shared/services/custom-dialog.service'; -import { - ArchivingMessageComponent, - RegistryBlocksSectionComponent, - RegistryRevisionsComponent, - RegistryStatusesComponent, -} from '../../components'; +import { ArchivingMessageComponent } from '../../components/archiving-message/archiving-message.component'; import { RegistrationOverviewToolbarComponent } from '../../components/registration-overview-toolbar/registration-overview-toolbar.component'; +import { RegistryBlocksSectionComponent } from '../../components/registry-blocks-section/registry-blocks-section.component'; import { RegistryOverviewMetadataComponent } from '../../components/registry-overview-metadata/registry-overview-metadata.component'; +import { RegistryRevisionsComponent } from '../../components/registry-revisions/registry-revisions.component'; +import { RegistryStatusesComponent } from '../../components/registry-statuses/registry-statuses.component'; import { WithdrawnMessageComponent } from '../../components/withdrawn-message/withdrawn-message.component'; import { RegistrySelectors } from '../../store/registry'; diff --git a/src/app/features/registry/pages/registry-overview/registry-overview.component.ts b/src/app/features/registry/pages/registry-overview/registry-overview.component.ts index 2638f3f5a..168adc55e 100644 --- a/src/app/features/registry/pages/registry-overview/registry-overview.component.ts +++ b/src/app/features/registry/pages/registry-overview/registry-overview.component.ts @@ -36,15 +36,13 @@ import { GetBookmarksCollectionId } from '@osf/shared/stores/bookmarks'; import { ContributorsSelectors, GetBibliographicContributors } from '@osf/shared/stores/contributors'; import { SchemaResponse } from '@shared/models/registration/schema-response.model'; -import { - ArchivingMessageComponent, - RegistryBlocksSectionComponent, - RegistryRevisionsComponent, - RegistryStatusesComponent, -} from '../../components'; +import { ArchivingMessageComponent } from '../../components/archiving-message/archiving-message.component'; import { RegistrationOverviewToolbarComponent } from '../../components/registration-overview-toolbar/registration-overview-toolbar.component'; +import { RegistryBlocksSectionComponent } from '../../components/registry-blocks-section/registry-blocks-section.component'; import { RegistryMakeDecisionComponent } from '../../components/registry-make-decision/registry-make-decision.component'; import { RegistryOverviewMetadataComponent } from '../../components/registry-overview-metadata/registry-overview-metadata.component'; +import { RegistryRevisionsComponent } from '../../components/registry-revisions/registry-revisions.component'; +import { RegistryStatusesComponent } from '../../components/registry-statuses/registry-statuses.component'; import { WithdrawnMessageComponent } from '../../components/withdrawn-message/withdrawn-message.component'; import { CreateSchemaResponse, diff --git a/src/app/features/registry/pages/registry-resources/registry-resources.component.ts b/src/app/features/registry/pages/registry-resources/registry-resources.component.ts index e9b92b6da..a433ece15 100644 --- a/src/app/features/registry/pages/registry-resources/registry-resources.component.ts +++ b/src/app/features/registry/pages/registry-resources/registry-resources.component.ts @@ -17,7 +17,8 @@ import { CustomConfirmationService } from '@osf/shared/services/custom-confirmat import { CustomDialogService } from '@osf/shared/services/custom-dialog.service'; import { ToastService } from '@osf/shared/services/toast.service'; -import { AddResourceDialogComponent, EditResourceDialogComponent } from '../../components'; +import { AddResourceDialogComponent } from '../../components/add-resource-dialog/add-resource-dialog.component'; +import { EditResourceDialogComponent } from '../../components/edit-resource-dialog/edit-resource-dialog.component'; import { RegistryResource } from '../../models'; import { RegistrySelectors } from '../../store/registry'; import { diff --git a/src/app/features/registry/pages/registry-wiki/registry-wiki.component.spec.ts b/src/app/features/registry/pages/registry-wiki/registry-wiki.component.spec.ts index 6c40a0960..48a66ad6c 100644 --- a/src/app/features/registry/pages/registry-wiki/registry-wiki.component.spec.ts +++ b/src/app/features/registry/pages/registry-wiki/registry-wiki.component.spec.ts @@ -1,41 +1,79 @@ import { MockComponents, MockProvider } from 'ng-mocks'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { Subject } from 'rxjs'; + +import { signal } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ActivatedRoute, Router } from '@angular/router'; import { SubHeaderComponent } from '@osf/shared/components/sub-header/sub-header.component'; -import { ViewOnlyLinkMessageComponent } from '@osf/shared/components/view-only-link-message/view-only-link-message.component'; import { CompareSectionComponent } from '@osf/shared/components/wiki/compare-section/compare-section.component'; import { ViewSectionComponent } from '@osf/shared/components/wiki/view-section/view-section.component'; import { WikiListComponent } from '@osf/shared/components/wiki/wiki-list/wiki-list.component'; +import { ResourceType } from '@osf/shared/enums/resource-type.enum'; import { WikiModes } from '@osf/shared/models/wiki/wiki.model'; -import { WikiSelectors } from '@osf/shared/stores/wiki'; +import { + ClearWiki, + GetCompareVersionContent, + GetComponentsWikiList, + GetWikiList, + GetWikiVersionContent, + GetWikiVersions, + SetCurrentWiki, + ToggleMode, + WikiSelectors, +} from '@osf/shared/stores/wiki'; +import { ViewOnlyLinkMessageComponent } from '@shared/components/view-only-link-message/view-only-link-message.component'; import { RegistryWikiComponent } from './registry-wiki.component'; 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('RegistryWikiComponent', () => { +describe('RegistryWikiComponent', () => { let component: RegistryWikiComponent; let fixture: ComponentFixture; - let mockActivatedRoute: ReturnType; - let mockRouter: jest.Mocked; + let routerMock: ReturnType; + let activatedRouteMock: ReturnType; + let storeDispatchSpy: jest.SpyInstance; + let queryParamsSubject: Subject; - const mockResourceId = 'test-resource-id'; - const mockWikiId = 'test-wiki-id'; + const mockResourceId = 'resource-123'; + const mockWikiId = 'wiki-123'; + const mockWikiList = [{ id: 'wiki-1', name: 'Wiki 1' }] as any; beforeEach(async () => { - mockActivatedRoute = ActivatedRouteMockBuilder.create() + queryParamsSubject = new Subject(); + routerMock = RouterMockBuilder.create().build(); + activatedRouteMock = ActivatedRouteMockBuilder.create() .withParams({ id: mockResourceId }) .withQueryParams({ wiki: mockWikiId }) .build(); - mockRouter = { - navigate: jest.fn(), - url: '/test-url', - } as any; + + Object.defineProperty(activatedRouteMock, 'queryParams', { + value: queryParamsSubject.asObservable(), + writable: true, + }); + + const mockStore = provideMockStore({ + signals: [ + { selector: WikiSelectors.getWikiModes, value: signal({ view: true, edit: false, compare: false }) }, + { selector: WikiSelectors.getPreviewContent, value: signal('Preview content') }, + { selector: WikiSelectors.getWikiVersionContent, value: signal('Version content') }, + { selector: WikiSelectors.getCompareVersionContent, value: signal('Compare content') }, + { selector: WikiSelectors.getWikiList, value: signal(mockWikiList) }, + { selector: WikiSelectors.getComponentsWikiList, value: signal([]) }, + { selector: WikiSelectors.getCurrentWikiId, value: signal(mockWikiId) }, + { selector: WikiSelectors.getWikiVersions, value: signal([]) }, + { selector: WikiSelectors.getWikiListLoading, value: signal(false) }, + { selector: WikiSelectors.getComponentsWikiListLoading, value: signal(false) }, + { selector: WikiSelectors.getWikiVersionsLoading, value: signal(false) }, + ], + }); + + storeDispatchSpy = jest.spyOn(mockStore.useValue, 'dispatch'); await TestBed.configureTestingModule({ imports: [ @@ -49,114 +87,102 @@ describe.skip('RegistryWikiComponent', () => { ViewOnlyLinkMessageComponent ), ], - schemas: [NO_ERRORS_SCHEMA], - providers: [ - MockProvider(ActivatedRoute, mockActivatedRoute), - MockProvider(Router, mockRouter), - provideMockStore({ - signals: [ - { selector: WikiSelectors.getWikiModes, value: WikiModes.View }, - { selector: WikiSelectors.getPreviewContent, value: null }, - { selector: WikiSelectors.getWikiVersionContent, value: null }, - { selector: WikiSelectors.getCompareVersionContent, value: null }, - { selector: WikiSelectors.getWikiListLoading, value: false }, - { selector: WikiSelectors.getWikiList, value: [] }, - { selector: WikiSelectors.getCurrentWikiId, value: mockWikiId }, - { selector: WikiSelectors.getWikiVersions, value: [] }, - { selector: WikiSelectors.getWikiVersionsLoading, value: false }, - { selector: WikiSelectors.getComponentsWikiList, value: [] }, - ], - }), - ], + providers: [MockProvider(Router, routerMock), MockProvider(ActivatedRoute, activatedRouteMock), mockStore], }).compileComponents(); fixture = TestBed.createComponent(RegistryWikiComponent); component = fixture.componentInstance; - fixture.detectChanges(); }); - it('should create', () => { - expect(component).toBeTruthy(); - }); + it('should dispatch getWikiList and getComponentsWikiList on construction', () => { + expect(storeDispatchSpy).toHaveBeenCalledTimes(2); - it('should initialize with resource ID from route', () => { - expect(component.resourceId).toBe(mockResourceId); - }); + const getWikiListCall = storeDispatchSpy.mock.calls.find((call) => call[0] instanceof GetWikiList); + const getComponentsWikiListCall = storeDispatchSpy.mock.calls.find( + (call) => call[0] instanceof GetComponentsWikiList + ); - it('should initialize with wiki ID from query params', () => { - expect(component.wikiIdFromQueryParams).toBe(mockWikiId); - }); + expect(getWikiListCall).toBeDefined(); + expect(getWikiListCall[0].resourceType).toBe(ResourceType.Registration); + expect(getWikiListCall[0].resourceId).toBe(mockResourceId); - it('should compute hasViewOnly correctly', () => { - expect(component.hasViewOnly()).toBe(false); + expect(getComponentsWikiListCall).toBeDefined(); + expect(getComponentsWikiListCall[0].resourceType).toBe(ResourceType.Registration); + expect(getComponentsWikiListCall[0].resourceId).toBe(mockResourceId); }); - it('should toggle mode', () => { - const toggleModeSpy = jest.spyOn(component.actions, 'toggleMode'); + it('should call toggleMode action when toggleMode is called', () => { + storeDispatchSpy.mockClear(); + component.toggleMode(WikiModes.Edit); - expect(toggleModeSpy).toHaveBeenCalledWith(WikiModes.Edit); + + expect(storeDispatchSpy).toHaveBeenCalledWith(expect.any(ToggleMode)); + const action = storeDispatchSpy.mock.calls[0][0] as ToggleMode; + expect(action.mode).toBe(WikiModes.Edit); }); - it('should select version', () => { - const versionId = 'version-1'; - const getWikiVersionContentSpy = jest.spyOn(component.actions, 'getWikiVersionContent'); + it('should dispatch getWikiVersionContent when onSelectVersion is called with versionId', () => { + storeDispatchSpy.mockClear(); + const versionId = 'version-123'; + component.onSelectVersion(versionId); - expect(getWikiVersionContentSpy).toHaveBeenCalledWith(component.currentWikiId(), versionId); + + expect(storeDispatchSpy).toHaveBeenCalledWith(expect.any(GetWikiVersionContent)); + const action = storeDispatchSpy.mock.calls[0][0] as GetWikiVersionContent; + expect(action.wikiId).toBe(mockWikiId); + expect(action.versionId).toBe(versionId); }); - it('should not select version if empty', () => { - const getWikiVersionContentSpy = jest.spyOn(component.actions, 'getWikiVersionContent'); + it('should not dispatch getWikiVersionContent when onSelectVersion is called with empty versionId', () => { + storeDispatchSpy.mockClear(); + component.onSelectVersion(''); - expect(getWikiVersionContentSpy).not.toHaveBeenCalled(); + + const getVersionContentCall = storeDispatchSpy.mock.calls.find((call) => call[0] instanceof GetWikiVersionContent); + expect(getVersionContentCall).toBeUndefined(); }); - it('should select compare version', () => { - const versionId = 'version-1'; - const getCompareVersionContentSpy = jest.spyOn(component.actions, 'getCompareVersionContent'); + it('should dispatch getCompareVersionContent when onSelectCompareVersion is called', () => { + storeDispatchSpy.mockClear(); + const versionId = 'version-123'; + component.onSelectCompareVersion(versionId); - expect(getCompareVersionContentSpy).toHaveBeenCalledWith(component.currentWikiId(), versionId); + + expect(storeDispatchSpy).toHaveBeenCalledWith(expect.any(GetCompareVersionContent)); + const action = storeDispatchSpy.mock.calls[0][0] as GetCompareVersionContent; + expect(action.wikiId).toBe(mockWikiId); + expect(action.versionId).toBe(versionId); }); - it('should handle different route configurations', () => { - const differentResourceId = 'different-resource-id'; - const differentWikiId = 'different-wiki-id'; + it('should handle query params changes and dispatch setCurrentWiki and getWikiVersions', () => { + storeDispatchSpy.mockClear(); + const newWikiId = 'new-wiki-123'; - mockActivatedRoute = ActivatedRouteMockBuilder.create() - .withParams({ id: differentResourceId }) - .withQueryParams({ wiki: differentWikiId }) - .build(); + queryParamsSubject.next({ wiki: newWikiId }); - fixture = TestBed.createComponent(RegistryWikiComponent); - component = fixture.componentInstance; - fixture.detectChanges(); + const setCurrentWikiCall = storeDispatchSpy.mock.calls.find((call) => call[0] instanceof SetCurrentWiki); + expect(setCurrentWikiCall).toBeDefined(); + expect((setCurrentWikiCall[0] as SetCurrentWiki).wikiId).toBe(newWikiId); - expect(component.resourceId).toBe(differentResourceId); - expect(component.wikiIdFromQueryParams).toBe(differentWikiId); + const getVersionsCall = storeDispatchSpy.mock.calls.find((call) => call[0] instanceof GetWikiVersions); + expect(getVersionsCall).toBeDefined(); + expect((getVersionsCall[0] as GetWikiVersions).wikiId).toBe(newWikiId); }); - it('should handle missing query params', () => { - mockActivatedRoute = ActivatedRouteMockBuilder.create() - .withParams({ id: mockResourceId }) - .withQueryParams({}) - .build(); + it('should not process query params when wiki is empty', () => { + storeDispatchSpy.mockClear(); - fixture = TestBed.createComponent(RegistryWikiComponent); - component = fixture.componentInstance; - fixture.detectChanges(); + queryParamsSubject.next({ wiki: '' }); - expect(component.wikiIdFromQueryParams).toBeUndefined(); + const setCurrentWikiCall = storeDispatchSpy.mock.calls.find((call) => call[0] instanceof SetCurrentWiki); + expect(setCurrentWikiCall).toBeUndefined(); }); - it('should handle missing route params', () => { - mockActivatedRoute = ActivatedRouteMockBuilder.create() - .withParams({}) - .withQueryParams({ wiki: mockWikiId }) - .build(); + it('should dispatch clearWiki on destroy', () => { + storeDispatchSpy.mockClear(); - fixture = TestBed.createComponent(RegistryWikiComponent); - component = fixture.componentInstance; - fixture.detectChanges(); + fixture.destroy(); - expect(component.resourceId).toBeUndefined(); + expect(storeDispatchSpy).toHaveBeenCalledWith(expect.any(ClearWiki)); }); }); diff --git a/src/app/shared/components/file-menu/file-menu.component.spec.ts b/src/app/shared/components/file-menu/file-menu.component.spec.ts index 6fe73f9b4..7e4358881 100644 --- a/src/app/shared/components/file-menu/file-menu.component.spec.ts +++ b/src/app/shared/components/file-menu/file-menu.component.spec.ts @@ -1,4 +1,13 @@ +import { MockComponent, MockProvider } from 'ng-mocks'; + +import { TieredMenu } from 'primeng/tieredmenu'; + import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { Router } from '@angular/router'; + +import { FileMenuType } from '@osf/shared/enums/file-menu-type.enum'; +import { MenuManagerService } from '@osf/shared/services/menu-manager.service'; +import { FileMenuFlags } from '@shared/models/files/file-menu-action.model'; import { FileMenuComponent } from './file-menu.component'; @@ -7,18 +16,211 @@ import { OSFTestingModule } from '@testing/osf.testing.module'; describe('FileMenuComponent', () => { let component: FileMenuComponent; let fixture: ComponentFixture; + let router: Router; + let menuManager: MenuManagerService; + let mockMenu: TieredMenu; beforeEach(async () => { + mockMenu = { + toggle: jest.fn(), + hide: jest.fn(), + } as any; + await TestBed.configureTestingModule({ - imports: [FileMenuComponent, OSFTestingModule], + imports: [FileMenuComponent, OSFTestingModule, MockComponent(TieredMenu)], + providers: [MockProvider(MenuManagerService)], }).compileComponents(); fixture = TestBed.createComponent(FileMenuComponent); component = fixture.componentInstance; + router = TestBed.inject(Router); + menuManager = TestBed.inject(MenuManagerService); + + Object.defineProperty(component, 'menu', { + value: () => mockMenu, + writable: true, + configurable: true, + }); + }); + + it('should have default values', () => { + expect(component.isFolder()).toBe(false); + expect(component.allowedActions()).toEqual({}); + }); + + describe('menuItems computed - View Only Mode', () => { + beforeEach(() => { + jest.spyOn(router, 'url', 'get').mockReturnValue('/test?view_only=true'); + Object.defineProperty(window, 'location', { + value: { search: '?view_only=true' }, + writable: true, + }); + }); + + it('should filter menu items for files in view-only mode', () => { + const allowedActions: FileMenuFlags = { + [FileMenuType.Download]: true, + [FileMenuType.Embed]: true, + [FileMenuType.Share]: true, + [FileMenuType.Copy]: true, + [FileMenuType.Rename]: false, + [FileMenuType.Move]: false, + [FileMenuType.Delete]: false, + }; + + fixture.componentRef.setInput('isFolder', false); + fixture.componentRef.setInput('allowedActions', allowedActions); + fixture.detectChanges(); + + const menuItems = component.menuItems(); + const menuItemIds = menuItems.map((item) => item.id); + + expect(menuItemIds).toContain(FileMenuType.Download); + expect(menuItemIds).toContain(FileMenuType.Embed); + expect(menuItemIds).toContain(FileMenuType.Share); + expect(menuItemIds).toContain(FileMenuType.Copy); + expect(menuItemIds).not.toContain(FileMenuType.Rename); + expect(menuItemIds).not.toContain(FileMenuType.Move); + expect(menuItemIds).not.toContain(FileMenuType.Delete); + }); + + it('should return empty array when no allowed actions in view-only mode', () => { + const allowedActions: FileMenuFlags = { + [FileMenuType.Download]: false, + [FileMenuType.Embed]: false, + [FileMenuType.Share]: false, + [FileMenuType.Copy]: false, + [FileMenuType.Rename]: false, + [FileMenuType.Move]: false, + [FileMenuType.Delete]: false, + }; + + fixture.componentRef.setInput('isFolder', false); + fixture.componentRef.setInput('allowedActions', allowedActions); + fixture.detectChanges(); + + expect(component.menuItems()).toEqual([]); + }); + }); + + describe('menuItems computed - Normal Mode', () => { + beforeEach(() => { + jest.spyOn(router, 'url', 'get').mockReturnValue('/test'); + Object.defineProperty(window, 'location', { + value: { search: '' }, + writable: true, + }); + }); + + it('should filter menu items for files in normal mode', () => { + const allowedActions: FileMenuFlags = { + [FileMenuType.Download]: true, + [FileMenuType.Embed]: true, + [FileMenuType.Share]: true, + [FileMenuType.Copy]: true, + [FileMenuType.Rename]: true, + [FileMenuType.Move]: true, + [FileMenuType.Delete]: true, + }; + + fixture.componentRef.setInput('isFolder', false); + fixture.componentRef.setInput('allowedActions', allowedActions); + fixture.detectChanges(); + + const menuItems = component.menuItems(); + const menuItemIds = menuItems.map((item) => item.id); + + expect(menuItemIds).toContain(FileMenuType.Download); + expect(menuItemIds).toContain(FileMenuType.Embed); + expect(menuItemIds).toContain(FileMenuType.Share); + expect(menuItemIds).toContain(FileMenuType.Copy); + expect(menuItemIds).toContain(FileMenuType.Rename); + expect(menuItemIds).toContain(FileMenuType.Move); + expect(menuItemIds).toContain(FileMenuType.Delete); + }); + + it('should filter menu items for folders in normal mode, excluding Share and Embed', () => { + const allowedActions: FileMenuFlags = { + [FileMenuType.Download]: true, + [FileMenuType.Embed]: true, + [FileMenuType.Share]: true, + [FileMenuType.Copy]: true, + [FileMenuType.Rename]: true, + [FileMenuType.Move]: true, + [FileMenuType.Delete]: true, + }; + + fixture.componentRef.setInput('isFolder', true); + fixture.componentRef.setInput('allowedActions', allowedActions); + fixture.detectChanges(); + + const menuItems = component.menuItems(); + const menuItemIds = menuItems.map((item) => item.id); + + expect(menuItemIds).toContain(FileMenuType.Download); + expect(menuItemIds).toContain(FileMenuType.Copy); + expect(menuItemIds).toContain(FileMenuType.Rename); + expect(menuItemIds).toContain(FileMenuType.Move); + expect(menuItemIds).toContain(FileMenuType.Delete); + expect(menuItemIds).not.toContain(FileMenuType.Embed); + expect(menuItemIds).not.toContain(FileMenuType.Share); + }); + + it('should return empty array when no allowed actions in normal mode', () => { + const allowedActions: FileMenuFlags = { + [FileMenuType.Download]: false, + [FileMenuType.Embed]: false, + [FileMenuType.Share]: false, + [FileMenuType.Copy]: false, + [FileMenuType.Rename]: false, + [FileMenuType.Move]: false, + [FileMenuType.Delete]: false, + }; + + fixture.componentRef.setInput('isFolder', false); + fixture.componentRef.setInput('allowedActions', allowedActions); + fixture.detectChanges(); + + expect(component.menuItems()).toEqual([]); + }); + }); + + it('should update isFolder input', () => { + fixture.componentRef.setInput('isFolder', true); fixture.detectChanges(); + expect(component.isFolder()).toBe(true); }); - it('should create', () => { - expect(component).toBeTruthy(); + it('should update allowedActions input', () => { + const allowedActions: FileMenuFlags = { + [FileMenuType.Download]: true, + [FileMenuType.Embed]: false, + [FileMenuType.Share]: false, + [FileMenuType.Copy]: false, + [FileMenuType.Rename]: false, + [FileMenuType.Move]: false, + [FileMenuType.Delete]: false, + }; + + fixture.componentRef.setInput('allowedActions', allowedActions); + fixture.detectChanges(); + expect(component.allowedActions()).toEqual(allowedActions); + }); + + it('should call menuManager.openMenu when onMenuToggle is called', () => { + const openMenuSpy = jest.spyOn(menuManager, 'openMenu'); + const mockEvent = new Event('click'); + + component.onMenuToggle(mockEvent); + + expect(openMenuSpy).toHaveBeenCalledWith(mockMenu, mockEvent); + }); + + it('should call menuManager.onMenuHide when onMenuHide is called', () => { + const onMenuHideSpy = jest.spyOn(menuManager, 'onMenuHide'); + + component.onMenuHide(); + + expect(onMenuHideSpy).toHaveBeenCalled(); }); }); diff --git a/src/app/shared/components/line-chart/line-chart.component.spec.ts b/src/app/shared/components/line-chart/line-chart.component.spec.ts index 77ea0e99f..3d2938fbf 100644 --- a/src/app/shared/components/line-chart/line-chart.component.spec.ts +++ b/src/app/shared/components/line-chart/line-chart.component.spec.ts @@ -1,26 +1,226 @@ -import { MockComponent } from 'ng-mocks'; +import { MockComponent, MockModule, MockProvider } from 'ng-mocks'; +import { ChartModule } from 'primeng/chart'; + +import { PLATFORM_ID } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { DatasetInput } from '@osf/shared/models/charts/dataset-input'; + import { LoadingSpinnerComponent } from '../loading-spinner/loading-spinner.component'; import { LineChartComponent } from './line-chart.component'; +import { OSFTestingModule } from '@testing/osf.testing.module'; + describe('LineChartComponent', () => { let component: LineChartComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [LineChartComponent, MockComponent(LoadingSpinnerComponent)], + imports: [LineChartComponent, OSFTestingModule, MockModule(ChartModule), MockComponent(LoadingSpinnerComponent)], + providers: [MockProvider(PLATFORM_ID, 'browser')], }).compileComponents(); fixture = TestBed.createComponent(LineChartComponent); component = fixture.componentInstance; - fixture.detectChanges(); }); - it('should create', () => { - expect(component).toBeTruthy(); + it('should have default values', () => { + expect(component.isLoading()).toBe(false); + expect(component.title()).toBe(''); + expect(component.labels()).toEqual([]); + expect(component.datasets()).toEqual([]); + expect(component.showLegend()).toBe(false); + expect(component.showGrid()).toBe(false); + }); + + it('should initialize data and options signals', () => { + const mockGetPropertyValue = jest.fn((prop: string) => { + const colors: Record = { + '--dark-blue-1': '#1a365d', + '--grey-2': '#e2e8f0', + '--pr-blue-1': '#3182ce', + }; + return colors[prop] || '#000000'; + }); + + jest.spyOn(window, 'getComputedStyle').mockReturnValue({ + getPropertyValue: mockGetPropertyValue, + } as any); + + component.ngOnInit(); + + expect(component.data()).toBeDefined(); + expect(component.options()).toBeDefined(); + }); + + it('should initialize chart on browser platform', () => { + const mockGetPropertyValue = jest.fn((prop: string) => { + const colors: Record = { + '--dark-blue-1': '#1a365d', + '--grey-2': '#e2e8f0', + '--pr-blue-1': '#3182ce', + }; + return colors[prop] || '#000000'; + }); + + const mockGetComputedStyle = jest.spyOn(window, 'getComputedStyle').mockReturnValue({ + getPropertyValue: mockGetPropertyValue, + } as any); + + const markForCheckSpy = jest.spyOn(component['cd'], 'markForCheck'); + + component.ngOnInit(); + + expect(mockGetComputedStyle).toHaveBeenCalledWith(document.documentElement); + expect(component.data()).toBeDefined(); + expect(component.options()).toBeDefined(); + expect(markForCheckSpy).toHaveBeenCalled(); + + mockGetComputedStyle.mockRestore(); + }); + + it('should call setChartData and setChartOptions on browser platform', () => { + const mockGetPropertyValue = jest.fn((prop: string) => { + const colors: Record = { + '--dark-blue-1': '#1a365d', + '--grey-2': '#e2e8f0', + '--pr-blue-1': '#3182ce', + }; + return colors[prop] || '#000000'; + }); + + jest.spyOn(window, 'getComputedStyle').mockReturnValue({ + getPropertyValue: mockGetPropertyValue, + } as any); + + const setChartDataSpy = jest.spyOn(component as any, 'setChartData'); + const setChartOptionsSpy = jest.spyOn(component as any, 'setChartOptions'); + + component.initChart(); + + expect(setChartDataSpy).toHaveBeenCalled(); + expect(setChartOptionsSpy).toHaveBeenCalled(); + }); + + describe('Chart Data Configuration', () => { + const mockLabels = ['Jan', 'Feb', 'Mar']; + const mockDatasets: DatasetInput[] = [ + { label: 'Dataset 1', data: [10, 20, 30] }, + { label: 'Dataset 2', data: [15, 25, 35], color: '#ff0000' }, + ]; + + beforeEach(() => { + const mockGetPropertyValue = jest.fn((prop: string) => { + const colors: Record = { + '--dark-blue-1': '#1a365d', + '--grey-2': '#e2e8f0', + '--pr-blue-1': '#3182ce', + }; + return colors[prop] || '#000000'; + }); + + jest.spyOn(window, 'getComputedStyle').mockReturnValue({ + getPropertyValue: mockGetPropertyValue, + } as any); + + fixture.componentRef.setInput('labels', mockLabels); + fixture.componentRef.setInput('datasets', mockDatasets); + component.ngOnInit(); + }); + + it('should map datasets correctly with default colors from CSS variables', () => { + const chartData = component.data(); + expect(chartData.datasets).toHaveLength(2); + expect(chartData.datasets[0].label).toBe('Dataset 1'); + expect(chartData.datasets[0].data).toEqual([10, 20, 30]); + expect(chartData.datasets[0].backgroundColor).toBe('#3182ce'); + expect(chartData.datasets[0].borderColor).toBe('#3182ce'); + }); + + it('should use custom color when provided in dataset', () => { + const chartData = component.data(); + expect(chartData.datasets[1].backgroundColor).toBe('#ff0000'); + expect(chartData.datasets[1].borderColor).toBe('#ff0000'); + }); + + it('should include correct labels from input', () => { + const chartData = component.data(); + expect(chartData.labels).toEqual(mockLabels); + }); + + it('should have correct dataset structure', () => { + const chartData = component.data(); + const dataset = chartData.datasets[0]; + expect(dataset).toHaveProperty('label'); + expect(dataset).toHaveProperty('data'); + expect(dataset).toHaveProperty('backgroundColor'); + expect(dataset).toHaveProperty('borderColor'); + }); + + it('should handle multiple datasets correctly', () => { + const chartData = component.data(); + expect(chartData.datasets).toHaveLength(2); + expect(chartData.datasets[0].label).toBe('Dataset 1'); + expect(chartData.datasets[1].label).toBe('Dataset 2'); + }); + }); + + describe('Input Updates', () => { + beforeEach(() => { + const mockGetPropertyValue = jest.fn((prop: string) => { + const colors: Record = { + '--dark-blue-1': '#1a365d', + '--grey-2': '#e2e8f0', + '--pr-blue-1': '#3182ce', + }; + return colors[prop] || '#000000'; + }); + + jest.spyOn(window, 'getComputedStyle').mockReturnValue({ + getPropertyValue: mockGetPropertyValue, + } as any); + }); + + it('should update title input', () => { + fixture.componentRef.setInput('title', 'Test Title'); + fixture.detectChanges(); + expect(component.title()).toBe('Test Title'); + }); + + it('should update isLoading input', () => { + fixture.componentRef.setInput('isLoading', true); + fixture.detectChanges(); + expect(component.isLoading()).toBe(true); + }); + + it('should update labels input and reinitialize chart', () => { + const newLabels = ['New Label 1', 'New Label 2']; + fixture.componentRef.setInput('labels', newLabels); + component.ngOnInit(); + expect(component.data().labels).toEqual(newLabels); + }); + + it('should update datasets input and reinitialize chart', () => { + const newDatasets: DatasetInput[] = [{ label: 'New Dataset', data: [1, 2, 3] }]; + fixture.componentRef.setInput('datasets', newDatasets); + component.ngOnInit(); + expect(component.data().datasets).toHaveLength(1); + expect(component.data().datasets[0].label).toBe('New Dataset'); + }); + + it('should update showLegend input', () => { + fixture.componentRef.setInput('showLegend', true); + fixture.detectChanges(); + expect(component.showLegend()).toBe(true); + }); + + it('should update showGrid input', () => { + fixture.componentRef.setInput('showGrid', true); + fixture.detectChanges(); + expect(component.showGrid()).toBe(true); + }); }); }); diff --git a/src/app/shared/components/pie-chart/pie-chart.component.spec.ts b/src/app/shared/components/pie-chart/pie-chart.component.spec.ts index 0e52768ea..9d0f4ef4a 100644 --- a/src/app/shared/components/pie-chart/pie-chart.component.spec.ts +++ b/src/app/shared/components/pie-chart/pie-chart.component.spec.ts @@ -1,18 +1,26 @@ -import { MockComponent } from 'ng-mocks'; +import { MockComponent, MockModule, MockProvider } from 'ng-mocks'; +import { ChartModule } from 'primeng/chart'; + +import { PLATFORM_ID } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { DatasetInput } from '@shared/models/charts/dataset-input'; + import { LoadingSpinnerComponent } from '../loading-spinner/loading-spinner.component'; import { PieChartComponent } from './pie-chart.component'; +import { OSFTestingModule } from '@testing/osf.testing.module'; + describe('PieChartComponent', () => { let component: PieChartComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [PieChartComponent, MockComponent(LoadingSpinnerComponent)], + imports: [PieChartComponent, OSFTestingModule, MockModule(ChartModule), MockComponent(LoadingSpinnerComponent)], + providers: [MockProvider(PLATFORM_ID, 'browser')], }).compileComponents(); fixture = TestBed.createComponent(PieChartComponent); @@ -20,7 +28,159 @@ describe('PieChartComponent', () => { fixture.detectChanges(); }); - it('should create', () => { - expect(component).toBeTruthy(); + it('should have default values', () => { + expect(component.isLoading()).toBe(false); + expect(component.title()).toBe(''); + expect(component.labels()).toEqual([]); + expect(component.datasets()).toEqual([]); + expect(component.showLegend()).toBe(false); + }); + + it('should initialize data and options signals', () => { + expect(component.data()).toBeDefined(); + expect(component.options()).toBeDefined(); + }); + + it('should initialize chart on browser platform', () => { + const markForCheckSpy = jest.spyOn(component['cd'], 'markForCheck'); + + component.ngOnInit(); + + expect(component.data().labels).toBeDefined(); + expect(component.data().datasets).toBeDefined(); + expect(component.options()).toBeDefined(); + expect(markForCheckSpy).toHaveBeenCalled(); + }); + + describe('Chart Data Configuration', () => { + const mockLabels = ['Label1', 'Label2', 'Label3']; + const mockDatasets: DatasetInput[] = [ + { label: 'Dataset 1', data: [10, 20, 30] }, + { label: 'Dataset 2', data: [15, 25, 35] }, + ]; + + beforeEach(() => { + fixture.componentRef.setInput('labels', mockLabels); + fixture.componentRef.setInput('datasets', mockDatasets); + component.ngOnInit(); + }); + + it('should map datasets correctly with PIE_CHART_PALETTE', () => { + const chartData = component.data(); + expect(chartData.datasets).toHaveLength(2); + expect(chartData.datasets[0].label).toBe('Dataset 1'); + expect(chartData.datasets[0].data).toEqual([10, 20, 30]); + expect(chartData.datasets[0].backgroundColor).toBeDefined(); + expect(chartData.datasets[0].borderWidth).toBe(0); + }); + + it('should include correct labels from input', () => { + const chartData = component.data(); + expect(chartData.labels).toEqual(mockLabels); + }); + + it('should have correct dataset structure', () => { + const chartData = component.data(); + const dataset = chartData.datasets[0]; + expect(dataset).toHaveProperty('label'); + expect(dataset).toHaveProperty('data'); + expect(dataset).toHaveProperty('backgroundColor'); + expect(dataset).toHaveProperty('borderWidth'); + }); + + it('should handle multiple datasets correctly', () => { + const chartData = component.data(); + expect(chartData.datasets).toHaveLength(2); + expect(chartData.datasets[0].label).toBe('Dataset 1'); + expect(chartData.datasets[1].label).toBe('Dataset 2'); + }); + }); + + describe('Chart Options Configuration', () => { + it('should set maintainAspectRatio and responsive', () => { + component.ngOnInit(); + const options = component.options(); + expect(options.maintainAspectRatio).toBe(true); + expect(options.responsive).toBe(true); + }); + + it('should set legend display based on showLegend input when false', () => { + fixture.componentRef.setInput('showLegend', false); + component.ngOnInit(); + const options = component.options(); + expect(options.plugins?.legend?.display).toBe(false); + }); + + it('should set legend display based on showLegend input when true', () => { + fixture.componentRef.setInput('showLegend', true); + component.ngOnInit(); + const options = component.options(); + expect(options.plugins?.legend?.display).toBe(true); + }); + + it('should set legend position to bottom', () => { + component.ngOnInit(); + const options = component.options(); + expect(options.plugins?.legend?.position).toBe('bottom'); + }); + }); + + describe('Input Updates', () => { + it('should update title input', () => { + fixture.componentRef.setInput('title', 'Test Title'); + fixture.detectChanges(); + expect(component.title()).toBe('Test Title'); + }); + + it('should update isLoading input', () => { + fixture.componentRef.setInput('isLoading', true); + fixture.detectChanges(); + expect(component.isLoading()).toBe(true); + }); + + it('should update labels input and reinitialize chart', () => { + const newLabels = ['New Label 1', 'New Label 2']; + fixture.componentRef.setInput('labels', newLabels); + component.ngOnInit(); + expect(component.data().labels).toEqual(newLabels); + }); + + it('should update datasets input and reinitialize chart', () => { + const newDatasets: DatasetInput[] = [{ label: 'New Dataset', data: [1, 2, 3] }]; + fixture.componentRef.setInput('datasets', newDatasets); + component.ngOnInit(); + expect(component.data().datasets).toHaveLength(1); + expect(component.data().datasets[0].label).toBe('New Dataset'); + }); + + it('should update showLegend input and reinitialize chart', () => { + fixture.componentRef.setInput('showLegend', true); + component.ngOnInit(); + expect(component.options().plugins?.legend?.display).toBe(true); + }); + }); + + describe('Edge Cases', () => { + it('should handle empty datasets array', () => { + fixture.componentRef.setInput('datasets', []); + fixture.componentRef.setInput('labels', ['Label1']); + component.ngOnInit(); + expect(component.data().datasets).toEqual([]); + }); + + it('should handle empty labels array', () => { + fixture.componentRef.setInput('labels', []); + fixture.componentRef.setInput('datasets', [{ label: 'Dataset 1', data: [10] }]); + component.ngOnInit(); + expect(component.data().labels).toEqual([]); + }); + + it('should handle component with no data', () => { + fixture.componentRef.setInput('labels', []); + fixture.componentRef.setInput('datasets', []); + component.ngOnInit(); + expect(component.data().labels).toEqual([]); + expect(component.data().datasets).toEqual([]); + }); }); }); diff --git a/src/app/shared/components/wiki/edit-section/edit-section.component.spec.ts b/src/app/shared/components/wiki/edit-section/edit-section.component.spec.ts index b9f9faa9b..fbe327010 100644 --- a/src/app/shared/components/wiki/edit-section/edit-section.component.spec.ts +++ b/src/app/shared/components/wiki/edit-section/edit-section.component.spec.ts @@ -1,322 +1,240 @@ -import { MockProvider } from 'ng-mocks'; +import { LMarkdownEditorModule } from 'ngx-markdown-editor'; +import { MockModule, MockProvider } from 'ng-mocks'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { CustomDialogService } from '@osf/shared/services/custom-dialog.service'; +import { WikiSyntaxHelpDialogComponent } from '../wiki-syntax-help-dialog/wiki-syntax-help-dialog.component'; + import { EditSectionComponent } from './edit-section.component'; import { OSFTestingModule } from '@testing/osf.testing.module'; -import { DialogServiceMockBuilder } from '@testing/providers/dialog-provider.mock'; +import { CustomDialogServiceMockBuilder } from '@testing/providers/custom-dialog-provider.mock'; describe('EditSectionComponent', () => { let component: EditSectionComponent; let fixture: ComponentFixture; - let mockCustomDialogService: ReturnType; + let mockCustomDialogService: ReturnType; + let mockEditorInstance: any; - const mockCurrentContent = '# Test Content\nThis is test content'; - const mockVersionContent = '# Version Content\nThis is version content'; + const mockVersionContent = 'Initial version content'; + const mockCurrentContent = 'Current content'; + const mockEditorValue = 'Editor content value'; beforeEach(async () => { - mockCustomDialogService = DialogServiceMockBuilder.create().withOpenMock().build(); + mockEditorInstance = { + setShowPrintMargin: jest.fn(), + setOptions: jest.fn(), + getValue: jest.fn().mockReturnValue(mockEditorValue), + insert: jest.fn(), + undo: jest.fn(), + redo: jest.fn(), + }; + + mockCustomDialogService = CustomDialogServiceMockBuilder.create().withDefaultOpen().build(); await TestBed.configureTestingModule({ - imports: [EditSectionComponent, OSFTestingModule], + imports: [EditSectionComponent, OSFTestingModule, MockModule(LMarkdownEditorModule)], providers: [MockProvider(CustomDialogService, mockCustomDialogService)], }).compileComponents(); fixture = TestBed.createComponent(EditSectionComponent); component = fixture.componentInstance; - }); - - it('should create', () => { - fixture.componentRef.setInput('currentContent', mockCurrentContent); - fixture.componentRef.setInput('versionContent', mockVersionContent); - fixture.detectChanges(); - - expect(component).toBeTruthy(); - }); - - it('should have required inputs', () => { - fixture.componentRef.setInput('currentContent', mockCurrentContent); - fixture.componentRef.setInput('versionContent', mockVersionContent); - fixture.detectChanges(); - - expect(component.currentContent()).toBe(mockCurrentContent); - expect(component.versionContent()).toBe(mockVersionContent); - }); - - it('should have isSaving input with default value false', () => { - fixture.componentRef.setInput('currentContent', mockCurrentContent); - fixture.componentRef.setInput('versionContent', mockVersionContent); - fixture.detectChanges(); - - expect(component.isSaving()).toBe(false); - }); - - it('should initialize content from currentContent on ngOnInit', () => { fixture.componentRef.setInput('currentContent', mockCurrentContent); fixture.componentRef.setInput('versionContent', mockVersionContent); fixture.detectChanges(); - - expect(component.content).toBe(mockCurrentContent); - expect(component.initialContent).toBe(mockCurrentContent); }); - it('should have default editor options', () => { - fixture.componentRef.setInput('currentContent', mockCurrentContent); - fixture.componentRef.setInput('versionContent', mockVersionContent); - fixture.detectChanges(); - + it('should initialize with default values', () => { + expect(component.autoCompleteEnabled).toBe(false); expect(component.options.showPreviewPanel).toBe(false); expect(component.options.fontAwesomeVersion).toBe('6'); - expect(component.options.markedjsOpt?.sanitize).toBe(true); }); - it('should have autoCompleteEnabled set to false by default', () => { - fixture.componentRef.setInput('currentContent', mockCurrentContent); - fixture.componentRef.setInput('versionContent', mockVersionContent); + it('should update content when currentContent input changes', () => { + const newContent = 'New current content'; + fixture.componentRef.setInput('currentContent', newContent); fixture.detectChanges(); - expect(component.autoCompleteEnabled).toBe(false); + expect(component.content).toBe(newContent); + expect(component.initialContent).toBe(newContent); }); - it('should store editor instance and configure it', () => { - fixture.componentRef.setInput('currentContent', mockCurrentContent); - fixture.componentRef.setInput('versionContent', mockVersionContent); - fixture.detectChanges(); + it('should set versionContent only once when initialContent is empty', () => { + const newFixture = TestBed.createComponent(EditSectionComponent); + const newComponent = newFixture.componentInstance; + newFixture.componentRef.setInput('versionContent', mockVersionContent); + newFixture.componentRef.setInput('currentContent', mockVersionContent); + newFixture.detectChanges(); - const mockEditor = { - setShowPrintMargin: jest.fn(), - setOptions: jest.fn(), - }; + expect(newComponent.content).toBe(mockVersionContent); + expect(newComponent.initialContent).toBe(mockVersionContent); - component.onEditorLoaded(mockEditor); + const updatedVersionContent = 'Updated version content'; + newFixture.componentRef.setInput('versionContent', updatedVersionContent); + newFixture.detectChanges(); - expect(mockEditor.setShowPrintMargin).toHaveBeenCalledWith(false); - expect(mockEditor.setOptions).toHaveBeenCalled(); + expect(newComponent.content).toBe(mockVersionContent); + expect(newComponent.initialContent).toBe(mockVersionContent); }); - it('should emit contentChange with editor value', () => { - fixture.componentRef.setInput('currentContent', mockCurrentContent); - fixture.componentRef.setInput('versionContent', mockVersionContent); + it('should set isSaving input', () => { + fixture.componentRef.setInput('isSaving', true); fixture.detectChanges(); - const mockEditor = { - getValue: jest.fn().mockReturnValue('Updated content'), - setShowPrintMargin: jest.fn(), - setOptions: jest.fn(), - }; - - component.onEditorLoaded(mockEditor); + expect(component.isSaving()).toBe(true); + }); + it('should emit contentChange when onPreviewDomChanged is called', () => { + (component as any).editorInstance = mockEditorInstance; const emitSpy = jest.spyOn(component.contentChange, 'emit'); component.onPreviewDomChanged(); - expect(mockEditor.getValue).toHaveBeenCalled(); - expect(emitSpy).toHaveBeenCalledWith('Updated content'); + expect(mockEditorInstance.getValue).toHaveBeenCalled(); + expect(emitSpy).toHaveBeenCalledWith(mockEditorValue); }); - it('should handle when editor is not loaded', () => { - fixture.componentRef.setInput('currentContent', mockCurrentContent); - fixture.componentRef.setInput('versionContent', mockVersionContent); - fixture.detectChanges(); - + it('should not emit contentChange when editorInstance is null', () => { + (component as any).editorInstance = null; const emitSpy = jest.spyOn(component.contentChange, 'emit'); - expect(() => component.onPreviewDomChanged()).not.toThrow(); + component.onPreviewDomChanged(); + expect(emitSpy).toHaveBeenCalledWith(undefined); }); - it('should emit saveContent with editor value', () => { - fixture.componentRef.setInput('currentContent', mockCurrentContent); - fixture.componentRef.setInput('versionContent', mockVersionContent); - fixture.detectChanges(); - - const mockEditor = { - getValue: jest.fn().mockReturnValue('Content to save'), - setShowPrintMargin: jest.fn(), - setOptions: jest.fn(), - }; - - component.onEditorLoaded(mockEditor); - + it('should emit saveContent when save is called', () => { + (component as any).editorInstance = mockEditorInstance; const emitSpy = jest.spyOn(component.saveContent, 'emit'); component.save(); - expect(mockEditor.getValue).toHaveBeenCalled(); - expect(emitSpy).toHaveBeenCalledWith('Content to save'); + expect(mockEditorInstance.getValue).toHaveBeenCalled(); + expect(emitSpy).toHaveBeenCalledWith(mockEditorValue); }); - it('should handle when editor is not loaded', () => { - fixture.componentRef.setInput('currentContent', mockCurrentContent); - fixture.componentRef.setInput('versionContent', mockVersionContent); - fixture.detectChanges(); - + it('should not emit saveContent when editorInstance is null', () => { + (component as any).editorInstance = null; const emitSpy = jest.spyOn(component.saveContent, 'emit'); - expect(() => component.save()).not.toThrow(); + component.save(); + expect(emitSpy).toHaveBeenCalledWith(undefined); }); it('should revert content to initialContent', () => { - fixture.componentRef.setInput('currentContent', mockCurrentContent); - fixture.componentRef.setInput('versionContent', mockVersionContent); - fixture.detectChanges(); - component.content = 'Modified content'; + component.initialContent = 'Original content'; component.revert(); - expect(component.content).toBe(mockCurrentContent); + expect(component.content).toBe('Original content'); }); - it('should call editor undo method', () => { - fixture.componentRef.setInput('currentContent', mockCurrentContent); - fixture.componentRef.setInput('versionContent', mockVersionContent); - fixture.detectChanges(); + it('should call undo on editorInstance', () => { + (component as any).editorInstance = mockEditorInstance; - const mockEditor = { - undo: jest.fn(), - setShowPrintMargin: jest.fn(), - setOptions: jest.fn(), - }; - - component.onEditorLoaded(mockEditor); component.undo(); - expect(mockEditor.undo).toHaveBeenCalled(); + expect(mockEditorInstance.undo).toHaveBeenCalled(); }); - it('should not throw when editor is not loaded', () => { - fixture.componentRef.setInput('currentContent', mockCurrentContent); - fixture.componentRef.setInput('versionContent', mockVersionContent); - fixture.detectChanges(); + it('should not call undo when editorInstance is null', () => { + (component as any).editorInstance = null; expect(() => component.undo()).not.toThrow(); }); - describe('redo', () => { - it('should call editor redo method', () => { - fixture.componentRef.setInput('currentContent', mockCurrentContent); - fixture.componentRef.setInput('versionContent', mockVersionContent); - fixture.detectChanges(); - - const mockEditor = { - redo: jest.fn(), - setShowPrintMargin: jest.fn(), - setOptions: jest.fn(), - }; + it('should call redo on editorInstance', () => { + (component as any).editorInstance = mockEditorInstance; - component.onEditorLoaded(mockEditor); - component.redo(); + component.redo(); - expect(mockEditor.redo).toHaveBeenCalled(); - }); + expect(mockEditorInstance.redo).toHaveBeenCalled(); + }); - it('should not throw when editor is not loaded', () => { - fixture.componentRef.setInput('currentContent', mockCurrentContent); - fixture.componentRef.setInput('versionContent', mockVersionContent); - fixture.detectChanges(); + it('should not call redo when editorInstance is null', () => { + (component as any).editorInstance = null; - expect(() => component.redo()).not.toThrow(); - }); + expect(() => component.redo()).not.toThrow(); }); - describe('doHorizontalRule', () => { - it('should insert horizontal rule into editor', () => { - fixture.componentRef.setInput('currentContent', mockCurrentContent); - fixture.componentRef.setInput('versionContent', mockVersionContent); - fixture.detectChanges(); + it('should insert horizontal rule markdown', () => { + (component as any).editorInstance = mockEditorInstance; - const mockEditor = { - insert: jest.fn(), - setShowPrintMargin: jest.fn(), - setOptions: jest.fn(), - }; + component.doHorizontalRule(); - component.onEditorLoaded(mockEditor); - component.doHorizontalRule(); - - expect(mockEditor.insert).toHaveBeenCalledTimes(3); - expect(mockEditor.insert).toHaveBeenCalledWith('\n'); - expect(mockEditor.insert).toHaveBeenCalledWith('----------\n'); - }); + expect(mockEditorInstance.insert).toHaveBeenCalledTimes(3); + expect(mockEditorInstance.insert).toHaveBeenNthCalledWith(1, '\n'); + expect(mockEditorInstance.insert).toHaveBeenNthCalledWith(2, '----------\n'); + expect(mockEditorInstance.insert).toHaveBeenNthCalledWith(3, '\n'); + }); - it('should not throw when editor is not loaded', () => { - fixture.componentRef.setInput('currentContent', mockCurrentContent); - fixture.componentRef.setInput('versionContent', mockVersionContent); - fixture.detectChanges(); + it('should not insert horizontal rule when editorInstance is null', () => { + (component as any).editorInstance = null; - expect(() => component.doHorizontalRule()).not.toThrow(); - }); + expect(() => component.doHorizontalRule()).not.toThrow(); }); - describe('openSyntaxHelpDialog', () => { - it('should open syntax help dialog', () => { - fixture.componentRef.setInput('currentContent', mockCurrentContent); - fixture.componentRef.setInput('versionContent', mockVersionContent); - fixture.detectChanges(); - - component.openSyntaxHelpDialog(); + it('should open syntax help dialog', () => { + component.openSyntaxHelpDialog(); - expect(mockCustomDialogService.open).toHaveBeenCalled(); + expect(mockCustomDialogService.open).toHaveBeenCalledWith(WikiSyntaxHelpDialogComponent, { + header: 'project.wiki.syntaxHelp.header', }); }); - describe('toggleAutocomplete', () => { - it('should toggle autocomplete on', () => { - fixture.componentRef.setInput('currentContent', mockCurrentContent); - fixture.componentRef.setInput('versionContent', mockVersionContent); - fixture.detectChanges(); + it('should configure editor when onEditorLoaded is called', () => { + component.onEditorLoaded(mockEditorInstance); - const mockEditor = { - setOptions: jest.fn(), - setShowPrintMargin: jest.fn(), - }; + expect((component as any).editorInstance).toBe(mockEditorInstance); + expect(mockEditorInstance.setShowPrintMargin).toHaveBeenCalledWith(false); + expect((global as any).ace.require).toHaveBeenCalledWith('ace/ext/language_tools'); + expect(mockEditorInstance.setOptions).toHaveBeenCalledWith({ + enableBasicAutocompletion: false, + enableLiveAutocompletion: false, + enableSnippets: [{}], + }); + }); - component.onEditorLoaded(mockEditor); - expect(component.autoCompleteEnabled).toBe(false); + it('should toggle autocomplete when editorInstance exists', () => { + (component as any).editorInstance = mockEditorInstance; + component.autoCompleteEnabled = false; - component.toggleAutocomplete(); + component.toggleAutocomplete(); - expect(component.autoCompleteEnabled).toBe(true); - expect(mockEditor.setOptions).toHaveBeenCalledWith({ - enableBasicAutocompletion: true, - enableLiveAutocompletion: true, - }); + expect(component.autoCompleteEnabled).toBe(true); + expect(mockEditorInstance.setOptions).toHaveBeenCalledWith({ + enableBasicAutocompletion: true, + enableLiveAutocompletion: true, }); - it('should toggle autocomplete off', () => { - fixture.componentRef.setInput('currentContent', mockCurrentContent); - fixture.componentRef.setInput('versionContent', mockVersionContent); - fixture.detectChanges(); + component.toggleAutocomplete(); - const mockEditor = { - setOptions: jest.fn(), - setShowPrintMargin: jest.fn(), - }; + expect(component.autoCompleteEnabled).toBe(false); + expect(mockEditorInstance.setOptions).toHaveBeenCalledWith({ + enableBasicAutocompletion: false, + enableLiveAutocompletion: false, + }); + }); - component.onEditorLoaded(mockEditor); - component.autoCompleteEnabled = true; + it('should not toggle autocomplete when editorInstance is null', () => { + (component as any).editorInstance = null; + component.autoCompleteEnabled = false; - component.toggleAutocomplete(); + component.toggleAutocomplete(); - expect(component.autoCompleteEnabled).toBe(false); - expect(mockEditor.setOptions).toHaveBeenCalledWith({ - enableBasicAutocompletion: false, - enableLiveAutocompletion: false, - }); - }); + expect(component.autoCompleteEnabled).toBe(false); + }); - it('should not throw when editor is not loaded', () => { - fixture.componentRef.setInput('currentContent', mockCurrentContent); - fixture.componentRef.setInput('versionContent', mockVersionContent); - fixture.detectChanges(); + it('should handle empty content values', () => { + fixture.componentRef.setInput('currentContent', ''); + fixture.componentRef.setInput('versionContent', ''); + fixture.detectChanges(); - expect(() => component.toggleAutocomplete()).not.toThrow(); - }); + expect(component.content).toBe(''); + expect(component.initialContent).toBe(''); }); }); diff --git a/src/app/shared/components/wiki/edit-section/edit-section.component.ts b/src/app/shared/components/wiki/edit-section/edit-section.component.ts index 1a4d84894..94393870d 100644 --- a/src/app/shared/components/wiki/edit-section/edit-section.component.ts +++ b/src/app/shared/components/wiki/edit-section/edit-section.component.ts @@ -16,7 +16,7 @@ import { WikiSyntaxHelpDialogComponent } from '../wiki-syntax-help-dialog/wiki-s @Component({ selector: 'osf-edit-section', - imports: [Panel, Button, TranslatePipe, FormsModule, LMarkdownEditorModule, Checkbox], + imports: [Checkbox, Panel, Button, TranslatePipe, FormsModule, LMarkdownEditorModule], templateUrl: './edit-section.component.html', styleUrl: './edit-section.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, diff --git a/src/app/shared/mappers/registration/registration-node.mapper.ts b/src/app/shared/mappers/registration/registration-node.mapper.ts index 9dcafab4d..6a296c7ff 100644 --- a/src/app/shared/mappers/registration/registration-node.mapper.ts +++ b/src/app/shared/mappers/registration/registration-node.mapper.ts @@ -2,11 +2,8 @@ import { CurrentResourceType } from '@osf/shared/enums/resource-type.enum'; import { replaceBadEncodedChars } from '@shared/helpers/format-bad-encoding.helper'; import { ProviderShortInfoModel } from '@shared/models/provider/provider.model'; import { RegistryProviderDetailsJsonApi } from '@shared/models/provider/registration-provider-json-api.model'; -import { RegistrationNodeModel, RegistrationResponses } from '@shared/models/registration/registration-node.model'; -import { - RegistrationNodeAttributesJsonApi, - RegistrationResponsesJsonApi, -} from '@shared/models/registration/registration-node-json-api.model'; +import { RegistrationNodeModel } from '@shared/models/registration/registration-node.model'; +import { RegistrationNodeAttributesJsonApi } from '@shared/models/registration/registration-node-json-api.model'; export class RegistrationNodeMapper { static getRegistrationNodeAttributes( @@ -52,7 +49,6 @@ export class RegistrationNodeMapper { pendingWithdrawal: attributes.pending_withdrawal, providerSpecificMetadata: attributes.provider_specific_metadata, registeredMeta: attributes.registered_meta, - registrationResponses: this.getRegistrationResponses(attributes.registration_responses), registrationSupplement: attributes.registration_supplement, reviewsState: attributes.reviews_state, revisionState: attributes.revision_state, @@ -64,18 +60,6 @@ export class RegistrationNodeMapper { }; } - static getRegistrationResponses(response: RegistrationResponsesJsonApi): RegistrationResponses { - return { - summary: response?.summary, - uploader: response?.uploader?.map((uploadItem) => ({ - fileId: uploadItem.file_id, - fileName: uploadItem.file_name, - fileUrls: uploadItem.file_urls, - fileHashes: uploadItem.file_hashes, - })), - }; - } - static getRegistrationProviderShortInfo(provider?: RegistryProviderDetailsJsonApi): ProviderShortInfoModel { if (!provider) { return {} as ProviderShortInfoModel; diff --git a/src/app/shared/models/registration/registration-node.model.ts b/src/app/shared/models/registration/registration-node.model.ts index 9f7dda0f2..fe861c23d 100644 --- a/src/app/shared/models/registration/registration-node.model.ts +++ b/src/app/shared/models/registration/registration-node.model.ts @@ -23,7 +23,6 @@ export interface RegistrationNodeModel extends BaseNodeModel { pendingWithdrawal: boolean; providerSpecificMetadata: string[]; registeredMeta: RegisteredMeta; - registrationResponses: RegistrationResponses; registrationSupplement: string; reviewsState: RegistrationReviewStates; revisionState: RevisionReviewStates; @@ -57,15 +56,3 @@ export interface FileUrls { export interface FileHashes { sha256: string; } - -export interface RegistrationUploader { - fileId: string; - fileName: string; - fileUrls: FileUrls; - fileHashes: FileHashes; -} - -export interface RegistrationResponses { - summary: string; - uploader: RegistrationUploader[]; -} diff --git a/src/app/shared/pipes/fix-special-char.pipe.ts b/src/app/shared/pipes/fix-special-char.pipe.ts index 31e3519ef..53c591764 100644 --- a/src/app/shared/pipes/fix-special-char.pipe.ts +++ b/src/app/shared/pipes/fix-special-char.pipe.ts @@ -2,7 +2,6 @@ import { Pipe, PipeTransform } from '@angular/core'; @Pipe({ name: 'fixSpecialChar', - standalone: true, }) export class FixSpecialCharPipe implements PipeTransform { transform(value: string | null | undefined): string { diff --git a/src/app/shared/pipes/sort-by-date.pipe.ts b/src/app/shared/pipes/sort-by-date.pipe.ts index d8a749454..d4d47369f 100644 --- a/src/app/shared/pipes/sort-by-date.pipe.ts +++ b/src/app/shared/pipes/sort-by-date.pipe.ts @@ -4,7 +4,6 @@ import { DateSortable } from '../models/user/date-sortable.model'; @Pipe({ name: 'sortByDate', - standalone: true, }) export class SortByDatePipe implements PipeTransform { transform(items: T[] | null | undefined): T[] { diff --git a/src/testing/mocks/linked-node.mock.ts b/src/testing/mocks/linked-node.mock.ts new file mode 100644 index 000000000..be3285ff1 --- /dev/null +++ b/src/testing/mocks/linked-node.mock.ts @@ -0,0 +1,16 @@ +import { LinkedNode } from '@osf/features/registry/models'; + +export const createMockLinkedNode = (overrides?: Partial): LinkedNode => ({ + id: 'node-123', + title: 'Test Node', + description: 'Test node description', + category: 'project', + dateCreated: '2024-01-01T00:00:00Z', + dateModified: '2024-01-02T00:00:00Z', + tags: ['tag1'], + isPublic: false, + contributors: [], + htmlUrl: 'https://example.com/node', + apiUrl: 'https://api.example.com/node', + ...overrides, +}); diff --git a/src/testing/mocks/linked-registration.mock.ts b/src/testing/mocks/linked-registration.mock.ts new file mode 100644 index 000000000..4e193baab --- /dev/null +++ b/src/testing/mocks/linked-registration.mock.ts @@ -0,0 +1,23 @@ +import { LinkedRegistration } from '@osf/features/registry/models/linked-nodes.model'; +import { RegistrationReviewStates } from '@osf/shared/enums/registration-review-states.enum'; + +export const createMockLinkedRegistration = (overrides?: Partial): LinkedRegistration => ({ + id: 'registration-123', + title: 'Test Registration', + description: 'Test description', + category: 'project', + dateCreated: '2024-01-01T00:00:00Z', + dateModified: '2024-01-02T00:00:00Z', + tags: ['tag1', 'tag2'], + isPublic: true, + reviewsState: RegistrationReviewStates.Accepted, + contributors: [], + currentUserPermissions: ['read', 'write'], + hasData: true, + hasAnalyticCode: false, + hasMaterials: true, + hasPapers: false, + hasSupplements: true, + registrationSupplement: 'supplement-123', + ...overrides, +}); diff --git a/src/testing/mocks/page-schema.mock.ts b/src/testing/mocks/page-schema.mock.ts new file mode 100644 index 000000000..48bb44ab5 --- /dev/null +++ b/src/testing/mocks/page-schema.mock.ts @@ -0,0 +1,29 @@ +import { PageSchema } from '@osf/shared/models/registration/page-schema.model'; + +export const createMockPageSchema = (overrides?: Partial): PageSchema => ({ + id: 'page-1', + title: 'Test Page', + description: 'Test description', + questions: [ + { + id: 'question-1', + displayText: 'Test Question', + required: false, + }, + ], + sections: [ + { + id: 'section-1', + title: 'Test Section', + description: 'Section description', + questions: [ + { + id: 'section-question-1', + displayText: 'Section Question', + required: true, + }, + ], + }, + ], + ...overrides, +}); diff --git a/src/testing/mocks/registry-overview.mock.ts b/src/testing/mocks/registration-overview-model.mock.ts similarity index 56% rename from src/testing/mocks/registry-overview.mock.ts rename to src/testing/mocks/registration-overview-model.mock.ts index a421ff72e..d61451fba 100644 --- a/src/testing/mocks/registry-overview.mock.ts +++ b/src/testing/mocks/registration-overview-model.mock.ts @@ -1,60 +1,63 @@ -import { RegistryOverview } from '@osf/features/registry/models'; +import { RegistrationOverviewModel } from '@osf/features/registry/models'; import { RegistrationReviewStates } from '@osf/shared/enums/registration-review-states.enum'; import { RegistryStatus } from '@osf/shared/enums/registry-status.enum'; import { RevisionReviewStates } from '@osf/shared/enums/revision-review-states.enum'; -export const MOCK_REGISTRY_OVERVIEW: RegistryOverview = { +export const MOCK_REGISTRATION_OVERVIEW_MODEL: RegistrationOverviewModel = { id: 'test-registry-id', type: 'registration', - isPublic: true, - forksCount: 0, title: 'Test Registry', description: 'Test Description', - dateModified: '2023-01-02T00:00:00Z', - dateCreated: '2023-01-01T00:00:00Z', - dateRegistered: '2023-01-01T00:00:00Z', - registrationType: 'osf-registration', - doi: '10.1234/test', - tags: [], - provider: undefined, - contributors: [], - citation: 'Test Citation', category: 'uncategorized', + customCitation: 'Test Custom Citation', + dateCreated: '2023-01-01T00:00:00Z', + dateModified: '2023-01-02T00:00:00Z', + isRegistration: true, + isPreprint: false, isFork: false, + isCollection: false, + isPublic: true, + tags: [], accessRequestsEnabled: false, - nodeLicense: undefined, - license: undefined, - licenseUrl: undefined, - identifiers: undefined, - analyticsKey: 'test-analytics-key', - currentUserCanComment: true, + nodeLicense: { + copyrightHolders: null, + year: null, + }, currentUserPermissions: [], currentUserIsContributor: false, - currentUserIsContributorOrGroupMember: false, wikiEnabled: false, - region: undefined, - subjects: undefined, - customCitation: 'Test Custom Citation', - hasData: false, + rootParentId: 'root-parent-id', + archiving: false, + articleDoi: '10.1234/test', + dateRegistered: '2023-01-01T00:00:00Z', + dateWithdrawn: null, + embargoEndDate: null, + embargoed: false, hasAnalyticCode: false, + hasData: false, hasMaterials: false, hasPapers: false, + hasProject: true, hasSupplements: false, - questions: { - summary: 'Test summary', - uploader: [], + iaUrl: null, + pendingEmbargoApproval: false, + pendingEmbargoTerminationApproval: false, + pendingRegistrationApproval: false, + pendingWithdrawal: false, + providerSpecificMetadata: [], + registeredMeta: { + summary: { extra: [], value: 'Test summary' }, + uploader: { extra: [], value: '' }, }, + registrationSupplement: '', + reviewsState: RegistrationReviewStates.Accepted, + revisionState: RevisionReviewStates.Approved, + withdrawalJustification: null, + withdrawn: false, registrationSchemaLink: 'https://example.com/schema', + licenseId: 'test-license-id', associatedProjectId: 'test-project-id', - schemaResponses: [], + providerId: 'test-provider-id', status: RegistryStatus.Accepted, - revisionStatus: RevisionReviewStates.Approved, - reviewsState: RegistrationReviewStates.Accepted, - archiving: false, - embargoEndDate: '2024-01-01T00:00:00Z', - withdrawn: false, - withdrawalJustification: undefined, - dateWithdrawn: null, - rootParentId: null, - iaUrl: null, + forksCount: 0, }; diff --git a/src/testing/mocks/registry-component.mock.ts b/src/testing/mocks/registry-component.mock.ts new file mode 100644 index 000000000..ff7561599 --- /dev/null +++ b/src/testing/mocks/registry-component.mock.ts @@ -0,0 +1,17 @@ +import { RegistryComponentModel } from '@osf/features/registry/models'; + +export const createMockRegistryComponent = (overrides?: Partial): RegistryComponentModel => ({ + id: 'component-123', + title: 'Test Component', + description: 'Test component description', + category: 'project', + dateCreated: '2024-01-01T00:00:00Z', + dateModified: '2024-01-02T00:00:00Z', + dateRegistered: '2024-01-03T00:00:00Z', + registrationSupplement: 'supplement-456', + tags: ['tag1', 'tag2'], + isPublic: true, + contributors: [], + registry: 'test-registry', + ...overrides, +}); diff --git a/src/testing/mocks/schema-response.mock.ts b/src/testing/mocks/schema-response.mock.ts new file mode 100644 index 000000000..780b8ea5f --- /dev/null +++ b/src/testing/mocks/schema-response.mock.ts @@ -0,0 +1,21 @@ +import { RevisionReviewStates } from '@osf/shared/enums/revision-review-states.enum'; +import { SchemaResponse } from '@osf/shared/models/registration/schema-response.model'; + +export const createMockSchemaResponse = ( + id: string, + reviewsState: RevisionReviewStates, + isOriginalResponse = false +): SchemaResponse => ({ + id, + dateCreated: '2023-01-01T00:00:00Z', + dateSubmitted: '2023-01-02T00:00:00Z', + dateModified: '2023-01-03T00:00:00Z', + revisionJustification: 'Test justification', + revisionResponses: {}, + updatedResponseKeys: [], + reviewsState, + isPendingCurrentUserApproval: false, + isOriginalResponse, + registrationSchemaId: 'schema-id', + registrationId: 'registration-id', +});