UNPKG

@instructure/quiz-interactions

Version:

A React UI component Library for quiz interaction types.

1,285 lines (1,106 loc) 37.5 kB
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. }) })