@instructure/quiz-interactions
Version:
A React UI component Library for quiz interaction types.
944 lines (800 loc) • 32.5 kB
JSX
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)
}
})
})
})
})