@instructure/quiz-grading
Version:
The Quiz React SDK by Instructure Inc.
207 lines (181 loc) • 8.02 kB
JavaScript
import React from 'react'
import {List} from 'immutable'
import {render, screen} from '../../../../../tests/utils/rtlRenderOverride'
import userEvent from '@testing-library/user-event'
import {describe, expect, it, vi, beforeEach} from 'vitest'
import InteractionType from '@instructure/quiz-interactions/records/InteractionType'
import {Item} from '@instructure/quiz-core/common/records/Item'
import {SessionItem} from '@instructure/quiz-core/common/records/SessionItem'
import {GradingSidebar as Sidebar} from '../presenter'
import {FORMULA_SLUG, RICH_FILL_BLANK_SLUG} from '@instructure/quiz-common/constants'
import runAxeCheck from '@instructure/ui-axe-check'
describe('Grading Sidebar Presenter', () => {
const intTypeA = new InteractionType({name: 'Fill in the Blank', slug: RICH_FILL_BLANK_SLUG})
const intTypeB = new InteractionType({name: 'Multiple Choice'})
const itemA = new Item({
id: '5',
position: 4,
itemBody: '<div>first</div>&<div>second <span id="blank_abcd-1234"></span></div>',
})
const itemB = new Item({id: '6', position: 8})
const itemC = new Item({
id: '7',
position: 5,
itemBody: 'what is 5 + `x`?',
interactionData: {variables: [{name: 'x', value: 7}]},
})
const sessionItemA = new SessionItem({id: '5', position: 1, questionNumber: 1, pointsPossible: 5})
const sessionItemB = new SessionItem({id: '6', position: 2, questionNumber: 2, pointsPossible: 5})
const sessionItemC = new SessionItem({id: '7', position: 4, questionNumber: 3, pointsPossible: 5})
beforeEach(() => {
vi.spyOn(itemA, 'getInteractionType').mockReturnValue(intTypeA)
vi.spyOn(itemB, 'getInteractionType').mockReturnValue(intTypeB)
vi.spyOn(itemC, 'getInteractionType').mockReturnValue(
new InteractionType({name: 'Formula', slug: FORMULA_SLUG}),
)
vi.spyOn(sessionItemA, 'getItem').mockReturnValue(itemA)
vi.spyOn(sessionItemB, 'getItem').mockReturnValue(itemB)
vi.spyOn(sessionItemC, 'getItem').mockReturnValue(itemC)
})
const sessionItems = List([sessionItemA, sessionItemB, sessionItemC])
const noop = () => {}
const defaultProps = {
quizTitle: 'Super fun quiz',
scrollToItem: noop,
sidebarOpen: true,
screenreaderNotification: noop,
sessionItems,
sessionNotLoaded: false,
toggleSidebar: () => {},
}
describe('#renderSidebarItems', () => {
it('renders the correct number', () => {
render(<Sidebar {...defaultProps} />)
const sidebarItems = screen.getAllByRole('link', {
name: /Navigate to question at position/i,
})
expect(sidebarItems).toHaveLength(3)
})
it('should extract text content of item body HTML', () => {
render(<Sidebar {...defaultProps} />)
expect(screen.getByText(/first&second _____/i)).toBeInTheDocument()
})
it('should replace the variables in formula questions', () => {
render(<Sidebar {...defaultProps} />)
expect(screen.getByText(/what is 5 \+ 7\?/i)).toBeInTheDocument()
})
describe('textblock handling', () => {
it('should display textblocks but not count them in question numbering', () => {
// Create textblock items (questionNumber: 0)
const textblockIntType = new InteractionType({
name: 'No Interaction',
slug: 'no-interaction',
})
const textblockItem1 = new Item({id: '100', position: 1})
const textblockItem2 = new Item({id: '101', position: 6})
const questionIntType = new InteractionType({name: 'Multiple Choice', slug: 'choice'})
const questionItem1 = new Item({id: '102', position: 2})
const questionItem2 = new Item({id: '103', position: 3})
const questionItem3 = new Item({id: '104', position: 7})
// Mock interaction types
vi.spyOn(textblockItem1, 'getInteractionType').mockReturnValue(textblockIntType)
vi.spyOn(textblockItem2, 'getInteractionType').mockReturnValue(textblockIntType)
vi.spyOn(questionItem1, 'getInteractionType').mockReturnValue(questionIntType)
vi.spyOn(questionItem2, 'getInteractionType').mockReturnValue(questionIntType)
vi.spyOn(questionItem3, 'getInteractionType').mockReturnValue(questionIntType)
// Create session items with textblocks at positions 1 and 6
const textblockSession1 = new SessionItem({
id: '100',
position: 1,
questionNumber: 0,
pointsPossible: 0,
})
const questionSession1 = new SessionItem({
id: '102',
position: 2,
questionNumber: 1,
pointsPossible: 1,
})
const questionSession2 = new SessionItem({
id: '103',
position: 3,
questionNumber: 2,
pointsPossible: 1,
})
const textblockSession2 = new SessionItem({
id: '101',
position: 6,
questionNumber: 0,
pointsPossible: 0,
})
const questionSession3 = new SessionItem({
id: '104',
position: 7,
questionNumber: 3,
pointsPossible: 1,
})
// Mock getItem for all session items
vi.spyOn(textblockSession1, 'getItem').mockReturnValue(textblockItem1)
vi.spyOn(textblockSession2, 'getItem').mockReturnValue(textblockItem2)
vi.spyOn(questionSession1, 'getItem').mockReturnValue(questionItem1)
vi.spyOn(questionSession2, 'getItem').mockReturnValue(questionItem2)
vi.spyOn(questionSession3, 'getItem').mockReturnValue(questionItem3)
const sessionItemsWithTextblocks = List([
textblockSession1,
questionSession1,
questionSession2,
textblockSession2,
questionSession3,
])
render(<Sidebar {...defaultProps} sessionItems={sessionItemsWithTextblocks} />)
// All items (including textblocks) should be rendered
const allItems = screen.getAllByRole('link', {
name: /Navigate to question at position/i,
})
expect(allItems).toHaveLength(5) // 2 textblocks + 3 questions
// Questions should be numbered 1, 2, 3 (not 1, 2, 4 or 2, 3, 4)
expect(screen.getByRole('link', {name: /position 1/i})).toBeInTheDocument()
expect(screen.getByRole('link', {name: /position 2/i})).toBeInTheDocument()
expect(screen.getByRole('link', {name: /position 3/i})).toBeInTheDocument()
// Textblocks should have position 0 (not numbered)
expect(screen.getAllByRole('link', {name: /position 0/i})).toHaveLength(2)
})
})
})
describe('scrolling and changing items', () => {
const scrollToItemMock = vi.fn()
const screenreaderNotificationMock = vi.fn()
it('binds scrollToItem to the itemId', () => {
render(
<Sidebar
{...defaultProps}
scrollToItem={scrollToItemMock}
screenreaderNotification={screenreaderNotificationMock}
/>,
)
const firstSidebarItem = screen.getAllByRole('link', {
name: /Navigate to question at position/i,
})[0]
userEvent.click(firstSidebarItem)
expect(scrollToItemMock).toHaveBeenCalledWith('5')
})
})
describe('#render', () => {
it('renders the sidebar navigation', () => {
render(<Sidebar {...defaultProps} />)
expect(screen.queryByRole('navigation')).toBeInTheDocument()
})
it('renders nothing if sessionNotLoaded is true', () => {
render(<Sidebar {...defaultProps} sessionNotLoaded={true} />)
expect(screen.queryByRole('navigation')).not.toBeInTheDocument()
})
it('passes the role prop to the sidebar component', () => {
render(<Sidebar {...defaultProps} role={'complementary'} />)
expect(screen.queryByRole('complementary')).toBeInTheDocument()
})
})
it('should meet a11y standards', async () => {
render(<Sidebar {...defaultProps} />)
expect(await runAxeCheck(document.body, {ignores: ['aria-tooltip-name']})).toBe(true)
})
})