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