From 11cc102d9df770af26254280c8ead6e15bf41ad9 Mon Sep 17 00:00:00 2001 From: Mahima Thacker <81761152+mahimathacker@users.noreply.github.com> Date: Fri, 12 Jun 2026 12:01:46 +0530 Subject: [PATCH] fix(client): show correct lab progress on resubmission (#67894) --- .../utils/get-completion-percentage.test.ts | 50 +++++++++++++++++++ client/src/utils/get-completion-percentage.ts | 6 ++- packages/shared/src/config/challenge-types.ts | 9 ++++ 3 files changed, 64 insertions(+), 1 deletion(-) diff --git a/client/src/utils/get-completion-percentage.test.ts b/client/src/utils/get-completion-percentage.test.ts index cd6520a165d..194b4d6ab43 100644 --- a/client/src/utils/get-completion-percentage.test.ts +++ b/client/src/utils/get-completion-percentage.test.ts @@ -84,6 +84,22 @@ describe('get-completion-percentage', () => { expect(result).toBe(33); }); + + it('reports 100% when resubmitting an already-completed single-challenge lab block', () => { + // Regression test for #67867: a completed lab whose block contains only + // the lab itself should read 100%, not 0%, on resubmission. + const labId = 'lab-challenge'; + const completedChallengesIds = [labId]; + const currentBlockIds = [labId]; + + const result = getCompletedPercentage( + completedChallengesIds, + currentBlockIds, + labId + ); + + expect(result).toBe(100); + }); }); describe('getCompletedChallengesInBlock', () => { @@ -251,6 +267,40 @@ describe('get-completion-percentage', () => { expect(result).toEqual(['project-1', 'project-2']); }); + // Regression test for #67867: labs are project-based but each is its own + // standalone block, so they must use their block IDs rather than the + // certification's tests (otherwise resubmitting a completed lab reads 0%). + it('returns block IDs for labs even when a certificate is available', () => { + const allChallengesInfo: AllChallengesInfo = { + challengeNodes: [ + { + challenge: { + id: 'lab-challenge', + block: 'lab-all-true-property-validator', + certification: Certification.JsV9 + } + } as Partial as ChallengeNode + ], + certificateNodes: [ + { + challenge: { + certification: Certification.JsV9, + tests: [{ id: 'javascript-certification-exam' }] + } + } + ] + }; + + const result = getCurrentBlockIds( + allChallengesInfo, + 'lab-all-true-property-validator', + Certification.JsV9, + challengeTypes.jsLab + ); + + expect(result).toEqual(['lab-challenge']); + }); + it('returns empty array when no matching challenges found', () => { const allChallengesInfo: AllChallengesInfo = { challengeNodes: [ diff --git a/client/src/utils/get-completion-percentage.ts b/client/src/utils/get-completion-percentage.ts index a9a8e47d534..4027e58a606 100644 --- a/client/src/utils/get-completion-percentage.ts +++ b/client/src/utils/get-completion-percentage.ts @@ -1,4 +1,5 @@ import { type Certification } from '@freecodecamp/shared/config/certification-settings'; +import { getIsLabChallenge } from '@freecodecamp/shared/config/challenge-types'; import { AllChallengesInfo } from '../redux/prop-types'; import { isProjectBased } from './curriculum-layout'; @@ -53,7 +54,10 @@ export const getCurrentBlockIds = ( ) .map(node => node.challenge.id); - if (isProjectBased(challengeType)) { + // Labs are project-based, but each lab is its own standalone block rather than + // part of a certification's project list. Excluding them keeps the progress bar + // accurate (otherwise an already-completed lab reads 0% on resubmit). See #67867. + if (isProjectBased(challengeType) && !getIsLabChallenge(challengeType)) { return currentCertificateIds.length > 0 ? currentCertificateIds : currentBlockIds; diff --git a/packages/shared/src/config/challenge-types.ts b/packages/shared/src/config/challenge-types.ts index 6ba66a0c692..e3269786e20 100644 --- a/packages/shared/src/config/challenge-types.ts +++ b/packages/shared/src/config/challenge-types.ts @@ -178,6 +178,15 @@ const dailyCodingChallengeTypes = [ export const getIsDailyCodingChallenge = (challengeType: number): boolean => dailyCodingChallengeTypes.includes(challengeType); +const labChallengeTypes = [ + challengeTypes.lab, + challengeTypes.jsLab, + challengeTypes.pyLab +]; + +export const getIsLabChallenge = (challengeType: number): boolean => + labChallengeTypes.includes(challengeType); + const dailyCodingChallengeLanguages = { [challengeTypes.dailyChallengeJs]: 'javascript', [challengeTypes.dailyChallengePy]: 'python'