accessible-autocomplete
Version:
An autocomplete component, built to be accessible.
810 lines (688 loc) • 34.5 kB
JavaScript
/* global after, describe, before, beforeEach, it */
import { expect } from 'chai'
import { createElement, render } from 'preact' /** @jsx createElement */
import Autocomplete from '../../src/autocomplete'
import Status from '../../src/status'
function suggest (query, syncResults) {
const results = [
'France',
'Germany',
'United Kingdom'
]
syncResults(query
? results.filter(function (result) {
return result.toLowerCase().indexOf(query.toLowerCase()) !== -1
})
: []
)
}
describe('Autocomplete', () => {
describe('rendering', () => {
let scratch
before(() => {
scratch = document.createElement('div');
(document.body || document.documentElement).appendChild(scratch)
})
beforeEach(() => {
scratch.innerHTML = ''
})
after(() => {
scratch.parentNode.removeChild(scratch)
scratch = null
})
describe('basic usage', () => {
it('renders an input', () => {
render(<Autocomplete />, scratch)
expect(scratch.innerHTML).to.contain('input')
expect(scratch.innerHTML).to.contain('class="autocomplete__input')
expect(scratch.innerHTML).to.contain('class="autocomplete__menu')
expect(scratch.innerHTML).to.contain('name="input-autocomplete"')
})
it('renders an input with a required attribute', () => {
render(<Autocomplete required />, scratch)
expect(scratch.innerHTML).to.contain('required')
})
it('renders an input without a required attribute', () => {
render(<Autocomplete required={false} />, scratch)
expect(scratch.innerHTML).to.not.contain('required')
})
it('renders an input with a name attribute', () => {
render(<Autocomplete name='bob' />, scratch)
expect(scratch.innerHTML).to.contain('name="bob"')
})
it('renders an input with a custom CSS namespace', () => {
render(<Autocomplete cssNamespace='bob' />, scratch)
expect(scratch.innerHTML).to.contain('class="bob__input')
expect(scratch.innerHTML).to.contain('class="bob__menu')
})
it('renders with an aria-expanded attribute', () => {
render(<Autocomplete required />, scratch)
const wrapperElement = scratch.getElementsByClassName('autocomplete__wrapper')[0]
const inputElement = wrapperElement.getElementsByTagName('input')[0]
expect(inputElement.getAttribute('aria-expanded')).to.equal('false')
})
it('renders with an aria-describedby attribute', () => {
render(<Autocomplete id='autocomplete-default' />, scratch)
const wrapperElement = scratch.getElementsByClassName('autocomplete__wrapper')[0]
const inputElement = wrapperElement.getElementsByTagName('input')[0]
expect(inputElement.getAttribute('aria-describedby')).to.equal('autocomplete-default__assistiveHint')
})
describe('renders with an aria-autocomplete attribute', () => {
it('of value "list", when autoselect is not enabled', () => {
render(<Autocomplete required />, scratch)
const wrapperElement = scratch.getElementsByClassName('autocomplete__wrapper')[0]
const inputElement = wrapperElement.getElementsByTagName('input')[0]
expect(inputElement.getAttribute('aria-autocomplete')).to.equal('list')
})
it('of value "both", when autoselect is enabled', () => {
render(<Autocomplete required autoselect />, scratch)
const wrapperElement = scratch.getElementsByClassName('autocomplete__wrapper')[0]
const inputElement = wrapperElement.getElementsByTagName('input')[0]
expect(inputElement.getAttribute('aria-autocomplete')).to.equal('both')
})
})
describe('menuAttributes', () => {
it('renders with extra attributes on the menu', () => {
render(<Autocomplete menuAttributes={{ 'data-test': 'test' }} id='autocomplete-default' />, scratch)
const wrapperElement = scratch.getElementsByClassName('autocomplete__wrapper')[0]
const dropdownElement = wrapperElement.getElementsByTagName('ul')[0]
expect(dropdownElement.getAttribute('data-test')).to.equal('test')
})
describe('attributes computed by the component', () => {
it('does not override attributes computed by the component', () => {
const menuAttributes = {
id: 'custom-id',
role: 'custom-role'
}
render(<Autocomplete menuAttributes={menuAttributes} id='autocomplete-default' />, scratch)
// Check that the computed values are the ones expected in the HTML
const menuElement = scratch.getElementsByClassName('autocomplete__menu')[0]
expect(menuElement.id).to.equal('autocomplete-default__listbox', 'HTML id')
expect(menuElement.role).to.equal('listbox', 'HTML role')
// Check that in protecting the menu, we don't affect the object passed as option
expect(menuAttributes.id).to.equal('custom-id', 'options id')
expect(menuAttributes.role).to.equal('custom-role', 'options role')
})
})
it('adds `className` to the computed value of the `class` attribute', () => {
const menuAttributes = {
className: 'custom-className'
}
render(<Autocomplete menuAttributes={menuAttributes} id='autocomplete-default' />, scratch)
// Check that the computed values are the ones expected in the HTML
const menuElement = scratch.getElementsByClassName('autocomplete__menu')[0]
expect(menuElement.getAttribute('class')).to.equal('autocomplete__menu autocomplete__menu--inline autocomplete__menu--hidden custom-className')
// Check that in protecting the menu, we don't affect the object passed as option
expect(menuAttributes.className).to.equal('custom-className')
})
// Align with Preact's behaviour where `class` takes precedence
it('adds `class` to the computed value of the `class` attribute, ignoring `className` if present', () => {
const menuAttributes = {
className: 'custom-className',
class: 'custom-class'
}
render(<Autocomplete menuAttributes={menuAttributes} id='autocomplete-default' />, scratch)
// Check that the computed values are the ones expected in the HTML
const menuElement = scratch.getElementsByClassName('autocomplete__menu')[0]
expect(menuElement.getAttribute('class')).to.equal('autocomplete__menu autocomplete__menu--inline autocomplete__menu--hidden custom-class')
// Check that in protecting the menu, we don't affect the object passed as option
expect(menuAttributes.className).to.equal('custom-className')
expect(menuAttributes.class).to.equal('custom-class')
})
it('adds `aria-labelledby` by default, based on the ID', () => {
render(<Autocomplete id='autocomplete-default' />, scratch)
const wrapperElement = scratch.getElementsByClassName('autocomplete__wrapper')[0]
const dropdownElement = wrapperElement.getElementsByTagName('ul')[0]
expect(dropdownElement.getAttribute('aria-labelledby')).to.equal('autocomplete-default')
})
it('overrides `aria-labelledby` if passed in menuAttributes', () => {
render(<Autocomplete menuAttributes={{ 'aria-labelledby': 'test' }} id='autocomplete-default' />, scratch)
const wrapperElement = scratch.getElementsByClassName('autocomplete__wrapper')[0]
const dropdownElement = wrapperElement.getElementsByTagName('ul')[0]
expect(dropdownElement.getAttribute('aria-labelledby')).to.equal('test')
})
})
it('renders with extra class on the input', () => {
render(<Autocomplete inputClasses='custom-class' id='autocomplete-default' />, scratch)
const inputElement = scratch.getElementsByClassName('autocomplete__input')[0]
expect(inputElement.getAttribute('class')).to.contain(' custom-class')
})
it('renders with extra class on the menu', () => {
render(<Autocomplete menuClasses='custom-class' id='autocomplete-default' />, scratch)
const menuElement = scratch.getElementsByClassName('autocomplete__menu')[0]
expect(menuElement.getAttribute('class')).to.contain('custom-class')
})
it('renders with the correct roles', () => {
render(<Autocomplete required />, scratch)
const wrapperElement = scratch.getElementsByClassName('autocomplete__wrapper')[0]
const inputElement = wrapperElement.getElementsByTagName('input')[0]
const dropdownElement = wrapperElement.getElementsByTagName('ul')[0]
expect(inputElement.getAttribute('role')).to.equal('combobox', 'input should have combobox role')
expect(dropdownElement.getAttribute('role')).to.equal('listbox', 'menu should have listbox role')
})
})
})
describe('behaviour', () => {
let autocomplete, autoselectAutocomplete, onConfirmAutocomplete, onConfirmTriggered,
autoselectOnSelectAutocomplete, confirmOnBlurAutocomplete
beforeEach(() => {
autocomplete = new Autocomplete({
...Autocomplete.defaultProps,
id: 'test',
source: suggest
})
autoselectAutocomplete = new Autocomplete({
...Autocomplete.defaultProps,
autoselect: true,
id: 'test2',
source: suggest
})
onConfirmTriggered = false
onConfirmAutocomplete = new Autocomplete({
...Autocomplete.defaultProps,
id: 'test3',
onConfirm: () => { onConfirmTriggered = true },
source: suggest
})
autoselectOnSelectAutocomplete = new Autocomplete({
...Autocomplete.defaultProps,
autoselect: true,
id: 'test4',
onConfirm: () => { onConfirmTriggered = true },
source: suggest
})
confirmOnBlurAutocomplete = new Autocomplete({
...Autocomplete.defaultProps,
id: 'test5',
onConfirm: () => { onConfirmTriggered = true },
confirmOnBlur: false,
source: suggest
})
})
describe('typing', () => {
it('searches for options', () => {
autocomplete.handleInputChange({ target: { value: 'f' } })
expect(autocomplete.state.menuOpen).to.equal(true)
expect(autocomplete.state.options).to.contain('France')
})
it('hides menu when no options are available', () => {
autocomplete.handleInputChange({ target: { value: 'aa' } })
expect(autocomplete.state.menuOpen).to.equal(false)
expect(autocomplete.state.options.length).to.equal(0)
})
it('hides menu when query becomes empty', () => {
autocomplete.setState({ query: 'f', options: ['France'], menuOpen: true })
autocomplete.handleInputChange({ target: { value: '' } })
expect(autocomplete.state.menuOpen).to.equal(false)
})
it('searches with the new term when query length changes', () => {
autocomplete.setState({ query: 'fr', options: ['France'] })
autocomplete.handleInputChange({ target: { value: 'fb' } })
expect(autocomplete.state.options.length).to.equal(0)
})
it('removes the aria-describedby attribute when query is non empty', () => {
expect(autocomplete.state.ariaHint).to.equal(true)
autocomplete.handleInputChange({ target: { value: 'a' } })
expect(autocomplete.state.ariaHint).to.equal(false)
autocomplete.handleInputChange({ target: { value: '' } })
expect(autocomplete.state.ariaHint).to.equal(true)
})
describe('with minLength', () => {
beforeEach(() => {
autocomplete = new Autocomplete({
...Autocomplete.defaultProps,
id: 'test',
source: suggest,
minLength: 2
})
})
it('doesn\'t search when under limit', () => {
autocomplete.handleInputChange({ target: { value: 'f' } })
expect(autocomplete.state.menuOpen).to.equal(false)
expect(autocomplete.state.options.length).to.equal(0)
})
it('does search when over limit', () => {
autocomplete.handleInputChange({ target: { value: 'fra' } })
expect(autocomplete.state.menuOpen).to.equal(true)
expect(autocomplete.state.options).to.contain('France')
})
it('hides results when going under limit', () => {
autocomplete.setState({ menuOpen: true, query: 'fr', options: ['France'] })
autocomplete.handleInputChange({ target: { value: 'f' } })
expect(autocomplete.state.menuOpen).to.equal(false)
expect(autocomplete.state.options.length).to.equal(0)
})
})
})
describe('focusing input', () => {
describe('when no query is present', () => {
it('does not display menu', () => {
autocomplete.setState({ query: '' })
autocomplete.handleInputFocus()
expect(autocomplete.state.menuOpen).to.equal(false)
expect(autocomplete.state.focused).to.equal(-1)
})
})
describe('when a non-matched query is present (no matching options are present)', () => {
it('does not display menu', () => {
autocomplete.setState({ query: 'f' })
autocomplete.handleInputFocus()
expect(autocomplete.state.menuOpen).to.equal(false)
expect(autocomplete.state.focused).to.equal(-1)
})
})
describe('when a matched query is present (matching options exist)', () => {
describe('and no user choice has yet been made', () => {
it('displays menu', () => {
autocomplete.setState({
menuOpen: false,
options: ['France'],
query: 'fr',
focused: null,
selected: null,
validChoiceMade: false
})
autocomplete.handleInputFocus()
expect(autocomplete.state.focused).to.equal(-1)
expect(autocomplete.state.menuOpen).to.equal(true)
expect(autocomplete.state.selected).to.equal(-1)
})
})
describe('and a user choice HAS been made', () => {
it('does not display menu', () => {
autocomplete.setState({
menuOpen: false,
options: ['France'],
query: 'fr',
focused: null,
selected: null,
validChoiceMade: true
})
autocomplete.handleInputFocus()
expect(autocomplete.state.focused).to.equal(-1)
expect(autocomplete.state.menuOpen).to.equal(false)
})
})
})
describe('with option selected', () => {
it('leaves menu open, does not change query', () => {
autocomplete.setState({ menuOpen: true, options: ['France'], query: 'fr', focused: 0, selected: 0 })
autocomplete.handleInputFocus()
expect(autocomplete.state.focused).to.equal(-1)
expect(autocomplete.state.menuOpen).to.equal(true)
expect(autocomplete.state.query).to.equal('fr')
})
})
describe('with defaultValue', () => {
beforeEach(() => {
autocomplete = new Autocomplete({
...Autocomplete.defaultProps,
defaultValue: 'France',
id: 'test',
source: suggest
})
})
it('is prefilled', () => {
expect(autocomplete.state.options.length).to.equal(1)
expect(autocomplete.state.options[0]).to.equal('France')
expect(autocomplete.state.query).to.equal('France')
})
})
})
describe('blurring input', () => {
it('unfocuses component', () => {
autocomplete.setState({ menuOpen: true, options: ['France'], query: 'fr', focused: -1, selected: -1 })
autocomplete.handleInputBlur({ relatedTarget: null })
expect(autocomplete.state.focused).to.equal(null)
expect(autocomplete.state.menuOpen).to.equal(false)
expect(autocomplete.state.query).to.equal('fr')
})
describe('with autoselect and onConfirm', () => {
it('unfocuses component, updates query, triggers onConfirm', () => {
autoselectOnSelectAutocomplete.setState({ menuOpen: true, options: ['France'], query: 'fr', focused: -1, selected: 0 })
autoselectOnSelectAutocomplete.handleInputBlur({ target: 'mock', relatedTarget: 'relatedMock' }, 0)
expect(autoselectOnSelectAutocomplete.state.focused).to.equal(null)
expect(autoselectOnSelectAutocomplete.state.menuOpen).to.equal(false)
expect(autoselectOnSelectAutocomplete.state.query).to.equal('France')
expect(onConfirmTriggered).to.equal(true)
})
})
describe('with confirmOnBlur false', () => {
it('unfocuses component, does not touch query, does not trigger onConfirm', () => {
confirmOnBlurAutocomplete.setState({ menuOpen: true, options: ['France'], query: 'fr', focused: -1, selected: 0 })
confirmOnBlurAutocomplete.handleInputBlur({ target: 'mock', relatedTarget: 'relatedMock' }, 0)
expect(confirmOnBlurAutocomplete.state.focused).to.equal(null)
expect(confirmOnBlurAutocomplete.state.menuOpen).to.equal(false)
expect(confirmOnBlurAutocomplete.state.query).to.equal('fr')
expect(onConfirmTriggered).to.equal(false)
})
})
})
describe('focusing option', () => {
it('sets the option as focused', () => {
autocomplete.setState({ options: ['France'] })
autocomplete.handleOptionFocus(0)
expect(autocomplete.state.focused).to.equal(0)
})
})
describe('focusing out option', () => {
describe('with input selected', () => {
it('unfocuses component, does not change query', () => {
autocomplete.setState({ menuOpen: true, options: ['France'], query: 'fr', focused: 0, selected: -1 })
autocomplete.handleOptionBlur({ target: 'mock', relatedTarget: 'relatedMock' }, 0)
expect(autocomplete.state.focused).to.equal(null)
expect(autocomplete.state.menuOpen).to.equal(false)
expect(autocomplete.state.query).to.equal('fr')
})
})
describe('with option selected', () => {
describe('with confirmOnBlur true', () => {
it('unfocuses component, updates query', () => {
autocomplete.setState({ menuOpen: true, options: ['France'], query: 'fr', focused: 0, selected: 0 })
autocomplete.handleOptionBlur({ target: 'mock', relatedTarget: 'relatedMock' }, 0)
expect(autocomplete.state.focused).to.equal(null)
expect(autocomplete.state.menuOpen).to.equal(false)
expect(autocomplete.state.query).to.equal('France')
})
})
describe('with confirmOnBlur false', () => {
it('unfocuses component, does not update query', () => {
confirmOnBlurAutocomplete.setState({ menuOpen: true, options: ['France'], query: 'fr', focused: 0, selected: 0 })
confirmOnBlurAutocomplete.handleOptionBlur({ target: 'mock', relatedTarget: 'relatedMock' }, 0)
expect(confirmOnBlurAutocomplete.state.focused).to.equal(null)
expect(confirmOnBlurAutocomplete.state.menuOpen).to.equal(false)
expect(confirmOnBlurAutocomplete.state.query).to.equal('fr')
})
})
})
})
describe('hovering option', () => {
it('sets the option as hovered, does not change focused, does not change selected', () => {
autocomplete.setState({ options: ['France'], hovered: null, focused: -1, selected: -1 })
autocomplete.handleOptionMouseEnter({}, 0)
expect(autocomplete.state.hovered).to.equal(0)
expect(autocomplete.state.focused).to.equal(-1)
expect(autocomplete.state.selected).to.equal(-1)
})
})
describe('hovering out option', () => {
it('sets focus back on selected, sets hovered to null', () => {
autocomplete.setState({ options: ['France'], hovered: 0, focused: -1, selected: -1 })
autocomplete.handleListMouseLeave({ toElement: 'mock' }, 0)
expect(autocomplete.state.hovered).to.equal(null)
expect(autocomplete.state.focused).to.equal(-1)
expect(autocomplete.state.selected).to.equal(-1)
})
})
describe('up key', () => {
it('focuses previous element', () => {
autocomplete.setState({ menuOpen: true, options: ['France'], focused: 0 })
autocomplete.handleKeyDown({ preventDefault: () => {}, keyCode: 38 })
expect(autocomplete.state.focused).to.equal(-1)
})
})
describe('down key', () => {
describe('0 options available', () => {
it('does nothing', () => {
autocomplete.setState({ menuOpen: false, options: [], focused: -1 })
const stateBefore = autocomplete.state
autocomplete.handleKeyDown({ preventDefault: () => {}, keyCode: 40 })
expect(autocomplete.state).to.equal(stateBefore)
})
})
describe('1 option available', () => {
it('focuses next element', () => {
autocomplete.setState({ menuOpen: true, options: ['France'], focused: -1, selected: -1 })
autocomplete.handleKeyDown({ preventDefault: () => {}, keyCode: 40 })
expect(autocomplete.state.focused).to.equal(0)
expect(autocomplete.state.selected).to.equal(0)
})
})
describe('2 or more option available', () => {
it('focuses next element', () => {
autocomplete.setState({ menuOpen: true, options: ['France', 'Germany'], focused: 0, selected: 0 })
autocomplete.handleKeyDown({ preventDefault: () => {}, keyCode: 40 })
expect(autocomplete.state.focused).to.equal(1)
expect(autocomplete.state.selected).to.equal(1)
})
})
describe('autoselect', () => {
describe('0 options available', () => {
it('does nothing', () => {
autoselectAutocomplete.setState({ menuOpen: false, options: [], focused: -1, selected: -1 })
const stateBefore = autoselectAutocomplete.state
autoselectAutocomplete.handleKeyDown({ preventDefault: () => {}, keyCode: 40 })
expect(autoselectAutocomplete.state).to.equal(stateBefore)
})
})
describe('1 option available', () => {
it('does nothing', () => {
autoselectAutocomplete.setState({ menuOpen: true, options: ['France'], focused: -1, selected: 0 })
const stateBefore = autoselectAutocomplete.state
autoselectAutocomplete.handleKeyDown({ preventDefault: () => {}, keyCode: 40 })
expect(autoselectAutocomplete.state).to.equal(stateBefore)
})
})
describe('2 or more option available', () => {
it('on input, focuses second element', () => {
autoselectAutocomplete.setState({ menuOpen: true, options: ['France', 'Germany'], focused: -1, selected: 0 })
autoselectAutocomplete.handleKeyDown({ preventDefault: () => {}, keyCode: 40 })
expect(autoselectAutocomplete.state.focused).to.equal(1)
expect(autoselectAutocomplete.state.selected).to.equal(1)
})
})
})
})
describe('escape key', () => {
it('unfocuses component', () => {
autocomplete.setState({ menuOpen: true, options: ['France'], focused: -1 })
autocomplete.handleKeyDown({ preventDefault: () => {}, keyCode: 27 })
expect(autocomplete.state.menuOpen).to.equal(false)
expect(autocomplete.state.focused).to.equal(null)
})
})
describe('enter key', () => {
describe('on an option', () => {
it('prevents default, closes the menu, sets the query, focuses the input, triggers onConfirm', () => {
let preventedDefault = false
onConfirmAutocomplete.setState({ menuOpen: true, options: ['France'], focused: 0, selected: 0 })
onConfirmAutocomplete.handleKeyDown({ preventDefault: () => { preventedDefault = true }, keyCode: 13 })
expect(onConfirmAutocomplete.state.menuOpen).to.equal(false)
expect(onConfirmAutocomplete.state.query).to.equal('France')
expect(onConfirmAutocomplete.state.focused).to.equal(-1)
expect(onConfirmAutocomplete.state.selected).to.equal(-1)
expect(preventedDefault).to.equal(true)
expect(onConfirmTriggered).to.equal(true)
})
})
describe('on the input', () => {
describe('with menu opened', () => {
it('prevents default, does nothing', () => {
let preventedDefault = false
autocomplete.setState({ menuOpen: true, options: [], query: 'asd', focused: -1, selected: -1 })
const stateBefore = autocomplete.state
autocomplete.handleKeyDown({ preventDefault: () => { preventedDefault = true }, keyCode: 13 })
expect(autocomplete.state).to.equal(stateBefore)
expect(preventedDefault).to.equal(true)
})
})
describe('with menu closed', () => {
it('bubbles, does not prevent default', () => {
let preventedDefault = false
autocomplete.setState({ menuOpen: false, options: ['France'], focused: -1, selected: -1 })
const stateBefore = autocomplete.state
autocomplete.handleKeyDown({ preventDefault: () => { preventedDefault = true }, keyCode: 13 })
expect(autocomplete.state).to.equal(stateBefore)
expect(preventedDefault).to.equal(false)
})
})
describe('autoselect', () => {
it('closes the menu, selects the first option, keeps input focused', () => {
autoselectAutocomplete.setState({ menuOpen: true, options: ['France'], focused: -1, selected: 0 })
autoselectAutocomplete.handleKeyDown({ preventDefault: () => {}, keyCode: 13 })
expect(autoselectAutocomplete.state.menuOpen).to.equal(false)
expect(autoselectAutocomplete.state.query).to.equal('France')
expect(autoselectAutocomplete.state.focused).to.equal(-1)
expect(autoselectAutocomplete.state.selected).to.equal(-1)
})
})
})
})
describe('space key', () => {
describe('on an option', () => {
it('prevents default, closes the menu, sets the query, focuses the input, triggers onConfirm', () => {
let preventedDefault = false
onConfirmAutocomplete.setState({ menuOpen: true, options: ['France'], focused: 0, selected: 0 })
onConfirmAutocomplete.handleKeyDown({ preventDefault: () => { preventedDefault = true }, keyCode: 32 })
expect(onConfirmAutocomplete.state.menuOpen).to.equal(false)
expect(onConfirmAutocomplete.state.query).to.equal('France')
expect(onConfirmAutocomplete.state.focused).to.equal(-1)
expect(onConfirmAutocomplete.state.selected).to.equal(-1)
expect(preventedDefault).to.equal(true)
expect(onConfirmTriggered).to.equal(true)
})
})
})
describe('an unrecognised key', () => {
it('does nothing', () => {
autocomplete.setState({ menuOpen: true, options: ['France'], focused: 0, selected: 0 })
autocomplete.elementReferences[-1] = 'input element'
autocomplete.handleKeyDown({ target: 'not the input element', keyCode: 4242 })
expect(autocomplete.state.focused).to.equal(0)
expect(autocomplete.state.selected).to.equal(0)
})
})
describe('derived state', () => {
it('initially assumes no valid choice on each new input', () => {
autocomplete.handleInputChange({ target: { value: 'F' } })
expect(autocomplete.state.validChoiceMade).to.equal(false)
})
describe('identifies that the user has made a valid choice', () => {
it('when an option is actively clicked', () => {
autocomplete.setState({ query: 'f', options: ['France'], validChoiceMade: false })
autocomplete.handleOptionClick({}, 0)
expect(autocomplete.state.validChoiceMade).to.equal(true)
})
it('when the input is blurred, autoselect is disabled, and the current query exactly matches an option', () => {
autocomplete.setState({ query: 'France', options: ['France'], validChoiceMade: false })
autocomplete.handleComponentBlur({})
expect(autocomplete.state.validChoiceMade).to.equal(true)
})
it('when in the same scenario, but the match differs only by case sensitivity', () => {
autocomplete.setState({ query: 'fraNCe', options: ['France'], validChoiceMade: false })
autocomplete.handleComponentBlur({})
expect(autocomplete.state.validChoiceMade).to.equal(true)
})
it('when the input is blurred, autoselect is enabled, and the current query results in at least one option', () => {
autoselectAutocomplete.setState({ options: ['France'], validChoiceMade: false })
autoselectAutocomplete.handleInputChange({ target: { value: 'France' } })
autoselectAutocomplete.handleComponentBlur({})
expect(autoselectAutocomplete.state.validChoiceMade).to.equal(true)
})
})
describe('identifies that the user has not made a valid choice', () => {
it('when the input is blurred, autoselect is disabled, and the current query does not match an option', () => {
autocomplete.setState({ query: 'Fracne', options: ['France'], validChoiceMade: false })
autocomplete.handleComponentBlur({})
expect(autocomplete.state.validChoiceMade).to.equal(false)
})
it('when the input is blurred, autoselect is enabled, but no options exist for the current query', () => {
autoselectAutocomplete.setState({ options: [], validChoiceMade: false })
autoselectAutocomplete.handleInputChange({ target: { value: 'gpvx' } })
autoselectAutocomplete.handleComponentBlur({})
expect(autoselectAutocomplete.state.validChoiceMade).to.equal(false)
})
})
describe('identifies that the valid choice situation has changed', () => {
it('when the user amends a previously matched query such that it no longer matches an option', () => {
autocomplete.setState({ query: 'France', options: ['France'], validChoiceMade: false })
autocomplete.handleComponentBlur({})
expect(autocomplete.state.validChoiceMade).to.equal(true)
autocomplete.handleInputChange({ target: { value: 'Francey' } })
autocomplete.handleComponentBlur({})
expect(autocomplete.state.validChoiceMade).to.equal(false)
autocomplete.handleInputChange({ target: { value: 'France' } })
autocomplete.handleComponentBlur({})
expect(autocomplete.state.validChoiceMade).to.equal(true)
autocomplete.handleInputChange({ target: { value: 'Franc' } })
autocomplete.handleComponentBlur({})
expect(autocomplete.state.validChoiceMade).to.equal(false)
})
})
})
})
})
describe('Status', () => {
describe('rendering', () => {
let scratch
before(() => {
scratch = document.createElement('div');
(document.body || document.documentElement).appendChild(scratch)
})
beforeEach(() => {
scratch.innerHTML = ''
})
after(() => {
scratch.parentNode.removeChild(scratch)
scratch = null
})
it('renders a pair of aria live regions', () => {
render(<Status />, scratch)
expect(scratch.innerHTML).to.contain('div')
const wrapperElement = scratch.getElementsByTagName('div')[0]
const ariaLiveA = wrapperElement.getElementsByTagName('div')[0]
const ariaLiveB = wrapperElement.getElementsByTagName('div')[1]
expect(ariaLiveA.getAttribute('role')).to.equal('status', 'first aria live region should be marked as role=status')
expect(ariaLiveA.getAttribute('aria-atomic')).to.equal('true', 'first aria live region should be marked as atomic')
expect(ariaLiveA.getAttribute('aria-live')).to.equal('polite', 'first aria live region should be marked as polite')
expect(ariaLiveB.getAttribute('role')).to.equal('status', 'second aria live region should be marked as role=status')
expect(ariaLiveB.getAttribute('aria-atomic')).to.equal('true', 'second aria live region should be marked as atomic')
expect(ariaLiveB.getAttribute('aria-live')).to.equal('polite', 'second aria live region should be marked as polite')
})
describe('behaviour', () => {
describe('silences aria live announcement', () => {
it('when a valid choice has been made and the input has focus', (done) => {
const status = new Status({
...Status.defaultProps,
validChoiceMade: true,
isInFocus: true
})
status.componentWillMount()
status.render()
setTimeout(() => {
expect(status.state.silenced).to.equal(true)
done()
}, 1500)
})
it('when the input no longer has focus', (done) => {
const status = new Status({
...Status.defaultProps,
validChoiceMade: false,
isInFocus: false
})
status.componentWillMount()
status.render()
setTimeout(() => {
expect(status.state.silenced).to.equal(true)
done()
}, 1500)
})
})
describe('does not silence aria live announcement', () => {
it('when a valid choice has not been made and the input has focus', (done) => {
const status = new Status({
...Status.defaultProps,
validChoiceMade: false,
isInFocus: true
})
status.componentWillMount()
status.render()
setTimeout(() => {
expect(status.state.silenced).to.equal(false)
done()
}, 1500)
})
})
})
})
})