text: describe('Dataset', function() {
    var www = WWW(), mockSuggestions, mockSuggestionsDisplayFn;
  
    mockSuggestions = [
      { value: 'one', raw: { value: 'one' } },
      { value: 'two', raw: { value: 'two' } },
      { value: 'three', raw: { value: 'three' } }
    ];
  
    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('three');
      });
  
      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('three');
          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 null out the reference to the dataset element', function() {
        this.dataset.destroy();
  
        expect(this.dataset.$el).toBeNull();
      });
    });
  
    // 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);
    }
  });
  