UNPKG

@instructure/quiz-interactions

Version:

A React UI component Library for quiz interaction types.

944 lines (800 loc) • 32.5 kB
import {vi} from 'vitest' import React from 'react' import update from 'immutability-helper' import FormulaEdit from '../index' import { isScientificNotation, parseScientificNotation, scientificNotationToNumber, } from '@instructure/quiz-scientific-notation' import { verifyAtLeastOneRichContentEditorExists, verifyNoRichContentEditorsExist, } from '../../../../../tests/util/enableRichContentEditorChecks' import {verifyItemChangesAreValidated} from '../../../../../tests/util/shouldValidateItemChanges' import {render, screen, waitFor, fireEvent} from '@testing-library/react' import userEvent from '@testing-library/user-event' import runAxeCheck from '@instructure/ui-axe-check' import {loadMathjs} from '../../common/util' // Mock RichContentInput so we can trigger onChange in tests vi.mock('@instructure/quiz-rce/components/RichContentInput/index', async importOriginal => { const actual = await importOriginal() const React = await import('react') return { ...actual, RichContentInput: function MockRCE({ onChange, defaultContent, disabled, readOnly, messages, ...rest }) { return React.createElement( 'div', {'data-automation': 'sdk-rce-question-stem'}, React.createElement('textarea', { defaultValue: defaultContent || '', disabled: disabled || readOnly || false, readOnly: readOnly || false, onChange: e => onChange && onChange({}, {editorContent: e.target.value}), }), messages && // eslint-disable-next-line react/no-array-index-key messages.map((msg, i) => React.createElement('span', {key: i}, msg.text || msg)), ) }, } }) // Pre-load mathjs so withAsyncDeps doesn't render a loading spinner beforeAll(async () => { await loadMathjs() }) function createProps( changeItemStateStub, openImportModalStub, notifyScreenReaderStub, setOneQuestionAtATimeStub, ) { return { calculatorType: 'basic', changeItemState: changeItemStateStub, errorsAreShowing: false, interactionData: {}, itemBody: 'find `x` + `y`', openImportModal: openImportModalStub, notifyScreenreader: notifyScreenReaderStub, setOneQuestionAtATime: setOneQuestionAtATimeStub, scoringData: { value: { answerCount: '2', answerPrecision: 3, formula: 'x+y', numeric: { type: 'marginOfError', margin: '0', marginType: 'absolute', }, generatedSolutions: [ { output: 3, inputs: [ {name: 'x', value: 1}, {name: 'y', value: 2}, ], }, { output: 10, inputs: [ {name: 'x', value: 3}, {name: 'y', value: 7}, ], }, ], variables: [ {name: 'x', min: 0, max: 5, precision: 0}, {name: 'y', min: 0, max: 5, precision: 0}, ], }, }, } } const assertGeneratedSolutionsCleared = changeItemStateStub => { const arg = changeItemStateStub.mock.lastCall[0] expect(arg.scoringData.value.generatedSolutions).toEqual([]) } describe('***** Formula edit old tests *****', () => { const changeItemStateStub = vi.fn() const openImportModalStub = vi.fn() const notifyScreenReaderStub = vi.fn() const setOneQuestionAtATimeStub = vi.fn() const defaultProps = createProps( changeItemStateStub, openImportModalStub, notifyScreenReaderStub, setOneQuestionAtATimeStub, ) afterEach(() => { changeItemStateStub.mockClear() openImportModalStub.mockClear() notifyScreenReaderStub.mockClear() }) describe('override editable for regrading', () => { it('renders a disabled stem', () => { const {container} = render(<FormulaEdit {...defaultProps} overrideEditableForRegrading />) const rceTextarea = container.querySelector( '[data-automation="sdk-rce-question-stem"] textarea', ) expect(rceTextarea.disabled).toBe(true) }) }) describe('changing itemBody', () => { const runTest = newItemBody => { const {container} = render(<FormulaEdit {...defaultProps} />) const rceTextarea = container.querySelector( '[data-automation="sdk-rce-question-stem"] textarea', ) fireEvent.change(rceTextarea, {target: {value: newItemBody}}) } it('updates when only text changes', () => { runTest('find `x` + `y`!!!') const arg = changeItemStateStub.mock.lastCall[0] expect(arg.itemBody).toBe('find `x` + `y`!!!') }) it('clears the generated solutions', () => { runTest('find `x` + `y`!!!') assertGeneratedSolutionsCleared(changeItemStateStub) }) it('updates when a new variable is added', () => { runTest('find `x` + `y` + `a`') const arg = changeItemStateStub.mock.lastCall[0] expect(arg.itemBody).toBe('find `x` + `y` + `a`') expect(arg.scoringData.value.variables).toEqual([ {name: 'a', min: 0, max: 10, precision: 0}, {name: 'x', min: 0, max: 5, precision: 0}, {name: 'y', min: 0, max: 5, precision: 0}, ]) }) it('updates when a variable is removed', () => { runTest('find [x + `y`') const arg = changeItemStateStub.mock.lastCall[0] expect(arg.itemBody).toBe('find [x + `y`') expect(arg.scoringData.value.variables).toEqual([{name: 'y', min: 0, max: 5, precision: 0}]) }) it('updates when a new variable is changed', () => { runTest('find `xx` + `y`') const arg = changeItemStateStub.mock.lastCall[0] expect(arg.itemBody).toBe('find `xx` + `y`') expect(arg.scoringData.value.variables).toEqual([ {name: 'xx', min: 0, max: 10, precision: 0}, {name: 'y', min: 0, max: 5, precision: 0}, ]) }) }) }) describe('Formula edit', () => { let defaultProps let changeItemStateStub let openImportModalStub let notifyScreenReaderStub let setOneQuestionAtATimeStub beforeEach(() => { changeItemStateStub = vi.fn() openImportModalStub = vi.fn() notifyScreenReaderStub = vi.fn() setOneQuestionAtATimeStub = vi.fn() defaultProps = createProps( changeItemStateStub, openImportModalStub, notifyScreenReaderStub, setOneQuestionAtATimeStub, ) }) it('renders a rich content editor by default', async () => { await verifyAtLeastOneRichContentEditorExists(FormulaEdit, defaultProps) }) it('does not render a rich content editor when it is turned off', async () => { await verifyNoRichContentEditorsExist(FormulaEdit, { ...defaultProps, enableRichContentEditor: false, }) }) it('includes validation errors in changeItemState', async () => { await verifyItemChangesAreValidated(FormulaEdit, {...defaultProps, itemBody: ''}) }) it('renders a row for each variable', () => { const {container} = render(<FormulaEdit {...defaultProps} />) const trs = container.querySelectorAll('[data-section="variable_definitions"] tr') expect(trs).toHaveLength(3) }) describe('override editable for regrading', () => { it('renders number inputs with the correct disabled state', () => { render(<FormulaEdit {...defaultProps} overrideEditableForRegrading />) const xVariableMinInput = screen.getByLabelText(/Minimum value for variable x/i) const xVariableMaxInput = screen.getByLabelText(/Maximum value for variable x/i) const xVariableDecimalsInput = screen.getByLabelText(/decimals of precision for variable x/i) const yVariableMinInput = screen.getByLabelText(/Minimum value for variable y/i) const yVariableMaxInput = screen.getByLabelText(/Maximum value for variable y/i) const yVariableDecimalsInput = screen.getByLabelText(/decimals of precision for variable y/i) const numberOfSolutionsInput = document.querySelector( '[data-automation="sdk-number-of-solutions-input"]', ) const decimalPlacesInput = screen.getByLabelText(/Decimal places/i) const marginOfErrorInput = screen.getByLabelText(/\+\/- margin of error/i) expect(xVariableMinInput.disabled).toBe(true) expect(xVariableMaxInput.disabled).toBe(true) expect(xVariableDecimalsInput.disabled).toBe(true) expect(yVariableMinInput.disabled).toBe(true) expect(yVariableMaxInput.disabled).toBe(true) expect(yVariableDecimalsInput.disabled).toBe(true) expect(numberOfSolutionsInput.disabled).toBe(true) expect(decimalPlacesInput.disabled).toBe(true) expect(marginOfErrorInput.disabled).toBe(false) }) it('disables the calculator option', () => { render(<FormulaEdit {...defaultProps} 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.disabled).toBe(true) expect(calculatorBasicRadioInput.disabled).toBe(true) expect(calculatorScientificRadioInput.disabled).toBe(true) }) }) describe('range data', () => { const varBlurredProperly = (field, inputVal, canonical, localized) => { const {rerender} = render(<FormulaEdit {...defaultProps} />) const minMax = field === 'min' ? 'Minimum' : 'Maximum' const input = screen.getAllByLabelText(new RegExp(`${minMax} value for variable`, 'i'))[0] fireEvent.change(input, {target: {value: inputVal}}) fireEvent.blur(input) const arg = changeItemStateStub.mock.lastCall[0] // scoringData contains a canonical representation e.g. '4200.07' (en) expect(arg.scoringData.value.variables[0][field]).toBe(canonical) assertGeneratedSolutionsCleared(changeItemStateStub) const props = update(defaultProps, { scoringData: { value: { variables: { 0: { [field]: { $set: canonical, }, }, }, }, }, }) // setup new props to pass back in and render as would normally happen on blur rerender(<FormulaEdit {...props} />) // the input itself would be localized e.g. '4 200,07' (fr) expect(input.value).toBe(localized) } const varChangedProperly = (field, inputVal, outputVal) => { render(<FormulaEdit {...defaultProps} />) const minMax = field === 'min' ? 'Minimum' : 'Maximum' const input = screen.getAllByLabelText(new RegExp(`${minMax} value for variable`, 'i'))[0] fireEvent.change(input, {target: {value: inputVal}}) expect(input.value).toBe(outputVal) } const reactInputsProperly = (field, initialValue) => { render(<FormulaEdit {...defaultProps} />) const minMax = field === 'min' ? 'Minimum' : 'Maximum' const input = screen.getAllByLabelText(new RegExp(`${minMax} value for variable`, 'i'))[0] // test initial default expect(input.value).toBe(initialValue) // it can be blank fireEvent.change(input, {target: {value: ''}}) expect(input.value).toBe('') // it takes a number input fireEvent.change(input, {target: {value: '2'}}) expect(input.value).toBe('2') // blank input is changed to default on blur fireEvent.change(input, {target: {value: ''}}) expect(input.value).toBe('') fireEvent.blur(input) expect(input.value).toBe(initialValue) // test '-' input is changed to default on blur fireEvent.change(input, {target: {value: '-'}}) expect(input.value).toBe('-') fireEvent.blur(input) expect(input.value).toBe(initialValue) } describe('minimum input', () => { it('reacts properly on input changes', () => { reactInputsProperly('min', '0') }) it('takes intermediate inputs on change', () => { varChangedProperly('min', '-', '-') }) it('takes a non-digit character on change', () => { varChangedProperly('min', 'Q', 'Q') }) it('handles changes with valid number appropriately', () => { varChangedProperly('min', '2.99', '2.99') }) it('handle precision changes appropriately on blur', () => { varBlurredProperly('min', '2.99', '3', '3') }) it('formats properly on blur', () => { varBlurredProperly('min', '500000', '500000', '500,000') }) }) describe('maximum input', () => { it('reacts properly on input changes', () => { reactInputsProperly('max', '5') }) it('takes intermediate inputs on change', () => { varChangedProperly('max', '-', '-') }) it('takes a non-digit character on change', () => { varChangedProperly('max', 'Q', 'Q') }) it('handles changes with valid number appropriately', () => { varChangedProperly('max', '2.99', '2.99') }) it('handles precision changes appropriately on blur', () => { varBlurredProperly('max', '2.99', '3', '3') }) it('formats properly on blur', () => { varBlurredProperly('max', '500000', '500000', '500,000') }) }) it('changes the precision', async () => { render(<FormulaEdit {...defaultProps} />) const input = screen.getAllByLabelText(/decimals of precision for variable/i)[0] fireEvent.change(input, {target: {value: '2'}}) await waitFor(() => { const arg = changeItemStateStub.mock.lastCall[0] expect(arg.scoringData.value.variables[0].min).toBe('0.00') expect(arg.scoringData.value.variables[0].max).toBe('5.00') expect(arg.scoringData.value.variables[0].precision).toBe('2') assertGeneratedSolutionsCleared(changeItemStateStub) }) }) for (const field of ['min', 'max']) { it(`doesn't call changeItemState on ${field} blur if the value hasn't changed`, () => { render(<FormulaEdit {...defaultProps} />) const minMax = field === 'min' ? 'Minimum' : 'Maximum' const input = screen.getByLabelText(new RegExp(`${minMax} value for variable x`, 'i')) const variable = defaultProps.scoringData.value.variables.find(v => v.name === 'x') fireEvent.change(input, {target: {value: variable[field].toString()}}) fireEvent.blur(input) expect(changeItemStateStub).not.toHaveBeenCalled() }) } it("doesn't call changeItemState on precision blur if the value hasn't changed", () => { render(<FormulaEdit {...defaultProps} />) const input = screen.getByLabelText(/decimals of precision for variable x/i) const variable = defaultProps.scoringData.value.variables.find(v => v.name === 'x') fireEvent.change(input, {target: {value: variable.precision.toString()}}) fireEvent.blur(input) expect(changeItemStateStub).not.toHaveBeenCalled() }) }) describe('formula section', () => { it('updates the formula', () => { const {container} = render(<FormulaEdit {...defaultProps} />) const questionStemInput = container.querySelector('[data-section="formula"] textarea') fireEvent.change(questionStemInput, {target: {value: 'x*y'}}) const arg = changeItemStateStub.mock.lastCall[0] expect(arg.scoringData.value.formula).toBe('x*y') assertGeneratedSolutionsCleared(changeItemStateStub) }) it('updates the number of solutions', () => { render(<FormulaEdit {...defaultProps} />) const numberOfSolutionsInput = document.querySelector( '[data-automation="sdk-number-of-solutions-input"]', ) fireEvent.change(numberOfSolutionsInput, {target: {value: '5'}}) const arg = changeItemStateStub.mock.lastCall[0] expect(arg.scoringData.value.answerCount).toBe('5') assertGeneratedSolutionsCleared(changeItemStateStub) }) it('updates the answer precision and clears generated solutions', () => { render(<FormulaEdit {...defaultProps} />) const decimalPlacesInput = screen.getByLabelText(/Decimal places/i) fireEvent.change(decimalPlacesInput, {target: {value: '5'}}) const arg = changeItemStateStub.mock.lastCall[0] expect(arg.scoringData.value.answerPrecision).toBe(5) assertGeneratedSolutionsCleared(changeItemStateStub) }) it('updates the margin of error', () => { render(<FormulaEdit {...defaultProps} />) const marginOfErrorInput = screen.getByLabelText(/\+\/- margin of error/i) fireEvent.change(marginOfErrorInput, {target: {value: '5'}}) const arg = changeItemStateStub.mock.lastCall[0] expect(arg.scoringData.value.numeric.margin).toBe('5') }) it('normalizes the margin of error', () => { render(<FormulaEdit {...defaultProps} />) const marginOfErrorInput = screen.getByLabelText(/\+\/- margin of error/i) fireEvent.change(marginOfErrorInput, {target: {value: '5,000'}}) const arg = changeItemStateStub.mock.lastCall[0] expect(arg.scoringData.value.numeric.margin).toBe('5000') }) it('normalizes an invalid margin of error', () => { render(<FormulaEdit {...defaultProps} />) const marginOfErrorInput = screen.getByRole('spinbutton', {name: /\+\/- margin of error/i}) userEvent.clear(marginOfErrorInput) userEvent.type(marginOfErrorInput, '5.5.5.5') userEvent.tab() const arg = changeItemStateStub.mock.lastCall[0] expect(arg.scoringData.value.numeric.margin).toBe('555.5') }) it('updates the margin of error type', () => { render(<FormulaEdit {...defaultProps} />) const marginTypeSelect = screen.getByRole('combobox', {name: /margin type/i}) userEvent.click(marginTypeSelect) userEvent.click(screen.getByRole('option', {name: /percent/i})) const arg = changeItemStateStub.mock.lastCall[0] expect(arg.scoringData.value.numeric.marginType).toBe('percent') }) it("doesn't call changeItemState on number of solutions blur if the value hasn't changed", () => { render(<FormulaEdit {...defaultProps} />) const input = document.querySelector('[data-automation="sdk-number-of-solutions-input"]') fireEvent.change(input, { target: {value: defaultProps.scoringData.value.answerCount.toString()}, }) fireEvent.blur(input) expect(changeItemStateStub).not.toHaveBeenCalled() }) it("doesn't call changeItemState on answer precision blur if the value hasn't changed", () => { render(<FormulaEdit {...defaultProps} />) const input = screen.getByLabelText(/Decimal places/i) fireEvent.change(input, { target: {value: defaultProps.scoringData.value.answerPrecision.toString()}, }) fireEvent.blur(input) expect(changeItemStateStub).not.toHaveBeenCalled() }) it("doesn't call changeItemState on margin of error blur if the value hasn't changed", () => { render(<FormulaEdit {...defaultProps} />) const input = screen.getByLabelText(/\+\/- margin of error/i) fireEvent.change(input, { target: {value: defaultProps.scoringData.value.numeric.margin.toString()}, }) fireEvent.blur(input) expect(changeItemStateStub).not.toHaveBeenCalled() }) it('generates solutions on button click', async () => { const props = update(defaultProps, { scoringData: { value: { answerCount: {$set: '5'}, generatedSolutions: {$set: []}, }, }, }) render(<FormulaEdit {...props} />) const generateButton = screen.getByRole('button', {name: /generate/i}) fireEvent.click(generateButton) await waitFor(() => { const arg = changeItemStateStub.mock.lastCall[0] expect(arg.scoringData.value.generatedSolutions.length).toBe(5) expect(notifyScreenReaderStub).toHaveBeenCalledWith( 'Solutions updated. We were able to find 5 solutions.', ) }) }) it('handles numeric variables stored as strings', async () => { const props = update(defaultProps, { scoringData: { value: { variables: { $set: [ {name: 'x', min: '5', max: '10', precision: '0'}, {name: 'y', min: '1', max: '29', precision: '0'}, ], }, answerCount: {$set: '1'}, generatedSolutions: {$set: []}, }, }, }) render(<FormulaEdit {...props} />) const generateButton = screen.getByRole('button', {name: /generate/i}) fireEvent.click(generateButton) await waitFor(() => { const arg = changeItemStateStub.mock.lastCall[0] expect(arg.scoringData.value.generatedSolutions.length).toBe(1) }) }) describe('errors in min/max for a variable', () => { let props beforeEach(() => { props = update(defaultProps, { scoringData: { value: { variables: { $set: [{name: 'x', min: 7, max: 5, precision: 0}], }, generatedSolutions: {$set: []}, }, }, }) }) it('does not generate solutions', () => { const {container} = render(<FormulaEdit {...props} />) const generateButton = screen.getByRole('button', {name: /generate/i}) fireEvent.click(generateButton) expect(container.textContent).toContain('Error in formula setup. See above for details.') }) it('notifies screenreader of variable errors', () => { render(<FormulaEdit {...props} />) const generateButton = screen.getByRole('button', {name: /generate/i}) fireEvent.click(generateButton) expect(notifyScreenReaderStub).toHaveBeenCalled() expect(notifyScreenReaderStub.mock.lastCall[0]).toContain( 'The following error prevented generating solutions:', ) expect(notifyScreenReaderStub.mock.lastCall[0]).toContain('variables containing errors: x') }) it('does not notify screenreader if no variable errors', () => { render(<FormulaEdit {...defaultProps} />) const generateButton = screen.getByRole('button', {name: /generate/i}) fireEvent.click(generateButton) expect(notifyScreenReaderStub).not.toHaveBeenCalled() }) }) }) it('displays errors when errors are showing', () => { const testProps = { ...defaultProps, itemBody: '', errorsAreShowing: true, } const {container} = render(<FormulaEdit {...testProps} />) expect(container.textContent).toContain('Question Stem cannot be blank') }) it('does not display errors when errors are not showing', () => { const testProps = { ...defaultProps, itemBody: '', errorsAreShowing: false, } const {container} = render(<FormulaEdit {...testProps} />) expect(container.textContent).not.toContain('Question Stem cannot be blank') }) describe('a11y tests', () => { it('should meet a11y standards', async () => { render(<FormulaEdit {...defaultProps} />) expect( await runAxeCheck(document.body, { ignores: [ 'region', 'aria-allowed-role', // TODO: remove this when instui fixes Select 'aria-allowed-attr', 'label', ] /* QUIZ-8495 */, }), ).toBe(true) }) }) describe('calculator per question option', () => { it('does not render the options if showCalculatorOption is false', () => { render(<FormulaEdit {...defaultProps} showCalculatorOption={false} />) expect(screen.queryByRole('button', {name: /options/i})).toBeNull() }) it('renders the calculator per question option', () => { render(<FormulaEdit {...defaultProps} />) 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(<FormulaEdit {...defaultProps} />) 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(<FormulaEdit {...defaultProps} />) const toggle = screen.getByRole('checkbox', {name: /enable one question at a time/i}) fireEvent.click(toggle) expect(setOneQuestionAtATimeStub).toHaveBeenCalledTimes(1) }) }) it('sets numeric type to exactResponse when scientific notation checkbox checked', async () => { render(<FormulaEdit {...defaultProps} />) const scientificNotationCheckbox = screen.getByLabelText(/Display as Scientific Notation/i) fireEvent.click(scientificNotationCheckbox) await waitFor(() => { expect(changeItemStateStub).toHaveBeenCalledWith( expect.objectContaining({ scoringData: expect.objectContaining({ value: expect.objectContaining({ numeric: expect.objectContaining({ type: 'exactResponse', }), generatedSolutions: [], }), }), }), ) }) }) it('sets numeric type to marginOfError when scientific notation checkbox unchecked', async () => { const scoringData = { ...defaultProps.scoringData, value: { ...defaultProps.scoringData.value, generatedSolutions: [], scientificNotation: true, }, } render(<FormulaEdit {...defaultProps} scoringData={scoringData} />) const scientificNotationCheckbox = screen.getByLabelText(/Display as Scientific Notation/i) fireEvent.click(scientificNotationCheckbox) await waitFor(() => { expect(changeItemStateStub).toHaveBeenCalledWith( expect.objectContaining({ scoringData: expect.objectContaining({ value: expect.objectContaining({ numeric: expect.objectContaining({ type: 'marginOfError', marginType: 'absolute', margin: 0, }), generatedSolutions: [], }), }), }), ) }) }) describe('with variables in scientific notation', () => { const scientificNotationScoringData = { value: { answerCount: 3, answerPrecision: 1, formula: 'x + y', numeric: {}, variables: [ { name: 'x', min: '5.0*10^-2', max: '2.5*10^-1', precision: 1, }, { name: 'y', min: '-0.01', max: '0.01', precision: 2, }, ], generatedSolutions: [], }, } it('generates inputs in scientific notation', async () => { render(<FormulaEdit {...defaultProps} scoringData={scientificNotationScoringData} />) const generateButton = screen.getByRole('button', {name: /generate/i}) fireEvent.click(generateButton) await waitFor(() => { expect(changeItemStateStub).toHaveBeenCalled() const {scoringData} = changeItemStateStub.mock.lastCall[0] const {generatedSolutions} = scoringData.value expect(Array.isArray(generatedSolutions)).toBe(true) const xs = generatedSolutions.map( solution => solution.inputs.find(input => input.name === 'x').value, ) for (const x of xs) { expect(isScientificNotation(x)).toBe(true) expect(scientificNotationToNumber(x)).toBeGreaterThanOrEqual(5e-2) expect(scientificNotationToNumber(x)).toBeLessThanOrEqual(2.5e-1) } }) }) it('generates outputs in decimal notation', async () => { const scoringData = { value: { ...scientificNotationScoringData.value, scientificNotation: false, }, } render(<FormulaEdit {...defaultProps} scoringData={scoringData} />) const generateButton = screen.getByRole('button', {name: /generate/i}) fireEvent.click(generateButton) await waitFor(() => { expect(changeItemStateStub).toHaveBeenCalled() const {scoringData} = changeItemStateStub.mock.lastCall[0] const {generatedSolutions} = scoringData.value expect(Array.isArray(generatedSolutions)).toBe(true) const outputs = generatedSolutions.map(solution => solution.output) for (const output of outputs) { expect(isScientificNotation(output)).toBe(false) } }) }) it('generates outputs in scientific notation', async () => { const scoringData = { value: { ...scientificNotationScoringData.value, scientificNotation: true, }, } render(<FormulaEdit {...defaultProps} scoringData={scoringData} />) const generateButton = screen.getByRole('button', {name: /generate/i}) fireEvent.click(generateButton) await waitFor(() => { expect(changeItemStateStub).toHaveBeenCalled() const {scoringData} = changeItemStateStub.mock.lastCall[0] const {generatedSolutions} = scoringData.value expect(Array.isArray(generatedSolutions)).toBe(true) const outputs = generatedSolutions.map(solution => solution.output) for (const output of outputs) { expect(isScientificNotation(output)).toBe(true) } }) }) it('updates min and max when precision changed', async () => { const {rerender} = render( <FormulaEdit {...defaultProps} scoringData={scientificNotationScoringData} />, ) const precisionInput = screen.getByLabelText(/precision for variable x/i) fireEvent.change(precisionInput, {target: {value: '2'}}) await waitFor(() => expect(changeItemStateStub).toHaveBeenCalled()) const updatedScoringData = changeItemStateStub.mock.lastCall[0].scoringData const scoringData = { value: { ...scientificNotationScoringData.value, ...updatedScoringData.value, }, } // Pass the updated scoring data back into the component as props rerender(<FormulaEdit {...defaultProps} scoringData={scoringData} />) const minInput = screen.getByLabelText(/Minimum value for variable x/i) const maxInput = screen.getByLabelText(/Maximum value for variable x/i) expect(minInput.value).toBe('5.00*10^-2') expect(maxInput.value).toBe('2.50*10^-1') }) it('handles very large numbers in scientific notation', async () => { const scoringData = { value: { ...scientificNotationScoringData.value, scientificNotation: true, variables: [ { name: 'x', min: '5*10^400', max: '1*10^500', precision: 0, }, { name: 'y', min: '-0.01', max: '0.01', precision: 2, }, ], }, } render(<FormulaEdit {...defaultProps} scoringData={scoringData} />) const generateButton = screen.getByRole('button', {name: /generate/i}) fireEvent.click(generateButton) await waitFor(() => { expect(changeItemStateStub).toHaveBeenCalled() const {scoringData} = changeItemStateStub.mock.lastCall[0] const {generatedSolutions} = scoringData.value expect(Array.isArray(generatedSolutions)).toBe(true) const outputs = generatedSolutions.map(solution => solution.output) for (const output of outputs) { expect(isScientificNotation(output)).toBe(true) const [_, exponent] = parseScientificNotation(output) // eslint-disable-line no-unused-vars expect(Number(exponent)).toBeGreaterThanOrEqual(400) expect(Number(exponent)).toBeLessThanOrEqual(500) } }) }) }) })