<?php namespace Codeception\Util; use Facebook\WebDriver\WebDriverBy; use Symfony\Component\CssSelector\CssSelectorConverter; use Symfony\Component\CssSelector\Exception\ParseException; use Symfony\Component\CssSelector\XPath\Translator; /** * Set of useful functions for using CSS and XPath locators. * Please check them before writing complex functional or acceptance tests. * */ class Locator { /** * Applies OR operator to any number of CSS or XPath selectors. * You can mix up CSS and XPath selectors here. * * ```php * <?php * use \Codeception\Util\Locator; * * $I->see('Title', Locator::combine('h1','h2','h3')); * ?> * ``` * * This will search for `Title` text in either `h1`, `h2`, or `h3` tag. * You can also combine CSS selector with XPath locator: * * ```php * <?php * use \Codeception\Util\Locator; * * $I->fillField(Locator::combine('form input[type=text]','//form/textarea[2]'), 'qwerty'); * ?> * ``` * * As a result the Locator will produce a mixed XPath value that will be used in fillField action. * * @static * * @param $selector1 * @param $selector2 * * @throws \Exception * * @return string */ public static function combine($selector1, $selector2) { $selectors = func_get_args(); foreach ($selectors as $k => $v) { $selectors[$k] = self::toXPath($v); if (!$selectors[$k]) { throw new \Exception("$v is invalid CSS or XPath"); } } return implode(' | ', $selectors); } /** * Matches the *a* element with given URL * * ```php * <?php * use \Codeception\Util\Locator; * * $I->see('Log In', Locator::href('/login.php')); * ?> * ``` * * @static * * @param $url * * @return string */ public static function href($url) { return sprintf('//a[@href=normalize-space(%s)]', Translator::getXpathLiteral($url)); } /** * Matches the element with given tab index * * Do you often use the `TAB` key to navigate through the web page? How do your site respond to this navigation? * You could try to match elements by their tab position using `tabIndex` method of `Locator` class. * ```php * <?php * use \Codeception\Util\Locator; * * $I->fillField(Locator::tabIndex(1), 'davert'); * $I->fillField(Locator::tabIndex(2) , 'qwerty'); * $I->click('Login'); * ?> * ``` * * @static * * @param $index * * @return string */ public static function tabIndex($index) { return sprintf('//*[@tabindex = normalize-space(%d)]', $index); } /** * Matches option by text: * * ```php * <?php * use Codeception\Util\Locator; * * $I->seeElement(Locator::option('Male'), '#select-gender'); * ``` * * @param $value * * @return string */ public static function option($value) { return sprintf('//option[.=normalize-space("%s")]', $value); } protected static function toXPath($selector) { try { $xpath = (new CssSelectorConverter())->toXPath($selector); return $xpath; } catch (ParseException $e) { if (self::isXPath($selector)) { return $selector; } } return null; } /** * Finds element by it's attribute(s) * * ```php * <?php * use \Codeception\Util\Locator; * * $I->seeElement(Locator::find('img', ['title' => 'diagram'])); * ``` * * @static * * @param $element * @param $attributes * * @return string */ public static function find($element, array $attributes) { $operands = []; foreach ($attributes as $attribute => $value) { if (is_int($attribute)) { $operands[] = '@' . $value; } else { $operands[] = '@' . $attribute . ' = ' . Translator::getXpathLiteral($value); } } return sprintf('//%s[%s]', $element, implode(' and ', $operands)); } /** * Checks that provided string is CSS selector * * ```php * <?php * Locator::isCSS('#user .hello') => true * Locator::isCSS('body') => true * Locator::isCSS('//body/p/user') => false * ``` * * @param $selector * * @return bool */ public static function isCSS($selector) { try { (new CssSelectorConverter())->toXPath($selector); } catch (ParseException $e) { return false; } return true; } /** * Checks that locator is an XPath * * ```php * <?php * Locator::isXPath('#user .hello') => false * Locator::isXPath('body') => false * Locator::isXPath('//body/p/user') => true * ``` * * @param $locator * * @return bool */ public static function isXPath($locator) { $document = new \DOMDocument('1.0', 'UTF-8'); $xpath = new \DOMXPath($document); return @$xpath->evaluate($locator, $document) !== false; } /** * @param $locator * @return bool */ public static function isPrecise($locator) { if (is_array($locator)) { return true; } if ($locator instanceof WebDriverBy) { return true; } if (Locator::isID($locator)) { return true; } if (strpos($locator, '//') === 0) { return true; // simple xpath check } return false; } /** * Checks that a string is valid CSS ID * * ```php * <?php * Locator::isID('#user') => true * Locator::isID('body') => false * Locator::isID('//body/p/user') => false * ``` * * @param $id * * @return bool */ public static function isID($id) { return (bool)preg_match('~^#[\w\.\-\[\]\=\^\~\:]+$~', $id); } /** * Checks that a string is valid CSS class * * ```php * <?php * Locator::isClass('.hello') => true * Locator::isClass('body') => false * Locator::isClass('//body/p/user') => false * ``` * * @param $class * @return bool */ public static function isClass($class) { return (bool)preg_match('~^\.[\w\.\-\[\]\=\^\~\:]+$~', $class); } /** * Locates an element containing a text inside. * Either CSS or XPath locator can be passed, however they will be converted to XPath. * * ```php * <?php * use Codeception\Util\Locator; * * Locator::contains('label', 'Name'); // label containing name * Locator::contains('div[@contenteditable=true]', 'hello world'); * ``` * * @param $element * @param $text * * @return string */ public static function contains($element, $text) { $text = Translator::getXpathLiteral($text); return sprintf('%s[%s]', self::toXPath($element), "contains(., $text)"); } /** * Locates element at position. * Either CSS or XPath locator can be passed as locator, * position is an integer. If a negative value is provided, counting starts from the last element. * First element has index 1 * * ```php * <?php * use Codeception\Util\Locator; * * Locator::elementAt('//table/tr', 2); // second row * Locator::elementAt('//table/tr', -1); // last row * Locator::elementAt('table#grind>tr', -2); // previous than last row * ``` * * @param string $element CSS or XPath locator * @param int $position xpath index * * @return mixed */ public static function elementAt($element, $position) { if (is_int($position) && $position < 0) { $position++; // -1 points to the last element $position = 'last()-'.abs($position); } if ($position === 0) { throw new \InvalidArgumentException( '0 is not valid element position. XPath expects first element to have index 1' ); } return sprintf('(%s)[position()=%s]', self::toXPath($element), $position); } /** * Locates first element of group elements. * Either CSS or XPath locator can be passed as locator, * Equal to `Locator::elementAt($locator, 1)` * * ```php * <?php * use Codeception\Util\Locator; * * Locator::firstElement('//table/tr'); * ``` * * @param $element * * @return mixed */ public static function firstElement($element) { return self::elementAt($element, 1); } /** * Locates last element of group elements. * Either CSS or XPath locator can be passed as locator, * Equal to `Locator::elementAt($locator, -1)` * * ```php * <?php * use Codeception\Util\Locator; * * Locator::lastElement('//table/tr'); * ``` * * @param $element * * @return mixed */ public static function lastElement($element) { return self::elementAt($element, 'last()'); } /** * Transforms strict locator, \Facebook\WebDriver\WebDriverBy into a string represenation * * @param $selector * * @return string */ public static function humanReadableString($selector) { if (is_string($selector)) { return "'$selector'"; } if (is_array($selector)) { $type = strtolower(key($selector)); $locator = $selector[$type]; return "$type '$locator'"; } if (class_exists('\Facebook\WebDriver\WebDriverBy')) { if ($selector instanceof WebDriverBy) { $type = $selector->getMechanism(); $locator = $selector->getValue(); return "$type '$locator'"; } } throw new \InvalidArgumentException("Unrecognized selector"); } }