diff --git a/plugins/image-prioritizer/class-image-prioritizer-img-tag-visitor.php b/plugins/image-prioritizer/class-image-prioritizer-img-tag-visitor.php index c55129d761..bd6bb88800 100644 --- a/plugins/image-prioritizer/class-image-prioritizer-img-tag-visitor.php +++ b/plugins/image-prioritizer/class-image-prioritizer-img-tag-visitor.php @@ -146,21 +146,44 @@ private function process_img( OD_HTML_Tag_Processor $processor, OD_Tag_Visitor_C $processor->remove_attribute( 'fetchpriority' ); } - // Ensure that sizes=auto is set properly. - $sizes = $processor->get_attribute( 'sizes' ); - if ( is_string( $sizes ) ) { + // Ensure that sizes is set properly when it is a responsive image (it has a srcset attribute). + if ( is_string( $processor->get_attribute( 'srcset' ) ) ) { + $sizes = $processor->get_attribute( 'sizes' ); + if ( ! is_string( $sizes ) ) { + $sizes = ''; + } + $is_lazy = 'lazy' === $this->get_attribute_value( $processor, 'loading' ); $has_auto = $this->sizes_attribute_includes_valid_auto( $sizes ); if ( $is_lazy && ! $has_auto ) { - $processor->set_attribute( 'sizes', "auto, $sizes" ); + $new_sizes = 'auto'; + if ( '' !== trim( $sizes, " \t\f\r\n" ) ) { + $new_sizes .= ', '; + } + $sizes = $new_sizes . $sizes; } elseif ( ! $is_lazy && $has_auto ) { // Remove auto from the beginning of the list. - $processor->set_attribute( - 'sizes', - (string) preg_replace( '/^[ \t\f\r\n]*auto[ \t\f\r\n]*(,[ \t\f\r\n]*)?/i', '', $sizes ) - ); + $sizes = (string) preg_replace( '/^[ \t\f\r\n]*auto[ \t\f\r\n]*(,[ \t\f\r\n]*)?/i', '', $sizes ); + } + + // Compute more accurate sizes when it isn't lazy-loaded and sizes=auto isn't taking care of it. + if ( ! $is_lazy ) { + $computed_sizes = $this->compute_sizes( $context ); + if ( count( $computed_sizes ) > 0 ) { + $new_sizes = join( ', ', $computed_sizes ); + + // Preserve the original sizes as a fallback when URL Metrics are missing from one or more viewport group. + // Note that when all groups are populated, the media features will span all possible viewport widths from + // zero to infinity, so there is no need to include the original sizes since they will never match. + if ( '' !== $sizes && ! $context->url_metric_group_collection->is_every_group_populated() ) { + $new_sizes .= ", $sizes"; + } + $sizes = $new_sizes; + } } + + $processor->set_attribute( 'sizes', $sizes ); } $parent_tag = $this->get_parent_tag_name( $context ); @@ -385,4 +408,38 @@ private function sizes_attribute_includes_valid_auto( string $sizes_attr ): bool return 'auto' === $sizes_attr || str_starts_with( $sizes_attr, 'auto,' ); } } + + /** + * Computes responsive sizes for the current element based on its boundingClientRect width captured in URL Metrics. + * + * @since n.e.x.t + * + * @param OD_Tag_Visitor_Context $context Context. + * @return non-empty-string[] Computed sizes. + */ + private function compute_sizes( OD_Tag_Visitor_Context $context ): array { + $sizes = array(); + + $xpath = $context->processor->get_xpath(); + foreach ( $context->url_metric_group_collection as $group ) { + // Obtain the maximum width that the image appears among all URL Metrics collected for this viewport group. + $element_max_width = 0; + foreach ( $group->get_xpath_elements_map()[ $xpath ] ?? array() as $element ) { + $element_max_width = max( $element_max_width, $element->get_bounding_client_rect()['width'] ); + } + + // Use the maximum width as the size for image in this breakpoint. + if ( $element_max_width > 0 ) { + $size = sprintf( '%dpx', $element_max_width ); + $media_feature = od_generate_media_query( $group->get_minimum_viewport_width(), $group->get_maximum_viewport_width() ); + if ( null !== $media_feature ) { + // Note: The null case only happens when a site has filtered od_breakpoint_max_widths to be an empty array, meaning there is only one viewport group. + $size = "$media_feature $size"; + } + $sizes[] = $size; + } + } + + return $sizes; + } } diff --git a/plugins/image-prioritizer/readme.txt b/plugins/image-prioritizer/readme.txt index c1e47602c3..e7ca146c2f 100644 --- a/plugins/image-prioritizer/readme.txt +++ b/plugins/image-prioritizer/readme.txt @@ -7,7 +7,7 @@ License: GPLv2 or later License URI: https://www.gnu.org/licenses/gpl-2.0.html Tags: performance, optimization, image, lcp, lazy-load -Prioritizes the loading of images and videos based on how visible they are to actual visitors; adds fetchpriority and applies lazy loading. +Prioritizes the loading of images and videos based on how they appear to actual visitors: adds fetchpriority, preloads, lazy-loads, and sets sizes. == Description == @@ -27,7 +27,9 @@ The current optimizations include: 1. Apply lazy loading to `IMG` tags based on whether they appear in any breakpoint’s initial viewport. 2. Implement lazy loading of CSS background images added via inline `style` attributes. 3. Lazy-load `VIDEO` tags by setting the appropriate attributes based on whether they appear in the initial viewport. If a `VIDEO` is the LCP element, it gets `preload=auto`; if it is in an initial viewport, the `preload=metadata` default is left; if it is not in an initial viewport, it gets `preload=none`. Lazy-loaded videos also get initial `preload`, `autoplay`, and `poster` attributes restored when the `VIDEO` is going to enter the viewport. -5. Ensure that [`sizes=auto`](https://make.wordpress.org/core/2024/10/18/auto-sizes-for-lazy-loaded-images-in-wordpress-6-7/) is added to all lazy-loaded `IMG` elements. +5. Responsive image sizes: + 1. Compute the `sizes` attribute using the widths of an image collected from URL Metrics for each breakpoint (when not lazy-loaded since then handled by `sizes=auto`). + 2. Ensure [`sizes=auto`](https://make.wordpress.org/core/2024/10/18/auto-sizes-for-lazy-loaded-images-in-wordpress-6-7/) is set on `IMG` tags after setting correct lazy-loading (above). 6. Reduce the size of the `poster` image of a `VIDEO` from full size to the size appropriate for the maximum width of the video (on desktop). **This plugin requires the [Optimization Detective](https://wordpress.org/plugins/optimization-detective/) plugin as a dependency.** Please refer to that plugin for additional background on how this plugin works as well as additional developer options. diff --git a/plugins/image-prioritizer/tests/test-cases/background-image-outside-viewport-on-all-breakpoints-but-not-desktop-with-fully-populated-sample-data/set-up.php b/plugins/image-prioritizer/tests/test-cases/background-image-outside-viewport-on-all-breakpoints-but-not-desktop-with-fully-populated-sample-data/set-up.php index 5839f6eb3c..b0acbfcb07 100644 --- a/plugins/image-prioritizer/tests/test-cases/background-image-outside-viewport-on-all-breakpoints-but-not-desktop-with-fully-populated-sample-data/set-up.php +++ b/plugins/image-prioritizer/tests/test-cases/background-image-outside-viewport-on-all-breakpoints-but-not-desktop-with-fully-populated-sample-data/set-up.php @@ -10,13 +10,6 @@ static function () use ( $breakpoint_max_widths ) { } ); - $outside_viewport_rect = array_merge( - $test_case->get_sample_dom_rect(), - array( - 'top' => 100000, - ) - ); - foreach ( $breakpoint_max_widths as $non_desktop_viewport_width ) { for ( $i = 0; $i < $sample_size; $i++ ) { OD_URL_Metrics_Post_Type::store_url_metric( @@ -29,8 +22,7 @@ static function () use ( $breakpoint_max_widths ) { 'xpath' => '/HTML/BODY/DIV[@id=\'page\']/*[2][self::DIV]', 'isLCP' => false, 'intersectionRatio' => 0.0, - 'intersectionRect' => $outside_viewport_rect, - 'boundingClientRect' => $outside_viewport_rect, + 'boundingClientRect' => array( 'top' => 100000 ), ), ), ) diff --git a/plugins/image-prioritizer/tests/test-cases/background-image-outside-viewport-with-desktop-metrics-missing/set-up.php b/plugins/image-prioritizer/tests/test-cases/background-image-outside-viewport-with-desktop-metrics-missing/set-up.php index dc4dc10f49..602fc0221d 100644 --- a/plugins/image-prioritizer/tests/test-cases/background-image-outside-viewport-with-desktop-metrics-missing/set-up.php +++ b/plugins/image-prioritizer/tests/test-cases/background-image-outside-viewport-with-desktop-metrics-missing/set-up.php @@ -9,13 +9,6 @@ static function () use ( $breakpoint_max_widths ) { } ); - $outside_viewport_rect = array_merge( - $test_case->get_sample_dom_rect(), - array( - 'top' => 100000, - ) - ); - foreach ( $breakpoint_max_widths as $non_desktop_viewport_width ) { OD_URL_Metrics_Post_Type::store_url_metric( od_get_url_metrics_slug( od_get_normalized_query_vars() ), @@ -27,8 +20,7 @@ static function () use ( $breakpoint_max_widths ) { 'xpath' => '/HTML/BODY/DIV[@id=\'page\']/*[2][self::DIV]', 'isLCP' => false, 'intersectionRatio' => 0.0, - 'intersectionRect' => $outside_viewport_rect, - 'boundingClientRect' => $outside_viewport_rect, + 'boundingClientRect' => array( 'top' => 100000 ), ), ), ) diff --git a/plugins/image-prioritizer/tests/test-cases/common-lcp-background-image-and-lazy-loaded-background-image-outside-viewport-with-fully-populated-sample-data/set-up.php b/plugins/image-prioritizer/tests/test-cases/common-lcp-background-image-and-lazy-loaded-background-image-outside-viewport-with-fully-populated-sample-data/set-up.php index fe81ed23db..bdf18f9142 100644 --- a/plugins/image-prioritizer/tests/test-cases/common-lcp-background-image-and-lazy-loaded-background-image-outside-viewport-with-fully-populated-sample-data/set-up.php +++ b/plugins/image-prioritizer/tests/test-cases/common-lcp-background-image-and-lazy-loaded-background-image-outside-viewport-with-fully-populated-sample-data/set-up.php @@ -1,12 +1,5 @@ get_sample_dom_rect(), - array( - 'top' => 100000, - ) - ); - $test_case->populate_url_metrics( array( array( @@ -17,15 +10,13 @@ 'xpath' => '/HTML/BODY/DIV[@id=\'page\']/*[3][self::DIV]', 'isLCP' => false, 'intersectionRatio' => 0.0, - 'intersectionRect' => $outside_viewport_rect, - 'boundingClientRect' => $outside_viewport_rect, + 'boundingClientRect' => array( 'top' => 100000 ), ), array( 'xpath' => '/HTML/BODY/DIV[@id=\'page\']/*[4][self::DIV]', 'isLCP' => false, 'intersectionRatio' => 0.0, - 'intersectionRect' => $outside_viewport_rect, - 'boundingClientRect' => $outside_viewport_rect, + 'boundingClientRect' => array( 'top' => 100000 ), ), ) ); diff --git a/plugins/image-prioritizer/tests/test-cases/common-lcp-image-and-lazy-loaded-image-outside-viewport-with-fully-populated-sample-data/buffer.html b/plugins/image-prioritizer/tests/test-cases/common-lcp-image-and-lazy-loaded-image-outside-viewport-with-fully-populated-sample-data/buffer.html index 06087ee6b6..806fd84b35 100644 --- a/plugins/image-prioritizer/tests/test-cases/common-lcp-image-and-lazy-loaded-image-outside-viewport-with-fully-populated-sample-data/buffer.html +++ b/plugins/image-prioritizer/tests/test-cases/common-lcp-image-and-lazy-loaded-image-outside-viewport-with-fully-populated-sample-data/buffer.html @@ -11,9 +11,9 @@ Foo

