Add confirmation dialog on navigating away
Add confirmation dialog on navigating away from mastery scales or calculation method closes OUT-4090 flag=account_level_mastery_scales TEST PLAN: - With account_level_mastery_scales FF enabled - Go to account outcomes > Mastery - Change some data - try to switch tab and assert a confirmation modal is present - Try to close browser's tab and asser the confirmation modal is present - Verify the confirm and cancel behavior is working correctly - Now save the data and verify you can switch tab or close browser's tab without any confirmation modal - Do the same for account outcomes > Calculation - Do the same for course outcomes Change-Id: I31573a3290115d0d0e2de6a4343ad7d8cdce6d87 Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/254003 Reviewed-by: Pat Renner <prenner@instructure.com> QA-Review: Pat Renner <prenner@instructure.com> Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com> Product-Review: Jody Sailor
This commit is contained in:
parent
32a65e8dd0
commit
63eb21dfd9
|
@ -25,7 +25,7 @@ const ManagementHeader = () => {
|
|||
const noop = () => {}
|
||||
|
||||
return (
|
||||
<div className="management-header">
|
||||
<div className="management-header" data-testid="managementHeader">
|
||||
<div>
|
||||
<h2 className="title">{I18n.t('Outcomes')}</h2>
|
||||
</div>
|
||||
|
|
|
@ -180,15 +180,29 @@ const getModalText = contextType => {
|
|||
)
|
||||
}
|
||||
|
||||
const ProficiencyCalculation = ({method, update, updateError, canManage, contextType}) => {
|
||||
const ProficiencyCalculation = ({
|
||||
method,
|
||||
update,
|
||||
updateError,
|
||||
canManage,
|
||||
contextType,
|
||||
onNotifyPendingChanges
|
||||
}) => {
|
||||
const {calculationMethod: initialMethodKey, calculationInt: initialInt} = method
|
||||
|
||||
const [calculationMethodKey, setCalculationMethodKey] = useState(initialMethodKey)
|
||||
const [calculationInt, setCalculationInt] = useState(initialInt)
|
||||
|
||||
const [allowSave, setAllowSave] = useState(false)
|
||||
const [allowSave, realSetAllowSave] = useState(false)
|
||||
const [showConfirmation, setShowConfirmationModal] = useState(false)
|
||||
|
||||
const setAllowSave = newAllowSave => {
|
||||
realSetAllowSave(newAllowSave)
|
||||
if (onNotifyPendingChanges) {
|
||||
onNotifyPendingChanges(newAllowSave)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (updateError) {
|
||||
$.flashError(I18n.t('An error occurred updating the calculation method'))
|
||||
|
@ -284,6 +298,7 @@ ProficiencyCalculation.propTypes = {
|
|||
}),
|
||||
canManage: PropTypes.bool,
|
||||
update: PropTypes.func.isRequired,
|
||||
onNotifyPendingChanges: PropTypes.func,
|
||||
updateError: PropTypes.string,
|
||||
contextType: PropTypes.string.isRequired
|
||||
}
|
||||
|
|
|
@ -44,6 +44,7 @@ describe('ProficiencyCalculation', () => {
|
|||
const makeProps = (overrides = {}) => ({
|
||||
update: Function.prototype,
|
||||
canManage: true,
|
||||
contextType: 'Account',
|
||||
...overrides,
|
||||
method: {
|
||||
calculationMethod: 'decaying_average',
|
||||
|
@ -159,6 +160,22 @@ describe('ProficiencyCalculation', () => {
|
|||
expect(update).toHaveBeenCalledWith('decaying_average', 41)
|
||||
})
|
||||
|
||||
it('calls onNotifyPendingChanges when changes data', async () => {
|
||||
const onNotifyPendingChangesSpy = jest.fn()
|
||||
const {getByText, getByLabelText} = render(
|
||||
<ProficiencyCalculation
|
||||
{...makeProps({onNotifyPendingChanges: onNotifyPendingChangesSpy})}
|
||||
/>
|
||||
)
|
||||
const parameter = getByLabelText('Parameter')
|
||||
fireEvent.input(parameter, {target: {value: '22'}})
|
||||
expect(onNotifyPendingChangesSpy).toHaveBeenCalledWith(true)
|
||||
onNotifyPendingChangesSpy.mockClear()
|
||||
fireEvent.click(getByText('Save Mastery Calculation'))
|
||||
fireEvent.click(getByText('Save'))
|
||||
expect(onNotifyPendingChangesSpy).toHaveBeenCalledWith(false)
|
||||
})
|
||||
|
||||
it('save button is initially disabled', () => {
|
||||
const {getByText} = render(<ProficiencyCalculation {...makeProps()} />)
|
||||
expect(getByText('Save Mastery Calculation').closest('button').disabled).toEqual(true)
|
||||
|
|
|
@ -19,12 +19,9 @@
|
|||
import React from 'react'
|
||||
import {render, wait, fireEvent, waitForElementToBeRemoved} from '@testing-library/react'
|
||||
import {MockedProvider} from '@apollo/react-testing'
|
||||
import {
|
||||
ACCOUNT_OUTCOME_PROFICIENCY_QUERY,
|
||||
COURSE_OUTCOME_PROFICIENCY_QUERY,
|
||||
SET_OUTCOME_CALCULATION_METHOD
|
||||
} from '../api'
|
||||
import {ACCOUNT_OUTCOME_CALCULATION_QUERY, SET_OUTCOME_CALCULATION_METHOD} from '../api'
|
||||
import MasteryCalculation from '../index'
|
||||
import {masteryCalculationGraphqlMocks as mocks} from '../../__tests__/mocks'
|
||||
|
||||
describe('MasteryCalculation', () => {
|
||||
beforeEach(() => {
|
||||
|
@ -55,50 +52,6 @@ describe('MasteryCalculation', () => {
|
|||
window.ENV = null
|
||||
})
|
||||
|
||||
const outcomeCalculationMethod = {
|
||||
__typename: 'OutcomeCalculationMethod',
|
||||
_id: '1',
|
||||
contextType: 'Account',
|
||||
contextId: 1,
|
||||
calculationMethod: 'decaying_average',
|
||||
calculationInt: 65
|
||||
}
|
||||
|
||||
const mocks = [
|
||||
{
|
||||
request: {
|
||||
query: ACCOUNT_OUTCOME_PROFICIENCY_QUERY,
|
||||
variables: {
|
||||
contextId: '11'
|
||||
}
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
context: {
|
||||
__typename: 'Account',
|
||||
outcomeCalculationMethod
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
request: {
|
||||
query: COURSE_OUTCOME_PROFICIENCY_QUERY,
|
||||
variables: {
|
||||
contextId: '12'
|
||||
}
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
context: {
|
||||
__typename: 'Course',
|
||||
outcomeCalculationMethod
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
it('loads proficiency data', async () => {
|
||||
const {getByText, queryByText, getByDisplayValue} = render(
|
||||
<MockedProvider mocks={mocks}>
|
||||
|
@ -153,7 +106,7 @@ describe('MasteryCalculation', () => {
|
|||
const emptyMocks = [
|
||||
{
|
||||
request: {
|
||||
query: ACCOUNT_OUTCOME_PROFICIENCY_QUERY,
|
||||
query: ACCOUNT_OUTCOME_CALCULATION_QUERY,
|
||||
variables: {
|
||||
contextId: '11'
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
|
||||
import {gql} from 'jsx/canvas-apollo'
|
||||
|
||||
export const ACCOUNT_OUTCOME_PROFICIENCY_QUERY = gql`
|
||||
export const ACCOUNT_OUTCOME_CALCULATION_QUERY = gql`
|
||||
query GetOutcomeProficiencyData($contextId: ID!) {
|
||||
context: account(id: $contextId) {
|
||||
outcomeCalculationMethod {
|
||||
|
@ -32,7 +32,7 @@ export const ACCOUNT_OUTCOME_PROFICIENCY_QUERY = gql`
|
|||
}
|
||||
`
|
||||
|
||||
export const COURSE_OUTCOME_PROFICIENCY_QUERY = gql`
|
||||
export const COURSE_OUTCOME_CALCULATION_QUERY = gql`
|
||||
query GetOutcomeProficiencyData($contextId: ID!) {
|
||||
context: course(id: $contextId) {
|
||||
outcomeCalculationMethod {
|
||||
|
|
|
@ -24,15 +24,15 @@ import {Text} from '@instructure/ui-text'
|
|||
import ProficiencyCalculation from './ProficiencyCalculation'
|
||||
import RoleList from '../RoleList'
|
||||
import {
|
||||
ACCOUNT_OUTCOME_PROFICIENCY_QUERY,
|
||||
COURSE_OUTCOME_PROFICIENCY_QUERY,
|
||||
ACCOUNT_OUTCOME_CALCULATION_QUERY,
|
||||
COURSE_OUTCOME_CALCULATION_QUERY,
|
||||
SET_OUTCOME_CALCULATION_METHOD
|
||||
} from './api'
|
||||
import {useQuery, useMutation} from 'react-apollo'
|
||||
|
||||
const MasteryCalculation = ({contextType, contextId}) => {
|
||||
const MasteryCalculation = ({contextType, contextId, onNotifyPendingChanges}) => {
|
||||
const query =
|
||||
contextType === 'Course' ? COURSE_OUTCOME_PROFICIENCY_QUERY : ACCOUNT_OUTCOME_PROFICIENCY_QUERY
|
||||
contextType === 'Course' ? COURSE_OUTCOME_CALCULATION_QUERY : ACCOUNT_OUTCOME_CALCULATION_QUERY
|
||||
const {loading, error, data} = useQuery(query, {
|
||||
variables: {contextId}
|
||||
})
|
||||
|
@ -77,6 +77,7 @@ const MasteryCalculation = ({contextType, contextId}) => {
|
|||
update={setCalculationMethod}
|
||||
updateError={setCalculationMethodError}
|
||||
canManage={canManage}
|
||||
onNotifyPendingChanges={onNotifyPendingChanges}
|
||||
/>
|
||||
|
||||
{accountRoles.length > 0 && (
|
||||
|
|
|
@ -57,7 +57,6 @@ const configToState = data => {
|
|||
return {
|
||||
rows,
|
||||
savedRows: rows,
|
||||
allowSave: false,
|
||||
showConfirmation: false
|
||||
}
|
||||
}
|
||||
|
@ -68,7 +67,8 @@ class ProficiencyTable extends React.Component {
|
|||
update: PropTypes.func.isRequired,
|
||||
focusTab: PropTypes.func,
|
||||
breakpoints: breakpointsShape,
|
||||
contextType: PropTypes.string.isRequired
|
||||
contextType: PropTypes.string.isRequired,
|
||||
onNotifyPendingChanges: PropTypes.func
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
|
@ -96,11 +96,20 @@ class ProficiencyTable extends React.Component {
|
|||
componentDidUpdate() {
|
||||
if (this.fieldWithFocus()) {
|
||||
// eslint-disable-next-line react/no-did-update-set-state
|
||||
this.setState(({rows}) => ({rows: rows.map(row => row.delete('focusField'))}))
|
||||
this.setState(
|
||||
({rows}) => ({rows: rows.map(row => row.delete('focusField'))}),
|
||||
this.notifyPendingChanges
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
allowSave = () => {
|
||||
notifyPendingChanges = () => {
|
||||
if (this.props.onNotifyPendingChanges) {
|
||||
this.props.onNotifyPendingChanges(this.hasPendingChanges())
|
||||
}
|
||||
}
|
||||
|
||||
hasPendingChanges = () => {
|
||||
const {rows, savedRows} = this.state
|
||||
|
||||
return !_.isEqual(rows, savedRows)
|
||||
|
@ -125,6 +134,7 @@ class ProficiencyTable extends React.Component {
|
|||
return {rows: rows.push(newRow)}
|
||||
},
|
||||
() => {
|
||||
this.notifyPendingChanges()
|
||||
$.screenReaderFlashMessage(I18n.t('Added mastery level'))
|
||||
}
|
||||
)
|
||||
|
@ -149,6 +159,7 @@ class ProficiencyTable extends React.Component {
|
|||
}
|
||||
},
|
||||
() => {
|
||||
this.notifyPendingChanges()
|
||||
this.props
|
||||
.update(this.stateToConfig())
|
||||
.then(() => $.flashMessage(I18n.t(`Mastery scale saved`)))
|
||||
|
@ -158,7 +169,7 @@ class ProficiencyTable extends React.Component {
|
|||
message: e.message
|
||||
})
|
||||
)
|
||||
this.setState({savedRows: oldRows})
|
||||
this.setState({savedRows: oldRows}, this.notifyPendingChanges)
|
||||
})
|
||||
}
|
||||
)
|
||||
|
@ -171,7 +182,7 @@ class ProficiencyTable extends React.Component {
|
|||
.setIn([masteryIndex, 'mastery'], false)
|
||||
.setIn([index, 'mastery'], true)
|
||||
return {rows: adjustedRows}
|
||||
})
|
||||
}, this.notifyPendingChanges)
|
||||
})
|
||||
|
||||
handleDescriptionChange = _.memoize(index => value => {
|
||||
|
@ -181,7 +192,7 @@ class ProficiencyTable extends React.Component {
|
|||
}
|
||||
rows = rows.setIn([index, 'description'], value)
|
||||
return {rows}
|
||||
})
|
||||
}, this.notifyPendingChanges)
|
||||
})
|
||||
|
||||
handlePointsChange = _.memoize(index => value => {
|
||||
|
@ -192,13 +203,16 @@ class ProficiencyTable extends React.Component {
|
|||
}
|
||||
rows = rows.setIn([index, 'points'], parsed)
|
||||
return {rows}
|
||||
})
|
||||
}, this.notifyPendingChanges)
|
||||
})
|
||||
|
||||
handleColorChange = _.memoize(index => value => {
|
||||
this.setState(({rows}) => ({
|
||||
rows: rows.update(index, row => row.set('color', unformatColor(value)))
|
||||
}))
|
||||
this.setState(
|
||||
({rows}) => ({
|
||||
rows: rows.update(index, row => row.set('color', unformatColor(value)))
|
||||
}),
|
||||
this.notifyPendingChanges
|
||||
)
|
||||
})
|
||||
|
||||
handleDelete = _.memoize(index => () => {
|
||||
|
@ -214,12 +228,17 @@ class ProficiencyTable extends React.Component {
|
|||
}
|
||||
|
||||
if (index === 0) {
|
||||
this.setState({rows})
|
||||
this.setState({rows}, this.notifyPendingChanges)
|
||||
if (this.props.focusTab) {
|
||||
setTimeout(this.props.focusTab, 700)
|
||||
}
|
||||
} else {
|
||||
this.setState({rows: rows.setIn([index - 1, 'focusField'], 'trash')})
|
||||
this.setState(
|
||||
{
|
||||
rows: rows.setIn([index - 1, 'focusField'], 'trash')
|
||||
},
|
||||
this.notifyPendingChanges
|
||||
)
|
||||
}
|
||||
$.screenReaderFlashMessage(I18n.t('Mastery level deleted'))
|
||||
})
|
||||
|
@ -289,7 +308,7 @@ class ProficiencyTable extends React.Component {
|
|||
return r
|
||||
})
|
||||
if (changed) {
|
||||
this.setState({rows})
|
||||
this.setState({rows}, this.notifyPendingChanges)
|
||||
}
|
||||
return hasError
|
||||
}
|
||||
|
@ -395,7 +414,7 @@ class ProficiencyTable extends React.Component {
|
|||
<div className="save">
|
||||
<Button
|
||||
variant="primary"
|
||||
interaction={this.allowSave() ? 'enabled' : 'disabled'}
|
||||
interaction={this.hasPendingChanges() ? 'enabled' : 'disabled'}
|
||||
onClick={this.confirmSubmit}
|
||||
>
|
||||
{I18n.t('Save Mastery Scale')}
|
||||
|
|
|
@ -37,7 +37,7 @@ describe('default proficiency', () => {
|
|||
})
|
||||
|
||||
it('renders the correct headers', () => {
|
||||
const {getByText} = render(<ProficiencyTable {...defaultProps} />)
|
||||
const {getByText} = render(<ProficiencyTable {...defaultProps()} />)
|
||||
expect(getByText('Mastery')).not.toBeNull()
|
||||
expect(getByText('Description')).not.toBeNull()
|
||||
expect(getByText('Points')).not.toBeNull()
|
||||
|
@ -45,13 +45,13 @@ describe('default proficiency', () => {
|
|||
})
|
||||
|
||||
it('renders five ratings', () => {
|
||||
const {getAllByLabelText} = render(<ProficiencyTable {...defaultProps} />)
|
||||
const {getAllByLabelText} = render(<ProficiencyTable {...defaultProps()} />)
|
||||
const inputs = getAllByLabelText(/Change description/)
|
||||
expect(inputs.length).toEqual(5)
|
||||
})
|
||||
|
||||
it('clicking button adds rating', () => {
|
||||
const {getByText, getAllByLabelText} = render(<ProficiencyTable {...defaultProps} />)
|
||||
const {getByText, getAllByLabelText} = render(<ProficiencyTable {...defaultProps()} />)
|
||||
const button = getByText(/Add Mastery Level/)
|
||||
fireEvent.click(button)
|
||||
const inputs = getAllByLabelText(/Change description/)
|
||||
|
@ -59,21 +59,21 @@ describe('default proficiency', () => {
|
|||
})
|
||||
|
||||
it('clicking add rating button flashes SR message', () => {
|
||||
const {getByText} = render(<ProficiencyTable {...defaultProps} />)
|
||||
const {getByText} = render(<ProficiencyTable {...defaultProps()} />)
|
||||
const button = getByText(/Add Mastery Level/)
|
||||
fireEvent.click(button)
|
||||
expect(srFlashMock).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('handling delete rating removes rating and flashes SR message', () => {
|
||||
const {getAllByText, getByText} = render(<ProficiencyTable {...defaultProps} />)
|
||||
const {getAllByText, getByText} = render(<ProficiencyTable {...defaultProps()} />)
|
||||
fireEvent.click(getAllByText(/Delete mastery level/)[0])
|
||||
fireEvent.click(getByText(/Confirm/))
|
||||
expect(srFlashMock).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('setting blank description sets error and focus', async () => {
|
||||
const {getByDisplayValue, getByText} = render(<ProficiencyTable {...defaultProps} />)
|
||||
const {getByDisplayValue, getByText} = render(<ProficiencyTable {...defaultProps()} />)
|
||||
const masteryField = getByDisplayValue('Mastery')
|
||||
fireEvent.change(masteryField, {target: {value: ''}})
|
||||
fireEvent.click(getByText('Save Mastery Scale'))
|
||||
|
@ -85,7 +85,7 @@ describe('default proficiency', () => {
|
|||
})
|
||||
|
||||
it('setting blank points sets error and focus', async () => {
|
||||
const {getByDisplayValue, getByText} = render(<ProficiencyTable {...defaultProps} />)
|
||||
const {getByDisplayValue, getByText} = render(<ProficiencyTable {...defaultProps()} />)
|
||||
const pointsInput = getByDisplayValue('3')
|
||||
fireEvent.change(pointsInput, {target: {value: ''}})
|
||||
fireEvent.click(getByText('Save Mastery Scale'))
|
||||
|
@ -95,7 +95,7 @@ describe('default proficiency', () => {
|
|||
})
|
||||
|
||||
it('setting invalid points sets error and focus', async () => {
|
||||
const {getByDisplayValue, getByText} = render(<ProficiencyTable {...defaultProps} />)
|
||||
const {getByDisplayValue, getByText} = render(<ProficiencyTable {...defaultProps()} />)
|
||||
const pointsInput = getByDisplayValue('3')
|
||||
fireEvent.change(pointsInput, {target: {value: '1.1.1'}})
|
||||
fireEvent.click(getByText('Save Mastery Scale'))
|
||||
|
@ -105,7 +105,7 @@ describe('default proficiency', () => {
|
|||
})
|
||||
|
||||
it('setting negative points sets error and focus', async () => {
|
||||
const {getByDisplayValue, getByText} = render(<ProficiencyTable {...defaultProps} />)
|
||||
const {getByDisplayValue, getByText} = render(<ProficiencyTable {...defaultProps()} />)
|
||||
const pointsInput = getByDisplayValue('3')
|
||||
fireEvent.change(pointsInput, {target: {value: '-1'}})
|
||||
fireEvent.click(getByText('Save Mastery Scale'))
|
||||
|
@ -115,7 +115,7 @@ describe('default proficiency', () => {
|
|||
})
|
||||
|
||||
it('setting duplicate point values sets error and focus', async () => {
|
||||
const {getByDisplayValue, getByText} = render(<ProficiencyTable {...defaultProps} />)
|
||||
const {getByDisplayValue, getByText} = render(<ProficiencyTable {...defaultProps()} />)
|
||||
const pointsInput = getByDisplayValue('3')
|
||||
fireEvent.change(pointsInput, {target: {value: '4'}})
|
||||
fireEvent.click(getByText('Save Mastery Scale'))
|
||||
|
@ -125,7 +125,7 @@ describe('default proficiency', () => {
|
|||
})
|
||||
|
||||
it('only sets focus on the first error', () => {
|
||||
const {getByDisplayValue, getByText} = render(<ProficiencyTable {...defaultProps} />)
|
||||
const {getByDisplayValue, getByText} = render(<ProficiencyTable {...defaultProps()} />)
|
||||
const masteryField = getByDisplayValue('Mastery')
|
||||
fireEvent.change(masteryField, {target: {value: ''}})
|
||||
const pointsInput = getByDisplayValue('3')
|
||||
|
@ -153,7 +153,7 @@ describe('default proficiency', () => {
|
|||
it('does not call save when canceling on the confirmation modal', async () => {
|
||||
const updateSpy = jest.fn(() => Promise.resolve())
|
||||
const {getByDisplayValue, getByText} = render(
|
||||
<ProficiencyTable {...defaultProps} update={updateSpy} />
|
||||
<ProficiencyTable {...defaultProps()} update={updateSpy} />
|
||||
)
|
||||
const masteryField = getByDisplayValue('Mastery')
|
||||
fireEvent.change(masteryField, {target: {value: 'Mastery2'}})
|
||||
|
@ -165,7 +165,7 @@ describe('default proficiency', () => {
|
|||
it('empty rating description does not call update', () => {
|
||||
const updateSpy = jest.fn(() => Promise.resolve())
|
||||
const {getByDisplayValue, getByText} = render(
|
||||
<ProficiencyTable {...defaultProps} update={updateSpy} />
|
||||
<ProficiencyTable {...defaultProps()} update={updateSpy} />
|
||||
)
|
||||
const masteryField = getByDisplayValue('Mastery')
|
||||
fireEvent.change(masteryField, {target: {value: ''}})
|
||||
|
@ -176,7 +176,7 @@ describe('default proficiency', () => {
|
|||
it('empty rating points does not call update', () => {
|
||||
const updateSpy = jest.fn(() => Promise.resolve())
|
||||
const {getByDisplayValue, getByText} = render(
|
||||
<ProficiencyTable {...defaultProps} update={updateSpy} />
|
||||
<ProficiencyTable {...defaultProps()} update={updateSpy} />
|
||||
)
|
||||
const pointsInput = getByDisplayValue('3')
|
||||
fireEvent.change(pointsInput, {target: {value: ''}})
|
||||
|
@ -187,7 +187,7 @@ describe('default proficiency', () => {
|
|||
it('invalid rating points does not call update', () => {
|
||||
const updateSpy = jest.fn(() => Promise.resolve())
|
||||
const {getByDisplayValue, getByText} = render(
|
||||
<ProficiencyTable {...defaultProps} update={updateSpy} />
|
||||
<ProficiencyTable {...defaultProps()} update={updateSpy} />
|
||||
)
|
||||
const pointsInput = getByDisplayValue('3')
|
||||
fireEvent.change(pointsInput, {target: {value: '1.1.1'}})
|
||||
|
@ -198,7 +198,7 @@ describe('default proficiency', () => {
|
|||
it('increasing rating points does call update', () => {
|
||||
const updateSpy = jest.fn(() => Promise.resolve())
|
||||
const {getByDisplayValue, getByText} = render(
|
||||
<ProficiencyTable {...defaultProps} update={updateSpy} />
|
||||
<ProficiencyTable {...defaultProps()} update={updateSpy} />
|
||||
)
|
||||
const pointsInput = getByDisplayValue('3')
|
||||
fireEvent.change(pointsInput, {target: {value: '1000'}})
|
||||
|
@ -210,7 +210,7 @@ describe('default proficiency', () => {
|
|||
it('negative rating points does not call update', () => {
|
||||
const updateSpy = jest.fn(() => Promise.resolve())
|
||||
const {getByDisplayValue, getByText} = render(
|
||||
<ProficiencyTable {...defaultProps} update={updateSpy} />
|
||||
<ProficiencyTable {...defaultProps()} update={updateSpy} />
|
||||
)
|
||||
const pointsInput = getByDisplayValue('3')
|
||||
fireEvent.change(pointsInput, {target: {value: '-10'}})
|
||||
|
@ -219,16 +219,13 @@ describe('default proficiency', () => {
|
|||
})
|
||||
|
||||
it('save button is initially disabled', () => {
|
||||
const {getByText} = render(<ProficiencyTable {...defaultProps} />)
|
||||
const {getByText} = render(<ProficiencyTable {...defaultProps()} />)
|
||||
const saveButton = getByText('Save Mastery Scale').closest('button')
|
||||
expect(saveButton.disabled).toEqual(true)
|
||||
})
|
||||
|
||||
it('save errors do not disable the save button', () => {
|
||||
const updateSpy = jest.fn(() => Promise.reject())
|
||||
const {getByText, getByDisplayValue} = render(
|
||||
<ProficiencyTable {...defaultProps} update={updateSpy} />
|
||||
)
|
||||
const {getByText, getByDisplayValue} = render(<ProficiencyTable {...defaultProps()} />)
|
||||
|
||||
const pointsInput = getByDisplayValue('3')
|
||||
fireEvent.change(pointsInput, {target: {value: '100'}})
|
||||
|
@ -237,12 +234,37 @@ describe('default proficiency', () => {
|
|||
const saveButton = getByText('Save Mastery Scale').closest('button')
|
||||
expect(saveButton.disabled).toEqual(false)
|
||||
})
|
||||
|
||||
it('calls onNotifyPendingChanges when changes data', async () => {
|
||||
const onNotifyPendingChangesSpy = jest.fn()
|
||||
const update = () => Promise.resolve()
|
||||
const {getByText, getByDisplayValue} = render(
|
||||
<ProficiencyTable
|
||||
{...defaultProps()}
|
||||
onNotifyPendingChanges={onNotifyPendingChangesSpy}
|
||||
update={update}
|
||||
/>
|
||||
)
|
||||
|
||||
const pointsInput = getByDisplayValue('3')
|
||||
fireEvent.change(pointsInput, {target: {value: '100'}})
|
||||
fireEvent.click(getByText('Save Mastery Scale'))
|
||||
fireEvent.click(getByText('Save'))
|
||||
|
||||
await wait(() => {
|
||||
expect(onNotifyPendingChangesSpy.mock.calls.length).toBe(2)
|
||||
// first call first argument
|
||||
expect(onNotifyPendingChangesSpy.mock.calls[0][0]).toBe(true)
|
||||
// second call first argument
|
||||
expect(onNotifyPendingChangesSpy.mock.calls[1][0]).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('custom proficiency', () => {
|
||||
it('renders two ratings that are deletable', () => {
|
||||
const customProficiencyProps = {
|
||||
...defaultProps,
|
||||
...defaultProps(),
|
||||
proficiency: {
|
||||
proficiencyRatingsConnection: {
|
||||
nodes: [
|
||||
|
@ -290,7 +312,7 @@ describe('custom proficiency', () => {
|
|||
mastery: false
|
||||
}
|
||||
const customProficiencyProps = {
|
||||
...defaultProps,
|
||||
...defaultProps(),
|
||||
proficiency: {
|
||||
proficiencyRatingsConnection: {
|
||||
nodes: [defaultRating1, defaultRating2, defaultRating3]
|
||||
|
@ -394,7 +416,6 @@ describe('custom proficiency', () => {
|
|||
|
||||
const pointsInput = getByDisplayValue('3')
|
||||
const masteryButton = getAllByText(/Mastery.*for mastery level/)[2].closest('label')
|
||||
const deleteButton = getAllByText(/Delete mastery level/)[0].closest('button')
|
||||
|
||||
fireEvent.change(pointsInput, {target: {value: '20'}})
|
||||
fireEvent.click(masteryButton)
|
||||
|
@ -415,7 +436,7 @@ describe('custom proficiency', () => {
|
|||
|
||||
it('renders one rating that is not deletable', () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
...defaultProps(),
|
||||
proficiency: {
|
||||
proficiencyRatingsConnection: {
|
||||
nodes: [
|
||||
|
@ -437,7 +458,7 @@ describe('custom proficiency', () => {
|
|||
|
||||
describe('can not manage', () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
...defaultProps(),
|
||||
canManage: false,
|
||||
proficiency: {
|
||||
proficiencyRatingsConnection: {
|
||||
|
@ -473,7 +494,7 @@ describe('custom proficiency', () => {
|
|||
|
||||
describe('confirmation modal', () => {
|
||||
it('renders correct text for the Account context', () => {
|
||||
const {getByDisplayValue, getByText} = render(<ProficiencyTable {...defaultProps} />)
|
||||
const {getByDisplayValue, getByText} = render(<ProficiencyTable {...defaultProps()} />)
|
||||
const pointsInput = getByDisplayValue('3')
|
||||
fireEvent.change(pointsInput, {target: {value: '1000'}})
|
||||
fireEvent.click(getByText('Save Mastery Scale'))
|
||||
|
@ -483,7 +504,7 @@ describe('confirmation modal', () => {
|
|||
|
||||
it('renders correct text for the Course context', () => {
|
||||
const {getByDisplayValue, getByText} = render(
|
||||
<ProficiencyTable {...defaultProps} contextType="Course" />
|
||||
<ProficiencyTable {...defaultProps()} contextType="Course" />
|
||||
)
|
||||
const pointsInput = getByDisplayValue('3')
|
||||
fireEvent.change(pointsInput, {target: {value: '1000'}})
|
||||
|
|
|
@ -20,8 +20,9 @@ import React from 'react'
|
|||
import {render, wait, fireEvent} from '@testing-library/react'
|
||||
import {MockedProvider} from '@apollo/react-testing'
|
||||
import moxios from 'moxios'
|
||||
import {ACCOUNT_OUTCOME_PROFICIENCY_QUERY, COURSE_OUTCOME_PROFICIENCY_QUERY} from '../api'
|
||||
import {ACCOUNT_OUTCOME_PROFICIENCY_QUERY} from '../api'
|
||||
import MasteryScale from '../index'
|
||||
import {masteryScalesGraphqlMocks as mocks} from '../../__tests__/mocks'
|
||||
|
||||
describe('MasteryScale', () => {
|
||||
beforeEach(() => {
|
||||
|
@ -52,70 +53,6 @@ describe('MasteryScale', () => {
|
|||
window.ENV = null
|
||||
})
|
||||
|
||||
const outcomeProficiency = {
|
||||
__typename: 'OutcomeProficiency',
|
||||
_id: '1',
|
||||
contextId: 1,
|
||||
contextType: 'Account',
|
||||
locked: false,
|
||||
proficiencyRatingsConnection: {
|
||||
__typename: 'ProficiencyRatingConnection',
|
||||
nodes: [
|
||||
{
|
||||
__typename: 'ProficiencyRating',
|
||||
_id: '2',
|
||||
color: '009606',
|
||||
description: 'Rating A',
|
||||
mastery: false,
|
||||
points: 9
|
||||
},
|
||||
{
|
||||
__typename: 'ProficiencyRating',
|
||||
_id: '6',
|
||||
color: 'EF4437',
|
||||
description: 'Rating B',
|
||||
mastery: false,
|
||||
points: 6
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
const mocks = [
|
||||
{
|
||||
request: {
|
||||
query: ACCOUNT_OUTCOME_PROFICIENCY_QUERY,
|
||||
variables: {
|
||||
contextId: '11'
|
||||
}
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
context: {
|
||||
__typename: 'Account',
|
||||
outcomeProficiency
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
request: {
|
||||
query: COURSE_OUTCOME_PROFICIENCY_QUERY,
|
||||
variables: {
|
||||
contextId: '12'
|
||||
}
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
context: {
|
||||
__typename: 'Course',
|
||||
outcomeProficiency
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
it('loads proficiency data', async () => {
|
||||
const {getByText, getByDisplayValue} = render(
|
||||
<MockedProvider mocks={mocks}>
|
||||
|
|
|
@ -29,13 +29,13 @@ import {
|
|||
} from './api'
|
||||
import {useQuery} from 'react-apollo'
|
||||
|
||||
const MasteryScale = ({contextType, contextId}) => {
|
||||
const MasteryScale = ({contextType, contextId, onNotifyPendingChanges}) => {
|
||||
const query =
|
||||
contextType === 'Course' ? COURSE_OUTCOME_PROFICIENCY_QUERY : ACCOUNT_OUTCOME_PROFICIENCY_QUERY
|
||||
|
||||
const {loading, error, data} = useQuery(query, {
|
||||
variables: {contextId},
|
||||
fetchPolicy: 'no-cache'
|
||||
fetchPolicy: process.env.NODE_ENV === 'test' ? undefined : 'no-cache'
|
||||
})
|
||||
|
||||
const [updateProficiencyRatingsError, setUpdateProficiencyRatingsError] = useState(null)
|
||||
|
@ -78,8 +78,9 @@ const MasteryScale = ({contextType, contextId}) => {
|
|||
const roles = ENV.PROFICIENCY_SCALES_ENABLED_ROLES || []
|
||||
const accountRoles = roles.filter(role => role.is_account_role)
|
||||
const canManage = ENV.PERMISSIONS.manage_proficiency_scales
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div data-testid="masteryScales">
|
||||
{canManage && contextType === 'Account' && (
|
||||
<p>
|
||||
<Text>
|
||||
|
@ -95,6 +96,7 @@ const MasteryScale = ({contextType, contextId}) => {
|
|||
proficiency={outcomeProficiency || undefined} // send undefined when value is null
|
||||
update={updateProficiencyRatings}
|
||||
updateError={updateProficiencyRatingsError}
|
||||
onNotifyPendingChanges={onNotifyPendingChanges}
|
||||
/>
|
||||
|
||||
{accountRoles.length > 0 && (
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import React, {useState, useEffect, useMemo} from 'react'
|
||||
import React, {useState, useEffect, useMemo, useRef} from 'react'
|
||||
import I18n from 'i18n!OutcomeManagement'
|
||||
import {Tabs} from '@instructure/ui-tabs'
|
||||
import MasteryScale from 'jsx/outcomes/MasteryScale'
|
||||
|
@ -40,25 +40,59 @@ export const OutcomePanel = () => {
|
|||
return null
|
||||
}
|
||||
|
||||
const OutcomeManagement = () => {
|
||||
export const OutcomeManagementWithoutGraphql = () => {
|
||||
const improvedManagement = ENV?.IMPROVED_OUTCOMES_MANAGEMENT
|
||||
const [selectedIndex, setSelectedIndex] = useState(() => {
|
||||
const tabs = {'#mastery_scale': 1, '#mastery_calculation': 2}
|
||||
return window.location.hash in tabs ? tabs[window.location.hash] : 0
|
||||
})
|
||||
|
||||
// Need to use a ref because a when normal setState is changed, this component will render
|
||||
// By rendering again, the handleTabChange will be a new function.
|
||||
// If we pass a new function to Tabs onRequestTabChange prop, it will recreate
|
||||
// the childs components, and we don't want that
|
||||
const hasUnsavedChangesRef = useRef(false)
|
||||
const setHasUnsavedChanges = hasUnsavedChanges =>
|
||||
(hasUnsavedChangesRef.current = hasUnsavedChanges)
|
||||
|
||||
const handleTabChange = (_, {index}) => {
|
||||
setSelectedIndex(index)
|
||||
if (hasUnsavedChangesRef.current) {
|
||||
/* eslint-disable no-restricted-globals */
|
||||
/* eslint-disable no-alert */
|
||||
if (
|
||||
confirm(I18n.t('Are you sure you want to proceed? Changes you made will not be saved.'))
|
||||
) {
|
||||
/* eslint-enable no-restricted-globals */
|
||||
/* eslint-enable no-alert */
|
||||
setHasUnsavedChanges(false)
|
||||
setSelectedIndex(index)
|
||||
}
|
||||
} else {
|
||||
setSelectedIndex(index)
|
||||
}
|
||||
}
|
||||
|
||||
const client = useMemo(() => createClient(), [])
|
||||
// close tab / load link
|
||||
useEffect(() => {
|
||||
const onBeforeUnload = e => {
|
||||
if (hasUnsavedChangesRef.current) {
|
||||
e.preventDefault()
|
||||
e.returnValue = true
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('beforeunload', onBeforeUnload)
|
||||
return () => {
|
||||
window.removeEventListener('beforeunload', onBeforeUnload)
|
||||
}
|
||||
}, [hasUnsavedChangesRef])
|
||||
|
||||
const [snakeContextType, contextId] = ENV.context_asset_string.split('_')
|
||||
|
||||
const contextType = snakeContextType === 'course' ? 'Course' : 'Account'
|
||||
|
||||
return (
|
||||
<ApolloProvider client={client}>
|
||||
<>
|
||||
{improvedManagement && <ManagementHeader />}
|
||||
<Tabs onRequestTabChange={handleTabChange}>
|
||||
<Tabs.Panel renderTitle={I18n.t('Manage')} isSelected={selectedIndex === 0}>
|
||||
|
@ -69,12 +103,30 @@ const OutcomeManagement = () => {
|
|||
)}
|
||||
</Tabs.Panel>
|
||||
<Tabs.Panel renderTitle={I18n.t('Mastery')} isSelected={selectedIndex === 1}>
|
||||
<MasteryScale contextType={contextType} contextId={contextId} />
|
||||
<MasteryScale
|
||||
onNotifyPendingChanges={setHasUnsavedChanges}
|
||||
contextType={contextType}
|
||||
contextId={contextId}
|
||||
/>
|
||||
</Tabs.Panel>
|
||||
<Tabs.Panel renderTitle={I18n.t('Calculation')} isSelected={selectedIndex === 2}>
|
||||
<MasteryCalculation contextType={contextType} contextId={contextId} />
|
||||
<MasteryCalculation
|
||||
onNotifyPendingChanges={setHasUnsavedChanges}
|
||||
contextType={contextType}
|
||||
contextId={contextId}
|
||||
/>
|
||||
</Tabs.Panel>
|
||||
</Tabs>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const OutcomeManagement = () => {
|
||||
const client = useMemo(() => createClient(), [])
|
||||
|
||||
return (
|
||||
<ApolloProvider client={client}>
|
||||
<OutcomeManagementWithoutGraphql />
|
||||
</ApolloProvider>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -24,7 +24,7 @@ import React from 'react'
|
|||
const OutcomeManagementPanel = ({contextType, contextId}) => {
|
||||
const isCourse = contextType === 'Course'
|
||||
return (
|
||||
<div className="management-panel">
|
||||
<div className="management-panel" data-testid="outcomeManagementPanel">
|
||||
<Billboard
|
||||
size="large"
|
||||
headingLevel="h3"
|
||||
|
|
|
@ -17,45 +17,162 @@
|
|||
*/
|
||||
|
||||
import React from 'react'
|
||||
import {mount, shallow} from 'enzyme'
|
||||
import OutcomeManagement, {OutcomePanel} from '../OutcomeManagement'
|
||||
import {render, fireEvent, act} from '@testing-library/react'
|
||||
import {MockedProvider} from '@apollo/react-testing'
|
||||
import {
|
||||
OutcomePanel,
|
||||
OutcomeManagementWithoutGraphql as OutcomeManagement
|
||||
} from '../OutcomeManagement'
|
||||
import {masteryCalculationGraphqlMocks, masteryScalesGraphqlMocks} from './mocks'
|
||||
|
||||
jest.useFakeTimers()
|
||||
|
||||
describe('OutcomeManagement', () => {
|
||||
const sharedExamples = () => {
|
||||
it('renders the OutcomeManagement and shows the "outcomes" div', () => {
|
||||
const wrapper = shallow(<OutcomeManagement />)
|
||||
expect(wrapper.find('OutcomePanel').exists()).toBe(true)
|
||||
document.body.innerHTML = '<div id="outcomes" style="display:none">Outcomes Tab</div>'
|
||||
render(<OutcomeManagement />)
|
||||
expect(document.getElementById('outcomes').style.display).toBe('block')
|
||||
})
|
||||
|
||||
it('does not render ManagementHeader', () => {
|
||||
const wrapper = shallow(<OutcomeManagement />)
|
||||
expect(wrapper.find('ManagementHeader').exists()).toBe(false)
|
||||
const {queryByTestId} = render(<OutcomeManagement />)
|
||||
expect(queryByTestId('managementHeader')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders ManagementHeader when improved outcomes enabled', () => {
|
||||
window.ENV.IMPROVED_OUTCOMES_MANAGEMENT = true
|
||||
const wrapper = shallow(<OutcomeManagement />)
|
||||
expect(wrapper.find('ManagementHeader').exists()).toBe(true)
|
||||
delete window.ENV.IMPROVED_OUTCOMES_MANAGEMENT
|
||||
})
|
||||
|
||||
it('renders OutcomeManagementPanel when improved outcomes enabled', () => {
|
||||
window.ENV.IMPROVED_OUTCOMES_MANAGEMENT = true
|
||||
const wrapper = shallow(<OutcomeManagement />)
|
||||
expect(wrapper.find('OutcomeManagementPanel').exists()).toBe(true)
|
||||
const {queryByTestId} = render(<OutcomeManagement />)
|
||||
expect(queryByTestId('managementHeader')).toBeInTheDocument()
|
||||
delete window.ENV.IMPROVED_OUTCOMES_MANAGEMENT
|
||||
})
|
||||
|
||||
it('does not render OutcomeManagementPanel when improved outcomes disabled', () => {
|
||||
const wrapper = shallow(<OutcomeManagement />)
|
||||
expect(wrapper.find('OutcomeManagementPanel').exists()).toBe(false)
|
||||
const {queryByTestId} = render(<OutcomeManagement />)
|
||||
expect(queryByTestId('outcomeManagementPanel')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders OutcomeManagementPanel when improved outcomes enabled', () => {
|
||||
window.ENV.IMPROVED_OUTCOMES_MANAGEMENT = true
|
||||
const {queryByTestId} = render(<OutcomeManagement />)
|
||||
expect(queryByTestId('outcomeManagementPanel')).toBeInTheDocument()
|
||||
delete window.ENV.IMPROVED_OUTCOMES_MANAGEMENT
|
||||
})
|
||||
|
||||
describe('Changes confirmation', () => {
|
||||
let originalConfirm, originalAddEventListener, unloadEventListener
|
||||
|
||||
beforeAll(() => {
|
||||
originalConfirm = window.confirm
|
||||
originalAddEventListener = window.addEventListener
|
||||
window.confirm = jest.fn(() => true)
|
||||
window.addEventListener = (eventName, callback) => {
|
||||
if (eventName === 'beforeunload') {
|
||||
unloadEventListener = callback
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
window.confirm = originalConfirm
|
||||
window.addEventListener = originalAddEventListener
|
||||
unloadEventListener = null
|
||||
})
|
||||
|
||||
it("Doesn't ask to confirm tab change when there is not change", () => {
|
||||
const {getByText} = render(
|
||||
<MockedProvider mocks={[...masteryCalculationGraphqlMocks, ...masteryScalesGraphqlMocks]}>
|
||||
<OutcomeManagement />
|
||||
</MockedProvider>
|
||||
)
|
||||
|
||||
fireEvent.click(getByText('Mastery'))
|
||||
fireEvent.click(getByText('Calculation'))
|
||||
|
||||
expect(window.confirm).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('Asks to confirm tab change when there is changes', () => {
|
||||
const {getByText, getByLabelText, getByTestId} = render(
|
||||
<MockedProvider mocks={[...masteryCalculationGraphqlMocks, ...masteryScalesGraphqlMocks]}>
|
||||
<OutcomeManagement />
|
||||
</MockedProvider>
|
||||
)
|
||||
|
||||
fireEvent.click(getByText('Calculation'))
|
||||
act(() => jest.runAllTimers())
|
||||
fireEvent.input(getByLabelText('Parameter'), {target: {value: ''}})
|
||||
fireEvent.click(getByText('Mastery'))
|
||||
act(() => jest.runAllTimers())
|
||||
expect(window.confirm).toHaveBeenCalled()
|
||||
expect(getByTestId('masteryScales')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("Doesn't change tabs when doesn't confirm", () => {
|
||||
// mock decline from user
|
||||
window.confirm = () => false
|
||||
|
||||
const {getByText, getByLabelText, queryByTestId} = render(
|
||||
<MockedProvider mocks={[...masteryCalculationGraphqlMocks, ...masteryScalesGraphqlMocks]}>
|
||||
<OutcomeManagement />
|
||||
</MockedProvider>
|
||||
)
|
||||
|
||||
fireEvent.click(getByText('Calculation'))
|
||||
act(() => jest.runAllTimers())
|
||||
fireEvent.input(getByLabelText('Parameter'), {target: {value: ''}})
|
||||
fireEvent.click(getByText('Mastery'))
|
||||
expect(queryByTestId('masteryScales')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("Allows to leave page when doesn't have changes", () => {
|
||||
const {getByText} = render(
|
||||
<MockedProvider mocks={[...masteryCalculationGraphqlMocks, ...masteryScalesGraphqlMocks]}>
|
||||
<OutcomeManagement />
|
||||
</MockedProvider>
|
||||
)
|
||||
|
||||
const calculationButton = getByText('Calculation')
|
||||
fireEvent.click(calculationButton)
|
||||
|
||||
act(() => jest.runAllTimers())
|
||||
|
||||
const e = jest.mock()
|
||||
e.preventDefault = jest.fn()
|
||||
unloadEventListener(e)
|
||||
expect(e.preventDefault).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("Doesn't Allow to leave page when has changes", async () => {
|
||||
const {getByText, getByLabelText} = render(
|
||||
<MockedProvider mocks={[...masteryCalculationGraphqlMocks, ...masteryScalesGraphqlMocks]}>
|
||||
<OutcomeManagement />
|
||||
</MockedProvider>
|
||||
)
|
||||
|
||||
const calculationButton = getByText('Calculation')
|
||||
fireEvent.click(calculationButton)
|
||||
|
||||
act(() => jest.runAllTimers())
|
||||
|
||||
const parameter = getByLabelText(/Parameter/)
|
||||
fireEvent.input(parameter, {target: {value: '88'}})
|
||||
|
||||
const e = jest.mock()
|
||||
e.preventDefault = jest.fn()
|
||||
unloadEventListener(e)
|
||||
expect(e.preventDefault).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
describe('account', () => {
|
||||
beforeEach(() => {
|
||||
window.ENV = {
|
||||
context_asset_string: 'account_1'
|
||||
context_asset_string: 'account_11',
|
||||
PERMISSIONS: {
|
||||
manage_proficiency_calculations: true
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -64,25 +181,15 @@ describe('OutcomeManagement', () => {
|
|||
})
|
||||
|
||||
sharedExamples()
|
||||
|
||||
it('passes accountId to the ProficiencyTable component', () => {
|
||||
const wrapper = shallow(<OutcomeManagement />)
|
||||
expect(wrapper.find('MasteryScale').prop('contextType')).toBe('Account')
|
||||
expect(wrapper.find('MasteryScale').prop('contextId')).toBe('1')
|
||||
})
|
||||
|
||||
it('passes accountId to the OutcomeManagementPanel component', () => {
|
||||
window.ENV.IMPROVED_OUTCOMES_MANAGEMENT = true
|
||||
const wrapper = shallow(<OutcomeManagement />)
|
||||
expect(wrapper.find('OutcomeManagementPanel').prop('contextType')).toBe('Account')
|
||||
expect(wrapper.find('OutcomeManagementPanel').prop('contextId')).toBe('1')
|
||||
})
|
||||
})
|
||||
|
||||
describe('course', () => {
|
||||
beforeEach(() => {
|
||||
window.ENV = {
|
||||
context_asset_string: 'course_2'
|
||||
context_asset_string: 'course_12',
|
||||
PERMISSIONS: {
|
||||
manage_proficiency_calculations: true
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -91,19 +198,6 @@ describe('OutcomeManagement', () => {
|
|||
})
|
||||
|
||||
sharedExamples()
|
||||
|
||||
it('passes courseId to the ProficiencyTable component', () => {
|
||||
const wrapper = shallow(<OutcomeManagement />)
|
||||
expect(wrapper.find('MasteryScale').prop('contextType')).toBe('Course')
|
||||
expect(wrapper.find('MasteryScale').prop('contextId')).toBe('2')
|
||||
})
|
||||
|
||||
it('passes courseId to the OutcomeManagementPanel component', () => {
|
||||
window.ENV.IMPROVED_OUTCOMES_MANAGEMENT = true
|
||||
const wrapper = shallow(<OutcomeManagement />)
|
||||
expect(wrapper.find('OutcomeManagementPanel').prop('contextType')).toBe('Course')
|
||||
expect(wrapper.find('OutcomeManagementPanel').prop('contextId')).toBe('2')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -117,13 +211,13 @@ describe('OutcomePanel', () => {
|
|||
})
|
||||
|
||||
it('sets style on mount', () => {
|
||||
mount(<OutcomePanel />)
|
||||
render(<OutcomePanel />)
|
||||
expect(document.getElementById('outcomes').style.display).toBe('block')
|
||||
})
|
||||
|
||||
it('sets style on unmount', () => {
|
||||
const wrapper = mount(<OutcomePanel />)
|
||||
wrapper.unmount()
|
||||
const {unmount} = render(<OutcomePanel />)
|
||||
unmount()
|
||||
expect(document.getElementById('outcomes').style.display).toBe('none')
|
||||
})
|
||||
})
|
||||
|
|
|
@ -0,0 +1,135 @@
|
|||
/*
|
||||
* Copyright (C) 2020 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {
|
||||
ACCOUNT_OUTCOME_PROFICIENCY_QUERY,
|
||||
COURSE_OUTCOME_PROFICIENCY_QUERY
|
||||
} from '../MasteryScale/api'
|
||||
|
||||
import {
|
||||
ACCOUNT_OUTCOME_CALCULATION_QUERY,
|
||||
COURSE_OUTCOME_CALCULATION_QUERY
|
||||
} from '../MasteryCalculation/api'
|
||||
|
||||
const outcomeCalculationMethod = {
|
||||
__typename: 'OutcomeCalculationMethod',
|
||||
_id: '1',
|
||||
contextType: 'Account',
|
||||
contextId: 1,
|
||||
calculationMethod: 'decaying_average',
|
||||
calculationInt: 65
|
||||
}
|
||||
|
||||
const outcomeProficiency = {
|
||||
__typename: 'OutcomeProficiency',
|
||||
_id: '1',
|
||||
contextId: 1,
|
||||
contextType: 'Account',
|
||||
locked: false,
|
||||
proficiencyRatingsConnection: {
|
||||
__typename: 'ProficiencyRatingConnection',
|
||||
nodes: [
|
||||
{
|
||||
__typename: 'ProficiencyRating',
|
||||
_id: '2',
|
||||
color: '009606',
|
||||
description: 'Rating A',
|
||||
mastery: false,
|
||||
points: 9
|
||||
},
|
||||
{
|
||||
__typename: 'ProficiencyRating',
|
||||
_id: '6',
|
||||
color: 'EF4437',
|
||||
description: 'Rating B',
|
||||
mastery: false,
|
||||
points: 6
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
export const masteryScalesGraphqlMocks = [
|
||||
{
|
||||
request: {
|
||||
query: ACCOUNT_OUTCOME_PROFICIENCY_QUERY,
|
||||
variables: {
|
||||
contextId: '11'
|
||||
}
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
context: {
|
||||
__typename: 'Account',
|
||||
outcomeProficiency
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
request: {
|
||||
query: COURSE_OUTCOME_PROFICIENCY_QUERY,
|
||||
variables: {
|
||||
contextId: '12'
|
||||
}
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
context: {
|
||||
__typename: 'Course',
|
||||
outcomeProficiency
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
export const masteryCalculationGraphqlMocks = [
|
||||
{
|
||||
request: {
|
||||
query: ACCOUNT_OUTCOME_CALCULATION_QUERY,
|
||||
variables: {
|
||||
contextId: '11'
|
||||
}
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
context: {
|
||||
__typename: 'Account',
|
||||
outcomeCalculationMethod
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
request: {
|
||||
query: COURSE_OUTCOME_CALCULATION_QUERY,
|
||||
variables: {
|
||||
contextId: '12'
|
||||
}
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
context: {
|
||||
__typename: 'Course',
|
||||
outcomeCalculationMethod
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
Loading…
Reference in New Issue