PageRenderTime 50ms CodeModel.GetById 22ms RepoModel.GetById 0ms app.codeStats 1ms

/src/Codeception/Util/Locator.php

http://github.com/Codeception/Codeception
PHP | 400 lines | 140 code | 17 blank | 243 comment | 15 complexity | 51a7fe7e6e6374b2460d95c2e85f8099 MD5 | raw file
  1. <?php
  2. namespace Codeception\Util;
  3. use Facebook\WebDriver\WebDriverBy;
  4. use Symfony\Component\CssSelector\CssSelectorConverter;
  5. use Symfony\Component\CssSelector\Exception\ParseException;
  6. use Symfony\Component\CssSelector\XPath\Translator;
  7. /**
  8. * Set of useful functions for using CSS and XPath locators.
  9. * Please check them before writing complex functional or acceptance tests.
  10. *
  11. */
  12. class Locator
  13. {
  14. /**
  15. * Applies OR operator to any number of CSS or XPath selectors.
  16. * You can mix up CSS and XPath selectors here.
  17. *
  18. * ```php
  19. * <?php
  20. * use \Codeception\Util\Locator;
  21. *
  22. * $I->see('Title', Locator::combine('h1','h2','h3'));
  23. * ?>
  24. * ```
  25. *
  26. * This will search for `Title` text in either `h1`, `h2`, or `h3` tag.
  27. * You can also combine CSS selector with XPath locator:
  28. *
  29. * ```php
  30. * <?php
  31. * use \Codeception\Util\Locator;
  32. *
  33. * $I->fillField(Locator::combine('form input[type=text]','//form/textarea[2]'), 'qwerty');
  34. * ?>
  35. * ```
  36. *
  37. * As a result the Locator will produce a mixed XPath value that will be used in fillField action.
  38. *
  39. * @static
  40. *
  41. * @param $selector1
  42. * @param $selector2
  43. *
  44. * @throws \Exception
  45. *
  46. * @return string
  47. */
  48. public static function combine($selector1, $selector2)
  49. {
  50. $selectors = func_get_args();
  51. foreach ($selectors as $k => $v) {
  52. $selectors[$k] = self::toXPath($v);
  53. if (!$selectors[$k]) {
  54. throw new \Exception("$v is invalid CSS or XPath");
  55. }
  56. }
  57. return implode(' | ', $selectors);
  58. }
  59. /**
  60. * Matches the *a* element with given URL
  61. *
  62. * ```php
  63. * <?php
  64. * use \Codeception\Util\Locator;
  65. *
  66. * $I->see('Log In', Locator::href('/login.php'));
  67. * ?>
  68. * ```
  69. *
  70. * @static
  71. *
  72. * @param $url
  73. *
  74. * @return string
  75. */
  76. public static function href($url)
  77. {
  78. return sprintf('//a[@href=normalize-space(%s)]', Translator::getXpathLiteral($url));
  79. }
  80. /**
  81. * Matches the element with given tab index
  82. *
  83. * Do you often use the `TAB` key to navigate through the web page? How do your site respond to this navigation?
  84. * You could try to match elements by their tab position using `tabIndex` method of `Locator` class.
  85. * ```php
  86. * <?php
  87. * use \Codeception\Util\Locator;
  88. *
  89. * $I->fillField(Locator::tabIndex(1), 'davert');
  90. * $I->fillField(Locator::tabIndex(2) , 'qwerty');
  91. * $I->click('Login');
  92. * ?>
  93. * ```
  94. *
  95. * @static
  96. *
  97. * @param $index
  98. *
  99. * @return string
  100. */
  101. public static function tabIndex($index)
  102. {
  103. return sprintf('//*[@tabindex = normalize-space(%d)]', $index);
  104. }
  105. /**
  106. * Matches option by text:
  107. *
  108. * ```php
  109. * <?php
  110. * use Codeception\Util\Locator;
  111. *
  112. * $I->seeElement(Locator::option('Male'), '#select-gender');
  113. * ```
  114. *
  115. * @param $value
  116. *
  117. * @return string
  118. */
  119. public static function option($value)
  120. {
  121. return sprintf('//option[.=normalize-space("%s")]', $value);
  122. }
  123. protected static function toXPath($selector)
  124. {
  125. try {
  126. $xpath = (new CssSelectorConverter())->toXPath($selector);
  127. return $xpath;
  128. } catch (ParseException $e) {
  129. if (self::isXPath($selector)) {
  130. return $selector;
  131. }
  132. }
  133. return null;
  134. }
  135. /**
  136. * Finds element by it's attribute(s)
  137. *
  138. * ```php
  139. * <?php
  140. * use \Codeception\Util\Locator;
  141. *
  142. * $I->seeElement(Locator::find('img', ['title' => 'diagram']));
  143. * ```
  144. *
  145. * @static
  146. *
  147. * @param $element
  148. * @param $attributes
  149. *
  150. * @return string
  151. */
  152. public static function find($element, array $attributes)
  153. {
  154. $operands = [];
  155. foreach ($attributes as $attribute => $value) {
  156. if (is_int($attribute)) {
  157. $operands[] = '@' . $value;
  158. } else {
  159. $operands[] = '@' . $attribute . ' = ' . Translator::getXpathLiteral($value);
  160. }
  161. }
  162. return sprintf('//%s[%s]', $element, implode(' and ', $operands));
  163. }
  164. /**
  165. * Checks that provided string is CSS selector
  166. *
  167. * ```php
  168. * <?php
  169. * Locator::isCSS('#user .hello') => true
  170. * Locator::isCSS('body') => true
  171. * Locator::isCSS('//body/p/user') => false
  172. * ```
  173. *
  174. * @param $selector
  175. *
  176. * @return bool
  177. */
  178. public static function isCSS($selector)
  179. {
  180. try {
  181. (new CssSelectorConverter())->toXPath($selector);
  182. } catch (ParseException $e) {
  183. return false;
  184. }
  185. return true;
  186. }
  187. /**
  188. * Checks that locator is an XPath
  189. *
  190. * ```php
  191. * <?php
  192. * Locator::isXPath('#user .hello') => false
  193. * Locator::isXPath('body') => false
  194. * Locator::isXPath('//body/p/user') => true
  195. * ```
  196. *
  197. * @param $locator
  198. *
  199. * @return bool
  200. */
  201. public static function isXPath($locator)
  202. {
  203. $document = new \DOMDocument('1.0', 'UTF-8');
  204. $xpath = new \DOMXPath($document);
  205. return @$xpath->evaluate($locator, $document) !== false;
  206. }
  207. /**
  208. * @param $locator
  209. * @return bool
  210. */
  211. public static function isPrecise($locator)
  212. {
  213. if (is_array($locator)) {
  214. return true;
  215. }
  216. if ($locator instanceof WebDriverBy) {
  217. return true;
  218. }
  219. if (Locator::isID($locator)) {
  220. return true;
  221. }
  222. if (strpos($locator, '//') === 0) {
  223. return true; // simple xpath check
  224. }
  225. return false;
  226. }
  227. /**
  228. * Checks that a string is valid CSS ID
  229. *
  230. * ```php
  231. * <?php
  232. * Locator::isID('#user') => true
  233. * Locator::isID('body') => false
  234. * Locator::isID('//body/p/user') => false
  235. * ```
  236. *
  237. * @param $id
  238. *
  239. * @return bool
  240. */
  241. public static function isID($id)
  242. {
  243. return (bool)preg_match('~^#[\w\.\-\[\]\=\^\~\:]+$~', $id);
  244. }
  245. /**
  246. * Checks that a string is valid CSS class
  247. *
  248. * ```php
  249. * <?php
  250. * Locator::isClass('.hello') => true
  251. * Locator::isClass('body') => false
  252. * Locator::isClass('//body/p/user') => false
  253. * ```
  254. *
  255. * @param $class
  256. * @return bool
  257. */
  258. public static function isClass($class)
  259. {
  260. return (bool)preg_match('~^\.[\w\.\-\[\]\=\^\~\:]+$~', $class);
  261. }
  262. /**
  263. * Locates an element containing a text inside.
  264. * Either CSS or XPath locator can be passed, however they will be converted to XPath.
  265. *
  266. * ```php
  267. * <?php
  268. * use Codeception\Util\Locator;
  269. *
  270. * Locator::contains('label', 'Name'); // label containing name
  271. * Locator::contains('div[@contenteditable=true]', 'hello world');
  272. * ```
  273. *
  274. * @param $element
  275. * @param $text
  276. *
  277. * @return string
  278. */
  279. public static function contains($element, $text)
  280. {
  281. $text = Translator::getXpathLiteral($text);
  282. return sprintf('%s[%s]', self::toXPath($element), "contains(., $text)");
  283. }
  284. /**
  285. * Locates element at position.
  286. * Either CSS or XPath locator can be passed as locator,
  287. * position is an integer. If a negative value is provided, counting starts from the last element.
  288. * First element has index 1
  289. *
  290. * ```php
  291. * <?php
  292. * use Codeception\Util\Locator;
  293. *
  294. * Locator::elementAt('//table/tr', 2); // second row
  295. * Locator::elementAt('//table/tr', -1); // last row
  296. * Locator::elementAt('table#grind>tr', -2); // previous than last row
  297. * ```
  298. *
  299. * @param string $element CSS or XPath locator
  300. * @param int $position xpath index
  301. *
  302. * @return mixed
  303. */
  304. public static function elementAt($element, $position)
  305. {
  306. if (is_int($position) && $position < 0) {
  307. $position++; // -1 points to the last element
  308. $position = 'last()-'.abs($position);
  309. }
  310. if ($position === 0) {
  311. throw new \InvalidArgumentException(
  312. '0 is not valid element position. XPath expects first element to have index 1'
  313. );
  314. }
  315. return sprintf('(%s)[position()=%s]', self::toXPath($element), $position);
  316. }
  317. /**
  318. * Locates first element of group elements.
  319. * Either CSS or XPath locator can be passed as locator,
  320. * Equal to `Locator::elementAt($locator, 1)`
  321. *
  322. * ```php
  323. * <?php
  324. * use Codeception\Util\Locator;
  325. *
  326. * Locator::firstElement('//table/tr');
  327. * ```
  328. *
  329. * @param $element
  330. *
  331. * @return mixed
  332. */
  333. public static function firstElement($element)
  334. {
  335. return self::elementAt($element, 1);
  336. }
  337. /**
  338. * Locates last element of group elements.
  339. * Either CSS or XPath locator can be passed as locator,
  340. * Equal to `Locator::elementAt($locator, -1)`
  341. *
  342. * ```php
  343. * <?php
  344. * use Codeception\Util\Locator;
  345. *
  346. * Locator::lastElement('//table/tr');
  347. * ```
  348. *
  349. * @param $element
  350. *
  351. * @return mixed
  352. */
  353. public static function lastElement($element)
  354. {
  355. return self::elementAt($element, 'last()');
  356. }
  357. /**
  358. * Transforms strict locator, \Facebook\WebDriver\WebDriverBy into a string represenation
  359. *
  360. * @param $selector
  361. *
  362. * @return string
  363. */
  364. public static function humanReadableString($selector)
  365. {
  366. if (is_string($selector)) {
  367. return "'$selector'";
  368. }
  369. if (is_array($selector)) {
  370. $type = strtolower(key($selector));
  371. $locator = $selector[$type];
  372. return "$type '$locator'";
  373. }
  374. if (class_exists('\Facebook\WebDriver\WebDriverBy')) {
  375. if ($selector instanceof WebDriverBy) {
  376. $type = $selector->getMechanism();
  377. $locator = $selector->getValue();
  378. return "$type '$locator'";
  379. }
  380. }
  381. throw new \InvalidArgumentException("Unrecognized selector");
  382. }
  383. }