From cfcbe1e2615cd0418b1ee9378ff714a931e89608 Mon Sep 17 00:00:00 2001 From: Pixelastic Date: Mon, 21 Dec 2015 12:34:59 +0100 Subject: [PATCH] test(formatHits): Add tests for formatHits The main logic of the library: splitting a set of 5 results into a neatly ordered array, to be able to display it as two columns in Hogan. --- .gitignore | 1 + package.json | 1 - src/lib/DocSearch.js | 13 +- src/lib/utils.js | 23 ++- test/DocSearch-test.js | 360 +++++++++++++++++++++++++++++++++- test/fixtures/formatHits.json | 2 +- test/utils-test.js | 58 ++++-- 7 files changed, 432 insertions(+), 26 deletions(-) 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-searchFooBarBaz'); + }); + 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'); + }); + }); });