@instructure/quiz-grading
Version:
The Quiz React SDK by Instructure Inc.
335 lines (296 loc) • 9.27 kB
JavaScript
import React from 'react'
import {QuizEntry} from '@instructure/quiz-core/common/records/QuizEntry'
import {SessionItem} from '@instructure/quiz-core/common/records/SessionItem'
import {describe, it, expect, beforeEach, vi} from 'vitest'
import {render, screen} from '../../../../../../tests/utils/rtlRenderOverride'
import {
OPEN_MODAL,
REGRADE_MODAL,
SHOW_ERROR,
REGRADE_SET_PAYLOAD,
HIDE_ERROR,
} from '@instructure/quiz-common/constants'
import userEvent from '@testing-library/user-event'
import {RegradeOverride} from '../index'
import {fromJS, Iterable} from 'immutable'
describe('Regrade Override', () => {
let dispatchStub = vi.fn()
const quizId = '22'
const quizEntryId = '222'
const regradedQuizEntryId = '223'
const sessionItem = new SessionItem({
id: '333',
quizEntryId,
})
const regradedSessionItem = new SessionItem({
id: '334',
quizEntryId: regradedQuizEntryId,
})
const scoreReconciliation = 'new_score'
const itemId = '100'
const otherItemId = '200'
const invalidItemId = '300'
const scoringAlgorithm = 'Equivalence'
const pointsPossible = 1.0
const regradeAdjustedPointsPossible = 5.0
const regradeAdjustedScoringData = {value: 'regraded'}
const handleValidationErrorsFromApi = Function.prototype
// The props below are to avoid prop type error
const clearValidationErrorsFromApi = Function.prototype
const displayPosition = 1
const parentId = '1'
const clearNextQuizEntry = Function.prototype
const createTempItem = Function.prototype
const guid = '1'
const parentType = 'div'
const createTempStimulus = Function.prototype
const addContentPopoverOpen = false
const onCancel = Function.prototype
const scoringData = {
value: 'true',
}
// Add some modifications
const newScoringData = {value: 'false'}
const newPointsPossible = 14.0
const defaultState = {
regrading: {
quizEntryId,
scoreReconciliation,
regradingPayload: {
quizEntryId: '223',
scoringAlgorithm: 'someAlgorithm',
scoringData,
},
},
items: {
[itemId]: {
id: itemId,
interactionType: '1',
scoringAlgorithm,
scoringData,
},
[otherItemId]: {
id: otherItemId,
interactionType: '1',
scoringAlgorithm,
scoringData,
},
[invalidItemId]: {
id: invalidItemId,
interactionType: '1',
scoringAlgorithm,
scoringData,
errors: {scoringData: {value: ['Answer must be a number']}},
},
},
modifications: {
items: {
[itemId]: {id: itemId, scoringData: newScoringData},
[invalidItemId]: {
id: invalidItemId,
scoringData: newScoringData,
errors: {scoringData: {value: ['Answer must be a number']}},
},
},
quizEntries: {[quizEntryId]: {pointsPossible: newPointsPossible}},
},
quizzes: {
activeQuizId: quizId,
[quizId]: {
id: quizId,
oneAtATimeType: 'question',
},
},
quizEntries: {
[quizEntryId]: {
entry: {
id: itemId,
schema: 'Item',
},
entryType: 'Item',
id: quizEntryId,
pointsPossible,
},
[regradedQuizEntryId]: {
entry: {
id: otherItemId,
schema: 'Item',
},
entryType: 'Item',
id: regradedQuizEntryId,
pointsPossible,
regradeAdjustedPointsPossible,
regradeAdjustedScoringData,
},
},
ui: {
appContainer: '.root',
},
}
const defaultProps = {
addContentPopoverOpen,
clearNextQuizEntry,
createTempItem,
createTempStimulus,
displayPosition,
guid,
itemId,
onCancel,
parentId,
parentType,
quizSessionId: '333',
sessionItem,
handleValidationErrorsFromApi,
clearValidationErrorsFromApi,
regradingPayload: {
quizEntryId: '222',
scoringAlgorithm: 'someAlgorithm',
scoringData,
},
}
let storeOptions
beforeEach(() => {
dispatchStub = vi.fn()
storeOptions = {state: {...defaultState, dispatchStub}}
vi.useFakeTimers()
})
afterEach(() => {
vi.useRealTimers()
})
it('renders interaction in overrideEditableForRegrading mode', () => {
render(<RegradeOverride {...defaultProps} />, {storeOptions})
expect(screen.getByRole('button', {name: /options/i})).toBeInTheDocument()
expect(screen.getByRole('checkbox', {name: /show on-screen calculator/i})).toBeDisabled()
})
it('passes the correct oneQuestionAtATime value to the interaction', () => {
render(<RegradeOverride {...defaultProps} />, {
storeOptions: {
state: {
...storeOptions.state,
items: {
...storeOptions.state.items,
[itemId]: {
...storeOptions.state.items[itemId],
calculatorType: 'basic',
},
},
},
},
})
expect(
screen.getByRole('checkbox', {name: /enable one question at a time/i}),
).toBeInTheDocument()
})
it('renders the alert messaging', () => {
render(<RegradeOverride {...defaultProps} />, {storeOptions})
expect(screen.getByText(/you are regrading this question/i)).toBeInTheDocument()
})
it('renders the action buttons', async () => {
render(<RegradeOverride {...defaultProps} />, {storeOptions})
const cancelButton = screen.getByRole('button', {name: /cancel/i})
const regradeButton = screen.getByRole('button', {name: /regrade/i})
expect(cancelButton).toBeInTheDocument()
expect(regradeButton).toBeInTheDocument()
expect(regradeButton).toHaveTextContent('Regrade question 0')
})
it('cancels modifications when the cancel button is pressed', async () => {
const onCancelStub = vi.fn()
render(<RegradeOverride {...defaultProps} onCancel={onCancelStub} />, {storeOptions})
const cancelButton = await screen.findByRole('button', {name: /cancel/i})
userEvent.click(cancelButton)
expect(onCancelStub).toHaveBeenCalled()
})
it('opens the modal when regrading', async () => {
render(<RegradeOverride {...defaultProps} />, {storeOptions})
const regradeButton = await screen.findByRole('button', {name: /regrade/i})
userEvent.click(regradeButton)
expect(dispatchStub).toHaveBeenCalledWith({
payload: {key: '1'},
type: HIDE_ERROR,
})
expect(dispatchStub).toHaveBeenCalledWith({
payload: {
options: {},
title: REGRADE_MODAL,
},
type: OPEN_MODAL,
})
})
it('does not open the modal with invalid input', async () => {
render(<RegradeOverride {...defaultProps} itemId={invalidItemId} />, {storeOptions})
const regradeButton = await screen.findByRole('button', {name: /regrade/i})
userEvent.click(regradeButton)
expect(dispatchStub).not.toHaveBeenCalledWith({
payload: {
options: {},
title: REGRADE_MODAL,
},
type: OPEN_MODAL,
})
})
it('shows errors with invalid input', async () => {
render(<RegradeOverride {...defaultProps} itemId={invalidItemId} />, {storeOptions})
const regradeButton = screen.getByRole('button', {name: /regrade/i})
userEvent.click(regradeButton)
expect(dispatchStub).toHaveBeenCalledWith({
payload: {key: '1'},
type: SHOW_ERROR,
})
})
// TODO [QUIZ-16313]: Add support for MSW
it.skip('sets the payload when regrading', async () => {
render(<RegradeOverride {...defaultProps} />, {
storeOptions: {
state: {...defaultState, dispatchStub},
reviver: (key, value) => {
if (key === 'regradingPayload') {
return value.toJS()
}
return Iterable.isKeyed(value) ? value.toMap() : value.toList()
},
},
})
const regradeButton = await screen.findByRole('button', {name: /regrade/i})
userEvent.click(regradeButton)
expect(dispatchStub).toHaveBeenCalledWith(
expect.objectContaining({
payload: expect.objectContaining({
itemId,
quizEntryId,
pointsPossible: newPointsPossible,
scoringAlgorithm,
scoringData: fromJS({
value: 'false',
}),
}),
type: REGRADE_SET_PAYLOAD,
}),
)
})
// TODO [QUIZ-16334]: Simulate the regrading workflow including the confirmation modal in a larger test suite
it('renders the correct answer after a regrade', () => {
render(
<RegradeOverride {...defaultProps} itemId={otherItemId} sessionItem={regradedSessionItem} />,
{storeOptions},
)
expect(screen.getByRole('spinbutton', {name: /points/i})).toHaveValue('5')
})
it('renders a spinner when the quizEntry ID is invalid', () => {
const quizEntry = new QuizEntry({id: 0})
const quizEntryId = '0'
const sessionItem = new SessionItem({
id: '333',
quizEntryId,
})
render(
<RegradeOverride
{...defaultProps}
quizEntry={quizEntry}
quizEntryId={quizEntryId}
sessionItem={sessionItem}
/>,
{storeOptions},
)
expect(screen.queryAllByText(/loading\.\.\./i).length).toBeGreaterThan(0)
})
})