Skip to content

Commit a9e416b

Browse files
committed
Allow passing NodeElement to/from JS scripts
1 parent 4ee4eca commit a9e416b

1 file changed

Lines changed: 133 additions & 16 deletions

File tree

src/Selenium2Driver.php

Lines changed: 133 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
namespace Behat\Mink\Driver;
1212

13+
use Behat\Mink\Element\NodeElement;
1314
use Behat\Mink\Exception\DriverException;
1415
use Behat\Mink\Selector\Xpath\Escaper;
1516
use WebDriver\Element;
@@ -246,6 +247,111 @@ protected static function charToOptions($char, $modifier = null)
246247
return json_encode($options);
247248
}
248249

250+
/**
251+
* Create Mink element from WebDriver element.
252+
*
253+
* @return NodeElement[]
254+
*
255+
* @throws DriverException When the operation cannot be done
256+
*/
257+
protected function createMinkElementFromWebDriverElement(Element $element)
258+
{
259+
// WebDriver element contains only a temporary ID assigned by Selenium,
260+
// to create a Mink element we must build a xpath for it first
261+
$script = <<<'JS'
262+
var buildXpathFromElement;
263+
buildXpathFromElement = function (element) {
264+
var tagNameLc = element.tagName.toLowerCase();
265+
if (element.parentElement === null) {
266+
return '/' + tagNameLc;
267+
}
268+
269+
if (element.id && document.querySelectorAll(tagNameLc + '#' + element.id).length === 1) {
270+
return '//' + tagNameLc + '[@id=\'' + element.id + '\']';
271+
}
272+
273+
var children = element.parentElement.children;
274+
var pos = 0;
275+
for (var i = 0; i < children.length; i++) {
276+
if (children[i].tagName.toLowerCase() === tagNameLc) {
277+
pos++;
278+
if (children[i] === element) {
279+
break;
280+
}
281+
}
282+
}
283+
284+
var xpath = buildXpathFromElement(element.parentElement) + '/' + tagNameLc + '[' + pos + ']';
285+
286+
return xpath;
287+
};
288+
289+
return buildXpathFromElement(arguments[0]);
290+
JS;
291+
$xpath = $this->wdSession->execute(array(
292+
'script' => $script,
293+
'args' => array($element),
294+
));
295+
296+
$minkElements = $this->find($xpath);
297+
if (count($minkElements) === 0) {
298+
throw new DriverException(sprintf('XPath "%s" built from WebDriver element did not find any element', $xpath));
299+
}
300+
if (count($minkElements) > 1) {
301+
throw new DriverException(sprintf('XPath "%s" built from WebDriver element find more than one element', $xpath));
302+
}
303+
$minkElement = reset($minkElements);
304+
305+
// DEBUG only
306+
if ($this->findElement($minkElement->getXpath())->getID() !== $element->getId()) {
307+
throw new DriverException(sprintf('XPath "%s" built from WebDriver element cannot find the same element', $xpath));
308+
}
309+
310+
return $minkElement;
311+
}
312+
313+
/**
314+
* Serialize execute arguments (containing web elements)
315+
*
316+
* @see https://w3c.github.io/webdriver/#executing-script
317+
*
318+
* @param array $args
319+
*
320+
* @return array
321+
*/
322+
private function serializeExecuteArguments(array $args)
323+
{
324+
foreach ($args as $k => $v) {
325+
if ($v instanceof NodeElement) {
326+
$args[$k] = $this->findElement($v->getXpath());
327+
} elseif (is_array($v)) {
328+
$args[$k] = $this->serializeExecuteArguments($v);
329+
}
330+
}
331+
332+
return $args;
333+
}
334+
335+
/**
336+
* Unserialize execute result (containing web elements)
337+
*
338+
* @param mixed $data
339+
*
340+
* @return mixed
341+
*/
342+
private function unserializeExecuteResult($data)
343+
{
344+
if ($data instanceof Element) {
345+
return $this->createMinkElementFromWebDriverElement($data);
346+
} elseif (is_array($data)) {
347+
foreach ($data as $k => $v) {
348+
$data[$k] = $this->unserializeExecuteResult($v);
349+
}
350+
}
351+
352+
return $data;
353+
}
354+
249355
/**
250356
* Executes JS on a given element - pass in a js script string and {{ELEMENT}} will
251357
* be replaced with a reference to the result of the $xpath query
@@ -284,11 +390,11 @@ private function executeJsOnElement(Element $element, $script, $sync = true)
284390
'args' => array($element),
285391
);
286392

287-
if ($sync) {
288-
return $this->wdSession->execute($options);
289-
}
393+
$result = $sync
394+
? $this->wdSession->execute($options)
395+
: $this->wdSession->execute_async($options);
290396

291-
return $this->wdSession->execute_async($options);
397+
return $this->unserializeExecuteResult($result);
292398
}
293399

294400
/**
@@ -585,7 +691,7 @@ public function getValue($xpath)
585691
}
586692

587693
if ('input' === $elementName && 'radio' === $elementType) {
588-
$script = <<<JS
694+
$script = <<<'JS'
589695
var node = {{ELEMENT}},
590696
value = null;
591697
@@ -611,7 +717,7 @@ public function getValue($xpath)
611717
// Using $element->attribute('value') on a select only returns the first selected option
612718
// even when it is a multiple select, so a custom retrieval is needed.
613719
if ('select' === $elementName && $element->attribute('multiple')) {
614-
$script = <<<JS
720+
$script = <<<'JS'
615721
var node = {{ELEMENT}},
616722
value = [];
617723
@@ -697,7 +803,7 @@ public function setValue($xpath, $value)
697803
// has lost focus in the meanwhile. If the element has lost focus
698804
// already then there is nothing to do as this will already have caused
699805
// the triggering of the change event for that element.
700-
$script = <<<JS
806+
$script = <<<'JS'
701807
var node = {{ELEMENT}};
702808
if (document.activeElement === node) {
703809
document.activeElement.blur();
@@ -917,7 +1023,7 @@ public function dragTo($sourceXpath, $destinationXpath)
9171023
'element' => $source->getID()
9181024
));
9191025

920-
$script = <<<JS
1026+
$script = <<<'JS'
9211027
(function (element) {
9221028
var event = document.createEvent("HTMLEvents");
9231029
@@ -935,7 +1041,7 @@ public function dragTo($sourceXpath, $destinationXpath)
9351041
));
9361042
$this->wdSession->buttonup();
9371043

938-
$script = <<<JS
1044+
$script = <<<'JS'
9391045
(function (element) {
9401046
var event = document.createEvent("HTMLEvents");
9411047
@@ -951,39 +1057,50 @@ public function dragTo($sourceXpath, $destinationXpath)
9511057
/**
9521058
* {@inheritdoc}
9531059
*/
954-
public function executeScript($script)
1060+
public function executeScript($script, array $args = [])
9551061
{
9561062
if (preg_match('/^function[\s\(]/', $script)) {
9571063
$script = preg_replace('/;$/', '', $script);
9581064
$script = '(' . $script . ')';
9591065
}
9601066

961-
$this->wdSession->execute(array('script' => $script, 'args' => array()));
1067+
$this->wdSession->execute(array(
1068+
'script' => $script,
1069+
'args' => $this->serializeExecuteArguments($args),
1070+
));
9621071
}
9631072

