@instructure/quiz-interactions
Version:
A React UI component Library for quiz interaction types.
325 lines (267 loc) • 9.54 kB
JavaScript
import {vi} from 'vitest'
import React from 'react'
import {ApplyLocale} from '@instructure/ui-i18n'
import NumericTake from '../index'
import {fireEvent, render, screen, waitFor} from '@testing-library/react'
import runAxe from '@instructure/ui-axe-check'
import userEvent from '@testing-library/user-event'
describe('Numeric Take', () => {
const props = {
itemBody: '',
userResponse: {
value: '18',
},
}
numericTakeTests(NumericTake, props)
})
export function numericTakeTests(Component, props) {
let handleResponseUpdateStub
beforeEach(() => {
handleResponseUpdateStub = vi.fn()
})
it('renders only one input', () => {
render(<Component handleResponseUpdate={handleResponseUpdateStub} {...props} />)
const inputs = screen.getAllByRole('spinbutton')
expect(inputs.length).toBe(1)
})
// To ensure that the component doesn't use a numeric keypad on mobile Chrome
it('does not use a numeric input', () => {
render(<Component handleResponseUpdate={handleResponseUpdateStub} {...props} />)
const input = screen.getByRole('spinbutton')
expect(input.getAttribute('inputmode')).not.toBe('numeric')
})
it('passes value to NumberInput on mount', () => {
render(<Component handleResponseUpdate={handleResponseUpdateStub} {...props} />)
const input = screen.getByRole('spinbutton')
expect(input.value).toBe(props.userResponse.value)
})
it('does invoke response update callback on change', async () => {
render(<Component handleResponseUpdate={handleResponseUpdateStub} {...props} />)
const input = screen.getByRole('spinbutton')
await userEvent.clear(input)
await userEvent.type(input, '4')
await userEvent.tab()
expect(handleResponseUpdateStub).toHaveBeenCalled()
})
it('invokes response update callback with value on change with valid input', async () => {
render(<Component handleResponseUpdate={handleResponseUpdateStub} {...props} />)
const input = screen.getByRole('spinbutton')
fireEvent.change(input, {target: {value: '4'}})
fireEvent.blur(input)
expect(handleResponseUpdateStub).toHaveBeenCalledWith('4', null, false)
})
it('invokes response update callback with raw value on change with invalid input', async () => {
render(<Component handleResponseUpdate={handleResponseUpdateStub} {...props} />)
const input = screen.getByRole('spinbutton')
fireEvent.change(input, {target: {value: 'lolwut'}})
fireEvent.blur(input)
expect(handleResponseUpdateStub).toHaveBeenCalledWith('lolwut', null, false)
})
it('does not invoke response update callback on blur with no change', async () => {
render(<Component handleResponseUpdate={handleResponseUpdateStub} {...props} />)
const input = screen.getByRole('spinbutton')
await userEvent.click(input)
await userEvent.tab()
expect(handleResponseUpdateStub).not.toHaveBeenCalled()
})
it('normalizes numeric responses', async () => {
render(
<ApplyLocale locale="fr">
<Component handleResponseUpdate={handleResponseUpdateStub} {...props} />
</ApplyLocale>,
)
const input = screen.getByRole('spinbutton')
fireEvent.change(input, {target: {value: '1,23'}})
fireEvent.blur(input)
expect(handleResponseUpdateStub).toHaveBeenCalledWith('1.23', null, false)
})
it('handles input in scientific notation', async () => {
render(
<ApplyLocale locale="fr">
<Component handleResponseUpdate={handleResponseUpdateStub} {...props} />
</ApplyLocale>,
)
const input = screen.getByRole('spinbutton')
fireEvent.change(input, {target: {value: '3.5*10^-2'}})
fireEvent.blur(input)
expect(handleResponseUpdateStub).toHaveBeenCalledWith('3.5*10^-2', null, false)
})
describe('a11y tests', () => {
it('should meet a11y standards', async () => {
const {container} = render(
<Component handleResponseUpdate={handleResponseUpdateStub} {...props} />,
)
expect(
await runAxe(container, {
ignores: [
'label',
'region',
'scrollable-region-focusable', // we conditionally make rceRenderer focusable when we detect content overflow
],
}),
).toBe(true)
})
})
}
// Non-shared tests
describe('invalid input tests', () => {
it('should alert if starting input is not a number', async () => {
render(
<NumericTake
displayValidationWarning={true}
handleResponseUpdate={Function.prototype}
itemBody=""
userResponse={{value: '53b'}}
/>,
)
expect(
screen.getByText(/answer must be a number. remove any symbols or units./i),
).toBeInTheDocument()
})
it('should clear alert if user types in valid input', async () => {
render(
<NumericTake
displayValidationWarning={true}
handleResponseUpdate={Function.prototype}
itemBody=""
userResponse={{value: '53b'}}
/>,
)
const input = screen.getByRole('spinbutton')
await userEvent.clear(input)
await userEvent.type(input, '123')
await userEvent.tab()
expect(screen.queryByText('Answer must be a number. Remove any symbols or units.')).toBeNull()
})
it('should alert if user types in invalid input', async () => {
render(
<NumericTake
displayValidationWarning={true}
handleResponseUpdate={Function.prototype}
itemBody=""
userResponse={{value: ''}}
/>,
)
const input = screen.getByRole('spinbutton')
await userEvent.clear(input)
await userEvent.type(input, '123b')
await userEvent.tab()
await waitFor(() => {
expect(
screen.getByText('Answer must be a number. Remove any symbols or units.'),
).toBeInTheDocument()
})
})
it('should not alert if user types in valid input', async () => {
render(
<NumericTake
displayValidationWarning={true}
handleResponseUpdate={Function.prototype}
itemBody=""
userResponse={{value: ''}}
/>,
)
const input = screen.getByRole('spinbutton')
await userEvent.clear(input)
await userEvent.type(input, '123')
await userEvent.tab()
expect(screen.queryByText('Answer must be a number. Remove any symbols or units.')).toBeNull()
})
it('should not alert if user types in scientific notation', async () => {
render(
<NumericTake
displayValidationWarning={true}
handleResponseUpdate={Function.prototype}
itemBody=""
userResponse={{value: ''}}
/>,
)
const input = screen.getByRole('spinbutton')
await userEvent.clear(input)
await userEvent.type(input, '1.23 * 10 ^ 4')
await userEvent.tab()
expect(screen.queryByText('Answer must be a number. Remove any symbols or units.')).toBeNull()
})
})
describe('valid input tests', () => {
let handleResponseUpdateStub
beforeEach(() => {
handleResponseUpdateStub = vi.fn()
})
it('should convert english 1.9 to french 1,9', () => {
render(
<ApplyLocale locale="fr">
<NumericTake
handleResponseUpdate={handleResponseUpdateStub}
itemBody=""
userResponse={{value: '1.9'}}
/>
</ApplyLocale>,
)
const input = screen.getByRole('spinbutton')
expect(input.value).toBe('1,9')
})
it('should convert english 1000.9 to french 1 000,9', async () => {
render(
<ApplyLocale locale="fr">
<NumericTake
handleResponseUpdate={handleResponseUpdateStub}
itemBody=""
userResponse={{value: '1000.9'}}
/>
</ApplyLocale>,
)
const input = screen.getByRole('spinbutton')
fireEvent.change(input, {target: {value: '1000.9'}})
fireEvent.blur(input)
await waitFor(() => {
expect(input.value).toBe('1\u202f000,9')
})
})
})
describe('formatting alerts', () => {
let handleResponseUpdateStub
beforeEach(() => {
handleResponseUpdateStub = vi.fn()
})
it('should render an alert if the user input uses a different decimal separator than the one set in the locale', async () => {
render(
<ApplyLocale locale="fr">
<NumericTake
displayValidationWarning={true}
handleResponseUpdate={handleResponseUpdateStub}
itemBody=""
userResponse={{value: '1.9'}}
/>
</ApplyLocale>,
)
const input = screen.getByRole('spinbutton')
// The expected value is '1,9' as per the fr locale, so the alert should show up
userEvent.clear(input)
userEvent.type(input, '1.9')
userEvent.tab()
await waitFor(() => {
expect(
screen.getByText(/decimal separator auto-adjusted. verify your entry!/i),
).toBeInTheDocument()
})
})
it('should not render an alert if only the thousand separator is different than the one set in the locale', async () => {
render(
<ApplyLocale locale="fr">
<NumericTake
displayValidationWarning={true}
handleResponseUpdate={handleResponseUpdateStub}
itemBody=""
userResponse={{value: '1000.9'}}
/>
</ApplyLocale>,
)
const input = screen.getByRole('spinbutton')
// The expected value is '1 000,9' as per the fr locale, so the alert should not show up
userEvent.clear(input)
userEvent.type(input, '1000,9')
userEvent.tab()
expect(screen.queryByText(/decimal separator auto-adjusted. verify your entry!/i)).toBeNull()
})
})