feat: make editable code available in tests
This commit is contained in:
committed by
Mrugesh Mohapatra
parent
6e091a7cdb
commit
68b223322f
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
@ -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
|
||||||
);
|
);
|
||||||
|
@ -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 } }) {
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
{}
|
{}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
{}
|
{}
|
||||||
|
@ -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 });
|
||||||
|
|
||||||
|
@ -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 });
|
||||||
|
@ -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)
|
||||||
|
Reference in New Issue
Block a user