|
| 1 | +import { Store } from '@ngxs/store'; |
| 2 | + |
1 | 3 | import { MockProvider, MockProviders } from 'ng-mocks'; |
2 | 4 |
|
3 | 5 | import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog'; |
4 | 6 |
|
5 | | -import { DestroyRef } from '@angular/core'; |
6 | | -import { ComponentFixture, TestBed } from '@angular/core/testing'; |
| 7 | +import { DestroyRef, signal } from '@angular/core'; |
| 8 | +import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; |
7 | 9 |
|
8 | 10 | import { RorFunderOption } from '../../models/ror.model'; |
9 | | -import { MetadataSelectors } from '../../store'; |
| 11 | +import { GetFundersList, MetadataSelectors } from '../../store'; |
10 | 12 |
|
11 | 13 | import { FundingDialogComponent } from './funding-dialog.component'; |
12 | 14 |
|
@@ -43,22 +45,15 @@ describe('FundingDialogComponent', () => { |
43 | 45 | expect(component).toBeTruthy(); |
44 | 46 | }); |
45 | 47 |
|
46 | | - it('should add funding entry', () => { |
47 | | - const initialLength = component.fundingEntries.length; |
48 | | - component.addFundingEntry(); |
49 | | - |
50 | | - expect(component.fundingEntries.length).toBe(initialLength + 1); |
51 | | - const entry = component.fundingEntries.at(component.fundingEntries.length - 1); |
52 | | - expect(entry.get('funderName')?.value).toBe(null); |
53 | | - expect(entry.get('awardTitle')?.value).toBe(''); |
54 | | - }); |
55 | | - |
56 | | - it('should not remove funding entry when only one exists', () => { |
| 48 | + it('should not remove last funding entry and close dialog with empty result', () => { |
| 49 | + const dialogRef = TestBed.inject(DynamicDialogRef); |
| 50 | + const closeSpy = jest.spyOn(dialogRef, 'close'); |
57 | 51 | expect(component.fundingEntries.length).toBe(1); |
58 | 52 |
|
59 | 53 | component.removeFundingEntry(0); |
60 | 54 |
|
61 | 55 | expect(component.fundingEntries.length).toBe(1); |
| 56 | + expect(closeSpy).toHaveBeenCalledWith({ fundingEntries: [] }); |
62 | 57 | }); |
63 | 58 |
|
64 | 59 | it('should save valid form data', () => { |
@@ -165,13 +160,6 @@ describe('FundingDialogComponent', () => { |
165 | 160 | expect(component.fundingEntries.length).toBe(1); |
166 | 161 | }); |
167 | 162 |
|
168 | | - it('should not remove funding entry when only one exists', () => { |
169 | | - expect(component.fundingEntries.length).toBe(1); |
170 | | - |
171 | | - component.removeFundingEntry(0); |
172 | | - expect(component.fundingEntries.length).toBe(1); |
173 | | - }); |
174 | | - |
175 | 163 | it('should not remove funding entry when index is out of bounds', () => { |
176 | 164 | component.addFundingEntry(); |
177 | 165 | const initialLength = component.fundingEntries.length; |
@@ -239,32 +227,117 @@ describe('FundingDialogComponent', () => { |
239 | 227 | expect(entry.get('awardNumber')?.value).toBe(''); |
240 | 228 | }); |
241 | 229 |
|
242 | | - it('should emit search query to searchSubject', () => { |
243 | | - const searchSpy = jest.spyOn(component['searchSubject'], 'next'); |
244 | | - |
245 | | - component.onFunderSearch('test search'); |
246 | | - |
247 | | - expect(searchSpy).toHaveBeenCalledWith('test search'); |
| 230 | + it('should dispatch getFundersList after debounce when searching', fakeAsync(() => { |
| 231 | + const store = TestBed.inject(Store); |
| 232 | + const dispatchSpy = jest.spyOn(store, 'dispatch'); |
| 233 | + |
| 234 | + component.onFunderSearch('query'); |
| 235 | + expect(dispatchSpy).not.toHaveBeenCalled(); |
| 236 | + tick(300); |
| 237 | + expect(dispatchSpy).toHaveBeenCalledWith(new GetFundersList('query')); |
| 238 | + })); |
| 239 | + |
| 240 | + it('should pre-populate entries from config funders on init', () => { |
| 241 | + TestBed.resetTestingModule(); |
| 242 | + const configFunders = [ |
| 243 | + { |
| 244 | + funderName: 'NSF', |
| 245 | + funderIdentifier: 'https://ror.org/nsf', |
| 246 | + funderIdentifierType: 'ROR', |
| 247 | + awardTitle: 'Grant A', |
| 248 | + awardUri: 'https://example.com/a', |
| 249 | + awardNumber: '123', |
| 250 | + }, |
| 251 | + ]; |
| 252 | + TestBed.configureTestingModule({ |
| 253 | + imports: [FundingDialogComponent, OSFTestingModule], |
| 254 | + providers: [ |
| 255 | + MockProviders(DynamicDialogRef, DestroyRef), |
| 256 | + MockProvider(DynamicDialogConfig, { data: { funders: configFunders } }), |
| 257 | + provideMockStore({ |
| 258 | + signals: [ |
| 259 | + { selector: MetadataSelectors.getFundersList, value: [] }, |
| 260 | + { selector: MetadataSelectors.getFundersLoading, value: false }, |
| 261 | + ], |
| 262 | + }), |
| 263 | + ], |
| 264 | + }).compileComponents(); |
| 265 | + const f = TestBed.createComponent(FundingDialogComponent); |
| 266 | + f.detectChanges(); |
| 267 | + const c = f.componentInstance; |
| 268 | + expect(c.fundingEntries.length).toBe(1); |
| 269 | + const entry = c.fundingEntries.at(0); |
| 270 | + expect(entry.get('funderName')?.value).toBe('NSF'); |
| 271 | + expect(entry.get('funderIdentifier')?.value).toBe('https://ror.org/nsf'); |
| 272 | + expect(entry.get('funderIdentifierType')?.value).toBe('ROR'); |
| 273 | + expect(entry.get('awardTitle')?.value).toBe('Grant A'); |
| 274 | + expect(entry.get('awardUri')?.value).toBe('https://example.com/a'); |
| 275 | + expect(entry.get('awardNumber')?.value).toBe('123'); |
248 | 276 | }); |
249 | 277 |
|
250 | | - it('should handle empty search term', () => { |
251 | | - const searchSpy = jest.spyOn(component['searchSubject'], 'next'); |
252 | | - |
253 | | - component.onFunderSearch(''); |
| 278 | + it('getOptionsForIndex returns custom option plus list when entry name is not in list', () => { |
| 279 | + const entry = component.fundingEntries.at(0); |
| 280 | + entry.patchValue({ funderName: 'Custom Funder', funderIdentifier: 'custom-id' }); |
| 281 | + const options = component.getOptionsForIndex(0); |
| 282 | + expect(options).toHaveLength(2); |
| 283 | + expect(options[0]).toEqual({ id: 'custom-id', name: 'Custom Funder' }); |
| 284 | + expect(options[1]).toEqual(MOCK_ROR_FUNDERS[0]); |
| 285 | + }); |
254 | 286 |
|
255 | | - expect(searchSpy).toHaveBeenCalledWith(''); |
| 287 | + it('getOptionsForIndex returns list when entry has no name', () => { |
| 288 | + const options = component.getOptionsForIndex(0); |
| 289 | + expect(options).toEqual(MOCK_ROR_FUNDERS); |
256 | 290 | }); |
257 | 291 |
|
258 | | - it('should handle multiple search calls', () => { |
259 | | - const searchSpy = jest.spyOn(component['searchSubject'], 'next'); |
| 292 | + it('filterMessage returns loading key when funders loading', () => { |
| 293 | + TestBed.resetTestingModule(); |
| 294 | + const loadingSignal = signal(true); |
| 295 | + TestBed.configureTestingModule({ |
| 296 | + imports: [FundingDialogComponent, OSFTestingModule], |
| 297 | + providers: [ |
| 298 | + MockProviders(DynamicDialogRef, DestroyRef), |
| 299 | + MockProvider(DynamicDialogConfig, { data: { funders: [] } }), |
| 300 | + provideMockStore({ |
| 301 | + signals: [ |
| 302 | + { selector: MetadataSelectors.getFundersList, value: [] }, |
| 303 | + { selector: MetadataSelectors.getFundersLoading, value: loadingSignal }, |
| 304 | + ], |
| 305 | + }), |
| 306 | + ], |
| 307 | + }).compileComponents(); |
| 308 | + const f = TestBed.createComponent(FundingDialogComponent); |
| 309 | + f.detectChanges(); |
| 310 | + expect(f.componentInstance.filterMessage()).toBe('project.metadata.funding.dialog.loadingFunders'); |
| 311 | + loadingSignal.set(false); |
| 312 | + expect(f.componentInstance.filterMessage()).toBe('project.metadata.funding.dialog.noFundersFound'); |
| 313 | + }); |
260 | 314 |
|
261 | | - component.onFunderSearch('first'); |
262 | | - component.onFunderSearch('second'); |
263 | | - component.onFunderSearch('third'); |
| 315 | + it('save returns only entries with at least one of funderName, awardTitle, awardUri, awardNumber', () => { |
| 316 | + const dialogRef = TestBed.inject(DynamicDialogRef); |
| 317 | + const closeSpy = jest.spyOn(dialogRef, 'close'); |
| 318 | + component.addFundingEntry(); |
| 319 | + component.fundingEntries.at(0).patchValue({ funderName: 'Funder A', awardTitle: 'Award A' }); |
| 320 | + component.fundingEntries.at(1).patchValue({ funderName: 'Funder B', awardTitle: 'Award B' }); |
| 321 | + fixture.detectChanges(); |
| 322 | + component.save(); |
| 323 | + expect(closeSpy).toHaveBeenCalledWith({ |
| 324 | + fundingEntries: [ |
| 325 | + expect.objectContaining({ funderName: 'Funder A', awardTitle: 'Award A' }), |
| 326 | + expect.objectContaining({ funderName: 'Funder B', awardTitle: 'Award B' }), |
| 327 | + ], |
| 328 | + }); |
| 329 | + }); |
264 | 330 |
|
265 | | - expect(searchSpy).toHaveBeenCalledTimes(3); |
266 | | - expect(searchSpy).toHaveBeenNthCalledWith(1, 'first'); |
267 | | - expect(searchSpy).toHaveBeenNthCalledWith(2, 'second'); |
268 | | - expect(searchSpy).toHaveBeenNthCalledWith(3, 'third'); |
| 331 | + it('should not save when awardUri is invalid', () => { |
| 332 | + const dialogRef = TestBed.inject(DynamicDialogRef); |
| 333 | + const closeSpy = jest.spyOn(dialogRef, 'close'); |
| 334 | + const entry = component.fundingEntries.at(0); |
| 335 | + entry.patchValue({ |
| 336 | + funderName: 'Test Funder', |
| 337 | + awardUri: 'not-a-valid-url', |
| 338 | + }); |
| 339 | + fixture.detectChanges(); |
| 340 | + component.save(); |
| 341 | + expect(closeSpy).not.toHaveBeenCalled(); |
269 | 342 | }); |
270 | 343 | }); |
0 commit comments