Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow passing NodeElement to/from JS scripts #352

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 127 additions & 16 deletions src/Selenium2Driver.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -246,6 +247,105 @@ 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));
}

return reset($minkElements);
}

/**
* 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
Expand Down Expand Up @@ -284,11 +384,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);
}

/**
Expand Down Expand Up @@ -585,7 +685,7 @@ public function getValue($xpath)
}

if ('input' === $elementName && 'radio' === $elementType) {
$script = <<<JS
$script = <<<'JS'
var node = {{ELEMENT}},
value = null;

Expand All @@ -611,7 +711,7 @@ public function getValue($xpath)
// Using $element->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 = <<<JS
$script = <<<'JS'
var node = {{ELEMENT}},
value = [];

Expand Down Expand Up @@ -697,7 +797,7 @@ public function setValue($xpath, $value)
// has lost focus in the meanwhile. If the element has lost focus
// already then there is nothing to do as this will already have caused
// the triggering of the change event for that element.
$script = <<<JS
$script = <<<'JS'
var node = {{ELEMENT}};
if (document.activeElement === node) {
document.activeElement.blur();
Expand Down Expand Up @@ -917,7 +1017,7 @@ public function dragTo($sourceXpath, $destinationXpath)
'element' => $source->getID()
));

$script = <<<JS
$script = <<<'JS'
(function (element) {
var event = document.createEvent("HTMLEvents");

Expand All @@ -935,7 +1035,7 @@ public function dragTo($sourceXpath, $destinationXpath)
));
$this->wdSession->buttonup();

$script = <<<JS
$script = <<<'JS'
(function (element) {
var event = document.createEvent("HTMLEvents");

Expand All @@ -951,39 +1051,50 @@ public function dragTo($sourceXpath, $destinationXpath)
/**
* {@inheritdoc}
*/
public function executeScript($script)
public function executeScript($script, array $args = [])
{
if (preg_match('/^function[\s\(]/', $script)) {
$script = preg_replace('/;$/', '', $script);
$script = '(' . $script . ')';
}

$this->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;
}
Expand Down Expand Up @@ -1130,7 +1241,7 @@ private function selectOptionOnElement(Element $element, $value, $multiple = fal
*/
private function deselectAllOptions(Element $element)
{
$script = <<<JS
$script = <<<'JS'
var node = {{ELEMENT}};
var i, l = node.options.length;
for (i = 0; i < l; i++) {
Expand Down