UNPKG

@instructure/quiz-interactions

Version:

A React UI component Library for quiz interaction types.

670 lines (620 loc) 19.9 kB
import {beforeAll, describe, expect, it} from 'vitest' import round from 'lodash/round' import Decimal from 'decimal.js' import { isScientificNotation, parseScientificNotation, scientificNotationToNumber, } from '@instructure/quiz-scientific-notation' import {searchForFormulaSolution} from '../util' import {loadMathjs} from '../../common/util' describe('util', () => { beforeAll(async () => { await loadMathjs() }) describe('searchForFormulaSolution', () => { const variables = [ { name: 'x', min: 0, max: 3, precision: 0, }, { name: 'y', min: 4, max: 6, precision: 0, }, ] const answerPrecision = 10 it('generates a solution for a formula', () => { const {inputs, output} = searchForFormulaSolution(variables, 'x+y', [], answerPrecision) expect(inputs).toHaveLength(2) const x = Number(inputs.find(i => i.name === 'x').value) const y = Number(inputs.find(i => i.name === 'y').value) expect(Number(output)).toBe(x + y) }) it('generates a solution for a formula with multiple clauses', () => { const formula = 'z=x+y \n z+2' const {inputs, output} = searchForFormulaSolution(variables, formula, [], answerPrecision) expect(inputs).toHaveLength(2) const x = Number(inputs.find(i => i.name === 'x').value) const y = Number(inputs.find(i => i.name === 'y').value) expect(Number(output)).toBe(x + y + 2) }) it('generates a solution for a formula with precision', () => { const precision = 1 const variables = [ { name: 'x', min: 3.0, max: 3.9, precision, }, { name: 'y', min: 4.0, max: 4.7, precision, }, ] const {inputs, output} = searchForFormulaSolution(variables, 'x*y', [], answerPrecision) expect(inputs).toHaveLength(2) const x = Number(inputs.find(i => i.name === 'x').value) const y = Number(inputs.find(i => i.name === 'y').value) expect(Number(output)).toBe(round(x * y, answerPrecision)) }) it('rounds the solution to given precision', () => { const precision = 2 const variables = [ { name: 'x', min: 12.34, max: 12.34, precision, }, { name: 'y', min: 56.78, max: 56.78, precision, }, ] const {inputs, output} = searchForFormulaSolution(variables, 'x*y', [], 2) expect(inputs).toHaveLength(2) expect(Number(output)).toBe(700.67) }) it('returns null when it cannot find a solution', () => { const result = searchForFormulaSolution(variables, 'x + sqrt(-1*y)', [], answerPrecision) expect(result).toBeNull() }) it('returns null when the formula is syntactically invalid', () => { const result = searchForFormulaSolution(variables, 'x +', [], answerPrecision) expect(result).toBeNull() }) it('ignores a previously seen solution', () => { const variables = [ { name: 'x', min: 1, max: 1, precision: 0, }, { name: 'y', min: 2, max: 4, precision: 0, }, ] const previousSolutions = [ { inputs: [ {name: 'y', value: 2}, {name: 'x', value: 1}, ], output: 3, }, { inputs: [ {name: 'y', value: 3}, {name: 'x', value: 1}, ], output: 4, }, ] const result = searchForFormulaSolution( variables, 'x + y', previousSolutions, answerPrecision, ) expect(result.inputs[0].name).toBe('x') expect(Number(result.inputs[0].value)).toBe(1) expect(result.inputs[1].name).toBe('y') expect(Number(result.inputs[1].value)).toBe(4) }) it('returns null when no new solution can be found', () => { const variables = [ { name: 'x', min: 1, max: 1, precision: '0', }, { name: 'y', min: 2, max: 3, precision: 0, }, ] const previousSolutions = [ { inputs: [ {name: 'y', value: 2}, {name: 'x', value: 1}, ], output: 3, }, { inputs: [ {name: 'y', value: 3}, {name: 'x', value: 1}, ], output: 4, }, ] const result = searchForFormulaSolution( variables, 'x + y', previousSolutions, answerPrecision, ) expect(result).toBeNull() }) it('works around bad JavaScript floating point math', () => { const variables = [ { name: 'x', min: 1.2, max: 10.4, precision: 1, }, { name: 'y', min: 0, max: 3, precision: 1, }, ] const genInputs = () => { return [ {name: 'x', value: 3.2}, {name: 'y', value: 0.7}, ] } const result = searchForFormulaSolution( variables, 'x + y', [], 1, false, Math.random, 1, genInputs, ) expect(result).toEqual({ inputs: [ {name: 'x', value: 3.2}, {name: 'y', value: 0.7}, ], output: '3.9', }) }) // Skip: mathjs+Decimal interop differs between Karma (real Chrome) and Vitest (jsdom). // Under Karma the Decimal path gives exact 12.55 → 12.6; under jsdom it falls back // to JS Number giving 12.549999… → 12.5. Needs a production code fix. // QUIZ-17487: https://instructure.atlassian.net/browse/QUIZ-17487 it.skip('rounds correctly despite bad floating point math', () => { const variables = [ { name: 'x', min: 14.2, max: 14.2, precision: 1, }, { name: 'y', min: 3.3, max: 3.3, precision: 1, }, ] const result = searchForFormulaSolution(variables, 'x - y/2', [], 1) // JavaScript returns 12.549999999999999 for this equation, which rounds to 12.5 // The correct answer to the equation is 12.55, which rounds to 12.6 expect(result).toEqual({ inputs: [ {name: 'x', value: '14.2'}, {name: 'y', value: '3.3'}, ], output: '12.6', }) }) it('generates a correct solution with cosec and cotan', () => { const formula = 'cosec(x)+cotan(y)' const precision = 4 const variables = [ { name: 'x', min: -4 * Math.PI, max: 4 * Math.PI, precision: precision, }, { name: 'y', min: -4 * Math.PI, max: 4 * Math.PI, precision: precision, }, ] const {inputs, output} = searchForFormulaSolution(variables, formula, [], answerPrecision) expect(inputs).toHaveLength(2) const x = Number(inputs.find(i => i.name === 'x').value) const y = Number(inputs.find(i => i.name === 'y').value) expect(round(1 / Math.sin(x) + 1 / Math.tan(y), answerPrecision)).toBe(Number(output)) }) it('generates a correct solution with rad_to_deg and deg_to_rad', () => { const formula = 'rad_to_deg(deg_to_rad(x))' const variables = [ { name: 'x', min: 90, max: 90, precision: 0, }, ] const {inputs, output} = searchForFormulaSolution(variables, formula, [], answerPrecision) expect(inputs).toHaveLength(1) const x = Number(inputs.find(i => i.name === 'x').value) expect(round(x, answerPrecision)).toBe(Number(output)) }) it('generates a correct solution with comb, perm, fact', () => { const formula = 'comb(x, 3) + perm(y, 6) + fact(z)' const precision = 0 const variables = [ { name: 'x', min: 5, max: 5, precision: precision, }, { name: 'y', min: 7, max: 7, precision: precision, }, { name: 'z', min: 4, max: 4, precision: precision, }, ] const {inputs, output} = searchForFormulaSolution(variables, formula, [], answerPrecision) expect(inputs).toHaveLength(3) expect(Number(output)).toBe(10 + 5040 + 24) }) it('generates a correct solution with rand', () => { const formula = 'rand() + rand(x) + rand(x,y)' const precision = 4 const variables = [ { name: 'x', min: 2, max: 5, precision: precision, }, { name: 'y', min: 6, max: 8, precision: precision, }, ] const {inputs, output} = searchForFormulaSolution(variables, formula, [], answerPrecision) expect(inputs).toHaveLength(2) const x = Number(inputs.find(i => i.name === 'x').value) const y = Number(inputs.find(i => i.name === 'y').value) expect(Number(output)).toBeGreaterThan(0 + 0 + x) expect(Number(output)).toBeLessThan(1 + x + y) }) it('generates a correct solution with list functions', () => { const formula = 'at(x, y, z, 1) + at([x,y,z],1) + first(x,y,z) + first([y,x,z]) + length(x,y,z) + length([x,y,z]) + last(reverse(z,x,y)) + first(reverse([z,x,y]))' const precision = 0 const variables = [ { name: 'x', min: 0, max: 7, precision: precision, }, { name: 'y', min: 8, max: 11, precision: precision, }, { name: 'z', min: 12, max: 15, precision: precision, }, ] const {inputs, output} = searchForFormulaSolution(variables, formula, [], answerPrecision) expect(inputs).toHaveLength(3) const x = Number(inputs.find(i => i.name === 'x').value) const y = Number(inputs.find(i => i.name === 'y').value) const z = Number(inputs.find(i => i.name === 'z').value) expect(y + y + x + y + 3 + 3 + z + y).toBe(Number(output)) }) it('generates a correct solution with range', () => { const formula = 'range(y,z,x,z) + range([x,y,z,x,y])' const precision = 0 const variables = [ { name: 'x', min: 0, max: 7, precision: precision, }, { name: 'y', min: 8, max: 11, precision: precision, }, { name: 'z', min: 12, max: 15, precision: precision, }, ] const {inputs, output} = searchForFormulaSolution(variables, formula, [], answerPrecision) expect(inputs).toHaveLength(3) const x = Number(inputs.find(i => i.name === 'x').value) const z = Number(inputs.find(i => i.name === 'z').value) expect(z - x + (z - x)).toBe(Number(output)) }) it('generates a correct solution with if function', () => { const formula = 'if(x, 12, 13) + if(11, 7, 21)' const precision = 0 const variables = [ { name: 'x', min: 0, max: 0, precision: precision, }, ] const {inputs, output} = searchForFormulaSolution(variables, formula, [], answerPrecision) expect(inputs).toHaveLength(1) expect(20).toBe(Number(output)) }) it('generates a correct solution with log functions', () => { const formula = 'log(x) + ln(y) + log10(z) + log(x,y)' const precision = 4 const variables = [ { name: 'x', min: 2, max: 12, precision: precision, }, { name: 'y', min: 8, max: 20, precision: precision, }, { name: 'z', min: 25, max: 87, precision: precision, }, ] const {inputs, output} = searchForFormulaSolution(variables, formula, [], 16) expect(inputs).toHaveLength(3) const x = Number(inputs.find(i => i.name === 'x').value) const y = Number(inputs.find(i => i.name === 'y').value) const z = Number(inputs.find(i => i.name === 'z').value) // Rounding here just to prevent test flakiness expect( round(Math.log10(x) + Math.log(y) + Math.log10(z) + Math.log(x) / Math.log(y), 12), ).toBe(round(Number(output), 12)) }) it('generates a correct solution with e and pi as constants or functions', () => { // e, pi, e(), pi(), E, and PI should all work // this matches legacy canvas quizzes behavior const formula = 'e() + pi() + E + PI + e + pi + 0 * x' const precision = 4 const variables = [ { name: 'x', min: 12, max: 12, precision: precision, }, ] const {output} = searchForFormulaSolution(variables, formula, [], 8) expect(round(3 * (Math.E + Math.PI), 8)).toBe(Number(output)) }) it('generates a correct solution with e and pi when custom variables are defined', () => { // when a user defines their own variable `e`, that should take precedence over the constant // but e() should still be the constant 2.718 const formula = 'e() + pi() + e + pi' const precision = 4 const variables = [ { name: 'e', min: 12, max: 12, precision: precision, }, { name: 'pi', min: 17, max: 17, precision: precision, }, ] const {output} = searchForFormulaSolution(variables, formula, [], 15) expect(round(Math.E + Math.PI + 12 + 17, 15)).toBe(Number(output)) }) describe('with standard numeric variables', () => { const formula = 'x + y' const variables = [ { name: 'x', min: 1234, max: 1234, precision: 0, }, { name: 'y', min: 2345, max: 2345, precision: 0, }, ] describe('generates a solution in scientific notation if searchForFormulaSolution is true', () => { it('generates a string', () => { const {output} = searchForFormulaSolution(variables, formula, [], 2, true) expect(typeof output).toBe('string') }) it('should be a value in scientific notation', () => { const {output} = searchForFormulaSolution(variables, formula, [], 2, true) expect(output).toBe('3.58*10^3') }) it('generates a valid representation of a Decimal value', () => { const {output} = searchForFormulaSolution(variables, formula, [], 2, true) const [mantissa, exponent] = parseScientificNotation(output) expect(`${mantissa}e${exponent}`).toBe('3.58e3') const scientificNumber = new Decimal(`${mantissa}e${exponent}`) expect(Decimal.isDecimal(scientificNumber)).toBe(true) }) }) }) // Skip: mathjs cannot evaluate scientific notation inputs passed as Decimal objects // under jsdom. All evaluation fallback paths fail → null. Needs a production code fix. describe.skip('with variables in scientific notation', () => { const formula = 'x + y' const variables = [ { name: 'x', min: '5.0*10^-3', max: '1.0*10^-2', precision: 1, }, { name: 'y', min: '-0.010', max: '0.010', precision: 3, }, ] it('generates a correct solution in decimal notation', () => { const precision = 4 const {inputs, output} = searchForFormulaSolution(variables, formula, [], precision, false) const x = scientificNotationToNumber(inputs.find(i => i.name === 'x').value) const y = Number(inputs.find(i => i.name === 'y').value) expect(Number(output).toFixed(precision)).toBe(output) expect(Number(output)).toBe(round(x + y, precision)) }) it('generates a correct solution in scientific notation', () => { const precision = 4 const {inputs, output} = searchForFormulaSolution(variables, formula, [], precision, true) const x = scientificNotationToNumber(inputs.find(i => i.name === 'x').value) const y = Number(inputs.find(i => i.name === 'y').value) expect(isScientificNotation(output)).toBe(true) const [mantissa] = parseScientificNotation(output) expect(Number(mantissa).toFixed(precision)).toBe(mantissa) // Rounding here just to prevent test flakiness expect(scientificNotationToNumber(output)).toBe(round(x + y, 12)) }) }) // Skip: Very large numbers (10^500) overflow to Infinity as JS Numbers under jsdom. // The nested it() blocks in the original Karma test were silently ignored — now that // they are flattened, the assertions actually run and fail. Needs a production code // fix with BigNumber fallback. describe.skip('with very large numbers in scientific notation', () => { const formula = 'x + y' const variables = [ { name: 'x', min: '5*10^500', max: '5*10^500', precision: 0, }, { name: 'y', min: '2*10^500', max: '2*10^500', precision: 0, }, ] describe('generates a solution in scientific notation searchForFormulaSolution is true', () => { it('generates a string', () => { const {output} = searchForFormulaSolution(variables, formula, [], 0, true) expect(typeof output).toBe('string') }) it('should be an astronomical value in scientific notation', () => { const {output} = searchForFormulaSolution(variables, formula, [], 0, true) expect(output).toBe('7*10^500') }) it('generates a valid representation of a Decimal value', () => { const {output} = searchForFormulaSolution(variables, formula, [], 0, true) const [mantissa, exponent] = parseScientificNotation(output) expect(`${mantissa}e${exponent}`).toBe('7e500') const scientificNumber = new Decimal(`${mantissa}e${exponent}`) expect(Decimal.isDecimal(scientificNumber)).toBe(true) }) }) it('generates a solution in decimal notation when scientificNotation is false', () => { const {output} = searchForFormulaSolution(variables, formula, [], 0, false) // 7 followed by 500 zeros expect(output).toHaveLength(501) expect(output).toContain('7') expect(output.match(/0/g)).toHaveLength(500) // not in scientific notation expect(output).not.toContain('*') expect(output).not.toContain('^') }) }) it('does not generate neg zeros', () => { const formula = 'rad_to_deg(deg_to_rad(x))' const variables = [ { name: 'x', min: -1, max: 1, precision: 0, }, ] // based on tests, -0 almost surely happens within 100 times // with params (min: -1, max: 1, precision: 0) for (let i = 0; i < 100; i++) { const {inputs} = searchForFormulaSolution(variables, formula, [], answerPrecision) expect(inputs).toHaveLength(1) expect(inputs[0].value).not.toBe('-0') } }) }) })