Created
March 31, 2015 16:17
-
-
Save gggeek/ca746db7b77048a1be8e to your computer and use it in GitHub Desktop.
Taking control of the CURL connection between behat and selenium
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/* | |
* Idea taken from Behat Symfony2Extension: register a driver factory which creates a custom driver | |
*/ | |
namespace XXX\Behat\KToolsExtension\ServiceContainer\Driver; | |
use Behat\MinkExtension\ServiceContainer\Driver\SauceLabsFactory; | |
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; | |
use Symfony\Component\DependencyInjection\Definition; | |
use Symfony\Component\DependencyInjection\Reference; | |
class KToolsSaucelabsFactory extends SauceLabsFactory | |
{ | |
public function getDriverName() | |
{ | |
return 'k_sauce_labs'; | |
} | |
public function configure(ArrayNodeDefinition $builder) | |
{ | |
$builder | |
->children() | |
->scalarNode('username')->defaultValue(getenv('SAUCE_USERNAME'))->end() | |
->scalarNode('access_key')->defaultValue(getenv('SAUCE_ACCESS_KEY'))->end() | |
->booleanNode('connect')->defaultFalse()->end() | |
->scalarNode('browser')->defaultValue('firefox')->end() | |
->scalarNode('local_proxy')->defaultValue('')->end() | |
->append($this->getCapabilitiesNode()) | |
->end() | |
; | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function buildDriver(array $config) | |
{ | |
// from class SaucelabsFactory | |
$host = 'ondemand.saucelabs.com'; | |
if ($config['connect']) { | |
$host = 'localhost:4445'; | |
} | |
$config['wd_host'] = sprintf('%s:%s@%s/wd/hub', $config['username'], $config['access_key'], $host); | |
// from class Selenium2Factory | |
if (!class_exists('Behat\Mink\Driver\Selenium2Driver')) { | |
throw new \RuntimeException(sprintf( | |
'Install MinkSelenium2Driver in order to use %s driver.', | |
$this->getDriverName() | |
)); | |
} | |
$extraCapabilities = $config['capabilities']['extra_capabilities']; | |
unset($config['capabilities']['extra_capabilities']); | |
if (getenv('TRAVIS_JOB_NUMBER')) { | |
$guessedCapabilities = array( | |
'tunnel-identifier' => getenv('TRAVIS_JOB_NUMBER'), | |
'build' => getenv('TRAVIS_BUILD_NUMBER'), | |
'tags' => array('Travis-CI', 'PHP '.phpversion()), | |
); | |
} elseif (getenv('JENKINS_HOME')) { | |
$guessedCapabilities = array( | |
'tunnel-identifier' => getenv('JOB_NAME'), | |
'build' => getenv('BUILD_NUMBER'), | |
'tags' => array('Jenkins', 'PHP '.phpversion(), getenv('BUILD_TAG')), | |
); | |
} else { | |
$guessedCapabilities = array( | |
'tags' => array(php_uname('n'), 'PHP '.phpversion()), | |
); | |
} | |
return new Definition('XXX\Behat\KToolsExtension\Driver\SaucelabsDriver', array( | |
$config['browser'], | |
array_replace($extraCapabilities, $guessedCapabilities, $config['capabilities']), | |
$config['wd_host'], | |
$config['local_proxy'], | |
)); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* A Mink driver for Selenium2. | |
* | |
* Compared to the default Selenium2Driver, it allows us to get a different Webdriver | |
*/ | |
namespace XXX\Behat\KToolsExtension\Driver; | |
use Behat\Mink\Element\NodeElement; | |
use Behat\Mink\Exception\DriverException; | |
use Behat\Mink\Selector\Xpath\Escaper; | |
use Behat\Mink\Session; | |
use WebDriver\Element; | |
use WebDriver\Exception\NoSuchElement; | |
use WebDriver\Exception\UnknownError; | |
use WebDriver\Exception; | |
use WebDriver\Key; | |
use Behat\Mink\Driver\Selenium2Driver; | |
class SaucelabsDriver extends Selenium2Driver | |
{ | |
protected $session; | |
protected $started = false; | |
protected $webDriver; | |
protected $browserName; | |
protected $desiredCapabilities; | |
protected $wdSession; | |
protected $timeouts = array(); | |
protected $xpathEscaper; | |
/** | |
* Instantiates the driver. | |
* | |
* @param string $browserName Browser name | |
* @param array $desiredCapabilities The desired capabilities | |
* @param string $wdHost The WebDriver host | |
* @param string $proxy | |
*/ | |
public function __construct($browserName = 'firefox', $desiredCapabilities = null, $wdHost = 'http://localhost:4444/wd/hub', $proxy = '') | |
{ | |
$this->setBrowserName($browserName); | |
$this->setDesiredCapabilities($desiredCapabilities); | |
$webDriver = new WebDriver($wdHost); | |
$webDriver->setProxy($proxy); | |
$this->setWebDriver($webDriver); | |
$this->xpathEscaper = new Escaper(); | |
} | |
/** | |
* Remove from this method the strong type hinting | |
* @param WebDriver $webDriver | |
*/ | |
public function setWebDriver($webDriver) | |
{ | |
$this->webDriver = $webDriver; | |
} | |
/// duplicate the rest of the class to work around the limitation of private members... :-( | |
/** | |
* Sets the browser name | |
* | |
* @param string $browserName the name of the browser to start, default is 'firefox' | |
*/ | |
protected function setBrowserName($browserName = 'firefox') | |
{ | |
$this->browserName = $browserName; | |
} | |
/** | |
* Sets the desired capabilities - called on construction. If null is provided, will set the | |
* defaults as desired. | |
* | |
* See http://code.google.com/p/selenium/wiki/DesiredCapabilities | |
* | |
* @param array $desiredCapabilities an array of capabilities to pass on to the WebDriver server | |
*/ | |
public function setDesiredCapabilities($desiredCapabilities = null) | |
{ | |
if (null === $desiredCapabilities) { | |
$desiredCapabilities = self::getDefaultCapabilities(); | |
} | |
if (isset($desiredCapabilities['firefox'])) { | |
foreach ($desiredCapabilities['firefox'] as $capability => $value) { | |
switch ($capability) { | |
case 'profile': | |
$desiredCapabilities['firefox_'.$capability] = base64_encode(file_get_contents($value)); | |
break; | |
default: | |
$desiredCapabilities['firefox_'.$capability] = $value; | |
} | |
} | |
unset($desiredCapabilities['firefox']); | |
} | |
// See https://sites.google.com/a/chromium.org/chromedriver/capabilities | |
if (isset($desiredCapabilities['chrome'])) { | |
$chromeOptions = array(); | |
foreach ($desiredCapabilities['chrome'] as $capability => $value) { | |
if ($capability == 'switches') { | |
$chromeOptions['args'] = $value; | |
} else { | |
$chromeOptions[$capability] = $value; | |
} | |
$desiredCapabilities['chrome.'.$capability] = $value; | |
} | |
$desiredCapabilities['chromeOptions'] = $chromeOptions; | |
unset($desiredCapabilities['chrome']); | |
} | |
$this->desiredCapabilities = $desiredCapabilities; | |
} | |
/** | |
* Gets the WebDriverSession instance | |
* | |
* @return \WebDriver\Session | |
*/ | |
public function getWebDriverSession() | |
{ | |
return $this->wdSession; | |
} | |
/** | |
* Returns the default capabilities | |
* | |
* @return array | |
*/ | |
public static function getDefaultCapabilities() | |
{ | |
return array( | |
'browserName' => 'firefox', | |
'version' => '9', | |
'platform' => 'ANY', | |
'browserVersion' => '9', | |
'browser' => 'firefox', | |
'name' => 'Behat Test', | |
'deviceOrientation' => 'portrait', | |
'deviceType' => 'tablet', | |
'selenium-version' => '2.31.0' | |
); | |
} | |
/** | |
* Makes sure that the Syn event library has been injected into the current page, | |
* and return $this for a fluid interface, | |
* | |
* $this->withSyn()->executeJsOnXpath($xpath, $script); | |
* | |
* @return Selenium2Driver | |
*/ | |
protected function withSyn() | |
{ | |
$hasSyn = $this->wdSession->execute(array( | |
'script' => 'return typeof window["Syn"]!=="undefined" && typeof window["Syn"].trigger!=="undefined"', | |
'args' => array() | |
)); | |
if (!$hasSyn) { | |
$synJs = file_get_contents(__DIR__.'/Selenium2/syn.js'); | |
$this->wdSession->execute(array( | |
'script' => $synJs, | |
'args' => array() | |
)); | |
} | |
return $this; | |
} | |
/** | |
* Creates some options for key events | |
* | |
* @param string $char the character or code | |
* @param string $modifier one of 'shift', 'alt', 'ctrl' or 'meta' | |
* | |
* @return string a json encoded options array for Syn | |
*/ | |
protected static function charToOptions($char, $modifier = null) | |
{ | |
$ord = ord($char); | |
if (is_numeric($char)) { | |
$ord = $char; | |
} | |
$options = array( | |
'keyCode' => $ord, | |
'charCode' => $ord | |
); | |
if ($modifier) { | |
$options[$modifier.'Key'] = 1; | |
} | |
return json_encode($options); | |
} | |
/** | |
* 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 | |
* | |
* @example $this->executeJsOnXpath($xpath, 'return {{ELEMENT}}.childNodes.length'); | |
* | |
* @param string $xpath the xpath to search with | |
* @param string $script the script to execute | |
* @param Boolean $sync whether to run the script synchronously (default is TRUE) | |
* | |
* @return mixed | |
*/ | |
protected function executeJsOnXpath($xpath, $script, $sync = true) | |
{ | |
return $this->executeJsOnElement($this->findElement($xpath), $script, $sync); | |
} | |
/** | |
* Executes JS on a given element - pass in a js script string and {{ELEMENT}} will | |
* be replaced with a reference to the element | |
* | |
* @example $this->executeJsOnXpath($xpath, 'return {{ELEMENT}}.childNodes.length'); | |
* | |
* @param Element $element the webdriver element | |
* @param string $script the script to execute | |
* @param Boolean $sync whether to run the script synchronously (default is TRUE) | |
* | |
* @return mixed | |
*/ | |
private function executeJsOnElement(Element $element, $script, $sync = true) | |
{ | |
$script = str_replace('{{ELEMENT}}', 'arguments[0]', $script); | |
$options = array( | |
'script' => $script, | |
'args' => array(array('ELEMENT' => $element->getID())), | |
); | |
if ($sync) { | |
return $this->wdSession->execute($options); | |
} | |
return $this->wdSession->execute_async($options); | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function setSession(Session $session) | |
{ | |
$this->session = $session; | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function start() | |
{ | |
try { | |
$this->wdSession = $this->webDriver->session($this->browserName, $this->desiredCapabilities); | |
$this->applyTimeouts(); | |
} catch (\Exception $e) { | |
throw new DriverException('Could not open connection: '.$e->getMessage(), 0, $e); | |
} | |
if (!$this->wdSession) { | |
throw new DriverException('Could not connect to a Selenium 2 / WebDriver server'); | |
} | |
$this->started = true; | |
} | |
/** | |
* Sets the timeouts to apply to the webdriver session | |
* | |
* @param array $timeouts The session timeout settings: Array of {script, implicit, page} => time in microsecconds | |
* | |
* @throws DriverException | |
*/ | |
public function setTimeouts($timeouts) | |
{ | |
$this->timeouts = $timeouts; | |
if ($this->isStarted()) { | |
$this->applyTimeouts(); | |
} | |
} | |
/** | |
* Applies timeouts to the current session | |
*/ | |
private function applyTimeouts() | |
{ | |
try { | |
foreach ($this->timeouts as $type => $param) { | |
$this->wdSession->timeouts($type, $param); | |
} | |
} catch (UnknownError $e) { | |
throw new DriverException('Error setting timeout: ' . $e->getMessage(), 0, $e); | |
} | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function isStarted() | |
{ | |
return $this->started; | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function stop() | |
{ | |
if (!$this->wdSession) { | |
throw new DriverException('Could not connect to a Selenium 2 / WebDriver server'); | |
} | |
$this->started = false; | |
try { | |
$this->wdSession->close(); | |
} catch (\Exception $e) { | |
throw new DriverException('Could not close connection', 0, $e); | |
} | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function reset() | |
{ | |
$this->wdSession->deleteAllCookies(); | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function visit($url) | |
{ | |
$this->wdSession->open($url); | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function getCurrentUrl() | |
{ | |
return $this->wdSession->url(); | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function reload() | |
{ | |
$this->wdSession->refresh(); | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function forward() | |
{ | |
$this->wdSession->forward(); | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function back() | |
{ | |
$this->wdSession->back(); | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function switchToWindow($name = null) | |
{ | |
$this->wdSession->focusWindow($name ? $name : ''); | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function switchToIFrame($name = null) | |
{ | |
$this->wdSession->frame(array('id' => $name)); | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function setCookie($name, $value = null) | |
{ | |
if (null === $value) { | |
$this->wdSession->deleteCookie($name); | |
return; | |
} | |
$cookieArray = array( | |
'name' => $name, | |
'value' => (string) $value, | |
'secure' => false, // thanks, chibimagic! | |
); | |
$this->wdSession->setCookie($cookieArray); | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function getCookie($name) | |
{ | |
$cookies = $this->wdSession->getAllCookies(); | |
foreach ($cookies as $cookie) { | |
if ($cookie['name'] === $name) { | |
return urldecode($cookie['value']); | |
} | |
} | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function getContent() | |
{ | |
return $this->wdSession->source(); | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function getScreenshot() | |
{ | |
return base64_decode($this->wdSession->screenshot()); | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function getWindowNames() | |
{ | |
return $this->wdSession->window_handles(); | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function getWindowName() | |
{ | |
return $this->wdSession->window_handle(); | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function find($xpath) | |
{ | |
$nodes = $this->wdSession->elements('xpath', $xpath); | |
$elements = array(); | |
foreach ($nodes as $i => $node) { | |
$elements[] = new NodeElement(sprintf('(%s)[%d]', $xpath, $i+1), $this->session); | |
} | |
return $elements; | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function getTagName($xpath) | |
{ | |
return $this->findElement($xpath)->name(); | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function getText($xpath) | |
{ | |
$node = $this->findElement($xpath); | |
$text = $node->text(); | |
$text = (string) str_replace(array("\r", "\r\n", "\n"), ' ', $text); | |
return $text; | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function getHtml($xpath) | |
{ | |
return $this->executeJsOnXpath($xpath, 'return {{ELEMENT}}.innerHTML;'); | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function getOuterHtml($xpath) | |
{ | |
return $this->executeJsOnXpath($xpath, 'return {{ELEMENT}}.outerHTML;'); | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function getAttribute($xpath, $name) | |
{ | |
$script = 'return {{ELEMENT}}.getAttribute(' . json_encode((string) $name) . ')'; | |
return $this->executeJsOnXpath($xpath, $script); | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function getValue($xpath) | |
{ | |
$element = $this->findElement($xpath); | |
$elementName = strtolower($element->name()); | |
$elementType = strtolower($element->attribute('type')); | |
// Getting the value of a checkbox returns its value if selected. | |
if ('input' === $elementName && 'checkbox' === $elementType) { | |
return $element->selected() ? $element->attribute('value') : null; | |
} | |
if ('input' === $elementName && 'radio' === $elementType) { | |
$script = <<<JS | |
var node = {{ELEMENT}}, | |
value = null; | |
var name = node.getAttribute('name'); | |
if (name) { | |
var fields = window.document.getElementsByName(name), | |
i, l = fields.length; | |
for (i = 0; i < l; i++) { | |
var field = fields.item(i); | |
if (field.form === node.form && field.checked) { | |
value = field.value; | |
break; | |
} | |
} | |
} | |
return value; | |
JS; | |
return $this->executeJsOnElement($element, $script); | |
} | |
// 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 | |
var node = {{ELEMENT}}, | |
value = []; | |
for (var i = 0; i < node.options.length; i++) { | |
if (node.options[i].selected) { | |
value.push(node.options[i].value); | |
} | |
} | |
return value; | |
JS; | |
return $this->executeJsOnElement($element, $script); | |
} | |
return $element->attribute('value'); | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function setValue($xpath, $value) | |
{ | |
$element = $this->findElement($xpath); | |
$elementName = strtolower($element->name()); | |
if ('select' === $elementName) { | |
if (is_array($value)) { | |
$this->deselectAllOptions($element); | |
foreach ($value as $option) { | |
$this->selectOptionOnElement($element, $option, true); | |
} | |
return; | |
} | |
$this->selectOptionOnElement($element, $value); | |
return; | |
} | |
if ('input' === $elementName) { | |
$elementType = strtolower($element->attribute('type')); | |
if (in_array($elementType, array('submit', 'image', 'button', 'reset'))) { | |
throw new DriverException(sprintf('Impossible to set value an element with XPath "%s" as it is not a select, textarea or textbox', $xpath)); | |
} | |
if ('checkbox' === $elementType) { | |
if ($element->selected() xor (bool) $value) { | |
$this->clickOnElement($element); | |
} | |
return; | |
} | |
if ('radio' === $elementType) { | |
$this->selectRadioValue($element, $value); | |
return; | |
} | |
if ('file' === $elementType) { | |
$element->postValue(array('value' => array(strval($value)))); | |
return; | |
} | |
} | |
$value = strval($value); | |
if (in_array($elementName, array('input', 'textarea'))) { | |
$existingValueLength = strlen($element->attribute('value')); | |
// Add the TAB key to ensure we unfocus the field as browsers are triggering the change event only | |
// after leaving the field. | |
$value = str_repeat(Key::BACKSPACE . Key::DELETE, $existingValueLength) . $value . Key::TAB; | |
} | |
$element->postValue(array('value' => array($value))); | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function check($xpath) | |
{ | |
$element = $this->findElement($xpath); | |
$this->ensureInputType($element, $xpath, 'checkbox', 'check'); | |
if ($element->selected()) { | |
return; | |
} | |
$this->clickOnElement($element); | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function uncheck($xpath) | |
{ | |
$element = $this->findElement($xpath); | |
$this->ensureInputType($element, $xpath, 'checkbox', 'uncheck'); | |
if (!$element->selected()) { | |
return; | |
} | |
$this->clickOnElement($element); | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function isChecked($xpath) | |
{ | |
return $this->findElement($xpath)->selected(); | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function selectOption($xpath, $value, $multiple = false) | |
{ | |
$element = $this->findElement($xpath); | |
$tagName = strtolower($element->name()); | |
if ('input' === $tagName && 'radio' === strtolower($element->attribute('type'))) { | |
$this->selectRadioValue($element, $value); | |
return; | |
} | |
if ('select' === $tagName) { | |
$this->selectOptionOnElement($element, $value, $multiple); | |
return; | |
} | |
throw new DriverException(sprintf('Impossible to select an option on the element with XPath "%s" as it is not a select or radio input', $xpath)); | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function isSelected($xpath) | |
{ | |
return $this->findElement($xpath)->selected(); | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function click($xpath) | |
{ | |
$this->clickOnElement($this->findElement($xpath)); | |
} | |
private function clickOnElement(Element $element) | |
{ | |
$this->wdSession->moveto(array('element' => $element->getID())); | |
$element->click(); | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function doubleClick($xpath) | |
{ | |
$this->mouseOver($xpath); | |
$this->wdSession->doubleclick(); | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function rightClick($xpath) | |
{ | |
$this->mouseOver($xpath); | |
$this->wdSession->click(array('button' => 2)); | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function attachFile($xpath, $path) | |
{ | |
$element = $this->findElement($xpath); | |
$this->ensureInputType($element, $xpath, 'file', 'attach a file on'); | |
$element->postValue(array('value' => array($path))); | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function isVisible($xpath) | |
{ | |
return $this->findElement($xpath)->displayed(); | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function mouseOver($xpath) | |
{ | |
$this->wdSession->moveto(array( | |
'element' => $this->findElement($xpath)->getID() | |
)); | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function focus($xpath) | |
{ | |
$script = 'Syn.trigger("focus", {}, {{ELEMENT}})'; | |
$this->withSyn()->executeJsOnXpath($xpath, $script); | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function blur($xpath) | |
{ | |
$script = 'Syn.trigger("blur", {}, {{ELEMENT}})'; | |
$this->withSyn()->executeJsOnXpath($xpath, $script); | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function keyPress($xpath, $char, $modifier = null) | |
{ | |
$options = self::charToOptions($char, $modifier); | |
$script = "Syn.trigger('keypress', $options, {{ELEMENT}})"; | |
$this->withSyn()->executeJsOnXpath($xpath, $script); | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function keyDown($xpath, $char, $modifier = null) | |
{ | |
$options = self::charToOptions($char, $modifier); | |
$script = "Syn.trigger('keydown', $options, {{ELEMENT}})"; | |
$this->withSyn()->executeJsOnXpath($xpath, $script); | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function keyUp($xpath, $char, $modifier = null) | |
{ | |
$options = self::charToOptions($char, $modifier); | |
$script = "Syn.trigger('keyup', $options, {{ELEMENT}})"; | |
$this->withSyn()->executeJsOnXpath($xpath, $script); | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function dragTo($sourceXpath, $destinationXpath) | |
{ | |
$source = $this->findElement($sourceXpath); | |
$destination = $this->findElement($destinationXpath); | |
$this->wdSession->moveto(array( | |
'element' => $source->getID() | |
)); | |
$script = <<<JS | |
(function (element) { | |
var event = document.createEvent("HTMLEvents"); | |
event.initEvent("dragstart", true, true); | |
event.dataTransfer = {}; | |
element.dispatchEvent(event); | |
}({{ELEMENT}})); | |
JS; | |
$this->withSyn()->executeJsOnElement($source, $script); | |
$this->wdSession->buttondown(); | |
$this->wdSession->moveto(array( | |
'element' => $destination->getID() | |
)); | |
$this->wdSession->buttonup(); | |
$script = <<<JS | |
(function (element) { | |
var event = document.createEvent("HTMLEvents"); | |
event.initEvent("drop", true, true); | |
event.dataTransfer = {}; | |
element.dispatchEvent(event); | |
}({{ELEMENT}})); | |
JS; | |
$this->withSyn()->executeJsOnElement($destination, $script); | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function executeScript($script) | |
{ | |
if (preg_match('/^function[\s\(]/', $script)) { | |
$script = preg_replace('/;$/', '', $script); | |
$script = '(' . $script . ')'; | |
} | |
$this->wdSession->execute(array('script' => $script, 'args' => array())); | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function evaluateScript($script) | |
{ | |
if (0 !== strpos(trim($script), 'return ')) { | |
$script = 'return ' . $script; | |
} | |
return $this->wdSession->execute(array('script' => $script, 'args' => array())); | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function wait($timeout, $condition) | |
{ | |
$script = "return $condition;"; | |
$start = microtime(true); | |
$end = $start + $timeout / 1000.0; | |
do { | |
$result = $this->wdSession->execute(array('script' => $script, 'args' => array())); | |
usleep(100000); | |
} while (microtime(true) < $end && !$result); | |
return (bool) $result; | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function resizeWindow($width, $height, $name = null) | |
{ | |
$this->wdSession->window($name ? $name : 'current')->postSize( | |
array('width' => $width, 'height' => $height) | |
); | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function submitForm($xpath) | |
{ | |
$this->findElement($xpath)->submit(); | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function maximizeWindow($name = null) | |
{ | |
$this->wdSession->window($name ? $name : 'current')->maximize(); | |
} | |
/** | |
* Returns Session ID of WebDriver or `null`, when session not started yet. | |
* | |
* @return string|null | |
*/ | |
public function getWebDriverSessionId() | |
{ | |
return $this->isStarted() ? basename($this->wdSession->getUrl()) : null; | |
} | |
/** | |
* @param string $xpath | |
* | |
* @return Element | |
*/ | |
private function findElement($xpath) | |
{ | |
return $this->wdSession->element('xpath', $xpath); | |
} | |
/** | |
* Selects a value in a radio button group | |
* | |
* @param Element $element An element referencing one of the radio buttons of the group | |
* @param string $value The value to select | |
* | |
* @throws DriverException when the value cannot be found | |
*/ | |
private function selectRadioValue(Element $element, $value) | |
{ | |
// short-circuit when we already have the right button of the group to avoid XPath queries | |
if ($element->attribute('value') === $value) { | |
$element->click(); | |
return; | |
} | |
$name = $element->attribute('name'); | |
if (!$name) { | |
throw new DriverException(sprintf('The radio button does not have the value "%s"', $value)); | |
} | |
$formId = $element->attribute('form'); | |
try { | |
if (null !== $formId) { | |
$xpath = <<<'XPATH' | |
//form[@id=%1$s]//input[@type="radio" and not(@form) and @name=%2$s and @value = %3$s] | |
| | |
//input[@type="radio" and @form=%1$s and @name=%2$s and @value = %3$s] | |
XPATH; | |
$xpath = sprintf( | |
$xpath, | |
$this->xpathEscaper->escapeLiteral($formId), | |
$this->xpathEscaper->escapeLiteral($name), | |
$this->xpathEscaper->escapeLiteral($value) | |
); | |
$input = $this->wdSession->element('xpath', $xpath); | |
} else { | |
$xpath = sprintf( | |
'./ancestor::form//input[@type="radio" and not(@form) and @name=%s and @value = %s]', | |
$this->xpathEscaper->escapeLiteral($name), | |
$this->xpathEscaper->escapeLiteral($value) | |
); | |
$input = $element->element('xpath', $xpath); | |
} | |
} catch (NoSuchElement $e) { | |
$message = sprintf('The radio group "%s" does not have an option "%s"', $name, $value); | |
throw new DriverException($message, 0, $e); | |
} | |
$input->click(); | |
} | |
/** | |
* @param Element $element | |
* @param string $value | |
* @param bool $multiple | |
*/ | |
private function selectOptionOnElement(Element $element, $value, $multiple = false) | |
{ | |
$escapedValue = $this->xpathEscaper->escapeLiteral($value); | |
// The value of an option is the normalized version of its text when it has no value attribute | |
$optionQuery = sprintf('.//option[@value = %s or (not(@value) and normalize-space(.) = %s)]', $escapedValue, $escapedValue); | |
$option = $element->element('xpath', $optionQuery); | |
if ($multiple || !$element->attribute('multiple')) { | |
if (!$option->selected()) { | |
$option->click(); | |
} | |
return; | |
} | |
// Deselect all options before selecting the new one | |
$this->deselectAllOptions($element); | |
$option->click(); | |
} | |
/** | |
* Deselects all options of a multiple select | |
* | |
* Note: this implementation does not trigger a change event after deselecting the elements. | |
* | |
* @param Element $element | |
*/ | |
private function deselectAllOptions(Element $element) | |
{ | |
$script = <<<JS | |
var node = {{ELEMENT}}; | |
var i, l = node.options.length; | |
for (i = 0; i < l; i++) { | |
node.options[i].selected = false; | |
} | |
JS; | |
$this->executeJsOnElement($element, $script); | |
} | |
/** | |
* Ensures the element is a checkbox | |
* | |
* @param Element $element | |
* @param string $xpath | |
* @param string $type | |
* @param string $action | |
* | |
* @throws DriverException | |
*/ | |
private function ensureInputType(Element $element, $xpath, $type, $action) | |
{ | |
if ('input' !== strtolower($element->name()) || $type !== strtolower($element->attribute('type'))) { | |
$message = 'Impossible to %s the element with XPath "%s" as it is not a %s input'; | |
throw new DriverException(sprintf($message, $action, $xpath, $type)); | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* A webdriver for Selenium2. | |
* | |
* Compared to the default WebDriver\WebDriver, it allows us to get a different Webdriver | |
*/ | |
namespace XXX\Behat\KToolsExtension\Driver; | |
use WebDriver\AbstractWebDriver; | |
use WebDriver\Capability; | |
use WebDriver\Browser; | |
use WebDriver\Session; | |
class WebDriver extends AbstractWebDriver | |
{ | |
protected $proxy = ''; | |
public function setProxy($proxy) | |
{ | |
$this->proxy = $proxy; | |
} | |
/** | |
* Given a set of options, adds to it the default 'extra' options | |
* @param array $extraOptions | |
* @return array | |
*/ | |
protected function getExtraOptions(array $extraOptions = array()) | |
{ | |
$defaults = array(); | |
if ($this->proxy !== '') { | |
$defaults[CURLOPT_PROXY] = $this->proxy; | |
} | |
$result = $extraOptions + $defaults; | |
return $result; | |
} | |
/** | |
* Magic method that maps calls to class methods to execute WebDriver commands | |
* | |
* @param string $name Method name | |
* @param array $arguments Arguments | |
* | |
* @return mixed | |
* | |
* @throws \WebDriver\Exception if invalid WebDriver command | |
*/ | |
public function __call($name, $arguments) | |
{ | |
if (count($arguments) > 1) { | |
throw WebDriverException::factory( | |
WebDriverException::JSON_PARAMETERS_EXPECTED, | |
'Commands should have at most only one parameter, which should be the JSON Parameter object' | |
); | |
} | |
if (preg_match('/^(get|post|delete)/', $name, $matches)) { | |
$requestMethod = strtoupper($matches[0]); | |
$webdriverCommand = strtolower(substr($name, strlen($requestMethod))); | |
} else { | |
$webdriverCommand = $name; | |
$requestMethod = $this->getRequestMethod($webdriverCommand); | |
} | |
$methods = $this->methods(); | |
if (!in_array($requestMethod, (array) $methods[$webdriverCommand])) { | |
throw WebDriverException::factory( | |
WebDriverException::INVALID_REQUEST, | |
sprintf( | |
'%s is not an available http request method for the command %s.', | |
$requestMethod, | |
$webdriverCommand | |
) | |
); | |
} | |
$result = $this->curl( | |
$requestMethod, | |
'/' . $webdriverCommand, | |
array_shift($arguments), | |
$this->getExtraOptions() | |
); | |
return $result['value']; | |
} | |
/// All the methods below are copy-pasted from Webdriver/Webdriver, which is unluckily declared FINAL :-( | |
/** | |
* {@inheritdoc} | |
*/ | |
protected function methods() | |
{ | |
return array( | |
'status' => 'GET', | |
); | |
} | |
/** | |
* New Session: /session (POST) | |
* Get session object for chaining | |
* | |
* @param array|string $requiredCapabilities Required capabilities (or browser name) | |
* @param array $desiredCapabilities Desired capabilities | |
* | |
* @return \WebDriver\Session | |
*/ | |
public function session($requiredCapabilities = Browser::FIREFOX, $desiredCapabilities = array()) | |
{ | |
// for backwards compatibility when the only required capability was browser name | |
if (! is_array($requiredCapabilities)) { | |
$desiredCapabilities[Capability::BROWSER_NAME] = $requiredCapabilities ?: Browser::FIREFOX; | |
$requiredCapabilities = array(); | |
} | |
// required | |
$parameters = array( | |
'desiredCapabilities' => array_merge($desiredCapabilities, $requiredCapabilities) | |
); | |
// optional | |
if ( ! empty($requiredCapabilities)) { | |
$parameters['requiredCapabilities'] = $requiredCapabilities; | |
} | |
$result = $this->curl( | |
'POST', | |
'/session', | |
$parameters, | |
$this->getExtraOptions(array(CURLOPT_FOLLOWLOCATION => true)) | |
); | |
return new Session($result['sessionUrl']); | |
} | |
/** | |
* Get list of currently active sessions | |
* | |
* @return array an array of \WebDriver\Session objects | |
*/ | |
public function sessions() | |
{ | |
$result = $this->curl('GET', '/sessions', null, $this->getExtraOptions()); | |
$sessions = array(); | |
foreach ($result['value'] as $session) { | |
$sessions[] = new Session($this->url . '/session/' . $session['id']); | |
} | |
return $sessions; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment