@instructure/quiz-grading
Version:
The Quiz React SDK by Instructure Inc.
266 lines (228 loc) • 9.04 kB
JavaScript
import React from 'react'
import {Map} from 'immutable'
import {beforeEach, describe, expect, it, vi} from 'vitest'
import {render, screen, waitFor} from '../../../../../../tests/utils/rtlRenderOverride'
import userEvent from '@testing-library/user-event'
import {Item as ItemRecord} from '@instructure/quiz-core/common/records/Item'
import {SessionItem as SessionItemRecord} from '@instructure/quiz-core/common/records/SessionItem'
import {SessionItemResult as SessionItemResultRecord} from '@instructure/quiz-core/common/records/SessionItemResult'
import {SessionItemResultWrapper} from '../presenter'
describe('SessionItemResultWrapper Presenter', () => {
const id = '1'
const item = new ItemRecord({id})
const sessionItem = new SessionItemRecord({id})
const sessionItemResult = new SessionItemResultRecord({id, score: 0})
let setActiveItemStub = vi.fn()
let switchOnEditingStub = vi.fn()
let switchOffEditingStub = vi.fn()
let handleChangeFeedbackStub = vi.fn()
const defaultProps = {
activeQuizSessionId: '1',
appContainer: '.root',
graderFeedback: Map(),
handleChangeFeedback: handleChangeFeedbackStub,
handleChangeGradeStatus: Function.prototype,
handleChangePoints: Function.prototype,
isEditing: false,
isRegrading: false,
isSurvey: false,
item,
itemResultsModifications: Map([['scoredData', ['gradeStatus', 'graded']]]),
regradingKey: 'someKey',
renderUpdateButton: Function.prototype,
sessionItem,
sessionItemResult,
setActiveItem: setActiveItemStub,
shouldRenderTopBorder: false,
switchOnEditing: switchOnEditingStub,
switchOffEditing: switchOffEditingStub,
toggleRegradingEditMode: Function.prototype,
quizSessionId: '1',
}
beforeEach(() => {
setActiveItemStub = vi.fn()
switchOnEditingStub = vi.fn()
switchOffEditingStub = vi.fn()
handleChangeFeedbackStub = vi.fn()
})
describe('focus tests', () => {
it('focuses on the feedback button on closeFeedbackEdit', () => {
render(<SessionItemResultWrapper {...defaultProps} isEditing />)
const feedbackButton = screen.getByRole('button', {name: /edit grader level feedback/i})
const cancelButton = screen.getByRole('button', {name: /cancel/i})
userEvent.click(cancelButton)
expect(feedbackButton).toHaveFocus()
})
describe('gradeByQuestion enabled', () => {
it('sets activeItem when focused', () => {
const setActiveItemStub = vi.fn()
const {container} = render(
<SessionItemResultWrapper
{...defaultProps}
gradeByQuestionEnabled={true}
setActiveItem={setActiveItemStub}
/>,
)
const wrapperElement = container.querySelector('[data-automation="sdk-position-box-text"]')
userEvent.click(wrapperElement)
expect(setActiveItemStub).toHaveBeenCalled()
})
})
describe('gradeByQuestion disabled', () => {
it('does not set activeItem when focused', () => {
const setActiveItemStub = vi.fn()
const {container} = render(
<SessionItemResultWrapper {...defaultProps} setActiveItem={setActiveItemStub} />,
)
const wrapperElement = container.querySelector(
'[data-automation="sdk-grading-result-wrapper"]',
)
userEvent.click(wrapperElement)
expect(setActiveItemStub).not.toHaveBeenCalled()
})
})
})
describe('feedback tests', () => {
it('calls startFeedbackEdit correctly', () => {
const switchOnEditingStub = vi.fn()
render(<SessionItemResultWrapper {...defaultProps} switchOnEditing={switchOnEditingStub} />)
const editButton = screen.getByRole('button', {
name: /edit grader level feedback/i,
})
userEvent.click(editButton)
expect(switchOnEditingStub).toHaveBeenCalled()
})
it('returns startFeedbackEdit early if already editing', () => {
const switchOnEditingStub = vi.fn()
render(
<SessionItemResultWrapper
{...defaultProps}
isEditing
switchOnEditing={switchOnEditingStub}
/>,
)
const editButton = screen.getByRole('button', {
name: /edit grader level feedback/i,
})
userEvent.click(editButton)
expect(switchOnEditingStub).not.toHaveBeenCalled()
})
it('calls closeFeedbackEdit correctly', () => {
const switchOffEditingStub = vi.fn()
render(
<SessionItemResultWrapper
{...defaultProps}
isEditing
switchOffEditing={switchOffEditingStub}
/>,
)
const cancelButton = screen.getByRole('button', {
name: /Cancel/i,
})
userEvent.click(cancelButton)
expect(switchOffEditingStub).toHaveBeenCalled()
})
it('calls submitFeedbackEdit correctly', () => {
const switchOffEditingStub = vi.fn()
const handleChangeFeedbackStub = vi.fn()
render(
<SessionItemResultWrapper
{...defaultProps}
isEditing
handleChangeFeedback={handleChangeFeedbackStub}
switchOffEditing={switchOffEditingStub}
/>,
)
const doneButton = screen.getByRole('button', {
name: /Done/i,
})
userEvent.click(doneButton)
expect(handleChangeFeedbackStub).toHaveBeenCalled()
expect(switchOffEditingStub).toHaveBeenCalled()
})
})
describe('handler tests', () => {
it('calls toggleRegradingEditMode with regradingKey on handleRegradingEditModeToggle', () => {
const toggleRegradingEditMode = vi.fn()
render(
<SessionItemResultWrapper
{...defaultProps}
toggleRegradingEditMode={toggleRegradingEditMode}
regradingKey="myKey"
/>,
{storeOptions: {state: {features: ['regrading_from_speedgrader']}}},
)
const regradeButton = document.querySelector('[data-automation="sdk-regrade-request-button"]')
userEvent.click(regradeButton)
expect(toggleRegradingEditMode).toHaveBeenCalledWith('myKey')
})
it('focuses the regrade button after canceling regrading', async () => {
// Render with isRegrading=false so GradingBar mounts and gradingBar ref is set.
// Then call handleRegradingOnCancel directly via class ref — no need to render
// RegradeOverride or its complex store dependencies.
const ref = React.createRef()
render(
<SessionItemResultWrapper
ref={ref}
{...defaultProps}
isRegrading={false}
regradingFromSpeedGrader={true}
/>,
{storeOptions: {state: {features: ['regrading_from_speedgrader']}}},
)
ref.current.handleRegradingOnCancel(id)
await waitFor(() => {
expect(
document.querySelector('[data-automation="sdk-regrade-request-button"]'),
).toHaveFocus()
})
})
})
describe('regrading_from_speedgrader tests', () => {
it('renders RegradeOverride (not the grading result wrapper) when isRegrading and regradingFromSpeedGrader are true', () => {
// When isRegrading && regradingFromSpeedGrader, SessionItemResultWrapper.render() returns
// renderRegradeOverride() instead of renderResult(). With a minimal store the connected
// RegradeOverride renders a Spinner (quizEntry.id === '0'). Assert the grading result
// wrapper is absent to confirm the regrading branch was taken.
//
// Store must include regrading.scoreReconciliation because RegradeOverride renders a
// connected RegradingModal which accesses that value unconditionally in renderModalBody().
const {container} = render(
<SessionItemResultWrapper
{...defaultProps}
isRegrading={true}
regradingFromSpeedGrader={true}
/>,
{
storeOptions: {
state: {
features: ['regrading_from_speedgrader'],
regrading: {scoreReconciliation: 'new_score'},
},
},
},
)
expect(container.querySelector('[data-automation="sdk-grading-result-wrapper"]')).toBeNull()
})
it('updateFeedbackEdit updates the grader feedback content in state', () => {
const handleChangeFeedback = vi.fn()
render(
<SessionItemResultWrapper
{...defaultProps}
isEditing={true}
handleChangeFeedback={handleChangeFeedback}
/>,
)
// The RichContentInput onChange triggers updateFeedbackEdit which updates graderFeedback state.
// Submitting the feedback form calls handleChangeFeedback with the updated state.
const doneButton = screen.getByRole('button', {name: /Done/i})
userEvent.click(doneButton)
// handleChangeFeedback is called with the current graderFeedback state
expect(handleChangeFeedback).toHaveBeenCalledWith(
sessionItem.id,
item.id,
expect.objectContaining({graderFeedback: expect.any(Object)}),
)
})
})
})