Repository: freeCodeCamp/freeCodeCamp
File: client/src/templates/Challenges/redux/execute-challenge-saga.js (JavaScript)

Code before:
    const testRunner = yield call(getTestRunner, { ...buildData, hooks });
    const testResults = yield executeTests(testRunner, tests);
    yield put(updateTests(testResults));

    const challengeComplete = testResults.every(test => test.pass && !test.err);
    const isBlockCompleted = yield select(isBlockNewlyCompletedSelector);
    if (challengeComplete) {
      playTone('tests-completed');
      if (isBlockCompleted) {
        const completedChallengesIds = yield select(
          completedChallengesIdsSelector
        );
        const currentBlockIds = yield select(currentBlockIdsSelector);

        if (challengeMeta.isLastChallengeInBlock) {
          const otherChallenges = currentBlockIds.filter(
            blockId => blockId !== challengeMeta.id
          );
          const allOthersCompleted = otherChallenges.every(blockId =>
            completedChallengesIds.includes(blockId)
          );

          if (!allOthersCompleted) {
            fireConfetti();
          }
        }
      }
    } else {
      playTone('tests-failed');
      if (challengeMeta.certification === 'responsive-web-design') {
        yield call(callGA, {
          event: 'challenge_failed',
          challenge_id: challengeMeta.id,
          challenge_path: window?.location?.pathname,
          challenge_files: challengeData.challengeFiles
        });
      }
    }

    if (challengeComplete && payload?.showCompletionModal) {
      yield put(openModal('completion'));
    }
    yield put(updateConsole(i18next.t('learn.tests-completed')));
    yield put(logsToConsole(i18next.t('learn.console-output')));
  } catch (e) {
    yield put(updateConsole(e));
  } finally {
    yield put(executeChallengeComplete());
    consoleProxy.close();
  }
}

Review comment (bug):
Spent some time looking into the issue.

The main issue is that on initial loads allChallengesInfo is empty.
Since some of the challenges such as the workshops do not have a progress bar (where allChallengesInfo gets pulled), the completion percentage and isBlockNewlyCompletedSelector disfunction.

The most straightforward fix for the confetti would be to check if all challenge data exist first before evaluation.
isBlockNewlyCompletedSelector could also return false if allChallengesInfo is empty.

feel free to use this selector to pull and check the data: allChallengesInfoSelector

Please Add the test for the scenario where the challenge data is empty. Make sure it fails before the fix.

Remove all additional changes.

Code after:
    const testResults = yield executeTests(testRunner, tests);
    yield put(updateTests(testResults));

    const challengeComplete = testResults.every(test => test.pass && !test.err);
    const isBlockCompleted = yield select(isBlockNewlyCompletedSelector);
    if (challengeComplete) {
      playTone('tests-completed');
      const allChallengesInfo = yield select(allChallengesInfoSelector);
      if (isBlockCompleted && allChallengesInfo?.challengeNodes?.length) {
        fireConfetti();
      }
    } else {
      playTone('tests-failed');
      if (challengeMeta.certification === 'responsive-web-design') {
        yield call(callGA, {
          event: 'challenge_failed',
          challenge_id: challengeMeta.id,
          challenge_path: window?.location?.pathname,
          challenge_files: challengeData.challengeFiles
        });
      }
    }

    if (challengeComplete && payload?.showCompletionModal) {
      yield put(openModal('completion'));
    }
    yield put(updateConsole(i18next.t('learn.tests-completed')));
    yield put(logsToConsole(i18next.t('learn.console-output')));
  } catch (e) {
    yield put(updateConsole(e));
  } finally {
    yield put(executeChallengeComplete());
    consoleProxy.close();
  }
}

function* takeEveryConsole(channel) {
  // TODO: move all stringifying and escaping into the reducer so there is a
  // single place responsible for formatting the console output.
  yield takeEvery(channel, function* (args) {
    yield put(updateConsole(escape(args)));
  });
}

function* buildChallengeData(challengeData, options) {
  try {
    return yield call(buildChallenge, challengeData, options);
  } catch (e) {
    yield put(disableBuildOnError());
    throw e;
  }