feat: make editable code available in tests

This commit is contained in:
Oliver Eyton-Williams
2020-07-15 11:28:20 +02:00
committed by Mrugesh Mohapatra
parent 6e091a7cdb
commit 68b223322f
9 changed files with 120 additions and 30 deletions

View File

@ -5,8 +5,10 @@ window.$ = jQuery;
document.__initTestFrame = initTestFrame; document.__initTestFrame = initTestFrame;
async function initTestFrame(e = {}) { async function initTestFrame(e = { code: {} }) {
const code = (e.code || '').slice(0); const code = (e.code.contents || '').slice();
// eslint-disable-next-line no-unused-vars
const editableContents = (e.code.editableContents || '').slice();
if (!e.getUserInput) { if (!e.getUserInput) {
e.getUserInput = () => code; e.getUserInput = () => code;
} }

View File

@ -55,7 +55,9 @@ const __utils = (() => {
/* Run the test if there is one. If not just evaluate the user code */ /* Run the test if there is one. If not just evaluate the user code */
self.onmessage = async e => { self.onmessage = async e => {
/* eslint-disable no-unused-vars */ /* eslint-disable no-unused-vars */
const { code = '' } = e.data; const code = (e.data?.code?.contents || '').slice();
const editableContents = (e.data?.code?.editableContents || '').slice();
const assert = chai.assert; const assert = chai.assert;
// Fake Deep Equal dependency // Fake Deep Equal dependency
const DeepEqual = (a, b) => JSON.stringify(a) === JSON.stringify(b); const DeepEqual = (a, b) => JSON.stringify(a) === JSON.stringify(b);

View File

@ -481,6 +481,7 @@ class Editor extends Component {
outputNode.innerHTML = 'TESTS GO HERE'; outputNode.innerHTML = 'TESTS GO HERE';
// TODO: does it?
// The z-index needs increasing as ViewZones default to below the lines. // The z-index needs increasing as ViewZones default to below the lines.
outputNode.style.zIndex = '10'; outputNode.style.zIndex = '10';
@ -508,7 +509,19 @@ class Editor extends Component {
onChange = editorValue => { onChange = editorValue => {
const { updateFile } = this.props; const { updateFile } = this.props;
updateFile({ key: this.state.fileKey, editorValue }); // TODO: use fileKey everywhere?
const { fileKey: key } = this.state;
// TODO: now that we have getCurrentEditableRegion, should the overlays
// follow that directly? We could subscribe to changes to that and redraw if
// those imply that the positions have changed (i.e. if the content height
// has changed or if content is dragged between regions)
const editableRegion = this.getCurrentEditableRegion(key);
const editableRegionBoundaries = editableRegion && [
editableRegion.startLineNumber - 1,
editableRegion.endLineNumber + 1
];
updateFile({ key, editorValue, editableRegionBoundaries });
}; };
changeTab = newFileKey => { changeTab = newFileKey => {
@ -620,6 +633,48 @@ class Editor extends Component {
).startLineNumber; ).startLineNumber;
} }
translateRange = (range, lineDelta) => {
const iRange = {
...range,
startLineNumber: range.startLineNumber + lineDelta,
endLineNumber: range.endLineNumber + lineDelta
};
return this._monaco.Range.lift(iRange);
};
getLinesBetweenRanges = (firstRange, secondRange) => {
const startRange = this.translateRange(toLastLine(firstRange), 1);
const endRange = this.translateRange(
toStartOfLine(secondRange),
-1
).collapseToStart();
return {
startLineNumber: startRange.startLineNumber,
endLineNumber: endRange.endLineNumber
};
};
getCurrentEditableRegion = key => {
const model = this.data[key].model;
// TODO: this is a little low-level, but we should bail if there is no
// editable region defined.
if (!this.data[key].startEditDecId || !this.data[key].endEditDecId)
return null;
const firstRange = model.getDecorationRange(this.data[key].startEditDecId);
const secondRange = model.getDecorationRange(this.data[key].endEditDecId);
const { startLineNumber, endLineNumber } = this.getLinesBetweenRanges(
firstRange,
secondRange
);
// getValueInRange includes column x if
// startColumnNumber <= x < endColumnNumber
// so we add 1 here
const endColumn = model.getLineLength(endLineNumber) + 1;
return new this._monaco.Range(startLineNumber, 1, endLineNumber, endColumn);
};
decorateForbiddenRanges(key, editableRegion) { decorateForbiddenRanges(key, editableRegion) {
const model = this.data[key].model; const model = this.data[key].model;
const forbiddenRanges = [ const forbiddenRanges = [
@ -674,15 +729,6 @@ class Editor extends Component {
return newLines.map(({ range }) => range); return newLines.map(({ range }) => range);
} }
const translateRange = (range, lineDelta) => {
const iRange = {
...range,
startLineNumber: range.startLineNumber + lineDelta,
endLineNumber: range.endLineNumber + lineDelta
};
return this._monaco.Range.lift(iRange);
};
// TODO refactor this mess // TODO refactor this mess
// TODO this listener needs to be replaced on reset. // TODO this listener needs to be replaced on reset.
model.onDidChangeContent(e => { model.onDidChangeContent(e => {
@ -730,9 +776,9 @@ class Editor extends Component {
).collapseToStart(); ).collapseToStart();
// the decoration needs adjusting if the user creates a line immediately // the decoration needs adjusting if the user creates a line immediately
// before the greyed out region... // before the greyed out region...
const lineOneRange = translateRange(startOfZone, -2); const lineOneRange = this.translateRange(startOfZone, -2);
// or immediately after it // or immediately after it
const lineTwoRange = translateRange(startOfZone, -1); const lineTwoRange = this.translateRange(startOfZone, -1);
for (const lineRange of newLineRanges) { for (const lineRange of newLineRanges) {
const shouldMoveZone = this._monaco.Range.areIntersectingOrTouching( const shouldMoveZone = this._monaco.Range.areIntersectingOrTouching(
@ -753,7 +799,7 @@ class Editor extends Component {
).collapseToStart(); ).collapseToStart();
// the decoration needs adjusting if the user creates a line immediately // the decoration needs adjusting if the user creates a line immediately
// before the editable region. // before the editable region.
const lineOneRange = translateRange(endOfZone, -1); const lineOneRange = this.translateRange(endOfZone, -1);
for (const lineRange of newLineRanges) { for (const lineRange of newLineRanges) {
const shouldMoveZone = this._monaco.Range.areIntersectingOrTouching( const shouldMoveZone = this._monaco.Range.areIntersectingOrTouching(
@ -776,7 +822,7 @@ class Editor extends Component {
// NOTE: any change in the decoration has already happened by this point // NOTE: any change in the decoration has already happened by this point
// so this covers the *new* decoration range. // so this covers the *new* decoration range.
const coveringRange = toStartOfLine(model.getDecorationRange(id)); const coveringRange = toStartOfLine(model.getDecorationRange(id));
const oldStartOfRange = translateRange( const oldStartOfRange = this.translateRange(
coveringRange.collapseToStart(), coveringRange.collapseToStart(),
1 1
); );

View File

@ -323,7 +323,9 @@ export default connect(
mapDispatchToProps mapDispatchToProps
)(ShowClassic); )(ShowClassic);
// TODO: handle jsx (not sure why it doesn't get an editableRegion) // TODO: handle jsx (not sure why it doesn't get an editableRegion) EDIT:
// probably because the dummy challenge didn't include it, so Gatsby couldn't
// infer it.
export const query = graphql` export const query = graphql`
query ClassicChallenge($slug: String!) { query ClassicChallenge($slug: String!) {
challengeNode(fields: { slug: { eq: $slug } }) { challengeNode(fields: { slug: { eq: $slug } }) {

View File

@ -138,7 +138,13 @@ function loadCodeEpic(action$, state$) {
...file, ...file,
contents: codeFound[file.key] contents: codeFound[file.key]
? codeFound[file.key].contents ? codeFound[file.key].contents
: file.contents : file.contents,
editableContents: codeFound[file.key]
? codeFound[file.key].editableContents
: file.editableContents,
editableRegionBoundaries: codeFound[file.key]
? codeFound[file.key].editableRegionBoundaries
: file.editableRegionBoundaries
} }
}), }),
{} {}

View File

@ -116,14 +116,27 @@ export const createFiles = createAction(types.createFiles, challengeFiles =>
...challengeFiles, ...challengeFiles,
[file.key]: { [file.key]: {
...createPoly(file), ...createPoly(file),
seed: file.contents.slice(0), seed: file.contents.slice(),
editableRegion: file.editableRegion editableContents: getLines(
file.contents,
file.editableRegionBoundaries
),
seedEditableRegionBoundaries: file.editableRegionBoundaries.slice()
} }
}), }),
{} {}
) )
); );
// TODO: secure with tests
function getLines(contents, range) {
const lines = contents.split('\n');
const editableLines = isEmpty(lines)
? []
: lines.slice(range[0], range[1] - 1);
return editableLines.join('\n');
}
export const createQuestion = createAction(types.createQuestion); export const createQuestion = createAction(types.createQuestion);
export const initTests = createAction(types.initTests); export const initTests = createAction(types.initTests);
export const updateTests = createAction(types.updateTests); export const updateTests = createAction(types.updateTests);
@ -251,13 +264,18 @@ export const reducer = handleActions(
...state, ...state,
challengeFiles: payload challengeFiles: payload
}), }),
[types.updateFile]: (state, { payload: { key, editorValue } }) => ({ [types.updateFile]: (
state,
{ payload: { key, editorValue, editableRegionBoundaries } }
) => ({
...state, ...state,
challengeFiles: { challengeFiles: {
...state.challengeFiles, ...state.challengeFiles,
[key]: { [key]: {
...state.challengeFiles[key], ...state.challengeFiles[key],
contents: editorValue contents: editorValue,
editableContents: getLines(editorValue, editableRegionBoundaries),
editableRegionBoundaries
} }
} }
}), }),
@ -265,7 +283,6 @@ export const reducer = handleActions(
...state, ...state,
challengeFiles: payload challengeFiles: payload
}), }),
[types.initTests]: (state, { payload }) => ({ [types.initTests]: (state, { payload }) => ({
...state, ...state,
challengeTests: payload challengeTests: payload
@ -314,7 +331,11 @@ export const reducer = handleActions(
[file.key]: { [file.key]: {
...file, ...file,
contents: file.seed.slice(), contents: file.seed.slice(),
editableRegion: file.editableRegion editableContents: getLines(
file.seed,
file.seedEditableRegionBoundaries
),
editableRegionBoundaries: file.seedEditableRegionBoundaries
} }
}), }),
{} {}

View File

@ -53,13 +53,15 @@ function buildSourceMap(files) {
// the same name 'index'. This made the last file the only file to appear in // the same name 'index'. This made the last file the only file to appear in
// sources. // sources.
// A better solution is to store and handle them separately. Perhaps never // A better solution is to store and handle them separately. Perhaps never
// setting the name to 'index'. // setting the name to 'index'. Use 'contents' instead?
// TODO: is file.source ever defined?
return files.reduce( return files.reduce(
(sources, file) => { (sources, file) => {
sources[file.name] += file.source || file.contents; sources[file.name] += file.source || file.contents;
sources.editableContents += file.editableContents;
return sources; return sources;
}, },
{ index: '' } { index: '', editableContents: '' }
); );
} }
@ -111,7 +113,10 @@ export function getTestRunner(buildData, { proxyLogger }, document) {
} }
function getJSTestRunner({ build, sources }, proxyLogger) { function getJSTestRunner({ build, sources }, proxyLogger) {
const code = sources && 'index' in sources ? sources['index'] : ''; const code = {
contents: sources.index,
editableContents: sources.editableContents
};
const testWorker = createWorker(testEvaluator, { terminateWorker: true }); const testWorker = createWorker(testEvaluator, { terminateWorker: true });

View File

@ -98,7 +98,10 @@ const initTestFrame = frameReady => ctx => {
const { sources, loadEnzyme } = ctx; const { sources, loadEnzyme } = ctx;
// default for classic challenges // default for classic challenges
// should not be used for modern // should not be used for modern
const code = sources && 'index' in sources ? sources['index'] : ''; const code = {
contents: sources.index,
editableContents: sources.editableContents
};
// provide the file name and get the original source // provide the file name and get the original source
const getUserInput = fileName => toString(sources[fileName]); const getUserInput = fileName => toString(sources[fileName]);
await ctx.document.__initTestFrame({ code, getUserInput, loadEnzyme }); await ctx.document.__initTestFrame({ code, getUserInput, loadEnzyme });

View File

@ -432,7 +432,10 @@ async function createTestRunner(challenge, solution, buildChallenge) {
required, required,
template template
}); });
const code = sources && 'index' in sources ? sources['index'] : ''; const code = {
contents: sources.index,
editableContents: sources.editableContents
};
const evaluator = await (buildChallenge === buildDOMChallenge const evaluator = await (buildChallenge === buildDOMChallenge
? getContextEvaluator(build, sources, code, loadEnzyme) ? getContextEvaluator(build, sources, code, loadEnzyme)