annotate vendor/behat/mink-browserkit-driver/src/BrowserKitDriver.php @ 19:fa3358dc1485 tip

Add ndrum files
author Chris Cannam
date Wed, 28 Aug 2019 13:14:47 +0100
parents c2387f117808
children
rev   line source
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 }