<?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");
}
}
|