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