diff --git a/client/utils/build-challenges.js b/client/utils/build-challenges.js index 575e61458e5..7b8073e24cf 100644 --- a/client/utils/build-challenges.js +++ b/client/utils/build-challenges.js @@ -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'; diff --git a/curriculum/src/build-curriculum.ts b/curriculum/src/build-curriculum.ts index 200cd956288..96b26a62b38 100644 --- a/curriculum/src/build-curriculum.ts +++ b/curriculum/src/build-curriculum.ts @@ -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`); diff --git a/curriculum/src/build-superblock.ts b/curriculum/src/build-superblock.ts index 66ac01d1ac3..1ddae448077 100644 --- a/curriculum/src/build-superblock.ts +++ b/curriculum/src/build-superblock.ts @@ -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; } diff --git a/curriculum/src/config.ts b/curriculum/src/config.ts new file mode 100644 index 00000000000..e4e9218a92b --- /dev/null +++ b/curriculum/src/config.ts @@ -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 +}; diff --git a/curriculum/src/filter.test.js b/curriculum/src/filter.test.js new file mode 100644 index 00000000000..50188d58a51 --- /dev/null +++ b/curriculum/src/filter.test.js @@ -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' + }); + }); +}); diff --git a/curriculum/src/utils.ts b/curriculum/src/filter.ts similarity index 79% rename from curriculum/src/utils.ts rename to curriculum/src/filter.ts index 4fc355f1f16..f06a2fe58e4 100644 --- a/curriculum/src/utils.ts +++ b/curriculum/src/filter.ts @@ -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. diff --git a/curriculum/src/lint-localized.js b/curriculum/src/lint-localized.js index d32da7fd5e7..7642bb8a140 100644 --- a/curriculum/src/lint-localized.js +++ b/curriculum/src/lint-localized.js @@ -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'); diff --git a/curriculum/src/super-order.test.ts b/curriculum/src/super-order.test.ts new file mode 100644 index 00000000000..f0e427166ee --- /dev/null +++ b/curriculum/src/super-order.test.ts @@ -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); + }); +}); diff --git a/curriculum/src/super-order.ts b/curriculum/src/super-order.ts new file mode 100644 index 00000000000..2cd40068440 --- /dev/null +++ b/curriculum/src/super-order.ts @@ -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]; +} diff --git a/curriculum/src/test/daily-challenges.test.js b/curriculum/src/test/daily-challenges.test.js index 185f65b0f97..cdc56e5527a 100644 --- a/curriculum/src/test/daily-challenges.test.js +++ b/curriculum/src/test/daily-challenges.test.js @@ -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 () => { diff --git a/curriculum/src/test/test-challenges.js b/curriculum/src/test/test-challenges.js index 050e700661c..1b3d908a15b 100644 --- a/curriculum/src/test/test-challenges.js +++ b/curriculum/src/test/test-challenges.js @@ -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, diff --git a/curriculum/src/test/utils/generate-block-tests.ts b/curriculum/src/test/utils/generate-block-tests.ts index 6899d01ff46..40c441260a2 100644 --- a/curriculum/src/test/utils/generate-block-tests.ts +++ b/curriculum/src/test/utils/generate-block-tests.ts @@ -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)}); `; } diff --git a/curriculum/src/utils.test.ts b/curriculum/src/utils.test.ts deleted file mode 100644 index 21671263067..00000000000 --- a/curriculum/src/utils.test.ts +++ /dev/null @@ -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' - }); - }); - }); -});