From a95ce3056814595b1f584e791a98bd9b1578e2fa Mon Sep 17 00:00:00 2001 From: Dennis Snell Date: Tue, 7 Mar 2023 19:12:25 -0700 Subject: [PATCH 1/5] WIP: Explore using the HTML API for link rel processing --- src/wp-includes/formatting.php | 94 ++++++++++------------------------ 1 file changed, 26 insertions(+), 68 deletions(-) diff --git a/src/wp-includes/formatting.php b/src/wp-includes/formatting.php index be37097220129..109634fe2a9f5 100644 --- a/src/wp-includes/formatting.php +++ b/src/wp-includes/formatting.php @@ -3306,89 +3306,47 @@ static function( $matches ) { * * @since 5.1.0 * @since 5.6.0 Removed 'noreferrer' relationship. + * @since 6.3.0 Rely on the Tag Processor for HTML searching and modification. * * @param string $text Content that may contain HTML A elements. * @return string Converted content. */ function wp_targeted_link_rel( $text ) { - // Don't run (more expensive) regex if no links with targets. + // Don't run (more expensive) code if no links with targets are possible. if ( stripos( $text, 'target' ) === false || stripos( $text, ']*target\s*=[^>]*)>|i', 'wp_targeted_link_rel_callback', $part ); - } - - $text = ''; - for ( $i = 0; $i < count( $html_parts ); $i++ ) { - $text .= $html_parts[ $i ]; - if ( isset( $extra_parts[ $i ] ) ) { - $text .= $extra_parts[ $i ]; + $p = new WP_HTML_Tag_Processor( $text ); + while ( $p->next_tag( 'a' ) ) { + $target = $p->get_attribute( 'target' ); + if ( null === $target ) { + continue; } - } - - return $text; -} - -/** - * Callback to add `rel="noopener"` string to HTML A element. - * - * Will not duplicate an existing 'noopener' value to avoid invalidating the HTML. - * - * @since 5.1.0 - * @since 5.6.0 Removed 'noreferrer' relationship. - * - * @param array $matches Single match. - * @return string HTML A Element with `rel="noopener"` in addition to any existing values. - */ -function wp_targeted_link_rel_callback( $matches ) { - $link_html = $matches[1]; - $original_link_html = $link_html; - - // Consider the HTML escaped if there are no unescaped quotes. - $is_escaped = ! preg_match( '/(^|[^\\\\])[\'"]/', $link_html ); - if ( $is_escaped ) { - // Replace only the quotes so that they are parsable by wp_kses_hair(), leave the rest as is. - $link_html = preg_replace( '/\\\\([\'"])/', '$1', $link_html ); - } - - $atts = wp_kses_hair( $link_html, wp_allowed_protocols() ); - - /** - * Filters the rel values that are added to links with `target` attribute. - * - * @since 5.1.0 - * - * @param string $rel The rel values. - * @param string $link_html The matched content of the link tag including all HTML attributes. - */ - $rel = apply_filters( 'wp_targeted_link_rel', 'noopener', $link_html ); - // Return early if no rel values to be added or if no actual target attribute. - if ( ! $rel || ! isset( $atts['target'] ) ) { - return ""; - } - - if ( isset( $atts['rel'] ) ) { - $all_parts = preg_split( '/\s/', "{$atts['rel']['value']} $rel", -1, PREG_SPLIT_NO_EMPTY ); - $rel = implode( ' ', array_unique( $all_parts ) ); - } + $rel = $p->get_attribute( 'rel' ); + $rel = true === $rel ? "" : $rel; + $link_text = "rel=\"{$rel}\""; - $atts['rel']['whole'] = 'rel="' . esc_attr( $rel ) . '"'; - $link_html = implode( ' ', array_column( $atts, 'whole' ) ); + /** + * Filters the rel values that are added to links with `target` attribute. + * + * @since 5.1.0 + * + * @param string $rel The rel values. + * @param string $link_html The matched content of the link tag including all HTML attributes. + */ + $updated_rel = apply_filters( 'wp_targeted_link_rel', 'noopener', $link_text ); + if ( ! $updated_rel ) { + continue; + } - if ( $is_escaped ) { - $link_html = preg_replace( '/[\'"]/', '\\\\$0', $link_html ); + $all_parts = preg_split( '/\s/', "$rel $updated_rel", -1, PREG_SPLIT_NO_EMPTY ); + $new_rel = implode( ' ', array_unique( $all_parts ) ); + $p->set_attribute( 'rel', $new_rel ); } - return ""; + return $p->get_updated_html(); } /** From d78f35eb710ee8ced2f2702d03e4e47103141555 Mon Sep 17 00:00:00 2001 From: Dennis Snell Date: Tue, 5 Sep 2023 13:48:50 -0700 Subject: [PATCH 2/5] Refactor a bit --- src/wp-includes/formatting.php | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/src/wp-includes/formatting.php b/src/wp-includes/formatting.php index 109634fe2a9f5..a76c49c758b0e 100644 --- a/src/wp-includes/formatting.php +++ b/src/wp-includes/formatting.php @@ -3318,19 +3318,18 @@ function wp_targeted_link_rel( $text ) { } $p = new WP_HTML_Tag_Processor( $text ); - while ( $p->next_tag( 'a' ) ) { - $target = $p->get_attribute( 'target' ); - if ( null === $target ) { - continue; - } - - $rel = $p->get_attribute( 'rel' ); - $rel = true === $rel ? "" : $rel; - $link_text = "rel=\"{$rel}\""; + while ( $p->next_tag( 'a' ) && ! is_string( $p->get_attribute( 'target' ) ) ) { + $href = $p->get_attribute( 'href' ); + $rel = $p->get_attribute( 'rel' ); + $rel = true === $rel ? "" : $rel; + $link_text = sprintf( 'href="%s" rel="%s"', esc_attr( $href ), esc_attr( $rel ) ); /** * Filters the rel values that are added to links with `target` attribute. * + * @TODO: Some plugins scan the link text, though they mostly scan for URL patterns. + * We need to provide the existing tag HTML or find a way to update filters. + * * @since 5.1.0 * * @param string $rel The rel values. @@ -3341,9 +3340,8 @@ function wp_targeted_link_rel( $text ) { continue; } - $all_parts = preg_split( '/\s/', "$rel $updated_rel", -1, PREG_SPLIT_NO_EMPTY ); - $new_rel = implode( ' ', array_unique( $all_parts ) ); - $p->set_attribute( 'rel', $new_rel ); + $all_rel_parts = preg_split( '/\s/', "$rel $updated_rel", -1, PREG_SPLIT_NO_EMPTY ); + $p->set_attribute( 'rel', implode( ' ', array_unique( $all_rel_parts ) ) ); } return $p->get_updated_html(); From f01955ccc9eb1bea96f614ec0e66a348ebad4f03 Mon Sep 17 00:00:00 2001 From: Dennis Snell Date: Tue, 5 Sep 2023 15:26:45 -0700 Subject: [PATCH 3/5] Move that continue conditional back inside the loop --- src/wp-includes/formatting.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/wp-includes/formatting.php b/src/wp-includes/formatting.php index a76c49c758b0e..c1eb2c878a115 100644 --- a/src/wp-includes/formatting.php +++ b/src/wp-includes/formatting.php @@ -3318,10 +3318,14 @@ function wp_targeted_link_rel( $text ) { } $p = new WP_HTML_Tag_Processor( $text ); - while ( $p->next_tag( 'a' ) && ! is_string( $p->get_attribute( 'target' ) ) ) { + while ( $p->next_tag( 'a' ) ) { + if ( ! is_string( $p->get_attribute( 'target' ) ) ) { + continue; + } + $href = $p->get_attribute( 'href' ); $rel = $p->get_attribute( 'rel' ); - $rel = true === $rel ? "" : $rel; + $rel = is_string( $rel ) ? $rel : ''; $link_text = sprintf( 'href="%s" rel="%s"', esc_attr( $href ), esc_attr( $rel ) ); /** From 48ff3d04b9797db49860b457ba0271f80a12cf0d Mon Sep 17 00:00:00 2001 From: Dennis Snell Date: Tue, 5 Sep 2023 16:27:54 -0700 Subject: [PATCH 4/5] Use assertEqualMarkup instead of assertSame --- .../tests/formatting/wpTargetedLinkRel.php | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/tests/phpunit/tests/formatting/wpTargetedLinkRel.php b/tests/phpunit/tests/formatting/wpTargetedLinkRel.php index 75a247e212caa..f73c85c02726e 100644 --- a/tests/phpunit/tests/formatting/wpTargetedLinkRel.php +++ b/tests/phpunit/tests/formatting/wpTargetedLinkRel.php @@ -11,13 +11,13 @@ class Tests_Formatting_wpTargetedLinkRel extends WP_UnitTestCase { public function test_add_to_links_with_target_blank() { $content = '

Links: No rel

'; $expected = '

Links: No rel

'; - $this->assertSame( $expected, wp_targeted_link_rel( $content ) ); + $this->assertEqualMarkup( $expected, wp_targeted_link_rel( $content ) ); } public function test_add_to_links_with_target_foo() { $content = '

Links: No rel

'; $expected = '

Links: No rel

'; - $this->assertSame( $expected, wp_targeted_link_rel( $content ) ); + $this->assertEqualMarkup( $expected, wp_targeted_link_rel( $content ) ); } public function test_target_as_first_attribute() { @@ -29,7 +29,7 @@ public function test_target_as_first_attribute() { public function test_add_to_existing_rel() { $content = '

Links: Existing rel

'; $expected = '

Links: Existing rel

'; - $this->assertSame( $expected, wp_targeted_link_rel( $content ) ); + $this->assertEqualMarkup( $expected, wp_targeted_link_rel( $content ) ); } public function test_no_duplicate_values_added() { @@ -41,31 +41,31 @@ public function test_no_duplicate_values_added() { public function test_rel_with_single_quote_delimiter() { $content = '

Links: Existing rel

'; $expected = '

Links: Existing rel

'; - $this->assertSame( $expected, wp_targeted_link_rel( $content ) ); + $this->assertEqualMarkup( $expected, wp_targeted_link_rel( $content ) ); } public function test_rel_with_no_delimiter() { $content = '

Links: Existing rel

'; $expected = '

Links: Existing rel

'; - $this->assertSame( $expected, wp_targeted_link_rel( $content ) ); + $this->assertEqualMarkup( $expected, wp_targeted_link_rel( $content ) ); } public function test_rel_value_spaced_and_no_delimiter() { $content = '

Links: Existing rel

'; $expected = '

Links: Existing rel

'; - $this->assertSame( $expected, wp_targeted_link_rel( $content ) ); + $this->assertEqualMarkup( $expected, wp_targeted_link_rel( $content ) ); } public function test_escaped_quotes() { $content = '

Links: Existing rel

'; $expected = '

Links: Existing rel

'; - $this->assertSame( $expected, wp_targeted_link_rel( $content ) ); + $this->assertEqualMarkup( $expected, wp_targeted_link_rel( $content ) ); } public function test_ignore_links_with_no_target() { $content = '

Links: Change me Do not change me

'; $expected = '

Links: Change me Do not change me

'; - $this->assertSame( $expected, wp_targeted_link_rel( $content ) ); + $this->assertEqualMarkup( $expected, wp_targeted_link_rel( $content ) ); } /** @@ -77,7 +77,7 @@ public function test_ignore_if_wp_targeted_link_rel_nulled() { add_filter( 'wp_targeted_link_rel', '__return_empty_string' ); $content = '

Links: Do not change me

'; $expected = '

Links: Do not change me

'; - $this->assertSame( $expected, wp_targeted_link_rel( $content ) ); + $this->assertEqualMarkup( $expected, wp_targeted_link_rel( $content ) ); } /** @@ -95,7 +95,7 @@ public function test_wp_targeted_link_rel_filters_run() { ) ); - $this->assertSame( $expected, $post->post_content ); + $this->assertEqualMarkup( $expected, $post->post_content ); } /** @@ -106,7 +106,7 @@ public function test_wp_targeted_link_rel_filters_run() { public function test_wp_targeted_link_rel_should_preserve_json() { $content = '

Links: No rel<\/a><\/p>'; $expected = '

Links: No rel<\/a><\/p>'; - $this->assertSame( $expected, wp_targeted_link_rel( $content ) ); + $this->assertEqualMarkup( $expected, wp_targeted_link_rel( $content ) ); } /** @@ -117,7 +117,7 @@ public function test_wp_targeted_link_rel_should_preserve_json() { public function test_wp_targeted_link_rel_skips_style_and_scripts() { $content = '

Links: here aq

'; $expected = '

Links: here aq

'; - $this->assertSame( $expected, wp_targeted_link_rel( $content ) ); + $this->assertEqualMarkup( $expected, wp_targeted_link_rel( $content ) ); } /** @@ -128,13 +128,13 @@ public function test_wp_targeted_link_rel_skips_style_and_scripts() { public function test_ignore_entirely_serialized_content() { $content = 'a:1:{s:4:"html";s:52:"

Links: No Rel

";}'; $expected = 'a:1:{s:4:"html";s:52:"

Links: No Rel

";}'; - $this->assertSame( $expected, wp_targeted_link_rel( $content ) ); + $this->assertEqualMarkup( $expected, wp_targeted_link_rel( $content ) ); } public function test_wp_targeted_link_rel_tab_separated_values_are_split() { $content = "

Links: No rel

"; $expected = '

Links: No rel

'; - $this->assertSame( $expected, wp_targeted_link_rel( $content ) ); + $this->assertEqualMarkup( $expected, wp_targeted_link_rel( $content ) ); } } From 13be7b3a034e5e4f8b71a44d67896336a459a029 Mon Sep 17 00:00:00 2001 From: Dennis Snell Date: Tue, 5 Sep 2023 17:59:10 -0700 Subject: [PATCH 5/5] More updates to tests --- .../tests/formatting/wpTargetedLinkRel.php | 2 +- .../rest-api/rest-attachments-controller.php | 6 +- .../tests/widgets/wpWidgetMediaImage.php | 86 +++++++++++++------ 3 files changed, 65 insertions(+), 29 deletions(-) diff --git a/tests/phpunit/tests/formatting/wpTargetedLinkRel.php b/tests/phpunit/tests/formatting/wpTargetedLinkRel.php index f73c85c02726e..1fc74a25cc560 100644 --- a/tests/phpunit/tests/formatting/wpTargetedLinkRel.php +++ b/tests/phpunit/tests/formatting/wpTargetedLinkRel.php @@ -23,7 +23,7 @@ public function test_add_to_links_with_target_foo() { public function test_target_as_first_attribute() { $content = '

Links: No rel

'; $expected = '

Links: No rel

'; - $this->assertSame( $expected, wp_targeted_link_rel( $content ) ); + $this->assertEqualMarkup( $expected, wp_targeted_link_rel( $content ) ); } public function test_add_to_existing_rel() { diff --git a/tests/phpunit/tests/rest-api/rest-attachments-controller.php b/tests/phpunit/tests/rest-api/rest-attachments-controller.php index 55cf74f3d72af..9b6ca6c21d0dc 100644 --- a/tests/phpunit/tests/rest-api/rest-attachments-controller.php +++ b/tests/phpunit/tests/rest-api/rest-attachments-controller.php @@ -1211,11 +1211,11 @@ public function verify_attachment_roundtrip( $input = array(), $expected_output } // Compare expected API output to actual API output. - $this->assertSame( $expected_output['title']['raw'], $actual_output['title']['raw'] ); + $this->assertEqualMarkup( $expected_output['title']['raw'], $actual_output['title']['raw'] ); $this->assertSame( $expected_output['title']['rendered'], trim( $actual_output['title']['rendered'] ) ); - $this->assertSame( $expected_output['description']['raw'], $actual_output['description']['raw'] ); + $this->assertEqualMarkup( $expected_output['description']['raw'], $actual_output['description']['raw'] ); $this->assertSame( $expected_output['description']['rendered'], trim( $actual_output['description']['rendered'] ) ); - $this->assertSame( $expected_output['caption']['raw'], $actual_output['caption']['raw'] ); + $this->assertEqualMarkup( $expected_output['caption']['raw'], $actual_output['caption']['raw'] ); $this->assertSame( $expected_output['caption']['rendered'], trim( $actual_output['caption']['rendered'] ) ); // Compare expected API output to WP internal values. diff --git a/tests/phpunit/tests/widgets/wpWidgetMediaImage.php b/tests/phpunit/tests/widgets/wpWidgetMediaImage.php index 3f96cf34ebb82..3b2ea72426738 100644 --- a/tests/phpunit/tests/widgets/wpWidgetMediaImage.php +++ b/tests/phpunit/tests/widgets/wpWidgetMediaImage.php @@ -449,12 +449,23 @@ public function test_render_media() { ); $output = ob_get_clean(); + $p = new WP_HTML_Tag_Processor( $output ); + // No default title. - $this->assertStringNotContainsString( 'title="', $output ); + $this->assertTrue( $p->next_tag( 'a' ), 'Failed to find wrapping A tag.' ); + $this->assertNull( $p->get_attribute( 'title' ), "Should not have returned title but found: {$p->get_attribute( 'title' )}." ); + // Default image classes. - $this->assertStringContainsString( 'class="image wp-image-' . $attachment_id, $output ); - $this->assertStringContainsString( 'style="max-width: 100%; height: auto;"', $output ); - $this->assertStringContainsString( 'alt=""', $output ); + $this->assertTrue( $p->next_tag( 'img' ), 'Failed to find IMG tag.' ); + $expected_classes = array( 'image', "wp-image-{$attachment_id}" ); + $this->assertSame( + $expected_classes, + array_intersect( $expected_classes, preg_split( '~[ \t\f\r\n]~', $p->get_attribute( 'class' ), -1, PREG_SPLIT_NO_EMPTY ) ), + "Expected 'image' and 'wp-image-{$attachment_id}' classes but instead found: {$p->get_attribute( 'class' )}" + ); + + $this->assertEquals( 'max-width: 100%; height: auto;', $p->get_attribute( 'style' ), "Found unexpected style values." ); + $this->assertEquals( '', $p->get_attribute( 'alt' ), 'Expected to find empty alt attribute.' ); ob_start(); $widget->render_media( @@ -470,13 +481,24 @@ public function test_render_media() { ); $output = ob_get_clean(); + $p = new WP_HTML_Tag_Processor( $output ); + // Custom image title. - $this->assertStringContainsString( 'title="Custom Title"', $output ); + $this->assertTrue( $p->next_tag( 'a' ), 'Failed to find wrapping A tag.' ); + $this->assertEquals( 'Custom Title', $p->get_attribute( 'title' ), 'Did not return proper title attribute.' ); + // Custom image class. - $this->assertStringContainsString( 'class="image wp-image-' . $attachment_id . ' custom-class', $output ); - $this->assertStringContainsString( 'alt="A flower"', $output ); - $this->assertStringContainsString( 'width="100"', $output ); - $this->assertStringContainsString( 'height="100"', $output ); + $this->assertTrue( $p->next_tag( 'img' ), 'Failed to find IMG tag.' ); + $expected_classes = array( 'image', "wp-image-{$attachment_id}", 'custom-class' ); + $this->assertSame( + $expected_classes, + array_intersect( $expected_classes, preg_split( '~[ \t\f\r\n]~', $p->get_attribute( 'class' ), -1, PREG_SPLIT_NO_EMPTY ) ), + "Expected 'image', 'wp-image-{$attachment_id}', and 'custom-class' classes but instead found: {$p->get_attribute( 'class' )}" + ); + + $this->assertEquals( '100', $p->get_attribute( 'height' ), 'Returned wrong height attribute value.' ); + $this->assertEquals( '100', $p->get_attribute( 'width' ), 'Returned wrong width attribute value.' ); + $this->assertEquals( 'A flower', $p->get_attribute( 'alt' ), 'Returned wrong alt attribute value.' ); // Embeded images. ob_start(); @@ -492,10 +514,13 @@ public function test_render_media() { ); $output = ob_get_clean(); + $p = new WP_HTML_Tag_Processor( $output ); + // Custom image class. - $this->assertStringContainsString( 'src="http://example.org/url/to/image.jpg"', $output ); - $this->assertStringContainsString( 'decoding="async"', $output ); - $this->assertStringContainsString( 'loading="lazy"', $output ); + $this->assertTrue( $p->next_tag( 'img' ), 'Failed to find IMG tag.' ); + $this->assertEquals( 'http://example.org/url/to/image.jpg', $p->get_attribute( 'src' ), 'Returned wrong src attribute value.' ); + $this->assertEquals( 'async', $p->get_attribute( 'decoding' ), 'Returned wrong decoding attribute value.' ); + $this->assertEquals( 'lazy', $p->get_attribute( 'loading' ), 'Returned wrong loading attribute value.' ); // Link settings. ob_start(); @@ -507,12 +532,13 @@ public function test_render_media() { ); $output = ob_get_clean(); - $link = 'assertStringContainsString( $link, $output ); - $this->assertTrue( (bool) preg_match( '##', $output, $matches ) ); - $this->assertStringNotContainsString( ' class="', $matches[0] ); - $this->assertStringNotContainsString( ' rel="', $matches[0] ); - $this->assertStringNotContainsString( ' target="', $matches[0] ); + $p = new WP_HTML_Tag_Processor( $output ); + + $this->assertTrue( $p->next_tag( 'a' ), 'Failed to find A tag.' ); + $this->assertEquals( wp_get_attachment_url( $attachment_id ), $p->get_attribute( 'href' ), 'Returned wrong href attribute value.' ); + $this->assertNull( $p->get_attribute( 'class' ), "Should not have found class attribute but instead found: {$p->get_attribute( 'class' )}." ); + $this->assertNull( $p->get_attribute( 'rel' ), "Should not have found rel attribute but instead found: {$p->get_attribute( 'rel' )}." ); + $this->assertNull( $p->get_attribute( 'target' ), "Should not have found target attribute but instead found: {$p->get_attribute( 'target' )}." ); ob_start(); $widget->render_media( @@ -526,10 +552,13 @@ public function test_render_media() { ); $output = ob_get_clean(); - $this->assertStringContainsString( 'assertStringContainsString( 'class="custom-link-class"', $output ); - $this->assertStringContainsString( 'rel="attachment"', $output ); - $this->assertStringNotContainsString( 'target=""', $output ); + $p = new WP_HTML_Tag_Processor( $output ); + + $this->assertTrue( $p->next_tag( 'a' ), 'Failed to find A tag.' ); + $this->assertEquals( wp_get_attachment_link( $attachment_id ), $p->get_attribute( 'href' ), 'Returned wrong href attribute value.' ); + $this->assertEquals( 'custom-link-class', $p->get_attribute( 'class' ), 'Returned wrong class attribute value.' ); + $this->assertEquals( 'attachment', $p->get_attribute( 'rel' ), 'Returned wrong rel attribute value.' ); + $this->assertNull( $p->get_attribute( 'target' ), "Should not have found target attribute but instead found: {$p->get_attribute( 'target' )}." ); ob_start(); $widget->render_media( @@ -542,9 +571,12 @@ public function test_render_media() { ); $output = ob_get_clean(); - $this->assertStringContainsString( 'assertStringContainsString( 'target="_blank"', $output ); - $this->assertStringContainsString( 'rel="noopener"', $output ); + $p = new WP_HTML_Tag_Processor( $output ); + + $this->assertTrue( $p->next_tag( 'a' ), 'Failed to find A tag.' ); + $this->assertEquals( 'https://example.org', $p->get_attribute( 'href' ), 'Returned wrong href attribute value.' ); + $this->assertEquals( 'noopener', $p->get_attribute( 'rel' ), 'Returned wrong rel attribute value.' ); + $this->assertEquals( '_blank', $p->get_attribute( 'target' ), 'Returned wrong target attribute value.' ); // Populate caption in attachment. wp_update_post( @@ -562,6 +594,7 @@ public function test_render_media() { ) ); $output = ob_get_clean(); + // @TODO: Update this test once $p->has_class() is available. $this->assertStringNotContainsString( 'wp-caption', $output ); $this->assertStringNotContainsString( '

', $output ); @@ -574,6 +607,7 @@ public function test_render_media() { ) ); $output = ob_get_clean(); + // @TODO: Update this test once $p->has_class() is available. $this->assertStringContainsString( 'class="wp-caption alignnone"', $output ); $this->assertStringContainsString( '

Default caption

', $output ); @@ -586,6 +620,7 @@ public function test_render_media() { ) ); $output = ob_get_clean(); + // @TODO: Update this test once $p->has_class() is available. $this->assertStringContainsString( 'class="wp-caption alignnone"', $output ); $this->assertStringContainsString( '

Custom caption

', $output ); @@ -601,6 +636,7 @@ public function test_render_media() { ) ); $output = ob_get_clean(); + // @TODO: Update this test once $p->has_class() is available. $this->assertStringContainsString( 'style="width: 310px"', $output ); $this->assertStringContainsString( '

Caption for an image with custom size

', $output ); }