diff --git a/lib/experimental/html/class-wp-html-tag-processor.php b/lib/experimental/html/class-wp-html-tag-processor.php
index c7bb67ba2788f..433c16a150806 100644
--- a/lib/experimental/html/class-wp-html-tag-processor.php
+++ b/lib/experimental/html/class-wp-html-tag-processor.php
@@ -1059,7 +1059,7 @@ private function parse_next_attribute() {
return true;
}
- /**
+ /*
* > There must never be two or more attributes on
* > the same start tag whose names are an ASCII
* > case-insensitive match for each other.
@@ -1116,9 +1116,6 @@ private function after_tag() {
* Converts class name updates into tag attributes updates
* (they are accumulated in different data formats for performance).
*
- * This method is only meant to run right before the attribute updates are applied.
- * The behavior in all other cases is undefined.
- *
* @return void
* @since 6.2.0
*
@@ -1126,14 +1123,26 @@ private function after_tag() {
* @see $lexical_updates
*/
private function class_name_updates_to_attributes_updates() {
- if ( count( $this->classname_updates ) === 0 || isset( $this->lexical_updates['class'] ) ) {
- $this->classname_updates = array();
+ if ( count( $this->classname_updates ) === 0 ) {
return;
}
- $existing_class = isset( $this->attributes['class'] )
- ? substr( $this->html, $this->attributes['class']->value_starts_at, $this->attributes['class']->value_length )
- : '';
+ $existing_class = $this->get_enqueued_attribute_value( 'class' );
+ if ( null === $existing_class || true === $existing_class ) {
+ $existing_class = '';
+ }
+
+ if ( false === $existing_class && isset( $this->attributes['class'] ) ) {
+ $existing_class = substr(
+ $this->html,
+ $this->attributes['class']->value_starts_at,
+ $this->attributes['class']->value_length
+ );
+ }
+
+ if ( false === $existing_class ) {
+ $existing_class = '';
+ }
/**
* Updated "class" attribute value.
@@ -1251,7 +1260,7 @@ private function apply_attributes_updates() {
return;
}
- /**
+ /*
* Attribute updates can be enqueued in any order but as we
* progress through the document to replace them we have to
* make our replacements in the order in which they are found
@@ -1270,7 +1279,7 @@ private function apply_attributes_updates() {
}
foreach ( $this->bookmarks as $bookmark ) {
- /**
+ /*
* As we loop through $this->lexical_updates, we keep comparing
* $bookmark->start and $bookmark->end to $diff->start. We can't
* change it and still expect the correct result, so let's accumulate
@@ -1370,6 +1379,69 @@ private static function sort_start_ascending( $a, $b ) {
return $a->end - $b->end;
}
+ /**
+ * Return the enqueued value for a given attribute, if one exists.
+ *
+ * Enqueued updates can take different data types:
+ * - If an update is enqueued and is boolean, the return will be `true`
+ * - If an update is otherwise enqueued, the return will be the string value of that update.
+ * - If an attribute is enqueued to be removed, the return will be `null` to indicate that.
+ * - If no updates are enqueued, the return will be `false` to differentiate from "removed."
+ *
+ * @since 6.2.0
+ *
+ * @param string $comparable_name The attribute name in its comparable form.
+ * @return string|boolean|null Value of enqueued update if present, otherwise false.
+ */
+ private function get_enqueued_attribute_value( $comparable_name ) {
+ if ( ! isset( $this->lexical_updates[ $comparable_name ] ) ) {
+ return false;
+ }
+
+ $enqueued_text = $this->lexical_updates[ $comparable_name ]->text;
+
+ // Removed attributes erase the entire span.
+ if ( '' === $enqueued_text ) {
+ return null;
+ }
+
+ /*
+ * Boolean attribute updates are just the attribute name without a corresponding value.
+ *
+ * This value might differ from the given comparable name in that there could be leading
+ * or trailing whitespace, and that the casing follows the name given in `set_attribute`.
+ *
+ * Example:
+ * ```
+ * $p->set_attribute( 'data-TEST-id', 'update' );
+ * 'update' === $p->get_enqueued_attribute_value( 'data-test-id' );
+ * ```
+ *
+ * Here we detect this based on the absence of the `=`, which _must_ exist in any
+ * attribute containing a value, e.g. ``.
+ * ¹ ²
+ * 1. Attribute with a string value.
+ * 2. Boolean attribute whose value is `true`.
+ */
+ $equals_at = strpos( $enqueued_text, '=' );
+ if ( false === $equals_at ) {
+ return true;
+ }
+
+ /*
+ * Finally, a normal update's value will appear after the `=` and
+ * be double-quoted, as performed incidentally by `set_attribute`.
+ *
+ * e.g. `type="text"`
+ * ¹² ³
+ * 1. Equals is here.
+ * 2. Double-quoting starts one after the equals sign.
+ * 3. Double-quoting ends at the last character in the update.
+ */
+ $enqueued_value = substr( $enqueued_text, $equals_at + 2, -1 );
+ return html_entity_decode( $enqueued_value );
+ }
+
/**
* Returns the value of the parsed attribute in the currently-opened tag.
*
@@ -1397,12 +1469,43 @@ public function get_attribute( $name ) {
}
$comparable = strtolower( $name );
+
+ /*
+ * For every attribute other than `class` we can perform a quick check if there's an
+ * enqueued lexical update whose value we should prefer over what's in the input HTML.
+ *
+ * The `class` attribute is special though because we expose the helpers `add_class`
+ * and `remove_class` which form a builder for the `class` attribute, so we have to
+ * additionally check if there are any enqueued class changes. If there are, we need
+ * to first flush them out so can report the full string value of the attribute.
+ */
+ if ( 'class' === $name ) {
+ $this->class_name_updates_to_attributes_updates();
+ }
+
+ // If we have an update for this attribute, return the updated value.
+ $enqueued_value = $this->get_enqueued_attribute_value( $comparable );
+ if ( false !== $enqueued_value ) {
+ return $enqueued_value;
+ }
+
if ( ! isset( $this->attributes[ $comparable ] ) ) {
return null;
}
$attribute = $this->attributes[ $comparable ];
+ /*
+ * This flag distinguishes an attribute with no value
+ * from an attribute with an empty string value. For
+ * unquoted attributes this could look very similar.
+ * It refers to whether an `=` follows the name.
+ *
+ * e.g.
+ * ¹ ²
+ * 1. Attribute `boolean-attribute` is `true`.
+ * 2. Attribute `empty-attribute` is `""`.
+ */
if ( true === $attribute->is_true ) {
return true;
}
@@ -1582,7 +1685,7 @@ public function set_attribute( $name, $value ) {
$updated_attribute = "{$name}=\"{$escaped_new_value}\"";
}
- /**
+ /*
* > There must never be two or more attributes on
* > the same start tag whose names are an ASCII
* > case-insensitive match for each other.
@@ -1628,6 +1731,14 @@ public function set_attribute( $name, $value ) {
' ' . $updated_attribute
);
}
+
+ /*
+ * Any calls to update the `class` attribute directly should wipe out any
+ * enqueued class changes from `add_class` and `remove_class`.
+ */
+ if ( 'class' === $comparable_name && ! empty( $this->classname_updates ) ) {
+ $this->classname_updates = array();
+ }
}
/**
@@ -1638,7 +1749,11 @@ public function set_attribute( $name, $value ) {
* @param string $name The attribute name to remove.
*/
public function remove_attribute( $name ) {
- /**
+ if ( $this->is_closing_tag ) {
+ return false;
+ }
+
+ /*
* > There must never be two or more attributes on
* > the same start tag whose names are an ASCII
* > case-insensitive match for each other.
@@ -1647,7 +1762,20 @@ public function remove_attribute( $name ) {
* @see https://html.spec.whatwg.org/multipage/syntax.html#attributes-2:ascii-case-insensitive
*/
$name = strtolower( $name );
- if ( $this->is_closing_tag || ! isset( $this->attributes[ $name ] ) ) {
+
+ /*
+ * Any calls to update the `class` attribute directly should wipe out any
+ * enqueued class changes from `add_class` and `remove_class`.
+ */
+ if ( 'class' === $name && count( $this->classname_updates ) !== 0 ) {
+ $this->classname_updates = array();
+ }
+
+ // If we updated an attribute we didn't originally have, remove the enqueued update and move on.
+ if ( ! isset( $this->attributes[ $name ] ) ) {
+ if ( isset( $this->lexical_updates[ $name ] ) ) {
+ unset( $this->lexical_updates[ $name ] );
+ }
return false;
}
diff --git a/phpunit/html/wp-html-tag-processor-test.php b/phpunit/html/wp-html-tag-processor-test.php
index 6750410f26d43..9d5da693d95f2 100644
--- a/phpunit/html/wp-html-tag-processor-test.php
+++ b/phpunit/html/wp-html-tag-processor-test.php
@@ -516,12 +516,256 @@ public function data_set_attribute_escapable_values() {
*
* @covers set_attribute
* @covers get_updated_html
+ * @covers get_attribute
*/
public function test_set_attribute_with_a_non_existing_attribute_adds_a_new_attribute_to_the_markup() {
$p = new WP_HTML_Tag_Processor( self::HTML_SIMPLE );
$p->next_tag();
$p->set_attribute( 'test-attribute', 'test-value' );
- $this->assertSame( '
',
+ $p->get_updated_html(),
+ 'Updated HTML does not include attribute added via set_attribute()'
+ );
+ $this->assertSame(
+ 'test-value',
+ $p->get_attribute( 'test-attribute' ),
+ 'get_attribute() (called after get_updated_html()) did not return attribute added via set_attribute()'
+ );
+ }
+
+ /**
+ * @ticket 56299
+ *
+ * @covers set_attribute
+ * @covers get_updated_html
+ * @covers get_attribute
+ */
+ public function test_get_attribute_returns_updated_values_before_they_are_updated() {
+ $p = new WP_HTML_Tag_Processor( self::HTML_SIMPLE );
+ $p->next_tag();
+ $p->set_attribute( 'test-attribute', 'test-value' );
+ $this->assertSame(
+ 'test-value',
+ $p->get_attribute( 'test-attribute' ),
+ 'get_attribute() (called before get_updated_html()) did not return attribute added via set_attribute()'
+ );
+ $this->assertSame(
+ '
Text
',
+ $p->get_updated_html(),
+ 'Updated HTML does not include attribute added via set_attribute()'
+ );
+ }
+
+ public function test_get_attribute_returns_updated_values_before_they_are_updated_with_different_name_casing() {
+ $p = new WP_HTML_Tag_Processor( self::HTML_SIMPLE );
+ $p->next_tag();
+ $p->set_attribute( 'test-ATTribute', 'test-value' );
+ $this->assertSame(
+ 'test-value',
+ $p->get_attribute( 'test-attribute' ),
+ 'get_attribute() (called before get_updated_html()) did not return attribute added via set_attribute()'
+ );
+ $this->assertSame(
+ '
Text
',
+ $p->get_updated_html(),
+ 'Updated HTML does not include attribute added via set_attribute()'
+ );
+ }
+
+ /**
+ * @ticket 56299
+ *
+ * @covers add_class
+ * @covers get_updated_html
+ * @covers get_attribute
+ */
+ public function test_get_attribute_reflects_added_class_names_before_they_are_updated() {
+ $p = new WP_HTML_Tag_Processor( self::HTML_SIMPLE );
+ $p->next_tag();
+ $p->add_class( 'my-class' );
+ $this->assertSame(
+ 'my-class',
+ $p->get_attribute( 'class' ),
+ 'get_attribute() (called before get_updated_html()) did not return class name added via add_class()'
+ );
+ $this->assertSame(
+ '
Text
',
+ $p->get_updated_html(),
+ 'Updated HTML does not include class name added via add_class()'
+ );
+ }
+
+ /**
+ * @ticket 56299
+ *
+ * @covers add_class
+ * @covers get_updated_html
+ * @covers get_attribute
+ */
+ public function test_get_attribute_reflects_added_class_names_before_they_are_updated_and_retains_classes_from_previous_add_class_calls() {
+ $p = new WP_HTML_Tag_Processor( self::HTML_SIMPLE );
+ $p->next_tag();
+ $p->add_class( 'my-class' );
+ $this->assertSame(
+ 'my-class',
+ $p->get_attribute( 'class' ),
+ 'get_attribute() (called before get_updated_html()) did not return class name added via add_class()'
+ );
+ $p->add_class( 'my-other-class' );
+ $this->assertSame(
+ 'my-class my-other-class',
+ $p->get_attribute( 'class' ),
+ 'get_attribute() (called before get_updated_html()) did not return class names added via subsequent add_class() calls'
+ );
+ $this->assertSame(
+ '
Text
',
+ $p->get_updated_html(),
+ 'Updated HTML does not include class names added via subsequent add_class() calls'
+ );
+ }
+
+ /**
+ * @ticket 56299
+ *
+ * @covers remove_attribute
+ * @covers get_attribute
+ * @covers get_updated_html
+ */
+ public function test_get_attribute_reflects_removed_attribute_before_it_is_updated() {
+ $p = new WP_HTML_Tag_Processor( self::HTML_SIMPLE );
+ $p->next_tag();
+ $p->remove_attribute( 'id' );
+ $this->assertNull(
+ $p->get_attribute( 'id' ),
+ 'get_attribute() (called before get_updated_html()) returned attribute that was removed by remove_attribute()'
+ );
+ $this->assertSame(
+ '
Text
',
+ $p->get_updated_html(),
+ 'Updated HTML includes attribute that was removed by remove_attribute()'
+ );
+ }
+
+ /**
+ * @ticket 56299
+ *
+ * @covers set_attribute
+ * @covers remove_attribute
+ * @covers get_attribute
+ * @covers get_updated_html
+ */
+ public function test_get_attribute_reflects_adding_and_then_removing_an_attribute_before_it_is_updated() {
+ $p = new WP_HTML_Tag_Processor( self::HTML_SIMPLE );
+ $p->next_tag();
+ $p->set_attribute( 'test-attribute', 'test-value' );
+ $p->remove_attribute( 'test-attribute' );
+ $this->assertNull(
+ $p->get_attribute( 'test-attribute' ),
+ 'get_attribute() (called before get_updated_html()) returned attribute that was added via set_attribute() and then removed by remove_attribute()'
+ );
+ $this->assertSame(
+ self::HTML_SIMPLE,
+ $p->get_updated_html(),
+ 'Updated HTML includes attribute that was added via set_attribute() and then removed by remove_attribute()'
+ );
+ }
+
+ /**
+ * @ticket 56299
+ *
+ * @covers set_attribute
+ * @covers remove_attribute
+ * @covers get_attribute
+ * @covers get_updated_html
+ */
+ public function test_get_attribute_reflects_setting_and_then_removing_an_existing_attribute_before_it_is_updated() {
+ $p = new WP_HTML_Tag_Processor( self::HTML_SIMPLE );
+ $p->next_tag();
+ $p->set_attribute( 'id', 'test-value' );
+ $p->remove_attribute( 'id' );
+ $this->assertNull(
+ $p->get_attribute( 'id' ),
+ 'get_attribute() (called before get_updated_html()) returned attribute that was overwritten by set_attribute() and then removed by remove_attribute()'
+ );
+ $this->assertSame(
+ '
Text
',
+ $p->get_updated_html(),
+ 'Updated HTML includes attribute that was overwritten by set_attribute() and then removed by remove_attribute()'
+ );
+ }
+
+ /**
+ * @ticket 56299
+ *
+ * @covers remove_class
+ * @covers get_updated_html
+ * @covers get_attribute
+ */
+ public function test_get_attribute_reflects_removed_class_names_before_they_are_updated() {
+ $p = new WP_HTML_Tag_Processor( self::HTML_WITH_CLASSES );
+ $p->next_tag();
+ $p->remove_class( 'with-border' );
+ $this->assertSame(
+ 'main',
+ $p->get_attribute( 'class' ),
+ 'get_attribute() (called before get_updated_html()) returned the wrong attribute after calling remove_attribute()'
+ );
+ $this->assertSame(
+ '
Text
',
+ $p->get_updated_html(),
+ 'Updated HTML includes wrong attribute after calling remove_attribute()'
+ );
+ }
+
+ /**
+ * @ticket 56299
+ *
+ * @covers add_class
+ * @covers remove_class
+ * @covers get_attribute
+ * @covers get_updated_html
+ */
+ public function test_get_attribute_reflects_setting_and_then_removing_a_class_name_before_it_is_updated() {
+ $p = new WP_HTML_Tag_Processor( self::HTML_WITH_CLASSES );
+ $p->next_tag();
+ $p->add_class( 'foo-class' );
+ $p->remove_class( 'foo-class' );
+ $this->assertSame(
+ 'main with-border',
+ $p->get_attribute( 'class' ),
+ 'get_attribute() (called before get_updated_html()) returned class name that was added via add_class() and then removed by remove_class()'
+ );
+ $this->assertSame(
+ self::HTML_WITH_CLASSES,
+ $p->get_updated_html(),
+ 'Updated HTML includes class that was added via add_class() and then removed by remove_class()'
+ );
+ }
+
+ /**
+ * @ticket 56299
+ *
+ * @covers add_class
+ * @covers remove_class
+ * @covers get_attribute
+ * @covers get_updated_html
+ */
+ public function test_get_attribute_reflects_duplicating_and_then_removing_an_existing_class_name_before_it_is_updated() {
+ $p = new WP_HTML_Tag_Processor( self::HTML_WITH_CLASSES );
+ $p->next_tag();
+ $p->add_class( 'with-border' );
+ $p->remove_class( 'with-border' );
+ $this->assertSame(
+ 'main',
+ $p->get_attribute( 'class' ),
+ 'get_attribute() (called before get_updated_html()) returned class name that was duplicated via add_class() and then removed by remove_class()'
+ );
+ $this->assertSame(
+ '
Text
',
+ $p->get_updated_html(),
+ 'Updated HTML includes class that was duplicated via add_class() and then removed by remove_class()'
+ );
}
/**
@@ -621,12 +865,22 @@ public function test_remove_attribute_with_a_non_existing_attribute_name_does_no
*
* @covers add_class
* @covers get_updated_html
+ * @covers get_attribute
*/
public function test_add_class_creates_a_class_attribute_when_there_is_none() {
$p = new WP_HTML_Tag_Processor( self::HTML_SIMPLE );
$p->next_tag();
$p->add_class( 'foo-class' );
- $this->assertSame( '
',
+ $p->get_updated_html(),
+ 'Updated HTML does not include class name added via add_class()'
+ );
+ $this->assertSame(
+ 'foo-class',
+ $p->get_attribute( 'class' ),
+ "get_attribute( 'class' ) did not return class name added via add_class()"
+ );
}
/**
@@ -634,13 +888,23 @@ public function test_add_class_creates_a_class_attribute_when_there_is_none() {
*
* @covers add_class
* @covers get_updated_html
+ * @covers get_attribute
*/
public function test_calling_add_class_twice_creates_a_class_attribute_with_both_class_names_when_there_is_no_class_attribute() {
$p = new WP_HTML_Tag_Processor( self::HTML_SIMPLE );
$p->next_tag();
$p->add_class( 'foo-class' );
$p->add_class( 'bar-class' );
- $this->assertSame( '
',
+ $p->get_updated_html(),
+ 'Updated HTML does not include class names added via subsequent add_class() calls'
+ );
+ $this->assertSame(
+ 'foo-class bar-class',
+ $p->get_attribute( 'class' ),
+ "get_attribute( 'class' ) did not return class names added via subsequent add_class() calls"
+ );
}
/**
@@ -648,12 +912,21 @@ public function test_calling_add_class_twice_creates_a_class_attribute_with_both
*
* @covers remove_class
* @covers get_updated_html
+ * @covers get_attribute
*/
public function test_remove_class_does_not_change_the_markup_when_there_is_no_class_attribute() {
$p = new WP_HTML_Tag_Processor( self::HTML_SIMPLE );
$p->next_tag();
$p->remove_class( 'foo-class' );
- $this->assertSame( self::HTML_SIMPLE, $p->get_updated_html() );
+ $this->assertSame(
+ self::HTML_SIMPLE,
+ $p->get_updated_html(),
+ 'Updated HTML includes class name that was removed by remove_class()'
+ );
+ $this->assertNull(
+ $p->get_attribute( 'class' ),
+ "get_attribute( 'class' ) did not return null for class name that was removed by remove_class()"
+ );
}
/**
@@ -661,6 +934,7 @@ public function test_remove_class_does_not_change_the_markup_when_there_is_no_cl
*
* @covers add_class
* @covers get_updated_html
+ * @covers get_attribute
*/
public function test_add_class_appends_class_names_to_the_existing_class_attribute_when_one_already_exists() {
$p = new WP_HTML_Tag_Processor( self::HTML_WITH_CLASSES );
@@ -669,7 +943,13 @@ public function test_add_class_appends_class_names_to_the_existing_class_attribu
$p->add_class( 'bar-class' );
$this->assertSame(
'
Text
',
- $p->get_updated_html()
+ $p->get_updated_html(),
+ 'Updated HTML does not reflect class names added to existing class attribute via subsequent add_class() calls'
+ );
+ $this->assertSame(
+ 'main with-border foo-class bar-class',
+ $p->get_attribute( 'class' ),
+ "get_attribute( 'class' ) does not reflect class names added to existing class attribute via subsequent add_class() calls"
);
}
@@ -678,6 +958,7 @@ public function test_add_class_appends_class_names_to_the_existing_class_attribu
*
* @covers remove_class
* @covers get_updated_html
+ * @covers get_attribute
*/
public function test_remove_class_removes_a_single_class_from_the_class_attribute_when_one_exists() {
$p = new WP_HTML_Tag_Processor( self::HTML_WITH_CLASSES );
@@ -685,7 +966,13 @@ public function test_remove_class_removes_a_single_class_from_the_class_attribut
$p->remove_class( 'main' );
$this->assertSame(
'
Text
',
- $p->get_updated_html()
+ $p->get_updated_html(),
+ 'Updated HTML does not reflect class name removed from existing class attribute via remove_class()'
+ );
+ $this->assertSame(
+ ' with-border',
+ $p->get_attribute( 'class' ),
+ "get_attribute( 'class' ) does not reflect class name removed from existing class attribute via remove_class()"
);
}
@@ -694,6 +981,7 @@ public function test_remove_class_removes_a_single_class_from_the_class_attribut
*
* @covers remove_class
* @covers get_updated_html
+ * @covers get_attribute
*/
public function test_calling_remove_class_with_all_listed_class_names_removes_the_existing_class_attribute_from_the_markup() {
$p = new WP_HTML_Tag_Processor( self::HTML_WITH_CLASSES );
@@ -702,7 +990,12 @@ public function test_calling_remove_class_with_all_listed_class_names_removes_th
$p->remove_class( 'with-border' );
$this->assertSame(
'
Text
',
- $p->get_updated_html()
+ $p->get_updated_html(),
+ 'Updated HTML does not reflect class attribute removed via subesequent remove_class() calls'
+ );
+ $this->assertNull(
+ $p->get_attribute( 'class' ),
+ "get_attribute( 'class' ) did not return null for class attribute removed via subesequent remove_class() calls"
);
}
@@ -711,6 +1004,7 @@ public function test_calling_remove_class_with_all_listed_class_names_removes_th
*
* @covers add_class
* @covers get_updated_html
+ * @covers get_attribute
*/
public function test_add_class_does_not_add_duplicate_class_names() {
$p = new WP_HTML_Tag_Processor( self::HTML_WITH_CLASSES );
@@ -718,7 +1012,13 @@ public function test_add_class_does_not_add_duplicate_class_names() {
$p->add_class( 'with-border' );
$this->assertSame(
'
Text
',
- $p->get_updated_html()
+ $p->get_updated_html(),
+ 'Updated HTML does not reflect deduplicated class name added via add_class()'
+ );
+ $this->assertSame(
+ 'main with-border',
+ $p->get_attribute( 'class' ),
+ "get_attribute( 'class' ) does not reflect deduplicated class name added via add_class()"
);
}
@@ -727,6 +1027,7 @@ public function test_add_class_does_not_add_duplicate_class_names() {
*
* @covers add_class
* @covers get_updated_html
+ * @covers get_attribute
*/
public function test_add_class_preserves_class_name_order_when_a_duplicate_class_name_is_added() {
$p = new WP_HTML_Tag_Processor( self::HTML_WITH_CLASSES );
@@ -734,7 +1035,13 @@ public function test_add_class_preserves_class_name_order_when_a_duplicate_class
$p->add_class( 'main' );
$this->assertSame(
'
Text
',
- $p->get_updated_html()
+ $p->get_updated_html(),
+ 'Updated HTML does not reflect class name order after adding duplicated class name via add_class()'
+ );
+ $this->assertSame(
+ 'main with-border',
+ $p->get_attribute( 'class' ),
+ "get_attribute( 'class' ) does not reflect class name order after adding duplicated class name added via add_class()"
);
}
@@ -743,6 +1050,7 @@ public function test_add_class_preserves_class_name_order_when_a_duplicate_class
*
* @covers add_class
* @covers get_updated_html
+ * @covers get_attribute
*/
public function test_add_class_when_there_is_a_class_attribute_with_excessive_whitespaces() {
$p = new WP_HTML_Tag_Processor(
@@ -752,7 +1060,13 @@ public function test_add_class_when_there_is_a_class_attribute_with_excessive_wh
$p->add_class( 'foo-class' );
$this->assertSame(
'
Text
',
- $p->get_updated_html()
+ $p->get_updated_html(),
+ 'Updated HTML does not reflect existing excessive whitespace after adding class name via add_class()'
+ );
+ $this->assertSame(
+ ' main with-border foo-class',
+ $p->get_attribute( 'class' ),
+ "get_attribute( 'class' ) does not reflect existing excessive whitespace after adding class name via add_class()"
);
}
@@ -761,6 +1075,7 @@ public function test_add_class_when_there_is_a_class_attribute_with_excessive_wh
*
* @covers remove_class
* @covers get_updated_html
+ * @covers get_attribute
*/
public function test_remove_class_preserves_whitespaces_when_there_is_a_class_attribute_with_excessive_whitespaces() {
$p = new WP_HTML_Tag_Processor(
@@ -770,7 +1085,13 @@ public function test_remove_class_preserves_whitespaces_when_there_is_a_class_at
$p->remove_class( 'with-border' );
$this->assertSame(
'
Text
',
- $p->get_updated_html()
+ $p->get_updated_html(),
+ 'Updated HTML does not reflect existing excessive whitespace after removing class name via remove_class()'
+ );
+ $this->assertSame(
+ ' main',
+ $p->get_attribute( 'class' ),
+ "get_attribute( 'class' ) does not reflect existing excessive whitespace after removing class name via removing_class()"
);
}
@@ -779,6 +1100,7 @@ public function test_remove_class_preserves_whitespaces_when_there_is_a_class_at
*
* @covers remove_class
* @covers get_updated_html
+ * @covers get_attribute
*/
public function test_removing_all_classes_removes_the_existing_class_attribute_from_the_markup_even_when_excessive_whitespaces_are_present() {
$p = new WP_HTML_Tag_Processor(
@@ -789,21 +1111,29 @@ public function test_removing_all_classes_removes_the_existing_class_attribute_f
$p->remove_class( 'with-border' );
$this->assertSame(
'
Text
',
- $p->get_updated_html()
+ $p->get_updated_html(),
+ 'Updated HTML does not reflect removed class attribute after removing all class names via remove_class()'
+ );
+ $this->assertNull(
+ $p->get_attribute( 'class' ),
+ "get_attribute( 'class' ) did not return null after removing all class names via remove_class()"
);
}
/**
- * When both set_attribute('class', $value) and add_class( $different_value ) are called,
- * the final class name should be $value. In other words, the `add_class` call should be ignored,
- * and the `set_attribute` call should win. This holds regardless of the order in which these methods
- * are called.
+ * When add_class( $different_value ) is called _after_ set_attribute( 'class', $value ), the
+ * final class name should be "$value $different_value". In other words, the `add_class` call
+ * should append its class to the one(s) set by `set_attribute`. When `add_class( $different_value )`
+ * is called _before_ `set_attribute( 'class', $value )`, however, the final class name should be
+ * "$value" instead, as any direct updates to the `class` attribute supersede any changes enqueued
+ * via the class builder methods.
*
* @ticket 56299
*
* @covers add_class
* @covers set_attribute
* @covers get_updated_html
+ * @covers get_attribute
*/
public function test_set_attribute_takes_priority_over_add_class() {
$p = new WP_HTML_Tag_Processor( self::HTML_WITH_CLASSES );
@@ -813,17 +1143,126 @@ public function test_set_attribute_takes_priority_over_add_class() {
$this->assertSame(
'
Text
',
$p->get_updated_html(),
- 'Calling get_updated_html after updating first tag\'s attributes did not return the expected HTML'
+ "Calling get_updated_html after updating first tag's attributes did not return the expected HTML"
+ );
+ $this->assertSame(
+ 'set_attribute',
+ $p->get_attribute( 'class' ),
+ "Calling get_attribute after updating first tag's attributes did not return the expected class name"
);
$p = new WP_HTML_Tag_Processor( self::HTML_WITH_CLASSES );
$p->next_tag();
$p->set_attribute( 'class', 'set_attribute' );
$p->add_class( 'add_class' );
+ $this->assertSame(
+ '
Text
',
+ $p->get_updated_html(),
+ "Calling get_updated_html after updating first tag's attributes did not return the expected HTML"
+ );
+ $this->assertSame(
+ 'set_attribute add_class',
+ $p->get_attribute( 'class' ),
+ "Calling get_attribute after updating first tag's attributes did not return the expected class name"
+ );
+ }
+
+ /**
+ * When add_class( $different_value ) is called _after_ set_attribute( 'class', $value ), the
+ * final class name should be "$value $different_value". In other words, the `add_class` call
+ * should append its class to the one(s) set by `set_attribute`. When `add_class( $different_value )`
+ * is called _before_ `set_attribute( 'class', $value )`, however, the final class name should be
+ * "$value" instead, as any direct updates to the `class` attribute supersede any changes enqueued
+ * via the class builder methods.
+ *
+ * This is still true if we read enqueued updates before calling `get_updated_html()`.
+ *
+ * @ticket 56299
+ *
+ * @covers add_class
+ * @covers set_attribute
+ * @covers get_attribute
+ * @covers get_updated_html
+ */
+ public function test_set_attribute_takes_priority_over_add_class_even_before_updating() {
+ $p = new WP_HTML_Tag_Processor( self::HTML_WITH_CLASSES );
+ $p->next_tag();
+ $p->add_class( 'add_class' );
+ $p->set_attribute( 'class', 'set_attribute' );
+ $this->assertSame(
+ 'set_attribute',
+ $p->get_attribute( 'class' ),
+ "Calling get_attribute after updating first tag's attributes did not return the expected class name"
+ );
$this->assertSame(
'
Text
',
$p->get_updated_html(),
- 'Calling get_updated_html after updating second tag\'s attributes did not return the expected HTML'
+ "Calling get_updated_html after updating first tag's attributes did not return the expected HTML"
+ );
+
+ $p = new WP_HTML_Tag_Processor( self::HTML_WITH_CLASSES );
+ $p->next_tag();
+ $p->set_attribute( 'class', 'set_attribute' );
+ $p->add_class( 'add_class' );
+ $this->assertSame(
+ 'set_attribute add_class',
+ $p->get_attribute( 'class' ),
+ "Calling get_attribute after updating first tag's attributes did not return the expected class name"
+ );
+ $this->assertSame(
+ '
Text
',
+ $p->get_updated_html(),
+ "Calling get_updated_html after updating first tag's attributes did not return the expected HTML"
+ );
+ }
+
+ /**
+ * @ticket 56299
+ *
+ * @covers set_attribute
+ * @covers add_class
+ * @covers get_attribute
+ * @covers get_updated_html
+ */
+ public function test_add_class_overrides_boolean_class_attribute() {
+ $p = new WP_HTML_Tag_Processor( self::HTML_SIMPLE );
+ $p->next_tag();
+ $p->set_attribute( 'class', true );
+ $p->add_class( 'add_class' );
+ $this->assertSame(
+ '
Text
',
+ $p->get_updated_html(),
+ "Updated HTML doesn't reflect class added via add_class that was originally set as boolean attribute"
+ );
+ $this->assertSame(
+ 'add_class',
+ $p->get_attribute( 'class' ),
+ "get_attribute (called after get_updated_html()) doesn't reflect class added via add_class that was originally set as boolean attribute"
+ );
+ }
+
+ /**
+ * @ticket 56299
+ *
+ * @covers set_attribute
+ * @covers add_class
+ * @covers get_attribute
+ * @covers get_updated_html
+ */
+ public function test_add_class_overrides_boolean_class_attribute_even_before_updating() {
+ $p = new WP_HTML_Tag_Processor( self::HTML_SIMPLE );
+ $p->next_tag();
+ $p->set_attribute( 'class', true );
+ $p->add_class( 'add_class' );
+ $this->assertSame(
+ 'add_class',
+ $p->get_attribute( 'class' ),
+ "get_attribute (called before get_updated_html()) doesn't reflect class added via add_class that was originally set as boolean attribute"
+ );
+ $this->assertSame(
+ '
Text
',
+ $p->get_updated_html(),
+ "Updated HTML doesn't reflect class added via add_class that was originally set as boolean attribute"
);
}