Skip to content

Commit 34478c1

Browse files
author
Eunjae Lee
authoredFeb 22, 2021
feat(stats): apply nbSortedHits (#4649)
* fix(stats): apply nbSortedHits * update default template for singular/plurial expressions * move the logic calculating isSmartSorted to the connector * clean up the condition for isSmartSorted * Revert "clean up the condition for isSmartSorted" This reverts commit a3b6761. * remove test code * rename isSmartSort to areHitsSorted and add more condition to it to compare nbHits !== nbSortedHits
1 parent 89c6e86 commit 34478c1

File tree

9 files changed

+286
-26
lines changed

9 files changed

+286
-26
lines changed
 

‎src/components/Stats/Stats.js

+9
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import Template from '../Template/Template';
66

77
const Stats = ({
88
nbHits,
9+
nbSortedHits,
10+
areHitsSorted,
911
hitsPerPage,
1012
nbPages,
1113
page,
@@ -21,11 +23,16 @@ const Stats = ({
2123
rootTagName="span"
2224
rootProps={{ className: cssClasses.text }}
2325
data={{
26+
hasManySortedResults: nbSortedHits > 1,
27+
hasNoSortedResults: nbSortedHits === 0,
28+
hasOneSortedResults: nbSortedHits === 1,
2429
hasManyResults: nbHits > 1,
2530
hasNoResults: nbHits === 0,
2631
hasOneResult: nbHits === 1,
32+
areHitsSorted,
2733
hitsPerPage,
2834
nbHits,
35+
nbSortedHits,
2936
nbPages,
3037
page,
3138
processingTimeMS,
@@ -43,6 +50,8 @@ Stats.propTypes = {
4350
}).isRequired,
4451
hitsPerPage: PropTypes.number,
4552
nbHits: PropTypes.number,
53+
nbSortedHits: PropTypes.number,
54+
areHitsSorted: PropTypes.bool,
4655
nbPages: PropTypes.number,
4756
page: PropTypes.number,
4857
processingTimeMS: PropTypes.number,

‎src/components/Stats/__tests__/Stats-test.js

+73
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { h } from 'preact';
44
import { mount } from 'enzyme';
55
import Stats from '../Stats';
66
import defaultTemplates from '../../../widgets/stats/defaultTemplates';
7+
import createHelpers from '../../../lib/createHelpers';
78

89
describe('Stats', () => {
910
const cssClasses = {
@@ -41,6 +42,78 @@ describe('Stats', () => {
4142
expect(wrapper).toMatchSnapshot();
4243
});
4344

45+
it('should render sorted hits', () => {
46+
const wrapper = mount(
47+
<Stats
48+
{...getProps({ nbSortedHits: 150, areHitsSorted: true })}
49+
templateProps={{
50+
templates: defaultTemplates,
51+
templatesConfig: {
52+
helpers: createHelpers({}),
53+
},
54+
}}
55+
/>
56+
);
57+
expect(wrapper.find('.text')).toMatchInlineSnapshot(`
58+
<span
59+
className="text"
60+
dangerouslySetInnerHTML={
61+
Object {
62+
"__html": "150 relevant results sorted out of 1,234 found in 42ms",
63+
}
64+
}
65+
/>
66+
`);
67+
});
68+
69+
it('should render 1 sorted hit', () => {
70+
const wrapper = mount(
71+
<Stats
72+
{...getProps({ nbSortedHits: 1, areHitsSorted: true })}
73+
templateProps={{
74+
templates: defaultTemplates,
75+
templatesConfig: {
76+
helpers: createHelpers({}),
77+
},
78+
}}
79+
/>
80+
);
81+
expect(wrapper.find('.text')).toMatchInlineSnapshot(`
82+
<span
83+
className="text"
84+
dangerouslySetInnerHTML={
85+
Object {
86+
"__html": "1 relevant result sorted out of 1,234 found in 42ms",
87+
}
88+
}
89+
/>
90+
`);
91+
});
92+
93+
it('should render 0 sorted hit', () => {
94+
const wrapper = mount(
95+
<Stats
96+
{...getProps({ nbSortedHits: 0, areHitsSorted: true })}
97+
templateProps={{
98+
templates: defaultTemplates,
99+
templatesConfig: {
100+
helpers: createHelpers({}),
101+
},
102+
}}
103+
/>
104+
);
105+
expect(wrapper.find('.text')).toMatchInlineSnapshot(`
106+
<span
107+
className="text"
108+
dangerouslySetInnerHTML={
109+
Object {
110+
"__html": "No relevant results sorted out of 1,234 found in 42ms",
111+
}
112+
}
113+
/>
114+
`);
115+
});
116+
44117
function getProps(extraProps = {}) {
45118
return {
46119
cssClasses,

‎src/connectors/stats/__tests__/connectStats-test.js

+91
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,10 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/stats/js/#c
7777

7878
expect(renderState.stats).toEqual({
7979
hitsPerPage: undefined,
80+
areHitsSorted: false,
8081
nbHits: 0,
8182
nbPages: 0,
83+
nbSortedHits: undefined,
8284
page: 0,
8385
processingTimeMS: -1,
8486
query: '',
@@ -102,8 +104,10 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/stats/js/#c
102104

103105
expect(renderState.stats).toEqual({
104106
hitsPerPage: 20,
107+
areHitsSorted: false,
105108
nbHits: 0,
106109
nbPages: 0,
110+
nbSortedHits: undefined,
107111
page: 0,
108112
processingTimeMS: 0,
109113
query: '',
@@ -137,8 +141,10 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/stats/js/#c
137141

138142
expect(renderState.stats).toEqual({
139143
hitsPerPage: 3,
144+
areHitsSorted: false,
140145
nbHits: 5,
141146
nbPages: 2,
147+
nbSortedHits: undefined,
142148
page: 0,
143149
processingTimeMS: 0,
144150
query: 'apple',
@@ -163,8 +169,10 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/stats/js/#c
163169

164170
expect(renderState).toEqual({
165171
hitsPerPage: undefined,
172+
areHitsSorted: false,
166173
nbHits: 0,
167174
nbPages: 0,
175+
nbSortedHits: undefined,
168176
page: 0,
169177
processingTimeMS: -1,
170178
query: '',
@@ -187,8 +195,10 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/stats/js/#c
187195

188196
expect(renderState).toEqual({
189197
hitsPerPage: 20,
198+
areHitsSorted: false,
190199
nbHits: 0,
191200
nbPages: 0,
201+
nbSortedHits: undefined,
192202
page: 0,
193203
processingTimeMS: 0,
194204
query: '',
@@ -221,8 +231,89 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/stats/js/#c
221231

222232
expect(renderState).toEqual({
223233
hitsPerPage: 3,
234+
areHitsSorted: false,
224235
nbHits: 5,
225236
nbPages: 2,
237+
nbSortedHits: undefined,
238+
page: 0,
239+
processingTimeMS: 0,
240+
query: 'apple',
241+
widgetParams: {},
242+
});
243+
});
244+
245+
test('returns areHitsSorted as true', () => {
246+
const [stats, helper] = getInitializedWidget();
247+
248+
const renderState = stats.getWidgetRenderState(
249+
createRenderOptions({
250+
helper,
251+
state: helper.state,
252+
results: new SearchResults(helper.state, [
253+
createSingleSearchResponse({
254+
hits: [
255+
{ brand: 'samsung', objectID: '1' },
256+
{ brand: 'apple', objectID: '2' },
257+
{ brand: 'sony', objectID: '3' },
258+
{ brand: 'benq', objectID: '4' },
259+
{ brand: 'dyson', objectID: '5' },
260+
],
261+
hitsPerPage: 3,
262+
query: 'apple',
263+
appliedRelevancyStrictness: 20,
264+
nbHits: 20,
265+
nbPages: 2,
266+
nbSortedHits: 5,
267+
}),
268+
]),
269+
})
270+
);
271+
272+
expect(renderState).toEqual({
273+
hitsPerPage: 3,
274+
areHitsSorted: true,
275+
nbHits: 20,
276+
nbPages: 2,
277+
nbSortedHits: 5,
278+
page: 0,
279+
processingTimeMS: 0,
280+
query: 'apple',
281+
widgetParams: {},
282+
});
283+
});
284+
285+
test('returns areHitsSorted as false when nbSortedHits === nbHits', () => {
286+
const [stats, helper] = getInitializedWidget();
287+
288+
const renderState = stats.getWidgetRenderState(
289+
createRenderOptions({
290+
helper,
291+
state: helper.state,
292+
results: new SearchResults(helper.state, [
293+
createSingleSearchResponse({
294+
hits: [
295+
{ brand: 'samsung', objectID: '1' },
296+
{ brand: 'apple', objectID: '2' },
297+
{ brand: 'sony', objectID: '3' },
298+
{ brand: 'benq', objectID: '4' },
299+
{ brand: 'dyson', objectID: '5' },
300+
],
301+
hitsPerPage: 3,
302+
query: 'apple',
303+
appliedRelevancyStrictness: 20,
304+
nbHits: 5,
305+
nbSortedHits: 5,
306+
}),
307+
]),
308+
})
309+
);
310+
311+
expect(renderState).toEqual({
312+
hitsPerPage: 3,
313+
areHitsSorted: false,
314+
nbHits: 5,
315+
nbPages: 2,
316+
nbSortedHits: 5,
226317
page: 0,
227318
processingTimeMS: 0,
228319
query: 'apple',

‎src/connectors/stats/connectStats.js

+7
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,8 @@ export default function connectStats(renderFn, unmountFn = noop) {
9797
return {
9898
hitsPerPage: helper.state.hitsPerPage,
9999
nbHits: 0,
100+
nbSortedHits: undefined,
101+
areHitsSorted: false,
100102
nbPages: 0,
101103
page: helper.state.page || 0,
102104
processingTimeMS: -1,
@@ -108,6 +110,11 @@ export default function connectStats(renderFn, unmountFn = noop) {
108110
return {
109111
hitsPerPage: results.hitsPerPage,
110112
nbHits: results.nbHits,
113+
nbSortedHits: results.nbSortedHits,
114+
areHitsSorted:
115+
typeof results.appliedRelevancyStrictness !== 'undefined' &&
116+
results.appliedRelevancyStrictness > 0 &&
117+
results.nbSortedHits !== results.nbHits,
111118
nbPages: results.nbPages,
112119
page: results.page,
113120
processingTimeMS: results.processingTimeMS,

‎src/widgets/stats/__tests__/__snapshots__/stats-test.js.snap

+30-6
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,33 @@
22

33
exports[`stats() calls twice render(<Stats props />, container) 1`] = `
44
Object {
5+
"areHitsSorted": false,
56
"cssClasses": Object {
67
"root": "ais-Stats",
78
"text": "ais-Stats-text text cx",
89
},
910
"hitsPerPage": 2,
1011
"nbHits": 20,
1112
"nbPages": 10,
13+
"nbSortedHits": undefined,
1214
"page": 0,
1315
"processingTimeMS": 42,
1416
"query": "a query",
1517
"templateProps": Object {
1618
"templates": Object {
17-
"text": "{{#hasNoResults}}No results{{/hasNoResults}}
18-
{{#hasOneResult}}1 result{{/hasOneResult}}
19-
{{#hasManyResults}}{{#helpers.formatNumber}}{{nbHits}}{{/helpers.formatNumber}} results{{/hasManyResults}} found in {{processingTimeMS}}ms",
19+
"text": "
20+
{{#areHitsSorted}}
21+
{{#hasNoSortedResults}}No relevant results{{/hasNoSortedResults}}
22+
{{#hasOneSortedResults}}1 relevant result{{/hasOneSortedResults}}
23+
{{#hasManySortedResults}}{{#helpers.formatNumber}}{{nbSortedHits}}{{/helpers.formatNumber}} relevant results{{/hasManySortedResults}}
24+
sorted out of {{#helpers.formatNumber}}{{nbHits}}{{/helpers.formatNumber}}
25+
{{/areHitsSorted}}
26+
{{^areHitsSorted}}
27+
{{#hasNoResults}}No results{{/hasNoResults}}
28+
{{#hasOneResult}}1 result{{/hasOneResult}}
29+
{{#hasManyResults}}{{#helpers.formatNumber}}{{nbHits}}{{/helpers.formatNumber}} results{{/hasManyResults}}
30+
{{/areHitsSorted}}
31+
found in {{processingTimeMS}}ms",
2032
},
2133
"templatesConfig": undefined,
2234
"useCustomCompileOptions": Object {
@@ -28,21 +40,33 @@ Object {
2840

2941
exports[`stats() calls twice render(<Stats props />, container) 2`] = `
3042
Object {
43+
"areHitsSorted": false,
3144
"cssClasses": Object {
3245
"root": "ais-Stats",
3346
"text": "ais-Stats-text text cx",
3447
},
3548
"hitsPerPage": 2,
3649
"nbHits": 20,
3750
"nbPages": 10,
51+
"nbSortedHits": undefined,
3852
"page": 0,
3953
"processingTimeMS": 42,
4054
"query": "a query",
4155
"templateProps": Object {
4256
"templates": Object {
43-
"text": "{{#hasNoResults}}No results{{/hasNoResults}}
44-
{{#hasOneResult}}1 result{{/hasOneResult}}
45-
{{#hasManyResults}}{{#helpers.formatNumber}}{{nbHits}}{{/helpers.formatNumber}} results{{/hasManyResults}} found in {{processingTimeMS}}ms",
57+
"text": "
58+
{{#areHitsSorted}}
59+
{{#hasNoSortedResults}}No relevant results{{/hasNoSortedResults}}
60+
{{#hasOneSortedResults}}1 relevant result{{/hasOneSortedResults}}
61+
{{#hasManySortedResults}}{{#helpers.formatNumber}}{{nbSortedHits}}{{/helpers.formatNumber}} relevant results{{/hasManySortedResults}}
62+
sorted out of {{#helpers.formatNumber}}{{nbHits}}{{/helpers.formatNumber}}
63+
{{/areHitsSorted}}
64+
{{^areHitsSorted}}
65+
{{#hasNoResults}}No results{{/hasNoResults}}
66+
{{#hasOneResult}}1 result{{/hasOneResult}}
67+
{{#hasManyResults}}{{#helpers.formatNumber}}{{nbHits}}{{/helpers.formatNumber}} results{{/hasManyResults}}
68+
{{/areHitsSorted}}
69+
found in {{processingTimeMS}}ms",
4670
},
4771
"templatesConfig": undefined,
4872
"useCustomCompileOptions": Object {

‎src/widgets/stats/__tests__/stats-test.js

+32-10
Original file line numberDiff line numberDiff line change
@@ -26,20 +26,10 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/stats/js/"
2626
describe('stats()', () => {
2727
let container;
2828
let widget;
29-
let results;
3029

3130
beforeEach(() => {
3231
container = document.createElement('div');
3332
widget = stats({ container, cssClasses: { text: ['text', 'cx'] } });
34-
results = {
35-
hits: [{}, {}],
36-
nbHits: 20,
37-
page: 0,
38-
nbPages: 10,
39-
hitsPerPage: 2,
40-
processingTimeMS: 42,
41-
query: 'a query',
42-
};
4333

4434
widget.init({
4535
helper: { state: {} },
@@ -50,6 +40,15 @@ describe('stats()', () => {
5040
});
5141

5242
it('calls twice render(<Stats props />, container)', () => {
43+
const results = {
44+
hits: [{}, {}],
45+
nbHits: 20,
46+
page: 0,
47+
nbPages: 10,
48+
hitsPerPage: 2,
49+
processingTimeMS: 42,
50+
query: 'a query',
51+
};
5352
widget.render({ results, instantSearchInstance });
5453
widget.render({ results, instantSearchInstance });
5554

@@ -61,4 +60,27 @@ describe('stats()', () => {
6160
expect(secondRender[0].props).toMatchSnapshot();
6261
expect(secondRender[1]).toEqual(container);
6362
});
63+
64+
it('renders sorted hits', () => {
65+
const results = {
66+
hits: [{}, {}],
67+
nbHits: 20,
68+
nbSortedHits: 16,
69+
appliedRelevancyStrictness: 20,
70+
page: 0,
71+
nbPages: 10,
72+
hitsPerPage: 2,
73+
processingTimeMS: 42,
74+
query: 'second query',
75+
};
76+
widget.render({ results, instantSearchInstance });
77+
78+
const [firstRender] = render.mock.calls;
79+
expect(firstRender[0].props).toEqual(
80+
expect.objectContaining({
81+
areHitsSorted: true,
82+
nbSortedHits: 16,
83+
})
84+
);
85+
});
6486
});

‎src/widgets/stats/defaultTemplates.js

+13-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
export default {
2-
text: `{{#hasNoResults}}No results{{/hasNoResults}}
3-
{{#hasOneResult}}1 result{{/hasOneResult}}
4-
{{#hasManyResults}}{{#helpers.formatNumber}}{{nbHits}}{{/helpers.formatNumber}} results{{/hasManyResults}} found in {{processingTimeMS}}ms`,
2+
text: `
3+
{{#areHitsSorted}}
4+
{{#hasNoSortedResults}}No relevant results{{/hasNoSortedResults}}
5+
{{#hasOneSortedResults}}1 relevant result{{/hasOneSortedResults}}
6+
{{#hasManySortedResults}}{{#helpers.formatNumber}}{{nbSortedHits}}{{/helpers.formatNumber}} relevant results{{/hasManySortedResults}}
7+
sorted out of {{#helpers.formatNumber}}{{nbHits}}{{/helpers.formatNumber}}
8+
{{/areHitsSorted}}
9+
{{^areHitsSorted}}
10+
{{#hasNoResults}}No results{{/hasNoResults}}
11+
{{#hasOneResult}}1 result{{/hasOneResult}}
12+
{{#hasManyResults}}{{#helpers.formatNumber}}{{nbHits}}{{/helpers.formatNumber}} results{{/hasManyResults}}
13+
{{/areHitsSorted}}
14+
found in {{processingTimeMS}}ms`,
515
};

‎src/widgets/stats/stats.js

+6-1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ const renderer = ({ containerNode, cssClasses, renderState, templates }) => (
1919
{
2020
hitsPerPage,
2121
nbHits,
22+
nbSortedHits,
23+
areHitsSorted,
2224
nbPages,
2325
page,
2426
processingTimeMS,
@@ -42,6 +44,8 @@ const renderer = ({ containerNode, cssClasses, renderState, templates }) => (
4244
cssClasses={cssClasses}
4345
hitsPerPage={hitsPerPage}
4446
nbHits={nbHits}
47+
nbSortedHits={nbSortedHits}
48+
areHitsSorted={areHitsSorted}
4549
nbPages={nbPages}
4650
page={page}
4751
processingTimeMS={processingTimeMS}
@@ -55,7 +59,7 @@ const renderer = ({ containerNode, cssClasses, renderState, templates }) => (
5559
/**
5660
* @typedef {Object} StatsWidgetTemplates
5761
* @property {string|function} [text] Text template, provided with `hasManyResults`,
58-
* `hasNoResults`, `hasOneResult`, `hitsPerPage`, `nbHits`, `nbPages`, `page`, `processingTimeMS`, `query`.
62+
* `hasNoResults`, `hasOneResult`, `hitsPerPage`, `nbHits`, `nbSortedHits`, `nbPages`, `page`, `processingTimeMS`, `query`.
5963
*/
6064

6165
/**
@@ -71,6 +75,7 @@ const renderer = ({ containerNode, cssClasses, renderState, templates }) => (
7175
* @property {boolean} hasOneResult True if the result set has exactly one result.
7276
* @property {number} hitsPerPage Number of hits per page.
7377
* @property {number} nbHits Number of hit in the result set.
78+
* @property {number} nbSortedHits Subset of hits selected when relevancyStrictness is applied
7479
* @property {number} nbPages Number of pages in the result set with regard to the hitsPerPage and number of hits.
7580
* @property {number} page Number of the current page. First page is 0.
7681
* @property {number} processingTimeMS Time taken to compute the results inside the engine.

‎stories/stats.stories.js

+25-6
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,28 @@
1+
import algoliasearch from 'algoliasearch/lite';
12
import { storiesOf } from '@storybook/html';
23
import { withHits } from '../.storybook/decorators';
34

4-
storiesOf('Metadata/Stats', module).add(
5-
'default',
6-
withHits(({ search, container, instantsearch }) => {
7-
search.addWidgets([instantsearch.widgets.stats({ container })]);
8-
})
9-
);
5+
storiesOf('Metadata/Stats', module)
6+
.add(
7+
'default',
8+
withHits(({ search, container, instantsearch }) => {
9+
search.addWidgets([instantsearch.widgets.stats({ container })]);
10+
})
11+
)
12+
.add(
13+
'with sorted hits',
14+
withHits(
15+
({ search, container, instantsearch }) => {
16+
search.addWidgets([instantsearch.widgets.stats({ container })]);
17+
},
18+
{
19+
appId: 'C7RIRJRYR9',
20+
apiKey: '77af6d5ffb27caa5ff4937099fcb92e8',
21+
indexName: 'test_Bestbuy_vr_price_asc',
22+
searchClient: algoliasearch(
23+
'C7RIRJRYR9',
24+
'77af6d5ffb27caa5ff4937099fcb92e8'
25+
),
26+
}
27+
)
28+
);

0 commit comments

Comments
 (0)
Please sign in to comment.