From 6600bb43dd475b4561ebb7c03d4b96a013500b36 Mon Sep 17 00:00:00 2001 From: "DESKTOP-G08HS3B\\Lee Yi" Date: Wed, 23 Apr 2025 22:59:55 -0400 Subject: [PATCH 01/14] Remove concurrent variant --- src/commons/__tests__/Markdown.tsx | 3 -- src/commons/application/ApplicationTypes.ts | 28 +++++----- .../application/__tests__/ApplicationTypes.ts | 5 -- src/commons/mocks/ContextMocks.ts | 9 +--- src/commons/repl/Repl.tsx | 38 +++++++++----- .../WorkspaceSaga/helpers/clearContext.ts | 8 +-- .../sagas/WorkspaceSaga/helpers/evalCode.ts | 51 +++++++++---------- .../sagas/WorkspaceSaga/helpers/evalEditor.ts | 8 +-- .../WorkspaceSaga/helpers/evalTestCode.ts | 7 ++- src/commons/sagas/WorkspaceSaga/index.ts | 10 ++-- src/commons/sagas/__tests__/BackendSaga.ts | 20 ++++---- src/commons/sagas/__tests__/WorkspaceSaga.ts | 25 ++++++--- src/commons/workspace/WorkspaceReducer.ts | 18 +++---- src/features/stories/StoriesReducer.ts | 16 +++--- 14 files changed, 121 insertions(+), 125 deletions(-) diff --git a/src/commons/__tests__/Markdown.tsx b/src/commons/__tests__/Markdown.tsx index 8393893744..d99f55558b 100644 --- a/src/commons/__tests__/Markdown.tsx +++ b/src/commons/__tests__/Markdown.tsx @@ -31,9 +31,6 @@ test('Markdown page renders correct Source information', () => { const source3Default = ; expect(source3Default.props.content).toContain('Source \xa73'); - const source3Concurrent = ; - expect(source3Concurrent.props.content).toContain('Source \xa73 Concurrent'); - const source4Default = ; expect(source4Default.props.content).toContain('Source \xa74'); }); diff --git a/src/commons/application/ApplicationTypes.ts b/src/commons/application/ApplicationTypes.ts index fbc2728d20..ef69fd0269 100644 --- a/src/commons/application/ApplicationTypes.ts +++ b/src/commons/application/ApplicationTypes.ts @@ -1,27 +1,27 @@ -import { Chapter, Language, SourceError, Variant } from 'js-slang/dist/types'; +import { Chapter, Language, type SourceError, Variant } from 'js-slang/dist/types'; -import { AchievementState } from '../../features/achievement/AchievementTypes'; -import { DashboardState } from '../../features/dashboard/DashboardTypes'; -import { PlaygroundState } from '../../features/playground/PlaygroundTypes'; +import type { AchievementState } from '../../features/achievement/AchievementTypes'; +import type { DashboardState } from '../../features/dashboard/DashboardTypes'; +import type { PlaygroundState } from '../../features/playground/PlaygroundTypes'; import { PlaybackStatus, RecordingStatus } from '../../features/sourceRecorder/SourceRecorderTypes'; -import { StoriesEnvState, StoriesState } from '../../features/stories/StoriesTypes'; +import type { StoriesEnvState, StoriesState } from '../../features/stories/StoriesTypes'; import { freshSortState } from '../../pages/academy/grading/subcomponents/GradingSubmissionsTable'; import { WORKSPACE_BASE_PATHS } from '../../pages/fileSystem/createInBrowserFileSystem'; import { defaultFeatureFlags, FeatureFlagsState } from '../featureFlags'; -import { FileSystemState } from '../fileSystem/FileSystemTypes'; -import { SideContentManagerState, SideContentState } from '../sideContent/SideContentTypes'; +import type { FileSystemState } from '../fileSystem/FileSystemTypes'; +import type { SideContentManagerState, SideContentState } from '../sideContent/SideContentTypes'; import Constants from '../utils/Constants'; import { createContext } from '../utils/JsSlangHelper'; -import { +import type { DebuggerContext, WorkspaceLocation, WorkspaceManagerState, WorkspaceState } from '../workspace/WorkspaceTypes'; -import { RouterState } from './types/CommonsTypes'; +import type { RouterState } from './types/CommonsTypes'; import { ExternalLibraryName } from './types/ExternalTypes'; -import { SessionState } from './types/SessionTypes'; -import { VscodeState as VscodeState } from './types/VscodeTypes'; +import type { SessionState } from './types/SessionTypes'; +import type { VscodeState as VscodeState } from './types/VscodeTypes'; export type OverallState = { readonly router: RouterState; @@ -160,7 +160,6 @@ type LanguageFeatures = Partial<{ const variantDisplay: Map = new Map([ [Variant.TYPED, 'Typed'], [Variant.WASM, 'WebAssembly'], - [Variant.CONCURRENT, 'Concurrent'], [Variant.NATIVE, 'Native'], [Variant.EXPLICIT_CONTROL, 'Explicit-Control'] ]); @@ -266,7 +265,6 @@ const sourceSubLanguages: Array> = [ { chapter: Chapter.SOURCE_3, variant: Variant.DEFAULT }, { chapter: Chapter.SOURCE_3, variant: Variant.TYPED }, - { chapter: Chapter.SOURCE_3, variant: Variant.CONCURRENT }, { chapter: Chapter.SOURCE_3, variant: Variant.NATIVE }, { chapter: Chapter.SOURCE_4, variant: Variant.DEFAULT }, @@ -288,13 +286,13 @@ export const sourceLanguages: SALanguage[] = sourceSubLanguages.map(sublang => { (variant === Variant.DEFAULT || variant === Variant.NATIVE || variant === Variant.TYPED); // Enable CSE Machine for Source Chapter 3 and above - supportedFeatures.cseMachine = chapter >= Chapter.SOURCE_3 && variant !== Variant.CONCURRENT; + supportedFeatures.cseMachine = chapter >= Chapter.SOURCE_3; // Local imports/exports require Source 2+ as Source 1 does not have lists. supportedFeatures.multiFile = chapter >= Chapter.SOURCE_2; // Disable REPL for concurrent variants - supportedFeatures.repl = variant !== Variant.CONCURRENT; + supportedFeatures.repl = true; return { ...sublang, diff --git a/src/commons/application/__tests__/ApplicationTypes.ts b/src/commons/application/__tests__/ApplicationTypes.ts index 902332f588..05f10785ec 100644 --- a/src/commons/application/__tests__/ApplicationTypes.ts +++ b/src/commons/application/__tests__/ApplicationTypes.ts @@ -55,11 +55,6 @@ describe('available Source language configurations', () => { variant: Variant.TYPED, supports: { dataVisualizer: true, cseMachine: true } }, - { - chapter: Chapter.SOURCE_3, - variant: Variant.CONCURRENT, - supports: { dataVisualizer: true } - }, { chapter: Chapter.SOURCE_3, variant: Variant.NATIVE, diff --git a/src/commons/mocks/ContextMocks.ts b/src/commons/mocks/ContextMocks.ts index 78f6f0fbf3..75439360d8 100644 --- a/src/commons/mocks/ContextMocks.ts +++ b/src/commons/mocks/ContextMocks.ts @@ -1,9 +1,8 @@ import { parse } from 'acorn'; -import { FunctionExpression, Node } from 'estree'; +import type { Node } from 'estree'; import { ACORN_PARSE_OPTIONS } from 'js-slang/dist/constants'; import createContext, { EnvTree } from 'js-slang/dist/createContext'; -import Closure from 'js-slang/dist/interpreter/closure'; -import { Context, Environment } from 'js-slang/dist/types'; +import type { Context } from 'js-slang/dist/types'; import { TypeError } from 'js-slang/dist/utils/rttc'; export function mockContext(chapter = 1): Context { @@ -44,10 +43,6 @@ export function mockRuntimeContext(): Context { return context; } -export function mockClosure(): Closure { - return new Closure({} as FunctionExpression, {} as Environment, {} as Context); -} - export function mockTypeError(): TypeError { // Typecast to Node to fix estree-acorn compatability. return new TypeError(parse('', ACORN_PARSE_OPTIONS) as Node, '', '', ''); diff --git a/src/commons/repl/Repl.tsx b/src/commons/repl/Repl.tsx index ad9ed588b5..2a8416c6b5 100644 --- a/src/commons/repl/Repl.tsx +++ b/src/commons/repl/Repl.tsx @@ -3,12 +3,13 @@ import { Ace } from 'ace-builds'; import classNames from 'classnames'; import { parseError } from 'js-slang'; import { Chapter, Variant } from 'js-slang/dist/types'; +import { stringify } from 'js-slang/dist/utils/stringify'; import React from 'react'; -import { InterpreterOutput } from '../application/ApplicationTypes'; +import type { InterpreterOutput, ResultOutput } from '../application/ApplicationTypes'; import { ExternalLibraryName } from '../application/types/ExternalTypes'; import { ReplInput } from './ReplInput'; -import { OutputProps } from './ReplTypes'; +import type { OutputProps } from './ReplTypes'; export type ReplProps = DispatchProps & StateProps & OwnProps; @@ -60,6 +61,26 @@ const Repl: React.FC = props => { ); }; +const ResultOutputDisplay: React.FC<{ output: ResultOutput }> = ({ + output: { value, consoleLogs } +}) => { + const stringified = React.useMemo(() => stringify(value), [value]); + if (consoleLogs.length === 0) { + return ( + +
{stringified}
+
+ ); + } else { + return ( + +
{consoleLogs.join('\n')}
+
{stringified}
+
+ ); + } +}; + export const Output: React.FC = props => { switch (props.output.type) { case 'code': @@ -88,19 +109,8 @@ export const Output: React.FC = props => {
Check out the HTML Display tab!
); - } else if (props.output.consoleLogs.length === 0) { - return ( - -
{props.output.value}
-
- ); } else { - return ( - -
{props.output.consoleLogs.join('\n')}
-
{props.output.value}
-
- ); + return ; } case 'errors': if (props.output.consoleLogs.length === 0) { diff --git a/src/commons/sagas/WorkspaceSaga/helpers/clearContext.ts b/src/commons/sagas/WorkspaceSaga/helpers/clearContext.ts index a12b6f3100..cda1d8cfd4 100644 --- a/src/commons/sagas/WorkspaceSaga/helpers/clearContext.ts +++ b/src/commons/sagas/WorkspaceSaga/helpers/clearContext.ts @@ -1,13 +1,13 @@ -import { Context } from 'js-slang'; +import type { Context } from 'js-slang'; import { defineSymbol } from 'js-slang/dist/createContext'; -import { Variant } from 'js-slang/dist/types'; +import type { Variant } from 'js-slang/dist/types'; import { put, select, take } from 'redux-saga/effects'; import WorkspaceActions from 'src/commons/workspace/WorkspaceActions'; -import { OverallState } from '../../../application/ApplicationTypes'; +import type { OverallState } from '../../../application/ApplicationTypes'; import { ExternalLibraryName } from '../../../application/types/ExternalTypes'; import { actions } from '../../../utils/ActionsHelper'; -import { WorkspaceLocation } from '../../../workspace/WorkspaceTypes'; +import type { WorkspaceLocation } from '../../../workspace/WorkspaceTypes'; export function* clearContext(workspaceLocation: WorkspaceLocation, entrypointCode: string) { const [chapter, symbols, externalLibraryName, globals, variant]: [ diff --git a/src/commons/sagas/WorkspaceSaga/helpers/evalCode.ts b/src/commons/sagas/WorkspaceSaga/helpers/evalCode.ts index 28217b184e..fc6e7f7639 100644 --- a/src/commons/sagas/WorkspaceSaga/helpers/evalCode.ts +++ b/src/commons/sagas/WorkspaceSaga/helpers/evalCode.ts @@ -1,11 +1,10 @@ import { compileAndRun as compileAndRunCCode } from '@sourceacademy/c-slang/ctowasm/dist/index'; import { tokenizer } from 'acorn'; -import { IConduit } from 'conductor/dist/conduit'; +import type { IConduit } from 'conductor/dist/conduit'; import { Context, interrupt, Result, resume, runFilesInContext } from 'js-slang'; import { ACORN_PARSE_OPTIONS } from 'js-slang/dist/constants'; import { InterruptedError } from 'js-slang/dist/errors/errors'; -import { manualToggleDebugger } from 'js-slang/dist/stdlib/inspector'; -import { Chapter, ErrorSeverity, ErrorType, SourceError, Variant } from 'js-slang/dist/types'; +import { Chapter, ErrorSeverity, ErrorType, type SourceError, Variant } from 'js-slang/dist/types'; import { eventChannel, SagaIterator } from 'redux-saga'; import { call, cancel, cancelled, fork, put, race, select, take } from 'redux-saga/effects'; import * as Sourceror from 'sourceror'; @@ -15,12 +14,12 @@ import { selectFeatureSaga } from '../../../../commons/featureFlags/selectFeatur import { makeCCompilerConfig, specialCReturnObject } from '../../../../commons/utils/CToWasmHelper'; import { javaRun } from '../../../../commons/utils/JavaHelper'; import { EventType } from '../../../../features/achievement/AchievementTypes'; -import { BrowserHostPlugin } from '../../../../features/conductor/BrowserHostPlugin'; +import type { BrowserHostPlugin } from '../../../../features/conductor/BrowserHostPlugin'; import { createConductor } from '../../../../features/conductor/createConductor'; import { flagConductorEnable } from '../../../../features/conductor/flagConductorEnable'; import { flagConductorEvaluatorUrl } from '../../../../features/conductor/flagConductorEvaluatorUrl'; import StoriesActions from '../../../../features/stories/StoriesActions'; -import { isSchemeLanguage, OverallState } from '../../../application/ApplicationTypes'; +import { isSchemeLanguage, type OverallState } from '../../../application/ApplicationTypes'; import { SideContentType } from '../../../sideContent/SideContentTypes'; import { actions } from '../../../utils/ActionsHelper'; import DisplayBufferService from '../../../utils/DisplayBufferService'; @@ -29,9 +28,9 @@ import { makeExternalBuiltins as makeSourcerorExternalBuiltins } from '../../../ import WorkspaceActions from '../../../workspace/WorkspaceActions'; import { EVAL_SILENT, - PlaygroundWorkspaceState, - SicpWorkspaceState, - WorkspaceLocation + type PlaygroundWorkspaceState, + type SicpWorkspaceState, + type WorkspaceLocation } from '../../../workspace/WorkspaceTypes'; import { dumpDisplayBuffer } from './dumpDisplayBuffer'; import { updateInspector } from './updateInspector'; @@ -78,10 +77,7 @@ export function* evalCodeSaga( const stepLimit: number = isStoriesBlock ? yield select((state: OverallState) => state.stories.envs[storyEnv!].stepLimit) : yield select((state: OverallState) => state.workspaces[workspaceLocation].stepLimit); - const substActiveAndCorrectChapter = context.chapter <= 2 && substIsActive; - if (substActiveAndCorrectChapter) { - context.executionMethod = 'interpreter'; - } + const substActiveAndCorrectChapter = context.chapter <= Chapter.SOURCE_2 && substIsActive; const uploadIsActive: boolean = correctWorkspace ? yield select( @@ -294,12 +290,12 @@ export function* evalCodeSaga( entrypointFilePath, context, { - scheduler: 'preemptive', originalMaxExecTime: execTime, stepLimit: stepLimit, throwInfiniteLoops: true, useSubst: substActiveAndCorrectChapter, - envSteps: currentStep + envSteps: currentStep, + executionMethod: cseActiveAndCorrectChapter ? 'cse-machine' : 'auto' } ), @@ -324,7 +320,7 @@ export function* evalCodeSaga( } if (paused) { yield put(actions.endDebuggerPause(workspaceLocation)); - yield put(actions.updateLastDebuggerResult(manualToggleDebugger(context), workspaceLocation)); + // yield put(actions.updateLastDebuggerResult(manualToggleDebugger(context), workspaceLocation)); yield call(updateInspector, workspaceLocation); yield call(showWarningMessage, 'Execution paused', 750); return; @@ -339,11 +335,11 @@ export function* evalCodeSaga( yield call(updateInspector, workspaceLocation); } - if ( - result.status !== 'suspended' && - result.status !== 'finished' && - result.status !== 'suspended-cse-eval' - ) { + if (result.status === 'suspended-cse-eval') { + yield put(actions.endDebuggerPause(workspaceLocation)); + yield put(actions.evalInterpreterSuccess('Breakpoint hit!', workspaceLocation)); + return; + } else if (result.status !== 'finished') { yield* dumpDisplayBuffer(workspaceLocation, isStoriesBlock, storyEnv); if (!isStoriesBlock) { const specialError = checkSpecialError(context.errors); @@ -381,10 +377,6 @@ export function* evalCodeSaga( yield put(actions.addEvent(events)); return; - } else if (result.status === 'suspended' || result.status === 'suspended-cse-eval') { - yield put(actions.endDebuggerPause(workspaceLocation)); - yield put(actions.evalInterpreterSuccess('Breakpoint hit!', workspaceLocation)); - return; } yield* dumpDisplayBuffer(workspaceLocation, isStoriesBlock, storyEnv); @@ -452,9 +444,14 @@ export function* evalCodeSaga( yield put(actions.updateChangePointSteps(context.runtime.changepointSteps, workspaceLocation)); } // Stop the home icon from flashing for an error if it is doing so since the evaluation is successful - if (context.executionMethod === 'cse-machine' || context.executionMethod === 'interpreter') { - const introIcon = document.getElementById(SideContentType.introduction + '-icon'); - introIcon?.classList.remove('side-content-tab-alert-error'); + if (context.executionMethod === 'cse-machine') { + if (workspaceLocation !== 'stories') { + yield put(actions.removeSideContentAlert(SideContentType.introduction, workspaceLocation)); + } else { + yield put( + actions.removeSideContentAlert(SideContentType.introduction, `stories.${storyEnv!}`) + ); + } } } diff --git a/src/commons/sagas/WorkspaceSaga/helpers/evalEditor.ts b/src/commons/sagas/WorkspaceSaga/helpers/evalEditor.ts index 4e7cb11ca2..8cdee6649a 100644 --- a/src/commons/sagas/WorkspaceSaga/helpers/evalEditor.ts +++ b/src/commons/sagas/WorkspaceSaga/helpers/evalEditor.ts @@ -1,15 +1,15 @@ -import { FSModule } from 'browserfs/dist/node/core/FS'; +import type { FSModule } from 'browserfs/dist/node/core/FS'; import { call, put, select, StrictEffect } from 'redux-saga/effects'; import WorkspaceActions from 'src/commons/workspace/WorkspaceActions'; import { EventType } from '../../../../features/achievement/AchievementTypes'; -import { DeviceSession } from '../../../../features/remoteExecution/RemoteExecutionTypes'; +import type { DeviceSession } from '../../../../features/remoteExecution/RemoteExecutionTypes'; import { WORKSPACE_BASE_PATHS } from '../../../../pages/fileSystem/createInBrowserFileSystem'; -import { OverallState } from '../../../application/ApplicationTypes'; +import type { OverallState } from '../../../application/ApplicationTypes'; import { retrieveFilesInWorkspaceAsRecord } from '../../../fileSystem/utils'; import { actions } from '../../../utils/ActionsHelper'; import { makeElevatedContext } from '../../../utils/JsSlangHelper'; -import { EditorTabState, EVAL_SILENT, WorkspaceLocation } from '../../../workspace/WorkspaceTypes'; +import { type EditorTabState, EVAL_SILENT, type WorkspaceLocation } from '../../../workspace/WorkspaceTypes'; import { blockExtraMethods } from './blockExtraMethods'; import { clearContext } from './clearContext'; import { evalCodeSaga } from './evalCode'; diff --git a/src/commons/sagas/WorkspaceSaga/helpers/evalTestCode.ts b/src/commons/sagas/WorkspaceSaga/helpers/evalTestCode.ts index e7c0cb43fa..146fbd1f07 100644 --- a/src/commons/sagas/WorkspaceSaga/helpers/evalTestCode.ts +++ b/src/commons/sagas/WorkspaceSaga/helpers/evalTestCode.ts @@ -1,12 +1,12 @@ -import { Context, interrupt, runInContext } from 'js-slang'; +import { type Context, interrupt, runInContext } from 'js-slang'; import { InterruptedError } from 'js-slang/dist/errors/errors'; import { call, put, race, take } from 'redux-saga/effects'; import InterpreterActions from 'src/commons/application/actions/InterpreterActions'; -import { TestcaseType, TestcaseTypes } from '../../../assessment/AssessmentTypes'; +import { type TestcaseType, TestcaseTypes } from '../../../assessment/AssessmentTypes'; import { actions } from '../../../utils/ActionsHelper'; import { showWarningMessage } from '../../../utils/notifications/NotificationsHelper'; -import { WorkspaceLocation } from '../../../workspace/WorkspaceTypes'; +import type { WorkspaceLocation } from '../../../workspace/WorkspaceTypes'; import { dumpDisplayBuffer } from './dumpDisplayBuffer'; export function* evalTestCode( @@ -20,7 +20,6 @@ export function* evalTestCode( yield put(actions.resetTestcase(workspaceLocation, index)); const { result, interrupted } = yield race({ result: call(runInContext, code, context, { - scheduler: 'preemptive', originalMaxExecTime: execTime, throwInfiniteLoops: true }), diff --git a/src/commons/sagas/WorkspaceSaga/index.ts b/src/commons/sagas/WorkspaceSaga/index.ts index 4ecba4d6f8..4334e5181b 100644 --- a/src/commons/sagas/WorkspaceSaga/index.ts +++ b/src/commons/sagas/WorkspaceSaga/index.ts @@ -1,5 +1,5 @@ -import { FSModule } from 'browserfs/dist/node/core/FS'; -import { Context, findDeclaration, getNames } from 'js-slang'; +import type { FSModule } from 'browserfs/dist/node/core/FS'; +import { type Context, findDeclaration, getNames } from 'js-slang'; import { Chapter, Variant } from 'js-slang/dist/types'; import Phaser from 'phaser'; import { call, put, select } from 'redux-saga/effects'; @@ -15,11 +15,11 @@ import DataVisualizer from '../../../features/dataVisualizer/dataVisualizer'; import { WORKSPACE_BASE_PATHS } from '../../../pages/fileSystem/createInBrowserFileSystem'; import { defaultEditorValue, - OverallState, + type OverallState, styliseSublanguage } from '../../application/ApplicationTypes'; import { externalLibraries, ExternalLibraryName } from '../../application/types/ExternalTypes'; -import { Library, Testcase } from '../../assessment/AssessmentTypes'; +import type { Library, Testcase } from '../../assessment/AssessmentTypes'; import { Documentation } from '../../documentation/Documentation'; import { writeFileRecursively } from '../../fileSystem/utils'; import { actions } from '../../utils/ActionsHelper'; @@ -34,7 +34,7 @@ import { showWarningMessage } from '../../utils/notifications/NotificationsHelper'; import { showFullJSDisclaimer, showFullTSDisclaimer } from '../../utils/WarningDialogHelper'; -import { EditorTabState } from '../../workspace/WorkspaceTypes'; +import type { EditorTabState } from '../../workspace/WorkspaceTypes'; import { evalCodeSaga } from './helpers/evalCode'; import { evalEditorSaga } from './helpers/evalEditor'; import { runTestCase } from './helpers/runTestCase'; diff --git a/src/commons/sagas/__tests__/BackendSaga.ts b/src/commons/sagas/__tests__/BackendSaga.ts index e707c49261..a92f0fde07 100644 --- a/src/commons/sagas/__tests__/BackendSaga.ts +++ b/src/commons/sagas/__tests__/BackendSaga.ts @@ -9,13 +9,13 @@ import { UsernameRoleGroup } from 'src/pages/academy/adminPanel/subcomponents/Ad import DashboardActions from '../../../features/dashboard/DashboardActions'; import SessionActions from '../../application/actions/SessionActions'; import { - GameState, + type GameState, Role, - SALanguage, - Story, + type SALanguage, + type Story, SupportedLanguage } from '../../application/ApplicationTypes'; -import { +import type { AdminPanelCourseRegistration, CourseConfiguration, CourseRegistration, @@ -23,10 +23,10 @@ import { User } from '../../application/types/SessionTypes'; import { - Assessment, - AssessmentConfiguration, + type Assessment, + type AssessmentConfiguration, AssessmentStatuses, - Question + type Question } from '../../assessment/AssessmentTypes'; import { mockAssessmentOverviews, @@ -43,7 +43,7 @@ import { showWarningMessage } from '../../utils/notifications/NotificationsHelper'; import WorkspaceActions from '../../workspace/WorkspaceActions'; -import { WorkspaceLocation } from '../../workspace/WorkspaceTypes'; +import type { WorkspaceLocation } from '../../workspace/WorkspaceTypes'; import BackendSaga from '../BackendSaga'; import { getAssessment, @@ -837,7 +837,7 @@ describe('Test CHANGE_SUBLANGUAGE action', () => { test('when chapter is changed', () => { const sublang: SALanguage = { chapter: Chapter.SOURCE_4, - variant: Variant.CONCURRENT, + variant: Variant.NATIVE, displayName: 'Source \xa74 Concurrent', mainLanguage: SupportedLanguage.JAVASCRIPT, supports: {} @@ -859,7 +859,7 @@ describe('Test CHANGE_SUBLANGUAGE action', () => { [ call(putCourseConfig, mockTokens, { sourceChapter: Chapter.SOURCE_4, - sourceVariant: Variant.CONCURRENT + sourceVariant: Variant.NATIVE }), { ok: true } ] diff --git a/src/commons/sagas/__tests__/WorkspaceSaga.ts b/src/commons/sagas/__tests__/WorkspaceSaga.ts index bd6f4f1159..f424297792 100644 --- a/src/commons/sagas/__tests__/WorkspaceSaga.ts +++ b/src/commons/sagas/__tests__/WorkspaceSaga.ts @@ -1,6 +1,13 @@ -import { Context, IOptions, Result, resume, runFilesInContext, runInContext } from 'js-slang'; +import { + type Context, + type IOptions, + type Result, + resume, + runFilesInContext, + runInContext +} from 'js-slang'; import createContext from 'js-slang/dist/createContext'; -import { Chapter, ErrorType, Finished, SourceError, Variant } from 'js-slang/dist/types'; +import { Chapter, ErrorType, type Finished, type SourceError, Variant } from 'js-slang/dist/types'; import { call } from 'redux-saga/effects'; import { expectSaga } from 'redux-saga-test-plan'; import * as matchers from 'redux-saga-test-plan/matchers'; @@ -11,10 +18,15 @@ import { defaultState, fullJSLanguage, fullTSLanguage, - OverallState + type OverallState } from '../../application/ApplicationTypes'; import { externalLibraries, ExternalLibraryName } from '../../application/types/ExternalTypes'; -import { Library, Testcase, TestcaseType, TestcaseTypes } from '../../assessment/AssessmentTypes'; +import { + type Library, + type Testcase, + type TestcaseType, + TestcaseTypes +} from '../../assessment/AssessmentTypes'; import { mockRuntimeContext } from '../../mocks/ContextMocks'; import { mockTestcases } from '../../mocks/GradingMocks'; import { @@ -22,7 +34,7 @@ import { showWarningMessage } from '../../utils/notifications/NotificationsHelper'; import WorkspaceActions from '../../workspace/WorkspaceActions'; -import { WorkspaceLocation, WorkspaceState } from '../../workspace/WorkspaceTypes'; +import type { WorkspaceLocation, WorkspaceState } from '../../workspace/WorkspaceTypes'; import workspaceSaga from '../WorkspaceSaga'; import { evalCodeSaga } from '../WorkspaceSaga/helpers/evalCode'; import { evalEditorSaga } from '../WorkspaceSaga/helpers/evalEditor'; @@ -797,7 +809,6 @@ describe('evalCode', () => { context = createContext(); // mockRuntimeContext(); value = 'test value'; options = { - scheduler: 'preemptive', originalMaxExecTime: 1000, stepLimit: 1000, useSubst: false, @@ -896,7 +907,6 @@ describe('evalCode', () => { }); runFilesInContext(files, codeFilePath, context, { - scheduler: 'preemptive', originalMaxExecTime: 1000, useSubst: false }).then(result => (context = (result as Finished).context)); @@ -1104,7 +1114,6 @@ describe('evalTestCode', () => { context = mockRuntimeContext(); value = 'another test value'; options = { - scheduler: 'preemptive', originalMaxExecTime: 1000, throwInfiniteLoops: true }; diff --git a/src/commons/workspace/WorkspaceReducer.ts b/src/commons/workspace/WorkspaceReducer.ts index a4cc98a8c3..16c5cf316b 100644 --- a/src/commons/workspace/WorkspaceReducer.ts +++ b/src/commons/workspace/WorkspaceReducer.ts @@ -1,5 +1,4 @@ -import { createReducer, Reducer } from '@reduxjs/toolkit'; -import { stringify } from 'js-slang/dist/utils/stringify'; +import { createReducer, type Reducer } from '@reduxjs/toolkit'; import { SourcecastReducer } from '../../features/sourceRecorder/sourcecast/SourcecastReducer'; import { SourcereelReducer } from '../../features/sourceRecorder/sourcereel/SourcereelReducer'; @@ -8,24 +7,24 @@ import InterpreterActions from '../application/actions/InterpreterActions'; import { createDefaultWorkspace, defaultWorkspaceManager, - ErrorOutput, - InterpreterOutput, - NotificationOutput, - ResultOutput + type ErrorOutput, + type InterpreterOutput, + type NotificationOutput, + type ResultOutput } from '../application/ApplicationTypes'; import { setEditorSessionId, setSessionDetails, setSharedbConnected } from '../collabEditing/CollabEditingActions'; -import { SourceActionType } from '../utils/ActionsHelper'; +import type { SourceActionType } from '../utils/ActionsHelper'; import { createContext } from '../utils/JsSlangHelper'; import { handleCseAndStepperActions } from './reducers/cseReducer'; import { handleDebuggerActions } from './reducers/debuggerReducer'; import { handleEditorActions } from './reducers/editorReducer'; import { handleReplActions } from './reducers/replReducer'; import WorkspaceActions from './WorkspaceActions'; -import { WorkspaceLocation, WorkspaceManagerState } from './WorkspaceTypes'; +import type { WorkspaceLocation, WorkspaceManagerState } from './WorkspaceTypes'; export const getWorkspaceLocation = (action: any): WorkspaceLocation => { return action.payload ? action.payload.workspaceLocation : 'assessment'; @@ -159,11 +158,10 @@ const newWorkspaceReducer = createReducer(defaultWorkspaceManager, builder => { }) .addCase(InterpreterActions.evalInterpreterSuccess, (state, action) => { const workspaceLocation = getWorkspaceLocation(action); - const execType = state[workspaceLocation].context.executionMethod; const tokens = state[workspaceLocation].tokenCount; const newOutputEntry: Partial = { type: action.payload.type as 'result' | undefined, - value: execType === 'interpreter' ? action.payload.value : stringify(action.payload.value) + value: action.payload.value }; const lastOutput: InterpreterOutput = state[workspaceLocation].output.slice(-1)[0]; diff --git a/src/features/stories/StoriesReducer.ts b/src/features/stories/StoriesReducer.ts index 56a4cf09a8..3e99dcd5cd 100644 --- a/src/features/stories/StoriesReducer.ts +++ b/src/features/stories/StoriesReducer.ts @@ -1,18 +1,17 @@ -import { createReducer, Reducer } from '@reduxjs/toolkit'; -import { stringify } from 'js-slang/dist/utils/stringify'; +import { createReducer, type Reducer } from '@reduxjs/toolkit'; import { logOut } from 'src/commons/application/actions/CommonsActions'; import { createDefaultStoriesEnv, defaultStories, - ErrorOutput, - InterpreterOutput, - ResultOutput + type ErrorOutput, + type InterpreterOutput, + type ResultOutput } from '../../commons/application/ApplicationTypes'; -import { SourceActionType } from '../../commons/utils/ActionsHelper'; +import type { SourceActionType } from '../../commons/utils/ActionsHelper'; import StoriesActions from './StoriesActions'; import { DEFAULT_ENV } from './storiesComponents/UserBlogContent'; -import { StoriesState } from './StoriesTypes'; +import type { StoriesState } from './StoriesTypes'; export const StoriesReducer: Reducer = ( state = defaultStories, @@ -73,10 +72,9 @@ const newStoriesReducer = createReducer(defaultStories, builder => { }) .addCase(StoriesActions.evalStorySuccess, (state, action) => { const env = getStoriesEnv(action); - const execType = state.envs[env].context.executionMethod; const newOutputEntry: Partial = { type: action.payload.type as 'result' | undefined, - value: execType === 'interpreter' ? action.payload.value : stringify(action.payload.value) + value: action.payload.value }; const lastOutput: InterpreterOutput = state.envs[env].output.slice(-1)[0]; From f9c323919f9468aee6c1d9a1bcc481d3dfe2c706 Mon Sep 17 00:00:00 2001 From: "DESKTOP-G08HS3B\\Lee Yi" Date: Thu, 24 Apr 2025 02:33:57 -0400 Subject: [PATCH 02/14] Fix broken tests --- .../__snapshots__/ApplicationTypes.ts.snap | 13 ----- .../__tests__/__snapshots__/Repl.tsx.snap | 51 +++++++++---------- .../sagas/WorkspaceSaga/helpers/evalEditor.ts | 6 ++- src/commons/sagas/__tests__/WorkspaceSaga.ts | 20 +++----- 4 files changed, 36 insertions(+), 54 deletions(-) diff --git a/src/commons/application/__tests__/__snapshots__/ApplicationTypes.ts.snap b/src/commons/application/__tests__/__snapshots__/ApplicationTypes.ts.snap index 17a957a1a4..be4eec8c40 100644 --- a/src/commons/application/__tests__/__snapshots__/ApplicationTypes.ts.snap +++ b/src/commons/application/__tests__/__snapshots__/ApplicationTypes.ts.snap @@ -188,19 +188,6 @@ Array [ }, "variant": "typed", }, - Object { - "chapter": 3, - "displayName": "Source §3 Concurrent", - "mainLanguage": "JavaScript", - "supports": Object { - "cseMachine": false, - "dataVisualizer": true, - "multiFile": true, - "repl": false, - "substVisualizer": false, - }, - "variant": "concurrent", - }, Object { "chapter": 3, "displayName": "Source §3 Native", diff --git a/src/commons/repl/__tests__/__snapshots__/Repl.tsx.snap b/src/commons/repl/__tests__/__snapshots__/Repl.tsx.snap index 3d26fde8e7..f96a3a494e 100644 --- a/src/commons/repl/__tests__/__snapshots__/Repl.tsx.snap +++ b/src/commons/repl/__tests__/__snapshots__/Repl.tsx.snap @@ -204,37 +204,32 @@ exports[`Repl renders correctly 1`] = ` `; exports[`Result output (no consoleLogs) renders correctly 1`] = ` - - - 42 - - + `; exports[`Result output (with consoleLogs) renders correctly 1`] = ` - - - a -bb -cccccccccccccccccccccccccccccccc -d - - - 42 - - + `; exports[`Running output renders correctly 1`] = ` diff --git a/src/commons/sagas/WorkspaceSaga/helpers/evalEditor.ts b/src/commons/sagas/WorkspaceSaga/helpers/evalEditor.ts index 8cdee6649a..540052f845 100644 --- a/src/commons/sagas/WorkspaceSaga/helpers/evalEditor.ts +++ b/src/commons/sagas/WorkspaceSaga/helpers/evalEditor.ts @@ -9,7 +9,11 @@ import type { OverallState } from '../../../application/ApplicationTypes'; import { retrieveFilesInWorkspaceAsRecord } from '../../../fileSystem/utils'; import { actions } from '../../../utils/ActionsHelper'; import { makeElevatedContext } from '../../../utils/JsSlangHelper'; -import { type EditorTabState, EVAL_SILENT, type WorkspaceLocation } from '../../../workspace/WorkspaceTypes'; +import { + type EditorTabState, + EVAL_SILENT, + type WorkspaceLocation +} from '../../../workspace/WorkspaceTypes'; import { blockExtraMethods } from './blockExtraMethods'; import { clearContext } from './clearContext'; import { evalCodeSaga } from './evalCode'; diff --git a/src/commons/sagas/__tests__/WorkspaceSaga.ts b/src/commons/sagas/__tests__/WorkspaceSaga.ts index f424297792..486bb48924 100644 --- a/src/commons/sagas/__tests__/WorkspaceSaga.ts +++ b/src/commons/sagas/__tests__/WorkspaceSaga.ts @@ -160,7 +160,6 @@ describe('EVAL_EDITOR', () => { { '/prepend.js': programPrependValue }, '/prepend.js', { - scheduler: 'preemptive', originalMaxExecTime: execTime, stepLimit: 1000, useSubst: false, @@ -180,7 +179,6 @@ describe('EVAL_EDITOR', () => { '/playground/program.js', context, { - scheduler: 'preemptive', originalMaxExecTime: execTime, stepLimit: 1000, useSubst: false, @@ -248,7 +246,6 @@ describe('EVAL_REPL', () => { .put(WorkspaceActions.sendReplInputToOutput(replValue, workspaceLocation)) // also calls evalCode here .call(runFilesInContext, { '/code.js': replValue }, '/code.js', context, { - scheduler: 'preemptive', originalMaxExecTime: 1000, stepLimit: 1000, useSubst: false, @@ -431,7 +428,7 @@ describe('EVAL_TESTCASE', () => { args: [ { '/prepend.js': programPrependValue }, '/prepend.js', - { scheduler: 'preemptive', originalMaxExecTime: execTime } + { originalMaxExecTime: execTime } ] }) // running the prepend block should return 'boink', but silent run -> not written to REPL @@ -445,7 +442,7 @@ describe('EVAL_TESTCASE', () => { { '/value.js': editorValue }, '/value.js', context, - { scheduler: 'preemptive', originalMaxExecTime: execTime } + { originalMaxExecTime: execTime } ] }) // running the student's program should return 69, which is NOT written to REPL (silent) @@ -458,7 +455,7 @@ describe('EVAL_TESTCASE', () => { args: [ { '/postpend.js': programPostpendValue }, '/postpend.js', - { scheduler: 'preemptive', originalMaxExecTime: execTime } + { originalMaxExecTime: execTime } ] }) // running the postpend block should return true, but silent run -> not written to REPL @@ -838,7 +835,6 @@ describe('evalCode', () => { ] ]) .call(runFilesInContext, files, codeFilePath, context, { - scheduler: 'preemptive', originalMaxExecTime: execTime, stepLimit: 1000, useSubst: false, @@ -861,10 +857,12 @@ describe('evalCode', () => { ) .withState(state) .provide([ - [call(runFilesInContext, files, codeFilePath, context, options), { status: 'suspended' }] + [ + call(runFilesInContext, files, codeFilePath, context, options), + { status: 'suspended-cse-eval' } + ] ]) .call(runFilesInContext, files, codeFilePath, context, { - scheduler: 'preemptive', originalMaxExecTime: execTime, stepLimit: 1000, useSubst: false, @@ -888,7 +886,6 @@ describe('evalCode', () => { ) .withState(state) .call(runFilesInContext, files, codeFilePath, context, { - scheduler: 'preemptive', originalMaxExecTime: execTime, stepLimit: 1000, useSubst: false, @@ -922,7 +919,6 @@ describe('evalCode', () => { ) .withState(state) .call(runFilesInContext, files, codeFilePath, context, { - scheduler: 'preemptive', originalMaxExecTime: execTime, stepLimit: 1000, useSubst: false, @@ -982,7 +978,7 @@ describe('evalCode', () => { actionType ) .withState(state) - .provide([[call(resume, lastDebuggerResult), { status: 'suspended' }]]) + .provide([[call(resume, lastDebuggerResult), { status: 'suspended-cse-eval' }]]) .call(resume, lastDebuggerResult) .put(InterpreterActions.endDebuggerPause(workspaceLocation)) .put(InterpreterActions.evalInterpreterSuccess('Breakpoint hit!', workspaceLocation)) From acabb9d2ba50d3e28ff6133a0323365567439596 Mon Sep 17 00:00:00 2001 From: Lee Yi Date: Thu, 24 Apr 2025 15:25:40 -0400 Subject: [PATCH 03/14] Begin fixing --- .../workspace/__tests__/WorkspaceReducer.ts | 255 ++++++------------ 1 file changed, 81 insertions(+), 174 deletions(-) diff --git a/src/commons/workspace/__tests__/WorkspaceReducer.ts b/src/commons/workspace/__tests__/WorkspaceReducer.ts index 9bfe49b50f..3a4b838f60 100644 --- a/src/commons/workspace/__tests__/WorkspaceReducer.ts +++ b/src/commons/workspace/__tests__/WorkspaceReducer.ts @@ -8,20 +8,20 @@ import { } from 'src/commons/collabEditing/CollabEditingActions'; import { - CodeOutput, + type CodeOutput, createDefaultWorkspace, defaultWorkspaceManager, - InterpreterOutput, - RunningOutput + type InterpreterOutput, + type RunningOutput } from '../../application/ApplicationTypes'; import { ExternalLibraryName } from '../../application/types/ExternalTypes'; -import { Library, Testcase, TestcaseTypes } from '../../assessment/AssessmentTypes'; -import { HighlightedLines, Position } from '../../editor/EditorTypes'; +import { type Library, type Testcase, TestcaseTypes } from '../../assessment/AssessmentTypes'; +import type { HighlightedLines, Position } from '../../editor/EditorTypes'; import Constants from '../../utils/Constants'; import { createContext } from '../../utils/JsSlangHelper'; import WorkspaceActions from '../WorkspaceActions'; import { WorkspaceReducer } from '../WorkspaceReducer'; -import { +import type { EditorTabState, PlaygroundWorkspaceState, WorkspaceLocation, @@ -39,8 +39,8 @@ const locations: ReadonlyArray = [ 'sicp' ] as const; -function generateActions(type: string, payload: any = {}): any[] { - return locations.map(l => ({ type, payload: { ...payload, workspaceLocation: l } })); +function generateActions(func: (loc: WorkspaceLocation) => T) { + return locations.map(func); } // cloneDeep not required for proper redux @@ -91,7 +91,7 @@ describe('BROWSE_REPL_HISTORY_DOWN', () => { }; const replDownDefaultState: WorkspaceManagerState = generateDefaultWorkspace({ replHistory }); - const actions = generateActions(WorkspaceActions.browseReplHistoryDown.type, { replHistory }); + const actions = generateActions(WorkspaceActions.browseReplHistoryDown); actions.forEach(action => { let result = WorkspaceReducer(replDownDefaultState, action); @@ -135,7 +135,7 @@ describe('BROWSE_REPL_HISTORY_DOWN', () => { }; const replDownDefaultState: WorkspaceManagerState = generateDefaultWorkspace({ replHistory }); - const actions = generateActions(WorkspaceActions.browseReplHistoryDown.type, { replHistory }); + const actions = generateActions(WorkspaceActions.browseReplHistoryDown); actions.forEach(action => { const result = WorkspaceReducer(replDownDefaultState, action); @@ -158,7 +158,7 @@ describe('BROWSE_REPL_HISTORY_UP', () => { replHistory, replValue }); - const actions = generateActions(WorkspaceActions.browseReplHistoryUp.type, { replHistory }); + const actions = generateActions(WorkspaceActions.browseReplHistoryUp); actions.forEach(action => { let result = WorkspaceReducer(replUpDefaultState, action); @@ -235,7 +235,7 @@ describe('CLEAR_REPL_INPUT', () => { test('clears replValue', () => { const replValue = 'test repl value'; const clearReplDefaultState: WorkspaceManagerState = generateDefaultWorkspace({ replValue }); - const actions = generateActions(WorkspaceActions.clearReplInput.type); + const actions = generateActions(WorkspaceActions.clearReplInput); actions.forEach(action => { const result = WorkspaceReducer(clearReplDefaultState, action); @@ -255,7 +255,7 @@ describe('CLEAR_REPL_OUTPUT', () => { test('clears output', () => { const output: InterpreterOutput[] = [{ type: 'code', value: 'test repl input' }]; const clearReplDefaultState: WorkspaceManagerState = generateDefaultWorkspace({ output }); - const actions = generateActions(WorkspaceActions.clearReplOutput.type); + const actions = generateActions(WorkspaceActions.clearReplOutput); actions.forEach(action => { const result = WorkspaceReducer(clearReplDefaultState, action); @@ -286,7 +286,7 @@ describe('CLEAR_REPL_OUTPUT_LAST', () => { } ]; const clearReplLastPriorState: WorkspaceManagerState = generateDefaultWorkspace({ output }); - const actions = generateActions(WorkspaceActions.clearReplOutputLast.type); + const actions = generateActions(WorkspaceActions.clearReplOutputLast); actions.forEach(action => { const result = WorkspaceReducer(clearReplLastPriorState, action); @@ -310,7 +310,7 @@ describe('DEBUG_RESET', () => { isRunning, isDebugging }); - const actions = generateActions(InterpreterActions.debuggerReset.type); + const actions = generateActions(InterpreterActions.debuggerReset); actions.forEach(action => { const result = WorkspaceReducer(debugResetDefaultState, action); @@ -333,7 +333,7 @@ describe('DEBUG_RESUME', () => { const debugResumeDefaultState: WorkspaceManagerState = generateDefaultWorkspace({ isDebugging }); - const actions = generateActions(InterpreterActions.debuggerResume.type); + const actions = generateActions(InterpreterActions.debuggerResume); actions.forEach(action => { const result = WorkspaceReducer(debugResumeDefaultState, action); @@ -371,7 +371,7 @@ describe('END_CLEAR_CONTEXT', () => { globals: mockGlobals }; - const actions = generateActions(WorkspaceActions.endClearContext.type, { library }); + const actions = generateActions(l => WorkspaceActions.endClearContext(library, l)); actions.forEach(action => { const result = WorkspaceReducer(defaultWorkspaceManager, action); @@ -404,7 +404,7 @@ describe('END_DEBUG_PAUSE', () => { test('sets isRunning to false and isDebugging to true', () => { const isRunning = true; const debugPauseDefaultState: WorkspaceManagerState = generateDefaultWorkspace({ isRunning }); - const actions = generateActions(InterpreterActions.endDebuggerPause.type); + const actions = generateActions(InterpreterActions.endDebuggerPause); actions.forEach(action => { const result = WorkspaceReducer(debugPauseDefaultState, action); @@ -429,7 +429,7 @@ describe('END_INTERRUPT_EXECUTION', () => { isRunning, isDebugging }); - const actions = generateActions(InterpreterActions.endInterruptExecution.type); + const actions = generateActions(InterpreterActions.endInterruptExecution); actions.forEach(action => { const result = WorkspaceReducer(interruptExecutionDefaultState, action); @@ -452,7 +452,7 @@ describe('EVAL_EDITOR', () => { const evalEditorDefaultState: WorkspaceManagerState = generateDefaultWorkspace({ isDebugging }); - const actions = generateActions(WorkspaceActions.evalEditor.type); + const actions = generateActions(WorkspaceActions.evalEditor); actions.forEach(action => { const result = WorkspaceReducer(evalEditorDefaultState, action); @@ -490,7 +490,7 @@ describe('EVAL_INTERPRETER_ERROR', () => { isRunning, isDebugging }); - const actions = generateActions(InterpreterActions.evalInterpreterError.type); + const actions = generateActions(l => InterpreterActions.evalInterpreterError.type); actions.forEach(action => { const result = WorkspaceReducer(evalEditorDefaultState, action); @@ -550,7 +550,7 @@ describe('EVAL_INTERPRETER_SUCCESS', () => { editorTabs: [{ highlightedLines, breakpoints }] }); - const actions = generateActions(InterpreterActions.evalInterpreterSuccess.type); + const actions = generateActions(l => InterpreterActions.evalInterpreterSuccess(undefined, l)); actions.forEach(action => { const result = WorkspaceReducer(evalEditorDefaultState, action); @@ -580,7 +580,7 @@ describe('EVAL_INTERPRETER_SUCCESS', () => { editorTabs: [{ highlightedLines, breakpoints }] }); - const actions = generateActions(InterpreterActions.evalInterpreterSuccess.type); + const actions = generateActions(l => InterpreterActions.evalInterpreterSuccess(undefined, l)); actions.forEach(action => { const result = WorkspaceReducer(evalEditorDefaultState, action); @@ -593,7 +593,7 @@ describe('EVAL_INTERPRETER_SUCCESS', () => { output: [ { ...outputWithRunningAndCodeOutput[0] }, { ...outputWithRunningAndCodeOutput[1] }, - { consoleLogs: [], value: 'undefined' } + { consoleLogs: [], value: undefined } ] } }); @@ -603,7 +603,7 @@ describe('EVAL_INTERPRETER_SUCCESS', () => { describe('EVAL_REPL', () => { test('sets isRunning to true', () => { - const actions = generateActions(WorkspaceActions.evalRepl.type); + const actions = generateActions(WorkspaceActions.evalRepl); actions.forEach(action => { const result = WorkspaceReducer(defaultWorkspaceManager, action); @@ -651,10 +651,7 @@ describe('EVAL_TESTCASE_FAILURE', () => { const evalFailureDefaultState: WorkspaceManagerState = generateDefaultWorkspace({ editorTestcases }); - const actions = generateActions(InterpreterActions.evalTestcaseFailure.type, { - value, - index: 1 - }); + const actions = generateActions(l => InterpreterActions.evalTestcaseFailure(value, l, 1)) actions.forEach(action => { const result = WorkspaceReducer(evalFailureDefaultState, action); @@ -683,10 +680,7 @@ describe('EVAL_TESTCASE_SUCCESS', () => { editorTestcases }); - const actions = generateActions(InterpreterActions.evalTestcaseSuccess.type, { - value, - index: 1 - }); + const actions = generateActions(l => InterpreterActions.evalTestcaseSuccess(value, l, 1)) actions.forEach(action => { const result = WorkspaceReducer(testcaseSuccessDefaultState, action); @@ -715,10 +709,7 @@ describe('EVAL_TESTCASE_SUCCESS', () => { editorTestcases }); - const actions = generateActions(InterpreterActions.evalTestcaseSuccess.type, { - value, - index: 0 - }); + const actions = generateActions(l => InterpreterActions.evalTestcaseSuccess(value, l, 0)) actions.forEach(action => { const result = WorkspaceReducer(testcaseSuccessDefaultState, action); @@ -743,9 +734,8 @@ describe('HANDLE_CONSOLE_LOG', () => { test('works correctly with RunningOutput', () => { const logString = 'test-log-string'; const consoleLogDefaultState = generateDefaultWorkspace({ output: outputWithRunningOutput }); - const actions = generateActions(InterpreterActions.handleConsoleLog.type, { - logString: [logString] - }); + const actions = generateActions(l => InterpreterActions.handleConsoleLog(l, logString)) + actions.forEach(action => { const result = WorkspaceReducer(cloneDeep(consoleLogDefaultState), action); const location: WorkspaceLocation = action.payload.workspaceLocation; @@ -771,9 +761,7 @@ describe('HANDLE_CONSOLE_LOG', () => { const consoleLogDefaultState = generateDefaultWorkspace({ output: outputWithRunningAndCodeOutput }); - const actions = generateActions(InterpreterActions.handleConsoleLog.type, { - logString: [logString] - }); + const actions = generateActions(l => InterpreterActions.handleConsoleLog(l, logString)) actions.forEach(action => { const result = WorkspaceReducer(consoleLogDefaultState, action); @@ -795,9 +783,7 @@ describe('HANDLE_CONSOLE_LOG', () => { const logString = 'test-log-string-3'; const consoleLogDefaultState = generateDefaultWorkspace({ output: [] }); - const actions = generateActions(InterpreterActions.handleConsoleLog.type, { - logString: [logString] - }); + const actions = generateActions(l => InterpreterActions.handleConsoleLog(l, logString)) actions.forEach(action => { const result = WorkspaceReducer(consoleLogDefaultState, action); @@ -864,10 +850,7 @@ describe('RESET_TESTCASE', () => { editorTestcases }); - const actions = generateActions(WorkspaceActions.resetTestcase.type, { - index: 1 - }); - + const actions = generateActions(l => WorkspaceActions.resetTestcase(l, 1)) actions.forEach(action => { const result = WorkspaceReducer(resetTestcaseDefaultState, action); const location: WorkspaceLocation = action.payload.workspaceLocation; @@ -899,7 +882,7 @@ describe('RESET_WORKSPACE', () => { replValue: 'test repl value' }; - const actions = generateActions(WorkspaceActions.resetWorkspace.type, { workspaceOptions }); + const actions = generateActions(l => WorkspaceActions.resetWorkspace(l, workspaceOptions)); actions.forEach(action => { const result = WorkspaceReducer(resetWorkspaceDefaultState, action); @@ -934,10 +917,7 @@ describe('SEND_REPL_INPUT_TO_OUTPUT', () => { }); const newOutput = 'new-output-test'; - const actions = generateActions(WorkspaceActions.sendReplInputToOutput.type, { - type: 'code', - value: newOutput - }); + const actions = generateActions(l => WorkspaceActions.sendReplInputToOutput(newOutput, l)) const newArray = [newOutput].concat(replHistory.records); newArray.pop(); @@ -975,10 +955,7 @@ describe('SEND_REPL_INPUT_TO_OUTPUT', () => { }); const newOutput = ''; - const actions = generateActions(WorkspaceActions.sendReplInputToOutput.type, { - type: 'code', - value: newOutput - }); + const actions = generateActions(l => WorkspaceActions.sendReplInputToOutput(newOutput, l)) actions.forEach(action => { const result = WorkspaceReducer(inputToOutputDefaultState, action); @@ -998,7 +975,7 @@ describe('SEND_REPL_INPUT_TO_OUTPUT', () => { describe('SET_EDITOR_SESSION_ID', () => { test('sets editorSessionId correctly', () => { const editorSessionId = 'test_editor_session_id'; - const actions = generateActions(setEditorSessionId.type, { editorSessionId }); + const actions = generateActions(l => setEditorSessionId(l, editorSessionId)); actions.forEach(action => { const result = WorkspaceReducer(defaultWorkspaceManager, action); @@ -1017,7 +994,7 @@ describe('SET_EDITOR_SESSION_ID', () => { describe('SET_SHAREDB_CONNECTED', () => { test('sets sharedbConnected correctly', () => { const connected = true; - const actions = generateActions(setSharedbConnected.type, { connected }); + const actions = generateActions(l => setSharedbConnected(l, connected)); actions.forEach(action => { const result = WorkspaceReducer(defaultWorkspaceManager, action); @@ -1035,7 +1012,7 @@ describe('SET_SHAREDB_CONNECTED', () => { describe('TOGGLE_EDITOR_AUTORUN', () => { test('toggles isEditorAutorun correctly', () => { - const actions = generateActions(WorkspaceActions.toggleEditorAutorun.type); + const actions = generateActions(WorkspaceActions.toggleEditorAutorun) actions.forEach(action => { let result = WorkspaceReducer(defaultWorkspaceManager, action); @@ -1105,7 +1082,7 @@ describe('UPDATE_CURRENT_SUBMISSION_ID', () => { describe('SET_FOLDER_MODE', () => { test('sets isFolderModeEnabled correctly', () => { const isFolderModeEnabled = true; - const actions = generateActions(WorkspaceActions.setFolderMode.type, { isFolderModeEnabled }); + const actions = generateActions(l => WorkspaceActions.setFolderMode(l, isFolderModeEnabled)); actions.forEach(action => { const result = WorkspaceReducer(defaultWorkspaceManager, action); @@ -1142,9 +1119,7 @@ describe('UPDATE_ACTIVE_EDITOR_TAB_INDEX', () => { editorTabs }); - const actions = generateActions(WorkspaceActions.updateActiveEditorTabIndex.type, { - activeEditorTabIndex - }); + const actions = generateActions(l => WorkspaceActions.updateActiveEditorTabIndex(l, activeEditorTabIndex)) actions.forEach(action => { const resultThunk = () => WorkspaceReducer(defaultWorkspaceState, action); @@ -1159,9 +1134,7 @@ describe('UPDATE_ACTIVE_EDITOR_TAB_INDEX', () => { editorTabs }); - const actions = generateActions(WorkspaceActions.updateActiveEditorTabIndex.type, { - activeEditorTabIndex - }); + const actions = generateActions(l => WorkspaceActions.updateActiveEditorTabIndex(l, activeEditorTabIndex)) actions.forEach(action => { const result = WorkspaceReducer(defaultWorkspaceState, action); @@ -1188,9 +1161,7 @@ describe('UPDATE_ACTIVE_EDITOR_TAB_INDEX', () => { editorTabs }); - const actions = generateActions(WorkspaceActions.updateActiveEditorTabIndex.type, { - activeEditorTabIndex - }); + const actions = generateActions(l => WorkspaceActions.updateActiveEditorTabIndex(l, activeEditorTabIndex)) actions.forEach(action => { const resultThunk = () => WorkspaceReducer(defaultWorkspaceState, action); @@ -1205,6 +1176,14 @@ describe('UPDATE_ACTIVE_EDITOR_TAB', () => { }; test('overrides the active editor tab correctly', () => { + // function testAction( + // func: (l: WorkspaceLocation) => T, + // payload?: any + // ) { + // const defaultWorkspaceState: WorkspaceManagerState = generateDefaultWorkspace(payload) + // const actions = generateActions(func) + // } + const defaultWorkspaceState: WorkspaceManagerState = generateDefaultWorkspace({ activeEditorTabIndex: 1, editorTabs: [ @@ -1221,9 +1200,7 @@ describe('UPDATE_ACTIVE_EDITOR_TAB', () => { ] }); - const actions = generateActions(WorkspaceActions.updateActiveEditorTab.type, { - activeEditorTabOptions - }); + const actions = generateActions(l => WorkspaceActions.updateActiveEditorTab(l, activeEditorTabOptions)) actions.forEach(action => { const result = WorkspaceReducer(defaultWorkspaceState, action); @@ -1263,9 +1240,7 @@ describe('UPDATE_ACTIVE_EDITOR_TAB', () => { editorTabs: [] }); - const actions = generateActions(WorkspaceActions.updateActiveEditorTab.type, { - activeEditorTabOptions - }); + const actions = generateActions(l => WorkspaceActions.updateActiveEditorTab(l, activeEditorTabOptions)) actions.forEach(action => { const result = WorkspaceReducer(defaultWorkspaceState, action); @@ -1295,10 +1270,7 @@ describe('UPDATE_EDITOR_VALUE', () => { editorTabs }); - const actions = generateActions(WorkspaceActions.updateEditorValue.type, { - editorTabIndex, - newEditorValue - }); + const actions = generateActions(l => WorkspaceActions.updateEditorValue(l, editorTabIndex, newEditorValue)) actions.forEach(action => { const resultThunk = () => WorkspaceReducer(defaultWorkspaceState, action); @@ -1313,10 +1285,7 @@ describe('UPDATE_EDITOR_VALUE', () => { editorTabs }); - const actions = generateActions(WorkspaceActions.updateEditorValue.type, { - editorTabIndex, - newEditorValue - }); + const actions = generateActions(l => WorkspaceActions.updateEditorValue(l, editorTabIndex, newEditorValue)) actions.forEach(action => { const resultThunk = () => WorkspaceReducer(defaultWorkspaceState, action); @@ -1331,10 +1300,7 @@ describe('UPDATE_EDITOR_VALUE', () => { editorTabs }); - const actions = generateActions(WorkspaceActions.updateEditorValue.type, { - editorTabIndex, - newEditorValue - }); + const actions = generateActions(l => WorkspaceActions.updateEditorValue(l, editorTabIndex, newEditorValue)) actions.forEach(action => { const result = WorkspaceReducer(defaultWorkspaceState, action); @@ -1362,7 +1328,7 @@ describe('UPDATE_EDITOR_BREAKPOINTS', () => { breakpoints: [] }; const editorTabs: EditorTabState[] = [zerothEditorTab, firstEditorTab]; - const newBreakpoints = [null, null, 'ace_breakpoint', null, 'ace_breakpoint']; + const newBreakpoints = [null, null, 'ace_breakpoint', null, 'ace_breakpoint'] as string[] test('throws an error if the editor tab index is negative', () => { const editorTabIndex = -1; @@ -1371,10 +1337,7 @@ describe('UPDATE_EDITOR_BREAKPOINTS', () => { editorTabs }); - const actions = generateActions(WorkspaceActions.setEditorBreakpoint.type, { - editorTabIndex, - newBreakpoints - }); + const actions = generateActions(l => WorkspaceActions.setEditorBreakpoint(l, editorTabIndex, newBreakpoints)) actions.forEach(action => { const resultThunk = () => WorkspaceReducer(defaultWorkspaceState, action); @@ -1389,10 +1352,7 @@ describe('UPDATE_EDITOR_BREAKPOINTS', () => { editorTabs }); - const actions = generateActions(WorkspaceActions.setEditorBreakpoint.type, { - editorTabIndex, - newBreakpoints - }); + const actions = generateActions(l => WorkspaceActions.setEditorBreakpoint(l, editorTabIndex, newBreakpoints)) actions.forEach(action => { const resultThunk = () => WorkspaceReducer(defaultWorkspaceState, action); @@ -1407,10 +1367,7 @@ describe('UPDATE_EDITOR_BREAKPOINTS', () => { editorTabs }); - const actions = generateActions(WorkspaceActions.setEditorBreakpoint.type, { - editorTabIndex, - newBreakpoints - }); + const actions = generateActions(l => WorkspaceActions.setEditorBreakpoint(l, editorTabIndex, newBreakpoints)) actions.forEach(action => { const result = WorkspaceReducer(defaultWorkspaceState, action); @@ -1450,10 +1407,7 @@ describe('UPDATE_EDITOR_HIGHLIGHTED_LINES', () => { editorTabs }); - const actions = generateActions(WorkspaceActions.setEditorHighlightedLines.type, { - editorTabIndex, - newHighlightedLines - }); + const actions = generateActions(l => WorkspaceActions.setEditorHighlightedLines(l, editorTabIndex, newHighlightedLines)) actions.forEach(action => { const resultThunk = () => WorkspaceReducer(defaultWorkspaceState, action); @@ -1468,10 +1422,7 @@ describe('UPDATE_EDITOR_HIGHLIGHTED_LINES', () => { editorTabs }); - const actions = generateActions(WorkspaceActions.setEditorHighlightedLines.type, { - editorTabIndex, - newHighlightedLines - }); + const actions = generateActions(l => WorkspaceActions.setEditorHighlightedLines(l, editorTabIndex, newHighlightedLines)) actions.forEach(action => { const resultThunk = () => WorkspaceReducer(defaultWorkspaceState, action); @@ -1486,10 +1437,7 @@ describe('UPDATE_EDITOR_HIGHLIGHTED_LINES', () => { editorTabs }); - const actions = generateActions(WorkspaceActions.setEditorHighlightedLines.type, { - editorTabIndex, - newHighlightedLines - }); + const actions = generateActions(l => WorkspaceActions.setEditorHighlightedLines(l, editorTabIndex, newHighlightedLines)) actions.forEach(action => { const result = WorkspaceReducer(defaultWorkspaceState, action); @@ -1532,10 +1480,7 @@ describe('MOVE_CURSOR', () => { editorTabs }); - const actions = generateActions(WorkspaceActions.moveCursor.type, { - editorTabIndex, - newCursorPosition - }); + const actions = generateActions(l => WorkspaceActions.moveCursor(l, editorTabIndex, newCursorPosition)) actions.forEach(action => { const resultThunk = () => WorkspaceReducer(defaultWorkspaceState, action); @@ -1550,11 +1495,7 @@ describe('MOVE_CURSOR', () => { editorTabs }); - const actions = generateActions(WorkspaceActions.moveCursor.type, { - editorTabIndex, - newCursorPosition - }); - + const actions = generateActions(l => WorkspaceActions.moveCursor(l, editorTabIndex, newCursorPosition)) actions.forEach(action => { const resultThunk = () => WorkspaceReducer(defaultWorkspaceState, action); expect(resultThunk).toThrow('Editor tab index must have a corresponding editor tab!'); @@ -1568,11 +1509,7 @@ describe('MOVE_CURSOR', () => { editorTabs }); - const actions = generateActions(WorkspaceActions.moveCursor.type, { - editorTabIndex, - newCursorPosition - }); - + const actions = generateActions(l => WorkspaceActions.moveCursor(l, editorTabIndex, newCursorPosition)) actions.forEach(action => { const result = WorkspaceReducer(defaultWorkspaceState, action); const location: WorkspaceLocation = action.payload.workspaceLocation; @@ -1610,7 +1547,7 @@ describe('ADD_EDITOR_TAB', () => { editorTabs }); - const actions = generateActions(WorkspaceActions.addEditorTab.type, { filePath, editorValue }); + const actions = generateActions(l => WorkspaceActions.addEditorTab(l, filePath, editorValue)); actions.forEach(action => { const result = WorkspaceReducer(defaultWorkspaceState, action); @@ -1642,7 +1579,7 @@ describe('ADD_EDITOR_TAB', () => { editorTabs }); - const actions = generateActions(WorkspaceActions.addEditorTab.type, { filePath, editorValue }); + const actions = generateActions(l => WorkspaceActions.addEditorTab(l, filePath, editorValue)); actions.forEach(action => { const result = WorkspaceReducer(defaultWorkspaceState, action); @@ -1698,11 +1635,7 @@ describe('SHIFT_EDITOR_TAB', () => { editorTabs }); - const actions = generateActions(WorkspaceActions.shiftEditorTab.type, { - previousEditorTabIndex, - newEditorTabIndex - }); - + const actions = generateActions(l => WorkspaceActions.shiftEditorTab(l, previousEditorTabIndex, newEditorTabIndex)) actions.forEach(action => { const resultThunk = () => WorkspaceReducer(defaultWorkspaceState, action); expect(resultThunk).toThrow('Previous editor tab index must be non-negative!'); @@ -1717,11 +1650,7 @@ describe('SHIFT_EDITOR_TAB', () => { editorTabs }); - const actions = generateActions(WorkspaceActions.shiftEditorTab.type, { - previousEditorTabIndex, - newEditorTabIndex - }); - + const actions = generateActions(l => WorkspaceActions.shiftEditorTab(l, previousEditorTabIndex, newEditorTabIndex)) actions.forEach(action => { const resultThunk = () => WorkspaceReducer(defaultWorkspaceState, action); expect(resultThunk).toThrow( @@ -1738,11 +1667,7 @@ describe('SHIFT_EDITOR_TAB', () => { editorTabs }); - const actions = generateActions(WorkspaceActions.shiftEditorTab.type, { - previousEditorTabIndex, - newEditorTabIndex - }); - + const actions = generateActions(l => WorkspaceActions.shiftEditorTab(l, previousEditorTabIndex, newEditorTabIndex)) actions.forEach(action => { const resultThunk = () => WorkspaceReducer(defaultWorkspaceState, action); expect(resultThunk).toThrow('New editor tab index must be non-negative!'); @@ -1757,11 +1682,7 @@ describe('SHIFT_EDITOR_TAB', () => { editorTabs }); - const actions = generateActions(WorkspaceActions.shiftEditorTab.type, { - previousEditorTabIndex, - newEditorTabIndex - }); - + const actions = generateActions(l => WorkspaceActions.shiftEditorTab(l, previousEditorTabIndex, newEditorTabIndex)) actions.forEach(action => { const resultThunk = () => WorkspaceReducer(defaultWorkspaceState, action); expect(resultThunk).toThrow('New editor tab index must have a corresponding editor tab!'); @@ -1776,11 +1697,7 @@ describe('SHIFT_EDITOR_TAB', () => { editorTabs }); - const actions = generateActions(WorkspaceActions.shiftEditorTab.type, { - previousEditorTabIndex, - newEditorTabIndex - }); - + const actions = generateActions(l => WorkspaceActions.shiftEditorTab(l, previousEditorTabIndex, newEditorTabIndex)) actions.forEach(action => { const result = WorkspaceReducer(defaultWorkspaceState, action); const location: WorkspaceLocation = action.payload.workspaceLocation; @@ -1806,10 +1723,7 @@ describe('SHIFT_EDITOR_TAB', () => { editorTabs }); - const actions = generateActions(WorkspaceActions.shiftEditorTab.type, { - previousEditorTabIndex, - newEditorTabIndex - }); + const actions = generateActions(l => WorkspaceActions.shiftEditorTab(l, previousEditorTabIndex, newEditorTabIndex)) actions.forEach(action => { const result = WorkspaceReducer(defaultWorkspaceState, action); @@ -1850,8 +1764,7 @@ describe('REMOVE_EDITOR_TAB', () => { editorTabs }); - const actions = generateActions(WorkspaceActions.removeEditorTab.type, { editorTabIndex }); - + const actions = generateActions(l => WorkspaceActions.removeEditorTab(l, editorTabIndex)); actions.forEach(action => { const resultThunk = () => WorkspaceReducer(defaultWorkspaceState, action); expect(resultThunk).toThrow('Editor tab index must be non-negative!'); @@ -1865,8 +1778,7 @@ describe('REMOVE_EDITOR_TAB', () => { editorTabs }); - const actions = generateActions(WorkspaceActions.removeEditorTab.type, { editorTabIndex }); - + const actions = generateActions(l => WorkspaceActions.removeEditorTab(l, editorTabIndex)); actions.forEach(action => { const resultThunk = () => WorkspaceReducer(defaultWorkspaceState, action); expect(resultThunk).toThrow('Editor tab index must have a corresponding editor tab!'); @@ -1880,8 +1792,7 @@ describe('REMOVE_EDITOR_TAB', () => { editorTabs: [zerothEditorTab] }); - const actions = generateActions(WorkspaceActions.removeEditorTab.type, { editorTabIndex }); - + const actions = generateActions(l => WorkspaceActions.removeEditorTab(l, editorTabIndex)); actions.forEach(action => { const result = WorkspaceReducer(defaultWorkspaceState, action); const location: WorkspaceLocation = action.payload.workspaceLocation; @@ -1907,8 +1818,7 @@ describe('REMOVE_EDITOR_TAB', () => { editorTabs }); - const actions = generateActions(WorkspaceActions.removeEditorTab.type, { editorTabIndex }); - + const actions = generateActions(l => WorkspaceActions.removeEditorTab(l, editorTabIndex)); actions.forEach(action => { const result = WorkspaceReducer(defaultWorkspaceState, action); const location: WorkspaceLocation = action.payload.workspaceLocation; @@ -1934,8 +1844,7 @@ describe('REMOVE_EDITOR_TAB', () => { editorTabs }); - const actions = generateActions(WorkspaceActions.removeEditorTab.type, { editorTabIndex }); - + const actions = generateActions(l => WorkspaceActions.removeEditorTab(l, editorTabIndex)); actions.forEach(action => { const result = WorkspaceReducer(defaultWorkspaceState, action); const location: WorkspaceLocation = action.payload.workspaceLocation; @@ -1961,8 +1870,7 @@ describe('REMOVE_EDITOR_TAB', () => { editorTabs }); - const actions = generateActions(WorkspaceActions.removeEditorTab.type, { editorTabIndex }); - + const actions = generateActions(l => WorkspaceActions.removeEditorTab(l, editorTabIndex)); actions.forEach(action => { const result = WorkspaceReducer(defaultWorkspaceState, action); const location: WorkspaceLocation = action.payload.workspaceLocation; @@ -1988,8 +1896,7 @@ describe('REMOVE_EDITOR_TAB', () => { editorTabs }); - const actions = generateActions(WorkspaceActions.removeEditorTab.type, { editorTabIndex }); - + const actions = generateActions(l => WorkspaceActions.removeEditorTab(l, editorTabIndex)); actions.forEach(action => { const result = WorkspaceReducer(defaultWorkspaceState, action); const location: WorkspaceLocation = action.payload.workspaceLocation; From f35c03d555a104f2b3b522d681fdd5fc5cce5edd Mon Sep 17 00:00:00 2001 From: "DESKTOP-G08HS3B\\Lee Yi" Date: Thu, 24 Apr 2025 16:06:40 -0400 Subject: [PATCH 04/14] Fix all broken workspace reducer tests --- .../workspace/__tests__/WorkspaceReducer.ts | 269 ++++++++++-------- 1 file changed, 155 insertions(+), 114 deletions(-) diff --git a/src/commons/workspace/__tests__/WorkspaceReducer.ts b/src/commons/workspace/__tests__/WorkspaceReducer.ts index 3a4b838f60..b688749fd4 100644 --- a/src/commons/workspace/__tests__/WorkspaceReducer.ts +++ b/src/commons/workspace/__tests__/WorkspaceReducer.ts @@ -12,6 +12,7 @@ import { createDefaultWorkspace, defaultWorkspaceManager, type InterpreterOutput, + type ResultOutput, type RunningOutput } from '../../application/ApplicationTypes'; import { ExternalLibraryName } from '../../application/types/ExternalTypes'; @@ -25,6 +26,7 @@ import type { EditorTabState, PlaygroundWorkspaceState, WorkspaceLocation, + WorkspaceLocationsWithTools, WorkspaceManagerState } from '../WorkspaceTypes'; @@ -490,7 +492,7 @@ describe('EVAL_INTERPRETER_ERROR', () => { isRunning, isDebugging }); - const actions = generateActions(l => InterpreterActions.evalInterpreterError.type); + const actions = generateActions(l => InterpreterActions.evalInterpreterError([], l)); actions.forEach(action => { const result = WorkspaceReducer(evalEditorDefaultState, action); @@ -501,7 +503,10 @@ describe('EVAL_INTERPRETER_ERROR', () => { ...evalEditorDefaultState[location], isRunning: false, isDebugging: false, - output: [{ ...outputWithRunningOutput[0] }, { consoleLogs: ['console-log-test-2'] }] + output: [ + { ...outputWithRunningOutput[0] }, + { type: 'errors', errors: [], consoleLogs: ['console-log-test-2'] } + ] } }); }); @@ -516,7 +521,7 @@ describe('EVAL_INTERPRETER_ERROR', () => { isDebugging }); - const actions = generateActions(InterpreterActions.evalInterpreterError.type); + const actions = generateActions(l => InterpreterActions.evalInterpreterError([], l)); actions.forEach(action => { const result = WorkspaceReducer(evalEditorDefaultState, action); @@ -530,7 +535,7 @@ describe('EVAL_INTERPRETER_ERROR', () => { output: [ { ...outputWithRunningAndCodeOutput[0] }, { ...outputWithRunningAndCodeOutput[1] }, - { consoleLogs: [] } + { type: 'errors', errors: [], consoleLogs: [] } ] } }); @@ -562,7 +567,7 @@ describe('EVAL_INTERPRETER_SUCCESS', () => { isRunning: false, output: [ { ...outputWithRunningOutput[0] }, - { consoleLogs: ['console-log-test-2'], value: 'undefined' } + { type: 'result', consoleLogs: ['console-log-test-2'], value: undefined } ] } }); @@ -580,6 +585,12 @@ describe('EVAL_INTERPRETER_SUCCESS', () => { editorTabs: [{ highlightedLines, breakpoints }] }); + const expectedOutput: ResultOutput = { + type: 'result', + consoleLogs: [], + value: undefined + }; + const actions = generateActions(l => InterpreterActions.evalInterpreterSuccess(undefined, l)); actions.forEach(action => { @@ -593,7 +604,7 @@ describe('EVAL_INTERPRETER_SUCCESS', () => { output: [ { ...outputWithRunningAndCodeOutput[0] }, { ...outputWithRunningAndCodeOutput[1] }, - { consoleLogs: [], value: undefined } + expectedOutput ] } }); @@ -651,7 +662,7 @@ describe('EVAL_TESTCASE_FAILURE', () => { const evalFailureDefaultState: WorkspaceManagerState = generateDefaultWorkspace({ editorTestcases }); - const actions = generateActions(l => InterpreterActions.evalTestcaseFailure(value, l, 1)) + const actions = generateActions(l => InterpreterActions.evalTestcaseFailure(value, l, 1)); actions.forEach(action => { const result = WorkspaceReducer(evalFailureDefaultState, action); @@ -680,7 +691,7 @@ describe('EVAL_TESTCASE_SUCCESS', () => { editorTestcases }); - const actions = generateActions(l => InterpreterActions.evalTestcaseSuccess(value, l, 1)) + const actions = generateActions(l => InterpreterActions.evalTestcaseSuccess(value, l, 1)); actions.forEach(action => { const result = WorkspaceReducer(testcaseSuccessDefaultState, action); @@ -709,7 +720,7 @@ describe('EVAL_TESTCASE_SUCCESS', () => { editorTestcases }); - const actions = generateActions(l => InterpreterActions.evalTestcaseSuccess(value, l, 0)) + const actions = generateActions(l => InterpreterActions.evalTestcaseSuccess(value, l, 0)); actions.forEach(action => { const result = WorkspaceReducer(testcaseSuccessDefaultState, action); @@ -734,7 +745,7 @@ describe('HANDLE_CONSOLE_LOG', () => { test('works correctly with RunningOutput', () => { const logString = 'test-log-string'; const consoleLogDefaultState = generateDefaultWorkspace({ output: outputWithRunningOutput }); - const actions = generateActions(l => InterpreterActions.handleConsoleLog(l, logString)) + const actions = generateActions(l => InterpreterActions.handleConsoleLog(l, logString)); actions.forEach(action => { const result = WorkspaceReducer(cloneDeep(consoleLogDefaultState), action); @@ -761,7 +772,7 @@ describe('HANDLE_CONSOLE_LOG', () => { const consoleLogDefaultState = generateDefaultWorkspace({ output: outputWithRunningAndCodeOutput }); - const actions = generateActions(l => InterpreterActions.handleConsoleLog(l, logString)) + const actions = generateActions(l => InterpreterActions.handleConsoleLog(l, logString)); actions.forEach(action => { const result = WorkspaceReducer(consoleLogDefaultState, action); @@ -783,7 +794,7 @@ describe('HANDLE_CONSOLE_LOG', () => { const logString = 'test-log-string-3'; const consoleLogDefaultState = generateDefaultWorkspace({ output: [] }); - const actions = generateActions(l => InterpreterActions.handleConsoleLog(l, logString)) + const actions = generateActions(l => InterpreterActions.handleConsoleLog(l, logString)); actions.forEach(action => { const result = WorkspaceReducer(consoleLogDefaultState, action); @@ -850,7 +861,7 @@ describe('RESET_TESTCASE', () => { editorTestcases }); - const actions = generateActions(l => WorkspaceActions.resetTestcase(l, 1)) + const actions = generateActions(l => WorkspaceActions.resetTestcase(l, 1)); actions.forEach(action => { const result = WorkspaceReducer(resetTestcaseDefaultState, action); const location: WorkspaceLocation = action.payload.workspaceLocation; @@ -882,7 +893,9 @@ describe('RESET_WORKSPACE', () => { replValue: 'test repl value' }; - const actions = generateActions(l => WorkspaceActions.resetWorkspace(l, workspaceOptions)); + const actions = generateActions(l => + WorkspaceActions.resetWorkspace(l, workspaceOptions as any) + ); actions.forEach(action => { const result = WorkspaceReducer(resetWorkspaceDefaultState, action); @@ -917,7 +930,7 @@ describe('SEND_REPL_INPUT_TO_OUTPUT', () => { }); const newOutput = 'new-output-test'; - const actions = generateActions(l => WorkspaceActions.sendReplInputToOutput(newOutput, l)) + const actions = generateActions(l => WorkspaceActions.sendReplInputToOutput(newOutput, l)); const newArray = [newOutput].concat(replHistory.records); newArray.pop(); @@ -955,7 +968,7 @@ describe('SEND_REPL_INPUT_TO_OUTPUT', () => { }); const newOutput = ''; - const actions = generateActions(l => WorkspaceActions.sendReplInputToOutput(newOutput, l)) + const actions = generateActions(l => WorkspaceActions.sendReplInputToOutput(newOutput, l)); actions.forEach(action => { const result = WorkspaceReducer(inputToOutputDefaultState, action); @@ -1012,7 +1025,7 @@ describe('SET_SHAREDB_CONNECTED', () => { describe('TOGGLE_EDITOR_AUTORUN', () => { test('toggles isEditorAutorun correctly', () => { - const actions = generateActions(WorkspaceActions.toggleEditorAutorun) + const actions = generateActions(WorkspaceActions.toggleEditorAutorun); actions.forEach(action => { let result = WorkspaceReducer(defaultWorkspaceManager, action); @@ -1119,7 +1132,9 @@ describe('UPDATE_ACTIVE_EDITOR_TAB_INDEX', () => { editorTabs }); - const actions = generateActions(l => WorkspaceActions.updateActiveEditorTabIndex(l, activeEditorTabIndex)) + const actions = generateActions(l => + WorkspaceActions.updateActiveEditorTabIndex(l, activeEditorTabIndex) + ); actions.forEach(action => { const resultThunk = () => WorkspaceReducer(defaultWorkspaceState, action); @@ -1134,7 +1149,9 @@ describe('UPDATE_ACTIVE_EDITOR_TAB_INDEX', () => { editorTabs }); - const actions = generateActions(l => WorkspaceActions.updateActiveEditorTabIndex(l, activeEditorTabIndex)) + const actions = generateActions(l => + WorkspaceActions.updateActiveEditorTabIndex(l, activeEditorTabIndex) + ); actions.forEach(action => { const result = WorkspaceReducer(defaultWorkspaceState, action); @@ -1161,7 +1178,9 @@ describe('UPDATE_ACTIVE_EDITOR_TAB_INDEX', () => { editorTabs }); - const actions = generateActions(l => WorkspaceActions.updateActiveEditorTabIndex(l, activeEditorTabIndex)) + const actions = generateActions(l => + WorkspaceActions.updateActiveEditorTabIndex(l, activeEditorTabIndex) + ); actions.forEach(action => { const resultThunk = () => WorkspaceReducer(defaultWorkspaceState, action); @@ -1200,7 +1219,9 @@ describe('UPDATE_ACTIVE_EDITOR_TAB', () => { ] }); - const actions = generateActions(l => WorkspaceActions.updateActiveEditorTab(l, activeEditorTabOptions)) + const actions = generateActions(l => + WorkspaceActions.updateActiveEditorTab(l, activeEditorTabOptions) + ); actions.forEach(action => { const result = WorkspaceReducer(defaultWorkspaceState, action); @@ -1240,7 +1261,9 @@ describe('UPDATE_ACTIVE_EDITOR_TAB', () => { editorTabs: [] }); - const actions = generateActions(l => WorkspaceActions.updateActiveEditorTab(l, activeEditorTabOptions)) + const actions = generateActions(l => + WorkspaceActions.updateActiveEditorTab(l, activeEditorTabOptions) + ); actions.forEach(action => { const result = WorkspaceReducer(defaultWorkspaceState, action); @@ -1270,7 +1293,9 @@ describe('UPDATE_EDITOR_VALUE', () => { editorTabs }); - const actions = generateActions(l => WorkspaceActions.updateEditorValue(l, editorTabIndex, newEditorValue)) + const actions = generateActions(l => + WorkspaceActions.updateEditorValue(l, editorTabIndex, newEditorValue) + ); actions.forEach(action => { const resultThunk = () => WorkspaceReducer(defaultWorkspaceState, action); @@ -1285,7 +1310,9 @@ describe('UPDATE_EDITOR_VALUE', () => { editorTabs }); - const actions = generateActions(l => WorkspaceActions.updateEditorValue(l, editorTabIndex, newEditorValue)) + const actions = generateActions(l => + WorkspaceActions.updateEditorValue(l, editorTabIndex, newEditorValue) + ); actions.forEach(action => { const resultThunk = () => WorkspaceReducer(defaultWorkspaceState, action); @@ -1300,7 +1327,9 @@ describe('UPDATE_EDITOR_VALUE', () => { editorTabs }); - const actions = generateActions(l => WorkspaceActions.updateEditorValue(l, editorTabIndex, newEditorValue)) + const actions = generateActions(l => + WorkspaceActions.updateEditorValue(l, editorTabIndex, newEditorValue) + ); actions.forEach(action => { const result = WorkspaceReducer(defaultWorkspaceState, action); @@ -1328,7 +1357,7 @@ describe('UPDATE_EDITOR_BREAKPOINTS', () => { breakpoints: [] }; const editorTabs: EditorTabState[] = [zerothEditorTab, firstEditorTab]; - const newBreakpoints = [null, null, 'ace_breakpoint', null, 'ace_breakpoint'] as string[] + const newBreakpoints = [null, null, 'ace_breakpoint', null, 'ace_breakpoint'] as string[]; test('throws an error if the editor tab index is negative', () => { const editorTabIndex = -1; @@ -1337,7 +1366,9 @@ describe('UPDATE_EDITOR_BREAKPOINTS', () => { editorTabs }); - const actions = generateActions(l => WorkspaceActions.setEditorBreakpoint(l, editorTabIndex, newBreakpoints)) + const actions = generateActions(l => + WorkspaceActions.setEditorBreakpoint(l, editorTabIndex, newBreakpoints) + ); actions.forEach(action => { const resultThunk = () => WorkspaceReducer(defaultWorkspaceState, action); @@ -1352,7 +1383,9 @@ describe('UPDATE_EDITOR_BREAKPOINTS', () => { editorTabs }); - const actions = generateActions(l => WorkspaceActions.setEditorBreakpoint(l, editorTabIndex, newBreakpoints)) + const actions = generateActions(l => + WorkspaceActions.setEditorBreakpoint(l, editorTabIndex, newBreakpoints) + ); actions.forEach(action => { const resultThunk = () => WorkspaceReducer(defaultWorkspaceState, action); @@ -1367,7 +1400,9 @@ describe('UPDATE_EDITOR_BREAKPOINTS', () => { editorTabs }); - const actions = generateActions(l => WorkspaceActions.setEditorBreakpoint(l, editorTabIndex, newBreakpoints)) + const actions = generateActions(l => + WorkspaceActions.setEditorBreakpoint(l, editorTabIndex, newBreakpoints) + ); actions.forEach(action => { const result = WorkspaceReducer(defaultWorkspaceState, action); @@ -1407,7 +1442,9 @@ describe('UPDATE_EDITOR_HIGHLIGHTED_LINES', () => { editorTabs }); - const actions = generateActions(l => WorkspaceActions.setEditorHighlightedLines(l, editorTabIndex, newHighlightedLines)) + const actions = generateActions(l => + WorkspaceActions.setEditorHighlightedLines(l, editorTabIndex, newHighlightedLines) + ); actions.forEach(action => { const resultThunk = () => WorkspaceReducer(defaultWorkspaceState, action); @@ -1422,7 +1459,9 @@ describe('UPDATE_EDITOR_HIGHLIGHTED_LINES', () => { editorTabs }); - const actions = generateActions(l => WorkspaceActions.setEditorHighlightedLines(l, editorTabIndex, newHighlightedLines)) + const actions = generateActions(l => + WorkspaceActions.setEditorHighlightedLines(l, editorTabIndex, newHighlightedLines) + ); actions.forEach(action => { const resultThunk = () => WorkspaceReducer(defaultWorkspaceState, action); @@ -1437,7 +1476,9 @@ describe('UPDATE_EDITOR_HIGHLIGHTED_LINES', () => { editorTabs }); - const actions = generateActions(l => WorkspaceActions.setEditorHighlightedLines(l, editorTabIndex, newHighlightedLines)) + const actions = generateActions(l => + WorkspaceActions.setEditorHighlightedLines(l, editorTabIndex, newHighlightedLines) + ); actions.forEach(action => { const result = WorkspaceReducer(defaultWorkspaceState, action); @@ -1480,7 +1521,9 @@ describe('MOVE_CURSOR', () => { editorTabs }); - const actions = generateActions(l => WorkspaceActions.moveCursor(l, editorTabIndex, newCursorPosition)) + const actions = generateActions(l => + WorkspaceActions.moveCursor(l, editorTabIndex, newCursorPosition) + ); actions.forEach(action => { const resultThunk = () => WorkspaceReducer(defaultWorkspaceState, action); @@ -1495,7 +1538,9 @@ describe('MOVE_CURSOR', () => { editorTabs }); - const actions = generateActions(l => WorkspaceActions.moveCursor(l, editorTabIndex, newCursorPosition)) + const actions = generateActions(l => + WorkspaceActions.moveCursor(l, editorTabIndex, newCursorPosition) + ); actions.forEach(action => { const resultThunk = () => WorkspaceReducer(defaultWorkspaceState, action); expect(resultThunk).toThrow('Editor tab index must have a corresponding editor tab!'); @@ -1509,7 +1554,9 @@ describe('MOVE_CURSOR', () => { editorTabs }); - const actions = generateActions(l => WorkspaceActions.moveCursor(l, editorTabIndex, newCursorPosition)) + const actions = generateActions(l => + WorkspaceActions.moveCursor(l, editorTabIndex, newCursorPosition) + ); actions.forEach(action => { const result = WorkspaceReducer(defaultWorkspaceState, action); const location: WorkspaceLocation = action.payload.workspaceLocation; @@ -1635,7 +1682,9 @@ describe('SHIFT_EDITOR_TAB', () => { editorTabs }); - const actions = generateActions(l => WorkspaceActions.shiftEditorTab(l, previousEditorTabIndex, newEditorTabIndex)) + const actions = generateActions(l => + WorkspaceActions.shiftEditorTab(l, previousEditorTabIndex, newEditorTabIndex) + ); actions.forEach(action => { const resultThunk = () => WorkspaceReducer(defaultWorkspaceState, action); expect(resultThunk).toThrow('Previous editor tab index must be non-negative!'); @@ -1650,7 +1699,9 @@ describe('SHIFT_EDITOR_TAB', () => { editorTabs }); - const actions = generateActions(l => WorkspaceActions.shiftEditorTab(l, previousEditorTabIndex, newEditorTabIndex)) + const actions = generateActions(l => + WorkspaceActions.shiftEditorTab(l, previousEditorTabIndex, newEditorTabIndex) + ); actions.forEach(action => { const resultThunk = () => WorkspaceReducer(defaultWorkspaceState, action); expect(resultThunk).toThrow( @@ -1667,7 +1718,9 @@ describe('SHIFT_EDITOR_TAB', () => { editorTabs }); - const actions = generateActions(l => WorkspaceActions.shiftEditorTab(l, previousEditorTabIndex, newEditorTabIndex)) + const actions = generateActions(l => + WorkspaceActions.shiftEditorTab(l, previousEditorTabIndex, newEditorTabIndex) + ); actions.forEach(action => { const resultThunk = () => WorkspaceReducer(defaultWorkspaceState, action); expect(resultThunk).toThrow('New editor tab index must be non-negative!'); @@ -1682,7 +1735,9 @@ describe('SHIFT_EDITOR_TAB', () => { editorTabs }); - const actions = generateActions(l => WorkspaceActions.shiftEditorTab(l, previousEditorTabIndex, newEditorTabIndex)) + const actions = generateActions(l => + WorkspaceActions.shiftEditorTab(l, previousEditorTabIndex, newEditorTabIndex) + ); actions.forEach(action => { const resultThunk = () => WorkspaceReducer(defaultWorkspaceState, action); expect(resultThunk).toThrow('New editor tab index must have a corresponding editor tab!'); @@ -1697,7 +1752,9 @@ describe('SHIFT_EDITOR_TAB', () => { editorTabs }); - const actions = generateActions(l => WorkspaceActions.shiftEditorTab(l, previousEditorTabIndex, newEditorTabIndex)) + const actions = generateActions(l => + WorkspaceActions.shiftEditorTab(l, previousEditorTabIndex, newEditorTabIndex) + ); actions.forEach(action => { const result = WorkspaceReducer(defaultWorkspaceState, action); const location: WorkspaceLocation = action.payload.workspaceLocation; @@ -1723,7 +1780,9 @@ describe('SHIFT_EDITOR_TAB', () => { editorTabs }); - const actions = generateActions(l => WorkspaceActions.shiftEditorTab(l, previousEditorTabIndex, newEditorTabIndex)) + const actions = generateActions(l => + WorkspaceActions.shiftEditorTab(l, previousEditorTabIndex, newEditorTabIndex) + ); actions.forEach(action => { const result = WorkspaceReducer(defaultWorkspaceState, action); @@ -1938,9 +1997,9 @@ describe('REMOVE_EDITOR_TAB_FOR_FILE', () => { editorTabs }); - const actions = generateActions(WorkspaceActions.removeEditorTabForFile.type, { - removedFilePath - }); + const actions = generateActions(l => + WorkspaceActions.removeEditorTabForFile(l, removedFilePath) + ); actions.forEach(action => { const result = WorkspaceReducer(defaultWorkspaceState, action); @@ -1957,10 +2016,9 @@ describe('REMOVE_EDITOR_TAB_FOR_FILE', () => { editorTabs: [zerothEditorTab] }); - const actions = generateActions(WorkspaceActions.removeEditorTabForFile.type, { - removedFilePath - }); - + const actions = generateActions(l => + WorkspaceActions.removeEditorTabForFile(l, removedFilePath) + ); actions.forEach(action => { const result = WorkspaceReducer(defaultWorkspaceState, action); const location: WorkspaceLocation = action.payload.workspaceLocation; @@ -1986,10 +2044,9 @@ describe('REMOVE_EDITOR_TAB_FOR_FILE', () => { editorTabs }); - const actions = generateActions(WorkspaceActions.removeEditorTabForFile.type, { - removedFilePath - }); - + const actions = generateActions(l => + WorkspaceActions.removeEditorTabForFile(l, removedFilePath) + ); actions.forEach(action => { const result = WorkspaceReducer(defaultWorkspaceState, action); const location: WorkspaceLocation = action.payload.workspaceLocation; @@ -2015,10 +2072,9 @@ describe('REMOVE_EDITOR_TAB_FOR_FILE', () => { editorTabs }); - const actions = generateActions(WorkspaceActions.removeEditorTabForFile.type, { - removedFilePath - }); - + const actions = generateActions(l => + WorkspaceActions.removeEditorTabForFile(l, removedFilePath) + ); actions.forEach(action => { const result = WorkspaceReducer(defaultWorkspaceState, action); const location: WorkspaceLocation = action.payload.workspaceLocation; @@ -2044,10 +2100,9 @@ describe('REMOVE_EDITOR_TAB_FOR_FILE', () => { editorTabs }); - const actions = generateActions(WorkspaceActions.removeEditorTabForFile.type, { - removedFilePath - }); - + const actions = generateActions(l => + WorkspaceActions.removeEditorTabForFile(l, removedFilePath) + ); actions.forEach(action => { const result = WorkspaceReducer(defaultWorkspaceState, action); const location: WorkspaceLocation = action.payload.workspaceLocation; @@ -2073,10 +2128,9 @@ describe('REMOVE_EDITOR_TAB_FOR_FILE', () => { editorTabs }); - const actions = generateActions(WorkspaceActions.removeEditorTabForFile.type, { - removedFilePath - }); - + const actions = generateActions(l => + WorkspaceActions.removeEditorTabForFile(l, removedFilePath) + ); actions.forEach(action => { const result = WorkspaceReducer(defaultWorkspaceState, action); const location: WorkspaceLocation = action.payload.workspaceLocation; @@ -2135,10 +2189,9 @@ describe('REMOVE_EDITOR_TABS_FOR_DIRECTORY', () => { editorTabs }); - const actions = generateActions(WorkspaceActions.removeEditorTabsForDirectory.type, { - removedDirectoryPath - }); - + const actions = generateActions(l => + WorkspaceActions.removeEditorTabsForDirectory(l, removedDirectoryPath) + ); actions.forEach(action => { const result = WorkspaceReducer(defaultWorkspaceState, action); // Note: we stringify because context contains functions which cause @@ -2154,10 +2207,9 @@ describe('REMOVE_EDITOR_TABS_FOR_DIRECTORY', () => { editorTabs }); - const actions = generateActions(WorkspaceActions.removeEditorTabsForDirectory.type, { - removedDirectoryPath - }); - + const actions = generateActions(l => + WorkspaceActions.removeEditorTabsForDirectory(l, removedDirectoryPath) + ); actions.forEach(action => { const result = WorkspaceReducer(defaultWorkspaceState, action); const location: WorkspaceLocation = action.payload.workspaceLocation; @@ -2183,10 +2235,9 @@ describe('REMOVE_EDITOR_TABS_FOR_DIRECTORY', () => { editorTabs }); - const actions = generateActions(WorkspaceActions.removeEditorTabsForDirectory.type, { - removedDirectoryPath - }); - + const actions = generateActions(l => + WorkspaceActions.removeEditorTabsForDirectory(l, removedDirectoryPath) + ); actions.forEach(action => { const result = WorkspaceReducer(defaultWorkspaceState, action); const location: WorkspaceLocation = action.payload.workspaceLocation; @@ -2212,10 +2263,9 @@ describe('REMOVE_EDITOR_TABS_FOR_DIRECTORY', () => { editorTabs }); - const actions = generateActions(WorkspaceActions.removeEditorTabsForDirectory.type, { - removedDirectoryPath - }); - + const actions = generateActions(l => + WorkspaceActions.removeEditorTabsForDirectory(l, removedDirectoryPath) + ); actions.forEach(action => { const result = WorkspaceReducer(defaultWorkspaceState, action); const location: WorkspaceLocation = action.payload.workspaceLocation; @@ -2241,10 +2291,9 @@ describe('REMOVE_EDITOR_TABS_FOR_DIRECTORY', () => { editorTabs }); - const actions = generateActions(WorkspaceActions.removeEditorTabsForDirectory.type, { - removedDirectoryPath - }); - + const actions = generateActions(l => + WorkspaceActions.removeEditorTabsForDirectory(l, removedDirectoryPath) + ); actions.forEach(action => { const result = WorkspaceReducer(defaultWorkspaceState, action); const location: WorkspaceLocation = action.payload.workspaceLocation; @@ -2270,10 +2319,9 @@ describe('REMOVE_EDITOR_TABS_FOR_DIRECTORY', () => { editorTabs }); - const actions = generateActions(WorkspaceActions.removeEditorTabsForDirectory.type, { - removedDirectoryPath - }); - + const actions = generateActions(l => + WorkspaceActions.removeEditorTabsForDirectory(l, removedDirectoryPath) + ); actions.forEach(action => { const result = WorkspaceReducer(defaultWorkspaceState, action); const location: WorkspaceLocation = action.payload.workspaceLocation; @@ -2315,11 +2363,9 @@ describe('RENAME_EDITOR_TAB_FOR_FILE', () => { editorTabs }); - const actions = generateActions(WorkspaceActions.renameEditorTabForFile.type, { - oldFilePath, - newFilePath - }); - + const actions = generateActions(l => + WorkspaceActions.renameEditorTabForFile(l, oldFilePath, newFilePath) + ); actions.forEach(action => { const result = WorkspaceReducer(defaultWorkspaceState, action); // Note: we stringify because context contains functions which cause @@ -2336,11 +2382,9 @@ describe('RENAME_EDITOR_TAB_FOR_FILE', () => { editorTabs }); - const actions = generateActions(WorkspaceActions.renameEditorTabForFile.type, { - oldFilePath, - newFilePath - }); - + const actions = generateActions(l => + WorkspaceActions.renameEditorTabForFile(l, oldFilePath, newFilePath) + ); actions.forEach(action => { const result = WorkspaceReducer(defaultWorkspaceState, action); const location: WorkspaceLocation = action.payload.workspaceLocation; @@ -2386,11 +2430,9 @@ describe('RENAME_EDITOR_TABS_FOR_DIRECTORY', () => { editorTabs }); - const actions = generateActions(WorkspaceActions.renameEditorTabsForDirectory.type, { - oldDirectoryPath, - newDirectoryPath - }); - + const actions = generateActions(l => + WorkspaceActions.renameEditorTabsForDirectory(l, oldDirectoryPath, newDirectoryPath) + ); actions.forEach(action => { const result = WorkspaceReducer(defaultWorkspaceState, action); // Note: we stringify because context contains functions which cause @@ -2406,11 +2448,9 @@ describe('RENAME_EDITOR_TABS_FOR_DIRECTORY', () => { editorTabs }); - const actions = generateActions(WorkspaceActions.renameEditorTabsForDirectory.type, { - oldDirectoryPath, - newDirectoryPath - }); - + const actions = generateActions(l => + WorkspaceActions.renameEditorTabsForDirectory(l, oldDirectoryPath, newDirectoryPath) + ); actions.forEach(action => { const result = WorkspaceReducer(defaultWorkspaceState, action); const location: WorkspaceLocation = action.payload.workspaceLocation; @@ -2439,10 +2479,9 @@ describe('RENAME_EDITOR_TABS_FOR_DIRECTORY', () => { describe('UPDATE_HAS_UNSAVED_CHANGES', () => { test('sets hasUnsavedChanges correctly', () => { const hasUnsavedChanges = true; - const actions = generateActions(WorkspaceActions.updateHasUnsavedChanges.type, { - hasUnsavedChanges - }); - + const actions = generateActions(l => + WorkspaceActions.updateHasUnsavedChanges(l, hasUnsavedChanges) + ); actions.forEach(action => { const result = WorkspaceReducer(defaultWorkspaceManager, action); const location: WorkspaceLocation = action.payload.workspaceLocation; @@ -2460,7 +2499,7 @@ describe('UPDATE_HAS_UNSAVED_CHANGES', () => { describe('UPDATE_REPL_VALUE', () => { test('sets replValue correctly', () => { const newReplValue = 'test new repl value'; - const actions = generateActions(WorkspaceActions.updateReplValue.type, { newReplValue }); + const actions = generateActions(l => WorkspaceActions.updateReplValue(newReplValue, l)); actions.forEach(action => { const result = WorkspaceReducer(defaultWorkspaceManager, action); @@ -2479,7 +2518,9 @@ describe('UPDATE_REPL_VALUE', () => { describe('TOGGLE_USING_SUBST', () => { test('sets usingSubst correctly', () => { const usingSubst = true; - const actions = generateActions(WorkspaceActions.toggleUsingSubst.type, { usingSubst }); + const actions = generateActions(l => + WorkspaceActions.toggleUsingSubst(usingSubst, l as WorkspaceLocationsWithTools) + ); actions.forEach(action => { const result = WorkspaceReducer(defaultWorkspaceManager, action); From 0bc8556d8c9684b560dd18e6f3de68e393b89b1b Mon Sep 17 00:00:00 2001 From: "DESKTOP-G08HS3B\\Lee Yi" Date: Thu, 24 Apr 2025 18:52:59 -0400 Subject: [PATCH 05/14] FIx more broken workspace saga tests --- .../sagas/WorkspaceSaga/helpers/evalCode.ts | 2 +- src/commons/sagas/__tests__/WorkspaceSaga.ts | 21 +++++++++++++------ 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/commons/sagas/WorkspaceSaga/helpers/evalCode.ts b/src/commons/sagas/WorkspaceSaga/helpers/evalCode.ts index fc6e7f7639..f55ca932ff 100644 --- a/src/commons/sagas/WorkspaceSaga/helpers/evalCode.ts +++ b/src/commons/sagas/WorkspaceSaga/helpers/evalCode.ts @@ -1,7 +1,7 @@ import { compileAndRun as compileAndRunCCode } from '@sourceacademy/c-slang/ctowasm/dist/index'; import { tokenizer } from 'acorn'; import type { IConduit } from 'conductor/dist/conduit'; -import { Context, interrupt, Result, resume, runFilesInContext } from 'js-slang'; +import { type Context, interrupt, type Result, resume, runFilesInContext } from 'js-slang'; import { ACORN_PARSE_OPTIONS } from 'js-slang/dist/constants'; import { InterruptedError } from 'js-slang/dist/errors/errors'; import { Chapter, ErrorSeverity, ErrorType, type SourceError, Variant } from 'js-slang/dist/types'; diff --git a/src/commons/sagas/__tests__/WorkspaceSaga.ts b/src/commons/sagas/__tests__/WorkspaceSaga.ts index 486bb48924..d869df7816 100644 --- a/src/commons/sagas/__tests__/WorkspaceSaga.ts +++ b/src/commons/sagas/__tests__/WorkspaceSaga.ts @@ -250,7 +250,8 @@ describe('EVAL_REPL', () => { stepLimit: 1000, useSubst: false, throwInfiniteLoops: true, - envSteps: -1 + envSteps: -1, + executionMethod: 'auto' }) .dispatch({ type: WorkspaceActions.evalRepl.type, @@ -810,7 +811,8 @@ describe('evalCode', () => { stepLimit: 1000, useSubst: false, throwInfiniteLoops: true, - envSteps: -1 + envSteps: -1, + executionMethod: 'auto' }; lastDebuggerResult = { status: 'error' }; state = generateDefaultState(workspaceLocation, { lastDebuggerResult: { status: 'error' } }); @@ -839,7 +841,8 @@ describe('evalCode', () => { stepLimit: 1000, useSubst: false, throwInfiniteLoops: true, - envSteps: -1 + envSteps: -1, + executionMethod: 'auto' }) .put(InterpreterActions.evalInterpreterSuccess(value, workspaceLocation)) .silentRun(); @@ -867,7 +870,8 @@ describe('evalCode', () => { stepLimit: 1000, useSubst: false, throwInfiniteLoops: true, - envSteps: -1 + envSteps: -1, + executionMethod: 'auto' }) .put(InterpreterActions.endDebuggerPause(workspaceLocation)) .put(InterpreterActions.evalInterpreterSuccess('Breakpoint hit!', workspaceLocation)) @@ -885,12 +889,16 @@ describe('evalCode', () => { actionType ) .withState(state) + .provide([ + [call(runFilesInContext, files, codeFilePath, context, options), { status: 'error' }] + ]) .call(runFilesInContext, files, codeFilePath, context, { originalMaxExecTime: execTime, stepLimit: 1000, useSubst: false, throwInfiniteLoops: true, - envSteps: -1 + envSteps: -1, + executionMethod: 'auto' }) .put.like({ action: { type: InterpreterActions.evalInterpreterError.type } }) .silentRun(); @@ -923,7 +931,8 @@ describe('evalCode', () => { stepLimit: 1000, useSubst: false, throwInfiniteLoops: true, - envSteps: -1 + envSteps: -1, + executionMethod: 'auto' }) .put(InterpreterActions.evalInterpreterError(context.errors, workspaceLocation)) .silentRun(); From 0854c6e730ded4a188b468f25f1473be6ff41628 Mon Sep 17 00:00:00 2001 From: "DESKTOP-G08HS3B\\Lee Yi" Date: Thu, 24 Apr 2025 19:16:12 -0400 Subject: [PATCH 06/14] Refactor eval code to utilize more type safety --- src/commons/sagas/SafeEffects.ts | 23 +- .../sagas/WorkspaceSaga/helpers/evalCode.ts | 417 +++++++++--------- 2 files changed, 226 insertions(+), 214 deletions(-) diff --git a/src/commons/sagas/SafeEffects.ts b/src/commons/sagas/SafeEffects.ts index c482a73259..a837c1f820 100644 --- a/src/commons/sagas/SafeEffects.ts +++ b/src/commons/sagas/SafeEffects.ts @@ -1,13 +1,18 @@ -import { ActionMatchingPattern } from '@redux-saga/types'; +import type { ActionMatchingPattern } from '@redux-saga/types'; import * as Sentry from '@sentry/browser'; import { - ActionPattern, - ForkEffect, - HelperWorkerParameters, + type ActionPattern, + type ForkEffect, + type HelperWorkerParameters, + select, takeEvery, takeLatest, takeLeading } from 'redux-saga/effects'; +import type { StoriesEnvState } from 'src/features/stories/StoriesTypes'; + +import type { OverallState } from '../application/ApplicationTypes'; +import type { WorkspaceLocation, WorkspaceManagerState } from '../workspace/WorkspaceTypes'; // it's not possible to abstract the two functions into HOF over takeEvery and takeLatest // without stepping out of TypeScript's type system because the type system does not support @@ -95,3 +100,13 @@ export function safeTakeLeading

(pattern, wrappedWorker, ...args); } + +export function* selectWorkspace(workspaceLocation: T) { + const workspace: WorkspaceManagerState[T] = yield select((state: OverallState) => state.workspaces[workspaceLocation]) + return workspace +} + +export function* selectStoryEnv(storyEnv: string) { + const workspace: StoriesEnvState = yield select((state: OverallState) => state.stories.envs[storyEnv]) + return workspace +} diff --git a/src/commons/sagas/WorkspaceSaga/helpers/evalCode.ts b/src/commons/sagas/WorkspaceSaga/helpers/evalCode.ts index f55ca932ff..6b64c1d5a6 100644 --- a/src/commons/sagas/WorkspaceSaga/helpers/evalCode.ts +++ b/src/commons/sagas/WorkspaceSaga/helpers/evalCode.ts @@ -5,7 +5,8 @@ import { type Context, interrupt, type Result, resume, runFilesInContext } from import { ACORN_PARSE_OPTIONS } from 'js-slang/dist/constants'; import { InterruptedError } from 'js-slang/dist/errors/errors'; import { Chapter, ErrorSeverity, ErrorType, type SourceError, Variant } from 'js-slang/dist/types'; -import { eventChannel, SagaIterator } from 'redux-saga'; +import { pick } from 'lodash'; +import { eventChannel, type SagaIterator } from 'redux-saga'; import { call, cancel, cancelled, fork, put, race, select, take } from 'redux-saga/effects'; import * as Sourceror from 'sourceror'; @@ -28,13 +29,118 @@ import { makeExternalBuiltins as makeSourcerorExternalBuiltins } from '../../../ import WorkspaceActions from '../../../workspace/WorkspaceActions'; import { EVAL_SILENT, - type PlaygroundWorkspaceState, - type SicpWorkspaceState, type WorkspaceLocation } from '../../../workspace/WorkspaceTypes'; +import { selectStoryEnv, selectWorkspace } from '../../SafeEffects'; import { dumpDisplayBuffer } from './dumpDisplayBuffer'; import { updateInspector } from './updateInspector'; +async function wasm_compile_and_run( + wasmCode: string, + context: Context, + isRepl: boolean +): Promise { + return Sourceror.compile(wasmCode, context, isRepl) + .then((wasmModule: WebAssembly.Module) => { + const transcoder = new Sourceror.Transcoder(); + return Sourceror.run( + wasmModule, + Sourceror.makePlatformImports(makeSourcerorExternalBuiltins(context), transcoder), + transcoder, + context, + isRepl + ); + }) + .then( + (returnedValue: any): Result => ({ status: 'finished', context, value: returnedValue }), + (e: any): Result => { + console.log(e); + return { status: 'error' }; + } + ); +} + +function reportCCompilationError(errorMessage: string, context: Context) { + context.errors.push({ + type: ErrorType.SYNTAX, + severity: ErrorSeverity.ERROR, + location: { + start: { + line: 0, + column: 0 + }, + end: { + line: 0, + column: 0 + } + }, + explain: () => errorMessage, + elaborate: () => '' + }); +} + +function reportCRuntimeError(errorMessage: string, context: Context) { + context.errors.push({ + type: ErrorType.RUNTIME, + severity: ErrorSeverity.ERROR, + location: { + start: { + line: 0, + column: 0 + }, + end: { + line: 0, + column: 0 + } + }, + explain: () => errorMessage, + elaborate: () => '' + }); +} + +async function cCompileAndRun(cCode: string, context: Context) { + const cCompilerConfig = await makeCCompilerConfig(cCode, context); + return await compileAndRunCCode(cCode, cCompilerConfig) + .then(compilationResult => { + if (compilationResult.status === 'failure') { + // report any compilation failure + reportCCompilationError( + `Compilation failed with the following error(s):\n\n${compilationResult.errorMessage}`, + context + ); + return { + status: 'error', + context + }; + } + if (compilationResult.warnings.length > 0) { + return { + status: 'finished', + context, + value: { + toReplString: () => + `Compilation and program execution successful with the following warning(s):\n${compilationResult.warnings.join( + '\n' + )}` + } + }; + } + if (specialCReturnObject === null) { + return { + status: 'finished', + context, + value: { toReplString: () => 'Compilation and program execution successful.' } + }; + } + return { status: 'finished', context, value: specialCReturnObject }; + }) + .catch((e: any): Result => { + console.log(e); + reportCRuntimeError(e.message, context); + return { status: 'error' }; + }); +} + export function* evalCodeSaga( files: Record, entrypointFilePath: string, @@ -62,71 +168,71 @@ export function* evalCodeSaga( context.chapter > 2; const isStoriesBlock = actionType === actions.evalStory.type || workspaceLocation === 'stories'; + function* getWorkspaceData() { + const workspace = yield* selectWorkspace(workspaceLocation) + const commons = pick(workspace, ['isFolderModeEnabled', 'stepLimit']) + + if (workspaceLocation === 'sicp' || workspaceLocation === 'playground') { + const { + currentStep, + updateCse, + usingCse, + usingSubst, + } = yield* selectWorkspace(workspaceLocation) + + return { + ...commons, + currentStep: updateCse ? -1: currentStep, + cseIsActive: usingCse, + needUpdateCse: updateCse, + substIsActive: usingSubst + } + } + + if (isStoriesBlock) { + const { usingSubst } = yield* selectStoryEnv(storyEnv!) + return { + ...commons, + currentStep: -1, + cseIsActive: false, + needUpdateCse: false, + substIsActive: usingSubst, + } + } + + return { + ...commons, + currentStep: -1, + cseIsActive: false, + needUpdateCse: false, + substIsActive: false + } + } + + const entrypointCode = files[entrypointFilePath]; // Logic for execution of substitution model visualizer - const correctWorkspace = workspaceLocation === 'playground' || workspaceLocation === 'sicp'; - const substIsActive: boolean = correctWorkspace - ? yield select( - (state: OverallState) => - (state.workspaces[workspaceLocation] as PlaygroundWorkspaceState | SicpWorkspaceState) - .usingSubst - ) - : isStoriesBlock - ? // Safe to use ! as storyEnv will be defined from above when we call from EVAL_STORY - yield select((state: OverallState) => state.stories.envs[storyEnv!].usingSubst) - : false; - const stepLimit: number = isStoriesBlock - ? yield select((state: OverallState) => state.stories.envs[storyEnv!].stepLimit) - : yield select((state: OverallState) => state.workspaces[workspaceLocation].stepLimit); - const substActiveAndCorrectChapter = context.chapter <= Chapter.SOURCE_2 && substIsActive; - - const uploadIsActive: boolean = correctWorkspace - ? yield select( - (state: OverallState) => - (state.workspaces[workspaceLocation] as PlaygroundWorkspaceState | SicpWorkspaceState) - .usingUpload - ) - : false; - const uploads = yield select((state: OverallState) => state.workspaces[workspaceLocation].files); - - // For the CSE machine slider - const cseIsActive: boolean = correctWorkspace - ? yield select( - (state: OverallState) => - (state.workspaces[workspaceLocation] as PlaygroundWorkspaceState | SicpWorkspaceState) - .usingCse - ) - : false; - const needUpdateCse: boolean = correctWorkspace - ? yield select( - (state: OverallState) => - (state.workspaces[workspaceLocation] as PlaygroundWorkspaceState | SicpWorkspaceState) - .updateCse - ) - : false; - // When currentStep is -1, the entire code is run from the start. - const currentStep: number = needUpdateCse - ? -1 - : correctWorkspace - ? yield select( - (state: OverallState) => - (state.workspaces[workspaceLocation] as PlaygroundWorkspaceState | SicpWorkspaceState) - .currentStep - ) - : -1; + const { + currentStep, + cseIsActive, + isFolderModeEnabled, + needUpdateCse, + stepLimit, + substIsActive + } = yield* getWorkspaceData() + const cseActiveAndCorrectChapter = (isSchemeLanguage(context.chapter) || context.chapter >= 3) && cseIsActive; if (cseActiveAndCorrectChapter) { context.executionMethod = 'cse-machine'; } - const isFolderModeEnabled: boolean = yield select( - (state: OverallState) => state.workspaces[workspaceLocation].isFolderModeEnabled - ); - - const entrypointCode = files[entrypointFilePath]; + function* getEvalEffect() { + if (actionType === InterpreterActions.debuggerResume.type) { + const { lastDebuggerResult } = yield* selectWorkspace(workspaceLocation) + return call(resume, lastDebuggerResult) + } - function call_variant(variant: Variant) { - if (variant === Variant.WASM) { + if (context.variant === Variant.WASM) { // Note: WASM does not support multiple file programs. return call( wasm_compile_and_run, @@ -134,130 +240,51 @@ export function* evalCodeSaga( context, actionType === WorkspaceActions.evalRepl.type ); - } else { - throw new Error('Unknown variant: ' + variant); } - } - async function wasm_compile_and_run( - wasmCode: string, - wasmContext: Context, - isRepl: boolean - ): Promise { - return Sourceror.compile(wasmCode, wasmContext, isRepl) - .then((wasmModule: WebAssembly.Module) => { - const transcoder = new Sourceror.Transcoder(); - return Sourceror.run( - wasmModule, - Sourceror.makePlatformImports(makeSourcerorExternalBuiltins(wasmContext), transcoder), - transcoder, - wasmContext, - isRepl - ); - }) - .then( - (returnedValue: any): Result => ({ status: 'finished', context, value: returnedValue }), - (e: any): Result => { - console.log(e); - return { status: 'error' }; - } - ); - } - function reportCCompilationError(errorMessage: string, context: Context) { - context.errors.push({ - type: ErrorType.SYNTAX, - severity: ErrorSeverity.ERROR, - location: { - start: { - line: 0, - column: 0 - }, - end: { - line: 0, - column: 0 - } - }, - explain: () => errorMessage, - elaborate: () => '' - }); - } + switch (context.chapter) { + case Chapter.FULL_C: + return call(cCompileAndRun, entrypointCode, context) + case Chapter.FULL_JAVA: { + const { + usingCse: isUsingCse, + usingUpload: uploadIsActive, + files: uploads + } = yield* selectWorkspace('playground') + + return call(javaRun, entrypointCode, context, currentStep, isUsingCse, { + uploadIsActive, + uploads + }) + } + } - function reportCRuntimeError(errorMessage: string, context: Context) { - context.errors.push({ - type: ErrorType.RUNTIME, - severity: ErrorSeverity.ERROR, - location: { - start: { - line: 0, - column: 0 - }, - end: { - line: 0, - column: 0 - } - }, - explain: () => errorMessage, - elaborate: () => '' - }); - } + const substActiveAndCorrectChapter = context.chapter <= Chapter.SOURCE_2 && substIsActive; - async function cCompileAndRun(cCode: string, context: Context) { - const cCompilerConfig = await makeCCompilerConfig(cCode, context); - return await compileAndRunCCode(cCode, cCompilerConfig) - .then(compilationResult => { - if (compilationResult.status === 'failure') { - // report any compilation failure - reportCCompilationError( - `Compilation failed with the following error(s):\n\n${compilationResult.errorMessage}`, - context - ); - return { - status: 'error', - context - }; - } - if (compilationResult.warnings.length > 0) { - return { - status: 'finished', - context, - value: { - toReplString: () => - `Compilation and program execution successful with the following warning(s):\n${compilationResult.warnings.join( - '\n' - )}` - } - }; - } - if (specialCReturnObject === null) { - return { - status: 'finished', - context, - value: { toReplString: () => 'Compilation and program execution successful.' } - }; - } - return { status: 'finished', context, value: specialCReturnObject }; - }) - .catch((e: any): Result => { - console.log(e); - reportCRuntimeError(e.message, context); - return { status: 'error' }; - }); + return call( + runFilesInContext, + isFolderModeEnabled + ? files + : { + [entrypointFilePath]: files[entrypointFilePath] + }, + entrypointFilePath, + context, + { + originalMaxExecTime: execTime, + stepLimit: stepLimit, + throwInfiniteLoops: true, + useSubst: substActiveAndCorrectChapter, + envSteps: currentStep, + executionMethod: cseActiveAndCorrectChapter ? 'cse-machine' : 'auto' + } + ) } - const isWasm: boolean = context.variant === Variant.WASM; - const isC: boolean = context.chapter === Chapter.FULL_C; - const isJava: boolean = context.chapter === Chapter.FULL_JAVA; - - let lastDebuggerResult = yield select( - (state: OverallState) => state.workspaces[workspaceLocation].lastDebuggerResult - ); - const isUsingCse = yield select((state: OverallState) => state.workspaces['playground'].usingCse); - // Handles `console.log` statements in fullJS - const detachConsole: () => void = - context.chapter === Chapter.FULL_JS - ? DisplayBufferService.attachConsole(workspaceLocation) - : () => {}; + if (context.chapter === Chapter.FULL_JS || context.chapter === Chapter.FULL_TS) { + yield call([DisplayBufferService, DisplayBufferService.attachConsole], workspaceLocation) + } const { result, @@ -268,37 +295,7 @@ export function* evalCodeSaga( interrupted: any; paused: any; } = yield race({ - result: - actionType === InterpreterActions.debuggerResume.type - ? call(resume, lastDebuggerResult) - : isWasm - ? call_variant(context.variant) - : isC - ? call(cCompileAndRun, entrypointCode, context) - : isJava - ? call(javaRun, entrypointCode, context, currentStep, isUsingCse, { - uploadIsActive, - uploads - }) - : call( - runFilesInContext, - isFolderModeEnabled - ? files - : { - [entrypointFilePath]: files[entrypointFilePath] - }, - entrypointFilePath, - context, - { - originalMaxExecTime: execTime, - stepLimit: stepLimit, - throwInfiniteLoops: true, - useSubst: substActiveAndCorrectChapter, - envSteps: currentStep, - executionMethod: cseActiveAndCorrectChapter ? 'cse-machine' : 'auto' - } - ), - + result: yield* getEvalEffect(), /** * A BEGIN_INTERRUPT_EXECUTION signals the beginning of an interruption, * i.e the trigger for the interpreter to interrupt execution. @@ -307,7 +304,7 @@ export function* evalCodeSaga( paused: take(InterpreterActions.beginDebuggerPause.type) }); - detachConsole(); + // detachConsole(); if (interrupted) { interrupt(context); @@ -398,7 +395,7 @@ export function* evalCodeSaga( } } - lastDebuggerResult = yield select( + const lastDebuggerResult = yield select( (state: OverallState) => state.workspaces[workspaceLocation].lastDebuggerResult ); // For EVAL_EDITOR and EVAL_REPL, we send notification to workspace that a program has been evaluated From a953608754d0aa1889bf102e65d27228bf61b40a Mon Sep 17 00:00:00 2001 From: "DESKTOP-G08HS3B\\Lee Yi" Date: Fri, 25 Apr 2025 19:12:52 -0400 Subject: [PATCH 07/14] Minor refactoring --- src/commons/application/ApplicationTypes.ts | 4 +- src/commons/sagas/PlaygroundSaga.ts | 47 ++-- src/commons/sagas/SafeEffects.ts | 12 +- src/commons/sagas/StoriesSaga.ts | 6 +- .../helpers/blockExtraMethods.ts | 12 +- .../WorkspaceSaga/helpers/clearContext.ts | 21 +- .../sagas/WorkspaceSaga/helpers/evalCode.ts | 238 ++++++++---------- .../sagas/WorkspaceSaga/helpers/evalEditor.ts | 8 +- .../helpers/restoreExtraMethods.ts | 8 +- .../WorkspaceSaga/helpers/runTestCase.ts | 20 +- src/commons/sagas/WorkspaceSaga/index.ts | 46 ++-- 11 files changed, 191 insertions(+), 231 deletions(-) diff --git a/src/commons/application/ApplicationTypes.ts b/src/commons/application/ApplicationTypes.ts index ef69fd0269..9f91a27e1f 100644 --- a/src/commons/application/ApplicationTypes.ts +++ b/src/commons/application/ApplicationTypes.ts @@ -1,4 +1,4 @@ -import { Chapter, Language, type SourceError, Variant } from 'js-slang/dist/types'; +import { Chapter, Language, type SourceError, type Value, Variant } from 'js-slang/dist/types'; import type { AchievementState } from '../../features/achievement/AchievementTypes'; import type { DashboardState } from '../../features/dashboard/DashboardTypes'; @@ -74,7 +74,7 @@ export type CodeOutput = { */ export type ResultOutput = { type: 'result'; - value: any; + value: Value; consoleLogs: string[]; runtime?: number; isProgram?: boolean; diff --git a/src/commons/sagas/PlaygroundSaga.ts b/src/commons/sagas/PlaygroundSaga.ts index c4cd8a71d9..1384af5343 100644 --- a/src/commons/sagas/PlaygroundSaga.ts +++ b/src/commons/sagas/PlaygroundSaga.ts @@ -1,5 +1,5 @@ import { FSModule } from 'browserfs/dist/node/core/FS'; -import { Chapter, Variant } from 'js-slang/dist/types'; +import { Chapter } from 'js-slang/dist/types'; import { compressToEncodedURIComponent } from 'lz-string'; import qs from 'query-string'; import { SagaIterator } from 'redux-saga'; @@ -8,16 +8,19 @@ import CseMachine from 'src/features/cseMachine/CseMachine'; import { CseMachine as JavaCseMachine } from 'src/features/cseMachine/java/CseMachine'; import PlaygroundActions from '../../features/playground/PlaygroundActions'; -import { isSchemeLanguage, isSourceLanguage, OverallState } from '../application/ApplicationTypes'; -import { ExternalLibraryName } from '../application/types/ExternalTypes'; +import { + isSchemeLanguage, + isSourceLanguage, + type OverallState +} from '../application/ApplicationTypes'; import { retrieveFilesInWorkspaceAsRecord } from '../fileSystem/utils'; import { visitSideContent } from '../sideContent/SideContentActions'; import { SideContentType } from '../sideContent/SideContentTypes'; import Constants from '../utils/Constants'; import { showSuccessMessage, showWarningMessage } from '../utils/notifications/NotificationsHelper'; import WorkspaceActions from '../workspace/WorkspaceActions'; -import { EditorTabState, PlaygroundWorkspaceState } from '../workspace/WorkspaceTypes'; -import { safeTakeEvery as takeEvery } from './SafeEffects'; +import type { PlaygroundWorkspaceState } from '../workspace/WorkspaceTypes'; +import { safeTakeEvery as takeEvery, selectWorkspace } from './SafeEffects'; export default function* PlaygroundSaga(): SagaIterator { yield takeEvery(PlaygroundActions.generateLzString.type, updateQueryString); @@ -127,9 +130,6 @@ export default function* PlaygroundSaga(): SagaIterator { } function* updateQueryString() { - const isFolderModeEnabled: boolean = yield select( - (state: OverallState) => state.workspaces.playground.isFolderModeEnabled - ); const fileSystem: FSModule = yield select( (state: OverallState) => state.fileSystem.inBrowserFileSystem ); @@ -138,27 +138,20 @@ function* updateQueryString() { 'playground', fileSystem ); - const editorTabs: EditorTabState[] = yield select( - (state: OverallState) => state.workspaces.playground.editorTabs - ); + + const { + activeEditorTabIndex, + context: { chapter, variant }, + editorTabs, + execTime, + externalLibrary: external, + isFolderModeEnabled + } = yield* selectWorkspace('playground'); + const editorTabFilePaths = editorTabs - .map((editorTab: EditorTabState) => editorTab.filePath) + .map(editorTab => editorTab.filePath) .filter((filePath): filePath is string => filePath !== undefined); - const activeEditorTabIndex: number | null = yield select( - (state: OverallState) => state.workspaces.playground.activeEditorTabIndex - ); - const chapter: Chapter = yield select( - (state: OverallState) => state.workspaces.playground.context.chapter - ); - const variant: Variant = yield select( - (state: OverallState) => state.workspaces.playground.context.variant - ); - const external: ExternalLibraryName = yield select( - (state: OverallState) => state.workspaces.playground.externalLibrary - ); - const execTime: number = yield select( - (state: OverallState) => state.workspaces.playground.execTime - ); + const newQueryString = qs.stringify({ isFolder: isFolderModeEnabled, files: compressToEncodedURIComponent(qs.stringify(files)), diff --git a/src/commons/sagas/SafeEffects.ts b/src/commons/sagas/SafeEffects.ts index a837c1f820..9379407193 100644 --- a/src/commons/sagas/SafeEffects.ts +++ b/src/commons/sagas/SafeEffects.ts @@ -102,11 +102,15 @@ export function safeTakeLeading

(workspaceLocation: T) { - const workspace: WorkspaceManagerState[T] = yield select((state: OverallState) => state.workspaces[workspaceLocation]) - return workspace + const workspace: WorkspaceManagerState[T] = yield select( + (state: OverallState) => state.workspaces[workspaceLocation] + ); + return workspace; } export function* selectStoryEnv(storyEnv: string) { - const workspace: StoriesEnvState = yield select((state: OverallState) => state.stories.envs[storyEnv]) - return workspace + const workspace: StoriesEnvState = yield select( + (state: OverallState) => state.stories.envs[storyEnv] + ); + return workspace; } diff --git a/src/commons/sagas/StoriesSaga.ts b/src/commons/sagas/StoriesSaga.ts index c071186724..f4c21d5ba8 100644 --- a/src/commons/sagas/StoriesSaga.ts +++ b/src/commons/sagas/StoriesSaga.ts @@ -12,10 +12,10 @@ import { putStoriesUserRole, updateStory } from 'src/features/stories/storiesComponents/BackendAccess'; -import { StoryData, StoryListView, StoryView } from 'src/features/stories/StoriesTypes'; +import type { StoryData, StoryListView, StoryView } from 'src/features/stories/StoriesTypes'; import SessionActions from '../application/actions/SessionActions'; -import { OverallState, StoriesRole } from '../application/ApplicationTypes'; +import { type OverallState, StoriesRole } from '../application/ApplicationTypes'; import { Tokens } from '../application/types/SessionTypes'; import { combineSagaHandlers } from '../redux/utils'; import { resetSideContent } from '../sideContent/SideContentActions'; @@ -143,8 +143,8 @@ const StoriesSaga = combineSagaHandlers(sagaActions, { codeFilePath, context, execTime, - 'stories', action.type, + 'stories', env ); }, diff --git a/src/commons/sagas/WorkspaceSaga/helpers/blockExtraMethods.ts b/src/commons/sagas/WorkspaceSaga/helpers/blockExtraMethods.ts index 95ec7e6fb7..a93c57b056 100644 --- a/src/commons/sagas/WorkspaceSaga/helpers/blockExtraMethods.ts +++ b/src/commons/sagas/WorkspaceSaga/helpers/blockExtraMethods.ts @@ -1,4 +1,4 @@ -import { Context } from 'js-slang'; +import type { Context } from 'js-slang'; import { call } from 'redux-saga/effects'; import { @@ -6,7 +6,7 @@ import { getDifferenceInMethods, getStoreExtraMethodsString } from '../../../utils/JsSlangHelper'; -import { EVAL_SILENT, WorkspaceLocation } from '../../../workspace/WorkspaceTypes'; +import { EVAL_SILENT, type WorkspaceLocation } from '../../../workspace/WorkspaceTypes'; import { evalCodeSaga } from './evalCode'; export function* blockExtraMethods( @@ -30,8 +30,8 @@ export function* blockExtraMethods( storeValuesFilePath, elevatedContext, execTime, - workspaceLocation, - EVAL_SILENT + EVAL_SILENT, + workspaceLocation ); } @@ -46,7 +46,7 @@ export function* blockExtraMethods( nullifierFilePath, elevatedContext, execTime, - workspaceLocation, - EVAL_SILENT + EVAL_SILENT, + workspaceLocation ); } diff --git a/src/commons/sagas/WorkspaceSaga/helpers/clearContext.ts b/src/commons/sagas/WorkspaceSaga/helpers/clearContext.ts index cda1d8cfd4..cb7d034334 100644 --- a/src/commons/sagas/WorkspaceSaga/helpers/clearContext.ts +++ b/src/commons/sagas/WorkspaceSaga/helpers/clearContext.ts @@ -1,28 +1,19 @@ import type { Context } from 'js-slang'; import { defineSymbol } from 'js-slang/dist/createContext'; -import type { Variant } from 'js-slang/dist/types'; import { put, select, take } from 'redux-saga/effects'; import WorkspaceActions from 'src/commons/workspace/WorkspaceActions'; import type { OverallState } from '../../../application/ApplicationTypes'; -import { ExternalLibraryName } from '../../../application/types/ExternalTypes'; import { actions } from '../../../utils/ActionsHelper'; import type { WorkspaceLocation } from '../../../workspace/WorkspaceTypes'; +import { selectWorkspace } from '../../SafeEffects'; export function* clearContext(workspaceLocation: WorkspaceLocation, entrypointCode: string) { - const [chapter, symbols, externalLibraryName, globals, variant]: [ - number, - string[], - ExternalLibraryName, - Array<[string, any]>, - Variant - ] = yield select((state: OverallState) => [ - state.workspaces[workspaceLocation].context.chapter, - state.workspaces[workspaceLocation].context.externalSymbols, - state.workspaces[workspaceLocation].externalLibrary, - state.workspaces[workspaceLocation].globals, - state.workspaces[workspaceLocation].context.variant - ]); + const { + context: { chapter, externalSymbols: symbols, variant }, + externalLibrary: externalLibraryName, + globals + } = yield* selectWorkspace(workspaceLocation); const library = { chapter, diff --git a/src/commons/sagas/WorkspaceSaga/helpers/evalCode.ts b/src/commons/sagas/WorkspaceSaga/helpers/evalCode.ts index 6b64c1d5a6..2348afe6d3 100644 --- a/src/commons/sagas/WorkspaceSaga/helpers/evalCode.ts +++ b/src/commons/sagas/WorkspaceSaga/helpers/evalCode.ts @@ -27,10 +27,7 @@ import DisplayBufferService from '../../../utils/DisplayBufferService'; import { showWarningMessage } from '../../../utils/notifications/NotificationsHelper'; import { makeExternalBuiltins as makeSourcerorExternalBuiltins } from '../../../utils/SourcerorHelper'; import WorkspaceActions from '../../../workspace/WorkspaceActions'; -import { - EVAL_SILENT, - type WorkspaceLocation -} from '../../../workspace/WorkspaceTypes'; +import { EVAL_SILENT, type WorkspaceLocation } from '../../../workspace/WorkspaceTypes'; import { selectStoryEnv, selectWorkspace } from '../../SafeEffects'; import { dumpDisplayBuffer } from './dumpDisplayBuffer'; import { updateInspector } from './updateInspector'; @@ -40,105 +37,100 @@ async function wasm_compile_and_run( context: Context, isRepl: boolean ): Promise { - return Sourceror.compile(wasmCode, context, isRepl) - .then((wasmModule: WebAssembly.Module) => { - const transcoder = new Sourceror.Transcoder(); - return Sourceror.run( - wasmModule, - Sourceror.makePlatformImports(makeSourcerorExternalBuiltins(context), transcoder), - transcoder, - context, - isRepl - ); - }) - .then( - (returnedValue: any): Result => ({ status: 'finished', context, value: returnedValue }), - (e: any): Result => { - console.log(e); - return { status: 'error' }; - } + try { + const wasmModule = await Sourceror.compile(wasmCode, context, isRepl); + const transcoder = new Sourceror.Transcoder(); + const returnedValue = await Sourceror.run( + wasmModule, + Sourceror.makePlatformImports(makeSourcerorExternalBuiltins(context), transcoder), + transcoder, + context, + isRepl ); + return { status: 'finished', context, value: returnedValue }; + } catch (e) { + console.log(e); + return { status: 'error' }; + } } -function reportCCompilationError(errorMessage: string, context: Context) { - context.errors.push({ - type: ErrorType.SYNTAX, - severity: ErrorSeverity.ERROR, - location: { - start: { - line: 0, - column: 0 +async function cCompileAndRun(cCode: string, context: Context): Promise { + function reportCCompilationError(errorMessage: string, context: Context) { + context.errors.push({ + type: ErrorType.SYNTAX, + severity: ErrorSeverity.ERROR, + location: { + start: { + line: 0, + column: 0 + }, + end: { + line: 0, + column: 0 + } }, - end: { - line: 0, - column: 0 - } - }, - explain: () => errorMessage, - elaborate: () => '' - }); -} + explain: () => errorMessage, + elaborate: () => '' + }); + } -function reportCRuntimeError(errorMessage: string, context: Context) { - context.errors.push({ - type: ErrorType.RUNTIME, - severity: ErrorSeverity.ERROR, - location: { - start: { - line: 0, - column: 0 + function reportCRuntimeError(errorMessage: string, context: Context) { + context.errors.push({ + type: ErrorType.RUNTIME, + severity: ErrorSeverity.ERROR, + location: { + start: { + line: 0, + column: 0 + }, + end: { + line: 0, + column: 0 + } }, - end: { - line: 0, - column: 0 - } - }, - explain: () => errorMessage, - elaborate: () => '' - }); -} - -async function cCompileAndRun(cCode: string, context: Context) { - const cCompilerConfig = await makeCCompilerConfig(cCode, context); - return await compileAndRunCCode(cCode, cCompilerConfig) - .then(compilationResult => { - if (compilationResult.status === 'failure') { - // report any compilation failure - reportCCompilationError( - `Compilation failed with the following error(s):\n\n${compilationResult.errorMessage}`, - context - ); - return { - status: 'error', - context - }; - } - if (compilationResult.warnings.length > 0) { - return { - status: 'finished', - context, - value: { - toReplString: () => - `Compilation and program execution successful with the following warning(s):\n${compilationResult.warnings.join( - '\n' - )}` - } - }; - } - if (specialCReturnObject === null) { - return { - status: 'finished', - context, - value: { toReplString: () => 'Compilation and program execution successful.' } - }; - } - return { status: 'finished', context, value: specialCReturnObject }; - }) - .catch((e: any): Result => { - console.log(e); - reportCRuntimeError(e.message, context); - return { status: 'error' }; + explain: () => errorMessage, + elaborate: () => '' }); + } + const cCompilerConfig = await makeCCompilerConfig(cCode, context); + try { + const compilationResult = await compileAndRunCCode(cCode, cCompilerConfig); + if (compilationResult.status === 'failure') { + // report any compilation failure + reportCCompilationError( + `Compilation failed with the following error(s):\n\n${compilationResult.errorMessage}`, + context + ); + return { + status: 'error', + context + } as Result; + } + if (compilationResult.warnings.length > 0) { + return { + status: 'finished', + context, + value: { + toReplString: () => + `Compilation and program execution successful with the following warning(s):\n${compilationResult.warnings.join( + '\n' + )}` + } + }; + } + if (specialCReturnObject === null) { + return { + status: 'finished', + context, + value: { toReplString: () => 'Compilation and program execution successful.' } + }; + } + return { status: 'finished', context, value: specialCReturnObject }; + } catch (e) { + console.log(e); + reportCRuntimeError(e.message, context); + return { status: 'error' }; + } } export function* evalCodeSaga( @@ -146,8 +138,8 @@ export function* evalCodeSaga( entrypointFilePath: string, context: Context, execTime: number, - workspaceLocation: WorkspaceLocation, actionType: string, + workspaceLocation: WorkspaceLocation, storyEnv?: string ): SagaIterator { if (yield call(selectFeatureSaga, flagConductorEnable)) { @@ -169,35 +161,31 @@ export function* evalCodeSaga( const isStoriesBlock = actionType === actions.evalStory.type || workspaceLocation === 'stories'; function* getWorkspaceData() { - const workspace = yield* selectWorkspace(workspaceLocation) - const commons = pick(workspace, ['isFolderModeEnabled', 'stepLimit']) + const workspace = yield* selectWorkspace(workspaceLocation); + const commons = pick(workspace, ['isFolderModeEnabled', 'stepLimit']); if (workspaceLocation === 'sicp' || workspaceLocation === 'playground') { - const { - currentStep, - updateCse, - usingCse, - usingSubst, - } = yield* selectWorkspace(workspaceLocation) + const { currentStep, updateCse, usingCse, usingSubst } = + yield* selectWorkspace(workspaceLocation); return { ...commons, - currentStep: updateCse ? -1: currentStep, + currentStep: updateCse ? -1 : currentStep, cseIsActive: usingCse, needUpdateCse: updateCse, substIsActive: usingSubst - } + }; } if (isStoriesBlock) { - const { usingSubst } = yield* selectStoryEnv(storyEnv!) + const { usingSubst } = yield* selectStoryEnv(storyEnv!); return { ...commons, currentStep: -1, cseIsActive: false, needUpdateCse: false, - substIsActive: usingSubst, - } + substIsActive: usingSubst + }; } return { @@ -206,30 +194,24 @@ export function* evalCodeSaga( cseIsActive: false, needUpdateCse: false, substIsActive: false - } + }; } const entrypointCode = files[entrypointFilePath]; // Logic for execution of substitution model visualizer - const { - currentStep, - cseIsActive, - isFolderModeEnabled, - needUpdateCse, - stepLimit, - substIsActive - } = yield* getWorkspaceData() + const { currentStep, cseIsActive, isFolderModeEnabled, needUpdateCse, stepLimit, substIsActive } = + yield* getWorkspaceData(); const cseActiveAndCorrectChapter = - (isSchemeLanguage(context.chapter) || context.chapter >= 3) && cseIsActive; + (isSchemeLanguage(context.chapter) || context.chapter >= Chapter.SOURCE_3) && cseIsActive; if (cseActiveAndCorrectChapter) { context.executionMethod = 'cse-machine'; } function* getEvalEffect() { if (actionType === InterpreterActions.debuggerResume.type) { - const { lastDebuggerResult } = yield* selectWorkspace(workspaceLocation) - return call(resume, lastDebuggerResult) + const { lastDebuggerResult } = yield* selectWorkspace(workspaceLocation); + return call(resume, lastDebuggerResult); } if (context.variant === Variant.WASM) { @@ -244,18 +226,18 @@ export function* evalCodeSaga( switch (context.chapter) { case Chapter.FULL_C: - return call(cCompileAndRun, entrypointCode, context) + return call(cCompileAndRun, entrypointCode, context); case Chapter.FULL_JAVA: { const { usingCse: isUsingCse, usingUpload: uploadIsActive, files: uploads - } = yield* selectWorkspace('playground') + } = yield* selectWorkspace('playground'); return call(javaRun, entrypointCode, context, currentStep, isUsingCse, { uploadIsActive, uploads - }) + }); } } @@ -278,12 +260,12 @@ export function* evalCodeSaga( envSteps: currentStep, executionMethod: cseActiveAndCorrectChapter ? 'cse-machine' : 'auto' } - ) + ); } // Handles `console.log` statements in fullJS if (context.chapter === Chapter.FULL_JS || context.chapter === Chapter.FULL_TS) { - yield call([DisplayBufferService, DisplayBufferService.attachConsole], workspaceLocation) + yield call([DisplayBufferService, DisplayBufferService.attachConsole], workspaceLocation); } const { @@ -304,8 +286,6 @@ export function* evalCodeSaga( paused: take(InterpreterActions.beginDebuggerPause.type) }); - // detachConsole(); - if (interrupted) { interrupt(context); /* Redundancy, added ensure that interruption results in an error. */ diff --git a/src/commons/sagas/WorkspaceSaga/helpers/evalEditor.ts b/src/commons/sagas/WorkspaceSaga/helpers/evalEditor.ts index 540052f845..2275f553d1 100644 --- a/src/commons/sagas/WorkspaceSaga/helpers/evalEditor.ts +++ b/src/commons/sagas/WorkspaceSaga/helpers/evalEditor.ts @@ -103,8 +103,8 @@ export function* evalEditorSaga( prependFilePath, elevatedContext, execTime, - workspaceLocation, - EVAL_SILENT + EVAL_SILENT, + workspaceLocation ); // Block use of methods from privileged context yield* blockExtraMethods(elevatedContext, context, execTime, workspaceLocation); @@ -116,8 +116,8 @@ export function* evalEditorSaga( entrypointFilePath, context, execTime, - workspaceLocation, - WorkspaceActions.evalEditor.type + WorkspaceActions.evalEditor.type, + workspaceLocation ); } } diff --git a/src/commons/sagas/WorkspaceSaga/helpers/restoreExtraMethods.ts b/src/commons/sagas/WorkspaceSaga/helpers/restoreExtraMethods.ts index 377d3c6eaf..12458d6f0b 100644 --- a/src/commons/sagas/WorkspaceSaga/helpers/restoreExtraMethods.ts +++ b/src/commons/sagas/WorkspaceSaga/helpers/restoreExtraMethods.ts @@ -1,8 +1,8 @@ -import { Context } from 'js-slang'; +import type { Context } from 'js-slang'; import { call } from 'redux-saga/effects'; import { getDifferenceInMethods, getRestoreExtraMethodsString } from '../../../utils/JsSlangHelper'; -import { EVAL_SILENT, WorkspaceLocation } from '../../../workspace/WorkspaceTypes'; +import { EVAL_SILENT, type WorkspaceLocation } from '../../../workspace/WorkspaceTypes'; import { evalCodeSaga } from './evalCode'; export function* restoreExtraMethods( @@ -24,7 +24,7 @@ export function* restoreExtraMethods( restorerFilePath, elevatedContext, execTime, - workspaceLocation, - EVAL_SILENT + EVAL_SILENT, + workspaceLocation ); } diff --git a/src/commons/sagas/WorkspaceSaga/helpers/runTestCase.ts b/src/commons/sagas/WorkspaceSaga/helpers/runTestCase.ts index 1a29a369d5..c5fa54e460 100644 --- a/src/commons/sagas/WorkspaceSaga/helpers/runTestCase.ts +++ b/src/commons/sagas/WorkspaceSaga/helpers/runTestCase.ts @@ -1,12 +1,12 @@ -import { Context } from 'js-slang'; +import type { Context } from 'js-slang'; import { random } from 'lodash'; import { call, put, select, StrictEffect } from 'redux-saga/effects'; -import { OverallState } from '../../../application/ApplicationTypes'; -import { TestcaseType } from '../../../assessment/AssessmentTypes'; +import type { OverallState } from '../../../application/ApplicationTypes'; +import type { TestcaseType } from '../../../assessment/AssessmentTypes'; import { actions } from '../../../utils/ActionsHelper'; import { makeElevatedContext } from '../../../utils/JsSlangHelper'; -import { EVAL_SILENT, WorkspaceLocation } from '../../../workspace/WorkspaceTypes'; +import { EVAL_SILENT, type WorkspaceLocation } from '../../../workspace/WorkspaceTypes'; import { blockExtraMethods } from './blockExtraMethods'; import { clearContext } from './clearContext'; import { evalCodeSaga } from './evalCode'; @@ -60,8 +60,8 @@ export function* runTestCase( prependFilePath, elevatedContext, execTime, - workspaceLocation, - EVAL_SILENT + EVAL_SILENT, + workspaceLocation ); // Block use of methods from privileged context using a randomly generated blocking key @@ -78,8 +78,8 @@ export function* runTestCase( valueFilePath, context, execTime, - workspaceLocation, - EVAL_SILENT + EVAL_SILENT, + workspaceLocation ); // Halt execution if the student's code in the editor results in an error @@ -103,8 +103,8 @@ export function* runTestCase( postpendFilePath, elevatedContext, execTime, - workspaceLocation, - EVAL_SILENT + EVAL_SILENT, + workspaceLocation ); yield* blockExtraMethods(elevatedContext, context, execTime, workspaceLocation, blockKey); } diff --git a/src/commons/sagas/WorkspaceSaga/index.ts b/src/commons/sagas/WorkspaceSaga/index.ts index 4334e5181b..ac1ba23bfb 100644 --- a/src/commons/sagas/WorkspaceSaga/index.ts +++ b/src/commons/sagas/WorkspaceSaga/index.ts @@ -35,6 +35,7 @@ import { } from '../../utils/notifications/NotificationsHelper'; import { showFullJSDisclaimer, showFullTSDisclaimer } from '../../utils/WarningDialogHelper'; import type { EditorTabState } from '../../workspace/WorkspaceTypes'; +import { selectWorkspace } from '../SafeEffects'; import { evalCodeSaga } from './helpers/evalCode'; import { evalEditorSaga } from './helpers/evalEditor'; import { runTestCase } from './helpers/runTestCase'; @@ -148,18 +149,15 @@ const WorkspaceSaga = combineSagaHandlers( }, promptAutocomplete: function* (action): any { const workspaceLocation = action.payload.workspaceLocation; + const { + activeEditorTabIndex, + editorTabs, + context, + externalLibrary: extLib, + programPrependValue: prepend + } = yield* selectWorkspace(workspaceLocation); - const context: Context = yield select( - (state: OverallState) => state.workspaces[workspaceLocation].context - ); - - const code: string = yield select((state: OverallState) => { - const prependCode = state.workspaces[workspaceLocation].programPrependValue; - // TODO: Hardcoded to make use of the first editor tab. Rewrite after editor tabs are added. - const editorCode = state.workspaces[workspaceLocation].editorTabs[0].value; - return [prependCode, editorCode] as [string, string]; - }); - const [prepend, editorValue] = code; + const editorValue = editorTabs[activeEditorTabIndex ?? 0].value; // Deal with prepended code let autocompleteCode; @@ -201,11 +199,6 @@ const WorkspaceSaga = combineSagaHandlers( } const builtinSuggestions = Documentation.builtins[chapterName] || []; - - const extLib = yield select( - (state: OverallState) => state.workspaces[workspaceLocation].externalLibrary - ); - const extLibSuggestions = Documentation.externalLibraries[extLib] || []; yield call( @@ -250,8 +243,8 @@ const WorkspaceSaga = combineSagaHandlers( codeFilePath, context, execTime, - workspaceLocation, - WorkspaceActions.evalRepl.type + WorkspaceActions.evalRepl.type, + workspaceLocation ); }, debuggerResume: function* (action) { @@ -281,8 +274,8 @@ const WorkspaceSaga = combineSagaHandlers( codeFilePath, context, execTime, - workspaceLocation, - InterpreterActions.debuggerResume.type + InterpreterActions.debuggerResume.type, + workspaceLocation ); }, debuggerReset: function* (action) { @@ -296,25 +289,24 @@ const WorkspaceSaga = combineSagaHandlers( context.runtime.break = false; yield put(actions.updateLastDebuggerResult(undefined, workspaceLocation)); }, - setEditorHighlightedLines: function* (action): any { + setEditorHighlightedLines: function* (action) { const newHighlightedLines = action.payload.newHighlightedLines; if (newHighlightedLines.length === 0) { - highlightClean(); + yield call(highlightClean); } else { try { - newHighlightedLines.forEach(([startRow, endRow]: [number, number]) => { + for (const [startRow, endRow] of newHighlightedLines) { for (let row = startRow; row <= endRow; row++) { - highlightLine(row); + yield call(highlightLine, row); } - }); + } } catch (e) { // Error most likely caused by trying to highlight the lines of the prelude // in CSE Machine. Can be ignored. } } - yield; }, - setEditorHighlightedLinesControl: function* (action): any { + setEditorHighlightedLinesControl: function* (action) { const newHighlightedLines = action.payload.newHighlightedLines; if (newHighlightedLines.length === 0) { yield call(highlightCleanForControl); From 9237602bf95f77917a92e85ccc7e74c31f38c1d5 Mon Sep 17 00:00:00 2001 From: "DESKTOP-G08HS3B\\Lee Yi" Date: Sat, 26 Apr 2025 20:25:06 -0400 Subject: [PATCH 08/14] Fix broken tsc --- src/commons/sagas/PlaygroundSaga.ts | 5 +- src/commons/sagas/SafeEffects.ts | 15 +++++- src/commons/sagas/__tests__/WorkspaceSaga.ts | 48 ++++++++++---------- 3 files changed, 39 insertions(+), 29 deletions(-) diff --git a/src/commons/sagas/PlaygroundSaga.ts b/src/commons/sagas/PlaygroundSaga.ts index 1384af5343..0ee2f9b107 100644 --- a/src/commons/sagas/PlaygroundSaga.ts +++ b/src/commons/sagas/PlaygroundSaga.ts @@ -19,7 +19,6 @@ import { SideContentType } from '../sideContent/SideContentTypes'; import Constants from '../utils/Constants'; import { showSuccessMessage, showWarningMessage } from '../utils/notifications/NotificationsHelper'; import WorkspaceActions from '../workspace/WorkspaceActions'; -import type { PlaygroundWorkspaceState } from '../workspace/WorkspaceTypes'; import { safeTakeEvery as takeEvery, selectWorkspace } from './SafeEffects'; export default function* PlaygroundSaga(): SagaIterator { @@ -77,9 +76,7 @@ export default function* PlaygroundSaga(): SagaIterator { const { context: { chapter: playgroundSourceChapter }, editorTabs - }: PlaygroundWorkspaceState = yield select( - (state: OverallState) => state.workspaces[workspaceLocation] - ); + } = yield* selectWorkspace('playground'); if (prevId === SideContentType.substVisualizer) { if (newId === SideContentType.mobileEditorRun) return; diff --git a/src/commons/sagas/SafeEffects.ts b/src/commons/sagas/SafeEffects.ts index 9379407193..cbf82404dd 100644 --- a/src/commons/sagas/SafeEffects.ts +++ b/src/commons/sagas/SafeEffects.ts @@ -5,6 +5,7 @@ import { type ForkEffect, type HelperWorkerParameters, select, + SelectEffect, takeEvery, takeLatest, takeLeading @@ -101,10 +102,22 @@ export function safeTakeLeading

(pattern, wrappedWorker, ...args); } -export function* selectWorkspace(workspaceLocation: T) { +export function selectWorkspace( + workspaceLocation: T, + func: (state: WorkspaceManagerState[T]) => U +): Generator; +export function selectWorkspace( + workspaceLocation: T +): Generator; +export function* selectWorkspace( + workspaceLocation: T, + f?: (state: WorkspaceManagerState[T]) => U +) { const workspace: WorkspaceManagerState[T] = yield select( (state: OverallState) => state.workspaces[workspaceLocation] ); + + if (f) return f(workspace); return workspace; } diff --git a/src/commons/sagas/__tests__/WorkspaceSaga.ts b/src/commons/sagas/__tests__/WorkspaceSaga.ts index d869df7816..cc45ecfa45 100644 --- a/src/commons/sagas/__tests__/WorkspaceSaga.ts +++ b/src/commons/sagas/__tests__/WorkspaceSaga.ts @@ -289,8 +289,8 @@ describe('DEBUG_RESUME', () => { editorValueFilePath, context, execTime, - workspaceLocation, - WorkspaceActions.evalEditor.type + WorkspaceActions.evalEditor.type, + workspaceLocation ) .withState(state) .silentRun(); @@ -826,8 +826,8 @@ describe('evalCode', () => { codeFilePath, context, execTime, - workspaceLocation, - actionType + actionType, + workspaceLocation ) .withState(state) .provide([ @@ -855,8 +855,8 @@ describe('evalCode', () => { codeFilePath, context, execTime, - workspaceLocation, - actionType + actionType, + workspaceLocation ) .withState(state) .provide([ @@ -885,8 +885,8 @@ describe('evalCode', () => { codeFilePath, context, execTime, - workspaceLocation, - actionType + actionType, + workspaceLocation ) .withState(state) .provide([ @@ -922,8 +922,8 @@ describe('evalCode', () => { codeFilePath, context, execTime, - workspaceLocation, - actionType + actionType, + workspaceLocation ) .withState(state) .call(runFilesInContext, files, codeFilePath, context, { @@ -948,8 +948,8 @@ describe('evalCode', () => { codeFilePath, context, execTime, - workspaceLocation, - WorkspaceActions.evalEditor.type + WorkspaceActions.evalEditor.type, + workspaceLocation ) .withState(state) .silentRun(); @@ -964,8 +964,8 @@ describe('evalCode', () => { codeFilePath, context, execTime, - workspaceLocation, - actionType + actionType, + workspaceLocation ) .withState(state) .provide([[call(resume, lastDebuggerResult), { status: 'finished', value }]]) @@ -983,8 +983,8 @@ describe('evalCode', () => { codeFilePath, context, execTime, - workspaceLocation, - actionType + actionType, + workspaceLocation ) .withState(state) .provide([[call(resume, lastDebuggerResult), { status: 'suspended-cse-eval' }]]) @@ -1003,8 +1003,8 @@ describe('evalCode', () => { codeFilePath, context, execTime, - workspaceLocation, - actionType + actionType, + workspaceLocation ) .withState(state) .call(resume, lastDebuggerResult) @@ -1021,8 +1021,8 @@ describe('evalCode', () => { codeFilePath, context, execTime, - workspaceLocation, - actionType + actionType, + workspaceLocation ) .withState(state) .provide({ @@ -1051,8 +1051,8 @@ describe('evalCode', () => { codeFilePath, context, execTime, - workspaceLocation, - actionType + actionType, + workspaceLocation ) .withState(state) .provide({ @@ -1080,8 +1080,8 @@ describe('evalCode', () => { codeFilePath, context, execTime, - workspaceLocation, - actionType + actionType, + workspaceLocation ) .withState(state) .provide([ From fed151c92f606b8a7a7ec23214f8a3f8cd9c544f Mon Sep 17 00:00:00 2001 From: "DESKTOP-G08HS3B\\Lee Yi" Date: Sun, 27 Apr 2025 19:12:25 -0400 Subject: [PATCH 09/14] Upgrade saga handler to allow using takeLatest or takeLeading --- src/commons/redux/utils.ts | 63 +++++++---- src/commons/sagas/PlaygroundSaga.ts | 128 ++++++++++++----------- src/commons/sagas/RemoteExecutionSaga.ts | 49 ++++----- src/commons/sagas/StoriesSaga.ts | 19 ++-- src/commons/utils/TypeHelper.ts | 17 ++- 5 files changed, 155 insertions(+), 121 deletions(-) diff --git a/src/commons/redux/utils.ts b/src/commons/redux/utils.ts index c372e9cc98..922b2e3150 100644 --- a/src/commons/redux/utils.ts +++ b/src/commons/redux/utils.ts @@ -1,12 +1,15 @@ import { - ActionCreatorWithOptionalPayload, - ActionCreatorWithoutPayload, - ActionCreatorWithPreparedPayload, + type ActionCreatorWithOptionalPayload, + type ActionCreatorWithoutPayload, + type ActionCreatorWithPreparedPayload, createAction } from '@reduxjs/toolkit'; import * as Sentry from '@sentry/browser'; -import { SagaIterator } from 'redux-saga'; -import { StrictEffect, takeEvery } from 'redux-saga/effects'; +import type { SagaIterator } from 'redux-saga'; +import { type StrictEffect, takeLatest, takeLeading } from 'redux-saga/effects'; + +import { safeTakeEvery } from '../sagas/SafeEffects'; +import { objectEntries } from '../utils/TypeHelper'; /** * Creates actions, given a base name and base actions @@ -39,6 +42,20 @@ export function createActions Generator>(saga: T) { + return function* (...args: Parameters) { + try { + return yield* saga(...args); + } catch (error) { + handleUncaughtError(error); + } + }; +} + +type SagaHandler< + T extends ActionCreatorWithPreparedPayload | ActionCreatorWithoutPayload +> = (action: ReturnType) => Generator; + export function combineSagaHandlers< TActions extends Record< string, @@ -47,17 +64,29 @@ export function combineSagaHandlers< >( actions: TActions, handlers: { - // TODO: Maybe this can be stricter? And remove the optional type after - // migration is fully done - [K in keyof TActions]?: (action: ReturnType) => SagaIterator; + // TODO: Maybe this can be stricter? And remove the optional type after migration is fully done + [K in keyof TActions]?: + | SagaHandler + | { takeLeading: SagaHandler } + | { takeLatest: SagaHandler }; }, others?: (takeEvery: typeof saferTakeEvery) => SagaIterator ): () => SagaIterator { - const sagaHandlers = Object.entries(handlers).map(([actionName, saga]) => - saferTakeEvery(actions[actionName], saga) - ); return function* (): SagaIterator { - yield* sagaHandlers; + for (const [actionName, saga] of objectEntries(handlers)) { + if (saga === undefined) { + continue; + } else if (typeof saga === 'function') { + yield safeTakeEvery(actions[actionName].type, saga); + } else if ('takeLeading' in saga) { + yield takeLeading(actions[actionName].type, wrapSaga(saga.takeLeading)); + } else if ('takeLatest' in saga) { + yield takeLatest(actions[actionName].type, wrapSaga(saga.takeLatest)); + } else { + throw new Error(`Unknown saga handler type for ${actionName as string}`); + } + } + if (others) { const obj = others(saferTakeEvery); while (true) { @@ -74,15 +103,7 @@ export function saferTakeEvery< | ActionCreatorWithOptionalPayload | ActionCreatorWithPreparedPayload >(actionPattern: Action, fn: (action: ReturnType) => Generator>) { - function* wrapper(action: ReturnType) { - try { - yield* fn(action); - } catch (error) { - handleUncaughtError(error); - } - } - - return takeEvery(actionPattern.type, wrapper); + return safeTakeEvery(actionPattern.type, fn); } function handleUncaughtError(error: any) { diff --git a/src/commons/sagas/PlaygroundSaga.ts b/src/commons/sagas/PlaygroundSaga.ts index 0ee2f9b107..9e4dedd821 100644 --- a/src/commons/sagas/PlaygroundSaga.ts +++ b/src/commons/sagas/PlaygroundSaga.ts @@ -1,8 +1,7 @@ -import { FSModule } from 'browserfs/dist/node/core/FS'; +import type { FSModule } from 'browserfs/dist/node/core/FS'; import { Chapter } from 'js-slang/dist/types'; import { compressToEncodedURIComponent } from 'lz-string'; import qs from 'query-string'; -import { SagaIterator } from 'redux-saga'; import { call, delay, put, race, select } from 'redux-saga/effects'; import CseMachine from 'src/features/cseMachine/CseMachine'; import { CseMachine as JavaCseMachine } from 'src/features/cseMachine/java/CseMachine'; @@ -14,21 +13,20 @@ import { type OverallState } from '../application/ApplicationTypes'; import { retrieveFilesInWorkspaceAsRecord } from '../fileSystem/utils'; +import { combineSagaHandlers } from '../redux/utils'; import { visitSideContent } from '../sideContent/SideContentActions'; import { SideContentType } from '../sideContent/SideContentTypes'; import Constants from '../utils/Constants'; import { showSuccessMessage, showWarningMessage } from '../utils/notifications/NotificationsHelper'; import WorkspaceActions from '../workspace/WorkspaceActions'; -import { safeTakeEvery as takeEvery, selectWorkspace } from './SafeEffects'; +import { selectWorkspace } from './SafeEffects'; -export default function* PlaygroundSaga(): SagaIterator { - yield takeEvery(PlaygroundActions.generateLzString.type, updateQueryString); - - yield takeEvery( - PlaygroundActions.shortenURL.type, - function* (action: ReturnType): any { +const PlaygroundSaga = combineSagaHandlers( + PlaygroundActions, + { + generateLzString: updateQueryString, + shortenURL: function* ({ payload: keyword }) { const queryString = yield select((state: OverallState) => state.playground.queryString); - const keyword = action.payload; const errorMsg = 'ERROR'; let resp, timeout; @@ -59,72 +57,76 @@ export default function* PlaygroundSaga(): SagaIterator { } yield put(PlaygroundActions.updateShortURL(Constants.urlShortenerBase + resp.url.keyword)); } - ); - - yield takeEvery( - visitSideContent.type, - function* ({ - payload: { newId, prevId, workspaceLocation } - }: ReturnType) { - if (workspaceLocation !== 'playground' || newId === prevId) return; + }, + function* (takeEvery) { + yield takeEvery( + visitSideContent, + function* ({ payload: { newId, prevId, workspaceLocation } }) { + if (workspaceLocation !== 'playground' || newId === prevId) return; + + // Do nothing when clicking the mobile 'Run' tab while on the stepper tab. + if ( + prevId === SideContentType.substVisualizer && + newId === SideContentType.mobileEditorRun + ) { + return; + } - // Do nothing when clicking the mobile 'Run' tab while on the stepper tab. - if (prevId === SideContentType.substVisualizer && newId === SideContentType.mobileEditorRun) { - return; - } + const { + context: { chapter: playgroundSourceChapter }, + editorTabs + } = yield* selectWorkspace('playground'); - const { - context: { chapter: playgroundSourceChapter }, - editorTabs - } = yield* selectWorkspace('playground'); + if (prevId === SideContentType.substVisualizer) { + if (newId === SideContentType.mobileEditorRun) return; + const hasBreakpoints = editorTabs.find(({ breakpoints }) => breakpoints.find(x => !!x)); - if (prevId === SideContentType.substVisualizer) { - if (newId === SideContentType.mobileEditorRun) return; - const hasBreakpoints = editorTabs.find(({ breakpoints }) => breakpoints.find(x => !!x)); + if (!hasBreakpoints) { + yield put(WorkspaceActions.toggleUsingSubst(false, workspaceLocation)); + yield put(WorkspaceActions.clearReplOutput(workspaceLocation)); + } + } - if (!hasBreakpoints) { - yield put(WorkspaceActions.toggleUsingSubst(false, workspaceLocation)); - yield put(WorkspaceActions.clearReplOutput(workspaceLocation)); + if (newId !== SideContentType.cseMachine) { + yield put(WorkspaceActions.toggleUsingCse(false, workspaceLocation)); + yield call([CseMachine, CseMachine.clearCse]); + yield call([JavaCseMachine, JavaCseMachine.clearCse]); + yield put(WorkspaceActions.updateCurrentStep(-1, workspaceLocation)); + yield put(WorkspaceActions.updateStepsTotal(0, workspaceLocation)); + yield put(WorkspaceActions.toggleUpdateCse(true, workspaceLocation)); + yield put(WorkspaceActions.setEditorHighlightedLines(workspaceLocation, 0, [])); } - } - if (newId !== SideContentType.cseMachine) { - yield put(WorkspaceActions.toggleUsingCse(false, workspaceLocation)); - yield call([CseMachine, CseMachine.clearCse]); - yield call([JavaCseMachine, JavaCseMachine.clearCse]); - yield put(WorkspaceActions.updateCurrentStep(-1, workspaceLocation)); - yield put(WorkspaceActions.updateStepsTotal(0, workspaceLocation)); - yield put(WorkspaceActions.toggleUpdateCse(true, workspaceLocation)); - yield put(WorkspaceActions.setEditorHighlightedLines(workspaceLocation, 0, [])); - } + if (playgroundSourceChapter === Chapter.FULL_JAVA && newId === SideContentType.cseMachine) { + yield put(WorkspaceActions.toggleUsingCse(true, workspaceLocation)); + } - if (playgroundSourceChapter === Chapter.FULL_JAVA && newId === SideContentType.cseMachine) { - yield put(WorkspaceActions.toggleUsingCse(true, workspaceLocation)); - } + if ( + isSourceLanguage(playgroundSourceChapter) && + (newId === SideContentType.substVisualizer || newId === SideContentType.cseMachine) + ) { + if (playgroundSourceChapter <= Chapter.SOURCE_2) { + yield put(WorkspaceActions.toggleUsingSubst(true, workspaceLocation)); + } else { + yield put(WorkspaceActions.toggleUsingCse(true, workspaceLocation)); + } + } - if ( - isSourceLanguage(playgroundSourceChapter) && - (newId === SideContentType.substVisualizer || newId === SideContentType.cseMachine) - ) { - if (playgroundSourceChapter <= Chapter.SOURCE_2) { - yield put(WorkspaceActions.toggleUsingSubst(true, workspaceLocation)); + if (newId === SideContentType.upload) { + yield put(WorkspaceActions.toggleUsingUpload(true, workspaceLocation)); } else { - yield put(WorkspaceActions.toggleUsingCse(true, workspaceLocation)); + yield put(WorkspaceActions.toggleUsingUpload(false, workspaceLocation)); } - } - if (newId === SideContentType.upload) { - yield put(WorkspaceActions.toggleUsingUpload(true, workspaceLocation)); - } else { - yield put(WorkspaceActions.toggleUsingUpload(false, workspaceLocation)); + if (isSchemeLanguage(playgroundSourceChapter) && newId === SideContentType.cseMachine) { + yield put(WorkspaceActions.toggleUsingCse(true, workspaceLocation)); + } } + ); + } +); - if (isSchemeLanguage(playgroundSourceChapter) && newId === SideContentType.cseMachine) { - yield put(WorkspaceActions.toggleUsingCse(true, workspaceLocation)); - } - } - ); -} +export default PlaygroundSaga; function* updateQueryString() { const fileSystem: FSModule = yield select( diff --git a/src/commons/sagas/RemoteExecutionSaga.ts b/src/commons/sagas/RemoteExecutionSaga.ts index 48000bcf81..e5046bc6ae 100644 --- a/src/commons/sagas/RemoteExecutionSaga.ts +++ b/src/commons/sagas/RemoteExecutionSaga.ts @@ -36,32 +36,33 @@ const dummyLocation = { // TODO: Refactor and combine in a future commit const sagaActions = { ...RemoteExecutionActions, ...InterpreterActions }; const RemoteExecutionSaga = combineSagaHandlers(sagaActions, { - // TODO: Should be `takeLatest`, not `takeEvery` - remoteExecFetchDevices: function* () { - const [tokens, session]: [any, DeviceSession | undefined] = yield select( - (state: OverallState) => [ - { - accessToken: state.session.accessToken, - refreshToken: state.session.refreshToken - }, - state.session.remoteExecutionSession - ] - ); - const devices: Device[] = yield call(fetchDevices, tokens); + remoteExecFetchDevices: { + takeLatest: function* () { + const [tokens, session]: [any, DeviceSession | undefined] = yield select( + (state: OverallState) => [ + { + accessToken: state.session.accessToken, + refreshToken: state.session.refreshToken + }, + state.session.remoteExecutionSession + ] + ); + const devices: Device[] = yield call(fetchDevices, tokens); - yield put(actions.remoteExecUpdateDevices(devices)); + yield put(actions.remoteExecUpdateDevices(devices)); - if (!session) { - return; - } - const updatedDevice = devices.find(({ id }) => id === session.device.id); - if (updatedDevice) { - yield put( - actions.remoteExecUpdateSession({ - ...session, - device: updatedDevice - }) - ); + if (!session) { + return; + } + const updatedDevice = devices.find(({ id }) => id === session.device.id); + if (updatedDevice) { + yield put( + actions.remoteExecUpdateSession({ + ...session, + device: updatedDevice + }) + ); + } } }, remoteExecConnect: function* (action): any { diff --git a/src/commons/sagas/StoriesSaga.ts b/src/commons/sagas/StoriesSaga.ts index f4c21d5ba8..e1aa6c3738 100644 --- a/src/commons/sagas/StoriesSaga.ts +++ b/src/commons/sagas/StoriesSaga.ts @@ -28,15 +28,16 @@ import { evalCodeSaga } from './WorkspaceSaga/helpers/evalCode'; // TODO: Refactor and combine in a future commit const sagaActions = { ...StoriesActions, ...SessionActions }; const StoriesSaga = combineSagaHandlers(sagaActions, { - // TODO: This should be using `takeLatest`, not `takeEvery` - getStoriesList: function* () { - const tokens: Tokens = yield selectTokens(); - const allStories: StoryListView[] = yield call(async () => { - const resp = await getStories(tokens); - return resp ?? []; - }); - - yield put(actions.updateStoriesList(allStories)); + getStoriesList: { + takeLatest: function* () { + const tokens: Tokens = yield selectTokens(); + const allStories: StoryListView[] = yield call(async () => { + const resp = await getStories(tokens); + return resp ?? []; + }); + + yield put(actions.updateStoriesList(allStories)); + } }, setCurrentStoryId: function* (action) { const tokens: Tokens = yield selectTokens(); diff --git a/src/commons/utils/TypeHelper.ts b/src/commons/utils/TypeHelper.ts index 56e62d4b1a..b4b2a26e65 100644 --- a/src/commons/utils/TypeHelper.ts +++ b/src/commons/utils/TypeHelper.ts @@ -157,11 +157,20 @@ export function objectValues(obj: Record) { return Object.values(obj) as T[]; } +/** + * Utility type for getting the key-value tuple types + * of a record + */ +type DeconstructRecord> = Exclude< + { + [K in keyof T]: [K, T[K]]; + }[keyof T], + undefined +>[]; + /** * Type safe `Object.entries` */ -export function objectEntries( - obj: Partial> -): [K, V][] { - return Object.entries(obj) as [K, V][]; +export function objectEntries>(obj: T) { + return Object.entries(obj) as DeconstructRecord; } From 5fbeb88780fa56c790c5d932edf436dee73a9df4 Mon Sep 17 00:00:00 2001 From: "DESKTOP-G08HS3B\\Lee Yi" Date: Mon, 28 Apr 2025 00:40:06 -0400 Subject: [PATCH 10/14] Modify sagas to use new combineSagaHandlers function --- src/commons/redux/utils.ts | 86 +- src/commons/sagas/AchievementSaga.ts | 68 +- src/commons/sagas/BackendSaga.ts | 141 ++-- src/commons/sagas/GitHubPersistenceSaga.ts | 32 +- src/commons/sagas/LoginSaga.ts | 43 +- src/commons/sagas/PersistenceSaga.tsx | 31 +- src/commons/sagas/PlaygroundSaga.ts | 189 ++--- src/commons/sagas/RemoteExecutionSaga.ts | 33 +- src/commons/sagas/SafeEffects.ts | 10 + src/commons/sagas/SideContentSaga.ts | 14 +- src/commons/sagas/StoriesSaga.ts | 26 +- .../sagas/WorkspaceSaga/helpers/evalEditor.ts | 41 +- .../WorkspaceSaga/helpers/runTestCase.ts | 29 +- src/commons/sagas/WorkspaceSaga/index.ts | 769 +++++++++--------- src/commons/utils/JsSlangHelper.ts | 10 +- src/commons/utils/TypeHelper.ts | 7 + .../achievement/AchievementActions.ts | 18 +- 17 files changed, 739 insertions(+), 808 deletions(-) diff --git a/src/commons/redux/utils.ts b/src/commons/redux/utils.ts index 922b2e3150..b68e25f0f1 100644 --- a/src/commons/redux/utils.ts +++ b/src/commons/redux/utils.ts @@ -4,12 +4,12 @@ import { type ActionCreatorWithPreparedPayload, createAction } from '@reduxjs/toolkit'; -import * as Sentry from '@sentry/browser'; import type { SagaIterator } from 'redux-saga'; -import { type StrictEffect, takeLatest, takeLeading } from 'redux-saga/effects'; +import { type StrictEffect, takeEvery, takeLatest, takeLeading } from 'redux-saga/effects'; -import { safeTakeEvery } from '../sagas/SafeEffects'; -import { objectEntries } from '../utils/TypeHelper'; +import { safeTakeEvery, wrapSaga } from '../sagas/SafeEffects'; +import type { SourceActionType } from '../utils/ActionsHelper'; +import { ActionTypeToCreator, objectEntries } from '../utils/TypeHelper'; /** * Creates actions, given a base name and base actions @@ -42,57 +42,36 @@ export function createActions Generator>(saga: T) { - return function* (...args: Parameters) { - try { - return yield* saga(...args); - } catch (error) { - handleUncaughtError(error); - } - }; -} +type SagaHandler = ( + action: ReturnType> +) => Generator; -type SagaHandler< - T extends ActionCreatorWithPreparedPayload | ActionCreatorWithoutPayload -> = (action: ReturnType) => Generator; +type SagaHandlers = { + [K in SourceActionType['type']]?: + | SagaHandler + | Partial>>; +}; -export function combineSagaHandlers< - TActions extends Record< - string, - ActionCreatorWithPreparedPayload | ActionCreatorWithoutPayload - > ->( - actions: TActions, - handlers: { - // TODO: Maybe this can be stricter? And remove the optional type after migration is fully done - [K in keyof TActions]?: - | SagaHandler - | { takeLeading: SagaHandler } - | { takeLatest: SagaHandler }; - }, - others?: (takeEvery: typeof saferTakeEvery) => SagaIterator -): () => SagaIterator { +export function combineSagaHandlers(handlers: SagaHandlers) { return function* (): SagaIterator { - for (const [actionName, saga] of objectEntries(handlers)) { + for (const [actionName, saga] of objectEntries(handlers as SagaHandlers)) { if (saga === undefined) { continue; } else if (typeof saga === 'function') { - yield safeTakeEvery(actions[actionName].type, saga); - } else if ('takeLeading' in saga) { - yield takeLeading(actions[actionName].type, wrapSaga(saga.takeLeading)); - } else if ('takeLatest' in saga) { - yield takeLatest(actions[actionName].type, wrapSaga(saga.takeLatest)); - } else { - throw new Error(`Unknown saga handler type for ${actionName as string}`); + yield takeEvery(actionName, wrapSaga(saga)); + continue; + } + + if (saga.takeEvery) { + yield takeEvery(actionName, wrapSaga(saga.takeEvery)); + } + + if (saga.takeLeading) { + yield takeLeading(actionName, wrapSaga(saga.takeLeading)); } - } - if (others) { - const obj = others(saferTakeEvery); - while (true) { - const { done, value } = obj.next(); - if (done) break; - yield value; + if (saga.takeLatest) { + yield takeLatest(actionName, wrapSaga(saga.takeLatest)); } } }; @@ -105,16 +84,3 @@ export function saferTakeEvery< >(actionPattern: Action, fn: (action: ReturnType) => Generator>) { return safeTakeEvery(actionPattern.type, fn); } - -function handleUncaughtError(error: any) { - if (process.env.NODE_ENV === 'development') { - // react-error-overlay is a "special" package that's automatically included - // in development mode by CRA - - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - import('react-error-overlay').then(reo => reo.reportRuntimeError(error)); - } - Sentry.captureException(error); - console.error(error); -} diff --git a/src/commons/sagas/AchievementSaga.ts b/src/commons/sagas/AchievementSaga.ts index aa0b940f28..f988eeb019 100644 --- a/src/commons/sagas/AchievementSaga.ts +++ b/src/commons/sagas/AchievementSaga.ts @@ -1,13 +1,15 @@ import { call, delay, put, select } from 'redux-saga/effects'; import AchievementActions from 'src/features/achievement/AchievementActions'; -import { AchievementGoal, EventType } from '../../features/achievement/AchievementTypes'; +import { type AchievementGoal, EventType } from '../../features/achievement/AchievementTypes'; import { updateGoalProcessed } from '../achievement/AchievementManualEditor'; import AchievementInferencer from '../achievement/utils/AchievementInferencer'; import { goalIncludesEvents, incrementCount } from '../achievement/utils/EventHandler'; -import { OverallState } from '../application/ApplicationTypes'; -import { Tokens } from '../application/types/SessionTypes'; +import type { OverallState } from '../application/ApplicationTypes'; +import type { Tokens } from '../application/types/SessionTypes'; import { combineSagaHandlers } from '../redux/utils'; +import SideContentActions from '../sideContent/SideContentActions'; +import { getLocation } from '../sideContent/SideContentHelper'; import { SideContentType } from '../sideContent/SideContentTypes'; import { actions } from '../utils/ActionsHelper'; import Constants from '../utils/Constants'; @@ -26,19 +28,17 @@ import { updateOwnGoalProgress } from './RequestsSaga'; -const AchievementSaga = combineSagaHandlers(AchievementActions, { - bulkUpdateAchievements: function* (action) { +const AchievementSaga = combineSagaHandlers({ + [AchievementActions.bulkUpdateAchievements.type]: function* (action) { const tokens: Tokens = yield selectTokens(); - const achievements = action.payload; - const resp = yield call(bulkUpdateAchievements, achievements, tokens); if (!resp) { return; } }, - bulkUpdateGoals: function* (action) { + [AchievementActions.bulkUpdateGoals.type]: function* (action) { const tokens: Tokens = yield selectTokens(); const goals = action.payload; @@ -49,7 +49,7 @@ const AchievementSaga = combineSagaHandlers(AchievementActions, { return; } }, - getAchievements: function* () { + [AchievementActions.getAchievements.type]: function* () { const tokens: Tokens = yield selectTokens(); const achievements = yield call(getAchievements, tokens); @@ -58,7 +58,7 @@ const AchievementSaga = combineSagaHandlers(AchievementActions, { yield put(actions.saveAchievements(achievements)); } }, - getGoals: function* (action) { + [AchievementActions.getGoals.type]: function* (action) { const tokens: Tokens = yield selectTokens(); const studentCourseRegId = action.payload; @@ -69,7 +69,7 @@ const AchievementSaga = combineSagaHandlers(AchievementActions, { yield put(actions.saveGoals(goals)); } }, - getOwnGoals: function* (action) { + [AchievementActions.getOwnGoals.type]: function* () { const tokens: Tokens = yield selectTokens(); const goals = yield call(getOwnGoals, tokens); @@ -78,7 +78,7 @@ const AchievementSaga = combineSagaHandlers(AchievementActions, { yield put(actions.saveGoals(goals)); } }, - getUsers: function* (action) { + [AchievementActions.getUsers.type]: function* () { const tokens: Tokens = yield selectTokens(); const users = yield call(getAllUsers, tokens); @@ -87,7 +87,7 @@ const AchievementSaga = combineSagaHandlers(AchievementActions, { yield put(actions.saveUsers(users)); } }, - removeAchievement: function* (action) { + [AchievementActions.removeAchievement.type]: function* (action) { const tokens: Tokens = yield selectTokens(); const achievement = action.payload; @@ -98,7 +98,7 @@ const AchievementSaga = combineSagaHandlers(AchievementActions, { return; } }, - removeGoal: function* (action) { + [AchievementActions.removeGoal.type]: function* (action) { const tokens: Tokens = yield selectTokens(); const definition = action.payload; @@ -109,7 +109,7 @@ const AchievementSaga = combineSagaHandlers(AchievementActions, { return; } }, - updateOwnGoalProgress: function* (action) { + [AchievementActions.updateOwnGoalProgress.type]: function* (action) { const tokens: Tokens = yield selectTokens(); const progress = action.payload; @@ -120,7 +120,7 @@ const AchievementSaga = combineSagaHandlers(AchievementActions, { return; } }, - updateGoalProgress: function* (action) { + [AchievementActions.updateGoalProgress.type]: function* (action) { const tokens: Tokens = yield selectTokens(); const { studentCourseRegId, progress } = action.payload; @@ -132,10 +132,10 @@ const AchievementSaga = combineSagaHandlers(AchievementActions, { } if (resp.ok) { yield put(actions.getGoals(studentCourseRegId)); - updateGoalProcessed(); + yield call(updateGoalProcessed); } }, - addEvent: function* (action) { + [AchievementActions.addEvent.type]: function* ({ payload: { eventNames, workspaceLocation } }) { let loggedEvents: EventType[][] = []; let timeoutSet: boolean = false; const updateInterval = 3000; @@ -144,25 +144,25 @@ const AchievementSaga = combineSagaHandlers(AchievementActions, { const enableAchievements = yield select( (state: OverallState) => state.session.enableAchievements ); - if (action.payload.find(e => e === EventType.ERROR)) { - // TODO update this to work with new side content system - // Flash the home icon if there is an error and the user is in the CSE machine or subst viz tab - const introIcon = document.getElementById(SideContentType.introduction + '-icon'); - const cseTab = document.getElementById( - 'bp5-tab-panel_side-content-tabs_' + SideContentType.cseMachine - ); - const substTab = document.getElementById( - 'bp5-tab-panel_side-content-tabs_' + SideContentType.substVisualizer - ); + if (workspaceLocation !== undefined && eventNames.find(e => e === EventType.ERROR)) { + const selectedTab: SideContentType | undefined = yield select((state: OverallState) => { + const [loc, storyEnv] = getLocation(workspaceLocation); + return loc === 'stories' + ? state.sideContent.stories[storyEnv].selectedTab + : state.sideContent[loc].selectedTab; + }); + if ( - (cseTab && cseTab.ariaHidden === 'false') || - (substTab && substTab.ariaHidden === 'false') + selectedTab === SideContentType.cseMachine || + selectedTab === SideContentType.substVisualizer ) { - introIcon?.classList.add('side-content-tab-alert-error'); + yield put( + SideContentActions.beginAlertSideContent(SideContentType.introduction, workspaceLocation) + ); } } if (role && enableAchievements && !Constants.playgroundOnly) { - loggedEvents.push(action.payload); + loggedEvents.push(eventNames); if (!timeoutSet && role) { // make sure that only one action every interval will handleEvent @@ -175,7 +175,7 @@ const AchievementSaga = combineSagaHandlers(AchievementActions, { } } }, - handleEvent: function* (action) { + [AchievementActions.handleEvent.type]: function* (action) { const tokens: Tokens = yield selectTokens(); // get the most recent list of achievements @@ -219,7 +219,7 @@ const AchievementSaga = combineSagaHandlers(AchievementActions, { } } }, - getUserAssessmentOverviews: function* (action) { + [AchievementActions.getUserAssessmentOverviews.type]: function* (action) { const tokens: Tokens = yield selectTokens(); const assessmentOverviews = yield call(getUserAssessmentOverviews, action.payload, tokens); diff --git a/src/commons/sagas/BackendSaga.ts b/src/commons/sagas/BackendSaga.ts index 22fd478e99..abca6e3d09 100644 --- a/src/commons/sagas/BackendSaga.ts +++ b/src/commons/sagas/BackendSaga.ts @@ -1,7 +1,7 @@ /*eslint no-eval: "error"*/ /*eslint-env browser*/ import _ from 'lodash'; -import { SagaIterator } from 'redux-saga'; +import type { SagaIterator } from 'redux-saga'; import { all, call, fork, put, select } from 'redux-saga/effects'; import AcademyActions from 'src/features/academy/AcademyActions'; import DashboardActions from 'src/features/dashboard/DashboardActions'; @@ -9,23 +9,23 @@ import GroundControlActions from 'src/features/groundControl/GroundControlAction import SourcecastActions from 'src/features/sourceRecorder/sourcecast/SourcecastActions'; import SourceRecorderActions from 'src/features/sourceRecorder/SourceRecorderActions'; import { postNewStoriesUsers } from 'src/features/stories/storiesComponents/BackendAccess'; -import { UsernameRoleGroup } from 'src/pages/academy/adminPanel/subcomponents/AddUserPanel'; +import type { UsernameRoleGroup } from 'src/pages/academy/adminPanel/subcomponents/AddUserPanel'; -import { GradingSummary } from '../../features/dashboard/DashboardTypes'; +import type { GradingSummary } from '../../features/dashboard/DashboardTypes'; import { - GradingOverview, - GradingOverviews, - GradingQuery, - GradingQuestion, + type GradingOverview, + type GradingOverviews, + type GradingQuery, + type GradingQuestion, SortStates } from '../../features/grading/GradingTypes'; -import { SourcecastData } from '../../features/sourceRecorder/SourceRecorderTypes'; +import type { SourcecastData } from '../../features/sourceRecorder/SourceRecorderTypes'; import SourcereelActions from '../../features/sourceRecorder/sourcereel/SourcereelActions'; -import { TeamFormationOverview } from '../../features/teamFormation/TeamFormationTypes'; +import type { TeamFormationOverview } from '../../features/teamFormation/TeamFormationTypes'; import SessionActions from '../application/actions/SessionActions'; -import { OverallState, Role } from '../application/ApplicationTypes'; -import { RouterState } from '../application/types/CommonsTypes'; -import { +import { type OverallState, Role } from '../application/ApplicationTypes'; +import type { RouterState } from '../application/types/CommonsTypes'; +import type { AdminPanelCourseRegistration, CourseConfiguration, CourseRegistration, @@ -34,14 +34,14 @@ import { User } from '../application/types/SessionTypes'; import { - Assessment, - AssessmentConfiguration, - AssessmentOverview, + type Assessment, + type AssessmentConfiguration, + type AssessmentOverview, AssessmentStatuses, ProgressStatuses, - Question + type Question } from '../assessment/AssessmentTypes'; -import { +import type { Notification, NotificationFilterFunction } from '../notificationBadge/NotificationBadgeTypes'; @@ -50,7 +50,7 @@ import { actions } from '../utils/ActionsHelper'; import { computeFrontendRedirectUri, getClientId, getDefaultProvider } from '../utils/AuthHelper'; import { showSuccessMessage, showWarningMessage } from '../utils/notifications/NotificationsHelper'; import WorkspaceActions from '../workspace/WorkspaceActions'; -import { WorkspaceLocation } from '../workspace/WorkspaceTypes'; +import type { WorkspaceLocation } from '../workspace/WorkspaceTypes'; import { checkAnswerLastModifiedAt, deleteAssessment, @@ -118,17 +118,8 @@ export function* routerNavigate(path: string) { return router?.navigate(path); } -// TODO: Refactor and combine in a future commit -const sagaActions = { - ...SessionActions, - ...SourcereelActions, - ...AcademyActions, - ...SourcecastActions, - ...SourceRecorderActions, - ...WorkspaceActions -}; -const newBackendSagaOne = combineSagaHandlers(sagaActions, { - fetchAuth: function* (action): any { +const newBackendSagaOne = combineSagaHandlers({ + [SessionActions.fetchAuth.type]: function* (action): any { const { code, providerId: payloadProviderId } = action.payload; const providerId = payloadProviderId || (getDefaultProvider() || [null])[0]; @@ -157,14 +148,14 @@ const newBackendSagaOne = combineSagaHandlers(sagaActions, { * - Thus handling navigation in allows us to directly access the latest router via `useNavigate`. */ }, - handleSamlRedirect: function* (action) { + [SessionActions.handleSamlRedirect.type]: function* (action) { const { jwtCookie } = action.payload; const tokens = _.mapKeys(JSON.parse(jwtCookie), (v, k) => _.camelCase(k)) as Tokens; yield put(actions.setTokens(tokens)); yield put(actions.fetchUserAndCourse()); }, - fetchUserAndCourse: function* (action) { + [SessionActions.fetchUserAndCourse.type]: function* (action) { const tokens: Tokens = yield selectTokens(); // Note: courseRegistration, courseConfiguration and assessmentConfigurations @@ -206,7 +197,7 @@ const newBackendSagaOne = combineSagaHandlers(sagaActions, { } } }, - fetchCourseConfig: function* () { + [SessionActions.fetchCourseConfig.type]: function* () { const tokens: Tokens = yield selectTokens(); const { config }: { config: CourseConfiguration | null } = yield call(getCourseConfig, tokens); if (config) { @@ -220,7 +211,7 @@ const newBackendSagaOne = combineSagaHandlers(sagaActions, { } } }, - fetchAssessmentOverviews: function* () { + [SessionActions.fetchAssessmentOverviews.type]: function* () { const tokens: Tokens = yield selectTokens(); const assessmentOverviews: AssessmentOverview[] | null = yield call( @@ -231,7 +222,7 @@ const newBackendSagaOne = combineSagaHandlers(sagaActions, { yield put(actions.updateAssessmentOverviews(assessmentOverviews)); } }, - fetchTotalXp: function* () { + [SessionActions.fetchTotalXp.type]: function* () { const tokens: Tokens = yield selectTokens(); const res: { totalXp: number } = yield call(getTotalXp, tokens); @@ -239,7 +230,7 @@ const newBackendSagaOne = combineSagaHandlers(sagaActions, { yield put(actions.updateTotalXp(res.totalXp)); } }, - fetchTotalXpAdmin: function* (action) { + [SessionActions.fetchTotalXpAdmin.type]: function* (action) { const tokens: Tokens = yield selectTokens(); const courseRegId = action.payload; @@ -249,7 +240,7 @@ const newBackendSagaOne = combineSagaHandlers(sagaActions, { yield put(actions.updateTotalXp(res.totalXp)); } }, - fetchAssessment: function* (action) { + [SessionActions.fetchAssessment.type]: function* (action) { const tokens: Tokens = yield selectTokens(); const { assessmentId, assessmentPassword } = action.payload; @@ -265,7 +256,7 @@ const newBackendSagaOne = combineSagaHandlers(sagaActions, { yield put(actions.updateAssessment(assessment)); } }, - fetchAssessmentAdmin: function* (action) { + [SessionActions.fetchAssessmentAdmin.type]: function* (action) { const tokens: Tokens = yield selectTokens(); const { assessmentId, courseRegId } = action.payload; @@ -280,7 +271,7 @@ const newBackendSagaOne = combineSagaHandlers(sagaActions, { yield put(actions.updateAssessment(assessment)); } }, - submitAnswer: function* (action) { + [SessionActions.submitAnswer.type]: function* (action) { const tokens: Tokens = yield selectTokens(); const questionId = action.payload.id; const answer = action.payload.answer; @@ -313,7 +304,7 @@ const newBackendSagaOne = combineSagaHandlers(sagaActions, { yield put(actions.updateAssessment(newAssessment)); return yield put(actions.updateHasUnsavedChanges('assessment' as WorkspaceLocation, false)); }, - checkAnswerLastModifiedAt: function* (action) { + [SessionActions.checkAnswerLastModifiedAt.type]: function* (action) { const tokens: Tokens = yield selectTokens(); const questionId = action.payload.id; const lastModifiedAt = action.payload.lastModifiedAt; @@ -327,7 +318,7 @@ const newBackendSagaOne = combineSagaHandlers(sagaActions, { ); saveAnswer(resp); }, - submitAssessment: function* (action) { + [SessionActions.submitAssessment.type]: function* (action) { const tokens: Tokens = yield selectTokens(); const assessmentId = action.payload; @@ -351,7 +342,7 @@ const newBackendSagaOne = combineSagaHandlers(sagaActions, { return yield put(actions.updateAssessmentOverviews(newOverviews)); }, - fetchGradingOverviews: function* (action) { + [SessionActions.fetchGradingOverviews.type]: function* (action) { const tokens: Tokens = yield selectTokens(); const role: Role = yield select((state: OverallState) => state.session.role!); @@ -391,7 +382,7 @@ const newBackendSagaOne = combineSagaHandlers(sagaActions, { yield put(actions.updateGradingOverviews(gradingOverviews)); } }, - fetchTeamFormationOverview: function* (action) { + [SessionActions.fetchTeamFormationOverview.type]: function* (action) { const tokens: Tokens = yield selectTokens(); const { assessmentId } = action.payload; @@ -404,7 +395,7 @@ const newBackendSagaOne = combineSagaHandlers(sagaActions, { yield put(actions.updateTeamFormationOverview(teamFormationOverview)); } }, - fetchTeamFormationOverviews: function* () { + [SessionActions.fetchTeamFormationOverviews.type]: function* () { const tokens: Tokens = yield selectTokens(); const role: Role = yield select((state: OverallState) => state.session.role!); @@ -420,7 +411,7 @@ const newBackendSagaOne = combineSagaHandlers(sagaActions, { yield put(actions.updateTeamFormationOverviews(teamFormationOverviews)); } }, - fetchStudents: function* () { + [SessionActions.fetchStudents.type]: function* () { const tokens: Tokens = yield selectTokens(); const role: Role = yield select((state: OverallState) => state.session.role!); if (role === Role.Student) { @@ -431,7 +422,7 @@ const newBackendSagaOne = combineSagaHandlers(sagaActions, { yield put(actions.updateStudents(students)); } }, - createTeam: function* (action) { + [SessionActions.createTeam.type]: function* (action) { const tokens: Tokens = yield selectTokens(); const { assessment, teams } = action.payload; @@ -451,7 +442,7 @@ const newBackendSagaOne = combineSagaHandlers(sagaActions, { return yield call(showWarningMessage, resp.statusText); } }, - bulkUploadTeam: function* (action) { + [SessionActions.bulkUploadTeam.type]: function* (action) { const tokens: Tokens = yield selectTokens(); const { assessment, file, students } = action.payload; @@ -475,7 +466,7 @@ const newBackendSagaOne = combineSagaHandlers(sagaActions, { yield put(actions.updateTeamFormationOverviews(teamFormationOverviews)); } }, - updateTeam: function* (action) { + [SessionActions.updateTeam.type]: function* (action) { const tokens: Tokens = yield selectTokens(); const { teamId, assessment, teams } = action.payload; const resp: Response | null = yield call(putTeams, assessment.id, teamId, teams, tokens); @@ -492,7 +483,7 @@ const newBackendSagaOne = combineSagaHandlers(sagaActions, { yield put(actions.updateTeamFormationOverviews(teamFormationOverviews)); } }, - deleteTeam: function* (action) { + [SessionActions.deleteTeam.type]: function* (action) { const tokens: Tokens = yield selectTokens(); const { teamId } = action.payload; @@ -510,7 +501,7 @@ const newBackendSagaOne = combineSagaHandlers(sagaActions, { yield put(actions.updateTeamFormationOverviews(teamFormationOverviews)); } }, - fetchGrading: function* (action) { + [SessionActions.fetchGrading.type]: function* (action) { const tokens: Tokens = yield selectTokens(); const id = action.payload; @@ -522,7 +513,7 @@ const newBackendSagaOne = combineSagaHandlers(sagaActions, { /** * Unsubmits the submission and updates the grading overviews of the new status. */ - unsubmitSubmission: function* (action) { + [SessionActions.unsubmitSubmission.type]: function* (action) { const tokens: Tokens = yield selectTokens(); const { submissionId } = action.payload; @@ -548,7 +539,7 @@ const newBackendSagaOne = combineSagaHandlers(sagaActions, { yield call(showSuccessMessage, 'Unsubmit successful', 1000); yield put(actions.updateGradingOverviews({ count: totalPossibleEntries, data: newOverviews })); }, - publishGrading: function* (action) { + [SessionActions.publishGrading.type]: function* (action) { const tokens: Tokens = yield selectTokens(); const { submissionId } = action.payload; @@ -574,7 +565,7 @@ const newBackendSagaOne = combineSagaHandlers(sagaActions, { yield call(showSuccessMessage, 'Publish grading successful', 1000); yield put(actions.updateGradingOverviews({ count: totalPossibleEntries, data: newOverviews })); }, - unpublishGrading: function* (action) { + [SessionActions.unpublishGrading.type]: function* (action) { const tokens: Tokens = yield selectTokens(); const { submissionId } = action.payload; @@ -600,8 +591,8 @@ const newBackendSagaOne = combineSagaHandlers(sagaActions, { yield call(showSuccessMessage, 'Unpublish grading successful', 1000); yield put(actions.updateGradingOverviews({ count: totalPossibleEntries, data: newOverviews })); }, - submitGrading: sendGrade, - submitGradingAndContinue: sendGradeAndContinue + [SessionActions.submitGrading.type]: sendGrade, + [SessionActions.submitGradingAndContinue.type]: sendGradeAndContinue }); function* sendGrade( @@ -672,15 +663,15 @@ function* sendGradeAndContinue(action: ReturnType state.session.role!); if (role === Role.Student) { return yield call(showWarningMessage, 'Only staff can delete sourcecasts.'); @@ -748,7 +739,7 @@ const newBackendSagaTwo = combineSagaHandlers(sagaActions, { yield call(showSuccessMessage, 'Deleted successfully!', 1000); }, - fetchSourcecastIndex: function* (action) { + [SourcecastActions.fetchSourcecastIndex.type]: function* (action) { const tokens: Tokens = yield selectTokens(); const sourcecastIndex: SourcecastData[] | null = yield call(getSourcecastIndex, tokens); @@ -756,7 +747,7 @@ const newBackendSagaTwo = combineSagaHandlers(sagaActions, { yield put(actions.updateSourcecastIndex(sourcecastIndex, action.payload.workspaceLocation)); } }, - saveSourcecastData: function* (action) { + [SourceRecorderActions.saveSourcecastData.type]: function* (action) { const [role, courseId]: [Role, number | undefined] = yield select((state: OverallState) => [ state.session.role!, state.session.courseId @@ -783,7 +774,7 @@ const newBackendSagaTwo = combineSagaHandlers(sagaActions, { yield call(showSuccessMessage, 'Saved successfully!', 1000); yield routerNavigate(`/courses/${courseId}/sourcecast`); }, - changeSublanguage: function* (action) { + [WorkspaceActions.changeSublanguage.type]: function* (action) { const tokens: Tokens = yield selectTokens(); const { sublang } = action.payload; @@ -803,7 +794,7 @@ const newBackendSagaTwo = combineSagaHandlers(sagaActions, { ); yield call(showSuccessMessage, 'Updated successfully!', 1000); }, - updateLatestViewedCourse: function* (action) { + [SessionActions.updateLatestViewedCourse.type]: function* (action) { const tokens: Tokens = yield selectTokens(); const { courseId } = action.payload; @@ -840,7 +831,7 @@ const newBackendSagaTwo = combineSagaHandlers(sagaActions, { yield call(showSuccessMessage, `Switched to ${courseConfiguration.courseName}!`, 5000); }, - updateCourseConfig: function* (action) { + [SessionActions.updateCourseConfig.type]: function* (action) { const tokens: Tokens = yield selectTokens(); const courseConfig: UpdateCourseConfiguration = action.payload; @@ -859,7 +850,7 @@ const newBackendSagaTwo = combineSagaHandlers(sagaActions, { yield put(actions.setCourseConfiguration(courseConfig)); yield call(showSuccessMessage, 'Updated successfully!', 1000); }, - fetchAssessmentConfigs: function* () { + [SessionActions.fetchAssessmentConfigs.type]: function* () { const tokens: Tokens = yield selectTokens(); const assessmentConfigs: AssessmentConfiguration[] | null = yield call( @@ -870,7 +861,7 @@ const newBackendSagaTwo = combineSagaHandlers(sagaActions, { yield put(actions.setAssessmentConfigurations(assessmentConfigs)); } }, - updateAssessmentConfigs: function* (action) { + [SessionActions.updateAssessmentConfigs.type]: function* (action) { const tokens: Tokens = yield selectTokens(); const assessmentConfigs: AssessmentConfiguration[] = action.payload; @@ -889,7 +880,7 @@ const newBackendSagaTwo = combineSagaHandlers(sagaActions, { } yield call(showSuccessMessage, 'Updated successfully!', 1000); }, - deleteAssessmentConfig: function* (action) { + [SessionActions.deleteAssessmentConfig.type]: function* (action) { const tokens: Tokens = yield selectTokens(); const assessmentConfig: AssessmentConfiguration = action.payload; @@ -898,7 +889,7 @@ const newBackendSagaTwo = combineSagaHandlers(sagaActions, { return yield handleResponseError(resp); } }, - fetchAdminPanelCourseRegistrations: function* (action) { + [SessionActions.fetchAdminPanelCourseRegistrations.type]: function* (action) { const tokens: Tokens = yield selectTokens(); const courseRegistrations: AdminPanelCourseRegistration[] | null = yield call( @@ -909,7 +900,7 @@ const newBackendSagaTwo = combineSagaHandlers(sagaActions, { yield put(actions.setAdminPanelCourseRegistrations(courseRegistrations)); } }, - createCourse: function* (action) { + [AcademyActions.createCourse.type]: function* (action) { const tokens: Tokens = yield selectTokens(); const courseConfig: UpdateCourseConfiguration = action.payload; @@ -978,7 +969,7 @@ const newBackendSagaTwo = combineSagaHandlers(sagaActions, { yield call(showSuccessMessage, 'Successfully created your new course!'); yield routerNavigate(`/courses/${courseRegistration.courseId}`); }, - addNewUsersToCourse: function* (action) { + [AcademyActions.addNewUsersToCourse.type]: function* (action) { const tokens: Tokens = yield selectTokens(); const { users, provider }: { users: UsernameRoleGroup[]; provider: string } = action.payload; @@ -990,7 +981,7 @@ const newBackendSagaTwo = combineSagaHandlers(sagaActions, { yield put(actions.fetchAdminPanelCourseRegistrations()); yield call(showSuccessMessage, 'Users added!'); }, - addNewStoriesUsersToCourse: function* (action) { + [AcademyActions.addNewStoriesUsersToCourse.type]: function* (action) { const tokens: Tokens = yield selectTokens(); const { users, provider } = action.payload; @@ -999,7 +990,7 @@ const newBackendSagaTwo = combineSagaHandlers(sagaActions, { // TODO: Refresh the list of story users // once that page is implemented }, - updateCourseResearchAgreement: function* (action) { + [SessionActions.updateCourseResearchAgreement.type]: function* (action) { const tokens: Tokens = yield selectTokens(); const { agreedToResearch } = action.payload; @@ -1011,7 +1002,7 @@ const newBackendSagaTwo = combineSagaHandlers(sagaActions, { yield put(actions.setCourseRegistration({ agreedToResearch })); yield call(showSuccessMessage, 'Research preference saved!'); }, - updateUserRole: function* (action) { + [SessionActions.updateUserRole.type]: function* (action) { const tokens: Tokens = yield selectTokens(); const { courseRegId, role }: { courseRegId: number; role: Role } = action.payload; @@ -1023,7 +1014,7 @@ const newBackendSagaTwo = combineSagaHandlers(sagaActions, { yield put(actions.fetchAdminPanelCourseRegistrations()); yield call(showSuccessMessage, 'Role updated!'); }, - deleteUserCourseRegistration: function* (action) { + [SessionActions.deleteUserCourseRegistration.type]: function* (action) { const tokens: Tokens = yield selectTokens(); const { courseRegId }: { courseRegId: number } = action.payload; diff --git a/src/commons/sagas/GitHubPersistenceSaga.ts b/src/commons/sagas/GitHubPersistenceSaga.ts index 75c7454e32..6df527f4e9 100644 --- a/src/commons/sagas/GitHubPersistenceSaga.ts +++ b/src/commons/sagas/GitHubPersistenceSaga.ts @@ -1,4 +1,4 @@ -import { +import type { GetResponseDataTypeFromEndpointMethod, GetResponseTypeFromEndpointMethod } from '@octokit/types'; @@ -9,27 +9,25 @@ import * as GitHubUtils from '../../features/github/GitHubUtils'; import { getGitHubOctokitInstance } from '../../features/github/GitHubUtils'; import { store } from '../../pages/createStore'; import SessionActions from '../application/actions/SessionActions'; -import { OverallState } from '../application/ApplicationTypes'; -import FileExplorerDialog, { FileExplorerDialogProps } from '../gitHubOverlay/FileExplorerDialog'; -import RepositoryDialog, { RepositoryDialogProps } from '../gitHubOverlay/RepositoryDialog'; +import type { OverallState } from '../application/ApplicationTypes'; +import FileExplorerDialog, { + type FileExplorerDialogProps +} from '../gitHubOverlay/FileExplorerDialog'; +import RepositoryDialog, { type RepositoryDialogProps } from '../gitHubOverlay/RepositoryDialog'; import { combineSagaHandlers } from '../redux/utils'; import { actions } from '../utils/ActionsHelper'; import Constants from '../utils/Constants'; import { promisifyDialog } from '../utils/DialogHelper'; import { showSuccessMessage } from '../utils/notifications/NotificationsHelper'; -import { EditorTabState } from '../workspace/WorkspaceTypes'; - -export const GitHubPersistenceSaga = combineSagaHandlers( - // TODO: Refactor and combine in a future commit - { ...SessionActions, ...GitHubActions }, - { - loginGitHub: githubLoginSaga, - logoutGitHub: githubLogoutSaga, - githubOpenFile: githubOpenFileSaga, - githubSaveFile: githubSaveFileSaga, - githubSaveFileAs: githubSaveFileAsSaga - } -); +import type { EditorTabState } from '../workspace/WorkspaceTypes'; + +export const GitHubPersistenceSaga = combineSagaHandlers({ + [SessionActions.loginGitHub.type]: githubLoginSaga, + [SessionActions.logoutGitHub.type]: githubLogoutSaga, + [GitHubActions.githubOpenFile.type]: githubOpenFileSaga, + [GitHubActions.githubSaveFile.type]: githubSaveFileSaga, + [GitHubActions.githubSaveFileAs.type]: githubSaveFileAsSaga +}); function* githubLoginSaga() { const githubOauthLoginLink = `https://github.com/login/oauth/authorize?client_id=${Constants.githubClientId}&scope=repo`; diff --git a/src/commons/sagas/LoginSaga.ts b/src/commons/sagas/LoginSaga.ts index 5c12b3648d..05678a5c8d 100644 --- a/src/commons/sagas/LoginSaga.ts +++ b/src/commons/sagas/LoginSaga.ts @@ -1,32 +1,27 @@ -import * as Sentry from '@sentry/browser'; -import { SagaIterator } from 'redux-saga'; +import { setUser } from '@sentry/browser'; import { call } from 'redux-saga/effects'; import CommonsActions from '../application/actions/CommonsActions'; import SessionActions from '../application/actions/SessionActions'; -import { actions } from '../utils/ActionsHelper'; +import { combineSagaHandlers } from '../redux/utils'; import { computeEndpointUrl } from '../utils/AuthHelper'; import { showWarningMessage } from '../utils/notifications/NotificationsHelper'; -import { safeTakeEvery as takeEvery } from './SafeEffects'; -export default function* LoginSaga(): SagaIterator { - yield takeEvery(SessionActions.login.type, updateLoginHref); - - yield takeEvery(SessionActions.setUser.type, (action: ReturnType) => { - Sentry.setUser({ id: action.payload.userId.toString() }); - }); - - yield takeEvery(CommonsActions.logOut.type, () => { - Sentry.setUser(null); - }); -} - -function* updateLoginHref({ payload: providerId }: ReturnType) { - const epUrl = computeEndpointUrl(providerId); - if (!epUrl) { - yield call(showWarningMessage, 'Could not log in; invalid provider name provided.'); - return undefined; +const LoginSaga = combineSagaHandlers({ + [SessionActions.login.type]: function* ({ payload: providerId }) { + const epUrl = computeEndpointUrl(providerId); + if (!epUrl) { + yield call(showWarningMessage, 'Could not log in; invalid provider name provided.'); + return; + } + window.location.href = epUrl; + }, + [SessionActions.setUser.type]: function* (action) { + yield call(setUser, { id: action.payload.userId.toString() }); + }, + [CommonsActions.logOut.type]: function* () { + yield call(setUser, null); } - window.location.href = epUrl; - return undefined; -} +}); + +export default LoginSaga; diff --git a/src/commons/sagas/PersistenceSaga.tsx b/src/commons/sagas/PersistenceSaga.tsx index 71668b2448..8ae0d04def 100644 --- a/src/commons/sagas/PersistenceSaga.tsx +++ b/src/commons/sagas/PersistenceSaga.tsx @@ -1,6 +1,6 @@ import { Intent } from '@blueprintjs/core'; import { Chapter, Variant } from 'js-slang/dist/types'; -import { SagaIterator } from 'redux-saga'; +import type { SagaIterator } from 'redux-saga'; import { call, put, select } from 'redux-saga/effects'; import { @@ -8,11 +8,11 @@ import { PERSISTENCE_OPEN_PICKER, PERSISTENCE_SAVE_FILE, PERSISTENCE_SAVE_FILE_AS, - PersistenceFile + type PersistenceFile } from '../../features/persistence/PersistenceTypes'; import { store } from '../../pages/createStore'; import SessionActions from '../application/actions/SessionActions'; -import { OverallState } from '../application/ApplicationTypes'; +import type { OverallState } from '../application/ApplicationTypes'; import { ExternalLibraryName } from '../application/types/ExternalTypes'; import { actions } from '../utils/ActionsHelper'; import Constants from '../utils/Constants'; @@ -23,7 +23,7 @@ import { showSuccessMessage, showWarningMessage } from '../utils/notifications/NotificationsHelper'; -import { AsyncReturnType } from '../utils/TypeHelper'; +import type { AsyncReturnType } from '../utils/TypeHelper'; import { safeTakeEvery as takeEvery, safeTakeLatest as takeLatest } from './SafeEffects'; const DISCOVERY_DOCS = ['https://www.googleapis.com/discovery/v1/apis/drive/v3/rest']; @@ -396,7 +396,7 @@ function pickFile( }); } -function createFile( +async function createFile( filename: string, parent: string, mimeType: string, @@ -416,17 +416,16 @@ function createFile( const { body, headers } = createMultipartBody(meta, contents, mimeType); - return gapi.client - .request({ - path: UPLOAD_PATH, - method: 'POST', - params: { - uploadType: 'multipart' - }, - headers, - body - }) - .then(({ result }) => ({ id: result.id, name: result.name })); + const { result } = await gapi.client.request({ + path: UPLOAD_PATH, + method: 'POST', + params: { + uploadType: 'multipart' + }, + headers, + body + }); + return { id: result.id, name: result.name }; } function updateFile( diff --git a/src/commons/sagas/PlaygroundSaga.ts b/src/commons/sagas/PlaygroundSaga.ts index 9e4dedd821..71b23c75ca 100644 --- a/src/commons/sagas/PlaygroundSaga.ts +++ b/src/commons/sagas/PlaygroundSaga.ts @@ -14,117 +14,108 @@ import { } from '../application/ApplicationTypes'; import { retrieveFilesInWorkspaceAsRecord } from '../fileSystem/utils'; import { combineSagaHandlers } from '../redux/utils'; -import { visitSideContent } from '../sideContent/SideContentActions'; +import SideContentActions from '../sideContent/SideContentActions'; import { SideContentType } from '../sideContent/SideContentTypes'; import Constants from '../utils/Constants'; import { showSuccessMessage, showWarningMessage } from '../utils/notifications/NotificationsHelper'; import WorkspaceActions from '../workspace/WorkspaceActions'; import { selectWorkspace } from './SafeEffects'; -const PlaygroundSaga = combineSagaHandlers( - PlaygroundActions, - { - generateLzString: updateQueryString, - shortenURL: function* ({ payload: keyword }) { - const queryString = yield select((state: OverallState) => state.playground.queryString); - const errorMsg = 'ERROR'; - - let resp, timeout; - - //we catch and move on if there are errors (plus have a timeout in case) - try { - const { result, hasTimedOut } = yield race({ - result: call(shortenURLRequest, queryString, keyword), - hasTimedOut: delay(10000) - }); - - resp = result; - timeout = hasTimedOut; - } catch (_) {} - - if (!resp || timeout) { - yield put(PlaygroundActions.updateShortURL(errorMsg)); - return yield call(showWarningMessage, 'Something went wrong trying to create the link.'); - } +const PlaygroundSaga = combineSagaHandlers({ + [PlaygroundActions.changeQueryString.type]: updateQueryString, + [PlaygroundActions.shortenURL.type]: function* ({ payload: keyword }) { + const queryString = yield select((state: OverallState) => state.playground.queryString); + const errorMsg = 'ERROR'; - if (resp.status !== 'success' && !resp.shorturl) { - yield put(PlaygroundActions.updateShortURL(errorMsg)); - return yield call(showWarningMessage, resp.message); - } + let resp, timeout; - if (resp.status !== 'success') { - yield call(showSuccessMessage, resp.message); - } - yield put(PlaygroundActions.updateShortURL(Constants.urlShortenerBase + resp.url.keyword)); + //we catch and move on if there are errors (plus have a timeout in case) + try { + const { result, hasTimedOut } = yield race({ + result: call(shortenURLRequest, queryString, keyword), + hasTimedOut: delay(10000) + }); + + resp = result; + timeout = hasTimedOut; + } catch (_) {} + + if (!resp || timeout) { + yield put(PlaygroundActions.updateShortURL(errorMsg)); + return yield call(showWarningMessage, 'Something went wrong trying to create the link.'); + } + + if (resp.status !== 'success' && !resp.shorturl) { + yield put(PlaygroundActions.updateShortURL(errorMsg)); + return yield call(showWarningMessage, resp.message); } + + if (resp.status !== 'success') { + yield call(showSuccessMessage, resp.message); + } + yield put(PlaygroundActions.updateShortURL(Constants.urlShortenerBase + resp.url.keyword)); }, - function* (takeEvery) { - yield takeEvery( - visitSideContent, - function* ({ payload: { newId, prevId, workspaceLocation } }) { - if (workspaceLocation !== 'playground' || newId === prevId) return; - - // Do nothing when clicking the mobile 'Run' tab while on the stepper tab. - if ( - prevId === SideContentType.substVisualizer && - newId === SideContentType.mobileEditorRun - ) { - return; - } - - const { - context: { chapter: playgroundSourceChapter }, - editorTabs - } = yield* selectWorkspace('playground'); - - if (prevId === SideContentType.substVisualizer) { - if (newId === SideContentType.mobileEditorRun) return; - const hasBreakpoints = editorTabs.find(({ breakpoints }) => breakpoints.find(x => !!x)); - - if (!hasBreakpoints) { - yield put(WorkspaceActions.toggleUsingSubst(false, workspaceLocation)); - yield put(WorkspaceActions.clearReplOutput(workspaceLocation)); - } - } - - if (newId !== SideContentType.cseMachine) { - yield put(WorkspaceActions.toggleUsingCse(false, workspaceLocation)); - yield call([CseMachine, CseMachine.clearCse]); - yield call([JavaCseMachine, JavaCseMachine.clearCse]); - yield put(WorkspaceActions.updateCurrentStep(-1, workspaceLocation)); - yield put(WorkspaceActions.updateStepsTotal(0, workspaceLocation)); - yield put(WorkspaceActions.toggleUpdateCse(true, workspaceLocation)); - yield put(WorkspaceActions.setEditorHighlightedLines(workspaceLocation, 0, [])); - } - - if (playgroundSourceChapter === Chapter.FULL_JAVA && newId === SideContentType.cseMachine) { - yield put(WorkspaceActions.toggleUsingCse(true, workspaceLocation)); - } - - if ( - isSourceLanguage(playgroundSourceChapter) && - (newId === SideContentType.substVisualizer || newId === SideContentType.cseMachine) - ) { - if (playgroundSourceChapter <= Chapter.SOURCE_2) { - yield put(WorkspaceActions.toggleUsingSubst(true, workspaceLocation)); - } else { - yield put(WorkspaceActions.toggleUsingCse(true, workspaceLocation)); - } - } - - if (newId === SideContentType.upload) { - yield put(WorkspaceActions.toggleUsingUpload(true, workspaceLocation)); - } else { - yield put(WorkspaceActions.toggleUsingUpload(false, workspaceLocation)); - } - - if (isSchemeLanguage(playgroundSourceChapter) && newId === SideContentType.cseMachine) { - yield put(WorkspaceActions.toggleUsingCse(true, workspaceLocation)); - } + [SideContentActions.visitSideContent.type]: function* ({ + payload: { newId, prevId, workspaceLocation } + }) { + if (workspaceLocation !== 'playground' || newId === prevId) return; + + // Do nothing when clicking the mobile 'Run' tab while on the stepper tab. + if (prevId === SideContentType.substVisualizer && newId === SideContentType.mobileEditorRun) { + return; + } + + const { + context: { chapter: playgroundSourceChapter }, + editorTabs + } = yield* selectWorkspace('playground'); + + if (prevId === SideContentType.substVisualizer) { + if (newId === SideContentType.mobileEditorRun) return; + const hasBreakpoints = editorTabs.find(({ breakpoints }) => breakpoints.find(x => !!x)); + + if (!hasBreakpoints) { + yield put(WorkspaceActions.toggleUsingSubst(false, workspaceLocation)); + yield put(WorkspaceActions.clearReplOutput(workspaceLocation)); } - ); + } + + if (newId !== SideContentType.cseMachine) { + yield put(WorkspaceActions.toggleUsingCse(false, workspaceLocation)); + yield call([CseMachine, CseMachine.clearCse]); + yield call([JavaCseMachine, JavaCseMachine.clearCse]); + yield put(WorkspaceActions.updateCurrentStep(-1, workspaceLocation)); + yield put(WorkspaceActions.updateStepsTotal(0, workspaceLocation)); + yield put(WorkspaceActions.toggleUpdateCse(true, workspaceLocation)); + yield put(WorkspaceActions.setEditorHighlightedLines(workspaceLocation, 0, [])); + } + + if (playgroundSourceChapter === Chapter.FULL_JAVA && newId === SideContentType.cseMachine) { + yield put(WorkspaceActions.toggleUsingCse(true, workspaceLocation)); + } + + if ( + isSourceLanguage(playgroundSourceChapter) && + (newId === SideContentType.substVisualizer || newId === SideContentType.cseMachine) + ) { + if (playgroundSourceChapter <= Chapter.SOURCE_2) { + yield put(WorkspaceActions.toggleUsingSubst(true, workspaceLocation)); + } else { + yield put(WorkspaceActions.toggleUsingCse(true, workspaceLocation)); + } + } + + if (newId === SideContentType.upload) { + yield put(WorkspaceActions.toggleUsingUpload(true, workspaceLocation)); + } else { + yield put(WorkspaceActions.toggleUsingUpload(false, workspaceLocation)); + } + + if (isSchemeLanguage(playgroundSourceChapter) && newId === SideContentType.cseMachine) { + yield put(WorkspaceActions.toggleUsingCse(true, workspaceLocation)); + } } -); +}); export default PlaygroundSaga; diff --git a/src/commons/sagas/RemoteExecutionSaga.ts b/src/commons/sagas/RemoteExecutionSaga.ts index e5046bc6ae..f42f263435 100644 --- a/src/commons/sagas/RemoteExecutionSaga.ts +++ b/src/commons/sagas/RemoteExecutionSaga.ts @@ -1,31 +1,31 @@ import { SlingClient } from '@sourceacademy/sling-client'; -import { assemble, compileFiles, Context } from 'js-slang'; +import { assemble, compileFiles, type Context } from 'js-slang'; import { ExceptionError } from 'js-slang/dist/errors/errors'; import { Chapter, Variant } from 'js-slang/dist/types'; import _ from 'lodash'; import { call, put, race, select, take } from 'redux-saga/effects'; import RemoteExecutionActions from 'src/features/remoteExecution/RemoteExecutionActions'; import { - Ev3DevicePeripherals, - Ev3MotorData, + type Ev3DevicePeripherals, + type Ev3MotorData, Ev3MotorTypes, - Ev3SensorData, - Ev3SensorTypes + type Ev3SensorData, + type Ev3SensorTypes } from 'src/features/remoteExecution/RemoteExecutionEv3Types'; import { - Device, - DeviceSession, + type Device, + type DeviceSession, deviceTypesById, - WebSocketEndpointInformation + type WebSocketEndpointInformation } from 'src/features/remoteExecution/RemoteExecutionTypes'; import { store } from 'src/pages/createStore'; import InterpreterActions from '../application/actions/InterpreterActions'; -import { OverallState } from '../application/ApplicationTypes'; +import type { OverallState } from '../application/ApplicationTypes'; import { ExternalLibraryName } from '../application/types/ExternalTypes'; import { combineSagaHandlers } from '../redux/utils'; import { actions } from '../utils/ActionsHelper'; -import { MaybePromise } from '../utils/TypeHelper'; +import type { MaybePromise } from '../utils/TypeHelper'; import { fetchDevices, getDeviceWSEndpoint } from './RequestsSaga'; const dummyLocation = { @@ -34,9 +34,8 @@ const dummyLocation = { }; // TODO: Refactor and combine in a future commit -const sagaActions = { ...RemoteExecutionActions, ...InterpreterActions }; -const RemoteExecutionSaga = combineSagaHandlers(sagaActions, { - remoteExecFetchDevices: { +const RemoteExecutionSaga = combineSagaHandlers({ + [RemoteExecutionActions.remoteExecFetchDevices.type]: { takeLatest: function* () { const [tokens, session]: [any, DeviceSession | undefined] = yield select( (state: OverallState) => [ @@ -65,7 +64,7 @@ const RemoteExecutionSaga = combineSagaHandlers(sagaActions, { } } }, - remoteExecConnect: function* (action): any { + [RemoteExecutionActions.remoteExecConnect.type]: function* (action): any { const [tokens, session]: [any, DeviceSession | undefined] = yield select( (state: OverallState) => [ { @@ -238,7 +237,7 @@ const RemoteExecutionSaga = combineSagaHandlers(sagaActions, { ); } }, - remoteExecDisconnect: function* (action) { + [RemoteExecutionActions.remoteExecDisconnect.type]: function* (action) { const session: DeviceSession | undefined = yield select( (state: OverallState) => state.session.remoteExecutionSession ); @@ -252,7 +251,7 @@ const RemoteExecutionSaga = combineSagaHandlers(sagaActions, { yield put(actions.remoteExecUpdateSession(undefined)); yield put(actions.externalLibrarySelect(ExternalLibraryName.NONE, session.workspace, true)); }, - remoteExecRun: function* (action) { + [RemoteExecutionActions.remoteExecRun.type]: function* (action) { const { files, entrypointFilePath } = action.payload; const session: DeviceSession | undefined = yield select( @@ -294,7 +293,7 @@ const RemoteExecutionSaga = combineSagaHandlers(sagaActions, { client.sendRun(Buffer.from(assembled)); }, - beginInterruptExecution: function* () { + [InterpreterActions.beginInterruptExecution.type]: function* () { const session: DeviceSession | undefined = yield select( (state: OverallState) => state.session.remoteExecutionSession ); diff --git a/src/commons/sagas/SafeEffects.ts b/src/commons/sagas/SafeEffects.ts index cbf82404dd..bc88f59c4e 100644 --- a/src/commons/sagas/SafeEffects.ts +++ b/src/commons/sagas/SafeEffects.ts @@ -36,6 +36,16 @@ function isIterator(obj: any) { return obj && typeof obj.next === 'function' && typeof obj.throw === 'function'; } +export function wrapSaga Generator>(saga: T) { + return function* (...args: Parameters) { + try { + return yield* saga(...args); + } catch (error) { + handleUncaughtError(error); + } + }; +} + export function safeTakeEvery

>( pattern: P, worker: (action: A) => any diff --git a/src/commons/sagas/SideContentSaga.ts b/src/commons/sagas/SideContentSaga.ts index 955c2b0173..2f99877591 100644 --- a/src/commons/sagas/SideContentSaga.ts +++ b/src/commons/sagas/SideContentSaga.ts @@ -1,4 +1,4 @@ -import { Action } from '@reduxjs/toolkit'; +import type { Action } from '@reduxjs/toolkit'; import { put, take } from 'redux-saga/effects'; import StoriesActions from 'src/features/stories/StoriesActions'; @@ -11,10 +11,10 @@ const isSpawnSideContent = ( ): action is ReturnType => action.type === SideContentActions.spawnSideContent.type; -// TODO: Refactor and combine in a future commit -const sagaActions = { ...SideContentActions, ...WorkspaceActions, ...StoriesActions }; -export const SideContentSaga = combineSagaHandlers(sagaActions, { - beginAlertSideContent: function* ({ payload: { id, workspaceLocation } }) { +const SideContentSaga = combineSagaHandlers({ + [SideContentActions.beginAlertSideContent.type]: function* ({ + payload: { id, workspaceLocation } + }) { // When a program finishes evaluation, we clear all alerts, // So we must wait until after and all module tabs have been spawned // to process any kind of alerts that were raised by non-module side content @@ -24,7 +24,7 @@ export const SideContentSaga = combineSagaHandlers(sagaActions, { ); yield put(SideContentActions.endAlertSideContent(id, workspaceLocation)); }, - notifyProgramEvaluated: function* (action) { + [WorkspaceActions.notifyProgramEvaluated.type]: function* (action) { if (!action.payload.workspaceLocation || action.payload.workspaceLocation === 'stories') return; const debuggerContext = { @@ -38,7 +38,7 @@ export const SideContentSaga = combineSagaHandlers(sagaActions, { SideContentActions.spawnSideContent(action.payload.workspaceLocation, debuggerContext) ); }, - notifyStoriesEvaluated: function* (action) { + [StoriesActions.notifyStoriesEvaluated.type]: function* (action) { yield put(SideContentActions.spawnSideContent(`stories.${action.payload.env}`, action.payload)); } }); diff --git a/src/commons/sagas/StoriesSaga.ts b/src/commons/sagas/StoriesSaga.ts index e1aa6c3738..dc43f442e3 100644 --- a/src/commons/sagas/StoriesSaga.ts +++ b/src/commons/sagas/StoriesSaga.ts @@ -1,4 +1,4 @@ -import { Context } from 'js-slang'; +import type { Context } from 'js-slang'; import { call, put, select } from 'redux-saga/effects'; import StoriesActions from 'src/features/stories/StoriesActions'; import { @@ -25,10 +25,8 @@ import { defaultStoryContent } from '../utils/StoriesHelper'; import { selectTokens } from './BackendSaga'; import { evalCodeSaga } from './WorkspaceSaga/helpers/evalCode'; -// TODO: Refactor and combine in a future commit -const sagaActions = { ...StoriesActions, ...SessionActions }; -const StoriesSaga = combineSagaHandlers(sagaActions, { - getStoriesList: { +const StoriesSaga = combineSagaHandlers({ + [StoriesActions.getStoriesList.type]: { takeLatest: function* () { const tokens: Tokens = yield selectTokens(); const allStories: StoryListView[] = yield call(async () => { @@ -39,7 +37,7 @@ const StoriesSaga = combineSagaHandlers(sagaActions, { yield put(actions.updateStoriesList(allStories)); } }, - setCurrentStoryId: function* (action) { + [StoriesActions.setCurrentStoryId.type]: function* (action) { const tokens: Tokens = yield selectTokens(); const storyId = action.payload; if (storyId) { @@ -54,7 +52,7 @@ const StoriesSaga = combineSagaHandlers(sagaActions, { yield put(actions.setCurrentStory(defaultStory)); } }, - createStory: function* (action) { + [StoriesActions.createStory.type]: function* (action) { const tokens: Tokens = yield selectTokens(); const story = action.payload; const userId: number | undefined = yield select((state: OverallState) => state.stories.userId); @@ -80,7 +78,7 @@ const StoriesSaga = combineSagaHandlers(sagaActions, { yield put(actions.getStoriesList()); }, - saveStory: function* (action) { + [StoriesActions.saveStory.type]: function* (action) { const tokens: Tokens = yield selectTokens(); const { story, id } = action.payload; const updatedStory: StoryView | null = yield call( @@ -100,7 +98,7 @@ const StoriesSaga = combineSagaHandlers(sagaActions, { yield put(actions.getStoriesList()); }, - deleteStory: function* (action) { + [StoriesActions.deleteStory.type]: function* (action) { const tokens: Tokens = yield selectTokens(); const storyId = action.payload; yield call(deleteStory, tokens, storyId); @@ -108,7 +106,7 @@ const StoriesSaga = combineSagaHandlers(sagaActions, { yield put(actions.getStoriesList()); }, - getStoriesUser: function* () { + [StoriesActions.getStoriesUser.type]: function* () { const tokens: Tokens = yield selectTokens(); const me: { id: number; @@ -126,7 +124,7 @@ const StoriesSaga = combineSagaHandlers(sagaActions, { yield put(actions.setCurrentStoriesUser(me.id, me.name)); yield put(actions.setCurrentStoriesGroup(me.groupId, me.groupName, me.role)); }, - evalStory: function* (action) { + [StoriesActions.evalStory.type]: function* (action) { const env = action.payload.env; const code = action.payload.code; const execTime: number = yield select( @@ -149,7 +147,7 @@ const StoriesSaga = combineSagaHandlers(sagaActions, { env ); }, - fetchAdminPanelStoriesUsers: function* (action) { + [StoriesActions.fetchAdminPanelStoriesUsers.type]: function* (action) { const tokens: Tokens = yield selectTokens(); const storiesUsers = yield call(getAdminPanelStoriesUsers, tokens); @@ -158,7 +156,7 @@ const StoriesSaga = combineSagaHandlers(sagaActions, { yield put(actions.setAdminPanelStoriesUsers(storiesUsers)); } }, - updateStoriesUserRole: function* (action) { + [SessionActions.updateStoriesUserRole.type]: function* (action) { const tokens: Tokens = yield selectTokens(); const { userId, role } = action.payload; @@ -169,7 +167,7 @@ const StoriesSaga = combineSagaHandlers(sagaActions, { yield call(showSuccessMessage, 'Role updated!'); } }, - deleteStoriesUserUserGroups: function* (action) { + [SessionActions.deleteStoriesUserUserGroups.type]: function* (action) { const tokens: Tokens = yield selectTokens(); const { userId } = action.payload; diff --git a/src/commons/sagas/WorkspaceSaga/helpers/evalEditor.ts b/src/commons/sagas/WorkspaceSaga/helpers/evalEditor.ts index 2275f553d1..1ad74892f8 100644 --- a/src/commons/sagas/WorkspaceSaga/helpers/evalEditor.ts +++ b/src/commons/sagas/WorkspaceSaga/helpers/evalEditor.ts @@ -9,11 +9,8 @@ import type { OverallState } from '../../../application/ApplicationTypes'; import { retrieveFilesInWorkspaceAsRecord } from '../../../fileSystem/utils'; import { actions } from '../../../utils/ActionsHelper'; import { makeElevatedContext } from '../../../utils/JsSlangHelper'; -import { - type EditorTabState, - EVAL_SILENT, - type WorkspaceLocation -} from '../../../workspace/WorkspaceTypes'; +import { EVAL_SILENT, type WorkspaceLocation } from '../../../workspace/WorkspaceTypes'; +import { selectWorkspace } from '../../SafeEffects'; import { blockExtraMethods } from './blockExtraMethods'; import { clearContext } from './clearContext'; import { evalCodeSaga } from './evalCode'; @@ -22,31 +19,20 @@ import { insertDebuggerStatements } from './insertDebuggerStatements'; export function* evalEditorSaga( workspaceLocation: WorkspaceLocation ): Generator { - const [ - prepend, + const { activeEditorTabIndex, + programPrependValue: prepend, editorTabs, execTime, - isFolderModeEnabled, - fileSystem, - remoteExecutionSession - ]: [ - string, - number | null, - EditorTabState[], - number, - boolean, - FSModule, - DeviceSession | undefined - ] = yield select((state: OverallState) => [ - state.workspaces[workspaceLocation].programPrependValue, - state.workspaces[workspaceLocation].activeEditorTabIndex, - state.workspaces[workspaceLocation].editorTabs, - state.workspaces[workspaceLocation].execTime, - state.workspaces[workspaceLocation].isFolderModeEnabled, - state.fileSystem.inBrowserFileSystem, - state.session.remoteExecutionSession - ]); + isFolderModeEnabled + } = yield* selectWorkspace(workspaceLocation); + + const [fileSystem, remoteExecutionSession]: [FSModule, DeviceSession | undefined] = yield select( + (state: OverallState) => [ + state.fileSystem.inBrowserFileSystem, + state.session.remoteExecutionSession + ] + ); if (activeEditorTabIndex === null) { throw new Error('Cannot evaluate program without an entrypoint file.'); @@ -62,7 +48,6 @@ export function* evalEditorSaga( }; } const entrypointFilePath = editorTabs[activeEditorTabIndex].filePath ?? defaultFilePath; - yield put(actions.addEvent([EventType.RUN_CODE])); if (remoteExecutionSession && remoteExecutionSession.workspace === workspaceLocation) { diff --git a/src/commons/sagas/WorkspaceSaga/helpers/runTestCase.ts b/src/commons/sagas/WorkspaceSaga/helpers/runTestCase.ts index c5fa54e460..35452c49cd 100644 --- a/src/commons/sagas/WorkspaceSaga/helpers/runTestCase.ts +++ b/src/commons/sagas/WorkspaceSaga/helpers/runTestCase.ts @@ -3,10 +3,10 @@ import { random } from 'lodash'; import { call, put, select, StrictEffect } from 'redux-saga/effects'; import type { OverallState } from '../../../application/ApplicationTypes'; -import type { TestcaseType } from '../../../assessment/AssessmentTypes'; import { actions } from '../../../utils/ActionsHelper'; import { makeElevatedContext } from '../../../utils/JsSlangHelper'; import { EVAL_SILENT, type WorkspaceLocation } from '../../../workspace/WorkspaceTypes'; +import { selectWorkspace } from '../../SafeEffects'; import { blockExtraMethods } from './blockExtraMethods'; import { clearContext } from './clearContext'; import { evalCodeSaga } from './evalCode'; @@ -17,22 +17,17 @@ export function* runTestCase( workspaceLocation: WorkspaceLocation, index: number ): Generator { - const [prepend, value, postpend, testcase]: [string, string, string, string] = yield select( - (state: OverallState) => { - const prepend = state.workspaces[workspaceLocation].programPrependValue; - const postpend = state.workspaces[workspaceLocation].programPostpendValue; - // TODO: Hardcoded to make use of the first editor tab. Rewrite after editor tabs are added. - const value = state.workspaces[workspaceLocation].editorTabs[0].value; - const testcase = state.workspaces[workspaceLocation].editorTestcases[index].program; - return [prepend, value, postpend, testcase] as [string, string, string, string]; - } - ); - const type: TestcaseType = yield select( - (state: OverallState) => state.workspaces[workspaceLocation].editorTestcases[index].type - ); - const execTime: number = yield select( - (state: OverallState) => state.workspaces[workspaceLocation].execTime - ); + const { + editorTabs: { + [0]: { value } + }, + editorTestcases: { + [index]: { program: testcase, type: type } + }, + execTime, + programPrependValue: prepend, + programPostpendValue: postpend + } = yield* selectWorkspace(workspaceLocation); yield* clearContext(workspaceLocation, value); diff --git a/src/commons/sagas/WorkspaceSaga/index.ts b/src/commons/sagas/WorkspaceSaga/index.ts index ac1ba23bfb..0c4002f4c1 100644 --- a/src/commons/sagas/WorkspaceSaga/index.ts +++ b/src/commons/sagas/WorkspaceSaga/index.ts @@ -34,439 +34,426 @@ import { showWarningMessage } from '../../utils/notifications/NotificationsHelper'; import { showFullJSDisclaimer, showFullTSDisclaimer } from '../../utils/WarningDialogHelper'; -import type { EditorTabState } from '../../workspace/WorkspaceTypes'; import { selectWorkspace } from '../SafeEffects'; import { evalCodeSaga } from './helpers/evalCode'; import { evalEditorSaga } from './helpers/evalEditor'; import { runTestCase } from './helpers/runTestCase'; -const WorkspaceSaga = combineSagaHandlers( - // TODO: Refactor and combine in a future commit - { ...WorkspaceActions, ...InterpreterActions }, - { - addHtmlConsoleError: function* (action) { - // TODO: Do not use if-else logic - if (!action.payload.storyEnv) { - yield put( - actions.handleConsoleLog(action.payload.workspaceLocation, action.payload.errorMsg) - ); - } else { - yield put( - actions.handleStoriesConsoleLog(action.payload.storyEnv, action.payload.errorMsg) - ); - } - }, - toggleFolderMode: function* (action) { - const workspaceLocation = action.payload.workspaceLocation; - const isFolderModeEnabled: boolean = yield select( - (state: OverallState) => state.workspaces[workspaceLocation].isFolderModeEnabled +const WorkspaceSaga = combineSagaHandlers({ + [WorkspaceActions.addHtmlConsoleError.type]: function* (action) { + // TODO: Do not use if-else logic + if (!action.payload.storyEnv) { + yield put( + actions.handleConsoleLog(action.payload.workspaceLocation, action.payload.errorMsg) ); - yield put(actions.setFolderMode(workspaceLocation, !isFolderModeEnabled)); - const warningMessage = `Folder mode ${!isFolderModeEnabled ? 'enabled' : 'disabled'}`; - yield call(showWarningMessage, warningMessage, 750); - }, - setFolderMode: function* (action): any { - const workspaceLocation = action.payload.workspaceLocation; - const isFolderModeEnabled: boolean = yield select( - (state: OverallState) => state.workspaces[workspaceLocation].isFolderModeEnabled + } else { + yield put(actions.handleStoriesConsoleLog(action.payload.storyEnv, action.payload.errorMsg)); + } + }, + [WorkspaceActions.toggleFolderMode.type]: function* (action) { + const workspaceLocation = action.payload.workspaceLocation; + const isFolderModeEnabled: boolean = yield select( + (state: OverallState) => state.workspaces[workspaceLocation].isFolderModeEnabled + ); + yield put(actions.setFolderMode(workspaceLocation, !isFolderModeEnabled)); + const warningMessage = `Folder mode ${!isFolderModeEnabled ? 'enabled' : 'disabled'}`; + yield call(showWarningMessage, warningMessage, 750); + }, + [WorkspaceActions.setFolderMode.type]: function* (action): any { + const workspaceLocation = action.payload.workspaceLocation; + const { editorTabs, isFolderModeEnabled } = yield* selectWorkspace(workspaceLocation); + + // Do nothing if Folder mode is enabled. + if (isFolderModeEnabled) { + return; + } + + // If Folder mode is disabled and there are no open editor tabs, add an editor tab. + if (editorTabs.length === 0) { + const defaultFilePath = `${WORKSPACE_BASE_PATHS[workspaceLocation]}/program.js`; + const fileSystem: FSModule | null = yield select( + (state: OverallState) => state.fileSystem.inBrowserFileSystem ); - // Do nothing if Folder mode is enabled. - if (isFolderModeEnabled) { + // If the file system is not initialised, add an editor tab with the default editor value. + if (fileSystem === null) { + yield put(actions.addEditorTab(workspaceLocation, defaultFilePath, defaultEditorValue)); return; } - - const editorTabs: EditorTabState[] = yield select( - (state: OverallState) => state.workspaces[workspaceLocation].editorTabs - ); - // If Folder mode is disabled and there are no open editor tabs, add an editor tab. - if (editorTabs.length === 0) { - const defaultFilePath = `${WORKSPACE_BASE_PATHS[workspaceLocation]}/program.js`; - const fileSystem: FSModule | null = yield select( - (state: OverallState) => state.fileSystem.inBrowserFileSystem - ); - // If the file system is not initialised, add an editor tab with the default editor value. - if (fileSystem === null) { - yield put(actions.addEditorTab(workspaceLocation, defaultFilePath, defaultEditorValue)); - return; - } - const editorValue: string = yield new Promise((resolve, reject) => { - fileSystem.exists(defaultFilePath, fileExists => { - if (!fileExists) { - // If the file does not exist, we need to also create it in the file system. - writeFileRecursively(fileSystem, defaultFilePath, defaultEditorValue) - .then(() => resolve(defaultEditorValue)) - .catch(err => reject(err)); + const editorValue: string = yield new Promise((resolve, reject) => { + fileSystem.exists(defaultFilePath, fileExists => { + if (!fileExists) { + // If the file does not exist, we need to also create it in the file system. + writeFileRecursively(fileSystem, defaultFilePath, defaultEditorValue) + .then(() => resolve(defaultEditorValue)) + .catch(err => reject(err)); + return; + } + fileSystem.readFile(defaultFilePath, 'utf-8', (err, fileContents) => { + if (err) { + reject(err); + return; + } + if (fileContents === undefined) { + reject(new Error('File exists but has no contents.')); return; } - fileSystem.readFile(defaultFilePath, 'utf-8', (err, fileContents) => { - if (err) { - reject(err); - return; - } - if (fileContents === undefined) { - reject(new Error('File exists but has no contents.')); - return; - } - resolve(fileContents); - }); + resolve(fileContents); }); }); - yield put(actions.addEditorTab(workspaceLocation, defaultFilePath, editorValue)); - } - }, - // Mirror editor updates to the associated file in the filesystem. - updateEditorValue: function* (action): any { - const workspaceLocation = action.payload.workspaceLocation; - const editorTabIndex = action.payload.editorTabIndex; + }); + yield put(actions.addEditorTab(workspaceLocation, defaultFilePath, editorValue)); + } + }, + // Mirror editor updates to the associated file in the filesystem. + [WorkspaceActions.updateEditorValue.type]: function* (action): any { + const workspaceLocation = action.payload.workspaceLocation; + const editorTabIndex = action.payload.editorTabIndex; - const filePath: string | undefined = yield select( - (state: OverallState) => - state.workspaces[workspaceLocation].editorTabs[editorTabIndex].filePath - ); - // If the code does not have an associated file, do nothing. - if (filePath === undefined) { - return; - } + const filePath: string | undefined = yield select( + (state: OverallState) => + state.workspaces[workspaceLocation].editorTabs[editorTabIndex].filePath + ); + // If the code does not have an associated file, do nothing. + if (filePath === undefined) { + return; + } - const fileSystem: FSModule | null = yield select( - (state: OverallState) => state.fileSystem.inBrowserFileSystem - ); - // If the file system is not initialised, do nothing. - if (fileSystem === null) { - return; + const fileSystem: FSModule | null = yield select( + (state: OverallState) => state.fileSystem.inBrowserFileSystem + ); + // If the file system is not initialised, do nothing. + if (fileSystem === null) { + return; + } + + fileSystem.writeFile(filePath, action.payload.newEditorValue, err => { + if (err) { + console.error(err); } + }); + }, + [WorkspaceActions.evalEditor.type]: ({ payload: { workspaceLocation } }) => + evalEditorSaga(workspaceLocation), + [WorkspaceActions.promptAutocomplete.type]: function* (action) { + const workspaceLocation = action.payload.workspaceLocation; + const { + activeEditorTabIndex, + editorTabs, + context, + externalLibrary: extLib, + programPrependValue: prepend + } = yield* selectWorkspace(workspaceLocation); - fileSystem.writeFile(filePath, action.payload.newEditorValue, err => { - if (err) { - console.error(err); - } - }); - yield; - }, - evalEditor: function* (action) { - const workspaceLocation = action.payload.workspaceLocation; - yield* evalEditorSaga(workspaceLocation); - }, - promptAutocomplete: function* (action): any { - const workspaceLocation = action.payload.workspaceLocation; - const { - activeEditorTabIndex, - editorTabs, - context, - externalLibrary: extLib, - programPrependValue: prepend - } = yield* selectWorkspace(workspaceLocation); + const editorValue = editorTabs[activeEditorTabIndex ?? 0].value; - const editorValue = editorTabs[activeEditorTabIndex ?? 0].value; + // Deal with prepended code + let autocompleteCode; + let prependLength = 0; + if (!prepend) { + autocompleteCode = editorValue; + } else { + prependLength = prepend.split('\n').length; + autocompleteCode = prepend + '\n' + editorValue; + } - // Deal with prepended code - let autocompleteCode; - let prependLength = 0; - if (!prepend) { - autocompleteCode = editorValue; - } else { - prependLength = prepend.split('\n').length; - autocompleteCode = prepend + '\n' + editorValue; - } + const [editorNames, displaySuggestions]: Awaited> = yield call( + getNames, + autocompleteCode, + action.payload.row + prependLength, + action.payload.column, + context + ); - const [editorNames, displaySuggestions] = yield call( - getNames, - autocompleteCode, - action.payload.row + prependLength, - action.payload.column, - context - ); + if (!displaySuggestions) { + yield call(action.payload.callback); + return; + } - if (!displaySuggestions) { - yield call(action.payload.callback); - return; - } + const editorSuggestions: any[] = editorNames.map(name => { + return { + ...name, + caption: name.name, + value: name.name, + score: name.score ? name.score + 1000 : 1000, // Prioritize suggestions from code + name: undefined + }; + }); - const editorSuggestions = editorNames.map((name: any) => { - return { - ...name, - caption: name.name, - value: name.name, - score: name.score ? name.score + 1000 : 1000, // Prioritize suggestions from code - name: undefined - }; - }); + let chapterName = context.chapter.toString(); + const variant = context.variant ?? Variant.DEFAULT; + if (variant !== Variant.DEFAULT) { + chapterName += '_' + variant; + } - let chapterName = context.chapter.toString(); - const variant = context.variant ?? Variant.DEFAULT; - if (variant !== Variant.DEFAULT) { - chapterName += '_' + variant; - } + const builtinSuggestions = Documentation.builtins[chapterName] || []; + const extLibSuggestions = Documentation.externalLibraries[extLib] || []; - const builtinSuggestions = Documentation.builtins[chapterName] || []; - const extLibSuggestions = Documentation.externalLibraries[extLib] || []; + yield call( + action.payload.callback, + null, + editorSuggestions.concat(builtinSuggestions, extLibSuggestions) + ); + }, + [WorkspaceActions.toggleEditorAutorun.type]: function* (action) { + const workspaceLocation = action.payload.workspaceLocation; + const isEditorAutorun = yield select( + (state: OverallState) => state.workspaces[workspaceLocation].isEditorAutorun + ); + yield call(showWarningMessage, 'Autorun ' + (isEditorAutorun ? 'Started' : 'Stopped'), 750); + }, + [WorkspaceActions.evalRepl.type]: function* (action) { + if (yield call(selectFeatureSaga, flagConductorEnable)) { + return; // no-op: evalCodeConductorSaga will pick up this action and handle it from there + } + const workspaceLocation = action.payload.workspaceLocation; + const { replValue: code, execTime } = yield* selectWorkspace(workspaceLocation); - yield call( - action.payload.callback, - null, - editorSuggestions.concat(builtinSuggestions, extLibSuggestions) - ); - }, - toggleEditorAutorun: function* (action): any { - const workspaceLocation = action.payload.workspaceLocation; - const isEditorAutorun = yield select( - (state: OverallState) => state.workspaces[workspaceLocation].isEditorAutorun - ); - yield call(showWarningMessage, 'Autorun ' + (isEditorAutorun ? 'Started' : 'Stopped'), 750); - }, - evalRepl: function* (action) { - if (yield call(selectFeatureSaga, flagConductorEnable)) { - return; // no-op: evalCodeConductorSaga will pick up this action and handle it from there - } - const workspaceLocation = action.payload.workspaceLocation; - const code: string = yield select( - (state: OverallState) => state.workspaces[workspaceLocation].replValue - ); - const execTime: number = yield select( - (state: OverallState) => state.workspaces[workspaceLocation].execTime - ); - yield put(actions.beginInterruptExecution(workspaceLocation)); - yield put(actions.clearReplInput(workspaceLocation)); - yield put(actions.sendReplInputToOutput(code, workspaceLocation)); - const context: Context = yield select( - (state: OverallState) => state.workspaces[workspaceLocation].context - ); - // Reset old context.errors - context.errors = []; - const codeFilePath = '/code.js'; - const codeFiles = { - [codeFilePath]: code - }; - yield call( - evalCodeSaga, - codeFiles, - codeFilePath, - context, - execTime, - WorkspaceActions.evalRepl.type, - workspaceLocation - ); - }, - debuggerResume: function* (action) { - const workspaceLocation = action.payload.workspaceLocation; - const code: string = yield select( - // TODO: Hardcoded to make use of the first editor tab. Rewrite after editor tabs are added. - (state: OverallState) => state.workspaces[workspaceLocation].editorTabs[0].value - ); - const execTime: number = yield select( - (state: OverallState) => state.workspaces[workspaceLocation].execTime - ); - yield put(actions.beginInterruptExecution(workspaceLocation)); - /** Clear the context, with the same chapter and externalSymbols as before. */ - yield put(actions.clearReplOutput(workspaceLocation)); - const context: Context = yield select( - (state: OverallState) => state.workspaces[workspaceLocation].context - ); + yield put(actions.beginInterruptExecution(workspaceLocation)); + yield put(actions.clearReplInput(workspaceLocation)); + yield put(actions.sendReplInputToOutput(code, workspaceLocation)); + const context: Context = yield select( + (state: OverallState) => state.workspaces[workspaceLocation].context + ); + // Reset old context.errors + context.errors = []; + const codeFilePath = '/code.js'; + const codeFiles = { + [codeFilePath]: code + }; + yield call( + evalCodeSaga, + codeFiles, + codeFilePath, + context, + execTime, + WorkspaceActions.evalRepl.type, + workspaceLocation + ); + }, + [InterpreterActions.debuggerResume.type]: function* (action) { + const workspaceLocation = action.payload.workspaceLocation; + const code: string = yield select( // TODO: Hardcoded to make use of the first editor tab. Rewrite after editor tabs are added. - yield put(actions.setEditorHighlightedLines(workspaceLocation, 0, [])); - const codeFilePath = '/code.js'; - const codeFiles = { - [codeFilePath]: code - }; - yield call( - evalCodeSaga, - codeFiles, - codeFilePath, - context, - execTime, - InterpreterActions.debuggerResume.type, - workspaceLocation - ); - }, - debuggerReset: function* (action) { - const workspaceLocation = action.payload.workspaceLocation; - const context: Context = yield select( - (state: OverallState) => state.workspaces[workspaceLocation].context - ); - yield put(actions.clearReplOutput(workspaceLocation)); - // TODO: Hardcoded to make use of the first editor tab. Rewrite after editor tabs are added. - yield put(actions.setEditorHighlightedLines(workspaceLocation, 0, [])); - context.runtime.break = false; - yield put(actions.updateLastDebuggerResult(undefined, workspaceLocation)); - }, - setEditorHighlightedLines: function* (action) { - const newHighlightedLines = action.payload.newHighlightedLines; - if (newHighlightedLines.length === 0) { - yield call(highlightClean); - } else { - try { - for (const [startRow, endRow] of newHighlightedLines) { - for (let row = startRow; row <= endRow; row++) { - yield call(highlightLine, row); - } + (state: OverallState) => state.workspaces[workspaceLocation].editorTabs[0].value + ); + const execTime: number = yield select( + (state: OverallState) => state.workspaces[workspaceLocation].execTime + ); + yield put(actions.beginInterruptExecution(workspaceLocation)); + /** Clear the context, with the same chapter and externalSymbols as before. */ + yield put(actions.clearReplOutput(workspaceLocation)); + const context: Context = yield select( + (state: OverallState) => state.workspaces[workspaceLocation].context + ); + // TODO: Hardcoded to make use of the first editor tab. Rewrite after editor tabs are added. + yield put(actions.setEditorHighlightedLines(workspaceLocation, 0, [])); + const codeFilePath = '/code.js'; + const codeFiles = { + [codeFilePath]: code + }; + yield call( + evalCodeSaga, + codeFiles, + codeFilePath, + context, + execTime, + InterpreterActions.debuggerResume.type, + workspaceLocation + ); + }, + [InterpreterActions.debuggerReset.type]: function* (action) { + const workspaceLocation = action.payload.workspaceLocation; + const context: Context = yield select( + (state: OverallState) => state.workspaces[workspaceLocation].context + ); + yield put(actions.clearReplOutput(workspaceLocation)); + // TODO: Hardcoded to make use of the first editor tab. Rewrite after editor tabs are added. + yield put(actions.setEditorHighlightedLines(workspaceLocation, 0, [])); + context.runtime.break = false; + yield put(actions.updateLastDebuggerResult(undefined, workspaceLocation)); + }, + [WorkspaceActions.setEditorHighlightedLines.type]: function* ({ + payload: { newHighlightedLines } + }) { + if (newHighlightedLines.length === 0) { + yield call(highlightClean); + } else { + try { + for (const [startRow, endRow] of newHighlightedLines) { + for (let row = startRow; row <= endRow; row++) { + yield call(highlightLine, row); } - } catch (e) { - // Error most likely caused by trying to highlight the lines of the prelude - // in CSE Machine. Can be ignored. } + } catch (e) { + // Error most likely caused by trying to highlight the lines of the prelude + // in CSE Machine. Can be ignored. } - }, - setEditorHighlightedLinesControl: function* (action) { - const newHighlightedLines = action.payload.newHighlightedLines; - if (newHighlightedLines.length === 0) { - yield call(highlightCleanForControl); - } else { - try { - for (const [startRow, endRow] of newHighlightedLines) { - for (let row = startRow; row <= endRow; row++) { - yield call(highlightLineForControl, row); - } + } + }, + [WorkspaceActions.setEditorHighlightedLinesControl.type]: function* ({ + payload: { newHighlightedLines } + }) { + if (newHighlightedLines.length === 0) { + yield call(highlightCleanForControl); + } else { + try { + for (const [startRow, endRow] of newHighlightedLines) { + for (let row = startRow; row <= endRow; row++) { + yield call(highlightLineForControl, row); } - } catch (e) { - // Error most likely caused by trying to highlight the lines of the prelude - // in CSE Machine. Can be ignored. } + } catch (e) { + // Error most likely caused by trying to highlight the lines of the prelude + // in CSE Machine. Can be ignored. } - }, - evalTestcase: function* (action) { - yield put(actions.addEvent([EventType.RUN_TESTCASE])); - const workspaceLocation = action.payload.workspaceLocation; - const index = action.payload.testcaseId; - yield* runTestCase(workspaceLocation, index); - }, - chapterSelect: function* (action) { - const { workspaceLocation, chapter: newChapter, variant: newVariant } = action.payload; - const [oldVariant, oldChapter, symbols, globals, externalLibraryName]: [ - Variant, - Chapter, - string[], - Array<[string, any]>, - ExternalLibraryName - ] = yield select((state: OverallState) => [ - state.workspaces[workspaceLocation].context.variant, - state.workspaces[workspaceLocation].context.chapter, - state.workspaces[workspaceLocation].context.externalSymbols, - state.workspaces[workspaceLocation].globals, - state.workspaces[workspaceLocation].externalLibrary - ]); + } + }, + [WorkspaceActions.evalTestcase.type]: function* (action) { + const workspaceLocation = action.payload.workspaceLocation; + if (workspaceLocation === 'stories') return; - const chapterChanged: boolean = newChapter !== oldChapter || newVariant !== oldVariant; - const toChangeChapter: boolean = - newChapter === Chapter.FULL_JS - ? chapterChanged && (yield call(showFullJSDisclaimer)) - : newChapter === Chapter.FULL_TS - ? chapterChanged && (yield call(showFullTSDisclaimer)) - : chapterChanged; + yield put(actions.addEvent([EventType.RUN_TESTCASE], workspaceLocation)); + const index = action.payload.testcaseId; + yield* runTestCase(workspaceLocation, index); + }, + [WorkspaceActions.chapterSelect.type]: function* (action) { + const { workspaceLocation, chapter: newChapter, variant: newVariant } = action.payload; + const [oldVariant, oldChapter, symbols, globals, externalLibraryName]: [ + Variant, + Chapter, + string[], + Array<[string, any]>, + ExternalLibraryName + ] = yield select((state: OverallState) => [ + state.workspaces[workspaceLocation].context.variant, + state.workspaces[workspaceLocation].context.chapter, + state.workspaces[workspaceLocation].context.externalSymbols, + state.workspaces[workspaceLocation].globals, + state.workspaces[workspaceLocation].externalLibrary + ]); - if (toChangeChapter) { - const library: Library = { - chapter: newChapter, - variant: newVariant, - external: { - name: externalLibraryName, - symbols - }, - globals - }; - yield put(actions.beginClearContext(workspaceLocation, library, false)); - yield put(actions.clearReplOutput(workspaceLocation)); - yield put(actions.debuggerReset(workspaceLocation)); - if (workspaceLocation !== 'stories') yield put(actions.resetSideContent(workspaceLocation)); - yield call( - showSuccessMessage, - `Switched to ${styliseSublanguage(newChapter, newVariant)}`, - 1000 - ); - } - }, - /** - * Note that the PLAYGROUND_EXTERNAL_SELECT action is made to - * select the library for playground. - * This is because assessments do not have a chapter & library select, the question - * specifies the chapter and library to be used. - * - * To abstract this to assessments, the state structure must be manipulated to store - * the external library name in a WorkspaceState (as compared to IWorkspaceManagerState). - * - * @see IWorkspaceManagerState @see WorkspaceState - */ - externalLibrarySelect: function* (action) { - const { workspaceLocation, externalLibraryName: newExternalLibraryName } = action.payload; - const [chapter, globals, oldExternalLibraryName]: [ - Chapter, - Array<[string, any]>, - ExternalLibraryName - ] = yield select((state: OverallState) => [ - state.workspaces[workspaceLocation].context.chapter, - state.workspaces[workspaceLocation].globals, - state.workspaces[workspaceLocation].externalLibrary - ]); - const symbols = externalLibraries.get(newExternalLibraryName)!; + const chapterChanged: boolean = newChapter !== oldChapter || newVariant !== oldVariant; + const toChangeChapter: boolean = + newChapter === Chapter.FULL_JS + ? chapterChanged && (yield call(showFullJSDisclaimer)) + : newChapter === Chapter.FULL_TS + ? chapterChanged && (yield call(showFullTSDisclaimer)) + : chapterChanged; + + if (toChangeChapter) { const library: Library = { - chapter, + chapter: newChapter, + variant: newVariant, external: { - name: newExternalLibraryName, + name: externalLibraryName, symbols }, globals }; - if (newExternalLibraryName !== oldExternalLibraryName || action.payload.initialise) { - yield put(actions.changeExternalLibrary(newExternalLibraryName, workspaceLocation)); - yield put(actions.beginClearContext(workspaceLocation, library, true)); - yield put(actions.clearReplOutput(workspaceLocation)); - if (!action.payload.initialise) { - yield call(showSuccessMessage, `Switched to ${newExternalLibraryName} library`, 1000); - } - } - }, - /** - * Handles the side effect of resetting the WebGL context when context is reset. - * - * @see webGLgraphics.js under 'public/externalLibs/graphics' for information on - * the function. - */ - beginClearContext: function* (action): any { - yield call([DataVisualizer, DataVisualizer.clear]); - yield call([CseMachine, CseMachine.clear]); - const globals: Array<[string, any]> = action.payload.library.globals as Array<[string, any]>; - for (const [key, value] of globals) { - window[key as any] = value; + yield put(actions.beginClearContext(workspaceLocation, library, false)); + yield put(actions.clearReplOutput(workspaceLocation)); + yield put(actions.debuggerReset(workspaceLocation)); + if (workspaceLocation !== 'stories') yield put(actions.resetSideContent(workspaceLocation)); + yield call( + showSuccessMessage, + `Switched to ${styliseSublanguage(newChapter, newVariant)}`, + 1000 + ); + } + }, + /** + * Note that the PLAYGROUND_EXTERNAL_SELECT action is made to + * select the library for playground. + * This is because assessments do not have a chapter & library select, the question + * specifies the chapter and library to be used. + * + * To abstract this to assessments, the state structure must be manipulated to store + * the external library name in a WorkspaceState (as compared to IWorkspaceManagerState). + * + * @see IWorkspaceManagerState @see WorkspaceState + */ + [WorkspaceActions.externalLibrarySelect.type]: function* (action) { + const { workspaceLocation, externalLibraryName: newExternalLibraryName } = action.payload; + const [chapter, globals, oldExternalLibraryName]: [ + Chapter, + Array<[string, any]>, + ExternalLibraryName + ] = yield select((state: OverallState) => [ + state.workspaces[workspaceLocation].context.chapter, + state.workspaces[workspaceLocation].globals, + state.workspaces[workspaceLocation].externalLibrary + ]); + const symbols = externalLibraries.get(newExternalLibraryName)!; + const library: Library = { + chapter, + external: { + name: newExternalLibraryName, + symbols + }, + globals + }; + if (newExternalLibraryName !== oldExternalLibraryName || action.payload.initialise) { + yield put(actions.changeExternalLibrary(newExternalLibraryName, workspaceLocation)); + yield put(actions.beginClearContext(workspaceLocation, library, true)); + yield put(actions.clearReplOutput(workspaceLocation)); + if (!action.payload.initialise) { + yield call(showSuccessMessage, `Switched to ${newExternalLibraryName} library`, 1000); } + } + }, + /** + * Handles the side effect of resetting the WebGL context when context is reset. + * + * @see webGLgraphics.js under 'public/externalLibs/graphics' for information on + * the function. + */ + [WorkspaceActions.beginClearContext.type]: function* (action): any { + yield call([DataVisualizer, DataVisualizer.clear]); + yield call([CseMachine, CseMachine.clear]); + const globals: Array<[string, any]> = action.payload.library.globals as Array<[string, any]>; + for (const [key, value] of globals) { + window[key as any] = value; + } + yield put( + actions.endClearContext( + { + ...action.payload.library, + moduleParams: { + runes: {}, + phaser: Phaser + } + }, + action.payload.workspaceLocation + ) + ); + yield undefined; + }, + [WorkspaceActions.navigateToDeclaration.type]: function* (action) { + const workspaceLocation = action.payload.workspaceLocation; + const code: string = yield select( + // TODO: Hardcoded to make use of the first editor tab. Rewrite after editor tabs are added. + (state: OverallState) => state.workspaces[workspaceLocation].editorTabs[0].value + ); + const context: Context = yield select( + (state: OverallState) => state.workspaces[workspaceLocation].context + ); + + const result = findDeclaration(code, context, { + line: action.payload.cursorPosition.row + 1, + column: action.payload.cursorPosition.column + }); + if (result) { + // TODO: Hardcoded to make use of the first editor tab. Rewrite after editor tabs are added. yield put( - actions.endClearContext( - { - ...action.payload.library, - moduleParams: { - runes: {}, - phaser: Phaser - } - }, - action.payload.workspaceLocation - ) - ); - yield undefined; - }, - navigateToDeclaration: function* (action) { - const workspaceLocation = action.payload.workspaceLocation; - const code: string = yield select( - // TODO: Hardcoded to make use of the first editor tab. Rewrite after editor tabs are added. - (state: OverallState) => state.workspaces[workspaceLocation].editorTabs[0].value + actions.moveCursor(action.payload.workspaceLocation, 0, { + row: result.start.line - 1, + column: result.start.column + }) ); - const context: Context = yield select( - (state: OverallState) => state.workspaces[workspaceLocation].context - ); - - const result = findDeclaration(code, context, { - line: action.payload.cursorPosition.row + 1, - column: action.payload.cursorPosition.column - }); - if (result) { - // TODO: Hardcoded to make use of the first editor tab. Rewrite after editor tabs are added. - yield put( - actions.moveCursor(action.payload.workspaceLocation, 0, { - row: result.start.line - 1, - column: result.start.column - }) - ); - } - }, - // TODO: Should be takeLeading, not takeEvery - runAllTestcases: function* (action): any { + } + }, + [WorkspaceActions.runAllTestcases.type]: { + takeLeading: function* (action): any { const { workspaceLocation } = action.payload; yield call(evalEditorSaga, workspaceLocation); @@ -492,6 +479,6 @@ const WorkspaceSaga = combineSagaHandlers( } } } -); +}); export default WorkspaceSaga; diff --git a/src/commons/utils/JsSlangHelper.ts b/src/commons/utils/JsSlangHelper.ts index ab88e89db8..dab9f04110 100644 --- a/src/commons/utils/JsSlangHelper.ts +++ b/src/commons/utils/JsSlangHelper.ts @@ -1,12 +1,18 @@ /* tslint:disable: ban-types*/ import createSlangContext, { defineBuiltin, importBuiltins } from 'js-slang/dist/createContext'; -import { Chapter, Context, CustomBuiltIns, Value, Variant } from 'js-slang/dist/types'; +import { + type Chapter, + type Context, + type CustomBuiltIns, + type Value, + Variant +} from 'js-slang/dist/types'; import { stringify } from 'js-slang/dist/utils/stringify'; import { difference, keys } from 'lodash'; import CseMachine from 'src/features/cseMachine/CseMachine'; import DataVisualizer from '../../features/dataVisualizer/dataVisualizer'; -import { Data } from '../../features/dataVisualizer/dataVisualizerTypes'; +import type { Data } from '../../features/dataVisualizer/dataVisualizerTypes'; import DisplayBufferService from './DisplayBufferService'; /** diff --git a/src/commons/utils/TypeHelper.ts b/src/commons/utils/TypeHelper.ts index b4b2a26e65..5bcef66e47 100644 --- a/src/commons/utils/TypeHelper.ts +++ b/src/commons/utils/TypeHelper.ts @@ -1,3 +1,5 @@ +import type { actions, SourceActionType } from './ActionsHelper'; + export type MaybePromise = T extends Promise ? V : U; export type PromiseResolveType = MaybePromise; @@ -174,3 +176,8 @@ type DeconstructRecord> = Exclude< export function objectEntries>(obj: T) { return Object.entries(obj) as DeconstructRecord; } + +export type ActionTypeToCreator = Extract< + (typeof actions)[keyof typeof actions], + (...args: any[]) => { type: T } +>; diff --git a/src/features/achievement/AchievementActions.ts b/src/features/achievement/AchievementActions.ts index 9509b9481d..cf16f35686 100644 --- a/src/features/achievement/AchievementActions.ts +++ b/src/features/achievement/AchievementActions.ts @@ -1,13 +1,14 @@ -import { AssessmentOverview } from 'src/commons/assessment/AssessmentTypes'; +import type { AssessmentOverview } from 'src/commons/assessment/AssessmentTypes'; import { createActions } from 'src/commons/redux/utils'; +import type { SideContentLocation } from 'src/commons/sideContent/SideContentTypes'; import { - AchievementGoal, - AchievementItem, - AchievementUser, + type AchievementGoal, + type AchievementItem, + type AchievementUser, EventType, - GoalDefinition, - GoalProgress + type GoalDefinition, + type GoalProgress } from './AchievementTypes'; const AchievementActions = createActions('achievement', { @@ -21,7 +22,10 @@ const AchievementActions = createActions('achievement', { removeAchievement: (uuid: string) => uuid, removeGoal: (uuid: string) => uuid, updateOwnGoalProgress: (progress: GoalProgress) => progress, - addEvent: (eventNames: EventType[]) => eventNames, + addEvent: (eventNames: EventType[], workspaceLocation?: SideContentLocation) => ({ + workspaceLocation, + eventNames + }), handleEvent: (loggedEvents: EventType[][]) => loggedEvents, updateGoalProgress: (studentCourseRegId: number, progress: GoalProgress) => ({ studentCourseRegId, From a06401e38e0352c810ab71d27598f1fded45bce5 Mon Sep 17 00:00:00 2001 From: "DESKTOP-G08HS3B\\Lee Yi" Date: Mon, 28 Apr 2025 14:11:12 -0400 Subject: [PATCH 11/14] Add redux tests --- src/commons/redux/__tests__/utils.ts | 43 ++++++++++++++++++++++++++++ src/commons/redux/utils.ts | 4 +-- 2 files changed, 45 insertions(+), 2 deletions(-) create mode 100644 src/commons/redux/__tests__/utils.ts diff --git a/src/commons/redux/__tests__/utils.ts b/src/commons/redux/__tests__/utils.ts new file mode 100644 index 0000000000..59944c4f52 --- /dev/null +++ b/src/commons/redux/__tests__/utils.ts @@ -0,0 +1,43 @@ +import { testSaga } from "redux-saga-test-plan" +import WorkspaceActions from "src/commons/workspace/WorkspaceActions" + +import { combineSagaHandlers } from "../utils" + +// Would have used spyOn, but for some reason that doesn't work properly +jest.mock('src/commons/sagas/SafeEffects', () => ({ + ...jest.requireActual('src/commons/sagas/SafeEffects'), + // Mock wrap saga to just be a passthrough so that the identity + // checking that testSaga uses will pass + wrapSaga: (x: any) => x +})) + +test('test combineSagaHandlers', () => { + const mockTakeEveryHandler = jest.fn() + const mockTakeLatestHandler = jest.fn() + const mockTakeLeadingHandler = jest.fn() + + const saga = combineSagaHandlers({ + [WorkspaceActions.toggleUsingUpload.type]: mockTakeEveryHandler, + [WorkspaceActions.toggleFolderMode.type]: { + takeEvery: mockTakeEveryHandler + }, + [WorkspaceActions.toggleUsingCse.type]: { + takeLatest: mockTakeLatestHandler + }, + [WorkspaceActions.toggleUsingSubst.type]: { + takeLeading: mockTakeLeadingHandler + } + }) + + testSaga(saga) + .next() + .takeEvery(WorkspaceActions.toggleUsingUpload.type, mockTakeEveryHandler) + .next() + .takeEvery(WorkspaceActions.toggleFolderMode.type, mockTakeEveryHandler) + .next() + .takeLatest(WorkspaceActions.toggleUsingCse.type, mockTakeLatestHandler) + .next() + .takeLeading(WorkspaceActions.toggleUsingSubst.type, mockTakeLeadingHandler) + .next() + .isDone() +}) diff --git a/src/commons/redux/utils.ts b/src/commons/redux/utils.ts index b68e25f0f1..998193500d 100644 --- a/src/commons/redux/utils.ts +++ b/src/commons/redux/utils.ts @@ -9,7 +9,7 @@ import { type StrictEffect, takeEvery, takeLatest, takeLeading } from 'redux-sag import { safeTakeEvery, wrapSaga } from '../sagas/SafeEffects'; import type { SourceActionType } from '../utils/ActionsHelper'; -import { ActionTypeToCreator, objectEntries } from '../utils/TypeHelper'; +import { type ActionTypeToCreator, objectEntries } from '../utils/TypeHelper'; /** * Creates actions, given a base name and base actions @@ -54,7 +54,7 @@ type SagaHandlers = { export function combineSagaHandlers(handlers: SagaHandlers) { return function* (): SagaIterator { - for (const [actionName, saga] of objectEntries(handlers as SagaHandlers)) { + for (const [actionName, saga] of objectEntries(handlers)) { if (saga === undefined) { continue; } else if (typeof saga === 'function') { From cb934b0e5d5db94c24235c82bb726d94585dd282 Mon Sep 17 00:00:00 2001 From: Lee Yi Date: Mon, 28 Apr 2025 15:25:37 -0400 Subject: [PATCH 12/14] Add more tests --- src/commons/redux/__tests__/utils.ts | 28 ++++++++++++++++- src/commons/redux/utils.ts | 4 +-- src/commons/sagas/__tests__/SafeEffects.ts | 35 ++++++++++++++++++++++ 3 files changed, 64 insertions(+), 3 deletions(-) create mode 100644 src/commons/sagas/__tests__/SafeEffects.ts diff --git a/src/commons/redux/__tests__/utils.ts b/src/commons/redux/__tests__/utils.ts index 59944c4f52..75b08414b2 100644 --- a/src/commons/redux/__tests__/utils.ts +++ b/src/commons/redux/__tests__/utils.ts @@ -1,7 +1,7 @@ import { testSaga } from "redux-saga-test-plan" import WorkspaceActions from "src/commons/workspace/WorkspaceActions" -import { combineSagaHandlers } from "../utils" +import { combineSagaHandlers, createActions } from "../utils" // Would have used spyOn, but for some reason that doesn't work properly jest.mock('src/commons/sagas/SafeEffects', () => ({ @@ -26,6 +26,10 @@ test('test combineSagaHandlers', () => { }, [WorkspaceActions.toggleUsingSubst.type]: { takeLeading: mockTakeLeadingHandler + }, + [WorkspaceActions.toggleEditorAutorun.type]: { + takeEvery: mockTakeEveryHandler, + takeLeading: mockTakeLeadingHandler } }) @@ -39,5 +43,27 @@ test('test combineSagaHandlers', () => { .next() .takeLeading(WorkspaceActions.toggleUsingSubst.type, mockTakeLeadingHandler) .next() + .takeEvery(WorkspaceActions.toggleEditorAutorun.type, mockTakeEveryHandler) + .next() + .takeLeading(WorkspaceActions.toggleEditorAutorun.type, mockTakeLeadingHandler) + .next() .isDone() }) + +test('createActions', () => { + const actions = createActions('workspace', { + act0: false, + act1: (value: string) => ({ value }), + act2: 525600 + }) + + const act0 = actions.act0() + expect(act0.type).toEqual('workspace/act0') + + const act1 = actions.act1('test') + expect(act1.type).toEqual('workspace/act1') + expect(act1.payload).toMatchObject({ value: 'test' }) + + const act2 = actions.act2() + expect(act2.type).toEqual('workspace/act2') +}) diff --git a/src/commons/redux/utils.ts b/src/commons/redux/utils.ts index 998193500d..0bcf0ed68e 100644 --- a/src/commons/redux/utils.ts +++ b/src/commons/redux/utils.ts @@ -14,7 +14,7 @@ import { type ActionTypeToCreator, objectEntries } from '../utils/TypeHelper'; /** * Creates actions, given a base name and base actions * @param baseName The base name of the actions - * @param baseActions The base actions. Use a falsy value to create an action without a payload. + * @param baseActions The base actions. Use a non function value to create an action without a payload. * @returns An object containing the actions */ export function createActions>( @@ -24,7 +24,7 @@ export function createActions ({ ...res, - [name]: func + [name]: typeof func === 'function' ? createAction(`${baseName}/${name}`, (...args: any) => ({ payload: func(...args) })) : createAction(`${baseName}/${name}`) }), diff --git a/src/commons/sagas/__tests__/SafeEffects.ts b/src/commons/sagas/__tests__/SafeEffects.ts new file mode 100644 index 0000000000..7e785baf53 --- /dev/null +++ b/src/commons/sagas/__tests__/SafeEffects.ts @@ -0,0 +1,35 @@ +import * as Sentry from '@sentry/browser' +import { call } from "redux-saga/effects" +import { expectSaga } from "redux-saga-test-plan" + +import { wrapSaga } from "../SafeEffects" + +jest.spyOn(Sentry, 'captureException') + +// Silence console error +jest.spyOn(console, 'error').mockImplementation(x => {}) + +describe('Test wrapSaga', () => { + test('wrapSaga is transparent', async () => { + const mockFn = jest.fn() + const wrappedSaga = wrapSaga(function* () { + yield call(mockFn) + }) + + await expectSaga(wrappedSaga).silentRun() + + expect(mockFn).toHaveBeenCalledTimes(1) + }) + + test('wrapSaga handles errors appropriately', async () => { + const errorToThrow = new Error() + const wrappedSaga = wrapSaga(function* () { + throw errorToThrow + }) + + await expectSaga(wrappedSaga).silentRun() + + expect(Sentry.captureException).toHaveBeenCalledWith(errorToThrow) + expect(console.error).toHaveBeenCalledTimes(1) + }) +}) From 62a37aa92f1e0f27797bca8fc49f32b300cd1919 Mon Sep 17 00:00:00 2001 From: Lee Yi Date: Mon, 28 Apr 2025 16:23:52 -0400 Subject: [PATCH 13/14] Modify actions and sagas to use new redux utils --- .../application/actions/SessionActions.ts | 16 +- src/commons/redux/utils.ts | 4 +- src/commons/sagas/PersistenceSaga.tsx | 419 +++++++++--------- src/commons/sagas/RemoteExecutionSaga.ts | 1 - src/commons/utils/ActionsHelper.ts | 4 +- src/commons/workspace/WorkspaceActions.ts | 56 +-- src/commons/workspace/WorkspaceReducer.ts | 2 +- src/commons/workspace/WorkspaceTypes.ts | 17 +- src/features/github/GitHubActions.ts | 11 +- .../persistence/PersistenceActions.ts | 27 +- src/features/persistence/PersistenceTypes.ts | 5 - src/pages/playground/Playground.tsx | 45 +- 12 files changed, 273 insertions(+), 334 deletions(-) diff --git a/src/commons/application/actions/SessionActions.ts b/src/commons/application/actions/SessionActions.ts index 1fc5590ce5..af363a0c91 100644 --- a/src/commons/application/actions/SessionActions.ts +++ b/src/commons/application/actions/SessionActions.ts @@ -4,27 +4,27 @@ import { unpublishedToBackendParams } from 'src/features/grading/GradingUtils'; import { freshSortState } from 'src/pages/academy/grading/subcomponents/GradingSubmissionsTable'; -import { OptionType } from 'src/pages/academy/teamFormation/subcomponents/TeamFormationForm'; +import type { OptionType } from 'src/pages/academy/teamFormation/subcomponents/TeamFormationForm'; -import { +import type { AllColsSortStates, GradingOverviews, GradingQuery } from '../../../features/grading/GradingTypes'; -import { TeamFormationOverview } from '../../../features/teamFormation/TeamFormationTypes'; -import { +import type { TeamFormationOverview } from '../../../features/teamFormation/TeamFormationTypes'; +import type { Assessment, AssessmentConfiguration, AssessmentOverview, ContestEntry } from '../../assessment/AssessmentTypes'; -import { +import type { Notification, NotificationFilterFunction } from '../../notificationBadge/NotificationBadgeTypes'; import { generateOctokitInstance } from '../../utils/GitHubPersistenceHelper'; import { Role, StoriesRole } from '../ApplicationTypes'; -import { +import type { AdminPanelCourseRegistration, CourseRegistration, Tokens, @@ -153,6 +153,4 @@ const SessionActions = createActions('session', { }); // For compatibility with existing code (actions helper) -export default { - ...SessionActions -}; +export default SessionActions diff --git a/src/commons/redux/utils.ts b/src/commons/redux/utils.ts index 0bcf0ed68e..c14b9cbd42 100644 --- a/src/commons/redux/utils.ts +++ b/src/commons/redux/utils.ts @@ -28,7 +28,7 @@ export function createActions ({ payload: func(...args) })) : createAction(`${baseName}/${name}`) }), - {} as { + {} as Readonly<{ [K in keyof BaseActions]: K extends string ? BaseActions[K] extends (...args: any) => any ? ActionCreatorWithPreparedPayload< @@ -38,7 +38,7 @@ export function createActions : ActionCreatorWithoutPayload<`${BaseName}/${K}`> : never; - } + }> ); } diff --git a/src/commons/sagas/PersistenceSaga.tsx b/src/commons/sagas/PersistenceSaga.tsx index 8ae0d04def..5a3ee8cd24 100644 --- a/src/commons/sagas/PersistenceSaga.tsx +++ b/src/commons/sagas/PersistenceSaga.tsx @@ -1,19 +1,12 @@ import { Intent } from '@blueprintjs/core'; import { Chapter, Variant } from 'js-slang/dist/types'; -import type { SagaIterator } from 'redux-saga'; import { call, put, select } from 'redux-saga/effects'; -import { - PERSISTENCE_INITIALISE, - PERSISTENCE_OPEN_PICKER, - PERSISTENCE_SAVE_FILE, - PERSISTENCE_SAVE_FILE_AS, - type PersistenceFile -} from '../../features/persistence/PersistenceTypes'; +import type { PersistenceFile } from '../../features/persistence/PersistenceTypes'; import { store } from '../../pages/createStore'; -import SessionActions from '../application/actions/SessionActions'; import type { OverallState } from '../application/ApplicationTypes'; import { ExternalLibraryName } from '../application/types/ExternalTypes'; +import { combineSagaHandlers } from '../redux/utils'; import { actions } from '../utils/ActionsHelper'; import Constants from '../utils/Constants'; import { showSimpleConfirmDialog, showSimplePromptDialog } from '../utils/DialogHelper'; @@ -24,7 +17,7 @@ import { showWarningMessage } from '../utils/notifications/NotificationsHelper'; import type { AsyncReturnType } from '../utils/TypeHelper'; -import { safeTakeEvery as takeEvery, safeTakeLatest as takeLatest } from './SafeEffects'; +import { selectWorkspace } from './SafeEffects'; const DISCOVERY_DOCS = ['https://www.googleapis.com/discovery/v1/apis/drive/v3/rest']; const SCOPES = 'profile https://www.googleapis.com/auth/drive.file'; @@ -36,217 +29,90 @@ const ROOT_ID = 'root'; const MIME_SOURCE = 'text/plain'; // const MIME_FOLDER = 'application/vnd.google-apps.folder'; -export function* persistenceSaga(): SagaIterator { - yield takeLatest(SessionActions.logoutGoogle.type, function* () { - yield put(actions.playgroundUpdatePersistenceFile(undefined)); - yield call(ensureInitialised); - yield call([gapi.auth2.getAuthInstance(), 'signOut']); - }); - - yield takeLatest(PERSISTENCE_OPEN_PICKER, function* (): any { - let toastKey: string | undefined; - try { - yield call(ensureInitialisedAndAuthorised); - - const { id, name, picked } = yield call(pickFile, 'Pick a file to open'); - if (!picked) { - return; - } - const confirmOpen: boolean = yield call(showSimpleConfirmDialog, { - title: 'Opening from Google Drive', - contents: ( -

- Opening {name} will overwrite the current contents of your workspace. - Are you sure? -

- ), - positiveLabel: 'Open', - negativeLabel: 'Cancel' - }); - if (!confirmOpen) { - return; - } - - toastKey = yield call(showMessage, { - message: 'Opening file...', - timeout: 0, - intent: Intent.PRIMARY - }); - - const { result: meta } = yield call([gapi.client.drive.files, 'get'], { - fileId: id, - fields: 'appProperties' - }); - const contents = yield call([gapi.client.drive.files, 'get'], { fileId: id, alt: 'media' }); - const activeEditorTabIndex: number | null = yield select( - (state: OverallState) => state.workspaces.playground.activeEditorTabIndex - ); - if (activeEditorTabIndex === null) { - throw new Error('No active editor tab found.'); - } - yield put(actions.updateEditorValue('playground', activeEditorTabIndex, contents.body)); - yield put(actions.playgroundUpdatePersistenceFile({ id, name, lastSaved: new Date() })); - if (meta && meta.appProperties) { - yield put( - actions.chapterSelect( - parseInt(meta.appProperties.chapter || '4', 10) as Chapter, - meta.appProperties.variant || Variant.DEFAULT, - 'playground' - ) - ); - yield put( - actions.externalLibrarySelect( - Object.values(ExternalLibraryName).find(v => v === meta.appProperties.external) || - ExternalLibraryName.NONE, - 'playground' - ) - ); - } - - yield call(showSuccessMessage, `Loaded ${name}.`, 1000); - } catch (ex) { - console.error(ex); - yield call(showWarningMessage, `Error while opening file.`, 1000); - } finally { - if (toastKey) { - dismiss(toastKey); - } +const PersistenceSaga = combineSagaHandlers({ + [actions.logoutGoogle.type]: { + takeLatest: function* () { + yield put(actions.playgroundUpdatePersistenceFile(undefined)); + yield call(ensureInitialised); + yield call([gapi.auth2.getAuthInstance(), 'signOut']); } - }); - - yield takeLatest(PERSISTENCE_SAVE_FILE_AS, function* (): any { - let toastKey: string | undefined; - try { - yield call(ensureInitialisedAndAuthorised); - - const [activeEditorTabIndex, editorTabs, chapter, variant, external] = yield select( - (state: OverallState) => [ - state.workspaces.playground.activeEditorTabIndex, - state.workspaces.playground.editorTabs, - state.workspaces.playground.context.chapter, - state.workspaces.playground.context.variant, - state.workspaces.playground.externalLibrary - ] - ); - - if (activeEditorTabIndex === null) { - throw new Error('No active editor tab found.'); - } - const code = editorTabs[activeEditorTabIndex].value; - - const pickedDir: PickFileResult = yield call( - pickFile, - 'Pick a folder, or cancel to pick the root folder', - { - pickFolders: true, - showFolders: true, - showFiles: false - } - ); + }, + [actions.persistenceOpenPicker.type]: { + takeLatest: function* () { + let toastKey: string | undefined; + try { + yield call(ensureInitialisedAndAuthorised); - const saveToDir: PersistenceFile = pickedDir.picked - ? pickedDir - : { id: ROOT_ID, name: 'My Drive' }; - - const pickedFile: PickFileResult = yield call( - pickFile, - `Saving to ${saveToDir.name}; pick a file to overwrite, or cancel to save as a new file`, - { - pickFolders: false, - showFolders: false, - showFiles: true, - rootFolder: saveToDir.id + const { id, name, picked } = yield call(pickFile, 'Pick a file to open'); + if (!picked) { + return; } - ); - - if (pickedFile.picked) { - const reallyOverwrite: boolean = yield call(showSimpleConfirmDialog, { - title: 'Saving to Google Drive', + const confirmOpen: boolean = yield call(showSimpleConfirmDialog, { + title: 'Opening from Google Drive', contents: ( - - Really overwrite {pickedFile.name}? - - ) +

+ Opening {name} will overwrite the current contents of your workspace. + Are you sure? +

+ ), + positiveLabel: 'Open', + negativeLabel: 'Cancel' }); - if (!reallyOverwrite) { - return; - } - yield put(actions.playgroundUpdatePersistenceFile(pickedFile)); - yield put(actions.persistenceSaveFile(pickedFile)); - } else { - const response: AsyncReturnType = yield call( - showSimplePromptDialog, - { - title: 'Saving to Google Drive', - contents: ( - <> -

- Saving to folder {saveToDir.name}. -

-

Save as name?

- - ), - positiveLabel: 'Save as new file', - negativeLabel: 'Cancel', - props: { - validationFunction: value => !!value - } - } - ); - - if (!response.buttonResponse) { + if (!confirmOpen) { return; } - const config: IPlaygroundConfig = { - chapter, - variant, - external - }; - toastKey = yield call(showMessage, { - message: `Saving as ${response.value}...`, + message: 'Opening file...', timeout: 0, intent: Intent.PRIMARY }); - const newFile = yield call( - createFile, - response.value, - saveToDir.id, - MIME_SOURCE, - code, - config + const { result: meta } = yield call([gapi.client.drive.files, 'get'], { + fileId: id, + fields: 'appProperties' + }); + const contents = yield call([gapi.client.drive.files, 'get'], { fileId: id, alt: 'media' }); + const activeEditorTabIndex: number | null = yield select( + (state: OverallState) => state.workspaces.playground.activeEditorTabIndex ); + if (activeEditorTabIndex === null) { + throw new Error('No active editor tab found.'); + } + yield put(actions.updateEditorValue('playground', activeEditorTabIndex, contents.body)); + yield put(actions.playgroundUpdatePersistenceFile({ id, name, lastSaved: new Date() })); + if (meta && meta.appProperties) { + yield put( + actions.chapterSelect( + parseInt(meta.appProperties.chapter || '4', 10) as Chapter, + meta.appProperties.variant || Variant.DEFAULT, + 'playground' + ) + ); + yield put( + actions.externalLibrarySelect( + Object.values(ExternalLibraryName).find(v => v === meta.appProperties.external) || + ExternalLibraryName.NONE, + 'playground' + ) + ); + } - yield put(actions.playgroundUpdatePersistenceFile({ ...newFile, lastSaved: new Date() })); - yield call( - showSuccessMessage, - `${response.value} successfully saved to Google Drive.`, - 1000 - ); - } - } catch (ex) { - console.error(ex); - yield call(showWarningMessage, `Error while saving file.`, 1000); - } finally { - if (toastKey) { - dismiss(toastKey); + yield call(showSuccessMessage, `Loaded ${name}.`, 1000); + } catch (ex) { + console.error(ex); + yield call(showWarningMessage, `Error while opening file.`, 1000); + } finally { + if (toastKey) { + dismiss(toastKey); + } } } - }); - - yield takeEvery( - PERSISTENCE_SAVE_FILE, - function* ({ payload: { id, name } }: ReturnType) { + }, + [actions.persistenceSaveFileAs.type]: { + takeLatest: function* () { let toastKey: string | undefined; try { - toastKey = yield call(showMessage, { - message: `Saving as ${name}...`, - timeout: 0, - intent: Intent.PRIMARY - }); - yield call(ensureInitialisedAndAuthorised); const [activeEditorTabIndex, editorTabs, chapter, variant, external] = yield select( @@ -264,14 +130,98 @@ export function* persistenceSaga(): SagaIterator { } const code = editorTabs[activeEditorTabIndex].value; - const config: IPlaygroundConfig = { - chapter, - variant, - external - }; - yield call(updateFile, id, name, MIME_SOURCE, code, config); - yield put(actions.playgroundUpdatePersistenceFile({ id, name, lastSaved: new Date() })); - yield call(showSuccessMessage, `${name} successfully saved to Google Drive.`, 1000); + const pickedDir: PickFileResult = yield call( + pickFile, + 'Pick a folder, or cancel to pick the root folder', + { + pickFolders: true, + showFolders: true, + showFiles: false + } + ); + + const saveToDir: PersistenceFile = pickedDir.picked + ? pickedDir + : { id: ROOT_ID, name: 'My Drive' }; + + const pickedFile: PickFileResult = yield call( + pickFile, + `Saving to ${saveToDir.name}; pick a file to overwrite, or cancel to save as a new file`, + { + pickFolders: false, + showFolders: false, + showFiles: true, + rootFolder: saveToDir.id + } + ); + + if (pickedFile.picked) { + const reallyOverwrite: boolean = yield call(showSimpleConfirmDialog, { + title: 'Saving to Google Drive', + contents: ( + + Really overwrite {pickedFile.name}? + + ) + }); + if (!reallyOverwrite) { + return; + } + yield put(actions.playgroundUpdatePersistenceFile(pickedFile)); + yield put(actions.persistenceSaveFile(pickedFile)); + } else { + const response: AsyncReturnType = yield call( + showSimplePromptDialog, + { + title: 'Saving to Google Drive', + contents: ( + <> +

+ Saving to folder {saveToDir.name}. +

+

Save as name?

+ + ), + positiveLabel: 'Save as new file', + negativeLabel: 'Cancel', + props: { + validationFunction: value => !!value + } + } + ); + + if (!response.buttonResponse) { + return; + } + + const config: IPlaygroundConfig = { + chapter, + variant, + external + }; + + toastKey = yield call(showMessage, { + message: `Saving as ${response.value}...`, + timeout: 0, + intent: Intent.PRIMARY + }); + + const newFile = yield call( + createFile, + response.value, + saveToDir.id, + MIME_SOURCE, + code, + config + ); + + yield put(actions.playgroundUpdatePersistenceFile({ ...newFile, lastSaved: new Date() })); + yield call( + showSuccessMessage, + `${response.value} successfully saved to Google Drive.`, + 1000 + ); + } } catch (ex) { console.error(ex); yield call(showWarningMessage, `Error while saving file.`, 1000); @@ -281,13 +231,52 @@ export function* persistenceSaga(): SagaIterator { } } } - ); + }, + [actions.persistenceSaveFile.type]: function* ({ payload: { id, name }}) { + let toastKey: string | undefined; + try { + toastKey = yield call(showMessage, { + message: `Saving as ${name}...`, + timeout: 0, + intent: Intent.PRIMARY + }); - yield takeEvery(PERSISTENCE_INITIALISE, ensureInitialised); -} + yield call(ensureInitialisedAndAuthorised); + + const { + activeEditorTabIndex, + editorTabs, + context: { chapter, variant }, + externalLibrary: external + } = yield* selectWorkspace('playground') + + if (activeEditorTabIndex === null) { + throw new Error('No active editor tab found.'); + } + const code = editorTabs[activeEditorTabIndex].value; + + const config: IPlaygroundConfig = { + chapter, + variant, + external + }; + yield call(updateFile, id, name, MIME_SOURCE, code, config); + yield put(actions.playgroundUpdatePersistenceFile({ id, name, lastSaved: new Date() })); + yield call(showSuccessMessage, `${name} successfully saved to Google Drive.`, 1000); + } catch (ex) { + console.error(ex); + yield call(showWarningMessage, `Error while saving file.`, 1000); + } finally { + if (toastKey) { + dismiss(toastKey); + } + } + }, + [actions.persistenceInitialise.type]: ensureInitialised as any +}) interface IPlaygroundConfig { - chapter: string; + chapter: Chapter; variant: string; external: string; } @@ -518,4 +507,4 @@ function generateBoundary(): string { // End adapted part -export default persistenceSaga; +export default PersistenceSaga; diff --git a/src/commons/sagas/RemoteExecutionSaga.ts b/src/commons/sagas/RemoteExecutionSaga.ts index f42f263435..a481665c6e 100644 --- a/src/commons/sagas/RemoteExecutionSaga.ts +++ b/src/commons/sagas/RemoteExecutionSaga.ts @@ -33,7 +33,6 @@ const dummyLocation = { end: { line: 0, column: 0 } }; -// TODO: Refactor and combine in a future commit const RemoteExecutionSaga = combineSagaHandlers({ [RemoteExecutionActions.remoteExecFetchDevices.type]: { takeLatest: function* () { diff --git a/src/commons/utils/ActionsHelper.ts b/src/commons/utils/ActionsHelper.ts index 33ff9e6aae..129fcc17a5 100644 --- a/src/commons/utils/ActionsHelper.ts +++ b/src/commons/utils/ActionsHelper.ts @@ -10,7 +10,7 @@ import AchievementActions from '../../features/achievement/AchievementActions'; import DashboardActions from '../../features/dashboard/DashboardActions'; import GitHubActions from '../../features/github/GitHubActions'; import GroundControlActions from '../../features/groundControl/GroundControlActions'; -import * as PersistenceActions from '../../features/persistence/PersistenceActions'; +import PersistenceActions from '../../features/persistence/PersistenceActions'; import PlaygroundActions from '../../features/playground/PlaygroundActions'; import RemoteExecutionActions from '../../features/remoteExecution/RemoteExecutionActions'; import SourcecastActions from '../../features/sourceRecorder/sourcecast/SourcecastActions'; @@ -19,7 +19,7 @@ import SourcereelActions from '../../features/sourceRecorder/sourcereel/Sourcere import StoriesActions from '../../features/stories/StoriesActions'; import VscodeActions from '../application/actions/VscodeActions'; import { FeatureFlagsActions } from '../featureFlags'; -import { ActionType } from './TypeHelper'; +import type { ActionType } from './TypeHelper'; export const actions = { ...AchievementActions, diff --git a/src/commons/workspace/WorkspaceActions.ts b/src/commons/workspace/WorkspaceActions.ts index 6c21ac1a28..74931ab63c 100644 --- a/src/commons/workspace/WorkspaceActions.ts +++ b/src/commons/workspace/WorkspaceActions.ts @@ -1,20 +1,16 @@ -import { createAction } from '@reduxjs/toolkit'; import type { Context } from 'js-slang'; import { Chapter, Variant } from 'js-slang/dist/types'; -import { AllColsSortStates, GradingColumnVisibility } from '../../features/grading/GradingTypes'; -import { SALanguage } from '../application/ApplicationTypes'; -import { ExternalLibraryName } from '../application/types/ExternalTypes'; -import { Library } from '../assessment/AssessmentTypes'; -import { HighlightedLines, Position } from '../editor/EditorTypes'; +import type { AllColsSortStates, GradingColumnVisibility } from '../../features/grading/GradingTypes'; +import type { SALanguage } from '../application/ApplicationTypes'; +import type { ExternalLibraryName } from '../application/types/ExternalTypes'; +import type { Library } from '../assessment/AssessmentTypes'; +import type { HighlightedLines, Position } from '../editor/EditorTypes'; import { createActions } from '../redux/utils'; -import { UploadResult } from '../sideContent/content/SideContentUpload'; -import { +import type { UploadResult } from '../sideContent/content/SideContentUpload'; +import type { EditorTabState, SubmissionsTableFilters, - TOGGLE_USING_UPLOAD, - UPDATE_LAST_DEBUGGER_RESULT, - UPLOAD_FILES, WorkspaceLocation, WorkspaceLocationsWithTools, WorkspaceState @@ -250,6 +246,9 @@ const newActions = createActions('workspace', { updateCse, workspaceLocation }), + toggleUsingUpload: (usingUpload: boolean, workspaceLocation: WorkspaceLocationsWithTools) => ({ + usingUpload, workspaceLocation + }), updateCurrentStep: (steps: number, workspaceLocation: WorkspaceLocation) => ({ steps, workspaceLocation @@ -266,6 +265,12 @@ const newActions = createActions('workspace', { changepointSteps, workspaceLocation }), + updateLastDebuggerResult: (lastDebuggerResult: any, workspaceLocation: WorkspaceLocation) => ({ + lastDebuggerResult, workspaceLocation + }), + uploadFiles: (files: UploadResult, workspaceLocation: WorkspaceLocation) => ({ + files, workspaceLocation + }), // For grading table increaseRequestCounter: 0, decreaseRequestCounter: 0, @@ -274,31 +279,4 @@ const newActions = createActions('workspace', { updateGradingColumnVisibility: (filters: GradingColumnVisibility) => ({ filters }) }); -export const updateLastDebuggerResult = createAction( - UPDATE_LAST_DEBUGGER_RESULT, - (lastDebuggerResult: any, workspaceLocation: WorkspaceLocation) => ({ - payload: { lastDebuggerResult, workspaceLocation } - }) -); - -export const toggleUsingUpload = createAction( - TOGGLE_USING_UPLOAD, - (usingUpload: boolean, workspaceLocation: WorkspaceLocationsWithTools) => ({ - payload: { usingUpload, workspaceLocation } - }) -); - -export const uploadFiles = createAction( - UPLOAD_FILES, - (files: UploadResult, workspaceLocation: WorkspaceLocation) => ({ - payload: { files, workspaceLocation } - }) -); - -// For compatibility with existing code (actions helper) -export default { - ...newActions, - updateLastDebuggerResult, - toggleUsingUpload, - uploadFiles -}; +export default newActions diff --git a/src/commons/workspace/WorkspaceReducer.ts b/src/commons/workspace/WorkspaceReducer.ts index 16c5cf316b..0883e7fb7f 100644 --- a/src/commons/workspace/WorkspaceReducer.ts +++ b/src/commons/workspace/WorkspaceReducer.ts @@ -160,7 +160,7 @@ const newWorkspaceReducer = createReducer(defaultWorkspaceManager, builder => { const workspaceLocation = getWorkspaceLocation(action); const tokens = state[workspaceLocation].tokenCount; const newOutputEntry: Partial = { - type: action.payload.type as 'result' | undefined, + type: action.payload.type as 'result', value: action.payload.value }; diff --git a/src/commons/workspace/WorkspaceTypes.ts b/src/commons/workspace/WorkspaceTypes.ts index b81d293b81..007f6ec058 100644 --- a/src/commons/workspace/WorkspaceTypes.ts +++ b/src/commons/workspace/WorkspaceTypes.ts @@ -1,18 +1,15 @@ import type { Context } from 'js-slang'; -import { AllColsSortStates, GradingColumnVisibility } from '../../features/grading/GradingTypes'; -import { SourcecastWorkspaceState } from '../../features/sourceRecorder/sourcecast/SourcecastTypes'; -import { SourcereelWorkspaceState } from '../../features/sourceRecorder/sourcereel/SourcereelTypes'; -import { InterpreterOutput } from '../application/ApplicationTypes'; +import type { AllColsSortStates, GradingColumnVisibility } from '../../features/grading/GradingTypes'; +import type { SourcecastWorkspaceState } from '../../features/sourceRecorder/sourcecast/SourcecastTypes'; +import type { SourcereelWorkspaceState } from '../../features/sourceRecorder/sourcereel/SourcereelTypes'; +import type { InterpreterOutput } from '../application/ApplicationTypes'; import { ExternalLibraryName } from '../application/types/ExternalTypes'; -import { AutogradingResult, Testcase } from '../assessment/AssessmentTypes'; -import { HighlightedLines, Position } from '../editor/EditorTypes'; -import { UploadResult } from '../sideContent/content/SideContentUpload'; +import type { AutogradingResult, Testcase } from '../assessment/AssessmentTypes'; +import type { HighlightedLines, Position } from '../editor/EditorTypes'; +import type { UploadResult } from '../sideContent/content/SideContentUpload'; export const EVAL_SILENT = 'EVAL_SILENT'; -export const UPDATE_LAST_DEBUGGER_RESULT = 'UPDATE_LAST_DEBUGGER_RESULT'; -export const TOGGLE_USING_UPLOAD = 'TOGGLE_USING_UPLOAD'; -export const UPLOAD_FILES = 'UPLOAD_FILES'; export type WorkspaceLocation = keyof WorkspaceManagerState; export type WorkspaceLocationsWithTools = Extract; diff --git a/src/features/github/GitHubActions.ts b/src/features/github/GitHubActions.ts index 397a1030bd..aae238c25b 100644 --- a/src/features/github/GitHubActions.ts +++ b/src/features/github/GitHubActions.ts @@ -1,12 +1,9 @@ import { createActions } from 'src/commons/redux/utils'; const newActions = createActions('github', { - githubOpenFile: () => ({}), - githubSaveFile: () => ({}), - githubSaveFileAs: () => ({}) + githubOpenFile: 0, + githubSaveFile: 0, + githubSaveFileAs: 0 }); -// For compatibility with existing code (actions helper) -export default { - ...newActions -}; +export default newActions diff --git a/src/features/persistence/PersistenceActions.ts b/src/features/persistence/PersistenceActions.ts index 1670422c7a..caab699b8f 100644 --- a/src/features/persistence/PersistenceActions.ts +++ b/src/features/persistence/PersistenceActions.ts @@ -1,21 +1,12 @@ -import { createAction } from '@reduxjs/toolkit'; +import { createActions } from 'src/commons/redux/utils'; -import { - PERSISTENCE_INITIALISE, - PERSISTENCE_OPEN_PICKER, - PERSISTENCE_SAVE_FILE, - PERSISTENCE_SAVE_FILE_AS, - PersistenceFile -} from './PersistenceTypes'; +import type { PersistenceFile } from './PersistenceTypes'; -export const persistenceOpenPicker = createAction(PERSISTENCE_OPEN_PICKER, () => ({ payload: {} })); +const PersistenceActions = createActions('persistence', { + persistenceOpenPicker: true, + persistenceSaveFile: (file: PersistenceFile) => file, + persistenceSaveFileAs: true, + persistenceInitialise: true, +}) -export const persistenceSaveFile = createAction(PERSISTENCE_SAVE_FILE, (file: PersistenceFile) => ({ - payload: file -})); - -export const persistenceSaveFileAs = createAction(PERSISTENCE_SAVE_FILE_AS, () => ({ - payload: {} -})); - -export const persistenceInitialise = createAction(PERSISTENCE_INITIALISE, () => ({ payload: {} })); +export default PersistenceActions diff --git a/src/features/persistence/PersistenceTypes.ts b/src/features/persistence/PersistenceTypes.ts index 08f915c4cf..8919c50d2a 100644 --- a/src/features/persistence/PersistenceTypes.ts +++ b/src/features/persistence/PersistenceTypes.ts @@ -1,8 +1,3 @@ -export const PERSISTENCE_OPEN_PICKER = 'PERSISTENCE_OPEN_PICKER'; -export const PERSISTENCE_SAVE_FILE_AS = 'PERSISTENCE_SAVE_FILE_AS'; -export const PERSISTENCE_SAVE_FILE = 'PERSISTENCE_SAVE_FILE'; -export const PERSISTENCE_INITIALISE = 'PERSISTENCE_INITIALISE'; - export type PersistenceState = 'INACTIVE' | 'SAVED' | 'DIRTY'; export type PersistenceFile = { diff --git a/src/pages/playground/Playground.tsx b/src/pages/playground/Playground.tsx index 58aeabeebd..d24f820dde 100644 --- a/src/pages/playground/Playground.tsx +++ b/src/pages/playground/Playground.tsx @@ -1,9 +1,9 @@ import { Classes } from '@blueprintjs/core'; import { IconNames } from '@blueprintjs/icons'; -import { HotkeyItem, useHotkeys } from '@mantine/hooks'; -import { AnyAction, Dispatch } from '@reduxjs/toolkit'; +import { type HotkeyItem, useHotkeys } from '@mantine/hooks'; +import type { AnyAction, Dispatch } from '@reduxjs/toolkit'; import { Ace, Range } from 'ace-builds'; -import { FSModule } from 'browserfs/dist/node/core/FS'; +import type { FSModule } from 'browserfs/dist/node/core/FS'; import classNames from 'classnames'; import { Chapter, Variant } from 'js-slang/dist/types'; import { isEqual } from 'lodash'; @@ -31,15 +31,10 @@ import { showFulTSWarningOnUrlLoad, showHTMLDisclaimer } from 'src/commons/utils/WarningDialogHelper'; -import WorkspaceActions, { uploadFiles } from 'src/commons/workspace/WorkspaceActions'; -import { WorkspaceLocation } from 'src/commons/workspace/WorkspaceTypes'; +import WorkspaceActions from 'src/commons/workspace/WorkspaceActions'; +import type { WorkspaceLocation } from 'src/commons/workspace/WorkspaceTypes'; import GithubActions from 'src/features/github/GitHubActions'; -import { - persistenceInitialise, - persistenceOpenPicker, - persistenceSaveFile, - persistenceSaveFileAs -} from 'src/features/persistence/PersistenceActions'; +import PersistenceActions from 'src/features/persistence/PersistenceActions'; import { generateLzString, playgroundConfigLanguage, @@ -52,9 +47,9 @@ import { getLanguageConfig, isCseVariant, isSourceLanguage, - OverallState, - ResultOutput, - SALanguage + type OverallState, + type ResultOutput, + type SALanguage } from '../../commons/application/ApplicationTypes'; import { ExternalLibraryName } from '../../commons/application/types/ExternalTypes'; import { ControlBarAutorunButtons } from '../../commons/controlBar/ControlBarAutorunButtons'; @@ -69,23 +64,23 @@ import { ControlBarToggleFolderModeButton } from '../../commons/controlBar/Contr import { ControlBarGitHubButtons } from '../../commons/controlBar/github/ControlBarGitHubButtons'; import { convertEditorTabStateToProps, - NormalEditorContainerProps + type NormalEditorContainerProps } from '../../commons/editor/EditorContainer'; -import { Position } from '../../commons/editor/EditorTypes'; +import type { Position } from '../../commons/editor/EditorTypes'; import { overwriteFilesInWorkspace } from '../../commons/fileSystem/utils'; import FileSystemView from '../../commons/fileSystemView/FileSystemView'; import MobileWorkspace, { - MobileWorkspaceProps + type MobileWorkspaceProps } from '../../commons/mobileWorkspace/MobileWorkspace'; import { SideBarTab } from '../../commons/sideBar/SideBar'; -import { SideContentTab, SideContentType } from '../../commons/sideContent/SideContentTypes'; +import { type SideContentTab, SideContentType } from '../../commons/sideContent/SideContentTypes'; import Constants, { Links } from '../../commons/utils/Constants'; import { generateLanguageIntroduction } from '../../commons/utils/IntroductionHelper'; import { convertParamToBoolean, convertParamToInt } from '../../commons/utils/ParamParseHelper'; -import { IParsedQuery, parseQuery } from '../../commons/utils/QueryHelper'; +import { type IParsedQuery, parseQuery } from '../../commons/utils/QueryHelper'; import Workspace, { WorkspaceProps } from '../../commons/workspace/Workspace'; import { initSession, log } from '../../features/eventLogging'; -import { +import type { CodeDelta, Input, SelectionRange @@ -561,13 +556,13 @@ const Playground: React.FC = props => { loggedInAs={persistenceUser} isDirty={persistenceIsDirty} key="googledrive" - onClickSaveAs={() => dispatch(persistenceSaveFileAs())} - onClickOpen={() => dispatch(persistenceOpenPicker())} + onClickSaveAs={() => dispatch(PersistenceActions.persistenceSaveFileAs())} + onClickOpen={() => dispatch(PersistenceActions.persistenceOpenPicker())} onClickSave={ - persistenceFile ? () => dispatch(persistenceSaveFile(persistenceFile)) : undefined + persistenceFile ? () => dispatch(PersistenceActions.persistenceSaveFile(persistenceFile)) : undefined } onClickLogOut={() => dispatch(SessionActions.logoutGoogle())} - onPopoverOpening={() => dispatch(persistenceInitialise())} + onPopoverOpening={() => dispatch(PersistenceActions.persistenceInitialise())} /> ); }, [isFolderModeEnabled, persistenceFile, persistenceUser, persistenceIsDirty, dispatch]); @@ -733,7 +728,7 @@ const Playground: React.FC = props => { } if (currentLang === Chapter.FULL_JAVA && process.env.NODE_ENV === 'development') { - tabs.push(makeUploadTabFrom(files => dispatch(uploadFiles(files, workspaceLocation)))); + tabs.push(makeUploadTabFrom(files => dispatch(WorkspaceActions.uploadFiles(files, workspaceLocation)))); } if (!usingRemoteExecution) { From 493194e785ea5819656cb83c97451b264a6a176f Mon Sep 17 00:00:00 2001 From: "DESKTOP-G08HS3B\\Lee Yi" Date: Mon, 5 May 2025 12:05:10 -0400 Subject: [PATCH 14/14] Fix Linting --- .../application/actions/SessionActions.ts | 2 +- src/commons/redux/__tests__/utils.ts | 40 +++++++-------- src/commons/redux/utils.ts | 7 +-- src/commons/sagas/PersistenceSaga.tsx | 6 +-- src/commons/sagas/__tests__/SafeEffects.ts | 40 +++++++-------- src/commons/utils/TypeHelper.ts | 4 ++ src/commons/workspace/WorkspaceActions.ts | 16 ++++-- src/commons/workspace/WorkspaceReducer.ts | 50 ++++--------------- src/commons/workspace/WorkspaceTypes.ts | 5 +- src/features/github/GitHubActions.ts | 2 +- .../persistence/PersistenceActions.ts | 6 +-- src/pages/playground/Playground.tsx | 8 ++- 12 files changed, 87 insertions(+), 99 deletions(-) diff --git a/src/commons/application/actions/SessionActions.ts b/src/commons/application/actions/SessionActions.ts index af363a0c91..78f0276d46 100644 --- a/src/commons/application/actions/SessionActions.ts +++ b/src/commons/application/actions/SessionActions.ts @@ -153,4 +153,4 @@ const SessionActions = createActions('session', { }); // For compatibility with existing code (actions helper) -export default SessionActions +export default SessionActions; diff --git a/src/commons/redux/__tests__/utils.ts b/src/commons/redux/__tests__/utils.ts index 75b08414b2..12b8875d0c 100644 --- a/src/commons/redux/__tests__/utils.ts +++ b/src/commons/redux/__tests__/utils.ts @@ -1,7 +1,7 @@ -import { testSaga } from "redux-saga-test-plan" -import WorkspaceActions from "src/commons/workspace/WorkspaceActions" +import { testSaga } from 'redux-saga-test-plan'; +import WorkspaceActions from 'src/commons/workspace/WorkspaceActions'; -import { combineSagaHandlers, createActions } from "../utils" +import { combineSagaHandlers, createActions } from '../utils'; // Would have used spyOn, but for some reason that doesn't work properly jest.mock('src/commons/sagas/SafeEffects', () => ({ @@ -9,12 +9,12 @@ jest.mock('src/commons/sagas/SafeEffects', () => ({ // Mock wrap saga to just be a passthrough so that the identity // checking that testSaga uses will pass wrapSaga: (x: any) => x -})) +})); test('test combineSagaHandlers', () => { - const mockTakeEveryHandler = jest.fn() - const mockTakeLatestHandler = jest.fn() - const mockTakeLeadingHandler = jest.fn() + const mockTakeEveryHandler = jest.fn(); + const mockTakeLatestHandler = jest.fn(); + const mockTakeLeadingHandler = jest.fn(); const saga = combineSagaHandlers({ [WorkspaceActions.toggleUsingUpload.type]: mockTakeEveryHandler, @@ -31,13 +31,13 @@ test('test combineSagaHandlers', () => { takeEvery: mockTakeEveryHandler, takeLeading: mockTakeLeadingHandler } - }) + }); testSaga(saga) .next() .takeEvery(WorkspaceActions.toggleUsingUpload.type, mockTakeEveryHandler) .next() - .takeEvery(WorkspaceActions.toggleFolderMode.type, mockTakeEveryHandler) + .takeEvery(WorkspaceActions.toggleFolderMode.type, mockTakeEveryHandler) .next() .takeLatest(WorkspaceActions.toggleUsingCse.type, mockTakeLatestHandler) .next() @@ -47,23 +47,23 @@ test('test combineSagaHandlers', () => { .next() .takeLeading(WorkspaceActions.toggleEditorAutorun.type, mockTakeLeadingHandler) .next() - .isDone() -}) + .isDone(); +}); test('createActions', () => { const actions = createActions('workspace', { act0: false, act1: (value: string) => ({ value }), act2: 525600 - }) + }); - const act0 = actions.act0() - expect(act0.type).toEqual('workspace/act0') + const act0 = actions.act0(); + expect(act0.type).toEqual('workspace/act0'); - const act1 = actions.act1('test') - expect(act1.type).toEqual('workspace/act1') - expect(act1.payload).toMatchObject({ value: 'test' }) + const act1 = actions.act1('test'); + expect(act1.type).toEqual('workspace/act1'); + expect(act1.payload).toMatchObject({ value: 'test' }); - const act2 = actions.act2() - expect(act2.type).toEqual('workspace/act2') -}) + const act2 = actions.act2(); + expect(act2.type).toEqual('workspace/act2'); +}); diff --git a/src/commons/redux/utils.ts b/src/commons/redux/utils.ts index c14b9cbd42..49bab8b7c4 100644 --- a/src/commons/redux/utils.ts +++ b/src/commons/redux/utils.ts @@ -24,9 +24,10 @@ export function createActions ({ ...res, - [name]: typeof func === 'function' - ? createAction(`${baseName}/${name}`, (...args: any) => ({ payload: func(...args) })) - : createAction(`${baseName}/${name}`) + [name]: + typeof func === 'function' + ? createAction(`${baseName}/${name}`, (...args: any) => ({ payload: func(...args) })) + : createAction(`${baseName}/${name}`) }), {} as Readonly<{ [K in keyof BaseActions]: K extends string diff --git a/src/commons/sagas/PersistenceSaga.tsx b/src/commons/sagas/PersistenceSaga.tsx index 5a3ee8cd24..9e44834c00 100644 --- a/src/commons/sagas/PersistenceSaga.tsx +++ b/src/commons/sagas/PersistenceSaga.tsx @@ -232,7 +232,7 @@ const PersistenceSaga = combineSagaHandlers({ } } }, - [actions.persistenceSaveFile.type]: function* ({ payload: { id, name }}) { + [actions.persistenceSaveFile.type]: function* ({ payload: { id, name } }) { let toastKey: string | undefined; try { toastKey = yield call(showMessage, { @@ -248,7 +248,7 @@ const PersistenceSaga = combineSagaHandlers({ editorTabs, context: { chapter, variant }, externalLibrary: external - } = yield* selectWorkspace('playground') + } = yield* selectWorkspace('playground'); if (activeEditorTabIndex === null) { throw new Error('No active editor tab found.'); @@ -273,7 +273,7 @@ const PersistenceSaga = combineSagaHandlers({ } }, [actions.persistenceInitialise.type]: ensureInitialised as any -}) +}); interface IPlaygroundConfig { chapter: Chapter; diff --git a/src/commons/sagas/__tests__/SafeEffects.ts b/src/commons/sagas/__tests__/SafeEffects.ts index 7e785baf53..c298b4f075 100644 --- a/src/commons/sagas/__tests__/SafeEffects.ts +++ b/src/commons/sagas/__tests__/SafeEffects.ts @@ -1,35 +1,35 @@ -import * as Sentry from '@sentry/browser' -import { call } from "redux-saga/effects" -import { expectSaga } from "redux-saga-test-plan" +import * as Sentry from '@sentry/browser'; +import { call } from 'redux-saga/effects'; +import { expectSaga } from 'redux-saga-test-plan'; -import { wrapSaga } from "../SafeEffects" +import { wrapSaga } from '../SafeEffects'; -jest.spyOn(Sentry, 'captureException') +jest.spyOn(Sentry, 'captureException'); // Silence console error -jest.spyOn(console, 'error').mockImplementation(x => {}) +jest.spyOn(console, 'error').mockImplementation(x => {}); describe('Test wrapSaga', () => { test('wrapSaga is transparent', async () => { - const mockFn = jest.fn() + const mockFn = jest.fn(); const wrappedSaga = wrapSaga(function* () { - yield call(mockFn) - }) + yield call(mockFn); + }); - await expectSaga(wrappedSaga).silentRun() + await expectSaga(wrappedSaga).silentRun(); - expect(mockFn).toHaveBeenCalledTimes(1) - }) + expect(mockFn).toHaveBeenCalledTimes(1); + }); test('wrapSaga handles errors appropriately', async () => { - const errorToThrow = new Error() + const errorToThrow = new Error(); const wrappedSaga = wrapSaga(function* () { - throw errorToThrow - }) + throw errorToThrow; + }); - await expectSaga(wrappedSaga).silentRun() + await expectSaga(wrappedSaga).silentRun(); - expect(Sentry.captureException).toHaveBeenCalledWith(errorToThrow) - expect(console.error).toHaveBeenCalledTimes(1) - }) -}) + expect(Sentry.captureException).toHaveBeenCalledWith(errorToThrow); + expect(console.error).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/commons/utils/TypeHelper.ts b/src/commons/utils/TypeHelper.ts index 5bcef66e47..289b729d2f 100644 --- a/src/commons/utils/TypeHelper.ts +++ b/src/commons/utils/TypeHelper.ts @@ -177,6 +177,10 @@ export function objectEntries>(obj: T) { return Object.entries(obj) as DeconstructRecord; } +/** + * Utility for extracting the ActionCreator type from all the action + * creators using the specific type string + */ export type ActionTypeToCreator = Extract< (typeof actions)[keyof typeof actions], (...args: any[]) => { type: T } diff --git a/src/commons/workspace/WorkspaceActions.ts b/src/commons/workspace/WorkspaceActions.ts index 74931ab63c..49da980224 100644 --- a/src/commons/workspace/WorkspaceActions.ts +++ b/src/commons/workspace/WorkspaceActions.ts @@ -1,7 +1,10 @@ import type { Context } from 'js-slang'; import { Chapter, Variant } from 'js-slang/dist/types'; -import type { AllColsSortStates, GradingColumnVisibility } from '../../features/grading/GradingTypes'; +import type { + AllColsSortStates, + GradingColumnVisibility +} from '../../features/grading/GradingTypes'; import type { SALanguage } from '../application/ApplicationTypes'; import type { ExternalLibraryName } from '../application/types/ExternalTypes'; import type { Library } from '../assessment/AssessmentTypes'; @@ -247,7 +250,8 @@ const newActions = createActions('workspace', { workspaceLocation }), toggleUsingUpload: (usingUpload: boolean, workspaceLocation: WorkspaceLocationsWithTools) => ({ - usingUpload, workspaceLocation + usingUpload, + workspaceLocation }), updateCurrentStep: (steps: number, workspaceLocation: WorkspaceLocation) => ({ steps, @@ -266,10 +270,12 @@ const newActions = createActions('workspace', { workspaceLocation }), updateLastDebuggerResult: (lastDebuggerResult: any, workspaceLocation: WorkspaceLocation) => ({ - lastDebuggerResult, workspaceLocation + lastDebuggerResult, + workspaceLocation }), uploadFiles: (files: UploadResult, workspaceLocation: WorkspaceLocation) => ({ - files, workspaceLocation + files, + workspaceLocation }), // For grading table increaseRequestCounter: 0, @@ -279,4 +285,4 @@ const newActions = createActions('workspace', { updateGradingColumnVisibility: (filters: GradingColumnVisibility) => ({ filters }) }); -export default newActions +export default newActions; diff --git a/src/commons/workspace/WorkspaceReducer.ts b/src/commons/workspace/WorkspaceReducer.ts index 0883e7fb7f..7e4f241ea9 100644 --- a/src/commons/workspace/WorkspaceReducer.ts +++ b/src/commons/workspace/WorkspaceReducer.ts @@ -66,7 +66,7 @@ export const WorkspaceReducer: Reducer break; } - state = oldWorkspaceReducer(state, action); + // state = oldWorkspaceReducer(state, action); state = newWorkspaceReducer(state, action); return state; }; @@ -354,15 +354,15 @@ const newWorkspaceReducer = createReducer(defaultWorkspaceManager, builder => { state.playground.context.chapter = chapter; state.playground.context.variant = variant; }) - // .addCase(notifyProgramEvaluated, (state, action) => { - // const workspaceLocation = getWorkspaceLocation(action); - // const debuggerContext = state[workspaceLocation].debuggerContext; - // debuggerContext.result = action.payload.result; - // debuggerContext.lastDebuggerResult = action.payload.lastDebuggerResult; - // debuggerContext.code = action.payload.code; - // debuggerContext.context = action.payload.context; - // debuggerContext.workspaceLocation = action.payload.workspaceLocation; - // }) + .addCase(WorkspaceActions.notifyProgramEvaluated, (state, action) => { + const workspaceLocation = getWorkspaceLocation(action); + const debuggerContext = state[workspaceLocation].debuggerContext; + debuggerContext.result = action.payload.result; + debuggerContext.lastDebuggerResult = action.payload.lastDebuggerResult; + debuggerContext.code = action.payload.code; + debuggerContext.context = action.payload.context; + debuggerContext.workspaceLocation = action.payload.workspaceLocation; + }) .addCase(WorkspaceActions.toggleUsingUpload, (state, action) => { const { workspaceLocation } = action.payload; if (workspaceLocation === 'playground' || workspaceLocation === 'sicp') { @@ -380,33 +380,3 @@ const newWorkspaceReducer = createReducer(defaultWorkspaceManager, builder => { state[workspaceLocation].lastDebuggerResult = action.payload.lastDebuggerResult; }); }); - -/** Temporarily kept to prevent conflicts */ -const oldWorkspaceReducer: Reducer = ( - state = defaultWorkspaceManager, - action -) => { - const workspaceLocation = getWorkspaceLocation(action); - - switch (action.type) { - case WorkspaceActions.notifyProgramEvaluated.type: { - const debuggerContext = { - ...state[workspaceLocation].debuggerContext, - result: action.payload.result, - lastDebuggerResult: action.payload.lastDebuggerResult, - code: action.payload.code, - context: action.payload.context, - workspaceLocation: action.payload.workspaceLocation - }; - return { - ...state, - [workspaceLocation]: { - ...state[workspaceLocation], - debuggerContext - } - }; - } - default: - return state; - } -}; diff --git a/src/commons/workspace/WorkspaceTypes.ts b/src/commons/workspace/WorkspaceTypes.ts index 007f6ec058..ea9f137a9e 100644 --- a/src/commons/workspace/WorkspaceTypes.ts +++ b/src/commons/workspace/WorkspaceTypes.ts @@ -1,6 +1,9 @@ import type { Context } from 'js-slang'; -import type { AllColsSortStates, GradingColumnVisibility } from '../../features/grading/GradingTypes'; +import type { + AllColsSortStates, + GradingColumnVisibility +} from '../../features/grading/GradingTypes'; import type { SourcecastWorkspaceState } from '../../features/sourceRecorder/sourcecast/SourcecastTypes'; import type { SourcereelWorkspaceState } from '../../features/sourceRecorder/sourcereel/SourcereelTypes'; import type { InterpreterOutput } from '../application/ApplicationTypes'; diff --git a/src/features/github/GitHubActions.ts b/src/features/github/GitHubActions.ts index aae238c25b..33638a2aee 100644 --- a/src/features/github/GitHubActions.ts +++ b/src/features/github/GitHubActions.ts @@ -6,4 +6,4 @@ const newActions = createActions('github', { githubSaveFileAs: 0 }); -export default newActions +export default newActions; diff --git a/src/features/persistence/PersistenceActions.ts b/src/features/persistence/PersistenceActions.ts index caab699b8f..0869f0eb9d 100644 --- a/src/features/persistence/PersistenceActions.ts +++ b/src/features/persistence/PersistenceActions.ts @@ -6,7 +6,7 @@ const PersistenceActions = createActions('persistence', { persistenceOpenPicker: true, persistenceSaveFile: (file: PersistenceFile) => file, persistenceSaveFileAs: true, - persistenceInitialise: true, -}) + persistenceInitialise: true +}); -export default PersistenceActions +export default PersistenceActions; diff --git a/src/pages/playground/Playground.tsx b/src/pages/playground/Playground.tsx index d24f820dde..02bd08ee99 100644 --- a/src/pages/playground/Playground.tsx +++ b/src/pages/playground/Playground.tsx @@ -559,7 +559,9 @@ const Playground: React.FC = props => { onClickSaveAs={() => dispatch(PersistenceActions.persistenceSaveFileAs())} onClickOpen={() => dispatch(PersistenceActions.persistenceOpenPicker())} onClickSave={ - persistenceFile ? () => dispatch(PersistenceActions.persistenceSaveFile(persistenceFile)) : undefined + persistenceFile + ? () => dispatch(PersistenceActions.persistenceSaveFile(persistenceFile)) + : undefined } onClickLogOut={() => dispatch(SessionActions.logoutGoogle())} onPopoverOpening={() => dispatch(PersistenceActions.persistenceInitialise())} @@ -728,7 +730,9 @@ const Playground: React.FC = props => { } if (currentLang === Chapter.FULL_JAVA && process.env.NODE_ENV === 'development') { - tabs.push(makeUploadTabFrom(files => dispatch(WorkspaceActions.uploadFiles(files, workspaceLocation)))); + tabs.push( + makeUploadTabFrom(files => dispatch(WorkspaceActions.uploadFiles(files, workspaceLocation))) + ); } if (!usingRemoteExecution) {