feat: show project preview (#43967)
* feat: add data for preview to challengeMeta
* feat: allow creation of project preview frames
* feat: make project preview data available for frame
* refactor: simplify reducer
* feat: show project preview for first challenge
* feat: show project preview on MultiFile challenges
* test: check for presence/absence of preview modal
* fix: simplify previewProject saga
* test: uncomment project preview test
* fix: increase modal size + change modal title
* modal-footer
* feat: adjust preview size
* fix: remove margin, padding, and line-height for preview of finished projects
* Revert "fix: remove margin, padding, and line-height for preview of finished projects"
This reverts commit 0db11a0819
.
* fix: remove margin on all previews
* refactor: use closeModal('projectPreview') for clarity
Co-authored-by: Shaun Hamilton <shauhami020@gmail.com>
* fix: get started -> start coding!
* fix: update closeModal type
Co-authored-by: moT01 <20648924+moT01@users.noreply.github.com>
Co-authored-by: Ahmad Abdolsaheb <ahmad.abdolsaheb@gmail.com>
Co-authored-by: Shaun Hamilton <shauhami020@gmail.com>
This commit is contained in:
committed by
GitHub
parent
a8b0332720
commit
bb7893db8e
@ -87,9 +87,21 @@ exports.createPages = function createPages({ graphql, actions, reporter }) {
|
||||
src
|
||||
}
|
||||
challengeOrder
|
||||
challengeFiles {
|
||||
name
|
||||
ext
|
||||
contents
|
||||
head
|
||||
tail
|
||||
}
|
||||
solutions {
|
||||
contents
|
||||
ext
|
||||
}
|
||||
superBlock
|
||||
superOrder
|
||||
template
|
||||
usesMultifileEditor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -62,7 +62,8 @@
|
||||
"verify-email": "Verify Email",
|
||||
"submit-and-go": "Submit and go to next challenge",
|
||||
"go-to-next": "Go to next challenge",
|
||||
"ask-later": "Ask me later"
|
||||
"ask-later": "Ask me later",
|
||||
"start-coding": "Start coding!"
|
||||
},
|
||||
"landing": {
|
||||
"big-heading-1": "Learn to code — for free.",
|
||||
@ -288,7 +289,8 @@
|
||||
"preview": "Preview"
|
||||
},
|
||||
"help-translate": "We are still translating the following certifications.",
|
||||
"help-translate-link": "Help us translate."
|
||||
"help-translate-link": "Help us translate.",
|
||||
"project-preview-title": "Here's a preview of what you will build"
|
||||
},
|
||||
"donate": {
|
||||
"title": "Support our nonprofit",
|
||||
|
@ -106,7 +106,7 @@ export type MarkdownRemark = {
|
||||
|
||||
type Question = { text: string; answers: string[]; solution: number };
|
||||
type Fields = { slug: string; blockName: string; tests: Test[] };
|
||||
type Required = {
|
||||
export type Required = {
|
||||
link: string;
|
||||
raw: boolean;
|
||||
src: string;
|
||||
|
@ -28,6 +28,9 @@ import CompletionModal from '../components/completion-modal';
|
||||
import HelpModal from '../components/help-modal';
|
||||
import Output from '../components/output';
|
||||
import Preview from '../components/preview';
|
||||
import ProjectPreviewModal, {
|
||||
PreviewConfig
|
||||
} from '../components/project-preview-modal';
|
||||
import SidePanel from '../components/side-panel';
|
||||
import VideoModal from '../components/video-modal';
|
||||
import {
|
||||
@ -41,7 +44,10 @@ import {
|
||||
initConsole,
|
||||
initTests,
|
||||
isChallengeCompletedSelector,
|
||||
updateChallengeMeta
|
||||
previewMounted,
|
||||
updateChallengeMeta,
|
||||
openModal,
|
||||
setEditorFocusability
|
||||
} from '../redux';
|
||||
import { getGuideUrl } from '../utils';
|
||||
import MultifileEditor from './MultifileEditor';
|
||||
@ -68,7 +74,10 @@ const mapDispatchToProps = (dispatch: Dispatch) =>
|
||||
updateChallengeMeta,
|
||||
challengeMounted,
|
||||
executeChallenge,
|
||||
cancelTests
|
||||
cancelTests,
|
||||
previewMounted,
|
||||
openModal,
|
||||
setEditorFocusability
|
||||
},
|
||||
dispatch
|
||||
);
|
||||
@ -87,10 +96,13 @@ interface ShowClassicProps {
|
||||
output: string[];
|
||||
pageContext: {
|
||||
challengeMeta: ChallengeMeta;
|
||||
projectPreview: PreviewConfig & { showProjectPreview: boolean };
|
||||
};
|
||||
t: TFunction;
|
||||
tests: Test[];
|
||||
updateChallengeMeta: (arg0: ChallengeMeta) => void;
|
||||
openModal: (modal: string) => void;
|
||||
setEditorFocusability: (canFocus: boolean) => void;
|
||||
}
|
||||
|
||||
interface ShowClassicState {
|
||||
@ -231,6 +243,7 @@ class ShowClassic extends Component<ShowClassicProps, ShowClassicState> {
|
||||
initConsole,
|
||||
initTests,
|
||||
updateChallengeMeta,
|
||||
openModal,
|
||||
data: {
|
||||
challengeNode: {
|
||||
challengeFiles,
|
||||
@ -240,11 +253,15 @@ class ShowClassic extends Component<ShowClassicProps, ShowClassicState> {
|
||||
helpCategory
|
||||
}
|
||||
},
|
||||
pageContext: { challengeMeta }
|
||||
pageContext: {
|
||||
challengeMeta,
|
||||
projectPreview: { showProjectPreview }
|
||||
}
|
||||
} = this.props;
|
||||
initConsole('');
|
||||
createFiles(challengeFiles ?? []);
|
||||
initTests(tests);
|
||||
if (showProjectPreview) openModal('projectPreview');
|
||||
updateChallengeMeta({
|
||||
...challengeMeta,
|
||||
title,
|
||||
@ -358,7 +375,11 @@ class ShowClassic extends Component<ShowClassicProps, ShowClassicState> {
|
||||
|
||||
renderPreview() {
|
||||
return (
|
||||
<Preview className='full-height' disableIframe={this.state.resizing} />
|
||||
<Preview
|
||||
className='full-height'
|
||||
disableIframe={this.state.resizing}
|
||||
previewMounted={previewMounted}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -383,7 +404,8 @@ class ShowClassic extends Component<ShowClassicProps, ShowClassicState> {
|
||||
const {
|
||||
executeChallenge,
|
||||
pageContext: {
|
||||
challengeMeta: { nextChallengePath, prevChallengePath }
|
||||
challengeMeta: { nextChallengePath, prevChallengePath },
|
||||
projectPreview
|
||||
},
|
||||
challengeFiles,
|
||||
t
|
||||
@ -443,6 +465,7 @@ class ShowClassic extends Component<ShowClassicProps, ShowClassicState> {
|
||||
<HelpModal />
|
||||
<VideoModal videoUrl={this.getVideoUrl()} />
|
||||
<ResetModal />
|
||||
<ProjectPreviewModal previewConfig={projectPreview} />
|
||||
</LearnLayout>
|
||||
</Hotkeys>
|
||||
);
|
||||
@ -456,9 +479,6 @@ export default connect(
|
||||
mapDispatchToProps
|
||||
)(withTranslation()(ShowClassic));
|
||||
|
||||
// TODO: handle jsx (not sure why it doesn't get an editableRegion) EDIT:
|
||||
// probably because the dummy challenge didn't include it, so Gatsby couldn't
|
||||
// infer it.
|
||||
export const query = graphql`
|
||||
query ClassicChallenge($slug: String!) {
|
||||
challengeNode(fields: { slug: { eq: $slug } }) {
|
||||
|
@ -1,6 +1,7 @@
|
||||
.challenge-preview,
|
||||
.challenge-preview-frame {
|
||||
height: 100%;
|
||||
min-height: 70vh;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
@ -1,30 +1,23 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators, Dispatch } from 'redux';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { previewMounted } from '../redux';
|
||||
import { mainPreviewId } from '../utils/frame';
|
||||
|
||||
import './preview.css';
|
||||
|
||||
const mainId = 'fcc-main-frame';
|
||||
|
||||
const mapDispatchToProps = (dispatch: Dispatch) =>
|
||||
bindActionCreators(
|
||||
{
|
||||
previewMounted
|
||||
},
|
||||
dispatch
|
||||
);
|
||||
|
||||
interface PreviewProps {
|
||||
className?: string;
|
||||
disableIframe?: boolean;
|
||||
previewMounted: () => void;
|
||||
t: (text: string) => string;
|
||||
previewId?: string;
|
||||
}
|
||||
|
||||
function Preview({ disableIframe, previewMounted, t }: PreviewProps) {
|
||||
function Preview({
|
||||
disableIframe,
|
||||
previewMounted,
|
||||
previewId
|
||||
}: PreviewProps): JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
const [iframeStatus, setIframeStatus] = useState<boolean | undefined>(false);
|
||||
const iframeToggle = iframeStatus ? 'disable' : 'enable';
|
||||
|
||||
@ -36,11 +29,14 @@ function Preview({ disableIframe, previewMounted, t }: PreviewProps) {
|
||||
setIframeStatus(disableIframe);
|
||||
}, [disableIframe]);
|
||||
|
||||
// TODO: remove type assertion once frame.js has been migrated.
|
||||
const id: string = previewId ?? (mainPreviewId as string);
|
||||
|
||||
return (
|
||||
<div className={`notranslate challenge-preview ${iframeToggle}-iframe`}>
|
||||
<iframe
|
||||
className={'challenge-preview-frame'}
|
||||
id={mainId}
|
||||
id={id}
|
||||
title={t('learn.chal-preview')}
|
||||
/>
|
||||
</div>
|
||||
@ -49,4 +45,4 @@ function Preview({ disableIframe, previewMounted, t }: PreviewProps) {
|
||||
|
||||
Preview.displayName = 'Preview';
|
||||
|
||||
export default connect(null, mapDispatchToProps)(withTranslation()(Preview));
|
||||
export default Preview;
|
||||
|
@ -0,0 +1,4 @@
|
||||
.project-preview-modal-body {
|
||||
line-height: 0;
|
||||
padding: 0;
|
||||
}
|
@ -0,0 +1,104 @@
|
||||
import { Button, Modal } from '@freecodecamp/react-bootstrap';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import type { ChallengeFile, Required } from '../../../redux/prop-types';
|
||||
import {
|
||||
closeModal,
|
||||
setEditorFocusability,
|
||||
isProjectPreviewModalOpenSelector,
|
||||
projectPreviewMounted
|
||||
} from '../redux';
|
||||
import { projectPreviewId } from '../utils/frame';
|
||||
import Preview from './preview';
|
||||
|
||||
import './project-preview-modal.css';
|
||||
|
||||
export interface PreviewConfig {
|
||||
challengeType: boolean;
|
||||
challengeFiles: ChallengeFile[];
|
||||
required: Required;
|
||||
template: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
closeModal: (arg: string) => void;
|
||||
isOpen: boolean;
|
||||
projectPreviewMounted: (previewConfig: PreviewConfig) => void;
|
||||
previewConfig: PreviewConfig;
|
||||
setEditorFocusability: (focusability: boolean) => void;
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: unknown) => ({
|
||||
isOpen: isProjectPreviewModalOpenSelector(state) as boolean
|
||||
});
|
||||
const mapDispatchToProps = {
|
||||
closeModal,
|
||||
setEditorFocusability,
|
||||
projectPreviewMounted
|
||||
};
|
||||
|
||||
export function ProjectPreviewModal({
|
||||
closeModal,
|
||||
isOpen,
|
||||
projectPreviewMounted,
|
||||
previewConfig,
|
||||
setEditorFocusability
|
||||
}: Props): JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
useEffect(() => {
|
||||
if (isOpen) setEditorFocusability(false);
|
||||
});
|
||||
|
||||
return (
|
||||
<Modal
|
||||
bsSize='lg'
|
||||
data-cy='project-preview-modal'
|
||||
dialogClassName='project-preview-modal'
|
||||
onHide={() => {
|
||||
closeModal('projectPreview');
|
||||
setEditorFocusability(true);
|
||||
}}
|
||||
show={isOpen}
|
||||
>
|
||||
<Modal.Header
|
||||
className='project-preview-modal-header fcc-modal'
|
||||
closeButton={true}
|
||||
>
|
||||
<Modal.Title className='text-center'>
|
||||
{t('learn.project-preview-title')}
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body className='project-preview-modal-body text-center'>
|
||||
{/* remove type assertion once frame.js has been migrated to TS */}
|
||||
<Preview
|
||||
previewId={projectPreviewId as string}
|
||||
previewMounted={() => {
|
||||
projectPreviewMounted(previewConfig);
|
||||
}}
|
||||
/>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button
|
||||
block={true}
|
||||
bsSize='lg'
|
||||
bsStyle='primary'
|
||||
onClick={() => {
|
||||
closeModal('projectPreview');
|
||||
setEditorFocusability(true);
|
||||
}}
|
||||
>
|
||||
{t('buttons.start-coding')}
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
ProjectPreviewModal.displayName = 'ProjectPreviewModal';
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(ProjectPreviewModal);
|
@ -20,7 +20,7 @@ const cssCatch = '\n/*fcc*/\n';
|
||||
|
||||
const defaultTemplate = ({ source }) => {
|
||||
return `
|
||||
<body id='display-body'style='margin:8px;'>
|
||||
<body id='display-body'>
|
||||
<!-- fcc-start-source -->
|
||||
${source}
|
||||
<!-- fcc-end-source -->
|
||||
|
@ -32,6 +32,7 @@ export const actionTypes = createTypes(
|
||||
'openModal',
|
||||
|
||||
'previewMounted',
|
||||
'projectPreviewMounted',
|
||||
'challengeMounted',
|
||||
'checkChallenge',
|
||||
'executeChallenge',
|
||||
|
@ -21,6 +21,7 @@ import {
|
||||
getTestRunner,
|
||||
challengeHasPreview,
|
||||
updatePreview,
|
||||
updateProjectPreview,
|
||||
isJavaScriptChallenge,
|
||||
isLoopProtected
|
||||
} from '../utils/build';
|
||||
@ -240,6 +241,24 @@ function* previewChallengeSaga({ flushLogs = true } = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
function* previewProjectSolutionSaga({ payload }) {
|
||||
if (!payload) return;
|
||||
const { showProjectPreview, challengeData } = payload;
|
||||
if (!showProjectPreview) return;
|
||||
|
||||
try {
|
||||
if (canBuildChallenge(challengeData)) {
|
||||
const buildData = yield buildChallengeData(challengeData);
|
||||
if (challengeHasPreview(challengeData)) {
|
||||
const document = yield getContext('document');
|
||||
yield call(updateProjectPreview, buildData, document);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
}
|
||||
|
||||
export function createExecuteChallengeSaga(types) {
|
||||
return [
|
||||
takeLatest(types.executeChallenge, executeCancellableChallengeSaga),
|
||||
@ -251,6 +270,7 @@ export function createExecuteChallengeSaga(types) {
|
||||
types.resetChallenge
|
||||
],
|
||||
executeCancellablePreviewSaga
|
||||
)
|
||||
),
|
||||
takeLatest(types.projectPreviewMounted, previewProjectSolutionSaga)
|
||||
];
|
||||
}
|
||||
|
@ -39,7 +39,8 @@ const initialState = {
|
||||
completion: false,
|
||||
help: false,
|
||||
video: false,
|
||||
reset: false
|
||||
reset: false,
|
||||
projectPreview: false
|
||||
},
|
||||
projectFormValues: {},
|
||||
successMessage: 'Happy Coding!'
|
||||
@ -61,21 +62,16 @@ export const sagas = [
|
||||
export const createFiles = createAction(
|
||||
actionTypes.createFiles,
|
||||
challengeFiles =>
|
||||
challengeFiles.reduce((challengeFiles, challengeFile) => {
|
||||
return [
|
||||
...challengeFiles,
|
||||
{
|
||||
...createPoly(challengeFile),
|
||||
seed: challengeFile.contents.slice(),
|
||||
editableContents: getLines(
|
||||
challengeFile.contents,
|
||||
challengeFile.editableRegionBoundaries
|
||||
),
|
||||
seedEditableRegionBoundaries:
|
||||
challengeFile.editableRegionBoundaries.slice()
|
||||
}
|
||||
];
|
||||
}, [])
|
||||
challengeFiles.map(challengeFile => ({
|
||||
...createPoly(challengeFile),
|
||||
seed: challengeFile.contents.slice(),
|
||||
editableContents: getLines(
|
||||
challengeFile.contents,
|
||||
challengeFile.editableRegionBoundaries
|
||||
),
|
||||
seedEditableRegionBoundaries:
|
||||
challengeFile.editableRegionBoundaries.slice()
|
||||
}))
|
||||
);
|
||||
|
||||
export const createQuestion = createAction(actionTypes.createQuestion);
|
||||
@ -114,6 +110,9 @@ export const closeModal = createAction(actionTypes.closeModal);
|
||||
export const openModal = createAction(actionTypes.openModal);
|
||||
|
||||
export const previewMounted = createAction(actionTypes.previewMounted);
|
||||
export const projectPreviewMounted = createAction(
|
||||
actionTypes.projectPreviewMounted
|
||||
);
|
||||
export const challengeMounted = createAction(actionTypes.challengeMounted);
|
||||
export const checkChallenge = createAction(actionTypes.checkChallenge);
|
||||
export const executeChallenge = createAction(actionTypes.executeChallenge);
|
||||
@ -148,6 +147,8 @@ export const isCompletionModalOpenSelector = state =>
|
||||
export const isHelpModalOpenSelector = state => state[ns].modal.help;
|
||||
export const isVideoModalOpenSelector = state => state[ns].modal.video;
|
||||
export const isResetModalOpenSelector = state => state[ns].modal.reset;
|
||||
export const isProjectPreviewModalOpenSelector = state =>
|
||||
state[ns].modal.projectPreview;
|
||||
export const isResettingSelector = state => state[ns].isResetting;
|
||||
|
||||
export const isBuildEnabledSelector = state => state[ns].isBuildEnabled;
|
||||
|
@ -9,7 +9,8 @@ import { getTransformers } from '../rechallenge/transformers';
|
||||
import {
|
||||
createTestFramer,
|
||||
runTestInTestFrame,
|
||||
createMainFramer
|
||||
createMainPreviewFramer,
|
||||
createProjectPreviewFramer
|
||||
} from './frame';
|
||||
import createWorker from './worker-executor';
|
||||
|
||||
@ -138,7 +139,7 @@ function getJSTestRunner({ build, sources }, { proxyLogger, removeComments }) {
|
||||
|
||||
async function getDOMTestRunner(buildData, { proxyLogger }, document) {
|
||||
await new Promise(resolve =>
|
||||
createTestFramer(document, resolve, proxyLogger)(buildData)
|
||||
createTestFramer(document, proxyLogger, resolve)(buildData)
|
||||
);
|
||||
return (testString, testTimeout) =>
|
||||
runTestInTestFrame(document, testString, testTimeout);
|
||||
@ -197,15 +198,23 @@ export function buildBackendChallenge({ url }) {
|
||||
};
|
||||
}
|
||||
|
||||
export async function updatePreview(buildData, document, proxyLogger) {
|
||||
const { challengeType } = buildData;
|
||||
|
||||
if (challengeType === challengeTypes.html) {
|
||||
await new Promise(resolve =>
|
||||
createMainFramer(document, resolve, proxyLogger)(buildData)
|
||||
);
|
||||
export function updatePreview(buildData, document, proxyLogger) {
|
||||
if (buildData.challengeType === challengeTypes.html) {
|
||||
createMainPreviewFramer(document, proxyLogger)(buildData);
|
||||
} else {
|
||||
throw new Error(`Cannot show preview for challenge type ${challengeType}`);
|
||||
throw new Error(
|
||||
`Cannot show preview for challenge type ${buildData.challengeType}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function updateProjectPreview(buildData, document) {
|
||||
if (buildData.challengeType === challengeTypes.html) {
|
||||
createProjectPreviewFramer(document)(buildData);
|
||||
} else {
|
||||
throw new Error(
|
||||
`Cannot show preview for challenge type ${buildData.challengeType}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3,9 +3,11 @@ import { format } from '../../../utils/format';
|
||||
|
||||
// we use two different frames to make them all essentially pure functions
|
||||
// main iframe is responsible rendering the preview and is where we proxy the
|
||||
const mainId = 'fcc-main-frame';
|
||||
export const mainPreviewId = 'fcc-main-frame';
|
||||
// the test frame is responsible for running the assert tests
|
||||
const testId = 'fcc-test-frame';
|
||||
// the project preview frame demos the finished project
|
||||
export const projectPreviewId = 'fcc-project-preview-frame';
|
||||
|
||||
// base tag here will force relative links
|
||||
// within iframe to point to '' instead of
|
||||
@ -16,7 +18,7 @@ const testId = 'fcc-test-frame';
|
||||
// window.onerror is added here to report any errors thrown during the building
|
||||
// of the frame. React dom errors already appear in the console, so onerror
|
||||
// does not need to pass them on to the default error handler.
|
||||
const createHeader = (id = mainId) => `
|
||||
const createHeader = (id = mainPreviewId) => `
|
||||
<base href='' />
|
||||
<script>
|
||||
window.__frameId = '${id}';
|
||||
@ -68,13 +70,15 @@ const createFrame = (document, id) => ctx => {
|
||||
|
||||
const hiddenFrameClassName = 'hide-test-frame';
|
||||
const mountFrame =
|
||||
document =>
|
||||
(document, id) =>
|
||||
({ element, ...rest }) => {
|
||||
const oldFrame = document.getElementById(element.id);
|
||||
if (oldFrame) {
|
||||
element.className = oldFrame.className || hiddenFrameClassName;
|
||||
oldFrame.parentNode.replaceChild(element, oldFrame);
|
||||
} else {
|
||||
// only test frames can be added (and hidden) here, other frames must be
|
||||
// added by react
|
||||
} else if (id === testId) {
|
||||
element.className = hiddenFrameClassName;
|
||||
document.body.appendChild(element);
|
||||
}
|
||||
@ -87,11 +91,13 @@ const mountFrame =
|
||||
};
|
||||
|
||||
const buildProxyConsole = proxyLogger => ctx => {
|
||||
const oldLog = ctx.window.console.log.bind(ctx.window.console);
|
||||
ctx.window.console.log = function proxyConsole(...args) {
|
||||
proxyLogger(args.map(arg => format(arg)).join(' '));
|
||||
return oldLog(...args);
|
||||
};
|
||||
if (proxyLogger) {
|
||||
const oldLog = ctx.window.console.log.bind(ctx.window.console);
|
||||
ctx.window.console.log = function proxyConsole(...args) {
|
||||
proxyLogger(args.map(arg => format(arg)).join(' '));
|
||||
return oldLog(...args);
|
||||
};
|
||||
}
|
||||
return ctx;
|
||||
};
|
||||
|
||||
@ -112,7 +118,7 @@ const initTestFrame = frameReady => ctx => {
|
||||
return ctx;
|
||||
};
|
||||
|
||||
const initMainFrame = (frameReady, proxyLogger) => ctx => {
|
||||
const initMainFrame = (_, proxyLogger) => ctx => {
|
||||
waitForFrame(ctx).then(() => {
|
||||
// Overwriting the onerror added by createHeader to catch any errors thrown
|
||||
// after the frame is ready. It has to be overwritten, as proxyLogger cannot
|
||||
@ -129,11 +135,12 @@ const initMainFrame = (frameReady, proxyLogger) => ctx => {
|
||||
// an error from a cross origin script just appears as 'Script error.'
|
||||
return false;
|
||||
};
|
||||
frameReady();
|
||||
});
|
||||
return ctx;
|
||||
};
|
||||
|
||||
const initPreviewFrame = () => ctx => ctx;
|
||||
|
||||
const waitForFrame = ctx => {
|
||||
return new Promise(resolve => {
|
||||
if (ctx.document.readyState === 'loading') {
|
||||
@ -156,16 +163,19 @@ const writeContentToFrame = ctx => {
|
||||
return ctx;
|
||||
};
|
||||
|
||||
export const createMainFramer = (document, frameReady, proxyLogger) =>
|
||||
createFramer(document, frameReady, proxyLogger, mainId, initMainFrame);
|
||||
export const createMainPreviewFramer = (document, proxyLogger) =>
|
||||
createFramer(document, mainPreviewId, initMainFrame, proxyLogger);
|
||||
|
||||
export const createTestFramer = (document, frameReady, proxyLogger) =>
|
||||
createFramer(document, frameReady, proxyLogger, testId, initTestFrame);
|
||||
export const createProjectPreviewFramer = document =>
|
||||
createFramer(document, projectPreviewId, initPreviewFrame);
|
||||
|
||||
const createFramer = (document, frameReady, proxyLogger, id, init) =>
|
||||
export const createTestFramer = (document, proxyLogger, frameReady) =>
|
||||
createFramer(document, testId, initTestFrame, proxyLogger, frameReady);
|
||||
|
||||
const createFramer = (document, id, init, proxyLogger, frameReady) =>
|
||||
flow(
|
||||
createFrame(document, id),
|
||||
mountFrame(document),
|
||||
mountFrame(document, id),
|
||||
buildProxyConsole(proxyLogger),
|
||||
writeContentToFrame,
|
||||
init(frameReady, proxyLogger)
|
||||
|
@ -1,6 +1,7 @@
|
||||
const path = require('path');
|
||||
const { createPoly } = require('../../../utils/polyvinyl');
|
||||
const { dasherize } = require('../../../utils/slugs');
|
||||
|
||||
const { sortChallengeFiles } = require('../../../utils/sort-challengefiles');
|
||||
const { viewTypes } = require('../challenge-types');
|
||||
|
||||
const backend = path.resolve(
|
||||
@ -57,7 +58,7 @@ function getTemplateComponent(challengeType) {
|
||||
}
|
||||
|
||||
exports.createChallengePages = function (createPage) {
|
||||
return function ({ node }, index, thisArray) {
|
||||
return function ({ node: challenge }, index, allChallengeEdges) {
|
||||
const {
|
||||
superBlock,
|
||||
block,
|
||||
@ -66,7 +67,7 @@ exports.createChallengePages = function (createPage) {
|
||||
template,
|
||||
challengeType,
|
||||
id
|
||||
} = node;
|
||||
} = challenge;
|
||||
// TODO: challengeType === 7 and isPrivate are the same, right? If so, we
|
||||
// should remove one of them.
|
||||
|
||||
@ -79,16 +80,56 @@ exports.createChallengePages = function (createPage) {
|
||||
block,
|
||||
template,
|
||||
required,
|
||||
nextChallengePath: getNextChallengePath(node, index, thisArray),
|
||||
prevChallengePath: getPrevChallengePath(node, index, thisArray),
|
||||
nextChallengePath: getNextChallengePath(
|
||||
challenge,
|
||||
index,
|
||||
allChallengeEdges
|
||||
),
|
||||
prevChallengePath: getPrevChallengePath(
|
||||
challenge,
|
||||
index,
|
||||
allChallengeEdges
|
||||
),
|
||||
id
|
||||
},
|
||||
projectPreview: getProjectPreviewConfig(challenge, allChallengeEdges),
|
||||
slug
|
||||
}
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
function getProjectPreviewConfig(challenge, allChallengeEdges) {
|
||||
const { block, challengeOrder, usesMultifileEditor } = challenge;
|
||||
|
||||
const challengesInBlock = allChallengeEdges
|
||||
.filter(({ node }) => node.block === block)
|
||||
.map(({ node }) => node);
|
||||
const lastChallenge = challengesInBlock[challengesInBlock.length - 1];
|
||||
const solutionToLastChallenge = sortChallengeFiles(
|
||||
lastChallenge.solutions[0] ?? []
|
||||
);
|
||||
const lastChallengeFiles = sortChallengeFiles(
|
||||
lastChallenge.challengeFiles ?? []
|
||||
);
|
||||
const projectPreviewChallengeFiles = lastChallengeFiles.map((file, id) =>
|
||||
createPoly({
|
||||
...file,
|
||||
contents: solutionToLastChallenge[id]?.contents ?? file.contents
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
showProjectPreview: challengeOrder === 0 && usesMultifileEditor,
|
||||
challengeData: {
|
||||
challengeType: lastChallenge.challengeType,
|
||||
challengeFiles: projectPreviewChallengeFiles,
|
||||
required: lastChallenge.required,
|
||||
template: lastChallenge.template
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
exports.createBlockIntroPages = function (createPage) {
|
||||
return function (edge) {
|
||||
const {
|
||||
|
53
cypress/integration/learn/challenges/project-preview.js
Normal file
53
cypress/integration/learn/challenges/project-preview.js
Normal file
@ -0,0 +1,53 @@
|
||||
const practiceProjectUrls = [
|
||||
'/learn/responsive-web-design/css-variables-skyline/',
|
||||
'/learn/responsive-web-design/basic-html-cat-photo-app/',
|
||||
'/learn/responsive-web-design/basic-css-cafe-menu/',
|
||||
'/learn/responsive-web-design/css-picasso-painting/',
|
||||
'/learn/responsive-web-design/css-box-model/',
|
||||
'/learn/responsive-web-design/css-piano/',
|
||||
'/learn/responsive-web-design/registration-form/',
|
||||
'/learn/responsive-web-design/accessibility-quiz/'
|
||||
];
|
||||
|
||||
const legacyFirstChallengeUrls = [
|
||||
'/learn/responsive-web-design/basic-html-and-html5/say-hello-to-html-elements',
|
||||
'/learn/responsive-web-design/basic-css/change-the-color-of-text',
|
||||
'/learn/responsive-web-design/applied-visual-design/create-visual-balance-using-the-text-align-property',
|
||||
'/learn/responsive-web-design/applied-accessibility/add-a-text-alternative-to-images-for-visually-impaired-accessibility',
|
||||
'/learn/responsive-web-design/css-flexbox/use-display-flex-to-position-two-boxes',
|
||||
'/learn/responsive-web-design/css-grid/create-your-first-css-grid'
|
||||
];
|
||||
|
||||
describe('project preview', () => {
|
||||
if (Cypress.env('SHOW_UPCOMING_CHANGES') === 'true') {
|
||||
it('should appear on the first challenges of each practice project', () => {
|
||||
practiceProjectUrls.forEach(url => {
|
||||
cy.visit(url + 'part-1');
|
||||
cy.contains('Complete project demo.');
|
||||
cy.get('[data-cy="project-preview-modal"]').should('be.focused');
|
||||
});
|
||||
});
|
||||
|
||||
// Tests for the absence of an element are tricky, if, as is the case here,
|
||||
// the element is not rendered straight away. So, instead, we test for a
|
||||
// side effect of not showing the modal: an editor is allowed to get focus.
|
||||
it('should NOT appear on the second challenges of each practice project', () => {
|
||||
practiceProjectUrls.forEach(url => {
|
||||
cy.visit(url + 'part-2');
|
||||
cy.focused()
|
||||
.parents()
|
||||
.should('have.class', 'react-monaco-editor-container');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
it('should NOT appear on the first challenges of legacy blocks', () => {
|
||||
legacyFirstChallengeUrls.forEach(url => {
|
||||
cy.visit(url);
|
||||
// if no modals are showing, then the editor should have focus:
|
||||
cy.focused()
|
||||
.parents()
|
||||
.should('have.class', 'react-monaco-editor-container');
|
||||
});
|
||||
});
|
||||
});
|
@ -14,11 +14,19 @@
|
||||
|
||||
const { execSync } = require('child_process');
|
||||
const { existsSync } = require('fs');
|
||||
require('dotenv').config();
|
||||
|
||||
module.exports = (on, config) => {
|
||||
// `on` is used to hook into various events Cypress emits
|
||||
// `config` is the resolved Cypress config
|
||||
config.env = config.env || {};
|
||||
on('before:run', () => {
|
||||
if (!existsSync('../../config/curriculum.json')) {
|
||||
execSync('npm run build:curriculum');
|
||||
}
|
||||
});
|
||||
|
||||
// Allows us to test the new curriculum before it's released:
|
||||
config.env.SHOW_UPCOMING_CHANGES = process.env.SHOW_UPCOMING_CHANGES;
|
||||
return config;
|
||||
};
|
||||
|
Reference in New Issue
Block a user