Skip to content

Commit bf03241

Browse files
authored
feat(redirect): await submit for pending requests (#1309)
* feat(redirect): await pending requests on submit * fix: remove duplicate code * chore: refactor promise syntax * test: add tests for awaitSubmit * test: awaitSubmit with onKeyDown * fix: lint import errors * test: update snapshot * fix(redirect): move property to root plugin level * Revert "fix(redirect): move property to root plugin level" This reverts commit 7fe53c1. * chore: rename function * feat(wip): support plugin submit promise timeout * chore: rename to awaitSubmit * test: temporarily skip tests * fix: reuse wait promise if already exists * test: fix troublesome snapshot test * chore: bump bundlesize values * test: unskip and add unit tests * chore: update the example to use default args * test: fix mock * chore: shrink bundlesize config number
1 parent 47e84f6 commit bf03241

12 files changed

+398
-30
lines changed

bundlesize.config.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22
"files": [
33
{
44
"path": "packages/autocomplete-core/dist/umd/index.production.js",
5-
"maxSize": "9 kB"
5+
"maxSize": "9.5 kB"
66
},
77
{
88
"path": "packages/autocomplete-js/dist/umd/index.production.js",
9-
"maxSize": "21.25 kB"
9+
"maxSize": "21.75 kB"
1010
},
1111
{
1212
"path": "packages/autocomplete-preset-algolia/dist/umd/index.production.js",

packages/autocomplete-core/src/__tests__/getFormProps.test.ts

+34-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
import { createPlayground } from '../../../../test/utils';
1+
import { createAlgoliaInsightsPlugin } from '@algolia/autocomplete-plugin-algolia-insights';
2+
import { createRedirectUrlPlugin } from '@algolia/autocomplete-plugin-redirect-url';
3+
4+
import { createPlayground, runAllMicroTasks } from '../../../../test/utils';
25
import { createAutocomplete } from '../createAutocomplete';
36

47
describe('getFormProps', () => {
@@ -176,6 +179,36 @@ describe('getFormProps', () => {
176179
})
177180
);
178181
});
182+
183+
describe.each([true, 1000])(
184+
'a plugin is configured with the option "awaitSubmit: () => %s"',
185+
(timeout) => {
186+
test('should await pending requests before triggering the submit event', async () => {
187+
const plugins = [
188+
createRedirectUrlPlugin({ awaitSubmit: () => timeout }),
189+
createAlgoliaInsightsPlugin({}), // "awaitSubmit" is neither configurable nor defined
190+
];
191+
const onSubmit = jest.fn();
192+
const { getFormProps, inputElement } = createPlayground(
193+
createAutocomplete,
194+
{
195+
onSubmit,
196+
plugins,
197+
}
198+
);
199+
200+
const formProps = getFormProps({ inputElement });
201+
202+
formProps.onSubmit(new Event('submit'));
203+
204+
expect(onSubmit).toHaveBeenCalledTimes(0);
205+
206+
await runAllMicroTasks();
207+
208+
expect(onSubmit).toHaveBeenCalledTimes(1);
209+
});
210+
}
211+
);
179212
});
180213

