From 59160750a42b6a6be308258e400722e44a788e6b Mon Sep 17 00:00:00 2001 From: Michal Czaplinski Date: Mon, 25 Nov 2024 21:47:21 -0500 Subject: [PATCH 01/13] Add support for multiple search queries via `pre_get_posts` filter --- lib/experimental/blocks.php | 29 +++++++++++++++++++++ packages/block-library/src/search/index.php | 15 +++++++---- 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/lib/experimental/blocks.php b/lib/experimental/blocks.php index 6990f1dab9c2c..286e3595dfa4b 100644 --- a/lib/experimental/blocks.php +++ b/lib/experimental/blocks.php @@ -151,6 +151,35 @@ function gutenberg_block_core_query_add_url_filtering( $query, $block ) { } add_filter( 'query_loop_block_query_vars', 'gutenberg_block_core_query_add_url_filtering', 10, 2 ); +function gutenberg_block_core_query_add_search_query_filtering( $query ) { + + // if the query is not the main query, return + if ( ! $query->is_main_query() ) { + return; + } + + // Check if the instant search gutenberg experiment is enabled + $gutenberg_experiments = get_option( 'gutenberg-experiments' ); + $instant_search_enabled = $gutenberg_experiments && array_key_exists( 'gutenberg-search-query-block', $gutenberg_experiments ); + if ( ! $instant_search_enabled ) { + return; + } + + // Get the search key from the URL + $search_key = 'instant-search'; + if ( ! isset( $_GET[ $search_key ] ) ) { + return; + } + + // Add the search parameter to the query + $query->set( 's', sanitize_text_field( $_GET[ $search_key ] ) ); +} + +add_action( + 'pre_get_posts', + 'gutenberg_block_core_query_add_search_query_filtering' +); + /** * Additional data to expose to the view script module in the Form block. */ diff --git a/packages/block-library/src/search/index.php b/packages/block-library/src/search/index.php index e042f0cbcdc7a..ac212e1982be5 100644 --- a/packages/block-library/src/search/index.php +++ b/packages/block-library/src/search/index.php @@ -213,8 +213,8 @@ function render_block_core_search( $attributes, $content, $block ) { } if ( $enhanced_pagination && $instant_search_enabled && isset( $block->context['queryId'] ) ) { - - $search = ''; + $is_inherited = isset( $block->context['query']['inherit'] ) && $block->context['query']['inherit'] && ! empty( $block->context['queryId'] ); + $search = ''; // If the query is defined in the block context, use it if ( isset( $block->context['query']['search'] ) && '' !== $block->context['query']['search'] ) { @@ -222,13 +222,18 @@ function render_block_core_search( $attributes, $content, $block ) { } // If the query is defined in the URL, it overrides the block context value if defined - $search = empty( $_GET[ 'instant-search-' . $block->context['queryId'] ] ) ? $search : sanitize_text_field( $_GET[ 'instant-search-' . $block->context['queryId'] ] ); + if ( $is_inherited ) { + $search = empty( $_GET['instant-search'] ) ? '' : sanitize_text_field( $_GET['instant-search'] ); + } else { + $search = empty( $_GET[ 'instant-search-' . $block->context['queryId'] ] ) ? '' : sanitize_text_field( $_GET[ 'instant-search-' . $block->context['queryId'] ] ); + } $form_context = array_merge( $form_context, array( - 'search' => $search, - 'queryId' => $block->context['queryId'], + 'search' => $search, + 'queryId' => $block->context['queryId'], + 'isInherited' => $is_inherited, ) ); } From b809838f598d305b5573e2ab9d1414dea51cfb3c Mon Sep 17 00:00:00 2001 From: Michal Czaplinski Date: Wed, 27 Nov 2024 18:41:22 +0000 Subject: [PATCH 02/13] Add the `is_admin()` checks --- lib/experimental/blocks.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/experimental/blocks.php b/lib/experimental/blocks.php index 286e3595dfa4b..6879182271b31 100644 --- a/lib/experimental/blocks.php +++ b/lib/experimental/blocks.php @@ -154,7 +154,7 @@ function gutenberg_block_core_query_add_url_filtering( $query, $block ) { function gutenberg_block_core_query_add_search_query_filtering( $query ) { // if the query is not the main query, return - if ( ! $query->is_main_query() ) { + if ( $query->is_admin() || ! $query->is_main_query() ) { return; } From ec6a88124bbed9d9d2840b9c04af8a7c065a2420 Mon Sep 17 00:00:00 2001 From: Michal Czaplinski Date: Wed, 4 Dec 2024 17:07:14 +0000 Subject: [PATCH 03/13] Enhance search block functionality by integrating canonical URL support and updating interactivity configuration. This change allows the search block to utilize the canonical URL when performing instant searches. --- packages/block-library/src/search/index.php | 3 ++ packages/block-library/src/search/view.js | 38 +++++++++++++++------ 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/packages/block-library/src/search/index.php b/packages/block-library/src/search/index.php index ac212e1982be5..8e79db1a26306 100644 --- a/packages/block-library/src/search/index.php +++ b/packages/block-library/src/search/index.php @@ -213,6 +213,9 @@ function render_block_core_search( $attributes, $content, $block ) { } if ( $enhanced_pagination && $instant_search_enabled && isset( $block->context['queryId'] ) ) { + + wp_interactivity_config( 'core/search', array( 'canonicalURL' => get_permalink() ) ); + $is_inherited = isset( $block->context['query']['inherit'] ) && $block->context['query']['inherit'] && ! empty( $block->context['queryId'] ); $search = ''; diff --git a/packages/block-library/src/search/view.js b/packages/block-library/src/search/view.js index d280d355322af..c9fa508335297 100644 --- a/packages/block-library/src/search/view.js +++ b/packages/block-library/src/search/view.js @@ -1,7 +1,12 @@ /** * WordPress dependencies */ -import { store, getContext, getElement } from '@wordpress/interactivity'; +import { + store, + getContext, + getElement, + getConfig, +} from '@wordpress/interactivity'; /** @type {( () => void ) | null} */ let supersedePreviousSearch = null; @@ -110,18 +115,31 @@ const { state, actions } = store( return; } - const url = new URL( window.location.href ); + let url = new URL( window.location.href ); if ( value ) { - // Set the instant-search parameter using the query ID and search value - const queryId = ctx.queryId; - url.searchParams.set( - `instant-search-${ queryId }`, - value - ); + if ( ctx.isInherited ) { + // Get the canonical URL from the config + const { canonicalURL } = getConfig( 'core/search' ); + + // Make sure we reset the pagination. + url = new URL( canonicalURL ); + url.searchParams.set( 'instant-search', value ); + } else { + // Set the instant-search parameter using the query ID and search value + const queryId = ctx.queryId; + url.searchParams.set( + `instant-search-${ queryId }`, + value + ); - // Make sure we reset the pagination. - url.searchParams.set( `query-${ queryId }-page`, '1' ); + // Make sure we reset the pagination. + url.searchParams.set( `query-${ queryId }-page`, '1' ); + } + } else if ( ctx.isInherited ) { + // Reset global search for inherited queries + url.searchParams.delete( 'instant-search' ); + url.searchParams.delete( 'paged' ); } else { // Reset specific search for non-inherited queries url.searchParams.delete( From fdb2b7f0d2d763d00683e531096cfad107e0d3f4 Mon Sep 17 00:00:00 2001 From: Michal Czaplinski Date: Tue, 10 Dec 2024 13:30:15 +0000 Subject: [PATCH 04/13] Handle pagination correctly by using the canonical URL without pagination. --- packages/block-library/src/search/index.php | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/block-library/src/search/index.php b/packages/block-library/src/search/index.php index 8e79db1a26306..09ae4db3f9373 100644 --- a/packages/block-library/src/search/index.php +++ b/packages/block-library/src/search/index.php @@ -214,7 +214,15 @@ function render_block_core_search( $attributes, $content, $block ) { if ( $enhanced_pagination && $instant_search_enabled && isset( $block->context['queryId'] ) ) { - wp_interactivity_config( 'core/search', array( 'canonicalURL' => get_permalink() ) ); + // Get the canonical URL without pagination + $canonical_url_no_pagination = get_pagenum_link(1); + + // If we're on a singular post/page, use its permalink instead + if (is_singular()) { + $canonical_url_no_pagination = get_permalink(); + } + + wp_interactivity_config( 'core/search', array( 'canonicalURL' => $canonical_url_no_pagination ) ); $is_inherited = isset( $block->context['query']['inherit'] ) && $block->context['query']['inherit'] && ! empty( $block->context['queryId'] ); $search = ''; @@ -224,11 +232,11 @@ function render_block_core_search( $attributes, $content, $block ) { $search = $block->context['query']['search']; } - // If the query is defined in the URL, it overrides the block context value if defined + // If the query is defined in the URL, it overrides the block context value. if ( $is_inherited ) { - $search = empty( $_GET['instant-search'] ) ? '' : sanitize_text_field( $_GET['instant-search'] ); + $search = empty( $_GET['instant-search'] ) ? $search : sanitize_text_field( $_GET['instant-search'] ); } else { - $search = empty( $_GET[ 'instant-search-' . $block->context['queryId'] ] ) ? '' : sanitize_text_field( $_GET[ 'instant-search-' . $block->context['queryId'] ] ); + $search = empty( $_GET[ 'instant-search-' . $block->context['queryId'] ] ) ? $search : sanitize_text_field( $_GET[ 'instant-search-' . $block->context['queryId'] ] ); } $form_context = array_merge( From 4df53f331674bf656176eedb192ea92e8cdbbbda Mon Sep 17 00:00:00 2001 From: Michal Czaplinski Date: Tue, 10 Dec 2024 13:30:45 +0000 Subject: [PATCH 05/13] Add e2e tests for instant search with default query. --- .../src/request-utils/index.ts | 8 +- .../src/request-utils/templates.ts | 39 +++- .../interactivity/instant-search.spec.ts | 174 ++++++++++++++++++ 3 files changed, 219 insertions(+), 2 deletions(-) diff --git a/packages/e2e-test-utils-playwright/src/request-utils/index.ts b/packages/e2e-test-utils-playwright/src/request-utils/index.ts index f6818945e1693..b125005f86f1c 100644 --- a/packages/e2e-test-utils-playwright/src/request-utils/index.ts +++ b/packages/e2e-test-utils-playwright/src/request-utils/index.ts @@ -16,7 +16,11 @@ import { listMedia, uploadMedia, deleteMedia, deleteAllMedia } from './media'; import { createUser, deleteAllUsers } from './users'; import { setupRest, rest, getMaxBatchSize, batchRest } from './rest'; import { getPluginsMap, activatePlugin, deactivatePlugin } from './plugins'; -import { deleteAllTemplates, createTemplate } from './templates'; +import { + deleteAllTemplates, + createTemplate, + updateTemplate, +} from './templates'; import { activateTheme, getCurrentThemeGlobalStylesPostId, @@ -175,6 +179,8 @@ class RequestUtils { deleteAllTemplates.bind( this ); /** @borrows createTemplate as this.createTemplate */ createTemplate: typeof createTemplate = createTemplate.bind( this ); + /** @borrows updateTemplate as this.updateTemplate */ + updateTemplate: typeof updateTemplate = updateTemplate.bind( this ); /** @borrows resetPreferences as this.resetPreferences */ resetPreferences: typeof resetPreferences = resetPreferences.bind( this ); /** @borrows listMedia as this.listMedia */ diff --git a/packages/e2e-test-utils-playwright/src/request-utils/templates.ts b/packages/e2e-test-utils-playwright/src/request-utils/templates.ts index 76ae7022e9b7a..bb7467cce87a3 100644 --- a/packages/e2e-test-utils-playwright/src/request-utils/templates.ts +++ b/packages/e2e-test-utils-playwright/src/request-utils/templates.ts @@ -8,6 +8,8 @@ type TemplateType = 'wp_template' | 'wp_template_part'; interface Template { wp_id: number; id: string; + title: string; + slug: string; } interface CreateTemplatePayload { @@ -80,4 +82,39 @@ async function createTemplate( return template; } -export { deleteAllTemplates, createTemplate }; +/** + * Updates a template using the REST API. + * + * @param this + * @param type Template type. + * @param payload Template attributes. + */ +async function updateTemplate( + this: RequestUtils, + type: TemplateType, + payload: CreateTemplatePayload +) { + const path = PATH_MAPPING[ type ]; + + if ( ! path ) { + throw new Error( `Unsupported template type: ${ type }.` ); + } + + const templates = await this.rest< Template[] >( { path } ); + + const template = templates.find( ( t ) => t.slug === payload.slug ); + + if ( ! template ) { + throw new Error( `Template with slug "${ payload.slug }" not found.` ); + } + + const updatedTemplate = await this.rest< Template >( { + method: 'POST', + path: `${ PATH_MAPPING[ type ] }/${ template.id }`, + params: { ...payload, type, status: 'publish', is_wp_suggestion: true }, + } ); + + return updatedTemplate; +} + +export { deleteAllTemplates, createTemplate, updateTemplate }; diff --git a/test/e2e/specs/interactivity/instant-search.spec.ts b/test/e2e/specs/interactivity/instant-search.spec.ts index 6ce62e7291931..cae516bf05d6a 100644 --- a/test/e2e/specs/interactivity/instant-search.spec.ts +++ b/test/e2e/specs/interactivity/instant-search.spec.ts @@ -548,6 +548,180 @@ test.describe( 'Instant Search', () => { } ); } ); + test.describe( 'Inherited (Default) Query', () => { + test.beforeEach( async ( { page } ) => { + // Navigate to the home page + await page.goto( '/' ); + } ); + + test.beforeAll( async ( { requestUtils } ) => { + // Edit the Home template instead of creating a new page + await requestUtils.updateTemplate( 'wp_template', { + slug: 'home', + content: ` + +
+ + + + + + + + + + + + +

No results found.

+ + +
+`, + } ); + } ); + + test( 'should update search results without page reload', async ( { + page, + } ) => { + // Check that the first post is shown initially + await expect( + page.getByText( 'First Test Post', { exact: true } ) + ).toBeVisible(); + + // Type in search input and verify results update + await page.locator( 'input[type="search"]' ).fill( 'Unique' ); + await page.waitForResponse( ( response ) => + response.url().includes( 'instant-search=Unique' ) + ); + + // Verify the unique post is shown + await expect( + page.getByText( 'Unique Post', { exact: true } ) + ).toBeVisible(); + + // Check that there is only one post + const posts = page + .getByTestId( 'default-query' ) + .getByRole( 'heading', { level: 3 } ); + await expect( posts ).toHaveCount( 1 ); + + // Verify that the other posts are hidden + await expect( + page.getByText( 'First Test Post', { exact: true } ) + ).toBeHidden(); + } ); + + test( 'should update URL with search parameter', async ( { page } ) => { + // Test global query search parameter + await page.locator( 'input[type="search"]' ).fill( 'Test' ); + await expect( page ).toHaveURL( /instant-search=Test/ ); + + // Clear search and verify parameter is removed + await page.locator( 'input[type="search"]' ).fill( '' ); + await expect( page ).not.toHaveURL( /instant-search=/ ); + } ); + + test( 'should handle search debouncing', async ( { page } ) => { + let responseCount = 0; + + // Monitor the number of requests + page.on( 'response', ( response ) => { + if ( response.url().includes( 'instant-search=' ) ) { + responseCount++; + } + } ); + + // Type quickly and wait for the response + let responsePromise = page.waitForResponse( ( response ) => { + return ( + response.url().includes( 'instant-search=Test' ) && + response.status() === 200 + ); + } ); + await page + .locator( 'input[type="search"]' ) + .pressSequentially( 'Test', { delay: 100 } ); + await responsePromise; + + // Check that only one request was made + expect( responseCount ).toBe( 1 ); + + // Verify URL is updated after debounce + await expect( page ).toHaveURL( /instant-search=Test/ ); + + responsePromise = page.waitForResponse( ( response ) => { + return response.url().includes( 'instant-search=Test1234' ); + } ); + // Type again with a large delay and verify that a request is made + // for each character + await page + .locator( 'input[type="search"]' ) + .pressSequentially( '1234', { delay: 500 } ); + await responsePromise; + + // Check that five requests were made (Test, Test1, Test12, Test123, Test1234) + expect( responseCount ).toBe( 5 ); + } ); + + test( 'should reset pagination when searching', async ( { page } ) => { + // Navigate to second page + await page.click( 'a.wp-block-query-pagination-next' ); + + // Check that the url contains either `?paged=2` or `/page/2/`. If the + // site has the `pretty` permalink structure, the url will contain + // `/page/2/` instead of `?paged=2`. + await expect( page ).toHaveURL( /(?:paged=2|\/page\/2\/)/ ); + + // Search and verify we're back to first page + await page.locator( 'input[type="search"]' ).fill( 'Test' ); + await expect( page ).not.toHaveURL( /paged=2/ ); + + // Now we're back on the first page, so the URL should just contain the search parameter + await expect( page ).toHaveURL( /\?instant-search=Test/ ); + } ); + + test( 'should show no-results block when search has no matches', async ( { + page, + } ) => { + await page + .locator( 'input[type="search"]' ) + .fill( 'NonexistentContent' ); + await page.waitForResponse( ( response ) => + response.url().includes( 'instant-search=NonexistentContent' ) + ); + + // Verify no-results block is shown + await expect( page.getByText( 'No results found.' ) ).toBeVisible(); + } ); + + test( 'should update pagination numbers based on search results', async ( { + page, + } ) => { + // Initially should show pagination numbers for 3 pages + await expect( + page.locator( '.wp-block-query-pagination-numbers' ) + ).toBeVisible(); + await expect( + page.getByRole( 'link', { name: '2' } ) + ).toBeVisible(); + await expect( + page.getByRole( 'link', { name: '3' } ) + ).toBeVisible(); + + // Search for unique post + await page.locator( 'input[type="search"]' ).fill( 'Unique' ); + await page.waitForResponse( ( response ) => + response.url().includes( 'instant-search=Unique' ) + ); + + // Pagination numbers should not be visible with single result + await expect( + page.locator( '.wp-block-query-pagination-numbers' ) + ).toBeHidden(); + } ); + } ); + test.describe( 'Editor', () => { test.beforeEach( async ( { admin } ) => { await admin.createNewPost( { From 2f50ac9e049e974d1c9f5e5fec5651b3eac8500e Mon Sep 17 00:00:00 2001 From: Michal Czaplinski Date: Tue, 10 Dec 2024 17:56:39 +0000 Subject: [PATCH 06/13] Refactor how we pass the query_id and add a comment on the filter --- lib/experimental/blocks.php | 6 ++++++ packages/block-library/src/search/index.php | 16 +++++++++------- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/lib/experimental/blocks.php b/lib/experimental/blocks.php index 6879182271b31..99a16ad302ed3 100644 --- a/lib/experimental/blocks.php +++ b/lib/experimental/blocks.php @@ -151,6 +151,12 @@ function gutenberg_block_core_query_add_url_filtering( $query, $block ) { } add_filter( 'query_loop_block_query_vars', 'gutenberg_block_core_query_add_url_filtering', 10, 2 ); +/** + * Adds the search query to Query blocks for the inherited queries if the instant search experiment is enabled. + * + * @param WP_Query $query The query object. + * @return void + */ function gutenberg_block_core_query_add_search_query_filtering( $query ) { // if the query is not the main query, return diff --git a/packages/block-library/src/search/index.php b/packages/block-library/src/search/index.php index 09ae4db3f9373..60887112c71a5 100644 --- a/packages/block-library/src/search/index.php +++ b/packages/block-library/src/search/index.php @@ -224,7 +224,7 @@ function render_block_core_search( $attributes, $content, $block ) { wp_interactivity_config( 'core/search', array( 'canonicalURL' => $canonical_url_no_pagination ) ); - $is_inherited = isset( $block->context['query']['inherit'] ) && $block->context['query']['inherit'] && ! empty( $block->context['queryId'] ); + $query_id = $block->context['queryId']; $search = ''; // If the query is defined in the block context, use it @@ -232,18 +232,20 @@ function render_block_core_search( $attributes, $content, $block ) { $search = $block->context['query']['search']; } + $is_inherited = isset( $block->context['query']['inherit'] ) && $block->context['query']['inherit'] && ! empty( $query_id ); + + // Inherited query: `instant-search=` + // Custom query: `instant-search-=` + $search_key = $is_inherited ? 'instant-search' : 'instant-search-' . $query_id; + // If the query is defined in the URL, it overrides the block context value. - if ( $is_inherited ) { - $search = empty( $_GET['instant-search'] ) ? $search : sanitize_text_field( $_GET['instant-search'] ); - } else { - $search = empty( $_GET[ 'instant-search-' . $block->context['queryId'] ] ) ? $search : sanitize_text_field( $_GET[ 'instant-search-' . $block->context['queryId'] ] ); - } + $search = empty( $_GET[ $search_key ] ) ? $search : sanitize_text_field( $_GET[ $search_key ] ); $form_context = array_merge( $form_context, array( 'search' => $search, - 'queryId' => $block->context['queryId'], + 'queryId' => $query_id, 'isInherited' => $is_inherited, ) ); From dc923dd573dea80df54ed01f7db5c5277940b9e8 Mon Sep 17 00:00:00 2001 From: Michal Czaplinski Date: Tue, 10 Dec 2024 18:55:34 +0000 Subject: [PATCH 07/13] Set the search state in the interactivity state for inherited queries. --- packages/block-library/src/search/index.php | 6 +++++- packages/block-library/src/search/view.js | 16 ++++++++++++---- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/packages/block-library/src/search/index.php b/packages/block-library/src/search/index.php index 60887112c71a5..3f8c24f04a773 100644 --- a/packages/block-library/src/search/index.php +++ b/packages/block-library/src/search/index.php @@ -112,7 +112,7 @@ function render_block_core_search( $attributes, $content, $block ) { wp_enqueue_script_module( '@wordpress/block-library/search/view' ); if ( $instant_search_enabled ) { - $input->set_attribute( 'data-wp-bind--value', 'context.search' ); + $input->set_attribute( 'data-wp-bind--value', 'state.searchGetter' ); $input->set_attribute( 'data-wp-on-async--input', 'actions.updateSearch' ); } } @@ -241,6 +241,10 @@ function render_block_core_search( $attributes, $content, $block ) { // If the query is defined in the URL, it overrides the block context value. $search = empty( $_GET[ $search_key ] ) ? $search : sanitize_text_field( $_GET[ $search_key ] ); + if ( $is_inherited ) { + wp_interactivity_state( 'core/search', array( 'search' => $search ) ); + } + $form_context = array_merge( $form_context, array( diff --git a/packages/block-library/src/search/view.js b/packages/block-library/src/search/view.js index c9fa508335297..6d99c2b4ca8ca 100644 --- a/packages/block-library/src/search/view.js +++ b/packages/block-library/src/search/view.js @@ -48,6 +48,10 @@ const { state, actions } = store( } return ctx.isSearchInputVisible; }, + get searchGetter() { + const { isInherited, search } = getContext(); + return isInherited ? state.search : search; + }, }, actions: { openSearchInput( event ) { @@ -88,14 +92,18 @@ const { state, actions } = store( *updateSearch( e ) { const { value } = e.target; - const ctx = getContext(); - // Don't navigate if the search didn't really change. - if ( value === ctx.search ) { + if ( value === state.searchGetter ) { return; } - ctx.search = value; + const ctx = getContext(); + + if ( ctx.isInherited ) { + state.search = value; + } else { + ctx.search = value; + } // Debounce the search by 300ms to prevent multiple navigations. supersedePreviousSearch?.(); From 3f7de9749f52f127c135ed307c72aa83a8478f11 Mon Sep 17 00:00:00 2001 From: Michal Czaplinski Date: Wed, 11 Dec 2024 11:49:20 +0000 Subject: [PATCH 08/13] Prevent default form submission on search submit. --- packages/block-library/src/search/index.php | 2 ++ packages/block-library/src/search/view.js | 3 +++ 2 files changed, 5 insertions(+) diff --git a/packages/block-library/src/search/index.php b/packages/block-library/src/search/index.php index 3f8c24f04a773..e64ced8d086f4 100644 --- a/packages/block-library/src/search/index.php +++ b/packages/block-library/src/search/index.php @@ -214,6 +214,8 @@ function render_block_core_search( $attributes, $content, $block ) { if ( $enhanced_pagination && $instant_search_enabled && isset( $block->context['queryId'] ) ) { + $form_directives .= ' data-wp-on--submit="actions.handleSearchSubmit"'; + // Get the canonical URL without pagination $canonical_url_no_pagination = get_pagenum_link(1); diff --git a/packages/block-library/src/search/view.js b/packages/block-library/src/search/view.js index 6d99c2b4ca8ca..1e4b3374c0326 100644 --- a/packages/block-library/src/search/view.js +++ b/packages/block-library/src/search/view.js @@ -89,6 +89,9 @@ const { state, actions } = store( actions.closeSearchInput(); } }, + handleSearchSubmit( e ) { + e.preventDefault(); + }, *updateSearch( e ) { const { value } = e.target; From ee12576343313776cbb3d6332e7275fa4a048e76 Mon Sep 17 00:00:00 2001 From: Michal Czaplinski Date: Wed, 11 Dec 2024 16:35:30 +0000 Subject: [PATCH 09/13] Add e2e tests for instant search with default query. --- .../interactivity/instant-search.spec.ts | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/test/e2e/specs/interactivity/instant-search.spec.ts b/test/e2e/specs/interactivity/instant-search.spec.ts index cae516bf05d6a..7dde7639483bd 100644 --- a/test/e2e/specs/interactivity/instant-search.spec.ts +++ b/test/e2e/specs/interactivity/instant-search.spec.ts @@ -722,6 +722,91 @@ test.describe( 'Instant Search', () => { } ); } ); + test.describe( 'Multiple Inherited (Default) Queries', () => { + test.beforeEach( async ( { page } ) => { + // Navigate to the home page + await page.goto( '/' ); + } ); + + test.beforeAll( async ( { requestUtils } ) => { + await requestUtils.updateTemplate( 'wp_template', { + slug: 'home', + content: ` + +
+ + + + + + + + + + + + +

No results found.

+ + +
+ + + + +
+ + + + + + + + + + + + +

No results found.

+ + +
+`, + } ); + } ); + + test( 'should keep the search state in sync across multiple inherited queries', async ( { + page, + } ) => { + // Get search inputs + const firstQuerySearch = page.getByLabel( '1st-instant-search' ); + const secondQuerySearch = page.getByLabel( '2nd-instant-search' ); + + // Search for "Unique" in the first query + await firstQuerySearch.fill( 'Unique' ); + + // Verify that the URL has been updated with the search parameter + await expect( page ).toHaveURL( /instant-search=Unique/ ); + + // Verify that the second query search input has the same value + await expect( secondQuerySearch ).toHaveValue( 'Unique' ); + + // Verify that the first query has only one post which is the "Unique" post + const firstQueryPosts = page + .getByTestId( 'default-query-1' ) + .getByRole( 'heading', { level: 3 } ); + await expect( firstQueryPosts ).toHaveCount( 1 ); + await expect( firstQueryPosts ).toContainText( 'Unique Post' ); + + // Verify that the second query also has only one post which is the "Unique" post + const secondQueryPosts = page + .getByTestId( 'default-query-2' ) + .getByRole( 'heading', { level: 3 } ); + await expect( secondQueryPosts ).toHaveCount( 1 ); + await expect( secondQueryPosts ).toContainText( 'Unique Post' ); + } ); + } ); + test.describe( 'Editor', () => { test.beforeEach( async ( { admin } ) => { await admin.createNewPost( { From fdfcf8ee9e15cde49ca49edf944d313e653190c4 Mon Sep 17 00:00:00 2001 From: Michal Czaplinski Date: Wed, 11 Dec 2024 17:35:50 +0000 Subject: [PATCH 10/13] Add an e2e test for multiple inherited and custom queries used in the same template --- .../interactivity/instant-search.spec.ts | 213 +++++++++++++----- 1 file changed, 162 insertions(+), 51 deletions(-) diff --git a/test/e2e/specs/interactivity/instant-search.spec.ts b/test/e2e/specs/interactivity/instant-search.spec.ts index 7dde7639483bd..a50c3df42a65d 100644 --- a/test/e2e/specs/interactivity/instant-search.spec.ts +++ b/test/e2e/specs/interactivity/instant-search.spec.ts @@ -722,62 +722,58 @@ test.describe( 'Instant Search', () => { } ); } ); - test.describe( 'Multiple Inherited (Default) Queries', () => { - test.beforeEach( async ( { page } ) => { - // Navigate to the home page - await page.goto( '/' ); - } ); - - test.beforeAll( async ( { requestUtils } ) => { + test.describe( 'Multiple Inherited and Custom Queries', () => { + test( 'should keep the search state in sync across multiple inherited queries', async ( { + page, + requestUtils, + } ) => { await requestUtils.updateTemplate( 'wp_template', { slug: 'home', content: ` - -
- - - - - - - - - - - - -

No results found.

- - -
- - - - -
- - - - - - - - - - - - -

No results found.

- - -
-`, + +
+ + + + + + + + + + + + +

No results found.

+ + +
+ + + + +
+ + + + + + + + + + + + +

No results found.

+ + +
+ `, } ); - } ); - test( 'should keep the search state in sync across multiple inherited queries', async ( { - page, - } ) => { + await page.goto( '/' ); + // Get search inputs const firstQuerySearch = page.getByLabel( '1st-instant-search' ); const secondQuerySearch = page.getByLabel( '2nd-instant-search' ); @@ -805,6 +801,121 @@ test.describe( 'Instant Search', () => { await expect( secondQueryPosts ).toHaveCount( 1 ); await expect( secondQueryPosts ).toContainText( 'Unique Post' ); } ); + + test( 'should handle searches independently when a Default and a Custom query are placed in a home template', async ( { + page, + requestUtils, + } ) => { + // Set up: Add one inherited and one custom query to the home template + await requestUtils.updateTemplate( 'wp_template', { + slug: 'home', + content: ` + +
+ + + + + + + + + + + + +

No results found.

+ + +
+ + + + +
+ + + + + + + + + + + + +

No results found.

+ + +
+ `, + } ); + + await page.goto( '/' ); + + // Get search inputs + const defaultQuerySearch = page.getByLabel( + 'Default Query Search' + ); + const customQuerySearch = page.getByLabel( 'Custom Query Search' ); + + // Search for "Unique" in the default query + await defaultQuerySearch.fill( 'Unique' ); + + // Verify that the URL has been updated with the search parameter + await expect( page ).toHaveURL( /instant-search=Unique/ ); + + // Verify that the custom query search input has no value + await expect( customQuerySearch ).toHaveValue( '' ); + + // Verify that the default query has only one post which is the "Unique" post + const defaultQueryPosts = page + .getByTestId( 'default-query' ) + .getByRole( 'heading', { level: 3 } ); + await expect( defaultQueryPosts ).toHaveCount( 1 ); + await expect( defaultQueryPosts ).toContainText( 'Unique Post' ); + + // Verify that the custom query shows exactly 2 posts: First Test Post and Second Test Post + const customQuery = page.getByTestId( 'custom-query' ); + const posts = customQuery.getByRole( 'heading', { level: 3 } ); + await expect( posts ).toHaveCount( 2 ); + await expect( posts ).toContainText( [ + 'First Test Post', + 'Second Test Post', + ] ); + + // Search for "Third" in the custom query + await customQuerySearch.fill( 'Third' ); + + // Verify that the URL has been updated with the search parameter + await expect( page ).toHaveURL( + /instant-search=Unique&instant-search-2222=Third/ + ); + + // Verify that the default query search input still has "Unique" + await expect( defaultQuerySearch ).toHaveValue( 'Unique' ); + + // Verify that the default query has only one post which is the "Unique" post + await expect( defaultQueryPosts ).toHaveCount( 1 ); + await expect( defaultQueryPosts ).toContainText( 'Unique Post' ); + + // Verify that the custom query has only one post which is the "Third Test Post" + const customQueryPosts = page + .getByTestId( 'custom-query' ) + .getByRole( 'heading', { level: 3 } ); + await expect( customQueryPosts ).toHaveCount( 1 ); + await expect( customQueryPosts ).toContainText( 'Third Test Post' ); + + // Clear default query search + await defaultQuerySearch.fill( '' ); + await expect( page ).not.toHaveURL( /instant-search=Unique/ ); + await expect( page ).toHaveURL( /instant-search-2222=Third/ ); + + // Clear custom query search + await customQuerySearch.fill( '' ); + await expect( page ).not.toHaveURL( /instant-search-2222=Third/ ); + } ); } ); test.describe( 'Editor', () => { From 8f32103878473ba4baa5b268cc36f3cdfc24bbc5 Mon Sep 17 00:00:00 2001 From: Michal Czaplinski Date: Thu, 12 Dec 2024 18:25:44 +0000 Subject: [PATCH 11/13] PHP formatting --- packages/block-library/src/search/index.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/block-library/src/search/index.php b/packages/block-library/src/search/index.php index e64ced8d086f4..a44d91b60bd61 100644 --- a/packages/block-library/src/search/index.php +++ b/packages/block-library/src/search/index.php @@ -217,17 +217,17 @@ function render_block_core_search( $attributes, $content, $block ) { $form_directives .= ' data-wp-on--submit="actions.handleSearchSubmit"'; // Get the canonical URL without pagination - $canonical_url_no_pagination = get_pagenum_link(1); + $canonical_url_no_pagination = get_pagenum_link( 1 ); // If we're on a singular post/page, use its permalink instead - if (is_singular()) { + if ( is_singular() ) { $canonical_url_no_pagination = get_permalink(); } wp_interactivity_config( 'core/search', array( 'canonicalURL' => $canonical_url_no_pagination ) ); $query_id = $block->context['queryId']; - $search = ''; + $search = ''; // If the query is defined in the block context, use it if ( isset( $block->context['query']['search'] ) && '' !== $block->context['query']['search'] ) { From 3c3075bc99d7f9fdc0f0a218a71880b2e568d8bd Mon Sep 17 00:00:00 2001 From: Michal Czaplinski Date: Thu, 12 Dec 2024 19:05:10 +0000 Subject: [PATCH 12/13] Add a new request utils function to update or create a template. We need this in case the `updateTemplate` function throws an error if the template is not found. --- .../e2e-test-utils-playwright/src/request-utils/index.ts | 7 ++++--- .../src/request-utils/templates.ts | 7 ++++--- test/e2e/specs/interactivity/instant-search.spec.ts | 6 +++--- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/packages/e2e-test-utils-playwright/src/request-utils/index.ts b/packages/e2e-test-utils-playwright/src/request-utils/index.ts index b125005f86f1c..364f5369564ac 100644 --- a/packages/e2e-test-utils-playwright/src/request-utils/index.ts +++ b/packages/e2e-test-utils-playwright/src/request-utils/index.ts @@ -19,7 +19,7 @@ import { getPluginsMap, activatePlugin, deactivatePlugin } from './plugins'; import { deleteAllTemplates, createTemplate, - updateTemplate, + updateOrCreateTemplate, } from './templates'; import { activateTheme, @@ -179,8 +179,9 @@ class RequestUtils { deleteAllTemplates.bind( this ); /** @borrows createTemplate as this.createTemplate */ createTemplate: typeof createTemplate = createTemplate.bind( this ); - /** @borrows updateTemplate as this.updateTemplate */ - updateTemplate: typeof updateTemplate = updateTemplate.bind( this ); + /** @borrows updateOrCreateTemplate as this.updateOrCreateTemplate */ + updateOrCreateTemplate: typeof updateOrCreateTemplate = + updateOrCreateTemplate.bind( this ); /** @borrows resetPreferences as this.resetPreferences */ resetPreferences: typeof resetPreferences = resetPreferences.bind( this ); /** @borrows listMedia as this.listMedia */ diff --git a/packages/e2e-test-utils-playwright/src/request-utils/templates.ts b/packages/e2e-test-utils-playwright/src/request-utils/templates.ts index bb7467cce87a3..6be65978ae48c 100644 --- a/packages/e2e-test-utils-playwright/src/request-utils/templates.ts +++ b/packages/e2e-test-utils-playwright/src/request-utils/templates.ts @@ -89,7 +89,7 @@ async function createTemplate( * @param type Template type. * @param payload Template attributes. */ -async function updateTemplate( +async function updateOrCreateTemplate( this: RequestUtils, type: TemplateType, payload: CreateTemplatePayload @@ -104,8 +104,9 @@ async function updateTemplate( const template = templates.find( ( t ) => t.slug === payload.slug ); + // If the template is not found, create it. if ( ! template ) { - throw new Error( `Template with slug "${ payload.slug }" not found.` ); + return createTemplate.bind( this )( type, payload ); } const updatedTemplate = await this.rest< Template >( { @@ -117,4 +118,4 @@ async function updateTemplate( return updatedTemplate; } -export { deleteAllTemplates, createTemplate, updateTemplate }; +export { deleteAllTemplates, createTemplate, updateOrCreateTemplate }; diff --git a/test/e2e/specs/interactivity/instant-search.spec.ts b/test/e2e/specs/interactivity/instant-search.spec.ts index a50c3df42a65d..b8bdeb8f0163b 100644 --- a/test/e2e/specs/interactivity/instant-search.spec.ts +++ b/test/e2e/specs/interactivity/instant-search.spec.ts @@ -556,7 +556,7 @@ test.describe( 'Instant Search', () => { test.beforeAll( async ( { requestUtils } ) => { // Edit the Home template instead of creating a new page - await requestUtils.updateTemplate( 'wp_template', { + await requestUtils.updateOrCreateTemplate( 'wp_template', { slug: 'home', content: ` @@ -727,7 +727,7 @@ test.describe( 'Instant Search', () => { page, requestUtils, } ) => { - await requestUtils.updateTemplate( 'wp_template', { + await requestUtils.updateOrCreateTemplate( 'wp_template', { slug: 'home', content: ` @@ -807,7 +807,7 @@ test.describe( 'Instant Search', () => { requestUtils, } ) => { // Set up: Add one inherited and one custom query to the home template - await requestUtils.updateTemplate( 'wp_template', { + await requestUtils.updateOrCreateTemplate( 'wp_template', { slug: 'home', content: ` From 67789ff5e9283d831e8de50e2e54a16d1627e537 Mon Sep 17 00:00:00 2001 From: Michal Czaplinski Date: Wed, 18 Dec 2024 22:33:45 +0000 Subject: [PATCH 13/13] Update e2e tests for Instant Search to verify block renaming in Inspector Controls and List View --- .../e2e/specs/interactivity/instant-search.spec.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/test/e2e/specs/interactivity/instant-search.spec.ts b/test/e2e/specs/interactivity/instant-search.spec.ts index b8bdeb8f0163b..8182596a36ceb 100644 --- a/test/e2e/specs/interactivity/instant-search.spec.ts +++ b/test/e2e/specs/interactivity/instant-search.spec.ts @@ -982,13 +982,15 @@ test.describe( 'Instant Search', () => { name: 'Editor settings', } ); + const blockCard = editorSettings.locator( + '.block-editor-block-card' + ); + // Check that the Search block is renamed to "Instant Search" in the Inspector Controls title await editor.canvas .getByRole( 'document', { name: 'Block: Search' } ) .click(); - await expect( editorSettings ).toContainText( - 'Instant Search (Search)' - ); + await expect( blockCard ).toContainText( 'Instant Search' ); // Select the Query Loop block and open the Advanced View and disable enhanced pagination await editor.selectBlocks( @@ -1008,10 +1010,8 @@ test.describe( 'Instant Search', () => { await editor.canvas .getByRole( 'document', { name: 'Block: Search' } ) .click(); - await expect( editorSettings ).toContainText( 'Search' ); - await expect( editorSettings ).not.toContainText( - 'Instant Search (Search)' - ); + await expect( blockCard ).toContainText( 'Search' ); + await expect( blockCard ).not.toContainText( 'Instant Search' ); // Check that the Search block is renamed back to "Search" in the List View await expect( listView.getByText( 'Search' ) ).toBeVisible();