Repository: algolia/algoliasearch-helper-js (js)

Issue:
exclude refinement results doesn't check whether the mainSubResponse.facets exist
We call this unconditionally, meaning that when a wrong result (eg fake one from React InstantSearch Hooks) is used, this throws an error. We need to change from 

```js
  // add the excludes
  Object.keys(state.facetsExcludes).forEach(function(facetName) {
    var excludes = state.facetsExcludes[facetName];
    var position = facetsIndices[facetName];

    self.facets[position] = {
      name: facetName,
      data: mainSubResponse.facets[facetName],
      exhaustive: mainSubResponse.exhaustiveFacetsCount
    };
    excludes.forEach(function(facetValue) {
      self.facets[position] = self.facets[position] || {name: facetName};
      self.facets[position].data = self.facets[position].data || {};
      self.facets[position].data[facetValue] = 0;
    });
  });
```

to 

```js
  // add the excludes
  Object.keys(state.facetsExcludes).forEach(function(facetName) {
    var excludes = state.facetsExcludes[facetName];
    var position = facetsIndices[facetName];

    self.facets[position] = {
      name: facetName,
      data: mainFacets[facetName],
      exhaustive: mainSubResponse.exhaustiveFacetsCount
    };
    excludes.forEach(function(facetValue) {
      self.facets[position] = self.facets[position] || {name: facetName};
      self.facets[position].data = self.facets[position].data || {};
      self.facets[position].data[facetValue] = 0;
    });
  });
```

Pull request:
fix(SearchResults): use empty facets object for exclusion when results are artificial
This PR ensures that facet exclusions are computed from a result's facets property that is defaulted to an empty object, in case this is called with artificial results.

FX-2179
Closes #921 

Patch:
diff --git a/src/SearchResults/index.js b/src/SearchResults/index.js
index 98508da..7d38af1 100644
--- a/src/SearchResults/index.js
+++ b/src/SearchResults/index.js
@@ -588,7 +588,7 @@ function SearchResults(state, results, options) {
 
     self.facets[position] = {
       name: facetName,
-      data: mainSubResponse.facets[facetName],
+      data: mainFacets[facetName],
       exhaustive: mainSubResponse.exhaustiveFacetsCount
     };
     excludes.forEach(function(facetValue) {


Test patch:
diff --git a/test/spec/SearchResults/getRefinements.js b/test/spec/SearchResults/getRefinements.js
index 4d9d55d..3c925c0 100644
--- a/test/spec/SearchResults/getRefinements.js
+++ b/test/spec/SearchResults/getRefinements.js
@@ -64,6 +64,27 @@ test('getRefinements(facetName) returns a refinement(exclude) when a facet exclu
   expect(hasSameNames(refinements, refinedFacetValues)).toBeTruthy();
 });
 
+// See: https://github.com/algolia/algoliasearch-helper-js/issues/921
+test.only('getRefinements(facetName) returns a refinement(exclude) when a facet exclusion is set and results are artificial', function() {
+  var data = require('./getRefinements/exclude-artificial-results.json');
+  var searchParams = new SearchParameters(data.state);
+  var result = new SearchResults(searchParams, data.content.results);
+
+  var refinements = result.getRefinements();
+  var facetValues = result.getFacetValues('brand');
+  var refinedFacetValues = facetValues.filter(function(f) {
+    return f.isExcluded === true;
+  });
+
+  var expected = [{
+    attributeName: 'brand', count: 0, exhaustive: true, name: 'Apple', type: 'exclude'
+  }];
+
+  expect(refinements).toEqual(expected);
+  expect(refinements.length).toBe(refinedFacetValues.length);
+  expect(hasSameNames(refinements, refinedFacetValues)).toBeTruthy();
+});
+
 test(
   'getRefinements(facetName) returns a refinement(disjunctive) when a disjunctive refinement is set',
   function() {
diff --git a/test/spec/SearchResults/getRefinements/exclude-artificial-results.json b/test/spec/SearchResults/getRefinements/exclude-artificial-results.json
new file mode 100644
index 0000000..7d0e09a
--- /dev/null
+++ b/test/spec/SearchResults/getRefinements/exclude-artificial-results.json
@@ -0,0 +1,37 @@
+{
+  "content": {
+    "results": [{
+      "query": "",
+      "page": 0,
+      "hitsPerPage": 20,
+      "hits": [],
+      "nbHits": 0,
+      "nbPages": 0,
+      "params": "",
+      "exhaustiveNbHits": true,
+      "exhaustiveFacetsCount": true,
+      "processingTimeMS": 0,
+      "index": "instant_search"
+    }]
+  },
+  "state": {
+    "index": "instant_search",
+    "query": "",
+    "facets": [
+      "brand"
+    ],
+    "disjunctiveFacets": [],
+    "hierarchicalFacets": [],
+    "facetsRefinements": {},
+    "facetsExcludes": {
+      "brand": [
+        "Apple"
+      ]
+    },
+    "disjunctiveFacetsRefinements": {},
+    "numericRefinements": {},
+    "tagRefinements": [],
+    "hierarchicalFacetsRefinements": {},
+    "page": 0
+  }
+}
\ No newline at end of file