Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(Insights): Insights inside Instantsearch #3598

Merged
merged 9 commits into from
Apr 15, 2019
43 changes: 43 additions & 0 deletions src/connectors/hits/__tests__/connectHits-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ import jsHelper, { SearchResults } from 'algoliasearch-helper';
import { TAG_PLACEHOLDER } from '../../../lib/escape-highlight';
import connectHits from '../connectHits';

jest.mock('../../../lib/utils/hits-absolute-position', () => ({
addAbsolutePosition: hits => hits,
}));

describe('connectHits', () => {
it('throws without render function', () => {
expect(() => {
Expand Down Expand Up @@ -223,6 +227,45 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/hits/js/#co
);
});

it('adds queryID if provided to results', () => {
const rendering = jest.fn();
const makeWidget = connectHits(rendering);
const widget = makeWidget({});

const helper = jsHelper({}, '', {});
helper.search = jest.fn();

widget.init({
helper,
state: helper.state,
createURL: () => '#',
onHistoryChange: () => {},
});

const hits = [{ name: 'name 1' }, { name: 'name 2' }];

const results = new SearchResults(helper.state, [
{ hits, queryID: 'theQueryID' },
]);
widget.render({
results,
state: helper.state,
helper,
createURL: () => '#',
});

expect(rendering).toHaveBeenNthCalledWith(
2,
expect.objectContaining({
hits: [
{ name: 'name 1', __queryID: 'theQueryID' },
{ name: 'name 2', __queryID: 'theQueryID' },
],
}),
expect.anything()
);
});

it('transform items after escaping', () => {
const rendering = jest.fn();
const makeWidget = connectHits(rendering);
Expand Down
10 changes: 10 additions & 0 deletions src/connectors/hits/connectHits.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import escapeHits, { TAG_PLACEHOLDER } from '../../lib/escape-highlight';
import {
checkRendering,
createDocumentationMessageGenerator,
addAbsolutePosition,
addQueryID,
} from '../../lib/utils';

const withUsage = createDocumentationMessageGenerator({
Expand Down Expand Up @@ -76,6 +78,14 @@ export default function connectHits(renderFn, unmountFn) {
results.hits = escapeHits(results.hits);
}

results.hits = addAbsolutePosition(
results.hits,
results.page,
results.hitsPerPage
);

results.hits = addQueryID(results.hits, results.queryID);

results.hits = transformItems(results.hits);

renderFn(
Expand Down
56 changes: 56 additions & 0 deletions src/connectors/infinite-hits/__tests__/connectInfiniteHits-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ import { TAG_PLACEHOLDER } from '../../../lib/escape-highlight';

import connectInfiniteHits from '../connectInfiniteHits';

jest.mock('../../../lib/utils/hits-absolute-position', () => ({
addAbsolutePosition: hits => hits,
}));

describe('connectInfiniteHits', () => {
it('throws without render function', () => {
expect(() => {
Expand Down Expand Up @@ -360,6 +364,58 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/infinite-hi
);
});

it('adds queryID if provided to results', () => {
const rendering = jest.fn();
const makeWidget = connectInfiniteHits(rendering);
const widget = makeWidget({});

const helper = jsHelper({}, '', {});
helper.search = jest.fn();

widget.init({
helper,
state: helper.state,
createURL: () => '#',
onHistoryChange: () => {},
});

const hits = [
{
name: 'name 1',
},
{
name: 'name 2',
},
];

const results = new SearchResults(helper.state, [
{ hits, queryID: 'theQueryID' },
]);
widget.render({
results,
state: helper.state,
helper,
createURL: () => '#',
});

expect(rendering).toHaveBeenNthCalledWith(
2,
expect.objectContaining({
hits: [
{
name: 'name 1',
__queryID: 'theQueryID',
},
{
name: 'name 2',
__queryID: 'theQueryID',
},
],
}),
false
);
});

it('does not render the same page twice', () => {
const rendering = jest.fn();
const makeWidget = connectInfiniteHits(rendering);
Expand Down
10 changes: 10 additions & 0 deletions src/connectors/infinite-hits/connectInfiniteHits.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import escapeHits, { TAG_PLACEHOLDER } from '../../lib/escape-highlight';
import {
checkRendering,
createDocumentationMessageGenerator,
addAbsolutePosition,
addQueryID,
} from '../../lib/utils';

const withUsage = createDocumentationMessageGenerator({
Expand Down Expand Up @@ -105,6 +107,14 @@ export default function connectInfiniteHits(renderFn, unmountFn) {
results.hits = escapeHits(results.hits);
}

results.hits = addAbsolutePosition(
results.hits,
results.page,
results.hitsPerPage
);

results.hits = addQueryID(results.hits, results.queryID);

results.hits = transformItems(results.hits);

if (lastReceivedPage < state.page) {
Expand Down
167 changes: 167 additions & 0 deletions src/helpers/__tests__/insights-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import insights, {
writeDataAttributes,
readDataAttributes,
hasDataAttributes,
} from '../insights';

const makeDomElement = (html: string): HTMLElement => {
const div = document.createElement('div');
div.innerHTML = html;
return (div.firstElementChild as HTMLElement) || div;
};

describe('insights', () => {
test('default behaviour', () => {
expect(
insights('clickedObjectIDsAfterSearch', {
objectIDs: ['3'],
eventName: 'Add to Cart',
})
).toMatchInlineSnapshot(
`"data-insights-method=\\"clickedObjectIDsAfterSearch\\" data-insights-payload=\\"eyJvYmplY3RJRHMiOlsiMyJdLCJldmVudE5hbWUiOiJBZGQgdG8gQ2FydCJ9\\""`
);
});
});

describe('writeDataAttributes', () => {
it('should output a string containing data-insights-* attributes', () => {
expect(
writeDataAttributes({
method: 'clickedObjectIDsAfterSearch',
payload: {
objectIDs: ['3'],
eventName: 'Add to Cart',
},
})
).toMatchInlineSnapshot(
`"data-insights-method=\\"clickedObjectIDsAfterSearch\\" data-insights-payload=\\"eyJvYmplY3RJRHMiOlsiMyJdLCJldmVudE5hbWUiOiJBZGQgdG8gQ2FydCJ9\\""`
);
});
it('should reject undefined payloads', () => {
expect(() =>
// @ts-ignore
writeDataAttributes({
method: 'clickedObjectIDsAfterSearch',
})
).toThrowErrorMatchingInlineSnapshot(
`"The insights helper expects the payload to be an object."`
);
});
it('should reject non object payloads', () => {
expect(() =>
writeDataAttributes({
method: 'clickedObjectIDsAfterSearch',
// @ts-ignore
payload: 2,
})
).toThrowErrorMatchingInlineSnapshot(
`"The insights helper expects the payload to be an object."`
);
});
it('should reject non JSON serializable payloads', () => {
const circularObject: any = { a: {} };
circularObject.a.circle = circularObject;
expect(() =>
writeDataAttributes({
method: 'clickedObjectIDsAfterSearch',
payload: circularObject,
})
).toThrowErrorMatchingInlineSnapshot(
`"Could not JSON serialize the payload object."`
);
});
});

describe('hasDataAttributes', () => {
it('should return true when there is a data-insights-method attribute', () => {
const domElement = makeDomElement(
tkrugg marked this conversation as resolved.
Show resolved Hide resolved
`<button
data-insights-method="clickedObjectIDsAfterSearch"
data-insights-payload='{"objectIDs":["3"],"eventName":"Add to Cart"}'
> Add to Cart </button>`
);

expect(hasDataAttributes(domElement)).toBe(true);
});
it("should return false when there isn't a data-insights-method attribute", () => {
const domElement = makeDomElement(
`<button
data-insights-payload='{"objectIDs":["3"],"eventName":"Add to Cart"}'
> Add to Cart </button>`
);

expect(hasDataAttributes(domElement)).toBe(false);
});
});

describe('readDataAttributes', () => {
describe('on handwritten data-insights-* attributes', () => {
let domElement: HTMLElement;

beforeEach(() => {
const payload = btoa(
JSON.stringify({ objectIDs: ['3'], eventName: 'Add to Cart' })
);
domElement = makeDomElement(
`<button
data-insights-method="clickedObjectIDsAfterSearch"
data-insights-payload="${payload}"
> Add to Cart </button>`
);
});

it('should extract the method name', () => {
expect(readDataAttributes(domElement).method).toEqual(
'clickedObjectIDsAfterSearch'
);
});

it('should extract the payload and parse it as a json object', () => {
expect(readDataAttributes(domElement).payload).toEqual({
objectIDs: ['3'],
eventName: 'Add to Cart',
});
});

it('should reject invalid payload', () => {
domElement = makeDomElement(
`<button
data-insights-method="clickedObjectIDsAfterSearch"
data-insights-payload='xxx'
> Add to Cart </button>`
);
expect(() =>
readDataAttributes(domElement)
).toThrowErrorMatchingInlineSnapshot(
`"The insights helper was unable to parse \`data-insights-payload\`."`
);
});
});

describe('on data-insights-* attributes generated with insights helper', () => {
let domElement: HTMLElement;

beforeEach(() => {
domElement = makeDomElement(
`<button
${insights('clickedObjectIDsAfterSearch', {
objectIDs: ['3'],
eventName: 'Add to Cart',
})}> Add to Cart </button>`
);
});

it('should extract the method name', () => {
expect(readDataAttributes(domElement).method).toEqual(
'clickedObjectIDsAfterSearch'
);
});

it('should extract the payload and parse it as a json object', () => {
expect(readDataAttributes(domElement).payload).toEqual({
objectIDs: ['3'],
eventName: 'Add to Cart',
});
});
});
});
Loading