4 * This file is part of the Mink package.
5 * (c) Konstantin Kudryashov <ever.zet@gmail.com>
7 * For the full copyright and license information, please view the LICENSE
8 * file that was distributed with this source code.
11 namespace Behat\Mink\Selector;
13 use Behat\Mink\Selector\Xpath\Escaper;
16 * Named selectors engine. Uses registered XPath selectors to create new expressions.
18 * @author Konstantin Kudryashov <ever.zet@gmail.com>
20 class NamedSelector implements SelectorInterface
22 private $replacements = array(
23 // simple replacements
24 '%lowercaseType%' => "translate(./@type, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')",
25 '%lowercaseRole%' => "translate(./@role, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')",
26 '%tagTextMatch%' => 'contains(normalize-space(string(.)), %locator%)',
27 '%labelTextMatch%' => './@id = //label[%tagTextMatch%]/@for',
28 '%idMatch%' => './@id = %locator%',
29 '%valueMatch%' => 'contains(./@value, %locator%)',
30 '%idOrValueMatch%' => '(%idMatch% or %valueMatch%)',
31 '%idOrNameMatch%' => '(%idMatch% or ./@name = %locator%)',
32 '%placeholderMatch%' => './@placeholder = %locator%',
33 '%titleMatch%' => 'contains(./@title, %locator%)',
34 '%altMatch%' => 'contains(./@alt, %locator%)',
35 '%relMatch%' => 'contains(./@rel, %locator%)',
36 '%labelAttributeMatch%' => 'contains(./@label, %locator%)',
38 // complex replacements
39 '%inputTypeWithoutPlaceholderFilter%' => "%lowercaseType% = 'radio' or %lowercaseType% = 'checkbox' or %lowercaseType% = 'file'",
40 '%fieldFilterWithPlaceholder%' => 'self::input[not(%inputTypeWithoutPlaceholderFilter%)] | self::textarea',
41 '%fieldMatchWithPlaceholder%' => '(%idOrNameMatch% or %labelTextMatch% or %placeholderMatch%)',
42 '%fieldMatchWithoutPlaceholder%' => '(%idOrNameMatch% or %labelTextMatch%)',
43 '%fieldFilterWithoutPlaceholder%' => 'self::input[%inputTypeWithoutPlaceholderFilter%] | self::select',
44 '%buttonTypeFilter%' => "%lowercaseType% = 'submit' or %lowercaseType% = 'image' or %lowercaseType% = 'button' or %lowercaseType% = 'reset'",
45 '%notFieldTypeFilter%' => "not(%buttonTypeFilter% or %lowercaseType% = 'hidden')",
46 '%buttonMatch%' => '%idOrNameMatch% or %valueMatch% or %titleMatch%',
47 '%linkMatch%' => '(%idMatch% or %tagTextMatch% or %titleMatch% or %relMatch%)',
48 '%imgAltMatch%' => './/img[%altMatch%]',
51 private $selectors = array(
52 'fieldset' => <<<XPATH
54 [(%idMatch% or .//legend[%tagTextMatch%])]
59 [%fieldFilterWithPlaceholder%][%notFieldTypeFilter%][%fieldMatchWithPlaceholder%]
61 .//label[%tagTextMatch%]//.//*[%fieldFilterWithPlaceholder%][%notFieldTypeFilter%]
64 [%fieldFilterWithoutPlaceholder%][%notFieldTypeFilter%][%fieldMatchWithoutPlaceholder%]
66 .//label[%tagTextMatch%]//.//*[%fieldFilterWithoutPlaceholder%][%notFieldTypeFilter%]
71 [./@href][(%linkMatch% or %imgAltMatch%)]
74 [%lowercaseRole% = 'link'][(%idOrValueMatch% or %titleMatch% or %tagTextMatch%)]
79 [%buttonTypeFilter%][(%buttonMatch%)]
82 [%lowercaseType% = 'image'][%altMatch%]
85 [(%buttonMatch% or %tagTextMatch%)]
88 [%lowercaseRole% = 'button'][(%buttonMatch% or %tagTextMatch%)]
91 ,'link_or_button' => <<<XPATH
93 [./@href][(%linkMatch% or %imgAltMatch%)]
96 [%buttonTypeFilter%][(%idOrValueMatch% or %titleMatch%)]
99 [%lowercaseType% = 'image'][%altMatch%]
102 [(%idOrValueMatch% or %titleMatch% or %tagTextMatch%)]
105 [(%lowercaseRole% = 'button' or %lowercaseRole% = 'link')][(%idOrValueMatch% or %titleMatch% or %tagTextMatch%)]
108 ,'content' => <<<XPATH
109 ./descendant-or-self::*
113 ,'select' => <<<XPATH
115 [%fieldMatchWithoutPlaceholder%]
117 .//label[%tagTextMatch%]//.//select
120 ,'checkbox' => <<<XPATH
122 [%lowercaseType% = 'checkbox'][%fieldMatchWithoutPlaceholder%]
124 .//label[%tagTextMatch%]//.//input[%lowercaseType% = 'checkbox']
129 [%lowercaseType% = 'radio'][%fieldMatchWithoutPlaceholder%]
131 .//label[%tagTextMatch%]//.//input[%lowercaseType% = 'radio']
136 [%lowercaseType% = 'file'][%fieldMatchWithoutPlaceholder%]
138 .//label[%tagTextMatch%]//.//input[%lowercaseType% = 'file']
141 ,'optgroup' => <<<XPATH
143 [%labelAttributeMatch%]
146 ,'option' => <<<XPATH
148 [(./@value = %locator% or %tagTextMatch%)]
153 [(%idMatch% or .//caption[%tagTextMatch%])]
158 ,'id_or_name' => <<<XPATH
159 .//*[%idOrNameMatch%]
162 private $xpathEscaper;
165 * Creates selector instance.
167 public function __construct()
169 $this->xpathEscaper = new Escaper();
171 foreach ($this->replacements as $from => $to) {
172 $this->replacements[$from] = strtr($to, $this->replacements);
175 foreach ($this->selectors as $alias => $selector) {
176 $this->selectors[$alias] = strtr($selector, $this->replacements);
181 * Registers new XPath selector with specified name.
183 * @param string $name name for selector
184 * @param string $xpath xpath expression
186 public function registerNamedXpath($name, $xpath)
188 $this->selectors[$name] = $xpath;
192 * Translates provided locator into XPath.
194 * @param string|array $locator selector name or array of (selector_name, locator)
198 * @throws \InvalidArgumentException
200 public function translateToXPath($locator)
202 if (2 < count($locator)) {
203 throw new \InvalidArgumentException('NamedSelector expects array(name, locator) as argument');
206 if (2 == count($locator)) {
207 $selector = $locator[0];
208 $locator = $locator[1];
210 $selector = (string) $locator;
214 if (!isset($this->selectors[$selector])) {
215 throw new \InvalidArgumentException(sprintf(
216 'Unknown named selector provided: "%s". Expected one of (%s)',
218 implode(', ', array_keys($this->selectors))
222 $xpath = $this->selectors[$selector];
224 if (null !== $locator) {
225 $xpath = strtr($xpath, array('%locator%' => $this->escapeLocator($locator)));
232 * Registers a replacement in the list of replacements.
234 * This method must be called in the constructor before calling the parent constructor.
236 * @param string $from
239 protected function registerReplacement($from, $to)
241 $this->replacements[$from] = $to;
244 private function escapeLocator($locator)
246 // If the locator looks like an escaped one, don't escape it again for BC reasons.
248 preg_match('/^\'[^\']*+\'$/', $locator)
249 || (false !== strpos($locator, '\'') && preg_match('/^"[^"]*+"$/', $locator))
250 || ((8 < $length = strlen($locator)) && 'concat(' === substr($locator, 0, 7) && ')' === $locator[$length - 1])
253 'Passing an escaped locator to the named selector is deprecated as of 1.7 and will be removed in 2.0.'
254 .' Pass the raw value instead.',
261 return $this->xpathEscaper->escapeLiteral($locator);