From c4953bf26a4878bdeca1a96933876a13b6c3b9c5 Mon Sep 17 00:00:00 2001 From: Andrey Morozov Date: Thu, 17 Sep 2026 22:26:38 +0200 Subject: [PATCH 1/3] f-0 fix eslint configuration --- .../assignment_builder/.eslintignore | 1 + .../assignment_builder/.eslintrc | 106 -------------- .../assignment_builder/.eslintrc.cjs | 132 +++++++++++++++--- .../assignment_builder/src/App.tsx | 7 + .../assignment_builder/src/componentFuncs.js | 7 + .../AssignmentBuilder/AssignmentBuilder.tsx | 3 +- .../assignmentMutationHandlers.spec.ts | 2 +- .../assignmentMutationHandlers.ts | 4 +- .../components/edit/AssignmentEdit.tsx | 6 +- .../components/edit/VisibilityControl.tsx | 3 +- .../AssignmentExercisesContainer.spec.tsx | 3 +- .../AssignmentExercisesContainer.tsx | 2 +- .../AssignmentExercisesTable.spec.tsx | 1 - .../AssignmentExercisesTable.tsx | 8 +- .../CreateView.spec.tsx | 2 +- .../AssignmentExercisesList/EditView.spec.tsx | 2 +- .../AssignmentExercisesList/EditView.tsx | 3 +- .../ExerciseListView.tsx | 3 +- .../ExercisesToolbar.spec.tsx | 1 - .../ExercisesToolbar.tsx | 3 +- .../ChooseExercises/ChooseExercises.spec.tsx | 1 - .../CopyExercise/CopyExerciseModal.spec.tsx | 1 - .../CopyExercise/CopyExerciseModal.tsx | 5 +- .../CreateExercise/CreateExercise.spec.tsx | 3 +- .../ActiveCodeExercise/ActiveCodeExercise.tsx | 1 + .../ActiveCodeExerciseSettings.spec.tsx | 1 - .../ActiveCodeExercise/ActiveCodePreview.tsx | 1 + .../components/DataFilesEditor.spec.tsx | 1 - .../components/DataFilesEditor.tsx | 13 +- .../components/BaseExerciseForm.spec.tsx | 4 +- .../components/BaseExerciseForm.tsx | 1 - .../ClickableAreaEditor.tsx | 17 ++- .../extensions/ClickableAreaMark.ts | 3 + .../extensions/CustomCodeBlockPrism.ts | 5 +- .../DragAndDropExercise.tsx | 13 +- .../components/SortableBlock.spec.tsx | 3 +- .../DragAndDropExercise/hooks/index.spec.ts | 3 +- .../hooks/useDragAndDropConnections.tsx | 1 - .../components/ExerciseTypeSelect.spec.tsx | 3 +- .../components/BlankManager.spec.tsx | 6 +- .../components/BlankManager.tsx | 2 + .../components/IframeUrlInput.spec.tsx | 3 +- .../ImportQuestionJsonModal.spec.tsx | 3 +- .../MatchingExercise/MatchingExercise.tsx | 23 +-- .../components/SortableBlock.spec.tsx | 3 +- .../MatchingExercise/hooks/index.spec.ts | 8 +- .../hooks/useMatchingConnections.tsx | 15 +- .../MultiChoiceOptions.spec.tsx | 2 + .../ParsonsExercise/ParsonsExercise.tsx | 4 +- .../components/BlockItem.spec.tsx | 1 - .../ParsonsExercise/components/BlockItem.tsx | 18 +++ .../components/ParsonsBlocksManager.spec.tsx | 1 - .../components/ParsonsBlocksManager.tsx | 10 ++ .../components/ParsonsExerciseTour.tsx | 9 ++ .../PollExercise/PollOptions.spec.tsx | 3 +- .../PollExercise/ScaleSettings.spec.tsx | 3 +- .../SelectQuestionExercise.tsx | 1 + .../components/ABExperimentSettings.spec.tsx | 3 +- .../components/QuestionListEditor.spec.tsx | 3 +- .../components/QuestionListEditor.tsx | 3 + .../components/ToggleOptionsSettings.spec.tsx | 3 +- .../ShortAnswerInstructions.spec.tsx | 3 +- .../CreateExercise/config/stepConfigs.spec.ts | 3 +- .../CreateExercise/config/stepConfigs.ts | 6 + .../hooks/useBaseExercise.spec.ts | 41 ++++++ .../CreateExercise/hooks/useBaseExercise.ts | 5 +- .../hooks/useExerciseStepNavigation.spec.ts | 25 +++- .../hooks/useStepValidation.spec.ts | 2 + .../BaseExerciseSettingsContent.spec.tsx | 1 + .../shared/BaseExerciseSettingsContent.tsx | 2 +- .../shared/CodeHighlighter.spec.tsx | 3 +- .../CreateExercise/shared/CodeHighlighter.tsx | 2 +- .../shared/connections/ConnectHandle.spec.tsx | 3 +- .../connections/ConnectTargetButton.spec.tsx | 3 +- .../connections/ConnectionList.spec.tsx | 3 +- .../CreateExercise/utils/validation.spec.ts | 14 ++ .../EditDropdownValueHeader.spec.tsx | 3 +- .../ExercisePreview/ExercisePreviewModal.tsx | 1 - .../SmartSearchExercises.spec.tsx | 1 - .../SearchExercises/SmartSearchExercises.tsx | 10 +- .../exercises/components/TipTap/Editor.tsx | 10 +- .../components/TipTap/SlashCommands.tsx | 1 + .../components/TipTap/TipTapDocModal.spec.tsx | 1 + .../TipTap/components/InsertFormPopover.tsx | 7 +- .../TipTap/components/TableColumnMenu.tsx | 1 + .../TipTap/components/TableRowMenu.tsx | 16 +++ .../components/TipTap/hooks/index.spec.ts | 8 ++ .../TipTap/hooks/useTableColumnMenu.spec.ts | 29 ++++ .../TipTap/hooks/useTableColumnMenu.ts | 4 + .../TipTap/hooks/useTableRowMenu.spec.ts | 26 ++++ .../TipTap/hooks/useTableRowMenu.ts | 8 ++ .../ImportAssignmentModal.spec.tsx | 26 +--- .../ImportAssignmentModal.tsx | 40 ++---- .../importAssignment/ImportPreviewPanel.tsx | 4 +- .../components/list/AssignmentList.tsx | 10 +- .../reading/AssignmentReadingsTable.spec.tsx | 1 - .../reading/AssignmentReadingsTable.tsx | 5 +- .../components/reading/ReadingsListView.tsx | 3 +- .../reading/ReadingsToolbar.spec.tsx | 1 - .../components/reading/ReadingsToolbar.tsx | 3 +- .../ActivitiesRequiredCell.spec.tsx | 4 +- .../EditDropdownValueHeaderReadings.spec.tsx | 3 +- .../components/wizard/AssignmentWizard.tsx | 5 +- .../hooks/useAssignmentForm.spec.ts | 5 +- .../hooks/useAssignmentRouting.spec.ts | 62 +++++++- .../hooks/useAssignmentState.spec.ts | 22 ++- .../hooks/useNameValidation.spec.ts | 20 ++- .../src/components/routes/Grader/Grader.tsx | 13 +- .../components/DeadlineExceptionDialog.tsx | 6 +- .../routes/Grader/components/GradePanel.tsx | 5 +- .../components/GradebookCellDialog.spec.tsx | 2 +- .../Grader/components/GradebookCellDialog.tsx | 10 +- .../components/GradebookUnitsToggle.tsx | 2 +- .../Grader/components/ManualTotalForm.tsx | 2 +- .../Grader/components/MultiGradeDialog.tsx | 14 +- .../Grader/components/RegradeWizard.tsx | 9 +- .../components/ReleaseGradesControl.spec.tsx | 3 + .../components/ReleaseGradesControl.tsx | 4 +- .../components/StudentListSidebar.spec.tsx | 5 +- .../Grader/components/StudentListSidebar.tsx | 6 +- .../Grader/components/StudentMultiSelect.tsx | 7 +- .../Grader/components/SubmissionPane.tsx | 15 +- .../components/ThresholdControl.spec.tsx | 2 + .../Grader/components/ThresholdControl.tsx | 5 +- .../questionTypes/AnswerRenderer.spec.tsx | 2 +- .../questionTypes/FitbAnswerView.tsx | 2 + .../questionTypes/IframeAnswerView.spec.tsx | 2 +- .../questionTypes/IframeAnswerView.tsx | 8 +- .../questionTypes/RunestoneGraderPreview.tsx | 11 +- .../Grader/components/questionTypes/types.ts | 1 - .../components/submissionPaneHelpers.spec.ts | 1 + .../Grader/hooks/useAutoSaveGrade.spec.ts | 3 +- .../routes/Grader/hooks/useAutoSaveGrade.ts | 14 +- .../useEnsureEbookConfigForGrader.spec.ts | 3 + .../hooks/useEnsureEbookConfigForGrader.ts | 2 + .../Grader/hooks/useGraderHotkeys.spec.ts | 27 +++- .../routes/Grader/hooks/useGraderHotkeys.ts | 6 +- .../Grader/hooks/useGraderPrefs.spec.ts | 8 ++ .../routes/Grader/hooks/useGraderPrefs.ts | 10 +- .../routes/Grader/hooks/useGraderTour.spec.ts | 36 ++++- .../routes/Grader/hooks/useGraderTour.ts | 26 ++-- .../routes/Grader/hooks/usePlatform.spec.ts | 3 + .../routes/Grader/hooks/usePlatform.ts | 1 + .../Grader/hooks/useStudentNavigation.spec.ts | 19 ++- .../Grader/hooks/useStudentNavigation.ts | 20 +-- .../Grader/hooks/useViewModeStorage.spec.ts | 3 + .../routes/Grader/hooks/useViewModeStorage.ts | 5 +- .../Grader/pages/GraderAssignmentsPage.tsx | 12 +- .../Grader/pages/GraderGradebookPage.spec.tsx | 4 +- .../Grader/pages/GraderGradebookPage.tsx | 12 +- .../Grader/pages/GraderQuestionPage.tsx | 29 ++-- .../Grader/pages/GraderQuestionsPage.tsx | 16 ++- .../Grader/state/gradebookSelectors.spec.ts | 16 +++ .../routes/Grader/state/gradebookSelectors.ts | 6 + .../Grader/state/graderSelectors.spec.ts | 32 +++++ .../routes/Grader/state/graderSelectors.ts | 4 + .../routes/Grader/tour/GraderTourContext.tsx | 12 +- .../routes/Grader/tour/graderDemoData.ts | 9 +- .../src/components/shell/AppNavBar.spec.tsx | 4 +- .../src/components/shell/AppNavBar.tsx | 3 +- .../src/components/ui/DataGrid.spec.tsx | 5 + .../src/components/ui/DataGrid.tsx | 6 +- .../EditableCellFactory.spec.tsx | 4 +- .../ui/EditableTable/EditableDataTable.tsx | 2 +- .../hoc/WithCellRangeSelector.spec.tsx | 1 - .../src/components/ui/Icon.tsx | 1 + .../src/components/ui/Loader.spec.tsx | 1 + .../components/ui/Regex/RegexEditor.spec.tsx | 2 + .../src/components/ui/Regex/RegexEditor.tsx | 4 + .../src/components/ui/SearchInput.spec.tsx | 2 + .../ui/TabToolbar/TabToolbar.spec.tsx | 6 +- .../src/components/ui/TreeTable.spec.tsx | 1 - .../src/components/ui/TreeTable.tsx | 3 +- .../src/hooks/useExerciseTypes.spec.ts | 6 +- .../src/hooks/useExercisesSelector.spec.ts | 25 ++-- .../src/hooks/useFullscreen.spec.ts | 15 +- .../src/hooks/useFullscreen.ts | 1 + .../src/hooks/useJwtUser.spec.ts | 10 +- .../src/hooks/useJwtUser.ts | 1 + .../src/hooks/useReadingsSelector.spec.ts | 38 +++-- .../src/hooks/useSelectedAssignment.spec.ts | 20 ++- .../src/hooks/useSmartExerciseSearch.spec.ts | 9 +- .../src/hooks/useTableDropdownOptions.spec.ts | 8 +- .../hooks/useUpdateAssignmentExercise.spec.ts | 9 +- .../src/hooks/useUpdateExercises.spec.ts | 18 ++- .../src/hooks/useUpdateReadings.spec.ts | 21 ++- .../assignment_builder/src/navUtils.spec.ts | 22 +++ .../src/renderers/activeCode.jsx | 14 +- .../src/renderers/assignmentPicker.jsx | 8 +- .../src/renderers/editorModeChooser.jsx | 8 +- .../src/renderers/preview.jsx | 2 +- .../src/renderers/shortAnswer.jsx | 4 +- .../src/service/NodeService.jsx | 32 ++--- .../src/state/assignment/assignSlice.js | 2 +- .../src/state/epicker/ePickerSlice.js | 2 +- .../src/state/student/studentSlice.js | 3 +- .../assignment/assignment.logic.api.spec.ts | 117 ++-------------- .../store/assignment/assignment.logic.spec.ts | 2 + .../assignmentExercise.logic.api.spec.ts | 24 +++- .../assignmentExercise.logic.api.ts | 2 +- .../assignmentExercise.logic.spec.ts | 18 ++- .../src/store/baseQuery.spec.ts | 9 ++ .../assignment_builder/src/store/baseQuery.ts | 2 +- .../chooseExercises.logic.spec.ts | 8 +- .../store/datafile/datafile.logic.api.spec.ts | 15 +- .../src/store/datafile/datafile.logic.api.ts | 10 +- .../store/dataset/dataset.logic.api.spec.ts | 10 +- .../src/store/dataset/dataset.logic.api.ts | 2 +- .../src/store/dataset/dataset.logic.spec.ts | 23 ++- .../exercises/exercises.logic.api.spec.ts | 9 +- .../store/exercises/exercises.logic.api.ts | 2 +- .../store/exercises/exercises.logic.spec.ts | 11 +- .../src/store/grader/grader.logic.api.spec.ts | 67 ++++++++- .../src/store/grader/grader.logic.api.ts | 7 + .../store/readings/readings.logic.api.spec.ts | 12 +- .../src/store/readings/readings.logic.api.ts | 2 +- .../src/store/readings/readings.logic.spec.ts | 6 + .../searchExercises.logic.spec.ts | 7 +- .../src/store/store.spec.ts | 5 + .../src/store/user/userLogic.spec.ts | 1 + .../src/utils/courseTimezone.ts | 3 + .../assignment_builder/src/utils/date.spec.ts | 13 ++ .../assignment_builder/src/utils/date.ts | 2 + .../src/utils/exercise.spec.ts | 56 ++++++-- .../src/utils/htmlRegeneration.spec.ts | 42 +++--- .../src/utils/htmlRegeneration.ts | 14 +- .../assignment_builder/src/utils/json.spec.ts | 14 ++ .../src/utils/object.spec.ts | 7 + .../src/utils/preview/activeCode.spec.ts | 24 ++++ .../src/utils/preview/activeCode.ts | 4 +- .../src/utils/preview/dndPreview.spec.ts | 5 +- .../src/utils/preview/dndPreview.ts | 3 +- .../src/utils/preview/fillInTheBlank.spec.ts | 16 ++- .../src/utils/preview/fillInTheBlank.ts | 3 +- .../src/utils/preview/matchingPreview.spec.ts | 5 +- .../src/utils/preview/matchingPreview.ts | 4 +- .../src/utils/preview/parsonsPreview.tsx | 8 ++ .../utils/preview/selectQuestionPreview.ts | 1 + .../src/utils/questionJson.spec.ts | 5 +- .../src/utils/questionJson.ts | 4 +- .../src/utils/sanitize.spec.ts | 36 +++++ .../assignment_builder/tsconfig.json | 9 +- .../assignment_builder/vite.config.ts | 2 +- .../assignment_builder/vitest.config.ts | 54 +++---- 244 files changed, 1713 insertions(+), 752 deletions(-) delete mode 100755 bases/rsptx/assignment_server_api/assignment_builder/.eslintrc diff --git a/bases/rsptx/assignment_server_api/assignment_builder/.eslintignore b/bases/rsptx/assignment_server_api/assignment_builder/.eslintignore index 3c3629e64..bb21ffed5 100755 --- a/bases/rsptx/assignment_server_api/assignment_builder/.eslintignore +++ b/bases/rsptx/assignment_server_api/assignment_builder/.eslintignore @@ -1 +1,2 @@ node_modules +public/ diff --git a/bases/rsptx/assignment_server_api/assignment_builder/.eslintrc b/bases/rsptx/assignment_server_api/assignment_builder/.eslintrc deleted file mode 100755 index 4454491f0..000000000 --- a/bases/rsptx/assignment_server_api/assignment_builder/.eslintrc +++ /dev/null @@ -1,106 +0,0 @@ -{ - "root": true, - "extends": [ - "airbnb-typescript", - "plugin:prettier/recommended" - ], - "parser": "@typescript-eslint/parser", - "parserOptions": { - "project": "./tsconfig.json", - "ecmaFeatures": { - "jsx": true - } - }, - "plugins": [ - "react", - "import", - "@typescript-eslint", - "prettier", - "react-hooks", - "only-warn" - ], - "settings": { - "react": { - "version": "detect" - } - }, - "rules": { - "no-unused-vars": "off", - "@typescript-eslint/no-unused-vars": "off", - "semi": "warn", - "import/prefer-default-export": "off", - "no-undef": "off", - "newline-after-var": ["error", "always"], - "max-len": [ - "error", - { - "ignorePattern": "^.+ from .+$", - "code": 140 - } - ], - "import/named": "off", - "import/no-extraneous-dependencies": "off", - "import/no-unresolved": "off", - "import/extensions": "off", - "no-restricted-imports": "off", - "import/order": [ - "warn", - { - "groups": [["builtin", "external", "internal"], "parent", ["sibling", "index"]], - "pathGroups": [ - { - "pattern": "react*", - "group": "builtin", - "position": "before" - }, - { - "pattern": "@*(reduxjs|dnd-kit|snowplow)/**/*", - "group": "external", - "position": "after" - } - ], - "alphabetize": { - "order": "asc", - "caseInsensitive": false - }, - "newlines-between": "always" - } - ], - "react/prop-types": "off", - "react/destructuring-assignment": "off", - "react/no-array-index-key": "off", - "react/no-unused-prop-types": "off", - "react/require-default-props": "off", - "jsx-quotes": ["error", "prefer-double"], - "react-hooks/rules-of-hooks": "error", - "react-hooks/exhaustive-deps": "error", - "react/react-in-jsx-scope": "off", - "react/jsx-curly-brace-presence": "error", - "jsx-a11y/click-events-have-key-events": "off", - "jsx-a11y/no-static-element-interactions": "off", - "jsx-a11y/control-has-associated-label": "off", - "jsx-a11y/label-has-associated-control": "off", - "jsx-a11y/alt-text": "off", - "import/no-cycle": "off", - "react/jsx-one-expression-per-line": "off", - "@typescript-eslint/semi": "off", - "@typescript-eslint/no-redeclare": "off", - "no-underscore-dangle": [ - "error", - { - "allowAfterThis": true - } - ], - "no-console": "off", - "no-use-before-define": "off", - "@typescript-eslint/no-use-before-define": [ - "error", - { - "variables": false - } - ], - "@typescript-eslint/naming-convention": "off", - "quotes": ["error", "double"], - "prettier/prettier": ["error", { "singleQuote": false, "trailingComma" : "none" }] - } -} diff --git a/bases/rsptx/assignment_server_api/assignment_builder/.eslintrc.cjs b/bases/rsptx/assignment_server_api/assignment_builder/.eslintrc.cjs index 24a122e57..760f23309 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/.eslintrc.cjs +++ b/bases/rsptx/assignment_server_api/assignment_builder/.eslintrc.cjs @@ -1,20 +1,116 @@ module.exports = { - parser: '@typescript-eslint/parser', - parserOptions: { - project: './tsconfig.json', - tsconfigRootDir: __dirname, - sourceType: 'module' - }, - plugins: ['@typescript-eslint'], - extends: ['plugin:@typescript-eslint/recommended'], - rules: {}, - overrides: [ - { - files: ['**/*.spec.ts', '**/*.spec.tsx'], - rules: { - '@typescript-eslint/no-explicit-any': 'off', - '@typescript-eslint/no-unused-vars': 'off' - } - } - ] + root: true, + parser: "@typescript-eslint/parser", + ignorePatterns: [".eslintrc.cjs"], + parserOptions: { + project: "./tsconfig.json", + tsconfigRootDir: __dirname, + sourceType: "module", + ecmaFeatures: { + jsx: true + } + }, + plugins: ["react", "import", "@typescript-eslint", "prettier", "react-hooks", "only-warn"], + extends: [ + "airbnb-typescript", + "plugin:@typescript-eslint/recommended", + "plugin:prettier/recommended" + ], + settings: { + react: { + version: "detect" + } + }, + rules: { + "no-unused-vars": "off", + "@typescript-eslint/no-unused-vars": "off", + semi: "warn", + "import/prefer-default-export": "off", + "no-undef": "off", + "newline-after-var": ["error", "always"], + "max-len": [ + "error", + { + ignorePattern: "^.+ from .+$", + code: 140, + ignoreStrings: true, + ignoreTemplateLiterals: true, + ignoreRegExpLiterals: true + } + ], + "import/named": "off", + "import/no-extraneous-dependencies": "off", + "import/no-unresolved": "off", + "import/extensions": "off", + "no-restricted-imports": "off", + "import/order": [ + "warn", + { + groups: [["builtin", "external", "internal"], "parent", ["sibling", "index"]], + pathGroups: [ + { + pattern: "react*", + group: "builtin", + position: "before" + }, + { + pattern: "@*(reduxjs|dnd-kit|snowplow)/**/*", + group: "external", + position: "after" + } + ], + alphabetize: { + order: "asc", + caseInsensitive: false + }, + "newlines-between": "always" + } + ], + "react/prop-types": "off", + "react/destructuring-assignment": "off", + "react/no-array-index-key": "off", + "react/no-unused-prop-types": "off", + "react/require-default-props": "off", + "jsx-quotes": ["error", "prefer-double"], + "react-hooks/rules-of-hooks": "error", + "react-hooks/exhaustive-deps": "error", + "react/react-in-jsx-scope": "off", + "react/jsx-curly-brace-presence": "error", + "jsx-a11y/click-events-have-key-events": "off", + "jsx-a11y/no-static-element-interactions": "off", + "jsx-a11y/control-has-associated-label": "off", + "jsx-a11y/label-has-associated-control": "off", + "jsx-a11y/alt-text": "off", + "import/no-cycle": "off", + "react/jsx-one-expression-per-line": "off", + "@typescript-eslint/semi": "off", + "@typescript-eslint/no-redeclare": "off", + "no-underscore-dangle": [ + "error", + { + allowAfterThis: true + } + ], + "no-console": "off", + "no-use-before-define": "off", + "@typescript-eslint/no-use-before-define": [ + "error", + { + functions: false, + variables: false + } + ], + "@typescript-eslint/naming-convention": "off", + quotes: ["error", "double", { avoidEscape: true, allowTemplateLiterals: true }], + "prettier/prettier": ["error", { singleQuote: false, trailingComma: "none" }] + }, + overrides: [ + { + files: ["**/*.spec.ts", "**/*.spec.tsx"], + rules: { + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-unused-vars": "off" + } + } + ] }; diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/App.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/App.tsx index c41eee19e..d98c8e614 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/App.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/App.tsx @@ -166,6 +166,7 @@ function App() { path: "grader", async lazy() { const { Grader } = await import("@components/routes/Grader"); + return { Component: Grader }; }, children: [ @@ -173,6 +174,7 @@ function App() { index: true, async lazy() { const { GraderAssignmentsPage } = await import("@components/routes/Grader"); + return { Component: GraderAssignmentsPage }; } }, @@ -180,6 +182,7 @@ function App() { path: "gradebook", async lazy() { const { GraderGradebookPage } = await import("@components/routes/Grader"); + return { Component: GraderGradebookPage }; } }, @@ -187,6 +190,7 @@ function App() { path: ":assignmentId", async lazy() { const { GraderQuestionsPage } = await import("@components/routes/Grader"); + return { Component: GraderQuestionsPage }; } }, @@ -194,6 +198,7 @@ function App() { path: ":assignmentId/questions/:questionId", async lazy() { const { GraderQuestionPage } = await import("@components/routes/Grader"); + return { Component: GraderQuestionPage }; } }, @@ -201,6 +206,7 @@ function App() { path: ":assignmentId/questions/:questionId/students/:sid", async lazy() { const { GraderQuestionPage } = await import("@components/routes/Grader"); + return { Component: GraderQuestionPage }; } }, @@ -208,6 +214,7 @@ function App() { path: ":assignmentId/questions/:questionId/:sid", async lazy() { const { GraderQuestionPage } = await import("@components/routes/Grader"); + return { Component: GraderQuestionPage }; } } diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/componentFuncs.js b/bases/rsptx/assignment_server_api/assignment_builder/src/componentFuncs.js index 7bbb28056..976f124a0 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/componentFuncs.js +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/componentFuncs.js @@ -83,11 +83,13 @@ export async function renderRunestoneComponent( let preamble = document.getElementById("latex_preamble") || document.querySelector("div.hidden-content.process-math"); + if (preamble) { preamble = preamble.innerHTML; opt.preamble = preamble; } let res = window.component_factory[componentKind](opt); + console.log("res", res); // For components with async initialization (like selectquestion), @@ -109,16 +111,19 @@ export async function renderRunestoneComponent( let key = moreOpts.gradingContainer ? `${moreOpts.gradingContainer} ${res.divid}` : res.divid; + window.componentMap[key] = res; } if (!moreOpts.suppressFlagForReview && !moreOpts.graderMode) { let flagButton = document.createElement("button"); + flagButton.classList.add("flag-for-review"); flagButton.textContent = "Flag for Review"; flagButton.addEventListener("click", async function () { let data = { question_name: res.divid || res.selector_id || res.origOpts.orig.id }; + if (!data.question_name) { alert("Error: Cannot determine question name to flag for review"); return; @@ -131,8 +136,10 @@ export async function renderRunestoneComponent( "Content-Type": "application/json" } }); + if (response.ok) { let resp = await response.json(); + if (resp.success === true) { flagButton.textContent = "Question Flagged"; flagButton.style.backgroundColor = "var(--rs-success)"; diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/AssignmentBuilder.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/AssignmentBuilder.tsx index 47a190ad9..5aff2fb42 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/AssignmentBuilder.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/AssignmentBuilder.tsx @@ -24,6 +24,7 @@ import { useDispatch } from "react-redux"; import { useSelectedAssignment } from "@/hooks/useSelectedAssignment"; import { Assignment, CreateAssignmentPayload } from "@/types/assignment"; +import styles from "./AssignmentBuilder.module.css"; import { saveBulkEnforceDue, saveBulkVisibility, @@ -41,8 +42,6 @@ import { useAssignmentRouting } from "./hooks/useAssignmentRouting"; import { useAssignmentState } from "./hooks/useAssignmentState"; import { useNameValidation } from "./hooks/useNameValidation"; -import styles from "./AssignmentBuilder.module.css"; - export const AssignmentBuilder = () => { const dispatch = useDispatch(); const [importModalVisible, setImportModalVisible] = useState(false); diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/assignmentMutationHandlers.spec.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/assignmentMutationHandlers.spec.ts index 7e9d39d79..97412179d 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/assignmentMutationHandlers.spec.ts +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/assignmentMutationHandlers.spec.ts @@ -1,7 +1,7 @@ +import { notify } from "@components/ui/notify"; import { vi } from "vitest"; import { Assignment } from "@/types/assignment"; -import { notify } from "@components/ui/notify"; import { BulkUpdateAssignmentsTrigger, diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/assignmentMutationHandlers.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/assignmentMutationHandlers.ts index a44ed0c7a..790125486 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/assignmentMutationHandlers.ts +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/assignmentMutationHandlers.ts @@ -1,6 +1,8 @@ -import { Assignment } from "@/types/assignment"; import { notify } from "@components/ui/notify"; import { BulkActionResult } from "@store/assignment/assignment.logic.api"; + +import { Assignment } from "@/types/assignment"; + import { getVisibilityMode } from "./components/edit/visibilityMode"; export interface VisibilityUpdate { diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/edit/AssignmentEdit.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/edit/AssignmentEdit.tsx index 3fb0346c9..b63e5a828 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/edit/AssignmentEdit.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/edit/AssignmentEdit.tsx @@ -23,13 +23,11 @@ import { useExercisesSelector } from "@/hooks/useExercisesSelector"; import { Assignment, KindOfAssignment } from "@/types/assignment"; import { DateTimePicker } from "../../../../ui/DateTimePicker"; - -import { CourseTimezoneNotice } from "./CourseTimezoneNotice"; - import { AssignmentReadings } from "../reading/AssignmentReadings"; -import { VisibilityControl } from "./VisibilityControl"; import styles from "./AssignmentEdit.module.css"; +import { CourseTimezoneNotice } from "./CourseTimezoneNotice"; +import { VisibilityControl } from "./VisibilityControl"; const YES_NO_OPTIONS = [ { label: "Yes", value: "true" }, diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/edit/VisibilityControl.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/edit/VisibilityControl.tsx index b528dcc8d..6b7703c41 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/edit/VisibilityControl.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/edit/VisibilityControl.tsx @@ -6,9 +6,8 @@ import { convertDateToISO, formatUTCDateLocaleString, parseUTCDate } from "@/uti import { DateTimePicker } from "../../../../ui/DateTimePicker"; -import { getVisibilityMode, VisibilityMode } from "./visibilityMode"; - import styles from "./VisibilityControl.module.css"; +import { getVisibilityMode, VisibilityMode } from "./visibilityMode"; interface VisibilityControlProps { control: Control; diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/AssignmentExercisesList/AssignmentExercisesContainer.spec.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/AssignmentExercisesList/AssignmentExercisesContainer.spec.tsx index 417802fe7..281ad7695 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/AssignmentExercisesList/AssignmentExercisesContainer.spec.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/AssignmentExercisesList/AssignmentExercisesContainer.spec.tsx @@ -1,6 +1,7 @@ +import { notify } from "@components/ui/notify"; + import { renderWithMantine, screen, waitFor } from "@/test/renderWithMantine"; import { Exercise } from "@/types/exercises"; -import { notify } from "@components/ui/notify"; import { AssignmentExercisesContainer } from "./AssignmentExercisesContainer"; diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/AssignmentExercisesList/AssignmentExercisesContainer.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/AssignmentExercisesList/AssignmentExercisesContainer.tsx index fdbd81937..6033ac9c1 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/AssignmentExercisesList/AssignmentExercisesContainer.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/AssignmentExercisesList/AssignmentExercisesContainer.tsx @@ -2,8 +2,8 @@ import styles from "@components/routes/AssignmentBuilder/AssignmentBuilder.modul import { ChooseExercises } from "@components/routes/AssignmentBuilder/components/exercises/components/ChooseExercises/ChooseExercises"; import { SmartSearchExercises } from "@components/routes/AssignmentBuilder/components/exercises/components/SearchExercises/SmartSearchExercises"; import { Loader } from "@components/ui/Loader"; -import { exercisesActions, exercisesSelectors } from "@store/exercises/exercises.logic"; import { notify } from "@components/ui/notify"; +import { exercisesActions, exercisesSelectors } from "@store/exercises/exercises.logic"; import { useEffect, useRef, useState } from "react"; import { useDispatch, useSelector } from "react-redux"; diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/AssignmentExercisesList/AssignmentExercisesTable.spec.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/AssignmentExercisesList/AssignmentExercisesTable.spec.tsx index 6ac8c214d..46c374987 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/AssignmentExercisesList/AssignmentExercisesTable.spec.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/AssignmentExercisesList/AssignmentExercisesTable.spec.tsx @@ -1,7 +1,6 @@ import userEvent from "@testing-library/user-event"; import { renderWithMantine, screen } from "@/test/renderWithMantine"; - import { Exercise } from "@/types/exercises"; import { AssignmentExercisesTable } from "./AssignmentExercisesTable"; diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/AssignmentExercisesList/AssignmentExercisesTable.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/AssignmentExercisesList/AssignmentExercisesTable.tsx index 40a72a5b4..24c9d497a 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/AssignmentExercisesList/AssignmentExercisesTable.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/AssignmentExercisesList/AssignmentExercisesTable.tsx @@ -15,9 +15,8 @@ import { } from "@store/assignmentExercise/assignmentExercise.logic.api"; import { RefCallback, useMemo, useState } from "react"; -import { useExercisesSelector } from "@/hooks/useExercisesSelector"; - import { difficultyOptions } from "@/config/exerciseTypes"; +import { useExercisesSelector } from "@/hooks/useExercisesSelector"; import { useJwtUser } from "@/hooks/useJwtUser"; import { useSelectedAssignment } from "@/hooks/useSelectedAssignment"; import { DraggingExerciseColumns } from "@/types/components/editableTableCell"; @@ -29,9 +28,8 @@ import { EditDropdownValueHeader } from "../components/EditAllExercises/EditDrop import { EditInputValueHeader } from "../components/EditAllExercises/EditInputValueHeader"; import { ExercisePreviewModal } from "../components/ExercisePreview/ExercisePreviewModal"; -import { SetCurrentEditExercise, ViewModeSetter, MouseUpHandler } from "./types"; - import styles from "./AssignmentExercisesTable.module.css"; +import { SetCurrentEditExercise, ViewModeSetter, MouseUpHandler } from "./types"; const ASYNC_MODE_OPTIONS = (hasApiKey: boolean) => [ { value: "Standard", label: "Standard" }, @@ -418,6 +416,8 @@ export const AssignmentExercisesTable = ({ } return baseColumns; + // TODO(eslint): Audit the complete dependency list without changing current behavior. + // eslint-disable-next-line react-hooks/exhaustive-deps }, [handleMouseDown, handleChange, startItemId, showAsyncColumn, hasApiKey, username]); return ( diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/AssignmentExercisesList/CreateView.spec.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/AssignmentExercisesList/CreateView.spec.tsx index 77140cc30..23e3dbc37 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/AssignmentExercisesList/CreateView.spec.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/AssignmentExercisesList/CreateView.spec.tsx @@ -1,8 +1,8 @@ +import { notify } from "@components/ui/notify"; import userEvent from "@testing-library/user-event"; import { renderWithMantine, screen } from "@/test/renderWithMantine"; import { CreateExerciseFormType } from "@/types/exercises"; -import { notify } from "@components/ui/notify"; import { CreateView } from "./CreateView"; diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/AssignmentExercisesList/EditView.spec.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/AssignmentExercisesList/EditView.spec.tsx index 0361b29ce..286bd07ab 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/AssignmentExercisesList/EditView.spec.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/AssignmentExercisesList/EditView.spec.tsx @@ -1,8 +1,8 @@ +import { notify } from "@components/ui/notify"; import userEvent from "@testing-library/user-event"; import { renderWithMantine, screen } from "@/test/renderWithMantine"; import { CreateExerciseFormType, Exercise } from "@/types/exercises"; -import { notify } from "@components/ui/notify"; import { EditView } from "./EditView"; diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/AssignmentExercisesList/EditView.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/AssignmentExercisesList/EditView.tsx index a1720a606..4c901392e 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/AssignmentExercisesList/EditView.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/AssignmentExercisesList/EditView.tsx @@ -1,10 +1,9 @@ +import { notify } from "@components/ui/notify"; import { useUpdateAssignmentQuestionsMutation } from "@store/assignmentExercise/assignmentExercise.logic.api"; import { datasetSelectors } from "@store/dataset/dataset.logic"; import { useState } from "react"; import { useSelector } from "react-redux"; -import { notify } from "@components/ui/notify"; - import { CreateExerciseFormType, Exercise, QuestionJSON } from "@/types/exercises"; import { safeJsonParse } from "@/utils/json"; import { buildQuestionJson, mergeQuestionJsonWithDefaults } from "@/utils/questionJson"; diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/AssignmentExercisesList/ExerciseListView.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/AssignmentExercisesList/ExerciseListView.tsx index 33d23e07a..09aae7276 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/AssignmentExercisesList/ExerciseListView.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/AssignmentExercisesList/ExerciseListView.tsx @@ -4,11 +4,10 @@ import { DraggingExerciseColumns } from "@/types/components/editableTableCell"; import { Exercise } from "@/types/exercises"; import { AssignmentExercisesTable } from "./AssignmentExercisesTable"; +import styles from "./ExerciseListView.module.css"; import { ExercisesToolbar } from "./ExercisesToolbar"; import { SetCurrentEditExercise, ViewModeSetter, MouseUpHandler } from "./types"; -import styles from "./ExerciseListView.module.css"; - interface ExerciseListViewProps { globalFilter: string; setGlobalFilter: (filter: string) => void; diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/AssignmentExercisesList/ExercisesToolbar.spec.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/AssignmentExercisesList/ExercisesToolbar.spec.tsx index c998aa480..3854fa724 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/AssignmentExercisesList/ExercisesToolbar.spec.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/AssignmentExercisesList/ExercisesToolbar.spec.tsx @@ -1,7 +1,6 @@ import userEvent from "@testing-library/user-event"; import { renderWithMantine, screen } from "@/test/renderWithMantine"; - import { Exercise } from "@/types/exercises"; import { ExercisesToolbar } from "./ExercisesToolbar"; diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/AssignmentExercisesList/ExercisesToolbar.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/AssignmentExercisesList/ExercisesToolbar.tsx index b751c5095..6d99a78b9 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/AssignmentExercisesList/ExercisesToolbar.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/AssignmentExercisesList/ExercisesToolbar.tsx @@ -6,9 +6,8 @@ import { modals } from "@mantine/modals"; import { Exercise } from "@/types/exercises"; -import { ViewModeSetter } from "./types"; - import styles from "./ExercisesToolbar.module.css"; +import { ViewModeSetter } from "./types"; interface ExercisesToolbarProps { globalFilter: string; diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/ChooseExercises/ChooseExercises.spec.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/ChooseExercises/ChooseExercises.spec.tsx index 5bb478d56..fee07c37c 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/ChooseExercises/ChooseExercises.spec.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/ChooseExercises/ChooseExercises.spec.tsx @@ -1,7 +1,6 @@ import userEvent from "@testing-library/user-event"; import { renderWithMantine, screen } from "@/test/renderWithMantine"; - import { Exercise } from "@/types/exercises"; import { TreeNode } from "@/types/treeNode"; diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CopyExercise/CopyExerciseModal.spec.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CopyExercise/CopyExerciseModal.spec.tsx index 57501fb71..0afacfd0a 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CopyExercise/CopyExerciseModal.spec.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CopyExercise/CopyExerciseModal.spec.tsx @@ -1,7 +1,6 @@ import userEvent from "@testing-library/user-event"; import { renderWithMantine, screen } from "@/test/renderWithMantine"; - import { Exercise } from "@/types/exercises"; import { COPY_EXERCISE_TOAST_COPY, CopyExerciseModal } from "./CopyExerciseModal"; diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CopyExercise/CopyExerciseModal.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CopyExercise/CopyExerciseModal.tsx index 11ce461c5..d864656ad 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CopyExercise/CopyExerciseModal.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CopyExercise/CopyExerciseModal.tsx @@ -1,4 +1,6 @@ +import { ViewModeSetter } from "@components/routes/AssignmentBuilder/components/exercises/AssignmentExercisesList/types"; import { Icon } from "@components/ui/Icon"; +import { notify } from "@components/ui/notify"; import { Alert, Button, Group, Modal, Stack, TextInput } from "@mantine/core"; import { useCopyQuestionMutation, @@ -7,12 +9,9 @@ import { } from "@store/assignmentExercise/assignmentExercise.logic.api"; import { useEffect, useState } from "react"; -import { ViewModeSetter } from "@components/routes/AssignmentBuilder/components/exercises/AssignmentExercisesList/types"; - import { useSelectedAssignment } from "@/hooks/useSelectedAssignment"; import { Exercise, supportedExerciseTypesToEdit } from "@/types/exercises"; import { regenerateHtmlSrc } from "@/utils/htmlRegeneration"; -import { notify } from "@components/ui/notify"; export const COPY_EXERCISE_TOAST_COPY = { copiedAndEditing: "Copy added to this assignment. Opening editor…", diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/CreateExercise.spec.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/CreateExercise.spec.tsx index 19ce9e550..b1ce3250f 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/CreateExercise.spec.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/CreateExercise.spec.tsx @@ -1,6 +1,7 @@ -import { renderWithMantine } from "@/test/renderWithMantine"; import { fireEvent, screen } from "@testing-library/react"; +import { renderWithMantine } from "@/test/renderWithMantine"; + import { CreateExercise } from "./CreateExercise"; let routedType: string | null = null; diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/ActiveCodeExercise/ActiveCodeExercise.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/ActiveCodeExercise/ActiveCodeExercise.tsx index 0617d2d0b..42a45e2d8 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/ActiveCodeExercise/ActiveCodeExercise.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/ActiveCodeExercise/ActiveCodeExercise.tsx @@ -81,6 +81,7 @@ export const ActiveCodeExercise: FC = ({ const selectedDatafilesInfo = selectedFileAcids .map((acid) => { const existingFile = allDatafiles.find((df: ExistingDataFile) => df.acid === acid); + return { acid, filename: existingFile?.filename diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/ActiveCodeExercise/ActiveCodeExerciseSettings.spec.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/ActiveCodeExercise/ActiveCodeExerciseSettings.spec.tsx index 20ac94b6e..f545e78cd 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/ActiveCodeExercise/ActiveCodeExerciseSettings.spec.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/ActiveCodeExercise/ActiveCodeExerciseSettings.spec.tsx @@ -2,7 +2,6 @@ import userEvent from "@testing-library/user-event"; import { ReactNode } from "react"; import { renderWithMantine, screen } from "@/test/renderWithMantine"; - import { CreateExerciseFormType } from "@/types/exercises"; import { ActiveCodeExerciseSettings } from "./ActiveCodeExerciseSettings"; diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/ActiveCodeExercise/ActiveCodePreview.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/ActiveCodeExercise/ActiveCodePreview.tsx index 4b4c64cc4..1bdc87145 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/ActiveCodeExercise/ActiveCodePreview.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/ActiveCodeExercise/ActiveCodePreview.tsx @@ -43,6 +43,7 @@ export const ActiveCodePreview: FC = ({ const selectedDatafilesInfo = selectedExistingDataFiles .map((acid) => { const existingFile = allDatafiles.find((df: ExistingDataFile) => df.acid === acid); + return { acid, filename: existingFile?.filename diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/ActiveCodeExercise/components/DataFilesEditor.spec.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/ActiveCodeExercise/components/DataFilesEditor.spec.tsx index 05482e68a..9467279bb 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/ActiveCodeExercise/components/DataFilesEditor.spec.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/ActiveCodeExercise/components/DataFilesEditor.spec.tsx @@ -2,7 +2,6 @@ import { modals } from "@mantine/modals"; import userEvent from "@testing-library/user-event"; import { renderWithMantine, screen, waitFor } from "@/test/renderWithMantine"; - import { ExistingDataFile } from "@/types/datafile"; import { DataFilesEditor } from "./DataFilesEditor"; diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/ActiveCodeExercise/components/DataFilesEditor.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/ActiveCodeExercise/components/DataFilesEditor.tsx index 9338b1da9..20d4605f9 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/ActiveCodeExercise/components/DataFilesEditor.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/ActiveCodeExercise/components/DataFilesEditor.tsx @@ -1,3 +1,4 @@ +import { notify } from "@components/ui/notify"; import { ActionIcon, Button, @@ -25,7 +26,6 @@ import { useUpdateDatafileMutation } from "@/store/datafile/datafile.logic.api"; import { DataFile, ExistingDataFile, SelectedDataFile } from "@/types/datafile"; -import { notify } from "@components/ui/notify"; import styles from "./DataFilesEditor.module.css"; @@ -56,6 +56,7 @@ const UPLOAD_ACCEPT = "text/*,image/*,.txt,.csv,.json,.py,.js,.html,.css"; const hasValidExtension = (filename: string): boolean => { if (!filename) return false; const lowerFilename = filename.toLowerCase(); + return SUPPORTED_EXTENSIONS.some((ext) => lowerFilename.endsWith(ext)); }; @@ -65,6 +66,7 @@ const validateFilename = (filename: string): { isValid: boolean; error: string } } const invalidChars = /[<>:"/\\|?*\x00-\x1f]/; + if (invalidChars.test(filename)) { return { isValid: false, error: "Filename contains invalid characters" }; } @@ -144,6 +146,7 @@ export const DataFilesEditor: FC = ({ reader.onload = (e) => { const content = e.target?.result as string; + setNewDataFile((prev) => ({ ...prev, filename: file.name, @@ -164,6 +167,7 @@ export const DataFilesEditor: FC = ({ const handleCreateDataFile = async () => { const validation = validateFilename(newDataFile.filename); + if (!validation.isValid) { setFilenameError(validation.error); return; @@ -171,8 +175,10 @@ export const DataFilesEditor: FC = ({ setFilenameError(""); let contentToSave = newDataFile.content; + if (newDataFile.isImage && contentToSave.startsWith("data:")) { const base64Index = contentToSave.indexOf(";base64,"); + if (base64Index !== -1) { contentToSave = contentToSave.substring(base64Index + 8); } @@ -209,6 +215,7 @@ export const DataFilesEditor: FC = ({ const handleEditDataLoaded = useCallback(() => { if (editingDatafileData) { const isImage = editingDatafileData.filename?.match(/\.(png|jpg|jpeg|gif|svg)$/i) !== null; + setEditDataFile({ filename: editingDatafileData.filename || "", content: editingDatafileData.main_code || "", @@ -226,6 +233,7 @@ export const DataFilesEditor: FC = ({ const getFileExtension = (filename: string): string => { const lastDot = filename.lastIndexOf("."); + if (lastDot === -1) return ""; return filename.substring(lastDot).toLowerCase(); }; @@ -274,8 +282,10 @@ export const DataFilesEditor: FC = ({ if (!editingAcid) return; let contentToSave = editDataFile.content; + if (editDataFile.isImage && contentToSave.startsWith("data:")) { const base64Index = contentToSave.indexOf(";base64,"); + if (base64Index !== -1) { contentToSave = contentToSave.substring(base64Index + 8); } @@ -356,6 +366,7 @@ export const DataFilesEditor: FC = ({ {selectedDataFiles.map((selectedFileAcid) => { const file = existingDatafiles.find((df) => df.acid === selectedFileAcid); const isOwner = file?.owner !== null; + return ( { await userEvent.click(screen.getByRole("button", { name: /Save/i })); - expect(screen.getByText("Couldn't find the assignment. Reopen it from the assignments list.")).toBeInTheDocument(); + expect( + screen.getByText("Couldn't find the assignment. Reopen it from the assignments list.") + ).toBeInTheDocument(); expect(onSave).not.toHaveBeenCalled(); }); diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/BaseExerciseForm.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/BaseExerciseForm.tsx index 67b3ad5d0..d88da3881 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/BaseExerciseForm.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/BaseExerciseForm.tsx @@ -15,7 +15,6 @@ import { Icon } from "@/components/ui/Icon"; import { Exercise } from "@/types/exercises"; import { Editor } from "../../TipTap/Editor"; - import { BaseExerciseProps, ExerciseValidation } from "../types/ExerciseTypes"; export const BaseExerciseForm = ({ diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/ClickableAreaExercise/ClickableAreaEditor.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/ClickableAreaExercise/ClickableAreaEditor.tsx index 6203d3c81..9d8c874aa 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/ClickableAreaExercise/ClickableAreaEditor.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/ClickableAreaExercise/ClickableAreaEditor.tsx @@ -7,6 +7,7 @@ import { import { InsertFormBridge } from "@components/routes/AssignmentBuilder/components/exercises/components/TipTap/extensions/InsertFormBridge"; import { TabIndent } from "@components/routes/AssignmentBuilder/components/exercises/components/TipTap/extensions/TabIndent"; import { katexMacros } from "@components/routes/AssignmentBuilder/mathMacros"; +import { ActionIcon, Textarea, Tooltip } from "@mantine/core"; import FontFamily from "@tiptap/extension-font-family"; import Highlight from "@tiptap/extension-highlight"; import Link from "@tiptap/extension-link"; @@ -21,7 +22,6 @@ import Youtube from "@tiptap/extension-youtube"; import { Mark, Node } from "@tiptap/pm/model"; import { Editor, useEditor, EditorContent, BubbleMenu } from "@tiptap/react"; import StarterKit from "@tiptap/starter-kit"; -import { ActionIcon, Textarea, Tooltip } from "@mantine/core"; import { FC, useEffect, useState } from "react"; import { Icon } from "@/components/ui/Icon"; @@ -31,17 +31,18 @@ import "tippy.js/dist/tippy.css"; import "@fortawesome/fontawesome-free/css/all.min.css"; import "katex/dist/katex.min.css"; -import styles from "./ClickableAreaEditor.module.css"; import tipTapStyles from "../../../TipTap/Editor.module.css"; import { Command, items } from "../../../TipTap/SlashCommands"; +import { TableColumnMenu } from "../../../TipTap/components/TableColumnMenu"; +import { TableRowMenu } from "../../../TipTap/components/TableRowMenu"; +import { useTableColumnMenu } from "../../../TipTap/hooks/useTableColumnMenu"; +import { useTableRowMenu } from "../../../TipTap/hooks/useTableRowMenu"; + +import styles from "./ClickableAreaEditor.module.css"; import { ClickableAreaMark } from "./extensions/ClickableAreaMark"; -import { CustomCodeBlockPrism } from "./extensions/CustomCodeBlockPrism"; import { CustomCode } from "./extensions/CustomCode"; +import { CustomCodeBlockPrism } from "./extensions/CustomCodeBlockPrism"; import { ClickableArea } from "./types"; -import { useTableColumnMenu } from "../../../TipTap/hooks/useTableColumnMenu"; -import { useTableRowMenu } from "../../../TipTap/hooks/useTableRowMenu"; -import { TableColumnMenu } from "../../../TipTap/components/TableColumnMenu"; -import { TableRowMenu } from "../../../TipTap/components/TableRowMenu"; const customStyles = ` .tippy-box { @@ -80,6 +81,7 @@ export const ClickableAreaEditor: FC = ({ useEffect(() => { const styleElement = document.createElement("style"); + styleElement.textContent = customStyles; document.head.appendChild(styleElement); @@ -199,6 +201,7 @@ export const ClickableAreaEditor: FC = ({ content, onUpdate: ({ editor: uEditor }) => { const html = uEditor.getHTML(); + onChange(html); extractClickableAreas(uEditor); } diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/ClickableAreaExercise/extensions/ClickableAreaMark.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/ClickableAreaExercise/extensions/ClickableAreaMark.ts index 2edad8108..2521ee50a 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/ClickableAreaExercise/extensions/ClickableAreaMark.ts +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/ClickableAreaExercise/extensions/ClickableAreaMark.ts @@ -102,10 +102,12 @@ export const ClickableAreaMark = Mark.create({ state.doc.nodesBetween(from, to, (node, pos) => { if (node.isText) { const marks = node.marks.filter((mark) => mark.type.name === this.name); + if (marks.length > 0) { // Calculate the full range of this text node with the mark const nodeFrom = pos; const nodeTo = pos + node.nodeSize; + marksToRemove.push({ from: nodeFrom, to: nodeTo }); } } @@ -127,6 +129,7 @@ export const ClickableAreaMark = Mark.create({ state.doc.nodesBetween(searchFrom, searchTo, (node, pos) => { if (node.isText) { const marks = node.marks.filter((mark) => mark.type.name === this.name); + if (marks.length > 0) { const nodeFrom = pos; const nodeTo = pos + node.nodeSize; diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/ClickableAreaExercise/extensions/CustomCodeBlockPrism.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/ClickableAreaExercise/extensions/CustomCodeBlockPrism.ts index 5329c21fb..48b9ed421 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/ClickableAreaExercise/extensions/CustomCodeBlockPrism.ts +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/ClickableAreaExercise/extensions/CustomCodeBlockPrism.ts @@ -1,6 +1,6 @@ // Custom CodeBlockPrism that preserves marks in HTML serialization -import CodeBlockPrism from "tiptap-extension-code-block-prism"; import { DOMOutputSpec } from "@tiptap/pm/model"; +import CodeBlockPrism from "tiptap-extension-code-block-prism"; export const CustomCodeBlockPrism = CodeBlockPrism.extend({ addAttributes() { @@ -29,12 +29,15 @@ export const CustomCodeBlockPrism = CodeBlockPrism.extend({ const element = node as HTMLElement; const codeElement = element.querySelector("code"); + if (codeElement) { const language = codeElement.getAttribute("data-language") || "javascript"; + return { language }; } const language = element.getAttribute("data-language") || "javascript"; + return { language }; } } diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/DragAndDropExercise/DragAndDropExercise.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/DragAndDropExercise/DragAndDropExercise.tsx index 806b1fa0a..4257e1531 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/DragAndDropExercise/DragAndDropExercise.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/DragAndDropExercise/DragAndDropExercise.tsx @@ -26,13 +26,13 @@ import { DEFAULT_INCORRECT_FEEDBACK, buildQuestionJson } from "@/utils/questionJ import { DRAG_AND_DROP_STEP_VALIDATORS } from "../../config/stepConfigs"; import { useBaseExercise } from "../../hooks/useBaseExercise"; +import { useExerciseStepNavigation } from "../../hooks/useExerciseStepNavigation"; +import { ExerciseLayout } from "../../shared/ExerciseLayout"; import { ConnectionList, connectionExistsBetween, makeConnectionLabelResolver } from "../../shared/connections"; -import { useExerciseStepNavigation } from "../../hooks/useExerciseStepNavigation"; -import { ExerciseLayout } from "../../shared/ExerciseLayout"; import { ExerciseComponentProps } from "../../types/ExerciseTypes"; import { validateCommonFields } from "../../utils/validation"; @@ -289,14 +289,12 @@ export const DragAndDropExercise: FC = ({ const handleRemoveRightBlock = useCallback( (id: string) => { const hasConnections = (formData.correctAnswers || []).some( - // eslint-disable-next-line @typescript-eslint/no-unused-vars - ([_, targetId]) => targetId === id + ([, targetId]) => targetId === id ); if (hasConnections) { const updatedConnections = (formData.correctAnswers || []).filter( - // eslint-disable-next-line @typescript-eslint/no-unused-vars - ([_, targetId]) => targetId !== id + ([, targetId]) => targetId !== id ); updateFormData("correctAnswers", updatedConnections); @@ -443,8 +441,7 @@ export const DragAndDropExercise: FC = ({ ).size; const connectedRightItems = new Set( - // eslint-disable-next-line @typescript-eslint/no-unused-vars - (formData.correctAnswers || []).map(([_, targetId]) => targetId) + (formData.correctAnswers || []).map(([, targetId]) => targetId) ).size; const leftConnectionPercentage = diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/DragAndDropExercise/components/SortableBlock.spec.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/DragAndDropExercise/components/SortableBlock.spec.tsx index dfe5a9a47..820578661 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/DragAndDropExercise/components/SortableBlock.spec.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/DragAndDropExercise/components/SortableBlock.spec.tsx @@ -1,6 +1,7 @@ -import { renderWithMantine } from "@/test/renderWithMantine"; import { fireEvent, screen } from "@testing-library/react"; +import { renderWithMantine } from "@/test/renderWithMantine"; + import { SortableBlock } from "./SortableBlock"; vi.mock( diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/DragAndDropExercise/hooks/index.spec.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/DragAndDropExercise/hooks/index.spec.ts index ee1c5fb39..e8375c7fa 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/DragAndDropExercise/hooks/index.spec.ts +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/DragAndDropExercise/hooks/index.spec.ts @@ -1,9 +1,10 @@ import { renderHook, act } from "@testing-library/react"; import { createRef } from "react"; -import { useDragAndDropConnections } from "./index"; import { CONNECTION_TOAST_COPY } from "../../../shared/connections"; +import { useDragAndDropConnections } from "./index"; + vi.mock("../DragAndDropExercise.module.css", () => ({ default: { columnsContent: "columnsContent", diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/DragAndDropExercise/hooks/useDragAndDropConnections.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/DragAndDropExercise/hooks/useDragAndDropConnections.tsx index acaf184db..2cc0e4fbc 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/DragAndDropExercise/hooks/useDragAndDropConnections.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/DragAndDropExercise/hooks/useDragAndDropConnections.tsx @@ -3,7 +3,6 @@ import { RefObject, useCallback, useEffect, useState } from "react"; import { notify } from "@/components/ui/notify"; import { CONNECTION_TOAST_COPY } from "../../../shared/connections"; - import styles from "../DragAndDropExercise.module.css"; import { DragAndDropData } from "../types"; diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/ExerciseTypeSelect.spec.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/ExerciseTypeSelect.spec.tsx index a8fab174f..38cb2426c 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/ExerciseTypeSelect.spec.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/ExerciseTypeSelect.spec.tsx @@ -1,6 +1,7 @@ -import { renderWithMantine } from "@/test/renderWithMantine"; import { fireEvent, screen } from "@testing-library/react"; +import { renderWithMantine } from "@/test/renderWithMantine"; + import { ExerciseTypeSelect } from "./ExerciseTypeSelect"; const mockTypes = [ diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/FillInTheBlankExercise/components/BlankManager.spec.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/FillInTheBlankExercise/components/BlankManager.spec.tsx index 978ddd69b..d82353ecc 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/FillInTheBlankExercise/components/BlankManager.spec.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/FillInTheBlankExercise/components/BlankManager.spec.tsx @@ -1,9 +1,11 @@ -import { renderWithMantine } from "@/test/renderWithMantine"; import { fireEvent, screen } from "@testing-library/react"; -import { BlankManager } from "./BlankManager"; +import { renderWithMantine } from "@/test/renderWithMantine"; + import { GraderType, type BlankWithFeedback } from "../types"; +import { BlankManager } from "./BlankManager"; + vi.mock("@components/ui/Regex/RegexEditor", () => ({ RegexEditor: () =>
})); diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/FillInTheBlankExercise/components/BlankManager.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/FillInTheBlankExercise/components/BlankManager.tsx index 672d1c817..1e3739a27 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/FillInTheBlankExercise/components/BlankManager.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/FillInTheBlankExercise/components/BlankManager.tsx @@ -53,6 +53,8 @@ export const BlankManager: FC = ({ blanks, onChange, question onChange(trimmedBlanks); } + // TODO(eslint): Audit the complete dependency list without changing current behavior. + // eslint-disable-next-line react-hooks/exhaustive-deps }, [blankCount, blanks.length]); const updateBlank = ( diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/IframeExercise/components/IframeUrlInput.spec.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/IframeExercise/components/IframeUrlInput.spec.tsx index 3fc316d22..ed2cab3cc 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/IframeExercise/components/IframeUrlInput.spec.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/IframeExercise/components/IframeUrlInput.spec.tsx @@ -1,6 +1,7 @@ -import { renderWithMantine } from "@/test/renderWithMantine"; import { fireEvent, screen } from "@testing-library/react"; +import { renderWithMantine } from "@/test/renderWithMantine"; + import { IframeUrlInput } from "./IframeUrlInput"; let showValidation = false; diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/ImportQuestionJsonModal.spec.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/ImportQuestionJsonModal.spec.tsx index 9848088a6..a09c454c8 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/ImportQuestionJsonModal.spec.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/ImportQuestionJsonModal.spec.tsx @@ -1,6 +1,7 @@ -import { renderWithMantine } from "@/test/renderWithMantine"; import { fireEvent, screen } from "@testing-library/react"; +import { renderWithMantine } from "@/test/renderWithMantine"; + import { ImportQuestionJsonModal } from "./ImportQuestionJsonModal"; vi.mock("@/hooks/useExerciseTypes", () => ({ diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/MatchingExercise/MatchingExercise.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/MatchingExercise/MatchingExercise.tsx index 623eb07bc..29ee20c6c 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/MatchingExercise/MatchingExercise.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/MatchingExercise/MatchingExercise.tsx @@ -26,14 +26,14 @@ import { DEFAULT_INCORRECT_FEEDBACK, buildQuestionJson } from "@/utils/questionJ import { MATCHING_STEP_VALIDATORS } from "../../config/stepConfigs"; import { useBaseExercise } from "../../hooks/useBaseExercise"; +import { useExerciseStepNavigation } from "../../hooks/useExerciseStepNavigation"; +import { ExerciseLayout } from "../../shared/ExerciseLayout"; import { ConnectionList, blockSide, connectionExistsBetween, makeConnectionLabelResolver } from "../../shared/connections"; -import { useExerciseStepNavigation } from "../../hooks/useExerciseStepNavigation"; -import { ExerciseLayout } from "../../shared/ExerciseLayout"; import { ExerciseComponentProps } from "../../types/ExerciseTypes"; import { validateCommonFields } from "../../utils/validation"; @@ -293,14 +293,12 @@ export const MatchingExercise: FC = ({ const handleRemoveRightBlock = useCallback( (id: string) => { const hasConnections = (formData.correctAnswers || []).some( - // eslint-disable-next-line @typescript-eslint/no-unused-vars - ([_, targetId]) => targetId === id + ([, targetId]) => targetId === id ); if (hasConnections) { const updatedConnections = (formData.correctAnswers || []).filter( - // eslint-disable-next-line @typescript-eslint/no-unused-vars - ([_, targetId]) => targetId !== id + ([, targetId]) => targetId !== id ); updateFormData("correctAnswers", updatedConnections); @@ -379,7 +377,8 @@ export const MatchingExercise: FC = ({ const renderActiveLine = useCallback(() => { if (!activeSource || !hasMovedEnough) return null; - const isLeftSource = blockSide(formData.left || [], formData.right || [], activeSource) === "left"; + const isLeftSource = + blockSide(formData.left || [], formData.right || [], activeSource) === "left"; const sourcePosition = getBlockPosition(activeSource, isLeftSource); if (!sourcePosition) return null; @@ -395,7 +394,15 @@ export const MatchingExercise: FC = ({ className={styles.activePath} /> ); - }, [activeSource, hasMovedEnough, formData.left, formData.right, getBlockPosition, generatePath, mousePosition]); + }, [ + activeSource, + hasMovedEnough, + formData.left, + formData.right, + getBlockPosition, + generatePath, + mousePosition + ]); useEffect(() => { if (activeStep === 1) { diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/MatchingExercise/components/SortableBlock.spec.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/MatchingExercise/components/SortableBlock.spec.tsx index 281c48765..ab3b37e1b 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/MatchingExercise/components/SortableBlock.spec.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/MatchingExercise/components/SortableBlock.spec.tsx @@ -1,6 +1,7 @@ -import { renderWithMantine } from "@/test/renderWithMantine"; import { fireEvent, screen } from "@testing-library/react"; +import { renderWithMantine } from "@/test/renderWithMantine"; + import { SortableBlock } from "./SortableBlock"; vi.mock( diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/MatchingExercise/hooks/index.spec.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/MatchingExercise/hooks/index.spec.ts index 64f905695..48b543132 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/MatchingExercise/hooks/index.spec.ts +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/MatchingExercise/hooks/index.spec.ts @@ -1,11 +1,11 @@ import { renderHook, act } from "@testing-library/react"; import { createRef } from "react"; -import { useMatchingConnections } from "./index"; - import { CONNECTION_TOAST_COPY } from "../../../shared/connections"; import type { MatchingData } from "../types"; +import { useMatchingConnections } from "./index"; + vi.mock("../MatchingExercise.module.css", () => ({ default: { columnsContent: "columnsContent", @@ -73,21 +73,25 @@ describe("useMatchingConnections", () => { describe("initial state", () => { it("starts with no active source", () => { const { result } = renderHook(() => useMatchingConnections(makeProps(makeFormData()))); + expect(result.current.activeSource).toBeNull(); }); it("starts with mouse position at origin", () => { const { result } = renderHook(() => useMatchingConnections(makeProps(makeFormData()))); + expect(result.current.mousePosition).toEqual({ x: 0, y: 0 }); }); it("starts with hasMovedEnough as false", () => { const { result } = renderHook(() => useMatchingConnections(makeProps(makeFormData()))); + expect(result.current.hasMovedEnough).toBe(false); }); it("starts with forceRedraw at 0", () => { const { result } = renderHook(() => useMatchingConnections(makeProps(makeFormData()))); + expect(result.current.forceRedraw).toBe(0); }); }); diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/MatchingExercise/hooks/useMatchingConnections.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/MatchingExercise/hooks/useMatchingConnections.tsx index 14bd5de9d..9e89ed03e 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/MatchingExercise/hooks/useMatchingConnections.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/MatchingExercise/hooks/useMatchingConnections.tsx @@ -3,7 +3,6 @@ import { RefObject, useCallback, useEffect, useState } from "react"; import { notify } from "@/components/ui/notify"; import { CONNECTION_TOAST_COPY, blockSide } from "../../../shared/connections"; - import styles from "../MatchingExercise.module.css"; import { MatchingData } from "../types"; @@ -68,7 +67,8 @@ export const useMatchingConnections = ({ setActiveSource(sourceId); setHasMovedEnough(false); - const isLeftSource = blockSide(formData.left || [], formData.right || [], sourceId) === "left"; + const isLeftSource = + blockSide(formData.left || [], formData.right || [], sourceId) === "left"; const position = getBlockPosition(sourceId, isLeftSource); if (position) { @@ -217,7 +217,13 @@ export const useMatchingConnections = ({ window.removeEventListener("mouseup", handleMouseUp); window.removeEventListener("keydown", handleKeyDown); }; - }, [activeSource, formData.left, formData.right, getScrollableContainer, handleCompleteConnection]); + }, [ + activeSource, + formData.left, + formData.right, + getScrollableContainer, + handleCompleteConnection + ]); useEffect(() => { const scrollableContainer = getScrollableContainer(); @@ -228,7 +234,8 @@ export const useMatchingConnections = ({ triggerConnectionsRedraw(); if (activeSource) { - const isLeftSource = blockSide(formData.left || [], formData.right || [], activeSource) === "left"; + const isLeftSource = + blockSide(formData.left || [], formData.right || [], activeSource) === "left"; const sourcePosition = getBlockPosition(activeSource, isLeftSource); if (sourcePosition && !hasMovedEnough) { diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/MultiChoiceExercise/MultiChoiceOptions.spec.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/MultiChoiceExercise/MultiChoiceOptions.spec.tsx index 95dbb7242..b3a0cb3d3 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/MultiChoiceExercise/MultiChoiceOptions.spec.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/MultiChoiceExercise/MultiChoiceOptions.spec.tsx @@ -66,6 +66,7 @@ describe("MultiChoiceOptions", () => { renderWithMantine(); const removeButtons = screen.getAllByRole("button", { name: "Remove option" }); + removeButtons.forEach((btn) => expect(btn).toBeDisabled()); }); @@ -75,6 +76,7 @@ describe("MultiChoiceOptions", () => { renderWithMantine(); const correctCheckboxes = screen.getAllByRole("checkbox", { name: "Correct" }); + await userEvent.click(correctCheckboxes[1]); expect(onChange).toHaveBeenCalledWith( diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/ParsonsExercise/ParsonsExercise.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/ParsonsExercise/ParsonsExercise.tsx index 8cba33238..60a114e81 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/ParsonsExercise/ParsonsExercise.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/ParsonsExercise/ParsonsExercise.tsx @@ -22,8 +22,8 @@ import { ParsonsBlocksManager, ParsonsOptions } from "./components"; -import { ParsonsExerciseTour } from "./components/ParsonsExerciseTour"; import parsonsStyles from "./components/ParsonsExercise.module.css"; +import { ParsonsExerciseTour } from "./components/ParsonsExerciseTour"; export type ParsonsMode = "simple" | "enhanced"; @@ -224,6 +224,7 @@ export const ParsonsExercise: FC = ({ depends: undefined, displayOrder: undefined })); + updateFormData("blocks", clearedBlocks); setMode("simple"); } @@ -292,6 +293,7 @@ export const ParsonsExercise: FC = ({ } return block; }); + updateFormData("blocks", updatedBlocks); } }} diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/ParsonsExercise/components/BlockItem.spec.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/ParsonsExercise/components/BlockItem.spec.tsx index 4c07d126d..e6f45c915 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/ParsonsExercise/components/BlockItem.spec.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/ParsonsExercise/components/BlockItem.spec.tsx @@ -1,7 +1,6 @@ import userEvent from "@testing-library/user-event"; import { renderWithMantine, screen } from "@/test/renderWithMantine"; - import { ParsonsBlock } from "@/utils/preview/parsonsPreview"; import { BlockItem } from "./BlockItem"; diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/ParsonsExercise/components/BlockItem.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/ParsonsExercise/components/BlockItem.tsx index 918ae7b86..4bb9133a2 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/ParsonsExercise/components/BlockItem.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/ParsonsExercise/components/BlockItem.tsx @@ -106,6 +106,7 @@ export const BlockItem: FC = ({ const calculateEditorHeight = useCallback((content: string) => { if (!content) return MIN_EDITOR_HEIGHT; const lineCount = content.split("\n").length; + return Math.max(lineCount * LINE_HEIGHT + EDITOR_PADDING + LINE_HEIGHT / 2, MIN_EDITOR_HEIGHT); }, []); @@ -116,6 +117,7 @@ export const BlockItem: FC = ({ const handleContentChange = useCallback( (e: React.ChangeEvent | string) => { const newContent = typeof e === "string" ? e : e.target.value; + onContentChange(block.id, newContent); }, [block.id, onContentChange] @@ -159,8 +161,10 @@ export const BlockItem: FC = ({ (y: number, monacoEditor: Element | null): { lineIndex: number; offsetY: number } | null => { if (!monacoEditor) return null; const lines = block.content.split("\n"); + if (lines.length <= 1) return null; const lineElements = monacoEditor.querySelectorAll(".view-line"); + if (!lineElements || lineElements.length <= 1) return null; let closestLine = 0; @@ -169,10 +173,12 @@ export const BlockItem: FC = ({ for (let i = 0; i < lineElements.length; i++) { const lineRect = (lineElements[i] as HTMLElement).getBoundingClientRect(); + if (i < lineElements.length - 1) { const nextRect = (lineElements[i + 1] as HTMLElement).getBoundingClientRect(); const boundary = (lineRect.bottom + nextRect.top) / 2; const distance = Math.abs(y - boundary); + if (distance < closestDistance) { closestDistance = distance; closestLine = i + 1; @@ -191,9 +197,11 @@ export const BlockItem: FC = ({ setCursorPosition({ x: e.clientX, y: e.clientY }); const container = editorContainerRef.current; const monacoEditor = container.querySelector(".monaco-editor"); + if (monacoEditor) { const containerRect = container.getBoundingClientRect(); const boundary = findNearestLineBoundary(e.clientY, monacoEditor); + if (boundary) { setHoveredLine(boundary.lineIndex); setHoveredLineOffset(boundary.offsetY - containerRect.top); @@ -213,14 +221,17 @@ export const BlockItem: FC = ({ if (cursorPosition && dividerRef.current && editorContainerRef.current) { const container = editorContainerRef.current; const monacoEditor = container.querySelector(".monaco-editor"); + if (monacoEditor) { const boundary = findNearestLineBoundary(cursorPosition.y, monacoEditor); + if (boundary) { setHoveredLineOffset(boundary.offsetY - container.getBoundingClientRect().top); } } } }; + window.addEventListener("scroll", handleScroll, true); return () => window.removeEventListener("scroll", handleScroll, true); }, [hoveredLine, cursorPosition, findNearestLineBoundary]); @@ -239,6 +250,7 @@ export const BlockItem: FC = ({ const getDividerStyle = useCallback((): CSSProperties => { const base: CSSProperties = { top: `${hoveredLineOffset}px` }; + return cursorPosition ? { ...base, pointerEvents: "none" as const } : base; }, [hoveredLineOffset, cursorPosition]); @@ -370,6 +382,7 @@ export const BlockItem: FC = ({ onClick={(e) => { e.stopPropagation(); const newIsDistractor = !block.isDistractor; + onDistractorChange!(block.id, newIsDistractor); }} onMouseDown={(e) => e.stopPropagation()} @@ -380,6 +393,7 @@ export const BlockItem: FC = ({ if (e.key === "Enter" || e.key === " ") { e.preventDefault(); const newIsDistractor = !block.isDistractor; + onDistractorChange!(block.id, newIsDistractor); } }} @@ -410,6 +424,7 @@ export const BlockItem: FC = ({ checked={block.isDistractor === true && block.pairedWithBlockAbove === true} onChange={(e) => { const paired = e.currentTarget.checked; + if (paired && !block.isDistractor) { onDistractorChange!(block.id, true); } @@ -466,6 +481,7 @@ export const BlockItem: FC = ({ .split(",") .map((d) => d.trim()) .filter((d) => d !== ""); + onDependsChange(block.id, deps); }} className={styles.optionsPanelInput} @@ -495,6 +511,7 @@ export const BlockItem: FC = ({ value={block.displayOrder !== undefined ? String(block.displayOrder) : ""} onChange={(e) => { const val = parseInt(e.target.value); + onOrderChange(block.id, isNaN(val) ? 0 : val); }} className={styles.optionsPanelInput} @@ -625,6 +642,7 @@ export const BlockItem: FC = ({ checked={block.isDistractor === true && block.pairedWithBlockAbove === true} onChange={(e) => { const paired = e.currentTarget.checked; + if (paired && !block.isDistractor) { onDistractorChange!(block.id, true); } diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/ParsonsExercise/components/ParsonsBlocksManager.spec.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/ParsonsExercise/components/ParsonsBlocksManager.spec.tsx index 16b287fa0..bcce94e63 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/ParsonsExercise/components/ParsonsBlocksManager.spec.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/ParsonsExercise/components/ParsonsBlocksManager.spec.tsx @@ -1,7 +1,6 @@ import userEvent from "@testing-library/user-event"; import { renderWithMantine, screen } from "@/test/renderWithMantine"; - import { ParsonsBlock } from "@/utils/preview/parsonsPreview"; import { ParsonsBlocksManager } from "./ParsonsBlocksManager"; diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/ParsonsExercise/components/ParsonsBlocksManager.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/ParsonsExercise/components/ParsonsBlocksManager.tsx index d837f7ad5..e55e5ae84 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/ParsonsExercise/components/ParsonsBlocksManager.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/ParsonsExercise/components/ParsonsBlocksManager.tsx @@ -527,6 +527,8 @@ export const ParsonsBlocksManager: FC = ({ (id: string, isCorrect: boolean) => { if (!isCorrect) return; + // TODO(eslint): Rename the callback parameter without obscuring the surrounding block value. + // eslint-disable-next-line @typescript-eslint/no-shadow const block = blocks.find((block) => block.id === id); if (!block || !block.groupId) return; @@ -602,6 +604,7 @@ export const ParsonsBlocksManager: FC = ({ } return block; }); + onChange(newBlocks); }, [blocks, onChange] @@ -615,6 +618,7 @@ export const ParsonsBlocksManager: FC = ({ } return block; }); + onChange(newBlocks); }, [blocks, onChange] @@ -628,6 +632,7 @@ export const ParsonsBlocksManager: FC = ({ } return block; }); + onChange(newBlocks); }, [blocks, onChange] @@ -641,6 +646,7 @@ export const ParsonsBlocksManager: FC = ({ } return block; }); + onChange(newBlocks); }, [blocks, onChange] @@ -654,6 +660,7 @@ export const ParsonsBlocksManager: FC = ({ } return block; }); + onChange(newBlocks); }, [blocks, onChange] @@ -667,6 +674,7 @@ export const ParsonsBlocksManager: FC = ({ } return block; }); + onChange(newBlocks); }, [blocks, onChange] @@ -680,11 +688,13 @@ export const ParsonsBlocksManager: FC = ({ const map: Record = {}; let idx = 0; const processedGroups = new Set(); + blocks.forEach((block) => { if (block.groupId) { if (!processedGroups.has(block.groupId)) { processedGroups.add(block.groupId); const groupBlocks = blocks.filter((b) => b.groupId === block.groupId); + groupBlocks.forEach((b) => { map[b.id] = idx; }); diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/ParsonsExercise/components/ParsonsExerciseTour.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/ParsonsExercise/components/ParsonsExerciseTour.tsx index 04ad9eb64..ea39c0149 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/ParsonsExercise/components/ParsonsExerciseTour.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/ParsonsExercise/components/ParsonsExerciseTour.tsx @@ -40,11 +40,13 @@ export interface ParsonsExerciseTourProps { const waitForElement = (selector: string, timeout = 1500): Promise => new Promise((resolve) => { const el = document.querySelector(selector); + if (el) return resolve(el); const start = Date.now(); const interval = setInterval(() => { const found = document.querySelector(selector); + if (found || Date.now() - start > timeout) { clearInterval(interval); resolve(found); @@ -86,6 +88,7 @@ export const ParsonsExerciseTour: FC = ({ const restoreSnapshot = useCallback(() => { const snap = snapshotRef.current; + if (!snap) return; onModeChange(snap.mode); @@ -106,6 +109,7 @@ export const ParsonsExerciseTour: FC = ({ const ensureBlockExists = useCallback(async () => { const blocks = formData.blocks ?? []; + if (blocks.length === 0) { updateFormData("blocks", [{ ...DEMO_BLOCK }]); await nextTick(); @@ -114,13 +118,16 @@ export const ParsonsExerciseTour: FC = ({ const ensureDemoContent = useCallback(async () => { const blocks = formData.blocks ?? []; + if (blocks.length === 0) { updateFormData("blocks", [{ ...DEMO_BLOCK }]); await nextTick(); } else { const first = blocks[0]; + if (first.content.split("\n").length < 2) { const updated = [...blocks]; + updated[0] = { ...first, content: "print('Hello')\nprint('World')" }; updateFormData("blocks", updated); await nextTick(); @@ -203,6 +210,8 @@ export const ParsonsExerciseTour: FC = ({ }, ...(stepBehaviors[idx] && { onHighlightStarted: stepBehaviors[idx] }) })); + // TODO(eslint): Audit the complete dependency list without changing current behavior. + // eslint-disable-next-line react-hooks/exhaustive-deps }, [mode, formData, onModeChange, updateFormData, ensureBlockExists, ensureDemoContent]); const startTour = useCallback(() => { diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/PollExercise/PollOptions.spec.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/PollExercise/PollOptions.spec.tsx index aa0ada063..decc4e6f1 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/PollExercise/PollOptions.spec.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/PollExercise/PollOptions.spec.tsx @@ -1,6 +1,7 @@ -import { renderWithMantine } from "@/test/renderWithMantine"; import { fireEvent, screen } from "@testing-library/react"; +import { renderWithMantine } from "@/test/renderWithMantine"; + import { PollOptions } from "./PollOptions"; vi.mock( diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/PollExercise/ScaleSettings.spec.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/PollExercise/ScaleSettings.spec.tsx index 31e389df5..316e81861 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/PollExercise/ScaleSettings.spec.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/PollExercise/ScaleSettings.spec.tsx @@ -1,6 +1,7 @@ -import { renderWithMantine } from "@/test/renderWithMantine"; import { fireEvent, screen } from "@testing-library/react"; +import { renderWithMantine } from "@/test/renderWithMantine"; + import { ScaleSettings } from "./ScaleSettings"; describe("ScaleSettings", () => { diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/SelectQuestionExercise/SelectQuestionExercise.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/SelectQuestionExercise/SelectQuestionExercise.tsx index 99e7e3293..338850e60 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/SelectQuestionExercise/SelectQuestionExercise.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/SelectQuestionExercise/SelectQuestionExercise.tsx @@ -127,6 +127,7 @@ export const SelectQuestionExercise: FC = ({ const handleQuestionListChange = (questionList: QuestionWithLabel[]) => { const labelsRecord: Record = {}; + questionList.forEach((q) => { if (q.label) { labelsRecord[q.questionId] = q.label; diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/SelectQuestionExercise/components/ABExperimentSettings.spec.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/SelectQuestionExercise/components/ABExperimentSettings.spec.tsx index 0165f9a62..868589b70 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/SelectQuestionExercise/components/ABExperimentSettings.spec.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/SelectQuestionExercise/components/ABExperimentSettings.spec.tsx @@ -1,6 +1,7 @@ -import { renderWithMantine } from "@/test/renderWithMantine"; import { fireEvent, screen } from "@testing-library/react"; +import { renderWithMantine } from "@/test/renderWithMantine"; + import { ABExperimentSettings } from "./ABExperimentSettings"; describe("ABExperimentSettings", () => { diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/SelectQuestionExercise/components/QuestionListEditor.spec.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/SelectQuestionExercise/components/QuestionListEditor.spec.tsx index a6d97123e..21ff202ca 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/SelectQuestionExercise/components/QuestionListEditor.spec.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/SelectQuestionExercise/components/QuestionListEditor.spec.tsx @@ -1,6 +1,7 @@ -import { renderWithMantine } from "@/test/renderWithMantine"; import { fireEvent, screen } from "@testing-library/react"; +import { renderWithMantine } from "@/test/renderWithMantine"; + import { QuestionListEditor } from "./QuestionListEditor"; vi.mock("@/hooks/useSmartExerciseSearch", () => { diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/SelectQuestionExercise/components/QuestionListEditor.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/SelectQuestionExercise/components/QuestionListEditor.tsx index d9da62692..7e424c700 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/SelectQuestionExercise/components/QuestionListEditor.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/SelectQuestionExercise/components/QuestionListEditor.tsx @@ -65,11 +65,14 @@ export const QuestionListEditor: FC = ({ const availableExercises = exercises.filter( (ex) => ex.name && !existingQuestionIds.includes(ex.name) ); + setSuggestions(createSuggestions(availableExercises)); }, [exercises, existingQuestionIds, createSuggestions]); useEffect(() => { toggleBaseCourse(dataLimitBasecourse); + // TODO(eslint): Audit the complete dependency list without changing current behavior. + // eslint-disable-next-line react-hooks/exhaustive-deps }, [dataLimitBasecourse]); useEffect(() => { diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/SelectQuestionExercise/components/ToggleOptionsSettings.spec.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/SelectQuestionExercise/components/ToggleOptionsSettings.spec.tsx index ddc2f9731..ffc42d3b9 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/SelectQuestionExercise/components/ToggleOptionsSettings.spec.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/SelectQuestionExercise/components/ToggleOptionsSettings.spec.tsx @@ -1,6 +1,7 @@ -import { renderWithMantine } from "@/test/renderWithMantine"; import { fireEvent, screen } from "@testing-library/react"; +import { renderWithMantine } from "@/test/renderWithMantine"; + import { ToggleOptionsSettings } from "./ToggleOptionsSettings"; vi.mock("./ToggleOptionsSettings.module.css", () => ({ default: {} })); diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/ShortAnswerExercise/components/ShortAnswerInstructions.spec.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/ShortAnswerExercise/components/ShortAnswerInstructions.spec.tsx index e3d4e39fa..c58698f98 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/ShortAnswerExercise/components/ShortAnswerInstructions.spec.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/ShortAnswerExercise/components/ShortAnswerInstructions.spec.tsx @@ -1,6 +1,7 @@ -import { renderWithMantine } from "@/test/renderWithMantine"; import { fireEvent, screen } from "@testing-library/react"; +import { renderWithMantine } from "@/test/renderWithMantine"; + import { ShortAnswerInstructions } from "./ShortAnswerInstructions"; vi.mock( diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/config/stepConfigs.spec.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/config/stepConfigs.spec.ts index dcc0bafeb..1c79c9b05 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/config/stepConfigs.spec.ts +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/config/stepConfigs.spec.ts @@ -110,8 +110,7 @@ describe("CLICKABLE_AREA_STEP_VALIDATORS content step", () => { }); it("passes once both mark kinds exist", () => { - const marked = - '

ab

'; + const marked = '

ab

'; expect(validateContentStep({ statement, questionText: marked })).toHaveLength(0); }); diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/config/stepConfigs.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/config/stepConfigs.ts index 9043f3876..cae1dad4f 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/config/stepConfigs.ts +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/config/stepConfigs.ts @@ -283,12 +283,14 @@ export const PARSONS_STEP_VALIDATORS: StepValidator[] = [ // Check that all non-distractor blocks have tags const missingTags = nonDistractorBlocks.filter((b) => !b.tag); + if (missingTags.length > 0) { errors.push("All non-distractor blocks must have a tag when using DAG grader"); } // Check for duplicate tags const tagSet = new Set(); + for (const tag of tags) { if (tagSet.has(tag)) { errors.push(`Duplicate tag "${tag}" found. Each block must have a unique tag`); @@ -316,6 +318,7 @@ export const PARSONS_STEP_VALIDATORS: StepValidator[] = [ if (data.blocks?.length) { for (let i = 0; i < data.blocks.length; i++) { const block = data.blocks[i]; + if (block.pairedWithBlockAbove && block.isDistractor) { if (i === 0) { errors.push( @@ -323,6 +326,7 @@ export const PARSONS_STEP_VALIDATORS: StepValidator[] = [ ); } else { const blockAbove = data.blocks[i - 1]; + if (blockAbove.isDistractor) { errors.push( `Paired distractor "${block.content.substring(0, 30)}…" must be placed directly after a solution block, not another distractor` @@ -770,12 +774,14 @@ export const CLICKABLE_AREA_STEP_VALIDATORS: StepValidator { describe("initial state", () => { it("returns default form data when no initialData is provided", () => { const { result } = renderHook(() => useBaseExercise(makeProps())); + expect(result.current.formData).toMatchObject(makeDefaultFormData()); }); @@ -65,6 +66,7 @@ describe("useBaseExercise", () => { initialData: { statement: "Custom question", points: 5 } }); const { result } = renderHook(() => useBaseExercise(props)); + expect(result.current.formData.statement).toBe("Custom question"); expect(result.current.formData.points).toBe(5); expect(result.current.formData.chapter).toBe(""); @@ -72,11 +74,13 @@ describe("useBaseExercise", () => { it("starts at step 0", () => { const { result } = renderHook(() => useBaseExercise(makeProps())); + expect(result.current.activeStep).toBe(0); }); it("initializes isSaving as false", () => { const { result } = renderHook(() => useBaseExercise(makeProps())); + expect(result.current.isSaving).toBe(false); }); @@ -84,16 +88,19 @@ describe("useBaseExercise", () => { const { result } = renderHook(() => useBaseExercise(makeProps({ steps: [{ label: "A" }, { label: "B" }, { label: "C" }] })) ); + expect(result.current.stepsVisited).toEqual({ 0: false, 1: false, 2: false }); }); it("initializes questionInteracted as false", () => { const { result } = renderHook(() => useBaseExercise(makeProps())); + expect(result.current.questionInteracted).toBe(false); }); it("initializes settingsInteracted as false", () => { const { result } = renderHook(() => useBaseExercise(makeProps())); + expect(result.current.settingsInteracted).toBe(false); }); @@ -105,6 +112,7 @@ describe("useBaseExercise", () => { }); const { result } = renderHook(() => useBaseExercise(props)); const options = result.current.formData.optionList as Array<{ choice: string; id: string }>; + expect(options[0].id).toMatch(/^option-/); expect(options[1].id).toBe("existing-id"); }); @@ -112,6 +120,7 @@ describe("useBaseExercise", () => { it("handles missing optionList without error", () => { const props = makeProps({ initialData: { statement: "No options" } }); const { result } = renderHook(() => useBaseExercise(props)); + expect(result.current.formData.optionList).toEqual([]); }); }); @@ -119,6 +128,7 @@ describe("useBaseExercise", () => { describe("updateFormData", () => { it("updates a single field in formData", () => { const { result } = renderHook(() => useBaseExercise(makeProps())); + act(() => { result.current.updateFormData("statement", "Updated question"); }); @@ -128,6 +138,7 @@ describe("useBaseExercise", () => { it("preserves other fields when updating one field", () => { const props = makeProps({ initialData: { points: 5, statement: "original" } }); const { result } = renderHook(() => useBaseExercise(props)); + act(() => { result.current.updateFormData("statement", "changed"); }); @@ -138,6 +149,7 @@ describe("useBaseExercise", () => { describe("handleSettingsChange", () => { it("merges partial settings into formData", () => { const { result } = renderHook(() => useBaseExercise(makeProps())); + act(() => { result.current.handleSettingsChange({ points: 10, difficulty: 5 }); }); @@ -147,6 +159,7 @@ describe("useBaseExercise", () => { it("sets settingsInteracted to true", () => { const { result } = renderHook(() => useBaseExercise(makeProps())); + act(() => { result.current.handleSettingsChange({ points: 3 }); }); @@ -157,6 +170,7 @@ describe("useBaseExercise", () => { describe("handleQuestionChange", () => { it("updates the statement field", () => { const { result } = renderHook(() => useBaseExercise(makeProps())); + act(() => { result.current.handleQuestionChange("New content"); }); @@ -165,6 +179,7 @@ describe("useBaseExercise", () => { it("sets questionInteracted to true", () => { const { result } = renderHook(() => useBaseExercise(makeProps())); + act(() => { result.current.handleQuestionChange("content"); }); @@ -176,12 +191,14 @@ describe("useBaseExercise", () => { it("returns true when validateStep returns true", () => { const props = makeProps({ validateStep: () => true }); const { result } = renderHook(() => useBaseExercise(props)); + expect(result.current.isCurrentStepValid()).toBe(true); }); it("returns false when validateStep returns false", () => { const props = makeProps({ validateStep: () => false }); const { result } = renderHook(() => useBaseExercise(props)); + expect(result.current.isCurrentStepValid()).toBe(false); }); @@ -189,6 +206,7 @@ describe("useBaseExercise", () => { const validateStep = vi.fn().mockReturnValue(true); const props = makeProps({ validateStep }); const { result } = renderHook(() => useBaseExercise(props)); + result.current.isCurrentStepValid(); expect(validateStep).toHaveBeenCalledWith(0, expect.any(Object)); }); @@ -198,6 +216,7 @@ describe("useBaseExercise", () => { it("increments activeStep when current step is valid", () => { const props = makeProps({ validateStep: () => true }); const { result } = renderHook(() => useBaseExercise(props)); + act(() => { result.current.goToNextStep(); }); @@ -207,6 +226,7 @@ describe("useBaseExercise", () => { it("does not increment activeStep when current step is invalid", () => { const props = makeProps({ validateStep: () => false }); const { result } = renderHook(() => useBaseExercise(props)); + act(() => { result.current.goToNextStep(); }); @@ -216,6 +236,7 @@ describe("useBaseExercise", () => { it("marks the current step as visited when advancing", () => { const props = makeProps({ validateStep: () => true }); const { result } = renderHook(() => useBaseExercise(props)); + act(() => { result.current.goToNextStep(); }); @@ -225,6 +246,7 @@ describe("useBaseExercise", () => { it("does not mark step as visited when step is invalid", () => { const props = makeProps({ validateStep: () => false }); const { result } = renderHook(() => useBaseExercise(props)); + act(() => { result.current.goToNextStep(); }); @@ -236,6 +258,7 @@ describe("useBaseExercise", () => { it("decrements activeStep when not at first step", () => { const props = makeProps({ validateStep: () => true }); const { result } = renderHook(() => useBaseExercise(props)); + act(() => { result.current.goToNextStep(); }); @@ -247,6 +270,7 @@ describe("useBaseExercise", () => { it("does not go below step 0", () => { const { result } = renderHook(() => useBaseExercise(makeProps())); + act(() => { result.current.goToPrevStep(); }); @@ -264,6 +288,7 @@ describe("useBaseExercise", () => { generatePreview: (data) => `

${data.statement}

` }); const { result } = renderHook(() => useBaseExercise(props)); + await act(async () => { await result.current.handleSave(); }); @@ -282,6 +307,7 @@ describe("useBaseExercise", () => { } }); const { result } = renderHook(() => useBaseExercise(props)); + await act(async () => { await result.current.handleSave(); }); @@ -301,6 +327,7 @@ describe("useBaseExercise", () => { initialData: { statement: "Q1", points: 2 } }); const { result } = renderHook(() => useBaseExercise(props)); + await act(async () => { await result.current.handleSave(); }); @@ -320,6 +347,7 @@ describe("useBaseExercise", () => { }); const props = makeProps({ validateForm: () => [], onSave }); const { result } = renderHook(() => useBaseExercise(props)); + await act(async () => { await result.current.handleSave(); }); @@ -334,6 +362,7 @@ describe("useBaseExercise", () => { onSave }); const { result } = renderHook(() => useBaseExercise(props)); + await act(async () => { await result.current.handleSave(); }); @@ -348,6 +377,7 @@ describe("useBaseExercise", () => { steps: [{ label: "S1" }, { label: "S2" }] }); const { result } = renderHook(() => useBaseExercise(props)); + await act(async () => { await result.current.handleSave(); }); @@ -357,6 +387,7 @@ describe("useBaseExercise", () => { it("sets questionInteracted and settingsInteracted to true on validation failure", async () => { const props = makeProps({ validateForm: () => ["error"] }); const { result } = renderHook(() => useBaseExercise(props)); + await act(async () => { await result.current.handleSave(); }); @@ -372,6 +403,7 @@ describe("useBaseExercise", () => { getDefaultFormData: () => ({ statement: "", chapter: "", question_type: "mchoice" }) }); const { result } = renderHook(() => useBaseExercise(props)); + await act(async () => { await result.current.handleSave(); }); @@ -386,6 +418,7 @@ describe("useBaseExercise", () => { getDefaultFormData: () => ({ statement: "", chapter: "", question_type: "mchoice" }) }); const { result } = renderHook(() => useBaseExercise(props)); + await act(async () => { await result.current.handleSave(); }); @@ -399,6 +432,7 @@ describe("useBaseExercise", () => { const onSave = vi.fn().mockRejectedValue(new Error("network error")); const props = makeProps({ validateForm: () => [], onSave }); const { result } = renderHook(() => useBaseExercise(props)); + await act(async () => { await result.current.handleSave(); }); @@ -414,6 +448,7 @@ describe("useBaseExercise", () => { generatePreview: () => "

preview

" }); const { result } = renderHook(() => useBaseExercise(props)); + await act(async () => { await result.current.handleSave(); }); @@ -424,11 +459,13 @@ describe("useBaseExercise", () => { describe("isDirty", () => { it("starts clean", () => { const { result } = renderHook(() => useBaseExercise(makeProps())); + expect(result.current.isDirty).toBe(false); }); it("becomes dirty after updateFormData", () => { const { result } = renderHook(() => useBaseExercise(makeProps())); + act(() => { result.current.updateFormData("statement", "changed"); }); @@ -437,6 +474,7 @@ describe("useBaseExercise", () => { it("becomes dirty after handleQuestionChange", () => { const { result } = renderHook(() => useBaseExercise(makeProps())); + act(() => { result.current.handleQuestionChange("typed"); }); @@ -445,6 +483,7 @@ describe("useBaseExercise", () => { it("becomes dirty after handleSettingsChange", () => { const { result } = renderHook(() => useBaseExercise(makeProps())); + act(() => { result.current.handleSettingsChange({ points: 7 }); }); @@ -504,6 +543,7 @@ describe("useBaseExercise", () => { (p: UseBaseExerciseProps) => useBaseExercise(p), { initialProps: props } ); + act(() => { result.current.goToNextStep(); }); @@ -519,6 +559,7 @@ describe("useBaseExercise", () => { onFormReset: undefined }); const { result } = renderHook(() => useBaseExercise(props)); + expect(result.current.activeStep).toBe(0); }); }); diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/hooks/useBaseExercise.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/hooks/useBaseExercise.ts index d5f73a914..0ef6ea3a7 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/hooks/useBaseExercise.ts +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/hooks/useBaseExercise.ts @@ -1,9 +1,9 @@ +import { OptionWithId } from "@components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components"; +import { notify } from "@components/ui/notify"; import { useCallback, useEffect, useState } from "react"; import { CreateExerciseFormType, Option } from "@/types/exercises"; -import { notify } from "@components/ui/notify"; import { createExerciseId } from "@/utils/exercise"; -import { OptionWithId } from "@components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components"; export type UseBaseExerciseProps> = { initialData?: T; @@ -22,6 +22,7 @@ export type UseBaseExerciseProps> = { const handleOptionIds = (options?: OptionWithId[] | Option[]) => { const opts = options ?? []; + return opts.map((opt) => { if (!("id" in opt) || !opt.id) { return { diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/hooks/useExerciseStepNavigation.spec.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/hooks/useExerciseStepNavigation.spec.ts index 5b73a8e15..116b566c2 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/hooks/useExerciseStepNavigation.spec.ts +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/hooks/useExerciseStepNavigation.spec.ts @@ -1,10 +1,12 @@ import { renderHook, act } from "@testing-library/react"; -import { useExerciseStepNavigation } from "./useExerciseStepNavigation"; -import type { UseExerciseStepNavigationProps } from "./useExerciseStepNavigation"; import type { CreateExerciseFormType } from "@/types/exercises"; + import type { StepValidator } from "../config/stepConfigs"; +import { useExerciseStepNavigation } from "./useExerciseStepNavigation"; +import type { UseExerciseStepNavigationProps } from "./useExerciseStepNavigation"; + type TestData = Partial; const steps = [{ label: "Step 1" }, { label: "Step 2" }, { label: "Step 3" }]; @@ -33,6 +35,7 @@ describe("useExerciseStepNavigation", () => { it("returns undefined validation before any user interaction", () => { const props = makeProps(); const { result } = renderHook(() => useExerciseStepNavigation(props)); + expect(result.current.validation).toBeUndefined(); }); @@ -44,6 +47,7 @@ describe("useExerciseStepNavigation", () => { ]; const props = makeProps({ stepValidators: validators }); const { result } = renderHook(() => useExerciseStepNavigation(props)); + expect(result.current.stepsValidity[0]).toBe(true); expect(result.current.stepsValidity[1]).toBe(false); expect(result.current.stepsValidity[2]).toBe(true); @@ -52,6 +56,7 @@ describe("useExerciseStepNavigation", () => { it("marks a step as valid when there is no validator for that index", () => { const props = makeProps({ stepValidators: [] }); const { result } = renderHook(() => useExerciseStepNavigation(props)); + expect(result.current.stepsValidity[0]).toBe(true); expect(result.current.stepsValidity[1]).toBe(true); expect(result.current.stepsValidity[2]).toBe(true); @@ -63,6 +68,7 @@ describe("useExerciseStepNavigation", () => { const goToNextStep = vi.fn(); const props = makeProps({ goToNextStep, stepValidators: [validatorAlwaysValid] }); const { result } = renderHook(() => useExerciseStepNavigation(props)); + act(() => result.current.handleNext()); expect(goToNextStep).toHaveBeenCalledOnce(); }); @@ -74,6 +80,7 @@ describe("useExerciseStepNavigation", () => { stepValidators: [validatorAlwaysInvalid, validatorAlwaysValid, validatorAlwaysValid] }); const { result } = renderHook(() => useExerciseStepNavigation(props)); + act(() => result.current.handleNext()); expect(goToNextStep).not.toHaveBeenCalled(); }); @@ -83,6 +90,7 @@ describe("useExerciseStepNavigation", () => { stepValidators: [validatorAlwaysInvalid, validatorAlwaysValid, validatorAlwaysValid] }); const { result } = renderHook(() => useExerciseStepNavigation(props)); + act(() => result.current.handleNext()); expect(result.current.validation?.isValid).toBe(false); expect(result.current.validation?.errors).toEqual(["Field is required"]); @@ -91,6 +99,7 @@ describe("useExerciseStepNavigation", () => { it("exposes empty errors after handleNext on a valid step", () => { const props = makeProps({ stepValidators: [validatorAlwaysValid] }); const { result } = renderHook(() => useExerciseStepNavigation(props)); + act(() => result.current.handleNext()); expect(result.current.validation?.isValid).toBe(true); expect(result.current.validation?.errors).toEqual([]); @@ -99,6 +108,7 @@ describe("useExerciseStepNavigation", () => { it("returns isValid true when there is no validator for the active step and handleNext is called", () => { const props = makeProps({ stepValidators: [] }); const { result } = renderHook(() => useExerciseStepNavigation(props)); + act(() => result.current.handleNext()); expect(result.current.validation?.isValid).toBe(true); }); @@ -113,6 +123,7 @@ describe("useExerciseStepNavigation", () => { stepValidators: [validatorAlwaysInvalid, validatorAlwaysInvalid, validatorAlwaysInvalid] }); const { result } = renderHook(() => useExerciseStepNavigation(props)); + act(() => result.current.handleStepSelect(0)); expect(setActiveStep).toHaveBeenCalledWith(0); expect(result.current.validation).toBeUndefined(); @@ -126,6 +137,7 @@ describe("useExerciseStepNavigation", () => { stepValidators: [validatorAlwaysValid, validatorAlwaysValid, validatorAlwaysValid] }); const { result } = renderHook(() => useExerciseStepNavigation(props)); + act(() => result.current.handleStepSelect(2)); expect(setActiveStep).toHaveBeenCalledWith(2); }); @@ -138,6 +150,7 @@ describe("useExerciseStepNavigation", () => { stepValidators: [validatorAlwaysInvalid, validatorAlwaysValid, validatorAlwaysValid] }); const { result } = renderHook(() => useExerciseStepNavigation(props)); + act(() => result.current.handleStepSelect(1)); expect(setActiveStep).not.toHaveBeenCalled(); }); @@ -148,6 +161,7 @@ describe("useExerciseStepNavigation", () => { stepValidators: [validatorAlwaysInvalid, validatorAlwaysValid, validatorAlwaysValid] }); const { result } = renderHook(() => useExerciseStepNavigation(props)); + act(() => result.current.handleStepSelect(1)); expect(result.current.validation?.isValid).toBe(false); }); @@ -160,6 +174,7 @@ describe("useExerciseStepNavigation", () => { stepValidators: [validatorAlwaysInvalid, validatorAlwaysInvalid, validatorAlwaysInvalid] }); const { result } = renderHook(() => useExerciseStepNavigation(props)); + act(() => result.current.handleStepSelect(1)); expect(setActiveStep).toHaveBeenCalledWith(1); }); @@ -173,6 +188,7 @@ describe("useExerciseStepNavigation", () => { stepValidators: [validatorAlwaysValid, validatorAlwaysValid, validatorAlwaysValid] }); const { result } = renderHook(() => useExerciseStepNavigation(props)); + await act(async () => result.current.handleSave()); expect(handleBaseSave).toHaveBeenCalledOnce(); }); @@ -184,6 +200,7 @@ describe("useExerciseStepNavigation", () => { stepValidators: [validatorAlwaysValid, validatorAlwaysInvalid, validatorAlwaysValid] }); const { result } = renderHook(() => useExerciseStepNavigation(props)); + await act(async () => result.current.handleSave()); expect(handleBaseSave).not.toHaveBeenCalled(); }); @@ -201,6 +218,7 @@ describe("useExerciseStepNavigation", () => { stepValidators: [validatorAlwaysValid, validatorAlwaysValid, validatorAlwaysValid] }); const { result } = renderHook(() => useExerciseStepNavigation(props)); + await act(async () => result.current.handleSave()); expect(generateHtmlSrc).toHaveBeenCalledWith(data); expect(updateFormData).toHaveBeenCalledWith("htmlsrc", "generated"); @@ -216,6 +234,7 @@ describe("useExerciseStepNavigation", () => { stepValidators: [validatorAlwaysValid, validatorAlwaysValid, validatorAlwaysValid] }); const { result } = renderHook(() => useExerciseStepNavigation(props)); + await act(async () => result.current.handleSave()); expect(generateHtmlSrc).not.toHaveBeenCalled(); expect(handleBaseSave).toHaveBeenCalledOnce(); @@ -227,6 +246,7 @@ describe("useExerciseStepNavigation", () => { stepValidators: [validatorAlwaysInvalid, validatorAlwaysValid, validatorAlwaysValid] }); const { result } = renderHook(() => useExerciseStepNavigation(props)); + await act(async () => result.current.handleSave()); expect(result.current.validation?.isValid).toBe(false); }); @@ -255,6 +275,7 @@ describe("useExerciseStepNavigation", () => { it("returns true for every step index when stepValidators array is empty", () => { const props = makeProps({ stepValidators: [] }); const { result } = renderHook(() => useExerciseStepNavigation(props)); + steps.forEach((_, i) => { expect(result.current.stepsValidity[i]).toBe(true); }); diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/hooks/useStepValidation.spec.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/hooks/useStepValidation.spec.ts index 1aa285be6..6f9acdf70 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/hooks/useStepValidation.spec.ts +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/hooks/useStepValidation.spec.ts @@ -3,6 +3,7 @@ import { renderHook } from "@testing-library/react"; import { CreateExerciseFormType } from "@/types/exercises"; import { StepValidator } from "../config/stepConfigs"; + import { useStepValidation } from "./useStepValidation"; type TestData = Partial; @@ -140,6 +141,7 @@ describe("useStepValidation", () => { ); const first = result.current; + rerender({ d: data, step: 0, v: validators }); expect(result.current).toBe(first); diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/shared/BaseExerciseSettingsContent.spec.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/shared/BaseExerciseSettingsContent.spec.tsx index b2f1cbb57..fb21fea92 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/shared/BaseExerciseSettingsContent.spec.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/shared/BaseExerciseSettingsContent.spec.tsx @@ -35,6 +35,7 @@ describe("BaseExerciseSettingsContent", () => { await waitFor(() => { const settings = lastSettings(onSettingsChange); + expect(settings.name).toBe("generated-id"); expect(settings.chapter).toBe("ch1"); expect(settings.subchapter).toBe("sec1"); diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/shared/BaseExerciseSettingsContent.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/shared/BaseExerciseSettingsContent.tsx index 099653e04..82aaf39ca 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/shared/BaseExerciseSettingsContent.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/shared/BaseExerciseSettingsContent.tsx @@ -1,5 +1,5 @@ -import { useGetSectionsForChapterQuery } from "@store/dataset/dataset.logic.api"; import { NumberInput, Select, Switch, TagsInput, TextInput } from "@mantine/core"; +import { useGetSectionsForChapterQuery } from "@store/dataset/dataset.logic.api"; import { ReactNode, useCallback, useEffect, useState } from "react"; import { difficultyOptions } from "@/config/exerciseTypes"; diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/shared/CodeHighlighter.spec.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/shared/CodeHighlighter.spec.tsx index deab941e6..64279e5d1 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/shared/CodeHighlighter.spec.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/shared/CodeHighlighter.spec.tsx @@ -1,6 +1,7 @@ -import { renderWithMantine } from "@/test/renderWithMantine"; import { fireEvent, screen } from "@testing-library/react"; +import { renderWithMantine } from "@/test/renderWithMantine"; + import { CodeHighlighter } from "./CodeHighlighter"; vi.mock("@monaco-editor/react", () => ({ diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/shared/CodeHighlighter.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/shared/CodeHighlighter.tsx index 8d5b0fe2b..c4383a4b3 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/shared/CodeHighlighter.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/shared/CodeHighlighter.tsx @@ -1,5 +1,5 @@ -import { Editor, OnMount } from "@monaco-editor/react"; import { ActionIcon, Tooltip } from "@mantine/core"; +import { Editor, OnMount } from "@monaco-editor/react"; import classNames from "classnames"; import { FC, useRef, useState } from "react"; diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/shared/connections/ConnectHandle.spec.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/shared/connections/ConnectHandle.spec.tsx index 1a86e3333..1de04ee17 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/shared/connections/ConnectHandle.spec.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/shared/connections/ConnectHandle.spec.tsx @@ -1,6 +1,7 @@ -import { renderWithMantine } from "@/test/renderWithMantine"; import { fireEvent, screen } from "@testing-library/react"; +import { renderWithMantine } from "@/test/renderWithMantine"; + import { ConnectHandle } from "./ConnectHandle"; const baseProps = { diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/shared/connections/ConnectTargetButton.spec.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/shared/connections/ConnectTargetButton.spec.tsx index 4de1aa426..4a26fcf5c 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/shared/connections/ConnectTargetButton.spec.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/shared/connections/ConnectTargetButton.spec.tsx @@ -1,6 +1,7 @@ -import { renderWithMantine } from "@/test/renderWithMantine"; import { fireEvent, screen } from "@testing-library/react"; +import { renderWithMantine } from "@/test/renderWithMantine"; + import { ConnectTargetButton } from "./ConnectTargetButton"; describe("ConnectTargetButton", () => { diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/shared/connections/ConnectionList.spec.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/shared/connections/ConnectionList.spec.tsx index 09b185def..4c3ade67e 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/shared/connections/ConnectionList.spec.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/shared/connections/ConnectionList.spec.tsx @@ -1,6 +1,7 @@ -import { renderWithMantine } from "@/test/renderWithMantine"; import { fireEvent, screen } from "@testing-library/react"; +import { renderWithMantine } from "@/test/renderWithMantine"; + import { ConnectionList } from "./ConnectionList"; const resolveLabel = (id: string) => (id === "l1" ? "Apple" : "Red fruit"); diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/utils/validation.spec.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/utils/validation.spec.ts index 17d4dfafe..813341ee2 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/utils/validation.spec.ts +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/utils/validation.spec.ts @@ -61,26 +61,31 @@ describe("validateCommonFields", () => { it("returns an error when name is missing", () => { const errors = validateCommonFields({ ...validBase, name: undefined }); + expect(errors).toContain("Exercise name is required"); }); it("returns an error when name is an empty string", () => { const errors = validateCommonFields({ ...validBase, name: "" }); + expect(errors).toContain("Exercise name is required"); }); it("returns an error when name is whitespace only", () => { const errors = validateCommonFields({ ...validBase, name: " " }); + expect(errors).toContain("Exercise name is required"); }); it("returns an error when chapter is missing", () => { const errors = validateCommonFields({ ...validBase, chapter: undefined }); + expect(errors).toContain("Chapter is required"); }); it("returns an error when chapter is an empty string", () => { const errors = validateCommonFields({ ...validBase, chapter: "" }); + expect(errors).toContain("Chapter is required"); }); @@ -89,26 +94,31 @@ describe("validateCommonFields", () => { ...validBase, subchapter: undefined }); + expect(errors).toContain("Section is required"); }); it("returns an error when subchapter is an empty string", () => { const errors = validateCommonFields({ ...validBase, subchapter: "" }); + expect(errors).toContain("Section is required"); }); it("returns an error when points is undefined", () => { const errors = validateCommonFields({ ...validBase, points: undefined }); + expect(errors).toContain("Points must be greater than 0"); }); it("returns an error when points is zero", () => { const errors = validateCommonFields({ ...validBase, points: 0 }); + expect(errors).toContain("Points must be greater than 0"); }); it("returns an error when points is negative", () => { const errors = validateCommonFields({ ...validBase, points: -1 }); + expect(errors).toContain("Points must be greater than 0"); }); @@ -117,16 +127,19 @@ describe("validateCommonFields", () => { ...validBase, difficulty: undefined }); + expect(errors).toContain("Difficulty is required"); }); it("does not return a difficulty error when difficulty is 0", () => { const errors = validateCommonFields({ ...validBase, difficulty: 0 }); + expect(errors).not.toContain("Difficulty is required"); }); it("returns multiple errors when several fields are invalid", () => { const errors = validateCommonFields({}); + expect(errors).toContain("Exercise name is required"); expect(errors).toContain("Chapter is required"); expect(errors).toContain("Section is required"); @@ -143,6 +156,7 @@ describe("validateCommonFields", () => { points: 3, difficulty: undefined }); + expect(errors).not.toContain("Exercise name is required"); expect(errors).not.toContain("Chapter is required"); expect(errors).toContain("Section is required"); diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/EditAllExercises/EditDropdownValueHeader.spec.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/EditAllExercises/EditDropdownValueHeader.spec.tsx index 28e01c23e..8ce2bad03 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/EditAllExercises/EditDropdownValueHeader.spec.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/EditAllExercises/EditDropdownValueHeader.spec.tsx @@ -1,3 +1,4 @@ +import { notify } from "@components/ui/notify"; import userEvent from "@testing-library/user-event"; import { renderWithMantine, screen } from "@/test/renderWithMantine"; @@ -24,8 +25,6 @@ vi.mock("@components/ui/notify", () => ({ } })); -import { notify } from "@components/ui/notify"; - vi.mock("@/hooks/useExercisesSelector", () => ({ useExercisesSelector: () => ({ assignmentExercises: [{ id: 1, question_json: {} }] }) })); diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/ExercisePreview/ExercisePreviewModal.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/ExercisePreview/ExercisePreviewModal.tsx index 304bc9d55..933a12d0d 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/ExercisePreview/ExercisePreviewModal.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/ExercisePreview/ExercisePreviewModal.tsx @@ -4,7 +4,6 @@ import { useState, cloneElement, MouseEvent, ReactElement } from "react"; import { Exercise } from "@/types/exercises"; import { ExercisePreview } from "./ExercisePreview"; - import styles from "./ExercisePreviewModal.module.css"; type ExercisePreviewModalProps = Pick & { diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/SearchExercises/SmartSearchExercises.spec.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/SearchExercises/SmartSearchExercises.spec.tsx index e06596d8d..02bb80bfd 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/SearchExercises/SmartSearchExercises.spec.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/SearchExercises/SmartSearchExercises.spec.tsx @@ -1,7 +1,6 @@ import userEvent from "@testing-library/user-event"; import { renderWithMantine, screen } from "@/test/renderWithMantine"; - import { Exercise } from "@/types/exercises"; import { SmartSearchExercises } from "./SmartSearchExercises"; diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/SearchExercises/SmartSearchExercises.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/SearchExercises/SmartSearchExercises.tsx index 03cf38395..2f0250183 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/SearchExercises/SmartSearchExercises.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/SearchExercises/SmartSearchExercises.tsx @@ -18,6 +18,11 @@ import { Text, Tooltip } from "@mantine/core"; +import { datasetSelectors } from "@store/dataset/dataset.logic"; +import { + searchExercisesActions, + searchExercisesSelectors +} from "@store/searchExercises/searchExercises.logic"; import { ColumnDef, ColumnFiltersState, @@ -26,11 +31,6 @@ import { RowSelectionState, SortingState } from "@tanstack/react-table"; -import { - searchExercisesActions, - searchExercisesSelectors -} from "@store/searchExercises/searchExercises.logic"; -import { datasetSelectors } from "@store/dataset/dataset.logic"; import { useCallback, useMemo, useState } from "react"; import { useDispatch, useSelector } from "react-redux"; diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/TipTap/Editor.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/TipTap/Editor.tsx index fddc17d28..adb5151b0 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/TipTap/Editor.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/TipTap/Editor.tsx @@ -1,6 +1,8 @@ import { MathExtension } from "@aarkue/tiptap-math-extension"; import { TipTapImage } from "@components/routes/AssignmentBuilder/components/exercises/components/TipTap/Plugins/Image"; import { katexMacros } from "@components/routes/AssignmentBuilder/mathMacros"; +import { Icon } from "@components/ui/Icon"; +import { ActionIcon, Tooltip } from "@mantine/core"; import { Editor as TipTapEditor, Range } from "@tiptap/core"; import FontFamily from "@tiptap/extension-font-family"; import Highlight from "@tiptap/extension-highlight"; @@ -13,8 +15,6 @@ import TableRow from "@tiptap/extension-table-row"; import TextAlign from "@tiptap/extension-text-align"; import Underline from "@tiptap/extension-underline"; import Youtube from "@tiptap/extension-youtube"; -import { Icon } from "@components/ui/Icon"; -import { ActionIcon, Tooltip } from "@mantine/core"; import { useEditor, EditorContent, BubbleMenu } from "@tiptap/react"; import StarterKit from "@tiptap/starter-kit"; import { useEffect } from "react"; @@ -29,13 +29,13 @@ import "katex/dist/katex.min.css"; import styles from "./Editor.module.css"; import { Command, items } from "./SlashCommands"; import { TipTapDocModal, useTipTapDocModal } from "./TipTapDocModal"; -import { useTableColumnMenu } from "./hooks/useTableColumnMenu"; -import { useTableRowMenu } from "./hooks/useTableRowMenu"; +import { InsertFormPopover, useInsertForm } from "./components/InsertFormPopover"; import { TableColumnMenu } from "./components/TableColumnMenu"; import { TableRowMenu } from "./components/TableRowMenu"; -import { InsertFormPopover, useInsertForm } from "./components/InsertFormPopover"; import { InsertFormBridge } from "./extensions/InsertFormBridge"; import { TabIndent } from "./extensions/TabIndent"; +import { useTableColumnMenu } from "./hooks/useTableColumnMenu"; +import { useTableRowMenu } from "./hooks/useTableRowMenu"; const customStyles = ` .tippy-box { diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/TipTap/SlashCommands.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/TipTap/SlashCommands.tsx index 46aecf900..2ded72863 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/TipTap/SlashCommands.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/TipTap/SlashCommands.tsx @@ -569,6 +569,7 @@ export const renderItems = () => { const getAppendTarget = () => { const exerciseLayout = document.getElementById("exercise-layout"); + return exerciseLayout || document.body; }; diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/TipTap/TipTapDocModal.spec.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/TipTap/TipTapDocModal.spec.tsx index 4ea2ee0e8..63484fc3c 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/TipTap/TipTapDocModal.spec.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/TipTap/TipTapDocModal.spec.tsx @@ -37,6 +37,7 @@ describe("TipTapDocModal", () => { it("calls onHide when the modal is closed", () => { const onHide = vi.fn(); + renderWithMantine(); fireEvent.click(screen.getByRole("button", { name: /close/i })); diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/TipTap/components/InsertFormPopover.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/TipTap/components/InsertFormPopover.tsx index 2308e0f53..bb604adb2 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/TipTap/components/InsertFormPopover.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/TipTap/components/InsertFormPopover.tsx @@ -1,11 +1,10 @@ -import { Button, Group, TextInput } from "@mantine/core"; -import { Editor } from "@tiptap/core"; -import { FC, FormEvent, KeyboardEvent, useEffect, useState } from "react"; - import { InsertFormKind, InsertFormRequest } from "@components/routes/AssignmentBuilder/components/exercises/components/TipTap/extensions/InsertFormBridge"; +import { Button, Group, TextInput } from "@mantine/core"; +import { Editor } from "@tiptap/core"; +import { FC, FormEvent, KeyboardEvent, useEffect, useState } from "react"; import styles from "./InsertFormPopover.module.css"; diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/TipTap/components/TableColumnMenu.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/TipTap/components/TableColumnMenu.tsx index ad3b92697..ec8b839e6 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/TipTap/components/TableColumnMenu.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/TipTap/components/TableColumnMenu.tsx @@ -1,5 +1,6 @@ import { Editor } from "@tiptap/react"; import React from "react"; + import defaultStyles from "../Editor.module.css"; interface TableColumnMenuProps { diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/TipTap/components/TableRowMenu.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/TipTap/components/TableRowMenu.tsx index 94dffc618..9f5d6d5b5 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/TipTap/components/TableRowMenu.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/TipTap/components/TableRowMenu.tsx @@ -1,5 +1,6 @@ import { Editor } from "@tiptap/react"; import React from "react"; + import defaultStyles from "../Editor.module.css"; interface TableRowMenuProps { @@ -34,6 +35,7 @@ export const TableRowMenu: React.FC = ({ // Find the position of the clicked row in the table const table = rowElement.closest("table"); + if (!table) return; const rows = Array.from(table.querySelectorAll("tr")); @@ -47,9 +49,11 @@ export const TableRowMenu: React.FC = ({ // Find the table node and calculate the position let tablePos = -1; + doc.descendants((node, pos) => { if (node.type.name === "table") { const domNode = view.nodeDOM(pos); + if (domNode === table) { tablePos = pos; return false; @@ -61,8 +65,10 @@ export const TableRowMenu: React.FC = ({ // Calculate position by traversing rows let currentPos = tablePos + 1; // Start inside table + for (let i = 0; i < rowIndex; i++) { const rowNode = doc.nodeAt(currentPos); + if (rowNode) { currentPos += rowNode.nodeSize; } @@ -82,6 +88,7 @@ export const TableRowMenu: React.FC = ({ if (!rowElement) return; const table = rowElement.closest("table"); + if (!table) return; const rows = Array.from(table.querySelectorAll("tr")); @@ -93,9 +100,11 @@ export const TableRowMenu: React.FC = ({ const { doc } = state; let tablePos = -1; + doc.descendants((node, pos) => { if (node.type.name === "table") { const domNode = view.nodeDOM(pos); + if (domNode === table) { tablePos = pos; return false; @@ -106,8 +115,10 @@ export const TableRowMenu: React.FC = ({ if (tablePos === -1) return; let currentPos = tablePos + 1; + for (let i = 0; i < rowIndex; i++) { const rowNode = doc.nodeAt(currentPos); + if (rowNode) { currentPos += rowNode.nodeSize; } @@ -133,6 +144,7 @@ export const TableRowMenu: React.FC = ({ } const table = rowElement.closest("table"); + if (!table) return; const rows = Array.from(table.querySelectorAll("tr")); @@ -144,9 +156,11 @@ export const TableRowMenu: React.FC = ({ const { doc } = state; let tablePos = -1; + doc.descendants((node, pos) => { if (node.type.name === "table") { const domNode = view.nodeDOM(pos); + if (domNode === table) { tablePos = pos; return false; @@ -157,8 +171,10 @@ export const TableRowMenu: React.FC = ({ if (tablePos === -1) return; let currentPos = tablePos + 1; + for (let i = 0; i < rowIndex; i++) { const rowNode = doc.nodeAt(currentPos); + if (rowNode) { currentPos += rowNode.nodeSize; } diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/TipTap/hooks/index.spec.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/TipTap/hooks/index.spec.ts index 3fe64b582..3dc9afbda 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/TipTap/hooks/index.spec.ts +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/TipTap/hooks/index.spec.ts @@ -1,4 +1,5 @@ import { renderHook, act } from "@testing-library/react"; + import { useTableColumnMenu } from "./useTableColumnMenu"; import { useTableRowMenu } from "./useTableRowMenu"; @@ -167,6 +168,7 @@ describe("useTableColumnMenu", () => { }); const addCalls = addSpy.mock.calls.filter(([event]) => event === "mousedown"); + expect(addCalls).toHaveLength(1); act(() => { @@ -174,6 +176,7 @@ describe("useTableColumnMenu", () => { }); const removeCalls = removeSpy.mock.calls.filter(([event]) => event === "mousedown"); + expect(removeCalls).toHaveLength(1); addSpy.mockRestore(); @@ -186,6 +189,7 @@ describe("useTableColumnMenu", () => { renderHook(() => useTableColumnMenu(null)); const mousedownCalls = addSpy.mock.calls.filter(([event]) => event === "mousedown"); + expect(mousedownCalls).toHaveLength(0); addSpy.mockRestore(); @@ -377,6 +381,7 @@ describe("useTableRowMenu", () => { }); const addCalls = addSpy.mock.calls.filter(([event]) => event === "mousedown"); + expect(addCalls).toHaveLength(1); act(() => { @@ -384,6 +389,7 @@ describe("useTableRowMenu", () => { }); const removeCalls = removeSpy.mock.calls.filter(([event]) => event === "mousedown"); + expect(removeCalls).toHaveLength(1); addSpy.mockRestore(); @@ -394,11 +400,13 @@ describe("useTableRowMenu", () => { describe("re-exports from index", () => { it("exports useTableColumnMenu", async () => { const mod = await import("./index"); + expect(typeof mod.useTableColumnMenu).toBe("function"); }); it("exports useTableRowMenu", async () => { const mod = await import("./index"); + expect(typeof mod.useTableRowMenu).toBe("function"); }); }); diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/TipTap/hooks/useTableColumnMenu.spec.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/TipTap/hooks/useTableColumnMenu.spec.ts index d23984314..bdc7ff038 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/TipTap/hooks/useTableColumnMenu.spec.ts +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/TipTap/hooks/useTableColumnMenu.spec.ts @@ -1,4 +1,5 @@ import { renderHook, act } from "@testing-library/react"; + import { useTableColumnMenu } from "./useTableColumnMenu"; const createMockNode = (name: string, childCount: number, firstChild: object | null = null) => ({ @@ -22,6 +23,7 @@ const createMockEditor = ( depth, node: vi.fn().mockImplementation((d: number) => { const index = depth - d; + return mockDepthNodes[index] ?? createMockNode("unknown", 0, null); }) } @@ -34,32 +36,38 @@ describe("useTableColumnMenu", () => { describe("initial state", () => { it("returns columnMenuVisible as false on mount", () => { const { result } = renderHook(() => useTableColumnMenu(null)); + expect(result.current.columnMenuVisible).toBe(false); }); it("returns columnMenuPosition as { x: 0, y: 0 } on mount", () => { const { result } = renderHook(() => useTableColumnMenu(null)); + expect(result.current.columnMenuPosition).toEqual({ x: 0, y: 0 }); }); it("returns a columnMenuRef object", () => { const { result } = renderHook(() => useTableColumnMenu(null)); + expect(result.current.columnMenuRef).toBeDefined(); expect(result.current.columnMenuRef).toHaveProperty("current"); }); it("exposes setColumnMenuVisible function", () => { const { result } = renderHook(() => useTableColumnMenu(null)); + expect(typeof result.current.setColumnMenuVisible).toBe("function"); }); it("exposes getColumnCount function", () => { const { result } = renderHook(() => useTableColumnMenu(null)); + expect(typeof result.current.getColumnCount).toBe("function"); }); it("exposes isLastColumn function", () => { const { result } = renderHook(() => useTableColumnMenu(null)); + expect(typeof result.current.isLastColumn).toBe("function"); }); }); @@ -67,6 +75,7 @@ describe("useTableColumnMenu", () => { describe("setColumnMenuVisible", () => { it("sets columnMenuVisible to true when called with true", () => { const { result } = renderHook(() => useTableColumnMenu(null)); + act(() => { result.current.setColumnMenuVisible(true); }); @@ -75,6 +84,7 @@ describe("useTableColumnMenu", () => { it("sets columnMenuVisible to false when called with false after being true", () => { const { result } = renderHook(() => useTableColumnMenu(null)); + act(() => { result.current.setColumnMenuVisible(true); }); @@ -88,6 +98,7 @@ describe("useTableColumnMenu", () => { describe("getColumnCount", () => { it("returns 0 when editor is null", () => { const { result } = renderHook(() => useTableColumnMenu(null)); + expect(result.current.getColumnCount()).toBe(0); }); @@ -96,6 +107,7 @@ describe("useTableColumnMenu", () => { const docNode = createMockNode("doc", 1, null); const editor = createMockEditor(true, [docNode, paragraphNode]); const { result } = renderHook(() => useTableColumnMenu(editor as any)); + expect(result.current.getColumnCount()).toBe(0); }); @@ -104,6 +116,7 @@ describe("useTableColumnMenu", () => { const docNode = createMockNode("doc", 1, null); const editor = createMockEditor(true, [docNode, tableNode]); const { result } = renderHook(() => useTableColumnMenu(editor as any)); + expect(result.current.getColumnCount()).toBe(0); }); @@ -113,6 +126,7 @@ describe("useTableColumnMenu", () => { const docNode = createMockNode("doc", 1, null); const editor = createMockEditor(true, [docNode, tableNode]); const { result } = renderHook(() => useTableColumnMenu(editor as any)); + expect(result.current.getColumnCount()).toBe(3); }); @@ -123,6 +137,7 @@ describe("useTableColumnMenu", () => { const docNode = createMockNode("doc", 1, null); const editor = createMockEditor(true, [docNode, tableNode, cellNode]); const { result } = renderHook(() => useTableColumnMenu(editor as any)); + expect(result.current.getColumnCount()).toBe(5); }); }); @@ -134,6 +149,7 @@ describe("useTableColumnMenu", () => { const docNode = createMockNode("doc", 1, null); const editor = createMockEditor(true, [docNode, tableNode]); const { result } = renderHook(() => useTableColumnMenu(editor as any)); + expect(result.current.isLastColumn()).toBe(true); }); @@ -143,11 +159,13 @@ describe("useTableColumnMenu", () => { const docNode = createMockNode("doc", 1, null); const editor = createMockEditor(true, [docNode, tableNode]); const { result } = renderHook(() => useTableColumnMenu(editor as any)); + expect(result.current.isLastColumn()).toBe(false); }); it("returns false when editor is null (column count is 0)", () => { const { result } = renderHook(() => useTableColumnMenu(null)); + expect(result.current.isLastColumn()).toBe(false); }); }); @@ -157,6 +175,7 @@ describe("useTableColumnMenu", () => { const { result } = renderHook(() => useTableColumnMenu(null)); const menuDiv = document.createElement("div"); + document.body.appendChild(menuDiv); Object.defineProperty(result.current.columnMenuRef, "current", { @@ -170,6 +189,7 @@ describe("useTableColumnMenu", () => { }); const outsideElement = document.createElement("div"); + document.body.appendChild(outsideElement); act(() => { @@ -187,6 +207,7 @@ describe("useTableColumnMenu", () => { const menuDiv = document.createElement("div"); const innerButton = document.createElement("button"); + menuDiv.appendChild(innerButton); document.body.appendChild(menuDiv); @@ -211,10 +232,12 @@ describe("useTableColumnMenu", () => { it("does not register mousedown listener when menu is not visible", () => { const addEventListenerSpy = vi.spyOn(document, "addEventListener"); + renderHook(() => useTableColumnMenu(null)); const mousedownCalls = addEventListenerSpy.mock.calls.filter( ([type]) => type === "mousedown" ); + expect(mousedownCalls.length).toBe(0); addEventListenerSpy.mockRestore(); }); @@ -223,12 +246,14 @@ describe("useTableColumnMenu", () => { describe("table header click handler", () => { it("sets menu position and shows menu when clicking a th inside a table when editor is active", () => { const proseMirrorDiv = document.createElement("div"); + proseMirrorDiv.className = "ProseMirror"; const table = document.createElement("table"); const tbody = document.createElement("tbody"); const tr = document.createElement("tr"); const th = document.createElement("th"); + th.textContent = "Header"; tr.appendChild(th); @@ -267,9 +292,11 @@ describe("useTableColumnMenu", () => { const { result } = renderHook(() => useTableColumnMenu(editor as any)); const proseMirrorDiv = document.createElement("div"); + proseMirrorDiv.className = "ProseMirror"; const paragraph = document.createElement("p"); + paragraph.textContent = "Some text"; proseMirrorDiv.appendChild(paragraph); document.body.appendChild(proseMirrorDiv); @@ -288,12 +315,14 @@ describe("useTableColumnMenu", () => { const { result } = renderHook(() => useTableColumnMenu(editor as any)); const proseMirrorDiv = document.createElement("div"); + proseMirrorDiv.className = "ProseMirror"; const table = document.createElement("table"); const tbody = document.createElement("tbody"); const tr = document.createElement("tr"); const th = document.createElement("th"); + th.textContent = "Header"; tr.appendChild(th); diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/TipTap/hooks/useTableColumnMenu.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/TipTap/hooks/useTableColumnMenu.ts index f7c2e15fa..6dbcf0c76 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/TipTap/hooks/useTableColumnMenu.ts +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/TipTap/hooks/useTableColumnMenu.ts @@ -40,6 +40,7 @@ export const useTableColumnMenu = (editor: Editor | null): UseTableColumnMenuRes if (tableHeader && editor?.isActive("table")) { event.preventDefault(); const rect = tableHeader.getBoundingClientRect(); + setColumnMenuPosition({ x: rect.left + rect.width / 2, y: rect.bottom + 5 @@ -49,6 +50,7 @@ export const useTableColumnMenu = (editor: Editor | null): UseTableColumnMenuRes }; const editorElement = document.querySelector(".ProseMirror"); + if (editorElement && editor) { editorElement.addEventListener("click", handleTableHeaderClick as EventListener); return () => { @@ -68,9 +70,11 @@ export const useTableColumnMenu = (editor: Editor | null): UseTableColumnMenuRes // Find the table node for (let depth = $anchor.depth; depth > 0; depth--) { const node = $anchor.node(depth); + if (node.type.name === "table") { // Get the first row to count columns const firstRow = node.firstChild; + if (firstRow) { return firstRow.childCount; } diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/TipTap/hooks/useTableRowMenu.spec.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/TipTap/hooks/useTableRowMenu.spec.ts index aa3c019b7..14e1988d5 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/TipTap/hooks/useTableRowMenu.spec.ts +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/TipTap/hooks/useTableRowMenu.spec.ts @@ -1,4 +1,5 @@ import { renderHook, act } from "@testing-library/react"; + import { useTableRowMenu } from "./useTableRowMenu"; const createMockEditor = (overrides: Record = {}) => { @@ -20,37 +21,44 @@ describe("useTableRowMenu", () => { describe("initial state", () => { it("returns rowMenuVisible as false initially", () => { const { result } = renderHook(() => useTableRowMenu(null)); + expect(result.current.rowMenuVisible).toBe(false); }); it("returns rowMenuPosition as { x: 0, y: 0 } initially", () => { const { result } = renderHook(() => useTableRowMenu(null)); + expect(result.current.rowMenuPosition).toEqual({ x: 0, y: 0 }); }); it("returns rowMenuRef as a ref object", () => { const { result } = renderHook(() => useTableRowMenu(null)); + expect(result.current.rowMenuRef).toBeDefined(); expect(result.current.rowMenuRef).toHaveProperty("current"); }); it("returns currentRowElement as null initially", () => { const { result } = renderHook(() => useTableRowMenu(null)); + expect(result.current.currentRowElement).toBeNull(); }); it("returns setRowMenuVisible as a function", () => { const { result } = renderHook(() => useTableRowMenu(null)); + expect(typeof result.current.setRowMenuVisible).toBe("function"); }); it("returns getRowCount as a function", () => { const { result } = renderHook(() => useTableRowMenu(null)); + expect(typeof result.current.getRowCount).toBe("function"); }); it("returns isLastRow as a function", () => { const { result } = renderHook(() => useTableRowMenu(null)); + expect(typeof result.current.isLastRow).toBe("function"); }); }); @@ -58,6 +66,7 @@ describe("useTableRowMenu", () => { describe("setRowMenuVisible", () => { it("sets rowMenuVisible to true when called with true", () => { const { result } = renderHook(() => useTableRowMenu(null)); + act(() => { result.current.setRowMenuVisible(true); }); @@ -66,6 +75,7 @@ describe("useTableRowMenu", () => { it("sets rowMenuVisible back to false when called with false", () => { const { result } = renderHook(() => useTableRowMenu(null)); + act(() => { result.current.setRowMenuVisible(true); }); @@ -79,6 +89,7 @@ describe("useTableRowMenu", () => { describe("getRowCount", () => { it("returns 0 when editor is null", () => { const { result } = renderHook(() => useTableRowMenu(null)); + expect(result.current.getRowCount()).toBe(0); }); @@ -95,6 +106,7 @@ describe("useTableRowMenu", () => { }); const { result } = renderHook(() => useTableRowMenu(mockEditor)); + expect(result.current.getRowCount()).toBe(0); }); @@ -144,6 +156,7 @@ describe("useTableRowMenu", () => { }); const { result } = renderHook(() => useTableRowMenu(mockEditor)); + expect(result.current.getRowCount()).toBe(2); }); @@ -178,6 +191,7 @@ describe("useTableRowMenu", () => { }); const { result } = renderHook(() => useTableRowMenu(mockEditor)); + expect(result.current.getRowCount()).toBe(0); }); @@ -213,6 +227,7 @@ describe("useTableRowMenu", () => { }); const { result } = renderHook(() => useTableRowMenu(mockEditor)); + expect(result.current.getRowCount()).toBe(1); }); }); @@ -246,6 +261,7 @@ describe("useTableRowMenu", () => { }); const { result } = renderHook(() => useTableRowMenu(mockEditor)); + expect(result.current.isLastRow()).toBe(true); }); @@ -279,11 +295,13 @@ describe("useTableRowMenu", () => { }); const { result } = renderHook(() => useTableRowMenu(mockEditor)); + expect(result.current.isLastRow()).toBe(false); }); it("returns false when editor is null (row count is 0)", () => { const { result } = renderHook(() => useTableRowMenu(null)); + expect(result.current.isLastRow()).toBe(false); }); }); @@ -299,6 +317,7 @@ describe("useTableRowMenu", () => { expect(result.current.rowMenuVisible).toBe(true); const menuDiv = document.createElement("div"); + document.body.appendChild(menuDiv); Object.defineProperty(result.current.rowMenuRef, "current", { value: menuDiv, @@ -306,10 +325,12 @@ describe("useTableRowMenu", () => { }); const outsideElement = document.createElement("div"); + document.body.appendChild(outsideElement); act(() => { const event = new MouseEvent("mousedown", { bubbles: true }); + Object.defineProperty(event, "target", { value: outsideElement }); document.dispatchEvent(event); }); @@ -322,8 +343,10 @@ describe("useTableRowMenu", () => { it("does not add mousedown listener when menu is not visible", () => { const addSpy = vi.spyOn(document, "addEventListener"); + renderHook(() => useTableRowMenu(null)); const mousedownCalls = addSpy.mock.calls.filter(([type]) => type === "mousedown"); + expect(mousedownCalls).toHaveLength(0); addSpy.mockRestore(); }); @@ -341,6 +364,7 @@ describe("useTableRowMenu", () => { }); const mousedownCalls = removeSpy.mock.calls.filter(([type]) => type === "mousedown"); + expect(mousedownCalls.length).toBeGreaterThan(0); removeSpy.mockRestore(); }); @@ -349,12 +373,14 @@ describe("useTableRowMenu", () => { describe("cleanup", () => { it("unmounts without error when editor is null", () => { const { unmount } = renderHook(() => useTableRowMenu(null)); + expect(() => unmount()).not.toThrow(); }); it("unmounts without error when editor is provided", () => { const mockEditor = createMockEditor(); const { unmount } = renderHook(() => useTableRowMenu(mockEditor)); + expect(() => unmount()).not.toThrow(); }); }); diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/TipTap/hooks/useTableRowMenu.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/TipTap/hooks/useTableRowMenu.ts index 46ebcb3ca..8193d1611 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/TipTap/hooks/useTableRowMenu.ts +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/TipTap/hooks/useTableRowMenu.ts @@ -22,6 +22,7 @@ export const useTableRowMenu = (editor: Editor | null): UseTableRowMenuResult => useEffect(() => { const handleClickOutside = (event: MouseEvent) => { const target = event.target as Node; + if (rowMenuRef.current && !rowMenuRef.current.contains(target)) { setRowMenuVisible(false); } @@ -53,6 +54,7 @@ export const useTableRowMenu = (editor: Editor | null): UseTableRowMenuResult => }; const menuElement = rowMenuRef.current; + if (menuElement) { menuElement.addEventListener("mouseenter", handleMouseEnter); menuElement.addEventListener("mouseleave", handleMouseLeave); @@ -75,6 +77,7 @@ export const useTableRowMenu = (editor: Editor | null): UseTableRowMenuResult => } const row = tableCell.closest("tr"); + if (!row || !editor?.isActive("table")) { return; } @@ -118,6 +121,7 @@ export const useTableRowMenu = (editor: Editor | null): UseTableRowMenuResult => hoverTimeout.current = setTimeout(() => { // Check if mouse is over the menu const menuElement = rowMenuRef.current; + if (menuElement) { const menuRect = menuElement.getBoundingClientRect(); const mouseX = event.clientX; @@ -139,6 +143,7 @@ export const useTableRowMenu = (editor: Editor | null): UseTableRowMenuResult => }; const editorElement = document.querySelector(".ProseMirror"); + if (editorElement && editor) { editorElement.addEventListener("mousemove", handleTableCellHover as EventListener); editorElement.addEventListener("mouseleave", handleMouseLeave as EventListener); @@ -163,13 +168,16 @@ export const useTableRowMenu = (editor: Editor | null): UseTableRowMenuResult => // Find the table node for (let depth = $anchor.depth; depth > 0; depth--) { const node = $anchor.node(depth); + if (node.type.name === "table") { // Count all rows, excluding header row let rowCount = 0; + node.forEach((child) => { if (child.type.name === "tableRow") { // Check if this row contains header cells let hasHeaderCell = false; + child.forEach((cell) => { if (cell.type.name === "tableHeader") { hasHeaderCell = true; diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/importAssignment/ImportAssignmentModal.spec.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/importAssignment/ImportAssignmentModal.spec.tsx index 72c34b14b..9b283fe09 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/importAssignment/ImportAssignmentModal.spec.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/importAssignment/ImportAssignmentModal.spec.tsx @@ -3,21 +3,10 @@ import userEvent from "@testing-library/user-event"; import { renderWithMantine, screen, waitFor } from "@/test/renderWithMantine"; import { ShareableTreeCourse } from "@/types/assignmentSharing"; -import { - ImportAssignmentModal, - buildSelectionKeys, - buildTree -} from "./ImportAssignmentModal"; - -const { - treeHolder, - previewHolder, - importMock, - importCourseMock, - treeSkipSpy, - treeParamsSpy -} = vi.hoisted( - () => ({ +import { ImportAssignmentModal, buildSelectionKeys, buildTree } from "./ImportAssignmentModal"; + +const { treeHolder, previewHolder, importMock, importCourseMock, treeSkipSpy, treeParamsSpy } = + vi.hoisted(() => ({ treeHolder: { value: { courses: [ @@ -120,8 +109,7 @@ const { importCourseMock: vi.fn(), treeSkipSpy: vi.fn(), treeParamsSpy: vi.fn() - }) -); + })); vi.mock("@store/assignment/assignment.logic.api", () => ({ useShareableTreeQuery: (params: unknown, options: { skip: boolean }) => { @@ -251,9 +239,7 @@ describe("ImportAssignmentModal", () => { renderWithMantine(); await user.click(screen.getByRole("checkbox", { name: "Select cs101-fall" })); - expect( - screen.getByRole("button", { name: "Import 2 assignments" }) - ).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "Import 2 assignments" })).toBeInTheDocument(); await user.click(screen.getByRole("button", { name: "Import 2 assignments" })); diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/importAssignment/ImportAssignmentModal.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/importAssignment/ImportAssignmentModal.tsx index 60c4415cc..411440ae7 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/importAssignment/ImportAssignmentModal.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/importAssignment/ImportAssignmentModal.tsx @@ -9,15 +9,11 @@ import { import debounce from "lodash/debounce"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { - ShareableTreeAssignment, - ShareableTreeCourse -} from "@/types/assignmentSharing"; +import { ShareableTreeAssignment, ShareableTreeCourse } from "@/types/assignmentSharing"; import { SelectedKey, TreeNode } from "@/types/treeNode"; -import { ImportPreviewPanel } from "./ImportPreviewPanel"; - import styles from "./ImportAssignmentModal.module.css"; +import { ImportPreviewPanel } from "./ImportPreviewPanel"; const SEARCH_DEBOUNCE_MS = 300; const PAGE_SIZE = 25; @@ -170,19 +166,16 @@ export const ImportAssignmentModal = ({ visible, onHide }: ImportAssignmentModal [selectableUnder] ); - const handleUnselect = useCallback( - (node: TreeNode) => { - setSelectedIds((prev) => { - const next = new Set(prev); + const handleUnselect = useCallback((node: TreeNode) => { + setSelectedIds((prev) => { + const next = new Set(prev); - for (const assignment of assignmentsUnder(node)) { - next.delete(assignment.id); - } - return next; - }); - }, - [] - ); + for (const assignment of assignmentsUnder(node)) { + next.delete(assignment.id); + } + return next; + }); + }, []); /** * Import the selection, one course at a time where a whole course is checked. @@ -195,9 +188,7 @@ export const ImportAssignmentModal = ({ visible, onHide }: ImportAssignmentModal const wholeCourses = courses.filter( (course) => course.assignments.length > 0 && - course.assignments.every( - (a) => a.already_imported || selectedIds.has(a.id) - ) && + course.assignments.every((a) => a.already_imported || selectedIds.has(a.id)) && course.assignments.some((a) => selectedIds.has(a.id)) ); const wholeCourseIds = new Set(wholeCourses.map((c) => c.id)); @@ -343,12 +334,7 @@ export const ImportAssignmentModal = ({ visible, onHide }: ImportAssignmentModal const totalPages = data?.pagination.pages ?? 0; return ( - + {preview.question_count} question{preview.question_count === 1 ? "" : "s"} ·{" "} - {preview.points} points · due{" "} - {formatUTCDateForDisplay(preview.duedate, DATE_FORMAT)} in your term + {preview.points} points · due {formatUTCDateForDisplay(preview.duedate, DATE_FORMAT)}{" "} + in your term diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/list/AssignmentList.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/list/AssignmentList.tsx index 9e85ea879..4d6225094 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/list/AssignmentList.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/list/AssignmentList.tsx @@ -360,15 +360,7 @@ export const AssignmentList = ({ ) } ], - [ - filteredAssignments.length, - selectColumn, - onEdit, - onEnforceDueChange, - onVisibilityChange, - onDuplicate, - confirmRemove - ] + [confirmRemove, onDuplicate, onEdit, onEnforceDueChange, onVisibilityChange] ); const showEmptyState = !loading && assignments.length === 0; diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/reading/AssignmentReadingsTable.spec.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/reading/AssignmentReadingsTable.spec.tsx index 2ea339e4b..6c38490b2 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/reading/AssignmentReadingsTable.spec.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/reading/AssignmentReadingsTable.spec.tsx @@ -1,5 +1,4 @@ import { renderWithMantine, screen } from "@/test/renderWithMantine"; - import { Exercise } from "@/types/exercises"; import { AssignmentReadingsTable } from "./AssignmentReadingsTable"; diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/reading/AssignmentReadingsTable.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/reading/AssignmentReadingsTable.tsx index 55af61220..147d1f57e 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/reading/AssignmentReadingsTable.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/reading/AssignmentReadingsTable.tsx @@ -9,12 +9,11 @@ import { Nullable } from "@/types/common"; import { DraggingExerciseColumns } from "@/types/components/editableTableCell"; import { Exercise } from "@/types/exercises"; +import styles from "./AssignmentReadingsTable.module.css"; import { ActivitiesRequiredCell } from "./components/ActivitiesRequiredCell"; import { EditInputValueHeaderReadings } from "./components/EditAllReadings/EditInputValueHeaderReadings"; import { MouseUpHandler } from "./types"; -import styles from "./AssignmentReadingsTable.module.css"; - interface AssignmentReadingsTableProps { assignmentReadings: Exercise[]; selectedReadings: Exercise[]; @@ -140,6 +139,8 @@ export const AssignmentReadingsTable = ({ ) } ], + // TODO(eslint): Audit the complete dependency list without changing current behavior. + // eslint-disable-next-line react-hooks/exhaustive-deps [handleMouseDown, handleChange, startItemId] ); diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/reading/ReadingsListView.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/reading/ReadingsListView.tsx index 2ff7cccb4..ab4f8caed 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/reading/ReadingsListView.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/reading/ReadingsListView.tsx @@ -4,11 +4,10 @@ import { DraggingExerciseColumns } from "@/types/components/editableTableCell"; import { Exercise } from "@/types/exercises"; import { AssignmentReadingsTable } from "./AssignmentReadingsTable"; +import styles from "./ReadingsListView.module.css"; import { ReadingsToolbar } from "./ReadingsToolbar"; import { MouseUpHandler } from "./types"; -import styles from "./ReadingsListView.module.css"; - interface ReadingsListViewProps { globalFilter: string; setGlobalFilter: (filter: string) => void; diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/reading/ReadingsToolbar.spec.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/reading/ReadingsToolbar.spec.tsx index a69749128..a51204b00 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/reading/ReadingsToolbar.spec.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/reading/ReadingsToolbar.spec.tsx @@ -1,7 +1,6 @@ import userEvent from "@testing-library/user-event"; import { renderWithMantine, screen } from "@/test/renderWithMantine"; - import { Exercise } from "@/types/exercises"; import { ReadingsToolbar } from "./ReadingsToolbar"; diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/reading/ReadingsToolbar.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/reading/ReadingsToolbar.tsx index 2eae94693..8d3192955 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/reading/ReadingsToolbar.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/reading/ReadingsToolbar.tsx @@ -6,9 +6,8 @@ import { modals } from "@mantine/modals"; import { Exercise } from "@/types/exercises"; -import { ChooseReadingsButton } from "./components/ChooseReadingsButton"; - import styles from "./ReadingsToolbar.module.css"; +import { ChooseReadingsButton } from "./components/ChooseReadingsButton"; const TITLE = "Sections to read"; diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/reading/components/ActivitiesRequiredCell.spec.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/reading/components/ActivitiesRequiredCell.spec.tsx index 2b89c73ee..96119d59f 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/reading/components/ActivitiesRequiredCell.spec.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/reading/components/ActivitiesRequiredCell.spec.tsx @@ -1,7 +1,7 @@ +import { notify } from "@components/ui/notify"; import userEvent from "@testing-library/user-event"; import { renderWithMantine, screen } from "@/test/renderWithMantine"; - import { Exercise } from "@/types/exercises"; import { ActivitiesRequiredCell } from "./ActivitiesRequiredCell"; @@ -18,8 +18,6 @@ vi.mock("@components/ui/notify", () => ({ } })); -import { notify } from "@components/ui/notify"; - const reading = { id: 7, name: "1.2 Variables", diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/reading/components/EditAllReadings/EditDropdownValueHeaderReadings.spec.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/reading/components/EditAllReadings/EditDropdownValueHeaderReadings.spec.tsx index de44ce4fb..10187f67b 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/reading/components/EditAllReadings/EditDropdownValueHeaderReadings.spec.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/reading/components/EditAllReadings/EditDropdownValueHeaderReadings.spec.tsx @@ -1,3 +1,4 @@ +import { notify } from "@components/ui/notify"; import userEvent from "@testing-library/user-event"; import { FC } from "react"; @@ -31,8 +32,6 @@ vi.mock("@components/ui/notify", () => ({ } })); -import { notify } from "@components/ui/notify"; - vi.mock("@/hooks/useReadingsSelector", () => ({ useReadingsSelector: () => ({ readingExercises: [{ id: 1, question_json: {} }] }) })); diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/wizard/AssignmentWizard.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/wizard/AssignmentWizard.tsx index cd472f361..582a10672 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/wizard/AssignmentWizard.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/wizard/AssignmentWizard.tsx @@ -12,16 +12,13 @@ import { import classNames from "classnames"; import { Control, Controller, UseFormSetValue, UseFormWatch } from "react-hook-form"; +import stepperStyles from "@/components/ui/WizardStepper.module.css"; import { Assignment, KindOfAssignment } from "@/types/assignment"; import { DateTimePicker } from "../../../../ui/DateTimePicker"; - import { CourseTimezoneNotice } from "../edit/CourseTimezoneNotice"; - import { VisibilityControl } from "../edit/VisibilityControl"; -import stepperStyles from "@/components/ui/WizardStepper.module.css"; - import styles from "./AssignmentWizard.module.css"; const YES_NO_OPTIONS = [ diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/hooks/useAssignmentForm.spec.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/hooks/useAssignmentForm.spec.ts index f3dfde6d3..0670bb495 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/hooks/useAssignmentForm.spec.ts +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/hooks/useAssignmentForm.spec.ts @@ -1,7 +1,9 @@ import { renderHook, act } from "@testing-library/react"; -import { useAssignmentForm } from "./useAssignmentForm"; + import { Assignment } from "@/types/assignment"; +import { useAssignmentForm } from "./useAssignmentForm"; + const makeAssignment = (overrides: Partial = {}): Assignment => ({ id: 1, name: "Test Assignment", @@ -223,6 +225,7 @@ describe("useAssignmentForm", () => { expect(onAssignmentUpdate).toHaveBeenCalled(); const calledWith = onAssignmentUpdate.mock.calls[onAssignmentUpdate.mock.calls.length - 1][0]; + expect(calledWith.points).toBe(99); }); diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/hooks/useAssignmentRouting.spec.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/hooks/useAssignmentRouting.spec.ts index 35fe4c6cd..a41e60b7c 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/hooks/useAssignmentRouting.spec.ts +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/hooks/useAssignmentRouting.spec.ts @@ -1,11 +1,12 @@ import { renderHook, act } from "@testing-library/react"; -import { MemoryRouter, Route, Routes } from "react-router-dom"; import React from "react"; +import { MemoryRouter, Route, Routes } from "react-router-dom"; const mockNavigate = vi.fn(); vi.mock("react-router-dom", async () => { const actual = await vi.importActual("react-router-dom"); + return { ...actual, useNavigate: () => mockNavigate @@ -59,6 +60,7 @@ const wrapEdit = (path: string) => { EDIT_ROUTES.find((r) => { const segments = r.split("/"); const pathSegments = path.split("/"); + if (segments.length !== pathSegments.length) return false; return segments.every((s, i) => s.startsWith(":") || s === pathSegments[i]); }) ?? "*"; @@ -85,23 +87,27 @@ describe("useAssignmentRouting", () => { describe("routeState derivation from path", () => { it("sets mode to list when path is /builder", () => { const { result } = renderHook(() => useAssignmentRouting(), wrap("/builder")); + expect(result.current.mode).toBe("list"); expect(result.current.selectedAssignmentId).toBeNull(); }); it("sets mode to list when path is /builder/", () => { const { result } = renderHook(() => useAssignmentRouting(), wrap("/builder/")); + expect(result.current.mode).toBe("list"); }); it("sets mode to create and wizardStep to basic for /builder/create", () => { const { result } = renderHook(() => useAssignmentRouting(), wrap("/builder/create")); + expect(result.current.mode).toBe("create"); expect(result.current.wizardStep).toBe("basic"); }); it("sets wizardStep to type for /builder/create/type", () => { const { result } = renderHook(() => useAssignmentRouting(), wrap("/builder/create/type")); + expect(result.current.mode).toBe("create"); expect(result.current.wizardStep).toBe("type"); }); @@ -111,12 +117,14 @@ describe("useAssignmentRouting", () => { () => useAssignmentRouting(), wrap("/builder/create/visibility") ); + expect(result.current.mode).toBe("create"); expect(result.current.wizardStep).toBe("visibility"); }); it("sets mode to edit and captures assignmentId for /builder/:id", () => { const { result } = renderHook(() => useAssignmentRouting(), wrapEdit("/builder/42")); + expect(result.current.mode).toBe("edit"); expect(result.current.selectedAssignmentId).toBe("42"); expect(result.current.activeTab).toBe("basic"); @@ -124,6 +132,7 @@ describe("useAssignmentRouting", () => { it("sets activeTab to readings for /builder/:id/readings", () => { const { result } = renderHook(() => useAssignmentRouting(), wrapEdit("/builder/42/readings")); + expect(result.current.mode).toBe("edit"); expect(result.current.activeTab).toBe("readings"); }); @@ -133,6 +142,7 @@ describe("useAssignmentRouting", () => { () => useAssignmentRouting(), wrapEdit("/builder/42/exercises") ); + expect(result.current.activeTab).toBe("exercises"); expect(result.current.exerciseViewMode).toBe("list"); }); @@ -142,6 +152,7 @@ describe("useAssignmentRouting", () => { () => useAssignmentRouting(), wrapEdit("/builder/42/exercises/browse") ); + expect(result.current.exerciseViewMode).toBe("browse"); }); @@ -150,6 +161,7 @@ describe("useAssignmentRouting", () => { () => useAssignmentRouting(), wrapEdit("/builder/42/exercises/search") ); + expect(result.current.exerciseViewMode).toBe("search"); }); @@ -158,6 +170,7 @@ describe("useAssignmentRouting", () => { () => useAssignmentRouting(), wrapEdit("/builder/42/exercises/create/mchoice") ); + expect(result.current.exerciseViewMode).toBe("create"); expect(result.current.exerciseType).toBe("mchoice"); expect(result.current.exerciseSubType).toBeNull(); @@ -169,6 +182,7 @@ describe("useAssignmentRouting", () => { () => useAssignmentRouting(), wrapEdit("/builder/42/exercises/create/mchoice/advanced") ); + expect(result.current.exerciseType).toBe("mchoice"); expect(result.current.exerciseSubType).toBe("advanced"); }); @@ -178,6 +192,7 @@ describe("useAssignmentRouting", () => { () => useAssignmentRouting(), wrapEdit("/builder/42/exercises/create/mchoice/advanced/2") ); + expect(result.current.exerciseStep).toBe(2); }); @@ -186,6 +201,7 @@ describe("useAssignmentRouting", () => { () => useAssignmentRouting(), wrapEdit("/builder/42/exercises/edit/99") ); + expect(result.current.exerciseViewMode).toBe("edit"); expect(result.current.exerciseId).toBe("99"); expect(result.current.exerciseStep).toBe(0); @@ -196,11 +212,13 @@ describe("useAssignmentRouting", () => { () => useAssignmentRouting(), wrapEdit("/builder/42/exercises/edit/99/3") ); + expect(result.current.exerciseStep).toBe(3); }); it("returns default state for unrecognized path", () => { const { result } = renderHook(() => useAssignmentRouting(), wrap("/unknown")); + expect(result.current.mode).toBe("list"); expect(result.current.wizardStep).toBe("basic"); expect(result.current.activeTab).toBe("basic"); @@ -215,6 +233,7 @@ describe("useAssignmentRouting", () => { describe("navigateToList", () => { it("calls navigate with /builder", () => { const { result } = renderHook(() => useAssignmentRouting(), wrap("/builder")); + act(() => result.current.navigateToList()); expect(mockNavigate).toHaveBeenCalledWith("/builder"); }); @@ -223,24 +242,28 @@ describe("useAssignmentRouting", () => { describe("navigateToCreate", () => { it("calls navigate with /builder/create when no step given", () => { const { result } = renderHook(() => useAssignmentRouting(), wrap("/builder/create")); + act(() => result.current.navigateToCreate()); expect(mockNavigate).toHaveBeenCalledWith("/builder/create"); }); it("calls navigate with /builder/create/type for step=type", () => { const { result } = renderHook(() => useAssignmentRouting(), wrap("/builder/create")); + act(() => result.current.navigateToCreate("type")); expect(mockNavigate).toHaveBeenCalledWith("/builder/create/type"); }); it("calls navigate with /builder/create/visibility for step=visibility", () => { const { result } = renderHook(() => useAssignmentRouting(), wrap("/builder/create")); + act(() => result.current.navigateToCreate("visibility")); expect(mockNavigate).toHaveBeenCalledWith("/builder/create/visibility"); }); it("calls navigate with /builder/create for step=basic", () => { const { result } = renderHook(() => useAssignmentRouting(), wrap("/builder/create")); + act(() => result.current.navigateToCreate("basic")); expect(mockNavigate).toHaveBeenCalledWith("/builder/create"); }); @@ -249,24 +272,28 @@ describe("useAssignmentRouting", () => { describe("navigateToEdit", () => { it("navigates to /builder/:id for tab=basic", () => { const { result } = renderHook(() => useAssignmentRouting(), wrapEdit("/builder/42")); + act(() => result.current.navigateToEdit("42", "basic")); expect(mockNavigate).toHaveBeenCalledWith("/builder/42"); }); it("navigates to /builder/:id/readings for tab=readings", () => { const { result } = renderHook(() => useAssignmentRouting(), wrapEdit("/builder/42")); + act(() => result.current.navigateToEdit("42", "readings")); expect(mockNavigate).toHaveBeenCalledWith("/builder/42/readings"); }); it("navigates to /builder/:id/exercises for tab=exercises", () => { const { result } = renderHook(() => useAssignmentRouting(), wrapEdit("/builder/42")); + act(() => result.current.navigateToEdit("42", "exercises")); expect(mockNavigate).toHaveBeenCalledWith("/builder/42/exercises"); }); it("defaults tab to basic when omitted", () => { const { result } = renderHook(() => useAssignmentRouting(), wrapEdit("/builder/42")); + act(() => result.current.navigateToEdit("42")); expect(mockNavigate).toHaveBeenCalledWith("/builder/42"); }); @@ -278,6 +305,7 @@ describe("useAssignmentRouting", () => { () => useAssignmentRouting(), wrapEdit("/builder/42/exercises") ); + act(() => result.current.navigateToExercises("42", "list")); expect(mockNavigate).toHaveBeenCalledWith("/builder/42/exercises"); }); @@ -287,6 +315,7 @@ describe("useAssignmentRouting", () => { () => useAssignmentRouting(), wrapEdit("/builder/42/exercises") ); + act(() => result.current.navigateToExercises("42")); expect(mockNavigate).toHaveBeenCalledWith("/builder/42/exercises"); }); @@ -296,6 +325,7 @@ describe("useAssignmentRouting", () => { () => useAssignmentRouting(), wrapEdit("/builder/42/exercises") ); + act(() => result.current.navigateToExercises("42", "browse")); expect(mockNavigate).toHaveBeenCalledWith("/builder/42/exercises/browse"); }); @@ -305,6 +335,7 @@ describe("useAssignmentRouting", () => { () => useAssignmentRouting(), wrapEdit("/builder/42/exercises") ); + act(() => result.current.navigateToExercises("42", "search")); expect(mockNavigate).toHaveBeenCalledWith("/builder/42/exercises/search"); }); @@ -314,6 +345,7 @@ describe("useAssignmentRouting", () => { () => useAssignmentRouting(), wrapEdit("/builder/42/exercises") ); + act(() => result.current.navigateToExercises("42", "create", { exerciseType: "mchoice" })); expect(mockNavigate).toHaveBeenCalledWith("/builder/42/exercises/create/mchoice"); }); @@ -323,6 +355,7 @@ describe("useAssignmentRouting", () => { () => useAssignmentRouting(), wrapEdit("/builder/42/exercises") ); + act(() => result.current.navigateToExercises("42", "create", { exerciseType: "mchoice", @@ -337,6 +370,7 @@ describe("useAssignmentRouting", () => { () => useAssignmentRouting(), wrapEdit("/builder/42/exercises") ); + act(() => result.current.navigateToExercises("42", "create", { exerciseType: "mchoice", @@ -352,6 +386,7 @@ describe("useAssignmentRouting", () => { () => useAssignmentRouting(), wrapEdit("/builder/42/exercises") ); + act(() => result.current.navigateToExercises("42", "create")); expect(mockNavigate).toHaveBeenCalledWith("/builder/42/exercises/create"); }); @@ -361,6 +396,7 @@ describe("useAssignmentRouting", () => { () => useAssignmentRouting(), wrapEdit("/builder/42/exercises") ); + act(() => result.current.navigateToExercises("42", "edit", { exerciseId: "99" })); expect(mockNavigate).toHaveBeenCalledWith("/builder/42/exercises/edit/99"); }); @@ -370,6 +406,7 @@ describe("useAssignmentRouting", () => { () => useAssignmentRouting(), wrapEdit("/builder/42/exercises") ); + act(() => result.current.navigateToExercises("42", "edit", { exerciseId: "99", step: 3 })); expect(mockNavigate).toHaveBeenCalledWith("/builder/42/exercises/edit/99/3"); }); @@ -379,6 +416,7 @@ describe("useAssignmentRouting", () => { () => useAssignmentRouting(), wrapEdit("/builder/42/exercises") ); + act(() => result.current.navigateToExercises("42", "edit")); expect(mockNavigate).toHaveBeenCalledWith("/builder/42/exercises/edit"); }); @@ -390,6 +428,7 @@ describe("useAssignmentRouting", () => { () => useAssignmentRouting(), wrapEdit("/builder/42/exercises") ); + act(() => result.current.navigateToExerciseTypeSelection("42", "mchoice")); expect(mockNavigate).toHaveBeenCalledWith("/builder/42/exercises/create/mchoice"); }); @@ -401,6 +440,7 @@ describe("useAssignmentRouting", () => { () => useAssignmentRouting(), wrapEdit("/builder/42/exercises") ); + act(() => result.current.navigateToExerciseSubTypeSelection("42", "mchoice", "advanced")); expect(mockNavigate).toHaveBeenCalledWith("/builder/42/exercises/create/mchoice/advanced"); }); @@ -409,18 +449,21 @@ describe("useAssignmentRouting", () => { describe("updateWizardStep", () => { it("navigates to /builder/create/type", () => { const { result } = renderHook(() => useAssignmentRouting(), wrap("/builder/create")); + act(() => result.current.updateWizardStep("type")); expect(mockNavigate).toHaveBeenCalledWith("/builder/create/type"); }); it("navigates to /builder/create/visibility", () => { const { result } = renderHook(() => useAssignmentRouting(), wrap("/builder/create")); + act(() => result.current.updateWizardStep("visibility")); expect(mockNavigate).toHaveBeenCalledWith("/builder/create/visibility"); }); it("navigates to /builder/create for basic step", () => { const { result } = renderHook(() => useAssignmentRouting(), wrap("/builder/create")); + act(() => result.current.updateWizardStep("basic")); expect(mockNavigate).toHaveBeenCalledWith("/builder/create"); }); @@ -429,18 +472,21 @@ describe("useAssignmentRouting", () => { describe("updateEditTab", () => { it("does not navigate when no selectedAssignmentId", () => { const { result } = renderHook(() => useAssignmentRouting(), wrap("/builder")); + act(() => result.current.updateEditTab("readings")); expect(mockNavigate).not.toHaveBeenCalled(); }); it("navigates to exercises tab", () => { const { result } = renderHook(() => useAssignmentRouting(), wrapEdit("/builder/42/readings")); + act(() => result.current.updateEditTab("exercises")); expect(mockNavigate).toHaveBeenCalledWith("/builder/42/exercises"); }); it("navigates to base path when tab is basic", () => { const { result } = renderHook(() => useAssignmentRouting(), wrapEdit("/builder/42/readings")); + act(() => result.current.updateEditTab("basic")); expect(mockNavigate).toHaveBeenCalledWith("/builder/42"); }); @@ -449,6 +495,7 @@ describe("useAssignmentRouting", () => { describe("updateExerciseViewMode", () => { it("does not navigate when no selectedAssignmentId", () => { const { result } = renderHook(() => useAssignmentRouting(), wrap("/builder")); + act(() => result.current.updateExerciseViewMode("browse")); expect(mockNavigate).not.toHaveBeenCalled(); }); @@ -458,6 +505,7 @@ describe("useAssignmentRouting", () => { () => useAssignmentRouting(), wrapEdit("/builder/42/exercises") ); + act(() => result.current.updateExerciseViewMode("browse")); expect(mockNavigate).toHaveBeenCalledWith("/builder/42/exercises/browse"); }); @@ -467,6 +515,7 @@ describe("useAssignmentRouting", () => { () => useAssignmentRouting(), wrapEdit("/builder/42/exercises") ); + act(() => result.current.updateExerciseViewMode("create", { exerciseType: "mchoice" })); expect(mockNavigate).toHaveBeenCalledWith("/builder/42/exercises/create/mchoice"); }); @@ -475,6 +524,7 @@ describe("useAssignmentRouting", () => { describe("updateExerciseType", () => { it("does not navigate when no selectedAssignmentId", () => { const { result } = renderHook(() => useAssignmentRouting(), wrap("/builder")); + act(() => result.current.updateExerciseType("mchoice")); expect(mockNavigate).not.toHaveBeenCalled(); }); @@ -484,6 +534,7 @@ describe("useAssignmentRouting", () => { () => useAssignmentRouting(), wrapEdit("/builder/42/exercises") ); + act(() => result.current.updateExerciseType("mchoice")); expect(mockNavigate).toHaveBeenCalledWith("/builder/42/exercises/create/mchoice"); }); @@ -492,6 +543,7 @@ describe("useAssignmentRouting", () => { describe("updateExerciseSubType", () => { it("does not navigate when no selectedAssignmentId", () => { const { result } = renderHook(() => useAssignmentRouting(), wrap("/builder")); + act(() => result.current.updateExerciseSubType("advanced")); expect(mockNavigate).not.toHaveBeenCalled(); }); @@ -501,6 +553,7 @@ describe("useAssignmentRouting", () => { () => useAssignmentRouting(), wrapEdit("/builder/42/exercises") ); + act(() => result.current.updateExerciseSubType("advanced")); expect(mockNavigate).not.toHaveBeenCalled(); }); @@ -510,6 +563,7 @@ describe("useAssignmentRouting", () => { () => useAssignmentRouting(), wrapEdit("/builder/42/exercises/create/mchoice") ); + act(() => result.current.updateExerciseSubType("advanced")); expect(mockNavigate).toHaveBeenCalledWith("/builder/42/exercises/create/mchoice/advanced"); }); @@ -518,6 +572,7 @@ describe("useAssignmentRouting", () => { describe("updateExerciseStep", () => { it("does not navigate when no selectedAssignmentId", () => { const { result } = renderHook(() => useAssignmentRouting(), wrap("/builder")); + act(() => result.current.updateExerciseStep(1)); expect(mockNavigate).not.toHaveBeenCalled(); }); @@ -527,6 +582,7 @@ describe("useAssignmentRouting", () => { () => useAssignmentRouting(), wrapEdit("/builder/42/exercises") ); + act(() => result.current.updateExerciseStep(1)); expect(mockNavigate).not.toHaveBeenCalled(); }); @@ -536,6 +592,7 @@ describe("useAssignmentRouting", () => { () => useAssignmentRouting(), wrapEdit("/builder/42/exercises/create") ); + act(() => result.current.updateExerciseStep(1)); expect(mockNavigate).not.toHaveBeenCalled(); }); @@ -545,6 +602,7 @@ describe("useAssignmentRouting", () => { () => useAssignmentRouting(), wrapEdit("/builder/42/exercises/create/mchoice/advanced/0") ); + act(() => result.current.updateExerciseStep(2)); expect(mockNavigate).toHaveBeenCalledWith("/builder/42/exercises/create/mchoice/advanced/2"); }); @@ -554,6 +612,7 @@ describe("useAssignmentRouting", () => { () => useAssignmentRouting(), wrapEdit("/builder/42/exercises/edit/99/0") ); + act(() => result.current.updateExerciseStep(3)); expect(mockNavigate).toHaveBeenCalledWith("/builder/42/exercises/edit/99/3"); }); @@ -563,6 +622,7 @@ describe("useAssignmentRouting", () => { () => useAssignmentRouting(), wrapEdit("/builder/42/exercises/edit") ); + act(() => result.current.updateExerciseStep(1)); expect(mockNavigate).not.toHaveBeenCalled(); }); diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/hooks/useAssignmentState.spec.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/hooks/useAssignmentState.spec.ts index 86e7ce809..fe45dcce0 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/hooks/useAssignmentState.spec.ts +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/hooks/useAssignmentState.spec.ts @@ -1,19 +1,23 @@ import { renderHook, act } from "@testing-library/react"; -import { useAssignmentState } from "./useAssignmentState"; -import { Assignment, KindOfAssignment } from "@/types/assignment"; import { UseFormSetValue } from "react-hook-form"; +import { Assignment, KindOfAssignment } from "@/types/assignment"; + +import { useAssignmentState } from "./useAssignmentState"; + const makeSetValue = (): jest.Mock => vi.fn() as unknown as jest.Mock; describe("useAssignmentState", () => { describe("initial state", () => { it("returns empty string as default globalFilter", () => { const { result } = renderHook(() => useAssignmentState()); + expect(result.current.globalFilter).toBe(""); }); it("returns false as default isCollapsed", () => { const { result } = renderHook(() => useAssignmentState()); + expect(result.current.isCollapsed).toBe(false); }); }); @@ -21,6 +25,7 @@ describe("useAssignmentState", () => { describe("setGlobalFilter", () => { it("updates globalFilter when called with a new value", () => { const { result } = renderHook(() => useAssignmentState()); + act(() => { result.current.setGlobalFilter("search term"); }); @@ -29,6 +34,7 @@ describe("useAssignmentState", () => { it("clears globalFilter when called with empty string", () => { const { result } = renderHook(() => useAssignmentState()); + act(() => { result.current.setGlobalFilter("something"); }); @@ -42,6 +48,7 @@ describe("useAssignmentState", () => { describe("setIsCollapsed", () => { it("updates isCollapsed to true", () => { const { result } = renderHook(() => useAssignmentState()); + act(() => { result.current.setIsCollapsed(true); }); @@ -50,6 +57,7 @@ describe("useAssignmentState", () => { it("toggles isCollapsed back to false", () => { const { result } = renderHook(() => useAssignmentState()); + act(() => { result.current.setIsCollapsed(true); }); @@ -137,6 +145,7 @@ describe("useAssignmentState", () => { const nopauseCalls = (setValue as unknown as ReturnType).mock.calls.filter( ([field]: [string]) => field === "nopause" ); + expect(nopauseCalls.at(-1)).toEqual(["nopause", false]); }); @@ -154,6 +163,7 @@ describe("useAssignmentState", () => { const nofeedbackCalls = (setValue as unknown as ReturnType).mock.calls.filter( ([field]: [string]) => field === "nofeedback" ); + expect(nofeedbackCalls.at(-1)).toEqual(["nofeedback", false]); }); @@ -171,6 +181,7 @@ describe("useAssignmentState", () => { const isPeerTrueCalls = (setValue as unknown as ReturnType).mock.calls.filter( ([field, value]: [string, unknown]) => field === "is_peer" && value === true ); + expect(isPeerTrueCalls).toHaveLength(0); }); @@ -218,6 +229,7 @@ describe("useAssignmentState", () => { const peerAsyncCalls = (setValue as unknown as ReturnType).mock.calls.filter( ([field]: [string]) => field === "peer_async_visible" ); + expect(peerAsyncCalls.at(-1)).toEqual(["peer_async_visible", false]); }); @@ -237,6 +249,7 @@ describe("useAssignmentState", () => { ).mock.calls.filter( ([field, value]: [string, unknown]) => field === "is_timed" && value === true ); + expect(isTimedTrueCalls).toHaveLength(0); }); @@ -256,6 +269,7 @@ describe("useAssignmentState", () => { ).mock.calls.filter( ([field, value]: [string, unknown]) => field === "time_limit" && value !== null ); + expect(timeLimitNonNullCalls).toHaveLength(0); }); @@ -291,6 +305,7 @@ describe("useAssignmentState", () => { ).mock.calls.filter( ([field, value]: [string, unknown]) => field === "is_timed" && value === true ); + expect(isTimedTrueCalls).toHaveLength(0); }); @@ -308,6 +323,7 @@ describe("useAssignmentState", () => { const isPeerTrueCalls = (setValue as unknown as ReturnType).mock.calls.filter( ([field, value]: [string, unknown]) => field === "is_peer" && value === true ); + expect(isPeerTrueCalls).toHaveLength(0); }); @@ -344,6 +360,7 @@ describe("useAssignmentState", () => { const isTimedResets = allCalls.filter( ([field, value]: [string, unknown]) => field === "is_timed" && value === false ); + expect(isTimedResets.length).toBeGreaterThanOrEqual(2); }); }); @@ -351,6 +368,7 @@ describe("useAssignmentState", () => { describe("return shape", () => { it("exposes all expected keys", () => { const { result } = renderHook(() => useAssignmentState()); + expect(result.current).toHaveProperty("globalFilter"); expect(result.current).toHaveProperty("setGlobalFilter"); expect(result.current).toHaveProperty("isCollapsed"); diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/hooks/useNameValidation.spec.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/hooks/useNameValidation.spec.ts index ebf6fb554..6804cb496 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/hooks/useNameValidation.spec.ts +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/hooks/useNameValidation.spec.ts @@ -1,7 +1,9 @@ import { renderHook, act } from "@testing-library/react"; -import { useNameValidation } from "./useNameValidation"; + import { Assignment } from "@/types/assignment"; +import { useNameValidation } from "./useNameValidation"; + const makeAssignment = (name: string): Pick & Partial => ({ name }) as Assignment; @@ -39,18 +41,21 @@ describe("useNameValidation", () => { it("returns an error when name is empty string", () => { const { watch } = makeWatch(""); const { result } = renderHook(() => useNameValidation({ assignments: [], watch })); + expect(result.current.validateName("")).toBe("Assignment name is required"); }); it("returns an error when name is only whitespace", () => { const { watch } = makeWatch(" "); const { result } = renderHook(() => useNameValidation({ assignments: [], watch })); + expect(result.current.validateName(" ")).toBe("Assignment name is required"); }); it("returns null for a valid unique name", () => { const { watch } = makeWatch("New"); const { result } = renderHook(() => useNameValidation({ assignments: [], watch })); + expect(result.current.validateName("New")).toBeNull(); }); @@ -58,6 +63,7 @@ describe("useNameValidation", () => { const assignments = [makeAssignment("Homework 1")] as Assignment[]; const { watch } = makeWatch("Homework 1"); const { result } = renderHook(() => useNameValidation({ assignments, watch })); + expect(result.current.validateName("homework 1")).toBe( "An assignment with this name already exists. Pick another name." ); @@ -67,6 +73,7 @@ describe("useNameValidation", () => { const assignments = [makeAssignment("Quiz")] as Assignment[]; const { watch } = makeWatch("Quiz"); const { result } = renderHook(() => useNameValidation({ assignments, watch })); + expect(result.current.validateName("Quiz")).toBe( "An assignment with this name already exists. Pick another name." ); @@ -76,6 +83,7 @@ describe("useNameValidation", () => { const assignments = [makeAssignment("Old Assignment")] as Assignment[]; const { watch } = makeWatch("New Assignment"); const { result } = renderHook(() => useNameValidation({ assignments, watch })); + expect(result.current.validateName("New Assignment")).toBeNull(); }); }); @@ -84,6 +92,7 @@ describe("useNameValidation", () => { it("starts with nameError null and canProceed false when initial name is empty", () => { const { watch } = makeWatch(""); const { result } = renderHook(() => useNameValidation({ assignments: [], watch })); + expect(result.current.nameError).toBeNull(); expect(result.current.canProceed).toBe(false); }); @@ -91,12 +100,14 @@ describe("useNameValidation", () => { it("starts with canProceed true when initial name is valid", () => { const { watch } = makeWatch("Valid Name"); const { result } = renderHook(() => useNameValidation({ assignments: [], watch })); + expect(result.current.canProceed).toBe(true); }); it("does not show nameError even if initial name is invalid (not touched)", () => { const { watch } = makeWatch(""); const { result } = renderHook(() => useNameValidation({ assignments: [], watch })); + expect(result.current.nameError).toBeNull(); }); }); @@ -140,7 +151,9 @@ describe("useNameValidation", () => { trigger("Homework", "name"); }); - expect(result.current.nameError).toBe("An assignment with this name already exists. Pick another name."); + expect(result.current.nameError).toBe( + "An assignment with this name already exists. Pick another name." + ); expect(result.current.canProceed).toBe(false); }); @@ -196,6 +209,7 @@ describe("useNameValidation", () => { it("is false when name is empty regardless of touched state", () => { const { watch } = makeWatch(""); const { result } = renderHook(() => useNameValidation({ assignments: [], watch })); + expect(result.current.canProceed).toBe(false); }); @@ -203,12 +217,14 @@ describe("useNameValidation", () => { const assignments = [makeAssignment("Dup")] as Assignment[]; const { watch } = makeWatch("Dup"); const { result } = renderHook(() => useNameValidation({ assignments, watch })); + expect(result.current.canProceed).toBe(false); }); it("is true when name is non-empty and unique", () => { const { watch } = makeWatch("Unique"); const { result } = renderHook(() => useNameValidation({ assignments: [], watch })); + expect(result.current.canProceed).toBe(true); }); }); diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/Grader.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/Grader.tsx index a656a8dca..226ed22f9 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/Grader.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/Grader.tsx @@ -1,23 +1,22 @@ import { Button, Tooltip } from "@mantine/core"; -import React, { useState } from "react"; -import { Link, Outlet, useLocation, useParams } from "react-router-dom"; - -import { Icon } from "@/components/ui/Icon"; - import { useGetAssignmentsQuery } from "@store/assignment/assignment.logic.api"; import { useGetGraderAnswersQuery, useGetGraderQuestionsQuery } from "@store/grader/grader.logic.api"; +import React, { useState } from "react"; +import { Link, Outlet, useLocation, useParams } from "react-router-dom"; + +import { Icon } from "@/components/ui/Icon"; -import { ShortcutsHelpDialog } from "./components/ShortcutsHelpDialog"; import styles from "./Grader.module.css"; +import { ShortcutsHelpDialog } from "./components/ShortcutsHelpDialog"; import { useEnsureEbookConfigForGrader } from "./hooks/useEnsureEbookConfigForGrader"; import { useGraderTour } from "./hooks/useGraderTour"; import { usePlatform } from "./hooks/usePlatform"; import { getQuestionProgress } from "./state/graderSelectors"; -import { DEMO_ASSIGNMENTS, getDemoAnswersFor, getDemoQuestionsFor } from "./tour/graderDemoData"; import { GraderTourProvider, useGraderTourContext } from "./tour/GraderTourContext"; +import { DEMO_ASSIGNMENTS, getDemoAnswersFor, getDemoQuestionsFor } from "./tour/graderDemoData"; export const Grader: React.FC = () => { return ( diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/DeadlineExceptionDialog.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/DeadlineExceptionDialog.tsx index 5b947898d..e109d5c0a 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/DeadlineExceptionDialog.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/DeadlineExceptionDialog.tsx @@ -1,10 +1,9 @@ import { Button, Checkbox, Group, Modal, NumberInput, Radio } from "@mantine/core"; -import React, { useEffect, useState } from "react"; - import { AccommodationPayload, useUpsertAccommodationMutation } from "@store/grader/grader.logic.api"; +import React, { useEffect, useState } from "react"; import styles from "../Grader.module.css"; @@ -38,6 +37,8 @@ export const DeadlineExceptionDialog: React.FC = ( if (visible) { setSelectedSids(presetSids ?? []); } + // TODO(eslint): Replace the derived dependency with a stable, behavior-preserving value. + // eslint-disable-next-line react-hooks/exhaustive-deps }, [visible, (presetSids ?? []).join(",")]); const close = () => onHide(); @@ -52,6 +53,7 @@ export const DeadlineExceptionDialog: React.FC = ( visible: visibleFlag, allowLink }; + await upsert(payload).unwrap(); onSaved?.(); close(); diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/GradePanel.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/GradePanel.tsx index a2bb5c5d9..fc7da58f5 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/GradePanel.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/GradePanel.tsx @@ -1,12 +1,13 @@ import { Button, NumberInput, Switch, Textarea, Tooltip } from "@mantine/core"; +import { GraderStudentAnswer } from "@store/grader/grader.logic.api"; import React, { forwardRef, useImperativeHandle, useRef } from "react"; import { Icon } from "@/components/ui/Icon"; -import { GraderStudentAnswer } from "@store/grader/grader.logic.api"; -import { AutoSaveStatus } from "../hooks/useAutoSaveGrade"; import styles from "../Grader.module.css"; +import { AutoSaveStatus } from "../hooks/useAutoSaveGrade"; import { studentDisplayName } from "../state/graderSelectors"; + import { SaveStatusPill } from "./SaveStatusPill"; interface Props { diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/GradebookCellDialog.spec.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/GradebookCellDialog.spec.tsx index 00a0db9bc..5537baea8 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/GradebookCellDialog.spec.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/GradebookCellDialog.spec.tsx @@ -1,8 +1,8 @@ +import type { StudentAssignmentScoresResponse } from "@store/grader/grader.logic.api"; import userEvent from "@testing-library/user-event"; import { MemoryRouter } from "react-router-dom"; import { renderWithMantine, screen } from "@/test/renderWithMantine"; -import type { StudentAssignmentScoresResponse } from "@store/grader/grader.logic.api"; import { GradebookCellDialog } from "./GradebookCellDialog"; diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/GradebookCellDialog.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/GradebookCellDialog.tsx index caebc1bb0..c79117491 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/GradebookCellDialog.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/GradebookCellDialog.tsx @@ -10,11 +10,6 @@ import { Table, Text } from "@mantine/core"; -import React, { useEffect, useState } from "react"; -import { Link } from "react-router-dom"; - -import { Icon } from "@/components/ui/Icon"; -import { notify } from "@/components/ui/notify"; import type { GradebookAssignment, GradebookStudent, @@ -25,6 +20,11 @@ import { useGetStudentAssignmentScoresQuery, useRegradeMutation } from "@store/grader/grader.logic.api"; +import React, { useEffect, useState } from "react"; +import { Link } from "react-router-dom"; + +import { Icon } from "@/components/ui/Icon"; +import { notify } from "@/components/ui/notify"; import styles from "../Grader.module.css"; import { formatScore, isTotalStale, questionScoreSum } from "../state/gradebookSelectors"; diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/GradebookUnitsToggle.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/GradebookUnitsToggle.tsx index 7f7d468ca..875555b98 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/GradebookUnitsToggle.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/GradebookUnitsToggle.tsx @@ -1,8 +1,8 @@ import { SegmentedControl, Tooltip } from "@mantine/core"; +import { useSetGradebookUnitsMutation } from "@store/grader/grader.logic.api"; import React from "react"; import { notify } from "@/components/ui/notify"; -import { useSetGradebookUnitsMutation } from "@store/grader/grader.logic.api"; interface GradebookUnitsToggleProps { /** Whether the course currently shows raw points instead of percentages. */ diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/ManualTotalForm.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/ManualTotalForm.tsx index bffb58478..7063247c5 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/ManualTotalForm.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/ManualTotalForm.tsx @@ -1,8 +1,8 @@ import { Badge, Button, Group, NumberInput, Stack, Text } from "@mantine/core"; +import { useSetManualTotalMutation } from "@store/grader/grader.logic.api"; import React, { useEffect, useState } from "react"; import { notify } from "@/components/ui/notify"; -import { useSetManualTotalMutation } from "@store/grader/grader.logic.api"; import styles from "../Grader.module.css"; import { formatScore } from "../state/gradebookSelectors"; diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/MultiGradeDialog.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/MultiGradeDialog.tsx index 6d25e80d2..7e1d6df80 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/MultiGradeDialog.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/MultiGradeDialog.tsx @@ -10,9 +10,6 @@ import { Textarea, TextInput } from "@mantine/core"; -import React, { useMemo, useState } from "react"; - -import { Icon } from "@/components/ui/Icon"; import { GraderQuestionStats, GraderStudentAnswer, @@ -21,6 +18,9 @@ import { useRecomputeTotalsMutation, useSaveGradeMutation } from "@store/grader/grader.logic.api"; +import React, { useMemo, useState } from "react"; + +import { Icon } from "@/components/ui/Icon"; import styles from "../Grader.module.css"; @@ -35,6 +35,7 @@ interface MultiGradeDialogProps { const studentLabel = (a: GraderStudentAnswer) => { const name = `${a.first_name ?? ""} ${a.last_name ?? ""}`.trim(); + return name ? `${name} (${a.sid})` : a.sid; }; @@ -63,6 +64,7 @@ const QuestionGradeSection: React.FC = ({ const rows = useMemo(() => { const answers = data?.answers ?? []; + return allowedSids ? answers.filter((a) => allowedSids.has(a.sid)) : answers; }, [data, allowedSids]); @@ -70,6 +72,7 @@ const QuestionGradeSection: React.FC = ({ const editedSids = Object.keys(edits).filter((sid) => { const e = edits[sid]; + return e && (e.score !== undefined || (e.comment ?? "") !== ""); }); @@ -82,11 +85,13 @@ const QuestionGradeSection: React.FC = ({ if (editedSids.length === 0) return; setSaving(true); const saved: string[] = []; + for (const sid of editedSids) { const e = edits[sid]; const row = rows.find((r) => r.sid === sid); const score = e.score ?? row?.score ?? 0; const comment = (e.comment ?? "").trim(); + try { await save({ sid, @@ -208,6 +213,7 @@ export const MultiGradeDialog: React.FC = ({ const addAffected = (sids: string[]) => setAffected((prev) => { const next = new Set(prev); + sids.forEach((s) => next.add(s)); return next; }); @@ -216,11 +222,13 @@ export const MultiGradeDialog: React.FC = ({ if (sameScore == null) return; const targetSids = selectedSids.length > 0 ? selectedSids : (roster ?? []).map((s) => s.username); + if (targetSids.length === 0) return; const comment = sameComment.trim(); const total = targetSids.length * questions.length; let done = 0; + setProgress({ done, total }); const savedSids: string[] = []; diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/RegradeWizard.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/RegradeWizard.tsx index bc8da58c9..4b8b5fbb7 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/RegradeWizard.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/RegradeWizard.tsx @@ -1,8 +1,4 @@ import { Button, Center, Checkbox, Loader, Modal, Stepper } from "@mantine/core"; -import { ColumnDef } from "@tanstack/react-table"; -import React, { useEffect, useMemo, useState } from "react"; - -import { DataGrid } from "@/components/ui/DataGrid"; import { GraderQuestionStats, RegradeReport, @@ -10,7 +6,10 @@ import { useRegradeMutation, useRegradePreviewMutation } from "@store/grader/grader.logic.api"; +import { ColumnDef } from "@tanstack/react-table"; +import React, { useEffect, useMemo, useState } from "react"; +import { DataGrid } from "@/components/ui/DataGrid"; import stepperStyles from "@/components/ui/WizardStepper.module.css"; import styles from "../Grader.module.css"; @@ -99,12 +98,14 @@ export const RegradeWizard: React.FC = ({ const toPreview = async () => { const res = await preview(buildRequest()).unwrap(); + setReport(res); setStep(2); }; const doRun = async () => { const res = await run(buildRequest()).unwrap(); + setReport(res); onComplete?.(res); close(); diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/ReleaseGradesControl.spec.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/ReleaseGradesControl.spec.tsx index c2352296d..3475caab4 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/ReleaseGradesControl.spec.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/ReleaseGradesControl.spec.tsx @@ -15,6 +15,7 @@ const { mockOpenConfirmModal, mockUseGetAssignmentsQuery, mockSetReleased, mockN vi.mock("@mantine/modals", async (importOriginal) => { const original = await importOriginal(); + return { ...original, modals: { ...original.modals, openConfirmModal: mockOpenConfirmModal } }; }); @@ -68,6 +69,7 @@ describe("ReleaseGradesControl", () => { expect(mockOpenConfirmModal).toHaveBeenCalledTimes(1); const config = mockOpenConfirmModal.mock.calls[0][0]; + expect(config.title).toBe("Release grades"); expect(config.labels.confirm).toBe("Release"); @@ -84,6 +86,7 @@ describe("ReleaseGradesControl", () => { await userEvent.click(screen.getByRole("switch")); const config = mockOpenConfirmModal.mock.calls[0][0]; + expect(config.title).toBe("Hide grades"); expect(config.confirmProps).toEqual({ color: "red" }); diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/ReleaseGradesControl.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/ReleaseGradesControl.tsx index 6fd11ad50..babafb68d 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/ReleaseGradesControl.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/ReleaseGradesControl.tsx @@ -1,11 +1,11 @@ import { Switch, Text, Tooltip } from "@mantine/core"; import { modals } from "@mantine/modals"; +import { useGetAssignmentsQuery } from "@store/assignment/assignment.logic.api"; +import { useSetAssignmentReleasedMutation } from "@store/grader/grader.logic.api"; import React from "react"; import { Icon } from "@/components/ui/Icon"; import { notify } from "@/components/ui/notify"; -import { useGetAssignmentsQuery } from "@store/assignment/assignment.logic.api"; -import { useSetAssignmentReleasedMutation } from "@store/grader/grader.logic.api"; import styles from "../Grader.module.css"; diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/StudentListSidebar.spec.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/StudentListSidebar.spec.tsx index 8f95c3d7b..410df8646 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/StudentListSidebar.spec.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/StudentListSidebar.spec.tsx @@ -1,8 +1,8 @@ +import type { GraderStudentAnswer } from "@store/grader/grader.logic.api"; import userEvent from "@testing-library/user-event"; import { vi } from "vitest"; import { renderWithMantine, screen, within } from "@/test/renderWithMantine"; -import type { GraderStudentAnswer } from "@store/grader/grader.logic.api"; import { StudentListSidebar } from "./StudentListSidebar"; @@ -26,6 +26,7 @@ const renderSidebar = (props: Partial { renderSidebar(); const options = screen.getAllByRole("option"); + expect(options).toHaveLength(3); expect(screen.getByText("Ada Lovelace")).toBeInTheDocument(); expect(within(options[2]).getAllByText("s3").length).toBeGreaterThanOrEqual(1); @@ -108,6 +110,7 @@ describe("StudentListSidebar", () => { renderSidebar({ activeSid: "s2" }); const selected = screen.getByRole("option", { selected: true }); + expect(within(selected).getByText("Bob Stone")).toBeInTheDocument(); }); diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/StudentListSidebar.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/StudentListSidebar.tsx index 2c291e3b1..0bbf3b42b 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/StudentListSidebar.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/StudentListSidebar.tsx @@ -1,8 +1,8 @@ import { Button, Progress, TextInput } from "@mantine/core"; +import { GraderStudentAnswer } from "@store/grader/grader.logic.api"; import React, { useMemo, useRef } from "react"; import { Icon } from "@/components/ui/Icon"; -import { GraderStudentAnswer } from "@store/grader/grader.logic.api"; import { getQuestionProgress, @@ -13,6 +13,7 @@ import { studentDisplayName, StudentGradingStatus } from "../state/graderSelectors"; + import styles from "./StudentListSidebar.module.css"; interface Props { @@ -42,8 +43,10 @@ export const StudentListSidebar: React.FC = ({ const filtered = useMemo(() => { const q = filter.trim().toLowerCase(); + return answers.filter((a) => { const status = getStudentStatus(a, question, { dirtySids }); + if (hideGraded && (status === "graded" || status === "autograded")) return false; if (!q) return true; return studentDisplayName(a).toLowerCase().includes(q) || a.sid.toLowerCase().includes(q); @@ -102,6 +105,7 @@ export const StudentListSidebar: React.FC = ({ {filtered.map((a) => { const status = getStudentStatus(a, question, { dirtySids }); const active = a.sid === activeSid; + return (
  • { const name = `${s.first_name ?? ""} ${s.last_name ?? ""}`.trim(); + return name ? `${name} (${s.username})` : s.username; }; @@ -29,11 +30,13 @@ export const StudentMultiSelect: React.FC = ({ const students = useMemo(() => { const list = roster ?? []; + return [...list].sort((a, b) => displayName(a).localeCompare(displayName(b))); }, [roster]); const filtered = useMemo(() => { const term = search.trim().toLowerCase(); + if (!term) return students; return students.filter((s) => displayName(s).toLowerCase().includes(term)); }, [students, search]); @@ -54,9 +57,11 @@ export const StudentMultiSelect: React.FC = ({ const toggleAllFiltered = () => { if (allFilteredSelected) { const remove = new Set(filtered.map((s) => s.username)); + onChange(selected.filter((u) => !remove.has(u))); } else { const merged = new Set(selected); + filtered.forEach((s) => merged.add(s.username)); onChange(Array.from(merged)); } diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/SubmissionPane.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/SubmissionPane.tsx index 4ba435ca1..765f3431a 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/SubmissionPane.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/SubmissionPane.tsx @@ -1,16 +1,17 @@ import { Slider } from "@mantine/core"; -import React, { forwardRef, useEffect, useImperativeHandle, useMemo, useState } from "react"; - -import { Icon } from "@/components/ui/Icon"; import { GraderAnswerHistoryItem, GraderStudentAnswer, useGetGraderHistoryQuery } from "@store/grader/grader.logic.api"; +import React, { forwardRef, useEffect, useImperativeHandle, useMemo, useState } from "react"; + +import { Icon } from "@/components/ui/Icon"; import styles from "../Grader.module.css"; -import { getDemoHistoryFor } from "../tour/graderDemoData"; import { useGraderTourContext } from "../tour/GraderTourContext"; +import { getDemoHistoryFor } from "../tour/graderDemoData"; + import { AnswerRenderer } from "./questionTypes/AnswerRenderer"; import { CorrectChipKind, correctChipKind, formatAnswer } from "./submissionPaneHelpers"; @@ -39,6 +40,7 @@ const correctChip = ( h: Pick ): { label: string; cls: string } | null => { const kind = correctChipKind(h); + return kind ? { label: kind, cls: CHIP_CLASS[kind] } : null; }; @@ -52,6 +54,8 @@ export const SubmissionPane = forwardRef(function S { skip: isDemo } ); + // TODO(eslint): Stabilize history without changing demo or query update behavior. + // eslint-disable-next-line react-hooks/exhaustive-deps const history: GraderAnswerHistoryItem[] = isDemo ? getDemoHistoryFor(student.sid).history : (historyData?.history ?? []); @@ -82,6 +86,8 @@ export const SubmissionPane = forwardRef(function S nextAttempt: goNextAttempt }), + // TODO(eslint): Audit the imperative handle dependencies without changing navigation behavior. + // eslint-disable-next-line react-hooks/exhaustive-deps [totalAttempts] ); @@ -163,6 +169,7 @@ export const SubmissionPane = forwardRef(function S const c = correctChip(h); const active = idx === activeAttempt; const raw = formatAnswer(h.answer); + return ( } /> + Back} + /> ); const back = screen.getByRole("button", { name: "Back" }); diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/ui/TreeTable.spec.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/ui/TreeTable.spec.tsx index 11bf04cd6..c925f5cd6 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/ui/TreeTable.spec.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/ui/TreeTable.spec.tsx @@ -1,7 +1,6 @@ import userEvent from "@testing-library/user-event"; import { renderWithMantine, screen } from "@/test/renderWithMantine"; - import { SelectedKey, TreeNode } from "@/types/treeNode"; import { TreeTable, TreeTableColumn } from "./TreeTable"; diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/ui/TreeTable.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/ui/TreeTable.tsx index fc658d34c..c43fa3af8 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/ui/TreeTable.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/ui/TreeTable.tsx @@ -5,7 +5,6 @@ import { ReactNode, RefCallback, useMemo, useState } from "react"; import { SelectedKey, TreeNode } from "@/types/treeNode"; import { Icon } from "./Icon"; - import styles from "./TreeTable.module.css"; export interface TreeTableColumn { @@ -71,6 +70,8 @@ export const TreeTable = ({ setExpanded((prev) => { const next = new Set(prev); + // TODO(eslint): Replace the conditional expression with an explicit branch. + // eslint-disable-next-line @typescript-eslint/no-unused-expressions next.has(key) ? next.delete(key) : next.add(key); return next; }); diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/hooks/useExerciseTypes.spec.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/hooks/useExerciseTypes.spec.ts index beb251dc5..4e779b596 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/hooks/useExerciseTypes.spec.ts +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/hooks/useExerciseTypes.spec.ts @@ -1,10 +1,11 @@ import { configureStore } from "@reduxjs/toolkit"; +import { datasetSlice, datasetActions } from "@store/dataset/dataset.logic"; import { renderHook, act } from "@testing-library/react"; -import { Provider } from "react-redux"; import React from "react"; +import { Provider } from "react-redux"; -import { datasetSlice, datasetActions } from "@store/dataset/dataset.logic"; import { getExerciseColorScheme } from "@/config/exerciseTypes"; + import { useExerciseTypes } from "./useExerciseTypes"; function makeWrapper(store: ReturnType) { @@ -45,6 +46,7 @@ describe("useExerciseTypes", () => { const { result } = renderHook(() => useExerciseTypes(), { wrapper: makeWrapper(store) }); const values = result.current.map((e) => e.value); + expect(values).not.toContain("webwork"); expect(values).toContain("mchoice"); expect(values).toContain("fitb"); diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/hooks/useExercisesSelector.spec.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/hooks/useExercisesSelector.spec.ts index 1f6f5ed03..7fcb98622 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/hooks/useExercisesSelector.spec.ts +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/hooks/useExercisesSelector.spec.ts @@ -1,14 +1,21 @@ -import { renderHook, act } from "@testing-library/react"; +import { notify } from "@components/ui/notify"; import { configureStore } from "@reduxjs/toolkit"; -import { Provider } from "react-redux"; -import React from "react"; import { assignmentSlice } from "@store/assignment/assignment.logic"; import { assignmentExerciseSlice } from "@store/assignmentExercise/assignmentExercise.logic"; +import { + useGetExercisesQuery, + useRemoveAssignmentExercisesMutation +} from "@store/assignmentExercise/assignmentExercise.logic.api"; import { exercisesSlice } from "@store/exercises/exercises.logic"; import { readingsSlice } from "@store/readings/readings.logic"; -import { useExercisesSelector } from "./useExercisesSelector"; +import { renderHook, act } from "@testing-library/react"; +import React from "react"; +import { Provider } from "react-redux"; + import type { Exercise } from "@/types/exercises"; +import { useExercisesSelector } from "./useExercisesSelector"; + vi.mock("@store/assignmentExercise/assignmentExercise.logic.api", () => ({ useGetExercisesQuery: vi.fn(), useRemoveAssignmentExercisesMutation: vi.fn() @@ -26,12 +33,6 @@ vi.mock("@components/ui/notify", () => ({ } })); -import { - useGetExercisesQuery, - useRemoveAssignmentExercisesMutation -} from "@store/assignmentExercise/assignmentExercise.logic.api"; -import { notify } from "@components/ui/notify"; - const mockRefetchExercises = vi.fn(); const mockRemoveExercisesPost = vi.fn(); @@ -198,6 +199,7 @@ describe("useExercisesSelector — success state", () => { expect(result.current).toHaveProperty("assignmentExercises"); const exercises = (result.current as any).assignmentExercises as Exercise[]; + expect(exercises.every((ex) => !ex.reading_assignment)).toBe(true); expect(exercises.every((ex) => ex.question_type !== "page")).toBe(true); }); @@ -220,6 +222,7 @@ describe("useExercisesSelector — success state", () => { }); const chapters = (result.current as any).chapters; + expect(chapters).toHaveLength(2); expect(chapters[0]).toEqual({ value: "ch1", label: "Chapter 1" }); expect(chapters[1]).toEqual({ value: "ch2", label: "Chapter 2" }); @@ -253,6 +256,7 @@ describe("useExercisesSelector — success state", () => { }); const r = result.current as any; + if ("assignmentExercises" in r) { expect(r.isExercisesError).toBe(true); } else { @@ -292,6 +296,7 @@ describe("useExercisesSelector — success state", () => { }); const exercises = (result.current as any).assignmentExercises as Exercise[]; + expect(exercises.map((e) => e.id)).toEqual([2, 3, 1]); }); }); diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/hooks/useFullscreen.spec.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/hooks/useFullscreen.spec.ts index a14dabf57..dab628def 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/hooks/useFullscreen.spec.ts +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/hooks/useFullscreen.spec.ts @@ -1,6 +1,7 @@ -import React from "react"; import { renderHook, act } from "@testing-library/react"; +import React from "react"; import screenfull from "screenfull"; + import { useFullscreen } from "./useFullscreen"; vi.mock("screenfull", () => { @@ -18,10 +19,11 @@ vi.mock("screenfull", () => { off: vi.fn((name: string, handler: (event: Event) => void) => { if (name === "change") { const idx = changeHandlers.indexOf(handler); + if (idx !== -1) changeHandlers.splice(idx, 1); } }), - _changeHandlers: changeHandlers + changeHandlers } }; }); @@ -33,11 +35,11 @@ const screenfullMock = screenfull as unknown as { exit: ReturnType; on: ReturnType; off: ReturnType; - _changeHandlers: Array<(event: Event) => void>; + changeHandlers: Array<(event: Event) => void>; }; function triggerChange() { - screenfullMock._changeHandlers.forEach((h) => h(new Event("fullscreenchange"))); + screenfullMock.changeHandlers.forEach((h) => h(new Event("fullscreenchange"))); } describe("useFullscreen", () => { @@ -51,22 +53,26 @@ describe("useFullscreen", () => { describe("initial state", () => { it("returns isFullscreen as false initially", () => { const { result } = renderHook(() => useFullscreen()); + expect(result.current.isFullscreen).toBe(false); }); it("returns isSupported as true when screenfull is enabled", () => { const { result } = renderHook(() => useFullscreen()); + expect(result.current.isSupported).toBe(true); }); it("returns isSupported as false when screenfull is not enabled", () => { screenfullMock.isEnabled = false; const { result } = renderHook(() => useFullscreen()); + expect(result.current.isSupported).toBe(false); }); it("exposes toggleFullscreen, enterFullscreen, and exitFullscreen as functions", () => { const { result } = renderHook(() => useFullscreen()); + expect(typeof result.current.toggleFullscreen).toBe("function"); expect(typeof result.current.enterFullscreen).toBe("function"); expect(typeof result.current.exitFullscreen).toBe("function"); @@ -87,6 +93,7 @@ describe("useFullscreen", () => { it("removes the change listener on unmount", () => { const { unmount } = renderHook(() => useFullscreen()); + unmount(); expect(screenfullMock.off).toHaveBeenCalledWith("change", expect.any(Function)); }); diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/hooks/useFullscreen.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/hooks/useFullscreen.ts index 384adc437..c25e08626 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/hooks/useFullscreen.ts +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/hooks/useFullscreen.ts @@ -31,6 +31,7 @@ export const useFullscreen = (elementRef?: RefObject): UseFullscree try { const element = elementRef?.current || document.documentElement; + await screenfull.request(element); } catch (error) { console.error("Failed to enter fullscreen:", error); diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/hooks/useJwtUser.spec.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/hooks/useJwtUser.spec.ts index 69a75883c..0051cec19 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/hooks/useJwtUser.spec.ts +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/hooks/useJwtUser.spec.ts @@ -1,7 +1,8 @@ +import { setupStore } from "@store/store"; import { renderHook } from "@testing-library/react"; import React from "react"; import { Provider } from "react-redux"; -import { setupStore } from "@store/store"; + import { useJwtUser } from "./useJwtUser"; declare let eBookConfig: { username?: string }; @@ -12,6 +13,7 @@ const buildJwt = (payload: Record): string => { .replace(/\+/g, "-") .replace(/\//g, "_") .replace(/=+$/, ""); + return `${header}.${body}.fakesig`; }; @@ -52,6 +54,7 @@ describe("useJwtUser", () => { it("extracts username from a valid, non-expired JWT cookie", () => { const token = buildJwt({ sub: "alice", exp: futureExp() }); + setCookie(`access_token=${token}`); (globalThis as any).eBookConfig = {}; @@ -62,6 +65,7 @@ describe("useJwtUser", () => { it("returns null when JWT token is expired", () => { const token = buildJwt({ sub: "bob", exp: pastExp() }); + setCookie(`access_token=${token}`); (globalThis as any).eBookConfig = {}; @@ -90,6 +94,7 @@ describe("useJwtUser", () => { it("prefers JWT token over eBookConfig when a valid token is present", () => { const token = buildJwt({ sub: "dave", exp: futureExp() }); + setCookie(`access_token=${token}`); (globalThis as any).eBookConfig = { username: "carol" }; @@ -100,6 +105,7 @@ describe("useJwtUser", () => { it("ignores surrounding whitespace around the cookie value", () => { const token = buildJwt({ sub: "eve", exp: futureExp() }); + setCookie(`other=x; access_token= ${token} ; another=y`); (globalThis as any).eBookConfig = {}; @@ -110,6 +116,7 @@ describe("useJwtUser", () => { it("returns null when JWT sub is an empty string because the hook does not update state for falsy sub", () => { const token = buildJwt({ sub: "" }); + setCookie(`access_token=${token}`); (globalThis as any).eBookConfig = {}; @@ -120,6 +127,7 @@ describe("useJwtUser", () => { it("returns a username when JWT has no exp claim (non-expiring token)", () => { const token = buildJwt({ sub: "frank" }); + setCookie(`access_token=${token}`); (globalThis as any).eBookConfig = {}; diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/hooks/useJwtUser.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/hooks/useJwtUser.ts index 46df2457f..bd0517c45 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/hooks/useJwtUser.ts +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/hooks/useJwtUser.ts @@ -5,6 +5,7 @@ declare let eBookConfig: { username?: string; // add other known properties here }; + interface JwtPayload { sub: string; exp: number; diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/hooks/useReadingsSelector.spec.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/hooks/useReadingsSelector.spec.ts index 93057360e..03245f962 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/hooks/useReadingsSelector.spec.ts +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/hooks/useReadingsSelector.spec.ts @@ -1,18 +1,21 @@ -import { renderHook } from "@testing-library/react"; import { configureStore } from "@reduxjs/toolkit"; -import { Provider } from "react-redux"; -import React from "react"; import { assignmentSlice } from "@store/assignment/assignment.logic"; import { assignmentExerciseSlice } from "@store/assignmentExercise/assignmentExercise.logic"; -import { readingsSlice } from "@store/readings/readings.logic"; import { assignmentExerciseApi } from "@store/assignmentExercise/assignmentExercise.logic.api"; -import { useReadingsSelector } from "./useReadingsSelector"; -import { TreeNode } from "@/types/treeNode"; +import { readingsSlice } from "@store/readings/readings.logic"; +import { renderHook } from "@testing-library/react"; +import React from "react"; +import { Provider } from "react-redux"; + import { Exercise } from "@/types/exercises"; +import { TreeNode } from "@/types/treeNode"; + +import { useReadingsSelector } from "./useReadingsSelector"; vi.mock("@store/assignmentExercise/assignmentExercise.logic.api", async (importOriginal) => { const actual = await importOriginal(); + return { ...actual, useGetExercisesQuery: vi.fn() @@ -110,6 +113,7 @@ describe("useReadingsSelector", () => { describe("error state", () => { it("returns error true and refetch when query errors", () => { const mockRefetch = vi.fn(); + vi.mocked(useGetExercisesQuery).mockReturnValue({ isLoading: false, isError: true, @@ -126,6 +130,7 @@ describe("useReadingsSelector", () => { it("returns error true when data is undefined and not loading", () => { const mockRefetch = vi.fn(); + vi.mocked(useGetExercisesQuery).mockReturnValue({ isLoading: false, isError: false, @@ -141,6 +146,7 @@ describe("useReadingsSelector", () => { it("refetch calls the underlying refetch function", () => { const mockRefetch = vi.fn(); + vi.mocked(useGetExercisesQuery).mockReturnValue({ isLoading: false, isError: true, @@ -159,6 +165,7 @@ describe("useReadingsSelector", () => { describe("success state with no readings", () => { it("returns empty selectedKeys when no readings are assigned", () => { const exercises: Exercise[] = [makeExercise()]; + vi.mocked(useGetExercisesQuery).mockReturnValue({ isLoading: false, isError: false, @@ -170,6 +177,7 @@ describe("useReadingsSelector", () => { const { result } = renderHook(() => useReadingsSelector(), { wrapper: wrapper(store) }); const output = result.current as any; + expect(output.readingExercises).toEqual([]); expect(output.selectedKeys).toEqual({}); }); @@ -202,9 +210,10 @@ describe("useReadingsSelector", () => { const { result } = renderHook(() => useReadingsSelector(), { wrapper: wrapper(store) }); const output = result.current as any; + expect(output.readingExercises).toHaveLength(1); - expect(output.selectedKeys["sub1"]).toEqual({ checked: true, partialChecked: false }); - expect(output.selectedKeys["ch1"]).toEqual({ checked: false, partialChecked: true }); + expect(output.selectedKeys.sub1).toEqual({ checked: true, partialChecked: false }); + expect(output.selectedKeys.ch1).toEqual({ checked: false, partialChecked: true }); }); it("returns chapter fully checked when all children are assigned", () => { @@ -234,9 +243,10 @@ describe("useReadingsSelector", () => { const { result } = renderHook(() => useReadingsSelector(), { wrapper: wrapper(store) }); const output = result.current as any; - expect(output.selectedKeys["ch1"]).toEqual({ checked: true, partialChecked: true }); - expect(output.selectedKeys["sub1"]).toEqual({ checked: true, partialChecked: false }); - expect(output.selectedKeys["sub2"]).toEqual({ checked: true, partialChecked: false }); + + expect(output.selectedKeys.ch1).toEqual({ checked: true, partialChecked: true }); + expect(output.selectedKeys.sub1).toEqual({ checked: true, partialChecked: false }); + expect(output.selectedKeys.sub2).toEqual({ checked: true, partialChecked: false }); }); it("returns chapter not checked when no children are assigned", () => { @@ -261,7 +271,8 @@ describe("useReadingsSelector", () => { const { result } = renderHook(() => useReadingsSelector(), { wrapper: wrapper(store) }); const output = result.current as any; - expect(output.selectedKeys["ch2"]).toEqual({ checked: false, partialChecked: false }); + + expect(output.selectedKeys.ch2).toEqual({ checked: false, partialChecked: false }); }); it("skips query when selectedAssignmentId is null", () => { @@ -273,6 +284,7 @@ describe("useReadingsSelector", () => { } as any); const store = buildStore(null, [], []); + renderHook(() => useReadingsSelector(), { wrapper: wrapper(store) }); expect(useGetExercisesQuery).toHaveBeenCalledWith(0, { @@ -290,6 +302,7 @@ describe("useReadingsSelector", () => { } as any); const store = buildStore(42, [], []); + renderHook(() => useReadingsSelector(), { wrapper: wrapper(store) }); expect(useGetExercisesQuery).toHaveBeenCalledWith(42, { @@ -365,6 +378,7 @@ describe("useReadingsSelector", () => { const { result } = renderHook(() => useReadingsSelector(), { wrapper: wrapper(store) }); const output = result.current as any; + expect(output.readingExercises[0].sorting_priority).toBe(1); expect(output.readingExercises[1].sorting_priority).toBe(2); }); diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/hooks/useSelectedAssignment.spec.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/hooks/useSelectedAssignment.spec.ts index d62e9cf21..e52ecb300 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/hooks/useSelectedAssignment.spec.ts +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/hooks/useSelectedAssignment.spec.ts @@ -1,10 +1,14 @@ import { configureStore } from "@reduxjs/toolkit"; +import { assignmentSlice, assignmentActions } from "@store/assignment/assignment.logic"; +import { + assignmentApi, + useGetAssignmentQuery, + useUpdateAssignmentMutation +} from "@store/assignment/assignment.logic.api"; import { renderHook, act } from "@testing-library/react"; import React from "react"; import { Provider } from "react-redux"; -import { assignmentSlice, assignmentActions } from "@store/assignment/assignment.logic"; -import { assignmentApi } from "@store/assignment/assignment.logic.api"; import { useSelectedAssignment } from "./useSelectedAssignment"; vi.mock("@store/assignment/assignment.logic.api", () => ({ @@ -17,11 +21,6 @@ vi.mock("@store/assignment/assignment.logic.api", () => ({ useUpdateAssignmentMutation: vi.fn() })); -import { - useGetAssignmentQuery, - useUpdateAssignmentMutation -} from "@store/assignment/assignment.logic.api"; - const mockPutAssignment = vi.fn(); function makeStore(selectedAssignmentId: number | null = null) { @@ -55,6 +54,7 @@ describe("useSelectedAssignment", () => { describe("selectedAssignment", () => { it("returns undefined when no assignment ID is selected", () => { const store = makeStore(null); + vi.mocked(useGetAssignmentQuery).mockReturnValue({ data: undefined } as any); const { result } = renderHook(() => useSelectedAssignment(), { @@ -74,6 +74,7 @@ describe("useSelectedAssignment", () => { } as any; const store = makeStore(42); + vi.mocked(useGetAssignmentQuery).mockReturnValue({ data: mockAssignment } as any); const { result } = renderHook(() => useSelectedAssignment(), { @@ -124,6 +125,7 @@ describe("useSelectedAssignment", () => { it("does not call putAssignment immediately when updateAssignment is called", () => { const store = makeStore(1); const mockAssignment = { id: 1, name: "Original" } as any; + vi.mocked(useGetAssignmentQuery).mockReturnValue({ data: mockAssignment } as any); const { result } = renderHook(() => useSelectedAssignment(), { @@ -142,6 +144,7 @@ describe("useSelectedAssignment", () => { const store = makeStore(1); const mockAssignment = { id: 1, name: "Original", points: 5 } as any; + vi.mocked(useGetAssignmentQuery).mockReturnValue({ data: mockAssignment } as any); const { result } = renderHook(() => useSelectedAssignment(), { @@ -173,6 +176,7 @@ describe("useSelectedAssignment", () => { const store = makeStore(1); const mockAssignment = { id: 1, name: "Original", points: 5 } as any; + vi.mocked(useGetAssignmentQuery).mockReturnValue({ data: mockAssignment } as any); const { result } = renderHook(() => useSelectedAssignment(), { @@ -213,6 +217,7 @@ describe("useSelectedAssignment", () => { vi.useFakeTimers(); const store = makeStore(null); + vi.mocked(useGetAssignmentQuery).mockReturnValue({ data: undefined } as any); const { result } = renderHook(() => useSelectedAssignment(), { @@ -236,6 +241,7 @@ describe("useSelectedAssignment", () => { vi.useFakeTimers(); const store = makeStore(1); + vi.mocked(useGetAssignmentQuery).mockReturnValue({ data: undefined } as any); const { result } = renderHook(() => useSelectedAssignment(), { diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/hooks/useSmartExerciseSearch.spec.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/hooks/useSmartExerciseSearch.spec.ts index 798b5c491..921d1c5a4 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/hooks/useSmartExerciseSearch.spec.ts +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/hooks/useSmartExerciseSearch.spec.ts @@ -1,8 +1,10 @@ +import { setupStore } from "@store/store"; import { renderHook, act } from "@testing-library/react"; import React from "react"; import { Provider } from "react-redux"; + import { FilterMatchMode } from "@/types/filterMatchMode"; -import { setupStore } from "@store/store"; + import { useSmartExerciseSearch } from "./useSmartExerciseSearch"; const mockQueryResult: { @@ -32,6 +34,7 @@ vi.mock("@store/exercises/exercises.logic.api", () => ({ vi.mock("@store/assignment/assignment.logic", async (importOriginal) => { const actual = await importOriginal(); + return { ...actual, assignmentSelectors: { @@ -110,6 +113,7 @@ describe("useSmartExerciseSearch", () => { const { result } = renderHook(() => useSmartExerciseSearch(), { wrapper }); const globalFilter = result.current.filters.global as { value: unknown; matchMode: string }; + expect(globalFilter.value).toBeNull(); expect(globalFilter.matchMode).toBe(FilterMatchMode.CONTAINS); }); @@ -126,6 +130,7 @@ describe("useSmartExerciseSearch", () => { }); const nameFilter = result.current.filters.name as { value: unknown }; + expect(nameFilter.value).toBe("test"); }); @@ -152,6 +157,7 @@ describe("useSmartExerciseSearch", () => { }); const globalFilter = result.current.filters.global as { value: unknown; matchMode: string }; + expect(globalFilter.value).toBe("search term"); expect(globalFilter.matchMode).toBe(FilterMatchMode.CONTAINS); }); @@ -277,6 +283,7 @@ describe("useSmartExerciseSearch", () => { it("returns error when isError is true", () => { const fakeError = new Error("fetch failed"); + mockQueryResult.isError = true; mockQueryResult.error = fakeError; diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/hooks/useTableDropdownOptions.spec.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/hooks/useTableDropdownOptions.spec.ts index a4bda018b..c56ca3c2a 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/hooks/useTableDropdownOptions.spec.ts +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/hooks/useTableDropdownOptions.spec.ts @@ -1,9 +1,11 @@ -import { renderHook } from "@testing-library/react"; import { configureStore } from "@reduxjs/toolkit"; -import { Provider } from "react-redux"; -import React from "react"; import { datasetSlice } from "@store/dataset/dataset.logic"; +import { renderHook } from "@testing-library/react"; +import React from "react"; +import { Provider } from "react-redux"; + import { TableDropdownOption } from "@/types/dataset"; + import { useTableDropdownOptions } from "./useTableDropdownOptions"; function buildOption(value: string, types: string[]): TableDropdownOption { diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/hooks/useUpdateAssignmentExercise.spec.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/hooks/useUpdateAssignmentExercise.spec.ts index cbde905aa..ba3e7f7e8 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/hooks/useUpdateAssignmentExercise.spec.ts +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/hooks/useUpdateAssignmentExercise.spec.ts @@ -1,3 +1,4 @@ +import { notify } from "@components/ui/notify"; import { renderHook, act } from "@testing-library/react"; import { vi, beforeEach } from "vitest"; @@ -27,8 +28,6 @@ vi.mock("@/hooks/useSelectedAssignment", () => ({ useSelectedAssignment: () => ({ selectedAssignment: mockSelectedAssignment }) })); -import { notify } from "@components/ui/notify"; - import { getExercisesUpdateMessage, useUpdateAssignmentExercise @@ -58,11 +57,13 @@ describe("useUpdateAssignmentExercise", () => { it("returns updateAssignmentExercises function", () => { const { result } = renderHook(() => useUpdateAssignmentExercise()); + expect(typeof result.current.updateAssignmentExercises).toBe("function"); }); it("exposes the mutation loading state as isUpdating", () => { const { result } = renderHook(() => useUpdateAssignmentExercise()); + expect(result.current.isUpdating).toBe(true); }); @@ -222,6 +223,7 @@ describe("useUpdateAssignmentExercise", () => { }); const message = vi.mocked(notify.success).mock.calls[0][0]; + expect(message).toBe("Added 1, removed 2 exercises"); expect(message).not.toContain("\n"); }); @@ -243,10 +245,12 @@ describe("useUpdateAssignmentExercise", () => { it("returns the mutation response on success", async () => { const mutationResponse = { data: { updated: true } }; + mockUpdateAssignmentExercisesPut.mockResolvedValue(mutationResponse); const { result } = renderHook(() => useUpdateAssignmentExercise()); let returnValue: any; + await act(async () => { returnValue = await result.current.updateAssignmentExercises({ isReading: false, @@ -263,6 +267,7 @@ describe("useUpdateAssignmentExercise", () => { const { result } = renderHook(() => useUpdateAssignmentExercise()); let returnValue: any; + await act(async () => { returnValue = await result.current.updateAssignmentExercises({ isReading: false, diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/hooks/useUpdateExercises.spec.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/hooks/useUpdateExercises.spec.ts index 9ec2abd62..0de9bc867 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/hooks/useUpdateExercises.spec.ts +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/hooks/useUpdateExercises.spec.ts @@ -1,8 +1,11 @@ -import { renderHook, act } from "@testing-library/react"; -import { Provider } from "react-redux"; +import { notify } from "@components/ui/notify"; import { configureStore } from "@reduxjs/toolkit"; -import { createElement } from "react"; import { assignmentExerciseSlice } from "@store/assignmentExercise/assignmentExercise.logic"; +import { useUpdateAssignmentQuestionsMutation } from "@store/assignmentExercise/assignmentExercise.logic.api"; +import { renderHook, act } from "@testing-library/react"; +import { createElement } from "react"; +import { Provider } from "react-redux"; + import { useUpdateExercises } from "./useUpdateExercises"; vi.mock("@store/assignmentExercise/assignmentExercise.logic.api", () => ({ @@ -21,9 +24,6 @@ vi.mock("@components/ui/notify", () => ({ } })); -import { useUpdateAssignmentQuestionsMutation } from "@store/assignmentExercise/assignmentExercise.logic.api"; -import { notify } from "@components/ui/notify"; - const mockExercise = { id: 1, assignment_id: 10, @@ -140,6 +140,7 @@ describe("useUpdateExercises", () => { expect(mockUpdateExercises).toHaveBeenCalledOnce(); const calledWith = mockUpdateExercises.mock.calls[0][0]; + expect(calledWith).toHaveLength(1); expect(calledWith[0].points).toBe(20); expect(calledWith[0].id).toBe(1); @@ -160,6 +161,7 @@ describe("useUpdateExercises", () => { }); const calledWith = mockUpdateExercises.mock.calls[0][0]; + expect(typeof calledWith[0].question_json).toBe("string"); expect(calledWith[0].question_json).toBe(JSON.stringify({ statement: "Parsed object" })); }); @@ -203,6 +205,7 @@ describe("useUpdateExercises", () => { }); const calledWith = mockUpdateExercises.mock.calls[0][0]; + expect(calledWith[0].autograde).toBe("pct_correct"); }); @@ -217,6 +220,7 @@ describe("useUpdateExercises", () => { }); const calledWith = mockUpdateExercises.mock.calls[0][0]; + expect(calledWith[0].which_to_grade).toBe("first_answer"); }); @@ -231,6 +235,7 @@ describe("useUpdateExercises", () => { }); const calledWith = mockUpdateExercises.mock.calls[0][0]; + expect(calledWith[0].activities_required).toBe(3); }); @@ -246,6 +251,7 @@ describe("useUpdateExercises", () => { const calledWith = mockUpdateExercises.mock.calls[0][0]; const updated = calledWith[0]; + expect(updated.id).toBe(mockExercise.id); expect(updated.assignment_id).toBe(mockExercise.assignment_id); expect(updated.name).toBe(mockExercise.name); diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/hooks/useUpdateReadings.spec.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/hooks/useUpdateReadings.spec.ts index 08f982cfa..c4bf548e7 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/hooks/useUpdateReadings.spec.ts +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/hooks/useUpdateReadings.spec.ts @@ -1,3 +1,5 @@ +import { notify } from "@components/ui/notify"; +import { useUpdateAssignmentQuestionsMutation } from "@store/assignmentExercise/assignmentExercise.logic.api"; import { renderHook, act } from "@testing-library/react"; import { vi } from "vitest"; @@ -21,12 +23,11 @@ vi.mock("@/hooks/useReadingsSelector", () => ({ useReadingsSelector: vi.fn() })); -import { notify } from "@components/ui/notify"; -import { useUpdateAssignmentQuestionsMutation } from "@store/assignmentExercise/assignmentExercise.logic.api"; import { useReadingsSelector } from "@/hooks/useReadingsSelector"; -import { useUpdateReadings } from "./useUpdateReadings"; import type { Exercise } from "@/types/exercises"; +import { useUpdateReadings } from "./useUpdateReadings"; + const makeExercise = (overrides: Partial = {}): Exercise => ({ id: 1, assignment_id: 10, @@ -82,6 +83,7 @@ describe("useUpdateReadings", () => { it("returns handleChange function", () => { const { result } = renderHook(() => useUpdateReadings()); + expect(typeof result.current.handleChange).toBe("function"); }); @@ -102,6 +104,7 @@ describe("useUpdateReadings", () => { it("does nothing when new value equals current value", async () => { const exercise = makeExercise({ id: 1, points: 5 }); + (useReadingsSelector as ReturnType).mockReturnValue({ readingExercises: [exercise] }); @@ -119,6 +122,7 @@ describe("useUpdateReadings", () => { it("calls updateExercises with merged exercise when value changes", async () => { const exercise = makeExercise({ id: 1, points: 5 }); + (useReadingsSelector as ReturnType).mockReturnValue({ readingExercises: [exercise] }); @@ -131,6 +135,7 @@ describe("useUpdateReadings", () => { expect(updateExercises).toHaveBeenCalledOnce(); const [calledWith] = updateExercises.mock.calls[0]; + expect(calledWith).toHaveLength(1); expect(calledWith[0]).toMatchObject({ id: 1, points: 10 }); }); @@ -142,6 +147,7 @@ describe("useUpdateReadings", () => { points: 3, question_json: parsedJson as unknown as string }); + (useReadingsSelector as ReturnType).mockReturnValue({ readingExercises: [exercise] }); @@ -153,12 +159,14 @@ describe("useUpdateReadings", () => { }); const [calledWith] = updateExercises.mock.calls[0]; + expect(calledWith[0].question_json).toBe(JSON.stringify(parsedJson)); }); it("shows success toast after successful update", async () => { updateExercises.mockResolvedValue({ error: undefined }); const exercise = makeExercise({ id: 1, points: 3 }); + (useReadingsSelector as ReturnType).mockReturnValue({ readingExercises: [exercise] }); @@ -175,6 +183,7 @@ describe("useUpdateReadings", () => { it("shows error toast when updateExercises returns an error", async () => { updateExercises.mockResolvedValue({ error: new Error("network error") }); const exercise = makeExercise({ id: 1, points: 3 }); + (useReadingsSelector as ReturnType).mockReturnValue({ readingExercises: [exercise] }); @@ -190,6 +199,7 @@ describe("useUpdateReadings", () => { it("updates autograde field correctly", async () => { const exercise = makeExercise({ id: 1, autograde: "all_or_nothing" }); + (useReadingsSelector as ReturnType).mockReturnValue({ readingExercises: [exercise] }); @@ -201,11 +211,13 @@ describe("useUpdateReadings", () => { }); const [calledWith] = updateExercises.mock.calls[0]; + expect(calledWith[0].autograde).toBe("pct_correct"); }); it("updates activities_required field correctly", async () => { const exercise = makeExercise({ id: 1, activities_required: 1 }); + (useReadingsSelector as ReturnType).mockReturnValue({ readingExercises: [exercise] }); @@ -217,6 +229,7 @@ describe("useUpdateReadings", () => { }); const [calledWith] = updateExercises.mock.calls[0]; + expect(calledWith[0].activities_required).toBe(3); }); @@ -248,6 +261,7 @@ describe("useUpdateReadings", () => { it("finds the correct reading among multiple exercises", async () => { const exercises = [makeExercise({ id: 1, points: 1 }), makeExercise({ id: 2, points: 2 })]; + (useReadingsSelector as ReturnType).mockReturnValue({ readingExercises: exercises }); @@ -259,6 +273,7 @@ describe("useUpdateReadings", () => { }); const [calledWith] = updateExercises.mock.calls[0]; + expect(calledWith[0].id).toBe(2); expect(calledWith[0].points).toBe(9); }); diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/navUtils.spec.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/navUtils.spec.ts index 1cf4e264c..b44b49297 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/navUtils.spec.ts +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/navUtils.spec.ts @@ -27,6 +27,7 @@ describe("buildNavBar", () => { originalLocationHref = Object.getOwnPropertyDescriptor(window, "location"); const locationValue: Record = {}; + Object.defineProperty(locationValue, "href", { configurable: true, get() { @@ -65,6 +66,7 @@ describe("buildNavBar", () => { describe("menu structure", () => { it("returns an array of menu items", () => { const items = buildNavBar(makeConfig()); + expect(Array.isArray(items)).toBe(true); expect(items.length).toBeGreaterThan(0); }); @@ -72,6 +74,7 @@ describe("buildNavBar", () => { it("includes Home, Dashboard, Grader, Assignment Builder, User, and Help labels", () => { const items = buildNavBar(makeConfig()); const topLabels = items.map((i) => i.label); + expect(topLabels).toContain("Home"); expect(topLabels).toContain("Dashboard"); expect(topLabels).toContain("Grader"); @@ -83,18 +86,21 @@ describe("buildNavBar", () => { it("includes a Back to Course item using the course name from config", () => { const items = buildNavBar(makeConfig({ course: "my-course" })); const backItem = items.find((i) => i.label?.includes("my-course")); + expect(backItem).toBeDefined(); }); it("uses fallback course label when course is empty string", () => { const items = buildNavBar(makeConfig({ course: "" })); const backItem = items.find((i) => i.label?.includes("Course")); + expect(backItem).toBeDefined(); }); it("uses default eBookConfig values when an empty object is provided", () => { const items = buildNavBar({} as EBookConfig); const backItem = items.find((i) => i.label?.includes("dev mode")); + expect(backItem).toBeDefined(); }); }); @@ -105,6 +111,7 @@ describe("buildNavBar", () => { const userMenu = items.find((i) => i.label === "User"); const subItems = (userMenu?.items as Array<{ label?: string }>) ?? []; const welcome = subItems.find((s) => s.label?.startsWith("Welcome")); + expect(welcome?.label).toBe("Welcome alice"); }); @@ -113,6 +120,7 @@ describe("buildNavBar", () => { const userMenu = items.find((i) => i.label === "User"); const subItems = (userMenu?.items as Array<{ label?: string }>) ?? []; const welcome = subItems.find((s) => s.label?.startsWith("Welcome")); + expect(welcome?.label).toBe("Welcome User"); }); @@ -121,6 +129,7 @@ describe("buildNavBar", () => { const userMenu = items.find((i) => i.label === "User"); const subItems = (userMenu?.items as Array<{ label?: string; visible?: boolean }>) ?? []; const peerInstructor = subItems.find((s) => s.label === "Peer instruction (instructor)"); + expect(peerInstructor?.visible).toBe(true); }); @@ -129,6 +138,7 @@ describe("buildNavBar", () => { const userMenu = items.find((i) => i.label === "User"); const subItems = (userMenu?.items as Array<{ label?: string; visible?: boolean }>) ?? []; const peerInstructor = subItems.find((s) => s.label === "Peer instruction (instructor)"); + expect(peerInstructor?.visible).toBe(false); }); }); @@ -139,6 +149,7 @@ describe("buildNavBar", () => { const helpMenu = items.find((i) => i.label === "Help"); const subItems = (helpMenu?.items as Array<{ label?: string; command?: () => void }>) ?? []; const guide = subItems.find((s) => s.label === "Instructor's guide"); + guide?.command?.(); expect(openSpy).toHaveBeenCalledWith("https://guide.runestone.academy/", "_blank"); }); @@ -148,6 +159,7 @@ describe("buildNavBar", () => { const helpMenu = items.find((i) => i.label === "Help"); const subItems = (helpMenu?.items as Array<{ label?: string; command?: () => void }>) ?? []; const discord = subItems.find((s) => s.label === "Join our Discord"); + discord?.command?.(); expect(openSpy).toHaveBeenCalledWith("https://discord.gg/f3Qmbk9P3U", "_blank"); }); @@ -157,6 +169,7 @@ describe("buildNavBar", () => { it("sets window.location.href for /runestone paths", () => { const items = buildNavBar(makeConfig()); const home = items.find((i) => i.label === "Home"); + home?.command?.(); expect(hrefSpy).toHaveBeenCalledWith("/runestone/default/index"); }); @@ -164,6 +177,7 @@ describe("buildNavBar", () => { it("sets window.location.href for /ns paths", () => { const items = buildNavBar(makeConfig({ course: "cs101" })); const back = items.find((i) => i.label?.startsWith("Back to")); + back?.command?.(); expect(hrefSpy).toHaveBeenCalledWith("/ns/books/published/cs101/index.html"); }); @@ -173,6 +187,7 @@ describe("buildNavBar", () => { const userMenu = items.find((i) => i.label === "User"); const subItems = (userMenu?.items as Array<{ label?: string; command?: () => void }>) ?? []; const assignments = subItems.find((s) => s.label === "Assignments"); + assignments?.command?.(); expect(hrefSpy).toHaveBeenCalledWith("/assignment/student/chooseAssignment"); }); @@ -180,6 +195,7 @@ describe("buildNavBar", () => { it("sets window.location.href for /admin paths", () => { const items = buildNavBar(makeConfig()); const dashboard = items.find((i) => i.label === "Dashboard"); + dashboard?.command?.(); expect(hrefSpy).toHaveBeenCalledWith("/admin/instructor/menu"); }); @@ -189,6 +205,7 @@ describe("buildNavBar", () => { it("sets window.location.hash for relative paths when no navigate fn is provided", () => { const items = buildNavBar(makeConfig()); const grader = items.find((i) => i.label === "Grader"); + grader?.command?.(); expect(hashSpy).toHaveBeenCalledWith("#/grader"); }); @@ -196,6 +213,7 @@ describe("buildNavBar", () => { it("sets window.location.hash for builder path", () => { const items = buildNavBar(makeConfig()); const builder = items.find((i) => i.label === "Assignment Builder"); + builder?.command?.(); expect(hashSpy).toHaveBeenCalledWith("#/builder"); }); @@ -206,6 +224,7 @@ describe("buildNavBar", () => { const navigate = vi.fn(); const items = buildNavBar(makeConfig(), navigate); const grader = items.find((i) => i.label === "Grader"); + grader?.command?.(); expect(navigate).toHaveBeenCalledWith("/grader"); }); @@ -214,6 +233,7 @@ describe("buildNavBar", () => { const navigate = vi.fn(); const items = buildNavBar(makeConfig(), navigate); const builder = items.find((i) => i.label === "Assignment Builder"); + builder?.command?.(); expect(navigate).toHaveBeenCalledWith("/builder"); }); @@ -222,6 +242,7 @@ describe("buildNavBar", () => { const navigate = vi.fn(); const items = buildNavBar(makeConfig(), navigate); const home = items.find((i) => i.label === "Home"); + home?.command?.(); expect(hrefSpy).toHaveBeenCalledWith("/runestone/default/index"); expect(navigate).not.toHaveBeenCalled(); @@ -233,6 +254,7 @@ describe("buildNavBar", () => { const helpMenu = items.find((i) => i.label === "Help"); const subItems = (helpMenu?.items as Array<{ label?: string; command?: () => void }>) ?? []; const guide = subItems.find((s) => s.label === "Instructor's guide"); + guide?.command?.(); expect(openSpy).toHaveBeenCalledWith("https://guide.runestone.academy/", "_blank"); expect(navigate).not.toHaveBeenCalled(); diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/renderers/activeCode.jsx b/bases/rsptx/assignment_server_api/assignment_builder/src/renderers/activeCode.jsx index c573864f5..3737a0b42 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/renderers/activeCode.jsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/renderers/activeCode.jsx @@ -8,18 +8,18 @@ import { selectLanguage, selectStarterCode, selectPrefixCode, - selectSuffixCode, + selectSuffixCode } from "../state/activecode/acSlice"; import { selectUniqueId, setQuestionJson, - setPreviewSrc, + setPreviewSrc } from "../state/interactive/interactiveSlice"; import { setCode } from "../state/preview/previewSlice"; const acStyle = { border: "1px solid black", - padding: "10px", + padding: "10px" }; /** * @@ -46,7 +46,7 @@ function ActiveCodeCreator() { language, prefix_code, starter_code, - suffix_code, + suffix_code ); dispatch(setCode(code)); @@ -61,7 +61,7 @@ function ActiveCodeCreator() { { value: "c", label: "C" }, { value: "javascript", label: "Javascript" }, { value: "html", label: "HTML" }, - { value: "sql", label: "SQL" }, + { value: "sql", label: "SQL" } ]; return ( @@ -126,8 +126,8 @@ function ActiveCodeCreator() { dispatch( updateField({ field: "suffix_code", - newVal: e.target.value, - }), + newVal: e.target.value + }) ); }} > diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/renderers/assignmentPicker.jsx b/bases/rsptx/assignment_server_api/assignment_builder/src/renderers/assignmentPicker.jsx index 58f242f8d..19777eebb 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/renderers/assignmentPicker.jsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/renderers/assignmentPicker.jsx @@ -19,7 +19,7 @@ import { setPoints, setFromSource, setReleased, - fetchAssignmentQuestions, + fetchAssignmentQuestions } from "../state/assignment/assignSlice"; /** @@ -44,19 +44,19 @@ export function AssignmentPicker() { const menuStyle = { width: "25rem", - marginBottom: "10px", + marginBottom: "10px" }; const optionStyle = { width: "12rem", marginRight: "10px", textAlign: "left", - float: "left", + float: "left" }; const optionStyle2 = { width: "12rem", - textAlign: "end", + textAlign: "end" }; const renderAssignmentOption = ({ option }) => { diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/renderers/editorModeChooser.jsx b/bases/rsptx/assignment_server_api/assignment_builder/src/renderers/editorModeChooser.jsx index 1bc4c514e..4dac005e9 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/renderers/editorModeChooser.jsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/renderers/editorModeChooser.jsx @@ -5,7 +5,7 @@ import { useSelector, useDispatch } from "react-redux"; import { setComponent, selectComponent, - selectComponentOptions, + selectComponentOptions } from "../state/componentEditor/editorSlice"; import { setQuestionType } from "../state/interactive/interactiveSlice"; @@ -41,7 +41,7 @@ export function EditorChooser() { export const kindMap = { activecode: ActiveCodeCreator, multiplechoice: MultipleChoiceCreator, - shortanswer: ShortAnswerCreator, + shortanswer: ShortAnswerCreator }; /** @@ -70,9 +70,9 @@ export function EditorContainer(props) { EditorContainer.propTypes = { componentName: PropTypes.string, - editonly: PropTypes.bool, + editonly: PropTypes.bool }; EditorContainer.defaultProps = { componentName: null, - editonly: false, + editonly: false }; diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/renderers/preview.jsx b/bases/rsptx/assignment_server_api/assignment_builder/src/renderers/preview.jsx index 8840e8c8c..aef3a2ead 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/renderers/preview.jsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/renderers/preview.jsx @@ -43,6 +43,6 @@ export function Preview(props) { Preview.propTypes = { code: PropTypes.string, - exercise: PropTypes.object, + exercise: PropTypes.object }; export default Preview; diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/renderers/shortAnswer.jsx b/bases/rsptx/assignment_server_api/assignment_builder/src/renderers/shortAnswer.jsx index 6d430e111..57a13a10c 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/renderers/shortAnswer.jsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/renderers/shortAnswer.jsx @@ -5,14 +5,14 @@ import { createShortAnswerTemplate } from "../componentFuncs"; import { setQuestionJson, selectUniqueId, - setPreviewSrc, + setPreviewSrc } from "../state/interactive/interactiveSlice"; import { setCode } from "../state/preview/previewSlice"; import { setStatement, setAttachment, selectAttachment, - selectStatement, + selectStatement } from "../state/shortanswer/shortSlice"; export function ShortAnswerCreator() { diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/service/NodeService.jsx b/bases/rsptx/assignment_server_api/assignment_builder/src/service/NodeService.jsx index c1c0a0e75..537081c3d 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/service/NodeService.jsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/service/NodeService.jsx @@ -11,13 +11,13 @@ export const NodeService = { { key: "chapter1", data: { - name: "Getting Started", + name: "Getting Started" }, children: [ { key: "subchap1", data: { - name: "Pretest for the CSA Exam", + name: "Pretest for the CSA Exam" }, children: [ { @@ -26,37 +26,37 @@ export const NodeService = { name: "Exercise 1.2.1", question_type: "Multiple Choice", autograde: "Yes", - question_json: "", - }, - }, - ], - }, - ], + question_json: "" + } + } + ] + } + ] }, { key: "1", data: { name: "Preview 2.1.1", question_type: "Parsons", - autograde: "Yes", - }, + autograde: "Yes" + } }, { key: "2", data: { name: "Exercise 3.1.1", question_type: "Active Code", - autograde: "No", - }, + autograde: "No" + } }, { key: "3", data: { name: "Exercise 3.2.1", question_type: "Active Code", - autograde: "Yes", - }, - }, + autograde: "Yes" + } + } // { // key: '4', // data: { @@ -248,5 +248,5 @@ export const NodeService = { getTreeNodes() { return Promise.resolve(this.getTreeNodesData()); - }, + } }; diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/state/assignment/assignSlice.js b/bases/rsptx/assignment_server_api/assignment_builder/src/state/assignment/assignSlice.js index 46e179ccf..70993423d 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/state/assignment/assignSlice.js +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/state/assignment/assignSlice.js @@ -18,8 +18,8 @@ * - `searchForQuestions` is used to search for questions in the question bank * @memberof AssignmentEditor */ -import { createSlice, createAsyncThunk } from "@reduxjs/toolkit"; import { notify } from "@components/ui/notify"; +import { createSlice, createAsyncThunk } from "@reduxjs/toolkit"; import { setSelectedNodes } from "../epicker/ePickerSlice"; diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/state/epicker/ePickerSlice.js b/bases/rsptx/assignment_server_api/assignment_builder/src/state/epicker/ePickerSlice.js index 60d6bbee6..6c3ddb533 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/state/epicker/ePickerSlice.js +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/state/epicker/ePickerSlice.js @@ -3,8 +3,8 @@ * @file ePickerSlice.js * @summary Redux slice for the ePicker component */ -import { createAsyncThunk, createSlice, createSelector } from "@reduxjs/toolkit"; import { notify } from "@components/ui/notify"; +import { createAsyncThunk, createSlice, createSelector } from "@reduxjs/toolkit"; /** * @function fetchChooserData diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/state/student/studentSlice.js b/bases/rsptx/assignment_server_api/assignment_builder/src/state/student/studentSlice.js index e6bdb1991..a9994c6c7 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/state/student/studentSlice.js +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/state/student/studentSlice.js @@ -1,5 +1,5 @@ -import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"; import { notify } from "@components/ui/notify"; +import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"; export const fetchClassRoster = createAsyncThunk( "student/fetchClassRoster", @@ -97,6 +97,7 @@ export const deleteAccommodations = createAsyncThunk( headers: jsheaders, method: "DELETE" }; + for (let id of idList) { let resp = await fetch(`/assignment/instructor/accommodation/${id}`, data); diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/store/assignment/assignment.logic.api.spec.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/store/assignment/assignment.logic.api.spec.ts index c212e4e10..9c1691e24 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/store/assignment/assignment.logic.api.spec.ts +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/store/assignment/assignment.logic.api.spec.ts @@ -1,21 +1,16 @@ +import type { DetailResponse } from "@/types/api"; +import type { Assignment, GetAssignmentResponse, GetAssignmentsResponse } from "@/types/assignment"; + import { ASSIGNMENT_TOAST_COPY, assignmentApi, - useGetAssignmentsQuery, - useGetAssignmentQuery, - useUpdateAssignmentMutation, useCreateAssignmentMutation, - useRemoveAssignmentMutation, useDuplicateAssignmentMutation, - useBulkUpdateAssignmentsMutation, - useBulkRemoveAssignmentsMutation + useGetAssignmentQuery, + useGetAssignmentsQuery, + useRemoveAssignmentMutation, + useUpdateAssignmentMutation } from "./assignment.logic.api"; -import type { Assignment } from "@/types/assignment"; -import type { DetailResponse } from "@/types/api"; -import type { GetAssignmentsResponse, GetAssignmentResponse } from "@/types/assignment"; -import { notify } from "@/components/ui/notify"; -import { baseQuery } from "../baseQuery"; -import { configureStore } from "@reduxjs/toolkit"; vi.mock("@components/ui/notify", () => ({ notify: { @@ -142,6 +137,7 @@ describe("getAssignments transformResponse", () => { }; const transformed = response.detail.assignments; + expect(transformed).toHaveLength(2); expect(transformed[0].id).toBe(1); expect(transformed[1].id).toBe(2); @@ -156,6 +152,7 @@ describe("getAssignment transformResponse", () => { }; const result = response.detail.assignment; + expect(result.id).toBe(42); expect(result.name).toBe("Test Assignment"); }); @@ -248,6 +245,7 @@ describe("getAssignments query builder", () => { it("builds correct GET request for assignments list", () => { const queryFn = () => ({ method: "GET", url: "/assignment/instructor/assignments" }); const result = queryFn(); + expect(result.method).toBe("GET"); expect(result.url).toBe("/assignment/instructor/assignments"); }); @@ -259,6 +257,7 @@ describe("getAssignment query builder", () => { method: "GET", url: `/assignment/instructor/assignments/${id}` }); + expect(queryFn(7)).toEqual({ method: "GET", url: "/assignment/instructor/assignments/7" }); }); }); @@ -272,6 +271,7 @@ describe("updateAssignment query builder", () => { body }); const result = queryFn(assignment); + expect(result.method).toBe("PUT"); expect(result.url).toBe("/assignment/instructor/assignments/3"); expect(result.body).toBe(assignment); @@ -286,6 +286,7 @@ describe("removeAssignment query builder", () => { url: `/assignment/instructor/assignments/${body.id}` }); const result = queryFn(assignment); + expect(result.method).toBe("DELETE"); expect(result.url).toBe("/assignment/instructor/assignments/9"); }); @@ -298,98 +299,8 @@ describe("duplicateAssignment query builder", () => { url: `/assignment/instructor/assignments/${assignmentId}/duplicate` }); const result = queryFn(15); + expect(result.method).toBe("POST"); expect(result.url).toBe("/assignment/instructor/assignments/15/duplicate"); }); }); - -describe("bulk mutations", () => { - const makeStore = () => - configureStore({ - reducer: { [assignmentApi.reducerPath]: assignmentApi.reducer }, - middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(assignmentApi.middleware) - }); - - const mockedBaseQuery = vi.mocked(baseQuery); - - beforeEach(() => { - vi.clearAllMocks(); - }); - - it("exports the bulk hooks", () => { - expect(typeof useBulkUpdateAssignmentsMutation).toBe("function"); - expect(typeof useBulkRemoveAssignmentsMutation).toBe("function"); - expect(assignmentApi.endpoints.bulkUpdateAssignments).toBeDefined(); - expect(assignmentApi.endpoints.bulkRemoveAssignments).toBeDefined(); - }); - - it("bulkUpdateAssignments PUTs each assignment and counts successes and failures", async () => { - mockedBaseQuery - .mockResolvedValueOnce({ data: {} }) - .mockResolvedValueOnce({ error: { status: 500, data: {} } }); - - const store = makeStore(); - const result = await store.dispatch( - assignmentApi.endpoints.bulkUpdateAssignments.initiate([ - makeAssignment({ id: 1 }), - makeAssignment({ id: 2 }) - ]) - ); - - expect(mockedBaseQuery).toHaveBeenCalledTimes(2); - expect(mockedBaseQuery.mock.calls[0][0]).toMatchObject({ - method: "PUT", - url: "/assignment/instructor/assignments/1" - }); - expect(mockedBaseQuery.mock.calls[1][0]).toMatchObject({ - method: "PUT", - url: "/assignment/instructor/assignments/2" - }); - expect("data" in result && result.data).toEqual({ succeeded: 1, failed: 1 }); - }); - - it("bulkRemoveAssignments DELETEs each assignment and toasts a summary", async () => { - mockedBaseQuery.mockResolvedValue({ data: {} }); - - const store = makeStore(); - const result = await store.dispatch( - assignmentApi.endpoints.bulkRemoveAssignments.initiate([ - makeAssignment({ id: 3 }), - makeAssignment({ id: 4 }) - ]) - ); - - expect(mockedBaseQuery).toHaveBeenCalledTimes(2); - expect(mockedBaseQuery.mock.calls[0][0]).toMatchObject({ - method: "DELETE", - url: "/assignment/instructor/assignments/3" - }); - expect("data" in result && result.data).toEqual({ succeeded: 2, failed: 0 }); - expect(notify.success).toHaveBeenCalledWith("Deleted 2 assignments"); - expect(notify.error).not.toHaveBeenCalled(); - }); - - it("bulkRemoveAssignments toasts the error copy for failed deletions", async () => { - mockedBaseQuery.mockResolvedValue({ error: { status: 500, data: {} } }); - - const store = makeStore(); - const result = await store.dispatch( - assignmentApi.endpoints.bulkRemoveAssignments.initiate([makeAssignment({ id: 5 })]) - ); - - expect("data" in result && result.data).toEqual({ succeeded: 0, failed: 1 }); - expect(notify.success).not.toHaveBeenCalled(); - expect(notify.error).toHaveBeenCalledWith("Couldn't delete 1 assignment. Try again."); - }); - - it("phrases the bulk delete copy with singular and plural subjects", () => { - expect(ASSIGNMENT_TOAST_COPY.bulkDeleted(1)).toBe("Deleted 1 assignment"); - expect(ASSIGNMENT_TOAST_COPY.bulkDeleted(3)).toBe("Deleted 3 assignments"); - expect(ASSIGNMENT_TOAST_COPY.bulkDeleteError(1)).toBe( - "Couldn't delete 1 assignment. Try again." - ); - expect(ASSIGNMENT_TOAST_COPY.bulkDeleteError(2)).toBe( - "Couldn't delete 2 assignments. Try again." - ); - }); -}); diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/store/assignment/assignment.logic.spec.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/store/assignment/assignment.logic.spec.ts index a11321143..e0d22d472 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/store/assignment/assignment.logic.spec.ts +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/store/assignment/assignment.logic.spec.ts @@ -1,4 +1,5 @@ import { configureStore } from "@reduxjs/toolkit"; + import { assignmentSlice, assignmentActions, assignmentSelectors } from "./assignment.logic"; const buildStore = (preloaded?: { assignmentTemp: { selectedAssignmentId: number | null } }) => @@ -10,6 +11,7 @@ const buildStore = (preloaded?: { assignmentTemp: { selectedAssignmentId: number describe("assignmentSlice reducer", () => { it("returns initial state with selectedAssignmentId null when no action dispatched", () => { const store = buildStore(); + expect(store.getState().assignmentTemp.selectedAssignmentId).toBeNull(); }); diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/store/assignmentExercise/assignmentExercise.logic.api.spec.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/store/assignmentExercise/assignmentExercise.logic.api.spec.ts index 5a67b52e5..63a5ad25c 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/store/assignmentExercise/assignmentExercise.logic.api.spec.ts +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/store/assignmentExercise/assignmentExercise.logic.api.spec.ts @@ -1,7 +1,7 @@ -import { assignmentExerciseApi } from "./assignmentExercise.logic.api"; - import type { Exercise } from "@/types/exercises"; +import { assignmentExerciseApi } from "./assignmentExercise.logic.api"; + const makeExercise = (overrides: Partial = {}): Exercise => ({ id: 1, assignment_id: 10, @@ -45,6 +45,7 @@ describe("assignmentExerciseApi", () => { it("exposes expected endpoint names", () => { const endpointNames = Object.keys(assignmentExerciseApi.endpoints); + expect(endpointNames).toContain("getExercises"); expect(endpointNames).toContain("updateAssignmentQuestions"); expect(endpointNames).toContain("removeAssignmentExercises"); @@ -59,6 +60,7 @@ describe("assignmentExerciseApi", () => { it("reducer returns non-null initial state", () => { const reducer = assignmentExerciseApi.reducer; const state = reducer(undefined, { type: "@@INIT" }); + expect(state).toBeDefined(); expect(typeof state).toBe("object"); }); @@ -73,12 +75,14 @@ describe("assignmentExerciseApi", () => { const response = { detail: { exercises } }; const transform = (r: typeof response) => r.detail.exercises; + expect(transform(response)).toEqual(exercises); }); it("returns empty array when exercises list is empty", () => { const response = { detail: { exercises: [] } }; const transform = (r: typeof response) => r.detail.exercises; + expect(transform(response)).toEqual([]); }); }); @@ -124,6 +128,7 @@ describe("assignmentExerciseApi", () => { if (!error) return [{ type: "Exercises" }]; return []; }; + expect(invalidatesTags(undefined, noError)).toEqual([{ type: "Exercises" }]); }); @@ -135,6 +140,7 @@ describe("assignmentExerciseApi", () => { if (!error) return [{ type: "Exercises" }]; return []; }; + expect(invalidatesTags(undefined, withError)).toEqual([]); }); }); @@ -148,6 +154,7 @@ describe("assignmentExerciseApi", () => { if (!error) return [{ type: "Exercises" }]; return []; }; + expect(invalidatesTags(undefined, noError)).toEqual([{ type: "Exercises" }]); }); @@ -159,6 +166,7 @@ describe("assignmentExerciseApi", () => { if (!error) return [{ type: "Exercises" }]; return []; }; + expect(invalidatesTags(undefined, withError)).toEqual([]); }); }); @@ -169,6 +177,7 @@ describe("assignmentExerciseApi", () => { if (!error) return ["Exercises"]; return []; }; + expect(invalidatesTags(undefined, noError)).toEqual(["Exercises"]); }); @@ -177,6 +186,7 @@ describe("assignmentExerciseApi", () => { if (!error) return ["Exercises"]; return []; }; + expect(invalidatesTags(undefined, withError)).toEqual([]); }); }); @@ -187,6 +197,7 @@ describe("assignmentExerciseApi", () => { if (!error) return ["Exercises"]; return []; }; + expect(invalidatesTags(undefined, noError)).toEqual(["Exercises"]); }); @@ -195,6 +206,7 @@ describe("assignmentExerciseApi", () => { if (!error) return ["Exercises"]; return []; }; + expect(invalidatesTags(undefined, withError)).toEqual([]); }); }); @@ -203,41 +215,49 @@ describe("assignmentExerciseApi", () => { describe("exported hooks", () => { it("exports useGetExercisesQuery as a function", async () => { const mod = await import("./assignmentExercise.logic.api"); + expect(typeof mod.useGetExercisesQuery).toBe("function"); }); it("exports useUpdateAssignmentQuestionsMutation as a function", async () => { const mod = await import("./assignmentExercise.logic.api"); + expect(typeof mod.useUpdateAssignmentQuestionsMutation).toBe("function"); }); it("exports useRemoveAssignmentExercisesMutation as a function", async () => { const mod = await import("./assignmentExercise.logic.api"); + expect(typeof mod.useRemoveAssignmentExercisesMutation).toBe("function"); }); it("exports useReorderAssignmentExercisesMutation as a function", async () => { const mod = await import("./assignmentExercise.logic.api"); + expect(typeof mod.useReorderAssignmentExercisesMutation).toBe("function"); }); it("exports useUpdateAssignmentExercisesMutation as a function", async () => { const mod = await import("./assignmentExercise.logic.api"); + expect(typeof mod.useUpdateAssignmentExercisesMutation).toBe("function"); }); it("exports useValidateQuestionNameMutation as a function", async () => { const mod = await import("./assignmentExercise.logic.api"); + expect(typeof mod.useValidateQuestionNameMutation).toBe("function"); }); it("exports useCopyQuestionMutation as a function", async () => { const mod = await import("./assignmentExercise.logic.api"); + expect(typeof mod.useCopyQuestionMutation).toBe("function"); }); it("exports useHasApiKeyQuery as a function", async () => { const mod = await import("./assignmentExercise.logic.api"); + expect(typeof mod.useHasApiKeyQuery).toBe("function"); }); }); diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/store/assignmentExercise/assignmentExercise.logic.api.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/store/assignmentExercise/assignmentExercise.logic.api.ts index d06736d26..1a35980d4 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/store/assignmentExercise/assignmentExercise.logic.api.ts +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/store/assignmentExercise/assignmentExercise.logic.api.ts @@ -1,9 +1,9 @@ +import { notify } from "@components/ui/notify"; import { createApi } from "@reduxjs/toolkit/query/react"; import { assignmentApi } from "@store/assignment/assignment.logic.api"; import { assignmentExerciseActions } from "@store/assignmentExercise/assignmentExercise.logic"; import { baseQuery } from "@store/baseQuery"; import { chooseExercisesActions } from "@store/chooseExercises/chooseExercises.logic"; -import { notify } from "@components/ui/notify"; import { RootState } from "@/state/store"; import { DetailResponse } from "@/types/api"; diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/store/assignmentExercise/assignmentExercise.logic.spec.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/store/assignmentExercise/assignmentExercise.logic.spec.ts index 75a837626..db80883ad 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/store/assignmentExercise/assignmentExercise.logic.spec.ts +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/store/assignmentExercise/assignmentExercise.logic.spec.ts @@ -1,12 +1,13 @@ import { configureStore } from "@reduxjs/toolkit"; +import { Exercise } from "@/types/exercises"; + import { assignmentExerciseActions, assignmentExerciseSelectors, assignmentExerciseSlice, AssignmentExerciseState } from "./assignmentExercise.logic"; -import { Exercise } from "@/types/exercises"; const makeExercise = (overrides: Partial = {}): Exercise => ({ id: 1, @@ -56,6 +57,7 @@ const buildStore = (preloadedExercises: Exercise[] = []) => describe("assignmentExerciseSlice reducer", () => { it("returns initial state with empty assignmentExercises when no action dispatched", () => { const store = buildStore(); + expect(store.getState().assignmentExercise.assignmentExercises).toEqual([]); }); @@ -91,6 +93,7 @@ describe("getAssignmentExercises selector", () => { it("returns empty array when no exercises are in state", () => { const store = buildStore(); const result = assignmentExerciseSelectors.getAssignmentExercises(store.getState() as any); + expect(result).toEqual([]); }); @@ -132,6 +135,7 @@ describe("getAssignmentReadings selector", () => { it("returns empty array when no exercises are in state", () => { const store = buildStore(); const result = assignmentExerciseSelectors.getAssignmentReadings(store.getState() as any); + expect(result).toEqual([]); }); @@ -186,9 +190,9 @@ describe("setAssignmentExercisesForAssignment", () => { ); expect(store.getState().assignmentExercise.assignmentExercises).toEqual(exercises); - expect( - assignmentExerciseSelectors.getExercisesForAssignmentId(store.getState() as any) - ).toBe(6); + expect(assignmentExerciseSelectors.getExercisesForAssignmentId(store.getState() as any)).toBe( + 6 + ); }); it("reports the data as belonging to the new assignment after a switch", () => { @@ -208,8 +212,8 @@ describe("setAssignmentExercisesForAssignment", () => { ); expect(store.getState().assignmentExercise.assignmentExercises).toEqual([]); - expect( - assignmentExerciseSelectors.getExercisesForAssignmentId(store.getState() as any) - ).toBe(7); + expect(assignmentExerciseSelectors.getExercisesForAssignmentId(store.getState() as any)).toBe( + 7 + ); }); }); diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/store/baseQuery.spec.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/store/baseQuery.spec.ts index 0605432c3..13e8e0add 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/store/baseQuery.spec.ts +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/store/baseQuery.spec.ts @@ -12,6 +12,7 @@ vi.mock("@components/ui/notify", () => ({ vi.mock("@reduxjs/toolkit/query", async (importOriginal) => { const actual = await importOriginal(); + return { ...actual, fetchBaseQuery: vi.fn(() => mockInnerBaseQuery) @@ -29,6 +30,7 @@ describe("baseQueryWithErrorHandlers", () => { it("returns the result from baseQuery when there is no error", async () => { const expectedResult = { data: { id: 1 } }; + mockInnerBaseQuery.mockResolvedValue(expectedResult); const { baseQueryWithErrorHandlers } = await import("@store/baseQuery"); @@ -42,9 +44,11 @@ describe("baseQueryWithErrorHandlers", () => { const unauthorizedResult = { error: { status: HttpStatusCode.UNAUTHORIZED, data: "Unauthorized" } }; + mockInnerBaseQuery.mockResolvedValue(unauthorizedResult); const { baseQueryWithErrorHandlers } = await import("@store/baseQuery"); + await baseQueryWithErrorHandlers("/protected-endpoint", mockApi, mockExtraOptions); expect(notify.error).toHaveBeenCalledWith({ @@ -63,6 +67,7 @@ describe("baseQueryWithErrorHandlers", () => { mockInnerBaseQuery.mockResolvedValue({ error: { status, data: "boom" } }); const { baseQueryWithErrorHandlers } = await import("@store/baseQuery"); + await baseQueryWithErrorHandlers("/some-endpoint", mockApi, mockExtraOptions); expect(notify.error).toHaveBeenCalledWith({ @@ -78,6 +83,7 @@ describe("baseQueryWithErrorHandlers", () => { }); const { baseQueryWithErrorHandlers } = await import("@store/baseQuery"); + await baseQueryWithErrorHandlers("/a", mockApi, mockExtraOptions); await baseQueryWithErrorHandlers("/b", mockApi, mockExtraOptions); @@ -90,6 +96,7 @@ describe("baseQueryWithErrorHandlers", () => { const errorResult = { error: { status: HttpStatusCode.FORBIDDEN, data: "Forbidden" } }; + mockInnerBaseQuery.mockResolvedValue(errorResult); const { baseQueryWithErrorHandlers } = await import("@store/baseQuery"); @@ -104,10 +111,12 @@ describe("baseQueryWithErrorHandlers", () => { it("passes the args, api, and extraOptions through to the inner baseQuery", async () => { const successResult = { data: "ok" }; + mockInnerBaseQuery.mockResolvedValue(successResult); const args = { url: "/test", method: "POST", body: { foo: "bar" } }; const { baseQueryWithErrorHandlers } = await import("@store/baseQuery"); + await baseQueryWithErrorHandlers(args, mockApi, mockExtraOptions); expect(mockInnerBaseQuery).toHaveBeenCalledWith(args, mockApi, mockExtraOptions); diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/store/baseQuery.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/store/baseQuery.ts index 2408aeb47..540fe6a43 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/store/baseQuery.ts +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/store/baseQuery.ts @@ -1,10 +1,10 @@ +import { notify } from "@components/ui/notify"; import { BaseQueryFn, FetchArgs, fetchBaseQuery, FetchBaseQueryError } from "@reduxjs/toolkit/query"; -import { notify } from "@components/ui/notify"; import { HttpStatusCode } from "@/types/api"; diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/store/chooseExercises/chooseExercises.logic.spec.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/store/chooseExercises/chooseExercises.logic.spec.ts index 9c1386ed5..2b81c1ba4 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/store/chooseExercises/chooseExercises.logic.spec.ts +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/store/chooseExercises/chooseExercises.logic.spec.ts @@ -1,12 +1,13 @@ import { configureStore } from "@reduxjs/toolkit"; +import { Exercise } from "@/types/exercises"; + import { chooseExercisesActions, chooseExercisesSelectors, chooseExercisesSlice, ChooseExercisesState } from "./chooseExercises.logic"; -import { Exercise } from "@/types/exercises"; const makeExercise = (overrides: Partial = {}): Exercise => ({ id: 1, @@ -78,6 +79,7 @@ describe("chooseExercisesSlice reducer", () => { store.dispatch(chooseExercisesActions.setSelectedExercises([ex1, ex2])); const { selectedExercises } = store.getState().chooseExercises; + expect(selectedExercises).toHaveLength(2); expect(selectedExercises[0].name).toBe("ex2"); expect(selectedExercises[1].name).toBe("ex1"); @@ -91,6 +93,7 @@ describe("chooseExercisesSlice reducer", () => { store.dispatch(chooseExercisesActions.setSelectedExercises([regular, reading])); const { selectedExercises } = store.getState().chooseExercises; + expect(selectedExercises).toHaveLength(1); expect(selectedExercises[0].name).toBe("regular"); }); @@ -103,6 +106,7 @@ describe("chooseExercisesSlice reducer", () => { store.dispatch(chooseExercisesActions.setSelectedExercises([regular, page])); const { selectedExercises } = store.getState().chooseExercises; + expect(selectedExercises).toHaveLength(1); expect(selectedExercises[0].name).toBe("regular"); }); @@ -118,6 +122,7 @@ describe("chooseExercisesSlice reducer", () => { it("sets selectedExercises to empty array when given an empty array", () => { const store = buildStore(); + store.dispatch(chooseExercisesActions.setSelectedExercises([])); expect(store.getState().chooseExercises.selectedExercises).toEqual([]); @@ -205,6 +210,7 @@ describe("chooseExercisesSlice reducer", () => { store.dispatch(chooseExercisesActions.resetSelections()); const state = store.getState().chooseExercises; + expect(state.exercisesToAdd).toEqual([]); expect(state.exercisesToRemove).toEqual([]); expect(state.selectedExercises).toHaveLength(1); diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/store/datafile/datafile.logic.api.spec.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/store/datafile/datafile.logic.api.spec.ts index c0185daed..3c2c57956 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/store/datafile/datafile.logic.api.spec.ts +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/store/datafile/datafile.logic.api.spec.ts @@ -1,8 +1,10 @@ import { configureStore } from "@reduxjs/toolkit"; -import { DATAFILE_TOAST_COPY, datafileApi, getErrorMessage } from "./datafile.logic.api"; import type { FetchBaseQueryError } from "@reduxjs/toolkit/query/react"; + import type { ExistingDataFile } from "@/types/datafile"; +import { DATAFILE_TOAST_COPY, datafileApi, getErrorMessage } from "./datafile.logic.api"; + vi.mock("@components/ui/notify", () => ({ notify: { show: vi.fn(), @@ -45,26 +47,31 @@ describe("datafileApi — module exports", () => { it("exports useFetchDatafilesQuery hook", () => { const { useFetchDatafilesQuery } = datafileApi; + expect(typeof useFetchDatafilesQuery).toBe("function"); }); it("exports useFetchDatafileQuery hook", () => { const { useFetchDatafileQuery } = datafileApi; + expect(typeof useFetchDatafileQuery).toBe("function"); }); it("exports useCreateDatafileMutation hook", () => { const { useCreateDatafileMutation } = datafileApi; + expect(typeof useCreateDatafileMutation).toBe("function"); }); it("exports useUpdateDatafileMutation hook", () => { const { useUpdateDatafileMutation } = datafileApi; + expect(typeof useUpdateDatafileMutation).toBe("function"); }); it("exports useDeleteDatafileMutation hook", () => { const { useDeleteDatafileMutation } = datafileApi; + expect(typeof useDeleteDatafileMutation).toBe("function"); }); }); @@ -75,12 +82,14 @@ describe("datafileApi — reducer integration", () => { it("registers under its reducerPath and returns defined initial state", () => { const store = buildStore(); + expect(store.getState()[datafileApi.reducerPath]).toBeDefined(); }); it("each call to buildStore creates an independent store instance", () => { const a = buildStore(); const b = buildStore(); + expect(a).not.toBe(b); }); }); @@ -100,6 +109,7 @@ describe("datafileApi — fetchDatafiles transformResponse logic", () => { main_code: "" } ]; + expect(transform({ detail: { datafiles } })).toEqual(datafiles); }); @@ -116,6 +126,7 @@ describe("datafileApi — fetchDatafiles transformResponse logic", () => { { id: 1, acid: "a1", filename: "f1.csv", course_id: "cs101", owner: "u1", main_code: "" }, { id: 2, acid: "a2", filename: "f2.csv", course_id: "cs101", owner: "u2", main_code: "" } ]; + expect(transform({ detail: { datafiles } })).toHaveLength(2); }); }); @@ -222,12 +233,14 @@ describe("datafileApi — updateDatafile invalidatesTags logic", () => { it("invalidates both Datafiles list and the specific Datafile entry", () => { const tags = invalidatesTags(undefined, undefined, { acid: "target", main_code: "" }); + expect(tags).toContainEqual("Datafiles"); expect(tags).toContainEqual({ type: "Datafile", id: "target" }); }); it("uses the acid from arg as the specific tag id", () => { const tags = invalidatesTags(undefined, undefined, { acid: "specific_acid", main_code: "x" }); + expect(tags).toContainEqual({ type: "Datafile", id: "specific_acid" }); }); }); diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/store/datafile/datafile.logic.api.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/store/datafile/datafile.logic.api.ts index 2bab6ad99..3c5daf6bb 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/store/datafile/datafile.logic.api.ts +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/store/datafile/datafile.logic.api.ts @@ -1,6 +1,6 @@ +import { notify } from "@components/ui/notify"; import { createApi, FetchBaseQueryError } from "@reduxjs/toolkit/query/react"; import { baseQuery } from "@store/baseQuery"; -import { notify } from "@components/ui/notify"; import { DetailResponse } from "@/types/api"; import { @@ -29,14 +29,17 @@ export const DATAFILE_TOAST_COPY = { export const getErrorMessage = (error: FetchBaseQueryError): string => { if (error.status === 409) { const data = error.data as { detail?: string }; + return data?.detail || DATAFILE_TOAST_COPY.duplicateNameError; } if (error.status === 403) { const data = error.data as { detail?: string }; + return data?.detail || DATAFILE_TOAST_COPY.notOwnerError; } if (error.status === 404) { const data = error.data as { detail?: string }; + return data?.detail || DATAFILE_TOAST_COPY.notFoundError; } if (error.data && typeof error.data === "object" && "detail" in error.data) { @@ -54,7 +57,7 @@ export const datafileApi = createApi({ fetchDatafiles: build.query({ query: () => ({ method: "GET", - url: `/assignment/instructor/datafiles` + url: "/assignment/instructor/datafiles" }), transformResponse: (response: DetailResponse) => { return response.detail.datafiles || []; @@ -98,6 +101,7 @@ export const datafileApi = createApi({ }) .catch((error) => { const message = getErrorMessage(error.error as FetchBaseQueryError); + notify.error(message); }); } @@ -119,6 +123,7 @@ export const datafileApi = createApi({ }) .catch((error) => { const message = getErrorMessage(error.error as FetchBaseQueryError); + notify.error(message); }); } @@ -139,6 +144,7 @@ export const datafileApi = createApi({ }) .catch((error) => { const message = getErrorMessage(error.error as FetchBaseQueryError); + notify.error(message); }); } diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/store/dataset/dataset.logic.api.spec.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/store/dataset/dataset.logic.api.spec.ts index b3f1b4b2d..f71764c39 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/store/dataset/dataset.logic.api.spec.ts +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/store/dataset/dataset.logic.api.spec.ts @@ -1,4 +1,5 @@ import { configureStore } from "@reduxjs/toolkit"; +import { datasetSlice } from "@store/dataset/dataset.logic"; import { datasetApi, useGetWhichToGradeOptionsQuery, @@ -7,7 +8,7 @@ import { useGetQuestionTypeOptionsQuery, useGetSectionsForChapterQuery } from "@store/dataset/dataset.logic.api"; -import { datasetSlice } from "@store/dataset/dataset.logic"; + import { DetailResponse } from "@/types/api"; import { SectionsResponse } from "@/types/dataset"; @@ -57,6 +58,7 @@ describe("datasetApi module exports", () => { it("provides a reducer that returns initial state", () => { const store = buildStore(); const state = store.getState()[datasetApi.reducerPath]; + expect(state).toBeDefined(); expect(typeof state).toBe("object"); }); @@ -65,11 +67,13 @@ describe("datasetApi module exports", () => { describe("datasetApi endpoint query builders", () => { it("getWhichToGradeOptions endpoint has a select method", () => { const endpoint = datasetApi.endpoints.getWhichToGradeOptions; + expect(typeof (endpoint as any).select).toBe("function"); }); it("getSectionsForChapter endpoint has a select method", () => { const endpoint = datasetApi.endpoints.getSectionsForChapter; + expect(typeof (endpoint as any).select).toBe("function"); }); }); @@ -86,6 +90,7 @@ describe("getSectionsForChapter transformResponse", () => { it("maps each section to a label/value pair", () => { const internalEndpoints = (datasetApi as any).internalActions; + expect(internalEndpoints).toBeDefined(); }); @@ -138,18 +143,21 @@ describe("datasetApi store integration", () => { it("initial datasetApi state has a queries key", () => { const store = buildStore(); const apiState = store.getState()[datasetApi.reducerPath]; + expect(apiState).toHaveProperty("queries"); }); it("initial datasetApi state has a mutations key", () => { const store = buildStore(); const apiState = store.getState()[datasetApi.reducerPath]; + expect(apiState).toHaveProperty("mutations"); }); it("dataset slice starts with empty option arrays alongside datasetApi", () => { const store = buildStore(); const datasetState = store.getState().dataset; + expect(datasetState.whichToGradeOptions).toEqual([]); expect(datasetState.autoGradeOptions).toEqual([]); expect(datasetState.languageOptions).toEqual([]); diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/store/dataset/dataset.logic.api.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/store/dataset/dataset.logic.api.ts index 30630c0d3..89edabdd4 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/store/dataset/dataset.logic.api.ts +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/store/dataset/dataset.logic.api.ts @@ -1,7 +1,7 @@ +import { notify } from "@components/ui/notify"; import { createApi } from "@reduxjs/toolkit/query/react"; import { baseQuery } from "@store/baseQuery"; import { datasetActions } from "@store/dataset/dataset.logic"; -import { notify } from "@components/ui/notify"; import { DetailResponse } from "@/types/api"; import { TableDropdownOption, SectionsResponse } from "@/types/dataset"; diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/store/dataset/dataset.logic.spec.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/store/dataset/dataset.logic.spec.ts index aba6d93d1..5c955eda2 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/store/dataset/dataset.logic.spec.ts +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/store/dataset/dataset.logic.spec.ts @@ -1,7 +1,9 @@ import { configureStore } from "@reduxjs/toolkit"; -import { datasetSlice, datasetActions, datasetSelectors, DatasetState } from "./dataset.logic"; + import { TableDropdownOption, SectionOption } from "@/types/dataset"; +import { datasetSlice, datasetActions, datasetSelectors, DatasetState } from "./dataset.logic"; + function makeStore(preloadedDataset?: Partial) { return configureStore({ reducer: { dataset: datasetSlice.reducer }, @@ -34,29 +36,34 @@ const sampleSections: SectionOption[] = [ describe("datasetSlice reducer", () => { it("returns the initial state when no action is dispatched", () => { const store = makeStore(); + expect(store.getState().dataset).toEqual(buildInitial()); }); it("setWhichToGradeOptions replaces the whichToGradeOptions array", () => { const store = makeStore(); + store.dispatch(datasetActions.setWhichToGradeOptions(sampleDropdownOptions)); expect(store.getState().dataset.whichToGradeOptions).toEqual(sampleDropdownOptions); }); it("setWhichToGradeOptions with an empty array clears previous options", () => { const store = makeStore({ whichToGradeOptions: sampleDropdownOptions }); + store.dispatch(datasetActions.setWhichToGradeOptions([])); expect(store.getState().dataset.whichToGradeOptions).toEqual([]); }); it("setAutoGradeOptions replaces the autoGradeOptions array", () => { const store = makeStore(); + store.dispatch(datasetActions.setAutoGradeOptions(sampleDropdownOptions)); expect(store.getState().dataset.autoGradeOptions).toEqual(sampleDropdownOptions); }); it("setAutoGradeOptions with an empty array clears previous options", () => { const store = makeStore({ autoGradeOptions: sampleDropdownOptions }); + store.dispatch(datasetActions.setAutoGradeOptions([])); expect(store.getState().dataset.autoGradeOptions).toEqual([]); }); @@ -66,12 +73,14 @@ describe("datasetSlice reducer", () => { const langOptions: TableDropdownOption[] = [ { value: "python", label: "Python", description: "Python language" } ]; + store.dispatch(datasetActions.setLanguageOptions(langOptions)); expect(store.getState().dataset.languageOptions).toEqual(langOptions); }); it("setLanguageOptions does not affect other state fields", () => { const store = makeStore({ autoGradeOptions: sampleDropdownOptions }); + store.dispatch( datasetActions.setLanguageOptions([{ value: "java", label: "Java", description: "" }]) ); @@ -83,24 +92,28 @@ describe("datasetSlice reducer", () => { const typeOptions: TableDropdownOption[] = [ { value: "mchoice", label: "Multiple Choice", description: "MC question" } ]; + store.dispatch(datasetActions.setQuestionTypeOptions(typeOptions)); expect(store.getState().dataset.questionTypeOptions).toEqual(typeOptions); }); it("setQuestionTypeOptions with an empty array clears previous options", () => { const store = makeStore({ questionTypeOptions: sampleDropdownOptions }); + store.dispatch(datasetActions.setQuestionTypeOptions([])); expect(store.getState().dataset.questionTypeOptions).toEqual([]); }); it("setSections replaces the sections array", () => { const store = makeStore(); + store.dispatch(datasetActions.setSections(sampleSections)); expect(store.getState().dataset.sections).toEqual(sampleSections); }); it("setSections with an empty array clears previous sections", () => { const store = makeStore({ sections: sampleSections }); + store.dispatch(datasetActions.setSections([])); expect(store.getState().dataset.sections).toEqual([]); }); @@ -110,10 +123,12 @@ describe("datasetSlice reducer", () => { whichToGradeOptions: sampleDropdownOptions, sections: sampleSections }); + store.dispatch( datasetActions.setAutoGradeOptions([{ value: "x", label: "X", description: "" }]) ); const state = store.getState().dataset; + expect(state.whichToGradeOptions).toEqual(sampleDropdownOptions); expect(state.sections).toEqual(sampleSections); expect(state.languageOptions).toEqual([]); @@ -126,6 +141,7 @@ describe("datasetSelectors", () => { const fakeState = { dataset: { ...buildInitial(), whichToGradeOptions: sampleDropdownOptions } } as any; + expect(datasetSelectors.getWhichToGradeOptions(fakeState)).toEqual(sampleDropdownOptions); }); @@ -133,12 +149,14 @@ describe("datasetSelectors", () => { const fakeState = { dataset: { ...buildInitial(), autoGradeOptions: sampleDropdownOptions } } as any; + expect(datasetSelectors.getAutoGradeOptions(fakeState)).toEqual(sampleDropdownOptions); }); it("getLanguageOptions returns the languageOptions from state", () => { const opts: TableDropdownOption[] = [{ value: "sql", label: "SQL", description: "" }]; const fakeState = { dataset: { ...buildInitial(), languageOptions: opts } } as any; + expect(datasetSelectors.getLanguageOptions(fakeState)).toEqual(opts); }); @@ -147,16 +165,19 @@ describe("datasetSelectors", () => { { value: "fitb", label: "Fill in the Blank", description: "" } ]; const fakeState = { dataset: { ...buildInitial(), questionTypeOptions: opts } } as any; + expect(datasetSelectors.getQuestionTypeOptions(fakeState)).toEqual(opts); }); it("getSections returns the sections from state", () => { const fakeState = { dataset: { ...buildInitial(), sections: sampleSections } } as any; + expect(datasetSelectors.getSections(fakeState)).toEqual(sampleSections); }); it("selectors return empty arrays when state is at initial values", () => { const fakeState = { dataset: buildInitial() } as any; + expect(datasetSelectors.getWhichToGradeOptions(fakeState)).toEqual([]); expect(datasetSelectors.getAutoGradeOptions(fakeState)).toEqual([]); expect(datasetSelectors.getLanguageOptions(fakeState)).toEqual([]); diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/store/exercises/exercises.logic.api.spec.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/store/exercises/exercises.logic.api.spec.ts index 5e1d8ac6b..7b8afc4e4 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/store/exercises/exercises.logic.api.spec.ts +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/store/exercises/exercises.logic.api.spec.ts @@ -1,4 +1,8 @@ import { configureStore } from "@reduxjs/toolkit"; + +import type { DetailResponse } from "@/types/api"; +import type { ExercisesSearchResponse } from "@/types/exercises"; + import { EXERCISES_TOAST_COPY, exercisesApi, @@ -6,9 +10,6 @@ import { useSearchExercisesSmartQuery } from "./exercises.logic.api"; -import type { DetailResponse } from "@/types/api"; -import type { ExercisesSearchResponse } from "@/types/exercises"; - vi.mock("@components/ui/notify", () => ({ notify: { show: vi.fn(), @@ -84,6 +85,7 @@ describe("exercisesApi", () => { }); const state = store.getState()[exercisesApi.reducerPath]; + expect(state).toBeDefined(); expect(state.queries).toBeDefined(); expect(state.mutations).toBeDefined(); @@ -96,6 +98,7 @@ describe("exercisesApi", () => { describe("searchExercisesSmart transformResponse", () => { const buildTransformResponse = () => { const endpoint = exercisesApi.endpoints.searchExercisesSmart; + return (endpoint as any).select; }; diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/store/exercises/exercises.logic.api.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/store/exercises/exercises.logic.api.ts index fa4c4cce0..f83845bfd 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/store/exercises/exercises.logic.api.ts +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/store/exercises/exercises.logic.api.ts @@ -1,7 +1,7 @@ +import { notify } from "@components/ui/notify"; import { createApi } from "@reduxjs/toolkit/query/react"; import { assignmentExerciseApi } from "@store/assignmentExercise/assignmentExercise.logic.api"; import { baseQuery } from "@store/baseQuery"; -import { notify } from "@components/ui/notify"; import { DetailResponse } from "@/types/api"; import { diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/store/exercises/exercises.logic.spec.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/store/exercises/exercises.logic.spec.ts index 2ff47a58f..c7d0fb15f 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/store/exercises/exercises.logic.spec.ts +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/store/exercises/exercises.logic.spec.ts @@ -3,6 +3,7 @@ import { assignmentActions } from "@store/assignment/assignment.logic"; import { Exercise } from "@/types/exercises"; import { TreeNode } from "@/types/treeNode"; + import { exercisesActions, exercisesSelectors, @@ -117,6 +118,7 @@ describe("exercisesSlice reducer", () => { store.dispatch(exercisesActions.setAvailableExercises([node])); const result = store.getState().exercises.availableExercises; + expect(result).toHaveLength(1); expect(result[0].key).toBe("ch1"); }); @@ -129,6 +131,7 @@ describe("exercisesSlice reducer", () => { store.dispatch(exercisesActions.setAvailableExercises([pageNode, regularNode])); const result = store.getState().exercises.availableExercises; + expect(result).toHaveLength(1); expect(result[0].key).toBe("q-1"); }); @@ -155,6 +158,7 @@ describe("exercisesSlice reducer", () => { store.dispatch(exercisesActions.setAvailableExercises([parent])); const result = store.getState().exercises.availableExercises; + expect(result[0].disabled).toBe(true); }); @@ -171,6 +175,7 @@ describe("exercisesSlice reducer", () => { store.dispatch(exercisesActions.setAvailableExercises([parent])); const result = store.getState().exercises.availableExercises; + expect(result[0].disabled).toBe(false); }); @@ -199,7 +204,11 @@ describe("exercisesSelectors", () => { it("getSelectedExercises returns the current selectedExercises array", () => { const exercises = [makeExercise()]; const store = makeStore({ - exercises: { selectedExercises: exercises, availableExercises: [], selectionAssignmentId: null } + exercises: { + selectedExercises: exercises, + availableExercises: [], + selectionAssignmentId: null + } }); const state = store.getState() as any; diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/store/grader/grader.logic.api.spec.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/store/grader/grader.logic.api.spec.ts index 13cf16fc1..83670715f 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/store/grader/grader.logic.api.spec.ts +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/store/grader/grader.logic.api.spec.ts @@ -1,7 +1,10 @@ import { configureStore } from "@reduxjs/toolkit"; + import { assignmentApi } from "@/store/assignment/assignment.logic.api"; import { baseQuery } from "@/store/baseQuery"; +import type { DetailResponse } from "@/types/api"; import type { Assignment } from "@/types/assignment"; + import { graderApi, useGetGraderQuestionsQuery, @@ -35,7 +38,6 @@ import type { SetManualTotalRequest, SetManualTotalResponse } from "./grader.logic.api"; -import type { DetailResponse } from "@/types/api"; vi.mock("@/store/baseQuery", () => ({ baseQuery: vi.fn() @@ -78,6 +80,7 @@ describe("graderApi module exports", () => { it("exposes the expected endpoint names", () => { const endpointNames = Object.keys(graderApi.endpoints); + expect(endpointNames).toContain("getGraderQuestions"); expect(endpointNames).toContain("getGraderAnswers"); expect(endpointNames).toContain("getGraderHistory"); @@ -93,6 +96,7 @@ describe("graderApi module exports", () => { it("reducer returns a defined initial state", () => { const state = graderApi.reducer(undefined, { type: "@@INIT" }); + expect(state).toBeDefined(); expect(typeof state).toBe("object"); }); @@ -106,18 +110,21 @@ describe("graderApi store integration", () => { it("initial graderApi state has a queries key", () => { const store = buildStore(); const apiState = store.getState()[graderApi.reducerPath]; + expect(apiState).toHaveProperty("queries"); }); it("initial graderApi state has a mutations key", () => { const store = buildStore(); const apiState = store.getState()[graderApi.reducerPath]; + expect(apiState).toHaveProperty("mutations"); }); it("initial queries state is an empty object", () => { const store = buildStore(); const apiState = store.getState()[graderApi.reducerPath]; + expect(apiState.queries).toEqual({}); }); }); @@ -125,26 +132,31 @@ describe("graderApi store integration", () => { describe("graderApi endpoint definitions", () => { it("getGraderQuestions endpoint has a select method", () => { const endpoint = graderApi.endpoints.getGraderQuestions; + expect(typeof (endpoint as any).select).toBe("function"); }); it("getGraderAnswers endpoint has a select method", () => { const endpoint = graderApi.endpoints.getGraderAnswers; + expect(typeof (endpoint as any).select).toBe("function"); }); it("getGraderHistory endpoint has a select method", () => { const endpoint = graderApi.endpoints.getGraderHistory; + expect(typeof (endpoint as any).select).toBe("function"); }); it("saveGrade endpoint has a select method", () => { const endpoint = graderApi.endpoints.saveGrade; + expect(typeof (endpoint as any).select).toBe("function"); }); it("regrade endpoint has a select method", () => { const endpoint = graderApi.endpoints.regrade; + expect(typeof (endpoint as any).select).toBe("function"); }); }); @@ -161,12 +173,14 @@ describe("saveGrade request body", () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any let sent: any; // eslint-disable-next-line @typescript-eslint/no-explicit-any + (baseQuery as any).mockImplementation((args: any) => { sent = args; return { data: { detail: { sid: "s1", div_id: "q1", score: 5, comment: "" } } }; }); const store = buildFullStore(); + await store .dispatch( graderApi.endpoints.saveGrade.initiate({ @@ -214,6 +228,7 @@ describe("getGraderQuestions transformResponse", () => { detail: { assignment: mockAssignment, questions: mockQuestions } }; const result = response.detail; + expect(result.assignment).toEqual(mockAssignment); expect(result.questions).toHaveLength(1); expect(result.questions[0].id).toBe(10); @@ -224,6 +239,7 @@ describe("getGraderQuestions transformResponse", () => { detail: { assignment: mockAssignment, questions: mockQuestions } }; const q = response.detail.questions[0]; + expect(q.answered_count).toBe(20); expect(q.correct_count).toBe(15); expect(q.average_score).toBe(3.5); @@ -263,6 +279,7 @@ describe("getGraderAnswers transformResponse", () => { detail: mockAnswersResponse }; const result = response.detail; + expect(result.answers).toHaveLength(2); expect(result.answers[0].sid).toBe("student1"); }); @@ -272,6 +289,7 @@ describe("getGraderAnswers transformResponse", () => { detail: mockAnswersResponse }; const ungraded = response.detail.answers.find((a) => a.sid === "student2"); + expect(ungraded?.score).toBeNull(); }); @@ -279,6 +297,7 @@ describe("getGraderAnswers transformResponse", () => { const response: DetailResponse = { detail: mockAnswersResponse }; + expect(response.detail.question.max_points).toBe(5); expect(response.detail.question.question_type).toBe("mchoice"); }); @@ -293,6 +312,7 @@ describe("getGraderHistory transformResponse", () => { } }; const result = response.detail; + expect(result.history).toHaveLength(1); expect(result.useinfo).toHaveLength(1); expect(result.history[0].answer).toBe("A"); @@ -306,6 +326,7 @@ describe("getGraderHistory transformResponse", () => { } }; const historyItem = response.detail.history[0]; + expect(historyItem.answer).toEqual({ key: "value" }); expect(historyItem.correct).toBeNull(); }); @@ -317,6 +338,7 @@ describe("getGraderHistory transformResponse", () => { useinfo: [] } }; + expect(response.detail.history[0].answer).toBeNull(); }); }); @@ -331,6 +353,7 @@ describe("getCourseRoster transformResponse", () => { detail: { students } }; const result = response.detail.students ?? []; + expect(result).toHaveLength(2); expect(result[0].username).toBe("alice"); }); @@ -340,6 +363,7 @@ describe("getCourseRoster transformResponse", () => { detail: { students: [] } }; const result = response.detail.students ?? []; + expect(result).toEqual([]); }); }); @@ -361,6 +385,7 @@ describe("getAccommodations transformResponse", () => { detail: { accommodations: [mockAccommodation] } }; const result = response.detail; + expect(result.accommodations).toHaveLength(1); expect(result.accommodations[0].sid).toBe("student1"); }); @@ -369,6 +394,7 @@ describe("getAccommodations transformResponse", () => { const response: DetailResponse<{ accommodations: Accommodation[] }> = { detail: { accommodations: [] } }; + expect(response.detail.accommodations).toEqual([]); }); }); @@ -396,6 +422,7 @@ describe("regradePreview and regrade transformResponse", () => { it("extracts RegradeReport from detail wrapper", () => { const response: DetailResponse = { detail: mockReport }; const result = response.detail; + expect(result.total).toBe(10); expect(result.changed).toBe(3); expect(result.skipped_manual).toBe(2); @@ -406,6 +433,7 @@ describe("regradePreview and regrade transformResponse", () => { it("preserves items array with diff details", () => { const response: DetailResponse = { detail: mockReport }; const item = response.detail.items[0]; + expect(item.sid).toBe("student1"); expect(item.old_score).toBe(3); expect(item.new_score).toBe(5); @@ -423,6 +451,7 @@ describe("regradePreview and regrade transformResponse", () => { items: [] }; const response: DetailResponse = { detail: emptyReport }; + expect(response.detail.items).toEqual([]); }); }); @@ -439,6 +468,7 @@ describe("regrade invalidatesTags logic", () => { ...r.question_ids.map((id) => ({ type: "GraderAnswers" as const, id })) ]; const tags = invalidatesTags({}, null, req); + expect(tags).toContainEqual({ type: "GraderQuestions", id: 5 }); }); @@ -453,6 +483,7 @@ describe("regrade invalidatesTags logic", () => { ...r.question_ids.map((id) => ({ type: "GraderAnswers" as const, id })) ]; const tags = invalidatesTags({}, null, req); + expect(tags).toContainEqual({ type: "GraderAnswers", id: 10 }); expect(tags).toContainEqual({ type: "GraderAnswers", id: 20 }); expect(tags).toHaveLength(3); @@ -469,6 +500,7 @@ describe("regrade invalidatesTags logic", () => { ...r.question_ids.map((id) => ({ type: "GraderAnswers" as const, id })) ]; const tags = invalidatesTags({}, null, req); + expect(tags).toHaveLength(1); expect(tags[0]).toEqual({ type: "GraderQuestions", id: 7 }); }); @@ -480,6 +512,7 @@ describe("recomputeTotals transformResponse and invalidatesTags", () => { detail: { assignment_id: 3, students: 25 } }; const result = response.detail; + expect(result.assignment_id).toBe(3); expect(result.students).toBe(25); }); @@ -490,6 +523,7 @@ describe("recomputeTotals transformResponse and invalidatesTags", () => { { type: "GraderQuestions" as const, id: r.assignment_id } ]; const tags = invalidatesTags({}, null, req); + expect(tags).toEqual([{ type: "GraderQuestions", id: 3 }]); }); }); @@ -507,6 +541,7 @@ describe("getGraderAnswers query builder", () => { url: `/assignment/instructor/grader/questions/answers?assignment_id=${assignmentId}&question_id=${questionId}` }); const result = queryFn({ assignmentId: 1, questionId: 10 }); + expect(result.url).toBe( "/assignment/instructor/grader/questions/answers?assignment_id=1&question_id=10" ); @@ -520,16 +555,17 @@ describe("getGraderHistory query builder", () => { const queryFn = ({ assignmentId, questionId, - sid + sid: studentSid }: { assignmentId: number; questionId: number; sid: string; }) => ({ method: "GET", - url: `/assignment/instructor/grader/questions/history?assignment_id=${assignmentId}&question_id=${questionId}&sid=${encodeURIComponent(sid)}` + url: `/assignment/instructor/grader/questions/history?assignment_id=${assignmentId}&question_id=${questionId}&sid=${encodeURIComponent(studentSid)}` }); const result = queryFn({ assignmentId: 1, questionId: 10, sid }); + expect(result.url).toContain("sid=user%40example.com"); }); @@ -547,6 +583,7 @@ describe("getGraderHistory query builder", () => { url: `/assignment/instructor/grader/questions/history?assignment_id=${assignmentId}&question_id=${questionId}&sid=${encodeURIComponent(sid)}` }); const result = queryFn({ assignmentId: 2, questionId: 20, sid: "student1" }); + expect(result.url).toContain("sid=student1"); }); }); @@ -558,6 +595,7 @@ describe("deleteAccommodation query builder", () => { url: `/assignment/instructor/accommodation/${id}` }); const result = queryFn(42); + expect(result.method).toBe("DELETE"); expect(result.url).toBe("/assignment/instructor/accommodation/42"); }); @@ -575,6 +613,7 @@ describe("GraderStudentAnswer optional fields", () => { percent: null, comment: null }; + expect(answer.score).toBeNull(); expect(answer.correct).toBeNull(); expect(answer.percent).toBeNull(); @@ -589,6 +628,7 @@ describe("RegradeRequest optional fields", () => { question_ids: [10], sids: ["s1"] }; + expect(req.overwrite_manual).toBeUndefined(); expect(req.enforce_deadline).toBeUndefined(); expect(req.recompute_totals).toBeUndefined(); @@ -605,6 +645,7 @@ describe("RegradeRequest optional fields", () => { recompute_totals: true, which_to_grade_override: "best_answer" }; + expect(req.overwrite_manual).toBe(true); expect(req.enforce_deadline).toBe(false); expect(req.recompute_totals).toBe(true); @@ -628,6 +669,7 @@ describe("setAssignmentReleased", () => { body }); const result = queryFn({ assignment_id: 7, released: true }); + expect(result.method).toBe("POST"); expect(result.url).toBe("/assignment/instructor/grader/release"); expect(result.body).toEqual({ assignment_id: 7, released: true }); @@ -637,6 +679,7 @@ describe("setAssignmentReleased", () => { const response: DetailResponse<{ assignment_id: number; released: boolean }> = { detail: { assignment_id: 7, released: true } }; + expect(response.detail).toEqual({ assignment_id: 7, released: true }); }); @@ -663,12 +706,14 @@ describe("setAssignmentReleased", () => { const list = entries.find((e) => e.endpointName === "getAssignments")?.data as | Assignment[] | undefined; + return list?.find((a) => a.id === 42)?.released; } it("optimistically flips released in the getAssignments cache and persists on success", async () => { let released = false; // eslint-disable-next-line @typescript-eslint/no-explicit-any + (baseQuery as any).mockImplementation((args: any) => { if (typeof args === "object" && args.url?.endsWith("/grader/release")) { released = args.body.released; @@ -682,6 +727,7 @@ describe("setAssignmentReleased", () => { }); const store = buildFullStore(); + await primeAssignments(store); expect(releasedInCache(store)).toBe(false); @@ -704,6 +750,7 @@ describe("setAssignmentReleased", () => { }); const store = buildFullStore(); + await primeAssignments(store); expect(releasedInCache(store)).toBe(false); @@ -735,6 +782,7 @@ describe("setAssignmentThreshold", () => { body }); const result = queryFn({ assignment_id: 7, threshold_pct: 0.9 }); + expect(result.method).toBe("POST"); expect(result.url).toBe("/assignment/instructor/grader/threshold"); expect(result.body).toEqual({ assignment_id: 7, threshold_pct: 0.9 }); @@ -744,6 +792,7 @@ describe("setAssignmentThreshold", () => { const response: DetailResponse<{ assignment_id: number; threshold_pct: number | null }> = { detail: { assignment_id: 7, threshold_pct: 0.9 } }; + expect(response.detail).toEqual({ assignment_id: 7, threshold_pct: 0.9 }); }); @@ -766,12 +815,14 @@ describe("setAssignmentThreshold", () => { const list = entries.find((e) => e.endpointName === "getAssignments")?.data as | Assignment[] | undefined; + return list?.find((a) => a.id === 42)?.threshold_pct; } it("optimistically patches threshold_pct in the getAssignments cache and persists on success", async () => { let threshold: number | null = null; // eslint-disable-next-line @typescript-eslint/no-explicit-any + (baseQuery as any).mockImplementation((args: any) => { if (typeof args === "object" && args.url?.endsWith("/grader/threshold")) { threshold = args.body.threshold_pct; @@ -790,6 +841,7 @@ describe("setAssignmentThreshold", () => { }); const store = buildFullStore(); + await store.dispatch(assignmentApi.endpoints.getAssignments.initiate()); expect(thresholdInCache(store)).toBeNull(); @@ -817,6 +869,7 @@ describe("setAssignmentThreshold", () => { }); const store = buildFullStore(); + await store.dispatch(assignmentApi.endpoints.getAssignments.initiate()); expect(thresholdInCache(store)).toBeNull(); @@ -850,6 +903,7 @@ describe("getGradebook", () => { url: "/assignment/instructor/grader/gradebook/data" }); const result = queryFn(); + expect(result.url).toBe("/assignment/instructor/grader/gradebook/data"); expect(result.method).toBe("GET"); }); @@ -865,6 +919,7 @@ describe("getGradebook", () => { } }; const result = response.detail; + expect(result.assignments[0].name).toBe("A1"); expect(result.students[0].sid).toBe("s1"); expect(result.cells[0].score).toBe(8); @@ -880,11 +935,13 @@ describe("gradebook CSV helpers", () => { it("builds a dated, sanitized filename", () => { const name = gradebookCsvFilename("My Course!", new Date("2026-06-13T12:00:00Z")); + expect(name).toBe("gradebook-My-Course--2026-06-13.csv"); }); it("falls back to a default course slug", () => { const name = gradebookCsvFilename("", new Date("2026-01-02T00:00:00Z")); + expect(name).toBe("gradebook-course-2026-01-02.csv"); }); }); @@ -906,6 +963,7 @@ describe("setManualTotal", () => { body }); const result = queryFn({ assignment_id: 7, sid: "s1", score: 9, manual: true }); + expect(result.url).toBe("/assignment/instructor/grader/manual_total"); expect(result.method).toBe("POST"); expect(result.body).toEqual({ assignment_id: 7, sid: "s1", score: 9, manual: true }); @@ -914,6 +972,7 @@ describe("setManualTotal", () => { it("builds a revert request without a score", () => { const queryFn = (body: SetManualTotalRequest) => ({ body }); const result = queryFn({ assignment_id: 7, sid: "s1", manual: false }); + expect(result.body.manual).toBe(false); expect(result.body.score).toBeUndefined(); }); @@ -923,6 +982,7 @@ describe("setManualTotal", () => { detail: { assignment_id: 7, sid: "s1", score: 9, manual_total: true } }; const result = response.detail; + expect(result.manual_total).toBe(true); expect(result.score).toBe(9); }); @@ -934,6 +994,7 @@ describe("setManualTotal", () => { "Gradebook" as const ]; const tags = invalidatesTags({}, null, req); + expect(tags).toEqual([{ type: "GraderQuestions", id: 7 }, "Gradebook"]); }); }); diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/store/grader/grader.logic.api.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/store/grader/grader.logic.api.ts index af2c30ef0..347025a80 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/store/grader/grader.logic.api.ts +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/store/grader/grader.logic.api.ts @@ -278,6 +278,7 @@ export const studentScoresTag = (assignmentId: number, sid: string): string => export const gradebookCsvFilename = (courseName: string, date: Date = new Date()): string => { const safe = (courseName || "course").replace(/[^a-zA-Z0-9_-]+/g, "-"); const stamp = date.toISOString().slice(0, 10); + return `gradebook-${safe}-${stamp}.csv`; }; @@ -349,6 +350,7 @@ export const graderApi = createApi({ entry.originalArgs as { assignmentId: number; questionId: number }, (draft) => { const row = draft.answers.find((a) => a.sid === sid); + if (row) { row.score = score; if (comment !== undefined) row.comment = comment; @@ -361,6 +363,7 @@ export const graderApi = createApi({ ) ) ); + try { await queryFulfilled; @@ -466,9 +469,11 @@ export const graderApi = createApi({ const patch = dispatch( assignmentApi.util.updateQueryData("getAssignments", undefined, (draft) => { const target = draft.find((a) => a.id === assignment_id); + if (target) target.released = released; }) ); + try { await queryFulfilled; dispatch(assignmentApi.util.invalidateTags([{ type: "Assignments" }])); @@ -488,9 +493,11 @@ export const graderApi = createApi({ const patch = dispatch( assignmentApi.util.updateQueryData("getAssignments", undefined, (draft) => { const target = draft.find((a) => a.id === assignment_id); + if (target) target.threshold_pct = threshold_pct; }) ); + try { await queryFulfilled; dispatch(assignmentApi.util.invalidateTags([{ type: "Assignments" }])); diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/store/readings/readings.logic.api.spec.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/store/readings/readings.logic.api.spec.ts index 45ad15eab..5990ffb00 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/store/readings/readings.logic.api.spec.ts +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/store/readings/readings.logic.api.spec.ts @@ -1,10 +1,12 @@ import { configureStore } from "@reduxjs/toolkit"; -import { readingsApi, useGetAvailableReadingsQuery } from "./readings.logic.api"; import { readingsSlice } from "@store/readings/readings.logic"; + import type { DetailResponse } from "@/types/api"; import type { GetAvailableReadingsPayload } from "@/types/readings"; import type { TreeNode } from "@/types/treeNode"; +import { readingsApi, useGetAvailableReadingsQuery } from "./readings.logic.api"; + vi.mock("@components/ui/notify", () => ({ notify: { show: vi.fn(), @@ -59,18 +61,21 @@ describe("readingsApi store integration", () => { it("initial readingsApi state has a queries key", () => { const store = buildStore(); const apiState = store.getState()[readingsApi.reducerPath]; + expect(apiState).toHaveProperty("queries"); }); it("initial readingsApi state has a mutations key", () => { const store = buildStore(); const apiState = store.getState()[readingsApi.reducerPath]; + expect(apiState).toHaveProperty("mutations"); }); it("readings slice starts with empty arrays alongside readingsApi", () => { const store = buildStore(); const readingsState = store.getState().readings; + expect(readingsState.availableReadings).toEqual([]); expect(readingsState.selectedReadings).toEqual([]); }); @@ -79,6 +84,7 @@ describe("readingsApi store integration", () => { describe("getAvailableReadings endpoint", () => { it("endpoint has a select method", () => { const endpoint = readingsApi.endpoints.getAvailableReadings; + expect(typeof (endpoint as any).select).toBe("function"); }); @@ -96,6 +102,7 @@ describe("getAvailableReadings endpoint", () => { }; const result = queryFn(payload); + expect(result.method).toBe("POST"); expect(result.url).toBe("/assignment/instructor/fetch_chooser_data"); expect(result.body).toBe(payload); @@ -113,6 +120,7 @@ describe("getAvailableReadings transformResponse", () => { }; const result = response.detail.questions; + expect(result).toHaveLength(3); expect(result).toBe(nodes); }); @@ -123,6 +131,7 @@ describe("getAvailableReadings transformResponse", () => { }; const result = response.detail.questions; + expect(result).toEqual([]); }); @@ -135,6 +144,7 @@ describe("getAvailableReadings transformResponse", () => { }; const result = response.detail.questions; + expect(result[0].key).toBe("ch1"); expect(result[0].label).toBe("Chapter 1"); expect(result[0].children).toHaveLength(1); diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/store/readings/readings.logic.api.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/store/readings/readings.logic.api.ts index fca4bc486..2bf0076fa 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/store/readings/readings.logic.api.ts +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/store/readings/readings.logic.api.ts @@ -1,8 +1,8 @@ +import { notify } from "@components/ui/notify"; import { createApi } from "@reduxjs/toolkit/query/react"; import { baseQuery } from "@store/baseQuery"; import { exercisesActions } from "@store/exercises/exercises.logic"; import { readingsActions } from "@store/readings/readings.logic"; -import { notify } from "@components/ui/notify"; import { DetailResponse } from "@/types/api"; import { GetAvailableReadingsPayload } from "@/types/readings"; diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/store/readings/readings.logic.spec.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/store/readings/readings.logic.spec.ts index c7fe7f880..4cd1035a9 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/store/readings/readings.logic.spec.ts +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/store/readings/readings.logic.spec.ts @@ -97,6 +97,7 @@ describe("readingsActions.setAvailableReadings", () => { store.dispatch(readingsActions.setAvailableReadings(nodes)); const stored = store.getState().readings.availableReadings; + expect(stored).toHaveLength(2); expect(stored[0].key).toBe("n1"); expect(stored[1].key).toBe("n2"); @@ -109,6 +110,7 @@ describe("readingsActions.setAvailableReadings", () => { store.dispatch(readingsActions.setAvailableReadings(nodes)); const stored = store.getState().readings.availableReadings; + expect(stored).toHaveLength(1); expect(stored[0].key).toBe("n1"); }); @@ -122,6 +124,7 @@ describe("readingsActions.setAvailableReadings", () => { store.dispatch(readingsActions.setAvailableReadings([parent])); const stored = store.getState().readings.availableReadings; + expect(stored).toHaveLength(1); expect(stored[0].children).toHaveLength(1); expect(stored[0].children![0].key).toBe("c1"); @@ -136,6 +139,7 @@ describe("readingsActions.setAvailableReadings", () => { store.dispatch(readingsActions.setAvailableReadings([parent])); const stored = store.getState().readings.availableReadings; + expect(stored).toHaveLength(1); expect(stored[0].key).toBe("p1"); expect(stored[0].children).toBeUndefined(); @@ -152,9 +156,11 @@ describe("readingsActions.setAvailableReadings", () => { }); const newNodes: TreeNode[] = [makeNode("new", "New Node")]; + store.dispatch(readingsActions.setAvailableReadings(newNodes)); const stored = store.getState().readings.availableReadings; + expect(stored).toHaveLength(1); expect(stored[0].key).toBe("new"); }); diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/store/searchExercises/searchExercises.logic.spec.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/store/searchExercises/searchExercises.logic.spec.ts index f8e1d644d..17e2794a8 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/store/searchExercises/searchExercises.logic.spec.ts +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/store/searchExercises/searchExercises.logic.spec.ts @@ -1,12 +1,13 @@ import { configureStore } from "@reduxjs/toolkit"; +import type { Exercise } from "@/types/exercises"; + import { searchExercisesSlice, searchExercisesActions, searchExercisesSelectors, SearchExercisesState } from "./searchExercises.logic"; -import type { Exercise } from "@/types/exercises"; const makeStore = (preloadedState?: { searchExercises: SearchExercisesState }) => configureStore({ @@ -52,6 +53,7 @@ const makeExercise = (overrides: Partial = {}): Exercise => ({ describe("searchExercisesSlice reducer", () => { it("returns the initial state with empty selectedExercises when no action is dispatched", () => { const store = makeStore(); + expect(store.getState().searchExercises.selectedExercises).toEqual([]); }); @@ -90,6 +92,7 @@ describe("searchExercisesSelectors.getSelectedExercises", () => { it("returns an empty array from the initial state", () => { const store = makeStore(); const result = searchExercisesSelectors.getSelectedExercises(store.getState() as any); + expect(result).toEqual([]); }); @@ -100,6 +103,7 @@ describe("searchExercisesSelectors.getSelectedExercises", () => { store.dispatch(searchExercisesActions.setSelectedExercises(exercises)); const result = searchExercisesSelectors.getSelectedExercises(store.getState() as any); + expect(result).toEqual(exercises); }); @@ -110,6 +114,7 @@ describe("searchExercisesSelectors.getSelectedExercises", () => { store.dispatch(searchExercisesActions.setSelectedExercises(exercises)); const result = searchExercisesSelectors.getSelectedExercises(store.getState() as any); + expect(result).toHaveLength(1); expect(result[0].id).toBe(7); }); diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/store/store.spec.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/store/store.spec.ts index 74197225f..c3f9ee328 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/store/store.spec.ts +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/store/store.spec.ts @@ -3,12 +3,14 @@ import { setupStore, store } from "@store/store"; describe("setupStore", () => { it("creates a store with a defined getState function", () => { const s = setupStore(); + expect(s.getState).toBeDefined(); expect(typeof s.getState).toBe("function"); }); it("creates a store with a defined dispatch function", () => { const s = setupStore(); + expect(s.dispatch).toBeDefined(); expect(typeof s.dispatch).toBe("function"); }); @@ -16,6 +18,7 @@ describe("setupStore", () => { it("returns initial state containing all expected top-level slices", () => { const s = setupStore(); const state = s.getState(); + expect(state).toHaveProperty("user"); expect(state).toHaveProperty("assignment"); expect(state).toHaveProperty("readings"); @@ -29,12 +32,14 @@ describe("setupStore", () => { it("applies preloaded state when provided", () => { const s = setupStore({ user: { loggedIn: false, userData: null } as never }); const state = s.getState(); + expect((state.user as { loggedIn: boolean }).loggedIn).toBe(false); }); it("returns a new store instance on every call", () => { const s1 = setupStore(); const s2 = setupStore(); + expect(s1).not.toBe(s2); }); }); diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/store/user/userLogic.spec.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/store/user/userLogic.spec.ts index 27ce625dc..2832d1904 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/store/user/userLogic.spec.ts +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/store/user/userLogic.spec.ts @@ -1,4 +1,5 @@ import { configureStore } from "@reduxjs/toolkit"; + import { userSlice, userActions, userSelectors } from "./userLogic"; const buildStore = (preloadedState?: { user: { isAuthorized: boolean } }) => diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/utils/courseTimezone.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/utils/courseTimezone.ts index f32be7513..7f4bd05da 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/utils/courseTimezone.ts +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/utils/courseTimezone.ts @@ -70,17 +70,20 @@ export const getCourseTimezoneMismatch = ( browserTimezone: string = getBrowserTimezone() ): CourseTimezoneMismatch | null => { const course = courseTimezone || "UTC"; + if (!browserTimezone || browserTimezone === course) { return null; } const courseOffset = offsetMinutes(course, at); const browserOffset = offsetMinutes(browserTimezone, at); + if (courseOffset === null || browserOffset === null) { return null; } const diff = browserOffset - courseOffset; + if (diff === 0) { return null; } diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/utils/date.spec.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/utils/date.spec.ts index 708b9d150..e41ac90b2 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/utils/date.spec.ts +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/utils/date.spec.ts @@ -10,18 +10,21 @@ describe("convertDateToISO", () => { it("returns a UTC ISO string without the trailing Z", () => { const date = new Date("2026-02-24T15:00:00Z"); const result = convertDateToISO(date); + expect(result).toBe("2026-02-24T15:00:00"); }); it("trims milliseconds from the result", () => { const date = new Date("2026-02-24T15:00:00.999Z"); const result = convertDateToISO(date); + expect(result).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$/); expect(result).toBe("2026-02-24T15:00:00"); }); it("returns exactly 19 characters", () => { const date = new Date("2000-01-01T00:00:00Z"); + expect(convertDateToISO(date)).toHaveLength(19); }); }); @@ -45,6 +48,7 @@ describe("getDatePickerFormat", () => { it("returns a string for unknown or empty locale string", () => { const result = getDatePickerFormat("xx-XX"); + expect(typeof result).toBe("string"); expect(result.length).toBeGreaterThan(0); }); @@ -53,6 +57,7 @@ describe("getDatePickerFormat", () => { describe("parseUTCDate", () => { it("appends Z and parses a naive UTC string as UTC", () => { const result = parseUTCDate("2026-02-24T15:00:00"); + expect(result.getUTCFullYear()).toBe(2026); expect(result.getUTCMonth()).toBe(1); expect(result.getUTCDate()).toBe(24); @@ -63,16 +68,19 @@ describe("parseUTCDate", () => { it("parses a string that already ends with Z without modification", () => { const result = parseUTCDate("2026-02-24T15:00:00Z"); + expect(result.getUTCHours()).toBe(15); }); it("parses a string with positive timezone offset as-is", () => { const result = parseUTCDate("2026-02-24T17:00:00+02:00"); + expect(result.getUTCHours()).toBe(15); }); it("parses a string with negative timezone offset as-is", () => { const result = parseUTCDate("2026-02-24T10:00:00-05:00"); + expect(result.getUTCHours()).toBe(15); }); @@ -84,6 +92,7 @@ describe("parseUTCDate", () => { describe("formatUTCDateForDisplay", () => { it("returns a non-empty string for a valid UTC naive string", () => { const result = formatUTCDateForDisplay("2026-02-24T15:00:00"); + expect(typeof result).toBe("string"); expect(result.length).toBeGreaterThan(0); }); @@ -94,12 +103,14 @@ describe("formatUTCDateForDisplay", () => { month: "2-digit", day: "2-digit" }); + expect(typeof result).toBe("string"); expect(result.length).toBeGreaterThan(0); }); it("returns a string for a string that already ends with Z", () => { const result = formatUTCDateForDisplay("2026-02-24T15:00:00Z"); + expect(typeof result).toBe("string"); }); }); @@ -107,12 +118,14 @@ describe("formatUTCDateForDisplay", () => { describe("formatUTCDateLocaleString", () => { it("returns a non-empty locale string for a valid UTC naive string", () => { const result = formatUTCDateLocaleString("2026-02-24T15:00:00"); + expect(typeof result).toBe("string"); expect(result.length).toBeGreaterThan(0); }); it("returns a string for a UTC string ending with Z", () => { const result = formatUTCDateLocaleString("2026-02-24T15:00:00Z"); + expect(typeof result).toBe("string"); expect(result.length).toBeGreaterThan(0); }); diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/utils/date.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/utils/date.ts index 0b9af3993..84d106a2e 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/utils/date.ts +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/utils/date.ts @@ -32,6 +32,7 @@ export const formatUTCDateForDisplay = ( options?: Intl.DateTimeFormatOptions ): string => { const date = parseUTCDate(utcString); + return date.toLocaleDateString(undefined, options); }; @@ -40,5 +41,6 @@ export const formatUTCDateForDisplay = ( */ export const formatUTCDateLocaleString = (utcString: string): string => { const date = parseUTCDate(utcString); + return date.toLocaleString(); }; diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/utils/exercise.spec.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/utils/exercise.spec.ts index e1e2c036f..f4e352b0a 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/utils/exercise.spec.ts +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/utils/exercise.spec.ts @@ -65,6 +65,7 @@ const makeExercise = (overrides: Partial = {}): Exercise => ({ describe("createExerciseId", () => { it("returns a string matching the expected pattern", () => { const id = createExerciseId(); + expect(id).toMatch(/^exercise_\d{8}_\d{1,4}$/); }); @@ -76,11 +77,13 @@ describe("createExerciseId", () => { const expectedDate = `${year}${month}${day}`; const id = createExerciseId(); + expect(id).toContain(`exercise_${expectedDate}_`); }); it("generates unique ids on repeated calls", () => { const ids = new Set(Array.from({ length: 20 }, () => createExerciseId())); + expect(ids.size).toBeGreaterThan(1); }); }); @@ -89,6 +92,7 @@ describe("getLeafNodes", () => { it("returns all nodes when tree has no children", () => { const tree = [makeLeaf("a"), makeLeaf("b")]; const leaves = getLeafNodes(tree); + expect(leaves).toHaveLength(2); expect(leaves.map((n) => n.key)).toEqual(["a", "b"]); }); @@ -96,6 +100,7 @@ describe("getLeafNodes", () => { it("returns only leaf nodes from a nested tree", () => { const tree = [makeParent("root", [makeLeaf("child1"), makeLeaf("child2")])]; const leaves = getLeafNodes(tree); + expect(leaves).toHaveLength(2); expect(leaves.map((n) => n.key)).toEqual(["child1", "child2"]); }); @@ -103,6 +108,7 @@ describe("getLeafNodes", () => { it("handles deeply nested trees", () => { const tree = [makeParent("level1", [makeParent("level2", [makeLeaf("deep_leaf")])])]; const leaves = getLeafNodes(tree); + expect(leaves).toHaveLength(1); expect(leaves[0].key).toBe("deep_leaf"); }); @@ -114,6 +120,7 @@ describe("getLeafNodes", () => { it("handles mixed parent and leaf nodes at root level", () => { const tree = [makeLeaf("leaf_at_root"), makeParent("parent", [makeLeaf("child")])]; const leaves = getLeafNodes(tree); + expect(leaves).toHaveLength(2); expect(leaves.map((n) => n.key)).toEqual(["leaf_at_root", "child"]); }); @@ -124,40 +131,46 @@ describe("getSelectedKeys", () => { const tree = [makeLeaf("ex1")]; const exercises = [makeExercise({ name: "ex1" })]; const result = getSelectedKeys(tree, exercises); - expect(result["ex1"]).toEqual({ checked: true, partialChecked: false }); + + expect(result.ex1).toEqual({ checked: true, partialChecked: false }); }); it("marks leaf node as unchecked when its key does not match any exercise", () => { const tree = [makeLeaf("ex2")]; const exercises = [makeExercise({ name: "ex1" })]; const result = getSelectedKeys(tree, exercises); - expect(result["ex2"]).toEqual({ checked: false, partialChecked: false }); + + expect(result.ex2).toEqual({ checked: false, partialChecked: false }); }); it("marks all nodes checked when checkedByDefault is true", () => { const tree = [makeLeaf("any_key")]; const result = getSelectedKeys(tree, [], true); - expect(result["any_key"]).toEqual({ checked: true, partialChecked: false }); + + expect(result.any_key).toEqual({ checked: true, partialChecked: false }); }); it("marks parent as fully checked when all children are checked", () => { const tree = [makeParent("parent", [makeLeaf("child1"), makeLeaf("child2")])]; const exercises = [makeExercise({ name: "child1" }), makeExercise({ id: 2, name: "child2" })]; const result = getSelectedKeys(tree, exercises); - expect(result["parent"]).toEqual({ checked: true, partialChecked: false }); + + expect(result.parent).toEqual({ checked: true, partialChecked: false }); }); it("marks parent as partialChecked when only some children are checked", () => { const tree = [makeParent("parent", [makeLeaf("child1"), makeLeaf("child2")])]; const exercises = [makeExercise({ name: "child1" })]; const result = getSelectedKeys(tree, exercises); - expect(result["parent"]).toEqual({ checked: false, partialChecked: true }); + + expect(result.parent).toEqual({ checked: false, partialChecked: true }); }); it("marks parent as unchecked when no children are checked", () => { const tree = [makeParent("parent", [makeLeaf("child1"), makeLeaf("child2")])]; const result = getSelectedKeys(tree, []); - expect(result["parent"]).toEqual({ checked: false, partialChecked: false }); + + expect(result.parent).toEqual({ checked: false, partialChecked: false }); }); it("propagates partial check state up the tree", () => { @@ -166,8 +179,9 @@ describe("getSelectedKeys", () => { ]; const exercises = [makeExercise({ name: "child1" })]; const result = getSelectedKeys(tree, exercises); - expect(result["parent"]).toEqual({ checked: false, partialChecked: true }); - expect(result["grandparent"]).toEqual({ checked: false, partialChecked: true }); + + expect(result.parent).toEqual({ checked: false, partialChecked: true }); + expect(result.grandparent).toEqual({ checked: false, partialChecked: true }); }); it("returns empty object for empty tree", () => { @@ -182,6 +196,7 @@ describe("filterAvailableExercises", () => { makeLeaf("q2", { question_type: "page" }) ]; const result = filterAvailableExercises(nodes); + expect(result).toHaveLength(1); expect(result[0].key).toBe("q1"); }); @@ -189,6 +204,7 @@ describe("filterAvailableExercises", () => { it("adds level property to each node", () => { const nodes = [makeLeaf("q1", { question_type: "mchoice" })]; const result = filterAvailableExercises(nodes); + expect((result[0] as any).level).toBe(0); }); @@ -199,6 +215,7 @@ describe("filterAvailableExercises", () => { }) ]; const result = filterAvailableExercises(nodes); + expect((result[0] as any).level).toBe(0); expect((result[0].children![0] as any).level).toBe(1); }); @@ -210,6 +227,7 @@ describe("filterAvailableExercises", () => { }) ]; const result = filterAvailableExercises(nodes); + expect(result[0].disabled).toBe(true); }); @@ -220,12 +238,14 @@ describe("filterAvailableExercises", () => { }) ]; const result = filterAvailableExercises(nodes); + expect(result[0].disabled).toBe(false); }); it("sets disabled to false for leaf nodes", () => { const nodes = [makeLeaf("q1", { question_type: "mchoice" })]; const result = filterAvailableExercises(nodes); + expect(result[0].disabled).toBe(false); }); @@ -238,6 +258,7 @@ describe("removeChildrenWithoutTitleImmutable", () => { it("removes nodes without a title", () => { const nodes = [makeLeaf("q1", { title: "Title One" }), { key: "q2", data: {} }]; const result = removeChildrenWithoutTitleImmutable(nodes); + expect(result).toHaveLength(1); expect(result[0].key).toBe("q1"); }); @@ -249,6 +270,7 @@ describe("removeChildrenWithoutTitleImmutable", () => { children: [makeLeaf("child1", { title: "Child Title" }), { key: "child2", data: {} }] }; const result = removeChildrenWithoutTitleImmutable([parent]); + expect(result).toHaveLength(1); expect(result[0].children).toHaveLength(1); expect(result[0].children![0].key).toBe("child1"); @@ -261,6 +283,7 @@ describe("removeChildrenWithoutTitleImmutable", () => { children: [{ key: "child", data: {} }] }; const result = removeChildrenWithoutTitleImmutable([parent]); + expect(result).toHaveLength(1); expect(result[0].children).toBeUndefined(); }); @@ -268,6 +291,7 @@ describe("removeChildrenWithoutTitleImmutable", () => { it("does not mutate original nodes", () => { const original: TreeNode[] = [makeLeaf("q1", { title: "Title" })]; const copy = JSON.parse(JSON.stringify(original)); + removeChildrenWithoutTitleImmutable(original); expect(original).toEqual(copy); }); @@ -284,6 +308,7 @@ describe("getExercisesWithoutReadings", () => { makeExercise({ id: 2, name: "normal", reading_assignment: false }) ]; const result = getExercisesWithoutReadings(exercises); + expect(result).toHaveLength(1); expect(result[0].name).toBe("normal"); }); @@ -294,6 +319,7 @@ describe("getExercisesWithoutReadings", () => { makeExercise({ id: 2, name: "normal" }) ]; const result = getExercisesWithoutReadings(exercises); + expect(result).toHaveLength(1); expect(result[0].name).toBe("normal"); }); @@ -305,6 +331,7 @@ describe("getExercisesWithoutReadings", () => { makeExercise({ id: 3, name: "second", sorting_priority: 2 }) ]; const result = getExercisesWithoutReadings(exercises); + expect(result.map((e) => e.name)).toEqual(["first", "second", "third"]); }); @@ -313,6 +340,7 @@ describe("getExercisesWithoutReadings", () => { makeExercise({ reading_assignment: true }), makeExercise({ id: 2, question_type: "page" }) ]; + expect(getExercisesWithoutReadings(exercises)).toEqual([]); }); }); @@ -320,6 +348,7 @@ describe("getExercisesWithoutReadings", () => { describe("filterExercisesByQuestionType", () => { it("returns all nodes when selectedQuestionTypes is empty", () => { const nodes = [makeLeaf("q1"), makeLeaf("q2")]; + expect(filterExercisesByQuestionType(nodes, [])).toEqual(nodes); }); @@ -329,6 +358,7 @@ describe("filterExercisesByQuestionType", () => { makeLeaf("q2", { question_type: "shortanswer" }) ]; const result = filterExercisesByQuestionType(nodes, ["mchoice"]); + expect(result).toHaveLength(1); expect(result[0].key).toBe("q1"); }); @@ -341,6 +371,7 @@ describe("filterExercisesByQuestionType", () => { ]) ]; const result = filterExercisesByQuestionType(nodes, ["mchoice"]); + expect(result).toHaveLength(1); expect(result[0].children).toHaveLength(1); expect(result[0].children![0].key).toBe("child1"); @@ -349,6 +380,7 @@ describe("filterExercisesByQuestionType", () => { it("removes parent nodes when none of their children match", () => { const nodes = [makeParent("parent", [makeLeaf("child", { question_type: "shortanswer" })])]; const result = filterExercisesByQuestionType(nodes, ["mchoice"]); + expect(result).toHaveLength(0); }); @@ -364,6 +396,7 @@ describe("filterOutExercisesByQuestionType", () => { makeLeaf("q2", { question_type: "page" }) ]; const result = filterOutExercisesByQuestionType(nodes, ["page"]); + expect(result).toHaveLength(1); expect(result[0].key).toBe("q1"); }); @@ -376,6 +409,7 @@ describe("filterOutExercisesByQuestionType", () => { ]) ]; const result = filterOutExercisesByQuestionType(nodes, ["page"]); + expect(result[0].children).toHaveLength(1); expect(result[0].children![0].key).toBe("child1"); }); @@ -383,6 +417,7 @@ describe("filterOutExercisesByQuestionType", () => { it("keeps all nodes when exclusion list is empty", () => { const nodes = [makeLeaf("q1"), makeLeaf("q2")]; const result = filterOutExercisesByQuestionType(nodes, []); + expect(result).toHaveLength(2); }); @@ -395,17 +430,20 @@ describe("filterExercisesByFromSource", () => { it("returns all nodes unchanged when fromSourceOnly is false", () => { const nodes = [makeLeaf("q1", { from_source: false }), makeLeaf("q2", { from_source: true })]; const result = filterExercisesByFromSource(nodes, false); + expect(result).toHaveLength(2); }); it("returns all nodes unchanged when fromSourceOnly is omitted", () => { const nodes = [makeLeaf("q1"), makeLeaf("q2")]; + expect(filterExercisesByFromSource(nodes)).toHaveLength(2); }); it("keeps only leaf nodes with from_source true when fromSourceOnly is true", () => { const nodes = [makeLeaf("q1", { from_source: true }), makeLeaf("q2", { from_source: false })]; const result = filterExercisesByFromSource(nodes, true); + expect(result).toHaveLength(1); expect(result[0].key).toBe("q1"); }); @@ -413,6 +451,7 @@ describe("filterExercisesByFromSource", () => { it("removes parent node when all children have from_source false", () => { const nodes = [makeParent("parent", [makeLeaf("child", { from_source: false })])]; const result = filterExercisesByFromSource(nodes, true); + expect(result).toHaveLength(0); }); @@ -424,6 +463,7 @@ describe("filterExercisesByFromSource", () => { ]) ]; const result = filterExercisesByFromSource(nodes, true); + expect(result).toHaveLength(1); expect(result[0].children).toHaveLength(1); expect(result[0].children![0].key).toBe("child1"); diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/utils/htmlRegeneration.spec.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/utils/htmlRegeneration.spec.ts index 56ff174f6..3f8053564 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/utils/htmlRegeneration.spec.ts +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/utils/htmlRegeneration.spec.ts @@ -1,14 +1,15 @@ -import { regenerateHtmlSrc } from "./htmlRegeneration"; import { Exercise } from "@/types/exercises"; -import * as multiChoice from "@/utils/preview/multichoice"; -import * as fillInTheBlank from "@/utils/preview/fillInTheBlank"; -import * as parsonsPreview from "@/utils/preview/parsonsPreview"; import * as activeCode from "@/utils/preview/activeCode"; -import * as shortAnswer from "@/utils/preview/shortAnswer"; -import * as matchingPreview from "@/utils/preview/matchingPreview"; import * as dndPreview from "@/utils/preview/dndPreview"; -import * as poll from "@/utils/preview/poll"; +import * as fillInTheBlank from "@/utils/preview/fillInTheBlank"; import * as iframePreview from "@/utils/preview/iframePreview"; +import * as matchingPreview from "@/utils/preview/matchingPreview"; +import * as multiChoice from "@/utils/preview/multichoice"; +import * as parsonsPreview from "@/utils/preview/parsonsPreview"; +import * as poll from "@/utils/preview/poll"; +import * as shortAnswer from "@/utils/preview/shortAnswer"; + +import { regenerateHtmlSrc } from "./htmlRegeneration"; const baseExercise: Exercise = { id: 1, @@ -382,7 +383,8 @@ describe("regenerateHtmlSrc", () => { describe("unsupported question type (default case)", () => { it("replaces id, data-component, data-question, and name attributes in existing HTML", () => { - const htmlSrc = `
    `; + const htmlSrc = + '
    '; const exercise: Exercise = { ...baseExercise, question_type: "clickablearea", @@ -393,14 +395,14 @@ describe("regenerateHtmlSrc", () => { const result = regenerateHtmlSrc(exercise, "new_name"); - expect(result).toContain(`id="new_name"`); - expect(result).toContain(`data-component="new_name"`); - expect(result).toContain(`data-question="new_name"`); - expect(result).not.toContain(`id="old_name"`); + expect(result).toContain('id="new_name"'); + expect(result).toContain('data-component="new_name"'); + expect(result).toContain('data-question="new_name"'); + expect(result).not.toContain('id="old_name"'); }); it("replaces multiple occurrences of old name in HTML", () => { - const htmlSrc = `
    `; + const htmlSrc = '
    '; const exercise: Exercise = { ...baseExercise, question_type: "selectquestion", @@ -411,11 +413,11 @@ describe("regenerateHtmlSrc", () => { const result = regenerateHtmlSrc(exercise, "new_name"); - expect(result).toBe(`
    `); + expect(result).toBe('
    '); }); it("returns original htmlsrc when oldName is empty", () => { - const htmlSrc = `
    `; + const htmlSrc = '
    '; const exercise: Exercise = { ...baseExercise, question_type: "selectquestion", @@ -430,7 +432,7 @@ describe("regenerateHtmlSrc", () => { }); it("returns original htmlsrc when newName is empty", () => { - const htmlSrc = `
    `; + const htmlSrc = '
    '; const exercise: Exercise = { ...baseExercise, question_type: "selectquestion", @@ -450,7 +452,7 @@ describe("regenerateHtmlSrc", () => { vi.spyOn(multiChoice, "generateMultiChoicePreview").mockImplementation(() => { throw new Error("preview error"); }); - const htmlSrc = `
    `; + const htmlSrc = '
    '; const exercise: Exercise = { ...baseExercise, question_type: "mchoice", @@ -461,11 +463,11 @@ describe("regenerateHtmlSrc", () => { const result = regenerateHtmlSrc(exercise, "new_name"); - expect(result).toContain(`id="new_name"`); + expect(result).toContain('id="new_name"'); }); it("falls back to updateNameInHtml when question_json is invalid JSON", () => { - const htmlSrc = `
    `; + const htmlSrc = '
    '; const exercise: Exercise = { ...baseExercise, question_type: "mchoice", @@ -476,7 +478,7 @@ describe("regenerateHtmlSrc", () => { const result = regenerateHtmlSrc(exercise, "new_name"); - expect(result).toContain(`id="new_name"`); + expect(result).toContain('id="new_name"'); }); }); }); diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/utils/htmlRegeneration.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/utils/htmlRegeneration.ts index ada41a988..e457ab094 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/utils/htmlRegeneration.ts +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/utils/htmlRegeneration.ts @@ -1,14 +1,14 @@ import { Exercise, QuestionJSON } from "@/types/exercises"; -import { generateMultiChoicePreview } from "@/utils/preview/multichoice"; -import { generateFillInTheBlankPreview } from "@/utils/preview/fillInTheBlank"; -import { generateParsonsPreview } from "@/utils/preview/parsonsPreview"; +import { safeJsonParse } from "@/utils/json"; import { generateActiveCodePreview } from "@/utils/preview/activeCode"; -import { generateShortAnswerPreview } from "@/utils/preview/shortAnswer"; -import { generateMatchingPreview } from "@/utils/preview/matchingPreview"; import { generateDragAndDropPreview } from "@/utils/preview/dndPreview"; -import { generatePollPreview } from "@/utils/preview/poll"; +import { generateFillInTheBlankPreview } from "@/utils/preview/fillInTheBlank"; import { generateIframePreview } from "@/utils/preview/iframePreview"; -import { safeJsonParse } from "@/utils/json"; +import { generateMatchingPreview } from "@/utils/preview/matchingPreview"; +import { generateMultiChoicePreview } from "@/utils/preview/multichoice"; +import { generateParsonsPreview } from "@/utils/preview/parsonsPreview"; +import { generatePollPreview } from "@/utils/preview/poll"; +import { generateShortAnswerPreview } from "@/utils/preview/shortAnswer"; /** * Regenerates HTML source for a copied exercise with the new name diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/utils/json.spec.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/utils/json.spec.ts index 8ad52e2b1..b71064079 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/utils/json.spec.ts +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/utils/json.spec.ts @@ -5,18 +5,21 @@ describe("safeJsonParse", () => { it("returns the object as-is when passed a plain object", () => { const input = { foo: "bar", count: 42 }; const result = safeJsonParse(input); + expect(result).toBe(input); }); it("returns the array as-is when passed an array", () => { const input = [1, 2, 3]; const result = safeJsonParse(input); + expect(result).toBe(input); }); it("returns the nested object as-is", () => { const input = { nested: { a: 1 } }; const result = safeJsonParse(input); + expect(result).toBe(input); }); }); @@ -24,31 +27,37 @@ describe("safeJsonParse", () => { describe("when given a valid JSON string", () => { it("parses a JSON object string and returns the parsed object", () => { const result = safeJsonParse<{ name: string }>('{"name":"Alice"}'); + expect(result).toEqual({ name: "Alice" }); }); it("parses a JSON array string and returns the parsed array", () => { const result = safeJsonParse("[1,2,3]"); + expect(result).toEqual([1, 2, 3]); }); it("parses a JSON number string and returns the number", () => { const result = safeJsonParse("42"); + expect(result).toBe(42); }); it("parses a JSON boolean string and returns the boolean", () => { const result = safeJsonParse("true"); + expect(result).toBe(true); }); it("parses a JSON null string and returns null", () => { const result = safeJsonParse("null"); + expect(result).toBeNull(); }); it("parses a JSON string value and returns the string", () => { const result = safeJsonParse('"hello"'); + expect(result).toBe("hello"); }); }); @@ -56,21 +65,25 @@ describe("safeJsonParse", () => { describe("when given an invalid JSON string", () => { it("returns undefined for a malformed JSON string", () => { const result = safeJsonParse("{invalid json}"); + expect(result).toBeUndefined(); }); it("returns undefined for an empty string", () => { const result = safeJsonParse(""); + expect(result).toBeUndefined(); }); it("returns undefined for a plain text string", () => { const result = safeJsonParse("not json"); + expect(result).toBeUndefined(); }); it("returns undefined for an unterminated JSON string", () => { const result = safeJsonParse('{"key": "value"'); + expect(result).toBeUndefined(); }); }); @@ -78,6 +91,7 @@ describe("safeJsonParse", () => { describe("null handling", () => { it("parses a 'null' string and returns null (not object branch)", () => { const result = safeJsonParse("null"); + expect(result).toBeNull(); }); }); diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/utils/object.spec.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/utils/object.spec.ts index 363e58bdf..66c62e23d 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/utils/object.spec.ts +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/utils/object.spec.ts @@ -29,16 +29,19 @@ describe("isEmptyObject", () => { describe("dumpObject", () => { it("serializes a plain object with default spacing of 2", () => { const result = dumpObject({ a: 1 }); + expect(result).toBe(JSON.stringify({ a: 1 }, null, 2)); }); it("serializes a nested object", () => { const data = { outer: { inner: "value" } }; + expect(dumpObject(data)).toBe(JSON.stringify(data, null, 2)); }); it("serializes an array", () => { const data = [1, 2, 3]; + expect(dumpObject(data)).toBe(JSON.stringify(data, null, 2)); }); @@ -56,18 +59,22 @@ describe("dumpObject", () => { it("respects a custom spacing argument", () => { const data = { key: "val" }; + expect(dumpObject(data, 4)).toBe(JSON.stringify(data, null, 4)); }); it("serializes with spacing 0 producing compact JSON", () => { const data = { a: 1, b: 2 }; + expect(dumpObject(data, 0)).toBe(JSON.stringify(data, null, 0)); }); it("returns an error message when the data cannot be serialized", () => { const circular: Record = {}; + circular.self = circular; const result = dumpObject(circular); + expect(result).toMatch(/^Error: Unable to serialize the provided data/); }); }); diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/utils/preview/activeCode.spec.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/utils/preview/activeCode.spec.ts index cfda39a04..c1310fb85 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/utils/preview/activeCode.spec.ts +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/utils/preview/activeCode.spec.ts @@ -38,32 +38,38 @@ describe("generateActiveCodePreview", () => { describe("basic structure", () => { it("returns HTML containing the runestone activecode wrapper", () => { const html = call(); + expect(html).toContain('class="runestone explainer ac_section '); expect(html).toContain('data-component="activecode"'); }); it("embeds the sanitized name as the element id", () => { const html = call(); + expect(html).toContain('id="my-exercise"'); }); it("embeds the original name as the data-question_label", () => { const html = call(); + expect(html).toContain('data-question_label="my-exercise"'); }); it("renders the instructions inside the question div", () => { const html = call(); + expect(html).toContain("

    Write a function.

    "); }); it("sets the data-lang attribute to the provided language", () => { const html = call(); + expect(html).toContain('data-lang="python"'); }); it("includes prefix, starter, and suffix code separated by the expected delimiters", () => { const html = call(); + expect(html).toContain("# prefix"); expect(html).toContain("^^^^"); expect(html).toContain("pass"); @@ -75,11 +81,13 @@ describe("generateActiveCodePreview", () => { describe("name sanitization", () => { it("strips special characters from the name when used as id", () => { const html = generateActiveCodePreview("Instructions", "python", "", "", "", "hello world!"); + expect(html).toContain('id="helloworld"'); }); it("prepends id_ when name starts with a digit", () => { const html = generateActiveCodePreview("Instructions", "python", "", "", "", "123abc"); + expect(html).toContain('id="id_123abc"'); }); }); @@ -87,16 +95,19 @@ describe("generateActiveCodePreview", () => { describe("codelens attribute", () => { it("defaults to data-codelens=true when no options are provided", () => { const html = call(); + expect(html).toContain('data-codelens="true"'); }); it("sets data-codelens=true when enableCodelens is true", () => { const html = call({}, { codeTailorOptions: { enableCodelens: true } }); + expect(html).toContain('data-codelens="true"'); }); it("sets data-codelens=false when enableCodelens is false", () => { const html = call({}, { codeTailorOptions: { enableCodelens: false } }); + expect(html).toContain('data-codelens="false"'); }); }); @@ -104,21 +115,25 @@ describe("generateActiveCodePreview", () => { describe("stdin attribute", () => { it("does not include data-stdin when stdin is undefined", () => { const html = call(); + expect(html).not.toContain("data-stdin"); }); it("does not include data-stdin when stdin is an empty string", () => { const html = call({}, { stdin: "" }); + expect(html).not.toContain("data-stdin"); }); it("does not include data-stdin when stdin is whitespace only", () => { const html = call({}, { stdin: " " }); + expect(html).not.toContain("data-stdin"); }); it("includes data-stdin with the provided value when stdin has content", () => { const html = call({}, { stdin: "hello input" }); + expect(html).toContain('data-stdin="hello input"'); }); }); @@ -126,16 +141,19 @@ describe("generateActiveCodePreview", () => { describe("data file attribute", () => { it("does not include data-datafile when selectedDataFiles is undefined", () => { const html = call(); + expect(html).not.toContain("data-datafile"); }); it("does not include data-datafile when selectedDataFiles is empty", () => { const html = call({}, { selectedDataFiles: [] }); + expect(html).not.toContain("data-datafile"); }); it("includes a single acid in data-datafile", () => { const html = call({}, { selectedDataFiles: [{ acid: "file1.csv" }] }); + expect(html).toContain('data-datafile="file1.csv"'); }); @@ -146,6 +164,7 @@ describe("generateActiveCodePreview", () => { selectedDataFiles: [{ acid: "file1.csv" }, { acid: "file2.csv" }, { acid: "file3.csv" }] } ); + expect(html).toContain('data-datafile="file1.csv,file2.csv,file3.csv"'); }); }); @@ -161,6 +180,7 @@ describe("generateActiveCodePreview", () => { } } ); + expect(html).not.toContain("data-parsonspersonalize"); expect(html).not.toContain("data-parsonsexample"); }); @@ -175,6 +195,7 @@ describe("generateActiveCodePreview", () => { } } ); + expect(html).not.toContain("data-parsonspersonalize"); }); @@ -188,6 +209,7 @@ describe("generateActiveCodePreview", () => { } } ); + expect(html).toContain('data-parsonspersonalize="movable"'); expect(html).toContain('data-parsonsexample="LLM-example"'); }); @@ -203,6 +225,7 @@ describe("generateActiveCodePreview", () => { } } ); + expect(html).toContain('data-parsonspersonalize="partial"'); expect(html).toContain('data-parsonsexample="my-example"'); }); @@ -218,6 +241,7 @@ describe("generateActiveCodePreview", () => { } } ); + expect(html).toContain('data-parsonsexample="LLM-example"'); }); }); diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/utils/preview/activeCode.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/utils/preview/activeCode.ts index b272d8bae..ae2afc86a 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/utils/preview/activeCode.ts +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/utils/preview/activeCode.ts @@ -35,13 +35,15 @@ export const generateActiveCodePreview = ( // CodeTailor attributes let codeTailorAttrs = ""; + if (codeTailorOptions?.enableCodeTailor && codeTailorOptions?.parsonspersonalize) { codeTailorAttrs += ` data-parsonspersonalize="${codeTailorOptions.parsonspersonalize}"`; // If parsonsexample is provided, use it; otherwise default to LLM-example const parsonsExampleValue = codeTailorOptions.parsonsexample?.trim() || "LLM-example"; + codeTailorAttrs += ` data-parsonsexample="${parsonsExampleValue}"`; if (codeTailorOptions.parsonsPersonalized === false) { - codeTailorAttrs += ` data-parsons-personalized="false"`; + codeTailorAttrs += ' data-parsons-personalized="false"'; } } diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/utils/preview/dndPreview.spec.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/utils/preview/dndPreview.spec.ts index ff47efcfc..88421b457 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/utils/preview/dndPreview.spec.ts +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/utils/preview/dndPreview.spec.ts @@ -1,7 +1,7 @@ -import { generateDragAndDropPreview } from "./dndPreview"; - import { DEFAULT_INCORRECT_FEEDBACK } from "@/utils/questionJson"; +import { generateDragAndDropPreview } from "./dndPreview"; + const baseLeft = [ { id: "l1", label: "Apple" }, { id: "l2", label: "Banana" } @@ -223,6 +223,7 @@ describe("generateDragAndDropPreview", () => { }); const dropzoneMatches = [...result.matchAll(/data-subcomponent="dropzone"/g)]; + expect(dropzoneMatches.length).toBe(1); }); diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/utils/preview/dndPreview.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/utils/preview/dndPreview.ts index 1aef93042..eae13e520 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/utils/preview/dndPreview.ts +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/utils/preview/dndPreview.ts @@ -1,8 +1,9 @@ import { ItemWithLabel } from "@components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/DragAndDropExercise/types"; -import { sanitizeId } from "../sanitize"; import { DEFAULT_INCORRECT_FEEDBACK } from "@/utils/questionJson"; +import { sanitizeId } from "../sanitize"; + interface DragAndDropPreviewProps { left: ItemWithLabel[]; right: ItemWithLabel[]; diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/utils/preview/fillInTheBlank.spec.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/utils/preview/fillInTheBlank.spec.ts index d34f6a190..0e759161a 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/utils/preview/fillInTheBlank.spec.ts +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/utils/preview/fillInTheBlank.spec.ts @@ -1,9 +1,10 @@ -import { generateFillInTheBlankPreview } from "./fillInTheBlank"; import { BlankWithFeedback, GraderType } from "@/components/routes/AssignmentBuilder/components/exercises/components/CreateExercise/components/FillInTheBlankExercise/types"; +import { generateFillInTheBlankPreview } from "./fillInTheBlank"; + const makeBlank = (overrides: Partial = {}): BlankWithFeedback => ({ id: "blank-1", graderType: GraderType.STRING, @@ -71,6 +72,7 @@ describe("generateFillInTheBlankPreview", () => { }); const parsed = JSON.parse(extractJsonData(result)); + expect(parsed.problemHtml).toContain(''); expect(parsed.problemHtml).not.toContain("{blank}"); }); @@ -83,6 +85,7 @@ describe("generateFillInTheBlankPreview", () => { }); const parsed = JSON.parse(extractJsonData(result)); + expect(parsed.dyn_vars).toBeNull(); }); @@ -122,6 +125,7 @@ describe("generateFillInTheBlankPreview", () => { }); const parsed = JSON.parse(extractJsonData(result)); + expect(parsed.feedbackArray[0][0].feedback).toContain("Correct"); }); @@ -177,6 +181,7 @@ describe("generateFillInTheBlankPreview", () => { }); const parsed = JSON.parse(extractJsonData(result)); + expect(parsed.feedbackArray[0]).toHaveLength(1); }); @@ -193,6 +198,7 @@ describe("generateFillInTheBlankPreview", () => { }); const parsed = JSON.parse(extractJsonData(result)); + expect(parsed.feedbackArray[0][0].regex).toContain('\\"hi\\"'); }); }); @@ -232,6 +238,7 @@ describe("generateFillInTheBlankPreview", () => { }); const parsed = JSON.parse(extractJsonData(result)); + expect(parsed.feedbackArray[0][0].regexFlags).toBe(""); }); @@ -247,6 +254,7 @@ describe("generateFillInTheBlankPreview", () => { }); const parsed = JSON.parse(extractJsonData(result)); + expect(parsed.feedbackArray[0]).toHaveLength(1); }); }); @@ -287,6 +295,7 @@ describe("generateFillInTheBlankPreview", () => { }); const parsed = JSON.parse(extractJsonData(result)); + expect(parsed.feedbackArray[0][0].number).toEqual([2, 10]); }); @@ -303,6 +312,7 @@ describe("generateFillInTheBlankPreview", () => { }); const parsed = JSON.parse(extractJsonData(result)); + expect(parsed.feedbackArray[0]).toHaveLength(1); }); }); @@ -322,6 +332,7 @@ describe("generateFillInTheBlankPreview", () => { }); const parsed = JSON.parse(extractJsonData(result)); + expect(parsed.blankNames).toEqual({ x: 2 }); }); @@ -333,6 +344,7 @@ describe("generateFillInTheBlankPreview", () => { }); const parsed = JSON.parse(extractJsonData(result)); + expect(parsed.blankNames).toEqual({ x: 0 }); }); @@ -344,6 +356,7 @@ describe("generateFillInTheBlankPreview", () => { }); const parsed = JSON.parse(extractJsonData(result)); + expect(parsed.blankNames).toEqual({}); }); }); @@ -351,6 +364,7 @@ describe("generateFillInTheBlankPreview", () => { function extractJsonData(html: string): string { const match = html.match(/