Skip to content
Closed
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
00617e7
Tests: Add assertEqualMarkup trait
ockham Jun 2, 2025
cbba80a
Add test coverage for assertion helper method
ockham Jun 2, 2025
0602711
Fix include path
ockham Jun 2, 2025
c01a3c1
Remove now-obsolete method of the same name from Tests_Dependencies_S…
ockham Jun 2, 2025
5a5b2b1
Remove obsolete require_lib
ockham Jun 2, 2025
1cb1f23
Add since PHPDoc
ockham Jun 2, 2025
38b575a
Move assertion to class WP_UnitTestCase_Base
ockham Jun 2, 2025
8204474
Get rid of trait
ockham Jun 2, 2025
03f7b09
WPCS
ockham Jun 2, 2025
09cf449
Remove potentially obsolete PHPCS ignore
ockham Jun 2, 2025
b2fc3ff
Revert "Remove now-obsolete method of the same name from Tests_Depend…
ockham Jun 2, 2025
ef27f4f
Bring back assertEqualMarkup override, harmonize signature
ockham Jun 2, 2025
51e4613
Remove trailing newline
ockham Jun 2, 2025
2258487
Fix class name
ockham Jun 2, 2025
5bfdf9b
Tweak tests
ockham Jun 2, 2025
2b33187
Remove strict_types declaration
ockham Jun 2, 2025
82d57b6
Fix heredoc indentation for PHP 7.2 compat :rolleyes:
ockham Jun 2, 2025
3fbf857
Use WP_Block_Parser for block parsing
ockham Jun 3, 2025
3090d65
Handle void blocks correctly
ockham Jun 3, 2025
47dccc1
Rearrange logic
ockham Jun 3, 2025
bb2404b
Allow for different whitespace in class attribute
ockham Jun 3, 2025
192009b
De-duplicate block class names
ockham Jun 3, 2025
7cf7c64
Change Html5lib test format reference link
ockham Jun 4, 2025
d0d46fe
Add more PHPDoc and an example to build_tree_representation
ockham Jun 4, 2025
252569d
Use Tag Processor to normalize block class names
ockham Jun 4, 2025
eae650f
Whitespace :rolleyes:
ockham Jun 4, 2025
e8ceff2
Rename function to build_equivalent_html_semantic_tree
ockham Jun 4, 2025
5c44e1a
Add covers PHPDoc to tests
ockham Jun 4, 2025
5ff8ad4
Fall back to DOM\HTMLDocument for PHP 8.4+
ockham Jun 4, 2025
90f958d
Revert "Bring back assertEqualMarkup override, harmonize signature"
ockham Jun 4, 2025
7735df4
Rename new method to assertEqualBlockMarkup
ockham Jun 4, 2025
7581a94
Add @todo note to "old" assertEqualMarkup
ockham Jun 4, 2025
8d7e402
Update PHPDoc for assertEqualBlockMarkup
ockham Jun 4, 2025
51600e6
Add test with capitalization/lower case letters.
ockham Jun 5, 2025
ddaec5e
Update to use the new assertEqualMarkup helper
sirreal Jun 5, 2025
98ad4c9
Rename test helper to assertEqualMarkup
sirreal Jun 5, 2025
492316d
Fix script async/defer strategy expectation to match printed scripts
sirreal Jun 5, 2025
c7317e7
Add CDATA inline script wrappers where they appear in markup
sirreal Jun 5, 2025
7472609
FIXME: Skip an inaccurate test
sirreal Jun 5, 2025
844a076
Adjust expected output to use attributes as generated
sirreal Jun 5, 2025
514967f
Add html5 support to a test that expects html5 support
sirreal Jun 5, 2025
fed7647
Script quotes in IE7 conditional _comments_ produce semantically _dif…
sirreal Jun 5, 2025
28e1fd7
Revert irrelevant change
sirreal Jun 5, 2025
c2f1bfa
Fix more missing CDATA comments
sirreal Jun 5, 2025
5a60a70
Remove scripts specific assertEqualMarkup implementation
sirreal Jun 5, 2025
b0df96a
Use more consistent code backticks in docblock
sirreal Jun 5, 2025
1cb1daf
Fix phpcs issues
sirreal Jun 5, 2025
defbe92
Remove now-obsolete parse_markup_fragment() method
ockham Jun 5, 2025
2b66454
Fix PHPDoc param order
ockham Jun 10, 2025
95ddd9a
Tweak PHPDoc description of build_equivalent_semantic_tree
ockham Jun 10, 2025
6a79216
Add return type, tweak PHPDoc
ockham Jun 10, 2025
9469f43
Rename to build_visual_html_tree
ockham Jun 10, 2025
b07677e
Rename to assertEqualHTML
ockham Jun 10, 2025
f7736c4
Fix indentation
ockham Jun 10, 2025
a83ff2b
Add ticket annotations
ockham Jun 10, 2025
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
28 changes: 27 additions & 1 deletion tests/phpunit/includes/abstract-testcase.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<?php

