Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion inc/rest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1029,7 +1029,7 @@ function ( $url ) {
}
$sanitized_lcp_data = [];
if ( ! empty( $lcp_data ) ) {
$sanitized_lcp_data['imageId'] = sanitize_text_field( $lcp_data['i'] ?? '' );
$sanitized_lcp_data['imageId'] = absint( $lcp_data['i'] ?? 0 );
$sanitized_lcp_data['bgSelector'] = sanitize_text_field( $lcp_data['s'] ?? '' );
if ( count( $lcp_data['u'] ?? [] ) > 3 ) {
return $this->response( 'LCP Background URLs limit exceeded', 'error' );
Expand Down
52 changes: 27 additions & 25 deletions inc/v2/PageProfiler/Profile.php
Original file line number Diff line number Diff line change
Expand Up @@ -154,30 +154,30 @@ public static function get_default_id( string $content ): string {
/**
* Stores above-fold image data for a specific profile ID and device type.
*
* @param string $id The profile ID.
* @param int $device_type The device type constant.
* @param array<int|string> $above_fold_images Array of above-fold image ids.
* @param array<string, array<string, array<int, string>>> $af_bg_selectors Array of above-fold background selectors.
* Array structure:
* [
* 'css_selector' => [
* 'above_the_fold_selector' => [
* 0 => 'background_image_url',
* 1 => 'background_image_url',
* ...
* ],
* ...
* ],
* ...
* ] Array of above-fold background selectors.
* @param array{imageId?: string, bgSelector?: string, bgUrls?: array<string>, type?: string} $lcp_data LCP (Largest Contentful Paint) data.
* where 'imageId' is the element identifier,
* 'bgSelector' is the selector,
* 'bgUrls' is an array of URLs
* 'type' is the type of the LCP element.
* @param array<int, array{w: int, h: int}> $missing_dimensions Array of missing dimensions.
* @param array<int, array<int, array{w: int, h: int, d: int, s: string, b: int}>> $missing_srcsets Array of missing srcsets.
* @param array<int, bool> $crop_status Array of crop status for images.
* @param string $id The profile ID.
* @param int $device_type The device type constant.
* @param array<int|string> $above_fold_images Array of above-fold image ids.
* @param array<string, array<string, array<int, string>>> $af_bg_selectors Array of above-fold background selectors.
* Array structure:
* [
* 'css_selector' => [
* 'above_the_fold_selector' => [
* 0 => 'background_image_url',
* 1 => 'background_image_url',
* ...
* ],
* ...
* ],
* ...
* ] Array of above-fold background selectors.
* @param array{imageId?: int, bgSelector?: string, bgUrls?: array<string>, type?: string} $lcp_data LCP (Largest Contentful Paint) data.
* where 'imageId' is the element identifier,
* 'bgSelector' is the selector,
* 'bgUrls' is an array of URLs
* 'type' is the type of the LCP element.
* @param array<int, array{w: int, h: int}> $missing_dimensions Array of missing dimensions.
* @param array<int, array<int, array{w: int, h: int, d: int, s: string, b: int}>> $missing_srcsets Array of missing srcsets.
* @param array<int, bool> $crop_status Array of crop status for images.
* @return void
*/
public function store( string $id, int $device_type, array $above_fold_images, $af_bg_selectors = [], $lcp_data = [], $missing_dimensions = [], $missing_srcsets = [], $crop_status = [] ) {
Expand Down Expand Up @@ -384,7 +384,9 @@ public function is_in_all_viewports( int $image_id ): bool {
*/
public function is_lcp_image_in_all_viewports( int $image_id ): bool {
foreach ( self::get_active_devices() as $device ) {
if ( ( ( self::$current_profile_data[ $device ]['lcp']['type'] ?? '' ) === 'img' ) && ( ( self::$current_profile_data[ $device ]['lcp']['imageId'] ?? null ) === $image_id ) ) {
$lcp = self::$current_profile_data[ $device ]['lcp'] ?? [];
// Profiles stored before the REST handler cast the ID hold it as a numeric string.
if ( ( $lcp['type'] ?? '' ) === 'img' && isset( $lcp['imageId'] ) && (int) $lcp['imageId'] === $image_id ) {
return true;
}
}
Expand Down
79 changes: 79 additions & 0 deletions tests/test-lazyload-viewport.php
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,85 @@ public function test_lazyload_viewport_with_lcp_data() {
$this->assertStringNotContainsString('data-opt-src', $lcpImageTag);
}

/**
* The LCP image reported through the optimizations endpoint is stored as an int and prioritized.
*/
public function test_lcp_image_reported_via_rest_gets_fetchpriority(): void {
// The page capture buffer is not under test; hooks are restored by WP_UnitTestCase.
remove_action('rest_api_init', [Optml_Manager::instance(), 'process_template_redirect_content'], PHP_INT_MIN);
foreach (Profile::get_active_devices() as $device) {
$response = rest_get_server()->dispatch($this->buildOptimizationsRequest($device, 1579989818));
$this->assertSame(200, $response->get_status());
}

$stored = Optml_Manager::instance()->page_profiler->get_profile_data((string) self::mock_page_ids());
$this->assertSame(1579989818, $stored[Profile::DEVICE_TYPE_DESKTOP]['lcp']['imageId']);

$this->assertLcpImagePrioritized(Optml_Manager::instance()->replace_content(self::get_sample_html()));
}

/**
* Profiles that stored the LCP image ID as a numeric string still prioritize that image.
*/
public function test_lcp_image_with_numeric_string_id_gets_fetchpriority(): void {
foreach (Profile::get_active_devices() as $device) {
$this->storeMockProfileData(self::mock_page_ids(), $device, [], [], ['type' => 'img', 'imageId' => '1579989818']);
}

$this->assertLcpImagePrioritized(Optml_Manager::instance()->replace_content(self::get_sample_html()));
}

/**
* Build a signed optimizations request as the frontend profiler sends it.
*
* @param int $device Device type.
* @param int $image_id LCP image ID.
*/
private function buildOptimizationsRequest(int $device, int $image_id): WP_REST_Request {
$time = time();
$url = (string) self::mock_page_ids();
$page = home_url('/');
$request = new WP_REST_Request('POST', '/' . OPTML_NAMESPACE . '/v1/optimizations');
$request->set_header('Content-Type', 'application/json');
$request->set_header('Origin', home_url());
$request->set_body(
wp_json_encode(
[
'd' => $device,
'a' => [],
'b' => [],
'u' => $url,
't' => $time,
'h' => wp_hash($url . $time . $page, 'nonce'),
'pu' => $page,
'l' => ['i' => $image_id, 's' => '', 'u' => []],
]
)
);

return $request;
}

/**
* Assert the LCP sample image is not lazy-loaded and has fetchpriority="high".
*
* @param string $html Replaced page HTML.
*/
private function assertLcpImagePrioritized(string $html): void {
$imageTags = Optml_Manager::instance()->parse_images_from_html($html);
$lcpTags = array_values(
array_filter(
$imageTags[0],
function (string $tag): bool {
return strpos($tag, 'data-opt-id=1579989818') !== false;
}
)
);
$this->assertNotEmpty($lcpTags);
$this->assertStringContainsString('fetchpriority="high"', $lcpTags[0]);
$this->assertStringNotContainsString('data-opt-src', $lcpTags[0]);
}

/**
* Test lazy loading with different profiles for different devices.
*
Expand Down
Loading