From 477494ed3abbc6abf7ebf056f9c7bed1f698e000 Mon Sep 17 00:00:00 2001 From: Diana Date: Sun, 14 Sep 2025 14:25:47 +0300 Subject: [PATCH 1/2] test(settings): added new tests and mocks --- jest.config.js | 1 - ...detail-setting-accordion.component.spec.ts | 25 ++- ...ct-setting-notifications.component.spec.ts | 99 ++++++++++- ...ngs-access-requests-card.component.spec.ts | 26 ++- ...ings-project-affiliation.component.spec.ts | 36 +++- ...ttings-project-form-card.component.spec.ts | 107 +++++++++++- ...gs-storage-location-card.component.spec.ts | 20 ++- ...ngs-view-only-links-card.component.spec.ts | 80 ++++++++- .../settings-wiki-card.component.spec.ts | 106 +++++++++++- .../settings/settings.component.spec.ts | 160 +++++++++++++++++- .../view-only-table.component.spec.ts | 34 +--- src/app/shared/mocks/index.ts | 3 + src/app/shared/mocks/node-details.mock.ts | 11 ++ .../mocks/notification-subscription.mock.ts | 25 +++ src/app/shared/mocks/view-only-link.mock.ts | 45 +++-- 15 files changed, 704 insertions(+), 74 deletions(-) create mode 100644 src/app/shared/mocks/node-details.mock.ts create mode 100644 src/app/shared/mocks/notification-subscription.mock.ts diff --git a/jest.config.js b/jest.config.js index f5f2446c1..1dd22f383 100644 --- a/jest.config.js +++ b/jest.config.js @@ -68,7 +68,6 @@ module.exports = { '/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/', diff --git a/src/app/features/project/settings/components/project-detail-setting-accordion/project-detail-setting-accordion.component.spec.ts b/src/app/features/project/settings/components/project-detail-setting-accordion/project-detail-setting-accordion.component.spec.ts index 320e8c125..028d9b986 100644 --- a/src/app/features/project/settings/components/project-detail-setting-accordion/project-detail-setting-accordion.component.spec.ts +++ b/src/app/features/project/settings/components/project-detail-setting-accordion/project-detail-setting-accordion.component.spec.ts @@ -1,22 +1,41 @@ +import { MockComponent } from 'ng-mocks'; + import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { SelectComponent } from '@shared/components'; + import { ProjectDetailSettingAccordionComponent } from './project-detail-setting-accordion.component'; -describe('AccordionTableComponent', () => { +import { OSFTestingModule } from '@testing/osf.testing.module'; + +describe('ProjectDetailSettingAccordionComponent', () => { let component: ProjectDetailSettingAccordionComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [ProjectDetailSettingAccordionComponent], + imports: [ProjectDetailSettingAccordionComponent, OSFTestingModule, MockComponent(SelectComponent)], }).compileComponents(); fixture = TestBed.createComponent(ProjectDetailSettingAccordionComponent); component = fixture.componentInstance; - fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); + + it('should initialize with expanded set to false', () => { + expect(component.expanded()).toBe(false); + }); + + it('should toggle expanded state when toggle() is called', () => { + expect(component.expanded()).toBe(false); + + component.toggle(); + expect(component.expanded()).toBe(true); + + component.toggle(); + expect(component.expanded()).toBe(false); + }); }); diff --git a/src/app/features/project/settings/components/project-setting-notifications/project-setting-notifications.component.spec.ts b/src/app/features/project/settings/components/project-setting-notifications/project-setting-notifications.component.spec.ts index b7d1665f5..84a4ca9be 100644 --- a/src/app/features/project/settings/components/project-setting-notifications/project-setting-notifications.component.spec.ts +++ b/src/app/features/project/settings/components/project-setting-notifications/project-setting-notifications.component.spec.ts @@ -1,22 +1,117 @@ +import { MockComponent, MockPipe } from 'ng-mocks'; + import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ProjectDetailSettingAccordionComponent } from '@osf/features/project/settings/components'; +import { NotificationDescriptionPipe } from '@osf/features/project/settings/pipes'; +import { SubscriptionEvent, SubscriptionFrequency } from '@osf/shared/enums'; +import { MOCK_NOTIFICATION_SUBSCRIPTIONS } from '@osf/shared/mocks'; + import { ProjectSettingNotificationsComponent } from './project-setting-notifications.component'; +import { OSFTestingModule } from '@testing/osf.testing.module'; + describe('ProjectSettingNotificationsComponent', () => { let component: ProjectSettingNotificationsComponent; let fixture: ComponentFixture; + const mockNotifications = MOCK_NOTIFICATION_SUBSCRIPTIONS; + beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [ProjectSettingNotificationsComponent], + imports: [ + ProjectSettingNotificationsComponent, + OSFTestingModule, + MockComponent(ProjectDetailSettingAccordionComponent), + MockPipe(NotificationDescriptionPipe), + ], }).compileComponents(); fixture = TestBed.createComponent(ProjectSettingNotificationsComponent); component = fixture.componentInstance; - fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); + + it('should initialize with empty accordion data', () => { + expect(component.allAccordionData).toEqual([]); + }); + + it('should have correct subscription frequency options', () => { + const expectedOptions = [ + { label: 'Never', value: SubscriptionFrequency.Never }, + { label: 'Daily', value: SubscriptionFrequency.Daily }, + { label: 'Instant', value: SubscriptionFrequency.Instant }, + ]; + + expect(component.subscriptionFrequencyOptions).toEqual(expectedOptions); + }); + + it('should update accordion data when notifications input changes', () => { + fixture.componentRef.setInput('notifications', mockNotifications); + fixture.detectChanges(); + + expect(component.allAccordionData).toBeDefined(); + expect(component.allAccordionData?.length).toBe(2); + + if (component.allAccordionData) { + expect(component.allAccordionData[0]).toEqual({ + label: 'settings.notifications.notificationPreferences.items.files', + value: SubscriptionFrequency.Instant, + type: 'dropdown', + options: component.subscriptionFrequencyOptions, + event: SubscriptionEvent.FileUpdated, + }); + + expect(component.allAccordionData[1]).toEqual({ + label: 'settings.notifications.notificationPreferences.items.files', + value: SubscriptionFrequency.Daily, + type: 'dropdown', + options: component.subscriptionFrequencyOptions, + event: SubscriptionEvent.GlobalFileUpdated, + }); + } + }); + + it('should emit notification value change when changeEmittedValue is called', () => { + jest.spyOn(component.notificationEmitValue, 'emit'); + fixture.componentRef.setInput('notifications', mockNotifications); + fixture.detectChanges(); + + const emittedValue = { index: 0, value: SubscriptionFrequency.Never }; + component.changeEmittedValue(emittedValue); + + expect(component.notificationEmitValue.emit).toHaveBeenCalledWith({ + id: mockNotifications[0].id, + event: SubscriptionEvent.FileUpdated, + frequency: SubscriptionFrequency.Never, + }); + }); + + it('should not emit when allAccordionData is undefined', () => { + jest.spyOn(component.notificationEmitValue, 'emit'); + component.allAccordionData = undefined; + + const emittedValue = { index: 0, value: SubscriptionFrequency.Never }; + component.changeEmittedValue(emittedValue); + + expect(component.notificationEmitValue.emit).not.toHaveBeenCalled(); + }); + + it('should handle multiple notifications correctly', () => { + const multipleNotifications = MOCK_NOTIFICATION_SUBSCRIPTIONS.slice(0, 3); + + fixture.componentRef.setInput('notifications', multipleNotifications); + fixture.detectChanges(); + + expect(component.allAccordionData?.length).toBe(3); + + if (component.allAccordionData) { + expect(component.allAccordionData[0].event).toBe(SubscriptionEvent.FileUpdated); + expect(component.allAccordionData[1].event).toBe(SubscriptionEvent.GlobalFileUpdated); + expect(component.allAccordionData[2].event).toBe(SubscriptionEvent.GlobalMentions); + } + }); }); diff --git a/src/app/features/project/settings/components/settings-access-requests-card/settings-access-requests-card.component.spec.ts b/src/app/features/project/settings/components/settings-access-requests-card/settings-access-requests-card.component.spec.ts index af1f438ef..c5b4ca7fc 100644 --- a/src/app/features/project/settings/components/settings-access-requests-card/settings-access-requests-card.component.spec.ts +++ b/src/app/features/project/settings/components/settings-access-requests-card/settings-access-requests-card.component.spec.ts @@ -2,21 +2,43 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { SettingsAccessRequestsCardComponent } from './settings-access-requests-card.component'; +import { OSFTestingModule } from '@testing/osf.testing.module'; + describe('SettingsAccessRequestsCardComponent', () => { let component: SettingsAccessRequestsCardComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [SettingsAccessRequestsCardComponent], + imports: [SettingsAccessRequestsCardComponent, OSFTestingModule], }).compileComponents(); fixture = TestBed.createComponent(SettingsAccessRequestsCardComponent); component = fixture.componentInstance; - fixture.detectChanges(); }); it('should create', () => { + fixture.componentRef.setInput('accessRequest', true); + fixture.detectChanges(); expect(component).toBeTruthy(); }); + + it('should initialize with accessRequest input', () => { + const testValue = true; + fixture.componentRef.setInput('accessRequest', testValue); + fixture.detectChanges(); + + expect(component.accessRequest()).toBe(testValue); + }); + + it('should emit accessRequestChange when checkbox value changes', () => { + jest.spyOn(component.accessRequestChange, 'emit'); + fixture.componentRef.setInput('accessRequest', false); + fixture.detectChanges(); + + const newValue = true; + component.accessRequestChange.emit(newValue); + + expect(component.accessRequestChange.emit).toHaveBeenCalledWith(newValue); + }); }); diff --git a/src/app/features/project/settings/components/settings-project-affiliation/settings-project-affiliation.component.spec.ts b/src/app/features/project/settings/components/settings-project-affiliation/settings-project-affiliation.component.spec.ts index 685bcc895..98006ba87 100644 --- a/src/app/features/project/settings/components/settings-project-affiliation/settings-project-affiliation.component.spec.ts +++ b/src/app/features/project/settings/components/settings-project-affiliation/settings-project-affiliation.component.spec.ts @@ -1,22 +1,54 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { Institution } from '@osf/shared/models'; +import { MOCK_INSTITUTION } from '@shared/mocks'; + import { SettingsProjectAffiliationComponent } from './settings-project-affiliation.component'; +import { OSFTestingModule } from '@testing/osf.testing.module'; + describe('SettingsProjectAffiliationComponent', () => { let component: SettingsProjectAffiliationComponent; let fixture: ComponentFixture; + const mockInstitutions: Institution[] = [MOCK_INSTITUTION]; + beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [SettingsProjectAffiliationComponent], + imports: [SettingsProjectAffiliationComponent, OSFTestingModule], }).compileComponents(); fixture = TestBed.createComponent(SettingsProjectAffiliationComponent); component = fixture.componentInstance; - fixture.detectChanges(); }); it('should create', () => { + fixture.componentRef.setInput('affiliations', []); + fixture.detectChanges(); expect(component).toBeTruthy(); }); + + it('should initialize with empty affiliations array', () => { + fixture.componentRef.setInput('affiliations', []); + fixture.detectChanges(); + + expect(component.affiliations()).toEqual([]); + }); + + it('should display affiliations when provided', () => { + fixture.componentRef.setInput('affiliations', mockInstitutions); + fixture.detectChanges(); + + expect(component.affiliations()).toEqual(mockInstitutions); + }); + + it('should emit removed event when removeAffiliation is called', () => { + jest.spyOn(component.removed, 'emit'); + fixture.componentRef.setInput('affiliations', mockInstitutions); + fixture.detectChanges(); + + component.removeAffiliation(MOCK_INSTITUTION); + + expect(component.removed.emit).toHaveBeenCalledWith(MOCK_INSTITUTION); + }); }); diff --git a/src/app/features/project/settings/components/settings-project-form-card/settings-project-form-card.component.spec.ts b/src/app/features/project/settings/components/settings-project-form-card/settings-project-form-card.component.spec.ts index 710657fe2..a3dfe2712 100644 --- a/src/app/features/project/settings/components/settings-project-form-card/settings-project-form-card.component.spec.ts +++ b/src/app/features/project/settings/components/settings-project-form-card/settings-project-form-card.component.spec.ts @@ -1,22 +1,125 @@ +import { MockComponent, MockDirective } from 'ng-mocks'; + +import { Textarea } from 'primeng/textarea'; + import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { TextInputComponent } from '@osf/shared/components'; +import { ProjectFormControls } from '@osf/shared/enums'; +import { MOCK_NODE_DETAILS } from '@osf/shared/mocks'; + +import { NodeDetailsModel } from '../../models'; + import { SettingsProjectFormCardComponent } from './settings-project-form-card.component'; +import { OSFTestingModule } from '@testing/osf.testing.module'; + describe('SettingsProjectFormCardComponent', () => { let component: SettingsProjectFormCardComponent; let fixture: ComponentFixture; + const mockNodeDetails = MOCK_NODE_DETAILS; + beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [SettingsProjectFormCardComponent], + imports: [ + SettingsProjectFormCardComponent, + OSFTestingModule, + MockComponent(TextInputComponent), + MockDirective(Textarea), + ], }).compileComponents(); fixture = TestBed.createComponent(SettingsProjectFormCardComponent); component = fixture.componentInstance; - fixture.detectChanges(); }); it('should create', () => { + fixture.componentRef.setInput('projectDetails', mockNodeDetails); + fixture.detectChanges(); expect(component).toBeTruthy(); }); + + it('should initialize form with project details', () => { + fixture.componentRef.setInput('projectDetails', mockNodeDetails); + fixture.detectChanges(); + + expect(component.projectForm.get(ProjectFormControls.Title)?.value).toBe(mockNodeDetails.title); + expect(component.projectForm.get(ProjectFormControls.Description)?.value).toBe(mockNodeDetails.description); + }); + + it('should emit submitForm when form is valid and submitted', () => { + jest.spyOn(component.submitForm, 'emit'); + fixture.componentRef.setInput('projectDetails', mockNodeDetails); + fixture.detectChanges(); + + component.projectForm.patchValue({ + [ProjectFormControls.Title]: 'Updated Title', + [ProjectFormControls.Description]: 'Updated Description', + }); + + component.submit(); + + expect(component.submitForm.emit).toHaveBeenCalledWith({ + title: 'Updated Title', + description: 'Updated Description', + }); + }); + + it('should not emit submitForm when form is invalid', () => { + jest.spyOn(component.submitForm, 'emit'); + fixture.componentRef.setInput('projectDetails', mockNodeDetails); + fixture.detectChanges(); + + component.projectForm.patchValue({ + [ProjectFormControls.Title]: '', + [ProjectFormControls.Description]: 'Updated Description', + }); + + component.submit(); + + expect(component.submitForm.emit).not.toHaveBeenCalled(); + }); + + it('should emit deleteProject when delete button is clicked', () => { + jest.spyOn(component.deleteProject, 'emit'); + fixture.componentRef.setInput('projectDetails', mockNodeDetails); + fixture.detectChanges(); + + component.deleteProject.emit(); + + expect(component.deleteProject.emit).toHaveBeenCalled(); + }); + + it('should reset form to original values', () => { + fixture.componentRef.setInput('projectDetails', mockNodeDetails); + fixture.detectChanges(); + + component.projectForm.patchValue({ + [ProjectFormControls.Title]: 'Changed Title', + [ProjectFormControls.Description]: 'Changed Description', + }); + + component.resetForm(); + + expect(component.projectForm.get(ProjectFormControls.Title)?.value).toBe(mockNodeDetails.title); + expect(component.projectForm.get(ProjectFormControls.Description)?.value).toBe(mockNodeDetails.description); + }); + + it('should update form when projectDetails input changes', () => { + fixture.componentRef.setInput('projectDetails', mockNodeDetails); + fixture.detectChanges(); + + const newDetails: NodeDetailsModel = { + ...mockNodeDetails, + title: 'New Title', + description: 'New Description', + }; + + fixture.componentRef.setInput('projectDetails', newDetails); + fixture.detectChanges(); + + expect(component.projectForm.get(ProjectFormControls.Title)?.value).toBe('New Title'); + expect(component.projectForm.get(ProjectFormControls.Description)?.value).toBe('New Description'); + }); }); diff --git a/src/app/features/project/settings/components/settings-storage-location-card/settings-storage-location-card.component.spec.ts b/src/app/features/project/settings/components/settings-storage-location-card/settings-storage-location-card.component.spec.ts index d48079cdc..e28b6b845 100644 --- a/src/app/features/project/settings/components/settings-storage-location-card/settings-storage-location-card.component.spec.ts +++ b/src/app/features/project/settings/components/settings-storage-location-card/settings-storage-location-card.component.spec.ts @@ -2,21 +2,37 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { SettingsStorageLocationCardComponent } from './settings-storage-location-card.component'; +import { OSFTestingModule } from '@testing/osf.testing.module'; + describe('SettingsStorageLocationCardComponent', () => { let component: SettingsStorageLocationCardComponent; let fixture: ComponentFixture; + const mockLocation = 'Location Test'; + const mockLocationText = 'Location Text Test'; + beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [SettingsStorageLocationCardComponent], + imports: [SettingsStorageLocationCardComponent, OSFTestingModule], }).compileComponents(); fixture = TestBed.createComponent(SettingsStorageLocationCardComponent); component = fixture.componentInstance; - fixture.detectChanges(); }); it('should create', () => { + fixture.componentRef.setInput('location', mockLocation); + fixture.componentRef.setInput('locationText', mockLocationText); + fixture.detectChanges(); expect(component).toBeTruthy(); }); + + it('should initialize with location and locationText inputs', () => { + fixture.componentRef.setInput('location', mockLocation); + fixture.componentRef.setInput('locationText', mockLocationText); + fixture.detectChanges(); + + expect(component.location()).toBe(mockLocation); + expect(component.locationText()).toBe(mockLocationText); + }); }); diff --git a/src/app/features/project/settings/components/settings-view-only-links-card/settings-view-only-links-card.component.spec.ts b/src/app/features/project/settings/components/settings-view-only-links-card/settings-view-only-links-card.component.spec.ts index 362aed8e5..1645220af 100644 --- a/src/app/features/project/settings/components/settings-view-only-links-card/settings-view-only-links-card.component.spec.ts +++ b/src/app/features/project/settings/components/settings-view-only-links-card/settings-view-only-links-card.component.spec.ts @@ -1,22 +1,98 @@ +import { MockComponent } from 'ng-mocks'; + import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ViewOnlyTableComponent } from '@osf/shared/components'; +import { MOCK_PAGINATED_VIEW_ONLY_LINKS, MOCK_VIEW_ONLY_LINK } from '@osf/shared/mocks'; +import { PaginatedViewOnlyLinksModel } from '@osf/shared/models'; + import { SettingsViewOnlyLinksCardComponent } from './settings-view-only-links-card.component'; +import { OSFTestingModule } from '@testing/osf.testing.module'; + describe('SettingsViewOnlyLinksCardComponent', () => { let component: SettingsViewOnlyLinksCardComponent; let fixture: ComponentFixture; + const mockViewOnlyLink = MOCK_VIEW_ONLY_LINK; + const mockTableData = MOCK_PAGINATED_VIEW_ONLY_LINKS; + beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [SettingsViewOnlyLinksCardComponent], + imports: [SettingsViewOnlyLinksCardComponent, OSFTestingModule, MockComponent(ViewOnlyTableComponent)], }).compileComponents(); fixture = TestBed.createComponent(SettingsViewOnlyLinksCardComponent); component = fixture.componentInstance; - fixture.detectChanges(); }); it('should create', () => { + fixture.componentRef.setInput('tableData', mockTableData); + fixture.detectChanges(); expect(component).toBeTruthy(); }); + + it('should initialize with tableData input', () => { + fixture.componentRef.setInput('tableData', mockTableData); + fixture.detectChanges(); + + expect(component.tableData()).toEqual(mockTableData); + }); + + it('should handle isLoading input when set to true', () => { + fixture.componentRef.setInput('tableData', mockTableData); + fixture.componentRef.setInput('isLoading', true); + fixture.detectChanges(); + + expect(component.isLoading()).toBe(true); + }); + + it('should emit deleteTableItem when deleteLink event is triggered', () => { + jest.spyOn(component.deleteTableItem, 'emit'); + fixture.componentRef.setInput('tableData', mockTableData); + fixture.detectChanges(); + + component.deleteTableItem.emit(mockViewOnlyLink); + + expect(component.deleteTableItem.emit).toHaveBeenCalledWith(mockViewOnlyLink); + }); + + it('should handle empty table data', () => { + const emptyTableData: PaginatedViewOnlyLinksModel = { + items: [], + total: 0, + perPage: 10, + next: null, + prev: null, + }; + + fixture.componentRef.setInput('tableData', emptyTableData); + fixture.detectChanges(); + + expect(component.tableData()).toEqual(emptyTableData); + }); + + it('should handle multiple view only links', () => { + const multipleLinksData: PaginatedViewOnlyLinksModel = { + items: [ + mockViewOnlyLink, + { + ...mockViewOnlyLink, + id: 'test-link-2', + name: 'Second View Only Link', + key: 'def456', + }, + ], + total: 2, + perPage: 10, + next: null, + prev: null, + }; + + fixture.componentRef.setInput('tableData', multipleLinksData); + fixture.detectChanges(); + + expect(component.tableData().items.length).toBe(2); + expect(component.tableData().total).toBe(2); + }); }); diff --git a/src/app/features/project/settings/components/settings-wiki-card/settings-wiki-card.component.spec.ts b/src/app/features/project/settings/components/settings-wiki-card/settings-wiki-card.component.spec.ts index f71d62166..97bb732b3 100644 --- a/src/app/features/project/settings/components/settings-wiki-card/settings-wiki-card.component.spec.ts +++ b/src/app/features/project/settings/components/settings-wiki-card/settings-wiki-card.component.spec.ts @@ -1,22 +1,124 @@ +import { MockComponent } from 'ng-mocks'; + import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ProjectDetailSettingAccordionComponent } from '../project-detail-setting-accordion/project-detail-setting-accordion.component'; + import { SettingsWikiCardComponent } from './settings-wiki-card.component'; +import { OSFTestingModule } from '@testing/osf.testing.module'; + describe('SettingsWikiCardComponent', () => { let component: SettingsWikiCardComponent; let fixture: ComponentFixture; + const mockTitle = 'Wiki Settings'; + const mockWikiEnabled = true; + const mockAnyoneCanEditWiki = false; + const mockIsPublic = true; + beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [SettingsWikiCardComponent], + imports: [SettingsWikiCardComponent, OSFTestingModule, MockComponent(ProjectDetailSettingAccordionComponent)], }).compileComponents(); fixture = TestBed.createComponent(SettingsWikiCardComponent); component = fixture.componentInstance; - fixture.detectChanges(); }); it('should create', () => { + fixture.componentRef.setInput('wikiEnabled', mockWikiEnabled); + fixture.componentRef.setInput('anyoneCanEditWiki', mockAnyoneCanEditWiki); + fixture.componentRef.setInput('title', mockTitle); + fixture.componentRef.setInput('isPublic', mockIsPublic); + fixture.detectChanges(); expect(component).toBeTruthy(); }); + + it('should initialize with required inputs', () => { + fixture.componentRef.setInput('wikiEnabled', mockWikiEnabled); + fixture.componentRef.setInput('anyoneCanEditWiki', mockAnyoneCanEditWiki); + fixture.componentRef.setInput('title', mockTitle); + fixture.componentRef.setInput('isPublic', mockIsPublic); + fixture.detectChanges(); + + expect(component.wikiEnabled()).toBe(mockWikiEnabled); + expect(component.anyoneCanEditWiki()).toBe(mockAnyoneCanEditWiki); + expect(component.title()).toBe(mockTitle); + expect(component.isPublic()).toBe(mockIsPublic); + }); + + it('should initialize with isPublic defaulting to false', () => { + fixture.componentRef.setInput('wikiEnabled', mockWikiEnabled); + fixture.componentRef.setInput('anyoneCanEditWiki', mockAnyoneCanEditWiki); + fixture.componentRef.setInput('title', mockTitle); + fixture.detectChanges(); + + expect(component.isPublic()).toBe(false); + }); + + it('should emit wikiChangeEmit when wiki checkbox changes', () => { + jest.spyOn(component.wikiChangeEmit, 'emit'); + fixture.componentRef.setInput('wikiEnabled', mockWikiEnabled); + fixture.componentRef.setInput('anyoneCanEditWiki', mockAnyoneCanEditWiki); + fixture.componentRef.setInput('title', mockTitle); + fixture.detectChanges(); + + component.wikiChangeEmit.emit(mockWikiEnabled); + + expect(component.wikiChangeEmit.emit).toHaveBeenCalledWith(mockWikiEnabled); + }); + + it('should emit anyoneCanEditWikiEmitValue when changeEmittedValue is called with boolean', () => { + jest.spyOn(component.anyoneCanEditWikiEmitValue, 'emit'); + fixture.componentRef.setInput('wikiEnabled', mockWikiEnabled); + fixture.componentRef.setInput('anyoneCanEditWiki', mockAnyoneCanEditWiki); + fixture.componentRef.setInput('title', mockTitle); + fixture.detectChanges(); + + const emittedValue = { index: 0, value: true }; + component.changeEmittedValue(emittedValue); + + expect(component.anyoneCanEditWikiEmitValue.emit).toHaveBeenCalledWith(true); + }); + + it('should not emit anyoneCanEditWikiEmitValue when changeEmittedValue is called with string', () => { + jest.spyOn(component.anyoneCanEditWikiEmitValue, 'emit'); + fixture.componentRef.setInput('wikiEnabled', mockWikiEnabled); + fixture.componentRef.setInput('anyoneCanEditWiki', mockAnyoneCanEditWiki); + fixture.componentRef.setInput('title', mockTitle); + fixture.detectChanges(); + + const emittedValue = { index: 0, value: 'string value' }; + component.changeEmittedValue(emittedValue); + + expect(component.anyoneCanEditWikiEmitValue.emit).not.toHaveBeenCalled(); + }); + + it('should initialize allAccordionData with correct structure', () => { + fixture.componentRef.setInput('wikiEnabled', mockWikiEnabled); + fixture.componentRef.setInput('anyoneCanEditWiki', mockAnyoneCanEditWiki); + fixture.componentRef.setInput('title', mockTitle); + fixture.detectChanges(); + + expect(component.allAccordionData).toHaveLength(1); + expect(component.allAccordionData[0].label).toBe('myProjects.settings.whoCanEdit'); + expect(component.allAccordionData[0].value).toBe(mockAnyoneCanEditWiki); + expect(component.allAccordionData[0].type).toBe('dropdown'); + expect(component.allAccordionData[0].options).toHaveLength(2); + }); + + it('should update allAccordionData when anyoneCanEditWiki input changes', () => { + fixture.componentRef.setInput('wikiEnabled', mockWikiEnabled); + fixture.componentRef.setInput('anyoneCanEditWiki', false); + fixture.componentRef.setInput('title', mockTitle); + fixture.detectChanges(); + + expect(component.allAccordionData[0].value).toBe(false); + + fixture.componentRef.setInput('anyoneCanEditWiki', true); + fixture.detectChanges(); + + expect(component.allAccordionData[0].value).toBe(true); + }); }); diff --git a/src/app/features/project/settings/settings.component.spec.ts b/src/app/features/project/settings/settings.component.spec.ts index 83f8f7e90..f0053c445 100644 --- a/src/app/features/project/settings/settings.component.spec.ts +++ b/src/app/features/project/settings/settings.component.spec.ts @@ -1,22 +1,178 @@ +import { MockComponents, MockProvider } from 'ng-mocks'; + import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ActivatedRoute, Router } from '@angular/router'; + +import { + ProjectSettingNotificationsComponent, + SettingsAccessRequestsCardComponent, + SettingsProjectAffiliationComponent, + SettingsProjectFormCardComponent, + SettingsStorageLocationCardComponent, + SettingsViewOnlyLinksCardComponent, + SettingsWikiCardComponent, +} from '@osf/features/project/settings/components'; +import { LoadingSpinnerComponent, SubHeaderComponent } from '@osf/shared/components'; +import { CustomConfirmationService, LoaderService, ToastService } from '@osf/shared/services'; +import { ViewOnlyLinkSelectors } from '@osf/shared/stores'; +import { MOCK_VIEW_ONLY_LINK } from '@shared/mocks'; import { SettingsComponent } from './settings.component'; +import { SettingsSelectors } from './store'; + +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'; +import { ToastServiceMockBuilder } from '@testing/providers/toast-provider.mock'; describe('SettingsComponent', () => { let component: SettingsComponent; let fixture: ComponentFixture; + let routerMock: ReturnType; + let activatedRouteMock: ReturnType; + let toastServiceMock: ReturnType; + let customConfirmationServiceMock: ReturnType; + let mockLoaderService: LoaderService; + + const mockProjectId = 'test-project-123'; + const mockSettings = { + attributes: { + accessRequestsEnabled: true, + wikiEnabled: false, + anyoneCanEditWiki: false, + anyoneCanComment: true, + }, + }; + const mockProjectDetails = { + id: mockProjectId, + title: 'Test Project', + description: 'Test Description', + isPublic: true, + region: { name: 'US East' }, + affiliatedInstitutions: [], + }; beforeEach(async () => { + activatedRouteMock = ActivatedRouteMockBuilder.create().withParams({ id: mockProjectId }).build(); + + routerMock = RouterMockBuilder.create().build(); + + toastServiceMock = ToastServiceMockBuilder.create().build(); + + customConfirmationServiceMock = CustomConfirmationServiceMockBuilder.create().build(); + await TestBed.configureTestingModule({ - imports: [SettingsComponent], + imports: [ + SettingsComponent, + OSFTestingModule, + ...MockComponents( + SubHeaderComponent, + SettingsProjectFormCardComponent, + SettingsStorageLocationCardComponent, + SettingsViewOnlyLinksCardComponent, + SettingsAccessRequestsCardComponent, + SettingsWikiCardComponent, + SettingsProjectAffiliationComponent, + ProjectSettingNotificationsComponent, + LoadingSpinnerComponent + ), + ], + providers: [ + MockProvider(ActivatedRoute, activatedRouteMock), + MockProvider(Router, routerMock), + MockProvider(CustomConfirmationService, customConfirmationServiceMock), + MockProvider(LoaderService, mockLoaderService), + MockProvider(ToastService, toastServiceMock), + provideMockStore({ + signals: [ + { selector: SettingsSelectors.getSettings, value: mockSettings }, + { selector: SettingsSelectors.getNotificationSubscriptions, value: [] }, + { selector: SettingsSelectors.areNotificationsLoading, value: false }, + { selector: SettingsSelectors.getProjectDetails, value: mockProjectDetails }, + { selector: SettingsSelectors.areProjectDetailsLoading, value: false }, + { selector: ViewOnlyLinkSelectors.getViewOnlyLinks, value: [] }, + { selector: ViewOnlyLinkSelectors.isViewOnlyLinksLoading, value: false }, + ], + }), + ], }).compileComponents(); fixture = TestBed.createComponent(SettingsComponent); component = fixture.componentInstance; - fixture.detectChanges(); + + mockLoaderService = TestBed.inject(LoaderService); }); it('should create', () => { + fixture.detectChanges(); expect(component).toBeTruthy(); }); + + it('should initialize with project ID from route', () => { + fixture.detectChanges(); + expect(component.projectId()).toBe(mockProjectId); + }); + + it('should not call actions on ngOnInit when project ID is undefined', () => { + expect(typeof component.ngOnInit).toBe('function'); + }); + + it('should handle access request change', () => { + component.onAccessRequestChange(true); + expect(component.accessRequest()).toBe(true); + }); + + it('should handle wiki request change', () => { + component.onWikiRequestChange(true); + expect(component.wikiEnabled()).toBe(true); + }); + + it('should handle anyone can edit wiki request change', () => { + component.onAnyoneCanEditWikiRequestChange(true); + expect(component.anyoneCanEditWiki()).toBe(true); + }); + + it('should handle delete link item', () => { + const mockLink = MOCK_VIEW_ONLY_LINK; + + component.deleteLinkItem(mockLink); + + expect(customConfirmationServiceMock.confirmDelete).toHaveBeenCalledWith({ + headerKey: 'myProjects.settings.delete.title', + headerParams: { name: mockLink.name }, + messageKey: 'myProjects.settings.delete.message', + onConfirm: expect.any(Function), + }); + }); + + it('should handle delete project', () => { + expect(() => component.deleteProject()).not.toThrow(); + expect(customConfirmationServiceMock.confirmDelete).toHaveBeenCalled(); + }); + + it('should submit form when project details change', () => { + const mockFormData = { title: 'New Title', description: 'New Description' }; + expect(() => component.submitForm(mockFormData)).not.toThrow(); + }); + + it('should not submit form when project details are unchanged', () => { + const mockFormData = { title: 'Same Title', description: 'Same Description' }; + + Object.defineProperty(component, 'projectDetails', { + value: () => ({ title: 'Same Title', description: 'Same Description' }), + writable: true, + }); + + expect(() => component.submitForm(mockFormData)).not.toThrow(); + }); + + it('should initialize signals with default values', () => { + expect(component.accessRequest()).toBe(false); + expect(component.wikiEnabled()).toBe(false); + expect(component.anyoneCanEditWiki()).toBe(false); + expect(component.anyoneCanComment()).toBe(false); + expect(component.title()).toBe(''); + }); }); diff --git a/src/app/shared/components/view-only-table/view-only-table.component.spec.ts b/src/app/shared/components/view-only-table/view-only-table.component.spec.ts index a12323f31..dcdaa55d8 100644 --- a/src/app/shared/components/view-only-table/view-only-table.component.spec.ts +++ b/src/app/shared/components/view-only-table/view-only-table.component.spec.ts @@ -3,8 +3,8 @@ import { MockComponent } from 'ng-mocks'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { CopyButtonComponent } from '@shared/components'; -import { MOCK_USER, TranslateServiceMock } from '@shared/mocks'; -import { PaginatedViewOnlyLinksModel, ViewOnlyLinkModel } from '@shared/models'; +import { MOCK_PAGINATED_VIEW_ONLY_LINKS, MOCK_VIEW_ONLY_LINK, TranslateServiceMock } from '@shared/mocks'; +import { PaginatedViewOnlyLinksModel } from '@shared/models'; import { ViewOnlyTableComponent } from './view-only-table.component'; @@ -12,34 +12,8 @@ describe('ViewOnlyTableComponent', () => { let component: ViewOnlyTableComponent; let fixture: ComponentFixture; - const mockViewOnlyLink: ViewOnlyLinkModel = { - id: 'link-1', - dateCreated: '2023-01-01T10:00:00Z', - key: 'key-1', - name: 'Test Link', - link: 'https://test.com/view-only-link', - creator: { - id: MOCK_USER.id, - fullName: MOCK_USER.fullName, - }, - nodes: [ - { - title: 'Test Node', - url: 'https://test.com/node', - scale: '1.0', - category: 'test', - }, - ], - anonymous: false, - }; - - const mockPaginatedData: PaginatedViewOnlyLinksModel = { - items: [mockViewOnlyLink], - total: 1, - perPage: 10, - next: null, - prev: null, - }; + const mockViewOnlyLink = MOCK_VIEW_ONLY_LINK; + const mockPaginatedData = MOCK_PAGINATED_VIEW_ONLY_LINKS; beforeEach(async () => { await TestBed.configureTestingModule({ diff --git a/src/app/shared/mocks/index.ts b/src/app/shared/mocks/index.ts index 1b3c1188e..9468edc46 100644 --- a/src/app/shared/mocks/index.ts +++ b/src/app/shared/mocks/index.ts @@ -16,6 +16,8 @@ export { LoaderServiceMock } from './loader-service.mock'; export * from './meeting.mock'; export { MOCK_MEETING } from './meeting.mock'; export { MOCK_STORE } from './mock-store.mock'; +export { MOCK_NODE_DETAILS } from './node-details.mock'; +export { MOCK_NOTIFICATION_SUBSCRIPTIONS } from './notification-subscription.mock'; export * from './project-overview.mock'; export { MOCK_PROVIDER } from './provider.mock'; export { MOCK_REGISTRATION } from './registration.mock'; @@ -25,3 +27,4 @@ export { MOCK_SCOPES } from './scope.mock'; export { MOCK_TOKEN } from './token.mock'; export { TranslateServiceMock } from './translate.service.mock'; export * from './view-only-link.mock'; +export * from './view-only-link.mock'; diff --git a/src/app/shared/mocks/node-details.mock.ts b/src/app/shared/mocks/node-details.mock.ts new file mode 100644 index 000000000..123fcfd91 --- /dev/null +++ b/src/app/shared/mocks/node-details.mock.ts @@ -0,0 +1,11 @@ +import { NodeDetailsModel } from '@osf/features/project/settings/models'; + +export const MOCK_NODE_DETAILS: NodeDetailsModel = { + id: 'test-project-1', + title: 'Test Project', + description: 'A test project description', + isPublic: true, + region: { id: 'us-east-1', name: 'US East' }, + affiliatedInstitutions: [], + lastFetched: Date.now(), +}; diff --git a/src/app/shared/mocks/notification-subscription.mock.ts b/src/app/shared/mocks/notification-subscription.mock.ts new file mode 100644 index 000000000..ebbcedfba --- /dev/null +++ b/src/app/shared/mocks/notification-subscription.mock.ts @@ -0,0 +1,25 @@ +import { SubscriptionEvent, SubscriptionFrequency } from '@osf/shared/enums'; +import { NotificationSubscription } from '@osf/shared/models'; + +export const MOCK_NOTIFICATION_SUBSCRIPTIONS: NotificationSubscription[] = [ + { + id: 'mock-notification-1', + event: SubscriptionEvent.FileUpdated, + frequency: SubscriptionFrequency.Instant, + }, + { + id: 'mock-notification-2', + event: SubscriptionEvent.GlobalFileUpdated, + frequency: SubscriptionFrequency.Daily, + }, + { + id: 'mock-notification-3', + event: SubscriptionEvent.GlobalMentions, + frequency: SubscriptionFrequency.Never, + }, + { + id: 'mock-notification-4', + event: SubscriptionEvent.GlobalReviews, + frequency: SubscriptionFrequency.Instant, + }, +]; diff --git a/src/app/shared/mocks/view-only-link.mock.ts b/src/app/shared/mocks/view-only-link.mock.ts index a51b61ab9..40a8548b3 100644 --- a/src/app/shared/mocks/view-only-link.mock.ts +++ b/src/app/shared/mocks/view-only-link.mock.ts @@ -1,31 +1,28 @@ -import { ViewOnlyLinkComponentItem } from '@osf/features/project/contributors/models'; -import { PaginatedViewOnlyLinksModel } from '@shared/models'; +import { PaginatedViewOnlyLinksModel, ViewOnlyLinkModel } from '@osf/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_VIEW_ONLY_LINK: ViewOnlyLinkModel = { + id: 'test-link-1', + dateCreated: '2024-01-15T10:30:00Z', + key: 'abc123', + name: 'Test View Only Link', + link: 'https://osf.io/test-link/', + creator: { + id: 'user-1', + fullName: 'Test User', }, -]; + nodes: [ + { + id: 'node-1', + title: 'Test Project', + category: 'project', + }, + ], + anonymous: false, +}; export const MOCK_PAGINATED_VIEW_ONLY_LINKS: PaginatedViewOnlyLinksModel = { - items: MOCK_VIEW_ONLY_LINKS, - total: MOCK_VIEW_ONLY_LINKS.length, + items: [MOCK_VIEW_ONLY_LINK], + total: 1, perPage: 10, next: null, prev: null, From 3651c869e9a943120386f19f8d18066fd9db9667 Mon Sep 17 00:00:00 2001 From: Diana Date: Sun, 14 Sep 2025 14:36:32 +0300 Subject: [PATCH 2/2] fix(tests): fixed some tests --- src/app/core/components/root/root.component.spec.ts | 3 +++ .../project-setting-notifications.component.spec.ts | 2 +- src/app/shared/mocks/view-only-link.mock.ts | 10 ++++++++++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/app/core/components/root/root.component.spec.ts b/src/app/core/components/root/root.component.spec.ts index 4a4ddf252..c086632a0 100644 --- a/src/app/core/components/root/root.component.spec.ts +++ b/src/app/core/components/root/root.component.spec.ts @@ -17,6 +17,8 @@ import { SidenavComponent } from '../sidenav/sidenav.component'; import { RootComponent } from './root.component'; +import { OSFTestingModule } from '@testing/osf.testing.module'; + describe('RootComponent', () => { let component: RootComponent; let fixture: ComponentFixture; @@ -30,6 +32,7 @@ describe('RootComponent', () => { await TestBed.configureTestingModule({ imports: [ RootComponent, + OSFTestingModule, ...MockComponents( HeaderComponent, FooterComponent, diff --git a/src/app/features/project/settings/components/project-setting-notifications/project-setting-notifications.component.spec.ts b/src/app/features/project/settings/components/project-setting-notifications/project-setting-notifications.component.spec.ts index 84a4ca9be..afc55272a 100644 --- a/src/app/features/project/settings/components/project-setting-notifications/project-setting-notifications.component.spec.ts +++ b/src/app/features/project/settings/components/project-setting-notifications/project-setting-notifications.component.spec.ts @@ -54,7 +54,7 @@ describe('ProjectSettingNotificationsComponent', () => { fixture.detectChanges(); expect(component.allAccordionData).toBeDefined(); - expect(component.allAccordionData?.length).toBe(2); + expect(component.allAccordionData?.length).toBe(4); if (component.allAccordionData) { expect(component.allAccordionData[0]).toEqual({ diff --git a/src/app/shared/mocks/view-only-link.mock.ts b/src/app/shared/mocks/view-only-link.mock.ts index 40a8548b3..1730d589a 100644 --- a/src/app/shared/mocks/view-only-link.mock.ts +++ b/src/app/shared/mocks/view-only-link.mock.ts @@ -1,3 +1,4 @@ +import { ViewOnlyLinkComponentItem } from '@osf/features/project/contributors/models'; import { PaginatedViewOnlyLinksModel, ViewOnlyLinkModel } from '@osf/shared/models'; export const MOCK_VIEW_ONLY_LINK: ViewOnlyLinkModel = { @@ -20,6 +21,15 @@ export const MOCK_VIEW_ONLY_LINK: ViewOnlyLinkModel = { anonymous: false, }; +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_PAGINATED_VIEW_ONLY_LINKS: PaginatedViewOnlyLinksModel = { items: [MOCK_VIEW_ONLY_LINK], total: 1,