Merge branch 'ValeraS-fix/disable-build-on-error'

This commit is contained in:
Bouncey
2019-02-16 17:50:02 +00:00
5 changed files with 214 additions and 146 deletions

View File

@ -10,56 +10,48 @@ import {
import { delay, channel } from 'redux-saga'; import { delay, channel } from 'redux-saga';
import { import {
backendFormValuesSelector, challengeDataSelector,
challengeFilesSelector,
challengeMetaSelector,
challengeTestsSelector, challengeTestsSelector,
initConsole, initConsole,
updateConsole, updateConsole,
initLogs, initLogs,
updateLogs, updateLogs,
logsToConsole, logsToConsole,
updateTests updateTests,
isBuildEnabledSelector,
disableBuildOnError
} from './'; } from './';
import { import {
buildJSChallenge, buildChallenge,
buildDOMChallenge, getTestRunner,
buildBackendChallenge challengeHasPreview,
updatePreview
} from '../utils/build'; } from '../utils/build';
import { challengeTypes } from '../../../../utils/challengeTypes';
import createWorker from '../utils/worker-executor';
import {
createMainFramer,
createTestFramer,
runTestInTestFrame
} from '../utils/frame.js';
export function* executeChallengeSaga() { export function* executeChallengeSaga() {
const isBuildEnabled = yield select(isBuildEnabledSelector);
if (!isBuildEnabled) {
return;
}
const consoleProxy = yield channel(); const consoleProxy = yield channel();
try { try {
const { js, bonfire, backend } = challengeTypes;
const { challengeType } = yield select(challengeMetaSelector);
yield put(initLogs()); yield put(initLogs());
yield put(initConsole('// running tests')); yield put(initConsole('// running tests'));
yield fork(logToConsole, consoleProxy); yield fork(logToConsole, consoleProxy);
const proxyLogger = args => consoleProxy.put(args); const proxyLogger = args => consoleProxy.put(args);
let testResults; const challengeData = yield select(challengeDataSelector);
switch (challengeType) { const buildData = yield buildChallengeData(challengeData);
case js: const document = yield getContext('document');
case bonfire: const testRunner = yield call(
testResults = yield executeJSChallengeSaga(proxyLogger); getTestRunner,
break; buildData,
case backend: proxyLogger,
testResults = yield executeBackendChallengeSaga(proxyLogger); document
break; );
default: const testResults = yield executeTests(testRunner);
testResults = yield executeDOMChallengeSaga(proxyLogger);
}
yield put(updateTests(testResults)); yield put(updateTests(testResults));
yield put(updateConsole('// tests completed')); yield put(updateConsole('// tests completed'));
@ -77,63 +69,16 @@ function* logToConsole(channel) {
}); });
} }
function* executeJSChallengeSaga(proxyLogger) { function* buildChallengeData(challengeData) {
const files = yield select(challengeFilesSelector);
const { build, sources } = yield call(buildJSChallenge, files);
const code = sources && 'index' in sources ? sources['index'] : '';
const testWorker = createWorker('test-evaluator');
testWorker.on('LOG', proxyLogger);
try { try {
return yield call(executeTests, async(testString, testTimeout) => { return yield call(buildChallenge, challengeData);
try { } catch (e) {
return await testWorker.execute( yield put(disableBuildOnError(e));
{ build, testString, code, sources }, // eslint-disable-next-line no-throw-literal
testTimeout throw 'Build failed';
);
} finally {
testWorker.killWorker();
}
});
} finally {
testWorker.remove('LOG', proxyLogger);
} }
} }
function createTestFrame(document, ctx, proxyLogger) {
return new Promise(resolve =>
createTestFramer(document, resolve, proxyLogger)(ctx)
);
}
function* executeDOMChallengeSaga(proxyLogger) {
const files = yield select(challengeFilesSelector);
const meta = yield select(challengeMetaSelector);
const document = yield getContext('document');
const ctx = yield call(buildDOMChallenge, files, meta);
ctx.loadEnzyme = Object.keys(files).some(key => files[key].ext === 'jsx');
yield call(createTestFrame, document, ctx, proxyLogger);
// wait for a code execution on a "ready" event in jQuery challenges
yield delay(100);
return yield call(executeTests, (testString, testTimeout) =>
runTestInTestFrame(document, testString, testTimeout)
);
}
// TODO: use a web worker
function* executeBackendChallengeSaga(proxyLogger) {
const formValues = yield select(backendFormValuesSelector);
const document = yield getContext('document');
const ctx = yield call(buildBackendChallenge, formValues);
yield call(createTestFrame, document, ctx, proxyLogger);
return yield call(executeTests, (testString, testTimeout) =>
runTestInTestFrame(document, testString, testTimeout)
);
}
function* executeTests(testRunner) { function* executeTests(testRunner) {
const tests = yield select(challengeTestsSelector); const tests = yield select(challengeTestsSelector);
const testTimeout = 5000; const testTimeout = 5000;
@ -167,20 +112,23 @@ function* executeTests(testRunner) {
return testResults; return testResults;
} }
function* updateMainSaga() { function* previewChallengeSaga() {
yield delay(500); yield delay(700);
const isBuildEnabled = yield select(isBuildEnabledSelector);
if (!isBuildEnabled) {
return;
}
const challengeData = yield select(challengeDataSelector);
if (!challengeHasPreview(challengeData)) {
return;
}
try { try {
const { html, modern } = challengeTypes; yield put(initConsole(''));
const meta = yield select(challengeMetaSelector); const ctx = yield buildChallengeData(challengeData);
const { challengeType } = meta;
if (challengeType !== html && challengeType !== modern) {
return;
}
const files = yield select(challengeFilesSelector);
const ctx = yield call(buildDOMChallenge, files, meta);
const document = yield getContext('document'); const document = yield getContext('document');
const frameMain = yield call(createMainFramer, document); yield call(updatePreview, ctx, document);
yield call(frameMain, ctx);
} catch (err) { } catch (err) {
console.error(err); console.error(err);
} }
@ -196,7 +144,7 @@ export function createExecuteChallengeSaga(types) {
types.challengeMounted, types.challengeMounted,
types.resetChallenge types.resetChallenge
], ],
updateMainSaga previewChallengeSaga
) )
]; ];
} }

