Files
freeCodeCamp/client/src/templates/Challenges/utils/frame.js

170 lines
4.7 KiB
JavaScript
Raw Normal View History

import { toString, flow } from 'lodash';
import { defer, of, from, Observable, throwError, queueScheduler } from 'rxjs';
import {
tap,
map,
toArray,
delay,
mergeMap,
timeout,
catchError
} from 'rxjs/operators';
2018-11-27 17:00:33 +03:00
import { configure, shallow, mount } from 'enzyme';
import Adapter16 from 'enzyme-adapter-react-16';
2018-11-27 17:00:33 +03:00
import { setConfig } from 'react-hot-loader';
2018-04-06 14:51:52 +01:00
2018-11-27 17:00:33 +03:00
import { isJSEnabledSelector } from '../redux';
2018-04-06 14:51:52 +01:00
// 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
// console.log
const mainId = 'fcc-main-frame';
// the test frame is responsible for running the assert tests
const testId = 'fcc-test-frame';
// base tag here will force relative links
// within iframe to point to '' instead of
2018-04-06 14:51:52 +01:00
// append to the current challenge url
// this also allows in-page anchors to work properly
// rather than load another instance of the learn
//
2018-04-06 14:51:52 +01:00
// if an error occurs during initialization
// the __err prop will be set
// This is then picked up in client/frame-runner.js during
// runTestsInTestFrame below
const createHeader = (id = mainId) => `
<base href='' />
2018-04-06 14:51:52 +01:00
<script>
window.__frameId = '${id}';
window.onerror = function(msg, url, ln, col, err) {
window.__err = err;
return true;
};
</script>
`;
export const runTestsInTestFrame = (document, tests) =>
defer(() => {
2018-04-06 14:51:52 +01:00
const { contentDocument: frame } = document.getElementById(testId);
2018-11-27 17:00:33 +03:00
// Enable Stateless Functional Component. Otherwise, enzyme-adapter-react-16
// does not work correctly.
setConfig({ pureSFC: true });
return frame
.__runTests(tests)
.pipe(tap(() => setConfig({ pureSFC: false })));
2018-04-06 14:51:52 +01:00
});
const createFrame = (document, state, id) => ctx => {
const isJSEnabled = isJSEnabledSelector(state);
2018-04-06 14:51:52 +01:00
const frame = document.createElement('iframe');
frame.id = id;
if (!isJSEnabled) {
frame.sandbox = 'allow-same-origin';
}
return {
...ctx,
element: frame
};
};
const hiddenFrameClassname = 'hide-test-frame';
const mountFrame = document => ({ element, ...rest }) => {
const oldFrame = document.getElementById(element.id);
if (oldFrame) {
element.className = oldFrame.className || hiddenFrameClassname;
oldFrame.parentNode.replaceChild(element, oldFrame);
} else {
element.className = hiddenFrameClassname;
document.body.appendChild(element);
}
return {
...rest,
element,
document: element.contentDocument,
window: element.contentWindow
};
};
const addDepsToDocument = ctx => {
ctx.document.__deps__ = {
rx: {
of,
from,
Observable,
throwError,
queueScheduler,
tap,
map,
toArray,
delay,
mergeMap,
timeout,
catchError
},
log: (...things) => console.log('from test frame', ...things)
};
2018-04-06 14:51:52 +01:00
// using require here prevents nodejs issues as loop-protect
// is added to the window object by webpack and not available to
// us server side.
/* eslint-disable import/no-unresolved */
ctx.document.loopProtect = require('loop-protect');
/* eslint-enable import/no-unresolved */
return ctx;
};
const buildProxyConsole = proxyLogger => ctx => {
const oldLog = ctx.window.console.log.bind(ctx.window.console);
ctx.window.console.log = function proxyConsole(...args) {
proxyLogger.next(args);
2018-04-06 14:51:52 +01:00
return oldLog(...args);
};
return ctx;
};
const writeTestDepsToDocument = frameReady => ctx => {
const { sources, checkChallengePayload } = ctx;
2018-04-06 14:51:52 +01:00
// add enzyme
// TODO: do programatically
// TODO: webpack lazyload this
2018-11-27 17:00:33 +03:00
configure({ adapter: new Adapter16() });
ctx.document.Enzyme = { shallow, mount };
2018-04-06 14:51:52 +01:00
// default for classic challenges
// should not be used for modern
ctx.document.__source = sources && 'index' in sources ? sources['index'] : '';
2018-04-06 14:51:52 +01:00
// provide the file name and get the original source
ctx.document.__getUserInput = fileName => toString(sources[fileName]);
ctx.document.__checkChallengePayload = checkChallengePayload;
ctx.document.__frameReady = frameReady;
2018-04-06 14:51:52 +01:00
return ctx;
};
function writeToFrame(content, frame) {
frame.open();
frame.write(content);
frame.close();
return frame;
}
const writeContentToFrame = ctx => {
writeToFrame(createHeader(ctx.element.id) + ctx.build, ctx.document);
return ctx;
};
export const createMainFramer = (document, state$) =>
flow(
createFrame(document, state$.value, mainId),
2018-04-06 14:51:52 +01:00
mountFrame(document),
addDepsToDocument,
writeContentToFrame
);
export const createTestFramer = (document, state$, frameReady, proxyConsole) =>
flow(
createFrame(document, state$.value, testId),
2018-04-06 14:51:52 +01:00
mountFrame(document),
addDepsToDocument,
writeTestDepsToDocument(frameReady),
buildProxyConsole(proxyConsole),
2018-04-06 14:51:52 +01:00
writeContentToFrame
);