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