mirror of
https://github.com/zhigang1992/typeahead.js.git
synced 2026-05-22 21:38:26 +08:00
Write test suite for DropdownView.
This commit is contained in:
@@ -13,9 +13,17 @@ var DropdownView = (function() {
|
||||
var that = this, onMouseEnter, onMouseLeave, onSuggestionClick,
|
||||
onSuggestionMouseEnter, onSuggestionMouseLeave;
|
||||
|
||||
o = o || {};
|
||||
|
||||
if (!o.menu || !o.sections) {
|
||||
$.error('menu and/or sections are required');
|
||||
}
|
||||
|
||||
this.isOpen = false;
|
||||
this.isMouseOverDropdown = false;
|
||||
|
||||
this.sections = o.sections;
|
||||
|
||||
// bound functions
|
||||
onMouseEnter = utils.bind(this._onMouseEnter, this);
|
||||
onMouseLeave = utils.bind(this._onMouseLeave, this);
|
||||
@@ -30,8 +38,6 @@ var DropdownView = (function() {
|
||||
.on('mouseenter.tt', '.tt-suggestion', onSuggestionMouseEnter)
|
||||
.on('mouseleave.tt', '.tt-suggestion', onSuggestionMouseLeave);
|
||||
|
||||
this.sections = utils.map(o.sections, initializeSection);
|
||||
|
||||
utils.each(this.sections, function(i, section) {
|
||||
that.$menu.append(section.getRoot());
|
||||
section.onSync('rendered', that._onRendered, that);
|
||||
@@ -66,7 +72,7 @@ var DropdownView = (function() {
|
||||
},
|
||||
|
||||
_onRendered: function onRendered() {
|
||||
this.trigger('suggestionsRendered');
|
||||
this.trigger('sectionRendered');
|
||||
},
|
||||
|
||||
_getSuggestions: function getSuggestions() {
|
||||
@@ -74,11 +80,11 @@ var DropdownView = (function() {
|
||||
},
|
||||
|
||||
_getCursor: function getCursor() {
|
||||
return this.$menu.find('.tt-cursor');
|
||||
return this.$menu.find('.tt-cursor').first();
|
||||
},
|
||||
|
||||
_setCursor: function setCursor($el) {
|
||||
$el.addClass('tt-cursor');
|
||||
$el.first().addClass('tt-cursor');
|
||||
},
|
||||
|
||||
_removeCursor: function removeCursor() {
|
||||
@@ -196,15 +202,21 @@ var DropdownView = (function() {
|
||||
utils.each(this.sections, clearSection);
|
||||
|
||||
function clearSection(i, section) { section.clear(); }
|
||||
},
|
||||
|
||||
isEmpty: function isEmpty() {
|
||||
var hasHeaderOrFooter, sectionsAreEmpty;
|
||||
|
||||
sectionsAreEmpty = utils.every(this.sections, isSectionEmpty);
|
||||
hasHeaderOrFooter =
|
||||
!!this.$menu.children(':not([class^="tt-section-"])').length;
|
||||
|
||||
return !hasHeaderOrFooter && sectionsAreEmpty;
|
||||
|
||||
function isSectionEmpty(section) { return section.isEmpty(); }
|
||||
}
|
||||
});
|
||||
|
||||
return DropdownView;
|
||||
|
||||
// helper functions
|
||||
// ----------------
|
||||
|
||||
function initializeSection(o) {
|
||||
return new SectionView(o);
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -5,17 +5,25 @@
|
||||
*/
|
||||
|
||||
var SectionView = (function() {
|
||||
var suggestionKey = 'ttSuggestion';
|
||||
var datumKey = 'ttDatum';
|
||||
|
||||
// constructor
|
||||
// -----------
|
||||
|
||||
function SectionView(o) {
|
||||
o = o || {};
|
||||
|
||||
if (!o.dataset) {
|
||||
$.error('missing dataset');
|
||||
}
|
||||
|
||||
// tracks the last query the section was updated for
|
||||
this.query = null;
|
||||
|
||||
this.dataset = o.dataset;
|
||||
this.templates = o.templates;
|
||||
this.templates = o.templates || {};
|
||||
this.templates.suggestion =
|
||||
this.templates.suggestion || defaultSuggestionTemplate;
|
||||
|
||||
this.$el = $(html.section.replace('%CLASS%', this.dataset.name));
|
||||
}
|
||||
@@ -23,8 +31,16 @@ var SectionView = (function() {
|
||||
// static methods
|
||||
// --------------
|
||||
|
||||
SectionView.many = function many(configs) {
|
||||
configs = utils.isArray(configs) ? configs : [configs];
|
||||
|
||||
return utils.map(configs, initialize);
|
||||
|
||||
function initialize(config) { return new SectionView(config); }
|
||||
};
|
||||
|
||||
SectionView.extractDatum = function extractDatum(el) {
|
||||
return $(el).data(suggestionKey);
|
||||
return $(el).data(datumKey);
|
||||
};
|
||||
|
||||
// instance methods
|
||||
@@ -52,7 +68,7 @@ var SectionView = (function() {
|
||||
|
||||
innerHtml = that.templates.suggestion(suggestion.raw);
|
||||
outerHtml = html.suggestion.replace('%BODY%', innerHtml);
|
||||
$el = $(outerHtml).data(suggestionKey, suggestion);
|
||||
$el = $(outerHtml).data(datumKey, suggestion);
|
||||
|
||||
$el.children().each(function() { $(this).css(css.suggestionChild); });
|
||||
|
||||
@@ -88,4 +104,10 @@ var SectionView = (function() {
|
||||
|
||||
return SectionView;
|
||||
|
||||
// helper functions
|
||||
// ----------------
|
||||
|
||||
function defaultSuggestionTemplate(context) {
|
||||
return '<p>' + context.value + '</p>';
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -13,6 +13,15 @@ var TypeaheadView = (function() {
|
||||
function TypeaheadView(o) {
|
||||
var $menu, $input, $hint;
|
||||
|
||||
o = o || {};
|
||||
|
||||
if (!o.input || !o.sections) {
|
||||
$.error('missing input and/or sections');
|
||||
}
|
||||
|
||||
// maps the section configs to SectionView instances
|
||||
o.sections = SectionView.many(o.sections);
|
||||
|
||||
this.$node = buildDomStructure(o.input);
|
||||
|
||||
$menu = this.$node.find('.tt-dropdown-menu');
|
||||
@@ -23,7 +32,7 @@ var TypeaheadView = (function() {
|
||||
.onSync('suggestionClicked', this._onSuggestionClicked, this)
|
||||
.onSync('cursorMoved', this._onCursorMoved, this)
|
||||
.onSync('cursorRemoved', this._onCursorRemoved, this)
|
||||
.onSync('suggestionsRendered', this._onSuggestionsRendered, this)
|
||||
.onSync('sectionRendered', this._onSectionRendered, this)
|
||||
.onSync('opened', this._onOpened, this)
|
||||
.onSync('closed', this._onClosed, this);
|
||||
|
||||
@@ -71,7 +80,7 @@ var TypeaheadView = (function() {
|
||||
this._updateHint();
|
||||
},
|
||||
|
||||
_onSuggestionsRendered: function onSuggestionsRendered() {
|
||||
_onSectionRendered: function onSectionRendered() {
|
||||
this._updateHint();
|
||||
},
|
||||
|
||||
|
||||
@@ -1,3 +1,290 @@
|
||||
describe('DropdownView', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
var $fixture;
|
||||
|
||||
jasmine.SectionView.useMock();
|
||||
|
||||
setFixtures(fixtures.html.menu);
|
||||
|
||||
$fixture = $('#jasmine-fixtures');
|
||||
this.$menu = $fixture.find('.tt-dropdown-menu');
|
||||
this.$menu.html(fixtures.html.section);
|
||||
|
||||
this.section = new SectionView();
|
||||
this.section.onSync.andCallThrough();
|
||||
this.section.onAsync.andCallThrough();
|
||||
this.section.off.andCallThrough();
|
||||
this.section.trigger.andCallThrough();
|
||||
|
||||
this.view = new DropdownView({
|
||||
menu: this.$menu,
|
||||
sections: [this.section]
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error if menu and/or sections is missing', function() {
|
||||
expect(noMenu).toThrow();
|
||||
expect(noSections).toThrow();
|
||||
|
||||
function noMenu() { new DropdownView({ menu: '.menu' }); }
|
||||
function noSections() { new DropdownView({ sections: true }); }
|
||||
});
|
||||
|
||||
describe('when mouseenter is triggered', function() {
|
||||
it('should set isMouseOverDropdown to true', function() {
|
||||
this.$menu.mouseleave().mouseenter();
|
||||
expect(this.view.isMouseOverDropdown).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when mouseleave is triggered', function() {
|
||||
it('should set isMouseOverDropdown to false', function() {
|
||||
this.$menu.mouseenter().mouseleave();
|
||||
expect(this.view.isMouseOverDropdown).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when click event is triggered on a suggestion', function() {
|
||||
it('should trigger suggestionClicked', function() {
|
||||
var spy;
|
||||
|
||||
this.view.onSync('suggestionClicked', spy = jasmine.createSpy());
|
||||
|
||||
this.$menu.find('.tt-suggestion').first().click();
|
||||
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when mouseenter is triggered on a suggestion', function() {
|
||||
it('should set the cursor', function() {
|
||||
var $suggestion;
|
||||
|
||||
$suggestion = this.$menu.find('.tt-suggestion').first();
|
||||
$suggestion.mouseenter();
|
||||
|
||||
expect($suggestion).toHaveClass('tt-cursor');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when mouseleave is triggered on a suggestion', function() {
|
||||
it('should remove the cursor', function() {
|
||||
var $suggestion;
|
||||
|
||||
$suggestion = this.$menu.find('.tt-suggestion').first();
|
||||
$suggestion.mouseenter().mouseleave();
|
||||
|
||||
expect($suggestion).not.toHaveClass('tt-cursor');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when rendered is triggered on a section', function() {
|
||||
it('should trigger sectionRendered', function() {
|
||||
var spy;
|
||||
|
||||
this.view.onSync('sectionRendered', spy = jasmine.createSpy());
|
||||
this.section.trigger('rendered');
|
||||
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('#open', function() {
|
||||
it('should display the menu', function() {
|
||||
this.view.close();
|
||||
this.view.open();
|
||||
|
||||
expect(this.$menu).toBeVisible();
|
||||
});
|
||||
|
||||
it('should trigger opened', function() {
|
||||
var spy;
|
||||
|
||||
this.view.onSync('opened', spy = jasmine.createSpy());
|
||||
|
||||
this.view.close();
|
||||
this.view.open();
|
||||
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('#close', function() {
|
||||
it('should hide the menu', function() {
|
||||
this.view.open();
|
||||
this.view.close();
|
||||
|
||||
expect(this.$menu).not.toBeVisible();
|
||||
});
|
||||
|
||||
it('should trigger closed', function() {
|
||||
var spy;
|
||||
|
||||
this.view.onSync('closed', spy = jasmine.createSpy());
|
||||
|
||||
this.view.open();
|
||||
this.view.close();
|
||||
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('#setLanguageDirection', function() {
|
||||
it('should update css for given language direction', function() {
|
||||
// TODO: eh, the toHaveCss matcher doesn't seem to work very well
|
||||
/*
|
||||
this.view.setLanguageDirection('rtl');
|
||||
expect(this.$menu).toHaveCss({ left: 'auto', right: '0px' });
|
||||
|
||||
this.view.setLanguageDirection('ltr');
|
||||
expect(this.$menu).toHaveCss({ left: '0px', right: 'auto' });
|
||||
*/
|
||||
});
|
||||
});
|
||||
|
||||
describe('#moveCursorUp', function() {
|
||||
beforeEach(function() {
|
||||
this.view.open();
|
||||
});
|
||||
|
||||
it('should move the cursor up', function() {
|
||||
var $first, $second;
|
||||
|
||||
$first = this.view._getSuggestions().eq(0);
|
||||
$second = this.view._getSuggestions().eq(1);
|
||||
|
||||
this.view._setCursor($second);
|
||||
this.view.moveCursorUp();
|
||||
expect(this.view._getCursor()).toBe($first);
|
||||
});
|
||||
|
||||
it('should move cursor to bottom if cursor is not present', function() {
|
||||
var $bottom;
|
||||
|
||||
$bottom = this.view._getSuggestions().eq(-1);
|
||||
|
||||
this.view.moveCursorUp();
|
||||
expect(this.view._getCursor()).toBe($bottom);
|
||||
});
|
||||
|
||||
it('should remove cursor if already at top', function() {
|
||||
var $first;
|
||||
|
||||
$first = this.view._getSuggestions().eq(0);
|
||||
|
||||
this.view._setCursor($first);
|
||||
this.view.moveCursorUp();
|
||||
expect(this.view._getCursor().length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#moveCursorDown', function() {
|
||||
beforeEach(function() {
|
||||
this.view.open();
|
||||
});
|
||||
|
||||
it('should move the cursor down', function() {
|
||||
var $first, $second;
|
||||
|
||||
$first = this.view._getSuggestions().eq(0);
|
||||
$second = this.view._getSuggestions().eq(1);
|
||||
|
||||
this.view._setCursor($first);
|
||||
this.view.moveCursorDown();
|
||||
expect(this.view._getCursor()).toBe($second);
|
||||
});
|
||||
|
||||
it('should move cursor to top if cursor is not present', function() {
|
||||
var $first;
|
||||
|
||||
$first = this.view._getSuggestions().eq(0);
|
||||
|
||||
this.view.moveCursorDown();
|
||||
expect(this.view._getCursor()).toBe($first);
|
||||
});
|
||||
|
||||
it('should remove cursor if already at bottom', function() {
|
||||
var $bottom;
|
||||
|
||||
$bottom = this.view._getSuggestions().eq(-1);
|
||||
|
||||
this.view._setCursor($bottom);
|
||||
this.view.moveCursorDown();
|
||||
expect(this.view._getCursor().length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getDatumForSuggestion', function() {
|
||||
it('should extract the datum from the suggestion element', function() {
|
||||
var $suggestion, datum;
|
||||
|
||||
$suggestion = $('<div>').data('ttDatum', { value: 'one' });
|
||||
datum = this.view.getDatumForSuggestion($suggestion);
|
||||
|
||||
expect(datum).toEqual({ value: 'one' });
|
||||
});
|
||||
|
||||
it('should return null if no element is given', function() {
|
||||
expect(this.view.getDatumForSuggestion($('notreal'))).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getDatumForCursor', function() {
|
||||
it('should return the datum for the cursor', function() {
|
||||
var $first;
|
||||
|
||||
$first = this.view._getSuggestions().eq(0);
|
||||
$first.data('ttDatum', { value: 'one' });
|
||||
|
||||
this.view._setCursor($first);
|
||||
expect(this.view.getDatumForCursor()).toEqual({ value: 'one' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getDatumForTopSuggestion', function() {
|
||||
it('should return the datum for top suggestion', function() {
|
||||
var $first;
|
||||
|
||||
$first = this.view._getSuggestions().eq(0);
|
||||
$first.data('ttDatum', { value: 'one' });
|
||||
|
||||
expect(this.view.getDatumForTopSuggestion()).toEqual({ value: 'one' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('#update', function() {
|
||||
it('should invoke update on each section', function() {
|
||||
this.view.update();
|
||||
expect(this.section.update).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('#empty', function() {
|
||||
it('should invoke clear on each section', function() {
|
||||
this.view.empty();
|
||||
expect(this.section.clear).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('#isEmpty', function() {
|
||||
it('should return false if a header or footer is present', function() {
|
||||
this.section.isEmpty.andReturn(true);
|
||||
this.$menu.append('<div class="footer">');
|
||||
|
||||
expect(this.view.isEmpty()).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false if a section is not empty', function() {
|
||||
this.section.isEmpty.andReturn(false);
|
||||
|
||||
expect(this.view.isEmpty()).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true otherwise', function() {
|
||||
this.section.isEmpty.andReturn(true);
|
||||
|
||||
expect(this.view.isEmpty()).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
16
test/fixtures/html.js
vendored
Normal file
16
test/fixtures/html.js
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
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>',
|
||||
menu: '<span class="tt-dropdown-menu"></span>',
|
||||
section: [
|
||||
'<div class="tt-section-test">',
|
||||
'<span class="tt-suggestions">',
|
||||
'<div class="tt-suggestion"><p>one</p></div>',
|
||||
'<div class="tt-suggestion"><p>two</p></div>',
|
||||
'<div class="tt-suggestion"><p>three</p></div>',
|
||||
'</span>',
|
||||
'</div>'
|
||||
].join('')
|
||||
};
|
||||
@@ -5,7 +5,8 @@
|
||||
'Dataset',
|
||||
'PersistentStorage',
|
||||
'Transport',
|
||||
'SearchIndex'
|
||||
'SearchIndex',
|
||||
'SectionView'
|
||||
];
|
||||
|
||||
for (var i = 0; i < components.length; i++) {
|
||||
@@ -31,15 +32,24 @@
|
||||
}
|
||||
|
||||
function mock(Constructor) {
|
||||
var mockConstructor;
|
||||
|
||||
Mock.prototype = Constructor.prototype;
|
||||
|
||||
return jasmine.createSpy('mock constructor').andCallFake(Mock);
|
||||
mockConstructor = jasmine.createSpy('mock constructor').andCallFake(Mock);
|
||||
|
||||
// copy instance methods
|
||||
for (var key in Constructor) {
|
||||
if (typeof Constructor[key] === 'function') {
|
||||
mockConstructor[key] = Constructor[key];
|
||||
}
|
||||
}
|
||||
|
||||
return mockConstructor;
|
||||
|
||||
function Mock() {
|
||||
var instance = utils.mixin({}, Constructor.prototype);
|
||||
|
||||
Constructor.apply(instance, arguments);
|
||||
|
||||
for (var key in instance) {
|
||||
if (typeof instance[key] === 'function') {
|
||||
spyOn(instance, key);
|
||||
|
||||
@@ -15,7 +15,6 @@ describe('InputView', function() {
|
||||
beforeEach(function() {
|
||||
var $fixture;
|
||||
|
||||
jasmine.SectionView.useMock();
|
||||
setFixtures(fixtures.html.input + fixtures.html.hint);
|
||||
|
||||
$fixture = $('#jasmine-fixtures');
|
||||
|
||||
@@ -3,18 +3,20 @@ describe('SectionView', function() {
|
||||
beforeEach(function() {
|
||||
jasmine.Dataset.useMock();
|
||||
|
||||
this.dataset = new Dataset({ name: 'test', local: [] });
|
||||
this.dataset = new Dataset();
|
||||
this.dataset.name = 'test';
|
||||
|
||||
this.section = new SectionView({
|
||||
dataset: this.dataset,
|
||||
templates: {
|
||||
suggestion: function(context) { return '<p>' + context.value + '</p>'; }
|
||||
}
|
||||
});
|
||||
this.section = new SectionView({ dataset: this.dataset });
|
||||
|
||||
this.$root = this.section.getRoot();
|
||||
});
|
||||
|
||||
it('should throw an error if dataset is missing', function() {
|
||||
expect(noDataset).toThrow();
|
||||
|
||||
function noDataset() { new SectionView(); }
|
||||
});
|
||||
|
||||
describe('#getRoot', function() {
|
||||
it('should return the root element', function() {
|
||||
expect(this.section.getRoot()).toBe('div.tt-section-test');
|
||||
@@ -48,6 +50,17 @@ describe('SectionView', function() {
|
||||
expect(this.$root).not.toContainText('five');
|
||||
});
|
||||
});
|
||||
|
||||
it('should trigger rendered after suggestions are rendered', function() {
|
||||
var spy;
|
||||
|
||||
this.section.onSync('rendered', spy = jasmine.createSpy());
|
||||
|
||||
this.dataset.get.andCallFake(fakeGetWithSyncResults);
|
||||
this.section.update('woah');
|
||||
|
||||
waitsFor(function() { return spy.callCount; });
|
||||
});
|
||||
});
|
||||
|
||||
describe('#clear', function() {
|
||||
@@ -64,17 +77,6 @@ describe('SectionView', function() {
|
||||
});
|
||||
});
|
||||
|
||||
it('should trigger rendered after suggestions are rendered', function() {
|
||||
var spy;
|
||||
|
||||
this.section.onSync('rendered', spy = jasmine.createSpy());
|
||||
|
||||
this.dataset.get.andCallFake(fakeGetWithSyncResults);
|
||||
this.section.update('woah');
|
||||
|
||||
waitsFor(function() { return spy.callCount; });
|
||||
});
|
||||
|
||||
describe('#isEmpty', function() {
|
||||
it('should return true when empty', function() {
|
||||
expect(this.section.isEmpty()).toBe(true);
|
||||
@@ -88,6 +90,9 @@ describe('SectionView', function() {
|
||||
});
|
||||
});
|
||||
|
||||
// helper functions
|
||||
// ----------------
|
||||
|
||||
function fakeGetWithSyncResults(query, cb) {
|
||||
cb([
|
||||
{ value: 'one', raw: { value: 'one' } },
|
||||
|
||||
Reference in New Issue
Block a user