diff --git a/tests/phpunit/tests/html-api/bootstrap.php b/tests/phpunit/tests/html-api/bootstrap.php new file mode 100644 index 0000000000000..d7618120c1ce0 --- /dev/null +++ b/tests/phpunit/tests/html-api/bootstrap.php @@ -0,0 +1,66 @@ +', '"' ], [ '<', '>', '"' ], $s ); + } +} + +if ( ! function_exists( 'esc_html' ) ) { + function esc_html( $s ) { + return esc_attr( $s ); + } +} + +if ( ! function_exists( '__' ) ) { + function __( $s ) { + return $s; + } +} + +if ( ! function_exists( '_doing_it_wrong' ) ) { + function _doing_it_wrong( ...$args ) { + + } +} + +if ( ! class_exists( 'HTMLProcessorDebugger' ) ) { + class HTMLProcessorDebugger extends WP_HTML_Tag_Processor { + + } +} diff --git a/tests/phpunit/tests/html-api/phpunit.xml b/tests/phpunit/tests/html-api/phpunit.xml new file mode 100644 index 0000000000000..6e0adc2c1730d --- /dev/null +++ b/tests/phpunit/tests/html-api/phpunit.xml @@ -0,0 +1,29 @@ + + + + + + + + WpHtmlProcessor.php + + WpHtmlProcessorSemanticRules.php + WpHtmlProcessorSemanticRulesHeadingElements.php + WpHtmlProcessorSemanticRulesListElements.php + WpHtmlProcessorBreadcrumbs.php + + + + + WpHtmlSupportRequiredHtmlProcessor.php + WpHtmlSupportRequiredOpenElements.php + WpHtmlTagProcessor.php + WpHtmlTagProcessor-bookmark.php + WpHtmlProcessor-bookmark.php + + WpHtmlTagProcessor-token-scanning.php + + + + + diff --git a/tests/phpunit/tests/html-api/wpHtmlProcessor-bookmark.php b/tests/phpunit/tests/html-api/wpHtmlProcessor-bookmark.php new file mode 100644 index 0000000000000..b8962e0d6cf4c --- /dev/null +++ b/tests/phpunit/tests/html-api/wpHtmlProcessor-bookmark.php @@ -0,0 +1,483 @@ +
  • One
  • Two
  • Three
  • ' ); + $processor->next_tag( 'li' ); + $this->assertTrue( $processor->set_bookmark( 'first li' ), 'Could not allocate a "first li" bookmark' ); + $processor->next_tag( 'li' ); + $this->assertTrue( $processor->set_bookmark( 'second li' ), 'Could not allocate a "second li" bookmark' ); + $this->assertTrue( $processor->set_bookmark( 'first li' ), 'Could not move the "first li" bookmark' ); + } + + /** + * @ticket 56299 + * + * [@]covers WP_HTML_Tag_Processor::release_bookmark + */ + public function test_release_bookmark() { + $processor = WP_HTML_Processor::create_fragment( '' ); + $processor->next_tag( 'li' ); + $this->assertFalse( $processor->release_bookmark( 'first li' ), 'Released a non-existing bookmark' ); + $processor->set_bookmark( 'first li' ); + $this->assertTrue( $processor->release_bookmark( 'first li' ), 'Could not release a bookmark' ); + } + + /** + * @ticket 57788 + * + * [@]covers WP_HTML_Tag_Processor::has_bookmark + */ + public function test_has_bookmark_returns_false_if_bookmark_does_not_exist() { + $processor = WP_HTML_Processor::create_fragment( '
    Test
    ' ); + $this->assertFalse( $processor->has_bookmark( 'my-bookmark' ) ); + } + + /** + * @ticket 57788 + * + * [@]covers WP_HTML_Tag_Processor::has_bookmark + */ + public function test_has_bookmark_returns_true_if_bookmark_exists() { + $processor = WP_HTML_Processor::create_fragment( '
    Test
    ' ); + $processor->next_tag(); + $processor->set_bookmark( 'my-bookmark' ); + $this->assertTrue( $processor->has_bookmark( 'my-bookmark' ) ); + } + + /** + * @ticket 57788 + * + * [@]covers WP_HTML_Tag_Processor::has_bookmark + */ + public function test_has_bookmark_returns_false_if_bookmark_has_been_released() { + $processor = WP_HTML_Processor::create_fragment( '
    Test
    ' ); + $processor->next_tag(); + $processor->set_bookmark( 'my-bookmark' ); + $processor->release_bookmark( 'my-bookmark' ); + $this->assertFalse( $processor->has_bookmark( 'my-bookmark' ) ); + } + + /** + * @ticket 56299 + * + * [@]covers WP_HTML_Tag_Processor::seek + */ + public function test_seek() { + $processor = WP_HTML_Processor::create_fragment( '' ); + $processor->next_tag( 'li' ); + $processor->set_bookmark( 'first li' ); + + $processor->next_tag( 'li' ); + $processor->set_attribute( 'foo-2', 'bar-2' ); + + $processor->seek( 'first li' ); + $processor->set_attribute( 'foo-1', 'bar-1' ); + + $this->assertSame( + '', + $processor->get_updated_html(), + 'Did not seek to the intended bookmark locations' + ); + } + + /** + * @ticket 57787 + * + * [@]covers WP_HTML_Tag_Processor::seek + */ + public function test_seeks_to_tag_closer_bookmark() { + $processor = WP_HTML_Processor::create_fragment( '
    First
    Second' ); + $processor->next_tag( array( 'tag_closers' => 'visit' ) ); + $processor->set_bookmark( 'first' ); + $processor->next_tag( array( 'tag_closers' => 'visit' ) ); + $processor->set_bookmark( 'second' ); + + $processor->seek( 'first' ); + $processor->seek( 'second' ); + + $this->assertSame( + 'DIV', + $processor->get_tag(), + 'Did not seek to the intended bookmark location' + ); + } + + /** + * WP_HTML_Tag_Processor used to test for the diffs affecting + * the adjusted bookmark position while simultaneously adjusting + * the bookmark in question. As a result, updating the bookmarks + * of a next tag while removing two subsequent attributes in + * a previous tag unfolded like this: + * + * 1. Check if the first removed attribute is before the bookmark: + * + * + * ^-------------------^ ^ + * diff applied here the bookmark is here + * + * (Yes it is) + * + * 2. Move the bookmark to the left by the attribute length: + * + * + * ^ + * the bookmark is here + * + * 3. Check if the second removed attribute is before the bookmark: + * + * + * ^ ^-----^ + * bookmark diff + * + * This time, it isn't! + * + * The fix in the WP_HTML_Tag_Processor involves doing all the checks + * before moving the bookmark. This test is here to guard us from + * the erroneous behavior accidentally returning one day. + * + * @ticket 56299 + * + * [@]covers WP_HTML_Tag_Processor::seek + * [@]covers WP_HTML_Tag_Processor::set_bookmark + */ + public function test_removing_long_attributes_doesnt_break_seek() { + $input = << +HTML; + $processor = WP_HTML_Processor::create_fragment( $input ); + $processor->next_tag( 'button' ); + $processor->set_bookmark( 'first' ); + $processor->next_tag( 'button' ); + $processor->set_bookmark( 'second' ); + + $this->assertTrue( + $processor->seek( 'first' ), + 'Seek() to the first button has failed' + ); + $processor->remove_attribute( 'twenty_one_characters' ); + $processor->remove_attribute( '7_chars' ); + + $this->assertTrue( + $processor->seek( 'second' ), + 'Seek() to the second button has failed' + ); + } + + /** + * @ticket 56299 + * + * [@]covers WP_HTML_Tag_Processor::seek + * [@]covers WP_HTML_Tag_Processor::set_bookmark + */ + public function test_bookmarks_complex_use_case() { + $input = << +
    +
    +
    + + + + + + + +
    +
    +
    +HTML; + $expected_output = << +
    +
    +
    + + + + + + + +
    +
    +
    +HTML; + $processor = WP_HTML_Processor::create_fragment( $input ); + $processor->next_tag( 'div' ); + $processor->next_tag( 'div' ); + $processor->next_tag( 'div' ); + $processor->set_bookmark( 'first div' ); + $processor->next_tag( 'button' ); + $processor->set_bookmark( 'first button' ); + $processor->next_tag( 'button' ); + $processor->set_bookmark( 'second button' ); + $processor->next_tag( 'button' ); + $processor->set_bookmark( 'third button' ); + $processor->next_tag( 'button' ); + $processor->set_bookmark( 'fourth button' ); + + $processor->seek( 'first button' ); + $processor->set_attribute( 'type', 'submit' ); + + $this->assertTrue( + $processor->seek( 'third button' ), + 'Seek() to the third button failed' + ); + $processor->remove_attribute( 'class' ); + $processor->remove_attribute( 'type' ); + $processor->remove_attribute( 'aria-expanded' ); + $processor->set_attribute( 'id', 'rebase-and-merge' ); + $processor->remove_attribute( 'data-details-container' ); + + $this->assertTrue( + $processor->seek( 'first div' ), + 'Seek() to the first div failed' + ); + $processor->set_attribute( 'checked', false ); + + $this->assertTrue( + $processor->seek( 'fourth button' ), + 'Seek() to the fourth button failed' + ); + $processor->set_attribute( 'id', 'last-button' ); + $processor->remove_attribute( 'class' ); + $processor->remove_attribute( 'type' ); + $processor->remove_attribute( 'checked' ); + $processor->remove_attribute( 'aria-label' ); + $processor->remove_attribute( 'disabled' ); + $processor->remove_attribute( 'data-view-component' ); + + $this->assertTrue( + $processor->seek( 'second button' ), + 'Seek() to the second button failed' + ); + $processor->remove_attribute( 'type' ); + $processor->set_attribute( 'class', 'hx_create-pr-button' ); + + $this->assertSame( + $expected_output, + $processor->get_updated_html(), + 'Performing several attribute updates on different tags does not produce the expected HTML snippet' + ); + } + + /** + * @ticket 56299 + * + * [@]covers WP_HTML_Tag_Processor::seek + */ + public function test_updates_bookmark_for_additions_after_both_sides() { + $processor = WP_HTML_Processor::create_fragment( '
    First
    Second
    ' ); + $processor->next_tag(); + $processor->set_bookmark( 'first' ); + $processor->next_tag(); + $processor->add_class( 'second' ); + + $processor->seek( 'first' ); + $processor->add_class( 'first' ); + + $this->assertSame( + '
    First
    Second
    ', + $processor->get_updated_html(), + 'The bookmark was updated incorrectly in response to HTML markup updates' + ); + } + + /** + * @ticket 56299 + * + * [@]covers WP_HTML_Tag_Processor::seek + */ + public function test_updates_bookmark_for_additions_before_both_sides() { + $processor = WP_HTML_Processor::create_fragment( '
    First
    Second
    ' ); + $processor->next_tag(); + $processor->set_bookmark( 'first' ); + $processor->next_tag(); + $processor->set_bookmark( 'second' ); + + $processor->seek( 'first' ); + $processor->add_class( 'first' ); + + $processor->seek( 'second' ); + $processor->add_class( 'second' ); + + $this->assertSame( + '
    First
    Second
    ', + $processor->get_updated_html(), + 'The bookmark was updated incorrectly in response to HTML markup updates' + ); + } + + /** + * @ticket 56299 + * + * [@]covers WP_HTML_Tag_Processor::seek + */ + public function test_updates_bookmark_for_deletions_after_both_sides() { + $processor = WP_HTML_Processor::create_fragment( '
    First
    Second
    ' ); + $processor->next_tag(); + $processor->set_bookmark( 'first' ); + $processor->next_tag(); + $processor->remove_attribute( 'disabled' ); + + $processor->seek( 'first' ); + $processor->set_attribute( 'untouched', true ); + + $this->assertSame( + /* + * It shouldn't be necessary to assert the extra space after the tag + * following the attribute removal, but doing so makes the test easier + * to see than it would be if parsing the output HTML for proper + * validation. If the Tag Processor changes so that this space no longer + * appears then this test should be updated to reflect that. The space + * is not required. + */ + '
    First
    Second
    ', + $processor->get_updated_html(), + 'The bookmark was incorrectly in response to HTML markup updates' + ); + } + + /** + * @ticket 56299 + * + * [@]covers WP_HTML_Tag_Processor::seek + */ + public function test_updates_bookmark_for_deletions_before_both_sides() { + $processor = WP_HTML_Processor::create_fragment( '
    First
    Second
    ' ); + $processor->next_tag(); + $processor->set_bookmark( 'first' ); + $processor->next_tag(); + $processor->set_bookmark( 'second' ); + + $processor->seek( 'first' ); + $processor->remove_attribute( 'disabled' ); + + $processor->seek( 'second' ); + $processor->set_attribute( 'safe', true ); + + $this->assertSame( + /* + * It shouldn't be necessary to assert the extra space after the tag + * following the attribute removal, but doing so makes the test easier + * to see than it would be if parsing the output HTML for proper + * validation. If the Tag Processor changes so that this space no longer + * appears then this test should be updated to reflect that. The space + * is not required. + */ + '
    First
    Second
    ', + $processor->get_updated_html(), + 'The bookmark was updated incorrectly in response to HTML markup updates' + ); + } + + /** + * @ticket 56299 + * + * [@]covers WP_HTML_Tag_Processor::set_bookmark + */ + public function test_limits_the_number_of_bookmarks() { + $processor = WP_HTML_Processor::create_fragment( '' ); + $processor->next_tag( 'li' ); + + for ( $i = 0; $i < WP_HTML_Tag_Processor::MAX_BOOKMARKS; $i++ ) { + $this->assertTrue( $processor->set_bookmark( "bookmark $i" ), "Could not allocate the bookmark #$i" ); + } + + $this->setExpectedIncorrectUsage( 'WP_HTML_Tag_Processor::set_bookmark' ); + $this->assertFalse( $processor->set_bookmark( 'final bookmark' ), "Allocated $i bookmarks, which is one above the limit" ); + } + + /** + * @ticket 56299 + * + * [@]covers WP_HTML_Tag_Processor::seek + */ + public function test_limits_the_number_of_seek_calls() { + $processor = WP_HTML_Processor::create_fragment( '' ); + $processor->next_tag( 'li' ); + $processor->set_bookmark( 'bookmark' ); + + for ( $i = 0; $i < WP_HTML_Tag_Processor::MAX_SEEK_OPS; $i++ ) { + $this->assertTrue( $processor->seek( 'bookmark' ), 'Could not seek to the "bookmark"' ); + } + + $this->setExpectedIncorrectUsage( 'WP_HTML_Tag_Processor::seek' ); + $this->assertFalse( $processor->seek( 'bookmark' ), "$i-th seek() to the bookmark succeeded, even though it should exceed the allowed limit" ); + } + + /** + * Ensures that it's possible to seek to an earlier location in a document even + * after reaching the end of a document, when most functionality shuts down. + * + * @ticket 60428 + * + * @dataProvider data_incomplete_html_with_target_nodes_for_seeking + * + * @param string $html_with_target_element HTML string containing a tag with a `target` attribute. + */ + public function test_can_seek_after_document_ends( $html_with_target_element ) { + $processor = WP_HTML_Processor::create_fragment( $html_with_target_element ); + + $sought_tag_name = null; + while ( $processor->next_tag() ) { + if ( null !== $processor->get_attribute( 'target' ) ) { + $processor->set_bookmark( 'target' ); + $sought_tag_name = $processor->get_tag(); + } + } + + $this->assertTrue( + $processor->seek( 'target' ), + 'Should have been able to seek to the target bookmark after reaching the end of the document.' + ); + + $this->assertSame( + $sought_tag_name, + $processor->get_tag(), + "Should have found original target node instead of {$processor->get_tag()}." + ); + } + + /** + * Data provider. + * + * @return array[]. + */ + public static function data_incomplete_html_with_target_nodes_for_seeking() { + return array( + 'Compete document' => array( '
    ' ), + 'Incomplete document' => array( '
    setExpectedIncorrectUsage( 'WP_HTML_Processor::__construct' ); @@ -49,7 +49,7 @@ public function test_warns_that_the_static_creator_methods_should_be_called_inst * * @ticket 59167 * - * @covers WP_HTML_Processor::get_tag + * [@]covers WP_HTML_Processor::get_tag */ public function test_get_tag_is_null_once_document_is_finished() { $processor = WP_HTML_Processor::create_fragment( '
    Test
    ' ); @@ -73,8 +73,8 @@ public function test_get_tag_is_null_once_document_is_finished() { * * @ticket 58517 * - * @covers WP_HTML_Processor::next_tag - * @covers WP_HTML_Processor::seek + * [@]covers WP_HTML_Processor::next_tag + * [@]covers WP_HTML_Processor::seek */ public function test_clear_to_navigate_after_seeking() { $processor = WP_HTML_Processor::create_fragment( '

    ' ); @@ -123,7 +123,7 @@ public function test_clear_to_navigate_after_seeking() { * * @ticket 58517 * - * @covers WP_HTML_Processor::reconstruct_active_formatting_elements + * [@]covers WP_HTML_Processor::reconstruct_active_formatting_elements */ public function test_fails_to_reconstruct_formatting_elements() { $processor = WP_HTML_Processor::create_fragment( '

    One

    Two

    Three

    Four' ); @@ -137,8 +137,8 @@ public function test_fails_to_reconstruct_formatting_elements() { * * @ticket 60283 * - * @covers WP_HTML_Processor::step_in_body - * @covers WP_HTML_Processor::is_void + * [@]covers WP_HTML_Processor::step_in_body + * [@]covers WP_HTML_Processor::is_void * * @dataProvider data_void_tags * @@ -275,7 +275,7 @@ public static function data_void_tags() { * * @dataProvider data_unsupported_special_in_body_tags * - * @covers WP_HTML_Processor::step_in_body + * [@]covers WP_HTML_Processor::step_in_body * * @param string $tag_name Name of the tag to test. */ diff --git a/tests/phpunit/tests/html-api/wpHtmlProcessorBreadcrumbs.php b/tests/phpunit/tests/html-api/wpHtmlProcessorBreadcrumbs.php index 1488be91654a7..8a7a11a5f204a 100644 --- a/tests/phpunit/tests/html-api/wpHtmlProcessorBreadcrumbs.php +++ b/tests/phpunit/tests/html-api/wpHtmlProcessorBreadcrumbs.php @@ -15,7 +15,7 @@ class Tests_HtmlApi_WpHtmlProcessorBreadcrumbs extends WP_UnitTestCase { /** * @ticket 58517 * - * @covers WP_HTML_Processor::step + * [@]covers WP_HTML_Processor::step * * @dataProvider data_single_tag_of_supported_elements * @@ -148,7 +148,7 @@ public static function data_single_tag_of_supported_elements() { * * @ticket 58517 * - * @covers WP_HTML_Processor::step + * [@]covers WP_HTML_Processor::step * * @dataProvider data_unsupported_elements * @@ -261,7 +261,7 @@ public static function data_unsupported_markup() { /** * @ticket 58517 * - * @covers WP_HTML_Processor::next_tag + * [@]covers WP_HTML_Processor::next_tag * * @dataProvider data_html_target_with_breadcrumbs * @@ -286,7 +286,7 @@ public function test_finds_correct_tag_given_breadcrumbs( $html, $breadcrumbs, $ /** * @ticket 58517 * - * @covers WP_HTML_Processor::get_breadcrumbs + * [@]covers WP_HTML_Processor::get_breadcrumbs * * @dataProvider data_html_target_with_breadcrumbs * @@ -430,7 +430,7 @@ public static function data_html_with_breadcrumbs_of_various_specificity() { * * @ticket 59607 * - * @covers WP_HTML_Tag_Processor::get_updated_html + * [@]covers WP_HTML_Tag_Processor::get_updated_html */ public function test_remains_stable_when_editing_attributes() { $processor = WP_HTML_Processor::create_fragment( '