fix(client): retain editableContents through reload (#59573)

This commit is contained in:
Oliver Eyton-Williams
2025-04-23 17:27:30 +02:00
committed by GitHub
parent 310d0c691c
commit 796368094c
6 changed files with 71 additions and 39 deletions

View File

@@ -108,7 +108,7 @@ export interface EditorProps {
previewOpen: boolean; previewOpen: boolean;
updateFile: (object: { updateFile: (object: {
fileKey: string; fileKey: string;
editorValue: string; contents: string;
editableRegionBoundaries?: number[]; editableRegionBoundaries?: number[];
}) => void; }) => void;
usesMultifileEditor: boolean; usesMultifileEditor: boolean;
@@ -846,7 +846,7 @@ const Editor = (props: EditorProps): JSX.Element => {
} }
} }
const onChange = (editorValue: string) => { const onChange = (contents: string) => {
const { updateFile, fileKey, isResetting } = props; const { updateFile, fileKey, isResetting } = props;
if (isResetting) return; if (isResetting) return;
// TODO: now that we have getCurrentEditableRegion, should the overlays // TODO: now that we have getCurrentEditableRegion, should the overlays
@@ -873,7 +873,7 @@ const Editor = (props: EditorProps): JSX.Element => {
} }
}); });
} }
updateFile({ fileKey, editorValue, editableRegionBoundaries }); updateFile({ fileKey, contents, editableRegionBoundaries });
}; };
function createBreadcrumb(): HTMLElement { function createBreadcrumb(): HTMLElement {

View File

@@ -23,7 +23,6 @@ export const actionTypes = createTypes(
'cancelTests', 'cancelTests',
'logsToConsole', 'logsToConsole',
'disableBuildOnError', 'disableBuildOnError',
'storedCodeFound',
'noStoredCodeFound', 'noStoredCodeFound',
'saveEditorContent', 'saveEditorContent',
'setShowPreviewPane', 'setShowPreviewPane',

View File

@@ -1,24 +1,8 @@
import { createAction } from 'redux-actions'; import { createAction } from 'redux-actions';
import { getLines } from '../../../../../shared/utils/get-lines';
import { actionTypes } from './action-types'; import { actionTypes } from './action-types';
export const createFiles = createAction( export const createFiles = createAction(actionTypes.createFiles);
actionTypes.createFiles,
challengeFiles =>
challengeFiles.map(challengeFile => ({
...challengeFile,
seed: challengeFile.contents.slice(),
editableContents: getLines(
challengeFile.contents,
challengeFile.editableRegionBoundaries
),
editableRegionBoundaries:
challengeFile.editableRegionBoundaries?.slice() ?? [],
seedEditableRegionBoundaries:
challengeFile.editableRegionBoundaries?.slice() ?? []
}))
);
export const createQuestion = createAction(actionTypes.createQuestion); export const createQuestion = createAction(actionTypes.createQuestion);
export const initTests = createAction(actionTypes.initTests); export const initTests = createAction(actionTypes.initTests);
@@ -50,7 +34,6 @@ export const logsToConsole = createAction(actionTypes.logsToConsole);
export const disableBuildOnError = createAction( export const disableBuildOnError = createAction(
actionTypes.disableBuildOnError actionTypes.disableBuildOnError
); );
export const storedCodeFound = createAction(actionTypes.storedCodeFound);
export const noStoredCodeFound = createAction(actionTypes.noStoredCodeFound); export const noStoredCodeFound = createAction(actionTypes.noStoredCodeFound);
export const saveEditorContent = createAction(actionTypes.saveEditorContent); export const saveEditorContent = createAction(actionTypes.saveEditorContent);
export const setIsAdvancing = createAction(actionTypes.setIsAdvancing); export const setIsAdvancing = createAction(actionTypes.setIsAdvancing);

View File

@@ -9,7 +9,7 @@ import { FlashMessages } from '../../../components/Flash/redux/flash-messages';
import { savedChallengesSelector } from '../../../redux/selectors'; import { savedChallengesSelector } from '../../../redux/selectors';
import { actionTypes as appTypes } from '../../../redux/action-types'; import { actionTypes as appTypes } from '../../../redux/action-types';
import { actionTypes } from './action-types'; import { actionTypes } from './action-types';
import { noStoredCodeFound, storedCodeFound } from './actions'; import { noStoredCodeFound, updateFile } from './actions';
import { challengeFilesSelector, challengeMetaSelector } from './selectors'; import { challengeFilesSelector, challengeMetaSelector } from './selectors';
const legacyPrefixes = [ const legacyPrefixes = [
@@ -200,7 +200,9 @@ function loadCodeEpic(action$, state$) {
} }
} }
if (finalFiles) { if (finalFiles) {
return of(storedCodeFound(finalFiles)); // update the contents, rather than replacing the entire file, so that
// we do not lose the seed values.
return of(...finalFiles.map(file => updateFile(file)));
} }
return of(noStoredCodeFound()); return of(noStoredCodeFound());
}) })

