A2: create message students who modal basics

refs ADMIN-1512

This is just the basic "show the modal" work. The modal doesn't actually
accomplish anything yet. That work will be done in a separate commit, as
this one was getting large.

test plan:
- unsubmitted button should open a message students who modal that more
  or less looks like the mockup.
- when there is no submission or a paper submission, the unsubmitted
  button is replaced with a "message students" button.
- the list of students in the course is shown. It may not be completely
  accurate yet. For example, we're not filtering out the test student.
- The dropdown doesn't work yet.
- you can delete students from the list, and add students back into the
  list.
- you can close and cancel the dialog.
- when you click save, the modal pretends to work for a few seconds, and
  then there is an alert indicating nothing really happened.

Change-Id: I815139c1d6c5b6108fda34c319de2686bb8c6ebd
Reviewed-on: https://gerrit.instructure.com/179827
Tested-by: Jenkins
Reviewed-by: Ed Schiebel <eschiebel@instructure.com>
QA-Review: Jeremy Stanley <jeremy@instructure.com>
Product-Review: Jon Willesen <jonw+gerrit@instructure.com>
This commit is contained in:
Jon Willesen 2019-01-29 10:57:55 -07:00 committed by Jon Willesen
parent 2c53fa4402
commit 559dc2953f
14 changed files with 444 additions and 182 deletions

View File

