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:
parent
2c53fa4402
commit
559dc2953f
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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('')
|
||||
})
|
||||
|
|
|
@ -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 */
|
||||
})
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
})
|
||||
|
|
|
@ -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 */
|
||||
})
|
||||
|
|
|
@ -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 */
|
||||
})
|
|
@ -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) {
|
||||
|
|
|
@ -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 = () => {}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue