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 c98023e8bf..d39cc2dac2 100644 --- a/plugins/image-prioritizer/class-image-prioritizer-img-tag-visitor.php +++ b/plugins/image-prioritizer/class-image-prioritizer-img-tag-visitor.php @@ -46,16 +46,9 @@ public function __invoke( OD_HTML_Tag_Walker $walker ): bool { $common_lcp_element = $this->url_metrics_group_collection->get_common_lcp_element(); if ( ! is_null( $common_lcp_element ) && $xpath === $common_lcp_element['xpath'] ) { if ( 'high' === $walker->get_attribute( 'fetchpriority' ) ) { - $walker->set_attribute( 'data-od-fetchpriority-already-added', true ); + $walker->set_meta_attribute( 'fetchpriority-already-added', true ); } else { $walker->set_attribute( 'fetchpriority', 'high' ); - $walker->set_attribute( 'data-od-added-fetchpriority', true ); - } - - // Never include loading=lazy on the LCP image common across all breakpoints. - if ( 'lazy' === $walker->get_attribute( 'loading' ) ) { - $walker->set_attribute( 'data-od-removed-loading', $walker->get_attribute( 'loading' ) ); - $walker->remove_attribute( 'loading' ); } } elseif ( is_string( $walker->get_attribute( 'fetchpriority' ) ) && $this->url_metrics_group_collection->is_every_group_populated() ) { /* @@ -68,12 +61,25 @@ public function __invoke( OD_HTML_Tag_Walker $walker ): bool { * Note also that if this is the LCP element for _some_ of the viewport groups, it will still get * fetchpriority=high by means of the preload link (with a media query) that is added further below. */ - $walker->set_attribute( 'data-od-removed-fetchpriority', $walker->get_attribute( 'fetchpriority' ) ); $walker->remove_attribute( 'fetchpriority' ); } - // TODO: If the image is visible (intersectionRatio!=0) in any of the URL metrics, remove loading=lazy. - // TODO: Conversely, if an image is the LCP element for one breakpoint but not another, add loading=lazy. This won't hurt performance since the image is being preloaded. + $element_max_intersection_ratio = $this->url_metrics_group_collection->get_element_max_intersection_ratio( $xpath ); + + // If the element was not found, we don't know if it was visible for not, so don't do anything. + if ( is_null( $element_max_intersection_ratio ) ) { + $walker->set_meta_attribute( 'unknown-tag', true ); // Mostly useful for debugging why an IMG isn't optimized. + } else { + // Otherwise, make sure visible elements omit the loading attribute, and hidden elements include loading=lazy. + $is_visible = $element_max_intersection_ratio > 0.0; + $loading = (string) $walker->get_attribute( 'loading' ); + if ( $is_visible && 'lazy' === $loading ) { + $walker->remove_attribute( 'loading' ); + } elseif ( ! $is_visible && 'lazy' !== $loading ) { + $walker->set_attribute( 'loading', 'lazy' ); + } + } + // TODO: If an image is visible in one breakpoint but not another, add loading=lazy AND add a regular-priority preload link with media queries (unless LCP in which case it should already have a fetchpriority=high link) so that the image won't be eagerly-loaded for viewports on which it is not shown. // If this element is the LCP (for a breakpoint group), add a preload link for it. foreach ( $this->url_metrics_group_collection->get_groups_by_lcp_element( $xpath ) as $group ) { diff --git a/plugins/image-prioritizer/load.php b/plugins/image-prioritizer/load.php index 5aa30810c6..aa5606fdf6 100644 --- a/plugins/image-prioritizer/load.php +++ b/plugins/image-prioritizer/load.php @@ -1,8 +1,8 @@ fetchpriority=high and applies image lazy-loading by leveraging client-side detection with real user metrics. * Requires at least: 6.4 * Requires PHP: 7.2 * Requires Plugins: optimization-detective diff --git a/plugins/image-prioritizer/readme.txt b/plugins/image-prioritizer/readme.txt index f4902fec46..cd0083ca4c 100644 --- a/plugins/image-prioritizer/readme.txt +++ b/plugins/image-prioritizer/readme.txt @@ -9,15 +9,24 @@ License: GPLv2 or later License URI: https://www.gnu.org/licenses/gpl-2.0.html Tags: performance, images -Optimizes the loading of the LCP image by leveraging client-side detection with real user metrics. +Optimizes LCP image loading with `fetchpriority=high` and applies image lazy-loading by leveraging client-side detection with real user metrics. == Description == -Optimizes the loading of the LCP image by leveraging client-side detection with real user metrics. Currently, this involves adding `fetchpriority=high` to the LCP image and adding preload links for the LCP `img` as well as any LCP `background-image`. +This plugin optimizes the loading of images which are the LCP (Largest Contentful Paint) element, including both `img` elements and elements with CSS background images (where there is a `style` attribute with an `background-image` property). Different breakpoints in a theme's responsive design may result in differing elements being the LCP element. Therefore, the LCP element for each breakpoint is captured so that high-fetchpriority preload links with media queries are added which prioritize loading the LCP image specific to the viewport of the visitor. -This plugin requires the [Optimization Detective](https://wordpress.org/plugins/optimization-detective/) plugin as a dependency. +In addition to prioritizing the loading of the LCP image, this plugin also optimizes image loading by ensuring that `loading=lazy` is omitted from any image that appears in the initial viewport for any of the breakpoints, which by default include: -TODO: Flesh out description. +1. 0-320 (small smartphone) +2. 321-480 (normal smartphone) +3. 481-576 (phablets) +4. >576 (desktop) + +If an image does not appear in the initial viewport for any of these viewport groups, then `loading=lazy` is added to the `img` element. + +Note that by default, URL Metrics are not gathered for administrator users, since they are not normal site visitors, and it is likely that additional elements will be present on the page which are not also shown to non-administrators. + +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. == Installation == diff --git a/plugins/image-prioritizer/tests/test-helper.php b/plugins/image-prioritizer/tests/test-helper.php index 52764863a5..8e9e345d5c 100644 --- a/plugins/image-prioritizer/tests/test-helper.php +++ b/plugins/image-prioritizer/tests/test-helper.php @@ -48,7 +48,7 @@ public function data_provider_test_filter_tag_walker_visitors(): array { ... - Foo + Foo @@ -189,7 +189,7 @@ public function data_provider_test_filter_tag_walker_visitors(): array { ', ), - 'common-lcp-image-with-fully-populated-sample-data' => array( + 'common-lcp-image-and-lazy-loaded-image-outside-viewport-with-fully-populated-sample-data' => array( 'set_up' => function (): void { $slug = od_get_url_metrics_slug( od_get_normalized_query_vars() ); $sample_size = od_get_url_metrics_breakpoint_sample_size(); @@ -205,8 +205,24 @@ public function data_provider_test_filter_tag_walker_visitors(): array { 'isLCP' => true, ), array( - 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[2][self::IMG]', + 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[3][self::IMG]', 'isLCP' => false, + 'intersectionRatio' => 0 === $i ? 0.5 : 0.0, // Make sure that the _max_ intersection ratio is considered. + ), + array( + 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[5][self::IMG]', + 'isLCP' => false, + 'intersectionRatio' => 0.0, + ), + array( + 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[6][self::IMG]', + 'isLCP' => false, + 'intersectionRatio' => 0.0, + ), + array( + 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[7][self::IMG]', + 'isLCP' => false, + 'intersectionRatio' => 0.0, ), ) ) @@ -222,7 +238,12 @@ public function data_provider_test_filter_tag_walker_visitors(): array { Foo - Bar +

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

+ Bar +

Now the following image is definitely outside the initial viewport.

+ Baz + Qux + Quux ', @@ -235,7 +256,12 @@ public function data_provider_test_filter_tag_walker_visitors(): array { Foo - Bar +

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

+ Bar +

Now the following image is definitely outside the initial viewport.

+ Baz + Qux + Quux ', @@ -286,8 +312,8 @@ public function data_provider_test_filter_tag_walker_visitors(): array { - Foo - Bar + Foo + Bar @@ -306,7 +332,7 @@ public function data_provider_test_filter_tag_walker_visitors(): array { $viewport_width, array( array( - 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::IMG]', + 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::IMG]', // Note: This is intentionally not reflecting the IMG in the HTML below. 'isLCP' => true, ), ) @@ -336,7 +362,7 @@ public function data_provider_test_filter_tag_walker_visitors(): array { - Foo + Foo ', @@ -553,6 +579,7 @@ static function () use ( $mobile_breakpoint, $tablet_breakpoint ): array { ', ), + // TODO: Eventually the images in this test should all be lazy-loaded, leaving the prioritization to the preload links. 'different-lcp-elements-for-non-consecutive-viewport-groups-with-missing-data-for-middle-group' => array( 'set_up' => function (): void { OD_URL_Metrics_Post_Type::store_url_metric( @@ -561,12 +588,14 @@ static function () use ( $mobile_breakpoint, $tablet_breakpoint ): array { 400, array( array( - 'isLCP' => true, - 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::IMG]', + 'isLCP' => true, + 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::IMG]', + 'intersectionRatio' => 1.0, ), array( - 'isLCP' => false, - 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[2][self::IMG]', + 'isLCP' => false, + 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[2][self::IMG]', + 'intersectionRatio' => 0.0, ), ) ) @@ -577,12 +606,14 @@ static function () use ( $mobile_breakpoint, $tablet_breakpoint ): array { 800, array( array( - 'isLCP' => false, - 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::IMG]', + 'isLCP' => false, + 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::IMG]', + 'intersectionRatio' => 0.0, ), array( - 'isLCP' => true, - 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[2][self::IMG]', + 'isLCP' => true, + 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[2][self::IMG]', + 'intersectionRatio' => 1.0, ), ) ) @@ -593,6 +624,7 @@ static function () use ( $mobile_breakpoint, $tablet_breakpoint ): array { ... + Mobile Logo @@ -605,6 +637,7 @@ static function () use ( $mobile_breakpoint, $tablet_breakpoint ): array { ... + @@ -803,7 +836,7 @@ static function () { Mobile Logo

