mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2025-12-19 10:07:46 -05:00
feat(client/curriculum): release new superblocks, certs coming soon (#63574)
Co-authored-by: ahmad abdolsaheb <ahmad.abdolsaheb@gmail.com>
This commit is contained in:
@@ -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
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
221
client/src/templates/Introduction/components/super-block-map.tsx
Normal file
221
client/src/templates/Introduction/components/super-block-map.tsx
Normal 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;
|
||||
@@ -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}
|
||||
<SuperBlockMap
|
||||
certification={certification}
|
||||
completedChallengeIds={completedChallenges.map(c => c.id)}
|
||||
disabledBlocks={disabledBlocksFeature}
|
||||
initialExpandedBlock={initialExpandedBlock}
|
||||
showCertification={showCertification}
|
||||
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}
|
||||
superBlockChallenges={superBlockChallenges}
|
||||
title={title}
|
||||
user={user}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{!isSignedIn && !signInLoading && (
|
||||
<>
|
||||
<Spacer size='l' />
|
||||
|
||||
@@ -327,9 +327,11 @@
|
||||
},
|
||||
{
|
||||
"chapterType": "exam",
|
||||
"comingSoon": true,
|
||||
"dashedName": "javascript-certification-exam",
|
||||
"modules": [
|
||||
{
|
||||
"comingSoon": true,
|
||||
"dashedName": "javascript-certification-exam",
|
||||
"blocks": ["exam-javascript-certification"]
|
||||
}
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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
|
||||
|
||||
82
e2e/full-stack-page.spec.ts
Normal file
82
e2e/full-stack-page.spec.ts
Normal 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.'
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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', {
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user