UNPKG

@instructure/quiz-grading

Version:

The Quiz React SDK by Instructure Inc.

537 lines (456 loc) • 17.6 kB
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) }) }) })