diff options
author | Vladislav Zarakovsky <vlad.zar@gmail.com> | 2015-12-28 09:31:07 +0300 |
---|---|---|
committer | Vladislav Zarakovsky <vlad.zar@gmail.com> | 2015-12-28 09:31:07 +0300 |
commit | 0b38bb595b9a4b8a6353fd4748dfb3c6530b57ac (patch) | |
tree | eaea8096b6ffa7f04fb218f5e9fea7942edfb668 | |
parent | 730f82a2bbe7aa3ac65f574e3ce1c481fd953f66 (diff) | |
parent | 5dbfd450fb5d5ce0c1b8886baed047f91d926691 (diff) | |
download | awesomplete-0b38bb595b9a4b8a6353fd4748dfb3c6530b57ac.zip awesomplete-0b38bb595b9a4b8a6353fd4748dfb3c6530b57ac.tar.gz awesomplete-0b38bb595b9a4b8a6353fd4748dfb3c6530b57ac.tar.bz2 |
Merge remote-tracking branch 'upstream/gh-pages' into features/code-climate
34 files changed, 1599 insertions, 22 deletions
diff --git a/awesomplete.js b/awesomplete.js index fe4d0c3..41f94ee 100644 --- a/awesomplete.js +++ b/awesomplete.js @@ -46,7 +46,7 @@ var _ = function (input, o) { }); this.ul = $.create("ul", { - hidden: "", + hidden: "hidden", inside: this.container }); @@ -95,15 +95,15 @@ var _ = function (input, o) { li = li.parentNode; } - if (li) { - me.select(li); + if (li && evt.button === 0) { // Only select on left click + me.select(li, evt); } } }}); if (this.input.hasAttribute("list")) { - this.list = "#" + input.getAttribute("list"); - input.removeAttribute("list"); + this.list = "#" + this.input.getAttribute("list"); + this.input.removeAttribute("list"); } else { this.list = this.input.getAttribute("data-list") || o.list || []; @@ -190,7 +190,7 @@ _.prototype = { $.fire(this.input, "awesomplete-highlight"); }, - select: function (selected) { + select: function (selected, originalEvent) { selected = selected || this.ul.children[this.index]; if (selected) { @@ -200,7 +200,8 @@ _.prototype = { text: selected.textContent, preventDefault: function () { prevented = true; - } + }, + originalEvent: originalEvent }); if (!prevented) { @@ -383,7 +384,7 @@ if (typeof self !== "undefined") { } // Expose Awesomplete as a CJS module -if (typeof exports === "object") { +if (typeof module === "object" && module.exports) { module.exports = _; } diff --git a/awesomplete.min.js b/awesomplete.min.js index f5a6f19..afebcaf 100644 --- a/awesomplete.min.js +++ b/awesomplete.min.js @@ -1,10 +1,10 @@ // Awesomplete - Lea Verou - MIT license (function(){function m(a,b){for(var c in a){var g=a[c],e=this.input.getAttribute("data-"+c.toLowerCase());this[c]="number"===typeof g?parseInt(e):!1===g?null!==e:g instanceof Function?null:e;this[c]||0===this[c]||(this[c]=c in b?b[c]:g)}}function d(a,b){return"string"===typeof a?(b||document).querySelector(a):a||null}function h(a,b){return k.call((b||document).querySelectorAll(a))}function l(){h("input.awesomplete").forEach(function(a){new f(a)})}var f=function(a,b){var c=this;this.input=d(a);this.input.setAttribute("autocomplete", -"off");this.input.setAttribute("aria-autocomplete","list");b=b||{};m.call(this,{minChars:2,maxItems:10,autoFirst:!1,filter:f.FILTER_CONTAINS,sort:f.SORT_BYLENGTH,item:function(a,b){return d.create("li",{innerHTML:a.replace(RegExp(d.regExpEscape(b.trim()),"gi"),"<mark>$&</mark>"),"aria-selected":"false"})},replace:function(a){this.input.value=a}},b);this.index=-1;this.container=d.create("div",{className:"awesomplete",around:a});this.ul=d.create("ul",{hidden:"",inside:this.container});this.status=d.create("span", -{className:"visually-hidden",role:"status","aria-live":"assertive","aria-relevant":"additions",inside:this.container});d.bind(this.input,{input:this.evaluate.bind(this),blur:this.close.bind(this),keydown:function(a){var b=a.keyCode;if(c.opened)if(13===b&&c.selected)a.preventDefault(),c.select();else if(27===b)c.close();else if(38===b||40===b)a.preventDefault(),c[38===b?"previous":"next"]()}});d.bind(this.input.form,{submit:this.close.bind(this)});d.bind(this.ul,{mousedown:function(a){a=a.target;if(a!== -this){for(;a&&!/li/i.test(a.nodeName);)a=a.parentNode;a&&c.select(a)}}});this.input.hasAttribute("list")?(this.list="#"+a.getAttribute("list"),a.removeAttribute("list")):this.list=this.input.getAttribute("data-list")||b.list||[];f.all.push(this)};f.prototype={set list(a){Array.isArray(a)?this._list=a:"string"===typeof a&&-1<a.indexOf(",")?this._list=a.split(/\s*,\s*/):(a=d(a))&&a.children&&(this._list=k.apply(a.children).map(function(a){return a.textContent.trim()}));document.activeElement===this.input&& -this.evaluate()},get selected(){return-1<this.index},get opened(){return this.ul&&null==this.ul.getAttribute("hidden")},close:function(){this.ul.setAttribute("hidden","");this.index=-1;d.fire(this.input,"awesomplete-close")},open:function(){this.ul.removeAttribute("hidden");this.autoFirst&&-1===this.index&&this.goto(0);d.fire(this.input,"awesomplete-open")},next:function(){this.goto(this.index<this.ul.children.length-1?this.index+1:-1)},previous:function(){var a=this.ul.children.length;this.goto(this.selected? -this.index-1:a-1)},goto:function(a){var b=this.ul.children;this.selected&&b[this.index].setAttribute("aria-selected","false");this.index=a;-1<a&&0<b.length&&(b[a].setAttribute("aria-selected","true"),this.status.textContent=b[a].textContent);d.fire(this.input,"awesomplete-highlight")},select:function(a){if(a=a||this.ul.children[this.index]){var b;d.fire(this.input,"awesomplete-select",{text:a.textContent,preventDefault:function(){b=!0}});b||(this.replace(a.textContent),this.close(),d.fire(this.input, -"awesomplete-selectcomplete"))}},evaluate:function(){var a=this,b=this.input.value;b.length>=this.minChars&&0<this._list.length?(this.index=-1,this.ul.innerHTML="",this._list.filter(function(c){return a.filter(c,b)}).sort(this.sort).every(function(c,d){a.ul.appendChild(a.item(c,b));return d<a.maxItems-1}),0===this.ul.children.length?this.close():this.open()):this.close()}};f.all=[];f.FILTER_CONTAINS=function(a,b){return RegExp(d.regExpEscape(b.trim()),"i").test(a)};f.FILTER_STARTSWITH=function(a, -b){return RegExp("^"+d.regExpEscape(b.trim()),"i").test(a)};f.SORT_BYLENGTH=function(a,b){return a.length!==b.length?a.length-b.length:a<b?-1:1};var k=Array.prototype.slice;d.create=function(a,b){var c=document.createElement(a),g;for(g in b){var e=b[g];"inside"===g?d(e).appendChild(c):"around"===g?(e=d(e),e.parentNode.insertBefore(c,e),c.appendChild(e)):g in c?c[g]=e:c.setAttribute(g,e)}return c};d.bind=function(a,b){if(a)for(var c in b){var d=b[c];c.split(/\s+/).forEach(function(b){a.addEventListener(b, +"off");this.input.setAttribute("aria-autocomplete","list");b=b||{};m.call(this,{minChars:2,maxItems:10,autoFirst:!1,filter:f.FILTER_CONTAINS,sort:f.SORT_BYLENGTH,item:function(a,b){var c=""===b?a:a.replace(RegExp(d.regExpEscape(b.trim()),"gi"),"<mark>$&</mark>");return d.create("li",{innerHTML:c,"aria-selected":"false"})},replace:function(a){this.input.value=a}},b);this.index=-1;this.container=d.create("div",{className:"awesomplete",around:a});this.ul=d.create("ul",{hidden:"",inside:this.container}); +this.status=d.create("span",{className:"visually-hidden",role:"status","aria-live":"assertive","aria-relevant":"additions",inside:this.container});d.bind(this.input,{input:this.evaluate.bind(this),blur:this.close.bind(this),keydown:function(a){var b=a.keyCode;if(c.opened)if(13===b&&c.selected)a.preventDefault(),c.select();else if(27===b)c.close();else if(38===b||40===b)a.preventDefault(),c[38===b?"previous":"next"]()}});d.bind(this.input.form,{submit:this.close.bind(this)});d.bind(this.ul,{mousedown:function(a){var b= +a.target;if(b!==this){for(;b&&!/li/i.test(b.nodeName);)b=b.parentNode;b&&0===a.button&&c.select(b)}}});this.input.hasAttribute("list")?(this.list="#"+a.getAttribute("list"),a.removeAttribute("list")):this.list=this.input.getAttribute("data-list")||b.list||[];f.all.push(this)};f.prototype={set list(a){Array.isArray(a)?this._list=a:"string"===typeof a&&-1<a.indexOf(",")?this._list=a.split(/\s*,\s*/):(a=d(a))&&a.children&&(this._list=k.apply(a.children).map(function(a){return a.textContent.trim()})); +document.activeElement===this.input&&this.evaluate()},get selected(){return-1<this.index},get opened(){return this.ul&&null==this.ul.getAttribute("hidden")},close:function(){this.ul.setAttribute("hidden","");this.index=-1;d.fire(this.input,"awesomplete-close")},open:function(){this.ul.removeAttribute("hidden");this.autoFirst&&-1===this.index&&this.goto(0);d.fire(this.input,"awesomplete-open")},next:function(){this.goto(this.index<this.ul.children.length-1?this.index+1:-1)},previous:function(){var a= +this.ul.children.length;this.goto(this.selected?this.index-1:a-1)},goto:function(a){var b=this.ul.children;this.selected&&b[this.index].setAttribute("aria-selected","false");this.index=a;-1<a&&0<b.length&&(b[a].setAttribute("aria-selected","true"),this.status.textContent=b[a].textContent);d.fire(this.input,"awesomplete-highlight")},select:function(a){if(a=a||this.ul.children[this.index]){var b;d.fire(this.input,"awesomplete-select",{text:a.textContent,preventDefault:function(){b=!0}});b||(this.replace(a.textContent), +this.close(),d.fire(this.input,"awesomplete-selectcomplete"))}},evaluate:function(){var a=this,b=this.input.value;b.length>=this.minChars&&0<this._list.length?(this.index=-1,this.ul.innerHTML="",this._list.filter(function(c){return a.filter(c,b)}).sort(this.sort).every(function(c,d){a.ul.appendChild(a.item(c,b));return d<a.maxItems-1}),0===this.ul.children.length?this.close():this.open()):this.close()}};f.all=[];f.FILTER_CONTAINS=function(a,b){return RegExp(d.regExpEscape(b.trim()),"i").test(a)}; +f.FILTER_STARTSWITH=function(a,b){return RegExp("^"+d.regExpEscape(b.trim()),"i").test(a)};f.SORT_BYLENGTH=function(a,b){return a.length!==b.length?a.length-b.length:a<b?-1:1};var k=Array.prototype.slice;d.create=function(a,b){var c=document.createElement(a),g;for(g in b){var e=b[g];"inside"===g?d(e).appendChild(c):"around"===g?(e=d(e),e.parentNode.insertBefore(c,e),c.appendChild(e)):g in c?c[g]=e:c.setAttribute(g,e)}return c};d.bind=function(a,b){if(a)for(var c in b){var d=b[c];c.split(/\s+/).forEach(function(b){a.addEventListener(b, d)})}};d.fire=function(a,b,c){var d=document.createEvent("HTMLEvents");d.initEvent(b,!0,!0);for(var e in c)d[e]=c[e];a.dispatchEvent(d)};d.regExpEscape=function(a){return a.replace(/[-\\^$*+?.()|[\]{}]/g,"\\$&")};"undefined"!==typeof Document&&("loading"!==document.readyState?l():document.addEventListener("DOMContentLoaded",l));f.$=d;f.$$=h;"undefined"!==typeof self&&(self.Awesomplete=f);"object"===typeof exports&&(module.exports=f);return f})(); diff --git a/karma.conf.js b/karma.conf.js index 1bd5b13..73aba77 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -5,12 +5,17 @@ module.exports = function(config) { // frameworks to use // available frameworks: https://npmjs.org/browse/keyword/karma-adapter - frameworks: ['jasmine'], + frameworks: ['jasmine', 'jasmine-def', 'fixture'], // list of files / patterns to load in the browser files: [ 'awesomplete.js', - 'test/**/*Shared.js', + 'test/specHelper.js', + { + pattern: 'test/fixtures/**/*.html', + watched: true, included: true, served: true + }, + 'test/**/*Shared.js', 'test/**/*Spec.js' ], @@ -23,12 +28,13 @@ module.exports = function(config) { // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor preprocessors: { 'awesomplete.js': ['coverage'] + '**/*.html' : ['html2js'] }, // test results reporter to use // possible values: 'dots', 'progress' // available reporters: https://npmjs.org/browse/keyword/karma-reporter - reporters: ['progress', 'coverage'], + reporters: ['dots', 'coverage'], coverageReporter: { type: 'lcov', subdir: '.' diff --git a/package.json b/package.json index 1a030e5..0ab76f6 100644 --- a/package.json +++ b/package.json @@ -15,10 +15,12 @@ "homepage": "https://leaverou.github.io/awesomplete/", "devDependencies": { "jasmine-core": "^2.3.0", - "karma": "^0.12.31", - "karma-chrome-launcher": "^0.1.8", + "karma": "^0.13.15", + "karma-chrome-launcher": "^0.2.1", "karma-coverage": "^0.5.3", - "karma-jasmine": "^0.3.5", - "karma-phantomjs-launcher": "^0.1.4" + "karma-fixture": "^0.2.5", + "karma-html2js-preprocessor": "^0.1.0", + "karma-jasmine": "^0.3.6", + "karma-jasmine-def": "^0.1.0" } } diff --git a/test/api/closeSpec.js b/test/api/closeSpec.js new file mode 100644 index 0000000..b1de6bc --- /dev/null +++ b/test/api/closeSpec.js @@ -0,0 +1,30 @@ +describe("awesomplete.close", function () { + + $.fixture("plain"); + + subject(function () { return new Awesomplete("#plain") }); + + beforeEach(function () { + this.subject.open(); + this.subject.next(); + }); + + it("closes completer", function () { + this.subject.close(); + expect(this.subject.ul.hasAttribute("hidden")).toBe(true); + }); + + it("makes no item selected", function () { + this.subject.close(); + + expect(this.subject.selected).toBe(false); + expect(this.subject.index).toBe(-1); + }); + + it("fires awesomplete-close event", function () { + var handler = $.spyOnEvent(this.subject.input, "awesomplete-close"); + this.subject.close(); + + expect(handler).toHaveBeenCalled(); + }); +}); diff --git a/test/api/evaluateSpec.js b/test/api/evaluateSpec.js new file mode 100644 index 0000000..cc064b6 --- /dev/null +++ b/test/api/evaluateSpec.js @@ -0,0 +1,57 @@ +describe("awesomplete.evaluate", function () { + + $.fixture("plain"); + + subject(function () { + return new Awesomplete("#plain", { list: ["item1", "item2", "item3"] }); + }); + + describe("with too short input value", function () { + beforeEach(function () { + $.type(this.subject.input, "i"); + }); + + it("closes completer", function () { + spyOn(this.subject, "close"); + this.subject.evaluate(); + + expect(this.subject.close).toHaveBeenCalled(); + }); + }); + + describe("with no items found", function () { + beforeEach(function () { + $.type(this.subject.input, "nosuchitem"); + }); + + it("closes completer", function () { + spyOn(this.subject, "close"); + this.subject.evaluate(); + + expect(this.subject.close).toHaveBeenCalled(); + }); + }); + + describe("with some items found", function () { + beforeEach(function () { + $.type(this.subject.input, "ite"); + }); + + it("opens completer", function () { + spyOn(this.subject, "open"); + this.subject.evaluate(); + + expect(this.subject.open).toHaveBeenCalled(); + }); + + it("fills completer with found items", function () { + this.subject.evaluate(); + expect(this.subject.ul.children.length).toBe(3); + }); + + it("makes no item selected", function () { + this.subject.evaluate(); + expect(this.subject.selected).toBe(false); + }); + }); +}); diff --git a/test/api/gotoSpec.js b/test/api/gotoSpec.js new file mode 100644 index 0000000..da96cf0 --- /dev/null +++ b/test/api/gotoSpec.js @@ -0,0 +1,64 @@ +describe("awesomplete.goto", function () { + + $.fixture("plain"); + + subject(function () { + return new Awesomplete("#plain", { list: ["item1", "item2", "item3"] }); + }); + + def("firstIndex", function () { return 0 }); + def("lastIndex", function () { return this.subject.ul.children.length - 1 }); + + beforeEach(function () { + $.type(this.subject.input, "ite"); + }); + + it("clears previous aria-selected", function () { + this.subject.goto(this.firstIndex); + this.subject.goto(this.lastIndex); + + expect(this.subject.ul.children[this.firstIndex].getAttribute("aria-selected")).toBe("false"); + }); + + it("goes to first item", function () { + this.subject.goto(this.firstIndex); + expect(this.subject.index).toBe(this.firstIndex); + }); + + it("goes to last item", function () { + this.subject.goto(this.lastIndex); + expect(this.subject.index).toBe(this.lastIndex); + }); + + it("fires awesomplete-highlight event", function () { + var handler = $.spyOnEvent(this.subject.input, "awesomplete-highlight"); + this.subject.goto(1); + + expect(handler).toHaveBeenCalled(); + }); + + describe("with item index > -1", function () { + beforeEach(function () { + this.subject.goto(this.firstIndex); + }); + + it("sets aria-selected", function () { + expect(this.subject.ul.children[this.firstIndex].getAttribute("aria-selected")).toBe("true"); + }); + + it("updates status", function () { + expect(this.subject.status.textContent).toBe("item1"); + }); + }); + + describe("with item index = -1", function () { + beforeEach(function () { + this.subject.goto(this.firstIndex); + this.subject.goto(-1); + }); + + it("does not update status", function () { + expect(this.subject.status.textContent).toBe("item1"); + }); + }); +}); diff --git a/test/api/nextSpec.js b/test/api/nextSpec.js new file mode 100644 index 0000000..bef3134 --- /dev/null +++ b/test/api/nextSpec.js @@ -0,0 +1,57 @@ +describe("awesomplete.next", function () { + + $.fixture("plain"); + + subject(function () { + return new Awesomplete("#plain", { list: ["item1", "item2", "item3"] }); + }); + + def("firstIndex", function () { return 0 }); + def("lastIndex", function () { return this.subject.ul.children.length - 1 }); + + describe("without any items found", function () { + beforeEach(function () { + $.type(this.subject.input, "nosuchitem"); + this.subject.open(); + }); + + it("does not select any item", function () { + this.subject.next(); + expect(this.subject.selected).toBe(false); + }); + }); + + describe("with some items found", function () { + beforeEach(function () { + $.type(this.subject.input, "ite"); + this.subject.open(); + }); + + describe("and no item was already selected", function () { + it("selects the first item ", function () { + this.subject.next(); + expect(this.subject.index).toBe(this.firstIndex); + }); + }); + + describe("and some item was already selected", function () { + it("selects the second item", function () { + this.subject.goto(this.firstIndex); + this.subject.next(); + expect(this.subject.index).toBe(this.firstIndex + 1); + }); + + it("selects the last item", function () { + this.subject.goto(this.lastIndex - 1); + this.subject.next(); + expect(this.subject.index).toBe(this.lastIndex); + }); + + it("selects no item after reaching the end", function () { + this.subject.goto(this.lastIndex); + this.subject.next(); + expect(this.subject.selected).toBe(false); + }); + }); + }); +}); diff --git a/test/api/openSpec.js b/test/api/openSpec.js new file mode 100644 index 0000000..848b960 --- /dev/null +++ b/test/api/openSpec.js @@ -0,0 +1,50 @@ +describe("awesomplete.open", function () { + + $.fixture("plain"); + + subject(function () { return new Awesomplete("#plain", this.options) }); + + it("opens completer", function () { + this.subject.open(); + expect(this.subject.ul.hasAttribute("hidden")).toBe(false); + }); + + // Exposes this bug https://github.com/LeaVerou/awesomplete/pull/16740 + // FIXME better fix is probably required as discussed in PR above + xit("fills in the list on creation", function () { + $("#plain").value = "ite"; + this.options = { list: "item1, item2" }; + this.subject.open(); + + expect(this.subject.ul.children.length).toBe(2); + }); + + it("fires awesomplete-open event", function () { + var handler = $.spyOnEvent(this.subject.input, "awesomplete-open"); + this.subject.open(); + + expect(handler).toHaveBeenCalled(); + }); + + describe("with autoFirst: true", function () { + def("options", function () { return { autoFirst: true } }); + + it("automatically selects first item", function () { + spyOn(this.subject, "goto"); + this.subject.open(); + + expect(this.subject.goto).toHaveBeenCalledWith(0); + }); + }); + + describe("with autoFirst: false", function () { + def("options", function () { return { autoFirst: false } }); + + it("does not select any item", function () { + this.subject.open(); + + expect(this.subject.selected).toBe(false); + expect(this.subject.index).toBe(-1); + }); + }); +}); diff --git a/test/api/openedSpec.js b/test/api/openedSpec.js new file mode 100644 index 0000000..370ffdc --- /dev/null +++ b/test/api/openedSpec.js @@ -0,0 +1,26 @@ +describe("awesomplete.opened", function () { + + $.fixture("plain"); + + subject(function () { return new Awesomplete("#plain") }); + + describe("with newly created completer", function () { + it("is false", function () { + expect(this.subject.opened).toBe(false); + }); + }); + + describe("with opened completer", function () { + it("is true", function () { + this.subject.open(); + expect(this.subject.opened).toBe(true); + }); + }); + + describe("with closed completer", function () { + it("is false", function () { + this.subject.close(); + expect(this.subject.opened).toBe(false); + }); + }); +}); diff --git a/test/api/previousSpec.js b/test/api/previousSpec.js new file mode 100644 index 0000000..c1d7daf --- /dev/null +++ b/test/api/previousSpec.js @@ -0,0 +1,57 @@ +describe("awesomplete.previous", function () { + + $.fixture("plain"); + + subject(function () { + return new Awesomplete("#plain", { list: ["item1", "item2", "item3"] }); + }); + + def("firstIndex", function () { return 0 }); + def("lastIndex", function () { return this.subject.ul.children.length - 1 }); + + describe("without any items found", function () { + beforeEach(function () { + $.type(this.subject.input, "nosuchitem"); + this.subject.open(); + }); + + it("does not select any item", function () { + this.subject.previous(); + expect(this.subject.selected).toBe(false); + }); + }); + + describe("with some items found", function () { + beforeEach(function () { + $.type(this.subject.input, "ite"); + this.subject.open(); + }); + + describe("and no item was already selected", function () { + it("selects the last item ", function () { + this.subject.previous(); + expect(this.subject.index).toBe(this.lastIndex); + }); + }); + + describe("and some item was already selected", function () { + it("selects the second item from the end", function () { + this.subject.goto(this.lastIndex); + this.subject.previous(); + expect(this.subject.index).toBe(this.lastIndex - 1); + }); + + it("selects the first item", function () { + this.subject.goto(this.firstIndex + 1); + this.subject.previous(); + expect(this.subject.index).toBe(this.firstIndex); + }); + + it("selects no item after reaching the start", function () { + this.subject.goto(this.firstIndex); + this.subject.previous(); + expect(this.subject.selected).toBe(false); + }); + }); + }); +}); diff --git a/test/api/selectSpec.js b/test/api/selectSpec.js new file mode 100644 index 0000000..82b4069 --- /dev/null +++ b/test/api/selectSpec.js @@ -0,0 +1,122 @@ +describe("awesomplete.select", function () { + + $.fixture("plain"); + + subject(function () { + return new Awesomplete("#plain", { list: ["item1", "item2", "item3"] }); + }); + + def("firstIndex", function () { return 0 }); + def("lastIndex", function () { return this.subject.ul.children.length - 1 }); + def("lastLi", function () { return this.subject.ul.children[this.lastIndex] }); + + beforeEach(function () { + $.type(this.subject.input, "ite"); + }); + + describe("with closed completer", itDoesNotSelectAnyItem); + + describe("with opened completer", function () { + beforeEach(function () { + this.subject.open(); + }); + + describe("and no current item", itDoesNotSelectAnyItem); + + describe("and current item", function () { + beforeEach(function () { + this.subject.goto(this.firstIndex); + }); + + itSelects("item1"); + }); + + describe("and item specified as argument", function () { + def("selectArgument", function () { return this.lastLi }); + + itSelects("item3"); + }); + }); + + // Shared behaviors + + function itSelects(expectedTxt) { + it("fires awesomplete-select event", function () { + var handler = $.spyOnEvent(this.subject.input, "awesomplete-select"); + this.subject.select(this.selectArgument); + + expect(handler).toHaveBeenCalledWith(jasmine.objectContaining({ text: expectedTxt })); + }); + + describe("and awesomplete-select event was not prevented", function () { + beforeEach(function () { + $.on(this.subject.input, "awesomplete-select", $.noop); + }); + + it("changes the input value", function () { + this.subject.select(this.selectArgument); + expect(this.subject.input.value).toBe(expectedTxt); + }); + + it("closes completer", function () { + spyOn(this.subject, "close"); + this.subject.select(this.selectArgument); + + expect(this.subject.close).toHaveBeenCalled(); + }); + + it("fires awesomplete-selectcomplete event", function () { + var handler = $.spyOnEvent(this.subject.input, "awesomplete-selectcomplete"); + this.subject.select(this.selectArgument); + + expect(handler).toHaveBeenCalled(); + }); + }); + + describe("and awesomplete-select event was prevented", function () { + beforeEach(function () { + $.on(this.subject.input, "awesomplete-select", function (evt) { evt.preventDefault() }); + }); + + it("does not change the input value", function () { + this.subject.select(this.selectArgument); + expect(this.subject.input.value).toBe("ite"); + }); + + it("does not close completer", function () { + spyOn(this.subject, "close"); + this.subject.select(this.selectArgument); + + expect(this.subject.close).not.toHaveBeenCalled(); + }); + + it("does not fire awesomplete-selectcomplete event", function () { + var handler = $.spyOnEvent(this.subject.input, "awesomplete-selectcomplete"); + this.subject.select(this.selectArgument); + + expect(handler).not.toHaveBeenCalled(); + }); + }); + } + + function itDoesNotSelectAnyItem() { + it("does not change the input value", function () { + this.subject.select(); + expect(this.subject.input.value).toBe("ite"); + }); + + it("does not fire awesomplete-select event", function () { + var handler = $.spyOnEvent(this.subject.input, "awesomplete-select"); + this.subject.select(); + + expect(handler).not.toHaveBeenCalled(); + }); + + it("does not fire awesomplete-selectcomplete event", function () { + var handler = $.spyOnEvent(this.subject.input, "awesomplete-selectcomplete"); + this.subject.select(); + + expect(handler).not.toHaveBeenCalled(); + }); + } +}); diff --git a/test/api/selectedSpec.js b/test/api/selectedSpec.js new file mode 100644 index 0000000..6e645be --- /dev/null +++ b/test/api/selectedSpec.js @@ -0,0 +1,41 @@ +describe("awesomplete.selected", function () { + + $.fixture("plain"); + + subject(function () { + return new Awesomplete("#plain", { list: ["item1", "item2", "item3"] }); + }); + + describe("with newly created completer", function () { + it("is false", function () { + expect(this.subject.selected).toBe(false); + }); + }); + + describe("with opened completer", function () { + beforeEach(function () { + this.subject.open(); + $.type(this.subject.input, "ite"); + }); + + describe("and no item selected", function () { + it("is false", function () { + expect(this.subject.selected).toBe(false); + }); + }); + + describe("and some item selected", function () { + it("is true", function () { + this.subject.next(); + expect(this.subject.selected).toBe(true); + }); + }); + }); + + describe("with closed completer", function () { + it("is false", function () { + this.subject.close(); + expect(this.subject.selected).toBe(false); + }); + }); +}); diff --git a/test/events/blurSpec.js b/test/events/blurSpec.js new file mode 100644 index 0000000..fd2df8c --- /dev/null +++ b/test/events/blurSpec.js @@ -0,0 +1,17 @@ +describe("blur event", function () { + + $.fixture("plain"); + + subject(function () { + return new Awesomplete("#plain", { list: ["item1", "item2", "item3"] }); + }); + + it("closes completer", function () { + spyOn(Awesomplete.prototype, "close"); + this.subject.input.focus(); + this.subject.open(); + + $.fire(this.subject.input, "blur"); + expect(Awesomplete.prototype.close).toHaveBeenCalled(); + }); +}); diff --git a/test/events/inputSpec.js b/test/events/inputSpec.js new file mode 100644 index 0000000..7f7282d --- /dev/null +++ b/test/events/inputSpec.js @@ -0,0 +1,17 @@ +describe("input event", function () { + + $.fixture("plain"); + + subject(function () { + return new Awesomplete("#plain", { list: ["item1", "item2", "item3"] }); + }); + + it("rebuilds the list", function () { + spyOn(Awesomplete.prototype, "evaluate"); + this.subject.input.focus(); + this.subject.open(); + + $.type(this.subject.input, "ite"); + expect(Awesomplete.prototype.evaluate).toHaveBeenCalled(); + }); +}); diff --git a/test/events/keydownSpec.js b/test/events/keydownSpec.js new file mode 100644 index 0000000..903d638 --- /dev/null +++ b/test/events/keydownSpec.js @@ -0,0 +1,67 @@ +describe("keydown event", function () { + + $.fixture("plain"); + + subject(function () { + return new Awesomplete("#plain", { list: ["item1", "item2", "item3"] }); + }); + + beforeEach(function () { + this.subject.open(); + this.subject.input.focus(); + $.type(this.subject.input, "ite"); + }); + + it("supports enter", function () { + this.subject.next(); + + spyOn(this.subject, "select"); + $.keydown(this.subject.input, $.k.ENTER); + + expect(this.subject.select).toHaveBeenCalled(); + }); + + it("supports escape", function () { + spyOn(this.subject, "close"); + $.keydown(this.subject.input, $.k.ESC); + + expect(this.subject.close).toHaveBeenCalled(); + }); + + it("supports down arrow", function () { + spyOn(this.subject, "next"); + $.keydown(this.subject.input, $.k.DOWN); + + expect(this.subject.next).toHaveBeenCalled(); + }); + + it("supports up arrow", function () { + spyOn(this.subject, "previous"); + $.keydown(this.subject.input, $.k.UP); + + expect(this.subject.previous).toHaveBeenCalled(); + }); + + it("ignores other keys", function() { + spyOn(this.subject, "select"); + spyOn(this.subject, "close"); + spyOn(this.subject, "next"); + spyOn(this.subject, "previous"); + + $.keydown(this.subject.input, 111); + + expect(this.subject.select).not.toHaveBeenCalled(); + expect(this.subject.close).not.toHaveBeenCalled(); + expect(this.subject.next).not.toHaveBeenCalled(); + expect(this.subject.previous).not.toHaveBeenCalled(); + }); + + it("does nothing if not opened", function () { + this.subject.close(); + + spyOn(this.subject, "next"); + $.keydown(this.subject.input, $.k.DOWN); + + expect(this.subject.next).not.toHaveBeenCalled(); + }); +}); diff --git a/test/events/mousedownSpec.js b/test/events/mousedownSpec.js new file mode 100644 index 0000000..5de338e --- /dev/null +++ b/test/events/mousedownSpec.js @@ -0,0 +1,55 @@ +describe("mousedown event", function () { + + $.fixture("plain"); + + subject(function () { + return new Awesomplete("#plain", { list: ["item1", "item2", "item3"] }); + }); + + beforeEach(function () { + this.subject.input.focus(); + this.subject.open(); + $.type(this.subject.input, "ite"); + this.subject.next(); + + spyOn(this.subject, "select"); + }); + + def("li", function () { return this.subject.ul.children[1] }); + + describe("with ul target", function () { + def("target", function () { return this.subject.ul }); + + it("does not select item", function () { + $.fire(this.target, "mousedown", { button: 0 }); + expect(this.subject.select).not.toHaveBeenCalled(); + }); + }); + + describe("with li target", function () { + def("target", function () { return this.li }); + + describe("on left click", function () { + it("selects item", function () { + $.fire(this.target, "mousedown", { button: 0 }); + expect(this.subject.select).toHaveBeenCalledWith(this.li); + }); + }); + + describe("on right click", function () { + it("does not select item", function () { + $.fire(this.target, "mousedown", { button: 2 }); + expect(this.subject.select).not.toHaveBeenCalled(); + }); + }); + }); + + describe("with child of li target", function () { + def("target", function () { return $("mark", this.li) }); + + it("selects item", function () { + $.fire(this.target, "mousedown", { button: 0 }); + expect(this.subject.select).toHaveBeenCalledWith(this.li); + }); + }); +}); diff --git a/test/events/submitSpec.js b/test/events/submitSpec.js new file mode 100644 index 0000000..bacd726 --- /dev/null +++ b/test/events/submitSpec.js @@ -0,0 +1,21 @@ +describe("form submit event", function () { + + $.fixture("options"); + + subject(function () { + return new Awesomplete("#inside-form", { list: ["item1", "item2", "item3"] }); + }); + + beforeEach(function () { + spyOn(Awesomplete.prototype, "close"); + this.subject.input.focus(); + this.subject.open(); + // prevent full page reload in Firefox, which causes tests to stop running + $.on(this.subject.input.form, "submit", function (evt) { evt.preventDefault() }); + }); + + it("closes completer", function () { + $.fire(this.subject.input.form, "submit"); + expect(Awesomplete.prototype.close).toHaveBeenCalled(); + }); +}); diff --git a/test/fixtures/options.html b/test/fixtures/options.html new file mode 100644 index 0000000..996992b --- /dev/null +++ b/test/fixtures/options.html @@ -0,0 +1,22 @@ +<input id="no-options" class="simple-input" /> + +<input id="with-custom-options" data-minchars="4" data-maxitems="8" data-autofirst="true" /> + +<input id="with-data-list-inline" data-list="With, Data, List, Inline" /> + +<input id="with-data-list" data-list="#data-list" /> +<ul id="data-list"> + <li>With</li> + <li>Data</li> + <li>List</li> +</ul> + +<input id="with-list" list="list" /> +<datalist id="list"> + <option>With</option> + <option>List</option> +</datalist> + +<form> + <input id="inside-form" /> +</form> diff --git a/test/fixtures/plain.html b/test/fixtures/plain.html new file mode 100644 index 0000000..9869bcc --- /dev/null +++ b/test/fixtures/plain.html @@ -0,0 +1 @@ +<input id="plain" /> diff --git a/test/helpers/bindSpec.js b/test/helpers/bindSpec.js new file mode 100644 index 0000000..8eea7ff --- /dev/null +++ b/test/helpers/bindSpec.js @@ -0,0 +1,59 @@ +describe("Awesomplete.$.bind", function () { + + $.fixture("plain"); + + subject(function () { + return function () { Awesomplete.$.bind(this.element, this.events) }; + }); + + describe("whith invalid element", function () { + it("does nothing if element is undefined", function () { + this.element = undefined; + expect(this.subject).not.toThrow(); + }); + + it("does nothing if element is null", function () { + this.element = null; + expect(this.subject).not.toThrow(); + }); + + it("does nothing if element is false", function () { + this.element = false; + expect(this.subject).not.toThrow(); + }); + + it("does nothing if element is 0", function () { + this.element = 0; + expect(this.subject).not.toThrow(); + }); + + it("does nothing if element is empty string", function () { + this.element = ""; + expect(this.subject).not.toThrow(); + }); + }); + + describe("with valid element", function () { + def("element", function () { return $("#plain") }); + + beforeEach(function () { + spyOn(this.element, "addEventListener"); + }); + + it("adds event listeners for all events", function () { + this.events = { click: $.noop, input: $.noop }; + this.subject(); + + expect(this.element.addEventListener).toHaveBeenCalledWith("click", this.events.click); + expect(this.element.addEventListener).toHaveBeenCalledWith("input", this.events.input); + }); + + it("adds single event listener for multiple events", function () { + this.events = { "click input": $.noop }; + this.subject(); + + expect(this.element.addEventListener).toHaveBeenCalledWith("click", this.events["click input"]); + expect(this.element.addEventListener).toHaveBeenCalledWith("input", this.events["click input"]); + }); + }); +}); diff --git a/test/helpers/createSpec.js b/test/helpers/createSpec.js new file mode 100644 index 0000000..a9ef31e --- /dev/null +++ b/test/helpers/createSpec.js @@ -0,0 +1,102 @@ +describe("Awesomplete.$.create", function () { + + $.fixture("options"); + + subject(function () { return Awesomplete.$.create(this.tag, this.options || {}) }); + + def("tag", "div"); + + it("creates DOM element", function () { + expect(this.subject instanceof HTMLElement).toBe(true); + }); + + describe("with various tag names", function () { + it("creates <ul> element", function () { + this.tag = "ul"; + expect(this.subject.tagName).toEqual("UL"); + }); + + it("creates <li> element", function () { + this.tag = "li"; + expect(this.subject.tagName).toEqual("LI"); + }); + }); + + describe("without options", function () { + it("creates element without any attributes", function () { + expect(this.subject.attributes.length).toEqual(0); + }); + }); + + describe("with simple options", function () { + it("assigns properties", function () { + this.options = { id: "id1", className: "class-name" }; + + expect(this.subject.id).toEqual("id1"); + expect(this.subject.className).toEqual("class-name"); + }); + + it("assigns attributes", function () { + this.options = { attr1: "val1", attr2: "val2" }; + + expect(this.subject.getAttribute("attr1")).toEqual("val1"); + expect(this.subject.getAttribute("attr2")).toEqual("val2"); + }); + }); + + describe("with option for boolean attribute/property", function () { + it("assigns from true value", function () { + this.options = { hidden: true }; + expect(this.subject.hasAttribute("hidden")).toBe(true); + }); + + it("assigns from truthy value", function () { + this.options = { hidden: "hidden" }; + expect(this.subject.hasAttribute("hidden")).toBe(true); + }); + + it("assigns from false value", function () { + this.options = { hidden: false }; + expect(this.subject.hasAttribute("hidden")).toBe(false); + }); + + it("assigns from falsy value", function () { + this.options = { hidden: "" }; + expect(this.subject.hasAttribute("hidden")).toBe(false); + }); + }); + + describe("with inside: option", function () { + it("appends to container by element", function () { + this.options = { inside: $("#data-list") }; + + expect(this.subject).toEqual(this.options.inside.lastChild); + }); + + it("appends to container by selector", function () { + this.options = { inside: "#data-list" }; + + expect(this.subject).toEqual($(this.options.inside).lastChild); + }); + }); + + describe("with around: option", function () { + it("wraps specified element", function () { + this.options = { around: $("#no-options") }; + + var originalParent = this.options.around.parentNode; + expect(this.subject.parentNode).toEqual(originalParent); + + expect(this.subject.firstChild).toEqual(this.options.around); + }); + + it("wraps element specified by selector", function () { + this.options = { around: "#no-options" }; + + var originalParent = $(this.options.around).parentNode; + expect(this.subject.parentNode).toEqual(originalParent); + + expect(this.subject.firstChild).toEqual($(this.options.around)); + }); + }); +}); diff --git a/test/helpers/dollarSpec.js b/test/helpers/dollarSpec.js new file mode 100644 index 0000000..3ba864e --- /dev/null +++ b/test/helpers/dollarSpec.js @@ -0,0 +1,62 @@ +describe("Awesomplete.$", function () { + + $.fixture("options"); + + subject(function () { return Awesomplete.$(this.expression, this.context) }); + + describe("with default context", itFindsElement); + + describe("with custom context", function () { + def("context", function () { return fixture.el }); + + itFindsElement(); + }); + + describe("with truthy non string expression", function () { + it("returns the expression back", function () { + this.expression = $("#no-options"); + expect(this.subject).toBe(this.expression); + }); + }); + + describe("with falsy non string expression", function () { + it("returns null if expression is undefined", function () { + this.expression = undefined; + expect(this.subject).toBeNull(); + }); + + it("returns null if expression is null", function () { + this.expression = null; + expect(this.subject).toBeNull(); + }); + + it("returns null if expression is false", function () { + this.expression = false; + expect(this.subject).toBeNull(); + }); + }); + + // Shared behaviors + + function itFindsElement() { + it("returns DOM element", function () { + this.expression = "#no-options"; + expect(this.subject instanceof HTMLElement).toBe(true); + }); + + it("finds by id", function () { + this.expression = "#no-options"; + expect(this.subject.id).toEqual("no-options"); + }); + + it("finds by class name", function () { + this.expression = ".simple-input"; + expect(this.subject.id).toEqual("no-options"); + }); + + it("finds by tag name", function () { + this.expression = "datalist"; + expect(this.subject.id).toEqual("list"); + }); + } +}); diff --git a/test/helpers/doubleDollarSpec.js b/test/helpers/doubleDollarSpec.js new file mode 100644 index 0000000..c76a10d --- /dev/null +++ b/test/helpers/doubleDollarSpec.js @@ -0,0 +1,49 @@ +describe("Awesomplete.$$", function () { + + $.fixture("options"); + + subject(function () { return Awesomplete.$$(this.expression, this.context) }); + + describe("with default context", itFindsAllElements); + + describe("with custom context", function () { + def("context", function () { return fixture.el }); + + itFindsAllElements(); + }); + + // Shared behaviors + + function itFindsAllElements() { + it("returns an array of DOM elements", function () { + this.expression = "#no-options"; + expect(this.subject).toEqual(jasmine.any(Array)); + expect(this.subject[0] instanceof HTMLElement).toBe(true); + }); + + it("finds all elements", function () { + this.expression = "input"; + expect(this.subject.length).toEqual($$("input").length); + }); + + it("finds DOM element", function () { + this.expression = "#no-options"; + expect(this.subject[0] instanceof HTMLElement).toBe(true); + }); + + it("finds by id", function () { + this.expression = "#no-options"; + expect(this.subject[0].id).toEqual("no-options"); + }); + + it("finds by class name", function () { + this.expression = ".simple-input"; + expect(this.subject[0].id).toEqual("no-options"); + }); + + it("finds by tag name", function () { + this.expression = "datalist"; + expect(this.subject[0].id).toEqual("list"); + }); + } +}); diff --git a/test/helpers/fireSpec.js b/test/helpers/fireSpec.js new file mode 100644 index 0000000..f876cce --- /dev/null +++ b/test/helpers/fireSpec.js @@ -0,0 +1,60 @@ +describe("Awesomplete.$.fire", function () { + + $.fixture("plain"); + + subject(function () { + return Awesomplete.$.fire.bind(Awesomplete.$, this.element); + }); + + def("element", function () { return $("#plain") }); + + beforeEach(function () { + spyOn(this.element, "dispatchEvent"); + }); + + it("fires event once", function () { + this.subject("click"); + expect(this.element.dispatchEvent.calls.count()).toEqual(1); + }); + + describe("fires different event types", function () { + it("fires click event", function () { + this.subject("click"); + expect(this.element.dispatchEvent).toHaveBeenCalledWith(jasmine.objectContaining({ type: "click" })); + }); + + it("fires input event", function () { + this.subject("input"); + expect(this.element.dispatchEvent).toHaveBeenCalledWith(jasmine.objectContaining({ type: "input" })); + }); + }); + + describe("sets event properties", function () { + it("makes cancelable event", function () { + this.subject("click"); + expect(this.element.dispatchEvent).toHaveBeenCalledWith(jasmine.objectContaining({ cancelable: true })); + }); + + it("can't make non cancelable event", function () { + this.subject("click", { cancelable: false }); + expect(this.element.dispatchEvent).toHaveBeenCalledWith(jasmine.objectContaining({ cancelable: true })); + }); + + it("makes event that bubbles", function () { + this.subject("click"); + expect(this.element.dispatchEvent).toHaveBeenCalledWith(jasmine.objectContaining({ bubbles: true })); + }); + + it("can't make event that does not bubble", function () { + this.subject("click", { bubles: false }); + expect(this.element.dispatchEvent).toHaveBeenCalledWith(jasmine.objectContaining({ bubbles: true })); + }); + + it("sets properties on the event", function () { + var properties = { text: "hello", preventDefault: $.noop }; + + this.subject("click", properties); + expect(this.element.dispatchEvent).toHaveBeenCalledWith(jasmine.objectContaining(properties)); + }); + }); +}); diff --git a/test/helpers/regExpEscapeSpec.js b/test/helpers/regExpEscapeSpec.js new file mode 100644 index 0000000..c50167b --- /dev/null +++ b/test/helpers/regExpEscapeSpec.js @@ -0,0 +1,38 @@ +describe("Awesomplete.$.regExpEscape", function () { + + subject(function () { return Awesomplete.$.regExpEscape(this.str) }); + + describe("with regular expression special characters", function () { + it("escapes backslashes", function () { + this.str = "\\"; + expect(this.subject).toBe("\\\\"); + }); + + it("escapes brackets, braces and parentheses", function () { + this.str = "[]{}()"; + expect(this.subject).toBe("\\[\\]\\{\\}\\(\\)"); + }); + + it("escapes other special characters", function () { + this.str = "-^$*+?.|"; + expect(this.subject).toBe("\\-\\^\\$\\*\\+\\?\\.\\|"); + }); + + it("escapes the whole string", function () { + this.str = "**"; + expect(this.subject).toBe("\\*\\*"); + }); + }); + + describe("with plain characters", function () { + it("does not escape letters", function () { + this.str = "abcdefjhijklmnopqrstuvwxyzABCDEFJHIJKLMNOPQRSTUVWXYZ"; + expect(this.subject).toBe(this.str); + }); + + it("does not escape numbers", function () { + this.str = "0123456789"; + expect(this.subject).toBe(this.str); + }); + }); +}); diff --git a/test/init/htmlSpec.js b/test/init/htmlSpec.js new file mode 100644 index 0000000..3e0b9c1 --- /dev/null +++ b/test/init/htmlSpec.js @@ -0,0 +1,60 @@ +describe("Html modifications", function () { + + $.fixture("plain"); + + subject(function () { return new Awesomplete("#plain") }); + + it("binds to correct input", function () { + expect(this.subject.input instanceof HTMLElement).toBe(true); + expect(this.subject.input.id).toBe("plain"); + }); + + it("turns native autocompleter off", function () { + expect(this.subject.input.getAttribute("autocomplete")).toBe("off"); + }); + + describe("HTML tweaks", function () { + it("creates container", function () { + expect(this.subject.container instanceof HTMLElement).toBe(true); + expect(this.subject.container.className).toBe("awesomplete"); + }); + + it("places input inside container", function () { + expect(this.subject.input.parentNode).toBe(this.subject.container); + }); + + it("creates list", function () { + expect(this.subject.ul instanceof HTMLElement).toBe(true); + expect(this.subject.ul.tagName).toBe("UL"); + }); + + it("places list inside container", function () { + expect(this.subject.ul.parentNode).toBe(this.subject.container); + }); + + it("hides list", function () { + expect(this.subject.ul.hasAttribute("hidden")).toBe(true); + }); + }); + + describe("ARIA support", function () { + it("makes input accessible", function () { + expect(this.subject.input.getAttribute("aria-autocomplete")).toBe("list"); + }); + + it("creates status", function () { + expect(this.subject.status instanceof HTMLElement).toBe(true); + expect(this.subject.status.getAttribute("role")).toBe("status"); + expect(this.subject.status.getAttribute("aria-live")).toBe("assertive"); + expect(this.subject.status.getAttribute("aria-relevant")).toBe("additions"); + }); + + it("puts status inside container", function () { + expect(this.subject.status.parentNode).toBe(this.subject.container); + }); + + it("hides status", function () { + expect(this.subject.status.className).toBe("visually-hidden"); + }); + }); +}); diff --git a/test/init/listSpec.js b/test/init/listSpec.js new file mode 100644 index 0000000..ac265aa --- /dev/null +++ b/test/init/listSpec.js @@ -0,0 +1,98 @@ +describe("Awesomplete list", function () { + + $.fixture("options"); + + subject(function () { return new Awesomplete(this.element, this.options) }); + + def("element", "#no-options"); + + it("is empty if not provided", function () { + expect(this.subject._list).toEqual([]); + }); + + describe("setter", function () { + it("assigns from array", function () { + this.subject.list = [ "From", "Array" ]; + expect(this.subject._list).toEqual([ "From", "Array" ]); + }); + + it("assigns from comma separated list", function () { + this.subject.list = "From,Inline,List"; + expect(this.subject._list).toEqual([ "From", "Inline", "List" ]); + }); + + it("assigns from element specified by selector", function () { + this.subject.list = "#data-list"; + expect(this.subject._list).toEqual([ "With", "Data", "List" ]); + }); + + it("assigns from element", function () { + this.subject.list = $("#data-list"); + expect(this.subject._list).toEqual([ "With", "Data", "List" ]); + }); + + it("does not assigns from not found list", function () { + this.subject.list = "#nosuchlist"; + expect(this.subject._list).toEqual([]); + }); + + it("does not assigns from empty list", function () { + this.subject.list = "#empty-list"; + expect(this.subject._list).toEqual([]); + }); + + describe("with active input", function() { + beforeEach(function() { + this.subject.input.focus(); + }); + + it("evaluates completer", function() { + spyOn(this.subject, "evaluate"); + this.subject.list = "#data-list"; + + expect(this.subject.evaluate).toHaveBeenCalled(); + }); + }); + }); + + describe("constructor option", function () { + it("assigns from array", function () { + this.options = { list: [ "From", "Array" ] }; + expect(this.subject._list).toEqual([ "From", "Array" ]); + }); + + it("assigns from comma separated list", function () { + this.options = { list: "From,Inline,List" }; + expect(this.subject._list).toEqual([ "From", "Inline", "List" ]); + }); + + it("assigns from element specified by selector", function () { + this.options = { list: "#data-list" }; + expect(this.subject._list).toEqual([ "With", "Data", "List" ]); + }); + + it("assigns from list specified by element", function () { + this.options = { list: $("#data-list") }; + expect(this.subject._list).toEqual([ "With", "Data", "List" ]); + }); + }); + + describe("data-list html attribute", function () { + it("assigns from comma separated list", function () { + this.element = "#with-data-list-inline"; + expect(this.subject._list).toEqual(["With", "Data", "List", "Inline"]); + }); + + it("assigns from element referenced by selector", function () { + this.element = "#with-data-list"; + expect(this.subject._list).toEqual(["With", "Data", "List"]); + }); + }); + + describe("list html attribute", function () { + it("assigns from element referenced by id", function () { + this.element = "#with-list"; + expect(this.subject._list).toEqual(["With", "List"]); + }); + }); +}); diff --git a/test/init/optionsSpec.js b/test/init/optionsSpec.js new file mode 100644 index 0000000..fdbc429 --- /dev/null +++ b/test/init/optionsSpec.js @@ -0,0 +1,76 @@ +describe("Constructor options", function () { + + $.fixture("options"); + + subject(function () { return new Awesomplete(this.element, this.options) }); + + describe("with default options", function () { + def("element", "#with-data-list"); + + it("requires minimum 2 chars to open completer", function () { + expect(this.subject.minChars).toBe(2); + }); + + it("shows 10 items in completer", function () { + expect(this.subject.maxItems).toBe(10); + }); + + it("does not select the first ocurrence automatically" , function () { + expect(this.subject.autoFirst).toBe(false); + }); + + it("filters with FILTER_CONTAINS", function () { + expect(this.subject.filter).toBe(Awesomplete.FILTER_CONTAINS); + }); + + it("orders with SORT_BYLENGTH", function () { + expect(this.subject.sort).toBe(Awesomplete.SORT_BYLENGTH); + }); + + it("generates each completer item with built-in function", function () { + expect(this.subject.item).toEqual(jasmine.any(Function)); + }); + + it("mirrors found item into input with built-in function", function () { + expect(this.subject.replace).toEqual(jasmine.any(Function)); + }); + }); + + describe("with custom options in constructor", function () { + def("element", "#with-data-list"); + def("options", function () { + return { + minChars: 3, + maxItems: 7, + autoFirst: true, + filter: $.noop, + sort: $.noop, + item: $.noop, + replace: $.noop + }; + }); + + it("overrides simple default options", function () { + expect(this.subject.minChars).toBe(3); + expect(this.subject.maxItems).toBe(7); + expect(this.subject.autoFirst).toBe(true); + }); + + it("overrides default functions", function () { + expect(this.subject.filter).toBe(this.options.filter); + expect(this.subject.sort).toBe(this.options.sort); + expect(this.subject.item).toBe(this.options.item); + expect(this.subject.replace).toBe(this.options.replace); + }); + }); + + describe("with custom options in data-* attributes", function () { + def("element", "#with-custom-options"); + + it("overrides simple default options", function () { + expect(this.subject.minChars).toBe(4); + expect(this.subject.maxItems).toBe(8); + expect(this.subject.autoFirst).toBe(true); + }); + }); +}); diff --git a/test/specHelper.js b/test/specHelper.js new file mode 100644 index 0000000..58282e5 --- /dev/null +++ b/test/specHelper.js @@ -0,0 +1,68 @@ +fixture.setBase("test/fixtures"); + +// finds DOM elements in tests +function $ (str, context) { + return (context || fixture.el).querySelector(str); +} + +function $$ (str, context) { + return (context || fixture.el).querySelectorAll(str); +} + +// bundled fixture load/cleanup +$.fixture = function (fixtureName) { + beforeEach(function () { + // Awesomplete probably needs to cleanup this by itself + try { Awesomplete.all = []; } catch(e) {}; + fixture.load(fixtureName + ".html"); + }); + + afterEach(function () { + fixture.cleanup(); + }); +}; + +// spy to check if event was fired or not +$.spyOnEvent = function (target, type) { + var handler = jasmine.createSpy(type); + $.on(target, type, handler); + return handler; +}; + +$.on = function (target, type, callback) { + target.addEventListener(type, callback); +}; + +$.fire = function (target, type, properties) { + var evt = document.createEvent("HTMLEvents"); + evt.initEvent(type, true, true ); + for (var j in properties) { + evt[j] = properties[j]; + } + target.dispatchEvent(evt); +}; + +// simulates text input (very simple, only "input" event is fired) +$.type = function (input, text) { + input.focus(); + input.value = text; + $.fire(input, "input"); +}; + +// simulates keydown events +$.keydown = function (target, keyCode) { + $.fire(target, "keydown", { keyCode: keyCode }); +}; +$.k = { + ENTER: 13, + ESC: 27, + DOWN: 40, + UP: 38 +}; + +// $.noop returns a new empty function each time it's being called +Object.defineProperty($, "noop", { + get: function () { + return function noop () {} + } +}); diff --git a/test/static/allSpec.js b/test/static/allSpec.js new file mode 100644 index 0000000..59e23eb --- /dev/null +++ b/test/static/allSpec.js @@ -0,0 +1,21 @@ +describe("Awesomplete.all", function () { + + $.fixture("options"); + + subject(function () { return Awesomplete.all }); + + it("is empty initially", function () { + expect(this.subject.length).toBe(0); + }); + + it("keeps a list of created instances", function () { + var first = new Awesomplete("#with-data-list-inline"); + expect(this.subject.length).toBe(1); + expect(this.subject).toContain(first); + + var second = new Awesomplete("#with-data-list"); + expect(this.subject.length).toBe(2); + expect(this.subject).toContain(first); + expect(this.subject).toContain(second); + }); +}); diff --git a/test/static/filterContainsSpec.js b/test/static/filterContainsSpec.js new file mode 100644 index 0000000..f6db514 --- /dev/null +++ b/test/static/filterContainsSpec.js @@ -0,0 +1,60 @@ +describe("Awesomplete.FILTER_CONTAINS", function () { + + subject(function () { return Awesomplete.FILTER_CONTAINS }); + + it("is a function", function () { + expect(this.subject).toEqual(jasmine.any(Function)); + }); + + describe("search in a plain string", function () { + it("matches at the start", function () { + expect(this.subject("Hello world", "Hello")).toBe(true); + }); + + it("matches in the middle", function () { + expect(this.subject("Ticket to the moon", "to the")).toBe(true); + }); + + it("matches at the end", function () { + expect(this.subject("This is the end", "end")).toBe(true); + }); + + it("performs case insensitive match", function () { + expect(this.subject("Hey You", "HEY YOU")).toBe(true); + }); + + it("ignores whitespaces around the search value", function () { + expect(this.subject("Watch this", " Watch ")).toBe(true); + }); + + it("does not match if substring is not found", function () { + expect(this.subject("No", "way")).toBe(false); + }); + }); + + describe("search in string with special RegExp chars", function () { + it("matches at the start", function () { + expect(this.subject("[^j(a)v?a-sc|ri\\p+t*]{.$}", "[^j(a)v?a-")).toBe(true); + }); + + it("matches in the middle", function () { + expect(this.subject("[^j(a)v?a-sc|ri\\p+t*]{.$}", "sc|ri\\p+t*")).toBe(true); + }); + + it("matches at the end", function () { + expect(this.subject("[^j(a)v?a-sc|ri\\p+t*]{.$}", "{.$}")).toBe(true); + }); + + it("performs case insensitive match", function () { + expect(this.subject("[^j(a)v?a-sc|ri\\p+t*]{.$}", "[^J(A)V?A-SC|RI\\P+T*]{.$}")).toBe(true); + }); + + it("ignores whitespaces around the search value", function () { + expect(this.subject("[^j(a)v?a-sc|ri\\p+t*]{.$}", " [^j(a)v?a- ")).toBe(true); + }); + + it("does not match if substring is not found", function () { + expect(this.subject("[^j(a)v?a-sc|ri\\p+t*]{.$}", "no way")).toBe(false); + }); + }); +}); diff --git a/test/static/filterStartsWithSpec.js b/test/static/filterStartsWithSpec.js new file mode 100644 index 0000000..655ee23 --- /dev/null +++ b/test/static/filterStartsWithSpec.js @@ -0,0 +1,60 @@ +describe("Awesomplete.FILTER_STARTSWITH", function () { + + subject(function () { return Awesomplete.FILTER_STARTSWITH }); + + it("is a function", function () { + expect(this.subject).toEqual(jasmine.any(Function)); + }); + + describe("search in plain string", function () { + it("matches at the start", function () { + expect(this.subject("Hello world", "Hello")).toBe(true); + }); + + it("does not match in the middle", function () { + expect(this.subject("Ticket to the moon", "to the")).toBe(false); + }); + + it("does not match at the end", function () { + expect(this.subject("This is the end", "end")).toBe(false); + }); + + it("performs case insensitive match", function () { + expect(this.subject("Hey You", "HEY YOU")).toBe(true); + }); + + it("ignores whitespaces around the search value", function () { + expect(this.subject("Watch this", " Watch ")).toBe(true); + }); + + it("does not match if substring is not found", function () { + expect(this.subject("No", "way")).toBe(false); + }); + }); + + describe("search in string with special RegExp chars", function () { + it("matches at the start", function () { + expect(this.subject("[^j(a)v?a-sc|ri\\p+t*]{.$}", "[^j(a)v?a-")).toBe(true); + }); + + it("does not match in the middle", function () { + expect(this.subject("[^j(a)v?a-sc|ri\\p+t*]{.$}", "sc|ri\\p+t*")).toBe(false); + }); + + it("does not match at the end", function () { + expect(this.subject("[^j(a)v?a-sc|ri\\p+t*]{.$}", "{.$}")).toBe(false); + }); + + it("performs case insensitive match", function () { + expect(this.subject("[^j(a)v?a-sc|ri\\p+t*]{.$}", "[^J(A)V?A-SC|RI\\P+T*]{.$}")).toBe(true); + }); + + it("ignores whitespaces around the search value", function () { + expect(this.subject("[^j(a)v?a-sc|ri\\p+t*]{.$}", " [^j(a)v?a- ")).toBe(true); + }); + + it("does not match if substring is not found", function () { + expect(this.subject("[^j(a)v?a-sc|ri\\p+t*]{.$}", "no way")).toBe(false); + }); + }); +}); diff --git a/test/static/sortByLengthSpec.js b/test/static/sortByLengthSpec.js new file mode 100644 index 0000000..e11d9db --- /dev/null +++ b/test/static/sortByLengthSpec.js @@ -0,0 +1,51 @@ +describe("Awesomplete.SORT_BYLENGTH", function () { + + subject(function () { return Awesomplete.SORT_BYLENGTH }); + + it("is a function", function () { + expect(this.subject).toEqual(jasmine.any(Function)); + }); + + describe("with strings of different length", function () { + it("returns negative number if the first string is shorter", function () { + expect(this.subject("a", "aa")).toBe(-1); + expect(this.subject("a", "bb")).toBe(-1); + expect(this.subject("b", "aa")).toBe(-1); + + expect(this.subject("a", "aaa")).toBe(-2); + expect(this.subject("a", "bbb")).toBe(-2); + expect(this.subject("b", "aaa")).toBe(-2); + }); + + it("returns positive number if the first string is longer", function () { + expect(this.subject("aa", "a")).toBe(1); + expect(this.subject("bb", "a")).toBe(1); + expect(this.subject("aa", "b")).toBe(1); + + expect(this.subject("aaa", "a")).toBe(2); + expect(this.subject("bbb", "a")).toBe(2); + expect(this.subject("aaa", "b")).toBe(2); + }); + }); + + describe("with strings of the same length", function () { + it("returns -1 if the first string < second string", function () { + expect(this.subject("a", "b")).toBe(-1); + expect(this.subject("aa", "bb")).toBe(-1); + expect(this.subject("aaa", "bbb")).toBe(-1); + }); + + it("returns 1 if the first string > second string", function () { + expect(this.subject("b", "a")).toBe(1); + expect(this.subject("bb", "aa")).toBe(1); + expect(this.subject("bbb", "aaa")).toBe(1); + }); + + // FIXME SORT_BYLENGTH should probably return 0 like classic string comparison + it("returns 1 if the first string == second string", function () { + expect(this.subject("a", "a")).toBe(1); + expect(this.subject("aa", "aa")).toBe(1); + expect(this.subject("aaa", "aaa")).toBe(1); + }); + }); +}); |