From 836909c91b0c53a17f932f942e5b33e280ddc9ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Mon, 18 Apr 2022 01:17:12 +0200 Subject: [PATCH] Convert element to NodeElement from execute result --- src/Selenium2Driver.php | 151 +++++++++++++++++++++++++++++++++++----- 1 file changed, 135 insertions(+), 16 deletions(-) diff --git a/src/Selenium2Driver.php b/src/Selenium2Driver.php index ee7f2e75..7a127886 100755 --- a/src/Selenium2Driver.php +++ b/src/Selenium2Driver.php @@ -10,6 +10,7 @@ namespace Behat\Mink\Driver; +use Behat\Mink\Element\NodeElement; use Behat\Mink\Exception\DriverException; use Behat\Mink\Selector\Xpath\Escaper; use WebDriver\Element; @@ -246,6 +247,113 @@ protected static function charToOptions($char, $modifier = null) return json_encode($options); } + /** + * Create Mink element from WebDriver element. + * + * @return NodeElement[] + * + * @throws DriverException When the operation cannot be done + */ + protected function createMinkElementFromWebDriverElement(Element $element) + { + // WebDriver element contains only a temporary ID assigned by Selenium, + // to create a Mink element we must build a xpath for it first + $script = <<<'JS' +var buildXpathFromElement; +buildXpathFromElement = function (element) { + var tagNameLc = element.tagName.toLowerCase(); + if (element.parentElement === null) { + return '/' + tagNameLc; + } + + if (element.id && document.querySelectorAll(tagNameLc + '#' + element.id).length === 1) { + return '//' + tagNameLc + '[@id=\'' + element.id + '\']'; + } + + var children = element.parentElement.children; + var pos = 0; + for (var i = 0; i < children.length; i++) { + if (children[i].tagName.toLowerCase() === tagNameLc) { + pos++; + if (children[i] === element) { + break; + } + } + } + + var xpath = buildXpathFromElement(element.parentElement) + '/' + tagNameLc + '[' + pos + ']'; + + return xpath; +}; + +return buildXpathFromElement(arguments[0]); +JS; + $xpath = $this->wdSession->execute(array( + 'script' => $script, + 'args' => array($element), + )); + + var_dump($xpath); + + $nodeElements = $this->find($xpath); + if (count($nodeElements) === 0) { + throw new DriverException(sprintf('XPath "%s" built from WebDriver element did not find any element', $xpath)); + } + if (count($nodeElements) > 1) { + throw new DriverException(sprintf('XPath "%s" built from WebDriver element find more than one element', $xpath)); + } + $nodeElement = reset($nodeElements); + + // DEBUG only + if ($this->findElement($nodeElement->getXpath())->getID() !== $element->getId()) { + throw new DriverException(sprintf('XPath "%s" built from WebDriver element cannot find the same element', $xpath)); + } + + return $nodeElement; + } + + /** + * Serialize execute arguments (containing web elements) + * + * @see https://w3c.github.io/webdriver/#executing-script + * + * @param array $args + * + * @return array + */ + private function serializeExecuteArguments(array $args) + { + foreach ($args as $k => $v) { + if ($v instanceof NodeElement) { + $args[$k] = $this->findElement($v->getXpath()); + } elseif (is_array($v)) { + $args[$k] = $this->serializeExecuteArguments($v); + } + } + + return $args; + } + + /** + * Unserialize execute result (containing web elements) + * + * @param mixed $data + * + * @return mixed + */ + private function unserializeExecuteResult($data) + { + if ($data instanceof Element) { + return $this->createMinkElementFromWebDriverElement($data); + } elseif (is_array($data)) { + foreach ($data as $k => $v) { + $data[$k] = $this->unserializeExecuteResult($v); + } + } + + return $data; + } + /** * Executes JS on a given element - pass in a js script string and {{ELEMENT}} will * be replaced with a reference to the result of the $xpath query @@ -284,11 +392,11 @@ private function executeJsOnElement(Element $element, $script, $sync = true) 'args' => array($element), ); - if ($sync) { - return $this->wdSession->execute($options); - } + $result = $sync + ? $this->wdSession->execute($options) + : $this->wdSession->execute_async($options); - return $this->wdSession->execute_async($options); + return $this->unserializeExecuteResult($result); } /** @@ -585,7 +693,7 @@ public function getValue($xpath) } if ('input' === $elementName && 'radio' === $elementType) { - $script = <<attribute('value') on a select only returns the first selected option // even when it is a multiple select, so a custom retrieval is needed. if ('select' === $elementName && $element->attribute('multiple')) { - $script = << $source->getID() )); - $script = <<wdSession->buttonup(); - $script = <<wdSession->execute(array('script' => $script, 'args' => array())); + $this->wdSession->execute(array( + 'script' => $script, + 'args' => $this->serializeExecuteArguments($args), + )); } /** * {@inheritdoc} */ - public function evaluateScript($script) + public function evaluateScript($script, array $args = []) { if (0 !== strpos(trim($script), 'return ')) { $script = 'return ' . $script; } - return $this->wdSession->execute(array('script' => $script, 'args' => array())); + $result = $this->wdSession->execute(array( + 'script' => $script, + 'args' => $this->serializeExecuteArguments($args), + )); + + return $this->unserializeExecuteResult($result); } /** * {@inheritdoc} */ - public function wait($timeout, $condition) + public function wait($timeout, $condition, array $args = []) { $script = 'return (' . rtrim($condition, " \t\n\r;") . ');'; $start = microtime(true); $end = $start + $timeout / 1000.0; do { - $result = $this->wdSession->execute(array('script' => $script, 'args' => array())); + $result = $this->wdSession->execute(array( + 'script' => $script, + 'args' => $this->serializeExecuteArguments($args), + )); if ($result) { break; } @@ -1130,7 +1249,7 @@ private function selectOptionOnElement(Element $element, $value, $multiple = fal */ private function deselectAllOptions(Element $element) { - $script = <<