@instructure/quiz-grading
Version:
The Quiz React SDK by Instructure Inc.
537 lines (456 loc) • 17.6 kB
JavaScript
import React from 'react'
import {render, screen} from '../../../../../../tests/utils/rtlRenderOverride'
import {vi, describe, it, expect, beforeEach, afterEach} from 'vitest'
import {fromJS, Map} from 'immutable'
import {
GRADE_BY_QUESTION_NEXT,
GRADE_BY_QUESTION_PREVIOUS,
} from '@instructure/quiz-common/constants'
import runAxeCheck from '@instructure/ui-axe-check'
import userEvent from '@testing-library/user-event'
import {Item as ItemRecord} from '@instructure/quiz-core/common/records/Item'
import {SessionItemResult as SessionItemResultRecord} from '@instructure/quiz-core/common/records/SessionItemResult'
import {SessionItem as SessionItemRecord} from '@instructure/quiz-core/common/records/SessionItem'
import {GradingBar} from '../presenter'
describe('GradingBar', () => {
const sessionItemResultRecord = new SessionItemResultRecord({
id: '1',
score: 1,
pointsPossible: 5,
})
const modifiedSessionItemResultRecord = new SessionItemResultRecord({
id: '1',
score: 3,
pointsPossible: 5,
})
const noScoreSessionItemResultRecord = new SessionItemResultRecord({
id: '1',
score: null,
pointsPossible: 5,
})
const regradeSessionItemResultRecord = new SessionItemResultRecord({
id: '1',
score: null,
pointsPossible: 5,
regradeInfo: fromJS({
original_score: 0,
new_score: 1,
reconciliation_method: 'idkstuff',
regraded: true,
}),
})
const itemRecord = new ItemRecord({id: '3'})
const sessionItemRecord = new SessionItemRecord({id: '1', item: itemRecord})
const correctSessionItemResultRecord = new SessionItemResultRecord({
id: '1',
score: 1,
scoredData: fromJS({gradeStatus: 'correct'}),
})
const ungradedSessionItemResultRecord = new SessionItemResultRecord({
id: '1',
score: 1,
scoredData: fromJS({gradeStatus: 'waiting'}),
})
const startFeedbackEditStub = vi.fn()
const cancelFeedbackEditStub = vi.fn()
const submitFeedbackEditStub = vi.fn()
const updateFeedbackEditStub = vi.fn()
const renderUpdateButtonStub = vi.fn()
const postToParentStub = vi.fn()
const defaultProps = {
handleChangePoints: vi.fn(),
handleChangeGradeStatus: vi.fn(),
sessionItem: sessionItemRecord,
isAutogradeable: true,
isSubjective: false,
postToParent: postToParentStub,
renderUpdateButton: renderUpdateButtonStub,
sessionItemResult: sessionItemResultRecord,
startFeedbackEdit: startFeedbackEditStub,
cancelFeedbackEdit: cancelFeedbackEditStub,
submitFeedbackEdit: submitFeedbackEditStub,
updateFeedbackEdit: updateFeedbackEditStub,
graderFeedback: Map(),
isEditing: false,
activeQuizSessionId: 'activeQuizSessionId',
itemResultsModifications: Map(),
regradeToggleAction: vi.fn(),
}
beforeEach(() => {
vi.spyOn(sessionItemResultRecord, 'getWorkingInstance').mockReturnValue(
modifiedSessionItemResultRecord,
)
vi.useFakeTimers()
})
afterEach(() => {
vi.useRealTimers()
})
it('renders a hideable button for KO navigation', () => {
render(<GradingBar {...defaultProps} />)
expect(screen.getByRole('link', /Jump to Question Navigator/i)).toBeInTheDocument()
})
it('should initialize state with null points if points is null', () => {
const sessionItemResult = new SessionItemResultRecord()
vi.spyOn(sessionItemResult, 'getWorkingInstance').mockReturnValue(sessionItemResult)
render(<GradingBar {...defaultProps} sessionItemResult={sessionItemResult} />)
const input = screen.getByRole('spinbutton', {name: /edit score/i})
expect(input).toHaveValue('')
})
it('should initialize state with points as a number if score exists', () => {
render(<GradingBar {...defaultProps} />)
const input = screen.getByRole('spinbutton', {name: /edit score/i})
expect(input).toHaveValue('3')
})
it('should initialize state with 0 points if points is 0', () => {
const sessionItemResult = new SessionItemResultRecord({score: 0})
vi.spyOn(sessionItemResult, 'getWorkingInstance').mockReturnValue(sessionItemResult)
render(<GradingBar {...defaultProps} sessionItemResult={sessionItemResult} />)
const input = screen.getByRole('spinbutton', {name: /edit score/i})
expect(input).toHaveValue('0')
})
it('renders regrading info when present', () => {
render(
<GradingBar
{...defaultProps}
sessionItem={sessionItemRecord}
isSubjective
sessionItemResult={regradeSessionItemResultRecord}
/>,
)
expect(screen.getByText(/regrade score/i)).toBeInTheDocument()
})
it('does not render regrade if its not present', () => {
render(
<GradingBar
{...defaultProps}
sessionItem={sessionItemRecord}
isSubjective
sessionItemResult={sessionItemResultRecord}
/>,
)
expect(screen.queryByText(/regrade score/i)).not.toBeInTheDocument()
})
it('renders the working score when modifications are made', () => {
render(<GradingBar {...defaultProps} />)
const input = screen.getByRole('spinbutton', {name: /edit score/i})
expect(input).toHaveValue('3')
})
it('renders gradeByQuestion controls when enabled', () => {
render(
<GradingBar
{...defaultProps}
gradeByQuestionEnabled={true}
activeItemId={itemRecord.id}
renderUpdateButton={vi.fn().mockImplementation(() => (
<button>Custom Update Button</button>
))}
/>,
)
expect(screen.getByRole('link', {name: /previous student/i})).toBeInTheDocument()
expect(screen.getByRole('link', {name: /next student/i})).toBeInTheDocument()
expect(screen.getByRole('button', {name: /custom update button/i})).toBeInTheDocument()
})
it('does a post message for prev/next student', () => {
render(<GradingBar {...defaultProps} gradeByQuestionEnabled activeItemId={itemRecord.id} />)
const prevStudentButton = screen.getByRole('link', {name: /previous student/i})
userEvent.click(prevStudentButton)
expect(postToParentStub).toHaveBeenCalledWith({subject: GRADE_BY_QUESTION_PREVIOUS})
const nextStudentButton = screen.getByRole('link', {name: /next student/i})
userEvent.click(nextStudentButton)
expect(postToParentStub).toHaveBeenCalledWith({subject: GRADE_BY_QUESTION_NEXT})
})
it('renders buttons for toggling correct/incorrect if subjective', () => {
render(
<GradingBar
{...defaultProps}
sessionItem={sessionItemRecord}
isSubjective
sessionItemResult={sessionItemResultRecord}
/>,
)
expect(screen.getByRole('button', {name: /toggle correct on/i})).toBeInTheDocument()
expect(screen.getByRole('button', {name: /toggle incorrect on/i})).toBeInTheDocument()
})
it('does not render buttons for toggling correct/incorrect if not subjective', () => {
render(<GradingBar {...defaultProps} />)
expect(screen.queryByRole('button', {name: /toggle correct on/i})).not.toBeInTheDocument()
expect(screen.queryByRole('button', {name: /toggle incorrect on/i})).not.toBeInTheDocument()
})
it('renders points possible', () => {
render(<GradingBar {...defaultProps} />)
expect(screen.getByText('/ 5 points')).toBeInTheDocument()
})
it('changes working gradeStatus when correct button clicked', () => {
const handleChangeGradeStatusStub = vi.fn()
render(
<GradingBar
{...defaultProps}
sessionItem={sessionItemRecord}
sessionItemResult={sessionItemResultRecord}
isSubjective
handleChangeGradeStatus={handleChangeGradeStatusStub}
/>,
)
const correctButton = screen.getByRole('button', {name: /toggle correct on/i})
userEvent.click(correctButton)
expect(handleChangeGradeStatusStub).toHaveBeenCalledWith('correct', false)
})
it('changes working gradeStatus when incorrect button clicked', () => {
const handleChangeGradeStatusStub = vi.fn()
render(
<GradingBar
{...defaultProps}
sessionItem={sessionItemRecord}
sessionItemResult={sessionItemResultRecord}
isSubjective
handleChangeGradeStatus={handleChangeGradeStatusStub}
/>,
)
const incorrectButton = screen.getByRole('button', {name: /toggle incorrect on/i})
userEvent.click(incorrectButton)
expect(handleChangeGradeStatusStub).toHaveBeenCalledWith('incorrect', false)
})
it('sets points to max points possible when marked as correct', () => {
render(
<GradingBar
{...defaultProps}
sessionItem={sessionItemRecord}
sessionItemResult={sessionItemResultRecord}
isSubjective
/>,
)
const correctButton = screen.getByRole('button', {name: /toggle correct on/i})
userEvent.click(correctButton)
const input = screen.getByRole('spinbutton', {name: /edit score/i})
expect(input).toHaveValue('5')
})
it('sets points to zero when marked as incorrect', () => {
render(
<GradingBar
{...defaultProps}
sessionItem={sessionItemRecord}
sessionItemResult={sessionItemResultRecord}
isSubjective
/>,
)
const incorrectButton = screen.getByRole('button', {name: /toggle incorrect on/i})
userEvent.click(incorrectButton)
const input = screen.getByRole('spinbutton', {name: /edit score/i})
expect(input).toHaveValue('0')
})
it('sets points to 0 when focus leaves and field empty', () => {
render(
<GradingBar
{...defaultProps}
sessionItem={sessionItemRecord}
sessionItemResult={noScoreSessionItemResultRecord}
/>,
)
const input = screen.getByRole('spinbutton', {name: /edit score/i})
expect(input).toHaveValue('')
userEvent.click(input)
userEvent.click(document.body)
expect(input).toHaveValue('0')
})
it('does not set points to 0 when focus leaves after clicking correct button', () => {
render(
<GradingBar
{...defaultProps}
isSubjective
sessionItemResult={noScoreSessionItemResultRecord}
/>,
)
const correctButton = screen.getByRole('button', {name: /toggle correct on/i})
userEvent.click(correctButton)
const input = screen.getByRole('spinbutton', {name: /edit score/i})
userEvent.click(input)
userEvent.click(document.body)
expect(input).toHaveValue('5')
})
describe('points field', () => {
it('calls handleChangePoints and handleChangeGradeStatus on points change', () => {
const value = '1'
const handleChangePointsStub = vi.fn()
const handleChangeGradeStatusStub = vi.fn()
render(
<GradingBar
{...defaultProps}
handleChangePoints={handleChangePointsStub}
handleChangeGradeStatus={handleChangeGradeStatusStub}
sessionItem={sessionItemRecord}
isSubjective={false}
sessionItemResult={sessionItemResultRecord}
/>,
)
const input = screen.getByRole('spinbutton', {name: /edit score/i})
userEvent.type(input, value)
vi.advanceTimersByTime(300)
expect(handleChangePointsStub).toHaveBeenCalled()
expect(handleChangeGradeStatusStub).toHaveBeenCalled()
})
it('handleChangePoints debounced and flushed on blur', () => {
const value1 = '1'
const value2 = '2'
const value3 = '3'
const handleChangePointsStub = vi.fn()
render(
<GradingBar
{...defaultProps}
handleChangePoints={handleChangePointsStub}
sessionItem={sessionItemRecord}
isSubjective={false}
sessionItemResult={sessionItemResultRecord}
/>,
)
const input = screen.getByRole('spinbutton', {name: /edit score/i})
userEvent.type(input, value1)
userEvent.type(input, value2)
userEvent.type(input, value3)
userEvent.click(document.body)
expect(handleChangePointsStub).toHaveBeenCalledTimes(2)
})
it('does not normalize on change', () => {
const value = '1.'
const handleChangePointsStub = vi.fn()
const handleChangeGradeStatusStub = vi.fn()
render(
<GradingBar
{...defaultProps}
handleChangePoints={handleChangePointsStub}
handleChangeGradeStatus={handleChangeGradeStatusStub}
sessionItem={sessionItemRecord}
isSubjective={false}
sessionItemResult={sessionItemResultRecord}
/>,
)
const input = screen.getByRole('spinbutton', {name: /edit score/i})
userEvent.clear(input)
userEvent.type(input, value)
vi.advanceTimersByTime(300)
expect(input).toHaveValue('1.')
})
it('normalizes on blur', () => {
const value = '1.'
const handleChangePointsStub = vi.fn()
const handleChangeGradeStatusStub = vi.fn()
render(
<GradingBar
{...defaultProps}
handleChangePoints={handleChangePointsStub}
handleChangeGradeStatus={handleChangeGradeStatusStub}
sessionItem={sessionItemRecord}
isSubjective={false}
sessionItemResult={sessionItemResultRecord}
/>,
)
const input = screen.getByRole('spinbutton', {name: /edit score/i})
userEvent.clear(input)
userEvent.type(input, value)
userEvent.click(document.body)
vi.advanceTimersByTime(300)
expect(input).toHaveValue('1')
})
})
describe('trigger a regrade button', () => {
it('renders the button if `isAutogradeable`', () => {
render(<GradingBar {...defaultProps} regradingFromSpeedGrader />)
expect(screen.getByRole('button', {name: /regrade/i})).toBeInTheDocument()
})
it('does not render unless `isAutogradeable`', () => {
render(<GradingBar {...defaultProps} isAutogradeable={false} regradingFromSpeedGrader />)
expect(screen.queryByRole('button', {name: /regrade/i})).not.toBeInTheDocument()
})
})
describe('external utils', () => {
it('provides them all', () => {
let componentRef
render(
<GradingBar {...defaultProps} ref={ref => (componentRef = ref)} regradingFromSpeedGrader />,
)
expect(componentRef.focusFeedbackButton).toBeTypeOf('function')
expect(componentRef.focusRegradeButton).toBeTypeOf('function')
})
it('provides focusRegradeButton that focuses the Regrade button', () => {
let componentRef
render(
<GradingBar {...defaultProps} ref={ref => (componentRef = ref)} regradingFromSpeedGrader />,
)
componentRef?.focusRegradeButton?.()
expect(screen.getByRole('button', {name: /regrade/i})).toHaveFocus()
})
it('provides focusFeedbackButton that focuses the Feedback button', () => {
let componentRef
render(
<GradingBar {...defaultProps} ref={ref => (componentRef = ref)} regradingFromSpeedGrader />,
)
componentRef?.focusFeedbackButton?.()
expect(screen.getByRole('button', {name: /edit grader level feedback/i})).toHaveFocus()
})
})
it('calls handleChangeGradeStatus with clearCorrect when marked as graded', () => {
const handleChangeGradeStatusStub = vi.fn()
render(
<GradingBar
{...defaultProps}
sessionItem={sessionItemRecord}
sessionItemResult={correctSessionItemResultRecord}
isSubjective
handleChangeGradeStatus={handleChangeGradeStatusStub}
/>,
)
const correctButton = screen.getByRole('button', {name: /toggle correct off/i})
userEvent.click(correctButton)
expect(handleChangeGradeStatusStub).toHaveBeenCalledWith('graded', true)
})
it('updates grade status from waiting to graded on points change', () => {
const value = '1'
const handleChangeGradeStatusStub = vi.fn()
render(
<GradingBar
{...defaultProps}
handleChangeGradeStatus={handleChangeGradeStatusStub}
sessionItem={sessionItemRecord}
isSubjective={false}
sessionItemResult={ungradedSessionItemResultRecord}
/>,
)
const input = screen.getByRole('spinbutton', {name: /edit score/i})
userEvent.type(input, value)
userEvent.click(document.body)
expect(handleChangeGradeStatusStub).toHaveBeenCalledWith('graded')
})
describe('Grader Feedback', () => {
const editFeedbackProps = {
...defaultProps,
isEditing: true,
graderFeedback: Map({content: 'graderFeedback'}),
}
it('switches to editing mode', () => {
render(<GradingBar {...defaultProps} />)
const feedbackButton = screen.getByRole('button', {name: /edit grader level feedback/i})
userEvent.click(feedbackButton)
expect(startFeedbackEditStub).toHaveBeenCalled()
})
it('submits grader feedback in editing mode', () => {
render(<GradingBar {...editFeedbackProps} />)
const editButton = screen.getByRole('button', {name: /done/i})
userEvent.click(editButton)
expect(submitFeedbackEditStub).toHaveBeenCalled()
})
it('cancels grader feedback in editing mode', () => {
render(<GradingBar {...editFeedbackProps} />)
const cancelButton = screen.getByRole('button', {name: /cancel/i})
userEvent.click(cancelButton)
expect(cancelFeedbackEditStub).toHaveBeenCalled()
})
})
describe('a11y tests', () => {
beforeEach(() => {
vi.useRealTimers()
})
it('should meet a11y standards', async () => {
const {container} = render(<GradingBar {...defaultProps} />)
expect(await runAxeCheck(container)).toBe(true)
})
})
})