add grade selection to new moderation page

closes GRADE-1192

test plan:
 1. Setup a course with AMM and a moderated assignment
 2. Log in as various instructors and grade some students
 3. Log in as the moderator
 4. Visit the moderation page for the assignment
 5. Select some grades
 6. Verify the grades were selected
    * populated in Select element
    * highlighted in row
 7. Verify a flash message indicate successful selection
 8. Refresh the page
 9. Verify the selected grades remain selected

Change-Id: Ib07a5c7a1838ebc79bd660505f950a328d1eabfa
Reviewed-on: https://gerrit.instructure.com/151602
Tested-by: Jenkins
Reviewed-by: Adrian Packel <apackel@instructure.com>
Reviewed-by: Keith T. Garner <kgarner@instructure.com>
QA-Review: Gary Mei <gmei@instructure.com>
Product-Review: Sidharth Oberoi <soberoi@instructure.com>
This commit is contained in:
Jeremy Neander 2018-05-25 10:23:34 -05:00
parent 882df38a7b
commit c08bfca4c2
23 changed files with 1179 additions and 48 deletions

View File

@ -17,12 +17,13 @@
*/
import {Component} from 'react'
import {oneOf} from 'prop-types'
import {oneOf, shape} from 'prop-types'
import {connect} from 'react-redux'
import I18n from 'i18n!assignment_grade_summary'
import {showFlashAlert} from '../../../shared/FlashAlert'
import * as AssignmentActions from '../assignment/AssignmentActions'
import * as GradeActions from '../grades/GradeActions'
import * as StudentActions from '../students/StudentActions'
function enumeratedStatuses(actions) {
@ -82,6 +83,7 @@ class FlashMessageHolder extends Component {
static propTypes = {
loadStudentsStatus: oneOf(enumeratedStatuses(StudentActions)),
publishGradesStatus: oneOf(assignmentStatuses),
selectProvisionalGradeStatuses: shape({}).isRequired,
unmuteAssignmentStatus: oneOf(enumeratedStatuses(AssignmentActions))
}
@ -106,6 +108,28 @@ class FlashMessageHolder extends Component {
}
}
if (changes.selectProvisionalGradeStatuses) {
Object.keys(nextProps.selectProvisionalGradeStatuses).forEach(studentId => {
if (
nextProps.selectProvisionalGradeStatuses[studentId] !==
this.props.selectProvisionalGradeStatuses[studentId]
) {
const status = nextProps.selectProvisionalGradeStatuses[studentId]
if (status === GradeActions.SUCCESS) {
showFlashAlert({
message: I18n.t('Grade saved.'),
type: 'success'
})
} else if (status === GradeActions.FAILURE) {
showFlashAlert({
message: I18n.t('There was a problem saving the grade.'),
type: 'error'
})
}
}
})
}
if (changes.publishGradesStatus) {
announcePublishGradesStatus(nextProps.publishGradesStatus)
}
@ -124,6 +148,7 @@ function mapStateToProps(state) {
return {
loadStudentsStatus: state.students.loadStudentsStatus,
publishGradesStatus: state.assignment.publishGradesStatus,
selectProvisionalGradeStatuses: state.grades.selectProvisionalGradeStatuses,
unmuteAssignmentStatus: state.assignment.unmuteAssignmentStatus
}
}

View File

@ -0,0 +1,64 @@
/*
* Copyright (C) 2018 - 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 React, {Component} from 'react'
import {bool, number, shape, string} from 'prop-types'
import Text from '@instructure/ui-elements/lib/components/Text'
import View from '@instructure/ui-layout/lib/components/View'
import I18n from 'i18n!assignment_grade_summary'
export default class GradeIndicator extends Component {
static propTypes = {
gradeInfo: shape({
grade: string,
graderId: string.isRequired,
id: string.isRequired,
score: number,
selected: bool.isRequired,
studentId: string.isRequired
})
}
static defaultProps = {
gradeInfo: null
}
shouldComponentUpdate(nextProps) {
return Object.keys(nextProps).some(key => this.props[key] !== nextProps[key])
}
render() {
const {gradeInfo} = this.props
const selected = gradeInfo && gradeInfo.selected
let textColor = selected ? 'primary-inverse' : 'primary'
textColor = gradeInfo ? textColor : 'secondary'
return (
<View
background={selected ? 'inverse' : 'default'}
borderRadius="small"
borderWidth={selected ? 'small' : '0'}
padding="xx-small small"
>
<Text color={textColor}>
{gradeInfo && gradeInfo.score != null ? I18n.n(gradeInfo.score) : ''}
</Text>
</View>
)
}
}

View File

@ -0,0 +1,123 @@
/*
* Copyright (C) 2018 - 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 React, {Component} from 'react'
import {arrayOf, func, oneOf, shape, string} from 'prop-types'
import ScreenReaderContent from '@instructure/ui-a11y/lib/components/ScreenReaderContent'
import Select from '@instructure/ui-forms/lib/components/Select'
import I18n from 'i18n!assignment_grade_summary'
import {FAILURE, STARTED, SUCCESS} from '../../grades/GradeActions'
export default class GradeSelect extends Component {
static propTypes = {
graders: arrayOf(
shape({
graderName: string,
graderId: string.isRequired
})
).isRequired,
grades: shape({}).isRequired,
onClose: func,
onOpen: func,
onSelect: func,
selectProvisionalGradeStatus: oneOf([FAILURE, STARTED, SUCCESS]),
studentName: string.isRequired
}
static defaultProps = {
onClose() {},
onOpen() {},
onSelect() {},
selectProvisionalGradeStatus: null
}
constructor(props) {
super(props)
this.handleSelect = this.handleSelect.bind(this)
}
shouldComponentUpdate(nextProps) {
return Object.keys(nextProps).some(key => this.props[key] !== nextProps[key])
}
handleSelect(_event, option) {
const gradeInfo = this.props.grades[option.value]
if (!gradeInfo.selected && this.props.onSelect) {
this.props.onSelect(gradeInfo)
}
}
render() {
const {graders, grades} = this.props
const gradeOptions = []
let selectedOption
for (let i = 0; i < graders.length; i++) {
const grader = graders[i]
const gradeInfo = grades[grader.graderId]
if (gradeInfo != null) {
const option = {
label: `${I18n.n(gradeInfo.score)} (${grader.graderName})`,
value: gradeInfo.graderId
}
gradeOptions.push(option)
if (gradeInfo.selected) {
selectedOption = option
}
}
}
if (!selectedOption) {
gradeOptions.unshift({label: '', value: 'no-selection'})
selectedOption = 'no-selection'
}
return (
<Select
aria-readonly={!this.props.onSelect || this.props.selectProvisionalGradeStatus === STARTED}
key={
/*
* TODO: This forces a unique instance per-student, which hurts
* performance. Remove this key entirely once the commit from
* INSTUI-1199 has been published to npm and pulled into Canvas.
*/
this.props.studentName
}
label={
<ScreenReaderContent>
{I18n.t('Grade for %{studentName}', {studentName: this.props.studentName})}
</ScreenReaderContent>
}
onChange={this.handleSelect}
onClose={this.props.onClose}
onOpen={this.props.onOpen}
selectedOption={selectedOption}
>
{gradeOptions.map(gradeOption => (
<option key={gradeOption.value} value={gradeOption.value}>
{gradeOption.label}
</option>
))}
</Select>
)
}
}

