diff --git a/src/wp-includes/block-supports/block-visibility.php b/src/wp-includes/block-supports/block-visibility.php index 756e0500418f4..1ba208e30c92e 100644 --- a/src/wp-includes/block-supports/block-visibility.php +++ b/src/wp-includes/block-supports/block-visibility.php @@ -139,6 +139,16 @@ function wp_render_block_visibility_support( $block_content, $block ) { $processor = new WP_HTML_Tag_Processor( $block_content ); if ( $processor->next_tag() ) { $processor->add_class( implode( ' ', $class_names ) ); + + /* + * Set all IMG tags to be `fetchpriority=auto` so that wp_get_loading_optimization_attributes() won't add + * `fetchpriority=high` or increment the media count to affect whether subsequent IMG tags get `loading=lazy`. + */ + do { + if ( 'IMG' === $processor->get_tag() ) { + $processor->set_attribute( 'fetchpriority', 'auto' ); + } + } while ( $processor->next_tag() ); $block_content = $processor->get_updated_html(); } } diff --git a/src/wp-includes/html-api/class-wp-html-tag-processor.php b/src/wp-includes/html-api/class-wp-html-tag-processor.php index 9289d5d27f880..148bbc0a471a8 100644 --- a/src/wp-includes/html-api/class-wp-html-tag-processor.php +++ b/src/wp-includes/html-api/class-wp-html-tag-processor.php @@ -881,6 +881,8 @@ public function change_parsing_namespace( string $new_namespace ): bool { * @type string|null $tag_closers "visit" or "skip": whether to stop on tag closers, e.g. . * } * @return bool Whether a tag was matched. + * + * @phpstan-impure */ public function next_tag( $query = null ): bool { $this->parse_query( $query ); diff --git a/src/wp-includes/media.php b/src/wp-includes/media.php index bfd2e58487429..f9f35f6dbb087 100644 --- a/src/wp-includes/media.php +++ b/src/wp-includes/media.php @@ -5967,6 +5967,7 @@ function wp_get_webp_info( $filename ) { * both attributes are present with those values. * * @since 6.3.0 + * @since 7.0.0 Support `fetchpriority=low` and `fetchpriority=auto` so that `loading=lazy` is not added and the media count is not increased. * * @global WP_Query $wp_query WordPress Query object. * @@ -6067,7 +6068,9 @@ function wp_get_loading_optimization_attributes( $tag_name, $attr, $context ) { } // Logic to handle a `fetchpriority` attribute that is already provided. - if ( isset( $attr['fetchpriority'] ) && 'high' === $attr['fetchpriority'] ) { + $existing_fetchpriority = ( $attr['fetchpriority'] ?? null ); + $is_low_fetchpriority = ( 'low' === $existing_fetchpriority ); + if ( 'high' === $existing_fetchpriority ) { /* * If the image was already determined to not be in the viewport (e.g. * from an already provided `loading` attribute), trigger a warning. @@ -6090,6 +6093,31 @@ function wp_get_loading_optimization_attributes( $tag_name, $attr, $context ) { } else { $maybe_in_viewport = true; } + } elseif ( $is_low_fetchpriority ) { + /* + * An IMG with fetchpriority=low is not initially displayed; it may be hidden in the Navigation Overlay, + * or it may be occluded in a non-initial carousel slide. Such images must not be lazy-loaded because the browser + * has no heuristic to know when to start loading them before the user needs to see them. + */ + $maybe_in_viewport = false; + + // Preserve fetchpriority=low. + $loading_attrs['fetchpriority'] = 'low'; + } elseif ( 'auto' === $existing_fetchpriority ) { + /* + * When a block's visibility support identifies that the block is conditionally displayed based on the viewport + * size, then it adds `fetchpriority=auto` to the block's IMG tags. These images must not be fetched with high + * priority because they could be erroneously loaded in viewports which do not even display them. Contrarily, + * they must not get `fetchpriority=low` because they may in fact be displayed in the current viewport. So as + * a signal to indicate that an IMG may be in the viewport, `fetchpriority=auto` is added. This has the effect + * here of preventing the media count from being increased, so that images hidden with block visibility do not + * affect whether a following IMG gets `loading=lazy`. In particular, `loading=lazy` should still be omitted + * on an IMG following any number of initial IMGs with `fetchpriority=auto` since those initial images may not + * be displayed. + */ + + // Preserve fetchpriority=auto. + $loading_attrs['fetchpriority'] = 'auto'; } if ( null === $maybe_in_viewport ) { @@ -6140,7 +6168,7 @@ function wp_get_loading_optimization_attributes( $tag_name, $attr, $context ) { * does not include any loop. */ && did_action( 'get_header' ) && ! did_action( 'get_footer' ) - ) { + ) { $maybe_in_viewport = true; $maybe_increase_count = true; } @@ -6149,12 +6177,14 @@ function wp_get_loading_optimization_attributes( $tag_name, $attr, $context ) { /* * If the element is in the viewport (`true`), potentially add * `fetchpriority` with a value of "high". Otherwise, i.e. if the element - * is not not in the viewport (`false`) or it is unknown (`null`), add - * `loading` with a value of "lazy". + * is not in the viewport (`false`) or it is unknown (`null`), add + * `loading` with a value of "lazy" if the element is not already being + * de-prioritized with `fetchpriority=low` due to occlusion in + * Navigation Overlay, non-initial carousel slides, or a collapsed Details block. */ if ( $maybe_in_viewport ) { $loading_attrs = wp_maybe_add_fetchpriority_high_attr( $loading_attrs, $tag_name, $attr ); - } else { + } elseif ( ! $is_low_fetchpriority ) { // Only add `loading="lazy"` if the feature is enabled. if ( wp_lazy_loading_enabled( $tag_name, $context ) ) { $loading_attrs['loading'] = 'lazy'; @@ -6164,16 +6194,20 @@ function wp_get_loading_optimization_attributes( $tag_name, $attr, $context ) { /* * If flag was set based on contextual logic above, increase the content * media count, either unconditionally, or based on whether the image size - * is larger than the threshold. + * is larger than the threshold. This does not apply when the IMG has + * fetchpriority=auto because it may be conditionally displayed by viewport + * size. */ - if ( $increase_count ) { - wp_increase_content_media_count(); - } elseif ( $maybe_increase_count ) { - /** This filter is documented in wp-includes/media.php */ - $wp_min_priority_img_pixels = apply_filters( 'wp_min_priority_img_pixels', 50000 ); - - if ( $wp_min_priority_img_pixels <= $attr['width'] * $attr['height'] ) { + if ( 'auto' !== $existing_fetchpriority ) { + if ( $increase_count ) { wp_increase_content_media_count(); + } elseif ( $maybe_increase_count ) { + /** This filter is documented in wp-includes/media.php */ + $wp_min_priority_img_pixels = apply_filters( 'wp_min_priority_img_pixels', 50000 ); + + if ( $wp_min_priority_img_pixels <= $attr['width'] * $attr['height'] ) { + wp_increase_content_media_count(); + } } } @@ -6245,12 +6279,13 @@ function wp_increase_content_media_count( $amount = 1 ) { * Determines whether to add `fetchpriority='high'` to loading attributes. * * @since 6.3.0 + * @since 7.0.0 Support is added for IMG tags with `fetchpriority='low'` and `fetchpriority='auto'`. * @access private * - * @param array $loading_attrs Array of the loading optimization attributes for the element. - * @param string $tag_name The tag name. - * @param array $attr Array of the attributes for the element. - * @return array Updated loading optimization attributes for the element. + * @param array $loading_attrs Array of the loading optimization attributes for the element. + * @param string $tag_name The tag name. + * @param array $attr Array of the attributes for the element. + * @return array Updated loading optimization attributes for the element. */ function wp_maybe_add_fetchpriority_high_attr( $loading_attrs, $tag_name, $attr ) { // For now, adding `fetchpriority="high"` is only supported for images. @@ -6258,14 +6293,17 @@ function wp_maybe_add_fetchpriority_high_attr( $loading_attrs, $tag_name, $attr return $loading_attrs; } - if ( isset( $attr['fetchpriority'] ) ) { + $existing_fetchpriority = $attr['fetchpriority'] ?? null; + if ( null !== $existing_fetchpriority && 'auto' !== $existing_fetchpriority ) { /* - * While any `fetchpriority` value could be set in `$loading_attrs`, - * for consistency we only do it for `fetchpriority="high"` since that - * is the only possible value that WordPress core would apply on its - * own. + * When an IMG has been explicitly marked with `fetchpriority=high`, then honor that this is the element that + * should have the priority. In contrast, the Navigation block may add `fetchpriority=low` to an IMG which + * appears in the Navigation Overlay; such images should never be considered candidates for + * `fetchpriority=high`. Lastly, block visibility may add `fetchpriority=auto` to an IMG when the block is + * conditionally displayed based on viewport size. Such an image is considered an LCP element candidate if it + * exceeds the threshold for the minimum number of square pixels. */ - if ( 'high' === $attr['fetchpriority'] ) { + if ( 'high' === $existing_fetchpriority ) { $loading_attrs['fetchpriority'] = 'high'; wp_high_priority_element_flag( false ); } @@ -6292,7 +6330,9 @@ function wp_maybe_add_fetchpriority_high_attr( $loading_attrs, $tag_name, $attr $wp_min_priority_img_pixels = apply_filters( 'wp_min_priority_img_pixels', 50000 ); if ( $wp_min_priority_img_pixels <= $attr['width'] * $attr['height'] ) { - $loading_attrs['fetchpriority'] = 'high'; + if ( 'auto' !== $existing_fetchpriority ) { + $loading_attrs['fetchpriority'] = 'high'; + } wp_high_priority_element_flag( false ); } @@ -6306,9 +6346,9 @@ function wp_maybe_add_fetchpriority_high_attr( $loading_attrs, $tag_name, $attr * @access private * * @param bool $value Optional. Used to change the static variable. Default null. - * @return bool Returns true if high-priority element was marked already, otherwise false. + * @return bool Returns true if the high-priority element was not already marked. */ -function wp_high_priority_element_flag( $value = null ) { +function wp_high_priority_element_flag( $value = null ): bool { static $high_priority_element = true; if ( is_bool( $value ) ) { diff --git a/tests/phpunit/tests/block-supports/block-visibility.php b/tests/phpunit/tests/block-supports/block-visibility.php index dd116472ba1f4..4f24b931744f8 100644 --- a/tests/phpunit/tests/block-supports/block-visibility.php +++ b/tests/phpunit/tests/block-supports/block-visibility.php @@ -11,18 +11,18 @@ * @covers ::wp_render_block_visibility_support */ class Tests_Block_Supports_Block_Visibility extends WP_UnitTestCase { - /** - * @var string|null - */ - private $test_block_name; - public function set_up() { + private ?string $test_block_name; + + public function set_up(): void { parent::set_up(); $this->test_block_name = null; } - public function tear_down() { - unregister_block_type( $this->test_block_name ); + public function tear_down(): void { + if ( $this->test_block_name ) { + unregister_block_type( $this->test_block_name ); + } $this->test_block_name = null; parent::tear_down(); } @@ -30,12 +30,10 @@ public function tear_down() { /** * Registers a new block for testing block visibility support. * - * @param string $block_name Name for the test block. - * @param array $supports Array defining block support configuration. - * - * @return WP_Block_Type The block type for the newly registered test block. + * @param string $block_name Name for the test block. + * @param array $supports Array defining block support configuration. */ - private function register_visibility_block_with_support( $block_name, $supports = array() ) { + private function register_visibility_block_with_support( string $block_name, array $supports = array() ): void { $this->test_block_name = $block_name; register_block_type( $this->test_block_name, @@ -51,7 +49,7 @@ private function register_visibility_block_with_support( $block_name, $supports ); $registry = WP_Block_Type_Registry::get_instance(); - return $registry->get_registered( $this->test_block_name ); + $registry->get_registered( $this->test_block_name ); } /** @@ -60,7 +58,7 @@ private function register_visibility_block_with_support( $block_name, $supports * * @ticket 64061 */ - public function test_block_visibility_support_hides_block_when_visibility_false() { + public function test_block_visibility_support_hides_block_when_visibility_false(): void { $this->register_visibility_block_with_support( 'test/visibility-block', array( 'visibility' => true ) @@ -87,13 +85,13 @@ public function test_block_visibility_support_hides_block_when_visibility_false( * * @ticket 64061 */ - public function test_block_visibility_support_shows_block_when_support_not_opted_in() { + public function test_block_visibility_support_shows_block_when_support_not_opted_in(): void { $this->register_visibility_block_with_support( 'test/visibility-block', array( 'visibility' => false ) ); - $block_content = '

This is a test block.

'; + $block_content = '
Test content
'; $block = array( 'blockName' => 'test/visibility-block', 'attrs' => array( @@ -108,10 +106,10 @@ public function test_block_visibility_support_shows_block_when_support_not_opted $this->assertSame( $block_content, $result, 'Block content should remain unchanged when blockVisibility support is not opted in.' ); } - /* + /** * @ticket 64414 */ - public function test_block_visibility_support_no_visibility_attribute() { + public function test_block_visibility_support_no_visibility_attribute(): void { $this->register_visibility_block_with_support( 'test/block-visibility-none', array( 'visibility' => true ) @@ -122,16 +120,16 @@ public function test_block_visibility_support_no_visibility_attribute() { 'attrs' => array(), ); - $block_content = '
Test content
'; + $block_content = '
Test content
'; $result = wp_render_block_visibility_support( $block_content, $block ); $this->assertSame( $block_content, $result, 'Block content should remain unchanged when no visibility attribute is present.' ); } - /* + /** * @ticket 64414 */ - public function test_block_visibility_support_generated_css_with_mobile_viewport_size() { + public function test_block_visibility_support_generated_css_with_mobile_viewport_size(): void { $this->register_visibility_block_with_support( 'test/viewport-mobile', array( 'visibility' => true ) @@ -150,7 +148,7 @@ public function test_block_visibility_support_generated_css_with_mobile_viewport ), ); - $block_content = '
Test content
'; + $block_content = '
Test content
'; $result = wp_render_block_visibility_support( $block_content, $block ); $this->assertStringContainsString( 'wp-block-hidden-mobile', $result, 'Block should have the visibility class for the mobile breakpoint.' ); @@ -164,10 +162,10 @@ public function test_block_visibility_support_generated_css_with_mobile_viewport ); } - /* + /** * @ticket 64414 */ - public function test_block_visibility_support_generated_css_with_tablet_viewport_size() { + public function test_block_visibility_support_generated_css_with_tablet_viewport_size(): void { $this->register_visibility_block_with_support( 'test/viewport-tablet', array( 'visibility' => true ) @@ -186,10 +184,15 @@ public function test_block_visibility_support_generated_css_with_tablet_viewport ), ); - $block_content = '
Test content
'; + $block_content = '
Test content
'; $result = wp_render_block_visibility_support( $block_content, $block ); - $this->assertStringContainsString( 'class="existing-class wp-block-hidden-tablet"', $result, 'Block should have the existing class and the visibility class for the tablet breakpoint in the class attribute.' ); + $this->assertEqualHTML( + '
Test content
', + $result, + '', + 'Block should have the existing class and the visibility class for the tablet breakpoint in the class attribute.' + ); $actual_stylesheet = wp_style_engine_get_stylesheet_from_context( 'block-supports', array( 'prettify' => false ) ); @@ -200,10 +203,10 @@ public function test_block_visibility_support_generated_css_with_tablet_viewport ); } - /* + /** * @ticket 64414 */ - public function test_block_visibility_support_generated_css_with_desktop_breakpoint() { + public function test_block_visibility_support_generated_css_with_desktop_breakpoint(): void { $this->register_visibility_block_with_support( 'test/viewport-desktop', array( 'visibility' => true ) @@ -222,10 +225,15 @@ public function test_block_visibility_support_generated_css_with_desktop_breakpo ), ); - $block_content = '
Test content
'; + $block_content = '
Test content
'; $result = wp_render_block_visibility_support( $block_content, $block ); - $this->assertStringContainsString( 'class="wp-block-hidden-desktop"', $result, 'Block should have the visibility class for the desktop breakpoint in the class attribute.' ); + $this->assertEqualHTML( + '
Test content
', + $result, + '', + 'Block should have the visibility class for the desktop breakpoint in the class attribute.' + ); $actual_stylesheet = wp_style_engine_get_stylesheet_from_context( 'block-supports', array( 'prettify' => false ) ); @@ -236,10 +244,11 @@ public function test_block_visibility_support_generated_css_with_desktop_breakpo ); } - /* + /** * @ticket 64414 + * @ticket 64823 */ - public function test_block_visibility_support_generated_css_with_two_viewport_sizes() { + public function test_block_visibility_support_generated_css_with_two_viewport_sizes(): void { $this->register_visibility_block_with_support( 'test/viewport-two', array( 'visibility' => true ) @@ -259,13 +268,14 @@ public function test_block_visibility_support_generated_css_with_two_viewport_si ), ); - $block_content = '
Test content
'; + $block_content = '
Test content
'; $result = wp_render_block_visibility_support( $block_content, $block ); - $this->assertStringContainsString( - 'class="wp-block-hidden-desktop wp-block-hidden-mobile"', + $this->assertEqualHTML( + '
Test content
', $result, - 'Block should have both visibility classes in the class attribute' + '', + 'Block should have both visibility classes in the class attribute, and the IMG should have fetchpriority=auto.' ); $actual_stylesheet = wp_style_engine_get_stylesheet_from_context( 'block-supports', array( 'prettify' => false ) ); @@ -277,10 +287,11 @@ public function test_block_visibility_support_generated_css_with_two_viewport_si ); } - /* + /** * @ticket 64414 + * @ticket 64823 */ - public function test_block_visibility_support_generated_css_with_all_viewport_sizes_visible() { + public function test_block_visibility_support_generated_css_with_all_viewport_sizes_visible(): void { $this->register_visibility_block_with_support( 'test/viewport-all-visible', array( 'visibility' => true ) @@ -301,16 +312,17 @@ public function test_block_visibility_support_generated_css_with_all_viewport_si ), ); - $block_content = '
Test content
'; + $block_content = '
Test content
'; $result = wp_render_block_visibility_support( $block_content, $block ); $this->assertSame( $block_content, $result, 'Block content should remain unchanged when all breakpoints are visible.' ); } - /* + /** * @ticket 64414 + * @ticket 64823 */ - public function test_block_visibility_support_generated_css_with_all_viewport_sizes_hidden() { + public function test_block_visibility_support_generated_css_with_all_viewport_sizes_hidden(): void { $this->register_visibility_block_with_support( 'test/viewport-all-hidden', array( 'visibility' => true ) @@ -331,16 +343,21 @@ public function test_block_visibility_support_generated_css_with_all_viewport_si ), ); - $block_content = '
Test content
'; + $block_content = '
Test content
'; $result = wp_render_block_visibility_support( $block_content, $block ); - $this->assertSame( '
Test content
', $result, 'Block content should have the visibility classes for all viewport sizes in the class attribute.' ); + $this->assertEqualHTML( + '
Test content
', + $result, + '', + 'Block content should have the visibility classes for all viewport sizes in the class attribute, and an IMG should get fetchpriority=auto.' + ); } - /* + /** * @ticket 64414 */ - public function test_block_visibility_support_generated_css_with_empty_object() { + public function test_block_visibility_support_generated_css_with_empty_object(): void { $this->register_visibility_block_with_support( 'test/viewport-empty', array( 'visibility' => true ) @@ -355,16 +372,16 @@ public function test_block_visibility_support_generated_css_with_empty_object() ), ); - $block_content = '
Test content
'; + $block_content = '
Test content
'; $result = wp_render_block_visibility_support( $block_content, $block ); $this->assertSame( $block_content, $result, 'Block content should remain unchanged when blockVisibility is an empty array.' ); } - /* + /** * @ticket 64414 */ - public function test_block_visibility_support_generated_css_with_unknown_viewport_sizes_ignored() { + public function test_block_visibility_support_generated_css_with_unknown_viewport_sizes_ignored(): void { $this->register_visibility_block_with_support( 'test/viewport-unknown-viewport-sizes', array( 'visibility' => true ) @@ -385,7 +402,7 @@ public function test_block_visibility_support_generated_css_with_unknown_viewpor ), ); - $block_content = '
Test content
'; + $block_content = '
Test content
'; $result = wp_render_block_visibility_support( $block_content, $block ); $this->assertStringContainsString( @@ -395,10 +412,10 @@ public function test_block_visibility_support_generated_css_with_unknown_viewpor ); } - /* + /** * @ticket 64414 */ - public function test_block_visibility_support_generated_css_with_empty_content() { + public function test_block_visibility_support_generated_css_with_empty_content(): void { $this->register_visibility_block_with_support( 'test/viewport-empty-content', array( 'visibility' => true ) diff --git a/tests/phpunit/tests/media.php b/tests/phpunit/tests/media.php index 0822de252d1b7..060a7295f6bb4 100644 --- a/tests/phpunit/tests/media.php +++ b/tests/phpunit/tests/media.php @@ -3916,7 +3916,10 @@ public function test_wp_get_loading_attr_default( $context ) { $this->assertFalse( wp_get_loading_attr_default( 'template_part_' . WP_TEMPLATE_PART_AREA_HEADER ), 'Images in the footer block template part should not be lazy-loaded.' ); } - public function data_wp_get_loading_attr_default() { + /** + * @return array + */ + public function data_wp_get_loading_attr_default(): array { return array( array( 'the_content' ), array( 'the_post_thumbnail' ), @@ -4481,7 +4484,7 @@ public function data_special_contexts_for_the_content_wp_get_loading_attr_defaul } /** - * Tests that wp_get_loading_attr_default() returns the expected loading attribute value. + * Tests that wp_get_loading_optimization_attributes() returns the expected loading attribute value. * * @ticket 53675 * @ticket 56930 @@ -4494,7 +4497,7 @@ public function data_special_contexts_for_the_content_wp_get_loading_attr_defaul * * @param string $context */ - public function test_wp_get_loading_optimization_attributes( $context ) { + public function test_wp_get_loading_optimization_attributes( string $context ): void { $attr = $this->get_width_height_for_high_priority(); // Return 'lazy' by default. @@ -4513,6 +4516,8 @@ public function test_wp_get_loading_optimization_attributes( $context ) { wp_get_loading_optimization_attributes( 'img', $attr, 'wp_get_attachment_image' ) ); + $this->assert_fetchpriority_low_loading_attrs( $attr, 'wp_get_attachment_image' ); + // Return 'lazy' if not in the loop or the main query. $this->assertSameSetsWithIndex( array( @@ -4527,6 +4532,8 @@ public function test_wp_get_loading_optimization_attributes( $context ) { while ( have_posts() ) { the_post(); + $this->assert_fetchpriority_low_loading_attrs( $attr, $context ); + // Return 'lazy' if in the loop but not in the main query. $this->assertSameSetsWithIndex( array( @@ -4539,6 +4546,8 @@ public function test_wp_get_loading_optimization_attributes( $context ) { // Set as main query. $this->set_main_query( $query ); + $this->assert_fetchpriority_low_loading_attrs( $attr, $context ); + // First three element are not lazy loaded. However, first image is loaded with fetchpriority high. $this->assertSameSetsWithIndex( array( @@ -4546,8 +4555,11 @@ public function test_wp_get_loading_optimization_attributes( $context ) { 'fetchpriority' => 'high', ), wp_get_loading_optimization_attributes( 'img', $attr, $context ), - "Expected first image to not be lazy-loaded. First large image get's high fetchpriority." + 'Expected first image to not be lazy-loaded. First large image gets high fetchpriority.' ); + + $this->assert_fetchpriority_low_loading_attrs( $attr, $context ); + $this->assertSameSetsWithIndex( array( 'decoding' => 'async', @@ -4555,6 +4567,9 @@ public function test_wp_get_loading_optimization_attributes( $context ) { wp_get_loading_optimization_attributes( 'img', $attr, $context ), 'Expected second image to not be lazy-loaded.' ); + + $this->assert_fetchpriority_low_loading_attrs( $attr, $context ); + $this->assertSameSetsWithIndex( array( 'decoding' => 'async', @@ -4563,6 +4578,8 @@ public function test_wp_get_loading_optimization_attributes( $context ) { 'Expected third image to not be lazy-loaded.' ); + $this->assert_fetchpriority_low_loading_attrs( $attr, $context ); + // Return 'lazy' if in the loop and in the main query for any subsequent elements. $this->assertSameSetsWithIndex( array( @@ -4572,6 +4589,8 @@ public function test_wp_get_loading_optimization_attributes( $context ) { wp_get_loading_optimization_attributes( 'img', $attr, $context ) ); + $this->assert_fetchpriority_low_loading_attrs( $attr, $context ); + // Yes, for all subsequent elements. $this->assertSameSetsWithIndex( array( @@ -4580,6 +4599,161 @@ public function test_wp_get_loading_optimization_attributes( $context ) { ), wp_get_loading_optimization_attributes( 'img', $attr, $context ) ); + + $this->assert_fetchpriority_low_loading_attrs( $attr, $context ); + + $this->assertSameSetsWithIndex( + array( + 'decoding' => 'async', + 'fetchpriority' => 'auto', + 'loading' => 'lazy', + ), + wp_get_loading_optimization_attributes( + 'img', + array_merge( $attr, array( 'fetchpriority' => 'auto' ) ), + $context + ), + 'Expected a fetchpriority=auto IMG appearing after the media count threshold to still be lazy-loaded.' + ); + } + } + + /** + * Tests that wp_get_loading_optimization_attributes() returns the expected loading attribute value. + * + * This test is the same as {@see self::test_wp_get_loading_optimization_attributes()} except that the IMG which + * previously got `fetchpriority=high` now initially has `fetchpriority=auto`. This causes the initial lazy-loaded + * image to be bumped down one. + * + * @ticket 64823 + * + * @covers ::wp_get_loading_optimization_attributes + * + * @dataProvider data_wp_get_loading_attr_default + * + * @param string $context + */ + public function test_wp_get_loading_optimization_attributes_with_fetchpriority_auto_for_lcp_candidate( string $context ): void { + $attr = $this->get_width_height_for_high_priority(); + + // Return 'lazy' by default. + $this->assertSameSetsWithIndex( + array( + 'decoding' => 'async', + 'loading' => 'lazy', + ), + wp_get_loading_optimization_attributes( 'img', $attr, 'test' ) + ); + $this->assertSameSetsWithIndex( + array( + 'decoding' => 'async', + 'loading' => 'lazy', + ), + wp_get_loading_optimization_attributes( 'img', $attr, 'wp_get_attachment_image' ) + ); + + $this->assert_fetchpriority_low_loading_attrs( $attr, 'wp_get_attachment_image' ); + + // Return 'lazy' if not in the loop or the main query. + $this->assertSameSetsWithIndex( + array( + 'decoding' => 'async', + 'loading' => 'lazy', + ), + wp_get_loading_optimization_attributes( 'img', $attr, $context ) + ); + + $query = $this->get_new_wp_query_for_published_post(); + + while ( have_posts() ) { + the_post(); + + $this->assert_fetchpriority_low_loading_attrs( $attr, $context ); + + // Return 'lazy' if in the loop but not in the main query. + $this->assertSameSetsWithIndex( + array( + 'decoding' => 'async', + 'loading' => 'lazy', + ), + wp_get_loading_optimization_attributes( 'img', $attr, $context ) + ); + + // Set as main query. + $this->set_main_query( $query ); + + $this->assert_fetchpriority_low_loading_attrs( $attr, $context ); + + // First three element are not lazy loaded. However, first image initially has `fetchpriority=auto` which marks it as a possible LCP element. + $this->assertSameSetsWithIndex( + array( + 'decoding' => 'async', + 'fetchpriority' => 'auto', + ), + wp_get_loading_optimization_attributes( + 'img', + array_merge( $attr, array( 'fetchpriority' => 'auto' ) ), + $context + ), + 'Expected first image to not be lazy-loaded.' + ); + + $this->assert_fetchpriority_low_loading_attrs( $attr, $context ); + + $this->assertSameSetsWithIndex( + array( + 'decoding' => 'async', + ), + wp_get_loading_optimization_attributes( 'img', $attr, $context ), + 'Expected second image to not be lazy-loaded.' + ); + + $this->assert_fetchpriority_low_loading_attrs( $attr, $context ); + + $this->assertSameSetsWithIndex( + array( + 'decoding' => 'async', + ), + wp_get_loading_optimization_attributes( 'img', $attr, $context ), + 'Expected third image to not be lazy-loaded.' + ); + + $this->assert_fetchpriority_low_loading_attrs( $attr, $context ); + + // This is the 4th subsequent image, and it still is not lazy-loaded because the first had fetchpriority=auto and so it may have been hidden with block visibility. + $this->assertSameSetsWithIndex( + array( + 'decoding' => 'async', + ), + wp_get_loading_optimization_attributes( 'img', $attr, $context ) + ); + + $this->assert_fetchpriority_low_loading_attrs( $attr, $context ); + + // Yes, for all subsequent elements. + $this->assertSameSetsWithIndex( + array( + 'decoding' => 'async', + 'loading' => 'lazy', + ), + wp_get_loading_optimization_attributes( 'img', $attr, $context ) + ); + + $this->assert_fetchpriority_low_loading_attrs( $attr, $context ); + + $this->assertSameSetsWithIndex( + array( + 'decoding' => 'async', + 'fetchpriority' => 'auto', + 'loading' => 'lazy', + ), + wp_get_loading_optimization_attributes( + 'img', + array_merge( $attr, array( 'fetchpriority' => 'auto' ) ), + $context + ), + 'Expected a fetchpriority=auto IMG appearing after the media count threshold to still be lazy-loaded.' + ); } } @@ -4606,12 +4780,17 @@ public function test_wp_get_loading_optimization_attributes_with_arbitrary_conte 'The "loading" attribute should be "lazy" when not in the loop or the main query.' ); + $this->assert_fetchpriority_low_loading_attrs( $attr, $context ); + $query = $this->get_new_wp_query_for_published_post(); // Set as main query. $this->set_main_query( $query ); while ( have_posts() ) { + + $this->assert_fetchpriority_low_loading_attrs( $attr, $context ); + the_post(); $this->assertSameSetsWithIndex( @@ -4656,9 +4835,13 @@ public function test_wp_get_loading_optimization_attributes_with_arbitrary_conte 'The "loading" attribute should be "lazy" before the main query loop.' ); + $this->assert_fetchpriority_low_loading_attrs( $attr, $context ); + while ( have_posts() ) { the_post(); + $this->assert_fetchpriority_low_loading_attrs( $attr, $context ); + $this->assertSameSetsWithIndex( array( 'decoding' => 'async', @@ -4740,6 +4923,8 @@ public function test_wp_get_loading_optimization_attributes_header_contexts( $co wp_get_loading_optimization_attributes( 'img', $attr, $context ), 'Images in the header context should get lazy-loaded after the wp_loading_optimization_force_header_contexts filter.' ); + + $this->assert_fetchpriority_low_loading_attrs( $attr, $context ); } /** @@ -4764,12 +4949,14 @@ public function test_wp_loading_optimization_force_header_contexts_filter() { add_filter( 'wp_loading_optimization_force_header_contexts', - function ( $context ) { + function ( $contexts ) { $contexts['something_completely_arbitrary'] = true; return $contexts; } ); + $this->assert_fetchpriority_low_loading_attrs( $attr, 'something_completely_arbitrary' ); + $this->assertSameSetsWithIndex( array( 'decoding' => 'async', @@ -4809,6 +4996,8 @@ public function test_wp_get_loading_optimization_attributes_before_loop_if_not_m ), wp_get_loading_optimization_attributes( 'img', $attr, $context ) ); + + $this->assert_fetchpriority_low_loading_attrs( $attr, $context ); } /** @@ -4840,6 +5029,8 @@ public function test_wp_get_loading_optimization_attributes_before_loop_in_main_ ), wp_get_loading_optimization_attributes( 'img', $attr, $context ) ); + + $this->assert_fetchpriority_low_loading_attrs( $attr, $context ); } /** @@ -4863,6 +5054,8 @@ public function test_wp_get_loading_optimization_attributes_before_loop_if_main_ $attr = $this->get_width_height_for_high_priority(); + $this->assert_fetchpriority_low_loading_attrs( $attr, $context ); + // First image is loaded with high fetchpriority. $this->assertSameSetsWithIndex( array( @@ -6049,52 +6242,83 @@ function ( $atts, $content = null ) { * * @dataProvider data_wp_maybe_add_fetchpriority_high_attr */ - public function test_wp_maybe_add_fetchpriority_high_attr( $loading_attrs, $tag_name, $attr, $expected_fetchpriority ) { + public function test_wp_maybe_add_fetchpriority_high_attr( array $loading_attrs, string $tag_name, array $attr, ?string $expected_fetchpriority, bool $expected_high_priority_element_flag ): void { $loading_attrs = wp_maybe_add_fetchpriority_high_attr( $loading_attrs, $tag_name, $attr ); - if ( $expected_fetchpriority ) { + if ( null !== $expected_fetchpriority ) { $this->assertArrayHasKey( 'fetchpriority', $loading_attrs, 'fetchpriority attribute should be present' ); $this->assertSame( $expected_fetchpriority, $loading_attrs['fetchpriority'], 'fetchpriority attribute has incorrect value' ); } else { $this->assertArrayNotHasKey( 'fetchpriority', $loading_attrs, 'fetchpriority attribute should not be present' ); } + $this->assertSame( $expected_high_priority_element_flag, wp_high_priority_element_flag() ); } /** * Data provider. * - * @return array[] + * @return array, + * 1: string, + * 2: array, + * 3: string|null, + * 4: bool, + * }> */ - public function data_wp_maybe_add_fetchpriority_high_attr() { + public function data_wp_maybe_add_fetchpriority_high_attr(): array { return array( - 'small image' => array( + 'small image' => array( array(), 'img', $this->get_insufficient_width_height_for_high_priority(), - false, + null, + true, + ), + 'small image with fetchpriority=auto' => array( + array(), + 'img', + array_merge( + $this->get_insufficient_width_height_for_high_priority(), + array( 'fetchpriority' => 'auto' ) + ), + null, + true, ), - 'large image' => array( + 'large image' => array( array(), 'img', $this->get_width_height_for_high_priority(), 'high', + false, + ), + 'large image with fetchpriority=auto' => array( + array(), + 'img', + array_merge( + $this->get_width_height_for_high_priority(), + array( 'fetchpriority' => 'auto' ) + ), + null, + false, ), - 'image with loading=lazy' => array( + 'image with loading=lazy' => array( array( 'loading' => 'lazy', 'decoding' => 'async', ), 'img', $this->get_width_height_for_high_priority(), - false, + null, + true, ), - 'image with loading=eager' => array( + 'image with loading=eager' => array( array( 'loading' => 'eager' ), 'img', $this->get_width_height_for_high_priority(), 'high', + false, ), - 'image with fetchpriority=high' => array( + 'image with fetchpriority=high' => array( array(), 'img', array_merge( @@ -6102,21 +6326,24 @@ public function data_wp_maybe_add_fetchpriority_high_attr() { array( 'fetchpriority' => 'high' ) ), 'high', + false, ), - 'image with fetchpriority=low' => array( + 'image with fetchpriority=low' => array( array(), 'img', array_merge( $this->get_insufficient_width_height_for_high_priority(), array( 'fetchpriority' => 'low' ) ), - false, + null, + true, ), - 'non-image element' => array( + 'non-image element' => array( array(), 'video', $this->get_width_height_for_high_priority(), - false, + null, + true, ), ); } @@ -6309,6 +6536,27 @@ static function ( $loading_attrs ) { ); } + /** + * Asserts that loading attributes for IMG with fetchpriority=low. + * + * It must not get lazy-loaded or increase the counter since they may be in the Navigation Overlay. + * + * @param array $attr + * @param string $context + */ + protected function assert_fetchpriority_low_loading_attrs( array $attr, string $context ): void { + $this->assertSameSetsWithIndex( + array( + 'fetchpriority' => 'low', + 'decoding' => 'async', + ), + wp_get_loading_optimization_attributes( + 'img', + array_merge( $attr, array( 'fetchpriority' => 'low' ) ), + $context + ) + ); + } /** * Test WebP lossless quality is handled correctly. @@ -7007,9 +7255,9 @@ public function set_main_query( $query ) { /** * Returns an array with dimension attribute values eligible for a high priority image. * - * @return array Associative array with 'width' and 'height' keys. + * @return array{ width: int, height: int } Associative array with 'width' and 'height' keys. */ - private function get_width_height_for_high_priority() { + private function get_width_height_for_high_priority(): array { /* * The product of width * height must be >50000 to qualify for high priority image. * 300 * 200 = 60000 @@ -7023,9 +7271,9 @@ private function get_width_height_for_high_priority() { /** * Returns an array with dimension attribute values ineligible for a high priority image. * - * @return array Associative array with 'width' and 'height' keys. + * @return array{ width: int, height: int } Associative array with 'width' and 'height' keys. */ - private function get_insufficient_width_height_for_high_priority() { + private function get_insufficient_width_height_for_high_priority(): array { /* * The product of width * height must be >50000 to qualify for high priority image. * 200 * 100 = 20000