mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-05-28 18:26:54 +00:00
refactor: modularize curriculum tooling (#63623)
This commit is contained in:
committed by
GitHub
parent
74dd292cc1
commit
f8dbb50b7e
@@ -19,7 +19,7 @@ const {
|
||||
const {
|
||||
transformSuperBlock
|
||||
} = 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';
|
||||
|
||||
|
||||
@@ -14,12 +14,8 @@ import {
|
||||
} from './build-superblock.js';
|
||||
|
||||
import { buildCertification } from './build-certification.js';
|
||||
import {
|
||||
applyFilters,
|
||||
closestFilters,
|
||||
Filter,
|
||||
getSuperOrder
|
||||
} from './utils.js';
|
||||
import { getSuperOrder } from './super-order.js';
|
||||
import { applyFilters, closestFilters, type Filter } from './filter.js';
|
||||
import {
|
||||
getContentDir,
|
||||
getLanguageConfig,
|
||||
@@ -30,6 +26,7 @@ import {
|
||||
getBlockStructureDir,
|
||||
type BlockStructure
|
||||
} from './file-handler.js';
|
||||
import { SHOW_UPCOMING_CHANGES } from './config.js';
|
||||
const log = debug('fcc:build-curriculum');
|
||||
|
||||
/**
|
||||
@@ -225,7 +222,7 @@ export const superBlockToFilename = Object.entries(superBlockNames).reduce(
|
||||
*/
|
||||
export function addSuperblockStructure(
|
||||
superBlockFilenames: string[],
|
||||
showComingSoon = process.env.SHOW_UPCOMING_CHANGES === 'true'
|
||||
showComingSoon = SHOW_UPCOMING_CHANGES
|
||||
) {
|
||||
log(`Building structure for ${superBlockFilenames.length} superblocks`);
|
||||
|
||||
|
||||
@@ -13,12 +13,13 @@ import {
|
||||
import { SuperBlocks } from '../../shared-dist/config/curriculum';
|
||||
import type { Chapter } from '../../shared-dist/config/chapters';
|
||||
import { Certification } from '../../shared-dist/config/certification-settings';
|
||||
import { getSuperOrder } from './utils.js';
|
||||
import { getSuperOrder } from './super-order.js';
|
||||
import type {
|
||||
BlockStructure,
|
||||
Challenge,
|
||||
ChallengeFile
|
||||
} from './file-handler.js';
|
||||
import { SHOW_UPCOMING_CHANGES } from './config';
|
||||
|
||||
const log = debug('fcc:build-superblock');
|
||||
|
||||
@@ -372,10 +373,7 @@ export class BlockCreator {
|
||||
throw Error(`Block directory not found: ${blockContentDir}`);
|
||||
}
|
||||
|
||||
if (
|
||||
block.isUpcomingChange &&
|
||||
process.env.SHOW_UPCOMING_CHANGES !== 'true'
|
||||
) {
|
||||
if (block.isUpcomingChange && !SHOW_UPCOMING_CHANGES) {
|
||||
log(`Ignoring upcoming block ${blockName}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
@@ -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 { 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.
|
||||
@@ -1,6 +1,6 @@
|
||||
var glob = require('glob');
|
||||
const lint = require('../../tools/scripts/lint');
|
||||
const { testedLang } = require('./utils');
|
||||
const { testedLang } = require('./config');
|
||||
|
||||
glob(`challenges/${testedLang()}/**/*.md`, (err, files) => {
|
||||
if (!files.length) throw Error('No files found');
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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];
|
||||
}
|
||||
@@ -4,7 +4,7 @@ vi.stubEnv('SHOW_UPCOMING_CHANGES', 'true');
|
||||
|
||||
// We need to use dynamic imports here to ensure the environment variable is set
|
||||
// before the module is loaded.
|
||||
const { testedLang } = await import('../utils.js');
|
||||
const { testedLang } = await import('../config.js');
|
||||
const { getChallenges } = await import('./test-challenges.js');
|
||||
|
||||
describe('Daily Coding Challenges', async () => {
|
||||
|
||||
@@ -15,11 +15,11 @@ import { prefixDoctype } from '../../../client/src/templates/Challenges/utils/fr
|
||||
|
||||
import { getChallengesForLang } from '../get-challenges.js';
|
||||
import { challengeSchemaValidator } from '../../schema/challenge-schema.js';
|
||||
import { testedLang } from '../utils.js';
|
||||
|
||||
import { curriculumSchemaValidator } from '../../schema/curriculum-schema.js';
|
||||
import { validateMetaSchema } from '../../schema/meta-schema.js';
|
||||
import { getBlockStructure } from '../file-handler.js';
|
||||
import { FCC_CHALLENGE_ID, testedLang } from '../config.js';
|
||||
import ChallengeTitles from './utils/challenge-titles.js';
|
||||
import MongoIds from './utils/mongo-ids.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.
|
||||
// Instead seed from next challenge is tested against tests from
|
||||
// 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;
|
||||
// TODO: once certifications are not included in the list of challenges,
|
||||
|
||||
@@ -4,7 +4,8 @@ import path from 'node:path';
|
||||
import _ from 'lodash';
|
||||
|
||||
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;
|
||||
|
||||
@@ -16,16 +17,6 @@ if (typeof __dirname !== 'undefined') {
|
||||
__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');
|
||||
|
||||
async function main() {
|
||||
@@ -33,7 +24,8 @@ async function main() {
|
||||
await fs.promises.rm(GENERATED_DIR, { force: true, recursive: true });
|
||||
await fs.promises.mkdir(GENERATED_DIR, { recursive: true });
|
||||
|
||||
const { fullSuperblockList } = await parseCurriculumStructure(testFilter);
|
||||
const { fullSuperblockList } =
|
||||
await parseCurriculumStructure(curriculumFilter);
|
||||
|
||||
const blocks = _.uniq(
|
||||
fullSuperblockList.flatMap(({ blocks }) => blocks).map(b => b.dashedName)
|
||||
@@ -41,17 +33,17 @@ async function main() {
|
||||
|
||||
for (const block of blocks) {
|
||||
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');
|
||||
}
|
||||
|
||||
console.log(`Generated ${blocks.length} block test file(s).`);
|
||||
}
|
||||
|
||||
function generateSingleBlockFile(testFilter: Filter) {
|
||||
function generateSingleBlockFile(filter: Filter) {
|
||||
return `import { defineTestsForBlock } from '../test-challenges.js';
|
||||
|
||||
await defineTestsForBlock(${JSON.stringify(testFilter)});
|
||||
await defineTestsForBlock(${JSON.stringify(filter)});
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -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'
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user