View File

@ -36,12 +36,18 @@ export default class Grid extends Component {
).isRequired,
grades: shape({}).isRequired,
horizontalScrollRef: func.isRequired,
onGradeSelect: func,
rows: arrayOf(
shape({
studentId: string.isRequired,
studentName: string.isRequired
}).isRequired
).isRequired
).isRequired,
selectProvisionalGradeStatuses: shape({}).isRequired
}
static defaultProps = {
onGradeSelect: null
}
shouldComponentUpdate(nextProps) {
@ -62,19 +68,20 @@ export default class Grid extends Component {
<Text>{I18n.t('Student')}</Text>
</th>
{this.props.graders.map((grader, index) => (
{this.props.graders.map(grader => (
<th
className="GradesGrid__GraderHeader"
key={grader.graderId}
role="columnheader"
scope="col"
>
<Text>
{grader.graderName ||
I18n.t('Grader %{graderNumber}', {graderNumber: I18n.n(index + 1)})}
</Text>
<Text>{grader.graderName}</Text>
</th>
))}
<th className="GradesGrid__FinalGradeHeader" role="columnheader" scope="col">
<Text>{I18n.t('Final Grade')}</Text>
</th>
</tr>
</thead>
@ -84,7 +91,11 @@ export default class Grid extends Component {
graders={this.props.graders}
grades={this.props.grades[row.studentId]}
key={index /* index used for performance reasons */}
onGradeSelect={this.props.onGradeSelect}
row={row}
selectProvisionalGradeStatus={
this.props.selectProvisionalGradeStatuses[row.studentId]
}
/>
))}
</tbody>

View File

@ -17,14 +17,12 @@
*/
import React, {Component} from 'react'
import {arrayOf, shape, string} from 'prop-types'
import {arrayOf, func, oneOf, shape, string} from 'prop-types'
import Text from '@instructure/ui-elements/lib/components/Text'
import I18n from 'i18n!assignment_grade_summary'
function getGrade(graderId, grades) {
const gradeInfo = grades[graderId]
return gradeInfo && gradeInfo.score != null ? I18n.n(gradeInfo.score) : ''
}
import {FAILURE, STARTED, SUCCESS} from '../../grades/GradeActions'
import GradeIndicator from './GradeIndicator'
import GradeSelect from './GradeSelect'
export default class GridRow extends Component {
static propTypes = {
@ -35,14 +33,18 @@ export default class GridRow extends Component {
})
).isRequired,
grades: shape({}),
onGradeSelect: func,
row: shape({
studentId: string.isRequired,
studentName: string.isRequired
}).isRequired
}).isRequired,
selectProvisionalGradeStatus: oneOf([FAILURE, STARTED, SUCCESS])
}
static defaultProps = {
grades: {}
grades: {},
onGradeSelect: null,
selectProvisionalGradeStatus: null
}
shouldComponentUpdate(nextProps) {
@ -61,10 +63,20 @@ export default class GridRow extends Component {
return (
<td className={classNames.join(' ')} key={grader.graderId} role="cell">
<Text>{getGrade(grader.graderId, this.props.grades)}</Text>
<GradeIndicator gradeInfo={this.props.grades[grader.graderId]} />
</td>
)
})}
<td className="GradesGrid__FinalGradeCell" role="cell">
<GradeSelect
graders={this.props.graders}
grades={this.props.grades}
onSelect={this.props.onGradeSelect}
selectProvisionalGradeStatus={this.props.selectProvisionalGradeStatus}
studentName={this.props.row.studentName}
/>
</td>
</tr>
)
}

View File

