diff --git a/tests/phpunit/tests/html-api/wpCssAttributeSelector.php b/tests/phpunit/tests/html-api/wpCssAttributeSelector.php
index e574cedd1876b..b26110bc7bb89 100644
--- a/tests/phpunit/tests/html-api/wpCssAttributeSelector.php
+++ b/tests/phpunit/tests/html-api/wpCssAttributeSelector.php
@@ -85,6 +85,32 @@ public static function data_attribute_selectors(): array {
"Invalid: [att='val\\n']" => array( "[att='val\n']" ),
'Invalid: [att=val i ' => array( '[att=val i ' ),
'Invalid: [att="val"ix' => array( '[att="val"ix' ),
+
+ // Additional malformed selector tests
+ 'Invalid: [att=val' => array( '[att=val' ),
+ 'Invalid: [att="val' => array( '[att="val' ),
+ 'Invalid: [att=val"' => array( '[att=val"' ),
+ 'Invalid: [att =val i i]' => array( '[att =val i i]' ),
+ 'Invalid: [att~=]' => array( '[att~=]' ),
+ 'Invalid: [att^=]' => array( '[att^=]' ),
+ 'Invalid: [att$=]' => array( '[att$=]' ),
+ 'Invalid: [att*=]' => array( '[att*=]' ),
+ 'Invalid: [att|=]' => array( '[att|=]' ),
+ 'Invalid: [att==val]' => array( '[att==val]' ),
+ 'Invalid: [att =~ val]' => array( '[att =~ val]' ),
+ 'Invalid: [att!val]' => array( '[att!val]' ),
+ 'Invalid: [att?=val]' => array( '[att?=val]' ),
+ 'Invalid: [att =val x]' => array( '[att =val x]' ),
+ 'Invalid: [att =val ii]' => array( '[att =val ii]' ),
+ 'Invalid: [att =val ss]' => array( '[att =val ss]' ),
+
+ // Namespace attribute selectors (currently unsupported)
+ 'Invalid: [ns|attr]' => array( '[ns|attr]' ),
+ 'Invalid: [*|attr]' => array( '[*|attr]' ),
+ 'Invalid: [|attr]' => array( '[|attr]' ),
+ 'Invalid: [xml|attr]' => array( '[xml|lang]' ),
+ 'Invalid: [svg|attr]' => array( '[svg|viewBox]' ),
+ 'Invalid: [xmlns|attr]' => array( '[xmlns:xlink]' ),
);
}
}
diff --git a/tests/phpunit/tests/html-api/wpCssClassSelector.php b/tests/phpunit/tests/html-api/wpCssClassSelector.php
index 9646d05da23d5..045a0ec6060c3 100644
--- a/tests/phpunit/tests/html-api/wpCssClassSelector.php
+++ b/tests/phpunit/tests/html-api/wpCssClassSelector.php
@@ -41,9 +41,20 @@ public static function data_class_selectors(): array {
'escaped .\31 23' => array( '.\\31 23', '123', '' ),
'with descendant .\31 23 div' => array( '.\\31 23 div', '123', ' div' ),
+ // Additional edge cases
+ 'multiple dots .a.b' => array( '.a.b', 'a', '.b' ),
+ 'escaped dot .\\2e class' => array( '.\\2e class', '.class', '' ),
+ 'unicode class .café' => array( '.café', 'café', '' ),
+ 'hyphen class .my-class' => array( '.my-class', 'my-class', '' ),
+ 'underscore class .my_class' => array( '.my_class', 'my_class', '' ),
+ 'long class name' => array( '.very-long-class-name-with-many-hyphens', 'very-long-class-name-with-many-hyphens', '' ),
+
'not class foo' => array( 'foo' ),
'not class #bar' => array( '#bar' ),
'not valid .1foo' => array( '.1foo' ),
+ 'empty after dot' => array( '.' ),
+ 'space after dot' => array( '. ' ),
+ 'invalid after dot' => array( '.@invalid' ),
);
}
}
diff --git a/tests/phpunit/tests/html-api/wpCssComplexSelector.php b/tests/phpunit/tests/html-api/wpCssComplexSelector.php
index 8738bb6fc32d2..decef65a62ffb 100644
--- a/tests/phpunit/tests/html-api/wpCssComplexSelector.php
+++ b/tests/phpunit/tests/html-api/wpCssComplexSelector.php
@@ -68,4 +68,74 @@ public function test_parse_empty_complex_selector() {
$result = WP_CSS_Complex_Selector::parse( $input, $offset );
$this->assertNull( $result );
}
+
+ /**
+ * @ticket 62653
+ */
+ public function test_parse_unsupported_next_sibling_combinator() {
+ $input = 'h1 + p';
+ $offset = 0;
+ $result = WP_CSS_Complex_Selector::parse( $input, $offset );
+ $this->assertNull( $result, 'Next sibling combinator (+) should not be supported' );
+ }
+
+ /**
+ * @ticket 62653
+ */
+ public function test_parse_unsupported_subsequent_sibling_combinator() {
+ $input = 'h1 ~ p';
+ $offset = 0;
+ $result = WP_CSS_Complex_Selector::parse( $input, $offset );
+ $this->assertNull( $result, 'Subsequent sibling combinator (~) should not be supported' );
+ }
+
+ /**
+ * @ticket 62653
+ */
+ public function test_parse_complex_selector_with_multiple_combinators() {
+ $input = 'div > ul li > a.link';
+ $offset = 0;
+ $result = WP_CSS_Complex_Selector::parse( $input, $offset );
+
+ $this->assertNotNull( $result );
+ $this->assertSame( 3, count( $result->context_selectors ) );
+
+ $this->assertInstanceOf( WP_CSS_Compound_Selector::class, $result->self_selector );
+ $this->assertSame( 'a', $result->self_selector->type_selector->type );
+ $this->assertSame( 'link', $result->self_selector->subclass_selectors[0]->class_name );
+
+ // Check context selectors are in reverse order
+ $this->assertSame( 3, count( $result->context_selectors ) );
+
+ $this->assertSame( 'li', $result->context_selectors[0][0]->type );
+ $this->assertSame( WP_CSS_Complex_Selector::COMBINATOR_CHILD, $result->context_selectors[0][1] );
+
+ $this->assertSame( 'ul', $result->context_selectors[1][0]->type );
+ $this->assertSame( WP_CSS_Complex_Selector::COMBINATOR_DESCENDANT, $result->context_selectors[1][1] );
+
+ $this->assertSame( 'div', $result->context_selectors[2][0]->type );
+ $this->assertSame( WP_CSS_Complex_Selector::COMBINATOR_CHILD, $result->context_selectors[2][1] );
+ }
+
+ /**
+ * @ticket 62653
+ */
+ public function test_parse_complex_selector_with_whitespace_variations() {
+ $input = "div\n>\t\rul \f li\r\n>\na.link";
+ $offset = 0;
+ $result = WP_CSS_Complex_Selector::parse( $input, $offset );
+
+ $this->assertNotNull( $result );
+ $this->assertSame( 3, count( $result->context_selectors ) );
+ }
+
+ /**
+ * @ticket 62653
+ */
+ public function test_parse_invalid_trailing_combinator() {
+ $input = 'div > ul >';
+ $offset = 0;
+ $result = WP_CSS_Complex_Selector::parse( $input, $offset );
+ $this->assertNull( $result, 'Trailing combinator should make selector invalid' );
+ }
}
diff --git a/tests/phpunit/tests/html-api/wpCssComplexSelectorList.php b/tests/phpunit/tests/html-api/wpCssComplexSelectorList.php
index edf912e97f490..628f4062982fb 100644
--- a/tests/phpunit/tests/html-api/wpCssComplexSelectorList.php
+++ b/tests/phpunit/tests/html-api/wpCssComplexSelectorList.php
@@ -48,4 +48,49 @@ public function test_parse_empty_selector_list() {
$result = WP_CSS_Complex_Selector_List::from_selectors( $input );
$this->assertNull( $result );
}
+
+ /**
+ * @ticket 62653
+ */
+ public function test_parse_single_selector() {
+ $input = 'div.class';
+ $result = WP_CSS_Complex_Selector_List::from_selectors( $input );
+ $this->assertNotNull( $result );
+ }
+
+ /**
+ * @ticket 62653
+ */
+ public function test_parse_selector_list_with_whitespace() {
+ $input = " div.class1 ,\n\t span#id2 , p[attr='value'] ";
+ $result = WP_CSS_Complex_Selector_List::from_selectors( $input );
+ $this->assertNotNull( $result );
+ }
+
+ /**
+ * @ticket 62653
+ */
+ public function test_parse_selector_list_with_unsupported_sibling_combinator() {
+ $input = 'div + p, span.class';
+ $result = WP_CSS_Complex_Selector_List::from_selectors( $input );
+ $this->assertNull( $result );
+ }
+
+ /**
+ * @ticket 62653
+ */
+ public function test_parse_selector_list_with_trailing_comma() {
+ $input = 'div.class,';
+ $result = WP_CSS_Complex_Selector_List::from_selectors( $input );
+ $this->assertNull( $result );
+ }
+
+ /**
+ * @ticket 62653
+ */
+ public function test_parse_selector_list_with_leading_comma() {
+ $input = ',div.class';
+ $result = WP_CSS_Complex_Selector_List::from_selectors( $input );
+ $this->assertNull( $result );
+ }
}
diff --git a/tests/phpunit/tests/html-api/wpCssCompoundSelector.php b/tests/phpunit/tests/html-api/wpCssCompoundSelector.php
index 8092ee049b6e1..7e798f950bca2 100644
--- a/tests/phpunit/tests/html-api/wpCssCompoundSelector.php
+++ b/tests/phpunit/tests/html-api/wpCssCompoundSelector.php
@@ -41,4 +41,133 @@ public function test_parse_empty_selector() {
$this->assertNull( $result );
$this->assertSame( 0, $offset );
}
+
+ /**
+ * @ticket 62653
+ */
+ public function test_parse_complex_compound_selector() {
+ $input = 'div#main.container.large[data-test="value"][role="button"][aria-expanded="false"]';
+ $offset = 0;
+ $sel = WP_CSS_Compound_Selector::parse( $input, $offset );
+
+ $this->assertNotNull( $sel );
+ $this->assertSame( 'div', $sel->type_selector->type );
+ $this->assertSame( 6, count( $sel->subclass_selectors ) );
+
+ // Check ID selector
+ $this->assertSame( 'main', $sel->subclass_selectors[0]->id );
+
+ // Check class selectors
+ $this->assertSame( 'container', $sel->subclass_selectors[1]->class_name );
+ $this->assertSame( 'large', $sel->subclass_selectors[2]->class_name );
+
+ // Check attribute selectors
+ $this->assertSame( 'data-test', $sel->subclass_selectors[3]->name );
+ $this->assertSame( 'value', $sel->subclass_selectors[3]->value );
+ $this->assertSame( 'role', $sel->subclass_selectors[4]->name );
+ $this->assertSame( 'button', $sel->subclass_selectors[4]->value );
+ $this->assertSame( 'aria-expanded', $sel->subclass_selectors[5]->name );
+ $this->assertSame( 'false', $sel->subclass_selectors[5]->value );
+ }
+
+ /**
+ * @ticket 62653
+ */
+ public function test_parse_selector_with_only_subclass_selectors() {
+ $input = '.class1.class2#id[attr="value"]';
+ $offset = 0;
+ $sel = WP_CSS_Compound_Selector::parse( $input, $offset );
+
+ $this->assertNotNull( $sel );
+ $this->assertNull( $sel->type_selector );
+ $this->assertSame( 4, count( $sel->subclass_selectors ) );
+ }
+
+ /**
+ * @ticket 62653
+ */
+ public function test_parse_universal_selector_with_subclass() {
+ $input = '*.class#id[attr]';
+ $offset = 0;
+ $sel = WP_CSS_Compound_Selector::parse( $input, $offset );
+
+ $this->assertNotNull( $sel );
+ $this->assertSame( '*', $sel->type_selector->type );
+ $this->assertSame( 3, count( $sel->subclass_selectors ) );
+ }
+
+ /**
+ * @ticket 62653
+ * @dataProvider data_unsupported_pseudo_selectors
+ */
+ public function test_parse_unsupported_pseudo_selectors( $input, $expected_type, $expected_offset ) {
+ $offset = 0;
+ $sel = WP_CSS_Compound_Selector::parse( $input, $offset );
+
+ if ( null === $expected_type ) {
+ $this->assertNull( $sel );
+ } else {
+ $this->assertNotNull( $sel );
+ $this->assertSame( $expected_type, $sel->type_selector->type );
+ }
+ $this->assertSame( $expected_offset, $offset );
+ }
+
+ /**
+ * Data provider for unsupported pseudo-selectors.
+ *
+ * @return array
+ */
+ public static function data_unsupported_pseudo_selectors(): array {
+ return array(
+ // Pseudo-classes that should be rejected
+ 'pseudo-class :hover' => array( 'a:hover', 'a', 1 ),
+ 'pseudo-class :focus' => array( 'input:focus', 'input', 5 ),
+ 'pseudo-class :active' => array( 'button:active', 'button', 6 ),
+ 'pseudo-class :visited' => array( 'a:visited', 'a', 1 ),
+ 'pseudo-class :nth-child' => array( 'p:nth-child(2)', 'p', 1 ),
+ 'pseudo-class :first-child' => array( 'li:first-child', 'li', 2 ),
+ 'pseudo-class :last-child' => array( 'li:last-child', 'li', 2 ),
+ 'pseudo-class :not' => array( 'div:not(.class)', 'div', 3 ),
+ 'pseudo-class :is' => array( 'div:is(.class)', 'div', 3 ),
+ 'pseudo-class :where' => array( 'div:where(.class)', 'div', 3 ),
+ 'pseudo-class :has' => array( 'div:has(.class)', 'div', 3 ),
+ 'pseudo-class :root' => array( 'html:root', 'html', 4 ),
+ 'pseudo-class :empty' => array( 'div:empty', 'div', 3 ),
+ 'pseudo-class :target' => array( 'div:target', 'div', 3 ),
+ 'pseudo-class :lang' => array( 'div:lang(en)', 'div', 3 ),
+ 'pseudo-class :dir' => array( 'div:dir(ltr)', 'div', 3 ),
+ 'pseudo-class :checked' => array( 'input:checked', 'input', 5 ),
+ 'pseudo-class :disabled' => array( 'input:disabled', 'input', 5 ),
+ 'pseudo-class :enabled' => array( 'input:enabled', 'input', 5 ),
+ 'pseudo-class :required' => array( 'input:required', 'input', 5 ),
+ 'pseudo-class :optional' => array( 'input:optional', 'input', 5 ),
+ 'pseudo-class :valid' => array( 'input:valid', 'input', 5 ),
+ 'pseudo-class :invalid' => array( 'input:invalid', 'input', 5 ),
+
+ // Pseudo-elements that should be rejected
+ 'pseudo-element ::before' => array( 'div::before', 'div', 3 ),
+ 'pseudo-element ::after' => array( 'div::after', 'div', 3 ),
+ 'pseudo-element ::first-line' => array( 'p::first-line', 'p', 1 ),
+ 'pseudo-element ::first-letter' => array( 'p::first-letter', 'p', 1 ),
+ 'pseudo-element ::selection' => array( 'p::selection', 'p', 1 ),
+ 'pseudo-element ::backdrop' => array( 'dialog::backdrop', 'dialog', 6 ),
+ 'pseudo-element ::placeholder' => array( 'input::placeholder', 'input', 5 ),
+ 'pseudo-element ::marker' => array( 'li::marker', 'li', 2 ),
+ 'pseudo-element ::cue' => array( 'video::cue', 'video', 5 ),
+ 'pseudo-element ::slotted' => array( 'slot::slotted(.class)', 'slot', 4 ),
+
+ // Legacy single-colon pseudo-elements
+ 'legacy :before' => array( 'div:before', 'div', 3 ),
+ 'legacy :after' => array( 'div:after', 'div', 3 ),
+ 'legacy :first-line' => array( 'p:first-line', 'p', 1 ),
+ 'legacy :first-letter' => array( 'p:first-letter', 'p', 1 ),
+
+ // Invalid pseudo-selectors
+ 'invalid ::' => array( 'div::', 'div', 3 ),
+ 'invalid : alone' => array( 'div: ', 'div', 3 ),
+ 'invalid :123' => array( 'div:123', 'div', 3 ),
+ 'invalid :@#$' => array( 'div:@#$', 'div', 3 ),
+ );
+ }
}
diff --git a/tests/phpunit/tests/html-api/wpCssCompoundSelectorList.php b/tests/phpunit/tests/html-api/wpCssCompoundSelectorList.php
index 8f1d3dfb88a45..0f8c6a869739a 100644
--- a/tests/phpunit/tests/html-api/wpCssCompoundSelectorList.php
+++ b/tests/phpunit/tests/html-api/wpCssCompoundSelectorList.php
@@ -57,4 +57,58 @@ public function test_unsupported_complex_selector() {
$result = WP_CSS_Compound_Selector_List::from_selectors( $input );
$this->assertNull( $result );
}
+
+ /**
+ * @ticket 62653
+ */
+ public function test_parse_single_compound_selector() {
+ $input = 'div.class#id[attr="value"]';
+ $result = WP_CSS_Compound_Selector_List::from_selectors( $input );
+ $this->assertNotNull( $result );
+ }
+
+ /**
+ * @ticket 62653
+ */
+ public function test_parse_compound_selector_list_with_whitespace() {
+ $input = " div.class1 ,\n\t span#id2 , p[attr='value'] ";
+ $result = WP_CSS_Compound_Selector_List::from_selectors( $input );
+ $this->assertNotNull( $result );
+ }
+
+ /**
+ * @ticket 62653
+ */
+ public function test_parse_compound_selector_list_with_child_combinator() {
+ $input = 'div > p, span.class';
+ $result = WP_CSS_Compound_Selector_List::from_selectors( $input );
+ $this->assertNull( $result );
+ }
+
+ /**
+ * @ticket 62653
+ */
+ public function test_parse_compound_selector_list_with_sibling_combinator() {
+ $input = 'div + p, span.class';
+ $result = WP_CSS_Compound_Selector_List::from_selectors( $input );
+ $this->assertNull( $result );
+ }
+
+ /**
+ * @ticket 62653
+ */
+ public function test_parse_compound_selector_list_with_trailing_comma() {
+ $input = 'div.class,';
+ $result = WP_CSS_Compound_Selector_List::from_selectors( $input );
+ $this->assertNull( $result );
+ }
+
+ /**
+ * @ticket 62653
+ */
+ public function test_parse_compound_selector_list_with_leading_comma() {
+ $input = ',div.class';
+ $result = WP_CSS_Compound_Selector_List::from_selectors( $input );
+ $this->assertNull( $result );
+ }
}
diff --git a/tests/phpunit/tests/html-api/wpCssIdSelector.php b/tests/phpunit/tests/html-api/wpCssIdSelector.php
index 6dc2e5461ea03..9e0f8555364e1 100644
--- a/tests/phpunit/tests/html-api/wpCssIdSelector.php
+++ b/tests/phpunit/tests/html-api/wpCssIdSelector.php
@@ -41,10 +41,21 @@ public static function data_id_selectors(): array {
'escaped #\31 23' => array( '#\\31 23', '123', '' ),
'with descendant #\31 23 div' => array( '#\\31 23 div', '123', ' div' ),
+ // Additional edge cases
+ 'multiple hashes #a#b' => array( '#a#b', 'a', '#b' ),
+ 'escaped hash #\\23 hash' => array( '#\\23 hash', '#hash', '' ),
+ 'unicode ID #café' => array( '#café', 'café', '' ),
+ 'hyphen ID #my-id' => array( '#my-id', 'my-id', '' ),
+ 'underscore ID #my_id' => array( '#my_id', 'my_id', '' ),
+ 'long ID name' => array( '#very-long-id-name-with-many-hyphens', 'very-long-id-name-with-many-hyphens', '' ),
+
// Invalid
'not ID foo' => array( 'foo' ),
'not ID .bar' => array( '.bar' ),
'not valid #1foo' => array( '#1foo' ),
+ 'empty after hash' => array( '#' ),
+ 'space after hash' => array( '# ' ),
+ 'invalid after hash' => array( '#@invalid' ),
);
}
}
diff --git a/tests/phpunit/tests/html-api/wpCssSelectorParserMatcher.php b/tests/phpunit/tests/html-api/wpCssSelectorParserMatcher.php
index 29372172da2b1..7d83befb92703 100644
--- a/tests/phpunit/tests/html-api/wpCssSelectorParserMatcher.php
+++ b/tests/phpunit/tests/html-api/wpCssSelectorParserMatcher.php
@@ -84,6 +84,19 @@ public static function data_idents(): array {
'can start with --\31 23' => array( '--\31 23', '--123', '' ),
'ident ends before ]' => array( 'ident]', 'ident', ']' ),
+ // Unicode normalization tests
+ 'combining-chars' => array( 'café', 'café', '' ),
+ 'high-codepoint' => array( '🌟element', '🌟element', '' ),
+ 'rtl-text' => array( 'العربية', 'العربية', '' ),
+ 'cyrillic' => array( 'элемент', 'элемент', '' ),
+ 'chinese' => array( '元素', '元素', '' ),
+ 'zero-width-joiner' => array( '👨💻', '👨💻', '' ),
+ 'mixed-scripts' => array( 'element元素', 'element元素', '' ),
+ 'private-use-area' => array( "\u{E000}test", "\u{E000}test", '' ),
+ 'surrogate-pair' => array( '𝒜', '𝒜', '' ),
+ 'normalization-nfc' => array( 'é', 'é', '' ), // pre-composed
+ 'normalization-nfd' => array( "e\u{0301}", "e\u{0301}", '' ), // decomposed
+
// Invalid
'Invalid: (empty string)' => array( '' ),
'Invalid: bad start >' => array( '>ident' ),
diff --git a/tests/phpunit/tests/html-api/wpCssTypeSelector.php b/tests/phpunit/tests/html-api/wpCssTypeSelector.php
index 23d5f5517453a..14a6e6d85cfc4 100644
--- a/tests/phpunit/tests/html-api/wpCssTypeSelector.php
+++ b/tests/phpunit/tests/html-api/wpCssTypeSelector.php
@@ -41,11 +41,27 @@ public static function data_type_selectors(): array {
'div.class' => array( 'div.class', 'div', '.class' ),
'custom-type#id' => array( 'custom-type#id', 'custom-type', '#id' ),
+ // Edge cases
+ 'hyphenated-element' => array( 'my-element', 'my-element', '' ),
+ 'underscored_element' => array( 'my_element', 'my_element', '' ),
+ 'CamelCase' => array( 'MyElement', 'MyElement', '' ),
+ 'single-char' => array( 'a', 'a', '' ),
+ 'numbers' => array( 'h1', 'h1', '' ),
+ 'with-space' => array( 'div ', 'div', ' ' ),
+ 'with-tab' => array( "div\t", 'div', "\t" ),
+ 'with-newline' => array( "div\n", 'div', "\n" ),
+ 'unicode-element' => array( 'é', 'é', '' ),
+
// Invalid
'Invalid: (empty string)' => array( '' ),
'Invalid: #id' => array( '#id' ),
'Invalid: .class' => array( '.class' ),
'Invalid: [attr]' => array( '[attr]' ),
+ 'Invalid: starts-digit' => array( '1div' ),
+ 'Invalid: special-char' => array( '@element' ),
+ 'Invalid: whitespace' => array( ' div' ),
+ 'Invalid: combinator' => array( '>' ),
+ 'Invalid: comma' => array( ',' ),
);
}
}