Skip to content

Instantly share code, notes, and snippets.

@gggeek
Created March 31, 2015 16:17
Show Gist options
  • Save gggeek/ca746db7b77048a1be8e to your computer and use it in GitHub Desktop.
Save gggeek/ca746db7b77048a1be8e to your computer and use it in GitHub Desktop.
Taking control of the CURL connection between behat and selenium
<?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'],
));
}
}
<?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));
}
}
}
<?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