From 5cf602a37c5324c4ecbd1571a349a21cb7789761 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 1/4] Allow passing NodeElement to/from JS scripts --- src/Selenium2Driver.php | 149 +++++++++++++++++++++++++++++++++++----- 1 file changed, 133 insertions(+), 16 deletions(-) diff --git a/src/Selenium2Driver.php b/src/Selenium2Driver.php index ee7f2e75..0c06133f 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,111 @@ 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), + )); + + $minkElements = $this->find($xpath); + if (count($minkElements) === 0) { + throw new DriverException(sprintf('XPath "%s" built from WebDriver element did not find any element', $xpath)); + } + if (count($minkElements) > 1) { + throw new DriverException(sprintf('XPath "%s" built from WebDriver element find more than one element', $xpath)); + } + $minkElement = reset($minkElements); + + // DEBUG only + if ($this->findElement($minkElement->getXpath())->getID() !== $element->getId()) { + throw new DriverException(sprintf('XPath "%s" built from WebDriver element cannot find the same element', $xpath)); + } + + return $minkElement; + } + + /** + * 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 +390,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 +691,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 +1247,7 @@ private function selectOptionOnElement(Element $element, $value, $multiple = fal */ private function deselectAllOptions(Element $element) { - $script = << Date: Wed, 20 Apr 2022 13:56:00 +0200 Subject: [PATCH 2/4] DEBUG test createMinkElementFromWebDriverElement method for all elems --- src/Selenium2Driver.php | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/Selenium2Driver.php b/src/Selenium2Driver.php index 0c06133f..af8a5094 100755 --- a/src/Selenium2Driver.php +++ b/src/Selenium2Driver.php @@ -304,7 +304,7 @@ protected function createMinkElementFromWebDriverElement(Element $element) // DEBUG only if ($this->findElement($minkElement->getXpath())->getID() !== $element->getId()) { - throw new DriverException(sprintf('XPath "%s" built from WebDriver element cannot find the same element', $xpath)); + throw new \Error(sprintf('XPath "%s" built from WebDriver element cannot find the same element', $xpath)); } return $minkElement; @@ -627,6 +627,22 @@ public function findElementXpaths($xpath) $elements[] = sprintf('(%s)[%d]', $xpath, $i+1); } + // DEBUG only + $recursiveCount = 0; + foreach (debug_backtrace() as $trace) { + if (($trace['function'] ?? null) === __FUNCTION__) { + $recursive = $recursiveCount++; + } + } + if ($recursiveCount <= 4) { + foreach ($nodes as $element) { + $minkElement = $this->createMinkElementFromWebDriverElement($element); + if ($this->findElement($minkElement->getXpath())->getID() !== $element->getId()) { + throw new \Error(sprintf('XPath "%s" built from WebDriver element cannot find the same element (in find)', $xpath)); + } + } + } + return $elements; } From 5a3e00fe3567a008cdad4678c7e16e23a7fdee90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Wed, 20 Apr 2022 14:01:57 +0200 Subject: [PATCH 3/4] remove debug code --- src/Selenium2Driver.php | 24 +----------------------- 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/src/Selenium2Driver.php b/src/Selenium2Driver.php index af8a5094..87c63c7c 100755 --- a/src/Selenium2Driver.php +++ b/src/Selenium2Driver.php @@ -300,14 +300,8 @@ protected function createMinkElementFromWebDriverElement(Element $element) if (count($minkElements) > 1) { throw new DriverException(sprintf('XPath "%s" built from WebDriver element find more than one element', $xpath)); } - $minkElement = reset($minkElements); - // DEBUG only - if ($this->findElement($minkElement->getXpath())->getID() !== $element->getId()) { - throw new \Error(sprintf('XPath "%s" built from WebDriver element cannot find the same element', $xpath)); - } - - return $minkElement; + return reset($minkElements); } /** @@ -627,22 +621,6 @@ public function findElementXpaths($xpath) $elements[] = sprintf('(%s)[%d]', $xpath, $i+1); } - // DEBUG only - $recursiveCount = 0; - foreach (debug_backtrace() as $trace) { - if (($trace['function'] ?? null) === __FUNCTION__) { - $recursive = $recursiveCount++; - } - } - if ($recursiveCount <= 4) { - foreach ($nodes as $element) { - $minkElement = $this->createMinkElementFromWebDriverElement($element); - if ($this->findElement($minkElement->getXpath())->getID() !== $element->getId()) { - throw new \Error(sprintf('XPath "%s" built from WebDriver element cannot find the same element (in find)', $xpath)); - } - } - } - return $elements; } From 6c3cd0624c8be347494b185b376505c501e86cb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Wed, 20 Apr 2022 14:56:42 +0200 Subject: [PATCH 4/4] fix phpdoc typo --- src/Selenium2Driver.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Selenium2Driver.php b/src/Selenium2Driver.php index 87c63c7c..7d5537bf 100755 --- a/src/Selenium2Driver.php +++ b/src/Selenium2Driver.php @@ -250,9 +250,9 @@ protected static function charToOptions($char, $modifier = null) /** * Create Mink element from WebDriver element. * - * @return NodeElement[] + * @return NodeElement * - * @throws DriverException When the operation cannot be done + * @throws DriverException When the operation cannot be done */ protected function createMinkElementFromWebDriverElement(Element $element) {