Merge pull request #20 from algolia/test/constructor
test(DocSearch): Add tests for constructor
This commit is contained in:
commit
e749465f3b
10 changed files with 252 additions and 67 deletions
5
.babelrc
5
.babelrc
|
|
@ -1,3 +1,8 @@
|
|||
{
|
||||
"env": {
|
||||
"test": {
|
||||
"plugins": ["babel-plugin-rewire"]
|
||||
}
|
||||
},
|
||||
"stage": 2
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@
|
|||
"babel-core": "^5.8.29",
|
||||
"babel-eslint": "^4.1.3",
|
||||
"babel-loader": "^5.3.2",
|
||||
"babel-plugin-rewire": "^0.1.22",
|
||||
"conventional-changelog": "^0.5.1",
|
||||
"cssnano": "^3.4.0",
|
||||
"doctoc": "^0.15.0",
|
||||
|
|
@ -50,6 +51,7 @@
|
|||
"postcss-cli": "^2.3.2",
|
||||
"pretty-bytes": "^2.0.1",
|
||||
"semver": "^5.1.0",
|
||||
"sinon": "^1.17.2",
|
||||
"uglify-js": "^2.6.1",
|
||||
"webpack": "^1.12.2",
|
||||
"webpack-dev-server": "^1.12.1"
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint no-console:0 max-len:0 */
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint no-console:0 */
|
||||
import ghpages from 'gh-pages';
|
||||
import {join} from 'path';
|
||||
|
||||
|
|
|
|||
|
|
@ -36,12 +36,18 @@ class DocSearch {
|
|||
hint: false
|
||||
}
|
||||
}) {
|
||||
this.checkArguments({apiKey, indexName, inputSelector, algoliaOptions, autocompleteOptions});
|
||||
DocSearch.checkArguments({apiKey, indexName, inputSelector, algoliaOptions, autocompleteOptions});
|
||||
|
||||
this.apiKey = apiKey;
|
||||
this.indexName = indexName;
|
||||
this.input = DocSearch.getInputFromSelector(inputSelector);
|
||||
this.algoliaOptions = algoliaOptions;
|
||||
this.autocompleteOptions = autocompleteOptions;
|
||||
|
||||
this.client = algoliasearch('BH4D9OD16A', this.apiKey);
|
||||
this.client.addAlgoliaAgent('docsearch.js ' + version);
|
||||
this.autocomplete = autocomplete(this.input, autocompleteOptions, [{
|
||||
source: this.getSource(),
|
||||
source: this.getAutocompleteSource(),
|
||||
templates: {
|
||||
suggestion: this.getSuggestionTemplate(),
|
||||
footer: '<div class="ads-footer">Search by <a class="ads-footer--logo" href="https://www.algolia.com/docsearch">Algolia</a></div>'
|
||||
|
|
@ -50,26 +56,42 @@ class DocSearch {
|
|||
this.autocomplete.on('autocomplete:selected', this.handleSelected);
|
||||
}
|
||||
|
||||
checkArguments(args) {
|
||||
/**
|
||||
* Checks that the passed arguments are valid. Will throw errors otherwise
|
||||
* @function checkArguments
|
||||
* @param {object} args Arguments as an option object
|
||||
* @returns {void}
|
||||
*/
|
||||
static checkArguments(args) {
|
||||
if (!args.apiKey || !args.indexName) {
|
||||
throw new Error(usage);
|
||||
}
|
||||
|
||||
const input = $(args.inputSelector).filter('input');
|
||||
if (input.length === 0) {
|
||||
if (!DocSearch.getInputFromSelector(args.inputSelector)) {
|
||||
throw new Error(`Error: No input element in the page matches ${args.inputSelector}`);
|
||||
}
|
||||
|
||||
this.apiKey = args.apiKey;
|
||||
this.indexName = args.indexName;
|
||||
this.input = input;
|
||||
this.algoliaOptions = args.algoliaOptions;
|
||||
this.autocompleteOptions = args.autocompleteOptions;
|
||||
}
|
||||
|
||||
// Returns a `source` method to be used by `autocomplete`. This will query the
|
||||
// Algolia index.
|
||||
getSource() {
|
||||
/**
|
||||
* Returns the matching input from a CSS selector, null if none matches
|
||||
* @function getInputFromSelector
|
||||
* @param {string} selector CSS selector that matches the search
|
||||
* input of the page
|
||||
* @returns {void}
|
||||
*/
|
||||
static getInputFromSelector(selector) {
|
||||
let input = $(selector).filter('input');
|
||||
return input.length ? $(input[0]) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the `source` method to be passed to autocomplete.js. It will query
|
||||
* the Algolia index and call the callbacks with the formatted hits.
|
||||
* @function getAutocompleteSource
|
||||
* @returns {function} Method to be passed as the `source` option of
|
||||
* autocomplete
|
||||
*/
|
||||
getAutocompleteSource() {
|
||||
return (query, callback) => {
|
||||
this.client.search([{
|
||||
indexName: this.indexName,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,10 @@ let prefix = 'ads-suggestion';
|
|||
|
||||
let templates = {
|
||||
suggestion: `
|
||||
<div class="${prefix} {{#isCategoryHeader}}${prefix}__main{{/isCategoryHeader}} {{#isSubcategoryHeader}}${prefix}__secondary{{/isSubcategoryHeader}}">
|
||||
<div class="${prefix}
|
||||
{{#isCategoryHeader}}${prefix}__main{{/isCategoryHeader}}
|
||||
{{#isSubcategoryHeader}}${prefix}__secondary{{/isSubcategoryHeader}}
|
||||
">
|
||||
<div class="${prefix}--category-header">{{{category}}}</div>
|
||||
<div class="${prefix}--wrapper">
|
||||
<div class="${prefix}--subcategory-column">{{{subcategory}}}</div>
|
||||
|
|
|
|||
|
|
@ -1,107 +1,257 @@
|
|||
/* eslint-env mocha */
|
||||
/* eslint no-new:0 */
|
||||
|
||||
import jsdom from 'mocha-jsdom';
|
||||
import expect from 'expect';
|
||||
import fixtures from 'node-fixtures';
|
||||
import sinon from 'sinon';
|
||||
// import fixtures from 'node-fixtures';
|
||||
|
||||
describe('DocSearch', () => {
|
||||
let docSearch;
|
||||
let DocSearch;
|
||||
let $;
|
||||
jsdom({useEach: true});
|
||||
|
||||
|
||||
beforeEach(() => {
|
||||
// We need a DOM to be ready before importing Zepto
|
||||
docSearch = require('../index.js');
|
||||
// We need to load DocSearch from here as it depends on Zepto, which itself
|
||||
// needs jsdom to be called before being loaded.
|
||||
DocSearch = require('../src/lib/DocSearch.js');
|
||||
$ = require('npm-zepto');
|
||||
|
||||
// Note: If you edit this HTML while doing TDD with `npm run test:watch`,
|
||||
// you will have to restart `npm run test:watch` for the new HTML to be
|
||||
// updated
|
||||
document.body.innerHTML = '<div><input id="input" name="input-name" /></div>';
|
||||
document.body.innerHTML = `
|
||||
<div>
|
||||
<input id="input" name="search" />
|
||||
<span class="i-am-a-span">span span</span>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
describe('constructor', () => {
|
||||
let AlgoliaSearch;
|
||||
let algoliasearch;
|
||||
let AutoComplete;
|
||||
let autocomplete;
|
||||
let checkArguments;
|
||||
let getInputFromSelector;
|
||||
let checkArgumentsInitial;
|
||||
let getInputFromSelectorInitial;
|
||||
let defaultOptions;
|
||||
|
||||
beforeEach(() => {
|
||||
algoliasearch = {
|
||||
algolia: 'client',
|
||||
addAlgoliaAgent: sinon.spy()
|
||||
};
|
||||
AlgoliaSearch = sinon.stub().returns(algoliasearch);
|
||||
autocomplete = {
|
||||
on: sinon.spy()
|
||||
};
|
||||
AutoComplete = sinon.stub().returns(autocomplete);
|
||||
|
||||
checkArgumentsInitial = DocSearch.checkArguments;
|
||||
checkArguments = sinon.spy();
|
||||
getInputFromSelectorInitial = DocSearch.getInputFromSelector;
|
||||
getInputFromSelector = sinon.stub();
|
||||
defaultOptions = {
|
||||
indexName: 'indexName',
|
||||
apiKey: 'apiKey',
|
||||
inputSelector: '#input'
|
||||
};
|
||||
|
||||
DocSearch.checkArguments = checkArguments;
|
||||
DocSearch.getInputFromSelector = getInputFromSelector;
|
||||
|
||||
DocSearch.__Rewire__('algoliasearch', AlgoliaSearch);
|
||||
DocSearch.__Rewire__('autocomplete', AutoComplete);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Cleanup the stubs on static methods
|
||||
DocSearch.checkArguments = checkArgumentsInitial;
|
||||
DocSearch.getInputFromSelector = getInputFromSelectorInitial;
|
||||
});
|
||||
|
||||
it('should call checkArguments', () => {
|
||||
// Given
|
||||
let options = defaultOptions;
|
||||
|
||||
// When
|
||||
new DocSearch(options);
|
||||
|
||||
// Then
|
||||
expect(checkArguments.calledOnce).toBe(true);
|
||||
});
|
||||
it('should pass main options as instance properties', () => {
|
||||
// Given
|
||||
let options = defaultOptions;
|
||||
|
||||
// When
|
||||
let actual = new DocSearch(options);
|
||||
|
||||
// Then
|
||||
expect(actual.indexName).toEqual('indexName');
|
||||
expect(actual.apiKey).toEqual('apiKey');
|
||||
});
|
||||
it('should pass the input element as an instance property', () => {
|
||||
// Given
|
||||
let options = defaultOptions;
|
||||
getInputFromSelector.returns($('<span>foo</span>'));
|
||||
|
||||
// When
|
||||
let actual = new DocSearch(options);
|
||||
|
||||
// Then
|
||||
let $input = actual.input;
|
||||
expect($input.text()).toEqual('foo');
|
||||
expect($input[0].tagName).toEqual('SPAN');
|
||||
});
|
||||
it('should pass secondary options as instance properties', () => {
|
||||
// Given
|
||||
let options = {
|
||||
...defaultOptions,
|
||||
algoliaOptions: 'algoliaOptions',
|
||||
autocompleteOptions: 'autocompleteOptions'
|
||||
};
|
||||
|
||||
// When
|
||||
let actual = new DocSearch(options);
|
||||
|
||||
// Then
|
||||
expect(actual.algoliaOptions).toEqual('algoliaOptions');
|
||||
expect(actual.autocompleteOptions).toEqual('autocompleteOptions');
|
||||
});
|
||||
it('should instanciate algoliasearch with the correct values', () => {
|
||||
// Given
|
||||
let options = defaultOptions;
|
||||
|
||||
// When
|
||||
new DocSearch(options);
|
||||
|
||||
// Then
|
||||
expect(AlgoliaSearch.calledOnce).toBe(true);
|
||||
expect(AlgoliaSearch.calledWith('BH4D9OD16A', 'apiKey')).toBe(true);
|
||||
});
|
||||
it('should set a custom User-Agent to algoliasearch', () => {
|
||||
// Given
|
||||
let options = defaultOptions;
|
||||
|
||||
// When
|
||||
new DocSearch(options);
|
||||
|
||||
// Then
|
||||
expect(algoliasearch.addAlgoliaAgent.calledOnce).toBe(true);
|
||||
});
|
||||
it('should instanciate autocomplete.js', () => {
|
||||
// Given
|
||||
let options = {
|
||||
...defaultOptions,
|
||||
autocompleteOptions: 'bar'
|
||||
};
|
||||
let $input = $('<input name="foo" />');
|
||||
getInputFromSelector.returns($input);
|
||||
|
||||
// When
|
||||
new DocSearch(options);
|
||||
|
||||
// Then
|
||||
expect(AutoComplete.calledOnce).toBe(true);
|
||||
expect(AutoComplete.calledWith($input, 'bar')).toBe(true);
|
||||
});
|
||||
it('should listen to the selected event of autocomplete', () => {
|
||||
// Given
|
||||
let options = defaultOptions;
|
||||
|
||||
// When
|
||||
new DocSearch(options);
|
||||
|
||||
// Then
|
||||
expect(autocomplete.on.calledOnce).toBe(true);
|
||||
expect(autocomplete.on.calledWith('autocomplete:selected')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkArguments', () => {
|
||||
let checkArguments;
|
||||
beforeEach(() => {
|
||||
checkArguments = DocSearch.checkArguments;
|
||||
});
|
||||
|
||||
it('should throw an error if no apiKey defined', () => {
|
||||
// Given
|
||||
let input = {
|
||||
let options = {
|
||||
indexName: 'indexName'
|
||||
};
|
||||
|
||||
// When
|
||||
expect(() => {
|
||||
docSearch(input);
|
||||
checkArguments(options);
|
||||
}).toThrow(/^Usage:/);
|
||||
});
|
||||
it('should throw an error if no indexName defined', () => {
|
||||
// Given
|
||||
let input = {
|
||||
let options = {
|
||||
apiKey: 'apiKey'
|
||||
};
|
||||
|
||||
// When
|
||||
expect(() => {
|
||||
docSearch(input);
|
||||
checkArguments(options);
|
||||
}).toThrow(/^Usage:/);
|
||||
});
|
||||
it('should throw an error if no input element matches the selector in the page', () => {
|
||||
it('should throw an error if no selector matches', () => {
|
||||
// Given
|
||||
let input = {
|
||||
let options = {
|
||||
apiKey: 'apiKey',
|
||||
indexName: 'indexName',
|
||||
inputSelector: '#unknown-input'
|
||||
indexName: 'indexName'
|
||||
};
|
||||
let getInputFromSelector = sinon.stub().returns(false);
|
||||
DocSearch.prototype.getInputFromSelector = getInputFromSelector;
|
||||
|
||||
// When
|
||||
expect(() => {
|
||||
docSearch(input);
|
||||
checkArguments(options);
|
||||
}).toThrow(/^Error:/);
|
||||
});
|
||||
it('should pass apiKey and indexName as properties of the instance', () => {
|
||||
// Given
|
||||
let input = {
|
||||
apiKey: 'apiKey',
|
||||
indexName: 'indexName',
|
||||
inputSelector: '#input'
|
||||
};
|
||||
});
|
||||
|
||||
// When
|
||||
let actual = docSearch(input);
|
||||
|
||||
// Then
|
||||
expect(actual.apiKey).toEqual('apiKey');
|
||||
expect(actual.indexName).toEqual('indexName');
|
||||
describe('getInputFromSelector', () => {
|
||||
let getInputFromSelector;
|
||||
beforeEach(() => {
|
||||
getInputFromSelector = DocSearch.getInputFromSelector;
|
||||
});
|
||||
it('should pass the matching input as property of the input', () => {
|
||||
|
||||
it('should return null if no element matches the selector', () => {
|
||||
// Given
|
||||
let input = {
|
||||
apiKey: 'apiKey',
|
||||
indexName: 'indexName',
|
||||
inputSelector: '#input'
|
||||
};
|
||||
let selector = '.i-do-not-exist > at #all';
|
||||
|
||||
// When
|
||||
let actual = docSearch(input);
|
||||
let actual = getInputFromSelector(selector);
|
||||
|
||||
// Then
|
||||
expect($(actual.input).attr('name')).toEqual('input-name');
|
||||
expect(actual).toEqual(null);
|
||||
});
|
||||
it('should pass options as properties of the input', () => {
|
||||
it('should return null if the matched element is not an input', () => {
|
||||
// Given
|
||||
let input = {
|
||||
apiKey: 'apiKey',
|
||||
indexName: 'indexName',
|
||||
inputSelector: '#input',
|
||||
algoliaOptions: {name: 'foo'},
|
||||
autocompleteOptions: {name: 'bar'}
|
||||
};
|
||||
let selector = '.i-am-a-span';
|
||||
|
||||
// When
|
||||
let actual = docSearch(input);
|
||||
let actual = getInputFromSelector(selector);
|
||||
|
||||
// Then
|
||||
expect(actual.algoliaOptions.name).toEqual('foo');
|
||||
expect(actual.autocompleteOptions.name).toEqual('bar');
|
||||
expect(actual).toEqual(null);
|
||||
});
|
||||
it('should return a Zepto wrapped element if it matches', () => {
|
||||
// Given
|
||||
let selector = '#input';
|
||||
|
||||
// When
|
||||
let actual = getInputFromSelector(selector);
|
||||
|
||||
// Then
|
||||
expect($.zepto.isZ(actual)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-env mocha */
|
||||
global.ddescribe = describe.only;
|
||||
global.xdescribe = describe.skip;
|
||||
global.iit = it.only;
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ describe('utils', () => {
|
|||
before(() => {
|
||||
// We need to load utils from here as it depends on Zepto, which itself
|
||||
// needs jsdom to be called before being loaded.
|
||||
utils = require('../src/lib/utils');
|
||||
utils = require('../src/lib/utils.js');
|
||||
});
|
||||
|
||||
describe('mergeKeyWithParent', () => {
|
||||
|
|
@ -175,16 +175,16 @@ describe('utils', () => {
|
|||
it('should flatten all values', () => {
|
||||
// Given
|
||||
let input = {
|
||||
'devs': [
|
||||
devs: [
|
||||
{name: 'Tim', category: 'dev'},
|
||||
{name: 'Vincent', category: 'dev'},
|
||||
{name: 'AlexS', category: 'dev'}
|
||||
],
|
||||
'sales': [
|
||||
],
|
||||
sales: [
|
||||
{name: 'Ben', category: 'sales'},
|
||||
{name: 'Jeremy', category: 'sales'},
|
||||
{name: 'AlexK', category: 'sales'}
|
||||
]
|
||||
]
|
||||
};
|
||||
|
||||
// When
|
||||
|
|
|
|||
|
|
@ -26,5 +26,5 @@ export default {
|
|||
// same issue, for loaders like babel
|
||||
resolveLoader: {
|
||||
fallback: [join(__dirname, '..', 'node_modules')]
|
||||
},
|
||||
}
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue