@instructure/quiz-interactions
Version:
A React UI component Library for quiz interaction types.
1,285 lines (1,106 loc) • 37.5 kB
JSX
import {vi} from 'vitest'
import React from 'react'
import {
verifyNoRichContentEditorsExist,
verifyAtLeastOneRichContentEditorExists,
} from '../../../../../tests/util/enableRichContentEditorChecks'
import {verifyItemChangesAreValidated} from '../../../../../tests/util/shouldValidateItemChanges'
import NumericEdit, {getNewValue} from '../index'
import {render, screen, fireEvent} from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import runAxeCheck from '@instructure/ui-axe-check'
function createProps(changeItemStateStub, setOneQuestionAtATimeStub) {
return {
calculatorType: 'basic',
itemId: '1',
itemBody: 'Give an example of a properly random number',
openImportModal: Function.prototype,
scoringData: {
value: [
{
type: 'exactResponse',
id: '1',
value: '3.1415926539',
},
{
type: 'withinARange',
id: '2',
start: '0.6931471805599453',
end: '1.4142135623730951',
},
{
type: 'exactResponse',
id: '3',
value: '1',
},
],
},
changeItemState: changeItemStateStub,
setOneQuestionAtATime: setOneQuestionAtATimeStub,
}
}
describe('Numeric Edit', () => {
let changeItemStateStub, setOneQuestionAtATimeStub, props
beforeEach(() => {
changeItemStateStub = vi.fn()
setOneQuestionAtATimeStub = vi.fn()
props = createProps(changeItemStateStub, setOneQuestionAtATimeStub)
})
it('renders a rich content editor by default', async () => {
await verifyAtLeastOneRichContentEditorExists(NumericEdit, props)
})
it('does not render a rich content editor when it is turned off', async () => {
await verifyNoRichContentEditorsExist(NumericEdit, {...props, enableRichContentEditor: false})
})
it('includes validation errors in changeItemState', async () => {
await verifyItemChangesAreValidated(NumericEdit, {...props, itemBody: ''})
})
// TODO: Stop testing implementation details...
it('NumericEdit.getNewValue', () => {
const exactAnswer = {
id: '1',
type: 'exactResponse',
value: '2',
}
const marginAnswer = {
id: '1',
type: 'marginOfError',
value: '2',
margin: '3',
marginType: '4',
}
const rangeAnswer = {
id: '1',
type: 'withinARange',
start: '2',
end: '3',
}
const preciseAnswer = {
id: '1',
type: 'preciseResponse',
value: '2',
precision: '3',
precisionType: '4',
}
expect(getNewValue({}, exactAnswer.type)).toEqual({
id: '',
type: 'exactResponse',
value: '',
})
expect(getNewValue({}, marginAnswer.type)).toEqual({
id: '',
type: 'marginOfError',
value: '',
margin: '',
marginType: 'percent',
})
expect(getNewValue({}, rangeAnswer.type)).toEqual({
id: '',
type: 'withinARange',
start: '',
end: '',
})
expect(getNewValue({}, preciseAnswer.type)).toEqual({
id: '',
type: 'preciseResponse',
value: '',
precision: '',
precisionType: 'significantDigits',
})
})
describe('render', () => {
it('renders correct fields for exactResponse', () => {
const testProps = {
...props,
scoringData: {
value: [
{
type: 'exactResponse',
id: '1',
value: '3.1415926539',
},
],
},
}
const {container} = render(<NumericEdit {...testProps} />)
const requirementSelect = screen.getByRole('combobox', {name: /Requirement/i})
const answerInput = container.querySelector(
'[data-automation="sdk-numeric-exact-response-number-input"]',
)
expect(requirementSelect).toBeInTheDocument()
expect(answerInput).toBeInTheDocument()
})
it('renders an answer of zero', () => {
const testProps = {
...props,
scoringData: {
value: [
{
type: 'exactResponse',
id: '1',
value: '0',
},
],
},
}
const {container} = render(<NumericEdit {...testProps} />)
const answerInput = container.querySelector(
'[data-automation="sdk-numeric-exact-response-number-input"]',
)
expect(answerInput.value).toBe('0')
})
it('localizes numbers', () => {
const testProps = {
...props,
locale: 'fr',
scoringData: {
value: [
{
type: 'withinARange',
id: '1',
start: '0.5',
end: '1.5',
},
],
},
}
render(<NumericEdit {...testProps} />)
const rangeStartInput = screen.getByLabelText(/Range start/i)
const rangeEndInput = screen.getByLabelText(/Range end/i)
expect(rangeStartInput.value).toBe('0,5')
expect(rangeEndInput.value).toBe('1,5')
})
it("doesn't strip trailing zeros", () => {
const testProps = {
...props,
locale: 'fr',
scoringData: {
value: [
{
type: 'exactResponse',
id: '1',
value: '12.20',
},
],
},
}
const {container} = render(<NumericEdit {...testProps} />)
const answerInput = container.querySelector(
'[data-automation="sdk-numeric-exact-response-number-input"]',
)
expect(answerInput.value).toBe('12,20')
})
describe('overrideEditableForRegrading', () => {
it('disables question stem input', () => {
const {container} = render(
<NumericEdit {...props} overrideEditableForRegrading enableRichContentEditor={false} />,
)
const textarea = container.querySelector('textarea')
expect(textarea).toBeDisabled()
})
it('disables the calculator option', () => {
render(<NumericEdit {...props} overrideEditableForRegrading />)
const calculatorCheckbox = screen.getByRole('checkbox', {
name: /show on-screen calculator/i,
})
const calculatorBasicRadioInput = screen.getByRole('radio', {name: /basic calculator/i})
const calculatorScientificRadioInput = screen.getByRole('radio', {
name: /scientific calculator/i,
})
expect(calculatorCheckbox).toBeDisabled()
expect(calculatorBasicRadioInput).toBeDisabled()
expect(calculatorScientificRadioInput).toBeDisabled()
})
})
it('renders correct fields for marginOfError', () => {
const testProps = {
...props,
scoringData: {
value: [
{
type: 'marginOfError',
id: '20',
value: '42',
margin: '1',
marginType: 'percent',
},
],
},
}
const {container} = render(<NumericEdit {...testProps} />)
const requirementSelect = screen.getByRole('combobox', {name: /Requirement/i})
const answerInput = container.querySelector(
'[data-automation="sdk-numeric-marginoferror-response-number-input"]',
)
const marginInput = screen.getByLabelText(/\+\/- margin/i)
expect(requirementSelect).toBeInTheDocument()
expect(answerInput).toBeInTheDocument()
expect(marginInput).toBeInTheDocument()
expect(container.textContent).toContain('Type')
})
it('renders correct fields for withinARange', () => {
const testProps = {
...props,
scoringData: {
value: [
{
type: 'withinARange',
id: '2',
start: '0.6931471805599453',
end: '1.4142135623730951',
},
],
},
}
render(<NumericEdit {...testProps} />)
const requirementSelect = screen.getByRole('combobox', {name: /Requirement/i})
const rangeStartInput = screen.getByLabelText(/Range start/i)
const rangeEndInput = screen.getByLabelText(/Range end/i)
expect(requirementSelect).toBeInTheDocument()
expect(rangeStartInput).toBeInTheDocument()
expect(rangeEndInput).toBeInTheDocument()
})
it('renders correct fields for preciseResponse', () => {
const testProps = {
...props,
scoringData: {
value: [
{
type: 'preciseResponse',
id: '20',
value: '4294967296',
precision: '8',
precisionType: 'significantDigits',
},
],
},
}
const {container} = render(<NumericEdit {...testProps} />)
const requirementSelect = screen.getByRole('combobox', {name: /Requirement/i})
const answerInput = container.querySelector(
'[data-automation="sdk-numeric-precise-answer-number-input"]',
)
const precisionInput = container.querySelector(
'[data-automation="sdk-numeric-precise-precision-number-input"]',
)
expect(requirementSelect).toBeInTheDocument()
expect(answerInput).toBeInTheDocument()
expect(precisionInput).toBeInTheDocument()
expect(container.textContent).toContain('Type')
})
})
it('handles removing second answer with three answers', () => {
render(<NumericEdit {...props} />)
const secondAnswerDeleteButton = screen.getByRole('button', {
name: /Remove Answer: range from 0.6931471805599453 to 1.4142135623730951/i,
})
fireEvent.click(secondAnswerDeleteButton)
expect(changeItemStateStub).toHaveBeenCalledTimes(1)
expect(changeItemStateStub).toHaveBeenCalledWith(
expect.objectContaining({
scoringData: {
value: [
{
type: 'exactResponse',
id: '1',
value: '3.1415926539',
},
{
type: 'exactResponse',
id: '3',
value: '1',
},
],
},
}),
)
})
it('handles removing second answer with two answers', () => {
const testProps = {
...props,
scoringData: {
value: [
{
type: 'exactResponse',
id: '1',
value: '3.1415926539',
},
{
type: 'withinARange',
id: '2',
start: '0.6931471805599453',
end: '1.4142135623730951',
},
],
},
}
render(<NumericEdit {...testProps} />)
const secondAnswerDeleteButton = screen.getByRole('button', {
name: /Remove Answer: range from 0.6931471805599453 to 1.4142135623730951/i,
})
fireEvent.click(secondAnswerDeleteButton)
expect(changeItemStateStub).toHaveBeenCalledTimes(1)
expect(changeItemStateStub).toHaveBeenCalledWith(
expect.objectContaining({
scoringData: {
value: [
{
type: 'exactResponse',
id: '1',
value: '3.1415926539',
},
],
},
}),
)
})
it('handles creating new answers', () => {
render(<NumericEdit {...props} />)
const answerLength = props.scoringData.value.length
const addPossibleAnswerButton = screen.getByRole('button', {name: /Add Possible Answer/i})
fireEvent.click(addPossibleAnswerButton)
expect(changeItemStateStub).toHaveBeenCalledTimes(1)
expect(changeItemStateStub.mock.calls[0][0].scoringData.value.length).toBe(answerLength + 1)
})
it('handles changing answer values', () => {
const {container} = render(<NumericEdit {...props} />)
const answerInput = container.querySelector(
'[data-automation="sdk-numeric-exact-response-number-input"]',
)
fireEvent.change(answerInput, {target: {value: '4'}})
expect(changeItemStateStub).toHaveBeenCalledTimes(1)
expect(changeItemStateStub).toHaveBeenCalledWith(
expect.objectContaining({
scoringData: {
value: [
{
type: 'exactResponse',
id: '1',
value: '4',
},
...props.scoringData.value.slice(1),
],
},
}),
)
})
it('handles changing answer types while maintaining focus', () => {
// Use fake timers to prevent the component's setTimeout focus call from
// firing after the component is unmounted (which would cause a null ref error)
vi.useFakeTimers()
render(<NumericEdit {...props} />)
// The editor contains multiple select inputs, so we pick the first one
const [firstRequirementSelect] = screen.queryAllByRole('combobox', {name: /requirement/i})
userEvent.click(firstRequirementSelect)
userEvent.click(screen.getByRole('option', {name: /within a range/i}))
expect(changeItemStateStub).toHaveBeenCalled()
expect(changeItemStateStub.mock.calls[0][0].scoringData.value[0]).toEqual({
type: 'withinARange',
id: '1',
start: '',
end: '',
})
expect(document.activeElement.id).toBe(firstRequirementSelect.id)
vi.useRealTimers()
})
it('changes Precise Response value to a string', () => {
const {container} = render(<NumericEdit {...props} />)
const answerInput = container.querySelector(
'[data-automation="sdk-numeric-exact-response-number-input"]',
)
fireEvent.change(answerInput, {target: {value: '4.123'}})
expect(changeItemStateStub).toHaveBeenCalled()
expect(typeof changeItemStateStub.mock.calls[0][0].scoringData.value[0].value).toBe('string')
})
it('should show Errors if errorsAreShowing is true and there are itemBody errors', () => {
const errorMsg = "I'm sorry Dave"
const testProps = {
...props,
errorsAreShowing: true,
errors: {
itemBody: [errorMsg],
},
}
const {container} = render(<NumericEdit {...testProps} />)
expect(container.textContent).toContain(errorMsg)
})
it('should show Errors if errorsAreShowing is true and there are scoringData errors', () => {
const errorMsg = "I'm afraid I can't do that"
const testProps = {
...props,
errorsAreShowing: true,
errors: {
scoringData: {
value: [
{
value: [errorMsg],
},
],
},
},
}
const {container} = render(<NumericEdit {...testProps} />)
expect(container.textContent).toContain(errorMsg)
})
describe('exactResponse', () => {
it('allows answers in scientific notation', () => {
const testProps = {
...props,
scoringData: {
value: [
{
type: 'exactResponse',
id: '1',
value: '1.23*10^-3',
},
],
},
}
render(<NumericEdit {...testProps} />)
// Should not have been called with errors
const callsWithErrors = changeItemStateStub.mock.calls.filter(
call => call[0] && call[0].errors && Object.keys(call[0].errors).length > 0,
)
expect(callsWithErrors).toHaveLength(0)
})
it('allows very large numbers in scientific notation', () => {
const testProps = {
...props,
scoringData: {
value: [
{
type: 'exactResponse',
id: '1',
value: '1*10^500',
},
],
},
}
render(<NumericEdit {...testProps} />)
// Should not have been called with errors
const callsWithErrors = changeItemStateStub.mock.calls.filter(
call => call[0] && call[0].errors && Object.keys(call[0].errors).length > 0,
)
expect(callsWithErrors).toHaveLength(0)
})
})
describe('marginOfError', () => {
it('does not allow answers in scientific notation', () => {
const testProps = {
...props,
scoringData: {
value: [
{
type: 'marginOfError',
id: '1',
value: '1.23*10^-3',
margin: '1',
marginType: 'percent',
},
],
},
}
render(<NumericEdit {...testProps} />)
const matchingCall = changeItemStateStub.mock.calls.find(([arg]) =>
arg.errors?.scoringData?.value?.[0]?.value?.some(
msg => msg === 'Scientific notation not supported with margin of error',
),
)
expect(matchingCall).toBeDefined()
})
})
describe('withinARange', () => {
it('allows ranges in scientific notation', () => {
const testProps = {
...props,
scoringData: {
value: [
{
type: 'withinARange',
id: '1',
start: '5*10^-3',
end: '1*10^-1',
},
],
},
}
render(<NumericEdit {...testProps} />)
// Should not have been called with errors
const callsWithErrors = changeItemStateStub.mock.calls.filter(
call => call[0] && call[0].errors && Object.keys(call[0].errors).length > 0,
)
expect(callsWithErrors).toHaveLength(0)
})
it('does not allow ranges with only the start in scientific notation', () => {
const testProps = {
...props,
scoringData: {
value: [
{
type: 'withinARange',
id: '1',
start: '5*10^-3',
end: '0.1',
},
],
},
}
render(<NumericEdit {...testProps} />)
const matchingCall = changeItemStateStub.mock.calls.find(([arg]) =>
arg.errors?.scoringData?.value?.[0]?.end?.some(
msg => msg === 'Must be in scientific notation',
),
)
expect(matchingCall).toBeDefined()
})
it('does not allow ranges with only the end in scientific notation', () => {
const testProps = {
...props,
scoringData: {
value: [
{
type: 'withinARange',
id: '1',
start: '0.005',
end: '1*10^-1',
},
],
},
}
render(<NumericEdit {...testProps} />)
const matchingCall = changeItemStateStub.mock.calls.find(([arg]) =>
arg.errors?.scoringData?.value?.[0]?.start?.some(
msg => msg === 'Must be in scientific notation',
),
)
expect(matchingCall).toBeDefined()
})
it('does not allow ranges with end < start', () => {
const testProps = {
...props,
scoringData: {
value: [
{
type: 'withinARange',
id: '1',
start: '5',
end: '3',
},
],
},
}
render(<NumericEdit {...testProps} />)
const matchingCall = changeItemStateStub.mock.calls.find(([arg]) =>
arg.errors?.scoringData?.value?.[0]?.end?.some(
msg => msg === 'Range end must be greater than range start',
),
)
expect(matchingCall).toBeDefined()
})
it('does not allow ranges in scientific notation with end < start', () => {
const testProps = {
...props,
scoringData: {
value: [
{
type: 'withinARange',
id: '1',
start: '1*10^3',
end: '5*10^2',
},
],
},
}
render(<NumericEdit {...testProps} />)
const matchingCall = changeItemStateStub.mock.calls.find(([arg]) =>
arg.errors?.scoringData?.value?.[0]?.end?.some(
msg => msg === 'Range end must be greater than range start',
),
)
expect(matchingCall).toBeDefined()
})
it('allows ranges with very large numbers in scientific notation', () => {
const testProps = {
...props,
scoringData: {
value: [
{
type: 'withinARange',
id: '1',
start: '1*10^400',
end: '1*10^500',
},
],
},
}
render(<NumericEdit {...testProps} />)
// Should not have been called with errors
const callsWithErrors = changeItemStateStub.mock.calls.filter(
call => call[0] && call[0].errors && Object.keys(call[0].errors).length > 0,
)
expect(callsWithErrors).toHaveLength(0)
})
})
describe('preciseResponse', () => {
it('does not allow answers in scientific notation', () => {
const testProps = {
...props,
scoringData: {
value: [
{
type: 'preciseResponse',
id: '1',
value: '1.23*10^-3',
precision: '3',
precisionType: 'significantDigits',
},
],
},
}
render(<NumericEdit {...testProps} />)
const matchingCall = changeItemStateStub.mock.calls.find(([arg]) =>
arg.errors?.scoringData?.value?.[0]?.value?.some(
msg => msg === 'Scientific notation not supported with precise response',
),
)
expect(matchingCall).toBeDefined()
})
it('updates precision as a string when switching precision type', () => {
const answer = {
type: 'preciseResponse',
id: '12',
value: '12.5',
precision: '1',
precisionType: 'decimals',
}
const testProps = {
...props,
scoringData: {
value: [answer],
},
}
render(<NumericEdit {...testProps} />)
const typeSelect = screen.getByRole('combobox', {name: /type/i})
userEvent.click(typeSelect)
userEvent.click(screen.getByRole('option', {name: /significant digits/i}))
expect(changeItemStateStub).toHaveBeenCalledTimes(1)
expect(changeItemStateStub).toHaveBeenCalledWith({
scoringData: {
value: [{...answer, precision: '3', precisionType: 'significantDigits'}],
},
})
})
describe('with significantDigits', () => {
it('adjusts precision when answer value changes', () => {
const answer = {
type: 'preciseResponse',
id: '42',
value: '3.14159',
precision: '6',
precisionType: 'significantDigits',
}
const testProps = {
...props,
scoringData: {
value: [answer],
},
}
const {container} = render(<NumericEdit {...testProps} />)
const answerInput = container.querySelector(
'[data-automation="sdk-numeric-precise-answer-number-input"]',
)
fireEvent.change(answerInput, {target: {value: '3.141593'}})
expect(changeItemStateStub).toHaveBeenCalledTimes(1)
expect(changeItemStateStub).toHaveBeenCalledWith({
scoringData: {
value: [{...answer, value: '3.141593', precision: '7'}],
},
})
})
it('sets precision correctly when answer has leading zeros', () => {
const answer = {
type: 'preciseResponse',
id: '42',
value: '0',
precision: '1',
precisionType: 'significantDigits',
}
const testProps = {
...props,
scoringData: {
value: [answer],
},
}
const {container} = render(<NumericEdit {...testProps} />)
const answerInput = container.querySelector(
'[data-automation="sdk-numeric-precise-answer-number-input"]',
)
fireEvent.change(answerInput, {target: {value: '0.0123'}})
expect(changeItemStateStub).toHaveBeenCalledTimes(1)
expect(changeItemStateStub).toHaveBeenCalledWith({
scoringData: {
value: [{...answer, value: '0.0123', precision: '3'}],
},
})
})
describe('when answer is equal to zero', () => {
// According to resident math expert mbd, zero always has 1 significant digit
it('sets precision to 1', () => {
const answer = {
type: 'preciseResponse',
id: '42',
value: '0',
precision: '1',
precisionType: 'significantDigits',
}
const testProps = {
...props,
scoringData: {
value: [answer],
},
}
const {container} = render(<NumericEdit {...testProps} />)
const answerInput = container.querySelector(
'[data-automation="sdk-numeric-precise-answer-number-input"]',
)
fireEvent.change(answerInput, {target: {value: '0.0'}})
expect(changeItemStateStub).toHaveBeenCalledTimes(1)
expect(changeItemStateStub).toHaveBeenCalledWith({
scoringData: {
value: [{...answer, value: '0.0', precision: '1'}],
},
})
})
it('prevents changing precision', () => {
const answer = {
type: 'preciseResponse',
id: '42',
value: '0',
precision: '1',
precisionType: 'significantDigits',
}
const testProps = {
...props,
scoringData: {
value: [answer],
},
}
const {container} = render(<NumericEdit {...testProps} />)
const precisionInput = container.querySelector(
'[data-automation="sdk-numeric-precise-precision-number-input"]',
)
const buttons = precisionInput.parentElement.querySelectorAll('button')
const upArrowButton = buttons[0]
const downArrowButton = buttons[1]
userEvent.click(upArrowButton)
expect(changeItemStateStub).not.toHaveBeenCalled()
userEvent.click(downArrowButton)
expect(changeItemStateStub).not.toHaveBeenCalled()
})
})
it('adjusts answer value when precision changes', () => {
const answer = {
type: 'preciseResponse',
id: '7',
value: '6.5',
precision: '2',
precisionType: 'significantDigits',
}
const testProps = {
...props,
scoringData: {
value: [answer],
},
}
const {container} = render(<NumericEdit {...testProps} />)
const precisionInput = container.querySelector(
'[data-automation="sdk-numeric-precise-precision-number-input"]',
)
fireEvent.change(precisionInput, {target: {value: '3'}})
expect(changeItemStateStub).toHaveBeenCalledTimes(1)
expect(changeItemStateStub).toHaveBeenCalledWith({
scoringData: {
value: [{...answer, value: '6.50', precision: '3'}],
},
})
})
it('sets min precision', () => {
const answer = {
type: 'preciseResponse',
id: '12',
value: '12.5',
precision: '3',
precisionType: 'significantDigits',
}
const testProps = {
...props,
scoringData: {
value: [answer],
},
}
const {container} = render(<NumericEdit {...testProps} />)
const precisionInput = container.querySelector(
'[data-automation="sdk-numeric-precise-precision-number-input"]',
)
const buttons = precisionInput.parentElement.querySelectorAll('button')
const downArrowButton = buttons[1]
// Ensure min is working by making sure we can't go below 3
userEvent.click(downArrowButton)
const callWithPrecision2 = changeItemStateStub.mock.calls.find(([arg]) =>
arg.scoringData?.value?.some(a => a.precision === '2'),
)
expect(callWithPrecision2).not.toBeDefined()
})
it('sets max precision', () => {
const answer = {
type: 'preciseResponse',
id: '12',
value: '12.50',
precision: '21',
precisionType: 'significantDigits',
}
const testProps = {
...props,
scoringData: {
value: [answer],
},
}
const {container} = render(<NumericEdit {...testProps} />)
const precisionInput = container.querySelector(
'[data-automation="sdk-numeric-precise-precision-number-input"]',
)
const buttons = precisionInput.parentElement.querySelectorAll('button')
const upArrowButton = buttons[0]
// Ensure max is working by making sure we can't go above 21
userEvent.click(upArrowButton)
const callWithPrecision22 = changeItemStateStub.mock.calls.find(([arg]) =>
arg.scoringData?.value?.some(a => a.precision === '22'),
)
expect(callWithPrecision22).not.toBeDefined()
})
})
describe('with decimals', () => {
it('adjusts precision when answer value changes', () => {
const answer = {
type: 'preciseResponse',
id: '42',
value: '3.14159',
precision: '5',
precisionType: 'decimals',
}
const testProps = {
...props,
scoringData: {
value: [answer],
},
}
const {container} = render(<NumericEdit {...testProps} />)
const answerInput = container.querySelector(
'[data-automation="sdk-numeric-precise-answer-number-input"]',
)
fireEvent.change(answerInput, {target: {value: '3.141593'}})
expect(changeItemStateStub).toHaveBeenCalledOnce()
expect(changeItemStateStub).toHaveBeenCalledWith({
scoringData: {
value: [{...answer, value: '3.141593', precision: '6'}],
},
})
})
it('adjusts answer value when precision changes', () => {
const answer = {
type: 'preciseResponse',
id: '7',
value: '6.5',
precision: '1',
precisionType: 'decimals',
}
const testProps = {
...props,
scoringData: {
value: [answer],
},
}
const {container} = render(<NumericEdit {...testProps} />)
const precisionInput = container.querySelector(
'[data-automation="sdk-numeric-precise-precision-number-input"]',
)
fireEvent.change(precisionInput, {target: {value: '2'}})
expect(changeItemStateStub).toHaveBeenCalledTimes(1)
expect(changeItemStateStub).toHaveBeenCalledWith({
scoringData: {
value: [{...answer, value: '6.50', precision: '2'}],
},
})
})
it('sets the minimum precision to the number of decimal digits in the answer', () => {
const answer = {
type: 'preciseResponse',
id: '12',
value: '12.5',
precision: '1',
precisionType: 'decimals',
}
const testProps = {
...props,
scoringData: {
value: [answer],
},
}
const {container} = render(<NumericEdit {...testProps} />)
const precisionInput = container.querySelector(
'[data-automation="sdk-numeric-precise-precision-number-input"]',
)
const buttons = precisionInput.parentElement.querySelectorAll('button')
const downArrowButton = buttons[1]
// Ensure min is working by making sure we can't go below 1
userEvent.click(downArrowButton)
const callWithPrecision0 = changeItemStateStub.mock.calls.find(([arg]) =>
arg.scoringData?.value?.some(a => a.precision === '0'),
)
expect(callWithPrecision0).not.toBeDefined()
})
it('sets max precision', () => {
const answer = {
type: 'preciseResponse',
id: '12',
value: '12.50',
precision: '100',
precisionType: 'decimals',
}
const testProps = {
...props,
scoringData: {
value: [answer],
},
}
const {container} = render(<NumericEdit {...testProps} />)
const precisionInput = container.querySelector(
'[data-automation="sdk-numeric-precise-precision-number-input"]',
)
const buttons = precisionInput.parentElement.querySelectorAll('button')
const upArrowButton = buttons[0]
// Ensure max is working by making sure we can't go above 100
userEvent.click(upArrowButton)
const callWithPrecision101 = changeItemStateStub.mock.calls.find(([arg]) =>
arg.scoringData?.value?.some(a => a.precision === '101'),
)
expect(callWithPrecision101).not.toBeDefined()
})
it("doesn't update answer when precision is not a number", () => {
const answer = {
type: 'preciseResponse',
id: '1',
value: '0',
precision: '1',
precisionType: 'significantDigits',
}
const testProps = {
...props,
scoringData: {
value: [answer],
},
}
const {container} = render(<NumericEdit {...testProps} />)
const precisionInput = container.querySelector(
'[data-automation="sdk-numeric-precise-precision-number-input"]',
)
fireEvent.change(precisionInput, {target: {value: 'not-a-number'}})
const callWithNaN = changeItemStateStub.mock.calls.find(([arg]) =>
arg.scoringData?.value?.some(a => isNaN(a.precision)),
)
expect(callWithNaN).not.toBeDefined()
})
it("doesn't update precision when answer is not a number", () => {
const answer = {
type: 'preciseResponse',
id: '1',
value: '0',
precision: '1',
precisionType: 'significantDigits',
}
const testProps = {
...props,
scoringData: {
value: [answer],
},
}
const {container} = render(<NumericEdit {...testProps} />)
const answerInput = container.querySelector(
'[data-automation="sdk-numeric-precise-precision-number-input"]',
)
fireEvent.change(answerInput, {target: {value: 'not-a-number'}})
const callWithNaN = changeItemStateStub.mock.calls.find(([arg]) =>
arg.scoringData?.value?.some(a => isNaN(a.value)),
)
expect(callWithNaN).not.toBeDefined()
})
})
})
describe('a11y tests', () => {
it('should meet a11y standards', async () => {
render(
<main>
<NumericEdit {...props} />
</main>,
)
expect(
await runAxeCheck(document.body, {
ignores: [
'aria-allowed-role', // TODO: remove this when instui fixes Select
'aria-allowed-attr',
] /* QUIZ-8495 */,
}),
).toBe(true)
})
})
describe('calculator per question option', () => {
it('does not render the options if showCalculatorOption is false', () => {
render(<NumericEdit {...props} showCalculatorOption={false} />)
expect(screen.queryByRole('button', {name: /options/i})).toBeNull()
})
it('renders the calculator per question option', () => {
render(<NumericEdit {...props} />)
const calculatorCheckboxes = screen.queryAllByRole('checkbox', {
name: /show on-screen calculator/i,
})
expect(calculatorCheckboxes).toHaveLength(1)
})
it('calls the correct callback function with the correct data when the calculator type is changed', () => {
render(<NumericEdit {...props} />)
const scientificOption = screen.getByRole('radio', {name: /scientific calculator/i})
fireEvent.click(scientificOption)
expect(changeItemStateStub).toHaveBeenCalledWith(
expect.objectContaining({
calculatorType: 'scientific',
}),
)
})
it('calls the correct callback function when OQAAT is changed', () => {
render(<NumericEdit {...props} />)
const toggle = screen.getByRole('checkbox', {name: /enable one question at a time/i})
fireEvent.click(toggle)
expect(setOneQuestionAtATimeStub).toHaveBeenCalledTimes(1)
})
})
it('displays help modal when help button clicked', () => {
render(<NumericEdit {...props} />)
const helpButtons = screen.getAllByRole('button', {
name: /Display help dialog for numeric requirement/i,
})
fireEvent.click(helpButtons[0])
const modal = screen.getByRole('dialog', {name: /Requirement help/i})
expect(modal).toBeInTheDocument()
// Verify modal content
expect(modal.textContent).toContain('Exact Response')
expect(modal.textContent).toContain('Margin of Error')
expect(modal.textContent).toContain('Within a Range')
expect(modal.textContent).toContain('Precise Response')
// Verify the modal content
expect(modal.textContent).toContain('Exact Response')
// TODO: QUIZ-XXXXX — Instructure UI Modal close button interaction
// cannot be tested with RTL. The click events (fireEvent.click, userEvent.click,
// native .click(), dispatchEvent) do not trigger the onDismiss callback through
// the Instructure UI Button inside a Modal portal. Verified the modal opens and
// displays the correct content above.
})
})