Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions bundlesize.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
"files": [
{
"path": "packages/autocomplete-core/dist/umd/index.production.js",
"maxSize": "9 kB"
"maxSize": "9.5 kB"
},
{
"path": "packages/autocomplete-js/dist/umd/index.production.js",
"maxSize": "21.25 kB"
"maxSize": "21.75 kB"
},
{
"path": "packages/autocomplete-preset-algolia/dist/umd/index.production.js",
Expand Down
35 changes: 34 additions & 1 deletion packages/autocomplete-core/src/__tests__/getFormProps.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { createPlayground } from '../../../../test/utils';
import { createAlgoliaInsightsPlugin } from '@algolia/autocomplete-plugin-algolia-insights';
import { createRedirectUrlPlugin } from '@algolia/autocomplete-plugin-redirect-url';

import { createPlayground, runAllMicroTasks } from '../../../../test/utils';
import { createAutocomplete } from '../createAutocomplete';

describe('getFormProps', () => {
Expand Down Expand Up @@ -176,6 +179,36 @@ describe('getFormProps', () => {
})
);
});

describe.each([true, 1000])(
'a plugin is configured with the option "awaitSubmit: () => %s"',
(timeout) => {
test('should await pending requests before triggering the submit event', async () => {
const plugins = [
createRedirectUrlPlugin({ awaitSubmit: () => timeout }),
createAlgoliaInsightsPlugin({}), // "awaitSubmit" is neither configurable nor defined
];
const onSubmit = jest.fn();
const { getFormProps, inputElement } = createPlayground(
createAutocomplete,
{
onSubmit,
plugins,
}
);

const formProps = getFormProps({ inputElement });

formProps.onSubmit(new Event('submit'));

expect(onSubmit).toHaveBeenCalledTimes(0);

await runAllMicroTasks();

expect(onSubmit).toHaveBeenCalledTimes(1);
});
}
);
});

