summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorVladislav Zarakovsky <vlad.zar@gmail.com>2015-12-28 09:31:07 +0300
committerVladislav Zarakovsky <vlad.zar@gmail.com>2015-12-28 09:31:07 +0300
commit0b38bb595b9a4b8a6353fd4748dfb3c6530b57ac (patch)
treeeaea8096b6ffa7f04fb218f5e9fea7942edfb668
parent730f82a2bbe7aa3ac65f574e3ce1c481fd953f66 (diff)
parent5dbfd450fb5d5ce0c1b8886baed047f91d926691 (diff)
downloadawesomplete-0b38bb595b9a4b8a6353fd4748dfb3c6530b57ac.zip
awesomplete-0b38bb595b9a4b8a6353fd4748dfb3c6530b57ac.tar.gz
awesomplete-0b38bb595b9a4b8a6353fd4748dfb3c6530b57ac.tar.bz2
Merge remote-tracking branch 'upstream/gh-pages' into features/code-climate
-rw-r--r--awesomplete.js17
-rw-r--r--awesomplete.min.js14
-rw-r--r--karma.conf.js12
-rw-r--r--package.json10
-rw-r--r--test/api/closeSpec.js30
-rw-r--r--test/api/evaluateSpec.js57
-rw-r--r--test/api/gotoSpec.js64
-rw-r--r--test/api/nextSpec.js57
-rw-r--r--test/api/openSpec.js50
-rw-r--r--test/api/openedSpec.js26
-rw-r--r--test/api/previousSpec.js57
-rw-r--r--test/api/selectSpec.js122
-rw-r--r--test/api/selectedSpec.js41
-rw-r--r--test/events/blurSpec.js17
-rw-r--r--test/events/inputSpec.js17
-rw-r--r--test/events/keydownSpec.js67
-rw-r--r--test/events/mousedownSpec.js55
-rw-r--r--test/events/submitSpec.js21
-rw-r--r--test/fixtures/options.html22
-rw-r--r--test/fixtures/plain.html1
-rw-r--r--test/helpers/bindSpec.js59
-rw-r--r--test/helpers/createSpec.js102
-rw-r--r--test/helpers/dollarSpec.js62
-rw-r--r--test/helpers/doubleDollarSpec.js49
-rw-r--r--test/helpers/fireSpec.js60
-rw-r--r--test/helpers/regExpEscapeSpec.js38
-rw-r--r--test/init/htmlSpec.js60
-rw-r--r--test/init/listSpec.js98
-rw-r--r--test/init/optionsSpec.js76
-rw-r--r--test/specHelper.js68
-rw-r--r--test/static/allSpec.js21
-rw-r--r--test/static/filterContainsSpec.js60
-rw-r--r--test/static/filterStartsWithSpec.js60
-rw-r--r--test/static/sortByLengthSpec.js51
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);
+ });
+ });
+});