9641073
/**
9651074
* {@inheritdoc}
9661075
*/
967-
public function evaluateScript($script)
1076+
public function evaluateScript($script, array $args = [])
9681077
{
9691078
if (0 !== strpos(trim($script), 'return ')) {
9701079
$script = 'return ' . $script;
9711080
}
9721081

973-
return $this->wdSession->execute(array('script' => $script, 'args' => array()));
1082+
$result = $this->wdSession->execute(array(
1083+
'script' => $script,
1084+
'args' => $this->serializeExecuteArguments($args),
1085+
));
1086+
1087+
return $this->unserializeExecuteResult($result);
9741088
}
9751089

9761090
/**
9771091
* {@inheritdoc}
9781092
*/
979-
public function wait($timeout, $condition)
1093+
public function wait($timeout, $condition, array $args = [])
9801094
{
9811095
$script = 'return (' . rtrim($condition, " \t\n\r;") . ');';
9821096
$start = microtime(true);
9831097
$end = $start + $timeout / 1000.0;
9841098

9851099
do {
986-
$result = $this->wdSession->execute(array('script' => $script, 'args' => array()));
1100+
$result = $this->wdSession->execute(array(
1101+
'script' => $script,
1102+
'args' => $this->serializeExecuteArguments($args),
1103+
));
9871104
if ($result) {
9881105
break;
9891106
}
@@ -1130,7 +1247,7 @@ private function selectOptionOnElement(Element $element, $value, $multiple = fal
11301247
*/
11311248
private function deselectAllOptions(Element $element)
11321249
{
1133-
$script = <<<JS
1250+
$script = <<<'JS'
11341251
var node = {{ELEMENT}};
11351252
var i, l = node.options.length;
11361253
for (i = 0; i < l; i++) {

0 commit comments

Comments
 (0)