fix: report errors thrown after the frame is ready

Certain challenges involve code that is not run until the user
interacts with the preview (typically via a click listener). This uses
consoleProxy to report those errors.

Error logging has been simplified, reducing the number of places errors
can be reported from.

Some of the redux-saga code has been renamed in an attempt to improve
clarity.
This commit is contained in:
Oliver Eyton-Williams
2019-11-07 14:35:17 +01:00
committed by mrugesh
parent 04d2de96df
commit beecb04c1a
5 changed files with 77 additions and 40 deletions

View File

@ -59,7 +59,6 @@ function tryTransform(wrap = identity) {
return function transformWrappedPoly(source) { return function transformWrappedPoly(source) {
const result = attempt(wrap, source); const result = attempt(wrap, source);
if (isError(result)) { if (isError(result)) {
console.error(result);
// note(Bouncey): Error thrown here to collapse the build pipeline // note(Bouncey): Error thrown here to collapse the build pipeline
// At the minute, it will not bubble up // At the minute, it will not bubble up
// We collapse the pipeline so the app doesn't fall over trying // We collapse the pipeline so the app doesn't fall over trying

View File

@ -50,7 +50,7 @@ export function* executeChallengeSaga() {
); );
yield put(updateTests(tests)); yield put(updateTests(tests));
yield fork(logToConsole, consoleProxy); yield fork(takeEveryLog, consoleProxy);
const proxyLogger = args => consoleProxy.put(args); const proxyLogger = args => consoleProxy.put(args);
const challengeData = yield select(challengeDataSelector); const challengeData = yield select(challengeDataSelector);
@ -59,7 +59,7 @@ export function* executeChallengeSaga() {
const testRunner = yield call( const testRunner = yield call(
getTestRunner, getTestRunner,
buildData, buildData,
proxyLogger, { proxyLogger },
document document
); );
const testResults = yield executeTests(testRunner, tests); const testResults = yield executeTests(testRunner, tests);
@ -74,19 +74,28 @@ export function* executeChallengeSaga() {
} }
} }
function* logToConsole(channel) { function* takeEveryLog(channel) {
// TODO: move all stringifying and escaping into the reducer so there is a
// single place responsible for formatting the logs.
yield takeEvery(channel, function*(args) { yield takeEvery(channel, function*(args) {
yield put(updateLogs(escape(args))); yield put(updateLogs(escape(args)));
}); });
} }
function* takeEveryConsole(channel) {
// TODO: move all stringifying and escaping into the reducer so there is a
// single place responsible for formatting the console output.
yield takeEvery(channel, function*(args) {
yield put(updateConsole(escape(args)));
});
}
function* buildChallengeData(challengeData) { function* buildChallengeData(challengeData) {
try { try {
return yield call(buildChallenge, challengeData); return yield call(buildChallenge, challengeData);
} catch (e) { } catch (e) {
yield put(disableBuildOnError(e)); yield put(disableBuildOnError());
// eslint-disable-next-line no-throw-literal throw e;
throw 'Build failed';
} }
} }
@ -136,30 +145,40 @@ function* previewChallengeSaga() {
return; return;
} }
const logProxy = yield channel();
const consoleProxy = yield channel(); const consoleProxy = yield channel();
const proxyLogger = args => logProxy.put(args);
const proxyUpdateConsole = args => consoleProxy.put(args);
try { try {
yield put(initLogs()); yield put(initLogs());
yield fork(logToConsole, consoleProxy); yield fork(takeEveryLog, logProxy);
const proxyLogger = args => consoleProxy.put(args); yield fork(takeEveryConsole, consoleProxy);
const challengeData = yield select(challengeDataSelector);
const challengeData = yield select(challengeDataSelector);
const buildData = yield buildChallengeData(challengeData); const buildData = yield buildChallengeData(challengeData);
// evaluate the user code in the preview frame or in the worker // evaluate the user code in the preview frame or in the worker
if (challengeHasPreview(challengeData)) { if (challengeHasPreview(challengeData)) {
const document = yield getContext('document'); const document = yield getContext('document');
yield call(updatePreview, buildData, document, proxyLogger); yield call(updatePreview, buildData, document, {
proxyLogger,
proxyUpdateConsole
});
} else if (isJavaScriptChallenge(challengeData)) { } else if (isJavaScriptChallenge(challengeData)) {
const runUserCode = getTestRunner(buildData, proxyLogger); const runUserCode = getTestRunner(buildData, { proxyLogger });
// without a testString the testRunner just evaluates the user's code // without a testString the testRunner just evaluates the user's code
yield call(runUserCode, null, 5000); yield call(runUserCode, null, 5000);
} }
} catch (err) {
console.log(err);
yield put(updateLogs(escape(err)));
} finally {
// consoleProxy is left open to record any errors triggered by user
// input.
logProxy.close();
// To avoid seeing the default console, initialise and output in one call. // To avoid seeing the default console, initialise and output in one call.
yield all([put(initConsole('')), put(logsToConsole('// console output'))]); yield all([put(initConsole('')), put(logsToConsole('// console output'))]);
} catch (err) {
console.error(err);
} finally {
consoleProxy.close();
} }
} }

