Skip to content

Commit d7b360b

Browse files
shaejazHaroenvdhayab
authored
feat(instantsearch.js): introduce chat widget (#6705)
--------- Co-authored-by: Haroen Viaene <hello@haroen.me> Co-authored-by: Dhaya <154633+dhayab@users.noreply.github.com>
1 parent 1a0d437 commit d7b360b

61 files changed

Lines changed: 1241 additions & 38 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

bundlesize.config.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@
1010
},
1111
{
1212
"path": "./packages/instantsearch.js/dist/instantsearch.production.min.js",
13-
"maxSize": "84.75 kB"
13+
"maxSize": "85 kB"
1414
},
1515
{
1616
"path": "./packages/instantsearch.js/dist/instantsearch.development.js",
17-
"maxSize": "185 kB"
17+
"maxSize": "183.75 kB"
1818
},
1919
{
2020
"path": "packages/react-instantsearch-core/dist/umd/ReactInstantSearchCore.min.js",

packages/algolia-experiences/src/render.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/** @jsx h */
22
import { getPropertyByPath } from 'instantsearch.js/es/lib/utils';
33
import { carousel } from 'instantsearch.js/es/templates';
4-
import { index, panel } from 'instantsearch.js/es/widgets';
4+
import { index, panel } from 'instantsearch.js/es/widgets/index.umd';
55
import { h, Fragment } from 'preact';
66

77
import { banner } from './banner';

packages/algolia-experiences/src/widgets.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import {
2222
stats,
2323
toggleRefinement,
2424
trendingItems,
25-
} from 'instantsearch.js/es/widgets';
25+
} from 'instantsearch.js/es/widgets/index.umd';
2626

2727
export const widgets = {
2828
'ais.breadcrumb': breadcrumb,

packages/instantsearch-ui-components/src/components/chat/Chat.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ export function createChatComponent({ createElement, Fragment }: Renderer) {
9898
{...toggleButtonProps}
9999
onClick={() => {
100100
toggleButtonProps.onClick?.();
101-
promptRef.current?.focus();
101+
promptRef.current?.focus?.();
102102
}}
103103
/>
104104
</div>

packages/instantsearch.js/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"@types/google.maps": "^3.55.12",
3232
"@types/hogan.js": "^3.0.0",
3333
"@types/qs": "^6.5.3",
34+
"ai": "^5.0.18",
3435
"algoliasearch-helper": "3.26.0",
3536
"hogan.js": "^3.0.2",
3637
"htm": "^3.0.0",

packages/instantsearch.js/src/__tests__/common-connectors.test.tsx

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
connectFrequentlyBoughtTogether,
2020
connectTrendingItems,
2121
connectLookingSimilar,
22+
connectChat,
2223
} from '../connectors';
2324
import instantsearch from '../index.es';
2425
import { refinementList } from '../widgets';
@@ -530,6 +531,51 @@ const testSetups: TestSetupsMap<TestSuites> = {
530531

531532
search.start();
532533
},
534+
createChatConnectorTests({ instantSearchOptions, widgetParams }) {
535+
const customChat = connectChat<{
536+
container: HTMLElement;
537+
}>((renderOptions) => {
538+
const { input, setInput, open, setOpen } = renderOptions;
539+
renderOptions.widgetParams.container.innerHTML = `
540+
<div data-testid="Chat-root" style="display: ${
541+
open ? 'block' : 'none'
542+
}">
543+
<input data-testid="Chat-input" type="text" value="${input}" />
544+
<button data-testid="Chat-updateInput">update input</button>
545+
</div>
546+
<button data-testid="Chat-toggleButton">
547+
toggle chat
548+
</button>
549+
`;
550+
551+
renderOptions.widgetParams.container
552+
.querySelector('[data-testid="Chat-toggleButton"]')!
553+
.addEventListener('click', () => {
554+
setOpen(!open);
555+
});
556+
557+
renderOptions.widgetParams.container
558+
.querySelector('[data-testid="Chat-updateInput"]')!
559+
.addEventListener('click', () => {
560+
setInput('hello world');
561+
});
562+
});
563+
564+
instantsearch(instantSearchOptions)
565+
.addWidgets([
566+
customChat({
567+
container: document.body.appendChild(document.createElement('div')),
568+
...widgetParams,
569+
}),
570+
])
571+
.on('error', () => {
572+
/*
573+
* prevent rethrowing InstantSearch errors, so tests can be asserted.
574+
* IRL this isn't needed, as the error doesn't stop execution.
575+
*/
576+
})
577+
.start();
578+
},
533579
};
534580

