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:
committed by
mrugesh
parent
04d2de96df
commit
beecb04c1a
@ -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
|
||||||
|
@ -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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
@ -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) {
|
||||||
|
@ -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
|
||||||
frameReady();
|
// cannot be added as part of createHeader.
|
||||||
|
ctx.window.onerror = function(msg) {
|
||||||
|
console.log(msg);
|
||||||
|
if (proxyUpdateConsole) {
|
||||||
|
proxyUpdateConsole(msg);
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
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)
|
|
||||||
);
|
);
|
||||||
|
Reference in New Issue
Block a user