From f56c6447331def98579fd2cc49f10c543083cd9c Mon Sep 17 00:00:00 2001 From: Dave Brotherstone Date: Thu, 16 Jul 2015 21:03:06 +0200 Subject: [PATCH 01/23] Add tests for changing props --- test/Select-test.js | 102 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 99 insertions(+), 3 deletions(-) diff --git a/test/Select-test.js b/test/Select-test.js index 796b8e95c3..c535a57e2a 100644 --- a/test/Select-test.js +++ b/test/Select-test.js @@ -18,12 +18,36 @@ var TestUtils = React.addons.TestUtils; var Select = require('../src/Select'); +// The displayed text of the currently selected item, when items collapsed +var DISPLAYED_SELECTION_SELECTOR = '.Select-placeholder'; +var FORM_VALUE = '.Select > input'; -describe('Select', function() { +class PropsWrapper extends React.Component { + + constructor(props) { + super(props); + this.state = props || {}; + } + + setPropsForChild(props) { + this.setState(props); + } + + getChild() { + return this.refs.child; + } + + render() { + var Component = this.props.childComponent; + return (); + } +} +describe('Select', function() { + var options, instance, onChange; + var searchInputNode; + describe('with simple options', function () { - var options, instance, onChange; - var searchInputNode; beforeEach(function () { @@ -251,7 +275,79 @@ describe('Select', function() { 'to have items satisfying', 'to have text', 'Three'); }); + + }); + + }); + + describe('with options and value', function () { + + var wrapper; + + beforeEach(function () { + + options = [ + { value: 'one', label: 'One' }, + { value: 'two', label: 'Two' }, + { value: 'three', label: 'Three' } + ]; + + onChange = sinon.spy(); + + // Render an instance of the component + wrapper = TestUtils.renderIntoDocument( + + ); + + // Focus on the input, such that mouse events are accepted + instance = wrapper.getChild(); + searchInputNode = instance.getInputNode().getDOMNode().querySelector('input'); + TestUtils.Simulate.focus(searchInputNode); + }); + + it('starts with the given value', function () { + + var node = React.findDOMNode(instance); + expect(node, 'queried for', DISPLAYED_SELECTION_SELECTOR, + 'to have items satisfying', 'to have text', 'One'); + }); + + + + it('supports setting the value via prop', function () { + + wrapper.setPropsForChild({ + value: 'three' + }); + + expect(React.findDOMNode(instance), 'queried for', DISPLAYED_SELECTION_SELECTOR, + 'to have items satisfying', 'to have text', 'Three'); + }); + + it('sets the value of the hidden form node', function () { + + wrapper.setPropsForChild({ + value: 'three' + }); + + expect(React.findDOMNode(wrapper).querySelector(FORM_VALUE).value, 'to equal', 'three' ); }); + it('display the raw value if the option is not available', function () { + + wrapper.setPropsForChild({ + value: 'something new' + }); + + expect(React.findDOMNode(instance), 'queried for', DISPLAYED_SELECTION_SELECTOR, + 'to have items satisfying', 'to have text', 'something new'); + }); }); }); From cb04c3000f7760b578527d2b5eec6a526866fa5c Mon Sep 17 00:00:00 2001 From: Dave Brotherstone Date: Thu, 16 Jul 2015 21:05:28 +0200 Subject: [PATCH 02/23] Add failing test for late-arriving options This specifically tests when the options change to include the value. This is a bug. --- test/Select-test.js | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/test/Select-test.js b/test/Select-test.js index c535a57e2a..8f0d0e43dc 100644 --- a/test/Select-test.js +++ b/test/Select-test.js @@ -349,5 +349,25 @@ describe('Select', function() { expect(React.findDOMNode(instance), 'queried for', DISPLAYED_SELECTION_SELECTOR, 'to have items satisfying', 'to have text', 'something new'); }); + + it('updates the display text if the option appears later', function () { + + wrapper.setPropsForChild({ + value: 'new' + }); + + wrapper.setPropsForChild({ + options: [ + { value: 'one', label: 'One' }, + { value: 'two', labal: 'Two' }, + { value: 'new', label: 'New item in the options' }, + { value: 'three', label: 'Three' } + ] + }); + + expect(React.findDOMNode(instance), 'queried for', DISPLAYED_SELECTION_SELECTOR, + 'to have items satisfying', 'to have text', 'New item in the options'); + + }); }); }); From b4f602529b1d5863a8d70bebc72cb7b06602fddc Mon Sep 17 00:00:00 2001 From: Dave Brotherstone Date: Fri, 24 Jul 2015 19:44:10 +0200 Subject: [PATCH 03/23] Move helper functions for tests Add a pressEscape() function --- test/Select-test.js | 34 ++++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/test/Select-test.js b/test/Select-test.js index 8f0d0e43dc..d663c1a810 100644 --- a/test/Select-test.js +++ b/test/Select-test.js @@ -46,6 +46,26 @@ class PropsWrapper extends React.Component { describe('Select', function() { var options, instance, onChange; var searchInputNode; + + function pressEnterToAccept() { + TestUtils.Simulate.keyDown(searchInputNode, { keyCode: 13, key: 'Enter' }); + } + + function pressTabToAccept() { + TestUtils.Simulate.keyDown(searchInputNode, { keyCode: 9, key: 'Tab' }); + } + + function pressEscape() { + TestUtils.Simulate.keyDown(searchInputNode, { keyCode: 27, key: 'Escape' }); + } + + function typeSearchText(text) { + TestUtils.Simulate.change(searchInputNode, { target: { value: text } }); + } + + function getSelectControl(instance) { + return React.findDOMNode(instance).querySelector('.Select-control'); + } describe('with simple options', function () { @@ -76,20 +96,6 @@ describe('Select', function() { }); - function pressEnterToAccept() { - TestUtils.Simulate.keyDown(searchInputNode, { keyCode: 13, key: 'Enter' }); - } - function pressTabToAccept() { - TestUtils.Simulate.keyDown(searchInputNode, { keyCode: 9, key: 'Tab' }); - } - - function typeSearchText(text) { - TestUtils.Simulate.change(searchInputNode, { target: { value: text } }); - } - - function getSelectControl(instance) { - return React.findDOMNode(instance).querySelector('.Select-control'); - } it('should assign the given name', function () { var selectInputElement = TestUtils.scryRenderedDOMComponentsWithTag(instance, 'input')[0]; From 1282cb9b47fd41015cac63c31867000e561549c7 Mon Sep 17 00:00:00 2001 From: Dave Brotherstone Date: Fri, 24 Jul 2015 19:45:52 +0200 Subject: [PATCH 04/23] Bug fix + tests for pressing escape event needs to be passed on to the clearValue() method Tests check that the escape handling is correct in the two states (open and closed) --- test/Select-test.js | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/test/Select-test.js b/test/Select-test.js index d663c1a810..c4cf2df7f5 100644 --- a/test/Select-test.js +++ b/test/Select-test.js @@ -159,6 +159,28 @@ describe('Select', function() { expect(onChange, 'was called with', 'three'); }); + describe('pressing escape', function () { + beforeEach(function () { + typeSearchText('h'); + pressTabToAccept(); + expect(onChange, 'was called with', 'three'); + onChange.reset(); + pressEscape(); + }); + + it('should call onChange with a empty value', function () { + + // TODO: Shouldn't this be null, really? + expect(onChange, 'was called with', ''); + }); + + it('should clear the display', function () { + + expect(React.findDOMNode(instance).querySelector(DISPLAYED_SELECTION_SELECTOR), + 'to have text', 'Select...'); + }); + }); + it('should focus the first value on mouse click', function () { TestUtils.Simulate.mouseDown(React.findDOMNode(instance).querySelector('.Select-control')); From e7a373e36471d7498b50e73392e5830aaee34d05 Mon Sep 17 00:00:00 2001 From: Dave Brotherstone Date: Fri, 24 Jul 2015 19:46:43 +0200 Subject: [PATCH 05/23] Tests for allowCreate=true --- test/Select-test.js | 122 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 122 insertions(+) diff --git a/test/Select-test.js b/test/Select-test.js index c4cf2df7f5..b2c3e73f69 100644 --- a/test/Select-test.js +++ b/test/Select-test.js @@ -398,4 +398,126 @@ describe('Select', function() { }); }); + + describe('with allowCreate=true', function () { + + var wrapper; + + beforeEach(function () { + + options = [ + { value: 'one', label: 'One' }, + { value: 'two', label: 'Two' }, + { value: 'got spaces', label: 'Label for spaces' }, + { value: 'gotnospaces', label: 'Label for gotnospaces' }, + { value: 'abc 123', label: 'Label for abc 123' }, + { value: 'three', label: 'Three' }, + { value: 'zzzzz', label: 'test value' } + ]; + + onChange = sinon.spy(); + + // Render an instance of the component + wrapper = TestUtils.renderIntoDocument( + + ); + + // Focus on the input, such that mouse events are accepted + instance = wrapper.getChild(); + searchInputNode = instance.getInputNode().getDOMNode().querySelector('input'); + TestUtils.Simulate.focus(searchInputNode); + }); + + it('has an "Add xyz" option when entering xyz', function () { + typeSearchText('xyz'); + + expect(React.findDOMNode(instance), 'queried for', '.Select-menu .Select-option', + 'to have items satisfying', 'to have text', 'Add xyz to values?'); + }); + + it('fires an onChange with the new value when selecting the Add option', function () { + + typeSearchText('xyz'); + TestUtils.Simulate.click(React.findDOMNode(instance).querySelector('.Select-menu .Select-option')); + + expect(onChange, 'was called with', 'xyz'); + }); + + it('allows updating the options with a new label, following the onChange', function () { + + typeSearchText('xyz'); + TestUtils.Simulate.click(React.findDOMNode(instance).querySelector('.Select-menu .Select-option')); + + expect(onChange, 'was called with', 'xyz'); + + // Now the client adds the option, with a new label + wrapper.setPropsForChild({ + options: [ + { value: 'one', label: 'One' }, + { value: 'xyz', label: 'XYZ Label' } + ], + value: 'xyz' + }); + + expect(React.findDOMNode(instance).querySelector(DISPLAYED_SELECTION_SELECTOR), + 'to have text', 'XYZ Label'); + }); + + it('displays an add option when a value with spaces is entered', function () { + + typeSearchText('got'); + + expect(React.findDOMNode(instance).querySelectorAll('.Select-menu .Select-option')[0], + 'to have text', 'Add got to values?'); + }); + + it('displays an add option when a value with spaces is entered', function () { + + typeSearchText('got'); + + expect(React.findDOMNode(instance).querySelectorAll('.Select-menu .Select-option')[0], + 'to have text', 'Add got to values?'); + }); + + it('displays an add option when a label with spaces is entered', function () { + + typeSearchText('test'); + + expect(React.findDOMNode(instance).querySelectorAll('.Select-menu .Select-option')[0], + 'to have text', 'Add test to values?'); + }); + + it('does not display the option label when an existing value is entered', function () { + + typeSearchText('zzzzz'); + + expect(React.findDOMNode(instance).querySelectorAll('.Select-menu .Select-option'), + 'to have length', 1); + expect(React.findDOMNode(instance).querySelectorAll('.Select-menu .Select-option')[0], + 'to have text', 'Add zzzzz to values?'); + }); + + it('renders the existing option and an add option when an existing display label is entered', function () { + + typeSearchText('test value'); + + // First item should be the add option (as the "value" is not in the collection) + expect(React.findDOMNode(instance).querySelectorAll('.Select-menu .Select-option')[0], + 'to have text', 'Add test value to values?'); + // Second item should be the existing option with the matching label + expect(React.findDOMNode(instance).querySelectorAll('.Select-menu .Select-option')[1], + 'to have text', 'test value'); + expect(React.findDOMNode(instance).querySelectorAll('.Select-menu .Select-option'), + 'to have length', 2); + }); + }); }); From 314fa55dc0e80baca7fa6864487964679f5d14ae Mon Sep 17 00:00:00 2001 From: Dave Brotherstone Date: Fri, 24 Jul 2015 19:47:39 +0200 Subject: [PATCH 06/23] Make "node" the runner for wallaby Otherwise wallaby uses the io.js default, which means jsdom has issues (different V8 ABI) --- wallaby.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/wallaby.js b/wallaby.js index 0a8cd8b128..d83b6f19dd 100644 --- a/wallaby.js +++ b/wallaby.js @@ -9,7 +9,8 @@ module.exports = function (wallaby) { tests: ['test/*-test.js' ], env: { - type: 'node' + type: 'node', + runner: 'node' }, preprocessors: { From 9e3efff2025ce2737e7427db4da87880b977396a Mon Sep 17 00:00:00 2001 From: Dave Brotherstone Date: Fri, 24 Jul 2015 21:55:13 +0200 Subject: [PATCH 07/23] Tests for asyncOptions using autoload=true and autoload=false --- test/Select-test.js | 170 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 170 insertions(+) diff --git a/test/Select-test.js b/test/Select-test.js index b2c3e73f69..bea628fac0 100644 --- a/test/Select-test.js +++ b/test/Select-test.js @@ -520,4 +520,174 @@ describe('Select', function() { 'to have length', 2); }); }); + + describe('with async options', function () { + + var asyncOptions; + + beforeEach(function () { + + onChange = sinon.spy(); + asyncOptions = sinon.stub(); + + asyncOptions.withArgs('te').callsArgWith(1, null, { + options: [ + {value: 'test', label: 'TEST one'}, + {value: 'test2', label: 'TEST two'}, + {value: 'tell', label: 'TELL three'} + ] + }); + + asyncOptions.withArgs('tes').callsArgWith(1, null, { + options: [ + {value: 'test', label: 'TEST one'}, + {value: 'test2', label: 'TEST two'} + ] + }); + + + }); + + describe('with autoload=true', function () { + + beforeEach(function () { + + // Render an instance of the component + instance = TestUtils.renderIntoDocument( + + ); + + // Focus on the input, such that mouse events are accepted + searchInputNode = instance.getInputNode().getDOMNode().querySelector('input'); + TestUtils.Simulate.focus(searchInputNode); + }); + + it('does not initially call asyncOptions', function () { + + expect(asyncOptions, 'was not called'); + }); + + it('calls the asyncOptions on first key entry', function () { + + typeSearchText('a'); + expect(asyncOptions, 'was called with', 'a'); + }); + }); + }); }); From d6fcb799147a6f65761261c7d19265ec8183791b Mon Sep 17 00:00:00 2001 From: Dave Brotherstone Date: Fri, 24 Jul 2015 23:23:44 +0200 Subject: [PATCH 08/23] Tests for multi-select Coverage now up to ~80% --- test/Select-test.js | 114 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) diff --git a/test/Select-test.js b/test/Select-test.js index bea628fac0..7e0f446fae 100644 --- a/test/Select-test.js +++ b/test/Select-test.js @@ -58,6 +58,10 @@ describe('Select', function() { function pressEscape() { TestUtils.Simulate.keyDown(searchInputNode, { keyCode: 27, key: 'Escape' }); } + + function pressBackspace() { + TestUtils.Simulate.keyDown(searchInputNode, { keyCode: 8, key: 'Backspace' }); + } function typeSearchText(text) { TestUtils.Simulate.change(searchInputNode, { target: { value: text } }); @@ -690,4 +694,114 @@ describe('Select', function() { }); }); }); + + describe('with multi-select', function () { + + beforeEach(function () { + + options = [ + { value: 'one', label: 'One' }, + { value: 'two', label: 'Two' }, + { value: 'three', label: 'Three' }, + { value: 'four', label: 'Four' } + ]; + + onChange = sinon.spy(); + + // Render an instance of the component + instance = TestUtils.renderIntoDocument( + + ); + + // Focus on the input, such that mouse events are accepted + searchInputNode = instance.getInputNode().getDOMNode().querySelector('input'); + TestUtils.Simulate.focus(searchInputNode); + return instance; + + }; + + describe('className', function () { + + it('assigns the className to the outer-most element', function () { + + var instance = createControl({ className: 'test-class' }); + expect(React.findDOMNode(instance), 'to have attributes', { + class: 'test-class' + }); + }); + }); + + }); + }); From 240c8a0abfcfcbb32922ca3f74d25349355acfe8 Mon Sep 17 00:00:00 2001 From: Dave Brotherstone Date: Mon, 27 Jul 2015 17:21:21 +0200 Subject: [PATCH 10/23] Bug fix and tests for clearable prop Pressing escape when clearable=false and menu is closed now has no effect --- src/Select.js | 2 +- test/Select-test.js | 141 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 140 insertions(+), 3 deletions(-) diff --git a/src/Select.js b/src/Select.js index 0ef2c4e406..f375fc3326 100644 --- a/src/Select.js +++ b/src/Select.js @@ -407,7 +407,7 @@ var Select = React.createClass({ case 27: // escape if (this.state.isOpen) { this.resetValue(); - } else { + } else if (this.props.clearable) { this.clearValue(event); } break; diff --git a/test/Select-test.js b/test/Select-test.js index 68f2bc7110..058e53ee0a 100644 --- a/test/Select-test.js +++ b/test/Select-test.js @@ -826,7 +826,7 @@ describe('Select', function() { ); // Focus on the input, such that mouse events are accepted - searchInputNode = instance.getInputNode().getDOMNode().querySelector('input'); + searchInputNode = React.findDOMNode(instance.getInputNode()).querySelector('input'); TestUtils.Simulate.focus(searchInputNode); return instance; @@ -843,6 +843,143 @@ describe('Select', function() { }); }); + describe('clearable=true', function () { + + beforeEach(function () { + + var instance = createControl({ + clearable: true, + options: defaultOptions, + value: 'three' + }); + + expect(React.findDOMNode(instance), 'queried for', DISPLAYED_SELECTION_SELECTOR, + 'to have items satisfying', 'to have text', 'Three'); + + }); + + describe('on pressing escape', function () { + + beforeEach(function () { + + pressEscape(); + }); + + it('calls onChange with empty', function () { + + expect(onChange, 'was called with', ''); + }); + + it('resets the display value', function () { + + expect(React.findDOMNode(instance), 'queried for', DISPLAYED_SELECTION_SELECTOR, + 'to have items satisfying', 'to have text', 'Select...'); + }); + + it('resets the control value', function () { + + expect(React.findDOMNode(instance).querySelector('input').value, 'to equal', ''); + }); + }); + + describe('on clicking `clear`', function () { + + beforeEach(function () { + TestUtils.Simulate.click(React.findDOMNode(instance).querySelector('.Select-clear')); + }); + + it('calls onChange with empty', function () { + + expect(onChange, 'was called with', ''); + }); + + it('resets the display value', function () { + + expect(React.findDOMNode(instance), 'queried for', DISPLAYED_SELECTION_SELECTOR, + 'to have items satisfying', 'to have text', 'Select...'); + }); + + it('resets the control value', function () { + + expect(React.findDOMNode(instance).querySelector('input').value, 'to equal', ''); + }); + }); + }); + + describe('clearable=false', function () { + + beforeEach(function () { + + var instance = createControl({ + clearable: false, + options: defaultOptions, + value: 'three' + }); + + expect(React.findDOMNode(instance), 'queried for', DISPLAYED_SELECTION_SELECTOR, + 'to have items satisfying', 'to have text', 'Three'); + + }); + + it('does not render a clear button', function () { + + expect(React.findDOMNode(instance).querySelectorAll('.Select-clear'), 'to have length', 0); + }); + + describe('on escape', function () { + beforeEach(function () { + + pressEscape(); + }); + + it('does not call onChange', function () { + + expect(onChange, 'was not called'); + }); + + it('does not reset the display value', function () { + + expect(React.findDOMNode(instance), 'queried for', DISPLAYED_SELECTION_SELECTOR, + 'to have items satisfying', 'to have text', 'Three'); + }); + + it('does not reset the control value', function () { + + expect(React.findDOMNode(instance).querySelector('input').value, 'to equal', 'three'); + }); + + }); + + describe('when open', function () { + + beforeEach(function () { + + typeSearchText('abc'); + }); + + describe('on escape', function () { + + beforeEach(function () { + pressEscape(); + }); + + it('closes the menu', function () { + + expect(React.findDOMNode(instance).querySelectorAll('.Select-menu'), 'to have length', 0); + }); + + it('resets the control value to the original', function () { + + expect(React.findDOMNode(instance).querySelector('input').value, 'to equal', 'three'); + }); + + it('renders the original display label', function () { + + expect(React.findDOMNode(instance), 'queried for', DISPLAYED_SELECTION_SELECTOR, + 'to have items satisfying', 'to have text', 'Three'); + }); + }); + }); + }); }); - }); From 89dfdd5aec4930a48f829f61ba4a8ddbe2e04bfb Mon Sep 17 00:00:00 2001 From: Dave Brotherstone Date: Mon, 27 Jul 2015 17:32:24 +0200 Subject: [PATCH 11/23] Tests for clearAllText and clearValueText --- test/Select-test.js | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/test/Select-test.js b/test/Select-test.js index 058e53ee0a..baca179817 100644 --- a/test/Select-test.js +++ b/test/Select-test.js @@ -981,5 +981,49 @@ describe('Select', function() { }); }); }); + + describe('clearAllText', function () { + + beforeEach(function () { + + instance = createControl({ + multi: true, + clearable: true, + value: 'three', + clearAllText: 'Remove All Items Test Title', + clearValueText: 'Remove Value Test Title', // Should be ignored, multi=true + options: defaultOptions + }); + }); + + it('uses the prop as the title for clear', function () { + + expect(React.findDOMNode(instance).querySelector('.Select-clear'), 'to have attributes', { + title: 'Remove All Items Test Title' + }); + }); + }); + + describe('clearValueText', function () { + + beforeEach(function () { + + instance = createControl({ + multi: false, + clearable: true, + value: 'three', + clearAllText: 'Remove All Items Test Title', // Should be ignored, multi=false + clearValueText: 'Remove Value Test Title', + options: defaultOptions + }); + }); + + it('uses the prop as the title for clear', function () { + + expect(React.findDOMNode(instance).querySelector('.Select-clear'), 'to have attributes', { + title: 'Remove Value Test Title' + }); + }); + }); }); }); From b47d788eed8556ff2c5610d2f96d1f68c5f35678 Mon Sep 17 00:00:00 2001 From: Dave Brotherstone Date: Mon, 27 Jul 2015 17:51:45 +0200 Subject: [PATCH 12/23] Tests for delimiter One batch for a single character (;), and another for a multi-char string --- test/Select-test.js | 73 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/test/Select-test.js b/test/Select-test.js index baca179817..4881c78d0f 100644 --- a/test/Select-test.js +++ b/test/Select-test.js @@ -1025,5 +1025,78 @@ describe('Select', function() { }); }); }); + + describe('delimiter', function () { + + describe('is ;', function () { + + beforeEach(function () { + + instance = createControl({ + multi: true, + value: 'four;three', + delimiter: ';', + options: defaultOptions + }); + }); + + it('interprets the initial options correctly', function () { + + var values = React.findDOMNode(instance).querySelectorAll('.Select-item'); + + expect(values[0], 'queried for', '.Select-item-label', 'to have items satisfying', + 'to have text', 'AbcDef'); + expect(values[1], 'queried for', '.Select-item-label', 'to have items satisfying', + 'to have text', 'Three'); + expect(values, 'to have length', 2); + }); + + it('adds an additional option with the correct delimiter', function () { + + typeSearchText('one'); + pressEnterToAccept(); + expect(onChange, 'was called with', 'four;three;one', [ + { value: 'four', label: 'AbcDef' }, + { value: 'three', label: 'Three' }, + { value: 'one', label: 'One' } + ]); + }); + }); + + describe('is a multi-character string (`==XXX==`)', function () { + + beforeEach(function () { + + instance = createControl({ + multi: true, + value: 'four==XXX==three', + delimiter: '==XXX==', + options: defaultOptions + }); + }); + + it('interprets the initial options correctly', function () { + + var values = React.findDOMNode(instance).querySelectorAll('.Select-item'); + + expect(values[0], 'queried for', '.Select-item-label', 'to have items satisfying', + 'to have text', 'AbcDef'); + expect(values[1], 'queried for', '.Select-item-label', 'to have items satisfying', + 'to have text', 'Three'); + expect(values, 'to have length', 2); + }); + + it('adds an additional option with the correct delimiter', function () { + + typeSearchText('one'); + pressEnterToAccept(); + expect(onChange, 'was called with', 'four==XXX==three==XXX==one', [ + { value: 'four', label: 'AbcDef' }, + { value: 'three', label: 'Three' }, + { value: 'one', label: 'One' } + ]); + }); + }); + }); }); }); From 43e24f8262671c8b756d49a7d6d2c644b5c2ce7f Mon Sep 17 00:00:00 2001 From: Dave Brotherstone Date: Mon, 27 Jul 2015 20:15:21 +0200 Subject: [PATCH 13/23] Fix + tests for disabled control onKeyDown was checking the state, not props, for disabled This didn't affect the normal operation, as the control was not focusable when disabled, but could have potentially caused issues in certain browsers --- src/Select.js | 2 +- test/Select-test.js | 49 +++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/src/Select.js b/src/Select.js index f375fc3326..000a6014fc 100644 --- a/src/Select.js +++ b/src/Select.js @@ -381,7 +381,7 @@ var Select = React.createClass({ }, handleKeyDown: function(event) { - if (this.state.disabled) return; + if (this.props.disabled) return; switch (event.keyCode) { diff --git a/test/Select-test.js b/test/Select-test.js index 4881c78d0f..e600b8f0e0 100644 --- a/test/Select-test.js +++ b/test/Select-test.js @@ -826,8 +826,12 @@ describe('Select', function() { ); // Focus on the input, such that mouse events are accepted - searchInputNode = React.findDOMNode(instance.getInputNode()).querySelector('input'); - TestUtils.Simulate.focus(searchInputNode); + var searchInstance = React.findDOMNode(instance.getInputNode()); + searchInputNode = null; + if (searchInstance) { + searchInputNode = searchInstance.querySelector('input'); + TestUtils.Simulate.focus(searchInputNode); + } return instance; }; @@ -1098,5 +1102,46 @@ describe('Select', function() { }); }); }); + + describe('disabled=true', function () { + + beforeEach(function () { + + instance = createControl({ + options: defaultOptions, + value: 'three', + disabled: true, + searchable: true + }); + }); + + it('does not render an input search control', function () { + + expect(searchInputNode, 'to be null'); + }); + + it('does not react to keyDown', function () { + + TestUtils.Simulate.keyDown(getSelectControl(instance), { keyCode: 40, key: 'ArrowDown' }); + expect(React.findDOMNode(instance).querySelectorAll('.Select-option'), 'to have length', 0); + }); + + it('does not respond to mouseDown', function () { + + TestUtils.Simulate.mouseDown(getSelectControl(instance)); + expect(React.findDOMNode(instance).querySelectorAll('.Select-option'), 'to have length', 0); + }); + + it('does not respond to mouseDown on the arrow', function () { + + TestUtils.Simulate.mouseDown(getSelectControl(instance).querySelector('.Select-arrow')); + expect(React.findDOMNode(instance).querySelectorAll('.Select-option'), 'to have length', 0); + }); + + it('renders the given value', function () { + + expect(React.findDOMNode(instance).querySelector(DISPLAYED_SELECTION_SELECTOR), 'to have text', 'Three'); + }); + }); }); }); From af3fc53379982d59703616671a47972af0b93fcf Mon Sep 17 00:00:00 2001 From: Dave Brotherstone Date: Mon, 27 Jul 2015 21:06:34 +0200 Subject: [PATCH 14/23] Tests for filterOption --- test/Select-test.js | 60 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/test/Select-test.js b/test/Select-test.js index e600b8f0e0..415d16101e 100644 --- a/test/Select-test.js +++ b/test/Select-test.js @@ -1143,5 +1143,65 @@ describe('Select', function() { expect(React.findDOMNode(instance).querySelector(DISPLAYED_SELECTION_SELECTOR), 'to have text', 'Three'); }); }); + + describe('custom filterOption function', function () { + + // Custom function returns true only for value "four" + var filterOption = function (option) { + if (option.value === 'four') { + return true; + } + + return false; + }; + var spyFilterOption; + + beforeEach(function () { + + spyFilterOption = sinon.spy(filterOption); + + instance = createControl({ + options: defaultOptions, + filterOption: spyFilterOption + }); + }); + + it('calls the filter with each option', function () { + + expect(spyFilterOption, 'was called times', 4); + expect(spyFilterOption, 'was called with', defaultOptions[0], ''); + expect(spyFilterOption, 'was called with', defaultOptions[1], ''); + expect(spyFilterOption, 'was called with', defaultOptions[2], ''); + expect(spyFilterOption, 'was called with', defaultOptions[3], ''); + }); + + describe('when entering text', function () { + + beforeEach(function () { + + spyFilterOption.reset(); + typeSearchText('xyz'); + }); + + it('calls the filterOption function for each option', function () { + + expect(spyFilterOption, 'was called times', 4); + expect(spyFilterOption, 'was called with', defaultOptions[0], 'xyz'); + expect(spyFilterOption, 'was called with', defaultOptions[1], 'xyz'); + expect(spyFilterOption, 'was called with', defaultOptions[2], 'xyz'); + expect(spyFilterOption, 'was called with', defaultOptions[3], 'xyz'); + }); + + it('only shows the filtered option', function () { + + expect(React.findDOMNode(instance).querySelectorAll('.Select-option'), + 'to have length', 1); + + expect(React.findDOMNode(instance).querySelectorAll('.Select-option'), + 'to have items satisfying', + 'to have text', 'AbcDef'); + }); + }); + }); }); }); From f7e67c104dcaa6767f39acc0557d3152004a4949 Mon Sep 17 00:00:00 2001 From: Dave Brotherstone Date: Mon, 27 Jul 2015 22:28:00 +0200 Subject: [PATCH 15/23] Tests for filterOptions prop --- test/Select-test.js | 60 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/test/Select-test.js b/test/Select-test.js index 415d16101e..02974dfaf0 100644 --- a/test/Select-test.js +++ b/test/Select-test.js @@ -1203,5 +1203,65 @@ describe('Select', function() { }); }); }); + + describe('custom filterOptions function', function () { + + var spyFilterOptions; + + beforeEach(function () { + + spyFilterOptions = sinon.stub(); + spyFilterOptions.returns([ + { label: 'Return One', value: 'one' }, + { label: 'Return Two', value: 'two' } + ]); + + instance = createControl({ + options: defaultOptions, + filterOptions: spyFilterOptions, + searchable: true + }); + }); + + it('calls the filterOptions function initially', function () { + + expect(spyFilterOptions, 'was called'); + }); + + it('calls the filterOptions function initially with the initial options', function () { + + expect(spyFilterOptions, 'was called with', defaultOptions, ''); + }); + + it('uses the returned options', function () { + + TestUtils.Simulate.mouseDown(React.findDOMNode(instance).querySelector('.Select-arrow')); + + var options = React.findDOMNode(instance).querySelectorAll('.Select-option'); + expect(options[0], 'to have text', 'Return One'); + expect(options[1], 'to have text', 'Return Two'); + expect(options, 'to have length', 2); + }); + + it('calls the filterOptions function on text change', function () { + + typeSearchText('xyz'); + expect(spyFilterOptions, 'was called with', defaultOptions, 'xyz'); + }); + + it('uses new options after text change', function () { + + spyFilterOptions.returns([ + { value: 'abc', label: 'AAbbcc' }, + { value: 'def', label: 'DDeeff' } + ]); + typeSearchText('xyz'); + + var options = React.findDOMNode(instance).querySelectorAll('.Select-option'); + expect(options[0], 'to have text', 'AAbbcc'); + expect(options[1], 'to have text', 'DDeeff'); + expect(options, 'to have length', 2); + }); + }); }); }); From 31f0537a11c2bbcbb9f6be3e7ad4d6bdf81e5363 Mon Sep 17 00:00:00 2001 From: Dave Brotherstone Date: Mon, 27 Jul 2015 22:52:52 +0200 Subject: [PATCH 16/23] Tests for ignoreCase=false --- test/Select-test.js | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/test/Select-test.js b/test/Select-test.js index 02974dfaf0..c5322eb37a 100644 --- a/test/Select-test.js +++ b/test/Select-test.js @@ -1263,5 +1263,32 @@ describe('Select', function() { expect(options, 'to have length', 2); }); }); + + describe('ignoreCase=false', function () { + + beforeEach(function () { + + instance = createControl({ + searchable: true, + ignoreCase: false, + options: defaultOptions + }); + }); + + it('does not find options in a different case', function () { + + typeSearchText('def'); + var options = React.findDOMNode(instance).querySelectorAll('.Select-option'); + expect(options, 'to have length', 0); + }); + + it('finds options in the same case', function () { + + typeSearchText('Def'); + var options = React.findDOMNode(instance).querySelectorAll('.Select-option'); + expect(options[0], 'to have text', 'AbcDef'); + expect(options, 'to have length', 1); + }); + }); }); }); From 39b2afa03260de6707ce3d9250cd227b9dc4b257 Mon Sep 17 00:00:00 2001 From: Dave Brotherstone Date: Mon, 27 Jul 2015 23:24:35 +0200 Subject: [PATCH 17/23] Fix and tests for inputProps prop className should be appended to selected, not replaced --- src/Select.js | 4 ++-- test/Select-test.js | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/src/Select.js b/src/Select.js index 000a6014fc..4711df8e1c 100644 --- a/src/Select.js +++ b/src/Select.js @@ -754,13 +754,13 @@ var Select = React.createClass({ var input; var inputProps = { ref: 'input', - className: 'Select-input', + className: 'Select-input ' + (this.props.inputProps.className || ''), tabIndex: this.props.tabIndex || 0, onFocus: this.handleInputFocus, onBlur: this.handleInputBlur }; for (var key in this.props.inputProps) { - if (this.props.inputProps.hasOwnProperty(key)) { + if (this.props.inputProps.hasOwnProperty(key) && key !== 'className') { inputProps[key] = this.props.inputProps[key]; } } diff --git a/test/Select-test.js b/test/Select-test.js index c5322eb37a..2a9c9c590a 100644 --- a/test/Select-test.js +++ b/test/Select-test.js @@ -1290,5 +1290,43 @@ describe('Select', function() { expect(options, 'to have length', 1); }); }); + + describe('inputProps', function () { + + + beforeEach(function () { + + instance = createControl({ + searchable: true, + inputProps: { + inputClassName: 'extra-input-class', + className: 'extra-class-name', + id: 'search-input-id' + }, + options: defaultOptions + }); + }); + + it('passes id through to the search input box', function () { + expect(searchInputNode, 'to have attributes', { + id: 'search-input-id' + }); + }); + + it('passes the inputClassName to the search input box', function () { + + expect(searchInputNode, 'to have attributes', { + class: 'extra-input-class' + }); + }); + + it('adds the className on to the auto-size input', function () { + + expect(React.findDOMNode(instance.getInputNode()), + 'to have attributes', { + class: ['extra-class-name', 'Select-input'] + }); + }); + }); }); }); From 1c07e33c6839dc019f8aa0fa425d7dfdbf2f4043 Mon Sep 17 00:00:00 2001 From: Dave Brotherstone Date: Mon, 27 Jul 2015 23:46:41 +0200 Subject: [PATCH 18/23] inputProps tests for not searchable or disabled --- test/Select-test.js | 51 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/test/Select-test.js b/test/Select-test.js index 2a9c9c590a..9488e6830d 100644 --- a/test/Select-test.js +++ b/test/Select-test.js @@ -830,7 +830,9 @@ describe('Select', function() { searchInputNode = null; if (searchInstance) { searchInputNode = searchInstance.querySelector('input'); - TestUtils.Simulate.focus(searchInputNode); + if (searchInputNode) { + TestUtils.Simulate.focus(searchInputNode); + } } return instance; @@ -1327,6 +1329,53 @@ describe('Select', function() { class: ['extra-class-name', 'Select-input'] }); }); + + describe('and not searchable', function () { + + beforeEach(function () { + + instance = createControl({ + searchable: false, + inputProps: { + inputClassName: 'extra-input-class', + className: 'extra-class-name', + id: 'search-input-id' + }, + options: defaultOptions + }); + }); + + it('sets the className and id on the placeholder for the input', function () { + + expect(React.findDOMNode(instance).querySelector('.extra-class-name'), + 'to have attributes', { + id: 'search-input-id' + }); + }); + }); + + describe('and disabled', function () { + + beforeEach(function () { + + instance = createControl({ + searchable: true, + disabled: true, + inputProps: { + inputClassName: 'extra-input-class', + className: 'extra-class-name', + id: 'search-input-id' + }, + options: defaultOptions + }); + }); + + it('doesn\'t pass the inputProps through', function () { + + expect(React.findDOMNode(instance).querySelectorAll('.extra-class-name'), 'to have length', 0); + expect(React.findDOMNode(instance).querySelectorAll('#search-input-id'), 'to have length', 0); + }); + }); }); }); }); From db296334db5b788fa89ec66e98ccd394f554db1b Mon Sep 17 00:00:00 2001 From: Dave Brotherstone Date: Tue, 28 Jul 2015 00:20:20 +0200 Subject: [PATCH 19/23] Tests for matchPos and matchProp --- test/Select-test.js | 122 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 122 insertions(+) diff --git a/test/Select-test.js b/test/Select-test.js index 9488e6830d..b5d6d9f19a 100644 --- a/test/Select-test.js +++ b/test/Select-test.js @@ -1377,5 +1377,127 @@ describe('Select', function() { }); }); }); + + describe('matchPos=start', function () { + + beforeEach(function () { + + instance = createControl({ + searchable: true, + matchPos: 'start', + options: defaultOptions + }); + }); + + it('searches only at the start', function () { + + typeSearchText('o'); + var options = React.findDOMNode(instance).querySelectorAll('.Select-option'); + expect(options[0], 'to have text', 'One'); + expect(options, 'to have length', 1); + }); + }); + + describe('matchProp=value', function () { + + beforeEach(function () { + + instance = createControl({ + searchable: true, + matchProp: 'value', + options: [ + { value: 'aaa', label: '111' }, + { value: 'bbb', label: '222' }, + { value: 'ccc', label: 'Three' }, + { value: 'four', label: 'Abcaaa' } + ] + }); + }); + + it('searches only the value', function () { + + typeSearchText('aa'); // Matches value "three", and label "AbcDef" + var options = React.findDOMNode(instance).querySelectorAll('.Select-option'); + expect(options, 'to have length', 1); + expect(options[0], 'to have text', '111'); + }); + }); + + describe('matchProp=label', function () { + + beforeEach(function () { + + instance = createControl({ + searchable: true, + matchProp: 'label', + options: [ + { value: 'aaa', label: 'bbb' }, + { value: 'bbb', label: '222' }, + { value: 'ccc', label: 'Three' }, + { value: 'four', label: 'Abcaaa' } + ] + }); + }); + + it('searches only the value', function () { + + typeSearchText('bb'); // Matches value "three", and label "AbcDef" + var options = React.findDOMNode(instance).querySelectorAll('.Select-option'); + expect(options, 'to have length', 1); + expect(options[0], 'to have text', 'bbb'); + }); + }); + + describe('matchPos=start and matchProp=value', function () { + + beforeEach(function () { + + instance = createControl({ + searchable: true, + matchProp: 'value', + matchPos: 'start', + options: [ + { value: 'aaa', label: '111' }, + { value: 'bbb', label: '222' }, + { value: 'cccaa', label: 'Three' }, + { value: 'four', label: 'aaAbca' } + ] + }); + }); + + it('searches only the value', function () { + + typeSearchText('aa'); // Matches value "three", and label "AbcDef" + var options = React.findDOMNode(instance).querySelectorAll('.Select-option'); + expect(options, 'to have length', 1); + expect(options[0], 'to have text', '111'); + }); + }); + + describe('matchPos=start and matchProp=label', function () { + + beforeEach(function () { + + instance = createControl({ + searchable: true, + matchProp: 'label', + matchPos: 'start', + options: [ + { value: 'aaa', label: 'bbb' }, + { value: 'bbb', label: '222' }, + { value: 'cccbbb', label: 'Three' }, + { value: 'four', label: 'Abcbbb' } + ] + }); + }); + + it('searches only the label', function () { + + typeSearchText('bb'); // Matches value "three", and label "AbcDef" + var options = React.findDOMNode(instance).querySelectorAll('.Select-option'); + expect(options, 'to have length', 1); + expect(options[0], 'to have text', 'bbb'); + }); + }); }); }); From dc2c5b508142a799c60397ceeda1c6bdf9018ead Mon Sep 17 00:00:00 2001 From: Dave Brotherstone Date: Tue, 28 Jul 2015 00:53:29 +0200 Subject: [PATCH 20/23] Test for closing the menu if clicking outside the control --- test/Select-test.js | 91 ++++++++++++++++++++++++++++++--------------- 1 file changed, 60 insertions(+), 31 deletions(-) diff --git a/test/Select-test.js b/test/Select-test.js index b5d6d9f19a..20965de2aa 100644 --- a/test/Select-test.js +++ b/test/Select-test.js @@ -71,6 +71,45 @@ describe('Select', function() { return React.findDOMNode(instance).querySelector('.Select-control'); } + function clickDocument() { + var clickEvent = document.createEvent('MouseEvents'); + clickEvent.initEvent('click', true, true); + document.dispatchEvent(clickEvent); + } + + var createControl = function(props) { + + + onChange = sinon.spy(); + // Render an instance of the component + instance = TestUtils.renderIntoDocument( + - ); - - // Focus on the input, such that mouse events are accepted - var searchInstance = React.findDOMNode(instance.getInputNode()); - searchInputNode = null; - if (searchInstance) { - searchInputNode = searchInstance.querySelector('input'); - if (searchInputNode) { - TestUtils.Simulate.focus(searchInputNode); - } - } - return instance; - - }; describe('className', function () { @@ -1500,4 +1508,25 @@ describe('Select', function() { }); }); }); + + describe('clicking outside', function () { + + beforeEach(function () { + + instance = createControl({ + options: defaultOptions + }); + }); + + it('closes the menu', function () { + + TestUtils.Simulate.mouseDown(getSelectControl(instance)); + expect(React.findDOMNode(instance), 'queried for', '.Select-option', + 'to have length', 4); + + clickDocument(); + expect(React.findDOMNode(instance).querySelectorAll('.Select-option'), + 'to have length', 0); + }); + }) }); From 2173636c0d31c84ffe42589bfd72bdb7543149d3 Mon Sep 17 00:00:00 2001 From: Dave Brotherstone Date: Tue, 28 Jul 2015 01:04:55 +0200 Subject: [PATCH 21/23] Tests for clicking arrow Second click closes the options Right click is ignored --- test/Select-test.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/test/Select-test.js b/test/Select-test.js index 20965de2aa..901e6b9888 100644 --- a/test/Select-test.js +++ b/test/Select-test.js @@ -306,6 +306,21 @@ describe('Select', function() { 'to have text', 'One'); }); + it('should close the options one the second click on the arrow', function () { + var selectArrow = React.findDOMNode(instance).querySelector('.Select-arrow'); + TestUtils.Simulate.mouseDown(selectArrow); + expect(React.findDOMNode(instance).querySelectorAll('.Select-option'), 'to have length', 3); + + TestUtils.Simulate.mouseDown(selectArrow); + expect(React.findDOMNode(instance).querySelectorAll('.Select-option'), 'to have length', 0); + }); + + it('should ignore a right mouse click on the arrow', function () { + var selectArrow = React.findDOMNode(instance).querySelector('.Select-arrow'); + TestUtils.Simulate.mouseDown(selectArrow, { type: 'mousedown', button: 1 }); + expect(React.findDOMNode(instance).querySelectorAll('.Select-option'), 'to have length', 0); + }); + describe('after mouseEnter and leave of an option', function () { From 6c1a3274712a88d074439a67619e4a6983aed957 Mon Sep 17 00:00:00 2001 From: Dave Brotherstone Date: Tue, 28 Jul 2015 01:25:05 +0200 Subject: [PATCH 22/23] Tests for onBlur and onFocus --- test/Select-test.js | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/test/Select-test.js b/test/Select-test.js index 901e6b9888..37df87eaba 100644 --- a/test/Select-test.js +++ b/test/Select-test.js @@ -1522,6 +1522,44 @@ describe('Select', function() { expect(options[0], 'to have text', 'bbb'); }); }); + + describe('onBlur', function () { + + var onBlur; + + it('calls the onBlur prop when blurring the input', function () { + + onBlur = sinon.spy(); + + instance = createControl({ + options: defaultOptions, + onBlur: onBlur + }); + + TestUtils.Simulate.blur(searchInputNode); + expect(onBlur, 'was called once'); + }); + }); + + describe('onFocus', function () { + + var onFocus; + + beforeEach(function () { + + onFocus = sinon.spy(); + + instance = createControl({ + options: defaultOptions, + onFocus: onFocus + }); + }); + + it('calls the onFocus prop when focusing the control', function () { + + expect(onFocus, 'was called once'); + }); + }); }); describe('clicking outside', function () { From 61ab9a80a741fe37b647058b29693bd2b73e9c91 Mon Sep 17 00:00:00 2001 From: Dave Brotherstone Date: Tue, 28 Jul 2015 01:25:33 +0200 Subject: [PATCH 23/23] Fix lint errors with tests --- test/Select-test.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/test/Select-test.js b/test/Select-test.js index 37df87eaba..62b679f355 100644 --- a/test/Select-test.js +++ b/test/Select-test.js @@ -590,16 +590,16 @@ describe('Select', function() { asyncOptions.withArgs('te').callsArgWith(1, null, { options: [ - {value: 'test', label: 'TEST one'}, - {value: 'test2', label: 'TEST two'}, - {value: 'tell', label: 'TELL three'} + { value: 'test', label: 'TEST one' }, + { value: 'test2', label: 'TEST two' }, + { value: 'tell', label: 'TELL three' } ] }); asyncOptions.withArgs('tes').callsArgWith(1, null, { options: [ - {value: 'test', label: 'TEST one'}, - {value: 'test2', label: 'TEST two'} + { value: 'test', label: 'TEST one' }, + { value: 'test2', label: 'TEST two' } ] }); @@ -682,9 +682,9 @@ describe('Select', function() { asyncOptions.withArgs('te').callsArgWith(1, null, { options: [ - {value: 'test', label: 'TEST one'}, - {value: 'test2', label: 'TEST two'}, - {value: 'tell', label: 'TELL three'} + { value: 'test', label: 'TEST one' }, + { value: 'test2', label: 'TEST two' }, + { value: 'tell', label: 'TELL three' } ], complete: true }); @@ -1581,5 +1581,5 @@ describe('Select', function() { expect(React.findDOMNode(instance).querySelectorAll('.Select-option'), 'to have length', 0); }); - }) + }); });