From f745954b7c9ce1ea47fd7a0b9f397a718fa795c1 Mon Sep 17 00:00:00 2001 From: Leangseu Kim <83240113+leangseu-edx@users.noreply.github.com> Date: Wed, 14 Feb 2024 11:18:12 -0500 Subject: [PATCH] Lk/unit test (#185) * chore: add tests * test: confirm dialog * test: criterion container * test: file preview * test: update some unaccounted error * test: file upload * chore: linting * test: hotjar survey * test: info popover * test: instructions * chore: linting * test: modal actions * test: progress bar * test: prompt * test: action button and modal container * test: text response * test: assessment view * test: grade view view * chore: add more ignore list for coverage * test: update file card * test: xblock view * test: xblock studio view * test: submission view * chore: fix format date consistency * chore: add file size * chore: update linting --- jest.config.js | 2 + src/components/ActionButton.test.jsx | 28 ++ .../__snapshots__/index.test.jsx.snap | 47 +++ src/components/AppContainer/index.test.jsx | 59 +++ .../AssessmentActions.test.jsx | 72 ++++ .../__snapshots__/index.test.jsx.snap | 31 ++ .../OverallFeedback/index.jsx | 2 +- .../OverallFeedback/index.test.jsx | 58 +++ .../AssessmentActions.test.jsx.snap | 40 +++ .../__snapshots__/index.test.jsx.snap | 125 +++++++ .../EditableAssessment/index.test.jsx | 37 ++ .../AssessmentCriteria.test.jsx | 63 ++++ .../CollapsibleAssessment.test.jsx | 36 ++ .../ReadonlyAssessment/Feedback.test.jsx | 45 +++ .../ReadOnlyAssessment.test.jsx | 46 +++ .../AssessmentCriteria.test.jsx.snap | 40 +++ .../CollapsibleAssessment.test.jsx.snap | 34 ++ .../__snapshots__/Feedback.test.jsx.snap | 224 ++++++++++++ .../ReadOnlyAssessment.test.jsx.snap | 86 +++++ .../__snapshots__/index.test.jsx.snap | 41 +++ .../Assessment/ReadonlyAssessment/index.jsx | 4 +- .../ReadonlyAssessment/index.test.jsx | 43 +++ .../__snapshots__/index.test.jsx.snap | 11 + src/components/Assessment/index.jsx | 5 +- src/components/Assessment/index.test.jsx | 38 ++ .../Assessment/useAssessmentData.js | 7 +- .../Assessment/useAssessmentData.test.js | 47 +++ .../__snapshots__/index.test.jsx.snap | 27 ++ src/components/ConfirmDialog/index.test.jsx | 19 + .../CriterionFeedback.test.jsx | 86 +++++ .../GradedCriterion.test.jsx | 24 ++ .../RadioCriterion.test.jsx | 108 ++++++ .../ReviewCriterion.test.jsx | 34 ++ .../CriterionFeedback.test.jsx.snap | 39 ++ .../GradedCriterion.test.jsx.snap | 72 ++++ .../RadioCriterion.test.jsx.snap | 136 +++++++ .../ReviewCriterion.test.jsx.snap | 80 +++++ .../__snapshots__/index.test.jsx.snap | 102 ++++++ .../CriterionContainer/index.test.jsx | 44 +++ .../__snapshots__/index.test.jsx.snap | 28 ++ .../FileRenderer/Banners/ErrorBanner.test.jsx | 32 ++ .../Banners/LoadingBanner.test.jsx | 10 + .../__snapshots__/ErrorBanner.test.jsx.snap | 34 ++ .../__snapshots__/LoadingBanner.test.jsx.snap | 12 + .../BaseRenderers/ImageRenderer.test.jsx | 17 + .../BaseRenderers/PDFRenderer.test.jsx | 67 ++++ .../BaseRenderers/TXTRenderer.test.jsx | 20 ++ .../__snapshots__/ImageRenderer.test.jsx.snap | 11 + .../__snapshots__/PDFRenderer.test.jsx.snap | 135 +++++++ .../__snapshots__/TXTRenderer.test.jsx.snap | 9 + .../BaseRenderers/pdfHooks.test.js | 92 +++++ .../BaseRenderers/textHooks.test.js | 60 ++++ .../__snapshots__/index.test.jsx.snap | 28 ++ .../FileRenderer/FileCard/index.test.jsx | 20 ++ .../__snapshots__/index.test.jsx.snap | 47 +++ .../components/FileRenderer/hooks.test.js | 46 +++ .../components/FileRenderer/index.jsx | 9 +- .../components/FileRenderer/index.test.jsx | 62 ++++ src/components/FilePreview/index.test.jsx | 57 +++ src/components/FileUpload/ActionCell.test.jsx | 48 +++ .../FileUpload/FileDownload.test.jsx | 41 +++ .../FileUpload/FileMetaDisplay.test.jsx | 25 ++ .../FileUpload/UploadConfirmModal.jsx | 3 +- .../FileUpload/UploadConfirmModal.test.jsx | 41 +++ .../__snapshots__/ActionCell.test.jsx.snap | 17 + .../__snapshots__/FileDownload.test.jsx.snap | 56 +++ .../FileMetaDisplay.test.jsx.snap | 89 +++++ .../UploadConfirmModal.test.jsx.snap | 111 ++++++ .../__snapshots__/index.test.jsx.snap | 338 ++++++++++++++++++ src/components/FileUpload/hooks.js | 1 + src/components/FileUpload/hooks.test.js | 110 ++++++ src/components/FileUpload/index.jsx | 10 +- src/components/FileUpload/index.test.jsx | 127 +++++++ src/components/HotjarSurvey/index.jsx | 6 +- src/components/HotjarSurvey/index.test.jsx | 113 ++++++ .../__snapshots__/index.test.jsx.snap | 34 ++ src/components/InfoPopover/index.jsx | 2 +- src/components/InfoPopover/index.test.jsx | 20 ++ .../__snapshots__/index.test.jsx.snap | 19 + src/components/Instructions/index.jsx | 11 +- src/components/Instructions/index.test.jsx | 23 ++ ...sMessage.jsx => useInstructionsMessage.js} | 0 .../useInstructionsMessage.test.js | 74 ++++ .../__snapshots__/index.test.jsx.snap | 50 +++ .../hooks/useFinishedStateActions.test.js | 132 +++++++ .../hooks/useInProgressActions.test.js | 154 ++++++++ src/components/ModalActions/index.test.jsx | 70 ++++ src/components/ModalContainer.test.jsx | 47 +++ src/components/ProgressBar/ProgressStep.jsx | 2 +- .../ProgressBar/ProgressStep.test.jsx | 85 +++++ .../__snapshots__/ProgressStep.test.jsx.snap | 177 +++++++++ .../__snapshots__/index.test.jsx.snap | 111 ++++++ src/components/ProgressBar/hooks.test.js | 128 +++++++ src/components/ProgressBar/index.jsx | 2 +- src/components/ProgressBar/index.test.jsx | 68 ++++ .../Prompt/__snapshots__/index.test.jsx.snap | 68 ++++ src/components/Prompt/index.test.jsx | 78 ++++ .../__snapshots__/index.test.jsx.snap | 45 +++ src/components/TextResponse/index.test.jsx | 45 +++ .../__snapshots__/ActionButton.test.jsx.snap | 21 ++ .../ModalContainer.test.jsx.snap | 90 +++++ src/setupTest.js | 24 +- .../__snapshots__/index.test.jsx.snap | 45 +++ .../BaseAssessmentView/index.test.jsx | 24 ++ .../__snapshots__/index.test.jsx.snap | 86 +++++ src/views/AssessmentView/index.jsx | 2 +- src/views/AssessmentView/index.test.jsx | 67 ++++ src/views/AssessmentView/useAssessmentData.js | 7 +- .../AssessmentView/useAssessmentData.test.js | 73 ++++ src/views/GradeView/Content.test.jsx | 36 ++ src/views/GradeView/FinalGrade.jsx | 4 +- src/views/GradeView/FinalGrade.test.jsx | 59 +++ .../__snapshots__/Content.test.jsx.snap | 99 +++++ .../__snapshots__/FinalGrade.test.jsx.snap | 70 ++++ .../__snapshots__/index.test.jsx.snap | 133 +++++++ src/views/GradeView/index.test.jsx | 18 + .../SubmissionView/SubmissionPrompts.jsx | 41 ++- .../SubmissionView/SubmissionPrompts.test.jsx | 88 +++++ .../TextResponseEditor/LaTextPreview.test.jsx | 10 + .../__snapshots__/LaTextPreview.test.jsx.snap | 15 + .../SubmissionPrompts.test.jsx.snap | 72 ++++ .../__snapshots__/index.test.jsx.snap | 112 ++++++ .../hooks/useUploadedFilesData.js | 4 +- .../hooks/useUploadedFilesData.test.js | 46 +++ src/views/SubmissionView/index.jsx | 2 +- src/views/SubmissionView/index.test.jsx | 47 +++ .../__snapshots__/index.test.jsx.snap | 16 + .../StudioSchedule/FormatDateTime.jsx | 2 +- .../StudioSchedule/FormatDateTime.test.jsx | 15 + .../StudioSchedule/StepInfo.test.jsx | 17 + .../FormatDateTime.test.jsx.snap | 13 + .../__snapshots__/StepInfo.test.jsx.snap | 28 ++ .../__snapshots__/index.test.jsx.snap | 85 +++++ .../components/StudioSchedule/index.test.jsx | 61 ++++ .../components/StudioViewPrompt.jsx | 22 +- .../components/StudioViewPrompt.test.jsx | 36 ++ .../components/StudioViewRubric.jsx | 2 +- .../components/StudioViewRubric.test.jsx | 68 ++++ .../FileUploadConfig.test.jsx | 34 ++ .../RequiredConfig.test.jsx | 20 ++ .../FileUploadConfig.test.jsx.snap | 24 ++ .../RequiredConfig.test.jsx.snap | 19 + .../__snapshots__/index.test.jsx.snap | 127 +++++++ .../components/StudioViewSettings/index.jsx | 2 +- .../StudioViewSettings/index.test.jsx | 72 ++++ .../components/StudioViewSteps.test.jsx | 44 +++ .../components/StudioViewTitle.test.jsx | 44 +++ .../StudioViewPrompt.test.jsx.snap | 22 ++ .../StudioViewRubric.test.jsx.snap | 122 +++++++ .../StudioViewSteps.test.jsx.snap | 60 ++++ .../StudioViewTitle.test.jsx.snap | 33 ++ src/views/XBlockStudioView/index.jsx | 2 +- src/views/XBlockStudioView/index.test.jsx | 26 ++ .../Actions/__snapshots__/index.test.jsx.snap | 101 ++++++ src/views/XBlockView/Actions/index.jsx | 30 +- src/views/XBlockView/Actions/index.test.jsx | 107 ++++++ .../__snapshots__/index.test.jsx.snap | 9 + .../StatusRow/DueDateMessage/index.test.jsx | 15 + .../DueDateMessage/useDueDateMessage.js | 11 +- .../DueDateMessage/useDueDateMessage.test.js | 66 ++++ .../__snapshots__/index.test.jsx.snap | 9 + .../StatusRow/StatusBadge/index.test.jsx | 18 + .../StatusRow/StatusBadge/useBadgeConfig.js | 2 +- .../StatusBadge/useBadgeConfig.test.js | 62 ++++ .../__snapshots__/index.test.jsx.snap | 10 + src/views/XBlockView/StatusRow/index.test.jsx | 15 + .../__snapshots__/index.test.jsx.snap | 72 ++++ src/views/XBlockView/index.jsx | 2 +- src/views/XBlockView/index.test.jsx | 65 ++++ 169 files changed, 8232 insertions(+), 92 deletions(-) create mode 100644 src/components/ActionButton.test.jsx create mode 100644 src/components/AppContainer/__snapshots__/index.test.jsx.snap create mode 100644 src/components/AppContainer/index.test.jsx create mode 100644 src/components/Assessment/EditableAssessment/AssessmentActions.test.jsx create mode 100644 src/components/Assessment/EditableAssessment/OverallFeedback/__snapshots__/index.test.jsx.snap create mode 100644 src/components/Assessment/EditableAssessment/OverallFeedback/index.test.jsx create mode 100644 src/components/Assessment/EditableAssessment/__snapshots__/AssessmentActions.test.jsx.snap create mode 100644 src/components/Assessment/EditableAssessment/__snapshots__/index.test.jsx.snap create mode 100644 src/components/Assessment/EditableAssessment/index.test.jsx create mode 100644 src/components/Assessment/ReadonlyAssessment/AssessmentCriteria.test.jsx create mode 100644 src/components/Assessment/ReadonlyAssessment/CollapsibleAssessment.test.jsx create mode 100644 src/components/Assessment/ReadonlyAssessment/Feedback.test.jsx create mode 100644 src/components/Assessment/ReadonlyAssessment/ReadOnlyAssessment.test.jsx create mode 100644 src/components/Assessment/ReadonlyAssessment/__snapshots__/AssessmentCriteria.test.jsx.snap create mode 100644 src/components/Assessment/ReadonlyAssessment/__snapshots__/CollapsibleAssessment.test.jsx.snap create mode 100644 src/components/Assessment/ReadonlyAssessment/__snapshots__/Feedback.test.jsx.snap create mode 100644 src/components/Assessment/ReadonlyAssessment/__snapshots__/ReadOnlyAssessment.test.jsx.snap create mode 100644 src/components/Assessment/ReadonlyAssessment/__snapshots__/index.test.jsx.snap create mode 100644 src/components/Assessment/ReadonlyAssessment/index.test.jsx create mode 100644 src/components/Assessment/__snapshots__/index.test.jsx.snap create mode 100644 src/components/Assessment/index.test.jsx create mode 100644 src/components/Assessment/useAssessmentData.test.js create mode 100644 src/components/ConfirmDialog/__snapshots__/index.test.jsx.snap create mode 100644 src/components/ConfirmDialog/index.test.jsx create mode 100644 src/components/CriterionContainer/CriterionFeedback.test.jsx create mode 100644 src/components/CriterionContainer/GradedCriterion.test.jsx create mode 100644 src/components/CriterionContainer/RadioCriterion.test.jsx create mode 100644 src/components/CriterionContainer/ReviewCriterion.test.jsx create mode 100644 src/components/CriterionContainer/__snapshots__/CriterionFeedback.test.jsx.snap create mode 100644 src/components/CriterionContainer/__snapshots__/GradedCriterion.test.jsx.snap create mode 100644 src/components/CriterionContainer/__snapshots__/RadioCriterion.test.jsx.snap create mode 100644 src/components/CriterionContainer/__snapshots__/ReviewCriterion.test.jsx.snap create mode 100644 src/components/CriterionContainer/__snapshots__/index.test.jsx.snap create mode 100644 src/components/CriterionContainer/index.test.jsx create mode 100644 src/components/FilePreview/__snapshots__/index.test.jsx.snap create mode 100644 src/components/FilePreview/components/FileRenderer/Banners/ErrorBanner.test.jsx create mode 100644 src/components/FilePreview/components/FileRenderer/Banners/LoadingBanner.test.jsx create mode 100644 src/components/FilePreview/components/FileRenderer/Banners/__snapshots__/ErrorBanner.test.jsx.snap create mode 100644 src/components/FilePreview/components/FileRenderer/Banners/__snapshots__/LoadingBanner.test.jsx.snap create mode 100644 src/components/FilePreview/components/FileRenderer/BaseRenderers/ImageRenderer.test.jsx create mode 100644 src/components/FilePreview/components/FileRenderer/BaseRenderers/PDFRenderer.test.jsx create mode 100644 src/components/FilePreview/components/FileRenderer/BaseRenderers/TXTRenderer.test.jsx create mode 100644 src/components/FilePreview/components/FileRenderer/BaseRenderers/__snapshots__/ImageRenderer.test.jsx.snap create mode 100644 src/components/FilePreview/components/FileRenderer/BaseRenderers/__snapshots__/PDFRenderer.test.jsx.snap create mode 100644 src/components/FilePreview/components/FileRenderer/BaseRenderers/__snapshots__/TXTRenderer.test.jsx.snap create mode 100644 src/components/FilePreview/components/FileRenderer/BaseRenderers/pdfHooks.test.js create mode 100644 src/components/FilePreview/components/FileRenderer/BaseRenderers/textHooks.test.js create mode 100644 src/components/FilePreview/components/FileRenderer/FileCard/__snapshots__/index.test.jsx.snap create mode 100644 src/components/FilePreview/components/FileRenderer/FileCard/index.test.jsx create mode 100644 src/components/FilePreview/components/FileRenderer/__snapshots__/index.test.jsx.snap create mode 100644 src/components/FilePreview/components/FileRenderer/hooks.test.js create mode 100644 src/components/FilePreview/components/FileRenderer/index.test.jsx create mode 100644 src/components/FilePreview/index.test.jsx create mode 100644 src/components/FileUpload/ActionCell.test.jsx create mode 100644 src/components/FileUpload/FileDownload.test.jsx create mode 100644 src/components/FileUpload/FileMetaDisplay.test.jsx create mode 100644 src/components/FileUpload/UploadConfirmModal.test.jsx create mode 100644 src/components/FileUpload/__snapshots__/ActionCell.test.jsx.snap create mode 100644 src/components/FileUpload/__snapshots__/FileDownload.test.jsx.snap create mode 100644 src/components/FileUpload/__snapshots__/FileMetaDisplay.test.jsx.snap create mode 100644 src/components/FileUpload/__snapshots__/UploadConfirmModal.test.jsx.snap create mode 100644 src/components/FileUpload/__snapshots__/index.test.jsx.snap create mode 100644 src/components/FileUpload/hooks.test.js create mode 100644 src/components/FileUpload/index.test.jsx create mode 100644 src/components/HotjarSurvey/index.test.jsx create mode 100644 src/components/InfoPopover/__snapshots__/index.test.jsx.snap create mode 100644 src/components/InfoPopover/index.test.jsx create mode 100644 src/components/Instructions/__snapshots__/index.test.jsx.snap create mode 100644 src/components/Instructions/index.test.jsx rename src/components/Instructions/{useInstructionsMessage.jsx => useInstructionsMessage.js} (100%) create mode 100644 src/components/Instructions/useInstructionsMessage.test.js create mode 100644 src/components/ModalActions/__snapshots__/index.test.jsx.snap create mode 100644 src/components/ModalActions/hooks/useFinishedStateActions.test.js create mode 100644 src/components/ModalActions/hooks/useInProgressActions.test.js create mode 100644 src/components/ModalActions/index.test.jsx create mode 100644 src/components/ModalContainer.test.jsx create mode 100644 src/components/ProgressBar/ProgressStep.test.jsx create mode 100644 src/components/ProgressBar/__snapshots__/ProgressStep.test.jsx.snap create mode 100644 src/components/ProgressBar/__snapshots__/index.test.jsx.snap create mode 100644 src/components/ProgressBar/hooks.test.js create mode 100644 src/components/ProgressBar/index.test.jsx create mode 100644 src/components/Prompt/__snapshots__/index.test.jsx.snap create mode 100644 src/components/Prompt/index.test.jsx create mode 100644 src/components/TextResponse/__snapshots__/index.test.jsx.snap create mode 100644 src/components/TextResponse/index.test.jsx create mode 100644 src/components/__snapshots__/ActionButton.test.jsx.snap create mode 100644 src/components/__snapshots__/ModalContainer.test.jsx.snap create mode 100644 src/views/AssessmentView/BaseAssessmentView/__snapshots__/index.test.jsx.snap create mode 100644 src/views/AssessmentView/BaseAssessmentView/index.test.jsx create mode 100644 src/views/AssessmentView/__snapshots__/index.test.jsx.snap create mode 100644 src/views/AssessmentView/index.test.jsx create mode 100644 src/views/AssessmentView/useAssessmentData.test.js create mode 100644 src/views/GradeView/Content.test.jsx create mode 100644 src/views/GradeView/FinalGrade.test.jsx create mode 100644 src/views/GradeView/__snapshots__/Content.test.jsx.snap create mode 100644 src/views/GradeView/__snapshots__/FinalGrade.test.jsx.snap create mode 100644 src/views/GradeView/__snapshots__/index.test.jsx.snap create mode 100644 src/views/GradeView/index.test.jsx create mode 100644 src/views/SubmissionView/SubmissionPrompts.test.jsx create mode 100644 src/views/SubmissionView/TextResponseEditor/LaTextPreview.test.jsx create mode 100644 src/views/SubmissionView/TextResponseEditor/__snapshots__/LaTextPreview.test.jsx.snap create mode 100644 src/views/SubmissionView/__snapshots__/SubmissionPrompts.test.jsx.snap create mode 100644 src/views/SubmissionView/__snapshots__/index.test.jsx.snap create mode 100644 src/views/SubmissionView/hooks/useUploadedFilesData.test.js create mode 100644 src/views/SubmissionView/index.test.jsx create mode 100644 src/views/XBlockStudioView/__snapshots__/index.test.jsx.snap create mode 100644 src/views/XBlockStudioView/components/StudioSchedule/FormatDateTime.test.jsx create mode 100644 src/views/XBlockStudioView/components/StudioSchedule/StepInfo.test.jsx create mode 100644 src/views/XBlockStudioView/components/StudioSchedule/__snapshots__/FormatDateTime.test.jsx.snap create mode 100644 src/views/XBlockStudioView/components/StudioSchedule/__snapshots__/StepInfo.test.jsx.snap create mode 100644 src/views/XBlockStudioView/components/StudioSchedule/__snapshots__/index.test.jsx.snap create mode 100644 src/views/XBlockStudioView/components/StudioSchedule/index.test.jsx create mode 100644 src/views/XBlockStudioView/components/StudioViewPrompt.test.jsx create mode 100644 src/views/XBlockStudioView/components/StudioViewRubric.test.jsx create mode 100644 src/views/XBlockStudioView/components/StudioViewSettings/FileUploadConfig.test.jsx create mode 100644 src/views/XBlockStudioView/components/StudioViewSettings/RequiredConfig.test.jsx create mode 100644 src/views/XBlockStudioView/components/StudioViewSettings/__snapshots__/FileUploadConfig.test.jsx.snap create mode 100644 src/views/XBlockStudioView/components/StudioViewSettings/__snapshots__/RequiredConfig.test.jsx.snap create mode 100644 src/views/XBlockStudioView/components/StudioViewSettings/__snapshots__/index.test.jsx.snap create mode 100644 src/views/XBlockStudioView/components/StudioViewSettings/index.test.jsx create mode 100644 src/views/XBlockStudioView/components/StudioViewSteps.test.jsx create mode 100644 src/views/XBlockStudioView/components/StudioViewTitle.test.jsx create mode 100644 src/views/XBlockStudioView/components/__snapshots__/StudioViewPrompt.test.jsx.snap create mode 100644 src/views/XBlockStudioView/components/__snapshots__/StudioViewRubric.test.jsx.snap create mode 100644 src/views/XBlockStudioView/components/__snapshots__/StudioViewSteps.test.jsx.snap create mode 100644 src/views/XBlockStudioView/components/__snapshots__/StudioViewTitle.test.jsx.snap create mode 100644 src/views/XBlockStudioView/index.test.jsx create mode 100644 src/views/XBlockView/Actions/__snapshots__/index.test.jsx.snap create mode 100644 src/views/XBlockView/Actions/index.test.jsx create mode 100644 src/views/XBlockView/StatusRow/DueDateMessage/__snapshots__/index.test.jsx.snap create mode 100644 src/views/XBlockView/StatusRow/DueDateMessage/index.test.jsx create mode 100644 src/views/XBlockView/StatusRow/DueDateMessage/useDueDateMessage.test.js create mode 100644 src/views/XBlockView/StatusRow/StatusBadge/__snapshots__/index.test.jsx.snap create mode 100644 src/views/XBlockView/StatusRow/StatusBadge/index.test.jsx create mode 100644 src/views/XBlockView/StatusRow/StatusBadge/useBadgeConfig.test.js create mode 100644 src/views/XBlockView/StatusRow/__snapshots__/index.test.jsx.snap create mode 100644 src/views/XBlockView/StatusRow/index.test.jsx create mode 100644 src/views/XBlockView/__snapshots__/index.test.jsx.snap create mode 100644 src/views/XBlockView/index.test.jsx diff --git a/jest.config.js b/jest.config.js index 23aa0724..0b5f7dd3 100644 --- a/jest.config.js +++ b/jest.config.js @@ -12,6 +12,8 @@ const config = createConfig('jest', { 'src/hooks/testHooks', // don't check coverage for jest mocking tools // 'src/data/services/lms/fakeData', // don't check coverage for mock data 'src/test', // don't check coverage for test integration test utils + 'messages.js', // don't check coverage for i18n messages + 'src/data/services/lms/fakeData', // don't check coverage for fake data ], testTimeout: 120000, }); diff --git a/src/components/ActionButton.test.jsx b/src/components/ActionButton.test.jsx new file mode 100644 index 00000000..3af2dfd8 --- /dev/null +++ b/src/components/ActionButton.test.jsx @@ -0,0 +1,28 @@ +import { shallow } from '@edx/react-unit-test-utils'; + +import ActionButton from './ActionButton'; + +describe('', () => { + const props = { + state: 'arbitraryState', + }; + + it('render empty when no onClick or href', () => { + const wrapper = shallow(); + expect(wrapper.isEmptyRender()).toBe(true); + }); + + it('render StatefulButton when state is provided', () => { + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + expect(wrapper.instance.findByType('StatefulButton')).toHaveLength(1); + expect(wrapper.instance.findByType('Button')).toHaveLength(0); + }); + + it('render Button when state is not provided', () => { + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + expect(wrapper.instance.findByType('StatefulButton')).toHaveLength(0); + expect(wrapper.instance.findByType('Button')).toHaveLength(1); + }); +}); diff --git a/src/components/AppContainer/__snapshots__/index.test.jsx.snap b/src/components/AppContainer/__snapshots__/index.test.jsx.snap new file mode 100644 index 00000000..30f8ec56 --- /dev/null +++ b/src/components/AppContainer/__snapshots__/index.test.jsx.snap @@ -0,0 +1,47 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` render default 1`] = ` +
+
+ children +
+
+`; + +exports[` render error oraConfigDataError 1`] = ` + +`; + +exports[` render error pageDataError 1`] = ` + +`; + +exports[` render loading isORAConfigLoaded 1`] = ` +
+ +
+`; + +exports[` render loading isPageDataLoaded 1`] = ` +
+ +
+`; diff --git a/src/components/AppContainer/index.test.jsx b/src/components/AppContainer/index.test.jsx new file mode 100644 index 00000000..b1ccd0c9 --- /dev/null +++ b/src/components/AppContainer/index.test.jsx @@ -0,0 +1,59 @@ +import { shallow } from '@edx/react-unit-test-utils'; +import { + useIsPageDataLoaded, + useIsORAConfigLoaded, + usePageDataError, + useORAConfigDataError, +} from 'hooks/app'; +import AppContainer from '.'; + +jest.mock('hooks/app', () => ({ + useIsPageDataLoaded: jest.fn().mockReturnValue(true), + useIsORAConfigLoaded: jest.fn().mockReturnValue(true), + usePageDataError: jest.fn().mockReturnValue(null), + useORAConfigDataError: jest.fn().mockReturnValue(null), +})); + +describe('', () => { + const props = { + children:
children
, + }; + + it('render default', () => { + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + expect(wrapper.instance.findByType('Spinner')).toHaveLength(0); + }); + + describe('render error', () => { + it('pageDataError', () => { + usePageDataError.mockReturnValueOnce({ response: { data: { error: { errorCode: 'error' } } } }); + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + expect(wrapper.instance.findByType('ErrorPage')).toHaveLength(1); + }); + + it('oraConfigDataError', () => { + useORAConfigDataError.mockReturnValueOnce({ response: { data: { error: { errorCode: 'error' } } } }); + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + expect(wrapper.instance.findByType('ErrorPage')).toHaveLength(1); + }); + }); + + describe('render loading', () => { + it('isPageDataLoaded', () => { + useIsPageDataLoaded.mockReturnValueOnce(false); + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + expect(wrapper.instance.findByType('Spinner')).toHaveLength(1); + }); + + it('isORAConfigLoaded', () => { + useIsORAConfigLoaded.mockReturnValueOnce(false); + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + expect(wrapper.instance.findByType('Spinner')).toHaveLength(1); + }); + }); +}); diff --git a/src/components/Assessment/EditableAssessment/AssessmentActions.test.jsx b/src/components/Assessment/EditableAssessment/AssessmentActions.test.jsx new file mode 100644 index 00000000..a66ca733 --- /dev/null +++ b/src/components/Assessment/EditableAssessment/AssessmentActions.test.jsx @@ -0,0 +1,72 @@ +import { shallow } from '@edx/react-unit-test-utils'; + +import { useExitWithoutSavingAction, useSubmitAssessmentAction } from 'hooks/actions'; +import AssessmentActions from './AssessmentActions'; + +jest.mock('hooks/actions', () => ({ + useExitWithoutSavingAction: jest.fn(), + useSubmitAssessmentAction: jest.fn(), +})); + +jest.mock('components/ActionButton', () => 'ActionButton'); +jest.mock('components/ConfirmDialog', () => 'ConfirmDialog'); + +describe('', () => { + const mockExitWithoutSavingAction = { + action: { + onClick: jest.fn().mockName('useExitWithoutSavingAction.onClick'), + }, + confirmProps: { + onConfirm: jest.fn().mockName('useExitWithoutSavingAction.onConfirm'), + }, + }; + const mockSubmitAssessmentAction = { + action: { + onClick: jest.fn().mockName('useSubmitAssessmentAction.onClick'), + }, + confirmProps: { + onConfirm: jest.fn().mockName('useSubmitAssessmentAction.onConfirm'), + }, + }; + + beforeEach(() => { + useExitWithoutSavingAction.mockReturnValue(mockExitWithoutSavingAction); + useSubmitAssessmentAction.mockReturnValue(mockSubmitAssessmentAction); + }); + + it('render default', () => { + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.instance.findByType('ActionButton')).toHaveLength(2); + expect(wrapper.instance.findByType('ConfirmDialog')).toHaveLength(2); + }); + + it('render without submitConfirmDialog', () => { + useSubmitAssessmentAction.mockReturnValueOnce({ + action: mockSubmitAssessmentAction.action, + confirmProps: null, + }); + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.instance.findByType('ActionButton')).toHaveLength(2); + expect(wrapper.instance.findByType('ConfirmDialog')).toHaveLength(1); + }); + + it('has correct mock value', () => { + const wrapper = shallow(); + + const exitButton = wrapper.instance.findByType('ActionButton')[0]; + expect(exitButton.props).toMatchObject(mockExitWithoutSavingAction.action); + + const exitConfirmDialog = wrapper.instance.findByType('ConfirmDialog')[0]; + expect(exitConfirmDialog.props).toMatchObject(mockExitWithoutSavingAction.confirmProps); + + const submitButton = wrapper.instance.findByType('ActionButton')[1]; + expect(submitButton.props).toMatchObject(mockSubmitAssessmentAction.action); + + const submitConfirmDialog = wrapper.instance.findByType('ConfirmDialog')[1]; + expect(submitConfirmDialog.props).toMatchObject(mockSubmitAssessmentAction.confirmProps); + }); +}); diff --git a/src/components/Assessment/EditableAssessment/OverallFeedback/__snapshots__/index.test.jsx.snap b/src/components/Assessment/EditableAssessment/OverallFeedback/__snapshots__/index.test.jsx.snap new file mode 100644 index 00000000..cf61a365 --- /dev/null +++ b/src/components/Assessment/EditableAssessment/OverallFeedback/__snapshots__/index.test.jsx.snap @@ -0,0 +1,31 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` render default 1`] = ` + + + + Overall comments + + +
+ useOverallFeedbackPrompt +
+
+
+ +
+`; + +exports[` render empty on studentTraining 1`] = `null`; diff --git a/src/components/Assessment/EditableAssessment/OverallFeedback/index.jsx b/src/components/Assessment/EditableAssessment/OverallFeedback/index.jsx index aaa13884..7b504ba6 100644 --- a/src/components/Assessment/EditableAssessment/OverallFeedback/index.jsx +++ b/src/components/Assessment/EditableAssessment/OverallFeedback/index.jsx @@ -33,7 +33,7 @@ const OverallFeedback = () => { {formatMessage(messages.overallComments)} -
{prompt}
+
{prompt}
({ + useOverallFeedbackPrompt: jest.fn(), + useOverallFeedbackFormFields: jest.fn(), +})); + +jest.mock('components/InfoPopover', () => 'InfoPopover'); + +jest.mock('hooks/routing', () => ({ + useViewStep: jest.fn().mockReturnValue('step'), +})); + +describe('', () => { + const mockOnChange = jest + .fn() + .mockName('useOverallFeedbackFormFields.onChange'); + const mockFeedbackValue = 'useOverallFeedbackFormFields.value'; + const mockPrompt = 'useOverallFeedbackPrompt'; + + beforeAll(() => { + useOverallFeedbackPrompt.mockReturnValue(mockPrompt); + useOverallFeedbackFormFields.mockReturnValue({ + value: mockFeedbackValue, + onChange: mockOnChange, + }); + }); + + it('render default', () => { + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + }); + + it('render empty on studentTraining', () => { + useViewStep.mockReturnValueOnce(stepNames.studentTraining); + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.isEmptyRender()).toBe(true); + }); + + it('has correct mock value', () => { + const wrapper = shallow(); + + expect(wrapper.instance.findByTestId('prompt-test-id')[0].children[0].el).toBe( + mockPrompt, + ); + + const { props } = wrapper.instance.findByType('Form.Control')[0]; + expect(props.value).toBe(mockFeedbackValue); + expect(props.onChange).toBe(mockOnChange); + }); +}); diff --git a/src/components/Assessment/EditableAssessment/__snapshots__/AssessmentActions.test.jsx.snap b/src/components/Assessment/EditableAssessment/__snapshots__/AssessmentActions.test.jsx.snap new file mode 100644 index 00000000..13e71f46 --- /dev/null +++ b/src/components/Assessment/EditableAssessment/__snapshots__/AssessmentActions.test.jsx.snap @@ -0,0 +1,40 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` render default 1`] = ` +
+ + + + +
+`; + +exports[` render without submitConfirmDialog 1`] = ` +
+ + + +
+`; diff --git a/src/components/Assessment/EditableAssessment/__snapshots__/index.test.jsx.snap b/src/components/Assessment/EditableAssessment/__snapshots__/index.test.jsx.snap new file mode 100644 index 00000000..20848fff --- /dev/null +++ b/src/components/Assessment/EditableAssessment/__snapshots__/index.test.jsx.snap @@ -0,0 +1,125 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` render empty criteria 1`] = ` + + +

+ Rubric +

+
+
+ +
+ +
+`; + +exports[` render with criteria 1`] = ` + + +

+ Rubric +

+
+ + } + input={ + + } + key="criterion1" + /> + + } + input={ + + } + key="criterion2" + /> + + } + input={ + + } + key="criterion3" + /> +
+ +
+ +
+`; diff --git a/src/components/Assessment/EditableAssessment/index.test.jsx b/src/components/Assessment/EditableAssessment/index.test.jsx new file mode 100644 index 00000000..592c8750 --- /dev/null +++ b/src/components/Assessment/EditableAssessment/index.test.jsx @@ -0,0 +1,37 @@ +import { shallow } from '@edx/react-unit-test-utils'; + +import { useCriteriaConfig } from 'hooks/assessment'; +import EditableAssessment from '.'; + +jest.mock('hooks/assessment', () => ({ + useCriteriaConfig: jest.fn(), +})); + +jest.mock('components/CriterionContainer', () => 'CriterionContainer'); +jest.mock('components/CriterionContainer/RadioCriterion', () => 'RadioCriterion'); +jest.mock('components/CriterionContainer/CriterionFeedback', () => 'CriterionFeedback'); +jest.mock('./OverallFeedback', () => 'OverallFeedback'); +jest.mock('./AssessmentActions', () => 'AssessmentActions'); + +describe('', () => { + it('render empty criteria', () => { + useCriteriaConfig.mockReturnValue([]); + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.instance.findByType('CriterionContainer')).toHaveLength(0); + }); + + it('render with criteria', () => { + const mockCriteria = [ + { name: 'criterion1' }, + { name: 'criterion2' }, + { name: 'criterion3' }, + ]; + useCriteriaConfig.mockReturnValue(mockCriteria); + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.instance.findByType('CriterionContainer')).toHaveLength(mockCriteria.length); + }); +}); diff --git a/src/components/Assessment/ReadonlyAssessment/AssessmentCriteria.test.jsx b/src/components/Assessment/ReadonlyAssessment/AssessmentCriteria.test.jsx new file mode 100644 index 00000000..bbc6e5c2 --- /dev/null +++ b/src/components/Assessment/ReadonlyAssessment/AssessmentCriteria.test.jsx @@ -0,0 +1,63 @@ +import { shallow } from '@edx/react-unit-test-utils'; + +import { useCriteriaConfig } from 'hooks/assessment'; +import AssessmentCriteria from './AssessmentCriteria'; + +jest.mock('hooks/assessment', () => ({ + useCriteriaConfig: jest.fn(), +})); + +describe('', () => { + const props = { + criteria: [ + { + selectedOption: 1, + feedback: 'Feedback 1', + }, + { + selectedOption: 2, + feedback: 'Feedback 2', + }, + ], + overallFeedback: 'Overall Feedback', + stepLabel: 'Step Label', + }; + + it('renders the component', () => { + useCriteriaConfig.mockReturnValue([ + { + name: 'Criterion Name', + description: 'Criterion Description', + options: { + 1: { + label: 'Selected Option 1', + points: 5, + }, + }, + }, + { + name: 'Criterion Name 2', + description: 'Criterion Description 2', + options: { + 2: { + label: 'Selected Option 2', + points: 10, + }, + }, + }, + ]); + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + // one for each criteria and one for overall feedback + expect(wrapper.instance.findByType('Feedback').length).toBe(3); + }); + + it('renders without props', () => { + useCriteriaConfig.mockReturnValue([]); + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.instance.findByType('Feedback').length).toBe(0); + }); +}); diff --git a/src/components/Assessment/ReadonlyAssessment/CollapsibleAssessment.test.jsx b/src/components/Assessment/ReadonlyAssessment/CollapsibleAssessment.test.jsx new file mode 100644 index 00000000..7d6c7731 --- /dev/null +++ b/src/components/Assessment/ReadonlyAssessment/CollapsibleAssessment.test.jsx @@ -0,0 +1,36 @@ +import { shallow } from '@edx/react-unit-test-utils'; + +import CollapsibleAssessment from './CollapsibleAssessment'; + +describe('', () => { + const defaultProps = { + stepScore: { + earned: 5, + possible: 10, + }, + stepLabel: 'Step Label', + defaultOpen: true, + }; + + const renderComponent = (props = {}) => shallow( + +
Children
+
, + ); + + it('renders the component', () => { + const wrapper = renderComponent(defaultProps); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.instance.findByType('Collapsible')[0].props.open).toBe(true); + }); + + it('renders without props', () => { + const wrapper = renderComponent(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.instance.findByType('Collapsible')[0].props.open).toBe( + false, + ); + }); +}); diff --git a/src/components/Assessment/ReadonlyAssessment/Feedback.test.jsx b/src/components/Assessment/ReadonlyAssessment/Feedback.test.jsx new file mode 100644 index 00000000..6924ce50 --- /dev/null +++ b/src/components/Assessment/ReadonlyAssessment/Feedback.test.jsx @@ -0,0 +1,45 @@ +import { shallow } from '@edx/react-unit-test-utils'; + +import Feedback from './Feedback'; + +jest.mock('components/InfoPopover', () => 'InfoPopover'); + +describe('', () => { + const props = { + criterionDescription: 'Criterion Description', + selectedOption: 'Selected Option', + selectedPoints: 5, + commentHeader: 'Comment Header', + criterionName: 'Criterion Name', + commentBody: 'Comment Body', + }; + + it('renders the component', () => { + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.instance.findByType('Collapsible.Advanced').length).toBe(1); + }); + + it('render without props', () => { + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.instance.findByType('Collapsible.Advanced').length).toBe(0); + }); + + it('renders without selectedOption', () => { + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + }); + + it('renders without criterionDescription', () => { + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + }); + + it('renders without commentBody', () => { + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + }); +}); diff --git a/src/components/Assessment/ReadonlyAssessment/ReadOnlyAssessment.test.jsx b/src/components/Assessment/ReadonlyAssessment/ReadOnlyAssessment.test.jsx new file mode 100644 index 00000000..abf848be --- /dev/null +++ b/src/components/Assessment/ReadonlyAssessment/ReadOnlyAssessment.test.jsx @@ -0,0 +1,46 @@ +import { shallow } from '@edx/react-unit-test-utils'; + +import ReadOnlyAssessment from './ReadOnlyAssessment'; + +jest.mock('./CollapsibleAssessment', () => 'CollapsibleAssessment'); +jest.mock('./AssessmentCriteria', () => 'AssessmentCriteria'); + +describe('', () => { + const props = { + assessment: { + abc: 'def', + }, + step: 'Step', + stepScore: { + earned: 5, + total: 10, + }, + }; + + it('render with assessment', () => { + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.instance.findByType('AssessmentCriteria').length).toBe(1); + }); + + it('render with assessments', () => { + const assessments = [ + { + abc: 'def', + }, + { + ghi: 'jkl', + }, + ]; + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.instance.findByType('AssessmentCriteria').length).toBe(2); + }); + + it('renders without props', () => { + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + }); +}); diff --git a/src/components/Assessment/ReadonlyAssessment/__snapshots__/AssessmentCriteria.test.jsx.snap b/src/components/Assessment/ReadonlyAssessment/__snapshots__/AssessmentCriteria.test.jsx.snap new file mode 100644 index 00000000..34c66b74 --- /dev/null +++ b/src/components/Assessment/ReadonlyAssessment/__snapshots__/AssessmentCriteria.test.jsx.snap @@ -0,0 +1,40 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders the component 1`] = ` + + + + +
+ +
+
+`; + +exports[` renders without props 1`] = ``; diff --git a/src/components/Assessment/ReadonlyAssessment/__snapshots__/CollapsibleAssessment.test.jsx.snap b/src/components/Assessment/ReadonlyAssessment/__snapshots__/CollapsibleAssessment.test.jsx.snap new file mode 100644 index 00000000..efe09632 --- /dev/null +++ b/src/components/Assessment/ReadonlyAssessment/__snapshots__/CollapsibleAssessment.test.jsx.snap @@ -0,0 +1,34 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders the component 1`] = ` + + Step Label grade: + 5 / 10 + + } +> +
+ Children +
+
+`; + +exports[` renders without props 1`] = ` + + Submitted assessment + + } +> +
+ Children +
+
+`; diff --git a/src/components/Assessment/ReadonlyAssessment/__snapshots__/Feedback.test.jsx.snap b/src/components/Assessment/ReadonlyAssessment/__snapshots__/Feedback.test.jsx.snap new file mode 100644 index 00000000..c0f4528f --- /dev/null +++ b/src/components/Assessment/ReadonlyAssessment/__snapshots__/Feedback.test.jsx.snap @@ -0,0 +1,224 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` render without props 1`] = ` + +
+
+
+
+
+
+`; + +exports[` renders the component 1`] = ` + +
+
+
+ Criterion Name +
+ +