@ -17,7 +17,7 @@
*/
import React, {Component} from 'react'
import {arrayOf, shape, string} from 'prop-types'
import {arrayOf, func, shape, string} from 'prop-types'
import View from '@instructure/ui-layout/lib/components/View'
import I18n from 'i18n!assignment_grade_summary'
@ -56,6 +56,8 @@ export default class GradesGrid extends Component {
})
).isRequired,
grades: shape({}).isRequired,
onGradeSelect: func,
selectProvisionalGradeStatuses: shape({}).isRequired,
students: arrayOf(
shape({
displayName: string,
@ -64,6 +66,10 @@ export default class GradesGrid extends Component {
).isRequired
}
static defaultProps = {
onGradeSelect: null
}
constructor(props) {
super(props)
@ -98,7 +104,9 @@ export default class GradesGrid extends Component {
graders={this.props.graders}
grades={this.props.grades}
horizontalScrollRef={props.horizontalScrollRef}
onGradeSelect={this.props.onGradeSelect}
rows={rows}
selectProvisionalGradeStatuses={this.props.selectProvisionalGradeStatuses}
/>
)}
</FocusableView>

View File

@ -17,13 +17,14 @@
*/
import React, {Component} from 'react'
import {arrayOf, func, shape, string} from 'prop-types'
import {arrayOf, bool, func, shape, string} from 'prop-types'
import {connect} from 'react-redux'
import Spinner from '@instructure/ui-elements/lib/components/Spinner'
import View from '@instructure/ui-layout/lib/components/View'
import I18n from 'i18n!assignment_grade_summary'
import '../../../context_cards/StudentContextCardTrigger'
import {selectProvisionalGrade} from '../grades/GradeActions'
import {loadStudents} from '../students/StudentActions'
import FlashMessageHolder from './FlashMessageHolder'
import GradesGrid from './GradesGrid'
@ -36,8 +37,11 @@ class Layout extends Component {
graderId: string.isRequired
})
).isRequired,
gradesPublished: bool.isRequired,
loadStudents: func.isRequired,
provisionalGrades: shape({}).isRequired,
selectGrade: func.isRequired,
selectProvisionalGradeStatuses: shape({}).isRequired,
students: arrayOf(
shape({
id: string.isRequired
@ -52,6 +56,8 @@ class Layout extends Component {
}
render() {
const onGradeSelect = this.props.gradesPublished ? null : this.props.selectGrade
return (
<div>
<FlashMessageHolder />
@ -64,6 +70,8 @@ class Layout extends Component {
<GradesGrid
graders={this.props.graders}
grades={this.props.provisionalGrades}
onGradeSelect={onGradeSelect}
selectProvisionalGradeStatuses={this.props.selectProvisionalGradeStatuses}
students={this.props.students}
/>
) : (
@ -79,7 +87,9 @@ class Layout extends Component {
function mapStateToProps(state) {
return {
graders: state.context.graders,
gradesPublished: state.assignment.assignment.gradesPublished,
provisionalGrades: state.grades.provisionalGrades,
selectProvisionalGradeStatuses: state.grades.selectProvisionalGradeStatuses,
students: state.students.list
}
}
@ -88,8 +98,15 @@ function mapDispatchToProps(dispatch) {
return {
loadStudents() {
dispatch(loadStudents())
},
selectGrade(gradeInfo) {
dispatch(selectProvisionalGrade(gradeInfo))
}
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Layout)
export default connect(
mapStateToProps,
mapDispatchToProps
)(Layout)

View File

@ -16,6 +16,8 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import I18n from 'i18n!assignment_grade_summary'
function normalizeGraders() {
const graders = ENV.GRADERS.map(grader => ({
graderId: grader.user_id || grader.anonymous_id,
@ -25,6 +27,13 @@ function normalizeGraders() {
graders.sort((a, b) => (a.graderId < b.graderId ? -1 : 1))
graders.forEach((grader, index) => {
/* eslint-disable no-param-reassign */
grader.graderName =
grader.graderName || I18n.t('Grader %{graderNumber}', {graderNumber: I18n.n(index + 1)})
/* eslint-enable no-param-reassign */
})
return graders
}

View File

@ -16,8 +16,40 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import * as GradesApi from './GradesApi'
export const ADD_PROVISIONAL_GRADES = 'ADD_PROVISIONAL_GRADES'
export const FAILURE = 'FAILURE'
export const SET_SELECTED_PROVISIONAL_GRADE = 'SET_SELECTED_PROVISIONAL_GRADE'
export const SET_SELECT_PROVISIONAL_GRADE_STATUS = 'SET_SELECT_PROVISIONAL_GRADE_STATUS'
export const STARTED = 'STARTED'
export const SUCCESS = 'SUCCESS'
export function addProvisionalGrades(provisionalGrades) {
return {type: ADD_PROVISIONAL_GRADES, payload: {provisionalGrades}}
}
export function setSelectProvisionalGradeStatus(gradeInfo, status) {
return {type: SET_SELECT_PROVISIONAL_GRADE_STATUS, status, payload: {gradeInfo, status}}
}
export function setSelectedProvisionalGrade(gradeInfo) {
return {type: SET_SELECTED_PROVISIONAL_GRADE, payload: {gradeInfo}}
}
export function selectProvisionalGrade(gradeInfo) {
return function(dispatch, getState) {
const {assignment} = getState().assignment
dispatch(setSelectProvisionalGradeStatus(gradeInfo, STARTED))
GradesApi.selectProvisionalGrade(assignment.courseId, assignment.id, gradeInfo.id)
.then(() => {
dispatch(setSelectedProvisionalGrade(gradeInfo))
dispatch(setSelectProvisionalGradeStatus(gradeInfo, SUCCESS))
})
.catch(() => {
dispatch(setSelectProvisionalGradeStatus(gradeInfo, FAILURE))
})
}
}

View File

@ -0,0 +1,26 @@
/*
* Copyright (C) 2018 - 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 axios from 'axios'
/* eslint-disable import/prefer-default-export */
export function selectProvisionalGrade(courseId, assignmentId, provisionalGradeId) {
const url = `/api/v1/courses/${courseId}/assignments/${assignmentId}/provisional_grades/${provisionalGradeId}/select`
return axios.put(url)
}

View File

@ -17,7 +17,11 @@
*/
import buildReducer from '../buildReducer'
import {ADD_PROVISIONAL_GRADES} from './GradeActions'
import {
ADD_PROVISIONAL_GRADES,
SET_SELECTED_PROVISIONAL_GRADE,
SET_SELECT_PROVISIONAL_GRADE_STATUS
} from './GradeActions'
function addProvisionalGrades(state, grades) {
const provisionalGrades = {...state.provisionalGrades}
@ -28,11 +32,37 @@ function addProvisionalGrades(state, grades) {
return {...state, provisionalGrades}
}
function setSelectedProvisionalGrade(state, gradeInfo) {
const provisionalGrades = {...state.provisionalGrades}
const studentGrades = {...provisionalGrades[gradeInfo.studentId]}
Object.keys(studentGrades).forEach(graderId => {
if (studentGrades[graderId].selected) {
studentGrades[graderId] = {...studentGrades[graderId], selected: false}
}
})
studentGrades[gradeInfo.graderId] = {...gradeInfo, selected: true}
provisionalGrades[gradeInfo.studentId] = studentGrades
return {...state, provisionalGrades}
}
function setSelectProvisionalGradeStatus(state, gradeInfo, status) {
const selectProvisionalGradeStatuses = {...state.selectProvisionalGradeStatuses}
selectProvisionalGradeStatuses[gradeInfo.studentId] = status
return {...state, selectProvisionalGradeStatuses}
}
const handlers = {}
handlers[ADD_PROVISIONAL_GRADES] = (state, action) =>
addProvisionalGrades(state, action.payload.provisionalGrades)
handlers[ADD_PROVISIONAL_GRADES] = (state, {payload}) =>
addProvisionalGrades(state, payload.provisionalGrades)
handlers[SET_SELECTED_PROVISIONAL_GRADE] = (state, {payload}) =>
setSelectedProvisionalGrade(state, payload.gradeInfo)
handlers[SET_SELECT_PROVISIONAL_GRADE_STATUS] = (state, {payload}) =>
setSelectProvisionalGradeStatus(state, payload.gradeInfo, payload.status)
export default buildReducer(handlers, {
provisionalGrades: {}
provisionalGrades: {},
selectProvisionalGradeStatuses: {}
})

View File

@ -21,6 +21,7 @@
$headerHeight: 50px;
$rowHeight: 50px;
$graderColumnWidth: 156px;
$finalGradeColumnWidth: 176px;
$studentColumnWidth: 200px;
$headerBorderColor: rgb(199, 205, 209);
@ -93,7 +94,8 @@ $headerBorderColor: rgb(199, 205, 209);
line-height: $headerHeight;
}
.GradesGrid__GraderHeader {
.GradesGrid__GraderHeader,
.GradesGrid__FinalGradeHeader {
max-width: $graderColumnWidth;
min-width: $graderColumnWidth;
overflow: hidden;
@ -102,6 +104,15 @@ $headerBorderColor: rgb(199, 205, 209);
text-overflow: ellipsis;
}
.GradesGrid__GraderHeader {
width: $graderColumnWidth;
}
.GradesGrid__FinalGradeHeader {
max-width: $finalGradeColumnWidth;
min-width: $finalGradeColumnWidth;
}
.GradesGrid__StudentColumnHeader {
max-width: $studentColumnWidth;
min-width: $studentColumnWidth;
@ -124,8 +135,14 @@ $headerBorderColor: rgb(199, 205, 209);
.GradesGrid__ProvisionalGradeCell {
max-width: $graderColumnWidth;
padding: 0 $ic-sp;
padding: 0;
text-align: direction(left);
}
.GradesGrid__FinalGradeCell {
max-width: $finalGradeColumnWidth;
padding: 0 0 0 $ic-sp;
text-align: center;
}
/* End GradesGrid */

View File

@ -22,6 +22,7 @@ import {Provider} from 'react-redux'
import * as FlashAlert from 'jsx/shared/FlashAlert'
import * as AssignmentActions from 'jsx/assignments/GradeSummary/assignment/AssignmentActions'
import * as GradeActions from 'jsx/assignments/GradeSummary/grades/GradeActions'
import * as StudentActions from 'jsx/assignments/GradeSummary/students/StudentActions'
import FlashMessageHolder from 'jsx/assignments/GradeSummary/components/FlashMessageHolder'
import configureStore from 'jsx/assignments/GradeSummary/configureStore'
@ -81,6 +82,64 @@ QUnit.module('GradeSummary FlashMessageHolder', suiteHooks => {
})
})
QUnit.module('when a provisional grade selection succeeds', hooks => {
hooks.beforeEach(() => {
mountComponent()
const gradeInfo = {
grade: 'A',
graderId: '1101',
id: '4601',
score: 10,
selected: false,
studentId: '1111'
}
store.dispatch(GradeActions.setSelectProvisionalGradeStatus(gradeInfo, GradeActions.SUCCESS))
})
test('displays a flash alert', () => {
strictEqual(FlashAlert.showFlashAlert.callCount, 1)
})
test('uses the success type', () => {
const {type} = FlashAlert.showFlashAlert.lastCall.args[0]
equal(type, 'success')
})
test('includes a "Grade saved" message', () => {
const {message} = FlashAlert.showFlashAlert.lastCall.args[0]
equal(message, 'Grade saved.')
})
})
QUnit.module('when a provisional grade selection fails', hooks => {
hooks.beforeEach(() => {
mountComponent()
const gradeInfo = {
grade: 'A',
graderId: '1101',
id: '4601',
score: 10,
selected: false,
studentId: '1111'
}
store.dispatch(GradeActions.setSelectProvisionalGradeStatus(gradeInfo, GradeActions.FAILURE))
})
test('displays a flash alert', () => {
strictEqual(FlashAlert.showFlashAlert.callCount, 1)
})
test('uses the error type', () => {
const {type} = FlashAlert.showFlashAlert.lastCall.args[0]
equal(type, 'error')
})
test('includes a message about saving the grade', () => {
const {message} = FlashAlert.showFlashAlert.lastCall.args[0]
equal(message, 'There was a problem saving the grade.')
})
})
test('does not display a flash alert when publishing grades starts', () => {
mountComponent()
store.dispatch(AssignmentActions.setPublishGradesStatus(AssignmentActions.STARTED))

View File

@ -0,0 +1,86 @@
/*
* Copyright (C) 2018 - 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 React from 'react'
import {mount} from 'enzyme'
import GradeIndicator from 'jsx/assignments/GradeSummary/components/GradesGrid/GradeIndicator'
QUnit.module('GradeSummary GradeIndicator', suiteHooks => {
let $container
let props
let wrapper
suiteHooks.beforeEach(() => {
$container = document.createElement('div')
document.body.appendChild($container)
props = {
gradeInfo: {
grade: 'A',
graderId: '1101',
id: '4601',
score: 10,
selected: false,
studentId: '1111'
}
}
})
suiteHooks.afterEach(() => {
wrapper.unmount()
$container.remove()
})
function mountComponent() {
wrapper = mount(<GradeIndicator {...props} />, {attachTo: $container})
}
test('displays the score', () => {
mountComponent()
strictEqual(wrapper.text(), '10')
})
test('displays a zero score', () => {
props.gradeInfo.score = 0
mountComponent()
strictEqual(wrapper.text(), '0')
})
test('displays "" (en dash) when there is no grade', () => {
delete props.gradeInfo
mountComponent()
strictEqual(wrapper.text(), '')
})
test('changes the background color when the grade is selected', () => {
mountComponent()
const style = window.getComputedStyle(wrapper.getDOMNode())
const backgroundColorBefore = style.backgroundColor
wrapper.setProps({gradeInfo: {...props.gradeInfo, selected: true}})
notEqual(style.backgroundColor, backgroundColorBefore)
})
test('changes the text color when the grade is selected', () => {
mountComponent()
const style = window.getComputedStyle(wrapper.childAt(0).getDOMNode())
const colorBefore = style.color
wrapper.setProps({gradeInfo: {...props.gradeInfo, selected: true}})
notEqual(style.color, colorBefore)
})
})

View File

@ -0,0 +1,257 @@
/*
* Copyright (C) 2018 - 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 React from 'react'
import {mount} from 'enzyme'
import GradeSelect from 'jsx/assignments/GradeSummary/components/GradesGrid/GradeSelect'
import {FAILURE, STARTED, SUCCESS} from 'jsx/assignments/GradeSummary/grades/GradeActions'
QUnit.module('GradeSummary GradeSelect', suiteHooks => {
let props
let qunitTimeout
let resolveOpenCloseState
let selectedGrade
let wrapper
suiteHooks.beforeEach(() => {
qunitTimeout = QUnit.config.testTimeout
QUnit.config.testTimeout = 500 // prevent accidental unresolved async
selectedGrade = null
props = {
graders: [
{graderId: '1101', graderName: 'Miss Frizzle'},
{graderId: '1102', graderName: 'Mr. Keating'}
],
grades: {
1101: {
grade: 'A',
graderId: '1101',
id: '4601',
score: 10,
selected: false,
studentId: '1111'
},
1102: {
grade: 'B',
graderId: '1102',
id: '4602',
score: 8.9,
selected: true,
studentId: '1111'
}
},
onClose() {
resolveOpenCloseState()
},
onOpen() {
resolveOpenCloseState()
},
onSelect(gradeInfo) {
selectedGrade = gradeInfo
},
selectProvisionalGradeStatus: null,
studentName: 'Adam Jones'
}
})
suiteHooks.afterEach(() => {
wrapper.unmount()
QUnit.config.testTimeout = qunitTimeout
})
function mountComponent() {
wrapper = mount(<GradeSelect {...props} />)
}
function getOptionList() {
const controlledContentId = wrapper.find('input').prop('aria-controls')
return document.getElementById(controlledContentId)
}
function getOptions() {
const $list = getOptionList()
const $items = $list.querySelectorAll('li[role="option"]')
return Array.from($items)
}
function getOptionLabels() {
return getOptions().map($option => $option.textContent.trim())
}
function openSelect() {
return new Promise(resolve => {
resolveOpenCloseState = resolve
wrapper.find('input').simulate('click')
})
}
function selectOption(optionLabel) {
return openSelect().then(
() =>
new Promise(resolve => {
resolveOpenCloseState = resolve
const $option = getOptions().find($el => $el.textContent.trim() === optionLabel)
$option.click()
})
)
}
function getTextInputValue() {
return wrapper.find('input').getDOMNode().value
}
function labelForGrader(graderId) {
const gradeInfo = props.grades[graderId]
const grader = props.graders.find(g => g.graderId === graderId)
return `${gradeInfo.score} (${grader.graderName})`
}
test('renders a text input', () => {
mountComponent()
const input = wrapper.find('input[type="text"]')
strictEqual(input.length, 1)
})
test('uses the student name for a label', () => {
mountComponent()
const label = wrapper.find('label')
strictEqual(label.text(), 'Grade for Adam Jones')
})
test('includes an option for each grader who graded', () => {
mountComponent()
strictEqual(getOptions().length, 2)
})
test('displays the grade and grader name as option labels', () => {
mountComponent()
deepEqual(getOptionLabels(), [labelForGrader('1101'), labelForGrader('1102')])
})
test('sets as the input value the selected provisional grade', () => {
mountComponent()
equal(getTextInputValue(), labelForGrader('1102'))
})
test('calls the onSelect prop when an option is clicked', async () => {
props.onSelect = sinon.spy()
mountComponent()
await selectOption(labelForGrader('1101'))
strictEqual(props.onSelect.callCount, 1)
})
test('includes the related grade info when calling onSelect', async () => {
mountComponent()
await selectOption(labelForGrader('1101'))
deepEqual(selectedGrade, props.grades[1101])
})
test('does not call the onSelect prop when the option for the selected grade is clicked', async () => {
props.onSelect = sinon.spy()
mountComponent()
await selectOption(labelForGrader('1102'))
strictEqual(props.onSelect.callCount, 0)
})
test('sets the input to read-only when not given an onSelect prop', () => {
props.onSelect = null
mountComponent()
const input = wrapper.find('input[type="text"]')
strictEqual(input.prop('aria-readonly'), true)
})
test('has no effect when an option is clicked and not given an onSelect prop', async () => {
props.onSelect = null
mountComponent()
await selectOption(labelForGrader('1102'))
ok('component gracefully ignores the event')
})
test('sets the input to read-only when grade selection is pending', () => {
props.selectProvisionalGradeStatus = STARTED
mountComponent()
const input = wrapper.find('input[type="text"]')
strictEqual(input.prop('aria-readonly'), true)
})
test('enables the input when grade selection was successful', () => {
props.selectProvisionalGradeStatus = SUCCESS
mountComponent()
const input = wrapper.find('input[type="text"]')
strictEqual(input.prop('aria-disabled'), null)
})
test('enables the input when grade selection has failed', () => {
props.selectProvisionalGradeStatus = FAILURE
mountComponent()
const input = wrapper.find('input[type="text"]')
strictEqual(input.prop('aria-disabled'), null)
})
QUnit.module('when no grade has been selected', hooks => {
hooks.beforeEach(() => {
props.grades[1102].selected = false
})
test('includes an option for "no selection"', () => {
mountComponent()
strictEqual(getOptionLabels()[0], '')
})
test('includes an option for each grader who graded', () => {
mountComponent()
strictEqual(getOptions().length, 3) // two graders plus "no selection" option
})
test('set the input value to "" (en dash)', () => {
mountComponent()
strictEqual(getTextInputValue(), '')
})
})
QUnit.module('when the component instance is reused for another student', hooks => {
/*
* The purpose of this context is to help ensure that component instance
* reuse does not result in state-based bugs. When updating input components
* like Select, updating internal state can easily lead to mismatches
* between old state and new props.
*/
hooks.beforeEach(() => {
mountComponent()
props = JSON.parse(JSON.stringify(props))
props.studentName = 'Betty Ford'
props.grades[1101].studentId = '1112'
props.grades[1102].studentId = '1112'
})
test('includes an option for "no selection" when the student has no selected grade', () => {
props.grades[1102].selected = false
wrapper.setProps(props)
strictEqual(getOptions().length, 3) // two graders plus "no selection" option
})
test('excludes the option for "no selection" when the student has a selected grade', () => {
wrapper.setProps(props)
strictEqual(getOptions().length, 2) // only the two graders
})
})
})

View File

@ -20,6 +20,7 @@ import React from 'react'
import {mount} from 'enzyme'
import GridRow from 'jsx/assignments/GradeSummary/components/GradesGrid/GridRow'
import {STARTED} from 'jsx/assignments/GradeSummary/grades/GradeActions'
QUnit.module('GradeSummary GridRow', suiteHooks => {
let props
@ -49,10 +50,12 @@ QUnit.module('GradeSummary GridRow', suiteHooks => {
studentId: '1111'
}
},
onGradeSelect() {},
row: {
studentId: '1111',
studentName: 'Adam Jones'
}
},
selectProvisionalGradeStatus: STARTED
}
})
@ -115,4 +118,39 @@ QUnit.module('GradeSummary GridRow', suiteHooks => {
const cell = wrapper.find('td.grader_1101')
equal(cell.text(), '')
})
QUnit.module('GradeSelect', () => {
test('receives the graders prop', () => {
mountComponent()
const gradeSelect = wrapper.find('GradeSelect')
strictEqual(gradeSelect.prop('graders'), props.graders)
})
test('receives the grades prop', () => {
mountComponent()
const gradeSelect = wrapper.find('GradeSelect')
strictEqual(gradeSelect.prop('grades'), props.grades)
})
test('receives the onGradeSelect prop as onSelect', () => {
mountComponent()
const gradeSelect = wrapper.find('GradeSelect')
strictEqual(gradeSelect.prop('onSelect'), props.onGradeSelect)
})
test('receives the selectProvisionalGradeStatus prop', () => {
mountComponent()
const gradeSelect = wrapper.find('GradeSelect')
strictEqual(
gradeSelect.prop('selectProvisionalGradeStatus'),
props.selectProvisionalGradeStatus
)
})
test('receives the studentName prop from the row', () => {
mountComponent()
const gradeSelect = wrapper.find('GradeSelect')
strictEqual(gradeSelect.prop('studentName'), 'Adam Jones')
})
})
})

View File

@ -20,6 +20,8 @@ import React from 'react'
import {mount} from 'enzyme'
import Grid from 'jsx/assignments/GradeSummary/components/GradesGrid/Grid'
import GridRow from 'jsx/assignments/GradeSummary/components/GradesGrid/GridRow'
import {STARTED, SUCCESS} from 'jsx/assignments/GradeSummary/grades/GradeActions'
QUnit.module('GradeSummary Grid', suiteHooks => {
let props
@ -74,12 +76,17 @@ QUnit.module('GradeSummary Grid', suiteHooks => {
},
horizontalScrollRef: sinon.spy(),
onGradeSelect() {},
rows: [
{studentId: '1111', studentName: 'Adam Jones'},
{studentId: '1112', studentName: 'Betty Ford'},
{studentId: '1113', studentName: 'Charlie Xi'},
{studentId: '1114', studentName: 'Dana Smith'}
]
],
selectProvisionalGradeStatuses: {
1111: SUCCESS,
1112: STARTED
}
}
})
@ -91,46 +98,61 @@ QUnit.module('GradeSummary Grid', suiteHooks => {
wrapper = mount(<Grid {...props} />)
}
test('includes a column header for the student name column', () => {
mountComponent()
strictEqual(wrapper.find('th.GradesGrid__StudentColumnHeader').length, 1)
})
test('includes a column header for each grader', () => {
mountComponent()
strictEqual(wrapper.find('th.GradesGrid__GraderHeader').length, 2)
})
test('includes a column header for the final grade column', () => {
mountComponent()
strictEqual(wrapper.find('th.GradesGrid__FinalGradeHeader').length, 1)
})
test('displays the grader names in the column headers', () => {
mountComponent()
const headers = wrapper.find('th.GradesGrid__GraderHeader')
deepEqual(headers.map(header => header.text()), ['Miss Frizzle', 'Mr. Keating'])
})
test('enumerates graders for names when graders are anonymous', () => {
props.graders[0].graderName = null
props.graders[1].graderName = null
mountComponent()
const headers = wrapper.find('th.GradesGrid__GraderHeader')
deepEqual(headers.map(header => header.text()), ['Grader 1', 'Grader 2'])
})
test('includes a GridRow for each student', () => {
mountComponent()
strictEqual(wrapper.find('GridRow').length, 4)
strictEqual(wrapper.find(GridRow).length, 4)
})
test('sends graders to each GridRow', () => {
mountComponent()
wrapper.find('GridRow').forEach(gridRow => {
wrapper.find(GridRow).forEach(gridRow => {
strictEqual(gridRow.prop('graders'), props.graders)
})
})
test('sends onGradeSelect to each GridRow', () => {
mountComponent()
wrapper.find(GridRow).forEach(gridRow => {
strictEqual(gridRow.prop('onGradeSelect'), props.onGradeSelect)
})
})
test('sends student-specific grades to each GridRow', () => {
mountComponent()
const gridRow = wrapper.find('GridRow').at(1)
const gridRow = wrapper.find(GridRow).at(1)
strictEqual(gridRow.prop('grades'), props.grades[1112])
})
test('sends student-specific select provisional grade statuses to each GridRow', () => {
mountComponent()
const gridRow = wrapper.find(GridRow).at(1)
strictEqual(gridRow.prop('selectProvisionalGradeStatus'), STARTED)
})
test('sends the related row to each GridRow', () => {
mountComponent()
const gridRow = wrapper.find('GridRow').at(1)
const gridRow = wrapper.find(GridRow).at(1)
strictEqual(gridRow.prop('row'), props.rows[1])
})

View File

@ -19,6 +19,7 @@
import React from 'react'
import {mount} from 'enzyme'
import Grid from 'jsx/assignments/GradeSummary/components/GradesGrid/Grid'
import GradesGrid from 'jsx/assignments/GradeSummary/components/GradesGrid'
QUnit.module('GradeSummary GradesGrid', suiteHooks => {
@ -71,6 +72,8 @@ QUnit.module('GradeSummary GradesGrid', suiteHooks => {
}
}
},
onGradeSelect() {},
selectProvisionalGradeStatuses: {},
students: [
{id: '1111', displayName: 'Adam Jones'},
{id: '1112', displayName: 'Betty Ford'},
@ -108,18 +111,23 @@ QUnit.module('GradeSummary GradesGrid', suiteHooks => {
deepEqual(getGraderNames(), ['Miss Frizzle', 'Mr. Keating'])
})
test('enumerates graders for names when graders are anonymous', () => {
props.graders[0].graderName = null
props.graders[1].graderName = null
mountComponent()
deepEqual(getGraderNames(), ['Grader 1', 'Grader 2'])
})
test('includes a row for each student', () => {
mountComponent()
strictEqual(wrapper.find('tr.GradesGrid__BodyRow').length, 4)
})
test('sends onGradeSelect to the Grid', () => {
mountComponent()
const grid = wrapper.find(Grid)
strictEqual(grid.prop('onGradeSelect'), props.onGradeSelect)
})
test('sends selectProvisionalGradeStatuses to the Grid', () => {
mountComponent()
const grid = wrapper.find(Grid)
strictEqual(grid.prop('selectProvisionalGradeStatuses'), props.selectProvisionalGradeStatuses)
})
test('adds rows as students are added', () => {
const {students} = props
props.students = students.slice(0, 2)

View File

@ -20,6 +20,8 @@ import React from 'react'
import {mount} from 'enzyme'
import {Provider} from 'react-redux'
import * as AssignmentActions from 'jsx/assignments/GradeSummary/assignment/AssignmentActions'
import * as GradeActions from 'jsx/assignments/GradeSummary/grades/GradeActions'
import * as StudentActions from 'jsx/assignments/GradeSummary/students/StudentActions'
import Layout from 'jsx/assignments/GradeSummary/components/Layout'
import configureStore from 'jsx/assignments/GradeSummary/configureStore'
@ -47,9 +49,13 @@ QUnit.module('GradeSummary Layout', suiteHooks => {
sinon
.stub(StudentActions, 'loadStudents')
.returns(StudentActions.setLoadStudentsStatus(StudentActions.STARTED))
sinon
.stub(GradeActions, 'selectProvisionalGrade')
.callsFake(gradeInfo => GradeActions.setSelectedProvisionalGrade(gradeInfo))
})
suiteHooks.afterEach(() => {
GradeActions.selectProvisionalGrade.restore()
StudentActions.loadStudents.restore()
wrapper.unmount()
})
@ -105,4 +111,39 @@ QUnit.module('GradeSummary Layout', suiteHooks => {
strictEqual(wrapper.find('Spinner').length, 0)
})
})
QUnit.module('GradesGrid', hooks => {
let grades
hooks.beforeEach(() => {
mountComponent()
const students = [
{id: '1111', displayName: 'Adam Jones'},
{id: '1112', displayName: 'Betty Ford'}
]
store.dispatch(StudentActions.addStudents(students))
grades = [
{grade: 'A', graderId: '1101', id: '4601', score: 10, selected: false, studentId: '1111'}
]
store.dispatch(GradeActions.addProvisionalGrades(grades))
})
test('onGradeSelect selects a provisional grade when grades have not been published', () => {
const onGradeSelect = wrapper.find('GradesGrid').prop('onGradeSelect')
onGradeSelect(grades[0])
const gradeInfo = store.getState().grades.provisionalGrades[1111][1101]
strictEqual(gradeInfo.selected, true)
})
test('is null when grades have not been published', () => {
store.dispatch(AssignmentActions.updateAssignment({gradesPublished: true}))
const onGradeSelect = wrapper.find('GradesGrid').prop('onGradeSelect')
strictEqual(onGradeSelect, null)
})
test('receives the selectProvisionalGradeStatuses from state', () => {
const statuses = wrapper.find('GradesGrid').prop('selectProvisionalGradeStatuses')
strictEqual(statuses, store.getState().grades.selectProvisionalGradeStatuses)
})
})
})

View File

@ -127,9 +127,9 @@ QUnit.module('GradeSummary getEnv()', suiteHooks => {
deepEqual(graderIds, ['abcde', 'b01ng', 'h2asd'])
})
test('sets .graderName to null', () => {
test('assigns enumerated names', () => {
const graderNames = getEnv().graders.map(grader => grader.graderName)
deepEqual(graderNames, [null, null, null])
deepEqual(graderNames, ['Grader 1', 'Grader 2', 'Grader 3'])
})
})
})

View File

@ -17,6 +17,7 @@
*/
import * as GradeActions from 'jsx/assignments/GradeSummary/grades/GradeActions'
import * as GradesApi from 'jsx/assignments/GradeSummary/grades/GradesApi'
import configureStore from 'jsx/assignments/GradeSummary/configureStore'
QUnit.module('GradeSummary GradeActions', suiteHooks => {
@ -56,4 +57,96 @@ QUnit.module('GradeSummary GradeActions', suiteHooks => {
deepEqual(grades[1112][1102], provisionalGrades[1])
})
})
QUnit.module('.selectProvisionalGrade()', hooks => {
let args
let rejectPromise
let resolvePromise
hooks.beforeEach(() => {
const fakePromise = {
then(callback) {
resolvePromise = callback
return fakePromise
},
catch(callback) {
rejectPromise = callback
}
}
sinon
.stub(GradesApi, 'selectProvisionalGrade')
.callsFake((courseId, assignmentId, provisionalGradeId) => {
args = {courseId, assignmentId, provisionalGradeId}
return fakePromise
})
const provisionalGrades = [
{
grade: 'A',
graderId: '1101',
id: '4601',
score: 10,
selected: false,
studentId: '1111'
},
{
grade: 'B',
graderId: '1102',
id: '4602',
score: 9,
selected: false,
studentId: '1111'
}
]
store.dispatch(GradeActions.addProvisionalGrades(provisionalGrades))
store.dispatch(GradeActions.selectProvisionalGrade(provisionalGrades[0]))
})
hooks.afterEach(() => {
args = null
GradesApi.selectProvisionalGrade.restore()
})
test('sets the "set selected provisional grade" status to "started"', () => {
const {selectProvisionalGradeStatuses} = store.getState().grades
equal(selectProvisionalGradeStatuses[1111], GradeActions.STARTED)
})
test('selects the provisional grade through the api', () => {
strictEqual(GradesApi.selectProvisionalGrade.callCount, 1)
})
test('includes the course id when selecting through the api', () => {
strictEqual(args.courseId, '1201')
})
test('includes the assignment id when selecting through the api', () => {
strictEqual(args.assignmentId, '2301')
})
test('includes the provisional grade id when selecting through the api', () => {
strictEqual(args.provisionalGradeId, '4601')
})
test('updates the selected provisional grade in the store when the request succeeds', () => {
resolvePromise()
const grades = store.getState().grades.provisionalGrades
strictEqual(grades[1111][1101].selected, true)
})
test('sets the "set selected provisional grade" status to "success" when the request succeeds', () => {
resolvePromise()
const {selectProvisionalGradeStatuses} = store.getState().grades
equal(selectProvisionalGradeStatuses[1111], GradeActions.SUCCESS)
})
test('sets the "set selected provisional grade" status to "failure" when a failure occurs', () => {
rejectPromise(new Error('server error'))
const {selectProvisionalGradeStatuses} = store.getState().grades
equal(selectProvisionalGradeStatuses[1111], GradeActions.FAILURE)
})
})
})

View File

@ -0,0 +1,63 @@
/*
* Copyright (C) 2018 - 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 * as GradesApi from 'jsx/assignments/GradeSummary/grades/GradesApi'
import FakeServer, {pathFromRequest} from 'jsx/__tests__/FakeServer'
QUnit.module('GradeSummary GradesApi', suiteHooks => {
let qunitTimeout
let server
suiteHooks.beforeEach(() => {
qunitTimeout = QUnit.config.testTimeout
QUnit.config.testTimeout = 500 // avoid accidental unresolved async
server = new FakeServer()
})
suiteHooks.afterEach(() => {
server.teardown()
QUnit.config.testTimeout = qunitTimeout
})
QUnit.module('.selectProvisionalGrade()', () => {
const url = `/api/v1/courses/1201/assignments/2301/provisional_grades/4601/select`
test('sends a request to select a provisional grade', async () => {
server.for(url).respond({status: 200, body: {}})
await GradesApi.selectProvisionalGrade('1201', '2301', '4601')
const request = server.receivedRequests[0]
equal(pathFromRequest(request), url)
})
test('sends a PUT request', async () => {
server.for(url).respond({status: 200, body: {}})
await GradesApi.selectProvisionalGrade('1201', '2301', '4601')
const request = server.receivedRequests[0]
equal(request.method, 'PUT')
})
test('does not catch failures', async () => {
server.for(url).respond({status: 500, body: {error: 'server error'}})
try {
await GradesApi.selectProvisionalGrade('1201', '2301', '4601')
} catch (e) {
ok(e.message.includes('500'))
}
})
})
})

View File

@ -89,4 +89,94 @@ QUnit.module('GradeSummary gradesReducer()', suiteHooks => {
deepEqual(getProvisionalGrades()[1112][1102], provisionalGrades[1])
})
})
QUnit.module('when handling "SET_SELECTED_PROVISIONAL_GRADE"', hooks => {
hooks.beforeEach(() => {
store.dispatch(GradeActions.addProvisionalGrades(provisionalGrades))
})
test('sets the given provisional grade as selected', () => {
store.dispatch(GradeActions.setSelectedProvisionalGrade(provisionalGrades[2]))
strictEqual(getProvisionalGrades()[1111][1102].selected, true)
})
test('sets the previously-selected provisional grade as not selected', () => {
store.dispatch(GradeActions.setSelectedProvisionalGrade(provisionalGrades[2]))
strictEqual(getProvisionalGrades()[1111][1101].selected, false)
})
test('replaces the instance of the given provisional grade', () => {
const grade = getProvisionalGrades()[1111][1102]
store.dispatch(GradeActions.setSelectedProvisionalGrade(grade))
notEqual(getProvisionalGrades()[1111][1102], grade)
})
test('replaces the instance of the de-selected provisional grade', () => {
const grade = getProvisionalGrades()[1111][1101]
store.dispatch(GradeActions.setSelectedProvisionalGrade(provisionalGrades[2]))
notEqual(getProvisionalGrades()[1111][1101], grade)
})
test('does not replace instances of related grades not previously selected', () => {
const grade = getProvisionalGrades()[1111][1101]
grade.selected = false
store.dispatch(GradeActions.setSelectedProvisionalGrade(provisionalGrades[2]))
strictEqual(getProvisionalGrades()[1111][1101], grade)
})
test('replaces the instance of student grades collection', () => {
const grades = getProvisionalGrades()[1111]
store.dispatch(GradeActions.setSelectedProvisionalGrade(provisionalGrades[2]))
notEqual(getProvisionalGrades()[1111], grades)
})
test('does not replace instances of unrelated student grades collections', () => {
const grades = getProvisionalGrades()[1112]
store.dispatch(GradeActions.setSelectedProvisionalGrade(provisionalGrades[2]))
strictEqual(getProvisionalGrades()[1112], grades)
})
test('replaces the provisional grades instance', () => {
const grades = getProvisionalGrades()
store.dispatch(GradeActions.setSelectedProvisionalGrade(provisionalGrades[2]))
notEqual(getProvisionalGrades(), grades)
})
})
QUnit.module('when handling "SET_SELECT_PROVISIONAL_GRADE_STATUS"', () => {
function setSelectProvisionalGradeStatus(gradeInfo, status) {
store.dispatch(GradeActions.setSelectProvisionalGradeStatus(gradeInfo, status))
}
function getSelectProvisionalGradeStatus(studentId) {
return store.getState().grades.selectProvisionalGradeStatuses[studentId]
}
test('optionally sets the "select provisional grade" status to "failure" for the related student', () => {
setSelectProvisionalGradeStatus(provisionalGrades[0], GradeActions.FAILURE)
equal(getSelectProvisionalGradeStatus(1111), GradeActions.FAILURE)
})
test('optionally sets the "select provisional grade" status to "started" for the related student', () => {
setSelectProvisionalGradeStatus(provisionalGrades[0], GradeActions.STARTED)
equal(getSelectProvisionalGradeStatus(1111), GradeActions.STARTED)
})
test('optionally sets the "select provisional grade" status to "success" for the related student', () => {
setSelectProvisionalGradeStatus(provisionalGrades[0], GradeActions.SUCCESS)
equal(getSelectProvisionalGradeStatus(1111), GradeActions.SUCCESS)
})
test('replaces the previous status for the related student', () => {
setSelectProvisionalGradeStatus(provisionalGrades[0], GradeActions.STARTED)
setSelectProvisionalGradeStatus(provisionalGrades[0], GradeActions.SUCCESS)
equal(getSelectProvisionalGradeStatus(1111), GradeActions.SUCCESS)
})
test('does not affect unrelated students', () => {
setSelectProvisionalGradeStatus(provisionalGrades[1], GradeActions.STARTED)
setSelectProvisionalGradeStatus(provisionalGrades[0], GradeActions.SUCCESS)
equal(getSelectProvisionalGradeStatus(1112), GradeActions.STARTED)
})
})
})