@ -17,7 +17,8 @@
*/
import React from 'react'
import {bool, func, string} from 'prop-types'
import {bool, func, string, object} from 'prop-types'
import I18n from 'i18n!assignments_2'
import Button from '@instructure/ui-buttons/lib/components/Button'
import CloseButton from '@instructure/ui-buttons/lib/components/CloseButton'
@ -30,97 +31,104 @@ import Modal, {
ModalFooter
} from '@instructure/ui-overlays/lib/components/Modal'
import Spinner from '@instructure/ui-elements/lib/components/Spinner'
import Text from '@instructure/ui-elements/lib/components/Text'
import View from '@instructure/ui-layout/lib/components/View'
export default class ConfirmDialog extends React.Component {
static propTypes = {
open: bool,
// set to true to disable the close buttons and the footer buttons
disabled: bool,
// set to true to show the busy mask
working: bool,
modalLabel: string, // defaults to heading
heading: string,
message: string.isRequired,
confirmLabel: string.isRequired,
cancelLabel: string.isRequired,
closeLabel: string.isRequired,
spinnerLabel: string.isRequired,
// return what should be in the body of the dialog
body: func.isRequired,
onClose: func,
onConfirm: func,
onCancel: func
// return array of property objects to create buttons in the footer.
// pass the children of the Button (usually the display text) as a `children` property
buttons: func,
// returns what should be on the mask when the dialog is busy. Defaults to a small spinner.
busyMaskBody: func,
// label to use if using the default spinner busy mask
spinnerLabel: string,
heading: string.isRequired,
modalLabel: string, // defaults to heading
// properties to pass to the modal
modalProps: object, // eslint-disable-line react/forbid-prop-types
closeLabel: string,
// invoked when the close button is clicked
onDismiss: func
}
static defaultProps = {
open: false,
heading: '',
onClose: () => {},
onConfirm: () => {},
onCancel: () => {}
working: false,
disabled: false,
buttons: () => [],
modalProps: {},
closeLabel: I18n.t('close'),
spinnerLabel: I18n.t('working...'),
onDismiss: () => {}
}
setTestIdCloseButton(buttonElt) {
if (!buttonElt) return
buttonElt.setAttribute('data-testid', 'confirm-dialog-close-button')
}
setTestIdCancelButton(buttonElt) {
if (!buttonElt) return
buttonElt.setAttribute('data-testid', 'confirm-dialog-cancel-button')
}
setTestIdConfirmButton(buttonElt) {
if (!buttonElt) return
buttonElt.setAttribute('data-testid', 'confirm-dialog-confirm-button')
closeButtonRef(elt) {
// because data-testid ends up on the wrong element if we just pass it through to the close button
if (elt) {
elt.setAttribute('data-testid', 'confirm-dialog-close-button')
}
}
modalLabel() {
return this.props.modalLabel ? this.props.modalLabel : this.props.heading
}
renderBusyMaskBody() {
if (this.props.busyMaskBody) return this.props.busyMaskBody()
return <Spinner size="small" title={this.props.spinnerLabel} />
}
renderButton = (buttonProps, index) => {
const defaultProps = {
key: index,
disabled: this.props.disabled,
margin: '0 x-small 0 0'
}
const props = {...defaultProps, ...buttonProps}
return <Button {...props} />
}
render() {
return (
<Modal label={this.modalLabel()} open={this.props.open}>
<Modal {...this.props.modalProps} label={this.modalLabel()} open={this.props.open}>
<ModalHeader>
<Heading level="h2">{this.props.heading}</Heading>
<CloseButton
placement="end"
onClick={this.props.onClose}
buttonRef={this.setTestIdCloseButton}
disabled={this.props.working}
onClick={this.props.onDismiss}
disabled={this.props.disabled}
buttonRef={this.closeButtonRef}
>
{this.props.closeLabel}
</CloseButton>
</ModalHeader>
<ModalBody padding="0">
<View as="div" padding="medium" style={{position: 'relative'}}>
<Text size="large">{this.props.message}</Text>
{this.props.working ? (
<Mask>
<Spinner size="small" title={this.props.spinnerLabel} />
</Mask>
) : null}
</View>
<div style={{position: 'relative'}}>
<View as="div" padding="medium">
{this.props.body()}
{this.props.working ? <Mask>{this.renderBusyMaskBody()}</Mask> : null}
</View>
</div>
</ModalBody>
<ModalFooter>
<Button
onClick={this.props.onCancel}
margin="0 x-small 0 0"
disabled={this.props.working}
buttonRef={this.setTestIdCancelButton}
>
{this.props.cancelLabel}
</Button>
<Button
variant="danger"
onClick={this.props.onConfirm}
margin="0 x-small 0 0"
disabled={this.props.working}
buttonRef={this.setTestIdConfirmButton}
>
{this.props.confirmLabel}
</Button>
</ModalFooter>
<ModalFooter>{this.props.buttons().map(this.renderButton)}</ModalFooter>
</Modal>
)
}

View File

@ -1,43 +0,0 @@
/*
* 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 I18n from 'i18n!assignments_2'
import CloseButton from '@instructure/ui-buttons/lib/components/CloseButton'
import Modal, {
ModalHeader,
ModalBody
// ModalFooter
} from '@instructure/ui-overlays/lib/components/Modal'
export default function MessageStudentsWho(props) {
return (
<Modal label={I18n.t('Message Students Who')} {...props}>
<ModalHeader>
<CloseButton placement="end" variant="icon" onClick={props.onDismiss}>
{I18n.t('Close')}
</CloseButton>
</ModalHeader>
<ModalBody>
<div data-testid="message-students-who">Message Students Who</div>
</ModalBody>
</Modal>
)
}

View File

@ -0,0 +1,99 @@
/*
* Copyright (C) 2019 - 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 _ from 'lodash'
import I18n from 'i18n!assignments_2'
import AccessibleContent from '@instructure/ui-a11y/lib/components/AccessibleContent'
import FormFieldGroup from '@instructure/ui-form-field/lib/components/FormFieldGroup'
import TextInput from '@instructure/ui-forms/lib/components/TextInput'
import TextArea from '@instructure/ui-forms/lib/components/TextArea'
import Select from '@instructure/ui-forms/lib/components/Select'
import {TeacherAssignmentShape} from '../assignmentData'
export default class MessageStudentsWhoForm extends React.Component {
static propTypes = {
assignment: TeacherAssignmentShape
}
constructor(...args) {
super(...args)
this.state = {
selectedStudents: this.getAllStudents()
}
}
getAllStudents() {
return this.props.assignment.submissions.nodes.map(submission => submission.user)
}
handleFilterChange = (_event, selectedOption) => {
if (selectedOption === 'not-submitted') this.handleNotSubmitted()
else if (selectedOption === 'not-graded') this.handleNotGraded()
else if (selectedOption === 'less-than') this.handleLessThan()
else if (selectedOption === 'more-than') this.handleMoreThan()
// eslint-disable-next-line no-console
else console.error('MessageStudentsWhoForm error: unrecognized filter', selectedOption)
}
handleStudentsChange = (_event, selection) => {
this.setState(() => ({
selectedStudents: _.intersectionWith(
this.getAllStudents(),
selection,
(student, selected) => student.lid === selected.value
)
}))
}
render() {
return (
<FormFieldGroup description={I18n.t('Message students who')}>
<Select label="" onChange={this.handleFilterChange} inline>
<option value="not-submitted">{I18n.t("Haven't submitted yet")}</option>
<option value="not-graded">{I18n.t("Haven't been graded")}</option>
<option value="less-than">{I18n.t('Scored less than')}</option>
<option value="more-than">{I18n.t('Scored more than')}</option>
</Select>
<Select
label={I18n.t('To:')}
multiple
selectedOption={this.state.selectedStudents.map(s => s.lid)}
onChange={this.handleStudentsChange}
formatSelectedOption={tag => (
<AccessibleContent alt={I18n.t('Remove %{studentName}', {studentName: tag.label})}>
{tag.label}
</AccessibleContent>
)}
>
{this.getAllStudents().map(student => (
<option key={student.lid} value={student.lid}>
{student.name}
</option>
))}
</Select>
<TextInput label={I18n.t('Subject:')} />
<TextArea label={I18n.t('Body:')} />
</FormFieldGroup>
)
}
}

View File

@ -24,7 +24,11 @@ import produce from 'immer'
import get from 'lodash/get'
import set from 'lodash/set'
import Alert from '@instructure/ui-alerts/lib/components/Alert'
import ScreenReaderContent from '@instructure/ui-a11y/lib/components/ScreenReaderContent'
import Text from '@instructure/ui-elements/lib/components/Text'
import {showFlashAlert} from 'jsx/shared/FlashAlert'
import {TeacherAssignmentShape, SET_WORKFLOW} from '../assignmentData'
import Header from './Header'
@ -32,7 +36,7 @@ import ContentTabs from './ContentTabs'
import TeacherFooter from './TeacherFooter'
import ConfirmDialog from './ConfirmDialog'
import MessageStudentsWho from './MessageStudentsWho'
import MessageStudentsWhoForm from './MessageStudentsWhoForm'
import TeacherViewContext, {TeacherViewContextDefaults} from './TeacherViewContext'
export default class TeacherView extends React.Component {
@ -44,6 +48,7 @@ export default class TeacherView extends React.Component {
super(props)
this.state = {
messageStudentsWhoOpen: false,
sendingMessageStudentsWhoNow: false,
confirmDelete: false,
deletingNow: false,
workingAssignment: props.assignment, // the assignment with updated fields while editing
@ -90,8 +95,20 @@ export default class TeacherView extends React.Component {
this.setState({messageStudentsWhoOpen: true})
}
handleDismissMessageStudentsWho = () => {
this.setState({messageStudentsWhoOpen: false})
handleCloseMessageStudentsWho = () => {
this.setState({messageStudentsWhoOpen: false, sendingMessageStudentsWhoNow: false})
}
handleSendMessageStudentsWho = () => {
this.setState({sendingMessageStudentsWhoNow: true})
showFlashAlert({message: I18n.t('Sending messages'), srOnly: true})
window.setTimeout(() => {
showFlashAlert({
message: 'We only pretended to send messages. Nothing was actually sent.',
type: 'error'
}) // don't bother translating this
this.handleCloseMessageStudentsWho()
}, 3000)
}
handleDeleteButtonPressed = () => {
@ -102,9 +119,9 @@ export default class TeacherView extends React.Component {
this.setState({confirmDelete: false})
}
handleReallyDelete(mutate) {
handleReallyDelete(deleteAssignment) {
this.setState({deletingNow: true})
mutate({
deleteAssignment({
variables: {
id: this.props.assignment.lid,
workflow: 'deleted'
@ -119,9 +136,10 @@ export default class TeacherView extends React.Component {
window.location.reload()
}
handleDeleteError = _apolloErrors => {
// TODO: properly handle this error
// this.setState({errors, confirmDelete: false, deletingNow: false})
handleDeleteError = apolloErrors => {
showFlashAlert({message: I18n.t('Unable to delete assignment'), type: 'error'})
console.error(apolloErrors) // eslint-disable-line no-console
this.setState({confirmDelete: false, deletingNow: false})
}
handleCancel = () => {
@ -142,7 +160,27 @@ export default class TeacherView extends React.Component {
}
}
renderConfirmDialog() {
deleteDialogButtonProps = deleteAssignment => [
{
children: I18n.t('Cancel'),
onClick: this.handleCancelDelete,
'data-testid': 'delete-dialog-cancel-button'
},
{
children: I18n.t('Delete'),
variant: 'danger',
onClick: () => this.handleReallyDelete(deleteAssignment),
'data-testid': 'delete-dialog-confirm-button'
}
]
renderDeleteDialogBody = () => (
<Alert variant="warning">
<Text size="large">{I18n.t('Are you sure you want to delete this assignment?')}</Text>
</Alert>
)
renderDeleteDialog() {
return (
<Mutation
mutation={SET_WORKFLOW}
@ -153,22 +191,49 @@ export default class TeacherView extends React.Component {
<ConfirmDialog
open={this.state.confirmDelete}
working={this.state.deletingNow}
disabled={this.state.deletingNow}
modalLabel={I18n.t('confirm delete')}
heading={I18n.t('Delete')}
message={I18n.t('Are you sure you want to delete this assignment?')}
confirmLabel={I18n.t('Delete')}
cancelLabel={I18n.t('Cancel')}
closeLabel={I18n.t('close')}
body={this.renderDeleteDialogBody}
buttons={() => this.deleteDialogButtonProps(deleteAssignment)}
spinnerLabel={I18n.t('deleting assignment')}
onClose={this.handleCancelDelete}
onCancel={this.handleCancelDelete}
onConfirm={() => this.handleReallyDelete(deleteAssignment)}
onDismiss={this.handleCancelDelete}
/>
)}
</Mutation>
)
}
messageStudentsWhoButtonProps = () => [
{
children: I18n.t('Cancel'),
onClick: this.handleCloseMessageStudentsWho
},
{
children: I18n.t('Send'),
variant: 'primary',
onClick: this.handleSendMessageStudentsWho
}
]
renderMessageStudentsWhoForm = () => <MessageStudentsWhoForm assignment={this.props.assignment} />
renderMessageStudentsWhoModal() {
return (
<ConfirmDialog
open={this.state.messageStudentsWhoOpen}
working={this.state.sendingMessageStudentsWhoNow}
disabled={this.state.sendingMessageStudentsWhoNow}
heading={I18n.t('Message Students Who...')}
body={this.renderMessageStudentsWhoForm}
buttons={this.messageStudentsWhoButtonProps}
onDismiss={this.handleCloseMessageStudentsWho}
modalProps={{size: 'medium'}}
spinnerLabel={I18n.t('sending messages...')}
/>
)
}
render() {
const dirty = this.state.isDirty
const assignment = this.state.workingAssignment
@ -176,7 +241,7 @@ export default class TeacherView extends React.Component {
return (
<TeacherViewContext.Provider value={this.contextValue}>
<div className={clazz}>
{this.renderConfirmDialog()}
{this.renderDeleteDialog()}
<ScreenReaderContent>
<h1>{assignment.name}</h1>
</ScreenReaderContent>
@ -192,10 +257,7 @@ export default class TeacherView extends React.Component {
onChangeAssignment={this.handleChangeAssignment}
readOnly={this.state.readOnly}
/>
<MessageStudentsWho
open={this.state.messageStudentsWhoOpen}
onDismiss={this.handleDismissMessageStudentsWho}
/>
{this.renderMessageStudentsWhoModal()}
{dirty ? (
<TeacherFooter
onCancel={this.handleCancel}

View File

@ -178,7 +178,7 @@ export default class Toolbox extends React.Component {
<FlexItem key="message students" padding="xx-small xx-small xxx-small">
{hasSubmission(this.props.assignment)
? this.renderUnsubmittedButton()
: this.renderMessageStudentsWhoButton(I18n.t('Message Students Who'))}
: this.renderMessageStudentsWhoButton(I18n.t('Message Students'))}
</FlexItem>
]
}

View File

@ -18,6 +18,7 @@
import React from 'react'
import {render, fireEvent} from 'react-testing-library'
import '../../test-utils'
import ConfirmDialog from '../ConfirmDialog'
@ -25,10 +26,10 @@ function renderConfirmDialog(overrideProps = {}) {
const props = {
open: true,
working: false,
disabled: false,
heading: 'the thing',
message: 'do you want to do the thing?',
confirmLabel: 'do the thing',
cancelLabel: 'refrain from doing the thing',
body: () => 'do you want to do the thing?',
buttons: () => [{children: 'a button', 'data-testid': 'the-button'}],
closeLabel: 'close the dialog',
spinnerLabel: 'doing the thing',
...overrideProps
@ -36,31 +37,46 @@ function renderConfirmDialog(overrideProps = {}) {
return render(<ConfirmDialog {...props} />)
}
it('renders the body', () => {
const {getByText} = renderConfirmDialog()
expect(getByText('do you want to do the thing?')).toBeInTheDocument()
})
it('triggers close', () => {
const onClose = jest.fn()
const {getByTestId} = renderConfirmDialog({onClose})
fireEvent.click(getByTestId('confirm-dialog-close-button'))
expect(onClose).toHaveBeenCalled()
const onDismiss = jest.fn()
const {getByText} = renderConfirmDialog({onDismiss})
fireEvent.click(getByText('close the dialog'))
expect(onDismiss).toHaveBeenCalled()
})
it('triggers cancel', () => {
const onCancel = jest.fn()
const {getByTestId} = renderConfirmDialog({onCancel})
fireEvent.click(getByTestId('confirm-dialog-cancel-button'))
expect(onCancel).toHaveBeenCalled()
it('creates buttons and passes through button properties', () => {
const clicked = jest.fn()
const {getByTestId} = renderConfirmDialog({
buttons: () => [
{children: 'click me', onClick: clicked, 'data-testid': 'test-button'},
{children: 'other button', disabled: true, 'data-testid': 'disabled-button'}
]
})
fireEvent.click(getByTestId('test-button'))
expect(clicked).toHaveBeenCalled()
expect(getByTestId('disabled-button').getAttribute('disabled')).toBe('')
})
it('triggers confirm', () => {
const onConfirm = jest.fn()
const {getByTestId} = renderConfirmDialog({onConfirm})
fireEvent.click(getByTestId('confirm-dialog-confirm-button'))
expect(onConfirm).toHaveBeenCalled()
})
it('shows the spinner and disabled buttons when working', () => {
it('shows the spinner with enabled buttons when working', () => {
const {getByText, getByTestId} = renderConfirmDialog({working: true})
expect(getByText('doing the thing')).toBeInTheDocument()
expect(getByTestId('confirm-dialog-close-button').getAttribute('disabled')).toBe('')
expect(getByTestId('confirm-dialog-cancel-button').getAttribute('disabled')).toBe('')
expect(getByTestId('confirm-dialog-confirm-button').getAttribute('disabled')).toBe('')
expect(getByTestId('confirm-dialog-close-button').getAttribute('disabled')).toBe(null)
expect(getByTestId('the-button').getAttribute('disabled')).toBe(null)
})
it('shows custom mask body', () => {
const {getByText} = renderConfirmDialog({busyMaskBody: () => 'I am busy', working: true})
expect(getByText('I am busy')).toBeInTheDocument()
})
it('shows disabled buttons when disabled', () => {
const {getByText, getByTestId} = renderConfirmDialog({disabled: true})
expect(() => getByText('doing the thing')).toThrow()
expect(getByTestId('confirm-dialog-close-button').getAttribute('disabled')).toBe('')
expect(getByTestId('the-button').getAttribute('disabled')).toBe('')
})

View File

@ -0,0 +1,62 @@
/*
* Copyright (C) 2019 - 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 {render, fireEvent} from 'react-testing-library'
import {closest, mockAssignment, mockSubmission, mockUser} from '../../test-utils'
import MessageStudentsWhoForm from '../MessageStudentsWhoForm'
function mockAssignmentWithStudents(students) {
const submissions = students.map(student => mockSubmission({user: mockUser(student)}))
return mockAssignment({submissions: {nodes: submissions}})
}
describe('MessageStudentsWhoForm', () => {
it('shows a list of students to message', () => {
const assignment = mockAssignmentWithStudents([
{lid: '1', gid: 'g1', name: 'first'},
{lid: '2', gid: 'g2', name: 'second'},
{lid: '3', gid: 'g3', name: 'third'}
])
const {getByText} = render(<MessageStudentsWhoForm assignment={assignment} />)
expect(getByText('first')).toBeInTheDocument()
expect(getByText('second')).toBeInTheDocument()
expect(getByText('third')).toBeInTheDocument()
})
it('removes students from the list', () => {
const assignment = mockAssignmentWithStudents([
{lid: '1', gid: 'g1', name: 'first'},
{lid: '2', gid: 'g2', name: 'second'}
])
const {getByText, queryByText} = render(<MessageStudentsWhoForm assignment={assignment} />)
const deleteFirstButton = closest(getByText('first'), 'button')
fireEvent.click(deleteFirstButton)
expect(queryByText('first')).toBeNull()
expect(getByText('second')).toBeInTheDocument()
})
// TODO: future tests to implement
/* eslint-disable jest/no-disabled-tests */
it.skip('populates selected students when message students who dropdown is changed', () => {
// need multiple submission statuses to check this
})
/* eslint-enable jest/no-disabled-tests */
})

