UNPKG

@instructure/quiz-taking

Version:
223 lines (194 loc) • 8.54 kB
import React from 'react' import {List} from 'immutable' 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 {TakingSidebar as Sidebar} from '../presenter' import {FORMULA_SLUG, RICH_FILL_BLANK_SLUG} from '@instructure/quiz-common/constants' import {beforeEach, describe, expect, it, vi} from 'vitest' import {render, screen, waitFor} from '../../../../../tests/utils/rtlRenderOverride' import userEvent from '@testing-library/user-event' import runAxeCheck from '@instructure/ui-axe-check' describe('Taking 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>&amp;<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: '4', 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}) const sessionItems = List([sessionItemA, sessionItemB, sessionItemC]) const noop = () => {} const defaultProps = { allowBacktracking: true, isOneQuestionAtATime: false, quizTitle: 'Super fun quiz', scrollToItem: noop, sidebarOpen: true, goToItem: noop, screenreaderNotification: noop, sessionItems, sessionNotLoaded: false, pinnedItems: List(), setFocusTitleAndInstructions: () => {}, toggleSidebar: () => {}, } 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) }) describe('#renderSidebarItems', () => { it('renders the correct number', () => { render(<Sidebar {...defaultProps} />) expect( screen.getAllByRole('link', { name: /navigate to question at position/i, }), ).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('scrolling and changing items', () => { const goToItemStub = vi.fn() const scrollToItemStub = vi.fn() const screenreaderNotificationStub = vi.fn() const setFocusTitleAndInstructionsStub = vi.fn() const overriddenProps = { goToItem: goToItemStub, scrollToItem: scrollToItemStub, screenreaderNotification: screenreaderNotificationStub, setFocusTitleAndInstructions: setFocusTitleAndInstructionsStub, } describe('oqaat on', () => { it('alerts the screenreader when scrollToItem is called', async () => { render(<Sidebar {...defaultProps} {...overriddenProps} isOneQuestionAtATime={true} />) userEvent.click( screen.getByRole('link', {name: /Navigate to question at position 1, unanswered/i}), ) await waitFor(() => expect(screenreaderNotificationStub).toHaveBeenCalledWith( 'Current question changed to question 1', ), ) }) it('gives a relevant SR message when clicking title/instructions', async () => { render(<Sidebar {...defaultProps} {...overriddenProps} isOneQuestionAtATime={true} />) userEvent.click(screen.getByRole('link', {name: /Navigate to title/i})) await waitFor(() => { expect(screenreaderNotificationStub).toHaveBeenCalledWith( 'Navigated to quiz title and instructions. Current question changed to question 1', ) }) }) it('sets focusTitleAndInstructions as true when clicking title/instructions', async () => { render(<Sidebar {...defaultProps} {...overriddenProps} isOneQuestionAtATime={true} />) userEvent.click(screen.getByRole('link', {name: /Navigate to title/i})) await waitFor(() => expect(setFocusTitleAndInstructionsStub).toHaveBeenCalledWith(true)) }) it('calls goToItem when scrollToItem is called', async () => { render(<Sidebar {...defaultProps} {...overriddenProps} isOneQuestionAtATime={true} />) userEvent.click( screen.getByRole('link', {name: /Navigate to question at position 1, unanswered/i}), ) await waitFor(() => expect(goToItemStub).toHaveBeenCalled()) }) }) describe('oqaat off', () => { it('binds scrollToItem to the itemId', async () => { render(<Sidebar {...defaultProps} {...overriddenProps} />) userEvent.click( screen.getByRole('link', {name: /Navigate to question at position 1, unanswered/i}), ) await waitFor(() => expect(scrollToItemStub).toHaveBeenCalledWith('4')) }) }) }) describe('#render', () => { it('renders the common sidebar', () => { render(<Sidebar {...defaultProps} />) expect(screen.getByRole('heading', {name: /question navigator/i})).toBeInTheDocument() }) it('renders nothing if OQAAT non-backtracking is on', () => { render(<Sidebar {...defaultProps} allowBacktracking={false} isOneQuestionAtATime={true} />) expect(screen.queryByRole('heading', {name: /question navigator/i})).not.toBeInTheDocument() }) it('renders nothing if sessionNotLoaded is true', () => { render(<Sidebar {...defaultProps} sessionNotLoaded />) expect(screen.queryByRole('heading', {name: /question navigator/i})).not.toBeInTheDocument() }) it('passes the role prop to the CommonSidebar component', () => { render(<Sidebar {...defaultProps} role="complementary" />) expect(screen.getByRole('complementary')).toBeInTheDocument() }) }) describe('a11y tests', () => { it('should meet a11y standards', async () => { render(<Sidebar {...defaultProps} />) expect(await runAxeCheck(document.body, {ignores: ['aria-tooltip-name']})).toBe(true) }) }) describe('QUIZ-17004: position prop for answered status', () => { it('should correctly identify answered status when position differs from questionNumber', () => { // Test case: SessionItem with position=4 and questionNumber=3 (happens with text blocks) // Response is stored at position 4 const stateWithResponse = { quizSessions: { activeQuizSessionId: '1', 1: {id: '1'}, }, taking: { responses: { 4: { position: 4, itemId: '7', userResponse: {value: 'test answer'}, }, }, }, } // Note: sessionItemC has position=4 but questionNumber=3 const propsWithState = { ...defaultProps, sessionItems: List([sessionItemA, sessionItemB, sessionItemC]), } render(<Sidebar {...propsWithState} />, {storeOptions: {state: stateWithResponse}}) // Find all sidebar items const sidebarItems = screen.getAllByRole('link', { name: /navigate to question at position/i, }) // Verify 3 items rendered expect(sidebarItems).toHaveLength(3) // The third item (position 4, questionNumber 3) should NOT show as unanswered // because we have a response at position 4 // If we incorrectly passed questionNumber (3) instead of position (4), // it would show as unanswered since there's no response at position 3 expect(screen.queryByText(/navigate to question at position 3, unanswered/i)).toBeNull() }) }) })