View File

@ -14,6 +14,7 @@ import codeStorageEpic from './code-storage-epic';
import { createIdToNameMapSaga } from './id-to-name-map-saga'; import { createIdToNameMapSaga } from './id-to-name-map-saga';
import { createExecuteChallengeSaga } from './execute-challenge-saga'; import { createExecuteChallengeSaga } from './execute-challenge-saga';
import { createCurrentChallengeSaga } from './current-challenge-saga'; import { createCurrentChallengeSaga } from './current-challenge-saga';
import { challengeTypes } from '../../../../utils/challengeTypes';
export const ns = 'challenge'; export const ns = 'challenge';
export const backendNS = 'backendChallenge'; export const backendNS = 'backendChallenge';
@ -23,12 +24,14 @@ const initialState = {
challengeIdToNameMap: {}, challengeIdToNameMap: {},
challengeMeta: { challengeMeta: {
id: '', id: '',
nextChallengePath: '/' nextChallengePath: '/',
introPath: '',
challengeType: -1
}, },
challengeTests: [], challengeTests: [],
consoleOut: '', consoleOut: '',
isCodeLocked: false, isCodeLocked: false,
isJSEnabled: true, isBuildEnabled: true,
modal: { modal: {
completion: false, completion: false,
help: false, help: false,
@ -59,7 +62,7 @@ export const types = createTypes(
'lockCode', 'lockCode',
'unlockCode', 'unlockCode',
'disableJSOnError', 'disableBuildOnError',
'storedCodeFound', 'storedCodeFound',
'noStoredCodeFound', 'noStoredCodeFound',
@ -136,7 +139,7 @@ export const logsToConsole = createAction(types.logsToConsole);
export const lockCode = createAction(types.lockCode); export const lockCode = createAction(types.lockCode);
export const unlockCode = createAction(types.unlockCode); export const unlockCode = createAction(types.unlockCode);
export const disableJSOnError = createAction(types.disableJSOnError); export const disableBuildOnError = createAction(types.disableBuildOnError);
export const storedCodeFound = createAction(types.storedCodeFound); export const storedCodeFound = createAction(types.storedCodeFound);
export const noStoredCodeFound = createAction(types.noStoredCodeFound); export const noStoredCodeFound = createAction(types.noStoredCodeFound);
@ -165,13 +168,55 @@ export const isCompletionModalOpenSelector = state =>
export const isHelpModalOpenSelector = state => state[ns].modal.help; export const isHelpModalOpenSelector = state => state[ns].modal.help;
export const isVideoModalOpenSelector = state => state[ns].modal.video; export const isVideoModalOpenSelector = state => state[ns].modal.video;
export const isResetModalOpenSelector = state => state[ns].modal.reset; export const isResetModalOpenSelector = state => state[ns].modal.reset;
export const isJSEnabledSelector = state => state[ns].isJSEnabled; export const isBuildEnabledSelector = state => state[ns].isBuildEnabled;
export const successMessageSelector = state => state[ns].successMessage; export const successMessageSelector = state => state[ns].successMessage;
export const backendFormValuesSelector = state => state.form[backendNS]; export const backendFormValuesSelector = state => state.form[backendNS];
export const projectFormValuesSelector = state => export const projectFormValuesSelector = state =>
state[ns].projectFormValues || {}; state[ns].projectFormValues || {};
export const challengeDataSelector = state => {
const { challengeType } = challengeMetaSelector(state);
let challengeData = { challengeType };
if (
challengeType === challengeTypes.js ||
challengeType === challengeTypes.bonfire
) {
challengeData = {
...challengeData,
files: challengeFilesSelector(state)
};
} else if (challengeType === challengeTypes.backend) {
const {
solution: { value: url }
} = backendFormValuesSelector(state);
challengeData = {
...challengeData,
url
};
} else if (
challengeType === challengeTypes.frontEndProject ||
challengeType === challengeTypes.backendEndProject
) {
challengeData = {
...challengeData,
...projectFormValuesSelector(state)
};
} else if (
challengeType === challengeTypes.html ||
challengeType === challengeTypes.modern
) {
const { required = [], template = '' } = challengeMetaSelector(state);
challengeData = {
...challengeData,
files: challengeFilesSelector(state),
required,
template
};
}
return challengeData;
};
export const reducer = handleActions( export const reducer = handleActions(
{ {
[types.fetchIdToNameMapComplete]: (state, { payload }) => ({ [types.fetchIdToNameMapComplete]: (state, { payload }) => ({
@ -269,13 +314,13 @@ export const reducer = handleActions(
}), }),
[types.unlockCode]: state => ({ [types.unlockCode]: state => ({
...state, ...state,
isJSEnabled: true, isBuildEnabled: true,
isCodeLocked: false isCodeLocked: false
}), }),
[types.disableJSOnError]: (state, { payload }) => ({ [types.disableBuildOnError]: (state, { payload }) => ({
...state, ...state,
consoleOut: state.consoleOut + ' \n' + payload, consoleOut: state.consoleOut + ' \n' + payload,
isJSEnabled: false isBuildEnabled: false
}), }),
[types.updateSuccessMessage]: (state, { payload }) => ({ [types.updateSuccessMessage]: (state, { payload }) => ({

View File

@ -1,5 +1,12 @@
import { transformers } from '../rechallenge/transformers'; import { transformers } from '../rechallenge/transformers';
import { cssToHtml, jsToHtml, concatHtml } from '../rechallenge/builders.js'; import { cssToHtml, jsToHtml, concatHtml } from '../rechallenge/builders.js';
import { challengeTypes } from '../../../../utils/challengeTypes';
import createWorker from './worker-executor';
import {
createTestFramer,
runTestInTestFrame,
createMainFramer
} from './frame';
const frameRunner = [ const frameRunner = [
{ {
@ -49,9 +56,67 @@ function checkFilesErrors(files) {
return files; return files;
} }
export function buildDOMChallenge(files, meta = {}) { const buildFunctions = {
const { required = [], template = '' } = meta; [challengeTypes.js]: buildJSChallenge,
[challengeTypes.bonfire]: buildJSChallenge,
[challengeTypes.html]: buildDOMChallenge,
[challengeTypes.modern]: buildDOMChallenge,
[challengeTypes.backend]: buildBackendChallenge
};
export async function buildChallenge(challengeData) {
const { challengeType } = challengeData;
let build = buildFunctions[challengeType];
if (build) {
return build(challengeData);
}
throw new Error(`Cannot build challenge of type ${challengeType}`);
}
const testRunners = {
[challengeTypes.js]: getJSTestRunner,
[challengeTypes.html]: getDOMTestRunner,
[challengeTypes.backend]: getDOMTestRunner
};
export function getTestRunner(buildData, proxyLogger, document) {
const { challengeType } = buildData;
const testRunner = testRunners[challengeType];
if (testRunner) {
return testRunner(buildData, proxyLogger, document);
}
throw new Error(`Cannot get test runner for challenge type ${challengeType}`);
}
function getJSTestRunner({ build, sources }, proxyLogger) {
const code = sources && 'index' in sources ? sources['index'] : '';
const testWorker = createWorker('test-evaluator');
return async(testString, testTimeout) => {
try {
testWorker.on('LOG', proxyLogger);
return await testWorker.execute(
{ build, testString, code, sources },
testTimeout
);
} finally {
testWorker.killWorker();
testWorker.remove('LOG', proxyLogger);
}
};
}
async function getDOMTestRunner(buildData, proxyLogger, document) {
await new Promise(resolve =>
createTestFramer(document, resolve, proxyLogger)(buildData)
);
return (testString, testTimeout) =>
runTestInTestFrame(document, testString, testTimeout);
}
export function buildDOMChallenge({ files, required = [], template = '' }) {
const finalRequires = [...globalRequires, ...required, ...frameRunner]; const finalRequires = [...globalRequires, ...required, ...frameRunner];
const loadEnzyme = Object.keys(files).some(key => files[key].ext === 'jsx');
const toHtml = [jsToHtml, cssToHtml]; const toHtml = [jsToHtml, cssToHtml];
const pipeLine = composeFunctions(...transformers, ...toHtml); const pipeLine = composeFunctions(...transformers, ...toHtml);
const finalFiles = Object.keys(files) const finalFiles = Object.keys(files)
@ -60,12 +125,14 @@ export function buildDOMChallenge(files, meta = {}) {
return Promise.all(finalFiles) return Promise.all(finalFiles)
.then(checkFilesErrors) .then(checkFilesErrors)
.then(files => ({ .then(files => ({
challengeType: challengeTypes.html,
build: concatHtml({ required: finalRequires, template, files }), build: concatHtml({ required: finalRequires, template, files }),
sources: buildSourceMap(files) sources: buildSourceMap(files),
loadEnzyme
})); }));
} }
export function buildJSChallenge(files) { export function buildJSChallenge({ files }) {
const pipeLine = composeFunctions(...transformers); const pipeLine = composeFunctions(...transformers);
const finalFiles = Object.keys(files) const finalFiles = Object.keys(files)
.map(key => files[key]) .map(key => files[key])
@ -73,6 +140,7 @@ export function buildJSChallenge(files) {
return Promise.all(finalFiles) return Promise.all(finalFiles)
.then(checkFilesErrors) .then(checkFilesErrors)
.then(files => ({ .then(files => ({
challengeType: challengeTypes.js,
build: files build: files
.reduce( .reduce(
(body, file) => [...body, file.head, file.contents, file.tail], (body, file) => [...body, file.head, file.contents, file.tail],
@ -83,12 +151,26 @@ export function buildJSChallenge(files) {
})); }));
} }
export function buildBackendChallenge(formValues) { export function buildBackendChallenge({ url }) {
const {
solution: { value: url }
} = formValues;
return { return {
challengeType: challengeTypes.backend,
build: concatHtml({ required: frameRunner }), build: concatHtml({ required: frameRunner }),
sources: { url } sources: { url }
}; };
} }
export function updatePreview(buildData, document) {
const { challengeType } = buildData;
if (challengeType === challengeTypes.html) {
createMainFramer(document)(buildData);
} else {
throw new Error(`Cannot show preview for challenge type ${challengeType}`);
}
}
export function challengeHasPreview({ challengeType }) {
return (
challengeType === challengeTypes.html ||
challengeType === challengeTypes.modern
);
}

View File

@ -280,9 +280,8 @@ async function createTestRunnerForDOMChallenge(
files[0].contents = solution; files[0].contents = solution;
} }
const loadEnzyme = files[0].ext === 'jsx'; const { build, sources, loadEnzyme } = await buildDOMChallenge({
files,
const { build, sources } = await buildDOMChallenge(files, {
required, required,
template template
}); });
@ -324,7 +323,7 @@ async function createTestRunnerForJSChallenge({ files }, solution) {
files[0].contents = solution; files[0].contents = solution;
} }
const { build, sources } = await buildJSChallenge(files); const { build, sources } = await buildJSChallenge({ files });
const code = sources && 'index' in sources ? sources['index'] : ''; const code = sources && 'index' in sources ? sources['index'] : '';
const testWorker = createWorker('test-evaluator'); const testWorker = createWorker('test-evaluator');

View File

@ -3,10 +3,9 @@ title: Logical Operators
--- ---
# Logical Operators # Logical Operators
Logical operators compare Boolean values and return a Boolean response. There are two types of logical operators - Logical AND and Logical OR. These operators are often written as && for AND, and || for OR.
Logical operators compare Boolean values and return a Boolean response. There are two types of logical operators: Logical AND, and Logical OR. These operators are often written as && for AND, and || for OR. ## Logical AND ( && )
#### Logical AND ( && )
The AND operator compares two expressions. If the first evaluates as ["truthy"](https://developer.mozilla.org/en-US/docs/Glossary/Truthy), the statement will return the value of the second expression. The AND operator compares two expressions. If the first evaluates as ["truthy"](https://developer.mozilla.org/en-US/docs/Glossary/Truthy), the statement will return the value of the second expression.
If the first evaluates as ["falsy"](https://developer.mozilla.org/en-US/docs/Glossary/Falsy), the statement will return the value of the first expression. If the first evaluates as ["falsy"](https://developer.mozilla.org/en-US/docs/Glossary/Falsy), the statement will return the value of the first expression.
@ -22,9 +21,9 @@ undefined && 'abc' //returns the first value, undefined
0 && false //returns the first value, 0 0 && false //returns the first value, 0
``` ```
#### Logical OR ( || ) ## Logical OR ( || )
The OR operator compares two expressions. If the first evaluates as "falsy", the statement will return the value of the second second expression. If the first evaluates as "truthy", the statement will return the value of the first expression. The OR operator compares two expressions. If the first evaluates as "falsy", the statement will return the value of the second expression. If the first evaluates as "truthy", the statement will return the value of the first expression.
When only involving boolean values (either `true` or `false`), it returns true if either expression is true. Both expressions can be true, but only one is needed to get true as a result. When only involving boolean values (either `true` or `false`), it returns true if either expression is true. Both expressions can be true, but only one is needed to get true as a result.
```js ```js
@ -36,28 +35,15 @@ false || false //returns the second value, false
undefined || 'abc' //returns the second value, 'abc' undefined || 'abc' //returns the second value, 'abc'
0 || false //returns the second value, false 0 || false //returns the second value, false
``` ```
#### Short-circuit evaluation
&& and || behave as a short-circuit operators.
In case of the logical AND, if the first operand returns false, the second operand is never evaluated and first operand is returned.
In case of the logical OR, if the first value returns true, the second value is never evaluated and the first operand is returned.
```js
false && (anything) // short-circuit evaluated to false.
true || (anything) // short-circuit evaluated to true.
```
#### Logical NOT (!)
## Logical NOT (!)
The NOT operator does not do any comparison like the AND and OR operators.Moreover it is operated only on 1 operand. The NOT operator does not do any comparison like the AND and OR operators.Moreover it is operated only on 1 operand.
An '!' (exclamation) mark is used for representing the NOT operator. An '!' (exclamation) mark is used for representing the NOT operator.
###### Use of NOT operators ### Use of NOT operators
- conversion of the expression into boolean.
1. conversion of the expression into boolean. - returns the inverse of the boolean value obtained in last step.
2. returns the inverse of the boolean value obtained in last step.
```js ```js
var spam = 'rinki'; //spam may be equal to any non empty string var spam = 'rinki'; //spam may be equal to any non empty string
@ -74,10 +60,20 @@ var booSpam2 = !spam2;
inverse of which is evaluated to true. inverse of which is evaluated to true.
*/ */
``` ```
#### Tips:
Both logical operators will return the value of the last evaluated expression. For example:
## Short-circuit evaluation
`&&` and `||` behave as a short-circuit operators.
In case of the logical AND, if the first operand returns false, the second operand is never evaluated and first operand is returned.
In case of the logical OR, if the first value returns true, the second value is never evaluated and the first operand is returned.
Some Examples:
```js ```js
false && (anything) // short-circuit evaluated to false.
true && (anything) // short-circuit evaluated to the result of the expression (anything)
true || (anything) // short-circuit evaluated to true.
"cat" && "dog" //returns "dog" "cat" && "dog" //returns "dog"
"cat" && false //returns false "cat" && false //returns false
0 && "cat" //returns 0 (which is a falsy value) 0 && "cat" //returns 0 (which is a falsy value)
@ -89,8 +85,6 @@ Both logical operators will return the value of the last evaluated expression. F
Note that where `&&` returns the first value, `||` returns the second value and vice versa. Note that where `&&` returns the first value, `||` returns the second value and vice versa.
#### More information: ## Additional Resources
- [Javascript Truth Table](https://guide.freecodecamp.org/javascript/truth-table)
* [Javascript Truth Table](https://guide.freecodecamp.org/javascript/truth-table) - [MDN](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Operators/Logical_Operators)
* [MDN](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Operators/Logical_Operators)