Chris@0
|
1 <?php
|
Chris@0
|
2
|
Chris@0
|
3 /*
|
Chris@0
|
4 * This file is part of the Behat\Mink.
|
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\Driver;
|
Chris@0
|
12
|
Chris@0
|
13 use Behat\Mink\Exception\DriverException;
|
Chris@0
|
14 use Behat\Mink\Exception\UnsupportedDriverActionException;
|
Chris@0
|
15 use Symfony\Component\BrowserKit\Client;
|
Chris@0
|
16 use Symfony\Component\BrowserKit\Cookie;
|
Chris@0
|
17 use Symfony\Component\BrowserKit\Response;
|
Chris@0
|
18 use Symfony\Component\DomCrawler\Crawler;
|
Chris@0
|
19 use Symfony\Component\DomCrawler\Field\ChoiceFormField;
|
Chris@0
|
20 use Symfony\Component\DomCrawler\Field\FileFormField;
|
Chris@0
|
21 use Symfony\Component\DomCrawler\Field\FormField;
|
Chris@0
|
22 use Symfony\Component\DomCrawler\Field\InputFormField;
|
Chris@0
|
23 use Symfony\Component\DomCrawler\Field\TextareaFormField;
|
Chris@0
|
24 use Symfony\Component\DomCrawler\Form;
|
Chris@0
|
25 use Symfony\Component\HttpKernel\Client as HttpKernelClient;
|
Chris@0
|
26
|
Chris@0
|
27 /**
|
Chris@0
|
28 * Symfony2 BrowserKit driver.
|
Chris@0
|
29 *
|
Chris@0
|
30 * @author Konstantin Kudryashov <ever.zet@gmail.com>
|
Chris@0
|
31 */
|
Chris@0
|
32 class BrowserKitDriver extends CoreDriver
|
Chris@0
|
33 {
|
Chris@0
|
34 private $client;
|
Chris@0
|
35
|
Chris@0
|
36 /**
|
Chris@0
|
37 * @var Form[]
|
Chris@0
|
38 */
|
Chris@0
|
39 private $forms = array();
|
Chris@0
|
40 private $serverParameters = array();
|
Chris@0
|
41 private $started = false;
|
Chris@0
|
42 private $removeScriptFromUrl = false;
|
Chris@0
|
43 private $removeHostFromUrl = false;
|
Chris@0
|
44
|
Chris@0
|
45 /**
|
Chris@0
|
46 * Initializes BrowserKit driver.
|
Chris@0
|
47 *
|
Chris@0
|
48 * @param Client $client BrowserKit client instance
|
Chris@0
|
49 * @param string|null $baseUrl Base URL for HttpKernel clients
|
Chris@0
|
50 */
|
Chris@0
|
51 public function __construct(Client $client, $baseUrl = null)
|
Chris@0
|
52 {
|
Chris@0
|
53 $this->client = $client;
|
Chris@0
|
54 $this->client->followRedirects(true);
|
Chris@0
|
55
|
Chris@0
|
56 if ($baseUrl !== null && $client instanceof HttpKernelClient) {
|
Chris@0
|
57 $client->setServerParameter('SCRIPT_FILENAME', parse_url($baseUrl, PHP_URL_PATH));
|
Chris@0
|
58 }
|
Chris@0
|
59 }
|
Chris@0
|
60
|
Chris@0
|
61 /**
|
Chris@0
|
62 * Returns BrowserKit HTTP client instance.
|
Chris@0
|
63 *
|
Chris@0
|
64 * @return Client
|
Chris@0
|
65 */
|
Chris@0
|
66 public function getClient()
|
Chris@0
|
67 {
|
Chris@0
|
68 return $this->client;
|
Chris@0
|
69 }
|
Chris@0
|
70
|
Chris@0
|
71 /**
|
Chris@0
|
72 * Tells driver to remove hostname from URL.
|
Chris@0
|
73 *
|
Chris@0
|
74 * @param Boolean $remove
|
Chris@0
|
75 *
|
Chris@0
|
76 * @deprecated Deprecated as of 1.2, to be removed in 2.0. Pass the base url in the constructor instead.
|
Chris@0
|
77 */
|
Chris@0
|
78 public function setRemoveHostFromUrl($remove = true)
|
Chris@0
|
79 {
|
Chris@16
|
80 @trigger_error(
|
Chris@0
|
81 'setRemoveHostFromUrl() is deprecated as of 1.2 and will be removed in 2.0. Pass the base url in the constructor instead.',
|
Chris@0
|
82 E_USER_DEPRECATED
|
Chris@0
|
83 );
|
Chris@0
|
84 $this->removeHostFromUrl = (bool) $remove;
|
Chris@0
|
85 }
|
Chris@0
|
86
|
Chris@0
|
87 /**
|
Chris@0
|
88 * Tells driver to remove script name from URL.
|
Chris@0
|
89 *
|
Chris@0
|
90 * @param Boolean $remove
|
Chris@0
|
91 *
|
Chris@0
|
92 * @deprecated Deprecated as of 1.2, to be removed in 2.0. Pass the base url in the constructor instead.
|
Chris@0
|
93 */
|
Chris@0
|
94 public function setRemoveScriptFromUrl($remove = true)
|
Chris@0
|
95 {
|
Chris@16
|
96 @trigger_error(
|
Chris@0
|
97 'setRemoveScriptFromUrl() is deprecated as of 1.2 and will be removed in 2.0. Pass the base url in the constructor instead.',
|
Chris@0
|
98 E_USER_DEPRECATED
|
Chris@0
|
99 );
|
Chris@0
|
100 $this->removeScriptFromUrl = (bool) $remove;
|
Chris@0
|
101 }
|
Chris@0
|
102
|
Chris@0
|
103 /**
|
Chris@0
|
104 * {@inheritdoc}
|
Chris@0
|
105 */
|
Chris@0
|
106 public function start()
|
Chris@0
|
107 {
|
Chris@0
|
108 $this->started = true;
|
Chris@0
|
109 }
|
Chris@0
|
110
|
Chris@0
|
111 /**
|
Chris@0
|
112 * {@inheritdoc}
|
Chris@0
|
113 */
|
Chris@0
|
114 public function isStarted()
|
Chris@0
|
115 {
|
Chris@0
|
116 return $this->started;
|
Chris@0
|
117 }
|
Chris@0
|
118
|
Chris@0
|
119 /**
|
Chris@0
|
120 * {@inheritdoc}
|
Chris@0
|
121 */
|
Chris@0
|
122 public function stop()
|
Chris@0
|
123 {
|
Chris@0
|
124 $this->reset();
|
Chris@0
|
125 $this->started = false;
|
Chris@0
|
126 }
|
Chris@0
|
127
|
Chris@0
|
128 /**
|
Chris@0
|
129 * {@inheritdoc}
|
Chris@0
|
130 */
|
Chris@0
|
131 public function reset()
|
Chris@0
|
132 {
|
Chris@0
|
133 // Restarting the client resets the cookies and the history
|
Chris@0
|
134 $this->client->restart();
|
Chris@0
|
135 $this->forms = array();
|
Chris@0
|
136 $this->serverParameters = array();
|
Chris@0
|
137 }
|
Chris@0
|
138
|
Chris@0
|
139 /**
|
Chris@0
|
140 * {@inheritdoc}
|
Chris@0
|
141 */
|
Chris@0
|
142 public function visit($url)
|
Chris@0
|
143 {
|
Chris@0
|
144 $this->client->request('GET', $this->prepareUrl($url), array(), array(), $this->serverParameters);
|
Chris@0
|
145 $this->forms = array();
|
Chris@0
|
146 }
|
Chris@0
|
147
|
Chris@0
|
148 /**
|
Chris@0
|
149 * {@inheritdoc}
|
Chris@0
|
150 */
|
Chris@0
|
151 public function getCurrentUrl()
|
Chris@0
|
152 {
|
Chris@0
|
153 $request = $this->client->getInternalRequest();
|
Chris@0
|
154
|
Chris@0
|
155 if ($request === null) {
|
Chris@0
|
156 throw new DriverException('Unable to access the request before visiting a page');
|
Chris@0
|
157 }
|
Chris@0
|
158
|
Chris@0
|
159 return $request->getUri();
|
Chris@0
|
160 }
|
Chris@0
|
161
|
Chris@0
|
162 /**
|
Chris@0
|
163 * {@inheritdoc}
|
Chris@0
|
164 */
|
Chris@0
|
165 public function reload()
|
Chris@0
|
166 {
|
Chris@0
|
167 $this->client->reload();
|
Chris@0
|
168 $this->forms = array();
|
Chris@0
|
169 }
|
Chris@0
|
170
|
Chris@0
|
171 /**
|
Chris@0
|
172 * {@inheritdoc}
|
Chris@0
|
173 */
|
Chris@0
|
174 public function forward()
|
Chris@0
|
175 {
|
Chris@0
|
176 $this->client->forward();
|
Chris@0
|
177 $this->forms = array();
|
Chris@0
|
178 }
|
Chris@0
|
179
|
Chris@0
|
180 /**
|
Chris@0
|
181 * {@inheritdoc}
|
Chris@0
|
182 */
|
Chris@0
|
183 public function back()
|
Chris@0
|
184 {
|
Chris@0
|
185 $this->client->back();
|
Chris@0
|
186 $this->forms = array();
|
Chris@0
|
187 }
|
Chris@0
|
188
|
Chris@0
|
189 /**
|
Chris@0
|
190 * {@inheritdoc}
|
Chris@0
|
191 */
|
Chris@0
|
192 public function setBasicAuth($user, $password)
|
Chris@0
|
193 {
|
Chris@0
|
194 if (false === $user) {
|
Chris@0
|
195 unset($this->serverParameters['PHP_AUTH_USER'], $this->serverParameters['PHP_AUTH_PW']);
|
Chris@0
|
196
|
Chris@0
|
197 return;
|
Chris@0
|
198 }
|
Chris@0
|
199
|
Chris@0
|
200 $this->serverParameters['PHP_AUTH_USER'] = $user;
|
Chris@0
|
201 $this->serverParameters['PHP_AUTH_PW'] = $password;
|
Chris@0
|
202 }
|
Chris@0
|
203
|
Chris@0
|
204 /**
|
Chris@0
|
205 * {@inheritdoc}
|
Chris@0
|
206 */
|
Chris@0
|
207 public function setRequestHeader($name, $value)
|
Chris@0
|
208 {
|
Chris@0
|
209 $contentHeaders = array('CONTENT_LENGTH' => true, 'CONTENT_MD5' => true, 'CONTENT_TYPE' => true);
|
Chris@0
|
210 $name = str_replace('-', '_', strtoupper($name));
|
Chris@0
|
211
|
Chris@0
|
212 // CONTENT_* are not prefixed with HTTP_ in PHP when building $_SERVER
|
Chris@0
|
213 if (!isset($contentHeaders[$name])) {
|
Chris@0
|
214 $name = 'HTTP_' . $name;
|
Chris@0
|
215 }
|
Chris@0
|
216
|
Chris@0
|
217 $this->serverParameters[$name] = $value;
|
Chris@0
|
218 }
|
Chris@0
|
219
|
Chris@0
|
220 /**
|
Chris@0
|
221 * {@inheritdoc}
|
Chris@0
|
222 */
|
Chris@0
|
223 public function getResponseHeaders()
|
Chris@0
|
224 {
|
Chris@0
|
225 return $this->getResponse()->getHeaders();
|
Chris@0
|
226 }
|
Chris@0
|
227
|
Chris@0
|
228 /**
|
Chris@0
|
229 * {@inheritdoc}
|
Chris@0
|
230 */
|
Chris@0
|
231 public function setCookie($name, $value = null)
|
Chris@0
|
232 {
|
Chris@0
|
233 if (null === $value) {
|
Chris@0
|
234 $this->deleteCookie($name);
|
Chris@0
|
235
|
Chris@0
|
236 return;
|
Chris@0
|
237 }
|
Chris@0
|
238
|
Chris@0
|
239 $jar = $this->client->getCookieJar();
|
Chris@0
|
240 $jar->set(new Cookie($name, $value));
|
Chris@0
|
241 }
|
Chris@0
|
242
|
Chris@0
|
243 /**
|
Chris@0
|
244 * Deletes a cookie by name.
|
Chris@0
|
245 *
|
Chris@0
|
246 * @param string $name Cookie name.
|
Chris@0
|
247 */
|
Chris@0
|
248 private function deleteCookie($name)
|
Chris@0
|
249 {
|
Chris@0
|
250 $path = $this->getCookiePath();
|
Chris@0
|
251 $jar = $this->client->getCookieJar();
|
Chris@0
|
252
|
Chris@0
|
253 do {
|
Chris@0
|
254 if (null !== $jar->get($name, $path)) {
|
Chris@0
|
255 $jar->expire($name, $path);
|
Chris@0
|
256 }
|
Chris@0
|
257
|
Chris@0
|
258 $path = preg_replace('/.$/', '', $path);
|
Chris@0
|
259 } while ($path);
|
Chris@0
|
260 }
|
Chris@0
|
261
|
Chris@0
|
262 /**
|
Chris@0
|
263 * Returns current cookie path.
|
Chris@0
|
264 *
|
Chris@0
|
265 * @return string
|
Chris@0
|
266 */
|
Chris@0
|
267 private function getCookiePath()
|
Chris@0
|
268 {
|
Chris@0
|
269 $path = dirname(parse_url($this->getCurrentUrl(), PHP_URL_PATH));
|
Chris@0
|
270
|
Chris@0
|
271 if ('\\' === DIRECTORY_SEPARATOR) {
|
Chris@0
|
272 $path = str_replace('\\', '/', $path);
|
Chris@0
|
273 }
|
Chris@0
|
274
|
Chris@0
|
275 return $path;
|
Chris@0
|
276 }
|
Chris@0
|
277
|
Chris@0
|
278 /**
|
Chris@0
|
279 * {@inheritdoc}
|
Chris@0
|
280 */
|
Chris@0
|
281 public function getCookie($name)
|
Chris@0
|
282 {
|
Chris@0
|
283 // Note that the following doesn't work well because
|
Chris@0
|
284 // Symfony\Component\BrowserKit\CookieJar stores cookies by name,
|
Chris@0
|
285 // path, AND domain and if you don't fill them all in correctly then
|
Chris@0
|
286 // you won't get the value that you're expecting.
|
Chris@0
|
287 //
|
Chris@0
|
288 // $jar = $this->client->getCookieJar();
|
Chris@0
|
289 //
|
Chris@0
|
290 // if (null !== $cookie = $jar->get($name)) {
|
Chris@0
|
291 // return $cookie->getValue();
|
Chris@0
|
292 // }
|
Chris@0
|
293
|
Chris@0
|
294 $allValues = $this->client->getCookieJar()->allValues($this->getCurrentUrl());
|
Chris@0
|
295
|
Chris@0
|
296 if (isset($allValues[$name])) {
|
Chris@0
|
297 return $allValues[$name];
|
Chris@0
|
298 }
|
Chris@0
|
299
|
Chris@0
|
300 return null;
|
Chris@0
|
301 }
|
Chris@0
|
302
|
Chris@0
|
303 /**
|
Chris@0
|
304 * {@inheritdoc}
|
Chris@0
|
305 */
|
Chris@0
|
306 public function getStatusCode()
|
Chris@0
|
307 {
|
Chris@0
|
308 return $this->getResponse()->getStatus();
|
Chris@0
|
309 }
|
Chris@0
|
310
|
Chris@0
|
311 /**
|
Chris@0
|
312 * {@inheritdoc}
|
Chris@0
|
313 */
|
Chris@0
|
314 public function getContent()
|
Chris@0
|
315 {
|
Chris@0
|
316 return $this->getResponse()->getContent();
|
Chris@0
|
317 }
|
Chris@0
|
318
|
Chris@0
|
319 /**
|
Chris@0
|
320 * {@inheritdoc}
|
Chris@0
|
321 */
|
Chris@0
|
322 public function findElementXpaths($xpath)
|
Chris@0
|
323 {
|
Chris@0
|
324 $nodes = $this->getCrawler()->filterXPath($xpath);
|
Chris@0
|
325
|
Chris@0
|
326 $elements = array();
|
Chris@0
|
327 foreach ($nodes as $i => $node) {
|
Chris@0
|
328 $elements[] = sprintf('(%s)[%d]', $xpath, $i + 1);
|
Chris@0
|
329 }
|
Chris@0
|
330
|
Chris@0
|
331 return $elements;
|
Chris@0
|
332 }
|
Chris@0
|
333
|
Chris@0
|
334 /**
|
Chris@0
|
335 * {@inheritdoc}
|
Chris@0
|
336 */
|
Chris@0
|
337 public function getTagName($xpath)
|
Chris@0
|
338 {
|
Chris@0
|
339 return $this->getCrawlerNode($this->getFilteredCrawler($xpath))->nodeName;
|
Chris@0
|
340 }
|
Chris@0
|
341
|
Chris@0
|
342 /**
|
Chris@0
|
343 * {@inheritdoc}
|
Chris@0
|
344 */
|
Chris@0
|
345 public function getText($xpath)
|
Chris@0
|
346 {
|
Chris@0
|
347 $text = $this->getFilteredCrawler($xpath)->text();
|
Chris@0
|
348 $text = str_replace("\n", ' ', $text);
|
Chris@0
|
349 $text = preg_replace('/ {2,}/', ' ', $text);
|
Chris@0
|
350
|
Chris@0
|
351 return trim($text);
|
Chris@0
|
352 }
|
Chris@0
|
353
|
Chris@0
|
354 /**
|
Chris@0
|
355 * {@inheritdoc}
|
Chris@0
|
356 */
|
Chris@0
|
357 public function getHtml($xpath)
|
Chris@0
|
358 {
|
Chris@0
|
359 // cut the tag itself (making innerHTML out of outerHTML)
|
Chris@0
|
360 return preg_replace('/^\<[^\>]+\>|\<[^\>]+\>$/', '', $this->getOuterHtml($xpath));
|
Chris@0
|
361 }
|
Chris@0
|
362
|
Chris@0
|
363 /**
|
Chris@0
|
364 * {@inheritdoc}
|
Chris@0
|
365 */
|
Chris@0
|
366 public function getOuterHtml($xpath)
|
Chris@0
|
367 {
|
Chris@0
|
368 $node = $this->getCrawlerNode($this->getFilteredCrawler($xpath));
|
Chris@0
|
369
|
Chris@0
|
370 return $node->ownerDocument->saveHTML($node);
|
Chris@0
|
371 }
|
Chris@0
|
372
|
Chris@0
|
373 /**
|
Chris@0
|
374 * {@inheritdoc}
|
Chris@0
|
375 */
|
Chris@0
|
376 public function getAttribute($xpath, $name)
|
Chris@0
|
377 {
|
Chris@0
|
378 $node = $this->getFilteredCrawler($xpath);
|
Chris@0
|
379
|
Chris@0
|
380 if ($this->getCrawlerNode($node)->hasAttribute($name)) {
|
Chris@0
|
381 return $node->attr($name);
|
Chris@0
|
382 }
|
Chris@0
|
383
|
Chris@0
|
384 return null;
|
Chris@0
|
385 }
|
Chris@0
|
386
|
Chris@0
|
387 /**
|
Chris@0
|
388 * {@inheritdoc}
|
Chris@0
|
389 */
|
Chris@0
|
390 public function getValue($xpath)
|
Chris@0
|
391 {
|
Chris@0
|
392 if (in_array($this->getAttribute($xpath, 'type'), array('submit', 'image', 'button'), true)) {
|
Chris@0
|
393 return $this->getAttribute($xpath, 'value');
|
Chris@0
|
394 }
|
Chris@0
|
395
|
Chris@0
|
396 $node = $this->getCrawlerNode($this->getFilteredCrawler($xpath));
|
Chris@0
|
397
|
Chris@0
|
398 if ('option' === $node->tagName) {
|
Chris@0
|
399 return $this->getOptionValue($node);
|
Chris@0
|
400 }
|
Chris@0
|
401
|
Chris@0
|
402 try {
|
Chris@0
|
403 $field = $this->getFormField($xpath);
|
Chris@0
|
404 } catch (\InvalidArgumentException $e) {
|
Chris@0
|
405 return $this->getAttribute($xpath, 'value');
|
Chris@0
|
406 }
|
Chris@0
|
407
|
Chris@0
|
408 return $field->getValue();
|
Chris@0
|
409 }
|
Chris@0
|
410
|
Chris@0
|
411 /**
|
Chris@0
|
412 * {@inheritdoc}
|
Chris@0
|
413 */
|
Chris@0
|
414 public function setValue($xpath, $value)
|
Chris@0
|
415 {
|
Chris@0
|
416 $this->getFormField($xpath)->setValue($value);
|
Chris@0
|
417 }
|
Chris@0
|
418
|
Chris@0
|
419 /**
|
Chris@0
|
420 * {@inheritdoc}
|
Chris@0
|
421 */
|
Chris@0
|
422 public function check($xpath)
|
Chris@0
|
423 {
|
Chris@0
|
424 $this->getCheckboxField($xpath)->tick();
|
Chris@0
|
425 }
|
Chris@0
|
426
|
Chris@0
|
427 /**
|
Chris@0
|
428 * {@inheritdoc}
|
Chris@0
|
429 */
|
Chris@0
|
430 public function uncheck($xpath)
|
Chris@0
|
431 {
|
Chris@0
|
432 $this->getCheckboxField($xpath)->untick();
|
Chris@0
|
433 }
|
Chris@0
|
434
|
Chris@0
|
435 /**
|
Chris@0
|
436 * {@inheritdoc}
|
Chris@0
|
437 */
|
Chris@0
|
438 public function selectOption($xpath, $value, $multiple = false)
|
Chris@0
|
439 {
|
Chris@0
|
440 $field = $this->getFormField($xpath);
|
Chris@0
|
441
|
Chris@0
|
442 if (!$field instanceof ChoiceFormField) {
|
Chris@0
|
443 throw new DriverException(sprintf('Impossible to select an option on the element with XPath "%s" as it is not a select or radio input', $xpath));
|
Chris@0
|
444 }
|
Chris@0
|
445
|
Chris@0
|
446 if ($multiple) {
|
Chris@0
|
447 $oldValue = (array) $field->getValue();
|
Chris@0
|
448 $oldValue[] = $value;
|
Chris@0
|
449 $value = $oldValue;
|
Chris@0
|
450 }
|
Chris@0
|
451
|
Chris@0
|
452 $field->select($value);
|
Chris@0
|
453 }
|
Chris@0
|
454
|
Chris@0
|
455 /**
|
Chris@0
|
456 * {@inheritdoc}
|
Chris@0
|
457 */
|
Chris@0
|
458 public function isSelected($xpath)
|
Chris@0
|
459 {
|
Chris@0
|
460 $optionValue = $this->getOptionValue($this->getCrawlerNode($this->getFilteredCrawler($xpath)));
|
Chris@0
|
461 $selectField = $this->getFormField('(' . $xpath . ')/ancestor-or-self::*[local-name()="select"]');
|
Chris@0
|
462 $selectValue = $selectField->getValue();
|
Chris@0
|
463
|
Chris@0
|
464 return is_array($selectValue) ? in_array($optionValue, $selectValue, true) : $optionValue === $selectValue;
|
Chris@0
|
465 }
|
Chris@0
|
466
|
Chris@0
|
467 /**
|
Chris@0
|
468 * {@inheritdoc}
|
Chris@0
|
469 */
|
Chris@0
|
470 public function click($xpath)
|
Chris@0
|
471 {
|
Chris@0
|
472 $crawler = $this->getFilteredCrawler($xpath);
|
Chris@0
|
473 $node = $this->getCrawlerNode($crawler);
|
Chris@0
|
474 $tagName = $node->nodeName;
|
Chris@0
|
475
|
Chris@0
|
476 if ('a' === $tagName) {
|
Chris@0
|
477 $this->client->click($crawler->link());
|
Chris@0
|
478 $this->forms = array();
|
Chris@0
|
479 } elseif ($this->canSubmitForm($node)) {
|
Chris@0
|
480 $this->submit($crawler->form());
|
Chris@0
|
481 } elseif ($this->canResetForm($node)) {
|
Chris@0
|
482 $this->resetForm($node);
|
Chris@0
|
483 } else {
|
Chris@0
|
484 $message = sprintf('%%s supports clicking on links and submit or reset buttons only. But "%s" provided', $tagName);
|
Chris@0
|
485
|
Chris@0
|
486 throw new UnsupportedDriverActionException($message, $this);
|
Chris@0
|
487 }
|
Chris@0
|
488 }
|
Chris@0
|
489
|
Chris@0
|
490 /**
|
Chris@0
|
491 * {@inheritdoc}
|
Chris@0
|
492 */
|
Chris@0
|
493 public function isChecked($xpath)
|
Chris@0
|
494 {
|
Chris@0
|
495 $field = $this->getFormField($xpath);
|
Chris@0
|
496
|
Chris@0
|
497 if (!$field instanceof ChoiceFormField || 'select' === $field->getType()) {
|
Chris@0
|
498 throw new DriverException(sprintf('Impossible to get the checked state of the element with XPath "%s" as it is not a checkbox or radio input', $xpath));
|
Chris@0
|
499 }
|
Chris@0
|
500
|
Chris@0
|
501 if ('checkbox' === $field->getType()) {
|
Chris@0
|
502 return $field->hasValue();
|
Chris@0
|
503 }
|
Chris@0
|
504
|
Chris@0
|
505 $radio = $this->getCrawlerNode($this->getFilteredCrawler($xpath));
|
Chris@0
|
506
|
Chris@0
|
507 return $radio->getAttribute('value') === $field->getValue();
|
Chris@0
|
508 }
|
Chris@0
|
509
|
Chris@0
|
510 /**
|
Chris@0
|
511 * {@inheritdoc}
|
Chris@0
|
512 */
|
Chris@0
|
513 public function attachFile($xpath, $path)
|
Chris@0
|
514 {
|
Chris@0
|
515 $field = $this->getFormField($xpath);
|
Chris@0
|
516
|
Chris@0
|
517 if (!$field instanceof FileFormField) {
|
Chris@0
|
518 throw new DriverException(sprintf('Impossible to attach a file on the element with XPath "%s" as it is not a file input', $xpath));
|
Chris@0
|
519 }
|
Chris@0
|
520
|
Chris@0
|
521 $field->upload($path);
|
Chris@0
|
522 }
|
Chris@0
|
523
|
Chris@0
|
524 /**
|
Chris@0
|
525 * {@inheritdoc}
|
Chris@0
|
526 */
|
Chris@0
|
527 public function submitForm($xpath)
|
Chris@0
|
528 {
|
Chris@0
|
529 $crawler = $this->getFilteredCrawler($xpath);
|
Chris@0
|
530
|
Chris@0
|
531 $this->submit($crawler->form());
|
Chris@0
|
532 }
|
Chris@0
|
533
|
Chris@0
|
534 /**
|
Chris@0
|
535 * @return Response
|
Chris@0
|
536 *
|
Chris@0
|
537 * @throws DriverException If there is not response yet
|
Chris@0
|
538 */
|
Chris@0
|
539 protected function getResponse()
|
Chris@0
|
540 {
|
Chris@0
|
541 $response = $this->client->getInternalResponse();
|
Chris@0
|
542
|
Chris@0
|
543 if (null === $response) {
|
Chris@0
|
544 throw new DriverException('Unable to access the response before visiting a page');
|
Chris@0
|
545 }
|
Chris@0
|
546
|
Chris@0
|
547 return $response;
|
Chris@0
|
548 }
|
Chris@0
|
549
|
Chris@0
|
550 /**
|
Chris@0
|
551 * Prepares URL for visiting.
|
Chris@0
|
552 * Removes "*.php/" from urls and then passes it to BrowserKitDriver::visit().
|
Chris@0
|
553 *
|
Chris@0
|
554 * @param string $url
|
Chris@0
|
555 *
|
Chris@0
|
556 * @return string
|
Chris@0
|
557 */
|
Chris@0
|
558 protected function prepareUrl($url)
|
Chris@0
|
559 {
|
Chris@0
|
560 $replacement = ($this->removeHostFromUrl ? '' : '$1') . ($this->removeScriptFromUrl ? '' : '$2');
|
Chris@0
|
561
|
Chris@0
|
562 return preg_replace('#(https?\://[^/]+)(/[^/\.]+\.php)?#', $replacement, $url);
|
Chris@0
|
563 }
|
Chris@0
|
564
|
Chris@0
|
565 /**
|
Chris@0
|
566 * Returns form field from XPath query.
|
Chris@0
|
567 *
|
Chris@0
|
568 * @param string $xpath
|
Chris@0
|
569 *
|
Chris@0
|
570 * @return FormField
|
Chris@0
|
571 *
|
Chris@0
|
572 * @throws DriverException
|
Chris@0
|
573 */
|
Chris@0
|
574 protected function getFormField($xpath)
|
Chris@0
|
575 {
|
Chris@0
|
576 $fieldNode = $this->getCrawlerNode($this->getFilteredCrawler($xpath));
|
Chris@0
|
577 $fieldName = str_replace('[]', '', $fieldNode->getAttribute('name'));
|
Chris@0
|
578
|
Chris@0
|
579 $formNode = $this->getFormNode($fieldNode);
|
Chris@0
|
580 $formId = $this->getFormNodeId($formNode);
|
Chris@0
|
581
|
Chris@0
|
582 if (!isset($this->forms[$formId])) {
|
Chris@0
|
583 $this->forms[$formId] = new Form($formNode, $this->getCurrentUrl());
|
Chris@0
|
584 }
|
Chris@0
|
585
|
Chris@0
|
586 if (is_array($this->forms[$formId][$fieldName])) {
|
Chris@0
|
587 return $this->forms[$formId][$fieldName][$this->getFieldPosition($fieldNode)];
|
Chris@0
|
588 }
|
Chris@0
|
589
|
Chris@0
|
590 return $this->forms[$formId][$fieldName];
|
Chris@0
|
591 }
|
Chris@0
|
592
|
Chris@0
|
593 /**
|
Chris@0
|
594 * Returns the checkbox field from xpath query, ensuring it is valid.
|
Chris@0
|
595 *
|
Chris@0
|
596 * @param string $xpath
|
Chris@0
|
597 *
|
Chris@0
|
598 * @return ChoiceFormField
|
Chris@0
|
599 *
|
Chris@0
|
600 * @throws DriverException when the field is not a checkbox
|
Chris@0
|
601 */
|
Chris@0
|
602 private function getCheckboxField($xpath)
|
Chris@0
|
603 {
|
Chris@0
|
604 $field = $this->getFormField($xpath);
|
Chris@0
|
605
|
Chris@0
|
606 if (!$field instanceof ChoiceFormField) {
|
Chris@0
|
607 throw new DriverException(sprintf('Impossible to check the element with XPath "%s" as it is not a checkbox', $xpath));
|
Chris@0
|
608 }
|
Chris@0
|
609
|
Chris@0
|
610 return $field;
|
Chris@0
|
611 }
|
Chris@0
|
612
|
Chris@0
|
613 /**
|
Chris@0
|
614 * @param \DOMElement $element
|
Chris@0
|
615 *
|
Chris@0
|
616 * @return \DOMElement
|
Chris@0
|
617 *
|
Chris@0
|
618 * @throws DriverException if the form node cannot be found
|
Chris@0
|
619 */
|
Chris@0
|
620 private function getFormNode(\DOMElement $element)
|
Chris@0
|
621 {
|
Chris@0
|
622 if ($element->hasAttribute('form')) {
|
Chris@0
|
623 $formId = $element->getAttribute('form');
|
Chris@0
|
624 $formNode = $element->ownerDocument->getElementById($formId);
|
Chris@0
|
625
|
Chris@0
|
626 if (null === $formNode || 'form' !== $formNode->nodeName) {
|
Chris@0
|
627 throw new DriverException(sprintf('The selected node has an invalid form attribute (%s).', $formId));
|
Chris@0
|
628 }
|
Chris@0
|
629
|
Chris@0
|
630 return $formNode;
|
Chris@0
|
631 }
|
Chris@0
|
632
|
Chris@0
|
633 $formNode = $element;
|
Chris@0
|
634
|
Chris@0
|
635 do {
|
Chris@0
|
636 // use the ancestor form element
|
Chris@0
|
637 if (null === $formNode = $formNode->parentNode) {
|
Chris@0
|
638 throw new DriverException('The selected node does not have a form ancestor.');
|
Chris@0
|
639 }
|
Chris@0
|
640 } while ('form' !== $formNode->nodeName);
|
Chris@0
|
641
|
Chris@0
|
642 return $formNode;
|
Chris@0
|
643 }
|
Chris@0
|
644
|
Chris@0
|
645 /**
|
Chris@0
|
646 * Gets the position of the field node among elements with the same name
|
Chris@0
|
647 *
|
Chris@0
|
648 * BrowserKit uses the field name as index to find the field in its Form object.
|
Chris@0
|
649 * When multiple fields have the same name (checkboxes for instance), it will return
|
Chris@0
|
650 * an array of elements in the order they appear in the DOM.
|
Chris@0
|
651 *
|
Chris@0
|
652 * @param \DOMElement $fieldNode
|
Chris@0
|
653 *
|
Chris@0
|
654 * @return integer
|
Chris@0
|
655 */
|
Chris@0
|
656 private function getFieldPosition(\DOMElement $fieldNode)
|
Chris@0
|
657 {
|
Chris@0
|
658 $elements = $this->getCrawler()->filterXPath('//*[@name=\''.$fieldNode->getAttribute('name').'\']');
|
Chris@0
|
659
|
Chris@0
|
660 if (count($elements) > 1) {
|
Chris@0
|
661 // more than one element contains this name !
|
Chris@0
|
662 // so we need to find the position of $fieldNode
|
Chris@0
|
663 foreach ($elements as $key => $element) {
|
Chris@0
|
664 /** @var \DOMElement $element */
|
Chris@0
|
665 if ($element->getNodePath() === $fieldNode->getNodePath()) {
|
Chris@0
|
666 return $key;
|
Chris@0
|
667 }
|
Chris@0
|
668 }
|
Chris@0
|
669 }
|
Chris@0
|
670
|
Chris@0
|
671 return 0;
|
Chris@0
|
672 }
|
Chris@0
|
673
|
Chris@0
|
674 private function submit(Form $form)
|
Chris@0
|
675 {
|
Chris@0
|
676 $formId = $this->getFormNodeId($form->getFormNode());
|
Chris@0
|
677
|
Chris@0
|
678 if (isset($this->forms[$formId])) {
|
Chris@0
|
679 $this->mergeForms($form, $this->forms[$formId]);
|
Chris@0
|
680 }
|
Chris@0
|
681
|
Chris@0
|
682 // remove empty file fields from request
|
Chris@0
|
683 foreach ($form->getFiles() as $name => $field) {
|
Chris@0
|
684 if (empty($field['name']) && empty($field['tmp_name'])) {
|
Chris@0
|
685 $form->remove($name);
|
Chris@0
|
686 }
|
Chris@0
|
687 }
|
Chris@0
|
688
|
Chris@0
|
689 foreach ($form->all() as $field) {
|
Chris@0
|
690 // Add a fix for https://github.com/symfony/symfony/pull/10733 to support Symfony versions which are not fixed
|
Chris@0
|
691 if ($field instanceof TextareaFormField && null === $field->getValue()) {
|
Chris@0
|
692 $field->setValue('');
|
Chris@0
|
693 }
|
Chris@0
|
694 }
|
Chris@0
|
695
|
Chris@0
|
696 $this->client->submit($form);
|
Chris@0
|
697
|
Chris@0
|
698 $this->forms = array();
|
Chris@0
|
699 }
|
Chris@0
|
700
|
Chris@0
|
701 private function resetForm(\DOMElement $fieldNode)
|
Chris@0
|
702 {
|
Chris@0
|
703 $formNode = $this->getFormNode($fieldNode);
|
Chris@0
|
704 $formId = $this->getFormNodeId($formNode);
|
Chris@0
|
705 unset($this->forms[$formId]);
|
Chris@0
|
706 }
|
Chris@0
|
707
|
Chris@0
|
708 /**
|
Chris@0
|
709 * Determines if a node can submit a form.
|
Chris@0
|
710 *
|
Chris@0
|
711 * @param \DOMElement $node Node.
|
Chris@0
|
712 *
|
Chris@0
|
713 * @return boolean
|
Chris@0
|
714 */
|
Chris@0
|
715 private function canSubmitForm(\DOMElement $node)
|
Chris@0
|
716 {
|
Chris@0
|
717 $type = $node->hasAttribute('type') ? $node->getAttribute('type') : null;
|
Chris@0
|
718
|
Chris@0
|
719 if ('input' === $node->nodeName && in_array($type, array('submit', 'image'), true)) {
|
Chris@0
|
720 return true;
|
Chris@0
|
721 }
|
Chris@0
|
722
|
Chris@0
|
723 return 'button' === $node->nodeName && (null === $type || 'submit' === $type);
|
Chris@0
|
724 }
|
Chris@0
|
725
|
Chris@0
|
726 /**
|
Chris@0
|
727 * Determines if a node can reset a form.
|
Chris@0
|
728 *
|
Chris@0
|
729 * @param \DOMElement $node Node.
|
Chris@0
|
730 *
|
Chris@0
|
731 * @return boolean
|
Chris@0
|
732 */
|
Chris@0
|
733 private function canResetForm(\DOMElement $node)
|
Chris@0
|
734 {
|
Chris@0
|
735 $type = $node->hasAttribute('type') ? $node->getAttribute('type') : null;
|
Chris@0
|
736
|
Chris@0
|
737 return in_array($node->nodeName, array('input', 'button'), true) && 'reset' === $type;
|
Chris@0
|
738 }
|
Chris@0
|
739
|
Chris@0
|
740 /**
|
Chris@0
|
741 * Returns form node unique identifier.
|
Chris@0
|
742 *
|
Chris@0
|
743 * @param \DOMElement $form
|
Chris@0
|
744 *
|
Chris@0
|
745 * @return string
|
Chris@0
|
746 */
|
Chris@0
|
747 private function getFormNodeId(\DOMElement $form)
|
Chris@0
|
748 {
|
Chris@0
|
749 return md5($form->getLineNo() . $form->getNodePath() . $form->nodeValue);
|
Chris@0
|
750 }
|
Chris@0
|
751
|
Chris@0
|
752 /**
|
Chris@0
|
753 * Gets the value of an option element
|
Chris@0
|
754 *
|
Chris@0
|
755 * @param \DOMElement $option
|
Chris@0
|
756 *
|
Chris@0
|
757 * @return string
|
Chris@0
|
758 *
|
Chris@0
|
759 * @see \Symfony\Component\DomCrawler\Field\ChoiceFormField::buildOptionValue
|
Chris@0
|
760 */
|
Chris@0
|
761 private function getOptionValue(\DOMElement $option)
|
Chris@0
|
762 {
|
Chris@0
|
763 if ($option->hasAttribute('value')) {
|
Chris@0
|
764 return $option->getAttribute('value');
|
Chris@0
|
765 }
|
Chris@0
|
766
|
Chris@0
|
767 if (!empty($option->nodeValue)) {
|
Chris@0
|
768 return $option->nodeValue;
|
Chris@0
|
769 }
|
Chris@0
|
770
|
Chris@0
|
771 return '1'; // DomCrawler uses 1 by default if there is no text in the option
|
Chris@0
|
772 }
|
Chris@0
|
773
|
Chris@0
|
774 /**
|
Chris@0
|
775 * Merges second form values into first one.
|
Chris@0
|
776 *
|
Chris@0
|
777 * @param Form $to merging target
|
Chris@0
|
778 * @param Form $from merging source
|
Chris@0
|
779 */
|
Chris@0
|
780 private function mergeForms(Form $to, Form $from)
|
Chris@0
|
781 {
|
Chris@0
|
782 foreach ($from->all() as $name => $field) {
|
Chris@0
|
783 $fieldReflection = new \ReflectionObject($field);
|
Chris@0
|
784 $nodeReflection = $fieldReflection->getProperty('node');
|
Chris@0
|
785 $valueReflection = $fieldReflection->getProperty('value');
|
Chris@0
|
786
|
Chris@0
|
787 $nodeReflection->setAccessible(true);
|
Chris@0
|
788 $valueReflection->setAccessible(true);
|
Chris@0
|
789
|
Chris@0
|
790 $isIgnoredField = $field instanceof InputFormField &&
|
Chris@0
|
791 in_array($nodeReflection->getValue($field)->getAttribute('type'), array('submit', 'button', 'image'), true);
|
Chris@0
|
792
|
Chris@0
|
793 if (!$isIgnoredField) {
|
Chris@0
|
794 $valueReflection->setValue($to[$name], $valueReflection->getValue($field));
|
Chris@0
|
795 }
|
Chris@0
|
796 }
|
Chris@0
|
797 }
|
Chris@0
|
798
|
Chris@0
|
799 /**
|
Chris@0
|
800 * Returns DOMElement from crawler instance.
|
Chris@0
|
801 *
|
Chris@0
|
802 * @param Crawler $crawler
|
Chris@0
|
803 *
|
Chris@0
|
804 * @return \DOMElement
|
Chris@0
|
805 *
|
Chris@0
|
806 * @throws DriverException when the node does not exist
|
Chris@0
|
807 */
|
Chris@0
|
808 private function getCrawlerNode(Crawler $crawler)
|
Chris@0
|
809 {
|
Chris@0
|
810 $node = null;
|
Chris@0
|
811
|
Chris@0
|
812 if ($crawler instanceof \Iterator) {
|
Chris@0
|
813 // for symfony 2.3 compatibility as getNode is not public before symfony 2.4
|
Chris@0
|
814 $crawler->rewind();
|
Chris@0
|
815 $node = $crawler->current();
|
Chris@0
|
816 } else {
|
Chris@0
|
817 $node = $crawler->getNode(0);
|
Chris@0
|
818 }
|
Chris@0
|
819
|
Chris@0
|
820 if (null !== $node) {
|
Chris@0
|
821 return $node;
|
Chris@0
|
822 }
|
Chris@0
|
823
|
Chris@0
|
824 throw new DriverException('The element does not exist');
|
Chris@0
|
825 }
|
Chris@0
|
826
|
Chris@0
|
827 /**
|
Chris@0
|
828 * Returns a crawler filtered for the given XPath, requiring at least 1 result.
|
Chris@0
|
829 *
|
Chris@0
|
830 * @param string $xpath
|
Chris@0
|
831 *
|
Chris@0
|
832 * @return Crawler
|
Chris@0
|
833 *
|
Chris@0
|
834 * @throws DriverException when no matching elements are found
|
Chris@0
|
835 */
|
Chris@0
|
836 private function getFilteredCrawler($xpath)
|
Chris@0
|
837 {
|
Chris@0
|
838 if (!count($crawler = $this->getCrawler()->filterXPath($xpath))) {
|
Chris@0
|
839 throw new DriverException(sprintf('There is no element matching XPath "%s"', $xpath));
|
Chris@0
|
840 }
|
Chris@0
|
841
|
Chris@0
|
842 return $crawler;
|
Chris@0
|
843 }
|
Chris@0
|
844
|
Chris@0
|
845 /**
|
Chris@0
|
846 * Returns crawler instance (got from client).
|
Chris@0
|
847 *
|
Chris@0
|
848 * @return Crawler
|
Chris@0
|
849 *
|
Chris@0
|
850 * @throws DriverException
|
Chris@0
|
851 */
|
Chris@0
|
852 private function getCrawler()
|
Chris@0
|
853 {
|
Chris@0
|
854 $crawler = $this->client->getCrawler();
|
Chris@0
|
855
|
Chris@0
|
856 if (null === $crawler) {
|
Chris@0
|
857 throw new DriverException('Unable to access the response content before visiting a page');
|
Chris@0
|
858 }
|
Chris@0
|
859
|
Chris@0
|
860 return $crawler;
|
Chris@0
|
861 }
|
Chris@0
|
862 }
|