Skip to content

Commit 5e47e9d

Browse files
feat: enhance course optimizer with prev run links update
1 parent f7bc0ab commit 5e47e9d

24 files changed

+5406
-473
lines changed

src/optimizer-page/CourseOptimizerPage.test.js

Lines changed: 196 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,16 @@ import initializeStore from '../store';
1313
import messages from './messages';
1414
import generalMessages from '../messages';
1515
import scanResultsMessages from './scan-results/messages';
16-
import CourseOptimizerPage, { pollLinkCheckDuringScan } from './CourseOptimizerPage';
16+
import CourseOptimizerPage, { pollLinkCheckDuringScan, pollRerunLinkUpdateDuringUpdate, pollRerunLinkUpdateStatus } from './CourseOptimizerPage';
1717
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';
1924
import * as thunks from './data/thunks';
25+
import { useWaffleFlags } from '../data/apiHooks';
2026

2127
let store;
2228
let axiosMock;
@@ -29,6 +35,19 @@ jest.mock('../generic/model-store', () => ({
2935
}),
3036
}));
3137

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+
3251
const OptimizerPage = () => (
3352
<AppProvider store={store}>
3453
<IntlProvider locale="en" messages={{}}>
@@ -155,11 +174,11 @@ describe('CourseOptimizerPage', () => {
155174
expect(getByText(messages.headingTitle.defaultMessage)).toBeInTheDocument();
156175
fireEvent.click(getByText(messages.buttonTitle.defaultMessage));
157176
await waitFor(() => {
158-
expect(getByText(scanResultsMessages.noBrokenLinksCard.defaultMessage)).toBeInTheDocument();
177+
expect(getByText(scanResultsMessages.noResultsFound.defaultMessage)).toBeInTheDocument();
159178
});
160179
});
161180

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 () => {
163182
axiosMock
164183
.onPost(postLinkCheckCourseApiUrl(courseId))
165184
.reply(500);
@@ -180,17 +199,17 @@ describe('CourseOptimizerPage', () => {
180199
} = await setupOptimizerPage();
181200
// Check if the modal is opened
182201
expect(getByText('Locked')).toBeInTheDocument();
183-
// Select the broken links checkbox
202+
// Select the locked links checkbox
184203
fireEvent.click(getByLabelText(scanResultsMessages.lockedLabel.defaultMessage));
185204

186205
const collapsibleTrigger = container.querySelector('.collapsible-trigger');
187206
expect(collapsibleTrigger).toBeInTheDocument();
188207
fireEvent.click(collapsibleTrigger);
189208

190209
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();
194213
});
195214
});
196215

@@ -205,15 +224,14 @@ describe('CourseOptimizerPage', () => {
205224
expect(getByText('Broken')).toBeInTheDocument();
206225
// Select the broken links checkbox
207226
fireEvent.click(getByLabelText(scanResultsMessages.brokenLabel.defaultMessage));
208-
209227
const collapsibleTrigger = container.querySelector('.collapsible-trigger');
210228
expect(collapsibleTrigger).toBeInTheDocument();
211229
fireEvent.click(collapsibleTrigger);
212230

213231
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();
217235
});
218236
});
219237

@@ -234,19 +252,19 @@ describe('CourseOptimizerPage', () => {
234252
fireEvent.click(collapsibleTrigger);
235253

236254
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();
240258
});
241259

242260
// Click the manual links checkbox again to clear the filter
243261
fireEvent.click(getByLabelText(scanResultsMessages.manualLabel.defaultMessage));
244262

245263
// Assert that all links are displayed after clearing the filter
246264
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();
250268
});
251269
});
252270

@@ -269,9 +287,9 @@ describe('CourseOptimizerPage', () => {
269287
fireEvent.click(collapsibleTrigger);
270288

271289
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();
275293
});
276294
});
277295

@@ -295,9 +313,9 @@ describe('CourseOptimizerPage', () => {
295313
fireEvent.click(collapsibleTrigger);
296314

297315
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();
301319
});
302320
});
303321

@@ -317,22 +335,22 @@ describe('CourseOptimizerPage', () => {
317335
expect(collapsibleTrigger).toBeInTheDocument();
318336
fireEvent.click(collapsibleTrigger);
319337

320-
// Assert that all links are displayed
338+
// Assert that both links are displayed
321339
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();
325343
});
326344

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)
328346
const brokenChip = getByTestId('chip-brokenLinks');
329347
fireEvent.click(brokenChip);
330348

331349
// Assert that only manual links are displayed
332350
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();
336354
});
337355

338356
// Click the "Clear filters" button
@@ -341,9 +359,9 @@ describe('CourseOptimizerPage', () => {
341359

342360
// Assert that all links are displayed after clearing filters
343361
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();
347365
});
348366
});
349367

@@ -361,5 +379,148 @@ describe('CourseOptimizerPage', () => {
361379
expect(getByText(scanResultsMessages.noResultsFound.defaultMessage)).toBeInTheDocument();
362380
});
363381
});
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+
});
364525
});
365526
});

0 commit comments

Comments
 (0)