From cc33edbef2f38e516ced27649290d93be74f9007 Mon Sep 17 00:00:00 2001 From: leangseu-edx <83240113+leangseu-edx@users.noreply.github.com> Date: Fri, 22 Sep 2023 14:58:41 -0400 Subject: [PATCH] lk/submission actions (#43) * chore: remove debris * feat: interactive submission actions * chore: add dirty for finishing later * chore: update fake api because of camelcase * chore: linting * chore: component tests for file upload * chore: component test for prompt * chore: component test for test response * chore: add component tests to submission view * chore: linting * chore: update test for data * chore: rename * chore: fix rich text editor --- src/App.jsx | 19 ++ src/components/FileUpload/ActionCell.jsx | 27 +++ src/components/FileUpload/ActionCell.test.jsx | 9 + src/components/FileUpload/FileMetaDisplay.jsx | 1 - .../FileUpload/UploadConfirmModal.jsx | 94 +++++++++ .../FileUpload/UploadConfirmModal.test.jsx | 57 ++++++ .../__snapshots__/ActionCell.test.jsx.snap | 16 ++ .../UploadConfirmModal.test.jsx.snap | 184 ++++++++++++++++++ .../__snapshots__/index.test.jsx.snap | 140 +++++++++++++ src/components/FileUpload/hooks.js | 73 +++++++ src/components/FileUpload/index.jsx | 59 +++++- src/components/FileUpload/index.test.jsx | 69 +++++++ src/components/FileUpload/messages.js | 40 ++++ src/components/FileUpload/styles.scss | 8 + .../Prompt/__snapshots__/index.test.jsx.snap | 33 ++++ src/components/Prompt/hooks.js | 14 ++ src/components/Prompt/index.jsx | 17 +- src/components/Prompt/index.test.jsx | 34 ++++ src/components/Rubric/index.jsx | 17 +- .../TextResponse/RichTextEditor.jsx | 39 ++-- .../TextResponse/RichTextEditor.test.jsx | 45 +++++ src/components/TextResponse/TextEditor.jsx | 39 ++-- .../TextResponse/TextEditor.test.jsx | 25 +++ .../RichTextEditor.test.jsx.snap | 101 ++++++++++ .../__snapshots__/TextEditor.test.jsx.snap | 29 +++ .../__snapshots__/index.test.jsx.snap | 26 +++ src/components/TextResponse/index.jsx | 23 ++- src/components/TextResponse/index.test.jsx | 35 ++++ src/components/UploadedFiles/index.jsx | 30 --- src/data/services/lms/constants.js | 9 +- .../services/lms/fakeData/pageData/index.jsx | 2 +- src/data/services/lms/hooks/actions.ts | 68 +++++++ src/data/services/lms/hooks/data.test.ts | 2 +- src/data/services/lms/hooks/data.ts | 2 +- src/setupTest.js | 10 + .../SubmissionView/SubmissionActions.jsx | 59 +++++- .../SubmissionView/SubmissionActions.test.jsx | 16 ++ .../SubmissionView/SubmissionContent.jsx | 78 ++++++-- .../SubmissionView/SubmissionContent.test.jsx | 48 +++++ .../SubmissionContentLayout.jsx | 47 +++-- .../SubmissionContentLayout.test.jsx | 31 +++ .../SubmissionActions.test.jsx.snap | 39 ++++ .../SubmissionContent.test.jsx.snap | 101 ++++++++++ .../SubmissionContentLayout.test.jsx.snap | 64 ++++++ .../__snapshots__/index.test.jsx.snap | 26 +++ src/views/SubmissionView/hooks.js | 70 +++++++ src/views/SubmissionView/index.jsx | 53 ++--- src/views/SubmissionView/index.test.jsx | 24 +++ src/views/SubmissionView/messages.js | 54 +++++ 49 files changed, 1932 insertions(+), 174 deletions(-) create mode 100644 src/components/FileUpload/ActionCell.jsx create mode 100644 src/components/FileUpload/ActionCell.test.jsx create mode 100644 src/components/FileUpload/UploadConfirmModal.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__/UploadConfirmModal.test.jsx.snap create mode 100644 src/components/FileUpload/__snapshots__/index.test.jsx.snap create mode 100644 src/components/FileUpload/hooks.js create mode 100644 src/components/FileUpload/index.test.jsx create mode 100644 src/components/FileUpload/styles.scss create mode 100644 src/components/Prompt/__snapshots__/index.test.jsx.snap create mode 100644 src/components/Prompt/hooks.js create mode 100644 src/components/Prompt/index.test.jsx create mode 100644 src/components/TextResponse/RichTextEditor.test.jsx create mode 100644 src/components/TextResponse/TextEditor.test.jsx create mode 100644 src/components/TextResponse/__snapshots__/RichTextEditor.test.jsx.snap create mode 100644 src/components/TextResponse/__snapshots__/TextEditor.test.jsx.snap create mode 100644 src/components/TextResponse/__snapshots__/index.test.jsx.snap create mode 100644 src/components/TextResponse/index.test.jsx delete mode 100644 src/components/UploadedFiles/index.jsx create mode 100644 src/views/SubmissionView/SubmissionActions.test.jsx create mode 100644 src/views/SubmissionView/SubmissionContent.test.jsx create mode 100644 src/views/SubmissionView/SubmissionContentLayout.test.jsx create mode 100644 src/views/SubmissionView/__snapshots__/SubmissionActions.test.jsx.snap create mode 100644 src/views/SubmissionView/__snapshots__/SubmissionContent.test.jsx.snap create mode 100644 src/views/SubmissionView/__snapshots__/SubmissionContentLayout.test.jsx.snap create mode 100644 src/views/SubmissionView/__snapshots__/index.test.jsx.snap create mode 100644 src/views/SubmissionView/hooks.js create mode 100644 src/views/SubmissionView/index.test.jsx create mode 100644 src/views/SubmissionView/messages.js diff --git a/src/App.jsx b/src/App.jsx index fa36e6c5..b607c6f2 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,6 +1,9 @@ import { Routes, Route } from 'react-router-dom'; import { ErrorPage } from '@edx/frontend-platform/react'; import { useIntl } from '@edx/frontend-platform/i18n'; +import { Spinner } from '@edx/paragon'; + +import { useIsORAConfigLoaded, useIsPageDataLoaded } from 'data/services/lms/hooks/selectors'; import PeerAssessmentView from 'views/PeerAssessmentView'; import SelfAssessmentView from 'views/SelfAssessmentView'; @@ -12,6 +15,22 @@ import routes from './routes'; const RouterRoot = () => { const { formatMessage } = useIntl(); + const isConfigLoaded = useIsORAConfigLoaded(); + const isPageLoaded = useIsPageDataLoaded(); + + if (!isConfigLoaded || !isPageLoaded) { + return ( +
+ +
+ ); + } + return ( } /> diff --git a/src/components/FileUpload/ActionCell.jsx b/src/components/FileUpload/ActionCell.jsx new file mode 100644 index 00000000..047795a5 --- /dev/null +++ b/src/components/FileUpload/ActionCell.jsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { IconButton, Icon } from '@edx/paragon'; + +import { useIntl } from '@edx/frontend-platform/i18n'; +import { Delete, Preview } from '@edx/paragon/icons'; + +import messages from './messages'; + +const ActionCell = () => { + const { formatMessage } = useIntl(); + return ( + <> + + + + ); +}; + +export default ActionCell; diff --git a/src/components/FileUpload/ActionCell.test.jsx b/src/components/FileUpload/ActionCell.test.jsx new file mode 100644 index 00000000..f9992b6a --- /dev/null +++ b/src/components/FileUpload/ActionCell.test.jsx @@ -0,0 +1,9 @@ +import { shallow } from '@edx/react-unit-test-utils'; +import ActionCell from './ActionCell'; + +describe('', () => { + it('renders', () => { + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + }); +}); diff --git a/src/components/FileUpload/FileMetaDisplay.jsx b/src/components/FileUpload/FileMetaDisplay.jsx index de66bccd..cbadc05e 100644 --- a/src/components/FileUpload/FileMetaDisplay.jsx +++ b/src/components/FileUpload/FileMetaDisplay.jsx @@ -8,7 +8,6 @@ import messages from './messages'; const FileMetaDisplay = ({ name, description, size }) => ( <> - {console.log({ name, description, size })}

diff --git a/src/components/FileUpload/UploadConfirmModal.jsx b/src/components/FileUpload/UploadConfirmModal.jsx new file mode 100644 index 00000000..8f537a1a --- /dev/null +++ b/src/components/FileUpload/UploadConfirmModal.jsx @@ -0,0 +1,94 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { + Form, FormLabel, ModalDialog, Button, ActionRow, +} from '@edx/paragon'; +import { useIntl } from '@edx/frontend-platform/i18n'; +import messages from './messages'; +import { useUploadConfirmModalHooks } from './hooks'; + +const UploadConfirmModal = ({ + open, files, closeHandler, uploadHandler, +}) => { + const { formatMessage } = useIntl(); + + const { + errors, exitHandler, confirmUploadClickHandler, onFileDescriptionChange, + } = useUploadConfirmModalHooks({ + files, + closeHandler, + uploadHandler, + }); + + return ( + + + + {formatMessage(messages.uploadFileModalTitle)} + + + + +
+ {files.map((file, i) => ( + // eslint-disable-next-line react/no-array-index-key + + + + {formatMessage(messages.uploadFileDescriptionFieldLabel)} + + {file.name} + + + {errors[i] && ( + + {errors[i] && formatMessage(messages.fileDescriptionMissingError)} + + )} + + ))} +
+
+ + + + {formatMessage(messages.cancelUploadFileButton)} + + + + +
+ ); +}; + +UploadConfirmModal.defaultProps = { + open: false, + files: [], + closeHandler: () => {}, + uploadHandler: () => {}, +}; +UploadConfirmModal.propTypes = { + open: PropTypes.bool, + files: PropTypes.arrayOf( + PropTypes.shape({ + name: PropTypes.string, + description: PropTypes.string, + }), + ), + closeHandler: PropTypes.func, + uploadHandler: PropTypes.func, +}; + +export default UploadConfirmModal; diff --git a/src/components/FileUpload/UploadConfirmModal.test.jsx b/src/components/FileUpload/UploadConfirmModal.test.jsx new file mode 100644 index 00000000..ff56839d --- /dev/null +++ b/src/components/FileUpload/UploadConfirmModal.test.jsx @@ -0,0 +1,57 @@ +import { shallow } from '@edx/react-unit-test-utils'; +import UploadConfirmModal from './UploadConfirmModal'; + +import { useUploadConfirmModalHooks } from './hooks'; + +jest.mock('./hooks', () => ({ + useUploadConfirmModalHooks: jest.fn(), +})); + +describe('', () => { + const props = { + open: true, + files: [], + closeHandler: jest.fn().mockName('closeHandler'), + uploadHandler: jest.fn().mockName('uploadHandler'), + }; + + const mockHooks = (overrides) => { + useUploadConfirmModalHooks.mockReturnValueOnce({ + errors: [], + exitHandler: jest.fn().mockName('exitHandler'), + confirmUploadClickHandler: jest.fn().mockName('confirmUploadClickHandler'), + onFileDescriptionChange: () => jest.fn().mockName('onFileDescriptionChange'), + ...overrides, + }); + }; + describe('renders', () => { + test('no files', () => { + mockHooks(); + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.instance.findByType('Form.Group').length).toBe(0); + }); + + test('multiple files', () => { + mockHooks( + { errors: new Array(2) }, + ); + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.instance.findByType('Form.Group').length).toBe(2); + expect(wrapper.instance.findByType('Form.Control.Feedback').length).toBe(0); + }); + + test('with errors', () => { + mockHooks({ errors: [true, false] }); + const wrapper = shallow(); + // wrapper.setState({ errors: [true, false] }); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.instance.findByType('Form.Group').length).toBe(2); + expect(wrapper.instance.findByType('Form.Control.Feedback').length).toBe(1); + }); + }); +}); 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..c8b38339 --- /dev/null +++ b/src/components/FileUpload/__snapshots__/ActionCell.test.jsx.snap @@ -0,0 +1,16 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders 1`] = ` + + + + +`; 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..7743f457 --- /dev/null +++ b/src/components/FileUpload/__snapshots__/UploadConfirmModal.test.jsx.snap @@ -0,0 +1,184 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders multiple files 1`] = ` + + + + Add a text description to your file + + + +
+ + + + Description for: + + + file1 + + + + + + + + Description for: + + + file2 + + + + +
+
+ + + + Cancel upload + + + + +
+`; + +exports[` renders no files 1`] = ` + + + + Add a text description to your file + + + +
+ + + + + Cancel upload + + + + + +`; + +exports[` renders with errors 1`] = ` + + + + Add a text description to your file + + + +
+ + + + Description for: + + + file1 + + + + + Please enter a file description + + + + + + Description for: + + + file2 + + + + +
+
+ + + + 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..1056f241 --- /dev/null +++ b/src/components/FileUpload/__snapshots__/index.test.jsx.snap @@ -0,0 +1,140 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders default 1`] = ` +
+

+ File Upload +

+ + + Uploaded Files + + + + + +
+`; + +exports[` renders no uploaded files 1`] = ` +
+

+ File Upload +

+ + +
+`; + +exports[` renders read only 1`] = ` +
+

+ File Upload +

+ + + Uploaded Files + + + + +
+`; diff --git a/src/components/FileUpload/hooks.js b/src/components/FileUpload/hooks.js new file mode 100644 index 00000000..fec24c8c --- /dev/null +++ b/src/components/FileUpload/hooks.js @@ -0,0 +1,73 @@ +import { useState, useReducer, useCallback } from 'react'; + +export const useUploadConfirmModalHooks = ({ + files, closeHandler, uploadHandler, +}) => { + const [errors, setErrors] = useState([]); + + const confirmUploadClickHandler = () => { + const errorList = files.map((file) => (!file.description)); + setErrors(errorList); + if (errorList.some((error) => error)) { + return; + } + uploadHandler(); + }; + + const exitHandler = () => { + setErrors([]); + closeHandler(); + }; + + // Modifying pointer of file object. This is not a good practice. + // eslint-disable-next-line no-param-reassign, no-return-assign + const onFileDescriptionChange = (file) => (event) => file.description = event.target.value; + + return { + errors, + confirmUploadClickHandler, + exitHandler, + onFileDescriptionChange, + }; +}; + +export const useFileUploadHooks = ({ + onFileUploaded, +}) => { + const [uploadState, dispatchUploadState] = useReducer( + (state, payload) => ({ ...state, ...payload }), + { + onProcessUploadArgs: {}, + openModal: false, + }, + ); + + const confirmUpload = useCallback(async () => { + dispatchUploadState({ openModal: false }); + await onFileUploaded(uploadState.onProcessUploadArgs); + dispatchUploadState({ onProcessUploadArgs: {} }); + }, [uploadState, onFileUploaded]); + + const closeUploadModal = useCallback(() => { + dispatchUploadState({ openModal: false, onProcessUploadArgs: {} }); + }, []); + + const onProcessUpload = useCallback(({ fileData, handleError, requestConfig }) => { + dispatchUploadState({ + onProcessUploadArgs: { fileData, handleError, requestConfig }, + openModal: true, + }); + }, []); + + return { + uploadState, + confirmUpload, + closeUploadModal, + onProcessUpload, + }; +}; + +export default { + useUploadConfirmModalHooks, + useFileUploadHooks, +}; diff --git a/src/components/FileUpload/index.jsx b/src/components/FileUpload/index.jsx index 02c4aa96..476355db 100644 --- a/src/components/FileUpload/index.jsx +++ b/src/components/FileUpload/index.jsx @@ -1,28 +1,43 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { DataTable, Dropzone } from '@edx/paragon'; - -import { useSubmissionResponse } from 'data/services/lms/hooks/selectors'; +import { DataTable, Dropzone } from '@edx/paragon'; import { useIntl } from '@edx/frontend-platform/i18n'; + import filesize from 'filesize'; +import UploadConfirmModal from './UploadConfirmModal'; +import ActionCell from './ActionCell'; + +import { useFileUploadHooks } from './hooks'; import messages from './messages'; -const FileUpload = ({ isReadOnly }) => { - const { uploadedFiles } = useSubmissionResponse(); +import './styles.scss'; + +const FileUpload = ({ isReadOnly, uploadedFiles, onFileUploaded }) => { const { formatMessage } = useIntl(); + + const { + uploadState, + confirmUpload, + closeUploadModal, + onProcessUpload, + } = useFileUploadHooks({ + onFileUploaded, + }); + return (

File Upload

- {uploadedFiles && ( + {uploadedFiles.length > 0 && ( <> Uploaded Files ({ + data={uploadedFiles.map((file) => ({ ...file, - size: typeof file.size === 'number' ? filesize(file.size) : 'Unknown', + size: + typeof file.size === 'number' ? filesize(file.size) : 'Unknown', }))} columns={[ { @@ -37,20 +52,46 @@ const FileUpload = ({ isReadOnly }) => { Header: formatMessage(messages.fileSizeTitle), accessor: 'fileSize', }, + { + Header: formatMessage(messages.fileActionsTitle), + accessor: 'actions', + Cell: ActionCell, + }, ]} /> )} - {!isReadOnly && } + {!isReadOnly && ( + + )} +
); }; FileUpload.defaultProps = { isReadOnly: false, + uploadedFiles: [], }; FileUpload.propTypes = { isReadOnly: PropTypes.bool, + uploadedFiles: PropTypes.arrayOf( + PropTypes.shape({ + fileDescription: PropTypes.string, + fileName: PropTypes.string, + fileSize: PropTypes.number, + }), + ), + onFileUploaded: PropTypes.func.isRequired, }; export default FileUpload; diff --git a/src/components/FileUpload/index.test.jsx b/src/components/FileUpload/index.test.jsx new file mode 100644 index 00000000..62407c82 --- /dev/null +++ b/src/components/FileUpload/index.test.jsx @@ -0,0 +1,69 @@ +import { shallow } from '@edx/react-unit-test-utils'; +import FileUpload from '.'; + +import { useFileUploadHooks } from './hooks'; + +jest.mock('./hooks', () => ({ + useFileUploadHooks: jest.fn(), +})); + +jest.mock('./UploadConfirmModal', () => 'UploadConfirmModal'); +jest.mock('./ActionCell', () => 'ActionCell'); + +describe('', () => { + const props = { + isReadOnly: false, + uploadedFiles: [ + { + fileName: 'file1', + fileDescription: 'file1 desc', + fileSize: 100, + }, + { + fileName: 'file2', + fileDescription: 'file2 desc', + fileSize: 200, + }, + ], + onFileUploaded: jest.fn(), + }; + + const mockHooks = (overrides) => { + useFileUploadHooks.mockReturnValueOnce({ + uploadState: { + onProcessUploadArgs: {}, + openModal: false, + }, + confirmUpload: jest.fn().mockName('confirmUpload'), + closeUploadModal: jest.fn().mockName('closeUploadModal'), + onProcessUpload: jest.fn().mockName('onProcessUpload'), + ...overrides, + }); + }; + describe('renders', () => { + test('default', () => { + mockHooks(); + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.instance.findByType('Dropzone')).toHaveLength(1); + expect(wrapper.instance.findByType('DataTable')).toHaveLength(1); + }); + + test('read only', () => { + mockHooks({ isReadOnly: true }); + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.instance.findByType('Dropzone')).toHaveLength(0); + }); + + test('no uploaded files', () => { + mockHooks(); + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.instance.findByType('DataTable')).toHaveLength(0); + }); + }); +}); diff --git a/src/components/FileUpload/messages.js b/src/components/FileUpload/messages.js index 8a4164bd..a3e06213 100644 --- a/src/components/FileUpload/messages.js +++ b/src/components/FileUpload/messages.js @@ -16,6 +16,46 @@ const messages = defineMessages({ defaultMessage: 'File Size', description: ' title for file size', }, + fileActionsTitle: { + id: 'ora-grading.FileCellContent.fileActionsTitle', + defaultMessage: 'Actions', + description: ' title for file actions', + }, + deleteButtonAltText: { + id: 'ora-grading.FileCellContent.deleteButtonAltText', + defaultMessage: 'Delete', + description: ' alt text for delete button', + }, + previewButtonAltText: { + id: 'ora-grading.FileCellContent.previewButtonAltText', + defaultMessage: 'Preview', + description: ' alt text for preview button', + }, + uploadFileModalTitle: { + id: 'ora-grading.FileCellContent.uploadFileModalTitle', + defaultMessage: 'Add a text description to your file', + description: 'Ask user to add a text description to the file', + }, + uploadFileDescriptionFieldLabel: { + id: 'ora-grading.FileCellContent.uploadFileDescriptionFieldLabel', + defaultMessage: 'Description for: ', + description: 'Label for file description field', + }, + cancelUploadFileButton: { + id: 'ora-grading.FileCellContent.cancelUploadFileButton', + defaultMessage: 'Cancel upload', + description: 'Label for cancel button', + }, + confirmUploadFileButton: { + id: 'ora-grading.FileCellContent.confirmUploadFileButton', + defaultMessage: 'Upload files', + description: 'Label for upload button', + }, + fileDescriptionMissingError: { + id: 'ora-grading.FileCellContent.fileDescriptionMissingError', + defaultMessage: 'Please enter a file description', + description: 'Error message when file description is missing', + }, }); export default messages; diff --git a/src/components/FileUpload/styles.scss b/src/components/FileUpload/styles.scss new file mode 100644 index 00000000..7ed24d08 --- /dev/null +++ b/src/components/FileUpload/styles.scss @@ -0,0 +1,8 @@ +.file-name-ellipsis { + width: 50%; + display: inline-block; + text-overflow: ellipsis; + vertical-align: middle; + overflow: hidden; + white-space: nowrap; +} \ No newline at end of file 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..7c4dfd9a --- /dev/null +++ b/src/components/Prompt/__snapshots__/index.test.jsx.snap @@ -0,0 +1,33 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders 1`] = ` + +
prompt

", + } + } + /> + +`; + +exports[` renders closed 1`] = ` + +
prompt

", + } + } + /> + +`; diff --git a/src/components/Prompt/hooks.js b/src/components/Prompt/hooks.js new file mode 100644 index 00000000..2cc77124 --- /dev/null +++ b/src/components/Prompt/hooks.js @@ -0,0 +1,14 @@ +import { useState } from 'react'; + +const usePromptHooks = () => { + const [open, setOpen] = useState(true); + + const toggleOpen = () => setOpen(!open); + + return { + open, + toggleOpen, + }; +}; + +export default usePromptHooks; diff --git a/src/components/Prompt/index.jsx b/src/components/Prompt/index.jsx index 2e66cce2..4e37a4ba 100644 --- a/src/components/Prompt/index.jsx +++ b/src/components/Prompt/index.jsx @@ -1,20 +1,21 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { useORAConfigData } from 'data/services/lms/hooks/selectors'; +import { Collapsible } from '@edx/paragon'; -export const Prompt = ({ promptIndex }) => { - const { prompts } = useORAConfigData(); +import usePromptHooks from './hooks'; + +const Prompt = ({ prompt }) => { + const { open, toggleOpen } = usePromptHooks(); return ( -
-

Prompt {promptIndex + 1}

-
-
+ +
+ ); }; Prompt.propTypes = { - promptIndex: PropTypes.number.isRequired, + prompt: PropTypes.string.isRequired, }; export default Prompt; diff --git a/src/components/Prompt/index.test.jsx b/src/components/Prompt/index.test.jsx new file mode 100644 index 00000000..627e22f4 --- /dev/null +++ b/src/components/Prompt/index.test.jsx @@ -0,0 +1,34 @@ +import { shallow } from '@edx/react-unit-test-utils'; +import Prompt from '.'; + +import usePromptHooks from './hooks'; + +jest.mock('./hooks', () => jest.fn()); + +describe('', () => { + const props = { + prompt: '

prompt

', + }; + const mockHooks = (overrides) => { + usePromptHooks.mockReturnValueOnce({ + open: true, + toggleOpen: jest.fn().mockName('toggleOpen'), + ...overrides, + }); + }; + it('renders', () => { + mockHooks(); + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.instance.findByType('Collapsible')[0].props.title).toEqual(''); + }); + + it('renders closed', () => { + mockHooks({ open: false }); + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.instance.findByType('Collapsible')[0].props.title).not.toEqual(''); + }); +}); diff --git a/src/components/Rubric/index.jsx b/src/components/Rubric/index.jsx index 7d6630db..a508ed7b 100644 --- a/src/components/Rubric/index.jsx +++ b/src/components/Rubric/index.jsx @@ -3,8 +3,8 @@ import PropTypes from 'prop-types'; import { Card, StatefulButton } from '@edx/paragon'; import { useIntl } from '@edx/frontend-platform/i18n'; -import { StrictDict } from '@edx/react-unit-test-utils'; +import { MutationStatus } from 'data/services/lms/constants'; import CriterionContainer from './CriterionContainer'; import RubricFeedback from './RubricFeedback'; @@ -13,13 +13,6 @@ import messages from './messages'; import './Rubric.scss'; -export const ButtonStates = StrictDict({ - idle: 'idle', - loading: 'loading', - error: 'error', - success: 'success', -}); - /** * */ @@ -64,11 +57,11 @@ export const Rubric = ({ isGrading }) => {
diff --git a/src/components/TextResponse/RichTextEditor.jsx b/src/components/TextResponse/RichTextEditor.jsx index f8139ff1..beecfc4b 100644 --- a/src/components/TextResponse/RichTextEditor.jsx +++ b/src/components/TextResponse/RichTextEditor.jsx @@ -11,21 +11,16 @@ import 'tinymce/plugins/image'; import 'tinymce/themes/silver'; import 'tinymce/skins/ui/oxide/skin.min.css'; -import { StrictDict, useKeyedState } from '@edx/react-unit-test-utils'; import { useIntl } from '@edx/frontend-platform/i18n'; import messages from './messages'; -export const stateKeys = StrictDict({ - value: 'value', -}); - const RichTextEditor = ({ // id, - initialValue, + value, disabled, optional, + onChange, }) => { - const [value, setValue] = useKeyedState(stateKeys.value, initialValue); const { formatMessage } = useIntl(); const extraConfig = disabled ? { @@ -43,7 +38,7 @@ const RichTextEditor = ({ setValue(e.target.getContent())} + onEditorChange={onChange} disabled={disabled} />
@@ -63,25 +58,27 @@ const RichTextEditor = ({ RichTextEditor.defaultProps = { disabled: false, - initialValue: '', + value: '', optional: false, + onChange: () => { }, }; RichTextEditor.propTypes = { // id: PropTypes.string.isRequired, - input: PropTypes.shape({ - value: PropTypes.string, - name: PropTypes.string, - onChange: PropTypes.func.isRequired, - }).isRequired, - meta: PropTypes.shape({ - touched: PropTypes.bool, - submitFailed: PropTypes.bool, - error: PropTypes.string, - }).isRequired, + // input: PropTypes.shape({ + // value: PropTypes.string, + // name: PropTypes.string, + // onChange: PropTypes.func.isRequired, + // }).isRequired, + // meta: PropTypes.shape({ + // touched: PropTypes.bool, + // submitFailed: PropTypes.bool, + // error: PropTypes.string, + // }).isRequired, disabled: PropTypes.bool, - initialValue: PropTypes.string, + value: PropTypes.string, optional: PropTypes.bool, + onChange: PropTypes.func, }; export default RichTextEditor; diff --git a/src/components/TextResponse/RichTextEditor.test.jsx b/src/components/TextResponse/RichTextEditor.test.jsx new file mode 100644 index 00000000..01c82a57 --- /dev/null +++ b/src/components/TextResponse/RichTextEditor.test.jsx @@ -0,0 +1,45 @@ +import { shallow } from '@edx/react-unit-test-utils'; +import RichTextEditor from './RichTextEditor'; + +jest.mock('@tinymce/tinymce-react', () => ({ + Editor: () => 'Editor', +})); + +jest.mock('tinymce/tinymce.min', () => 'tinymce'); +jest.mock('tinymce/icons/default', () => 'default'); +jest.mock('tinymce/plugins/link', () => 'link'); +jest.mock('tinymce/plugins/lists', () => 'lists'); +jest.mock('tinymce/plugins/code', () => 'code'); +jest.mock('tinymce/plugins/image', () => 'image'); +jest.mock('tinymce/themes/silver', () => 'silver'); + +describe('', () => { + const props = { + optional: true, + disabled: false, + value: 'value', + onChange: jest.fn().mockName('onChange'), + }; + + it('render optional', () => { + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.instance.findByType('label')[0].el.children).toContain('Optional'); + expect(wrapper.instance.findByType('Editor')[0].props.init.readonly).not.toEqual(1); + }); + + it('render required', () => { + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.instance.findByType('label')[0].el.children).toContain('Required'); + }); + + it('render disabled', () => { + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.instance.findByType('Editor')[0].props.init.readonly).toEqual(1); + }); +}); diff --git a/src/components/TextResponse/TextEditor.jsx b/src/components/TextResponse/TextEditor.jsx index 6a0f613e..e9441aa4 100644 --- a/src/components/TextResponse/TextEditor.jsx +++ b/src/components/TextResponse/TextEditor.jsx @@ -1,23 +1,18 @@ -import React, { useState } from 'react'; +import React from 'react'; import PropTypes from 'prop-types'; import { TextArea } from '@edx/paragon'; -import { StrictDict } from '@edx/react-unit-test-utils'; import { useIntl } from '@edx/frontend-platform/i18n'; import messages from './messages'; -export const stateKeys = StrictDict({ - value: 'value', -}); - const TextEditor = ({ // id, - initialValue, + value, disabled, optional, + onChange, }) => { const { formatMessage } = useIntl(); - const [value, setValue] = useState(initialValue); return (