@instructure/quiz-interactions
Version:
A React UI component Library for quiz interaction types.
399 lines (337 loc) • 13.5 kB
JavaScript
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')
})
})
})