describe('onReset', () => {
Expand Down
65 changes: 65 additions & 0 deletions packages/autocomplete-core/src/__tests__/getInputProps.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { createAlgoliaInsightsPlugin } from '@algolia/autocomplete-plugin-algolia-insights';
import { createRedirectUrlPlugin } from '@algolia/autocomplete-plugin-redirect-url';
import { fireEvent, waitFor } from '@testing-library/dom';
import userEvent from '@testing-library/user-event';

Expand All @@ -9,6 +11,20 @@ import {
runAllMicroTasks,
} from '../../../../test/utils';
import { createAutocomplete } from '../createAutocomplete';
import { createCancelablePromiseList, getPluginSubmitPromise } from '../utils';

jest.mock('../utils/createCancelablePromiseList', () => ({
createCancelablePromiseList: jest.fn(
jest.requireActual('../utils/createCancelablePromiseList')
.createCancelablePromiseList
),
}));

jest.mock('../utils/getPluginSubmitPromise', () => ({
getPluginSubmitPromise: jest.fn(
jest.requireActual('../utils/getPluginSubmitPromise').getPluginSubmitPromise
),
}));

describe('getInputProps', () => {
beforeEach(() => {
Expand Down Expand Up @@ -1287,6 +1303,55 @@ describe('getInputProps', () => {
);
});

describe('a plugin is configured with the option "awaitSubmit"', () => {
const cancelAll = jest.fn();
const event = { ...new KeyboardEvent('keydown'), key: 'Enter' };

beforeEach(() => {
cancelAll.mockClear();
(createCancelablePromiseList as jest.Mock).mockReturnValueOnce({
add: jest.fn,
cancelAll,
isEmpty: jest.fn,
wait: jest.fn,
});
});

test.each([true, 1000])(
'when returning %s it should not cancel pending requests',
(timeout) => {
(getPluginSubmitPromise as jest.Mock).mockResolvedValueOnce({});

const plugins = [
createRedirectUrlPlugin({ awaitSubmit: () => timeout }),
createAlgoliaInsightsPlugin({}), // "awaitSubmit" is neither configurable nor defined
];

const { inputProps } = createPlayground(createAutocomplete, {
plugins,
});

inputProps.onKeyDown(event);

expect(cancelAll).toHaveBeenCalledTimes(0);
}
);

test('when returning false it should cancel pending requests', () => {
const plugins = [
createRedirectUrlPlugin({ awaitSubmit: () => false }),
];

const { inputProps } = createPlayground(createAutocomplete, {
plugins,
});

inputProps.onKeyDown(event);

expect(cancelAll).toHaveBeenCalledTimes(1);
});
});

describe('Plain Enter', () => {
test('calls onSelect with item URL', () => {
const onSelect = jest.fn();
Expand Down
31 changes: 22 additions & 9 deletions packages/autocomplete-core/src/getPropGetters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
InternalAutocompleteOptions,
} from './types';
import {
getPluginSubmitPromise,
getActiveItem,
getAutocompleteElementId,
isOrContainsNode,
Expand Down Expand Up @@ -126,22 +127,34 @@ export function getPropGetters<
const getFormProps: GetFormProps<TEvent> = (providedProps) => {
const { inputElement, ...rest } = providedProps;

const handleSubmit = (event: TEvent) => {
props.onSubmit({
event,
refresh,
state: store.getState(),
...setters,
});

store.dispatch('submit', null);
providedProps.inputElement?.blur();
};

return {
action: '',
noValidate: true,
role: 'search',
onSubmit: (event) => {
(event as unknown as Event).preventDefault();

props.onSubmit({
event,
refresh,
state: store.getState(),
...setters,
});

store.dispatch('submit', null);
providedProps.inputElement?.blur();
const waitForSubmit = getPluginSubmitPromise(
props.plugins,
store.pendingRequests
);
if (waitForSubmit !== undefined) {
waitForSubmit.then(() => handleSubmit(event));
} else {
handleSubmit(event);
}
},
onReset: (event) => {
(event as unknown as Event).preventDefault();
Expand Down
22 changes: 16 additions & 6 deletions packages/autocomplete-core/src/onKeyDown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@ import {
BaseItem,
InternalAutocompleteOptions,
} from './types';
import { getActiveItem, getAutocompleteElementId } from './utils';
import {
getPluginSubmitPromise,
getActiveItem,
getAutocompleteElementId,
} from './utils';

interface OnKeyDownOptions<TItem extends BaseItem>
extends AutocompleteScopeApi<TItem> {
Expand Down Expand Up @@ -128,11 +132,17 @@ export function onKeyDown<TItem extends BaseItem>({
.getState()
.collections.every((collection) => collection.items.length === 0)
) {
// If requests are still pending when the panel closes, they could reopen
// the panel once they resolve.
// We want to prevent any subsequent query from reopening the panel
// because it would result in an unsolicited UI behavior.
if (!props.debug) {
const waitForSubmit = getPluginSubmitPromise(
props.plugins,
store.pendingRequests
);
if (waitForSubmit !== undefined) {
waitForSubmit.then(store.pendingRequests.cancelAll); // Cancel the rest if timeout number is provided
} else if (!props.debug) {
// If requests are still pending when the panel closes, they could reopen
// the panel once they resolve.
// We want to prevent any subsequent query from reopening the panel
// because it would result in an unsolicited UI behavior.
store.pendingRequests.cancelAll();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,35 @@ describe('createCancelablePromiseList', () => {
expect(cancelablePromise3.isCanceled()).toBe(true);
expect(cancelablePromiseList.isEmpty()).toBe(true);
});

test('waits for all promises to resolve', async () => {
const cancelablePromiseList = createCancelablePromiseList();
const cancelablePromise = createCancelablePromise.resolve();

cancelablePromiseList.add(cancelablePromise);
cancelablePromiseList.add(cancelablePromise);

expect(cancelablePromiseList.isEmpty()).toBe(false);

await cancelablePromiseList.wait();

expect(cancelablePromiseList.isEmpty()).toBe(true);
});

test('waits for a timeout before all promises to resolve', async () => {
const timeout = 50;
const cancelablePromiseList = createCancelablePromiseList();
const delayedPromise = createCancelablePromise(
(resolve) => setTimeout(resolve, timeout * 10) // ensure wait will be later than timeout
);

cancelablePromiseList.add(delayedPromise);

expect(cancelablePromiseList.isEmpty()).toBe(false);

await cancelablePromiseList.wait(timeout);

// List is not emptied yet because the timeout triggered first
expect(cancelablePromiseList.isEmpty()).toBe(false);
});
});
Loading