summaryrefslogtreecommitdiffstatshomepage
path: root/tests/phpunit/includes
diff options
context:
space:
mode:
Diffstat (limited to 'tests/phpunit/includes')
-rw-r--r--tests/phpunit/includes/abstract-testcase.php40
-rw-r--r--tests/phpunit/includes/build-visual-html-tree.php304
-rw-r--r--tests/phpunit/includes/factory/class-wp-unittest-factory-for-attachment.php5
-rw-r--r--tests/phpunit/includes/object-cache.php8
4 files changed, 350 insertions, 7 deletions
diff --git a/tests/phpunit/includes/abstract-testcase.php b/tests/phpunit/includes/abstract-testcase.php
index f665bdafb1..6750df13c7 100644
--- a/tests/phpunit/includes/abstract-testcase.php
+++ b/tests/phpunit/includes/abstract-testcase.php
@@ -1,5 +1,6 @@
<?php
+require_once __DIR__ . '/build-visual-html-tree.php';
require_once __DIR__ . '/factory.php';
require_once __DIR__ . '/trac.php';
@@ -13,7 +14,6 @@ require_once __DIR__ . '/trac.php';
* All WordPress unit tests should inherit from this class.
*/
abstract class WP_UnitTestCase_Base extends PHPUnit_Adapter_TestCase {
-
protected static $forced_tickets = array();
protected $expected_deprecated = array();
protected $caught_deprecated = array();
@@ -1181,6 +1181,44 @@ abstract class WP_UnitTestCase_Base extends PHPUnit_Adapter_TestCase {
}
/**
+ * Check HTML markup (including blocks) for semantic equivalence.
+ *
+ * Given two markup strings, assert that they translate to the same semantic HTML tree,
+ * normalizing tag names, attribute names, and attribute order. Furthermore, attributes
+ * and class names are sorted and deduplicated, and whitespace in style attributes
+ * is normalized. Finally, block delimiter comments are recognized and normalized,
+ * applying the same principles.
+ *
+ * @since 6.9.0
+ *
+ * @param string $expected The expected HTML.
+ * @param string $actual The actual HTML.
+ * @param string|null $fragment_context Optional. The fragment context, for example "<td>" expected HTML
+ * must occur within "<table><tr>" fragment context. Default "<body>".
+ * Only "<body>" or `null` are supported at this time.
+ * Set to `null` to parse a full HTML document.
+ * @param string|null $message Optional. The assertion error message.
+ */
+ public function assertEqualHTML( string $expected, string $actual, ?string $fragment_context = '<body>', $message = 'HTML markup was not equivalent.' ): void {
+ try {
+ $tree_expected = build_visual_html_tree( $expected, $fragment_context );
+ $tree_actual = build_visual_html_tree( $actual, $fragment_context );
+ } catch ( Exception $e ) {
+ // For PHP 8.4+, we can retry, using the built-in DOM\HTMLDocument parser.
+ if ( class_exists( 'DOM\HtmlDocument' ) ) {
+ $dom_expected = DOM\HtmlDocument::createFromString( $expected, LIBXML_NOERROR );
+ $tree_expected = build_visual_html_tree( $dom_expected->saveHtml(), $fragment_context );
+ $dom_actual = DOM\HtmlDocument::createFromString( $actual, LIBXML_NOERROR );
+ $tree_actual = build_visual_html_tree( $dom_actual->saveHtml(), $fragment_context );
+ } else {
+ throw $e;
+ }
+ }
+
+ $this->assertSame( $tree_expected, $tree_actual, $message );
+ }
+
+ /**
* Helper function to convert a single-level array containing text strings to a named data provider.
*
* The value of the data set will also be used as the name of the data set.
diff --git a/tests/phpunit/includes/build-visual-html-tree.php b/tests/phpunit/includes/build-visual-html-tree.php
new file mode 100644
index 0000000000..e0f6c3ac3d
--- /dev/null
+++ b/tests/phpunit/includes/build-visual-html-tree.php
@@ -0,0 +1,304 @@
+<?php
+
+/* phpcs:disable WordPress.Security.EscapeOutput.ExceptionNotEscaped */
+
+/**
+ * Generates representation of the semantic HTML tree structure.
+ *
+ * This is inspired by the representation used by the HTML5lib tests. It's been extended here for
+ * blocks to render the semantic structure of blocks and their attributes.
+ * The order of attributes and class names is normalized both for HTML tags and blocks,
+ * as is the whitespace in HTML tags' style attribute.
+ *
+ * For example, consider the following block markup:
+ *
+ * <!-- wp:separator {"className":"is-style-default has-custom-classname","style":{"spacing":{"margin":{"top":"50px","bottom":"50px"}}},"backgroundColor":"accent-1"} -->
+ * <hr class="wp-block-separator is-style-default has-custom-classname" style="margin-top: 50px; margin-bottom: 50px" />
+ * <!-- /wp:separator -->
+ *
+ * This will be represented as:
+ *
+ * BLOCK["core/separator"]
+ * {
+ * "backgroundColor": "accent-1",
+ * "className": "has-custom-classname is-style-default",
+ * "style": {
+ * "spacing": {
+ * "margin": {
+ * "top": "50px",
+ * "bottom": "50px"
+ * }
+ * }
+ * }
+ * }
+ * <hr>
+ * class="has-custom-classname is-style-default wp-block-separator"
+ * style="margin-top:50px;margin-bottom:50px;"
+ *
+ *
+ * @see https://github.com/WordPress/wordpress-develop/blob/trunk/tests/phpunit/data/html5lib-tests/tree-construction/README.md
+ *
+ * @since 6.9.0
+ *
+ * @throws WP_HTML_Unsupported_Exception|Error If the markup could not be parsed.
+ *
+ * @param string $html Given test HTML.
+ * @param string|null $fragment_context Context element in which to parse HTML, such as BODY or SVG.
+ * @return string Tree structure of parsed HTML, if supported.
+ */
+function build_visual_html_tree( string $html, ?string $fragment_context ): string {
+ $processor = $fragment_context
+ ? WP_HTML_Processor::create_fragment( $html, $fragment_context )
+ : WP_HTML_Processor::create_full_parser( $html );
+ if ( null === $processor ) {
+ throw new Error( 'Could not create a parser.' );
+ }
+ $tree_indent = ' ';
+
+ $output = '';
+ $indent_level = 0;
+ $was_text = null;
+ $text_node = '';
+
+ $block_context = array();
+
+ while ( $processor->next_token() ) {
+ if ( null !== $processor->get_last_error() ) {
+ break;
+ }
+
+ $token_name = $processor->get_token_name();
+ $token_type = $processor->get_token_type();
+ $is_closer = $processor->is_tag_closer();
+
+ if ( $was_text && '#text' !== $token_name ) {
+ if ( '' !== $text_node ) {
+ $output .= "{$text_node}\"\n";
+ }
+ $was_text = false;
+ $text_node = '';
+ }
+
+ switch ( $token_type ) {
+ case '#doctype':
+ $doctype = $processor->get_doctype_info();
+ $output .= "<!DOCTYPE {$doctype->name}";
+ if ( null !== $doctype->public_identifier || null !== $doctype->system_identifier ) {
+ $output .= " \"{$doctype->public_identifier}\" \"{$doctype->system_identifier}\"";
+ }
+ $output .= ">\n";
+ break;
+
+ case '#tag':
+ $namespace = $processor->get_namespace();
+ $tag_name = 'html' === $namespace
+ ? strtolower( $processor->get_tag() )
+ : "{$namespace} {$processor->get_qualified_tag_name()}";
+
+ if ( $is_closer ) {
+ --$indent_level;
+
+ if ( 'html' === $namespace && 'TEMPLATE' === $token_name ) {
+ --$indent_level;
+ }
+
+ break;
+ }
+
+ $tag_indent = $indent_level;
+
+ if ( $processor->expects_closer() ) {
+ ++$indent_level;
+ }
+
+ $output .= str_repeat( $tree_indent, $tag_indent ) . "<{$tag_name}>\n";
+
+ $attribute_names = $processor->get_attribute_names_with_prefix( '' );
+ if ( $attribute_names ) {
+ $sorted_attributes = array();
+ foreach ( $attribute_names as $attribute_name ) {
+ $sorted_attributes[ $attribute_name ] = $processor->get_qualified_attribute_name( $attribute_name );
+ }
+
+ /*
+ * Sorts attributes to match html5lib sort order.
+ *
+ * - First comes normal HTML attributes.
+ * - Then come adjusted foreign attributes; these have spaces in their names.
+ * - Finally come non-adjusted foreign attributes; these have a colon in their names.
+ *
+ * Example:
+ *
+ * From: <math xlink:author definitionurl xlink:title xlink:show>
+ * Sorted: 'definitionURL', 'xlink show', 'xlink title', 'xlink:author'
+ */
+ uasort(
+ $sorted_attributes,
+ static function ( $a, $b ) {
+ $a_has_ns = str_contains( $a, ':' );
+ $b_has_ns = str_contains( $b, ':' );
+
+ // Attributes with `:` should follow all other attributes.
+ if ( $a_has_ns !== $b_has_ns ) {
+ return $a_has_ns ? 1 : -1;
+ }
+
+ $a_has_sp = str_contains( $a, ' ' );
+ $b_has_sp = str_contains( $b, ' ' );
+
+ // Attributes with a namespace ' ' should come after those without.
+ if ( $a_has_sp !== $b_has_sp ) {
+ return $a_has_sp ? 1 : -1;
+ }
+
+ return $a <=> $b;
+ }
+ );
+
+ foreach ( $sorted_attributes as $attribute_name => $display_name ) {
+ $val = $processor->get_attribute( $attribute_name );
+ /*
+ * Attributes with no value are `true` with the HTML API,
+ * we use the empty string value in the tree structure.
+ */
+ if ( true === $val ) {
+ $val = '';
+ } elseif ( 'class' === $attribute_name ) {
+ $class_names = iterator_to_array( $processor->class_list() );
+ sort( $class_names, SORT_STRING );
+ $val = implode( ' ', $class_names );
+ } elseif ( 'style' === $attribute_name ) {
+ $normalized_style = '';
+ foreach ( explode( ';', $val ) as $style ) {
+ if ( empty( trim( $style ) ) ) {
+ continue;
+ }
+ list( $style_key, $style_val ) = explode( ':', $style );
+
+ $style_key = trim( $style_key );
+ $style_val = trim( $style_val );
+
+ $normalized_style .= "{$style_key}:{$style_val};";
+ }
+ $val = $normalized_style;
+ }
+ $output .= str_repeat( $tree_indent, $tag_indent + 1 ) . "{$display_name}=\"{$val}\"\n";
+ }
+ }
+
+ // Self-contained tags contain their inner contents as modifiable text.
+ $modifiable_text = $processor->get_modifiable_text();
+ if ( '' !== $modifiable_text ) {
+ $output .= str_repeat( $tree_indent, $tag_indent + 1 ) . "\"{$modifiable_text}\"\n";
+ }
+
+ if ( 'html' === $namespace && 'TEMPLATE' === $token_name ) {
+ $output .= str_repeat( $tree_indent, $indent_level ) . "content\n";
+ ++$indent_level;
+ }
+
+ break;
+
+ case '#cdata-section':
+ case '#text':
+ $text_content = $processor->get_modifiable_text();
+ if ( '' === trim( $text_content, " \f\t\r\n" ) ) {
+ break;
+ }
+ $was_text = true;
+ if ( '' === $text_node ) {
+ $text_node .= str_repeat( $tree_indent, $indent_level ) . '"';
+ }
+ $text_node .= $text_content;
+ break;
+
+ case '#funky-comment':
+ // Comments must be "<" then "!-- " then the data then " -->".
+ $output .= str_repeat( $tree_indent, $indent_level ) . "<!-- {$processor->get_modifiable_text()} -->\n";
+ break;
+
+ case '#comment':
+ // Comments must be "<" then "!--" then the data then "-->".
+ $comment = "<!--{$processor->get_full_comment_text()}-->";
+
+ // Maybe the comment is a block delimiter.
+ $parser = new WP_Block_Parser();
+ $parser->document = $comment;
+ $parser->offset = 0;
+ list( $delimiter_type, $block_name, $block_attrs, $start_offset, $token_length ) = $parser->next_token();
+
+ switch ( $delimiter_type ) {
+ case 'block-opener':
+ case 'void-block':
+ $output .= str_repeat( $tree_indent, $indent_level ) . "BLOCK[\"{$block_name}\"]\n";
+
+ if ( 'block-opener' === $delimiter_type ) {
+ $block_context[] = $block_name;
+ ++$indent_level;
+ }
+
+ // If they're no attributes, we're done here.
+ if ( empty( $block_attrs ) ) {
+ break;
+ }
+
+ // Normalize attribute order.
+ ksort( $block_attrs, SORT_STRING );
+
+ if ( isset( $block_attrs['className'] ) ) {
+ // Normalize class name order (and de-duplicate), as we need to be tolerant of different orders.
+ // (Style attributes don't need this treatment, as they are parsed into a nested array.)
+ $block_class_processor = new WP_HTML_Tag_Processor( '<div>' );
+ $block_class_processor->next_token();
+ $block_class_processor->set_attribute( 'class', $block_attrs['className'] );
+ $class_names = iterator_to_array( $block_class_processor->class_list() );
+ sort( $class_names, SORT_STRING );
+ $block_attrs['className'] = implode( ' ', $class_names );
+ }
+
+ $block_attrs = json_encode( $block_attrs, JSON_PRETTY_PRINT );
+ // Fix indentation by "halving" it (2 spaces instead of 4).
+ // Additionally, we need to indent each line by the current indentation level.
+ $block_attrs = preg_replace( '/^( +)\1/m', str_repeat( $tree_indent, $indent_level ) . '$1', $block_attrs );
+ // Finally, indent the first line, and the last line (with the closing curly brace).
+ $output .= str_repeat( $tree_indent, $indent_level ) . substr( $block_attrs, 0, -1 ) . str_repeat( $tree_indent, $indent_level ) . "}\n";
+ break;
+ case 'block-closer':
+ // Is this a closer for the currently open block?
+ if ( ! empty( $block_context ) && end( $block_context ) === $block_name ) {
+ // If it's a closer, we don't add it to the output.
+ // Instead, we decrease indentation and remove the block from block context stack.
+ --$indent_level;
+ array_pop( $block_context );
+ }
+ break;
+ default: // Not a block delimiter.
+ $output .= str_repeat( $tree_indent, $indent_level ) . $comment . "\n";
+ break;
+ }
+ break;
+ default:
+ // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_var_export
+ $serialized_token_type = var_export( $processor->get_token_type(), true );
+ throw new Error( "Unhandled token type for tree construction: {$serialized_token_type}" );
+ }
+ }
+
+ if ( null !== $processor->get_unsupported_exception() ) {
+ throw $processor->get_unsupported_exception();
+ }
+
+ if ( null !== $processor->get_last_error() ) {
+ throw new Error( "Parser error: {$processor->get_last_error()}" );
+ }
+
+ if ( $processor->paused_at_incomplete_token() ) {
+ throw new Error( 'Paused at incomplete token.' );
+ }
+
+ if ( '' !== $text_node ) {
+ $output .= "{$text_node}\"\n";
+ }
+
+ return $output;
+}
diff --git a/tests/phpunit/includes/factory/class-wp-unittest-factory-for-attachment.php b/tests/phpunit/includes/factory/class-wp-unittest-factory-for-attachment.php
index 262c6c4640..2c1872795f 100644
--- a/tests/phpunit/includes/factory/class-wp-unittest-factory-for-attachment.php
+++ b/tests/phpunit/includes/factory/class-wp-unittest-factory-for-attachment.php
@@ -50,12 +50,13 @@ class WP_UnitTest_Factory_For_Attachment extends WP_UnitTest_Factory_For_Post {
}
/**
- * Saves an attachment.
+ * Saves a file as an attachment.
*
* @since 4.4.0
* @since 6.2.0 Returns a WP_Error object on failure.
*
- * @param string $file The file name to create attachment object for.
+ * @param string $file Full path to the file to create an attachment object for.
+ * The name of the file will be used as the attachment name.
* @param int $parent_post_id ID of the post to attach the file to.
*
* @return int|WP_Error The attachment ID on success, WP_Error object on failure.
diff --git a/tests/phpunit/includes/object-cache.php b/tests/phpunit/includes/object-cache.php
index ef03546892..daffcabac5 100644
--- a/tests/phpunit/includes/object-cache.php
+++ b/tests/phpunit/includes/object-cache.php
@@ -160,8 +160,8 @@ function wp_cache_cas( $cas_token, $key, $value, $group = '', $expiration = 0 )
*
* @link https://www.php.net/manual/en/memcached.casbykey.php
*
- * @param string $server_key The key identifying the server to store the value on.
* @param float $cas_token Unique value associated with the existing item. Generated by memcached.
+ * @param string $server_key The key identifying the server to store the value on.
* @param string $key The key under which to store the value.
* @param mixed $value The value to store.
* @param string $group The group value appended to the $key.
@@ -1238,8 +1238,8 @@ class WP_Object_Cache {
*
* @link https://www.php.net/manual/en/memcached.casbykey.php
*
- * @param string $server_key The key identifying the server to store the value on.
* @param float $cas_token Unique value associated with the existing item. Generated by memcached.
+ * @param string $server_key The key identifying the server to store the value on.
* @param string $key The key under which to store the value.
* @param mixed $value The value to store.
* @param string $group The group value appended to the $key.
@@ -1929,12 +1929,12 @@ class WP_Object_Cache {
*
* @link https://www.php.net/manual/en/memcached.replace.php
*
- * @param string $server_key The key identifying the server to store the value on.
* @param string $key The key under which to store the value.
* @param mixed $value The value to store.
* @param string $group The group value appended to the $key.
- * @param bool $by_key True to store in internal cache by key; false to not store by key.
* @param int $expiration The expiration time, defaults to 0.
+ * @param string $server_key The key identifying the server to store the value on.
+ * @param bool $by_key True to store in internal cache by key; false to not store by key.
* @return bool True on success, false on failure.
*/
public function replace( $key, $value, $group = 'default', $expiration = 0, $server_key = '', $by_key = false ) {