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

View File

@@ -5087,6 +5087,7 @@
}, },
"javascript-v9": { "javascript-v9": {
"title": "JavaScript Certification", "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": [ "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.", "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:", "To qualify for the exam, you must complete the following projects:",
@@ -5101,6 +5102,12 @@
"javascript": "JavaScript", "javascript": "JavaScript",
"javascript-certification-exam": "JavaScript Certification Exam" "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": { "modules": {
"javascript-variables-and-strings": "Variables and Strings", "javascript-variables-and-strings": "Variables and Strings",
"javascript-booleans-and-numbers": "Booleans and Numbers", "javascript-booleans-and-numbers": "Booleans and Numbers",
@@ -7483,6 +7490,12 @@
"intro": [ "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." "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.", "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": [ "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.", "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:", "To qualify for the exam, you must earn the certifications below. Pass the exam to earn your Full Stack Developer Certification."
"- 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."
], ],
"chapters": { "chapters": {
"certified-full-stack-developer-exam": "Certified Full Stack Developer Exam" "certified-full-stack-developer-exam": "Certified Full Stack Developer Exam"
@@ -7510,7 +7516,7 @@
"certified-full-stack-developer-exam": { "certified-full-stack-developer-exam": {
"note": "Coming Late 2026", "note": "Coming Late 2026",
"intro": [ "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": { "responsive-web-design-v9": {
"title": "Responsive Web Design Certification", "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": [ "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.", "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:", "To qualify for the exam, you must complete the following projects:",
@@ -7673,6 +7680,14 @@
"css": "CSS", "css": "CSS",
"responsive-web-design-certification-exam": "Responsive Web Design Certification Exam" "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": { "modules": {
"basic-html": "Basic HTML", "basic-html": "Basic HTML",
"semantic-html": "Semantic HTML", "semantic-html": "Semantic HTML",
@@ -9023,6 +9038,7 @@
"misc-text": { "misc-text": {
"browse-other": "Browse our other free certifications", "browse-other": "Browse our other free certifications",
"courses": "Courses", "courses": "Courses",
"requirements": "Requirements",
"steps": "Steps", "steps": "Steps",
"expand": "Expand course", "expand": "Expand course",
"collapse": "Collapse course", "collapse": "Collapse course",

View File

@@ -213,6 +213,7 @@
"next-heading": "Try our beta curriculum:", "next-heading": "Try our beta curriculum:",
"upcoming-heading": "Upcoming curriculum:", "upcoming-heading": "Upcoming curriculum:",
"catalog-heading": "Explore our Catalog:", "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>.", "archive-link": "Looking for older coursework? Check out <0>our archive page</0>.",
"faq": "Frequently asked questions:", "faq": "Frequently asked questions:",
"faqs": [ "faqs": [
@@ -1227,6 +1228,18 @@
"a2-english-for-developers-cert": "A2 English for Developers Certification", "a2-english-for-developers-cert": "A2 English for Developers Certification",
"b1-english-for-developers": "B1 English for Developers", "b1-english-for-developers": "B1 English for Developers",
"b1-english-for-developers-cert": "B1 English for Developers Certification", "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": "Full Stack Developer",
"full-stack-developer-v9-cert": "Full Stack Developer Certification", "full-stack-developer-v9-cert": "Full Stack Developer Certification",
"a1-professional-spanish": "A1 Professional Spanish", "a1-professional-spanish": "A1 Professional Spanish",

View File

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

View File

@@ -18,6 +18,7 @@ import { type Module } from '../../../../../shared-dist/config/modules';
import envData from '../../../../config/env.json'; import envData from '../../../../config/env.json';
import Block from './block'; import Block from './block';
import CheckMark from './check-mark'; import CheckMark from './check-mark';
import { default as BlockLabelComponent } from './block-label';
import './super-block-accordion.css'; import './super-block-accordion.css';
@@ -107,9 +108,12 @@ const Chapter = ({
</span> </span>
<ChapterIcon className='map-icon' chapter={dashedName as FsdChapters} /> <ChapterIcon className='map-icon' chapter={dashedName as FsdChapters} />
{chapterLabel} {chapterLabel}
{isLinkChapter && examSlug && (
<BlockLabelComponent blockLabel={BlockLabel.exam} />
)}
</div> </div>
<div className='chapter-button-right'> <div className='chapter-button-right'>
{!comingSoon && ( {!comingSoon && !isLinkChapter && (
<span className='chapter-steps'> <span className='chapter-steps'>
{t('learn.steps-completed', { {t('learn.steps-completed', {
totalSteps, 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 i18next from 'i18next';
import { WindowLocation } from '@gatsbyjs/reach-router'; import { WindowLocation } from '@gatsbyjs/reach-router';
import { graphql } from 'gatsby'; 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 React, { useEffect, memo, useMemo } from 'react';
import Helmet from 'react-helmet'; import Helmet from 'react-helmet';
import { useTranslation, withTranslation } from 'react-i18next'; 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 { useFeatureValue } from '@growthbook/growthbook-react';
import { import {
chapterBasedSuperBlocks, SuperBlocks,
SuperBlocks certificationCollectionSuperBlocks
} from '../../../../shared-dist/config/curriculum'; } from '../../../../shared-dist/config/curriculum';
import DonateModal from '../../components/Donation/donation-modal'; import DonateModal from '../../components/Donation/donation-modal';
import Login from '../../components/Header/components/login'; import Login from '../../components/Header/components/login';
@@ -39,12 +39,10 @@ import {
BlockLayouts, BlockLayouts,
BlockLabel BlockLabel
} from '../../../../shared-dist/config/blocks'; } from '../../../../shared-dist/config/blocks';
import Block from './components/block';
import CertChallenge from './components/cert-challenge';
import LegacyLinks from './components/legacy-links'; import LegacyLinks from './components/legacy-links';
import HelpTranslate from './components/help-translate'; import HelpTranslate from './components/help-translate';
import SuperBlockIntro from './components/super-block-intro'; 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 { resetExpansion, toggleBlock } from './redux';
import './intro.css'; import './intro.css';
@@ -178,8 +176,6 @@ const SuperBlockIntroductionPage = (props: SuperBlockProps) => {
() => allChallenges.filter(c => c.superBlock === superBlock), () => allChallenges.filter(c => c.superBlock === superBlock),
[allChallenges, superBlock] [allChallenges, superBlock]
); );
const blocks = uniq(superBlockChallenges.map(({ block }) => block));
const completedChallenges = useMemo( const completedChallenges = useMemo(
() => () =>
(user?.completedChallenges ?? []).filter(completedChallenge => (user?.completedChallenges ?? []).filter(completedChallenge =>
@@ -239,7 +235,9 @@ const SuperBlockIntroductionPage = (props: SuperBlockProps) => {
} }
} }
return blocks[0]; const fallbackBlock = superBlockChallenges[0]?.block;
return fallbackBlock ?? '';
}; };
const initializeExpandedState = () => { const initializeExpandedState = () => {
@@ -279,51 +277,25 @@ const SuperBlockIntroductionPage = (props: SuperBlockProps) => {
<HelpTranslate superBlock={superBlock} /> <HelpTranslate superBlock={superBlock} />
<Spacer size='l' /> <Spacer size='l' />
<h2 className='text-center big-subheading'> <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> </h2>
<Spacer size='m' /> <Spacer size='m' />
{chapterBasedSuperBlocks.includes(superBlock) ? ( <SuperBlockMap
<SuperBlockAccordion certification={certification}
challenges={superBlockChallenges} completedChallengeIds={completedChallenges.map(c => c.id)}
superBlock={superBlock} disabledBlocks={disabledBlocksFeature}
structure={ initialExpandedBlock={initialExpandedBlock}
currentSuperBlockStructure as ChapterBasedSuperBlockStructure showCertification={showCertification}
} structure={
chosenBlock={initialExpandedBlock} currentSuperBlockStructure as ChapterBasedSuperBlockStructure
completedChallengeIds={completedChallenges.map(c => c.id)} }
/> superBlock={superBlock}
) : ( superBlockChallenges={superBlockChallenges}
<div className='block-ui'> title={title}
{blocks user={user}
.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>
)}
{!isSignedIn && !signInLoading && ( {!isSignedIn && !signInLoading && (
<> <>
<Spacer size='l' /> <Spacer size='l' />

View File

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

View File

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

View File

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

View File

@@ -318,7 +318,7 @@ test.describe('Donation modal appearance logic - Certified user claiming a new b
page page
}) => { }) => {
await page.goto( 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(); 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 ({ test('should not appear if FSD review module is completed', async ({
page 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('checkbox', { name: /Review/ }).click();
await page.getByRole('button', { name: 'Submit', exact: true }).click(); await page.getByRole('button', { name: 'Submit', exact: true }).click();
await page.getByRole('button', { name: /Submit and go/ }).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. // 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. // By completing this lecture, we claim both the block and its module.
await page.goto( 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 // 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: multipleChoiceQuestion:
'/learn/a2-english-for-developers/learn-greetings-in-your-first-day-at-the-office/task-7', '/learn/a2-english-for-developers/learn-greetings-in-your-first-day-at-the-office/task-7',
assignment: assignment:
'/learn/full-stack-developer/review-semantic-html/review-semantic-html' '/learn/responsive-web-design-v9/review-semantic-html/review-semantic-html'
}; };
const titles = { const titles = {

View File

@@ -26,7 +26,7 @@ interface PageData {
} }
const challengePath = 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'; const challengeTitle = 'Test Challenge Title';

View File

@@ -18,7 +18,13 @@ const landingPageElements = {
} as const; } as const;
const nonArchivedSuperBlocks = [ 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.A2English].title,
intro[SuperBlocks.B1English].title, intro[SuperBlocks.B1English].title,
intro[SuperBlocks.TheOdinProject].title, intro[SuperBlocks.TheOdinProject].title,

View File

@@ -7,7 +7,31 @@ test.beforeEach(async ({ page }) => {
const LANDING_PAGE_LINKS = [ 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' name: 'Certified Full Stack Developer Curriculum'
}, },
{ {
@@ -40,7 +64,7 @@ test.describe('Map Component', () => {
page.getByText(translations.landing['interview-prep-heading']) page.getByText(translations.landing['interview-prep-heading'])
).toBeVisible(); ).toBeVisible();
const curriculumBtns = page.getByTestId('curriculum-map-button'); 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) { for (const { name, slug } of LANDING_PAGE_LINKS) {
const superblockLink = page.getByRole('link', { const superblockLink = page.getByRole('link', {

View File

@@ -4,7 +4,7 @@ import translations from '../client/i18n/locales/english/translations.json';
const pageWithSpeaking = const pageWithSpeaking =
'/learn/b1-english-for-developers/learn-about-adverbial-phrases/task-19'; '/learn/b1-english-for-developers/learn-about-adverbial-phrases/task-19';
const pageWithoutSpeaking = 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.describe('Multiple Choice Question Challenge - With Speaking Modal', () => {
test.beforeEach(async ({ page }) => { test.beforeEach(async ({ page }) => {

View File

@@ -52,7 +52,7 @@ test.describe('Should be shown automatically', () => {
test.describe('Should be shown manually', () => { test.describe('Should be shown manually', () => {
test.beforeEach(async ({ page }) => { test.beforeEach(async ({ page }) => {
const urlWithProjectPreview = 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); 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.describe('Quiz challenge', () => {
test.beforeEach(async ({ page }) => { 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 // The navigation should be blocked, the user should stay on the same page
await expect(page).toHaveURL( await expect(page).toHaveURL(
allowTrailingSlash( 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' }) .getByRole('button', { name: 'Yes, I want to leave the quiz' })
.click(); .click();
await page.waitForURL('/learn/full-stack-developer/#quiz-basic-html'); await page.waitForURL('/learn/responsive-web-design-v9/#quiz-basic-html');
await expect( await expect(
page.getByRole('heading', { level: 3, name: 'Basic HTML Quiz' }) page.getByRole('heading', { level: 3, name: 'Basic HTML Quiz' })
).toBeVisible(); ).toBeVisible();
@@ -242,7 +243,7 @@ test.describe('Quiz challenge', () => {
.getByRole('button', { name: 'Yes, I want to leave the quiz' }) .getByRole('button', { name: 'Yes, I want to leave the quiz' })
.click(); .click();
await page.waitForURL('/learn/full-stack-developer/#quiz-basic-html'); await page.waitForURL('/learn/responsive-web-design-v9/#quiz-basic-html');
await expect( await expect(
page.getByRole('heading', { level: 3, name: 'Basic HTML Quiz' }) page.getByRole('heading', { level: 3, name: 'Basic HTML Quiz' })
).toBeVisible(); ).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 ({ test('should expand the correct block when user goes to the page from breadcrumb click', async ({
page 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 await page
.getByRole('link', { .getByRole('link', {
@@ -114,7 +116,9 @@ test.describe('Super Block Page - Authenticated User', () => {
}) })
.click(); .click();
await page.waitForURL('/learn/full-stack-developer/#workshop-cafe-menu'); await page.waitForURL(
'/learn/responsive-web-design-v9/#workshop-cafe-menu'
);
// Chapter // Chapter
await expect( 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 // HTML chapter
await expect( await expect(
@@ -173,7 +177,7 @@ test.describe('Super Block Page - Authenticated User', () => {
}) => { }) => {
test.setTimeout(20000); test.setTimeout(20000);
await page.goto('/learn/full-stack-developer'); await page.goto('/learn/responsive-web-design-v9');
// HTML chapter // HTML chapter
await expect( await expect(
@@ -194,7 +198,9 @@ test.describe('Super Block Page - Authenticated User', () => {
}) })
).toHaveAttribute('aria-expanded', 'true'); ).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. // Wait for the page to finish loading so that the current challenge ID can be registered.
await expect( await expect(
@@ -202,7 +208,7 @@ test.describe('Super Block Page - Authenticated User', () => {
).toBeVisible(); ).toBeVisible();
// Go back to the super block page // 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 // Semantic HTML module
await expect( await expect(
@@ -248,7 +254,7 @@ test.describe('Super Block Page - Unauthenticated User', () => {
test('should expand the first block of the super block', async ({ test('should expand the first block of the super block', async ({
page page
}) => { }) => {
await page.goto('/learn/full-stack-developer'); await page.goto('/learn/responsive-web-design-v9');
// First chapter // First chapter
await expect( await expect(

View File

@@ -339,6 +339,19 @@ export const superBlockToCertMap: {
[SuperBlocks.FullStackDeveloper]: null [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 type CertSlug = (typeof Certification)[keyof typeof Certification];
export const linkedInCredentialIds = { export const linkedInCredentialIds = {

View File

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

View File

@@ -40,6 +40,13 @@ interface Block<T> {
const ver = 'v1'; const ver = 'v1';
export const orderedSuperBlockInfo = [ 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.RespWebDesignNew, public: true },
{ dashedName: SuperBlocks.DataAnalysisPy, public: true }, { dashedName: SuperBlocks.DataAnalysisPy, public: true },
{ dashedName: SuperBlocks.MachineLearningPy, public: true }, { dashedName: SuperBlocks.MachineLearningPy, public: true },
@@ -49,7 +56,6 @@ export const orderedSuperBlockInfo = [
{ dashedName: SuperBlocks.TheOdinProject, public: true }, { dashedName: SuperBlocks.TheOdinProject, public: true },
{ dashedName: SuperBlocks.RespWebDesign, public: true }, { dashedName: SuperBlocks.RespWebDesign, public: true },
{ dashedName: SuperBlocks.PythonForEverybody, public: true }, { dashedName: SuperBlocks.PythonForEverybody, public: true },
{ dashedName: SuperBlocks.FullStackDeveloper, public: false },
{ dashedName: SuperBlocks.JsAlgoDataStructNew, public: false }, { dashedName: SuperBlocks.JsAlgoDataStructNew, public: false },
{ dashedName: SuperBlocks.FrontEndDevLibs, public: false }, { dashedName: SuperBlocks.FrontEndDevLibs, public: false },
{ dashedName: SuperBlocks.DataVis, public: false }, { dashedName: SuperBlocks.DataVis, public: false },

View File

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

View File

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