fix(external curricula): use strings for superblock stages (#60813)

This commit is contained in:
Huyen Nguyen
2025-06-11 14:51:23 +07:00
committed by GitHub
parent b9ee08ac58
commit 66bc604bd7
2 changed files with 53 additions and 18 deletions
@@ -20,7 +20,8 @@ import {
type GeneratedBlockBasedCurriculumProps, type GeneratedBlockBasedCurriculumProps,
type GeneratedChapterBasedCurriculumProps, type GeneratedChapterBasedCurriculumProps,
type ChapterBasedCurriculumIntros, type ChapterBasedCurriculumIntros,
orderedSuperBlockInfo orderedSuperBlockInfo,
OrderedSuperBlocks
} from './build-external-curricula-data-v2'; } from './build-external-curricula-data-v2';
const VERSION = 'v2'; const VERSION = 'v2';
@@ -55,16 +56,29 @@ describe('external curriculum data build', () => {
}); });
test('the available-superblocks file should have the correct structure', async () => { test('the available-superblocks file should have the correct structure', async () => {
const filteredSuperBlockStages: string[] = Object.keys(SuperBlockStage)
.filter(key => isNaN(Number(key))) // Filter out numeric keys to get only the names
.filter(name => name !== 'Upcoming' && name !== 'Next') // Filter out 'Upcoming' and 'Next'
.map(name => name.toLowerCase());
const validateAvailableSuperBlocks = availableSuperBlocksValidator(); const validateAvailableSuperBlocks = availableSuperBlocksValidator();
const availableSuperblocks: unknown = JSON.parse( const availableSuperblocks = JSON.parse(
await fs.promises.readFile( await fs.promises.readFile(
`${clientStaticPath}/curriculum-data/${VERSION}/available-superblocks.json`, `${clientStaticPath}/curriculum-data/${VERSION}/available-superblocks.json`,
'utf-8' 'utf-8'
) )
); ) as { superblocks: OrderedSuperBlocks };
const result = validateAvailableSuperBlocks(availableSuperblocks); const result = validateAvailableSuperBlocks(availableSuperblocks);
expect(Object.keys(availableSuperblocks.superblocks)).toHaveLength(
filteredSuperBlockStages.length
);
expect(Object.keys(availableSuperblocks.superblocks)).toEqual(
expect.arrayContaining(filteredSuperBlockStages)
);
if (result.error) { if (result.error) {
throw Error( throw Error(
`file: available-superblocks.json `file: available-superblocks.json
@@ -263,23 +277,35 @@ ${result.error.message}`);
}); });
test('All public SuperBlocks should be present in the SuperBlock object', () => { test('All public SuperBlocks should be present in the SuperBlock object', () => {
const stages = Object.keys(orderedSuperBlockInfo).map( // Create a mapping from string to shared/config SuperBlockStage enum value
key => Number(key) as SuperBlockStage // so we can look up the enum value by string.
); const superBlockStageStringMap: Record<string, SuperBlockStage> = {
core: SuperBlockStage.Core,
english: SuperBlockStage.English,
professional: SuperBlockStage.Professional,
extra: SuperBlockStage.Extra,
legacy: SuperBlockStage.Legacy,
upcoming: SuperBlockStage.Upcoming,
next: SuperBlockStage.Next
};
expect(stages).not.toContain(SuperBlockStage.Next); const stages = Object.keys(orderedSuperBlockInfo);
expect(stages).not.toContain(SuperBlockStage.Upcoming);
expect(stages).not.toContain('next');
expect(stages).not.toContain('upcoming');
for (const stage of stages) { for (const stage of stages) {
const superBlockDashedNames = orderedSuperBlockInfo[stage].map( const superBlockDashedNames = orderedSuperBlockInfo[stage].map(
superBlock => superBlock.dashedName superBlock => superBlock.dashedName
); );
const stageValueInNum = superBlockStageStringMap[stage];
expect(superBlockDashedNames).toEqual( expect(superBlockDashedNames).toEqual(
expect.arrayContaining(superBlockStages[stage]) expect.arrayContaining(superBlockStages[stageValueInNum])
); );
expect(superBlockDashedNames).toHaveLength( expect(superBlockDashedNames).toHaveLength(
superBlockStages[stage].length superBlockStages[stageValueInNum].length
); );
} }
}); });
@@ -2,10 +2,7 @@ import { mkdirSync, writeFileSync, readFileSync } from 'fs';
import { resolve, dirname } from 'path'; import { resolve, dirname } from 'path';
import { submitTypes } from '../../../shared/config/challenge-types'; import { submitTypes } from '../../../shared/config/challenge-types';
import { type ChallengeNode } from '../../../client/src/redux/prop-types'; import { type ChallengeNode } from '../../../client/src/redux/prop-types';
import { import { SuperBlocks } from '../../../shared/config/curriculum';
SuperBlocks,
SuperBlockStage
} from '../../../shared/config/curriculum';
import fullStackSuperBlockStructure from '../../../curriculum/superblock-structure/full-stack.json'; import fullStackSuperBlockStructure from '../../../curriculum/superblock-structure/full-stack.json';
import type { Chapter } from '../../../shared/config/chapters'; import type { Chapter } from '../../../shared/config/chapters';
@@ -85,6 +82,21 @@ interface GeneratedBlock {
meta: Record<string, unknown>; meta: Record<string, unknown>;
} }
// This enum is based on the `SuperBlockStage` enum in shared/config,
// but with string value instead of number.
enum SuperBlockStage {
Core = 'core',
English = 'english',
Professional = 'professional',
Extra = 'extra',
Legacy = 'legacy'
}
export type OrderedSuperBlocks = Record<
string,
Array<{ dashedName: SuperBlocks; public: boolean; title: string }>
>;
const ver = 'v2'; const ver = 'v2';
const staticFolderPath = resolve(__dirname, '../../../client/static'); const staticFolderPath = resolve(__dirname, '../../../client/static');
@@ -97,10 +109,7 @@ const intros = JSON.parse(
readFileSync(blockIntroPath, 'utf-8') readFileSync(blockIntroPath, 'utf-8')
) as CurriculumIntros; ) as CurriculumIntros;
export const orderedSuperBlockInfo: Record< export const orderedSuperBlockInfo: OrderedSuperBlocks = {
string,
Array<{ dashedName: SuperBlocks; public: boolean; title: string }>
> = {
[SuperBlockStage.Core]: [ [SuperBlockStage.Core]: [
{ {
dashedName: SuperBlocks.FullStackDeveloper, dashedName: SuperBlocks.FullStackDeveloper,