181214
describe('onReset', () => {

packages/autocomplete-core/src/__tests__/getInputProps.test.ts

+65
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { createAlgoliaInsightsPlugin } from '@algolia/autocomplete-plugin-algolia-insights';
2+
import { createRedirectUrlPlugin } from '@algolia/autocomplete-plugin-redirect-url';
13
import { fireEvent, waitFor } from '@testing-library/dom';
24
import userEvent from '@testing-library/user-event';
35

@@ -9,6 +11,20 @@ import {
911
runAllMicroTasks,
1012
} from '../../../../test/utils';
1113
import { createAutocomplete } from '../createAutocomplete';
14+
import { createCancelablePromiseList, getPluginSubmitPromise } from '../utils';
15+
16+
jest.mock('../utils/createCancelablePromiseList', () => ({
17+
createCancelablePromiseList: jest.fn(
18+
jest.requireActual('../utils/createCancelablePromiseList')
19+
.createCancelablePromiseList
20+
),
21+
}));
22+
23+
jest.mock('../utils/getPluginSubmitPromise', () => ({
24+
getPluginSubmitPromise: jest.fn(
25+
jest.requireActual('../utils/getPluginSubmitPromise').getPluginSubmitPromise
26+
),
27+
}));
1228

1329
describe('getInputProps', () => {
1430
beforeEach(() => {
@@ -1287,6 +1303,55 @@ describe('getInputProps', () => {
12871303
);
12881304
});
12891305

1306+
describe('a plugin is configured with the option "awaitSubmit"', () => {
1307+
const cancelAll = jest.fn();
1308+
const event = { ...new KeyboardEvent('keydown'), key: 'Enter' };
1309+
1310+
beforeEach(() => {
1311+
cancelAll.mockClear();
1312+
(createCancelablePromiseList as jest.Mock).mockReturnValueOnce({
1313+
add: jest.fn,
1314+
cancelAll,
1315+
isEmpty: jest.fn,
1316+
wait: jest.fn,
1317+
});
1318+
});
1319+
1320+
test.each([true, 1000])(
1321+
'when returning %s it should not cancel pending requests',
1322+
(timeout) => {
1323+
(getPluginSubmitPromise as jest.Mock).mockResolvedValueOnce({});
1324+
1325+
const plugins = [
1326+
createRedirectUrlPlugin({ awaitSubmit: () => timeout }),
1327+
createAlgoliaInsightsPlugin({}), // "awaitSubmit" is neither configurable nor defined
1328+
];
1329+
1330+
const { inputProps } = createPlayground(createAutocomplete, {
1331+
plugins,
1332+
});
1333+
1334+
inputProps.onKeyDown(event);
1335+
1336+
expect(cancelAll).toHaveBeenCalledTimes(0);
1337+
}
1338+
);
1339+
1340+
test('when returning false it should cancel pending requests', () => {
1341+
const plugins = [
1342+
createRedirectUrlPlugin({ awaitSubmit: () => false }),
1343+
];
1344+
1345+
const { inputProps } = createPlayground(createAutocomplete, {
1346+
plugins,
1347+
});
1348+
1349+
inputProps.onKeyDown(event);
1350+
1351+
expect(cancelAll).toHaveBeenCalledTimes(1);
1352+
});
1353+
});
1354+
12901355
describe('Plain Enter', () => {
12911356
test('calls onSelect with item URL', () => {
12921357
const onSelect = jest.fn();

packages/autocomplete-core/src/getPropGetters.ts

+22-9
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
InternalAutocompleteOptions,
1818
} from './types';
1919
import {
20+
getPluginSubmitPromise,
2021
getActiveItem,
2122
getAutocompleteElementId,
2223
isOrContainsNode,
@@ -126,22 +127,34 @@ export function getPropGetters<
126127
const getFormProps: GetFormProps<TEvent> = (providedProps) => {
127128
const { inputElement, ...rest } = providedProps;
128129

130+
const handleSubmit = (event: TEvent) => {
131+
props.onSubmit({
132+
event,
133+
refresh,
134+
state: store.getState(),
135+
...setters,
136+
});
137+
138+
store.dispatch('submit', null);
139+
providedProps.inputElement?.blur();
140+
};
141+
129142
return {
130143
action: '',
131144
noValidate: true,
132145
role: 'search',
133146
onSubmit: (event) => {
134147
(event as unknown as Event).preventDefault();
135148

136-
props.onSubmit({
137-
event,
138-
refresh,
139-
state: store.getState(),
140-
...setters,
141-
});
142-
143-
store.dispatch('submit', null);
144-
providedProps.inputElement?.blur();
149+
const waitForSubmit = getPluginSubmitPromise(
150+
props.plugins,
151+
store.pendingRequests
152+
);
153+
if (waitForSubmit !== undefined) {
154+
waitForSubmit.then(() => handleSubmit(event));
155+
} else {
156+
handleSubmit(event);
157+
}
145158
},
146159
onReset: (event) => {
147160
(event as unknown as Event).preventDefault();

packages/autocomplete-core/src/onKeyDown.ts

+16-6
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@ import {
66
BaseItem,
77
InternalAutocompleteOptions,
88
} from './types';
9-
import { getActiveItem, getAutocompleteElementId } from './utils';
9+
import {
10+
getPluginSubmitPromise,
11+
getActiveItem,
12+
getAutocompleteElementId,
13+
} from './utils';
1014

1115
interface OnKeyDownOptions<TItem extends BaseItem>
1216
extends AutocompleteScopeApi<TItem> {
@@ -128,11 +132,17 @@ export function onKeyDown<TItem extends BaseItem>({
128132
.getState()
129133
.collections.every((collection) => collection.items.length === 0)
130134
) {
131-
// If requests are still pending when the panel closes, they could reopen
132-
// the panel once they resolve.
133-
// We want to prevent any subsequent query from reopening the panel
134-
// because it would result in an unsolicited UI behavior.
135-
if (!props.debug) {
135+
const waitForSubmit = getPluginSubmitPromise(
136+
props.plugins,
137+
store.pendingRequests
138+
);
139+
if (waitForSubmit !== undefined) {
140+
waitForSubmit.then(store.pendingRequests.cancelAll); // Cancel the rest if timeout number is provided
141+
} else if (!props.debug) {
142+
// If requests are still pending when the panel closes, they could reopen
143+
// the panel once they resolve.
144+
// We want to prevent any subsequent query from reopening the panel
145+
// because it would result in an unsolicited UI behavior.
136146
store.pendingRequests.cancelAll();
137147
}
138148

packages/autocomplete-core/src/utils/__tests__/createCancelablePromiseList.test.ts

+31
Original file line numberDiff line numberDiff line change
@@ -75,4 +75,35 @@ describe('createCancelablePromiseList', () => {
7575
expect(cancelablePromise3.isCanceled()).toBe(true);
7676
expect(cancelablePromiseList.isEmpty()).toBe(true);
7777
});
78+
79+
test('waits for all promises to resolve', async () => {
80+
const cancelablePromiseList = createCancelablePromiseList();
81+
const cancelablePromise = createCancelablePromise.resolve();
82+
83+
cancelablePromiseList.add(cancelablePromise);
84+
cancelablePromiseList.add(cancelablePromise);
85+
86+
expect(cancelablePromiseList.isEmpty()).toBe(false);
87+
88+
await cancelablePromiseList.wait();
89+
90+
expect(cancelablePromiseList.isEmpty()).toBe(true);
91+
});
92+
93+
test('waits for a timeout before all promises to resolve', async () => {
94+
const timeout = 50;
95+
const cancelablePromiseList = createCancelablePromiseList();
96+
const delayedPromise = createCancelablePromise(
97+
(resolve) => setTimeout(resolve, timeout * 10) // ensure wait will be later than timeout
98+
);
99+
100+
cancelablePromiseList.add(delayedPromise);
101+
102+
expect(cancelablePromiseList.isEmpty()).toBe(false);
103+
104+
await cancelablePromiseList.wait(timeout);
105+
106+
// List is not emptied yet because the timeout triggered first
107+
expect(cancelablePromiseList.isEmpty()).toBe(false);
108+
});
78109
});

0 commit comments

Comments
 (0)