diff --git a/jest.config.js b/jest.config.js index 118091e14..73eb092c6 100644 --- a/jest.config.js +++ b/jest.config.js @@ -63,7 +63,6 @@ module.exports = { '/src/app/app.routes.ts', '/src/app/features/files/components', '/src/app/features/files/pages/file-detail', - '/src/app/features/preprints/', '/src/app/features/project/addons/', '/src/app/features/project/overview/', '/src/app/features/project/registrations', diff --git a/src/app/features/institutions/pages/institutions-list/institutions-list.component.spec.ts b/src/app/features/institutions/pages/institutions-list/institutions-list.component.spec.ts index 0ce011f73..a875cbcb0 100644 --- a/src/app/features/institutions/pages/institutions-list/institutions-list.component.spec.ts +++ b/src/app/features/institutions/pages/institutions-list/institutions-list.component.spec.ts @@ -1,8 +1,7 @@ import { MockProvider } from 'ng-mocks'; -import { PaginatorState } from 'primeng/paginator'; - import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormControl } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; import { InstitutionsSelectors } from '@osf/shared/stores/institutions'; @@ -54,69 +53,38 @@ describe.skip('Component: Institutions List', () => { expect(component).toBeTruthy(); }); - it('should update currentPage, first, and call updateQueryParams when page is provided', () => { - const paginatorEvent: PaginatorState = { - page: 1, - first: 20, - rows: 10, - pageCount: 5, - }; - - component.onPageChange(paginatorEvent); - - expect(component.currentPage()).toBe(2); - expect(component.first()).toBe(20); - expect(routerMock.navigate).toHaveBeenCalledWith([], { - relativeTo: expect.any(Object), - queryParams: { - page: '2', - size: '10', - }, - queryParamsHandling: 'merge', - }); + it('should initialize with correct default values', () => { + expect(component.classes).toBe('flex-1 flex flex-column w-full'); + expect(component.searchControl).toBeInstanceOf(FormControl); + expect(component.searchControl.value).toBe(''); + }); + + it('should return institutions from store', () => { + const institutions = component.institutions(); + expect(institutions).toBe(mockInstitutions); + }); + + it('should return loading state from store', () => { + const loading = component.institutionsLoading(); + expect(loading).toBe(false); }); - it('should set currentPage to 1 when page is not provided', () => { - const paginatorEvent: PaginatorState = { - page: undefined, - first: 0, - rows: 20, - pageCount: 3, - }; - - component.onPageChange(paginatorEvent); - - expect(component.currentPage()).toBe(1); - expect(component.first()).toBe(0); - expect(routerMock.navigate).toHaveBeenCalledWith([], { - relativeTo: expect.any(Object), - queryParams: { - page: '1', - size: '20', - }, - queryParamsHandling: 'merge', - }); + it('should handle search control value changes', () => { + const searchValue = 'test search'; + component.searchControl.setValue(searchValue); + + expect(component.searchControl.value).toBe(searchValue); }); - it('should handle first being undefined', () => { - const paginatorEvent: PaginatorState = { - page: 2, - first: undefined, - rows: 15, - pageCount: 4, - }; - - component.onPageChange(paginatorEvent); - - expect(component.currentPage()).toBe(2); - expect(component.first()).toBe(0); - expect(routerMock.navigate).toHaveBeenCalledWith([], { - relativeTo: expect.any(Object), - queryParams: { - page: '2', - size: '15', - }, - queryParamsHandling: 'merge', - }); + it('should handle empty search', () => { + component.searchControl.setValue(''); + + expect(component.searchControl.value).toBe(''); + }); + + it('should handle null search value', () => { + component.searchControl.setValue(null); + + expect(component.searchControl.value).toBe(null); }); }); diff --git a/src/app/features/preprints/components/advisory-board/advisory-board.component.spec.ts b/src/app/features/preprints/components/advisory-board/advisory-board.component.spec.ts index d65d19e97..588caf723 100644 --- a/src/app/features/preprints/components/advisory-board/advisory-board.component.spec.ts +++ b/src/app/features/preprints/components/advisory-board/advisory-board.component.spec.ts @@ -6,6 +6,9 @@ describe('AdvisoryBoardComponent', () => { let component: AdvisoryBoardComponent; let fixture: ComponentFixture; + const mockHtmlContent = + '

Advisory Board

This is advisory board content.

'; + beforeEach(async () => { await TestBed.configureTestingModule({ imports: [AdvisoryBoardComponent], @@ -19,4 +22,71 @@ describe('AdvisoryBoardComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + it('should have default input values', () => { + expect(component.htmlContent()).toBeNull(); + expect(component.brand()).toBeUndefined(); + expect(component.isLandingPage()).toBe(false); + }); + + it('should not render section when htmlContent is null', () => { + fixture.detectChanges(); + + const compiled = fixture.nativeElement; + const section = compiled.querySelector('section'); + + expect(section).toBeNull(); + }); + + it('should not render section when htmlContent is undefined', () => { + fixture.componentRef.setInput('htmlContent', undefined); + fixture.detectChanges(); + + const compiled = fixture.nativeElement; + const section = compiled.querySelector('section'); + + expect(section).toBeNull(); + }); + + it('should render section when htmlContent is provided', () => { + fixture.componentRef.setInput('htmlContent', mockHtmlContent); + fixture.detectChanges(); + + const compiled = fixture.nativeElement; + const section = compiled.querySelector('section'); + + expect(section).toBeTruthy(); + expect(section.innerHTML).toBe(mockHtmlContent); + }); + + it('should apply correct CSS classes when isLandingPage is false', () => { + fixture.componentRef.setInput('htmlContent', mockHtmlContent); + fixture.componentRef.setInput('isLandingPage', false); + fixture.detectChanges(); + + const compiled = fixture.nativeElement; + const section = compiled.querySelector('section'); + + expect(section).toBeTruthy(); + expect(section.classList.contains('osf-preprint-service')).toBe(false); + expect(section.classList.contains('preprints-advisory-board-section')).toBe(true); + expect(section.classList.contains('pt-3')).toBe(true); + expect(section.classList.contains('pb-5')).toBe(true); + expect(section.classList.contains('px-3')).toBe(true); + expect(section.classList.contains('flex')).toBe(true); + expect(section.classList.contains('flex-column')).toBe(true); + }); + + it('should apply correct CSS classes when isLandingPage is true', () => { + fixture.componentRef.setInput('htmlContent', mockHtmlContent); + fixture.componentRef.setInput('isLandingPage', true); + fixture.detectChanges(); + + const compiled = fixture.nativeElement; + const section = compiled.querySelector('section'); + + expect(section).toBeTruthy(); + expect(section.classList.contains('osf-preprint-service')).toBe(true); + expect(section.classList.contains('preprints-advisory-board-section')).toBe(true); + }); }); diff --git a/src/app/features/preprints/components/browse-by-subjects/browse-by-subjects.component.spec.ts b/src/app/features/preprints/components/browse-by-subjects/browse-by-subjects.component.spec.ts index 369fbe7c0..806ac482b 100644 --- a/src/app/features/preprints/components/browse-by-subjects/browse-by-subjects.component.spec.ts +++ b/src/app/features/preprints/components/browse-by-subjects/browse-by-subjects.component.spec.ts @@ -1,34 +1,143 @@ -import { TranslatePipe } from '@ngx-translate/core'; -import { MockPipe } from 'ng-mocks'; - import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { provideRouter } from '@angular/router'; -import { TranslateServiceMock } from '@shared/mocks'; +import { ResourceType } from '@shared/enums'; +import { SubjectModel } from '@shared/models'; import { BrowseBySubjectsComponent } from './browse-by-subjects.component'; +import { SUBJECTS_MOCK } from '@testing/mocks/subject.mock'; +import { OSFTestingModule } from '@testing/osf.testing.module'; + describe('BrowseBySubjectsComponent', () => { let component: BrowseBySubjectsComponent; let fixture: ComponentFixture; + const mockSubjects: SubjectModel[] = SUBJECTS_MOCK; + beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [BrowseBySubjectsComponent, MockPipe(TranslatePipe)], - providers: [provideRouter([]), TranslateServiceMock], + imports: [BrowseBySubjectsComponent, OSFTestingModule], }).compileComponents(); fixture = TestBed.createComponent(BrowseBySubjectsComponent); component = fixture.componentInstance; + }); + it('should create', () => { fixture.componentRef.setInput('subjects', []); fixture.componentRef.setInput('areSubjectsLoading', false); fixture.componentRef.setInput('isProviderLoading', false); fixture.detectChanges(); + expect(component).toBeTruthy(); }); - it('should create', () => { - expect(component).toBeTruthy(); + it('should have default input values', () => { + fixture.componentRef.setInput('subjects', []); + fixture.componentRef.setInput('areSubjectsLoading', false); + fixture.componentRef.setInput('isProviderLoading', false); + fixture.detectChanges(); + + expect(component.subjects()).toEqual([]); + expect(component.areSubjectsLoading()).toBe(false); + expect(component.isProviderLoading()).toBe(false); + expect(component.isLandingPage()).toBe(false); + }); + + it('should display title', () => { + fixture.componentRef.setInput('subjects', []); + fixture.componentRef.setInput('areSubjectsLoading', false); + fixture.componentRef.setInput('isProviderLoading', false); + fixture.detectChanges(); + + const compiled = fixture.nativeElement; + const title = compiled.querySelector('h2'); + + expect(title).toBeTruthy(); + expect(title.textContent).toBe('preprints.browseBySubjects.title'); + }); + + it('should display correct subject names in buttons', () => { + fixture.componentRef.setInput('subjects', mockSubjects); + fixture.componentRef.setInput('areSubjectsLoading', false); + fixture.componentRef.setInput('isProviderLoading', false); + fixture.detectChanges(); + + const compiled = fixture.nativeElement; + const buttons = compiled.querySelectorAll('p-button'); + + expect(buttons[0].getAttribute('ng-reflect-label')).toBe('Mathematics'); + expect(buttons[1].getAttribute('ng-reflect-label')).toBe('Physics'); + }); + + it('should compute linksToSearchPageForSubject correctly', () => { + fixture.componentRef.setInput('subjects', mockSubjects); + fixture.componentRef.setInput('areSubjectsLoading', false); + fixture.componentRef.setInput('isProviderLoading', false); + fixture.detectChanges(); + + const links = component.linksToSearchPageForSubject(); + + expect(links).toHaveLength(2); + expect(links[0]).toEqual({ + tab: ResourceType.Preprint, + filter_subject: 'https://example.com/subjects/mathematics', + }); + expect(links[1]).toEqual({ + tab: ResourceType.Preprint, + filter_subject: 'https://example.com/subjects/physics', + }); + }); + + it('should set correct routerLink for non-landing page', () => { + fixture.componentRef.setInput('subjects', mockSubjects); + fixture.componentRef.setInput('areSubjectsLoading', false); + fixture.componentRef.setInput('isProviderLoading', false); + fixture.componentRef.setInput('isLandingPage', false); + fixture.detectChanges(); + + const compiled = fixture.nativeElement; + const buttons = compiled.querySelectorAll('p-button'); + + expect(buttons[0].getAttribute('ng-reflect-router-link')).toBe('discover'); + }); + + it('should set correct routerLink for landing page', () => { + fixture.componentRef.setInput('subjects', mockSubjects); + fixture.componentRef.setInput('areSubjectsLoading', false); + fixture.componentRef.setInput('isProviderLoading', false); + fixture.componentRef.setInput('isLandingPage', true); + fixture.detectChanges(); + + const compiled = fixture.nativeElement; + const buttons = compiled.querySelectorAll('p-button'); + + expect(buttons[0].getAttribute('ng-reflect-router-link')).toBe('/search'); + }); + + it('should handle subjects without iri', () => { + const subjectsWithoutIri: SubjectModel[] = [ + { + id: 'subject-1', + name: 'Physics', + iri: undefined, + children: [], + parent: null, + expanded: false, + }, + ]; + + fixture.componentRef.setInput('subjects', subjectsWithoutIri); + fixture.componentRef.setInput('areSubjectsLoading', false); + fixture.componentRef.setInput('isProviderLoading', false); + fixture.detectChanges(); + + const links = component.linksToSearchPageForSubject(); + + expect(links).toHaveLength(1); + expect(links[0]).toEqual({ + tab: ResourceType.Preprint, + filter_subject: undefined, + }); }); }); diff --git a/src/app/features/preprints/components/preprint-details/additional-info/additional-info.component.spec.ts b/src/app/features/preprints/components/preprint-details/additional-info/additional-info.component.spec.ts index ce10e28aa..f4f98b3c1 100644 --- a/src/app/features/preprints/components/preprint-details/additional-info/additional-info.component.spec.ts +++ b/src/app/features/preprints/components/preprint-details/additional-info/additional-info.component.spec.ts @@ -1,34 +1,54 @@ -import { Store } from '@ngxs/store'; - -import { TranslatePipe } from '@ngx-translate/core'; -import { MockPipe, MockProvider } from 'ng-mocks'; +import { MockComponent, MockPipe } from 'ng-mocks'; import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { CitationSectionComponent } from '@osf/features/preprints/components/preprint-details/citation-section/citation-section.component'; import { PreprintSelectors } from '@osf/features/preprints/store/preprint'; -import { MOCK_STORE, TranslateServiceMock } from '@shared/mocks'; +import { InterpolatePipe } from '@shared/pipes'; import { SubjectsSelectors } from '@shared/stores'; import { AdditionalInfoComponent } from './additional-info.component'; +import { PREPRINT_MOCK } from '@testing/mocks/preprint.mock'; +import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideMockStore } from '@testing/providers/store-provider.mock'; + describe('AdditionalInfoComponent', () => { let component: AdditionalInfoComponent; let fixture: ComponentFixture; - const mockStore = MOCK_STORE; + const mockPreprint = PREPRINT_MOCK; beforeEach(async () => { - (mockStore.selectSignal as jest.Mock).mockImplementation((selector) => { - if (selector === PreprintSelectors.getPreprint) return () => null; - if (selector === PreprintSelectors.isPreprintLoading) return () => false; - if (selector === SubjectsSelectors.getSelectedSubjects) return () => []; - if (selector === SubjectsSelectors.areSelectedSubjectsLoading) return () => false; - return () => null; - }); - await TestBed.configureTestingModule({ - imports: [AdditionalInfoComponent, MockPipe(TranslatePipe)], - providers: [MockProvider(Store, mockStore), TranslateServiceMock], + imports: [ + AdditionalInfoComponent, + OSFTestingModule, + MockComponent(CitationSectionComponent), + MockPipe(InterpolatePipe), + ], + providers: [ + provideMockStore({ + signals: [ + { + selector: PreprintSelectors.getPreprint, + value: mockPreprint, + }, + { + selector: PreprintSelectors.isPreprintLoading, + value: false, + }, + { + selector: SubjectsSelectors.getSelectedSubjects, + value: [], + }, + { + selector: SubjectsSelectors.areSelectedSubjectsLoading, + value: false, + }, + ], + }), + ], }).compileComponents(); fixture = TestBed.createComponent(AdditionalInfoComponent); @@ -39,4 +59,29 @@ describe('AdditionalInfoComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + it('should return license from preprint when available', () => { + const license = component.license(); + expect(license).toBe(mockPreprint.embeddedLicense); + }); + + it('should return license options record from preprint when available', () => { + const licenseOptionsRecord = component.licenseOptionsRecord(); + expect(licenseOptionsRecord).toEqual(mockPreprint.licenseOptions); + }); + + it('should have skeleton data array with 5 null elements', () => { + expect(component.skeletonData).toHaveLength(5); + expect(component.skeletonData.every((item) => item === null)).toBe(true); + }); + + it('should return license from preprint when available', () => { + const license = component.license(); + expect(license).toBe(mockPreprint.embeddedLicense); + }); + + it('should return license options record from preprint when available', () => { + const licenseOptionsRecord = component.licenseOptionsRecord(); + expect(licenseOptionsRecord).toEqual(mockPreprint.licenseOptions); + }); }); diff --git a/src/app/features/preprints/components/preprint-details/citation-section/citation-section.component.spec.ts b/src/app/features/preprints/components/preprint-details/citation-section/citation-section.component.spec.ts index 34e2232c5..599b465d6 100644 --- a/src/app/features/preprints/components/preprint-details/citation-section/citation-section.component.spec.ts +++ b/src/app/features/preprints/components/preprint-details/citation-section/citation-section.component.spec.ts @@ -1,25 +1,104 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { TranslateServiceMock } from '@shared/mocks'; +import { CitationStyle } from '@shared/models'; +import { CitationsSelectors } from '@shared/stores'; import { CitationSectionComponent } from './citation-section.component'; -describe.skip('CitationSectionComponent', () => { +import { CITATION_STYLES_MOCK } from '@testing/mocks/citation-style.mock'; +import { TranslationServiceMock } from '@testing/mocks/translation.service.mock'; +import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideMockStore } from '@testing/providers/store-provider.mock'; + +describe('CitationSectionComponent', () => { let component: CitationSectionComponent; let fixture: ComponentFixture; + const mockCitationStyles: CitationStyle[] = CITATION_STYLES_MOCK; + const mockDefaultCitations = { + apa: 'APA Citation Text', + mla: 'MLA Citation Text', + }; + const mockStyledCitation = 'Styled Citation Text'; + beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [CitationSectionComponent], - providers: [TranslateServiceMock], + imports: [CitationSectionComponent, OSFTestingModule], + providers: [ + TranslationServiceMock, + provideMockStore({ + signals: [ + { + selector: CitationsSelectors.getDefaultCitations, + value: mockDefaultCitations, + }, + { + selector: CitationsSelectors.getDefaultCitationsLoading, + value: false, + }, + { + selector: CitationsSelectors.getCitationStyles, + value: mockCitationStyles, + }, + { + selector: CitationsSelectors.getCitationStylesLoading, + value: false, + }, + { + selector: CitationsSelectors.getStyledCitation, + value: mockStyledCitation, + }, + ], + }), + ], }).compileComponents(); fixture = TestBed.createComponent(CitationSectionComponent); component = fixture.componentInstance; - fixture.detectChanges(); + + fixture.componentRef.setInput('preprintId', 'test-preprint-id'); }); it('should create', () => { expect(component).toBeTruthy(); }); + + it('should return default citations from store', () => { + const defaultCitations = component.defaultCitations(); + expect(defaultCitations).toBe(mockDefaultCitations); + }); + + it('should return citation styles from store', () => { + const citationStyles = component.citationStyles(); + expect(citationStyles).toBe(mockCitationStyles); + }); + + it('should return styled citation from store', () => { + const styledCitation = component.styledCitation(); + expect(styledCitation).toBe(mockStyledCitation); + }); + + it('should have citation styles options signal', () => { + const citationStylesOptions = component.citationStylesOptions(); + expect(citationStylesOptions).toBeDefined(); + expect(Array.isArray(citationStylesOptions)).toBe(true); + }); + + it('should handle citation style filter search', () => { + const mockEvent = { + originalEvent: new Event('input'), + filter: 'test filter', + }; + + expect(() => component.handleCitationStyleFilterSearch(mockEvent)).not.toThrow(); + }); + + it('should handle get styled citation', () => { + const mockEvent = { + value: { id: 'style-1' }, + originalEvent: new Event('change'), + }; + + expect(() => component.handleGetStyledCitation(mockEvent)).not.toThrow(); + }); }); diff --git a/src/app/features/preprints/components/preprint-details/general-information/general-information.component.spec.ts b/src/app/features/preprints/components/preprint-details/general-information/general-information.component.spec.ts index 4ebb014fc..a99d42f8f 100644 --- a/src/app/features/preprints/components/preprint-details/general-information/general-information.component.spec.ts +++ b/src/app/features/preprints/components/preprint-details/general-information/general-information.component.spec.ts @@ -1,55 +1,117 @@ -import { Store } from '@ngxs/store'; +import { MockComponents, MockProvider } from 'ng-mocks'; -import { TranslatePipe } from '@ngx-translate/core'; -import { MockPipe, MockProvider } from 'ng-mocks'; - -import { Location } from '@angular/common'; -import { signal } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { Router } from '@angular/router'; +import { ENVIRONMENT } from '@core/provider/environment.provider'; +import { PreprintDoiSectionComponent } from '@osf/features/preprints/components/preprint-details/preprint-doi-section/preprint-doi-section.component'; +import { PreprintProviderDetails } from '@osf/features/preprints/models'; import { PreprintSelectors } from '@osf/features/preprints/store/preprint'; -import { MOCK_STORE, TranslateServiceMock } from '@shared/mocks'; -import { ContributorsSelectors } from '@shared/stores'; +import { AffiliatedInstitutionsViewComponent, IconComponent, TruncatedTextComponent } from '@shared/components'; +import { MOCK_CONTRIBUTOR, MOCK_INSTITUTION } from '@shared/mocks'; +import { ContributorsSelectors, InstitutionsSelectors } from '@shared/stores'; import { GeneralInformationComponent } from './general-information.component'; +import { PREPRINT_MOCK } from '@testing/mocks/preprint.mock'; +import { PREPRINT_PROVIDER_DETAILS_MOCK } from '@testing/mocks/preprint-provider-details'; +import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideMockStore } from '@testing/providers/store-provider.mock'; + describe('GeneralInformationComponent', () => { let component: GeneralInformationComponent; let fixture: ComponentFixture; - const mockStore = MOCK_STORE; + const mockPreprint = PREPRINT_MOCK; + const mockContributors = [MOCK_CONTRIBUTOR]; + const mockInstitutions = [MOCK_INSTITUTION]; + const mockPreprintProvider: PreprintProviderDetails = PREPRINT_PROVIDER_DETAILS_MOCK; + const mockWebUrl = 'https://staging4.osf.io'; beforeEach(async () => { - (mockStore.selectSignal as jest.Mock).mockImplementation((selector) => { - if ( - selector === PreprintSelectors.getPreprint || - selector === PreprintSelectors.isPreprintLoading || - selector === PreprintSelectors.getPreprintVersionIds || - selector === PreprintSelectors.arePreprintVersionIdsLoading - ) { - return signal(null); - } - if ( - selector === ContributorsSelectors.getContributors || - selector === ContributorsSelectors.isContributorsLoading - ) { - return signal([]); - } - return signal(null); - }); - await TestBed.configureTestingModule({ - imports: [GeneralInformationComponent, MockPipe(TranslatePipe)], - providers: [MockProvider(Store, mockStore), MockProvider(Router), MockProvider(Location), TranslateServiceMock], + imports: [ + GeneralInformationComponent, + OSFTestingModule, + ...MockComponents( + TruncatedTextComponent, + PreprintDoiSectionComponent, + IconComponent, + AffiliatedInstitutionsViewComponent + ), + ], + providers: [ + MockProvider(ENVIRONMENT, { + webUrl: mockWebUrl, + }), + provideMockStore({ + signals: [ + { + selector: PreprintSelectors.getPreprint, + value: mockPreprint, + }, + { + selector: PreprintSelectors.isPreprintLoading, + value: false, + }, + { + selector: ContributorsSelectors.getContributors, + value: mockContributors, + }, + { + selector: ContributorsSelectors.isContributorsLoading, + value: false, + }, + { + selector: InstitutionsSelectors.getResourceInstitutions, + value: mockInstitutions, + }, + ], + }), + ], }).compileComponents(); fixture = TestBed.createComponent(GeneralInformationComponent); component = fixture.componentInstance; - fixture.detectChanges(); + + fixture.componentRef.setInput('preprintProvider', mockPreprintProvider); }); it('should create', () => { expect(component).toBeTruthy(); }); + + it('should return preprint from store', () => { + const preprint = component.preprint(); + expect(preprint).toBe(mockPreprint); + }); + + it('should return contributors from store', () => { + const contributors = component.contributors(); + expect(contributors).toBe(mockContributors); + }); + + it('should filter bibliographic contributors', () => { + const bibliographicContributors = component.bibliographicContributors(); + expect(bibliographicContributors).toHaveLength(1); + expect(bibliographicContributors.every((contributor) => contributor.isBibliographic)).toBe(true); + }); + + it('should return affiliated institutions from store', () => { + const institutions = component.affiliatedInstitutions(); + expect(institutions).toBe(mockInstitutions); + }); + + it('should compute node link from preprint', () => { + const nodeLink = component.nodeLink(); + expect(nodeLink).toBe(`${mockWebUrl}/node-123`); + }); + + it('should have skeleton data array with 5 null elements', () => { + expect(component.skeletonData).toHaveLength(5); + expect(component.skeletonData.every((item) => item === null)).toBe(true); + }); + + it('should have preprint provider input', () => { + expect(component.preprintProvider()).toBe(mockPreprintProvider); + }); }); diff --git a/src/app/features/preprints/components/preprint-details/make-decision/make-decision.component.spec.ts b/src/app/features/preprints/components/preprint-details/make-decision/make-decision.component.spec.ts index 54701ba1e..e9ca1062d 100644 --- a/src/app/features/preprints/components/preprint-details/make-decision/make-decision.component.spec.ts +++ b/src/app/features/preprints/components/preprint-details/make-decision/make-decision.component.spec.ts @@ -1,22 +1,200 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ReviewAction } from '@osf/features/moderation/models'; +import { ProviderReviewsWorkflow, ReviewsState } from '@osf/features/preprints/enums'; +import { PreprintProviderDetails, PreprintRequest } from '@osf/features/preprints/models'; +import { PreprintSelectors } from '@osf/features/preprints/store/preprint'; + import { MakeDecisionComponent } from './make-decision.component'; -describe.skip('MakeDecisionComponent', () => { +import { PREPRINT_MOCK } from '@testing/mocks/preprint.mock'; +import { PREPRINT_PROVIDER_DETAILS_MOCK } from '@testing/mocks/preprint-provider-details'; +import { PREPRINT_REQUEST_MOCK } from '@testing/mocks/preprint-request.mock'; +import { REVIEW_ACTION_MOCK } from '@testing/mocks/review-action.mock'; +import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideMockStore } from '@testing/providers/store-provider.mock'; + +describe('MakeDecisionComponent', () => { let component: MakeDecisionComponent; let fixture: ComponentFixture; + const mockPreprint = PREPRINT_MOCK; + const mockProvider: PreprintProviderDetails = PREPRINT_PROVIDER_DETAILS_MOCK; + const mockLatestAction: ReviewAction = REVIEW_ACTION_MOCK; + const mockWithdrawalRequest: PreprintRequest = PREPRINT_REQUEST_MOCK; + beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [MakeDecisionComponent], + imports: [MakeDecisionComponent, OSFTestingModule], + providers: [ + provideMockStore({ + signals: [ + { + selector: PreprintSelectors.getPreprint, + value: mockPreprint, + }, + ], + }), + ], }).compileComponents(); fixture = TestBed.createComponent(MakeDecisionComponent); component = fixture.componentInstance; - fixture.detectChanges(); + + fixture.componentRef.setInput('provider', mockProvider); + fixture.componentRef.setInput('latestAction', mockLatestAction); + fixture.componentRef.setInput('latestWithdrawalRequest', mockWithdrawalRequest); + fixture.componentRef.setInput('isPendingWithdrawal', false); }); it('should create', () => { expect(component).toBeTruthy(); }); + + it('should return preprint from store', () => { + const preprint = component.preprint(); + expect(preprint).toBe(mockPreprint); + }); + + it('should compute label decision button for pending preprint', () => { + const label = component.labelDecisionButton(); + expect(label).toBe('preprints.details.decision.makeDecision'); + }); + + it('should compute make decision button disabled state', () => { + const disabled = component.makeDecisionButtonDisabled(); + expect(disabled).toBe(false); + }); + + it('should compute label decision dialog header for pending preprint', () => { + const header = component.labelDecisionDialogHeader(); + expect(header).toBe('preprints.details.decision.header.submitDecision'); + }); + + it('should compute label submit button for pending preprint', () => { + const label = component.labelSubmitButton(); + expect(label).toBe('preprints.details.decision.submitButton.submitDecision'); + }); + + it('should compute accept option explanation for pre-moderation', () => { + const explanation = component.acceptOptionExplanation(); + expect(explanation).toBe('preprints.details.decision.accept.pre'); + }); + + it('should compute reject option label for unpublished preprint', () => { + const label = component.rejectOptionLabel(); + expect(label).toBe('preprints.details.decision.reject.label'); + }); + + it('should compute settings comments for private comments', () => { + const settings = component.settingsComments(); + expect(settings).toBeDefined(); + }); + + it('should compute settings names for named comments', () => { + const settings = component.settingsNames(); + expect(settings).toBeDefined(); + }); + + it('should compute settings moderation for pre-moderation workflow', () => { + const settings = component.settingsModeration(); + expect(settings).toBeDefined(); + }); + + it('should compute comment edited state', () => { + const edited = component.commentEdited(); + expect(typeof edited).toBe('boolean'); + }); + + it('should compute comment exceeds limit state', () => { + const exceeds = component.commentExceedsLimit(); + expect(typeof exceeds).toBe('boolean'); + }); + + it('should compute decision changed state', () => { + const changed = component.decisionChanged(); + expect(typeof changed).toBe('boolean'); + }); + + it('should have initial signal values', () => { + expect(component.dialogVisible).toBe(false); + expect(component.didValidate()).toBe(false); + expect(component.decision()).toBe(ReviewsState.Accepted); + expect(component.saving()).toBe(false); + }); + + it('should handle request decision toggled', () => { + expect(() => component.requestDecisionToggled()).not.toThrow(); + }); + + it('should handle cancel', () => { + expect(() => component.cancel()).not.toThrow(); + }); + + it('should compute label submit button when decision changed', () => { + jest.spyOn(component, 'isPendingWithdrawal').mockReturnValue(false); + jest.spyOn(component, 'preprint').mockReturnValue({ ...mockPreprint, reviewsState: ReviewsState.Accepted }); + jest.spyOn(component, 'decisionChanged').mockReturnValue(true); + jest.spyOn(component, 'commentEdited').mockReturnValue(false); + const label = component.labelSubmitButton(); + expect(label).toBe('preprints.details.decision.submitButton.modifyDecision'); + }); + + it('should compute label submit button when comment edited', () => { + jest.spyOn(component, 'isPendingWithdrawal').mockReturnValue(false); + jest.spyOn(component, 'preprint').mockReturnValue({ ...mockPreprint, reviewsState: ReviewsState.Accepted }); + jest.spyOn(component, 'decisionChanged').mockReturnValue(false); + jest.spyOn(component, 'commentEdited').mockReturnValue(true); + const label = component.labelSubmitButton(); + expect(label).toBe('preprints.details.decision.submitButton.updateComment'); + }); + + it('should compute submit button disabled when neither decision changed nor comment edited', () => { + jest.spyOn(component, 'decisionChanged').mockReturnValue(false); + jest.spyOn(component, 'commentEdited').mockReturnValue(false); + const disabled = component.submitButtonDisabled(); + expect(disabled).toBe(true); + }); + + it('should compute accept option explanation for post-moderation', () => { + const postModerationProvider = { ...mockProvider, reviewsWorkflow: ProviderReviewsWorkflow.PostModeration }; + fixture.componentRef.setInput('provider', postModerationProvider); + const explanation = component.acceptOptionExplanation(); + expect(explanation).toBe('preprints.details.decision.accept.post'); + }); + + it('should compute label request decision justification for accepted decision', () => { + component.decision.set(ReviewsState.Accepted); + const label = component.labelRequestDecisionJustification(); + expect(label).toBe('preprints.details.decision.withdrawalJustification'); + }); + + it('should compute label request decision justification for rejected decision', () => { + component.decision.set(ReviewsState.Rejected); + const label = component.labelRequestDecisionJustification(); + expect(label).toBe('preprints.details.decision.denialJustification'); + }); + + it('should compute reject option explanation for pre-moderation with accepted preprint', () => { + jest.spyOn(component, 'preprint').mockReturnValue({ ...mockPreprint, reviewsState: ReviewsState.Accepted }); + const explanation = component.rejectOptionExplanation(); + expect(explanation).toBe('preprints.details.decision.approve.explanation'); + }); + + it('should compute reject option explanation for post-moderation', () => { + const postModerationProvider = { ...mockProvider, reviewsWorkflow: ProviderReviewsWorkflow.PostModeration }; + fixture.componentRef.setInput('provider', postModerationProvider); + const explanation = component.rejectOptionExplanation(); + expect(explanation).toBeDefined(); + }); + + it('should compute reject radio button value for published preprint', () => { + jest.spyOn(component, 'preprint').mockReturnValue({ ...mockPreprint, isPublished: true }); + const value = component.rejectRadioButtonValue(); + expect(value).toBe(ReviewsState.Withdrawn); + }); + + it('should handle submit method', () => { + expect(() => component.submit()).not.toThrow(); + }); }); diff --git a/src/app/features/preprints/components/preprint-details/moderation-status-banner/moderation-status-banner.component.spec.ts b/src/app/features/preprints/components/preprint-details/moderation-status-banner/moderation-status-banner.component.spec.ts index 5ce1214d5..62a9e563f 100644 --- a/src/app/features/preprints/components/preprint-details/moderation-status-banner/moderation-status-banner.component.spec.ts +++ b/src/app/features/preprints/components/preprint-details/moderation-status-banner/moderation-status-banner.component.spec.ts @@ -1,22 +1,199 @@ +import { MockComponent, MockPipes, MockProvider } from 'ng-mocks'; + +import { DatePipe, TitleCasePipe } from '@angular/common'; import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ENVIRONMENT } from '@core/provider/environment.provider'; +import { ReviewAction } from '@osf/features/moderation/models'; +import { ProviderReviewsWorkflow, ReviewsState } from '@osf/features/preprints/enums'; +import { PreprintRequest } from '@osf/features/preprints/models'; +import { PreprintSelectors } from '@osf/features/preprints/store/preprint'; +import { IconComponent } from '@shared/components'; +import { MOCK_PROVIDER } from '@shared/mocks'; + import { ModerationStatusBannerComponent } from './moderation-status-banner.component'; -describe.skip('ModerationStatusBannerComponent', () => { +import { PREPRINT_MOCK } from '@testing/mocks/preprint.mock'; +import { PREPRINT_REQUEST_MOCK } from '@testing/mocks/preprint-request.mock'; +import { REVIEW_ACTION_MOCK } from '@testing/mocks/review-action.mock'; +import { TranslationServiceMock } from '@testing/mocks/translation.service.mock'; +import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideMockStore } from '@testing/providers/store-provider.mock'; + +describe('ModerationStatusBannerComponent', () => { let component: ModerationStatusBannerComponent; let fixture: ComponentFixture; + const mockPreprint = PREPRINT_MOCK; + const mockProvider = MOCK_PROVIDER; + const mockReviewAction: ReviewAction = REVIEW_ACTION_MOCK; + const mockWithdrawalRequest: PreprintRequest = PREPRINT_REQUEST_MOCK; + const mockWebUrl = 'https://staging4.osf.io'; + beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [ModerationStatusBannerComponent], + imports: [ + ModerationStatusBannerComponent, + OSFTestingModule, + MockComponent(IconComponent), + MockPipes(TitleCasePipe, DatePipe), + ], + providers: [ + TranslationServiceMock, + MockProvider(ENVIRONMENT, { + webUrl: mockWebUrl, + }), + provideMockStore({ + signals: [ + { + selector: PreprintSelectors.getPreprint, + value: mockPreprint, + }, + ], + }), + ], }).compileComponents(); fixture = TestBed.createComponent(ModerationStatusBannerComponent); component = fixture.componentInstance; - fixture.detectChanges(); + + fixture.componentRef.setInput('provider', mockProvider); + fixture.componentRef.setInput('latestAction', mockReviewAction); + fixture.componentRef.setInput('latestWithdrawalRequest', mockWithdrawalRequest); + fixture.componentRef.setInput('isPendingWithdrawal', false); }); it('should create', () => { expect(component).toBeTruthy(); }); + + it('should return preprint from store', () => { + const preprint = component.preprint(); + expect(preprint).toBe(mockPreprint); + }); + + it('should compute noActions when latestAction is null', () => { + fixture.componentRef.setInput('latestAction', null); + const noActions = component.noActions(); + expect(noActions).toBe(true); + }); + + it('should compute noActions when latestAction exists', () => { + const noActions = component.noActions(); + expect(noActions).toBe(false); + }); + + it('should compute documentType from provider', () => { + const documentType = component.documentType(); + expect(documentType).toBeDefined(); + expect(documentType?.singular).toBeDefined(); + }); + + it('should compute labelDate from preprint dateLastTransitioned', () => { + const labelDate = component.labelDate(); + expect(labelDate).toBe(mockPreprint.dateLastTransitioned); + }); + + it('should compute status for pending preprint', () => { + const status = component.status(); + expect(status).toBe('preprints.details.statusBanner.pending'); + }); + + it('should compute status for accepted preprint', () => { + const acceptedPreprint = { ...mockPreprint, reviewsState: ReviewsState.Accepted }; + jest.spyOn(component, 'preprint').mockReturnValue(acceptedPreprint); + const status = component.status(); + expect(status).toBe('preprints.details.statusBanner.accepted'); + }); + + it('should compute status for pending withdrawal', () => { + fixture.componentRef.setInput('isPendingWithdrawal', true); + const status = component.status(); + expect(status).toBe('preprints.details.statusBanner.pending'); + }); + + it('should compute iconClass for pending preprint', () => { + const iconClass = component.iconClass(); + expect(iconClass).toBe('hourglass'); + }); + + it('should compute iconClass for accepted preprint', () => { + const acceptedPreprint = { ...mockPreprint, reviewsState: ReviewsState.Accepted }; + jest.spyOn(component, 'preprint').mockReturnValue(acceptedPreprint); + const iconClass = component.iconClass(); + expect(iconClass).toBe('check-circle'); + }); + + it('should compute iconClass for pending withdrawal', () => { + fixture.componentRef.setInput('isPendingWithdrawal', true); + const iconClass = component.iconClass(); + expect(iconClass).toBe('hourglass'); + }); + + it('should compute severity for pending preprint with post-moderation', () => { + const postModerationProvider = { ...mockProvider, reviewsWorkflow: ProviderReviewsWorkflow.PostModeration }; + fixture.componentRef.setInput('provider', postModerationProvider); + const severity = component.severity(); + expect(severity).toBe('secondary'); + }); + + it('should compute severity for accepted preprint', () => { + const acceptedPreprint = { ...mockPreprint, reviewsState: ReviewsState.Accepted }; + jest.spyOn(component, 'preprint').mockReturnValue(acceptedPreprint); + const severity = component.severity(); + expect(severity).toBe('success'); + }); + + it('should compute severity for pending withdrawal', () => { + fixture.componentRef.setInput('isPendingWithdrawal', true); + const severity = component.severity(); + expect(severity).toBe('warn'); + }); + + it('should compute recentActivityLanguage for no actions', () => { + fixture.componentRef.setInput('latestAction', null); + const language = component.recentActivityLanguage(); + expect(language).toBe('preprints.details.moderationStatusBanner.recentActivity.automatic.pending'); + }); + + it('should compute recentActivityLanguage with actions', () => { + const language = component.recentActivityLanguage(); + expect(language).toBe('preprints.details.moderationStatusBanner.recentActivity.pending'); + }); + + it('should compute requestActivityLanguage for pending withdrawal', () => { + fixture.componentRef.setInput('isPendingWithdrawal', true); + const language = component.requestActivityLanguage(); + expect(language).toBe('preprints.details.moderationStatusBanner.recentActivity.pendingWithdrawal'); + }); + + it('should not compute requestActivityLanguage when not pending withdrawal', () => { + const language = component.requestActivityLanguage(); + expect(language).toBeUndefined(); + }); + + it('should compute actionCreatorName from latestAction', () => { + const name = component.actionCreatorName(); + expect(name).toBe('Test User'); + }); + + it('should compute actionCreatorId from latestAction', () => { + const id = component.actionCreatorId(); + expect(id).toBe('user-1'); + }); + + it('should compute actionCreatorLink with environment webUrl', () => { + const link = component.actionCreatorLink(); + expect(link).toBe(`${mockWebUrl}/user-1`); + }); + + it('should compute withdrawalRequesterName from latestWithdrawalRequest', () => { + const name = component.withdrawalRequesterName(); + expect(name).toBe('John Doe'); + }); + + it('should compute withdrawalRequesterId from latestWithdrawalRequest', () => { + const id = component.withdrawalRequesterId(); + expect(id).toBe('user-123'); + }); }); diff --git a/src/app/features/preprints/components/preprint-details/preprint-doi-section/preprint-doi-section.component.spec.ts b/src/app/features/preprints/components/preprint-details/preprint-doi-section/preprint-doi-section.component.spec.ts index a9751d431..67b5d5463 100644 --- a/src/app/features/preprints/components/preprint-details/preprint-doi-section/preprint-doi-section.component.spec.ts +++ b/src/app/features/preprints/components/preprint-details/preprint-doi-section/preprint-doi-section.component.spec.ts @@ -1,22 +1,88 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { Preprint, PreprintProviderDetails } from '@osf/features/preprints/models'; +import { PreprintSelectors } from '@osf/features/preprints/store/preprint'; + import { PreprintDoiSectionComponent } from './preprint-doi-section.component'; -describe.skip('PreprintDoiSectionComponent', () => { +import { PREPRINT_MOCK } from '@testing/mocks/preprint.mock'; +import { PREPRINT_PROVIDER_DETAILS_MOCK } from '@testing/mocks/preprint-provider-details'; +import { TranslationServiceMock } from '@testing/mocks/translation.service.mock'; +import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideMockStore } from '@testing/providers/store-provider.mock'; + +describe('PreprintDoiSectionComponent', () => { let component: PreprintDoiSectionComponent; let fixture: ComponentFixture; + const mockPreprint: Preprint = PREPRINT_MOCK; + + const mockProvider: PreprintProviderDetails = PREPRINT_PROVIDER_DETAILS_MOCK; + const mockVersionIds = ['version-1', 'version-2', 'version-3']; + beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [PreprintDoiSectionComponent], + imports: [PreprintDoiSectionComponent, OSFTestingModule], + providers: [ + TranslationServiceMock, + provideMockStore({ + signals: [ + { + selector: PreprintSelectors.getPreprint, + value: mockPreprint, + }, + { + selector: PreprintSelectors.getPreprintVersionIds, + value: mockVersionIds, + }, + { + selector: PreprintSelectors.arePreprintVersionIdsLoading, + value: false, + }, + ], + }), + ], }).compileComponents(); fixture = TestBed.createComponent(PreprintDoiSectionComponent); component = fixture.componentInstance; - fixture.detectChanges(); + + fixture.componentRef.setInput('preprintProvider', mockProvider); }); it('should create', () => { expect(component).toBeTruthy(); }); + + it('should compute versions dropdown options from version IDs', () => { + const options = component.versionsDropdownOptions(); + expect(options).toEqual([ + { label: 'Version 3', value: 'version-1' }, + { label: 'Version 2', value: 'version-2' }, + { label: 'Version 1', value: 'version-3' }, + ]); + }); + + it('should return empty array when no version IDs', () => { + jest.spyOn(component, 'preprintVersionIds').mockReturnValue([]); + const options = component.versionsDropdownOptions(); + expect(options).toEqual([]); + }); + + it('should emit preprintVersionSelected when selecting different version', () => { + const emitSpy = jest.spyOn(component.preprintVersionSelected, 'emit'); + component.selectPreprintVersion('version-2'); + expect(emitSpy).toHaveBeenCalledWith('version-2'); + }); + + it('should not emit when selecting current preprint version', () => { + const emitSpy = jest.spyOn(component.preprintVersionSelected, 'emit'); + component.selectPreprintVersion('preprint-1'); + expect(emitSpy).not.toHaveBeenCalled(); + }); + + it('should handle preprint provider input', () => { + const provider = component.preprintProvider(); + expect(provider).toBe(mockProvider); + }); }); diff --git a/src/app/features/preprints/components/preprint-details/preprint-file-section/preprint-file-section.component.spec.ts b/src/app/features/preprints/components/preprint-details/preprint-file-section/preprint-file-section.component.spec.ts index b683b038a..1259d4ea0 100644 --- a/src/app/features/preprints/components/preprint-details/preprint-file-section/preprint-file-section.component.spec.ts +++ b/src/app/features/preprints/components/preprint-details/preprint-file-section/preprint-file-section.component.spec.ts @@ -1,86 +1,191 @@ -import { Store } from '@ngxs/store'; - -import { TranslateModule } from '@ngx-translate/core'; -import { MockProvider } from 'ng-mocks'; +import { MockComponent, MockProvider } from 'ng-mocks'; import { BehaviorSubject, of } from 'rxjs'; -import { signal } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ProviderReviewsWorkflow } from '@osf/features/preprints/enums'; import { PreprintSelectors } from '@osf/features/preprints/store/preprint'; import { IS_LARGE, IS_MEDIUM } from '@osf/shared/helpers'; -import { MOCK_STORE } from '@shared/mocks'; +import { LoadingSpinnerComponent } from '@shared/components'; import { DataciteService } from '@shared/services/datacite/datacite.service'; import { PreprintFileSectionComponent } from './preprint-file-section.component'; +import { PREPRINT_MOCK } from '@testing/mocks/preprint.mock'; +import { TranslationServiceMock } from '@testing/mocks/translation.service.mock'; +import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideMockStore } from '@testing/providers/store-provider.mock'; + describe('PreprintFileSectionComponent', () => { let component: PreprintFileSectionComponent; let fixture: ComponentFixture; let dataciteService: jest.Mocked; - const mockStore = MOCK_STORE; let isMediumSubject: BehaviorSubject; let isLargeSubject: BehaviorSubject; - // const - beforeEach(async () => { - (MOCK_STORE.selectSignal as jest.Mock).mockImplementation((selector) => { - if ( - selector === PreprintSelectors.isPreprintFileLoading || - // selector === PreprintSelectors.getPreprintFileVersions || - selector === PreprintSelectors.arePreprintFileVersionsLoading - ) { - return () => []; - } else if (selector == PreprintSelectors.getPreprint) { - return () => ({ - id: 1, - }); - } else if (selector == PreprintSelectors.getPreprintFileVersions) { - return signal([{ date: '12312312', downloadUrl: '21312', id: '1' }]); - } - return () => null; - }); + const mockPreprint = PREPRINT_MOCK; + const mockFile = { + id: 'file-1', + name: 'test-file.pdf', + links: { + render: 'https://example.com/render', + }, + }; + const mockFileVersions = [ + { + id: '1', + dateCreated: '2024-01-15T10:00:00Z', + downloadLink: 'https://example.com/download/1', + }, + { + id: '2', + dateCreated: '2024-01-16T10:00:00Z', + downloadLink: 'https://example.com/download/2', + }, + ]; + beforeEach(async () => { isMediumSubject = new BehaviorSubject(false); isLargeSubject = new BehaviorSubject(true); await TestBed.configureTestingModule({ - imports: [PreprintFileSectionComponent, TranslateModule.forRoot()], + imports: [PreprintFileSectionComponent, OSFTestingModule, MockComponent(LoadingSpinnerComponent)], providers: [ - MockProvider(Store, mockStore), - MockProvider(DataciteService, { - logIdentifiableDownload: jest.fn().mockReturnValue(of(void 0)), - }), + TranslationServiceMock, + { + provide: DataciteService, + useValue: { + logIdentifiableDownload: jest.fn().mockReturnValue(of(void 0)), + }, + }, MockProvider(IS_MEDIUM, isMediumSubject), MockProvider(IS_LARGE, isLargeSubject), + provideMockStore({ + signals: [ + { + selector: PreprintSelectors.getPreprint, + value: mockPreprint, + }, + { + selector: PreprintSelectors.getPreprintFile, + value: mockFile, + }, + { + selector: PreprintSelectors.isPreprintFileLoading, + value: false, + }, + { + selector: PreprintSelectors.getPreprintFileVersions, + value: mockFileVersions, + }, + { + selector: PreprintSelectors.arePreprintFileVersionsLoading, + value: false, + }, + ], + }), ], }).compileComponents(); fixture = TestBed.createComponent(PreprintFileSectionComponent); component = fixture.componentInstance; - fixture.detectChanges(); + + fixture.componentRef.setInput('providerReviewsWorkflow', ProviderReviewsWorkflow.PreModeration); + dataciteService = TestBed.inject(DataciteService) as jest.MockedObject; }); + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should return preprint from store', () => { + const preprint = component.preprint(); + expect(preprint).toBe(mockPreprint); + }); + + it('should return file from store', () => { + const file = component.file(); + expect(file).toBe(mockFile); + }); + + it('should return file loading state from store', () => { + const loading = component.isFileLoading(); + expect(loading).toBe(false); + }); + + it('should return file versions from store', () => { + const versions = component.fileVersions(); + expect(versions).toBe(mockFileVersions); + }); + + it('should return file versions loading state from store', () => { + const loading = component.areFileVersionsLoading(); + expect(loading).toBe(false); + }); + + it('should compute safe link from file render link', () => { + const safeLink = component.safeLink(); + expect(safeLink).toBeDefined(); + }); + + it('should compute version menu items from file versions', () => { + const menuItems = component.versionMenuItems(); + expect(menuItems).toHaveLength(2); + expect(menuItems[0]).toHaveProperty('label'); + expect(menuItems[0]).toHaveProperty('url'); + expect(menuItems[0]).toHaveProperty('command'); + }); + + it('should return empty array when no file versions', () => { + jest.spyOn(component, 'fileVersions').mockReturnValue([]); + const menuItems = component.versionMenuItems(); + expect(menuItems).toEqual([]); + }); + + it('should compute date label for pre-moderation workflow', () => { + const label = component.dateLabel(); + expect(label).toBe('preprints.details.file.submitted'); + }); + + it('should compute date label for post-moderation workflow', () => { + fixture.componentRef.setInput('providerReviewsWorkflow', ProviderReviewsWorkflow.PostModeration); + const label = component.dateLabel(); + expect(label).toBe('preprints.details.file.created'); + }); + + it('should return empty string when no reviews workflow', () => { + fixture.componentRef.setInput('providerReviewsWorkflow', null); + const label = component.dateLabel(); + expect(label).toBe(''); + }); + it('should call dataciteService.logIdentifiableDownload when logDownload is called', () => { component.logDownload(); expect(dataciteService.logIdentifiableDownload).toHaveBeenCalledWith(component.preprint$); }); - it('should call logDownload when version menu item is clicked', () => { - // Get the command from versionMenuItems - fixture.detectChanges(); + it('should call logDownload when version menu item command is executed', () => { const menuItems = component.versionMenuItems(); expect(menuItems.length).toBeGreaterThan(0); const versionCommand = menuItems[0].command!; jest.spyOn(component, 'logDownload'); - // simulate clicking the menu item versionCommand(); expect(component.logDownload).toHaveBeenCalled(); expect(dataciteService.logIdentifiableDownload).toHaveBeenCalledWith(expect.anything()); }); + + it('should handle isMedium signal', () => { + const isMedium = component.isMedium(); + expect(typeof isMedium).toBe('boolean'); + }); + + it('should handle isLarge signal', () => { + const isLarge = component.isLarge(); + expect(typeof isLarge).toBe('boolean'); + }); }); diff --git a/src/app/features/preprints/components/preprint-details/preprint-tombstone/preprint-tombstone.component.spec.ts b/src/app/features/preprints/components/preprint-details/preprint-tombstone/preprint-tombstone.component.spec.ts index 184b82368..3ef8fe1ce 100644 --- a/src/app/features/preprints/components/preprint-details/preprint-tombstone/preprint-tombstone.component.spec.ts +++ b/src/app/features/preprints/components/preprint-details/preprint-tombstone/preprint-tombstone.component.spec.ts @@ -1,22 +1,121 @@ +import { MockComponents, MockPipes } from 'ng-mocks'; + import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { PreprintDoiSectionComponent } from '@osf/features/preprints/components/preprint-details/preprint-doi-section/preprint-doi-section.component'; +import { PreprintProviderDetails } from '@osf/features/preprints/models'; +import { PreprintSelectors } from '@osf/features/preprints/store/preprint'; +import { ContributorsSelectors, SubjectsSelectors } from '@osf/shared/stores'; +import { TruncatedTextComponent } from '@shared/components'; +import { MOCK_CONTRIBUTOR } from '@shared/mocks'; +import { InterpolatePipe } from '@shared/pipes'; + import { PreprintTombstoneComponent } from './preprint-tombstone.component'; -describe.skip('PreprintTombstoneComponent', () => { +import { PREPRINT_MOCK } from '@testing/mocks/preprint.mock'; +import { PREPRINT_PROVIDER_DETAILS_MOCK } from '@testing/mocks/preprint-provider-details'; +import { SUBJECTS_MOCK } from '@testing/mocks/subject.mock'; +import { TranslationServiceMock } from '@testing/mocks/translation.service.mock'; +import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideMockStore } from '@testing/providers/store-provider.mock'; + +describe('PreprintTombstoneComponent', () => { let component: PreprintTombstoneComponent; let fixture: ComponentFixture; + const mockPreprint = PREPRINT_MOCK; + const mockProvider: PreprintProviderDetails = PREPRINT_PROVIDER_DETAILS_MOCK; + const mockContributors = [MOCK_CONTRIBUTOR]; + const mockSubjects = SUBJECTS_MOCK; + beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [PreprintTombstoneComponent], + imports: [ + PreprintTombstoneComponent, + OSFTestingModule, + ...MockComponents(PreprintDoiSectionComponent, TruncatedTextComponent), + MockPipes(InterpolatePipe), + ], + providers: [ + TranslationServiceMock, + provideMockStore({ + signals: [ + { + selector: PreprintSelectors.getPreprint, + value: mockPreprint, + }, + { + selector: PreprintSelectors.isPreprintLoading, + value: false, + }, + { + selector: ContributorsSelectors.getContributors, + value: mockContributors, + }, + { + selector: ContributorsSelectors.isContributorsLoading, + value: false, + }, + { + selector: SubjectsSelectors.getSelectedSubjects, + value: mockSubjects, + }, + { + selector: SubjectsSelectors.areSelectedSubjectsLoading, + value: false, + }, + ], + }), + ], }).compileComponents(); fixture = TestBed.createComponent(PreprintTombstoneComponent); component = fixture.componentInstance; - fixture.detectChanges(); + + fixture.componentRef.setInput('preprintProvider', mockProvider); }); it('should create', () => { expect(component).toBeTruthy(); }); + + it('should compute bibliographic contributors', () => { + const bibliographicContributors = component.bibliographicContributors(); + expect(bibliographicContributors).toHaveLength(1); + expect(bibliographicContributors[0].isBibliographic).toBe(true); + }); + + it('should compute license from preprint', () => { + const license = component.license(); + expect(license).toBe(mockPreprint.embeddedLicense); + }); + + it('should return null license when no preprint', () => { + jest.spyOn(component, 'preprint').mockReturnValue(null); + const license = component.license(); + expect(license).toBeNull(); + }); + + it('should compute license options record from preprint', () => { + const licenseOptionsRecord = component.licenseOptionsRecord(); + expect(licenseOptionsRecord).toEqual(mockPreprint.licenseOptions); + }); + + it('should return empty object when no license options', () => { + const preprintWithoutOptions = { ...mockPreprint, licenseOptions: null }; + jest.spyOn(component, 'preprint').mockReturnValue(preprintWithoutOptions); + const licenseOptionsRecord = component.licenseOptionsRecord(); + expect(licenseOptionsRecord).toEqual({}); + }); + + it('should handle preprint provider input', () => { + const provider = component.preprintProvider(); + expect(provider).toBe(mockProvider); + }); + + it('should emit preprintVersionSelected when version is selected', () => { + const emitSpy = jest.spyOn(component.preprintVersionSelected, 'emit'); + component.preprintVersionSelected.emit('version-1'); + expect(emitSpy).toHaveBeenCalledWith('version-1'); + }); }); diff --git a/src/app/features/preprints/components/preprint-details/share-and-downlaod/share-and-download.component.spec.ts b/src/app/features/preprints/components/preprint-details/share-and-downlaod/share-and-download.component.spec.ts index 12471a8b6..04311ebc6 100644 --- a/src/app/features/preprints/components/preprint-details/share-and-downlaod/share-and-download.component.spec.ts +++ b/src/app/features/preprints/components/preprint-details/share-and-downlaod/share-and-download.component.spec.ts @@ -1,45 +1,163 @@ -import { Store } from '@ngxs/store'; +import { MockComponent, MockProvider } from 'ng-mocks'; -import { MockProvider } from 'ng-mocks'; +import { of } from 'rxjs'; import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { PreprintProviderDetails } from '@osf/features/preprints/models'; import { PreprintSelectors } from '@osf/features/preprints/store/preprint'; -import { MOCK_STORE } from '@shared/mocks'; +import { IconComponent } from '@shared/components'; +import { SocialShareLinks } from '@shared/models'; +import { SocialShareService } from '@shared/services'; import { DataciteService } from '@shared/services/datacite/datacite.service'; import { ShareAndDownloadComponent } from './share-and-download.component'; -import { DataciteMockFactory } from '@testing/mocks/datacite.service.mock'; +import { PREPRINT_MOCK } from '@testing/mocks/preprint.mock'; import { PREPRINT_PROVIDER_DETAILS_MOCK } from '@testing/mocks/preprint-provider-details'; +import { SOCIAL_SHARE_LINKS_MOCK } from '@testing/mocks/social-share-links.mock'; +import { TranslationServiceMock } from '@testing/mocks/translation.service.mock'; import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideMockStore } from '@testing/providers/store-provider.mock'; describe('ShareAndDownloadComponent', () => { let component: ShareAndDownloadComponent; let fixture: ComponentFixture; let dataciteService: jest.Mocked; + let socialShareService: jest.Mocked; + + const mockPreprint = PREPRINT_MOCK; + const mockProvider: PreprintProviderDetails = PREPRINT_PROVIDER_DETAILS_MOCK; + const mockShareLinks: SocialShareLinks = SOCIAL_SHARE_LINKS_MOCK; beforeEach(async () => { - dataciteService = DataciteMockFactory(); - (MOCK_STORE.selectSignal as jest.Mock).mockImplementation((selector) => { - if (selector === PreprintSelectors.getPreprint) return () => null; - if (selector === PreprintSelectors.isPreprintLoading) return () => false; - return () => null; - }); + dataciteService = { + logIdentifiableDownload: jest.fn().mockReturnValue(of(void 0)), + } as any; + + socialShareService = { + createDownloadUrl: jest.fn().mockReturnValue('https://example.com/download/preprint-1'), + createPreprintUrl: jest.fn().mockReturnValue('https://example.com/preprint/preprint-1'), + generateAllSharingLinks: jest.fn().mockReturnValue(mockShareLinks), + } as any; await TestBed.configureTestingModule({ - imports: [ShareAndDownloadComponent, OSFTestingModule], - providers: [MockProvider(Store, MOCK_STORE), { provide: DataciteService, useValue: dataciteService }], + imports: [ShareAndDownloadComponent, OSFTestingModule, MockComponent(IconComponent)], + providers: [ + TranslationServiceMock, + MockProvider(DataciteService, dataciteService), + MockProvider(SocialShareService, socialShareService), + provideMockStore({ + signals: [ + { + selector: PreprintSelectors.getPreprint, + value: mockPreprint, + }, + { + selector: PreprintSelectors.isPreprintLoading, + value: false, + }, + ], + }), + ], }).compileComponents(); fixture = TestBed.createComponent(ShareAndDownloadComponent); component = fixture.componentInstance; - fixture.componentRef.setInput('preprintProvider', PREPRINT_PROVIDER_DETAILS_MOCK); - fixture.detectChanges(); + + fixture.componentRef.setInput('preprintProvider', mockProvider); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should return preprint from store', () => { + const preprint = component.preprint(); + expect(preprint).toBe(mockPreprint); + }); + + it('should return preprint loading state from store', () => { + const loading = component.isPreprintLoading(); + expect(loading).toBe(false); }); - it('should call dataciteService.logIdentifiableDownload when logDownload is triggered', () => { + it('should compute metrics from preprint', () => { + const metrics = component.metrics(); + expect(metrics).toBe(mockPreprint.metrics); + }); + + it('should return null metrics when no preprint', () => { + jest.spyOn(component, 'preprint').mockReturnValue(null); + const metrics = component.metrics(); + expect(metrics).toBeNull(); + }); + + it('should compute download link from preprint', () => { + const downloadLink = component.downloadLink(); + expect(downloadLink).toBe('https://example.com/download/preprint-1'); + expect(socialShareService.createDownloadUrl).toHaveBeenCalledWith('preprint-1'); + }); + + it('should return default download link when no preprint', () => { + jest.spyOn(component, 'preprint').mockReturnValue(null); + const downloadLink = component.downloadLink(); + expect(downloadLink).toBe('#'); + }); + + it('should return null shareable content when no preprint or provider', () => { + jest.spyOn(component, 'preprint').mockReturnValue(null); + const shareableContent = (component as any).shareableContent(); + expect(shareableContent).toBeNull(); + }); + + it('should compute share links from shareable content', () => { + const shareLinks = component.shareLinks(); + expect(shareLinks).toBe(mockShareLinks); + expect(socialShareService.generateAllSharingLinks).toHaveBeenCalled(); + }); + + it('should return null share links when no shareable content', () => { + jest.spyOn(component as any, 'shareableContent').mockReturnValue(null); + const shareLinks = component.shareLinks(); + expect(shareLinks).toBeNull(); + }); + + it('should compute email share link', () => { + const emailLink = component.emailShareLink(); + expect(emailLink).toBe(mockShareLinks.email); + }); + + it('should compute twitter share link', () => { + const twitterLink = component.twitterShareLink(); + expect(twitterLink).toBe(mockShareLinks.twitter); + }); + + it('should compute facebook share link', () => { + const facebookLink = component.facebookShareLink(); + expect(facebookLink).toBe(mockShareLinks.facebook); + }); + + it('should compute linkedIn share link', () => { + const linkedInLink = component.linkedInShareLink(); + expect(linkedInLink).toBe(mockShareLinks.linkedIn); + }); + + it('should return empty string for share links when no share links', () => { + jest.spyOn(component, 'shareLinks').mockReturnValue(null); + expect(component.emailShareLink()).toBe(''); + expect(component.twitterShareLink()).toBe(''); + expect(component.facebookShareLink()).toBe(''); + expect(component.linkedInShareLink()).toBe(''); + }); + + it('should call dataciteService.logIdentifiableDownload when logDownload is called', () => { component.logDownload(); expect(dataciteService.logIdentifiableDownload).toHaveBeenCalledWith(component.preprint$); }); + + it('should handle preprint provider input', () => { + const provider = component.preprintProvider(); + expect(provider).toBe(mockProvider); + }); }); diff --git a/src/app/features/preprints/components/preprint-details/status-banner/status-banner.component.spec.ts b/src/app/features/preprints/components/preprint-details/status-banner/status-banner.component.spec.ts index fd0305b6d..ba1771f3c 100644 --- a/src/app/features/preprints/components/preprint-details/status-banner/status-banner.component.spec.ts +++ b/src/app/features/preprints/components/preprint-details/status-banner/status-banner.component.spec.ts @@ -1,22 +1,94 @@ +import { MockComponent, MockPipe } from 'ng-mocks'; + +import { TitleCasePipe } from '@angular/common'; import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ReviewAction } from '@osf/features/moderation/models'; +import { Preprint, PreprintProviderDetails } from '@osf/features/preprints/models'; +import { PreprintSelectors } from '@osf/features/preprints/store/preprint'; +import { IconComponent } from '@shared/components'; + import { StatusBannerComponent } from './status-banner.component'; -describe.skip('StatusBarComponent', () => { +import { PREPRINT_MOCK } from '@testing/mocks/preprint.mock'; +import { PREPRINT_PROVIDER_DETAILS_MOCK } from '@testing/mocks/preprint-provider-details'; +import { REVIEW_ACTION_MOCK } from '@testing/mocks/review-action.mock'; +import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideMockStore } from '@testing/providers/store-provider.mock'; + +describe('StatusBannerComponent', () => { let component: StatusBannerComponent; let fixture: ComponentFixture; + const mockPreprint: Preprint = PREPRINT_MOCK; + const mockProvider: PreprintProviderDetails = PREPRINT_PROVIDER_DETAILS_MOCK; + const mockReviewAction: ReviewAction = REVIEW_ACTION_MOCK; + const mockRequestAction = REVIEW_ACTION_MOCK; + beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [StatusBannerComponent], + imports: [StatusBannerComponent, OSFTestingModule, MockComponent(IconComponent), MockPipe(TitleCasePipe)], + providers: [ + provideMockStore({ + signals: [ + { + selector: PreprintSelectors.getPreprint, + value: mockPreprint, + }, + ], + }), + ], }).compileComponents(); fixture = TestBed.createComponent(StatusBannerComponent); component = fixture.componentInstance; - fixture.detectChanges(); + + fixture.componentRef.setInput('provider', mockProvider); + fixture.componentRef.setInput('latestAction', mockReviewAction); + fixture.componentRef.setInput('isPendingWithdrawal', false); + fixture.componentRef.setInput('isWithdrawalRejected', false); + fixture.componentRef.setInput('latestRequestAction', mockRequestAction); }); it('should create', () => { expect(component).toBeTruthy(); }); + + it('should compute severity for pending preprint', () => { + const severity = component.severity(); + expect(severity).toBe('warn'); + }); + + it('should compute severity for pending withdrawal', () => { + fixture.componentRef.setInput('isPendingWithdrawal', true); + const severity = component.severity(); + expect(severity).toBe('error'); + }); + + it('should compute status for pending preprint', () => { + const status = component.status(); + expect(status).toBe('preprints.details.statusBanner.pending'); + }); + + it('should compute status for pending withdrawal', () => { + fixture.componentRef.setInput('isPendingWithdrawal', true); + const status = component.status(); + expect(status).toBe('preprints.details.statusBanner.pendingWithdrawal'); + }); + + it('should compute reviewer name from latest action', () => { + const name = component.reviewerName(); + expect(name).toBe('Test User'); + }); + + it('should compute reviewer comment from latest action', () => { + const comment = component.reviewerComment(); + expect(comment).toBe('Initial comment'); + }); + + it('should show feedback dialog', () => { + expect(component.feedbackDialogVisible).toBe(false); + component.showFeedbackDialog(); + expect(component.feedbackDialogVisible).toBe(true); + }); }); diff --git a/src/app/features/preprints/components/preprint-details/withdraw-dialog/withdraw-dialog.component.spec.ts b/src/app/features/preprints/components/preprint-details/withdraw-dialog/withdraw-dialog.component.spec.ts index 93260e0b7..57d0e0094 100644 --- a/src/app/features/preprints/components/preprint-details/withdraw-dialog/withdraw-dialog.component.spec.ts +++ b/src/app/features/preprints/components/preprint-details/withdraw-dialog/withdraw-dialog.component.spec.ts @@ -1,14 +1,47 @@ +import { MockPipe, MockProvider } from 'ng-mocks'; + +import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog'; + +import { TitleCasePipe } from '@angular/common'; import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { formInputLimits } from '@osf/features/preprints/constants'; +import { ProviderReviewsWorkflow, ReviewsState } from '@osf/features/preprints/enums'; +import { Preprint, PreprintProviderDetails } from '@osf/features/preprints/models'; + import { WithdrawDialogComponent } from './withdraw-dialog.component'; -describe.skip('WithdrawDialogComponent', () => { +import { PREPRINT_MOCK } from '@testing/mocks/preprint.mock'; +import { PREPRINT_PROVIDER_DETAILS_MOCK } from '@testing/mocks/preprint-provider-details'; +import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideMockStore } from '@testing/providers/store-provider.mock'; + +describe('WithdrawDialogComponent', () => { let component: WithdrawDialogComponent; let fixture: ComponentFixture; + let dialogRefMock: any; + let dialogConfigMock: any; + + const mockProvider: PreprintProviderDetails = PREPRINT_PROVIDER_DETAILS_MOCK; + const mockPreprint: Preprint = PREPRINT_MOCK; beforeEach(async () => { + dialogRefMock = { + close: jest.fn(), + }; + dialogConfigMock = { + data: { provider: mockProvider, preprint: mockPreprint }, + }; + await TestBed.configureTestingModule({ - imports: [WithdrawDialogComponent], + imports: [WithdrawDialogComponent, OSFTestingModule, MockPipe(TitleCasePipe)], + providers: [ + MockProvider(DynamicDialogRef, dialogRefMock), + MockProvider(DynamicDialogConfig, dialogConfigMock), + provideMockStore({ + signals: [], + }), + ], }).compileComponents(); fixture = TestBed.createComponent(WithdrawDialogComponent); @@ -19,4 +52,119 @@ describe.skip('WithdrawDialogComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + it('should set modal explanation on init', () => { + expect(component.modalExplanation()).toBeDefined(); + expect(typeof component.modalExplanation()).toBe('string'); + }); + + it('should handle form validation correctly', () => { + const formControl = component.withdrawalJustificationFormControl; + + formControl.setValue(''); + expect(formControl.invalid).toBe(true); + + const minLength = formInputLimits.withdrawalJustification.minLength; + formControl.setValue('a'.repeat(minLength)); + expect(formControl.valid).toBe(true); + }); + + it('should handle withdraw with valid form', () => { + const validJustification = 'Valid withdrawal justification'; + component.withdrawalJustificationFormControl.setValue(validJustification); + + expect(() => component.withdraw()).not.toThrow(); + }); + + it('should not proceed with withdraw if form is invalid', () => { + component.withdrawalJustificationFormControl.setValue(''); + + expect(() => component.withdraw()).not.toThrow(); + }); + + it('should handle withdraw request completion', () => { + const validJustification = 'Valid withdrawal justification'; + component.withdrawalJustificationFormControl.setValue(validJustification); + + expect(() => component.withdraw()).not.toThrow(); + }); + + it('should handle withdraw request error', () => { + const validJustification = 'Valid withdrawal justification'; + component.withdrawalJustificationFormControl.setValue(validJustification); + + expect(() => component.withdraw()).not.toThrow(); + }); + + it('should calculate modal explanation for pre-moderation pending', () => { + const providerWithPreMod = { ...mockProvider, reviewsWorkflow: ProviderReviewsWorkflow.PreModeration }; + const preprintWithPending = { ...mockPreprint, reviewsState: ReviewsState.Pending }; + + dialogConfigMock.data = { provider: providerWithPreMod, preprint: preprintWithPending }; + + expect(() => { + fixture = TestBed.createComponent(WithdrawDialogComponent); + component = fixture.componentInstance; + component.ngOnInit(); + }).not.toThrow(); + }); + + it('should calculate modal explanation for pre-moderation accepted', () => { + const providerWithPreMod = { ...mockProvider, reviewsWorkflow: ProviderReviewsWorkflow.PreModeration }; + const preprintWithAccepted = { ...mockPreprint, reviewsState: ReviewsState.Accepted }; + + dialogConfigMock.data = { provider: providerWithPreMod, preprint: preprintWithAccepted }; + + expect(() => { + fixture = TestBed.createComponent(WithdrawDialogComponent); + component = fixture.componentInstance; + component.ngOnInit(); + }).not.toThrow(); + }); + + it('should calculate modal explanation for post-moderation', () => { + const providerWithPostMod = { ...mockProvider, reviewsWorkflow: ProviderReviewsWorkflow.PostModeration }; + + dialogConfigMock.data = { provider: providerWithPostMod, preprint: mockPreprint }; + + expect(() => { + fixture = TestBed.createComponent(WithdrawDialogComponent); + component = fixture.componentInstance; + component.ngOnInit(); + }).not.toThrow(); + }); + + it('should handle form control state changes', () => { + const formControl = component.withdrawalJustificationFormControl; + formControl.markAsTouched(); + expect(formControl.touched).toBe(true); + + formControl.setValue('test'); + formControl.markAsDirty(); + expect(formControl.dirty).toBe(true); + }); + + it('should handle minimum length validation', () => { + const formControl = component.withdrawalJustificationFormControl; + const minLength = formInputLimits.withdrawalJustification.minLength; + + formControl.setValue('a'.repeat(minLength - 1)); + expect(formControl.hasError('minlength')).toBe(true); + + formControl.setValue('a'.repeat(minLength)); + expect(formControl.hasError('minlength')).toBe(false); + }); + + it('should handle required validation', () => { + const formControl = component.withdrawalJustificationFormControl; + + formControl.setValue(''); + expect(formControl.hasError('required')).toBe(true); + + formControl.setValue(' '); + expect(formControl.hasError('required')).toBe(true); + + formControl.setValue('Valid text'); + expect(formControl.hasError('required')).toBe(false); + }); }); diff --git a/src/app/features/preprints/components/preprint-provider-footer/preprint-provider-footer.component.spec.ts b/src/app/features/preprints/components/preprint-provider-footer/preprint-provider-footer.component.spec.ts index ee399ea57..b2c0e251c 100644 --- a/src/app/features/preprints/components/preprint-provider-footer/preprint-provider-footer.component.spec.ts +++ b/src/app/features/preprints/components/preprint-provider-footer/preprint-provider-footer.component.spec.ts @@ -13,13 +13,42 @@ describe('PreprintProviderFooterComponent', () => { fixture = TestBed.createComponent(PreprintProviderFooterComponent); component = fixture.componentInstance; + }); + it('should create', () => { fixture.componentRef.setInput('footerHtml', ''); + fixture.detectChanges(); + + expect(component).toBeTruthy(); + }); + it('should not render section when footerHtml is null', () => { + fixture.componentRef.setInput('footerHtml', null); fixture.detectChanges(); + + const compiled = fixture.nativeElement; + const section = compiled.querySelector('section'); + + expect(section).toBeNull(); }); - it('should create', () => { - expect(component).toBeTruthy(); + it('should not render section when footerHtml is undefined', () => { + fixture.componentRef.setInput('footerHtml', undefined); + fixture.detectChanges(); + + const compiled = fixture.nativeElement; + const section = compiled.querySelector('section'); + + expect(section).toBeNull(); + }); + + it('should not render section when footerHtml is empty string', () => { + fixture.componentRef.setInput('footerHtml', ''); + fixture.detectChanges(); + + const compiled = fixture.nativeElement; + const section = compiled.querySelector('section'); + + expect(section).toBeNull(); }); }); diff --git a/src/app/features/preprints/components/preprint-provider-hero/preprint-provider-hero.component.spec.ts b/src/app/features/preprints/components/preprint-provider-hero/preprint-provider-hero.component.spec.ts index 3cde88583..6debac41d 100644 --- a/src/app/features/preprints/components/preprint-provider-hero/preprint-provider-hero.component.spec.ts +++ b/src/app/features/preprints/components/preprint-provider-hero/preprint-provider-hero.component.spec.ts @@ -1,5 +1,4 @@ -import { TranslatePipe, TranslateService } from '@ngx-translate/core'; -import { MockPipe, MockProvider, MockProviders } from 'ng-mocks'; +import { MockProvider } from 'ng-mocks'; import { DialogService } from 'primeng/dynamicdialog'; @@ -7,31 +6,170 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { FormControl } from '@angular/forms'; import { ActivatedRoute } from '@angular/router'; -import { MOCK_PROVIDER } from '@shared/mocks'; +import { PreprintProviderDetails } from '@osf/features/preprints/models'; import { PreprintProviderHeroComponent } from './preprint-provider-hero.component'; +import { PREPRINT_PROVIDER_DETAILS_MOCK } from '@testing/mocks/preprint-provider-details'; +import { TranslationServiceMock } from '@testing/mocks/translation.service.mock'; +import { OSFTestingModule } from '@testing/osf.testing.module'; +import { DialogServiceMockBuilder } from '@testing/providers/dialog-provider.mock'; +import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock'; + describe('PreprintProviderHeroComponent', () => { let component: PreprintProviderHeroComponent; let fixture: ComponentFixture; + let mockDialogService: ReturnType; + + const mockPreprintProvider: PreprintProviderDetails = PREPRINT_PROVIDER_DETAILS_MOCK; beforeEach(async () => { + mockDialogService = DialogServiceMockBuilder.create().build(); + await TestBed.configureTestingModule({ - imports: [PreprintProviderHeroComponent, MockPipe(TranslatePipe)], - providers: [MockProviders(DialogService, TranslateService, ActivatedRoute), MockProvider(TranslateService)], - }).compileComponents(); + imports: [PreprintProviderHeroComponent, OSFTestingModule], + providers: [ + MockProvider(DialogService, mockDialogService), + MockProvider(ActivatedRoute, ActivatedRouteMockBuilder.create().build()), + TranslationServiceMock, + ], + }) + .overrideComponent(PreprintProviderHeroComponent, { + set: { + providers: [{ provide: DialogService, useValue: mockDialogService }], + }, + }) + .compileComponents(); fixture = TestBed.createComponent(PreprintProviderHeroComponent); component = fixture.componentInstance; + }); + it('should create', () => { fixture.componentRef.setInput('searchControl', new FormControl('')); - fixture.componentRef.setInput('preprintProvider', MOCK_PROVIDER); + fixture.componentRef.setInput('preprintProvider', mockPreprintProvider); fixture.componentRef.setInput('isPreprintProviderLoading', false); + fixture.detectChanges(); + expect(component).toBeTruthy(); + }); + + it('should display loading skeletons when isPreprintProviderLoading is true', () => { + fixture.componentRef.setInput('isPreprintProviderLoading', true); fixture.detectChanges(); + + const compiled = fixture.nativeElement; + const skeletons = compiled.querySelectorAll('p-skeleton'); + const providerName = compiled.querySelector('.preprint-provider-name'); + const providerLogo = compiled.querySelector('img'); + const addButton = compiled.querySelector('p-button'); + const searchInput = compiled.querySelector('osf-search-input'); + + expect(skeletons.length).toBeGreaterThan(0); + expect(providerName).toBeNull(); + expect(providerLogo).toBeNull(); + expect(addButton).toBeNull(); + expect(searchInput).toBeNull(); }); - it('should create', () => { - expect(component).toBeTruthy(); + it('should display provider information when not loading', () => { + fixture.componentRef.setInput('searchControl', new FormControl('')); + fixture.componentRef.setInput('preprintProvider', mockPreprintProvider); + fixture.componentRef.setInput('isPreprintProviderLoading', false); + fixture.detectChanges(); + + const compiled = fixture.nativeElement; + const providerName = compiled.querySelector('.preprint-provider-name'); + const providerLogo = compiled.querySelector('img'); + const description = compiled.querySelector('.provider-description div'); + + expect(providerName).toBeTruthy(); + expect(providerName?.textContent).toBe('OSF Preprints'); + expect(providerLogo).toBeTruthy(); + expect(providerLogo?.getAttribute('src')).toBe('https://osf.io/assets/hero-logo.png'); + expect(description).toBeTruthy(); + expect(description?.innerHTML).toContain('

Open preprints for all disciplines

'); + }); + + it('should display add preprint button when allowSubmissions is true', () => { + fixture.componentRef.setInput('searchControl', new FormControl('')); + fixture.componentRef.setInput('preprintProvider', mockPreprintProvider); + fixture.componentRef.setInput('isPreprintProviderLoading', false); + fixture.detectChanges(); + + const compiled = fixture.nativeElement; + const addButton = compiled.querySelector('p-button'); + + expect(addButton).toBeTruthy(); + expect(addButton?.getAttribute('ng-reflect-label')).toBe('Preprints.addpreprint'); + expect(addButton?.getAttribute('ng-reflect-router-link')).toBe('/preprints,osf-preprints,submi'); + }); + + it('should display search input when not loading', () => { + fixture.componentRef.setInput('searchControl', new FormControl('')); + fixture.componentRef.setInput('preprintProvider', mockPreprintProvider); + fixture.componentRef.setInput('isPreprintProviderLoading', false); + fixture.detectChanges(); + + const compiled = fixture.nativeElement; + const searchInput = compiled.querySelector('osf-search-input'); + + expect(searchInput).toBeTruthy(); + expect(searchInput?.getAttribute('ng-reflect-show-help-icon')).toBe('true'); + expect(searchInput?.getAttribute('ng-reflect-placeholder')).toBe('Preprints.searchplaceholder'); + }); + + it('should emit triggerSearch when onTriggerSearch is called', () => { + fixture.componentRef.setInput('searchControl', new FormControl('')); + fixture.componentRef.setInput('preprintProvider', mockPreprintProvider); + fixture.componentRef.setInput('isPreprintProviderLoading', false); + fixture.detectChanges(); + + jest.spyOn(component.triggerSearch, 'emit'); + const searchValue = 'test search query'; + + component.onTriggerSearch(searchValue); + + expect(component.triggerSearch.emit).toHaveBeenCalledWith(searchValue); + }); + + it('should open help dialog when openHelpDialog is called', () => { + fixture.componentRef.setInput('searchControl', new FormControl('')); + fixture.componentRef.setInput('preprintProvider', mockPreprintProvider); + fixture.componentRef.setInput('isPreprintProviderLoading', false); + fixture.detectChanges(); + + expect(mockDialogService.open).toBeDefined(); + expect(typeof mockDialogService.open).toBe('function'); + + component.openHelpDialog(); + + expect(mockDialogService.open).toHaveBeenCalledWith(expect.any(Function), { + focusOnShow: false, + header: 'preprints.helpDialog.header', + closeOnEscape: true, + modal: true, + closable: true, + }); + }); + + it('should update when input properties change', () => { + fixture.componentRef.setInput('searchControl', new FormControl('')); + fixture.componentRef.setInput('preprintProvider', undefined); + fixture.componentRef.setInput('isPreprintProviderLoading', true); + fixture.detectChanges(); + + let compiled = fixture.nativeElement; + expect(compiled.querySelectorAll('p-skeleton').length).toBeGreaterThan(0); + expect(compiled.querySelector('.preprint-provider-name')).toBeNull(); + + fixture.componentRef.setInput('preprintProvider', mockPreprintProvider); + fixture.componentRef.setInput('isPreprintProviderLoading', false); + fixture.detectChanges(); + + compiled = fixture.nativeElement; + expect(compiled.querySelectorAll('p-skeleton').length).toBe(0); + expect(compiled.querySelector('.preprint-provider-name')).toBeTruthy(); + expect(compiled.querySelector('.preprint-provider-name')?.textContent).toBe('OSF Preprints'); }); }); diff --git a/src/app/features/preprints/components/preprint-services/preprint-services.component.spec.ts b/src/app/features/preprints/components/preprint-services/preprint-services.component.spec.ts index 3cb06d0df..fd76e1ec9 100644 --- a/src/app/features/preprints/components/preprint-services/preprint-services.component.spec.ts +++ b/src/app/features/preprints/components/preprint-services/preprint-services.component.spec.ts @@ -1,30 +1,45 @@ -import { TranslatePipe } from '@ngx-translate/core'; -import { MockPipe } from 'ng-mocks'; - import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { provideRouter } from '@angular/router'; + +import { PreprintProviderShortInfo } from '@osf/features/preprints/models'; import { PreprintServicesComponent } from './preprint-services.component'; +import { PREPRINT_PROVIDER_SHORT_INFO_MOCK } from '@testing/mocks/preprint-provider-short-info.mock'; +import { OSFTestingModule } from '@testing/osf.testing.module'; + describe('PreprintServicesComponent', () => { let component: PreprintServicesComponent; let fixture: ComponentFixture; + const mockProviders: PreprintProviderShortInfo[] = [PREPRINT_PROVIDER_SHORT_INFO_MOCK]; + beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [PreprintServicesComponent, MockPipe(TranslatePipe)], - providers: [provideRouter([])], + imports: [PreprintServicesComponent, OSFTestingModule], }).compileComponents(); fixture = TestBed.createComponent(PreprintServicesComponent); component = fixture.componentInstance; + }); + it('should create', () => { fixture.componentRef.setInput('preprintProvidersToAdvertise', []); + fixture.detectChanges(); + + expect(component).toBeTruthy(); + }); + it('should accept preprint providers input', () => { + fixture.componentRef.setInput('preprintProvidersToAdvertise', mockProviders); fixture.detectChanges(); + + expect(component.preprintProvidersToAdvertise()).toEqual(mockProviders); }); - it('should create', () => { - expect(component).toBeTruthy(); + it('should handle empty providers array', () => { + fixture.componentRef.setInput('preprintProvidersToAdvertise', []); + fixture.detectChanges(); + + expect(component.preprintProvidersToAdvertise()).toEqual([]); }); }); diff --git a/src/app/features/preprints/components/preprints-help-dialog/preprints-help-dialog.component.spec.ts b/src/app/features/preprints/components/preprints-help-dialog/preprints-help-dialog.component.spec.ts index 469f8d72d..dfe20d050 100644 --- a/src/app/features/preprints/components/preprints-help-dialog/preprints-help-dialog.component.spec.ts +++ b/src/app/features/preprints/components/preprints-help-dialog/preprints-help-dialog.component.spec.ts @@ -1,17 +1,16 @@ -import { TranslatePipe } from '@ngx-translate/core'; -import { MockPipe } from 'ng-mocks'; - import { ComponentFixture, TestBed } from '@angular/core/testing'; import { PreprintsHelpDialogComponent } from './preprints-help-dialog.component'; +import { OSFTestingModule } from '@testing/osf.testing.module'; + describe('PreprintsHelpDialogComponent', () => { let component: PreprintsHelpDialogComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [PreprintsHelpDialogComponent, MockPipe(TranslatePipe)], + imports: [PreprintsHelpDialogComponent, OSFTestingModule], }).compileComponents(); fixture = TestBed.createComponent(PreprintsHelpDialogComponent); diff --git a/src/app/features/preprints/components/stepper/author-assertion-step/array-input/array-input.component.spec.ts b/src/app/features/preprints/components/stepper/author-assertion-step/array-input/array-input.component.spec.ts index 404218819..73598cb90 100644 --- a/src/app/features/preprints/components/stepper/author-assertion-step/array-input/array-input.component.spec.ts +++ b/src/app/features/preprints/components/stepper/author-assertion-step/array-input/array-input.component.spec.ts @@ -1,27 +1,30 @@ -import { TranslateService } from '@ngx-translate/core'; -import { MockProvider } from 'ng-mocks'; +import { MockComponent } from 'ng-mocks'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { FormArray, FormControl, Validators } from '@angular/forms'; +import { TextInputComponent } from '@shared/components'; + import { ArrayInputComponent } from './array-input.component'; +import { OSFTestingModule } from '@testing/osf.testing.module'; + describe('ArrayInputComponent', () => { let component: ArrayInputComponent; let fixture: ComponentFixture; - const array = new FormArray([new FormControl('')]); + let formArray: FormArray; beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [ArrayInputComponent], - providers: [MockProvider(TranslateService)], + imports: [ArrayInputComponent, MockComponent(TextInputComponent), OSFTestingModule], }).compileComponents(); fixture = TestBed.createComponent(ArrayInputComponent); component = fixture.componentInstance; - fixture.componentRef.setInput('formArray', array); - fixture.componentRef.setInput('inputPlaceholder', ''); + formArray = new FormArray([new FormControl('test')]); + fixture.componentRef.setInput('formArray', formArray); + fixture.componentRef.setInput('inputPlaceholder', 'Enter value'); fixture.componentRef.setInput('validators', [Validators.required]); fixture.detectChanges(); @@ -30,4 +33,71 @@ describe('ArrayInputComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + it('should have correct input values', () => { + expect(component.formArray()).toBe(formArray); + expect(component.inputPlaceholder()).toBe('Enter value'); + expect(component.validators()).toEqual([Validators.required]); + }); + + it('should add new control to form array', () => { + const initialLength = formArray.length; + + component.add(); + + expect(formArray.length).toBe(initialLength + 1); + expect(formArray.at(formArray.length - 1)).toBeInstanceOf(FormControl); + }); + + it('should add control with correct validators', () => { + component.add(); + + const newControl = formArray.at(formArray.length - 1); + expect(newControl.hasError('required')).toBe(true); + }); + + it('should remove control at specified index', () => { + component.add(); + component.add(); + const initialLength = formArray.length; + + component.remove(1); + + expect(formArray.length).toBe(initialLength - 1); + }); + + it('should not remove control if only one control exists', () => { + const singleControlArray = new FormArray([new FormControl('only')]); + fixture.componentRef.setInput('formArray', singleControlArray); + fixture.detectChanges(); + + const initialLength = singleControlArray.length; + + component.remove(0); + + expect(singleControlArray.length).toBe(initialLength); + }); + + it('should handle multiple add and remove operations', () => { + const initialLength = formArray.length; + + component.add(); + component.add(); + component.add(); + + expect(formArray.length).toBe(initialLength + 3); + + component.remove(1); + component.remove(2); + + expect(formArray.length).toBe(initialLength + 1); + }); + + it('should create controls with nonNullable true', () => { + component.add(); + + const newControl = formArray.at(formArray.length - 1); + expect(newControl.value).toBe(''); + expect(newControl.hasError('required')).toBe(true); + }); }); diff --git a/src/app/features/preprints/components/stepper/author-assertion-step/author-assertions-step.component.spec.ts b/src/app/features/preprints/components/stepper/author-assertion-step/author-assertions-step.component.spec.ts index 66cfe2c13..22fdaa745 100644 --- a/src/app/features/preprints/components/stepper/author-assertion-step/author-assertions-step.component.spec.ts +++ b/src/app/features/preprints/components/stepper/author-assertion-step/author-assertions-step.component.spec.ts @@ -1,55 +1,110 @@ -import { Store } from '@ngxs/store'; - -import { TranslatePipe, TranslateService } from '@ngx-translate/core'; -import { MockPipe, MockProvider, MockProviders } from 'ng-mocks'; +import { MockComponents, MockProvider } from 'ng-mocks'; import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ArrayInputComponent } from '@osf/features/preprints/components/stepper/author-assertion-step/array-input/array-input.component'; import { ApplicabilityStatus } from '@osf/features/preprints/enums'; +import { Preprint } from '@osf/features/preprints/models'; import { PreprintStepperSelectors } from '@osf/features/preprints/store/preprint-stepper'; -import { MOCK_STORE } from '@shared/mocks'; +import { FormSelectComponent } from '@shared/components'; import { CustomConfirmationService, ToastService } from '@shared/services'; import { AuthorAssertionsStepComponent } from './author-assertions-step.component'; -const mockPreprint = { - id: '1', - hasCoi: false, - coiStatement: null, - hasDataLinks: ApplicabilityStatus.NotApplicable, - dataLinks: [], - whyNoData: null, - hasPreregLinks: ApplicabilityStatus.NotApplicable, - preregLinks: [], - whyNoPrereg: null, - preregLinkInfo: null, -}; +import { PREPRINT_MOCK } from '@testing/mocks/preprint.mock'; +import { TranslationServiceMock } from '@testing/mocks/translation.service.mock'; +import { OSFTestingModule } from '@testing/osf.testing.module'; +import { CustomConfirmationServiceMockBuilder } from '@testing/providers/custom-confirmation-provider.mock'; +import { provideMockStore } from '@testing/providers/store-provider.mock'; +import { ToastServiceMockBuilder } from '@testing/providers/toast-provider.mock'; describe('AuthorAssertionsStepComponent', () => { let component: AuthorAssertionsStepComponent; let fixture: ComponentFixture; + let toastServiceMock: ReturnType; + let customConfirmationServiceMock: ReturnType; + + const mockPreprint: Preprint = PREPRINT_MOCK; beforeEach(async () => { - (MOCK_STORE.selectSignal as jest.Mock).mockImplementation((selector) => { - if (selector === PreprintStepperSelectors.getPreprint) return () => mockPreprint; - if (selector === PreprintStepperSelectors.isPreprintSubmitting) return () => false; - return () => null; - }); + toastServiceMock = ToastServiceMockBuilder.create().build(); + customConfirmationServiceMock = CustomConfirmationServiceMockBuilder.create().build(); await TestBed.configureTestingModule({ - imports: [AuthorAssertionsStepComponent, MockPipe(TranslatePipe)], + imports: [ + AuthorAssertionsStepComponent, + OSFTestingModule, + MockComponents(ArrayInputComponent, FormSelectComponent), + ], providers: [ - MockProvider(Store, MOCK_STORE), - MockProviders(ToastService, CustomConfirmationService, TranslateService), + TranslationServiceMock, + MockProvider(ToastService, toastServiceMock), + MockProvider(CustomConfirmationService, customConfirmationServiceMock), + provideMockStore({ + signals: [ + { + selector: PreprintStepperSelectors.getPreprint, + value: mockPreprint, + }, + { + selector: PreprintStepperSelectors.isPreprintSubmitting, + value: false, + }, + ], + }), ], }).compileComponents(); fixture = TestBed.createComponent(AuthorAssertionsStepComponent); component = fixture.componentInstance; - fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); + + it('should initialize form with preprint data', () => { + expect(component.authorAssertionsForm.get('hasCoi')?.value).toBe(false); + expect(component.authorAssertionsForm.get('coiStatement')?.value).toBeNull(); + expect(component.authorAssertionsForm.get('hasDataLinks')?.value).toBe(ApplicabilityStatus.NotApplicable); + expect(component.authorAssertionsForm.get('hasPreregLinks')?.value).toBe(ApplicabilityStatus.NotApplicable); + }); + + it('should emit nextClicked when nextButtonClicked is called', () => { + const emitSpy = jest.spyOn(component.nextClicked, 'emit'); + component.nextButtonClicked(); + + expect(toastServiceMock.showSuccess).toHaveBeenCalledWith( + 'preprints.preprintStepper.common.successMessages.preprintSaved' + ); + expect(emitSpy).toHaveBeenCalled(); + }); + + it('should show confirmation dialog when backButtonClicked is called with changes', () => { + component.authorAssertionsForm.patchValue({ hasCoi: true }); + + component.backButtonClicked(); + + expect(customConfirmationServiceMock.confirmContinue).toHaveBeenCalledWith({ + headerKey: 'common.discardChanges.header', + messageKey: 'common.discardChanges.message', + onConfirm: expect.any(Function), + onReject: expect.any(Function), + }); + }); + + it('should expose readonly properties', () => { + expect(component.CustomValidators).toBeDefined(); + expect(component.ApplicabilityStatus).toBe(ApplicabilityStatus); + expect(component.inputLimits).toBeDefined(); + expect(component.INPUT_VALIDATION_MESSAGES).toBeDefined(); + expect(component.preregLinkOptions).toBeDefined(); + expect(component.linkValidators).toBeDefined(); + }); + + it('should have correct signal values', () => { + expect(component.hasCoiValue()).toBe(false); + expect(component.hasDataLinks()).toBe(ApplicabilityStatus.NotApplicable); + expect(component.hasPreregLinks()).toBe(ApplicabilityStatus.NotApplicable); + }); }); diff --git a/src/app/features/preprints/components/stepper/file-step/file-step.component.spec.ts b/src/app/features/preprints/components/stepper/file-step/file-step.component.spec.ts index 5d9c504b0..2a557fc0f 100644 --- a/src/app/features/preprints/components/stepper/file-step/file-step.component.spec.ts +++ b/src/app/features/preprints/components/stepper/file-step/file-step.component.spec.ts @@ -1,46 +1,218 @@ -import { Store } from '@ngxs/store'; - -import { TranslatePipe, TranslateService } from '@ngx-translate/core'; -import { MockPipe, MockProvider } from 'ng-mocks'; - -import { DialogService } from 'primeng/dynamicdialog'; +import { MockComponents, MockProvider } from 'ng-mocks'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { ActivatedRoute, Router } from '@angular/router'; -import { MOCK_PROVIDER, MOCK_STORE } from '@shared/mocks'; -import { CustomConfirmationService, FilesService, ToastService } from '@shared/services'; +import { PreprintFileSource } from '@osf/features/preprints/enums'; +import { Preprint, PreprintProviderDetails } from '@osf/features/preprints/models'; +import { PreprintStepperSelectors } from '@osf/features/preprints/store/preprint-stepper'; +import { FilesTreeComponent, IconComponent } from '@shared/components'; +import { OsfFile } from '@shared/models'; +import { CustomConfirmationService, ToastService } from '@shared/services'; import { FileStepComponent } from './file-step.component'; -describe.skip('FileStepComponent', () => { +import { OSF_FILE_MOCK } from '@testing/mocks/osf-file.mock'; +import { PREPRINT_MOCK } from '@testing/mocks/preprint.mock'; +import { PREPRINT_PROVIDER_DETAILS_MOCK } from '@testing/mocks/preprint-provider-details'; +import { OSFTestingModule } from '@testing/osf.testing.module'; +import { CustomConfirmationServiceMockBuilder } from '@testing/providers/custom-confirmation-provider.mock'; +import { provideMockStore } from '@testing/providers/store-provider.mock'; +import { ToastServiceMockBuilder } from '@testing/providers/toast-provider.mock'; + +describe('FileStepComponent', () => { let component: FileStepComponent; let fixture: ComponentFixture; + let toastServiceMock: ReturnType; + let confirmationServiceMock: ReturnType; + + const mockProvider: PreprintProviderDetails = PREPRINT_PROVIDER_DETAILS_MOCK; + const mockPreprint: Preprint = PREPRINT_MOCK; + const mockProjectFiles: OsfFile[] = [OSF_FILE_MOCK]; + const mockPreprintFile: OsfFile = OSF_FILE_MOCK; + + const mockAvailableProjects = [ + { id: 'project-1', title: 'Test Project 1' }, + { id: 'project-2', title: 'Test Project 2' }, + ]; beforeEach(async () => { + toastServiceMock = ToastServiceMockBuilder.create().build(); + confirmationServiceMock = CustomConfirmationServiceMockBuilder.create().build(); + await TestBed.configureTestingModule({ - imports: [FileStepComponent, MockPipe(TranslatePipe)], + imports: [FileStepComponent, MockComponents(IconComponent, FilesTreeComponent), OSFTestingModule], providers: [ - MockProvider(Store, MOCK_STORE), - MockProvider(DialogService), - MockProvider(ToastService), - MockProvider(CustomConfirmationService), - MockProvider(TranslateService), - MockProvider(ActivatedRoute, {}), - MockProvider(Router, {}), - MockProvider(FilesService), + MockProvider(ToastService, toastServiceMock), + MockProvider(CustomConfirmationService, confirmationServiceMock), + provideMockStore({ + signals: [ + { + selector: PreprintStepperSelectors.getPreprint, + value: mockPreprint, + }, + { + selector: PreprintStepperSelectors.getSelectedProviderId, + value: 'provider-1', + }, + { + selector: PreprintStepperSelectors.getSelectedFileSource, + value: PreprintFileSource.None, + }, + { + selector: PreprintStepperSelectors.getUploadLink, + value: 'upload-link', + }, + { + selector: PreprintStepperSelectors.getPreprintFile, + value: mockPreprintFile, + }, + { + selector: PreprintStepperSelectors.isPreprintFilesLoading, + value: false, + }, + { + selector: PreprintStepperSelectors.getAvailableProjects, + value: mockAvailableProjects, + }, + { + selector: PreprintStepperSelectors.areAvailableProjectsLoading, + value: false, + }, + { + selector: PreprintStepperSelectors.getProjectFiles, + value: mockProjectFiles, + }, + { + selector: PreprintStepperSelectors.areProjectFilesLoading, + value: false, + }, + { + selector: PreprintStepperSelectors.getCurrentFolder, + value: null, + }, + ], + }), ], }).compileComponents(); fixture = TestBed.createComponent(FileStepComponent); component = fixture.componentInstance; - - fixture.componentRef.setInput('provider', MOCK_PROVIDER); - + fixture.componentRef.setInput('provider', mockProvider); fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); + + it('should initialize with correct values', () => { + expect(component.provider()).toBe(mockProvider); + expect(component.preprint()).toBe(mockPreprint); + expect(component.selectedFileSource()).toBe(PreprintFileSource.None); + expect(component.preprintFile()).toBe(mockPreprintFile); + }); + + it('should emit backClicked when backButtonClicked is called', () => { + const emitSpy = jest.spyOn(component.backClicked, 'emit'); + + component.backButtonClicked(); + + expect(emitSpy).toHaveBeenCalled(); + }); + + it('should emit nextClicked when nextButtonClicked is called with primary file', () => { + const emitSpy = jest.spyOn(component.nextClicked, 'emit'); + + component.nextButtonClicked(); + + expect(toastServiceMock.showSuccess).toHaveBeenCalledWith( + 'preprints.preprintStepper.common.successMessages.preprintSaved' + ); + expect(emitSpy).toHaveBeenCalled(); + }); + + it('should not emit nextClicked when nextButtonClicked is called without primary file', () => { + const emitSpy = jest.spyOn(component.nextClicked, 'emit'); + jest.spyOn(component, 'preprint').mockReturnValue({ ...mockPreprint, primaryFileId: null }); + + component.nextButtonClicked(); + + expect(emitSpy).not.toHaveBeenCalled(); + }); + + it('should handle file selection for upload', () => { + const mockFile = new File(['test'], 'test.pdf', { type: 'application/pdf' }); + const mockEvent = { + target: { + files: [mockFile], + }, + } as any; + + component.onFileSelected(mockEvent); + + expect(mockFile).toBeDefined(); + }); + + it('should handle file selection for reupload', () => { + component.versionFileMode.set(true); + + const mockFile = new File(['test'], 'test.pdf', { type: 'application/pdf' }); + const mockEvent = { + target: { + files: [mockFile], + }, + } as any; + + component.onFileSelected(mockEvent); + + expect(component.versionFileMode()).toBe(false); + }); + + it('should handle project file selection', () => { + const mockFile: OsfFile = OSF_FILE_MOCK; + + component.selectProjectFile(mockFile); + + expect(mockFile).toBeDefined(); + }); + + it('should handle version file confirmation', () => { + confirmationServiceMock.confirmContinue.mockImplementation(({ onConfirm }) => { + onConfirm(); + }); + + component.versionFile(); + + expect(confirmationServiceMock.confirmContinue).toHaveBeenCalledWith({ + headerKey: 'preprints.preprintStepper.file.versionFile.header', + messageKey: 'preprints.preprintStepper.file.versionFile.message', + onConfirm: expect.any(Function), + onReject: expect.any(Function), + }); + expect(component.versionFileMode()).toBe(true); + }); + + it('should handle cancel button click', () => { + jest.spyOn(component, 'preprintFile').mockReturnValue(null); + + component.cancelButtonClicked(); + + expect(component.preprintFile()).toBeNull(); + }); + + it('should not handle cancel button click when preprint file exists', () => { + component.cancelButtonClicked(); + + expect(component.preprintFile()).toBeDefined(); + }); + + it('should expose readonly properties', () => { + expect(component.PreprintFileSource).toBe(PreprintFileSource); + expect(component.filesTreeActions).toBeDefined(); + }); + + it('should have correct form control', () => { + expect(component.projectNameControl).toBeDefined(); + expect(component.projectNameControl.value).toBeNull(); + }); }); diff --git a/src/app/features/preprints/components/stepper/metadata-step/contributors/contributors.component.spec.ts b/src/app/features/preprints/components/stepper/metadata-step/contributors/contributors.component.spec.ts index 4bd1cf675..f0a6965bd 100644 --- a/src/app/features/preprints/components/stepper/metadata-step/contributors/contributors.component.spec.ts +++ b/src/app/features/preprints/components/stepper/metadata-step/contributors/contributors.component.spec.ts @@ -1,46 +1,97 @@ -import { Store } from '@ngxs/store'; - -import { TranslatePipe } from '@ngx-translate/core'; -import { MockPipe, MockProvider } from 'ng-mocks'; - -import { DialogService } from 'primeng/dynamicdialog'; +import { MockComponent, MockProvider } from 'ng-mocks'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { provideNoopAnimations } from '@angular/platform-browser/animations'; -import { MOCK_STORE, TranslateServiceMock } from '@shared/mocks'; +import { UserSelectors } from '@core/store/user'; +import { ContributorsListComponent } from '@shared/components/contributors'; +import { MOCK_CONTRIBUTOR, MOCK_USER } from '@shared/mocks'; +import { ContributorModel } from '@shared/models'; import { CustomConfirmationService, ToastService } from '@shared/services'; +import { ContributorsSelectors } from '@shared/stores'; import { ContributorsComponent } from './contributors.component'; +import { OSFTestingModule } from '@testing/osf.testing.module'; +import { CustomConfirmationServiceMockBuilder } from '@testing/providers/custom-confirmation-provider.mock'; +import { provideMockStore } from '@testing/providers/store-provider.mock'; +import { ToastServiceMockBuilder } from '@testing/providers/toast-provider.mock'; + describe('ContributorsComponent', () => { let component: ContributorsComponent; let fixture: ComponentFixture; + let toastServiceMock: ReturnType; + let confirmationServiceMock: ReturnType; + + const mockContributors: ContributorModel[] = [MOCK_CONTRIBUTOR]; + const mockCurrentUser = MOCK_USER; beforeEach(async () => { - (MOCK_STORE.selectSignal as jest.Mock).mockImplementation(() => () => []); + toastServiceMock = ToastServiceMockBuilder.create().build(); + confirmationServiceMock = CustomConfirmationServiceMockBuilder.create().build(); await TestBed.configureTestingModule({ - imports: [ContributorsComponent, MockPipe(TranslatePipe)], + imports: [ContributorsComponent, OSFTestingModule, MockComponent(ContributorsListComponent)], providers: [ - MockProvider(Store, MOCK_STORE), - MockProvider(DialogService), - MockProvider(ToastService), - MockProvider(CustomConfirmationService), - TranslateServiceMock, - provideNoopAnimations(), + MockProvider(ToastService, toastServiceMock), + MockProvider(CustomConfirmationService, confirmationServiceMock), + provideMockStore({ + signals: [ + { + selector: ContributorsSelectors.getContributors, + value: mockContributors, + }, + { + selector: ContributorsSelectors.isContributorsLoading, + value: false, + }, + { + selector: UserSelectors.getCurrentUser, + value: mockCurrentUser, + }, + ], + }), ], }).compileComponents(); fixture = TestBed.createComponent(ContributorsComponent); component = fixture.componentInstance; + fixture.componentRef.setInput('preprintId', 'preprint-1'); + }); - fixture.componentRef.setInput('preprintId', '1'); + it('should create', () => { + expect(component).toBeTruthy(); + }); - fixture.detectChanges(); + it('should remove contributor with confirmation', () => { + const contributorToRemove = mockContributors[0]; + + confirmationServiceMock.confirmDelete.mockImplementation(({ onConfirm }) => { + onConfirm(); + }); + + component.removeContributor(contributorToRemove); + + expect(confirmationServiceMock.confirmDelete).toHaveBeenCalledWith({ + headerKey: 'project.contributors.removeDialog.title', + messageKey: 'project.contributors.removeDialog.message', + messageParams: { name: contributorToRemove.fullName }, + acceptLabelKey: 'common.buttons.remove', + onConfirm: expect.any(Function), + }); }); - it('should create', () => { + it('should expose readonly properties', () => { + expect(component.destroyRef).toBeDefined(); + expect(component.translateService).toBeDefined(); + expect(component.dialogService).toBeDefined(); + expect(component.toastService).toBeDefined(); + expect(component.customConfirmationService).toBeDefined(); + expect(component.actions).toBeDefined(); + }); + + it('should handle effect for contributors', () => { + component.ngOnInit(); + expect(component).toBeTruthy(); }); }); diff --git a/src/app/features/preprints/components/stepper/metadata-step/metadata-step.component.spec.ts b/src/app/features/preprints/components/stepper/metadata-step/metadata-step.component.spec.ts index 381e51df7..9edbb8481 100644 --- a/src/app/features/preprints/components/stepper/metadata-step/metadata-step.component.spec.ts +++ b/src/app/features/preprints/components/stepper/metadata-step/metadata-step.component.spec.ts @@ -1,52 +1,179 @@ -import { Store } from '@ngxs/store'; - -import { TranslatePipe } from '@ngx-translate/core'; -import { MockPipe, MockProvider } from 'ng-mocks'; +import { MockComponents, MockProvider } from 'ng-mocks'; import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormControl, FormGroup } from '@angular/forms'; import { provideNoopAnimations } from '@angular/platform-browser/animations'; +import { formInputLimits } from '@osf/features/preprints/constants'; +import { PreprintProviderDetails } from '@osf/features/preprints/models'; import { PreprintStepperSelectors } from '@osf/features/preprints/store/preprint-stepper'; -import { MOCK_STORE, TranslateServiceMock } from '@shared/mocks'; +import { IconComponent, LicenseComponent, TextInputComponent } from '@shared/components'; +import { INPUT_VALIDATION_MESSAGES } from '@shared/constants'; +import { MOCK_LICENSE } from '@shared/mocks'; +import { LicenseModel } from '@shared/models'; import { CustomConfirmationService, ToastService } from '@shared/services'; +import { ContributorsComponent } from './contributors/contributors.component'; +import { PreprintsAffiliatedInstitutionsComponent } from './preprints-affiliated-institutions/preprints-affiliated-institutions.component'; +import { PreprintsSubjectsComponent } from './preprints-subjects/preprints-subjects.component'; import { MetadataStepComponent } from './metadata-step.component'; +import { PREPRINT_MOCK } from '@testing/mocks/preprint.mock'; +import { PREPRINT_PROVIDER_DETAILS_MOCK } from '@testing/mocks/preprint-provider-details'; +import { OSFTestingModule } from '@testing/osf.testing.module'; +import { CustomConfirmationServiceMockBuilder } from '@testing/providers/custom-confirmation-provider.mock'; +import { provideMockStore } from '@testing/providers/store-provider.mock'; +import { ToastServiceMockBuilder } from '@testing/providers/toast-provider.mock'; + describe('MetadataStepComponent', () => { let component: MetadataStepComponent; let fixture: ComponentFixture; + let toastServiceMock: ReturnType; + let customConfirmationServiceMock: ReturnType; + + const mockProvider: PreprintProviderDetails = PREPRINT_PROVIDER_DETAILS_MOCK; + const mockPreprint = PREPRINT_MOCK; + const mockLicenses: LicenseModel[] = [MOCK_LICENSE]; beforeEach(async () => { - (MOCK_STORE.selectSignal as jest.Mock).mockImplementation((selector) => { - switch (selector) { - case PreprintStepperSelectors.getLicenses: - return () => []; - case PreprintStepperSelectors.getPreprint: - return () => null; - case PreprintStepperSelectors.isPreprintSubmitting: - return () => false; - default: - return () => []; - } - }); + toastServiceMock = ToastServiceMockBuilder.create().withShowSuccess(jest.fn()).build(); + customConfirmationServiceMock = CustomConfirmationServiceMockBuilder.create() + .withConfirmContinue(jest.fn()) + .build(); await TestBed.configureTestingModule({ - imports: [MetadataStepComponent, MockPipe(TranslatePipe)], + imports: [ + MetadataStepComponent, + OSFTestingModule, + ...MockComponents( + ContributorsComponent, + PreprintsAffiliatedInstitutionsComponent, + PreprintsSubjectsComponent, + IconComponent, + LicenseComponent, + TextInputComponent + ), + ], providers: [ - MockProvider(Store, MOCK_STORE), - MockProvider(ToastService), - MockProvider(CustomConfirmationService), - TranslateServiceMock, + MockProvider(ToastService, toastServiceMock), + MockProvider(CustomConfirmationService, customConfirmationServiceMock), provideNoopAnimations(), + provideMockStore({ + signals: [ + { + selector: PreprintStepperSelectors.getLicenses, + value: mockLicenses, + }, + { + selector: PreprintStepperSelectors.getPreprint, + value: mockPreprint, + }, + { + selector: PreprintStepperSelectors.isPreprintSubmitting, + value: false, + }, + ], + }), ], }).compileComponents(); fixture = TestBed.createComponent(MetadataStepComponent); component = fixture.componentInstance; + fixture.componentRef.setInput('provider', mockProvider); fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); + + it('should initialize with correct default values', () => { + expect(component.inputLimits).toBe(formInputLimits); + expect(component.INPUT_VALIDATION_MESSAGES).toBe(INPUT_VALIDATION_MESSAGES); + expect(component.today).toBeInstanceOf(Date); + }); + + it('should initialize form with correct structure', () => { + fixture.detectChanges(); + expect(component.metadataForm).toBeInstanceOf(FormGroup); + expect(component.metadataForm.controls['doi']).toBeInstanceOf(FormControl); + expect(component.metadataForm.controls['originalPublicationDate']).toBeInstanceOf(FormControl); + expect(component.metadataForm.controls['customPublicationCitation']).toBeInstanceOf(FormControl); + expect(component.metadataForm.controls['tags']).toBeInstanceOf(FormControl); + expect(component.metadataForm.controls['subjects']).toBeInstanceOf(FormControl); + }); + + it('should return licenses from store', () => { + const licenses = component.licenses(); + expect(licenses).toBe(mockLicenses); + }); + + it('should return created preprint from store', () => { + const preprint = component.createdPreprint(); + expect(preprint).toBe(mockPreprint); + }); + + it('should return submission state from store', () => { + const isSubmitting = component.isUpdatingPreprint(); + expect(isSubmitting).toBe(false); + }); + + it('should return provider input', () => { + const provider = component.provider(); + expect(provider).toBe(mockProvider); + }); + + it('should handle next button click with valid form', () => { + fixture.detectChanges(); + const nextClickedSpy = jest.spyOn(component.nextClicked, 'emit'); + + component.metadataForm.patchValue({ + subjects: [{ id: 'subject1', name: 'Test Subject' }], + }); + + component.nextButtonClicked(); + + expect(nextClickedSpy).toHaveBeenCalled(); + }); + + it('should not proceed with next button click when form is invalid', () => { + fixture.detectChanges(); + const nextClickedSpy = jest.spyOn(component.nextClicked, 'emit'); + + component.metadataForm.patchValue({ + subjects: [], + }); + + component.nextButtonClicked(); + + expect(nextClickedSpy).not.toHaveBeenCalled(); + }); + + it('should handle back button click with changes', () => { + component.metadataForm.patchValue({ + doi: 'new-doi', + }); + + component.backButtonClicked(); + + expect(customConfirmationServiceMock.confirmContinue).toHaveBeenCalled(); + }); + + it('should handle select license without required fields', () => { + const license = mockLicenses[0]; + + expect(() => component.selectLicense(license)).not.toThrow(); + }); + + it('should handle select license with required fields', () => { + const license = mockLicenses[0]; + + expect(() => component.selectLicense(license)).not.toThrow(); + }); + + it('should handle edge case with empty licenses', () => { + const licenses = component.licenses(); + expect(licenses).toBeDefined(); + expect(Array.isArray(licenses)).toBe(true); + }); }); diff --git a/src/app/features/preprints/components/stepper/metadata-step/preprints-affiliated-institutions/preprints-affiliated-institutions.component.spec.ts b/src/app/features/preprints/components/stepper/metadata-step/preprints-affiliated-institutions/preprints-affiliated-institutions.component.spec.ts index 031eda263..b750716b7 100644 --- a/src/app/features/preprints/components/stepper/metadata-step/preprints-affiliated-institutions/preprints-affiliated-institutions.component.spec.ts +++ b/src/app/features/preprints/components/stepper/metadata-step/preprints-affiliated-institutions/preprints-affiliated-institutions.component.spec.ts @@ -1,22 +1,121 @@ +import { MockComponent } from 'ng-mocks'; + import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { PreprintProviderDetails } from '@osf/features/preprints/models'; +import { AffiliatedInstitutionSelectComponent } from '@shared/components'; +import { MOCK_INSTITUTION } from '@shared/mocks'; +import { Institution } from '@shared/models'; +import { InstitutionsSelectors } from '@shared/stores/institutions'; + import { PreprintsAffiliatedInstitutionsComponent } from './preprints-affiliated-institutions.component'; -describe.skip('PreprintsAffiliatedInstitutionsComponent', () => { +import { PREPRINT_PROVIDER_DETAILS_MOCK } from '@testing/mocks/preprint-provider-details'; +import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideMockStore } from '@testing/providers/store-provider.mock'; + +describe('PreprintsAffiliatedInstitutionsComponent', () => { let component: PreprintsAffiliatedInstitutionsComponent; let fixture: ComponentFixture; + const mockProvider: PreprintProviderDetails = PREPRINT_PROVIDER_DETAILS_MOCK; + const mockUserInstitutions: Institution[] = [MOCK_INSTITUTION]; + const mockResourceInstitutions: Institution[] = [MOCK_INSTITUTION]; + beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [PreprintsAffiliatedInstitutionsComponent], + imports: [ + PreprintsAffiliatedInstitutionsComponent, + OSFTestingModule, + MockComponent(AffiliatedInstitutionSelectComponent), + ], + providers: [ + provideMockStore({ + signals: [ + { + selector: InstitutionsSelectors.getUserInstitutions, + value: mockUserInstitutions, + }, + { + selector: InstitutionsSelectors.areUserInstitutionsLoading, + value: false, + }, + { + selector: InstitutionsSelectors.getResourceInstitutions, + value: mockResourceInstitutions, + }, + { + selector: InstitutionsSelectors.areResourceInstitutionsLoading, + value: false, + }, + { + selector: InstitutionsSelectors.areResourceInstitutionsSubmitting, + value: false, + }, + ], + }), + ], }).compileComponents(); fixture = TestBed.createComponent(PreprintsAffiliatedInstitutionsComponent); component = fixture.componentInstance; + fixture.componentRef.setInput('provider', mockProvider); + fixture.componentRef.setInput('preprintId', 'preprint-1'); fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); + + it('should initialize with correct values', () => { + expect(component.provider()).toBe(mockProvider); + expect(component.preprintId()).toBe('preprint-1'); + expect(component.userInstitutions()).toBe(mockUserInstitutions); + expect(component.areUserInstitutionsLoading()).toBe(false); + expect(component.resourceInstitutions()).toBe(mockResourceInstitutions); + expect(component.areResourceInstitutionsLoading()).toBe(false); + expect(component.areResourceInstitutionsSubmitting()).toBe(false); + }); + + it('should initialize selectedInstitutions with resource institutions', () => { + expect(component.selectedInstitutions()).toEqual(mockResourceInstitutions); + }); + + it('should handle institutions change', () => { + const newInstitutions = [MOCK_INSTITUTION]; + + component.onInstitutionsChange(newInstitutions); + + expect(component.selectedInstitutions()).toEqual(newInstitutions); + }); + + it('should handle effect for resource institutions', () => { + const newResourceInstitutions = [MOCK_INSTITUTION]; + + jest.spyOn(component, 'resourceInstitutions').mockReturnValue(newResourceInstitutions); + component.ngOnInit(); + + expect(component.selectedInstitutions()).toEqual(newResourceInstitutions); + }); + + it('should not update selectedInstitutions when resource institutions is empty', () => { + const initialInstitutions = component.selectedInstitutions(); + + jest.spyOn(component, 'resourceInstitutions').mockReturnValue([]); + component.ngOnInit(); + + expect(component.selectedInstitutions()).toEqual(initialInstitutions); + }); + + it('should handle multiple institution changes', () => { + const firstChange = [MOCK_INSTITUTION]; + const secondChange = [MOCK_INSTITUTION]; + + component.onInstitutionsChange(firstChange); + expect(component.selectedInstitutions()).toEqual(firstChange); + + component.onInstitutionsChange(secondChange); + expect(component.selectedInstitutions()).toEqual(secondChange); + }); }); diff --git a/src/app/features/preprints/components/stepper/supplements-step/supplements-step.component.spec.ts b/src/app/features/preprints/components/stepper/supplements-step/supplements-step.component.spec.ts index f72bb653c..0d5fe09fb 100644 --- a/src/app/features/preprints/components/stepper/supplements-step/supplements-step.component.spec.ts +++ b/src/app/features/preprints/components/stepper/supplements-step/supplements-step.component.spec.ts @@ -1,47 +1,65 @@ -import { Store } from '@ngxs/store'; +import { MockComponent, MockProvider } from 'ng-mocks'; -import { TranslatePipe } from '@ngx-translate/core'; -import { MockPipe, MockProvider, MockProviders } from 'ng-mocks'; - -import { ConfirmationService, MessageService } from 'primeng/api'; +import { ConfirmationService } from 'primeng/api'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { PreprintStepperSelectors } from '@osf/features/preprints/store/preprint-stepper'; -import { MOCK_STORE, TranslateServiceMock } from '@shared/mocks'; +import { AddProjectFormComponent } from '@shared/components'; +import { TranslateServiceMock } from '@shared/mocks'; +import { ToastService } from '@shared/services'; import { SupplementsStepComponent } from './supplements-step.component'; +import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideMockStore } from '@testing/providers/store-provider.mock'; +import { ToastServiceMock } from '@testing/providers/toast-provider.mock'; + describe('SupplementsStepComponent', () => { let component: SupplementsStepComponent; let fixture: ComponentFixture; + let mockToastService: ReturnType; beforeEach(async () => { - (MOCK_STORE.selectSignal as jest.Mock).mockImplementation((selector) => { - switch (selector) { - case PreprintStepperSelectors.getPreprint: - return () => ({}); - case PreprintStepperSelectors.isPreprintSubmitting: - return () => false; - case PreprintStepperSelectors.getAvailableProjects: - return () => []; - case PreprintStepperSelectors.areAvailableProjectsLoading: - return () => false; - case PreprintStepperSelectors.getPreprintProject: - return () => null; - case PreprintStepperSelectors.isPreprintProjectLoading: - return () => false; - default: - return () => null; - } - }); + mockToastService = ToastServiceMock.simple(); await TestBed.configureTestingModule({ - imports: [SupplementsStepComponent, MockPipe(TranslatePipe)], + imports: [SupplementsStepComponent, MockComponent(AddProjectFormComponent), OSFTestingModule], providers: [ - MockProvider(Store, MOCK_STORE), + provideMockStore({ + signals: [ + { + selector: PreprintStepperSelectors.getPreprint, + value: {}, + }, + { + selector: PreprintStepperSelectors.isPreprintSubmitting, + value: false, + }, + { + selector: PreprintStepperSelectors.getAvailableProjects, + value: [], + }, + { + selector: PreprintStepperSelectors.areAvailableProjectsLoading, + value: false, + }, + { + selector: PreprintStepperSelectors.getPreprintProject, + value: null, + }, + { + selector: PreprintStepperSelectors.isPreprintProjectLoading, + value: false, + }, + ], + }), TranslateServiceMock, - MockProviders(ConfirmationService, MessageService), + MockProvider(ConfirmationService, { + confirm: jest.fn(), + close: jest.fn(), + }), + { provide: ToastService, useValue: mockToastService }, ], }).compileComponents(); @@ -53,4 +71,55 @@ describe('SupplementsStepComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + it('should call getAvailableProjects when project name changes after debounce', () => { + jest.useFakeTimers(); + + const getAvailableProjectsSpy = jest.fn(); + Object.defineProperty(component, 'actions', { + value: { getAvailableProjects: getAvailableProjectsSpy }, + writable: true, + }); + + component.ngOnInit(); + component.projectNameControl.setValue('test-project'); + jest.advanceTimersByTime(500); + + expect(getAvailableProjectsSpy).toHaveBeenCalledWith('test-project'); + jest.useRealTimers(); + }); + + it('should not call getAvailableProjects if value is the same as selectedProjectId', () => { + jest.useFakeTimers(); + const getAvailableProjectsSpy = jest.fn(); + + Object.defineProperty(component, 'actions', { + value: { getAvailableProjects: getAvailableProjectsSpy }, + writable: true, + }); + jest.spyOn(component, 'selectedProjectId').mockReturnValue('test-project'); + + component.ngOnInit(); + component.projectNameControl.setValue('test-project'); + jest.advanceTimersByTime(500); + + expect(getAvailableProjectsSpy).not.toHaveBeenCalled(); + jest.useRealTimers(); + }); + + it('should handle empty values', () => { + jest.useFakeTimers(); + const getAvailableProjectsSpy = jest.fn(); + Object.defineProperty(component, 'actions', { + value: { getAvailableProjects: getAvailableProjectsSpy }, + writable: true, + }); + + component.ngOnInit(); + component.projectNameControl.setValue(''); + jest.advanceTimersByTime(500); + + expect(getAvailableProjectsSpy).toHaveBeenCalledWith(''); + jest.useRealTimers(); + }); }); diff --git a/src/app/features/preprints/components/stepper/title-and-abstract-step/title-and-abstract-step.component.spec.ts b/src/app/features/preprints/components/stepper/title-and-abstract-step/title-and-abstract-step.component.spec.ts index 489470a08..a5645f971 100644 --- a/src/app/features/preprints/components/stepper/title-and-abstract-step/title-and-abstract-step.component.spec.ts +++ b/src/app/features/preprints/components/stepper/title-and-abstract-step/title-and-abstract-step.component.spec.ts @@ -1,45 +1,173 @@ -import { Store } from '@ngxs/store'; - -import { TranslatePipe } from '@ngx-translate/core'; -import { MockPipe, MockProvider, MockProviders } from 'ng-mocks'; +import { MockComponent } from 'ng-mocks'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { ActivatedRoute } from '@angular/router'; import { TitleAndAbstractStepComponent } from '@osf/features/preprints/components'; import { PreprintStepperSelectors } from '@osf/features/preprints/store/preprint-stepper'; -import { MOCK_STORE, TranslateServiceMock } from '@shared/mocks'; -import { ToastService } from '@shared/services'; +import { TextInputComponent } from '@shared/components'; + +import { PREPRINT_MOCK } from '@testing/mocks/preprint.mock'; +import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideMockStore } from '@testing/providers/store-provider.mock'; describe('TitleAndAbstractStepComponent', () => { let component: TitleAndAbstractStepComponent; let fixture: ComponentFixture; - beforeEach(async () => { - (MOCK_STORE.selectSignal as jest.Mock).mockImplementation((selector) => { - switch (selector) { - case PreprintStepperSelectors.getPreprint: - return () => null; - case PreprintStepperSelectors.getSelectedProviderId: - return () => '1'; - case PreprintStepperSelectors.isPreprintSubmitting: - return () => false; - default: - return () => null; - } - }); + const mockPreprint = PREPRINT_MOCK; + beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [TitleAndAbstractStepComponent, MockPipe(TranslatePipe)], - providers: [MockProviders(ActivatedRoute, ToastService), MockProvider(Store, MOCK_STORE), TranslateServiceMock], + imports: [TitleAndAbstractStepComponent, OSFTestingModule, MockComponent(TextInputComponent)], + providers: [ + provideMockStore({ + signals: [ + { + selector: PreprintStepperSelectors.getPreprint, + value: null, + }, + { + selector: PreprintStepperSelectors.getSelectedProviderId, + value: 'provider-1', + }, + { + selector: PreprintStepperSelectors.isPreprintSubmitting, + value: false, + }, + ], + }), + ], }).compileComponents(); fixture = TestBed.createComponent(TitleAndAbstractStepComponent); component = fixture.componentInstance; - fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); + + it('should initialize form with empty values', () => { + expect(component.titleAndAbstractForm.get('title')?.value).toBe(''); + expect(component.titleAndAbstractForm.get('description')?.value).toBe(''); + }); + + it('should have form invalid when fields are empty', () => { + expect(component.titleAndAbstractForm.invalid).toBe(true); + }); + + it('should have form valid when fields are filled correctly', () => { + component.titleAndAbstractForm.patchValue({ + title: 'Valid Title', + description: 'Valid description with sufficient length', + }); + expect(component.titleAndAbstractForm.valid).toBe(true); + }); + + it('should validate title max length', () => { + const longTitle = 'a'.repeat(513); + component.titleAndAbstractForm.patchValue({ + title: longTitle, + description: 'Valid description', + }); + expect(component.titleAndAbstractForm.get('title')?.hasError('maxlength')).toBe(true); + }); + + it('should validate description is required', () => { + component.titleAndAbstractForm.patchValue({ + title: 'Valid Title', + description: '', + }); + expect(component.titleAndAbstractForm.get('description')?.hasError('required')).toBe(true); + }); + + it('should not proceed when form is invalid', () => { + const nextClickedSpy = jest.spyOn(component.nextClicked, 'emit'); + component.nextButtonClicked(); + expect(nextClickedSpy).not.toHaveBeenCalled(); + }); + + it('should emit nextClicked when form is valid and no existing preprint', () => { + component.titleAndAbstractForm.patchValue({ + title: 'Valid Title', + description: 'Valid description with sufficient length', + }); + + const nextClickedSpy = jest.spyOn(component.nextClicked, 'emit'); + component.nextButtonClicked(); + expect(nextClickedSpy).toHaveBeenCalled(); + }); + + it('should initialize form with existing preprint data', () => { + component.titleAndAbstractForm.patchValue({ + title: mockPreprint.title, + description: mockPreprint.description, + }); + expect(component.titleAndAbstractForm.get('title')?.value).toBe(mockPreprint.title); + expect(component.titleAndAbstractForm.get('description')?.value).toBe(mockPreprint.description); + }); + + it('should emit nextClicked when form is valid and preprint exists', () => { + component.titleAndAbstractForm.patchValue({ + title: mockPreprint.title, + description: mockPreprint.description, + }); + const nextClickedSpy = jest.spyOn(component.nextClicked, 'emit'); + component.nextButtonClicked(); + expect(nextClickedSpy).toHaveBeenCalled(); + }); + + it('should emit nextClicked when form is valid and no existing preprint', () => { + component.titleAndAbstractForm.patchValue({ + title: 'Test Title', + description: 'Test description with sufficient length', + }); + + const nextClickedSpy = jest.spyOn(component.nextClicked, 'emit'); + component.nextButtonClicked(); + + expect(nextClickedSpy).toHaveBeenCalled(); + }); + + it('should emit nextClicked when form is valid and preprint exists', () => { + jest.spyOn(component, 'createdPreprint').mockReturnValue(mockPreprint); + + component.titleAndAbstractForm.patchValue({ + title: 'Updated Title', + description: 'Updated description with sufficient length', + }); + + const nextClickedSpy = jest.spyOn(component.nextClicked, 'emit'); + component.nextButtonClicked(); + + expect(nextClickedSpy).toHaveBeenCalled(); + }); + + it('should not emit nextClicked when form is invalid', () => { + const nextClickedSpy = jest.spyOn(component.nextClicked, 'emit'); + + component.nextButtonClicked(); + + expect(nextClickedSpy).not.toHaveBeenCalled(); + }); + + it('should have correct form validation for title and description', () => { + component.titleAndAbstractForm.patchValue({ + title: '', + description: 'Valid description', + }); + expect(component.titleAndAbstractForm.get('title')?.hasError('required')).toBe(true); + + component.titleAndAbstractForm.patchValue({ + title: 'Valid Title', + description: 'Short', + }); + expect(component.titleAndAbstractForm.get('description')?.hasError('minlength')).toBe(true); + + component.titleAndAbstractForm.patchValue({ + title: 'Valid Title', + description: 'Valid description with sufficient length', + }); + expect(component.titleAndAbstractForm.valid).toBe(true); + }); }); diff --git a/src/app/features/preprints/pages/create-new-version/create-new-version.component.spec.ts b/src/app/features/preprints/pages/create-new-version/create-new-version.component.spec.ts index 7a083f16f..16102a442 100644 --- a/src/app/features/preprints/pages/create-new-version/create-new-version.component.spec.ts +++ b/src/app/features/preprints/pages/create-new-version/create-new-version.component.spec.ts @@ -1,39 +1,88 @@ -import { Store } from '@ngxs/store'; +import { MockComponents, MockProvider } from 'ng-mocks'; -import { TranslatePipe } from '@ngx-translate/core'; -import { MockPipe, MockProvider, MockProviders } from 'ng-mocks'; +import { of } from 'rxjs'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ActivatedRoute, Router } from '@angular/router'; +import { FileStepComponent, ReviewStepComponent } from '@osf/features/preprints/components'; +import { createNewVersionStepsConst } from '@osf/features/preprints/constants'; +import { PreprintSteps } from '@osf/features/preprints/enums'; +import { PreprintProviderDetails } from '@osf/features/preprints/models'; +import { PreprintProvidersSelectors } from '@osf/features/preprints/store/preprint-providers'; import { PreprintStepperSelectors } from '@osf/features/preprints/store/preprint-stepper'; -import { MOCK_STORE, TranslateServiceMock } from '@shared/mocks'; -import { ToastService } from '@shared/services'; +import { StepperComponent } from '@shared/components'; +import { BrowserTabHelper, HeaderStyleHelper, IS_WEB } from '@shared/helpers'; +import { StepOption } from '@shared/models'; +import { BrandService } from '@shared/services'; import { CreateNewVersionComponent } from './create-new-version.component'; -describe.skip('CreateNewVersionComponent', () => { +import { PREPRINT_MOCK } from '@testing/mocks/preprint.mock'; +import { PREPRINT_PROVIDER_DETAILS_MOCK } from '@testing/mocks/preprint-provider-details'; +import { TranslationServiceMock } from '@testing/mocks/translation.service.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('CreateNewVersionComponent', () => { let component: CreateNewVersionComponent; let fixture: ComponentFixture; + let routerMock: ReturnType; + let routeMock: ReturnType; + + const mockProvider: PreprintProviderDetails = PREPRINT_PROVIDER_DETAILS_MOCK; + const mockPreprint = PREPRINT_MOCK; + const mockProviderId = 'osf'; + const mockPreprintId = 'test_preprint_123'; beforeEach(async () => { - (MOCK_STORE.selectSignal as jest.Mock).mockImplementation((selector) => { - switch (selector) { - case PreprintStepperSelectors.getPreprint: - return () => ({}); - case PreprintStepperSelectors.isPreprintSubmitting: - return () => false; - default: - return () => null; - } - }); + jest.spyOn(BrowserTabHelper, 'updateTabStyles').mockImplementation(() => {}); + jest.spyOn(BrowserTabHelper, 'resetToDefaults').mockImplementation(() => {}); + jest.spyOn(HeaderStyleHelper, 'applyHeaderStyles').mockImplementation(() => {}); + jest.spyOn(HeaderStyleHelper, 'resetToDefaults').mockImplementation(() => {}); + jest.spyOn(BrandService, 'applyBranding').mockImplementation(() => {}); + jest.spyOn(BrandService, 'resetBranding').mockImplementation(() => {}); + + routerMock = RouterMockBuilder.create().withNavigate(jest.fn().mockResolvedValue(true)).build(); + routeMock = ActivatedRouteMockBuilder.create() + .withParams({ providerId: mockProviderId, preprintId: mockPreprintId }) + .withQueryParams({}) + .build(); await TestBed.configureTestingModule({ - imports: [CreateNewVersionComponent, MockPipe(TranslatePipe)], + imports: [ + CreateNewVersionComponent, + OSFTestingModule, + ...MockComponents(StepperComponent, FileStepComponent, ReviewStepComponent), + ], providers: [ - MockProvider(Store, MOCK_STORE), - MockProviders(Router, ActivatedRoute, ToastService), - TranslateServiceMock, + TranslationServiceMock, + MockProvider(BrandService), + MockProvider(Router, routerMock), + MockProvider(ActivatedRoute, routeMock), + { provide: IS_WEB, useValue: of(true) }, + provideMockStore({ + signals: [ + { + selector: PreprintStepperSelectors.getPreprint, + value: mockPreprint, + }, + { + selector: PreprintProvidersSelectors.getPreprintProviderDetails(mockProviderId), + value: mockProvider, + }, + { + selector: PreprintProvidersSelectors.isPreprintProviderDetailsLoading, + value: false, + }, + { + selector: PreprintStepperSelectors.hasBeenSubmitted, + value: false, + }, + ], + }), ], }).compileComponents(); @@ -42,7 +91,112 @@ describe.skip('CreateNewVersionComponent', () => { fixture.detectChanges(); }); + afterEach(() => { + if (fixture) { + fixture.destroy(); + } + jest.restoreAllMocks(); + }); + it('should create', () => { expect(component).toBeTruthy(); }); + + it('should initialize with correct default values', () => { + expect(component.PreprintSteps).toBe(PreprintSteps); + expect(component.newVersionSteps).toBe(createNewVersionStepsConst); + expect(component.currentStep()).toEqual(createNewVersionStepsConst[0]); + expect(component.classes).toBe('flex-1 flex flex-column w-full'); + }); + + it('should return preprint from store', () => { + const preprint = component.preprint(); + expect(preprint).toBe(mockPreprint); + }); + + it('should return preprint provider from store', () => { + const provider = component.preprintProvider(); + expect(provider).toBe(mockProvider); + }); + + it('should return loading state from store', () => { + const loading = component.isPreprintProviderLoading(); + expect(loading).toBe(false); + }); + + it('should return submission state from store', () => { + const submitted = component.hasBeenSubmitted(); + expect(submitted).toBe(false); + }); + + it('should return web environment state', () => { + const isWeb = component.isWeb(); + expect(typeof isWeb).toBe('boolean'); + }); + + it('should initialize with first step as current step', () => { + expect(component.currentStep()).toEqual(createNewVersionStepsConst[0]); + }); + + it('should handle step change when moving to previous step', () => { + const previousStep = createNewVersionStepsConst[0]; + + component.stepChange(previousStep); + + expect(component.currentStep()).toEqual(previousStep); + }); + + it('should not change step when moving to next step', () => { + const currentStep = component.currentStep(); + const nextStep = createNewVersionStepsConst[1]; + + component.stepChange(nextStep); + + expect(component.currentStep()).toEqual(currentStep); + }); + + it('should move to next step', () => { + const currentIndex = component.currentStep()?.index ?? 0; + const nextStep = createNewVersionStepsConst[currentIndex + 1]; + + component.moveToNextStep(); + + expect(component.currentStep()).toEqual(nextStep); + }); + + it('should navigate to previous step (preprint page)', () => { + component.moveToPreviousStep(); + + expect(routerMock.navigate).toHaveBeenCalledWith([mockPreprintId.split('_')[0]]); + }); + + it('should return canDeactivate state', () => { + const canDeactivate = component.canDeactivate(); + expect(canDeactivate).toBe(false); + }); + + it('should handle beforeunload event', () => { + const event = { + preventDefault: jest.fn(), + } as unknown as BeforeUnloadEvent; + + const result = component.onBeforeUnload(event); + + expect(event.preventDefault).toHaveBeenCalled(); + expect(result).toBe(false); + }); + + it('should handle step navigation correctly', () => { + component.moveToNextStep(); + expect(component.currentStep()).toEqual(createNewVersionStepsConst[1]); + + component.stepChange(createNewVersionStepsConst[0]); + expect(component.currentStep()).toEqual(createNewVersionStepsConst[0]); + }); + + it('should handle edge case when moving to next step with undefined current step', () => { + component.currentStep.set({} as StepOption); + + expect(() => component.moveToNextStep()).not.toThrow(); + }); }); diff --git a/src/app/features/preprints/pages/landing/preprints-landing.component.spec.ts b/src/app/features/preprints/pages/landing/preprints-landing.component.spec.ts index ea8b6f6c7..8b8ff0118 100644 --- a/src/app/features/preprints/pages/landing/preprints-landing.component.spec.ts +++ b/src/app/features/preprints/pages/landing/preprints-landing.component.spec.ts @@ -1,47 +1,88 @@ -import { Store } from '@ngxs/store'; - -import { TranslatePipe } from '@ngx-translate/core'; -import { MockPipe, MockProvider } from 'ng-mocks'; +import { MockComponents, MockPipe, MockProvider } from 'ng-mocks'; +import { TitleCasePipe } from '@angular/common'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { ActivatedRoute, provideRouter } from '@angular/router'; +import { Router } from '@angular/router'; +import { ENVIRONMENT } from '@core/provider/environment.provider'; +import { + AdvisoryBoardComponent, + BrowseBySubjectsComponent, + PreprintServicesComponent, +} from '@osf/features/preprints/components'; +import { PreprintProviderDetails } from '@osf/features/preprints/models'; import { PreprintProvidersSelectors } from '@osf/features/preprints/store/preprint-providers'; -import { MOCK_PROVIDER, MOCK_STORE, TranslateServiceMock } from '@shared/mocks'; +import { SearchInputComponent } from '@shared/components'; +import { ResourceType } from '@shared/enums'; import { BrandService } from '@shared/services'; import { PreprintsLandingComponent } from './preprints-landing.component'; +import { PREPRINT_PROVIDER_DETAILS_MOCK } from '@testing/mocks/preprint-provider-details'; +import { PREPRINT_PROVIDER_SHORT_INFO_MOCK } from '@testing/mocks/preprint-provider-short-info.mock'; +import { SUBJECTS_MOCK } from '@testing/mocks/subject.mock'; +import { TranslationServiceMock } from '@testing/mocks/translation.service.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('PreprintsLandingComponent', () => { let component: PreprintsLandingComponent; let fixture: ComponentFixture; + let routerMock: ReturnType; + + const mockProvider: PreprintProviderDetails = PREPRINT_PROVIDER_DETAILS_MOCK; + const mockProvidersToAdvertise = [PREPRINT_PROVIDER_SHORT_INFO_MOCK]; + const mockHighlightedSubjects = SUBJECTS_MOCK; + const mockDefaultProvider = 'osf'; beforeEach(async () => { - (MOCK_STORE.selectSignal as jest.Mock).mockImplementation((selector) => { - switch (selector) { - case PreprintProvidersSelectors.getPreprintProviderDetails('osf'): - return () => MOCK_PROVIDER; - case PreprintProvidersSelectors.isPreprintProviderDetailsLoading: - return () => false; - case PreprintProvidersSelectors.getPreprintProvidersToAdvertise: - return () => []; - case PreprintProvidersSelectors.getHighlightedSubjectsForProvider: - return () => []; - case PreprintProvidersSelectors.areSubjectsLoading: - return () => false; - default: - return () => []; - } - }); + routerMock = RouterMockBuilder.create().withNavigate(jest.fn().mockResolvedValue(true)).build(); await TestBed.configureTestingModule({ - imports: [PreprintsLandingComponent, MockPipe(TranslatePipe)], + imports: [ + PreprintsLandingComponent, + OSFTestingModule, + ...MockComponents( + SearchInputComponent, + AdvisoryBoardComponent, + PreprintServicesComponent, + BrowseBySubjectsComponent + ), + MockPipe(TitleCasePipe), + ], providers: [ - MockProvider(Store, MOCK_STORE), - provideRouter([]), - MockProvider(ActivatedRoute, {}), + TranslationServiceMock, + MockProvider(ENVIRONMENT, { + defaultProvider: mockDefaultProvider, + supportEmail: 'support@osf.io', + }), MockProvider(BrandService), - TranslateServiceMock, + MockProvider(Router, routerMock), + provideMockStore({ + signals: [ + { + selector: PreprintProvidersSelectors.getPreprintProviderDetails(mockDefaultProvider), + value: mockProvider, + }, + { + selector: PreprintProvidersSelectors.isPreprintProviderDetailsLoading, + value: false, + }, + { + selector: PreprintProvidersSelectors.getPreprintProvidersToAdvertise, + value: mockProvidersToAdvertise, + }, + { + selector: PreprintProvidersSelectors.getHighlightedSubjectsForProvider, + value: mockHighlightedSubjects, + }, + { + selector: PreprintProvidersSelectors.areSubjectsLoading, + value: false, + }, + ], + }), ], }).compileComponents(); @@ -53,4 +94,84 @@ describe('PreprintsLandingComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + it('should initialize with correct default values', () => { + expect(component.searchControl.value).toBe(''); + expect(component.supportEmail).toBeDefined(); + }); + + it('should return preprint provider from store', () => { + const provider = component.osfPreprintProvider(); + expect(provider).toBe(mockProvider); + }); + + it('should return loading state from store', () => { + const loading = component.isPreprintProviderLoading(); + expect(loading).toBe(false); + }); + + it('should return providers to advertise from store', () => { + const providers = component.preprintProvidersToAdvertise(); + expect(providers).toBe(mockProvidersToAdvertise); + }); + + it('should return highlighted subjects from store', () => { + const subjects = component.highlightedSubjectsByProviderId(); + expect(subjects).toBe(mockHighlightedSubjects); + }); + + it('should return subjects loading state from store', () => { + const loading = component.areSubjectsLoading(); + expect(loading).toBe(false); + }); + + it('should have correct CSS classes', () => { + expect(component.classes).toBe('flex-1 flex flex-column w-full h-full'); + }); + + it('should navigate to search page with search value', () => { + component.searchControl.setValue('test search'); + + component.redirectToSearchPageWithValue(); + + expect(routerMock.navigate).toHaveBeenCalledWith(['/search'], { + queryParams: { search: 'test search', resourceTab: ResourceType.Preprint }, + }); + }); + + it('should navigate to search page with empty search value', () => { + component.searchControl.setValue(''); + + component.redirectToSearchPageWithValue(); + + expect(routerMock.navigate).toHaveBeenCalledWith(['/search'], { + queryParams: { search: '', resourceTab: ResourceType.Preprint }, + }); + }); + + it('should navigate to search page with null search value', () => { + component.searchControl.setValue(null); + + component.redirectToSearchPageWithValue(); + + expect(routerMock.navigate).toHaveBeenCalledWith(['/search'], { + queryParams: { search: null, resourceTab: ResourceType.Preprint }, + }); + }); + + it('should handle search control value changes', () => { + const testValue = 'new search term'; + component.searchControl.setValue(testValue); + expect(component.searchControl.value).toBe(testValue); + }); + + it('should have readonly properties', () => { + expect(component.supportEmail).toBeDefined(); + expect(typeof component.supportEmail).toBe('string'); + }); + + it('should initialize form control correctly', () => { + expect(component.searchControl).toBeDefined(); + expect(component.searchControl.value).toBe(''); + }); }); diff --git a/src/app/features/preprints/pages/my-preprints/my-preprints.component.spec.ts b/src/app/features/preprints/pages/my-preprints/my-preprints.component.spec.ts index 343a56bda..6bad5d023 100644 --- a/src/app/features/preprints/pages/my-preprints/my-preprints.component.spec.ts +++ b/src/app/features/preprints/pages/my-preprints/my-preprints.component.spec.ts @@ -1,52 +1,79 @@ -import { Store } from '@ngxs/store'; - -import { TranslatePipe } from '@ngx-translate/core'; -import { MockPipe, MockProvider } from 'ng-mocks'; +import { MockComponents, MockPipe, MockProvider } from 'ng-mocks'; import { BehaviorSubject } from 'rxjs'; +import { TitleCasePipe } from '@angular/common'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { ActivatedRoute, provideRouter, Router } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; +import { PreprintShortInfo } from '@osf/features/preprints/models'; import { PreprintSelectors } from '@osf/features/preprints/store/preprint'; -import { MOCK_STORE, TranslateServiceMock } from '@shared/mocks'; +import { ListInfoShortenerComponent, SearchInputComponent, SubHeaderComponent } from '@shared/components'; +import { DEFAULT_TABLE_PARAMS } from '@shared/constants'; +import { SortOrder } from '@shared/enums'; import { MyPreprintsComponent } from './my-preprints.component'; +import { PREPRINT_SHORT_INFO_ARRAY_MOCK } from '@testing/mocks/preprint-short-info.mock'; +import { TranslationServiceMock } from '@testing/mocks/translation.service.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('MyPreprintsComponent', () => { let component: MyPreprintsComponent; let fixture: ComponentFixture; + let routerMock: ReturnType; + let activatedRouteMock: ReturnType; let queryParamsSubject: BehaviorSubject>; - const mockRouter: Partial = { - navigateByUrl: jest.fn(), - navigate: jest.fn(), - }; + + const mockPreprints: PreprintShortInfo[] = PREPRINT_SHORT_INFO_ARRAY_MOCK; beforeEach(async () => { queryParamsSubject = new BehaviorSubject>({}); - (MOCK_STORE.selectSignal as jest.Mock).mockImplementation((selector) => { - if (selector === PreprintSelectors.getMyPreprints) return () => []; - if (selector === PreprintSelectors.getMyPreprintsTotalCount) return () => 0; - if (selector === PreprintSelectors.areMyPreprintsLoading) return () => false; - return () => null; - }); + routerMock = RouterMockBuilder.create() + .withNavigate(jest.fn().mockResolvedValue(true)) + .withNavigateByUrl(jest.fn().mockResolvedValue(true)) + .build(); - const mockActivatedRoute = { - queryParams: queryParamsSubject.asObservable(), - snapshot: { - queryParams: {}, - }, - } as Partial; + activatedRouteMock = ActivatedRouteMockBuilder.create() + .withQueryParams({ page: '1', size: '10', search: '' }) + .build(); + + Object.defineProperty(activatedRouteMock, 'queryParams', { + value: queryParamsSubject.asObservable(), + writable: true, + }); await TestBed.configureTestingModule({ - imports: [MyPreprintsComponent, MockPipe(TranslatePipe)], + imports: [ + MyPreprintsComponent, + OSFTestingModule, + ...MockComponents(SubHeaderComponent, SearchInputComponent, ListInfoShortenerComponent), + MockPipe(TitleCasePipe), + ], providers: [ - provideRouter([]), - MockProvider(ActivatedRoute, mockActivatedRoute), - MockProvider(Router, mockRouter), - MockProvider(Store, MOCK_STORE), - TranslateServiceMock, + TranslationServiceMock, + MockProvider(Router, routerMock), + MockProvider(ActivatedRoute, activatedRouteMock), + provideMockStore({ + signals: [ + { + selector: PreprintSelectors.getMyPreprints, + value: mockPreprints, + }, + { + selector: PreprintSelectors.getMyPreprintsTotalCount, + value: 5, + }, + { + selector: PreprintSelectors.areMyPreprintsLoading, + value: false, + }, + ], + }), ], }).compileComponents(); @@ -58,4 +85,183 @@ describe('MyPreprintsComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + it('should initialize with correct default values', () => { + expect(component.searchControl.value).toBe(''); + expect(component.sortColumn()).toBe(''); + expect(component.sortOrder()).toBe(SortOrder.Asc); + expect(component.currentPage()).toBe(1); + expect(component.currentPageSize()).toBe(DEFAULT_TABLE_PARAMS.rows); + }); + + it('should return preprints from store', () => { + const preprints = component.preprints(); + expect(preprints).toBe(mockPreprints); + }); + + it('should return preprints total count from store', () => { + const totalCount = component.preprintsTotalCount(); + expect(totalCount).toBe(5); + }); + + it('should return loading state from store', () => { + const loading = component.areMyPreprintsLoading(); + expect(loading).toBe(false); + }); + + it('should have correct CSS classes', () => { + expect(component.classes).toBe('flex-1 flex flex-column w-full'); + }); + + it('should have skeleton data with correct length', () => { + expect(component.skeletonData).toHaveLength(10); + expect(component.skeletonData.every((item) => typeof item === 'object')).toBe(true); + }); + + it('should navigate to preprint details when navigateToPreprintDetails is called', () => { + const mockPreprint: PreprintShortInfo = { + id: 'preprint-1', + title: 'Test Preprint', + dateModified: '2024-01-01T00:00:00Z', + contributors: [], + providerId: 'provider-1', + }; + + component.navigateToPreprintDetails(mockPreprint); + + expect(routerMock.navigateByUrl).toHaveBeenCalledWith('/preprints/provider-1/preprint-1'); + }); + + it('should handle page change correctly', () => { + const mockEvent = { + first: 20, + rows: 10, + }; + + component.onPageChange(mockEvent); + + expect(routerMock.navigate).toHaveBeenCalledWith([], { + relativeTo: expect.any(Object), + queryParams: { page: '3', size: '10' }, + queryParamsHandling: 'merge', + }); + }); + + it('should handle sort correctly for ascending order', () => { + const mockEvent = { + field: 'title', + order: 1, + }; + + component.onSort(mockEvent); + + expect(routerMock.navigate).toHaveBeenCalledWith([], { + relativeTo: expect.any(Object), + queryParams: { sortColumn: 'title', sortOrder: 'asc' }, + queryParamsHandling: 'merge', + }); + }); + + it('should handle sort correctly for descending order', () => { + const mockEvent = { + field: 'title', + order: -1, + }; + + component.onSort(mockEvent); + + expect(routerMock.navigate).toHaveBeenCalledWith([], { + relativeTo: expect.any(Object), + queryParams: { sortColumn: 'title', sortOrder: 'desc' }, + queryParamsHandling: 'merge', + }); + }); + + it('should not navigate when sort field is undefined', () => { + const mockEvent = { + field: undefined, + order: 1, + }; + + component.onSort(mockEvent); + + expect(routerMock.navigate).not.toHaveBeenCalled(); + }); + + it('should navigate to add preprint page when addPreprintBtnClicked is called', () => { + component.addPreprintBtnClicked(); + + expect(routerMock.navigateByUrl).toHaveBeenCalledWith('/preprints/select'); + }); + + it('should handle search control value changes', () => { + const testValue = 'test search'; + component.searchControl.setValue(testValue); + expect(component.searchControl.value).toBe(testValue); + }); + + it('should update component state when query params change', () => { + queryParamsSubject.next({ page: '2', size: '20', search: 'test' }); + + fixture.detectChanges(); + + expect(component.currentPage()).toBe(2); + expect(component.currentPageSize()).toBe(20); + expect(component.searchControl.value).toBe('test'); + }); + + it('should initialize form control correctly', () => { + expect(component.searchControl).toBeDefined(); + expect(component.searchControl.value).toBe(''); + }); + + it('should have correct table parameters', () => { + const tableParams = component.tableParams(); + expect(tableParams).toEqual({ + ...DEFAULT_TABLE_PARAMS, + firstRowIndex: 0, + totalRecords: 5, + }); + }); + + it('should update table parameters when total records change', () => { + const newTableParams = { totalRecords: 100 }; + component['updateTableParams'](newTableParams); + + const updatedParams = component.tableParams(); + expect(updatedParams.totalRecords).toBe(100); + }); + + it('should create filters correctly', () => { + const mockParams = { + page: 1, + size: 10, + search: 'test search', + sortColumn: 'title', + sortOrder: SortOrder.Desc, + }; + + const filters = component['createFilters'](mockParams); + + expect(filters).toEqual({ + searchValue: 'test search', + searchFields: ['title', 'tags', 'description'], + sortColumn: 'title', + sortOrder: SortOrder.Desc, + }); + }); + + it('should handle empty search value in filters', () => { + const mockParams = { + page: 1, + size: 10, + search: '', + sortColumn: 'title', + sortOrder: SortOrder.Asc, + }; + + const filters = component['createFilters'](mockParams); + + expect(filters.searchValue).toBe(''); + }); }); diff --git a/src/app/features/preprints/pages/preprint-details/preprint-details.component.spec.ts b/src/app/features/preprints/pages/preprint-details/preprint-details.component.spec.ts index e94ed966e..159b41479 100644 --- a/src/app/features/preprints/pages/preprint-details/preprint-details.component.spec.ts +++ b/src/app/features/preprints/pages/preprint-details/preprint-details.component.spec.ts @@ -1,110 +1,369 @@ -import { Store } from '@ngxs/store'; - -import { TranslatePipe } from '@ngx-translate/core'; -import { MockComponents, MockPipe, MockProvider } from 'ng-mocks'; +import { MockComponents, MockProvider } from 'ng-mocks'; import { of } from 'rxjs'; -import { signal } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { provideNoopAnimations } from '@angular/platform-browser/animations'; import { ActivatedRoute, Router } from '@angular/router'; -import { HelpScoutService } from '@core/services/help-scout.service'; +import { UserSelectors } from '@core/store/user'; +import { + MakeDecisionComponent, + ModerationStatusBannerComponent, + PreprintTombstoneComponent, + StatusBannerComponent, +} from '@osf/features/preprints/components'; import { AdditionalInfoComponent } from '@osf/features/preprints/components/preprint-details/additional-info/additional-info.component'; import { GeneralInformationComponent } from '@osf/features/preprints/components/preprint-details/general-information/general-information.component'; import { PreprintFileSectionComponent } from '@osf/features/preprints/components/preprint-details/preprint-file-section/preprint-file-section.component'; +import { PreprintWarningBannerComponent } from '@osf/features/preprints/components/preprint-details/preprint-warning-banner/preprint-warning-banner.component'; import { ShareAndDownloadComponent } from '@osf/features/preprints/components/preprint-details/share-and-downlaod/share-and-download.component'; +import { ReviewsState } from '@osf/features/preprints/enums'; import { PreprintSelectors } from '@osf/features/preprints/store/preprint'; import { PreprintProvidersSelectors } from '@osf/features/preprints/store/preprint-providers'; -import { MOCK_PROVIDER, MOCK_STORE, TranslateServiceMock } from '@shared/mocks'; +import { UserPermissions } from '@shared/enums'; +import { MOCK_CONTRIBUTOR, MOCK_USER } from '@shared/mocks'; import { MetaTagsService } from '@shared/services'; import { DataciteService } from '@shared/services/datacite/datacite.service'; +import { ContributorsSelectors } from '@shared/stores'; import { PreprintDetailsComponent } from './preprint-details.component'; -import { DataciteMockFactory } from '@testing/mocks/datacite.service.mock'; +import { PREPRINT_MOCK } from '@testing/mocks/preprint.mock'; +import { PREPRINT_PROVIDER_DETAILS_MOCK } from '@testing/mocks/preprint-provider-details'; +import { PREPRINT_REQUEST_MOCK } from '@testing/mocks/preprint-request.mock'; +import { REVIEW_ACTION_MOCK } from '@testing/mocks/review-action.mock'; +import { TranslationServiceMock } from '@testing/mocks/translation.service.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('Component: Preprint Details', () => { +describe('PreprintDetailsComponent', () => { let component: PreprintDetailsComponent; let fixture: ComponentFixture; - let helpScountService: HelpScoutService; - + let routerMock: ReturnType; + let activatedRouteMock: ReturnType; let dataciteService: jest.Mocked; + let metaTagsService: jest.Mocked; - const preprintSignal = signal({ id: 'p1', title: 'Test', description: '' }); - const mockRoute: Partial = { - params: of({ providerId: 'osf', preprintId: 'p1' }), - queryParams: of({ providerId: 'osf', preprintId: 'p1' }), - }; + const mockPreprint = PREPRINT_MOCK; + const mockProvider = PREPRINT_PROVIDER_DETAILS_MOCK; + const mockReviewActions = [REVIEW_ACTION_MOCK]; + const mockWithdrawalRequests = [PREPRINT_REQUEST_MOCK]; + const mockRequestActions = [REVIEW_ACTION_MOCK]; + const mockContributors = [MOCK_CONTRIBUTOR]; + const mockCurrentUser = MOCK_USER; beforeEach(async () => { - (MOCK_STORE.selectSignal as jest.Mock).mockImplementation((selector) => { - switch (selector) { - case PreprintProvidersSelectors.getPreprintProviderDetails('osf'): - return () => MOCK_PROVIDER; - case PreprintProvidersSelectors.isPreprintProviderDetailsLoading: - return () => false; - case PreprintSelectors.getPreprint: - return preprintSignal; - case PreprintSelectors.isPreprintLoading: - return () => false; - default: - return () => []; - } - }); - (MOCK_STORE.dispatch as jest.Mock).mockImplementation(() => of()); - dataciteService = DataciteMockFactory(); + routerMock = RouterMockBuilder.create() + .withNavigate(jest.fn().mockResolvedValue(true)) + .withNavigateByUrl(jest.fn().mockResolvedValue(true)) + .build(); + + activatedRouteMock = ActivatedRouteMockBuilder.create() + .withParams({ providerId: 'osf', id: 'preprint-1' }) + .withQueryParams({ mode: 'moderator' }) + .build(); + + dataciteService = { + logIdentifiableView: jest.fn().mockReturnValue(of(void 0)), + logIdentifiableDownload: jest.fn().mockReturnValue(of(void 0)), + } as any; + + metaTagsService = { + updateMetaTags: jest.fn(), + } as any; await TestBed.configureTestingModule({ imports: [ PreprintDetailsComponent, - MockPipe(TranslatePipe), + OSFTestingModule, ...MockComponents( PreprintFileSectionComponent, ShareAndDownloadComponent, GeneralInformationComponent, - AdditionalInfoComponent + AdditionalInfoComponent, + StatusBannerComponent, + PreprintTombstoneComponent, + PreprintWarningBannerComponent, + ModerationStatusBannerComponent, + MakeDecisionComponent ), ], providers: [ - MockProvider(Store, MOCK_STORE), - provideNoopAnimations(), - { provide: DataciteService, useValue: dataciteService }, - MockProvider(Router), - MockProvider(ActivatedRoute, mockRoute), - TranslateServiceMock, - MockProvider(MetaTagsService), - { - provide: HelpScoutService, - useValue: { - setResourceType: jest.fn(), - unsetResourceType: jest.fn(), - }, - }, + TranslationServiceMock, + MockProvider(Router, routerMock), + MockProvider(ActivatedRoute, activatedRouteMock), + MockProvider(DataciteService, dataciteService), + MockProvider(MetaTagsService, metaTagsService), + provideMockStore({ + signals: [ + { + selector: UserSelectors.getCurrentUser, + value: mockCurrentUser, + }, + { + selector: PreprintProvidersSelectors.getPreprintProviderDetails('osf'), + value: mockProvider, + }, + { + selector: PreprintProvidersSelectors.isPreprintProviderDetailsLoading, + value: false, + }, + { + selector: PreprintSelectors.getPreprint, + value: mockPreprint, + }, + { + selector: PreprintSelectors.isPreprintLoading, + value: false, + }, + { + selector: ContributorsSelectors.getContributors, + value: mockContributors, + }, + { + selector: ContributorsSelectors.isContributorsLoading, + value: false, + }, + { + selector: PreprintSelectors.getPreprintReviewActions, + value: mockReviewActions, + }, + { + selector: PreprintSelectors.arePreprintReviewActionsLoading, + value: false, + }, + { + selector: PreprintSelectors.getPreprintRequests, + value: mockWithdrawalRequests, + }, + { + selector: PreprintSelectors.arePreprintRequestsLoading, + value: false, + }, + { + selector: PreprintSelectors.getPreprintRequestActions, + value: mockRequestActions, + }, + { + selector: PreprintSelectors.arePreprintRequestActionsLoading, + value: false, + }, + ], + }), ], }).compileComponents(); - helpScountService = TestBed.inject(HelpScoutService); fixture = TestBed.createComponent(PreprintDetailsComponent); component = fixture.componentInstance; fixture.detectChanges(); }); - it('should have a default value', () => { - expect(component.classes).toBe(''); + it('should create', () => { + expect(component).toBeTruthy(); }); - it('should called the helpScoutService', () => { - expect(helpScountService.setResourceType).toHaveBeenCalledWith('preprint'); + it('should initialize with correct default values', () => { + expect(component.classes).toBe('flex-1 flex flex-column w-full'); }); - it('isOsfPreprint should be true if providerId === osf', () => { - expect(component.isOsfPreprint()).toBeTruthy(); + it('should return preprint from store', () => { + const preprint = component.preprint(); + expect(preprint).toBe(mockPreprint); }); - it('reacts to sequence of state changes', () => { - fixture.detectChanges(); + it('should return preprint provider from store', () => { + const provider = component.preprintProvider(); + expect(provider).toBe(mockProvider); + }); + + it('should return loading states from store', () => { + expect(component.isPreprintLoading()).toBe(false); + expect(component.isPreprintProviderLoading()).toBe(false); + expect(component.areReviewActionsLoading()).toBe(false); + expect(component.areWithdrawalRequestsLoading()).toBe(false); + expect(component.areRequestActionsLoading()).toBe(false); + }); + + it('should return review actions from store', () => { + const actions = component.reviewActions(); + expect(actions).toBe(mockReviewActions); + }); + + it('should return withdrawal requests from store', () => { + const requests = component.withdrawalRequests(); + expect(requests).toBe(mockWithdrawalRequests); + }); + + it('should return request actions from store', () => { + const actions = component.requestActions(); + expect(actions).toBe(mockRequestActions); + }); + + it('should return contributors from store', () => { + const contributors = component.contributors(); + expect(contributors).toBe(mockContributors); + }); + + it('should return current user from store', () => { + const currentUser = component.currentUser(); + expect(currentUser).toBe(mockCurrentUser); + }); + + it('should return contributors loading state from store', () => { + const loading = component.areContributorsLoading(); + expect(loading).toBe(false); + }); + + it('should compute latest action correctly', () => { + const latestAction = component.latestAction(); + expect(latestAction).toBe(mockReviewActions[0]); + }); + + it('should compute latest withdrawal request correctly', () => { + const latestRequest = component.latestWithdrawalRequest(); + expect(latestRequest).toBe(mockWithdrawalRequests[0]); + }); + + it('should compute latest request action correctly', () => { + const latestAction = component.latestRequestAction(); + expect(latestAction).toBe(mockRequestActions[0]); + }); + + it('should compute isOsfPreprint correctly', () => { + const isOsf = component.isOsfPreprint(); + expect(isOsf).toBe(true); + }); + + it('should compute moderation mode correctly', () => { + const moderationMode = component.moderationMode(); + expect(moderationMode).toBe(true); + }); + + it('should compute create new version button visibility', () => { + const visible = component.createNewVersionButtonVisible(); + expect(typeof visible).toBe('boolean'); + }); + + it('should compute edit button visibility', () => { + const visible = component.editButtonVisible(); + expect(typeof visible).toBe('boolean'); + }); + + it('should compute edit button label', () => { + const label = component.editButtonLabel(); + expect(typeof label).toBe('string'); + }); + + it('should compute withdrawal button visibility', () => { + const visible = component.withdrawalButtonVisible(); + expect(typeof visible).toBe('boolean'); + }); + + it('should compute is pending withdrawal', () => { + const pending = component.isPendingWithdrawal(); + expect(typeof pending).toBe('boolean'); + }); + + it('should compute is withdrawal rejected', () => { + const rejected = component.isWithdrawalRejected(); + expect(typeof rejected).toBe('boolean'); + }); + + it('should compute moderation status banner visibility', () => { + const visible = component.moderationStatusBannerVisible(); + expect(typeof visible).toBe('boolean'); + }); + + it('should compute status banner visibility', () => { + const visible = component.statusBannerVisible(); + expect(typeof visible).toBe('boolean'); + }); + + it('should navigate to edit page when editPreprintClicked is called', () => { + component.editPreprintClicked(); + + expect(routerMock.navigate).toHaveBeenCalledWith(['preprints', 'osf', 'edit', 'preprint-1']); + }); + + it('should handle withdraw clicked', () => { + expect(() => component.handleWithdrawClicked()).not.toThrow(); + }); + + it('should handle create new version clicked', () => { + expect(() => component.createNewVersionClicked()).not.toThrow(); + }); + + it('should handle fetch preprint version', () => { + const newVersionId = 'preprint-2'; + expect(() => component.fetchPreprintVersion(newVersionId)).not.toThrow(); + }); + + it('should have correct CSS classes', () => { + expect(component.classes).toBe('flex-1 flex flex-column w-full'); + }); + + it('should call dataciteService.logIdentifiableView on init', () => { expect(dataciteService.logIdentifiableView).toHaveBeenCalledWith(component.preprint$); }); + + it('should handle preprint with different states', () => { + const acceptedPreprint = { ...mockPreprint, reviewsState: ReviewsState.Accepted }; + jest.spyOn(component, 'preprint').mockReturnValue(acceptedPreprint); + + const withdrawable = component['preprintWithdrawableState'](); + expect(typeof withdrawable).toBe('boolean'); + }); + + it('should handle preprint with pending state', () => { + const pendingPreprint = { ...mockPreprint, reviewsState: ReviewsState.Pending }; + jest.spyOn(component, 'preprint').mockReturnValue(pendingPreprint); + + const withdrawable = component['preprintWithdrawableState'](); + expect(withdrawable).toBe(true); + }); + + it('should handle preprint with accepted state', () => { + const acceptedPreprint = { ...mockPreprint, reviewsState: ReviewsState.Accepted }; + jest.spyOn(component, 'preprint').mockReturnValue(acceptedPreprint); + + const withdrawable = component['preprintWithdrawableState'](); + expect(withdrawable).toBe(true); + }); + + it('should handle preprint with pending state', () => { + const pendingPreprint = { ...mockPreprint, reviewsState: ReviewsState.Pending }; + jest.spyOn(component, 'preprint').mockReturnValue(pendingPreprint); + + const withdrawable = component['preprintWithdrawableState'](); + expect(withdrawable).toBe(true); + }); + + it('should handle hasReadWriteAccess correctly', () => { + const hasAccess = component['hasReadWriteAccess'](); + expect(typeof hasAccess).toBe('boolean'); + }); + + it('should handle preprint with write permissions', () => { + const preprintWithWrite = { + ...mockPreprint, + currentUserPermissions: [UserPermissions.Write], + }; + jest.spyOn(component, 'preprint').mockReturnValue(preprintWithWrite); + + const hasAccess = component['hasReadWriteAccess'](); + expect(hasAccess).toBe(true); + }); + + it('should handle preprint without write permissions', () => { + const preprintWithoutWrite = { + ...mockPreprint, + currentUserPermissions: [UserPermissions.Read], + }; + jest.spyOn(component, 'preprint').mockReturnValue(preprintWithoutWrite); + + const hasAccess = component['hasReadWriteAccess'](); + expect(hasAccess).toBe(false); + }); }); diff --git a/src/app/features/preprints/pages/preprint-provider-discover/preprint-provider-discover.component.spec.ts b/src/app/features/preprints/pages/preprint-provider-discover/preprint-provider-discover.component.spec.ts index 44d3862e4..db019900f 100644 --- a/src/app/features/preprints/pages/preprint-provider-discover/preprint-provider-discover.component.spec.ts +++ b/src/app/features/preprints/pages/preprint-provider-discover/preprint-provider-discover.component.spec.ts @@ -1,42 +1,64 @@ -import { Store } from '@ngxs/store'; - -import { TranslatePipe } from '@ngx-translate/core'; -import { MockPipe, MockProvider } from 'ng-mocks'; +import { MockComponents, MockProvider } from 'ng-mocks'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { ActivatedRoute, provideRouter } from '@angular/router'; +import { FormControl } from '@angular/forms'; +import { ActivatedRoute } from '@angular/router'; -import { PreprintProviderDiscoverComponent } from '@osf/features/preprints/pages'; +import { PreprintProviderHeroComponent } from '@osf/features/preprints/components'; +import { PreprintProviderDetails } from '@osf/features/preprints/models'; import { PreprintProvidersSelectors } from '@osf/features/preprints/store/preprint-providers'; -import { MOCK_PROVIDER, MOCK_STORE, TranslateServiceMock } from '@shared/mocks'; +import { GlobalSearchComponent } from '@shared/components'; +import { BrowserTabHelper, HeaderStyleHelper } from '@shared/helpers'; +import { BrandService } from '@shared/services'; + +import { PreprintProviderDiscoverComponent } from './preprint-provider-discover.component'; -describe.skip('PreprintProviderDiscoverComponent', () => { +import { PREPRINT_PROVIDER_DETAILS_MOCK } from '@testing/mocks/preprint-provider-details'; +import { OSFTestingModule } from '@testing/osf.testing.module'; +import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock'; +import { provideMockStore } from '@testing/providers/store-provider.mock'; + +describe('PreprintProviderDiscoverComponent', () => { let component: PreprintProviderDiscoverComponent; let fixture: ComponentFixture; + let routeMock: ReturnType; + + const mockProvider: PreprintProviderDetails = PREPRINT_PROVIDER_DETAILS_MOCK; + const mockProviderId = 'osf'; beforeEach(async () => { - (MOCK_STORE.selectSignal as jest.Mock).mockImplementation((selector) => { - switch (selector) { - case PreprintProvidersSelectors.getPreprintProviderDetails('prov1'): - return () => MOCK_PROVIDER; - case PreprintProvidersSelectors.isPreprintProviderDetailsLoading: - return () => false; - case PreprintProvidersSelectors.getHighlightedSubjectsForProvider: - return () => []; - case PreprintProvidersSelectors.areSubjectsLoading: - return () => false; - default: - return () => []; - } - }); + jest.spyOn(BrowserTabHelper, 'updateTabStyles').mockImplementation(() => {}); + jest.spyOn(BrowserTabHelper, 'resetToDefaults').mockImplementation(() => {}); + jest.spyOn(HeaderStyleHelper, 'applyHeaderStyles').mockImplementation(() => {}); + jest.spyOn(HeaderStyleHelper, 'resetToDefaults').mockImplementation(() => {}); + jest.spyOn(BrandService, 'applyBranding').mockImplementation(() => {}); + jest.spyOn(BrandService, 'resetBranding').mockImplementation(() => {}); + + routeMock = ActivatedRouteMockBuilder.create() + .withParams({ providerId: mockProviderId }) + .withQueryParams({}) + .build(); await TestBed.configureTestingModule({ - imports: [PreprintProviderDiscoverComponent, MockPipe(TranslatePipe)], + imports: [ + PreprintProviderDiscoverComponent, + OSFTestingModule, + ...MockComponents(PreprintProviderHeroComponent, GlobalSearchComponent), + ], providers: [ - MockProvider(Store, MOCK_STORE), - provideRouter([]), - MockProvider(ActivatedRoute), - TranslateServiceMock, + MockProvider(ActivatedRoute, routeMock), + provideMockStore({ + signals: [ + { + selector: PreprintProvidersSelectors.getPreprintProviderDetails(mockProviderId), + value: mockProvider, + }, + { + selector: PreprintProvidersSelectors.isPreprintProviderDetailsLoading, + value: false, + }, + ], + }), ], }).compileComponents(); @@ -48,4 +70,72 @@ describe.skip('PreprintProviderDiscoverComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + it('should initialize with correct default values', () => { + expect(component.providerId).toBe(mockProviderId); + expect(component.classes).toBe('flex-1 flex flex-column w-full h-full'); + expect(component.searchControl).toBeDefined(); + expect(component.searchControl.value).toBe(''); + }); + + it('should return preprint provider from store', () => { + const provider = component.preprintProvider(); + expect(provider).toBe(mockProvider); + }); + + it('should return loading state from store', () => { + const loading = component.isPreprintProviderLoading(); + expect(loading).toBe(false); + }); + + it('should initialize search control correctly', () => { + expect(component.searchControl).toBeDefined(); + expect(component.searchControl.value).toBe(''); + }); + + it('should handle search control value changes', () => { + const testValue = 'test search'; + component.searchControl.setValue(testValue); + expect(component.searchControl.value).toBe(testValue); + }); + + it('should initialize signals correctly', () => { + expect(component.preprintProvider).toBeDefined(); + expect(component.isPreprintProviderLoading).toBeDefined(); + }); + + it('should handle provider data correctly', () => { + const provider = component.preprintProvider(); + expect(provider).toBe(mockProvider); + expect(provider?.id).toBe(mockProvider.id); + expect(provider?.name).toBe(mockProvider.name); + }); + + it('should handle loading state correctly', () => { + const loading = component.isPreprintProviderLoading(); + expect(typeof loading).toBe('boolean'); + expect(loading).toBe(false); + }); + + it('should handle search control initialization', () => { + expect(component.searchControl).toBeInstanceOf(FormControl); + expect(component.searchControl.value).toBe(''); + }); + + it('should handle search control updates', () => { + const newValue = 'new search term'; + component.searchControl.setValue(newValue); + expect(component.searchControl.value).toBe(newValue); + }); + + it('should handle search control reset', () => { + component.searchControl.setValue('some value'); + component.searchControl.setValue(''); + expect(component.searchControl.value).toBe(''); + }); + + it('should handle search control with null value', () => { + component.searchControl.setValue(null); + expect(component.searchControl.value).toBe(null); + }); }); diff --git a/src/app/features/preprints/pages/preprint-provider-overview/preprint-provider-overview.component.spec.ts b/src/app/features/preprints/pages/preprint-provider-overview/preprint-provider-overview.component.spec.ts index 3d82f8d6a..9f3cfa173 100644 --- a/src/app/features/preprints/pages/preprint-provider-overview/preprint-provider-overview.component.spec.ts +++ b/src/app/features/preprints/pages/preprint-provider-overview/preprint-provider-overview.component.spec.ts @@ -1,12 +1,7 @@ -import { Store } from '@ngxs/store'; - -import { TranslatePipe } from '@ngx-translate/core'; -import { MockComponents, MockPipe, MockProvider } from 'ng-mocks'; - -import { of } from 'rxjs'; +import { MockComponents, MockProvider } from 'ng-mocks'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { ActivatedRoute } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; import { AdvisoryBoardComponent, @@ -14,35 +9,48 @@ import { PreprintProviderFooterComponent, PreprintProviderHeroComponent, } from '@osf/features/preprints/components'; -import { PreprintProviderOverviewComponent } from '@osf/features/preprints/pages'; +import { PreprintProviderDetails } from '@osf/features/preprints/models'; import { PreprintProvidersSelectors } from '@osf/features/preprints/store/preprint-providers'; -import { MOCK_PROVIDER, MOCK_STORE, TranslateServiceMock } from '@shared/mocks'; +import { BrowserTabHelper, HeaderStyleHelper } from '@shared/helpers'; import { BrandService } from '@shared/services'; +import { PreprintProviderOverviewComponent } from './preprint-provider-overview.component'; + +import { PREPRINT_PROVIDER_DETAILS_MOCK } from '@testing/mocks/preprint-provider-details'; +import { SUBJECTS_MOCK } from '@testing/mocks/subject.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('PreprintProviderOverviewComponent', () => { let component: PreprintProviderOverviewComponent; let fixture: ComponentFixture; + let routerMock: ReturnType; + let routeMock: ReturnType; + + const mockProvider: PreprintProviderDetails = PREPRINT_PROVIDER_DETAILS_MOCK; + const mockSubjects = SUBJECTS_MOCK; + const mockProviderId = 'osf'; beforeEach(async () => { - (MOCK_STORE.selectSignal as jest.Mock).mockImplementation((selector) => { - switch (selector) { - case PreprintProvidersSelectors.getPreprintProviderDetails('osf'): - return () => MOCK_PROVIDER; - case PreprintProvidersSelectors.isPreprintProviderDetailsLoading: - return () => false; - case PreprintProvidersSelectors.getHighlightedSubjectsForProvider: - return () => []; - case PreprintProvidersSelectors.areSubjectsLoading: - return () => false; - default: - return () => []; - } - }); + jest.spyOn(BrowserTabHelper, 'updateTabStyles').mockImplementation(() => {}); + jest.spyOn(BrowserTabHelper, 'resetToDefaults').mockImplementation(() => {}); + jest.spyOn(HeaderStyleHelper, 'applyHeaderStyles').mockImplementation(() => {}); + jest.spyOn(HeaderStyleHelper, 'resetToDefaults').mockImplementation(() => {}); + jest.spyOn(BrandService, 'applyBranding').mockImplementation(() => {}); + jest.spyOn(BrandService, 'resetBranding').mockImplementation(() => {}); + + routerMock = RouterMockBuilder.create().withNavigate(jest.fn().mockResolvedValue(true)).build(); + routeMock = ActivatedRouteMockBuilder.create() + .withParams({ providerId: mockProviderId }) + .withQueryParams({}) + .build(); await TestBed.configureTestingModule({ imports: [ PreprintProviderOverviewComponent, - MockPipe(TranslatePipe), + OSFTestingModule, ...MockComponents( PreprintProviderHeroComponent, PreprintProviderFooterComponent, @@ -50,18 +58,30 @@ describe('PreprintProviderOverviewComponent', () => { BrowseBySubjectsComponent ), ], - teardown: { destroyAfterEach: false }, providers: [ - MockProvider(Store, MOCK_STORE), - { - provide: ActivatedRoute, - useValue: { - params: of({ providerId: 'osf' }), - snapshot: { params: { providerId: 'osf' } }, - }, - }, MockProvider(BrandService), - TranslateServiceMock, + MockProvider(Router, routerMock), + MockProvider(ActivatedRoute, routeMock), + provideMockStore({ + signals: [ + { + selector: PreprintProvidersSelectors.getPreprintProviderDetails(mockProviderId), + value: mockProvider, + }, + { + selector: PreprintProvidersSelectors.isPreprintProviderDetailsLoading, + value: false, + }, + { + selector: PreprintProvidersSelectors.getHighlightedSubjectsForProvider, + value: mockSubjects, + }, + { + selector: PreprintProvidersSelectors.areSubjectsLoading, + value: false, + }, + ], + }), ], }).compileComponents(); @@ -73,4 +93,103 @@ describe('PreprintProviderOverviewComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + it('should initialize with correct default values', () => { + expect(component.preprintProvider).toBeDefined(); + expect(component.isPreprintProviderLoading).toBeDefined(); + expect(component.highlightedSubjectsByProviderId).toBeDefined(); + expect(component.areSubjectsLoading).toBeDefined(); + }); + + it('should return preprint provider from store', () => { + const provider = component.preprintProvider(); + expect(provider).toBe(mockProvider); + }); + + it('should return loading state from store', () => { + const loading = component.isPreprintProviderLoading(); + expect(loading).toBe(false); + }); + + it('should return highlighted subjects from store', () => { + const subjects = component.highlightedSubjectsByProviderId(); + expect(subjects).toBe(mockSubjects); + }); + + it('should return subjects loading state from store', () => { + const loading = component.areSubjectsLoading(); + expect(loading).toBe(false); + }); + + it('should handle provider data correctly', () => { + const provider = component.preprintProvider(); + expect(provider).toBe(mockProvider); + expect(provider?.id).toBe(mockProvider.id); + expect(provider?.name).toBe(mockProvider.name); + }); + + it('should handle subjects data correctly', () => { + const subjects = component.highlightedSubjectsByProviderId(); + expect(subjects).toBe(mockSubjects); + expect(Array.isArray(subjects)).toBe(true); + }); + + it('should handle loading states correctly', () => { + const providerLoading = component.isPreprintProviderLoading(); + const subjectsLoading = component.areSubjectsLoading(); + + expect(typeof providerLoading).toBe('boolean'); + expect(typeof subjectsLoading).toBe('boolean'); + expect(providerLoading).toBe(false); + expect(subjectsLoading).toBe(false); + }); + + it('should navigate to discover page with search value', () => { + const searchValue = 'test search'; + component.redirectToDiscoverPageWithValue(searchValue); + + expect(routerMock.navigate).toHaveBeenCalledWith(['discover'], { + relativeTo: expect.any(Object), + queryParams: { search: searchValue }, + }); + }); + + it('should navigate to discover page with empty search value', () => { + const searchValue = ''; + component.redirectToDiscoverPageWithValue(searchValue); + + expect(routerMock.navigate).toHaveBeenCalledWith(['discover'], { + relativeTo: expect.any(Object), + queryParams: { search: searchValue }, + }); + }); + + it('should navigate to discover page with null search value', () => { + const searchValue = null as any; + component.redirectToDiscoverPageWithValue(searchValue); + + expect(routerMock.navigate).toHaveBeenCalledWith(['discover'], { + relativeTo: expect.any(Object), + queryParams: { search: searchValue }, + }); + }); + + it('should initialize signals correctly', () => { + expect(component.preprintProvider).toBeDefined(); + expect(component.isPreprintProviderLoading).toBeDefined(); + expect(component.highlightedSubjectsByProviderId).toBeDefined(); + expect(component.areSubjectsLoading).toBeDefined(); + }); + + it('should handle provider data with null values', () => { + const provider = component.preprintProvider(); + expect(provider).toBeDefined(); + expect(provider).toBe(mockProvider); + }); + + it('should handle subjects data with empty array', () => { + const subjects = component.highlightedSubjectsByProviderId(); + expect(subjects).toBeDefined(); + expect(Array.isArray(subjects)).toBe(true); + }); }); diff --git a/src/app/features/preprints/pages/select-preprint-service/select-preprint-service.component.spec.ts b/src/app/features/preprints/pages/select-preprint-service/select-preprint-service.component.spec.ts index ed3235ec5..59159d2cb 100644 --- a/src/app/features/preprints/pages/select-preprint-service/select-preprint-service.component.spec.ts +++ b/src/app/features/preprints/pages/select-preprint-service/select-preprint-service.component.spec.ts @@ -1,39 +1,64 @@ -import { Store } from '@ngxs/store'; - import { TranslatePipe } from '@ngx-translate/core'; -import { MockPipes, MockProvider, MockProviders } from 'ng-mocks'; +import { MockComponents, MockPipe, MockProvider } from 'ng-mocks'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ActivatedRoute, Router } from '@angular/router'; +import { PreprintProviderShortInfo } from '@osf/features/preprints/models'; import { PreprintProvidersSelectors } from '@osf/features/preprints/store/preprint-providers'; import { PreprintStepperSelectors } from '@osf/features/preprints/store/preprint-stepper'; -import { MOCK_STORE, TranslateServiceMock } from '@shared/mocks'; +import { SubHeaderComponent } from '@shared/components'; import { DecodeHtmlPipe } from '@shared/pipes'; import { SelectPreprintServiceComponent } from './select-preprint-service.component'; +import { PREPRINT_PROVIDER_SHORT_INFO_MOCK } from '@testing/mocks/preprint-provider-short-info.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('SelectPreprintServiceComponent', () => { let component: SelectPreprintServiceComponent; let fixture: ComponentFixture; + let routerMock: ReturnType; + let routeMock: ReturnType; + + const mockProviders: PreprintProviderShortInfo[] = [PREPRINT_PROVIDER_SHORT_INFO_MOCK]; + const mockSelectedProviderId = 'osf'; beforeEach(async () => { - (MOCK_STORE.selectSignal as jest.Mock).mockImplementation((selector) => { - switch (selector) { - case PreprintProvidersSelectors.getPreprintProvidersAllowingSubmissions: - return () => []; - case PreprintProvidersSelectors.arePreprintProvidersAllowingSubmissionsLoading: - return () => false; - case PreprintStepperSelectors.getSelectedProviderId: - return () => null; - default: - return () => []; - } - }); + routerMock = RouterMockBuilder.create().withNavigate(jest.fn().mockResolvedValue(true)).build(); + routeMock = ActivatedRouteMockBuilder.create().withParams({}).withQueryParams({}).build(); await TestBed.configureTestingModule({ - imports: [SelectPreprintServiceComponent, MockPipes(TranslatePipe, DecodeHtmlPipe)], - providers: [MockProvider(Store, MOCK_STORE), TranslateServiceMock, MockProviders(Router, ActivatedRoute)], + imports: [ + SelectPreprintServiceComponent, + OSFTestingModule, + ...MockComponents(SubHeaderComponent), + MockPipe(TranslatePipe), + MockPipe(DecodeHtmlPipe), + ], + providers: [ + MockProvider(Router, routerMock), + MockProvider(ActivatedRoute, routeMock), + provideMockStore({ + signals: [ + { + selector: PreprintProvidersSelectors.getPreprintProvidersAllowingSubmissions, + value: mockProviders, + }, + { + selector: PreprintProvidersSelectors.arePreprintProvidersAllowingSubmissionsLoading, + value: false, + }, + { + selector: PreprintStepperSelectors.getSelectedProviderId, + value: mockSelectedProviderId, + }, + ], + }), + ], }).compileComponents(); fixture = TestBed.createComponent(SelectPreprintServiceComponent); @@ -44,4 +69,70 @@ describe('SelectPreprintServiceComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + it('should return preprint providers from store', () => { + const providers = component.preprintProvidersAllowingSubmissions(); + expect(providers).toBe(mockProviders); + }); + + it('should return loading state from store', () => { + const loading = component.areProvidersLoading(); + expect(loading).toBe(false); + }); + + it('should return selected provider ID from store', () => { + const selectedId = component.selectedProviderId(); + expect(selectedId).toBe(mockSelectedProviderId); + }); + + it('should handle provider data correctly', () => { + const providers = component.preprintProvidersAllowingSubmissions(); + expect(providers).toBe(mockProviders); + expect(Array.isArray(providers)).toBe(true); + expect(providers.length).toBe(1); + expect(providers[0].id).toBe(mockProviders[0].id); + }); + + it('should handle loading states correctly', () => { + const loading = component.areProvidersLoading(); + expect(typeof loading).toBe('boolean'); + expect(loading).toBe(false); + }); + + it('should handle selected provider ID correctly', () => { + const selectedId = component.selectedProviderId(); + expect(selectedId).toBe(mockSelectedProviderId); + expect(typeof selectedId).toBe('string'); + }); + + it('should initialize skeleton array correctly', () => { + expect(component.skeletonArray).toBeDefined(); + expect(Array.isArray(component.skeletonArray)).toBe(true); + expect(component.skeletonArray.length).toBe(8); + expect(component.skeletonArray).toEqual([1, 2, 3, 4, 5, 6, 7, 8]); + }); + + it('should handle provider selection when provider is not selected', () => { + const provider = mockProviders[0]; + component.selectDeselectProvider(provider); + + expect(component.selectedProviderId()).toBe(mockSelectedProviderId); + }); + + it('should handle provider deselection when provider is already selected', () => { + const provider = mockProviders[0]; + + expect(() => component.selectDeselectProvider(provider)).not.toThrow(); + }); + + it('should handle empty providers array', () => { + const providers = component.preprintProvidersAllowingSubmissions(); + expect(providers).toBeDefined(); + expect(Array.isArray(providers)).toBe(true); + }); + + it('should handle null selected provider ID', () => { + const selectedId = component.selectedProviderId(); + expect(selectedId).toBeDefined(); + }); }); diff --git a/src/app/features/preprints/pages/submit-preprint-stepper/submit-preprint-stepper.component.spec.ts b/src/app/features/preprints/pages/submit-preprint-stepper/submit-preprint-stepper.component.spec.ts index 874d7fad8..f04b18ce7 100644 --- a/src/app/features/preprints/pages/submit-preprint-stepper/submit-preprint-stepper.component.spec.ts +++ b/src/app/features/preprints/pages/submit-preprint-stepper/submit-preprint-stepper.component.spec.ts @@ -1,34 +1,95 @@ -import { Store } from '@ngxs/store'; +import { MockComponents, MockProvider } from 'ng-mocks'; -import { TranslatePipe } from '@ngx-translate/core'; -import { MockPipe, MockProvider } from 'ng-mocks'; +import { of } from 'rxjs'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { Router } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; +import { + AuthorAssertionsStepComponent, + FileStepComponent, + MetadataStepComponent, + ReviewStepComponent, + SupplementsStepComponent, + TitleAndAbstractStepComponent, +} from '@osf/features/preprints/components'; +import { submitPreprintSteps } from '@osf/features/preprints/constants'; +import { PreprintSteps } from '@osf/features/preprints/enums'; +import { PreprintProviderDetails } from '@osf/features/preprints/models'; +import { PreprintProvidersSelectors } from '@osf/features/preprints/store/preprint-providers'; import { PreprintStepperSelectors } from '@osf/features/preprints/store/preprint-stepper'; -import { MOCK_STORE, TranslateServiceMock } from '@shared/mocks'; +import { StepperComponent } from '@shared/components'; +import { BrowserTabHelper, HeaderStyleHelper, IS_WEB } from '@shared/helpers'; +import { StepOption } from '@shared/models'; +import { BrandService } from '@shared/services'; import { SubmitPreprintStepperComponent } from './submit-preprint-stepper.component'; -describe.skip('SubmitPreprintStepperComponent', () => { +import { PREPRINT_PROVIDER_DETAILS_MOCK } from '@testing/mocks/preprint-provider-details'; +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('SubmitPreprintStepperComponent', () => { let component: SubmitPreprintStepperComponent; let fixture: ComponentFixture; + let routerMock: ReturnType; + let routeMock: ReturnType; + + const mockProvider: PreprintProviderDetails = PREPRINT_PROVIDER_DETAILS_MOCK; + const mockProviderId = 'osf'; beforeEach(async () => { - (MOCK_STORE.selectSignal as jest.Mock).mockImplementation((selector) => { - switch (selector) { - case PreprintStepperSelectors.getSelectedProviderId: - return () => 'id1'; - case PreprintStepperSelectors.isPreprintSubmitting: - return () => false; - default: - return () => null; - } - }); + jest.spyOn(BrowserTabHelper, 'updateTabStyles').mockImplementation(() => {}); + jest.spyOn(BrowserTabHelper, 'resetToDefaults').mockImplementation(() => {}); + jest.spyOn(HeaderStyleHelper, 'applyHeaderStyles').mockImplementation(() => {}); + jest.spyOn(HeaderStyleHelper, 'resetToDefaults').mockImplementation(() => {}); + jest.spyOn(BrandService, 'applyBranding').mockImplementation(() => {}); + jest.spyOn(BrandService, 'resetBranding').mockImplementation(() => {}); + + routerMock = RouterMockBuilder.create().withNavigate(jest.fn().mockResolvedValue(true)).build(); + routeMock = ActivatedRouteMockBuilder.create() + .withParams({ providerId: mockProviderId }) + .withQueryParams({}) + .build(); + await TestBed.configureTestingModule({ - imports: [SubmitPreprintStepperComponent, MockPipe(TranslatePipe)], - providers: [MockProvider(Store, MOCK_STORE), TranslateServiceMock, MockProvider(Router)], + imports: [ + SubmitPreprintStepperComponent, + OSFTestingModule, + ...MockComponents( + StepperComponent, + TitleAndAbstractStepComponent, + FileStepComponent, + MetadataStepComponent, + AuthorAssertionsStepComponent, + SupplementsStepComponent, + ReviewStepComponent + ), + ], + providers: [ + MockProvider(BrandService), + MockProvider(Router, routerMock), + MockProvider(ActivatedRoute, routeMock), + { provide: IS_WEB, useValue: of(true) }, + provideMockStore({ + signals: [ + { + selector: PreprintProvidersSelectors.getPreprintProviderDetails(mockProviderId), + value: mockProvider, + }, + { + selector: PreprintProvidersSelectors.isPreprintProviderDetailsLoading, + value: false, + }, + { + selector: PreprintStepperSelectors.hasBeenSubmitted, + value: false, + }, + ], + }), + ], }).compileComponents(); fixture = TestBed.createComponent(SubmitPreprintStepperComponent); @@ -39,4 +100,94 @@ describe.skip('SubmitPreprintStepperComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + it('should initialize with correct default values', () => { + expect(component.SubmitStepsEnum).toBe(PreprintSteps); + expect(component.classes).toBe('flex-1 flex flex-column w-full'); + expect(component.currentStep()).toEqual(submitPreprintSteps[0]); + }); + + it('should return submission state from store', () => { + const submitted = component.hasBeenSubmitted(); + expect(submitted).toBe(false); + }); + + it('should return web environment state', () => { + const isWeb = component.isWeb(); + expect(typeof isWeb).toBe('boolean'); + }); + + it('should initialize with first step as current step', () => { + expect(component.currentStep()).toEqual(submitPreprintSteps[0]); + }); + + it('should compute submitPreprintSteps correctly', () => { + const steps = component.submitPreprintSteps(); + expect(steps).toBeDefined(); + expect(Array.isArray(steps)).toBe(true); + }); + + it('should handle step change when moving to previous step', () => { + const previousStep = submitPreprintSteps[0]; + + component.stepChange(previousStep); + + expect(component.currentStep()).toEqual(previousStep); + }); + + it('should not change step when moving to next step', () => { + const currentStep = component.currentStep(); + const nextStep = submitPreprintSteps[1]; + + component.stepChange(nextStep); + + expect(component.currentStep()).toEqual(currentStep); + }); + + it('should move to next step', () => { + const currentIndex = component.currentStep()?.index ?? 0; + const nextStep = component.submitPreprintSteps()[currentIndex + 1]; + + if (nextStep) { + component.moveToNextStep(); + expect(component.currentStep()).toEqual(nextStep); + } + }); + + it('should move to previous step', () => { + component.moveToNextStep(); + const nextStep = component.currentStep(); + + component.moveToPreviousStep(); + const previousStep = component.currentStep(); + + expect(previousStep?.index).toBeLessThan(nextStep?.index ?? 0); + }); + + it('should handle beforeunload event', () => { + const event = { + preventDefault: jest.fn(), + } as unknown as BeforeUnloadEvent; + + const result = component.onBeforeUnload(event); + + expect(event.preventDefault).toHaveBeenCalled(); + expect(result).toBe(false); + }); + + it('should handle step navigation correctly', () => { + component.moveToNextStep(); + const nextStep = component.currentStep(); + expect(nextStep).toBeDefined(); + + component.moveToPreviousStep(); + const previousStep = component.currentStep(); + expect(previousStep).toBeDefined(); + }); + + it('should handle edge case when moving to next step with undefined current step', () => { + component.currentStep.set({} as StepOption); + + expect(() => component.moveToNextStep()).not.toThrow(); + }); }); diff --git a/src/app/features/preprints/pages/update-preprint-stepper/update-preprint-stepper.component.spec.ts b/src/app/features/preprints/pages/update-preprint-stepper/update-preprint-stepper.component.spec.ts index e33dd73ab..f3afe5c33 100644 --- a/src/app/features/preprints/pages/update-preprint-stepper/update-preprint-stepper.component.spec.ts +++ b/src/app/features/preprints/pages/update-preprint-stepper/update-preprint-stepper.component.spec.ts @@ -1,53 +1,101 @@ -import { Store } from '@ngxs/store'; - -import { TranslatePipe } from '@ngx-translate/core'; -import { MockPipe, MockProvider } from 'ng-mocks'; +import { MockComponents, MockProvider } from 'ng-mocks'; import { of } from 'rxjs'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ActivatedRoute, Router } from '@angular/router'; +import { + AuthorAssertionsStepComponent, + FileStepComponent, + MetadataStepComponent, + ReviewStepComponent, + SupplementsStepComponent, + TitleAndAbstractStepComponent, +} from '@osf/features/preprints/components'; +import { submitPreprintSteps } from '@osf/features/preprints/constants'; +import { PreprintSteps } from '@osf/features/preprints/enums'; +import { PreprintProviderDetails } from '@osf/features/preprints/models'; import { PreprintProvidersSelectors } from '@osf/features/preprints/store/preprint-providers'; import { PreprintStepperSelectors } from '@osf/features/preprints/store/preprint-stepper'; -import { MOCK_PROVIDER, MOCK_STORE, TranslateServiceMock } from '@shared/mocks'; -import { ToastService } from '@shared/services'; +import { StepperComponent } from '@shared/components'; +import { BrowserTabHelper, HeaderStyleHelper, IS_WEB } from '@shared/helpers'; +import { StepOption } from '@shared/models'; +import { BrandService } from '@shared/services'; import { UpdatePreprintStepperComponent } from './update-preprint-stepper.component'; +import { PREPRINT_MOCK } from '@testing/mocks/preprint.mock'; +import { PREPRINT_PROVIDER_DETAILS_MOCK } from '@testing/mocks/preprint-provider-details'; +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('UpdatePreprintStepperComponent', () => { let component: UpdatePreprintStepperComponent; let fixture: ComponentFixture; - const mockActivatedRoute = { - params: of({ providerId: 'osf', preprintId: 'id1' }), - snapshot: { params: { providerId: 'osf', preprintId: 'id1' } }, - }; + let routerMock: ReturnType; + let routeMock: ReturnType; + + const mockProvider: PreprintProviderDetails = PREPRINT_PROVIDER_DETAILS_MOCK; + const mockPreprint = PREPRINT_MOCK; + const mockProviderId = 'osf'; + const mockPreprintId = 'test_preprint_123'; beforeEach(async () => { - (MOCK_STORE.selectSignal as jest.Mock).mockImplementation((selector) => { - switch (selector) { - case PreprintStepperSelectors.getPreprint: - return () => ({ id: 'id1', title: 'Test', description: '' }); - case PreprintStepperSelectors.isPreprintSubmitting: - return () => false; - case PreprintProvidersSelectors.getPreprintProviderDetails('osf'): - return () => MOCK_PROVIDER; - case PreprintProvidersSelectors.isPreprintProviderDetailsLoading: - return () => false; - default: - return () => null; - } - }); + jest.spyOn(BrowserTabHelper, 'updateTabStyles').mockImplementation(() => {}); + jest.spyOn(BrowserTabHelper, 'resetToDefaults').mockImplementation(() => {}); + jest.spyOn(HeaderStyleHelper, 'applyHeaderStyles').mockImplementation(() => {}); + jest.spyOn(HeaderStyleHelper, 'resetToDefaults').mockImplementation(() => {}); + jest.spyOn(BrandService, 'applyBranding').mockImplementation(() => {}); + jest.spyOn(BrandService, 'resetBranding').mockImplementation(() => {}); + + routerMock = RouterMockBuilder.create().withNavigate(jest.fn().mockResolvedValue(true)).build(); + routeMock = ActivatedRouteMockBuilder.create() + .withParams({ providerId: mockProviderId, preprintId: mockPreprintId }) + .withQueryParams({}) + .build(); await TestBed.configureTestingModule({ - imports: [UpdatePreprintStepperComponent, MockPipe(TranslatePipe)], - teardown: { destroyAfterEach: false }, + imports: [ + UpdatePreprintStepperComponent, + OSFTestingModule, + ...MockComponents( + StepperComponent, + TitleAndAbstractStepComponent, + FileStepComponent, + MetadataStepComponent, + AuthorAssertionsStepComponent, + SupplementsStepComponent, + ReviewStepComponent + ), + ], providers: [ - MockProvider(Store, MOCK_STORE), - TranslateServiceMock, - MockProvider(ToastService), - MockProvider(Router), - { provide: ActivatedRoute, useValue: mockActivatedRoute }, + MockProvider(BrandService), + MockProvider(Router, routerMock), + MockProvider(ActivatedRoute, routeMock), + { provide: IS_WEB, useValue: of(true) }, + provideMockStore({ + signals: [ + { + selector: PreprintProvidersSelectors.getPreprintProviderDetails(mockProviderId), + value: mockProvider, + }, + { + selector: PreprintProvidersSelectors.isPreprintProviderDetailsLoading, + value: false, + }, + { + selector: PreprintStepperSelectors.getPreprint, + value: mockPreprint, + }, + { + selector: PreprintStepperSelectors.hasBeenSubmitted, + value: false, + }, + ], + }), ], }).compileComponents(); @@ -59,4 +107,126 @@ describe('UpdatePreprintStepperComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + it('should initialize with correct default values', () => { + expect(component.PreprintSteps).toBe(PreprintSteps); + expect(component.classes).toBe('flex-1 flex flex-column w-full'); + expect(component.currentStep()).toEqual(submitPreprintSteps[0]); + }); + + it('should return preprint provider from store', () => { + const provider = component.preprintProvider(); + expect(provider).toBe(mockProvider); + }); + + it('should return preprint from store', () => { + const preprint = component.preprint(); + expect(preprint).toBe(mockPreprint); + }); + + it('should return web environment state', () => { + const isWeb = component.isWeb(); + expect(typeof isWeb).toBe('boolean'); + }); + + it('should initialize with first step as current step', () => { + expect(component.currentStep()).toEqual(submitPreprintSteps[0]); + }); + + it('should compute updateSteps correctly', () => { + const steps = component.updateSteps(); + expect(steps).toBeDefined(); + expect(Array.isArray(steps)).toBe(true); + }); + + it('should compute currentUserIsAdmin correctly', () => { + const isAdmin = component.currentUserIsAdmin(); + expect(typeof isAdmin).toBe('boolean'); + }); + + it('should compute editAndResubmitMode correctly', () => { + const editMode = component.editAndResubmitMode(); + expect(typeof editMode).toBe('boolean'); + }); + + it('should handle step change when moving to previous step', () => { + const previousStep = submitPreprintSteps[0]; + + component.stepChange(previousStep); + + expect(component.currentStep()).toEqual(previousStep); + }); + + it('should not change step when moving to next step', () => { + const currentStep = component.currentStep(); + const nextStep = submitPreprintSteps[1]; + + component.stepChange(nextStep); + + expect(component.currentStep()).toEqual(currentStep); + }); + + it('should move to next step', () => { + const currentIndex = component.currentStep()?.index ?? 0; + const nextStep = component.updateSteps()[currentIndex + 1]; + + if (nextStep) { + component.moveToNextStep(); + expect(component.currentStep()).toEqual(nextStep); + } + }); + + it('should move to previous step', () => { + component.moveToNextStep(); + const nextStep = component.currentStep(); + + component.moveToPreviousStep(); + const previousStep = component.currentStep(); + + expect(previousStep?.index).toBeLessThan(nextStep?.index ?? 0); + }); + + it('should handle beforeunload event', () => { + const event = { + preventDefault: jest.fn(), + } as unknown as BeforeUnloadEvent; + + const result = component.onBeforeUnload(event); + + expect(event.preventDefault).toHaveBeenCalled(); + expect(result).toBe(false); + }); + + it('should handle step navigation correctly', () => { + component.moveToNextStep(); + const nextStep = component.currentStep(); + expect(nextStep).toBeDefined(); + + component.moveToPreviousStep(); + const previousStep = component.currentStep(); + expect(previousStep).toBeDefined(); + }); + + it('should handle edge case when moving to next step with undefined current step', () => { + component.currentStep.set({} as StepOption); + + expect(() => component.moveToNextStep()).not.toThrow(); + }); + + it('should handle edge case when moving to previous step with undefined current step', () => { + component.currentStep.set({} as StepOption); + + expect(() => component.moveToPreviousStep()).not.toThrow(); + }); + + it('should handle empty updateSteps array', () => { + const steps = component.updateSteps(); + expect(steps).toBeDefined(); + expect(Array.isArray(steps)).toBe(true); + }); + + it('should handle null preprint provider', () => { + const provider = component.preprintProvider(); + expect(provider).toBeDefined(); + }); }); diff --git a/src/app/shared/components/bar-chart/bar-chart.component.spec.ts b/src/app/shared/components/bar-chart/bar-chart.component.spec.ts index 4aef43323..0169d09a7 100644 --- a/src/app/shared/components/bar-chart/bar-chart.component.spec.ts +++ b/src/app/shared/components/bar-chart/bar-chart.component.spec.ts @@ -34,25 +34,21 @@ describe('BarChartComponent', () => { expect(component.showExpandedSection()).toBe(false); }); - it('should return color from getColor method', () => { - const color1 = component.getColor(0); - const color2 = component.getColor(1); - const color3 = component.getColor(10); - - expect(color1).toBeDefined(); - expect(color2).toBeDefined(); - expect(color3).toBeDefined(); - expect(typeof color1).toBe('string'); + it('should have access to PIE_CHART_PALETTE', () => { + expect(component.PIE_CHART_PALETTE).toBeDefined(); + expect(Array.isArray(component.PIE_CHART_PALETTE)).toBe(true); }); - it('should return different colors for different indices', () => { - const color1 = component.getColor(0); - const color2 = component.getColor(1); + it('should return different colors from PIE_CHART_PALETTE for different indices', () => { + const color1 = component.PIE_CHART_PALETTE[0]; + const color2 = component.PIE_CHART_PALETTE[1]; + expect(color1).toBeDefined(); + expect(color2).toBeDefined(); expect(color1).not.toBe(color2); }); - it('should initialize chart successfully', () => { + it('should initialize chart data and options on ngOnInit', () => { const mockGetPropertyValue = jest.fn((prop: string) => { const colors: Record = { '--dark-blue-1': '#1a365d', @@ -66,9 +62,11 @@ describe('BarChartComponent', () => { getPropertyValue: mockGetPropertyValue, } as any); - (component as any).initChart(); + component.ngOnInit(); expect(mockGetComputedStyle).toHaveBeenCalledWith(document.documentElement); + expect(component.data()).toBeDefined(); + expect(component.options()).toBeDefined(); mockGetComputedStyle.mockRestore(); }); diff --git a/src/app/shared/components/doughnut-chart/doughnut-chart.component.spec.ts b/src/app/shared/components/doughnut-chart/doughnut-chart.component.spec.ts index f721e6384..424cefd15 100644 --- a/src/app/shared/components/doughnut-chart/doughnut-chart.component.spec.ts +++ b/src/app/shared/components/doughnut-chart/doughnut-chart.component.spec.ts @@ -35,10 +35,9 @@ describe('DoughnutChartComponent', () => { expect(component.showExpandedSection()).toBe(false); }); - it('should test getColor method with various indices', () => { - expect(component.getColor(0)).toBeDefined(); - expect(component.getColor(5)).toBeDefined(); - expect(component.getColor(10)).toBeDefined(); + it('should have access to PIE_CHART_PALETTE', () => { + expect(component.PIE_CHART_PALETTE).toBeDefined(); + expect(Array.isArray(component.PIE_CHART_PALETTE)).toBe(true); }); it('should handle input updates', () => { diff --git a/src/app/shared/components/search-help-tutorial/search-help-tutorial.component.spec.ts b/src/app/shared/components/search-help-tutorial/search-help-tutorial.component.spec.ts index 8ed65d52c..ba5745abd 100644 --- a/src/app/shared/components/search-help-tutorial/search-help-tutorial.component.spec.ts +++ b/src/app/shared/components/search-help-tutorial/search-help-tutorial.component.spec.ts @@ -13,6 +13,7 @@ describe('SearchHelpTutorialComponent', () => { title: 'Test Step', description: 'This is a test step', position: { top: '10px', left: '20px' }, + mobilePosition: { top: '10px', left: '20px' }, }; beforeEach(async () => { diff --git a/src/app/shared/mocks/provider.mock.ts b/src/app/shared/mocks/provider.mock.ts index bafd01c8d..0cf16267a 100644 --- a/src/app/shared/mocks/provider.mock.ts +++ b/src/app/shared/mocks/provider.mock.ts @@ -20,8 +20,12 @@ export const MOCK_PROVIDER = { allowSubmissions: true, assertionsEnabled: false, reviewsWorkflow: null, + permissions: [], brand: mockBrand, iri: '', faviconUrl: '', squareColorNoTransparentImageUrl: '', + facebookAppId: null, + reviewsCommentsPrivate: null, + reviewsCommentsAnonymous: null, }; diff --git a/src/testing/mocks/citation-style.mock.ts b/src/testing/mocks/citation-style.mock.ts new file mode 100644 index 000000000..48039e7ab --- /dev/null +++ b/src/testing/mocks/citation-style.mock.ts @@ -0,0 +1,39 @@ +import { CitationStyle } from '@shared/models'; + +export const CITATION_STYLES_MOCK: CitationStyle[] = [ + { + id: 'style-1', + title: 'APA Style', + shortTitle: 'APA', + summary: 'American Psychological Association style', + dateParsed: '2024-01-01', + }, + { + id: 'style-2', + title: 'MLA Style', + shortTitle: 'MLA', + summary: 'Modern Language Association style', + dateParsed: '2024-01-01', + }, + { + id: 'style-3', + title: 'Chicago Style', + shortTitle: 'Chicago', + summary: 'Chicago Manual of Style', + dateParsed: '2024-01-01', + }, + { + id: 'style-4', + title: 'Harvard Style', + shortTitle: 'Harvard', + summary: 'Harvard referencing style', + dateParsed: '2024-01-01', + }, + { + id: 'style-5', + title: 'IEEE Style', + shortTitle: 'IEEE', + summary: 'Institute of Electrical and Electronics Engineers style', + dateParsed: '2024-01-01', + }, +]; diff --git a/src/testing/mocks/preprint-provider-short-info.mock.ts b/src/testing/mocks/preprint-provider-short-info.mock.ts new file mode 100644 index 000000000..a5d2dfa9b --- /dev/null +++ b/src/testing/mocks/preprint-provider-short-info.mock.ts @@ -0,0 +1,10 @@ +import { PreprintProviderShortInfo } from '@osf/features/preprints/models'; + +export const PREPRINT_PROVIDER_SHORT_INFO_MOCK: PreprintProviderShortInfo = { + id: 'provider-1', + name: 'Test Provider 1', + descriptionHtml: '

Description 1

', + whiteWideImageUrl: 'https://example.com/image1.png', + squareColorNoTransparentImageUrl: 'https://example.com/square1.png', + submissionCount: 100, +}; diff --git a/src/testing/mocks/preprint-request.mock.ts b/src/testing/mocks/preprint-request.mock.ts new file mode 100644 index 000000000..694ca6efb --- /dev/null +++ b/src/testing/mocks/preprint-request.mock.ts @@ -0,0 +1,14 @@ +import { PreprintRequestMachineState, PreprintRequestType } from '@osf/features/preprints/enums'; +import { PreprintRequest } from '@osf/features/preprints/models'; + +export const PREPRINT_REQUEST_MOCK: PreprintRequest = { + id: 'request-1', + comment: 'Withdrawal request comment', + machineState: PreprintRequestMachineState.Pending, + requestType: PreprintRequestType.Withdrawal, + dateLastTransitioned: new Date('2024-01-01T10:00:00Z'), + creator: { + id: 'user-123', + name: 'John Doe', + }, +}; diff --git a/src/testing/mocks/preprint-short-info.mock.ts b/src/testing/mocks/preprint-short-info.mock.ts new file mode 100644 index 000000000..8a811c5eb --- /dev/null +++ b/src/testing/mocks/preprint-short-info.mock.ts @@ -0,0 +1,71 @@ +import { PreprintShortInfo } from '@osf/features/preprints/models'; + +export const PREPRINT_SHORT_INFO_ARRAY_MOCK: PreprintShortInfo[] = [ + { + id: 'preprint-1', + title: 'Test Preprint 1', + dateModified: '2024-01-01T00:00:00Z', + contributors: [ + { + id: 'user-1', + name: 'John Doe', + }, + ], + providerId: 'provider-1', + }, + { + id: 'preprint-2', + title: 'Test Preprint 2', + dateModified: '2024-01-02T00:00:00Z', + contributors: [ + { + id: 'user-2', + name: 'Jane Smith', + }, + { + id: 'user-3', + name: 'Bob Wilson', + }, + ], + providerId: 'provider-2', + }, + { + id: 'preprint-3', + title: 'Test Preprint 3', + dateModified: '2024-01-03T00:00:00Z', + contributors: [], + providerId: 'provider-1', + }, + { + id: 'preprint-4', + title: 'Test Preprint 4', + dateModified: '2024-01-04T00:00:00Z', + contributors: [ + { + id: 'user-4', + name: 'Alice Johnson', + }, + ], + providerId: 'provider-3', + }, + { + id: 'preprint-5', + title: 'Test Preprint 5', + dateModified: '2024-01-05T00:00:00Z', + contributors: [ + { + id: 'user-5', + name: 'Charlie Brown', + }, + { + id: 'user-6', + name: 'Diana Prince', + }, + { + id: 'user-7', + name: 'Eve Adams', + }, + ], + providerId: 'provider-2', + }, +]; diff --git a/src/testing/mocks/preprint.mock.ts b/src/testing/mocks/preprint.mock.ts new file mode 100644 index 000000000..9a4d1a867 --- /dev/null +++ b/src/testing/mocks/preprint.mock.ts @@ -0,0 +1,56 @@ +import { ReviewsState } from '@osf/features/preprints/enums'; +import { LicenseModel, LicenseOptions } from '@shared/models'; + +export const PREPRINT_MOCK = { + id: 'preprint-1', + dateCreated: '2023-01-01T00:00:00Z', + dateModified: '2023-01-15T00:00:00Z', + dateWithdrawn: null, + datePublished: new Date('2023-01-15T00:00:00Z'), + dateLastTransitioned: new Date('2023-01-15T00:00:00Z'), + title: 'Sample Preprint Title', + description: 'This is a sample preprint description for testing purposes.', + reviewsState: ReviewsState.Pending, + preprintDoiCreated: new Date('2023-01-15T00:00:00Z'), + currentUserPermissions: [], + doi: '10.1234/sample.doi', + originalPublicationDate: new Date('2023-01-15T00:00:00Z'), + customPublicationCitation: null, + isPublished: false, + tags: ['research', 'science', 'preprint'], + isPublic: true, + version: 1, + isLatestVersion: true, + isPreprintOrphan: false, + withdrawalJustification: null, + nodeId: 'node-123', + primaryFileId: 'file-123', + licenseId: 'license-123', + licenseOptions: { + copyrightHolders: 'John Doe', + year: '2023', + } as LicenseOptions, + hasCoi: false, + coiStatement: null, + hasDataLinks: null, + dataLinks: [], + whyNoData: null, + hasPreregLinks: null, + whyNoPrereg: null, + preregLinks: [], + preregLinkInfo: null, + metrics: { + downloads: 150, + views: 500, + }, + embeddedLicense: { + id: 'license-123', + name: 'MIT License', + requiredFields: ['copyrightHolders', 'year'], + url: 'https://opensource.org/licenses/MIT', + text: 'MIT License text...', + } as LicenseModel, + preprintDoiLink: 'https://doi.org/10.1234/sample.doi', + articleDoiLink: undefined, + identifiers: [], +}; diff --git a/src/testing/mocks/review-action.mock.ts b/src/testing/mocks/review-action.mock.ts new file mode 100644 index 000000000..7e9c3eb8c --- /dev/null +++ b/src/testing/mocks/review-action.mock.ts @@ -0,0 +1,14 @@ +import { ReviewAction } from '@osf/features/moderation/models'; + +export const REVIEW_ACTION_MOCK: ReviewAction = { + id: 'action-1', + trigger: 'accept', + fromState: 'pending', + toState: 'accepted', + dateModified: '2024-01-01T10:00:00Z', + creator: { + id: 'user-1', + name: 'Test User', + }, + comment: 'Initial comment', +}; diff --git a/src/testing/mocks/social-share-links.mock.ts b/src/testing/mocks/social-share-links.mock.ts new file mode 100644 index 000000000..86bafcc8d --- /dev/null +++ b/src/testing/mocks/social-share-links.mock.ts @@ -0,0 +1,10 @@ +import { SocialShareLinks } from '@shared/models'; + +export const SOCIAL_SHARE_LINKS_MOCK: SocialShareLinks = { + email: + 'mailto:?subject=Sample%20Preprint%20Title&body=Check%20out%20this%20preprint%3A%20https%3A//example.com/preprint/preprint-1', + twitter: + 'https://twitter.com/intent/tweet?text=Sample%20Preprint%20Title&url=https%3A//example.com/preprint/preprint-1', + facebook: 'https://www.facebook.com/sharer/sharer.php?u=https%3A//example.com/preprint/preprint-1', + linkedIn: 'https://www.linkedin.com/sharing/share-offsite/?url=https%3A//example.com/preprint/preprint-1', +}; diff --git a/src/testing/mocks/subject.mock.ts b/src/testing/mocks/subject.mock.ts new file mode 100644 index 000000000..2440a75ec --- /dev/null +++ b/src/testing/mocks/subject.mock.ts @@ -0,0 +1,20 @@ +import { SubjectModel } from '@shared/models'; + +export const SUBJECTS_MOCK: SubjectModel[] = [ + { + id: 'subject-1', + name: 'Mathematics', + iri: 'https://example.com/subjects/mathematics', + children: [], + parent: null, + expanded: false, + }, + { + id: 'subject-2', + name: 'Physics', + iri: 'https://example.com/subjects/physics', + children: [], + parent: null, + expanded: false, + }, +]; diff --git a/src/testing/providers/dialog-provider.mock.ts b/src/testing/providers/dialog-provider.mock.ts new file mode 100644 index 000000000..94ee40087 --- /dev/null +++ b/src/testing/providers/dialog-provider.mock.ts @@ -0,0 +1,62 @@ +import { DialogService, DynamicDialogRef } from 'primeng/dynamicdialog'; + +export class DialogServiceMockBuilder { + private openSpy = jest.fn(); + private getInstanceSpy = jest.fn(); + + static create(): DialogServiceMockBuilder { + return new DialogServiceMockBuilder(); + } + + withOpenMock(mockFn?: jest.Mock): DialogServiceMockBuilder { + this.openSpy = mockFn || jest.fn().mockReturnValue({} as DynamicDialogRef); + return this; + } + + withGetInstanceMock(mockFn?: jest.Mock): DialogServiceMockBuilder { + this.getInstanceSpy = mockFn || jest.fn(); + return this; + } + + withOpenReturning(ref: DynamicDialogRef): DialogServiceMockBuilder { + this.openSpy = jest.fn().mockReturnValue(ref); + return this; + } + + withOpenThrowing(error: Error): DialogServiceMockBuilder { + this.openSpy = jest.fn().mockImplementation(() => { + throw error; + }); + return this; + } + + build(): Partial { + return { + open: this.openSpy, + getInstance: this.getInstanceSpy, + dialogComponentRefMap: new Map(), + }; + } +} + +export const DialogServiceMock = { + create() { + return DialogServiceMockBuilder.create(); + }, + + withOpenMock(mockFn?: jest.Mock) { + return DialogServiceMockBuilder.create().withOpenMock(mockFn); + }, + + withGetInstanceMock(mockFn?: jest.Mock) { + return DialogServiceMockBuilder.create().withGetInstanceMock(mockFn); + }, + + withOpenReturning(ref: DynamicDialogRef) { + return DialogServiceMockBuilder.create().withOpenReturning(ref); + }, + + withOpenThrowing(error: Error) { + return DialogServiceMockBuilder.create().withOpenThrowing(error); + }, +};