From 8fd2712820e709401cd63f0c4e3362d03039e888 Mon Sep 17 00:00:00 2001 From: Diana Date: Tue, 29 Jul 2025 15:59:23 +0300 Subject: [PATCH 1/6] test(testing): created tests for components: meetings, meetings-feature-card, meetings-landing --- .../meetings-feature-card.component.spec.ts | 33 ++- .../meetings/meetings.component.spec.ts | 6 + .../meetings-landing.component.spec.ts | 216 ++++++++++++++++-- src/app/shared/mocks/data.mock.ts | 10 + src/app/shared/mocks/index.ts | 1 + 5 files changed, 251 insertions(+), 15 deletions(-) diff --git a/src/app/features/meetings/components/meetings-feature-card/meetings-feature-card.component.spec.ts b/src/app/features/meetings/components/meetings-feature-card/meetings-feature-card.component.spec.ts index 479bb16c6..ff4c63ad4 100644 --- a/src/app/features/meetings/components/meetings-feature-card/meetings-feature-card.component.spec.ts +++ b/src/app/features/meetings/components/meetings-feature-card/meetings-feature-card.component.spec.ts @@ -1,22 +1,53 @@ +import { TranslatePipe } from '@ngx-translate/core'; +import { MockComponent, MockPipe } from 'ng-mocks'; + +import { Card } from 'primeng/card'; + +import { ComponentRef } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MeetingsFeatureCardComponent } from './meetings-feature-card.component'; describe('MeetingsFeatureCardComponent', () => { let component: MeetingsFeatureCardComponent; + let componentRef: ComponentRef; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [MeetingsFeatureCardComponent], + imports: [MeetingsFeatureCardComponent, MockComponent(Card), MockPipe(TranslatePipe, (value) => value)], }).compileComponents(); fixture = TestBed.createComponent(MeetingsFeatureCardComponent); component = fixture.componentInstance; + componentRef = fixture.componentRef; + + componentRef.setInput('iconSrc', 'meeting-icon.svg'); + componentRef.setInput('iconAlt', 'Meeting icon'); + componentRef.setInput('titleKey', 'meetings.title'); + componentRef.setInput('descriptionKey', 'meetings.description'); + fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); + + it('should render icon with correct src and alt attributes', () => { + const img: HTMLImageElement = fixture.nativeElement.querySelector('img'); + expect(img).toBeTruthy(); + expect(img.src).toContain('meeting-icon.svg'); + expect(img.alt).toBe('Meeting icon'); + }); + + it('should update icon inputs when setInput is called again', () => { + componentRef.setInput('iconAlt', 'New meeting icon'); + componentRef.setInput('iconSrc', 'new-meeting-icon.svg'); + fixture.detectChanges(); + + const img: HTMLImageElement = fixture.nativeElement.querySelector('img'); + expect(img.src).toContain('new-meeting-icon.svg'); + expect(img.alt).toBe('New meeting icon'); + }); }); diff --git a/src/app/features/meetings/meetings.component.spec.ts b/src/app/features/meetings/meetings.component.spec.ts index b51d5ddfb..083b7b0ad 100644 --- a/src/app/features/meetings/meetings.component.spec.ts +++ b/src/app/features/meetings/meetings.component.spec.ts @@ -1,4 +1,5 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; import { MeetingsComponent } from './meetings.component'; @@ -19,4 +20,9 @@ describe('MeetingsComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + it('should render router outlet', () => { + const routerOutlet = fixture.debugElement.query(By.css('router-outlet')); + expect(routerOutlet).toBeTruthy(); + }); }); diff --git a/src/app/features/meetings/pages/meetings-landing/meetings-landing.component.spec.ts b/src/app/features/meetings/pages/meetings-landing/meetings-landing.component.spec.ts index 52eeb2c6e..cdee596e1 100644 --- a/src/app/features/meetings/pages/meetings-landing/meetings-landing.component.spec.ts +++ b/src/app/features/meetings/pages/meetings-landing/meetings-landing.component.spec.ts @@ -1,40 +1,228 @@ -import { provideStates } from '@ngxs/store'; +import { provideStore } from '@ngxs/store'; -import { TranslateModule } from '@ngx-translate/core'; -import { MockComponents, MockModule } from 'ng-mocks'; +import { TranslateModule, TranslatePipe } from '@ngx-translate/core'; +import { MockComponents, MockPipe, MockProvider } from 'ng-mocks'; -import { provideHttpClient } from '@angular/common/http'; -import { provideHttpClientTesting } from '@angular/common/http/testing'; +import { Skeleton } from 'primeng/skeleton'; +import { TableModule } from 'primeng/table'; + +import { of } from 'rxjs'; + +import { signal } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { RouterModule } from '@angular/router'; +import { FormControl, ReactiveFormsModule } from '@angular/forms'; +import { ActivatedRoute, Router } from '@angular/router'; -import { SearchInputComponent, SubHeaderComponent } from '@osf/shared/components'; +import { MOCK_MEETING } from '@osf/shared/mocks'; +import { SearchInputComponent, SubHeaderComponent } from '@shared/components'; +import { TABLE_PARAMS } from '@shared/constants'; +import { SortOrder } from '@shared/enums'; -import { MeetingsState } from '../../store'; +import { MeetingsFeatureCardComponent } from '../../components'; +import { MEETINGS_FEATURE_CARDS, PARTNER_ORGANIZATIONS } from '../../constants'; import { MeetingsLandingComponent } from './meetings-landing.component'; +const mockQueryParams = { + page: 1, + size: 10, + search: '', + sortColumn: 'name', + sortOrder: SortOrder.Asc, +}; + +const mockActivatedRoute = { + queryParams: of(mockQueryParams), +}; + +const mockRouter = { + navigate: jest.fn(), +}; + describe('MeetingsLandingComponent', () => { let component: MeetingsLandingComponent; let fixture: ComponentFixture; + let router: Router; + const mockMeeting = MOCK_MEETING; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [ MeetingsLandingComponent, - MockModule(TranslateModule), - MockModule(RouterModule), - ...MockComponents(SubHeaderComponent, SearchInputComponent), + ...MockComponents(SubHeaderComponent, SearchInputComponent, MeetingsFeatureCardComponent), + ReactiveFormsModule, + TranslateModule.forRoot(), + TableModule, + Skeleton, + MockPipe(TranslatePipe), ], - providers: [provideStates([MeetingsState]), provideHttpClient(), provideHttpClientTesting()], + providers: [MockProvider(ActivatedRoute, mockActivatedRoute), MockProvider(Router, mockRouter), provideStore([])], }).compileComponents(); fixture = TestBed.createComponent(MeetingsLandingComponent); component = fixture.componentInstance; - fixture.detectChanges(); + router = TestBed.inject(Router); }); - it('should create', () => { + it('should create and have correct initial signals', () => { expect(component).toBeTruthy(); + expect(component.searchControl).toBeInstanceOf(FormControl); + expect(component.partnerOrganizations).toEqual(PARTNER_ORGANIZATIONS); + expect(component.meetingsFeatureCards).toEqual(MEETINGS_FEATURE_CARDS); + expect(component.skeletonData).toHaveLength(10); + expect(component.tableParams().rows).toBe(TABLE_PARAMS.rows); + expect(component.tableParams().firstRowIndex).toBe(0); + expect(component.currentPage()).toBe(1); + expect(component.currentPageSize()).toBe(TABLE_PARAMS.rows); + expect(component.sortColumn()).toBe(''); + expect(component.sortOrder()).toBe(SortOrder.Asc); + }); + + it('should navigate to meeting when navigateToMeeting is called', () => { + component.navigateToMeeting(mockMeeting); + expect(router.navigate).toHaveBeenCalledWith(['/meetings', '1']); + }); + + describe('router.navigate scenarios', () => { + const cases = [ + { + name: 'onPageChange', + action: (c: MeetingsLandingComponent) => c.onPageChange({ first: 40, rows: 20 }), + expected: { page: '3', size: '20' }, + }, + { + name: 'onSort ascending', + action: (c: MeetingsLandingComponent) => c.onSort({ field: 'location', order: 1 }), + expected: { sortColumn: 'location', sortOrder: 'asc' }, + }, + { + name: 'onSort descending', + action: (c: MeetingsLandingComponent) => c.onSort({ field: 'location', order: -1 }), + expected: { sortColumn: 'location', sortOrder: 'desc' }, + }, + { + name: 'onSort with bad params (order=undefined)', + action: (c: MeetingsLandingComponent) => c.onSort({ field: 'location', order: undefined }), + expected: { sortColumn: 'location', sortOrder: 'asc' }, + }, + ]; + cases.forEach(({ name, action, expected }) => { + it(`should call router.navigate with correct params: ${name}`, () => { + jest.clearAllMocks(); + action(component); + if (expected) { + expect(router.navigate).toHaveBeenCalledWith( + [], + expect.objectContaining({ + queryParams: expect.objectContaining(expected), + queryParamsHandling: 'merge', + }) + ); + } else { + expect(router.navigate).not.toHaveBeenCalled(); + } + }); + }); + }); + + it('should not update query params when sort field is undefined', () => { + jest.clearAllMocks(); + component.onSort({ field: undefined, order: 1 }); + expect(router.navigate).not.toHaveBeenCalled(); + }); + + it('should call router.navigate with correct params on search', (done) => { + jest.clearAllMocks(); + setTimeout(() => { + component.searchControl.setValue('test search'); + setTimeout(() => { + expect(router.navigate).toHaveBeenCalledWith( + [], + expect.objectContaining({ + queryParams: expect.objectContaining({ search: 'test search', page: '1' }), + queryParamsHandling: 'merge', + }) + ); + done(); + }, 350); + }, 100); + }); + + it('should show skeletonData when isMeetingsLoading is true', () => { + component.isMeetingsLoading = signal(true); + expect(component.isMeetingsLoading()).toBe(true); + expect(component.skeletonData.length).toBe(10); + }); + + it('should show empty table when meetings is empty and not loading', () => { + component.meetings = signal([]); + component.isMeetingsLoading = signal(false); + expect(component.meetings().length).toBe(0); + expect(component.isMeetingsLoading()).toBe(false); + }); + + it('should use skeletonData when meetings is empty and isMeetingsLoading is true', () => { + component.meetings = signal([]); + component.isMeetingsLoading = signal(true); + const data = component.isMeetingsLoading() ? component.skeletonData : component.meetings(); + expect(data).toEqual(component.skeletonData); + expect(data.length).toBe(10); + }); + + it('should use meetings when meetings is not empty and isMeetingsLoading is false', () => { + component.meetings = signal([mockMeeting]); + component.isMeetingsLoading = signal(false); + const data = component.isMeetingsLoading() ? component.skeletonData : component.meetings(); + expect(data).toEqual([mockMeeting]); + }); + + it('should debounce search input and call router.navigate only once', (done) => { + jest.clearAllMocks(); + component.searchControl.setValue('first'); + setTimeout(() => { + component.searchControl.setValue('second'); + setTimeout(() => { + expect(router.navigate).toHaveBeenCalledTimes(1); + done(); + }, 350); + }, 100); + }); + + it('should not throw on onPageChange with rows=0', () => { + expect(() => component.onPageChange({ first: 0, rows: 0 })).not.toThrow(); + }); + + it('should not call router.navigate if onSort called with field undefined', () => { + jest.clearAllMocks(); + component.onSort({ field: undefined, order: 1 }); + expect(router.navigate).not.toHaveBeenCalled(); + }); + + it('should call router.navigate with only provided queryParams', () => { + jest.clearAllMocks(); + component.onPageChange({ first: 0, rows: 10 }); + expect(router.navigate).toHaveBeenCalledWith( + [], + expect.objectContaining({ + queryParams: expect.objectContaining({ page: '1', size: '10' }), + queryParamsHandling: 'merge', + }) + ); + jest.clearAllMocks(); + component.onSort({ field: 'name', order: 1 }); + expect(router.navigate).toHaveBeenCalledWith( + [], + expect.objectContaining({ + queryParams: expect.objectContaining({ sortColumn: 'name', sortOrder: 'asc' }), + queryParamsHandling: 'merge', + }) + ); + }); + + it('should use empty array when meetings is empty and isMeetingsLoading is false', () => { + component.meetings = signal([]); + component.isMeetingsLoading = signal(false); + const data = component.isMeetingsLoading() ? component.skeletonData : component.meetings(); + expect(data).toEqual([]); }); }); diff --git a/src/app/shared/mocks/data.mock.ts b/src/app/shared/mocks/data.mock.ts index dd7390f08..0990872da 100644 --- a/src/app/shared/mocks/data.mock.ts +++ b/src/app/shared/mocks/data.mock.ts @@ -1,4 +1,5 @@ import { User } from '@osf/core/models'; +import { Meeting } from '@osf/features/meetings/models/meetings.models'; export const MOCK_USER: User = { id: '1', @@ -52,3 +53,12 @@ export const MOCK_USER: User = { defaultRegionId: 'us', allowIndexing: true, }; + +export const MOCK_MEETING: Meeting = { + id: '1', + name: 'Test Meeting', + submissionsCount: 10, + location: 'New York', + startDate: new Date('2024-01-15'), + endDate: new Date('2024-01-16'), +}; diff --git a/src/app/shared/mocks/index.ts b/src/app/shared/mocks/index.ts index abc4199b6..c1aa5b6b4 100644 --- a/src/app/shared/mocks/index.ts +++ b/src/app/shared/mocks/index.ts @@ -1,3 +1,4 @@ export { MOCK_USER } from './data.mock'; +export { MOCK_MEETING } from './data.mock'; export { MOCK_STORE } from './mock-store.mock'; export { TranslateServiceMock } from './translate.service.mock'; From bbade81e4bf13501e357a986afc9ddc9dd0cb83d Mon Sep 17 00:00:00 2001 From: Diana Date: Tue, 29 Jul 2025 17:56:03 +0300 Subject: [PATCH 2/6] test(testing): created test for meeting-details --- .../meeting-details.component.spec.ts | 118 +++++++++++++++++- src/app/shared/mocks/data.mock.ts | 23 +++- src/app/shared/mocks/index.ts | 1 + src/app/shared/mocks/mock-store.mock.ts | 1 + 4 files changed, 138 insertions(+), 5 deletions(-) diff --git a/src/app/features/meetings/pages/meeting-details/meeting-details.component.spec.ts b/src/app/features/meetings/pages/meeting-details/meeting-details.component.spec.ts index d84d035a9..4cb227b19 100644 --- a/src/app/features/meetings/pages/meeting-details/meeting-details.component.spec.ts +++ b/src/app/features/meetings/pages/meeting-details/meeting-details.component.spec.ts @@ -1,18 +1,81 @@ -import { TranslateModule } from '@ngx-translate/core'; -import { MockModule } from 'ng-mocks'; +import { Store } from '@ngxs/store'; +import { TranslatePipe } from '@ngx-translate/core'; +import { MockComponents, MockPipe, MockProvider } from 'ng-mocks'; + +import { SortEvent } from 'primeng/api'; +import { Button } from 'primeng/button'; +import { Skeleton } from 'primeng/skeleton'; +import { TableModule, TablePageEvent } from 'primeng/table'; + +import { of } from 'rxjs'; + +import { DatePipe } from '@angular/common'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { RouterModule } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; + +import { MeetingsSelectors } from '@osf/features/meetings/store'; +import { SearchInputComponent, SubHeaderComponent } from '@shared/components'; +import { MOCK_MEETING, MOCK_MEETING_SUBMISSIONS, MOCK_STORE } from '@shared/mocks'; import { MeetingDetailsComponent } from './meeting-details.component'; +const mockActivatedRoute = { + params: of({ id: 'test-meeting-id' }), + queryParams: of({}), + snapshot: { + params: { id: 'test-meeting-id' }, + queryParams: {}, + }, +}; + +const mockRouter = { + navigate: jest.fn(), + url: '/', + createUrlTree: jest.fn(), + navigateByUrl: jest.fn(), + events: { + subscribe: jest.fn(), + }, +}; + describe('MeetingDetailsComponent', () => { let component: MeetingDetailsComponent; let fixture: ComponentFixture; beforeEach(async () => { + (MOCK_STORE.selectSignal as jest.Mock).mockImplementation((selector) => { + if (selector === MeetingsSelectors.getAllMeetingSubmissions) return () => MOCK_MEETING_SUBMISSIONS; + if (selector === MeetingsSelectors.getMeetingSubmissionsTotalCount) return () => MOCK_MEETING_SUBMISSIONS.length; + if (selector === MeetingsSelectors.isMeetingSubmissionsLoading) return () => false; + if (selector === MeetingsSelectors.getMeetingById) { + return () => (id: string) => (id === MOCK_MEETING.id ? MOCK_MEETING : null); + } + return () => null; + }); + + (MOCK_STORE.selectSnapshot as jest.Mock).mockImplementation((selector) => { + if (selector === MeetingsSelectors.getMeetingById) { + return (id: string) => (id === MOCK_MEETING.id ? MOCK_MEETING : null); + } + return () => null; + }); + await TestBed.configureTestingModule({ - imports: [MeetingDetailsComponent, MockModule(TranslateModule), MockModule(RouterModule)], + imports: [ + MeetingDetailsComponent, + ...MockComponents(SubHeaderComponent, SearchInputComponent), + MockPipe(TranslatePipe), + TableModule, + Button, + Skeleton, + MockPipe(DatePipe), + ], + providers: [ + MockProvider(Store, MOCK_STORE), + { provide: ActivatedRoute, useValue: mockActivatedRoute }, + { provide: Router, useValue: mockRouter }, + ], }).compileComponents(); fixture = TestBed.createComponent(MeetingDetailsComponent); @@ -23,4 +86,51 @@ describe('MeetingDetailsComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + it('should initialize with default table params', () => { + expect(component.tableParams().rows).toBeDefined(); + expect(component.tableParams().firstRowIndex).toBe(0); + }); + + it('should open download link if present', () => { + const openSpy = jest.spyOn(window, 'open').mockImplementation(); + const event = { stopPropagation: jest.fn() } as unknown as Event; + component.downloadSubmission(event, MOCK_MEETING_SUBMISSIONS[0]); + expect(openSpy).toHaveBeenCalledWith('https://example.com/file.pdf', '_blank'); + openSpy.mockRestore(); + }); + + it('should not open download link if not present', () => { + const openSpy = jest.spyOn(window, 'open').mockImplementation(); + const event = { stopPropagation: jest.fn() } as unknown as Event; + component.downloadSubmission(event, MOCK_MEETING_SUBMISSIONS[1]); + expect(openSpy).not.toHaveBeenCalled(); + openSpy.mockRestore(); + }); + + it('should update query params in router on page change', () => { + const router = TestBed.inject(Router); + const navigateSpy = jest.spyOn(router, 'navigate'); + component.onPageChange({ first: 10, rows: 10 } as TablePageEvent); + expect(navigateSpy).toHaveBeenCalledWith( + [], + expect.objectContaining({ + queryParams: expect.objectContaining({ page: '2', size: '10' }), + queryParamsHandling: 'merge', + }) + ); + }); + + it('should update query params in router on sort', () => { + const router = TestBed.inject(Router); + const navigateSpy = jest.spyOn(router, 'navigate'); + component.onSort({ field: 'title', order: 1 } as SortEvent); + expect(navigateSpy).toHaveBeenCalledWith( + [], + expect.objectContaining({ + queryParams: expect.objectContaining({ sortColumn: 'title', sortOrder: 'asc' }), + queryParamsHandling: 'merge', + }) + ); + }); }); diff --git a/src/app/shared/mocks/data.mock.ts b/src/app/shared/mocks/data.mock.ts index 0990872da..13e7b0f5f 100644 --- a/src/app/shared/mocks/data.mock.ts +++ b/src/app/shared/mocks/data.mock.ts @@ -1,5 +1,5 @@ import { User } from '@osf/core/models'; -import { Meeting } from '@osf/features/meetings/models/meetings.models'; +import { Meeting, MeetingSubmission } from '@osf/features/meetings/models/meetings.models'; export const MOCK_USER: User = { id: '1', @@ -62,3 +62,24 @@ export const MOCK_MEETING: Meeting = { startDate: new Date('2024-01-15'), endDate: new Date('2024-01-16'), }; + +export const MOCK_MEETING_SUBMISSIONS: MeetingSubmission[] = [ + { + id: '1', + title: 'The Impact of Open Science on Research Collaboration', + authorName: 'John Doe', + meetingCategory: 'Open Science', + dateCreated: new Date('2024-01-15'), + downloadCount: 5, + downloadLink: 'https://example.com/file.pdf', + }, + { + id: '2', + title: "'Data Sharing Practices in Modern Biology", + authorName: 'Jane Smith', + meetingCategory: 'Biology', + dateCreated: new Date('2024-01-19'), + downloadCount: 0, + downloadLink: null, + }, +]; diff --git a/src/app/shared/mocks/index.ts b/src/app/shared/mocks/index.ts index c1aa5b6b4..dcb47094e 100644 --- a/src/app/shared/mocks/index.ts +++ b/src/app/shared/mocks/index.ts @@ -1,4 +1,5 @@ export { MOCK_USER } from './data.mock'; export { MOCK_MEETING } from './data.mock'; +export { MOCK_MEETING_SUBMISSIONS } from './data.mock'; export { MOCK_STORE } from './mock-store.mock'; export { TranslateServiceMock } from './translate.service.mock'; diff --git a/src/app/shared/mocks/mock-store.mock.ts b/src/app/shared/mocks/mock-store.mock.ts index 252a1717e..db678a7b4 100644 --- a/src/app/shared/mocks/mock-store.mock.ts +++ b/src/app/shared/mocks/mock-store.mock.ts @@ -1,4 +1,5 @@ export const MOCK_STORE = { selectSignal: jest.fn(), + selectSnapshot: jest.fn(), dispatch: jest.fn(), }; From b315899f103a03c4225665bc7f47984c0808f418 Mon Sep 17 00:00:00 2001 From: Diana Date: Wed, 30 Jul 2025 14:11:12 +0300 Subject: [PATCH 3/6] test(testing): refactored meeting component tests --- .../meetings-feature-card.component.spec.ts | 6 +- .../meeting-details.component.spec.ts | 7 +- .../meetings-landing.component.spec.ts | 111 ++++++------------ 3 files changed, 41 insertions(+), 83 deletions(-) diff --git a/src/app/features/meetings/components/meetings-feature-card/meetings-feature-card.component.spec.ts b/src/app/features/meetings/components/meetings-feature-card/meetings-feature-card.component.spec.ts index ff4c63ad4..dbc686b94 100644 --- a/src/app/features/meetings/components/meetings-feature-card/meetings-feature-card.component.spec.ts +++ b/src/app/features/meetings/components/meetings-feature-card/meetings-feature-card.component.spec.ts @@ -1,7 +1,5 @@ import { TranslatePipe } from '@ngx-translate/core'; -import { MockComponent, MockPipe } from 'ng-mocks'; - -import { Card } from 'primeng/card'; +import { MockPipe } from 'ng-mocks'; import { ComponentRef } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; @@ -15,7 +13,7 @@ describe('MeetingsFeatureCardComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [MeetingsFeatureCardComponent, MockComponent(Card), MockPipe(TranslatePipe, (value) => value)], + imports: [MeetingsFeatureCardComponent, MockPipe(TranslatePipe, (value) => value)], }).compileComponents(); fixture = TestBed.createComponent(MeetingsFeatureCardComponent); diff --git a/src/app/features/meetings/pages/meeting-details/meeting-details.component.spec.ts b/src/app/features/meetings/pages/meeting-details/meeting-details.component.spec.ts index 4cb227b19..d1cb299a7 100644 --- a/src/app/features/meetings/pages/meeting-details/meeting-details.component.spec.ts +++ b/src/app/features/meetings/pages/meeting-details/meeting-details.component.spec.ts @@ -4,9 +4,7 @@ import { TranslatePipe } from '@ngx-translate/core'; import { MockComponents, MockPipe, MockProvider } from 'ng-mocks'; import { SortEvent } from 'primeng/api'; -import { Button } from 'primeng/button'; -import { Skeleton } from 'primeng/skeleton'; -import { TableModule, TablePageEvent } from 'primeng/table'; +import { TablePageEvent } from 'primeng/table'; import { of } from 'rxjs'; @@ -66,9 +64,6 @@ describe('MeetingDetailsComponent', () => { MeetingDetailsComponent, ...MockComponents(SubHeaderComponent, SearchInputComponent), MockPipe(TranslatePipe), - TableModule, - Button, - Skeleton, MockPipe(DatePipe), ], providers: [ diff --git a/src/app/features/meetings/pages/meetings-landing/meetings-landing.component.spec.ts b/src/app/features/meetings/pages/meetings-landing/meetings-landing.component.spec.ts index cdee596e1..f56a5ff37 100644 --- a/src/app/features/meetings/pages/meetings-landing/meetings-landing.component.spec.ts +++ b/src/app/features/meetings/pages/meetings-landing/meetings-landing.component.spec.ts @@ -1,18 +1,17 @@ import { provideStore } from '@ngxs/store'; -import { TranslateModule, TranslatePipe } from '@ngx-translate/core'; +import { TranslatePipe, TranslateService } from '@ngx-translate/core'; import { MockComponents, MockPipe, MockProvider } from 'ng-mocks'; -import { Skeleton } from 'primeng/skeleton'; -import { TableModule } from 'primeng/table'; - import { of } from 'rxjs'; -import { signal } from '@angular/core'; +import { provideHttpClient } from '@angular/common/http'; +import { provideHttpClientTesting } from '@angular/common/http/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { FormControl, ReactiveFormsModule } from '@angular/forms'; +import { FormControl } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; +import { MeetingsState } from '@osf/features/meetings/store'; import { MOCK_MEETING } from '@osf/shared/mocks'; import { SearchInputComponent, SubHeaderComponent } from '@shared/components'; import { TABLE_PARAMS } from '@shared/constants'; @@ -50,13 +49,16 @@ describe('MeetingsLandingComponent', () => { imports: [ MeetingsLandingComponent, ...MockComponents(SubHeaderComponent, SearchInputComponent, MeetingsFeatureCardComponent), - ReactiveFormsModule, - TranslateModule.forRoot(), - TableModule, - Skeleton, MockPipe(TranslatePipe), ], - providers: [MockProvider(ActivatedRoute, mockActivatedRoute), MockProvider(Router, mockRouter), provideStore([])], + providers: [ + MockProvider(ActivatedRoute, mockActivatedRoute), + MockProvider(Router, mockRouter), + MockProvider(TranslateService), + provideStore([MeetingsState]), + provideHttpClient(), + provideHttpClientTesting(), + ], }).compileComponents(); fixture = TestBed.createComponent(MeetingsLandingComponent); @@ -125,74 +127,44 @@ describe('MeetingsLandingComponent', () => { }); }); - it('should not update query params when sort field is undefined', () => { + it('should call router.navigate with correct params on search', () => { + jest.useFakeTimers(); jest.clearAllMocks(); - component.onSort({ field: undefined, order: 1 }); - expect(router.navigate).not.toHaveBeenCalled(); + + component.searchControl.setValue('test search'); + jest.advanceTimersByTime(450); + + expect(router.navigate).toHaveBeenCalledWith( + [], + expect.objectContaining({ + queryParams: expect.objectContaining({ search: 'test search', page: '1' }), + queryParamsHandling: 'merge', + }) + ); }); - it('should call router.navigate with correct params on search', (done) => { + it('should call router.navigate only once on second input', () => { + jest.useFakeTimers(); jest.clearAllMocks(); - setTimeout(() => { - component.searchControl.setValue('test search'); - setTimeout(() => { - expect(router.navigate).toHaveBeenCalledWith( - [], - expect.objectContaining({ - queryParams: expect.objectContaining({ search: 'test search', page: '1' }), - queryParamsHandling: 'merge', - }) - ); - done(); - }, 350); - }, 100); - }); - it('should show skeletonData when isMeetingsLoading is true', () => { - component.isMeetingsLoading = signal(true); - expect(component.isMeetingsLoading()).toBe(true); - expect(component.skeletonData.length).toBe(10); - }); + component.searchControl.setValue('first'); - it('should show empty table when meetings is empty and not loading', () => { - component.meetings = signal([]); - component.isMeetingsLoading = signal(false); - expect(component.meetings().length).toBe(0); - expect(component.isMeetingsLoading()).toBe(false); - }); + jest.advanceTimersByTime(100); - it('should use skeletonData when meetings is empty and isMeetingsLoading is true', () => { - component.meetings = signal([]); - component.isMeetingsLoading = signal(true); - const data = component.isMeetingsLoading() ? component.skeletonData : component.meetings(); - expect(data).toEqual(component.skeletonData); - expect(data.length).toBe(10); - }); + component.searchControl.setValue('second'); - it('should use meetings when meetings is not empty and isMeetingsLoading is false', () => { - component.meetings = signal([mockMeeting]); - component.isMeetingsLoading = signal(false); - const data = component.isMeetingsLoading() ? component.skeletonData : component.meetings(); - expect(data).toEqual([mockMeeting]); - }); + jest.advanceTimersByTime(350); - it('should debounce search input and call router.navigate only once', (done) => { - jest.clearAllMocks(); - component.searchControl.setValue('first'); - setTimeout(() => { - component.searchControl.setValue('second'); - setTimeout(() => { - expect(router.navigate).toHaveBeenCalledTimes(1); - done(); - }, 350); - }, 100); + expect(router.navigate).toHaveBeenCalledTimes(1); }); - it('should not throw on onPageChange with rows=0', () => { - expect(() => component.onPageChange({ first: 0, rows: 0 })).not.toThrow(); + it('should not call router.navigate if onSort called with field undefined', () => { + jest.clearAllMocks(); + component.onSort({ field: undefined, order: 1 }); + expect(router.navigate).not.toHaveBeenCalled(); }); - it('should not call router.navigate if onSort called with field undefined', () => { + it('should not update query params when sort field is undefined', () => { jest.clearAllMocks(); component.onSort({ field: undefined, order: 1 }); expect(router.navigate).not.toHaveBeenCalled(); @@ -218,11 +190,4 @@ describe('MeetingsLandingComponent', () => { }) ); }); - - it('should use empty array when meetings is empty and isMeetingsLoading is false', () => { - component.meetings = signal([]); - component.isMeetingsLoading = signal(false); - const data = component.isMeetingsLoading() ? component.skeletonData : component.meetings(); - expect(data).toEqual([]); - }); }); From f2e63b4cb409dc6b5c7b96718fe6513324ff5f37 Mon Sep 17 00:00:00 2001 From: Diana Date: Wed, 30 Jul 2025 16:34:53 +0300 Subject: [PATCH 4/6] test(testing): added test for handling undefined query params --- .../meetings-landing.component.spec.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/app/features/meetings/pages/meetings-landing/meetings-landing.component.spec.ts b/src/app/features/meetings/pages/meetings-landing/meetings-landing.component.spec.ts index f56a5ff37..21d1816ba 100644 --- a/src/app/features/meetings/pages/meetings-landing/meetings-landing.component.spec.ts +++ b/src/app/features/meetings/pages/meetings-landing/meetings-landing.component.spec.ts @@ -11,6 +11,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { FormControl } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; +import { parseQueryFilterParams } from '@core/helpers'; import { MeetingsState } from '@osf/features/meetings/store'; import { MOCK_MEETING } from '@osf/shared/mocks'; import { SearchInputComponent, SubHeaderComponent } from '@shared/components'; @@ -190,4 +191,15 @@ describe('MeetingsLandingComponent', () => { }) ); }); + + it('should do nothing when queryParams is undefined', () => { + const parseQueryFilterParamsSpy = jest.spyOn({ parseQueryFilterParams }, 'parseQueryFilterParams'); + jest.spyOn(component, 'queryParams').mockReturnValue(undefined); + + fixture.detectChanges(); + + expect(parseQueryFilterParamsSpy).not.toHaveBeenCalled(); + + parseQueryFilterParamsSpy.mockRestore(); + }); }); From a60658d984ddb325fbfaa3cdbc0e8e44f76b37a2 Mon Sep 17 00:00:00 2001 From: Diana Date: Wed, 30 Jul 2025 16:38:25 +0300 Subject: [PATCH 5/6] test(refactor): move meeting mock data to separate file --- src/app/shared/mocks/data.mock.ts | 31 ---------------------------- src/app/shared/mocks/index.ts | 4 ++-- src/app/shared/mocks/meeting.mock.ts | 31 ++++++++++++++++++++++++++++ 3 files changed, 33 insertions(+), 33 deletions(-) create mode 100644 src/app/shared/mocks/meeting.mock.ts diff --git a/src/app/shared/mocks/data.mock.ts b/src/app/shared/mocks/data.mock.ts index 13e7b0f5f..dd7390f08 100644 --- a/src/app/shared/mocks/data.mock.ts +++ b/src/app/shared/mocks/data.mock.ts @@ -1,5 +1,4 @@ import { User } from '@osf/core/models'; -import { Meeting, MeetingSubmission } from '@osf/features/meetings/models/meetings.models'; export const MOCK_USER: User = { id: '1', @@ -53,33 +52,3 @@ export const MOCK_USER: User = { defaultRegionId: 'us', allowIndexing: true, }; - -export const MOCK_MEETING: Meeting = { - id: '1', - name: 'Test Meeting', - submissionsCount: 10, - location: 'New York', - startDate: new Date('2024-01-15'), - endDate: new Date('2024-01-16'), -}; - -export const MOCK_MEETING_SUBMISSIONS: MeetingSubmission[] = [ - { - id: '1', - title: 'The Impact of Open Science on Research Collaboration', - authorName: 'John Doe', - meetingCategory: 'Open Science', - dateCreated: new Date('2024-01-15'), - downloadCount: 5, - downloadLink: 'https://example.com/file.pdf', - }, - { - id: '2', - title: "'Data Sharing Practices in Modern Biology", - authorName: 'Jane Smith', - meetingCategory: 'Biology', - dateCreated: new Date('2024-01-19'), - downloadCount: 0, - downloadLink: null, - }, -]; diff --git a/src/app/shared/mocks/index.ts b/src/app/shared/mocks/index.ts index dcb47094e..b212a8b6a 100644 --- a/src/app/shared/mocks/index.ts +++ b/src/app/shared/mocks/index.ts @@ -1,5 +1,5 @@ export { MOCK_USER } from './data.mock'; -export { MOCK_MEETING } from './data.mock'; -export { MOCK_MEETING_SUBMISSIONS } from './data.mock'; +export { MOCK_MEETING } from './meeting.mock'; +export { MOCK_MEETING_SUBMISSIONS } from './meeting.mock'; export { MOCK_STORE } from './mock-store.mock'; export { TranslateServiceMock } from './translate.service.mock'; diff --git a/src/app/shared/mocks/meeting.mock.ts b/src/app/shared/mocks/meeting.mock.ts new file mode 100644 index 000000000..6a60bfcfd --- /dev/null +++ b/src/app/shared/mocks/meeting.mock.ts @@ -0,0 +1,31 @@ +import { Meeting, MeetingSubmission } from '@osf/features/meetings/models'; + +export const MOCK_MEETING: Meeting = { + id: '1', + name: 'Test Meeting', + submissionsCount: 10, + location: 'New York', + startDate: new Date('2024-01-15'), + endDate: new Date('2024-01-16'), +}; + +export const MOCK_MEETING_SUBMISSIONS: MeetingSubmission[] = [ + { + id: '1', + title: 'The Impact of Open Science on Research Collaboration', + authorName: 'John Doe', + meetingCategory: 'Open Science', + dateCreated: new Date('2024-01-15'), + downloadCount: 5, + downloadLink: 'https://example.com/file.pdf', + }, + { + id: '2', + title: "'Data Sharing Practices in Modern Biology", + authorName: 'Jane Smith', + meetingCategory: 'Biology', + dateCreated: new Date('2024-01-19'), + downloadCount: 0, + downloadLink: null, + }, +]; From 23fc9df5236977590af28a00f94e9a32717676d9 Mon Sep 17 00:00:00 2001 From: Diana Date: Wed, 30 Jul 2025 16:58:23 +0300 Subject: [PATCH 6/6] test(refactor): simplify exports --- src/app/shared/mocks/index.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/app/shared/mocks/index.ts b/src/app/shared/mocks/index.ts index b212a8b6a..23d8a20fe 100644 --- a/src/app/shared/mocks/index.ts +++ b/src/app/shared/mocks/index.ts @@ -1,5 +1,4 @@ export { MOCK_USER } from './data.mock'; -export { MOCK_MEETING } from './meeting.mock'; -export { MOCK_MEETING_SUBMISSIONS } from './meeting.mock'; +export * from './meeting.mock'; export { MOCK_STORE } from './mock-store.mock'; export { TranslateServiceMock } from './translate.service.mock';