require_once __DIR__ . '/build-tree-representation.php';
require_once __DIR__ . '/factory.php';
require_once __DIR__ . '/trac.php';

Expand All @@ -13,7 +14,6 @@
* 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();
Expand Down Expand Up @@ -1180,6 +1180,32 @@ public function assertQueryTrue( ...$prop ) {
}
}

/**
* Asserts that HTML markup produces a semantically equivalent tree.
*
* - Tag names are normalized.
* - Attribute names are normalized.
* - Attributes are sorted and deduplicated.
* - HTML Entities are correctly decoded.
* - Class attributes are normalized.
*
* @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 assertEqualMarkup( string $expected, string $actual, ?string $fragment_context = '<body>', $message = 'HTML markup was not equivalent.' ): void {
$tree_expected = build_tree_representation( $expected, $fragment_context );
$tree_actual = build_tree_representation( $actual, $fragment_context );

$this->assertSame( $tree_expected, $tree_actual, $message );
}

/**
* Helper function to convert a single-level array containing text strings to a named data provider.
*
Expand Down
264 changes: 264 additions & 0 deletions tests/phpunit/includes/build-tree-representation.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,264 @@
<?php declare( strict_types = 1 );

/* phpcs:disable WordPress.Security.EscapeOutput.ExceptionNotEscaped */

/**
* Generates the tree-like structure represented in the Html5lib tests.
*
* @see https://github.com/html5lib/html5lib-tests/blob/a9f44960a9fedf265093d22b2aa3c7ca123727b9/tree-construction/README.md
*
* @param string|null $fragment_context Context element in which to parse HTML, such as BODY or SVG.
* @param string $html Given test HTML.
* @return string|null Tree structure of parsed HTML, if supported, else null.
*/
function build_tree_representation( string $html, ?string $fragment_context ) {
$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.
$parsed_comment = parse_blocks( $comment )[0];

if ( ! isset( $parsed_comment['blockName'] ) ) {
// Doesn't look like an *opening* block delimiter.
// Let's see if it's a closer for the currently open block.
if ( ! empty( $block_context ) && '/' . end( $block_context ) === trim( $processor->get_full_comment_text() ) ) {
// 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 );
} else {
$output .= str_repeat( $tree_indent, $indent_level ) . $comment . "\n";
}
} else {
// Looks like an opening block delimiter.
$output .= str_repeat( $tree_indent, $indent_level ) . "BLOCK[\"{$parsed_comment['blockName']}\"]\n";

if ( ! str_ends_with( $processor->get_full_comment_text(), '/' ) ) {
// If the block is not self-closing, we add it name (as it appears in markup) to the block context stack and increase indentation.
$block_context[] = 'wp:' . strip_core_block_namespace( $parsed_comment['blockName'] );
++$indent_level;
}

// If they're no attributes, we're done here.
if ( empty( $parsed_comment['attrs'] ) ) {
break;
}

// Normalize attribute order.
$block_attrs = $parsed_comment['attrs'];
ksort( $block_attrs, SORT_STRING );

if ( isset( $block_attrs['className'] ) ) {
// Normalize class name order, 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_names = explode( ' ', $block_attrs['className'] );
sort( $block_class_names, SORT_STRING );
$block_attrs['className'] = implode( ' ', $block_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;

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;
}

Check failure on line 263 in tests/phpunit/includes/build-tree-representation.php

View workflow job for this annotation

GitHub Actions / PHP coding standards / Run coding standards checks

Expected 1 blank line at end of file; 2 found

Loading
Loading