Set Mastery Level Independent from Individual Proficiency Ratings
closes OUT-4921 flag=individual_outcome_rating_and_calculation Test plan: - Enable Improved Outcomes Management FF - Enable Individual Outcome Rating and Calculation FF - Disable Account Level Mastery Scales FF - Go to Account > Outcomes and select an outcome group - Click on Create button and in Create modal enter outcome name and delete the default rating with 0 points - Try to delete mastery points or enter a value that is below min rating or above max rating - Verify that relevant error messages appear below the mastery points input field and that Create button is disabled - Enter value for mastery points between min and max rating and verify that error msg disappears and Create button is enabled - Click on Create button, when outcome is created expand its description and verify that ratings & mastery points are correct - Verify that the ratings in expanded description look like the figma mock referenced in the ticket - Click on kebab menu -> Edit and verify that the ratings in Edit modal looks like the figma mock referenced in the ticket - Add a rating, change mastery points then click Save - Verify that the outcome is updated with correct data - Select a global outcome added to Account context and click on its kebab menu -> Edit - Verify that ratings are displayed but user does not have ability to change them - Go to Course > Outcomes, repeat the above tests and verify that results are the same Change-Id: I315a617c236b0db816c7711c08017283ede0ac81 Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/283610 Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com> Reviewed-by: Kyle Rosenbaum <krosenbaum@instructure.com> QA-Review: Marcus Pompeu <marcus.pompeu@instructure.com> Product-Review: Ben Friedman <ben.friedman@instructure.com>
This commit is contained in:
parent
ca31b6bd2a
commit
23d5f3593b
|
@ -44,8 +44,11 @@ import OutcomesRceField from './shared/OutcomesRceField'
|
|||
import ProficiencyCalculation, {
|
||||
defaultProficiencyCalculation
|
||||
} from './MasteryCalculation/ProficiencyCalculation'
|
||||
import useRatings, {defaultOutcomesManagementRatings} from '@canvas/outcomes/react/hooks/useRatings'
|
||||
import {convertRatings} from '@canvas/outcomes/react/helpers/ratingsHelpers'
|
||||
import useRatings, {
|
||||
defaultRatings,
|
||||
defaultMasteryPoints
|
||||
} from '@canvas/outcomes/react/hooks/useRatings'
|
||||
import {processRatingsAndMastery} from '@canvas/outcomes/react/helpers/ratingsHelpers'
|
||||
import Ratings from './Management/Ratings'
|
||||
|
||||
const CreateOutcomeModal = ({isOpen, onCloseHandler, onSuccess, starterGroupId}) => {
|
||||
|
@ -69,9 +72,15 @@ const CreateOutcomeModal = ({isOpen, onCloseHandler, onSuccess, starterGroupId})
|
|||
})
|
||||
const {
|
||||
ratings,
|
||||
masteryPoints,
|
||||
setRatings,
|
||||
setMasteryPoints,
|
||||
hasError: proficiencyRatingsError
|
||||
} = useRatings({initialRatings: defaultOutcomesManagementRatings})
|
||||
} = useRatings({
|
||||
initialRatings: defaultRatings,
|
||||
initialMasteryPoints: defaultMasteryPoints
|
||||
})
|
||||
|
||||
const [selectedGroup, setSelectedGroup] = useState(null)
|
||||
const [selectedGroupAncestorIds, setSelectedGroupAncestorIds] = useState([])
|
||||
const [proficiencyCalculation, setProficiencyCalculation] = useState(
|
||||
|
@ -133,8 +142,9 @@ const CreateOutcomeModal = ({isOpen, onCloseHandler, onSuccess, starterGroupId})
|
|||
if (individualOutcomeRatingAndCalculationFF) {
|
||||
input.calculationMethod = proficiencyCalculation.calculationMethod
|
||||
input.calculationInt = proficiencyCalculation.calculationInt
|
||||
const {masteryPoints, ratings: inputRatings} = convertRatings(ratings)
|
||||
input.masteryPoints = masteryPoints
|
||||
const {masteryPoints: inputMasteryPoints, ratings: inputRatings} =
|
||||
processRatingsAndMastery(ratings, masteryPoints.value)
|
||||
input.masteryPoints = inputMasteryPoints
|
||||
input.ratings = inputRatings
|
||||
}
|
||||
const createLearningOutcomeResult = await createLearningOutcome({
|
||||
|
@ -257,7 +267,13 @@ const CreateOutcomeModal = ({isOpen, onCloseHandler, onSuccess, starterGroupId})
|
|||
)}
|
||||
{individualOutcomeRatingAndCalculationFF && (
|
||||
<View as="div" padding="small 0 0">
|
||||
<Ratings ratings={ratings} onChangeRatings={setRatings} canManage />
|
||||
<Ratings
|
||||
ratings={ratings}
|
||||
onChangeRatings={setRatings}
|
||||
masteryPoints={masteryPoints}
|
||||
onChangeMasteryPoints={setMasteryPoints}
|
||||
canManage
|
||||
/>
|
||||
<View as="div" minHeight="14rem">
|
||||
<hr style={{margin: '1rem 0 0'}} />
|
||||
<ProficiencyCalculation
|
||||
|
|
|
@ -25,7 +25,7 @@ import {PresentationContent, ScreenReaderContent} from '@instructure/ui-a11y-con
|
|||
import {stripHtmlTags} from '@canvas/outcomes/stripHtmlTags'
|
||||
import useCanvasContext from '@canvas/outcomes/react/hooks/useCanvasContext'
|
||||
import ProficiencyCalculation from '../MasteryCalculation/ProficiencyCalculation'
|
||||
import {prepareRatings} from '@canvas/outcomes/react/helpers/ratingsHelpers'
|
||||
import {prepareRatings} from '@canvas/outcomes/react/hooks/useRatings'
|
||||
import Ratings from './Ratings'
|
||||
import {ratingsShape} from './shapes'
|
||||
|
||||
|
@ -114,7 +114,14 @@ const OutcomeDescription = ({
|
|||
|
||||
{!truncated && individualOutcomeRatingAndCalculationFF && (
|
||||
<View>
|
||||
<Ratings ratings={prepareRatings(ratings, masteryPoints)} canManage={false} />
|
||||
<Ratings
|
||||
ratings={prepareRatings(ratings)}
|
||||
masteryPoints={{
|
||||
value: masteryPoints,
|
||||
error: null
|
||||
}}
|
||||
canManage={false}
|
||||
/>
|
||||
<ProficiencyCalculation
|
||||
method={{calculationMethod, calculationInt}}
|
||||
individualOutcome="display"
|
||||
|
|
|
@ -41,7 +41,7 @@ import {useMutation} from 'react-apollo'
|
|||
import OutcomesRceField from '../shared/OutcomesRceField'
|
||||
import ProficiencyCalculation from '../MasteryCalculation/ProficiencyCalculation'
|
||||
import useRatings from '@canvas/outcomes/react/hooks/useRatings'
|
||||
import {convertRatings, prepareRatings} from '@canvas/outcomes/react/helpers/ratingsHelpers'
|
||||
import {processRatingsAndMastery} from '@canvas/outcomes/react/helpers/ratingsHelpers'
|
||||
import Ratings from './Ratings'
|
||||
import {outcomeEditShape} from './shapes'
|
||||
|
||||
|
@ -57,10 +57,15 @@ const OutcomeEditModal = ({outcome, isOpen, onCloseHandler, onEditLearningOutcom
|
|||
useCanvasContext()
|
||||
const {
|
||||
ratings,
|
||||
masteryPoints,
|
||||
setRatings,
|
||||
setMasteryPoints,
|
||||
hasError: proficiencyRatingsError,
|
||||
hasChanged: proficiencyRatingsChanged
|
||||
} = useRatings({initialRatings: prepareRatings(outcome.ratings, outcome.masteryPoints)})
|
||||
} = useRatings({
|
||||
initialRatings: outcome.ratings,
|
||||
initialMasteryPoints: outcome.masteryPoints
|
||||
})
|
||||
const [updateLearningOutcomeMutation] = useMutation(UPDATE_LEARNING_OUTCOME)
|
||||
const [setOutcomeFriendlyDescription] = useMutation(SET_OUTCOME_FRIENDLY_DESCRIPTION_MUTATION)
|
||||
let attributesEditable = {
|
||||
|
@ -127,8 +132,9 @@ const OutcomeEditModal = ({outcome, isOpen, onCloseHandler, onEditLearningOutcom
|
|||
input.calculationInt = calculationInt
|
||||
}
|
||||
if (proficiencyRatingsChanged) {
|
||||
const {masteryPoints, ratings: inputRatings} = convertRatings(ratings)
|
||||
input.masteryPoints = masteryPoints
|
||||
const {masteryPoints: inputMasteryPoints, ratings: inputRatings} =
|
||||
processRatingsAndMastery(ratings, masteryPoints.value)
|
||||
input.masteryPoints = inputMasteryPoints
|
||||
input.ratings = inputRatings
|
||||
}
|
||||
}
|
||||
|
@ -265,6 +271,8 @@ const OutcomeEditModal = ({outcome, isOpen, onCloseHandler, onEditLearningOutcom
|
|||
<View as="div" padding="small 0 0">
|
||||
<Ratings
|
||||
ratings={ratings}
|
||||
masteryPoints={masteryPoints}
|
||||
onChangeMasteryPoints={setMasteryPoints}
|
||||
onChangeRatings={setRatings}
|
||||
canManage={!!attributesEditable.individualRatings}
|
||||
/>
|
||||
|
|
|
@ -23,17 +23,27 @@ import {Flex} from '@instructure/ui-flex'
|
|||
import {IconPlusLine} from '@instructure/ui-icons'
|
||||
import I18n from 'i18n!OutcomeManagement'
|
||||
import {View} from '@instructure/ui-view'
|
||||
import {Text} from '@instructure/ui-text'
|
||||
import {TextInput} from '@instructure/ui-text-input'
|
||||
import {ScreenReaderContent} from '@instructure/ui-a11y-content'
|
||||
import {createRating} from '@canvas/outcomes/react/hooks/useRatings'
|
||||
import useCanvasContext from '@canvas/outcomes/react/hooks/useCanvasContext'
|
||||
import ProficiencyRating from '../MasteryScale/ProficiencyRating'
|
||||
|
||||
const ratingsShape = PropTypes.shape({
|
||||
key: PropTypes.string,
|
||||
description: PropTypes.string,
|
||||
points: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
mastery: PropTypes.bool
|
||||
points: PropTypes.number,
|
||||
descriptionError: PropTypes.string,
|
||||
pointsError: PropTypes.string
|
||||
})
|
||||
|
||||
const Ratings = ({ratings, onChangeRatings, canManage}) => {
|
||||
const masteryPointsShape = PropTypes.shape({
|
||||
value: PropTypes.number,
|
||||
error: PropTypes.string
|
||||
})
|
||||
|
||||
const Ratings = ({ratings, masteryPoints, onChangeRatings, onChangeMasteryPoints, canManage}) => {
|
||||
const {isMobileView} = useCanvasContext()
|
||||
|
||||
const addRow = () => {
|
||||
|
@ -99,30 +109,34 @@ const Ratings = ({ratings, onChangeRatings, canManage}) => {
|
|||
[onChangeRatings]
|
||||
)
|
||||
|
||||
const renderBorder = () => (
|
||||
<View
|
||||
width="100%"
|
||||
textAlign="start"
|
||||
margin="0 0 small 0"
|
||||
as="div"
|
||||
borderWidth="none none small none"
|
||||
/>
|
||||
const handleMasteryPointsChange = e => onChangeMasteryPoints(e.target.value)
|
||||
|
||||
const renderDisplayMasteryPoints = () => (
|
||||
<View as="div" padding="small none none">
|
||||
<Flex wrap="wrap" direction="row" padding="none small small none">
|
||||
<Flex.Item
|
||||
as="div"
|
||||
padding="none xx-small none none"
|
||||
data-testid="read-only-mastery-points"
|
||||
>
|
||||
<Text weight="bold">{I18n.t('Mastery at:')}</Text>
|
||||
</Flex.Item>
|
||||
<Flex.Item>
|
||||
<Text color="primary">{I18n.t('%{points} points', {points: masteryPoints.value})}</Text>
|
||||
</Flex.Item>
|
||||
</Flex>
|
||||
</View>
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex
|
||||
width="100%"
|
||||
padding={isMobileView ? '0 0 small 0' : '0 small small small'}
|
||||
padding={isMobileView ? '0 0 small 0' : '0 small small 0'}
|
||||
margin="medium none none"
|
||||
data-testid="outcome-management-ratings"
|
||||
>
|
||||
<Flex.Item size={isMobileView ? '25%' : '15%'} padding="0 medium 0 0">
|
||||
<div aria-hidden="true" className="header">
|
||||
{I18n.t('Mastery')}
|
||||
</div>
|
||||
</Flex.Item>
|
||||
<Flex.Item size={isMobileView ? '75%' : '65%'}>
|
||||
<Flex.Item size={isMobileView ? '75%' : canManage ? '80%' : '60%'}>
|
||||
<div aria-hidden="true" className="header">
|
||||
{I18n.t('Proficiency Rating')}
|
||||
</div>
|
||||
|
@ -135,41 +149,73 @@ const Ratings = ({ratings, onChangeRatings, canManage}) => {
|
|||
</Flex.Item>
|
||||
)}
|
||||
</Flex>
|
||||
{renderBorder()}
|
||||
{ratings.map(({key, description, descriptionError, pointsError, mastery, points}, index) => (
|
||||
<React.Fragment key={key}>
|
||||
<ProficiencyRating
|
||||
description={description}
|
||||
descriptionError={descriptionError}
|
||||
disableDelete={ratings.length === 1}
|
||||
mastery={mastery}
|
||||
onDelete={() => handleDelete(index)}
|
||||
onDescriptionChange={value => onRatingFieldChange('description', value, index)}
|
||||
onMasteryChange={() => onRatingFieldChange('mastery', true, index)}
|
||||
onPointsChange={value => onRatingFieldChange('points', value, index)}
|
||||
points={points?.toString()}
|
||||
pointsError={pointsError}
|
||||
isMobileView={isMobileView}
|
||||
position={index + 1}
|
||||
canManage={canManage}
|
||||
individualOutcome
|
||||
/>
|
||||
{renderBorder()}
|
||||
</React.Fragment>
|
||||
{ratings.map(({key, description, descriptionError, pointsError, points}, index) => (
|
||||
<ProficiencyRating
|
||||
key={key}
|
||||
description={description}
|
||||
descriptionError={descriptionError}
|
||||
disableDelete={ratings.length === 1}
|
||||
onDelete={() => handleDelete(index)}
|
||||
onDescriptionChange={value => onRatingFieldChange('description', value, index)}
|
||||
onMasteryChange={() => onRatingFieldChange('mastery', true, index)}
|
||||
onPointsChange={value => onRatingFieldChange('points', value, index)}
|
||||
points={points?.toString()}
|
||||
pointsError={pointsError}
|
||||
isMobileView={isMobileView}
|
||||
position={index + 1}
|
||||
canManage={canManage}
|
||||
individualOutcome
|
||||
/>
|
||||
))}
|
||||
{canManage && (
|
||||
<View textAlign="center" padding="small" as="div">
|
||||
<IconButton
|
||||
onClick={addRow}
|
||||
withBorder={false}
|
||||
color="primary"
|
||||
size="large"
|
||||
shape="circle"
|
||||
screenReaderLabel={I18n.t('Add Mastery Level')}
|
||||
>
|
||||
<IconPlusLine />
|
||||
</IconButton>
|
||||
</View>
|
||||
{canManage ? (
|
||||
<Flex width="100%" padding="small" alignItems="start">
|
||||
<Flex.Item size="80%">
|
||||
<Flex padding="0 small 0 0" textAlign="end">
|
||||
<Flex.Item size="60%">
|
||||
<IconButton
|
||||
onClick={addRow}
|
||||
withBorder={false}
|
||||
color="primary"
|
||||
size="medium"
|
||||
shape="circle"
|
||||
screenReaderLabel={I18n.t('Add Mastery Level')}
|
||||
>
|
||||
<IconPlusLine />
|
||||
</IconButton>
|
||||
</Flex.Item>
|
||||
<Flex.Item size="40%">
|
||||
<Text weight="bold">{I18n.t('Mastery at')}</Text>
|
||||
</Flex.Item>
|
||||
</Flex>
|
||||
</Flex.Item>
|
||||
<Flex.Item size="20%">
|
||||
<Flex alignItems="start">
|
||||
<Flex.Item>
|
||||
<div className="points" data-testid="mastery-points-input">
|
||||
<TextInput
|
||||
type="text"
|
||||
messages={
|
||||
masteryPoints.error ? [{text: masteryPoints.error, type: 'error'}] : null
|
||||
}
|
||||
renderLabel={
|
||||
<ScreenReaderContent>{I18n.t('Change mastery points')}</ScreenReaderContent>
|
||||
}
|
||||
onChange={handleMasteryPointsChange}
|
||||
defaultValue={I18n.n(masteryPoints.value)}
|
||||
width="4rem"
|
||||
/>
|
||||
</div>
|
||||
</Flex.Item>
|
||||
<Flex.Item>
|
||||
<div style={{paddingTop: '0.45rem', paddingLeft: '0.60rem'}}>
|
||||
{I18n.t('points')}
|
||||
</div>
|
||||
</Flex.Item>
|
||||
</Flex>
|
||||
</Flex.Item>
|
||||
</Flex>
|
||||
) : (
|
||||
renderDisplayMasteryPoints()
|
||||
)}
|
||||
</>
|
||||
)
|
||||
|
@ -177,12 +223,20 @@ const Ratings = ({ratings, onChangeRatings, canManage}) => {
|
|||
|
||||
Ratings.propTypes = {
|
||||
ratings: PropTypes.arrayOf(ratingsShape).isRequired,
|
||||
masteryPoints: masteryPointsShape.isRequired,
|
||||
canManage: PropTypes.bool,
|
||||
onChangeRatings: PropTypes.func
|
||||
onChangeRatings: PropTypes.func,
|
||||
onChangeMasteryPoints: PropTypes.func
|
||||
}
|
||||
|
||||
Ratings.defaultProps = {
|
||||
onChangeRatings: () => {}
|
||||
onChangeRatings: () => {},
|
||||
onChangeMasteryPoints: () => {},
|
||||
ratings: [],
|
||||
masteryPoints: {
|
||||
value: null,
|
||||
error: null
|
||||
}
|
||||
}
|
||||
|
||||
export default Ratings
|
||||
|
|
|
@ -28,7 +28,7 @@ import {
|
|||
updateLearningOutcomeMocks,
|
||||
setFriendlyDescriptionOutcomeMock
|
||||
} from '@canvas/outcomes/mocks/Management'
|
||||
import {defaultOutcomesManagementRatings} from '@canvas/outcomes/react/hooks/useRatings'
|
||||
import {defaultRatings, defaultMasteryPoints} from '@canvas/outcomes/react/hooks/useRatings'
|
||||
|
||||
jest.useFakeTimers()
|
||||
|
||||
|
@ -46,8 +46,8 @@ describe('OutcomeEditModal', () => {
|
|||
contextId: '1',
|
||||
calculationMethod: 'decaying_average',
|
||||
calculationInt: 65,
|
||||
masteryPoints: 3,
|
||||
ratings: defaultOutcomesManagementRatings.map(rating => pick(rating, ['description', 'points']))
|
||||
masteryPoints: defaultMasteryPoints,
|
||||
ratings: defaultRatings.map(rating => pick(rating, ['description', 'points']))
|
||||
}
|
||||
|
||||
const defaultProps = (props = {}) => ({
|
||||
|
@ -328,7 +328,7 @@ describe('OutcomeEditModal', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('with Individual Outcome Proficiency and Calculation Feature Flag enabled', () => {
|
||||
describe('Individual Outcome Proficiency and Calculation Feature Flag', () => {
|
||||
describe('when feature flag enabled', () => {
|
||||
it('displays calculation method selection form if outcome is created in same context', async () => {
|
||||
const {getByLabelText} = renderWithProvider({
|
||||
|
@ -414,7 +414,7 @@ describe('OutcomeEditModal', () => {
|
|||
const mocks = updateLearningOutcomeMocks({
|
||||
description: 'Updated description',
|
||||
displayName: 'Updated friendly outcome name',
|
||||
ratings: defaultOutcomesManagementRatings
|
||||
ratings: defaultRatings
|
||||
.filter(r => r.points !== 0)
|
||||
.map(rating => pick(rating, ['description', 'points'])),
|
||||
individualRatings: true
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
*/
|
||||
|
||||
import React from 'react'
|
||||
import {render as realRender, fireEvent} from '@testing-library/react'
|
||||
import {render as realRender, fireEvent, within} from '@testing-library/react'
|
||||
import Ratings from '../Ratings'
|
||||
import {createRating} from '@canvas/outcomes/react/hooks/useRatings'
|
||||
import OutcomesContext from '@canvas/outcomes/react/contexts/OutcomesContext'
|
||||
|
@ -30,19 +30,23 @@ const render = (children, {isMobileView = false} = {}) => {
|
|||
|
||||
describe('Ratings', () => {
|
||||
let onChangeRatingsMock
|
||||
let onChangeMasteryPointsMock
|
||||
|
||||
const defaultProps = (props = {}) => ({
|
||||
onChangeRatings: onChangeRatingsMock,
|
||||
onChangeMasteryPoints: onChangeMasteryPointsMock,
|
||||
canManage: true,
|
||||
ratings: [
|
||||
createRating('Exceeds Mastery', 4, '127A1B'),
|
||||
createRating('Mastery', 3, '00AC18', true)
|
||||
],
|
||||
ratings: [createRating('Exceeds Mastery', 4), createRating('Mastery', 3)],
|
||||
masteryPoints: {
|
||||
value: 3,
|
||||
error: null
|
||||
},
|
||||
...props
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
onChangeRatingsMock = jest.fn()
|
||||
onChangeMasteryPointsMock = jest.fn()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
|
@ -59,16 +63,6 @@ describe('Ratings', () => {
|
|||
expect(newRatings[1].description).toEqual('New value for description')
|
||||
})
|
||||
|
||||
it('call onChangeRatings with correct mastery when change mastery', () => {
|
||||
const {getByLabelText} = render(<Ratings {...defaultProps()} />)
|
||||
const mastery = getByLabelText('Mastery false for mastery level 1').closest('input')
|
||||
fireEvent.click(mastery)
|
||||
expect(onChangeRatingsMock).toHaveBeenCalled()
|
||||
const newRatings = onChangeRatingsMock.mock.calls[0][0](defaultProps().ratings)
|
||||
// assert set false to previous mastery too
|
||||
expect(newRatings.map(r => r.mastery)).toStrictEqual([true, false])
|
||||
})
|
||||
|
||||
it('calls onChangeRatings when Add Mastery Level is clicked', () => {
|
||||
const {getByText} = render(<Ratings {...defaultProps()} />)
|
||||
fireEvent.click(getByText('Add Mastery Level'))
|
||||
|
@ -87,14 +81,30 @@ describe('Ratings', () => {
|
|||
expect(newRatings[0].description).toEqual('Mastery')
|
||||
})
|
||||
|
||||
it('calls onChangeRatings when delete a mastery rating with new mastery', () => {
|
||||
const {getByText} = render(<Ratings {...defaultProps()} />)
|
||||
fireEvent.click(getByText('Delete mastery level 2'))
|
||||
expect(onChangeRatingsMock).toHaveBeenCalled()
|
||||
const newRatings = onChangeRatingsMock.mock.calls[0][0](defaultProps().ratings)
|
||||
expect(newRatings.length).toEqual(1)
|
||||
expect(newRatings[0].description).toEqual('Exceeds Mastery')
|
||||
expect(newRatings[0].mastery).toBeTruthy()
|
||||
it('call onChangeMasteryPoints with new value of points when mastery points are changed', () => {
|
||||
const {getByLabelText} = render(<Ratings {...defaultProps()} />)
|
||||
fireEvent.change(getByLabelText('Change mastery points').closest('input'), {
|
||||
target: {value: '5'}
|
||||
})
|
||||
expect(onChangeMasteryPointsMock).toHaveBeenCalled()
|
||||
expect(onChangeMasteryPointsMock).toHaveBeenCalledWith('5')
|
||||
})
|
||||
|
||||
it('displays error message for mastery points if validation error', async () => {
|
||||
const {getByDisplayValue} = render(
|
||||
<Ratings
|
||||
{...defaultProps({
|
||||
masteryPoints: {
|
||||
value: 11,
|
||||
error: 'Invalid points'
|
||||
}
|
||||
})}
|
||||
/>
|
||||
)
|
||||
const masteryPointsInput = getByDisplayValue('11')
|
||||
expect(
|
||||
within(masteryPointsInput.closest('.points')).getByText('Invalid points')
|
||||
).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -104,6 +114,11 @@ describe('Ratings', () => {
|
|||
const {queryByText} = render(<Ratings {...defaultProps({canManage: false})} />)
|
||||
expect(queryByText(/Add Mastery Level/)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows read only view of mastery points', () => {
|
||||
const {getByTestId} = render(<Ratings {...defaultProps({canManage: false})} />)
|
||||
expect(getByTestId('read-only-mastery-points')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('when canManage is true', () => {
|
||||
|
@ -111,6 +126,11 @@ describe('Ratings', () => {
|
|||
const {queryByText} = render(<Ratings {...defaultProps()} />)
|
||||
expect(queryByText(/Add Mastery Level/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows mastery points input', () => {
|
||||
const {getByTestId} = render(<Ratings {...defaultProps()} />)
|
||||
expect(getByTestId('mastery-points-input')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -45,7 +45,7 @@ class ProficiencyRating extends React.Component {
|
|||
descriptionError: PropTypes.string,
|
||||
disableDelete: PropTypes.bool.isRequired,
|
||||
focusField: PropTypes.oneOf(['description', 'points', 'mastery', 'trash']),
|
||||
mastery: PropTypes.bool.isRequired,
|
||||
mastery: requiredIf(({individualOutcome}) => !individualOutcome, PropTypes.bool),
|
||||
onColorChange: requiredIf(({individualOutcome}) => !individualOutcome, PropTypes.func),
|
||||
onDelete: PropTypes.func.isRequired,
|
||||
onDescriptionChange: PropTypes.func.isRequired,
|
||||
|
@ -364,21 +364,24 @@ class ProficiencyRating extends React.Component {
|
|||
const {isMobileView, canManage, individualOutcome} = this.props
|
||||
return (
|
||||
<Flex
|
||||
padding={`${isMobileView ? '0 0 small 0' : '0 small small small'}`}
|
||||
padding={`${
|
||||
isMobileView
|
||||
? '0 0 small 0'
|
||||
: individualOutcome
|
||||
? '0 small small 0'
|
||||
: '0 small small small'
|
||||
}`}
|
||||
width="100%"
|
||||
alignItems={isMobileView ? 'center' : 'start'}
|
||||
>
|
||||
<Flex.Item
|
||||
align="start"
|
||||
textAlign="center"
|
||||
padding="0 medium 0 0"
|
||||
size={isMobileView ? '25%' : '15%'}
|
||||
>
|
||||
{this.renderMastery()}
|
||||
</Flex.Item>
|
||||
{!individualOutcome && (
|
||||
<Flex.Item textAlign="center" padding="0 medium 0 0" size={isMobileView ? '25%' : '15%'}>
|
||||
{this.renderMastery()}
|
||||
</Flex.Item>
|
||||
)}
|
||||
<Flex.Item
|
||||
padding="0 small 0 0"
|
||||
size={isMobileView ? '75%' : individualOutcome ? '65%' : '40%'}
|
||||
size={isMobileView ? '75%' : individualOutcome ? (canManage ? '80%' : '60%') : '40%'}
|
||||
align="start"
|
||||
>
|
||||
{this.renderDescription()}
|
||||
|
|
|
@ -14,7 +14,6 @@ exports[`ProficiencyRating can manage renders the ProficiencyRating component 1`
|
|||
wrap="no-wrap"
|
||||
>
|
||||
<Item
|
||||
align="start"
|
||||
as="span"
|
||||
elementRef={[Function]}
|
||||
padding="0 medium 0 0"
|
||||
|
@ -346,7 +345,6 @@ exports[`ProficiencyRating can not manage renders the ProficiencyRating componen
|
|||
wrap="no-wrap"
|
||||
>
|
||||
<Item
|
||||
align="start"
|
||||
as="span"
|
||||
elementRef={[Function]}
|
||||
padding="0 medium 0 0"
|
||||
|
|
|
@ -28,15 +28,14 @@ import {
|
|||
IMPORT_OUTCOMES,
|
||||
CREATE_LEARNING_OUTCOME_GROUP
|
||||
} from '../graphql/Management'
|
||||
import {defaultOutcomesManagementRatings} from '../react/hooks/useRatings'
|
||||
import {defaultRatings, defaultMasteryPoints} from '../react/hooks/useRatings'
|
||||
import {pick, uniq, flattenDeep} from 'lodash'
|
||||
|
||||
const ratingsWithTypename = ratings =>
|
||||
ratings
|
||||
.map(rating => pick(rating, ['description', 'points', 'mastery']))
|
||||
.map(r => ({...r, __typename: 'ProficiencyRating'}))
|
||||
const testRatings = defaultRatings.map(rating => pick(rating, ['description', 'points']))
|
||||
|
||||
const getMasteryPoints = ratings => ratings.filter(rating => rating.mastery)[0].points
|
||||
const ratingsWithTypename = ratings => ratings.map(r => ({...r, __typename: 'ProficiencyRating'}))
|
||||
|
||||
const maxPoints = ratings => ratings.sort((a, b) => b.points - a.points)[0].points
|
||||
|
||||
export const accountMocks = ({childGroupsCount = 10, accountId = '1'} = {}) => [
|
||||
{
|
||||
|
@ -237,8 +236,8 @@ export const treeGroupMocks = ({
|
|||
const childrenOutcomes = (detailsStructure[gid] || []).map(toString)
|
||||
const calculationMethod = 'decaying_average'
|
||||
const calculationInt = 65
|
||||
const masteryPoints = getMasteryPoints(defaultOutcomesManagementRatings)
|
||||
const ratings = ratingsWithTypename(defaultOutcomesManagementRatings)
|
||||
const masteryPoints = defaultMasteryPoints
|
||||
const ratings = ratingsWithTypename(testRatings)
|
||||
|
||||
return {
|
||||
request: {
|
||||
|
@ -484,8 +483,8 @@ const createSearchGroupOutcomesOutcomeMocks = (
|
|||
) => {
|
||||
const calculationMethod = 'decaying_average'
|
||||
const calculationInt = 65
|
||||
const masteryPoints = getMasteryPoints(defaultOutcomesManagementRatings)
|
||||
const ratings = ratingsWithTypename(defaultOutcomesManagementRatings)
|
||||
const masteryPoints = defaultMasteryPoints
|
||||
const ratings = ratingsWithTypename(testRatings)
|
||||
|
||||
// Tech Debt - see OUT-4776 - need to switch this over to a dynamic array like the below code
|
||||
// for now too many tests are dependant on the number of outcomes and the order
|
||||
|
@ -594,8 +593,8 @@ export const groupDetailMocks = ({
|
|||
} = {}) => {
|
||||
const calculationMethod = 'decaying_average'
|
||||
const calculationInt = 65
|
||||
const masteryPoints = getMasteryPoints(defaultOutcomesManagementRatings)
|
||||
const ratings = ratingsWithTypename(defaultOutcomesManagementRatings)
|
||||
const masteryPoints = defaultMasteryPoints
|
||||
const ratings = ratingsWithTypename(testRatings)
|
||||
|
||||
return [
|
||||
{
|
||||
|
@ -1137,8 +1136,8 @@ export const groupDetailMocksFetchMore = ({
|
|||
} = {}) => {
|
||||
const calculationMethod = 'decaying_average'
|
||||
const calculationInt = 65
|
||||
const masteryPoints = getMasteryPoints(defaultOutcomesManagementRatings)
|
||||
const ratings = ratingsWithTypename(defaultOutcomesManagementRatings)
|
||||
const masteryPoints = defaultMasteryPoints
|
||||
const ratings = ratingsWithTypename(testRatings)
|
||||
|
||||
return [
|
||||
{
|
||||
|
@ -1328,8 +1327,8 @@ export const findOutcomesMocks = ({
|
|||
} = {}) => {
|
||||
const calculationMethod = 'decaying_average'
|
||||
const calculationInt = 65
|
||||
const masteryPoints = getMasteryPoints(defaultOutcomesManagementRatings)
|
||||
const ratings = ratingsWithTypename(defaultOutcomesManagementRatings)
|
||||
const masteryPoints = defaultMasteryPoints
|
||||
const ratings = ratingsWithTypename(testRatings)
|
||||
|
||||
return [
|
||||
{
|
||||
|
@ -1545,13 +1544,12 @@ export const createLearningOutcomeMock = ({
|
|||
calculationMethod = 'decaying_average',
|
||||
calculationInt = 65,
|
||||
individualCalculation = false,
|
||||
masteryPoints = 3,
|
||||
ratings = defaultOutcomesManagementRatings,
|
||||
masteryPoints = defaultMasteryPoints,
|
||||
ratings = testRatings,
|
||||
individualRatings = false
|
||||
} = {}) => {
|
||||
const inputRatings = ratings.map(rating => pick(rating, ['description', 'points']))
|
||||
const pointsPossible = ratings.sort((a, b) => b.points - a.points)[0].points
|
||||
const outputRatings = inputRatings.map(r => ({...r, __typename: 'ProficiencyRating'}))
|
||||
const pointsPossible = maxPoints(ratings)
|
||||
const outputRatings = ratingsWithTypename(ratings)
|
||||
|
||||
const successfulResponse = {
|
||||
data: {
|
||||
|
@ -1621,7 +1619,7 @@ export const createLearningOutcomeMock = ({
|
|||
}
|
||||
if (individualRatings) {
|
||||
input.masteryPoints = masteryPoints
|
||||
input.ratings = inputRatings
|
||||
input.ratings = ratings
|
||||
}
|
||||
|
||||
return {
|
||||
|
@ -1643,13 +1641,12 @@ export const updateLearningOutcomeMocks = ({
|
|||
calculationMethod = 'decaying_average',
|
||||
calculationInt = 65,
|
||||
individualCalculation = false,
|
||||
masteryPoints = 3,
|
||||
ratings = defaultOutcomesManagementRatings,
|
||||
masteryPoints = defaultMasteryPoints,
|
||||
ratings = testRatings,
|
||||
individualRatings = false
|
||||
} = {}) => {
|
||||
const inputRatings = ratings.map(rating => pick(rating, ['description', 'points']))
|
||||
const pointsPossible = ratings.sort((a, b) => b.points - a.points)[0].points
|
||||
const outputRatings = inputRatings.map(r => ({...r, __typename: 'ProficiencyRating'}))
|
||||
const pointsPossible = maxPoints(ratings)
|
||||
const outputRatings = ratingsWithTypename(ratings)
|
||||
|
||||
const input = {
|
||||
title,
|
||||
|
@ -1662,7 +1659,7 @@ export const updateLearningOutcomeMocks = ({
|
|||
}
|
||||
if (individualRatings) {
|
||||
input.masteryPoints = masteryPoints
|
||||
input.ratings = inputRatings
|
||||
input.ratings = ratings
|
||||
}
|
||||
const output = {
|
||||
...input,
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {convertRatings, prepareRatings} from '../ratingsHelpers'
|
||||
import {processRatingsAndMastery} from '../ratingsHelpers'
|
||||
|
||||
describe('Ratings Helpers', () => {
|
||||
const testMasteryPoints = 3
|
||||
|
@ -34,47 +34,31 @@ describe('Ratings Helpers', () => {
|
|||
points: 3
|
||||
}
|
||||
]
|
||||
const testConvertRatings = testRatings.map(({description, points}) => ({
|
||||
description,
|
||||
points,
|
||||
mastery: Number(points) === Number(testMasteryPoints)
|
||||
}))
|
||||
|
||||
describe('convertRatings', () => {
|
||||
it('converts masteryPoints', () => {
|
||||
const {masteryPoints} = convertRatings(testConvertRatings)
|
||||
describe('processRatingsAndMastery', () => {
|
||||
it('processes masteryPoints provided as number', () => {
|
||||
const {masteryPoints} = processRatingsAndMastery(testRatings, testMasteryPoints)
|
||||
expect(masteryPoints).toEqual(3)
|
||||
})
|
||||
|
||||
it('converts points provided as number', () => {
|
||||
const {ratings} = convertRatings(testConvertRatings)
|
||||
it('processes masteryPoints provided as string', () => {
|
||||
const {masteryPoints} = processRatingsAndMastery(testRatings, '123.45')
|
||||
expect(masteryPoints).toEqual(123.45)
|
||||
})
|
||||
|
||||
it('rounds masteryPoints to 2 decimal places', () => {
|
||||
const {masteryPoints} = processRatingsAndMastery(testRatings, 3.125)
|
||||
expect(masteryPoints).toEqual(3.13)
|
||||
})
|
||||
|
||||
it('processes points provided as number', () => {
|
||||
const {ratings} = processRatingsAndMastery(testRatings, testMasteryPoints)
|
||||
expect(ratings[0].points).toEqual(1.25)
|
||||
})
|
||||
|
||||
it('converts points provided as string', () => {
|
||||
const {ratings} = convertRatings(testConvertRatings)
|
||||
it('processes points provided as string', () => {
|
||||
const {ratings} = processRatingsAndMastery(testRatings, testMasteryPoints)
|
||||
expect(ratings[1].points).toEqual(2.5)
|
||||
})
|
||||
})
|
||||
|
||||
describe('prepareRatings', () => {
|
||||
it('adds mastery prop to ratings', () => {
|
||||
const result = prepareRatings(testRatings, testMasteryPoints)
|
||||
expect(result[0].mastery).toBeFalsy()
|
||||
expect(result[1].mastery).toBeFalsy()
|
||||
expect(result[2].mastery).toBeTruthy()
|
||||
})
|
||||
|
||||
it('adds key prop to ratings', () => {
|
||||
const result = prepareRatings(testRatings, testMasteryPoints)
|
||||
expect(result[0].key).not.toBeNull()
|
||||
expect(result[1].key).not.toBeNull()
|
||||
expect(result[2].key).not.toBeNull()
|
||||
})
|
||||
|
||||
it('handles null ratings argument', () => {
|
||||
const result = prepareRatings(null, testMasteryPoints)
|
||||
expect(result.length).toBe(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -16,22 +16,7 @@
|
|||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import uuid from 'uuid/v1'
|
||||
|
||||
export const convertRatings = rawRatings => {
|
||||
let masteryPoints
|
||||
const ratings = rawRatings.map(({description, points, mastery}) => {
|
||||
const pointsFloat = parseFloat(points)
|
||||
if (mastery) masteryPoints = pointsFloat
|
||||
return {description, points: pointsFloat}
|
||||
})
|
||||
return {masteryPoints, ratings}
|
||||
}
|
||||
|
||||
export const prepareRatings = (ratings, masteryPoints) =>
|
||||
(ratings || []).map(({description, points}) => ({
|
||||
description,
|
||||
points,
|
||||
mastery: Number(points) === Number(masteryPoints),
|
||||
key: uuid()
|
||||
}))
|
||||
export const processRatingsAndMastery = (ratings, masteryPoints) => ({
|
||||
ratings: ratings.map(({description, points}) => ({description, points: parseFloat(points)})),
|
||||
masteryPoints: Number(parseFloat(masteryPoints).toFixed(2))
|
||||
})
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
*/
|
||||
|
||||
import {renderHook} from '@testing-library/react-hooks/dom'
|
||||
import useRatings, {defaultOutcomesManagementRatings} from '../useRatings'
|
||||
import useRatings, {defaultRatings, defaultMasteryPoints, prepareRatings} from '../useRatings'
|
||||
|
||||
const expectDescriptions = (result, expectedDescriptions) => {
|
||||
expect(result.current.ratings.map(r => r.description)).toStrictEqual(expectedDescriptions)
|
||||
|
@ -47,11 +47,22 @@ const changeRating = (result, ratingIndex, attrs) => {
|
|||
result.current.setRatings(newRatings)
|
||||
}
|
||||
|
||||
const changeMasteryPoints = (result, points) => result.current.setMasteryPoints(points)
|
||||
|
||||
const expectMasteryPoints = (result, expectedMasteryPoints) => {
|
||||
expect(result.current.masteryPoints.value).toEqual(expectedMasteryPoints)
|
||||
}
|
||||
|
||||
const expectMasteryPointsErrors = (result, expectedMasteryPointsError) => {
|
||||
expect(result.current.masteryPoints.error).toEqual(expectedMasteryPointsError)
|
||||
}
|
||||
|
||||
describe('useRatings', () => {
|
||||
const initialRatings = defaultOutcomesManagementRatings
|
||||
const initialRatings = defaultRatings
|
||||
const initialMasteryPoints = defaultMasteryPoints
|
||||
|
||||
test('should create custom hook with initialRatings', () => {
|
||||
const {result} = renderHook(() => useRatings({initialRatings}))
|
||||
const {result} = renderHook(() => useRatings({initialRatings, initialMasteryPoints}))
|
||||
expectDescriptions(result, [
|
||||
'Exceeds Mastery',
|
||||
'Mastery',
|
||||
|
@ -60,11 +71,12 @@ describe('useRatings', () => {
|
|||
'Well Below Mastery'
|
||||
])
|
||||
expectPoints(result, [4, 3, 2, 1, 0])
|
||||
expect(result.current.masteryPoints.value).toEqual(initialMasteryPoints)
|
||||
})
|
||||
|
||||
describe('Updating', () => {
|
||||
it('should reflect changes in ratings', () => {
|
||||
const {result} = renderHook(() => useRatings({initialRatings}))
|
||||
const {result} = renderHook(() => useRatings({initialRatings, initialMasteryPoints}))
|
||||
|
||||
changeRating(result, 0, {description: 'New Exceeds Mastery', points: 5})
|
||||
changeRating(result, 1, {description: 'New Mastery', points: 4})
|
||||
|
@ -80,23 +92,39 @@ describe('useRatings', () => {
|
|||
expectPoints(result, [5, 4, 2, 1, 0])
|
||||
})
|
||||
|
||||
it('returns false for hasChanged if ratings have not changed', () => {
|
||||
const {result} = renderHook(() => useRatings({initialRatings}))
|
||||
it('should reflect changes in mastery points', () => {
|
||||
const {result} = renderHook(() => useRatings({initialRatings, initialMasteryPoints}))
|
||||
|
||||
changeMasteryPoints(result, 11)
|
||||
|
||||
expectMasteryPoints(result, 11)
|
||||
})
|
||||
|
||||
it('returns false for hasChanged if neither ratings nor mastery points have changed', () => {
|
||||
const {result} = renderHook(() => useRatings({initialRatings, initialMasteryPoints}))
|
||||
expect(result.current.hasChanged).toBe(false)
|
||||
})
|
||||
|
||||
it('returns true for hasChanged if ratings have changed', () => {
|
||||
const {result} = renderHook(() => useRatings({initialRatings}))
|
||||
const {result} = renderHook(() => useRatings({initialRatings, initialMasteryPoints}))
|
||||
changeRating(result, 0, {description: 'New Exceeds Mastery', points: 5})
|
||||
changeRating(result, 1, {description: 'New Mastery', points: 4})
|
||||
expect(result.current.hasChanged).toBe(true)
|
||||
})
|
||||
|
||||
it('returns true for hasChanged if mastery points have changed', () => {
|
||||
const {result} = renderHook(() => useRatings({initialRatings, initialMasteryPoints}))
|
||||
|
||||
changeMasteryPoints(result, 11)
|
||||
|
||||
expect(result.current.hasChanged).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Validations', () => {
|
||||
describe('Description validations', () => {
|
||||
it('should be ok when receiving good data', () => {
|
||||
const {result} = renderHook(() => useRatings({initialRatings}))
|
||||
const {result} = renderHook(() => useRatings({initialRatings, initialMasteryPoints}))
|
||||
|
||||
changeRating(result, 0, {description: 'Any description'})
|
||||
|
||||
|
@ -105,7 +133,7 @@ describe('useRatings', () => {
|
|||
})
|
||||
|
||||
it('should validate blank', () => {
|
||||
const {result} = renderHook(() => useRatings({initialRatings}))
|
||||
const {result} = renderHook(() => useRatings({initialRatings, initialMasteryPoints}))
|
||||
|
||||
changeRating(result, 0, {description: ''})
|
||||
|
||||
|
@ -116,7 +144,7 @@ describe('useRatings', () => {
|
|||
|
||||
describe('Points validations', () => {
|
||||
it('should be ok for positive integer', () => {
|
||||
const {result} = renderHook(() => useRatings({initialRatings}))
|
||||
const {result} = renderHook(() => useRatings({initialRatings, initialMasteryPoints}))
|
||||
|
||||
changeRating(result, 0, {points: '10'})
|
||||
|
||||
|
@ -125,7 +153,7 @@ describe('useRatings', () => {
|
|||
})
|
||||
|
||||
it('should be ok for positive float', () => {
|
||||
const {result} = renderHook(() => useRatings({initialRatings}))
|
||||
const {result} = renderHook(() => useRatings({initialRatings, initialMasteryPoints}))
|
||||
|
||||
changeRating(result, 0, {points: '10.5'})
|
||||
|
||||
|
@ -134,7 +162,7 @@ describe('useRatings', () => {
|
|||
})
|
||||
|
||||
it('should validate blank points', () => {
|
||||
const {result} = renderHook(() => useRatings({initialRatings}))
|
||||
const {result} = renderHook(() => useRatings({initialRatings, initialMasteryPoints}))
|
||||
|
||||
changeRating(result, 0, {points: ''})
|
||||
|
||||
|
@ -143,7 +171,7 @@ describe('useRatings', () => {
|
|||
})
|
||||
|
||||
it('should validate invalid points', () => {
|
||||
const {result} = renderHook(() => useRatings({initialRatings}))
|
||||
const {result} = renderHook(() => useRatings({initialRatings, initialMasteryPoints}))
|
||||
|
||||
changeRating(result, 0, {points: 'lorem'})
|
||||
|
||||
|
@ -152,7 +180,7 @@ describe('useRatings', () => {
|
|||
})
|
||||
|
||||
it('should validate points with comma as invalid points', () => {
|
||||
const {result} = renderHook(() => useRatings({initialRatings}))
|
||||
const {result} = renderHook(() => useRatings({initialRatings, initialMasteryPoints}))
|
||||
|
||||
changeRating(result, 0, {points: '0,5'})
|
||||
|
||||
|
@ -161,7 +189,7 @@ describe('useRatings', () => {
|
|||
})
|
||||
|
||||
it('should validate negative points', () => {
|
||||
const {result} = renderHook(() => useRatings({initialRatings}))
|
||||
const {result} = renderHook(() => useRatings({initialRatings, initialMasteryPoints}))
|
||||
|
||||
changeRating(result, 0, {points: '-1'})
|
||||
|
||||
|
@ -170,7 +198,7 @@ describe('useRatings', () => {
|
|||
})
|
||||
|
||||
it('should validate unique points', () => {
|
||||
const {result} = renderHook(() => useRatings({initialRatings}))
|
||||
const {result} = renderHook(() => useRatings({initialRatings, initialMasteryPoints}))
|
||||
|
||||
// change the first points to match the second
|
||||
changeRating(result, 0, {points: '3'})
|
||||
|
@ -180,5 +208,114 @@ describe('useRatings', () => {
|
|||
expect(result.current.hasError).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Mastery points validations', () => {
|
||||
it('should pass validations for positive integer', () => {
|
||||
const {result} = renderHook(() => useRatings({initialRatings, initialMasteryPoints}))
|
||||
|
||||
changeMasteryPoints(result, 3)
|
||||
|
||||
expectMasteryPointsErrors(result, null)
|
||||
expect(result.current.hasError).toBeFalsy()
|
||||
})
|
||||
|
||||
it('should pass validations for positive float', () => {
|
||||
const {result} = renderHook(() => useRatings({initialRatings, initialMasteryPoints}))
|
||||
|
||||
changeMasteryPoints(result, 3.5)
|
||||
|
||||
expectMasteryPointsErrors(result, null)
|
||||
expect(result.current.hasError).toBeFalsy()
|
||||
})
|
||||
|
||||
it('should generate error if no points', () => {
|
||||
const {result} = renderHook(() => useRatings({initialRatings, initialMasteryPoints}))
|
||||
|
||||
changeMasteryPoints(result, '')
|
||||
|
||||
expectMasteryPointsErrors(result, 'Missing required points')
|
||||
expect(result.current.hasError).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should generate error if invalid points', () => {
|
||||
const {result} = renderHook(() => useRatings({initialRatings, initialMasteryPoints}))
|
||||
|
||||
changeMasteryPoints(result, 'lorem')
|
||||
|
||||
expectMasteryPointsErrors(result, 'Invalid points')
|
||||
expect(result.current.hasError).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should generate error if points have comma delimiter', () => {
|
||||
const {result} = renderHook(() => useRatings({initialRatings, initialMasteryPoints}))
|
||||
|
||||
changeMasteryPoints(result, '0,5')
|
||||
|
||||
expectMasteryPointsErrors(result, 'Invalid points')
|
||||
expect(result.current.hasError).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should generate error if negative points', () => {
|
||||
const {result} = renderHook(() => useRatings({initialRatings, initialMasteryPoints}))
|
||||
|
||||
changeMasteryPoints(result, '-1')
|
||||
|
||||
expectMasteryPointsErrors(result, 'Negative points')
|
||||
expect(result.current.hasError).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should generate error if mastery points greater than max rating', () => {
|
||||
const {result} = renderHook(() => useRatings({initialRatings, initialMasteryPoints}))
|
||||
|
||||
changeMasteryPoints(result, 11)
|
||||
|
||||
expectMasteryPointsErrors(result, 'Above max rating')
|
||||
expect(result.current.hasError).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should generate error if mastery points less than min rating', () => {
|
||||
const {result} = renderHook(() =>
|
||||
useRatings({
|
||||
initialRatings: defaultRatings.filter(r => r.points !== 0),
|
||||
initialMasteryPoints
|
||||
})
|
||||
)
|
||||
|
||||
changeMasteryPoints(result, 0)
|
||||
|
||||
expectMasteryPointsErrors(result, 'Below min rating')
|
||||
expect(result.current.hasError).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should bypass mastery point validations if no ratings', () => {
|
||||
const {result} = renderHook(() =>
|
||||
useRatings({
|
||||
initialRatings: null,
|
||||
initialMasteryPoints
|
||||
})
|
||||
)
|
||||
|
||||
changeMasteryPoints(result, 0)
|
||||
|
||||
expectMasteryPointsErrors(result, null)
|
||||
expect(result.current.hasError).toBeFalsy()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('prepareRatings', () => {
|
||||
it('should add a key prop to each rating', () => {
|
||||
const result = prepareRatings(defaultRatings)
|
||||
expect(result[0].key).not.toBeNull()
|
||||
expect(result[1].key).not.toBeNull()
|
||||
expect(result[2].key).not.toBeNull()
|
||||
expect(result[3].key).not.toBeNull()
|
||||
expect(result[4].key).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should return an empty array if no ratings', () => {
|
||||
const result = prepareRatings()
|
||||
expect(result.length).toBe(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -16,32 +16,38 @@
|
|||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {useMemo, useState} from 'react'
|
||||
import {useMemo, useState, useCallback} from 'react'
|
||||
import I18n from 'i18n!ProficiencyTable'
|
||||
import uuid from 'uuid/v1'
|
||||
import useBoolean from './useBoolean'
|
||||
|
||||
const floatRegex = /^[+-]?\d+(\.\d+)?$/
|
||||
|
||||
export const createRating = (description, points, color, mastery = false) => ({
|
||||
export const createRating = (description, points) => ({
|
||||
description,
|
||||
points,
|
||||
key: uuid(),
|
||||
color,
|
||||
mastery
|
||||
key: uuid()
|
||||
})
|
||||
|
||||
export const defaultOutcomesManagementRatings = [
|
||||
createRating(I18n.t('Exceeds Mastery'), 4, '127A1B'),
|
||||
createRating(I18n.t('Mastery'), 3, '00AC18', true),
|
||||
createRating(I18n.t('Near Mastery'), 2, 'FAB901'),
|
||||
createRating(I18n.t('Below Mastery'), 1, 'FD5D10'),
|
||||
createRating(I18n.t('Well Below Mastery'), 0, 'EE0612')
|
||||
export const defaultRatings = [
|
||||
createRating(I18n.t('Exceeds Mastery'), 4),
|
||||
createRating(I18n.t('Mastery'), 3),
|
||||
createRating(I18n.t('Near Mastery'), 2),
|
||||
createRating(I18n.t('Below Mastery'), 1),
|
||||
createRating(I18n.t('Well Below Mastery'), 0)
|
||||
]
|
||||
|
||||
export const defaultMasteryPoints = 3
|
||||
|
||||
export const prepareRatings = ratings =>
|
||||
(ratings || []).map(({description, points}) => createRating(description, points))
|
||||
|
||||
const invalidDescription = description => (description?.trim() || '').length === 0
|
||||
|
||||
const useRatings = ({initialRatings}) => {
|
||||
const [ratings, setRatings] = useState(initialRatings)
|
||||
const useRatings = ({initialRatings, initialMasteryPoints}) => {
|
||||
const [ratings, setRatings] = useState(() => prepareRatings(initialRatings))
|
||||
const [masteryPoints, setMasteryPoints] = useState(initialMasteryPoints)
|
||||
const [hasChanged, setHasChanged] = useBoolean(false)
|
||||
|
||||
const ratingsWithValidations = useMemo(() => {
|
||||
const allPoints = ratings.map(r => r.points)
|
||||
|
@ -70,25 +76,69 @@ const useRatings = ({initialRatings}) => {
|
|||
|
||||
return {
|
||||
...r,
|
||||
points,
|
||||
pointsError,
|
||||
descriptionError
|
||||
}
|
||||
})
|
||||
}, [ratings])
|
||||
|
||||
const hasError = useMemo(() => {
|
||||
return ratingsWithValidations.some(r => r.pointsError || r.descriptionError)
|
||||
}, [ratingsWithValidations])
|
||||
const masteryPointsWithValidations = useMemo(() => {
|
||||
let error = null
|
||||
const masteryPointsFloat = masteryPoints ? parseFloat(masteryPoints) : null
|
||||
|
||||
const hasChanged = useMemo(
|
||||
() => JSON.stringify(ratings) !== JSON.stringify(initialRatings),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[ratings]
|
||||
if (ratings.length > 0) {
|
||||
const sortedRatings = [...ratings].sort((a, b) => parseFloat(a.points) - parseFloat(b.points))
|
||||
const minRatingPoints = parseFloat(sortedRatings[0].points)
|
||||
const maxRatingPoints = parseFloat(sortedRatings[sortedRatings.length - 1].points)
|
||||
|
||||
if ([null, undefined, ''].includes(masteryPoints)) {
|
||||
error = I18n.t('Missing required points')
|
||||
} else if (!floatRegex.test(masteryPoints)) {
|
||||
error = I18n.t('Invalid points')
|
||||
} else if (masteryPointsFloat < 0) {
|
||||
error = I18n.t('Negative points')
|
||||
} else if (masteryPointsFloat > maxRatingPoints) {
|
||||
error = I18n.t('Above max rating')
|
||||
} else if (masteryPointsFloat < minRatingPoints) {
|
||||
error = I18n.t('Below min rating')
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
value: masteryPointsFloat,
|
||||
error
|
||||
}
|
||||
}, [ratings, masteryPoints])
|
||||
|
||||
const hasError = useMemo(() => {
|
||||
return (
|
||||
ratingsWithValidations.some(r => r.pointsError || r.descriptionError) ||
|
||||
masteryPointsWithValidations.error
|
||||
)
|
||||
}, [ratingsWithValidations, masteryPointsWithValidations])
|
||||
|
||||
const changeRatings = useCallback(
|
||||
value => {
|
||||
if (!hasChanged) setHasChanged()
|
||||
setRatings(value)
|
||||
},
|
||||
[hasChanged, setHasChanged]
|
||||
)
|
||||
|
||||
const changeMasteryPoints = useCallback(
|
||||
value => {
|
||||
if (!hasChanged) setHasChanged()
|
||||
setMasteryPoints(value)
|
||||
},
|
||||
[hasChanged, setHasChanged]
|
||||
)
|
||||
|
||||
return {
|
||||
ratings: ratingsWithValidations,
|
||||
setRatings,
|
||||
masteryPoints: masteryPointsWithValidations,
|
||||
setRatings: changeRatings,
|
||||
setMasteryPoints: changeMasteryPoints,
|
||||
hasError,
|
||||
hasChanged
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue