UNPKG

@instructure/quiz-interactions

Version:

A React UI component Library for quiz interaction types.

399 lines (337 loc) • 13.5 kB
import React from 'react' import {expect} from '@instructure/ui-test-utils' import {renderWithDnd} from '../../../../../tests/util/dndTestUtils' import FillBlankTake from '../index' import {screen, waitFor} from '@testing-library/react' import userEvent from '@testing-library/user-event' const firstBlankId = 'firstBlankId' const firstStemItemId = 'firstStemItemId' const secondStemItemId = 'secondStemItemId' const thirdStemItemId = 'thirdStemItemId' const handleResponseUpdateStub = vi.fn() const props = { handleResponseUpdate: handleResponseUpdateStub, userResponse: {value: [{id: firstBlankId, value: 'stem', type: 'Text'}]}, interactionData: { stemItems: [ {id: firstStemItemId, position: 1, type: 'text', value: 'This is the '}, {id: secondStemItemId, position: 2, type: 'blank', blankId: firstBlankId}, {id: thirdStemItemId, position: 3, type: 'text', value: ' value.'}, ], blanks: [{id: firstBlankId, answerType: 'openEntry'}], }, } const newprops = { ...props, interactionData: { ...props.interactionData, blanks: [ { id: firstBlankId, answerType: 'wordbank', choices: [ {id: 'choice_uuid11_brazil', position: 1, itemBody: 'Brazil'}, {id: 'choice_uuid12_austria', position: 2, itemBody: 'Austria'}, {id: 'choice_uuid13_america', position: 3, itemBody: 'America'}, ], }, ], }, userResponse: { value: [ { id: firstBlankId, value: 'choice_uuid12_austria', type: 'Text', }, ], }, } const dropdownBlankId = 'dropdown-blank-1' const dropdownProps = { handleResponseUpdate: handleResponseUpdateStub, userResponse: {value: []}, interactionData: { stemItems: [ {id: 'stem-1', position: 1, type: 'text', value: 'The capital of France is '}, {id: 'stem-2', position: 2, type: 'blank', blankId: dropdownBlankId}, {id: 'stem-3', position: 3, type: 'text', value: '.'}, ], blanks: [ { id: dropdownBlankId, answerType: 'dropdown', choices: [ {id: 'choice-paris', itemBody: 'Paris'}, {id: 'choice-london', itemBody: 'London'}, {id: 'choice-berlin', itemBody: 'Berlin'}, ], }, ], }, } const multipleBlankIds = { dropdown: 'blank-dropdown', openEntry: 'blank-open', wordbank: 'blank-wordbank', } const multipleBlanksProps = { handleResponseUpdate: handleResponseUpdateStub, userResponse: { value: [ {id: multipleBlankIds.dropdown, value: 'choice-red', type: 'Text'}, {id: multipleBlankIds.openEntry, value: 'answer', type: 'Text'}, {id: multipleBlankIds.wordbank, value: 'choice-dog', type: 'Text'}, ], }, interactionData: { stemItems: [ {id: 'stem-1', position: 1, type: 'text', value: 'My favorite color is '}, {id: 'stem-2', position: 2, type: 'blank', blankId: multipleBlankIds.dropdown}, {id: 'stem-3', position: 3, type: 'text', value: ' and I like '}, {id: 'stem-4', position: 4, type: 'blank', blankId: multipleBlankIds.openEntry}, {id: 'stem-5', position: 5, type: 'text', value: ' with my pet '}, {id: 'stem-6', position: 6, type: 'blank', blankId: multipleBlankIds.wordbank}, {id: 'stem-7', position: 7, type: 'text', value: '.'}, ], blanks: [ { id: multipleBlankIds.dropdown, answerType: 'dropdown', choices: [ {id: 'choice-red', itemBody: 'red'}, {id: 'choice-blue', itemBody: 'blue'}, ], }, { id: multipleBlankIds.openEntry, answerType: 'openEntry', }, { id: multipleBlankIds.wordbank, answerType: 'wordbank', choices: [ {id: 'choice-dog', itemBody: 'dog'}, {id: 'choice-cat', itemBody: 'cat'}, ], }, ], }, } describe('Fill in the Blank', () => { afterEach(() => { handleResponseUpdateStub.mockClear() }) describe('Rendering Different Blank Types', () => { it('renders openEntry blank with correct placeholder', () => { renderWithDnd(<FillBlankTake {...props} />) const input = screen.getByRole('textbox', {value: 'type your answer...'}) expect(input).to.exist() }) it('renders dropdown blank with default placeholder and all choices', () => { renderWithDnd(<FillBlankTake {...dropdownProps} />) // Check for the placeholder option const placeholder = screen.getByRole('combobox', {value: 'choose your answer...'}) expect(placeholder).to.exist() // Check that all choices are available const parisOption = screen.getByText('Paris') const londonOption = screen.getByText('London') const berlinOption = screen.getByText('Berlin') expect(parisOption).to.exist() expect(londonOption).to.exist() expect(berlinOption).to.exist() }) it('renders wordbank blank with drag-drop target area', () => { renderWithDnd(<FillBlankTake {...newprops} />) // Wordbank component should be rendered const wordbankText = screen.getByText('Brazil') expect(wordbankText).to.exist() }) }) describe('User Interactions and Response Updates', () => { it('calls handleResponseUpdate with trimmed value when typing in openEntry blank', async () => { renderWithDnd(<FillBlankTake {...props} userResponse={{value: []}} />) const input = screen.getByRole('textbox', {value: 'type your answer...'}) userEvent.clear(input) userEvent.type(input, ' answer ') await waitFor(() => { globalThis.expect(handleResponseUpdateStub).toHaveBeenCalled() }) const lastCall = handleResponseUpdateStub.mock.lastCall[0] expect(lastCall[0]).to.deep.include({ id: firstBlankId, value: 'answer', type: 'Text', }) }) it('calls handleResponseUpdate when selecting dropdown choice', async () => { renderWithDnd(<FillBlankTake {...dropdownProps} />) // Find and click the dropdown const dropdown = screen.getByRole('combobox', {value: 'choose your answer...'}) userEvent.click(dropdown) // Select Paris option const parisOption = screen.getByRole('option', {name: 'Paris'}) userEvent.click(parisOption) await waitFor(() => { globalThis.expect(handleResponseUpdateStub).toHaveBeenCalled() }) const lastCall = handleResponseUpdateStub.mock.lastCall[0] expect(lastCall[0]).to.deep.include({ id: dropdownBlankId, type: 'Text', }) expect(lastCall[0].value).to.equal('choice-paris') }) it('calls handleResponseUpdate with null when returning wordbank choice to wordbank', () => { const wordbankChoiceId = 'choice_uuid12_austria' const wordbankProps = { ...newprops, handleResponseUpdate: handleResponseUpdateStub, } renderWithDnd(<FillBlankTake {...wordbankProps} />) expect(wordbankProps.userResponse.value[0].value).to.equal(wordbankChoiceId) }) }) describe('ReadOnly Mode', () => { it('renders openEntry blank as readonly and displays existing value', () => { renderWithDnd( <FillBlankTake {...props} readOnly={true} userResponse={{value: [{id: firstBlankId, value: 'readonly answer', type: 'Text'}]}} />, ) const input = screen.getByDisplayValue('readonly answer') expect(input).to.exist() expect(input).to.have.attribute('readonly') }) it('renders dropdown blank as readonly with selected choice', () => { renderWithDnd( <FillBlankTake {...dropdownProps} readOnly={true} userResponse={{value: [{id: dropdownBlankId, value: 'choice-paris', type: 'Text'}]}} />, ) const selectedValue = screen.getByText('Paris') expect(selectedValue).to.exist() }) it('does not trigger handleResponseUpdate when interacting with readonly openEntry blank', () => { renderWithDnd( <FillBlankTake {...props} readOnly={true} userResponse={{value: [{id: firstBlankId, value: 'initial', type: 'Text'}]}} />, ) const input = screen.getByDisplayValue('initial') userEvent.type(input, 'new text') // Verify the stub was not called globalThis.expect(handleResponseUpdateStub).not.toHaveBeenCalled() }) }) describe('Multiple Blanks with Mixed Types and Positioning', () => { it('renders multiple blanks of different types in correct order', () => { renderWithDnd(<FillBlankTake {...multipleBlanksProps} />) expect(screen.getByText(/My favorite color is/)).to.exist() expect(screen.getByText(/and I like/)).to.exist() expect(screen.getByText(/with my pet/)).to.exist() // Check that all blank types are rendered expect(screen.getByRole('combobox', {value: 'choose your answer...'})).to.exist() // Dropdown expect(screen.getByRole('textbox', {value: 'type your answer...'})).to.exist() // OpenEntry }) it('displays correct screen reader text for blank positions', () => { renderWithDnd(<FillBlankTake {...multipleBlanksProps} />) // Check for screen reader position announcements expect(screen.getByText('Question Blank 1 of 3')).to.exist() expect(screen.getByText('Question Blank 2 of 3')).to.exist() expect(screen.getByText('Question Blank 3 of 3')).to.exist() }) it('displays saved values for multiple blanks correctly', () => { renderWithDnd(<FillBlankTake {...multipleBlanksProps} />) // OpenEntry should show the saved value const openEntryInput = screen.getByDisplayValue('answer') expect(openEntryInput).to.exist() // Dropdown should have selected value const dropdownValue = screen.getByText('red') expect(dropdownValue).to.exist() }) }) describe('Edge Cases and Error Handling', () => { it('renders blanks in empty/default state when userResponses is empty', () => { renderWithDnd(<FillBlankTake {...dropdownProps} userResponse={{value: []}} />) // Dropdown should show placeholder const placeholder = screen.getByRole('combobox', {value: 'choose your answer...'}) expect(placeholder).to.exist() }) it('shows placeholder when dropdown has no selected value', () => { renderWithDnd( <FillBlankTake {...dropdownProps} userResponse={{value: [{id: dropdownBlankId, value: '', type: 'Text'}]}} />, ) const placeholder = screen.getByRole('combobox', {value: 'choose your answer...'}) expect(placeholder).to.exist() }) it('handles missing blank ID in userResponses gracefully', () => { const propsWithMismatchedResponse = { ...dropdownProps, userResponse: {value: [{id: 'non-existent-blank', value: 'test', type: 'Text'}]}, } expect(() => { renderWithDnd(<FillBlankTake {...propsWithMismatchedResponse} />) }).to.not.throw() // Should render with default state const placeholder = screen.getByRole('combobox', {value: 'choose your answer...'}) expect(placeholder).to.exist() }) it('renders without crashing when blanks array is empty', () => { const emptyBlanksProps = { handleResponseUpdate: handleResponseUpdateStub, userResponse: {value: []}, interactionData: { stemItems: [{id: 'stem-1', position: 1, type: 'text', value: 'No blanks here.'}], blanks: [], }, } expect(() => { renderWithDnd(<FillBlankTake {...emptyBlanksProps} />) }).to.not.throw() expect(screen.getByText('No blanks here.')).to.exist() }) }) describe('Accessibility', () => { it('announces blank position for openEntry blank', () => { renderWithDnd(<FillBlankTake {...props} />) const srText = screen.getByText('Question Blank 1 of 1') expect(srText).to.exist() }) it('announces blank position for dropdown blank', () => { renderWithDnd(<FillBlankTake {...dropdownProps} />) const srText = screen.getByText('Question Blank 1 of 1') expect(srText).to.exist() }) it('has autoComplete="off" for openEntry inputs', () => { renderWithDnd(<FillBlankTake {...props} />) const input = screen.getByRole('textbox', {value: 'type your answer...'}) expect(input).to.have.attribute('autocomplete', 'off') }) it('provides proper labeling for all blank types', () => { renderWithDnd(<FillBlankTake {...multipleBlanksProps} />) // All three blanks should have screen reader labels const labels = screen.getAllByText(/Question Blank \d+ of 3/) expect(labels.length).to.equal(3) }) it('maintains accessibility with multiple blanks of different types', () => { renderWithDnd(<FillBlankTake {...multipleBlanksProps} />) // Each blank should have its position announced expect(screen.getByText('Question Blank 1 of 3')).to.exist() expect(screen.getByText('Question Blank 2 of 3')).to.exist() expect(screen.getByText('Question Blank 3 of 3')).to.exist() // OpenEntry should have autocomplete off const input = screen.getByRole('textbox', {value: 'type your answer...'}) expect(input).to.have.attribute('autocomplete', 'off') }) }) })