diff --git a/packages/autocomplete-plugin-redirect-url/src/__tests__/createRedirectUrlPlugin.test.ts b/packages/autocomplete-plugin-redirect-url/src/__tests__/createRedirectUrlPlugin.test.ts index b13b55ad5..f3adc722b 100644 --- a/packages/autocomplete-plugin-redirect-url/src/__tests__/createRedirectUrlPlugin.test.ts +++ b/packages/autocomplete-plugin-redirect-url/src/__tests__/createRedirectUrlPlugin.test.ts @@ -42,7 +42,7 @@ function createMockSource({ }, templates: { item({ item, html }) { - return html`${item.query}`; + return html`${item.name}`; }, }, ...props, @@ -122,73 +122,12 @@ describe('createRedirectUrlPlugin', () => { await waitFor(() => { expect(findHitsSection(panelContainer)).not.toBeInTheDocument(); - expect(findDropdownOptions(findRedirectSection(panelContainer))) - .toMatchInlineSnapshot(` - Array [ - HTMLCollection [ -
-
-
- - - -
- -
-
-
- - - - -
-
-
, - ], - ] - `); + const dropdownOptions = findDropdownOptions( + findRedirectSection(panelContainer) + ); + + expect(dropdownOptions).toHaveLength(1); + expect(dropdownOptions[0].item(0)).toHaveTextContent(REDIRECT_QUERY); }); }); @@ -222,18 +161,25 @@ describe('createRedirectUrlPlugin', () => { await waitFor(() => { expect(findHitsSection(panelContainer)).not.toBeInTheDocument(); - const dropdownText = findDropdownOptions( + const dropdownOptions = findDropdownOptions( findRedirectSection(panelContainer) - )[0].item(0)?.textContent; + ); - expect(dropdownText).toBe('My custom option: redirect item'); + expect(dropdownOptions).toHaveLength(1); + expect(dropdownOptions[0].item(0)).toHaveTextContent( + 'My custom option: redirect item' + ); }); }); - test('renders a redirect item when a custom expected payload is returned', async () => { + test('does not render a redirect item when the query from the state does not match the response', async () => { const redirectUrlPlugin = createRedirectUrlPlugin({ transformResponse(response) { - return (response as Record).customRedirect?.url; + return (response as Record).renderingContent?.redirect + ?.url; + }, + transformResponseToQuery() { + return 'different query'; }, }); @@ -266,7 +212,7 @@ describe('createRedirectUrlPlugin', () => { await waitFor(() => { expect(findHitsSection(panelContainer)).not.toBeInTheDocument(); - expect(findRedirectSection(panelContainer)).toBeInTheDocument(); + expect(findRedirectSection(panelContainer)).not.toBeInTheDocument(); }); }); @@ -284,7 +230,7 @@ describe('createRedirectUrlPlugin', () => { panelContainer, plugins: [redirectUrlPlugin], getSources() { - return [createMockSource({ results: [{ hits: [{ query }] }] })]; + return [createMockSource({ results: [{ hits: [{ name: query }] }] })]; }, }); @@ -293,16 +239,14 @@ describe('createRedirectUrlPlugin', () => { fireEvent.input(input, { target: { value: query } }); await waitFor(() => { - expect(findDropdownOptions(findHitsSection(panelContainer))) - .toMatchInlineSnapshot(` - Array [ - HTMLCollection [ - - not a redirect item - , - ], - ] - `); + const dropdownOptions = findDropdownOptions( + findHitsSection(panelContainer) + ); + + expect(dropdownOptions).toHaveLength(1); + expect(dropdownOptions[0].item(0)).toHaveTextContent( + 'not a redirect item' + ); expect(findRedirectSection(panelContainer)).not.toBeInTheDocument(); }); @@ -327,9 +271,9 @@ describe('createRedirectUrlPlugin', () => { { ...RESPONSE, hits: [ - { query: 'redirect item' }, - { query: 'not a redirect item 1' }, - { query: 'not a redirect item 2' }, + { name: 'redirect item' }, + { name: 'not a redirect item 1' }, + { name: 'not a redirect item 2' }, ], }, ], @@ -345,26 +289,18 @@ describe('createRedirectUrlPlugin', () => { await waitFor(() => { expect(findRedirectSection(panelContainer)).toBeInTheDocument(); - expect(findDropdownOptions(findHitsSection(panelContainer))) - .toMatchInlineSnapshot(` - Array [ - HTMLCollection [ - - redirect item - , - ], - HTMLCollection [ - - not a redirect item 1 - , - ], - HTMLCollection [ - - not a redirect item 2 - , - ], - ] - `); + const dropdownOptions = findDropdownOptions( + findHitsSection(panelContainer) + ); + + expect(dropdownOptions).toHaveLength(3); + expect(dropdownOptions[0].item(0)).toHaveTextContent(REDIRECT_QUERY); + expect(dropdownOptions[1].item(0)).toHaveTextContent( + 'not a redirect item 1' + ); + expect(dropdownOptions[2].item(0)).toHaveTextContent( + 'not a redirect item 2' + ); }); }); @@ -387,14 +323,14 @@ describe('createRedirectUrlPlugin', () => { { ...RESPONSE, hits: [ - { query: 'redirect item' }, - { query: 'not a redirect item 1' }, - { query: 'not a redirect item 2' }, + { name: REDIRECT_QUERY }, + { name: 'not a redirect item 1' }, + { name: 'not a redirect item 2' }, ], }, ], getItemInputValue({ item }) { - return item.query; + return item.name; }, }), ]; @@ -408,21 +344,16 @@ describe('createRedirectUrlPlugin', () => { await waitFor(() => { expect(findRedirectSection(panelContainer)).toBeInTheDocument(); - expect(findDropdownOptions(findHitsSection(panelContainer))) - .toMatchInlineSnapshot(` - Array [ - HTMLCollection [ - - not a redirect item 1 - , - ], - HTMLCollection [ - - not a redirect item 2 - , - ], - ] - `); + const dropdownOptions = findDropdownOptions( + findHitsSection(panelContainer) + ); + expect(dropdownOptions).toHaveLength(2); + expect(dropdownOptions[0].item(0)).toHaveTextContent( + 'not a redirect item 1' + ); + expect(dropdownOptions[1].item(0)).toHaveTextContent( + 'not a redirect item 2' + ); }); }); @@ -486,15 +417,18 @@ describe('createRedirectUrlPlugin', () => { fireEvent.input(input, { target: { value: REDIRECT_QUERY } }); await waitFor(() => { - expect( - findDropdownOptions(findRedirectSection(panelContainer))[0][0] - ).toHaveTextContent(REDIRECT_QUERY); + const dropdownOptions = findDropdownOptions( + findRedirectSection(panelContainer) + ); + + expect(dropdownOptions).toHaveLength(1); + expect(dropdownOptions[0].item(0)).toHaveTextContent(REDIRECT_QUERY); }); fireEvent.submit(input); await waitFor(() => { - expect(input.value).toBe(REDIRECT_QUERY); + expect(input).toHaveValue(REDIRECT_QUERY); expect(navigator.navigate).toHaveBeenCalledTimes(1); }); }); @@ -520,7 +454,8 @@ describe('createRedirectUrlPlugin', () => { query === REDIRECT_QUERY ? [ { - hits: [{ query: REDIRECT_QUERY }], + hits: [{ name: REDIRECT_QUERY }], + query: REDIRECT_QUERY, renderingContent: { redirect: { url: 'https://www.algolia.com', @@ -531,13 +466,14 @@ describe('createRedirectUrlPlugin', () => { : [ { hits: [ - { query: 'something else' }, - { query: REDIRECT_QUERY }, + { name: 'something else' }, + { name: REDIRECT_QUERY }, ], + query: 'something else', }, ], getItemInputValue({ item }) { - return item.query; + return item.name; }, }), ]; @@ -550,95 +486,25 @@ describe('createRedirectUrlPlugin', () => { await waitFor(() => { expect(findRedirectSection(panelContainer)).not.toBeInTheDocument(); - expect(findDropdownOptions(findHitsSection(panelContainer))) - .toMatchInlineSnapshot(` - Array [ - HTMLCollection [ - - something else - , - ], - HTMLCollection [ - - redirect item - , - ], - ] - `); + const dropdownOptions = findDropdownOptions( + findHitsSection(panelContainer) + ); + expect(dropdownOptions).toHaveLength(2); + expect(dropdownOptions[0].item(0)).toHaveTextContent('something else'); + expect(dropdownOptions[1].item(0)).toHaveTextContent(REDIRECT_QUERY); }); fireEvent.click(findDropdownOptions(panelContainer)[1][0]); await waitFor(() => { - expect(input.value).toBe(REDIRECT_QUERY); + expect(input).toHaveValue(REDIRECT_QUERY); expect(findHitsSection(panelContainer)).not.toBeInTheDocument(); - expect(findDropdownOptions(findRedirectSection(panelContainer))) - .toMatchInlineSnapshot(` - Array [ - HTMLCollection [ -
-
-
- - - -
- -
-
-
- - - - -
-
-
, - ], - ] - `); + const dropdownOptions = findDropdownOptions( + findRedirectSection(panelContainer) + ); + + expect(dropdownOptions).toHaveLength(1); + expect(dropdownOptions[0].item(0)).toHaveTextContent(REDIRECT_QUERY); }); fireEvent.submit(input); @@ -728,7 +594,7 @@ describe('createRedirectUrlPlugin', () => { fireEvent.submit(input); await waitFor(() => { - expect(input.value).toBe(REDIRECT_QUERY); + expect(input).toHaveValue(REDIRECT_QUERY); expect(navigator.navigate).toHaveBeenCalledWith( expect.objectContaining({ item: { @@ -802,7 +668,7 @@ describe('createRedirectUrlPlugin', () => { fireEvent.submit(input); await waitFor(() => { - expect(input.value).toBe(REDIRECT_QUERY); + expect(input).toHaveValue(REDIRECT_QUERY); expect(navigator.navigate).toHaveBeenCalledWith( expect.objectContaining({ item: { diff --git a/packages/autocomplete-plugin-redirect-url/src/createRedirectUrlPlugin.ts b/packages/autocomplete-plugin-redirect-url/src/createRedirectUrlPlugin.ts index 22adff66d..6804f540a 100644 --- a/packages/autocomplete-plugin-redirect-url/src/createRedirectUrlPlugin.ts +++ b/packages/autocomplete-plugin-redirect-url/src/createRedirectUrlPlugin.ts @@ -21,6 +21,12 @@ function defaultTransformResponse( return (response as Record).renderingContent?.redirect?.url; } +function defaultTransformResponseToQuery( + response: TransformResponseParams +): string | undefined { + return (response as Record).query; +} + function defaultOnRedirect( redirects: RedirectUrlItem[], { event, navigator, state }: OnRedirectOptions @@ -46,6 +52,7 @@ function getOptions( ) { return { transformResponse: defaultTransformResponse, + transformResponseToQuery: defaultTransformResponseToQuery, templates: defaultTemplates, onRedirect: defaultOnRedirect, ...options, @@ -61,7 +68,8 @@ function getRedirectData({ state }) { export function createRedirectUrlPlugin( options: CreateRedirectUrlPluginParams = {} ): AutocompletePlugin { - const { transformResponse, templates, onRedirect } = getOptions(options); + const { transformResponse, transformResponseToQuery, templates, onRedirect } = + getOptions(options); function createRedirects({ results, source, state }): RedirectUrlItem[] { const redirect: RedirectUrlItem = { @@ -94,6 +102,14 @@ export function createRedirectUrlPlugin( name: 'aa.redirectUrlPlugin', subscribe({ onResolve, onSelect, setContext, setIsOpen }) { onResolve(({ results, source, state }) => { + // Ensure the resolved response matches the input query text before processing redirects. + const matchesCurrentQuery = (results as any).some( + (result) => transformResponseToQuery(result) === state.query + ); + if (!matchesCurrentQuery) { + return; + } + setContext({ ...state.context, redirectUrlPlugin: { diff --git a/packages/autocomplete-plugin-redirect-url/src/types/Redirect.ts b/packages/autocomplete-plugin-redirect-url/src/types/Redirect.ts index 1f90e50f3..7ab60b298 100644 --- a/packages/autocomplete-plugin-redirect-url/src/types/Redirect.ts +++ b/packages/autocomplete-plugin-redirect-url/src/types/Redirect.ts @@ -29,13 +29,44 @@ export type TransformResponseParams = | SearchForFacetValuesResponse; export type CreateRedirectUrlPluginParams = { + /** + * Maps the response to the redirect url that will be used by the plugin. + * + * Already supports Algolia results by default. + */ transformResponse?( response: TransformResponseParams ): string | undefined; + /** + * Maps the query used in the request to match with the redirect. + * Without this mapping, there is a risk of the race condition when processing + * redirect items where the last-resolved response can reflect an earlier query value. + * (ex: typing "shoes" quickly risks mismatching the redirect item with "sho" instead) + * + * Already supports Algolia results by default. + */ + transformResponseToQuery?( + response: TransformResponseParams + ): string | undefined; + /** + * Handles the navigation logic once a redirect is triggered. + * + * Already supports Algolia results by default. + */ onRedirect?( redirects: RedirectUrlItem[], options: OnRedirectOptions ): void; + /** + * The template used to render injected redirect dropdown items. + */ templates?: SourceTemplates; + /** + * Waits for all pending requests to complete before handling a form submission. + * (ex: pressing the "enter" key in the input) + * + * A boolean return value will wait for all pending requests to resolve. + * A number value will achieve the above with a timeout (in ms) to exit if it takes too long. + */ awaitSubmit?: () => boolean | number; };