first commit

This commit is contained in:
2024-04-19 14:04:41 +07:00
commit 014283036f
7282 changed files with 1324127 additions and 0 deletions
@@ -0,0 +1,299 @@
describe('Bloodhound', function() {
function build(o) {
return new Bloodhound(_.mixin({
datumTokenizer: datumTokenizer,
queryTokenizer: queryTokenizer
}, o || {}));
}
beforeEach(function() {
jasmine.Remote.useMock();
jasmine.Prefetch.useMock();
jasmine.Transport.useMock();
jasmine.PersistentStorage.useMock();
});
afterEach(function() {
clearAjaxRequests();
});
describe('#initialize', function() {
beforeEach(function() {
this.bloodhound = build({ initialize: false });
spyOn(this.bloodhound, '_initialize').andCallThrough();
});
it('should not initialize if intialize option is false', function() {
expect(this.bloodhound._initialize).not.toHaveBeenCalled();
});
it('should not support reinitialization by default', function() {
var p1, p2;
p1 = this.bloodhound.initialize();
p2 = this.bloodhound.initialize();
expect(p1).toBe(p2);
expect(this.bloodhound._initialize.callCount).toBe(1);
});
it('should reinitialize if reintialize flag is true', function() {
var p1, p2;
p1 = this.bloodhound.initialize();
p2 = this.bloodhound.initialize(true);
expect(p1).not.toBe(p2);
expect(this.bloodhound._initialize.callCount).toBe(2);
});
it('should clear the index', function() {
this.bloodhound = build({ initialize: false, prefetch: '/prefetch' });
spyOn(this.bloodhound, 'clear');
this.bloodhound.initialize();
expect(this.bloodhound.clear).toHaveBeenCalled();
});
it('should load data from prefetch cache if available', function() {
this.bloodhound = build({ initialize: false, prefetch: '/prefetch' });
this.bloodhound.prefetch.fromCache.andReturn(fixtures.serialized.simple);
this.bloodhound.initialize();
expect(this.bloodhound.all()).toEqual(fixtures.data.simple);
expect(this.bloodhound.prefetch.fromNetwork).not.toHaveBeenCalled();
});
it('should load data from prefetch network as fallback', function() {
this.bloodhound = build({ initialize: false, prefetch: '/prefetch' });
this.bloodhound.prefetch.fromCache.andReturn(null);
this.bloodhound.prefetch.fromNetwork.andCallFake(fakeFromNetwork);
this.bloodhound.initialize();
expect(this.bloodhound.all()).toEqual(fixtures.data.simple);
function fakeFromNetwork(cb) { cb(null, fixtures.data.simple); }
});
it('should store prefetch network data in the prefetch cache', function() {
this.bloodhound = build({ initialize: false, prefetch: '/prefetch' });
this.bloodhound.prefetch.fromCache.andReturn(null);
this.bloodhound.prefetch.fromNetwork.andCallFake(fakeFromNetwork);
this.bloodhound.initialize();
expect(this.bloodhound.prefetch.store)
.toHaveBeenCalledWith(fixtures.serialized.simple);
function fakeFromNetwork(cb) { cb(null, fixtures.data.simple); }
});
it('should add local after prefetch is loaded', function() {
this.bloodhound = build({
initialize: false,
local: [{ foo: 'bar' }],
prefetch: '/prefetch'
});
this.bloodhound.prefetch.fromNetwork.andCallFake(fakeFromNetwork);
expect(this.bloodhound.all()).toEqual([]);
this.bloodhound.initialize();
expect(this.bloodhound.all()).toEqual([{ foo: 'bar' }]);
function fakeFromNetwork(cb) { cb(null, []); }
});
});
describe('#add', function() {
it('should add datums to search index', function() {
var spy = jasmine.createSpy();
this.bloodhound = build().add(fixtures.data.simple);
this.bloodhound.search('big', spy);
expect(spy).toHaveBeenCalledWith([
{ value: 'big' },
{ value: 'bigger' },
{ value: 'biggest' }
]);
});
});
describe('#get', function() {
beforeEach(function() {
this.bloodhound = build({
identify: function(d) { return d.value; },
local: fixtures.data.simple
});
});
it('should support array signature', function() {
expect(this.bloodhound.get(['big', 'bigger'])).toEqual([
{ value: 'big' },
{ value: 'bigger' }
]);
});
it('should support splat signature', function() {
expect(this.bloodhound.get('big', 'bigger')).toEqual([
{ value: 'big' },
{ value: 'bigger' }
]);
});
it('should return nothing for unknown ids', function() {
expect(this.bloodhound.get('big', 'foo', 'bigger')).toEqual([
{ value: 'big' },
{ value: 'bigger' }
]);
});
});
describe('#clear', function() {
it('should remove all datums to search index', function() {
var spy = jasmine.createSpy();
this.bloodhound = build({ local: fixtures.data.simple }).clear();
this.bloodhound.search('big', spy);
expect(spy).toHaveBeenCalledWith([]);
});
});
describe('#clearPrefetchCache', function() {
it('should clear persistent storage', function() {
this.bloodhound = build({ prefetch: '/prefetch' }).clearPrefetchCache();
expect(this.bloodhound.prefetch.clear).toHaveBeenCalled();
});
});
describe('#clearRemoteCache', function() {
it('should clear remote request cache', function() {
spyOn(Transport, 'resetCache');
this.bloodhound = build({ remote: '/remote' }).clearRemoteCache();
expect(Transport.resetCache).toHaveBeenCalled();
});
});
describe('#all', function() {
it('should return all local results', function() {
this.bloodhound = build({ local: fixtures.data.simple });
expect(this.bloodhound.all()).toEqual(fixtures.data.simple);
});
});
describe('#search  local', function() {
it('should return sync matches', function() {
var spy = jasmine.createSpy();
this.bloodhound = build({ local: fixtures.data.simple });
this.bloodhound.search('big', spy);
expect(spy).toHaveBeenCalledWith([
{ value: 'big' },
{ value: 'bigger' },
{ value: 'biggest' }
]);
});
});
describe('#search  prefetch', function() {
it('should return sync matches', function() {
var spy = jasmine.createSpy();
this.bloodhound = build({ initialize: false, prefetch: '/prefetch' });
this.bloodhound.prefetch.fromCache.andReturn(fixtures.serialized.simple);
this.bloodhound.initialize();
this.bloodhound.search('big', spy);
expect(spy).toHaveBeenCalledWith([
{ value: 'big' },
{ value: 'bigger' },
{ value: 'biggest' }
]);
});
});
describe('#search  remote', function() {
it('should return async matches', function() {
var spy = jasmine.createSpy();
this.bloodhound = build({ remote: '/remote' });
this.bloodhound.remote.get.andCallFake(fakeGet);
this.bloodhound.search('dog', $.noop, spy);
expect(spy.callCount).toBe(1);
function fakeGet(o, cb) { cb(fixtures.data.animals); }
});
});
describe('#search integration', function() {
it('should backfill when local/prefetch is not sufficient', function() {
var syncSpy, asyncSpy;
syncSpy = jasmine.createSpy();
asyncSpy = jasmine.createSpy();
this.bloodhound = build({
sufficient: 3,
local: fixtures.data.simple,
remote: '/remote'
});
this.bloodhound.remote.get.andCallFake(fakeGet);
this.bloodhound.search('big', syncSpy, asyncSpy);
expect(syncSpy).toHaveBeenCalledWith([
{ value: 'big' },
{ value: 'bigger' },
{ value: 'biggest' }
]);
expect(asyncSpy).not.toHaveBeenCalled();
this.bloodhound.search('bigg', syncSpy, asyncSpy);
expect(syncSpy).toHaveBeenCalledWith([
{ value: 'bigger' },
{ value: 'biggest' }
]);
expect(asyncSpy).toHaveBeenCalledWith(fixtures.data.animals);
function fakeGet(o, cb) { cb(fixtures.data.animals); }
});
it('should remove duplicates from backfill', function() {
var syncSpy, asyncSpy;
syncSpy = jasmine.createSpy();
asyncSpy = jasmine.createSpy();
this.bloodhound = build({
identify: function(d) { return d.value; },
local: fixtures.data.animals,
remote: '/remote'
});
this.bloodhound.remote.get.andCallFake(fakeGet);
this.bloodhound.search('dog', syncSpy, asyncSpy);
expect(syncSpy).toHaveBeenCalledWith([{ value: 'dog' }]);
expect(asyncSpy).toHaveBeenCalledWith([
{ value: 'cat' },
{ value: 'moose' }
]);
function fakeGet(o, cb) { cb(fixtures.data.animals); }
});
});
// helper functions
// ----------------
function datumTokenizer(d) { return $.trim(d.value).split(/\s+/); }
function queryTokenizer(s) { return $.trim(s).split(/\s+/); }
});
@@ -0,0 +1,43 @@
describe('LruCache', function() {
beforeEach(function() {
this.cache = new LruCache(3);
});
it('should make entries retrievable by their keys', function() {
var key = 'key', val = 42;
this.cache.set(key, val);
expect(this.cache.get(key)).toBe(val);
});
it('should return undefined if key has not been set', function() {
expect(this.cache.get('wat?')).toBeUndefined();
});
it('should hold up to maxSize entries', function() {
this.cache.set('one', 1);
this.cache.set('two', 2);
this.cache.set('three', 3);
this.cache.set('four', 4);
expect(this.cache.get('one')).toBeUndefined();
expect(this.cache.get('two')).toBe(2);
expect(this.cache.get('three')).toBe(3);
expect(this.cache.get('four')).toBe(4);
});
it('should evict lru entry if cache is full', function() {
this.cache.set('one', 1);
this.cache.set('two', 2);
this.cache.set('three', 3);
this.cache.get('one');
this.cache.set('four', 4);
expect(this.cache.get('one')).toBe(1);
expect(this.cache.get('two')).toBeUndefined();
expect(this.cache.get('three')).toBe(3);
expect(this.cache.get('four')).toBe(4);
expect(this.cache.size).toBe(3);
});
});
@@ -0,0 +1,194 @@
describe('options parser', function() {
function build(o) {
return oParser(_.mixin({
datumTokenizer: $.noop,
queryTokenizer: $.noop
}, o || {}));
}
function prefetch(o) {
return oParser({
datumTokenizer: $.noop,
queryTokenizer: $.noop,
prefetch: _.mixin({
url: '/example'
}, o || {})
});
}
function remote(o) {
return oParser({
datumTokenizer: $.noop,
queryTokenizer: $.noop,
remote: _.mixin({
url: '/example'
}, o || {})
});
}
it('should throw exception if datumTokenizer is not set', function() {
expect(parse).toThrow();
function parse() { build({ datumTokenizer: null }); }
});
it('should throw exception if queryTokenizer is not set', function() {
expect(parse).toThrow();
function parse() { build({ queryTokenizer: null }); }
});
it('should wrap sorter', function() {
var o = build({ sorter: function(a, b) { return a -b; } });
expect(o.sorter([2, 1, 3])).toEqual([1, 2, 3]);
});
it('should default sorter to identity function', function() {
var o = build();
expect(o.sorter([2, 1, 3])).toEqual([2, 1, 3]);
});
describe('local', function() {
it('should default to empty array', function() {
var o = build();
expect(o.local).toEqual([]);
});
it('should support function', function() {
var o = build({ local: function() { return [1]; } });
expect(o.local).toEqual([1]);
});
it('should support arrays', function() {
var o = build({ local: [1] });
expect(o.local).toEqual([1]);
});
});
describe('prefetch', function() {
it('should throw exception if url is not set', function() {
expect(parse).toThrow();
function parse() { prefetch({ url: null }); }
});
it('should support simple string format', function() {
expect(build({ prefetch: '/prefetch' }).prefetch).toBeDefined();
});
it('should default ttl to 1 day', function() {
var o = prefetch();
expect(o.prefetch.ttl).toBe(86400000);
});
it('should default cache to true', function() {
var o = prefetch();
expect(o.prefetch.cache).toBe(true);
});
it('should default transform to identiy function', function() {
var o = prefetch();
expect(o.prefetch.transform('foo')).toBe('foo');
});
it('should default cacheKey to url', function() {
var o = prefetch();
expect(o.prefetch.cacheKey).toBe(o.prefetch.url);
});
it('should default transport to jQuery.ajax', function() {
var o = prefetch();
expect(o.prefetch.transport).toBe($.ajax);
});
it('should prepend verison to thumbprint', function() {
var o = prefetch();
expect(o.prefetch.thumbprint).toBe('%VERSION%');
o = prefetch({ thumbprint: 'foo' });
expect(o.prefetch.thumbprint).toBe('%VERSION%foo');
});
it('should wrap custom transport to be deferred compatible', function() {
var o, errDeferred, successDeferred;
o = prefetch({ transport: errTransport });
errDeferred = o.prefetch.transport('q');
o = prefetch({ transport: successTransport });
successDeferred = o.prefetch.transport('q');
waits(0);
runs(function() {
expect(errDeferred.isRejected()).toBe(true);
expect(successDeferred.isResolved()).toBe(true);
});
function errTransport(q, success, error) { error(); }
function successTransport(q, success, error) { success(); }
});
});
describe('remote', function() {
it('should throw exception if url is not set', function() {
expect(parse).toThrow();
function parse() { remote({ url: null }); }
});
it('should support simple string format', function() {
expect(build({ remote: '/remote' }).remote).toBeDefined();
});
it('should default transform to identiy function', function() {
var o = remote();
expect(o.remote.transform('foo')).toBe('foo');
});
it('should default transport to jQuery.ajax', function() {
var o = remote();
expect(o.remote.transport).toBe($.ajax);
});
it('should default limiter to debouce', function() {
var o = remote();
expect(o.remote.limiter.name).toBe('debounce');
});
it('should default prepare to identity function', function() {
var o = remote();
expect(o.remote.prepare('q', { url: '/foo' })).toEqual({ url: '/foo' });
});
it('should support wildcard for prepare', function() {
var o = remote({ wildcard: '%FOO' });
expect(o.remote.prepare('=', { url: '/%FOO' })).toEqual({ url: '/%3D' });
});
it('should support replace for prepare', function() {
var o = remote({ replace: function() { return '/bar'; } });
expect(o.remote.prepare('q', { url: '/foo' })).toEqual({ url: '/bar' });
});
it('should should rateLimitBy for limiter', function() {
var o = remote({ rateLimitBy: 'throttle' });
expect(o.remote.limiter.name).toBe('throttle');
});
it('should wrap custom transport to be deferred compatible', function() {
var o, errDeferred, successDeferred;
o = remote({ transport: errTransport });
errDeferred = o.remote.transport('q');
o = remote({ transport: successTransport });
successDeferred = o.remote.transport('q');
waits(0);
runs(function() {
expect(errDeferred.isRejected()).toBe(true);
expect(successDeferred.isResolved()).toBe(true);
});
function errTransport(q, success, error) { error(); }
function successTransport(q, success, error) { success(); }
});
});
});
@@ -0,0 +1,194 @@
describe('PersistentStorage', function() {
var engine, ls;
// test suite is dependent on localStorage being available
if (!window.localStorage) {
console.warn('no localStorage support  skipping PersistentStorage suite');
return;
}
// for good measure!
localStorage.clear();
beforeEach(function() {
ls = {
get length() { return localStorage.length; },
key: spyThrough('key'),
clear: spyThrough('clear'),
getItem: spyThrough('getItem'),
setItem: spyThrough('setItem'),
removeItem: spyThrough('removeItem')
};
engine = new PersistentStorage('ns', ls);
spyOn(Date.prototype, 'getTime').andReturn(0);
});
afterEach(function() {
localStorage.clear();
});
// public methods
// --------------
describe('#get', function() {
it('should access localStorage with prefixed key', function() {
engine.get('key');
expect(ls.getItem).toHaveBeenCalledWith('__ns__key');
});
it('should return undefined when key does not exist', function() {
expect(engine.get('does not exist')).toEqual(undefined);
});
it('should return value as correct type', function() {
engine.set('string', 'i am a string');
engine.set('number', 42);
engine.set('boolean', true);
engine.set('null', null);
engine.set('object', { obj: true });
expect(engine.get('string')).toEqual('i am a string');
expect(engine.get('number')).toEqual(42);
expect(engine.get('boolean')).toEqual(true);
expect(engine.get('null')).toBeNull();
expect(engine.get('object')).toEqual({ obj: true });
});
it('should expire stale keys', function() {
engine.set('key', 'value', -1);
expect(engine.get('key')).toBeNull();
expect(ls.getItem('__ns__key__ttl')).toBeNull();
});
});
describe('#set', function() {
it('should access localStorage with prefixed key', function() {
engine.set('key', 'val');
expect(ls.setItem.mostRecentCall.args[0]).toEqual('__ns__key');
});
it('should JSON.stringify value before storing', function() {
engine.set('key', 'val');
expect(ls.setItem.mostRecentCall.args[1]).toEqual(JSON.stringify('val'));
});
it('should store ttl if provided', function() {
var ttl = 1;
engine.set('key', 'value', ttl);
expect(ls.setItem.argsForCall[0])
.toEqual(['__ns__key__ttl__', ttl.toString()]);
});
it('should call clear if the localStorage limit has been reached', function() {
var spy;
ls.setItem.andCallFake(function() {
var err = new Error();
err.name = 'QuotaExceededError';
throw err;
});
engine.clear = spy = jasmine.createSpy();
engine.set('key', 'value', 1);
expect(spy).toHaveBeenCalled();
});
it('should noop if the localStorage limit has been reached', function() {
var get, set, remove, clear, isExpired;
ls.setItem.andCallFake(function() {
var err = new Error();
err.name = 'QuotaExceededError';
throw err;
});
get = engine.get;
set = engine.set;
remove = engine.remove;
clear = engine.clear;
isExpired = engine.isExpired;
engine.set('key', 'value', 1);
expect(engine.get).not.toBe(get);
expect(engine.set).not.toBe(set);
expect(engine.remove).not.toBe(remove);
expect(engine.clear).not.toBe(clear);
expect(engine.isExpired).not.toBe(isExpired);
});
});
describe('#remove', function() {
it('should remove key from storage', function() {
engine.set('key', 'val');
engine.remove('key');
expect(engine.get('key')).toBeNull();
});
});
describe('#clear', function() {
it('should work with namespaces that contain regex characters', function() {
engine = new PersistentStorage('ns?()');
engine.set('key1', 'val1');
engine.set('key2', 'val2');
engine.clear();
expect(engine.get('key1')).toEqual(undefined);
expect(engine.get('key2')).toEqual(undefined);
});
it('should remove all keys that exist in namespace of engine', function() {
engine.set('key1', 'val1');
engine.set('key2', 'val2');
engine.set('key3', 'val3');
engine.set('key4', 'val4', 0);
engine.clear();
expect(engine.get('key1')).toEqual(undefined);
expect(engine.get('key2')).toEqual(undefined);
expect(engine.get('key3')).toEqual(undefined);
expect(engine.get('key4')).toEqual(undefined);
});
it('should not affect keys with different namespace', function() {
ls.setItem('diff_namespace', 'val');
engine.clear();
expect(ls.getItem('diff_namespace')).toEqual('val');
});
});
describe('#isExpired', function() {
it('should be false for keys without ttl', function() {
engine.set('key', 'value');
expect(engine.isExpired('key')).toBe(false);
});
it('should be false for fresh keys', function() {
engine.set('key', 'value', 1);
expect(engine.isExpired('key')).toBe(false);
});
it('should be true for stale keys', function() {
engine.set('key', 'value', -1);
expect(engine.isExpired('key')).toBe(true);
});
});
// compatible across browsers
function spyThrough(method) {
return jasmine.createSpy().andCallFake(fake);
function fake() {
return localStorage[method].apply(localStorage, arguments);
}
}
});
@@ -0,0 +1,182 @@
describe('Prefetch', function() {
function build(o) {
return new Prefetch(_.mixin({
url: '/prefetch',
ttl: 3600,
cache: true,
thumbprint: '',
cacheKey: 'cachekey',
prepare: function(x) { return x; },
transform: function(x) { return x; },
transport: $.ajax
}, o || {}));
}
beforeEach(function() {
jasmine.PersistentStorage.useMock();
this.prefetch = build();
this.storage = this.prefetch.storage;
this.thumbprint = this.prefetch.thumbprint;
});
describe('#clear', function() {
it('should clear cache storage', function() {
this.prefetch.clear();
expect(this.storage.clear).toHaveBeenCalled();
});
});
describe('#store', function() {
it('should store data in the storage cache', function() {
this.prefetch.store({ foo: 'bar' });
expect(this.storage.set)
.toHaveBeenCalledWith('data', { foo: 'bar' }, 3600);
});
it('should store thumbprint in the storage cache', function() {
this.prefetch.store({ foo: 'bar' });
expect(this.storage.set)
.toHaveBeenCalledWith('thumbprint', jasmine.any(String), 3600);
});
it('should store protocol in the storage cache', function() {
this.prefetch.store({ foo: 'bar' });
expect(this.storage.set)
.toHaveBeenCalledWith('protocol', location.protocol, 3600);
});
it('should be noop if cache option is false', function() {
this.prefetch = build({ cache: false });
this.prefetch.store({ foo: 'bar' });
expect(this.storage.set).not.toHaveBeenCalled();
});
});
describe('#fromCache', function() {
it('should return data if available', function() {
this.storage.get
.andCallFake(fakeStorageGet({ foo: 'bar' }, this.thumbprint));
expect(this.prefetch.fromCache()).toEqual({ foo: 'bar' });
});
it('should return null if data is expired', function() {
this.storage.get
.andCallFake(fakeStorageGet({ foo: 'bar' }, 'foo'));
expect(this.prefetch.fromCache()).toBeNull();
});
it('should return null if data does not exist', function() {
this.storage.get
.andCallFake(fakeStorageGet(null, this.thumbprint));
expect(this.prefetch.fromCache()).toBeNull();
});
it('should return null if cache option is false', function() {
this.prefetch = build({ cache: false });
this.storage.get
.andCallFake(fakeStorageGet({ foo: 'bar' }, this.thumbprint));
expect(this.prefetch.fromCache()).toBeNull();
expect(this.storage.get).not.toHaveBeenCalled();
});
});
describe('#fromNetwork', function() {
it('should have sensible default request settings', function() {
var spy;
spy = jasmine.createSpy();
spyOn(this.prefetch, 'transport').andReturn($.Deferred());
this.prefetch.fromNetwork(spy);
expect(this.prefetch.transport).toHaveBeenCalledWith({
url: '/prefetch',
type: 'GET',
dataType: 'json'
});
});
it('should transform request settings with prepare', function() {
var spy;
spy = jasmine.createSpy();
spyOn(this.prefetch, 'prepare').andReturn({ foo: 'bar' });
spyOn(this.prefetch, 'transport').andReturn($.Deferred());
this.prefetch.fromNetwork(spy);
expect(this.prefetch.transport).toHaveBeenCalledWith({ foo: 'bar' });
});
it('should transform the response using transform', function() {
var spy;
this.prefetch = build({
transform: function() { return { bar: 'foo' }; }
});
spy = jasmine.createSpy();
spyOn(this.prefetch, 'transport')
.andReturn($.Deferred().resolve({ foo: 'bar' }));
this.prefetch.fromNetwork(spy);
expect(spy).toHaveBeenCalledWith(null, { bar: 'foo' });
});
it('should invoke callback with data if success', function() {
var spy;
spy = jasmine.createSpy();
spyOn(this.prefetch, 'transport')
.andReturn($.Deferred().resolve({ foo: 'bar' }));
this.prefetch.fromNetwork(spy);
expect(spy).toHaveBeenCalledWith(null, { foo: 'bar' });
});
it('should invoke callback with err argument true if failure', function() {
var spy;
spy = jasmine.createSpy();
spyOn(this.prefetch, 'transport').andReturn($.Deferred().reject());
this.prefetch.fromNetwork(spy);
expect(spy).toHaveBeenCalledWith(true);
});
});
function fakeStorageGet(data, thumbprint, protocol) {
return function(key) {
var val;
switch (key) {
case 'data':
val = data;
break;
case 'protocol':
val = protocol || location.protocol;
break;
case 'thumbprint':
val = thumbprint;
break;
}
return val;
};
}
});
@@ -0,0 +1,73 @@
describe('Remote', function() {
beforeEach(function() {
jasmine.Transport.useMock();
this.remote = new Remote({
url: '/test?q=%QUERY',
prepare: function(x) { return x; },
transform: function(x) { return x; }
});
this.transport = this.remote.transport;
});
describe('#cancelLastRequest', function() {
it('should cancel last request', function() {
this.remote.cancelLastRequest();
expect(this.transport.cancel).toHaveBeenCalled();
});
});
describe('#get', function() {
it('should have sensible default request settings', function() {
var spy;
spy = jasmine.createSpy();
spyOn(this.remote, 'prepare');
this.remote.get('foo', spy);
expect(this.remote.prepare).toHaveBeenCalledWith('foo', {
url: '/test?q=%QUERY',
type: 'GET',
dataType: 'json'
});
});
it('should transform request settings with prepare', function() {
var spy;
spy = jasmine.createSpy();
spyOn(this.remote, 'prepare').andReturn([{ foo: 'bar' }]);
this.remote.get('foo', spy);
expect(this.transport.get)
.toHaveBeenCalledWith([{ foo: 'bar' }], jasmine.any(Function));
});
it('should transform response with transform', function() {
var spy;
spy = jasmine.createSpy();
spyOn(this.remote, 'transform').andReturn([{ foo: 'bar' }]);
this.transport.get.andCallFake(function(_, cb) { cb(null, {}); });
this.remote.get('foo', spy);
expect(spy).toHaveBeenCalledWith([{ foo: 'bar' }]);
});
it('should return empty array on error', function() {
var spy;
spy = jasmine.createSpy();
this.transport.get.andCallFake(function(_, cb) { cb(true); });
this.remote.get('foo', spy);
expect(spy).toHaveBeenCalledWith([]);
});
});
});
@@ -0,0 +1,72 @@
describe('SearchIndex', function() {
function build(o) {
return new SearchIndex(_.mixin({
datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'),
queryTokenizer: Bloodhound.tokenizers.whitespace
}, o || {}));
}
beforeEach(function() {
this.index = build();
this.index.add(fixtures.data.simple);
});
it('should support serialization/deserialization', function() {
var serialized = this.index.serialize();
this.index.bootstrap(serialized);
expect(this.index.search('smaller')).toEqual([{ value: 'smaller' }]);
});
it('should be able to add data on the fly', function() {
this.index.add({ value: 'new' });
expect(this.index.search('new')).toEqual([{ value: 'new' }]);
});
it('#get should return datums by id', function() {
this.index = build({ identify: function(d) { return d.value; } });
this.index.add(fixtures.data.simple);
expect(this.index.get(['big', 'bigger'])).toEqual([
{ value: 'big' },
{ value: 'bigger' }
]);
});
it('#search should return datums that match the given query', function() {
expect(this.index.search('big')).toEqual([
{ value: 'big' },
{ value: 'bigger' },
{ value: 'biggest' }
]);
expect(this.index.search('small')).toEqual([
{ value: 'small' },
{ value: 'smaller' },
{ value: 'smallest' }
]);
});
it('#search should return an empty array of there are no matches', function() {
expect(this.index.search('wtf')).toEqual([]);
});
it('#serach should handle multi-token queries', function() {
this.index.add({ value: 'foo bar' });
expect(this.index.search('foo b')).toEqual([{ value: 'foo bar' }]);
});
it('#all should return all datums', function() {
expect(this.index.all()).toEqual(fixtures.data.simple);
});
it('#reset should empty the search index', function() {
this.index.reset();
expect(this.index.datums).toEqual([]);
expect(this.index.trie.i).toEqual([]);
expect(this.index.trie.c).toEqual({});
});
});
@@ -0,0 +1,74 @@
describe('tokenizers', function() {
it('.whitespace should tokenize on whitespace', function() {
var tokens = tokenizers.whitespace('big-deal ok');
expect(tokens).toEqual(['big-deal', 'ok']);
});
it('.whitespace should treat null as empty string', function() {
var tokens = tokenizers.whitespace(null);
expect(tokens).toEqual([]);
});
it('.whitespace should treat undefined as empty string', function() {
var tokens = tokenizers.whitespace(undefined);
expect(tokens).toEqual([]);
});
it('.nonword should tokenize on non-word characters', function() {
var tokens = tokenizers.nonword('big-deal ok');
expect(tokens).toEqual(['big', 'deal', 'ok']);
});
it('.nonword should treat null as empty string', function() {
var tokens = tokenizers.nonword(null);
expect(tokens).toEqual([]);
});
it('.nonword should treat undefined as empty string', function() {
var tokens = tokenizers.nonword(undefined);
expect(tokens).toEqual([]);
});
it('.obj.whitespace should tokenize on whitespace', function() {
var t = tokenizers.obj.whitespace('val');
var tokens = t({ val: 'big-deal ok' });
expect(tokens).toEqual(['big-deal', 'ok']);
});
it('.obj.whitespace should accept multiple properties', function() {
var t = tokenizers.obj.whitespace('one', 'two');
var tokens = t({ one: 'big-deal ok', two: 'buzz' });
expect(tokens).toEqual(['big-deal', 'ok', 'buzz']);
});
it('.obj.whitespace should accept array', function() {
var t = tokenizers.obj.whitespace(['one', 'two']);
var tokens = t({ one: 'big-deal ok', two: 'buzz' });
expect(tokens).toEqual(['big-deal', 'ok', 'buzz']);
});
it('.obj.nonword should tokenize on non-word characters', function() {
var t = tokenizers.obj.nonword('val');
var tokens = t({ val: 'big-deal ok' });
expect(tokens).toEqual(['big', 'deal', 'ok']);
});
it('.obj.nonword should accept multiple properties', function() {
var t = tokenizers.obj.nonword('one', 'two');
var tokens = t({ one: 'big-deal ok', two: 'buzz' });
expect(tokens).toEqual(['big', 'deal', 'ok', 'buzz']);
});
it('.obj.nonword should accept array', function() {
var t = tokenizers.obj.nonword(['one', 'two']);
var tokens = t({ one: 'big-deal ok', two: 'buzz' });
expect(tokens).toEqual(['big', 'deal', 'ok', 'buzz']);
});
});
@@ -0,0 +1,175 @@
describe('Transport', function() {
beforeEach(function() {
jasmine.Ajax.useMock();
jasmine.Clock.useMock();
this.transport = new Transport({ transport: $.ajax });
});
afterEach(function() {
// run twice to flush out on-deck requests
$.each(ajaxRequests, drop);
$.each(ajaxRequests, drop);
clearAjaxRequests();
Transport.resetCache();
function drop(i, req) {
req.readyState !== 4 && req.response(fixtures.ajaxResps.ok);
}
});
it('should use jQuery.ajax as the default transport mechanism', function() {
var req, resp = fixtures.ajaxResps.ok, spy = jasmine.createSpy();
this.transport.get('/test', spy);
req = mostRecentAjaxRequest();
req.response(resp);
expect(req.url).toBe('/test');
expect(spy).toHaveBeenCalledWith(null, resp.parsed);
});
it('should respect maxPendingRequests configuration', function() {
for (var i = 0; i < 10; i++) {
this.transport.get('/test' + i, $.noop);
}
expect(ajaxRequests.length).toBe(6);
});
it('should support rate limiting', function() {
this.transport = new Transport({ transport: $.ajax, limiter: limiter });
for (var i = 0; i < 5; i++) {
this.transport.get('/test' + i, $.noop);
}
jasmine.Clock.tick(100);
expect(ajaxRequests.length).toBe(1);
function limiter(fn) { return _.debounce(fn, 20); }
});
it('should cache most recent requests', function() {
var spy1 = jasmine.createSpy(), spy2 = jasmine.createSpy();
this.transport.get('/test1', $.noop);
mostRecentAjaxRequest().response(fixtures.ajaxResps.ok);
this.transport.get('/test2', $.noop);
mostRecentAjaxRequest().response(fixtures.ajaxResps.ok1);
expect(ajaxRequests.length).toBe(2);
this.transport.get('/test1', spy1);
this.transport.get('/test2', spy2);
jasmine.Clock.tick(0);
// no ajax requests were made on subsequent requests
expect(ajaxRequests.length).toBe(2);
expect(spy1).toHaveBeenCalledWith(null, fixtures.ajaxResps.ok.parsed);
expect(spy2).toHaveBeenCalledWith(null, fixtures.ajaxResps.ok1.parsed);
});
it('should not cache requests if cache option is false', function() {
this.transport = new Transport({ transport: $.ajax, cache: false });
this.transport.get('/test1', $.noop);
mostRecentAjaxRequest().response(fixtures.ajaxResps.ok);
this.transport.get('/test1', $.noop);
mostRecentAjaxRequest().response(fixtures.ajaxResps.ok);
expect(ajaxRequests.length).toBe(2);
});
it('should prevent dog pile', function() {
var spy1 = jasmine.createSpy(), spy2 = jasmine.createSpy();
this.transport.get('/test1', spy1);
this.transport.get('/test1', spy2);
mostRecentAjaxRequest().response(fixtures.ajaxResps.ok);
expect(ajaxRequests.length).toBe(1);
waitsFor(function() { return spy1.callCount && spy2.callCount; });
runs(function() {
expect(spy1).toHaveBeenCalledWith(null, fixtures.ajaxResps.ok.parsed);
expect(spy2).toHaveBeenCalledWith(null, fixtures.ajaxResps.ok.parsed);
});
});
it('should always make a request for the last call to #get', function() {
var spy = jasmine.createSpy();
for (var i = 0; i < 6; i++) {
this.transport.get('/test' + i, $.noop);
}
this.transport.get('/test' + i, spy);
expect(ajaxRequests.length).toBe(6);
_.each(ajaxRequests, function(req) {
req.response(fixtures.ajaxResps.ok);
});
expect(ajaxRequests.length).toBe(7);
mostRecentAjaxRequest().response(fixtures.ajaxResps.ok);
expect(spy).toHaveBeenCalled();
});
it('should invoke the callback with err set to true on failure', function() {
var req, resp = fixtures.ajaxResps.err, spy = jasmine.createSpy();
this.transport.get('/test', spy);
req = mostRecentAjaxRequest();
req.response(resp);
expect(req.url).toBe('/test');
expect(spy).toHaveBeenCalledWith(true);
});
it('should not send cancelled requests', function() {
this.transport = new Transport({ transport: $.ajax, limiter: limiter });
this.transport.get('/test', $.noop);
this.transport.cancel();
jasmine.Clock.tick(100);
expect(ajaxRequests.length).toBe(0);
function limiter(fn) { return _.debounce(fn, 20); }
});
it('should not send outdated requests', function() {
this.transport = new Transport({ transport: $.ajax, limiter: limiter });
// warm cache
this.transport.get('/test1', $.noop);
jasmine.Clock.tick(100);
mostRecentAjaxRequest().response(fixtures.ajaxResps.ok);
expect(mostRecentAjaxRequest().url).toBe('/test1');
expect(ajaxRequests.length).toBe(1);
// within the same rate-limit cycle, request test2 and test1. test2 becomes
// outdated after test1 is requested and no request is sent for test1
// because it's a cache hit
this.transport.get('/test2', $.noop);
this.transport.get('/test1', $.noop);
jasmine.Clock.tick(100);
expect(ajaxRequests.length).toBe(1);
function limiter(fn) { return _.debounce(fn, 20); }
});
});
+12
View File
@@ -0,0 +1,12 @@
#!/bin/bash -x
if [ "$TEST_SUITE" == "unit" ]; then
./node_modules/karma/bin/karma start --single-run --browsers PhantomJS
elif [ "$TRAVIS_SECURE_ENV_VARS" == "true" -a "$TEST_SUITE" == "integration" ]; then
static -p 8888 &
sleep 3
# integration tests are flaky, don't let them fail the build
./node_modules/mocha/bin/mocha --harmony -R spec ./test/integration/test.js || true
else
echo "Not running any tests"
fi
+19
View File
@@ -0,0 +1,19 @@
var fixtures = fixtures || {};
fixtures.ajaxResps = {
ok: {
status: 200,
responseText: '[{ "value": "big" }, { "value": "bigger" }, { "value": "biggest" }, { "value": "small" }, { "value": "smaller" }, { "value": "smallest" }]'
},
ok1: {
status: 200,
responseText: '["dog", "cat", "moose"]'
},
err: {
status: 500
}
};
$.each(fixtures.ajaxResps, function(i, resp) {
resp.responseText && (resp.parsed = $.parseJSON(resp.responseText));
});
+128
View File
@@ -0,0 +1,128 @@
var fixtures = fixtures || {};
fixtures.data = {
simple: [
{ value: 'big' },
{ value: 'bigger' },
{ value: 'biggest' },
{ value: 'small' },
{ value: 'smaller' },
{ value: 'smallest' }
],
animals: [
{ value: 'dog' },
{ value: 'cat' },
{ value: 'moose' }
]
};
fixtures.serialized = {
simple: {
"datums": {
"{\"value\":\"big\"}": {
"value": "big"
},
"{\"value\":\"bigger\"}": {
"value": "bigger"
},
"{\"value\":\"biggest\"}": {
"value": "biggest"
},
"{\"value\":\"small\"}": {
"value": "small"
},
"{\"value\":\"smaller\"}": {
"value": "smaller"
},
"{\"value\":\"smallest\"}": {
"value": "smallest"
}
},
"trie": {
"i": [],
"c": {
"b": {
"i": ["{\"value\":\"big\"}", "{\"value\":\"bigger\"}", "{\"value\":\"biggest\"}"],
"c": {
"i": {
"i": ["{\"value\":\"big\"}", "{\"value\":\"bigger\"}", "{\"value\":\"biggest\"}"],
"c": {
"g": {
"i": ["{\"value\":\"big\"}", "{\"value\":\"bigger\"}", "{\"value\":\"biggest\"}"],
"c": {
"g": {
"i": ["{\"value\":\"bigger\"}", "{\"value\":\"biggest\"}"],
"c": {
"e": {
"i": ["{\"value\":\"bigger\"}", "{\"value\":\"biggest\"}"],
"c": {
"r": {
"i": ["{\"value\":\"bigger\"}"],
"c": {}
},
"s": {
"i": ["{\"value\":\"biggest\"}"],
"c": {
"t": {
"i": ["{\"value\":\"biggest\"}"],
"c": {}
}
}
}
}
}
}
}
}
}
}
}
}
},
"s": {
"i": ["{\"value\":\"small\"}", "{\"value\":\"smaller\"}", "{\"value\":\"smallest\"}"],
"c": {
"m": {
"i": ["{\"value\":\"small\"}", "{\"value\":\"smaller\"}", "{\"value\":\"smallest\"}"],
"c": {
"a": {
"i": ["{\"value\":\"small\"}", "{\"value\":\"smaller\"}", "{\"value\":\"smallest\"}"],
"c": {
"l": {
"i": ["{\"value\":\"small\"}", "{\"value\":\"smaller\"}", "{\"value\":\"smallest\"}"],
"c": {
"l": {
"i": ["{\"value\":\"small\"}", "{\"value\":\"smaller\"}", "{\"value\":\"smallest\"}"],
"c": {
"e": {
"i": ["{\"value\":\"smaller\"}", "{\"value\":\"smallest\"}"],
"c": {
"r": {
"i": ["{\"value\":\"smaller\"}"],
"c": {}
},
"s": {
"i": ["{\"value\":\"smallest\"}"],
"c": {
"t": {
"i": ["{\"value\":\"smallest\"}"],
"c": {}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
+13
View File
@@ -0,0 +1,13 @@
var fixtures = fixtures || {};
fixtures.html = {
input: '<input class="tt-input" type="text" autocomplete="false" spellcheck="false">',
hint: '<input class="tt-hint" type="text" autocomplete="false" spellcheck="false" disabled>',
dataset: [
'<div class="tt-dataset-test">',
'<div class="tt-selectable"><p>one</p></div>',
'<div class="tt-selectable"><p>two</p></div>',
'<div class="tt-selectable"><p>three</p></div>',
'</div>'
].join('')
};
@@ -0,0 +1,78 @@
(function(root) {
var components;
components = [
'Bloodhound',
'Prefetch',
'Remote',
'PersistentStorage',
'Transport',
'SearchIndex',
'Input',
'Dataset',
'Menu'
];
for (var i = 0; i < components.length; i++) {
makeMockable(components[i]);
}
function makeMockable(component) {
var Original, Mock;
Original = root[component];
Mock = mock(Original);
jasmine[component] = { useMock: useMock, uninstallMock: uninstallMock };
function useMock() {
root[component] = Mock;
jasmine.getEnv().currentSpec.after(uninstallMock);
}
function uninstallMock() {
root[component] = Original;
}
}
function mock(Constructor) {
var constructorSpy;
Mock.prototype = Constructor.prototype;
constructorSpy = jasmine.createSpy('mock constructor').andCallFake(Mock);
// copy instance methods
for (var key in Constructor) {
if (typeof Constructor[key] === 'function') {
constructorSpy[key] = Constructor[key];
}
}
return constructorSpy;
function Mock() {
var instance = _.mixin({}, Constructor.prototype);
for (var key in instance) {
if (typeof instance[key] === 'function') {
spyOn(instance, key);
// special case for some components
if (key === 'bind') {
instance[key].andCallFake(function() { return this; });
}
}
}
// have the event emitter methods call through
instance.onSync && instance.onSync.andCallThrough();
instance.onAsync && instance.onAsync.andCallThrough();
instance.off && instance.off.andCallThrough();
instance.trigger && instance.trigger.andCallThrough();
instance.constructor = Constructor;
return instance;
}
}
})(this);
@@ -0,0 +1,108 @@
<!DOCTYPE html>
<html>
<head>
<title></title>
<script src="../../bower_components/jquery/jquery.js"></script>
<script src="../../dist/typeahead.bundle.js"></script>
<style>
.container {
width: 800px;
margin: 50px auto;
}
.typeahead-wrapper {
display: block;
margin: 50px 0;
}
.tt-menu {
background-color: #fff;
border: 1px solid #000;
}
.tt-suggestion.tt-cursor {
background-color: #ccc;
}
</style>
</head>
<body>
<div class="container">
<form action="/where" method="GET">
<div class="typeahead-wrapper">
<input id="states" name="states" type="text">
<input type="submit">
</div>
</form>
</div>
<script>
var states = new Bloodhound({
datumTokenizer: Bloodhound.tokenizers.obj.whitespace('val'),
queryTokenizer: Bloodhound.tokenizers.whitespace,
local: [
{ val: 'Alabama' },
{ val: 'Alaska' },
{ val: 'Arizona' },
{ val: 'Arkansas' },
{ val: 'California' },
{ val: 'Colorado' },
{ val: 'Connecticut' },
{ val: 'Delaware' },
{ val: 'Florida' },
{ val: 'Georgia' },
{ val: 'Hawaii' },
{ val: 'Idaho' },
{ val: 'Illinois' },
{ val: 'Indiana' },
{ val: 'Iowa' },
{ val: 'Kansas' },
{ val: 'Kentucky' },
{ val: 'Louisiana' },
{ val: 'Maine' },
{ val: 'Maryland' },
{ val: 'Massachusetts' },
{ val: 'Michigan' },
{ val: 'Minnesota' },
{ val: 'Mississippi' },
{ val: 'Missouri' },
{ val: 'Montana' },
{ val: 'Nebraska' },
{ val: 'Nevada' },
{ val: 'New Hampshire' },
{ val: 'New Jersey' },
{ val: 'New Mexico' },
{ val: 'New York' },
{ val: 'North Carolina' },
{ val: 'North Dakota' },
{ val: 'Ohio' },
{ val: 'Oklahoma' },
{ val: 'Oregon' },
{ val: 'Pennsylvania' },
{ val: 'Rhode Island' },
{ val: 'South Carolina' },
{ val: 'South Dakota' },
{ val: 'Tennessee' },
{ val: 'Texas' },
{ val: 'Utah' },
{ val: 'Vermont' },
{ val: 'Virginia' },
{ val: 'Washington' },
{ val: 'West Virginia' },
{ val: 'Wisconsin' },
{ val: 'Wyoming' },
{ val: 'this is a very long value so deal with it' }
]
});
$('#states').typeahead({
highlight: true
},
{
display: 'val',
source: states
});
</script>
</body>
</html>
@@ -0,0 +1,395 @@
/* jshint esnext: true, evil: true, sub: true */
var wd = require('yiewd'),
colors = require('colors'),
expect = require('chai').expect,
_ = require('underscore'),
f = require('util').format,
env = process.env;
var browser, caps;
browser = (process.env.BROWSER || 'chrome').split(':');
caps = {
name: f('[%s] typeahead.js ui', browser.join(' , ')),
browserName: browser[0]
};
setIf(caps, 'version', browser[1]);
setIf(caps, 'platform', browser[2]);
setIf(caps, 'tunnel-identifier', env['TRAVIS_JOB_NUMBER']);
setIf(caps, 'build', env['TRAVIS_BUILD_NUMBER']);
setIf(caps, 'tags', env['CI'] ? ['CI'] : ['local']);
function setIf(obj, key, val) {
val && (obj[key] = val);
}
describe('jquery-typeahead.js', function() {
var driver, body, input, hint, dropdown, allPassed = true;
this.timeout(300000);
before(function(done) {
var host = 'ondemand.saucelabs.com', port = 80, username, password;
if (env['CI']) {
host = 'localhost';
port = 4445;
username = env['SAUCE_USERNAME'];
password = env['SAUCE_ACCESS_KEY'];
}
driver = wd.remote(host, port, username, password);
driver.configureHttp({
timeout: 30000,
retries: 5,
retryDelay: 200
});
driver.on('status', function(info) {
console.log(info.cyan);
});
driver.on('command', function(meth, path, data) {
console.log(' > ' + meth.yellow, path.grey, data || '');
});
driver.run(function*() {
yield this.init(caps);
yield this.get('http://localhost:8888/test/integration/test.html');
body = yield this.elementByTagName('body');
input = yield this.elementById('states');
hint = yield this.elementByClassName('tt-hint');
dropdown = yield this.elementByClassName('tt-menu');
done();
});
});
afterEach(function(done) {
allPassed = allPassed && (this.currentTest.state === 'passed');
driver.run(function*() {
yield body.click();
yield this.execute('window.jQuery("#states").typeahead("val", "")');
done();
});
});
after(function(done) {
driver.run(function*() {
yield this.quit();
yield driver.sauceJobStatus(allPassed);
done();
});
});
describe('on blur', function() {
it('should close dropdown', function(done) {
driver.run(function*() {
yield input.click();
yield input.type('mi');
expect(yield dropdown.isDisplayed()).to.equal(true);
yield body.click();
expect(yield dropdown.isDisplayed()).to.equal(false);
done();
});
});
it('should clear hint', function(done) {
driver.run(function*() {
yield input.click();
yield input.type('mi');
expect(yield hint.getValue()).to.equal('michigan');
yield body.click();
expect(yield hint.getValue()).to.equal('');
done();
});
});
});
describe('on query change', function() {
it('should open dropdown if suggestions', function(done) {
driver.run(function*() {
yield input.click();
yield input.type('mi');
expect(yield dropdown.isDisplayed()).to.equal(true);
done();
});
});
it('should close dropdown if no suggestions', function(done) {
driver.run(function*() {
yield input.click();
yield input.type('huh?');
expect(yield dropdown.isDisplayed()).to.equal(false);
done();
});
});
it('should render suggestions if suggestions', function(done) {
driver.run(function*() {
var suggestions;
yield input.click();
yield input.type('mi');
suggestions = yield dropdown.elementsByClassName('tt-suggestion');
expect(suggestions).to.have.length('4');
expect(yield suggestions[0].text()).to.equal('Michigan');
expect(yield suggestions[1].text()).to.equal('Minnesota');
expect(yield suggestions[2].text()).to.equal('Mississippi');
expect(yield suggestions[3].text()).to.equal('Missouri');
done();
});
});
it('should show hint if top suggestion is a match', function(done) {
driver.run(function*() {
yield input.click();
yield input.type('mi');
expect(yield hint.getValue()).to.equal('michigan');
done();
});
});
it('should match hint to query', function(done) {
driver.run(function*() {
yield input.click();
yield input.type('NeW JE');
expect(yield hint.getValue()).to.equal('NeW JErsey');
done();
});
});
it('should not show hint if top suggestion is not a match', function(done) {
driver.run(function*() {
yield input.click();
yield input.type('ham');
expect(yield hint.getValue()).to.equal('');
done();
});
});
it('should not show hint if there is query overflow', function(done) {
driver.run(function*() {
yield input.click();
yield input.type('this is a very long value so ');
expect(yield hint.getValue()).to.equal('');
done();
});
});
});
describe('on up arrow', function() {
it('should cycle through suggestions', function(done) {
driver.run(function*() {
var suggestions;
yield input.click();
yield input.type('mi');
suggestions = yield dropdown.elementsByClassName('tt-suggestion');
yield input.type(wd.SPECIAL_KEYS['Up arrow']);
expect(yield input.getValue()).to.equal('Missouri');
expect(yield suggestions[3].getAttribute('class')).to.equal('tt-suggestion tt-selectable tt-cursor');
yield input.type(wd.SPECIAL_KEYS['Up arrow']);
expect(yield input.getValue()).to.equal('Mississippi');
expect(yield suggestions[2].getAttribute('class')).to.equal('tt-suggestion tt-selectable tt-cursor');
yield input.type(wd.SPECIAL_KEYS['Up arrow']);
expect(yield input.getValue()).to.equal('Minnesota');
expect(yield suggestions[1].getAttribute('class')).to.equal('tt-suggestion tt-selectable tt-cursor');
yield input.type(wd.SPECIAL_KEYS['Up arrow']);
expect(yield input.getValue()).to.equal('Michigan');
expect(yield suggestions[0].getAttribute('class')).to.equal('tt-suggestion tt-selectable tt-cursor');
yield input.type(wd.SPECIAL_KEYS['Up arrow']);
expect(yield input.getValue()).to.equal('mi');
expect(yield suggestions[0].getAttribute('class')).to.equal('tt-suggestion tt-selectable');
expect(yield suggestions[1].getAttribute('class')).to.equal('tt-suggestion tt-selectable');
expect(yield suggestions[2].getAttribute('class')).to.equal('tt-suggestion tt-selectable');
expect(yield suggestions[3].getAttribute('class')).to.equal('tt-suggestion tt-selectable');
done();
});
});
});
describe('on down arrow', function() {
it('should cycle through suggestions', function(done) {
driver.run(function*() {
var suggestions;
yield input.click();
yield input.type('mi');
suggestions = yield dropdown.elementsByClassName('tt-suggestion');
yield input.type(wd.SPECIAL_KEYS['Down arrow']);
expect(yield input.getValue()).to.equal('Michigan');
expect(yield suggestions[0].getAttribute('class')).to.equal('tt-suggestion tt-selectable tt-cursor');
yield input.type(wd.SPECIAL_KEYS['Down arrow']);
expect(yield input.getValue()).to.equal('Minnesota');
expect(yield suggestions[1].getAttribute('class')).to.equal('tt-suggestion tt-selectable tt-cursor');
yield input.type(wd.SPECIAL_KEYS['Down arrow']);
expect(yield input.getValue()).to.equal('Mississippi');
expect(yield suggestions[2].getAttribute('class')).to.equal('tt-suggestion tt-selectable tt-cursor');
yield input.type(wd.SPECIAL_KEYS['Down arrow']);
expect(yield input.getValue()).to.equal('Missouri');
expect(yield suggestions[3].getAttribute('class')).to.equal('tt-suggestion tt-selectable tt-cursor');
yield input.type(wd.SPECIAL_KEYS['Down arrow']);
expect(yield input.getValue()).to.equal('mi');
expect(yield suggestions[0].getAttribute('class')).to.equal('tt-suggestion tt-selectable');
expect(yield suggestions[1].getAttribute('class')).to.equal('tt-suggestion tt-selectable');
expect(yield suggestions[2].getAttribute('class')).to.equal('tt-suggestion tt-selectable');
expect(yield suggestions[3].getAttribute('class')).to.equal('tt-suggestion tt-selectable');
done();
});
});
});
describe('on escape', function() {
it('should close dropdown', function(done) {
driver.run(function*() {
yield input.click();
yield input.type('mi');
expect(yield dropdown.isDisplayed()).to.equal(true);
yield input.type(wd.SPECIAL_KEYS['Escape']);
expect(yield dropdown.isDisplayed()).to.equal(false);
done();
});
});
it('should clear hint', function(done) {
driver.run(function*() {
yield input.click();
yield input.type('mi');
expect(yield hint.getValue()).to.equal('michigan');
yield input.type(wd.SPECIAL_KEYS['Escape']);
expect(yield hint.getValue()).to.equal('');
done();
});
});
});
describe('on tab', function() {
it('should autocomplete if hint is present', function(done) {
driver.run(function*() {
yield input.click();
yield input.type('mi');
yield input.type(wd.SPECIAL_KEYS['Tab']);
expect(yield input.getValue()).to.equal('Michigan');
done();
});
});
it('should select if cursor is on suggestion', function(done) {
driver.run(function*() {
var suggestions;
yield input.click();
yield input.type('mi');
suggestions = yield dropdown.elementsByClassName('tt-suggestion');
yield input.type(wd.SPECIAL_KEYS['Down arrow']);
yield input.type(wd.SPECIAL_KEYS['Down arrow']);
yield input.type(wd.SPECIAL_KEYS['Tab']);
expect(yield dropdown.isDisplayed()).to.equal(false);
expect(yield input.getValue()).to.equal('Minnesota');
done();
});
});
});
describe('on right arrow', function() {
it('should autocomplete if hint is present', function(done) {
driver.run(function*() {
yield input.click();
yield input.type('mi');
yield input.type(wd.SPECIAL_KEYS['Right arrow']);
expect(yield input.getValue()).to.equal('Michigan');
done();
});
});
});
describe('on suggestion click', function() {
it('should select suggestion', function(done) {
driver.run(function*() {
var suggestions;
yield input.click();
yield input.type('mi');
suggestions = yield dropdown.elementsByClassName('tt-suggestion');
yield suggestions[1].click();
expect(yield dropdown.isDisplayed()).to.equal(false);
expect(yield input.getValue()).to.equal('Minnesota');
done();
});
});
});
describe('on enter', function() {
it('should select if cursor is on suggestion', function(done) {
driver.run(function*() {
var suggestions;
yield input.click();
yield input.type('mi');
suggestions = yield dropdown.elementsByClassName('tt-suggestion');
yield input.type(wd.SPECIAL_KEYS['Down arrow']);
yield input.type(wd.SPECIAL_KEYS['Down arrow']);
yield input.type(wd.SPECIAL_KEYS['Return']);
expect(yield dropdown.isDisplayed()).to.equal(false);
expect(yield input.getValue()).to.equal('Minnesota');
done();
});
});
});
});
+346
View File
@@ -0,0 +1,346 @@
<!DOCTYPE html>
<html>
<head>
<script src="../bower_components/jquery/jquery.js"></script>
<script src="../dist/typeahead.bundle.js"></script>
<style>
.container {
width: 800px;
margin: 50px auto;
}
.typeahead-wrapper {
display: block;
margin: 50px 0;
}
.tt-dropdown-menu {
background-color: #fff;
border: 1px solid #000;
}
.tt-suggestion.tt-cursor {
background-color: #ccc;
}
.triggered-events {
float: right;
width: 500px;
height: 300px;
}
</style>
</head>
<body>
<div class="container">
<textarea class="triggered-events"></textarea>
<form action="/where" method="GET">
<div class="typeahead-wrapper">
<input class="states" name="states" type="text" placeholder="states" value="Michigan">
<input type="submit">
</div>
</form>
<div class="typeahead-wrapper">
<input class="bad-tokens" type="text" placeholder="bad tokens">
</div>
<div class="typeahead-wrapper">
<input class="regex-symbols" type="text" placeholder="regex symbols">
</div>
<div class="typeahead-wrapper">
<input class="header-footer" type="text" placeholder="header footer">
</div>
<div class="typeahead-wrapper">
<input class="ltr" type="text" placeholder="ltr">
</div>
<div class="typeahead-wrapper">
<input class="rtl" type="text" placeholder="rtl">
</div>
<div class="typeahead-wrapper">
<input class="mixed" type="text" placeholder="mixed">
</div>
</div>
</div>
<script>
var states = new Bloodhound({
datumTokenizer: Bloodhound.tokenizers.whitespace,
queryTokenizer: Bloodhound.tokenizers.whitespace,
local: [
'Alabama',
'Alaska',
'Arizona',
'Arkansas',
'California',
'Colorado',
'Connecticut',
'Delaware',
'Florida',
'Georgia',
'Hawaii',
'Idaho',
'Illinois',
'Indiana',
'Iowa',
'Kansas',
'Kentucky',
'Louisiana',
'Maine',
'Maryland',
'Massachusetts',
'Michigan',
'Minnesota',
'Mississippi',
'Missouri',
'Montana',
'Nebraska',
'Nevada',
'New Hampshire',
'New Jersey',
'New Mexico',
'New York',
'North Carolina',
'North Dakota',
'Ohio',
'Oklahoma',
'Oregon',
'Pennsylvania',
'Rhode Island',
'South Carolina',
'South Dakota',
'Tennessee',
'Texas',
'Utah',
'Vermont',
'Virginia',
'Washington',
'West Virginia',
'Wisconsin',
'Wyoming'
]
});
states.initialize();
$('.states').typeahead({
highlight: true
},
{
source: states
});
var badTokens = new Bloodhound({
datumTokenizer: function(d) { return d.tokens; },
queryTokenizer: Bloodhound.tokenizers.whitespace,
local: [
{
value1: 'all bad',
jake: '111',
tokens: [' ', ' ', null, undefined, false, 'all', 'bad']
},
{
value1: 'whitespace',
jake: '112',
tokens: [' ', ' ', '\t', '\n', 'whitespace']
},
{
value1: 'undefined',
jake: '113',
tokens: [undefined, 'undefined']
},
{
value1: 'null',
jake: '114',
tokens: [null, 'null']
},
{
value1: 'false',
jake: '115',
tokens: [false, 'false']
}
]
});
badTokens.initialize();
$('.bad-tokens').typeahead(null, {
displayKey: 'value1',
source: badTokens
});
var regexSymbols = new Bloodhound({
datumTokenizer: function(d) {
return Bloodhound.tokenizers.whitespace(d.val);
},
queryTokenizer: Bloodhound.tokenizers.whitespace,
local: [
{ val: '*.js' },
{ val: '[Tt]ypeahead.js' },
{ val: '^typeahead.js$' },
{ val: 'typeahead.js(0.8.2)' },
{ val: 'typeahead.js(@\\d.\\d.\\d)' },
{ val: 'typeahead.js@0.8.2' }
]
});
regexSymbols.initialize();
$('.regex-symbols').typeahead(null, {
displayKey: 'val',
source: regexSymbols
});
var abc = new Bloodhound({
datumTokenizer: function(d) {
return Bloodhound.tokenizers.whitespace(d.val);
},
queryTokenizer: Bloodhound.tokenizers.whitespace,
local: [
{ val: 'a' },
{ val: 'ab' },
{ val: 'abc' },
{ val: 'abcd' },
{ val: 'abcde' }
]
});
abc.initialize();
$('.header-footer').typeahead(null, {
displayKey: 'val',
source: abc,
templates: {
header: '<h5>Header</h5>',
footer: '<h5>Footer</h5>'
}
},
{
displayKey: 'val',
source: abc,
templates: {
header: '<h5>start</h5>',
footer: '<h5>end</h5>',
empty: '<h5>empty</h5>'
}
});
var ltr = new Bloodhound({
datumTokenizer: function(d) {
return Bloodhound.tokenizers.whitespace(d.val);
},
queryTokenizer: Bloodhound.tokenizers.whitespace,
local: [
{ val: "one" },
{ val: "two three" },
{ val: "four" },
{ val: "five six" },
{ val: "seven" }
]
});
ltr.initialize();
$('.ltr').typeahead({
highlight: true
},
{
displayKey: 'val',
source: ltr
});
var rtl = new Bloodhound({
datumTokenizer: function(d) {
return Bloodhound.tokenizers.whitespace(d.val);
},
queryTokenizer: Bloodhound.tokenizers.whitespace,
local: [
{ val: "שלום" },
{ val: "ערב טוב" },
{ val: "מה שלומך" },
{ val: "רב תודות" },
{ val: "אין דבר" }
]
});
rtl.initialize();
$('.rtl').typeahead({
highlight: true
},
{
displayKey: 'val',
source: rtl
});
var mixed = new Bloodhound({
datumTokenizer: function(d) {
return Bloodhound.tokenizers.whitespace(d.val);
},
queryTokenizer: Bloodhound.tokenizers.whitespace,
local: [
{ val: "שלום" },
{ val: "ערב טוב" },
{ val: "מה שלומך" },
{ val: "one" },
{ val: "two three" }
]
});
mixed.initialize();
$('.mixed').typeahead({
highlight: true
},
{
displayKey: 'val',
source: mixed
});
$('input').on([
'typeahead:active',
'typeahead:idle',
'typeahead:open',
'typeahead:close',
'typeahead:change',
'typeahead:render',
'typeahead:select',
'typeahead:autocomplete',
'typeahead:cursorchange',
].join(' '), logTypeaheadEvent);
$('form').on('submit', logSubmitEvent);
function logSubmitEvent($e) {
var text;
$e && $e.preventDefault();
text = JSON.stringify($(this).serializeArray());
writeToTextarea('submit', text);
}
function logTypeaheadEvent($e) {
var args, type, text;
args = [].slice.call(arguments, 1);
type = $e.type;
text = window.JSON ? JSON.stringify(args) : '';
writeToTextarea(type, text);
}
function writeToTextarea(/* lines */) {
var $textarea, val, text;
$textarea = $('.triggered-events');
val = $textarea.val();
text = [].join.call(arguments, '\n');
$textarea.val([val, text, '\n'].join('\n'));
$textarea[0].scrollTop = $textarea[0].scrollHeight;
}
</script>
</body>
</html>
@@ -0,0 +1,469 @@
describe('Dataset', function() {
var www = WWW(), mockSuggestions, mockSuggestionsDisplayFn;
mockSuggestions = [
{ value: 'one', raw: { value: 'one' } },
{ value: 'two', raw: { value: 'two' } },
{ value: 'html', raw: { value: '<b>html</b>' } }
];
mockSuggestionsDisplayFn = [
{ display: '4' },
{ display: '5' },
{ display: '6' }
];
beforeEach(function() {
this.dataset = new Dataset({
name: 'test',
node: $('<div>'),
source: this.source = jasmine.createSpy('source')
}, www);
});
it('should throw an error if source is missing', function() {
expect(noSource).toThrow();
function noSource() { new Dataset({}, www); }
});
it('should throw an error if the name is not a valid class name', function() {
expect(fn).toThrow();
function fn() {
var d = new Dataset({
name: 'a space',
node: $('<div>'),
source: $.noop
}, www);
}
});
describe('#getRoot', function() {
it('should return the root element', function() {
var sel = 'div' + www.selectors.dataset + www.selectors.dataset + '-test';
expect(this.dataset.$el).toBe(sel);
});
});
describe('#update', function() {
it('should render suggestions', function() {
this.source.andCallFake(syncMockSuggestions);
this.dataset.update('woah');
expect(this.dataset.$el).toContainText('one');
expect(this.dataset.$el).toContainText('two');
expect(this.dataset.$el).toContainText('html');
});
it('should escape html chars from display value when using default template', function() {
this.source.andCallFake(syncMockSuggestions);
this.dataset.update('woah');
expect(this.dataset.$el).toContainText('<b>html</b>');
});
it('should respect limit option', function() {
this.dataset.limit = 2;
this.source.andCallFake(syncMockSuggestions);
this.dataset.update('woah');
expect(this.dataset.$el).toContainText('one');
expect(this.dataset.$el).toContainText('two');
expect(this.dataset.$el).not.toContainText('three');
});
it('should allow custom display functions', function() {
this.dataset = new Dataset({
name: 'test',
node: $('<div>'),
display: function(o) { return o.display; },
source: this.source = jasmine.createSpy('source')
}, www);
this.source.andCallFake(syncMockSuggestionsDisplayFn);
this.dataset.update('woah');
expect(this.dataset.$el).toContainText('4');
expect(this.dataset.$el).toContainText('5');
expect(this.dataset.$el).toContainText('6');
});
it('should ignore async invocations of sync', function() {
this.source.andCallFake(asyncSync);
this.dataset.update('woah');
expect(this.dataset.$el).not.toContainText('one');
});
it('should ignore subesequent invocations of sync', function() {
this.source.andCallFake(multipleSync);
this.dataset.update('woah');
expect(this.dataset.$el.find('.tt-suggestion')).toHaveLength(3);
});
it('should trigger asyncRequested when needing/expecting backfill', function() {
var spy = jasmine.createSpy();
this.dataset.async = true;
this.dataset.onSync('asyncRequested', spy);
this.source.andCallFake(fakeGetWithAsyncSuggestions);
this.dataset.update('woah');
expect(spy).toHaveBeenCalled();
});
it('should not trigger asyncRequested when not expecting backfill', function() {
var spy = jasmine.createSpy();
this.dataset.async = false;
this.dataset.onSync('asyncRequested', spy);
this.source.andCallFake(fakeGetWithAsyncSuggestions);
this.dataset.update('woah');
expect(spy).not.toHaveBeenCalled();
});
it('should not trigger asyncRequested when not expecting backfill', function() {
var spy = jasmine.createSpy();
this.dataset.limit = 2;
this.dataset.async = true;
this.dataset.onSync('asyncRequested', spy);
this.source.andCallFake(fakeGetWithAsyncSuggestions);
this.dataset.update('woah');
expect(spy).not.toHaveBeenCalled();
});
it('should trigger asyncCanceled when pending aysnc is canceled', function() {
var spy = jasmine.createSpy();
this.dataset.async = true;
this.dataset.onSync('asyncCanceled', spy);
this.source.andCallFake(fakeGetWithAsyncSuggestions);
this.dataset.update('woah');
this.dataset.cancel();
waits(100);
runs(function() {
expect(spy).toHaveBeenCalled();
});
});
it('should not trigger asyncCanceled when cancel happens after update', function() {
var spy = jasmine.createSpy();
this.dataset.async = true;
this.dataset.onSync('asyncCanceled', spy);
this.source.andCallFake(fakeGetWithAsyncSuggestions);
this.dataset.update('woah');
waits(100);
runs(function() {
this.dataset.cancel();
expect(spy).not.toHaveBeenCalled();
});
});
it('should trigger asyncReceived when aysnc is received', function() {
var spy = jasmine.createSpy();
this.dataset.async = true;
this.dataset.onSync('asyncReceived', spy);
this.source.andCallFake(fakeGetWithAsyncSuggestions);
this.dataset.update('woah');
waits(100);
runs(function() {
expect(spy).toHaveBeenCalled();
});
});
it('should not trigger asyncReceived if canceled', function() {
var spy = jasmine.createSpy();
this.dataset.async = true;
this.dataset.onSync('asyncReceived', spy);
this.source.andCallFake(fakeGetWithAsyncSuggestions);
this.dataset.update('woah');
this.dataset.cancel();
waits(100);
runs(function() {
expect(spy).not.toHaveBeenCalled();
});
});
it('should not modify sync when async is added', function() {
var $test;
this.dataset.async = true;
this.source.andCallFake(fakeGetWithAsyncSuggestions);
this.dataset.update('woah');
$test = this.dataset.$el.find('.tt-suggestion').first();
$test.addClass('test');
waits(100);
runs(function() {
expect($test).toHaveClass('test');
});
});
it('should respect limit option in regard to async', function() {
this.dataset.async = true;
this.source.andCallFake(fakeGetWithAsyncSuggestions);
this.dataset.update('woah');
waits(100);
runs(function() {
expect(this.dataset.$el.find('.tt-suggestion')).toHaveLength(5);
});
});
it('should cancel pending async', function() {
var spy1 = jasmine.createSpy(), spy2 = jasmine.createSpy();
this.dataset.async = true;
this.dataset.onSync('asyncCanceled', spy1);
this.dataset.onSync('asyncReceived', spy2);
this.source.andCallFake(fakeGetWithAsyncSuggestions);
this.dataset.update('woah');
this.dataset.update('woah again');
waits(100);
runs(function() {
expect(spy1.callCount).toBe(1);
expect(spy2.callCount).toBe(1);
});
});
it('should render notFound when no suggestions are available', function() {
this.dataset = new Dataset({
source: this.source,
node: $('<div>'),
templates: {
notFound: '<h2>empty</h2>'
}
}, www);
this.source.andCallFake(syncEmptySuggestions);
this.dataset.update('woah');
expect(this.dataset.$el).toContainText('empty');
});
it('should render pending when no suggestions are available but async is pending', function() {
this.dataset = new Dataset({
source: this.source,
node: $('<div>'),
async: true,
templates: {
pending: '<h2>pending</h2>'
}
}, www);
this.source.andCallFake(syncEmptySuggestions);
this.dataset.update('woah');
expect(this.dataset.$el).toContainText('pending');
});
it('should render header when suggestions are rendered', function() {
this.dataset = new Dataset({
source: this.source,
node: $('<div>'),
templates: {
header: '<h2>header</h2>'
}
}, www);
this.source.andCallFake(syncMockSuggestions);
this.dataset.update('woah');
expect(this.dataset.$el).toContainText('header');
});
it('should render footer when suggestions are rendered', function() {
this.dataset = new Dataset({
source: this.source,
node: $('<div>'),
templates: {
footer: function(c) { return '<p>' + c.query + '</p>'; }
}
}, www);
this.source.andCallFake(syncMockSuggestions);
this.dataset.update('woah');
expect(this.dataset.$el).toContainText('woah');
});
it('should not render header/footer if there is no content', function() {
this.dataset = new Dataset({
source: this.source,
node: $('<div>'),
templates: {
header: '<h2>header</h2>',
footer: '<h2>footer</h2>'
}
}, www);
this.source.andCallFake(syncEmptySuggestions);
this.dataset.update('woah');
expect(this.dataset.$el).not.toContainText('header');
expect(this.dataset.$el).not.toContainText('footer');
});
it('should not render stale suggestions', function() {
this.source.andCallFake(fakeGetWithAsyncSuggestions);
this.dataset.update('woah');
this.source.andCallFake(syncMockSuggestions);
this.dataset.update('nelly');
waits(100);
runs(function() {
expect(this.dataset.$el).toContainText('one');
expect(this.dataset.$el).toContainText('two');
expect(this.dataset.$el).toContainText('html');
expect(this.dataset.$el).not.toContainText('four');
expect(this.dataset.$el).not.toContainText('five');
});
});
it('should not render async suggestions if update was canceled', function() {
this.source.andCallFake(fakeGetWithAsyncSuggestions);
this.dataset.update('woah');
this.dataset.cancel();
waits(100);
runs(function() {
var rendered = this.dataset.$el.find('.tt-suggestion');
expect(rendered).toHaveLength(3);
});
});
it('should trigger rendered after suggestions are rendered', function() {
var spy;
this.dataset.onSync('rendered', spy = jasmine.createSpy());
this.source.andCallFake(syncMockSuggestions);
this.dataset.update('woah');
waitsFor(function() { return spy.callCount; });
});
});
describe('#clear', function() {
it('should clear suggestions', function() {
this.source.andCallFake(syncMockSuggestions);
this.dataset.update('woah');
this.dataset.clear();
expect(this.dataset.$el).toBeEmpty();
});
it('should cancel pending updates', function() {
var spy;
this.source.andCallFake(syncMockSuggestions);
this.dataset.update('woah');
spy = spyOn(this.dataset, 'cancel');
this.dataset.clear();
expect(spy).toHaveBeenCalled();
});
it('should trigger cleared', function() {
var spy;
this.dataset.onSync('cleared', spy = jasmine.createSpy());
this.dataset.clear();
expect(spy).toHaveBeenCalled();
});
});
describe('#isEmpty', function() {
it('should return true when empty', function() {
expect(this.dataset.isEmpty()).toBe(true);
});
it('should return false when not empty', function() {
this.source.andCallFake(syncMockSuggestions);
this.dataset.update('woah');
expect(this.dataset.isEmpty()).toBe(false);
});
});
describe('#destroy', function() {
it('should set dataset element to dummy element', function() {
var $prevEl = this.dataset.$el;
this.dataset.destroy();
expect(this.dataset.$el).not.toBe($prevEl);
});
});
// helper functions
// ----------------
function syncEmptySuggestions(q, sync, async) {
sync([]);
}
function syncMockSuggestions(q, sync, async) {
sync(mockSuggestions);
}
function syncMockSuggestionsDisplayFn(q, sync, async) {
sync(mockSuggestionsDisplayFn);
}
function asyncSync(q, sync, async) {
setTimeout(function() { sync(mockSuggestions); }, 0);
}
function multipleSync(q, sync, async) {
sync(mockSuggestions);
sync(mockSuggestions);
}
function fakeGetWithAsyncSuggestions(query, sync, async) {
sync(mockSuggestions);
setTimeout(function() {
async([
{ value: 'four', raw: { value: 'four' } },
{ value: 'five', raw: { value: 'five' } },
{ value: 'six', raw: { value: 'six' } },
{ value: 'seven', raw: { value: 'seven' } },
{ value: 'eight', raw: { value: 'eight' } },
]);
}, 0);
}
});
@@ -0,0 +1,103 @@
describe('DefaultMenu', function() {
var www = WWW();
beforeEach(function() {
var $fixture;
jasmine.Dataset.useMock();
setFixtures('<div id="menu-fixture"></div>');
$fixture = $('#jasmine-fixtures');
this.$node = $fixture.find('#menu-fixture');
this.$node.html(fixtures.html.dataset);
this.view = new DefaultMenu({ node: this.$node, datasets: [{}] }, www).bind();
this.dataset = this.view.datasets[0];
});
describe('when rendered is triggered on a dataset', function() {
it('should hide menu if empty', function() {
this.dataset.isEmpty.andReturn(true);
this.view._show();
this.dataset.trigger('rendered');
expect(this.$node).not.toBeVisible();
});
it('should not show menu if not open', function() {
this.dataset.isEmpty.andReturn(false);
this.view._hide();
this.dataset.trigger('rendered');
expect(this.$node).not.toBeVisible();
});
it('should show menu if not empty and open', function() {
this.dataset.isEmpty.andReturn(false);
this.view._hide();
this.view.open();
this.dataset.trigger('rendered');
expect(this.$node).toBeVisible();
});
});
describe('when cleared is triggered on a dataset', function() {
it('should hide menu if empty', function() {
this.dataset.isEmpty.andReturn(true);
this.view._show();
this.dataset.trigger('cleared');
expect(this.$node).not.toBeVisible();
});
it('should not show menu if not open', function() {
this.dataset.isEmpty.andReturn(false);
this.view._hide();
this.dataset.trigger('cleared');
expect(this.$node).not.toBeVisible();
});
it('should show menu if not empty and open', function() {
this.dataset.isEmpty.andReturn(false);
this.view._hide();
this.view.open();
this.dataset.trigger('cleared');
expect(this.$node).toBeVisible();
});
});
describe('#open', function() {
it('should show menu if not empty', function() {
spyOn(this.view, '_allDatasetsEmpty').andReturn(false);
this.view.open();
expect(this.$node[0].getAttribute('style')).toMatch(/display: block/);
});
it('should not show menu if empty', function() {
spyOn(this.view, '_allDatasetsEmpty').andReturn(true);
this.view.open();
expect(this.$node).not.toHaveAttr('style', 'display: block;');
});
});
describe('#close', function() {
it('should hide menu', function() {
this.view._show();
this.view.close();
expect(this.$node).not.toBeVisible();
});
});
});
@@ -0,0 +1,42 @@
describe('EventBus', function() {
beforeEach(function() {
var $fixture;
setFixtures(fixtures.html.input);
$fixture = $('#jasmine-fixtures');
this.$el = $fixture.find('.tt-input');
this.eventBus = new EventBus({ el: this.$el });
});
it('#trigger should trigger event', function() {
var spy = jasmine.createSpy();
this.$el.on('typeahead:fiz', spy);
this.eventBus.trigger('fiz');
expect(spy).toHaveBeenCalled();
});
it('#before should return false if default was not prevented', function() {
var spy = jasmine.createSpy();
this.$el.on('typeahead:beforefiz', spy);
expect(this.eventBus.before('fiz')).toBe(false);
expect(spy).toHaveBeenCalled();
});
it('#before should return true if default was prevented', function() {
var spy = jasmine.createSpy().andCallFake(prevent);
this.$el.on('typeahead:beforefiz', spy);
expect(this.eventBus.before('fiz')).toBe(true);
expect(spy).toHaveBeenCalled();
function prevent($e) { $e.preventDefault(); }
});
});
@@ -0,0 +1,111 @@
describe('EventEmitter', function() {
beforeEach(function() {
this.spy = jasmine.createSpy();
this.target = _.mixin({}, EventEmitter);
});
it('methods should be chainable', function() {
expect(this.target.onSync()).toEqual(this.target);
expect(this.target.onAsync()).toEqual(this.target);
expect(this.target.off()).toEqual(this.target);
expect(this.target.trigger()).toEqual(this.target);
});
it('#on should take the context a callback should be called in', function() {
var context = { val: 3 }, cbContext;
this.target.onSync('xevent', setCbContext, context).trigger('xevent');
waitsFor(assertCbContext, 'callback was called in the wrong context');
function setCbContext() { cbContext = this; }
function assertCbContext() { return cbContext === context; }
});
it('#onAsync callbacks should be invoked asynchronously', function() {
this.target.onAsync('event', this.spy).trigger('event');
expect(this.spy.callCount).toBe(0);
waitsFor(assertCallCount(this.spy, 1), 'the callback was not invoked');
});
it('#onSync callbacks should be invoked synchronously', function() {
this.target.onSync('event', this.spy).trigger('event');
expect(this.spy.callCount).toBe(1);
});
it('#off should remove callbacks', function() {
this.target
.onSync('event1 event2', this.spy)
.onAsync('event1 event2', this.spy)
.off('event1 event2')
.trigger('event1 event2');
waits(100);
runs(assertCallCount(this.spy, 0));
});
it('methods should accept multiple event types', function() {
this.target
.onSync('event1 event2', this.spy)
.onAsync('event1 event2', this.spy)
.trigger('event1 event2');
expect(this.spy.callCount).toBe(2);
waitsFor(assertCallCount(this.spy, 4), 'the callback was not invoked');
});
it('the event type should be passed to the callback', function() {
this.target
.onSync('sync', this.spy)
.onAsync('async', this.spy)
.trigger('sync async');
waitsFor(assertArgs(this.spy, 0, ['sync']), 'bad args');
waitsFor(assertArgs(this.spy, 1, ['async']), 'bad args');
});
it('arbitrary args should be passed to the callback', function() {
this.target
.onSync('event', this.spy)
.onAsync('event', this.spy)
.trigger('event', 1, 2);
waitsFor(assertArgs(this.spy, 0, ['event', 1, 2]), 'bad args');
waitsFor(assertArgs(this.spy, 1, ['event', 1, 2]), 'bad args');
});
it('callback execution should be cancellable', function() {
var cancelSpy = jasmine.createSpy().andCallFake(cancel);
this.target
.onSync('one', cancelSpy)
.onSync('one', this.spy)
.onAsync('two', cancelSpy)
.onAsync('two', this.spy)
.onSync('three', cancelSpy)
.onAsync('three', this.spy)
.trigger('one two three');
waitsFor(assertCallCount(cancelSpy, 3));
waitsFor(assertCallCount(this.spy, 0));
function cancel() { return false; }
});
function assertCallCount(spy, expected) {
return function() { return spy.callCount === expected; };
}
function assertArgs(spy, call, expected) {
return function() {
var env = jasmine.getEnv(),
actual = spy.calls[call] ? spy.calls[call].args : undefined;
return env.equals_(actual, expected);
};
}
});
@@ -0,0 +1,117 @@
describe('highlight', function() {
it('should allow tagName to be specified', function() {
var before = 'abcde',
after = 'a<span>bcd</span>e',
testNode = buildTestNode(before);
highlight({ node: testNode, pattern: 'bcd', tagName: 'span' });
expect(testNode.innerHTML).toEqual(after);
});
it('should allow className to be specified', function() {
var before = 'abcde',
after = 'a<strong class="one two">bcd</strong>e',
testNode = buildTestNode(before);
highlight({ node: testNode, pattern: 'bcd', className: 'one two' });
expect(testNode.innerHTML).toEqual(after);
});
it('should be case insensitive by default', function() {
var before = 'ABCDE',
after = 'A<strong>BCD</strong>E',
testNode = buildTestNode(before);
highlight({ node: testNode, pattern: 'bcd' });
expect(testNode.innerHTML).toEqual(after);
});
it('should support case sensitivity', function() {
var before = 'ABCDE',
after = 'ABCDE',
testNode = buildTestNode(before);
highlight({ node: testNode, pattern: 'bcd', caseSensitive: true });
expect(testNode.innerHTML).toEqual(after);
});
it('should support words only matching', function() {
var before = 'tone one phone',
after = 'tone <strong>one</strong> phone',
testNode = buildTestNode(before);
highlight({ node: testNode, pattern: 'one', wordsOnly: true });
expect(testNode.innerHTML).toEqual(after);
});
it('should support matching multiple patterns', function() {
var before = 'tone one phone',
after = '<strong>tone</strong> one <strong>phone</strong>',
testNode = buildTestNode(before);
highlight({ node: testNode, pattern: ['tone', 'phone'] });
expect(testNode.innerHTML).toEqual(after);
});
it('should support regex chars in the pattern', function() {
var before = '*.js when?',
after = '<strong>*.</strong>js when<strong>?</strong>',
testNode = buildTestNode(before);
highlight({ node: testNode, pattern: ['*.', '?'] });
expect(testNode.innerHTML).toEqual(after);
});
it('should work on complex html structures', function() {
var before = [
'<div>abcde',
'<span>abcde</span>',
'<div><p>abcde</p></div>',
'</div>'
].join(''),
after = [
'<div><strong>abc</strong>de',
'<span><strong>abc</strong>de</span>',
'<div><p><strong>abc</strong>de</p></div>',
'</div>'
].join(''),
testNode = buildTestNode(before);
highlight({ node: testNode, pattern: 'abc' });
expect(testNode.innerHTML).toEqual(after);
});
it('should ignore html tags and attributes', function() {
var before = '<span class="class"></span>',
after = '<span class="class"></span>',
testNode = buildTestNode(before);
highlight({ node: testNode, pattern: ['span', 'class'] });
expect(testNode.innerHTML).toEqual(after);
});
it('should not match across tags', function() {
var before = 'a<span>b</span>c',
after = 'a<span>b</span>c',
testNode = buildTestNode(before);
highlight({ node: testNode, pattern: 'abc' });
expect(testNode.innerHTML).toEqual(after);
});
it('should ignore html comments', function() {
var before = '<!-- abc -->',
after = '<!-- abc -->',
testNode = buildTestNode(before);
highlight({ node: testNode, pattern: 'abc' });
expect(testNode.innerHTML).toEqual(after);
});
function buildTestNode(content) {
var node = document.createElement('div');
node.innerHTML = content;
return node;
}
});
@@ -0,0 +1,538 @@
describe('Input', function() {
var KEYS, www;
KEYS = {
enter: 13,
esc: 27,
tab: 9,
left: 37,
right: 39,
up: 38,
down: 40,
normal: 65 // "A" key
};
www = WWW();
beforeEach(function() {
var $fixture;
setFixtures(fixtures.html.input + fixtures.html.hint);
$fixture = $('#jasmine-fixtures');
this.$input = $fixture.find('.tt-input');
this.$hint = $fixture.find('.tt-hint');
this.view = new Input({ input: this.$input, hint: this.$hint }, www).bind();
});
it('should throw an error if no input is provided', function() {
expect(noInput).toThrow();
function noInput() { new Input({}, www); }
});
describe('when the blur DOM event is triggered', function() {
it('should reset the input value', function() {
this.view.setQuery('wine');
this.view.setInputValue('cheese');
this.$input.blur();
expect(this.$input.val()).toBe('wine');
});
it('should trigger blurred', function() {
var spy;
this.view.onSync('blurred', spy = jasmine.createSpy());
this.$input.blur();
expect(spy).toHaveBeenCalled();
});
});
describe('when the focus DOM event is triggered', function() {
it('should update queryWhenFocused', function() {
this.view.setQuery('hi');
this.$input.focus();
expect(this.view.hasQueryChangedSinceLastFocus()).toBe(false);
this.view.setQuery('bye');
expect(this.view.hasQueryChangedSinceLastFocus()).toBe(true);
});
it('should trigger focused', function() {
var spy;
this.view.onSync('focused', spy = jasmine.createSpy());
this.$input.focus();
expect(spy).toHaveBeenCalled();
});
});
describe('when the keydown DOM event is triggered by tab', function() {
it('should trigger tabKeyed if no modifiers were pressed', function() {
var spy;
this.view.onSync('tabKeyed', spy = jasmine.createSpy());
simulateKeyEvent(this.$input, 'keydown', KEYS.tab);
expect(spy).toHaveBeenCalled();
});
it('should not trigger tabKeyed if modifiers were pressed', function() {
var spy;
this.view.onSync('tabKeyed', spy = jasmine.createSpy());
simulateKeyEvent(this.$input, 'keydown', KEYS.tab, true);
expect(spy).not.toHaveBeenCalled();
});
});
describe('when the keydown DOM event is triggered by esc', function() {
it('should trigger escKeyed', function() {
var spy;
this.view.onSync('escKeyed', spy = jasmine.createSpy());
simulateKeyEvent(this.$input, 'keydown', KEYS.esc);
expect(spy).toHaveBeenCalled();
});
});
describe('when the keydown DOM event is triggered by left', function() {
it('should trigger leftKeyed', function() {
var spy;
this.view.onSync('leftKeyed', spy = jasmine.createSpy());
simulateKeyEvent(this.$input, 'keydown', KEYS.left);
expect(spy).toHaveBeenCalled();
});
});
describe('when the keydown DOM event is triggered by right', function() {
it('should trigger rightKeyed', function() {
var spy;
this.view.onSync('rightKeyed', spy = jasmine.createSpy());
simulateKeyEvent(this.$input, 'keydown', KEYS.right);
expect(spy).toHaveBeenCalled();
});
});
describe('when the keydown DOM event is triggered by enter', function() {
it('should trigger enterKeyed', function() {
var spy;
this.view.onSync('enterKeyed', spy = jasmine.createSpy());
simulateKeyEvent(this.$input, 'keydown', KEYS.enter);
expect(spy).toHaveBeenCalled();
});
});
describe('when the keydown DOM event is triggered by up', function() {
it('should trigger upKeyed', function() {
var spy;
this.view.onSync('upKeyed', spy = jasmine.createSpy());
simulateKeyEvent(this.$input, 'keydown', KEYS.up);
expect(spy).toHaveBeenCalled();
});
it('should prevent default if no modifers were pressed', function() {
var $e = simulateKeyEvent(this.$input, 'keydown', KEYS.up);
expect($e.preventDefault).toHaveBeenCalled();
});
it('should not prevent default if modifers were pressed', function() {
var $e = simulateKeyEvent(this.$input, 'keydown', KEYS.up, true);
expect($e.preventDefault).not.toHaveBeenCalled();
});
});
describe('when the keydown DOM event is triggered by down', function() {
it('should trigger downKeyed', function() {
var spy;
this.view.onSync('downKeyed', spy = jasmine.createSpy());
simulateKeyEvent(this.$input, 'keydown', KEYS.down);
expect(spy).toHaveBeenCalled();
});
it('should prevent default if no modifers were pressed', function() {
var $e = simulateKeyEvent(this.$input, 'keydown', KEYS.down);
expect($e.preventDefault).toHaveBeenCalled();
});
it('should not prevent default if modifers were pressed', function() {
var $e = simulateKeyEvent(this.$input, 'keydown', KEYS.down, true);
expect($e.preventDefault).not.toHaveBeenCalled();
});
});
// NOTE: have to treat these as async because the ie polyfill acts
// in a async manner
describe('when the input DOM event is triggered', function() {
it('should update query', function() {
this.view.setQuery('wine');
this.view.setInputValue('cheese');
simulateInputEvent(this.$input);
waitsFor(function() { return this.view.getQuery() === 'cheese'; });
});
it('should trigger queryChanged if the query changed', function() {
var spy;
this.view.setQuery('wine');
this.view.setInputValue('cheese');
this.view.onSync('queryChanged', spy = jasmine.createSpy());
simulateInputEvent(this.$input);
expect(spy).toHaveBeenCalled();
});
it('should trigger whitespaceChanged if whitespace changed', function() {
var spy;
this.view.setQuery('wine bar');
this.view.setInputValue('wine bar');
this.view.onSync('whitespaceChanged', spy = jasmine.createSpy());
simulateInputEvent(this.$input);
expect(spy).toHaveBeenCalled();
});
it('should clear hint if invalid', function() {
spyOn(this.view, 'clearHintIfInvalid');
simulateInputEvent(this.$input);
expect(this.view.clearHintIfInvalid).toHaveBeenCalled();
});
it('should check lang direction', function() {
var spy;
this.$input.css('direction', 'rtl');
this.view.onSync('langDirChanged', spy = jasmine.createSpy());
simulateInputEvent(this.$input);
expect(this.view.dir).toBe('rtl');
expect(this.$hint).toHaveAttr('dir', 'rtl');
expect(spy).toHaveBeenCalled();
});
});
describe('.normalizeQuery', function() {
it('should strip leading whitespace', function() {
expect(Input.normalizeQuery(' foo')).toBe('foo');
});
it('should condense whitespace', function() {
expect(Input.normalizeQuery('foo bar')).toBe('foo bar');
});
it('should play nice with non-string values', function() {
expect(Input.normalizeQuery(2)).toBe('2');
expect(Input.normalizeQuery([])).toBe('');
expect(Input.normalizeQuery(null)).toBe('');
expect(Input.normalizeQuery(undefined)).toBe('');
expect(Input.normalizeQuery(false)).toBe('false');
});
});
describe('#focus', function() {
it('should focus the input', function() {
this.$input.blur();
this.view.focus();
expect(this.$input).toBeFocused();
});
});
describe('#blur', function() {
it('should blur the input', function() {
this.$input.focus();
this.view.blur();
expect(this.$input).not.toBeFocused();
});
});
describe('#getQuery', function() {
it('should act as getter to the query property', function() {
this.view.setQuery('mouse');
expect(this.view.getQuery()).toBe('mouse');
});
});
describe('#setQuery', function() {
it('should act as setter to the query property', function() {
this.view.setQuery('mouse');
expect(this.view.getQuery()).toBe('mouse');
});
it('should update input value', function() {
this.view.setQuery('mouse');
expect(this.view.getInputValue()).toBe('mouse');
});
it('should trigger queryChanged if the query changed', function() {
var spy;
this.view.setQuery('wine');
this.view.onSync('queryChanged', spy = jasmine.createSpy());
this.view.setQuery('cheese');
expect(spy).toHaveBeenCalled();
});
it('should trigger whitespaceChanged if whitespace changed', function() {
var spy;
this.view.setQuery('wine bar');
this.view.onSync('whitespaceChanged', spy = jasmine.createSpy());
this.view.setQuery('wine bar');
expect(spy).toHaveBeenCalled();
});
it('should clear hint if invalid', function() {
spyOn(this.view, 'clearHintIfInvalid');
simulateInputEvent(this.$input);
expect(this.view.clearHintIfInvalid).toHaveBeenCalled();
});
});
describe('#hasQueryChangedSinceLastFocus', function() {
it('should return true if the query has changed since focus', function() {
this.view.setQuery('hi');
this.$input.focus();
this.view.setQuery('bye');
expect(this.view.hasQueryChangedSinceLastFocus()).toBe(true);
});
it('should return false if the query has not changed since focus', function() {
this.view.setQuery('hi');
this.$input.focus();
expect(this.view.hasQueryChangedSinceLastFocus()).toBe(false);
});
});
describe('#getInputValue', function() {
it('should act as getter to the input value', function() {
this.$input.val('cheese');
expect(this.view.getInputValue()).toBe('cheese');
});
});
describe('#setInputValue', function() {
it('should act as setter to the input value', function() {
this.view.setInputValue('cheese');
expect(this.view.getInputValue()).toBe('cheese');
});
it('should clear hint if invalid', function() {
spyOn(this.view, 'clearHintIfInvalid');
this.view.setInputValue('cheese head');
expect(this.view.clearHintIfInvalid).toHaveBeenCalled();
});
it('should check lang direction', function() {
var spy;
this.$input.css('direction', 'rtl');
this.view.onSync('langDirChanged', spy = jasmine.createSpy());
simulateInputEvent(this.$input);
expect(this.view.dir).toBe('rtl');
expect(this.$hint).toHaveAttr('dir', 'rtl');
expect(spy).toHaveBeenCalled();
});
});
describe('#getHint/#setHint', function() {
it('should act as getter/setter to value of hint', function() {
this.view.setHint('mountain');
expect(this.view.getHint()).toBe('mountain');
});
});
describe('#resetInputValue', function() {
it('should reset input value to last query', function() {
this.view.setQuery('cheese');
this.view.setInputValue('wine');
this.view.resetInputValue();
expect(this.view.getInputValue()).toBe('cheese');
});
});
describe('#clearHint', function() {
it('should set the hint value to the empty string', function() {
this.view.setHint('cheese');
this.view.clearHint();
expect(this.view.getHint()).toBe('');
});
});
describe('#clearHintIfInvalid', function() {
it('should clear hint if input value is empty string', function() {
this.view.setInputValue('');
this.view.setHint('cheese');
this.view.clearHintIfInvalid();
expect(this.view.getHint()).toBe('');
});
it('should clear hint if input value is not prefix of input', function() {
this.view.setInputValue('milk');
this.view.setHint('cheese');
this.view.clearHintIfInvalid();
expect(this.view.getHint()).toBe('');
});
it('should clear hint if overflow exists', function() {
spyOn(this.view, 'hasOverflow').andReturn(true);
this.view.setInputValue('che');
this.view.setHint('cheese');
this.view.clearHintIfInvalid();
expect(this.view.getHint()).toBe('');
});
it('should not clear hint if input value is prefix of input', function() {
this.view.setInputValue('che');
this.view.setHint('cheese');
this.view.clearHintIfInvalid();
expect(this.view.getHint()).toBe('cheese');
});
});
describe('#hasOverflow', function() {
it('should return true if the input has overflow text', function() {
var longStr = new Array(1000).join('a');
this.view.setInputValue(longStr);
expect(this.view.hasOverflow()).toBe(true);
});
it('should return false if the input has no overflow text', function() {
var shortStr = 'aah';
this.view.setInputValue(shortStr);
expect(this.view.hasOverflow()).toBe(false);
});
});
describe('#isCursorAtEnd', function() {
it('should return true if the text cursor is at the end', function() {
this.view.setInputValue('boo');
setCursorPosition(this.$input, 3);
expect(this.view.isCursorAtEnd()).toBe(true);
});
it('should return false if the text cursor is not at the end', function() {
this.view.setInputValue('boo');
setCursorPosition(this.$input, 1);
expect(this.view.isCursorAtEnd()).toBe(false);
});
});
describe('#destroy', function() {
it('should remove event handlers', function() {
var $input, $hint;
$hint = this.view.$hint;
$input = this.view.$input;
spyOn($hint, 'off');
spyOn($input, 'off');
this.view.destroy();
expect($hint.off).toHaveBeenCalledWith('.tt');
expect($input.off).toHaveBeenCalledWith('.tt');
});
it('should set references to DOM elements to dummy element', function() {
var $hint, $input, $overflowHelper;
$hint = this.view.$hint;
$input = this.view.$input;
$overflowHelper = this.view.$overflowHelper;
this.view.destroy();
expect(this.view.$hint).not.toBe($hint);
expect(this.view.$input).not.toBe($input);
expect(this.view.$overflowHelper).not.toBe($overflowHelper);
});
});
// helper functions
// ----------------
function simulateInputEvent($node) {
var $e, type;
type = _.isMsie() ? 'keypress' : 'input';
$e = $.Event(type);
$node.trigger($e);
}
function simulateKeyEvent($node, type, key, withModifier) {
var $e;
$e = $.Event(type, {
keyCode: key,
altKey: !!withModifier,
ctrlKey: !!withModifier,
metaKey: !!withModifier,
shiftKey: !!withModifier
});
spyOn($e, 'preventDefault');
$node.trigger($e);
return $e;
}
function setCursorPosition($input, pos) {
var input = $input[0], range;
if (input.setSelectionRange) {
input.focus();
input.setSelectionRange(pos, pos);
}
else if (input.createTextRange) {
range = input.createTextRange();
range.collapse(true);
range.moveEnd('character', pos);
range.moveStart('character', pos);
range.select();
}
}
});
@@ -0,0 +1,197 @@
describe('$plugin', function() {
beforeEach(function() {
var $fixture;
setFixtures('<input class="test-input" type="text" autocomplete="on">');
$fixture = $('#jasmine-fixtures');
this.$input = $fixture.find('.test-input');
this.$input.typeahead(null, {
displayKey: 'v',
source: function(q, sync) {
sync([{ v: '1' }, { v: '2' }, { v: '3' }]);
}
});
});
it('#enable should enable the typaahead', function() {
this.$input.typeahead('disable');
expect(this.$input.typeahead('isEnabled')).toBe(false);
this.$input.typeahead('enable');
expect(this.$input.typeahead('isEnabled')).toBe(true);
});
it('#disable should disable the typaahead', function() {
this.$input.typeahead('enable');
expect(this.$input.typeahead('isEnabled')).toBe(true);
this.$input.typeahead('disable');
expect(this.$input.typeahead('isEnabled')).toBe(false);
});
it('#activate should activate the typaahead', function() {
this.$input.typeahead('deactivate');
expect(this.$input.typeahead('isActive')).toBe(false);
this.$input.typeahead('activate');
expect(this.$input.typeahead('isActive')).toBe(true);
});
it('#activate should fail to activate the typaahead if disabled', function() {
this.$input.typeahead('deactivate');
expect(this.$input.typeahead('isActive')).toBe(false);
this.$input.typeahead('disable');
this.$input.typeahead('activate');
expect(this.$input.typeahead('isActive')).toBe(false);
});
it('#deactivate should deactivate the typaahead', function() {
this.$input.typeahead('activate');
expect(this.$input.typeahead('isActive')).toBe(true);
this.$input.typeahead('deactivate');
expect(this.$input.typeahead('isActive')).toBe(false);
});
it('#open should open the menu', function() {
this.$input.typeahead('close');
expect(this.$input.typeahead('isOpen')).toBe(false);
this.$input.typeahead('open');
expect(this.$input.typeahead('isOpen')).toBe(true);
});
it('#close should close the menu', function() {
this.$input.typeahead('open');
expect(this.$input.typeahead('isOpen')).toBe(true);
this.$input.typeahead('close');
expect(this.$input.typeahead('isOpen')).toBe(false);
});
it('#select should select selectable', function() {
var $el;
// activate and set val to render some selectables
this.$input.typeahead('activate');
this.$input.typeahead('val', 'o');
$el = $('.tt-selectable').first();
expect(this.$input.typeahead('select', $el)).toBe(true);
expect(this.$input.typeahead('val')).toBe('1');
});
it('#select should return false if not valid selectable', function() {
var body;
// activate and set val to render some selectables
this.$input.typeahead('activate');
this.$input.typeahead('val', 'o');
body = document.body;
expect(this.$input.typeahead('select', body)).toBe(false);
});
it('#autocomplete should autocomplete to selectable', function() {
var $el;
// activate and set val to render some selectables
this.$input.typeahead('activate');
this.$input.typeahead('val', 'o');
$el = $('.tt-selectable').first();
expect(this.$input.typeahead('autocomplete', $el)).toBe(true);
expect(this.$input.typeahead('val')).toBe('1');
});
it('#autocomplete should return false if not valid selectable', function() {
var body;
// activate and set val to render some selectables
this.$input.typeahead('activate');
this.$input.typeahead('val', 'o');
body = document.body;
expect(this.$input.typeahead('autocomplete', body)).toBe(false);
});
it('#moveCursor should move cursor', function() {
var $el;
// activate and set val to render some selectables
this.$input.typeahead('activate');
this.$input.typeahead('val', 'o');
$el = $('.tt-selectable').first();
expect($el).not.toHaveClass('tt-cursor');
expect(this.$input.typeahead('moveCursor', 1)).toBe(true);
expect($el).toHaveClass('tt-cursor');
});
it('#select should return false if not valid selectable', function() {
var body;
// activate and set val to render some selectables
this.$input.typeahead('activate');
this.$input.typeahead('val', 'o');
body = document.body;
expect(this.$input.typeahead('select', body)).toBe(false);
});
it('#val() should typeahead value of element', function() {
var $els;
this.$input.typeahead('val', 'foo');
$els = this.$input.add('<div>');
expect($els.typeahead('val')).toBe('foo');
});
it('#val(q) should set query', function() {
this.$input.typeahead('val', 'foo');
expect(this.$input.typeahead('val')).toBe('foo');
});
it('#destroy should revert modified attributes', function() {
expect(this.$input).toHaveAttr('autocomplete', 'off');
expect(this.$input).toHaveAttr('dir');
expect(this.$input).toHaveAttr('spellcheck');
expect(this.$input).toHaveAttr('style');
this.$input.typeahead('destroy');
expect(this.$input).toHaveAttr('autocomplete', 'on');
expect(this.$input).not.toHaveAttr('dir');
expect(this.$input).not.toHaveAttr('spellcheck');
expect(this.$input).not.toHaveAttr('style');
});
it('#destroy should remove data', function() {
expect(this.$input.data('tt-www')).toBeTruthy();
expect(this.$input.data('tt-attrs')).toBeTruthy();
expect(this.$input.data('tt-typeahead')).toBeTruthy();
this.$input.typeahead('destroy');
expect(this.$input.data('tt-www')).toBeFalsy();
expect(this.$input.data('tt-attrs')).toBeFalsy();
expect(this.$input.data('tt-typeahead')).toBeFalsy();
});
it('#destroy should remove add classes', function() {
expect(this.$input).toHaveClass('tt-input');
this.$input.typeahead('destroy');
expect(this.$input).not.toHaveClass('tt-input');
});
it('#destroy should revert DOM changes', function() {
expect($('.twitter-typeahead')).toExist();
this.$input.typeahead('destroy');
expect($('.twitter-typeahead')).not.toExist();
});
});
@@ -0,0 +1,332 @@
describe('Menu', function() {
var www = WWW();
beforeEach(function() {
var $fixture;
jasmine.Dataset.useMock();
setFixtures('<div id="menu-fixture"></div>');
$fixture = $('#jasmine-fixtures');
this.$node = $fixture.find('#menu-fixture');
this.$node.html(fixtures.html.dataset);
this.view = new Menu({ node: this.$node, datasets: [{}] }, www).bind();
this.dataset = this.view.datasets[0];
});
it('should throw an error if node is missing', function() {
expect(noNode).toThrow();
function noNode() { new Menu({ datasets: [{}] }, www); }
});
describe('when click event is triggered on a selectable', function() {
it('should trigger selectableClicked', function() {
var spy;
this.view.onSync('selectableClicked', spy = jasmine.createSpy());
this.$node.find(www.selectors.selectable).first().click();
expect(spy).toHaveBeenCalled();
});
});
describe('when rendered is triggered on a dataset', function() {
it('should add empty class to node if empty', function() {
this.dataset.isEmpty.andReturn(true);
this.$node.removeClass(www.classes.empty);
this.dataset.trigger('rendered');
expect(this.$node).toHaveClass(www.classes.empty);
});
it('should remove empty class from node if not empty', function() {
this.dataset.isEmpty.andReturn(false);
this.$node.addClass(www.classes.empty);
this.dataset.trigger('rendered');
expect(this.$node).not.toHaveClass(www.classes.empty);
});
it('should trigger datasetRendered', function() {
var spy;
this.view.onSync('datasetRendered', spy = jasmine.createSpy());
this.dataset.trigger('rendered');
expect(spy).toHaveBeenCalled();
});
});
describe('when cleared is triggered on a dataset', function() {
it('should add empty class to node if empty', function() {
this.dataset.isEmpty.andReturn(true);
this.$node.removeClass(www.classes.empty);
this.dataset.trigger('cleared');
expect(this.$node).toHaveClass(www.classes.empty);
});
it('should remove empty class from node if not empty', function() {
this.dataset.isEmpty.andReturn(false);
this.$node.addClass(www.classes.empty);
this.dataset.trigger('cleared');
expect(this.$node).not.toHaveClass(www.classes.empty);
});
it('should trigger datasetCleared', function() {
var spy;
this.view.onSync('datasetCleared', spy = jasmine.createSpy());
this.dataset.trigger('cleared');
expect(spy).toHaveBeenCalled();
});
});
describe('when asyncRequested is triggered on a dataset', function() {
it('should propagate event', function() {
var spy = jasmine.createSpy();
this.dataset.onSync('asyncRequested', spy);
this.dataset.trigger('asyncRequested');
expect(spy).toHaveBeenCalled();
});
});
describe('when asyncCanceled is triggered on a dataset', function() {
it('should propagate event', function() {
var spy = jasmine.createSpy();
this.dataset.onSync('asyncCanceled', spy);
this.dataset.trigger('asyncCanceled');
expect(spy).toHaveBeenCalled();
});
});
describe('when asyncReceieved is triggered on a dataset', function() {
it('should propagate event', function() {
var spy = jasmine.createSpy();
this.dataset.onSync('asyncReceived', spy);
this.dataset.trigger('asyncReceived');
expect(spy).toHaveBeenCalled();
});
});
describe('#open', function() {
it('should add open class to node', function() {
this.$node.removeClass(www.classes.open);
this.view.open();
expect(this.$node).toHaveClass(www.classes.open);
});
});
describe('#close', function() {
it('should remove open class to node', function() {
this.$node.addClass(www.classes.open);
this.view.close();
expect(this.$node).not.toHaveClass(www.classes.open);
});
it('should remove cursor', function() {
var $selectable;
$selectable = this.view._getSelectables().first();
this.view.setCursor($selectable);
expect($selectable).toHaveClass(www.classes.cursor);
this.view.close();
expect($selectable).not.toHaveClass(www.classes.cursor);
});
});
describe('#setLanguageDirection', function() {
it('should update css for given language direction', function() {
this.view.setLanguageDirection('rtl');
expect(this.$node).toHaveAttr('dir', 'rtl');
this.view.setLanguageDirection('ltr');
expect(this.$node).toHaveAttr('dir', 'ltr');
});
});
describe('#selectableRelativeToCursor', function() {
it('should return selectable delta spots away from cursor', function() {
var $first, $second;
$first = this.view._getSelectables().eq(0);
$second = this.view._getSelectables().eq(1);
this.view.setCursor($first);
expect(this.view.selectableRelativeToCursor(+1)).toBe($second);
});
it('should support negative deltas', function() {
var $first, $second;
$first = this.view._getSelectables().eq(0);
$second = this.view._getSelectables().eq(1);
this.view.setCursor($second);
expect(this.view.selectableRelativeToCursor(-1)).toBe($first);
});
it('should wrap', function() {
var $expected, $actual;
$expected = this.view._getSelectables().eq(-1);
$actual = this.view.selectableRelativeToCursor(-1);
expect($actual).toBe($expected);
});
it('should return null if delta lands on input', function() {
var $first;
$first = this.view._getSelectables().eq(0);
this.view.setCursor($first);
expect(this.view.selectableRelativeToCursor(-1)).toBeNull();
});
});
describe('#setCursor', function() {
it('should remove cursor if null is passed in', function() {
var $selectable;
$selectable = this.view._getSelectables().eq(0);
this.view.setCursor($selectable);
expect(this.view.getActiveSelectable()).toBe($selectable);
this.view.setCursor(null);
expect(this.view.getActiveSelectable()).toBeNull();
});
it('should move cursor to passed in selectable', function() {
var $selectable;
$selectable = this.view._getSelectables().eq(0);
expect(this.view.getActiveSelectable()).toBeNull();
this.view.setCursor($selectable);
expect(this.view.getActiveSelectable()).toBe($selectable);
});
});
describe('#getSelectableData', function() {
it('should extract the data from the selectable element', function() {
var $selectable, datum;
$selectable = $('<div>').data({
'tt-selectable-display': 'one',
'tt-selectable-object': 'two'
});
data = this.view.getSelectableData($selectable);
expect(data).toEqual({ val: 'one', obj: 'two' });
});
it('should return null if no element is given', function() {
expect(this.view.getSelectableData($('notreal'))).toBeNull();
});
});
describe('#getActiveSelectable', function() {
it('should return the selectable the cursor is on', function() {
var $first;
$first = this.view._getSelectables().eq(0);
this.view.setCursor($first);
expect(this.view.getActiveSelectable()).toBe($first);
});
it('should return null if the cursor is off', function() {
expect(this.view.getActiveSelectable()).toBeNull();
});
});
describe('#getTopSelectable', function() {
it('should return the selectable at the top of the menu', function() {
var $first;
$first = this.view._getSelectables().eq(0);
expect(this.view.getTopSelectable()).toBe($first);
});
});
describe('#update', function() {
it('should invoke update on each dataset if valid update', function() {
this.view.update('fiz');
expect(this.dataset.update).toHaveBeenCalled();
});
it('should return true when valid update', function() {
expect(this.view.update('fiz')).toBe(true);
});
it('should return false when invalid update', function() {
this.view.update('fiz');
expect(this.view.update('fiz')).toBe(false);
});
});
describe('#empty', function() {
it('should set query to null', function() {
this.view.query = 'fiz';
this.view.empty();
expect(this.view.query).toBeNull();
});
it('should add empty class to node', function() {
this.$node.removeClass(www.classes.empty);
this.view.empty();
expect(this.$node).toHaveClass(www.classes.empty);
});
it('should invoke clear on each dataset', function() {
this.view.empty();
expect(this.dataset.clear).toHaveBeenCalled();
});
});
describe('#destroy', function() {
it('should remove event handlers', function() {
var $node = this.view.$node;
spyOn($node, 'off');
this.view.destroy();
expect($node.off).toHaveBeenCalledWith('.tt');
});
it('should destroy its datasets', function() {
this.view.destroy();
expect(this.dataset.destroy).toHaveBeenCalled();
});
it('should set node element to dummy element', function() {
var $node = this.view.$node;
this.view.destroy();
expect(this.view.$node).not.toBe($node);
});
});
});
File diff suppressed because it is too large Load Diff