text: 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 set scroll top of node to 0', function() {
        spyOn(this.view.$node, 'scrollTop');
        this.view.open();
  
        expect(this.view.$node.scrollTop).toHaveBeenCalledWith(0);
      });
  
      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);
      });
    });
  });
  