UNPKG

react-typeahead

Version:

React-based typeahead and typeahead-tokenizer

709 lines (612 loc) 26.2 kB
var _ = require('lodash'); var assert = require('chai').assert; var sinon = require('sinon'); var React = require('react'); var ReactDOM = require('react-dom'); var Typeahead = require('../src/typeahead'); var TypeaheadOption = require('../src/typeahead/option'); var TypeaheadSelector = require('../src/typeahead/selector'); var Keyevent = require('../src/keyevent'); var TestUtils = require('react-addons-test-utils'); var createReactClass = require('create-react-class'); function simulateTextInput(component, value) { var node = component.refs.entry; node.value = value; TestUtils.Simulate.change(node); return TestUtils.scryRenderedComponentsWithType(component, TypeaheadOption); } var BEATLES = ['John', 'Paul', 'George', 'Ringo']; var BEATLES_COMPLEX = [ { firstName: 'John', lastName: 'Lennon', nameWithTitle: 'John Winston Ono Lennon MBE' }, { firstName: 'Paul', lastName: 'McCartney', nameWithTitle: 'Sir James Paul McCartney MBE' }, { firstName: 'George', lastName: 'Harrison', nameWithTitle: 'George Harrison MBE' }, { firstName: 'Ringo', lastName: 'Starr', nameWithTitle: 'Richard Starkey Jr. MBE' } ]; describe('Typeahead Component', function() { describe('sanity', function() { beforeEach(function() { this.component = TestUtils.renderIntoDocument(<Typeahead options={ BEATLES } />); }); it('should fuzzy search and render matching results', function() { // input value: num of expected results var testplan = { 'o': 3, 'pa': 1, 'Grg': 1, 'Ringo': 1, 'xxx': 0 }; _.each(testplan, function(expected, value) { var results = simulateTextInput(this.component, value); assert.equal(results.length, expected, 'Text input: ' + value); }, this); }); it('does not change the url hash when clicking on options', function() { var results = simulateTextInput(this.component, 'o'); var firstResult = results[0]; var anchor = TestUtils.findRenderedDOMComponentWithTag(firstResult, 'a'); var href = ReactDOM.findDOMNode(anchor).getAttribute('href'); assert.notEqual(href, '#'); }); describe('keyboard controls', function() { it('down arrow + return selects an option', function() { var results = simulateTextInput(this.component, 'o'); var secondItem = ReactDOM.findDOMNode(results[1]).innerText; var node = this.component.refs.entry; TestUtils.Simulate.keyDown(node, { keyCode: Keyevent.DOM_VK_DOWN }); TestUtils.Simulate.keyDown(node, { keyCode: Keyevent.DOM_VK_DOWN }); TestUtils.Simulate.keyDown(node, { keyCode: Keyevent.DOM_VK_RETURN }); assert.equal(node.value, secondItem); // Poor Ringo }); it('up arrow + return navigates and selects an option', function() { var results = simulateTextInput(this.component, 'o'); var firstItem = ReactDOM.findDOMNode(results[0]).innerText; var node = this.component.refs.entry; TestUtils.Simulate.keyDown(node, { keyCode: Keyevent.DOM_VK_DOWN }); TestUtils.Simulate.keyDown(node, { keyCode: Keyevent.DOM_VK_DOWN }); TestUtils.Simulate.keyDown(node, { keyCode: Keyevent.DOM_VK_UP }); TestUtils.Simulate.keyDown(node, { keyCode: Keyevent.DOM_VK_RETURN }); assert.equal(node.value, firstItem); }); it('escape clears selection', function() { var results = simulateTextInput(this.component, 'o'); var firstItem = ReactDOM.findDOMNode(results[0]); var node = this.component.refs.entry; TestUtils.Simulate.keyDown(node, { keyCode: Keyevent.DOM_VK_DOWN }); assert.ok(firstItem.classList.contains('hover')); TestUtils.Simulate.keyDown(node, { keyCode: Keyevent.DOM_VK_ESCAPE }); assert.notOk(firstItem.classList.contains('hover')); }); it('tab to choose first item', function() { var results = simulateTextInput(this.component, 'o'); var itemText = ReactDOM.findDOMNode(results[0]).innerText; var node = this.component.refs.entry; TestUtils.Simulate.keyDown(node, { keyCode: Keyevent.DOM_VK_TAB }); assert.equal(node.value, itemText); }); it('tab to selected current item', function() { var results = simulateTextInput(this.component, 'o'); var itemText = ReactDOM.findDOMNode(results[1]).innerText; var node = this.component.refs.entry; TestUtils.Simulate.keyDown(node, { keyCode: Keyevent.DOM_VK_DOWN }); TestUtils.Simulate.keyDown(node, { keyCode: Keyevent.DOM_VK_DOWN }); TestUtils.Simulate.keyDown(node, { keyCode: Keyevent.DOM_VK_TAB }); assert.equal(node.value, itemText); }); it('tab on no selection should not be undefined', function() { var results = simulateTextInput(this.component, 'oz'); assert(results.length == 0); var node = this.component.refs.entry; TestUtils.Simulate.keyDown(node, { keyCode: Keyevent.DOM_VK_TAB }); assert.equal("oz", node.value); }); it('should set hover', function() { var results = simulateTextInput(this.component, 'o'); var node = this.component.refs.entry; TestUtils.Simulate.keyDown(node, { keyCode: Keyevent.DOM_VK_DOWN }); TestUtils.Simulate.keyDown(node, { keyCode: Keyevent.DOM_VK_DOWN }); assert.equal(true, results[1].props.hover); }); }); describe('mouse controls', function() { // as of React 15.5.4 this does not work xit('mouse click selects an option (click event)', function() { var results = simulateTextInput(this.component, 'o'); var secondItem = ReactDOM.findDOMNode(results[1]); var secondItemValue = secondItem.innerText; var node = this.component.refs.entry; TestUtils.Simulate.click(secondItem); assert.equal(node.value, secondItemValue); }); // but this one works it('mouse click selects an option (mouseDown event)', function() { var results = simulateTextInput(this.component, 'o'); var secondItem = ReactDOM.findDOMNode(results[1]); var secondItemValue = secondItem.innerText; var node = this.component.refs.entry; TestUtils.Simulate.mouseDown(secondItem); assert.equal(node.value, secondItemValue); }); }); describe('component functions', function() { beforeEach(function() { this.sinon = sinon.sandbox.create(); }); afterEach(function() { this.sinon.restore(); }); it('focuses the typeahead', function() { var node = ReactDOM.findDOMNode(this.component.refs.entry); this.sinon.spy(node, 'focus'); this.component.focus(); assert.equal(node.focus.calledOnce, true); }); }); }); describe('props', function() { context('maxVisible', function() { it('limits the result set based on the maxVisible option', function() { var component = TestUtils.renderIntoDocument(<Typeahead options={ BEATLES } maxVisible={ 1 } ></Typeahead>); var results = simulateTextInput(component, 'o'); assert.equal(results.length, 1); }); it('limits the result set based on the maxVisible option, and shows resultsTruncatedMessage when specified', function() { var component = TestUtils.renderIntoDocument(<Typeahead options={ BEATLES } maxVisible={ 1 } resultsTruncatedMessage='Results truncated' ></Typeahead>); var results = simulateTextInput(component, 'o'); assert.equal(TestUtils.findRenderedDOMComponentWithClass(component, 'results-truncated').textContent, 'Results truncated'); }); }); context('displayOption', function() { it('renders simple options verbatim when not specified', function() { var component = TestUtils.renderIntoDocument(<Typeahead options={ BEATLES } />); var results = simulateTextInput(component, 'john'); assert.equal(ReactDOM.findDOMNode(results[0]).textContent, 'John'); }); it('renders custom options when specified as a string', function() { var component = TestUtils.renderIntoDocument(<Typeahead options={ BEATLES_COMPLEX } filterOption='firstName' displayOption='nameWithTitle' />); var results = simulateTextInput(component, 'john'); assert.equal(ReactDOM.findDOMNode(results[0]).textContent, 'John Winston Ono Lennon MBE'); }); it('renders custom options when specified as a function', function() { var component = TestUtils.renderIntoDocument(<Typeahead options={ BEATLES_COMPLEX } filterOption='firstName' displayOption={ function(o, i) { return i + ' ' + o.firstName + ' ' + o.lastName; } } />); var results = simulateTextInput(component, 'john'); assert.equal(ReactDOM.findDOMNode(results[0]).textContent, '0 John Lennon'); }); }); context('searchOptions', function() { it('maps correctly when specified with map function', function() { var createObject = function(o) { return { len: o.length, orig: o }; }; var component = TestUtils.renderIntoDocument(<Typeahead options={ BEATLES } searchOptions={ function(inp, opts) { return opts.map(createObject); } } displayOption={ function(o, i) { return 'Score: ' + o.len + ' ' + o.orig; } } inputDisplayOption={ function(o, i) { return o.orig; } } />); var results = simulateTextInput(component, 'john'); assert.equal(ReactDOM.findDOMNode(results[0]).textContent, 'Score: 4 John'); }); it('can sort displayed items when specified with map function wrapped with sort', function() { var createObject = function(o) { return { len: o.length, orig: o }; }; var component = TestUtils.renderIntoDocument(<Typeahead options={ BEATLES } searchOptions={ function(inp, opts) { return opts.map(function(o) { return o; }).sort().map(createObject); } } displayOption={ function(o, i) { return 'Score: ' + o.len + ' ' + o.orig; } } inputDisplayOption={ function(o, i) { return o.orig; } } />); var results = simulateTextInput(component, 'john'); assert.equal(ReactDOM.findDOMNode(results[0]).textContent, 'Score: 6 George'); }); }); context('inputDisplayOption', function() { it('displays a different value in input field and in list display', function() { var createObject = function(o) { return { len: o.length, orig: o }; }; var component = TestUtils.renderIntoDocument(<Typeahead options={ BEATLES } searchOptions={ function(inp, opts) { return opts.map(function(o) { return o; }).sort().map(createObject); } } displayOption={ function(o, i) { return 'Score: ' + o.len + ' ' + o.orig; } } inputDisplayOption={ function(o, i) { return o.orig; } } />); var results = simulateTextInput(component, 'john'); var node = component.refs.entry; TestUtils.Simulate.keyDown(node, { keyCode: Keyevent.DOM_VK_TAB }); assert.equal(node.value, 'George'); }); }); context('allowCustomValues', function() { beforeEach(function() { this.sinon = sinon.sandbox.create() this.selectSpy = this.sinon.spy(); this.component = TestUtils.renderIntoDocument(<Typeahead options={BEATLES} allowCustomValues={3} onOptionSelected={this.selectSpy} ></Typeahead>); }); afterEach(function() { this.sinon.restore(); }) it('should not display custom value if input length is less than entered', function() { var input = this.component.refs.entry; input.value = "zz"; TestUtils.Simulate.change(input); var results = TestUtils.scryRenderedComponentsWithType(this.component, TypeaheadOption); assert.equal(0, results.length); assert.equal(false, this.selectSpy.called); }); it('should display custom value if input exceeds props.allowCustomValues', function() { var input = this.component.refs.entry; input.value = "ZZZ"; TestUtils.Simulate.change(input); var results = TestUtils.scryRenderedComponentsWithType(this.component, TypeaheadOption); assert.equal(1, results.length); assert.equal(false, this.selectSpy.called); }); it('should call onOptionSelected when selecting from options', function() { var results = simulateTextInput(this.component, 'o'); var firstItem = ReactDOM.findDOMNode(results[0]).innerText; var node = this.component.refs.entry; TestUtils.Simulate.keyDown(node, { keyCode: Keyevent.DOM_VK_DOWN }); TestUtils.Simulate.keyDown(node, { keyCode: Keyevent.DOM_VK_DOWN }); TestUtils.Simulate.keyDown(node, { keyCode: Keyevent.DOM_VK_UP }); TestUtils.Simulate.keyDown(node, { keyCode: Keyevent.DOM_VK_RETURN }); assert.equal(true, this.selectSpy.called); assert(this.selectSpy.calledWith(firstItem)); }) it('should call onOptionSelected when custom value is selected', function() { var input = this.component.refs.entry; input.value = "ZZZ"; TestUtils.Simulate.change(input); TestUtils.Simulate.keyDown(input, { keyCode: Keyevent.DOM_VK_DOWN }); TestUtils.Simulate.keyDown(input, { keyCode: Keyevent.DOM_VK_RETURN }); assert.equal(true, this.selectSpy.called); assert(this.selectSpy.calledWith(input.value)); }) it('should add hover prop to customValue', function() { var input = this.component.refs.entry; input.value = "ZZZ"; TestUtils.Simulate.change(input); var results = TestUtils.scryRenderedComponentsWithType(this.component, TypeaheadOption); TestUtils.Simulate.keyDown(input, { keyCode: Keyevent.DOM_VK_DOWN }); assert.equal(true, results[0].props.hover) }) }); context('customClasses', function() { before(function() { var customClasses = { input: 'topcoat-text-input', results: 'topcoat-list__container', listItem: 'topcoat-list__item', listAnchor: 'topcoat-list__link', hover: 'topcoat-list__item-active' }; this.component = TestUtils.renderIntoDocument(<Typeahead options={ BEATLES } customClasses={ customClasses } ></Typeahead>); simulateTextInput(this.component, 'o'); }); it('adds a custom class to the typeahead input', function() { var input = this.component.refs.entry; assert.isTrue(input.classList.contains('topcoat-text-input')); }); it('adds a custom class to the results component', function() { var results = ReactDOM.findDOMNode(TestUtils.findRenderedComponentWithType(this.component, TypeaheadSelector)); assert.isTrue(results.classList.contains('topcoat-list__container')); }); it('adds a custom class to the list items', function() { var typeaheadOptions = TestUtils.scryRenderedComponentsWithType(this.component, TypeaheadOption); var listItem = ReactDOM.findDOMNode(typeaheadOptions[1]); assert.isTrue(listItem.classList.contains('topcoat-list__item')); }); it('adds a custom class to the option anchor tags', function() { var typeaheadOptions = TestUtils.scryRenderedComponentsWithType(this.component, TypeaheadOption); var listAnchor = typeaheadOptions[1].refs.anchor; assert.isTrue(listAnchor.classList.contains('topcoat-list__link')); }); it('adds a custom class to the list items when active', function() { var typeaheadOptions = TestUtils.scryRenderedComponentsWithType(this.component, TypeaheadOption); var node = this.component.refs.entry; TestUtils.Simulate.keyDown(node, { keyCode: Keyevent.DOM_VK_DOWN }); var listItem = typeaheadOptions[0]; var domListItem = ReactDOM.findDOMNode(listItem); assert.isTrue(domListItem.classList.contains('topcoat-list__item-active')); }); }); context('initialValue', function() { it('should perform an initial search if a default value is provided', function() { var component = TestUtils.renderIntoDocument(<Typeahead options={ BEATLES } initialValue={ 'o' } />); var results = TestUtils.scryRenderedComponentsWithType(component, TypeaheadOption); assert.equal(results.length, 3); }); }); context('value', function() { it('should set input value', function() { var component = TestUtils.renderIntoDocument(<Typeahead options={ BEATLES } value={ 'John' } />); var input = component.refs.entry; assert.equal(input.value, 'John'); }); }); context('onKeyDown', function() { it('should bind to key events on the input', function() { var component = TestUtils.renderIntoDocument(<Typeahead options={ BEATLES } onKeyDown={ function(e) { assert.equal(e.keyCode, 87); } } />); var input = component.refs.entry; TestUtils.Simulate.keyDown(input, { keyCode: 87 }); }); }); context('onKeyPress', function() { it('should bind to key events on the input', function() { var component = TestUtils.renderIntoDocument(<Typeahead options={ BEATLES } onKeyPress={ function(e) { assert.equal(e.keyCode, 87); } } />); var input = component.refs.entry; TestUtils.Simulate.keyPress(input, { keyCode: 87 }); }); }); context('onKeyUp', function() { it('should bind to key events on the input', function() { var component = TestUtils.renderIntoDocument(<Typeahead options={ BEATLES } onKeyUp={ function(e) { assert.equal(e.keyCode, 87); } } />); var input = component.refs.entry; TestUtils.Simulate.keyUp(input, { keyCode: 87 }); }); }); context('inputProps', function() { it('should forward props to the input element', function() { var component = TestUtils.renderIntoDocument(<Typeahead options={ BEATLES } inputProps={{ autoCorrect: 'off' }} />); var input = component.refs.entry; assert.equal(input.getAttribute('autoCorrect'), 'off'); }); }); context('defaultClassNames', function() { it('should remove default classNames when this prop is specified and false', function() { var component = TestUtils.renderIntoDocument(<Typeahead options={ BEATLES } defaultClassNames={false} />); simulateTextInput(component, 'o'); assert.notOk(ReactDOM.findDOMNode(component).classList.contains("typeahead")); assert.notOk(ReactDOM.findDOMNode(component.refs.sel).classList.contains("typeahead-selector")); }); }); context('filterOption', function() { var FN_TEST_PLANS = [ { name: 'accepts everything', fn: function() { return true; }, input: 'xxx', output: 4 }, { name: 'rejects everything', fn: function() { return false; }, input: 'o', output: 0 } ]; _.each(FN_TEST_PLANS, function(testplan) { it('should filter with a custom function that ' + testplan.name, function() { var component = TestUtils.renderIntoDocument(<Typeahead options={ BEATLES } filterOption={ testplan.fn } />); var results = simulateTextInput(component, testplan.input); assert.equal(results.length, testplan.output); }); }); var STRING_TEST_PLANS = { 'o': 3, 'pa': 1, 'Grg': 1, 'Ringo': 1, 'xxx': 0 }; it('should filter using fuzzy matching on the provided field name', function() { var component = TestUtils.renderIntoDocument(<Typeahead options={ BEATLES_COMPLEX } filterOption='firstName' displayOption='firstName' />); _.each(STRING_TEST_PLANS, function(expected, value) { var results = simulateTextInput(component, value); assert.equal(results.length, expected, 'Text input: ' + value); }, this); }); }); context('formInputOption', function() { var FORM_INPUT_TEST_PLANS = [ { name: 'uses simple options verbatim when not specified', props: { options: BEATLES }, output: 'John' }, { name: 'defaults to the display string when not specified', props: { options: BEATLES_COMPLEX, filterOption: 'firstName', displayOption: 'nameWithTitle' }, output: 'John Winston Ono Lennon MBE' }, { name: 'uses custom options when specified as a string', props: { options: BEATLES_COMPLEX, filterOption: 'firstName', displayOption: 'nameWithTitle', formInputOption: 'lastName' }, output: 'Lennon' }, { name: 'uses custom optinos when specified as a function', props: { options: BEATLES_COMPLEX, filterOption: 'firstName', displayOption: 'nameWithTitle', formInputOption: function(o, i) { return o.firstName + ' ' + o.lastName; } }, output: 'John Lennon' } ]; _.each(FORM_INPUT_TEST_PLANS, function(testplan) { it(testplan.name, function() { var component = TestUtils.renderIntoDocument(<Typeahead {...testplan.props} name='beatles' />); var results = simulateTextInput(component, 'john'); var node = component.refs.entry; TestUtils.Simulate.keyDown(node, { keyCode: Keyevent.DOM_VK_DOWN }); TestUtils.Simulate.keyDown(node, { keyCode: Keyevent.DOM_VK_RETURN }); assert.equal(component.state.selection, testplan.output); }); }); }); context('customListComponent', function() { before(function() { ListComponent = createReactClass({ render: function() { return <div></div>; } }); this.ListComponent = ListComponent; }) beforeEach(function() { this.component = TestUtils.renderIntoDocument( <Typeahead options={ BEATLES } customListComponent={this.ListComponent}/> ); }); it('should not show the customListComponent when the input is empty', function() { var results = TestUtils.scryRenderedComponentsWithType(this.component, this.ListComponent); assert.equal(0, results.length); }); it('should show the customListComponent when the input is not empty', function() { var input = this.component.refs.entry; input.value = "o"; TestUtils.Simulate.change(input); var results = TestUtils.scryRenderedComponentsWithType(this.component, this.ListComponent); assert.equal(1, results.length); }); it('should no longer show the customListComponent after an option has been selected', function() { var input = this.component.refs.entry; input.value = "o"; TestUtils.Simulate.change(input); TestUtils.Simulate.keyDown(input, { keyCode: Keyevent.DOM_VK_TAB }); var results = TestUtils.scryRenderedComponentsWithType(this.component, this.ListComponent); assert.equal(0, results.length); }); }); context('textarea', function() { it('should render a <textarea> input', function() { var component = TestUtils.renderIntoDocument(<Typeahead options={ BEATLES } textarea={ true } />); var input = component.refs.entry; assert.equal(input.tagName.toLowerCase(), 'textarea'); }); it('should render a <input> input', function() { var component = TestUtils.renderIntoDocument(<Typeahead options={ BEATLES } />); var input = component.refs.entry; assert.equal(input.tagName.toLowerCase(), 'input'); }); }); context('showOptionsWhenEmpty', function() { it('do not render options when value is empty by default', function() { var component = TestUtils.renderIntoDocument( <Typeahead options={ BEATLES } /> ); var results = TestUtils.scryRenderedComponentsWithType(component, TypeaheadOption); assert.equal(0, results.length); }); it('do not render options when value is empty when set to true and not focused', function() { var component = TestUtils.renderIntoDocument( <Typeahead options={ BEATLES } showOptionsWhenEmpty={ true } /> ); var results = TestUtils.scryRenderedComponentsWithType(component, TypeaheadOption); assert.equal(0, results.length); }); it('render options when value is empty when set to true and focused', function() { var component = TestUtils.renderIntoDocument( <Typeahead options={ BEATLES } showOptionsWhenEmpty={ true } /> ); TestUtils.Simulate.focus(component.refs.entry); var results = TestUtils.scryRenderedComponentsWithType(component, TypeaheadOption); assert.equal(4, results.length); }); }); }); });