first commit
This commit is contained in:
@@ -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); }
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
@@ -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
@@ -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
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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
Reference in New Issue
Block a user