refactor: modularize curriculum tooling (#63623)

This commit is contained in:
Oliver Eyton-Williams
2025-11-07 16:12:37 +01:00
committed by GitHub
parent 74dd292cc1
commit f8dbb50b7e
13 changed files with 410 additions and 414 deletions
+1 -1
View File
@@ -19,7 +19,7 @@ const {
const { const {
transformSuperBlock transformSuperBlock
} = require('../../curriculum/dist/build-superblock.js'); } = require('../../curriculum/dist/build-superblock.js');
const { getSuperOrder } = require('../../curriculum/dist/utils.js'); const { getSuperOrder } = require('../../curriculum/dist/config.js');
const curriculumLocale = process.env.CURRICULUM_LOCALE || 'english'; const curriculumLocale = process.env.CURRICULUM_LOCALE || 'english';
+4 -7
View File
@@ -14,12 +14,8 @@ import {
} from './build-superblock.js'; } from './build-superblock.js';
import { buildCertification } from './build-certification.js'; import { buildCertification } from './build-certification.js';
import { import { getSuperOrder } from './super-order.js';
applyFilters, import { applyFilters, closestFilters, type Filter } from './filter.js';
closestFilters,
Filter,
getSuperOrder
} from './utils.js';
import { import {
getContentDir, getContentDir,
getLanguageConfig, getLanguageConfig,
@@ -30,6 +26,7 @@ import {
getBlockStructureDir, getBlockStructureDir,
type BlockStructure type BlockStructure
} from './file-handler.js'; } from './file-handler.js';
import { SHOW_UPCOMING_CHANGES } from './config.js';
const log = debug('fcc:build-curriculum'); const log = debug('fcc:build-curriculum');
/** /**
@@ -225,7 +222,7 @@ export const superBlockToFilename = Object.entries(superBlockNames).reduce(
*/ */
export function addSuperblockStructure( export function addSuperblockStructure(
superBlockFilenames: string[], superBlockFilenames: string[],
showComingSoon = process.env.SHOW_UPCOMING_CHANGES === 'true' showComingSoon = SHOW_UPCOMING_CHANGES
) { ) {
log(`Building structure for ${superBlockFilenames.length} superblocks`); log(`Building structure for ${superBlockFilenames.length} superblocks`);
+3 -5
View File
@@ -13,12 +13,13 @@ import {
import { SuperBlocks } from '../../shared-dist/config/curriculum'; import { SuperBlocks } from '../../shared-dist/config/curriculum';
import type { Chapter } from '../../shared-dist/config/chapters'; import type { Chapter } from '../../shared-dist/config/chapters';
import { Certification } from '../../shared-dist/config/certification-settings'; import { Certification } from '../../shared-dist/config/certification-settings';
import { getSuperOrder } from './utils.js'; import { getSuperOrder } from './super-order.js';
import type { import type {
BlockStructure, BlockStructure,
Challenge, Challenge,
ChallengeFile ChallengeFile
} from './file-handler.js'; } from './file-handler.js';
import { SHOW_UPCOMING_CHANGES } from './config';
const log = debug('fcc:build-superblock'); const log = debug('fcc:build-superblock');
@@ -372,10 +373,7 @@ export class BlockCreator {
throw Error(`Block directory not found: ${blockContentDir}`); throw Error(`Block directory not found: ${blockContentDir}`);
} }
if ( if (block.isUpcomingChange && !SHOW_UPCOMING_CHANGES) {
block.isUpcomingChange &&
process.env.SHOW_UPCOMING_CHANGES !== 'true'
) {
log(`Ignoring upcoming block ${blockName}`); log(`Ignoring upcoming block ${blockName}`);
return null; return null;
} }
+37
View File
@@ -0,0 +1,37 @@
import { resolve } from 'path';
import { config } from 'dotenv';
import { availableLangs } from '../../shared-dist/config/i18n.js';
config({ path: resolve(__dirname, '../../.env') });
const curriculumLangs = availableLangs.curriculum;
// checks that the CURRICULUM_LOCALE exists and is an available language
export function testedLang() {
if (process.env.CURRICULUM_LOCALE) {
if (curriculumLangs.includes(process.env.CURRICULUM_LOCALE)) {
return process.env.CURRICULUM_LOCALE;
} else {
throw Error(`${process.env.CURRICULUM_LOCALE} is not a supported language.
Before the site can be built, this language needs to be manually approved`);
}
} else {
throw Error('LOCALE must be set for testing');
}
}
export const SHOW_UPCOMING_CHANGES =
process.env.SHOW_UPCOMING_CHANGES === 'true';
export const FCC_CHALLENGE_ID = process.env.FCC_CHALLENGE_ID
? process.env.FCC_CHALLENGE_ID.trim()
: undefined;
export const curriculumFilter = {
block: process.env.FCC_BLOCK ? process.env.FCC_BLOCK.trim() : undefined,
challengeId: FCC_CHALLENGE_ID,
superBlock: process.env.FCC_SUPERBLOCK
? process.env.FCC_SUPERBLOCK.trim()
: undefined
};
+244
View File
@@ -0,0 +1,244 @@
import { describe, it, expect } from 'vitest';
import {
closestFilters,
closestMatch,
filterByBlock,
filterByChallengeId,
filterBySuperblock
} from './filter';
describe('filterByChallengeId', () => {
it('returns the same superblocks if no challengeId is provided', () => {
const superblocks = [
{
name: 'superblock-1',
blocks: [{ dashedName: 'block-1', challengeOrder: [{ id: '1' }] }]
},
{
name: 'superblock-2',
blocks: [{ dashedName: 'block-2', challengeOrder: [{ id: '2' }] }]
}
];
expect(filterByChallengeId(superblocks)).toEqual(superblocks);
});
it('ignores blocks without the specified challengeId', () => {
const superblocks = [
{
name: 'superblock-1',
blocks: [
{ dashedName: 'block-1', challengeOrder: [{ id: '1' }] },
{ dashedName: 'block-2', challengeOrder: [{ id: '2' }] }
]
}
];
const filtered = filterByChallengeId(superblocks, { challengeId: '2' });
expect(filtered).toEqual([
{
name: 'superblock-1',
blocks: [{ dashedName: 'block-2', challengeOrder: [{ id: '2' }] }]
}
]);
});
it('returns only the specified challenge and its solution challenge', () => {
const superblocks = [
{
name: 'superblock-1',
blocks: [
{
dashedName: 'block-1',
challengeOrder: [{ id: '1' }, { id: '2' }, { id: '3' }]
},
{ dashedName: 'block-2', challengeOrder: [{ id: '4' }] }
]
}
];
const filtered = filterByChallengeId(superblocks, { challengeId: '1' });
expect(filtered).toEqual([
{
name: 'superblock-1',
blocks: [
{
dashedName: 'block-1',
challengeOrder: [{ id: '1' }, { id: '2' }]
}
]
}
]);
});
it('returns only superblocks containing the specified challenge', () => {
const superblocks = [
{
name: 'superblock-1',
blocks: [
{ dashedName: 'block-1', challengeOrder: [{ id: '1' }] },
{ dashedName: 'block-2', challengeOrder: [{ id: '2' }] }
]
},
{
name: 'superblock-2',
blocks: [{ dashedName: 'block-3', challengeOrder: [{ id: '3' }] }]
}
];
const filtered = filterByChallengeId(superblocks, { challengeId: '2' });
expect(filtered).toEqual([
{
name: 'superblock-1',
blocks: [{ dashedName: 'block-2', challengeOrder: [{ id: '2' }] }]
}
]);
});
});
describe('filterByBlock', () => {
it('returns the same superblocks if no block is provided', () => {
const superblocks = [
{
name: 'superblock-1',
blocks: [{ dashedName: 'block-1' }, { dashedName: 'block-2' }]
}
];
expect(filterByBlock(superblocks)).toEqual(superblocks);
});
it('returns only the specified block', () => {
const superblocks = [
{
name: 'superblock-1',
blocks: [{ dashedName: 'block-1' }, { dashedName: 'block-2' }]
}
];
const filtered = filterByBlock(superblocks, { block: 'block-1' });
expect(filtered).toEqual([
{
name: 'superblock-1',
blocks: [{ dashedName: 'block-1' }]
}
]);
});
it('returns an empty array if no blocks match the specified block', () => {
const superblocks = [
{
name: 'superblock-1',
blocks: [{ dashedName: 'block-1' }, { dashedName: 'block-2' }]
}
];
const filtered = filterByBlock(superblocks, {
block: 'nonexistent-block'
});
expect(filtered).toEqual([]);
});
});
describe('filterBySuperblock', () => {
it('returns the same superblocks if no superBlock is provided', () => {
const superblocks = [
{
name: 'superblock-1',
blocks: [{ dashedName: 'block-1' }, { dashedName: 'block-2' }]
}
];
expect(filterBySuperblock(superblocks)).toEqual(superblocks);
});
it('returns only the specified superblock', () => {
const superblocks = [
{
name: 'superblock-1',
blocks: [{ dashedName: 'block-1' }, { dashedName: 'block-2' }]
},
{
name: 'superblock-2',
blocks: [{ dashedName: 'block-3' }]
}
];
const filtered = filterBySuperblock(superblocks, {
superBlock: 'superblock-1'
});
expect(filtered).toEqual([
{
name: 'superblock-1',
blocks: [{ dashedName: 'block-1' }, { dashedName: 'block-2' }]
}
]);
});
});
describe('closestMatch', () => {
it('returns the closest matching element', () => {
const items = [
'responsive-web-design',
'javascript-algorithms-and-data-structures',
'front-end-development-libraries',
'data-visualization'
];
const input = 'responsiv web design';
const closest = 'responsive-web-design';
expect(closestMatch(input, items)).toBe(closest);
});
it('ignores case when finding the closest match', () => {
const items = [
'responsive-web-design',
'ReSPonSivE-WeB-DeSiGne',
'javascript-algorithms-and-data-structures',
'front-end-development-libraries',
'data-visualization'
];
const input = 'ReSPonSiv WeB DeSiGn';
const closest = 'responsive-web-design';
expect(closestMatch(input, items)).toBe(closest);
});
});
describe('closestFilters', () => {
it('returns the closest matching superblock filter', () => {
const superblocks = [
{
name: 'responsive-web-design',
blocks: [
{ dashedName: 'basic-html-and-html5', challengeOrder: [] },
{ dashedName: 'css-flexbox', challengeOrder: [] }
]
},
{
name: 'javascript-algorithms-and-data-structures',
blocks: [
{ dashedName: 'basic-javascript', challengeOrder: [] },
{ dashedName: 'es6', challengeOrder: [] }
]
}
];
expect(
closestFilters(superblocks, { superBlock: 'responsiv web design' })
).toEqual({ superBlock: 'responsive-web-design' });
});
it('returns the closest matching block filter', () => {
const superblocks = [
{
name: 'responsive-web-design',
blocks: [
{ dashedName: 'basic-html-and-html5', challengeOrder: [] },
{ dashedName: 'css-flexbox', challengeOrder: [] }
]
},
{
name: 'javascript-algorithms-and-data-structures',
blocks: [
{ dashedName: 'basic-javascript', challengeOrder: [] },
{ dashedName: 'es6', challengeOrder: [] }
]
}
];
expect(closestFilters(superblocks, { block: 'basic-javascr' })).toEqual({
block: 'basic-javascript'
});
});
});
@@ -1,51 +1,4 @@
import { resolve } from 'path';
import comparison from 'string-similarity'; import comparison from 'string-similarity';
import { config } from 'dotenv';
import { generateSuperBlockList } from '../../shared-dist/config/curriculum.js';
config({ path: resolve(__dirname, '../../.env') });
import { availableLangs } from '../../shared-dist/config/i18n.js';
const curriculumLangs = availableLangs.curriculum;
// checks that the CURRICULUM_LOCALE exists and is an available language
export function testedLang() {
if (process.env.CURRICULUM_LOCALE) {
if (curriculumLangs.includes(process.env.CURRICULUM_LOCALE)) {
return process.env.CURRICULUM_LOCALE;
} else {
throw Error(`${process.env.CURRICULUM_LOCALE} is not a supported language.
Before the site can be built, this language needs to be manually approved`);
}
} else {
throw Error('LOCALE must be set for testing');
}
}
export function createSuperOrder(superBlocks: string[]) {
const superOrder: { [sb: string]: number } = {};
superBlocks.forEach((superBlock, i) => {
superOrder[superBlock] = i;
});
return superOrder;
}
export function getSuperOrder(
superblock: string,
showUpcomingChanges = process.env.SHOW_UPCOMING_CHANGES === 'true'
) {
const flatSuperBlockMap = generateSuperBlockList({
showUpcomingChanges
});
const superOrder = createSuperOrder(flatSuperBlockMap);
return superOrder[superblock];
}
/** /**
* Filters the superblocks array to include, at most, a single superblock with the specified block. * Filters the superblocks array to include, at most, a single superblock with the specified block.
+1 -1
View File
@@ -1,6 +1,6 @@
var glob = require('glob'); var glob = require('glob');
const lint = require('../../tools/scripts/lint'); const lint = require('../../tools/scripts/lint');
const { testedLang } = require('./utils'); const { testedLang } = require('./config');
glob(`challenges/${testedLang()}/**/*.md`, (err, files) => { glob(`challenges/${testedLang()}/**/*.md`, (err, files) => {
if (!files.length) throw Error('No files found'); if (!files.length) throw Error('No files found');
+86
View File
@@ -0,0 +1,86 @@
import { describe, it, expect } from 'vitest';
import { SuperBlocks } from '../../shared-dist/config/curriculum';
import { createSuperOrder, getSuperOrder } from './super-order.js';
const mockSuperBlocks = [
SuperBlocks.RespWebDesignNew,
SuperBlocks.JsAlgoDataStructNew,
SuperBlocks.FrontEndDevLibs,
SuperBlocks.DataVis,
SuperBlocks.RelationalDb,
SuperBlocks.BackEndDevApis,
SuperBlocks.QualityAssurance,
SuperBlocks.SciCompPy,
SuperBlocks.DataAnalysisPy,
SuperBlocks.InfoSec,
SuperBlocks.MachineLearningPy,
SuperBlocks.CollegeAlgebraPy,
SuperBlocks.FoundationalCSharp,
SuperBlocks.CodingInterviewPrep,
SuperBlocks.ProjectEuler,
SuperBlocks.RespWebDesign,
SuperBlocks.JsAlgoDataStruct,
SuperBlocks.TheOdinProject,
SuperBlocks.FullStackDeveloper
];
const fullSuperOrder = {
[SuperBlocks.RespWebDesignNew]: 0,
[SuperBlocks.JsAlgoDataStructNew]: 1,
[SuperBlocks.FrontEndDevLibs]: 2,
[SuperBlocks.DataVis]: 3,
[SuperBlocks.RelationalDb]: 4,
[SuperBlocks.BackEndDevApis]: 5,
[SuperBlocks.QualityAssurance]: 6,
[SuperBlocks.SciCompPy]: 7,
[SuperBlocks.DataAnalysisPy]: 8,
[SuperBlocks.InfoSec]: 9,
[SuperBlocks.MachineLearningPy]: 10,
[SuperBlocks.CollegeAlgebraPy]: 11,
[SuperBlocks.FoundationalCSharp]: 12,
[SuperBlocks.CodingInterviewPrep]: 13,
[SuperBlocks.ProjectEuler]: 14,
[SuperBlocks.RespWebDesign]: 15,
[SuperBlocks.JsAlgoDataStruct]: 16,
[SuperBlocks.TheOdinProject]: 17,
[SuperBlocks.FullStackDeveloper]: 18
};
describe('createSuperOrder', () => {
const superOrder = createSuperOrder(mockSuperBlocks);
it('should create the correct object given an array of SuperBlocks', () => {
expect(superOrder).toStrictEqual(fullSuperOrder);
});
});
describe('getSuperOrder', () => {
it('returns a number for valid curriculum', () => {
expect(typeof getSuperOrder('responsive-web-design')).toBe('number');
});
it('returns undefined for unknown curriculum', () => {
expect(getSuperOrder('')).toBeUndefined();
expect(getSuperOrder('respansive-wib-desoin')).toBeUndefined();
expect(getSuperOrder('certifications')).toBeUndefined();
});
it('returns numbers for all current curriculum', () => {
const superBlocks = Object.values(SuperBlocks);
const superOrderValues = superBlocks.map(sb => getSuperOrder(sb, true));
const definedValues = superOrderValues.filter(v => typeof v === 'number');
expect(definedValues.length).toBe(superBlocks.length);
});
it('returns unique numbers for all current curriculum', () => {
const superBlocks = Object.values(SuperBlocks);
const superOrderValues = superBlocks.map(sb => getSuperOrder(sb, true));
const uniqueValues = Array.from(new Set(superOrderValues));
expect(uniqueValues.length).toBe(superBlocks.length);
});
});
+24
View File
@@ -0,0 +1,24 @@
import { generateSuperBlockList } from '../../shared-dist/config/curriculum.js';
import { SHOW_UPCOMING_CHANGES } from './config.js';
export function createSuperOrder(superBlocks: string[]) {
const superOrder: { [sb: string]: number } = {};
superBlocks.forEach((superBlock, i) => {
superOrder[superBlock] = i;
});
return superOrder;
}
export function getSuperOrder(
superblock: string,
showUpcomingChanges = SHOW_UPCOMING_CHANGES
) {
const flatSuperBlockMap = generateSuperBlockList({
showUpcomingChanges
});
const superOrder = createSuperOrder(flatSuperBlockMap);
return superOrder[superblock];
}
+1 -1
View File
@@ -4,7 +4,7 @@ vi.stubEnv('SHOW_UPCOMING_CHANGES', 'true');
// We need to use dynamic imports here to ensure the environment variable is set // We need to use dynamic imports here to ensure the environment variable is set
// before the module is loaded. // before the module is loaded.
const { testedLang } = await import('../utils.js'); const { testedLang } = await import('../config.js');
const { getChallenges } = await import('./test-challenges.js'); const { getChallenges } = await import('./test-challenges.js');
describe('Daily Coding Challenges', async () => { describe('Daily Coding Challenges', async () => {
+2 -2
View File
@@ -15,11 +15,11 @@ import { prefixDoctype } from '../../../client/src/templates/Challenges/utils/fr
import { getChallengesForLang } from '../get-challenges.js'; import { getChallengesForLang } from '../get-challenges.js';
import { challengeSchemaValidator } from '../../schema/challenge-schema.js'; import { challengeSchemaValidator } from '../../schema/challenge-schema.js';
import { testedLang } from '../utils.js';
import { curriculumSchemaValidator } from '../../schema/curriculum-schema.js'; import { curriculumSchemaValidator } from '../../schema/curriculum-schema.js';
import { validateMetaSchema } from '../../schema/meta-schema.js'; import { validateMetaSchema } from '../../schema/meta-schema.js';
import { getBlockStructure } from '../file-handler.js'; import { getBlockStructure } from '../file-handler.js';
import { FCC_CHALLENGE_ID, testedLang } from '../config.js';
import ChallengeTitles from './utils/challenge-titles.js'; import ChallengeTitles from './utils/challenge-titles.js';
import MongoIds from './utils/mongo-ids.js'; import MongoIds from './utils/mongo-ids.js';
import createPseudoWorker from './utils/pseudo-worker.js'; import createPseudoWorker from './utils/pseudo-worker.js';
@@ -135,7 +135,7 @@ function populateTestsForLang({ lang, challenges, meta }) {
// challenge to test (current challenge) might not have solution. // challenge to test (current challenge) might not have solution.
// Instead seed from next challenge is tested against tests from // Instead seed from next challenge is tested against tests from
// current challenge. Next challenge is skipped from testing. // current challenge. Next challenge is skipped from testing.
if (process.env.FCC_CHALLENGE_ID && id > 0) return; if (FCC_CHALLENGE_ID && id > 0) return;
const dashedBlockName = challenge.block; const dashedBlockName = challenge.block;
// TODO: once certifications are not included in the list of challenges, // TODO: once certifications are not included in the list of challenges,
@@ -4,7 +4,8 @@ import path from 'node:path';
import _ from 'lodash'; import _ from 'lodash';
import { parseCurriculumStructure } from '../../build-curriculum.js'; import { parseCurriculumStructure } from '../../build-curriculum.js';
import { Filter } from '../../utils.js'; import { type Filter } from '../../filter.js';
import { curriculumFilter } from '../../config.js';
let __dirnameCompat: string; let __dirnameCompat: string;
@@ -16,16 +17,6 @@ if (typeof __dirname !== 'undefined') {
__dirnameCompat = new Function('return import.meta.dirname')() as string; __dirnameCompat = new Function('return import.meta.dirname')() as string;
} }
const testFilter: Filter = {
block: process.env.FCC_BLOCK ? process.env.FCC_BLOCK.trim() : undefined,
challengeId: process.env.FCC_CHALLENGE_ID
? process.env.FCC_CHALLENGE_ID.trim()
: undefined,
superBlock: process.env.FCC_SUPERBLOCK
? process.env.FCC_SUPERBLOCK.trim()
: undefined
};
const GENERATED_DIR = path.resolve(__dirnameCompat, '../blocks-generated'); const GENERATED_DIR = path.resolve(__dirnameCompat, '../blocks-generated');
async function main() { async function main() {
@@ -33,7 +24,8 @@ async function main() {
await fs.promises.rm(GENERATED_DIR, { force: true, recursive: true }); await fs.promises.rm(GENERATED_DIR, { force: true, recursive: true });
await fs.promises.mkdir(GENERATED_DIR, { recursive: true }); await fs.promises.mkdir(GENERATED_DIR, { recursive: true });
const { fullSuperblockList } = await parseCurriculumStructure(testFilter); const { fullSuperblockList } =
await parseCurriculumStructure(curriculumFilter);
const blocks = _.uniq( const blocks = _.uniq(
fullSuperblockList.flatMap(({ blocks }) => blocks).map(b => b.dashedName) fullSuperblockList.flatMap(({ blocks }) => blocks).map(b => b.dashedName)
@@ -41,17 +33,17 @@ async function main() {
for (const block of blocks) { for (const block of blocks) {
const filePath = path.join(GENERATED_DIR, `${block}.test.js`); const filePath = path.join(GENERATED_DIR, `${block}.test.js`);
const contents = generateSingleBlockFile({ ...testFilter, block }); const contents = generateSingleBlockFile({ ...curriculumFilter, block });
await fs.promises.writeFile(filePath, contents, 'utf8'); await fs.promises.writeFile(filePath, contents, 'utf8');
} }
console.log(`Generated ${blocks.length} block test file(s).`); console.log(`Generated ${blocks.length} block test file(s).`);
} }
function generateSingleBlockFile(testFilter: Filter) { function generateSingleBlockFile(filter: Filter) {
return `import { defineTestsForBlock } from '../test-challenges.js'; return `import { defineTestsForBlock } from '../test-challenges.js';
await defineTestsForBlock(${JSON.stringify(testFilter)}); await defineTestsForBlock(${JSON.stringify(filter)});
`; `;
} }
-335
View File
@@ -1,335 +0,0 @@
import path from 'path';
import { config } from 'dotenv';
import { describe, it, expect } from 'vitest';
import { SuperBlocks } from '../../shared-dist/config/curriculum';
import {
closestFilters,
closestMatch,
createSuperOrder,
filterByBlock,
filterByChallengeId,
filterBySuperblock,
getSuperOrder
} from './utils.js';
config({ path: path.resolve(__dirname, '../.env') });
const mockSuperBlocks = [
SuperBlocks.RespWebDesignNew,
SuperBlocks.JsAlgoDataStructNew,
SuperBlocks.FrontEndDevLibs,
SuperBlocks.DataVis,
SuperBlocks.RelationalDb,
SuperBlocks.BackEndDevApis,
SuperBlocks.QualityAssurance,
SuperBlocks.SciCompPy,
SuperBlocks.DataAnalysisPy,
SuperBlocks.InfoSec,
SuperBlocks.MachineLearningPy,
SuperBlocks.CollegeAlgebraPy,
SuperBlocks.FoundationalCSharp,
SuperBlocks.CodingInterviewPrep,
SuperBlocks.ProjectEuler,
SuperBlocks.RespWebDesign,
SuperBlocks.JsAlgoDataStruct,
SuperBlocks.TheOdinProject,
SuperBlocks.FullStackDeveloper
];
const fullSuperOrder = {
[SuperBlocks.RespWebDesignNew]: 0,
[SuperBlocks.JsAlgoDataStructNew]: 1,
[SuperBlocks.FrontEndDevLibs]: 2,
[SuperBlocks.DataVis]: 3,
[SuperBlocks.RelationalDb]: 4,
[SuperBlocks.BackEndDevApis]: 5,
[SuperBlocks.QualityAssurance]: 6,
[SuperBlocks.SciCompPy]: 7,
[SuperBlocks.DataAnalysisPy]: 8,
[SuperBlocks.InfoSec]: 9,
[SuperBlocks.MachineLearningPy]: 10,
[SuperBlocks.CollegeAlgebraPy]: 11,
[SuperBlocks.FoundationalCSharp]: 12,
[SuperBlocks.CodingInterviewPrep]: 13,
[SuperBlocks.ProjectEuler]: 14,
[SuperBlocks.RespWebDesign]: 15,
[SuperBlocks.JsAlgoDataStruct]: 16,
[SuperBlocks.TheOdinProject]: 17,
[SuperBlocks.FullStackDeveloper]: 18
};
describe('createSuperOrder', () => {
const superOrder = createSuperOrder(mockSuperBlocks);
it('should create the correct object given an array of SuperBlocks', () => {
expect(superOrder).toStrictEqual(fullSuperOrder);
});
});
describe('getSuperOrder', () => {
it('returns a number for valid curriculum', () => {
expect(typeof getSuperOrder('responsive-web-design')).toBe('number');
});
it('returns undefined for unknown curriculum', () => {
expect(getSuperOrder('')).toBeUndefined();
expect(getSuperOrder('respansive-wib-desoin')).toBeUndefined();
expect(getSuperOrder('certifications')).toBeUndefined();
});
it('returns numbers for all current curriculum', () => {
const superBlocks = Object.values(SuperBlocks);
const superOrderValues = superBlocks.map(sb => getSuperOrder(sb, true));
const definedValues = superOrderValues.filter(v => typeof v === 'number');
expect(definedValues.length).toBe(superBlocks.length);
});
it('returns unique numbers for all current curriculum', () => {
const superBlocks = Object.values(SuperBlocks);
const superOrderValues = superBlocks.map(sb => getSuperOrder(sb, true));
const uniqueValues = Array.from(new Set(superOrderValues));
expect(uniqueValues.length).toBe(superBlocks.length);
});
});
describe('filter utils', () => {
describe('filterByChallengeId', () => {
it('returns the same superblocks if no challengeId is provided', () => {
const superblocks = [
{
name: 'superblock-1',
blocks: [{ dashedName: 'block-1', challengeOrder: [{ id: '1' }] }]
},
{
name: 'superblock-2',
blocks: [{ dashedName: 'block-2', challengeOrder: [{ id: '2' }] }]
}
];
expect(filterByChallengeId(superblocks)).toEqual(superblocks);
});
it('ignores blocks without the specified challengeId', () => {
const superblocks = [
{
name: 'superblock-1',
blocks: [
{ dashedName: 'block-1', challengeOrder: [{ id: '1' }] },
{ dashedName: 'block-2', challengeOrder: [{ id: '2' }] }
]
}
];
const filtered = filterByChallengeId(superblocks, { challengeId: '2' });
expect(filtered).toEqual([
{
name: 'superblock-1',
blocks: [{ dashedName: 'block-2', challengeOrder: [{ id: '2' }] }]
}
]);
});
it('returns only the specified challenge and its solution challenge', () => {
const superblocks = [
{
name: 'superblock-1',
blocks: [
{
dashedName: 'block-1',
challengeOrder: [{ id: '1' }, { id: '2' }, { id: '3' }]
},
{ dashedName: 'block-2', challengeOrder: [{ id: '4' }] }
]
}
];
const filtered = filterByChallengeId(superblocks, { challengeId: '1' });
expect(filtered).toEqual([
{
name: 'superblock-1',
blocks: [
{
dashedName: 'block-1',
challengeOrder: [{ id: '1' }, { id: '2' }]
}
]
}
]);
});
it('returns only superblocks containing the specified challenge', () => {
const superblocks = [
{
name: 'superblock-1',
blocks: [
{ dashedName: 'block-1', challengeOrder: [{ id: '1' }] },
{ dashedName: 'block-2', challengeOrder: [{ id: '2' }] }
]
},
{
name: 'superblock-2',
blocks: [{ dashedName: 'block-3', challengeOrder: [{ id: '3' }] }]
}
];
const filtered = filterByChallengeId(superblocks, { challengeId: '2' });
expect(filtered).toEqual([
{
name: 'superblock-1',
blocks: [{ dashedName: 'block-2', challengeOrder: [{ id: '2' }] }]
}
]);
});
});
describe('filterByBlock', () => {
it('returns the same superblocks if no block is provided', () => {
const superblocks = [
{
name: 'superblock-1',
blocks: [{ dashedName: 'block-1' }, { dashedName: 'block-2' }]
}
];
expect(filterByBlock(superblocks)).toEqual(superblocks);
});
it('returns only the specified block', () => {
const superblocks = [
{
name: 'superblock-1',
blocks: [{ dashedName: 'block-1' }, { dashedName: 'block-2' }]
}
];
const filtered = filterByBlock(superblocks, { block: 'block-1' });
expect(filtered).toEqual([
{
name: 'superblock-1',
blocks: [{ dashedName: 'block-1' }]
}
]);
});
it('returns an empty array if no blocks match the specified block', () => {
const superblocks = [
{
name: 'superblock-1',
blocks: [{ dashedName: 'block-1' }, { dashedName: 'block-2' }]
}
];
const filtered = filterByBlock(superblocks, {
block: 'nonexistent-block'
});
expect(filtered).toEqual([]);
});
});
describe('filterBySuperblock', () => {
it('returns the same superblocks if no superBlock is provided', () => {
const superblocks = [
{
name: 'superblock-1',
blocks: [{ dashedName: 'block-1' }, { dashedName: 'block-2' }]
}
];
expect(filterBySuperblock(superblocks)).toEqual(superblocks);
});
it('returns only the specified superblock', () => {
const superblocks = [
{
name: 'superblock-1',
blocks: [{ dashedName: 'block-1' }, { dashedName: 'block-2' }]
},
{
name: 'superblock-2',
blocks: [{ dashedName: 'block-3' }]
}
];
const filtered = filterBySuperblock(superblocks, {
superBlock: 'superblock-1'
});
expect(filtered).toEqual([
{
name: 'superblock-1',
blocks: [{ dashedName: 'block-1' }, { dashedName: 'block-2' }]
}
]);
});
});
describe('closestMatch', () => {
it('returns the closest matching element', () => {
const items = [
'responsive-web-design',
'javascript-algorithms-and-data-structures',
'front-end-development-libraries',
'data-visualization'
];
const input = 'responsiv web design';
const closest = 'responsive-web-design';
expect(closestMatch(input, items)).toBe(closest);
});
it('ignores case when finding the closest match', () => {
const items = [
'responsive-web-design',
'ReSPonSivE-WeB-DeSiGne',
'javascript-algorithms-and-data-structures',
'front-end-development-libraries',
'data-visualization'
];
const input = 'ReSPonSiv WeB DeSiGn';
const closest = 'responsive-web-design';
expect(closestMatch(input, items)).toBe(closest);
});
});
describe('closestFilters', () => {
it('returns the closest matching superblock filter', () => {
const superblocks = [
{
name: 'responsive-web-design',
blocks: [
{ dashedName: 'basic-html-and-html5', challengeOrder: [] },
{ dashedName: 'css-flexbox', challengeOrder: [] }
]
},
{
name: 'javascript-algorithms-and-data-structures',
blocks: [
{ dashedName: 'basic-javascript', challengeOrder: [] },
{ dashedName: 'es6', challengeOrder: [] }
]
}
];
expect(
closestFilters(superblocks, { superBlock: 'responsiv web design' })
).toEqual({ superBlock: 'responsive-web-design' });
});
it('returns the closest matching block filter', () => {
const superblocks = [
{
name: 'responsive-web-design',
blocks: [
{ dashedName: 'basic-html-and-html5', challengeOrder: [] },
{ dashedName: 'css-flexbox', challengeOrder: [] }
]
},
{
name: 'javascript-algorithms-and-data-structures',
blocks: [
{ dashedName: 'basic-javascript', challengeOrder: [] },
{ dashedName: 'es6', challengeOrder: [] }
]
}
];
expect(closestFilters(superblocks, { block: 'basic-javascr' })).toEqual({
block: 'basic-javascript'
});
});
});
});