diff --git a/.gitignore b/.gitignore
index e3736c0e..e3f8c0c4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
+.DS_Store
node_modules/
dist/
npm-debug.log*
diff --git a/package.json b/package.json
index 2cb1b515..68493838 100644
--- a/package.json
+++ b/package.json
@@ -45,7 +45,6 @@
"mocha-jsdom": "^1.0.0",
"mversion": "^1.10.1",
"nd": "^1.2.0",
- "node-fixtures": "0.0.1",
"node-sass": "^3.4.2",
"npm-shrinkwrap": "^200.4.0",
"onchange": "^2.0.0",
diff --git a/src/lib/DocSearch.js b/src/lib/DocSearch.js
index d494e5b7..81f2c2a8 100644
--- a/src/lib/DocSearch.js
+++ b/src/lib/DocSearch.js
@@ -102,16 +102,19 @@ class DocSearch {
query: query,
params: this.algoliaOptions
}]).then((data) => {
- callback(this.formatHits(data.results[0].hits));
+ callback(DocSearch.formatHits(data.results[0].hits));
});
};
}
// Given a list of hits returned by the API, will reformat them to be used in
// a Hogan template
- formatHits(receivedHits) {
- let hits = receivedHits.map((hit) => {
- hit._highlightResult = utils.mergeKeyWithParent(hit._highlightResult, 'hierarchy');
+ static formatHits(receivedHits) {
+ let clonedHits = utils.deepClone(receivedHits);
+ let hits = clonedHits.map((hit) => {
+ if (hit._highlightResult) {
+ hit._highlightResult = utils.mergeKeyWithParent(hit._highlightResult, 'hierarchy');
+ }
return utils.mergeKeyWithParent(hit, 'hierarchy');
});
@@ -140,7 +143,7 @@ class DocSearch {
return {
isCategoryHeader: hit.isCategoryHeader,
- isSubcategoryHeader: hit.isSubcategoryHeader,
+ isSubCategoryHeader: hit.isSubCategoryHeader,
category: category,
subcategory: subcategory,
title: displayTitle,
diff --git a/src/lib/utils.js b/src/lib/utils.js
index ee79fae9..e4e94d34 100644
--- a/src/lib/utils.js
+++ b/src/lib/utils.js
@@ -24,10 +24,10 @@ let utils = {
*/
mergeKeyWithParent(object, property) {
if (object[property] === undefined) {
- throw new Error(`[mergeKeyWithParent]: Object has no key ${property}`);
+ return object;
}
if (typeof object[property] !== 'object') {
- throw new Error(`[mergeKeyWithParent]: Key ${property} is not an object`);
+ return object;
}
let newObject = $.extend({}, object, object[property]);
delete newObject[property];
@@ -144,9 +144,11 @@ let utils = {
* @return {array}
*/
flattenAndFlagFirst(object, flag) {
- let values = this.values(object).map(value => {
- value[0][flag] = true;
- return value;
+ let values = this.values(object).map(collection => {
+ return collection.map((item, index) => {
+ item[flag] = (index === 0);
+ return item;
+ });
});
return this.flatten(values);
},
@@ -230,9 +232,16 @@ let utils = {
snippet = `${snippet}…`;
}
return snippet;
+ },
+ /*
+ * Deep clone an object.
+ * Note: This will not clone functions and dates
+ * @param {object} object Object to clone
+ * @return {object}
+ */
+ deepClone(object) {
+ return JSON.parse(JSON.stringify(object));
}
-
-
};
export default utils;
diff --git a/test/DocSearch-test.js b/test/DocSearch-test.js
index 092c442b..a032957e 100644
--- a/test/DocSearch-test.js
+++ b/test/DocSearch-test.js
@@ -4,11 +4,11 @@
import jsdom from 'mocha-jsdom';
import expect from 'expect';
import sinon from 'sinon';
-// import fixtures from 'node-fixtures';
describe('DocSearch', () => {
let DocSearch;
let $;
+
jsdom({useEach: true});
beforeEach(() => {
@@ -309,4 +309,362 @@ describe('DocSearch', () => {
});
});
});
+
+ describe('formatHits', () => {
+ it('should not mutate the input', () => {
+ // Given
+ let input = [{
+ hierarchy: {
+ lvl0: 'Ruby',
+ lvl1: 'API',
+ lvl2: null,
+ lvl3: null,
+ lvl4: null,
+ lvl5: null
+ }
+ }];
+
+ // When
+ let actual = DocSearch.formatHits(input);
+
+ // Then
+ expect(input).toNotBe(actual);
+ });
+ it('should set category headers to the first of each category', () => {
+ // Given
+ let input = [{
+ hierarchy: {
+ lvl0: 'Ruby',
+ lvl1: 'API',
+ lvl2: null,
+ lvl3: null,
+ lvl4: null,
+ lvl5: null
+ }
+ }, {
+ hierarchy: {
+ lvl0: 'Ruby',
+ lvl1: 'Geo-search',
+ lvl2: null,
+ lvl3: null,
+ lvl4: null,
+ lvl5: null
+ }
+ }, {
+ hierarchy: {
+ lvl0: 'Python',
+ lvl1: 'API',
+ lvl2: null,
+ lvl3: null,
+ lvl4: null,
+ lvl5: null
+ }
+ }];
+
+ // When
+ let actual = DocSearch.formatHits(input);
+
+ // Then
+ expect(actual[0].isCategoryHeader).toEqual(true);
+ expect(actual[2].isCategoryHeader).toEqual(true);
+ });
+ it('should group items of same category together', () => {
+ // Given
+ let input = [{
+ hierarchy: {
+ lvl0: 'Ruby',
+ lvl1: 'API',
+ lvl2: null,
+ lvl3: null,
+ lvl4: null,
+ lvl5: null
+ }
+ }, {
+ hierarchy: {
+ lvl0: 'Python',
+ lvl1: 'API',
+ lvl2: null,
+ lvl3: null,
+ lvl4: null,
+ lvl5: null
+ }
+ }, {
+ hierarchy: {
+ lvl0: 'Ruby',
+ lvl1: 'Geo-search',
+ lvl2: null,
+ lvl3: null,
+ lvl4: null,
+ lvl5: null
+ }
+ }];
+
+ // When
+ let actual = DocSearch.formatHits(input);
+
+ // Then
+ expect(actual[0].category).toEqual('Ruby');
+ expect(actual[1].category).toEqual('Ruby');
+ expect(actual[2].category).toEqual('Python');
+ });
+ it('should mark all first elements as subcategories', () => {
+ // Given
+ let input = [{
+ hierarchy: {
+ lvl0: 'Ruby',
+ lvl1: 'API',
+ lvl2: null,
+ lvl3: null,
+ lvl4: null,
+ lvl5: null
+ }
+ }, {
+ hierarchy: {
+ lvl0: 'Python',
+ lvl1: 'API',
+ lvl2: null,
+ lvl3: null,
+ lvl4: null,
+ lvl5: null
+ }
+ }, {
+ hierarchy: {
+ lvl0: 'Ruby',
+ lvl1: 'Geo-search',
+ lvl2: null,
+ lvl3: null,
+ lvl4: null,
+ lvl5: null
+ }
+ }];
+
+ // When
+ let actual = DocSearch.formatHits(input);
+
+ // Then
+ expect(actual[0].isSubCategoryHeader).toEqual(true);
+ expect(actual[2].isSubCategoryHeader).toEqual(true);
+ });
+ it('should mark new subcategories as such', () => {
+ // Given
+ let input = [{
+ hierarchy: {
+ lvl0: 'Ruby',
+ lvl1: 'API',
+ lvl2: 'Foo',
+ lvl3: null,
+ lvl4: null,
+ lvl5: null
+ }
+ }, {
+ hierarchy: {
+ lvl0: 'Python',
+ lvl1: 'API',
+ lvl2: null,
+ lvl3: null,
+ lvl4: null,
+ lvl5: null
+ }
+ }, {
+ hierarchy: {
+ lvl0: 'Ruby',
+ lvl1: 'API',
+ lvl2: 'Bar',
+ lvl3: null,
+ lvl4: null,
+ lvl5: null
+ }
+ }, {
+ hierarchy: {
+ lvl0: 'Ruby',
+ lvl1: 'Geo-search',
+ lvl2: null,
+ lvl3: null,
+ lvl4: null,
+ lvl5: null
+ }
+ }];
+
+ // When
+ let actual = DocSearch.formatHits(input);
+
+ // Then
+ expect(actual[0].isSubCategoryHeader).toEqual(true);
+ expect(actual[1].isSubCategoryHeader).toEqual(false);
+ expect(actual[2].isSubCategoryHeader).toEqual(true);
+ expect(actual[3].isSubCategoryHeader).toEqual(true);
+ });
+ it('should use highlighted category and subcategory if exists', () => {
+ // Given
+ let input = [{
+ hierarchy: {
+ lvl0: 'Ruby',
+ lvl1: 'API',
+ lvl2: 'Foo',
+ lvl3: null,
+ lvl4: null,
+ lvl5: null
+ },
+ _highlightResult: {
+ hierarchy: {
+ lvl0: {
+ value: 'Ruby'
+ },
+ lvl1: {
+ value: 'API'
+ }
+ }
+ }
+ }];
+
+ // When
+ let actual = DocSearch.formatHits(input);
+
+ // Then
+ expect(actual[0].category).toEqual('Ruby');
+ expect(actual[0].subcategory).toEqual('API');
+ });
+ it('should use lvl2 as title', () => {
+ // Given
+ let input = [{
+ hierarchy: {
+ lvl0: 'Ruby',
+ lvl1: 'API',
+ lvl2: 'Foo',
+ lvl3: null,
+ lvl4: null,
+ lvl5: null
+ }
+ }];
+
+ // When
+ let actual = DocSearch.formatHits(input);
+
+ // Then
+ expect(actual[0].title).toEqual('Foo');
+ });
+ it('should use lvl1 as title if no lvl2', () => {
+ // Given
+ let input = [{
+ hierarchy: {
+ lvl0: 'Ruby',
+ lvl1: 'API',
+ lvl2: null,
+ lvl3: null,
+ lvl4: null,
+ lvl5: null
+ }
+ }];
+
+ // When
+ let actual = DocSearch.formatHits(input);
+
+ // Then
+ expect(actual[0].title).toEqual('API');
+ });
+ it('should use lvl0 as title if no lvl2 nor lvl2', () => {
+ // Given
+ let input = [{
+ hierarchy: {
+ lvl0: 'Ruby',
+ lvl1: null,
+ lvl2: null,
+ lvl3: null,
+ lvl4: null,
+ lvl5: null
+ }
+ }];
+
+ // When
+ let actual = DocSearch.formatHits(input);
+
+ // Then
+ expect(actual[0].title).toEqual('Ruby');
+ });
+ it('should concatenate lvl2+ for title if more', () => {
+ // Given
+ let input = [{
+ hierarchy: {
+ lvl0: 'Ruby',
+ lvl1: 'API',
+ lvl2: 'Geo-search',
+ lvl3: 'Foo',
+ lvl4: 'Bar',
+ lvl5: 'Baz'
+ }
+ }];
+
+ // When
+ let actual = DocSearch.formatHits(input);
+
+ // Then
+ expect(actual[0].title).toEqual('Geo-search › Foo › Bar › Baz');
+ });
+ it('should concatenate highlighted elements', () => {
+ // Given
+ let input = [{
+ hierarchy: {
+ lvl0: 'Ruby',
+ lvl1: 'API',
+ lvl2: 'Geo-search',
+ lvl3: 'Foo',
+ lvl4: 'Bar',
+ lvl5: 'Baz'
+ },
+ _highlightResult: {
+ hierarchy: {
+ lvl0: {
+ value: 'Ruby'
+ },
+ lvl1: {
+ value: 'API'
+ },
+ lvl2: {
+ value: 'Geo-search'
+ },
+ lvl3: {
+ value: 'Foo'
+ },
+ lvl4: {
+ value: 'Bar'
+ },
+ lvl5: {
+ value: 'Baz'
+ }
+ }
+ }
+ }];
+
+ // When
+ let actual = DocSearch.formatHits(input);
+
+ // Then
+ expect(actual[0].title).toEqual('Geo-search › Foo › Bar › Baz');
+ });
+ it('should add ellipsis to content', () => {
+ // Given
+ let input = [{
+ hierarchy: {
+ lvl0: 'Ruby',
+ lvl1: 'API',
+ lvl2: null,
+ lvl3: null,
+ lvl4: null,
+ lvl5: null
+ },
+ content: 'foo bar',
+ _snippetResult: {
+ content: {
+ value: 'lorem foo bar ipsum.'
+ }
+ }
+ }];
+
+ // When
+ let actual = DocSearch.formatHits(input);
+
+ // Then
+ expect(actual[0].text).toEqual('…lorem foo bar ipsum.');
+ });
+ });
});
diff --git a/test/fixtures/formatHits.json b/test/fixtures/formatHits.json
index d5d8a5f5..3e89e9b5 100644
--- a/test/fixtures/formatHits.json
+++ b/test/fixtures/formatHits.json
@@ -23,7 +23,7 @@
"value": "API"
},
"lvl2": {
- "value": "Search"
+ "value": "Search"
},
"lvl3": null,
"lvl4": null,
diff --git a/test/utils-test.js b/test/utils-test.js
index d8e3714c..7665c574 100644
--- a/test/utils-test.js
+++ b/test/utils-test.js
@@ -65,16 +65,17 @@ describe('utils', () => {
expect(actual.lvl0).toNotEqual(42);
expect(actual.lvl0).toEqual('bar');
});
- it('should throw an error if no such key', () => {
+ it('should do nothing if no such key', () => {
// Given
let input = {
name: 'foo'
};
// When
- expect(() => {
- utils.mergeKeyWithParent(input, 'hierarchy');
- }).toThrow(Error);
+ let actual = utils.mergeKeyWithParent(input, 'hierarchy');
+
+ // Then
+ expect(actual).toBe(input);
});
it('should throw an error if key is no an object', () => {
// Given
@@ -84,9 +85,10 @@ describe('utils', () => {
};
// When
- expect(() => {
- utils.mergeKeyWithParent(input, 'hierarchy');
- }).toThrow(Error);
+ let actual = utils.mergeKeyWithParent(input, 'hierarchy');
+
+ // Then
+ expect(actual).toBe(input);
});
});
@@ -193,11 +195,11 @@ describe('utils', () => {
// Then
expect(actual).toEqual([
{name: 'Tim', category: 'dev', isTop: true},
- {name: 'Vincent', category: 'dev'},
- {name: 'AlexS', category: 'dev'},
+ {name: 'Vincent', category: 'dev', isTop: false},
+ {name: 'AlexS', category: 'dev', isTop: false},
{name: 'Ben', category: 'sales', isTop: true},
- {name: 'Jeremy', category: 'sales'},
- {name: 'AlexK', category: 'sales'}
+ {name: 'Jeremy', category: 'sales', isTop: false},
+ {name: 'AlexK', category: 'sales', isTop: false}
]);
});
});
@@ -328,4 +330,38 @@ describe('utils', () => {
expect(actual).toEqual('This is an finished sentence…');
});
});
+
+ describe('deepClone', () => {
+ it('should create an object with the exact same value', () => {
+ // Given
+ let input = {
+ foo: {
+ bar: 'baz'
+ }
+ };
+
+ // When
+ let actual = utils.deepClone(input);
+
+ // Then
+ expect(actual.foo.bar).toEqual('baz');
+ });
+ it('should not change the initial object', () => {
+ // Given
+ let input = {
+ foo: {
+ bar: 'baz'
+ }
+ };
+
+ // When
+ let actual = utils.deepClone(input);
+ input.foo.bar = 42;
+
+ // Then
+ expect(input.foo.bar).toEqual(42);
+ expect(actual.foo.bar).toNotEqual(42);
+ expect(actual.foo.bar).toEqual('baz');
+ });
+ });
});