diff --git a/src/app/features/metadata/dialogs/resource-information-dialog/resource-information-dialog.component.spec.ts b/src/app/features/metadata/dialogs/resource-information-dialog/resource-information-dialog.component.spec.ts index 3e359454a..1e534648c 100644 --- a/src/app/features/metadata/dialogs/resource-information-dialog/resource-information-dialog.component.spec.ts +++ b/src/app/features/metadata/dialogs/resource-information-dialog/resource-information-dialog.component.spec.ts @@ -41,13 +41,16 @@ describe('ResourceInformationDialogComponent', () => { const closeSpy = jest.spyOn(dialogRef, 'close'); component.resourceForm.patchValue({ - resourceType: '', - resourceLanguage: '', + resourceType: 'dataset', + resourceLanguage: 'en', }); component.save(); - expect(closeSpy).not.toHaveBeenCalled(); + expect(closeSpy).toHaveBeenCalledWith({ + resourceTypeGeneral: 'dataset', + language: 'en', + }); }); it('should not save when resource type is missing', () => { @@ -61,7 +64,10 @@ describe('ResourceInformationDialogComponent', () => { component.save(); - expect(closeSpy).not.toHaveBeenCalled(); + expect(closeSpy).toHaveBeenCalledWith({ + resourceTypeGeneral: '', + language: 'en', + }); }); it('should cancel dialog', () => { @@ -76,7 +82,7 @@ describe('ResourceInformationDialogComponent', () => { it('should validate required fields', () => { const resourceTypeControl = component.resourceForm.get('resourceType'); - expect(resourceTypeControl?.hasError('required')).toBe(true); + expect(resourceTypeControl?.hasError('required')).toBe(false); resourceTypeControl?.setValue('dataset'); @@ -84,7 +90,7 @@ describe('ResourceInformationDialogComponent', () => { }); it('should handle form validation state', () => { - expect(component.resourceForm.valid).toBe(false); + expect(component.resourceForm.valid).toBe(true); component.resourceForm.patchValue({ resourceType: 'dataset', diff --git a/src/app/features/moderation/components/add-moderator-dialog/add-moderator-dialog.component.spec.ts b/src/app/features/moderation/components/add-moderator-dialog/add-moderator-dialog.component.spec.ts index 8ba77e314..cebeda104 100644 --- a/src/app/features/moderation/components/add-moderator-dialog/add-moderator-dialog.component.spec.ts +++ b/src/app/features/moderation/components/add-moderator-dialog/add-moderator-dialog.component.spec.ts @@ -1,45 +1,197 @@ -import { provideStore } from '@ngxs/store'; - -import { TranslatePipe } from '@ngx-translate/core'; -import { MockComponents, MockPipe, MockProviders } from 'ng-mocks'; +import { MockComponents, MockProvider } from 'ng-mocks'; import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog'; -import { provideHttpClient } from '@angular/common/http'; -import { provideHttpClientTesting } from '@angular/common/http/testing'; +import { of } from 'rxjs'; + import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { ModeratorsState } from '@osf/features/moderation/store/moderators'; import { CustomPaginatorComponent, LoadingSpinnerComponent, SearchInputComponent } from '@shared/components'; +import { MOCK_USER } from '@shared/mocks'; + +import { ModeratorAddModel } from '../../models'; +import { ModeratorsSelectors } from '../../store/moderators'; import { AddModeratorDialogComponent } from './add-moderator-dialog.component'; +import { DynamicDialogRefMock } from '@testing/mocks/dynamic-dialog-ref.mock'; +import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideMockStore } from '@testing/providers/store-provider.mock'; + describe('AddModeratorDialogComponent', () => { let component: AddModeratorDialogComponent; let fixture: ComponentFixture; + let mockDialogRef: jest.Mocked; + let mockDialogConfig: jest.Mocked; + + const mockUsers = [MOCK_USER]; beforeEach(async () => { + mockDialogRef = DynamicDialogRefMock.useValue as unknown as jest.Mocked; + + mockDialogConfig = { + data: [], + } as jest.Mocked; + await TestBed.configureTestingModule({ imports: [ AddModeratorDialogComponent, - MockPipe(TranslatePipe), + OSFTestingModule, ...MockComponents(SearchInputComponent, LoadingSpinnerComponent, CustomPaginatorComponent), ], - teardown: { destroyAfterEach: false }, providers: [ - MockProviders(DynamicDialogRef, DynamicDialogConfig), - provideStore([ModeratorsState]), - provideHttpClient(), - provideHttpClientTesting(), + DynamicDialogRefMock, + MockProvider(DynamicDialogConfig, mockDialogConfig), + provideMockStore({ + signals: [ + { selector: ModeratorsSelectors.getUsers, value: mockUsers }, + { selector: ModeratorsSelectors.isUsersLoading, value: false }, + { selector: ModeratorsSelectors.getUsersTotalCount, value: 2 }, + ], + }), ], }).compileComponents(); fixture = TestBed.createComponent(AddModeratorDialogComponent); component = fixture.componentInstance; - fixture.detectChanges(); + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.useRealTimers(); }); it('should create', () => { + fixture.detectChanges(); expect(component).toBeTruthy(); }); + + it('should initialize with default values', () => { + fixture.detectChanges(); + + expect(component.isInitialState()).toBe(true); + expect(component.currentPage()).toBe(1); + expect(component.first()).toBe(0); + expect(component.rows()).toBe(10); + expect(component.selectedUsers()).toEqual([]); + expect(component.searchControl.value).toBe(''); + }); + + it('should load users on initialization', () => { + fixture.detectChanges(); + + expect(component.users()).toEqual(mockUsers); + expect(component.isLoading()).toBe(false); + expect(component.totalUsersCount()).toBe(2); + }); + + it('should add moderator', () => { + const mockSelectedUsers: ModeratorAddModel[] = [ + { + id: '1', + fullName: 'John Doe', + email: 'john@example.com', + permission: 'read' as any, + }, + ]; + component.selectedUsers.set(mockSelectedUsers); + + component.addModerator(); + + expect(mockDialogRef.close).toHaveBeenCalledWith({ + data: mockSelectedUsers, + type: 1, + }); + }); + + it('should invite moderator', () => { + component.inviteModerator(); + + expect(mockDialogRef.close).toHaveBeenCalledWith({ + data: [], + type: 2, + }); + }); + + it('should handle page change correctly', () => { + const mockEvent = { page: 1, first: 10, rows: 10 }; + const searchUsersSpy = jest.fn(); + component.actions = { + ...component.actions, + searchUsers: searchUsersSpy, + }; + + component.pageChanged(mockEvent); + + expect(component.currentPage()).toBe(2); + expect(component.first()).toBe(10); + expect(searchUsersSpy).toHaveBeenCalledWith('', 2); + }); + + it('should handle page change when page is null', () => { + const mockEvent = { page: undefined, first: 0, rows: 10 }; + const searchUsersSpy = jest.fn(); + component.actions = { + ...component.actions, + searchUsers: searchUsersSpy, + }; + + component.pageChanged(mockEvent); + + expect(component.currentPage()).toBe(1); + expect(component.first()).toBe(0); + expect(searchUsersSpy).toHaveBeenCalledWith('', 1); + }); + + it('should clear users on destroy', () => { + const clearUsersSpy = jest.fn(); + component.actions = { + ...component.actions, + clearUsers: clearUsersSpy, + }; + + component.ngOnDestroy(); + + expect(clearUsersSpy).toHaveBeenCalled(); + }); + + it('should have actions defined', () => { + expect(component.actions).toBeDefined(); + expect(component.actions.searchUsers).toBeDefined(); + expect(component.actions.clearUsers).toBeDefined(); + }); + + it('should handle search control value changes', () => { + jest.useFakeTimers(); + fixture.detectChanges(); + const searchUsersSpy = jest.fn().mockReturnValue(of({})); + component.actions = { + ...component.actions, + searchUsers: searchUsersSpy, + }; + + component.searchControl.setValue('test search'); + + jest.advanceTimersByTime(600); + + expect(searchUsersSpy).toHaveBeenCalledWith('test search', 1); + expect(component.isInitialState()).toBe(false); + expect(component.selectedUsers()).toEqual([]); + + jest.useRealTimers(); + }); + + it('should not search when search term is empty', () => { + fixture.detectChanges(); + const searchUsersSpy = jest.fn(); + component.actions = { + ...component.actions, + searchUsers: searchUsersSpy, + }; + + component.searchControl.setValue(''); + component.searchControl.setValue(' '); + + expect(searchUsersSpy).not.toHaveBeenCalled(); + }); }); diff --git a/src/app/features/moderation/components/bulk-upload/bulk-upload.component.spec.ts b/src/app/features/moderation/components/bulk-upload/bulk-upload.component.spec.ts index 13ccbe4d5..54dc588b9 100644 --- a/src/app/features/moderation/components/bulk-upload/bulk-upload.component.spec.ts +++ b/src/app/features/moderation/components/bulk-upload/bulk-upload.component.spec.ts @@ -1,20 +1,18 @@ -import { TranslatePipe } from '@ngx-translate/core'; -import { MockPipe } from 'ng-mocks'; - -import { provideHttpClient } from '@angular/common/http'; -import { provideHttpClientTesting } from '@angular/common/http/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { BYTES_IN_MB, FILE_TYPES } from '../../constants'; + import { BulkUploadComponent } from './bulk-upload.component'; +import { OSFTestingModule } from '@testing/osf.testing.module'; + describe('BulkUploadComponent', () => { let component: BulkUploadComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [BulkUploadComponent, MockPipe(TranslatePipe)], - providers: [provideHttpClient(), provideHttpClientTesting()], + imports: [BulkUploadComponent, OSFTestingModule], }).compileComponents(); fixture = TestBed.createComponent(BulkUploadComponent); @@ -25,4 +23,22 @@ describe('BulkUploadComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + it('should have default maxSize', () => { + expect(component.maxSize()).toBe(BYTES_IN_MB); + }); + + it('should have default acceptTypes', () => { + expect(component.acceptTypes()).toBe(FILE_TYPES.CSV); + }); + + it('should accept custom maxSize input', () => { + fixture.componentRef.setInput('maxSize', 5 * BYTES_IN_MB); + expect(component.maxSize()).toBe(5 * BYTES_IN_MB); + }); + + it('should accept custom acceptTypes input', () => { + fixture.componentRef.setInput('acceptTypes', '.xlsx'); + expect(component.acceptTypes()).toBe('.xlsx'); + }); }); diff --git a/src/app/features/moderation/components/collection-moderation-submissions/collection-moderation-submissions.component.spec.ts b/src/app/features/moderation/components/collection-moderation-submissions/collection-moderation-submissions.component.spec.ts index 7d1a590c8..81cca34f2 100644 --- a/src/app/features/moderation/components/collection-moderation-submissions/collection-moderation-submissions.component.spec.ts +++ b/src/app/features/moderation/components/collection-moderation-submissions/collection-moderation-submissions.component.spec.ts @@ -1,30 +1,48 @@ -import { provideStore } from '@ngxs/store'; +import { MockComponents, MockProvider } from 'ng-mocks'; -import { TranslatePipe } from '@ngx-translate/core'; -import { MockComponents, MockPipe } from 'ng-mocks'; - -import { of } from 'rxjs'; - -import { provideHttpClient } from '@angular/common/http'; -import { provideHttpClientTesting } from '@angular/common/http/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { ActivatedRoute } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; import { CollectionSubmissionsListComponent } from '@osf/features/moderation/components'; -import { CollectionsModerationState } from '@osf/features/moderation/store/collections-moderation'; +import { CollectionsSelectors } from '@osf/shared/stores'; import { CustomPaginatorComponent, IconComponent, LoadingSpinnerComponent, SelectComponent } from '@shared/components'; -import { CollectionsState } from '@shared/stores'; +import { MOCK_PROVIDER } from '@shared/mocks'; + +import { SubmissionReviewStatus } from '../../enums'; +import { CollectionsModerationSelectors } from '../../store/collections-moderation'; import { CollectionModerationSubmissionsComponent } from './collection-moderation-submissions.component'; +import { OSFTestingModule } from '@testing/osf.testing.module'; +import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock'; +import { RouterMockBuilder } from '@testing/providers/router-provider.mock'; +import { provideMockStore } from '@testing/providers/store-provider.mock'; + describe('CollectionModerationSubmissionsComponent', () => { let component: CollectionModerationSubmissionsComponent; let fixture: ComponentFixture; + let mockRouter: ReturnType; + let mockActivatedRoute: ReturnType; + + const mockCollectionProvider = MOCK_PROVIDER; + const mockSubmissions = [ + { + id: '1', + title: 'Test Submission', + status: SubmissionReviewStatus.Pending, + }, + ]; beforeEach(async () => { + mockRouter = RouterMockBuilder.create().build(); + mockActivatedRoute = ActivatedRouteMockBuilder.create() + .withQueryParams({ status: 'pending', sortBy: 'date_created', page: '1' }) + .build(); + await TestBed.configureTestingModule({ imports: [ CollectionModerationSubmissionsComponent, + OSFTestingModule, ...MockComponents( SelectComponent, CollectionSubmissionsListComponent, @@ -32,22 +50,19 @@ describe('CollectionModerationSubmissionsComponent', () => { CustomPaginatorComponent, LoadingSpinnerComponent ), - MockPipe(TranslatePipe), ], providers: [ - provideStore([CollectionsState, CollectionsModerationState]), - provideHttpClient(), - provideHttpClientTesting(), - { - provide: ActivatedRoute, - useValue: { - snapshot: { - paramMap: { get: () => '1' }, - queryParams: {}, - }, - queryParams: of({}), - }, - }, + MockProvider(Router, mockRouter), + MockProvider(ActivatedRoute, mockActivatedRoute), + provideMockStore({ + signals: [ + { selector: CollectionsSelectors.getCollectionProvider, value: mockCollectionProvider }, + { selector: CollectionsSelectors.getCollectionProviderLoading, value: false }, + { selector: CollectionsModerationSelectors.getCollectionSubmissionsLoading, value: false }, + { selector: CollectionsModerationSelectors.getCollectionSubmissions, value: mockSubmissions }, + { selector: CollectionsModerationSelectors.getCollectionSubmissionsTotalCount, value: 1 }, + ], + }), ], }).compileComponents(); @@ -59,4 +74,50 @@ describe('CollectionModerationSubmissionsComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + it('should initialize with default values', () => { + expect(component.reviewStatus()).toBe(SubmissionReviewStatus.Pending); + expect(component.currentPage()).toBe('1'); + expect(component.pageSize).toBe(10); + expect(component.selectedSortOption()).toBeDefined(); + }); + + it('should change review status', () => { + component.changeReviewStatus(SubmissionReviewStatus.Accepted); + expect(component.reviewStatus()).toBe(SubmissionReviewStatus.Accepted); + expect(component.currentPage()).toBe('1'); + }); + + it('should change sort option', () => { + component.changeSort('title'); + expect(component.selectedSortOption()).toBe('title'); + expect(component.currentPage()).toBe('1'); + }); + + it('should handle page change', () => { + const mockEvent = { page: 1, first: 10, rows: 10 }; + component.onPageChange(mockEvent); + expect(component.currentPage()).toBe('2'); + }); + + it('should handle page change when page is undefined', () => { + const mockEvent = { page: undefined, first: 0, rows: 10 }; + component.onPageChange(mockEvent); + expect(component.currentPage()).toBe('1'); + }); + + it('should compute firstIndex correctly', () => { + component.currentPage.set('3'); + expect(component.firstIndex()).toBe(20); + }); + + it('should compute isLoading correctly', () => { + expect(component.isLoading()).toBe(false); + }); + + it('should initialize from query params', () => { + expect(component.reviewStatus()).toBe(SubmissionReviewStatus.Pending); + expect(component.selectedSortOption()).toBe('date_created'); + expect(component.currentPage()).toBe('1'); + }); }); diff --git a/src/app/features/moderation/components/collection-submission-item/collection-submission-item.component.spec.ts b/src/app/features/moderation/components/collection-submission-item/collection-submission-item.component.spec.ts index d5c41db5a..2c1e68271 100644 --- a/src/app/features/moderation/components/collection-submission-item/collection-submission-item.component.spec.ts +++ b/src/app/features/moderation/components/collection-submission-item/collection-submission-item.component.spec.ts @@ -1,55 +1,161 @@ -import { provideStore } from '@ngxs/store'; +import { MockComponent, MockPipe, MockProvider } from 'ng-mocks'; -import { TranslatePipe } from '@ngx-translate/core'; -import { MockComponent, MockPipes } from 'ng-mocks'; - -import { of } from 'rxjs'; - -import { provideHttpClient } from '@angular/common/http'; -import { provideHttpClientTesting } from '@angular/common/http/testing'; -import { ComponentRef } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { ActivatedRoute } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; +import { CollectionSubmissionWithGuid } from '@osf/shared/models'; +import { CollectionsSelectors } from '@osf/shared/stores'; import { IconComponent } from '@shared/components'; import { DateAgoPipe } from '@shared/pipes'; -import { CollectionsState } from '@shared/stores'; + +import { SubmissionReviewStatus } from '../../enums'; import { CollectionSubmissionItemComponent } from './collection-submission-item.component'; -describe('SubmissionItemComponent', () => { +import { MOCK_COLLECTION_SUBMISSION_WITH_GUID } from '@testing/mocks/submission.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('CollectionSubmissionItemComponent', () => { let component: CollectionSubmissionItemComponent; - let componentRef: ComponentRef; let fixture: ComponentFixture; + let mockRouter: ReturnType; + let mockActivatedRoute: ReturnType; + + const mockSubmission: CollectionSubmissionWithGuid = MOCK_COLLECTION_SUBMISSION_WITH_GUID; + + const mockCollectionProvider = { + id: 'provider-123', + name: 'Test Provider', + }; beforeEach(async () => { + mockRouter = RouterMockBuilder.create().build(); + mockActivatedRoute = ActivatedRouteMockBuilder.create().withQueryParams({ status: 'pending' }).build(); + await TestBed.configureTestingModule({ - imports: [CollectionSubmissionItemComponent, MockComponent(IconComponent), MockPipes(DateAgoPipe, TranslatePipe)], + imports: [ + CollectionSubmissionItemComponent, + OSFTestingModule, + MockComponent(IconComponent), + MockPipe(DateAgoPipe), + ], providers: [ - { - provide: ActivatedRoute, - useValue: { - snapshot: { - paramMap: { get: () => '1' }, - queryParams: {}, - }, - queryParams: of({}), - }, - }, - provideStore([CollectionsState]), - provideHttpClient(), - provideHttpClientTesting(), + MockProvider(Router, mockRouter), + MockProvider(ActivatedRoute, mockActivatedRoute), + provideMockStore({ + signals: [{ selector: CollectionsSelectors.getCollectionProvider, value: mockCollectionProvider }], + }), ], }).compileComponents(); fixture = TestBed.createComponent(CollectionSubmissionItemComponent); component = fixture.componentInstance; - componentRef = fixture.componentRef; - componentRef.setInput('submission', {}); - fixture.detectChanges(); }); it('should create', () => { + fixture.componentRef.setInput('submission', mockSubmission); + fixture.detectChanges(); expect(component).toBeTruthy(); }); + + it('should initialize with submission input', () => { + fixture.componentRef.setInput('submission', mockSubmission); + fixture.detectChanges(); + + expect(component.submission()).toEqual(mockSubmission); + }); + + it('should compute current review action correctly', () => { + fixture.componentRef.setInput('submission', mockSubmission); + fixture.detectChanges(); + + const currentAction = component.currentReviewAction(); + expect(currentAction).toEqual(mockSubmission.actions![0]); + }); + + it('should return null for current review action when no actions exist', () => { + const submissionWithoutActions = { ...mockSubmission, actions: [] }; + fixture.componentRef.setInput('submission', submissionWithoutActions); + fixture.detectChanges(); + + const currentAction = component.currentReviewAction(); + expect(currentAction).toBeNull(); + }); + + it('should return null for current review action when actions is undefined', () => { + const submissionWithoutActions = { ...mockSubmission, actions: undefined }; + fixture.componentRef.setInput('submission', submissionWithoutActions); + fixture.detectChanges(); + + const currentAction = component.currentReviewAction(); + expect(currentAction).toBeNull(); + }); + + it('should compute current submission attributes correctly', () => { + fixture.componentRef.setInput('submission', mockSubmission); + fixture.detectChanges(); + + const attributes = component.currentSubmissionAttributes(); + expect(attributes).toBeDefined(); + expect(Array.isArray(attributes)).toBe(true); + }); + + it('should return attributes even when submission has no actions', () => { + const submissionWithoutActions = { ...mockSubmission, actions: [] }; + fixture.componentRef.setInput('submission', submissionWithoutActions); + fixture.detectChanges(); + + const attributes = component.currentSubmissionAttributes(); + expect(attributes).toBeDefined(); + expect(attributes).not.toBeNull(); + expect(Array.isArray(attributes)).toBe(true); + expect(attributes!.length).toBeGreaterThan(0); + }); + + it('should return attributes with filtered null values', () => { + const submissionWithNullFields = { ...mockSubmission, programArea: null, collectedType: null, dataType: null }; + fixture.componentRef.setInput('submission', submissionWithNullFields); + fixture.detectChanges(); + + const attributes = component.currentSubmissionAttributes(); + expect(attributes).toBeDefined(); + expect(attributes).not.toBeNull(); + expect(Array.isArray(attributes)).toBe(true); + expect(attributes!.length).toBeGreaterThan(0); + }); + + it('should handle navigation correctly', () => { + fixture.componentRef.setInput('submission', mockSubmission); + fixture.detectChanges(); + + component.handleNavigation(); + + expect(mockRouter.navigate).toHaveBeenCalledWith(['../', mockSubmission.nodeId], { + relativeTo: expect.any(Object), + queryParams: { status: 'pending', mode: 'moderation' }, + }); + }); + + it('should have SubmissionReviewStatus enum available', () => { + expect(component.SubmissionReviewStatus).toBe(SubmissionReviewStatus); + }); + + it('should load collection provider from store', () => { + fixture.componentRef.setInput('submission', mockSubmission); + fixture.detectChanges(); + + expect(component.collectionProvider()).toEqual(mockCollectionProvider); + }); + + it('should handle empty actions array', () => { + const submissionWithEmptyActions = { ...mockSubmission, actions: [] }; + fixture.componentRef.setInput('submission', submissionWithEmptyActions); + fixture.detectChanges(); + + const currentAction = component.currentReviewAction(); + expect(currentAction).toBeNull(); + }); }); diff --git a/src/app/features/moderation/components/collection-submission-overview/collection-submission-overview.component.spec.ts b/src/app/features/moderation/components/collection-submission-overview/collection-submission-overview.component.spec.ts index 6b2fe2e5c..6dfd3ea5e 100644 --- a/src/app/features/moderation/components/collection-submission-overview/collection-submission-overview.component.spec.ts +++ b/src/app/features/moderation/components/collection-submission-overview/collection-submission-overview.component.spec.ts @@ -1,20 +1,33 @@ -import { MockComponent, MockProvider } from 'ng-mocks'; +import { MockComponents } from 'ng-mocks'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { Router } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; import { ProjectOverviewComponent } from '@osf/features/project/overview/project-overview.component'; +import { Mode } from '@shared/enums'; import { CollectionSubmissionOverviewComponent } from './collection-submission-overview.component'; -describe.skip('CollectionSubmissionOverviewComponent', () => { +import { OSFTestingModule } from '@testing/osf.testing.module'; +import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock'; +import { RouterMockBuilder } from '@testing/providers/router-provider.mock'; + +describe('CollectionSubmissionOverviewComponent', () => { let component: CollectionSubmissionOverviewComponent; let fixture: ComponentFixture; + let mockRouter: ReturnType; + let mockActivatedRoute: ReturnType; beforeEach(async () => { + mockRouter = RouterMockBuilder.create().build(); + mockActivatedRoute = ActivatedRouteMockBuilder.create().withQueryParams({ mode: Mode.Moderation }).build(); + await TestBed.configureTestingModule({ - imports: [CollectionSubmissionOverviewComponent, MockComponent(ProjectOverviewComponent)], - providers: [MockProvider(Router)], + imports: [CollectionSubmissionOverviewComponent, OSFTestingModule, ...MockComponents(ProjectOverviewComponent)], + providers: [ + { provide: Router, useValue: mockRouter }, + { provide: ActivatedRoute, useValue: mockActivatedRoute }, + ], }).compileComponents(); fixture = TestBed.createComponent(CollectionSubmissionOverviewComponent); @@ -25,4 +38,30 @@ describe.skip('CollectionSubmissionOverviewComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + it('should compute isModerationMode correctly when mode is moderation', () => { + expect(component.isModerationMode()).toBe(true); + }); + + it('should compute isModerationMode correctly when mode is not moderation', () => { + mockActivatedRoute.snapshot!.queryParams = { mode: 'other' }; + fixture = TestBed.createComponent(CollectionSubmissionOverviewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + + expect(component.isModerationMode()).toBe(false); + }); + + it('should navigate away when not in moderation mode', () => { + mockActivatedRoute.snapshot!.queryParams = { mode: 'other' }; + fixture = TestBed.createComponent(CollectionSubmissionOverviewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + + expect(mockRouter.navigate).toHaveBeenCalledWith(['../'], { relativeTo: mockActivatedRoute }); + }); + + it('should not navigate when in moderation mode', () => { + expect(mockRouter.navigate).not.toHaveBeenCalled(); + }); }); diff --git a/src/app/features/moderation/components/collection-submissions-list/collection-submissions-list.component.spec.ts b/src/app/features/moderation/components/collection-submissions-list/collection-submissions-list.component.spec.ts index 3ec050120..e86fbea75 100644 --- a/src/app/features/moderation/components/collection-submissions-list/collection-submissions-list.component.spec.ts +++ b/src/app/features/moderation/components/collection-submissions-list/collection-submissions-list.component.spec.ts @@ -1,31 +1,68 @@ -import { provideStore } from '@ngxs/store'; +import { MockComponent } from 'ng-mocks'; -import { TranslatePipe } from '@ngx-translate/core'; -import { MockPipe } from 'ng-mocks'; - -import { provideHttpClient } from '@angular/common/http'; -import { provideHttpClientTesting } from '@angular/common/http/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { CollectionSubmissionsListComponent } from '@osf/features/moderation/components'; -import { CollectionsModerationState } from '@osf/features/moderation/store/collections-moderation'; +import { CollectionSubmissionItemComponent } from '@osf/features/moderation/components'; + +import { CollectionsModerationSelectors } from '../../store/collections-moderation'; + +import { CollectionSubmissionsListComponent } from './collection-submissions-list.component'; -describe('SubmissionsListComponent', () => { +import { MOCK_COLLECTION_SUBMISSION_WITH_GUID } from '@testing/mocks/submission.mock'; +import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideMockStore } from '@testing/providers/store-provider.mock'; + +describe('CollectionSubmissionsListComponent', () => { let component: CollectionSubmissionsListComponent; let fixture: ComponentFixture; + const mockSubmissions = [MOCK_COLLECTION_SUBMISSION_WITH_GUID]; + beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [CollectionSubmissionsListComponent, MockPipe(TranslatePipe)], - providers: [provideStore([CollectionsModerationState]), provideHttpClient(), provideHttpClientTesting()], + imports: [CollectionSubmissionsListComponent, OSFTestingModule, MockComponent(CollectionSubmissionItemComponent)], + providers: [ + provideMockStore({ + signals: [{ selector: CollectionsModerationSelectors.getCollectionSubmissions, value: mockSubmissions }], + }), + ], }).compileComponents(); fixture = TestBed.createComponent(CollectionSubmissionsListComponent); component = fixture.componentInstance; - fixture.detectChanges(); }); it('should create', () => { + fixture.detectChanges(); expect(component).toBeTruthy(); }); + + it('should load submissions from store', () => { + fixture.detectChanges(); + + expect(component.submissions()).toEqual(mockSubmissions); + }); + + it('should handle empty submissions list', () => { + Object.defineProperty(component, 'submissions', { + value: () => [], + writable: true, + }); + + fixture.detectChanges(); + + expect(component.submissions()).toEqual([]); + }); + + it('should handle empty submissions array', () => { + Object.defineProperty(component, 'submissions', { + value: () => [], + writable: true, + }); + + fixture.detectChanges(); + + expect(component.submissions()).toEqual([]); + expect(component.submissions().length).toBe(0); + }); }); diff --git a/src/app/features/moderation/components/invite-moderator-dialog/invite-moderator-dialog.component.spec.ts b/src/app/features/moderation/components/invite-moderator-dialog/invite-moderator-dialog.component.spec.ts index 998d1da48..238e7689f 100644 --- a/src/app/features/moderation/components/invite-moderator-dialog/invite-moderator-dialog.component.spec.ts +++ b/src/app/features/moderation/components/invite-moderator-dialog/invite-moderator-dialog.component.spec.ts @@ -1,27 +1,33 @@ -import { TranslatePipe } from '@ngx-translate/core'; -import { MockComponents, MockPipe, MockProvider } from 'ng-mocks'; +import { MockComponents } from 'ng-mocks'; import { DynamicDialogRef } from 'primeng/dynamicdialog'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { ActivatedRoute } from '@angular/router'; import { FormSelectComponent, TextInputComponent } from '@shared/components'; +import { ModeratorPermission } from '../../enums'; + import { InviteModeratorDialogComponent } from './invite-moderator-dialog.component'; +import { DynamicDialogRefMock } from '@testing/mocks/dynamic-dialog-ref.mock'; +import { OSFTestingModule } from '@testing/osf.testing.module'; + describe('InviteModeratorDialogComponent', () => { let component: InviteModeratorDialogComponent; let fixture: ComponentFixture; + let mockDialogRef: jest.Mocked; beforeEach(async () => { + mockDialogRef = DynamicDialogRefMock.useValue as unknown as jest.Mocked; + await TestBed.configureTestingModule({ imports: [ InviteModeratorDialogComponent, + OSFTestingModule, ...MockComponents(TextInputComponent, FormSelectComponent), - MockPipe(TranslatePipe), ], - providers: [MockProvider(DynamicDialogRef), MockProvider(ActivatedRoute)], + providers: [DynamicDialogRefMock], }).compileComponents(); fixture = TestBed.createComponent(InviteModeratorDialogComponent); @@ -29,7 +35,90 @@ describe('InviteModeratorDialogComponent', () => { fixture.detectChanges(); }); + afterEach(() => { + jest.clearAllMocks(); + }); + it('should create', () => { expect(component).toBeTruthy(); }); + + it('should initialize form with default values', () => { + expect(component.moderatorForm).toBeDefined(); + expect(component.moderatorForm.get('fullName')?.value).toBe(''); + expect(component.moderatorForm.get('email')?.value).toBe(''); + expect(component.moderatorForm.get('permission')?.value).toBe(ModeratorPermission.Moderator); + }); + + it('should have input limits defined', () => { + expect(component.inputLimits).toBeDefined(); + }); + + it('should have permissions options defined', () => { + expect(component.permissionsOptions).toBeDefined(); + expect(component.permissionsOptions.length).toBeGreaterThan(0); + }); + + it('should search moderator and close dialog', () => { + component.searchModerator(); + + expect(mockDialogRef.close).toHaveBeenCalledWith({ + data: [], + type: 1, + }); + }); + + it('should submit form with valid data and close dialog', () => { + component.moderatorForm.patchValue({ + fullName: 'John Doe', + email: 'john@example.com', + permission: ModeratorPermission.Admin, + }); + + component.submit(); + + expect(mockDialogRef.close).toHaveBeenCalledWith({ + data: [ + { + fullName: 'John Doe', + email: 'john@example.com', + permission: ModeratorPermission.Admin, + }, + ], + type: 2, + }); + }); + + it('should not submit form with invalid data', () => { + component.moderatorForm.patchValue({ + fullName: '', + email: 'invalid-email', + permission: ModeratorPermission.Moderator, + }); + + component.submit(); + + expect(mockDialogRef.close).not.toHaveBeenCalled(); + }); + + it('should use default permission when not provided', () => { + component.moderatorForm.patchValue({ + fullName: 'John Doe', + email: 'john@example.com', + permission: undefined, + }); + + component.submit(); + + expect(mockDialogRef.close).toHaveBeenCalledWith({ + data: [ + { + fullName: 'John Doe', + email: 'john@example.com', + permission: ModeratorPermission.Moderator, + }, + ], + type: 2, + }); + }); }); diff --git a/src/app/features/moderation/components/moderators-list/moderators-list.component.spec.ts b/src/app/features/moderation/components/moderators-list/moderators-list.component.spec.ts index 8dc0f1d3d..4c071fc7a 100644 --- a/src/app/features/moderation/components/moderators-list/moderators-list.component.spec.ts +++ b/src/app/features/moderation/components/moderators-list/moderators-list.component.spec.ts @@ -1,59 +1,207 @@ -import { provideStore } 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 { provideHttpClient } from '@angular/common/http'; -import { provideHttpClientTesting } from '@angular/common/http/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ActivatedRoute } from '@angular/router'; -import { UserState } from '@core/store/user'; +import { UserSelectors } from '@core/store/user'; import { ModeratorsTableComponent } from '@osf/features/moderation/components'; -import { ModeratorsState } from '@osf/features/moderation/store/moderators'; +import { ResourceType } from '@osf/shared/enums'; import { SearchInputComponent } from '@shared/components'; -import { MockCustomConfirmationServiceProvider, TranslateServiceMock } from '@shared/mocks'; -import { ToastService } from '@shared/services'; +import { MOCK_USER, TranslateServiceMock } from '@shared/mocks'; +import { CustomConfirmationService } from '@shared/services'; + +import { ModeratorPermission } from '../../enums'; +import { ModeratorModel } from '../../models'; +import { ModeratorsSelectors } from '../../store/moderators'; import { ModeratorsListComponent } from './moderators-list.component'; +import { MOCK_MODERATORS } from '@testing/mocks/moderator.mock'; +import { OSFTestingModule } from '@testing/osf.testing.module'; +import { CustomConfirmationServiceMockBuilder } from '@testing/providers/custom-confirmation-provider.mock'; +import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock'; +import { provideMockStore } from '@testing/providers/store-provider.mock'; + describe('ModeratorsListComponent', () => { let component: ModeratorsListComponent; let fixture: ComponentFixture; + let mockActivatedRoute: ReturnType; + let customConfirmationServiceMock: ReturnType; + + const mockProviderId = 'test-provider-123'; + const mockResourceType = ResourceType.Preprint; + const mockCurrentUser = MOCK_USER; - const mockRoute = { - data: of({ resourceType: undefined }), - }; + const mockModerators: ModeratorModel[] = MOCK_MODERATORS; beforeEach(async () => { + mockActivatedRoute = ActivatedRouteMockBuilder.create() + .withParams({ providerId: mockProviderId }) + .withData({ resourceType: mockResourceType }) + .build(); + customConfirmationServiceMock = CustomConfirmationServiceMockBuilder.create().build(); + await TestBed.configureTestingModule({ imports: [ ModeratorsListComponent, + OSFTestingModule, ...MockComponents(ModeratorsTableComponent, SearchInputComponent), - MockPipe(TranslatePipe), ], providers: [ + MockProvider(ActivatedRoute, mockActivatedRoute), + MockProvider(CustomConfirmationService, customConfirmationServiceMock), TranslateServiceMock, - MockCustomConfirmationServiceProvider, - MockProvider(ToastService), - { - provide: ActivatedRoute, - useValue: mockRoute, - }, - provideStore([ModeratorsState, UserState]), - provideHttpClient(), - provideHttpClientTesting(), + provideMockStore({ + signals: [ + { selector: UserSelectors.getCurrentUser, value: mockCurrentUser }, + { selector: ModeratorsSelectors.getModerators, value: mockModerators }, + { selector: ModeratorsSelectors.isModeratorsLoading, value: false }, + ], + }), ], }).compileComponents(); fixture = TestBed.createComponent(ModeratorsListComponent); component = fixture.componentInstance; - fixture.detectChanges(); }); it('should create', () => { + fixture.detectChanges(); expect(component).toBeTruthy(); }); + + it('should initialize with correct values', () => { + fixture.detectChanges(); + + expect(component.providerId()).toBe(mockProviderId); + expect(component.resourceType()).toBe(mockResourceType); + expect(component.searchControl.value).toBe(''); + expect(component.moderators()).toEqual(mockModerators); + expect(component.isModeratorsLoading()).toBe(false); + expect(component.currentUser()).toEqual(mockCurrentUser); + }); + + it('should return false for admin moderator when user is not admin', () => { + const nonAdminModerators = mockModerators.map((mod) => ({ + ...mod, + permission: ModeratorPermission.Moderator, + })); + + Object.defineProperty(component, 'initialModerators', { + value: () => nonAdminModerators, + writable: true, + }); + + fixture.detectChanges(); + + expect(component.isCurrentUserAdminModerator()).toBe(false); + }); + + it('should return false for admin moderator when user is not found', () => { + Object.defineProperty(component, 'currentUser', { + value: () => null, + writable: true, + }); + + fixture.detectChanges(); + + expect(component.isCurrentUserAdminModerator()).toBe(false); + }); + + it('should load moderators on initialization', () => { + const loadModeratorsSpy = jest.fn(); + component.actions = { + ...component.actions, + loadModerators: loadModeratorsSpy, + }; + + component.ngOnInit(); + + expect(loadModeratorsSpy).toHaveBeenCalledWith(mockProviderId, mockResourceType); + }); + + it('should set search subscription on initialization', () => { + const setSearchSubscriptionSpy = jest.fn(); + (component as any).setSearchSubscription = setSearchSubscriptionSpy; + + component.ngOnInit(); + + expect(setSearchSubscriptionSpy).toHaveBeenCalled(); + }); + + it('should handle search control value changes', () => { + jest.useFakeTimers(); + fixture.detectChanges(); + const updateSearchValueSpy = jest.fn(); + const loadModeratorsSpy = jest.fn().mockReturnValue(of({})); + component.actions = { + ...component.actions, + updateSearchValue: updateSearchValueSpy, + loadModerators: loadModeratorsSpy, + }; + + component.searchControl.setValue('test search'); + + jest.advanceTimersByTime(600); + + expect(updateSearchValueSpy).toHaveBeenCalledWith('test search'); + expect(loadModeratorsSpy).toHaveBeenCalledWith(mockProviderId, mockResourceType); + + jest.useRealTimers(); + }); + + it('should handle empty search value', () => { + jest.useFakeTimers(); + fixture.detectChanges(); + const updateSearchValueSpy = jest.fn(); + const loadModeratorsSpy = jest.fn().mockReturnValue(of({})); + component.actions = { + ...component.actions, + updateSearchValue: updateSearchValueSpy, + loadModerators: loadModeratorsSpy, + }; + + component.searchControl.setValue(''); + + jest.advanceTimersByTime(600); + + expect(updateSearchValueSpy).toHaveBeenCalledWith(null); + expect(loadModeratorsSpy).toHaveBeenCalledWith(mockProviderId, mockResourceType); + + jest.useRealTimers(); + }); + + it('should have actions defined', () => { + expect(component.actions).toBeDefined(); + expect(component.actions.loadModerators).toBeDefined(); + expect(component.actions.updateSearchValue).toBeDefined(); + expect(component.actions.addModerators).toBeDefined(); + expect(component.actions.updateModerator).toBeDefined(); + expect(component.actions.deleteModerator).toBeDefined(); + }); + + it('should update moderators when initial moderators change', () => { + const newModerators = [ + ...mockModerators, + { + id: '3', + userId: 'user-3', + fullName: 'Bob Wilson', + email: 'bob@example.com', + permission: ModeratorPermission.Moderator, + isActive: true, + }, + ]; + + Object.defineProperty(component, 'initialModerators', { + value: () => newModerators, + writable: true, + }); + + fixture.detectChanges(); + + expect(component.moderators()).toEqual(newModerators); + }); }); diff --git a/src/app/features/moderation/components/moderators-table/moderators-table.component.spec.ts b/src/app/features/moderation/components/moderators-table/moderators-table.component.spec.ts index 42b937eb0..0c61a421b 100644 --- a/src/app/features/moderation/components/moderators-table/moderators-table.component.spec.ts +++ b/src/app/features/moderation/components/moderators-table/moderators-table.component.spec.ts @@ -1,29 +1,113 @@ -import { TranslatePipe } from '@ngx-translate/core'; -import { MockComponent, MockPipe } from 'ng-mocks'; +import { MockComponent } from 'ng-mocks'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { SelectComponent } from '@shared/components'; -import { TranslateServiceMock } from '@shared/mocks'; + +import { ModeratorModel } from '../../models'; import { ModeratorsTableComponent } from './moderators-table.component'; +import { MOCK_MODERATORS } from '@testing/mocks/moderator.mock'; +import { OSFTestingModule } from '@testing/osf.testing.module'; + describe('ModeratorsTableComponent', () => { let component: ModeratorsTableComponent; let fixture: ComponentFixture; + const mockModerators: ModeratorModel[] = MOCK_MODERATORS; + beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [ModeratorsTableComponent, MockComponent(SelectComponent), MockPipe(TranslatePipe)], - providers: [TranslateServiceMock], + imports: [ModeratorsTableComponent, OSFTestingModule, MockComponent(SelectComponent)], }).compileComponents(); fixture = TestBed.createComponent(ModeratorsTableComponent); component = fixture.componentInstance; - fixture.detectChanges(); }); it('should create', () => { + fixture.detectChanges(); expect(component).toBeTruthy(); }); + + it('should handle input values correctly', () => { + fixture.componentRef.setInput('items', mockModerators); + fixture.componentRef.setInput('isLoading', true); + fixture.componentRef.setInput('currentUserId', 'current-user-123'); + fixture.componentRef.setInput('isCurrentUserAdminModerator', true); + + fixture.detectChanges(); + + expect(component.items()).toEqual(mockModerators); + expect(component.isLoading()).toBe(true); + expect(component.currentUserId()).toBe('current-user-123'); + expect(component.isCurrentUserAdminModerator()).toBe(true); + }); + + it('should emit update event when updatePermission is called', () => { + jest.spyOn(component.update, 'emit'); + const moderator = mockModerators[0]; + + component.updatePermission(moderator); + + expect(component.update.emit).toHaveBeenCalledWith(moderator); + }); + + it('should emit remove event when removeModerator is called', () => { + jest.spyOn(component.remove, 'emit'); + const moderator = mockModerators[0]; + + component.removeModerator(moderator); + + expect(component.remove.emit).toHaveBeenCalledWith(moderator); + }); + + it('should have skeleton data for loading state', () => { + expect(component.skeletonData).toBeDefined(); + expect(component.skeletonData.length).toBe(3); + expect(component.skeletonData.every((item) => typeof item === 'object')).toBe(true); + }); + + it('should have dialog service injected', () => { + expect(component.dialogService).toBeDefined(); + }); + + it('should have translate service injected', () => { + expect(component.translateService).toBeDefined(); + }); + + it('should open education history dialog', () => { + const moderator = mockModerators[0]; + jest.spyOn(component.dialogService, 'open'); + + component.openEducationHistory(moderator); + + expect(component.dialogService.open).toHaveBeenCalled(); + }); + + it('should open employment history dialog', () => { + const moderator = mockModerators[0]; + jest.spyOn(component.dialogService, 'open'); + + component.openEmploymentHistory(moderator); + + expect(component.dialogService.open).toHaveBeenCalled(); + }); + + it('should handle empty items array', () => { + fixture.componentRef.setInput('items', []); + + fixture.detectChanges(); + + expect(component.items()).toEqual([]); + }); + + it('should handle undefined currentUserId', () => { + fixture.componentRef.setInput('currentUserId', undefined); + + fixture.detectChanges(); + + expect(component.currentUserId()).toBeUndefined(); + }); }); diff --git a/src/app/features/moderation/components/my-reviewing-navigation/my-reviewing-navigation.component.spec.ts b/src/app/features/moderation/components/my-reviewing-navigation/my-reviewing-navigation.component.spec.ts index 3faaac3a5..122953240 100644 --- a/src/app/features/moderation/components/my-reviewing-navigation/my-reviewing-navigation.component.spec.ts +++ b/src/app/features/moderation/components/my-reviewing-navigation/my-reviewing-navigation.component.spec.ts @@ -1,34 +1,50 @@ -import { TranslatePipe } from '@ngx-translate/core'; -import { MockPipe, MockProvider } from 'ng-mocks'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { of } from 'rxjs'; +import { MOCK_PROVIDER } from '@shared/mocks'; -import { ComponentRef } from '@angular/core'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { ActivatedRoute } from '@angular/router'; +import { PreprintModerationTab } from '../../enums'; import { MyReviewingNavigationComponent } from './my-reviewing-navigation.component'; +import { OSFTestingModule } from '@testing/osf.testing.module'; + describe('MyReviewingNavigationComponent', () => { let component: MyReviewingNavigationComponent; - let componentRef: ComponentRef; let fixture: ComponentFixture; + const mockProvider = MOCK_PROVIDER; + beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [MyReviewingNavigationComponent, MockPipe(TranslatePipe)], - providers: [MockProvider(ActivatedRoute, { params: of({}) })], + imports: [MyReviewingNavigationComponent, OSFTestingModule], }).compileComponents(); fixture = TestBed.createComponent(MyReviewingNavigationComponent); component = fixture.componentInstance; - componentRef = fixture.componentRef; - - componentRef.setInput('provider', {}); + fixture.componentRef.setInput('provider', mockProvider); fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); + + it('should have provider input', () => { + expect(component.provider()).toEqual(mockProvider); + }); + + it('should have tab options defined', () => { + expect(component.tabOptions).toBeDefined(); + expect(component.tabOptions.length).toBeGreaterThan(0); + }); + + it('should have tab option enum defined', () => { + expect(component.tabOption).toBe(PreprintModerationTab); + }); + + it('should accept custom provider input', () => { + const customProvider = { ...mockProvider, name: 'Custom Provider' }; + fixture.componentRef.setInput('provider', customProvider); + expect(component.provider()).toEqual(customProvider); + }); }); diff --git a/src/app/features/moderation/components/notification-settings/notification-settings.component.spec.ts b/src/app/features/moderation/components/notification-settings/notification-settings.component.spec.ts index 993d6128d..a5411f8d7 100644 --- a/src/app/features/moderation/components/notification-settings/notification-settings.component.spec.ts +++ b/src/app/features/moderation/components/notification-settings/notification-settings.component.spec.ts @@ -1,21 +1,16 @@ -import { TranslatePipe } from '@ngx-translate/core'; -import { MockPipe, MockProvider } from 'ng-mocks'; - -import { of } from 'rxjs'; - import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { ActivatedRoute } from '@angular/router'; import { NotificationSettingsComponent } from './notification-settings.component'; +import { OSFTestingModule } from '@testing/osf.testing.module'; + describe('NotificationSettingsComponent', () => { let component: NotificationSettingsComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [NotificationSettingsComponent, MockPipe(TranslatePipe)], - providers: [MockProvider(ActivatedRoute, { params: of({}) })], + imports: [NotificationSettingsComponent, OSFTestingModule], }).compileComponents(); fixture = TestBed.createComponent(NotificationSettingsComponent); diff --git a/src/app/features/moderation/components/preprint-moderation-settings/preprint-moderation-settings.component.spec.ts b/src/app/features/moderation/components/preprint-moderation-settings/preprint-moderation-settings.component.spec.ts index c38906232..7872b7d22 100644 --- a/src/app/features/moderation/components/preprint-moderation-settings/preprint-moderation-settings.component.spec.ts +++ b/src/app/features/moderation/components/preprint-moderation-settings/preprint-moderation-settings.component.spec.ts @@ -1,30 +1,45 @@ -import { provideStore } from '@ngxs/store'; +import { MockComponent, MockProvider } from 'ng-mocks'; -import { TranslatePipe } from '@ngx-translate/core'; -import { MockComponent, MockPipe, MockProvider } from 'ng-mocks'; - -import { provideHttpClient } from '@angular/common/http'; -import { provideHttpClientTesting } from '@angular/common/http/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ActivatedRoute } from '@angular/router'; -import { PreprintModerationState } from '@osf/features/moderation/store/preprint-moderation'; import { LoadingSpinnerComponent } from '@shared/components'; +import { SettingsSectionControl } from '../../enums'; +import { PreprintProviderModerationInfo } from '../../models'; +import { PreprintModerationSelectors } from '../../store/preprint-moderation'; + import { PreprintModerationSettingsComponent } from './preprint-moderation-settings.component'; +import { EnvironmentTokenMock } from '@testing/mocks/environment.token.mock'; +import { MOCK_PREPRINT_PROVIDER_MODERATION_INFO } from '@testing/mocks/preprint-provider-moderation-info.mock'; +import { OSFTestingModule } from '@testing/osf.testing.module'; +import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock'; +import { provideMockStore } from '@testing/providers/store-provider.mock'; + describe('PreprintModerationSettingsComponent', () => { let component: PreprintModerationSettingsComponent; let fixture: ComponentFixture; + let mockActivatedRoute: ReturnType; + + const mockProviderId = 'test-provider-id'; + const mockSettings: PreprintProviderModerationInfo = MOCK_PREPRINT_PROVIDER_MODERATION_INFO; beforeEach(async () => { + mockActivatedRoute = ActivatedRouteMockBuilder.create().withParams({ providerId: mockProviderId }).build(); + await TestBed.configureTestingModule({ - imports: [PreprintModerationSettingsComponent, MockPipe(TranslatePipe), MockComponent(LoadingSpinnerComponent)], + imports: [PreprintModerationSettingsComponent, OSFTestingModule, MockComponent(LoadingSpinnerComponent)], providers: [ - MockProvider(ActivatedRoute), - provideStore([PreprintModerationState]), - provideHttpClient(), - provideHttpClientTesting(), + MockProvider(ActivatedRoute, mockActivatedRoute), + EnvironmentTokenMock, + provideMockStore({ + signals: [ + { selector: PreprintModerationSelectors.arePreprintProviderLoading, value: false }, + { selector: PreprintModerationSelectors.getPreprintProviders, value: [mockSettings] }, + { selector: PreprintModerationSelectors.getPreprintProvider, value: (_id: string) => mockSettings }, + ], + }), ], }).compileComponents(); @@ -36,4 +51,40 @@ describe('PreprintModerationSettingsComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + it('should initialize form on ngOnInit', () => { + component.ngOnInit(); + expect(component.settingsForm).toBeDefined(); + expect(component.settingsForm.get(SettingsSectionControl.ModerationType)).toBeDefined(); + expect(component.settingsForm.get(SettingsSectionControl.CommentVisibility)).toBeDefined(); + expect(component.settingsForm.get(SettingsSectionControl.ModeratorComments)).toBeDefined(); + }); + + it('should have providerId from route params', () => { + expect(component.providerId()).toBe(mockProviderId); + }); + + it('should compute settings correctly', () => { + expect(component.settings()).toBeDefined(); + }); + + it('should compute isLoading correctly', () => { + expect(component.isLoading()).toBe(false); + }); + + it('should initialize form with settings values', () => { + component.ngOnInit(); + expect(component.settingsForm.get(SettingsSectionControl.ModerationType)?.value).toBe(mockSettings.reviewsWorkflow); + expect(component.settingsForm.get(SettingsSectionControl.CommentVisibility)?.value).toBe( + mockSettings.reviewsCommentsPrivate + ); + expect(component.settingsForm.get(SettingsSectionControl.ModeratorComments)?.value).toBe( + mockSettings.reviewsCommentsAnonymous + ); + }); + + it('should disable form initially', () => { + component.ngOnInit(); + expect(component.settingsForm.disabled).toBe(true); + }); }); diff --git a/src/app/features/moderation/components/preprint-recent-activity-list/preprint-recent-activity-list.component.spec.ts b/src/app/features/moderation/components/preprint-recent-activity-list/preprint-recent-activity-list.component.spec.ts index b3332b1ef..c1baca4ba 100644 --- a/src/app/features/moderation/components/preprint-recent-activity-list/preprint-recent-activity-list.component.spec.ts +++ b/src/app/features/moderation/components/preprint-recent-activity-list/preprint-recent-activity-list.component.spec.ts @@ -1,37 +1,101 @@ -import { TranslatePipe } from '@ngx-translate/core'; -import { MockComponents, MockPipes } from 'ng-mocks'; +import { MockComponents } from 'ng-mocks'; -import { DatePipe } from '@angular/common'; -import { ComponentRef } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { CustomPaginatorComponent, IconComponent } from '@osf/shared/components'; +import { PreprintReviewActionModel } from '../../models'; + import { PreprintRecentActivityListComponent } from './preprint-recent-activity-list.component'; +import { MOCK_PREPRINT_REVIEW_ACTIONS } from '@testing/mocks/preprint-review-action.mock'; +import { OSFTestingModule } from '@testing/osf.testing.module'; + describe('PreprintRecentActivityListComponent', () => { let component: PreprintRecentActivityListComponent; - let componentRef: ComponentRef; let fixture: ComponentFixture; + const mockReviews: PreprintReviewActionModel[] = MOCK_PREPRINT_REVIEW_ACTIONS; + beforeEach(async () => { await TestBed.configureTestingModule({ imports: [ PreprintRecentActivityListComponent, + OSFTestingModule, ...MockComponents(IconComponent, CustomPaginatorComponent), - MockPipes(TranslatePipe, DatePipe), ], }).compileComponents(); fixture = TestBed.createComponent(PreprintRecentActivityListComponent); component = fixture.componentInstance; - componentRef = fixture.componentRef; - - componentRef.setInput('reviews', []); + fixture.componentRef.setInput('reviews', mockReviews); + fixture.componentRef.setInput('isLoading', false); + fixture.componentRef.setInput('totalCount', 1); fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); + + it('should have input values', () => { + expect(component.reviews()).toEqual(mockReviews); + expect(component.isLoading()).toBe(false); + expect(component.totalCount()).toBe(1); + }); + + it('should have default first value', () => { + expect(component.first()).toBe(0); + }); + + it('should have constants defined', () => { + expect(component.reviewStatusIcon).toBeDefined(); + expect(component.preprintReviewStatus).toBeDefined(); + }); + + it('should emit page change event', () => { + jest.spyOn(component.pageChanged, 'emit'); + const mockEvent = { page: 2, first: 10, rows: 10 }; + + component.onPageChange(mockEvent); + + expect(component.pageChanged.emit).toHaveBeenCalledWith(2); + }); + + it('should emit page 1 when page is undefined', () => { + jest.spyOn(component.pageChanged, 'emit'); + const mockEvent = { page: undefined, first: 0, rows: 10 }; + + component.onPageChange(mockEvent); + + expect(component.pageChanged.emit).toHaveBeenCalledWith(1); + }); + + it('should accept custom input values', () => { + const customReviews = [ + ...mockReviews, + { + id: '2', + fromState: 'pending', + toState: 'approved', + dateModified: '2023-01-02', + creator: { id: 'user-2', name: 'Jane Doe' }, + preprint: { + id: 'preprint-2', + name: 'Test Preprint 2', + }, + provider: { + id: 'provider-2', + name: 'Test Provider 2', + }, + }, + ]; + fixture.componentRef.setInput('reviews', customReviews); + fixture.componentRef.setInput('isLoading', true); + fixture.componentRef.setInput('totalCount', 2); + + expect(component.reviews()).toEqual(customReviews); + expect(component.isLoading()).toBe(true); + expect(component.totalCount()).toBe(2); + }); }); diff --git a/src/app/features/moderation/components/preprint-submission-item/preprint-submission-item.component.spec.ts b/src/app/features/moderation/components/preprint-submission-item/preprint-submission-item.component.spec.ts index e06b5e076..11749a014 100644 --- a/src/app/features/moderation/components/preprint-submission-item/preprint-submission-item.component.spec.ts +++ b/src/app/features/moderation/components/preprint-submission-item/preprint-submission-item.component.spec.ts @@ -1,54 +1,104 @@ import { TranslatePipe } from '@ngx-translate/core'; -import { MockComponent, MockPipes } from 'ng-mocks'; +import { MockComponents, MockPipes } from 'ng-mocks'; -import { ComponentRef } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { IconComponent } from '@shared/components'; -import { DateAgoPipe } from '@shared/pipes'; +import { IconComponent } from '@osf/shared/components'; +import { DateAgoPipe } from '@osf/shared/pipes'; + +import { SubmissionReviewStatus } from '../../enums'; +import { PreprintSubmission } from '../../models'; import { PreprintSubmissionItemComponent } from './preprint-submission-item.component'; +import { MOCK_PREPRINT_SUBMISSION } from '@testing/mocks/submission.mock'; +import { OSFTestingModule } from '@testing/osf.testing.module'; + describe('PreprintSubmissionItemComponent', () => { let component: PreprintSubmissionItemComponent; - let componentRef: ComponentRef; let fixture: ComponentFixture; - const mockSubmission = { - id: 'reg1', - title: 'Test registry submission', - reviewsState: 'pending', - public: true, - embargoed: false, - embargoEndDate: null, - actions: [ - { - id: 'a1', - fromState: 'pending', - toState: 'accepted', - dateModified: new Date().toISOString(), - creator: { id: 'u1', name: 'Bob' }, - comment: '', - }, - ], - }; + const mockSubmission: PreprintSubmission = MOCK_PREPRINT_SUBMISSION; beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [PreprintSubmissionItemComponent, MockComponent(IconComponent), MockPipes(DateAgoPipe, TranslatePipe)], + imports: [ + PreprintSubmissionItemComponent, + OSFTestingModule, + ...MockComponents(IconComponent), + MockPipes(DateAgoPipe, TranslatePipe), + ], }).compileComponents(); fixture = TestBed.createComponent(PreprintSubmissionItemComponent); component = fixture.componentInstance; - componentRef = fixture.componentRef; - - componentRef.setInput('status', 'pending'); - componentRef.setInput('submission', mockSubmission); - + fixture.componentRef.setInput('status', SubmissionReviewStatus.Pending); + fixture.componentRef.setInput('submission', mockSubmission); fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); + + it('should have input values', () => { + expect(component.status()).toBe(SubmissionReviewStatus.Pending); + expect(component.submission()).toEqual(mockSubmission); + }); + + it('should have constants defined', () => { + expect(component.reviewStatusIcon).toBeDefined(); + expect(component.actionLabel).toBeDefined(); + expect(component.actionState).toBeDefined(); + }); + + it('should have default values', () => { + expect(component.limitValue).toBe(1); + expect(component.showAll).toBe(false); + }); + + it('should toggle history', () => { + expect(component.showAll).toBe(false); + + component.toggleHistory(); + expect(component.showAll).toBe(true); + + component.toggleHistory(); + expect(component.showAll).toBe(false); + }); + + it('should emit selected event', () => { + jest.spyOn(component.selected, 'emit'); + component.selected.emit(); + expect(component.selected.emit).toHaveBeenCalled(); + }); + + it('should accept custom input values', () => { + const customStatus = SubmissionReviewStatus.Accepted; + const customSubmission = { + ...mockSubmission, + title: 'Custom Preprint', + actions: [ + ...mockSubmission.actions, + { + id: '2', + trigger: 'manual', + fromState: 'pending', + toState: 'accepted', + dateModified: '2023-01-02', + creator: { + id: 'user-2', + name: 'Jane Doe', + }, + comment: 'Approved', + }, + ], + }; + + fixture.componentRef.setInput('status', customStatus); + fixture.componentRef.setInput('submission', customSubmission); + + expect(component.status()).toBe(customStatus); + expect(component.submission()).toEqual(customSubmission); + }); }); diff --git a/src/app/features/moderation/components/preprint-submissions/preprint-submissions.component.spec.ts b/src/app/features/moderation/components/preprint-submissions/preprint-submissions.component.spec.ts index 40ef30b9e..546d41a5b 100644 --- a/src/app/features/moderation/components/preprint-submissions/preprint-submissions.component.spec.ts +++ b/src/app/features/moderation/components/preprint-submissions/preprint-submissions.component.spec.ts @@ -1,26 +1,43 @@ -import { provideStore } from '@ngxs/store'; +import { MockComponents, MockProvider } from 'ng-mocks'; -import { MockComponents } from 'ng-mocks'; - -import { provideHttpClient } from '@angular/common/http'; -import { provideHttpClientTesting } from '@angular/common/http/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { ActivatedRoute } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; import { PreprintSubmissionItemComponent } from '@osf/features/moderation/components'; -import { PreprintModerationState } from '@osf/features/moderation/store/preprint-moderation'; +import { RegistryModeration } from '@osf/features/moderation/models'; import { CustomPaginatorComponent, IconComponent, LoadingSpinnerComponent, SelectComponent } from '@shared/components'; +import { PreprintSubmissionsSort, SubmissionReviewStatus } from '../../enums'; +import { PreprintModerationSelectors } from '../../store/preprint-moderation'; + import { PreprintSubmissionsComponent } from './preprint-submissions.component'; +import { MOCK_REGISTRY_MODERATIONS } from '@testing/mocks/registry-moderation.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('PreprintSubmissionsComponent', () => { let component: PreprintSubmissionsComponent; let fixture: ComponentFixture; + let mockRouter: ReturnType; + let mockActivatedRoute: ReturnType; + + const mockProviderId = 'test-provider-id'; + const mockSubmissions: RegistryModeration[] = MOCK_REGISTRY_MODERATIONS; beforeEach(async () => { + mockRouter = RouterMockBuilder.create().build(); + mockActivatedRoute = ActivatedRouteMockBuilder.create() + .withParams({ providerId: mockProviderId }) + .withQueryParams({ status: 'pending' }) + .build(); + await TestBed.configureTestingModule({ imports: [ PreprintSubmissionsComponent, + OSFTestingModule, ...MockComponents( SelectComponent, IconComponent, @@ -30,18 +47,18 @@ describe('PreprintSubmissionsComponent', () => { ), ], providers: [ - { - provide: ActivatedRoute, - useValue: { - snapshot: { - params: {}, - queryParams: {}, - }, - }, - }, - provideStore([PreprintModerationState]), - provideHttpClient(), - provideHttpClientTesting(), + MockProvider(Router, mockRouter), + MockProvider(ActivatedRoute, mockActivatedRoute), + provideMockStore({ + signals: [ + { selector: PreprintModerationSelectors.getPreprintSubmissions, value: mockSubmissions }, + { selector: PreprintModerationSelectors.arePreprintSubmissionsLoading, value: false }, + { selector: PreprintModerationSelectors.getPreprintSubmissionsPendingCount, value: 1 }, + { selector: PreprintModerationSelectors.getPreprintSubmissionsAcceptedCount, value: 0 }, + { selector: PreprintModerationSelectors.getPreprintSubmissionsRejectedCount, value: 0 }, + { selector: PreprintModerationSelectors.getPreprintSubmissionsWithdrawnCount, value: 0 }, + ], + }), ], }).compileComponents(); @@ -53,4 +70,85 @@ describe('PreprintSubmissionsComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + it('should initialize with default values', () => { + expect(component.currentPage()).toBe(1); + expect(component.pageSize()).toBe(10); + expect(component.first()).toBe(0); + expect(component.selectedSortOption()).toBe(PreprintSubmissionsSort.Newest); + expect(component.selectedReviewOption()).toBeDefined(); + }); + + it('should have sort options defined', () => { + expect(component.sortOptions).toBeDefined(); + expect(component.sortOptions.length).toBeGreaterThan(0); + }); + + it('should have actions defined', () => { + expect(component.actions).toBeDefined(); + expect(component.actions.getPreprintSubmissions).toBeDefined(); + }); + + it('should compute submission review options with counts', () => { + const options = component.submissionReviewOptions(); + expect(options).toBeDefined(); + expect(options.length).toBeGreaterThan(0); + expect(options[0]).toHaveProperty('count'); + }); + + it('should compute total count', () => { + expect(component.totalCount()).toBeDefined(); + }); + + it('should change review status', () => { + component.changeReviewStatus(SubmissionReviewStatus.Accepted); + expect(component.selectedReviewOption()).toBe(SubmissionReviewStatus.Accepted); + expect(mockRouter.navigate).toHaveBeenCalledWith( + [], + expect.objectContaining({ + queryParams: { status: SubmissionReviewStatus.Accepted }, + }) + ); + }); + + it('should change sort option', () => { + component.changeSort(PreprintSubmissionsSort.Oldest); + expect(component.selectedSortOption()).toBe(PreprintSubmissionsSort.Oldest); + }); + + it('should handle page change', () => { + const mockEvent = { page: 1, first: 10, rows: 10 }; + component.onPageChange(mockEvent); + expect(component.currentPage()).toBe(2); + expect(component.first()).toBe(10); + }); + + it('should handle page change when page is undefined', () => { + const mockEvent = { page: undefined, first: 0, rows: 10 }; + component.onPageChange(mockEvent); + expect(component.currentPage()).toBe(1); + expect(component.first()).toBe(0); + }); + + it('should navigate to preprint', () => { + const mockItem = mockSubmissions[0]; + component.navigateToPreprint(mockItem); + expect(mockRouter.navigate).toHaveBeenCalledWith(['/preprints/', mockProviderId, mockItem.id], { + queryParams: { mode: 'moderator' }, + }); + }); + + it('should get status from query params on init', () => { + expect(component.selectedReviewOption()).toBe(SubmissionReviewStatus.Pending); + }); + + it('should reset pagination when changing review status', () => { + component.currentPage.set(3); + component.first.set(20); + + component.changeReviewStatus(SubmissionReviewStatus.Accepted); + + expect(component.currentPage()).toBe(1); + expect(component.first()).toBe(0); + }); }); diff --git a/src/app/features/moderation/components/preprint-withdrawal-submissions/preprint-withdrawal-submissions.component.spec.ts b/src/app/features/moderation/components/preprint-withdrawal-submissions/preprint-withdrawal-submissions.component.spec.ts index 0c41f3a33..e818f173d 100644 --- a/src/app/features/moderation/components/preprint-withdrawal-submissions/preprint-withdrawal-submissions.component.spec.ts +++ b/src/app/features/moderation/components/preprint-withdrawal-submissions/preprint-withdrawal-submissions.component.spec.ts @@ -1,28 +1,43 @@ -import { provideStore } from '@ngxs/store'; +import { MockComponents, MockProvider } from 'ng-mocks'; -import { MockComponents } from 'ng-mocks'; - -import { of } from 'rxjs'; - -import { provideHttpClient } from '@angular/common/http'; -import { provideHttpClientTesting } from '@angular/common/http/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { ActivatedRoute } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; import { PreprintSubmissionItemComponent } from '@osf/features/moderation/components'; -import { PreprintModerationState } from '@osf/features/moderation/store/preprint-moderation'; +import { PreprintWithdrawalSubmission } from '@osf/features/moderation/models'; import { CustomPaginatorComponent, IconComponent, LoadingSpinnerComponent, SelectComponent } from '@shared/components'; +import { PreprintSubmissionsSort, SubmissionReviewStatus } from '../../enums'; +import { PreprintModerationSelectors } from '../../store/preprint-moderation'; + import { PreprintWithdrawalSubmissionsComponent } from './preprint-withdrawal-submissions.component'; +import { MOCK_PREPRINT_WITHDRAWAL_SUBMISSIONS } from '@testing/mocks/preprint-withdrawal-submission.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('PreprintWithdrawalSubmissionsComponent', () => { let component: PreprintWithdrawalSubmissionsComponent; let fixture: ComponentFixture; + let mockRouter: ReturnType; + let mockActivatedRoute: ReturnType; + + const mockProviderId = 'test-provider-id'; + const mockSubmissions: PreprintWithdrawalSubmission[] = MOCK_PREPRINT_WITHDRAWAL_SUBMISSIONS; beforeEach(async () => { + mockRouter = RouterMockBuilder.create().build(); + mockActivatedRoute = ActivatedRouteMockBuilder.create() + .withParams({ providerId: mockProviderId }) + .withQueryParams({ status: 'pending' }) + .build(); + await TestBed.configureTestingModule({ imports: [ PreprintWithdrawalSubmissionsComponent, + OSFTestingModule, ...MockComponents( SelectComponent, IconComponent, @@ -32,21 +47,17 @@ describe('PreprintWithdrawalSubmissionsComponent', () => { ), ], providers: [ - { - provide: ActivatedRoute, - useValue: { - parent: { - params: of({ id: '1' }), - }, - snapshot: { - queryParams: { wiki: 'test' }, - }, - queryParams: of({ wiki: 'test' }), - }, - }, - provideStore([PreprintModerationState]), - provideHttpClient(), - provideHttpClientTesting(), + MockProvider(Router, mockRouter), + MockProvider(ActivatedRoute, mockActivatedRoute), + provideMockStore({ + signals: [ + { selector: PreprintModerationSelectors.getPreprintWithdrawalSubmissions, value: mockSubmissions }, + { selector: PreprintModerationSelectors.arePreprintWithdrawalSubmissionsLoading, value: false }, + { selector: PreprintModerationSelectors.getPreprintWithdrawalSubmissionsPendingCount, value: 1 }, + { selector: PreprintModerationSelectors.getPreprintWithdrawalSubmissionsAcceptedCount, value: 0 }, + { selector: PreprintModerationSelectors.getPreprintWithdrawalSubmissionsRejectedCount, value: 0 }, + ], + }), ], }).compileComponents(); @@ -58,4 +69,85 @@ describe('PreprintWithdrawalSubmissionsComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + it('should initialize with default values', () => { + expect(component.currentPage()).toBe(1); + expect(component.pageSize()).toBe(10); + expect(component.first()).toBe(0); + expect(component.selectedSortOption()).toBe(PreprintSubmissionsSort.Newest); + expect(component.selectedReviewOption()).toBeDefined(); + }); + + it('should have sort options defined', () => { + expect(component.sortOptions).toBeDefined(); + expect(component.sortOptions.length).toBeGreaterThan(0); + }); + + it('should have actions defined', () => { + expect(component.actions).toBeDefined(); + expect(component.actions.getPreprintWithdrawalSubmissions).toBeDefined(); + }); + + it('should compute submission review options with counts', () => { + const options = component.submissionReviewOptions(); + expect(options).toBeDefined(); + expect(options.length).toBeGreaterThan(0); + expect(options[0]).toHaveProperty('count'); + }); + + it('should compute total count', () => { + expect(component.totalCount()).toBeDefined(); + }); + + it('should change review status', () => { + component.changeReviewStatus(SubmissionReviewStatus.Accepted); + expect(component.selectedReviewOption()).toBe(SubmissionReviewStatus.Accepted); + expect(mockRouter.navigate).toHaveBeenCalledWith( + [], + expect.objectContaining({ + queryParams: { status: SubmissionReviewStatus.Accepted }, + }) + ); + }); + + it('should change sort option', () => { + component.changeSort(PreprintSubmissionsSort.Oldest); + expect(component.selectedSortOption()).toBe(PreprintSubmissionsSort.Oldest); + }); + + it('should handle page change', () => { + const mockEvent = { page: 1, first: 10, rows: 10 }; + component.onPageChange(mockEvent); + expect(component.currentPage()).toBe(2); + expect(component.first()).toBe(10); + }); + + it('should handle page change when page is undefined', () => { + const mockEvent = { page: undefined, first: 0, rows: 10 }; + component.onPageChange(mockEvent); + expect(component.currentPage()).toBe(1); + expect(component.first()).toBe(0); + }); + + it('should navigate to preprint', () => { + const mockItem = mockSubmissions[0]; + component.navigateToPreprint(mockItem); + expect(mockRouter.navigate).toHaveBeenCalledWith(['/preprints/', mockProviderId, mockItem.preprintId], { + queryParams: { mode: 'moderator' }, + }); + }); + + it('should get status from query params on init', () => { + expect(component.selectedReviewOption()).toBe(SubmissionReviewStatus.Pending); + }); + + it('should reset pagination when changing review status', () => { + component.currentPage.set(3); + component.first.set(20); + + component.changeReviewStatus(SubmissionReviewStatus.Accepted); + + expect(component.currentPage()).toBe(1); + expect(component.first()).toBe(0); + }); }); diff --git a/src/app/features/moderation/components/registry-pending-submissions/registry-pending-submissions.component.spec.ts b/src/app/features/moderation/components/registry-pending-submissions/registry-pending-submissions.component.spec.ts index 8b5e8dac1..055391f6f 100644 --- a/src/app/features/moderation/components/registry-pending-submissions/registry-pending-submissions.component.spec.ts +++ b/src/app/features/moderation/components/registry-pending-submissions/registry-pending-submissions.component.spec.ts @@ -1,43 +1,50 @@ -import { provideStore } from '@ngxs/store'; - import { TranslatePipe } from '@ngx-translate/core'; -import { MockPipes, MockProvider } from 'ng-mocks'; - -import { of } from 'rxjs'; +import { MockComponents, MockProvider } from 'ng-mocks'; -import { provideHttpClient } from '@angular/common/http'; -import { provideHttpClientTesting } from '@angular/common/http/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ActivatedRoute, Router } from '@angular/router'; -import { RegistryPendingSubmissionsComponent } from '@osf/features/moderation/components'; -import { RegistryModerationState } from '@osf/features/moderation/store/registry-moderation'; -import { TranslateServiceMock } from '@shared/mocks'; +import { RegistryModeration } from '@osf/features/moderation/models'; + +import { RegistrySort, SubmissionReviewStatus } from '../../enums'; +import { RegistryModerationSelectors } from '../../store/registry-moderation'; + +import { RegistryPendingSubmissionsComponent } from './registry-pending-submissions.component'; -describe.skip('RegistryPendingSubmissionsComponent', () => { +import { MOCK_REGISTRY_MODERATIONS } from '@testing/mocks/registry-moderation.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('RegistryPendingSubmissionsComponent', () => { let component: RegistryPendingSubmissionsComponent; let fixture: ComponentFixture; + let mockRouter: ReturnType; + let mockActivatedRoute: ReturnType; + + const mockProviderId = 'test-provider-id'; + const mockSubmissions: RegistryModeration[] = MOCK_REGISTRY_MODERATIONS; beforeEach(async () => { + mockRouter = RouterMockBuilder.create().build(); + mockActivatedRoute = ActivatedRouteMockBuilder.create() + .withParams({ providerId: mockProviderId }) + .withQueryParams({ status: 'pending' }) + .build(); + await TestBed.configureTestingModule({ - imports: [RegistryPendingSubmissionsComponent, ...MockPipes(TranslatePipe)], + imports: [RegistryPendingSubmissionsComponent, OSFTestingModule, ...MockComponents(), TranslatePipe], providers: [ - provideStore([RegistryModerationState]), - { - provide: ActivatedRoute, - useValue: { - parent: { - params: of({ providerId: 'id1' }), - }, - snapshot: { - queryParams: {}, - }, - }, - }, - MockProvider(Router), - TranslateServiceMock, - provideHttpClient(), - provideHttpClientTesting(), + MockProvider(Router, mockRouter), + MockProvider(ActivatedRoute, mockActivatedRoute), + provideMockStore({ + signals: [ + { selector: RegistryModerationSelectors.getRegistrySubmissions, value: mockSubmissions }, + { selector: RegistryModerationSelectors.areRegistrySubmissionLoading, value: false }, + { selector: RegistryModerationSelectors.getRegistrySubmissionTotalCount, value: 1 }, + ], + }), ], }).compileComponents(); @@ -49,4 +56,71 @@ describe.skip('RegistryPendingSubmissionsComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + it('should initialize with default values', () => { + expect(component.currentPage()).toBe(1); + expect(component.pageSize()).toBe(10); + expect(component.first()).toBe(0); + expect(component.selectedSortOption()).toBe(RegistrySort.RegisteredNewest); + expect(component.selectedReviewOption()).toBeDefined(); + }); + + it('should have submission review options defined', () => { + expect(component.submissionReviewOptions).toBeDefined(); + expect(component.submissionReviewOptions.length).toBeGreaterThan(0); + }); + + it('should have sort options defined', () => { + expect(component.sortOptions).toBeDefined(); + expect(component.sortOptions.length).toBeGreaterThan(0); + }); + + it('should have actions defined', () => { + expect(component.actions).toBeDefined(); + expect(component.actions.getRegistrySubmissions).toBeDefined(); + }); + + it('should change review status', () => { + component.changeReviewStatus(SubmissionReviewStatus.Accepted); + expect(component.selectedReviewOption()).toBe(SubmissionReviewStatus.Accepted); + expect(mockRouter.navigate).toHaveBeenCalledWith( + [], + expect.objectContaining({ + queryParams: { status: SubmissionReviewStatus.Accepted }, + }) + ); + }); + + it('should change sort option', () => { + component.changeSort(RegistrySort.RegisteredOldest); + expect(component.selectedSortOption()).toBe(RegistrySort.RegisteredOldest); + }); + + it('should handle page change', () => { + const mockEvent = { page: 1, first: 10, rows: 10 }; + component.onPageChange(mockEvent); + expect(component.currentPage()).toBe(2); + expect(component.first()).toBe(10); + }); + + it('should handle page change when page is undefined', () => { + const mockEvent = { page: undefined, first: 0, rows: 10 }; + component.onPageChange(mockEvent); + expect(component.currentPage()).toBe(1); + expect(component.first()).toBe(0); + }); + + it('should get status from query params on init', () => { + expect(component.selectedReviewOption()).toBe(SubmissionReviewStatus.Pending); + }); + + it('should reset pagination when changing review status', () => { + component.currentPage.set(3); + component.first.set(20); + + component.changeReviewStatus(SubmissionReviewStatus.Accepted); + + expect(component.currentPage()).toBe(1); + expect(component.first()).toBe(0); + }); }); diff --git a/src/app/features/moderation/components/registry-settings/registry-settings.component.spec.ts b/src/app/features/moderation/components/registry-settings/registry-settings.component.spec.ts index 9a0e2c5d7..346ef660b 100644 --- a/src/app/features/moderation/components/registry-settings/registry-settings.component.spec.ts +++ b/src/app/features/moderation/components/registry-settings/registry-settings.component.spec.ts @@ -1,30 +1,16 @@ -import { TranslatePipe } from '@ngx-translate/core'; -import { MockComponent, MockPipe } from 'ng-mocks'; - -import { of } from 'rxjs'; - import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { ActivatedRoute } from '@angular/router'; - -import { BulkUploadComponent } from '@osf/features/moderation/components'; import { RegistrySettingsComponent } from './registry-settings.component'; +import { OSFTestingModule } from '@testing/osf.testing.module'; + describe('RegistrySettingsComponent', () => { let component: RegistrySettingsComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [RegistrySettingsComponent, MockComponent(BulkUploadComponent), MockPipe(TranslatePipe)], - providers: [ - { - provide: ActivatedRoute, - useValue: { - queryParams: of({}), - }, - }, - ], + imports: [RegistrySettingsComponent, OSFTestingModule], }).compileComponents(); fixture = TestBed.createComponent(RegistrySettingsComponent); diff --git a/src/app/features/moderation/components/registry-submission-item/registry-submission-item.component.spec.ts b/src/app/features/moderation/components/registry-submission-item/registry-submission-item.component.spec.ts index 0ad6d7381..28f873d38 100644 --- a/src/app/features/moderation/components/registry-submission-item/registry-submission-item.component.spec.ts +++ b/src/app/features/moderation/components/registry-submission-item/registry-submission-item.component.spec.ts @@ -1,60 +1,82 @@ -import { TranslatePipe } from '@ngx-translate/core'; -import { MockComponent, MockPipes } from 'ng-mocks'; +import { MockComponent, MockPipe } from 'ng-mocks'; -import { ComponentRef } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { RouterTestingModule } from '@angular/router/testing'; -import { IconComponent } from '@shared/components'; -import { DateAgoPipe } from '@shared/pipes'; +import { IconComponent } from '@osf/shared/components'; +import { DateAgoPipe } from '@osf/shared/pipes'; + +import { SubmissionReviewStatus } from '../../enums'; +import { RegistryModeration } from '../../models'; import { RegistrySubmissionItemComponent } from './registry-submission-item.component'; +import { MOCK_REGISTRY_MODERATIONS } from '@testing/mocks/registry-moderation.mock'; +import { OSFTestingModule } from '@testing/osf.testing.module'; + describe('RegistrySubmissionItemComponent', () => { let component: RegistrySubmissionItemComponent; - let componentRef: ComponentRef; let fixture: ComponentFixture; + const mockSubmission: RegistryModeration = MOCK_REGISTRY_MODERATIONS[0]; + beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [ - RegistrySubmissionItemComponent, - RouterTestingModule, - MockComponent(IconComponent), - MockPipes(DateAgoPipe, TranslatePipe), - ], + imports: [RegistrySubmissionItemComponent, OSFTestingModule, MockComponent(IconComponent), MockPipe(DateAgoPipe)], }).compileComponents(); fixture = TestBed.createComponent(RegistrySubmissionItemComponent); component = fixture.componentInstance; - componentRef = fixture.componentRef; - - const mockSubmission = { - id: 'reg1', - title: 'Test registry submission', - reviewsState: 'pending', - public: true, - embargoed: false, - embargoEndDate: null, - actions: [ - { - id: 'a1', - fromState: 'pending', - toState: 'accepted', - dateModified: new Date().toISOString(), - creator: { id: 'u1', name: 'Bob' }, - comment: '', - }, - ], - }; - - componentRef.setInput('status', 'pending'); - componentRef.setInput('submission', mockSubmission); - + fixture.componentRef.setInput('status', SubmissionReviewStatus.Pending); + fixture.componentRef.setInput('submission', mockSubmission); fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); + + it('should have input values', () => { + expect(component.status()).toBe(SubmissionReviewStatus.Pending); + expect(component.submission()).toEqual(mockSubmission); + }); + + it('should have constants defined', () => { + expect(component.reviewStatusIcon).toBeDefined(); + expect(component.registryActionLabel).toBeDefined(); + expect(component.registryActionState).toBeDefined(); + }); + + it('should have default values', () => { + expect(component.limitValue).toBe(1); + expect(component.showAll).toBe(false); + }); + + it('should toggle history', () => { + expect(component.showAll).toBe(false); + + component.toggleHistory(); + expect(component.showAll).toBe(true); + + component.toggleHistory(); + expect(component.showAll).toBe(false); + }); + + it('should compute isPendingModeration correctly', () => { + expect(component.isPendingModeration).toBe(true); + }); + + it('should compute isPending correctly', () => { + expect(component.isPending).toBe(true); + }); + + it('should accept custom input values', () => { + const customStatus = SubmissionReviewStatus.Accepted; + const customSubmission = { ...mockSubmission, title: 'Custom Registry Submission' }; + + fixture.componentRef.setInput('status', customStatus); + fixture.componentRef.setInput('submission', customSubmission); + + expect(component.status()).toBe(customStatus); + expect(component.submission()).toEqual(customSubmission); + }); }); diff --git a/src/app/features/moderation/components/registry-submissions/registry-submissions.component.spec.ts b/src/app/features/moderation/components/registry-submissions/registry-submissions.component.spec.ts index 6c730a37e..aa145cbc0 100644 --- a/src/app/features/moderation/components/registry-submissions/registry-submissions.component.spec.ts +++ b/src/app/features/moderation/components/registry-submissions/registry-submissions.component.spec.ts @@ -1,28 +1,43 @@ -import { provideStore } from '@ngxs/store'; +import { MockComponents, MockProvider } from 'ng-mocks'; -import { MockComponents } from 'ng-mocks'; - -import { of } from 'rxjs'; - -import { provideHttpClient } from '@angular/common/http'; -import { provideHttpClientTesting } from '@angular/common/http/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { ActivatedRoute } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; import { RegistrySubmissionItemComponent } from '@osf/features/moderation/components'; -import { RegistryModerationState } from '@osf/features/moderation/store/registry-moderation'; +import { RegistryModeration } from '@osf/features/moderation/models'; import { CustomPaginatorComponent, IconComponent, LoadingSpinnerComponent, SelectComponent } from '@shared/components'; +import { RegistrySort, SubmissionReviewStatus } from '../../enums'; +import { RegistryModerationSelectors } from '../../store/registry-moderation'; + import { RegistrySubmissionsComponent } from './registry-submissions.component'; +import { MOCK_REGISTRY_MODERATIONS } from '@testing/mocks/registry-moderation.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('RegistrySubmissionsComponent', () => { let component: RegistrySubmissionsComponent; let fixture: ComponentFixture; + let mockActivatedRoute: ReturnType; + let mockRouter: ReturnType; + + const mockProviderId = 'test-provider-id'; + const mockSubmissions: RegistryModeration[] = MOCK_REGISTRY_MODERATIONS; beforeEach(async () => { + mockRouter = RouterMockBuilder.create().build(); + mockActivatedRoute = ActivatedRouteMockBuilder.create() + .withParams({ providerId: mockProviderId }) + .withQueryParams({ status: 'pending' }) + .build(); + await TestBed.configureTestingModule({ imports: [ RegistrySubmissionsComponent, + OSFTestingModule, ...MockComponents( SelectComponent, IconComponent, @@ -32,20 +47,15 @@ describe('RegistrySubmissionsComponent', () => { ), ], providers: [ - provideStore([RegistryModerationState]), - { - provide: ActivatedRoute, - useValue: { - parent: { - params: of({ providerId: '1' }), - }, - snapshot: { - queryParams: {}, - }, - }, - }, - provideHttpClient(), - provideHttpClientTesting(), + MockProvider(Router, mockRouter), + MockProvider(ActivatedRoute, mockActivatedRoute), + provideMockStore({ + signals: [ + { selector: RegistryModerationSelectors.getRegistrySubmissions, value: mockSubmissions }, + { selector: RegistryModerationSelectors.areRegistrySubmissionLoading, value: false }, + { selector: RegistryModerationSelectors.getRegistrySubmissionTotalCount, value: 1 }, + ], + }), ], }).compileComponents(); @@ -57,4 +67,71 @@ describe('RegistrySubmissionsComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + it('should initialize with default values', () => { + expect(component.currentPage()).toBe(1); + expect(component.pageSize()).toBe(10); + expect(component.first()).toBe(0); + expect(component.selectedSortOption()).toBe(RegistrySort.RegisteredNewest); + expect(component.selectedReviewOption()).toBeDefined(); + }); + + it('should have submission review options defined', () => { + expect(component.submissionReviewOptions).toBeDefined(); + expect(component.submissionReviewOptions.length).toBeGreaterThan(0); + }); + + it('should have sort options defined', () => { + expect(component.sortOptions).toBeDefined(); + expect(component.sortOptions.length).toBeGreaterThan(0); + }); + + it('should have actions defined', () => { + expect(component.actions).toBeDefined(); + expect(component.actions.getRegistrySubmissions).toBeDefined(); + }); + + it('should change review status', () => { + component.changeReviewStatus(SubmissionReviewStatus.Accepted); + expect(component.selectedReviewOption()).toBe(SubmissionReviewStatus.Accepted); + expect(mockRouter.navigate).toHaveBeenCalledWith( + [], + expect.objectContaining({ + queryParams: { status: SubmissionReviewStatus.Accepted }, + }) + ); + }); + + it('should change sort option', () => { + component.changeSort(RegistrySort.RegisteredOldest); + expect(component.selectedSortOption()).toBe(RegistrySort.RegisteredOldest); + }); + + it('should handle page change', () => { + const mockEvent = { page: 1, first: 10, rows: 10 }; + component.onPageChange(mockEvent); + expect(component.currentPage()).toBe(2); + expect(component.first()).toBe(10); + }); + + it('should handle page change when page is undefined', () => { + const mockEvent = { page: undefined, first: 0, rows: 10 }; + component.onPageChange(mockEvent); + expect(component.currentPage()).toBe(1); + expect(component.first()).toBe(0); + }); + + it('should get status from query params on init', () => { + expect(component.selectedReviewOption()).toBe(SubmissionReviewStatus.Pending); + }); + + it('should reset pagination when changing review status', () => { + component.currentPage.set(3); + component.first.set(20); + + component.changeReviewStatus(SubmissionReviewStatus.Accepted); + + expect(component.currentPage()).toBe(1); + expect(component.first()).toBe(0); + }); }); 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 62a9e563f..19307ab27 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,9 +1,8 @@ -import { MockComponent, MockPipes, MockProvider } from 'ng-mocks'; +import { MockComponent, MockPipes } 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'; @@ -13,6 +12,7 @@ import { MOCK_PROVIDER } from '@shared/mocks'; import { ModerationStatusBannerComponent } from './moderation-status-banner.component'; +import { EnvironmentTokenMock } from '@testing/mocks/environment.token.mock'; 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'; @@ -28,7 +28,6 @@ describe('ModerationStatusBannerComponent', () => { 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({ @@ -40,9 +39,7 @@ describe('ModerationStatusBannerComponent', () => { ], providers: [ TranslationServiceMock, - MockProvider(ENVIRONMENT, { - webUrl: mockWebUrl, - }), + EnvironmentTokenMock, provideMockStore({ signals: [ { @@ -184,7 +181,7 @@ describe('ModerationStatusBannerComponent', () => { it('should compute actionCreatorLink with environment webUrl', () => { const link = component.actionCreatorLink(); - expect(link).toBe(`${mockWebUrl}/user-1`); + expect(link).toBe(`${EnvironmentTokenMock.useValue.webUrl}/user-1`); }); it('should compute withdrawalRequesterName from latestWithdrawalRequest', () => { 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 8b8ff0118..fcacfcef1 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 @@ -4,7 +4,6 @@ import { TitleCasePipe } from '@angular/common'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { Router } from '@angular/router'; -import { ENVIRONMENT } from '@core/provider/environment.provider'; import { AdvisoryBoardComponent, BrowseBySubjectsComponent, @@ -18,6 +17,7 @@ import { BrandService } from '@shared/services'; import { PreprintsLandingComponent } from './preprints-landing.component'; +import { EnvironmentTokenMock } from '@testing/mocks/environment.token.mock'; 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'; @@ -53,10 +53,7 @@ describe('PreprintsLandingComponent', () => { ], providers: [ TranslationServiceMock, - MockProvider(ENVIRONMENT, { - defaultProvider: mockDefaultProvider, - supportEmail: 'support@osf.io', - }), + EnvironmentTokenMock, MockProvider(BrandService), MockProvider(Router, routerMock), provideMockStore({ diff --git a/src/app/shared/components/resource-metadata/resource-metadata.component.spec.ts b/src/app/shared/components/resource-metadata/resource-metadata.component.spec.ts index d53526691..7c340e23e 100644 --- a/src/app/shared/components/resource-metadata/resource-metadata.component.spec.ts +++ b/src/app/shared/components/resource-metadata/resource-metadata.component.spec.ts @@ -29,9 +29,9 @@ describe('ResourceMetadataComponent', () => { expect(component.currentResource()).toEqual(mockResourceOverview); }); - it('should have canWrite as required input', () => { - fixture.componentRef.setInput('canWrite', true); - expect(component.canWrite()).toBe(true); + it('should have canEdit as required input', () => { + fixture.componentRef.setInput('canEdit', true); + expect(component.canEdit()).toBe(true); }); it('should have customCitationUpdated output', () => { @@ -62,9 +62,9 @@ describe('ResourceMetadataComponent', () => { expect(component.currentResource()).toBeNull(); }); - it('should handle false canWrite input', () => { - fixture.componentRef.setInput('canWrite', false); - expect(component.canWrite()).toBe(false); + it('should handle false canEdit input', () => { + fixture.componentRef.setInput('canEdit', false); + expect(component.canEdit()).toBe(false); }); it('should handle true isCollectionsRoute input', () => { diff --git a/src/app/shared/mocks/provider.mock.ts b/src/app/shared/mocks/provider.mock.ts index 0cf16267a..d027b2e6e 100644 --- a/src/app/shared/mocks/provider.mock.ts +++ b/src/app/shared/mocks/provider.mock.ts @@ -28,4 +28,9 @@ export const MOCK_PROVIDER = { facebookAppId: null, reviewsCommentsPrivate: null, reviewsCommentsAnonymous: null, + primaryCollection: { + id: 'collection-1', + title: 'Test Collection', + description: 'Test collection description', + }, }; diff --git a/src/testing/mocks/environment.token.mock.ts b/src/testing/mocks/environment.token.mock.ts index f451d7379..1c8c64216 100644 --- a/src/testing/mocks/environment.token.mock.ts +++ b/src/testing/mocks/environment.token.mock.ts @@ -30,5 +30,7 @@ export const EnvironmentTokenMock = { googleTagManagerId: 'test-goolge-tag-manager-id', addonsApiUrl: 'http://addons.localhost:8000', webUrl: 'http://localhost:4200', + supportEmail: 'support@test.com', + defaultProvider: 'osf', }, }; diff --git a/src/testing/mocks/moderator.mock.ts b/src/testing/mocks/moderator.mock.ts new file mode 100644 index 000000000..d8fa446d2 --- /dev/null +++ b/src/testing/mocks/moderator.mock.ts @@ -0,0 +1,95 @@ +import { ModeratorPermission } from '@osf/features/moderation/enums'; +import { ModeratorModel } from '@osf/features/moderation/models'; + +export const MOCK_MODERATORS: ModeratorModel[] = [ + { + id: '1', + userId: 'user-1', + fullName: 'John Doe', + permission: ModeratorPermission.Admin, + employment: [ + { + title: 'Professor', + institution: 'University of Example', + department: 'Computer Science', + startMonth: 1, + startYear: 2020, + endMonth: null, + endYear: null, + ongoing: true, + }, + ], + education: [ + { + institution: 'University of Example', + department: 'Computer Science', + degree: 'PhD', + startMonth: 9, + startYear: 2015, + endMonth: 6, + endYear: 2020, + ongoing: false, + }, + ], + }, + { + id: '2', + userId: 'user-2', + fullName: 'Jane Smith', + permission: ModeratorPermission.Moderator, + employment: [ + { + title: 'Research Scientist', + institution: 'Tech University', + department: 'Data Science', + startMonth: 3, + startYear: 2019, + endMonth: null, + endYear: null, + ongoing: true, + }, + ], + education: [ + { + institution: 'Tech University', + department: 'Data Science', + degree: 'Master', + startMonth: 9, + startYear: 2017, + endMonth: 6, + endYear: 2019, + ongoing: false, + }, + ], + }, + { + id: '3', + userId: 'user-3', + fullName: 'Bob Johnson', + permission: ModeratorPermission.Moderator, + employment: [ + { + title: 'Engineer', + institution: 'State University', + department: 'Engineering', + startMonth: 6, + startYear: 2018, + endMonth: 12, + endYear: 2022, + ongoing: false, + }, + ], + education: [ + { + institution: 'State University', + department: 'Engineering', + degree: 'Bachelor', + startMonth: 9, + startYear: 2014, + endMonth: 6, + endYear: 2018, + ongoing: false, + }, + ], + }, +]; diff --git a/src/testing/mocks/preprint-provider-moderation-info.mock.ts b/src/testing/mocks/preprint-provider-moderation-info.mock.ts new file mode 100644 index 000000000..b67a24934 --- /dev/null +++ b/src/testing/mocks/preprint-provider-moderation-info.mock.ts @@ -0,0 +1,11 @@ +import { PreprintProviderModerationInfo } from '@osf/features/moderation/models'; + +export const MOCK_PREPRINT_PROVIDER_MODERATION_INFO: PreprintProviderModerationInfo = { + id: 'test-provider-id', + name: 'Test Provider', + submissionCount: 10, + reviewsCommentsAnonymous: true, + reviewsCommentsPrivate: false, + reviewsWorkflow: 'pre_moderation', + supportEmail: 'support@test.com', +}; diff --git a/src/testing/mocks/preprint-review-action.mock.ts b/src/testing/mocks/preprint-review-action.mock.ts new file mode 100644 index 000000000..da45ec470 --- /dev/null +++ b/src/testing/mocks/preprint-review-action.mock.ts @@ -0,0 +1,58 @@ +import { PreprintReviewActionModel } from '@osf/features/moderation/models'; + +export const MOCK_PREPRINT_REVIEW_ACTIONS: PreprintReviewActionModel[] = [ + { + id: '1', + fromState: 'pending', + toState: 'pending', + dateModified: '2023-01-01', + creator: { + id: 'user-1', + name: 'John Doe', + }, + preprint: { + id: 'preprint-1', + name: 'Test Preprint', + }, + provider: { + id: 'provider-1', + name: 'Test Provider', + }, + }, + { + id: '2', + fromState: 'pending', + toState: 'accepted', + dateModified: '2023-01-02', + creator: { + id: 'user-2', + name: 'Jane Doe', + }, + preprint: { + id: 'preprint-2', + name: 'Test Preprint 2', + }, + provider: { + id: 'provider-2', + name: 'Test Provider 2', + }, + }, + { + id: '3', + fromState: 'pending', + toState: 'rejected', + dateModified: '2023-01-03', + creator: { + id: 'user-3', + name: 'Bob Smith', + }, + preprint: { + id: 'preprint-3', + name: 'Test Preprint 3', + }, + provider: { + id: 'provider-3', + name: 'Test Provider 3', + }, + }, +]; diff --git a/src/testing/mocks/preprint-withdrawal-submission.mock.ts b/src/testing/mocks/preprint-withdrawal-submission.mock.ts new file mode 100644 index 000000000..b3fd7cd34 --- /dev/null +++ b/src/testing/mocks/preprint-withdrawal-submission.mock.ts @@ -0,0 +1,61 @@ +import { PreprintWithdrawalSubmission } from '@osf/features/moderation/models'; + +export const MOCK_PREPRINT_WITHDRAWAL_SUBMISSIONS: PreprintWithdrawalSubmission[] = [ + { + id: '1', + title: 'Test Withdrawal 1', + preprintId: 'preprint-1', + actions: [ + { + id: '1', + trigger: 'manual', + fromState: 'pending', + toState: 'pending', + dateModified: '2023-01-01', + creator: { + id: 'user-1', + name: 'John Doe', + }, + comment: 'Withdrawal request', + }, + ], + }, + { + id: '2', + preprintId: 'preprint-2', + title: 'Test Withdrawal 2', + actions: [ + { + id: '2', + trigger: 'manual', + fromState: 'pending', + toState: 'accepted', + dateModified: '2023-01-02', + creator: { + id: 'user-2', + name: 'Jane Doe', + }, + comment: 'Withdrawal approved', + }, + ], + }, + { + id: '3', + preprintId: 'preprint-3', + title: 'Test Withdrawal 3', + actions: [ + { + id: '3', + trigger: 'manual', + fromState: 'pending', + toState: 'rejected', + dateModified: '2023-01-03', + creator: { + id: 'user-3', + name: 'Bob Smith', + }, + comment: 'Withdrawal rejected', + }, + ], + }, +]; diff --git a/src/testing/mocks/registry-moderation.mock.ts b/src/testing/mocks/registry-moderation.mock.ts new file mode 100644 index 000000000..db664f1e4 --- /dev/null +++ b/src/testing/mocks/registry-moderation.mock.ts @@ -0,0 +1,77 @@ +import { RegistryModeration } from '@osf/features/moderation/models'; +import { RegistrationReviewStates, RevisionReviewStates } from '@shared/enums'; + +export const MOCK_REGISTRY_MODERATIONS: RegistryModeration[] = [ + { + id: '1', + title: 'Test Registry 1', + revisionStatus: RevisionReviewStates.RevisionPendingModeration, + reviewsState: RegistrationReviewStates.Pending, + public: false, + embargoed: false, + embargoEndDate: null, + actions: [ + { + id: '1', + trigger: 'manual', + fromState: 'pending', + toState: 'pending', + dateModified: '2023-01-01', + creator: { + id: 'user-1', + name: 'John Doe', + }, + comment: 'Registry submission', + }, + ], + revisionId: 'revision-1', + }, + { + id: '2', + title: 'Test Registry 2', + revisionStatus: RevisionReviewStates.Approved, + reviewsState: RegistrationReviewStates.Accepted, + public: true, + embargoed: false, + embargoEndDate: null, + actions: [ + { + id: '2', + trigger: 'manual', + fromState: 'pending', + toState: 'accepted', + dateModified: '2023-01-02', + creator: { + id: 'user-2', + name: 'Jane Doe', + }, + comment: 'Registry approved', + }, + ], + revisionId: 'revision-2', + }, + { + id: '3', + title: 'Test Registry 3', + revisionStatus: RevisionReviewStates.Unapproved, + reviewsState: RegistrationReviewStates.Rejected, + public: false, + embargoed: true, + embargoEndDate: '2024-01-01', + actions: [ + { + id: '3', + trigger: 'manual', + fromState: 'pending', + toState: 'rejected', + dateModified: '2023-01-03', + creator: { + id: 'user-3', + name: 'Bob Smith', + }, + comment: 'Registry rejected', + }, + ], + revisionId: 'revision-3', + }, +]; diff --git a/src/testing/mocks/submission.mock.ts b/src/testing/mocks/submission.mock.ts new file mode 100644 index 000000000..bc7a5b2a0 --- /dev/null +++ b/src/testing/mocks/submission.mock.ts @@ -0,0 +1,66 @@ +import { PreprintSubmission } from '@osf/features/moderation/models'; +import { CollectionSubmissionWithGuid } from '@shared/models'; + +export const MOCK_PREPRINT_SUBMISSION: PreprintSubmission = { + id: '1', + title: 'Test Preprint Submission', + reviewsState: 'pending', + public: false, + actions: [ + { + id: '1', + trigger: 'manual', + fromState: 'pending', + toState: 'pending', + dateModified: '2023-01-01', + creator: { + id: 'user-1', + name: 'John Doe', + }, + comment: 'Test comment', + }, + ], +}; + +export const MOCK_COLLECTION_SUBMISSION_WITH_GUID: CollectionSubmissionWithGuid = { + id: '1', + type: 'collection-submission', + nodeId: 'node-123', + nodeUrl: 'https://osf.io/node-123/', + title: 'Test Collection Submission', + description: 'This is a test collection submission', + category: 'project', + dateCreated: '2024-01-01T00:00:00Z', + dateModified: '2024-01-02T00:00:00Z', + public: false, + reviewsState: 'pending', + collectedType: 'preprint', + status: 'pending', + volume: '1', + issue: '1', + programArea: 'Science', + schoolType: 'University', + studyDesign: 'Experimental', + dataType: 'Quantitative', + disease: 'None', + gradeLevels: 'Graduate', + creator: { + id: 'user-123', + fullName: 'John Doe', + }, + actions: [ + { + id: 'action-1', + type: 'review', + dateCreated: '2024-01-01T00:00:00Z', + dateModified: '2024-01-01T00:00:00Z', + fromState: 'pending', + toState: 'pending', + comment: 'Initial review', + trigger: 'manual', + targetId: '1', + targetNodeId: 'node-123', + createdBy: 'moderator-123', + }, + ], +};