diff --git a/.wp-env.json b/.wp-env.json index 858e671e80..a38fb9d76c 100644 --- a/.wp-env.json +++ b/.wp-env.json @@ -46,7 +46,8 @@ "wp-content/plugins/unsupported-elasticsearch-version.php": "./tests/e2e/src/wordpress-files/test-plugins/unsupported-elasticsearch-version.php", "wp-content/plugins/multiple-required-features.php": "./tests/e2e/src/wordpress-files/test-plugins/multiple-required-features.php", "wp-content/uploads/content-example.xml": "./tests/e2e/src/wordpress-files/test-docs/content-example.xml", - "wp-content/plugins/echo-shortcode.php": "./tests/e2e/src/wordpress-files/test-plugins/echo-shortcode.php" + "wp-content/plugins/echo-shortcode.php": "./tests/e2e/src/wordpress-files/test-plugins/echo-shortcode.php", + "wp-content/plugins/custom-search-form.php": "./tests/e2e/src/wordpress-files/test-plugins/custom-search-form.php" } } } diff --git a/assets/js/api-search/src/reducer.js b/assets/js/api-search/src/reducer.js index b4250ac862..f17757044b 100644 --- a/assets/js/api-search/src/reducer.js +++ b/assets/js/api-search/src/reducer.js @@ -34,7 +34,7 @@ export default (state, action) => { newState.args = { ...newState.args, ...args, offset: 0 }; newState.isOn = true; - if (updateDefaults && args.post_type.length) { + if (updateDefaults && args.post_type?.length) { newState.argsSchema.post_type.default = args.post_type; } diff --git a/assets/js/instant-results/apps/modal.js b/assets/js/instant-results/apps/modal.js index 648b807bcd..f894591ed7 100644 --- a/assets/js/instant-results/apps/modal.js +++ b/assets/js/instant-results/apps/modal.js @@ -8,8 +8,8 @@ import { __ } from '@wordpress/i18n'; * Internal dependencies. */ import { useApiSearch } from '../../api-search'; -import { facets } from '../config'; -import { getPostTypesFromForm } from '../utilities'; +import { argsSchema, facets } from '../config'; +import { getArgsFromForm } from '../utilities'; import Modal from '../components/common/modal'; import Layout from '../components/layout'; @@ -59,11 +59,10 @@ export default () => { return; } - const { value } = inputRef.current; - const post_type = getPostTypesFromForm(inputRef.current.form); + const args = getArgsFromForm(inputRef.current.form, argsSchema); const updateDefaults = !facets.some((f) => f.name === 'post_type'); - search({ post_type, search: value, updateDefaults }); + search({ ...args, updateDefaults }); }, [inputRef, search], ); diff --git a/assets/js/instant-results/utilities.js b/assets/js/instant-results/utilities.js index af15c4b02f..37027cfed4 100644 --- a/assets/js/instant-results/utilities.js +++ b/assets/js/instant-results/utilities.js @@ -1,3 +1,13 @@ +/** + * WordPress dependencies. + */ +import { applyFilters } from '@wordpress/hooks'; + +/** + * Internal dependencies. + */ +import { sanitizeArg } from '../api-search/src/utilities'; + /** * Format a date. * @@ -27,21 +37,62 @@ export const formatPrice = (number, options) => { }; /** - * Get the post types from a search form. + * Get search args from a search form for Instant Results. * - * @param {HTMLFormElement} form Form element. - * @returns {Array} Post types. + * @param {HTMLFormElement} form Form element. + * @param {object} argsSchema Search args schema. + * @returns {object} Search args. */ -export const getPostTypesFromForm = (form) => { - const data = new FormData(form); +export const getArgsFromForm = (form, argsSchema) => { + /** + * Filter the map of query variable names to Instant Results arg names. + * + * @filter ep.instantResults.queryVarMap + * @since 5.4.0 + * + * @param {object} map Map of WordPress query var names to Instant Results arg names. + * @param {HTMLFormElement} form Form element. + * @param {object} argsSchema Search args schema. + * @returns {object} Map of WordPress query var names to Instant Results arg names. + */ + const QUERY_VAR_MAP = applyFilters( + 'ep.instantResults.queryVarMap', + { + s: 'search', + cat: 'tax-category', + tag_id: 'tax-post_tag', + }, + { form, argsSchema }, + ); + + const formData = new FormData(form); + const params = new URLSearchParams(); + const formEntries = Array.from(formData.entries()); - if (data.has('post_type')) { - return data.getAll('post_type').slice(-1); - } + formEntries.forEach(([key, value]) => { + // Strip trailing [] from array-style field names. + const cleanKey = key.replace(/\[\]$/, ''); + // Resolve the EP arg name: explicit map → direct schema match → tax- prefix. + const argName = + QUERY_VAR_MAP[cleanKey] || + (argsSchema[cleanKey] ? cleanKey : null) || + (argsSchema[`tax-${cleanKey}`] ? `tax-${cleanKey}` : null); + + if (value && argName) { + const existing = params.get(argName); + params.set(argName, existing ? `${existing},${value}` : value); + } + }); - if (data.has('post_type[]')) { - return data.getAll('post_type[]'); - } + return Object.entries(argsSchema).reduce((args, [arg, options]) => { + const param = params.get(arg); + if (param !== null) { + const value = sanitizeArg(param, options, false); + if (value !== null) { + args[arg] = value; + } + } - return []; + return args; + }, {}); }; diff --git a/tests/e2e/src/specs/instant-results.spec.ts b/tests/e2e/src/specs/instant-results.spec.ts index 6a40945cbc..d01c9e3a42 100644 --- a/tests/e2e/src/specs/instant-results.spec.ts +++ b/tests/e2e/src/specs/instant-results.spec.ts @@ -26,12 +26,20 @@ test.describe('Instant Results Feature', { tag: '@group1' }, () => { }); }; - const addInstantResultFilter = async (page: Page, filterName: string) => { + const addInstantResultFilter = async ( + page: Page, + filterName: string, + options?: { keepExisting?: boolean }, + ) => { await page.locator('.components-form-token-field__input').focus(); - await page.keyboard.press('Backspace'); - await page.keyboard.press('Backspace'); - await page.keyboard.press('Backspace'); - await page.keyboard.press('Backspace'); + + if (!options?.keepExisting) { + await page.keyboard.press('Backspace'); + await page.keyboard.press('Backspace'); + await page.keyboard.press('Backspace'); + await page.keyboard.press('Backspace'); + } + await page.locator('.components-form-token-field__input').fill(filterName); await page.keyboard.press('ArrowDown'); await page.keyboard.press('Enter'); @@ -598,6 +606,58 @@ test.describe('Instant Results Feature', { tag: '@group1' }, () => { 'wpCli', ); }); + + test('Is possible to search with taxonomies and tags in search form', async ({ + loggedInPage, + }) => { + await maybeEnableFeature('instant-results'); + await activatePlugin(loggedInPage, 'custom-search-form', 'wpCli'); + + await goToAdminPage(loggedInPage, 'admin.php?page=elasticpress'); + const apiResponsePromise = loggedInPage.waitForResponse( + '**/wp-json/elasticpress/v1/features*', + ); + + await loggedInPage.getByRole('button', { name: 'Live Search' }).click(); + await loggedInPage.getByRole('button', { name: 'Instant Results' }).click(); + await addInstantResultFilter(loggedInPage, '(category)'); + await addInstantResultFilter(loggedInPage, '(post_tag)', { + keepExisting: true, + }); + await loggedInPage.getByRole('button', { name: 'Save changes' }).click(); + + await apiResponsePromise; + + await publishPost( + loggedInPage, + { + title: 'Custom Search Form Page', + content: '[ep_custom_search_form]', + }, + true, + ); + + await expect(loggedInPage.locator('form.searchform')).toBeVisible(); + await expect(loggedInPage.locator('form.searchform input[name="s"]')).toBeVisible(); + await expect( + loggedInPage.locator('form.searchform .search-tags-checkboxes'), + ).toBeVisible(); + + const responsePromise = instantResultRequestPromise(loggedInPage, 'search='); + + await loggedInPage.locator('#cat').selectOption({ label: 'aciform' }); + await loggedInPage.locator('form.searchform').getByLabel('edge case').check(); + await loggedInPage.locator('form.searchform').getByLabel('categories').check(); + await loggedInPage.locator('#searchform #s').click(); + await loggedInPage.keyboard.press('Enter'); + + await expect(loggedInPage.locator('.ep-search-modal')).toBeVisible(); + await responsePromise; + + await expect(loggedInPage.locator('.ep-search-tokens')).toContainText('aciform'); + await expect(loggedInPage.locator('.ep-search-tokens')).toContainText('edge case'); + await expect(loggedInPage.locator('.ep-search-tokens')).toContainText('categories'); + }); }); test('Is possible to filter the arguments schema', async ({ loggedInPage }) => { diff --git a/tests/e2e/src/wordpress-files/test-plugins/custom-search-form.php b/tests/e2e/src/wordpress-files/test-plugins/custom-search-form.php new file mode 100644 index 0000000000..b43ffc2c42 --- /dev/null +++ b/tests/e2e/src/wordpress-files/test-plugins/custom-search-form.php @@ -0,0 +1,72 @@ + +
+ + +