mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-05-28 18:26:54 +00:00
refactor: replace isChallenge (#50033)
* refactor: replace isChallenge Determining if a path is a challenge by the number of path segments is brittle and we ended up writing bizarre things like isChallenge(nextChallengePath). This should be a little more robust. i.e. if we need to know if a page is a challenge, we can check the challengeMeta * test: update tests with new logic
This commit is contained in:
committed by
GitHub
parent
fdbe03d2bb
commit
e955bccfcf
@@ -292,8 +292,8 @@ export type ChallengeMeta = {
|
|||||||
id: string;
|
id: string;
|
||||||
introPath: string;
|
introPath: string;
|
||||||
isFirstStep: boolean;
|
isFirstStep: boolean;
|
||||||
nextChallengePath: string;
|
nextChallengePath: string | null;
|
||||||
prevChallengePath: string;
|
prevChallengePath: string | null;
|
||||||
removeComments: boolean;
|
removeComments: boolean;
|
||||||
superBlock: SuperBlocks;
|
superBlock: SuperBlocks;
|
||||||
title?: string;
|
title?: string;
|
||||||
|
|||||||
@@ -4,8 +4,12 @@ import { HotKeys, GlobalHotKeys } from 'react-hotkeys';
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { editor } from 'monaco-editor';
|
import { editor } from 'monaco-editor';
|
||||||
import { ChallengeFiles, Test, User } from '../../../redux/prop-types';
|
import type {
|
||||||
import { isChallenge } from '../../../utils/path-parsers';
|
ChallengeFiles,
|
||||||
|
Test,
|
||||||
|
User,
|
||||||
|
ChallengeMeta
|
||||||
|
} from '../../../redux/prop-types';
|
||||||
|
|
||||||
import { userSelector } from '../../../redux/selectors';
|
import { userSelector } from '../../../redux/selectors';
|
||||||
import {
|
import {
|
||||||
@@ -57,7 +61,8 @@ const keyMap = {
|
|||||||
showShortcuts: 'shift+/'
|
showShortcuts: 'shift+/'
|
||||||
};
|
};
|
||||||
|
|
||||||
interface HotkeysProps {
|
interface HotkeysProps
|
||||||
|
extends Pick<ChallengeMeta, 'nextChallengePath' | 'prevChallengePath'> {
|
||||||
canFocusEditor: boolean;
|
canFocusEditor: boolean;
|
||||||
challengeFiles: ChallengeFiles;
|
challengeFiles: ChallengeFiles;
|
||||||
challengeType?: number;
|
challengeType?: number;
|
||||||
@@ -67,8 +72,6 @@ interface HotkeysProps {
|
|||||||
submitChallenge: () => void;
|
submitChallenge: () => void;
|
||||||
innerRef: MutableRefObject<HTMLElement | undefined>;
|
innerRef: MutableRefObject<HTMLElement | undefined>;
|
||||||
instructionsPanelRef?: React.RefObject<HTMLElement>;
|
instructionsPanelRef?: React.RefObject<HTMLElement>;
|
||||||
nextChallengePath: string;
|
|
||||||
prevChallengePath: string;
|
|
||||||
setEditorFocusability: (arg0: boolean) => void;
|
setEditorFocusability: (arg0: boolean) => void;
|
||||||
setIsAdvancing: (arg0: boolean) => void;
|
setIsAdvancing: (arg0: boolean) => void;
|
||||||
tests: Test[];
|
tests: Test[];
|
||||||
@@ -137,14 +140,22 @@ function Hotkeys({
|
|||||||
navigationMode: () => setEditorFocusability(false),
|
navigationMode: () => setEditorFocusability(false),
|
||||||
navigatePrev: () => {
|
navigatePrev: () => {
|
||||||
if (!canFocusEditor) {
|
if (!canFocusEditor) {
|
||||||
if (isChallenge(prevChallengePath)) setIsAdvancing(true);
|
if (prevChallengePath) {
|
||||||
void navigate(prevChallengePath);
|
setIsAdvancing(true);
|
||||||
|
void navigate(prevChallengePath);
|
||||||
|
} else {
|
||||||
|
void navigate('/learn');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
navigateNext: () => {
|
navigateNext: () => {
|
||||||
if (!canFocusEditor) {
|
if (!canFocusEditor) {
|
||||||
if (isChallenge(nextChallengePath)) setIsAdvancing(true);
|
if (nextChallengePath) {
|
||||||
void navigate(nextChallengePath);
|
setIsAdvancing(true);
|
||||||
|
void navigate(nextChallengePath);
|
||||||
|
} else {
|
||||||
|
void navigate('/learn');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
showShortcuts: (e: React.KeyboardEvent) => {
|
showShortcuts: (e: React.KeyboardEvent) => {
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import {
|
|||||||
tap,
|
tap,
|
||||||
mergeMap
|
mergeMap
|
||||||
} from 'rxjs/operators';
|
} from 'rxjs/operators';
|
||||||
import { isChallenge } from '../../../utils/path-parsers';
|
|
||||||
import { challengeTypes, submitTypes } from '../../../../utils/challenge-types';
|
import { challengeTypes, submitTypes } from '../../../../utils/challenge-types';
|
||||||
import { actionTypes as submitActionTypes } from '../../../redux/action-types';
|
import { actionTypes as submitActionTypes } from '../../../redux/action-types';
|
||||||
import {
|
import {
|
||||||
@@ -184,16 +183,19 @@ export default function completionEpic(action$, state$) {
|
|||||||
submitter = submitters[submitTypes[challengeType]];
|
submitter = submitters[submitTypes[challengeType]];
|
||||||
}
|
}
|
||||||
|
|
||||||
const pathToNavigateTo = () => {
|
const isNextChallengeInSameSuperBlock =
|
||||||
return findPathToNavigateTo(nextChallengePath, superBlock);
|
nextChallengePath.includes(superBlock);
|
||||||
};
|
|
||||||
|
const pathToNavigateTo = isNextChallengeInSameSuperBlock
|
||||||
|
? nextChallengePath
|
||||||
|
: `/learn/${superBlock}/#${superBlock}-projects`;
|
||||||
|
|
||||||
const canAllowDonationRequest = (state, action) =>
|
const canAllowDonationRequest = (state, action) =>
|
||||||
isBlockNewlyCompletedSelector(state) &&
|
isBlockNewlyCompletedSelector(state) &&
|
||||||
action.type === submitActionTypes.submitComplete;
|
action.type === submitActionTypes.submitComplete;
|
||||||
|
|
||||||
return submitter(type, state).pipe(
|
return submitter(type, state).pipe(
|
||||||
concat(of(setIsAdvancing(isChallenge(pathToNavigateTo())))),
|
concat(of(setIsAdvancing(isNextChallengeInSameSuperBlock))),
|
||||||
mergeMap(x =>
|
mergeMap(x =>
|
||||||
canAllowDonationRequest(state, x)
|
canAllowDonationRequest(state, x)
|
||||||
? of(x, allowBlockDonationRequests({ superBlock, block }))
|
? of(x, allowBlockDonationRequests({ superBlock, block }))
|
||||||
@@ -201,7 +203,7 @@ export default function completionEpic(action$, state$) {
|
|||||||
),
|
),
|
||||||
tap(res => {
|
tap(res => {
|
||||||
if (res.type !== submitActionTypes.updateFailed) {
|
if (res.type !== submitActionTypes.updateFailed) {
|
||||||
navigate(pathToNavigateTo());
|
navigate(pathToNavigateTo);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
concat(of(closeModal('completion')))
|
concat(of(closeModal('completion')))
|
||||||
@@ -209,11 +211,3 @@ export default function completionEpic(action$, state$) {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function findPathToNavigateTo(nextChallengePath, superBlock) {
|
|
||||||
if (nextChallengePath.includes(superBlock)) {
|
|
||||||
return nextChallengePath;
|
|
||||||
} else {
|
|
||||||
return `/learn/${superBlock}/#${superBlock}-projects`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { isChallenge, isLanding } from './path-parsers';
|
import { isLanding } from './path-parsers';
|
||||||
|
|
||||||
const pathnames = {
|
const pathnames = {
|
||||||
english: {
|
english: {
|
||||||
@@ -56,33 +56,3 @@ describe('isLanding', () => {
|
|||||||
expect(isLanding(pathnames.espanolWithYear.challenge)).toBe(false);
|
expect(isLanding(pathnames.espanolWithYear.challenge)).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('isChallenge', () => {
|
|
||||||
it('returns a boolean', () => {
|
|
||||||
expect(typeof isChallenge('/')).toBe('boolean');
|
|
||||||
});
|
|
||||||
it('returns false for Espanol landing pathname', () => {
|
|
||||||
expect(isChallenge(pathnames.espanol.landing)).toBe(false);
|
|
||||||
});
|
|
||||||
it('returns false for Espanol super block pathname', () => {
|
|
||||||
expect(isChallenge(pathnames.espanol.superBlock)).toBe(false);
|
|
||||||
});
|
|
||||||
it('returns true for Espanol challenge pathname', () => {
|
|
||||||
expect(isChallenge(pathnames.espanol.challenge)).toBe(true);
|
|
||||||
});
|
|
||||||
it('returns false for English landing pathname', () => {
|
|
||||||
expect(isChallenge(pathnames.english.landing)).toBe(false);
|
|
||||||
});
|
|
||||||
it('returns false for English super block pathname', () => {
|
|
||||||
expect(isChallenge(pathnames.english.superBlock)).toBe(false);
|
|
||||||
});
|
|
||||||
it('returns true for English challenge pathname', () => {
|
|
||||||
expect(isChallenge(pathnames.english.challenge)).toBe(true);
|
|
||||||
});
|
|
||||||
it('returns true for English with year challenge pathname', () => {
|
|
||||||
expect(isChallenge(pathnames.englishWithYear.challenge)).toBe(true);
|
|
||||||
});
|
|
||||||
it('returns true for Espanol with year challenge pathname', () => {
|
|
||||||
expect(isChallenge(pathnames.espanolWithYear.challenge)).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -4,20 +4,6 @@ import { i18nConstants } from '../../../config/constants';
|
|||||||
const splitPath = (pathname: string): string[] =>
|
const splitPath = (pathname: string): string[] =>
|
||||||
pathname.split('/').filter(x => x);
|
pathname.split('/').filter(x => x);
|
||||||
|
|
||||||
export const isChallenge = (pathname: string): boolean => {
|
|
||||||
const pathArray = splitPath(pathname);
|
|
||||||
return (
|
|
||||||
// learn/<superBlock>/<block>/<challenge>
|
|
||||||
(pathArray.length === 4 && pathArray[0] === 'learn') ||
|
|
||||||
// learn/<year>/<superBlock>/<block>/<challenge>
|
|
||||||
(pathArray.length === 5 && pathArray[0] === 'learn') ||
|
|
||||||
// <i18n>/learn/<superBlock>/<block>/<challenge>
|
|
||||||
(pathArray.length === 5 && pathArray[1] === 'learn') ||
|
|
||||||
// <i18n>/learn/<year>/<superBlock>/<block>/<challenge>
|
|
||||||
(pathArray.length === 6 && pathArray[1] === 'learn')
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const isLanding = (pathname: string): boolean => {
|
export const isLanding = (pathname: string): boolean => {
|
||||||
const pathArray = splitPath(pathname);
|
const pathArray = splitPath(pathname);
|
||||||
const isEnglishLanding = pathArray.length === 0;
|
const isEnglishLanding = pathArray.length === 0;
|
||||||
|
|||||||
@@ -58,12 +58,12 @@ function getIsFirstStep(_node, index, nodeArray) {
|
|||||||
|
|
||||||
function getNextChallengePath(_node, index, nodeArray) {
|
function getNextChallengePath(_node, index, nodeArray) {
|
||||||
const next = nodeArray[index + 1];
|
const next = nodeArray[index + 1];
|
||||||
return next ? next.node.challenge.fields.slug : '/learn';
|
return next ? next.node.challenge.fields.slug : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPrevChallengePath(_node, index, nodeArray) {
|
function getPrevChallengePath(_node, index, nodeArray) {
|
||||||
const prev = nodeArray[index - 1];
|
const prev = nodeArray[index - 1];
|
||||||
return prev ? prev.node.challenge.fields.slug : '/learn';
|
return prev ? prev.node.challenge.fields.slug : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTemplateComponent(challengeType) {
|
function getTemplateComponent(challengeType) {
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ interface NameAndProps {
|
|||||||
}
|
}
|
||||||
function getComponentNameAndProps(
|
function getComponentNameAndProps(
|
||||||
elementType: React.JSXElementConstructor<never>,
|
elementType: React.JSXElementConstructor<never>,
|
||||||
pathname: string
|
pathname: string,
|
||||||
|
pageContext?: { challengeMeta?: { block?: string; superBlock?: string } }
|
||||||
): NameAndProps {
|
): NameAndProps {
|
||||||
// eslint-disable-next-line testing-library/render-result-naming-convention
|
// eslint-disable-next-line testing-library/render-result-naming-convention
|
||||||
const shallow = ShallowRenderer.createRenderer();
|
const shallow = ShallowRenderer.createRenderer();
|
||||||
@@ -28,7 +29,8 @@ function getComponentNameAndProps(
|
|||||||
props: {
|
props: {
|
||||||
location: {
|
location: {
|
||||||
pathname
|
pathname
|
||||||
}
|
},
|
||||||
|
pageContext
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
shallow.render(<Provider store={store}>{LayoutReactComponent}</Provider>);
|
shallow.render(<Provider store={store}>{LayoutReactComponent}</Provider>);
|
||||||
@@ -44,10 +46,21 @@ function getComponentNameAndProps(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
test('Challenge path should have DefaultLayout and no footer', () => {
|
const challengePageContext = {
|
||||||
|
challengeMeta: {
|
||||||
|
block: 'Basic HTML and HTML5',
|
||||||
|
superBlock: 'responsive-web-design'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
test('Challenges should have DefaultLayout and no footer', () => {
|
||||||
const challengePath =
|
const challengePath =
|
||||||
'/learn/responsive-web-design/basic-html-and-html5/say-hello-to-html-elements';
|
'/learn/responsive-web-design/basic-html-and-html5/say-hello-to-html-elements';
|
||||||
const compnentObj = getComponentNameAndProps(Learn, challengePath);
|
const compnentObj = getComponentNameAndProps(
|
||||||
|
Learn,
|
||||||
|
challengePath,
|
||||||
|
challengePageContext
|
||||||
|
);
|
||||||
expect(compnentObj.name).toEqual('DefaultLayout');
|
expect(compnentObj.name).toEqual('DefaultLayout');
|
||||||
expect(compnentObj.props.showFooter).toEqual(false);
|
expect(compnentObj.props.showFooter).toEqual(false);
|
||||||
});
|
});
|
||||||
@@ -59,15 +72,19 @@ test('SuperBlock path should have DefaultLayout and footer', () => {
|
|||||||
expect(compnentObj.props.showFooter).toEqual(true);
|
expect(compnentObj.props.showFooter).toEqual(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('i18l challenge path should have DefaultLayout and no footer', () => {
|
test('i18n challenge path should have DefaultLayout and no footer', () => {
|
||||||
const challengePath =
|
const challengePath =
|
||||||
'espanol/learn/responsive-web-design/basic-html-and-html5/say-hello-to-html-elements/';
|
'espanol/learn/responsive-web-design/basic-html-and-html5/say-hello-to-html-elements/';
|
||||||
const compnentObj = getComponentNameAndProps(Learn, challengePath);
|
const compnentObj = getComponentNameAndProps(
|
||||||
|
Learn,
|
||||||
|
challengePath,
|
||||||
|
challengePageContext
|
||||||
|
);
|
||||||
expect(compnentObj.name).toEqual('DefaultLayout');
|
expect(compnentObj.name).toEqual('DefaultLayout');
|
||||||
expect(compnentObj.props.showFooter).toEqual(false);
|
expect(compnentObj.props.showFooter).toEqual(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('i18l superBlock path should have DefaultLayout and footer', () => {
|
test('i18n superBlock path should have DefaultLayout and footer', () => {
|
||||||
const superBlockPath = '/learn/responsive-web-design/';
|
const superBlockPath = '/learn/responsive-web-design/';
|
||||||
const compnentObj = getComponentNameAndProps(Learn, superBlockPath);
|
const compnentObj = getComponentNameAndProps(Learn, superBlockPath);
|
||||||
expect(compnentObj.name).toEqual('DefaultLayout');
|
expect(compnentObj.name).toEqual('DefaultLayout');
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import React from 'react';
|
|||||||
import CertificationLayout from '../../src/components/layouts/certification';
|
import CertificationLayout from '../../src/components/layouts/certification';
|
||||||
import DefaultLayout from '../../src/components/layouts/default';
|
import DefaultLayout from '../../src/components/layouts/default';
|
||||||
import FourOhFourPage from '../../src/pages/404';
|
import FourOhFourPage from '../../src/pages/404';
|
||||||
import { isChallenge } from '../../src/utils/path-parsers';
|
|
||||||
|
|
||||||
interface LayoutSelectorProps {
|
interface LayoutSelectorProps {
|
||||||
element: JSX.Element;
|
element: JSX.Element;
|
||||||
@@ -20,6 +19,8 @@ export default function layoutSelector({
|
|||||||
location: { pathname }
|
location: { pathname }
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
const isChallenge = !!props.pageContext?.challengeMeta;
|
||||||
|
|
||||||
if (element.type === FourOhFourPage) {
|
if (element.type === FourOhFourPage) {
|
||||||
return (
|
return (
|
||||||
<DefaultLayout pathname={pathname} showFooter={true}>
|
<DefaultLayout pathname={pathname} showFooter={true}>
|
||||||
@@ -30,7 +31,7 @@ export default function layoutSelector({
|
|||||||
return (
|
return (
|
||||||
<CertificationLayout pathname={pathname}>{element}</CertificationLayout>
|
<CertificationLayout pathname={pathname}>{element}</CertificationLayout>
|
||||||
);
|
);
|
||||||
} else if (isChallenge(pathname)) {
|
} else if (isChallenge) {
|
||||||
return (
|
return (
|
||||||
<DefaultLayout
|
<DefaultLayout
|
||||||
pathname={pathname}
|
pathname={pathname}
|
||||||
|
|||||||
Reference in New Issue
Block a user