535581
function addWidgetToggleUi(search: InstantSearch, widget: Widget) {
@@ -561,6 +607,7 @@ const testOptions: TestOptionsMap<TestSuites> = {
561607
createFrequentlyBoughtTogetherConnectorTests: undefined,
562608
createTrendingItemsConnectorTests: undefined,
563609
createLookingSimilarConnectorTests: undefined,
610+
createChatConnectorTests: undefined,
564611
};
565612

566613
describe('Common connector tests (InstantSearch.js)', () => {

packages/instantsearch.js/src/__tests__/common-widgets.test.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232
poweredBy,
3333
menuSelect,
3434
dynamicWidgets,
35+
chat,
3536
} from '../widgets';
3637

3738
import type { TestOptionsMap, TestSetupsMap } from '@instantsearch/tests';
@@ -617,6 +618,22 @@ const testSetups: TestSetupsMap<TestSuites> = {
617618
])
618619
.start();
619620
},
621+
createChatWidgetTests({ instantSearchOptions, widgetParams }) {
622+
instantsearch(instantSearchOptions)
623+
.addWidgets([
624+
chat({
625+
container: document.body.appendChild(document.createElement('div')),
626+
...widgetParams,
627+
}),
628+
])
629+
.on('error', () => {
630+
/*
631+
* prevent rethrowing InstantSearch errors, so tests can be asserted.
632+
* IRL this isn't needed, as the error doesn't stop execution.
633+
*/
634+
})
635+
.start();
636+
},
620637
};
621638

622639
const testOptions: TestOptionsMap<TestSuites> = {
@@ -649,6 +666,7 @@ const testOptions: TestOptionsMap<TestSuites> = {
649666
createPoweredByWidgetTests: undefined,
650667
createMenuSelectWidgetTests: undefined,
651668
createDynamicWidgetsWidgetTests: undefined,
669+
createChatWidgetTests: undefined,
652670
};
653671

654672
describe('Common widget tests (InstantSearch.js)', () => {
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import * as connectors from '..';
2+
import * as connectorsUmd from '../index.umd';
3+
4+
describe('connectors', () => {
5+
describe('umd', () => {
6+
test('has the same number of exports as the main entrypoint', () => {
7+
expect(Object.keys(connectorsUmd)).toEqual(Object.keys(connectors));
8+
});
9+
});
10+
});
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { createSearchClient } from '@instantsearch/mocks';
2+
import algoliasearchHelper from 'algoliasearch-helper';
3+
4+
import {
5+
createInitOptions,
6+
createRenderOptions,
7+
} from '../../../../test/createWidget';
8+
import connectChat from '../connectChat';
9+
10+
describe('connectChat', () => {
11+
it('throws without render function', () => {
12+
expect(() => {
13+
// @ts-expect-error
14+
connectChat()({});
15+
}).toThrowErrorMatchingInlineSnapshot(`
16+
"The render function is not valid (received type Undefined).
17+
18+
See documentation: https://www.algolia.com/doc/api-reference/widgets/chat/js/#connector"
19+
`);
20+
});
21+
22+
it('is a widget', () => {
23+
const render = jest.fn();
24+
const unmount = jest.fn();
25+
26+
const customChat = connectChat(render, unmount);
27+
const widget = customChat({});
28+
29+
expect(widget).toEqual(
30+
expect.objectContaining({
31+
$$type: 'ais.chat',
32+
init: expect.any(Function),
33+
render: expect.any(Function),
34+
dispose: expect.any(Function),
35+
})
36+
);
37+
});
38+
39+
it('Renders during init and render', () => {
40+
const renderFn = jest.fn();
41+
const makeWidget = connectChat(renderFn);
42+
const widget = makeWidget({ agentId: 'agentId' });
43+
44+
// test if widget is not rendered yet at this point
45+
expect(renderFn).toHaveBeenCalledTimes(0);
46+
47+
const helper = algoliasearchHelper(createSearchClient(), '', {});
48+
helper.search = jest.fn();
49+
50+
widget.init(createInitOptions({ helper, state: helper.state }));
51+
52+
expect(renderFn).toHaveBeenCalledTimes(1);
53+
expect(renderFn).toHaveBeenLastCalledWith(
54+
expect.objectContaining({ widgetParams: { agentId: 'agentId' } }),
55+
true
56+
);
57+
58+
const renderOptions = createRenderOptions({ helper });
59+
widget.render(renderOptions);
60+
61+
expect(renderFn).toHaveBeenCalledTimes(2);
62+
expect(renderFn).toHaveBeenLastCalledWith(
63+
expect.objectContaining({ widgetParams: { agentId: 'agentId' } }),
64+
false
65+
);
66+
});
67+
68+
describe('dispose', () => {
69+
it('calls the unmount function', () => {
70+
const unmountFn = jest.fn();
71+
const makeWidget = connectChat(() => {}, unmountFn);
72+
const widget = makeWidget({ agentId: 'agentId' });
73+
74+
expect(unmountFn).toHaveBeenCalledTimes(0);
75+
76+
widget.dispose();
77+
expect(unmountFn).toHaveBeenCalledTimes(1);
78+
});
79+
80+
it('does not throw without the unmount function', () => {
81+
const makeWidget = connectChat(() => {});
82+
const widget = makeWidget({ agentId: 'agentId' });
83+
84+
expect(() => widget.dispose()).not.toThrow();
85+
});
86+
});
87+
});

0 commit comments

Comments
 (0)