describe('Dataset', function() { var fixtureStrings = ['grape', 'coconut', 'cake', 'tea', 'coffee'], fixtureDatums = [ { value: 'grape' }, { value: 'coconut' }, { value: 'cake' }, { value: 'tea' }, { value: 'coffee' } ], expectedAdjacencyList = { g: ['grape'], c: ['coconut', 'cake', 'coffee'], t: ['tea'] }, expectedItemHash = { grape: createItem('grape'), coconut: createItem('coconut'), cake: createItem('cake'), tea: createItem('tea'), coffee: createItem('coffee') }, prefetchResp = { status: 200, responseText: JSON.stringify(fixtureStrings) }, mockStorageFns = { getMiss: function() { return null; }, getHit: function(key) { if (/itemHash/.test(key)) { return expectedItemHash; } else if (/adjacencyList/.test(key)) { return expectedAdjacencyList; } else if (/thumbprint/.test(key)) { return VERSION; } else if (/protocol/.test(key)) { return utils.getProtocol(); } } }; beforeEach(function() { jasmine.Ajax.useMock(); jasmine.PersistentStorage.useMock(); jasmine.Transport.useMock(); spyOn(utils, 'getUniqueId').andCallFake(function(name) { return name; }); }); afterEach(function() { clearAjaxRequests(); }); // public methods // -------------- describe('#constructor', function() { describe('when called with a name', function() { beforeEach(function() { this.dataset = new Dataset({ name: '#constructor', local: fixtureStrings }); }); it('should initialize persistent storage', function() { expect(this.dataset.storage).toBeDefined(); expect(PersistentStorage).toHaveBeenCalled(); }); }); describe('when called with no name', function() { beforeEach(function() { this.dataset = new Dataset({ local: fixtureStrings }); }); it('should not use persistent storage', function() { expect(this.dataset.storage).toBeNull(); expect(PersistentStorage).not.toHaveBeenCalled(); }); }); describe('when called with a template but no engine', function() { beforeEach(function() { this.fn = function() { var d = new Dataset({ template: 't' }); }; }); it('should throw an error', function() { expect(this.fn).toThrow(); }); }); describe('when called without local, prefetch, or remote', function() { beforeEach(function() { this.fn = function() { this.dataset = new Dataset(); }; }); it('should throw an error', function() { expect(this.fn).toThrow(); }); }); describe('when called with no template', function() { beforeEach(function() { this.dataset = new Dataset({ local: fixtureStrings }); }); it('should compile default template', function() { expect(this.dataset.template({ value: 'boo' })) .toBe('
boo
'); }); }); describe('when called with a template and engine', function() { beforeEach(function() { this.spy = jasmine.createSpy().andReturn({ render: function() { return 'boo!'; } }); this.dataset = new Dataset({ local: fixtureStrings, template: 't', engine: { compile: this.spy } }); }); it('should compile the template', function() { expect(this.spy) .toHaveBeenCalledWith('t'); expect(this.dataset.template()).toBe('boo!'); }); }); describe('when called with a compiled template', function() { beforeEach(function() { this.dataset = new Dataset({ local: fixtureStrings, template: $.noop }); }); it('should use it', function() { expect(this.dataset.template).toBe($.noop); }); }); }); describe('#initialize', function() { it('should return Deferred instance', function() { var returnVal; this.dataset = new Dataset({ local: fixtureStrings }); returnVal = this.dataset.initialize(); // eh, have to rely on duck typing unfortunately expect(returnVal.fail).toBeDefined(); expect(returnVal.done).toBeDefined(); expect(returnVal.always).toBeDefined(); }); describe('when called with local', function() { beforeEach(function() { this.dataset1 = new Dataset({ local: fixtureStrings }); this.dataset2 = new Dataset({ local: fixtureDatums }); this.dataset1.initialize(); this.dataset2.initialize(); }); it('should process and merge the data', function() { expect(this.dataset1.itemHash).toEqual(expectedItemHash); expect(this.dataset1.adjacencyList).toEqual(expectedAdjacencyList); expect(this.dataset2.itemHash).toEqual(expectedItemHash); expect(this.dataset2.adjacencyList).toEqual(expectedAdjacencyList); }); }); describe('when called with prefetch', function() { describe('if data is available in storage', function() { beforeEach(function() { this.dataset = new Dataset({ name: 'prefetch', prefetch: '/prefetch.json' }); this.dataset.storage.get.andCallFake(mockStorageFns.getHit); this.dataset.initialize(); }); it('should not make ajax request', function() { expect(mostRecentAjaxRequest()).toBeNull(); }); it('should use data from storage', function() { expect(this.dataset.itemHash).toEqual(expectedItemHash); expect(this.dataset.adjacencyList).toEqual(expectedAdjacencyList); }); }); describe('if data is not available in storage', function() { // default ttl var ttl = 24 * 60 * 60 * 1000; describe('if filter was passed in', function() { var filteredAdjacencyList = { f: ['filter'] }, filteredItemHash = { filter: createItem('filter') }; beforeEach(function() { this.dataset = new Dataset({ name: 'prefetch', prefetch: { url: '/prefetch.json', filter: function(data) { return ['filter']; } } }); this.dataset.storage.get.andCallFake(mockStorageFns.getMiss); this.dataset.initialize(); this.request = mostRecentAjaxRequest(); this.request.response(prefetchResp); }); it('should make ajax request', function() { expect(this.request).not.toBeNull(); }); it('should process and merge fileered data', function() { expect(this.dataset.adjacencyList).toEqual(filteredAdjacencyList); expect(this.dataset.itemHash).toEqual(filteredItemHash); }); it('should store processed data in storage', function() { expect(this.dataset.storage.set) .toHaveBeenCalledWith('itemHash', filteredItemHash, ttl); expect(this.dataset.storage.set) .toHaveBeenCalledWith('adjacencyList', filteredAdjacencyList, ttl); }); it('should store metadata in storage', function() { expect(this.dataset.storage.set) .toHaveBeenCalledWith('protocol', utils.getProtocol(), ttl); expect(this.dataset.storage.set) .toHaveBeenCalledWith('thumbprint', VERSION, ttl); }); }); describe('if filter was not passed in', function() { beforeEach(function() { this.dataset = new Dataset({ name: 'prefetch', prefetch: '/prefetch.json' }); this.dataset.storage.get.andCallFake(mockStorageFns.getMiss); this.dataset.initialize(); this.request = mostRecentAjaxRequest(); this.request.response(prefetchResp); }); it('should make ajax request', function() { expect(this.request).not.toBeNull(); }); it('should process and merge fetched data', function() { expect(this.dataset.itemHash).toEqual(expectedItemHash); expect(this.dataset.adjacencyList).toEqual(expectedAdjacencyList); }); it('should store processed data in storage', function() { expect(this.dataset.storage.set) .toHaveBeenCalledWith('itemHash', expectedItemHash, ttl); expect(this.dataset.storage.set) .toHaveBeenCalledWith('adjacencyList', expectedAdjacencyList, ttl); }); it('should store metadata in storage', function() { expect(this.dataset.storage.set) .toHaveBeenCalledWith('protocol', utils.getProtocol(), ttl); expect(this.dataset.storage.set) .toHaveBeenCalledWith('thumbprint', VERSION, ttl); }); }); }); }); describe('when called with remote', function() { beforeEach(function() { this.dataset = new Dataset({ remote: '/remote' }); this.dataset.initialize(); }); it('should initialize the transport', function() { expect(Transport).toHaveBeenCalledWith('/remote'); }); }); }); describe('#getSuggestions', function() { describe('when length of query is less than minLength', function() { beforeEach(function() { this.spy = jasmine.createSpy(); this.dataset = new Dataset({ local: fixtureStrings, minLength: 3 }); this.dataset.initialize(); }); it('should be a noop', function() { this.dataset.getSuggestions('co', this.spy); expect(this.spy).not.toHaveBeenCalled(); }); }); }); describe('Matching, combining, returning results', function() { beforeEach(function() { this.dataset = new Dataset({ local: fixtureStrings, remote: '/remote' }); this.dataset.initialize(); }); it('network requests are not triggered with enough local results', function() { this.dataset.limit = 3; this.dataset.getSuggestions('c', function(items) { expect(items).toEqual([ createItem('coconut'), createItem('cake'), createItem('coffee') ]); }); expect(this.dataset.transport.get).not.toHaveBeenCalled(); this.dataset.limit = 100; this.dataset.getSuggestions('c', function(items) { expect(items).toEqual([ createItem('coconut'), createItem('cake'), createItem('coffee') ]); }); expect(this.dataset.transport.get).toHaveBeenCalled(); }); it('matches', function() { this.dataset.getSuggestions('c', function(items) { expect(items).toEqual([ createItem('coconut'), createItem('cake'), createItem('coffee') ]); }); }); it('does not match', function() { this.dataset.getSuggestions('q', function(items) { expect(items).toEqual([]); }); }); it('does not match multiterm queries', function() { this.dataset.getSuggestions('coff ca', function(items) { expect(items).toEqual([]); }); }); it('concatenates local and remote results and dedups them', function() { var spy = jasmine.createSpy(), remote = [fixtureDatums[0], fixtureStrings[2]]; this.dataset.transport.get.andCallFake(function(q, cb) { utils.defer(function() { cb(remote); }); }); this.dataset.getSuggestions('c', spy); waitsFor(function() { return spy.callCount === 2; }); runs(function() { // local suggestions expect(spy.argsForCall[0][0]).toEqual([ expectedItemHash.coconut, expectedItemHash.cake, expectedItemHash.coffee ]); // local + remote suggestions expect(spy.argsForCall[1][0]).toEqual([ expectedItemHash.coconut, expectedItemHash.cake, expectedItemHash.coffee, expectedItemHash.grape ]); }); }); }); describe('tokenization', function() { describe('with datum strings', function() { var fixtureData = ['course-106', 'user_name', 'One-Two', 'two three']; beforeEach(function() { this.dataset = new Dataset({ local: fixtureData }); this.dataset.initialize(); }); it('normalizes capitalization to match items', function() { this.dataset.getSuggestions('Cours', function(items) { expect(items).toEqual([createItem('course-106')]); }); this.dataset.getSuggestions('cOuRsE 106', function(items) { expect(items).toEqual([createItem('course-106')]); }); this.dataset.getSuggestions('one two', function(items) { expect(items).toEqual([createItem('One-Two')]); }); this.dataset.getSuggestions('THREE TWO', function(items) { expect(items).toEqual([createItem('two three')]); }); }); it('matches items with dashes', function() { this.dataset.getSuggestions('106 course', function(items) { expect(items).toEqual([createItem('course-106')]); }); this.dataset.getSuggestions('course-106', function(items) { expect(items).toEqual([]); }); }); it('matches items with underscores', function() { this.dataset.getSuggestions('user name', function(items) { expect(items).toEqual([createItem('user_name')]); }); }); }); }); describe('with datum objects', function() { var fixtureData = [{ value: 'course-106' }]; beforeEach(function() { this.dataset = new Dataset({ local: fixtureData }); this.dataset.initialize(); }); it('matches items with dashes', function() { this.dataset.getSuggestions('106 course', function(items) { expect(items).toEqual([createItem('course-106')]); }); this.dataset.getSuggestions('course-106', function(items) { expect(items).toEqual([]); }); }); }); // helper functions // ---------------- function createItem(val) { return { value: val, tokens: utils.tokenizeText(val), datum: { value: val } }; } });