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;
async function initTestFrame(e = {}) {
const code = (e.code || '').slice(0);
async function initTestFrame(e = { code: {} }) {
const code = (e.code.contents || '').slice();
// eslint-disable-next-line no-unused-vars
const editableContents = (e.code.editableContents || '').slice();
if (!e.getUserInput) {
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 */
self.onmessage = async e => {
/* 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;
// Fake Deep Equal dependency
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';
// TODO: does it?
// The z-index needs increasing as ViewZones default to below the lines.
outputNode.style.zIndex = '10';
@ -508,7 +509,19 @@ class Editor extends Component {
onChange = editorValue => {
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 => {
@ -620,6 +633,48 @@ class Editor extends Component {
).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) {
const model = this.data[key].model;
const forbiddenRanges = [
@ -674,15 +729,6 @@ class Editor extends Component {
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 this listener needs to be replaced on reset.
model.onDidChangeContent(e => {
@ -730,9 +776,9 @@ class Editor extends Component {
).collapseToStart();
// the decoration needs adjusting if the user creates a line immediately
// before the greyed out region...
const lineOneRange = translateRange(startOfZone, -2);
const lineOneRange = this.translateRange(startOfZone, -2);
// or immediately after it
const lineTwoRange = translateRange(startOfZone, -1);
const lineTwoRange = this.translateRange(startOfZone, -1);
for (const lineRange of newLineRanges) {
const shouldMoveZone = this._monaco.Range.areIntersectingOrTouching(
@ -753,7 +799,7 @@ class Editor extends Component {
).collapseToStart();
// the decoration needs adjusting if the user creates a line immediately
// before the editable region.
const lineOneRange = translateRange(endOfZone, -1);
const lineOneRange = this.translateRange(endOfZone, -1);
for (const lineRange of newLineRanges) {
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
// so this covers the *new* decoration range.
const coveringRange = toStartOfLine(model.getDecorationRange(id));
const oldStartOfRange = translateRange(
const oldStartOfRange = this.translateRange(
coveringRange.collapseToStart(),
1
);

View File

@ -323,7 +323,9 @@ export default connect(
mapDispatchToProps
)(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`
query ClassicChallenge($slug: String!) {
challengeNode(fields: { slug: { eq: $slug } }) {

View File

@ -138,7 +138,13 @@ function loadCodeEpic(action$, state$) {
...file,
contents: codeFound[file.key]
? 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,
[file.key]: {
...createPoly(file),
seed: file.contents.slice(0),
editableRegion: file.editableRegion
seed: file.contents.slice(),
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 initTests = createAction(types.initTests);
export const updateTests = createAction(types.updateTests);
@ -251,13 +264,18 @@ export const reducer = handleActions(
...state,
challengeFiles: payload
}),
[types.updateFile]: (state, { payload: { key, editorValue } }) => ({
[types.updateFile]: (
state,
{ payload: { key, editorValue, editableRegionBoundaries } }
) => ({
...state,
challengeFiles: {
...state.challengeFiles,
[key]: {
...state.challengeFiles[key],
contents: editorValue
contents: editorValue,
editableContents: getLines(editorValue, editableRegionBoundaries),
editableRegionBoundaries
}
}
}),
@ -265,7 +283,6 @@ export const reducer = handleActions(
...state,
challengeFiles: payload
}),
[types.initTests]: (state, { payload }) => ({
...state,
challengeTests: payload
@ -314,7 +331,11 @@ export const reducer = handleActions(
[file.key]: {
...file,
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
// sources.
// 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(
(sources, file) => {
sources[file.name] += file.source || file.contents;
sources.editableContents += file.editableContents;
return sources;
},
{ index: '' }
{ index: '', editableContents: '' }
);
}
@ -111,7 +113,10 @@ export function getTestRunner(buildData, { proxyLogger }, document) {
}
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 });

View File

@ -98,7 +98,10 @@ const initTestFrame = frameReady => ctx => {
const { sources, loadEnzyme } = ctx;
// default for classic challenges
// 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
const getUserInput = fileName => toString(sources[fileName]);
await ctx.document.__initTestFrame({ code, getUserInput, loadEnzyme });

View File

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