diff --git a/Tests/Unit/classes/Picture/BuildPictureTagTest.php b/Tests/Unit/classes/Picture/BuildPictureTagTest.php new file mode 100644 index 000000000..6598e8bdf --- /dev/null +++ b/Tests/Unit/classes/Picture/BuildPictureTagTest.php @@ -0,0 +1,189 @@ +returnArg(); + } + + /** + * Get an instance of Display without invoking the constructor (its only + * dependency, Imagify_Filesystem, is not needed by build_picture_tag()). + * + * @return Display + */ + private function get_display_instance(): Display { + $reflection = new ReflectionClass( Display::class ); + + return $reflection->newInstanceWithoutConstructor(); + } + + /** + * Build a minimal $image array as expected by build_picture_tag(), build_source_tag() + * and build_img_tag(). $attributes is the raw attributes array. + * + * @param array $attributes The attributes. + * + * @return array + */ + private function get_image_data( array $attributes ): array { + return [ + 'attributes' => $attributes, + 'src_attribute' => 'src', + 'srcset_attribute' => false, + 'srcset' => [], + 'src' => [], + ]; + } + + /** + * Extract the opening tag's attribute string from the built output. + * + * @param string $output The output of build_picture_tag(). + * + * @return string + */ + private function get_picture_opening_tag( string $output ): string { + preg_match( '/^]*)>/', $output, $matches ); + + return $matches[1] ?? ''; + } + + /** + * Extract the tag's attribute string from the built output. + * + * @param string $output The output of build_picture_tag(). + * + * @return string + */ + private function get_img_tag( string $output ): string { + preg_match( '/]*)\/>/', $output, $matches ); + + return $matches[1] ?? ''; + } + + /** + * `data-wp-*` directive attributes must be filtered out of the tag, + * but kept on the inner tag. + */ + public function testDataWpAttributesAreExcludedFromPictureTagButKeptOnImgTag(): void { + $attributes = [ + 'data-object-fit' => 'cover', + 'data-wp-interactive' => 'core/image', + 'data-wp-context' => '{"isOpen":false}', + 'data-wp-on--click' => 'actions.showLightbox', + ]; + + $image = $this->get_image_data( $attributes ); + + $method = $this->get_reflective_method( 'build_picture_tag', Display::class ); + $output = $method->invoke( $this->get_display_instance(), $image ); + + $picture_tag = $this->get_picture_opening_tag( $output ); + $img_tag = $this->get_img_tag( $output ); + + $this->assertStringNotContainsString( 'data-wp-interactive', $picture_tag ); + $this->assertStringNotContainsString( 'data-wp-context', $picture_tag ); + $this->assertStringNotContainsString( 'data-wp-on--click', $picture_tag ); + + $this->assertStringContainsString( 'data-wp-interactive="core/image"', $img_tag ); + $this->assertStringContainsString( 'data-wp-context="{"isOpen":false}"', $img_tag ); + $this->assertStringContainsString( 'data-wp-on--click="actions.showLightbox"', $img_tag ); + } + + /** + * Non `data-wp` attributes are unaffected by the new filter and keep behaving + * as they did before: they remain on the tag, and follow the + * pre-existing removal rules on the tag. + */ + public function testNonDataWpAttributesAreUnaffected(): void { + $attributes = [ + 'data-object-fit' => 'cover', + 'class' => 'my-image', + 'id' => 'img-1', + ]; + + $image = $this->get_image_data( $attributes ); + + $method = $this->get_reflective_method( 'build_picture_tag', Display::class ); + $output = $method->invoke( $this->get_display_instance(), $image ); + + $picture_tag = $this->get_picture_opening_tag( $output ); + $img_tag = $this->get_img_tag( $output ); + + // All 3 attributes remain on the tag, since none of them contain 'data-wp'. + $this->assertStringContainsString( 'data-object-fit="cover"', $picture_tag ); + $this->assertStringContainsString( 'class="my-image"', $picture_tag ); + $this->assertStringContainsString( 'id="img-1"', $picture_tag ); + + // Pre-existing logic (unrelated to this fix) strips class/id from the tag, + // but keeps data-object-fit. + $this->assertStringContainsString( 'data-object-fit="cover"', $img_tag ); + $this->assertStringNotContainsString( 'class=', $img_tag ); + $this->assertStringNotContainsString( 'id=', $img_tag ); + } + + /** + * The new `data-wp` filter composes correctly with the pre-existing + * 'wp-block-cover__image-background' exclusion logic: both sets of + * attributes are removed from the tag, while the tag + * (which is treated as a Gutenberg cover background image) keeps + * everything, including the `data-wp` attributes. + */ + public function testDataWpFilterComposesWithGutenbergCoverExclusion(): void { + $attributes = [ + 'class' => 'wp-block-cover__image-background', + 'style' => 'object-fit:cover', + 'data-object-fit' => 'cover', + 'data-object-position' => '50% 50%', + 'data-wp-interactive' => 'core/image', + 'data-wp-context' => '{}', + ]; + + $image = $this->get_image_data( $attributes ); + + $method = $this->get_reflective_method( 'build_picture_tag', Display::class ); + $output = $method->invoke( $this->get_display_instance(), $image ); + + $picture_tag = $this->get_picture_opening_tag( $output ); + $img_tag = $this->get_img_tag( $output ); + + // None of the Gutenberg-specific attributes, nor the data-wp attributes, remain on . + $this->assertStringNotContainsString( 'class=', $picture_tag ); + $this->assertStringNotContainsString( 'style=', $picture_tag ); + $this->assertStringNotContainsString( 'data-object-fit', $picture_tag ); + $this->assertStringNotContainsString( 'data-object-position', $picture_tag ); + $this->assertStringNotContainsString( 'data-wp-interactive', $picture_tag ); + $this->assertStringNotContainsString( 'data-wp-context', $picture_tag ); + $this->assertSame( '', trim( $picture_tag ) ); + + // The tag, being the Gutenberg cover background image, keeps everything + // (only 'id' and 'title' are ever stripped from it in that branch). + $this->assertStringContainsString( 'class="wp-block-cover__image-background"', $img_tag ); + $this->assertStringContainsString( 'style="object-fit:cover"', $img_tag ); + $this->assertStringContainsString( 'data-object-fit="cover"', $img_tag ); + $this->assertStringContainsString( 'data-object-position="50% 50%"', $img_tag ); + $this->assertStringContainsString( 'data-wp-interactive="core/image"', $img_tag ); + $this->assertStringContainsString( 'data-wp-context="{}"', $img_tag ); + } +} diff --git a/classes/Picture/Display.php b/classes/Picture/Display.php index 38848e874..c2bc2ff49 100644 --- a/classes/Picture/Display.php +++ b/classes/Picture/Display.php @@ -218,7 +218,15 @@ protected function build_picture_tag( $image ) { unset( $attributes['data-object-position'] ); } - $output = 'build_attributes( $attributes ) . ">\n"; + $picture_attributes = array_filter( + $attributes, + function ( $attribute ) { + return strpos( $attribute, 'data-wp' ) === false; + }, + ARRAY_FILTER_USE_KEY + ); + + $output = 'build_attributes( $picture_attributes ) . ">\n"; /** * Allow to add more tags to the tag. *