add required peer review link to assignment header

add a link to the assignment header that allows the user to navigate
between assigned peer reviews

closes EVAL-3182
flag=peer_reviews_for_a2

test plan:
- Have a course with at three students
- Create an assignment with anonymous peer review disabled
- Assign the same student the other two peer reviews
- As the primary student with the two peer reviews, do not submit
  the assignment.
- Go to the non-anonymous peer review assignment page and see that
  there is a link called Required Peer Reviews on the header
- Confirm that all two students are under the Not Yet Submitted
  category when the link is clicked on and the popover is displayed
- Click on one of the students in the popover and see that it will
  navigate to the student's peer review page
- Confirm that the student whose peer review page is being viewed is
  the same student on the popover that has a grey box
- Confirm that there is a required peer review link on the page
  and the page says that you need to first submit before you
  can peer review
- As the primary student, submit the assignment and click on another
  student in the required peer review link
- Confirm that the page says the no submissions are available to review
  yet and the peer review link is shown on the page
- Go to one of the peer reviewee's account and submit the assignment
- Go back to the primary student's account and go to that assignments
  page.
- Confirm that the peer reviewee who just submitted is now in the
  Ready to Review section of the popover
- Click on the peer reviewee in the popover and submit a peer review
- Open the peer review popover again and confirm that the peer reviewee
  is now in the Completed Peer Reviews Section
- Perform the same steps with an anonymous assignment and confirm that
  the names on the popover says Anonymous with a number.
- Perform the same steps with a non-anonymous assignment with a rubric
- Create an assignment with peer reviews enabled but no peer reviews
  assigned
- Confirm that there is no required peer review link for the assignment

Change-Id: I314cb0842620cf5cf7129ec0bbe17064eb8f4dbe
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/323717
Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
Reviewed-by: Spencer Olson <solson@instructure.com>
Reviewed-by: Derek Williams <derek.williams@instructure.com>
QA-Review: Derek Williams <derek.williams@instructure.com>
Product-Review: Ravi Koll <ravi.koll@instructure.com>
This commit is contained in:
Samuel Lee 2023-07-27 10:06:30 -05:00
parent 6c2a88c254
commit e7e9f2aa4d
15 changed files with 593 additions and 24 deletions

6
ui/api.d.ts vendored
View File

