From 733cb50da909f96397271abbf1504046d359525c Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Tue, 15 Jul 2025 18:25:12 +0200 Subject: [PATCH] Add additional tests --- .../tests/html-api/wpCssAttributeSelector.php | 26 ++++ .../tests/html-api/wpCssClassSelector.php | 11 ++ .../tests/html-api/wpCssComplexSelector.php | 70 ++++++++++ .../html-api/wpCssComplexSelectorList.php | 45 ++++++ .../tests/html-api/wpCssCompoundSelector.php | 129 ++++++++++++++++++ .../html-api/wpCssCompoundSelectorList.php | 54 ++++++++ .../tests/html-api/wpCssIdSelector.php | 11 ++ .../html-api/wpCssSelectorParserMatcher.php | 13 ++ .../tests/html-api/wpCssTypeSelector.php | 16 +++ 9 files changed, 375 insertions(+) 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( ',' ), ); } }