chore: import upstream snapshot with attribution
i18n - Build Validation / Validate i18n Builds (24) (push) Has been cancelled
CI - Node.js / Lint (24) (push) Has been cancelled
CI - Node.js / Build (24) (push) Has been cancelled
CI - Node.js / Test (24) (push) Has been cancelled
CI - Node.js / Test - Upcoming Changes (24) (push) Has been cancelled
CI - Node.js / Test - i18n (italian, 24) (push) Has been cancelled
CI - Node.js / Test - i18n (portuguese, 24) (push) Has been cancelled
CD - Docker - GHCR Images / Build and Push Images (push) Has been cancelled
i18n - Build Validation / Validate i18n Builds (24) (push) Has been cancelled
CI - Node.js / Lint (24) (push) Has been cancelled
CI - Node.js / Build (24) (push) Has been cancelled
CI - Node.js / Test (24) (push) Has been cancelled
CI - Node.js / Test - Upcoming Changes (24) (push) Has been cancelled
CI - Node.js / Test - i18n (italian, 24) (push) Has been cancelled
CI - Node.js / Test - i18n (portuguese, 24) (push) Has been cancelled
CD - Docker - GHCR Images / Build and Push Images (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
const path = require('path');
|
||||
const { viewTypes } = require('@freecodecamp/shared/config/challenge-types');
|
||||
|
||||
const backend = path.resolve(
|
||||
__dirname,
|
||||
'../../src/templates/Challenges/projects/backend/show.tsx'
|
||||
);
|
||||
const classic = path.resolve(
|
||||
__dirname,
|
||||
'../../src/templates/Challenges/classic/show.tsx'
|
||||
);
|
||||
const frontend = path.resolve(
|
||||
__dirname,
|
||||
'../../src/templates/Challenges/projects/frontend/show.tsx'
|
||||
);
|
||||
const codeAlly = path.resolve(
|
||||
__dirname,
|
||||
'../../src/templates/Challenges/codeally/show.tsx'
|
||||
);
|
||||
const superBlockIntro = path.resolve(
|
||||
__dirname,
|
||||
'../../src/templates/Introduction/super-block-intro.tsx'
|
||||
);
|
||||
const quiz = path.resolve(
|
||||
__dirname,
|
||||
'../../src/templates/Challenges/quiz/show.tsx'
|
||||
);
|
||||
|
||||
const exam = path.resolve(
|
||||
__dirname,
|
||||
'../../src/templates/Challenges/exam/show.tsx'
|
||||
);
|
||||
|
||||
const msTrophy = path.resolve(
|
||||
__dirname,
|
||||
'../../src/templates/Challenges/ms-trophy/show.tsx'
|
||||
);
|
||||
|
||||
const fillInTheBlank = path.resolve(
|
||||
__dirname,
|
||||
'../../src/templates/Challenges/fill-in-the-blank/show.tsx'
|
||||
);
|
||||
|
||||
const generic = path.resolve(
|
||||
__dirname,
|
||||
'../../src/templates/Challenges/generic/show.tsx'
|
||||
);
|
||||
|
||||
const examDownload = path.resolve(
|
||||
__dirname,
|
||||
'../../src/templates/Challenges/exam-download/show.tsx'
|
||||
);
|
||||
|
||||
const views = {
|
||||
backend,
|
||||
classic,
|
||||
modern: classic,
|
||||
frontend,
|
||||
quiz,
|
||||
codeAlly,
|
||||
exam,
|
||||
msTrophy,
|
||||
fillInTheBlank,
|
||||
generic,
|
||||
examDownload
|
||||
};
|
||||
|
||||
function getIsFirstStepInBlock(id, nodes) {
|
||||
const current = nodes[id];
|
||||
const previous = nodes[id - 1];
|
||||
|
||||
if (!previous) return true;
|
||||
return previous.challenge.block !== current.challenge.block;
|
||||
}
|
||||
|
||||
function getTemplateComponent(challengeType) {
|
||||
return views[viewTypes[challengeType]];
|
||||
}
|
||||
|
||||
exports.getTemplateComponent = getTemplateComponent;
|
||||
|
||||
exports.createChallengePages = function (
|
||||
createPage,
|
||||
{ idToNextPathCurrentCurriculum, idToPrevPathCurrentCurriculum }
|
||||
) {
|
||||
return function (node, index, allChallengeNodes) {
|
||||
const {
|
||||
dashedName,
|
||||
disableLoopProtectTests,
|
||||
disableLoopProtectPreview,
|
||||
certification,
|
||||
superBlock,
|
||||
chapter,
|
||||
module,
|
||||
block,
|
||||
fields: { slug, blockHashSlug },
|
||||
required = [],
|
||||
template,
|
||||
challengeType,
|
||||
id,
|
||||
isLastChallengeInBlock,
|
||||
saveSubmissionToDB
|
||||
} = node.challenge;
|
||||
|
||||
createPage({
|
||||
path: slug,
|
||||
component: getTemplateComponent(challengeType),
|
||||
context: {
|
||||
challengeMeta: {
|
||||
blockHashSlug,
|
||||
dashedName,
|
||||
certification,
|
||||
disableLoopProtectTests,
|
||||
disableLoopProtectPreview,
|
||||
superBlock,
|
||||
chapter,
|
||||
module,
|
||||
block,
|
||||
isFirstStep: getIsFirstStepInBlock(index, allChallengeNodes),
|
||||
template,
|
||||
required,
|
||||
isLastChallengeInBlock: isLastChallengeInBlock,
|
||||
nextChallengePath: idToNextPathCurrentCurriculum[node.id],
|
||||
prevChallengePath: idToPrevPathCurrentCurriculum[node.id],
|
||||
id,
|
||||
saveSubmissionToDB
|
||||
},
|
||||
projectPreview: getProjectPreviewConfig(
|
||||
node.challenge,
|
||||
allChallengeNodes
|
||||
),
|
||||
id: node.id
|
||||
}
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
// TODO: figure out a cleaner way to get the last challenge in a block.
|
||||
function getProjectPreviewConfig(challenge, allChallengeNodes) {
|
||||
const { block } = challenge;
|
||||
|
||||
const challengesInBlock = allChallengeNodes
|
||||
.filter(({ challenge }) => challenge.block === block)
|
||||
.map(({ challenge }) => challenge);
|
||||
const lastChallenge = challengesInBlock[challengesInBlock.length - 1];
|
||||
const solutionFiles = lastChallenge.solutions[0] ?? [];
|
||||
const lastChallengeFiles = lastChallenge.challengeFiles ?? [];
|
||||
|
||||
const findFileByKey = (key, files) =>
|
||||
files.find(file => file.fileKey === key);
|
||||
|
||||
const projectPreviewChallengeFiles = lastChallengeFiles.map(file => ({
|
||||
...file,
|
||||
contents:
|
||||
findFileByKey(file.fileKey, solutionFiles)?.contents ?? file.contents
|
||||
}));
|
||||
|
||||
return {
|
||||
challengeData: {
|
||||
challengeType: lastChallenge.challengeType,
|
||||
challengeFiles: projectPreviewChallengeFiles
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
exports.createSuperBlockIntroPages = function (createPage) {
|
||||
return function ({ superBlock }) {
|
||||
createPage({
|
||||
path: `/learn/${superBlock}/`,
|
||||
component: superBlockIntro,
|
||||
context: {
|
||||
superBlock
|
||||
}
|
||||
});
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
module.exports = require('./challenge-page-creator');
|
||||
@@ -0,0 +1,142 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
import ShallowRenderer from 'react-test-renderer/shallow';
|
||||
import { describe, test, expect, vi } from 'vitest';
|
||||
|
||||
import FourOhFourPage from '../../src/pages/404';
|
||||
import Certification from '../../src/pages/certification';
|
||||
import Learn from '../../src/pages/learn';
|
||||
import { createStore } from '../../src/redux/create-store';
|
||||
import layoutSelector from './layout-selector';
|
||||
|
||||
vi.mock('../../src/analytics');
|
||||
|
||||
vi.mock('../../src/utils/get-words');
|
||||
|
||||
const store = createStore();
|
||||
|
||||
// TODO: rather than testing which props passed from layoutSelector to the
|
||||
// component it renders, test that the rendered component has the expected
|
||||
// features (i.e. has a footer or not, etc.). That should be possible in
|
||||
// react-testing-library.
|
||||
|
||||
interface NameAndProps {
|
||||
props: Record<string, unknown>;
|
||||
name: string;
|
||||
}
|
||||
function getComponentNameAndProps(
|
||||
elementType: React.JSXElementConstructor<never>,
|
||||
pathname: string,
|
||||
pageContext?: { challengeMeta?: { block?: string; superBlock?: string } }
|
||||
): NameAndProps {
|
||||
const utils = ShallowRenderer.createRenderer();
|
||||
const LayoutReactComponent = layoutSelector({
|
||||
element: { type: elementType, props: {}, key: '' },
|
||||
props: {
|
||||
data: {},
|
||||
location: {
|
||||
pathname
|
||||
},
|
||||
pageContext,
|
||||
params: { '*': '' },
|
||||
path: ''
|
||||
}
|
||||
});
|
||||
utils.render(<Provider store={store}>{LayoutReactComponent}</Provider>);
|
||||
const view = utils.getRenderOutput();
|
||||
return {
|
||||
props: view.props.children.props as Record<string, unknown>,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
name: view.props.children.type.WrappedComponent.displayName
|
||||
// TODO: Revisit this when react-test-renderer is replaced with
|
||||
// react-testing-library
|
||||
};
|
||||
}
|
||||
|
||||
const challengePageContext = {
|
||||
challengeMeta: {
|
||||
block: 'Basic HTML and HTML5',
|
||||
superBlock: 'responsive-web-design'
|
||||
}
|
||||
};
|
||||
|
||||
describe('Layout selector', () => {
|
||||
test('Challenges should have DefaultLayout and no footer', () => {
|
||||
const challengePath =
|
||||
'/learn/responsive-web-design/basic-html-and-html5/say-hello-to-html-elements';
|
||||
const componentObj = getComponentNameAndProps(
|
||||
Learn,
|
||||
challengePath,
|
||||
challengePageContext
|
||||
);
|
||||
expect(componentObj.name).toEqual('DefaultLayout');
|
||||
expect(componentObj.props.showFooter).toEqual(false);
|
||||
});
|
||||
|
||||
test('SuperBlock path should have DefaultLayout and footer', () => {
|
||||
const superBlockPath = '/learn/responsive-web-design/';
|
||||
const componentObj = getComponentNameAndProps(Learn, superBlockPath);
|
||||
expect(componentObj.name).toEqual('DefaultLayout');
|
||||
expect(componentObj.props.showFooter).toEqual(true);
|
||||
});
|
||||
|
||||
test('i18n challenge path should have DefaultLayout and no footer', () => {
|
||||
const challengePath =
|
||||
'espanol/learn/responsive-web-design/basic-html-and-html5/say-hello-to-html-elements/';
|
||||
const componentObj = getComponentNameAndProps(
|
||||
Learn,
|
||||
challengePath,
|
||||
challengePageContext
|
||||
);
|
||||
expect(componentObj.name).toEqual('DefaultLayout');
|
||||
expect(componentObj.props.showFooter).toEqual(false);
|
||||
});
|
||||
|
||||
test('i18n superBlock path should have DefaultLayout and footer', () => {
|
||||
const superBlockPath = '/learn/responsive-web-design/';
|
||||
const componentObj = getComponentNameAndProps(Learn, superBlockPath);
|
||||
expect(componentObj.name).toEqual('DefaultLayout');
|
||||
expect(componentObj.props.showFooter).toEqual(true);
|
||||
});
|
||||
|
||||
test('404 page should have DefaultLayout and footer', () => {
|
||||
const challengePath =
|
||||
'/espanol/learn/responsive-web-design/basic-html-and-html5/say-hello-to-html-elements/';
|
||||
const componentObj = getComponentNameAndProps(
|
||||
FourOhFourPage,
|
||||
challengePath
|
||||
);
|
||||
expect(componentObj.name).toEqual('DefaultLayout');
|
||||
expect(componentObj.props.showFooter).toEqual(true);
|
||||
});
|
||||
|
||||
test('Certification path should have CertificationLayout', () => {
|
||||
const challengePath =
|
||||
'/certification/mot01/javascript-algorithms-and-data-structures/';
|
||||
const componentObj = getComponentNameAndProps(Certification, challengePath);
|
||||
expect(componentObj.name).toEqual('CertificationLayout');
|
||||
});
|
||||
|
||||
test('Status paths should return raw element without layout', () => {
|
||||
const TestComponent = () => <div>Test</div>;
|
||||
const statusPath = '/status/version';
|
||||
|
||||
const result = layoutSelector({
|
||||
element: { type: TestComponent, props: {}, key: '' },
|
||||
props: {
|
||||
data: {},
|
||||
location: {
|
||||
pathname: statusPath
|
||||
},
|
||||
params: { '*': '' },
|
||||
path: ''
|
||||
}
|
||||
});
|
||||
|
||||
// The result should be the element directly, not wrapped in a layout
|
||||
expect(result.type).toBe(TestComponent);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,69 @@
|
||||
import React from 'react';
|
||||
|
||||
import CertificationLayout from '../../src/components/layouts/certification';
|
||||
import DefaultLayout from '../../src/components/layouts/default';
|
||||
import FourOhFourPage from '../../src/pages/404';
|
||||
|
||||
interface LayoutSelectorProps {
|
||||
element: JSX.Element;
|
||||
props: {
|
||||
data: { challengeNode?: { challenge?: { usesMultifileEditor?: boolean } } };
|
||||
location: { pathname: string };
|
||||
pageContext?: { challengeMeta?: { block?: string; superBlock?: string } };
|
||||
params: { '*'?: string };
|
||||
path: string;
|
||||
};
|
||||
}
|
||||
export default function layoutSelector({
|
||||
element,
|
||||
props
|
||||
}: LayoutSelectorProps): JSX.Element {
|
||||
const {
|
||||
location: { pathname }
|
||||
} = props;
|
||||
|
||||
const isDailyChallenge = props.path === '/learn/daily-coding-challenge/*';
|
||||
const dailyChallengeParam = props.params['*'];
|
||||
|
||||
const isChallenge = !!props.pageContext?.challengeMeta || isDailyChallenge;
|
||||
|
||||
// Return raw element for status endpoints without any layout
|
||||
if (/^\/status\//.test(pathname)) {
|
||||
return element;
|
||||
}
|
||||
|
||||
if (element.type === FourOhFourPage) {
|
||||
return (
|
||||
<DefaultLayout pathname={pathname} showFooter={true}>
|
||||
{element}
|
||||
</DefaultLayout>
|
||||
);
|
||||
} else if (/\/certification\//.test(pathname)) {
|
||||
return (
|
||||
<CertificationLayout pathname={pathname}>{element}</CertificationLayout>
|
||||
);
|
||||
} else if (isChallenge) {
|
||||
return (
|
||||
<DefaultLayout
|
||||
pathname={pathname}
|
||||
showFooter={false}
|
||||
isChallenge={true}
|
||||
isDailyChallenge={isDailyChallenge}
|
||||
dailyChallengeParam={dailyChallengeParam}
|
||||
usesMultifileEditor={
|
||||
props.data?.challengeNode?.challenge?.usesMultifileEditor
|
||||
}
|
||||
block={props.pageContext?.challengeMeta?.block}
|
||||
superBlock={props.pageContext?.challengeMeta?.superBlock}
|
||||
>
|
||||
{element}
|
||||
</DefaultLayout>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<DefaultLayout pathname={pathname} showFooter={true}>
|
||||
{element}
|
||||
</DefaultLayout>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* The function is useful in cases where we want to preload a page
|
||||
* but the link of the page isn't rendered on the screen.
|
||||
* For more details, see https://github.com/freeCodeCamp/freeCodeCamp/pull/55472.
|
||||
*/
|
||||
export const preloadPage = (path?: string) => {
|
||||
if (!window.___loader || !path) return;
|
||||
|
||||
window.___loader.hovering(path);
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
const envData = require('../../config/env.json');
|
||||
|
||||
const { clientLocale } = envData;
|
||||
|
||||
exports.pathPrefix = clientLocale === 'english' ? '' : '/' + clientLocale;
|
||||
Reference in New Issue
Block a user