Skip to content

Commit f8abe08

Browse files
authored
fix(learn): fixed confetti trigger on every step (freeCodeCamp#65160)
1 parent 311fb3c commit f8abe08

2 files changed

Lines changed: 63 additions & 14 deletions

File tree

client/src/templates/Challenges/components/completion-modal.test.tsx

Lines changed: 60 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import React from 'react';
22
import { runSaga } from 'redux-saga';
3-
import { describe, test, it, expect, beforeEach, vi, Mock } from 'vitest';
4-
import { buildChallenge } from '@freecodecamp/challenge-builder/build';
5-
3+
import { describe, test, it, expect, beforeEach, vi, type Mock } from 'vitest';
64
import { render } from '../../../../utils/test-utils';
75

86
import { getCompletedPercentage } from '../../../utils/get-completion-percentage';
@@ -14,14 +12,20 @@ import {
1412
challengeMetaSelector,
1513
challengeTestsSelector,
1614
isBuildEnabledSelector,
17-
isBlockNewlyCompletedSelector
15+
isBlockNewlyCompletedSelector,
16+
currentBlockIdsSelector
1817
} from '../redux/selectors';
18+
import {
19+
completedChallengesIdsSelector,
20+
allChallengesInfoSelector
21+
} from '../../../redux/selectors';
1922
import { getTestRunner } from '../utils/build';
2023
import CompletionModal, { combineFileData } from './completion-modal';
2124
vi.mock('../../../analytics');
2225
vi.mock('../../../utils/fire-confetti');
2326
vi.mock('../../../components/Progress');
2427
vi.mock('../redux/selectors');
28+
vi.mock('../../../redux/selectors');
2529
vi.mock('../utils/build');
2630
vi.mock('../../../utils/get-words');
2731
vi.mock('@freecodecamp/challenge-builder/build');
@@ -32,7 +36,11 @@ const mockChallengeTestsSelector = challengeTestsSelector as Mock;
3236
const mockChallengeMetaSelector = challengeMetaSelector as Mock;
3337
const mockChallengeDataSelector = challengeDataSelector as Mock;
3438
const mockIsBlockNewlyCompletedSelector = isBlockNewlyCompletedSelector as Mock;
35-
const mockBuildChallenge = buildChallenge as Mock;
39+
const mockCurrentBlockIdsSelector = vi.mocked(currentBlockIdsSelector);
40+
const mockCompletedChallengesIdsSelector =
41+
completedChallengesIdsSelector as unknown as Mock;
42+
const mockAllChallengesInfoSelector =
43+
allChallengesInfoSelector as unknown as Mock;
3644
const mockGetTestRunner = getTestRunner as Mock;
3745
mockBuildEnabledSelector.mockReturnValue(true);
3846
mockChallengeTestsSelector.mockReturnValue([
@@ -44,36 +52,75 @@ mockChallengeMetaSelector.mockReturnValue({
4452
mockChallengeDataSelector.mockReturnValue({
4553
challengeFiles: ['mock_challenge_files']
4654
});
47-
mockBuildChallenge.mockReturnValue({ challengeType: 'mock_challenge_type' });
4855
mockGetTestRunner.mockReturnValue(mockTestRunner);
4956

50-
const completedChallengesIds = ['1', '3', '5'],
51-
currentBlockIds = ['1', '3', '5', '7'],
52-
id = '7',
53-
fakeCompletedChallengesIds = ['1', '3', '5', '7', '8'];
57+
const completedChallengesIds = ['1', '3', '5'];
58+
const currentBlockIds = ['1', '3', '5', '7'];
59+
const id = '7';
60+
const fakeCompletedChallengesIds = ['1', '3', '5', '7', '8'];
5461

5562
describe('<CompletionModal />', () => {
5663
describe('fireConfetti', () => {
5764
beforeEach(() => {
5865
mockFireConfetti.mockClear();
5966
});
60-
test('should fire when block is completed', async () => {
67+
test('should fire when block is completed and challenge data exists', async () => {
6168
const payload = { showCompletionModal: true };
69+
const challengeId = 'bd7158d8c442eddfaeb5bd18';
70+
const blockIds = ['step1', 'step2', 'step3', challengeId];
6271
const store = createStore({
6372
challenge: {
6473
modal: { completion: true },
6574
challengeMeta: {
66-
id: 'bd7158d8c442eddfaeb5bd18',
67-
certification: 'responsive-web-design' // Make sure the certification matches
75+
id: challengeId,
76+
certification: 'responsive-web-design'
6877
}
6978
}
7079
});
7180
mockIsBlockNewlyCompletedSelector.mockReturnValue(true);
81+
mockChallengeMetaSelector.mockReturnValue({
82+
id: challengeId,
83+
isLastChallengeInBlock: true,
84+
challengeType: 'mock_challenge_type'
85+
});
86+
mockCurrentBlockIdsSelector.mockReturnValue(blockIds);
87+
mockCompletedChallengesIdsSelector.mockReturnValue(['step1', 'step2']);
88+
mockAllChallengesInfoSelector.mockReturnValue({
89+
challengeNodes: [{ challenge: { id: challengeId } }],
90+
certificateNodes: []
91+
});
7292
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
7393
// @ts-ignore
7494
await runSaga(store, executeChallengeSaga, { payload }).done;
7595
expect(mockFireConfetti).toHaveBeenCalledTimes(1);
7696
});
97+
test('should not fire when challenge data is empty (saga guard)', async () => {
98+
const payload = { showCompletionModal: true };
99+
const challengeId = 'bd7158d8c442eddfaeb5bd18';
100+
const store = createStore({
101+
challenge: {
102+
modal: { completion: true },
103+
challengeMeta: {
104+
id: challengeId,
105+
certification: 'responsive-web-design'
106+
}
107+
}
108+
});
109+
mockIsBlockNewlyCompletedSelector.mockReturnValue(true);
110+
mockChallengeMetaSelector.mockReturnValue({
111+
id: challengeId,
112+
isLastChallengeInBlock: true,
113+
challengeType: 'mock_challenge_type'
114+
});
115+
mockAllChallengesInfoSelector.mockReturnValue({
116+
challengeNodes: [],
117+
certificateNodes: []
118+
});
119+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
120+
// @ts-ignore
121+
await runSaga(store, executeChallengeSaga, { payload }).done;
122+
expect(mockFireConfetti).toHaveBeenCalledTimes(0);
123+
});
77124
test('should not fire when block is not completed', async () => {
78125
const payload = { showCompletionModal: true };
79126
const store = createStore({

client/src/templates/Challenges/redux/execute-challenge-saga.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ import {
5454
updateLogs,
5555
updateTests
5656
} from './actions';
57+
import { allChallengesInfoSelector } from '../../../redux/selectors';
5758
import {
5859
challengeDataSelector,
5960
challengeMetaSelector,
@@ -141,7 +142,8 @@ export function* executeChallengeSaga({ payload }) {
141142
const isBlockCompleted = yield select(isBlockNewlyCompletedSelector);
142143
if (challengeComplete) {
143144
playTone('tests-completed');
144-
if (isBlockCompleted) {
145+
const allChallengesInfo = yield select(allChallengesInfoSelector);
146+
if (isBlockCompleted && allChallengesInfo?.challengeNodes?.length) {
145147
fireConfetti();
146148
}
147149
} else {

0 commit comments

Comments
 (0)