diff --git a/client/src/templates/Challenges/redux/execute-challenge-saga.js b/client/src/templates/Challenges/redux/execute-challenge-saga.js index ef6240df277..9d9bfbb6b3b 100644 --- a/client/src/templates/Challenges/redux/execute-challenge-saga.js +++ b/client/src/templates/Challenges/redux/execute-challenge-saga.js @@ -246,22 +246,22 @@ export function* executeTests(testRunner, tests, testTimeout = 5000) { return testResults; } -// updates preview frame and the fcc console. -export function* previewChallengeSaga(action) { - const flushLogs = action?.type !== actionTypes.previewMounted; +function* flush() { + yield put(initLogs()); + yield put(initConsole('')); +} + +function* previewChallengeSaga() { const isBuildEnabled = yield select(isBuildEnabledSelector); if (!isBuildEnabled) { return; } - const isExecuting = yield select(isExecutingSelector); - // executeChallengeSaga flushes the logs, so there's no need to if that's - // just happened. - if (flushLogs && !isExecuting) { - yield put(initLogs()); - yield put(initConsole('')); + // the challenge execution will update the preview, so this saga doesn't + // need to do anything. + if (yield select(isExecutingSelector)) { + return; } - yield delay(700); const logProxy = yield channel(); const proxyLogger = args => { @@ -317,7 +317,9 @@ export function* previewChallengeSaga(action) { const logs = results[0].logs?.filter( log => !LOGS_TO_IGNORE.some(msg => log.msg === msg) ); - yield put(updateConsole(logs?.map(log => log.msg).join('\n'))); + const output = logs?.map(log => log.msg).join('\n'); + + yield put(updateConsole(output)); } } } @@ -333,10 +335,11 @@ export function* previewChallengeSaga(action) { } } -// TODO: refactor this so that we can use a single saga for all challenge -// updates (then they can all go in the same `takeLatest` call and be cancelled -// appropriately) -function* updatePreviewSaga(action) { +export function* updatePreviewSaga(action) { + yield flush(); + if (action.type === actionTypes.updateFile) { + yield delay(700); + } const challengeData = yield select(challengeDataSelector); if ( challengeData.challengeType === challengeTypes.python || @@ -390,10 +393,14 @@ function* previewProjectSolutionSaga({ payload }) { export function createExecuteChallengeSaga(types) { return [ takeLatest(types.executeChallenge, executeCancellableChallengeSaga), - takeLatest(types.updateFile, updatePreviewSaga), takeLatest( - [types.challengeMounted, types.resetChallenge, types.previewMounted], - previewChallengeSaga + [ + types.updateFile, + types.challengeMounted, + types.resetChallenge, + types.previewMounted + ], + updatePreviewSaga ), takeLatest(types.projectPreviewMounted, previewProjectSolutionSaga) ]; diff --git a/client/src/templates/Challenges/redux/execute-challenge-saga.test.js b/client/src/templates/Challenges/redux/execute-challenge-saga.test.js index b50f2096f00..6dee8217e1f 100644 --- a/client/src/templates/Challenges/redux/execute-challenge-saga.test.js +++ b/client/src/templates/Challenges/redux/execute-challenge-saga.test.js @@ -4,15 +4,7 @@ import { expectSaga } from 'redux-saga-test-plan'; import { describe, it, vi } from 'vitest'; -import { previewChallengeSaga, executeTests } from './execute-challenge-saga'; - -vi.mock('redux-saga/effects', async importOriginal => { - const actual = await importOriginal(); - return { - ...actual, - delay: vi.fn() - }; -}); +import { executeTests, updatePreviewSaga } from './execute-challenge-saga'; vi.mock('i18next', async () => ({ default: { @@ -33,9 +25,9 @@ const challengeMounted = { type: 'challenge.challengeMounted' }; const previewMounted = { type: 'challenge.previewMounted' }; const resetChallenge = { type: 'challenge.resetChallenge' }; -describe('previewChallengeSaga', () => { +describe('updatePreviewSaga', () => { it('flushes logs on challengeMounted', () => { - return expectSaga(previewChallengeSaga, challengeMounted) + return expectSaga(updatePreviewSaga, challengeMounted) .withReducer(reducer) .put({ type: 'challenge.initLogs' }) .silentRun(); @@ -43,15 +35,15 @@ describe('previewChallengeSaga', () => { // warnings. Increasing the timeout just makes the tests take longer. }); it('flushes logs on reset', () => { - return expectSaga(previewChallengeSaga, resetChallenge) + return expectSaga(updatePreviewSaga, resetChallenge) .withReducer(reducer) .put({ type: 'challenge.initLogs' }) .silentRun(); }); - it('does not flush logs on previewMounted', () => { - return expectSaga(previewChallengeSaga, previewMounted) + it('flushes logs on previewMounted', () => { + return expectSaga(updatePreviewSaga, previewMounted) .withReducer(reducer) - .not.put({ type: 'challenge.initLogs' }) + .put({ type: 'challenge.initLogs' }) .silentRun(); }); }); diff --git a/e2e/output.spec.ts b/e2e/output.spec.ts index e73939ce529..918cc90d3c2 100644 --- a/e2e/output.spec.ts +++ b/e2e/output.spec.ts @@ -13,8 +13,8 @@ const outputTexts = { > 1 | var | ^`, empty: `// running tests - 1. You should declare myName with the var keyword, ending with a semicolon - // tests completed`, +1. You should declare myName with the var keyword, ending with a semicolon +// tests completed`, passed: `// running tests // tests completed` }; @@ -33,20 +33,18 @@ const replaceTextInCodeEditor = async ({ browserName, isMobile, text, - containerId = 'editor-container-indexhtml', - updatesConsole = false + containerId = 'editor-container-indexhtml' }: InsertTextParameters) => { await expect(async () => { await clearEditor({ page, browserName, isMobile }); await getEditors(page).fill(text); await expect(page.getByTestId(containerId)).toContainText(text); - if (updatesConsole) { - await expect( - page.getByRole('region', { - name: translations.learn['editor-tabs'].console - }) - ).not.toContainText('Your test output will go here'); - } + + await expect( + page.getByRole('region', { + name: translations.learn['editor-tabs'].console + }) + ).toContainText('Your test output will go here'); }).toPass(); }; @@ -211,12 +209,13 @@ test.describe('Challenge Output Component Tests', () => { page, isMobile }) => { - await runChallengeTest(page, isMobile); + await expect(async () => { + await runChallengeTest(page, isMobile); - await expect(page.getByTestId('output-text')).toContainText( - outputTexts.empty, - { timeout: 10000 } - ); + expect(await page.getByTestId('output-text').textContent()).toEqual( + expect.stringContaining(outputTexts.empty) + ); + }).toPass(); }); test('should contain final output after test pass', async ({ @@ -226,15 +225,17 @@ test.describe('Challenge Output Component Tests', () => { }) => { const closeButton = page.getByRole('button', { name: 'Close' }); await focusEditor({ page, isMobile }); - await replaceTextInCodeEditor({ - browserName, - page, - isMobile, - text: 'var myName;', - containerId: 'editor-container-scriptjs', - updatesConsole: true - }); - await runChallengeTest(page, isMobile); + await expect(async () => { + await replaceTextInCodeEditor({ + browserName, + page, + isMobile, + text: 'var myName;', + containerId: 'editor-container-scriptjs' + }); + await runChallengeTest(page, isMobile); + await expect(closeButton).toBeVisible(); + }).toPass(); await closeButton.click(); await expect(