describe('chosen.ajaxaddition', function(){ var select = $('#test-select'), multiSelect = $('#test-multi-select'), space = $('#test-space'), emptySelect = $('#empty-select'); beforeEach(function(){ space.append(select.clone()); }); afterEach(function(){ space.html(''); }); it("should initial chosen", function(){ var mock = sinon.mock($.fn); mock.expects('chosen').once(); $('select', space).ajaxChosen({},{}); expect(mock.verify()).to.be.true; }); it("should not add an extra empty option if one is present", function(){ expect($('select option', space)).to.have.length(1); $('select', space).ajaxChosen({},{}); expect($('select option', space)).to.have.length(1); }); it("should proxy chosen options", function(){ var mock = sinon.mock($.fn), args = {no_results_text: "No results matched"}; mock.expects('chosen').once().withExactArgs(args); $('select', space).ajaxChosen({},{},args); expect(mock.verify()).to.be.true; }); it("should not fire of ajax for non-essential keys", function(){ var keyCodes = [9,13,16,17,18,19,20,27,33,34,35,36,37,38,39,40,44,45,144,145,91,93,224,112,113,114,115,116,117,118,119,120,121,122,123], key, chosen, input, server = sinon.fakeServer.create(), clock = sinon.useFakeTimers(); server.respondWith( '/search', [200, { 'Content-Type': 'application/json' }, '{ q: "", results: []}'] ); chosen = $('select', space).ajaxChosen({ dataType: 'json', type: 'POST', url:'/search' },{}).next(); chosen.trigger('click'); clock.tick(50);//AbstractChosen.prototype.input_focus -> fires focus logic after 50 input = $('input', chosen).val('word'); for(var i=0, len=keyCodes.length; i < len; i++){ key = $.Event('keyup'); key.which = keyCodes[i]; input.trigger(key); }; clock.tick(750); server.restore(); clock.restore(); expect(server.requests).to.have.length(0); }); it("should fire ajax if non empty value was pasted to the input field", function() { var chosen, input, server = sinon.fakeServer.create(), clock = sinon.useFakeTimers(); server.respondWith( '/search', [200, { 'Content-Type': 'application/json' }, '{ q: "por", results: [{ id: 1, text: "porshe" }] }'] ); chosen = $('select', space).ajaxChosen({ dataType: 'json', type: 'POST', url:'/search' },{}).next(); chosen.trigger('click'); clock.tick(50);//AbstractChosen.prototype.input_focus -> fires focus logic after 50 input = $('input', chosen).val('por'); input.focus(); // Slow down Chrome to focus on the input field clock.tick(50); expect(input.is(":focus")).to.be.true; input.trigger('paste'); clock.tick(750); expect(server.requests).to.have.length(1); server.respond(); expect(input.val()).to.equal('por'); server.restore(); clock.restore(); }); it("should not fire ajax if value was pasted without focused input field", function() { var chosen, input, server = sinon.fakeServer.create(), clock = sinon.useFakeTimers(); server.respondWith( '/search', [200, { 'Content-Type': 'application/json' }, '{ q: "", results: [] }'] ); chosen = $('select', space).ajaxChosen({ dataType: 'json', type: 'POST', url:'/search' },{}).next(); chosen.trigger('click'); clock.tick(50);//AbstractChosen.prototype.input_focus -> fires focus logic after 50 input = $('input', chosen).val('por'); input.blur(); clock.tick(50); expect(input.is(":focus")).to.be.false; input.trigger('paste'); expect(server.requests).to.have.length(0); server.restore(); clock.restore(); }); it("should not fire of ajax if the whole string has been deleted", function() { var chosen, input, key, server = sinon.fakeServer.create(), clock = sinon.useFakeTimers(), response; response = { q: "fer", results: [{ id: 1, text: "ferrari" }] }; server.respondWith( '/search', [200, { 'Content-Type': 'application/json' }, JSON.stringify(response)] ); chosen = $('select', space).ajaxChosen({ dataType: 'json', type: 'POST', url:'/search' },{}).next(); chosen.trigger('click'); clock.tick(50);//AbstractChosen.prototype.input_focus -> fires focus logic after 50 input = $('input', chosen).val('fe'); key = $.Event('keyup'); key.which = 82; input.trigger(key); clock.tick(750); server.respond(); expect(input.val()).to.equal('fer'); key = $.Event('keyup'); key.which = 8; // Delete everything from input var query = 'fer'; for(var i = query.length - 1; i >= 0; --i) { input.val(query.substr(0, i)); input.trigger(key); } clock.tick(750); expect(server.requests).to.have.length(1); expect(input.val()).to.be.empty; server.restore(); clock.restore(); }); it("should allow the last character to be deleted", function(){ var chosen, input, key, returnQuery, clock = sinon.useFakeTimers(), xhr = sinon.useFakeXMLHttpRequest(), requests = []; xhr.onCreate = function (xhr) { requests.push(xhr) }; chosen = $('select', space).ajaxChosen({ dataType: 'json', type: 'POST', url: '/search' },{}).next(); chosen.trigger('click'); clock.tick(50);//AbstractChosen.prototype.input_focus -> fires focus logic after 50 input = $('input', chosen).val('monkey'); key = $.Event('keyup'); key.which = 32; //fire off request input.trigger(key); clock.tick(750); //respond returnQuery = 'monkey'; requests[0].respond(200, { "Content-Type": "application/json" }, '{ "q": "'+returnQuery+'", "results": [{"id":1, "text":"first monkey"}]}'); //assert expect(input.val()).to.equal(returnQuery); expect($('.chosen-results li', chosen)).to.have.length(1); //delete all but 1 character input.val('m'); key = $.Event('keyup'); key.which = 32; //fire request input.trigger(key); clock.tick(750); //delete all characters before server responds key = $.Event('keyup'); key.which = 8; input.val(''); input.trigger(key); returnQuery = 'm'; requests[1].respond(200, { "Content-Type": "application/json" }, '{ "q": "'+returnQuery+'", "results": [{"id":1, "text":"first monkey"}]}'); //assert that the text box is empty and doesn't contain the last response q expect(input.val()).to.be.empty; expect($('.chosen-results li', chosen)).to.have.length(0); }); it("should allow the last character to be deleted with selected option", function(){ var chosen, input, key, returnQuery, clock = sinon.useFakeTimers(), xhr = sinon.useFakeXMLHttpRequest(), requests = [], selectedId; xhr.onCreate = function (xhr) { requests.push(xhr) }; selectedId = 'bananas'; $('select', space).append('<option value="bananas">monkeys eat</option>').val(selectedId); chosen = $('select', space).ajaxChosen({ dataType: 'json', type: 'POST', url: '/search' },{}).next(); expect($('select', space).val()).to.equal(selectedId); expect($('select option', space)).to.have.length(2); chosen.trigger('click'); clock.tick(50);//AbstractChosen.prototype.input_focus -> fires focus logic after 50 input = $('input', chosen).val('monkey'); key = $.Event('keyup'); key.which = 83; input.trigger(key); clock.tick(750); requests[0].respond(200, { "Content-Type": "application/json" }, '{ "q": "monkeys", "results": [{"id":1, "text":"first monkeys"}]}'); expect($('select', space).val()).to.equal(selectedId); expect($('select option', space)).to.have.length(3); //delete all but 1 character input.val('m'); key = $.Event('keyup'); key.which = 32; //fire request input.trigger(key); clock.tick(750); //delete all characters before server responds key = $.Event('keyup'); key.which = 8; input.val(''); input.trigger(key); requests[1].respond(200, { "Content-Type": "application/json" }, '{ "q": "m", "results": [{"id":1, "text":"first monkey"}]}'); //assert that the text box is empty and doesn't contain the last response q expect(input.val()).to.be.empty; expect($('select', space).val()).to.equal(selectedId); expect($('select option', space)).to.have.length(2); }); it("should allow the minLength characters to be deleted", function(){ var chosen, input, key, returnQuery, clock = sinon.useFakeTimers(), xhr = sinon.useFakeXMLHttpRequest(), requests = [], minLength = 3; xhr.onCreate = function (xhr) { requests.push(xhr) }; chosen = $('select', space).ajaxChosen({ dataType: 'json', type: 'POST', url: '/search' },{ minLength: minLength }).next(); chosen.trigger('click'); clock.tick(50);//AbstractChosen.prototype.input_focus -> fires focus logic after 50 input = $('input', chosen).val('monkey'); key = $.Event('keyup'); key.which = 32; //fire off request input.trigger(key); clock.tick(750); //respond returnQuery = 'monkey'; requests[0].respond(200, { "Content-Type": "application/json" }, '{ "q": "'+returnQuery+'", "results": [{"id":1, "text":"first monkey"}]}'); expect(input.val()).to.equal(returnQuery); expect($('.chosen-results li', chosen)).to.have.length(1); //delete all but minLength character input.val('mon'); key = $.Event('keyup'); key.which = 32; //fire request input.trigger(key); clock.tick(750); //delete minLength - 1 characters before server responds //if this still matches a result from the previous response, you are going to have it highlighted by chosen. key = $.Event('keyup'); key.which = 8; input.val('ba'); input.trigger(key); returnQuery = 'mon'; requests[1].respond(200, { "Content-Type": "application/json" }, '{ "q": "'+returnQuery+'", "results": [{"id":1, "text":"first monkey"}]}'); //assert that the text box is empty and doesn't contain the last response q expect(input.val()).to.equal('ba'); expect($('.chosen-results li:visible', chosen)).to.have.length(0);//no-results is hidden by processValue }); it("should allow the minLength characters to be deleted with selected option", function(){ var chosen, input, key, returnQuery, clock = sinon.useFakeTimers(), xhr = sinon.useFakeXMLHttpRequest(), requests = [], selectedId, minLength = 3; xhr.onCreate = function (xhr) { requests.push(xhr) }; selectedId = 'bananas'; $('select', space).append('<option value="bananas">monkeys eat</option>').val(selectedId); chosen = $('select', space).ajaxChosen({ dataType: 'json', type: 'POST', url: '/search' },{ minLength: 3 }).next(); expect($('select', space).val()).to.equal(selectedId); expect($('select option', space)).to.have.length(2); chosen.trigger('click'); clock.tick(50);//AbstractChosen.prototype.input_focus -> fires focus logic after 50 input = $('input', chosen).val('monkey'); key = $.Event('keyup'); key.which = 83; input.trigger(key); clock.tick(750); requests[0].respond(200, { "Content-Type": "application/json" }, '{ "q": "monkeys", "results": [{"id":1, "text":"first monkeys"}]}'); expect($('select', space).val()).to.equal(selectedId); expect($('select option', space)).to.have.length(3); //delete all but minLength character input.val('mon'); key = $.Event('keyup'); key.which = 32; //fire request input.trigger(key); clock.tick(750); //minLength - 1 characters before server responds key = $.Event('keyup'); key.which = 8; input.val('ba'); input.trigger(key); requests[1].respond(200, { "Content-Type": "application/json" }, '{ "q": "mon", "results": [{"id":1, "text":"first monkey"}]}'); //assert that the text box is "empty" and doesn't contain the last response q expect(input.val()).to.equal('ba'); expect($('select', space).val()).to.equal(selectedId); expect($('select option', space)).to.have.length(2); }); describe('options', function(){ beforeEach(function(){ this.server = sinon.fakeServer.create(); this.clock = sinon.useFakeTimers(); this.response = { q: "banana", complex: { results: [{id:1, text:"Chiquita banana"},{id:2, text:"Dole banana"}] }}; this.server.respondWith( '/search', [200, { 'Content-Type': 'application/json' }, JSON.stringify(this.response)] ); }); afterEach(function(){ this.server.restore(); this.clock.restore(); }); it('should initial chosen with empty options', function(){ var mock = sinon.mock($.fn); mock.expects('chosen').once(); $('select', space).ajaxChosen({}); expect(mock.verify()).to.be.true; }); it('should use loadingImg while processing', function(){ var chosen, input, key; chosen = $('select', space).ajaxChosen({ dataType: 'json', type: 'POST', url:'/search' },{ loadingImg: 'img/l04der.gif' }).next(); chosen.trigger('click'); this.clock.tick(50);//AbstractChosen.prototype.input_focus -> fires focus logic after 50 input = $('input', chosen).val('banan'); expect(input.css('background-image')).to.match(/chosen-sprite\.png/i); key = $.Event('keyup'); key.which = 65; input.trigger(key); expect(input.css('background-image')).to.match(/l04der\.gif/i); }); it('should apply the processItems function to response data', function(){ var chosen, input, key, ajaxChosenOptions = { processItems: function(data){ return data.complex.results; } }; sinon.spy(ajaxChosenOptions, "processItems"); chosen = $('select', space).ajaxChosen({ dataType: 'json', type: 'POST', url:'/search' },ajaxChosenOptions).next(); chosen.trigger('click'); this.clock.tick(50);//AbstractChosen.prototype.input_focus -> fires focus logic after 50 input = $('input', chosen).val('banan'); key = $.Event('keyup'); key.which = 65; input.trigger(key); this.clock.tick(750); this.server.respond(); expect(ajaxChosenOptions.processItems.calledWithExactly(this.response)).to.be.true; expect($('select option', space)).to.have.length(3); }); it('should use return value of useAjax to determine if ajax should be fired after keyups', function(){ var chosen, input, key, ajaxChosenOptions = { useAjax: function(e){ return false; } }; sinon.spy(ajaxChosenOptions, "useAjax"); chosen = $('select', space).ajaxChosen({ dataType: 'json', type: 'POST', url:'/search' },ajaxChosenOptions).next(); chosen.trigger('click'); this.clock.tick(50);//AbstractChosen.prototype.input_focus -> fires focus logic after 50 input = $('input', chosen).val('banan'); key = $.Event('keyup'); key.which = 65; input.trigger(key); this.clock.tick(750); expect(ajaxChosenOptions.useAjax.calledWithExactly(key)).to.be.true; expect(this.server.requests).to.have.length(0); }); it("should use generateUrl to create dynmic url", function(){ var chosen, input, key, ajaxChosenOptions = { generateUrl: function(q){ return '/search'; } }; sinon.spy(ajaxChosenOptions, "generateUrl"); chosen = $('select', space).ajaxChosen({ dataType: 'json', type: 'POST', url:'/abc123' },ajaxChosenOptions).next(); chosen.trigger('click'); this.clock.tick(50);//AbstractChosen.prototype.input_focus -> fires focus logic after 50 input = $('input', chosen).val('banan'); key = $.Event('keyup'); key.which = 65; input.trigger(key); this.clock.tick(750); expect(ajaxChosenOptions.generateUrl.calledOnce).to.be.true; expect(ajaxChosenOptions.generateUrl.calledWithExactly('banan')).to.be.true; expect(this.server.requests).to.have.length(1); }); it('should use a default minLength of 1 character', function(){ var chosen, input, key; chosen = $('select', space).ajaxChosen({ dataType: 'json', type: 'POST', url:'/search' },{}).next(); chosen.trigger('click'); this.clock.tick(50);//AbstractChosen.prototype.input_focus -> fires focus logic after 50 input = $('input', chosen).val('b'); key = $.Event('keyup'); key.which = 32; input.trigger(key); expect( $.trim(input.val()) ).to.have.length(1); this.clock.tick(750); expect(this.server.requests).to.have.length(1); }); it('should not fire off server requests until minLength is met', function(){ var chosen, input, key, minLength = 3; chosen = $('select', space).ajaxChosen({ dataType: 'json', type: 'POST', url:'/search' },{ minLength: minLength }).next(); chosen.trigger('click'); this.clock.tick(50);//AbstractChosen.prototype.input_focus -> fires focus logic after 50 input = $('input', chosen); for(var m = 0; m < minLength-1; m++){ input.val($.trim(input.val()) + 'b'); key = $.Event('keyup'); key.which = 32; input.trigger(key); expect( $.trim(input.val()) ).to.have.length(m+1); this.clock.tick(750); expect(this.server.requests).to.have.length(0); } input.val($.trim(input.val()) + 'b'); key = $.Event('keyup'); key.which = 32; input.trigger(key); expect( $.trim(input.val()) ).to.have.length(minLength); this.clock.tick(750); expect(this.server.requests).to.have.length(1); }); }); describe('ajaxOptions', function(){ beforeEach(function(){ this.server = sinon.fakeServer.create(); this.clock = sinon.useFakeTimers(); this.server.respondWith( '/search', [200, { 'Content-Type': 'application/json' }, '{ "q": "banana", "results": []}'] ); }); afterEach(function(){ this.server.restore(); this.clock.restore(); }); it('should fire ajax options callback on completion', function(){ var chosen, input, key, successFn = sinon.mock(); chosen = $('select', space).ajaxChosen({ dataType: 'json', type: 'POST', url: '/search', success: successFn },{}).next(); chosen.trigger('click'); this.clock.tick(50);//AbstractChosen.prototype.input_focus -> fires focus logic after 50 input = $('input', chosen).val('banan'); key = $.Event('keyup'); key.which = 65; input.trigger(key); this.clock.tick(750); this.server.respond(); successFn.once().withArgs({ q: "", results: []}); expect(successFn.verify()).to.be.true; }); it('should handle and preserve ajax.data in hash format', function(){ var chosen, input, key, aData, expectedData; sinon.spy(jQuery, "ajax"); chosen = $('select', space).ajaxChosen({ dataType: 'json', type: 'POST', url: '/search', data: {'somekey':'somevalue'} },{}).next(); chosen.trigger('click'); this.clock.tick(50);//AbstractChosen.prototype.input_focus -> fires focus logic after 50 input = $('input', chosen).val('banana'); key = $.Event('keyup'); key.which = 65; input.trigger(key); this.clock.tick(750); expect(jQuery.ajax.calledOnce).to.be.true; aData = jQuery.ajax.getCall(0).args[0].data; expectedData = {'somekey':'somevalue', 'data' : { 'q': 'banana' }}; expect(aData.somekey).to.equal(expectedData.somekey); expect(aData.data.q).to.equal(expectedData.data.q); jQuery.ajax.restore(); }); it('should handle and preserve ajax.data in array format', function(){ var chosen, input, key, aData, expectedData; sinon.spy(jQuery, "ajax"); chosen = $('select', space).ajaxChosen({ dataType: 'json', type: 'POST', url: '/search', data: [{'name':'keyboard', 'value':'cat'}] },{}).next(); chosen.trigger('click'); this.clock.tick(50);//AbstractChosen.prototype.input_focus -> fires focus logic after 50 input = $('input', chosen).val('banana'); key = $.Event('keyup'); key.which = 65; input.trigger(key); this.clock.tick(750); expect(jQuery.ajax.calledOnce).to.be.true; aData = jQuery.ajax.getCall(0).args[0].data; expectedData = [{'name':'keyboard', 'value':'cat'},{'name':'q', 'value':'banana'}] $.each(aData, function(i, v){ expect(expectedData[i].name).to.equal(v.name); expect(expectedData[i].value).to.equal(v.value); }) jQuery.ajax.restore(); }); //in array format it shouldn't re-add/duplicate the q - value pair in the array }); describe('results element', function(){ beforeEach(function(){ this.server = sinon.fakeServer.create(); this.clock = sinon.useFakeTimers(); }); afterEach(function(){ this.server.restore(); this.clock.restore(); }); it('should show no results if response has no items', function(){ var chosen, input, key; this.server.respondWith( '/search', [200, { 'Content-Type': 'application/json' }, '{ "q": "banana", "results": []}'] ); chosen = $('select', space).ajaxChosen({ dataType: 'json', type: 'POST', url: '/search' },{}).next(); chosen.trigger('click'); this.clock.tick(50);//AbstractChosen.prototype.input_focus -> fires focus logic after 50 input = $('input', chosen).val('banan'); key = $.Event('keyup'); key.which = 65; input.trigger(key); this.clock.tick(750); this.server.respond(); expect($('.no-results', chosen).is(':visible')).to.be.true; }); it('should hide no results if response has at least 1 item', function(){ var chosen, input, key; this.server.respondWith( '/search', [200, { 'Content-Type': 'application/json' }, '{ "q": "banana", "results": [{"id":1, "text":"Chiquita banana"}]}'] ); chosen = $('select', space).ajaxChosen({ dataType: 'json', type: 'POST', url: '/search' },{}).next(); chosen.trigger('click'); this.clock.tick(50);//AbstractChosen.prototype.input_focus -> fires focus logic after 50 input = $('input', chosen).val('banan'); key = $.Event('keyup'); key.which = 65; input.trigger(key); this.clock.tick(750); this.server.respond(); expect($('.no-results', chosen).is(':visible')).to.be.false; }); it('should hide no results even if the input box is cleared', function(){ var chosen, input, key; this.server.respondWith( '/search', [200, { 'Content-Type': 'application/json' }, '{ "q": "banana", "results": []}'] ); chosen = $('select', space).ajaxChosen({ dataType: 'json', type: 'POST', url: '/search' },{}).next(); chosen.trigger('click'); this.clock.tick(50);//AbstractChosen.prototype.input_focus -> fires focus logic after 50 input = $('input', chosen).val('banan'); key = $.Event('keyup'); key.which = 65; input.trigger(key); this.clock.tick(750); this.server.respond(); expect($('.no-results', chosen).is(':visible')).to.be.true; input.val(''); key = $.Event('keyup'); key.which = 8; input.trigger(key); this.clock.tick(750); expect($('.no-results', chosen).is(':visible')).to.be.false; }); it('should not show no results if minLength is not met', function(){ var chosen, input, key, minLength = 3; chosen = $('select', space).ajaxChosen({ dataType: 'json', type: 'POST', url: '/search' },{ minLength: minLength }).next(); chosen.trigger('click'); this.clock.tick(50);//AbstractChosen.prototype.input_focus -> fires focus logic after 50 input = $('input', chosen).val('ba'); key = $.Event('keyup'); key.which = 32; input.trigger(key); expect( $.trim(input.val()) ).to.have.length(minLength - 1); this.clock.tick(750); expect($('.no-results', chosen).is(':visible')).to.be.false; }); it('should hide no results when minLength is not met', function(){ var chosen, input, key, minLength = 3; this.server.respondWith( '/search', [200, { 'Content-Type': 'application/json' }, '{ "q": "banana", "results": []}'] ); chosen = $('select', space).ajaxChosen({ dataType: 'json', type: 'POST', url: '/search' },{ minLength: minLength }).next(); chosen.trigger('click'); this.clock.tick(50);//AbstractChosen.prototype.input_focus -> fires focus logic after 50 input = $('input', chosen).val('banan'); key = $.Event('keyup'); key.which = 65; input.trigger(key); this.clock.tick(750); this.server.respond(); expect($('.no-results', chosen).is(':visible')).to.be.true; input.val('ba'); key = $.Event('keyup'); key.which = 32; input.trigger(key); expect( $.trim(input.val()) ).to.have.length(minLength - 1); this.clock.tick(750); expect($('.no-results', chosen).is(':visible')).to.be.false; }); it('should hide no results after chosen does a js match and when minLength is not met', function(){ var chosen, input, key, minLength = 3; this.server.respondWith( '/search', [200, { 'Content-Type': 'application/json' }, '{ "q": "banana", "results": [{"id":1, "text":"banana yay"}]}'] ); chosen = $('select', space).ajaxChosen({ dataType: 'json', type: 'POST', url: '/search' },{ minLength: minLength }).next(); chosen.trigger('click'); this.clock.tick(100);//AbstractChosen.prototype.input_focus -> fires focus logic after 50 input = $('input', chosen).val('banan'); key = $.Event('keyup'); key.which = 65; input.trigger(key); this.clock.tick(750); this.server.respond(); expect($('.chosen-results li:not(.no-results)')).to.have.length(1); expect($('.no-results', chosen).is(':visible')).to.be.false; input.val('mo'); key = $.Event('keyup'); key.which = 32; input.trigger(key); expect( $.trim(input.val()) ).to.have.length(minLength - 1); this.clock.tick(750); expect($('.no-results', chosen).is(':visible')).to.be.false; }); it("should hide the loading image if the user backs out of the searchd with minLength characters", function(){ var chosen, input, key, minLength = 3; chosen = $('select', space).ajaxChosen({ dataType: 'json', type: 'POST', url: '/search' },{ minLength: minLength }).next().width('100px'); chosen.trigger('click'); this.clock.tick(50);//AbstractChosen.prototype.input_focus -> fires focus logic after 50 input = $('input', chosen).val('monkey'); key = $.Event('keyup'); key.which = 32; //start processing request input.trigger(key); this.clock.tick(250); //server request not fired yet expect(this.server.requests).to.have.length(0); //expect processing image expect(input.prop('style')['background']).to.match(/loading\.gif/i); //delete all but minLength character input.val('mo'); key = $.Event('keyup'); key.which = 8; //"process request" input.trigger(key); this.clock.tick(750); expect(this.server.requests).to.have.length(0); //processing image should be gone expect(input.prop('style')['background']).to.not.match(/loading\.gif/i); }); it('should display error if no processItems option is supplied and there is no results key in data returned', function(){ var chosen, input, key; sinon.spy(console, "log"); this.server.respondWith( '/search', [200, { 'Content-Type': 'application/json' }, '{ "q": "banana", "dataz": [{"id":1, "text":"Chiquita banana"}]}'] ); chosen = $('select', space).ajaxChosen({ dataType: 'json', type: 'POST', url: '/search' },{}).next(); chosen.trigger('click'); this.clock.tick(50);//AbstractChosen.prototype.input_focus -> fires focus logic after 50 input = $('input', chosen).val('banan'); key = $.Event('keyup'); key.which = 65; input.trigger(key); this.clock.tick(750); this.server.respond(); expect(console.log.calledOnce).to.be.true; console.log.restore(); }); }); it('should not fire off ajax if trim(q) is of 0 length', function(){ var chosen, input, key; this.server = sinon.fakeServer.create(); this.clock = sinon.useFakeTimers(); chosen = $('select', space).ajaxChosen({ dataType: 'json', type: 'POST', url: '/search' },{}).next(); chosen.trigger('click'); this.clock.tick(50);//AbstractChosen.prototype.input_focus -> fires focus logic after 50 input = $('input', chosen).val(''); key = $.Event('keyup'); key.which = 32; input.trigger(key); this.clock.tick(750); this.server.respond(); expect(this.server.requests).to.have.length(0); this.server.restore(); this.clock.restore(); }); it('should not trim the space at the end of a users input - it might be what they actually meant', function(){ var chosen, input, key; this.server = sinon.fakeServer.create(); this.clock = sinon.useFakeTimers(); this.server.respondWith( '/search', [200, { 'Content-Type': 'application/json' }, '{ "q": "Trailing Spaces are cool yo ", "results": [{"id":1, "text":"I hate trailing spaces"}]}'] ); chosen = $('select', space).ajaxChosen({ dataType: 'json', type: 'POST', url: '/search' },{}).next(); chosen.trigger('click'); this.clock.tick(50);//AbstractChosen.prototype.input_focus -> fires focus logic after 50 input = $('input', chosen).val('Trailing Spaces are cool yo '); key = $.Event('keyup'); key.which = 32; input.trigger(key); this.clock.tick(750); this.server.respond(); expect(this.server.requests[0].requestBody).to.equal("data%5Bq%5D=Trailing+Spaces+are+cool+yo+"); }); it("should reselect option on single select when chosen input is blurred", function(){ var clock = sinon.useFakeTimers(), xhr = sinon.useFakeXMLHttpRequest(), chosen, input, key, requests = []; xhr.onCreate = function (xhr) { requests.push(xhr) }; chosen = $('select', space).ajaxChosen({ dataType: 'json', type: 'POST', url: '/search' },{}).next(); chosen.trigger('click'); clock.tick(50);//AbstractChosen.prototype.input_focus -> fires focus logic after 50 //first request input = $('input', chosen).val('monkey'); key = $.Event('keyup'); key.which = 32; input.trigger(key); clock.tick(750); //return response var selectedId = "1", selectedText = "first monkey"; requests[0].respond(200, { "Content-Type": "application/json" }, '{ "q": "monkey", "results": [{"id":'+selectedId+', "text":"'+selectedText+'"}]}'); //select the item that is returned $('.active-result', chosen).trigger('mouseup'); expect($('select', space).val()).to.equal(selectedId); expect($('> a.chosen-single', chosen).text()).to.equal(selectedText); //go for the second request.. chosen.trigger('click'); clock.tick(50);//AbstractChosen.prototype.input_focus -> fires focus logic after 50 input = $('input', chosen).val('banana'); key = $.Event('keyup'); key.which = 32; input.trigger(key); clock.tick(750); expect(input.val()).to.equal('banana'); requests[1].respond(200, { "Content-Type": "application/json" }, '{ "q": "banana", "results": [{"id":2, "text":"banana tree"}]}'); //hit the escape key to trigger input blur key = $.Event('keyup'); key.which = 27; //esc key input.trigger(key); //ensure what was selected, stays selected expect($('select', space).val()).to.equal(selectedId); expect($('> a.chosen-single', chosen).text()).to.equal(selectedText); clock.restore(); xhr.restore(); }); describe('duplication', function(){ beforeEach(function(){ this.server = sinon.fakeServer.create(); this.clock = sinon.useFakeTimers(); }); afterEach(function(){ this.server.restore(); this.clock.restore(); }); describe('single select', function(){ it('should not duplicate pre-selected result', function(){ var chosen, input, key, selectedId; this.server.respondWith( '/search', [200, { 'Content-Type': 'application/json' }, '{ "q": "monkeys", "results": [{"id":"bananas", "text":"monkeys eat"}]}'] ); selectedId = 'bananas'; $('select', space).append('<option value="bananas">monkeys eat</option>').val(selectedId); chosen = $('select', space).ajaxChosen({ dataType: 'json', type: 'POST', url: '/search' },{}).next(); expect($('select option', space)).to.have.length(2); chosen.trigger('click'); this.clock.tick(50);//AbstractChosen.prototype.input_focus -> fires focus logic after 50 input = $('input', chosen).val('monkey'); key = $.Event('keyup'); key.which = 83; input.trigger(key); this.clock.tick(750); this.server.respond(); expect($('select option', space)).to.have.length(2);//empty + pre-selected result (not duplicate) expect($('select option', space).eq(1).text()).to.equal('monkeys eat'); //check option to make sure it has monkeys eat option //have the monkeys eat result once expect($('.chosen-results li', chosen)).to.have.length(1); expect($('.chosen-results li', chosen).text()).to.equal('monkeys eat'); //pre-selected result should still be selected expect($('.chosen-results li.result-selected', chosen).text()).to.equal('monkeys eat'); expect($('select', space).val()).to.equal(selectedId); }); }); describe('multi-select', function(){ beforeEach(function(){ space.html(''); space.append(multiSelect.clone()); }); it('should not duplicate pre-selected single result', function(){ var select, chosen, input, key, selectedIds; this.server.respondWith( '/search', [200, { 'Content-Type': 'application/json' }, '{ "q": "toyota", "results": [{"id":"1", "text":"toyota tundra"},{"id":"2", "text":"toyota camery"}]}'] ); selectedIds = ['1']; select = $('select', space); select.append('<option value="1">toyota tundra</option><option value="2">toyota camery</option>').val(selectedIds); chosen = select.ajaxChosen({ dataType: 'json', type: 'POST', url: '/search' },{}).next(); expect($('option', select)).to.have.length(3); $('input', chosen).trigger('click'); this.clock.tick(50);//AbstractChosen.prototype.input_focus -> fires focus logic after 50 input = $('input', chosen).val('toyot'); key = $.Event('keyup'); key.which = 32; input.trigger(key); this.clock.tick(750); this.server.respond(); expect($('option', select)).to.have.length(3); expect($('.chosen-results li', chosen)).to.have.length(2); expect($('option', select).eq(1).text()).to.equal('toyota tundra'); expect($('option', select).eq(2).text()).to.equal('toyota camery'); expect($('.chosen-results li', chosen).eq(0).text()).to.equal('toyota tundra'); expect($('.chosen-results li', chosen).eq(1).text()).to.equal('toyota camery'); expect($('.chosen-choices li.search-choice',chosen)).to.have.length(1); expect($('.chosen-choices li.search-choice',chosen).text()).to.equal('toyota tundra'); //weird how chai doesn't make ['1'] === ['1'] expect(select.val()[0]).to.have.equal(selectedIds[0]); }); it('should not duplicate pre-selected results', function(){ var select, chosen, input, key, selectedIds; this.server.respondWith( '/search', [200, { 'Content-Type': 'application/json' }, '{ "q": "toyota", "results": [{"id":"1", "text":"toyota tundra"},{"id":"2", "text":"toyota camery"}]}'] ); selectedIds = ['1','2']; select = $('select', space); select.append('<option value="1">toyota tundra</option><option value="2">toyota camery</option>').val(selectedIds); chosen = select.ajaxChosen({ dataType: 'json', type: 'POST', url: '/search' },{}).next(); expect($('option', select)).to.have.length(3); $('input', chosen).trigger('click'); this.clock.tick(50);//AbstractChosen.prototype.input_focus -> fires focus logic after 50 input = $('input', chosen).val('toyot'); key = $.Event('keyup'); key.which = 32; input.trigger(key); this.clock.tick(750); this.server.respond(); expect($('option', select)).to.have.length(3); expect($('.chosen-results li', chosen).not('.no-results')).to.have.length(2); expect($('option', select).eq(1).text()).to.equal('toyota tundra'); expect($('option', select).eq(2).text()).to.equal('toyota camery'); expect($('.chosen-results li', chosen).not('.no-results').eq(0).text()).to.equal('toyota tundra'); expect($('.chosen-results li', chosen).not('.no-results').eq(1).text()).to.equal('toyota camery'); expect($('.chosen-choices li.search-choice',chosen)).to.have.length(2); expect($('.chosen-choices li.search-choice',chosen).eq(0).text()).to.equal('toyota tundra'); expect($('.chosen-choices li.search-choice',chosen).eq(1).text()).to.equal('toyota camery'); //weird how chai doesn't make ['1','2'] === ['1','2'] expect(select.val()[0]).to.have.equal(selectedIds[0]); expect(select.val()[1]).to.have.equal(selectedIds[1]); }); }); }); describe('multi-select', function(){ beforeEach(function(){ space.html(''); space.append(multiSelect.clone()); }); it('should bind the keyup event to the input', function(){ var chosen, input; this.clock = sinon.useFakeTimers(); chosen = $('select', space).ajaxChosen({ dataType: 'json', type: 'POST', url: '/search' },{}).next(); $('input', chosen).trigger('click'); this.clock.tick(50);//AbstractChosen.prototype.input_focus -> fires focus logic after 50 input = $('input', chosen).val(''); expect($._data( input[0], "events" )['keyup']).to.have.length(2); this.clock.restore() }); it('should allow the last character to be deleted with selected options', function() { var select, chosen, input, key; this.server = sinon.fakeServer.create(); this.clock = sinon.useFakeTimers(); this.server.respondWith( '/search', [200, { 'Content-Type': 'application/json' }, '{ "q": "Fe", "results": [{"id": 1, "text": "Ferrari"}] }'] ); select = $('select', space).ajaxChosen({ dataType: 'json', type: 'POST', url: '/search' }); chosen = select.next(); $('input', chosen).trigger('click'); this.clock.tick(50);//AbstractChosen.prototype.input_focus -> fires focus logic after 50 input = $('input', space).val('F'); key = $.Event('keyup'); key.which = 69; input.trigger(key); this.clock.tick(750); this.server.respond(); expect(input.val()).to.equal("Fe"); expect($('option',select)).to.have.length(2);//empty + 1 result // not results selected expect($('.chosen-choices li.search-choice', chosen)).to.have.length(0); // click on result to add to selected items $('.chosen-results li',chosen).not('.no-results').eq(0).addClass('active-result'); $('.chosen-results li',chosen).not('.no-results').eq(0).trigger('mouseup'); //verify that it has been added to the selected list expect($('option:selected',select)).to.have.length(1); expect($('option:selected',select).text()).to.equal('Ferrari'); this.server.responses.length = 0; this.server.respondWith( '/search', [200, { 'Content-Type': 'application/json' }, '{ "q": "La", "results": [{"id": 2, "text": "Lamborgini"}] }'] ); $('input', chosen).trigger('click'); this.clock.tick(50);//AbstractChosen.prototype.input_focus -> fires focus logic after 50 input.val('L'); key = $.Event('keyup'); key.which = 65; input.trigger(key) this.clock.tick(750); // Remove everything from input before server respond key = $.Event('keyup'); key.which = 8; input.val(''); input.trigger(key); this.server.respond(); expect(input.val()).to.be.empty; //verify that it still has options selected expect($('option:selected',select)).to.have.length(1); expect($('option:selected',select).text()).to.equal('Ferrari'); this.clock.restore(); this.server.restore(); }) it('should keep the options previously selected', function(){ var select, chosen, input, key; this.server = sinon.fakeServer.create(); this.clock = sinon.useFakeTimers(); this.server.respondWith( '/search', [200, { 'Content-Type': 'application/json' }, '{ "q": "Ala", "results": [{"id":1, "text":"Alabama"}]}'] ); select = $('select', space).ajaxChosen({ dataType: 'json', type: 'POST', url: '/search' },{}); chosen = select.next(); $('input', chosen).trigger('click'); this.clock.tick(50);//AbstractChosen.prototype.input_focus -> fires focus logic after 50 input = $('input', chosen).val('Al'); key = $.Event('keyup'); key.which = 97; input.trigger(key); this.clock.tick(750); this.server.respond(); expect($('option',select)).to.have.length(2);//empty + 1 result //not results in selected expect($('.chosen-choices li.search-choice',chosen)).to.have.length(0); //have the chiquita result expect($('.chosen-results li', chosen).not('.no-results')).to.have.length(1); expect($('.chosen-results li', chosen).not('.no-results').text()).to.equal('Alabama'); //click on result to add to selected items $('.chosen-results li',chosen).not('.no-results').eq(0).addClass('active-result'); $('.chosen-results li',chosen).not('.no-results').eq(0).trigger('mouseup'); //verify that it has been added to the selected list expect($('option:selected',select)).to.have.length(1); expect($('option:selected',select).text()).to.equal('Alabama'); expect($('.chosen-choices li.search-field',chosen)).to.have.length(1); expect($('.chosen-choices li.search-choice',chosen).text()).to.equal('Alabama'); this.server.responses.length = 0; this.server.respondWith( '/search', [200, { 'Content-Type': 'application/json' }, '{ "q": "ala", "results": [{"id":2, "text":"Alaska"}]}'] ); chosen.trigger('click'); this.clock.tick(50);//AbstractChosen.prototype.input_focus -> fires focus logic after 50 input = $('input', chosen).val('al'); key = $.Event('keyup'); key.which = 97; input.trigger(key); this.clock.tick(750); this.server.respond(); //select box still has chiquita selected expect($('option:selected',select)).to.have.length(1); expect($('option:selected',select).text()).to.equal('Alabama'); //have the original result expect($('.chosen-choices li.search-choice',chosen)).to.have.length(1); expect($('.chosen-choices li.search-choice',chosen).text()).to.equal('Alabama'); //notice how we have two results and one is 'active-result' and the other is 'result-selected' expect($('.chosen-results li.result-selected', chosen)).to.have.length(1); expect($('.chosen-results li.result-selected', chosen).text()).to.equal('Alabama'); expect($('.chosen-results li.active-result', chosen).not('.no-results')).to.have.length(1); expect($('.chosen-results li.active-result', chosen).not('.no-results').text()).to.equal('Alaska'); //click on result to add to selected items $('.chosen-results li.active-result', chosen).not('.no-results').eq(0).addClass('active-result'); $('.chosen-results li.active-result', chosen).not('.no-results').eq(0).trigger('mouseup'); //verify selected results expect($('option:selected',select)).to.have.length(2); expect($('.chosen-choices li.search-choice',chosen)).to.have.length(2); var expectedResults = ['Alabama', 'Alaska']; $('.chosen-choices li.search-choice', chosen).each(function(i, elem){ expect($(elem).text()).to.equal(expectedResults[i]); }); $('option:selected',select).each(function(i, elem){ expect($(elem).text()).to.equal(expectedResults[i]); }); }); }); describe('empty-select', function(){ beforeEach(function(){ space.html(''); space.append(emptySelect.clone()); }); it("should append an empty option if one is not present", function(){ expect($('select option', space)).to.have.length(0); $('select', space).ajaxChosen({},{}); expect($('select option', space)).to.have.length(1); }); it("should not have the nosearch class", function(){ var select, chosen; select = $('select', space).ajaxChosen({},{}); chosen = select.next(); expect(chosen.hasClass('chzn-container-single-nosearch')).to.be.false; }); }); describe('a request queue to ensure the user only sees the last response', function(){ beforeEach(function(){ this.clock = sinon.useFakeTimers(); this.xhr = sinon.useFakeXMLHttpRequest(); var requests = this.requests = []; this.xhr.onCreate = function (xhr) { requests.push(xhr) }; }); afterEach(function(){ this.clock.restore(); this.xhr.restore(); }); it('during typing', function(){ var chosen, input, key; chosen = $('select', space).ajaxChosen({ dataType: 'json', type: 'POST', url: '/search' },{}).next(); chosen.trigger('click'); this.clock.tick(50);//AbstractChosen.prototype.input_focus -> fires focus logic after 50 //first request input = $('input', chosen).val('monkey'); key = $.Event('keyup'); key.which = 32; input.trigger(key); this.clock.tick(750); //server begins processing request 1 expect(input.val()).to.equal('monkey'); expect(this.requests).to.have.length(1); //second request input.val('banana'); key = $.Event('keyup'); key.which = 32; input.trigger(key); this.clock.tick(350);//not enough time to start processing the request expect(input.val()).to.equal('banana'); expect(this.requests).to.have.length(1);//see still one request //request 1 comes back while we're still typing! Dam the server is fast this.requests[0].respond(200, { "Content-Type": "application/json" }, '{ "q": "monkey", "results": [{"id":1, "text":"first monkey"}]}'); //discard first response keep newly typed word and ajax chosen should still look like it's still processing expect(input.val()).to.equal('banana'); expect(input.prop('style')['background']).to.match(/loading\.gif/i); //the rest of the clock ticks down so we should now fire off the second request this.clock.tick(400); expect(this.requests).to.have.length(2); expect(input.val()).to.equal('banana'); //yup still waiting for the server to respond expect(input.prop('style')['background']).to.match(/loading\.gif/i); //response 2 comes back and banana is selected this.requests[1].respond(200, { "Content-Type": "application/json" }, '{ "q": "banana", "results": [{"id":1, "text":"banana bunch"}]}'); expect(input.val()).to.equal('banana'); expect(input.prop('style')['background']).to.not.match(/loading\.gif/i); }); it('in order', function(){ var chosen, input, key; chosen = $('select', space).ajaxChosen({ dataType: 'json', type: 'POST', url: '/search' },{}).next(); chosen.trigger('click'); this.clock.tick(50);//AbstractChosen.prototype.input_focus -> fires focus logic after 50 //first request input = $('input', chosen).val('monkey'); key = $.Event('keyup'); key.which = 32; input.trigger(key); this.clock.tick(750); //server begins processing request 1 expect(input.val()).to.equal('monkey'); expect(this.requests).to.have.length(1); //second request input.val('banana'); key = $.Event('keyup'); key.which = 32; input.trigger(key); this.clock.tick(750); //server begins processing request 2 expect(input.val()).to.equal('banana'); expect(this.requests).to.have.length(2); //response 1 comes back this.requests[0].respond(200, { "Content-Type": "application/json" }, '{ "q": "monkey", "results": [{"id":1, "text":"first monkey"}]}'); //discard first response keep newly typed word and ajax chosen should still look like it's still processing expect(input.val()).to.equal('banana'); expect(input.prop('style')['background']).to.match(/loading\.gif/i); //response 2 comes back and banana is selected this.requests[1].respond(200, { "Content-Type": "application/json" }, '{ "q": "banana", "results": [{"id":1, "text":"banana bunch"}]}'); expect(input.val()).to.equal('banana'); expect(input.prop('style')['background']).to.not.match(/loading\.gif/i); }); it('out of order', function(){ var chosen, input, key; chosen = $('select', space).ajaxChosen({ dataType: 'json', type: 'POST', url: '/search' },{}).next(); chosen.trigger('click'); this.clock.tick(50);//AbstractChosen.prototype.input_focus -> fires focus logic after 50 //first request input = $('input', chosen).val('monkey'); key = $.Event('keyup'); key.which = 32; input.trigger(key); this.clock.tick(750); //server begins processing request 1 expect(input.val()).to.equal('monkey'); expect(this.requests).to.have.length(1); //second request input.val('banana'); key = $.Event('keyup'); key.which = 32; input.trigger(key); this.clock.tick(750); //server begins processing request 2 expect(input.val()).to.equal('banana'); expect(this.requests).to.have.length(2); //response 2 comes back first and banana is selected this.requests[1].respond(200, { "Content-Type": "application/json" }, '{ "q": "banana", "results": [{"id":1, "text":"banana bunch"}]}'); expect(input.val()).to.equal('banana'); expect(input.css('background-image')).to.not.match(/loading\.gif/i); //response 1 comes back out of order this.requests[0].respond(200, { "Content-Type": "application/json" }, '{ "q": "monkey", "results": [{"id":1, "text":"first monkey"}]}'); //discard first response keep newly typed word and result set expect(input.val()).to.equal('banana'); expect(input.css('background-image')).to.not.match(/loading\.gif/i); }); }); });