@@ -13,10 +13,16 @@ import initializeStore from '../store';
13
13
import messages from './messages' ;
14
14
import generalMessages from '../messages' ;
15
15
import scanResultsMessages from './scan-results/messages' ;
16
- import CourseOptimizerPage , { pollLinkCheckDuringScan } from './CourseOptimizerPage' ;
16
+ import CourseOptimizerPage , { pollLinkCheckDuringScan , pollRerunLinkUpdateDuringUpdate , pollRerunLinkUpdateStatus } from './CourseOptimizerPage' ;
17
17
import { postLinkCheckCourseApiUrl , getLinkCheckStatusApiUrl } from './data/api' ;
18
- import { mockApiResponse , mockApiResponseForNoResultFound } from './mocks/mockApiResponse' ;
18
+ import {
19
+ mockApiResponse ,
20
+ mockApiResponseForNoResultFound ,
21
+ mockApiResponseWithPreviousRunLinks ,
22
+ mockApiResponseEmpty ,
23
+ } from './mocks/mockApiResponse' ;
19
24
import * as thunks from './data/thunks' ;
25
+ import { useWaffleFlags } from '../data/apiHooks' ;
20
26
21
27
let store ;
22
28
let axiosMock ;
@@ -29,6 +35,19 @@ jest.mock('../generic/model-store', () => ({
29
35
} ) ,
30
36
} ) ) ;
31
37
38
+ // Mock the waffle flags hook
39
+ jest . mock ( '../data/apiHooks' , ( ) => ( {
40
+ useWaffleFlags : jest . fn ( ( ) => ( {
41
+ enableCourseOptimizerCheckPrevRunLinks : false ,
42
+ } ) ) ,
43
+ } ) ) ;
44
+
45
+ jest . mock ( '../generic/model-store' , ( ) => ( {
46
+ useModel : jest . fn ( ) . mockReturnValue ( {
47
+ name : 'About Node JS' ,
48
+ } ) ,
49
+ } ) ) ;
50
+
32
51
const OptimizerPage = ( ) => (
33
52
< AppProvider store = { store } >
34
53
< IntlProvider locale = "en" messages = { { } } >
@@ -155,11 +174,11 @@ describe('CourseOptimizerPage', () => {
155
174
expect ( getByText ( messages . headingTitle . defaultMessage ) ) . toBeInTheDocument ( ) ;
156
175
fireEvent . click ( getByText ( messages . buttonTitle . defaultMessage ) ) ;
157
176
await waitFor ( ( ) => {
158
- expect ( getByText ( scanResultsMessages . noBrokenLinksCard . defaultMessage ) ) . toBeInTheDocument ( ) ;
177
+ expect ( getByText ( scanResultsMessages . noResultsFound . defaultMessage ) ) . toBeInTheDocument ( ) ;
159
178
} ) ;
160
179
} ) ;
161
180
162
- it ( 'should show error message if request does not go through' , async ( ) => {
181
+ it ( 'should show an error state in the scan stepper if request does not go through' , async ( ) => {
163
182
axiosMock
164
183
. onPost ( postLinkCheckCourseApiUrl ( courseId ) )
165
184
. reply ( 500 ) ;
@@ -180,17 +199,17 @@ describe('CourseOptimizerPage', () => {
180
199
} = await setupOptimizerPage ( ) ;
181
200
// Check if the modal is opened
182
201
expect ( getByText ( 'Locked' ) ) . toBeInTheDocument ( ) ;
183
- // Select the broken links checkbox
202
+ // Select the locked links checkbox
184
203
fireEvent . click ( getByLabelText ( scanResultsMessages . lockedLabel . defaultMessage ) ) ;
185
204
186
205
const collapsibleTrigger = container . querySelector ( '.collapsible-trigger' ) ;
187
206
expect ( collapsibleTrigger ) . toBeInTheDocument ( ) ;
188
207
fireEvent . click ( collapsibleTrigger ) ;
189
208
190
209
await waitFor ( ( ) => {
191
- expect ( getByText ( 'Test Locked Links ' ) ) . toBeInTheDocument ( ) ;
192
- expect ( queryByText ( 'Test Broken Links ' ) ) . not . toBeInTheDocument ( ) ;
193
- expect ( queryByText ( 'Test Manual Links ' ) ) . not . toBeInTheDocument ( ) ;
210
+ expect ( getByText ( 'https://example.com/locked-link ' ) ) . toBeInTheDocument ( ) ;
211
+ expect ( queryByText ( 'https://example.com/broken-link ' ) ) . not . toBeInTheDocument ( ) ;
212
+ expect ( queryByText ( 'https://outsider.com/forbidden-link ' ) ) . not . toBeInTheDocument ( ) ;
194
213
} ) ;
195
214
} ) ;
196
215
@@ -205,15 +224,14 @@ describe('CourseOptimizerPage', () => {
205
224
expect ( getByText ( 'Broken' ) ) . toBeInTheDocument ( ) ;
206
225
// Select the broken links checkbox
207
226
fireEvent . click ( getByLabelText ( scanResultsMessages . brokenLabel . defaultMessage ) ) ;
208
-
209
227
const collapsibleTrigger = container . querySelector ( '.collapsible-trigger' ) ;
210
228
expect ( collapsibleTrigger ) . toBeInTheDocument ( ) ;
211
229
fireEvent . click ( collapsibleTrigger ) ;
212
230
213
231
await waitFor ( ( ) => {
214
- expect ( getByText ( 'Test Broken Links ' ) ) . toBeInTheDocument ( ) ;
215
- expect ( queryByText ( 'Test Locked Links ' ) ) . not . toBeInTheDocument ( ) ;
216
- expect ( queryByText ( 'Test Manual Links ' ) ) . not . toBeInTheDocument ( ) ;
232
+ expect ( getByText ( 'https://example.com/broken-link ' ) ) . toBeInTheDocument ( ) ;
233
+ expect ( queryByText ( 'https://example.com/locked-link ' ) ) . not . toBeInTheDocument ( ) ;
234
+ expect ( queryByText ( 'https://outsider.com/forbidden-link ' ) ) . not . toBeInTheDocument ( ) ;
217
235
} ) ;
218
236
} ) ;
219
237
@@ -234,19 +252,19 @@ describe('CourseOptimizerPage', () => {
234
252
fireEvent . click ( collapsibleTrigger ) ;
235
253
236
254
await waitFor ( ( ) => {
237
- expect ( getByText ( 'Test Manual Links ' ) ) . toBeInTheDocument ( ) ;
238
- expect ( queryByText ( 'Test Broken Links ' ) ) . not . toBeInTheDocument ( ) ;
239
- expect ( queryByText ( 'Test Locked Links ' ) ) . not . toBeInTheDocument ( ) ;
255
+ expect ( getByText ( 'https://outsider.com/forbidden-link ' ) ) . toBeInTheDocument ( ) ;
256
+ expect ( queryByText ( 'https://example.com/broken-link ' ) ) . not . toBeInTheDocument ( ) ;
257
+ expect ( queryByText ( 'https://example.com/locked-link ' ) ) . not . toBeInTheDocument ( ) ;
240
258
} ) ;
241
259
242
260
// Click the manual links checkbox again to clear the filter
243
261
fireEvent . click ( getByLabelText ( scanResultsMessages . manualLabel . defaultMessage ) ) ;
244
262
245
263
// Assert that all links are displayed after clearing the filter
246
264
await waitFor ( ( ) => {
247
- expect ( getByText ( 'Test Broken Links ' ) ) . toBeInTheDocument ( ) ;
248
- expect ( getByText ( 'Test Manual Links ' ) ) . toBeInTheDocument ( ) ;
249
- expect ( getByText ( 'Test Locked Links ' ) ) . toBeInTheDocument ( ) ;
265
+ expect ( getByText ( 'https://example.com/broken-link ' ) ) . toBeInTheDocument ( ) ;
266
+ expect ( getByText ( 'https://outsider.com/forbidden-link ' ) ) . toBeInTheDocument ( ) ;
267
+ expect ( getByText ( 'https://example.com/locked-link ' ) ) . toBeInTheDocument ( ) ;
250
268
} ) ;
251
269
} ) ;
252
270
@@ -269,9 +287,9 @@ describe('CourseOptimizerPage', () => {
269
287
fireEvent . click ( collapsibleTrigger ) ;
270
288
271
289
await waitFor ( ( ) => {
272
- expect ( getByText ( 'Test Manual Links ' ) ) . toBeInTheDocument ( ) ;
273
- expect ( getByText ( 'Test Locked Links ' ) ) . toBeInTheDocument ( ) ;
274
- expect ( queryByText ( 'Test Broken Links ' ) ) . not . toBeInTheDocument ( ) ;
290
+ expect ( getByText ( 'https://outsider.com/forbidden-link ' ) ) . toBeInTheDocument ( ) ;
291
+ expect ( getByText ( 'https://example.com/locked-link ' ) ) . toBeInTheDocument ( ) ;
292
+ expect ( queryByText ( 'https://example.com/broken-link ' ) ) . not . toBeInTheDocument ( ) ;
275
293
} ) ;
276
294
} ) ;
277
295
@@ -295,9 +313,9 @@ describe('CourseOptimizerPage', () => {
295
313
fireEvent . click ( collapsibleTrigger ) ;
296
314
297
315
await waitFor ( ( ) => {
298
- expect ( getByText ( 'Test Broken Links ' ) ) . toBeInTheDocument ( ) ;
299
- expect ( getByText ( 'Test Manual Links ' ) ) . toBeInTheDocument ( ) ;
300
- expect ( getByText ( 'Test Locked Links ' ) ) . toBeInTheDocument ( ) ;
316
+ expect ( getByText ( 'https://example.com/broken-link ' ) ) . toBeInTheDocument ( ) ;
317
+ expect ( getByText ( 'https://outsider.com/forbidden-link ' ) ) . toBeInTheDocument ( ) ;
318
+ expect ( getByText ( 'https://example.com/locked-link ' ) ) . toBeInTheDocument ( ) ;
301
319
} ) ;
302
320
} ) ;
303
321
@@ -317,22 +335,22 @@ describe('CourseOptimizerPage', () => {
317
335
expect ( collapsibleTrigger ) . toBeInTheDocument ( ) ;
318
336
fireEvent . click ( collapsibleTrigger ) ;
319
337
320
- // Assert that all links are displayed
338
+ // Assert that both links are displayed
321
339
await waitFor ( ( ) => {
322
- expect ( getByText ( 'Test Broken Links ' ) ) . toBeInTheDocument ( ) ;
323
- expect ( getByText ( 'Test Manual Links ' ) ) . toBeInTheDocument ( ) ;
324
- expect ( queryByText ( 'Test Locked Links ' ) ) . not . toBeInTheDocument ( ) ;
340
+ expect ( getByText ( 'https://example.com/broken-link ' ) ) . toBeInTheDocument ( ) ;
341
+ expect ( getByText ( 'https://outsider.com/forbidden-link ' ) ) . toBeInTheDocument ( ) ;
342
+ expect ( queryByText ( 'https://example.com/locked-link ' ) ) . not . toBeInTheDocument ( ) ;
325
343
} ) ;
326
344
327
- // Click on the "Broken" chip to filter the results
345
+ // Click on the "Broken" chip to remove the broken filter (should leave only manual)
328
346
const brokenChip = getByTestId ( 'chip-brokenLinks' ) ;
329
347
fireEvent . click ( brokenChip ) ;
330
348
331
349
// Assert that only manual links are displayed
332
350
await waitFor ( ( ) => {
333
- expect ( queryByText ( 'Test Broken Links ' ) ) . not . toBeInTheDocument ( ) ;
334
- expect ( getByText ( 'Test Manual Links ' ) ) . toBeInTheDocument ( ) ;
335
- expect ( queryByText ( 'Test Locked Links ' ) ) . not . toBeInTheDocument ( ) ;
351
+ expect ( queryByText ( 'https://example.com/broken-link ' ) ) . not . toBeInTheDocument ( ) ;
352
+ expect ( getByText ( 'https://outsider.com/forbidden-link ' ) ) . toBeInTheDocument ( ) ;
353
+ expect ( queryByText ( 'https://example.com/locked-link ' ) ) . not . toBeInTheDocument ( ) ;
336
354
} ) ;
337
355
338
356
// Click the "Clear filters" button
@@ -341,9 +359,9 @@ describe('CourseOptimizerPage', () => {
341
359
342
360
// Assert that all links are displayed after clearing filters
343
361
await waitFor ( ( ) => {
344
- expect ( getByText ( 'Test Broken Links ' ) ) . toBeInTheDocument ( ) ;
345
- expect ( getByText ( 'Test Manual Links ' ) ) . toBeInTheDocument ( ) ;
346
- expect ( getByText ( 'Test Locked Links ' ) ) . toBeInTheDocument ( ) ;
362
+ expect ( getByText ( 'https://example.com/broken-link ' ) ) . toBeInTheDocument ( ) ;
363
+ expect ( getByText ( 'https://outsider.com/forbidden-link ' ) ) . toBeInTheDocument ( ) ;
364
+ expect ( getByText ( 'https://example.com/locked-link ' ) ) . toBeInTheDocument ( ) ;
347
365
} ) ;
348
366
} ) ;
349
367
@@ -361,5 +379,148 @@ describe('CourseOptimizerPage', () => {
361
379
expect ( getByText ( scanResultsMessages . noResultsFound . defaultMessage ) ) . toBeInTheDocument ( ) ;
362
380
} ) ;
363
381
} ) ;
382
+
383
+ it ( 'should always show no scan data message when data is empty' , async ( ) => {
384
+ axiosMock . onGet ( getLinkCheckStatusApiUrl ( courseId ) ) . reply ( 200 , mockApiResponseEmpty ) ;
385
+ const { getByText } = render ( < OptimizerPage /> ) ;
386
+
387
+ fireEvent . click ( getByText ( messages . buttonTitle . defaultMessage ) ) ;
388
+
389
+ await waitFor ( ( ) => {
390
+ expect ( getByText ( scanResultsMessages . noResultsFound . defaultMessage ) ) . toBeInTheDocument ( ) ;
391
+ } ) ;
392
+ } ) ;
393
+
394
+ describe ( 'Previous Run Links Feature' , ( ) => {
395
+ beforeEach ( ( ) => {
396
+ // Enable the waffle flag for previous run links
397
+ useWaffleFlags . mockReturnValue ( {
398
+ enableCourseOptimizerCheckPrevRunLinks : true ,
399
+ } ) ;
400
+ } ) ;
401
+
402
+ afterEach ( ( ) => {
403
+ // Reset to default (disabled)
404
+ useWaffleFlags . mockReturnValue ( {
405
+ enableCourseOptimizerCheckPrevRunLinks : false ,
406
+ } ) ;
407
+ } ) ;
408
+
409
+ it ( 'should show previous run links section when waffle flag is enabled and links exist' , async ( ) => {
410
+ axiosMock . onGet ( getLinkCheckStatusApiUrl ( courseId ) ) . reply ( 200 , mockApiResponseWithPreviousRunLinks ) ;
411
+ const { getByText } = render ( < OptimizerPage /> ) ;
412
+
413
+ fireEvent . click ( getByText ( messages . buttonTitle . defaultMessage ) ) ;
414
+
415
+ await waitFor ( ( ) => {
416
+ expect ( getByText ( scanResultsMessages . linkToPrevCourseRun . defaultMessage ) ) . toBeInTheDocument ( ) ;
417
+ } ) ;
418
+ } ) ;
419
+
420
+ it ( 'should show no results found for previous run links when flag is enabled but no links exist' , async ( ) => {
421
+ axiosMock . onGet ( getLinkCheckStatusApiUrl ( courseId ) ) . reply ( 200 , mockApiResponseForNoResultFound ) ;
422
+ const { getByText, getAllByText } = render ( < OptimizerPage /> ) ;
423
+
424
+ fireEvent . click ( getByText ( messages . buttonTitle . defaultMessage ) ) ;
425
+
426
+ await waitFor ( ( ) => {
427
+ expect ( getByText ( scanResultsMessages . linkToPrevCourseRun . defaultMessage ) ) . toBeInTheDocument ( ) ;
428
+ // Should show "No results found" for previous run section
429
+ const noResultsElements = getAllByText ( scanResultsMessages . noResultsFound . defaultMessage ) ;
430
+ expect ( noResultsElements . length ) . toBeGreaterThan ( 0 ) ;
431
+ } ) ;
432
+ } ) ;
433
+
434
+ it ( 'should not show previous run links section when waffle flag is disabled' , async ( ) => {
435
+ // Disable the flag
436
+ useWaffleFlags . mockReturnValue ( {
437
+ enableCourseOptimizerCheckPrevRunLinks : false ,
438
+ } ) ;
439
+
440
+ axiosMock . onGet ( getLinkCheckStatusApiUrl ( courseId ) ) . reply ( 200 , mockApiResponseWithPreviousRunLinks ) ;
441
+ const { getByText, queryByText } = render ( < OptimizerPage /> ) ;
442
+
443
+ fireEvent . click ( getByText ( messages . buttonTitle . defaultMessage ) ) ;
444
+
445
+ await waitFor ( ( ) => {
446
+ expect ( queryByText ( scanResultsMessages . linkToPrevCourseRun . defaultMessage ) ) . not . toBeInTheDocument ( ) ;
447
+ } ) ;
448
+ } ) ;
449
+
450
+ it ( 'should handle previous run links in course updates and custom pages' , async ( ) => {
451
+ axiosMock . onGet ( getLinkCheckStatusApiUrl ( courseId ) ) . reply ( 200 , mockApiResponseWithPreviousRunLinks ) ;
452
+ const { getByText, container } = render ( < OptimizerPage /> ) ;
453
+
454
+ fireEvent . click ( getByText ( messages . buttonTitle . defaultMessage ) ) ;
455
+
456
+ await waitFor ( ( ) => {
457
+ expect ( getByText ( scanResultsMessages . linkToPrevCourseRun . defaultMessage ) ) . toBeInTheDocument ( ) ;
458
+
459
+ const prevRunSections = container . querySelectorAll ( '.scan-results' ) ;
460
+ expect ( prevRunSections . length ) . toBeGreaterThan ( 1 ) ;
461
+ } ) ;
462
+ } ) ;
463
+ } ) ;
464
+
465
+ describe ( 'CourseOptimizerPage polling helpers - rerun' , ( ) => {
466
+ beforeEach ( ( ) => {
467
+ jest . restoreAllMocks ( ) ;
468
+ } ) ;
469
+
470
+ it ( 'starts polling when shouldPoll is true' , ( ) => {
471
+ const mockDispatch = jest . fn ( ) ;
472
+ const courseId = 'course-v1:Test+001' ;
473
+
474
+ // Mock setInterval to return a sentinel id
475
+ const intervalId = 123 ;
476
+ const setIntervalSpy = jest . spyOn ( global , 'setInterval' ) . mockImplementation ( ( ) => intervalId ) ;
477
+
478
+ const intervalRef = { current : undefined } ;
479
+
480
+ // Call with rerunLinkUpdateInProgress true so shouldPoll === true
481
+ pollRerunLinkUpdateDuringUpdate ( true , null , intervalRef , mockDispatch , courseId ) ;
482
+
483
+ expect ( setIntervalSpy ) . toHaveBeenCalled ( ) ;
484
+ expect ( intervalRef . current ) . toBe ( intervalId ) ;
485
+ } ) ;
486
+
487
+ it ( 'clears existing interval when shouldPoll is false' , ( ) => {
488
+ const mockDispatch = jest . fn ( ) ;
489
+ const courseId = 'course-v1:Test+002' ;
490
+ const clearIntervalSpy = jest . spyOn ( global , 'clearInterval' ) . mockImplementation ( ( ) => { } ) ;
491
+ const setIntervalSpy = jest . spyOn ( global , 'setInterval' ) . mockImplementation ( ( ) => 456 ) ;
492
+ const intervalRef = { current : 456 } ;
493
+
494
+ pollRerunLinkUpdateDuringUpdate ( false , { status : 'Succeeded' } , intervalRef , mockDispatch , courseId ) ;
495
+
496
+ expect ( clearIntervalSpy ) . toHaveBeenCalledWith ( 456 ) ;
497
+ expect ( intervalRef . current ) . toBeUndefined ( ) ;
498
+
499
+ setIntervalSpy . mockRestore ( ) ;
500
+ clearIntervalSpy . mockRestore ( ) ;
501
+ } ) ;
502
+
503
+ it ( 'pollRerunLinkUpdateStatus schedules dispatch at provided delay' , ( ) => {
504
+ jest . useFakeTimers ( ) ;
505
+ const mockDispatch = jest . fn ( ) ;
506
+ const courseId = 'course-v1:Test+003' ;
507
+
508
+ let capturedFn = null ;
509
+ jest . spyOn ( global , 'setInterval' ) . mockImplementation ( ( fn ) => {
510
+ capturedFn = fn ;
511
+ return 789 ;
512
+ } ) ;
513
+
514
+ const id = pollRerunLinkUpdateStatus ( mockDispatch , courseId , 1000 ) ;
515
+ expect ( id ) . toBe ( 789 ) ;
516
+
517
+ if ( capturedFn ) {
518
+ capturedFn ( ) ;
519
+ }
520
+ expect ( mockDispatch ) . toHaveBeenCalledWith ( expect . any ( Function ) ) ;
521
+
522
+ jest . useRealTimers ( ) ;
523
+ } ) ;
524
+ } ) ;
364
525
} ) ;
365
526
} ) ;
0 commit comments