Skip to content

Html api/additional css selector tests #12

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: html-api/add-css-selector-parser
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
26 changes: 26 additions & 0 deletions tests/phpunit/tests/html-api/wpCssAttributeSelector.php
Original file line number Diff line number Diff line change
Expand Up @@ -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]' ),
);
}
}
11 changes: 11 additions & 0 deletions tests/phpunit/tests/html-api/wpCssClassSelector.php
Original file line number Diff line number Diff line change
Expand Up @@ -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' ),
);
}
}
70 changes: 70 additions & 0 deletions tests/phpunit/tests/html-api/wpCssComplexSelector.php
Original file line number Diff line number Diff line change
Expand Up @@ -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' );
}
}
45 changes: 45 additions & 0 deletions tests/phpunit/tests/html-api/wpCssComplexSelectorList.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 );
}
}
129 changes: 129 additions & 0 deletions tests/phpunit/tests/html-api/wpCssCompoundSelector.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 ),
);
}
}
54 changes: 54 additions & 0 deletions tests/phpunit/tests/html-api/wpCssCompoundSelectorList.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 );
}
}
11 changes: 11 additions & 0 deletions tests/phpunit/tests/html-api/wpCssIdSelector.php
Original file line number Diff line number Diff line change
Expand Up @@ -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' ),
);
}
}
Loading