NamedSelector.php 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263
  1. <?php
  2. /*
  3. * This file is part of the Mink package.
  4. * (c) Konstantin Kudryashov <ever.zet@gmail.com>
  5. *
  6. * For the full copyright and license information, please view the LICENSE
  7. * file that was distributed with this source code.
  8. */
  9. namespace Behat\Mink\Selector;
  10. use Behat\Mink\Selector\Xpath\Escaper;
  11. /**
  12. * Named selectors engine. Uses registered XPath selectors to create new expressions.
  13. *
  14. * @author Konstantin Kudryashov <ever.zet@gmail.com>
  15. */
  16. class NamedSelector implements SelectorInterface
  17. {
  18. private $replacements = array(
  19. // simple replacements
  20. '%lowercaseType%' => "translate(./@type, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')",
  21. '%lowercaseRole%' => "translate(./@role, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')",
  22. '%tagTextMatch%' => 'contains(normalize-space(string(.)), %locator%)',
  23. '%labelTextMatch%' => './@id = //label[%tagTextMatch%]/@for',
  24. '%idMatch%' => './@id = %locator%',
  25. '%valueMatch%' => 'contains(./@value, %locator%)',
  26. '%idOrValueMatch%' => '(%idMatch% or %valueMatch%)',
  27. '%idOrNameMatch%' => '(%idMatch% or ./@name = %locator%)',
  28. '%placeholderMatch%' => './@placeholder = %locator%',
  29. '%titleMatch%' => 'contains(./@title, %locator%)',
  30. '%altMatch%' => 'contains(./@alt, %locator%)',
  31. '%relMatch%' => 'contains(./@rel, %locator%)',
  32. '%labelAttributeMatch%' => 'contains(./@label, %locator%)',
  33. // complex replacements
  34. '%inputTypeWithoutPlaceholderFilter%' => "%lowercaseType% = 'radio' or %lowercaseType% = 'checkbox' or %lowercaseType% = 'file'",
  35. '%fieldFilterWithPlaceholder%' => 'self::input[not(%inputTypeWithoutPlaceholderFilter%)] | self::textarea',
  36. '%fieldMatchWithPlaceholder%' => '(%idOrNameMatch% or %labelTextMatch% or %placeholderMatch%)',
  37. '%fieldMatchWithoutPlaceholder%' => '(%idOrNameMatch% or %labelTextMatch%)',
  38. '%fieldFilterWithoutPlaceholder%' => 'self::input[%inputTypeWithoutPlaceholderFilter%] | self::select',
  39. '%buttonTypeFilter%' => "%lowercaseType% = 'submit' or %lowercaseType% = 'image' or %lowercaseType% = 'button' or %lowercaseType% = 'reset'",
  40. '%notFieldTypeFilter%' => "not(%buttonTypeFilter% or %lowercaseType% = 'hidden')",
  41. '%buttonMatch%' => '%idOrNameMatch% or %valueMatch% or %titleMatch%',
  42. '%linkMatch%' => '(%idMatch% or %tagTextMatch% or %titleMatch% or %relMatch%)',
  43. '%imgAltMatch%' => './/img[%altMatch%]',
  44. );
  45. private $selectors = array(
  46. 'fieldset' => <<<XPATH
  47. .//fieldset
  48. [(%idMatch% or .//legend[%tagTextMatch%])]
  49. XPATH
  50. ,'field' => <<<XPATH
  51. .//*
  52. [%fieldFilterWithPlaceholder%][%notFieldTypeFilter%][%fieldMatchWithPlaceholder%]
  53. |
  54. .//label[%tagTextMatch%]//.//*[%fieldFilterWithPlaceholder%][%notFieldTypeFilter%]
  55. |
  56. .//*
  57. [%fieldFilterWithoutPlaceholder%][%notFieldTypeFilter%][%fieldMatchWithoutPlaceholder%]
  58. |
  59. .//label[%tagTextMatch%]//.//*[%fieldFilterWithoutPlaceholder%][%notFieldTypeFilter%]
  60. XPATH
  61. ,'link' => <<<XPATH
  62. .//a
  63. [./@href][(%linkMatch% or %imgAltMatch%)]
  64. |
  65. .//*
  66. [%lowercaseRole% = 'link'][(%idOrValueMatch% or %titleMatch% or %tagTextMatch%)]
  67. XPATH
  68. ,'button' => <<<XPATH
  69. .//input
  70. [%buttonTypeFilter%][(%buttonMatch%)]
  71. |
  72. .//input
  73. [%lowercaseType% = 'image'][%altMatch%]
  74. |
  75. .//button
  76. [(%buttonMatch% or %tagTextMatch%)]
  77. |
  78. .//*
  79. [%lowercaseRole% = 'button'][(%buttonMatch% or %tagTextMatch%)]
  80. XPATH
  81. ,'link_or_button' => <<<XPATH
  82. .//a
  83. [./@href][(%linkMatch% or %imgAltMatch%)]
  84. |
  85. .//input
  86. [%buttonTypeFilter%][(%idOrValueMatch% or %titleMatch%)]
  87. |
  88. .//input
  89. [%lowercaseType% = 'image'][%altMatch%]
  90. |
  91. .//button
  92. [(%idOrValueMatch% or %titleMatch% or %tagTextMatch%)]
  93. |
  94. .//*
  95. [(%lowercaseRole% = 'button' or %lowercaseRole% = 'link')][(%idOrValueMatch% or %titleMatch% or %tagTextMatch%)]
  96. XPATH
  97. ,'content' => <<<XPATH
  98. ./descendant-or-self::*
  99. [%tagTextMatch%]
  100. XPATH
  101. ,'select' => <<<XPATH
  102. .//select
  103. [%fieldMatchWithoutPlaceholder%]
  104. |
  105. .//label[%tagTextMatch%]//.//select
  106. XPATH
  107. ,'checkbox' => <<<XPATH
  108. .//input
  109. [%lowercaseType% = 'checkbox'][%fieldMatchWithoutPlaceholder%]
  110. |
  111. .//label[%tagTextMatch%]//.//input[%lowercaseType% = 'checkbox']
  112. XPATH
  113. ,'radio' => <<<XPATH
  114. .//input
  115. [%lowercaseType% = 'radio'][%fieldMatchWithoutPlaceholder%]
  116. |
  117. .//label[%tagTextMatch%]//.//input[%lowercaseType% = 'radio']
  118. XPATH
  119. ,'file' => <<<XPATH
  120. .//input
  121. [%lowercaseType% = 'file'][%fieldMatchWithoutPlaceholder%]
  122. |
  123. .//label[%tagTextMatch%]//.//input[%lowercaseType% = 'file']
  124. XPATH
  125. ,'optgroup' => <<<XPATH
  126. .//optgroup
  127. [%labelAttributeMatch%]
  128. XPATH
  129. ,'option' => <<<XPATH
  130. .//option
  131. [(./@value = %locator% or %tagTextMatch%)]
  132. XPATH
  133. ,'table' => <<<XPATH
  134. .//table
  135. [(%idMatch% or .//caption[%tagTextMatch%])]
  136. XPATH
  137. ,'id' => <<<XPATH
  138. .//*[%idMatch%]
  139. XPATH
  140. ,'id_or_name' => <<<XPATH
  141. .//*[%idOrNameMatch%]
  142. XPATH
  143. );
  144. private $xpathEscaper;
  145. /**
  146. * Creates selector instance.
  147. */
  148. public function __construct()
  149. {
  150. $this->xpathEscaper = new Escaper();
  151. foreach ($this->replacements as $from => $to) {
  152. $this->replacements[$from] = strtr($to, $this->replacements);
  153. }
  154. foreach ($this->selectors as $alias => $selector) {
  155. $this->selectors[$alias] = strtr($selector, $this->replacements);
  156. }
  157. }
  158. /**
  159. * Registers new XPath selector with specified name.
  160. *
  161. * @param string $name name for selector
  162. * @param string $xpath xpath expression
  163. */
  164. public function registerNamedXpath($name, $xpath)
  165. {
  166. $this->selectors[$name] = $xpath;
  167. }
  168. /**
  169. * Translates provided locator into XPath.
  170. *
  171. * @param string|array $locator selector name or array of (selector_name, locator)
  172. *
  173. * @return string
  174. *
  175. * @throws \InvalidArgumentException
  176. */
  177. public function translateToXPath($locator)
  178. {
  179. if (2 < count($locator)) {
  180. throw new \InvalidArgumentException('NamedSelector expects array(name, locator) as argument');
  181. }
  182. if (2 == count($locator)) {
  183. $selector = $locator[0];
  184. $locator = $locator[1];
  185. } else {
  186. $selector = (string) $locator;
  187. $locator = null;
  188. }
  189. if (!isset($this->selectors[$selector])) {
  190. throw new \InvalidArgumentException(sprintf(
  191. 'Unknown named selector provided: "%s". Expected one of (%s)',
  192. $selector,
  193. implode(', ', array_keys($this->selectors))
  194. ));
  195. }
  196. $xpath = $this->selectors[$selector];
  197. if (null !== $locator) {
  198. $xpath = strtr($xpath, array('%locator%' => $this->escapeLocator($locator)));
  199. }
  200. return $xpath;
  201. }
  202. /**
  203. * Registers a replacement in the list of replacements.
  204. *
  205. * This method must be called in the constructor before calling the parent constructor.
  206. *
  207. * @param string $from
  208. * @param string $to
  209. */
  210. protected function registerReplacement($from, $to)
  211. {
  212. $this->replacements[$from] = $to;
  213. }
  214. private function escapeLocator($locator)
  215. {
  216. // If the locator looks like an escaped one, don't escape it again for BC reasons.
  217. if (
  218. preg_match('/^\'[^\']*+\'$/', $locator)
  219. || (false !== strpos($locator, '\'') && preg_match('/^"[^"]*+"$/', $locator))
  220. || ((8 < $length = strlen($locator)) && 'concat(' === substr($locator, 0, 7) && ')' === $locator[$length - 1])
  221. ) {
  222. @trigger_error(
  223. 'Passing an escaped locator to the named selector is deprecated as of 1.7 and will be removed in 2.0.'
  224. .' Pass the raw value instead.',
  225. E_USER_DEPRECATED
  226. );
  227. return $locator;
  228. }
  229. return $this->xpathEscaper->escapeLiteral($locator);
  230. }
  231. }