2021-08-02 15:39:40 +02:00
import i18next from 'i18next' ;
import { escape } from 'lodash-es' ;
import { channel } from 'redux-saga' ;
2018-12-10 08:22:32 +03:00
import {
2019-06-10 15:22:45 +03:00
delay ,
2018-12-10 08:22:32 +03:00
put ,
select ,
call ,
takeLatest ,
takeEvery ,
2019-01-09 03:36:45 +03:00
fork ,
2020-02-03 15:22:49 +02:00
getContext ,
take ,
cancel
2018-12-10 08:22:32 +03:00
} from 'redux-saga/effects' ;
2018-11-26 02:17:38 +03:00
2021-12-01 18:45:17 +00:00
import { playTone } from '../../../utils/tone' ;
2021-08-02 15:39:40 +02:00
import {
buildChallenge ,
canBuildChallenge ,
getTestRunner ,
challengeHasPreview ,
updatePreview ,
2021-11-29 19:30:28 +01:00
updateProjectPreview ,
2021-08-02 15:39:40 +02:00
isJavaScriptChallenge ,
isLoopProtected
} from '../utils/build' ;
import { actionTypes } from './action-types' ;
2018-11-26 02:17:38 +03:00
import {
2019-02-08 17:33:05 +03:00
challengeDataSelector ,
2020-02-04 06:03:56 +01:00
challengeMetaSelector ,
2018-11-26 02:17:38 +03:00
challengeTestsSelector ,
initConsole ,
updateConsole ,
initLogs ,
updateLogs ,
logsToConsole ,
2019-02-08 17:33:05 +03:00
updateTests ,
2021-02-01 13:34:04 +00:00
openModal ,
2019-02-08 17:33:05 +03:00
isBuildEnabledSelector ,
2021-08-02 15:39:40 +02:00
disableBuildOnError
2018-11-26 02:17:38 +03:00
} from './' ;
2019-11-27 02:51:43 +01:00
// How long before bailing out of a preview.
const previewTimeout = 2500 ;
2020-07-20 17:33:56 +02:00
let previewTask ;
2019-11-27 02:51:43 +01:00
2021-02-01 13:34:04 +00:00
export function * executeCancellableChallengeSaga ( payload ) {
2020-07-20 17:33:56 +02:00
if ( previewTask ) {
yield cancel ( previewTask ) ;
}
2021-10-13 13:47:59 +02:00
// executeChallenge with payload containing {showCompletionModal}
2021-02-01 13:34:04 +00:00
const task = yield fork ( executeChallengeSaga , payload ) ;
2020-07-20 17:33:56 +02:00
previewTask = yield fork ( previewChallengeSaga , { flushLogs : false } ) ;
2020-02-03 15:22:49 +02:00
2021-08-02 15:39:40 +02:00
yield take ( actionTypes . cancelTests ) ;
2020-02-03 15:22:49 +02:00
yield cancel ( task ) ;
}
2020-07-20 17:33:56 +02:00
export function * executeCancellablePreviewSaga ( ) {
previewTask = yield fork ( previewChallengeSaga ) ;
}
2021-10-13 13:47:59 +02:00
export function * executeChallengeSaga ( { payload } ) {
2019-02-08 17:33:05 +03:00
const isBuildEnabled = yield select ( isBuildEnabledSelector ) ;
if ( ! isBuildEnabled ) {
return ;
}
2018-12-27 13:34:55 +03:00
const consoleProxy = yield channel ( ) ;
2019-05-26 17:00:12 -04:00
2018-12-05 12:37:48 +03:00
try {
yield put ( initLogs ( ) ) ;
2020-12-16 02:02:52 -06:00
yield put ( initConsole ( i18next . t ( 'learn.running-tests' ) ) ) ;
2019-05-26 17:00:12 -04:00
// reset tests to initial state
2021-05-10 08:48:49 -07:00
const tests = ( yield select ( challengeTestsSelector ) ) . map (
( { text , testString } ) => ( { text , testString } )
) ;
2019-05-26 17:00:12 -04:00
yield put ( updateTests ( tests ) ) ;
2019-11-07 14:35:17 +01:00
yield fork ( takeEveryLog , consoleProxy ) ;
2019-01-09 03:35:31 +03:00
const proxyLogger = args => consoleProxy . put ( args ) ;
2018-12-05 12:37:48 +03:00
2019-02-13 15:47:00 +03:00
const challengeData = yield select ( challengeDataSelector ) ;
2020-02-04 06:03:56 +01:00
const challengeMeta = yield select ( challengeMetaSelector ) ;
const protect = isLoopProtected ( challengeMeta ) ;
const buildData = yield buildChallengeData ( challengeData , {
preview : false ,
2022-01-07 11:42:27 +01:00
protect ,
usesTestRunner : true
2020-02-04 06:03:56 +01:00
} ) ;
2019-02-08 17:33:05 +03:00
const document = yield getContext ( 'document' ) ;
const testRunner = yield call (
getTestRunner ,
buildData ,
2021-04-30 21:30:06 +02:00
{ proxyLogger , removeComments : challengeMeta . removeComments } ,
2019-02-08 17:33:05 +03:00
document
) ;
2019-05-26 17:00:12 -04:00
const testResults = yield executeTests ( testRunner , tests ) ;
2018-12-05 12:37:48 +03:00
yield put ( updateTests ( testResults ) ) ;
2021-02-01 13:34:04 +00:00
const challengeComplete = testResults . every ( test => test . pass && ! test . err ) ;
2021-12-01 18:45:17 +00:00
if ( challengeComplete ) {
playTone ( 'tests-completed' ) ;
} else {
playTone ( 'tests-failed' ) ;
2021-10-27 15:50:29 -07:00
}
2021-10-13 13:47:59 +02:00
if ( challengeComplete && payload ? . showCompletionModal ) {
2021-02-01 13:34:04 +00:00
yield put ( openModal ( 'completion' ) ) ;
}
2020-12-16 02:02:52 -06:00
yield put ( updateConsole ( i18next . t ( 'learn.tests-completed' ) ) ) ;
yield put ( logsToConsole ( i18next . t ( 'learn.console-output' ) ) ) ;
2018-12-05 12:37:48 +03:00
} catch ( e ) {
yield put ( updateConsole ( e ) ) ;
2018-12-27 13:34:55 +03:00
} finally {
consoleProxy . close ( ) ;
2018-11-26 02:17:38 +03:00
}
2018-12-04 17:23:15 +03:00
}
2019-11-07 14:35:17 +01:00
function * takeEveryLog ( channel ) {
// TODO: move all stringifying and escaping into the reducer so there is a
// single place responsible for formatting the logs.
2021-03-11 00:31:46 +05:30
yield takeEvery ( channel , function * ( args ) {
2019-10-31 12:26:10 +00:00
yield put ( updateLogs ( escape ( args ) ) ) ;
2018-12-10 15:06:39 +03:00
} ) ;
}
2019-11-07 14:35:17 +01:00
function * takeEveryConsole ( channel ) {
// TODO: move all stringifying and escaping into the reducer so there is a
// single place responsible for formatting the console output.
2021-03-11 00:31:46 +05:30
yield takeEvery ( channel , function * ( args ) {
2019-11-07 14:35:17 +01:00
yield put ( updateConsole ( escape ( args ) ) ) ;
} ) ;
}
2020-02-04 06:03:56 +01:00
function * buildChallengeData ( challengeData , options ) {
2018-12-27 13:34:55 +03:00
try {
2020-02-04 06:03:56 +01:00
return yield call ( buildChallenge , challengeData , options ) ;
2019-02-08 17:33:05 +03:00
} catch ( e ) {
2019-11-07 14:35:17 +01:00
yield put ( disableBuildOnError ( ) ) ;
throw e ;
2018-12-27 13:34:55 +03:00
}
2018-11-26 02:17:38 +03:00
}
2019-05-26 17:00:12 -04:00
function * executeTests ( testRunner , tests , testTimeout = 5000 ) {
2018-12-10 09:19:47 +03:00
const testResults = [ ] ;
2019-11-04 12:42:19 +01:00
for ( let i = 0 ; i < tests . length ; i ++ ) {
const { text , testString } = tests [ i ] ;
2018-12-10 01:46:26 +03:00
const newTest = { text , testString } ;
2019-11-04 12:42:19 +01:00
// only the last test outputs console.logs to avoid log duplication.
const firstTest = i === 1 ;
2018-12-10 01:46:26 +03:00
try {
2019-11-04 12:42:19 +01:00
const { pass , err } = yield call (
testRunner ,
testString ,
testTimeout ,
firstTest
) ;
2018-12-10 01:46:26 +03:00
if ( pass ) {
newTest . pass = true ;
} else {
throw err ;
}
} catch ( err ) {
2021-11-25 16:10:01 +01:00
const { actual , expected } = err ;
newTest . message = text
. replace ( '--fcc-expected--' , expected )
. replace ( '--fcc-actual--' , actual ) ;
2018-12-10 01:46:26 +03:00
if ( err === 'timeout' ) {
newTest . err = 'Test timed out' ;
newTest . message = ` ${ newTest . message } ( ${ newTest . err } ) ` ;
} else {
const { message , stack } = err ;
newTest . err = message + '\n' + stack ;
newTest . stack = stack ;
}
yield put ( updateConsole ( newTest . message ) ) ;
} finally {
testResults . push ( newTest ) ;
}
}
return testResults ;
}
2019-11-02 12:03:47 +01:00
// updates preview frame and the fcc console.
2020-07-20 17:33:56 +02:00
function * previewChallengeSaga ( { flushLogs = true } = { } ) {
2019-02-13 15:47:00 +03:00
yield delay ( 700 ) ;
2019-02-08 17:33:05 +03:00
const isBuildEnabled = yield select ( isBuildEnabledSelector ) ;
if ( ! isBuildEnabled ) {
return ;
}
2019-11-01 13:02:23 +01:00
2019-11-07 14:35:17 +01:00
const logProxy = yield channel ( ) ;
const proxyLogger = args => logProxy . put ( args ) ;
2019-02-08 17:33:05 +03:00
2018-12-10 17:29:58 +03:00
try {
2020-07-20 17:33:56 +02:00
if ( flushLogs ) {
yield put ( initLogs ( ) ) ;
yield put ( initConsole ( '' ) ) ;
}
2019-11-07 15:54:00 +01:00
yield fork ( takeEveryConsole , logProxy ) ;
2019-11-01 13:02:23 +01:00
2019-11-07 14:35:17 +01:00
const challengeData = yield select ( challengeDataSelector ) ;
2020-02-04 06:03:56 +01:00
2019-11-19 12:46:48 +01:00
if ( canBuildChallenge ( challengeData ) ) {
2020-02-04 06:03:56 +01:00
const challengeMeta = yield select ( challengeMetaSelector ) ;
const protect = isLoopProtected ( challengeMeta ) ;
const buildData = yield buildChallengeData ( challengeData , {
preview : true ,
protect
} ) ;
2019-11-19 12:46:48 +01:00
// evaluate the user code in the preview frame or in the worker
if ( challengeHasPreview ( challengeData ) ) {
const document = yield getContext ( 'document' ) ;
yield call ( updatePreview , buildData , document , proxyLogger ) ;
} else if ( isJavaScriptChallenge ( challengeData ) ) {
2021-04-30 21:30:06 +02:00
const runUserCode = getTestRunner ( buildData , {
proxyLogger ,
removeComments : challengeMeta . removeComments
} ) ;
2019-11-19 12:46:48 +01:00
// without a testString the testRunner just evaluates the user's code
2019-11-27 02:51:43 +01:00
yield call ( runUserCode , null , previewTimeout ) ;
2019-11-19 12:46:48 +01:00
}
2019-10-30 17:15:10 +01:00
}
2018-12-10 17:29:58 +03:00
} catch ( err ) {
2022-01-08 07:25:38 -08:00
if ( err [ 0 ] === 'timeout' ) {
2020-12-16 02:02:52 -06:00
// TODO: translate the error
2019-11-27 02:51:43 +01:00
// eslint-disable-next-line no-ex-assign
2022-01-08 07:25:38 -08:00
err [ 0 ] = ` The code you have written is taking longer than the ${ previewTimeout } ms our challenges allow. You may have created an infinite loop or need to write a more efficient algorithm ` ;
2019-11-27 02:51:43 +01:00
}
2019-11-07 14:35:17 +01:00
console . log ( err ) ;
2019-11-07 15:54:00 +01:00
yield put ( updateConsole ( escape ( err ) ) ) ;
2018-12-10 17:29:58 +03:00
}
}
2021-11-29 19:30:28 +01:00
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 ) ;
}
}
2018-11-26 02:17:38 +03:00
export function createExecuteChallengeSaga ( types ) {
2018-12-10 01:46:26 +03:00
return [
2020-02-03 15:22:49 +02:00
takeLatest ( types . executeChallenge , executeCancellableChallengeSaga ) ,
2018-12-10 01:46:26 +03:00
takeLatest (
[
types . updateFile ,
types . previewMounted ,
types . challengeMounted ,
types . resetChallenge
] ,
2020-07-20 17:33:56 +02:00
executeCancellablePreviewSaga
2021-11-29 19:30:28 +01:00
) ,
takeLatest ( types . projectPreviewMounted , previewProjectSolutionSaga )
2018-12-10 01:46:26 +03:00
] ;
2018-11-26 02:17:38 +03:00
}