Skip to content

Commit 9aa6f05

Browse files
fix(client): duplicate console output (freeCodeCamp#66350)
1 parent b72d31c commit 9aa6f05

3 files changed

Lines changed: 58 additions & 58 deletions

File tree

client/src/templates/Challenges/redux/execute-challenge-saga.js

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -246,22 +246,22 @@ export function* executeTests(testRunner, tests, testTimeout = 5000) {
246246
return testResults;
247247
}
248248

249-
// updates preview frame and the fcc console.
250-
export function* previewChallengeSaga(action) {
251-
const flushLogs = action?.type !== actionTypes.previewMounted;
249+
function* flush() {
250+
yield put(initLogs());
251+
yield put(initConsole(''));
252+
}
253+
254+
function* previewChallengeSaga() {
252255
const isBuildEnabled = yield select(isBuildEnabledSelector);
253256
if (!isBuildEnabled) {
254257
return;
255258
}
256259

257-
const isExecuting = yield select(isExecutingSelector);
258-
// executeChallengeSaga flushes the logs, so there's no need to if that's
259-
// just happened.
260-
if (flushLogs && !isExecuting) {
261-
yield put(initLogs());
262-
yield put(initConsole(''));
260+
// the challenge execution will update the preview, so this saga doesn't
261+
// need to do anything.
262+
if (yield select(isExecutingSelector)) {
263+
return;
263264
}
264-
yield delay(700);
265265

266266
const logProxy = yield channel();
267267
const proxyLogger = args => {
@@ -317,7 +317,9 @@ export function* previewChallengeSaga(action) {
317317
const logs = results[0].logs?.filter(
318318
log => !LOGS_TO_IGNORE.some(msg => log.msg === msg)
319319
);
320-
yield put(updateConsole(logs?.map(log => log.msg).join('\n')));
320+
const output = logs?.map(log => log.msg).join('\n');
321+
322+
yield put(updateConsole(output));
321323
}
322324
}
323325
}
@@ -333,10 +335,11 @@ export function* previewChallengeSaga(action) {
333335
}
334336
}
335337

336-
// TODO: refactor this so that we can use a single saga for all challenge
337-
// updates (then they can all go in the same `takeLatest` call and be cancelled
338-
// appropriately)
339-
function* updatePreviewSaga(action) {
338+
export function* updatePreviewSaga(action) {
339+
yield flush();
340+
if (action.type === actionTypes.updateFile) {
341+
yield delay(700);
342+
}
340343
const challengeData = yield select(challengeDataSelector);
341344
if (
342345
challengeData.challengeType === challengeTypes.python ||
@@ -390,10 +393,14 @@ function* previewProjectSolutionSaga({ payload }) {
390393
export function createExecuteChallengeSaga(types) {
391394
return [
392395
takeLatest(types.executeChallenge, executeCancellableChallengeSaga),
393-
takeLatest(types.updateFile, updatePreviewSaga),
394396
takeLatest(
395-
[types.challengeMounted, types.resetChallenge, types.previewMounted],
396-
previewChallengeSaga
397+
[
398+
types.updateFile,
399+
types.challengeMounted,
400+
types.resetChallenge,
401+
types.previewMounted
402+
],
403+
updatePreviewSaga
397404
),
398405
takeLatest(types.projectPreviewMounted, previewProjectSolutionSaga)
399406
];

client/src/templates/Challenges/redux/execute-challenge-saga.test.js

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,7 @@
44
import { expectSaga } from 'redux-saga-test-plan';
55
import { describe, it, vi } from 'vitest';
66

7-
import { previewChallengeSaga, executeTests } from './execute-challenge-saga';
8-
9-
vi.mock('redux-saga/effects', async importOriginal => {
10-
const actual = await importOriginal();
11-
return {
12-
...actual,
13-
delay: vi.fn()
14-
};
15-
});
7+
import { executeTests, updatePreviewSaga } from './execute-challenge-saga';
168

179
vi.mock('i18next', async () => ({
1810
default: {
@@ -33,25 +25,25 @@ const challengeMounted = { type: 'challenge.challengeMounted' };
3325
const previewMounted = { type: 'challenge.previewMounted' };
3426
const resetChallenge = { type: 'challenge.resetChallenge' };
3527

36-
describe('previewChallengeSaga', () => {
28+
describe('updatePreviewSaga', () => {
3729
it('flushes logs on challengeMounted', () => {
38-
return expectSaga(previewChallengeSaga, challengeMounted)
30+
return expectSaga(updatePreviewSaga, challengeMounted)
3931
.withReducer(reducer)
4032
.put({ type: 'challenge.initLogs' })
4133
.silentRun();
4234
// TODO: figure out why silentRun is necessary. Without it, we get timeout
4335
// warnings. Increasing the timeout just makes the tests take longer.
4436
});
4537
it('flushes logs on reset', () => {
46-
return expectSaga(previewChallengeSaga, resetChallenge)
38+
return expectSaga(updatePreviewSaga, resetChallenge)
4739
.withReducer(reducer)
4840
.put({ type: 'challenge.initLogs' })
4941
.silentRun();
5042
});
51-
it('does not flush logs on previewMounted', () => {
52-
return expectSaga(previewChallengeSaga, previewMounted)
43+
it('flushes logs on previewMounted', () => {
44+
return expectSaga(updatePreviewSaga, previewMounted)
5345
.withReducer(reducer)
54-
.not.put({ type: 'challenge.initLogs' })
46+
.put({ type: 'challenge.initLogs' })
5547
.silentRun();
5648
});
5749
});

e2e/output.spec.ts

Lines changed: 26 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ const outputTexts = {
1313
> 1 | var
1414
| ^`,
1515
empty: `// running tests
16-
1. You should declare myName with the var keyword, ending with a semicolon
17-
// tests completed`,
16+
1. You should declare myName with the var keyword, ending with a semicolon
17+
// tests completed`,
1818
passed: `// running tests
1919
// tests completed`
2020
};
@@ -33,20 +33,18 @@ const replaceTextInCodeEditor = async ({
3333
browserName,
3434
isMobile,
3535
text,
36-
containerId = 'editor-container-indexhtml',
37-
updatesConsole = false
36+
containerId = 'editor-container-indexhtml'
3837
}: InsertTextParameters) => {
3938
await expect(async () => {
4039
await clearEditor({ page, browserName, isMobile });
4140
await getEditors(page).fill(text);
4241
await expect(page.getByTestId(containerId)).toContainText(text);
43-
if (updatesConsole) {
44-
await expect(
45-
page.getByRole('region', {
46-
name: translations.learn['editor-tabs'].console
47-
})
48-
).not.toContainText('Your test output will go here');
49-
}
42+
43+
await expect(
44+
page.getByRole('region', {
45+
name: translations.learn['editor-tabs'].console
46+
})
47+
).toContainText('Your test output will go here');
5048
}).toPass();
5149
};
5250

@@ -211,12 +209,13 @@ test.describe('Challenge Output Component Tests', () => {
211209
page,
212210
isMobile
213211
}) => {
214-
await runChallengeTest(page, isMobile);
212+
await expect(async () => {
213+
await runChallengeTest(page, isMobile);
215214

216-
await expect(page.getByTestId('output-text')).toContainText(
217-
outputTexts.empty,
218-
{ timeout: 10000 }
219-
);
215+
expect(await page.getByTestId('output-text').textContent()).toEqual(
216+
expect.stringContaining(outputTexts.empty)
217+
);
218+
}).toPass();
220219
});
221220

222221
test('should contain final output after test pass', async ({
@@ -226,15 +225,17 @@ test.describe('Challenge Output Component Tests', () => {
226225
}) => {
227226
const closeButton = page.getByRole('button', { name: 'Close' });
228227
await focusEditor({ page, isMobile });
229-
await replaceTextInCodeEditor({
230-
browserName,
231-
page,
232-
isMobile,
233-
text: 'var myName;',
234-
containerId: 'editor-container-scriptjs',
235-
updatesConsole: true
236-
});
237-
await runChallengeTest(page, isMobile);
228+
await expect(async () => {
229+
await replaceTextInCodeEditor({
230+
browserName,
231+
page,
232+
isMobile,
233+
text: 'var myName;',
234+
containerId: 'editor-container-scriptjs'
235+
});
236+
await runChallengeTest(page, isMobile);
237+
await expect(closeButton).toBeVisible();
238+
}).toPass();
238239
await closeButton.click();
239240

240241
await expect(

0 commit comments

Comments
 (0)