New paragraph since URL Metrics were captured!

- Desktop Logo + Desktop Logo ', @@ -817,7 +850,7 @@ static function () { Mobile Logo

New paragraph since URL Metrics were captured!

- Desktop Logo + Desktop Logo diff --git a/plugins/optimization-detective/class-od-html-tag-walker.php b/plugins/optimization-detective/class-od-html-tag-walker.php index 9425e05b0a..1a742d2069 100644 --- a/plugins/optimization-detective/class-od-html-tag-walker.php +++ b/plugins/optimization-detective/class-od-html-tag-walker.php @@ -453,7 +453,29 @@ public function get_attribute( string $name ) { * @return bool Whether an attribute value was set. */ public function set_attribute( string $name, $value ): bool { - return $this->processor->set_attribute( $name, $value ); + $existing_value = $this->processor->get_attribute( $name ); + $result = $this->processor->set_attribute( $name, $value ); + if ( $result ) { + if ( is_string( $existing_value ) ) { + $this->set_meta_attribute( "replaced-{$name}", $existing_value ); + } else { + $this->set_meta_attribute( "added-{$name}", true ); + } + } + return $result; + } + + /** + * Sets a meta attribute. + * + * All meta attributes are prefixed with 'data-od-'. + * + * @param string $name Meta attribute name. + * @param string|true $value Value. + * @return bool Whether an attribute was set. + */ + public function set_meta_attribute( string $name, $value ): bool { + return $this->processor->set_attribute( "data-od-{$name}", $value ); } /** @@ -469,7 +491,12 @@ public function set_attribute( string $name, $value ): bool { * @return bool Whether an attribute was removed. */ public function remove_attribute( string $name ): bool { - return $this->processor->remove_attribute( $name ); + $old_value = $this->processor->get_attribute( $name ); + $result = $this->processor->remove_attribute( $name ); + if ( $result ) { + $this->set_meta_attribute( "removed-{$name}", is_string( $old_value ) ? $old_value : true ); + } + return $result; } /** diff --git a/plugins/optimization-detective/class-od-url-metrics-group-collection.php b/plugins/optimization-detective/class-od-url-metrics-group-collection.php index 06e6339db7..95c723c4e4 100644 --- a/plugins/optimization-detective/class-od-url-metrics-group-collection.php +++ b/plugins/optimization-detective/class-od-url-metrics-group-collection.php @@ -78,7 +78,8 @@ final class OD_URL_Metrics_Group_Collection implements Countable, IteratorAggreg * is_every_group_populated?: bool, * is_every_group_complete?: bool, * get_groups_by_lcp_element?: array, - * get_common_lcp_element?: ElementData|null + * get_common_lcp_element?: ElementData|null, + * get_all_element_max_intersection_ratios?: array * } */ private $result_cache = array(); @@ -370,6 +371,51 @@ public function get_common_lcp_element(): ?array { return $result; } + /** + * Gets the max intersection ratios of all elements across all groups and their captured URL metrics. + * + * @return array Keys are XPaths and values are the intersection ratios. + */ + public function get_all_element_max_intersection_ratios(): array { + if ( array_key_exists( __FUNCTION__, $this->result_cache ) ) { + return $this->result_cache[ __FUNCTION__ ]; + } + + $result = ( function () { + $element_max_intersection_ratios = array(); + + /* + * O(n^3) my! Yes. This is why the result is cached. This being said, the number of groups should be 4 (one + * more than the default number of breakpoints) and the number of URL metrics for each group should be 3 + * (the default sample size). Therefore, given the number (n) of visited elements on the page this will only + * end up running n*4*3 times. + */ + foreach ( $this->groups as $group ) { + foreach ( $group as $url_metric ) { + foreach ( $url_metric->get_elements() as $element ) { + $element_max_intersection_ratios[ $element['xpath'] ] = array_key_exists( $element['xpath'], $element_max_intersection_ratios ) + ? max( $element_max_intersection_ratios[ $element['xpath'] ], $element['intersectionRatio'] ) + : $element['intersectionRatio']; + } + } + } + return $element_max_intersection_ratios; + } )(); + + $this->result_cache[ __FUNCTION__ ] = $result; + return $result; + } + + /** + * Gets the max intersection ratio of an element across all groups and their captured URL metrics. + * + * @param string $xpath XPath for the element. + * @return float|null Max intersection ratio of null if tag is unknown (not captured). + */ + public function get_element_max_intersection_ratio( string $xpath ): ?float { + return $this->get_all_element_max_intersection_ratios()[ $xpath ] ?? null; + } + /** * Gets URL metrics from all groups flattened into one list. * diff --git a/plugins/optimization-detective/load.php b/plugins/optimization-detective/load.php index 036c5bb080..454914611b 100644 --- a/plugins/optimization-detective/load.php +++ b/plugins/optimization-detective/load.php @@ -1,8 +1,8 @@ set_attribute( 'data-od-xpath', $walker->get_xpath() ); + $walker->set_meta_attribute( 'xpath', $walker->get_xpath() ); } $generator->next(); } diff --git a/plugins/optimization-detective/readme.txt b/plugins/optimization-detective/readme.txt index 741fad74f5..62ebeb0bd6 100644 --- a/plugins/optimization-detective/readme.txt +++ b/plugins/optimization-detective/readme.txt @@ -9,11 +9,11 @@ License: GPLv2 or later License URI: https://www.gnu.org/licenses/gpl-2.0.html Tags: performance -Provides an API for leveraging real user metrics to improve the heuristics WordPress applies on the frontend to optimize page performance. +Provides an API for leveraging real user metrics to detect optimizations to apply on the frontend to improve page performance. == Description == -This plugin captures real user metrics about what elements are displayed on the page across a variety of device form factors (e.g. desktop, tablet, and phone) in order to apply loading optimizations which are not possible with WordPress’s current server-side heuristics. This plugin is a dependency which does not provide end-user functionality on its own. For that, please install the dependent plugin [Image Prioritizer](https://wordpress.org/plugins/image-prioritizer/) (among others to come from the WordPress Core Performance team). +This plugin captures real user metrics about what elements are displayed on the page across a variety of device form factors (e.g. desktop, tablet, and phone) in order to apply loading optimizations which are not possible with WordPress’s current server-side heuristics. This plugin is a dependency which does not provide end-user functionality on its own. For that, please install the dependent plugin [Image Prioritizer](https://wordpress.org/plugins/image-prioritizer/) (among [others](https://github.com/WordPress/performance/labels/%5BPlugin%5D%20Optimization%20Detective) to come from the WordPress Core Performance team). = Background = @@ -23,7 +23,7 @@ In order to increase the accuracy of identifying the LCP element, including acro = Technical Foundation = -At the core of Optimization Detective is the “URL Metric”, information about a page according to how it was loaded by a client with a specific viewport width. This includes which elements were visible in the initial viewport and which one was the LCP element. Each URL on a site can have an associated set of these URL Metrics (stored in a custom post type) which are gathered from real users. It gathers a sample of URL Metrics according to common responsive breakpoints (e.g. mobile, tablet, and desktop). When no more URL Metrics are needed for a URL due to the sample size being obtained for the breakpoints, it discontinues serving the JavaScript to gather the metrics (leveraging the [web-vitals.js](https://github.com/GoogleChrome/web-vitals) library). With the URL Metrics in hand, the output-buffered page is sent through the HTML Tag Processor and the images which were the LCP element for various breakpoints will get prioritized with high-priority preload links (along with `fetchpriority=high` on the actual `img` tag when it is the common LCP element across all breakpoints). LCP elements with background images added via inline `background-image` styles are also prioritized with preload links. +At the core of Optimization Detective is the “URL Metric”, information about a page according to how it was loaded by a client with a specific viewport width. This includes which elements were visible in the initial viewport and which one was the LCP element. Each URL on a site can have an associated set of these URL Metrics (stored in a custom post type) which are gathered from real users. It gathers a sample of URL Metrics according to common responsive breakpoints (e.g. mobile, tablet, and desktop). When no more URL Metrics are needed for a URL due to the sample size being obtained for the breakpoints, it discontinues serving the JavaScript to gather the metrics (leveraging the [web-vitals.js](https://github.com/GoogleChrome/web-vitals) library). With the URL Metrics in hand, the output-buffered page is sent through the HTML Tag Processor and--when the [Image Prioritizer](https://wordpress.org/plugins/image-prioritizer/) dependent plugin is installed--the images which were the LCP element for various breakpoints will get prioritized with high-priority preload links (along with `fetchpriority=high` on the actual `img` tag when it is the common LCP element across all breakpoints). LCP elements with background images added via inline `background-image` styles are also prioritized with preload links. URL Metrics have a “freshness TTL” after which they will be stale and the JavaScript will be served again to start gathering metrics again to ensure that the right elements continue to get their loading prioritized. When a URL Metrics custom post type hasn't been touched in a while, it is automatically garbage-collected. diff --git a/plugins/optimization-detective/tests/test-class-od-html-tag-walker.php b/plugins/optimization-detective/tests/test-class-od-html-tag-walker.php index f6d117060a..a57e1a6c9a 100644 --- a/plugins/optimization-detective/tests/test-class-od-html-tag-walker.php +++ b/plugins/optimization-detective/tests/test-class-od-html-tag-walker.php @@ -310,6 +310,8 @@ public function data_provider_sample_documents(): array { * @param string $document Document. * @param string[] $open_tags Open tags. * @param string[] $xpaths XPaths. + * + * @throws Exception But not really. */ public function test_open_tags_and_get_xpath( string $document, array $open_tags, array $xpaths ): void { $p = new OD_HTML_Tag_Walker( $document ); @@ -348,6 +350,8 @@ public function test_open_tags_throwing_exception(): void { * * @covers ::append_head_html * @covers OD_HTML_Tag_Processor::append_html + * + * @throws Exception But not really. */ public function test_append_head_html(): void { $html = ' @@ -396,6 +400,8 @@ public function test_append_head_html(): void { * @covers ::append_head_html * @covers ::append_body_html * @covers OD_HTML_Tag_Processor::append_html + * + * @throws Exception But not really. */ public function test_append_head_and_body_html(): void { $html = ' @@ -457,18 +463,24 @@ public function test_append_head_and_body_html(): void { * @covers ::set_attribute * @covers ::remove_attribute * @covers ::get_updated_html + * @covers ::set_meta_attribute + * + * @throws Exception But not really. */ public function test_html_tag_processor_wrapper_methods(): void { - $processor = new OD_HTML_Tag_Walker( '' ); + $processor = new OD_HTML_Tag_Walker( '' ); foreach ( $processor->open_tags() as $open_tag ) { if ( 'HTML' === $open_tag ) { $this->assertSame( $open_tag, $processor->get_tag() ); $this->assertSame( 'en', $processor->get_attribute( 'lang' ) ); $processor->set_attribute( 'lang', 'es' ); - $processor->remove_attribute( 'xml:lang' ); + $processor->remove_attribute( 'dir' ); + $processor->set_attribute( 'id', 'root' ); + $processor->set_meta_attribute( 'foo', 'bar' ); + $processor->set_meta_attribute( 'baz', true ); } } - $this->assertSame( '', $processor->get_updated_html() ); + $this->assertSame( '', $processor->get_updated_html() ); } /** diff --git a/plugins/optimization-detective/tests/test-class-od-url-metrics-group-collection.php b/plugins/optimization-detective/tests/test-class-od-url-metrics-group-collection.php index aa038ac4d8..b854aa7f85 100644 --- a/plugins/optimization-detective/tests/test-class-od-url-metrics-group-collection.php +++ b/plugins/optimization-detective/tests/test-class-od-url-metrics-group-collection.php @@ -618,6 +618,76 @@ public function test_get_common_lcp_element(): void { $this->assertSame( $lcp_element_xpath, $common_lcp_element['xpath'] ); } + /** + * Data provider. + * + * @return array + * @throws OD_Data_Validation_Exception But it won't really. + */ + public function data_provider_element_max_intersection_ratios(): array { + $xpath1 = '/*[0][self::HTML]/*[1][self::BODY]/*[0][self::IMG]/*[1]'; + $xpath2 = '/*[0][self::HTML]/*[1][self::BODY]/*[0][self::IMG]/*[2]'; + $xpath3 = '/*[0][self::HTML]/*[1][self::BODY]/*[0][self::IMG]/*[3]'; + return array( + 'one-element-sample-size-one' => array( + 'url_metrics' => array( + $this->get_validated_url_metric( 400, $xpath1, 0.0 ), + $this->get_validated_url_metric( 600, $xpath1, 0.5 ), + $this->get_validated_url_metric( 800, $xpath1, 1.0 ), + ), + 'expected' => array( + $xpath1 => 1.0, + ), + ), + 'three-elements-sample-size-two' => array( + 'url_metrics' => array( + // Group 1. + $this->get_validated_url_metric( 400, $xpath1, 0.0 ), + $this->get_validated_url_metric( 400, $xpath1, 1.0 ), + // Group 2. + $this->get_validated_url_metric( 600, $xpath2, 0.9 ), + $this->get_validated_url_metric( 600, $xpath2, 0.1 ), + // Group 3. + $this->get_validated_url_metric( 800, $xpath3, 0.5 ), + $this->get_validated_url_metric( 800, $xpath3, 0.6 ), + ), + 'expected' => array( + $xpath1 => 1.0, + $xpath2 => 0.9, + $xpath3 => 0.6, + ), + ), + 'no-url-metrics' => array( + 'url_metrics' => array(), + 'expected' => array(), + ), + + ); + } + + /** + * Test get_all_element_max_intersection_ratios() and get_element_max_intersection_ratio(). + * + * @covers ::get_all_element_max_intersection_ratios + * @covers ::get_element_max_intersection_ratio + * + * @dataProvider data_provider_element_max_intersection_ratios + * + * @param array $url_metrics URL metrics. + * @param array $expected Expected. + */ + public function test_get_all_element_max_intersection_ratios( array $url_metrics, array $expected ): void { + $breakpoints = array( 480, 600, 782 ); + $sample_size = 3; + $group_collection = new OD_URL_Metrics_Group_Collection( $url_metrics, $breakpoints, $sample_size, 0 ); + $actual = $group_collection->get_all_element_max_intersection_ratios(); + $this->assertSame( $actual, $group_collection->get_all_element_max_intersection_ratios(), 'Cached result is identical.' ); + $this->assertSame( $expected, $actual ); + foreach ( $expected as $expected_xpath => $expected_max_ratio ) { + $this->assertSame( $expected_max_ratio, $group_collection->get_element_max_intersection_ratio( $expected_xpath ) ); + } + } + /** * Test get_flattened_url_metrics(). * @@ -648,12 +718,13 @@ public function test_get_flattened_url_metrics(): void { /** * Gets a validated URL metric for testing. * - * @param int $viewport_width Viewport width. - * @param string $lcp_element_xpath LCP element XPath. + * @param int $viewport_width Viewport width. + * @param string $lcp_element_xpath LCP element XPath. + * @param float $intersection_ratio Intersection ratio. * @return OD_URL_Metric Validated URL metric. * @throws OD_Data_Validation_Exception From OD_URL_Metric if there is a parse error, but there won't be. */ - private function get_validated_url_metric( int $viewport_width = 480, string $lcp_element_xpath = '/*[0][self::HTML]/*[1][self::BODY]/*[0][self::IMG]/*[1]' ): OD_URL_Metric { + private function get_validated_url_metric( int $viewport_width = 480, string $lcp_element_xpath = '/*[0][self::HTML]/*[1][self::BODY]/*[0][self::IMG]/*[1]', float $intersection_ratio = 1.0 ): OD_URL_Metric { $data = array( 'url' => home_url( '/' ), 'viewport' => array( @@ -666,7 +737,7 @@ private function get_validated_url_metric( int $viewport_width = 480, string $lc 'isLCP' => true, 'isLCPCandidate' => true, 'xpath' => $lcp_element_xpath, - 'intersectionRatio' => 1, + 'intersectionRatio' => $intersection_ratio, 'intersectionRect' => array( 'width' => 100, 'height' => 100,