feat(client/curriculum): release new superblocks, certs coming soon (#63574)

Co-authored-by: ahmad abdolsaheb <ahmad.abdolsaheb@gmail.com>
This commit is contained in:
Tom
2025-11-12 12:50:49 -06:00
committed by GitHub
parent 4352a5b628
commit 8ec4053a05
25 changed files with 520 additions and 101 deletions

View File

@@ -52,6 +52,8 @@ describe('certificate routes', () => {
isMachineLearningPyCertV7: false,
isCollegeAlgebraPyCertV8: false,
isFoundationalCSharpCertV8: false,
// isJavascriptCertV9: false,
// isRespWebDesignCertV9: false,
username: 'fcc'
}
});
@@ -241,6 +243,8 @@ describe('certificate routes', () => {
isMachineLearningPyCertV7: true,
isCollegeAlgebraPyCertV8: true,
isFoundationalCSharpCertV8: true,
// isJavascriptCertV9: true,
// isRespWebDesignCertV9: true,
isA2EnglishCert: true
}
});

View File

@@ -5087,6 +5087,7 @@
},
"javascript-v9": {
"title": "JavaScript Certification",
"note": "This certification is currently in development and will be available soon. We recommend completing the available courses below to prepare for the certification exam once it is released.",
"intro": [
"This course teaches you core JavaScript programming concepts such as working with variables, functions, objects, arrays, and control flow. You'll also learn how to manipulate the DOM, handle events, and apply techniques like asynchronous programming, functional programming, and accessibility best practices.",
"To qualify for the exam, you must complete the following projects:",
@@ -5101,6 +5102,12 @@
"javascript": "JavaScript",
"javascript-certification-exam": "JavaScript Certification Exam"
},
"module-intros": {
"javascript-certification-exam": {
"note": "Coming Winter 2025",
"intro": ["Pass this exam to earn your JavaScript Certification."]
}
},
"modules": {
"javascript-variables-and-strings": "Variables and Strings",
"javascript-booleans-and-numbers": "Booleans and Numbers",
@@ -7483,6 +7490,12 @@
"intro": [
"Learn the fundamentals of how web communication works through the HTTP request-response model, explore different types of web assets and responses, and understand how forms handle data submission using various HTTP methods."
]
},
"exam-back-end-development-and-apis-certification": {
"title": "Back End Development and APIs Certification Exam",
"intro": [
"Pass this exam to earn your Back End Development and APIs Certification"
]
}
}
},
@@ -7491,14 +7504,7 @@
"note": "If you were previously working through our full stack curriculum, don't worry - you're progress is saved. We split it into smaller certifications for you to earn along your journey. This certification is currently in development and will be available soon. Start earning the required certifications so you're ready when it launches.",
"intro": [
"This certification represents the culmination of your full stack developer journey. It demonstrates your ability to build complete, modern web applications from start to finish.",
"To qualify for the exam, you must earn the following certifications:",
"- Responsive Web Design Certification",
"- JavaScript Certification",
"- Front End Development Libraries Certification",
"- Python Certification",
"- Relational Databases Certification",
"- Back End Development and APIs Certification",
"Pass the exam to earn your Full Stack Developer Certification."
"To qualify for the exam, you must earn the certifications below. Pass the exam to earn your Full Stack Developer Certification."
],
"chapters": {
"certified-full-stack-developer-exam": "Certified Full Stack Developer Exam"
@@ -7510,7 +7516,7 @@
"certified-full-stack-developer-exam": {
"note": "Coming Late 2026",
"intro": [
"This will be a 90 question exam testing what you have learned throughout this certification."
"This exam will test what you have learned throughout the previous six certifications."
]
}
},
@@ -7658,6 +7664,7 @@
},
"responsive-web-design-v9": {
"title": "Responsive Web Design Certification",
"note": "This certification is currently in development and will be available soon. We recommend completing the available courses below to prepare for the certification exam once it is released.",
"intro": [
"This course teaches the fundamentals of HTML and CSS, including modern layout, design, accessibility, and responsive web development. You'll build practical projects and gain the skills to create professional, user-friendly webpages.",
"To qualify for the exam, you must complete the following projects:",
@@ -7673,6 +7680,14 @@
"css": "CSS",
"responsive-web-design-certification-exam": "Responsive Web Design Certification Exam"
},
"module-intros": {
"responsive-web-design-certification-exam": {
"note": "Coming Winter 2025",
"intro": [
"Pass this exam to earn your Responsive Web Design Certification."
]
}
},
"modules": {
"basic-html": "Basic HTML",
"semantic-html": "Semantic HTML",
@@ -9023,6 +9038,7 @@
"misc-text": {
"browse-other": "Browse our other free certifications",
"courses": "Courses",
"requirements": "Requirements",
"steps": "Steps",
"expand": "Expand course",
"collapse": "Collapse course",

View File

@@ -213,6 +213,7 @@
"next-heading": "Try our beta curriculum:",
"upcoming-heading": "Upcoming curriculum:",
"catalog-heading": "Explore our Catalog:",
"fsd-restructure-note": "If you were previously working through our Certified Full Stack Developer curriculum, don't worry - your progress is saved. We've split it into smaller certifications you can earn along your journey.",
"archive-link": "Looking for older coursework? Check out <0>our archive page</0>.",
"faq": "Frequently asked questions:",
"faqs": [
@@ -1227,6 +1228,18 @@
"a2-english-for-developers-cert": "A2 English for Developers Certification",
"b1-english-for-developers": "B1 English for Developers",
"b1-english-for-developers-cert": "B1 English for Developers Certification",
"responsive-web-design-v9": "Responsive Web Design",
"responsive-web-design-v9-cert": "Responsive Web Design Certification",
"javascript-v9": "JavaScript",
"javascript-v9-cert": "JavaScript Certification",
"front-end-development-libraries-v9": "Front End Development Libraries",
"front-end-development-libraries-v9-cert": "Front End Development Libraries Certification",
"python-v9": "Python",
"python-v9-cert": "Python Certification",
"relational-databases-v9": "Relational Database",
"relational-databases-v9-cert": "Relational Database Certification",
"back-end-development-and-apis-v9": "Back End Development and APIs",
"back-end-development-and-apis-v9-cert": "Back End Development and APIs Certification",
"full-stack-developer-v9": "Full Stack Developer",
"full-stack-developer-v9-cert": "Full Stack Developer Certification",
"a1-professional-spanish": "A1 Professional Spanish",

View File

@@ -113,6 +113,9 @@ function Map({ forLanding = false }: MapProps) {
<h2 className={forLanding ? 'big-heading' : ''}>
{t(superBlockHeadings[stage])}
</h2>
{stage === SuperBlockStage.Core && (
<p>{t('landing.fsd-restructure-note')}</p>
)}
<ul key={stage}>
{superblocks.map(superblock => (
<MapLi

View File

@@ -18,6 +18,7 @@ import { type Module } from '../../../../../shared-dist/config/modules';
import envData from '../../../../config/env.json';
import Block from './block';
import CheckMark from './check-mark';
import { default as BlockLabelComponent } from './block-label';
import './super-block-accordion.css';
@@ -107,9 +108,12 @@ const Chapter = ({
</span>
<ChapterIcon className='map-icon' chapter={dashedName as FsdChapters} />
{chapterLabel}
{isLinkChapter && examSlug && (
<BlockLabelComponent blockLabel={BlockLabel.exam} />
)}
</div>
<div className='chapter-button-right'>
{!comingSoon && (
{!comingSoon && !isLinkChapter && (
<span className='chapter-steps'>
{t('learn.steps-completed', {
totalSteps,

View File

@@ -0,0 +1,221 @@
import React, { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { Spacer } from '@freecodecamp/ui';
import {
certificationCollectionSuperBlocks,
chapterBasedSuperBlocks,
SuperBlocks
} from '../../../../../shared-dist/config/curriculum';
import type { CertTitle } from '../../../../config/cert-and-project-map';
import type {
ChapterBasedSuperBlockStructure,
ClaimedCertifications,
User
} from '../../../redux/prop-types';
import type {
BlockLabel,
BlockLayouts
} from '../../../../../shared-dist/config/blocks';
import { SuperBlockIcon } from '../../../assets/superblock-icon';
import { Link } from '../../../components/helpers';
import {
certSlugTypeMap,
certificationRequirements,
superBlockToCertMap
} from '../../../../../shared-dist/config/certification-settings';
import CheckMark from './check-mark';
import Block from './block';
import CertChallenge from './cert-challenge';
import { SuperBlockAccordion } from './super-block-accordion';
import './super-block-accordion.css';
type Challenge = {
block: string;
blockLabel: BlockLabel;
blockLayout: BlockLayouts;
challengeType: number;
dashedName: string;
fields: { slug: string };
id: string;
module: string;
order: number;
superBlock: SuperBlocks;
title: string;
};
type SuperBlockMapProps = {
certification: string;
completedChallengeIds: string[];
disabledBlocks: string[];
initialExpandedBlock: string;
showCertification: boolean;
structure?: ChapterBasedSuperBlockStructure;
superBlock: SuperBlocks;
superBlockChallenges: Challenge[];
title: CertTitle;
user: User | null;
};
const BlockList = ({
certification,
disabledBlocks,
showCertification,
superBlock,
superBlockChallenges,
title,
user
}: {
certification: string;
disabledBlocks: string[];
showCertification: boolean;
superBlock: SuperBlocks;
superBlockChallenges: Challenge[];
title: CertTitle;
user: User | null;
}) => {
const visibleBlocks = useMemo(() => {
const uniqueBlocks = Array.from(
new Set(superBlockChallenges.map(({ block }) => block))
);
return uniqueBlocks.filter(block => !disabledBlocks.includes(block));
}, [disabledBlocks, superBlockChallenges]);
return (
<div className='block-ui'>
{visibleBlocks.map(block => {
const blockChallenges = superBlockChallenges.filter(
challenge => challenge.block === block
);
const blockLabel = blockChallenges[0]?.blockLabel ?? null;
if (!blockChallenges.length) return null;
return (
<Block
key={block}
block={block}
blockLabel={blockLabel}
challenges={blockChallenges}
superBlock={superBlock}
/>
);
})}
{showCertification && !!user && (
<CertChallenge
certification={certification}
superBlock={superBlock}
title={title}
user={user}
/>
)}
</div>
);
};
export const SuperBlockMap = ({
certification,
completedChallengeIds,
disabledBlocks,
initialExpandedBlock,
showCertification,
structure,
superBlock,
superBlockChallenges,
title,
user
}: SuperBlockMapProps) => {
const { t } = useTranslation();
if (chapterBasedSuperBlocks.includes(superBlock)) {
if (!structure) return null;
const getRequirementItems = () => {
const certificationForSuperBlock = superBlockToCertMap[superBlock];
const requirementsLookup = certificationRequirements as Partial<
Record<string, SuperBlocks[]>
>;
const requirements: SuperBlocks[] =
(certificationForSuperBlock &&
requirementsLookup[certificationForSuperBlock]) ??
[];
const requirementItems = requirements.map((requirement: SuperBlocks) => {
const requirementTitle = t(`intro:${requirement}.title`);
const requirementLink = `/learn/${requirement}/`;
const certSlug = superBlockToCertMap[requirement];
const certFlagLookup = certSlugTypeMap as Record<
string,
keyof ClaimedCertifications
>;
const certFlagKey = certSlug ? certFlagLookup[certSlug] : undefined;
const isRequirementComplete = Boolean(
certFlagKey && user?.[certFlagKey]
);
return (
<li className='chapter requirement' key={requirement}>
<Link
className='chapter-button'
data-playwright-test-label='requirement-button'
to={requirementLink}
>
<div className='chapter-button-left'>
<span className='checkmark-wrap chapter-checkmark-wrap'>
<CheckMark isCompleted={isRequirementComplete} />
</span>
<SuperBlockIcon className='map-icon' superBlock={requirement} />
{requirementTitle}
</div>
</Link>
</li>
);
});
return requirementItems;
};
return (
<>
{certificationCollectionSuperBlocks.includes(superBlock) && (
<>
<ul className='super-block-accordion requirement-list'>
{getRequirementItems()}
</ul>
<Spacer size='m' />
<h2 className='text-center big-subheading'>
{t(`intro:misc-text.courses`)}
</h2>
<Spacer size='m' />
</>
)}
<SuperBlockAccordion
challenges={superBlockChallenges}
superBlock={superBlock}
structure={structure}
chosenBlock={initialExpandedBlock}
completedChallengeIds={completedChallengeIds}
/>
</>
);
}
return (
<BlockList
certification={certification}
disabledBlocks={disabledBlocks}
showCertification={showCertification}
superBlock={superBlock}
superBlockChallenges={superBlockChallenges}
title={title}
user={user}
/>
);
};
SuperBlockMap.displayName = 'SuperBlockMap';
export default SuperBlockMap;

View File

@@ -1,7 +1,7 @@
import i18next from 'i18next';
import { WindowLocation } from '@gatsbyjs/reach-router';
import { graphql } from 'gatsby';
import { uniq, isEmpty, last } from 'lodash-es';
import { isEmpty, last } from 'lodash-es';
import React, { useEffect, memo, useMemo } from 'react';
import Helmet from 'react-helmet';
import { useTranslation, withTranslation } from 'react-i18next';
@@ -13,8 +13,8 @@ import { Container, Col, Row, Spacer } from '@freecodecamp/ui';
import { useFeatureValue } from '@growthbook/growthbook-react';
import {
chapterBasedSuperBlocks,
SuperBlocks
SuperBlocks,
certificationCollectionSuperBlocks
} from '../../../../shared-dist/config/curriculum';
import DonateModal from '../../components/Donation/donation-modal';
import Login from '../../components/Header/components/login';
@@ -39,12 +39,10 @@ import {
BlockLayouts,
BlockLabel
} from '../../../../shared-dist/config/blocks';
import Block from './components/block';
import CertChallenge from './components/cert-challenge';
import LegacyLinks from './components/legacy-links';
import HelpTranslate from './components/help-translate';
import SuperBlockIntro from './components/super-block-intro';
import { SuperBlockAccordion } from './components/super-block-accordion';
import SuperBlockMap from './components/super-block-map';
import { resetExpansion, toggleBlock } from './redux';
import './intro.css';
@@ -178,8 +176,6 @@ const SuperBlockIntroductionPage = (props: SuperBlockProps) => {
() => allChallenges.filter(c => c.superBlock === superBlock),
[allChallenges, superBlock]
);
const blocks = uniq(superBlockChallenges.map(({ block }) => block));
const completedChallenges = useMemo(
() =>
(user?.completedChallenges ?? []).filter(completedChallenge =>
@@ -239,7 +235,9 @@ const SuperBlockIntroductionPage = (props: SuperBlockProps) => {
}
}
return blocks[0];
const fallbackBlock = superBlockChallenges[0]?.block;
return fallbackBlock ?? '';
};
const initializeExpandedState = () => {
@@ -279,51 +277,25 @@ const SuperBlockIntroductionPage = (props: SuperBlockProps) => {
<HelpTranslate superBlock={superBlock} />
<Spacer size='l' />
<h2 className='text-center big-subheading'>
{t(`intro:misc-text.courses`)}
{certificationCollectionSuperBlocks.includes(superBlock)
? t(`intro:misc-text.requirements`)
: t(`intro:misc-text.courses`)}
</h2>
<Spacer size='m' />
{chapterBasedSuperBlocks.includes(superBlock) ? (
<SuperBlockAccordion
challenges={superBlockChallenges}
superBlock={superBlock}
structure={
currentSuperBlockStructure as ChapterBasedSuperBlockStructure
}
chosenBlock={initialExpandedBlock}
completedChallengeIds={completedChallenges.map(c => c.id)}
/>
) : (
<div className='block-ui'>
{blocks
.filter(block => {
return !disabledBlocksFeature.includes(block);
})
.map(block => {
const blockChallenges = superBlockChallenges.filter(
c => c.block === block
);
const blockLabel = blockChallenges[0].blockLabel;
return (
<Block
key={block}
block={block}
blockLabel={blockLabel}
challenges={blockChallenges}
superBlock={superBlock}
/>
);
})}
{showCertification && !!user && (
<CertChallenge
certification={certification}
superBlock={superBlock}
title={title}
user={user}
/>
)}
</div>
)}
<SuperBlockMap
certification={certification}
completedChallengeIds={completedChallenges.map(c => c.id)}
disabledBlocks={disabledBlocksFeature}
initialExpandedBlock={initialExpandedBlock}
showCertification={showCertification}
structure={
currentSuperBlockStructure as ChapterBasedSuperBlockStructure
}
superBlock={superBlock}
superBlockChallenges={superBlockChallenges}
title={title}
user={user}
/>
{!isSignedIn && !signInLoading && (
<>
<Spacer size='l' />

View File

@@ -327,9 +327,11 @@
},
{
"chapterType": "exam",
"comingSoon": true,
"dashedName": "javascript-certification-exam",
"modules": [
{
"comingSoon": true,
"dashedName": "javascript-certification-exam",
"blocks": ["exam-javascript-certification"]
}

View File

@@ -296,9 +296,11 @@
},
{
"chapterType": "exam",
"comingSoon": true,
"dashedName": "responsive-web-design-certification-exam",
"modules": [
{
"comingSoon": true,
"dashedName": "responsive-web-design-certification-exam",
"blocks": ["exam-responsive-web-design-certification"]
}

View File

@@ -16,7 +16,7 @@ test.describe('Public profile certifications', () => {
await expect(
page.getByRole('link', { name: /View.+Certification/ })
).toHaveCount(20);
).toHaveCount(22);
});
test('Should show claimed certifications if the username includes uppercase characters', async ({
@@ -48,7 +48,7 @@ test.describe('Public profile certifications', () => {
await page.waitForURL('/certifiedboozer');
await expect(
page.getByRole('link', { name: /View.+Certification/ })
).toHaveCount(20);
).toHaveCount(22);
});
test.afterAll(() => {

View File

@@ -318,7 +318,7 @@ test.describe('Donation modal appearance logic - Certified user claiming a new b
page
}) => {
await page.goto(
'/learn/full-stack-developer/review-basic-html/basic-html-review'
'/learn/responsive-web-design-v9/review-basic-html/basic-html-review'
);
await page.getByRole('checkbox', { name: /Review/ }).click();
@@ -334,7 +334,7 @@ test.describe('Donation modal appearance logic - Certified user claiming a new b
test('should not appear if FSD review module is completed', async ({
page
}) => {
await page.goto('/learn/full-stack-developer/review-html/review-html');
await page.goto('/learn/responsive-web-design-v9/review-html/review-html');
await page.getByRole('checkbox', { name: /Review/ }).click();
await page.getByRole('button', { name: 'Submit', exact: true }).click();
await page.getByRole('button', { name: /Submit and go/ }).click();
@@ -361,7 +361,7 @@ test.describe('Donation modal appearance logic - Certified user claiming a new m
// This lecture is not added to the seed data, so it is not completed.
// By completing this lecture, we claim both the block and its module.
await page.goto(
'/learn/full-stack-developer/lecture-working-with-code-editors-and-ides/what-are-some-good-vs-code-extensions-you-can-use-in-your-editor'
'/learn/relational-databases-v9/lecture-working-with-code-editors-and-ides/what-are-some-good-vs-code-extensions-you-can-use-in-your-editor'
);
// Wait for the page content to render

View File

@@ -0,0 +1,82 @@
import { test, expect } from '@playwright/test';
const requiredCerts = [
{
text: 'Responsive Web Design Certification',
slug: '/learn/responsive-web-design-v9/'
},
{
text: 'JavaScript Certification',
slug: '/learn/javascript-v9/'
},
{
text: 'Front End Development Libraries Certification',
slug: '/learn/front-end-development-libraries-v9/'
},
{
text: 'Python Certification',
slug: '/learn/python-v9/'
},
{
text: 'Relational Databases Certification',
slug: '/learn/relational-databases-v9/'
},
{
text: 'Back End Development and APIs Certification',
slug: '/learn/back-end-development-and-apis-v9/'
}
];
test.describe('Full Stack Developer V9 superBlock page', () => {
test('lists and links to requirements', async ({ page }) => {
await page.goto('/learn/full-stack-developer-v9/');
const reqList = page.locator('.requirement-list');
await expect(reqList).toBeVisible();
const reqLinks = reqList.locator('.chapter.requirement .chapter-button');
await expect(reqLinks).toHaveCount(requiredCerts.length);
for (let i = 0; i < requiredCerts.length; i++) {
const reqLink = reqLinks.nth(i);
await expect(reqLink).toBeVisible();
await expect(reqLink).toContainText(requiredCerts[i].text);
await expect(reqLink).toHaveAttribute('href', requiredCerts[i].slug);
}
});
if (process.env.SHOW_UPCOMING_CHANGES === 'true') {
test('shows the exam', async ({ page }) => {
await page.goto('/learn/full-stack-developer-v9/');
const examChapterButton = page.locator('.chapter .chapter-button', {
hasText: /certified full stack developer exam/i
});
await expect(examChapterButton).toBeVisible();
await expect(examChapterButton).toHaveAttribute(
'href',
'/learn/full-stack-developer-v9/exam-certified-full-stack-developer/exam-certified-full-stack-developer'
);
});
} else {
test('shows the exam module and coming soon text', async ({ page }) => {
await page.goto('/learn/full-stack-developer-v9/');
const examChapterButton = page.locator('.chapter .chapter-button', {
hasText: /certified full stack developer exam/i
});
await expect(examChapterButton).toBeVisible();
const examModuleButton = page.locator('.module-button', {
hasText: /certified full stack developer exam/i
});
await examModuleButton.click();
const moduleIntro = page.locator('.module-intro');
await expect(moduleIntro).toBeVisible();
await expect(moduleIntro).toContainText('Coming Late 2026');
await expect(moduleIntro).toContainText(
'This exam will test what you have learned throughout the previous six certifications.'
);
});
}
});

View File

@@ -25,7 +25,7 @@ const links = {
multipleChoiceQuestion:
'/learn/a2-english-for-developers/learn-greetings-in-your-first-day-at-the-office/task-7',
assignment:
'/learn/full-stack-developer/review-semantic-html/review-semantic-html'
'/learn/responsive-web-design-v9/review-semantic-html/review-semantic-html'
};
const titles = {

View File

@@ -26,7 +26,7 @@ interface PageData {
}
const challengePath =
'/learn/full-stack-developer/lecture-what-is-css/what-are-some-default-browser-styles-applied-to-html';
'/learn/responsive-web-design-v9/lecture-what-is-css/what-are-some-default-browser-styles-applied-to-html';
const challengeTitle = 'Test Challenge Title';

View File

@@ -18,7 +18,13 @@ const landingPageElements = {
} as const;
const nonArchivedSuperBlocks = [
intro[SuperBlocks.FullStackDeveloper].title,
intro[SuperBlocks.RespWebDesignV9].title,
intro[SuperBlocks.JsV9].title,
intro[SuperBlocks.FrontEndDevLibsV9].title,
intro[SuperBlocks.PythonV9].title,
intro[SuperBlocks.RelationalDbV9].title,
intro[SuperBlocks.BackEndDevApisV9].title,
intro[SuperBlocks.FullStackDeveloperV9].title,
intro[SuperBlocks.A2English].title,
intro[SuperBlocks.B1English].title,
intro[SuperBlocks.TheOdinProject].title,

View File

@@ -7,7 +7,31 @@ test.beforeEach(async ({ page }) => {
const LANDING_PAGE_LINKS = [
{
slug: 'full-stack-developer',
slug: 'responsive-web-design-v9',
name: 'Responsive Web Design Certification'
},
{
slug: 'javascript-v9',
name: 'JavaScript Certification'
},
{
slug: 'front-end-development-libraries-v9',
name: 'Front End Development Libraries Certification'
},
{
slug: 'python-v9',
name: 'Python Certification'
},
{
slug: 'relational-databases-v9',
name: 'Relational Databases Certification'
},
{
slug: 'back-end-development-and-apis-v9',
name: 'Back End Development and APIs Certification'
},
{
slug: 'full-stack-developer-v9',
name: 'Certified Full Stack Developer Curriculum'
},
{
@@ -40,7 +64,7 @@ test.describe('Map Component', () => {
page.getByText(translations.landing['interview-prep-heading'])
).toBeVisible();
const curriculumBtns = page.getByTestId('curriculum-map-button');
await expect(curriculumBtns).toHaveCount(8);
await expect(curriculumBtns).toHaveCount(14);
for (const { name, slug } of LANDING_PAGE_LINKS) {
const superblockLink = page.getByRole('link', {

View File

@@ -4,7 +4,7 @@ import translations from '../client/i18n/locales/english/translations.json';
const pageWithSpeaking =
'/learn/b1-english-for-developers/learn-about-adverbial-phrases/task-19';
const pageWithoutSpeaking =
'/learn/full-stack-developer/lecture-what-is-css/what-is-the-basic-anatomy-of-a-css-rule';
'/learn/responsive-web-design-v9/lecture-what-is-css/what-is-the-basic-anatomy-of-a-css-rule';
test.describe('Multiple Choice Question Challenge - With Speaking Modal', () => {
test.beforeEach(async ({ page }) => {

View File

@@ -52,7 +52,7 @@ test.describe('Should be shown automatically', () => {
test.describe('Should be shown manually', () => {
test.beforeEach(async ({ page }) => {
const urlWithProjectPreview =
'/learn/full-stack-developer/lab-drum-machine/build-drum-machine';
'/learn/javascript-v9/lab-drum-machine/build-drum-machine';
await page.goto(urlWithProjectPreview);
});

View File

@@ -23,7 +23,8 @@ interface PageData {
};
}
const quizPath = '/learn/full-stack-developer/quiz-basic-html/quiz-basic-html';
const quizPath =
'/learn/responsive-web-design-v9/quiz-basic-html/quiz-basic-html';
test.describe('Quiz challenge', () => {
test.beforeEach(async ({ page }) => {
@@ -205,7 +206,7 @@ test.describe('Quiz challenge', () => {
// The navigation should be blocked, the user should stay on the same page
await expect(page).toHaveURL(
allowTrailingSlash(
'/learn/full-stack-developer/quiz-basic-html/quiz-basic-html'
'/learn/responsive-web-design-v9/quiz-basic-html/quiz-basic-html'
)
);
@@ -223,7 +224,7 @@ test.describe('Quiz challenge', () => {
.getByRole('button', { name: 'Yes, I want to leave the quiz' })
.click();
await page.waitForURL('/learn/full-stack-developer/#quiz-basic-html');
await page.waitForURL('/learn/responsive-web-design-v9/#quiz-basic-html');
await expect(
page.getByRole('heading', { level: 3, name: 'Basic HTML Quiz' })
).toBeVisible();
@@ -242,7 +243,7 @@ test.describe('Quiz challenge', () => {
.getByRole('button', { name: 'Yes, I want to leave the quiz' })
.click();
await page.waitForURL('/learn/full-stack-developer/#quiz-basic-html');
await page.waitForURL('/learn/responsive-web-design-v9/#quiz-basic-html');
await expect(
page.getByRole('heading', { level: 3, name: 'Basic HTML Quiz' })
).toBeVisible();

View File

@@ -106,7 +106,9 @@ test.describe('Super Block Page - Authenticated User', () => {
test('should expand the correct block when user goes to the page from breadcrumb click', async ({
page
}) => {
await page.goto(`/learn/full-stack-developer/workshop-cafe-menu/step-2`);
await page.goto(
`/learn/responsive-web-design-v9/workshop-cafe-menu/step-2`
);
await page
.getByRole('link', {
@@ -114,7 +116,9 @@ test.describe('Super Block Page - Authenticated User', () => {
})
.click();
await page.waitForURL('/learn/full-stack-developer/#workshop-cafe-menu');
await page.waitForURL(
'/learn/responsive-web-design-v9/#workshop-cafe-menu'
);
// Chapter
await expect(
@@ -146,7 +150,7 @@ test.describe('Super Block Page - Authenticated User', () => {
);
});
await page.goto('/learn/full-stack-developer');
await page.goto('/learn/responsive-web-design-v9');
// HTML chapter
await expect(
@@ -173,7 +177,7 @@ test.describe('Super Block Page - Authenticated User', () => {
}) => {
test.setTimeout(20000);
await page.goto('/learn/full-stack-developer');
await page.goto('/learn/responsive-web-design-v9');
// HTML chapter
await expect(
@@ -194,7 +198,9 @@ test.describe('Super Block Page - Authenticated User', () => {
})
).toHaveAttribute('aria-expanded', 'true');
await page.goto('/learn/full-stack-developer/workshop-blog-page/step-2');
await page.goto(
'/learn/responsive-web-design-v9/workshop-blog-page/step-2'
);
// Wait for the page to finish loading so that the current challenge ID can be registered.
await expect(
@@ -202,7 +208,7 @@ test.describe('Super Block Page - Authenticated User', () => {
).toBeVisible();
// Go back to the super block page
await page.goto('/learn/full-stack-developer');
await page.goto('/learn/responsive-web-design-v9');
// Semantic HTML module
await expect(
@@ -248,7 +254,7 @@ test.describe('Super Block Page - Unauthenticated User', () => {
test('should expand the first block of the super block', async ({
page
}) => {
await page.goto('/learn/full-stack-developer');
await page.goto('/learn/responsive-web-design-v9');
// First chapter
await expect(

View File

@@ -339,6 +339,19 @@ export const superBlockToCertMap: {
[SuperBlocks.FullStackDeveloper]: null
};
export const certificationRequirements: Partial<
Record<Certification, SuperBlocks[]>
> = {
[Certification.FullStackDeveloperV9]: [
SuperBlocks.RespWebDesignV9,
SuperBlocks.JsV9,
SuperBlocks.FrontEndDevLibsV9,
SuperBlocks.PythonV9,
SuperBlocks.RelationalDbV9,
SuperBlocks.BackEndDevApisV9
]
};
export type CertSlug = (typeof Certification)[keyof typeof Certification];
export const linkedInCredentialIds = {

View File

@@ -104,7 +104,15 @@ export type StageMap = {
// Groups of superblocks in learn map. This should include all superblocks.
export const superBlockStages: StageMap = {
[SuperBlockStage.Core]: [SuperBlocks.FullStackDeveloper],
[SuperBlockStage.Core]: [
SuperBlocks.RespWebDesignV9,
SuperBlocks.JsV9,
SuperBlocks.FrontEndDevLibsV9,
SuperBlocks.PythonV9,
SuperBlocks.RelationalDbV9,
SuperBlocks.BackEndDevApisV9,
SuperBlocks.FullStackDeveloperV9
],
[SuperBlockStage.English]: [SuperBlocks.A2English, SuperBlocks.B1English],
[SuperBlockStage.Professional]: [SuperBlocks.FoundationalCSharp],
[SuperBlockStage.Extra]: [
@@ -133,18 +141,12 @@ export const superBlockStages: StageMap = {
[SuperBlockStage.Next]: [],
[SuperBlockStage.Upcoming]: [
SuperBlocks.FullStackOpen,
SuperBlocks.RespWebDesignV9,
SuperBlocks.JsV9,
SuperBlocks.FrontEndDevLibsV9,
SuperBlocks.PythonV9,
SuperBlocks.RelationalDbV9,
SuperBlocks.BackEndDevApisV9,
SuperBlocks.FullStackDeveloperV9,
SuperBlocks.A1Spanish,
SuperBlocks.A2Spanish,
SuperBlocks.A2Chinese,
SuperBlocks.A1Chinese,
SuperBlocks.DevPlayground
SuperBlocks.DevPlayground,
SuperBlocks.FullStackDeveloper
],
// Catalog is treated like upcoming for now
// Add catalog superBlocks to catalog.ts when adding new superBlocks
@@ -428,6 +430,11 @@ export const chapterBasedSuperBlocks = [
];
Object.freeze(chapterBasedSuperBlocks);
export const certificationCollectionSuperBlocks = [
SuperBlocks.FullStackDeveloperV9
];
Object.freeze(certificationCollectionSuperBlocks);
type Config = {
showUpcomingChanges: boolean;
};

View File

@@ -40,6 +40,13 @@ interface Block<T> {
const ver = 'v1';
export const orderedSuperBlockInfo = [
{ dashedName: SuperBlocks.RespWebDesignV9, public: false },
{ dashedName: SuperBlocks.JsV9, public: false },
{ dashedName: SuperBlocks.FrontEndDevLibsV9, public: false },
{ dashedName: SuperBlocks.PythonV9, public: false },
{ dashedName: SuperBlocks.RelationalDbV9, public: false },
{ dashedName: SuperBlocks.BackEndDevApisV9, public: false },
{ dashedName: SuperBlocks.FullStackDeveloperV9, public: false },
{ dashedName: SuperBlocks.RespWebDesignNew, public: true },
{ dashedName: SuperBlocks.DataAnalysisPy, public: true },
{ dashedName: SuperBlocks.MachineLearningPy, public: true },
@@ -49,7 +56,6 @@ export const orderedSuperBlockInfo = [
{ dashedName: SuperBlocks.TheOdinProject, public: true },
{ dashedName: SuperBlocks.RespWebDesign, public: true },
{ dashedName: SuperBlocks.PythonForEverybody, public: true },
{ dashedName: SuperBlocks.FullStackDeveloper, public: false },
{ dashedName: SuperBlocks.JsAlgoDataStructNew, public: false },
{ dashedName: SuperBlocks.FrontEndDevLibs, public: false },
{ dashedName: SuperBlocks.DataVis, public: false },

View File

@@ -3,7 +3,10 @@ import { resolve, dirname } from 'path';
import { omit } from 'lodash';
import { submitTypes } from '../../../shared-dist/config/challenge-types';
import { type ChallengeNode } from '../../../client/src/redux/prop-types';
import { SuperBlocks } from '../../../shared-dist/config/curriculum';
import {
SuperBlocks,
chapterBasedSuperBlocks
} from '../../../shared-dist/config/curriculum';
import type { Chapter } from '../../../shared-dist/config/chapters';
import { getSuperblockStructure } from '../../../curriculum/src/file-handler';
import { patchBlock } from './patches';
@@ -114,9 +117,39 @@ const intros = JSON.parse(
export const orderedSuperBlockInfo: OrderedSuperBlocks = {
[SuperBlockStage.Core]: [
{
dashedName: SuperBlocks.FullStackDeveloper,
dashedName: SuperBlocks.RespWebDesignV9,
public: false,
title: intros[SuperBlocks.FullStackDeveloper].title
title: intros[SuperBlocks.RespWebDesignV9].title
},
{
dashedName: SuperBlocks.JsV9,
public: false,
title: intros[SuperBlocks.JsV9].title
},
{
dashedName: SuperBlocks.FrontEndDevLibsV9,
public: false,
title: intros[SuperBlocks.FrontEndDevLibsV9].title
},
{
dashedName: SuperBlocks.PythonV9,
public: false,
title: intros[SuperBlocks.PythonV9].title
},
{
dashedName: SuperBlocks.RelationalDbV9,
public: false,
title: intros[SuperBlocks.RelationalDbV9].title
},
{
dashedName: SuperBlocks.BackEndDevApisV9,
public: false,
title: intros[SuperBlocks.BackEndDevApisV9].title
},
{
dashedName: SuperBlocks.FullStackDeveloperV9,
public: false,
title: intros[SuperBlocks.FullStackDeveloperV9].title
}
],
@@ -273,7 +306,7 @@ export function buildExtCurriculumDataV2(
});
for (const superBlockKey of superBlockKeys) {
if (superBlockKey === SuperBlocks.FullStackDeveloper) {
if (chapterBasedSuperBlocks.includes(superBlockKey)) {
buildChapterBasedCurriculum(superBlockKey);
} else {
buildBlockBasedCurriculum(superBlockKey);
@@ -284,7 +317,7 @@ export function buildExtCurriculumDataV2(
}
function buildChapterBasedCurriculum(superBlockKey: SuperBlocks) {
const { chapters } = getSuperblockStructure('full-stack-developer') as {
const { chapters } = getSuperblockStructure(superBlockKey) as {
chapters: Chapter[];
};
const blocksWithData = curriculum[superBlockKey].blocks;

View File

@@ -91,7 +91,11 @@ const chapterBasedCurriculumSchema = Joi.object().pattern(
modules: Joi.array()
.items(
Joi.object().keys({
moduleType: Joi.valid('review', 'exam').optional(),
moduleType: Joi.valid(
'review',
'exam',
'cert-project'
).optional(),
name: Joi.string().required(),
comingSoon: Joi.boolean().optional(),
dashedName: Joi.string().regex(slugRE).required(),