Chris@0: Chris@0: * Chris@0: * For the full copyright and license information, please view the LICENSE Chris@0: * file that was distributed with this source code. Chris@0: */ Chris@0: Chris@0: namespace Behat\Mink\Driver; Chris@0: Chris@0: use Behat\Mink\Exception\DriverException; Chris@0: use Behat\Mink\Exception\UnsupportedDriverActionException; Chris@0: use Symfony\Component\BrowserKit\Client; Chris@0: use Symfony\Component\BrowserKit\Cookie; Chris@0: use Symfony\Component\BrowserKit\Response; Chris@0: use Symfony\Component\DomCrawler\Crawler; Chris@0: use Symfony\Component\DomCrawler\Field\ChoiceFormField; Chris@0: use Symfony\Component\DomCrawler\Field\FileFormField; Chris@0: use Symfony\Component\DomCrawler\Field\FormField; Chris@0: use Symfony\Component\DomCrawler\Field\InputFormField; Chris@0: use Symfony\Component\DomCrawler\Field\TextareaFormField; Chris@0: use Symfony\Component\DomCrawler\Form; Chris@0: use Symfony\Component\HttpKernel\Client as HttpKernelClient; Chris@0: Chris@0: /** Chris@0: * Symfony2 BrowserKit driver. Chris@0: * Chris@0: * @author Konstantin Kudryashov Chris@0: */ Chris@0: class BrowserKitDriver extends CoreDriver Chris@0: { Chris@0: private $client; Chris@0: Chris@0: /** Chris@0: * @var Form[] Chris@0: */ Chris@0: private $forms = array(); Chris@0: private $serverParameters = array(); Chris@0: private $started = false; Chris@0: private $removeScriptFromUrl = false; Chris@0: private $removeHostFromUrl = false; Chris@0: Chris@0: /** Chris@0: * Initializes BrowserKit driver. Chris@0: * Chris@0: * @param Client $client BrowserKit client instance Chris@0: * @param string|null $baseUrl Base URL for HttpKernel clients Chris@0: */ Chris@0: public function __construct(Client $client, $baseUrl = null) Chris@0: { Chris@0: $this->client = $client; Chris@0: $this->client->followRedirects(true); Chris@0: Chris@0: if ($baseUrl !== null && $client instanceof HttpKernelClient) { Chris@0: $client->setServerParameter('SCRIPT_FILENAME', parse_url($baseUrl, PHP_URL_PATH)); Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Returns BrowserKit HTTP client instance. Chris@0: * Chris@0: * @return Client Chris@0: */ Chris@0: public function getClient() Chris@0: { Chris@0: return $this->client; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Tells driver to remove hostname from URL. Chris@0: * Chris@0: * @param Boolean $remove Chris@0: * Chris@0: * @deprecated Deprecated as of 1.2, to be removed in 2.0. Pass the base url in the constructor instead. Chris@0: */ Chris@0: public function setRemoveHostFromUrl($remove = true) Chris@0: { Chris@16: @trigger_error( Chris@0: 'setRemoveHostFromUrl() is deprecated as of 1.2 and will be removed in 2.0. Pass the base url in the constructor instead.', Chris@0: E_USER_DEPRECATED Chris@0: ); Chris@0: $this->removeHostFromUrl = (bool) $remove; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Tells driver to remove script name from URL. Chris@0: * Chris@0: * @param Boolean $remove Chris@0: * Chris@0: * @deprecated Deprecated as of 1.2, to be removed in 2.0. Pass the base url in the constructor instead. Chris@0: */ Chris@0: public function setRemoveScriptFromUrl($remove = true) Chris@0: { Chris@16: @trigger_error( Chris@0: 'setRemoveScriptFromUrl() is deprecated as of 1.2 and will be removed in 2.0. Pass the base url in the constructor instead.', Chris@0: E_USER_DEPRECATED Chris@0: ); Chris@0: $this->removeScriptFromUrl = (bool) $remove; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function start() Chris@0: { Chris@0: $this->started = true; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function isStarted() Chris@0: { Chris@0: return $this->started; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function stop() Chris@0: { Chris@0: $this->reset(); Chris@0: $this->started = false; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function reset() Chris@0: { Chris@0: // Restarting the client resets the cookies and the history Chris@0: $this->client->restart(); Chris@0: $this->forms = array(); Chris@0: $this->serverParameters = array(); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function visit($url) Chris@0: { Chris@0: $this->client->request('GET', $this->prepareUrl($url), array(), array(), $this->serverParameters); Chris@0: $this->forms = array(); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function getCurrentUrl() Chris@0: { Chris@0: $request = $this->client->getInternalRequest(); Chris@0: Chris@0: if ($request === null) { Chris@0: throw new DriverException('Unable to access the request before visiting a page'); Chris@0: } Chris@0: Chris@0: return $request->getUri(); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function reload() Chris@0: { Chris@0: $this->client->reload(); Chris@0: $this->forms = array(); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function forward() Chris@0: { Chris@0: $this->client->forward(); Chris@0: $this->forms = array(); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function back() Chris@0: { Chris@0: $this->client->back(); Chris@0: $this->forms = array(); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function setBasicAuth($user, $password) Chris@0: { Chris@0: if (false === $user) { Chris@0: unset($this->serverParameters['PHP_AUTH_USER'], $this->serverParameters['PHP_AUTH_PW']); Chris@0: Chris@0: return; Chris@0: } Chris@0: Chris@0: $this->serverParameters['PHP_AUTH_USER'] = $user; Chris@0: $this->serverParameters['PHP_AUTH_PW'] = $password; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function setRequestHeader($name, $value) Chris@0: { Chris@0: $contentHeaders = array('CONTENT_LENGTH' => true, 'CONTENT_MD5' => true, 'CONTENT_TYPE' => true); Chris@0: $name = str_replace('-', '_', strtoupper($name)); Chris@0: Chris@0: // CONTENT_* are not prefixed with HTTP_ in PHP when building $_SERVER Chris@0: if (!isset($contentHeaders[$name])) { Chris@0: $name = 'HTTP_' . $name; Chris@0: } Chris@0: Chris@0: $this->serverParameters[$name] = $value; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function getResponseHeaders() Chris@0: { Chris@0: return $this->getResponse()->getHeaders(); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function setCookie($name, $value = null) Chris@0: { Chris@0: if (null === $value) { Chris@0: $this->deleteCookie($name); Chris@0: Chris@0: return; Chris@0: } Chris@0: Chris@0: $jar = $this->client->getCookieJar(); Chris@0: $jar->set(new Cookie($name, $value)); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Deletes a cookie by name. Chris@0: * Chris@0: * @param string $name Cookie name. Chris@0: */ Chris@0: private function deleteCookie($name) Chris@0: { Chris@0: $path = $this->getCookiePath(); Chris@0: $jar = $this->client->getCookieJar(); Chris@0: Chris@0: do { Chris@0: if (null !== $jar->get($name, $path)) { Chris@0: $jar->expire($name, $path); Chris@0: } Chris@0: Chris@0: $path = preg_replace('/.$/', '', $path); Chris@0: } while ($path); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Returns current cookie path. Chris@0: * Chris@0: * @return string Chris@0: */ Chris@0: private function getCookiePath() Chris@0: { Chris@0: $path = dirname(parse_url($this->getCurrentUrl(), PHP_URL_PATH)); Chris@0: Chris@0: if ('\\' === DIRECTORY_SEPARATOR) { Chris@0: $path = str_replace('\\', '/', $path); Chris@0: } Chris@0: Chris@0: return $path; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function getCookie($name) Chris@0: { Chris@0: // Note that the following doesn't work well because Chris@0: // Symfony\Component\BrowserKit\CookieJar stores cookies by name, Chris@0: // path, AND domain and if you don't fill them all in correctly then Chris@0: // you won't get the value that you're expecting. Chris@0: // Chris@0: // $jar = $this->client->getCookieJar(); Chris@0: // Chris@0: // if (null !== $cookie = $jar->get($name)) { Chris@0: // return $cookie->getValue(); Chris@0: // } Chris@0: Chris@0: $allValues = $this->client->getCookieJar()->allValues($this->getCurrentUrl()); Chris@0: Chris@0: if (isset($allValues[$name])) { Chris@0: return $allValues[$name]; Chris@0: } Chris@0: Chris@0: return null; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function getStatusCode() Chris@0: { Chris@0: return $this->getResponse()->getStatus(); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function getContent() Chris@0: { Chris@0: return $this->getResponse()->getContent(); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function findElementXpaths($xpath) Chris@0: { Chris@0: $nodes = $this->getCrawler()->filterXPath($xpath); Chris@0: Chris@0: $elements = array(); Chris@0: foreach ($nodes as $i => $node) { Chris@0: $elements[] = sprintf('(%s)[%d]', $xpath, $i + 1); Chris@0: } Chris@0: Chris@0: return $elements; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function getTagName($xpath) Chris@0: { Chris@0: return $this->getCrawlerNode($this->getFilteredCrawler($xpath))->nodeName; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function getText($xpath) Chris@0: { Chris@0: $text = $this->getFilteredCrawler($xpath)->text(); Chris@0: $text = str_replace("\n", ' ', $text); Chris@0: $text = preg_replace('/ {2,}/', ' ', $text); Chris@0: Chris@0: return trim($text); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function getHtml($xpath) Chris@0: { Chris@0: // cut the tag itself (making innerHTML out of outerHTML) Chris@0: return preg_replace('/^\<[^\>]+\>|\<[^\>]+\>$/', '', $this->getOuterHtml($xpath)); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function getOuterHtml($xpath) Chris@0: { Chris@0: $node = $this->getCrawlerNode($this->getFilteredCrawler($xpath)); Chris@0: Chris@0: return $node->ownerDocument->saveHTML($node); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function getAttribute($xpath, $name) Chris@0: { Chris@0: $node = $this->getFilteredCrawler($xpath); Chris@0: Chris@0: if ($this->getCrawlerNode($node)->hasAttribute($name)) { Chris@0: return $node->attr($name); Chris@0: } Chris@0: Chris@0: return null; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function getValue($xpath) Chris@0: { Chris@0: if (in_array($this->getAttribute($xpath, 'type'), array('submit', 'image', 'button'), true)) { Chris@0: return $this->getAttribute($xpath, 'value'); Chris@0: } Chris@0: Chris@0: $node = $this->getCrawlerNode($this->getFilteredCrawler($xpath)); Chris@0: Chris@0: if ('option' === $node->tagName) { Chris@0: return $this->getOptionValue($node); Chris@0: } Chris@0: Chris@0: try { Chris@0: $field = $this->getFormField($xpath); Chris@0: } catch (\InvalidArgumentException $e) { Chris@0: return $this->getAttribute($xpath, 'value'); Chris@0: } Chris@0: Chris@0: return $field->getValue(); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function setValue($xpath, $value) Chris@0: { Chris@0: $this->getFormField($xpath)->setValue($value); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function check($xpath) Chris@0: { Chris@0: $this->getCheckboxField($xpath)->tick(); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function uncheck($xpath) Chris@0: { Chris@0: $this->getCheckboxField($xpath)->untick(); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function selectOption($xpath, $value, $multiple = false) Chris@0: { Chris@0: $field = $this->getFormField($xpath); Chris@0: Chris@0: if (!$field instanceof ChoiceFormField) { Chris@0: 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: } Chris@0: Chris@0: if ($multiple) { Chris@0: $oldValue = (array) $field->getValue(); Chris@0: $oldValue[] = $value; Chris@0: $value = $oldValue; Chris@0: } Chris@0: Chris@0: $field->select($value); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function isSelected($xpath) Chris@0: { Chris@0: $optionValue = $this->getOptionValue($this->getCrawlerNode($this->getFilteredCrawler($xpath))); Chris@0: $selectField = $this->getFormField('(' . $xpath . ')/ancestor-or-self::*[local-name()="select"]'); Chris@0: $selectValue = $selectField->getValue(); Chris@0: Chris@0: return is_array($selectValue) ? in_array($optionValue, $selectValue, true) : $optionValue === $selectValue; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function click($xpath) Chris@0: { Chris@0: $crawler = $this->getFilteredCrawler($xpath); Chris@0: $node = $this->getCrawlerNode($crawler); Chris@0: $tagName = $node->nodeName; Chris@0: Chris@0: if ('a' === $tagName) { Chris@0: $this->client->click($crawler->link()); Chris@0: $this->forms = array(); Chris@0: } elseif ($this->canSubmitForm($node)) { Chris@0: $this->submit($crawler->form()); Chris@0: } elseif ($this->canResetForm($node)) { Chris@0: $this->resetForm($node); Chris@0: } else { Chris@0: $message = sprintf('%%s supports clicking on links and submit or reset buttons only. But "%s" provided', $tagName); Chris@0: Chris@0: throw new UnsupportedDriverActionException($message, $this); Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function isChecked($xpath) Chris@0: { Chris@0: $field = $this->getFormField($xpath); Chris@0: Chris@0: if (!$field instanceof ChoiceFormField || 'select' === $field->getType()) { Chris@0: 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: } Chris@0: Chris@0: if ('checkbox' === $field->getType()) { Chris@0: return $field->hasValue(); Chris@0: } Chris@0: Chris@0: $radio = $this->getCrawlerNode($this->getFilteredCrawler($xpath)); Chris@0: Chris@0: return $radio->getAttribute('value') === $field->getValue(); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function attachFile($xpath, $path) Chris@0: { Chris@0: $field = $this->getFormField($xpath); Chris@0: Chris@0: if (!$field instanceof FileFormField) { Chris@0: 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: } Chris@0: Chris@0: $field->upload($path); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function submitForm($xpath) Chris@0: { Chris@0: $crawler = $this->getFilteredCrawler($xpath); Chris@0: Chris@0: $this->submit($crawler->form()); Chris@0: } Chris@0: Chris@0: /** Chris@0: * @return Response Chris@0: * Chris@0: * @throws DriverException If there is not response yet Chris@0: */ Chris@0: protected function getResponse() Chris@0: { Chris@0: $response = $this->client->getInternalResponse(); Chris@0: Chris@0: if (null === $response) { Chris@0: throw new DriverException('Unable to access the response before visiting a page'); Chris@0: } Chris@0: Chris@0: return $response; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Prepares URL for visiting. Chris@0: * Removes "*.php/" from urls and then passes it to BrowserKitDriver::visit(). Chris@0: * Chris@0: * @param string $url Chris@0: * Chris@0: * @return string Chris@0: */ Chris@0: protected function prepareUrl($url) Chris@0: { Chris@0: $replacement = ($this->removeHostFromUrl ? '' : '$1') . ($this->removeScriptFromUrl ? '' : '$2'); Chris@0: Chris@0: return preg_replace('#(https?\://[^/]+)(/[^/\.]+\.php)?#', $replacement, $url); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Returns form field from XPath query. Chris@0: * Chris@0: * @param string $xpath Chris@0: * Chris@0: * @return FormField Chris@0: * Chris@0: * @throws DriverException Chris@0: */ Chris@0: protected function getFormField($xpath) Chris@0: { Chris@0: $fieldNode = $this->getCrawlerNode($this->getFilteredCrawler($xpath)); Chris@0: $fieldName = str_replace('[]', '', $fieldNode->getAttribute('name')); Chris@0: Chris@0: $formNode = $this->getFormNode($fieldNode); Chris@0: $formId = $this->getFormNodeId($formNode); Chris@0: Chris@0: if (!isset($this->forms[$formId])) { Chris@0: $this->forms[$formId] = new Form($formNode, $this->getCurrentUrl()); Chris@0: } Chris@0: Chris@0: if (is_array($this->forms[$formId][$fieldName])) { Chris@0: return $this->forms[$formId][$fieldName][$this->getFieldPosition($fieldNode)]; Chris@0: } Chris@0: Chris@0: return $this->forms[$formId][$fieldName]; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Returns the checkbox field from xpath query, ensuring it is valid. Chris@0: * Chris@0: * @param string $xpath Chris@0: * Chris@0: * @return ChoiceFormField Chris@0: * Chris@0: * @throws DriverException when the field is not a checkbox Chris@0: */ Chris@0: private function getCheckboxField($xpath) Chris@0: { Chris@0: $field = $this->getFormField($xpath); Chris@0: Chris@0: if (!$field instanceof ChoiceFormField) { Chris@0: throw new DriverException(sprintf('Impossible to check the element with XPath "%s" as it is not a checkbox', $xpath)); Chris@0: } Chris@0: Chris@0: return $field; Chris@0: } Chris@0: Chris@0: /** Chris@0: * @param \DOMElement $element Chris@0: * Chris@0: * @return \DOMElement Chris@0: * Chris@0: * @throws DriverException if the form node cannot be found Chris@0: */ Chris@0: private function getFormNode(\DOMElement $element) Chris@0: { Chris@0: if ($element->hasAttribute('form')) { Chris@0: $formId = $element->getAttribute('form'); Chris@0: $formNode = $element->ownerDocument->getElementById($formId); Chris@0: Chris@0: if (null === $formNode || 'form' !== $formNode->nodeName) { Chris@0: throw new DriverException(sprintf('The selected node has an invalid form attribute (%s).', $formId)); Chris@0: } Chris@0: Chris@0: return $formNode; Chris@0: } Chris@0: Chris@0: $formNode = $element; Chris@0: Chris@0: do { Chris@0: // use the ancestor form element Chris@0: if (null === $formNode = $formNode->parentNode) { Chris@0: throw new DriverException('The selected node does not have a form ancestor.'); Chris@0: } Chris@0: } while ('form' !== $formNode->nodeName); Chris@0: Chris@0: return $formNode; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Gets the position of the field node among elements with the same name Chris@0: * Chris@0: * BrowserKit uses the field name as index to find the field in its Form object. Chris@0: * When multiple fields have the same name (checkboxes for instance), it will return Chris@0: * an array of elements in the order they appear in the DOM. Chris@0: * Chris@0: * @param \DOMElement $fieldNode Chris@0: * Chris@0: * @return integer Chris@0: */ Chris@0: private function getFieldPosition(\DOMElement $fieldNode) Chris@0: { Chris@0: $elements = $this->getCrawler()->filterXPath('//*[@name=\''.$fieldNode->getAttribute('name').'\']'); Chris@0: Chris@0: if (count($elements) > 1) { Chris@0: // more than one element contains this name ! Chris@0: // so we need to find the position of $fieldNode Chris@0: foreach ($elements as $key => $element) { Chris@0: /** @var \DOMElement $element */ Chris@0: if ($element->getNodePath() === $fieldNode->getNodePath()) { Chris@0: return $key; Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: return 0; Chris@0: } Chris@0: Chris@0: private function submit(Form $form) Chris@0: { Chris@0: $formId = $this->getFormNodeId($form->getFormNode()); Chris@0: Chris@0: if (isset($this->forms[$formId])) { Chris@0: $this->mergeForms($form, $this->forms[$formId]); Chris@0: } Chris@0: Chris@0: // remove empty file fields from request Chris@0: foreach ($form->getFiles() as $name => $field) { Chris@0: if (empty($field['name']) && empty($field['tmp_name'])) { Chris@0: $form->remove($name); Chris@0: } Chris@0: } Chris@0: Chris@0: foreach ($form->all() as $field) { Chris@0: // Add a fix for https://github.com/symfony/symfony/pull/10733 to support Symfony versions which are not fixed Chris@0: if ($field instanceof TextareaFormField && null === $field->getValue()) { Chris@0: $field->setValue(''); Chris@0: } Chris@0: } Chris@0: Chris@0: $this->client->submit($form); Chris@0: Chris@0: $this->forms = array(); Chris@0: } Chris@0: Chris@0: private function resetForm(\DOMElement $fieldNode) Chris@0: { Chris@0: $formNode = $this->getFormNode($fieldNode); Chris@0: $formId = $this->getFormNodeId($formNode); Chris@0: unset($this->forms[$formId]); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Determines if a node can submit a form. Chris@0: * Chris@0: * @param \DOMElement $node Node. Chris@0: * Chris@0: * @return boolean Chris@0: */ Chris@0: private function canSubmitForm(\DOMElement $node) Chris@0: { Chris@0: $type = $node->hasAttribute('type') ? $node->getAttribute('type') : null; Chris@0: Chris@0: if ('input' === $node->nodeName && in_array($type, array('submit', 'image'), true)) { Chris@0: return true; Chris@0: } Chris@0: Chris@0: return 'button' === $node->nodeName && (null === $type || 'submit' === $type); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Determines if a node can reset a form. Chris@0: * Chris@0: * @param \DOMElement $node Node. Chris@0: * Chris@0: * @return boolean Chris@0: */ Chris@0: private function canResetForm(\DOMElement $node) Chris@0: { Chris@0: $type = $node->hasAttribute('type') ? $node->getAttribute('type') : null; Chris@0: Chris@0: return in_array($node->nodeName, array('input', 'button'), true) && 'reset' === $type; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Returns form node unique identifier. Chris@0: * Chris@0: * @param \DOMElement $form Chris@0: * Chris@0: * @return string Chris@0: */ Chris@0: private function getFormNodeId(\DOMElement $form) Chris@0: { Chris@0: return md5($form->getLineNo() . $form->getNodePath() . $form->nodeValue); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Gets the value of an option element Chris@0: * Chris@0: * @param \DOMElement $option Chris@0: * Chris@0: * @return string Chris@0: * Chris@0: * @see \Symfony\Component\DomCrawler\Field\ChoiceFormField::buildOptionValue Chris@0: */ Chris@0: private function getOptionValue(\DOMElement $option) Chris@0: { Chris@0: if ($option->hasAttribute('value')) { Chris@0: return $option->getAttribute('value'); Chris@0: } Chris@0: Chris@0: if (!empty($option->nodeValue)) { Chris@0: return $option->nodeValue; Chris@0: } Chris@0: Chris@0: return '1'; // DomCrawler uses 1 by default if there is no text in the option Chris@0: } Chris@0: Chris@0: /** Chris@0: * Merges second form values into first one. Chris@0: * Chris@0: * @param Form $to merging target Chris@0: * @param Form $from merging source Chris@0: */ Chris@0: private function mergeForms(Form $to, Form $from) Chris@0: { Chris@0: foreach ($from->all() as $name => $field) { Chris@0: $fieldReflection = new \ReflectionObject($field); Chris@0: $nodeReflection = $fieldReflection->getProperty('node'); Chris@0: $valueReflection = $fieldReflection->getProperty('value'); Chris@0: Chris@0: $nodeReflection->setAccessible(true); Chris@0: $valueReflection->setAccessible(true); Chris@0: Chris@0: $isIgnoredField = $field instanceof InputFormField && Chris@0: in_array($nodeReflection->getValue($field)->getAttribute('type'), array('submit', 'button', 'image'), true); Chris@0: Chris@0: if (!$isIgnoredField) { Chris@0: $valueReflection->setValue($to[$name], $valueReflection->getValue($field)); Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Returns DOMElement from crawler instance. Chris@0: * Chris@0: * @param Crawler $crawler Chris@0: * Chris@0: * @return \DOMElement Chris@0: * Chris@0: * @throws DriverException when the node does not exist Chris@0: */ Chris@0: private function getCrawlerNode(Crawler $crawler) Chris@0: { Chris@0: $node = null; Chris@0: Chris@0: if ($crawler instanceof \Iterator) { Chris@0: // for symfony 2.3 compatibility as getNode is not public before symfony 2.4 Chris@0: $crawler->rewind(); Chris@0: $node = $crawler->current(); Chris@0: } else { Chris@0: $node = $crawler->getNode(0); Chris@0: } Chris@0: Chris@0: if (null !== $node) { Chris@0: return $node; Chris@0: } Chris@0: Chris@0: throw new DriverException('The element does not exist'); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Returns a crawler filtered for the given XPath, requiring at least 1 result. Chris@0: * Chris@0: * @param string $xpath Chris@0: * Chris@0: * @return Crawler Chris@0: * Chris@0: * @throws DriverException when no matching elements are found Chris@0: */ Chris@0: private function getFilteredCrawler($xpath) Chris@0: { Chris@0: if (!count($crawler = $this->getCrawler()->filterXPath($xpath))) { Chris@0: throw new DriverException(sprintf('There is no element matching XPath "%s"', $xpath)); Chris@0: } Chris@0: Chris@0: return $crawler; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Returns crawler instance (got from client). Chris@0: * Chris@0: * @return Crawler Chris@0: * Chris@0: * @throws DriverException Chris@0: */ Chris@0: private function getCrawler() Chris@0: { Chris@0: $crawler = $this->client->getCrawler(); Chris@0: Chris@0: if (null === $crawler) { Chris@0: throw new DriverException('Unable to access the response content before visiting a page'); Chris@0: } Chris@0: Chris@0: return $crawler; Chris@0: } Chris@0: }