+ Criterion Description +

+
+
+

+ Selected Option + : + 5 + + Points +

+
+
+ + +
+ Comment Header comment +
+
+ + Read less + + +
+
+ +

+ Comment Body +

+
+
+
+
+`; + +exports[` renders without commentBody 1`] = ` + +
+
+
+ Criterion Name +
+ +

+ Criterion Description +

+
+
+

+ Selected Option + : + 5 + + Points +

+
+
+`; + +exports[` renders without criterionDescription 1`] = ` + +
+
+
+ Criterion Name +
+
+

+ Selected Option + : + 5 + + Points +

+
+
+ + +
+ Comment Header comment +
+
+ + Read less + + +
+
+ +

+ Comment Body +

+
+
+
+
+`; + +exports[` renders without selectedOption 1`] = ` + +
+
+
+ Criterion Name +
+ +

+ Criterion Description +

+
+
+
+
+ + +
+ Comment Header comment +
+
+ + Read less + + +
+
+ +

+ Comment Body +

+
+
+
+
+`; diff --git a/src/components/Assessment/ReadonlyAssessment/__snapshots__/ReadOnlyAssessment.test.jsx.snap b/src/components/Assessment/ReadonlyAssessment/__snapshots__/ReadOnlyAssessment.test.jsx.snap new file mode 100644 index 00000000..51bd369a --- /dev/null +++ b/src/components/Assessment/ReadonlyAssessment/__snapshots__/ReadOnlyAssessment.test.jsx.snap @@ -0,0 +1,86 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` render with assessment 1`] = ` + + + +`; + +exports[` render with assessments 1`] = ` +
+ + +

+ + 1 + : +

+ +
+
+ +

+ + 2 + : +

