Fix up <Select> in gradebook history for InstUI 7

Closes FOO-1107
flag=none

Another update to a component using features in the version 6
<Select> component. CanvasAsyncSelect seemed to work pretty
nicely, although it needed a couple of features added as well
to round out the functionality.

Test plan:
* Go to courses/:id/gradebook/history
* The filter boxes should work like they always have

Change-Id: I3cd015a4edac4fa608d01158f9f320642f78a144
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/250903
Reviewed-by: Ed Schiebel <eschiebel@instructure.com>
Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
Product-Review: Charley Kline <ckline@instructure.com>
QA-Review: August Thornton <august@instructure.com>
This commit is contained in:
Charley Kline 2020-10-21 16:53:15 -05:00
parent 12eb84db7b
commit c730122179
4 changed files with 313 additions and 296 deletions

View File

@ -22,8 +22,9 @@ import {arrayOf, func, shape, string} from 'prop-types'
import I18n from 'i18n!gradebook_history'
import tz from 'timezone'
import moment from 'moment'
import {debounce} from 'lodash'
import {Checkbox} from '@instructure/ui-checkbox'
import {Select} from '@instructure/ui-forms'
import CanvasAsyncSelect from 'jsx/shared/components/CanvasAsyncSelect'
import {Button} from '@instructure/ui-buttons'
import {View, Grid} from '@instructure/ui-layout'
import {FormFieldGroup} from '@instructure/ui-form-field'
@ -33,6 +34,8 @@ import {showFlashAlert} from '../shared/FlashAlert'
import environment from './environment'
import CanvasDateInput from 'jsx/shared/components/CanvasDateInput'
const DEBOUNCE_DELAY = 500 // milliseconds
const recordShape = shape({
fetchStatus: string.isRequired,
items: arrayOf(
@ -58,20 +61,24 @@ class SearchFormComponent extends Component {
getSearchOptionsNextPage: func.isRequired
}
state = {
selected: {
assignment: '',
grader: '',
student: '',
from: {value: ''},
to: {value: ''},
showFinalGradeOverridesOnly: false
},
messages: {
assignments: I18n.t('Type a few letters to start searching'),
graders: I18n.t('Type a few letters to start searching'),
students: I18n.t('Type a few letters to start searching')
constructor(props) {
super(props)
this.state = {
selected: {
assignment: '',
grader: '',
student: '',
from: {value: ''},
to: {value: ''},
showFinalGradeOverridesOnly: false
},
messages: {
assignments: I18n.t('Type a few letters to start searching'),
graders: I18n.t('Type a few letters to start searching'),
students: I18n.t('Type a few letters to start searching')
}
}
this.debouncedGetSearchOptions = debounce(props.getSearchOptions, DEBOUNCE_DELAY)
}
componentDidMount() {
@ -150,17 +157,18 @@ class SearchFormComponent extends Component {
}))
}
setSelectedAssignment = (event, selectedOption) => {
this.props.clearSearchOptions('assignments')
setSelectedAssignment = (_event, selectedOption) => {
const selname = this.props.assignments.items.find(e => e.id === selectedOption)?.name
if (selname) this.props.getSearchOptions('assignments', selname)
this.setState(prevState => {
const selected = {
...prevState.selected,
assignment: selectedOption ? selectedOption.id : ''
assignment: selectedOption || ''
}
// If we selected an assignment, uncheck the "show final grade overrides
// only" checkbox
if (selectedOption != null) {
if (selectedOption) {
selected.showFinalGradeOverridesOnly = false
}
@ -168,22 +176,24 @@ class SearchFormComponent extends Component {
})
}
setSelectedGrader = (event, selected) => {
this.props.clearSearchOptions('graders')
setSelectedGrader = (_event, selected) => {
const selname = this.props.graders.items.find(e => e.id === selected)?.name
if (selname) this.props.getSearchOptions('graders', selname)
this.setState(prevState => ({
selected: {
...prevState.selected,
grader: selected ? selected.id : ''
grader: selected || ''
}
}))
}
setSelectedStudent = (event, selected) => {
this.props.clearSearchOptions('students')
setSelectedStudent = (_event, selected) => {
const selname = this.props.students.items.find(e => e.id === selected)?.name
if (selname) this.props.getSearchOptions('students', selname)
this.setState(prevState => ({
selected: {
...prevState.selected,
student: selected ? selected.id : ''
student: selected || ''
}
}))
}
@ -245,21 +255,13 @@ class SearchFormComponent extends Component {
this.props.clearSearchOptions('assignments')
}
this.setState(
prevState => ({
selected: {
...prevState.selected,
assignment: enabled ? '' : prevState.selected.assignment,
showFinalGradeOverridesOnly: enabled
}
}),
() => {
if (enabled) {
// Also manually clear the contents of the assignment select input
this.assignmentInput.value = ''
}
this.setState(prevState => ({
selected: {
...prevState.selected,
assignment: enabled ? '' : prevState.selected.assignment,
showFinalGradeOverridesOnly: enabled
}
)
}))
}
handleSearchEntry = (target, searchTerm) => {
@ -272,23 +274,18 @@ class SearchFormComponent extends Component {
return
}
this.props.getSearchOptions(target, searchTerm)
this.debouncedGetSearchOptions(target, searchTerm)
}
handleSubmit = () => {
this.props.getGradebookHistory(this.state.selected)
}
filterNone = options =>
// empty function here as the default filter function for Select
// does a startsWith call, and won't match `nora` -> `Elenora` for example
options
renderAsOptions = data =>
data.map(item => (
<option key={item.id} value={item.id}>
{item.name}
</option>
data.map(i => (
<CanvasAsyncSelect.Option key={i.id} id={i.id}>
{i.name}
</CanvasAsyncSelect.Option>
))
render() {
@ -313,63 +310,42 @@ class SearchFormComponent extends Component {
vAlign="top"
startAt="medium"
>
<Select
editable
<CanvasAsyncSelect
id="students"
allowEmpty
emptyOption={this.state.messages.students}
filter={this.filterNone}
label={I18n.t('Student')}
loadingText={
this.props.students.fetchStatus === 'started'
? I18n.t('Loading Students')
: undefined
}
renderLabel={I18n.t('Student')}
isLoading={this.props.students.fetchStatus === 'started'}
selectedOptionId={this.state.selected.student}
noOptionsLabel={this.state.messages.students}
onBlur={this.promptUserEntry}
onChange={this.setSelectedStudent}
onOptionSelected={this.setSelectedStudent}
onInputChange={this.handleStudentChange}
>
{this.renderAsOptions(this.props.students.items)}
</Select>
<Select
editable
</CanvasAsyncSelect>
<CanvasAsyncSelect
id="graders"
allowEmpty
emptyOption={this.state.messages.graders}
filter={this.filterNone}
label={I18n.t('Grader')}
loadingText={
this.props.graders.fetchStatus === 'started'
? I18n.t('Loading Graders')
: undefined
}
renderLabel={I18n.t('Grader')}
isLoading={this.props.graders.fetchStatus === 'started'}
selectedOptionId={this.state.selected.grader}
noOptionsLabel={this.state.messages.graders}
onBlur={this.promptUserEntry}
onChange={this.setSelectedGrader}
onOptionSelected={this.setSelectedGrader}
onInputChange={this.handleGraderChange}
>
{this.renderAsOptions(this.props.graders.items)}
</Select>
<Select
editable
</CanvasAsyncSelect>
<CanvasAsyncSelect
id="assignments"
allowEmpty
emptyOption={this.state.messages.assignments}
filter={this.filterNone}
inputRef={ref => {
this.assignmentInput = ref
}}
label={I18n.t('Artifact')}
loadingText={
this.props.assignments.fetchStatus === 'started'
? I18n.t('Loading Artifact')
: undefined
}
renderLabel={I18n.t('Artifact')}
isLoading={this.props.assignments.fetchStatus === 'started'}
selectedOptionId={this.state.selected.assignment}
noOptionsLabel={this.state.messages.assignments}
onBlur={this.promptUserEntry}
onChange={this.setSelectedAssignment}
onOptionSelected={this.setSelectedAssignment}
onInputChange={this.handleAssignmentChange}
>
{this.renderAsOptions(this.props.assignments.items)}
</Select>
</CanvasAsyncSelect>
</FormFieldGroup>
<FormFieldGroup

View File

@ -0,0 +1,174 @@
/* 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 React from 'react'
import MockDate from 'mockdate'
import {render, act, fireEvent} from '@testing-library/react'
import {SearchFormComponent as Subject} from '../SearchForm'
function defaultProps() {
return {
fetchHistoryStatus: 'started',
getGradebookHistory: Function.prototype,
clearSearchOptions: Function.prototype,
getSearchOptions: Function.prototype,
getSearchOptionsNextPage: Function.prototype,
assignments: {
fetchStatus: 'started',
items: [],
nextPage: ''
},
graders: {
fetchStatus: 'started',
items: [],
nextPage: ''
},
students: {
fetchStatus: 'started',
items: [],
nextPage: ''
}
}
}
const fields = ['assignments', 'graders', 'students']
function mountSubject(props = {}) {
return render(<Subject {...defaultProps()} {...props} />)
}
let liveRegion = null
beforeAll(() => {
if (!document.getElementById('flash_screenreader_holder')) {
liveRegion = document.createElement('div')
liveRegion.id = 'flash_screenreader_holder'
liveRegion.setAttribute('role', 'alert')
document.body.appendChild(liveRegion)
}
})
afterAll(() => {
if (liveRegion) liveRegion.remove()
})
describe('GradebookHistory::SearchFormComponent', () => {
beforeEach(() => {
jest.useFakeTimers()
})
afterEach(() => {
MockDate.reset()
})
function advance(msec) {
act(() => {
const now = Date.now()
MockDate.set(now + msec)
jest.advanceTimersByTime(msec)
})
}
it('displays a flash alert on fetch failure', () => {
const {rerender} = mountSubject()
let flash = document.getElementById('flashalert_message_holder')
expect(flash).toBeNull()
rerender(<Subject {...defaultProps()} fetchHistoryStatus="failure" />)
flash = document.getElementById('flashalert_message_holder')
expect(flash).toBeInTheDocument()
})
it('properly debounces getSearchOptions calls', () => {
const getSearchOptions = jest.fn()
const {container} = mountSubject({getSearchOptions})
const input = container.querySelector('input#assignments')
fireEvent.click(input)
fireEvent.input(input, {target: {id: 'assignments', value: 'onetwo'}})
expect(getSearchOptions).not.toHaveBeenCalled()
})
describe('calls getSearchOptions with correct arguments after more than two letters are typed', () => {
fields.forEach(field => {
it(`for ${field}`, () => {
const getSearchOptions = jest.fn()
const {container} = mountSubject({getSearchOptions})
const input = container.querySelector(`input#${field}`)
fireEvent.click(input)
fireEvent.input(input, {target: {id: field, value: 'onetwo'}})
advance(1000) // wait for debounce
expect(getSearchOptions).toHaveBeenCalledWith(field, 'onetwo')
})
})
})
describe('hits clearSearchOptions after two or fewer letters are typed', () => {
fields.forEach(field => {
it(`for ${field}`, () => {
const clearSearchOptions = jest.fn()
const {container} = mountSubject({
clearSearchOptions,
[field]: {
fetchStatus: 'success',
items: [{id: '1', name: `One of the ${field}`}],
nextPage: ''
}
})
const input = container.querySelector(`input#${field}`)
fireEvent.click(input)
fireEvent.input(input, {target: {id: field, value: 'xy'}})
expect(clearSearchOptions).toHaveBeenCalledWith(field)
})
})
})
describe('displays correct "not found" message when no search records are returned', () => {
fields.forEach(field => {
it(`for ${field}`, () => {
// slight mismatch between prop name and what's displayed, here...
const displayField = field === 'assignments' ? 'artifacts' : field
const {rerender, container, getByText} = mountSubject()
const results = {
[field]: {fetchStatus: 'success', items: [], nextPage: ''}
}
rerender(<Subject {...defaultProps()} {...results} />)
const input = container.querySelector(`input#${field}`)
fireEvent.click(input)
expect(getByText(`No ${displayField} with that name found`)).toBeInTheDocument()
})
})
})
it('calls getSearchOptionsNextPage if there are more items to load', () => {
const getSearchOptionsNextPage = jest.fn()
const {rerender} = mountSubject({getSearchOptionsNextPage})
expect(getSearchOptionsNextPage).not.toHaveBeenCalled()
rerender(
<Subject
getSearchOptionsNextPage={getSearchOptionsNextPage}
{...defaultProps()}
students={{
fetchStatus: 'success',
items: [],
nextPage: 'https://nextpage.example.com'
}}
/>
)
expect(getSearchOptionsNextPage).toHaveBeenCalledWith(
'students',
'https://nextpage.example.com'
)
})
})

View File

@ -33,16 +33,26 @@ CanvasAsyncSelect.propTypes = {
isLoading: bool,
selectedOptionId: string,
noOptionsLabel: string,
noOptionsValue: string, // value for selected option when no or invalid selection
onOptionSelected: func, // (event, optionId | null) => {}
onInputChange: func // (event, value) => {}
onInputChange: func, // (event, value) => {}
onBlur: func,
onFocus: func
}
// NOTE:
// If the inputValue prop is not specified, this component will control the inputValue
// of <Select> itself. If it is specified, it is the caller's responsibility to manage
// its value in response to input changes!
CanvasAsyncSelect.defaultProps = {
inputValue: '',
isLoading: false,
noOptionsLabel: '---',
onOptionSelected: () => {},
onInputChange: () => {}
noOptionsValue: '',
onOptionSelected: Function.prototype,
onInputChange: Function.prototype,
onBlur: Function.prototype,
onFocus: Function.prototype
}
export default function CanvasAsyncSelect({
@ -51,17 +61,23 @@ export default function CanvasAsyncSelect({
isLoading,
selectedOptionId,
noOptionsLabel,
noOptionsValue,
onOptionSelected,
onInputChange,
onFocus,
onBlur,
children,
...selectProps
}) {
const previousLoadingRef = useRef(isLoading)
const previousLoading = previousLoadingRef.current
const previousSelectedOptionIdRef = useRef(selectedOptionId)
const [isShowingOptions, setIsShowingOptions] = useState(false)
const [highlightedOptionId, setHighlightedOptionId] = useState(null)
const [announcement, setAnnouncement] = useState('')
const [hasFocus, setHasFocus] = useState(false)
const [managedInputValue, setManagedInputValue] = useState('')
function findOptionById(id) {
let option
@ -88,7 +104,7 @@ export default function CanvasAsyncSelect({
if (isLoading) {
return (
<Select.Option id={noOptionsId}>
<Spinner renderTitle={I18n.t('Loading options...')} size="small" />
<Spinner renderTitle={I18n.t('Loading options...')} size="x-small" />
</Select.Option>
)
} else if (React.Children.count(children) === 0) {
@ -101,6 +117,7 @@ export default function CanvasAsyncSelect({
function handleInputChange(ev) {
// user typing in the input negates the selection
const newValue = ev.target.value
if (typeof inputValue === 'undefined') setManagedInputValue(newValue)
setIsShowingOptions(true)
onInputChange(ev, newValue)
}
@ -124,23 +141,36 @@ export default function CanvasAsyncSelect({
function handleSelectOption(ev, {id}) {
const selectedOption = findOptionById(id)
const selectedText = selectedOption?.props.children
if (!selectedOption) return
setIsShowingOptions(false)
setAnnouncement(
<>
{I18n.t('Option selected:')} {selectedOption.props.children} {I18n.t('List collapsed.')}
{I18n.t('Option selected:')} {selectedText} {I18n.t('List collapsed.')}
</>
)
if (id !== noOptionsId) onOptionSelected(ev, id)
if (id !== noOptionsId) {
if (typeof inputValue === 'undefined') setManagedInputValue(selectedText)
onOptionSelected(ev, id)
}
}
function handleFocus() {
function handleFocus(ev) {
setHasFocus(true)
onFocus(ev)
}
function handleBlur() {
function handleBlur(ev) {
setHasFocus(false)
setHighlightedOptionId(null)
// if we're managing our own input value and all our possible options just
// went away, be sure to notify the parent the the selection has gone away
// as well.
if (React.Children.count(children) === 0 && typeof inputValue === 'undefined') {
setManagedInputValue('')
onOptionSelected(ev, noOptionsValue)
}
onBlur(ev)
}
if (hasFocus && previousLoading !== isLoading) {
@ -151,8 +181,18 @@ export default function CanvasAsyncSelect({
}
}
// If we're controlling our own inputValue, take care to clear it if the selection resets
// out from under us.
if (
typeof inputValue === 'undefined' &&
selectedOptionId !== previousSelectedOptionIdRef.current &&
!selectedOptionId
) {
setManagedInputValue('')
}
const controlledProps = {
inputValue,
inputValue: typeof inputValue === 'undefined' ? managedInputValue : inputValue,
isShowingOptions,
assistiveText: I18n.t('Type to search'),
onFocus: handleFocus,
@ -167,6 +207,7 @@ export default function CanvasAsyncSelect({
// remember previous isLoading value so we know whether we need to send announcements
// (we can't use an effect for this because effects only run after the DOM changes)
previousLoadingRef.current = isLoading
previousSelectedOptionIdRef.current = selectedOptionId
return (
<>
<Select {...controlledProps} {...selectProps}>

View File

@ -21,8 +21,8 @@ import {mount, shallow} from 'enzyme'
import {SearchFormComponent} from 'jsx/gradebook-history/SearchForm'
import {Button} from '@instructure/ui-buttons'
import CanvasDateInput from 'jsx/shared/components/CanvasDateInput'
import CanvasAsyncSelect from 'jsx/shared/components/CanvasAsyncSelect'
import {FormFieldGroup} from '@instructure/ui-form-field'
import {destroyContainer} from 'jsx/shared/FlashAlert'
import Fixtures from './Fixtures'
import fakeENV from 'helpers/fakeENV'
@ -49,6 +49,11 @@ const defaultProps = () => ({
}
})
const liveRegion = document.createElement('div')
liveRegion.id = 'flash_screenreader_holder'
liveRegion.setAttribute('role', 'alert')
document.body.appendChild(liveRegion)
const mountComponent = (props = {}) =>
shallow(<SearchFormComponent {...defaultProps()} {...props} />)
@ -69,19 +74,19 @@ test('has a form field group', function() {
test('has an Autocomplete with id #graders', function() {
const input = this.wrapper.find('#graders')
equal(input.length, 1)
ok(input.is('Select'))
ok(input.is(CanvasAsyncSelect))
})
test('has an Autocomplete with id #students', function() {
const input = this.wrapper.find('#students')
equal(input.length, 1)
ok(input.is('Select'))
ok(input.is(CanvasAsyncSelect))
})
test('has an Autocomplete with id #assignments', function() {
const input = this.wrapper.find('#assignments')
equal(input.length, 1)
ok(input.is('Select'))
ok(input.is(CanvasAsyncSelect))
})
test('has date pickers for from date and to date', function() {
@ -200,125 +205,9 @@ test('dispatches with the state of input', function() {
)
})
QUnit.module('SearchForm fetchHistoryStatus prop', {
setup() {
this.wrapper = mountComponent({fetchHistoryStatus: 'started'})
},
teardown() {
this.wrapper.unmount()
destroyContainer()
}
})
test('turning from started to failure displays an AjaxFlashAlert', function() {
// the container the alerts get rendered into doesn't exist until ajaxFlashAlert needs it
// and then it'll create it itself, appending the error message into this new container
equal(document.getElementById('flash_message_holder'), null)
this.wrapper.setProps({fetchHistoryStatus: 'failure'})
const flashMessageContainer = document.getElementById('flashalert_message_holder')
ok(flashMessageContainer.childElementCount > 0)
})
QUnit.module('SearchForm Autocomplete', {
setup() {
this.props = {
...defaultProps(),
fetchHistoryStatus: 'started',
clearSearchOptions: sinon.stub(),
getSearchOptions: sinon.stub(),
getSearchOptionsNextPage: sinon.stub()
}
this.wrapper = mount(<SearchFormComponent {...this.props} />)
},
teardown() {
this.wrapper.unmount()
}
})
test('typing more than two letters for assignments hits getSearchOptions prop', function() {
const input = this.wrapper.find('#assignments').last()
input.simulate('change', {target: {id: 'assignments', value: 'Chapter 11 Questions'}})
strictEqual(this.props.getSearchOptions.callCount, 1)
})
test('typing more than two letters for graders hits getSearchOptions prop', function() {
const input = this.wrapper.find('#graders').last()
input.simulate('change', {target: {id: 'graders', value: 'Norval'}})
strictEqual(this.props.getSearchOptions.callCount, 1)
})
test('typing more than two letters for students hits getSearchOptions prop if not empty', function() {
const input = this.wrapper.find('#students').last()
input.simulate('change', {target: {id: 'students', value: 'Norval'}})
strictEqual(this.props.getSearchOptions.callCount, 1)
})
test('typing two or fewer letters for assignments hits clearSearchOptions prop if not empty', function() {
this.wrapper.setProps({
assignments: {
fetchStatus: 'success',
items: [{id: '1', name: 'Gary'}],
nextPage: ''
}
})
const input = this.wrapper.find('#assignments').last()
input.simulate('change', {target: {id: 'assignments', value: 'ab'}})
strictEqual(this.props.clearSearchOptions.callCount, 1)
})
test('typing two or fewer letters for graders hits clearSearchOptions prop', function() {
this.wrapper.setProps({
graders: {
fetchStatus: 'success',
items: [{id: '1', name: 'Gary'}],
nextPage: ''
}
})
const input = this.wrapper.find('#graders').last()
input.simulate('change', {target: {id: 'graders', value: 'ab'}})
strictEqual(this.props.clearSearchOptions.callCount, 1)
})
test('typing two or fewer letters for students hits clearSearchOptions prop if not empty', function() {
this.wrapper.setProps({
students: {
fetchStatus: 'success',
items: [{id: '1', name: 'Gary'}],
nextPage: ''
}
})
const input = this.wrapper.find('#students').last()
input.simulate('change', {target: {id: 'students', value: 'ab'}})
strictEqual(this.props.clearSearchOptions.callCount, 1)
})
test('getSearchOptions is called with search term and input id', function() {
const input = this.wrapper.find('#graders').last()
const inputId = 'graders'
const searchTerm = 'Norval Abbott'
input.simulate('change', {target: {id: inputId, value: searchTerm}})
strictEqual(this.props.getSearchOptions.firstCall.args[0], inputId)
strictEqual(this.props.getSearchOptions.firstCall.args[1], searchTerm)
})
test('getSearchOptionsNextPage is called if there are more options to load', function() {
this.wrapper.setProps({
students: {
fetchStatus: 'success',
items: [],
nextPage: 'https://example.com'
}
})
strictEqual(this.props.getSearchOptionsNextPage.firstCall.args[0], 'students')
strictEqual(this.props.getSearchOptionsNextPage.firstCall.args[1], 'https://example.com')
})
QUnit.module('SearchForm Autocomplete options', {
setup() {
this.props = {...defaultProps(), clearSearchOptions: sinon.stub()}
this.props = {...defaultProps(), getSearchOptions: sinon.stub()}
this.assignments = Fixtures.assignmentArray()
this.graders = Fixtures.userArray()
this.students = Fixtures.userArray()
@ -400,7 +289,7 @@ test('selecting an assignment from options sets state to its id', function() {
strictEqual(this.wrapper.state().selected.assignment, this.assignments[0].id)
})
test('selecting an assignment from options clears options for assignments', function() {
test('selecting an assignment from options sets that option in the list', function() {
this.wrapper.setProps({
assignments: {
fetchStatus: 'success',
@ -420,8 +309,9 @@ test('selecting an assignment from options clears options for assignments', func
.find(span => assignmentNames.includes(span.textContent))
.click()
ok(this.props.clearSearchOptions.called)
strictEqual(this.props.clearSearchOptions.firstCall.args[0], 'assignments')
ok(this.props.getSearchOptions.called)
strictEqual(this.props.getSearchOptions.firstCall.args[0], 'assignments')
strictEqual(this.props.getSearchOptions.firstCall.args[1], this.assignments[0].name)
})
test('selecting an assignment from options sets showFinalGradeOverridesOnly to false', function() {
@ -455,7 +345,7 @@ test('selecting an assignment from options sets showFinalGradeOverridesOnly to f
strictEqual(this.wrapper.state().selected.showFinalGradeOverridesOnly, false)
})
test('selecting a grader from options clears options for graders', function() {
test('selecting a grader from options sets that option in the list', function() {
this.wrapper.setProps({
graders: {
fetchStatus: 'success',
@ -475,11 +365,12 @@ test('selecting a grader from options clears options for graders', function() {
.find(span => graderNames.includes(span.textContent))
.click()
ok(this.props.clearSearchOptions.called)
strictEqual(this.props.clearSearchOptions.firstCall.args[0], 'graders')
ok(this.props.getSearchOptions.called)
strictEqual(this.props.getSearchOptions.firstCall.args[0], 'graders')
strictEqual(this.props.getSearchOptions.firstCall.args[1], this.graders[0].name)
})
test('selecting a student from options clears options for students', function() {
test('selecting a student from options sets that option in the list', function() {
this.wrapper.setProps({
students: {
fetchStatus: 'success',
@ -499,74 +390,9 @@ test('selecting a student from options clears options for students', function()
;[...document.getElementsByTagName('span')]
.find(span => studentNames.includes(span.textContent))
.click()
ok(this.props.clearSearchOptions.called)
strictEqual(this.props.clearSearchOptions.firstCall.args[0], 'students')
})
test('no search records found for students results in a message instead', function() {
this.wrapper.setProps({
students: {
fetchStatus: 'success',
items: [],
nextPage: ''
}
})
this.wrapper
.find('#students')
.last()
.instance()
.click()
const noRecords = [...document.getElementsByTagName('span')].find(
span => span.textContent === 'No students with that name found'
)
ok(noRecords)
})
test('no search records found for graders results in a message instead', function() {
this.wrapper.setProps({
graders: {
fetchStatus: 'success',
items: [],
nextPage: ''
}
})
this.wrapper
.find('#graders')
.last()
.instance()
.click()
const noRecords = [...document.getElementsByTagName('span')].find(
span => span.textContent === 'No graders with that name found'
)
ok(noRecords)
})
test('no search records found for assignments results in a message instead', function() {
this.wrapper.setProps({
assignments: {
fetchStatus: 'success',
items: [],
nextPage: ''
}
})
this.wrapper
.find('#assignments')
.last()
.instance()
.click()
const noRecords = [...document.getElementsByTagName('span')].find(
span => span.textContent === 'No artifacts with that name found'
)
ok(noRecords)
ok(this.props.getSearchOptions.called)
strictEqual(this.props.getSearchOptions.firstCall.args[0], 'students')
strictEqual(this.props.getSearchOptions.firstCall.args[1], this.students[0].name)
})
QUnit.module('SearchForm "Show Final Grade Overrides Only" checkbox', () => {