Pretend this is a super long paragraph that pushes the next image mostly out of the initial viewport.

- Bar + Bar

Now the following image is definitely outside the initial viewport.

- Baz + Baz Qux Quux diff --git a/plugins/image-prioritizer/tests/test-cases/common-lcp-image-and-lazy-loaded-image-outside-viewport-with-fully-populated-sample-data/expected.html b/plugins/image-prioritizer/tests/test-cases/common-lcp-image-and-lazy-loaded-image-outside-viewport-with-fully-populated-sample-data/expected.html index 0d86051f12..9c2f2341f3 100644 --- a/plugins/image-prioritizer/tests/test-cases/common-lcp-image-and-lazy-loaded-image-outside-viewport-with-fully-populated-sample-data/expected.html +++ b/plugins/image-prioritizer/tests/test-cases/common-lcp-image-and-lazy-loaded-image-outside-viewport-with-fully-populated-sample-data/expected.html @@ -2,19 +2,19 @@ ... - +

Pretend this is a super long paragraph that pushes the next image mostly out of the initial viewport.

- Bar + Bar

Now the following image is definitely outside the initial viewport.

- Baz + Baz Qux Quux
diff --git a/plugins/image-prioritizer/tests/test-cases/common-lcp-image-and-lazy-loaded-image-outside-viewport-with-fully-populated-sample-data/set-up.php b/plugins/image-prioritizer/tests/test-cases/common-lcp-image-and-lazy-loaded-image-outside-viewport-with-fully-populated-sample-data/set-up.php index 2bf089a7c4..e72cbe3167 100644 --- a/plugins/image-prioritizer/tests/test-cases/common-lcp-image-and-lazy-loaded-image-outside-viewport-with-fully-populated-sample-data/set-up.php +++ b/plugins/image-prioritizer/tests/test-cases/common-lcp-image-and-lazy-loaded-image-outside-viewport-with-fully-populated-sample-data/set-up.php @@ -1,13 +1,7 @@ get_sample_dom_rect(), - array( - 'top' => 100000, - ) - ); + $slug = od_get_url_metrics_slug( od_get_normalized_query_vars() ); + $sample_size = od_get_url_metrics_breakpoint_sample_size(); foreach ( array_merge( od_get_breakpoint_max_widths(), array( 1000 ) ) as $viewport_width ) { for ( $i = 0; $i < $sample_size; $i++ ) { OD_URL_Metrics_Post_Type::store_url_metric( @@ -31,31 +25,31 @@ 'intersectionRatio' => 0.0, // Subsequent carousel slide. ), array( - 'xpath' => '/HTML/BODY/DIV[@id=\'page\']/*[3][self::IMG]', - 'isLCP' => false, - 'intersectionRatio' => 0 === $i ? 0.5 : 0.0, // Make sure that the _max_ intersection ratio is considered. + 'xpath' => '/HTML/BODY/DIV[@id=\'page\']/*[3][self::IMG]', + 'isLCP' => false, + 'intersectionRatio' => 0 === $i ? 0.5 : 0.0, // Make sure that the _max_ intersection ratio is considered. + 'boundingClientRect' => array( + 'width' => $viewport_width - 10, + ), ), // All are outside all initial viewports. array( 'xpath' => '/HTML/BODY/DIV[@id=\'page\']/*[5][self::IMG]', 'isLCP' => false, 'intersectionRatio' => 0.0, - 'intersectionRect' => $outside_viewport_rect, - 'boundingClientRect' => $outside_viewport_rect, + 'boundingClientRect' => array( 'top' => 100000 ), ), array( 'xpath' => '/HTML/BODY/DIV[@id=\'page\']/*[6][self::IMG]', 'isLCP' => false, 'intersectionRatio' => 0.0, - 'intersectionRect' => $outside_viewport_rect, - 'boundingClientRect' => $outside_viewport_rect, + 'boundingClientRect' => array( 'top' => 100000 ), ), array( 'xpath' => '/HTML/BODY/DIV[@id=\'page\']/*[7][self::IMG]', 'isLCP' => false, 'intersectionRatio' => 0.0, - 'intersectionRect' => $outside_viewport_rect, - 'boundingClientRect' => $outside_viewport_rect, + 'boundingClientRect' => array( 'top' => 100000 ), ), ), ) diff --git a/plugins/image-prioritizer/tests/test-cases/only-mobile-and-desktop-groups-are-populated/expected.html b/plugins/image-prioritizer/tests/test-cases/only-mobile-and-desktop-groups-are-populated/expected.html index 5a72433931..1d7b82b7a3 100644 --- a/plugins/image-prioritizer/tests/test-cases/only-mobile-and-desktop-groups-are-populated/expected.html +++ b/plugins/image-prioritizer/tests/test-cases/only-mobile-and-desktop-groups-are-populated/expected.html @@ -5,7 +5,7 @@ - +
@@ -23,7 +23,7 @@

Last Post

First Post

This post does have a featured image, and the server-side heuristics in WordPress cause it to get fetchpriority=high, but it should not have this since it is out of the viewport on mobile.

diff --git a/plugins/image-prioritizer/tests/test-cases/only-mobile-and-desktop-groups-are-populated/set-up.php b/plugins/image-prioritizer/tests/test-cases/only-mobile-and-desktop-groups-are-populated/set-up.php index 72ff40f7e4..0f422707b4 100644 --- a/plugins/image-prioritizer/tests/test-cases/only-mobile-and-desktop-groups-are-populated/set-up.php +++ b/plugins/image-prioritizer/tests/test-cases/only-mobile-and-desktop-groups-are-populated/set-up.php @@ -10,13 +10,6 @@ static function () { $slug = od_get_url_metrics_slug( od_get_normalized_query_vars() ); $sample_size = od_get_url_metrics_breakpoint_sample_size(); - $outside_viewport_rect = array_merge( - $test_case->get_sample_dom_rect(), - array( - 'top' => 100000, - ) - ); - // Populate the mobile and desktop viewport groups only. foreach ( array( 400, 800 ) as $viewport_width ) { for ( $i = 0; $i < $sample_size; $i++ ) { @@ -35,8 +28,7 @@ static function () { 'xpath' => '/HTML/BODY/DIV[@id=\'page\']/*[2][self::MAIN]/*[4][self::DIV]', 'isLCP' => false, 'intersectionRatio' => 0.0, - 'intersectionRect' => $outside_viewport_rect, - 'boundingClientRect' => $outside_viewport_rect, + 'boundingClientRect' => array( 'top' => 100000 ), ), ), ) diff --git a/plugins/image-prioritizer/tests/test-cases/picture-element-as-lcp-tablet-and-desktop-metrics-missing/expected.html b/plugins/image-prioritizer/tests/test-cases/picture-element-as-lcp-tablet-and-desktop-metrics-missing/expected.html index b8afa42a9f..af3517b09d 100644 --- a/plugins/image-prioritizer/tests/test-cases/picture-element-as-lcp-tablet-and-desktop-metrics-missing/expected.html +++ b/plugins/image-prioritizer/tests/test-cases/picture-element-as-lcp-tablet-and-desktop-metrics-missing/expected.html @@ -9,7 +9,7 @@ - Foo + Foo
diff --git a/plugins/image-prioritizer/tests/test-cases/picture-element-with-lcp-image-and-fully-populated-sample-data/expected.html b/plugins/image-prioritizer/tests/test-cases/picture-element-with-lcp-image-and-fully-populated-sample-data/expected.html index f42e7fd2b4..c7e42571c4 100644 --- a/plugins/image-prioritizer/tests/test-cases/picture-element-with-lcp-image-and-fully-populated-sample-data/expected.html +++ b/plugins/image-prioritizer/tests/test-cases/picture-element-with-lcp-image-and-fully-populated-sample-data/expected.html @@ -8,7 +8,7 @@
- Foo + Foo
diff --git a/plugins/image-prioritizer/tests/test-cases/picture-element-with-source-having-media-attribute/expected.html b/plugins/image-prioritizer/tests/test-cases/picture-element-with-source-having-media-attribute/expected.html index 1ce78a0f70..1ce00fed27 100644 --- a/plugins/image-prioritizer/tests/test-cases/picture-element-with-source-having-media-attribute/expected.html +++ b/plugins/image-prioritizer/tests/test-cases/picture-element-with-source-having-media-attribute/expected.html @@ -7,7 +7,7 @@
- Foo + Foo
diff --git a/plugins/image-prioritizer/tests/test-cases/picture-element-with-source-missing-type-attribute/expected.html b/plugins/image-prioritizer/tests/test-cases/picture-element-with-source-missing-type-attribute/expected.html index 50244f0fbb..2ed73953a8 100644 --- a/plugins/image-prioritizer/tests/test-cases/picture-element-with-source-missing-type-attribute/expected.html +++ b/plugins/image-prioritizer/tests/test-cases/picture-element-with-source-missing-type-attribute/expected.html @@ -7,7 +7,7 @@
- Foo + Foo
diff --git a/plugins/image-prioritizer/tests/test-helper.php b/plugins/image-prioritizer/tests/test-helper.php index 0c1092853f..79a4092c41 100644 --- a/plugins/image-prioritizer/tests/test-helper.php +++ b/plugins/image-prioritizer/tests/test-helper.php @@ -102,13 +102,6 @@ public function test_end_to_end( string $directory ): void { * @return array Data. */ public function data_provider_test_auto_sizes(): array { - $outside_viewport_rect = array_merge( - $this->get_sample_dom_rect(), - array( - 'top' => 1000, - ) - ); - return array( // Note: The Image Prioritizer plugin removes the loading attribute, and so then Auto Sizes does not then add sizes=auto. 'wrongly_lazy_responsive_img' => array( @@ -118,7 +111,7 @@ public function data_provider_test_auto_sizes(): array { 'intersectionRatio' => 1, ), 'buffer' => 'Foo', - 'expected' => 'Foo', + 'expected' => 'Foo', ), 'non_responsive_image' => array( @@ -126,8 +119,7 @@ public function data_provider_test_auto_sizes(): array { 'xpath' => '/HTML/BODY/DIV[@id=\'page\']/*[1][self::IMG]', 'isLCP' => false, 'intersectionRatio' => 0, - 'intersectionRect' => $outside_viewport_rect, - 'boundingClientRect' => $outside_viewport_rect, + 'boundingClientRect' => array( 'top' => 100000 ), ), 'buffer' => 'Quux', 'expected' => 'Quux', @@ -138,20 +130,29 @@ public function data_provider_test_auto_sizes(): array { 'xpath' => '/HTML/BODY/DIV[@id=\'page\']/*[1][self::IMG]', 'isLCP' => false, 'intersectionRatio' => 0, - 'intersectionRect' => $outside_viewport_rect, - 'boundingClientRect' => $outside_viewport_rect, + 'boundingClientRect' => array( 'top' => 100000 ), ), 'buffer' => 'Foo', 'expected' => 'Foo', ), + 'sizes_attribute_added' => array( + 'element_metrics' => array( + 'xpath' => '/HTML/BODY/DIV[@id=\'page\']/*[1][self::IMG]', + 'isLCP' => false, + 'intersectionRatio' => 0, + 'boundingClientRect' => array( 'top' => 100000 ), + ), + 'buffer' => 'Foo', + 'expected' => 'Foo', + ), + 'auto_sizes_already_added' => array( 'element_metrics' => array( 'xpath' => '/HTML/BODY/DIV[@id=\'page\']/*[1][self::IMG]', 'isLCP' => false, 'intersectionRatio' => 0, - 'intersectionRect' => $outside_viewport_rect, - 'boundingClientRect' => $outside_viewport_rect, + 'boundingClientRect' => array( 'top' => 100000 ), ), 'buffer' => 'Foo', 'expected' => 'Foo', @@ -165,7 +166,7 @@ public function data_provider_test_auto_sizes(): array { 'intersectionRatio' => 1, ), 'buffer' => 'Foo', - 'expected' => 'Foo', + 'expected' => 'Foo', ), 'wrongly_auto_sized_responsive_img_with_only_auto' => array( @@ -175,7 +176,7 @@ public function data_provider_test_auto_sizes(): array { 'intersectionRatio' => 1, ), 'buffer' => 'Foo', - 'expected' => 'Foo', + 'expected' => 'Foo', ), ); } @@ -184,6 +185,10 @@ public function data_provider_test_auto_sizes(): array { * Test auto sizes. * * @covers Image_Prioritizer_Img_Tag_Visitor::__invoke + * @covers Image_Prioritizer_Img_Tag_Visitor::process_img + * @covers Image_Prioritizer_Tag_Visitor::get_attribute_value + * @covers Image_Prioritizer_Img_Tag_Visitor::compute_sizes + * @covers Image_Prioritizer_Img_Tag_Visitor::sizes_attribute_includes_valid_auto * * @dataProvider data_provider_test_auto_sizes * @phpstan-param array{ xpath: string, isLCP: bool, intersectionRatio: int } $element_metrics diff --git a/plugins/optimization-detective/tests/class-optimization-detective-test-helpers.php b/plugins/optimization-detective/tests/class-optimization-detective-test-helpers.php index 3cf43cdf99..82bc2c140f 100644 --- a/plugins/optimization-detective/tests/class-optimization-detective-test-helpers.php +++ b/plugins/optimization-detective/tests/class-optimization-detective-test-helpers.php @@ -12,7 +12,9 @@ * @phpstan-type ElementDataSubset array{ * xpath: string, * isLCP?: bool, - * intersectionRatio?: float + * intersectionRatio?: float, + * intersectionRect?: array, + * boundingClientRect?: array, * } */ trait Optimization_Detective_Test_Helpers { @@ -61,18 +63,22 @@ public function populate_url_metrics( array $elements, bool $complete = true ): /** * Gets a sample DOM rect for testing. * + * @param float $width Width. + * @param float $height Height. * @return array */ - public function get_sample_dom_rect(): array { + public function get_sample_dom_rect( float $width = 500.1, float $height = 500.2 ): array { + $x = 100.3; + $y = 200.0; return array( - 'width' => 500.1, - 'height' => 500.2, - 'x' => 100.3, - 'y' => 100.4, - 'top' => 0.1, + 'width' => $width, + 'height' => $height, + 'x' => $x, + 'y' => $y, + 'top' => $y, 'right' => 0.2, 'bottom' => 0.3, - 'left' => 0.4, + 'left' => $x, ); } @@ -103,6 +109,9 @@ public function get_sample_url_metric( array $params ): OD_URL_Metric { ), $params ); + if ( ! isset( $params['viewport_height'] ) ) { + $params['viewport_height'] = ceil( $params['viewport_width'] / 2 ); + } if ( array_key_exists( 'element', $params ) ) { $params['elements'][] = $params['element']; @@ -114,19 +123,38 @@ public function get_sample_url_metric( array $params ): OD_URL_Metric { 'url' => $params['url'], 'viewport' => array( 'width' => $params['viewport_width'], - 'height' => $params['viewport_height'] ?? ceil( $params['viewport_width'] / 2 ), + 'height' => $params['viewport_height'], ), 'timestamp' => $params['timestamp'], 'elements' => array_map( - function ( array $element ): array { + function ( array $element ) use ( $params ): array { + $default_props = array( + 'isLCP' => false, + 'isLCPCandidate' => $element['isLCP'] ?? false, + 'intersectionRatio' => 1, + 'boundingClientRect' => $this->get_sample_dom_rect( round( $params['viewport_width'] ) * 0.9, round( $params['viewport_height'] * 0.9 ) ), + ); + + if ( isset( $element['intersectionRatio'] ) && $element['intersectionRatio'] < PHP_FLOAT_EPSILON ) { + $default_props['intersectionRect'] = array_fill_keys( array_keys( $this->get_sample_dom_rect() ), 0.0 ); + } else { + $default_props['intersectionRect'] = $default_props['boundingClientRect']; + } + + $element['intersectionRect'] = array_merge( + $this->get_sample_dom_rect(), + $default_props['intersectionRect'], + $element['intersectionRect'] ?? array() + ); + + $element['boundingClientRect'] = array_merge( + $this->get_sample_dom_rect(), + $default_props['boundingClientRect'], + $element['boundingClientRect'] ?? array() + ); + return array_merge( - array( - 'isLCP' => false, - 'isLCPCandidate' => $element['isLCP'] ?? false, - 'intersectionRatio' => 1, - 'intersectionRect' => $this->get_sample_dom_rect(), - 'boundingClientRect' => $this->get_sample_dom_rect(), - ), + $default_props, $element ); },