diff --git a/package.json b/package.json index 0417175c..937b97f5 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,8 @@ "lint:fix": "fedx-scripts eslint --fix --ext .js --ext .jsx .", "snapshot": "fedx-scripts jest --updateSnapshot", "start": "fedx-scripts webpack-dev-server --progress", - "test": "fedx-scripts jest --coverage --passWithNoTests" + "test": "fedx-scripts jest --coverage --passWithNoTests", + "test:debug": "node --inspect-brk node_modules/.bin/fedx-scripts jest --runInBand --watch" }, "husky": { "hooks": { diff --git a/src/components/Rubric/CriterionContainer/CriterionFeedback.jsx b/src/components/Rubric/CriterionContainer/CriterionFeedback.jsx index 98bee746..a9939b21 100644 --- a/src/components/Rubric/CriterionContainer/CriterionFeedback.jsx +++ b/src/components/Rubric/CriterionContainer/CriterionFeedback.jsx @@ -3,24 +3,24 @@ import PropTypes from 'prop-types'; import { Form } from '@edx/paragon'; import { useIntl } from '@edx/frontend-platform/i18n'; -import { StrictDict, useKeyedState } from '@edx/react-unit-test-utils'; import { feedbackRequirement } from 'data/services/lms/constants'; -import messages from './messages'; -export const stateKeys = StrictDict({ - value: 'value', -}); +import messages from './messages'; /** * */ -const CriterionFeedback = ({ - criterion, -}) => { - const [value, setValue] = useKeyedState(stateKeys.value, ''); +const CriterionFeedback = ({ criterion }) => { const { formatMessage } = useIntl(); + let commentMessage = formatMessage(messages.addComments); + if (criterion.feedbackRequired === feedbackRequirement.optional) { + commentMessage += ` ${formatMessage(messages.optional)}`; + } + + const { feedbackValue, feedbackIsInvalid, feedbackOnChange } = criterion; + if ( !criterion.feedbackEnabled || criterion.feedbackRequired === feedbackRequirement.disabled @@ -28,24 +28,16 @@ const CriterionFeedback = ({ return null; } - const onChange = ({ target }) => { setValue(target.value); }; - let commentMessage = formatMessage(messages.addComments); - if (criterion.feedbackRequired === feedbackRequirement.optional) { - commentMessage += ` ${formatMessage(messages.optional)}`; - } - - const isInvalid = value === ''; - return ( - + - {isInvalid && ( + {feedbackIsInvalid && ( {formatMessage(messages.criterionFeedbackError)} @@ -56,8 +48,11 @@ const CriterionFeedback = ({ CriterionFeedback.propTypes = { criterion: PropTypes.shape({ - feedbackEnabled: PropTypes.bool, - feedbackRequired: PropTypes.string, + feedbackValue: PropTypes.string.isRequired, + feedbackIsInvalid: PropTypes.bool.isRequired, + feedbackOnChange: PropTypes.func.isRequired, + feedbackEnabled: PropTypes.bool.isRequired, + feedbackRequired: PropTypes.oneOf(Object.values(feedbackRequirement)).isRequired, }).isRequired, }; diff --git a/src/components/Rubric/CriterionContainer/CriterionFeedback.test.jsx b/src/components/Rubric/CriterionContainer/CriterionFeedback.test.jsx new file mode 100644 index 00000000..290d0020 --- /dev/null +++ b/src/components/Rubric/CriterionContainer/CriterionFeedback.test.jsx @@ -0,0 +1,73 @@ +import React from 'react'; +import { shallow } from '@edx/react-unit-test-utils'; +import { feedbackRequirement } from 'data/services/lms/constants'; + +import CriterionFeedback from './CriterionFeedback'; + +describe('', () => { + const props = { + criterion: { + feedbackValue: 'feedback-1', + feedbackIsInvalid: false, + feedbackOnChange: jest.fn().mockName('feedbackOnChange'), + feedbackEnabled: true, + feedbackRequired: feedbackRequirement.required, + }, + }; + describe('renders', () => { + test('feedbackEnabled', () => { + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + }); + + test('feedbackDisabled render empty', () => { + const wrapper = shallow( + , + ); + expect(wrapper.snapshot).toMatchSnapshot(); + expect(wrapper.isEmptyRender()).toBe(true); + }); + + test('feedbackRequired disabled render empty', () => { + const wrapper = shallow( + , + ); + expect(wrapper.snapshot).toMatchSnapshot(); + expect(wrapper.isEmptyRender()).toBe(true); + }); + + test('feedbackRequired: optional', () => { + const wrapper = shallow( + , + ); + expect(wrapper.snapshot).toMatchSnapshot(); + expect(wrapper.instance.findByType('Form.Control')[0].props.floatingLabel).toContain('Optional'); + }); + + test('feedbackIsInvalid', () => { + const wrapper = shallow( + , + ); + expect(wrapper.snapshot).toMatchSnapshot(); + expect(wrapper.instance.findByType('Form.Control.Feedback')[0].props.type).toBe('invalid'); + }); + }); +}); diff --git a/src/components/Rubric/CriterionContainer/RadioCriterion.jsx b/src/components/Rubric/CriterionContainer/RadioCriterion.jsx index 0b1cf835..f8ab2830 100644 --- a/src/components/Rubric/CriterionContainer/RadioCriterion.jsx +++ b/src/components/Rubric/CriterionContainer/RadioCriterion.jsx @@ -3,44 +3,37 @@ import PropTypes from 'prop-types'; import { Form } from '@edx/paragon'; import { useIntl } from '@edx/frontend-platform/i18n'; -import { StrictDict, useKeyedState } from '@edx/react-unit-test-utils'; import messages from './messages'; -export const stateKeys = StrictDict({ - value: 'value', -}); - /** * */ -const RadioCriterion = ({ - isGrading, - criterion, -}) => { - const [value, setValue] = useKeyedState(stateKeys.value, ''); +const RadioCriterion = ({ isGrading, criterion }) => { const { formatMessage } = useIntl(); - const onChange = ({ target }) => { setValue(target.value); }; - const isInvalid = value === ''; + + const { optionsValue, optionsIsInvalid, optionsOnChange } = criterion; return ( - + {criterion.options.map((option) => ( {option.name} ))} - {isInvalid && ( - - {formatMessage(messages.rubricSelectedError)} - + {optionsIsInvalid && ( + + {formatMessage(messages.rubricSelectedError)} + )} ); @@ -49,12 +42,16 @@ const RadioCriterion = ({ RadioCriterion.propTypes = { isGrading: PropTypes.bool.isRequired, criterion: PropTypes.shape({ - name: PropTypes.string, - options: PropTypes.arrayOf(PropTypes.shape({ - name: PropTypes.string, - description: PropTypes.string, - points: PropTypes.number, - })), + optionsValue: PropTypes.string.isRequired, + optionsIsInvalid: PropTypes.bool.isRequired, + optionsOnChange: PropTypes.func.isRequired, + name: PropTypes.string.isRequired, + options: PropTypes.arrayOf( + PropTypes.shape({ + name: PropTypes.string.isRequired, + points: PropTypes.number.isRequired, + }), + ).isRequired, }).isRequired, }; diff --git a/src/components/Rubric/CriterionContainer/RadioCriterion.test.jsx b/src/components/Rubric/CriterionContainer/RadioCriterion.test.jsx new file mode 100644 index 00000000..51ba64b6 --- /dev/null +++ b/src/components/Rubric/CriterionContainer/RadioCriterion.test.jsx @@ -0,0 +1,67 @@ +import React from 'react'; +import { shallow } from '@edx/react-unit-test-utils'; + +import RadioCriterion from './RadioCriterion'; + +describe('', () => { + const props = { + isGrading: true, + criterion: { + name: 'criterion-1', + optionsValue: 'option-1', + optionsIsInvalid: true, + optionsOnChange: jest.fn().mockName('optionsOnChange'), + options: [ + { + name: 'option-1', + description: 'description-1', + points: 1, + }, + { + name: 'option-2', + description: 'description-2', + points: 2, + }, + ], + }, + }; + describe('renders', () => { + test('options is invalid', () => { + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.instance.findByType('Form.Radio').length).toEqual( + props.criterion.options.length, + ); + wrapper.instance.findByType('Form.Radio').forEach((radio) => { + expect(radio.props.disabled).toEqual(false); + }); + expect( + wrapper.instance.findByType('Form.Control.Feedback')[0].props.type, + ).toEqual('invalid'); + }); + + test('options is valid no invalid feedback get render', () => { + const wrapper = shallow( + , + ); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect( + wrapper.instance.findByType('Form.Control.Feedback').length, + ).toEqual(0); + }); + + test('not isGrading all radios will be disabled', () => { + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + wrapper.instance.findByType('Form.Radio').forEach((radio) => { + expect(radio.props.disabled).toEqual(true); + }); + }); + }); +}); diff --git a/src/components/Rubric/CriterionContainer/ReviewCriterion.jsx b/src/components/Rubric/CriterionContainer/ReviewCriterion.jsx index cd1f8fe8..e2ade3c9 100644 --- a/src/components/Rubric/CriterionContainer/ReviewCriterion.jsx +++ b/src/components/Rubric/CriterionContainer/ReviewCriterion.jsx @@ -28,11 +28,10 @@ ReviewCriterion.propTypes = { criterion: PropTypes.shape({ options: PropTypes.arrayOf( PropTypes.shape({ - description: PropTypes.string, - name: PropTypes.string, - points: PropTypes.number, + name: PropTypes.string.isRequired, + points: PropTypes.number.isRequired, }), - ), + ).isRequired, }).isRequired, }; diff --git a/src/components/Rubric/CriterionContainer/ReviewCriterion.test.jsx b/src/components/Rubric/CriterionContainer/ReviewCriterion.test.jsx new file mode 100644 index 00000000..3c24026d --- /dev/null +++ b/src/components/Rubric/CriterionContainer/ReviewCriterion.test.jsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { shallow } from '@edx/react-unit-test-utils'; + +import ReviewCriterion from './ReviewCriterion'; + +describe('', () => { + const props = { + criterion: { + options: [ + { + name: 'option-1', + description: 'description-1', + points: 1, + }, + { + name: 'option-2', + description: 'description-2', + points: 2, + }, + ], + }, + }; + + test('renders', () => { + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + expect(wrapper.instance.findByType('FormControlFeedback').length).toEqual(props.criterion.options.length); + }); +}); diff --git a/src/components/Rubric/CriterionContainer/__snapshots__/CriterionFeedback.test.jsx.snap b/src/components/Rubric/CriterionContainer/__snapshots__/CriterionFeedback.test.jsx.snap new file mode 100644 index 00000000..4634ed11 --- /dev/null +++ b/src/components/Rubric/CriterionContainer/__snapshots__/CriterionFeedback.test.jsx.snap @@ -0,0 +1,53 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders feedbackDisabled render empty 1`] = `null`; + +exports[` renders feedbackEnabled 1`] = ` + + + +`; + +exports[` renders feedbackIsInvalid 1`] = ` + + + + The feedback is required + + +`; + +exports[` renders feedbackRequired disabled render empty 1`] = `null`; + +exports[` renders feedbackRequired: optional 1`] = ` + + + +`; diff --git a/src/components/Rubric/CriterionContainer/__snapshots__/RadioCriterion.test.jsx.snap b/src/components/Rubric/CriterionContainer/__snapshots__/RadioCriterion.test.jsx.snap new file mode 100644 index 00000000..d7d2f32f --- /dev/null +++ b/src/components/Rubric/CriterionContainer/__snapshots__/RadioCriterion.test.jsx.snap @@ -0,0 +1,97 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders not isGrading all radios will be disabled 1`] = ` + + + option-1 + + + option-2 + + + Rubric selection is required + + +`; + +exports[` renders options is invalid 1`] = ` + + + option-1 + + + option-2 + + + Rubric selection is required + + +`; + +exports[` renders options is valid no invalid feedback get render 1`] = ` + + + option-1 + + + option-2 + + +`; diff --git a/src/components/Rubric/CriterionContainer/__snapshots__/ReviewCriterion.test.jsx.snap b/src/components/Rubric/CriterionContainer/__snapshots__/ReviewCriterion.test.jsx.snap new file mode 100644 index 00000000..437f28d5 --- /dev/null +++ b/src/components/Rubric/CriterionContainer/__snapshots__/ReviewCriterion.test.jsx.snap @@ -0,0 +1,60 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders 1`] = ` +
+
+
+ + option-1 + + + + +
+
+
+
+ + option-2 + + + + +
+
+
+`; diff --git a/src/components/Rubric/CriterionContainer/__snapshots__/index.test.jsx.snap b/src/components/Rubric/CriterionContainer/__snapshots__/index.test.jsx.snap new file mode 100644 index 00000000..b3f52425 --- /dev/null +++ b/src/components/Rubric/CriterionContainer/__snapshots__/index.test.jsx.snap @@ -0,0 +1,158 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders is grading 1`] = ` + + + + criterion-1 + + +
+ description-1 +
+
+
+ + option-1 + +
+ description-1 +
+
+ + option-2 + +
+ description-2 +
+
+
+
+ +
+ +
+`; + +exports[` renders is not grading 1`] = ` + + + + criterion-1 + + +
+ description-1 +
+
+
+ + option-1 + +
+ description-1 +
+
+ + option-2 + +
+ description-2 +
+
+
+
+ +
+
+`; diff --git a/src/components/Rubric/CriterionContainer/index.jsx b/src/components/Rubric/CriterionContainer/index.jsx index 2f551405..8d2f3512 100644 --- a/src/components/Rubric/CriterionContainer/index.jsx +++ b/src/components/Rubric/CriterionContainer/index.jsx @@ -19,9 +19,7 @@ const CriterionContainer = ({ {criterion.name} -
- {criterion.description} -
+
{criterion.description}

{criterion.options.map((option) => (
@@ -46,14 +44,14 @@ const CriterionContainer = ({ CriterionContainer.propTypes = { isGrading: PropTypes.bool.isRequired, criterion: PropTypes.shape({ - name: PropTypes.string, - description: PropTypes.string, + name: PropTypes.string.isRequired, + description: PropTypes.string.isRequired, options: PropTypes.arrayOf( PropTypes.shape({ - description: PropTypes.string, - name: PropTypes.string, + name: PropTypes.string.isRequired, + description: PropTypes.string.isRequired, }), - ), + ).isRequired, }).isRequired, }; diff --git a/src/components/Rubric/CriterionContainer/index.test.jsx b/src/components/Rubric/CriterionContainer/index.test.jsx new file mode 100644 index 00000000..ccdc9d78 --- /dev/null +++ b/src/components/Rubric/CriterionContainer/index.test.jsx @@ -0,0 +1,49 @@ +import React from 'react'; +import { shallow } from '@edx/react-unit-test-utils'; + +import CriterionContainer from '.'; + +jest.mock('./RadioCriterion', () => 'RadioCriterion'); +jest.mock('./CriterionFeedback', () => 'CriterionFeedback'); +jest.mock('./ReviewCriterion', () => 'ReviewCriterion'); + +describe('', () => { + const props = { + isGrading: true, + criterion: { + name: 'criterion-1', + description: 'description-1', + options: [ + { + name: 'option-1', + description: 'description-1', + points: 1, + }, + { + name: 'option-2', + description: 'description-2', + points: 2, + }, + ], + }, + }; + describe('renders', () => { + test('is grading', () => { + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.instance.findByType('RadioCriterion')).toHaveLength(1); + expect(wrapper.instance.findByType('ReviewCriterion')).toHaveLength(0); + expect(wrapper.instance.findByType('CriterionFeedback')).toHaveLength(1); + }); + + test('is not grading', () => { + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.instance.findByType('RadioCriterion')).toHaveLength(0); + expect(wrapper.instance.findByType('ReviewCriterion')).toHaveLength(1); + expect(wrapper.instance.findByType('CriterionFeedback')).toHaveLength(0); + }); + }); +}); diff --git a/src/components/Rubric/RubricFeedback.jsx b/src/components/Rubric/RubricFeedback.jsx index 6d215df2..c6985a57 100644 --- a/src/components/Rubric/RubricFeedback.jsx +++ b/src/components/Rubric/RubricFeedback.jsx @@ -3,34 +3,25 @@ import PropTypes from 'prop-types'; import { Form } from '@edx/paragon'; import { useIntl } from '@edx/frontend-platform/i18n'; -import { StrictDict, useKeyedState } from '@edx/react-unit-test-utils'; - -import { useRubricConfig } from 'data/services/lms/hooks/selectors'; import InfoPopover from 'components/InfoPopover'; import messages from './messages'; -export const stateKeys = StrictDict({ - value: 'value', -}); - /** * */ const RubricFeedback = ({ - isGrading, + overallFeedbackPrompt, + overallFeedback, + overallFeedbackDisabled, + overallFeedbackIsInvalid, + onOverallFeedbackChange, }) => { - const [value, setValue] = useKeyedState(''); const { formatMessage } = useIntl(); - const feedbackPrompt = useRubricConfig().feedbackConfig.defaultText; - - const onChange = (event) => { - setValue(event.target.value); - }; const inputLabel = formatMessage( - isGrading ? messages.addComments : messages.comments, + !overallFeedbackDisabled ? messages.addComments : messages.comments, ); return ( @@ -40,32 +31,38 @@ const RubricFeedback = ({ {formatMessage(messages.overallComments)} -
{feedbackPrompt}
+
{overallFeedbackPrompt}
- { - /* - {isInvalid && ( - - - - )} - */ - } + {overallFeedbackIsInvalid && ( + + {formatMessage(messages.overallFeedbackError)} + + )} ); }; +RubricFeedback.defaultProps = { + overallFeedback: '', + overallFeedbackDisabled: false, + overallFeedbackIsInvalid: false, +}; + RubricFeedback.propTypes = { - isGrading: PropTypes.bool.isRequired, + overallFeedbackPrompt: PropTypes.string.isRequired, + overallFeedback: PropTypes.string, + overallFeedbackDisabled: PropTypes.bool, + onOverallFeedbackChange: PropTypes.func.isRequired, + overallFeedbackIsInvalid: PropTypes.bool, }; export default RubricFeedback; diff --git a/src/components/Rubric/RubricFeedback.test.jsx b/src/components/Rubric/RubricFeedback.test.jsx new file mode 100644 index 00000000..e1e491e1 --- /dev/null +++ b/src/components/Rubric/RubricFeedback.test.jsx @@ -0,0 +1,42 @@ +import React from 'react'; +import { shallow } from '@edx/react-unit-test-utils'; + +import RubricFeedback from './RubricFeedback'; +import messages from './messages'; + +describe('', () => { + const props = { + overallFeedbackPrompt: 'overallFeedbackPrompt', + overallFeedback: 'overallFeedback', + overallFeedbackDisabled: false, + overallFeedbackIsInvalid: false, + onOverallFeedbackChange: jest.fn().mockName('onOverallFeedbackChange'), + }; + + describe('renders', () => { + test('overall feedback is enabled', () => { + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.instance.findByType('Form.Control.Feedback').length).toBe(0); + expect(wrapper.instance.findByType('Form.Control')[0].props.disabled).toBe(false); + expect(wrapper.instance.findByType('Form.Control')[0].props.floatingLabel).toBe(messages.addComments.defaultMessage); + }); + + test('overall feedback is disabled', () => { + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.instance.findByType('Form.Control.Feedback').length).toBe(0); + expect(wrapper.instance.findByType('Form.Control')[0].props.disabled).toBe(true); + expect(wrapper.instance.findByType('Form.Control')[0].props.floatingLabel).toBe(messages.comments.defaultMessage); + }); + + test('overall feedback is invalid', () => { + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.instance.findByType('Form.Control.Feedback').length).toBe(1); + }); + }); +}); diff --git a/src/components/Rubric/__snapshots__/RubricFeedback.test.jsx.snap b/src/components/Rubric/__snapshots__/RubricFeedback.test.jsx.snap new file mode 100644 index 00000000..a5a655c2 --- /dev/null +++ b/src/components/Rubric/__snapshots__/RubricFeedback.test.jsx.snap @@ -0,0 +1,94 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders overall feedback is disabled 1`] = ` + + + + Overall comments + + +
+ overallFeedbackPrompt +
+
+
+ +
+`; + +exports[` renders overall feedback is enabled 1`] = ` + + + + Overall comments + + +
+ overallFeedbackPrompt +
+
+
+ +
+`; + +exports[` renders overall feedback is invalid 1`] = ` + + + + Overall comments + + +
+ overallFeedbackPrompt +
+
+
+ + + The overall feedback is required + +
+`; diff --git a/src/components/Rubric/__snapshots__/index.test.jsx.snap b/src/components/Rubric/__snapshots__/index.test.jsx.snap new file mode 100644 index 00000000..ad94dd11 --- /dev/null +++ b/src/components/Rubric/__snapshots__/index.test.jsx.snap @@ -0,0 +1,152 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders is grading 1`] = ` + + +

+ Rubric +

+
+ + +
+ +
+
+ +
+
+`; + +exports[` renders is not grading, no submit button or feedback get render 1`] = ` + + +

+ Rubric +

+
+ + +
+
+
+`; diff --git a/src/components/Rubric/hooks.js b/src/components/Rubric/hooks.js deleted file mode 100644 index 2d6b074e..00000000 --- a/src/components/Rubric/hooks.js +++ /dev/null @@ -1,32 +0,0 @@ -import { StrictDict } from 'utils'; -import { useRubricConfig } from 'data/services/lms/hooks/selectors'; - -export const ButtonStates = StrictDict({ - default: 'default', - pending: 'pending', - complete: 'complete', - error: 'error', -}); - -export const useRubricData = ({ - isGrading, -}) => { - const criteria = useRubricConfig().criteria.map( - (_, index) => ({ - isGrading, - key: index, - orderNum: index, - }), - ); - const submitButtonState = ButtonStates.default; - - return { - criteria, - showFooter: isGrading, - buttonProps: { - onClick: () => ({}), - state: submitButtonState, - disabledStates: [ButtonStates.pending, ButtonStates.complete], - }, - }; -}; diff --git a/src/components/Rubric/hooks.test.ts b/src/components/Rubric/hooks.test.ts new file mode 100644 index 00000000..e72e1e1d --- /dev/null +++ b/src/components/Rubric/hooks.test.ts @@ -0,0 +1,134 @@ +import { + usePageData, + useRubricConfig, +} from 'data/services/lms/hooks/selectors'; +import { mockUseKeyedState } from '@edx/react-unit-test-utils'; +import { submitRubric } from 'data/services/lms/hooks/actions'; +import { useRubricData, stateKeys } from './hooks'; +import { RubricData } from 'data/services/lms/types'; + +import { when } from 'jest-when'; + +jest.mock('data/services/lms/hooks/selectors', () => ({ + usePageData: jest.fn(), + useRubricConfig: jest.fn(), +})); + +jest.mock('data/services/lms/hooks/actions', () => ({ + submitRubric: jest.fn(), +})); + +const state = mockUseKeyedState(stateKeys); + +describe('useRubricData', () => { + const mutateFn = jest.fn(); + + const mockRubricData: RubricData = { + optionsSelected: { + 'criterion-1': 'option-1', + 'criterion-2': 'option-2', + }, + criterionFeedback: { + 'criterion-1': 'feedback-1', + 'criterion-2': 'feedback-2', + }, + overallFeedback: 'overall-feedback', + }; + + when(usePageData).mockReturnValue({ + rubric: mockRubricData, + }); + + when(useRubricConfig).mockReturnValue({ + criteria: [ + { + name: 'criterion-1', + options: [ + { label: 'Option 1', value: 'option-1' }, + { label: 'Option 2', value: 'option-2' }, + ], + }, + { + name: 'criterion-2', + options: [ + { label: 'Option 1', value: 'option-1' }, + { label: 'Option 2', value: 'option-2' }, + ], + }, + ], + feedbackConfig: { + enabled: true, + }, + } as any); + + when(submitRubric).mockReturnValue({ + mutate: mutateFn, + } as any); + + describe('state keys', () => { + beforeEach(() => { + state.mock(); + }); + afterEach(() => { + state.resetVals(); + }); + + it('initializes state values from page data', () => { + useRubricData({ isGrading: true }); + state.expectInitializedWith(stateKeys.rubric, mockRubricData); + state.expectInitializedWith( + stateKeys.overallFeedback, + mockRubricData.overallFeedback + ); + }); + it('returns the correct getter/setter for state', () => { + const out = useRubricData({ isGrading: true }); + expect(out.rubricData).toEqual(mockRubricData); + + out.setRubricData('foo' as any); + expect(state.values[stateKeys.rubric]).toEqual('foo'); + expect(out.overallFeedback).toEqual(mockRubricData.overallFeedback); + out.onOverallFeedbackChange({ + target: { + value: 'bar', + }, + } as any); + expect(state.values[stateKeys.overallFeedback]).toEqual('bar'); + }); + }); + + it('should return the correct data', () => { + const { rubricData, criteria } = useRubricData({ isGrading: true }); + + expect(rubricData).toEqual(mockRubricData); + + expect(criteria).toEqual([ + { + name: 'criterion-1', + options: [ + { label: 'Option 1', value: 'option-1' }, + { label: 'Option 2', value: 'option-2' }, + ], + optionsValue: 'option-1', + optionsIsInvalid: false, + optionsOnChange: expect.any(Function), + feedbackValue: 'feedback-1', + feedbackIsInvalid: false, + feedbackOnChange: expect.any(Function), + }, + { + name: 'criterion-2', + options: [ + { label: 'Option 1', value: 'option-1' }, + { label: 'Option 2', value: 'option-2' }, + ], + optionsValue: 'option-2', + optionsIsInvalid: false, + optionsOnChange: expect.any(Function), + feedbackValue: 'feedback-2', + feedbackIsInvalid: false, + feedbackOnChange: expect.any(Function), + }, + ]); + }); +}); diff --git a/src/components/Rubric/hooks.ts b/src/components/Rubric/hooks.ts new file mode 100644 index 00000000..e578f18b --- /dev/null +++ b/src/components/Rubric/hooks.ts @@ -0,0 +1,73 @@ +import { useKeyedState, StrictDict } from '@edx/react-unit-test-utils'; +import { usePageData, useRubricConfig } from 'data/services/lms/hooks/selectors'; +import { submitRubric } from 'data/services/lms/hooks/actions'; +import { RubricHookData } from './types'; + +export const stateKeys = StrictDict({ + rubric: 'rubric', + overallFeedback: 'overallFeedback', +}); + +export const useRubricData = ({ isGrading }): RubricHookData => { + const data = usePageData(); + const { criteria, feedbackConfig } = useRubricConfig(); + + const [rubricData, setRubricData] = useKeyedState(stateKeys.rubric, data.rubric); + const [overallFeedback, setOverallFeedback] = useKeyedState( + stateKeys.overallFeedback, data.rubric.overallFeedback + ); + const submitRubricMutation = submitRubric(); + + const onOverallFeedbackChange = (e: React.ChangeEvent) => + setOverallFeedback(e.target.value); + + const onSubmit = () => { + submitRubricMutation.mutate({ + overallFeedback, + optionsSelected: rubricData.optionsSelected, + criterionFeedback: rubricData.criterionFeedback, + } as any); + }; + + return { + rubricData, + setRubricData, + criteria: criteria.map((criterion) => ({ + ...criterion, + optionsValue: rubricData.optionsSelected[criterion.name], + optionsIsInvalid: false, + optionsOnChange: (e: React.ChangeEvent) => { + setRubricData({ + ...rubricData, + optionsSelected: { + ...rubricData.optionsSelected, + [criterion.name]: e.target.value, + }, + }); + }, + + feedbackValue: rubricData.criterionFeedback[criterion.name], + feedbackIsInvalid: false, + feedbackOnChange: (e: React.ChangeEvent) => { + setRubricData({ + ...rubricData, + criterionFeedback: { + ...rubricData.criterionFeedback, + [criterion.name]: e.target.value, + }, + }); + }, + })), + onSubmit, + submitStatus: submitRubricMutation.status, + + // overall feedback + overallFeedback, + onOverallFeedbackChange, + overallFeedbackDisabled: !isGrading, + overallFeedbackIsInvalid: false, + overallFeedbackPrompt: feedbackConfig.defaultText, + }; +}; + +export default useRubricData; diff --git a/src/components/Rubric/index.jsx b/src/components/Rubric/index.jsx index 56f9551f..7d6630db 100644 --- a/src/components/Rubric/index.jsx +++ b/src/components/Rubric/index.jsx @@ -5,49 +5,70 @@ import { Card, StatefulButton } from '@edx/paragon'; import { useIntl } from '@edx/frontend-platform/i18n'; import { StrictDict } from '@edx/react-unit-test-utils'; -import { useRubricConfig } from 'data/services/lms/hooks/selectors'; - import CriterionContainer from './CriterionContainer'; import RubricFeedback from './RubricFeedback'; +import { useRubricData } from './hooks'; import messages from './messages'; import './Rubric.scss'; export const ButtonStates = StrictDict({ - default: 'default', - pending: 'pending', - complete: 'complete', + idle: 'idle', + loading: 'loading', error: 'error', + success: 'success', }); /** * */ export const Rubric = ({ isGrading }) => { - const { criteria } = useRubricConfig(); + const { + criteria, + onSubmit, + overallFeedbackPrompt, + overallFeedback, + overallFeedbackDisabled, + onOverallFeedbackChange, + submitStatus, + } = useRubricData({ + isGrading, + }); + const { formatMessage } = useIntl(); return (

{formatMessage(messages.rubric)}


- {criteria.map(criterion => ( - + {criteria.map((criterion) => ( + ))}
- {isGrading && } + {isGrading && ( + + )}
{isGrading && (
({})} - state="default" - disabledStates={['pending', 'complete']} + onClick={onSubmit} + state={submitStatus} + disabledStates={['loading', 'success']} labels={{ - [ButtonStates.default]: formatMessage(messages.submitGrade), - [ButtonStates.pending]: formatMessage(messages.submittingGrade), - [ButtonStates.complete]: formatMessage(messages.gradeSubmitted), + [ButtonStates.idle]: formatMessage(messages.submitGrade), + [ButtonStates.loading]: formatMessage(messages.submittingGrade), + [ButtonStates.success]: formatMessage(messages.gradeSubmitted), }} />
@@ -55,6 +76,7 @@ export const Rubric = ({ isGrading }) => {
); }; + Rubric.propTypes = { isGrading: PropTypes.bool.isRequired, }; diff --git a/src/components/Rubric/index.test.jsx b/src/components/Rubric/index.test.jsx new file mode 100644 index 00000000..259515ad --- /dev/null +++ b/src/components/Rubric/index.test.jsx @@ -0,0 +1,87 @@ +import React from 'react'; +import { shallow } from '@edx/react-unit-test-utils'; +import { when } from 'jest-when'; + +import { useRubricData } from './hooks'; +import { Rubric } from '.'; + +jest.mock('./RubricFeedback', () => 'RubricFeedback'); +jest.mock('./CriterionContainer', () => 'CriterionContainer'); + +jest.mock('./hooks', () => ({ + useRubricData: jest.fn(), +})); + +describe('', () => { + const mockRubricDataResponse = { + criteria: [ + { + name: 'criterion-1', + optionsValue: 'option-1', + optionsIsInvalid: false, + optionsOnChange: jest.fn().mockName('optionsOnChange'), + options: [ + { + name: 'option-1', + points: 1, + }, + { + name: 'option-2', + points: 2, + }, + ], + }, + { + name: 'criterion-2', + optionsValue: 'option-1', + optionsIsInvalid: false, + optionsOnChange: jest.fn().mockName('optionsOnChange'), + options: [ + { + name: 'option-1', + points: 1, + }, + { + name: 'option-2', + points: 2, + }, + ], + }, + ], + onSubmit: jest.fn().mockName('onSubmit'), + overallFeedbackPrompt: 'overallFeedbackPrompt', + overallFeedback: 'overallFeedback', + overallFeedbackDisabled: true, + onOverallFeedbackChange: jest.fn().mockName('onOverallFeedbackChange'), + submitStatus: 'idle', + }; + + when(useRubricData).mockReturnValue(mockRubricDataResponse); + + describe('renders', () => { + test('is grading', () => { + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + }); + test('is not grading, no submit button or feedback get render', () => { + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.instance.findByType('RubricFeedback').length).toBe(0); + expect(wrapper.instance.findByType('StatefulButton').length).toBe(0); + }); + }); + + describe('behavior', () => { + const wrapper = shallow(); + it('has CriterionContainer equal to the number of criteria', () => { + expect(wrapper.instance.findByType('CriterionContainer').length).toBe(mockRubricDataResponse.criteria.length); + }); + + test('StatefulButton onClick calls onSubmit', () => { + expect(mockRubricDataResponse.onSubmit).not.toHaveBeenCalled(); + wrapper.instance.findByType('StatefulButton')[0].props.onClick(); + expect(mockRubricDataResponse.onSubmit).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/components/Rubric/types.ts b/src/components/Rubric/types.ts new file mode 100644 index 00000000..48bf3aa5 --- /dev/null +++ b/src/components/Rubric/types.ts @@ -0,0 +1,26 @@ +import { CriterionConfig, MutationStatus, RubricData } from "data/services/lms/types"; + +export type Criterion = { + optionsValue: string | null; + optionsIsInvalid: boolean; + optionsOnChange: (e: React.ChangeEvent) => void; + + feedbackValue: string | null; + feedbackIsInvalid: boolean; + feedbackOnChange: (e: React.ChangeEvent) => void; +} & CriterionConfig; + +export type RubricHookData = { + rubricData: RubricData; + setRubricData: (data: RubricData) => void; + criteria: Criterion[]; + onSubmit: () => void; + submitStatus: MutationStatus; + + // overall feedback + overallFeedback: string; + onOverallFeedbackChange: (e: React.ChangeEvent) => void; + overallFeedbackDisabled: boolean; + overallFeedbackIsInvalid: boolean; + overallFeedbackPrompt: string; +}; \ No newline at end of file diff --git a/src/data/services/lms/fakeData/oraConfig.js b/src/data/services/lms/fakeData/oraConfig.js index 18a30877..8c6d4c8a 100644 --- a/src/data/services/lms/fakeData/oraConfig.js +++ b/src/data/services/lms/fakeData/oraConfig.js @@ -52,22 +52,22 @@ const rubricConfig = { criteria: [ genCriterion({ index: 0, - config: { feedback_enabled: true, feedback_required: true }, + config: { feedback_enabled: true, feedback_required: 'required' }, optionPointsArray: [0, 2, 5, 7], }), genCriterion({ index: 1, - config: { feedback_enabled: true, feedback_required: true }, + config: { feedback_enabled: true, feedback_required: 'disabled' }, optionPointsArray: [0, 2, 5, 7], }), genCriterion({ index: 2, - config: { feedback_enabled: true, feedback_required: true }, + config: { feedback_enabled: true, feedback_required: 'optional' }, optionPointsArray: [0, 2, 5, 7], }), genCriterion({ index: 3, - config: { feedback_enabled: true, feedback_required: true }, + config: { feedback_enabled: true, feedback_required: 'optional' }, optionPointsArray: [0, 2, 5, 7], }), ], diff --git a/src/data/services/lms/fakeData/pageData/index.jsx b/src/data/services/lms/fakeData/pageData/index.jsx index 3afe557c..b3ac5afe 100644 --- a/src/data/services/lms/fakeData/pageData/index.jsx +++ b/src/data/services/lms/fakeData/pageData/index.jsx @@ -10,7 +10,7 @@ export const emptySubmission = { export const peerAssessment = { progress: progressStates.peer(), - rubric: rubricStates.criteriaFeedbackEnabled.empty, + rubric: rubricStates.criteriaFeedbackEnabled.filled, submission: submissionStates.individialSubmission, }; diff --git a/src/data/services/lms/hooks/actions.test.ts b/src/data/services/lms/hooks/actions.test.ts new file mode 100644 index 00000000..e28748d8 --- /dev/null +++ b/src/data/services/lms/hooks/actions.test.ts @@ -0,0 +1,30 @@ +import { useQueryClient, useMutation } from '@tanstack/react-query'; +import { when } from 'jest-when'; + +import { createMutationAction } from './actions'; + +jest.mock('@tanstack/react-query', () => ({ + useQueryClient: jest.fn(), + useMutation: jest.fn(), +})); + +describe('actions', () => { + const queryClient = { setQueryData: jest.fn() }; + + when(useQueryClient).mockReturnValue(queryClient); + when(useMutation).mockImplementation(({ mutationFn }) => { + return { + mutate: mutationFn, + }; + }); + + describe('createMutationAction', () => { + it('returns a mutation function', () => { + const aribtraryMutationFn = jest.fn(); + const mutation = createMutationAction(aribtraryMutationFn) as any; + + mutation.mutate('foo', 'bar'); + expect(aribtraryMutationFn).toHaveBeenCalledWith('foo', 'bar', queryClient); + }); + }); +}); diff --git a/src/data/services/lms/hooks/actions.ts b/src/data/services/lms/hooks/actions.ts new file mode 100644 index 00000000..1645180b --- /dev/null +++ b/src/data/services/lms/hooks/actions.ts @@ -0,0 +1,30 @@ +import { useQueryClient, useMutation } from '@tanstack/react-query'; +import { queryKeys } from '../constants'; +import { ActionMutationFunction, RubricData } from '../types'; + +import fakeData from '../fakeData'; + +export const createMutationAction = (mutationFn: ActionMutationFunction) => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (...args) => mutationFn(...args, queryClient), + }); +}; + +export const submitRubric = () => + createMutationAction(async (data: RubricData, queryClient) => { + // TODO: submit rubric + await new Promise((resolve) => setTimeout(() => { + fakeData.pageData.shapes.peerAssessment.rubric = { + criterion_feedback: data.criterionFeedback, + options_selected: data.optionsSelected, + overall_feedback: data.overallFeedback, + } as any; + resolve(null); + }, 1000)); + + queryClient.invalidateQueries([queryKeys.pageData, true]) + + return Promise.resolve(data); + }); \ No newline at end of file diff --git a/src/data/services/lms/hooks/api.test.ts b/src/data/services/lms/hooks/data.test.ts similarity index 86% rename from src/data/services/lms/hooks/api.test.ts rename to src/data/services/lms/hooks/data.test.ts index eab9f214..07f1ab2b 100644 --- a/src/data/services/lms/hooks/api.test.ts +++ b/src/data/services/lms/hooks/data.test.ts @@ -8,7 +8,7 @@ import * as types from '../types'; import { queryKeys } from '../constants'; import fakeData from '../fakeData'; -import { useORAConfig, usePageData } from './api'; +import { useORAConfig, usePageData } from './data'; jest.mock('@tanstack/react-query', () => ({ useQuery: jest.fn() })); @@ -26,7 +26,7 @@ interface MockUsePageDataQuery { (QueryArgs): MockPageDataQuery } interface MockPageDataUseConfigHook { (): MockPageDataQuery } let out; -describe('lms api hooks', () => { +describe('lms data hooks', () => { describe('useORAConfig', () => { const mockUseQuery = (hasData: boolean): MockUseORAQuery => ({ queryKey, queryFn }) => ({ data: hasData ? camelCaseObject(fakeData.oraConfig.assessmentText) : undefined, @@ -74,8 +74,16 @@ describe('lms api hooks', () => { }); }); describe('usePageData', () => { + const pageDataCamelCase = (data: any) => ({ + ...data, + rubric: { + optionsSelected: {...data.rubric.options_selected}, + criterionFeedback: {...data.rubric.criterion_feedback}, + overallFeedback: data.rubric.overall_feedback, + }, + }); const mockUseQuery = (data?: types.PageData): MockUsePageDataQuery => ({ queryKey, queryFn }) => ({ - data: data ? camelCaseObject(data) : undefined, + data: data ? pageDataCamelCase(data) : {}, queryKey, queryFn, }); @@ -104,10 +112,10 @@ describe('lms api hooks', () => { }); it('initializes query with promise pointing to empty submission page data', async () => { const response = await out.queryFn(); - expect(response).toEqual(fakeData.pageData.shapes.emptySubmission); + expect(response).toEqual(pageDataCamelCase(fakeData.pageData.shapes.emptySubmission)); }); it('returns camelCase object from data if data has been returned', () => { - expect(out.data).toEqual(camelCaseObject(fakeData.pageData.shapes.emptySubmission)); + expect(out.data).toEqual(pageDataCamelCase(fakeData.pageData.shapes.emptySubmission)); }); }); describe('assessment', () => { @@ -121,10 +129,10 @@ describe('lms api hooks', () => { }); it('initializes query with promise pointing to peer assessment page data', async () => { const response = await out.queryFn(); - expect(response).toEqual(fakeData.pageData.shapes.peerAssessment); + expect(response).toEqual(pageDataCamelCase(fakeData.pageData.shapes.peerAssessment)); }); it('returns camelCase object from data if data has been returned', () => { - expect(out.data).toEqual(camelCaseObject(fakeData.pageData.shapes.peerAssessment)); + expect(out.data).toEqual(pageDataCamelCase(fakeData.pageData.shapes.peerAssessment)); }); }); it('returns empty object from data if data has not been returned', () => { diff --git a/src/data/services/lms/hooks/api.ts b/src/data/services/lms/hooks/data.ts similarity index 59% rename from src/data/services/lms/hooks/api.ts rename to src/data/services/lms/hooks/data.ts index 1926c403..c6cc9171 100644 --- a/src/data/services/lms/hooks/api.ts +++ b/src/data/services/lms/hooks/data.ts @@ -13,7 +13,9 @@ export const useORAConfig = (): types.QueryData => { queryKey: [queryKeys.oraConfig], // queryFn: () => getAuthenticatedClient().get(...), queryFn: () => { - const result = window.location.pathname.endsWith('text') ? fakeData.oraConfig.assessmentText : fakeData.oraConfig.assessmentTinyMCE; + const result = window.location.pathname.endsWith('text') + ? fakeData.oraConfig.assessmentText + : fakeData.oraConfig.assessmentTinyMCE; return Promise.resolve(result); }, }); @@ -26,17 +28,29 @@ export const useORAConfig = (): types.QueryData => { export const usePageData = (): types.QueryData => { const route = useMatch(routes.peerAssessment); const isAssessment = !!route && route.pattern.path === routes.peerAssessment; - const returnData = isAssessment - ? fakeData.pageData.shapes.peerAssessment - : fakeData.pageData.shapes.emptySubmission; const { data, ...status } = useQuery({ queryKey: [queryKeys.pageData, isAssessment], // queryFn: () => getAuthenticatedClient().get(...), - queryFn: () => Promise.resolve(returnData), + queryFn: () => { + const assessmentData = isAssessment + ? fakeData.pageData.shapes.peerAssessment + : fakeData.pageData.shapes.emptySubmission; + + const returnData = assessmentData ? { + ...assessmentData, + rubric: { + optionsSelected: {...assessmentData.rubric.options_selected}, + criterionFeedback: {...assessmentData.rubric.criterion_feedback}, + overallFeedback: assessmentData.rubric.overall_feedback, + }, + }: {}; + + return Promise.resolve(returnData as any); + }, }); return { ...status, - data: data ? camelCaseObject(data) : {}, + data, }; }; diff --git a/src/data/services/lms/hooks/selectors.test.ts b/src/data/services/lms/hooks/selectors.test.ts index faa4f1b6..0a9bb0f6 100644 --- a/src/data/services/lms/hooks/selectors.test.ts +++ b/src/data/services/lms/hooks/selectors.test.ts @@ -1,7 +1,7 @@ import { when } from 'jest-when'; import { keyStore } from '@edx/react-unit-test-utils'; -import * as api from './api'; +import * as data from './data'; import * as selectors from './selectors'; const statusData = { @@ -14,7 +14,7 @@ const statusData = { const testValue = 'some-test-data'; -const apiKeys = keyStore(api); +const dataKeys = keyStore(data); const selectorKeys = keyStore(selectors); const mockHook = (module, key, returnValue) => { @@ -22,9 +22,9 @@ const mockHook = (module, key, returnValue) => { when(spy).calledWith().mockReturnValueOnce(returnValue); }; -describe('lms api selector hooks', () => { +describe('lms data selector hooks', () => { const mockORAConfig = (returnValue) => { - mockHook(api, apiKeys.useORAConfig, returnValue); + mockHook(data, dataKeys.useORAConfig, returnValue); }; const mockORAData = (returnValue) => { mockHook(selectors, selectorKeys.useORAConfigData, returnValue); @@ -83,7 +83,7 @@ describe('lms api selector hooks', () => { }); describe('Page Data selectors', () => { const mockPageDataQuery = (returnValue) => { - mockHook(api, apiKeys.usePageData, returnValue); + mockHook(data, dataKeys.usePageData, returnValue); }; const mockPageData = (returnValue) => { mockHook(selectors, selectorKeys.usePageData, returnValue); diff --git a/src/data/services/lms/hooks/selectors.ts b/src/data/services/lms/hooks/selectors.ts index f1ef82d0..99272791 100644 --- a/src/data/services/lms/hooks/selectors.ts +++ b/src/data/services/lms/hooks/selectors.ts @@ -1,9 +1,9 @@ -import * as api from './api'; +import * as data from './data'; import * as types from '../types'; export const useORAConfigDataStatus = (): types.QueryStatus => { - const queryStatus = api.useORAConfig(); + const queryStatus = data.useORAConfig(); return { isLoading: queryStatus.isLoading, isFetching: queryStatus.isFetching, @@ -14,11 +14,11 @@ export const useORAConfigDataStatus = (): types.QueryStatus => { }; export const useIsORAConfigLoaded = (): boolean => ( - api.useORAConfig().status === 'success' + data.useORAConfig().status === 'success' ); export const useORAConfigData = (): types.ORAConfig => ( - api.useORAConfig().data + data.useORAConfig().data ); export const useSubmissionConfig = (): types.SubmissionConfig => ( @@ -34,7 +34,7 @@ export const useRubricConfig = (): types.RubricConfig => useORAConfigData().rubr export const useLeaderboardConfig = (): types.LeaderboardConfig => useORAConfigData().leaderboardConfig; export const usePageDataStatus = () => { - const queryStatus = api.usePageData(); + const queryStatus = data.usePageData(); return { isLoading: queryStatus.isLoading, isFetching: queryStatus.isFetching, @@ -44,10 +44,10 @@ export const usePageDataStatus = () => { }; }; export const useIsPageDataLoaded = (): boolean => ( - api.usePageData().status === 'success' + data.usePageData().status === 'success' ); -export const usePageData = (): types.PageData => api.usePageData().data; +export const usePageData = (): types.PageData => data.usePageData().data; export const useSubmissionTeamInfo = (): types.SubmissionTeamData => usePageData().submission.teamInfo; diff --git a/src/data/services/lms/index.js b/src/data/services/lms/index.js index 08e4502d..98d7d489 100644 --- a/src/data/services/lms/index.js +++ b/src/data/services/lms/index.js @@ -1,10 +1,12 @@ import { StrictDict } from '@edx/react-unit-test-utils'; import urls from './urls'; -import api from './hooks/api'; +import data from './hooks/data'; import selectors from './hooks/selectors'; +import actions from './hooks/actions'; export default StrictDict({ - api, + data, selectors, urls, + actions, }); diff --git a/src/data/services/lms/types/react-query.ts b/src/data/services/lms/types/react-query.ts index 400e0fc7..20fc8ee4 100644 --- a/src/data/services/lms/types/react-query.ts +++ b/src/data/services/lms/types/react-query.ts @@ -1,3 +1,5 @@ +import { QueryClient } from "@tanstack/query-core"; + // React-Query fields export interface QueryStatus { isLoading: boolean, @@ -14,3 +16,7 @@ export interface QueryData extends QueryStatus { error: unknown, data: Response, } + +export type MutationStatus = 'idle' | 'loading' | 'error' | 'success' + +export type ActionMutationFunction = (args: any, queryClient: QueryClient) => Promise diff --git a/src/setupTest.js b/src/setupTest.js index 34bd72ce..00e08236 100644 --- a/src/setupTest.js +++ b/src/setupTest.js @@ -1,2 +1,76 @@ import 'core-js/stable'; import 'regenerator-runtime/runtime'; + +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'); + return { + ...i18n, + useIntl: () => ({ + formatMessage, + formatDate, + }), + defineMessages: m => m, + FormattedMessage: () => 'FormattedMessage', + }; +}); + +jest.mock('@edx/paragon', () => jest.requireActual('@edx/react-unit-test-utils').mockComponents({ + Alert: { + Heading: 'Alert.Heading', + }, + AlertModal: 'AlertModal', + ActionRow: 'ActionRow', + Badge: 'Badge', + Button: 'Button', + Card: { + Body: 'Card.Body', + Section: 'Card.Section', + Footer: 'Card.Footer', + }, + Col: 'Col', + Collapsible: { + Advanced: 'Collapsible.Advanced', + Body: 'Collapsible.Body', + Trigger: 'Collapsible.Trigger', + Visible: 'Collapsible.Visible', + }, + Container: 'Container', + DataTable: { + EmptyTable: 'DataTable.EmptyTable', + Table: 'DataTable.Table', + TableControlBar: 'DataTable.TableControlBar', + TableController: 'DataTable.TableController', + TableFooter: 'DataTable.TableFooter', + }, + Dropdown: { + Item: 'Dropdown.Item', + Menu: 'Dropdown.Menu', + Toggle: 'Dropdown.Toggle', + }, + Form: { + Control: { + Feedback: 'Form.Control.Feedback', + }, + Group: 'Form.Group', + Label: 'Form.Label', + Radio: 'Form.Radio', + RadioSet: 'Form.RadioSet', + }, + FormControlFeedback: 'FormControlFeedback', + FullscreenModal: 'FullscreenModal', + Hyperlink: 'Hyperlink', + Icon: 'Icon', + IconButton: 'IconButton', + MultiSelectDropdownFilter: 'MultiSelectDropdownFilter', + OverlayTrigger: 'OverlayTrigger', + PageBanner: 'PageBanner', + Popover: { + Content: 'Popover.Content', + }, + Row: 'Row', + StatefulButton: 'StatefulButton', + TextFilter: 'TextFilter', + Spinner: 'Spinner', +})); diff --git a/src/views/PeerAssessmentView/AssessmentContentLayout.jsx b/src/views/PeerAssessmentView/AssessmentContentLayout.jsx index 55f600f5..80f0fc8f 100644 --- a/src/views/PeerAssessmentView/AssessmentContentLayout.jsx +++ b/src/views/PeerAssessmentView/AssessmentContentLayout.jsx @@ -18,7 +18,7 @@ const AssessmentContentLayout = () => { - {showRubric && ()} + {showRubric && ()}
diff --git a/tsconfig.json b/tsconfig.json index f168de28..173f94f6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "@edx/typescript-config", "compilerOptions": { - "rootDir": ".", + "rootDir": "./src", "outDir": "dist", "baseUrl": "./src", "paths": {