diff --git a/README.md.orig b/README.md.orig deleted file mode 100644 index 08d734aab5..0000000000 --- a/README.md.orig +++ /dev/null @@ -1,93 +0,0 @@ -# Cadet Frontend - -[![Build Status](https://travis-ci.org/source-academy/cadet-frontend.svg?branch=master)](https://travis-ci.org/source-academy/cadet-frontend) -[![Coverage Status](https://coveralls.io/repos/github/source-academy/cadet-frontend/badge.svg?branch=travis)](https://coveralls.io/github/source-academy/cadet-frontend?branch=travis) - -## Development Setup - -1. Install a stable version of NodeJS (tested: Node 10.15.0). -2. Run `npm install` to install dependencies. -3. Copy the `.env.example` file as `.env` and set the variable `REACT_APP_IVLE_KEY` - to contain your IVLE Lapi key. -4. Run `npm start` to start the server at `localhost:80`. Admin permissions may - be required for your OS to serve at port 80. -5. If running cadet without ngix, `npm run cors-proxy` to solve CORS problems. - -## IVLE LAPI Key -For NUS students, you can access your IVLE LAPI key [here](https://ivle.nus.edu.sg/LAPI/default.aspx). - -## For Windows Users - -### Running cadet-frontend -Run `npm run win-start` - -### Dealing with hooks -In package.json, change line 28:\ -"pre-push": "bash scripts/test.sh",\ -to an empty line. - -Please note that doing this will disable the test suite, so you will need to run the tests manually instead. Using Git Bash (or any other UNIX-based command line), run the following:\ -cd scripts\ -bash test.sh - -## js-slang - -Currently using a version of js-slang with native and verbose errors. - -Edit https://github.com/source-academy/cadet-frontend/blob/57ba44f6b55c214d0f20339cd45bece57f24f48c/src/sagas/index.ts#L260 - -to toggle native (default is native enabled). - -### To run local copy of js-slang - -1. Follow the instructions on the js-slang repository to transpile your own copy -2. Edit line 41 of package.json in this project to link to the directory of your js-slang and then run `npm install`: - -`"js-slang": "file:path/to/js-slang",` - -Note that this copies your files over, any future changes will not be reflected. - -You may try [this](https://medium.com/@alexishevia/the-magic-behind-npm-link-d94dcb3a81af) for a smoother experience. - -<<<<<<< HEAD -## Inspector -This requires the use of the `debugger` branch of js-slang to work. Clone both the frontend and the `debugger` slang to the same directory. You would want to `yarn build` the slang you just obtained and then `yarn && sudo yarn start` in the frontend and it should just work. The merge over there is still ongoing. Meanwhile, please try to break this. - -The mental model we are using is: A breakpoint means that the interpreter will stop right before it. Whatever is highlighted is going to be evaluated next. If you meet any inconsistencies with this, also please raise it up for discussion. - -### What you can do -- Set breakpoints by clicking on the gutter -- `debugger;` just like ECMAScript -- Inspect! -- Run stuff in the context of the paused program! - -### Usage -Here's what happens: After you click run, if there the interpreter meets a breakpoint, the first thing you're going to notice is that the REPL feedbacks to you it hit a breakpoint, the line is highlighted, and one of the icons on the right pane is going to start blinking. If you click on the icon, it reveals the inspector. All the variables in every frame is exposed here. The REPL is also now in the context of where ever you are. So you can evaluate anything you would normally be able to in the REPL. It is all quite simple really. - -### Note -Because we use a local version of `js-slang`, the CI just breaks all the time. - -## For Editing And Creating New Local XML Missions - -1. Use the branch 'mission-editing' in cadet-frontend -2. Run in browser with npm start -2. Go to Incubator tab. - -======= ->>>>>>> master -## Application Structure - -1. `actions` contains action creators, one file per reducer, combined in index. -2. `assets` contains static assets. -3. `components` contains all react components. -4. `containers` contains HOC that inject react components with Redux state. -5. `mocks` contains mock data structures for testing -6. `reducers` contains all Redux reducers and their state, combined in index. -7. `sagas` contains all Redux sagas, combined in index. -8. `slang` contains the source interpreter. -9. `styles` contains all SCSS styles. -10. `utils` contains utility modules. - -## TypeScript Coding Conventions - -We reference [this guide](https://github.com/piotrwitek/react-redux-typescript-guide). \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 0337aa122e..28352be6b0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5435,7 +5435,7 @@ }, "brace": { "version": "0.11.1", - "resolved": "https://registry.npmjs.org/brace/-/brace-0.11.1.tgz", + "resolved": "http://registry.npmjs.org/brace/-/brace-0.11.1.tgz", "integrity": "sha1-SJb8ydVE7vRfS7dmDbMg07N5/lg=" }, "brace-expansion": { @@ -8965,7 +8965,8 @@ }, "ansi-regex": { "version": "2.1.1", - "bundled": true + "bundled": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -8983,11 +8984,13 @@ }, "balanced-match": { "version": "1.0.0", - "bundled": true + "bundled": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -9000,15 +9003,18 @@ }, "code-point-at": { "version": "1.1.0", - "bundled": true + "bundled": true, + "optional": true }, "concat-map": { "version": "0.0.1", - "bundled": true + "bundled": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", - "bundled": true + "bundled": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -9111,7 +9117,8 @@ }, "inherits": { "version": "2.0.3", - "bundled": true + "bundled": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -9121,6 +9128,7 @@ "is-fullwidth-code-point": { "version": "1.0.0", "bundled": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -9133,17 +9141,20 @@ "minimatch": { "version": "3.0.4", "bundled": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } }, "minimist": { "version": "0.0.8", - "bundled": true + "bundled": true, + "optional": true }, "minipass": { "version": "2.2.4", "bundled": true, + "optional": true, "requires": { "safe-buffer": "^5.1.1", "yallist": "^3.0.0" @@ -9160,6 +9171,7 @@ "mkdirp": { "version": "0.5.1", "bundled": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -9232,7 +9244,8 @@ }, "number-is-nan": { "version": "1.0.1", - "bundled": true + "bundled": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -9242,6 +9255,7 @@ "once": { "version": "1.4.0", "bundled": true, + "optional": true, "requires": { "wrappy": "1" } @@ -9317,7 +9331,8 @@ }, "safe-buffer": { "version": "5.1.1", - "bundled": true + "bundled": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -9347,6 +9362,7 @@ "string-width": { "version": "1.0.2", "bundled": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -9364,6 +9380,7 @@ "strip-ansi": { "version": "3.0.1", "bundled": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -9402,11 +9419,13 @@ }, "wrappy": { "version": "1.0.2", - "bundled": true + "bundled": true, + "optional": true }, "yallist": { "version": "3.0.2", - "bundled": true + "bundled": true, + "optional": true } } }, diff --git a/src/actions/actionTypes.ts b/src/actions/actionTypes.ts old mode 100755 new mode 100644 index 56b2a7e062..6f5bd9b550 --- a/src/actions/actionTypes.ts +++ b/src/actions/actionTypes.ts @@ -19,6 +19,7 @@ export const BEGIN_INTERRUPT_EXECUTION = 'BEGIN_INTERRUPT_EXECUTION'; export const END_INTERRUPT_EXECUTION = 'END_INTERRUPT_EXECUTION'; export const EVAL_INTERPRETER_ERROR = 'EVAL_INTERPRETER_ERROR'; export const EVAL_INTERPRETER_SUCCESS = 'EVAL_INTERPRETER_SUCCESS'; +export const EVAL_TESTCASE_SUCCESS = 'EVAL_TESTCASE_SUCCESS'; export const HANDLE_CONSOLE_LOG = 'HANDLE_CONSOLE_LOG'; export const BEGIN_DEBUG_PAUSE = 'BEGIN_DEBUG_PAUSE'; export const END_DEBUG_PAUSE = 'END_DEBUG_PAUSE'; @@ -31,6 +32,7 @@ export const BEGIN_CLEAR_CONTEXT = 'BEGIN_CLEAR_CONTEXT'; export const BROWSE_REPL_HISTORY_DOWN = 'BROWSE_REPL_HISTORY_DOWN'; export const BROWSE_REPL_HISTORY_UP = 'BROWSE_REPL_HISTORY_UP'; export const CHANGE_ACTIVE_TAB = 'CHANGE_ACTIVE_TAB'; +export const CHANGE_EDITOR_HEIGHT = 'CHANGE_EDITOR_HEIGHT'; export const CHANGE_EDITOR_WIDTH = 'CHANGE_EDITOR_WIDTH'; export const CHANGE_PLAYGROUND_EXTERNAL = 'CHANGE_PLAYGROUND_EXTERNAL'; export const CHANGE_SIDE_CONTENT_HEIGHT = 'CHANGE_SIDE_CONTENT_HEIGHT'; @@ -41,6 +43,7 @@ export const END_CLEAR_CONTEXT = 'END_CLEAR_CONTEXT'; export const ENSURE_LIBRARIES_LOADED = 'ENSURE_LIBRARIES_LOADED'; export const EVAL_EDITOR = 'EVAL_EDITOR'; export const EVAL_REPL = 'EVAL_REPL'; +export const EVAL_TESTCASE = 'EVAL_TESTCASE'; export const INVALID_EDITOR_SESSION_ID = 'INVALID_EDITOR_SESSION_ID'; export const PLAYGROUND_EXTERNAL_SELECT = 'PLAYGROUND_EXTERNAL_SELECT '; export const RESET_WORKSPACE = 'RESET_WORKSPACE'; diff --git a/src/actions/interpreter.ts b/src/actions/interpreter.ts index 4a83afe9bd..3c7ea3c73a 100644 --- a/src/actions/interpreter.ts +++ b/src/actions/interpreter.ts @@ -13,6 +13,15 @@ export const evalInterpreterSuccess = (value: Value, workspaceLocation: Workspac payload: { type: 'result', value, workspaceLocation } }); +export const evalTestcaseSuccess = ( + value: Value, + workspaceLocation: WorkspaceLocation, + index: number +) => ({ + type: actionTypes.EVAL_TESTCASE_SUCCESS, + payload: { type: 'result', value, workspaceLocation, index } +}); + export const evalInterpreterError = ( errors: SourceError[], workspaceLocation: WorkspaceLocation diff --git a/src/actions/workspaces.ts b/src/actions/workspaces.ts index d35dc34c42..4b389fac7f 100755 --- a/src/actions/workspaces.ts +++ b/src/actions/workspaces.ts @@ -51,6 +51,14 @@ export const changePlaygroundExternal: ActionCreator = ( payload: { newExternal } }); +export const changeEditorHeight: ActionCreator = ( + height: number, + workspaceLocation: WorkspaceLocation +) => ({ + type: actionTypes.CHANGE_EDITOR_HEIGHT, + payload: { height, workspaceLocation } +}); + export const changeEditorWidth: ActionCreator = ( widthChange: string, workspaceLocation: WorkspaceLocation @@ -161,6 +169,11 @@ export const evalRepl = (workspaceLocation: WorkspaceLocation) => ({ payload: { workspaceLocation } }); +export const evalTestcase = (workspaceLocation: WorkspaceLocation, testcaseId: number) => ({ + type: actionTypes.EVAL_TESTCASE, + payload: { workspaceLocation, testcaseId } +}); + export const invalidEditorSessionId = () => ({ type: actionTypes.INVALID_EDITOR_SESSION_ID }); diff --git a/src/components/Playground.tsx b/src/components/Playground.tsx index 61f6765a24..30cd95a0fd 100755 --- a/src/components/Playground.tsx +++ b/src/components/Playground.tsx @@ -37,6 +37,7 @@ export interface IStateProps { activeTab: number; editorSessionId: string; editorValue: string; + editorHeight?: number; editorWidth: string; breakpoints: string[]; highlightedLines: number[][]; @@ -59,6 +60,7 @@ export interface IDispatchProps { handleChangeActiveTab: (activeTab: number) => void; handleChapterSelect: (chapter: number) => void; handleEditorEval: () => void; + handleEditorHeightChange: (height: number) => void; handleEditorValueChange: (val: string) => void; handleEditorWidthChange: (widthChange: number) => void; handleEditorUpdateBreakpoints: (breakpoints: string[]) => void; @@ -130,6 +132,8 @@ class Playground extends React.Component { websocketStatus: this.props.websocketStatus }, editorProps: { + editorPrepend: '', + editorPrependLines: 0, editorValue: this.props.editorValue, editorSessionId: this.props.editorSessionId, handleEditorEval: this.props.handleEditorEval, @@ -140,7 +144,9 @@ class Playground extends React.Component { handleEditorUpdateBreakpoints: this.props.handleEditorUpdateBreakpoints, handleSetWebsocketStatus: this.props.handleSetWebsocketStatus }, + editorHeight: this.props.editorHeight, editorWidth: this.props.editorWidth, + handleEditorHeightChange: this.props.handleEditorHeightChange, handleEditorWidthChange: this.props.handleEditorWidthChange, handleSideContentHeightChange: this.props.handleSideContentHeightChange, replProps: { diff --git a/src/components/__tests__/Playground.tsx b/src/components/__tests__/Playground.tsx index 5399d813b9..665802b21b 100755 --- a/src/components/__tests__/Playground.tsx +++ b/src/components/__tests__/Playground.tsx @@ -27,6 +27,7 @@ const baseProps = { handleChangeActiveTab: (n: number) => {}, handleChapterSelect: (chapter: number) => {}, handleEditorEval: () => {}, + handleEditorHeightChange: (height: number) => {}, handleEditorValueChange: () => {}, handleEditorWidthChange: (widthChange: number) => {}, handleEditorUpdateBreakpoints: (breakpoints: string[]) => {}, diff --git a/src/components/__tests__/__snapshots__/Playground.tsx.snap b/src/components/__tests__/__snapshots__/Playground.tsx.snap index 3f1b0bc8cf..29e75a499a 100644 --- a/src/components/__tests__/__snapshots__/Playground.tsx.snap +++ b/src/components/__tests__/__snapshots__/Playground.tsx.snap @@ -2,12 +2,12 @@ exports[`Playground renders correctly 1`] = ` " - + " `; exports[`Playground with link renders correctly 1`] = ` " - + " `; diff --git a/src/components/academy/grading/GradingWorkspace.tsx b/src/components/academy/grading/GradingWorkspace.tsx index 741e7d6bf0..3a93402a21 100755 --- a/src/components/academy/grading/GradingWorkspace.tsx +++ b/src/components/academy/grading/GradingWorkspace.tsx @@ -9,6 +9,7 @@ import { IMCQQuestion, IProgrammingQuestion, IQuestion, + ITestcase, Library, QuestionTypes } from '../../assessment/assessmentShape'; @@ -23,7 +24,11 @@ export type GradingWorkspaceProps = DispatchProps & OwnProps & StateProps; export type StateProps = { activeTab: number; grading?: Grading; + editorPrepend: string | null; editorValue: string | null; + editorPostpend: string | null; + editorTestcases: ITestcase[] | null; + editorHeight?: number; editorWidth: string; breakpoints: string[]; highlightedLines: number[][]; @@ -51,6 +56,7 @@ export type DispatchProps = { handleClearContext: (library: Library) => void; handleEditorEval: () => void; handleEditorValueChange: (val: string) => void; + handleEditorHeightChange: (height: number) => void; handleEditorWidthChange: (widthChange: number) => void; handleEditorUpdateBreakpoints: (breakpoints: string[]) => void; handleGradingFetch: (submissionId: number) => void; @@ -113,6 +119,11 @@ class GradingWorkspace extends React.Component { editorProps: question.type === QuestionTypes.programming ? { + editorPrepend: this.props.editorPrepend!, + editorPrependLines: + this.props.editorPrepend === null || this.props.editorPrepend.length === 0 + ? 0 + : this.props.editorPrepend.split('\n').length, editorSessionId: '', editorValue: editorValue!, handleEditorEval: this.props.handleEditorEval, @@ -123,7 +134,9 @@ class GradingWorkspace extends React.Component { isEditorAutorun: false } : undefined, + editorHeight: this.props.editorHeight, editorWidth: this.props.editorWidth, + handleEditorHeightChange: this.props.handleEditorHeightChange, handleEditorWidthChange: this.props.handleEditorWidthChange, handleSideContentHeightChange: this.props.handleSideContentHeightChange, mcqProps: { @@ -173,9 +186,32 @@ class GradingWorkspace extends React.Component { ? ((question as IProgrammingQuestion).answer as string) : (question as IProgrammingQuestion).solutionTemplate : null; + const editorPrepend = + question.type === QuestionTypes.programming + ? (question as IProgrammingQuestion).prepend !== null + ? (question as IProgrammingQuestion).prepend + : '' + : ''; + const editorPostpend = + question.type === QuestionTypes.programming + ? (question as IProgrammingQuestion).postpend !== null + ? (question as IProgrammingQuestion).postpend + : '' + : ''; + const editorTestcases = + question.type === QuestionTypes.programming + ? (question as IProgrammingQuestion).testcases !== null + ? (question as IProgrammingQuestion).testcases + : [] + : []; this.props.handleEditorUpdateBreakpoints([]); this.props.handleUpdateCurrentSubmissionId(submissionId, questionId); - this.props.handleResetWorkspace({ editorValue }); + this.props.handleResetWorkspace({ + editorPrepend, + editorValue, + editorPostpend, + editorTestcases + }); this.props.handleClearContext(question.library); this.props.handleUpdateHasUnsavedChanges(false); if (editorValue) { diff --git a/src/components/assessment/AssessmentWorkspace.tsx b/src/components/assessment/AssessmentWorkspace.tsx index 012e784453..8dd17bc127 100755 --- a/src/components/assessment/AssessmentWorkspace.tsx +++ b/src/components/assessment/AssessmentWorkspace.tsx @@ -20,12 +20,14 @@ import Markdown from '../commons/Markdown'; import Workspace, { WorkspaceProps } from '../workspace'; import { ControlBarProps } from '../workspace/ControlBar'; import { SideContentProps } from '../workspace/side-content'; +import Autograder from '../workspace/side-content/Autograder'; import ToneMatrix from '../workspace/side-content/ToneMatrix'; import { IAssessment, IMCQQuestion, IProgrammingQuestion, IQuestion, + ITestcase, Library, QuestionTypes } from './assessmentShape'; @@ -36,7 +38,11 @@ export type AssessmentWorkspaceProps = DispatchProps & OwnProps & StateProps; export type StateProps = { activeTab: number; assessment?: IAssessment; + editorPrepend: string | null; editorValue: string | null; + editorPostpend: string | null; + editorTestcases: ITestcase[] | null; + editorHeight?: number; editorWidth: string; breakpoints: string[]; highlightedLines: number[][]; @@ -67,6 +73,7 @@ export type DispatchProps = { handleClearContext: (library: Library) => void; handleEditorEval: () => void; handleEditorValueChange: (val: string) => void; + handleEditorHeightChange: (height: number) => void; handleEditorWidthChange: (widthChange: number) => void; handleEditorUpdateBreakpoints: (breakpoints: string[]) => void; handleInterruptEval: () => void; @@ -76,6 +83,7 @@ export type DispatchProps = { handleResetWorkspace: (options: Partial) => void; handleSave: (id: number, answer: number | string) => void; handleSideContentHeightChange: (heightChange: number) => void; + handleTestcaseEval: (testcaseId: number) => void; handleUpdateCurrentAssessmentId: (assessmentId: number, questionId: number) => void; handleUpdateHasUnsavedChanges: (hasUnsavedChanges: boolean) => void; handleDebuggerPause: () => void; @@ -200,6 +208,11 @@ class AssessmentWorkspace extends React.Component< question.type === QuestionTypes.programming ? { editorSessionId: '', + editorPrepend: this.props.editorPrepend!, + editorPrependLines: + this.props.editorPrepend === null || this.props.editorPrepend.length === 0 + ? 0 + : this.props.editorPrepend.split('\n').length, editorValue: this.props.editorValue!, handleEditorEval: this.props.handleEditorEval, handleEditorValueChange: this.props.handleEditorValueChange, @@ -210,7 +223,9 @@ class AssessmentWorkspace extends React.Component< isEditorAutorun: false } : undefined, + editorHeight: this.props.editorHeight, editorWidth: this.props.editorWidth, + handleEditorHeightChange: this.props.handleEditorHeightChange, handleEditorWidthChange: this.props.handleEditorWidthChange, handleSideContentHeightChange: this.props.handleSideContentHeightChange, hasUnsavedChanges: this.props.hasUnsavedChanges, @@ -264,9 +279,34 @@ class AssessmentWorkspace extends React.Component< ? ((question as IProgrammingQuestion).answer as string) : (question as IProgrammingQuestion).solutionTemplate : ''; + const editorPrepend = + question.type === QuestionTypes.programming + ? (question as IProgrammingQuestion).prepend !== null + ? (question as IProgrammingQuestion).prepend + : '' + : ''; + const editorPostpend = + question.type === QuestionTypes.programming + ? (question as IProgrammingQuestion).postpend !== null + ? (question as IProgrammingQuestion).postpend + : '' + : ''; + const editorTestcases = + question.type === QuestionTypes.programming + ? (question as IProgrammingQuestion).testcases !== null + ? (question as IProgrammingQuestion).testcases.map(testcase => { + return testcase; + }) + : [] + : []; this.props.handleEditorUpdateBreakpoints([]); this.props.handleUpdateCurrentAssessmentId(assessmentId, questionId); - this.props.handleResetWorkspace({ editorValue }); + this.props.handleResetWorkspace({ + editorPrepend, + editorValue, + editorPostpend, + editorTestcases + }); this.props.handleClearContext(question.library); this.props.handleUpdateHasUnsavedChanges(false); if (editorValue) { @@ -290,6 +330,16 @@ class AssessmentWorkspace extends React.Component< label: `${props.assessment!.category} Briefing`, icon: IconNames.BRIEFCASE, body: + }, + { + label: `${props.assessment!.category} Autograder`, + icon: IconNames.AIRPLANE, + body: ( + + ) } ]; const isGraded = props.assessment!.questions[questionId].grader !== null; diff --git a/src/components/assessment/__tests__/AssessmentWorkspace.tsx b/src/components/assessment/__tests__/AssessmentWorkspace.tsx index a028b030df..34c0804be8 100644 --- a/src/components/assessment/__tests__/AssessmentWorkspace.tsx +++ b/src/components/assessment/__tests__/AssessmentWorkspace.tsx @@ -10,7 +10,10 @@ const defaultProps: AssessmentWorkspaceProps = { assessmentId: 0, notAttempted: true, closeDate: '2048-06-18T05:24:26.026Z', + editorPrepend: '', editorValue: null, + editorPostpend: '', + editorTestcases: [], editorWidth: '50%', breakpoints: [], highlightedLines: [], @@ -23,6 +26,7 @@ const defaultProps: AssessmentWorkspaceProps = { handleClearContext: (library: Library) => {}, handleEditorEval: () => {}, handleEditorValueChange: (val: string) => {}, + handleEditorHeightChange: (height: number) => {}, handleEditorWidthChange: (widthChange: number) => {}, handleEditorUpdateBreakpoints: (breakpoints: string[]) => {}, handleInterruptEval: () => {}, @@ -32,6 +36,7 @@ const defaultProps: AssessmentWorkspaceProps = { handleResetWorkspace: () => {}, handleSave: (id: number, answer: string | number) => {}, handleSideContentHeightChange: (heightChange: number) => {}, + handleTestcaseEval: (testcaseId: number) => {}, handleUpdateHasUnsavedChanges: (hasUnsavedChanges: boolean) => {}, handleUpdateCurrentAssessmentId: (a: number, q: number) => {}, handleDebuggerPause: () => {}, diff --git a/src/components/assessment/__tests__/__snapshots__/AssessmentWorkspace.tsx.snap b/src/components/assessment/__tests__/__snapshots__/AssessmentWorkspace.tsx.snap index 8bb6a58ee0..45b0cf2d8f 100644 --- a/src/components/assessment/__tests__/__snapshots__/AssessmentWorkspace.tsx.snap +++ b/src/components/assessment/__tests__/__snapshots__/AssessmentWorkspace.tsx.snap @@ -26,7 +26,7 @@ exports[`AssessmentWorkspace page with MCQ question renders correctly 1`] = ` - + " `; @@ -54,7 +54,7 @@ exports[`AssessmentWorkspace page with overdue assessment renders correctly 1`] - + " `; @@ -82,6 +82,6 @@ exports[`AssessmentWorkspace page with programming question renders correctly 1` - + " `; diff --git a/src/components/assessment/assessmentShape.ts b/src/components/assessment/assessmentShape.ts index 928b19d976..c94c239f17 100644 --- a/src/components/assessment/assessmentShape.ts +++ b/src/components/assessment/assessmentShape.ts @@ -68,11 +68,21 @@ export type AssessmentCategory = keyof typeof AssessmentCategories; export interface IProgrammingQuestion extends IQuestion { answer: string | null; + prepend: string; solutionTemplate: string; + postpend: string; + testcases: ITestcase[]; type: 'programming'; graderTemplate?: string; } +export interface ITestcase { + answer: string; + score: number; + program: string; + actual?: any; +} + export interface IMCQQuestion extends IQuestion { solution: number | null; answer: number | null; diff --git a/src/components/missionControl/EditingWorkspace.tsx b/src/components/missionControl/EditingWorkspace.tsx index cd909da01c..26ed879013 100644 --- a/src/components/missionControl/EditingWorkspace.tsx +++ b/src/components/missionControl/EditingWorkspace.tsx @@ -38,6 +38,7 @@ export type AssessmentWorkspaceProps = DispatchProps & OwnProps & StateProps; export type StateProps = { activeTab: number; assessment?: IAssessment; + editorHeight?: number; editorValue: string | null; editorWidth: string; breakpoints: string[]; @@ -69,6 +70,7 @@ export type DispatchProps = { handleClearContext: (library: Library) => void; handleEditorEval: () => void; handleEditorValueChange: (val: string) => void; + handleEditorHeightChange: (height: number) => void; handleEditorWidthChange: (widthChange: number) => void; handleEditorUpdateBreakpoints: (breakpoints: string[]) => void; handleInterruptEval: () => void; @@ -150,6 +152,8 @@ class AssessmentWorkspace extends React.Component { id: 0, library: emptyLibrary(), graderLibrary: emptyLibrary(), + prepend: '', solutionTemplate: '//This is a mock solution template', + postpend: '', + testcases: [], type: 'programming', grader: { name: 'avenger', diff --git a/src/components/missionControl/xmlParseHelper.ts b/src/components/missionControl/xmlParseHelper.ts index 6774e5d271..d9e735c2bc 100644 --- a/src/components/missionControl/xmlParseHelper.ts +++ b/src/components/missionControl/xmlParseHelper.ts @@ -8,6 +8,7 @@ import { IMCQQuestion, IProgrammingQuestion, IQuestion, + ITestcase, Library, MCQChoice } from '../assessment/assessmentShape'; @@ -18,7 +19,8 @@ import { IXmlParseStrPProblem, IXmlParseStrProblem, IXmlParseStrProblemChoice, - IXmlParseStrTask + IXmlParseStrTask, + IXmlParseStrTestcase } from './xmlParseStrShapes'; const editingId = -1; @@ -201,7 +203,10 @@ const makeProgramming = ( ): IProgrammingQuestion => { const result: IProgrammingQuestion = { ...question, + prepend: problem.SNIPPET[0].PREPEND as string, solutionTemplate: problem.SNIPPET[0].TEMPLATE[0] as string, + postpend: problem.SNIPPET[0].POSTPEND as string, + testcases: problem.SNIPPET[0].TESTCASES.map(testcase => makeTestcase(testcase)), answer: problem.SNIPPET[0].SOLUTION[0] as string, type: 'programming' }; @@ -211,6 +216,14 @@ const makeProgramming = ( return result; }; +const makeTestcase = (testcase: IXmlParseStrTestcase): ITestcase => { + return { + answer: testcase.$.answer, + score: parseInt(testcase.$.score, 10), + program: testcase.TEXT + }; +}; + export const exportXml = () => { const assessmentStr = localStorage.getItem('MissionEditingAssessmentSA'); const overviewStr = localStorage.getItem('MissionEditingOverviewSA'); diff --git a/src/components/missionControl/xmlParseStrShapes.ts b/src/components/missionControl/xmlParseStrShapes.ts index f93cfbe595..7a3245e2cb 100644 --- a/src/components/missionControl/xmlParseStrShapes.ts +++ b/src/components/missionControl/xmlParseStrShapes.ts @@ -56,7 +56,10 @@ export interface IXmlParseStrProblem { export interface IXmlParseStrPProblem extends IXmlParseStrProblem { SNIPPET: Array<{ TEMPLATE: string[]; + PREPEND: string; SOLUTION: string[]; + POSTPEND: string; + TESTCASES: IXmlParseStrTestcase[]; GRADER: string[]; }>; TEXT: string[]; @@ -75,3 +78,11 @@ export interface IXmlParseStrProblemChoice { }; TEXT: string[]; } + +export interface IXmlParseStrTestcase { + $: { + answer: string; + score: string; + }; + TEXT: string; +} diff --git a/src/components/workspace/Editor.tsx b/src/components/workspace/Editor.tsx old mode 100644 new mode 100755 index fde1cf01d5..3720ce3931 --- a/src/components/workspace/Editor.tsx +++ b/src/components/workspace/Editor.tsx @@ -18,6 +18,8 @@ import { LINKS } from '../../utils/constants'; export interface IEditorProps { isEditorAutorun: boolean; editorSessionId: string; + editorPrepend: string; + editorPrependLines: number; editorValue: string; breakpoints: string[]; highlightedLines: number[][]; @@ -223,6 +225,7 @@ class Editor extends React.PureComponent { theme="cobalt" value={this.props.editorValue} width="100%" + setOptions={{ firstLineNumber: 1 + this.props.editorPrependLines }} /> diff --git a/src/components/workspace/EditorPrepend.tsx b/src/components/workspace/EditorPrepend.tsx new file mode 100644 index 0000000000..c43aa5c8fe --- /dev/null +++ b/src/components/workspace/EditorPrepend.tsx @@ -0,0 +1,51 @@ +import * as React from 'react'; +import AceEditor from 'react-ace'; +import { HotKeys } from 'react-hotkeys'; + +import 'brace/mode/javascript'; +import 'brace/theme/cobalt'; +import 'brace/theme/terminal'; + +export interface IEditorPrependProps { + editorPrependValue: string; +} + +class EditorPrepend extends React.PureComponent { + public AceEditor: React.RefObject; + + constructor(props: IEditorPrependProps) { + super(props); + this.AceEditor = React.createRef(); + } + + public render() { + return ( + +
+ +
+
+ ); + } +} + +/* Override handler, so does not trigger when focus is in editor */ +const handlers = { + goGreen: () => {} +}; + +export default EditorPrepend; diff --git a/src/components/workspace/__tests__/Editor.tsx b/src/components/workspace/__tests__/Editor.tsx old mode 100644 new mode 100755 index af028ccced..a9a219726f --- a/src/components/workspace/__tests__/Editor.tsx +++ b/src/components/workspace/__tests__/Editor.tsx @@ -11,6 +11,8 @@ jest.spyOn(Editor.prototype, 'componentDidMount').mockImplementation(componentDi test('Editor renders correctly', () => { const props: IEditorProps = { editorSessionId: '', + editorPrepend: '', + editorPrependLines: 0, editorValue: '', breakpoints: [], highlightedLines: [], diff --git a/src/components/workspace/__tests__/__snapshots__/Editor.tsx.snap b/src/components/workspace/__tests__/__snapshots__/Editor.tsx.snap index 69758f470a..c18a12e126 100644 --- a/src/components/workspace/__tests__/__snapshots__/Editor.tsx.snap +++ b/src/components/workspace/__tests__/__snapshots__/Editor.tsx.snap @@ -3,7 +3,7 @@ exports[`Editor renders correctly 1`] = ` "
- +
" `; diff --git a/src/components/workspace/index.tsx b/src/components/workspace/index.tsx index 1d9f94c6a2..ba1a1a0ead 100755 --- a/src/components/workspace/index.tsx +++ b/src/components/workspace/index.tsx @@ -4,6 +4,7 @@ import { Prompt } from 'react-router'; import ControlBar, { ControlBarProps } from './ControlBar'; import Editor, { IEditorProps } from './Editor'; +import EditorPrepend from './EditorPrepend'; import MCQChooser, { IMCQChooserProps } from './MCQChooser'; import Repl, { IReplProps } from './Repl'; import SideContent, { SideContentProps } from './side-content'; @@ -12,7 +13,9 @@ export type WorkspaceProps = { // Either editorProps or mcqProps must be provided controlBarProps: ControlBarProps; editorProps?: IEditorProps; + editorHeight?: string | number; editorWidth: string; + handleEditorHeightChange: (height: number) => void; handleEditorWidthChange: (widthChange: number) => void; handleSideContentHeightChange: (height: number) => void; mcqProps?: IMCQChooserProps; @@ -24,14 +27,17 @@ export type WorkspaceProps = { class Workspace extends React.Component { private editorDividerDiv: HTMLDivElement; + private editorPrependDividerDiv: HTMLDivElement; private leftParentResizable: Resizable; private maxDividerHeight: number; private sideDividerDiv: HTMLDivElement; + private editorPrependRef: React.RefObject; private editorRef: React.RefObject; public constructor(props: WorkspaceProps) { super(props); this.editorRef = React.createRef(); + this.editorPrependRef = React.createRef(); } public componentDidMount() { @@ -93,6 +99,26 @@ class Workspace extends React.Component { } as ResizableProps; } + private editorPrependResizableProps() { + const onResizeStop: ResizeCallback = ({}, {}, ref, {}) => + this.props.handleEditorHeightChange(ref.clientHeight); + return { + bounds: 'parent', + className: 'resize-editor-prepend left-parent', + enable: bottomResizeOnly, + minHeight: 0, + onResize: this.toggleeditorPrependDividerDisplay, + onResizeStop, + size: + this.props.editorHeight === undefined + ? undefined + : { + height: this.props.editorHeight, + width: '100%' + } + } as ResizableProps; + } + private sideContentResizableProps() { const onResizeStop: ResizeCallback = ({}, {}, ref, {}) => this.props.handleSideContentHeightChange(ref.clientHeight); @@ -104,6 +130,9 @@ class Workspace extends React.Component { onResize: this.toggleDividerDisplay, onResizeStop, size: + /* It will always be undefined... + Default workspace state does not have sideContentHeight... + */ this.props.sideContentHeight === undefined ? undefined : { @@ -141,6 +170,7 @@ class Workspace extends React.Component { * so that it's bottom border snaps flush with editor's bottom border */ private toggleDividerDisplay: ResizeCallback = ({}, {}, ref) => { + /* This is actually broken... */ this.maxDividerHeight = this.sideDividerDiv.clientHeight > this.maxDividerHeight ? this.sideDividerDiv.clientHeight @@ -154,6 +184,14 @@ class Workspace extends React.Component { } }; + private toggleeditorPrependDividerDisplay: ResizeCallback = ({}, {}, ref) => { + /* Guaranteed that there will be editor refs */ + // @ts-ignore + this.editorPrependRef.current!.AceEditor.current!.editor.resize(); + // @ts-ignore + this.editorRef.current!.AceEditor.current!.editor.resize(); + }; + /** * Pre-condition: `this.props.editorProps` * XOR `this.props.mcq` are defined. @@ -161,13 +199,38 @@ class Workspace extends React.Component { private createWorkspaceInput = (props: WorkspaceProps) => { if (props.editorProps) { // Set key to force remount of Editor component when session id changes - return ( - - ); + if ( + props.editorProps.editorPrepend !== null && + props.editorProps.editorPrepend.length !== 0 + ) { + return ( +
+ + +
(this.editorPrependDividerDiv = e!)} + /> + + +
+ ); + } else { + return ( + + ); + } } else { return ; } diff --git a/src/components/workspace/side-content/Autograder.tsx b/src/components/workspace/side-content/Autograder.tsx new file mode 100644 index 0000000000..4ba00aa9e6 --- /dev/null +++ b/src/components/workspace/side-content/Autograder.tsx @@ -0,0 +1,28 @@ +import * as React from 'react'; +import { ITestcase } from '../../assessment/assessmentShape'; +import AutograderCard from './AutograderCard'; + +type AutograderProps = { + testcases: ITestcase[] | null; + handleTestcaseEval: (testcaseId: number) => void; +}; + +class Autograder extends React.Component { + public render() { + return this.props.testcases != null ? ( + this.props.testcases.map((testcase, index) => ( +
+ +
+ )) + ) : ( +
There are no testcases provided for this mission.
+ ); + } +} + +export default Autograder; diff --git a/src/components/workspace/side-content/AutograderCard.tsx b/src/components/workspace/side-content/AutograderCard.tsx new file mode 100644 index 0000000000..279bd550c1 --- /dev/null +++ b/src/components/workspace/side-content/AutograderCard.tsx @@ -0,0 +1,97 @@ +import { + // Button, + // ButtonGroup, + Card, + // Classes, + // Collapse, + // Dialog, + Elevation + // Icon, + // IconName + // Intent, + // NonIdealState, + // Position, + // Spinner, + // Text, + // Tooltip +} from '@blueprintjs/core'; +import { IconNames } from '@blueprintjs/icons'; +import { stringify } from 'js-slang/dist/interop'; +import * as React from 'react'; +// tslint:disable-next-line +import { controlButton } from '../../commons'; +// tslint:disable-next-line +import { ITestcase } from '../../assessment/assessmentShape'; +// tslint:disable-next-line +import CanvasOutput from '../CanvasOutput'; +// tslint:disable-next-line +import Markdown from '../../commons/Markdown'; + +// import { InterpreterOutput } from '../../../reducers/states'; + +type AutograderCardProps = { + testcase: ITestcase; + index: number; + handleTestcaseEval: (testcaseId: number) => void; +}; + +class AutograderCard extends React.Component { + public render() { + const renderResult = (value: any) => { + /** A class which is the output of the show() function */ + const ShapeDrawn = (window as any).ShapeDrawn; + if (typeof ShapeDrawn !== 'undefined' && value instanceof ShapeDrawn) { + return ; + } else { + return stringify(value); + } + }; + + return ( +
+ +
+ {/* {makeOverviewCardTitle(overview, index, setBetchaAssessment, renderGradingStatus)} */} +
+
+ +
+
+
+
+ +
+
+
+
+ {'Actual Answer: '}{' '} + {this.props.testcase.actual !== undefined ? ( +
{renderResult(this.props.testcase.actual.value)}
+ ) : ( + 'No Answer' + )} +
+
+
+
+ {controlButton('Test', IconNames.PLAY, () => + this.props.handleTestcaseEval(this.props.index) + )} +
+
+
+
+
+ ); + } +} + +export default AutograderCard; diff --git a/src/containers/PlaygroundContainer.ts b/src/containers/PlaygroundContainer.ts old mode 100644 new mode 100755 index fc10532e69..7c62f830c2 --- a/src/containers/PlaygroundContainer.ts +++ b/src/containers/PlaygroundContainer.ts @@ -8,6 +8,7 @@ import { browseReplHistoryDown, browseReplHistoryUp, changeActiveTab, + changeEditorHeight, changeEditorWidth, changeSideContentHeight, chapterSelect, @@ -62,6 +63,7 @@ const mapDispatchToProps: MapDispatchToProps = (dispatch: Di handleChapterSelect: (chapter: number) => chapterSelect(chapter, location), handleEditorEval: () => evalEditor(location), handleEditorValueChange: (val: string) => updateEditorValue(val, location), + handleEditorHeightChange: (height: number) => changeEditorHeight(height, location), handleEditorWidthChange: (widthChange: number) => changeEditorWidth(widthChange, location), handleEditorUpdateBreakpoints: (breakpoints: string[]) => setEditorBreakpoint(breakpoints, location), diff --git a/src/containers/academy/grading/GradingWorkspaceContainer.ts b/src/containers/academy/grading/GradingWorkspaceContainer.ts index 612a858f3a..f81fbfaf5f 100644 --- a/src/containers/academy/grading/GradingWorkspaceContainer.ts +++ b/src/containers/academy/grading/GradingWorkspaceContainer.ts @@ -7,6 +7,7 @@ import { browseReplHistoryDown, browseReplHistoryUp, changeActiveTab, + changeEditorHeight, changeEditorWidth, changeSideContentHeight, chapterSelect, @@ -40,7 +41,11 @@ const workspaceLocation: WorkspaceLocation = 'grading'; const mapStateToProps: MapStateToProps = (state, props) => { return { activeTab: state.workspaces.grading.sideContentActiveTab, - editorValue: state.workspaces.grading.editorValue, + editorPrepend: state.workspaces.assessment.editorPrepend, + editorValue: state.workspaces.assessment.editorValue, + editorPostpend: state.workspaces.assessment.editorPostpend, + editorTestcases: state.workspaces.assessment.editorTestcases, + editorHeight: state.workspaces.assessment.editorHeight, editorWidth: state.workspaces.grading.editorWidth, breakpoints: state.workspaces.grading.breakpoints, highlightedLines: state.workspaces.grading.highlightedLines, @@ -68,6 +73,7 @@ const mapDispatchToProps: MapDispatchToProps = (dispatch: Dis handleClearContext: (library: Library) => beginClearContext(library, workspaceLocation), handleEditorEval: () => evalEditor(workspaceLocation), handleEditorValueChange: (val: string) => updateEditorValue(val, workspaceLocation), + handleEditorHeightChange: (height: number) => changeEditorHeight(height, workspaceLocation), handleEditorWidthChange: (widthChange: number) => changeEditorWidth(widthChange, workspaceLocation), handleEditorUpdateBreakpoints: (breakpoints: string[]) => diff --git a/src/containers/assessment/AssessmentWorkspaceContainer.ts b/src/containers/assessment/AssessmentWorkspaceContainer.ts index 24c249f6fd..17c52736b2 100644 --- a/src/containers/assessment/AssessmentWorkspaceContainer.ts +++ b/src/containers/assessment/AssessmentWorkspaceContainer.ts @@ -8,6 +8,7 @@ import { browseReplHistoryDown, browseReplHistoryUp, changeActiveTab, + changeEditorHeight, changeEditorWidth, changeSideContentHeight, chapterSelect, @@ -16,6 +17,7 @@ import { debuggerResume, evalEditor, evalRepl, + evalTestcase, fetchAssessment, setEditorBreakpoint, submitAnswer, @@ -40,7 +42,11 @@ const mapStateToProps: MapStateToProps = (state, p return { activeTab: state.workspaces.assessment.sideContentActiveTab, assessment: state.session.assessments.get(props.assessmentId), + editorPrepend: state.workspaces.assessment.editorPrepend, editorValue: state.workspaces.assessment.editorValue, + editorPostpend: state.workspaces.assessment.editorPostpend, + editorTestcases: state.workspaces.assessment.editorTestcases, + editorHeight: state.workspaces.assessment.editorHeight, editorWidth: state.workspaces.assessment.editorWidth, breakpoints: state.workspaces.assessment.breakpoints, highlightedLines: state.workspaces.assessment.highlightedLines, @@ -70,6 +76,7 @@ const mapDispatchToProps: MapDispatchToProps = (dispatch: Dis handleClearContext: (library: Library) => beginClearContext(library, workspaceLocation), handleEditorEval: () => evalEditor(workspaceLocation), handleEditorValueChange: (val: string) => updateEditorValue(val, workspaceLocation), + handleEditorHeightChange: (height: number) => changeEditorHeight(height, workspaceLocation), handleEditorWidthChange: (widthChange: number) => changeEditorWidth(widthChange, workspaceLocation), handleEditorUpdateBreakpoints: (breakpoints: string[]) => @@ -83,6 +90,7 @@ const mapDispatchToProps: MapDispatchToProps = (dispatch: Dis handleSave: submitAnswer, handleSideContentHeightChange: (heightChange: number) => changeSideContentHeight(heightChange, workspaceLocation), + handleTestcaseEval: (testcaseId: number) => evalTestcase(workspaceLocation, testcaseId), handleUpdateHasUnsavedChanges: (hasUnsavedChanges: boolean) => updateHasUnsavedChanges(workspaceLocation, hasUnsavedChanges), handleUpdateCurrentAssessmentId: updateCurrentAssessmentId, diff --git a/src/containers/missionControl/EditingWorkspaceContainer.ts b/src/containers/missionControl/EditingWorkspaceContainer.ts index f5e8a407ac..fcb1c8091f 100644 --- a/src/containers/missionControl/EditingWorkspaceContainer.ts +++ b/src/containers/missionControl/EditingWorkspaceContainer.ts @@ -7,6 +7,7 @@ import { beginInterruptExecution, browseReplHistoryDown, browseReplHistoryUp, + changeEditorHeight, changeEditorWidth, changeSideContentHeight, chapterSelect, @@ -39,6 +40,7 @@ const mapStateToProps: MapStateToProps = (state, p activeTab: state.workspaces.assessment.sideContentActiveTab, assessment: state.session.assessments.get(props.assessmentId), editorValue: state.workspaces.assessment.editorValue, + editorHeight: state.workspaces.assessment.editorHeight, editorWidth: state.workspaces.assessment.editorWidth, breakpoints: state.workspaces.assessment.breakpoints, highlightedLines: state.workspaces.assessment.highlightedLines, @@ -66,6 +68,7 @@ const mapDispatchToProps: MapDispatchToProps = (dispatch: Dis handleClearContext: (library: Library) => beginClearContext(library, workspaceLocation), handleEditorEval: () => evalEditor(workspaceLocation), handleEditorValueChange: (val: string) => updateEditorValue(val, workspaceLocation), + handleEditorHeightChange: (height: number) => changeEditorHeight(height, workspaceLocation), handleEditorWidthChange: (widthChange: number) => changeEditorWidth(widthChange, workspaceLocation), handleEditorUpdateBreakpoints: (breakpoints: string[]) => diff --git a/src/mocks/assessmentAPI.ts b/src/mocks/assessmentAPI.ts index d33da0c6a4..c7eb8e6c8a 100644 --- a/src/mocks/assessmentAPI.ts +++ b/src/mocks/assessmentAPI.ts @@ -208,6 +208,9 @@ What's your favourite dinner food? comment: null, id: 0, library: mockSoundLibrary, + prepend: '', + postpend: '', + testcases: [], solutionTemplate: '0th question mock solution template', type: 'programming', grader: { @@ -226,6 +229,9 @@ What's your favourite dinner food? content: 'Hello and welcome to this assessment! This is the 1st question.', id: 1, library: mock3DRuneLibrary, + prepend: '', + postpend: '', + testcases: [], solutionTemplate: '1st question mock solution template', type: 'programming', grader: { @@ -318,6 +324,9 @@ What's your favourite dinner food? content: 'You have reached the last question! Have some fun with the tone matrix...', id: 1, library: mockToneMatrixLibrary, + prepend: '', + postpend: '', + testcases: [], solutionTemplate: '5th question mock solution template', type: 'programming', grader: { diff --git a/src/reducers/states.ts b/src/reducers/states.ts old mode 100755 new mode 100644 index 3f760cbd26..9062e589df --- a/src/reducers/states.ts +++ b/src/reducers/states.ts @@ -8,7 +8,8 @@ import { ExternalLibraryName, ExternalLibraryNames, IAssessment, - IAssessmentOverview + IAssessmentOverview, + ITestcase } from '../components/assessment/assessmentShape'; import { HistoryHelper } from '../utils/history'; import { createContext } from '../utils/slangHelper'; @@ -59,8 +60,12 @@ export interface IWorkspaceManagerState { export interface IWorkspaceState { readonly context: Context; + readonly editorPrepend: string | null; readonly editorSessionId: string; readonly editorValue: string | null; + readonly editorPostpend: string | null; + readonly editorTestcases: ITestcase[]; + readonly editorHeight: number; readonly editorWidth: string; readonly breakpoints: string[]; readonly highlightedLines: number[][]; @@ -203,8 +208,12 @@ export const defaultEditorValue = '// Type your program in here!'; */ export const createDefaultWorkspace = (location: WorkspaceLocation): IWorkspaceState => ({ context: createContext(latestSourceChapter, [], location), + editorPrepend: '', editorSessionId: '', editorValue: location === WorkspaceLocations.playground ? defaultEditorValue : null, + editorPostpend: '', + editorTestcases: [], + editorHeight: 150, editorWidth: '50%', breakpoints: [], highlightedLines: [], diff --git a/src/reducers/workspaces.ts b/src/reducers/workspaces.ts index e8b1d71f00..e826caec16 100755 --- a/src/reducers/workspaces.ts +++ b/src/reducers/workspaces.ts @@ -4,6 +4,7 @@ import { BROWSE_REPL_HISTORY_DOWN, BROWSE_REPL_HISTORY_UP, CHANGE_ACTIVE_TAB, + CHANGE_EDITOR_HEIGHT, CHANGE_EDITOR_WIDTH, CHANGE_PLAYGROUND_EXTERNAL, CHANGE_SIDE_CONTENT_HEIGHT, @@ -18,6 +19,8 @@ import { EVAL_INTERPRETER_ERROR, EVAL_INTERPRETER_SUCCESS, EVAL_REPL, + EVAL_TESTCASE, + EVAL_TESTCASE_SUCCESS, HANDLE_CONSOLE_LOG, HIGHLIGHT_LINE, IAction, @@ -58,6 +61,7 @@ export const reducer: Reducer = ( ) => { const location: WorkspaceLocation = action.payload !== undefined ? action.payload.workspaceLocation : undefined; + const index: number = action.payload !== undefined ? action.payload.index : undefined; let newOutput: InterpreterOutput[]; let lastOutput: InterpreterOutput; @@ -151,6 +155,14 @@ export const reducer: Reducer = ( sideContentActiveTab: action.payload.activeTab } }; + case CHANGE_EDITOR_HEIGHT: + return { + ...state, + [location]: { + ...state[location], + editorHeight: action.payload.height + } + }; case CHANGE_EDITOR_WIDTH: return { ...state, @@ -283,6 +295,14 @@ export const reducer: Reducer = ( isRunning: true } }; + case EVAL_TESTCASE: + return { + ...state, + [location]: { + ...state[location], + isRunning: true + } + }; case EVAL_INTERPRETER_SUCCESS: lastOutput = state[location].output.slice(-1)[0]; if (lastOutput !== undefined && lastOutput.type === 'running') { @@ -308,6 +328,36 @@ export const reducer: Reducer = ( highlightedLines: [] } }; + case EVAL_TESTCASE_SUCCESS: + lastOutput = state[location].output.slice(-1)[0]; + if (lastOutput !== undefined && lastOutput.type === 'running') { + newOutput = state[location].output.slice(0, -1).concat({ + ...action.payload, + workspaceLocation: undefined, + consoleLogs: lastOutput.consoleLogs + }); + } else { + newOutput = state[location].output.concat({ + ...action.payload, + workspaceLocation: undefined, + consoleLogs: [] + }); + } + return { + ...state, + [location]: { + ...state[location], + editorTestcases: state[location].editorTestcases.map((testcase, i) => { + if (i === index) { + testcase.actual = newOutput[0]; + return testcase; + } else { + return testcase; + } + }), + isRunning: false + } + }; case EVAL_INTERPRETER_ERROR: lastOutput = state[location].output.slice(-1)[0]; if (lastOutput !== undefined && lastOutput.type === 'running') { diff --git a/src/sagas/index.ts b/src/sagas/index.ts index 8617eb5292..ac72f19921 100755 --- a/src/sagas/index.ts +++ b/src/sagas/index.ts @@ -30,7 +30,12 @@ function* workspaceSaga(): SagaIterator { yield takeEvery(actionTypes.EVAL_EDITOR, function*(action) { const location = (action as actionTypes.IAction).payload.workspaceLocation; const code: string = yield select( - (state: IState) => (state.workspaces[location] as IWorkspaceState).editorValue + (state: IState) => + (state.workspaces[location] as IWorkspaceState).editorPrepend! + + '\n' + + (state.workspaces[location] as IWorkspaceState).editorValue! + + '\n' + + (state.workspaces[location] as IWorkspaceState).editorPostpend! ); const chapter: number = yield select( (state: IState) => (state.workspaces[location] as IWorkspaceState).context.chapter @@ -124,6 +129,47 @@ function* workspaceSaga(): SagaIterator { yield; }); + yield takeEvery(actionTypes.EVAL_TESTCASE, function*(action) { + const location = (action as actionTypes.IAction).payload.workspaceLocation; + const index = (action as actionTypes.IAction).payload.testcaseId; + const code: string = yield select( + (state: IState) => + (state.workspaces[location] as IWorkspaceState).editorPrepend! + + '\n' + + (state.workspaces[location] as IWorkspaceState).editorValue! + + '\n' + + (state.workspaces[location] as IWorkspaceState).editorPostpend! + + '\n' + + (state.workspaces[location] as IWorkspaceState).editorTestcases[index].program! + ); + const chapter: number = yield select( + (state: IState) => (state.workspaces[location] as IWorkspaceState).context.chapter + ); + const symbols: string[] = yield select( + (state: IState) => (state.workspaces[location] as IWorkspaceState).context.externalSymbols + ); + const globals: Array<[string, any]> = yield select( + (state: IState) => (state.workspaces[location] as IWorkspaceState).globals + ); + const library = { + chapter, + external: { + name: ExternalLibraryNames.NONE, + symbols + }, + globals + }; + /** End any code that is running right now. */ + yield put(actions.beginInterruptExecution(location)); + /** Clear the context, with the same chapter and externalSymbols as before. */ + yield put(actions.beginClearContext(library, location)); + yield put(actions.clearReplOutput(location)); + context = yield select( + (state: IState) => (state.workspaces[location] as IWorkspaceState).context + ); + yield* evalTestCode(code, context, location, index); + }); + yield takeEvery(actionTypes.CHAPTER_SELECT, function*(action) { const location = (action as actionTypes.IAction).payload.workspaceLocation; const newChapter = (action as actionTypes.IAction).payload.chapter; @@ -368,4 +414,29 @@ function* evalCode( } } +function* evalTestCode(code: string, context: Context, location: WorkspaceLocation, index: number) { + const { result, interrupted } = yield race({ + result: call(runInContext, code, context, { scheduler: 'preemptive' }), + /** + * A BEGIN_INTERRUPT_EXECUTION signals the beginning of an interruption, + * i.e the trigger for the interpreter to interrupt execution. + */ + interrupted: take(actionTypes.BEGIN_INTERRUPT_EXECUTION) + }); + if (result) { + if (result.status === 'finished') { + yield put(actions.evalInterpreterSuccess(result.value, location)); + yield put(actions.evalTestcaseSuccess(result.value, location, index)); + } else { + yield put(actions.evalInterpreterError(context.errors, location)); + } + } else if (interrupted) { + interrupt(context); + /* Redundancy, added ensure that interruption results in an error. */ + context.errors.push(new InterruptedError(context.runtime.nodes[0])); + yield put(actions.endInterruptExecution(location)); + yield call(showWarningMessage, 'Execution aborted by user', 750); + } +} + export default mainSaga; diff --git a/src/styles/_workspace.scss b/src/styles/_workspace.scss index f89f84d982..6fa602170a 100755 --- a/src/styles/_workspace.scss +++ b/src/styles/_workspace.scss @@ -54,6 +54,16 @@ $code-color-error: #ff4444; padding-bottom: 0.6rem; } + .editor-content { + display: flex; + flex-direction: column; + height: 100%; + } + + .editor-content-divider { + flex: initial; + } + .editor-divider { flex: initial; } @@ -61,7 +71,7 @@ $code-color-error: #ff4444; .Editor { display: flex; flex-direction: column; - height: 100%; + height: auto; margin: 0 0.6rem 0.6rem 0.6rem; padding-bottom: 0.6rem; padding-left: 0.6rem; @@ -75,6 +85,14 @@ $code-color-error: #ff4444; } } + .editor-prepend-react-ace { + flex: 1; + + #brace-editor { + height: 100%; + } + } + .ace_gutter-cell { padding-left: 0px; } @@ -132,6 +150,11 @@ $code-color-error: #ff4444; flex-direction: column; } + .resize-editor-content { + display: flex; + flex-direction: column; + } + .side-content-header { align-items: center; display: flex; @@ -248,6 +271,11 @@ $code-color-error: #ff4444; margin: 0 0.5rem 0 0.5rem; padding: 0; } + + .editor-content { + flex: 1 1 auto; + padding: 0; + } } hr {