Chris@0: Chris@0: * Chris@0: * For the full copyright and license information, please view the LICENSE Chris@0: * file that was distributed with this source code. Chris@0: */ Chris@0: Chris@0: namespace Behat\Mink\Selector; Chris@0: Chris@0: use Behat\Mink\Selector\Xpath\Escaper; Chris@0: Chris@0: /** Chris@0: * Named selectors engine. Uses registered XPath selectors to create new expressions. Chris@0: * Chris@0: * @author Konstantin Kudryashov Chris@0: */ Chris@0: class NamedSelector implements SelectorInterface Chris@0: { Chris@0: private $replacements = array( Chris@0: // simple replacements Chris@0: '%lowercaseType%' => "translate(./@type, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')", Chris@0: '%lowercaseRole%' => "translate(./@role, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')", Chris@0: '%tagTextMatch%' => 'contains(normalize-space(string(.)), %locator%)', Chris@0: '%labelTextMatch%' => './@id = //label[%tagTextMatch%]/@for', Chris@0: '%idMatch%' => './@id = %locator%', Chris@0: '%valueMatch%' => 'contains(./@value, %locator%)', Chris@0: '%idOrValueMatch%' => '(%idMatch% or %valueMatch%)', Chris@0: '%idOrNameMatch%' => '(%idMatch% or ./@name = %locator%)', Chris@0: '%placeholderMatch%' => './@placeholder = %locator%', Chris@0: '%titleMatch%' => 'contains(./@title, %locator%)', Chris@0: '%altMatch%' => 'contains(./@alt, %locator%)', Chris@0: '%relMatch%' => 'contains(./@rel, %locator%)', Chris@0: '%labelAttributeMatch%' => 'contains(./@label, %locator%)', Chris@0: Chris@0: // complex replacements Chris@0: '%inputTypeWithoutPlaceholderFilter%' => "%lowercaseType% = 'radio' or %lowercaseType% = 'checkbox' or %lowercaseType% = 'file'", Chris@0: '%fieldFilterWithPlaceholder%' => 'self::input[not(%inputTypeWithoutPlaceholderFilter%)] | self::textarea', Chris@0: '%fieldMatchWithPlaceholder%' => '(%idOrNameMatch% or %labelTextMatch% or %placeholderMatch%)', Chris@0: '%fieldMatchWithoutPlaceholder%' => '(%idOrNameMatch% or %labelTextMatch%)', Chris@0: '%fieldFilterWithoutPlaceholder%' => 'self::input[%inputTypeWithoutPlaceholderFilter%] | self::select', Chris@0: '%buttonTypeFilter%' => "%lowercaseType% = 'submit' or %lowercaseType% = 'image' or %lowercaseType% = 'button' or %lowercaseType% = 'reset'", Chris@0: '%notFieldTypeFilter%' => "not(%buttonTypeFilter% or %lowercaseType% = 'hidden')", Chris@0: '%buttonMatch%' => '%idOrNameMatch% or %valueMatch% or %titleMatch%', Chris@0: '%linkMatch%' => '(%idMatch% or %tagTextMatch% or %titleMatch% or %relMatch%)', Chris@0: '%imgAltMatch%' => './/img[%altMatch%]', Chris@0: ); Chris@0: Chris@0: private $selectors = array( Chris@0: 'fieldset' => << << << << << << << << << << << << << << <<xpathEscaper = new Escaper(); Chris@0: Chris@0: foreach ($this->replacements as $from => $to) { Chris@0: $this->replacements[$from] = strtr($to, $this->replacements); Chris@0: } Chris@0: Chris@0: foreach ($this->selectors as $alias => $selector) { Chris@0: $this->selectors[$alias] = strtr($selector, $this->replacements); Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Registers new XPath selector with specified name. Chris@0: * Chris@0: * @param string $name name for selector Chris@0: * @param string $xpath xpath expression Chris@0: */ Chris@0: public function registerNamedXpath($name, $xpath) Chris@0: { Chris@0: $this->selectors[$name] = $xpath; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Translates provided locator into XPath. Chris@0: * Chris@0: * @param string|array $locator selector name or array of (selector_name, locator) Chris@0: * Chris@0: * @return string Chris@0: * Chris@0: * @throws \InvalidArgumentException Chris@0: */ Chris@0: public function translateToXPath($locator) Chris@0: { Chris@12: if (\is_array($locator)) { Chris@12: if (2 !== \count($locator)) { Chris@12: throw new \InvalidArgumentException('NamedSelector expects array(name, locator) as argument'); Chris@12: } Chris@0: Chris@0: $selector = $locator[0]; Chris@0: $locator = $locator[1]; Chris@0: } else { Chris@0: $selector = (string) $locator; Chris@0: $locator = null; Chris@0: } Chris@0: Chris@0: if (!isset($this->selectors[$selector])) { Chris@0: throw new \InvalidArgumentException(sprintf( Chris@0: 'Unknown named selector provided: "%s". Expected one of (%s)', Chris@0: $selector, Chris@0: implode(', ', array_keys($this->selectors)) Chris@0: )); Chris@0: } Chris@0: Chris@0: $xpath = $this->selectors[$selector]; Chris@0: Chris@0: if (null !== $locator) { Chris@0: $xpath = strtr($xpath, array('%locator%' => $this->escapeLocator($locator))); Chris@0: } Chris@0: Chris@0: return $xpath; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Registers a replacement in the list of replacements. Chris@0: * Chris@0: * This method must be called in the constructor before calling the parent constructor. Chris@0: * Chris@0: * @param string $from Chris@0: * @param string $to Chris@0: */ Chris@0: protected function registerReplacement($from, $to) Chris@0: { Chris@0: $this->replacements[$from] = $to; Chris@0: } Chris@0: Chris@0: private function escapeLocator($locator) Chris@0: { Chris@0: // If the locator looks like an escaped one, don't escape it again for BC reasons. Chris@0: if ( Chris@0: preg_match('/^\'[^\']*+\'$/', $locator) Chris@0: || (false !== strpos($locator, '\'') && preg_match('/^"[^"]*+"$/', $locator)) Chris@0: || ((8 < $length = strlen($locator)) && 'concat(' === substr($locator, 0, 7) && ')' === $locator[$length - 1]) Chris@0: ) { Chris@0: @trigger_error( Chris@0: 'Passing an escaped locator to the named selector is deprecated as of 1.7 and will be removed in 2.0.' Chris@0: .' Pass the raw value instead.', Chris@0: E_USER_DEPRECATED Chris@0: ); Chris@0: Chris@0: return $locator; Chris@0: } Chris@0: Chris@0: return $this->xpathEscaper->escapeLiteral($locator); Chris@0: } Chris@0: }