Skip to content

Commit

Permalink
Added validation of CSS selectors
Browse files Browse the repository at this point in the history
  • Loading branch information
mnocon committed Jul 21, 2023
1 parent 210c20c commit 331b660
Show file tree
Hide file tree
Showing 4 changed files with 172 additions and 1 deletion.
9 changes: 9 additions & 0 deletions src/lib/Browser/Locator/CSSLocator.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,17 @@

namespace Ibexa\Behat\Browser\Locator;

use Ibexa\Behat\Browser\Locator\Validator\CssLocatorValidator;

class CSSLocator extends BaseLocator
{
public function __construct(string $identifier, string $selector)
{
parent::__construct($identifier, $selector);
$validator = new CssLocatorValidator();
$validator->validate($this);
}

public function getType(): string
{
return 'css';
Expand Down
85 changes: 85 additions & 0 deletions src/lib/Browser/Locator/Validator/CssLocatorValidator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<?php

/**
* @copyright Copyright (C) Ibexa AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
declare(strict_types=1);

namespace Ibexa\Behat\Browser\Locator\Validator;

use Ibexa\Behat\Browser\Locator\CSSLocator;
use Symfony\Component\CssSelector\Exception\ParseException;
use Symfony\Component\CssSelector\Node\CombinedSelectorNode;
use Symfony\Component\CssSelector\Node\ElementNode;
use Symfony\Component\CssSelector\Node\FunctionNode;
use Symfony\Component\CssSelector\Node\SelectorNode;
use Symfony\Component\CssSelector\Parser\Parser;

class CssLocatorValidator
{
private Parser $parser;

private array $partiallySupportedPseudoClasses = ['first-of-type', 'last-of-type', 'nth-of-type', 'nth-last-of-type'];

public function __construct()
{
$this->parser = new Parser();
}

public function validate(CSSLocator $cssLocator): void
{
if (!$this->isValidSelector($cssLocator->getSelector())) {
throw new ParseException(
sprintf(
"Locator '%s' with ID '%s' cannot be used because of limitations of the CssSelector component. See more: https://symfony.com/doc/current/components/css_selector.html#limitations-of-the-cssselector-component",
$cssLocator->getSelector(),
$cssLocator->getIdentifier()
)
);
}
}

private function isValidSelector(string $selector): bool
{
$tokens = $this->parser->parse($selector);

while (!empty($tokens)) {
$token = array_pop($tokens);

if ($token instanceof SelectorNode) {
array_push($tokens, $token->getTree());
}
if ($token instanceof CombinedSelectorNode) {
array_push($tokens, $token->getSelector(), $token->getSubSelector());
}

if ($token instanceof FunctionNode) {
if (!$this->isValidFunctionNode($token)) {
return false;
}
}
}

return true;
}

private function isValidFunctionNode(FunctionNode $node): bool
{
if (!in_array($node->getName(), $this->partiallySupportedPseudoClasses)) {
return true;
}

$selector = $node->getSelector();

if (!($selector instanceof ElementNode)) {
$selector = $selector->getSelector();
}

if ($selector->getElement() === null) {
return false;
}

return true;
}
}
78 changes: 78 additions & 0 deletions tests/Browser/Locator/Validator/CssLocatorValidatorTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?php

/**
* @copyright Copyright (C) Ibexa AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
declare(strict_types=1);

namespace Ibexa\Tests\Behat\Browser\Locator\Validator;

use Ibexa\Behat\Browser\Locator\CSSLocator;
use Ibexa\Behat\Browser\Locator\Validator\CssLocatorValidator;
use PHPUnit\Framework\TestCase;
use Symfony\Component\CssSelector\Exception\ParseException;

class CssLocatorValidatorTest extends TestCase
{
private CssLocatorValidator $validator;

protected function setUp(): void
{
$this->validator = new CssLocatorValidator();
}

/**
* @dataProvider provideValidSelectors
*/
public function testValidationPasses(string $selector): void
{
$this->validator->validate(new CSSLocator('test', $selector));
$this->expectNotToPerformAssertions();
}

/**
* @dataProvider provideInvalidSelectors
*/
public function testValidationFails(string $selector): void
{
$this->expectException(ParseException::class);
$this->expectExceptionMessage(
sprintf(
"Locator '%s' with ID 'test' cannot be used because of limitations of the CssSelector component. See more: https://symfony.com/doc/current/components/css_selector.html#limitations-of-the-cssselector-component",
$selector
)
);
$this->validator->validate(new CSSLocator('test', $selector));
}

public static function provideValidSelectors(): iterable
{
return [
['div'],
['div:nth-of-type(1)'],
['div.ibexa-content-field:nth-of-type(3)'],
['selector1 section:nth-of-type(2)'],
['tr td:nth-of-type(2)'],
['[data-id="test"] div.ibexa-field-edit:nth-of-type(3)'],
['div.ibexa-details__item:nth-of-type(2) .ibexa-details__item-content'],
['.ibexa-version-compare__field-wrapper div.ibexa-content-field:nth-of-type(1) .ibexa-content-field__name'],
['div.ibexa-dropdown__wrapper > ul.ibexa-dropdown__selection-info > li:nth-child(1)'],
['.ibexa-dropdown__item:nth-child(3)'],
['.nav-item:nth-child(2) .nav-link'],
];
}

public static function provideInvalidSelectors(): iterable
{
return [
['.ibexa:nth-of-type(1)'],
['.ibexa-table__cell:nth-of-type(5),td:nth-of-type(5)'],
['.ibexa-available-field-types__list > li:not(.ibexa-available-field-type--hidden) .ibexa-available-field-type__content:nth-of-type(5)'],
['.c-finder-branch:nth-of-type(3) .c-finder-leaf'],
['.selector .ibexa:nth-of-type(2)'],
['.tab-pane.active .ibexa-fieldgroup:nth-of-type(2)'],
['div.ibexa-ca-company-tab-company-profile__top-wrapper .ibexa-details__items .ibexa-details__item:nth-of-type(3) .ibexa-details__item-content'],
];
}
}
1 change: 0 additions & 1 deletion tests/lib/Core/Behat/ExtendedTableNodeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@

/**
* @copyright Copyright (C) eZ Systems AS. All rights reserved.
*
* @license For full copyright and license information view LICENSE file distributed with this source code.
*
* @internal
Expand Down

0 comments on commit 331b660

Please sign in to comment.