View File

@@ -2,7 +2,6 @@ import { isEmpty } from 'lodash-es';
import { handleActions } from 'redux-actions'; import { handleActions } from 'redux-actions';
import { getLines } from '../../../../../shared/utils/get-lines'; import { getLines } from '../../../../../shared/utils/get-lines';
import { mergeChallengeFiles } from '../classic/saved-challenges';
import { getTargetEditor } from '../utils/get-target-editor'; import { getTargetEditor } from '../utils/get-target-editor';
import { actionTypes, ns } from './action-types'; import { actionTypes, ns } from './action-types';
import codeStorageEpic from './code-storage-epic'; import codeStorageEpic from './code-storage-epic';
@@ -84,23 +83,31 @@ export const reducer = handleActions(
}), }),
[actionTypes.createFiles]: (state, { payload }) => ({ [actionTypes.createFiles]: (state, { payload }) => ({
...state, ...state,
challengeFiles: payload challengeFiles: payload.map(challengeFile => ({
...challengeFile,
seed: challengeFile.contents.slice(),
editableContents: getLines(
challengeFile.contents,
challengeFile.editableRegionBoundaries
),
editableRegionBoundaries:
challengeFile.editableRegionBoundaries?.slice() ?? [],
seedEditableRegionBoundaries:
challengeFile.editableRegionBoundaries?.slice() ?? []
}))
}), }),
[actionTypes.updateFile]: ( [actionTypes.updateFile]: (
state, state,
{ payload: { fileKey, editorValue, editableRegionBoundaries } } { payload: { fileKey, contents, editableRegionBoundaries } }
) => { ) => {
const updates = {}; const updates = {};
// if a given part of the payload is null, we leave that part of the state // if a given part of the payload is null, we leave that part of the state
// unchanged // unchanged
if (editableRegionBoundaries !== null) if (editableRegionBoundaries !== null)
updates.editableRegionBoundaries = editableRegionBoundaries; updates.editableRegionBoundaries = editableRegionBoundaries;
if (editorValue !== null) updates.contents = editorValue; if (contents !== null) updates.contents = contents;
if (editableRegionBoundaries !== null && editorValue !== null) if (editableRegionBoundaries !== null && contents !== null)
updates.editableContents = getLines( updates.editableContents = getLines(contents, editableRegionBoundaries);
editorValue,
editableRegionBoundaries
);
return { return {
...state, ...state,
challengeFiles: state.challengeFiles.map(challengeFile => challengeFiles: state.challengeFiles.map(challengeFile =>
@@ -111,12 +118,6 @@ export const reducer = handleActions(
isBuildEnabled: true isBuildEnabled: true
}; };
}, },
[actionTypes.storedCodeFound]: (state, { payload }) => ({
...state,
challengeFiles: state.challengeFiles.length
? mergeChallengeFiles(state.challengeFiles, payload)
: payload
}),
[actionTypes.initTests]: (state, { payload }) => ({ [actionTypes.initTests]: (state, { payload }) => ({
...state, ...state,
challengeTests: payload challengeTests: payload

47
e2e/challenge.spec.ts Normal file
View File

@@ -0,0 +1,47 @@
import { test, expect } from '@playwright/test';
import translations from '../client/i18n/locales/english/translations.json';
import { clearEditor, focusEditor, getEditors } from './utils/editor';
test.describe('when reloading the page', () => {
test.beforeEach(async ({ page }) => {
const pageUsingEditableRegionInTests =
'/learn/2022/responsive-web-design/learn-basic-css-by-building-a-cafe-menu/step-14';
await page.goto(pageUsingEditableRegionInTests);
});
// This is quite brittle. If it breaks, try to come up with a unit test instead.
test('should keep the editable content for testing', async ({
page,
isMobile,
browserName
}) => {
await focusEditor({ page, isMobile });
await clearEditor({ page, browserName });
// For some reason, fill doesn't work properly on firefox if there are new lines
// in the text, hence one line:
const solution = `h1, h2, p { text-align: center; }`;
await getEditors(page).fill(solution);
const editorTextLocator = page
.getByTestId('editor-container-stylescss')
.getByText(solution);
await expect(editorTextLocator).toBeVisible();
// save the code
await page.keyboard.down('Control');
await page.keyboard.press('S');
await page.reload();
await expect(editorTextLocator).toBeVisible();
// check the tests pass
await page.keyboard.down('Control');
await page.keyboard.press('Enter');
await expect(
page.getByText(translations.learn.congratulations)
).toBeVisible();
});
});