From 5f73773cd4704eda9b90afd805045b0d9c5ae7de Mon Sep 17 00:00:00 2001 From: Tom <20648924+moT01@users.noreply.github.com> Date: Tue, 8 Nov 2022 09:30:46 -0600 Subject: [PATCH] feat(client): move legacy rwd button to bottom of map (#47596) Co-authored-by: Oliver Eyton-Williams Closes https://github.com/freeCodeCamp/freeCodeCamp/issues/46523 --- .eslintignore | 1 + .gitignore | 2 + .prettierignore | 2 + .../Header/components/nav-links.tsx | 2 +- .../Map/__snapshots__/map.test.tsx.snap | 535 --------------- client/src/components/Map/index.tsx | 213 +++--- client/src/components/Map/map.test.tsx | 27 - .../Introduction/super-block-intro.tsx | 10 +- client/src/utils/superblock-map-titles.ts | 22 + config/i18n.ts | 255 ++----- config/superblock-order.test.ts | 196 ++++++ config/superblock-order.ts | 626 ++++++++++++++++++ curriculum/utils.js | 134 ++-- curriculum/utils.test.ts | 237 ++++--- cypress/e2e/default/landing.js | 3 +- cypress/e2e/default/learn/index.js | 4 +- package.json | 1 + tools/challenge-auditor/index.ts | 9 +- tools/scripts/build/ensure-env.ts | 10 +- utils/is-audited.js | 21 +- 20 files changed, 1236 insertions(+), 1074 deletions(-) delete mode 100644 client/src/components/Map/__snapshots__/map.test.tsx.snap delete mode 100644 client/src/components/Map/map.test.tsx create mode 100644 client/src/utils/superblock-map-titles.ts create mode 100644 config/superblock-order.test.ts create mode 100644 config/superblock-order.ts diff --git a/.eslintignore b/.eslintignore index 55ab6121244..7512bafbeb9 100644 --- a/.eslintignore +++ b/.eslintignore @@ -5,4 +5,5 @@ api-server/src/public/** api-server/lib/** config/i18n.js config/certification-settings.js +config/superblock-order.js web/** diff --git a/.gitignore b/.gitignore index bc796f46f03..b526cf7dc13 100644 --- a/.gitignore +++ b/.gitignore @@ -164,6 +164,8 @@ config/client/test-evaluator.json config/curriculum.json config/i18n.js config/certification-settings.js +config/superblock-order.js +config/superblock-order.test.js ### Generated utils files ### utils/block-nameify.js diff --git a/.prettierignore b/.prettierignore index c2f88809e28..4d01cd25f97 100644 --- a/.prettierignore +++ b/.prettierignore @@ -7,6 +7,8 @@ curriculum/challenges/**/* config/**/*.json config/i18n.js config/certification-settings.js +config/superblock-order.js +config/superblock-order.test.js docs/i18n utils/block-nameify.js utils/block-nameify.test.js diff --git a/client/src/components/Header/components/nav-links.tsx b/client/src/components/Header/components/nav-links.tsx index 2499264872f..dc9bd246338 100644 --- a/client/src/components/Header/components/nav-links.tsx +++ b/client/src/components/Header/components/nav-links.tsx @@ -480,7 +480,7 @@ export class NavLinks extends Component { className='nav-link nav-lang-menu-option' data-value={lang} {...(LangCodes[lang] && { - lang: LangCodes[lang] as string + lang: LangCodes[lang] })} onClick={this.handleLanguageChange} onKeyDown={this.handleLanguageMenuKeyDown} diff --git a/client/src/components/Map/__snapshots__/map.test.tsx.snap b/client/src/components/Map/__snapshots__/map.test.tsx.snap deleted file mode 100644 index aa60c08a26d..00000000000 --- a/client/src/components/Map/__snapshots__/map.test.tsx.snap +++ /dev/null @@ -1,535 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` snapshot: Map 1`] = ` -
- -
-`; diff --git a/client/src/components/Map/index.tsx b/client/src/components/Map/index.tsx index 73799ad1618..a3e0fb8795a 100644 --- a/client/src/components/Map/index.tsx +++ b/client/src/components/Map/index.tsx @@ -1,39 +1,29 @@ -import { graphql, useStaticQuery } from 'gatsby'; import i18next from 'i18next'; import React from 'react'; import { SuperBlocks } from '../../../../config/certification-settings'; +import { + CurriculumMaps, + getAuditedSuperBlocks, + getNotAuditedSuperBlocks, + superBlockOrder +} from '../../../../config/superblock-order'; +import { Languages } from '../../../../config/i18n'; import envData from '../../../../config/env.json'; -import { isAuditedCert } from '../../../../utils/is-audited'; import { generateIconComponent } from '../../assets/icons'; import LinkButton from '../../assets/icons/link-button'; -import { ChallengeNode } from '../../redux/prop-types'; import { Link, Spacer } from '../helpers'; +import { getSuperBlockTitleForMap } from '../../utils/superblock-map-titles'; import './map.css'; -const { curriculumLocale } = envData; +const { curriculumLocale, showNewCurriculum, showUpcomingChanges } = envData; interface MapProps { currentSuperBlock?: SuperBlocks | null; forLanding?: boolean; } -interface MapData { - allChallengeNode: { - nodes: ChallengeNode[]; - }; -} - -function createSuperBlockTitle(superBlock: SuperBlocks) { - const superBlockTitle = i18next.t(`intro:${superBlock}.title`); - return superBlock === 'coding-interview-prep' - ? superBlockTitle - : i18next.t('learn.cert-map-estimates.certs', { - title: superBlockTitle - }); -} - const linkSpacingStyle = { display: 'flex', justifyContent: 'space-between', @@ -41,102 +31,91 @@ const linkSpacingStyle = { gap: '15px' }; -function renderLandingMap(nodes: ChallengeNode[]) { - nodes = nodes.filter( - ({ challenge }) => challenge.superBlock !== SuperBlocks.CodingInterviewPrep +function MapLi({ + superBlock, + landing = false +}: { + superBlock: SuperBlocks; + landing: boolean; +}) { + return ( +
  • + +
    + {generateIconComponent(superBlock, 'map-icon')} + {getSuperBlockTitleForMap(superBlock)} +
    + {landing && } + +
  • ); +} + +function renderLandingMap() { + const landingSuperOrder = + superBlockOrder[curriculumLocale as Languages][CurriculumMaps.Landing]; + return (
      - {nodes.map(({ challenge }, i) => ( -
    • - -
      - {generateIconComponent(challenge.superBlock, 'map-icon')} - {i18next.t(`intro:${challenge.superBlock}.title`)} -
      - - -
    • + {landingSuperOrder.map((superBlock, i) => ( + ))}
    ); } -function renderLearnMap( - nodes: ChallengeNode[], - currentSuperBlock: MapProps['currentSuperBlock'] -) { - nodes = nodes.filter( - ({ challenge }) => challenge.superBlock !== currentSuperBlock +function renderLearnMap(currentSuperBlock: MapProps['currentSuperBlock']) { + const tempAuditedSuperBlocks = getAuditedSuperBlocks({ + language: curriculumLocale, + showNewCurriculum: showNewCurriculum.toString(), + showUpcomingChanges: showUpcomingChanges.toString() + }); + const tempNotAuditedSuperBlocks = getNotAuditedSuperBlocks({ + language: curriculumLocale, + showNewCurriculum: showNewCurriculum.toString(), + showUpcomingChanges: showUpcomingChanges.toString() + }); + + const auditedSuperBlocks = tempAuditedSuperBlocks.filter( + superBlock => superBlock !== currentSuperBlock ); - return curriculumLocale === 'english' ? ( + + const notAuditedSuperBlocks = tempNotAuditedSuperBlocks.filter( + superBlock => superBlock !== currentSuperBlock + ); + + return (
      - {nodes.map(({ challenge }, i) => ( -
    • - -
      - {generateIconComponent(challenge.superBlock, 'map-icon')} - {createSuperBlockTitle(challenge.superBlock)} -
      - -
    • + {/* audited superblocks */} + {auditedSuperBlocks.map((superBlock, i) => ( + ))} -
    - ) : ( -
      - {nodes - .filter(({ challenge }) => - isAuditedCert(curriculumLocale, challenge.superBlock) - ) - .map(({ challenge }, i) => ( -
    • + + {/* has not audited superblocks */} + {notAuditedSuperBlocks.length > 0 && ( + <> + {' '} +
      +
      +

      + {i18next.t('learn.help-translate')}{' '} +

      -
      - {generateIconComponent(challenge.superBlock, 'map-icon')} - {createSuperBlockTitle(challenge.superBlock)} -
      + {i18next.t('learn.help-translate-link')} -
    • - ))} -
      -
      -

      {i18next.t('learn.help-translate')}

      - - {i18next.t('learn.help-translate-link')} - - -
      - {nodes - .filter( - ({ challenge }) => - !isAuditedCert(curriculumLocale, challenge.superBlock) - ) - .map(({ challenge }, i) => ( -
    • - -
      - {generateIconComponent(challenge.superBlock, 'map-icon')} - {createSuperBlockTitle(challenge.superBlock)} -
      - -
    • - ))} + + + + )} + + {/* not audited superblocks */} + {notAuditedSuperBlocks.map((superBlock, i) => ( + + ))}
    ); } @@ -145,39 +124,9 @@ export function Map({ forLanding = false, currentSuperBlock = null }: MapProps): React.ReactElement { - /* - * this query gets the first challenge from each block and the first block - * from each superblock, leaving you with one challenge from each - * superblock - */ - const data: MapData = useStaticQuery(graphql` - query SuperBlockNodes { - allChallengeNode( - sort: { fields: [challenge___superOrder] } - filter: { challenge: { order: { eq: 0 }, challengeOrder: { eq: 0 } } } - ) { - nodes { - challenge { - superBlock - dashedName - } - } - } - } - `); - - const nodes = data.allChallengeNode.nodes; - const temp = [ - nodes[0], - nodes[12], - ...nodes.filter((_, i) => i !== 0 && i !== 12) - ]; - return (
    - {forLanding - ? renderLandingMap(temp) - : renderLearnMap(temp, currentSuperBlock)} + {forLanding ? renderLandingMap() : renderLearnMap(currentSuperBlock)}
    ); } diff --git a/client/src/components/Map/map.test.tsx b/client/src/components/Map/map.test.tsx deleted file mode 100644 index 336608bdf07..00000000000 --- a/client/src/components/Map/map.test.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { render } from '@testing-library/react'; -import { useStaticQuery } from 'gatsby'; -import React from 'react'; - -import mockChallengeNodes from '../../__mocks__/challenge-nodes'; -import { Map } from '.'; - -beforeEach(() => { - (useStaticQuery as jest.Mock).mockImplementationOnce(() => ({ - allChallengeNode: { - nodes: mockChallengeNodes - } - })); -}); - -// set .scrollTo to avoid errors in default test environment -window.scrollTo = jest.fn(); - -test(' snapshot', () => { - const { container } = render(); - - expect(container).toMatchSnapshot('Map'); -}); - -const props = { - forLanding: true -}; diff --git a/client/src/templates/Introduction/super-block-intro.tsx b/client/src/templates/Introduction/super-block-intro.tsx index 493e6a1bdc9..e48882d03ad 100644 --- a/client/src/templates/Introduction/super-block-intro.tsx +++ b/client/src/templates/Introduction/super-block-intro.tsx @@ -11,6 +11,7 @@ import { bindActionCreators, Dispatch } from 'redux'; import { createSelector } from 'reselect'; import { SuperBlocks } from '../../../../config/certification-settings'; +import { getSuperBlockTitleForMap } from '../../utils/superblock-map-titles'; import DonateModal from '../../components/Donation/donation-modal'; import Login from '../../components/Header/components/Login'; import Map from '../../components/Map'; @@ -177,14 +178,7 @@ const SuperBlockIntroductionPage = (props: SuperBlockProp) => { nodesForSuperBlock.map(({ challenge: { block } }) => block) ); - const i18nSuperBlock = t(`intro:${superBlock}.title`); - const i18nTitle = - superBlock === SuperBlocks.CodingInterviewPrep - ? i18nSuperBlock - : t(`intro:misc-text.certification`, { - cert: i18nSuperBlock - }); - + const i18nTitle = getSuperBlockTitleForMap(superBlock); const defaultCurriculumNames = blockDashedNames; return ( diff --git a/client/src/utils/superblock-map-titles.ts b/client/src/utils/superblock-map-titles.ts new file mode 100644 index 00000000000..fa2ec32c994 --- /dev/null +++ b/client/src/utils/superblock-map-titles.ts @@ -0,0 +1,22 @@ +import i18next from 'i18next'; +import { SuperBlocks } from '../../../config/certification-settings'; + +// these are keys from i18n translations.json files +enum SuperBlockI18nKeys { + Certification = 'learn.cert-map-estimates.certs' +} + +// the key above is used to create the last word for superBlock titles used on +// the map and window. e.g. 'Certification' in Responsive Web Design +// Certification +const superBlocksWithoutLastWord = [SuperBlocks.CodingInterviewPrep]; + +export function getSuperBlockTitleForMap(superBlock: SuperBlocks) { + const i18nSuperBlock = i18next.t(`intro:${superBlock}.title`); + + return superBlocksWithoutLastWord.includes(superBlock) + ? i18nSuperBlock + : i18next.t([SuperBlockI18nKeys.Certification], { + title: i18nSuperBlock + }); +} diff --git a/config/i18n.ts b/config/i18n.ts index e7d8203aab1..9fe7a6311d2 100644 --- a/config/i18n.ts +++ b/config/i18n.ts @@ -1,5 +1,16 @@ -// --------------------------------------------------------------------------- -import { SuperBlocks } from './certification-settings'; +export enum Languages { + English = 'english', + Espanol = 'espanol', + Chinese = 'chinese', + ChineseTrandational = 'chinese-traditional', + Italian = 'italian', + Portuguese = 'portuguese', + Ukrainian = 'ukrainian', + Japanese = 'japanese', + German = 'german', + Arabic = 'arabic' +} + /* * List of languages with localizations enabled for builds. * @@ -10,160 +21,31 @@ import { SuperBlocks } from './certification-settings'; */ export const availableLangs = { client: [ - 'english', - 'espanol', - 'chinese', - 'chinese-traditional', - 'italian', - 'portuguese', - 'ukrainian', - 'japanese', - 'german', - 'arabic' + Languages.English, + Languages.Espanol, + Languages.Chinese, + Languages.ChineseTrandational, + Languages.Italian, + Languages.Portuguese, + Languages.Ukrainian, + Languages.Japanese, + Languages.German, + Languages.Arabic ], curriculum: [ - 'english', - 'espanol', - 'chinese', - 'chinese-traditional', - 'italian', - 'portuguese', - 'ukrainian', - 'japanese', - 'german', - 'arabic' + Languages.English, + Languages.Espanol, + Languages.Chinese, + Languages.ChineseTrandational, + Languages.Italian, + Languages.Portuguese, + Languages.Ukrainian, + Languages.Japanese, + Languages.German, + Languages.Arabic ] }; -/* - * List of certifications with localization enabled in their world language. - * - * These certifications have been approved 100% on Crowdin at least during - * their launch, and hence meet the QA standard to be published live. Other - * certifications which have not been audited & approved will fallback to - * English equivalent. - */ -export const auditedCerts = { - espanol: [ - SuperBlocks.RespWebDesign, - SuperBlocks.JsAlgoDataStruct, - SuperBlocks.FrontEndDevLibs, - SuperBlocks.DataVis, - SuperBlocks.BackEndDevApis, - SuperBlocks.QualityAssurance, - SuperBlocks.SciCompPy, - SuperBlocks.DataAnalysisPy - ], - chinese: [ - SuperBlocks.RespWebDesignNew, - SuperBlocks.RespWebDesign, - SuperBlocks.JsAlgoDataStruct, - SuperBlocks.FrontEndDevLibs, - SuperBlocks.DataVis, - SuperBlocks.BackEndDevApis, - SuperBlocks.QualityAssurance, - SuperBlocks.SciCompPy, - SuperBlocks.DataAnalysisPy, - SuperBlocks.InfoSec, - SuperBlocks.MachineLearningPy - ], - 'chinese-traditional': [ - SuperBlocks.RespWebDesignNew, - SuperBlocks.RespWebDesign, - SuperBlocks.JsAlgoDataStruct, - SuperBlocks.FrontEndDevLibs, - SuperBlocks.DataVis, - SuperBlocks.BackEndDevApis, - SuperBlocks.QualityAssurance, - SuperBlocks.SciCompPy, - SuperBlocks.DataAnalysisPy, - SuperBlocks.InfoSec, - SuperBlocks.MachineLearningPy - ], - italian: [ - SuperBlocks.RespWebDesignNew, - SuperBlocks.RespWebDesign, - SuperBlocks.JsAlgoDataStruct, - SuperBlocks.FrontEndDevLibs, - SuperBlocks.DataVis, - SuperBlocks.BackEndDevApis, - SuperBlocks.QualityAssurance, - SuperBlocks.SciCompPy, - SuperBlocks.DataAnalysisPy, - SuperBlocks.InfoSec, - SuperBlocks.MachineLearningPy, - SuperBlocks.CodingInterviewPrep - ], - portuguese: [ - SuperBlocks.RespWebDesignNew, - SuperBlocks.RespWebDesign, - SuperBlocks.JsAlgoDataStruct, - SuperBlocks.FrontEndDevLibs, - SuperBlocks.DataVis, - SuperBlocks.BackEndDevApis, - SuperBlocks.QualityAssurance, - SuperBlocks.SciCompPy, - SuperBlocks.DataAnalysisPy, - SuperBlocks.InfoSec, - SuperBlocks.MachineLearningPy, - SuperBlocks.CodingInterviewPrep, - SuperBlocks.RelationalDb - ], - ukrainian: [ - SuperBlocks.RespWebDesignNew, - SuperBlocks.RespWebDesign, - SuperBlocks.JsAlgoDataStruct, - SuperBlocks.FrontEndDevLibs, - SuperBlocks.DataVis, - SuperBlocks.BackEndDevApis, - SuperBlocks.QualityAssurance, - SuperBlocks.SciCompPy, - SuperBlocks.DataAnalysisPy, - SuperBlocks.InfoSec, - SuperBlocks.MachineLearningPy, - SuperBlocks.RelationalDb - ], - japanese: [ - SuperBlocks.RespWebDesign, - SuperBlocks.JsAlgoDataStruct, - SuperBlocks.FrontEndDevLibs, - SuperBlocks.DataVis, - SuperBlocks.BackEndDevApis, - SuperBlocks.QualityAssurance, - SuperBlocks.SciCompPy, - SuperBlocks.DataAnalysisPy, - SuperBlocks.InfoSec, - SuperBlocks.MachineLearningPy, - SuperBlocks.CodingInterviewPrep, - SuperBlocks.RelationalDb - ], - german: [ - SuperBlocks.RespWebDesign, - SuperBlocks.JsAlgoDataStruct, - SuperBlocks.FrontEndDevLibs - ], - arabic: [ - SuperBlocks.RespWebDesignNew, - SuperBlocks.JsAlgoDataStruct, - SuperBlocks.FrontEndDevLibs - ] -}; - -/** - * This contains the list of languages which have a beta->stable release - * that has been 100% translated. This will only be used during the window - * where a beta goes to stable but the translation isn't complete yet. - */ -export const languagesWithAuditedBetaReleases = [ - 'english', - 'portuguese', - 'italian', - 'ukrainian', - 'chinese', - 'chinese-traditional', - 'arabic' -]; - // --------------------------------------------------------------------------- // Each client language needs an entry in the rest of the variables below @@ -173,51 +55,48 @@ export const languagesWithAuditedBetaReleases = [ * Use a 639-1 code here https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes */ export const i18nextCodes = { - english: 'en', - espanol: 'es', - chinese: 'zh', - 'chinese-traditional': 'zh-Hant', - italian: 'it', - portuguese: 'pt-BR', - ukrainian: 'uk', - japanese: 'ja', - german: 'de', - arabic: 'ar' + [Languages.English]: 'en', + [Languages.Espanol]: 'es', + [Languages.Chinese]: 'zh', + [Languages.ChineseTrandational]: 'zh-Hant', + [Languages.Italian]: 'it', + [Languages.Portuguese]: 'pt-BR', + [Languages.Ukrainian]: 'uk', + [Languages.Japanese]: 'ja', + [Languages.German]: 'de', + [Languages.Arabic]: 'ar' }; -/* eslint-disable @typescript-eslint/naming-convention */ - // These are for the language selector dropdown menu in the footer -export enum LangNames { - english = 'English', - espanol = 'Español', - chinese = '中文(简体字)', - 'chinese-traditional' = '中文(繁體字)', - italian = 'Italiano', - portuguese = 'Português', - ukrainian = 'Українська', - japanese = '日本語', - german = 'Deutsch', - arabic = 'العربية' -} +export const LangNames = { + [Languages.English]: 'English', + [Languages.Espanol]: 'Español', + [Languages.Chinese]: '中文(简体字)', + [Languages.ChineseTrandational]: '中文(繁體字)', + [Languages.Italian]: 'Italiano', + [Languages.Portuguese]: 'Português', + [Languages.Ukrainian]: 'Українська', + [Languages.Japanese]: '日本語', + [Languages.German]: 'Deutsch', + [Languages.Arabic]: 'العربية' +}; /* These are for formatting dates and numbers. Used with JS .toLocaleString(). * There's an example in profile/components/Camper.js * List: https://github.com/unicode-cldr/cldr-dates-modern/tree/master/main */ -export enum LangCodes { - english = 'en-US', - espanol = 'es-419', - chinese = 'zh', - 'chinese-traditional' = 'zh-Hant', - italian = 'it', - portuguese = 'pt-BR', - ukrainian = 'uk', - japanese = 'ja', - german = 'de', - arabic = 'ar' -} -/* eslint-enable @typescript-eslint/naming-convention */ +export const LangCodes = { + [Languages.English]: 'en-US', + [Languages.Espanol]: 'es-419', + [Languages.Chinese]: 'zh', + [Languages.ChineseTrandational]: 'zh-Hant', + [Languages.Italian]: 'it', + [Languages.Portuguese]: 'pt-BR', + [Languages.Ukrainian]: 'uk', + [Languages.Japanese]: 'ja', + [Languages.German]: 'de', + [Languages.Arabic]: 'ar' +}; /** * This array contains languages that should NOT appear in the language selector. diff --git a/config/superblock-order.test.ts b/config/superblock-order.test.ts new file mode 100644 index 00000000000..233f7c4d60c --- /dev/null +++ b/config/superblock-order.test.ts @@ -0,0 +1,196 @@ +import { Languages } from './i18n'; +import { SuperBlocks } from './certification-settings'; +import { + CurriculumMaps, + defaultSuperBlockOrder, + getAuditedSuperBlocks, + getNotAuditedSuperBlocks, + getLearnSuperBlocks, + numberOfSuperBlocksOnLanding, + superBlockOrder, + SuperBlockStates, + TranslationStates +} from './superblock-order'; + +const superBlocks = Object.values(SuperBlocks); +const translationStates = Object.values(TranslationStates); +const superBlockStates = Object.values(SuperBlockStates); +const superBlockOrderLanguages = Object.keys(superBlockOrder); + +describe("'defaultSuperBlockOrder'", () => { + it("should have a matching item for each value in the 'SuperBlocks' object", () => { + expect(defaultSuperBlockOrder).toEqual(expect.arrayContaining(superBlocks)); + }); + + it('should not have any extra keys', () => { + expect(defaultSuperBlockOrder.length).toEqual(superBlocks.length); + }); +}); + +describe("'superBlockOrder'", () => { + superBlockOrderLanguages.forEach(language => { + describe(`'${language}'`, () => { + describe("'landing'", () => { + const landingSuperBlocks = + superBlockOrder[language as Languages][CurriculumMaps.Landing]; + + it(`should have ${numberOfSuperBlocksOnLanding} items (superBlocks)`, () => { + expect(landingSuperBlocks.length).toEqual( + numberOfSuperBlocksOnLanding + ); + }); + + it('should not have a superBlock out of order', () => { + landingSuperBlocks.forEach((superBlock, index) => { + const defaultIndex = defaultSuperBlockOrder.indexOf(superBlock); + const defaultSbsAfterCurrentSb = defaultSuperBlockOrder.slice( + defaultIndex + 1 + ); + + for (let j = index + 1; j < landingSuperBlocks.length; j++) { + expect(defaultSbsAfterCurrentSb).toContain(landingSuperBlocks[j]); + } + }); + }); + }); + + describe("'learn'", () => { + const learn = + superBlockOrder[language as Languages][CurriculumMaps.Learn]; + const audited = learn[TranslationStates.Audited]; + const notAudited = learn[TranslationStates.NotAudited]; + + describe("'audited'", () => { + superBlockStates.forEach(superBlockState => { + const stateSuperBlocks = audited[superBlockState]; + + describe(`'${superBlockState}'`, () => { + it('should not have a superBlock out of order', () => { + stateSuperBlocks.forEach((superBlock, index) => { + const defaultIndex = + defaultSuperBlockOrder.indexOf(superBlock); + const defaultSbsAfterCurrentSb = defaultSuperBlockOrder.slice( + defaultIndex + 1 + ); + + for (let j = index + 1; j < stateSuperBlocks.length; j++) { + expect(defaultSbsAfterCurrentSb).toContain( + stateSuperBlocks[j] + ); + } + }); + }); + }); + }); + }); + + describe('not audited', () => { + superBlockStates.forEach(superBlockState => { + const stateSuperBlocks = notAudited[superBlockState]; + + describe(`'${superBlockState}'`, () => { + it('should not have a superBlock out of order', () => { + stateSuperBlocks.forEach((superBlock, index) => { + const defaultIndex = + defaultSuperBlockOrder.indexOf(superBlock); + const defaultSbsAfterCurrentSb = defaultSuperBlockOrder.slice( + defaultIndex + 1 + ); + + for (let j = index + 1; j < stateSuperBlocks.length; j++) { + expect(defaultSbsAfterCurrentSb).toContain( + stateSuperBlocks[j] + ); + } + }); + }); + }); + }); + }); + + it("should have exactly one of each 'SuperBlocks' among it's children", () => { + // flatten all ${language}.learn superblocks into one array + const learnSuperBlocks: SuperBlocks[] = []; + + translationStates.forEach(translationState => { + superBlockStates.forEach(superBlockState => { + learnSuperBlocks.push( + ...learn[translationState][superBlockState] + ); + }); + }); + + superBlocks.forEach(superBlock => { + expect(learnSuperBlocks).toContain(superBlock); + }); + }); + }); + }); + }); +}); + +describe("'superBlockOrder' helper functions", () => { + it("'getLearnSuperBlocks('english')' should return the correct array", () => { + const learnSuperBlocks = getLearnSuperBlocks({ + language: 'english', + showNewCurriculum: 'true', + showUpcomingChanges: 'true' + }); + const test = [ + SuperBlocks.RespWebDesignNew, + SuperBlocks.JsAlgoDataStruct, + SuperBlocks.FrontEndDevLibs, + SuperBlocks.DataVis, + SuperBlocks.RelationalDb, + SuperBlocks.BackEndDevApis, + SuperBlocks.QualityAssurance, + SuperBlocks.SciCompPy, + SuperBlocks.DataAnalysisPy, + SuperBlocks.InfoSec, + SuperBlocks.MachineLearningPy, + SuperBlocks.CodingInterviewPrep, + SuperBlocks.JsAlgoDataStructNew, + SuperBlocks.RespWebDesign + ]; + expect(learnSuperBlocks).toStrictEqual(test); + expect(learnSuperBlocks.length).toEqual(test.length); + }); + + it("'getAuditedSuperBlocks('german')' should return the correct array", () => { + const auditedSuperBlocks = getAuditedSuperBlocks({ + language: 'german', + showNewCurriculum: 'true', + showUpcomingChanges: 'true' + }); + const test = [ + SuperBlocks.RespWebDesign, + SuperBlocks.JsAlgoDataStruct, + SuperBlocks.FrontEndDevLibs + ]; + expect(auditedSuperBlocks).toStrictEqual(test); + expect(auditedSuperBlocks.length).toEqual(test.length); + }); + + it("'getNotAuditedSuperBlocks('german')' should return the correct array", () => { + const notAuditedSuperBlocks = getNotAuditedSuperBlocks({ + language: 'german', + showNewCurriculum: 'true', + showUpcomingChanges: 'true' + }); + const test = [ + SuperBlocks.RespWebDesignNew, + SuperBlocks.DataVis, + SuperBlocks.RelationalDb, + SuperBlocks.BackEndDevApis, + SuperBlocks.QualityAssurance, + SuperBlocks.SciCompPy, + SuperBlocks.DataAnalysisPy, + SuperBlocks.InfoSec, + SuperBlocks.MachineLearningPy, + SuperBlocks.CodingInterviewPrep, + SuperBlocks.JsAlgoDataStructNew + ]; + expect(notAuditedSuperBlocks).toStrictEqual(test); + expect(notAuditedSuperBlocks.length).toEqual(test.length); + }); +}); diff --git a/config/superblock-order.ts b/config/superblock-order.ts new file mode 100644 index 00000000000..8277ad6dc2c --- /dev/null +++ b/config/superblock-order.ts @@ -0,0 +1,626 @@ +import { Languages } from './i18n'; +import { SuperBlocks } from './certification-settings'; + +/* + * .env SHOW_NEW_CURRICULUM = SuperBlockStates.New + * 'New' -> shown only on english staging at the moment + * + * .env SHOW_UPCOMING_CHANGES = SuperBlockStates.Upcoming + * 'Upcoming' is for development -> not shown on stag or prod anywhere + * + */ + +export enum CurriculumMaps { + Landing = 'landing', + Learn = 'learn' +} + +export enum TranslationStates { + Audited = 'audited', + NotAudited = 'notAudited' +} + +export enum SuperBlockStates { + Current = 'current', + New = 'new', + Upcoming = 'upcoming', + Legacy = 'legacy' +} + +export const orderedSuperBlockStates = [ + SuperBlockStates.Current, + SuperBlockStates.New, + SuperBlockStates.Upcoming, + SuperBlockStates.Legacy +]; + +type SuperBlockOrder = { + [key in Languages]: { + [CurriculumMaps.Landing]: SuperBlocks[]; + [CurriculumMaps.Learn]: { + [TranslationStates.Audited]: { + [SuperBlockStates.Current]: SuperBlocks[]; + [SuperBlockStates.New]: SuperBlocks[]; + [SuperBlockStates.Upcoming]: SuperBlocks[]; + [SuperBlockStates.Legacy]: SuperBlocks[]; + }; + [TranslationStates.NotAudited]: { + [SuperBlockStates.Current]: SuperBlocks[]; + [SuperBlockStates.New]: SuperBlocks[]; + [SuperBlockStates.Upcoming]: SuperBlocks[]; + [SuperBlockStates.Legacy]: SuperBlocks[]; + }; + }; + }; +}; + +// all languages should have this many, one for each current cert +export const numberOfSuperBlocksOnLanding = 11; + +/* + * This is the used for tests to make sure a superBlock isn't out of order + * e.g. so that a RWD button isn't below a JS button. + * It compares each array in `superBlockOrder` to this - those arrays do not + * have to include all these superBlocks, but the ones it does include, have + * to be in this order + */ +export const defaultSuperBlockOrder: SuperBlocks[] = [ + SuperBlocks.RespWebDesignNew, + SuperBlocks.RespWebDesign, + SuperBlocks.JsAlgoDataStructNew, + SuperBlocks.JsAlgoDataStruct, + SuperBlocks.FrontEndDevLibs, + SuperBlocks.DataVis, + SuperBlocks.RelationalDb, + SuperBlocks.BackEndDevApis, + SuperBlocks.QualityAssurance, + SuperBlocks.SciCompPy, + SuperBlocks.DataAnalysisPy, + SuperBlocks.InfoSec, + SuperBlocks.MachineLearningPy, + SuperBlocks.CodingInterviewPrep +]; + +/* + * The order of superblocks in the arrays below are how they appear on the maps + * + * The 'Landing' map array should contain exactly one superblock for each + * current, non-legacy certification, and only one superblock of each type - + * e.g. only one RWD superblock (button) + * + * The 'Learn' map arrays should contain ALL available SuperBlocks, sorted into + * their various states. These will be used to create the 'superOrder' property. + * + */ +export const superBlockOrder: SuperBlockOrder = { + [Languages.English]: { + [CurriculumMaps.Landing]: [ + SuperBlocks.RespWebDesignNew, + SuperBlocks.JsAlgoDataStruct, + SuperBlocks.FrontEndDevLibs, + SuperBlocks.DataVis, + SuperBlocks.RelationalDb, + SuperBlocks.BackEndDevApis, + SuperBlocks.QualityAssurance, + SuperBlocks.SciCompPy, + SuperBlocks.DataAnalysisPy, + SuperBlocks.InfoSec, + SuperBlocks.MachineLearningPy + ], + [CurriculumMaps.Learn]: { + [TranslationStates.Audited]: { + [SuperBlockStates.Current]: [ + SuperBlocks.RespWebDesignNew, + SuperBlocks.JsAlgoDataStruct, + SuperBlocks.FrontEndDevLibs, + SuperBlocks.DataVis, + SuperBlocks.RelationalDb, + SuperBlocks.BackEndDevApis, + SuperBlocks.QualityAssurance, + SuperBlocks.SciCompPy, + SuperBlocks.DataAnalysisPy, + SuperBlocks.InfoSec, + SuperBlocks.MachineLearningPy, + SuperBlocks.CodingInterviewPrep + ], + [SuperBlockStates.New]: [], + [SuperBlockStates.Upcoming]: [SuperBlocks.JsAlgoDataStructNew], + [SuperBlockStates.Legacy]: [SuperBlocks.RespWebDesign] + }, + [TranslationStates.NotAudited]: { + [SuperBlockStates.Current]: [], + [SuperBlockStates.New]: [], + [SuperBlockStates.Upcoming]: [], + [SuperBlockStates.Legacy]: [] + } + } + }, + [Languages.Espanol]: { + [CurriculumMaps.Landing]: [ + SuperBlocks.RespWebDesign, + SuperBlocks.JsAlgoDataStruct, + SuperBlocks.FrontEndDevLibs, + SuperBlocks.DataVis, + SuperBlocks.RelationalDb, + SuperBlocks.BackEndDevApis, + SuperBlocks.QualityAssurance, + SuperBlocks.SciCompPy, + SuperBlocks.DataAnalysisPy, + SuperBlocks.InfoSec, + SuperBlocks.MachineLearningPy + ], + [CurriculumMaps.Learn]: { + [TranslationStates.Audited]: { + [SuperBlockStates.Current]: [ + SuperBlocks.RespWebDesign, + SuperBlocks.JsAlgoDataStruct, + SuperBlocks.FrontEndDevLibs, + SuperBlocks.DataVis, + SuperBlocks.BackEndDevApis, + SuperBlocks.QualityAssurance, + SuperBlocks.SciCompPy, + SuperBlocks.DataAnalysisPy + ], + [SuperBlockStates.New]: [], + [SuperBlockStates.Upcoming]: [], + [SuperBlockStates.Legacy]: [] + }, + [TranslationStates.NotAudited]: { + [SuperBlockStates.Current]: [ + SuperBlocks.RespWebDesignNew, + SuperBlocks.RelationalDb, + SuperBlocks.InfoSec, + SuperBlocks.MachineLearningPy, + SuperBlocks.CodingInterviewPrep + ], + [SuperBlockStates.New]: [], + [SuperBlockStates.Upcoming]: [SuperBlocks.JsAlgoDataStructNew], + [SuperBlockStates.Legacy]: [] + } + } + }, + [Languages.Chinese]: { + [CurriculumMaps.Landing]: [ + SuperBlocks.RespWebDesignNew, + SuperBlocks.JsAlgoDataStruct, + SuperBlocks.FrontEndDevLibs, + SuperBlocks.DataVis, + SuperBlocks.RelationalDb, + SuperBlocks.BackEndDevApis, + SuperBlocks.QualityAssurance, + SuperBlocks.SciCompPy, + SuperBlocks.DataAnalysisPy, + SuperBlocks.InfoSec, + SuperBlocks.MachineLearningPy + ], + [CurriculumMaps.Learn]: { + [TranslationStates.Audited]: { + [SuperBlockStates.Current]: [ + SuperBlocks.RespWebDesignNew, + SuperBlocks.JsAlgoDataStruct, + SuperBlocks.FrontEndDevLibs, + SuperBlocks.DataVis, + SuperBlocks.BackEndDevApis, + SuperBlocks.QualityAssurance, + SuperBlocks.SciCompPy, + SuperBlocks.DataAnalysisPy, + SuperBlocks.InfoSec, + SuperBlocks.MachineLearningPy + ], + [SuperBlockStates.New]: [], + [SuperBlockStates.Upcoming]: [], + [SuperBlockStates.Legacy]: [SuperBlocks.RespWebDesign] + }, + [TranslationStates.NotAudited]: { + [SuperBlockStates.Current]: [ + SuperBlocks.RelationalDb, + SuperBlocks.CodingInterviewPrep + ], + [SuperBlockStates.New]: [], + [SuperBlockStates.Upcoming]: [SuperBlocks.JsAlgoDataStructNew], + [SuperBlockStates.Legacy]: [] + } + } + }, + [Languages.ChineseTrandational]: { + [CurriculumMaps.Landing]: [ + SuperBlocks.RespWebDesignNew, + SuperBlocks.JsAlgoDataStruct, + SuperBlocks.FrontEndDevLibs, + SuperBlocks.DataVis, + SuperBlocks.RelationalDb, + SuperBlocks.BackEndDevApis, + SuperBlocks.QualityAssurance, + SuperBlocks.SciCompPy, + SuperBlocks.DataAnalysisPy, + SuperBlocks.InfoSec, + SuperBlocks.MachineLearningPy + ], + [CurriculumMaps.Learn]: { + [TranslationStates.Audited]: { + [SuperBlockStates.Current]: [ + SuperBlocks.RespWebDesignNew, + SuperBlocks.JsAlgoDataStruct, + SuperBlocks.FrontEndDevLibs, + SuperBlocks.DataVis, + SuperBlocks.BackEndDevApis, + SuperBlocks.QualityAssurance, + SuperBlocks.SciCompPy, + SuperBlocks.DataAnalysisPy, + SuperBlocks.InfoSec, + SuperBlocks.MachineLearningPy + ], + [SuperBlockStates.New]: [], + [SuperBlockStates.Upcoming]: [], + [SuperBlockStates.Legacy]: [SuperBlocks.RespWebDesign] + }, + [TranslationStates.NotAudited]: { + [SuperBlockStates.Current]: [ + SuperBlocks.RelationalDb, + SuperBlocks.CodingInterviewPrep + ], + [SuperBlockStates.New]: [], + [SuperBlockStates.Upcoming]: [SuperBlocks.JsAlgoDataStructNew], + [SuperBlockStates.Legacy]: [] + } + } + }, + [Languages.Italian]: { + [CurriculumMaps.Landing]: [ + SuperBlocks.RespWebDesignNew, + SuperBlocks.JsAlgoDataStruct, + SuperBlocks.FrontEndDevLibs, + SuperBlocks.DataVis, + SuperBlocks.RelationalDb, + SuperBlocks.BackEndDevApis, + SuperBlocks.QualityAssurance, + SuperBlocks.SciCompPy, + SuperBlocks.DataAnalysisPy, + SuperBlocks.InfoSec, + SuperBlocks.MachineLearningPy + ], + [CurriculumMaps.Learn]: { + [TranslationStates.Audited]: { + [SuperBlockStates.Current]: [ + SuperBlocks.RespWebDesignNew, + SuperBlocks.JsAlgoDataStruct, + SuperBlocks.FrontEndDevLibs, + SuperBlocks.DataVis, + SuperBlocks.RelationalDb, + SuperBlocks.BackEndDevApis, + SuperBlocks.QualityAssurance, + SuperBlocks.SciCompPy, + SuperBlocks.DataAnalysisPy, + SuperBlocks.InfoSec, + SuperBlocks.MachineLearningPy, + SuperBlocks.CodingInterviewPrep + ], + [SuperBlockStates.New]: [], + [SuperBlockStates.Upcoming]: [], + [SuperBlockStates.Legacy]: [SuperBlocks.RespWebDesign] + }, + [TranslationStates.NotAudited]: { + [SuperBlockStates.Current]: [], + [SuperBlockStates.New]: [], + [SuperBlockStates.Upcoming]: [SuperBlocks.JsAlgoDataStructNew], + [SuperBlockStates.Legacy]: [] + } + } + }, + [Languages.Portuguese]: { + [CurriculumMaps.Landing]: [ + SuperBlocks.RespWebDesignNew, + SuperBlocks.JsAlgoDataStruct, + SuperBlocks.FrontEndDevLibs, + SuperBlocks.DataVis, + SuperBlocks.RelationalDb, + SuperBlocks.BackEndDevApis, + SuperBlocks.QualityAssurance, + SuperBlocks.SciCompPy, + SuperBlocks.DataAnalysisPy, + SuperBlocks.InfoSec, + SuperBlocks.MachineLearningPy + ], + [CurriculumMaps.Learn]: { + [TranslationStates.Audited]: { + [SuperBlockStates.Current]: [ + SuperBlocks.RespWebDesignNew, + SuperBlocks.JsAlgoDataStruct, + SuperBlocks.FrontEndDevLibs, + SuperBlocks.DataVis, + SuperBlocks.RelationalDb, + SuperBlocks.BackEndDevApis, + SuperBlocks.QualityAssurance, + SuperBlocks.SciCompPy, + SuperBlocks.DataAnalysisPy, + SuperBlocks.InfoSec, + SuperBlocks.MachineLearningPy, + SuperBlocks.CodingInterviewPrep + ], + [SuperBlockStates.New]: [], + [SuperBlockStates.Upcoming]: [], + [SuperBlockStates.Legacy]: [SuperBlocks.RespWebDesign] + }, + [TranslationStates.NotAudited]: { + [SuperBlockStates.Current]: [], + [SuperBlockStates.New]: [], + [SuperBlockStates.Upcoming]: [SuperBlocks.JsAlgoDataStructNew], + [SuperBlockStates.Legacy]: [] + } + } + }, + [Languages.Ukrainian]: { + [CurriculumMaps.Landing]: [ + SuperBlocks.RespWebDesignNew, + SuperBlocks.JsAlgoDataStruct, + SuperBlocks.FrontEndDevLibs, + SuperBlocks.DataVis, + SuperBlocks.RelationalDb, + SuperBlocks.BackEndDevApis, + SuperBlocks.QualityAssurance, + SuperBlocks.SciCompPy, + SuperBlocks.DataAnalysisPy, + SuperBlocks.InfoSec, + SuperBlocks.MachineLearningPy + ], + [CurriculumMaps.Learn]: { + [TranslationStates.Audited]: { + [SuperBlockStates.Current]: [ + SuperBlocks.RespWebDesignNew, + SuperBlocks.JsAlgoDataStruct, + SuperBlocks.FrontEndDevLibs, + SuperBlocks.DataVis, + SuperBlocks.RelationalDb, + SuperBlocks.BackEndDevApis, + SuperBlocks.QualityAssurance, + SuperBlocks.SciCompPy, + SuperBlocks.DataAnalysisPy, + SuperBlocks.InfoSec, + SuperBlocks.MachineLearningPy + ], + [SuperBlockStates.New]: [], + [SuperBlockStates.Upcoming]: [], + [SuperBlockStates.Legacy]: [SuperBlocks.RespWebDesign] + }, + [TranslationStates.NotAudited]: { + [SuperBlockStates.Current]: [SuperBlocks.CodingInterviewPrep], + [SuperBlockStates.New]: [], + [SuperBlockStates.Upcoming]: [SuperBlocks.JsAlgoDataStructNew], + [SuperBlockStates.Legacy]: [] + } + } + }, + [Languages.Japanese]: { + [CurriculumMaps.Landing]: [ + SuperBlocks.RespWebDesign, + SuperBlocks.JsAlgoDataStruct, + SuperBlocks.FrontEndDevLibs, + SuperBlocks.DataVis, + SuperBlocks.RelationalDb, + SuperBlocks.BackEndDevApis, + SuperBlocks.QualityAssurance, + SuperBlocks.SciCompPy, + SuperBlocks.DataAnalysisPy, + SuperBlocks.InfoSec, + SuperBlocks.MachineLearningPy + ], + [CurriculumMaps.Learn]: { + [TranslationStates.Audited]: { + [SuperBlockStates.Current]: [ + SuperBlocks.RespWebDesign, + SuperBlocks.JsAlgoDataStruct, + SuperBlocks.FrontEndDevLibs, + SuperBlocks.DataVis, + SuperBlocks.RelationalDb, + SuperBlocks.BackEndDevApis, + SuperBlocks.QualityAssurance, + SuperBlocks.SciCompPy, + SuperBlocks.DataAnalysisPy, + SuperBlocks.InfoSec, + SuperBlocks.MachineLearningPy, + SuperBlocks.CodingInterviewPrep + ], + [SuperBlockStates.New]: [], + [SuperBlockStates.Upcoming]: [], + [SuperBlockStates.Legacy]: [] + }, + [TranslationStates.NotAudited]: { + [SuperBlockStates.Current]: [SuperBlocks.RespWebDesignNew], + [SuperBlockStates.New]: [], + [SuperBlockStates.Upcoming]: [SuperBlocks.JsAlgoDataStructNew], + [SuperBlockStates.Legacy]: [] + } + } + }, + [Languages.German]: { + [CurriculumMaps.Landing]: [ + SuperBlocks.RespWebDesign, + SuperBlocks.JsAlgoDataStruct, + SuperBlocks.FrontEndDevLibs, + SuperBlocks.DataVis, + SuperBlocks.RelationalDb, + SuperBlocks.BackEndDevApis, + SuperBlocks.QualityAssurance, + SuperBlocks.SciCompPy, + SuperBlocks.DataAnalysisPy, + SuperBlocks.InfoSec, + SuperBlocks.MachineLearningPy + ], + [CurriculumMaps.Learn]: { + [TranslationStates.Audited]: { + [SuperBlockStates.Current]: [ + SuperBlocks.RespWebDesign, + SuperBlocks.JsAlgoDataStruct, + SuperBlocks.FrontEndDevLibs + ], + [SuperBlockStates.New]: [], + [SuperBlockStates.Upcoming]: [], + [SuperBlockStates.Legacy]: [] + }, + [TranslationStates.NotAudited]: { + [SuperBlockStates.Current]: [ + SuperBlocks.RespWebDesignNew, + SuperBlocks.DataVis, + SuperBlocks.RelationalDb, + SuperBlocks.BackEndDevApis, + SuperBlocks.QualityAssurance, + SuperBlocks.SciCompPy, + SuperBlocks.DataAnalysisPy, + SuperBlocks.InfoSec, + SuperBlocks.MachineLearningPy, + SuperBlocks.CodingInterviewPrep + ], + [SuperBlockStates.New]: [], + [SuperBlockStates.Upcoming]: [SuperBlocks.JsAlgoDataStructNew], + [SuperBlockStates.Legacy]: [] + } + } + }, + [Languages.Arabic]: { + [CurriculumMaps.Landing]: [ + SuperBlocks.RespWebDesignNew, + SuperBlocks.JsAlgoDataStruct, + SuperBlocks.FrontEndDevLibs, + SuperBlocks.DataVis, + SuperBlocks.RelationalDb, + SuperBlocks.BackEndDevApis, + SuperBlocks.QualityAssurance, + SuperBlocks.SciCompPy, + SuperBlocks.DataAnalysisPy, + SuperBlocks.InfoSec, + SuperBlocks.MachineLearningPy + ], + [CurriculumMaps.Learn]: { + [TranslationStates.Audited]: { + [SuperBlockStates.Current]: [ + SuperBlocks.RespWebDesignNew, + SuperBlocks.JsAlgoDataStruct, + SuperBlocks.FrontEndDevLibs + ], + [SuperBlockStates.New]: [], + [SuperBlockStates.Upcoming]: [], + [SuperBlockStates.Legacy]: [] + }, + [TranslationStates.NotAudited]: { + [SuperBlockStates.Current]: [ + SuperBlocks.DataVis, + SuperBlocks.RelationalDb, + SuperBlocks.BackEndDevApis, + SuperBlocks.QualityAssurance, + SuperBlocks.SciCompPy, + SuperBlocks.DataAnalysisPy, + SuperBlocks.InfoSec, + SuperBlocks.MachineLearningPy, + SuperBlocks.CodingInterviewPrep + ], + [SuperBlockStates.New]: [], + [SuperBlockStates.Upcoming]: [SuperBlocks.JsAlgoDataStructNew], + [SuperBlockStates.Legacy]: [SuperBlocks.RespWebDesign] + } + } + } +}; + +// The client uses the object above to create the map +// Keep this so it can't change +Object.freeze(superBlockOrder); + +function shouldShowSuperblocks({ + superBlockState, + showNewCurriculum = 'false', + showUpcomingChanges = 'false' +}: { + superBlockState: string; + showNewCurriculum: string; + showUpcomingChanges: string; +}) { + if ( + (superBlockState === SuperBlockStates.New && + showNewCurriculum !== 'true') || + (superBlockState === SuperBlockStates.Upcoming && + showUpcomingChanges !== 'true') + ) { + return false; + } + return true; +} + +export function getLearnSuperBlocks({ + language = 'english', + showNewCurriculum = 'false', + showUpcomingChanges = 'false' +}) { + const learnSuperBlocks: SuperBlocks[] = []; + + Object.values(TranslationStates).forEach(translationState => { + Object.values(SuperBlockStates).forEach(superBlockState => { + if ( + shouldShowSuperblocks({ + superBlockState, + showNewCurriculum, + showUpcomingChanges + }) + ) { + learnSuperBlocks.push( + ...superBlockOrder[language as Languages][CurriculumMaps.Learn][ + translationState as TranslationStates + ][superBlockState as SuperBlockStates] + ); + } + }); + }); + + return learnSuperBlocks; +} + +export function getAuditedSuperBlocks({ + language = 'english', + showNewCurriculum = 'false', + showUpcomingChanges = 'false' +}) { + const auditedSuperBlocks: SuperBlocks[] = []; + + Object.values(SuperBlockStates).forEach(superBlockState => { + if ( + shouldShowSuperblocks({ + superBlockState, + showNewCurriculum, + showUpcomingChanges + }) + ) { + auditedSuperBlocks.push( + ...superBlockOrder[language as Languages][CurriculumMaps.Learn][ + TranslationStates.Audited + ][superBlockState as SuperBlockStates] + ); + } + }); + + return auditedSuperBlocks; +} + +export function getNotAuditedSuperBlocks({ + language = 'english', + showNewCurriculum = 'false', + showUpcomingChanges = 'false' +}) { + const notAuditedSuperBlocks: SuperBlocks[] = []; + + Object.values(SuperBlockStates).forEach(superBlockState => { + if ( + shouldShowSuperblocks({ + superBlockState, + showNewCurriculum, + showUpcomingChanges + }) + ) { + notAuditedSuperBlocks.push( + ...superBlockOrder[language as Languages][CurriculumMaps.Learn][ + TranslationStates.NotAudited + ][superBlockState as SuperBlockStates] + ); + } + }); + + return notAuditedSuperBlocks; +} diff --git a/curriculum/utils.js b/curriculum/utils.js index d06f9bb58c4..16b0beba57f 100644 --- a/curriculum/utils.js +++ b/curriculum/utils.js @@ -1,12 +1,18 @@ const path = require('path'); +const { + CurriculumMaps, + superBlockOrder, + SuperBlockStates, + TranslationStates, + orderedSuperBlockStates +} = require('../config/superblock-order'); + require('dotenv').config({ path: path.resolve(__dirname, '../.env') }); -const { - availableLangs, - languagesWithAuditedBetaReleases -} = require('../config/i18n'); +const { availableLangs } = require('../config/i18n'); const curriculumLangs = availableLangs.curriculum; +// checks that the CURRICULUM_LOCALE exists and is an available language exports.testedLang = function testedLang() { if (process.env.CURRICULUM_LOCALE) { if (curriculumLangs.includes(process.env.CURRICULUM_LOCALE)) { @@ -20,67 +26,78 @@ exports.testedLang = function testedLang() { } }; -// TODO: migrate to TS and use the SuperBlocks enum from -// config/certification-settings.ts - -const superBlockToOrder = { - '2022/responsive-web-design': 0, - 'javascript-algorithms-and-data-structures': 1, - 'front-end-development-libraries': 2, - 'data-visualization': 3, - 'relational-database': 4, - 'back-end-development-and-apis': 5, - 'quality-assurance': 6, - 'scientific-computing-with-python': 7, - 'data-analysis-with-python': 8, - 'information-security': 9, - 'machine-learning-with-python': 10, - 'coding-interview-prep': 11, - 'responsive-web-design': 12 -}; - -/** - * This order is used for i18n instances where a new certification is released - * from beta but is not audited, so cannot be reordered (due to the way we - * split the map) +/* + * creates an object with all the superblocks in + * 'superBlockOrder[lang][learn]' as keys and gives them + * a number (superOrder), starting with 0, as the value */ -const superBlockNonAuditedOrder = { - 'responsive-web-design': 0, - 'javascript-algorithms-and-data-structures': 1, - 'front-end-development-libraries': 2, - 'data-visualization': 3, - 'relational-database': 4, - 'back-end-development-and-apis': 5, - 'quality-assurance': 6, - 'scientific-computing-with-python': 7, - 'data-analysis-with-python': 8, - 'information-security': 9, - 'machine-learning-with-python': 10, - 'coding-interview-prep': 11, - '2022/responsive-web-design': 12 -}; - -const superBlockToNewOrder = { - ...superBlockToOrder, - '2022/javascript-algorithms-and-data-structures': 13 -}; - -function getSuperOrder( - superblock, - { showNewCurriculum } = { showNewCurriculum: false } -) { - let orderMap = superBlockToOrder; - if (showNewCurriculum) { - orderMap = superBlockToNewOrder; +function createSuperOrder({ + language = 'english', + showNewCurriculum = 'false', + showUpcomingChanges = 'false' +}) { + if (!Object.prototype.hasOwnProperty.call(superBlockOrder, language)) { + throw Error(`${language} not found in superblock-order.ts`); } + if ( - !languagesWithAuditedBetaReleases.includes(process.env.CURRICULUM_LOCALE) + !Object.prototype.hasOwnProperty.call(superBlockOrder[language], [ + CurriculumMaps.Learn + ]) ) { - orderMap = superBlockNonAuditedOrder; + throw Error( + `${language} does not have a 'learn' key in superblock-order.ts` + ); } + + const audited = + superBlockOrder[language][CurriculumMaps.Learn][TranslationStates.Audited]; + const notAudited = + superBlockOrder[language][CurriculumMaps.Learn][ + TranslationStates.NotAudited + ]; + + const superOrder = {}; + let i = 0; + + function addToSuperOrder(superBlocks) { + superBlocks.forEach(key => { + superOrder[key] = i; + i++; + }); + } + + function canAddToSuperOrder(superBlockState) { + if (superBlockState === SuperBlockStates.New) + return showNewCurriculum === 'true'; + if (superBlockState === SuperBlockStates.Upcoming) + return showUpcomingChanges === 'true'; + return true; + } + + function addSuperBlockStates(translationState) { + orderedSuperBlockStates.forEach(state => { + if (canAddToSuperOrder(state)) addToSuperOrder(translationState[state]); + }); + } + + addSuperBlockStates(audited); + addSuperBlockStates(notAudited); + + return superOrder; +} + +const superOrder = createSuperOrder({ + language: process.env.CURRICULUM_LOCALE, + showNewCurriculum: process.env.SHOW_NEW_CURRICULUM, + showUpcomingChanges: process.env.SHOW_UPCOMING_CHANGES +}); + +// gets the superOrder of a superBlock from the object created above +function getSuperOrder(superblock) { if (typeof superblock !== 'string') throw Error('superblock must be a string'); - const order = orderMap[superblock]; + const order = superOrder[superblock]; if (typeof order === 'undefined') throw Error(`${superblock} is not a valid superblock`); return order; @@ -112,5 +129,6 @@ function getSuperBlockFromDir(dir) { return directoryToSuperblock[dir]; } +exports.createSuperOrder = createSuperOrder; exports.getSuperOrder = getSuperOrder; exports.getSuperBlockFromDir = getSuperBlockFromDir; diff --git a/curriculum/utils.test.ts b/curriculum/utils.test.ts index b663a305c80..03919e73824 100644 --- a/curriculum/utils.test.ts +++ b/curriculum/utils.test.ts @@ -5,11 +5,117 @@ import fs from 'fs'; import path from 'path'; import { config } from 'dotenv'; import { SuperBlocks } from '../config/certification-settings'; -import { languagesWithAuditedBetaReleases } from '../config/i18n'; -import { getSuperOrder, getSuperBlockFromDir } from './utils'; +import { createSuperOrder, getSuperOrder, getSuperBlockFromDir } from './utils'; config({ path: path.resolve(__dirname, '../.env') }); +const englishTest = { + [SuperBlocks.RespWebDesignNew]: 0, + [SuperBlocks.JsAlgoDataStruct]: 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.CodingInterviewPrep]: 11, + [SuperBlocks.RespWebDesign]: 12 +}; + +const upcomingTest = { + [SuperBlocks.RespWebDesignNew]: 0, + [SuperBlocks.JsAlgoDataStruct]: 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.CodingInterviewPrep]: 11, + [SuperBlocks.JsAlgoDataStructNew]: 12, + [SuperBlocks.RespWebDesign]: 13 +}; + +const espanolTest = { + [SuperBlocks.RespWebDesign]: 0, + [SuperBlocks.JsAlgoDataStruct]: 1, + [SuperBlocks.FrontEndDevLibs]: 2, + [SuperBlocks.DataVis]: 3, + [SuperBlocks.BackEndDevApis]: 4, + [SuperBlocks.QualityAssurance]: 5, + [SuperBlocks.SciCompPy]: 6, + [SuperBlocks.DataAnalysisPy]: 7, + [SuperBlocks.RespWebDesignNew]: 8, + [SuperBlocks.RelationalDb]: 9, + [SuperBlocks.InfoSec]: 10, + [SuperBlocks.MachineLearningPy]: 11, + [SuperBlocks.CodingInterviewPrep]: 12 +}; + +const chineseTest = { + [SuperBlocks.RespWebDesignNew]: 0, + [SuperBlocks.JsAlgoDataStruct]: 1, + [SuperBlocks.FrontEndDevLibs]: 2, + [SuperBlocks.DataVis]: 3, + [SuperBlocks.BackEndDevApis]: 4, + [SuperBlocks.QualityAssurance]: 5, + [SuperBlocks.SciCompPy]: 6, + [SuperBlocks.DataAnalysisPy]: 7, + [SuperBlocks.InfoSec]: 8, + [SuperBlocks.MachineLearningPy]: 9, + [SuperBlocks.RespWebDesign]: 10, + [SuperBlocks.RelationalDb]: 11, + [SuperBlocks.CodingInterviewPrep]: 12 +}; + +describe('createSuperOrder', () => { + const englishSuperOrder = createSuperOrder({ + language: 'english', + showNewCurriculum: 'false', + showUpcomingChanges: 'false' + }); + + const upcomingSuperOrder = createSuperOrder({ + language: 'english', + showNewCurriculum: 'false', + showUpcomingChanges: 'true' + }); + + const espanolSuperOrder = createSuperOrder({ + language: 'espanol', + showNewCurriculum: 'false', + showUpcomingChanges: 'false' + }); + + const chineseSuperOrder = createSuperOrder({ + language: 'chinese', + showNewCurriculum: 'false', + showUpcomingChanges: 'false' + }); + + it("should create the correct object for 'english'", () => { + expect(englishSuperOrder).toStrictEqual(englishTest); + }); + + it('should create the correct object with upcoming changes shown', () => { + expect(upcomingSuperOrder).toStrictEqual(upcomingTest); + }); + + it("should create the correct object for 'espanol'", () => { + expect(espanolSuperOrder).toStrictEqual(espanolTest); + }); + + it("should create the correct object for 'chinese'", () => { + expect(chineseSuperOrder).toStrictEqual(chineseTest); + }); +}); + describe('getSuperOrder', () => { it('returns a number for valid superblocks', () => { expect.assertions(1); @@ -29,108 +135,37 @@ describe('getSuperOrder', () => { expect(() => getSuperOrder('certifications')).toThrow(); }); - if ( - languagesWithAuditedBetaReleases.includes( - process.env.CURRICULUM_LOCALE as string - ) - ) { - it('returns unique numbers for all current superblocks (audited beta)', () => { - expect.assertions(13); - expect(getSuperOrder('2022/responsive-web-design')).toBe(0); - expect(getSuperOrder('javascript-algorithms-and-data-structures')).toBe( - 1 - ); - expect(getSuperOrder('front-end-development-libraries')).toBe(2); - expect(getSuperOrder('data-visualization')).toBe(3); - expect(getSuperOrder('relational-database')).toBe(4); - expect(getSuperOrder('back-end-development-and-apis')).toBe(5); - expect(getSuperOrder('quality-assurance')).toBe(6); - expect(getSuperOrder('scientific-computing-with-python')).toBe(7); - expect(getSuperOrder('data-analysis-with-python')).toBe(8); - expect(getSuperOrder('information-security')).toBe(9); - expect(getSuperOrder('machine-learning-with-python')).toBe(10); - expect(getSuperOrder('coding-interview-prep')).toBe(11); - expect(getSuperOrder('responsive-web-design')).toBe(12); - }); - } else { - it('returns unique numbers for all current superblocks (not audited beta)', () => { - expect.assertions(13); - expect(getSuperOrder('responsive-web-design')).toBe(0); - expect(getSuperOrder('javascript-algorithms-and-data-structures')).toBe( - 1 - ); - expect(getSuperOrder('front-end-development-libraries')).toBe(2); - expect(getSuperOrder('data-visualization')).toBe(3); - expect(getSuperOrder('relational-database')).toBe(4); - expect(getSuperOrder('back-end-development-and-apis')).toBe(5); - expect(getSuperOrder('quality-assurance')).toBe(6); - expect(getSuperOrder('scientific-computing-with-python')).toBe(7); - expect(getSuperOrder('data-analysis-with-python')).toBe(8); - expect(getSuperOrder('information-security')).toBe(9); - expect(getSuperOrder('machine-learning-with-python')).toBe(10); - expect(getSuperOrder('coding-interview-prep')).toBe(11); - expect(getSuperOrder('2022/responsive-web-design')).toBe(12); - }); - } - - it('returns a different order if passed the option showNewCurriculum: true', () => { - // Skip non-english tests while the RWD cert is still being translated. + it('returns unique numbers for all current superblocks', () => { + // Skip non-english tests if (process.env.CURRICULUM_LOCALE !== 'english') { return; } - expect.assertions(14); - expect( - getSuperOrder('2022/responsive-web-design', { showNewCurriculum: true }) - ).toBe(0); - expect( - getSuperOrder('javascript-algorithms-and-data-structures', { - showNewCurriculum: true - }) - ).toBe(1); - expect( - getSuperOrder('front-end-development-libraries', { - showNewCurriculum: true - }) - ).toBe(2); - expect( - getSuperOrder('data-visualization', { showNewCurriculum: true }) - ).toBe(3); - expect( - getSuperOrder('relational-database', { showNewCurriculum: true }) - ).toBe(4); - expect( - getSuperOrder('back-end-development-and-apis', { - showNewCurriculum: true - }) - ).toBe(5); - expect( - getSuperOrder('quality-assurance', { showNewCurriculum: true }) - ).toBe(6); - expect( - getSuperOrder('scientific-computing-with-python', { - showNewCurriculum: true - }) - ).toBe(7); - expect( - getSuperOrder('data-analysis-with-python', { showNewCurriculum: true }) - ).toBe(8); - expect( - getSuperOrder('information-security', { showNewCurriculum: true }) - ).toBe(9); - expect( - getSuperOrder('machine-learning-with-python', { showNewCurriculum: true }) - ).toBe(10); - expect( - getSuperOrder('coding-interview-prep', { showNewCurriculum: true }) - ).toBe(11); - expect( - getSuperOrder('responsive-web-design', { showNewCurriculum: true }) - ).toBe(12); - expect( - getSuperOrder('2022/javascript-algorithms-and-data-structures', { - showNewCurriculum: true - }) - ).toBe(13); + + if (process.env.SHOW_UPCOMING_CHANGES !== 'true') { + expect.assertions(13); + } else { + expect.assertions(14); + } + + expect(getSuperOrder(SuperBlocks.RespWebDesignNew)).toBe(0); + expect(getSuperOrder(SuperBlocks.JsAlgoDataStruct)).toBe(1); + expect(getSuperOrder(SuperBlocks.FrontEndDevLibs)).toBe(2); + expect(getSuperOrder(SuperBlocks.DataVis)).toBe(3); + expect(getSuperOrder(SuperBlocks.RelationalDb)).toBe(4); + expect(getSuperOrder(SuperBlocks.BackEndDevApis)).toBe(5); + expect(getSuperOrder(SuperBlocks.QualityAssurance)).toBe(6); + expect(getSuperOrder(SuperBlocks.SciCompPy)).toBe(7); + expect(getSuperOrder(SuperBlocks.DataAnalysisPy)).toBe(8); + expect(getSuperOrder(SuperBlocks.InfoSec)).toBe(9); + expect(getSuperOrder(SuperBlocks.MachineLearningPy)).toBe(10); + expect(getSuperOrder(SuperBlocks.CodingInterviewPrep)).toBe(11); + + if (process.env.SHOW_UPCOMING_CHANGES === 'true') { + expect(getSuperOrder(SuperBlocks.JsAlgoDataStructNew)).toBe(12); + expect(getSuperOrder(SuperBlocks.RespWebDesign)).toBe(13); + } else { + expect(getSuperOrder(SuperBlocks.RespWebDesign)).toBe(12); + } }); }); diff --git a/cypress/e2e/default/landing.js b/cypress/e2e/default/landing.js index 21f58e917a0..8f25c16a00f 100644 --- a/cypress/e2e/default/landing.js +++ b/cypress/e2e/default/landing.js @@ -8,7 +8,6 @@ const selectors = { const certifications = [ '(New) Responsive Web Design', - 'Legacy Responsive Web Design', 'JavaScript Algorithms and Data Structures', 'Front End Development Libraries', 'Data Visualization', @@ -59,7 +58,7 @@ describe('Landing page', () => { }); it('Has links to all the certifications', function () { - cy.get(selectors.certifications).children().its('length').should('eq', 12); + cy.get(selectors.certifications).children().its('length').should('eq', 11); cy.wrap(certifications).each(cert => { cy.get(selectors.certifications).contains(cert); }); diff --git a/cypress/e2e/default/learn/index.js b/cypress/e2e/default/learn/index.js index 63f0fc2c6dc..408dec4a8bc 100644 --- a/cypress/e2e/default/learn/index.js +++ b/cypress/e2e/default/learn/index.js @@ -8,7 +8,6 @@ const locations = { const superBlockNames = [ '(New) Responsive Web Design Certification', - 'Legacy Responsive Web Design Certification', 'JavaScript Algorithms and Data Structures Certification', 'Front End Development Libraries Certification', 'Data Visualization Certification', @@ -19,7 +18,8 @@ const superBlockNames = [ 'Data Analysis with Python Certification', 'Information Security Certification', 'Machine Learning with Python Certification', - 'Coding Interview Prep' + 'Coding Interview Prep', + 'Legacy Responsive Web Design Certification' ]; describe('Learn Landing page (not logged in)', () => { diff --git a/package.json b/package.json index f86bb3ab81b..91f9c801e93 100644 --- a/package.json +++ b/package.json @@ -101,6 +101,7 @@ "test:curriculum": "cd ./curriculum && npm test", "test-curriculum-full-output": "cd ./curriculum && npm run test:full-output", "test-client": "jest client", + "test-config": "jest config", "test-curriculum-js": "jest curriculum", "test-server": "jest api-server", "test-tools": "jest tools", diff --git a/tools/challenge-auditor/index.ts b/tools/challenge-auditor/index.ts index 202a0146c6f..564deb5a72a 100644 --- a/tools/challenge-auditor/index.ts +++ b/tools/challenge-auditor/index.ts @@ -7,10 +7,11 @@ import { config } from 'dotenv'; const envPath = resolve(__dirname, '../../.env'); config({ path: envPath }); -import { availableLangs, auditedCerts } from '../../config/i18n'; +import { availableLangs } from '../../config/i18n'; import { getChallengesForLang } from '../../curriculum/getChallenges'; import { SuperBlocks } from '../../config/certification-settings'; import { ChallengeNode } from '../../client/src/redux/prop-types'; +import { getAuditedSuperBlocks } from '../../config/superblock-order'; const superBlockFolderMap = { 'responsive-web-design': '01-responsive-web-design', @@ -89,7 +90,11 @@ void (async () => { ); for (const lang of langsToCheck) { console.log(`\n=== ${lang} ===`); - const certs = auditedCerts[lang as keyof typeof auditedCerts]; + const certs = getAuditedSuperBlocks({ + language: lang, + showNewCurriculum: process.env.SHOW_NEW_CURRICULUM, + showUpcomingChanges: process.env.SHOW_UPCOMING_CHANGES + }); const langCurriculumDirectory = join( process.cwd(), 'curriculum', diff --git a/tools/scripts/build/ensure-env.ts b/tools/scripts/build/ensure-env.ts index eac3cd37dc6..07bd2a20a97 100644 --- a/tools/scripts/build/ensure-env.ts +++ b/tools/scripts/build/ensure-env.ts @@ -2,7 +2,7 @@ import { spawn } from 'child_process'; import * as fs from 'fs'; import * as path from 'path'; -import { availableLangs } from '../../../config/i18n'; +import { availableLangs, Languages } from '../../../config/i18n'; import env from '../../../config/read-env'; const globalConfigPath = path.resolve(__dirname, '../../../config'); @@ -11,7 +11,7 @@ const { FREECODECAMP_NODE_ENV } = process.env; function checkClientLocale() { if (!process.env.CLIENT_LOCALE) throw Error('CLIENT_LOCALE is not set'); - if (!availableLangs.client.includes(process.env.CLIENT_LOCALE)) { + if (!availableLangs.client.includes(process.env.CLIENT_LOCALE as Languages)) { throw Error(` CLIENT_LOCALE, ${process.env.CLIENT_LOCALE}, is not an available language in config/i18n.ts @@ -23,7 +23,11 @@ function checkClientLocale() { function checkCurriculumLocale() { if (!process.env.CURRICULUM_LOCALE) throw Error('CURRICULUM_LOCALE is not set'); - if (!availableLangs.curriculum.includes(process.env.CURRICULUM_LOCALE)) { + if ( + !availableLangs.curriculum.includes( + process.env.CURRICULUM_LOCALE as Languages + ) + ) { throw Error(` CURRICULUM_LOCALE, ${process.env.CURRICULUM_LOCALE}, is not an available language in config/i18n.ts diff --git a/utils/is-audited.js b/utils/is-audited.js index 3bca0d21038..2a7a3e6c540 100644 --- a/utils/is-audited.js +++ b/utils/is-audited.js @@ -1,20 +1,11 @@ -// this can go once all certs have been audited. +const { getAuditedSuperBlocks } = require('../config/superblock-order'); -// Currently the auditing is going through Crowdin, so once a cert has been 100% -// proofread, we can add it in here. That means that translations can come -// through from Crowdin whenever they are done, but we don't show them on the -// client until we decide the entire cert is ready. - -// NOTE: certificates themselves (.yml files) are not currently being -// translated, but when they are they can be included by adding 'certificates' -// to the arrays below - -const { auditedCerts } = require('../config/i18n'); - -function isAuditedCert(lang, cert) { - if (!lang || !cert) +function isAuditedCert(language, superblock) { + if (!language || !superblock) throw Error('Both arguments must be provided for auditing'); - return lang === 'english' || auditedCerts[lang].includes(cert); + + const auditedSuperBlocks = getAuditedSuperBlocks(language); + return auditedSuperBlocks.includes(superblock); } exports.isAuditedCert = isAuditedCert;