diff --git a/src/wp-includes/class-wp-query.php b/src/wp-includes/class-wp-query.php index 9f56a2acc4..ce9fcb8233 100644 --- a/src/wp-includes/class-wp-query.php +++ b/src/wp-includes/class-wp-query.php @@ -108,6 +108,14 @@ class WP_Query { */ public $current_post = -1; + /** + * Whether the caller is before the loop. + * + * @since 6.3.0 + * @var bool + */ + public $before_loop = true; + /** * Whether the loop has started and the caller is in the loop. * @@ -517,6 +525,7 @@ public function init() { $this->post_count = 0; $this->current_post = -1; $this->in_the_loop = false; + $this->before_loop = true; unset( $this->request ); unset( $this->post ); unset( $this->comments ); @@ -3631,6 +3640,7 @@ public function the_post() { } $this->in_the_loop = true; + $this->before_loop = false; if ( -1 == $this->current_post ) { // Loop has just started. /** @@ -3671,6 +3681,8 @@ public function have_posts() { // Do some cleaning up after the loop. $this->rewind_posts(); } elseif ( 0 === $this->post_count ) { + $this->before_loop = false; + /** * Fires if no results are found in a post query. * diff --git a/src/wp-includes/formatting.php b/src/wp-includes/formatting.php index 5db54014d6..4befdf523c 100644 --- a/src/wp-includes/formatting.php +++ b/src/wp-includes/formatting.php @@ -3923,10 +3923,26 @@ function wp_trim_excerpt( $text = '', $post = null ) { $text = strip_shortcodes( $text ); + /* + * Temporarily unhook wp_filter_content_tags() since any tags + * within the excerpt are stripped out. Modifying the tags here + * is wasteful and can lead to bugs in the image counting logic. + */ + $filter_removed = remove_filter( 'the_content', 'wp_filter_content_tags' ); + /** This filter is documented in wp-includes/post-template.php */ $text = apply_filters( 'the_content', $text ); $text = str_replace( ']]>', ']]>', $text ); + /** + * Only restore the filter callback if it was removed above. The logic + * to unhook and restore only applies on the default priority of 10, + * which is generally used for the filter callback in WordPress core. + */ + if ( $filter_removed ) { + add_filter( 'the_content', 'wp_filter_content_tags' ); + } + /* translators: Maximum number of words used in a post excerpt. */ $excerpt_length = (int) _x( '55', 'excerpt_length' ); diff --git a/src/wp-includes/media.php b/src/wp-includes/media.php index 5626c63ad3..0de8cf82d5 100644 --- a/src/wp-includes/media.php +++ b/src/wp-includes/media.php @@ -5336,16 +5336,20 @@ function wp_get_webp_info( $filename ) { * * Under the hood, the function uses {@see wp_increase_content_media_count()} every time it is called for an element * within the main content. If the element is the very first content element, the `loading` attribute will be omitted. - * This default threshold of 1 content element to omit the `loading` attribute for can be customized using the + * This default threshold of 3 content elements to omit the `loading` attribute for can be customized using the * {@see 'wp_omit_loading_attr_threshold'} filter. * * @since 5.9.0 * + * @global WP_Query $wp_query WordPress Query object. + * * @param string $context Context for the element for which the `loading` attribute value is requested. * @return string|bool The default `loading` attribute value. Either 'lazy', 'eager', or a boolean `false`, to indicate * that the `loading` attribute should be skipped. */ function wp_get_loading_attr_default( $context ) { + global $wp_query; + // Skip lazy-loading for the overall block template, as it is handled more granularly. if ( 'template' === $context ) { return false; @@ -5357,6 +5361,43 @@ function wp_get_loading_attr_default( $context ) { return false; } + // Special handling for programmatically created image tags. + if ( ( 'the_post_thumbnail' === $context || 'wp_get_attachment_image' === $context ) ) { + /* + * Skip programmatically created images within post content as they need to be handled together with the other + * images within the post content. + * Without this clause, they would already be counted below which skews the number and can result in the first + * post content image being lazy-loaded only because there are images elsewhere in the post content. + */ + if ( doing_filter( 'the_content' ) ) { + return false; + } + + // Conditionally skip lazy-loading on images before the loop. + if ( + // Only apply for main query but before the loop. + $wp_query->before_loop && $wp_query->is_main_query() + /* + * Any image before the loop, but after the header has started should not be lazy-loaded, + * except when the footer has already started which can happen when the current template + * does not include any loop. + */ + && did_action( 'get_header' ) && ! did_action( 'get_footer' ) + ) { + return false; + } + } + + /* + * Skip programmatically created images within post content as they need to be handled together with the other + * images within the post content. + * Without this clause, they would already be counted below which skews the number and can result in the first + * post content image being lazy-loaded only because there are images elsewhere in the post content. + */ + if ( ( 'the_post_thumbnail' === $context || 'wp_get_attachment_image' === $context ) && doing_filter( 'the_content' ) ) { + return false; + } + /* * The first elements in 'the_content' or 'the_post_thumbnail' should not be lazy-loaded, * as they are likely above the fold. @@ -5386,7 +5427,7 @@ function wp_get_loading_attr_default( $context ) { /** * Gets the threshold for how many of the first content media elements to not lazy-load. * - * This function runs the {@see 'wp_omit_loading_attr_threshold'} filter, which uses a default threshold value of 1. + * This function runs the {@see 'wp_omit_loading_attr_threshold'} filter, which uses a default threshold value of 3. * The filter is only run once per page load, unless the `$force` parameter is used. * * @since 5.9.0 @@ -5407,10 +5448,11 @@ function wp_omit_loading_attr_threshold( $force = false ) { * for only the very first content media element. * * @since 5.9.0 + * @since 6.3.0 The default threshold was changed from 1 to 3. * - * @param int $omit_threshold The number of media elements where the `loading` attribute will not be added. Default 1. + * @param int $omit_threshold The number of media elements where the `loading` attribute will not be added. Default 3. */ - $omit_threshold = apply_filters( 'wp_omit_loading_attr_threshold', 1 ); + $omit_threshold = apply_filters( 'wp_omit_loading_attr_threshold', 3 ); } return $omit_threshold; diff --git a/tests/phpunit/tests/formatting/WpTrimExcerpt.php b/tests/phpunit/tests/formatting/WpTrimExcerpt.php index c3ab336bf9..ba5e99f7d8 100644 --- a/tests/phpunit/tests/formatting/WpTrimExcerpt.php +++ b/tests/phpunit/tests/formatting/WpTrimExcerpt.php @@ -92,4 +92,60 @@ public function test_should_generate_excerpt_for_empty_values() { $this->assertSame( 'Post content', wp_trim_excerpt( null, $post ) ); $this->assertSame( 'Post content', wp_trim_excerpt( false, $post ) ); } + + /** + * Tests that `wp_trim_excerpt()` unhooks `wp_filter_content_tags()` from 'the_content' filter. + * + * @ticket 56588 + */ + public function test_wp_trim_excerpt_unhooks_wp_filter_content_tags() { + $post = self::factory()->post->create(); + + /* + * Record that during 'the_content' filter run by wp_trim_excerpt() the + * wp_filter_content_tags() callback is not used. + */ + $has_filter = true; + add_filter( + 'the_content', + static function ( $content ) use ( &$has_filter ) { + $has_filter = has_filter( 'the_content', 'wp_filter_content_tags' ); + return $content; + } + ); + + wp_trim_excerpt( '', $post ); + + $this->assertFalse( $has_filter, 'wp_filter_content_tags() was not unhooked in wp_trim_excerpt()' ); + } + + /** + * Tests that `wp_trim_excerpt()` doesn't permanently unhook `wp_filter_content_tags()` from 'the_content' filter. + * + * @ticket 56588 + */ + public function test_wp_trim_excerpt_should_not_permanently_unhook_wp_filter_content_tags() { + $post = self::factory()->post->create(); + + wp_trim_excerpt( '', $post ); + + $this->assertSame( 10, has_filter( 'the_content', 'wp_filter_content_tags' ), 'wp_filter_content_tags() was not restored in wp_trim_excerpt()' ); + } + + /** + * Tests that `wp_trim_excerpt()` doesn't restore `wp_filter_content_tags()` if it was previously unhooked. + * + * @ticket 56588 + */ + public function test_wp_trim_excerpt_does_not_restore_wp_filter_content_tags_if_previously_unhooked() { + $post = self::factory()->post->create(); + + // Remove wp_filter_content_tags() from 'the_content' filter generally. + remove_filter( 'the_content', 'wp_filter_content_tags' ); + + wp_trim_excerpt( '', $post ); + + // Assert that the filter callback was not restored after running 'the_content'. + $this->assertFalse( has_filter( 'the_content', 'wp_filter_content_tags' ) ); + } } diff --git a/tests/phpunit/tests/media.php b/tests/phpunit/tests/media.php index 0335e45d0e..e3273bf9de 100644 --- a/tests/phpunit/tests/media.php +++ b/tests/phpunit/tests/media.php @@ -75,6 +75,16 @@ public static function tear_down_after_class() { parent::tear_down_after_class(); } + /** + * Ensures that the static content media count and related filter are reset between tests. + */ + public function set_up() { + parent::set_up(); + + $this->reset_content_media_count(); + $this->reset_omit_loading_attr_filter(); + } + public function test_img_caption_shortcode_added() { global $shortcode_tags; $this->assertSame( 'img_caption_shortcode', $shortcode_tags['caption'] ); @@ -3349,8 +3359,6 @@ public function data_attachment_permalinks_based_on_parent_status() { * @param string $context */ public function test_wp_get_loading_attr_default( $context ) { - global $wp_query, $wp_the_query; - // Return 'lazy' by default. $this->assertSame( 'lazy', wp_get_loading_attr_default( 'test' ) ); $this->assertSame( 'lazy', wp_get_loading_attr_default( 'wp_get_attachment_image' ) ); @@ -3358,9 +3366,7 @@ public function test_wp_get_loading_attr_default( $context ) { // Return 'lazy' if not in the loop or the main query. $this->assertSame( 'lazy', wp_get_loading_attr_default( $context ) ); - $wp_query = new WP_Query( array( 'post__in' => array( self::$post_ids['publish'] ) ) ); - $this->reset_content_media_count(); - $this->reset_omit_loading_attr_filter(); + $query = $this->get_new_wp_query_for_published_post(); while ( have_posts() ) { the_post(); @@ -3369,14 +3375,16 @@ public function test_wp_get_loading_attr_default( $context ) { $this->assertSame( 'lazy', wp_get_loading_attr_default( $context ) ); // Set as main query. - $wp_the_query = $wp_query; + $this->set_main_query( $query ); // For contexts other than for the main content, still return 'lazy' even in the loop // and in the main query, and do not increase the content media count. $this->assertSame( 'lazy', wp_get_loading_attr_default( 'wp_get_attachment_image' ) ); - // Return `false` if in the loop and in the main query and it is the first element. - $this->assertFalse( wp_get_loading_attr_default( $context ) ); + // Return `false` in the main query for first three element. + $this->assertFalse( wp_get_loading_attr_default( $context ), 'Expected first image to not be lazy-loaded.' ); + $this->assertFalse( wp_get_loading_attr_default( $context ), 'Expected second image to not be lazy-loaded.' ); + $this->assertFalse( wp_get_loading_attr_default( $context ), 'Expected third image to not be lazy-loaded.' ); // Return 'lazy' if in the loop and in the main query for any subsequent elements. $this->assertSame( 'lazy', wp_get_loading_attr_default( $context ) ); @@ -3397,26 +3405,17 @@ public function data_wp_get_loading_attr_default() { * @ticket 53675 */ public function test_wp_omit_loading_attr_threshold_filter() { - global $wp_query, $wp_the_query; + $query = $this->get_new_wp_query_for_published_post(); + $this->set_main_query( $query ); - $wp_query = new WP_Query( array( 'post__in' => array( self::$post_ids['publish'] ) ) ); - $wp_the_query = $wp_query; - $this->reset_content_media_count(); - $this->reset_omit_loading_attr_filter(); - - // Use the filter to alter the threshold for not lazy-loading to the first three elements. - add_filter( - 'wp_omit_loading_attr_threshold', - function () { - return 3; - } - ); + // Use the filter to alter the threshold for not lazy-loading to the first five elements. + $this->force_omit_loading_attr_threshold( 5 ); while ( have_posts() ) { the_post(); - // Due to the filter, now the first three elements should not be lazy-loaded, i.e. return `false`. - for ( $i = 0; $i < 3; $i++ ) { + // Due to the filter, now the first five elements should not be lazy-loaded, i.e. return `false`. + for ( $i = 0; $i < 5; $i++ ) { $this->assertFalse( wp_get_loading_attr_default( 'the_content' ) ); } @@ -3429,8 +3428,6 @@ function () { * @ticket 53675 */ public function test_wp_filter_content_tags_with_wp_get_loading_attr_default() { - global $wp_query, $wp_the_query; - $img1 = get_image_tag( self::$large_id, '', '', '', 'large' ); $iframe1 = ''; $img2 = get_image_tag( self::$large_id, '', '', '', 'medium' ); @@ -3441,22 +3438,15 @@ public function test_wp_filter_content_tags_with_wp_get_loading_attr_default() { $lazy_iframe2 = wp_iframe_tag_add_loading_attr( $iframe2, 'the_content' ); // Use a threshold of 2. - add_filter( - 'wp_omit_loading_attr_threshold', - function () { - return 2; - } - ); + $this->force_omit_loading_attr_threshold( 2 ); // Following the threshold of 2, the first two content media elements should not be lazy-loaded. $content_unfiltered = $img1 . $iframe1 . $img2 . $img3 . $iframe2; $content_expected = $img1 . $iframe1 . $lazy_img2 . $lazy_img3 . $lazy_iframe2; $content_expected = wp_img_tag_add_decoding_attr( $content_expected, 'the_content' ); - $wp_query = new WP_Query( array( 'post__in' => array( self::$post_ids['publish'] ) ) ); - $wp_the_query = $wp_query; - $this->reset_content_media_count(); - $this->reset_omit_loading_attr_filter(); + $query = $this->get_new_wp_query_for_published_post(); + $this->set_main_query( $query ); while ( have_posts() ) { the_post(); @@ -3476,24 +3466,332 @@ function () { public function test_wp_omit_loading_attr_threshold() { $this->reset_omit_loading_attr_filter(); - // Apply filter, ensure default value of 1. + // Apply filter, ensure default value of 3. + $omit_threshold = wp_omit_loading_attr_threshold(); + $this->assertSame( 3, $omit_threshold ); + + // Add a filter that changes the value to 1. However, the filter is not applied a subsequent time in a single + // page load by default, so the value is still 3. + $this->force_omit_loading_attr_threshold( 1 ); + $omit_threshold = wp_omit_loading_attr_threshold(); + $this->assertSame( 3, $omit_threshold ); + + // Only by enforcing a fresh check, the filter gets re-applied. + $omit_threshold = wp_omit_loading_attr_threshold( true ); $this->assertSame( 1, $omit_threshold ); + } + + /** + * Tests that wp_get_loading_attr_default() returns the expected loading attribute value before loop but after get_header if not main query. + * + * @ticket 58211 + * + * @covers ::wp_get_loading_attr_default + * + * @dataProvider data_wp_get_loading_attr_default_before_and_no_loop + * + * @param string $context Context for the element for which the `loading` attribute value is requested. + */ + public function test_wp_get_loading_attr_default_before_loop_if_not_main_query( $context ) { + global $wp_query; + + $wp_query = $this->get_new_wp_query_for_published_post(); + + do_action( 'get_header' ); + + // Lazy if not main query. + $this->assertSame( 'lazy', wp_get_loading_attr_default( $context ) ); + } - // Add a filter that changes the value to 3. However, the filter is not applied a subsequent time in a single - // page load by default, so the value is still 1. + /** + * Tests that wp_get_loading_attr_default() returns the expected loading attribute value before loop but after get_header in main query but header was not called. + * + * @ticket 58211 + * + * @covers ::wp_get_loading_attr_default + * + * @dataProvider data_wp_get_loading_attr_default_before_and_no_loop + * + * @param string $context Context for the element for which the `loading` attribute value is requested. + */ + public function test_wp_get_loading_attr_default_before_loop_in_main_query_but_header_not_called( $context ) { + global $wp_query; + + $wp_query = $this->get_new_wp_query_for_published_post(); + $this->set_main_query( $wp_query ); + + // Lazy if header not called. + $this->assertSame( 'lazy', wp_get_loading_attr_default( $context ) ); + } + + /** + * Tests that wp_get_loading_attr_default() returns the expected loading attribute value before loop but after get_header for main query. + * + * @ticket 58211 + * + * @covers ::wp_get_loading_attr_default + * + * @dataProvider data_wp_get_loading_attr_default_before_and_no_loop + * + * @param string $context Context for the element for which the `loading` attribute value is requested. + */ + public function test_wp_get_loading_attr_default_before_loop_if_main_query( $context ) { + global $wp_query; + + $wp_query = $this->get_new_wp_query_for_published_post(); + $this->set_main_query( $wp_query ); + + do_action( 'get_header' ); + $this->assertFalse( wp_get_loading_attr_default( $context ) ); + } + + /** + * Tests that wp_get_loading_attr_default() returns the expected loading attribute value after get_header and after loop. + * + * @ticket 58211 + * + * @covers ::wp_get_loading_attr_default + * + * @dataProvider data_wp_get_loading_attr_default_before_and_no_loop + * + * @param string $context Context for the element for which the `loading` attribute value is requested. + */ + public function test_wp_get_loading_attr_default_after_loop( $context ) { + global $wp_query; + + $wp_query = $this->get_new_wp_query_for_published_post(); + $this->set_main_query( $wp_query ); + + do_action( 'get_header' ); + + while ( have_posts() ) { + the_post(); + } + $this->assertSame( 'lazy', wp_get_loading_attr_default( $context ) ); + } + + /** + * Tests that wp_get_loading_attr_default() returns the expected loading attribute if no loop. + * + * @ticket 58211 + * + * @covers ::wp_get_loading_attr_default + * + * @dataProvider data_wp_get_loading_attr_default_before_and_no_loop + * + * @param string $context Context for the element for which the `loading` attribute value is requested. + */ + public function test_wp_get_loading_attr_default_no_loop( $context ) { + global $wp_query; + + $wp_query = $this->get_new_wp_query_for_published_post(); + $this->set_main_query( $wp_query ); + + // Ensure header and footer is called. + do_action( 'get_header' ); + do_action( 'get_footer' ); + + // Load lazy if the there is no loop and footer was called. + $this->assertSame( 'lazy', wp_get_loading_attr_default( $context ) ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_wp_get_loading_attr_default_before_and_no_loop() { + return array( + array( 'wp_get_attachment_image' ), + array( 'the_post_thumbnail' ), + ); + } + + /** + * @ticket 58089 + * + * @covers ::wp_filter_content_tags + * @covers ::wp_get_loading_attr_default + */ + public function test_wp_filter_content_tags_does_not_lazy_load_special_images_within_the_content() { + global $wp_query, $wp_the_query; + + // Force no lazy-loading on the image tag expected in the content. + $expected_content = wpautop( wp_get_attachment_image( self::$large_id, 'large', false, array( 'loading' => false ) ) ); + + // Overwrite post content with an image. add_filter( - 'wp_omit_loading_attr_threshold', - function () { - return 3; + 'the_content', + static function () { + // Replace content with an image tag, i.e. the 'wp_get_attachment_image' context is used while running 'the_content' filter. + return wp_get_attachment_image( self::$large_id, 'large', false ); + }, + 9 // Run before wp_filter_content_tags(). + ); + + /* + * We have to run a main query loop so that the first 'the_content' context image is not + * lazy-loaded. + * Without the fix from 58089, the image would still be lazy-loaded since the check for the + * separately invoked 'wp_get_attachment_image' context would lead to that. + */ + $wp_query = new WP_Query( array( 'post__in' => array( self::$post_ids['publish'] ) ) ); + $wp_the_query = $wp_query; + + $content = ''; + while ( have_posts() ) { + the_post(); + $content = get_echo( 'the_content' ); + } + + // Ensure that parsed content has the image without lazy-loading. + $this->assertSame( $expected_content, $content ); + } + + /** + * Tests that wp_get_loading_attr_default() returns 'lazy' for special contexts when they're used outside of 'the_content' filter. + * + * @ticket 58089 + * + * @covers ::wp_get_loading_attr_default + * + * @dataProvider data_special_contexts_for_the_content + * + * @param string $context Context for the element for which the `loading` attribute value is requested. + */ + public function test_wp_get_loading_attr_default_should_return_lazy_for_special_contexts_outside_of_the_content( $context ) { + $this->assertSame( 'lazy', wp_get_loading_attr_default( $context ) ); + } + + /** + * Tests that wp_get_loading_attr_default() returns false for special contexts when they're used within 'the_content' filter. + * + * @ticket 58089 + * + * @covers ::wp_get_loading_attr_default + * + * @dataProvider data_special_contexts_for_the_content + * + * @param string $context Context for the element for which the `loading` attribute value is requested. + */ + public function test_wp_get_loading_attr_default_should_return_false_for_special_contexts_within_the_content( $context ) { + remove_all_filters( 'the_content' ); + + $result = null; + add_filter( + 'the_content', + function ( $content ) use ( &$result, $context ) { + $result = wp_get_loading_attr_default( $context ); + return $content; } ); - $omit_threshold = wp_omit_loading_attr_threshold(); - $this->assertSame( 1, $omit_threshold ); + apply_filters( 'the_content', '' ); + $this->assertFalse( $result ); + } - // Only by enforcing a fresh check, the filter gets re-applied. - $omit_threshold = wp_omit_loading_attr_threshold( true ); - $this->assertSame( 3, $omit_threshold ); + /** + * Data provider. + * + * @return array[] + */ + public function data_special_contexts_for_the_content() { + return array( + 'the_post_thumbnail' => array( 'context' => 'the_post_thumbnail' ), + 'wp_get_attachment_image' => array( 'context' => 'wp_get_attachment_image' ), + ); + } + + /** + * Tests that the content media count is not affected by `the_excerpt()` calls for posts that contain images. + * + * @ticket 56588 + * + * @covers ::wp_trim_excerpt + */ + public function test_the_excerpt_does_not_affect_content_media_count() { + global $wp_query, $wp_the_query; + + /* + * Use the filter to alter the threshold for not lazy-loading to the first 2 elements, + * then use a post that contains exactly 2 images. + */ + $this->force_omit_loading_attr_threshold( 2 ); + $post_content = ''; + $post_content .= '

Some text.

'; + $post_content .= ''; + + $post_id = self::factory()->post->create( + array( + 'post_content' => $post_content, + 'post_excerpt' => '', + ) + ); + + $wp_query = new WP_Query( array( 'post__in' => array( $post_id ) ) ); + $wp_the_query = $wp_query; + + while ( have_posts() ) { + the_post(); + + // Call `the_excerpt()` without generating output. + get_echo( 'the_excerpt' ); + } + + // The only way to access the value is by calling this function without increasing the value. + $content_media_count = wp_increase_content_media_count( 0 ); + + // Assert that the media count was not increased even though there are 3 images in the post's content. + $this->assertSame( 0, $content_media_count ); + } + + /** + * Tests that the lazy-loading result is not affected by `the_excerpt()` calls for posts that + * contain images. + * + * Printing the excerpt for a post that contains images in its content prior to its featured image should result in + * that featured image not being lazy-loaded, since the images in the post content aren't displayed in the excerpt. + * + * @ticket 56588 + * + * @covers ::wp_trim_excerpt + */ + public function test_the_excerpt_does_not_affect_omit_lazy_loading_logic() { + global $wp_query, $wp_the_query; + + /* + * Use the filter to alter the threshold for not lazy-loading to the first 2 elements, + * then use a post that contains exactly 2 images. + */ + $this->force_omit_loading_attr_threshold( 2 ); + $post_content = ''; + $post_content .= '

Some text.

'; + $post_content .= ''; + + $post_id = self::factory()->post->create( + array( + 'post_content' => $post_content, + 'post_excerpt' => '', + ) + ); + $featured_image_id = self::$large_id; + update_post_meta( $post_id, '_thumbnail_id', $featured_image_id ); + + $expected_image_tag = get_the_post_thumbnail( $post_id, 'post-thumbnail', array( 'loading' => false ) ); + + $wp_query = new WP_Query( array( 'post__in' => array( $post_id ) ) ); + $wp_the_query = $wp_query; + + $output = ''; + while ( have_posts() ) { + the_post(); + + // Print excerpt first, then the featured image. + $output .= get_echo( 'the_excerpt' ); + $output .= get_echo( 'the_post_thumbnail' ); + } + + $this->assertStringContainsString( $expected_image_tag, $output ); } private function reset_content_media_count() { @@ -3644,6 +3942,48 @@ public function image_editor_change_quality_low_jpeg( $quality, $mime_type ) { return 30; } } + + /** + * Change the omit loading attribute threshold value. + * + * @param int $threshold Threshold value to change. + */ + public function force_omit_loading_attr_threshold( $threshold ) { + add_filter( + 'wp_omit_loading_attr_threshold', + static function () use ( $threshold ) { + return $threshold; + } + ); + } + + /** + * Returns a new WP_Query. + * + * @global WP_Query $wp_query WordPress Query object. + * + * @return WP_Query a new query. + */ + public function get_new_wp_query_for_published_post() { + global $wp_query; + + // New query to $wp_query. update global for the loop. + $wp_query = new WP_Query( array( 'post__in' => array( self::$post_ids['publish'] ) ) ); + + return $wp_query; + } + + /** + * Sets a query as main query. + * + * @global WP_Query $wp_the_query WordPress Query object. + * + * @param WP_Query $query query to be set as main query. + */ + public function set_main_query( $query ) { + global $wp_the_query; + $wp_the_query = $query; + } } /** diff --git a/tests/phpunit/tests/query.php b/tests/phpunit/tests/query.php index f04139c085..37dd51a063 100644 --- a/tests/phpunit/tests/query.php +++ b/tests/phpunit/tests/query.php @@ -897,4 +897,71 @@ public function test_query_tag_404_does_not_throw_warning() { $this->assertFalse( $q->is_tax() ); $this->assertFalse( $q->is_tag( 'non-existent-tag' ) ); } + + /** + * Test if $before_loop is true before loop. + * + * @ticket 58211 + */ + public function test_before_loop_value_set_true_before_the_loop() { + // Get a new query with 3 posts. + $query = $this->get_new_wp_query_with_posts( 3 ); + + $this->assertTrue( $query->before_loop ); + } + + /** + * Test $before_loop value is set to false when the loop starts. + * + * @ticket 58211 + * + * @covers WP_Query::the_post + */ + public function test_before_loop_value_set_to_false_in_loop_with_post() { + // Get a new query with 2 posts. + $query = $this->get_new_wp_query_with_posts( 2 ); + + while ( $query->have_posts() ) { + // $before_loop should be set false as soon as the_post is called for the first time. + $query->the_post(); + + $this->assertFalse( $query->before_loop ); + break; + } + } + + /** + * Test $before_loop value is set to false when there is no post in the loop. + * + * @ticket 58211 + * + * @covers WP_Query::have_posts + */ + public function test_before_loop_set_false_after_loop_with_no_post() { + // New query without any posts in the result. + $query = new WP_Query( + array( + 'category_name' => 'non-existent-category', + ) + ); + + // There will not be any posts, so the loop will never actually enter. + while ( $query->have_posts() ) { + $query->the_post(); + } + + // Still, this should be false as there are no results and entering the loop was attempted. + $this->assertFalse( $query->before_loop ); + } + + /** + * Get a new query with a given number of posts. + * + * @param int $no_of_posts Number of posts to be added in the query. + */ + public function get_new_wp_query_with_posts( $no_of_posts ) { + $post_ids = self::factory()->post->create_many( $no_of_posts ); + $query = new WP_Query( array( 'post__in' => $post_ids ) ); + return $query; + } }