diff --git a/src/Symfony/Components/DomCrawler/Crawler.php b/src/Symfony/Components/DomCrawler/Crawler.php new file mode 100644 index 000000000000..99acb21f02a9 --- /dev/null +++ b/src/Symfony/Components/DomCrawler/Crawler.php @@ -0,0 +1,640 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Crawler eases navigation of a list of \DOMNode objects. + * + * @package Symfony + * @subpackage Components_DomCrawler + * @author Fabien Potencier + */ +class Crawler extends \SplObjectStorage +{ + protected $uri; + protected $host; + protected $path; + + /** + * Constructor. + * + * @param mixed $node A Node to use as the base for the crawling + * @param string $uri The base URI to use for absolute links or form actions + */ + public function __construct($node = null, $uri = null) + { + $this->uri = $uri; + list($this->host, $this->path) = $this->parseUri($this->uri); + + $this->add($node); + } + + /** + * Removes all the nodes. + */ + public function clear() + { + $this->removeAll($this); + } + + /** + * Adds a node to the current list of nodes. + * + * This method uses the appropriate specialized add*() method based + * on the type of the argument. + * + * @param null|\DOMNodeList|array|\DOMNode $node A node + */ + public function add($node) + { + if ($node instanceof \DOMNodeList) + { + $this->addNodeList($node); + } + elseif (is_array($node)) + { + $this->addNodes($node); + } + elseif (null !== $node) + { + $this->addNode($node); + } + } + + /** + * Adds an HTML content to the list of nodes. + * + * @param string $content The HTML content + * @param string $charset The charset + */ + public function addHtmlContent($content, $charset = 'UTF-8') + { + $dom = new \DOMDocument('1.0', $charset); + $dom->validateOnParse = true; + + @$dom->loadHTML($content); + $this->addDocument($dom); + } + + /** + * Adds an XML content to the list of nodes. + * + * @param string $content The XML content + * @param string $charset The charset + */ + public function addXmlContent($content, $charset = 'UTF-8') + { + $dom = new \DOMDocument('1.0', $charset); + $dom->validateOnParse = true; + + // remove the default namespace to make XPath expressions simpler + @$dom->loadXML(str_replace('xmlns', 'ns', $content)); + $this->addDocument($dom); + } + + /** + * Adds a \DOMDocument to the list of nodes. + * + * @param \DOMDocument $dom A \DOMDocument instance + */ + public function addDocument(\DOMDocument $dom) + { + if ($dom->documentElement) + { + $this->addNode($dom->documentElement); + } + } + + /** + * Adds a \DOMNodeList to the list of nodes. + * + * @param \DOMNodeList $nodes A \DOMNodeList instance + */ + public function addNodeList(\DOMNodeList $nodes) + { + foreach ($nodes as $node) + { + $this->addNode($node); + } + } + + /** + * Adds an array of \DOMNode instances to the list of nodes. + * + * @param array $nodes An array of \DOMNode instances + */ + public function addNodes(array $nodes) + { + foreach ($nodes as $node) + { + $this->add($node); + } + } + + /** + * Adds a \DOMNode instance to the list of nodes. + * + * @param \DOMNode $node A \DOMNode instance + */ + public function addNode(\DOMNode $node) + { + if ($node instanceof \DOMDocument) + { + $this->attach($node->documentElement); + } + else + { + $this->attach($node); + } + } + + /** + * Returns true if the list of nodes is empty. + * + * @return Boolean true if the list of nodes is empty, false otherwise + */ + public function isEmpty() + { + return $this->count() < 1; + } + + /** + * Returns a node given its position in the node list. + * + * @param integer $position The position + * + * @return A new instance of the Crawler with the selected node, or an empty Crawler if it does not exist. + */ + public function eq($position) + { + foreach ($this as $i => $node) + { + if ($i == $position) + { + return new static($node, $this->uri); + } + } + + return new static(null, $this->uri); + } + + /** + * Calls an anonymous function on each node of the list. + * + * The anonymous function receives the position and the node as arguments. + * + * Example: + * + * $crawler->filter('h1')->each(function ($i, $node) + * { + * return $node->nodeValue; + * }); + * + * @param \Closure $closure An anonymous function + * + * @return array An array of values returned by the anonymous function + */ + public function each(\Closure $closure) + { + $data = array(); + foreach ($this as $i => $node) + { + $data[] = $closure($node, $i); + } + + return $data; + } + + /** + * Reduces the list of nodes by calling an anonymous function. + * + * To remove a node from the list, the anonymous function must return false. + * + * @param \Closure $closure An anonymous function + * + * @param Crawler A Crawler instance with the selected nodes. + */ + public function reduce(\Closure $closure) + { + $nodes = array(); + foreach ($this as $i => $node) + { + if (false !== $closure($node, $i)) + { + $nodes[] = $node; + } + } + + return new static($nodes, $this->uri); + } + + /** + * Returns the first node of the current selection + * + * @return Crawler A Crawler instance with the first selected node + */ + public function first() + { + return $this->eq(0); + } + + /** + * Returns the last node of the current selection + * + * @return Crawler A Crawler instance with the last selected node + */ + public function last() + { + return $this->eq($this->count() - 1); + } + + /** + * Returns the siblings nodes of the current selection + * + * @return Crawler A Crawler instance with the sibling nodes + */ + public function siblings() + { + if (!count($this)) + { + throw new \InvalidArgumentException('The current node list is empty.'); + } + + return new static($this->sibling($this->getNode(0)->parentNode->firstChild), $this->uri); + } + + /** + * Returns the next siblings nodes of the current selection + * + * @return Crawler A Crawler instance with the next sibling nodes + */ + public function nextAll() + { + if (!count($this)) + { + throw new \InvalidArgumentException('The current node list is empty.'); + } + + return new static($this->sibling($this->getNode(0)), $this->uri); + } + + /** + * Returns the previous sibling nodes of the current selection + * + * @return Crawler A Crawler instance with the previous sibling nodes + */ + public function previousAll() + { + if (!count($this)) + { + throw new \InvalidArgumentException('The current node list is empty.'); + } + + return new static($this->sibling($this->getNode(0), 'previousSibling'), $this->uri); + } + + /** + * Returns the parents nodes of the current selection + * + * @return Crawler A Crawler instance with the parents nodes of the current selection + */ + public function parents() + { + if (!count($this)) + { + throw new \InvalidArgumentException('The current node list is empty.'); + } + + $node = $this->getNode(0); + $nodes = array(); + + while ($node = $node->parentNode) + { + if (1 === $node->nodeType && '_root' !== $node->nodeName) + { + $nodes[] = $node; + } + } + + return new static($nodes, $this->uri); + } + + /** + * Returns the children nodes of the current selection + * + * @return Crawler A Crawler instance with the chidren nodes + */ + public function children() + { + if (!count($this)) + { + throw new \InvalidArgumentException('The current node list is empty.'); + } + + return new static($this->sibling($this->getNode(0)->firstChild), $this->uri); + } + + /** + * Returns the attribute value of the first node of the list. + * + * @param string $attribute The attribute name + * + * @return string The attribute value + */ + public function attr($attribute) + { + if (!count($this)) + { + throw new \InvalidArgumentException('The current node list is empty.'); + } + + return $this->getNode(0)->getAttribute($attribute); + } + + /** + * Returns the node value of the first node of the list. + * + * @return string The node value + */ + public function text() + { + if (!count($this)) + { + throw new \InvalidArgumentException('The current node list is empty.'); + } + + return $this->getNode(0)->nodeValue; + } + + /** + * Extracts information from the list of nodes. + * + * You can extract attributes or/and the node value (_text). + * + * Example: + * + * $crawler->filter('h1 a')->extract(array('_text', 'href')); + * + * @param array $attributes An array of attributes + * + * @param array An array of extracted values + */ + public function extract($attributes) + { + if (!is_array($attributes)) + { + $attributes = array($attributes); + } + + $data = array(); + foreach ($this as $node) + { + $elements = array(); + foreach ($attributes as $attribute) + { + if ('_text' === $attribute) + { + $elements[] = $node->nodeValue; + } + else + { + $elements[] = $node->getAttribute($attribute); + } + } + + $data[] = count($attributes) > 1 ? $elements : $elements[0]; + } + + return $data; + } + + /** + * Filters the list of nodes with an XPath expression. + * + * @param string $xpath An XPath expression + * + * @return Crawler A new instance of Crawler with the filtered list of nodes + */ + public function filterXPath($xpath) + { + $document = new \DOMDocument('1.0', 'UTF-8'); + $root = $document->appendChild($document->createElement('_root')); + foreach ($this as $node) + { + $root->appendChild($document->importNode($node, true)); + } + + $domxpath = new \DOMXPath($document); + + return new static($domxpath->query($xpath), $this->uri); + } + + /** + * Filters the list of nodes with a CSS selector. + * + * This method only works if you have installed the CssSelector Symfony Component. + * + * @param string $selector A CSS selector + * + * @return Crawler A new instance of Crawler with the filtered list of nodes + * + * @throws \RuntimeException if the CssSelector Component is not available + */ + public function filter($selector) + { + if (!class_exists('Symfony\\Components\\CssSelector\\Parser')) + { + // @codeCoverageIgnoreStart + throw new \RuntimeException('Unable to filter with a CSS selector as the Symfony CssSelector is not installed (you can use filterXPath instead).'); + // @codeCoverageIgnoreEnd + } + + return $this->filterXPath(CssParser::cssToXpath($selector)); + } + + /** + * Selects links by name or alt value for clickable images. + * + * @param string $value The link text + * + * @return Crawler A new instance of Crawler with the filtered list of nodes + */ + public function selectLink($value) + { + $xpath = sprintf('//a[contains(concat(\' \', normalize-space(string(.)), \' \'), %s)] ', static::xpathLiteral(' '.$value.' ')). + sprintf('| //a/img[contains(concat(\' \', normalize-space(string(@alt)), \' \'), %s)]/ancestor::a', static::xpathLiteral(' '.$value.' ')); + + return $this->filterXPath($xpath); + } + + /** + * Selects a button by name or alt value for images. + * + * @param string $value The button text + * + * @return Crawler A new instance of Crawler with the filtered list of nodes + */ + public function selectButton($value) + { + $xpath = sprintf('//input[((@type="submit" or @type="button") and contains(concat(\' \', normalize-space(string(@value)), \' \'), %s)) ', static::xpathLiteral(' '.$value.' ')). + sprintf('or (@type="image" and contains(concat(\' \', normalize-space(string(@alt)), \' \'), %s)) or @id="%s" or @name="%s"] ', static::xpathLiteral(' '.$value.' '), $value, $value). + sprintf('| //button[contains(concat(\' \', normalize-space(string(.)), \' \'), %s) or @id="%s" or @name="%s"]', static::xpathLiteral(' '.$value.' '), $value, $value); + + return $this->filterXPath($xpath); + } + + /** + * Returns a Link object for the first node in the list. + * + * @param string $method The method for the link (get by default) + * + * @return Link A Link instance + * + * @throws \InvalidArgumentException If the current node list is empty + */ + public function link($method = 'get') + { + if (!count($this)) + { + throw new \InvalidArgumentException('The current node list is empty.'); + } + + $node = $this->getNode(0); + + return new Link($node, $method, $this->host, $this->path); + } + + /** + * Returns an array of Link objects for the nodes in the list. + * + * @return array An array of Link instances + */ + public function links() + { + $links = array(); + foreach ($this as $node) + { + $links[] = new Link($node, 'get', $this->host, $this->path); + } + + return $links; + } + + /** + * Returns a Form object for the first node in the list. + * + * @param array $arguments An array of values for the form fields + * @param string $method The method for the form + * + * @return Form A Form instance + * + * @throws \InvalidArgumentException If the current node list is empty + */ + public function form(array $values = null, $method = null) + { + if (!count($this)) + { + throw new \InvalidArgumentException('The current node list is empty.'); + } + + $form = new Form($this->getNode(0), $method, $this->host, $this->path); + + if (null !== $values) + { + $form->setValues($values); + } + + return $form; + } + + protected function getNode($position) + { + foreach ($this as $i => $node) + { + if ($i == $position) + { + return $node; + } + // @codeCoverageIgnoreStart + } + + return null; + // @codeCoverageIgnoreEnd + } + + protected function parseUri($uri) + { + if ('http' !== substr($uri, 0, 4)) + { + return array(null, '/'); + } + + $path = parse_url($uri, PHP_URL_PATH); + + if ('/' !== substr($path, -1)) + { + $path = dirname($path); + } + + return array(preg_replace('#^(.*?//[^/]+)\/.*$#', '$1', $uri), $path); + } + + protected function sibling($node, $siblingDir = 'nextSibling') + { + $nodes = array(); + + do + { + if ($node !== $this->getNode(0) && $node->nodeType === 1) + { + $nodes[] = $node; + } + } + while($node = $node->$siblingDir); + + return $nodes; + } + + static public function xpathLiteral($s) + { + if (false === strpos($s, "'")) + { + return sprintf("'%s'", $s); + } + + if (false === strpos($s, '"')) + { + return sprintf('"%s"', $s); + } + + $string = $s; + $parts = array(); + while (true) + { + if (false !== $pos = strpos($string, "'")) + { + $parts[] = sprintf("'%s'", substr($string, 0, $pos)); + $parts[] = "\"'\""; + $string = substr($string, $pos + 1); + } + else + { + $parts[] = "'$string'"; + break; + } + } + + return sprintf("concat(%s)", implode($parts, ', ')); + } +} diff --git a/src/Symfony/Components/DomCrawler/Field/ChoiceFormField.php b/src/Symfony/Components/DomCrawler/Field/ChoiceFormField.php new file mode 100644 index 000000000000..59173017b6ed --- /dev/null +++ b/src/Symfony/Components/DomCrawler/Field/ChoiceFormField.php @@ -0,0 +1,208 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * ChoiceFormField represents a choice form field. + * + * It is constructed from a HTML select tag, or a HTML checkbox, or radio inputs. + * + * @package Symfony + * @subpackage Components_DomCrawler + * @author Fabien Potencier + */ +class ChoiceFormField extends FormField +{ + protected $type; + protected $multiple; + protected $options; + + /** + * Returns true if the field should be included in the submitted values. + * + * @return Boolean true if the field should be included in the submitted values, false otherwise + */ + public function hasValue() + { + // don't send a value for unchecked checkboxes + if (in_array($this->type, array('checkbox', 'radio')) && null === $this->value) + { + return false; + } + + return true; + } + + /** + * Sets the value of the field. + * + * @param string $value The value of the field + */ + public function setValue($value) + { + if ('checkbox' == $this->type && false === $value) + { + // uncheck + $this->value = null; + } + elseif ('checkbox' == $this->type && true === $value) + { + // check + $this->value = $this->options[0]; + } + else + { + if (is_array($value)) + { + if (!$this->multiple) + { + throw new \InvalidArgumentException(sprintf('The value for "%s" cannot be an array.', $this->name)); + } + + foreach ($value as $v) + { + if (!in_array($v, $this->options)) + { + throw new \InvalidArgumentException(sprintf('Input "%s" cannot take "%s" as a value (possible values: %s).', $this->name, $v, implode(', ', $this->options))); + } + } + } + elseif (!in_array($value, $this->options)) + { + throw new \InvalidArgumentException(sprintf('Input "%s" cannot take "%s" as a value (possible values: %s).', $this->name, $value, implode(', ', $this->options))); + } + + if ($this->multiple && !is_array($value)) + { + $value = array($value); + } + + if (is_array($value)) + { + $this->value = $value; + } + else + { + parent::setValue($value); + } + } + } + + /** + * Adds a choice to the current ones. + * + * This method should only be used internally. + * + * @param \DOMNode $node A \DOMNode + */ + public function addChoice(\DOMNode $node) + { + if (!$this->multiple && 'radio' != $this->type) + { + throw new \LogicException(sprintf('Unable to add a choice for "%s" as it is not multiple or is not a radio button.', $this->name)); + } + + $this->options[] = $value = $node->hasAttribute('value') ? $node->getAttribute('value') : '1'; + + if ($node->getAttribute('checked')) + { + $this->value = $value; + } + } + + /** + * Returns the type of the choice field (radio, select, or checkbox). + * + * @return string The type + */ + public function getType() + { + return $this->type; + } + + /** + * Returns true if the field accepts multiple values. + * + * @return Boolean true if the field accepts multiple values, false otherwise + */ + public function isMultiple() + { + return $this->multiple; + } + + /** + * Initializes the form field. + */ + protected function initialize() + { + if ('input' != $this->node->nodeName && 'select' != $this->node->nodeName) + { + throw new \LogicException(sprintf('A ChoiceFormField can only be created from an input or select tag (%s given).', $this->node->nodeName)); + } + + if ('input' == $this->node->nodeName && 'checkbox' != $this->node->getAttribute('type') && 'radio' != $this->node->getAttribute('type')) + { + throw new \LogicException(sprintf('A ChoiceFormField can only be created from an input tag with a type of checkbox or radio (given type is %s).', $this->node->getAttribute('type'))); + } + + $this->value = null; + $this->options = array(); + $this->multiple = false; + + if ('input' == $this->node->nodeName) + { + $this->type = $this->node->getAttribute('type'); + $this->options[] = $value = $this->node->hasAttribute('value') ? $this->node->getAttribute('value') : '1'; + + if ($this->node->getAttribute('checked')) + { + $this->value = $value; + } + } + else + { + $this->type = 'select'; + if ($this->node->hasAttribute('multiple')) + { + $this->multiple = true; + $this->value = array(); + $this->name = str_replace('[]', '', $this->name); + } + + $found = false; + foreach ($this->xpath->query('descendant::option', $this->node) as $option) + { + $this->options[] = $option->getAttribute('value'); + + if ($option->getAttribute('selected')) + { + $found = true; + if ($this->multiple) + { + $this->value[] = $option->getAttribute('value'); + } + else + { + $this->value = $option->getAttribute('value'); + } + } + } + + // if no option is selected and if it is a simple select box, take the first option as the value + $option = $this->xpath->query('descendant::option', $this->node)->item(0); + if (!$found && !$this->multiple && $option instanceof \DOMElement) + { + $this->value = $option->getAttribute('value'); + } + } + } +} diff --git a/src/Symfony/Components/DomCrawler/Field/FileFormField.php b/src/Symfony/Components/DomCrawler/Field/FileFormField.php new file mode 100644 index 000000000000..40f9ee16bc84 --- /dev/null +++ b/src/Symfony/Components/DomCrawler/Field/FileFormField.php @@ -0,0 +1,78 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * FileFormField represents a file form field (an HTML file input tag). + * + * @package Symfony + * @subpackage Components_DomCrawler + * @author Fabien Potencier + */ +class FileFormField extends FormField +{ + /** + * Sets the PHP error code associated with the field. + * + * @param integer $error The error code (one of UPLOAD_ERR_INI_SIZE, UPLOAD_ERR_FORM_SIZE, UPLOAD_ERR_PARTIAL, UPLOAD_ERR_NO_FILE, UPLOAD_ERR_NO_TMP_DIR, UPLOAD_ERR_CANT_WRITE, or UPLOAD_ERR_EXTENSION) + */ + public function setErrorCode($error) + { + $codes = array(UPLOAD_ERR_INI_SIZE, UPLOAD_ERR_FORM_SIZE, UPLOAD_ERR_PARTIAL, UPLOAD_ERR_NO_FILE, UPLOAD_ERR_NO_TMP_DIR, UPLOAD_ERR_CANT_WRITE, UPLOAD_ERR_EXTENSION); + if (!in_array($error, $codes)) + { + throw new \InvalidArgumentException(sprintf('The error code %s is not valid.', $error)); + } + + $this->value = array('name' => '', 'type' => '', 'tmp_name' => '', 'error' => $error, 'size' => 0); + } + + /** + * Sets the value of the field. + * + * @param string $value The value of the field + */ + public function setValue($value) + { + if (null !== $value && is_readable($value)) + { + $error = UPLOAD_ERR_OK; + $size = filesize($value); + } + else + { + $error = UPLOAD_ERR_NO_FILE; + $size = 0; + $value = ''; + } + + $this->value = array('name' => basename($value), 'type' => '', 'tmp_name' => $value, 'error' => $error, 'size' => $size); + } + + /** + * Initializes the form field. + */ + protected function initialize() + { + if ('input' != $this->node->nodeName) + { + throw new \LogicException(sprintf('A FileFormField can only be created from an input tag (%s given).', $this->node->nodeName)); + } + + if ('file' != $this->node->getAttribute('type')) + { + throw new \LogicException(sprintf('A FileFormField can only be created from an input tag with a type of file (given type is %s).', $this->node->getAttribute('type'))); + } + + $this->setValue(null); + } +} diff --git a/src/Symfony/Components/DomCrawler/Field/FormField.php b/src/Symfony/Components/DomCrawler/Field/FormField.php new file mode 100644 index 000000000000..f5491f551262 --- /dev/null +++ b/src/Symfony/Components/DomCrawler/Field/FormField.php @@ -0,0 +1,93 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * FormField is the abstract class for all form fields. + * + * @package Symfony + * @subpackage Components_DomCrawler + * @author Fabien Potencier + */ +abstract class FormField +{ + protected $node; + protected $name; + protected $value; + protected $document; + protected $xpath; + + /** + * Constructor. + * + * @param \DOMNode $node The node associated with this field + */ + public function __construct(\DOMNode $node) + { + $this->node = $node; + $this->name = $node->getAttribute('name'); + + $this->document = new \DOMDocument('1.0', 'UTF-8'); + $this->node = $this->document->importNode($this->node, true); + + $root = $this->document->appendChild($this->document->createElement('_root')); + $root->appendChild($this->node); + $this->xpath = new \DOMXPath($this->document); + + $this->initialize(); + } + + /** + * Returns the name of the field. + * + * @return string The name of the field + */ + public function getName() + { + return $this->name; + } + + /** + * Gets the value of the field. + * + * @return string|array The value of the field + */ + public function getValue() + { + return $this->value; + } + + /** + * Sets the value of the field. + * + * @param string $value The value of the field + */ + public function setValue($value) + { + $this->value = (string) $value; + } + + /** + * Returns true if the field should be included in the submitted values. + * + * @return Boolean true if the field should be included in the submitted values, false otherwise + */ + public function hasValue() + { + return true; + } + + /** + * Initializes the form field. + */ + abstract protected function initialize(); +} diff --git a/src/Symfony/Components/DomCrawler/Field/InputFormField.php b/src/Symfony/Components/DomCrawler/Field/InputFormField.php new file mode 100644 index 000000000000..d15b6c4df1f1 --- /dev/null +++ b/src/Symfony/Components/DomCrawler/Field/InputFormField.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * InputFormField represents an input form field (an HTML input tag). + * + * For inputs with type of file, checkbox, or radio, there are other more + * specialized classes (cf. FileFormField and ChoiceFormField). + * + * @package Symfony + * @subpackage Components_DomCrawler + * @author Fabien Potencier + */ +class InputFormField extends FormField +{ + /** + * Initializes the form field. + */ + protected function initialize() + { + if ('input' != $this->node->nodeName) + { + throw new \LogicException(sprintf('An InputFormField can only be created from an input tag (%s given).', $this->node->nodeName)); + } + + if ('checkbox' == $this->node->getAttribute('type')) + { + throw new \LogicException('Checkboxes should be instances of ChoiceFormField.'); + } + + if ('file' == $this->node->getAttribute('type')) + { + throw new \LogicException('File inputs should be instances of FileFormField.'); + } + + $this->value = $this->node->getAttribute('value'); + } +} diff --git a/src/Symfony/Components/DomCrawler/Field/TextareaFormField.php b/src/Symfony/Components/DomCrawler/Field/TextareaFormField.php new file mode 100644 index 000000000000..cb93f0702fcf --- /dev/null +++ b/src/Symfony/Components/DomCrawler/Field/TextareaFormField.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * TextareaFormField represents a textarea form field (an HTML textarea tag). + * + * @package Symfony + * @subpackage Components_DomCrawler + * @author Fabien Potencier + */ +class TextareaFormField extends FormField +{ + /** + * Initializes the form field. + */ + protected function initialize() + { + if ('textarea' != $this->node->nodeName) + { + throw new \LogicException(sprintf('A TextareaFormField can only be created from a textarea tag (%s given).', $this->node->nodeName)); + } + + $this->value = null; + foreach ($this->node->childNodes as $node) + { + $this->value .= $this->document->saveXML($node); + } + } +} diff --git a/src/Symfony/Components/DomCrawler/Form.php b/src/Symfony/Components/DomCrawler/Form.php new file mode 100644 index 000000000000..0934278ab564 --- /dev/null +++ b/src/Symfony/Components/DomCrawler/Form.php @@ -0,0 +1,363 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Form represents an HTML form. + * + * @package Symfony + * @subpackage Components_DomCrawler + * @author Fabien Potencier + */ +class Form +{ + protected $document; + protected $button; + protected $node; + protected $fields; + protected $method; + protected $host; + protected $path; + + /** + * Constructor. + * + * @param \DOMNode $node A \DOMNode instance + * @param string $method The method to use for the link (if null, it defaults to the method defined by the form) + * @param string $host The base URI to use for absolute links (like http://localhost) + * @param string $path The base path for relative links (/ by default) + * + * @throws \LogicException if the node is not a button inside a form tag + */ + public function __construct(\DOMNode $node, $method = null, $host = null, $path = '/') + { + $this->button = $node; + if ('button' == $node->nodeName || ('input' == $node->nodeName && in_array($node->getAttribute('type'), array('submit', 'button', 'image')))) + { + do + { + // use the ancestor form element + if (null === $node = $node->parentNode) + { + throw new \LogicException('The selected node does not have a form ancestor.'); + } + } + while ('form' != $node->nodeName); + } + else + { + throw new \LogicException(sprintf('Unable to submit on a "%s" tag.', $node->nodeName)); + } + $this->node = $node; + $this->method = $method; + $this->host = $host; + $this->path = empty($path) ? '/' : $path; + + $this->initialize(); + } + + /** + * Gets the form node associated with this form. + * + * @return \DOMNode $node A \DOMNode instance + */ + public function getFormNode() + { + return $this->node; + } + + /** + * Gets the value of a field. + * + * @param string $name The field name + * + * @throws \InvalidArgumentException if the field does not exist + */ + public function getValue($name) + { + if (!$this->hasField($name)) + { + throw new \InvalidArgumentException(sprintf('The form field "%s" does not exist', $name)); + } + + return $this->fields[$name]->getValue(); + } + + /** + * Sets the value of a field. + * + * @param string $name The field name + * @param string|array $value The value of the field + * + * @throws \InvalidArgumentException if the field does not exist + */ + public function setValue($name, $value) + { + if (!$this->hasField($name)) + { + throw new \InvalidArgumentException(sprintf('The form field "%s" does not exist', $name)); + } + + $this->fields[$name]->setValue($value); + + return $this; + } + + /** + * Sets the value of the fields. + * + * @param array $values An array of field values + */ + public function setValues(array $values) + { + foreach ($values as $name => $value) + { + $this->setValue($name, $value); + } + + return $this; + } + + /** + * Gets the field values. + * + * The returned array does not include file fields (@see getFiles). + * + * @return array An array of field values. + */ + public function getValues() + { + $values = array(); + foreach ($this->fields as $name => $field) + { + if (!$field instanceof Field\FileFormField && $field->hasValue()) + { + $values[$name] = $field->getValue(); + } + } + + return $values; + } + + /** + * Gets the file field values. + * + * @return array An array of file field values. + */ + public function getFiles() + { + if (!in_array($this->getMethod(), array('post', 'put', 'delete'))) + { + return array(); + } + + $files = array(); + foreach ($this->fields as $name => $field) + { + if ($field instanceof Field\FileFormField) + { + $files[$name] = $field->getValue(); + } + } + + return $files; + } + + /** + * Gets the field values as PHP. + * + * This method converts fields with th array notation + * (like foo[bar] to arrays) like PHP does. + * + * @return array An array of field values. + */ + public function getPhpValues() + { + $qs = http_build_query($this->getValues()); + parse_str($qs, $values); + + return $values; + } + + /** + * Gets the file field values as PHP. + * + * This method converts fields with th array notation + * (like foo[bar] to arrays) like PHP does. + * + * @return array An array of field values. + */ + public function getPhpFiles() + { + $qs = http_build_query($this->getFiles()); + parse_str($qs, $values); + + return $values; + } + + /** + * Gets the URI of the form. + * + * The returned URI is not the same as the form "action" attribute. + * This method merges the value if the method is GET to mimics + * browser behavior. + * + * @param Boolean $absolute Wheter to return an absolute URI or not (this only works if a base URI has been provided) + * + * @return string The URI + */ + public function getUri($absolute = true) + { + $uri = $this->node->getAttribute('action'); + + if (!in_array($this->getMethod(), array('post', 'put', 'delete')) && $queryString = http_build_query($this->getValues(), null, '&')) + { + $sep = false === strpos($uri, '?') ? '?' : '&'; + $uri .= $sep.$queryString; + } + + if ($uri && '/' !== $uri[0]) + { + $uri = $this->path.$uri; + } + + if ($absolute && null !== $this->host) + { + return $this->host.$uri; + } + + return $uri; + } + + /** + * Gets the form method. + * + * If no method is defined in the form, GET is returned. + * + * @return string The method + */ + public function getMethod() + { + if (null !== $this->method) + { + return $this->method; + } + + return $this->node->getAttribute('method') ? strtolower($this->node->getAttribute('method')) : 'get'; + } + + /** + * Returns true if the named field exists. + * + * @param string $name The field name + * + * @param Boolean true if the field exists, false otherwise + */ + public function hasField($name) + { + return isset($this->fields[$name]); + } + + /** + * Gets a named field. + * + * @param string $name The field name + * + * @return Field\FormField The field instance + */ + public function getField($name) + { + if (!$this->hasField($name)) + { + throw new \InvalidArgumentException(sprintf('The form has no "%s" field', $name)); + } + + return $this->fields[$name]; + } + + /** + * Sets a named field. + * + * @param string $name The field name + * + * @return Field\FormField The field instance + */ + public function setField(Field\FormField $field) + { + $this->fields[$field->getName()] = $field; + } + + /** + * Gets all fields. + * + * @return array An array of fields + */ + public function getFields() + { + return $this->fields; + } + + protected function initialize() + { + $this->fields = array(); + + $document = new \DOMDocument('1.0', 'UTF-8'); + $node = $document->importNode($this->node, true); + $button = $document->importNode($this->button, true); + $root = $document->appendChild($document->createElement('_root')); + $root->appendChild($node); + $root->appendChild($button); + $xpath = new \DOMXPath($document); + + foreach ($xpath->query('descendant::input | descendant::textarea | descendant::select', $root) as $node) + { + if ($node->hasAttribute('disabled') || !$node->hasAttribute('name')) + { + continue; + } + + $nodeName = $node->nodeName; + + if ($node === $button) + { + $this->setField(new Field\InputFormField($node)); + } + elseif ('select' == $nodeName || 'input' == $nodeName && 'checkbox' == $node->getAttribute('type')) + { + $this->setField(new Field\ChoiceFormField($node)); + } + elseif ('input' == $nodeName && 'radio' == $node->getAttribute('type')) + { + if ($this->hasField($node->getAttribute('name'))) + { + $this->getField($node->getAttribute('name'))->addChoice($node); + } + else + { + $this->setField(new Field\ChoiceFormField($node)); + } + } + elseif ('input' == $nodeName && 'file' == $node->getAttribute('type')) + { + $this->setField(new Field\FileFormField($node)); + } + elseif ('input' == $nodeName && !in_array($node->getAttribute('type'), array('submit', 'button', 'image'))) + { + $this->setField(new Field\InputFormField($node)); + } + elseif ('textarea' == $nodeName) + { + $this->setField(new Field\TextareaFormField($node)); + } + } + } +} diff --git a/src/Symfony/Components/DomCrawler/Link.php b/src/Symfony/Components/DomCrawler/Link.php new file mode 100644 index 000000000000..e3ee20a3ddc1 --- /dev/null +++ b/src/Symfony/Components/DomCrawler/Link.php @@ -0,0 +1,94 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Link represents an HTML link (an HTML a tag). + * + * @package Symfony + * @subpackage Components_DomCrawler + * @author Fabien Potencier + */ +class Link +{ + protected $node; + protected $method; + protected $host; + protected $path; + + /** + * Constructor. + * + * @param \DOMNode $node A \DOMNode instance + * @param string $method The method to use for the link (get by default) + * @param string $host The base URI to use for absolute links (like http://localhost) + * @param string $path The base path for relative links (/ by default) + * + * @throws \LogicException if the node is not a link + */ + public function __construct(\DOMNode $node, $method = 'get', $host = null, $path = '/') + { + if ('a' != $node->nodeName) + { + throw new \LogicException(sprintf('Unable to click on a "%s" tag.', $node->nodeName)); + } + + $this->node = $node; + $this->method = $method; + $this->host = $host; + $this->path = empty($path) ? '/' : $path; + } + + /** + * Gets the node associated with this link. + * + * @return \DOMNode A \DOMNode instance + */ + public function getNode() + { + return $this->node; + } + + /** + * Gets the URI associated with this link. + * + * @param Boolean $absolute Wheter to return an absolute URI or not (this only works if a base URI has been provided) + * + * @return string The URI + */ + public function getUri($absolute = true) + { + $uri = $this->node->getAttribute('href'); + + if ($uri && '/' !== $uri[0]) + { + $uri = $this->path.$uri; + } + + if ($absolute && null !== $this->host) + { + return $this->host.$uri; + } + + return $uri; + } + + /** + * Gets the method associated with this link. + * + * @return string The method + */ + public function getMethod() + { + return $this->method; + } +} diff --git a/tests/Symfony/Tests/Components/DomCrawler/CrawlerTest.php b/tests/Symfony/Tests/Components/DomCrawler/CrawlerTest.php new file mode 100644 index 000000000000..a3d231ed68b9 --- /dev/null +++ b/tests/Symfony/Tests/Components/DomCrawler/CrawlerTest.php @@ -0,0 +1,524 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Tests\Components\DomCrawler; + +use Symfony\Components\DomCrawler\Crawler; + +class CrawlerTest extends \PHPUnit_Framework_TestCase +{ + public function testConstructor() + { + $crawler = new Crawler(); + $this->assertEquals(0, count($crawler), '__construct() returns an empty crawler'); + + $crawler = new Crawler(new \DOMNode()); + $this->assertEquals(1, count($crawler), '__construct() takes a node as a first argument'); + } + + /** + * @covers Symfony\Components\DomCrawler\Crawler::add + */ + public function testAdd() + { + $crawler = new Crawler(); + $crawler->add($this->createDomDocument()); + $this->assertEquals('foo', $crawler->filter('div')->attr('class'), '->add() adds nodes from a \DOMDocument'); + + $crawler = new Crawler(); + $crawler->add($this->createNodeList()); + $this->assertEquals('foo', $crawler->filter('div')->attr('class'), '->add() adds nodes from a \DOMNodeList'); + + foreach ($this->createNodeList() as $node) + { + $list[] = $node; + } + $crawler = new Crawler(); + $crawler->add($list); + $this->assertEquals('foo', $crawler->filter('div')->attr('class'), '->add() adds nodes from an array of nodes'); + + $crawler = new Crawler(); + $crawler->add($this->createNodeList()->item(0)); + $this->assertEquals('foo', $crawler->filter('div')->attr('class'), '->add() adds nodes from an \DOMNode'); + } + + /** + * @covers Symfony\Components\DomCrawler\Crawler::addHtmlContent + */ + public function testAddHtmlContent() + { + $crawler = new Crawler(); + $crawler->addHtmlContent('
', 'UTF-8'); + + $this->assertEquals('foo', $crawler->filter('div')->attr('class'), '->addHtmlContent() adds nodes from an HTML string'); + } + + /** + * @covers Symfony\Components\DomCrawler\Crawler::addXmlContent + */ + public function testAddXmlContent() + { + $crawler = new Crawler(); + $crawler->addXmlContent('
', 'UTF-8'); + + $this->assertEquals('foo', $crawler->filter('div')->attr('class'), '->addXmlContent() adds nodes from an XML string'); + } + + /** + * @covers Symfony\Components\DomCrawler\Crawler::addDocument + */ + public function testAddDocument() + { + $crawler = new Crawler(); + $crawler->addDocument($this->createDomDocument()); + + $this->assertEquals('foo', $crawler->filter('div')->attr('class'), '->addDocument() adds nodes from a \DOMDocument'); + } + + /** + * @covers Symfony\Components\DomCrawler\Crawler::addNodeList + */ + public function testAddNodeList() + { + $crawler = new Crawler(); + $crawler->addNodeList($this->createNodeList()); + + $this->assertEquals('foo', $crawler->filter('div')->attr('class'), '->addNodeList() adds nodes from a \DOMNodeList'); + } + + /** + * @covers Symfony\Components\DomCrawler\Crawler::addNodes + */ + public function testAddNodes() + { + foreach ($this->createNodeList() as $node) + { + $list[] = $node; + } + + $crawler = new Crawler(); + $crawler->addNodes($list); + + $this->assertEquals('foo', $crawler->filter('div')->attr('class'), '->addNodes() adds nodes from an array of nodes'); + } + + /** + * @covers Symfony\Components\DomCrawler\Crawler::addNode + */ + public function testAddNode() + { + $crawler = new Crawler(); + $crawler->addNode($this->createNodeList()->item(0)); + + $this->assertEquals('foo', $crawler->filter('div')->attr('class'), '->addNode() adds nodes from an \DOMNode'); + } + + public function testClear() + { + $crawler = new Crawler(new \DOMNode()); + $crawler->clear(); + $this->assertEquals(0, count($crawler), '->clear() removes all the nodes from the crawler'); + } + + public function testIsEmpty() + { + $crawler = new Crawler(new \DOMNode()); + $this->assertFalse($crawler->isEmpty(), '->isEmpty() returns false if the crawler node list is not empty'); + $crawler->clear(); + $this->assertTrue($crawler->isEmpty(), '->isEmpty() returns true if the crawler node list is empty'); + } + + public function testEq() + { + $crawler = $this->createTestCrawler()->filter('li'); + $this->assertNotSame($crawler, $crawler->eq(0), '->eq() returns a new instance of a crawler'); + $this->assertInstanceOf('Symfony\\Components\\DomCrawler\\Crawler', $crawler, '->eq() returns a new instance of a crawler'); + + $this->assertEquals('Two', $crawler->eq(1)->text(), '->eq() returns the nth node of the list'); + $this->assertTrue($crawler->eq(100)->isEmpty(), '->eq() returns an empty crawler if the nth node does not exist'); + } + + public function testEach() + { + $data = $this->createTestCrawler()->filter('ul.first li')->each(function ($node, $i) + { + return $i.'-'.$node->nodeValue; + }); + + $this->assertEquals(array('0-One', '1-Two', '2-Three'), $data, '->each() executes an anonymous function on each node of the list'); + } + + public function testReduce() + { + $crawler = $this->createTestCrawler()->filter('ul.first li'); + $nodes = $crawler->reduce(function ($node, $i) + { + return $i == 1 ? false : true; + }); + $this->assertNotSame($nodes, $crawler, '->reduce() returns a new instance of a crawler'); + $this->assertInstanceOf('Symfony\\Components\\DomCrawler\\Crawler', $nodes, '->reduce() returns a new instance of a crawler'); + + $this->assertEquals(2, count($nodes), '->reduce() filters the nodes in the list'); + } + + public function testAttr() + { + $this->assertEquals('first', $this->createTestCrawler()->filter('li')->attr('class'), '->attr() returns the attribute of the first element of the node list'); + + try + { + $this->createTestCrawler()->filter('ol')->attr('class'); + $this->fail('->attr() throws an \InvalidArgumentException if the node list is empty'); + } + catch (\InvalidArgumentException $e) + { + $this->assertTrue(true, '->attr() throws an \InvalidArgumentException if the node list is empty'); + } + } + + public function testText() + { + $this->assertEquals('One', $this->createTestCrawler()->filter('li')->text(), '->text() returns the node value of the first element of the node list'); + + try + { + $this->createTestCrawler()->filter('ol')->text(); + $this->fail('->text() throws an \InvalidArgumentException if the node list is empty'); + } + catch (\InvalidArgumentException $e) + { + $this->assertTrue(true, '->text() throws an \InvalidArgumentException if the node list is empty'); + } + } + + public function testExtract() + { + $crawler = $this->createTestCrawler()->filter('ul.first li'); + + $this->assertEquals(array('One', 'Two', 'Three'), $crawler->extract('_text'), '->extract() returns an array of extracted data from the node list'); + $this->assertEquals(array(array('One', 'first'), array('Two', ''), array('Three', '')), $crawler->extract(array('_text', 'class')), '->extract() returns an array of extracted data from the node list'); + + $this->assertEquals(array(), $this->createTestCrawler()->filter('lo')->extract('_text'), '->extract() returns an empty array if the node list is empty'); + } + + /** + * @covers Symfony\Components\DomCrawler\Crawler::filterXPath + */ + public function testFilterXPath() + { + $crawler = $this->createTestCrawler(); + $this->assertNotSame($crawler, $crawler->filterXPath('//li'), '->filterXPath() returns a new instance of a crawler'); + $this->assertInstanceOf('Symfony\\Components\\DomCrawler\\Crawler', $crawler, '->filterXPath() returns a new instance of a crawler'); + + $crawler = $this->createTestCrawler()->filter('ul'); + + $this->assertEquals(6, $crawler->filterXPath('//li')->count(), '->filterXPath() filters the node list with the XPath expression'); + } + + /** + * @covers Symfony\Components\DomCrawler\Crawler::filter + */ + public function testFilter() + { + $crawler = $this->createTestCrawler(); + $this->assertNotSame($crawler, $crawler->filter('li'), '->filter() returns a new instance of a crawler'); + $this->assertInstanceOf('Symfony\\Components\\DomCrawler\\Crawler', $crawler, '->filter() returns a new instance of a crawler'); + + $crawler = $this->createTestCrawler()->filter('ul'); + + $this->assertEquals(6, $crawler->filter('li')->count(), '->filter() filters the node list with the CSS selector'); + } + + public function testSelectLink() + { + $crawler = $this->createTestCrawler(); + $this->assertNotSame($crawler, $crawler->selectLink('Foo'), '->selectLink() returns a new instance of a crawler'); + $this->assertInstanceOf('Symfony\\Components\\DomCrawler\\Crawler', $crawler, '->selectLink() returns a new instance of a crawler'); + + $this->assertEquals(1, $crawler->selectLink('Fabien\'s Foo')->count(), '->selectLink() selects links by the node values'); + $this->assertEquals(1, $crawler->selectLink('Fabien\'s Bar')->count(), '->selectLink() selects links by the alt attribute of a clickable image'); + + $this->assertEquals(2, $crawler->selectLink('Fabien"s Foo')->count(), '->selectLink() selects links by the node values'); + $this->assertEquals(2, $crawler->selectLink('Fabien"s Bar')->count(), '->selectLink() selects links by the alt attribute of a clickable image'); + + $this->assertEquals(1, $crawler->selectLink('\' Fabien"s Foo')->count(), '->selectLink() selects links by the node values'); + $this->assertEquals(1, $crawler->selectLink('\' Fabien"s Bar')->count(), '->selectLink() selects links by the alt attribute of a clickable image'); + + $this->assertEquals(4, $crawler->selectLink('Foo')->count(), '->selectLink() selects links by the node values'); + $this->assertEquals(4, $crawler->selectLink('Bar')->count(), '->selectLink() selects links by the node values'); + } + + public function testSelectButton() + { + $crawler = $this->createTestCrawler(); + $this->assertNotSame($crawler, $crawler->selectButton('FooValue'), '->selectButton() returns a new instance of a crawler'); + $this->assertInstanceOf('Symfony\\Components\\DomCrawler\\Crawler', $crawler, '->selectButton() returns a new instance of a crawler'); + + $this->assertEquals(1, $crawler->selectButton('FooValue')->count(), '->selectButton() selects buttons'); + $this->assertEquals(1, $crawler->selectButton('FooName')->count(), '->selectButton() selects buttons'); + $this->assertEquals(1, $crawler->selectButton('FooId')->count(), '->selectButton() selects buttons'); + + $this->assertEquals(1, $crawler->selectButton('BarValue')->count(), '->selectButton() selects buttons'); + $this->assertEquals(1, $crawler->selectButton('BarName')->count(), '->selectButton() selects buttons'); + $this->assertEquals(1, $crawler->selectButton('BarId')->count(), '->selectButton() selects buttons'); + } + + public function testLink() + { + $crawler = $this->createTestCrawler()->selectLink('Foo'); + $this->assertInstanceOf('Symfony\\Components\\DomCrawler\\Link', $crawler->link(), '->link() returns a Link instance'); + + $this->assertEquals('/foo', $crawler->link()->getUri(), '->link() returns a Link instance'); + $this->assertEquals('post', $crawler->link('post')->getMethod(), '->link() takes a method as its argument'); + + $crawler = $this->createTestCrawler('http://example.com/bar/')->selectLink('Foo'); + $this->assertEquals('http://example.com/bar/foo', $crawler->link()->getUri(), '->link() returns a Link instance'); + + $crawler = $this->createTestCrawler('http://example.com/bar')->selectLink('Foo'); + $this->assertEquals('http://example.com/foo', $crawler->link()->getUri(), '->form() linketurns a Link instance'); + + try + { + $this->createTestCrawler()->filter('ol')->link(); + $this->fail('->link() throws an \InvalidArgumentException if the node list is empty'); + } + catch (\InvalidArgumentException $e) + { + $this->assertTrue(true, '->link() throws an \InvalidArgumentException if the node list is empty'); + } + } + + public function testLinks() + { + $crawler = $this->createTestCrawler()->selectLink('Foo'); + $this->assertType('array', $crawler->links(), '->links() returns an array'); + + $this->assertEquals(4, count($crawler->links()), '->links() returns an array'); + $links = $crawler->links(); + $this->assertInstanceOf('Symfony\\Components\\DomCrawler\\Link', $links[0], '->links() returns an array of Link instances'); + + $this->assertEquals(array(), $this->createTestCrawler()->filter('ol')->links(), '->links() returns an empty array if the node selection is empty'); + } + + public function testForm() + { + $crawler = $this->createTestCrawler()->selectButton('FooValue'); + $this->assertInstanceOf('Symfony\\Components\\DomCrawler\\Form', $crawler->form(), '->form() returns a Form instance'); + + $this->assertEquals('/foo?FooName=FooValue', $crawler->form()->getUri(), '->form() returns a Form instance'); + $this->assertEquals(array('FooName' => 'FooBar'), $crawler->form(array('FooName' => 'FooBar'))->getValues(), '->form() takes an array of values to submit as its first argument'); + + $crawler = $this->createTestCrawler('http://example.com/bar/')->selectButton('FooValue'); + $this->assertEquals('http://example.com/bar/foo?FooName=FooValue', $crawler->form()->getUri(), '->form() returns a Form instance'); + + $crawler = $this->createTestCrawler('http://example.com/bar')->selectButton('FooValue'); + $this->assertEquals('http://example.com/foo?FooName=FooValue', $crawler->form()->getUri(), '->form() returns a Form instance'); + + try + { + $this->createTestCrawler()->filter('ol')->form(); + $this->fail('->form() throws an \InvalidArgumentException if the node list is empty'); + } + catch (\InvalidArgumentException $e) + { + $this->assertTrue(true, '->form() throws an \InvalidArgumentException if the node list is empty'); + } + } + + public function testLast() + { + $crawler = $this->createTestCrawler()->filter('ul.first li'); + $this->assertNotSame($crawler, $crawler->last(), '->last() returns a new instance of a crawler'); + $this->assertInstanceOf('Symfony\\Components\\DomCrawler\\Crawler', $crawler, '->last() returns a new instance of a crawler'); + + $this->assertEquals('Three', $crawler->last()->text()); + } + + public function testFirst() + { + $crawler = $this->createTestCrawler()->filter('li'); + $this->assertNotSame($crawler, $crawler->first(), '->first() returns a new instance of a crawler'); + $this->assertInstanceOf('Symfony\\Components\\DomCrawler\\Crawler', $crawler, '->first() returns a new instance of a crawler'); + + $this->assertEquals('One', $crawler->first()->text()); + } + + public function testSiblings() + { + $crawler = $this->createTestCrawler()->filter('li')->eq(1); + $this->assertNotSame($crawler, $crawler->siblings(), '->siblings() returns a new instance of a crawler'); + $this->assertInstanceOf('Symfony\\Components\\DomCrawler\\Crawler', $crawler, '->siblings() returns a new instance of a crawler'); + + $nodes = $crawler->siblings(); + $this->assertEquals(2, $nodes->count()); + $this->assertEquals('One', $nodes->eq(0)->text()); + $this->assertEquals('Three', $nodes->eq(1)->text()); + + $nodes = $this->createTestCrawler()->filter('li')->eq(0)->siblings(); + $this->assertEquals(2, $nodes->count()); + $this->assertEquals('Two', $nodes->eq(0)->text()); + $this->assertEquals('Three', $nodes->eq(1)->text()); + + try + { + $this->createTestCrawler()->filter('ol')->siblings(); + $this->fail('->siblings() throws an \InvalidArgumentException if the node list is empty'); + } + catch (\InvalidArgumentException $e) + { + $this->assertTrue(true, '->siblings() throws an \InvalidArgumentException if the node list is empty'); + } + } + + public function testNextAll() + { + $crawler = $this->createTestCrawler()->filter('li')->eq(1); + $this->assertNotSame($crawler, $crawler->nextAll(), '->nextAll() returns a new instance of a crawler'); + $this->assertInstanceOf('Symfony\\Components\\DomCrawler\\Crawler', $crawler, '->nextAll() returns a new instance of a crawler'); + + $nodes = $crawler->nextAll(); + $this->assertEquals(1, $nodes->count()); + $this->assertEquals('Three', $nodes->eq(0)->text()); + + try + { + $this->createTestCrawler()->filter('ol')->nextAll(); + $this->fail('->nextAll() throws an \InvalidArgumentException if the node list is empty'); + } + catch (\InvalidArgumentException $e) + { + $this->assertTrue(true, '->nextAll() throws an \InvalidArgumentException if the node list is empty'); + } + } + + public function testPreviousAll() + { + $crawler = $this->createTestCrawler()->filter('li')->eq(2); + $this->assertNotSame($crawler, $crawler->previousAll(), '->previousAll() returns a new instance of a crawler'); + $this->assertInstanceOf('Symfony\\Components\\DomCrawler\\Crawler', $crawler, '->previousAll() returns a new instance of a crawler'); + + $nodes = $crawler->previousAll(); + $this->assertEquals(2, $nodes->count()); + $this->assertEquals('Two', $nodes->eq(0)->text()); + + try + { + $this->createTestCrawler()->filter('ol')->previousAll(); + $this->fail('->previousAll() throws an \InvalidArgumentException if the node list is empty'); + } + catch (\InvalidArgumentException $e) + { + $this->assertTrue(true, '->previousAll() throws an \InvalidArgumentException if the node list is empty'); + } + } + + public function testChildren() + { + $crawler = $this->createTestCrawler()->filter('ul'); + $this->assertNotSame($crawler, $crawler->children(), '->children() returns a new instance of a crawler'); + $this->assertInstanceOf('Symfony\\Components\\DomCrawler\\Crawler', $crawler, '->children() returns a new instance of a crawler'); + + $nodes = $crawler->children(); + $this->assertEquals(3, $nodes->count()); + $this->assertEquals('One', $nodes->eq(0)->text()); + $this->assertEquals('Two', $nodes->eq(1)->text()); + $this->assertEquals('Three', $nodes->eq(2)->text()); + + try + { + $this->createTestCrawler()->filter('ol')->children(); + $this->fail('->children() throws an \InvalidArgumentException if the node list is empty'); + } + catch (\InvalidArgumentException $e) + { + $this->assertTrue(true, '->children() throws an \InvalidArgumentException if the node list is empty'); + } + } + + public function testParents() + { + $crawler = $this->createTestCrawler()->filter('li:first-child'); + $this->assertNotSame($crawler, $crawler->parents(), '->parents() returns a new instance of a crawler'); + $this->assertInstanceOf('Symfony\\Components\\DomCrawler\\Crawler', $crawler, '->parents() returns a new instance of a crawler'); + + $nodes = $crawler->parents(); + $this->assertEquals(3, $nodes->count()); + + $nodes = $this->createTestCrawler()->filter('html')->parents(); + $this->assertEquals(0, $nodes->count()); + + try + { + $this->createTestCrawler()->filter('ol')->parents(); + $this->fail('->parents() throws an \InvalidArgumentException if the node list is empty'); + } + catch (\InvalidArgumentException $e) + { + $this->assertTrue(true, '->parents() throws an \InvalidArgumentException if the node list is empty'); + } + } + + public function createTestCrawler($uri = null) + { + $dom = new \DOMDocument(); + $dom->loadHTML(' + + + Foo + Fabien\'s Foo + Fabien"s Foo + \' Fabien"s Foo + + Bar +    Fabien\'s Bar   + Fabien"s Bar + \' Fabien"s Bar + +
+ + +