View File

@ -330,9 +330,8 @@ export const reducer = handleActions(
isBuildEnabled: true, isBuildEnabled: true,
isCodeLocked: false isCodeLocked: false
}), }),
[types.disableBuildOnError]: (state, { payload }) => ({ [types.disableBuildOnError]: state => ({
...state, ...state,
consoleOut: state.consoleOut + ' \n' + payload,
isBuildEnabled: false isBuildEnabled: false
}), }),

View File

@ -85,7 +85,7 @@ const testRunners = {
[challengeTypes.html]: getDOMTestRunner, [challengeTypes.html]: getDOMTestRunner,
[challengeTypes.backend]: getDOMTestRunner [challengeTypes.backend]: getDOMTestRunner
}; };
export function getTestRunner(buildData, proxyLogger, document) { export function getTestRunner(buildData, { proxyLogger }, document) {
const { challengeType } = buildData; const { challengeType } = buildData;
const testRunner = testRunners[challengeType]; const testRunner = testRunners[challengeType];
if (testRunner) { if (testRunner) {

View File

@ -11,17 +11,14 @@ const testId = 'fcc-test-frame';
// append to the current challenge url // append to the current challenge url
// this also allows in-page anchors to work properly // this also allows in-page anchors to work properly
// rather than load another instance of the learn // rather than load another instance of the learn
//
// if an error occurs during initialization // window.onerror is added here to catch any errors thrown during the building
// the __err prop will be set // of the frame.
// This is then picked up in client/frame-runner.js during
// runTestsInTestFrame below
const createHeader = (id = mainId) => ` const createHeader = (id = mainId) => `
<base href='' /> <base href='' />
<script> <script>
window.__frameId = '${id}'; window.__frameId = '${id}';
window.onerror = function(msg, url, ln, col, err) { window.onerror = function(msg) {
window.__err = err;
console.log(msg); console.log(msg);
return true; return true;
}; };
@ -91,8 +88,7 @@ const buildProxyConsole = proxyLogger => ctx => {
}; };
const initTestFrame = frameReady => ctx => { const initTestFrame = frameReady => ctx => {
const contentLoaded = new Promise(resolve => waitForFrame(resolve)(ctx)); waitForFrame(ctx).then(async () => {
contentLoaded.then(async () => {
const { sources, loadEnzyme } = ctx; const { sources, loadEnzyme } = ctx;
// default for classic challenges // default for classic challenges
// should not be used for modern // should not be used for modern
@ -105,15 +101,33 @@ const initTestFrame = frameReady => ctx => {
return ctx; return ctx;
}; };
const waitForFrame = frameReady => ctx => { const initMainFrame = (frameReady, proxyUpdateConsole) => ctx => {
if (ctx.document.readyState === 'loading') { waitForFrame(ctx).then(() => {
ctx.document.addEventListener('DOMContentLoaded', frameReady); // Overwriting the onerror added by createHeader to catch any errors thrown
} else { // after the frame is ready. It has to be overwritten, as proxyUpdateConsole
// cannot be added as part of createHeader.
ctx.window.onerror = function(msg) {
console.log(msg);
if (proxyUpdateConsole) {
proxyUpdateConsole(msg);
}
return true;
};
frameReady(); frameReady();
} });
return ctx; return ctx;
}; };
const waitForFrame = ctx => {
return new Promise(resolve => {
if (ctx.document.readyState === 'loading') {
ctx.document.addEventListener('DOMContentLoaded', resolve);
} else {
resolve();
}
});
};
function writeToFrame(content, frame) { function writeToFrame(content, frame) {
frame.open(); frame.open();
frame.write(content); frame.write(content);
@ -126,17 +140,23 @@ const writeContentToFrame = ctx => {
return ctx; return ctx;
}; };
export const createMainFramer = (document, frameReady, proxyConsole) => export const createMainFramer = (document, frameReady, proxy) =>
createFramer(document, frameReady, proxyConsole, mainId, waitForFrame); createFramer(document, frameReady, proxy, mainId, initMainFrame);
export const createTestFramer = (document, frameReady, proxyConsole) => export const createTestFramer = (document, frameReady, proxy) =>
createFramer(document, frameReady, proxyConsole, testId, initTestFrame); createFramer(document, frameReady, proxy, testId, initTestFrame);
const createFramer = (document, frameReady, proxyConsole, id, init) => const createFramer = (
document,
frameReady,
{ proxyLogger, proxyUpdateConsole },
id,
init
) =>
flow( flow(
createFrame(document, id), createFrame(document, id),
mountFrame(document), mountFrame(document),
buildProxyConsole(proxyLogger),
writeContentToFrame, writeContentToFrame,
buildProxyConsole(proxyConsole), init(frameReady, proxyUpdateConsole)
init(frameReady)
); );