Chris@14: Chris@14: * Chris@14: * For the full copyright and license information, please view the LICENSE Chris@14: * file that was distributed with this source code. Chris@14: */ Chris@14: Chris@14: namespace Behat\Mink\Driver; Chris@14: Chris@14: use Behat\Mink\Exception\DriverException; Chris@14: use Behat\Mink\Selector\Xpath\Escaper; Chris@14: use WebDriver\Element; Chris@14: use WebDriver\Exception\NoSuchElement; Chris@14: use WebDriver\Exception\UnknownCommand; Chris@14: use WebDriver\Exception\UnknownError; Chris@14: use WebDriver\Exception; Chris@14: use WebDriver\Key; Chris@14: use WebDriver\WebDriver; Chris@14: Chris@14: /** Chris@14: * Selenium2 driver. Chris@14: * Chris@14: * @author Pete Otaqui Chris@14: */ Chris@14: class Selenium2Driver extends CoreDriver Chris@14: { Chris@14: /** Chris@14: * Whether the browser has been started Chris@14: * @var Boolean Chris@14: */ Chris@14: private $started = false; Chris@14: Chris@14: /** Chris@14: * The WebDriver instance Chris@14: * @var WebDriver Chris@14: */ Chris@14: private $webDriver; Chris@14: Chris@14: /** Chris@14: * @var string Chris@14: */ Chris@14: private $browserName; Chris@14: Chris@14: /** Chris@14: * @var array Chris@14: */ Chris@14: private $desiredCapabilities; Chris@14: Chris@14: /** Chris@14: * The WebDriverSession instance Chris@14: * @var \WebDriver\Session Chris@14: */ Chris@14: private $wdSession; Chris@14: Chris@14: /** Chris@14: * The timeout configuration Chris@14: * @var array Chris@14: */ Chris@14: private $timeouts = array(); Chris@14: Chris@14: /** Chris@14: * @var Escaper Chris@14: */ Chris@14: private $xpathEscaper; Chris@14: Chris@14: /** Chris@14: * Instantiates the driver. Chris@14: * Chris@14: * @param string $browserName Browser name Chris@14: * @param array $desiredCapabilities The desired capabilities Chris@14: * @param string $wdHost The WebDriver host Chris@14: */ Chris@14: public function __construct($browserName = 'firefox', $desiredCapabilities = null, $wdHost = 'http://localhost:4444/wd/hub') Chris@14: { Chris@14: $this->setBrowserName($browserName); Chris@14: $this->setDesiredCapabilities($desiredCapabilities); Chris@14: $this->setWebDriver(new WebDriver($wdHost)); Chris@14: $this->xpathEscaper = new Escaper(); Chris@14: } Chris@14: Chris@14: /** Chris@14: * Sets the browser name Chris@14: * Chris@14: * @param string $browserName the name of the browser to start, default is 'firefox' Chris@14: */ Chris@14: protected function setBrowserName($browserName = 'firefox') Chris@14: { Chris@14: $this->browserName = $browserName; Chris@14: } Chris@14: Chris@14: /** Chris@14: * Sets the desired capabilities - called on construction. If null is provided, will set the Chris@14: * defaults as desired. Chris@14: * Chris@14: * See http://code.google.com/p/selenium/wiki/DesiredCapabilities Chris@14: * Chris@14: * @param array $desiredCapabilities an array of capabilities to pass on to the WebDriver server Chris@14: * Chris@14: * @throws DriverException Chris@14: */ Chris@14: public function setDesiredCapabilities($desiredCapabilities = null) Chris@14: { Chris@14: if ($this->started) { Chris@14: throw new DriverException("Unable to set desiredCapabilities, the session has already started"); Chris@14: } Chris@14: Chris@14: if (null === $desiredCapabilities) { Chris@14: $desiredCapabilities = array(); Chris@14: } Chris@14: Chris@14: // Join $desiredCapabilities with defaultCapabilities Chris@14: $desiredCapabilities = array_replace(self::getDefaultCapabilities(), $desiredCapabilities); Chris@14: Chris@14: if (isset($desiredCapabilities['firefox'])) { Chris@14: foreach ($desiredCapabilities['firefox'] as $capability => $value) { Chris@14: switch ($capability) { Chris@14: case 'profile': Chris@14: $desiredCapabilities['firefox_'.$capability] = base64_encode(file_get_contents($value)); Chris@14: break; Chris@14: default: Chris@14: $desiredCapabilities['firefox_'.$capability] = $value; Chris@14: } Chris@14: } Chris@14: Chris@14: unset($desiredCapabilities['firefox']); Chris@14: } Chris@14: Chris@14: // See https://sites.google.com/a/chromium.org/chromedriver/capabilities Chris@14: if (isset($desiredCapabilities['chrome'])) { Chris@14: Chris@14: $chromeOptions = array(); Chris@14: Chris@14: foreach ($desiredCapabilities['chrome'] as $capability => $value) { Chris@14: if ($capability == 'switches') { Chris@14: $chromeOptions['args'] = $value; Chris@14: } else { Chris@14: $chromeOptions[$capability] = $value; Chris@14: } Chris@14: $desiredCapabilities['chrome.'.$capability] = $value; Chris@14: } Chris@14: Chris@14: $desiredCapabilities['chromeOptions'] = $chromeOptions; Chris@14: Chris@14: unset($desiredCapabilities['chrome']); Chris@14: } Chris@14: Chris@14: $this->desiredCapabilities = $desiredCapabilities; Chris@14: } Chris@14: Chris@14: /** Chris@14: * Gets the desiredCapabilities Chris@14: * Chris@14: * @return array $desiredCapabilities Chris@14: */ Chris@14: public function getDesiredCapabilities() Chris@14: { Chris@14: return $this->desiredCapabilities; Chris@14: } Chris@14: Chris@14: /** Chris@14: * Sets the WebDriver instance Chris@14: * Chris@14: * @param WebDriver $webDriver An instance of the WebDriver class Chris@14: */ Chris@14: public function setWebDriver(WebDriver $webDriver) Chris@14: { Chris@14: $this->webDriver = $webDriver; Chris@14: } Chris@14: Chris@14: /** Chris@14: * Gets the WebDriverSession instance Chris@14: * Chris@14: * @return \WebDriver\Session Chris@14: */ Chris@14: public function getWebDriverSession() Chris@14: { Chris@14: return $this->wdSession; Chris@14: } Chris@14: Chris@14: /** Chris@14: * Returns the default capabilities Chris@14: * Chris@14: * @return array Chris@14: */ Chris@14: public static function getDefaultCapabilities() Chris@14: { Chris@14: return array( Chris@14: 'browserName' => 'firefox', Chris@14: 'name' => 'Behat Test', Chris@14: ); Chris@14: } Chris@14: Chris@14: /** Chris@14: * Makes sure that the Syn event library has been injected into the current page, Chris@14: * and return $this for a fluid interface, Chris@14: * Chris@14: * $this->withSyn()->executeJsOnXpath($xpath, $script); Chris@14: * Chris@14: * @return Selenium2Driver Chris@14: */ Chris@14: protected function withSyn() Chris@14: { Chris@14: $hasSyn = $this->wdSession->execute(array( Chris@14: 'script' => 'return typeof window["Syn"]!=="undefined" && typeof window["Syn"].trigger!=="undefined"', Chris@14: 'args' => array() Chris@14: )); Chris@14: Chris@14: if (!$hasSyn) { Chris@14: $synJs = file_get_contents(__DIR__.'/Resources/syn.js'); Chris@14: $this->wdSession->execute(array( Chris@14: 'script' => $synJs, Chris@14: 'args' => array() Chris@14: )); Chris@14: } Chris@14: Chris@14: return $this; Chris@14: } Chris@14: Chris@14: /** Chris@14: * Creates some options for key events Chris@14: * Chris@14: * @param string $char the character or code Chris@14: * @param string $modifier one of 'shift', 'alt', 'ctrl' or 'meta' Chris@14: * Chris@14: * @return string a json encoded options array for Syn Chris@14: */ Chris@14: protected static function charToOptions($char, $modifier = null) Chris@14: { Chris@14: $ord = ord($char); Chris@14: if (is_numeric($char)) { Chris@14: $ord = $char; Chris@14: } Chris@14: Chris@14: $options = array( Chris@14: 'keyCode' => $ord, Chris@14: 'charCode' => $ord Chris@14: ); Chris@14: Chris@14: if ($modifier) { Chris@14: $options[$modifier.'Key'] = 1; Chris@14: } Chris@14: Chris@14: return json_encode($options); Chris@14: } Chris@14: Chris@14: /** Chris@14: * Executes JS on a given element - pass in a js script string and {{ELEMENT}} will Chris@14: * be replaced with a reference to the result of the $xpath query Chris@14: * Chris@14: * @example $this->executeJsOnXpath($xpath, 'return {{ELEMENT}}.childNodes.length'); Chris@14: * Chris@14: * @param string $xpath the xpath to search with Chris@14: * @param string $script the script to execute Chris@14: * @param Boolean $sync whether to run the script synchronously (default is TRUE) Chris@14: * Chris@14: * @return mixed Chris@14: */ Chris@14: protected function executeJsOnXpath($xpath, $script, $sync = true) Chris@14: { Chris@14: return $this->executeJsOnElement($this->findElement($xpath), $script, $sync); Chris@14: } Chris@14: Chris@14: /** Chris@14: * Executes JS on a given element - pass in a js script string and {{ELEMENT}} will Chris@14: * be replaced with a reference to the element Chris@14: * Chris@14: * @example $this->executeJsOnXpath($xpath, 'return {{ELEMENT}}.childNodes.length'); Chris@14: * Chris@14: * @param Element $element the webdriver element Chris@14: * @param string $script the script to execute Chris@14: * @param Boolean $sync whether to run the script synchronously (default is TRUE) Chris@14: * Chris@14: * @return mixed Chris@14: */ Chris@14: private function executeJsOnElement(Element $element, $script, $sync = true) Chris@14: { Chris@14: $script = str_replace('{{ELEMENT}}', 'arguments[0]', $script); Chris@14: Chris@14: $options = array( Chris@14: 'script' => $script, Chris@14: 'args' => array(array('ELEMENT' => $element->getID())), Chris@14: ); Chris@14: Chris@14: if ($sync) { Chris@14: return $this->wdSession->execute($options); Chris@14: } Chris@14: Chris@14: return $this->wdSession->execute_async($options); Chris@14: } Chris@14: Chris@14: /** Chris@14: * {@inheritdoc} Chris@14: */ Chris@14: public function start() Chris@14: { Chris@14: try { Chris@14: $this->wdSession = $this->webDriver->session($this->browserName, $this->desiredCapabilities); Chris@14: $this->applyTimeouts(); Chris@14: } catch (\Exception $e) { Chris@14: throw new DriverException('Could not open connection: '.$e->getMessage(), 0, $e); Chris@14: } Chris@14: Chris@14: if (!$this->wdSession) { Chris@14: throw new DriverException('Could not connect to a Selenium 2 / WebDriver server'); Chris@14: } Chris@14: $this->started = true; Chris@14: } Chris@14: Chris@14: /** Chris@14: * Sets the timeouts to apply to the webdriver session Chris@14: * Chris@14: * @param array $timeouts The session timeout settings: Array of {script, implicit, page} => time in milliseconds Chris@14: * Chris@14: * @throws DriverException Chris@14: */ Chris@14: public function setTimeouts($timeouts) Chris@14: { Chris@14: $this->timeouts = $timeouts; Chris@14: Chris@14: if ($this->isStarted()) { Chris@14: $this->applyTimeouts(); Chris@14: } Chris@14: } Chris@14: Chris@14: /** Chris@14: * Applies timeouts to the current session Chris@14: */ Chris@14: private function applyTimeouts() Chris@14: { Chris@14: try { Chris@14: foreach ($this->timeouts as $type => $param) { Chris@14: $this->wdSession->timeouts($type, $param); Chris@14: } Chris@14: } catch (UnknownError $e) { Chris@14: throw new DriverException('Error setting timeout: ' . $e->getMessage(), 0, $e); Chris@14: } Chris@14: } Chris@14: Chris@14: /** Chris@14: * {@inheritdoc} Chris@14: */ Chris@14: public function isStarted() Chris@14: { Chris@14: return $this->started; Chris@14: } Chris@14: Chris@14: /** Chris@14: * {@inheritdoc} Chris@14: */ Chris@14: public function stop() Chris@14: { Chris@14: if (!$this->wdSession) { Chris@14: throw new DriverException('Could not connect to a Selenium 2 / WebDriver server'); Chris@14: } Chris@14: Chris@14: $this->started = false; Chris@14: try { Chris@14: $this->wdSession->close(); Chris@14: } catch (\Exception $e) { Chris@14: throw new DriverException('Could not close connection', 0, $e); Chris@14: } Chris@14: } Chris@14: Chris@14: /** Chris@14: * {@inheritdoc} Chris@14: */ Chris@14: public function reset() Chris@14: { Chris@14: $this->wdSession->deleteAllCookies(); Chris@14: } Chris@14: Chris@14: /** Chris@14: * {@inheritdoc} Chris@14: */ Chris@14: public function visit($url) Chris@14: { Chris@14: $this->wdSession->open($url); Chris@14: } Chris@14: Chris@14: /** Chris@14: * {@inheritdoc} Chris@14: */ Chris@14: public function getCurrentUrl() Chris@14: { Chris@14: return $this->wdSession->url(); Chris@14: } Chris@14: Chris@14: /** Chris@14: * {@inheritdoc} Chris@14: */ Chris@14: public function reload() Chris@14: { Chris@14: $this->wdSession->refresh(); Chris@14: } Chris@14: Chris@14: /** Chris@14: * {@inheritdoc} Chris@14: */ Chris@14: public function forward() Chris@14: { Chris@14: $this->wdSession->forward(); Chris@14: } Chris@14: Chris@14: /** Chris@14: * {@inheritdoc} Chris@14: */ Chris@14: public function back() Chris@14: { Chris@14: $this->wdSession->back(); Chris@14: } Chris@14: Chris@14: /** Chris@14: * {@inheritdoc} Chris@14: */ Chris@14: public function switchToWindow($name = null) Chris@14: { Chris@14: $this->wdSession->focusWindow($name ? $name : ''); Chris@14: } Chris@14: Chris@14: /** Chris@14: * {@inheritdoc} Chris@14: */ Chris@14: public function switchToIFrame($name = null) Chris@14: { Chris@14: $this->wdSession->frame(array('id' => $name)); Chris@14: } Chris@14: Chris@14: /** Chris@14: * {@inheritdoc} Chris@14: */ Chris@14: public function setCookie($name, $value = null) Chris@14: { Chris@14: if (null === $value) { Chris@14: $this->wdSession->deleteCookie($name); Chris@14: Chris@14: return; Chris@14: } Chris@14: Chris@14: $cookieArray = array( Chris@14: 'name' => $name, Chris@14: 'value' => urlencode($value), Chris@14: 'secure' => false, // thanks, chibimagic! Chris@14: ); Chris@14: Chris@14: $this->wdSession->setCookie($cookieArray); Chris@14: } Chris@14: Chris@14: /** Chris@14: * {@inheritdoc} Chris@14: */ Chris@14: public function getCookie($name) Chris@14: { Chris@14: $cookies = $this->wdSession->getAllCookies(); Chris@14: foreach ($cookies as $cookie) { Chris@14: if ($cookie['name'] === $name) { Chris@14: return urldecode($cookie['value']); Chris@14: } Chris@14: } Chris@14: } Chris@14: Chris@14: /** Chris@14: * {@inheritdoc} Chris@14: */ Chris@14: public function getContent() Chris@14: { Chris@14: return $this->wdSession->source(); Chris@14: } Chris@14: Chris@14: /** Chris@14: * {@inheritdoc} Chris@14: */ Chris@14: public function getScreenshot() Chris@14: { Chris@14: return base64_decode($this->wdSession->screenshot()); Chris@14: } Chris@14: Chris@14: /** Chris@14: * {@inheritdoc} Chris@14: */ Chris@14: public function getWindowNames() Chris@14: { Chris@14: return $this->wdSession->window_handles(); Chris@14: } Chris@14: Chris@14: /** Chris@14: * {@inheritdoc} Chris@14: */ Chris@14: public function getWindowName() Chris@14: { Chris@14: return $this->wdSession->window_handle(); Chris@14: } Chris@14: Chris@14: /** Chris@14: * {@inheritdoc} Chris@14: */ Chris@14: public function findElementXpaths($xpath) Chris@14: { Chris@14: $nodes = $this->wdSession->elements('xpath', $xpath); Chris@14: Chris@14: $elements = array(); Chris@14: foreach ($nodes as $i => $node) { Chris@14: $elements[] = sprintf('(%s)[%d]', $xpath, $i+1); Chris@14: } Chris@14: Chris@14: return $elements; Chris@14: } Chris@14: Chris@14: /** Chris@14: * {@inheritdoc} Chris@14: */ Chris@14: public function getTagName($xpath) Chris@14: { Chris@14: return $this->findElement($xpath)->name(); Chris@14: } Chris@14: Chris@14: /** Chris@14: * {@inheritdoc} Chris@14: */ Chris@14: public function getText($xpath) Chris@14: { Chris@14: $node = $this->findElement($xpath); Chris@14: $text = $node->text(); Chris@14: $text = (string) str_replace(array("\r", "\r\n", "\n"), ' ', $text); Chris@14: Chris@14: return $text; Chris@14: } Chris@14: Chris@14: /** Chris@14: * {@inheritdoc} Chris@14: */ Chris@14: public function getHtml($xpath) Chris@14: { Chris@14: return $this->executeJsOnXpath($xpath, 'return {{ELEMENT}}.innerHTML;'); Chris@14: } Chris@14: Chris@14: /** Chris@14: * {@inheritdoc} Chris@14: */ Chris@14: public function getOuterHtml($xpath) Chris@14: { Chris@14: return $this->executeJsOnXpath($xpath, 'return {{ELEMENT}}.outerHTML;'); Chris@14: } Chris@14: Chris@14: /** Chris@14: * {@inheritdoc} Chris@14: */ Chris@14: public function getAttribute($xpath, $name) Chris@14: { Chris@14: $script = 'return {{ELEMENT}}.getAttribute(' . json_encode((string) $name) . ')'; Chris@14: Chris@14: return $this->executeJsOnXpath($xpath, $script); Chris@14: } Chris@14: Chris@14: /** Chris@14: * {@inheritdoc} Chris@14: */ Chris@14: public function getValue($xpath) Chris@14: { Chris@14: $element = $this->findElement($xpath); Chris@14: $elementName = strtolower($element->name()); Chris@14: $elementType = strtolower($element->attribute('type')); Chris@14: Chris@14: // Getting the value of a checkbox returns its value if selected. Chris@14: if ('input' === $elementName && 'checkbox' === $elementType) { Chris@14: return $element->selected() ? $element->attribute('value') : null; Chris@14: } Chris@14: Chris@14: if ('input' === $elementName && 'radio' === $elementType) { Chris@14: $script = <<executeJsOnElement($element, $script); Chris@14: } Chris@14: Chris@14: // Using $element->attribute('value') on a select only returns the first selected option Chris@14: // even when it is a multiple select, so a custom retrieval is needed. Chris@14: if ('select' === $elementName && $element->attribute('multiple')) { Chris@14: $script = <<executeJsOnElement($element, $script); Chris@14: } Chris@14: Chris@14: return $element->attribute('value'); Chris@14: } Chris@14: Chris@14: /** Chris@14: * {@inheritdoc} Chris@14: */ Chris@14: public function setValue($xpath, $value) Chris@14: { Chris@14: $element = $this->findElement($xpath); Chris@14: $elementName = strtolower($element->name()); Chris@14: Chris@14: if ('select' === $elementName) { Chris@14: if (is_array($value)) { Chris@14: $this->deselectAllOptions($element); Chris@14: Chris@14: foreach ($value as $option) { Chris@14: $this->selectOptionOnElement($element, $option, true); Chris@14: } Chris@14: Chris@14: return; Chris@14: } Chris@14: Chris@14: $this->selectOptionOnElement($element, $value); Chris@14: Chris@14: return; Chris@14: } Chris@14: Chris@14: if ('input' === $elementName) { Chris@14: $elementType = strtolower($element->attribute('type')); Chris@14: Chris@14: if (in_array($elementType, array('submit', 'image', 'button', 'reset'))) { Chris@14: throw new DriverException(sprintf('Impossible to set value an element with XPath "%s" as it is not a select, textarea or textbox', $xpath)); Chris@14: } Chris@14: Chris@14: if ('checkbox' === $elementType) { Chris@14: if ($element->selected() xor (bool) $value) { Chris@14: $this->clickOnElement($element); Chris@14: } Chris@14: Chris@14: return; Chris@14: } Chris@14: Chris@14: if ('radio' === $elementType) { Chris@14: $this->selectRadioValue($element, $value); Chris@14: Chris@14: return; Chris@14: } Chris@14: Chris@14: if ('file' === $elementType) { Chris@14: $element->postValue(array('value' => array(strval($value)))); Chris@14: Chris@14: return; Chris@14: } Chris@14: } Chris@14: Chris@14: $value = strval($value); Chris@14: Chris@14: if (in_array($elementName, array('input', 'textarea'))) { Chris@14: $existingValueLength = strlen($element->attribute('value')); Chris@14: // Add the TAB key to ensure we unfocus the field as browsers are triggering the change event only Chris@14: // after leaving the field. Chris@14: $value = str_repeat(Key::BACKSPACE . Key::DELETE, $existingValueLength) . $value; Chris@14: } Chris@14: Chris@14: $element->postValue(array('value' => array($value))); Chris@16: // Remove the focus from the element if the field still has focus in Chris@16: // order to trigger the change event. By doing this instead of simply Chris@16: // triggering the change event for the given xpath we ensure that the Chris@16: // change event will not be triggered twice for the same element if it Chris@16: // has lost focus in the meanwhile. If the element has lost focus Chris@16: // already then there is nothing to do as this will already have caused Chris@16: // the triggering of the change event for that element. Chris@16: $script = <<executeJsOnElement($element, $script); Chris@14: } Chris@14: Chris@14: /** Chris@14: * {@inheritdoc} Chris@14: */ Chris@14: public function check($xpath) Chris@14: { Chris@14: $element = $this->findElement($xpath); Chris@14: $this->ensureInputType($element, $xpath, 'checkbox', 'check'); Chris@14: Chris@14: if ($element->selected()) { Chris@14: return; Chris@14: } Chris@14: Chris@14: $this->clickOnElement($element); Chris@14: } Chris@14: Chris@14: /** Chris@14: * {@inheritdoc} Chris@14: */ Chris@14: public function uncheck($xpath) Chris@14: { Chris@14: $element = $this->findElement($xpath); Chris@14: $this->ensureInputType($element, $xpath, 'checkbox', 'uncheck'); Chris@14: Chris@14: if (!$element->selected()) { Chris@14: return; Chris@14: } Chris@14: Chris@14: $this->clickOnElement($element); Chris@14: } Chris@14: Chris@14: /** Chris@14: * {@inheritdoc} Chris@14: */ Chris@14: public function isChecked($xpath) Chris@14: { Chris@14: return $this->findElement($xpath)->selected(); Chris@14: } Chris@14: Chris@14: /** Chris@14: * {@inheritdoc} Chris@14: */ Chris@14: public function selectOption($xpath, $value, $multiple = false) Chris@14: { Chris@14: $element = $this->findElement($xpath); Chris@14: $tagName = strtolower($element->name()); Chris@14: Chris@14: if ('input' === $tagName && 'radio' === strtolower($element->attribute('type'))) { Chris@14: $this->selectRadioValue($element, $value); Chris@14: Chris@14: return; Chris@14: } Chris@14: Chris@14: if ('select' === $tagName) { Chris@14: $this->selectOptionOnElement($element, $value, $multiple); Chris@14: Chris@14: return; Chris@14: } Chris@14: Chris@14: 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)); Chris@14: } Chris@14: Chris@14: /** Chris@14: * {@inheritdoc} Chris@14: */ Chris@14: public function isSelected($xpath) Chris@14: { Chris@14: return $this->findElement($xpath)->selected(); Chris@14: } Chris@14: Chris@14: /** Chris@14: * {@inheritdoc} Chris@14: */ Chris@14: public function click($xpath) Chris@14: { Chris@14: $this->clickOnElement($this->findElement($xpath)); Chris@14: } Chris@14: Chris@14: private function clickOnElement(Element $element) Chris@14: { Chris@14: try { Chris@14: // Move the mouse to the element as Selenium does not allow clicking on an element which is outside the viewport Chris@14: $this->wdSession->moveto(array('element' => $element->getID())); Chris@14: } catch (UnknownCommand $e) { Chris@14: // If the Webdriver implementation does not support moveto (which is not part of the W3C WebDriver spec), proceed to the click Chris@14: } Chris@14: Chris@14: $element->click(); Chris@14: } Chris@14: Chris@14: /** Chris@14: * {@inheritdoc} Chris@14: */ Chris@14: public function doubleClick($xpath) Chris@14: { Chris@14: $this->mouseOver($xpath); Chris@14: $this->wdSession->doubleclick(); Chris@14: } Chris@14: Chris@14: /** Chris@14: * {@inheritdoc} Chris@14: */ Chris@14: public function rightClick($xpath) Chris@14: { Chris@14: $this->mouseOver($xpath); Chris@14: $this->wdSession->click(array('button' => 2)); Chris@14: } Chris@14: Chris@14: /** Chris@14: * {@inheritdoc} Chris@14: */ Chris@14: public function attachFile($xpath, $path) Chris@14: { Chris@14: $element = $this->findElement($xpath); Chris@14: $this->ensureInputType($element, $xpath, 'file', 'attach a file on'); Chris@14: Chris@14: // Upload the file to Selenium and use the remote path. This will Chris@14: // ensure that Selenium always has access to the file, even if it runs Chris@14: // as a remote instance. Chris@14: try { Chris@14: $remotePath = $this->uploadFile($path); Chris@14: } catch (\Exception $e) { Chris@14: // File could not be uploaded to remote instance. Use the local path. Chris@14: $remotePath = $path; Chris@14: } Chris@14: Chris@14: $element->postValue(array('value' => array($remotePath))); Chris@14: } Chris@14: Chris@14: /** Chris@14: * {@inheritdoc} Chris@14: */ Chris@14: public function isVisible($xpath) Chris@14: { Chris@14: return $this->findElement($xpath)->displayed(); Chris@14: } Chris@14: Chris@14: /** Chris@14: * {@inheritdoc} Chris@14: */ Chris@14: public function mouseOver($xpath) Chris@14: { Chris@14: $this->wdSession->moveto(array( Chris@14: 'element' => $this->findElement($xpath)->getID() Chris@14: )); Chris@14: } Chris@14: Chris@14: /** Chris@14: * {@inheritdoc} Chris@14: */ Chris@14: public function focus($xpath) Chris@14: { Chris@14: $this->trigger($xpath, 'focus'); Chris@14: } Chris@14: Chris@14: /** Chris@14: * {@inheritdoc} Chris@14: */ Chris@14: public function blur($xpath) Chris@14: { Chris@14: $this->trigger($xpath, 'blur'); Chris@14: } Chris@14: Chris@14: /** Chris@14: * {@inheritdoc} Chris@14: */ Chris@14: public function keyPress($xpath, $char, $modifier = null) Chris@14: { Chris@14: $options = self::charToOptions($char, $modifier); Chris@14: $this->trigger($xpath, 'keypress', $options); Chris@14: } Chris@14: Chris@14: /** Chris@14: * {@inheritdoc} Chris@14: */ Chris@14: public function keyDown($xpath, $char, $modifier = null) Chris@14: { Chris@14: $options = self::charToOptions($char, $modifier); Chris@14: $this->trigger($xpath, 'keydown', $options); Chris@14: } Chris@14: Chris@14: /** Chris@14: * {@inheritdoc} Chris@14: */ Chris@14: public function keyUp($xpath, $char, $modifier = null) Chris@14: { Chris@14: $options = self::charToOptions($char, $modifier); Chris@14: $this->trigger($xpath, 'keyup', $options); Chris@14: } Chris@14: Chris@14: /** Chris@14: * {@inheritdoc} Chris@14: */ Chris@14: public function dragTo($sourceXpath, $destinationXpath) Chris@14: { Chris@14: $source = $this->findElement($sourceXpath); Chris@14: $destination = $this->findElement($destinationXpath); Chris@14: Chris@14: $this->wdSession->moveto(array( Chris@14: 'element' => $source->getID() Chris@14: )); Chris@14: Chris@14: $script = <<withSyn()->executeJsOnElement($source, $script); Chris@14: Chris@14: $this->wdSession->buttondown(); Chris@14: $this->wdSession->moveto(array( Chris@14: 'element' => $destination->getID() Chris@14: )); Chris@14: $this->wdSession->buttonup(); Chris@14: Chris@14: $script = <<withSyn()->executeJsOnElement($destination, $script); Chris@14: } Chris@14: Chris@14: /** Chris@14: * {@inheritdoc} Chris@14: */ Chris@14: public function executeScript($script) Chris@14: { Chris@14: if (preg_match('/^function[\s\(]/', $script)) { Chris@14: $script = preg_replace('/;$/', '', $script); Chris@14: $script = '(' . $script . ')'; Chris@14: } Chris@14: Chris@14: $this->wdSession->execute(array('script' => $script, 'args' => array())); Chris@14: } Chris@14: Chris@14: /** Chris@14: * {@inheritdoc} Chris@14: */ Chris@14: public function evaluateScript($script) Chris@14: { Chris@14: if (0 !== strpos(trim($script), 'return ')) { Chris@14: $script = 'return ' . $script; Chris@14: } Chris@14: Chris@14: return $this->wdSession->execute(array('script' => $script, 'args' => array())); Chris@14: } Chris@14: Chris@14: /** Chris@14: * {@inheritdoc} Chris@14: */ Chris@14: public function wait($timeout, $condition) Chris@14: { Chris@14: $script = "return $condition;"; Chris@14: $start = microtime(true); Chris@14: $end = $start + $timeout / 1000.0; Chris@14: Chris@14: do { Chris@14: $result = $this->wdSession->execute(array('script' => $script, 'args' => array())); Chris@14: usleep(100000); Chris@14: } while (microtime(true) < $end && !$result); Chris@14: Chris@14: return (bool) $result; Chris@14: } Chris@14: Chris@14: /** Chris@14: * {@inheritdoc} Chris@14: */ Chris@14: public function resizeWindow($width, $height, $name = null) Chris@14: { Chris@14: $this->wdSession->window($name ? $name : 'current')->postSize( Chris@14: array('width' => $width, 'height' => $height) Chris@14: ); Chris@14: } Chris@14: Chris@14: /** Chris@14: * {@inheritdoc} Chris@14: */ Chris@14: public function submitForm($xpath) Chris@14: { Chris@14: $this->findElement($xpath)->submit(); Chris@14: } Chris@14: Chris@14: /** Chris@14: * {@inheritdoc} Chris@14: */ Chris@14: public function maximizeWindow($name = null) Chris@14: { Chris@14: $this->wdSession->window($name ? $name : 'current')->maximize(); Chris@14: } Chris@14: Chris@14: /** Chris@14: * Returns Session ID of WebDriver or `null`, when session not started yet. Chris@14: * Chris@14: * @return string|null Chris@14: */ Chris@14: public function getWebDriverSessionId() Chris@14: { Chris@14: return $this->isStarted() ? basename($this->wdSession->getUrl()) : null; Chris@14: } Chris@14: Chris@14: /** Chris@14: * @param string $xpath Chris@14: * Chris@14: * @return Element Chris@14: */ Chris@14: private function findElement($xpath) Chris@14: { Chris@14: return $this->wdSession->element('xpath', $xpath); Chris@14: } Chris@14: Chris@14: /** Chris@14: * Selects a value in a radio button group Chris@14: * Chris@14: * @param Element $element An element referencing one of the radio buttons of the group Chris@14: * @param string $value The value to select Chris@14: * Chris@14: * @throws DriverException when the value cannot be found Chris@14: */ Chris@14: private function selectRadioValue(Element $element, $value) Chris@14: { Chris@14: // short-circuit when we already have the right button of the group to avoid XPath queries Chris@14: if ($element->attribute('value') === $value) { Chris@14: $element->click(); Chris@14: Chris@14: return; Chris@14: } Chris@14: Chris@14: $name = $element->attribute('name'); Chris@14: Chris@14: if (!$name) { Chris@14: throw new DriverException(sprintf('The radio button does not have the value "%s"', $value)); Chris@14: } Chris@14: Chris@14: $formId = $element->attribute('form'); Chris@14: Chris@14: try { Chris@14: if (null !== $formId) { Chris@14: $xpath = <<<'XPATH' Chris@14: //form[@id=%1$s]//input[@type="radio" and not(@form) and @name=%2$s and @value = %3$s] Chris@14: | Chris@14: //input[@type="radio" and @form=%1$s and @name=%2$s and @value = %3$s] Chris@14: XPATH; Chris@14: Chris@14: $xpath = sprintf( Chris@14: $xpath, Chris@14: $this->xpathEscaper->escapeLiteral($formId), Chris@14: $this->xpathEscaper->escapeLiteral($name), Chris@14: $this->xpathEscaper->escapeLiteral($value) Chris@14: ); Chris@14: $input = $this->wdSession->element('xpath', $xpath); Chris@14: } else { Chris@14: $xpath = sprintf( Chris@14: './ancestor::form//input[@type="radio" and not(@form) and @name=%s and @value = %s]', Chris@14: $this->xpathEscaper->escapeLiteral($name), Chris@14: $this->xpathEscaper->escapeLiteral($value) Chris@14: ); Chris@14: $input = $element->element('xpath', $xpath); Chris@14: } Chris@14: } catch (NoSuchElement $e) { Chris@14: $message = sprintf('The radio group "%s" does not have an option "%s"', $name, $value); Chris@14: Chris@14: throw new DriverException($message, 0, $e); Chris@14: } Chris@14: Chris@14: $input->click(); Chris@14: } Chris@14: Chris@14: /** Chris@14: * @param Element $element Chris@14: * @param string $value Chris@14: * @param bool $multiple Chris@14: */ Chris@14: private function selectOptionOnElement(Element $element, $value, $multiple = false) Chris@14: { Chris@14: $escapedValue = $this->xpathEscaper->escapeLiteral($value); Chris@14: // The value of an option is the normalized version of its text when it has no value attribute Chris@14: $optionQuery = sprintf('.//option[@value = %s or (not(@value) and normalize-space(.) = %s)]', $escapedValue, $escapedValue); Chris@14: $option = $element->element('xpath', $optionQuery); Chris@14: Chris@14: if ($multiple || !$element->attribute('multiple')) { Chris@14: if (!$option->selected()) { Chris@14: $option->click(); Chris@14: } Chris@14: Chris@14: return; Chris@14: } Chris@14: Chris@14: // Deselect all options before selecting the new one Chris@14: $this->deselectAllOptions($element); Chris@14: $option->click(); Chris@14: } Chris@14: Chris@14: /** Chris@14: * Deselects all options of a multiple select Chris@14: * Chris@14: * Note: this implementation does not trigger a change event after deselecting the elements. Chris@14: * Chris@14: * @param Element $element Chris@14: */ Chris@14: private function deselectAllOptions(Element $element) Chris@14: { Chris@14: $script = <<executeJsOnElement($element, $script); Chris@14: } Chris@14: Chris@14: /** Chris@14: * Ensures the element is a checkbox Chris@14: * Chris@14: * @param Element $element Chris@14: * @param string $xpath Chris@14: * @param string $type Chris@14: * @param string $action Chris@14: * Chris@14: * @throws DriverException Chris@14: */ Chris@14: private function ensureInputType(Element $element, $xpath, $type, $action) Chris@14: { Chris@14: if ('input' !== strtolower($element->name()) || $type !== strtolower($element->attribute('type'))) { Chris@14: $message = 'Impossible to %s the element with XPath "%s" as it is not a %s input'; Chris@14: Chris@14: throw new DriverException(sprintf($message, $action, $xpath, $type)); Chris@14: } Chris@14: } Chris@14: Chris@14: /** Chris@14: * @param $xpath Chris@14: * @param $event Chris@14: * @param string $options Chris@14: */ Chris@14: private function trigger($xpath, $event, $options = '{}') Chris@14: { Chris@14: $script = 'Syn.trigger("' . $event . '", ' . $options . ', {{ELEMENT}})'; Chris@14: $this->withSyn()->executeJsOnXpath($xpath, $script); Chris@14: } Chris@14: Chris@14: /** Chris@14: * Uploads a file to the Selenium instance. Chris@14: * Chris@14: * Note that uploading files is not part of the official WebDriver Chris@14: * specification, but it is supported by Selenium. Chris@14: * Chris@14: * @param string $path The path to the file to upload. Chris@14: * Chris@14: * @return string The remote path. Chris@14: * Chris@14: * @throws DriverException When PHP is compiled without zip support, or the file doesn't exist. Chris@14: * @throws UnknownError When an unknown error occurred during file upload. Chris@14: * @throws \Exception When a known error occurred during file upload. Chris@14: * Chris@14: * @see https://github.com/SeleniumHQ/selenium/blob/master/py/selenium/webdriver/remote/webelement.py#L533 Chris@14: */ Chris@14: private function uploadFile($path) Chris@14: { Chris@14: if (!is_file($path)) { Chris@14: throw new DriverException('File does not exist locally and cannot be uploaded to the remote instance.'); Chris@14: } Chris@14: Chris@14: if (!class_exists('ZipArchive')) { Chris@14: throw new DriverException('Could not compress file, PHP is compiled without zip support.'); Chris@14: } Chris@14: Chris@14: // Selenium only accepts uploads that are compressed as a Zip archive. Chris@14: $tempFilename = tempnam('', 'WebDriverZip'); Chris@14: Chris@14: $archive = new \ZipArchive(); Chris@14: $result = $archive->open($tempFilename, \ZipArchive::CREATE); Chris@14: if (!$result) { Chris@14: throw new DriverException('Zip archive could not be created. Error ' . $result); Chris@14: } Chris@14: $result = $archive->addFile($path, basename($path)); Chris@14: if (!$result) { Chris@14: throw new DriverException('File could not be added to zip archive.'); Chris@14: } Chris@14: $result = $archive->close(); Chris@14: if (!$result) { Chris@14: throw new DriverException('Zip archive could not be closed.'); Chris@14: } Chris@14: Chris@14: try { Chris@14: $remotePath = $this->wdSession->file(array('file' => base64_encode(file_get_contents($tempFilename)))); Chris@14: Chris@14: // If no path is returned the file upload failed silently. In this Chris@14: // case it is possible Selenium was not used but another web driver Chris@14: // such as PhantomJS. Chris@14: // @todo Support other drivers when (if) they get remote file transfer Chris@14: // capability. Chris@14: if (empty($remotePath)) { Chris@14: throw new UnknownError(); Chris@14: } Chris@14: } catch (\Exception $e) { Chris@14: // Catch any error so we can still clean up the temporary archive. Chris@14: } Chris@14: Chris@14: unlink($tempFilename); Chris@14: Chris@14: if (isset($e)) { Chris@14: throw $e; Chris@14: } Chris@14: Chris@14: return $remotePath; Chris@14: } Chris@14: Chris@14: }