From 33dd585462998f9f10b0c9a5b64a71909cdea417 Mon Sep 17 00:00:00 2001 From: Diana Date: Fri, 12 Sep 2025 13:29:00 +0300 Subject: [PATCH 1/7] test(contributors): added new tests --- jest.config.js | 1 - .../component-checkbox-item.component.spec.ts | 41 +++- .../create-view-link-dialog.component.spec.ts | 196 +++++++++++++++++- .../contributors.component.spec.ts | 176 +++++++++++++++- src/app/shared/mocks/index.ts | 1 + src/app/shared/mocks/view-only-link.mock.ts | 32 +++ 6 files changed, 443 insertions(+), 4 deletions(-) create mode 100644 src/app/shared/mocks/view-only-link.mock.ts diff --git a/jest.config.js b/jest.config.js index ccef4bc7e..369b04e14 100644 --- a/jest.config.js +++ b/jest.config.js @@ -71,7 +71,6 @@ module.exports = { '/src/app/features/files/pages/community-metadata', '/src/app/features/files/pages/file-detail', '/src/app/features/preprints/', - '/src/app/features/project/contributors/', '/src/app/features/project/overview/', '/src/app/features/project/registrations', '/src/app/features/project/settings', diff --git a/src/app/features/project/contributors/components/component-checkbox-item/component-checkbox-item.component.spec.ts b/src/app/features/project/contributors/components/component-checkbox-item/component-checkbox-item.component.spec.ts index d0e56e9e7..b8229fc2d 100644 --- a/src/app/features/project/contributors/components/component-checkbox-item/component-checkbox-item.component.spec.ts +++ b/src/app/features/project/contributors/components/component-checkbox-item/component-checkbox-item.component.spec.ts @@ -1,11 +1,18 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; + +import { MOCK_VIEW_ONLY_LINK_COMPONENT_ITEM } from '@shared/mocks'; + +import { ViewOnlyLinkComponentItem } from '../../models/view-only-components.models'; import { ComponentCheckboxItemComponent } from './component-checkbox-item.component'; -describe.skip('ComponentCheckboxItemComponent', () => { +describe('ComponentCheckboxItemComponent', () => { let component: ComponentCheckboxItemComponent; let fixture: ComponentFixture; + const mockItem: ViewOnlyLinkComponentItem = MOCK_VIEW_ONLY_LINK_COMPONENT_ITEM; + beforeEach(async () => { await TestBed.configureTestingModule({ imports: [ComponentCheckboxItemComponent], @@ -13,9 +20,41 @@ describe.skip('ComponentCheckboxItemComponent', () => { fixture = TestBed.createComponent(ComponentCheckboxItemComponent); component = fixture.componentInstance; + + fixture.componentRef.setInput('item', mockItem); + fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); + + it('should emit checkboxChange when checkbox is clicked', () => { + jest.spyOn(component.checkboxChange, 'emit'); + + const checkboxElement = fixture.debugElement.query(By.css('p-checkbox')); + checkboxElement.triggerEventHandler('onChange', {}); + + expect(component.checkboxChange.emit).toHaveBeenCalled(); + }); + + it('should handle item with parentId', () => { + const itemWithParent = { ...mockItem, parentId: 'parent-123' }; + fixture.componentRef.setInput('item', itemWithParent); + fixture.detectChanges(); + + expect(component.item().parentId).toBe('parent-123'); + }); + + it('should handle item without parentId', () => { + expect(component.item().parentId).toBeNull(); + }); + + it('should handle different item IDs', () => { + const differentItem = { ...mockItem, id: 'different-id' }; + fixture.componentRef.setInput('item', differentItem); + fixture.detectChanges(); + + expect(component.item().id).toBe('different-id'); + }); }); diff --git a/src/app/features/project/contributors/components/create-view-link-dialog/create-view-link-dialog.component.spec.ts b/src/app/features/project/contributors/components/create-view-link-dialog/create-view-link-dialog.component.spec.ts index c5b3defdf..7c9db265e 100644 --- a/src/app/features/project/contributors/components/create-view-link-dialog/create-view-link-dialog.component.spec.ts +++ b/src/app/features/project/contributors/components/create-view-link-dialog/create-view-link-dialog.component.spec.ts @@ -1,14 +1,66 @@ +import { MockProvider } from 'ng-mocks'; + +import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog'; + import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ResourceType } from '@osf/shared/enums'; +import { CurrentResourceSelectors } from '@osf/shared/stores'; + +import { ResourceInfoModel } from '../../models'; + import { CreateViewLinkDialogComponent } from './create-view-link-dialog.component'; +import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideMockStore } from '@testing/providers/store-provider.mock'; + describe('CreateViewLinkDialogComponent', () => { let component: CreateViewLinkDialogComponent; let fixture: ComponentFixture; + let dialogRef: jest.Mocked; + let dialogConfig: DynamicDialogConfig; + + const mockResourceInfo: ResourceInfoModel = { + id: 'project-123', + title: 'Test Project', + type: ResourceType.Project, + rootParentId: 'root-123', + }; + + const mockComponents = [ + { id: 'project-123', title: 'Test Project', parentId: null }, + { id: 'component-1', title: 'Component 1', parentId: 'project-123' }, + { id: 'component-2', title: 'Component 2', parentId: 'project-123' }, + { id: 'component-3', title: 'Component 3', parentId: 'component-1' }, + ]; beforeEach(async () => { + dialogRef = { + close: jest.fn(), + } as any; + + dialogConfig = { + data: mockResourceInfo, + } as DynamicDialogConfig; + await TestBed.configureTestingModule({ - imports: [CreateViewLinkDialogComponent], + imports: [CreateViewLinkDialogComponent, OSFTestingModule], + providers: [ + provideMockStore({ + signals: [ + { + selector: CurrentResourceSelectors.getResourceWithChildren, + value: mockComponents, + }, + { + selector: CurrentResourceSelectors.isResourceWithChildrenLoading, + value: false, + }, + ], + }), + MockProvider(DynamicDialogRef, dialogRef), + MockProvider(DynamicDialogConfig, dialogConfig), + ], }).compileComponents(); fixture = TestBed.createComponent(CreateViewLinkDialogComponent); @@ -19,4 +71,146 @@ describe('CreateViewLinkDialogComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + it('should have invalid form when linkName is empty', () => { + expect(component.linkName.invalid).toBe(true); + }); + + it('should have invalid form when linkName contains only whitespace', () => { + component.linkName.setValue(' '); + expect(component.linkName.invalid).toBe(true); + }); + + it('should have valid form when linkName has content', () => { + component.linkName.setValue('Test Link'); + expect(component.linkName.valid).toBe(true); + }); + + it('should mark current resource as checked and disabled', () => { + const currentResourceItem = component.componentsList().find((item) => item.id === 'project-123'); + expect(currentResourceItem?.checked).toBe(true); + expect(currentResourceItem?.disabled).toBe(true); + expect(currentResourceItem?.isCurrentResource).toBe(true); + }); + + it('should uncheck children when parent is unchecked', () => { + const parentItem = component.componentsList().find((item) => item.id === 'component-1'); + + component.onCheckboxChange({ ...parentItem!, checked: true }); + fixture.detectChanges(); + + component.onCheckboxChange({ ...parentItem!, checked: false }); + fixture.detectChanges(); + + const updatedChildItem = component.componentsList().find((item) => item.id === 'component-3'); + expect(updatedChildItem?.checked).toBe(false); + }); + + it('should handle items without parent correctly', () => { + const rootItem = component.componentsList().find((item) => item.id === 'project-123'); + expect(rootItem?.disabled).toBe(true); + }); + + it('should add link and close dialog when form is valid', () => { + component.linkName.setValue('Test Link'); + component.anonymous.set(false); + + component.addLink(); + + expect(dialogRef.close).toHaveBeenCalledWith({ + attributes: { + name: 'Test Link', + anonymous: false, + }, + nodes: [{ id: 'project-123', type: 'nodes' }], + }); + }); + + it('should add link with relationships when additional components are selected', () => { + const result = component['buildLinkData']( + ['project-123', 'component-1', 'component-2'], + 'project-123', + 'Test Link', + false + ); + + expect(result).toEqual({ + attributes: { + name: 'Test Link', + anonymous: false, + }, + nodes: [{ id: 'project-123', type: 'nodes' }], + relationships: { + nodes: { + data: [ + { id: 'component-1', type: 'nodes' }, + { id: 'component-2', type: 'nodes' }, + ], + }, + }, + }); + }); + + it('should build correct link data with only root project', () => { + component.linkName.setValue('Test Link'); + component.anonymous.set(true); + + const result = component['buildLinkData'](['project-123'], 'project-123', 'Test Link', true); + + expect(result).toEqual({ + attributes: { + name: 'Test Link', + anonymous: true, + }, + nodes: [{ id: 'project-123', type: 'nodes' }], + }); + }); + + it('should build correct link data with root project and components', () => { + component.linkName.setValue('Test Link'); + component.anonymous.set(false); + + const result = component['buildLinkData']( + ['project-123', 'component-1', 'component-2'], + 'project-123', + 'Test Link', + false + ); + + expect(result).toEqual({ + attributes: { + name: 'Test Link', + anonymous: false, + }, + nodes: [{ id: 'project-123', type: 'nodes' }], + relationships: { + nodes: { + data: [ + { id: 'component-1', type: 'nodes' }, + { id: 'component-2', type: 'nodes' }, + ], + }, + }, + }); + }); + + it('should build correct link data without root project', () => { + const result = component['buildLinkData'](['component-1', 'component-2'], 'project-123', 'Test Link', true); + + expect(result).toEqual({ + attributes: { + name: 'Test Link', + anonymous: true, + }, + nodes: [], + relationships: { + nodes: { + data: [ + { id: 'component-1', type: 'nodes' }, + { id: 'component-2', type: 'nodes' }, + ], + }, + }, + }); + }); }); diff --git a/src/app/features/project/contributors/contributors.component.spec.ts b/src/app/features/project/contributors/contributors.component.spec.ts index fb45f2cde..4c47b5d4a 100644 --- a/src/app/features/project/contributors/contributors.component.spec.ts +++ b/src/app/features/project/contributors/contributors.component.spec.ts @@ -1,14 +1,76 @@ +import { MockProvider } from 'ng-mocks'; + +import { ConfirmationService } from 'primeng/api'; +import { DialogService } from 'primeng/dynamicdialog'; + +import { of } from 'rxjs'; + import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ActivatedRoute, Router } from '@angular/router'; + +import { ContributorPermission } from '@shared/enums'; +import { MOCK_CONTRIBUTOR, MOCK_CONTRIBUTOR_WITHOUT_HISTORY, MOCK_PAGINATED_VIEW_ONLY_LINKS } from '@shared/mocks'; +import { ContributorModel } from '@shared/models'; +import { CustomConfirmationService, ToastService } from '@shared/services'; +import { ContributorsSelectors, CurrentResourceSelectors, ViewOnlyLinkSelectors } from '@shared/stores'; import { ContributorsComponent } from './contributors.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('ContributorsComponent', () => { let component: ContributorsComponent; let fixture: ComponentFixture; + let routerMock: ReturnType; + let activatedRouteMock: ReturnType; + + const mockContributors: ContributorModel[] = [MOCK_CONTRIBUTOR, MOCK_CONTRIBUTOR_WITHOUT_HISTORY]; + + const mockResourceDetails = { + id: 'test-id', + title: 'Test Project', + rootParentId: 'root-id', + }; beforeEach(async () => { + jest.useFakeTimers(); + + routerMock = RouterMockBuilder.create().build(); + activatedRouteMock = ActivatedRouteMockBuilder.create() + .withId('test-id') + .withData({ resourceType: 'project' }) + .build(); + await TestBed.configureTestingModule({ - imports: [ContributorsComponent], + imports: [ContributorsComponent, OSFTestingModule], + providers: [ + MockProvider(Router, routerMock), + MockProvider(ActivatedRoute, activatedRouteMock), + + MockProvider(DialogService, { + open: jest.fn().mockReturnValue({ onClose: of({}) }), + }), + MockProvider(ToastService, { + showSuccess: jest.fn(), + showError: jest.fn(), + }), + MockProvider(CustomConfirmationService, { + confirmDelete: jest.fn(), + }), + MockProvider(ConfirmationService, {}), + provideMockStore({ + signals: [ + { selector: ContributorsSelectors.getContributors, value: mockContributors }, + { selector: ContributorsSelectors.isContributorsLoading, value: false }, + { selector: ViewOnlyLinkSelectors.getViewOnlyLinks, value: MOCK_PAGINATED_VIEW_ONLY_LINKS }, + { selector: ViewOnlyLinkSelectors.isViewOnlyLinksLoading, value: false }, + { selector: CurrentResourceSelectors.getResourceDetails, value: mockResourceDetails }, + ], + }), + ], }).compileComponents(); fixture = TestBed.createComponent(ContributorsComponent); @@ -16,7 +78,119 @@ describe('ContributorsComponent', () => { fixture.detectChanges(); }); + afterEach(() => { + jest.useRealTimers(); + }); + it('should create', () => { expect(component).toBeTruthy(); }); + + it('should update search value with debounce', () => { + expect(() => component.searchControl.setValue('test search')).not.toThrow(); + + jest.advanceTimersByTime(600); + + expect(component.searchControl.value).toBe('test search'); + }); + + it('should handle null search value', () => { + expect(() => component.searchControl.setValue(null)).not.toThrow(); + + jest.advanceTimersByTime(600); + + expect(component.searchControl.value).toBe(null); + }); + + it('should update permission filter', () => { + expect(() => component.onPermissionChange(ContributorPermission.Read)).not.toThrow(); + }); + + it('should update bibliography filter', () => { + expect(() => component.onBibliographyChange(true)).not.toThrow(); + }); + + it('should create view link', () => { + const mockDialogRef = { + onClose: of({ name: 'Test Link', anonymous: false }), + }; + jest.spyOn(component.dialogService, 'open').mockReturnValue(mockDialogRef as any); + jest.spyOn(component.toastService, 'showSuccess'); + + expect(() => component.createViewLink()).not.toThrow(); + expect(component.dialogService.open).toHaveBeenCalled(); + }); + + it('should delete view link with confirmation', () => { + jest.spyOn(component.customConfirmationService, 'confirmDelete'); + jest.spyOn(component.toastService, 'showSuccess'); + + component.deleteLinkItem(MOCK_PAGINATED_VIEW_ONLY_LINKS.items[0]); + + expect(component.customConfirmationService.confirmDelete).toHaveBeenCalledWith({ + headerKey: 'myProjects.settings.delete.title', + headerParams: { name: MOCK_PAGINATED_VIEW_ONLY_LINKS.items[0].name }, + messageKey: 'myProjects.settings.delete.message', + onConfirm: expect.any(Function), + }); + }); + + it('should handle view link deletion confirmation', () => { + let confirmCallback: () => void; + jest.spyOn(component.customConfirmationService, 'confirmDelete').mockImplementation((options) => { + confirmCallback = options.onConfirm; + }); + jest.spyOn(component.toastService, 'showSuccess'); + + component.deleteLinkItem(MOCK_PAGINATED_VIEW_ONLY_LINKS.items[0]); + + expect(() => confirmCallback!()).not.toThrow(); + }); + + it('should detect changes correctly', () => { + expect(component.hasChanges).toBe(false); + + const modifiedContributors = [...mockContributors]; + modifiedContributors[0].permission = 'write'; + (component.contributors as any).set(modifiedContributors); + + expect((component.contributors as any)()).toEqual(modifiedContributors); + }); + + it('should cancel changes', () => { + const modifiedContributors = [...mockContributors]; + modifiedContributors[0].permission = 'write'; + (component.contributors as any).set(modifiedContributors); + + component.cancel(); + + expect((component.contributors as any)()).toEqual(mockContributors); + }); + + it('should save changes', () => { + jest.spyOn(component.toastService, 'showSuccess'); + + const modifiedContributors = [...mockContributors]; + modifiedContributors[0].permission = 'write'; + (component.contributors as any).set(modifiedContributors); + + expect(() => component.save()).not.toThrow(); + }); + + it('should handle save errors', () => { + jest.spyOn(component.toastService, 'showError'); + + const modifiedContributors = [...mockContributors]; + modifiedContributors[0].permission = 'write'; + (component.contributors as any).set(modifiedContributors); + + expect(() => component.save()).not.toThrow(); + }); + + it('should update contributors when initialContributors changes', () => { + const newContributors = [...mockContributors, MOCK_CONTRIBUTOR]; + + expect(() => (component.contributors as any).set(newContributors)).not.toThrow(); + expect((component.contributors as any)()).toEqual(newContributors); + }); }); diff --git a/src/app/shared/mocks/index.ts b/src/app/shared/mocks/index.ts index b18854749..1b3c1188e 100644 --- a/src/app/shared/mocks/index.ts +++ b/src/app/shared/mocks/index.ts @@ -24,3 +24,4 @@ export { MOCK_REVIEW } from './review.mock'; export { MOCK_SCOPES } from './scope.mock'; export { MOCK_TOKEN } from './token.mock'; export { TranslateServiceMock } from './translate.service.mock'; +export * from './view-only-link.mock'; diff --git a/src/app/shared/mocks/view-only-link.mock.ts b/src/app/shared/mocks/view-only-link.mock.ts new file mode 100644 index 000000000..a51b61ab9 --- /dev/null +++ b/src/app/shared/mocks/view-only-link.mock.ts @@ -0,0 +1,32 @@ +import { ViewOnlyLinkComponentItem } from '@osf/features/project/contributors/models'; +import { PaginatedViewOnlyLinksModel } from '@shared/models'; + +export const MOCK_VIEW_ONLY_LINK_COMPONENT_ITEM: ViewOnlyLinkComponentItem = { + id: 'test-id', + title: 'Test Component', + isCurrentResource: false, + disabled: false, + checked: false, + parentId: null, +}; + +export const MOCK_VIEW_ONLY_LINKS = [ + { + id: 'link-1', + dateCreated: '2023-01-01', + key: 'test-key', + name: 'Test Link', + link: 'https://test.com', + creator: { id: 'user-1', fullName: 'John Doe' }, + nodes: [{ id: 'node-1', title: 'Test Node', category: 'project' }], + anonymous: false, + }, +]; + +export const MOCK_PAGINATED_VIEW_ONLY_LINKS: PaginatedViewOnlyLinksModel = { + items: MOCK_VIEW_ONLY_LINKS, + total: MOCK_VIEW_ONLY_LINKS.length, + perPage: 10, + next: null, + prev: null, +}; From 44c4fda090ecdba9c216d9811b8e0dcf68868607 Mon Sep 17 00:00:00 2001 From: Diana Date: Fri, 12 Sep 2025 13:56:26 +0300 Subject: [PATCH 2/7] fix(config): fixed --- jest.config.js | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/jest.config.js b/jest.config.js index 369b04e14..4dfc0f820 100644 --- a/jest.config.js +++ b/jest.config.js @@ -62,23 +62,13 @@ module.exports = { testPathIgnorePatterns: [ '/src/app/app.config.ts', '/src/app/app.routes.ts', - '/src/app/features/registry/', - '/src/app/features/project/addons/components/configure-configure-addon/', - '/src/app/features/project/addons/components/connect-configured-addon/', - '/src/app/features/project/addons/components/disconnect-addon-modal/', - '/src/app/features/project/addons/components/confirm-account-connection-modal/', '/src/app/features/files/components', - '/src/app/features/files/pages/community-metadata', '/src/app/features/files/pages/file-detail', '/src/app/features/preprints/', - '/src/app/features/project/overview/', - '/src/app/features/project/registrations', - '/src/app/features/project/settings', - '/src/app/features/project/wiki', - '/src/app/features/project/project.component.ts', + '/src/app/features/project/', '/src/app/features/registries/', + '/src/app/features/registry/', '/src/app/features/settings/addons/', - '/src/app/features/settings/tokens/mappers/', '/src/app/features/settings/tokens/store/', '/src/app/shared/components/file-menu/', '/src/app/shared/components/files-tree/', @@ -87,7 +77,6 @@ module.exports = { '/src/app/shared/components/pie-chart/', '/src/app/shared/components/resource-citations/', '/src/app/shared/components/reusable-filter/', - '/src/app/shared/components/subjects/', '/src/app/shared/components/wiki/edit-section/', '/src/app/shared/components/wiki/wiki-list/', ], From b88148e3602aa10372c19c901561487e79cb28c2 Mon Sep 17 00:00:00 2001 From: Diana Date: Fri, 12 Sep 2025 14:12:13 +0300 Subject: [PATCH 3/7] fix(config): fixed --- jest.config.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/jest.config.js b/jest.config.js index c7511d422..f5f2446c1 100644 --- a/jest.config.js +++ b/jest.config.js @@ -65,7 +65,11 @@ module.exports = { '/src/app/features/files/components', '/src/app/features/files/pages/file-detail', '/src/app/features/preprints/', - '/src/app/features/project/', + '/src/app/features/project/addons/', + '/src/app/features/project/overview/', + '/src/app/features/project/registrations', + '/src/app/features/project/settings', + '/src/app/features/project/wiki', '/src/app/features/registries/', '/src/app/features/registry/', '/src/app/features/settings/addons/', From 65e86acb922cae5dd9a4a585926ecffd224a471e Mon Sep 17 00:00:00 2001 From: Diana Date: Fri, 12 Sep 2025 14:25:33 +0300 Subject: [PATCH 4/7] test(contributors): added mocks --- .../create-view-link-dialog.component.spec.ts | 22 +++---------------- .../contributors.component.spec.ts | 15 ++++++------- src/app/shared/mocks/resource.mock.ts | 15 +++++++++++++ 3 files changed, 25 insertions(+), 27 deletions(-) diff --git a/src/app/features/project/contributors/components/create-view-link-dialog/create-view-link-dialog.component.spec.ts b/src/app/features/project/contributors/components/create-view-link-dialog/create-view-link-dialog.component.spec.ts index 7c9db265e..2ece6ab17 100644 --- a/src/app/features/project/contributors/components/create-view-link-dialog/create-view-link-dialog.component.spec.ts +++ b/src/app/features/project/contributors/components/create-view-link-dialog/create-view-link-dialog.component.spec.ts @@ -4,10 +4,8 @@ import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { ResourceType } from '@osf/shared/enums'; import { CurrentResourceSelectors } from '@osf/shared/stores'; - -import { ResourceInfoModel } from '../../models'; +import { MOCK_RESOURCE_INFO, MOCK_RESOURCE_WITH_CHILDREN } from '@shared/mocks'; import { CreateViewLinkDialogComponent } from './create-view-link-dialog.component'; @@ -20,27 +18,13 @@ describe('CreateViewLinkDialogComponent', () => { let dialogRef: jest.Mocked; let dialogConfig: DynamicDialogConfig; - const mockResourceInfo: ResourceInfoModel = { - id: 'project-123', - title: 'Test Project', - type: ResourceType.Project, - rootParentId: 'root-123', - }; - - const mockComponents = [ - { id: 'project-123', title: 'Test Project', parentId: null }, - { id: 'component-1', title: 'Component 1', parentId: 'project-123' }, - { id: 'component-2', title: 'Component 2', parentId: 'project-123' }, - { id: 'component-3', title: 'Component 3', parentId: 'component-1' }, - ]; - beforeEach(async () => { dialogRef = { close: jest.fn(), } as any; dialogConfig = { - data: mockResourceInfo, + data: MOCK_RESOURCE_INFO, } as DynamicDialogConfig; await TestBed.configureTestingModule({ @@ -50,7 +34,7 @@ describe('CreateViewLinkDialogComponent', () => { signals: [ { selector: CurrentResourceSelectors.getResourceWithChildren, - value: mockComponents, + value: MOCK_RESOURCE_WITH_CHILDREN, }, { selector: CurrentResourceSelectors.isResourceWithChildrenLoading, diff --git a/src/app/features/project/contributors/contributors.component.spec.ts b/src/app/features/project/contributors/contributors.component.spec.ts index 4c47b5d4a..67f1c65ba 100644 --- a/src/app/features/project/contributors/contributors.component.spec.ts +++ b/src/app/features/project/contributors/contributors.component.spec.ts @@ -9,7 +9,12 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ActivatedRoute, Router } from '@angular/router'; import { ContributorPermission } from '@shared/enums'; -import { MOCK_CONTRIBUTOR, MOCK_CONTRIBUTOR_WITHOUT_HISTORY, MOCK_PAGINATED_VIEW_ONLY_LINKS } from '@shared/mocks'; +import { + MOCK_CONTRIBUTOR, + MOCK_CONTRIBUTOR_WITHOUT_HISTORY, + MOCK_PAGINATED_VIEW_ONLY_LINKS, + MOCK_RESOURCE_INFO, +} from '@shared/mocks'; import { ContributorModel } from '@shared/models'; import { CustomConfirmationService, ToastService } from '@shared/services'; import { ContributorsSelectors, CurrentResourceSelectors, ViewOnlyLinkSelectors } from '@shared/stores'; @@ -29,12 +34,6 @@ describe('ContributorsComponent', () => { const mockContributors: ContributorModel[] = [MOCK_CONTRIBUTOR, MOCK_CONTRIBUTOR_WITHOUT_HISTORY]; - const mockResourceDetails = { - id: 'test-id', - title: 'Test Project', - rootParentId: 'root-id', - }; - beforeEach(async () => { jest.useFakeTimers(); @@ -67,7 +66,7 @@ describe('ContributorsComponent', () => { { selector: ContributorsSelectors.isContributorsLoading, value: false }, { selector: ViewOnlyLinkSelectors.getViewOnlyLinks, value: MOCK_PAGINATED_VIEW_ONLY_LINKS }, { selector: ViewOnlyLinkSelectors.isViewOnlyLinksLoading, value: false }, - { selector: CurrentResourceSelectors.getResourceDetails, value: mockResourceDetails }, + { selector: CurrentResourceSelectors.getResourceDetails, value: MOCK_RESOURCE_INFO }, ], }), ], diff --git a/src/app/shared/mocks/resource.mock.ts b/src/app/shared/mocks/resource.mock.ts index 43255215b..7de6b6947 100644 --- a/src/app/shared/mocks/resource.mock.ts +++ b/src/app/shared/mocks/resource.mock.ts @@ -1,3 +1,4 @@ +import { ResourceInfoModel } from '@osf/features/project/contributors/models'; import { ResourceType } from '@shared/enums'; import { ResourceModel, ResourceOverview } from '@shared/models'; @@ -67,3 +68,17 @@ export const MOCK_RESOURCE_OVERVIEW: ResourceOverview = { customCitation: 'Custom citation text', forksCount: 0, }; + +export const MOCK_RESOURCE_INFO: ResourceInfoModel = { + id: 'project-123', + title: 'Test Project', + type: ResourceType.Project, + rootParentId: 'root-123', +}; + +export const MOCK_RESOURCE_WITH_CHILDREN = [ + { id: 'project-123', title: 'Test Project', parentId: null }, + { id: 'component-1', title: 'Component 1', parentId: 'project-123' }, + { id: 'component-2', title: 'Component 2', parentId: 'project-123' }, + { id: 'component-3', title: 'Component 3', parentId: 'component-1' }, +]; From 565c533e54db4e69099bc7d8478dc8a47e8c7a52 Mon Sep 17 00:00:00 2001 From: Diana Date: Fri, 12 Sep 2025 15:17:55 +0300 Subject: [PATCH 5/7] test(contributors): added toast service mock builder --- .../contributors.component.spec.ts | 9 ++-- .../notifications.component.spec.ts | 10 ++-- .../token-add-edit-form.component.spec.ts | 10 ++-- src/testing/providers/toast-provider.mock.ts | 53 +++++++++++++++++++ 4 files changed, 67 insertions(+), 15 deletions(-) create mode 100644 src/testing/providers/toast-provider.mock.ts diff --git a/src/app/features/project/contributors/contributors.component.spec.ts b/src/app/features/project/contributors/contributors.component.spec.ts index 67f1c65ba..231aea41c 100644 --- a/src/app/features/project/contributors/contributors.component.spec.ts +++ b/src/app/features/project/contributors/contributors.component.spec.ts @@ -25,12 +25,14 @@ import { OSFTestingModule } from '@testing/osf.testing.module'; import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock'; import { RouterMockBuilder } from '@testing/providers/router-provider.mock'; import { provideMockStore } from '@testing/providers/store-provider.mock'; +import { ToastServiceMockBuilder } from '@testing/providers/toast-provider.mock'; describe('ContributorsComponent', () => { let component: ContributorsComponent; let fixture: ComponentFixture; let routerMock: ReturnType; let activatedRouteMock: ReturnType; + let toastServiceMock: ReturnType; const mockContributors: ContributorModel[] = [MOCK_CONTRIBUTOR, MOCK_CONTRIBUTOR_WITHOUT_HISTORY]; @@ -42,20 +44,17 @@ describe('ContributorsComponent', () => { .withId('test-id') .withData({ resourceType: 'project' }) .build(); + toastServiceMock = ToastServiceMockBuilder.create().build(); await TestBed.configureTestingModule({ imports: [ContributorsComponent, OSFTestingModule], providers: [ MockProvider(Router, routerMock), MockProvider(ActivatedRoute, activatedRouteMock), - MockProvider(DialogService, { open: jest.fn().mockReturnValue({ onClose: of({}) }), }), - MockProvider(ToastService, { - showSuccess: jest.fn(), - showError: jest.fn(), - }), + MockProvider(ToastService, toastServiceMock), MockProvider(CustomConfirmationService, { confirmDelete: jest.fn(), }), diff --git a/src/app/features/settings/notifications/notifications.component.spec.ts b/src/app/features/settings/notifications/notifications.component.spec.ts index 192cb7d70..1195c354f 100644 --- a/src/app/features/settings/notifications/notifications.component.spec.ts +++ b/src/app/features/settings/notifications/notifications.component.spec.ts @@ -20,10 +20,13 @@ import { UserSettings } from '@shared/models'; import { NotificationsComponent } from './notifications.component'; import { NotificationSubscriptionSelectors } from './store'; +import { ToastServiceMockBuilder } from '@testing/providers/toast-provider.mock'; + describe('NotificationsComponent', () => { let component: NotificationsComponent; let fixture: ComponentFixture; let loaderService: LoaderService; + let toastServiceMock: ReturnType; const mockUserSettings: UserSettings = { subscribeOsfGeneralEmail: true, @@ -40,10 +43,7 @@ describe('NotificationsComponent', () => { ]; beforeEach(async () => { - const mockToastService = { - showSuccess: jest.fn(), - showError: jest.fn(), - }; + toastServiceMock = ToastServiceMockBuilder.create().build(); const mockLoaderService = { show: jest.fn(), @@ -82,7 +82,7 @@ describe('NotificationsComponent', () => { TranslateServiceMock, MockProvider(Store, MOCK_STORE), MockProvider(LoaderService, mockLoaderService), - MockProvider(ToastService, mockToastService), + MockProvider(ToastService, toastServiceMock), FormBuilder, ], }).compileComponents(); diff --git a/src/app/features/settings/tokens/components/token-add-edit-form/token-add-edit-form.component.spec.ts b/src/app/features/settings/tokens/components/token-add-edit-form/token-add-edit-form.component.spec.ts index cca9abd13..7b5a56a98 100644 --- a/src/app/features/settings/tokens/components/token-add-edit-form/token-add-edit-form.component.spec.ts +++ b/src/app/features/settings/tokens/components/token-add-edit-form/token-add-edit-form.component.spec.ts @@ -22,6 +22,7 @@ import { CreateToken, TokensSelectors } from '../../store'; import { TokenAddEditFormComponent } from './token-add-edit-form.component'; import { OSFTestingStoreModule } from '@testing/osf.testing.module'; +import { ToastServiceMockBuilder } from '@testing/providers/toast-provider.mock'; describe('TokenAddEditFormComponent', () => { let component: TokenAddEditFormComponent; @@ -32,6 +33,7 @@ describe('TokenAddEditFormComponent', () => { let router: Partial; let toastService: jest.Mocked; let translateService: jest.Mocked; + let toastServiceMock: ReturnType; const mockTokens: TokenModel[] = [MOCK_TOKEN]; @@ -69,6 +71,8 @@ describe('TokenAddEditFormComponent', () => { navigate: jest.fn(), }; + toastServiceMock = ToastServiceMockBuilder.create().build(); + await TestBed.configureTestingModule({ imports: [TokenAddEditFormComponent, ReactiveFormsModule, OSFTestingStoreModule], providers: [ @@ -78,11 +82,7 @@ describe('TokenAddEditFormComponent', () => { MockProvider(DynamicDialogRef, dialogRef), MockProvider(ActivatedRoute, activatedRoute), MockProvider(Router, router), - MockProvider(ToastService, { - showSuccess: jest.fn(), - showWarn: jest.fn(), - showError: jest.fn(), - }), + MockProvider(ToastService, toastServiceMock), ], }).compileComponents(); diff --git a/src/testing/providers/toast-provider.mock.ts b/src/testing/providers/toast-provider.mock.ts new file mode 100644 index 000000000..f5db862f2 --- /dev/null +++ b/src/testing/providers/toast-provider.mock.ts @@ -0,0 +1,53 @@ +import { ToastService } from '@osf/shared/services'; + +export type ToastServiceMockType = Partial & { + showSuccess: jest.Mock; + showWarn: jest.Mock; + showError: jest.Mock; +}; + +export class ToastServiceMockBuilder { + private showSuccessMock: jest.Mock = jest.fn(); + private showWarnMock: jest.Mock = jest.fn(); + private showErrorMock: jest.Mock = jest.fn(); + + static create(): ToastServiceMockBuilder { + return new ToastServiceMockBuilder(); + } + + withShowSuccess(mockImpl: jest.Mock): ToastServiceMockBuilder { + this.showSuccessMock = mockImpl; + return this; + } + + withShowWarn(mockImpl: jest.Mock): ToastServiceMockBuilder { + this.showWarnMock = mockImpl; + return this; + } + + withShowError(mockImpl: jest.Mock): ToastServiceMockBuilder { + this.showErrorMock = mockImpl; + return this; + } + + build(): ToastServiceMockType { + return { + showSuccess: this.showSuccessMock, + showWarn: this.showWarnMock, + showError: this.showErrorMock, + } as ToastServiceMockType; + } +} + +export const ToastServiceMock = { + create() { + return ToastServiceMockBuilder.create(); + }, + simple() { + return { + showSuccess: jest.fn(), + showWarn: jest.fn(), + showError: jest.fn(), + } as ToastServiceMockType; + }, +}; From 4b789d58e02f7c8244816dfa6dd76ade6f70ea6d Mon Sep 17 00:00:00 2001 From: Diana Date: Fri, 12 Sep 2025 15:28:53 +0300 Subject: [PATCH 6/7] test(contributors): added custom confirmation service mock builder --- .../contributors.component.spec.ts | 7 +-- .../custom-confirmation-provider.mock.ts | 54 +++++++++++++++++++ 2 files changed, 58 insertions(+), 3 deletions(-) create mode 100644 src/testing/providers/custom-confirmation-provider.mock.ts diff --git a/src/app/features/project/contributors/contributors.component.spec.ts b/src/app/features/project/contributors/contributors.component.spec.ts index 231aea41c..d5764ab3e 100644 --- a/src/app/features/project/contributors/contributors.component.spec.ts +++ b/src/app/features/project/contributors/contributors.component.spec.ts @@ -22,6 +22,7 @@ import { ContributorsSelectors, CurrentResourceSelectors, ViewOnlyLinkSelectors import { ContributorsComponent } from './contributors.component'; 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 { RouterMockBuilder } from '@testing/providers/router-provider.mock'; import { provideMockStore } from '@testing/providers/store-provider.mock'; @@ -33,6 +34,7 @@ describe('ContributorsComponent', () => { let routerMock: ReturnType; let activatedRouteMock: ReturnType; let toastServiceMock: ReturnType; + let customConfirmationServiceMock: ReturnType; const mockContributors: ContributorModel[] = [MOCK_CONTRIBUTOR, MOCK_CONTRIBUTOR_WITHOUT_HISTORY]; @@ -45,6 +47,7 @@ describe('ContributorsComponent', () => { .withData({ resourceType: 'project' }) .build(); toastServiceMock = ToastServiceMockBuilder.create().build(); + customConfirmationServiceMock = CustomConfirmationServiceMockBuilder.create().build(); await TestBed.configureTestingModule({ imports: [ContributorsComponent, OSFTestingModule], @@ -55,9 +58,7 @@ describe('ContributorsComponent', () => { open: jest.fn().mockReturnValue({ onClose: of({}) }), }), MockProvider(ToastService, toastServiceMock), - MockProvider(CustomConfirmationService, { - confirmDelete: jest.fn(), - }), + MockProvider(CustomConfirmationService, customConfirmationServiceMock), MockProvider(ConfirmationService, {}), provideMockStore({ signals: [ diff --git a/src/testing/providers/custom-confirmation-provider.mock.ts b/src/testing/providers/custom-confirmation-provider.mock.ts new file mode 100644 index 000000000..e95130886 --- /dev/null +++ b/src/testing/providers/custom-confirmation-provider.mock.ts @@ -0,0 +1,54 @@ +import { CustomConfirmationService } from '@osf/shared/services'; +import { AcceptConfirmationOptions, ContinueConfirmationOptions, DeleteConfirmationOptions } from '@shared/models'; + +export type CustomConfirmationServiceMockType = Partial & { + confirmDelete: jest.Mock; + confirmAccept: jest.Mock; + confirmContinue: jest.Mock; +}; + +export class CustomConfirmationServiceMockBuilder { + private confirmDeleteMock: jest.Mock = jest.fn(); + private confirmAcceptMock: jest.Mock = jest.fn(); + private confirmContinueMock: jest.Mock = jest.fn(); + + static create(): CustomConfirmationServiceMockBuilder { + return new CustomConfirmationServiceMockBuilder(); + } + + withConfirmDelete(mockImpl: jest.Mock): CustomConfirmationServiceMockBuilder { + this.confirmDeleteMock = mockImpl; + return this; + } + + withConfirmAccept(mockImpl: jest.Mock): CustomConfirmationServiceMockBuilder { + this.confirmAcceptMock = mockImpl; + return this; + } + + withConfirmContinue(mockImpl: jest.Mock): CustomConfirmationServiceMockBuilder { + this.confirmContinueMock = mockImpl; + return this; + } + + build(): CustomConfirmationServiceMockType { + return { + confirmDelete: this.confirmDeleteMock, + confirmAccept: this.confirmAcceptMock, + confirmContinue: this.confirmContinueMock, + } as CustomConfirmationServiceMockType; + } +} + +export const CustomConfirmationServiceMock = { + create() { + return CustomConfirmationServiceMockBuilder.create(); + }, + simple() { + return { + confirmDelete: jest.fn(), + confirmAccept: jest.fn(), + confirmContinue: jest.fn(), + } as CustomConfirmationServiceMockType; + }, +}; From 4717cb87a6a1637790ca26228d7fb7325b598f2b Mon Sep 17 00:00:00 2001 From: Diana Date: Fri, 12 Sep 2025 16:16:49 +0300 Subject: [PATCH 7/7] fix(tests): fixed some tests --- .../funding-dialog/funding-dialog.component.spec.ts | 4 ++-- src/app/shared/services/addons/addons.service.spec.ts | 2 +- src/app/shared/stores/addons/addons.state.spec.ts | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/app/features/metadata/dialogs/funding-dialog/funding-dialog.component.spec.ts b/src/app/features/metadata/dialogs/funding-dialog/funding-dialog.component.spec.ts index 3054bbaf2..777fae191 100644 --- a/src/app/features/metadata/dialogs/funding-dialog/funding-dialog.component.spec.ts +++ b/src/app/features/metadata/dialogs/funding-dialog/funding-dialog.component.spec.ts @@ -29,7 +29,7 @@ describe('FundingDialogComponent', () => { providers: [ TranslateServiceMock, MockProviders(DynamicDialogRef, DestroyRef), - MockProvider(DynamicDialogConfig, { data: null }), + MockProvider(DynamicDialogConfig, { data: { funders: [] } }), MockProvider(Store, MOCK_STORE), ], }).compileComponents(); @@ -123,7 +123,7 @@ describe('FundingDialogComponent', () => { const awardTitleControl = entry.get('awardTitle'); expect(funderNameControl?.hasError('required')).toBe(true); - expect(awardTitleControl?.hasError('required')).toBe(true); + expect(awardTitleControl?.hasError('required')).toBe(false); funderNameControl?.setValue('Test Funder'); awardTitleControl?.setValue('Test Award'); diff --git a/src/app/shared/services/addons/addons.service.spec.ts b/src/app/shared/services/addons/addons.service.spec.ts index 9920d2444..334562221 100644 --- a/src/app/shared/services/addons/addons.service.spec.ts +++ b/src/app/shared/services/addons/addons.service.spec.ts @@ -88,7 +88,7 @@ describe('Service: Addons', () => { }); const request = httpMock.expectOne( - 'https://addons.staging4.osf.io/v1/user-references/reference-id/authorized_storage_accounts/?include=external-storage-service&fields%5Bexternal-storage-services%5D=external_service_name' + 'https://addons.staging4.osf.io/v1/user-references/reference-id/authorized_storage_accounts/?include=external-storage-service&fields%5Bexternal-storage-services%5D=external_service_name,credentials_format' ); expect(request.request.method).toBe('GET'); request.flush(getAddonsAuthorizedStorageData()); diff --git a/src/app/shared/stores/addons/addons.state.spec.ts b/src/app/shared/stores/addons/addons.state.spec.ts index 573c561dd..1c71b922d 100644 --- a/src/app/shared/stores/addons/addons.state.spec.ts +++ b/src/app/shared/stores/addons/addons.state.spec.ts @@ -206,7 +206,7 @@ describe('State: Addons', () => { expect(loading()).toBeTruthy(); const request = httpMock.expectOne( - 'https://addons.staging4.osf.io/v1/user-references/reference-id/authorized_storage_accounts/?include=external-storage-service&fields%5Bexternal-storage-services%5D=external_service_name' + 'https://addons.staging4.osf.io/v1/user-references/reference-id/authorized_storage_accounts/?include=external-storage-service&fields%5Bexternal-storage-services%5D=external_service_name,credentials_format' ); expect(request.request.method).toBe('GET'); request.flush(getAddonsAuthorizedStorageData()); @@ -255,7 +255,7 @@ describe('State: Addons', () => { expect(loading()).toBeTruthy(); const request = httpMock.expectOne( - 'https://addons.staging4.osf.io/v1/user-references/reference-id/authorized_storage_accounts/?include=external-storage-service&fields%5Bexternal-storage-services%5D=external_service_name' + 'https://addons.staging4.osf.io/v1/user-references/reference-id/authorized_storage_accounts/?include=external-storage-service&fields%5Bexternal-storage-services%5D=external_service_name,credentials_format' ); expect(request.request.method).toBe('GET'); @@ -264,7 +264,7 @@ describe('State: Addons', () => { expect(result).toEqual({ data: [], error: - 'Http failure response for https://addons.staging4.osf.io/v1/user-references/reference-id/authorized_storage_accounts/?include=external-storage-service&fields%5Bexternal-storage-services%5D=external_service_name: 500 Server Error', + 'Http failure response for https://addons.staging4.osf.io/v1/user-references/reference-id/authorized_storage_accounts/?include=external-storage-service&fields%5Bexternal-storage-services%5D=external_service_name,credentials_format: 500 Server Error', isLoading: false, isSubmitting: false, }); @@ -335,7 +335,7 @@ describe('State: Addons', () => { expect(loading()).toBeTruthy(); let request = httpMock.expectOne( - 'https://addons.staging4.osf.io/v1/user-references/reference-id/authorized_storage_accounts/?include=external-storage-service&fields%5Bexternal-storage-services%5D=external_service_name' + 'https://addons.staging4.osf.io/v1/user-references/reference-id/authorized_storage_accounts/?include=external-storage-service&fields%5Bexternal-storage-services%5D=external_service_name,credentials_format' ); expect(request.request.method).toBe('GET'); request.flush(getAddonsAuthorizedStorageData());