View File

@ -25,20 +25,6 @@ import {
describe('TeacherView', () => {
describe('basic TeacherView stuff', () => {
it('shows the message students who dialog when the unsubmitted button is clicked', async () => {
const {getByText, getByTestId} = await renderTeacherView()
fireEvent.click(getByText(/unsubmitted/i))
expect(await waitForElement(() => getByTestId('message-students-who'))).toBeInTheDocument()
})
it('shows the message students who dialog when the message students who button is clicked', async () => {
const {getByText, getByTestId} = await renderTeacherView(
mockAssignment({submissionTypes: ['none']})
)
fireEvent.click(getByText(/message students who/i))
expect(await waitForElement(() => getByTestId('message-students-who'))).toBeInTheDocument()
})
it('shows the assignment', async () => {
const assignment = mockAssignment()
const {getByText} = await renderTeacherView(assignment)

View File

@ -47,7 +47,7 @@ describe('assignments 2 teacher view toolbox', () => {
/\/courses\/course-lid\/gradebook\/speed_grader\?assignment_id=assignment-lid/
)
expect(closest(getByText('1 unsubmitted'), 'button')).toBeTruthy()
expect(queryByText(/message students who/i)).toBeNull()
expect(queryByText(/message students/i)).toBeNull()
expect(getByTestId('AssignmentPoints')).toBeInTheDocument()
})
@ -63,13 +63,13 @@ describe('assignments 2 teacher view toolbox', () => {
expect(sgLink.getAttribute('target')).toEqual('_blank')
})
it('renders the message students who button when the assignment does not have an online submission', () => {
it('renders the message students button when the assignment does not have an online submission', () => {
const assignment = mockAssignment({
submissionTypes: ['on_paper']
})
const {queryByText, getByText} = renderToolbox(assignment)
expect(queryByText('unsubmitted', {exact: false})).toBeNull()
expect(getByText(/message students who/i)).toBeInTheDocument()
expect(getByText(/message students/i)).toBeInTheDocument()
})
})
@ -78,5 +78,5 @@ it('does not render submission and grading links when assignment is not publishe
const {queryByText} = renderToolbox(assignment)
expect(queryByText('to grade', {exact: false})).toBeNull()
expect(queryByText('unsubmitted', {exact: false})).toBeNull()
expect(queryByText('message students who', {exact: false})).toBeNull()
expect(queryByText('message students', {exact: false})).toBeNull()
})

View File

@ -41,9 +41,9 @@ describe('assignments 2 delete dialog', () => {
it('allows cancel', async () => {
const {getByTestId} = await openDeleteDialog()
const cancelButton = await waitForElement(() => getByTestId('confirm-dialog-cancel-button'))
const cancelButton = await waitForElement(() => getByTestId('delete-dialog-cancel-button'))
fireEvent.click(cancelButton)
expect(await waitForNoElement(() => getByTestId('confirm-dialog-cancel-button'))).toBe(true)
expect(await waitForNoElement(() => getByTestId('delete-dialog-cancel-button'))).toBe(true)
})
it('deletes the assignment and reloads', async () => {
@ -53,24 +53,21 @@ describe('assignments 2 delete dialog', () => {
workflowMutationResult(assignment, 'deleted')
])
const reallyDeleteButton = await waitForElement(() =>
getByTestId('confirm-dialog-confirm-button')
getByTestId('delete-dialog-confirm-button')
)
fireEvent.click(reallyDeleteButton)
await wait(() => expect(reloadSpy).toHaveBeenCalled())
})
/* eslint-disable jest/no-disabled-tests */
// errors aren't really implemented yet
it.skip('reports errors', async () => {
it('reports errors', async () => {
const assignment = mockAssignment()
const {getByTestId} = await openDeleteDialog(assignment, [
// mutation result with an error
const {getByTestId, getByText} = await openDeleteDialog(assignment, [
workflowMutationResult(assignment, 'deleted', 'well rats')
])
const reallyDeleteButton = await waitForElement(() =>
getByTestId('confirm-dialog-confirm-button')
getByTestId('delete-dialog-confirm-button')
)
fireEvent.click(reallyDeleteButton)
// await waitForElement(() => {getBySomething('some kind of error message alert')})
expect(await waitForElement(() => getByText(/unable to delete/i))).toBeInTheDocument()
})
/* eslint-enable jest/no-disabled-tests */
})

View File

@ -0,0 +1,71 @@
/*
* Copyright (C) 2019 - 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 {fireEvent, waitForElement} from 'react-testing-library'
import {renderTeacherView} from './integration-utils'
import {mockAssignment, waitForNoElement} from '../../../test-utils'
jest.mock('jsx/shared/rce/RichContentEditor')
// TODO: some of these tests are essentially duplicates of the delete dialog tests. Should unify somehow.
describe('MessageStudentsWho integration', () => {
it('shows the message students who dialog when the unsubmitted button is clicked', async () => {
const {getByText, queryByText} = await renderTeacherView()
expect(queryByText('Message Students Who...')).toBeNull()
fireEvent.click(getByText(/unsubmitted/i))
expect(await waitForElement(() => getByText('Message Students Who...'))).toBeInTheDocument()
})
it('shows the message students who dialog when the message students who button is clicked', async () => {
const {getByText} = await renderTeacherView(mockAssignment({submissionTypes: ['none']}))
fireEvent.click(getByText(/message students/i))
expect(await waitForElement(() => getByText('Message Students Who...'))).toBeInTheDocument()
})
it('closes message students who when cancel is clicked', async () => {
const {getByText} = await renderTeacherView()
fireEvent.click(getByText(/unsubmitted/i))
await waitForElement(() => getByText('Message Students Who...'))
fireEvent.click(getByText(/cancel/i))
await waitForNoElement(() => getByText('Message Students Who...'))
})
it('closes message students who when the close button is clicked', async () => {
const {getByText, getByTestId} = await renderTeacherView()
fireEvent.click(getByText(/unsubmitted/i))
await waitForElement(() => getByText('Message Students Who...'))
fireEvent.click(getByTestId('confirm-dialog-close-button'))
await waitForNoElement(() => getByText('Message Students Who...'))
})
/* eslint-disable jest/no-disabled-tests */
describe.skip('sending messages', () => {
it('calls api to message remaining students when "send" is clicked', () => {
// check set of students, subject, and message parameters
})
it('disables the dialog while sending is in progress', () => {
// make the api call wait until we say it can finish
})
it('dismisses the dialog and sr-flashes success when the save finishes successfully', () => {})
it('renders errors, does not dismiss the dialog, and reenables it when the save fails', () => {})
})
/* eslint-enable jest/no-disabled-tests */
})

View File

@ -53,8 +53,8 @@ export async function waitForNoElement(queryFn) {
return true
}
export function workflowMutationResult(assignment, newWorkflowState) {
return {
export function workflowMutationResult(assignment, newWorkflowState, errorMessage) {
const result = {
request: {
query: SET_WORKFLOW,
variables: {
@ -74,6 +74,10 @@ export function workflowMutationResult(assignment, newWorkflowState) {
}
}
}
if (errorMessage !== undefined) {
result.error = new Error(errorMessage)
}
return result
}
export function mockCourse(overrides) {

View File

@ -23,6 +23,3 @@ import 'react-testing-library/cleanup-after-each'
// doesn't, we could remove this, though it might make sense to leave it if
// rendering the actual rce is slow.
jest.mock('jsx/shared/rce/RichContentEditor')
// silence errors from jsdom
window.scroll = () => {}

View File

@ -21,6 +21,9 @@ import Adapter from 'enzyme-adapter-react-16'
window.fetch = require('unfetch')
window.scroll = () => {}
window.ENV = {}
Enzyme.configure({ adapter: new Adapter() })
// because InstUI themeable components need an explicit "dir" attribute on the <html> element