@ -278,12 +278,12 @@ export type AssessmentRequest = Readonly<{
export type AssignedAssessments = {
assetId: string
workflowState: string
assetSubmissionType: string
assetSubmissionType: string | null
anonymizedUser?: {
displayName: string
_id: string
}
anonymousId?: string
} | null
anonymousId?: string | null
}
export type AttachmentData = Readonly<{

View File

@ -37,7 +37,7 @@ import StudentViewContext from '../Context'
import {SUBMISSION_COMMENT_QUERY} from '@canvas/assignments/graphql/student/Queries'
import {Submission} from '@canvas/assignments/graphql/student/Submission'
import {useQuery} from 'react-apollo'
import {bool} from 'prop-types'
import {bool, func} from 'prop-types'
import PeerReviewPromptModal from '../PeerReviewPromptModal'
import {
getRedirectUrlToFirstPeerReview,
@ -189,6 +189,7 @@ export default function CommentsTrayBody(props) {
onSendCommentSuccess={() => {
if (props.isPeerReviewEnabled && !props.assignment.rubric) {
handlePeerReviewPromptModal()
props.onSuccessfulPeerReview?.(props.reviewerSubmission)
}
}}
/>
@ -221,6 +222,7 @@ CommentsTrayBody.propTypes = {
submission: Submission.shape.isRequired,
reviewerSubmission: Submission.shape,
isPeerReviewEnabled: bool,
onSuccessfulPeerReview: func,
}
CommentsTrayBody.defaultProps = {

View File

@ -60,6 +60,7 @@ function TrayContent(props) {
submission={props.submission}
reviewerSubmission={props.reviewerSubmission}
isPeerReviewEnabled={props.isPeerReviewEnabled}
onSuccessfulPeerReview={props.onSuccessfulPeerReview}
/>
</Suspense>
)
@ -70,6 +71,7 @@ TrayContent.propTypes = {
submission: Submission.shape.isRequired,
reviewerSubmission: Submission.shape,
isPeerReviewEnabled: bool,
onSuccessfulPeerReview: func,
}
TrayContent.defaultProps = {
@ -120,6 +122,7 @@ export default function CommentsTray(props) {
assignment={props.assignment}
submission={props.submission}
reviewerSubmission={props.reviewerSubmission}
onSuccessfulPeerReview={props.onSuccessfulPeerReview}
/>
</Flex.Item>
</Flex>
@ -135,6 +138,7 @@ CommentsTray.propTypes = {
closeTray: func.isRequired,
open: bool.isRequired,
isPeerReviewEnabled: bool,
onSuccessfulPeerReview: func,
}
CommentsTray.defaultProps = {

View File

@ -25,6 +25,7 @@ import SubmissionManager from './SubmissionManager'
import {Submission} from '@canvas/assignments/graphql/student/Submission'
import {Tabs} from '@instructure/ui-tabs'
import {View} from '@instructure/ui-view'
import PropTypes from 'prop-types'
const I18n = useI18nScope('assignments_2')
@ -40,6 +41,7 @@ ContentTabs.propTypes = {
assignment: Assignment.shape,
submission: Submission.shape,
reviewerSubmission: Submission.shape,
onSuccessfulPeerReview: PropTypes.func,
}
function LoggedInContentTabs(props) {
@ -52,6 +54,7 @@ function LoggedInContentTabs(props) {
assignment={props.assignment}
submission={props.submission}
reviewerSubmission={props.reviewerSubmission}
onSuccessfulPeerReview={props.onSuccessfulPeerReview}
/>
</View>
</div>

View File

@ -30,7 +30,6 @@ import {Link} from '@instructure/ui-link'
import {IconChatLine, IconQuestionLine} from '@instructure/ui-icons'
import {useScope as useI18nScope} from '@canvas/i18n'
import LatePolicyToolTipContent from './LatePolicyStatusDisplay/LatePolicyToolTipContent'
import {Popover} from '@instructure/ui-popover'
import {arrayOf, func} from 'prop-types'
import OriginalityReport from './OriginalityReport'
import React from 'react'
@ -43,10 +42,12 @@ import {Text} from '@instructure/ui-text'
import {Tooltip} from '@instructure/ui-tooltip'
import {View} from '@instructure/ui-view'
import CommentsTray from './CommentsTray/index'
import {Popover} from '@instructure/ui-popover'
import {
getOriginalityData,
isOriginalityReportVisible,
} from '@canvas/grading/originalityReportHelper'
import PeerReviewNavigationLink from './PeerReviewNavigationLink'
const I18n = useI18nScope('assignments_2_student_header')
@ -57,6 +58,8 @@ class Header extends React.Component {
onChangeSubmission: func,
submission: Submission.shape,
reviewerSubmission: Submission.shape,
peerReviewLinkData: Submission.shape,
onSuccessfulPeerReview: func,
}
static defaultProps = {
@ -86,11 +89,11 @@ class Header extends React.Component {
)
}
currentAssessmentIndex = () => {
currentAssessmentIndex = assignedAssessments => {
const userId = this.props.assignment.env.revieweeId
const anonymousId = this.props.assignment.env.anonymousAssetId
const value =
this.props.reviewerSubmission?.assignedAssessments?.findIndex(assessment => {
assignedAssessments?.findIndex(assessment => {
return (
(userId && userId === assessment.anonymizedUser._id) ||
(anonymousId && assessment.anonymousId === anonymousId)
@ -276,15 +279,52 @@ class Header extends React.Component {
let topRightComponent
if (this.isPeerReviewModeEnabled()) {
topRightComponent = (
<Flex.Item margin="0 small 0 0">
<PeerReviewsCounter
current={this.currentAssessmentIndex()}
total={this.props.reviewerSubmission?.assignedAssessments?.length || 0}
/>
</Flex.Item>
<Flex direction="column" alignItems="end">
{this.props.peerReviewLinkData ? (
<Flex.Item padding="0 small 0 0">
<PeerReviewNavigationLink
assignedAssessments={this.props.peerReviewLinkData?.assignedAssessments}
currentAssessmentIndex={this.currentAssessmentIndex(
this.props.peerReviewLinkData?.assignedAssessments
)}
/>
</Flex.Item>
) : (
<>
<Flex.Item padding="0 small 0 0">
<PeerReviewsCounter
current={this.currentAssessmentIndex(
this.props.reviewerSubmission?.assignedAssessments
)}
total={this.props.reviewerSubmission?.assignedAssessments?.length || 0}
/>
</Flex.Item>
<Flex.Item padding="0 small 0 0">
<PeerReviewNavigationLink
assignedAssessments={this.props.reviewerSubmission?.assignedAssessments}
currentAssessmentIndex={this.currentAssessmentIndex(
this.props.reviewerSubmission?.assignedAssessments
)}
/>
</Flex.Item>
</>
)}
</Flex>
)
} else {
topRightComponent = <Flex.Item>{this.renderLatestGrade()}</Flex.Item>
topRightComponent = (
<Flex direction="column" alignItems="end">
<Flex.Item padding="0 small 0 0">{this.renderLatestGrade()}</Flex.Item>
{this.props.submission?.assignedAssessments?.length > 0 && (
<Flex.Item overflowX="hidden" padding="0 small 0 0">
<PeerReviewNavigationLink
assignedAssessments={this.props.submission.assignedAssessments}
currentAssessmentIndex={0}
/>
</Flex.Item>
)}
</Flex>
)
}
return (
@ -301,10 +341,13 @@ class Header extends React.Component {
<ScreenReaderContent> {this.props.assignment.name} </ScreenReaderContent>
</Heading>
<Flex as="div" margin="0" wrap="wrap" alignItems="start">
<Flex as="div" margin="0" wrap="wrap" alignItems="start" padding="0 0 large 0">
<Flex.Item shouldShrink={true}>
<AssignmentDetails assignment={this.props.assignment} />
</Flex.Item>
{this.props.peerReviewLinkData && (
<Flex.Item shouldGrow={true}>{topRightComponent}</Flex.Item>
)}
{this.props.submission && (
<Flex.Item shouldGrow={true}>
<Flex as="div" justifyItems="end" alignItems="center">
@ -323,6 +366,7 @@ class Header extends React.Component {
open={this.state.commentsTrayOpen}
closeTray={this.closeCommentsTray}
isPeerReviewEnabled={this.isPeerReviewModeEnabled()}
onSuccessfulPeerReview={this.props.onSuccessfulPeerReview}
/>
</Flex.Item>
)}

View File

@ -0,0 +1,179 @@
/*
* Copyright (C) 2023 - 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, {useState} from 'react'
import {useScope as useI18nScope} from '@canvas/i18n'
// @ts-expect-error
import {Popover} from '@instructure/ui-popover'
// @ts-expect-error
import {IconPeerReviewLine, IconCheckLine} from '@instructure/ui-icons'
// @ts-expect-error
import {TruncateText} from '@instructure/ui-truncate-text'
// @ts-expect-error
import {Tooltip} from '@instructure/ui-tooltip'
import {Menu} from '@instructure/ui-menu'
import {Flex} from '@instructure/ui-flex'
import {View} from '@instructure/ui-view'
import {Text} from '@instructure/ui-text'
import {Link} from '@instructure/ui-link'
import {getPeerReviewUrl} from '../helpers/PeerReviewHelpers'
import {AssignedAssessments} from 'api'
import {AccessibleContent} from '@instructure/ui-a11y-content'
const I18n = useI18nScope('assignments_2_student_header')
const {Item: MenuItem, Group: MenuGroup} = Menu as any
type PeerReviewNavigationLinkProps = {
assignedAssessments: AssignedAssessments[]
currentAssessmentIndex: number
}
type NavigationMenuItemLabelProps = {
assessment: AssignedAssessments
index: number
peerReviewStatus: string
}
const TruncateWithTooltip = ({children}: {children: React.ReactNode}) => {
const [isTruncated, setIsTruncated] = useState(false)
return isTruncated ? (
<Tooltip as="div" placement="start" renderTip={children}>
<TruncateText position="middle" onUpdate={setIsTruncated}>
{children}
</TruncateText>
</Tooltip>
) : (
<TruncateText onUpdate={setIsTruncated} position="middle">
{children}
</TruncateText>
)
}
const NavigationMenuItemLabel = ({
assessment,
index,
peerReviewStatus,
}: NavigationMenuItemLabelProps) => {
const label = assessment.anonymizedUser
? assessment.anonymizedUser?.displayName
: I18n.t('Anonymous %{peerReviewNumber}', {peerReviewNumber: index + 1})
return (
<AccessibleContent alt={`${label} ${peerReviewStatus}`}>
<TruncateWithTooltip>{label}</TruncateWithTooltip>
</AccessibleContent>
)
}
export default ({assignedAssessments, currentAssessmentIndex}: PeerReviewNavigationLinkProps) => {
const renderNavigationMenuItem = (
assessment: AssignedAssessments,
index: number,
testId: string,
peerReviewStatus: string
) => {
return (
<MenuItem
key={assessment.assetId}
href={getPeerReviewUrl(assessment)}
/* currentAssessmentIndex is a passed in prop that is already 1-indexed while the parameter 'index' is 0-indexed, hence the need for a +1.
This is comparing to see if the current peer review page we are on matches the current item in the map, which will then add a custom background
to the theme to the rendered menu item.
*/
theme={
currentAssessmentIndex === index + 1 ? {background: '#6B7780', labelColor: 'white'} : null
}
data-testid={`${testId}-${assessment.assetId}`}
>
<Flex as="div">
{testId === 'peer-review-completed' ? (
<View margin="0 x-small 0 0">
<IconCheckLine />
</View>
) : (
<View margin="0 medium 0 0" />
)}
<NavigationMenuItemLabel
assessment={assessment}
index={index}
peerReviewStatus={peerReviewStatus}
/>
</Flex>
</MenuItem>
)
}
return (
<Popover
renderTrigger={
<View as="div" margin="xx-small 0 xx-small xx-small" data-testid="header-peer-review-link">
<View margin="0 xx-small 0 0">
<IconPeerReviewLine />
</View>
<Link as="button" isWithinText={false}>
<Text weight="bold" size="small" color="brand">
{I18n.t('Required Peer Reviews')}
</Text>
</Link>
</View>
}
on="click"
placement="bottom end"
>
<Menu>
<MenuGroup label={I18n.t('Ready to Review')} />
{assignedAssessments?.map(
(assessment, index) =>
assessment.assetSubmissionType != null &&
assessment.workflowState === 'assigned' &&
renderNavigationMenuItem(
assessment,
index,
'peer-review-ready',
I18n.t('Ready to Review')
)
)}
<MenuGroup label={I18n.t('Not Yet Submitted')} />
{assignedAssessments?.map(
(assessment, index) =>
assessment.assetSubmissionType === null &&
renderNavigationMenuItem(
assessment,
index,
'peer-review-not-submitted',
I18n.t('Peer Review Not Yet Submitted')
)
)}
<MenuGroup label={I18n.t('Completed Peer Reviews')} />
{assignedAssessments?.map(
(assessment, index) =>
assessment.assetSubmissionType != null &&
assessment.workflowState === 'completed' &&
renderNavigationMenuItem(
assessment,
index,
'peer-review-completed',
I18n.t('Peer Review Completed')
)
)}
</Menu>
</Popover>
)
}

View File

@ -28,7 +28,7 @@ import MarkAsDoneButton from './MarkAsDoneButton'
import LoadingIndicator from '@canvas/loading-indicator'
import MissingPrereqs from './MissingPrereqs'
import DateLocked from '../DateLocked'
import React, {Suspense, lazy, useContext, useEffect} from 'react'
import React, {Suspense, lazy, useContext, useEffect, useState} from 'react'
import PropTypes from 'prop-types'
import {Spinner} from '@instructure/ui-spinner'
import {Submission} from '@canvas/assignments/graphql/student/Submission'
@ -128,7 +128,8 @@ function renderAttemptsAndAvailability(assignment) {
function renderContentBaseOnAvailability(
{assignment, submission, reviewerSubmission},
alertContext
alertContext,
onSuccessfulPeerReview
) {
if (assignment.env.modulePrereq) {
return <MissingPrereqs moduleUrl={assignment.env.moduleUrl} />
@ -195,6 +196,7 @@ function renderContentBaseOnAvailability(
assignment={assignment}
submission={submission}
reviewerSubmission={reviewerSubmission}
onSuccessfulPeerReview={onSuccessfulPeerReview}
/>
) : (
<SubmissionlessFooter onMarkAsDoneError={onMarkAsDoneError} />
@ -216,6 +218,7 @@ function renderContentBaseOnAvailability(
function StudentContent(props) {
const alertContext = useContext(AlertManagerContext)
const [assignedAssessments, setAssignedAssessments] = useState([])
const {description, name} = props.assignment
useEffect(() => {
@ -247,6 +250,10 @@ function StudentContent(props) {
setUpImmersiveReader()
}, [description, name])
const onSuccessfulPeerReview = (assignedAssessments) => {
setAssignedAssessments(assignedAssessments)
}
// TODO: Move the button provider up one level
return (
<div data-testid="assignments-2-student-view">
@ -257,8 +264,9 @@ function StudentContent(props) {
scrollThreshold={150}
submission={props.submission}
reviewerSubmission={props.reviewerSubmission}
onSuccessfulPeerReview={onSuccessfulPeerReview}
/>
{renderContentBaseOnAvailability(props, alertContext)}
{renderContentBaseOnAvailability(props, alertContext, onSuccessfulPeerReview)}
</div>
)
}
@ -269,6 +277,7 @@ StudentContent.propTypes = {
onChangeSubmission: PropTypes.func.isRequired,
submission: Submission.shape,
reviewerSubmission: Submission.shape,
onSuccessfulPeerReview: PropTypes.func,
}
StudentContent.defaultProps = {

View File

@ -61,7 +61,7 @@ class SubmissionHistoriesQuery extends React.Component {
if (shouldDisplayNeedsSubmissionPeerReview(this.props.initialQueryData)) {
return (
<>
<Header scrollThreshold={150} assignment={this.props.initialQueryData.assignment} />
<Header scrollThreshold={150} assignment={this.props.initialQueryData.assignment} peerReviewLinkData={this.props.initialQueryData.reviewerSubmission} />
<AssignmentToggleDetails
description={this.props.initialQueryData.assignment.description}
/>
@ -73,7 +73,7 @@ class SubmissionHistoriesQuery extends React.Component {
if (shouldDisplayUnavailablePeerReview(this.props.initialQueryData)) {
return (
<>
<Header scrollThreshold={150} assignment={this.props.initialQueryData.assignment} />
<Header scrollThreshold={150} assignment={this.props.initialQueryData.assignment} peerReviewLinkData={this.props.initialQueryData.reviewerSubmission}/>
<AssignmentToggleDetails
description={this.props.initialQueryData.assignment.description}
/>

View File

@ -167,7 +167,7 @@ CancelAttemptButton.propTypes = {
submission: PropTypes.object.isRequired,
}
const SubmissionManager = ({assignment, submission, reviewerSubmission}) => {
const SubmissionManager = ({assignment, submission, reviewerSubmission, onSuccessfulPeerReview}) => {
const [draftStatus, setDraftStatus] = useState(null)
const [editingDraft, setEditingDraft] = useState(false)
const [focusAttemptOnInit, setFocusAttemptOnInit] = useState(false)
@ -570,6 +570,7 @@ const SubmissionManager = ({assignment, submission, reviewerSubmission}) => {
if (assignment.env.peerReviewModeEnabled) {
const matchingAssessment = assignedAssessments.find(x => x.assetId === submission._id)
if (matchingAssessment) matchingAssessment.workflowState = 'completed'
onSuccessfulPeerReview?.(assignedAssessments)
}
const {availableCount, unavailableCount} = availableAndUnavailableCounts(assignedAssessments)
handlePeerReviewPromptSettings(availableCount, unavailableCount)
@ -868,6 +869,7 @@ SubmissionManager.propTypes = {
assignment: Assignment.shape,
submission: Submission.shape,
reviewerSubmission: Submission.shape,
onSuccessfulPeerReview: PropTypes.func,
}
export default SubmissionManager

View File

@ -91,6 +91,7 @@ class ViewManager extends React.Component {
displayedAttempt: null,
dummyNextSubmission: null,
submissions: [],
reviewerSubmission: [],
}
static getDerivedStateFromProps(props, state) {

View File

@ -1024,5 +1024,30 @@ describe('CommentsTrayBody', () => {
expect(queryByTestId('peer-review-prompt-modal')).not.toBeInTheDocument()
})
it('calls the onSuccessfulPeerReview function to re-render page when a peer review comment is successful', async () => {
const mocks = await Promise.all([
mockSubmissionCommentQuery({}, {peerReview: true}),
mockCreateSubmissionComment(),
])
const onSuccessfulPeerReviewMockFunction = jest.fn()
const props = {
...(await getDefaultPropsWithReviewerSubmission('assigned')),
onSuccessfulPeerReview: onSuccessfulPeerReviewMockFunction,
}
props.isPeerReviewEnabled = true
const {findByPlaceholderText, getByText} = render(
mockContext(
<MockedProvider mocks={mocks}>
<CommentsTrayBody {...props} />
</MockedProvider>
)
)
const textArea = await findByPlaceholderText('Submit a Comment')
fireEvent.change(textArea, {target: {value: 'lion'}})
fireEvent.click(getByText('Send Comment'))
await waitFor(() => expect(onSuccessfulPeerReviewMockFunction).toHaveBeenCalled())
expect(props.reviewerSubmission.assignedAssessments[0].workflowState).toEqual('completed')
})
})
})

View File

@ -373,6 +373,7 @@ it('does not render the attempt select if peerReviewModeEnabled is set to true',
...props.submission,
assignedAssessments: [
{
assetId: '1',
anonymousUser: null,
anonymousId: 'xaU9cd',
workflowState: 'assigned',
@ -443,6 +444,7 @@ describe('submission workflow tracker', () => {
...props.submission,
assignedAssessments: [
{
assetId: '1',
anonymousUser: null,
anonymousId: 'xaU9cd',
workflowState: 'assigned',
@ -768,6 +770,7 @@ describe('Add Comment/View Feedback button', () => {
...props.submission,
assignedAssessments: [
{
assetId: '1',
anonymousUser: null,
anonymousId: 'xaU9cd',
workflowState: 'assigned',
@ -794,6 +797,7 @@ describe('Add Comment/View Feedback button', () => {
...props.submission,
assignedAssessments: [
{
assetId: '1',
anonymousUser: null,
anonymousId: 'xaU9cd',
workflowState: 'assigned',
@ -825,6 +829,7 @@ describe('Peer reviews counter', () => {
...props.submission,
assignedAssessments: [
{
assetId: '1',
anonymousUser: null,
anonymousId: 'xaU9cd',
workflowState: 'assigned',
@ -854,16 +859,19 @@ describe('Peer reviews counter', () => {
...props.submission,
assignedAssessments: [
{
assetId: '1',
anonymousId: 'xaU9cd',
workflowState: 'assigned',
assetSubmissionType: 'online_text_entry',
},
{
assetId: '2',
anonymousId: 'maT9fd',
workflowState: 'assigned',
assetSubmissionType: 'online_text_entry',
},
{
assetId: '3',
anonymousId: 'vaN9fd',
workflowState: 'assigned',
assetSubmissionType: 'online_text_entry',
@ -902,19 +910,22 @@ describe('Peer reviews counter', () => {
...props.submission,
assignedAssessments: [
{
anonymizedUser: {_id: '1'},
assetId: '1',
anonymizedUser: {_id: '1', displayName: "Jim"},
anonymousId: null,
workflowState: 'assigned',
assetSubmissionType: 'online_text_entry',
},
{
anonymizedUser: {_id: '2'},
assetId: '2',
anonymizedUser: {_id: '2', displayName: "Bob"},
anonymousId: null,
workflowState: 'assigned',
assetSubmissionType: 'online_text_entry',
},
{
anonymizedUser: {_id: '3'},
assetId: '3',
anonymizedUser: {_id: '3', displayName: "Tim"},
anonymousId: null,
workflowState: 'assigned',
assetSubmissionType: 'online_text_entry',
@ -951,11 +962,13 @@ describe('Peer reviews counter', () => {
...props.submission,
assignedAssessments: [
{
assetId: '1',
anonymousId: 'xaU9cd',
workflowState: 'assigned',
assetSubmissionType: 'online_text_entry',
},
{
assetId: '2',
anonymousId: 'maT9fd',
workflowState: 'assigned',
assetSubmissionType: 'online_text_entry',
@ -966,4 +979,78 @@ describe('Peer reviews counter', () => {
const assessmentsCount = props.reviewerSubmission.assignedAssessments.length
expect(queryByTestId('total-counter')).toHaveTextContent(assessmentsCount.toString())
})
describe('required peer reviews link in assignment header with peer review mode disabled', () => {
let props
beforeAll(async () => {
props = await mockAssignmentAndSubmission({
Submission: {...SubmissionMocks.submitted},
})
props.allSubmissions = [props.submission]
props.assignment.env.peerReviewModeEnabled = false
props.submission.assignedAssessments = [
{
assetId: '1',
anonymizedUser: {_id: '1', displayName: 'Jim'},
anonymousId: null,
workflowState: 'assigned',
assetSubmissionType: null,
},
]
})
it('renders the required peer review link with peer reviews assigned', () => {
const {queryByTestId} = render(<Header {...props} />)
expect(queryByTestId('assignment-student-header')).toHaveTextContent('Required Peer Reviews')
})
it('does not render the required peer review link with the number of peer reviews assigned when no peer reviews are assigned', () => {
props.submission.assignedAssessments = []
const {queryByTestId} = render(<Header {...props} />)
expect(queryByTestId('header-peer-review-link')).toBeNull()
})
})
describe('required peer reviews link in assignment header with peer review mode enabled', () => {
let props
beforeAll(async () => {
props = await mockAssignmentAndSubmission()
props.assignment.env.peerReviewModeEnabled = true
})
it('renders the required peer review link with peer reviews assigned when both the reviewer and reviewee have submitted to the assignment', () => {
props.reviewerSubmission = {
...props.submission,
assignedAssessments: [
{
assetId: '1',
anonymousUser: null,
anonymousId: 'xaU9cd',
workflowState: 'assigned',
assetSubmissionType: 'online_text_entry',
},
],
}
const {queryByTestId} = render(<Header {...props} />)
expect(queryByTestId('assignment-student-header')).toHaveTextContent('Required Peer Reviews')
})
it('renders the required peer review link with peer reviews assigned when no reviews are ready or reviewer has not submitted to the assignment', () => {
props.reviewerSubmission = null
props.peerReviewLinkData = {
...props.submission,
assignedAssessments: [
{
assetId: '1',
anonymousUser: null,
anonymousId: 'xaU9cd',
workflowState: 'assigned',
assetSubmissionType: null,
},
],
}
const {queryByTestId} = render(<Header {...props} />)
expect(queryByTestId('assignment-student-header')).toHaveTextContent('Required Peer Reviews')
})
})
})

View File

@ -0,0 +1,157 @@
/*
* Copyright (C) 2023 - 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 '@testing-library/react'
import PeerReviewNavigationLink from '../PeerReviewNavigationLink'
const props = {
assignedAssessments: [
{
assetId: '1',
anonymizedUser: {_id: '1', displayName: 'Jim'},
anonymousId: null,
workflowState: 'completed',
assetSubmissionType: 'online_text_entry',
},
{
assetId: '2',
anonymizedUser: {_id: '2', displayName: 'Bob'},
anonymousId: null,
workflowState: 'assigned',
assetSubmissionType: null,
},
{
assetId: '3',
anonymizedUser: {_id: '3', displayName: 'Jill'},
anonymousId: null,
workflowState: 'assigned',
assetSubmissionType: 'online_text_entry',
},
],
currentAssessmentIndex: 1,
}
it('displays the labels for peer review states', () => {
const {getByTestId, getByText} = render(<PeerReviewNavigationLink {...props} />)
fireEvent.click(getByTestId('header-peer-review-link'))
expect(getByText('Ready to Review')).toBeInTheDocument()
expect(getByText('Not Yet Submitted')).toBeInTheDocument()
expect(getByText('Completed Peer Reviews')).toBeInTheDocument()
})
it('displays a gray highlight on the current peer review', () => {
const {getByTestId} = render(<PeerReviewNavigationLink {...props} />)
fireEvent.click(getByTestId('header-peer-review-link'))
const completedMenuItem = getByTestId('peer-review-completed-1')
const style = window.getComputedStyle(completedMenuItem as Element) as {[key: string]: any}
const labelColorKey = Object.keys(style._values).find(key => key.includes('labelColor'))
const backgroundKey = Object.keys(style._values).find(key => key.includes('background'))
expect(style._values[labelColorKey ?? '']).toEqual('white')
expect(style._values[backgroundKey ?? '']).toEqual('#6B7780')
})
describe('required peer review link when the anonymous peer review option is disabled', () => {
it('displays a ready peer review in the Ready to Review section', () => {
const {getByTestId} = render(<PeerReviewNavigationLink {...props} />)
fireEvent.click(getByTestId('header-peer-review-link'))
expect(getByTestId('peer-review-ready-3')).toHaveTextContent('Jill')
})
it('displays a ready peer review in the Not Yet Submitted section', () => {
const {getByTestId} = render(<PeerReviewNavigationLink {...props} />)
fireEvent.click(getByTestId('header-peer-review-link'))
expect(getByTestId('peer-review-not-submitted-2')).toHaveTextContent('Bob')
})
it('displays a ready peer review in the Completed Peer Reviews section', () => {
const {getByTestId} = render(<PeerReviewNavigationLink {...props} />)
fireEvent.click(getByTestId('header-peer-review-link'))
expect(getByTestId('peer-review-completed-1')).toHaveTextContent('Jim')
})
it('contains the correct url for the item is clicked on', () => {
ENV.COURSE_ID = '1'
ENV.ASSIGNMENT_ID = '1'
const {getByTestId} = render(<PeerReviewNavigationLink {...props} />)
fireEvent.click(getByTestId('header-peer-review-link'))
expect(getByTestId('peer-review-completed-1')).toHaveAttribute(
'href',
'/courses/1/assignments/1?reviewee_id=1'
)
})
})
describe('required peer review link when the anonymous peer review option is enabled', () => {
const props = {
assignedAssessments: [
{
assetId: '1',
anonymizedUser: null,
anonymousId: 'anon_1',
workflowState: 'completed',
assetSubmissionType: 'online_text_entry',
},
{
assetId: '2',
anonymizedUser: null,
anonymousId: 'anon_2',
workflowState: 'assigned',
assetSubmissionType: null,
},
{
assetId: '3',
anonymizedUser: null,
anonymousId: 'anon_3',
workflowState: 'assigned',
assetSubmissionType: 'online_text_entry',
},
],
currentAssessmentIndex: 1,
}
it('displays a ready peer review in the Ready to Review section', () => {
const {getByTestId} = render(<PeerReviewNavigationLink {...props} />)
fireEvent.click(getByTestId('header-peer-review-link'))
expect(getByTestId('peer-review-ready-3')).toHaveTextContent('Anonymous 3')
})
it('displays a ready peer review in the Not Yet Submitted section', () => {
const {getByTestId} = render(<PeerReviewNavigationLink {...props} />)
fireEvent.click(getByTestId('header-peer-review-link'))
expect(getByTestId('peer-review-not-submitted-2')).toHaveTextContent('Anonymous 2')
})
it('displays a ready peer review in the Completed Peer Reviews section', () => {
const {getByTestId} = render(<PeerReviewNavigationLink {...props} />)
fireEvent.click(getByTestId('header-peer-review-link'))
expect(getByTestId('peer-review-completed-1')).toHaveTextContent('Anonymous 1')
})
it('contains the correct url for the item is clicked on', () => {
ENV.COURSE_ID = '1'
ENV.ASSIGNMENT_ID = '1'
const {getByTestId} = render(<PeerReviewNavigationLink {...props} />)
fireEvent.click(getByTestId('header-peer-review-link'))
expect(getByTestId('peer-review-completed-1')).toHaveAttribute(
'href',
'/courses/1/assignments/1?anonymous_asset_id=anon_1'
)
})
})

View File

@ -1964,6 +1964,58 @@ describe('SubmissionManager', () => {
expect(await findByText(COMPLETED_PEER_REVIEW_TEXT)).toBeTruthy()
})
it('calls the onSuccessfulPeerReview function to re-render page when a peer review with rubric is successful', async () => {
setOtherUserAsAssessmentOwner()
store.setState({
displayedAssessment: {
score: 5,
data: [
generateAssessmentItem(props.assignment.rubric.criteria[0].id, {hasComments: true}),
generateAssessmentItem(props.assignment.rubric.criteria[1].id, {hasComments: true}),
],
},
})
const assetId = props.submission._id
const reviewerSubmission = {
id: 'test-id',
_id: 'test-id',
assignedAssessments: [
{
assetId,
workflowState: 'assigned',
assetSubmissionType: 'online-text',
},
{
assetId: 'some other user id',
workflowState: 'assigned',
assetSubmissionType: 'online-text',
},
],
}
props.reviewerSubmission = reviewerSubmission
props.assignment.env.peerReviewModeEnabled = true
props.assignment.env.peerReviewAvailable = true
const onSuccessfulPeerReviewMockFunction = jest.fn()
const prop = {
...props,
onSuccessfulPeerReview: onSuccessfulPeerReviewMockFunction,
}
const {getByText, findByText} = render(
<MockedProvider mocks={mocks}>
<SubmissionManager {...prop} />
</MockedProvider>
)
await new Promise(resolve => setTimeout(resolve, 1))
const submitButton = getByText('Submit')
fireEvent.click(submitButton)
await waitFor(() => expect(onSuccessfulPeerReviewMockFunction).toHaveBeenCalled())
expect(props.reviewerSubmission.assignedAssessments[0].workflowState).toEqual('completed')
})
it('creates an error alert when the http request fails', async () => {
setOtherUserAsAssessmentOwner()
doFetchApi.mockImplementation(() => Promise.reject(new Error('Network error')))

View File

@ -31,6 +31,10 @@ export const getRedirectUrlToFirstPeerReview = (
if (!assessment) {
return
}
return getPeerReviewUrl(assessment)
}
export const getPeerReviewUrl = (assessment: AssignedAssessments) => {
let url = `/courses/${ENV.COURSE_ID}/assignments/${ENV.ASSIGNMENT_ID}`
if (assessment.anonymizedUser) {
url += `?reviewee_id=${assessment.anonymizedUser._id}`