+ +
+
+
+
+`; + +exports[` renders without props 1`] = ` + + + +`; diff --git a/src/components/Assessment/ReadonlyAssessment/__snapshots__/index.test.jsx.snap b/src/components/Assessment/ReadonlyAssessment/__snapshots__/index.test.jsx.snap new file mode 100644 index 00000000..b16dd705 --- /dev/null +++ b/src/components/Assessment/ReadonlyAssessment/__snapshots__/index.test.jsx.snap @@ -0,0 +1,41 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders the component 1`] = ` + +`; + +exports[` renders without props 1`] = ` + +`; diff --git a/src/components/Assessment/ReadonlyAssessment/index.jsx b/src/components/Assessment/ReadonlyAssessment/index.jsx index 64256b6b..1fcc4bf8 100644 --- a/src/components/Assessment/ReadonlyAssessment/index.jsx +++ b/src/components/Assessment/ReadonlyAssessment/index.jsx @@ -26,7 +26,7 @@ const ReadOnlyAssessmentContainer = (props) => { /> ); }; -ReadOnlyAssessment.defaultProps = { +ReadOnlyAssessmentContainer.defaultProps = { defaultOpen: false, assessment: null, assessments: null, @@ -34,7 +34,7 @@ ReadOnlyAssessment.defaultProps = { stepLabel: null, step: null, }; -ReadOnlyAssessment.propTypes = { +ReadOnlyAssessmentContainer.propTypes = { stepLabel: PropTypes.string, step: PropTypes.string, stepScore: PropTypes.shape({ diff --git a/src/components/Assessment/ReadonlyAssessment/index.test.jsx b/src/components/Assessment/ReadonlyAssessment/index.test.jsx new file mode 100644 index 00000000..c2462630 --- /dev/null +++ b/src/components/Assessment/ReadonlyAssessment/index.test.jsx @@ -0,0 +1,43 @@ +import { shallow } from '@edx/react-unit-test-utils'; + +import ReadOnlyAssessmentContainer from '.'; + +jest.mock('hooks/app', () => ({ + useHasSubmitted: jest.fn(), + useRefreshPageData: jest.fn(), +})); +jest.mock('hooks/assessment', () => ({ + useSubmittedAssessment: jest.fn(), +})); +jest.mock('./ReadOnlyAssessment', () => 'ReadOnlyAssessment'); + +describe('', () => { + const props = { + assessment: { + abc: 'def', + }, + assessments: [ + { + abc: 'def', + }, + { + ghi: 'jkl', + }, + ], + step: 'Step', + stepScore: { + earned: 5, + total: 10, + }, + defaultOpen: true, + }; + it('renders the component', () => { + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + }); + + it('renders without props', () => { + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + }); +}); diff --git a/src/components/Assessment/__snapshots__/index.test.jsx.snap b/src/components/Assessment/__snapshots__/index.test.jsx.snap new file mode 100644 index 00000000..763d745d --- /dev/null +++ b/src/components/Assessment/__snapshots__/index.test.jsx.snap @@ -0,0 +1,11 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders nothing if not initialized 1`] = `null`; + +exports[` renders the EditableAssessment 1`] = ``; + +exports[` renders the ReadonlyAssessment 1`] = ` + +`; diff --git a/src/components/Assessment/index.jsx b/src/components/Assessment/index.jsx index 153a8102..148cf9fd 100644 --- a/src/components/Assessment/index.jsx +++ b/src/components/Assessment/index.jsx @@ -2,14 +2,15 @@ import React from 'react'; import EditableAssessment from './EditableAssessment'; import ReadonlyAssessment from './ReadonlyAssessment'; -import useAssessmentData from './useAssessmentData'; + +import { useAssessmentData } from './useAssessmentData'; import './Assessment.scss'; /** * */ -export const Assessment = () => { +const Assessment = () => { const { initialized, hasSubmitted } = useAssessmentData(); if (!initialized) { return null; diff --git a/src/components/Assessment/index.test.jsx b/src/components/Assessment/index.test.jsx new file mode 100644 index 00000000..f43855e2 --- /dev/null +++ b/src/components/Assessment/index.test.jsx @@ -0,0 +1,38 @@ +import { shallow } from '@edx/react-unit-test-utils'; + +import Assessment from './index'; + +import { useAssessmentData } from './useAssessmentData'; + +jest.mock('./useAssessmentData', () => ({ + useAssessmentData: jest.fn(), +})); + +jest.mock('./EditableAssessment', () => 'EditableAssessment'); +jest.mock('./ReadonlyAssessment', () => 'ReadonlyAssessment'); + +describe('', () => { + it('renders the ReadonlyAssessment', () => { + useAssessmentData.mockReturnValue({ initialized: true, hasSubmitted: true }); + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.instance.findByType('ReadonlyAssessment')).toHaveLength(1); + expect(wrapper.instance.findByType('EditableAssessment')).toHaveLength(0); + }); + it('renders the EditableAssessment', () => { + useAssessmentData.mockReturnValue({ initialized: true, hasSubmitted: false }); + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.instance.findByType('ReadonlyAssessment')).toHaveLength(0); + expect(wrapper.instance.findByType('EditableAssessment')).toHaveLength(1); + }); + it('renders nothing if not initialized', () => { + useAssessmentData.mockReturnValue({ initialized: false }); + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.isEmptyRender()).toBe(true); + }); +}); diff --git a/src/components/Assessment/useAssessmentData.js b/src/components/Assessment/useAssessmentData.js index 1a5a7d76..91031a89 100644 --- a/src/components/Assessment/useAssessmentData.js +++ b/src/components/Assessment/useAssessmentData.js @@ -1,16 +1,13 @@ import React from 'react'; import { StrictDict, useKeyedState } from '@edx/react-unit-test-utils'; -import { - useHasSubmitted, - useInitializeAssessment, -} from 'hooks/assessment'; +import { useHasSubmitted, useInitializeAssessment } from 'hooks/assessment'; export const stateKeys = StrictDict({ initialized: 'initialized', }); -const useAssessmentData = () => { +export const useAssessmentData = () => { const [initialized, setInitialized] = useKeyedState(stateKeys.initialized, false); const hasSubmitted = useHasSubmitted(); const initialize = useInitializeAssessment(); diff --git a/src/components/Assessment/useAssessmentData.test.js b/src/components/Assessment/useAssessmentData.test.js new file mode 100644 index 00000000..5fb47cd2 --- /dev/null +++ b/src/components/Assessment/useAssessmentData.test.js @@ -0,0 +1,47 @@ +import React from 'react'; + +import { mockUseKeyedState } from '@edx/react-unit-test-utils'; + +import { useHasSubmitted, useInitializeAssessment } from 'hooks/assessment'; + +import { useAssessmentData, stateKeys } from './useAssessmentData'; + +jest.mock('hooks/assessment', () => ({ + useHasSubmitted: jest.fn(), + useInitializeAssessment: jest.fn(), +})); + +const state = mockUseKeyedState(stateKeys); + +describe('useAssessmentData', () => { + beforeEach(() => { + jest.clearAllMocks(); + state.mock(); + }); + afterEach(() => { state.resetVals(); }); + + it('initializes initialized state to false', () => { + useAssessmentData(); + state.expectInitializedWith(stateKeys.initialized, false); + }); + + it('calls useInitializeAssessment', () => { + const mockInitialize = jest.fn(); + useInitializeAssessment.mockReturnValue(mockInitialize); + useAssessmentData(); + expect(useInitializeAssessment).toHaveBeenCalled(); + expect(mockInitialize).not.toHaveBeenCalled(); + const [[cb, prereqs]] = React.useEffect.mock.calls; + expect(prereqs).toEqual([mockInitialize, state.setState[stateKeys.initialized]]); + cb(); + expect(mockInitialize).toHaveBeenCalled(); + }); + + it('calls useHasSubmitted', () => { + useHasSubmitted.mockReturnValue('def'); + const out = useAssessmentData(); + expect(useHasSubmitted).toHaveBeenCalled(); + + expect(out.hasSubmitted).toEqual('def'); + }); +}); diff --git a/src/components/ConfirmDialog/__snapshots__/index.test.jsx.snap b/src/components/ConfirmDialog/__snapshots__/index.test.jsx.snap new file mode 100644 index 00000000..dc780d2a --- /dev/null +++ b/src/components/ConfirmDialog/__snapshots__/index.test.jsx.snap @@ -0,0 +1,27 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders correctly 1`] = ` + + + + + + + } + isOpen={true} + title="Title" +> + Description + +`; diff --git a/src/components/ConfirmDialog/index.test.jsx b/src/components/ConfirmDialog/index.test.jsx new file mode 100644 index 00000000..487eb1ac --- /dev/null +++ b/src/components/ConfirmDialog/index.test.jsx @@ -0,0 +1,19 @@ +import { shallow } from '@edx/react-unit-test-utils'; + +import ConfirmDialog from './index'; + +describe('', () => { + const props = { + title: 'Title', + description: 'Description', + action: { + onClick: jest.fn().mockName('onClick'), + }, + isOpen: true, + close: jest.fn().mockName('close'), + }; + it('renders correctly', () => { + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + }); +}); diff --git a/src/components/CriterionContainer/CriterionFeedback.test.jsx b/src/components/CriterionContainer/CriterionFeedback.test.jsx new file mode 100644 index 00000000..fc4a1abd --- /dev/null +++ b/src/components/CriterionContainer/CriterionFeedback.test.jsx @@ -0,0 +1,86 @@ +import { shallow } from '@edx/react-unit-test-utils'; + +import { useViewStep } from 'hooks/routing'; +import { stepNames } from 'constants/index'; +import { + useShowValidation, + useCriterionFeedbackFormFields, +} from 'hooks/assessment'; + +import CriterionFeedback from './CriterionFeedback'; + +jest.mock('hooks/assessment'); +jest.mock('hooks/routing'); + +describe('', () => { + const props = { + criterion: { + feedbackEnabled: true, + feedbackRequired: true, + }, + criterionIndex: 0, + }; + + it('render empty on student training', () => { + useViewStep.mockReturnValue(stepNames.studentTraining); + useCriterionFeedbackFormFields.mockReturnValue({ + value: '', + onChange: jest.fn(), + isInvalid: false, + }); + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.isEmptyRender()).toBe(true); + }); + + it('render empty on feedback not enable', () => { + useViewStep.mockReturnValue(stepNames.self); + useCriterionFeedbackFormFields.mockReturnValue({ + value: '', + onChange: jest.fn(), + isInvalid: false, + }); + + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.isEmptyRender()).toBe(true); + }); + + it('render with validation error', () => { + useViewStep.mockReturnValue(stepNames.self); + useShowValidation.mockReturnValue(true); + useCriterionFeedbackFormFields.mockReturnValue({ + value: '', + onChange: jest.fn(), + isInvalid: true, + }); + + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.instance.findByType('Form.Control.Feedback').length).toBe(1); + }); + + it('render without validation error', () => { + useViewStep.mockReturnValue(stepNames.self); + useShowValidation.mockReturnValue(false); + useCriterionFeedbackFormFields.mockReturnValue({ + value: '', + onChange: jest.fn(), + isInvalid: false, + }); + + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.instance.findByType('Form.Control.Feedback').length).toBe(0); + }); +}); diff --git a/src/components/CriterionContainer/GradedCriterion.test.jsx b/src/components/CriterionContainer/GradedCriterion.test.jsx new file mode 100644 index 00000000..df044fff --- /dev/null +++ b/src/components/CriterionContainer/GradedCriterion.test.jsx @@ -0,0 +1,24 @@ +import { shallow } from '@edx/react-unit-test-utils'; + +import GradedCriterion from './GradedCriterion'; + +describe('', () => { + const props = { + selectedOption: { + name: 'option1', + label: 'Option 1', + points: 1, + }, + feedbackValue: 'Feedback', + }; + + it('renders correctly', () => { + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + }); + + it('renders correctly with no feedback', () => { + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + }); +}); diff --git a/src/components/CriterionContainer/RadioCriterion.test.jsx b/src/components/CriterionContainer/RadioCriterion.test.jsx new file mode 100644 index 00000000..92dfc5f4 --- /dev/null +++ b/src/components/CriterionContainer/RadioCriterion.test.jsx @@ -0,0 +1,108 @@ +import { shallow } from '@edx/react-unit-test-utils'; + +import { + useShowValidation, + useShowTrainingError, + useCriterionOptionFormFields, +} from 'hooks/assessment'; +import RadioCriterion from './RadioCriterion'; + +jest.mock('hooks/assessment'); + +describe('', () => { + const props = { + criterion: { + name: 'criterionName', + options: [ + { + name: 'option1', + label: 'Option 1', + points: 1, + }, + { + name: 'option2', + label: 'Option 2', + points: 2, + }, + ], + }, + criterionIndex: 0, + }; + + const defaultUseCriterionOptionFormFields = { + value: 'abc', + onChange: jest.fn().mockName('onChange'), + isInvalid: false, + trainingOptionValidity: '', + }; + + it('renders correctly', () => { + useShowValidation.mockReturnValueOnce(false); + useShowTrainingError.mockReturnValueOnce(false); + useCriterionOptionFormFields.mockReturnValueOnce(defaultUseCriterionOptionFormFields); + + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.instance.findByType('Form.Radio').length).toBe(2); + }); + + it('renders correctly with no options', () => { + useShowValidation.mockReturnValueOnce(false); + useShowTrainingError.mockReturnValueOnce(false); + useCriterionOptionFormFields.mockReturnValueOnce(defaultUseCriterionOptionFormFields); + + const wrapper = shallow( + , + ); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.instance.findByType('Form.Radio').length).toBe(0); + }); + + it('renders correctly with validation error', () => { + useShowValidation.mockReturnValueOnce(true); + useShowTrainingError.mockReturnValueOnce(false); + useCriterionOptionFormFields.mockReturnValueOnce({ + ...defaultUseCriterionOptionFormFields, + isInvalid: true, + }); + + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.instance.findByType('Form.Control.Feedback').length).toBe(1); + }); + + it('renders correctly with training error', () => { + useShowValidation.mockReturnValueOnce(false); + useShowTrainingError.mockReturnValueOnce(true); + useCriterionOptionFormFields.mockReturnValueOnce({ + ...defaultUseCriterionOptionFormFields, + trainingOptionValidity: 'invalid', + }); + + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.instance.findByType('Form.Control.Feedback').length).toBe(1); + }); + + it('renders correctly with validation and training error', () => { + useShowValidation.mockReturnValueOnce(true); + useShowTrainingError.mockReturnValueOnce(true); + useCriterionOptionFormFields.mockReturnValueOnce({ + ...defaultUseCriterionOptionFormFields, + isInvalid: true, + trainingOptionValidity: 'invalid', + }); + + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.instance.findByType('Form.Control.Feedback').length).toBe(2); + }); +}); diff --git a/src/components/CriterionContainer/ReviewCriterion.test.jsx b/src/components/CriterionContainer/ReviewCriterion.test.jsx new file mode 100644 index 00000000..e8885cc4 --- /dev/null +++ b/src/components/CriterionContainer/ReviewCriterion.test.jsx @@ -0,0 +1,34 @@ +import { shallow } from '@edx/react-unit-test-utils'; + +import ReviewCriterion from './ReviewCriterion'; + +describe('', () => { + const criterion = { + options: [ + { + name: 'option1', + label: 'Option 1', + points: 1, + }, + { + name: 'option2', + label: 'Option 2', + points: 2, + }, + ], + }; + + it('renders correctly', () => { + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.instance.findByType('Form.Label').length).toBe(2); + }); + + it('renders correctly with no options', () => { + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.instance.findByType('Form.Label').length).toBe(0); + }); +}); diff --git a/src/components/CriterionContainer/__snapshots__/CriterionFeedback.test.jsx.snap b/src/components/CriterionContainer/__snapshots__/CriterionFeedback.test.jsx.snap new file mode 100644 index 00000000..84425e84 --- /dev/null +++ b/src/components/CriterionContainer/__snapshots__/CriterionFeedback.test.jsx.snap @@ -0,0 +1,39 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` render empty on feedback not enable 1`] = `null`; + +exports[` render empty on student training 1`] = `null`; + +exports[` render with validation error 1`] = ` + + + + The feedback is required + + +`; + +exports[` render without validation error 1`] = ` + + + +`; diff --git a/src/components/CriterionContainer/__snapshots__/GradedCriterion.test.jsx.snap b/src/components/CriterionContainer/__snapshots__/GradedCriterion.test.jsx.snap new file mode 100644 index 00000000..dab21afa --- /dev/null +++ b/src/components/CriterionContainer/__snapshots__/GradedCriterion.test.jsx.snap @@ -0,0 +1,72 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders correctly 1`] = ` +
+ + Option 1 + +
+
+ + + +
+ Feedback +
+
+
+
+`; + +exports[` renders correctly with no feedback 1`] = ` +
+ + Option 1 + +
+
+ + + +
+
+
+`; diff --git a/src/components/CriterionContainer/__snapshots__/RadioCriterion.test.jsx.snap b/src/components/CriterionContainer/__snapshots__/RadioCriterion.test.jsx.snap new file mode 100644 index 00000000..93dc382c --- /dev/null +++ b/src/components/CriterionContainer/__snapshots__/RadioCriterion.test.jsx.snap @@ -0,0 +1,136 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders correctly 1`] = ` + + + Option 1 + + + Option 2 + + +`; + +exports[` renders correctly with no options 1`] = ` + +`; + +exports[` renders correctly with training error 1`] = ` + + + Option 1 + + + Option 2 + + + Reevaluate and select a new score + + +`; + +exports[` renders correctly with validation and training error 1`] = ` + + + Option 1 + + + Option 2 + + + Rubric selection is required + + + Reevaluate and select a new score + + +`; + +exports[` renders correctly with validation error 1`] = ` + + + Option 1 + + + Option 2 + + + Rubric selection is required + + +`; diff --git a/src/components/CriterionContainer/__snapshots__/ReviewCriterion.test.jsx.snap b/src/components/CriterionContainer/__snapshots__/ReviewCriterion.test.jsx.snap new file mode 100644 index 00000000..3fdc3fd2 --- /dev/null +++ b/src/components/CriterionContainer/__snapshots__/ReviewCriterion.test.jsx.snap @@ -0,0 +1,80 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders correctly 1`] = ` + +
+
+ + Option 1 + +
+
+ + + +
+
+
+
+ + Option 2 + +
+
+ + + +
+
+
+
+
+`; + +exports[` renders correctly with no options 1`] = ` + +
+ +`; diff --git a/src/components/CriterionContainer/__snapshots__/index.test.jsx.snap b/src/components/CriterionContainer/__snapshots__/index.test.jsx.snap new file mode 100644 index 00000000..a6540840 --- /dev/null +++ b/src/components/CriterionContainer/__snapshots__/index.test.jsx.snap @@ -0,0 +1,102 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders default 1`] = ` + + + + criterionName + + +
+ description +
+
+
+ + Option 1 + +
+ description1 +
+
+ + Option 2 + +
+ description2 +
+
+
+
+
+ input +
+
+
+ feedback +
+
+`; + +exports[` renders without input and feedback 1`] = ` + + + + criterionName + + +
+ description +
+
+
+ + Option 1 + +
+ description1 +
+
+ + Option 2 + +
+ description2 +
+
+
+
+ +`; diff --git a/src/components/CriterionContainer/index.test.jsx b/src/components/CriterionContainer/index.test.jsx new file mode 100644 index 00000000..28c9870f --- /dev/null +++ b/src/components/CriterionContainer/index.test.jsx @@ -0,0 +1,44 @@ +import { shallow } from '@edx/react-unit-test-utils'; + +import CriterionContainer from './index'; + +jest.mock('components/InfoPopover', () => 'InfoPopover'); + +describe('', () => { + const props = { + input:
input
, + feedback:
feedback
, + criterion: { + name: 'criterionName', + description: 'description', + options: [ + { + name: 'option1', + label: 'Option 1', + description: 'description1', + }, + { + name: 'option2', + label: 'Option 2', + description: 'description2', + }, + ], + }, + }; + + it('renders default', () => { + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.instance.findByTestId('input').length).toBe(1); + expect(wrapper.instance.findByTestId('feedback').length).toBe(1); + }); + + it('renders without input and feedback', () => { + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.instance.findByTestId('input').length).toBe(0); + expect(wrapper.instance.findByTestId('feedback').length).toBe(0); + }); +}); diff --git a/src/components/FilePreview/__snapshots__/index.test.jsx.snap b/src/components/FilePreview/__snapshots__/index.test.jsx.snap new file mode 100644 index 00000000..cc0b932a --- /dev/null +++ b/src/components/FilePreview/__snapshots__/index.test.jsx.snap @@ -0,0 +1,28 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` render only supported files 1`] = ` +
+ + +
+`; + +exports[` renders nothing when no files are uploaded 1`] = `null`; + +exports[` renders only div when no supported files are uploaded 1`] = `
`; diff --git a/src/components/FilePreview/components/FileRenderer/Banners/ErrorBanner.test.jsx b/src/components/FilePreview/components/FileRenderer/Banners/ErrorBanner.test.jsx new file mode 100644 index 00000000..28abbbdb --- /dev/null +++ b/src/components/FilePreview/components/FileRenderer/Banners/ErrorBanner.test.jsx @@ -0,0 +1,32 @@ +import { shallow } from '@edx/react-unit-test-utils'; + +import ErrorBanner from './ErrorBanner'; + +describe('', () => { + const props = { + headerMessage: { + id: 'headerMessageId', + defaultMessage: 'headerMessage', + }, + actions: [ + { + id: 'actionId', + onClick: jest.fn().mockName('onClick'), + message: { + id: 'actionMessageId', + defaultMessage: 'actionMessage', + }, + }, + ], + }; + + it('renders correctly', () => { + const wrapper = shallow(children); + expect(wrapper.snapshot).toMatchSnapshot(); + }); + + it('renders without actions', () => { + const wrapper = shallow(children); + expect(wrapper.snapshot).toMatchSnapshot(); + }); +}); diff --git a/src/components/FilePreview/components/FileRenderer/Banners/LoadingBanner.test.jsx b/src/components/FilePreview/components/FileRenderer/Banners/LoadingBanner.test.jsx new file mode 100644 index 00000000..bf5e8528 --- /dev/null +++ b/src/components/FilePreview/components/FileRenderer/Banners/LoadingBanner.test.jsx @@ -0,0 +1,10 @@ +import { shallow } from '@edx/react-unit-test-utils'; + +import LoadingBanner from './LoadingBanner'; + +describe('', () => { + it('renders correctly', () => { + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + }); +}); diff --git a/src/components/FilePreview/components/FileRenderer/Banners/__snapshots__/ErrorBanner.test.jsx.snap b/src/components/FilePreview/components/FileRenderer/Banners/__snapshots__/ErrorBanner.test.jsx.snap new file mode 100644 index 00000000..0f818137 --- /dev/null +++ b/src/components/FilePreview/components/FileRenderer/Banners/__snapshots__/ErrorBanner.test.jsx.snap @@ -0,0 +1,34 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders correctly 1`] = ` + + actionMessage + , + ] + } + variant="danger" +> + + headerMessage + + children + +`; + +exports[` renders without actions 1`] = ` + + + headerMessage + + children + +`; diff --git a/src/components/FilePreview/components/FileRenderer/Banners/__snapshots__/LoadingBanner.test.jsx.snap b/src/components/FilePreview/components/FileRenderer/Banners/__snapshots__/LoadingBanner.test.jsx.snap new file mode 100644 index 00000000..8908865f --- /dev/null +++ b/src/components/FilePreview/components/FileRenderer/Banners/__snapshots__/LoadingBanner.test.jsx.snap @@ -0,0 +1,12 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders correctly 1`] = ` + + + +`; diff --git a/src/components/FilePreview/components/FileRenderer/BaseRenderers/ImageRenderer.test.jsx b/src/components/FilePreview/components/FileRenderer/BaseRenderers/ImageRenderer.test.jsx new file mode 100644 index 00000000..20795a23 --- /dev/null +++ b/src/components/FilePreview/components/FileRenderer/BaseRenderers/ImageRenderer.test.jsx @@ -0,0 +1,17 @@ +import { shallow } from '@edx/react-unit-test-utils'; + +import ImageRenderer from './ImageRenderer'; + +describe('', () => { + const props = { + fileName: 'fileName', + url: 'url', + onError: jest.fn().mockName('onError'), + onSuccess: jest.fn().mockName('onSuccess'), + }; + + it('renders default', () => { + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + }); +}); diff --git a/src/components/FilePreview/components/FileRenderer/BaseRenderers/PDFRenderer.test.jsx b/src/components/FilePreview/components/FileRenderer/BaseRenderers/PDFRenderer.test.jsx new file mode 100644 index 00000000..d2daf081 --- /dev/null +++ b/src/components/FilePreview/components/FileRenderer/BaseRenderers/PDFRenderer.test.jsx @@ -0,0 +1,67 @@ +import React from 'react'; +import { shallow } from '@edx/react-unit-test-utils'; + +import PDFRenderer from './PDFRenderer'; + +import { usePDFRendererData } from './pdfHooks'; + +jest.mock('react-pdf', () => ({ + pdfjs: { GlobalWorkerOptions: {} }, + Document: () => 'Document', + Page: () => 'Page', +})); + +jest.mock('./pdfHooks', () => ({ + usePDFRendererData: jest.fn(), +})); + +describe('PDF Renderer Component', () => { + const props = { + url: 'some_url.pdf', + onError: jest.fn().mockName('this.props.onError'), + onSuccess: jest.fn().mockName('this.props.onSuccess'), + }; + const hookProps = { + pageNumber: 1, + numPages: 10, + relativeHeight: 200, + wrapperRef: { current: 'wrapperRef' }, + onDocumentLoadSuccess: jest.fn().mockName('onDocumentLoadSuccess'), + onLoadPageSuccess: jest.fn().mockName('onLoadPageSuccess'), + onDocumentLoadError: jest.fn().mockName('onDocumentLoadError'), + onInputPageChange: jest.fn().mockName('onInputPageChange'), + onNextPageButtonClick: jest.fn().mockName('onNextPageButtonClick'), + onPrevPageButtonClick: jest.fn().mockName('onPrevPageButtonClick'), + hasNext: true, + hasPref: false, + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + describe('snapshots', () => { + test('first page, prev is disabled', () => { + usePDFRendererData.mockReturnValue(hookProps); + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + const [prevButton, nextButton] = wrapper.instance.findByType('IconButton'); + expect(prevButton.props.disabled).toBe(true); + expect(nextButton.props.disabled).toBe(false); + }); + test('on last page, next is disabled', () => { + usePDFRendererData.mockReturnValue({ + ...hookProps, + pageNumber: hookProps.numPages, + hasNext: false, + hasPrev: true, + }); + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + const [prevButton, nextButton] = wrapper.instance.findByType('IconButton'); + expect(prevButton.props.disabled).toBe(false); + expect(nextButton.props.disabled).toBe(true); + }); + }); +}); diff --git a/src/components/FilePreview/components/FileRenderer/BaseRenderers/TXTRenderer.test.jsx b/src/components/FilePreview/components/FileRenderer/BaseRenderers/TXTRenderer.test.jsx new file mode 100644 index 00000000..99af08ca --- /dev/null +++ b/src/components/FilePreview/components/FileRenderer/BaseRenderers/TXTRenderer.test.jsx @@ -0,0 +1,20 @@ +import { shallow } from '@edx/react-unit-test-utils'; + +import TXTRenderer from './TXTRenderer'; + +jest.mock('./textHooks', () => ({ + useTextRendererData: jest.fn().mockReturnValue({ content: 'content' }), +})); + +describe('', () => { + const props = { + url: 'url', + onError: jest.fn().mockName('onError'), + onSuccess: jest.fn().mockName('onSuccess'), + }; + + it('renders default', () => { + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + }); +}); diff --git a/src/components/FilePreview/components/FileRenderer/BaseRenderers/__snapshots__/ImageRenderer.test.jsx.snap b/src/components/FilePreview/components/FileRenderer/BaseRenderers/__snapshots__/ImageRenderer.test.jsx.snap new file mode 100644 index 00000000..53b34faf --- /dev/null +++ b/src/components/FilePreview/components/FileRenderer/BaseRenderers/__snapshots__/ImageRenderer.test.jsx.snap @@ -0,0 +1,11 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders default 1`] = ` +fileName +`; diff --git a/src/components/FilePreview/components/FileRenderer/BaseRenderers/__snapshots__/PDFRenderer.test.jsx.snap b/src/components/FilePreview/components/FileRenderer/BaseRenderers/__snapshots__/PDFRenderer.test.jsx.snap new file mode 100644 index 00000000..29e21ec3 --- /dev/null +++ b/src/components/FilePreview/components/FileRenderer/BaseRenderers/__snapshots__/PDFRenderer.test.jsx.snap @@ -0,0 +1,135 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PDF Renderer Component snapshots first page, prev is disabled 1`] = ` +
+ +
+ +
+
+ + + + + Page + + + + of + 10 + + + + +
+`; + +exports[`PDF Renderer Component snapshots on last page, next is disabled 1`] = ` +
+ +
+ +
+
+ + + + + Page + + + + of + 10 + + + + +
+`; diff --git a/src/components/FilePreview/components/FileRenderer/BaseRenderers/__snapshots__/TXTRenderer.test.jsx.snap b/src/components/FilePreview/components/FileRenderer/BaseRenderers/__snapshots__/TXTRenderer.test.jsx.snap new file mode 100644 index 00000000..58c2c859 --- /dev/null +++ b/src/components/FilePreview/components/FileRenderer/BaseRenderers/__snapshots__/TXTRenderer.test.jsx.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders default 1`] = ` +
+  content
+
+`; diff --git a/src/components/FilePreview/components/FileRenderer/BaseRenderers/pdfHooks.test.js b/src/components/FilePreview/components/FileRenderer/BaseRenderers/pdfHooks.test.js new file mode 100644 index 00000000..4d6e9c11 --- /dev/null +++ b/src/components/FilePreview/components/FileRenderer/BaseRenderers/pdfHooks.test.js @@ -0,0 +1,92 @@ +import React from 'react'; +import { mockUseKeyedState } from '@edx/react-unit-test-utils'; +import { when } from 'jest-when'; + +import { + usePDFRendererData, safeSetPageNumber, stateKeys, initialState, +} from './pdfHooks'; + +const state = mockUseKeyedState(stateKeys); + +describe('PDF Renderer hooks', () => { + describe('safeSetPageNumber', () => { + it('returns value handler that sets page number if valid', () => { + const rawSetPageNumber = jest.fn(); + const numPages = 10; + const goToPage = safeSetPageNumber({ numPages, rawSetPageNumber }); + // should not call rawSetPageNumber when page number smaller than 1 or greater than numPages + goToPage(0); + expect(rawSetPageNumber).not.toHaveBeenCalled(); + goToPage(numPages + 1); + expect(rawSetPageNumber).not.toHaveBeenCalled(); + // should call rawSetPageNumber when page number is valid + goToPage(numPages); + expect(rawSetPageNumber).toHaveBeenCalledWith(numPages); + }); + }); + + describe('usePDFRendererData', () => { + const onSuccess = jest.fn(); + const onError = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + state.mock(); + }); + afterEach(() => { + state.resetVals(); + }); + + it('start with initial state', () => { + usePDFRendererData({}); + state.expectInitializedWith(stateKeys.pageNumber, initialState.pageNumber); + state.expectInitializedWith(stateKeys.numPages, initialState.numPages); + state.expectInitializedWith(stateKeys.relativeHeight, initialState.relativeHeight); + }); + + it('calls onSuccess and sets numPages based on args', () => { + const out = usePDFRendererData({ onSuccess }); + out.onDocumentLoadSuccess({ numPages: 5 }); + expect(onSuccess).toHaveBeenCalled(); + state.expectSetStateCalledWith(stateKeys.numPages, 5); + }); + + it('sets relative height based on page size', () => { + when(React.useRef) + .calledWith() + .mockReturnValueOnce({ + current: { getBoundingClientRect: () => ({ width: 20 }) }, + }); + const out = usePDFRendererData({}); + + const page = { view: [0, 0, 20, 30] }; + out.onLoadPageSuccess(page); + expect(state.setState.relativeHeight).toHaveBeenCalledWith(30); + }); + + it('calls onErro if error happened', () => { + const out = usePDFRendererData({ onError }); + out.onDocumentLoadError('notFound'); + expect(onError).toHaveBeenCalledWith('notFound'); + }); + + it('has good page logic', () => { + // start with 3 pages + // this seems to be the only way to mock initial value + initialState.pageNumber = 2; + initialState.numPages = 3; + const out = usePDFRendererData({ onSuccess }); + // go to next page + out.onNextPageButtonClick(); + state.expectSetStateCalledWith(stateKeys.pageNumber, 3); + + // go to prev page + out.onPrevPageButtonClick(); + state.expectSetStateCalledWith(stateKeys.pageNumber, 1); + + // reset initial state + initialState.pageNumber = 1; + initialState.numPages = 1; + }); + }); +}); diff --git a/src/components/FilePreview/components/FileRenderer/BaseRenderers/textHooks.test.js b/src/components/FilePreview/components/FileRenderer/BaseRenderers/textHooks.test.js new file mode 100644 index 00000000..1a5c6179 --- /dev/null +++ b/src/components/FilePreview/components/FileRenderer/BaseRenderers/textHooks.test.js @@ -0,0 +1,60 @@ +import React from 'react'; +import { mockUseKeyedState } from '@edx/react-unit-test-utils'; + +import axios from 'axios'; +import { useTextRendererData, fetchFile, stateKeys } from './textHooks'; + +jest.mock('axios'); + +const state = mockUseKeyedState(stateKeys); + +describe('textHooks', () => { + const url = 'http://example.com'; + const setContent = jest.fn(); + const onError = jest.fn(); + const onSuccess = jest.fn(); + + describe('fetchFile', () => { + it('should call onSuccess and setContent when the request is successful', async () => { + const response = { data: 'file content' }; + axios.get.mockResolvedValue(response); + await fetchFile({ setContent, url, onSuccess }); + expect(onSuccess).toHaveBeenCalled(); + expect(setContent).toHaveBeenCalledWith(response.data); + }); + + it('should call onError when the request fails', async () => { + const response = { response: { status: 404 } }; + axios.get.mockRejectedValue(response); + await fetchFile({ url, onError }); + expect(onError).toHaveBeenCalledWith(response.response.status); + }); + }); + + describe('useTextRendererData', () => { + beforeEach(() => { + jest.clearAllMocks(); + state.mock(); + }); + afterEach(() => { state.resetVals(); }); + + it('start with empty content', () => { + useTextRendererData({}); + state.expectInitializedWith(stateKeys.content, ''); + }); + + it('update content after useEffect get call', async () => { + axios.get.mockResolvedValue({ data: 'file content' }); + useTextRendererData({ url, onError, onSuccess }); + // wouldn't call before useEffect + expect(axios.get).not.toHaveBeenCalled(); + const [[cb]] = React.useEffect.mock.calls; + cb(); + // because fetchFile was written with async/await, we need to wait for the next tick + await new Promise(process.nextTick); + expect(axios.get).toHaveBeenCalled(); + expect(onSuccess).toHaveBeenCalled(); + state.expectSetStateCalledWith(stateKeys.content, 'file content'); + }); + }); +}); diff --git a/src/components/FilePreview/components/FileRenderer/FileCard/__snapshots__/index.test.jsx.snap b/src/components/FilePreview/components/FileRenderer/FileCard/__snapshots__/index.test.jsx.snap new file mode 100644 index 00000000..69b7500f --- /dev/null +++ b/src/components/FilePreview/components/FileRenderer/FileCard/__snapshots__/index.test.jsx.snap @@ -0,0 +1,28 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders default 1`] = ` + + + fileName + + } + > +
+
+ children +
+
+
+
+`; diff --git a/src/components/FilePreview/components/FileRenderer/FileCard/index.test.jsx b/src/components/FilePreview/components/FileRenderer/FileCard/index.test.jsx new file mode 100644 index 00000000..052f6938 --- /dev/null +++ b/src/components/FilePreview/components/FileRenderer/FileCard/index.test.jsx @@ -0,0 +1,20 @@ +import { shallow } from '@edx/react-unit-test-utils'; + +import FileCard from './index'; + +describe('', () => { + const props = { + file: { + fileName: 'fileName', + }, + children:
children
, + defaultOpen: true, + }; + + it('renders default', () => { + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.instance.findByType('Collapsible')).toHaveLength(1); + }); +}); diff --git a/src/components/FilePreview/components/FileRenderer/__snapshots__/index.test.jsx.snap b/src/components/FilePreview/components/FileRenderer/__snapshots__/index.test.jsx.snap new file mode 100644 index 00000000..411d97d9 --- /dev/null +++ b/src/components/FilePreview/components/FileRenderer/__snapshots__/index.test.jsx.snap @@ -0,0 +1,47 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`FileRenderer Component render default 1`] = ` + + + +`; + +exports[`FileRenderer Component render error banner 1`] = ` + + + +`; + +exports[`FileRenderer Component render loading banner 1`] = ` + + + +`; diff --git a/src/components/FilePreview/components/FileRenderer/hooks.test.js b/src/components/FilePreview/components/FileRenderer/hooks.test.js new file mode 100644 index 00000000..5a7cbc9f --- /dev/null +++ b/src/components/FilePreview/components/FileRenderer/hooks.test.js @@ -0,0 +1,46 @@ +import { mockUseKeyedState } from '@edx/react-unit-test-utils'; + +import { useRenderData, stateKeys } from './hooks'; + +const state = mockUseKeyedState(stateKeys); + +describe('useRenderData', () => { + const props = { + file: { fileName: 'file.pdf', fileUrl: 'http://example.com' }, + formatMessage: jest.fn(), + }; + beforeEach(() => { + jest.clearAllMocks(); + state.mock(); + }); + afterEach(() => { + state.resetVals(); + }); + + it('start with initial state', () => { + useRenderData(props); + state.expectInitializedWith(stateKeys.errorStatus, null); + state.expectInitializedWith(stateKeys.isLoading, true); + }); + + it('stop loading with success', () => { + const out = useRenderData(props); + out.rendererProps.onSuccess(); + state.expectSetStateCalledWith(stateKeys.isLoading, false); + state.expectSetStateCalledWith(stateKeys.errorStatus, null); + }); + + it('stop loading with error', () => { + const out = useRenderData(props); + out.rendererProps.onError('error'); + state.expectSetStateCalledWith(stateKeys.isLoading, false); + state.expectSetStateCalledWith(stateKeys.errorStatus, 'error'); + }); + + it('retry resets error status and starts loading', () => { + const out = useRenderData(props); + out.error.actions[0].onClick(); + state.expectSetStateCalledWith(stateKeys.errorStatus, null); + state.expectSetStateCalledWith(stateKeys.isLoading, true); + }); +}); diff --git a/src/components/FilePreview/components/FileRenderer/index.jsx b/src/components/FilePreview/components/FileRenderer/index.jsx index 5120ea82..66b64cf2 100644 --- a/src/components/FilePreview/components/FileRenderer/index.jsx +++ b/src/components/FilePreview/components/FileRenderer/index.jsx @@ -20,9 +20,16 @@ export const FileRenderer = ({ file, defaultOpen }) => { rendererProps, } = useRenderData({ file, formatMessage }); + if (isLoading) { + return ( + + + + ); + } + return ( - {isLoading && } {errorStatus ? ( ) : ( diff --git a/src/components/FilePreview/components/FileRenderer/index.test.jsx b/src/components/FilePreview/components/FileRenderer/index.test.jsx new file mode 100644 index 00000000..68ab7178 --- /dev/null +++ b/src/components/FilePreview/components/FileRenderer/index.test.jsx @@ -0,0 +1,62 @@ +import { shallow } from '@edx/react-unit-test-utils'; + +import { useRenderData } from './hooks'; +import { FileRenderer } from './index'; + +jest.mock('./Banners', () => ({ + ErrorBanner: () => 'ErrorBanner', + LoadingBanner: () => 'LoadingBanner', +})); + +jest.mock('./hooks', () => ({ + useRenderData: jest.fn(), +})); + +describe('FileRenderer Component', () => { + const props = { + file: { + fileName: 'some_file', + fileUrl: 'some_url', + }, + defaultOpen: true, + }; + + const defaultRenderData = { + Renderer: () => 'Renderer', + isLoading: false, + errorStatus: false, + error: null, + rendererProps: { + abc: 123, + }, + }; + + it('render default', () => { + useRenderData.mockReturnValue(defaultRenderData); + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.instance.findByType('Renderer')).toHaveLength(1); + expect(wrapper.instance.findByType('LoadingBanner')).toHaveLength(0); + expect(wrapper.instance.findByType('ErrorBanner')).toHaveLength(0); + }); + + it('render loading banner', () => { + useRenderData.mockReturnValue({ ...defaultRenderData, isLoading: true }); + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.instance.findByType('LoadingBanner')).toHaveLength(1); + expect(wrapper.instance.findByType('ErrorBanner')).toHaveLength(0); + expect(wrapper.instance.findByType('Renderer')).toHaveLength(0); + }); + + it('render error banner', () => { + useRenderData.mockReturnValue({ errorStatus: true, error: { message: 'some_error' } }); + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.instance.findByType('ErrorBanner')).toHaveLength(1); + expect(wrapper.instance.findByType('LoadingBanner')).toHaveLength(0); + }); +}); diff --git a/src/components/FilePreview/index.test.jsx b/src/components/FilePreview/index.test.jsx new file mode 100644 index 00000000..75b05d1c --- /dev/null +++ b/src/components/FilePreview/index.test.jsx @@ -0,0 +1,57 @@ +import { shallow } from '@edx/react-unit-test-utils'; + +import { useResponse } from 'hooks/app'; +import { isSupported } from './components'; + +import FilePreview from './index'; + +jest.mock('hooks/app', () => ({ + useResponse: jest.fn(), +})); + +jest.mock('./components', () => ({ + FileRenderer: () => 'FileRenderer', + isSupported: jest.fn(), +})); + +describe('', () => { + const props = { + defaultCollapsePreview: false, + }; + it('renders nothing when no files are uploaded', () => { + useResponse.mockReturnValueOnce({ uploadedFiles: null }); + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + expect(wrapper.isEmptyRender()).toBe(true); + }); + + it('renders only div when no supported files are uploaded', () => { + useResponse.mockReturnValueOnce({ + uploadedFiles: [{ fileName: 'file1.txt' }], + }); + isSupported.mockReturnValueOnce(false); + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.instance.findByType('FileRenderer')).toHaveLength(0); + }); + + it('render only supported files', () => { + isSupported + .mockReturnValueOnce(false) + .mockReturnValueOnce(true) + .mockReturnValueOnce(true); + + useResponse.mockReturnValueOnce({ + uploadedFiles: [ + { fileName: 'file1.txt' }, + { fileName: 'file2.pdf' }, + { fileName: 'file3.jpg' }, + ], + }); + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.instance.findByType('FileRenderer')).toHaveLength(2); + }); +}); diff --git a/src/components/FileUpload/ActionCell.test.jsx b/src/components/FileUpload/ActionCell.test.jsx new file mode 100644 index 00000000..52835616 --- /dev/null +++ b/src/components/FileUpload/ActionCell.test.jsx @@ -0,0 +1,48 @@ +import { shallow } from '@edx/react-unit-test-utils'; + +import { useDeleteFileAction } from 'hooks/actions'; + +import ActionCell from './ActionCell'; + +jest.mock('components/ConfirmDialog', () => 'ConfirmDialog'); +jest.mock('hooks/actions', () => ({ + useDeleteFileAction: jest.fn(), +})); + +describe('', () => { + const props = { + onDeletedFile: jest.fn().mockName('onDeletedFile'), + disabled: false, + row: { + original: { + fileIndex: 1, + }, + }, + }; + + const deleteFileAction = { + action: { + onClick: jest.fn().mockName('onClick'), + }, + confirmProps: { + abc: 123, + }, + }; + + useDeleteFileAction.mockReturnValue(deleteFileAction); + + it('render empty on disabled', () => { + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.isEmptyRender()).toBe(true); + }); + + it('render action cell and confirm dialog', () => { + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.instance.findByType('IconButton')).toHaveLength(1); + expect(wrapper.instance.findByType('ConfirmDialog')).toHaveLength(1); + }); +}); diff --git a/src/components/FileUpload/FileDownload.test.jsx b/src/components/FileUpload/FileDownload.test.jsx new file mode 100644 index 00000000..100d5f02 --- /dev/null +++ b/src/components/FileUpload/FileDownload.test.jsx @@ -0,0 +1,41 @@ +import { shallow } from '@edx/react-unit-test-utils'; + +import FileDownload from './FileDownload'; + +jest.mock('react-router', () => ({ + useParams: () => ({ + xblockId: 'xblockId', + }), +})); +jest.mock('./hooks', () => ({ + useFileDownloadHooks: () => ({ + downloadFiles: jest.fn().mockName('downloadFiles'), + status: 'not that it matters', + }), +})); + +describe('', () => { + const props = { + files: [ + { + fileUrl: 'fileUrl', + fileName: 'fileName', + fileDescription: 'fileDescription', + }, + ], + }; + + it('render empty on no files', () => { + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.isEmptyRender()).toBe(true); + }); + + it('render download button', () => { + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.instance.findByType('StatefulButton')).toHaveLength(1); + }); +}); diff --git a/src/components/FileUpload/FileMetaDisplay.test.jsx b/src/components/FileUpload/FileMetaDisplay.test.jsx new file mode 100644 index 00000000..7379aa2e --- /dev/null +++ b/src/components/FileUpload/FileMetaDisplay.test.jsx @@ -0,0 +1,25 @@ +import { shallow } from '@edx/react-unit-test-utils'; + +import fileSize from 'filesize'; + +import FileMetaDisplay from './FileMetaDisplay'; + +jest.mock('filesize'); + +describe('', () => { + const props = { + name: 'name', + description: 'description', + size: 123456, + }; + it('render file meta display', () => { + fileSize.mockReturnValue('123 KB'); + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + }); + + it('render file meta display with no size', () => { + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + }); +}); diff --git a/src/components/FileUpload/UploadConfirmModal.jsx b/src/components/FileUpload/UploadConfirmModal.jsx index 7e24bc28..2454d959 100644 --- a/src/components/FileUpload/UploadConfirmModal.jsx +++ b/src/components/FileUpload/UploadConfirmModal.jsx @@ -82,13 +82,14 @@ UploadConfirmModal.defaultProps = { open: false, closeHandler: () => {}, uploadHandler: () => {}, + file: null, }; UploadConfirmModal.propTypes = { open: PropTypes.bool, file: PropTypes.shape({ name: PropTypes.string, description: PropTypes.string, - }).isRequired, + }), closeHandler: PropTypes.func, uploadHandler: PropTypes.func, }; diff --git a/src/components/FileUpload/UploadConfirmModal.test.jsx b/src/components/FileUpload/UploadConfirmModal.test.jsx new file mode 100644 index 00000000..3cac01dc --- /dev/null +++ b/src/components/FileUpload/UploadConfirmModal.test.jsx @@ -0,0 +1,41 @@ +import { shallow } from '@edx/react-unit-test-utils'; + +import { useUploadConfirmModalHooks } from './hooks'; + +import UploadConfirmModal from './UploadConfirmModal'; + +jest.mock('./hooks', () => ({ + useUploadConfirmModalHooks: jest.fn(), +})); + +describe('', () => { + const props = { + open: true, + file: { + name: 'file name', + }, + closeHandler: jest.fn(), + uploadHandler: jest.fn(), + }; + + useUploadConfirmModalHooks.mockReturnValue({ + shouldShowError: false, + exitHandler: jest.fn().mockName('exitHandler'), + confirmUploadClickHandler: jest.fn().mockName('confirmUploadClickHandler'), + onFileDescriptionChange: jest.fn().mockName('onFileDescriptionChange'), + }); + + it('render upload confirm modal', () => { + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.instance.findByType('Form.Group')).toHaveLength(1); + }); + + it('render upload confirm modal with no file', () => { + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.instance.findByType('Form.Group')).toHaveLength(0); + }); +}); diff --git a/src/components/FileUpload/__snapshots__/ActionCell.test.jsx.snap b/src/components/FileUpload/__snapshots__/ActionCell.test.jsx.snap new file mode 100644 index 00000000..35b6b509 --- /dev/null +++ b/src/components/FileUpload/__snapshots__/ActionCell.test.jsx.snap @@ -0,0 +1,17 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` render action cell and confirm dialog 1`] = ` + + + + +`; + +exports[` render empty on disabled 1`] = `false`; diff --git a/src/components/FileUpload/__snapshots__/FileDownload.test.jsx.snap b/src/components/FileUpload/__snapshots__/FileDownload.test.jsx.snap new file mode 100644 index 00000000..2e1f88fb --- /dev/null +++ b/src/components/FileUpload/__snapshots__/FileDownload.test.jsx.snap @@ -0,0 +1,56 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` render download button 1`] = ` +, + "idle": , + "loading": , + "success": , + } + } + labels={ + Object { + "error": , + "idle": , + "loading": , + "success": , + } + } + onClick={[MockFunction downloadFiles]} + state="not that it matters" +/> +`; + +exports[` render empty on no files 1`] = `null`; diff --git a/src/components/FileUpload/__snapshots__/FileMetaDisplay.test.jsx.snap b/src/components/FileUpload/__snapshots__/FileMetaDisplay.test.jsx.snap new file mode 100644 index 00000000..bfec06ba --- /dev/null +++ b/src/components/FileUpload/__snapshots__/FileMetaDisplay.test.jsx.snap @@ -0,0 +1,89 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` render file meta display 1`] = ` + +
+ + + +
+ name +
+
+ + + +
+ description +
+
+ + + +
+ 123 KB +
+
+`; + +exports[` render file meta display with no size 1`] = ` + +
+ + + +
+ name +
+
+ + + +
+ description +
+
+ + + +
+ Unknown +
+
+`; diff --git a/src/components/FileUpload/__snapshots__/UploadConfirmModal.test.jsx.snap b/src/components/FileUpload/__snapshots__/UploadConfirmModal.test.jsx.snap new file mode 100644 index 00000000..ed1ddd4a --- /dev/null +++ b/src/components/FileUpload/__snapshots__/UploadConfirmModal.test.jsx.snap @@ -0,0 +1,111 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` render upload confirm modal 1`] = ` + + + + Add a text description to your file + + + + +
+ + + + Description for: + + + file name + + + + +
+
+
+ + + + Cancel upload + + + + +
+`; + +exports[` render upload confirm modal with no file 1`] = ` + + + + Add a text description to your file + + + + +
+ + + + + + Cancel upload + + + + + +`; diff --git a/src/components/FileUpload/__snapshots__/index.test.jsx.snap b/src/components/FileUpload/__snapshots__/index.test.jsx.snap new file mode 100644 index 00000000..035280cd --- /dev/null +++ b/src/components/FileUpload/__snapshots__/index.test.jsx.snap @@ -0,0 +1,338 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` render default 1`] = ` +
+

+ File upload +

+ + Uploaded files + + , + ] + } + /> + +
+`; + +exports[` render empty on studentTraining 1`] = `null`; + +exports[` render empty when file upload is not enabled 1`] = `null`; + +exports[` render extra columns when activeStepName is submission 1`] = ` +
+

+ File upload +

+ + Uploaded files + + , + ] + } + /> + +
+`; + +exports[` render without dropzone and confirm modal when isReadOnly 1`] = ` +
+

+ File upload +

+ + + Uploaded files + + , + ] + } + /> +
+`; + +exports[` render without file preview if uploadedFiles are empty and isReadOnly 1`] = ` +
+

+ File upload +

+ + Uploaded files + + , + ] + } + /> +
+`; + +exports[` render without header 1`] = ` +
+ + Uploaded files + + , + ] + } + /> + +
+`; + +exports[`createFileActionCell should return a function that is an action cell 1`] = ` + +`; diff --git a/src/components/FileUpload/hooks.js b/src/components/FileUpload/hooks.js index fd461aa1..49bcfb21 100644 --- a/src/components/FileUpload/hooks.js +++ b/src/components/FileUpload/hooks.js @@ -102,4 +102,5 @@ export const useFileDownloadHooks = ({ files, zipFileName }) => { export default { useUploadConfirmModalHooks, useFileUploadHooks, + useFileDownloadHooks, }; diff --git a/src/components/FileUpload/hooks.test.js b/src/components/FileUpload/hooks.test.js new file mode 100644 index 00000000..c0f17d0a --- /dev/null +++ b/src/components/FileUpload/hooks.test.js @@ -0,0 +1,110 @@ +import { mockUseKeyedState } from '@edx/react-unit-test-utils'; + +import { + useUploadConfirmModalHooks, + useFileDownloadHooks, + useFileUploadHooks, + stateKeys, +} from './hooks'; + +jest.mock('hooks/app', () => ({ + useDownloadFiles: jest.fn(() => ({ + mutate: () => 'download-result', + status: 'anything', + })), +})); + +const state = mockUseKeyedState(stateKeys); + +describe('File Upload hooks', () => { + describe('useUploadConfirmModalHooks', () => { + const props = { + file: { fileName: 'file.pdf', fileUrl: 'http://example.com' }, + closeHandler: jest.fn(), + uploadHandler: jest.fn(), + }; + beforeEach(() => { + jest.clearAllMocks(); + state.mock(); + }); + afterEach(() => { + state.resetVals(); + }); + + it('start with initial state', () => { + useUploadConfirmModalHooks(props); + state.expectInitializedWith(stateKeys.description, ''); + state.expectInitializedWith(stateKeys.shouldShowError, false); + }); + + it('confirm upload with description', () => { + state.mockVal(stateKeys.description, 'description'); + const out = useUploadConfirmModalHooks(props); + out.confirmUploadClickHandler(); + expect(props.uploadHandler).toBeCalledWith(props.file, 'description'); + }); + + it('confirm upload without description', () => { + state.mockVal(stateKeys.description, ''); + const out = useUploadConfirmModalHooks(props); + out.confirmUploadClickHandler(); + state.expectSetStateCalledWith(stateKeys.shouldShowError, true); + }); + + it('exit handler', () => { + const out = useUploadConfirmModalHooks(props); + out.exitHandler(); + state.expectSetStateCalledWith(stateKeys.shouldShowError, false); + state.expectSetStateCalledWith(stateKeys.description, ''); + expect(props.closeHandler).toBeCalled(); + }); + + it('on file description change', () => { + const out = useUploadConfirmModalHooks(props); + out.onFileDescriptionChange({ target: { value: 'new description' } }); + state.expectSetStateCalledWith(stateKeys.description, 'new description'); + }); + }); + + describe('useFileUploadHooks', () => { + const props = { + onFileUploaded: jest.fn(), + }; + beforeEach(() => { + jest.clearAllMocks(); + state.mock(); + }); + afterEach(() => { + state.resetVals(); + }); + + it('start with initial state', () => { + useFileUploadHooks(props); + state.expectInitializedWith(stateKeys.uploadArgs, {}); + state.expectInitializedWith(stateKeys.isModalOpen, false); + }); + + it('confirm upload', () => { + const out = useFileUploadHooks(props); + out.confirmUpload.useCallback.cb(); + state.expectSetStateCalledWith(stateKeys.isModalOpen, false); + }); + }); + + describe('useFileDownloadHooks', () => { + const props = { + file: { fileName: 'file.pdf', fileUrl: 'http://example.com' }, + zipFileName: 'zipFileName', + }; + + it('status from mutation', () => { + const out = useFileDownloadHooks(props); + out.status = 'anything'; + }); + + it('download files', () => { + const out = useFileDownloadHooks(props); + expect(out.downloadFiles()).toBe('download-result'); + }); + }); +}); diff --git a/src/components/FileUpload/index.jsx b/src/components/FileUpload/index.jsx index 9327a417..ae286db7 100644 --- a/src/components/FileUpload/index.jsx +++ b/src/components/FileUpload/index.jsx @@ -8,13 +8,14 @@ import { useIntl } from '@edx/frontend-platform/i18n'; import { nullMethod } from 'utils'; import { useActiveStepName, useFileUploadConfig } from 'hooks/app'; import { useViewStep } from 'hooks/routing'; -import FilePreview from 'components/FilePreview'; import { stepNames } from 'constants/index'; +import FilePreview from 'components/FilePreview'; import UploadConfirmModal from './UploadConfirmModal'; import ActionCell from './ActionCell'; -import { useFileUploadHooks } from './hooks'; import FileDownload from './FileDownload'; + +import { useFileUploadHooks } from './hooks'; import messages from './messages'; import './styles.scss'; @@ -86,7 +87,7 @@ const FileUpload = ({ itemCount={uploadedFiles.length} data={uploadedFiles.map((file) => ({ ...file, - size: typeof file.size === 'number' ? filesize(file.size) : 'Unknown', + fileSize: typeof file.fileSize === 'number' ? filesize(file.fileSize) : 'Unknown', }))} tableActions={[]} columns={columns} @@ -128,7 +129,8 @@ FileUpload.propTypes = { PropTypes.shape({ fileDescription: PropTypes.string, fileName: PropTypes.string, - fileSize: PropTypes.number, + // eslint-disable-next-line react/forbid-prop-types + fileSize: PropTypes.any, }), ), onFileUploaded: PropTypes.func, diff --git a/src/components/FileUpload/index.test.jsx b/src/components/FileUpload/index.test.jsx new file mode 100644 index 00000000..b8ac4bc9 --- /dev/null +++ b/src/components/FileUpload/index.test.jsx @@ -0,0 +1,127 @@ +import { shallow } from '@edx/react-unit-test-utils'; + +import { useActiveStepName, useFileUploadConfig } from 'hooks/app'; +import { useViewStep } from 'hooks/routing'; +import { stepNames } from 'constants/index'; + +import { useFileUploadHooks } from './hooks'; + +import FileUpload, { createFileActionCell } from './index'; + +jest.mock('hooks/app', () => ({ + useActiveStepName: jest.fn(), + useFileUploadConfig: jest.fn(), +})); +jest.mock('hooks/routing', () => ({ + useViewStep: jest.fn(), +})); +jest.mock('./hooks', () => ({ + useFileUploadHooks: jest.fn(), +})); +jest.mock('./UploadConfirmModal', () => 'UploadConfirmModal'); +jest.mock('./ActionCell', () => 'ActionCell'); +jest.mock('./FileDownload', () => 'FileDownload'); +jest.mock('components/FilePreview', () => 'FilePreview'); + +describe('', () => { + const props = { + isReadOnly: false, + uploadedFiles: [ + { + abc: 123, + fileSize: 123, + }, + { + def: 456, + fileSize: 'will be unknown', + }, + ], + onFileUploaded: jest.fn(), + onDeletedFile: jest.fn(), + defaultCollapsePreview: false, + hideHeader: false, + }; + + const defaultFileUploadHooks = { + confirmUpload: jest.fn().mockName('confirmUpload'), + closeUploadModal: jest.fn().mockName('closeUploadModal'), + isModalOpen: false, + onProcessUpload: jest.fn().mockName('onProcessUpload'), + uploadArgs: { + abc: 123, + }, + }; + + const defaultUploadConfig = { + enabled: true, + fileUploadLimit: 10, + allowedExtensions: ['pdf', 'jpg'], + maxFileSize: 123456, + }; + + beforeEach(() => { + useActiveStepName.mockReturnValue('someActiveStep'); + useViewStep.mockReturnValue('someStep'); + useFileUploadHooks.mockReturnValue(defaultFileUploadHooks); + useFileUploadConfig.mockReturnValue(defaultUploadConfig); + }); + + it('render default', () => { + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + }); + + it('render without header', () => { + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + }); + + it('render without file preview if uploadedFiles are empty and isReadOnly', () => { + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.instance.findByType('FilePreview')).toHaveLength(0); + }); + + it('render without dropzone and confirm modal when isReadOnly', () => { + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.instance.findByType('UploadConfirmModal')).toHaveLength(0); + expect(wrapper.instance.findByType('Dropzone')).toHaveLength(0); + }); + + it('render empty on studentTraining', () => { + useViewStep.mockReturnValueOnce(stepNames.studentTraining); + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + }); + + it('render empty when file upload is not enabled', () => { + useFileUploadConfig.mockReturnValueOnce({ enabled: false }); + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + }); + + it('render extra columns when activeStepName is submission', () => { + useActiveStepName.mockReturnValueOnce(stepNames.submission); + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.instance.findByType('DataTable')[0].props.columns).toHaveLength(4); + }); +}); + +describe('createFileActionCell', () => { + it('should return a function that is an action cell', () => { + const onDeletedFile = jest.fn(); + const isReadOnly = false; + const FileActionCell = createFileActionCell({ onDeletedFile, isReadOnly }); + expect(typeof FileActionCell).toBe('function'); + + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.instance.findByType('ActionCell')).toHaveLength(1); + }); +}); diff --git a/src/components/HotjarSurvey/index.jsx b/src/components/HotjarSurvey/index.jsx index 52ea9bdc..d02489cf 100644 --- a/src/components/HotjarSurvey/index.jsx +++ b/src/components/HotjarSurvey/index.jsx @@ -1,8 +1,8 @@ import React, { useEffect } from 'react'; import { useGlobalState, useStepInfo, useAssessmentStepConfig } from 'hooks/app'; -import { stepNames, stepStates } from 'constants'; +import { stepNames, stepStates } from 'constants/index'; -export const HotjarSurvey = () => { +const HotjarSurvey = () => { const configInfo = useAssessmentStepConfig(); const stepInfo = useStepInfo(); const isSelfRequired = configInfo.settings[stepNames.self].required; @@ -31,7 +31,7 @@ export const HotjarSurvey = () => { if (isShowSurvey && window.hj) { window.hj('event', 'lms_openassessment_survey_mfe'); } - }); + }, [isShowSurvey]); return (
); diff --git a/src/components/HotjarSurvey/index.test.jsx b/src/components/HotjarSurvey/index.test.jsx new file mode 100644 index 00000000..783fd760 --- /dev/null +++ b/src/components/HotjarSurvey/index.test.jsx @@ -0,0 +1,113 @@ +import React from 'react'; +// import {cleanup, fireEvent, render} from '@testing-library/react'; +import { shallow } from '@edx/react-unit-test-utils'; + +import { + useGlobalState, + useStepInfo, + useAssessmentStepConfig, +} from 'hooks/app'; +import { stepNames, stepStates } from 'constants/index'; + +import HotjarSurvey from './index'; + +jest.mock('hooks/app', () => ({ + useGlobalState: jest.fn(), + useStepInfo: jest.fn(), + useAssessmentStepConfig: jest.fn(), +})); + +describe('', () => { + // * These tests opts to use shallow instead of render because I want to trigger the useEffect + // * manually. + beforeEach(() => { + window.hj = jest.fn(); + }); + afterEach(() => { + delete window.hj; + jest.clearAllMocks(); + }); + + it('show survey when self is require and step is not while peer is not require', () => { + useAssessmentStepConfig.mockReturnValueOnce({ + settings: { + self: { required: true }, + peer: { required: false }, + }, + }); + useGlobalState.mockReturnValueOnce({ + activeStepState: stepStates.done, + }); + + expect(React.useEffect).not.toHaveBeenCalled(); + shallow(); + + const [[cb, [isShowSurvey]]] = React.useEffect.mock.calls; + expect(isShowSurvey).toBe(true); + cb(); + expect(window.hj).toHaveBeenCalledWith( + 'event', + 'lms_openassessment_survey_mfe', + ); + }); + + it('show survey when peer is require iif completed the peer grading', () => { + useAssessmentStepConfig.mockReturnValueOnce({ + settings: { + self: { required: false }, + peer: { required: true, minNumberToGrade: 2 }, + }, + }); + useStepInfo.mockReturnValueOnce({ + [stepNames.peer]: { numberOfAssessmentsCompleted: 2 }, + }); + useGlobalState.mockReturnValueOnce({ + activeStepState: 'abitrairy', + }); + + expect(React.useEffect).not.toHaveBeenCalled(); + shallow(); + + const [[, [isShowSurvey]]] = React.useEffect.mock.calls; + expect(isShowSurvey).toBe(true); + }); + + it('should not show survey when peer is require but not completed the peer grading', () => { + useAssessmentStepConfig.mockReturnValueOnce({ + settings: { + self: { required: false }, + peer: { required: true, minNumberToGrade: 2 }, + }, + }); + useStepInfo.mockReturnValueOnce({ + [stepNames.peer]: { numberOfAssessmentsCompleted: 1 }, + }); + useGlobalState.mockReturnValueOnce({ + activeStepState: 'abitrairy', + }); + + expect(React.useEffect).not.toHaveBeenCalled(); + shallow(); + + const [[, [isShowSurvey]]] = React.useEffect.mock.calls; + expect(isShowSurvey).toBe(false); + }); + + it('should not show survey neither step is required', () => { + useAssessmentStepConfig.mockReturnValueOnce({ + settings: { + [stepNames.self]: { required: false }, + [stepNames.peer]: { required: false }, + }, + }); + useGlobalState.mockReturnValueOnce({ + activeStepState: 'abitrairy', + }); + + expect(React.useEffect).not.toHaveBeenCalled(); + + shallow(); + const [[, [isShowSurvey]]] = React.useEffect.mock.calls; + expect(isShowSurvey).toBe(false); + }); +}); diff --git a/src/components/InfoPopover/__snapshots__/index.test.jsx.snap b/src/components/InfoPopover/__snapshots__/index.test.jsx.snap new file mode 100644 index 00000000..14993638 --- /dev/null +++ b/src/components/InfoPopover/__snapshots__/index.test.jsx.snap @@ -0,0 +1,34 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders correctly 1`] = ` + + + +
+ Children +
+
+ + } + placement="bottom" + trigger="focus" + > + +
+
+`; diff --git a/src/components/InfoPopover/index.jsx b/src/components/InfoPopover/index.jsx index 5da13eb6..aa998c51 100644 --- a/src/components/InfoPopover/index.jsx +++ b/src/components/InfoPopover/index.jsx @@ -17,7 +17,7 @@ import messages from './messages'; /** * */ -export const InfoPopover = ({ onClick, children }) => { +const InfoPopover = ({ onClick, children }) => { const { formatMessage } = useIntl(); return ( diff --git a/src/components/InfoPopover/index.test.jsx b/src/components/InfoPopover/index.test.jsx new file mode 100644 index 00000000..17928ff6 --- /dev/null +++ b/src/components/InfoPopover/index.test.jsx @@ -0,0 +1,20 @@ +import { shallow } from '@edx/react-unit-test-utils'; + +import InfoPopover from './index'; + +describe('', () => { + const props = { + onClick: jest.fn().mockName('onClick'), + }; + + const renderComponent = () => shallow( + +
Children
+
, + ); + + it('renders correctly', () => { + const wrapper = renderComponent(); + expect(wrapper.snapshot).toMatchSnapshot(); + }); +}); diff --git a/src/components/Instructions/__snapshots__/index.test.jsx.snap b/src/components/Instructions/__snapshots__/index.test.jsx.snap new file mode 100644 index 00000000..6bba04ce --- /dev/null +++ b/src/components/Instructions/__snapshots__/index.test.jsx.snap @@ -0,0 +1,19 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` render empty on no message 1`] = `null`; + +exports[` render message 1`] = ` +
+

+ + Instructions + : + + arbitrarilyInstructionsMessage +

+
+`; diff --git a/src/components/Instructions/index.jsx b/src/components/Instructions/index.jsx index f8617909..d6f5bb33 100644 --- a/src/components/Instructions/index.jsx +++ b/src/components/Instructions/index.jsx @@ -2,12 +2,6 @@ import React from 'react'; import { useIntl } from '@edx/frontend-platform/i18n'; -import { stepNames, stepStates } from 'constants/index'; - -import { useGlobalState } from 'hooks/app'; -import { useViewStep } from 'hooks/routing'; -import { isXblockStep } from 'utils'; - import useInstructionsMessage from './useInstructionsMessage'; import messages from './messages'; @@ -15,10 +9,7 @@ import messages from './messages'; const Instructions = () => { const { formatMessage } = useIntl(); const message = useInstructionsMessage(); - const viewStep = useViewStep(); - const { activeStepName, stepState } = useGlobalState(); - const stepName = isXblockStep(viewStep) ? activeStepName : viewStep; - if (stepState !== stepStates.inProgress || stepName === stepNames.staff) { + if (!message) { return null; } return ( diff --git a/src/components/Instructions/index.test.jsx b/src/components/Instructions/index.test.jsx new file mode 100644 index 00000000..7fd3064f --- /dev/null +++ b/src/components/Instructions/index.test.jsx @@ -0,0 +1,23 @@ +import { shallow } from '@edx/react-unit-test-utils'; + +import useInstructionsMessage from './useInstructionsMessage'; + +import Instructions from './index'; + +jest.mock('./useInstructionsMessage', () => jest.fn()); + +describe('', () => { + it('render empty on no message', () => { + useInstructionsMessage.mockReturnValue(null); + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.isEmptyRender()).toBe(true); + }); + + it('render message', () => { + useInstructionsMessage.mockReturnValue('arbitrarilyInstructionsMessage'); + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + }); +}); diff --git a/src/components/Instructions/useInstructionsMessage.jsx b/src/components/Instructions/useInstructionsMessage.js similarity index 100% rename from src/components/Instructions/useInstructionsMessage.jsx rename to src/components/Instructions/useInstructionsMessage.js diff --git a/src/components/Instructions/useInstructionsMessage.test.js b/src/components/Instructions/useInstructionsMessage.test.js new file mode 100644 index 00000000..2dc85839 --- /dev/null +++ b/src/components/Instructions/useInstructionsMessage.test.js @@ -0,0 +1,74 @@ +import { useGlobalState } from 'hooks/app'; +import { useViewStep } from 'hooks/routing'; +import { isXblockStep } from 'utils'; + +import { stepNames, stepStates } from 'constants/index'; + +import useInstructionsMessage from './useInstructionsMessage'; + +import messages from './messages'; + +jest.mock('hooks/app', () => ({ + useGlobalState: jest.fn(), +})); +jest.mock('hooks/routing', () => ({ + useViewStep: jest.fn(), +})); +jest.mock('utils', () => ({ + isXblockStep: jest.fn(), +})); + +describe('useInstructionsMessage', () => { + it('return null when stepState is not inProgress', () => { + useGlobalState.mockReturnValueOnce({ + stepState: 'abitrarilyStepState', + activeStepName: 'abitrarilyActiveStepName', + }); + useViewStep.mockReturnValueOnce('abitrarilyViewStepName'); + isXblockStep.mockReturnValueOnce(true); + + expect(useInstructionsMessage()).toBe(null); + }); + + it('return null when stepName is staff', () => { + useGlobalState.mockReturnValueOnce({ + stepState: stepStates.inProgress, + activeStepName: stepNames.staff, + }); + useViewStep.mockReturnValueOnce('abitrarilyViewStepName'); + isXblockStep.mockReturnValueOnce(true); + + expect(useInstructionsMessage()).toBe(null); + }); + + it('render value when step is not staff and in progress', () => { + const step = stepNames.self; + useGlobalState.mockReturnValueOnce({ + stepState: stepStates.inProgress, + activeStepName: step, + }); + useViewStep.mockReturnValueOnce(step); + isXblockStep.mockReturnValueOnce(false); + + expect(useInstructionsMessage()).toBe(messages[step].defaultMessage); + }); + + it('render include effective grade when step is done', () => { + const step = stepNames.done; + const effectiveGrade = { + earned: 1, + possible: 2, + }; + useGlobalState.mockReturnValueOnce({ + stepState: stepStates.inProgress, + activeStepName: step, + effectiveGrade, + }); + useViewStep.mockReturnValueOnce(step); + isXblockStep.mockReturnValueOnce(false); + + const out = useInstructionsMessage(); + + expect(out).toContain(`${effectiveGrade.earned}/${effectiveGrade.possible}`); + }); +}); diff --git a/src/components/ModalActions/__snapshots__/index.test.jsx.snap b/src/components/ModalActions/__snapshots__/index.test.jsx.snap new file mode 100644 index 00000000..614e5c99 --- /dev/null +++ b/src/components/ModalActions/__snapshots__/index.test.jsx.snap @@ -0,0 +1,50 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` can render primary and secondary with confirm 1`] = ` +
+ + + + +
+`; + +exports[` can render primary and secondary without confirm 1`] = ` +
+ + +
+`; + +exports[` render empty when no actions 1`] = ` +
+`; + +exports[` render skeleton when page data is loading 1`] = ` + +`; diff --git a/src/components/ModalActions/hooks/useFinishedStateActions.test.js b/src/components/ModalActions/hooks/useFinishedStateActions.test.js new file mode 100644 index 00000000..60e5110a --- /dev/null +++ b/src/components/ModalActions/hooks/useFinishedStateActions.test.js @@ -0,0 +1,132 @@ +import { useGlobalState, useTrainingStepIsCompleted } from 'hooks/app'; +import { + useHasSubmitted, + useSubmittedAssessment, +} from 'hooks/assessment'; +import { useViewStep } from 'hooks/routing'; +import { + useStartStepAction, + useLoadNextAction, + useCloseModalAction, +} from 'hooks/actions'; +import { stepNames, stepStates } from 'constants/index'; + +import useFinishedStateActions from './useFinishedStateActions'; + +jest.mock('hooks/app', () => ({ + useGlobalState: jest.fn(), + useTrainingStepIsCompleted: jest.fn(), +})); +jest.mock('hooks/assessment', () => ({ + useHasSubmitted: jest.fn(), + useSubmittedAssessment: jest.fn(), +})); +jest.mock('hooks/routing', () => ({ + useViewStep: jest.fn(), +})); +jest.mock('hooks/actions', () => ({ + useStartStepAction: jest.fn(), + useLoadNextAction: jest.fn(), + useCloseModalAction: jest.fn(), +})); + +describe('useFinishedStateActions', () => { + const mockStartStepAction = jest.fn(); + const mockLoadNextAction = jest.fn(); + const mockCloseModalAction = jest.fn(); + + useGlobalState.mockReturnValue({}); + useTrainingStepIsCompleted.mockReturnValue(false); + useHasSubmitted.mockReturnValue(false); + useSubmittedAssessment.mockReturnValue(false); + useViewStep.mockReturnValue('abitrarilyViewStepName'); + + useStartStepAction.mockReturnValue(mockStartStepAction); + useLoadNextAction.mockReturnValue(mockLoadNextAction); + useCloseModalAction.mockReturnValue(mockCloseModalAction); + + describe('when has submitted', () => { + it('return null when has not submitted', () => { + expect(useFinishedStateActions()).toBe(null); + }); + + it('return null when has not submitted and is in training but not completed', () => { + useViewStep.mockReturnValue(stepNames.studentTraining); + expect(useFinishedStateActions()).toBe(null); + }); + + it('return start action as primary when has submitted and is in done step', () => { + useHasSubmitted.mockReturnValue(true); + useSubmittedAssessment.mockReturnValue(true); + useGlobalState.mockReturnValue({ activeStepName: stepNames.done }); + + expect(useFinishedStateActions()).toEqual({ primary: mockStartStepAction }); + }); + }); + + describe('when assessment submitted', () => { + useHasSubmitted.mockReturnValue(true); + useSubmittedAssessment.mockReturnValue(true); + + it('return start action as primary when step is done', () => { + useGlobalState.mockReturnValue({ activeStepName: stepNames.done }); + + expect(useFinishedStateActions()).toEqual({ primary: mockStartStepAction }); + }); + + it('return exit action as primary action when step is staff', () => { + useGlobalState.mockReturnValue({ activeStepName: stepNames.staff }); + + expect(useFinishedStateActions()).toEqual({ primary: mockCloseModalAction }); + }); + + it('return start and exit actions when view step is submission', () => { + useGlobalState.mockReturnValue({ activeStepName: stepNames.submission }); + useViewStep.mockReturnValue(stepNames.submission); + + expect(useFinishedStateActions()).toEqual({ primary: mockStartStepAction, secondary: mockCloseModalAction }); + }); + + it('return start and exit actions when view step is self', () => { + useGlobalState.mockReturnValue({ activeStepName: stepNames.self }); + useViewStep.mockReturnValue(stepNames.submission); + + expect(useFinishedStateActions()).toEqual({ primary: mockStartStepAction, secondary: mockCloseModalAction }); + }); + + it('return null when step is not active and step state is not in progress', () => { + useGlobalState.mockReturnValue({ activeStepName: 'abitrarilyActiveStepName', activeStepState: stepStates.done }); + useViewStep.mockReturnValue('abitrarilyViewStepName'); + + expect(useFinishedStateActions()).toBe(null); + }); + + it('return start and exit actions when step is not active and step state is in progress', () => { + useGlobalState.mockReturnValue({ activeStepName: 'abitrarilyActiveStepName', activeStepState: stepStates.inProgress }); + useViewStep.mockReturnValue('abitrarilyViewStepName'); + + expect(useFinishedStateActions()).toEqual({ primary: mockStartStepAction, secondary: mockCloseModalAction }); + }); + + it('return load next and exit actions when step is active and step state is in progress', () => { + useGlobalState.mockReturnValue({ activeStepName: 'active', activeStepState: stepStates.inProgress }); + useViewStep.mockReturnValue('active'); + + expect(useFinishedStateActions()).toEqual({ primary: mockLoadNextAction, secondary: mockCloseModalAction }); + }); + + it('return exit action when all steps are finished', () => { + useGlobalState.mockReturnValue({ activeStepName: 'active' }); + useViewStep.mockReturnValue('active'); + + expect(useFinishedStateActions()).toEqual({ primary: mockCloseModalAction }); + }); + }); + + it('return start and exit actions when submission is finished', () => { + useHasSubmitted.mockReturnValue(true); + useSubmittedAssessment.mockReturnValue(false); + + expect(useFinishedStateActions()).toEqual({ primary: mockStartStepAction, secondary: mockCloseModalAction }); + }); +}); diff --git a/src/components/ModalActions/hooks/useInProgressActions.test.js b/src/components/ModalActions/hooks/useInProgressActions.test.js new file mode 100644 index 00000000..51a71227 --- /dev/null +++ b/src/components/ModalActions/hooks/useInProgressActions.test.js @@ -0,0 +1,154 @@ +import { useGlobalState, useStepInfo } from 'hooks/app'; +import { useHasSubmitted } from 'hooks/assessment'; +import { useViewStep } from 'hooks/routing'; +import { stepNames, stepStates } from 'constants/index'; + +import { + useCloseModalAction, + useLoadNextAction, + useStartStepAction, + useSubmitResponseAction, +} from 'hooks/actions'; + +import useInProgressActions from './useInProgressActions'; + +jest.mock('hooks/app', () => ({ + useGlobalState: jest.fn(), + useStepInfo: jest.fn(), +})); +jest.mock('hooks/assessment', () => ({ + useHasSubmitted: jest.fn(), +})); +jest.mock('hooks/routing', () => ({ + useViewStep: jest.fn(), +})); +jest.mock('hooks/actions', () => ({ + useCloseModalAction: jest.fn(), + useLoadNextAction: jest.fn(), + useStartStepAction: jest.fn(), + useSubmitResponseAction: jest.fn(), +})); + +describe('useInProgressActions', () => { + const mockCloseModalAction = jest.fn().mockName('closeModalAction'); + const mockLoadNextAction = jest.fn().mockName('loadNextAction'); + const mockStartStepAction = jest.fn().mockName('startStepAction'); + const mockSubmitResponseAction = jest.fn().mockName('submitResponseAction'); + useCloseModalAction.mockReturnValue(mockCloseModalAction); + useLoadNextAction.mockReturnValue(mockLoadNextAction); + useStartStepAction.mockReturnValue(mockStartStepAction); + useSubmitResponseAction.mockReturnValue(mockSubmitResponseAction); + + useGlobalState.mockReturnValue({}); + useStepInfo.mockReturnValue({}); + useHasSubmitted.mockReturnValue(false); + useViewStep.mockReturnValue(stepNames.submission); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('return startStepAction and exitAction when hasReceivedFinalGrade is true', () => { + useGlobalState.mockReturnValueOnce({ hasReceivedFinalGrade: true }); + const result = useInProgressActions({ options: {} }); + expect(result).toEqual({ + primary: mockStartStepAction, + secondary: mockCloseModalAction, + }); + }); + + it('return null when hasSubmitted is true', () => { + useGlobalState.mockReturnValueOnce({ hasReceivedFinalGrade: false }); + useHasSubmitted.mockReturnValueOnce(true); + const result = useInProgressActions({ options: {} }); + expect(result).toBeNull(); + }); + + it('return null when activeStepState is not inProgress', () => { + useGlobalState.mockReturnValueOnce({ + hasReceivedFinalGrade: false, + activeStepState: stepStates.done, + }); + const result = useInProgressActions({ options: {} }); + expect(result).toBeNull(); + }); + + it('return null if globalState.stepState is inProgress and step is not submission', () => { + useGlobalState.mockReturnValueOnce({ + hasReceivedFinalGrade: false, + activeStepState: stepStates.inProgress, + stepState: stepStates.inProgress, + }); + useViewStep.mockReturnValueOnce(stepNames.peer); + const result = useInProgressActions({ options: {} }); + expect(result).toBeNull(); + }); + + it('return activeSubmissionConfig when globalState.stepState is inProgress and step is submission', () => { + useGlobalState.mockReturnValueOnce({ + hasReceivedFinalGrade: false, + activeStepState: stepStates.inProgress, + stepState: stepStates.inProgress, + }); + const result = useInProgressActions({ options: {} }); + expect(result.primary.getMockName()).toBe( + mockSubmitResponseAction.getMockName(), + ); + expect(result.secondary.getMockName()).toBe( + mockCloseModalAction.getMockName(), + ); + }); + + it('return loadNextAction and exitAction when activeStepName is peer and numberOfAssessmentsCompleted is true', () => { + useGlobalState.mockReturnValueOnce({ + hasReceivedFinalGrade: false, + activeStepState: stepStates.inProgress, + stepState: stepStates.done, + activeStepName: stepNames.peer, + }); + useStepInfo.mockReturnValueOnce({ + peer: { numberOfAssessmentsCompleted: true }, + }); + const result = useInProgressActions({ options: {} }); + expect(result.primary.getMockName()).toBe(mockLoadNextAction.getMockName()); + expect(result.secondary.getMockName()).toBe( + mockCloseModalAction.getMockName(), + ); + }); + + it('return loadNextAction and exitAction when activeStepName is studentTraining and numberOfAssessmentsCompleted is true', () => { + useGlobalState.mockReturnValueOnce({ + hasReceivedFinalGrade: false, + activeStepState: stepStates.inProgress, + stepState: stepStates.done, + activeStepName: stepNames.studentTraining, + }); + useStepInfo.mockReturnValueOnce({ + studentTraining: { numberOfAssessmentsCompleted: true }, + }); + const result = useInProgressActions({ options: {} }); + expect(result.primary.getMockName()).toBe(mockLoadNextAction.getMockName()); + expect(result.secondary.getMockName()).toBe( + mockCloseModalAction.getMockName(), + ); + }); + + it('return startStepAction and exitAction when activeStepName is peer and numberOfAssessmentsCompleted is false', () => { + useGlobalState.mockReturnValueOnce({ + hasReceivedFinalGrade: false, + activeStepState: stepStates.inProgress, + stepState: stepStates.done, + activeStepName: stepNames.peer, + }); + useStepInfo.mockReturnValueOnce({ + peer: { numberOfAssessmentsCompleted: false }, + }); + const result = useInProgressActions({ options: {} }); + expect(result.primary.getMockName()).toBe( + mockStartStepAction.getMockName(), + ); + expect(result.secondary.getMockName()).toBe( + mockCloseModalAction.getMockName(), + ); + }); +}); diff --git a/src/components/ModalActions/index.test.jsx b/src/components/ModalActions/index.test.jsx new file mode 100644 index 00000000..a191a383 --- /dev/null +++ b/src/components/ModalActions/index.test.jsx @@ -0,0 +1,70 @@ +import { shallow } from '@edx/react-unit-test-utils'; + +import { useIsPageDataLoading } from 'hooks/app'; + +import useModalActionConfig from './hooks/useModalActionConfig'; + +import ModalActions from './index'; + +jest.mock('components/ActionButton', () => 'ActionButton'); +jest.mock('components/ConfirmDialog', () => 'ConfirmDialog'); +jest.mock('hooks/app', () => ({ + useIsPageDataLoading: jest.fn(), +})); +jest.mock('./hooks/useModalActionConfig', () => jest.fn()); + +describe('', () => { + const props = { + options: {}, + }; + beforeEach(() => { + useIsPageDataLoading.mockReturnValue(false); + }); + + it('render skeleton when page data is loading', () => { + useIsPageDataLoading.mockReturnValueOnce(true); + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + expect(wrapper.instance.findByType('Skeleton')).toHaveLength(1); + }); + + it('render empty when no actions', () => { + useModalActionConfig.mockReturnValue({}); + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + expect(wrapper.instance.findByType('ActionButton')).toHaveLength(0); + expect(wrapper.instance.findByType('ConfirmDialog')).toHaveLength(0); + }); + + it('can render primary and secondary without confirm', () => { + useModalActionConfig.mockReturnValue({ + primary: { + action: {}, + }, + secondary: { + action: {}, + }, + }); + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + expect(wrapper.instance.findByType('ActionButton')).toHaveLength(2); + expect(wrapper.instance.findByType('ConfirmDialog')).toHaveLength(0); + }); + + it('can render primary and secondary with confirm', () => { + useModalActionConfig.mockReturnValue({ + primary: { + action: {}, + confirmProps: {}, + }, + secondary: { + action: {}, + confirmProps: {}, + }, + }); + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + expect(wrapper.instance.findByType('ActionButton')).toHaveLength(2); + expect(wrapper.instance.findByType('ConfirmDialog')).toHaveLength(2); + }); +}); diff --git a/src/components/ModalContainer.test.jsx b/src/components/ModalContainer.test.jsx new file mode 100644 index 00000000..fdcd3a43 --- /dev/null +++ b/src/components/ModalContainer.test.jsx @@ -0,0 +1,47 @@ +import { shallow } from '@edx/react-unit-test-utils'; + +import { useORAConfigData } from 'hooks/app'; +import { useCloseModalAction } from 'hooks/actions'; +import ModalContainer from './ModalContainer'; + +jest.mock('hooks/app', () => ({ + useORAConfigData: jest.fn(), +})); +jest.mock('hooks/actions', () => ({ + useCloseModalAction: jest.fn(), +})); +jest.mock('components/ConfirmDialog', () => 'ConfirmDialog'); +jest.mock('components/ProgressBar', () => 'ProgressBar'); + +describe('', () => { + useORAConfigData.mockReturnValue({ title: 'title' }); + useCloseModalAction.mockReturnValue({ + confirmProps: { + abc: 'def', + }, + action: { onClick: jest.fn().mockName('closeModalAction') }, + }); + const renderComponent = () => shallow( + +
children
+
, + ); + + it('render default', () => { + const wrapper = renderComponent(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.instance.findByType('ConfirmDialog')).toHaveLength(1); + }); + + it('render without confirmProps', () => { + useCloseModalAction.mockReturnValue({ + confirmProps: null, + action: { onClick: jest.fn().mockName('closeModalAction') }, + }); + const wrapper = renderComponent(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.instance.findByType('ConfirmDialog')).toHaveLength(0); + }); +}); diff --git a/src/components/ProgressBar/ProgressStep.jsx b/src/components/ProgressBar/ProgressStep.jsx index 0c2267a7..783c272d 100644 --- a/src/components/ProgressBar/ProgressStep.jsx +++ b/src/components/ProgressBar/ProgressStep.jsx @@ -68,7 +68,7 @@ const ProgressStep = ({
{label} {subLabel && ( -

{subLabel}

+

{subLabel}

)}
diff --git a/src/components/ProgressBar/ProgressStep.test.jsx b/src/components/ProgressBar/ProgressStep.test.jsx new file mode 100644 index 00000000..387a9d31 --- /dev/null +++ b/src/components/ProgressBar/ProgressStep.test.jsx @@ -0,0 +1,85 @@ +import { shallow } from '@edx/react-unit-test-utils'; + +import { CheckCircle, Error } from '@edx/paragon/icons'; + +import { stepNames } from 'constants/index'; +import { useProgressStepData } from './hooks'; + +import ProgressStep, { stepIcons } from './ProgressStep'; + +jest.mock('./hooks', () => ({ + useProgressStepData: jest.fn(), +})); + +describe('', () => { + const props = { + step: stepNames.submission, + canRevisit: true, + label: 'Test Step', + }; + + const mockProgressStepData = { + onClick: jest.fn().mockName('onClick'), + href: 'link', + isActive: false, + isEnabled: true, + isComplete: false, + isPastDue: false, + myGrade: null, + }; + + Object.keys(stepIcons).forEach((step) => { + it(`renders ${step} step with correct icon`, () => { + useProgressStepData.mockReturnValueOnce(mockProgressStepData); + + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.instance.findByType('Icon')[0].props.src).toBe( + stepIcons[step], + ); + }); + }); + + it('render error on past due', () => { + useProgressStepData.mockReturnValue({ + ...mockProgressStepData, + isPastDue: true, + }); + + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.instance.findByType('Icon')[0].props.src).toBe(Error); + }); + + it('render is complete but not done', () => { + useProgressStepData.mockReturnValue({ + ...mockProgressStepData, + isComplete: true, + }); + + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.instance.findByType('Icon')[0].props.src).toBe(CheckCircle); + }); + + it('render is complete and done will have sublabel', () => { + useProgressStepData.mockReturnValue({ + ...mockProgressStepData, + isComplete: true, + myGrade: { + stepScore: { + earned: 1, + possible: 2, + }, + }, + }); + + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.instance.findByTestId('sublabel-test-id')[0].children[0].el).toContain('1 / 2'); + }); +}); diff --git a/src/components/ProgressBar/__snapshots__/ProgressStep.test.jsx.snap b/src/components/ProgressBar/__snapshots__/ProgressStep.test.jsx.snap new file mode 100644 index 00000000..07e9a6c6 --- /dev/null +++ b/src/components/ProgressBar/__snapshots__/ProgressStep.test.jsx.snap @@ -0,0 +1,177 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` render error on past due 1`] = ` + + +
+ Test Step +

+ Past due! +

+
+
+`; + +exports[` render is complete and done will have sublabel 1`] = ` + + +
+ Test Step +

+ 1 / 2 +

+
+
+`; + +exports[` render is complete but not done 1`] = ` + + +
+ Test Step +
+
+`; + +exports[` renders done step with correct icon 1`] = ` + + +
+ Test Step +
+
+`; + +exports[` renders peer step with correct icon 1`] = ` + + +
+ Test Step +
+
+`; + +exports[` renders self step with correct icon 1`] = ` + + +
+ Test Step +
+
+`; + +exports[` renders studentTraining step with correct icon 1`] = ` + + +
+ Test Step +
+
+`; + +exports[` renders submission step with correct icon 1`] = ` + + +
+ Test Step +
+
+`; diff --git a/src/components/ProgressBar/__snapshots__/index.test.jsx.snap b/src/components/ProgressBar/__snapshots__/index.test.jsx.snap new file mode 100644 index 00000000..6da556ee --- /dev/null +++ b/src/components/ProgressBar/__snapshots__/index.test.jsx.snap @@ -0,0 +1,111 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders all steps 1`] = ` + + +
+ + + Create response + (Step 1/5) + + +
+
+ +
+ + + + + +
+
+`; + +exports[` renders at least 2 steps: submission and done 1`] = ` + + +
+ + + Create response + (Step 1/2) + + +
+
+ +
+ + +
+
+`; + +exports[` renders null when page data is not loaded 1`] = `null`; diff --git a/src/components/ProgressBar/hooks.test.js b/src/components/ProgressBar/hooks.test.js new file mode 100644 index 00000000..d18e702b --- /dev/null +++ b/src/components/ProgressBar/hooks.test.js @@ -0,0 +1,128 @@ +import { useViewStep } from 'hooks/routing'; +import { useGlobalState, useStepInfo } from 'hooks/app'; +import { useOpenModal } from 'hooks/modal'; +import { isXblockStep } from 'utils'; + +import { stepRoutes, stepStates, stepNames } from 'constants/index'; +import { useProgressStepData } from './hooks'; + +jest.mock('hooks/routing', () => ({ + useViewStep: jest.fn(), +})); +jest.mock('hooks/app', () => ({ + useGlobalState: jest.fn(), + useStepInfo: jest.fn(), +})); +jest.mock('hooks/modal', () => ({ + useOpenModal: jest.fn(), +})); +jest.mock('utils', () => ({ + isXblockStep: jest.fn(), +})); +jest.mock('react-router-dom', () => ({ + useParams: jest.fn(() => ({ + xblockId: 'xblockId', + courseId: 'courseId', + })), +})); + +describe('useProgressStepData', () => { + const mockOpenModal = jest.fn(); + const props = { + step: stepNames.self, + canRevisit: false, + }; + beforeEach(() => { + useViewStep.mockReturnValue(stepNames.self); + useGlobalState.mockReturnValue({ + effectiveGrade: 8, + stepState: stepStates.done, + activeStepName: stepNames.self, + stepIsUnavailable: false, + }); + useStepInfo.mockReturnValue({ + peer: { + numberOfReceivedAssessments: 0, + isWaitingForSubmissions: false, + }, + }); + useOpenModal.mockReturnValue(mockOpenModal); + isXblockStep.mockReturnValue(false); + }); + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should have on click when is xblock', () => { + isXblockStep.mockReturnValueOnce(true); + const result = useProgressStepData(props); + result.onClick(); + expect(mockOpenModal).toHaveBeenCalledWith({ view: stepNames.self, title: stepNames.self }); + }); + + it('should have href when is not xblock', () => { + const result = useProgressStepData(props); + expect(result.href).toBe(`/${stepRoutes[stepNames.self]}/courseId/xblockId`); + }); + + it('is complete when step state is done', () => { + const result = useProgressStepData(props); + expect(result.isComplete).toBe(true); + }); + + it('is in progress when step state is in progress', () => { + useGlobalState.mockReturnValue({ + stepState: stepStates.inProgress, + }); + const result = useProgressStepData(props); + expect(result.inProgress).toBe(true); + }); + + it('is past due when step state is closed', () => { + useGlobalState.mockReturnValue({ + stepState: stepStates.closed, + }); + const result = useProgressStepData(props); + expect(result.isPastDue).toBe(true); + }); + + it('is active when step is active', () => { + const result = useProgressStepData(props); + expect(result.isActive).toBe(true); + }); + + it('is not enabled when step is unavailable', () => { + useGlobalState.mockReturnValue({ + stepIsUnavailable: true, + }); + const result = useProgressStepData(props); + expect(result.isEnabled).toBe(false); + }); + + it('use effect grade from global state', () => { + const result = useProgressStepData(props); + expect(result.myGrade).toBe(8); + }); + + test('for peer step is not enabled when waiting for submissions', () => { + useStepInfo.mockReturnValue({ + peer: { + numberOfReceivedAssessments: 0, + isWaitingForSubmissions: true, + }, + }); + const result = useProgressStepData({ ...props, step: stepNames.peer }); + expect(result.isEnabled).toBe(false); + }); + + test('for peer step is enabled iif peer is complete and no waiting for submission', () => { + useStepInfo.mockReturnValue({ + peer: { + numberOfReceivedAssessments: 1, + isWaitingForSubmissions: false, + }, + }); + const result = useProgressStepData({ ...props, step: stepNames.peer }); + expect(result.isEnabled).toBe(true); + }); +}); diff --git a/src/components/ProgressBar/index.jsx b/src/components/ProgressBar/index.jsx index bff57445..36880148 100644 --- a/src/components/ProgressBar/index.jsx +++ b/src/components/ProgressBar/index.jsx @@ -38,7 +38,7 @@ export const stepCanRevisit = { [stepNames.done]: true, }; -export const ProgressBar = ({ className }) => { +const ProgressBar = ({ className }) => { const isLoaded = useIsPageDataLoaded(); const hasReceivedFinalGrade = useHasReceivedFinalGrade(); diff --git a/src/components/ProgressBar/index.test.jsx b/src/components/ProgressBar/index.test.jsx new file mode 100644 index 00000000..92413303 --- /dev/null +++ b/src/components/ProgressBar/index.test.jsx @@ -0,0 +1,68 @@ +import { shallow } from '@edx/react-unit-test-utils'; + +import { + useAssessmentStepOrder, + useGlobalState, + useHasReceivedFinalGrade, + useIsPageDataLoaded, +} from 'hooks/app'; +import { stepNames } from 'constants/index'; +import { useViewStep } from 'hooks/routing'; +import { isXblockStep } from 'utils'; + +import ProgressBar from './index'; + +jest.mock('hooks/app', () => ({ + useAssessmentStepOrder: jest.fn(), + useGlobalState: jest.fn(), + useHasReceivedFinalGrade: jest.fn(), + useIsPageDataLoaded: jest.fn(), +})); +jest.mock('hooks/routing', () => ({ + useViewStep: jest.fn(), +})); +jest.mock('utils', () => ({ + isXblockStep: jest.fn(), +})); +jest.mock('./ProgressStep', () => 'ProgressStep'); + +describe('', () => { + const props = { + className: 'test-class', + }; + + beforeEach(() => { + useIsPageDataLoaded.mockReturnValue(true); + useHasReceivedFinalGrade.mockReturnValue(false); + useGlobalState.mockReturnValue({ activeStepName: stepNames.submission }); + useAssessmentStepOrder.mockReturnValue([]); + useViewStep.mockReturnValue(stepNames.submission); + isXblockStep.mockReturnValue(false); + }); + + it('renders null when page data is not loaded', () => { + useIsPageDataLoaded.mockReturnValueOnce(false); + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.isEmptyRender()).toBe(true); + }); + + it('renders at least 2 steps: submission and done', () => { + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + expect(wrapper.instance.findByType('ProgressStep')).toHaveLength(2); + }); + + it('renders all steps', () => { + isXblockStep.mockReturnValueOnce(true); + useAssessmentStepOrder.mockReturnValueOnce([ + stepNames.studentTraining, + stepNames.self, + stepNames.peer, + ]); + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + expect(wrapper.instance.findByType('ProgressStep')).toHaveLength(5); + }); +}); diff --git a/src/components/Prompt/__snapshots__/index.test.jsx.snap b/src/components/Prompt/__snapshots__/index.test.jsx.snap new file mode 100644 index 00000000..0dd3bb4c --- /dev/null +++ b/src/components/Prompt/__snapshots__/index.test.jsx.snap @@ -0,0 +1,68 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` render default 1`] = ` + + title + + } +> +
+ +`; + +exports[` render with open and onToggle 1`] = ` + + title + + } +> +
+ +`; + +exports[` render without title 1`] = ` + + Prompt + + } +> +
+ +`; diff --git a/src/components/Prompt/index.test.jsx b/src/components/Prompt/index.test.jsx new file mode 100644 index 00000000..95bc88b1 --- /dev/null +++ b/src/components/Prompt/index.test.jsx @@ -0,0 +1,78 @@ +import { shallow } from '@edx/react-unit-test-utils'; + +import { useActiveStepName, useORAConfigData } from 'hooks/app'; +import { useViewStep } from 'hooks/routing'; + +import Prompt from './index'; + +jest.mock('hooks/app', () => ({ + useActiveStepName: jest.fn(), + useORAConfigData: jest.fn(), +})); +jest.mock('hooks/routing', () => ({ + useViewStep: jest.fn(), +})); + +describe('', () => { + const props = { + prompt: 'prompt', + title: 'title', + }; + + useActiveStepName.mockReturnValue('activeStepName'); + useORAConfigData.mockReturnValue({ baseAssetUrl: 'baseAssetUrl/' }); + useViewStep.mockReturnValue('viewStep'); + + it('render default', () => { + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + // if open and onToggle are not passed, defaultOpen should be true + expect(wrapper.instance.findByType('Collapsible')[0].props.defaultOpen).toBe(true); + }); + + it('render with open and onToggle', () => { + const wrapper = shallow( {}} />); + expect(wrapper.snapshot).toMatchSnapshot(); + + // if open and onToggle are passed, open and onToggle should be passed to Collapsible + expect(wrapper.instance.findByType('Collapsible')[0].props.open).toBe(true); + expect(wrapper.instance.findByType('Collapsible')[0].props.onToggle).toBeInstanceOf(Function); + expect(wrapper.instance.findByType('Collapsible')[0].props.defaultOpen).toBe(undefined); + }); + + it('render without title', () => { + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + }); + + describe('test prompt', () => { + const getPromptHtml = (prompt) => { + const wrapper = shallow(); + // eslint-disable-next-line no-underscore-dangle + return wrapper.instance.findByType('div')[0].props.dangerouslySetInnerHTML.__html; + }; + const abitraryEndpoint = '/abc/def/ghi'; + const fullAssetUrl = `http://localhost:18000/asset-v1${abitraryEndpoint}`; + const fullStaticAssetUrl = `http://localhost:18000/baseAssetUrl${abitraryEndpoint}`; + const relativeAssetUrl = `/asset-v1${abitraryEndpoint}`; + const relativeStaticAssetUrl = `/static${abitraryEndpoint}`; + + it('does not update url for anchor and image that is not using relative url', () => { + expect(getPromptHtml(``)).toBe(``); + expect(getPromptHtml(``)).toBe(``); + // ignore non image and anchor + expect(getPromptHtml(``)).toBe(``); + }); + + it('update assets url for anchor and image', () => { + expect(getPromptHtml(``)).toBe(``); + expect(getPromptHtml(``)).toBe(``); + }); + + it('update static assets url for anchor and image', () => { + expect(getPromptHtml(``)).toBe(``); + expect(getPromptHtml(``)).toBe(``); + }); + }); +}); diff --git a/src/components/TextResponse/__snapshots__/index.test.jsx.snap b/src/components/TextResponse/__snapshots__/index.test.jsx.snap new file mode 100644 index 00000000..79521dd9 --- /dev/null +++ b/src/components/TextResponse/__snapshots__/index.test.jsx.snap @@ -0,0 +1,45 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` render default 1`] = ` +
+
+
+`; + +exports[` render without allowLatexPreview 1`] = ` +
+
+
+`; + +exports[` render without textResponseConfig 1`] = ` +
+
+
+`; diff --git a/src/components/TextResponse/index.test.jsx b/src/components/TextResponse/index.test.jsx new file mode 100644 index 00000000..ba5c96e4 --- /dev/null +++ b/src/components/TextResponse/index.test.jsx @@ -0,0 +1,45 @@ +import React from 'react'; +import { shallow } from '@edx/react-unit-test-utils'; + +import { useSubmissionConfig } from 'hooks/app'; + +import TextResponse from './index'; + +jest.mock('hooks/app', () => ({ + useSubmissionConfig: jest.fn(), +})); + +describe('', () => { + beforeEach(() => { + useSubmissionConfig.mockReturnValue({ + textResponseConfig: { editorType: 'text', allowLatexPreview: true }, + }); + }); + afterEach(() => { + jest.clearAllMocks(); + }); + it('render default', () => { + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + React.useEffect.mock.calls[0][0](); + expect(MathJax.Hub.Queue).toHaveBeenCalled(); + }); + + it('render without allowLatexPreview', () => { + useSubmissionConfig.mockReturnValue({ + textResponseConfig: { editorType: 'text' }, + }); + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + React.useEffect.mock.calls[0][0](); + expect(MathJax.Hub.Queue).not.toHaveBeenCalled(); + }); + + it('render without textResponseConfig', () => { + useSubmissionConfig.mockReturnValue({}); + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + }); +}); diff --git a/src/components/__snapshots__/ActionButton.test.jsx.snap b/src/components/__snapshots__/ActionButton.test.jsx.snap new file mode 100644 index 00000000..928d1257 --- /dev/null +++ b/src/components/__snapshots__/ActionButton.test.jsx.snap @@ -0,0 +1,21 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` render Button when state is not provided 1`] = ` + +
+ +
+
+
+ children +
+
+
+`; + +exports[` render without confirmProps 1`] = ` +
+
+

+ title +

+
+ +
+ +
+
+
+ children +
+
+
+`; diff --git a/src/setupTest.js b/src/setupTest.js index 72202834..ed81d118 100644 --- a/src/setupTest.js +++ b/src/setupTest.js @@ -12,7 +12,8 @@ jest.mock('react', () => ({ jest.mock('@edx/frontend-platform/i18n', () => { const i18n = jest.requireActual('@edx/frontend-platform/i18n'); const { formatMessage } = jest.requireActual('@edx/react-unit-test-utils'); - const formatDate = jest.fn(date => new Date(date).toLocaleDateString()).mockName('useIntl.formatDate'); + // this provide consistent for the test on different platform/timezone + const formatDate = jest.fn(date => new Date(date).toISOString()).mockName('useIntl.formatDate'); return { ...i18n, useIntl: jest.fn(() => ({ @@ -73,6 +74,9 @@ jest.mock('@edx/paragon', () => jest.requireActual('@edx/react-unit-test-utils') Hyperlink: 'Hyperlink', Icon: 'Icon', IconButton: 'IconButton', + Layout: { + Element: 'Layout.Element', + }, ModalDialog: { Body: 'ModalDialog.Body', Footer: 'ModalDialog.Footer', @@ -81,6 +85,16 @@ jest.mock('@edx/paragon', () => jest.requireActual('@edx/react-unit-test-utils') CloseButton: 'ModalDialog.CloseButton', }, MultiSelectDropdownFilter: 'MultiSelectDropdownFilter', + Nav: { + Item: 'Nav.Item', + Link: 'Nav.Link', + }, + Navbar: { + Brand: 'Navbar.Brand', + Collapse: 'Navbar.Collapse', + Nav: 'Navbar.Nav', + Toggle: 'Navbar.Toggle', + }, OverlayTrigger: 'OverlayTrigger', PageBanner: 'PageBanner', Popover: { @@ -95,11 +109,19 @@ jest.mock('@edx/paragon', () => jest.requireActual('@edx/react-unit-test-utils') Spinner: 'Spinner', })); jest.mock('@edx/paragon/icons', () => ({ + CheckCircle: jest.fn().mockName('icons.CheckCircle'), + Edit: jest.fn().mockName('icons.Edit'), + Error: jest.fn().mockName('icons.Error'), + Highlight: jest.fn().mockName('icons.Highlight'), Rule: jest.fn().mockName('icons.Rule'), })); jest.mock('@zip.js/zip.js', () => ({})); +jest.mock('uuid', () => ({ + v4: () => 'some_uuid', +})); + Object.defineProperty(window, 'matchMedia', { writable: true, value: jest.fn().mockImplementation(query => ({ diff --git a/src/views/AssessmentView/BaseAssessmentView/__snapshots__/index.test.jsx.snap b/src/views/AssessmentView/BaseAssessmentView/__snapshots__/index.test.jsx.snap new file mode 100644 index 00000000..89814d45 --- /dev/null +++ b/src/views/AssessmentView/BaseAssessmentView/__snapshots__/index.test.jsx.snap @@ -0,0 +1,45 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` render default 1`] = ` +
+
+ + +
+

+ Self grading +

+ +
+
+ + + + children + + + + + + +
+
+`; diff --git a/src/views/AssessmentView/BaseAssessmentView/index.test.jsx b/src/views/AssessmentView/BaseAssessmentView/index.test.jsx new file mode 100644 index 00000000..8ef1e929 --- /dev/null +++ b/src/views/AssessmentView/BaseAssessmentView/index.test.jsx @@ -0,0 +1,24 @@ +import { shallow } from '@edx/react-unit-test-utils'; + +import { useViewStep } from 'hooks/routing'; + +import { stepNames } from 'constants/index'; + +import BaseAssessmentView from './index'; + +jest.mock('hooks/routing', () => ({ + useViewStep: jest.fn(), +})); +jest.mock('components/Assessment', () => 'Assessment'); +jest.mock('components/Instructions', () => 'Instructions'); +jest.mock('components/ModalActions', () => 'ModalActions'); +jest.mock('components/StatusAlert', () => 'StatusAlert'); +jest.mock('components/StepProgressIndicator', () => 'StepProgressIndicator'); + +describe('', () => { + it('render default', () => { + useViewStep.mockReturnValue(stepNames.self); + const wrapper = shallow(children); + expect(wrapper.snapshot).toMatchSnapshot(); + }); +}); diff --git a/src/views/AssessmentView/__snapshots__/index.test.jsx.snap b/src/views/AssessmentView/__snapshots__/index.test.jsx.snap new file mode 100644 index 00000000..d3873a38 --- /dev/null +++ b/src/views/AssessmentView/__snapshots__/index.test.jsx.snap @@ -0,0 +1,86 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` render empty on not loaded 1`] = `null`; + +exports[` render with files 1`] = ` + +
+ +
+
+`; + +exports[` render with prompts and text responses 1`] = ` + +
+
+ + +

+ Your response +

+ +
+
+
+ + +

+ Your response +

+ +
+
+
+
+`; diff --git a/src/views/AssessmentView/index.jsx b/src/views/AssessmentView/index.jsx index 1cf6d0f6..f159d5f2 100644 --- a/src/views/AssessmentView/index.jsx +++ b/src/views/AssessmentView/index.jsx @@ -11,7 +11,7 @@ import BaseAssessmentView from './BaseAssessmentView'; import useAssessmentData from './useAssessmentData'; import messages from './messages'; -export const AssessmentView = () => { +const AssessmentView = () => { const { prompts, response, isLoaded } = useAssessmentData(); const { formatMessage } = useIntl(); const step = useViewStep(); diff --git a/src/views/AssessmentView/index.test.jsx b/src/views/AssessmentView/index.test.jsx new file mode 100644 index 00000000..ea28ee67 --- /dev/null +++ b/src/views/AssessmentView/index.test.jsx @@ -0,0 +1,67 @@ +import { shallow } from '@edx/react-unit-test-utils'; + +import { useViewStep } from 'hooks/routing'; + +import { stepNames } from 'constants/index'; +import useAssessmentData from './useAssessmentData'; +import AssessmentView from './index'; + +jest.mock('hooks/routing', () => ({ + useViewStep: jest.fn(), +})); +jest.mock('components/Prompt', () => 'Prompt'); +jest.mock('components/TextResponse', () => 'TextResponse'); +jest.mock('components/FileUpload', () => 'FileUpload'); +jest.mock('./useAssessmentData', () => jest.fn()); + +describe('', () => { + useViewStep.mockReturnValue(stepNames.self); + it('render empty on not loaded', () => { + useAssessmentData.mockReturnValue({ + prompts: [], + response: {}, + isLoaded: false, + }); + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + expect(wrapper.isEmptyRender()).toBe(true); + }); + + it('render with prompts and text responses', () => { + useAssessmentData.mockReturnValue({ + prompts: [ + { id: 1, prompt: 'prompt' }, + { id: 2, prompt: 'prompt' }, + ], + response: { + textResponses: [ + { id: 1, response: 'response' }, + { id: 2, response: 'response' }, + ], + }, + isLoaded: true, + }); + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.instance.findByType('Prompt').length).toBe(2); + expect(wrapper.instance.findByType('TextResponse').length).toBe(2); + expect(wrapper.instance.findByType('FileUpload').length).toBe(0); + }); + + it('render with files', () => { + useAssessmentData.mockReturnValue({ + prompts: [], + response: { + uploadedFiles: [{ id: 1, name: 'file' }], + }, + isLoaded: true, + }); + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.instance.findByType('Prompt').length).toBe(0); + expect(wrapper.instance.findByType('TextResponse').length).toBe(0); + expect(wrapper.instance.findByType('FileUpload').length).toBe(1); + }); +}); diff --git a/src/views/AssessmentView/useAssessmentData.js b/src/views/AssessmentView/useAssessmentData.js index 47fec368..e80b6a5e 100644 --- a/src/views/AssessmentView/useAssessmentData.js +++ b/src/views/AssessmentView/useAssessmentData.js @@ -9,7 +9,7 @@ import { useResponseData, } from 'hooks/app'; -const stateKeys = StrictDict({ +export const stateKeys = StrictDict({ initialized: 'initialized', }); @@ -25,11 +25,10 @@ const useAssessmentData = () => { if (!initialized && isLoaded && isPageDataLoaded) { setResponse(responseData); setInitialized(true); - } - if (initialized && responseData && response !== responseData) { + } else if (initialized && responseData && response !== responseData) { setResponse(responseData); } - }, [responseData, initialized]); // eslint-disable-line react-hooks/exhaustive-deps + }, [responseData, initialized, isLoaded, isPageDataLoaded, response, setResponse, setInitialized]); return { isLoaded, response, diff --git a/src/views/AssessmentView/useAssessmentData.test.js b/src/views/AssessmentView/useAssessmentData.test.js new file mode 100644 index 00000000..b1c9f741 --- /dev/null +++ b/src/views/AssessmentView/useAssessmentData.test.js @@ -0,0 +1,73 @@ +import React from 'react'; +import { mockUseKeyedState } from '@edx/react-unit-test-utils'; + +import { + useIsORAConfigLoaded, + useIsPageDataLoaded, + usePrompts, + useResponse, + useSetResponse, + useResponseData, +} from 'hooks/app'; + +import useAssessmentData, { stateKeys } from './useAssessmentData'; + +jest.mock('hooks/app', () => ({ + useIsORAConfigLoaded: jest.fn(), + useIsPageDataLoaded: jest.fn(), + usePrompts: jest.fn(), + useResponse: jest.fn(), + useSetResponse: jest.fn(), + useResponseData: jest.fn(), +})); + +const state = mockUseKeyedState(stateKeys); + +describe('useAssessmentData', () => { + const mockPrompts = 'mockPrompts'; + const mockResponse = 'mockResponse'; + const mockIsLoaded = 'mockIsLoaded'; + const mockSetResponse = jest.fn(); + beforeEach(() => { + jest.clearAllMocks(); + state.mock(); + useIsORAConfigLoaded.mockReturnValue(mockIsLoaded); + useIsPageDataLoaded.mockReturnValue(true); + usePrompts.mockReturnValue(mockPrompts); + useResponse.mockReturnValue(mockResponse); + useSetResponse.mockReturnValue(mockSetResponse); + }); + afterEach(() => { state.resetVals(); }); + + it('initializes initialized state to false', () => { + useAssessmentData(); + state.expectInitializedWith(stateKeys.initialized, false); + }); + + it('returns isLoaded, response, and prompts', () => { + const result = useAssessmentData(); + expect(result).toEqual({ + isLoaded: mockIsLoaded, + response: mockResponse, + prompts: mockPrompts, + }); + }); + + it('is not initialized but loaded, update the response', () => { + useIsORAConfigLoaded.mockReturnValue(true); + useIsPageDataLoaded.mockReturnValue(true); + useResponseData.mockReturnValue(mockResponse); + useAssessmentData(); + React.useEffect.mock.calls[0][0](); + expect(mockSetResponse).toHaveBeenCalledWith(mockResponse); + state.expectSetStateCalledWith(stateKeys.initialized, true); + }); + + it('is initialized and update the response iif responseData is different', () => { + state.mockVal(stateKeys.initialized, true); + useResponseData.mockReturnValue('differentResponse'); + useAssessmentData(); + React.useEffect.mock.calls[0][0](); + expect(mockSetResponse).toHaveBeenCalledWith('differentResponse'); + }); +}); diff --git a/src/views/GradeView/Content.test.jsx b/src/views/GradeView/Content.test.jsx new file mode 100644 index 00000000..c08924fc --- /dev/null +++ b/src/views/GradeView/Content.test.jsx @@ -0,0 +1,36 @@ +import { shallow } from '@edx/react-unit-test-utils'; + +import { usePrompts, useResponseData, useEffectiveGradeStep } from 'hooks/app'; +import { stepNames } from 'constants/index'; + +import Content from './Content'; + +jest.mock('hooks/app', () => ({ + usePrompts: jest.fn(), + useResponseData: jest.fn(), + useEffectiveGradeStep: jest.fn(), +})); +jest.mock('components/FileUpload', () => 'FileUpload'); +jest.mock('components/Prompt', () => 'Prompt'); +jest.mock('components/TextResponse', () => 'TextResponse'); + +describe('', () => { + it('render without prompt and effectiveGradeStep is not peer', () => { + usePrompts.mockReturnValue([]); + useResponseData.mockReturnValue({ textResponses: [], uploadedFiles: [] }); + useEffectiveGradeStep.mockReturnValue(stepNames.self); + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + }); + + it('render with prompt, textResponse and effectiveGradeStep is peer', () => { + usePrompts.mockReturnValue(['prompt1', 'prompt2']); + useResponseData.mockReturnValue({ + textResponses: ['text response 1', 'text response 2'], + uploadedFiles: ['upload'], + }); + useEffectiveGradeStep.mockReturnValue(stepNames.peer); + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + }); +}); diff --git a/src/views/GradeView/FinalGrade.jsx b/src/views/GradeView/FinalGrade.jsx index 13ac7f1c..c295d412 100644 --- a/src/views/GradeView/FinalGrade.jsx +++ b/src/views/GradeView/FinalGrade.jsx @@ -8,6 +8,8 @@ import { } from 'hooks/app'; import InfoPopover from 'components/InfoPopover'; import ReadOnlyAssessment from 'components/Assessment/ReadonlyAssessment'; +import { stepNames } from 'constants/index'; + import messages, { labelMessages } from './messages'; const FinalGrade = () => { @@ -39,7 +41,7 @@ const FinalGrade = () => { {formatMessage(messages.yourFinalGrade, finalStepScore)}

- {effectiveAssessmentType === 'peer' + {effectiveAssessmentType === stepNames.peer ? formatMessage(messages.peerAsFinalGradeInfo) : formatMessage(messages.finalGradeInfo, { step: effectiveAssessmentType })}

diff --git a/src/views/GradeView/FinalGrade.test.jsx b/src/views/GradeView/FinalGrade.test.jsx new file mode 100644 index 00000000..aac5526d --- /dev/null +++ b/src/views/GradeView/FinalGrade.test.jsx @@ -0,0 +1,59 @@ +import { shallow } from '@edx/react-unit-test-utils'; + +import { + useAssessmentData, + useStepInfo, +} from 'hooks/app'; +import { stepNames } from 'constants/index'; + +import FinalGrade from './FinalGrade'; + +jest.mock('hooks/app', () => ({ + useAssessmentData: jest.fn(), + useStepInfo: jest.fn(), +})); +jest.mock('components/InfoPopover', () => 'InfoPopover'); +jest.mock('components/Assessment/ReadonlyAssessment', () => 'ReadOnlyAssessment'); + +describe('', () => { + const mockUseAssessmentData = { + effectiveAssessmentType: stepNames.self, + [stepNames.self]: { + stepScore: 1, + }, + [stepNames.peer]: { + stepScore: 2, + }, + }; + + it('self and peer grades', () => { + useAssessmentData.mockReturnValue(mockUseAssessmentData); + useStepInfo.mockReturnValue({ + [stepNames.self]: true, + [stepNames.peer]: true, + }); + + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.instance.findByType('ReadOnlyAssessment')).toHaveLength(2); + }); + + it('peer only', () => { + useAssessmentData.mockReturnValue({ + effectiveAssessmentType: stepNames.peer, + [stepNames.peer]: { + stepScore: 2, + }, + }); + useStepInfo.mockReturnValue({ + [stepNames.self]: false, + [stepNames.peer]: true, + }); + + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.instance.findByType('ReadOnlyAssessment')).toHaveLength(1); + }); +}); diff --git a/src/views/GradeView/__snapshots__/Content.test.jsx.snap b/src/views/GradeView/__snapshots__/Content.test.jsx.snap new file mode 100644 index 00000000..52b7407c --- /dev/null +++ b/src/views/GradeView/__snapshots__/Content.test.jsx.snap @@ -0,0 +1,99 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` render with prompt, textResponse and effectiveGradeStep is peer 1`] = ` +
+ + About your grade: + +

+ Your grade is based on your Peer assessment score for this problem. Other assessments don't count towards your final score. + +
+ Only the required number of peer grades will counted against your final grade. The others are shown, but are not included in your grade calculation +
+

+
+
+ +

+ Your response +

+ +
+
+ +

+ Your response +

+ +
+ +
+
+`; + +exports[` render without prompt and effectiveGradeStep is not peer 1`] = ` +
+ + About your grade: + +

+ Your grade is based on your Self assessment score for this problem. Other assessments don't count towards your final score. +

+
+ +
+
+`; diff --git a/src/views/GradeView/__snapshots__/FinalGrade.test.jsx.snap b/src/views/GradeView/__snapshots__/FinalGrade.test.jsx.snap new file mode 100644 index 00000000..c7017d75 --- /dev/null +++ b/src/views/GradeView/__snapshots__/FinalGrade.test.jsx.snap @@ -0,0 +1,70 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` peer only 1`] = ` +
+

+ Your final grade: {earned}/{possible} + +

+ Only the required number of peer grades will counted against your final grade. The others are shown, but are not included in your grade calculation +

+
+

+
+ +
+
+
+`; + +exports[` self and peer grades 1`] = ` +
+

+ Your final grade: {earned}/{possible} + +

+ Your grade is based on your self score for this problem. Other assessments don't count towards your final score. +

+
+

+
+ +
+
+

+ Unweighted grades + +

+ These grades are given to your response. However, these are not used to compute your final grade. +

+
+

+ +
+`; diff --git a/src/views/GradeView/__snapshots__/index.test.jsx.snap b/src/views/GradeView/__snapshots__/index.test.jsx.snap new file mode 100644 index 00000000..2762a2bb --- /dev/null +++ b/src/views/GradeView/__snapshots__/index.test.jsx.snap @@ -0,0 +1,133 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders correctly 1`] = ` +
+
+ + + + + + + + + + + + + +
+
+`; diff --git a/src/views/GradeView/index.test.jsx b/src/views/GradeView/index.test.jsx new file mode 100644 index 00000000..9cf2486d --- /dev/null +++ b/src/views/GradeView/index.test.jsx @@ -0,0 +1,18 @@ +import { shallow } from '@edx/react-unit-test-utils'; + +import GradeView from './index'; + +jest.mock('components/ModalActions', () => 'ModalActions'); +jest.mock('./FinalGrade', () => 'FinalGrade'); +jest.mock('./Content', () => 'Content'); + +describe('', () => { + it('renders correctly', () => { + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.instance.findByType('ModalActions')).toHaveLength(1); + expect(wrapper.instance.findByType('FinalGrade')).toHaveLength(1); + expect(wrapper.instance.findByType('Content')).toHaveLength(1); + }); +}); diff --git a/src/views/SubmissionView/SubmissionPrompts.jsx b/src/views/SubmissionView/SubmissionPrompts.jsx index 29e7727c..f2986e21 100644 --- a/src/views/SubmissionView/SubmissionPrompts.jsx +++ b/src/views/SubmissionView/SubmissionPrompts.jsx @@ -1,10 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { - usePrompts, - useSubmissionConfig, -} from 'hooks/app'; +import { usePrompts, useSubmissionConfig } from 'hooks/app'; import Prompt from 'components/Prompt'; import TextResponse from 'components/TextResponse'; @@ -16,28 +13,33 @@ const SubmissionPrompts = ({ isReadOnly, }) => { const submissionConfig = useSubmissionConfig(); + const prompts = usePrompts(); const response = (index) => { if (!submissionConfig.textResponseConfig.enabled) { return null; } - return isReadOnly - ? - : ( - - ); + return isReadOnly ? ( + + ) : ( + + ); }; - return usePrompts().map((prompt, index) => ( - // eslint-disable-next-line react/no-array-index-key -
- - {response(index)} -
- )); + return ( + <> + {prompts.map((prompt, index) => ( + // eslint-disable-next-line react/no-array-index-key +
+ + {response(index)} +
+ ))} + + ); }; SubmissionPrompts.defaultProps = { textResponses: null, @@ -45,6 +47,7 @@ SubmissionPrompts.defaultProps = { SubmissionPrompts.propTypes = { textResponses: PropTypes.arrayOf(PropTypes.string), onUpdateTextResponse: PropTypes.func.isRequired, + isReadOnly: PropTypes.bool.isRequired, }; export default SubmissionPrompts; diff --git a/src/views/SubmissionView/SubmissionPrompts.test.jsx b/src/views/SubmissionView/SubmissionPrompts.test.jsx new file mode 100644 index 00000000..a53c81ab --- /dev/null +++ b/src/views/SubmissionView/SubmissionPrompts.test.jsx @@ -0,0 +1,88 @@ +import { shallow } from '@edx/react-unit-test-utils'; + +import { + usePrompts, + useSubmissionConfig, +} from 'hooks/app'; + +import SubmissionPrompts from './SubmissionPrompts'; + +jest.mock('hooks/app', () => ({ + usePrompts: jest.fn(), + useSubmissionConfig: jest.fn(), +})); +jest.mock('components/Prompt', () => 'Prompt'); +jest.mock('components/TextResponse', () => 'TextResponse'); +jest.mock('./TextResponseEditor', () => 'TextResponseEditor'); + +describe('', () => { + it('render text response editor when readOnly is false', () => { + usePrompts.mockReturnValue(['prompt1', 'prompt2']); + useSubmissionConfig.mockReturnValue({ textResponseConfig: { enabled: true } }); + + const wrapper = shallow( + , + ); + + expect(wrapper.snapshot).toMatchSnapshot(); + expect(wrapper.instance.findByType('Prompt').length).toBe(2); + expect(wrapper.instance.findByType('TextResponseEditor').length).toBe(2); + expect(wrapper.instance.findByType('TextResponse').length).toBe(0); + }); + + it('render text response when readOnly is true', () => { + usePrompts.mockReturnValue(['prompt1', 'prompt2']); + useSubmissionConfig.mockReturnValue({ textResponseConfig: { enabled: true } }); + + const wrapper = shallow( + , + ); + + expect(wrapper.snapshot).toMatchSnapshot(); + expect(wrapper.instance.findByType('Prompt').length).toBe(2); + expect(wrapper.instance.findByType('TextResponseEditor').length).toBe(0); + expect(wrapper.instance.findByType('TextResponse').length).toBe(2); + }); + + it('render empty prompts', () => { + usePrompts.mockReturnValue([]); + useSubmissionConfig.mockReturnValue({ textResponseConfig: { enabled: true } }); + + const wrapper = shallow( + , + ); + + expect(wrapper.snapshot).toMatchSnapshot(); + expect(wrapper.instance.findByType('Prompt').length).toBe(0); + }); + + it('do not render response when textResponseConfig is disabled', () => { + usePrompts.mockReturnValue(['prompt1', 'prompt2']); + useSubmissionConfig.mockReturnValue({ textResponseConfig: { enabled: false } }); + + const wrapper = shallow( + , + ); + + expect(wrapper.snapshot).toMatchSnapshot(); + expect(wrapper.instance.findByType('Prompt').length).toBe(2); + expect(wrapper.instance.findByType('TextResponseEditor').length).toBe(0); + expect(wrapper.instance.findByType('TextResponse').length).toBe(0); + }); +}); diff --git a/src/views/SubmissionView/TextResponseEditor/LaTextPreview.test.jsx b/src/views/SubmissionView/TextResponseEditor/LaTextPreview.test.jsx new file mode 100644 index 00000000..24392c64 --- /dev/null +++ b/src/views/SubmissionView/TextResponseEditor/LaTextPreview.test.jsx @@ -0,0 +1,10 @@ +import { shallow } from '@edx/react-unit-test-utils'; + +import LatexPreview from './LaTexPreview'; + +describe('', () => { + it('renders', () => { + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + }); +}); diff --git a/src/views/SubmissionView/TextResponseEditor/__snapshots__/LaTextPreview.test.jsx.snap b/src/views/SubmissionView/TextResponseEditor/__snapshots__/LaTextPreview.test.jsx.snap new file mode 100644 index 00000000..91df2d2a --- /dev/null +++ b/src/views/SubmissionView/TextResponseEditor/__snapshots__/LaTextPreview.test.jsx.snap @@ -0,0 +1,15 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders 1`] = ` +
+
+
+`; diff --git a/src/views/SubmissionView/__snapshots__/SubmissionPrompts.test.jsx.snap b/src/views/SubmissionView/__snapshots__/SubmissionPrompts.test.jsx.snap new file mode 100644 index 00000000..248add58 --- /dev/null +++ b/src/views/SubmissionView/__snapshots__/SubmissionPrompts.test.jsx.snap @@ -0,0 +1,72 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` do not render response when textResponseConfig is disabled 1`] = ` + +
+ +
+
+ +
+
+`; + +exports[` render empty prompts 1`] = ``; + +exports[` render text response editor when readOnly is false 1`] = ` + +
+ + +
+
+ + +
+
+`; + +exports[` render text response when readOnly is true 1`] = ` + +
+ + +
+
+ + +
+
+`; diff --git a/src/views/SubmissionView/__snapshots__/index.test.jsx.snap b/src/views/SubmissionView/__snapshots__/index.test.jsx.snap new file mode 100644 index 00000000..fe5d8005 --- /dev/null +++ b/src/views/SubmissionView/__snapshots__/index.test.jsx.snap @@ -0,0 +1,112 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders disable show rubric 1`] = ` +
+
+ + +
+
+

+ Your response +

+
+ + + + +
+ + +
+
+
+`; + +exports[` renders enable show rubric 1`] = ` +
+
+ + +
+
+

+ Your response +

+
+ + + + +
+ + + +
+
+
+`; diff --git a/src/views/SubmissionView/hooks/useUploadedFilesData.js b/src/views/SubmissionView/hooks/useUploadedFilesData.js index 743d733d..632ad0fa 100644 --- a/src/views/SubmissionView/hooks/useUploadedFilesData.js +++ b/src/views/SubmissionView/hooks/useUploadedFilesData.js @@ -14,11 +14,11 @@ const useUploadedFilesData = () => { const deleteFileMutation = useDeleteFile(); const uploadFilesMutation = useUploadFiles(); - const response = useResponseData(); + const response = useResponseData() || {}; const [value, setValue] = useKeyedState( stateKeys.uploadedFiles, - response ? response.uploadedFiles : [], + response?.uploadedFiles ? response.uploadedFiles : [], ); React.useEffect(() => { diff --git a/src/views/SubmissionView/hooks/useUploadedFilesData.test.js b/src/views/SubmissionView/hooks/useUploadedFilesData.test.js new file mode 100644 index 00000000..c49b61f7 --- /dev/null +++ b/src/views/SubmissionView/hooks/useUploadedFilesData.test.js @@ -0,0 +1,46 @@ +import { mockUseKeyedState } from '@edx/react-unit-test-utils'; + +import { + useResponseData, + useUploadFiles, + useDeleteFile, +} from 'hooks/app'; + +import useUploadedFilesData, { stateKeys } from './useUploadedFilesData'; + +jest.mock('hooks/app', () => ({ + useResponseData: jest.fn(), + useUploadFiles: jest.fn(), + useDeleteFile: jest.fn(), +})); + +const state = mockUseKeyedState(stateKeys); + +describe('useUploadedFilesData', () => { + const mockUploadFilesMutation = jest.fn(); + const mockDeleteFileMutation = jest.fn(); + useUploadFiles.mockReturnValue({ mutateAsync: mockUploadFilesMutation }); + useDeleteFile.mockReturnValue({ mutateAsync: mockDeleteFileMutation }); + + beforeEach(() => { + jest.clearAllMocks(); + state.mock(); + }); + afterEach(() => { state.resetVals(); }); + + it('initializes uploadedFiles state to empty array if response is null', () => { + useResponseData.mockReturnValue(); + useUploadedFilesData(); + state.expectInitializedWith(stateKeys.uploadedFiles, []); + }); + it('initializes uploadedFiles state to response.uploadedFiles', () => { + useResponseData.mockReturnValue({ uploadedFiles: ['file1', 'file2'] }); + useUploadedFilesData(); + state.expectInitializedWith(stateKeys.uploadedFiles, ['file1', 'file2']); + }); + it('return correct mutation function', () => { + const { onFileUploaded, onDeletedFile } = useUploadedFilesData(); + expect(onFileUploaded).toBe(mockUploadFilesMutation); + expect(onDeletedFile).toBe(mockDeleteFileMutation); + }); +}); diff --git a/src/views/SubmissionView/index.jsx b/src/views/SubmissionView/index.jsx index 5e9dc7d6..e2b8e20c 100644 --- a/src/views/SubmissionView/index.jsx +++ b/src/views/SubmissionView/index.jsx @@ -17,7 +17,7 @@ import './index.scss'; import messages from './messages'; -export const SubmissionView = () => { +const SubmissionView = () => { const { actionOptions, showRubric, diff --git a/src/views/SubmissionView/index.test.jsx b/src/views/SubmissionView/index.test.jsx new file mode 100644 index 00000000..427fcf18 --- /dev/null +++ b/src/views/SubmissionView/index.test.jsx @@ -0,0 +1,47 @@ +import { shallow } from '@edx/react-unit-test-utils'; + +import useSubmissionViewData from './hooks'; + +import SubmissionView from './index'; + +jest.mock('components/Rubric', () => 'Rubric'); +jest.mock('components/ModalActions', () => 'ModalActions'); +jest.mock('components/FileUpload', () => 'FileUpload'); +jest.mock('components/Instructions', () => 'Instructions'); +jest.mock('components/StatusAlert', () => 'StatusAlert'); +jest.mock('./SubmissionPrompts', () => 'SubmissionPrompts'); +jest.mock('./hooks', () => jest.fn()); + +describe('', () => { + const mockUseSubmissionViewData = { + actionOptions: { + hasSubmitted: false, + }, + showRubric: false, + response: { + textResponses: ['response1', 'response2'], + uploadedFiles: [], + }, + onUpdateTextResponse: jest.fn(), + isDraftSaved: false, + onDeletedFile: jest.fn(), + onFileUploaded: jest.fn(), + isReadOnly: false, + }; + useSubmissionViewData.mockReturnValue(mockUseSubmissionViewData); + + it('renders disable show rubric', () => { + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.instance.findByType('Rubric').length).toBe(0); + }); + + it('renders enable show rubric', () => { + mockUseSubmissionViewData.showRubric = true; + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.instance.findByType('Rubric').length).toBe(1); + }); +}); diff --git a/src/views/XBlockStudioView/__snapshots__/index.test.jsx.snap b/src/views/XBlockStudioView/__snapshots__/index.test.jsx.snap new file mode 100644 index 00000000..40bc65ef --- /dev/null +++ b/src/views/XBlockStudioView/__snapshots__/index.test.jsx.snap @@ -0,0 +1,16 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` should render 1`] = ` + +
+ + + + + + +
+
+`; diff --git a/src/views/XBlockStudioView/components/StudioSchedule/FormatDateTime.jsx b/src/views/XBlockStudioView/components/StudioSchedule/FormatDateTime.jsx index 05f56f82..a4703988 100644 --- a/src/views/XBlockStudioView/components/StudioSchedule/FormatDateTime.jsx +++ b/src/views/XBlockStudioView/components/StudioSchedule/FormatDateTime.jsx @@ -13,7 +13,7 @@ const FormatDateTime = ({ date }) => { day: 'numeric', hour: 'numeric', minute: 'numeric', - }) : formatMessage(messages.notSet)} + }) : formatMessage(messages.notSetLabel)} ); }; diff --git a/src/views/XBlockStudioView/components/StudioSchedule/FormatDateTime.test.jsx b/src/views/XBlockStudioView/components/StudioSchedule/FormatDateTime.test.jsx new file mode 100644 index 00000000..9c4d0973 --- /dev/null +++ b/src/views/XBlockStudioView/components/StudioSchedule/FormatDateTime.test.jsx @@ -0,0 +1,15 @@ +import { shallow } from '@edx/react-unit-test-utils'; + +import FormatDateTime from './FormatDateTime'; + +describe('', () => { + it('should render', () => { + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + }); + + it('should render with date', () => { + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + }); +}); diff --git a/src/views/XBlockStudioView/components/StudioSchedule/StepInfo.test.jsx b/src/views/XBlockStudioView/components/StudioSchedule/StepInfo.test.jsx new file mode 100644 index 00000000..b2e50c71 --- /dev/null +++ b/src/views/XBlockStudioView/components/StudioSchedule/StepInfo.test.jsx @@ -0,0 +1,17 @@ +import { shallow } from '@edx/react-unit-test-utils'; + +import StepInfo from './StepInfo'; + +jest.mock('./FormatDateTime', () => 'FormatDateTime'); + +describe('', () => { + it('should render', () => { + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + }); + + it('should render with startDatetime and endDatetime', () => { + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + }); +}); diff --git a/src/views/XBlockStudioView/components/StudioSchedule/__snapshots__/FormatDateTime.test.jsx.snap b/src/views/XBlockStudioView/components/StudioSchedule/__snapshots__/FormatDateTime.test.jsx.snap new file mode 100644 index 00000000..802682a3 --- /dev/null +++ b/src/views/XBlockStudioView/components/StudioSchedule/__snapshots__/FormatDateTime.test.jsx.snap @@ -0,0 +1,13 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` should render 1`] = ` + + Not set + +`; + +exports[` should render with date 1`] = ` + + 2020-01-01T00:00:00.000Z + +`; diff --git a/src/views/XBlockStudioView/components/StudioSchedule/__snapshots__/StepInfo.test.jsx.snap b/src/views/XBlockStudioView/components/StudioSchedule/__snapshots__/StepInfo.test.jsx.snap new file mode 100644 index 00000000..9c75a562 --- /dev/null +++ b/src/views/XBlockStudioView/components/StudioSchedule/__snapshots__/StepInfo.test.jsx.snap @@ -0,0 +1,28 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` should render 1`] = `
`; + +exports[` should render with startDatetime and endDatetime 1`] = ` +
+

+ + test + + start: + + +

+

+ + test + + due: + + +

+
+`; diff --git a/src/views/XBlockStudioView/components/StudioSchedule/__snapshots__/index.test.jsx.snap b/src/views/XBlockStudioView/components/StudioSchedule/__snapshots__/index.test.jsx.snap new file mode 100644 index 00000000..d2a96b5f --- /dev/null +++ b/src/views/XBlockStudioView/components/StudioSchedule/__snapshots__/index.test.jsx.snap @@ -0,0 +1,85 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` render with assesssment steps 1`] = ` + + Schedule + + } +> +
+

+ + Response + + start: + + +

+

+ + Response + + due: + + +

+ + +
+
+`; + +exports[` render without assesssment steps 1`] = ` + + Schedule + + } +> +
+

+ + Response + + start: + + +

+

+ + Response + + due: + + +

+
+
+`; diff --git a/src/views/XBlockStudioView/components/StudioSchedule/index.test.jsx b/src/views/XBlockStudioView/components/StudioSchedule/index.test.jsx new file mode 100644 index 00000000..31fd1189 --- /dev/null +++ b/src/views/XBlockStudioView/components/StudioSchedule/index.test.jsx @@ -0,0 +1,61 @@ +import { shallow } from '@edx/react-unit-test-utils'; + +import { useORAConfigData } from 'hooks/app'; +import { stepNames } from 'constants/index'; + +import StudioSchedule from './index'; + +jest.mock('hooks/app', () => ({ + useORAConfigData: jest.fn(), +})); +jest.mock('../XBlockStudioViewProvider', () => ({ + useXBlockStudioViewContext: () => ({ + scheduleIsOpen: true, + toggleSchedule: jest.fn().mockName('toggleSchedule'), + }), +})); +jest.mock('./FormatDateTime', () => 'FormatDateTime'); +jest.mock('./StepInfo', () => 'StepInfo'); + +describe('', () => { + it('render without assesssment steps', () => { + useORAConfigData.mockReturnValue({ + assessmentSteps: { + settings: {}, + }, + submissionConfig: { + startDatetime: '2020-01-01T00:00:00Z', + endDatetime: '2020-01-01T00:00:00Z', + }, + }); + + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.instance.findByType('StepInfo')).toHaveLength(0); + }); + + it('render with assesssment steps', () => { + useORAConfigData.mockReturnValue({ + assessmentSteps: { + settings: { + [stepNames.self]: { + abc: 'def', + }, + [stepNames.peer]: { + ghi: 'jkl', + }, + }, + }, + submissionConfig: { + startDatetime: '2020-01-01T00:00:00Z', + endDatetime: '2020-01-01T00:00:00Z', + }, + }); + + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.instance.findByType('StepInfo')).toHaveLength(2); + }); +}); diff --git a/src/views/XBlockStudioView/components/StudioViewPrompt.jsx b/src/views/XBlockStudioView/components/StudioViewPrompt.jsx index ac30fe6d..7cf60187 100644 --- a/src/views/XBlockStudioView/components/StudioViewPrompt.jsx +++ b/src/views/XBlockStudioView/components/StudioViewPrompt.jsx @@ -14,15 +14,19 @@ const StudioViewPrompt = () => { const { promptIsOpen, togglePrompt } = useXBlockStudioViewContext(); - return prompts.map((prompt, index) => ( - - )); + return ( + <> + {prompts.map((prompt, index) => ( + + ))} + + ); }; export default StudioViewPrompt; diff --git a/src/views/XBlockStudioView/components/StudioViewPrompt.test.jsx b/src/views/XBlockStudioView/components/StudioViewPrompt.test.jsx new file mode 100644 index 00000000..29ddc48a --- /dev/null +++ b/src/views/XBlockStudioView/components/StudioViewPrompt.test.jsx @@ -0,0 +1,36 @@ +import { shallow } from '@edx/react-unit-test-utils'; + +import { usePrompts } from 'hooks/app'; + +import StudioViewPrompt from './StudioViewPrompt'; + +jest.mock('hooks/app', () => ({ + usePrompts: jest.fn(), +})); +jest.mock('./XBlockStudioViewProvider', () => ({ + useXBlockStudioViewContext: () => ({ + promptIsOpen: true, + togglePrompt: jest.fn().mockName('togglePrompt'), + }), +})); +jest.mock('components/Prompt', () => 'Prompt'); + +describe('', () => { + it('render with prompt', () => { + usePrompts.mockReturnValue(['prompt1', 'prompt2']); + + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.instance.findByType('Prompt')).toHaveLength(2); + }); + + it('render without prompt', () => { + usePrompts.mockReturnValue([]); + + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.instance.findByType('Prompt')).toHaveLength(0); + }); +}); diff --git a/src/views/XBlockStudioView/components/StudioViewRubric.jsx b/src/views/XBlockStudioView/components/StudioViewRubric.jsx index d46a481f..c28889b3 100644 --- a/src/views/XBlockStudioView/components/StudioViewRubric.jsx +++ b/src/views/XBlockStudioView/components/StudioViewRubric.jsx @@ -19,7 +19,7 @@ const StudioViewRubric = () => { onToggle={toggleRubric} > {criteria.map((criterion) => ( -
+

{formatMessage(messages.criteriaNameLabel)} {criterion.name} diff --git a/src/views/XBlockStudioView/components/StudioViewRubric.test.jsx b/src/views/XBlockStudioView/components/StudioViewRubric.test.jsx new file mode 100644 index 00000000..e816da96 --- /dev/null +++ b/src/views/XBlockStudioView/components/StudioViewRubric.test.jsx @@ -0,0 +1,68 @@ +import { shallow } from '@edx/react-unit-test-utils'; + +import { useRubricConfig } from 'hooks/app'; + +import StudioViewRubric from './StudioViewRubric'; + +jest.mock('hooks/app', () => ({ + useRubricConfig: jest.fn(), +})); +jest.mock('./XBlockStudioViewProvider', () => ({ + useXBlockStudioViewContext: () => ({ + rubricIsOpen: true, + toggleRubric: jest.fn().mockName('toggleRubric'), + }), +})); + +describe('', () => { + it('render with criteria and options', () => { + useRubricConfig.mockReturnValue({ + criteria: [ + { + name: 'criterion1', + description: 'description1', + options: [ + { + name: 'option1', + label: 'label1', + points: 1, + description: 'description1', + }, + { + name: 'option2', + label: 'label2', + points: 2, + description: 'description2', + }, + ], + }, + { + name: 'criterion2', + description: 'description2', + options: [ + { + name: 'option2', + label: 'label2', + points: 2, + description: 'description2', + }, + ], + }, + ], + }); + + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.instance.findByTestId('criteria-test-id').length).toBe(2); + }); + + it('render without criteria', () => { + useRubricConfig.mockReturnValue({ criteria: [] }); + + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.instance.findByTestId('criteria-test-id').length).toBe(0); + }); +}); diff --git a/src/views/XBlockStudioView/components/StudioViewSettings/FileUploadConfig.test.jsx b/src/views/XBlockStudioView/components/StudioViewSettings/FileUploadConfig.test.jsx new file mode 100644 index 00000000..51e08ef2 --- /dev/null +++ b/src/views/XBlockStudioView/components/StudioViewSettings/FileUploadConfig.test.jsx @@ -0,0 +1,34 @@ +import { shallow } from '@edx/react-unit-test-utils'; + +import { useFileUploadConfig } from 'hooks/app'; + +import FileUploadConfig from './FileUploadConfig'; + +jest.mock('hooks/app', () => ({ + useFileUploadConfig: jest.fn(), +})); +jest.mock('./RequiredConfig', () => 'RequiredConfig'); + +describe('', () => { + it('should render', () => { + useFileUploadConfig.mockReturnValue({ + enabled: true, + fileUploadLimit: 10, + required: true, + }); + + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + }); + + it('should not render', () => { + useFileUploadConfig.mockReturnValue({ + enabled: false, + }); + + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.isEmptyRender()).toBe(true); + }); +}); diff --git a/src/views/XBlockStudioView/components/StudioViewSettings/RequiredConfig.test.jsx b/src/views/XBlockStudioView/components/StudioViewSettings/RequiredConfig.test.jsx new file mode 100644 index 00000000..fc1d132a --- /dev/null +++ b/src/views/XBlockStudioView/components/StudioViewSettings/RequiredConfig.test.jsx @@ -0,0 +1,20 @@ +import { shallow } from '@edx/react-unit-test-utils'; + +import RequiredConfig from './RequiredConfig'; + +describe('', () => { + it('render empty when required is undefined', () => { + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + }); + + it('render required label when required is true', () => { + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + }); + + it('render optional label when required is false', () => { + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + }); +}); diff --git a/src/views/XBlockStudioView/components/StudioViewSettings/__snapshots__/FileUploadConfig.test.jsx.snap b/src/views/XBlockStudioView/components/StudioViewSettings/__snapshots__/FileUploadConfig.test.jsx.snap new file mode 100644 index 00000000..b2161144 --- /dev/null +++ b/src/views/XBlockStudioView/components/StudioViewSettings/__snapshots__/FileUploadConfig.test.jsx.snap @@ -0,0 +1,24 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` should not render 1`] = `null`; + +exports[` should render 1`] = ` + +

+ + File uploads: + + +

+

+ + File upload limit: + + + 10 + +

+ +`; diff --git a/src/views/XBlockStudioView/components/StudioViewSettings/__snapshots__/RequiredConfig.test.jsx.snap b/src/views/XBlockStudioView/components/StudioViewSettings/__snapshots__/RequiredConfig.test.jsx.snap new file mode 100644 index 00000000..80d14c93 --- /dev/null +++ b/src/views/XBlockStudioView/components/StudioViewSettings/__snapshots__/RequiredConfig.test.jsx.snap @@ -0,0 +1,19 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` render empty when required is undefined 1`] = ` + + None + +`; + +exports[` render optional label when required is false 1`] = ` + + Optional + +`; + +exports[` render required label when required is true 1`] = ` + + Required + +`; diff --git a/src/views/XBlockStudioView/components/StudioViewSettings/__snapshots__/index.test.jsx.snap b/src/views/XBlockStudioView/components/StudioViewSettings/__snapshots__/index.test.jsx.snap new file mode 100644 index 00000000..f1e19d75 --- /dev/null +++ b/src/views/XBlockStudioView/components/StudioViewSettings/__snapshots__/index.test.jsx.snap @@ -0,0 +1,127 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` render with leaderboardConfig and enable everything 1`] = ` + + Settings + + } +> +
+

+ + Text response: + + +

+

+ + Response editor: + + + WYSIWYG editor + +

+ +

+ + Allow LaTeX responses: + + + True + +

+

+ + Top responses: + + + 10 + +

+

+ + Teams enabled: + + + True + +

+

+ + Show rubric during response: + + + False + +

+
+
+`; + +exports[` render without leaderboardConfig and disable everything 1`] = ` + + Settings + + } +> +
+

+ + Text response: + + +

+

+ + Response editor: + + + Text editor + +

+ +

+ + Allow LaTeX responses: + + + False + +

+

+ + Teams enabled: + + + False + +

+

+ + Show rubric during response: + + + False + +

+
+
+`; diff --git a/src/views/XBlockStudioView/components/StudioViewSettings/index.jsx b/src/views/XBlockStudioView/components/StudioViewSettings/index.jsx index 2fd073c8..bc999a78 100644 --- a/src/views/XBlockStudioView/components/StudioViewSettings/index.jsx +++ b/src/views/XBlockStudioView/components/StudioViewSettings/index.jsx @@ -54,7 +54,7 @@ const StudioViewSettings = () => {

{leaderboardConfig && leaderboardConfig.enabled ? ( -

+

{formatMessage(messages.topResponsesLabel)} {leaderboardConfig.numberOfEntries}

diff --git a/src/views/XBlockStudioView/components/StudioViewSettings/index.test.jsx b/src/views/XBlockStudioView/components/StudioViewSettings/index.test.jsx new file mode 100644 index 00000000..100c7d0e --- /dev/null +++ b/src/views/XBlockStudioView/components/StudioViewSettings/index.test.jsx @@ -0,0 +1,72 @@ +import { shallow } from '@edx/react-unit-test-utils'; + +import { useORAConfigData } from 'hooks/app'; + +import StudioViewSettings from './index'; + +jest.mock('hooks/app', () => ({ + useORAConfigData: jest.fn(), +})); +jest.mock('../XBlockStudioViewProvider', () => ({ + useXBlockStudioViewContext: () => ({ + settingIsOpen: true, + toggleStudioViewSetting: jest.fn().mockName('toggleStudioViewSetting'), + }), +})); +jest.mock('./RequiredConfig', () => 'RequiredConfig'); +jest.mock('./FileUploadConfig', () => 'FileUploadConfig'); + +describe('', () => { + it('render without leaderboardConfig and disable everything', () => { + useORAConfigData.mockReturnValue({ + submissionConfig: { + textResponseConfig: { + required: false, + allowLatexPreview: false, + editorType: 'text', + }, + teamsConfig: { + enabled: false, + }, + }, + rubricConfig: { + enabled: false, + }, + leaderboardConfig: { + enabled: false, + }, + }); + + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.instance.findByTestId('leaderboard-test-id').length).toBe(0); + }); + + it('render with leaderboardConfig and enable everything', () => { + useORAConfigData.mockReturnValue({ + submissionConfig: { + textResponseConfig: { + required: true, + allowLatexPreview: true, + editorType: 'wysiwyg', + }, + teamsConfig: { + enabled: true, + }, + }, + rubricConfig: { + enabled: true, + }, + leaderboardConfig: { + enabled: true, + numberOfEntries: 10, + }, + }); + + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.instance.findByTestId('leaderboard-test-id').length).toBe(1); + }); +}); diff --git a/src/views/XBlockStudioView/components/StudioViewSteps.test.jsx b/src/views/XBlockStudioView/components/StudioViewSteps.test.jsx new file mode 100644 index 00000000..a3131830 --- /dev/null +++ b/src/views/XBlockStudioView/components/StudioViewSteps.test.jsx @@ -0,0 +1,44 @@ +import { shallow } from '@edx/react-unit-test-utils'; + +import { useORAConfigData } from 'hooks/app'; +import { stepNames } from 'constants/index'; + +import StudioViewSteps from './StudioViewSteps'; + +jest.mock('hooks/app', () => ({ + useORAConfigData: jest.fn(), +})); +jest.mock('./XBlockStudioViewProvider', () => ({ + useXBlockStudioViewContext: () => ({ + assessmentStepsIsOpen: true, + toggleAssessmentSteps: jest.fn().mockName('toggleAssessmentSteps'), + }), +})); + +describe('', () => { + it('render without steps', () => { + useORAConfigData.mockReturnValue({ + assessmentSteps: { + order: [], + }, + }); + + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.instance.findByType('p')).toHaveLength(0); + }); + + it('render with steps', () => { + useORAConfigData.mockReturnValue({ + assessmentSteps: { + order: [stepNames.self, stepNames.peer], + }, + }); + + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.instance.findByType('p')).toHaveLength(2); + }); +}); diff --git a/src/views/XBlockStudioView/components/StudioViewTitle.test.jsx b/src/views/XBlockStudioView/components/StudioViewTitle.test.jsx new file mode 100644 index 00000000..be96c6aa --- /dev/null +++ b/src/views/XBlockStudioView/components/StudioViewTitle.test.jsx @@ -0,0 +1,44 @@ +import { shallow } from '@edx/react-unit-test-utils'; + +import { useORAConfigData } from 'hooks/app'; + +import { useXBlockStudioViewContext } from './XBlockStudioViewProvider'; +import messages from './messages'; + +import StudioViewTitle from './StudioViewTitle'; + +jest.mock('hooks/app', () => ({ + useORAConfigData: jest.fn(), +})); +jest.mock('./XBlockStudioViewProvider', () => ({ + useXBlockStudioViewContext: jest.fn(), +})); + +describe('', () => { + const mockIsAllClosed = jest.fn().mockName('isAllClosed'); + useXBlockStudioViewContext.mockReturnValue({ + isAllClosed: mockIsAllClosed, + toggleAll: jest.fn().mockName('toggleAll'), + }); + useORAConfigData.mockReturnValue({ + title: 'Test Title', + }); + + it('render title and button', () => { + mockIsAllClosed.mockReturnValue(true); + + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.instance.findByType('Button')[0].children[0].el).toBe(messages.expandAllButton.defaultMessage); + }); + + it('render when all is not closed', () => { + mockIsAllClosed.mockReturnValue(false); + + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.instance.findByType('Button')[0].children[0].el).toBe(messages.collapseAllButton.defaultMessage); + }); +}); diff --git a/src/views/XBlockStudioView/components/__snapshots__/StudioViewPrompt.test.jsx.snap b/src/views/XBlockStudioView/components/__snapshots__/StudioViewPrompt.test.jsx.snap new file mode 100644 index 00000000..e9f783ee --- /dev/null +++ b/src/views/XBlockStudioView/components/__snapshots__/StudioViewPrompt.test.jsx.snap @@ -0,0 +1,22 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` render with prompt 1`] = ` + + + + +`; + +exports[` render without prompt 1`] = ``; diff --git a/src/views/XBlockStudioView/components/__snapshots__/StudioViewRubric.test.jsx.snap b/src/views/XBlockStudioView/components/__snapshots__/StudioViewRubric.test.jsx.snap new file mode 100644 index 00000000..9d21574c --- /dev/null +++ b/src/views/XBlockStudioView/components/__snapshots__/StudioViewRubric.test.jsx.snap @@ -0,0 +1,122 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` render with criteria and options 1`] = ` + + Rubric + + } +> +
+

+ + Criteria name: + + criterion1 +

+

+ + Criteria description: + + description1 +

+

+ + Criteria options: + +

+
    +
  • + + label1 + : + 1 + + points + +

    + description1 +

    +
  • +
  • + + label2 + : + 2 + + points + +

    + description2 +

    +
  • +
+
+
+

+ + Criteria name: + + criterion2 +

+

+ + Criteria description: + + description2 +

+

+ + Criteria options: + +

+
    +
  • + + label2 + : + 2 + + points + +

    + description2 +

    +
  • +
+
+
+`; + +exports[` render without criteria 1`] = ` + + Rubric + + } +/> +`; diff --git a/src/views/XBlockStudioView/components/__snapshots__/StudioViewSteps.test.jsx.snap b/src/views/XBlockStudioView/components/__snapshots__/StudioViewSteps.test.jsx.snap new file mode 100644 index 00000000..3cb84d19 --- /dev/null +++ b/src/views/XBlockStudioView/components/__snapshots__/StudioViewSteps.test.jsx.snap @@ -0,0 +1,60 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` render with steps 1`] = ` + + Assessment steps + + } +> +
+

+ + Step + + 1 + : + + + Self assessment + +

+

+ + Step + + 2 + : + + + Peer assessment + +

+
+
+`; + +exports[` render without steps 1`] = ` + + Assessment steps + + } +> +
+ +`; diff --git a/src/views/XBlockStudioView/components/__snapshots__/StudioViewTitle.test.jsx.snap b/src/views/XBlockStudioView/components/__snapshots__/StudioViewTitle.test.jsx.snap new file mode 100644 index 00000000..a4cf8482 --- /dev/null +++ b/src/views/XBlockStudioView/components/__snapshots__/StudioViewTitle.test.jsx.snap @@ -0,0 +1,33 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` render title and button 1`] = ` +
+

+ Test Title +

+ +
+`; + +exports[` render when all is not closed 1`] = ` +
+

+ Test Title +

+ +
+`; diff --git a/src/views/XBlockStudioView/index.jsx b/src/views/XBlockStudioView/index.jsx index 778999ad..5099a134 100644 --- a/src/views/XBlockStudioView/index.jsx +++ b/src/views/XBlockStudioView/index.jsx @@ -10,7 +10,7 @@ import StudioViewPrompt from './components/StudioViewPrompt'; import './index.scss'; -export const XBlockStudioView = () => { +const XBlockStudioView = () => { useEffect(() => { if (window.parent.length > 0) { new ResizeObserver(() => { diff --git a/src/views/XBlockStudioView/index.test.jsx b/src/views/XBlockStudioView/index.test.jsx new file mode 100644 index 00000000..5dd19667 --- /dev/null +++ b/src/views/XBlockStudioView/index.test.jsx @@ -0,0 +1,26 @@ +import { shallow } from '@edx/react-unit-test-utils'; + +import XBlockStudioView from './index'; + +jest.mock('./components/XBlockStudioViewProvider', () => 'XBlockStudioViewProvider'); +jest.mock('./components/StudioSchedule', () => 'StudioSchedule'); +jest.mock('./components/StudioViewSteps', () => 'StudioViewSteps'); +jest.mock('./components/StudioViewSettings', () => 'StudioViewSettings'); +jest.mock('./components/StudioViewRubric', () => 'StudioViewRubric'); +jest.mock('./components/StudioViewTitle', () => 'StudioViewTitle'); +jest.mock('./components/StudioViewPrompt', () => 'StudioViewPrompt'); + +describe('', () => { + it('should render', () => { + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.instance.findByType('XBlockStudioViewProvider').length).toBe(1); + expect(wrapper.instance.findByType('StudioSchedule').length).toBe(1); + expect(wrapper.instance.findByType('StudioViewSteps').length).toBe(1); + expect(wrapper.instance.findByType('StudioViewSettings').length).toBe(1); + expect(wrapper.instance.findByType('StudioViewRubric').length).toBe(1); + expect(wrapper.instance.findByType('StudioViewTitle').length).toBe(1); + expect(wrapper.instance.findByType('StudioViewPrompt').length).toBe(1); + }); +}); diff --git a/src/views/XBlockView/Actions/__snapshots__/index.test.jsx.snap b/src/views/XBlockView/Actions/__snapshots__/index.test.jsx.snap new file mode 100644 index 00000000..edb33fc2 --- /dev/null +++ b/src/views/XBlockView/Actions/__snapshots__/index.test.jsx.snap @@ -0,0 +1,101 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` does not render button when step is staff 1`] = ` +
+`; + +exports[` render load next when step peer is not waiting nor waiting for submission 1`] = ` +
+ +
+`; + +exports[` render load next when step studentTraining is not waiting nor waiting for submission 1`] = ` +
+ +
+`; + +exports[` render message step is not staff and step state done 1`] = ` +
+ +
+`; + +exports[` render message step is not staff and step state inProgress 1`] = ` +
+ +
+`; diff --git a/src/views/XBlockView/Actions/index.jsx b/src/views/XBlockView/Actions/index.jsx index 48afa1cc..c80dea5d 100644 --- a/src/views/XBlockView/Actions/index.jsx +++ b/src/views/XBlockView/Actions/index.jsx @@ -30,6 +30,10 @@ const SubmissionActions = () => { const loadNextAction = useLoadNextAction(); const stepConfigInfo = useAssessmentStepConfig().settings[activeStepName]; + const onClick = React.useCallback(() => { + openModal({ view: activeStepName, title: activeStepName }); + }, [activeStepName, openModal]); + const action = (() => { if ( [stepNames.studentTraining, stepNames.peer].includes(activeStepName) @@ -37,12 +41,17 @@ const SubmissionActions = () => { && stepInfo.numberOfAssessmentsCompleted > 0 && !stepInfo.isWaitingForSubmissions ) { - const onClick = () => openModal({ view: activeStepName, title: activeStepName }); const isOptional = activeStepName === stepNames.peer - && stepInfo.numberOfAssessmentsCompleted >= stepConfigInfo.minNumberToGrade; + && stepInfo.numberOfAssessmentsCompleted + >= stepConfigInfo.minNumberToGrade; return ( - ); } @@ -50,20 +59,19 @@ const SubmissionActions = () => { activeStepName !== stepNames.staff && (stepState === stepStates.inProgress || activeStepName === stepNames.done) ) { - const onClick = () => openModal({ view: activeStepName, title: activeStepName }); return ( - ); } return null; })(); - return ( -
- {action} -
- ); + return
{action}
; }; export default SubmissionActions; diff --git a/src/views/XBlockView/Actions/index.test.jsx b/src/views/XBlockView/Actions/index.test.jsx new file mode 100644 index 00000000..d8a0d4f0 --- /dev/null +++ b/src/views/XBlockView/Actions/index.test.jsx @@ -0,0 +1,107 @@ +import React from 'react'; +import { shallow } from '@edx/react-unit-test-utils'; + +import { stepNames, stepStates } from 'constants/index'; +import { + useAssessmentStepConfig, + useGlobalState, + useStepInfo, +} from 'hooks/app'; +import { useOpenModal } from 'hooks/modal'; + +import SubmissionActions from './index'; + +jest.mock('hooks/actions', () => ({ + useLoadNextAction: () => ({ + action: { + labels: { + default: 'default', + }, + }, + }), +})); +jest.mock('hooks/app', () => ({ + useAssessmentStepConfig: jest.fn(), + useGlobalState: jest.fn(), + useStepInfo: jest.fn(), +})); +jest.mock('hooks/modal', () => ({ + useOpenModal: jest.fn(), +})); + +describe('', () => { + const mockOpenModal = jest.fn(); + beforeEach(() => { + useOpenModal.mockReturnValue(mockOpenModal); + }); + afterEach(() => { + jest.clearAllMocks(); + }); + + [stepNames.studentTraining, stepNames.peer].forEach((stepName) => { + it(`render load next when step ${stepName} is not waiting nor waiting for submission`, () => { + useGlobalState.mockReturnValue({ + activeStepName: stepName, + stepState: stepStates.notAvailable, + }); + useStepInfo.mockReturnValue({ + [stepName]: { + numberOfAssessmentsCompleted: 1, + isWaitingForSubmissions: false, + }, + }); + useAssessmentStepConfig.mockReturnValue({ + settings: { + [stepName]: { + minNumberToGrade: 1, + }, + }, + }); + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.instance.findByType('Button')).toHaveLength(1); + + React.useCallback.mock.calls[0][0](); + expect(mockOpenModal).toHaveBeenCalledWith({ view: stepName, title: stepName }); + }); + }); + + [stepStates.inProgress, stepNames.done].forEach((stepState) => { + it(`render message step is not staff and step state ${stepState}`, () => { + useGlobalState.mockReturnValue({ + activeStepName: stepNames.studentTraining, + stepState, + }); + useStepInfo.mockReturnValue({ + [stepNames.studentTraining]: { + numberOfAssessmentsCompleted: 1, + isWaitingForSubmissions: false, + }, + }); + useAssessmentStepConfig.mockReturnValue({ + settings: { + [stepNames.studentTraining]: { + minNumberToGrade: 1, + }, + }, + }); + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.instance.findByType('Button')).toHaveLength(1); + }); + }); + + it('does not render button when step is staff', () => { + useGlobalState.mockReturnValue({ + activeStepName: stepNames.staff, + stepState: stepStates.notAvailable, + }); + + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.instance.findByType('Button')).toHaveLength(0); + }); +}); diff --git a/src/views/XBlockView/StatusRow/DueDateMessage/__snapshots__/index.test.jsx.snap b/src/views/XBlockView/StatusRow/DueDateMessage/__snapshots__/index.test.jsx.snap new file mode 100644 index 00000000..6bda50b0 --- /dev/null +++ b/src/views/XBlockView/StatusRow/DueDateMessage/__snapshots__/index.test.jsx.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` should render the due date message 1`] = ` +
+ Due in 1 day +
+`; diff --git a/src/views/XBlockView/StatusRow/DueDateMessage/index.test.jsx b/src/views/XBlockView/StatusRow/DueDateMessage/index.test.jsx new file mode 100644 index 00000000..9c30370c --- /dev/null +++ b/src/views/XBlockView/StatusRow/DueDateMessage/index.test.jsx @@ -0,0 +1,15 @@ +import { shallow } from '@edx/react-unit-test-utils'; + +import useDueDateMessage from './useDueDateMessage'; + +import DueDateMessage from './index'; + +jest.mock('./useDueDateMessage'); + +describe('', () => { + it('should render the due date message', () => { + useDueDateMessage.mockReturnValue('Due in 1 day'); + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + }); +}); diff --git a/src/views/XBlockView/StatusRow/DueDateMessage/useDueDateMessage.js b/src/views/XBlockView/StatusRow/DueDateMessage/useDueDateMessage.js index 3604269a..3c72f8a0 100644 --- a/src/views/XBlockView/StatusRow/DueDateMessage/useDueDateMessage.js +++ b/src/views/XBlockView/StatusRow/DueDateMessage/useDueDateMessage.js @@ -9,14 +9,15 @@ import { stepNames, stepStates } from 'constants/index'; import messages from './messages'; +export const dispDate = (value) => { + const date = new Date(moment(value)); + return date.toLocaleString(); +}; + const useDueDateMessage = () => { const { formatMessage } = useIntl(); const { activeStepName, stepState } = useGlobalState(); const stepConfig = useActiveStepConfig(); - const dispDate = (value) => { - const date = new Date(moment(value)); - return date.toLocaleString(); - }; let dueDate; if ( activeStepName === stepNames.done @@ -42,7 +43,7 @@ const useDueDateMessage = () => { const step = formatMessage(pastDueSteps[activeStepName]); return formatMessage(messages.pastDue, { dueDate, step }); } - if (stepStates === stepStates.waitingForPeerGrades) { + if (stepState === stepStates.waitingForPeerGrades) { return formatMessage(messages.waitingForPeerGrades); } const inProgressSteps = { diff --git a/src/views/XBlockView/StatusRow/DueDateMessage/useDueDateMessage.test.js b/src/views/XBlockView/StatusRow/DueDateMessage/useDueDateMessage.test.js new file mode 100644 index 00000000..63bd8d7e --- /dev/null +++ b/src/views/XBlockView/StatusRow/DueDateMessage/useDueDateMessage.test.js @@ -0,0 +1,66 @@ +import { + useActiveStepConfig, + useGlobalState, +} from 'hooks/app'; +import { stepNames, stepStates } from 'constants/index'; + +import useDueDateMessage, { dispDate } from './useDueDateMessage'; + +import messages from './messages'; + +jest.mock('hooks/app', () => ({ + useActiveStepConfig: jest.fn(), + useGlobalState: jest.fn(), +})); + +describe('useDueDateMessage', () => { + const mockActiveStepConfig = { + startDatetime: '2021-01-01T00:00:00Z', + endDatetime: '2021-01-01T00:00:00Z', + }; + const expectedStartDatetime = dispDate(mockActiveStepConfig.startDatetime); + const expectedEndDatetime = dispDate(mockActiveStepConfig.endDatetime); + + useActiveStepConfig.mockReturnValue(mockActiveStepConfig); + [stepNames.done, stepNames.studentTraining, stepNames.staff].forEach((stepName) => { + it(`should return null when stepName is ${stepName}`, () => { + useGlobalState.mockReturnValueOnce({ activeStepName: stepName }); + const output = useDueDateMessage(); + expect(output).toBeNull(); + }); + }); + + [stepStates.cancelled, stepStates.needTeam, stepStates.teamAlreadySubmitted].forEach((stepState) => { + it(`should return null when stepState is ${stepState}`, () => { + useGlobalState.mockReturnValueOnce({ stepState }); + const output = useDueDateMessage(); + expect(output).toBeNull(); + }); + }); + + it('should return availableStartingOn message when stepState is notAvailable', () => { + useGlobalState.mockReturnValueOnce({ stepState: stepStates.notAvailable }); + const output = useDueDateMessage(); + expect(output).toContain(expectedStartDatetime); + }); + + it('should return pastDue message when stepState is closed', () => { + useGlobalState.mockReturnValueOnce({ activeStepName: stepNames.submission, stepState: stepStates.closed }); + const output = useDueDateMessage(); + expect(output).toContain(expectedEndDatetime); + }); + + it('should return waitingForPeerGrades message when stepState is waitingForPeerGrades', () => { + useGlobalState.mockReturnValueOnce({ stepState: stepStates.waitingForPeerGrades }); + const output = useDueDateMessage(); + expect(output).toEqual(messages.waitingForPeerGrades.defaultMessage); + }); + + [stepNames.submission, stepNames.self, stepNames.peer].forEach((stepName) => { + it(`should return dueDate message when stepState is not ${stepStates.closed} or ${stepStates.waitingForPeerGrades}`, () => { + useGlobalState.mockReturnValueOnce({ activeStepName: stepName }); + const output = useDueDateMessage(); + expect(output).toContain(expectedEndDatetime); + }); + }); +}); diff --git a/src/views/XBlockView/StatusRow/StatusBadge/__snapshots__/index.test.jsx.snap b/src/views/XBlockView/StatusRow/StatusBadge/__snapshots__/index.test.jsx.snap new file mode 100644 index 00000000..ae3955e0 --- /dev/null +++ b/src/views/XBlockView/StatusRow/StatusBadge/__snapshots__/index.test.jsx.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`StatusBadge renders 1`] = ` + + defaultMessage + +`; diff --git a/src/views/XBlockView/StatusRow/StatusBadge/index.test.jsx b/src/views/XBlockView/StatusRow/StatusBadge/index.test.jsx new file mode 100644 index 00000000..bcd9f0f0 --- /dev/null +++ b/src/views/XBlockView/StatusRow/StatusBadge/index.test.jsx @@ -0,0 +1,18 @@ +import { shallow } from '@edx/react-unit-test-utils'; + +import StatusBadge from './index'; + +jest.mock('./useBadgeConfig', () => () => ({ + variant: 'variant', + message: { + id: 'message', + defaultMessage: 'defaultMessage', + }, +})); + +describe('StatusBadge', () => { + it('renders', () => { + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + }); +}); diff --git a/src/views/XBlockView/StatusRow/StatusBadge/useBadgeConfig.js b/src/views/XBlockView/StatusRow/StatusBadge/useBadgeConfig.js index 755fe849..f67817de 100644 --- a/src/views/XBlockView/StatusRow/StatusBadge/useBadgeConfig.js +++ b/src/views/XBlockView/StatusRow/StatusBadge/useBadgeConfig.js @@ -8,7 +8,7 @@ import { import messages from './messages'; -const badgeConfig = StrictDict({ +export const badgeConfig = StrictDict({ [stepStates.cancelled]: { variant: 'danger', message: messages.cancelled }, [stepStates.notAvailable]: { variant: 'light', message: messages.notAvailable }, [stepStates.inProgress]: { variant: 'primary', message: messages.inProgress }, diff --git a/src/views/XBlockView/StatusRow/StatusBadge/useBadgeConfig.test.js b/src/views/XBlockView/StatusRow/StatusBadge/useBadgeConfig.test.js new file mode 100644 index 00000000..faac9475 --- /dev/null +++ b/src/views/XBlockView/StatusRow/StatusBadge/useBadgeConfig.test.js @@ -0,0 +1,62 @@ +import { useGlobalState } from 'hooks/app'; +import { stepNames, stepStates } from 'constants/index'; +import useBadgeConfig, { badgeConfig } from './useBadgeConfig'; + +jest.mock('hooks/app', () => ({ + useGlobalState: jest.fn(), +})); + +describe('useBadgeConfig', () => { + const mockUseGlobalState = { + activeStepName: stepNames.submission, + stepState: stepStates.inProgress, + lastStep: stepNames.submission, + hasReceivedFinalGrade: false, + }; + + it('should return badge done when hasReceivedFinalGrade is true', () => { + useGlobalState.mockReturnValue({ + ...mockUseGlobalState, + hasReceivedFinalGrade: true, + }); + const output = useBadgeConfig(); + expect(output).toEqual(badgeConfig[stepNames.done]); + }); + + it('should return badge cancelled when stepState is cancelled', () => { + useGlobalState.mockReturnValue({ + ...mockUseGlobalState, + stepState: stepStates.cancelled, + }); + const output = useBadgeConfig(); + expect(output).toEqual(badgeConfig[stepStates.cancelled]); + }); + + it('should return badge staffAfter when activeStepName is staff', () => { + useGlobalState.mockReturnValue({ + ...mockUseGlobalState, + activeStepName: stepNames.staff, + }); + const output = useBadgeConfig(); + expect(output).toEqual(badgeConfig.staffAfter[stepNames.submission]); + }); + + [ + stepStates.notAvailable, + stepStates.inProgress, + stepStates.closed, + stepStates.needTeam, + stepStates.teamAlreadySubmitted, + stepStates.waiting, + stepStates.waitingForPeerGrades, + ].forEach((state) => { + it(`should return badge ${state} when stepState is ${state}`, () => { + useGlobalState.mockReturnValue({ + ...mockUseGlobalState, + stepState: state, + }); + const output = useBadgeConfig(); + expect(output).toEqual(badgeConfig[state]); + }); + }); +}); diff --git a/src/views/XBlockView/StatusRow/__snapshots__/index.test.jsx.snap b/src/views/XBlockView/StatusRow/__snapshots__/index.test.jsx.snap new file mode 100644 index 00000000..e2c5756d --- /dev/null +++ b/src/views/XBlockView/StatusRow/__snapshots__/index.test.jsx.snap @@ -0,0 +1,10 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders StatusBadge and DueDateMessage 1`] = ` +
+ + +
+`; diff --git a/src/views/XBlockView/StatusRow/index.test.jsx b/src/views/XBlockView/StatusRow/index.test.jsx new file mode 100644 index 00000000..ef52026d --- /dev/null +++ b/src/views/XBlockView/StatusRow/index.test.jsx @@ -0,0 +1,15 @@ +import { shallow } from '@edx/react-unit-test-utils'; + +import StatusRow from './index'; + +jest.mock('./StatusBadge', () => 'StatusBadge'); +jest.mock('./DueDateMessage', () => 'DueDateMessage'); + +describe('', () => { + it('renders StatusBadge and DueDateMessage', () => { + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + expect(wrapper.instance.findByType('StatusBadge')).toHaveLength(1); + expect(wrapper.instance.findByType('DueDateMessage')).toHaveLength(1); + }); +}); diff --git a/src/views/XBlockView/__snapshots__/index.test.jsx.snap b/src/views/XBlockView/__snapshots__/index.test.jsx.snap new file mode 100644 index 00000000..8ffdd92f --- /dev/null +++ b/src/views/XBlockView/__snapshots__/index.test.jsx.snap @@ -0,0 +1,72 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` does not render prompts and rubric when step is unavailable 1`] = ` +
+

+ title +

+ + + + + + +
+`; + +exports[` render everything 1`] = ` +
+

+ title +

+ + + + + + + + + + + +
+`; + +exports[` render everything without rubric 1`] = ` +
+

+ title +

+ + + + + + + + + + +
+`; diff --git a/src/views/XBlockView/index.jsx b/src/views/XBlockView/index.jsx index 68223a44..1d7a13fa 100644 --- a/src/views/XBlockView/index.jsx +++ b/src/views/XBlockView/index.jsx @@ -19,7 +19,7 @@ import Actions from './Actions'; import './index.scss'; -export const XBlockView = () => { +const XBlockView = () => { const { title } = useORAConfigData(); const prompts = usePrompts(); const rubricConfig = useRubricConfig(); diff --git a/src/views/XBlockView/index.test.jsx b/src/views/XBlockView/index.test.jsx new file mode 100644 index 00000000..9bc968f8 --- /dev/null +++ b/src/views/XBlockView/index.test.jsx @@ -0,0 +1,65 @@ +import { shallow } from '@edx/react-unit-test-utils'; +import { + useORAConfigData, + usePrompts, + useRubricConfig, + useGlobalState, +} from 'hooks/app'; + +import XBlockView from './index'; + +jest.mock('hooks/app', () => ({ + useORAConfigData: jest.fn(), + usePrompts: jest.fn(), + useRubricConfig: jest.fn(), + useGlobalState: jest.fn(), +})); +jest.mock('components/ProgressBar', () => 'ProgressBar'); +jest.mock('components/Prompt', () => 'Prompt'); +jest.mock('components/Rubric', () => 'Rubric'); +jest.mock('components/Instructions', () => 'Instructions'); +jest.mock('components/StatusAlert', () => 'StatusAlert'); +jest.mock('components/HotjarSurvey', () => 'HotjarSurvey'); +jest.mock('./StatusRow', () => 'StatusRow'); +jest.mock('./Actions', () => 'Actions'); + +describe('', () => { + it('render everything', () => { + useORAConfigData.mockReturnValue({ title: 'title' }); + usePrompts.mockReturnValue(['prompt1', 'prompt2']); + useRubricConfig.mockReturnValue({ showDuringResponse: true }); + useGlobalState.mockReturnValue({ stepIsUnavailable: false }); + + const wrapper = shallow(); + + expect(wrapper.snapshot).toMatchSnapshot(); + expect(wrapper.instance.findByType('Prompt')).toHaveLength(2); + expect(wrapper.instance.findByType('Rubric')).toHaveLength(1); + }); + + it('render everything without rubric', () => { + useORAConfigData.mockReturnValue({ title: 'title' }); + usePrompts.mockReturnValue(['prompt1', 'prompt2']); + useRubricConfig.mockReturnValue({ showDuringResponse: false }); + useGlobalState.mockReturnValue({ stepIsUnavailable: false }); + + const wrapper = shallow(); + + expect(wrapper.snapshot).toMatchSnapshot(); + expect(wrapper.instance.findByType('Prompt')).toHaveLength(2); + expect(wrapper.instance.findByType('Rubric')).toHaveLength(0); + }); + + it('does not render prompts and rubric when step is unavailable', () => { + useORAConfigData.mockReturnValue({ title: 'title' }); + usePrompts.mockReturnValue(['prompt1', 'prompt2']); + useRubricConfig.mockReturnValue({ showDuringResponse: true }); + useGlobalState.mockReturnValue({ stepIsUnavailable: true }); + + const wrapper = shallow(); + + expect(wrapper.snapshot).toMatchSnapshot(); + expect(wrapper.instance.findByType('Prompt')).toHaveLength(0); + expect(wrapper.instance.findByType('Rubric')).toHaveLength(0); + }); +});