From 3f73de8364e7285115750e7e47b9a27e08c730d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Machulda?= Date: Fri, 14 Jun 2019 11:33:44 +0200 Subject: [PATCH 001/350] Revert Chromedriver bug workaround, which is fixed in Chromedriver 75.0.3770.90 --- .travis.yml | 2 +- CHANGELOG.md | 3 +++ lib/Remote/HttpCommandExecutor.php | 5 ----- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index b6850baf1..ac6cc1e24 100644 --- a/.travis.yml +++ b/.travis.yml @@ -47,7 +47,7 @@ matrix: # Stable Chrome + Chromedriver 75+ inside Travis environment - php: '7.3' - env: BROWSER_NAME="chrome" CHROME_HEADLESS="1" CHROMEDRIVER_VERSION="75.0.3770.8" + env: BROWSER_NAME="chrome" CHROME_HEADLESS="1" CHROMEDRIVER_VERSION="75.0.3770.90" addons: chrome: stable diff --git a/CHANGELOG.md b/CHANGELOG.md index a5e4ca748..f1e733516 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,9 @@ This project versioning adheres to [Semantic Versioning](http://semver.org/). ## Unreleased +### Changed +- Revert no longer needed workaround for Chromedriver bug [2943](https://bugs.chromium.org/p/chromedriver/issues/detail?id=2943). + ## 1.7.1 - 2019-06-13 ### Fixed - Error `Call to a member function toArray()` if capabilities were already converted to an array. diff --git a/lib/Remote/HttpCommandExecutor.php b/lib/Remote/HttpCommandExecutor.php index 1891fdb35..f584324b7 100644 --- a/lib/Remote/HttpCommandExecutor.php +++ b/lib/Remote/HttpCommandExecutor.php @@ -273,11 +273,6 @@ public function execute(WebDriverCommand $command) if ($http_method === 'POST' && $params && is_array($params)) { $encoded_params = json_encode($params); - } elseif ($http_method === 'POST' && $encoded_params === null) { - // Workaround for bug https://bugs.chromium.org/p/chromedriver/issues/detail?id=2943 in Chrome 75. - // Chromedriver now erroneously does not allow POST body to be empty even for the JsonWire protocol. - // If the command POST is empty, here we send some dummy data as a workaround: - $encoded_params = json_encode(['_' => '_']); } curl_setopt($this->curl, CURLOPT_POSTFIELDS, $encoded_params); From 4f8b32289daa41c9c96b6a9dbfcb352a24e5af1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Machulda?= Date: Fri, 1 Nov 2019 19:16:13 +0000 Subject: [PATCH 002/350] Download latest chromedriver without a need of manual version bumping --- .travis.yml | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/.travis.yml b/.travis.yml index ac6cc1e24..37dfc2cf1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -39,15 +39,9 @@ matrix: addons: firefox: "45.8.0esr" - # Stable Chrome + Chromedriver 74 inside Travis environment + # Stable Chrome + Chromedriver inside Travis environment - php: '7.3' - env: BROWSER_NAME="chrome" CHROME_HEADLESS="1" CHROMEDRIVER_VERSION="74.0.3729.6" - addons: - chrome: stable - - # Stable Chrome + Chromedriver 75+ inside Travis environment - - php: '7.3' - env: BROWSER_NAME="chrome" CHROME_HEADLESS="1" CHROMEDRIVER_VERSION="75.0.3770.90" + env: BROWSER_NAME="chrome" CHROME_HEADLESS="1" addons: chrome: stable @@ -102,7 +96,7 @@ install: - travis_retry composer update --no-interaction $DEPENDENCIES before_script: - - if [ "$BROWSER_NAME" = "chrome" ]; then mkdir chromedriver; wget -q -t 3 https://chromedriver.storage.googleapis.com/$CHROMEDRIVER_VERSION/chromedriver_linux64.zip; unzip chromedriver_linux64 -d chromedriver; fi + - if [ "$BROWSER_NAME" = "chrome" ]; then mkdir chromedriver; CHROMEDRIVER_VERSION=$(wget -qO- "https://chromedriver.storage.googleapis.com/LATEST_RELEASE"); wget -q -t 3 https://chromedriver.storage.googleapis.com/$CHROMEDRIVER_VERSION/chromedriver_linux64.zip; unzip chromedriver_linux64 -d chromedriver; fi - if [ "$BROWSER_NAME" = "chrome" ]; then export CHROMEDRIVER_PATH=$PWD/chromedriver/chromedriver; fi - sh -e /etc/init.d/xvfb start - if [ ! -f jar/selenium-server-standalone-3.8.1.jar ]; then wget -q -t 3 -P jar https://selenium-release.storage.googleapis.com/3.8/selenium-server-standalone-3.8.1.jar; fi From 87be63361d6670b8110b7d084887746b1adc685b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Mon, 9 Apr 2018 16:25:57 +0200 Subject: [PATCH 003/350] W3C-compatible implementation --- .travis.yml | 12 +- CONTRIBUTING.md | 10 +- lib/AbstractWebDriverCheckboxOrRadio.php | 2 +- lib/Cookie.php | 8 +- lib/Exception/WebDriverException.php | 50 ++++- lib/Interactions/WebDriverActions.php | 1 - lib/Remote/DriverCommand.php | 4 + lib/Remote/HttpCommandExecutor.php | 63 ++++++- lib/Remote/JsonWireCompat.php | 102 ++++++++++ lib/Remote/RemoteMouse.php | 174 +++++++++++++++++- lib/Remote/RemoteTargetLocator.php | 4 +- lib/Remote/RemoteWebDriver.php | 82 ++++++--- lib/Remote/RemoteWebElement.php | 112 ++++++++--- lib/WebDriverDimension.php | 12 +- lib/WebDriverOptions.php | 9 +- lib/WebDriverPoint.php | 4 +- lib/WebDriverTimeouts.php | 34 +++- .../functional/RemoteWebDriverCreateTest.php | 5 +- .../RemoteWebDriverFindElementTest.php | 20 ++ tests/functional/RemoteWebDriverTest.php | 23 ++- tests/functional/RemoteWebElementTest.php | 4 + tests/functional/WebDriverActionsTest.php | 24 ++- tests/functional/WebDriverTestCase.php | 7 +- tests/functional/web/escape_css.html | 14 ++ tests/functional/web/upload.html | 2 +- 25 files changed, 699 insertions(+), 83 deletions(-) create mode 100644 lib/Remote/JsonWireCompat.php create mode 100644 tests/functional/web/escape_css.html diff --git a/.travis.yml b/.travis.yml index 37dfc2cf1..47ddb461e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -34,11 +34,21 @@ matrix: env: DEPENDENCIES="--prefer-lowest" # Firefox inside Travis environment - - php: '7.3' + - name: 'Firefox 45 on Travis (OSS protocol)' + php: '7.3' env: BROWSER_NAME="firefox" addons: firefox: "45.8.0esr" + # Firefox with Geckodriver (W3C mode) inside Travis environment + - name: 'Firefox latest on Travis (W3C protocol)' + php: 7.3 + env: + - BROWSER_NAME="firefox" + - GECKODRIVER="1" + addons: + firefox: latest + # Stable Chrome + Chromedriver inside Travis environment - php: '7.3' env: BROWSER_NAME="chrome" CHROME_HEADLESS="1" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1a01c7adb..741a140a9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -37,10 +37,10 @@ For the functional tests you must first [download](http://selenium-release.stora the selenium standalone server, start the local PHP server which will serve the test pages and then run the `functional` test suite: - java -jar selenium-server-standalone-2.53.1.jar -log selenium.log & + java -jar selenium-server-standalone-3.9.1.jar -log selenium.log & php -S localhost:8000 -t tests/functional/web/ & ./vendor/bin/phpunit --testsuite functional - + The functional tests will be started in HtmlUnit headless browser by default. If you want to run them in eg. Firefox, simply set the `BROWSER_NAME` environment variable: @@ -48,6 +48,12 @@ simply set the `BROWSER_NAME` environment variable: export BROWSER_NAME="firefox" ./vendor/bin/phpunit --testsuite functional +To test with Geckodriver, [download](https://github.com/mozilla/geckodriver/releases) and start the server, then run: + + export GECKODRIVER=1 + export BROWSER_NAME=firefox + ./vendor/bin/phpunit --testsuite functional + ### Check coding style Your code-style should comply with [PSR-2](http://www.php-fig.org/psr/psr-2/). To make sure your code matches this requirement run: diff --git a/lib/AbstractWebDriverCheckboxOrRadio.php b/lib/AbstractWebDriverCheckboxOrRadio.php index f70a2ef04..1268ce7c0 100644 --- a/lib/AbstractWebDriverCheckboxOrRadio.php +++ b/lib/AbstractWebDriverCheckboxOrRadio.php @@ -211,7 +211,7 @@ protected function getRelatedElements($value = null) $form = $this->element->findElement(WebDriverBy::xpath('ancestor::form')); $formId = $form->getAttribute('id'); - if ($formId === '') { + if (!$formId) { return $form->findElements(WebDriverBy::xpath( sprintf('.//input[@name = %s%s]', XPathEscaper::escapeQuotes($this->name), $valueSelector) )); diff --git a/lib/Cookie.php b/lib/Cookie.php index 216ac3033..170b46486 100644 --- a/lib/Cookie.php +++ b/lib/Cookie.php @@ -190,7 +190,13 @@ public function isHttpOnly() */ public function toArray() { - return $this->cookie; + $cookie = $this->cookie; + if (!isset($cookie['secure'])) { + // Passing a boolean value for the "secure" flag is mandatory when using geckodriver + $cookie['secure'] = false; + } + + return $cookie; } public function offsetExists($offset) diff --git a/lib/Exception/WebDriverException.php b/lib/Exception/WebDriverException.php index 8545729ad..b17b2f70f 100644 --- a/lib/Exception/WebDriverException.php +++ b/lib/Exception/WebDriverException.php @@ -42,7 +42,7 @@ public function getResults() /** * Throw WebDriverExceptions based on WebDriver status code. * - * @param int $status_code + * @param int|string $status_code * @param string $message * @param mixed $results * @@ -85,6 +85,54 @@ public function getResults() */ public static function throwException($status_code, $message, $results) { + if (is_string($status_code)) { + // see https://w3c.github.io/webdriver/webdriver-spec.html#handling-errors + switch ($status_code) { + case 'no such element': + throw new NoSuchElementException($message, $results); + case 'no such frame': + throw new NoSuchFrameException($message, $results); + case 'unknown command': + throw new UnknownCommandException($message, $results); + case 'stale element reference': + throw new StaleElementReferenceException($message, $results); + case 'invalid element state': + throw new InvalidElementStateException($message, $results); + case 'unknown error': + throw new UnknownServerException($message, $results); + case 'unsupported operation': + throw new ExpectedException($message, $results); + case 'element not interactable': + throw new ElementNotSelectableException($message, $results); + case 'no such window': + throw new NoSuchDocumentException($message, $results); + case 'javascript error': + throw new UnexpectedJavascriptException($message, $results); + case 'timeout': + throw new TimeOutException($message, $results); + case 'no such window': + throw new NoSuchWindowException($message, $results); + case 'invalid cookie domain': + throw new InvalidCookieDomainException($message, $results); + case 'unable to set cookie': + throw new UnableToSetCookieException($message, $results); + case 'unexpected alert open': + throw new UnexpectedAlertOpenException($message, $results); + case 'no such alert': + throw new NoAlertOpenException($message, $results); + case 'script timeout': + throw new ScriptTimeoutException($message, $results); + case 'invalid selector': + throw new InvalidSelectorException($message, $results); + case 'session not created': + throw new SessionNotCreatedException($message, $results); + case 'move target out of bounds': + throw new MoveTargetOutOfBoundsException($message, $results); + default: + throw new UnrecognizedExceptionException($message, $results); + } + } + switch ($status_code) { case 1: throw new IndexOutOfBoundsException($message, $results); diff --git a/lib/Interactions/WebDriverActions.php b/lib/Interactions/WebDriverActions.php index e97796854..66ac7c1ab 100644 --- a/lib/Interactions/WebDriverActions.php +++ b/lib/Interactions/WebDriverActions.php @@ -25,7 +25,6 @@ use Facebook\WebDriver\Interactions\Internal\WebDriverMouseMoveAction; use Facebook\WebDriver\Interactions\Internal\WebDriverMoveToOffsetAction; use Facebook\WebDriver\Interactions\Internal\WebDriverSendKeysAction; -use Facebook\WebDriver\WebDriver; use Facebook\WebDriver\WebDriverElement; use Facebook\WebDriver\WebDriverHasInputDevices; diff --git a/lib/Remote/DriverCommand.php b/lib/Remote/DriverCommand.php index c69052030..fd24a691c 100644 --- a/lib/Remote/DriverCommand.php +++ b/lib/Remote/DriverCommand.php @@ -146,6 +146,10 @@ class DriverCommand const GET_NETWORK_CONNECTION = 'getNetworkConnection'; const SET_NETWORK_CONNECTION = 'setNetworkConnection'; + // W3C specific + const ACTIONS = 'actions'; + const GET_ELEMENT_PROPERTY = 'getElementProperty'; + private function __construct() { } diff --git a/lib/Remote/HttpCommandExecutor.php b/lib/Remote/HttpCommandExecutor.php index f584324b7..c783b2892 100644 --- a/lib/Remote/HttpCommandExecutor.php +++ b/lib/Remote/HttpCommandExecutor.php @@ -137,6 +137,29 @@ class HttpCommandExecutor implements WebDriverCommandExecutor DriverCommand::TOUCH_SCROLL => ['method' => 'POST', 'url' => '/session/:sessionId/touch/scroll'], DriverCommand::TOUCH_UP => ['method' => 'POST', 'url' => '/session/:sessionId/touch/up'], ]; + /** + * @var array Will be merged with $commands + */ + protected static $w3cCompliantCommands = [ + DriverCommand::ACCEPT_ALERT => ['method' => 'POST', 'url' => '/session/:sessionId/alert/accept'], + DriverCommand::ACTIONS => ['method' => 'POST', 'url' => '/session/:sessionId/actions'], + DriverCommand::DISMISS_ALERT => ['method' => 'POST', 'url' => '/session/:sessionId/alert/dismiss'], + DriverCommand::EXECUTE_SCRIPT => ['method' => 'POST', 'url' => '/session/:sessionId/execute/sync'], + DriverCommand::EXECUTE_ASYNC_SCRIPT => ['method' => 'POST', 'url' => '/session/:sessionId/execute/async'], + DriverCommand::GET_CURRENT_WINDOW_HANDLE => ['method' => 'GET', 'url' => '/session/:sessionId/window'], + DriverCommand::GET_ELEMENT_LOCATION => ['method' => 'GET', 'url' => '/session/:sessionId/element/:id/rect'], + DriverCommand::GET_ELEMENT_PROPERTY => [ + 'method' => 'GET', + 'url' => '/session/:sessionId/element/:id/property/:name', + ], + DriverCommand::GET_ELEMENT_SIZE => ['method' => 'GET', 'url' => '/session/:sessionId/element/:id/rect'], + DriverCommand::GET_WINDOW_HANDLES => ['method' => 'GET', 'url' => '/session/:sessionId/window/handles'], + DriverCommand::GET_ALERT_TEXT => ['method' => 'GET', 'url' => '/session/:sessionId/alert/text'], + DriverCommand::IMPLICITLY_WAIT => ['method' => 'POST', 'url' => '/session/:sessionId/timeouts'], + DriverCommand::SET_ALERT_VALUE => ['method' => 'POST', 'url' => '/session/:sessionId/alert/text'], + DriverCommand::SET_SCRIPT_TIMEOUT => ['method' => 'POST', 'url' => '/session/:sessionId/timeouts'], + DriverCommand::SET_TIMEOUT => ['method' => 'POST', 'url' => '/session/:sessionId/timeouts'], + ]; /** * @var string */ @@ -145,6 +168,10 @@ class HttpCommandExecutor implements WebDriverCommandExecutor * @var resource */ protected $curl; + /** + * @var bool + */ + protected $w3cCompliant = true; /** * @param string $url @@ -153,6 +180,8 @@ class HttpCommandExecutor implements WebDriverCommandExecutor */ public function __construct($url, $http_proxy = null, $http_proxy_port = null) { + self::$w3cCompliantCommands = array_merge(self::$commands, self::$w3cCompliantCommands); + $this->url = $url; $this->curl = curl_init(); @@ -179,6 +208,11 @@ public function __construct($url, $http_proxy = null, $http_proxy_port = null) $this->setConnectionTimeout(30000); } + public function disableW3CCompliance() + { + $this->w3cCompliant = false; + } + /** * Set timeout for the connect phase * @@ -226,11 +260,19 @@ public function setRequestTimeout($timeout_in_ms) */ public function execute(WebDriverCommand $command) { - if (!isset(self::$commands[$command->getName()])) { - throw new InvalidArgumentException($command->getName() . ' is not a valid command.'); + $commandName = $command->getName(); + if (!isset(self::$commands[$commandName])) { + if ($this->w3cCompliant && !isset(self::$w3cCompliantCommands[$commandName])) { + throw new InvalidArgumentException($command->getName() . ' is not a valid command.'); + } + } + + if ($this->w3cCompliant) { + $raw = self::$w3cCompliantCommands[$command->getName()]; + } else { + $raw = self::$commands[$command->getName()]; } - $raw = self::$commands[$command->getName()]; $http_method = $raw['method']; $url = $raw['url']; $url = str_replace(':sessionId', $command->getSessionID(), $url); @@ -317,12 +359,23 @@ public function execute(WebDriverCommand $command) } $sessionId = null; - if (is_array($results) && array_key_exists('sessionId', $results)) { + if (is_array($value) && array_key_exists('sessionId', $value)) { + // W3C's WebDriver + $sessionId = $value['sessionId']; + } elseif (is_array($results) && array_key_exists('sessionId', $results)) { + // Legacy JsonWire $sessionId = $results['sessionId']; } + // @see https://w3c.github.io/webdriver/webdriver-spec.html#handling-errors + if (isset($value['error'])) { + // W3C's WebDriver + WebDriverException::throwException($value['error'], $message, $results); + } + $status = isset($results['status']) ? $results['status'] : 0; - if ($status != 0) { + if ($status !== 0) { + // Legacy JsonWire WebDriverException::throwException($status, $message, $results); } diff --git a/lib/Remote/JsonWireCompat.php b/lib/Remote/JsonWireCompat.php new file mode 100644 index 000000000..78b9cba0e --- /dev/null +++ b/lib/Remote/JsonWireCompat.php @@ -0,0 +1,102 @@ +getMechanism(); + $value = $by->getValue(); + + if ($w3cCompliant) { + switch ($mechanism) { + // Convert to CSS selectors + case 'class name': + $mechanism = 'css selector'; + $value = sprintf('.%s', self::escapeSelector($value)); + break; + case 'id': + $mechanism = 'css selector'; + $value = sprintf('#%s', self::escapeSelector($value)); + break; + case 'name': + $mechanism = 'css selector'; + $value = sprintf('[name=\'%s\']', self::escapeSelector($value)); + break; + } + } + + return ['using' => $mechanism, 'value' => $value]; + } + + /** + * Escapes a CSS selector. + * + * Code adapted from the Zend Escaper project. + * + * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * @see https://github.com/zendframework/zend-escaper/blob/master/src/Escaper.php + * + * @param string $selector + * @return string + */ + private static function escapeSelector($selector) + { + return preg_replace_callback('/[^a-z0-9]/iSu', function ($matches) { + $chr = $matches[0]; + if (mb_strlen($chr) == 1) { + $ord = ord($chr); + } else { + $chr = mb_convert_encoding($chr, 'UTF-32BE', 'UTF-8'); + $ord = hexdec(bin2hex($chr)); + } + + return sprintf('\\%X ', $ord); + }, $selector); + } +} diff --git a/lib/Remote/RemoteMouse.php b/lib/Remote/RemoteMouse.php index 48c578df7..aa0d59829 100644 --- a/lib/Remote/RemoteMouse.php +++ b/lib/Remote/RemoteMouse.php @@ -27,13 +27,19 @@ class RemoteMouse implements WebDriverMouse * @var RemoteExecuteMethod */ private $executor; + /** + * @var bool + */ + private $w3cCompliant; /** * @param RemoteExecuteMethod $executor + * @param bool $w3cCompliant */ - public function __construct(RemoteExecuteMethod $executor) + public function __construct(RemoteExecuteMethod $executor, $w3cCompliant = false) { $this->executor = $executor; + $this->w3cCompliant = $w3cCompliant; } /** @@ -43,6 +49,22 @@ public function __construct(RemoteExecuteMethod $executor) */ public function click(WebDriverCoordinates $where = null) { + if ($this->w3cCompliant) { + $moveAction = $where ? [$this->createMoveAction($where)] : []; + $this->executor->execute(DriverCommand::ACTIONS, [ + 'actions' => [ + [ + 'type' => 'pointer', + 'id' => 'mouse', + 'parameters' => ['pointerType' => 'mouse'], + 'actions' => array_merge($moveAction, $this->createClickActions()), + ], + ], + ]); + + return $this; + } + $this->moveIfNeeded($where); $this->executor->execute(DriverCommand::CLICK, [ 'button' => 0, @@ -58,6 +80,33 @@ public function click(WebDriverCoordinates $where = null) */ public function contextClick(WebDriverCoordinates $where = null) { + if ($this->w3cCompliant) { + $moveAction = $where ? [$this->createMoveAction($where)] : []; + $this->executor->execute(DriverCommand::ACTIONS, [ + 'actions' => [ + [ + 'type' => 'pointer', + 'id' => 'mouse', + 'parameters' => ['pointerType' => 'mouse'], + 'actions' => array_merge($moveAction, [ + [ + 'type' => 'pointerDown', + 'duration' => 0, + 'button' => 2, + ], + [ + 'type' => 'pointerUp', + 'duration' => 0, + 'button' => 2, + ], + ]), + ], + ], + ]); + + return $this; + } + $this->moveIfNeeded($where); $this->executor->execute(DriverCommand::CLICK, [ 'button' => 2, @@ -73,6 +122,23 @@ public function contextClick(WebDriverCoordinates $where = null) */ public function doubleClick(WebDriverCoordinates $where = null) { + if ($this->w3cCompliant) { + $clickActions = $this->createClickActions(); + $moveAction = null === $where ? [] : [$this->createMoveAction($where)]; + $this->executor->execute(DriverCommand::ACTIONS, [ + 'actions' => [ + [ + 'type' => 'pointer', + 'id' => 'mouse', + 'parameters' => ['pointerType' => 'mouse'], + 'actions' => array_merge($moveAction, $clickActions, $clickActions), + ], + ], + ]); + + return $this; + } + $this->moveIfNeeded($where); $this->executor->execute(DriverCommand::DOUBLE_CLICK); @@ -86,6 +152,28 @@ public function doubleClick(WebDriverCoordinates $where = null) */ public function mouseDown(WebDriverCoordinates $where = null) { + if ($this->w3cCompliant) { + $this->executor->execute(DriverCommand::ACTIONS, [ + 'actions' => [ + [ + 'type' => 'pointer', + 'id' => 'mouse', + 'parameters' => ['pointerType' => 'mouse'], + 'actions' => [ + $this->createMoveAction($where), + [ + 'type' => 'pointerDown', + 'duration' => 0, + 'button' => 0, + ], + ], + ], + ], + ]); + + return $this; + } + $this->moveIfNeeded($where); $this->executor->execute(DriverCommand::MOUSE_DOWN); @@ -104,6 +192,21 @@ public function mouseMove( $x_offset = null, $y_offset = null ) { + if ($this->w3cCompliant) { + $this->executor->execute(DriverCommand::ACTIONS, [ + 'actions' => [ + [ + 'type' => 'pointer', + 'id' => 'mouse', + 'parameters' => ['pointerType' => 'mouse'], + 'actions' => [$this->createMoveAction($where, $x_offset, $y_offset)], + ], + ], + ]); + + return $this; + } + $params = []; if ($where !== null) { $params['element'] = $where->getAuxiliary(); @@ -114,6 +217,7 @@ public function mouseMove( if ($y_offset !== null) { $params['yoffset'] = $y_offset; } + $this->executor->execute(DriverCommand::MOVE_TO, $params); return $this; @@ -126,6 +230,29 @@ public function mouseMove( */ public function mouseUp(WebDriverCoordinates $where = null) { + if ($this->w3cCompliant) { + $moveAction = $where ? [$this->createMoveAction($where)] : []; + + $this->executor->execute(DriverCommand::ACTIONS, [ + 'actions' => [ + [ + 'type' => 'pointer', + 'id' => 'mouse', + 'parameters' => ['pointerType' => 'mouse'], + 'actions' => array_merge($moveAction, [ + [ + 'type' => 'pointerDown', + 'duration' => 0, + 'button' => 0, + ], + ]), + ], + ], + ]); + + return $this; + } + $this->moveIfNeeded($where); $this->executor->execute(DriverCommand::MOUSE_UP); @@ -141,4 +268,49 @@ protected function moveIfNeeded(WebDriverCoordinates $where = null) $this->mouseMove($where); } } + + /** + * @param WebDriverCoordinates $where + * @param int|null $x_offset + * @param int|null $y_offset + * + * @return array + */ + private function createMoveAction( + WebDriverCoordinates $where = null, + $x_offset = null, + $y_offset = null + ) { + $move_action = [ + 'type' => 'pointerMove', + 'duration' => 0, + 'x' => $x_offset === null ? 0 : $x_offset, + 'y' => $y_offset === null ? 0 : $y_offset, + ]; + + if ($where !== null) { + $move_action['origin'] = [JsonWireCompat::WEB_DRIVER_ELEMENT_IDENTIFIER => $where->getAuxiliary()]; + } + + return $move_action; + } + + /** + * @return array + */ + private function createClickActions() + { + return [ + [ + 'type' => 'pointerDown', + 'duration' => 0, + 'button' => 0, + ], + [ + 'type' => 'pointerUp', + 'duration' => 0, + 'button' => 0, + ], + ]; + } } diff --git a/lib/Remote/RemoteTargetLocator.php b/lib/Remote/RemoteTargetLocator.php index 5c218075d..55a879c0d 100644 --- a/lib/Remote/RemoteTargetLocator.php +++ b/lib/Remote/RemoteTargetLocator.php @@ -112,6 +112,8 @@ public function activeElement() $response = $this->driver->execute(DriverCommand::GET_ACTIVE_ELEMENT, []); $method = new RemoteExecuteMethod($this->driver); - return new RemoteWebElement($method, $response['ELEMENT']); + $w3cCompliant = $this->driver instanceof RemoteWebDriver ? $this->driver->isW3cCompliant() : false; + + return new RemoteWebElement($method, JsonWireCompat::getElement($response), $w3cCompliant); } } diff --git a/lib/Remote/RemoteWebDriver.php b/lib/Remote/RemoteWebDriver.php index 71f45704e..6d3a7d833 100644 --- a/lib/Remote/RemoteWebDriver.php +++ b/lib/Remote/RemoteWebDriver.php @@ -59,19 +59,26 @@ class RemoteWebDriver implements WebDriver, JavaScriptExecutor, WebDriverHasInpu * @var RemoteExecuteMethod */ protected $executeMethod; + /** + * @var bool + */ + protected $w3cCompliant; /** * @param HttpCommandExecutor $commandExecutor * @param string $sessionId * @param WebDriverCapabilities|null $capabilities + * @param bool $w3cCompliant false to use the legacy JsonWire protocol, true for the W3C WebDriver spec */ protected function __construct( HttpCommandExecutor $commandExecutor, $sessionId, - WebDriverCapabilities $capabilities = null + WebDriverCapabilities $capabilities = null, + $w3cCompliant = false ) { $this->executor = $commandExecutor; $this->sessionID = $sessionId; + $this->w3cCompliant = $w3cCompliant; if ($capabilities !== null) { $this->capabilities = $capabilities; @@ -88,6 +95,7 @@ protected function __construct( * @param string|null $http_proxy The proxy to tunnel requests to the remote Selenium WebDriver through * @param int|null $http_proxy_port The proxy port to tunnel requests to the remote Selenium WebDriver through * @param DesiredCapabilities $required_capabilities The required capabilities + * * @return static */ public static function create( @@ -99,6 +107,7 @@ public static function create( $http_proxy_port = null, DesiredCapabilities $required_capabilities = null ) { + // BC layer to not break the method signature $selenium_server_url = preg_replace('#/+$#', '', $selenium_server_url); $desired_capabilities = self::castToDesiredCapabilitiesObject($desired_capabilities); @@ -128,23 +137,43 @@ public static function create( $executor->setRequestTimeout($request_timeout_in_ms); } + // W3C + $parameters = [ + 'capabilities' => [ + 'firstMatch' => [$desired_capabilities->toArray()], + ], + ]; + + // Legacy protocol + if (null !== $required_capabilities && $required_capabilities_array = $required_capabilities->toArray()) { + $parameters['capabilities']['alwaysMatch'] = $required_capabilities_array; + } + if ($required_capabilities !== null) { // TODO: Selenium (as of v3.0.1) does accept requiredCapabilities only as a property of desiredCapabilities. - // This will probably change in future with the W3C WebDriver spec, but is the only way how to pass these - // values now. + // This has changed with the W3C WebDriver spec, but is the only way how to pass these + // values with the legacy protocol. $desired_capabilities->setCapability('requiredCapabilities', $required_capabilities->toArray()); } + $parameters['desiredCapabilities'] = $desired_capabilities->toArray(); + $command = new WebDriverCommand( null, DriverCommand::NEW_SESSION, - ['desiredCapabilities' => $desired_capabilities->toArray()] + $parameters ); $response = $executor->execute($command); - $returnedCapabilities = new DesiredCapabilities($response->getValue()); + $value = $response->getValue(); + + if (!$w3c_compliant = isset($value['capabilities'])) { + $executor->disableW3CCompliance(); + } - $driver = new static($executor, $response->getSessionID(), $returnedCapabilities); + $returnedCapabilities = new DesiredCapabilities($w3c_compliant ? $value['capabilities'] : $value); + + $driver = new static($executor, $response->getSessionID(), $returnedCapabilities, $w3c_compliant); return $driver; } @@ -167,7 +196,9 @@ public static function createBySessionID( $connection_timeout_in_ms = null, $request_timeout_in_ms = null ) { - $executor = new HttpCommandExecutor($selenium_server_url); + // BC layer to not break the method signature + $w3c_compliant = func_num_args() > 3 ? func_get_arg(3) : false; + $executor = new HttpCommandExecutor($selenium_server_url, null, null); if ($connection_timeout_in_ms !== null) { $executor->setConnectionTimeout($connection_timeout_in_ms); } @@ -175,7 +206,7 @@ public static function createBySessionID( $executor->setRequestTimeout($request_timeout_in_ms); } - return new static($executor, $session_id); + return new static($executor, $session_id, null, $w3c_compliant); } /** @@ -199,13 +230,12 @@ public function close() */ public function findElement(WebDriverBy $by) { - $params = ['using' => $by->getMechanism(), 'value' => $by->getValue()]; $raw_element = $this->execute( DriverCommand::FIND_ELEMENT, - $params + JsonWireCompat::getUsing($by, $this->w3cCompliant) ); - return $this->newElement($raw_element['ELEMENT']); + return $this->newElement(JsonWireCompat::getElement($raw_element)); } /** @@ -217,15 +247,14 @@ public function findElement(WebDriverBy $by) */ public function findElements(WebDriverBy $by) { - $params = ['using' => $by->getMechanism(), 'value' => $by->getValue()]; $raw_elements = $this->execute( DriverCommand::FIND_ELEMENTS, - $params + JsonWireCompat::getUsing($by, $this->w3cCompliant) ); $elements = []; foreach ($raw_elements as $raw_element) { - $elements[] = $this->newElement($raw_element['ELEMENT']); + $elements[] = $this->newElement(JsonWireCompat::getElement($raw_element)); } return $elements; @@ -399,7 +428,7 @@ public function wait($timeout_in_second = 30, $interval_in_millisecond = 250) */ public function manage() { - return new WebDriverOptions($this->getExecuteMethod()); + return new WebDriverOptions($this->getExecuteMethod(), $this->w3cCompliant); } /** @@ -430,7 +459,7 @@ public function switchTo() public function getMouse() { if (!$this->mouse) { - $this->mouse = new RemoteMouse($this->getExecuteMethod()); + $this->mouse = new RemoteMouse($this->getExecuteMethod(), $this->w3cCompliant); } return $this->mouse; @@ -541,7 +570,7 @@ public function getCapabilities() */ public static function getAllSessions($selenium_server_url = 'http://localhost:4444/wd/hub', $timeout_in_ms = 30000) { - $executor = new HttpCommandExecutor($selenium_server_url); + $executor = new HttpCommandExecutor($selenium_server_url, null, null); $executor->setConnectionTimeout($timeout_in_ms); $command = new WebDriverCommand( @@ -570,6 +599,15 @@ public function execute($command_name, $params = []) return null; } + /** + * @internal + * @return bool + */ + public function isW3cCompliant() + { + return $this->w3cCompliant; + } + /** * Prepare arguments for JavaScript injection * @@ -581,9 +619,11 @@ protected function prepareScriptArguments(array $arguments) $args = []; foreach ($arguments as $key => $value) { if ($value instanceof WebDriverElement) { - $args[$key] = ['ELEMENT' => $value->getID()]; + $args[$key] = [ + $this->w3cCompliant ? JsonWireCompat::WEB_DRIVER_ELEMENT_IDENTIFIER : 'ELEMENT' => $value->getID(), + ]; } else { - if (is_array($value)) { + if (\is_array($value)) { $value = $this->prepareScriptArguments($value); } $args[$key] = $value; @@ -613,7 +653,7 @@ protected function getExecuteMethod() */ protected function newElement($id) { - return new RemoteWebElement($this->getExecuteMethod(), $id); + return new RemoteWebElement($this->getExecuteMethod(), $id, $this->w3cCompliant); } /** @@ -629,7 +669,7 @@ protected static function castToDesiredCapabilitiesObject($desired_capabilities return new DesiredCapabilities(); } - if (is_array($desired_capabilities)) { + if (\is_array($desired_capabilities)) { return new DesiredCapabilities($desired_capabilities); } diff --git a/lib/Remote/RemoteWebElement.php b/lib/Remote/RemoteWebElement.php index 26936d8b9..b07b18227 100644 --- a/lib/Remote/RemoteWebElement.php +++ b/lib/Remote/RemoteWebElement.php @@ -15,6 +15,7 @@ namespace Facebook\WebDriver\Remote; +use Facebook\WebDriver\Exception\UnsupportedOperationException; use Facebook\WebDriver\Exception\WebDriverException; use Facebook\WebDriver\Interactions\Internal\WebDriverCoordinates; use Facebook\WebDriver\Internal\WebDriverLocatable; @@ -42,16 +43,22 @@ class RemoteWebElement implements WebDriverElement, WebDriverLocatable * @var UselessFileDetector */ protected $fileDetector; + /** + * @var bool + */ + protected $w3cCompliant; /** * @param RemoteExecuteMethod $executor * @param string $id + * @param bool $w3cCompliant */ - public function __construct(RemoteExecuteMethod $executor, $id) + public function __construct(RemoteExecuteMethod $executor, $id, $w3cCompliant = false) { $this->executor = $executor; $this->id = $id; $this->fileDetector = new UselessFileDetector(); + $this->w3cCompliant = $w3cCompliant; } /** @@ -66,6 +73,23 @@ public function clear() [':id' => $this->id] ); + if ($this->w3cCompliant) { + $this->executor->execute(DriverCommand::ACTIONS, [ + 'actions' => [[ + 'type' => 'key', + 'id' => 'keyboard', + 'actions' => [ + ['type' => 'keyDown' , 'value' => WebDriverKeys::CONTROL], + ['type' => 'keyDown' , 'value' => 'a'], + ['type' => 'keyUp' , 'value' => WebDriverKeys::CONTROL], + ['type' => 'keyUp' , 'value' => 'a'], + ['type' => 'keyDown' , 'value' => WebDriverKeys::BACKSPACE], + ['type' => 'keyUp' , 'value' => WebDriverKeys::BACKSPACE], + ], + ]], + ]); + } + return $this; } @@ -94,17 +118,15 @@ public function click() */ public function findElement(WebDriverBy $by) { - $params = [ - 'using' => $by->getMechanism(), - 'value' => $by->getValue(), - ':id' => $this->id, - ]; + $params = JsonWireCompat::getUsing($by, $this->w3cCompliant); + $params[':id'] = $this->id; + $raw_element = $this->executor->execute( DriverCommand::FIND_CHILD_ELEMENT, $params ); - return $this->newElement($raw_element['ELEMENT']); + return $this->newElement(JsonWireCompat::getElement($raw_element)); } /** @@ -117,11 +139,8 @@ public function findElement(WebDriverBy $by) */ public function findElements(WebDriverBy $by) { - $params = [ - 'using' => $by->getMechanism(), - 'value' => $by->getValue(), - ':id' => $this->id, - ]; + $params = JsonWireCompat::getUsing($by, $this->w3cCompliant); + $params[':id'] = $this->id; $raw_elements = $this->executor->execute( DriverCommand::FIND_CHILD_ELEMENTS, $params @@ -129,7 +148,7 @@ public function findElements(WebDriverBy $by) $elements = []; foreach ($raw_elements as $raw_element) { - $elements[] = $this->newElement($raw_element['ELEMENT']); + $elements[] = $this->newElement(JsonWireCompat::getElement($raw_element)); } return $elements; @@ -148,10 +167,23 @@ public function getAttribute($attribute_name) ':id' => $this->id, ]; - return $this->executor->execute( - DriverCommand::GET_ELEMENT_ATTRIBUTE, - $params - ); + if ($this->w3cCompliant && ($attribute_name === 'value' || $attribute_name === 'index')) { + $value = $this->executor->execute(DriverCommand::GET_ELEMENT_PROPERTY, $params); + + if ($value === true) { + return 'true'; + } + + if ($value === false) { + return 'false'; + } + + if ($value !== null) { + return (string) $value; + } + } + + return $this->executor->execute(DriverCommand::GET_ELEMENT_ATTRIBUTE, $params); } /** @@ -325,20 +357,37 @@ public function sendKeys($value) { $local_file = $this->fileDetector->getLocalFile($value); if ($local_file === null) { + if ($this->w3cCompliant) { + $params = [ + 'text' => (string) $value, + ':id' => $this->id, + ]; + } else { + $params = [ + 'value' => WebDriverKeys::encode($value), + ':id' => $this->id, + ]; + } + + $this->executor->execute(DriverCommand::SEND_KEYS_TO_ELEMENT, $params); + + return $this; + } + + if ($this->w3cCompliant) { $params = [ - 'value' => WebDriverKeys::encode($value), + 'text' => $local_file, ':id' => $this->id, ]; - $this->executor->execute(DriverCommand::SEND_KEYS_TO_ELEMENT, $params); } else { - $remote_path = $this->upload($local_file); $params = [ - 'value' => WebDriverKeys::encode($remote_path), + 'value' => WebDriverKeys::encode($this->upload($local_file)), ':id' => $this->id, ]; - $this->executor->execute(DriverCommand::SEND_KEYS_TO_ELEMENT, $params); } + $this->executor->execute(DriverCommand::SEND_KEYS_TO_ELEMENT, $params); + return $this; } @@ -371,6 +420,19 @@ public function setFileDetector(FileDetector $detector) */ public function submit() { + if ($this->w3cCompliant) { + $this->executor->execute(DriverCommand::EXECUTE_SCRIPT, [ + // cannot call the submit method directly in case an input of this form is named "submit" + 'script' => sprintf( + 'return Object.getPrototypeOf(%1$s).submit.call(%1$s);', + 'form' === $this->getTagName() ? 'arguments[0]' : 'arguments[0].form' + ), + 'args' => [[JsonWireCompat::WEB_DRIVER_ELEMENT_IDENTIFIER => $this->id]], + ]); + + return $this; + } + $this->executor->execute( DriverCommand::SUBMIT_ELEMENT, [':id' => $this->id] @@ -397,6 +459,10 @@ public function getID() */ public function equals(WebDriverElement $other) { + if ($this->w3cCompliant) { + throw new UnsupportedOperationException('"elementEquals" is not supported by the W3C specification'); + } + return $this->executor->execute(DriverCommand::ELEMENT_EQUALS, [ ':id' => $this->id, ':other' => $other->getID(), @@ -412,7 +478,7 @@ public function equals(WebDriverElement $other) */ protected function newElement($id) { - return new static($this->executor, $id); + return new static($this->executor, $id, $this->w3cCompliant); } /** diff --git a/lib/WebDriverDimension.php b/lib/WebDriverDimension.php index 308c1990f..81a223989 100644 --- a/lib/WebDriverDimension.php +++ b/lib/WebDriverDimension.php @@ -21,17 +21,17 @@ class WebDriverDimension { /** - * @var int + * @var int|float */ private $height; /** - * @var int + * @var int|float */ private $width; /** - * @param int $width - * @param int $height + * @param int|float $width + * @param int|float $height */ public function __construct($width, $height) { @@ -46,7 +46,7 @@ public function __construct($width, $height) */ public function getHeight() { - return $this->height; + return (int) $this->height; } /** @@ -56,7 +56,7 @@ public function getHeight() */ public function getWidth() { - return $this->width; + return (int) $this->width; } /** diff --git a/lib/WebDriverOptions.php b/lib/WebDriverOptions.php index bec7fd18b..4c584bd07 100644 --- a/lib/WebDriverOptions.php +++ b/lib/WebDriverOptions.php @@ -28,10 +28,15 @@ class WebDriverOptions * @var ExecuteMethod */ protected $executor; + /** + * @var bool + */ + protected $w3cCompliant; - public function __construct(ExecuteMethod $executor) + public function __construct(ExecuteMethod $executor, $w3cCompliant = false) { $this->executor = $executor; + $this->w3cCompliant = $w3cCompliant; } /** @@ -128,7 +133,7 @@ public function getCookies() */ public function timeouts() { - return new WebDriverTimeouts($this->executor); + return new WebDriverTimeouts($this->executor, $this->w3cCompliant); } /** diff --git a/lib/WebDriverPoint.php b/lib/WebDriverPoint.php index 4e2dbd211..9600af9d8 100644 --- a/lib/WebDriverPoint.php +++ b/lib/WebDriverPoint.php @@ -36,7 +36,7 @@ public function __construct($x, $y) */ public function getX() { - return $this->x; + return (int) $this->x; } /** @@ -46,7 +46,7 @@ public function getX() */ public function getY() { - return $this->y; + return (int) $this->y; } /** diff --git a/lib/WebDriverTimeouts.php b/lib/WebDriverTimeouts.php index 6903f7080..eeba9a10e 100644 --- a/lib/WebDriverTimeouts.php +++ b/lib/WebDriverTimeouts.php @@ -27,10 +27,15 @@ class WebDriverTimeouts * @var ExecuteMethod */ protected $executor; + /** + * @var bool + */ + protected $w3cCompliant; - public function __construct(ExecuteMethod $executor) + public function __construct(ExecuteMethod $executor, $w3cCompliant = false) { $this->executor = $executor; + $this->w3cCompliant = $w3cCompliant; } /** @@ -41,6 +46,15 @@ public function __construct(ExecuteMethod $executor) */ public function implicitlyWait($seconds) { + if ($this->w3cCompliant) { + $this->executor->execute( + DriverCommand::IMPLICITLY_WAIT, + ['implicit' => $seconds * 1000] + ); + + return $this; + } + $this->executor->execute( DriverCommand::IMPLICITLY_WAIT, ['ms' => $seconds * 1000] @@ -57,6 +71,15 @@ public function implicitlyWait($seconds) */ public function setScriptTimeout($seconds) { + if ($this->w3cCompliant) { + $this->executor->execute( + DriverCommand::SET_SCRIPT_TIMEOUT, + ['script' => $seconds * 1000] + ); + + return $this; + } + $this->executor->execute( DriverCommand::SET_SCRIPT_TIMEOUT, ['ms' => $seconds * 1000] @@ -73,6 +96,15 @@ public function setScriptTimeout($seconds) */ public function pageLoadTimeout($seconds) { + if ($this->w3cCompliant) { + $this->executor->execute( + DriverCommand::SET_SCRIPT_TIMEOUT, + ['pageLoad' => $seconds * 1000] + ); + + return $this; + } + $this->executor->execute(DriverCommand::SET_TIMEOUT, [ 'type' => 'page load', 'ms' => $seconds * 1000, diff --git a/tests/functional/RemoteWebDriverCreateTest.php b/tests/functional/RemoteWebDriverCreateTest.php index 90c072922..a2f6e9d0f 100644 --- a/tests/functional/RemoteWebDriverCreateTest.php +++ b/tests/functional/RemoteWebDriverCreateTest.php @@ -33,7 +33,10 @@ public function testShouldStartBrowserAndCreateInstanceOfRemoteWebDriver() $this->serverUrl, $this->desiredCapabilities, $this->connectionTimeout, - $this->requestTimeout + $this->requestTimeout, + null, + null, + null ); $this->assertInstanceOf(RemoteWebDriver::class, $this->driver); diff --git a/tests/functional/RemoteWebDriverFindElementTest.php b/tests/functional/RemoteWebDriverFindElementTest.php index eea14b6e5..066179f1f 100644 --- a/tests/functional/RemoteWebDriverFindElementTest.php +++ b/tests/functional/RemoteWebDriverFindElementTest.php @@ -61,4 +61,24 @@ public function testShouldFindMultipleElements() $this->assertCount(5, $elements); $this->assertContainsOnlyInstancesOf(RemoteWebElement::class, $elements); } + + public function testEscapeCssSelector() + { + if (getenv('GECKODRIVER') !== '1') { + $this->markTestSkipped( + 'CSS selectors containing special characters are not supported by the legacy protocol' + ); + } + + $this->driver->get($this->getTestPageUrl('escape_css.html')); + + $element = $this->driver->findElement(WebDriverBy::id('.fo\'oo')); + $this->assertSame('Foo', $element->getText()); + + $element = $this->driver->findElement(WebDriverBy::className('#ba\'r')); + $this->assertSame('Bar', $element->getText()); + + $element = $this->driver->findElement(WebDriverBy::name('.#ba\'z')); + $this->assertSame('Baz', $element->getText()); + } } diff --git a/tests/functional/RemoteWebDriverTest.php b/tests/functional/RemoteWebDriverTest.php index d996d304a..9105aa93f 100644 --- a/tests/functional/RemoteWebDriverTest.php +++ b/tests/functional/RemoteWebDriverTest.php @@ -77,7 +77,11 @@ public function testShouldGetSessionId() */ public function testShouldGetAllSessions() { - $sessions = RemoteWebDriver::getAllSessions($this->serverUrl); + if (getenv('GECKODRIVER') === '1') { + $this->markTestSkipped('"getAllSessions" is not supported by the W3C specification'); + } + + $sessions = RemoteWebDriver::getAllSessions($this->serverUrl, 30000); $this->assertInternalType('array', $sessions); $this->assertCount(1, $sessions); @@ -94,12 +98,22 @@ public function testShouldGetAllSessions() */ public function testShouldQuitAndUnsetExecutor() { - $this->assertCount(1, RemoteWebDriver::getAllSessions($this->serverUrl)); + if (getenv('GECKODRIVER') === '1') { + $this->markTestSkipped('"getAllSessions" is not supported by the W3C specification'); + } + + $this->assertCount( + 1, + RemoteWebDriver::getAllSessions($this->serverUrl, 30000) + ); $this->assertInstanceOf(HttpCommandExecutor::class, $this->driver->getCommandExecutor()); $this->driver->quit(); - $this->assertCount(0, RemoteWebDriver::getAllSessions($this->serverUrl)); + $this->assertCount( + 0, + RemoteWebDriver::getAllSessions($this->serverUrl, 30000) + ); $this->assertNull($this->driver->getCommandExecutor()); } @@ -136,6 +150,9 @@ public function testShouldCloseWindow() $this->driver->get($this->getTestPageUrl('open_new_window.html')); $this->driver->findElement(WebDriverBy::cssSelector('a'))->click(); + // Mandatory for Geckodriver + $this->driver->wait()->until(WebDriverExpectedCondition::numberOfWindowsToBe(2)); + $this->assertCount(2, $this->driver->getWindowHandles()); $this->driver->close(); diff --git a/tests/functional/RemoteWebElementTest.php b/tests/functional/RemoteWebElementTest.php index d4614a431..a4684ace3 100644 --- a/tests/functional/RemoteWebElementTest.php +++ b/tests/functional/RemoteWebElementTest.php @@ -272,6 +272,10 @@ public function testShouldSubmitFormByClickOnSubmitInput() */ public function testShouldCompareEqualsElement() { + if (getenv('GECKODRIVER') === '1') { + $this->markTestSkipped('"equals" is not supported by the W3C specification'); + } + $this->driver->get($this->getTestPageUrl('index.html')); $firstElement = $this->driver->findElement(WebDriverBy::cssSelector('ul.list')); diff --git a/tests/functional/WebDriverActionsTest.php b/tests/functional/WebDriverActionsTest.php index 3df984810..b2bd51835 100644 --- a/tests/functional/WebDriverActionsTest.php +++ b/tests/functional/WebDriverActionsTest.php @@ -46,10 +46,16 @@ public function testShouldClickOnElement() ->click($element) ->perform(); - $this->assertSame( - ['mouseover item-1', 'mousedown item-1', 'mouseup item-1', 'click item-1'], - $this->retrieveLoggedEvents() - ); + $logs = ['mouseover item-1', 'mousedown item-1', 'mouseup item-1', 'click item-1']; + $loggedEvents = $this->retrieveLoggedEvents(); + + if ('1' === getenv('GECKODRIVER')) { + $loggedEvents = array_slice($loggedEvents, 0, count($logs)); + // Firefox sometimes triggers some extra events + // it's not related to Geckodriver, it's Firefox's own behavior + } + + $this->assertSame($logs, $loggedEvents); } /** @@ -71,10 +77,12 @@ public function testShouldClickAndHoldOnElementAndRelease() ->release() ->perform(); - $this->assertSame( - ['mouseover item-1', 'mousedown item-1', 'mouseup item-1', 'click item-1'], - $this->retrieveLoggedEvents() - ); + if ('1' === getenv('GECKODRIVER')) { + $logs = ['mouseover item-1', 'mousedown item-1', 'dragstart item-1']; + } else { + $logs = ['mouseover item-1', 'mousedown item-1', 'mouseup item-1', 'click item-1']; + } + $this->assertSame($logs, $this->retrieveLoggedEvents()); } /** diff --git a/tests/functional/WebDriverTestCase.php b/tests/functional/WebDriverTestCase.php index 5fe6a7e8a..7a0f8af23 100644 --- a/tests/functional/WebDriverTestCase.php +++ b/tests/functional/WebDriverTestCase.php @@ -58,6 +58,8 @@ protected function setUp() // --no-sandbox is a workaround for Chrome crashing: https://github.com/SeleniumHQ/selenium/issues/4961 $chromeOptions->addArguments(['--headless', 'window-size=1024,768', '--no-sandbox']); $this->desiredCapabilities->setCapability(ChromeOptions::CAPABILITY, $chromeOptions); + } elseif (getenv('GECKODRIVER') === '1') { + $this->serverUrl = 'http://localhost:4444/'; } $this->desiredCapabilities->setBrowserName($browserName); @@ -68,7 +70,10 @@ protected function setUp() $this->serverUrl, $this->desiredCapabilities, $this->connectionTimeout, - $this->requestTimeout + $this->requestTimeout, + null, + null, + null ); } } diff --git a/tests/functional/web/escape_css.html b/tests/functional/web/escape_css.html new file mode 100644 index 000000000..48213084a --- /dev/null +++ b/tests/functional/web/escape_css.html @@ -0,0 +1,14 @@ + + + + + Test CSS selector escaping + + + +
Foo
+
Bar
+
Baz
+ + + diff --git a/tests/functional/web/upload.html b/tests/functional/web/upload.html index 65a622c50..6762873c3 100644 --- a/tests/functional/web/upload.html +++ b/tests/functional/web/upload.html @@ -8,7 +8,7 @@

- +

From 4a65bed00dd2bdbbfa81206590df29d2060221c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Tue, 20 Aug 2019 16:18:50 +0200 Subject: [PATCH 004/350] Add back Geckodriver to the Travis matrix --- .travis.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 47ddb461e..906951171 100644 --- a/.travis.yml +++ b/.travis.yml @@ -108,9 +108,14 @@ install: before_script: - if [ "$BROWSER_NAME" = "chrome" ]; then mkdir chromedriver; CHROMEDRIVER_VERSION=$(wget -qO- "https://chromedriver.storage.googleapis.com/LATEST_RELEASE"); wget -q -t 3 https://chromedriver.storage.googleapis.com/$CHROMEDRIVER_VERSION/chromedriver_linux64.zip; unzip chromedriver_linux64 -d chromedriver; fi - if [ "$BROWSER_NAME" = "chrome" ]; then export CHROMEDRIVER_PATH=$PWD/chromedriver/chromedriver; fi + - if [ "$GECKODRIVER" = "1" ]; then mkdir geckodriver; wget -q -t 3 https://github.com/mozilla/geckodriver/releases/download/v0.24.0/geckodriver-v0.24.0-linux64.tar.gz; tar xzf geckodriver-v0.24.0-linux64.tar.gz -C geckodriver; fi - sh -e /etc/init.d/xvfb start - if [ ! -f jar/selenium-server-standalone-3.8.1.jar ]; then wget -q -t 3 -P jar https://selenium-release.storage.googleapis.com/3.8/selenium-server-standalone-3.8.1.jar; fi - - java -Dwebdriver.firefox.marionette=false -Dwebdriver.chrome.driver="$CHROMEDRIVER_PATH" -jar jar/selenium-server-standalone-3.8.1.jar -enablePassThrough false -log ./logs/selenium.log & + - if [ "$GECKODRIVER" = "1" ]; then + geckodriver/geckodriver &> ./logs/geckodriver.log & + else + java -Dwebdriver.firefox.marionette=false -Dwebdriver.chrome.driver="$CHROMEDRIVER_PATH" -jar jar/selenium-server-standalone-3.8.1.jar -enablePassThrough false -log ./logs/selenium.log & + fi - until $(echo | nc localhost 4444); do sleep 1; echo Waiting for Selenium server on port 4444...; done; echo "Selenium server started" - php -S 127.0.0.1:8000 -t tests/functional/web/ &>>./logs/php-server.log & - until $(echo | nc localhost 8000); do sleep 1; echo waiting for PHP server on port 8000...; done; echo "PHP server started" @@ -124,6 +129,7 @@ script: after_script: - if [ -f ./logs/selenium.log ]; then cat ./logs/selenium.log; fi - if [ -f ./logs/php-server.log ]; then cat ./logs/php-server.log; fi + - if [ -f ./logs/geckodriver.log ]; then cat ./logs/geckodriver.log; fi after_success: - travis_retry php vendor/bin/php-coveralls -v From 5729355c5d37953d6a1a5a2f55afc7ba1c23bf57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Tue, 20 Aug 2019 17:06:26 +0200 Subject: [PATCH 005/350] Improve tests stability --- tests/functional/WebDriverActionsTest.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/functional/WebDriverActionsTest.php b/tests/functional/WebDriverActionsTest.php index b2bd51835..1f03c8899 100644 --- a/tests/functional/WebDriverActionsTest.php +++ b/tests/functional/WebDriverActionsTest.php @@ -78,11 +78,13 @@ public function testShouldClickAndHoldOnElementAndRelease() ->perform(); if ('1' === getenv('GECKODRIVER')) { - $logs = ['mouseover item-1', 'mousedown item-1', 'dragstart item-1']; + $this->assertArraySubset(['mouseover item-1', 'mousedown item-1'], $this->retrieveLoggedEvents()); } else { - $logs = ['mouseover item-1', 'mousedown item-1', 'mouseup item-1', 'click item-1']; + $this->assertSame( + ['mouseover item-1', 'mousedown item-1', 'mouseup item-1', 'click item-1'], + $this->retrieveLoggedEvents() + ); } - $this->assertSame($logs, $this->retrieveLoggedEvents()); } /** From 721a7c2737ea8353c01e76ec7752aa0751e667eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Machulda?= Date: Mon, 4 Nov 2019 19:36:50 +0000 Subject: [PATCH 006/350] Use latest geckodriver --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 906951171..950e8fe64 100644 --- a/.travis.yml +++ b/.travis.yml @@ -108,7 +108,7 @@ install: before_script: - if [ "$BROWSER_NAME" = "chrome" ]; then mkdir chromedriver; CHROMEDRIVER_VERSION=$(wget -qO- "https://chromedriver.storage.googleapis.com/LATEST_RELEASE"); wget -q -t 3 https://chromedriver.storage.googleapis.com/$CHROMEDRIVER_VERSION/chromedriver_linux64.zip; unzip chromedriver_linux64 -d chromedriver; fi - if [ "$BROWSER_NAME" = "chrome" ]; then export CHROMEDRIVER_PATH=$PWD/chromedriver/chromedriver; fi - - if [ "$GECKODRIVER" = "1" ]; then mkdir geckodriver; wget -q -t 3 https://github.com/mozilla/geckodriver/releases/download/v0.24.0/geckodriver-v0.24.0-linux64.tar.gz; tar xzf geckodriver-v0.24.0-linux64.tar.gz -C geckodriver; fi + - if [ "$GECKODRIVER" = "1" ]; then mkdir geckodriver; wget -q -t 3 https://github.com/mozilla/geckodriver/releases/download/v0.26.0/geckodriver-v0.26.0-linux64.tar.gz; tar xzf geckodriver-v0.26.0-linux64.tar.gz -C geckodriver; fi - sh -e /etc/init.d/xvfb start - if [ ! -f jar/selenium-server-standalone-3.8.1.jar ]; then wget -q -t 3 -P jar https://selenium-release.storage.googleapis.com/3.8/selenium-server-standalone-3.8.1.jar; fi - if [ "$GECKODRIVER" = "1" ]; then From 71a6698227485b368d4212980ed9efcde15b292f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Machulda?= Date: Tue, 5 Nov 2019 15:35:40 +0000 Subject: [PATCH 007/350] Make POST body always valid JSON in W3C mode --- lib/Remote/HttpCommandExecutor.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/Remote/HttpCommandExecutor.php b/lib/Remote/HttpCommandExecutor.php index c783b2892..8114d7817 100644 --- a/lib/Remote/HttpCommandExecutor.php +++ b/lib/Remote/HttpCommandExecutor.php @@ -313,8 +313,13 @@ public function execute(WebDriverCommand $command) $encoded_params = null; - if ($http_method === 'POST' && $params && is_array($params)) { - $encoded_params = json_encode($params); + if ($http_method === 'POST') { + if ($params && is_array($params)) { + $encoded_params = json_encode($params); + } elseif ($this->w3cCompliant) { + // POST body must be valid JSON in W3C, even if empty: https://www.w3.org/TR/webdriver/#processing-model + $encoded_params = '{}'; + } } curl_setopt($this->curl, CURLOPT_POSTFIELDS, $encoded_params); From 7119d7fc97c5291768cd8632a15bfa7d75bff145 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Machulda?= Date: Tue, 5 Nov 2019 15:37:36 +0000 Subject: [PATCH 008/350] Codestyle: unify casing in w3c methods --- lib/Remote/HttpCommandExecutor.php | 12 +++---- lib/Remote/JsonWireCompat.php | 8 ++--- lib/Remote/RemoteMouse.php | 20 ++++++------ lib/Remote/RemoteTargetLocator.php | 4 +-- lib/Remote/RemoteWebDriver.php | 30 +++++++++--------- lib/Remote/RemoteWebElement.php | 50 ++++++++++++++++-------------- lib/WebDriverOptions.php | 8 ++--- lib/WebDriverTimeouts.php | 12 +++---- 8 files changed, 74 insertions(+), 70 deletions(-) diff --git a/lib/Remote/HttpCommandExecutor.php b/lib/Remote/HttpCommandExecutor.php index 8114d7817..b87c94069 100644 --- a/lib/Remote/HttpCommandExecutor.php +++ b/lib/Remote/HttpCommandExecutor.php @@ -171,7 +171,7 @@ class HttpCommandExecutor implements WebDriverCommandExecutor /** * @var bool */ - protected $w3cCompliant = true; + protected $isW3cCompliant = true; /** * @param string $url @@ -208,9 +208,9 @@ public function __construct($url, $http_proxy = null, $http_proxy_port = null) $this->setConnectionTimeout(30000); } - public function disableW3CCompliance() + public function disableW3cCompliance() { - $this->w3cCompliant = false; + $this->isW3cCompliant = false; } /** @@ -262,12 +262,12 @@ public function execute(WebDriverCommand $command) { $commandName = $command->getName(); if (!isset(self::$commands[$commandName])) { - if ($this->w3cCompliant && !isset(self::$w3cCompliantCommands[$commandName])) { + if ($this->isW3cCompliant && !isset(self::$w3cCompliantCommands[$commandName])) { throw new InvalidArgumentException($command->getName() . ' is not a valid command.'); } } - if ($this->w3cCompliant) { + if ($this->isW3cCompliant) { $raw = self::$w3cCompliantCommands[$command->getName()]; } else { $raw = self::$commands[$command->getName()]; @@ -316,7 +316,7 @@ public function execute(WebDriverCommand $command) if ($http_method === 'POST') { if ($params && is_array($params)) { $encoded_params = json_encode($params); - } elseif ($this->w3cCompliant) { + } elseif ($this->isW3cCompliant) { // POST body must be valid JSON in W3C, even if empty: https://www.w3.org/TR/webdriver/#processing-model $encoded_params = '{}'; } diff --git a/lib/Remote/JsonWireCompat.php b/lib/Remote/JsonWireCompat.php index 78b9cba0e..3488b8604 100644 --- a/lib/Remote/JsonWireCompat.php +++ b/lib/Remote/JsonWireCompat.php @@ -44,16 +44,16 @@ public static function getElement(array $rawElement) /** * @param WebDriverBy $by - * @param bool $w3cCompliant + * @param bool $isW3cCompliant * * @return array */ - public static function getUsing(WebDriverBy $by, $w3cCompliant) + public static function getUsing(WebDriverBy $by, $isW3cCompliant) { $mechanism = $by->getMechanism(); $value = $by->getValue(); - if ($w3cCompliant) { + if ($isW3cCompliant) { switch ($mechanism) { // Convert to CSS selectors case 'class name': @@ -89,7 +89,7 @@ private static function escapeSelector($selector) { return preg_replace_callback('/[^a-z0-9]/iSu', function ($matches) { $chr = $matches[0]; - if (mb_strlen($chr) == 1) { + if (mb_strlen($chr) === 1) { $ord = ord($chr); } else { $chr = mb_convert_encoding($chr, 'UTF-32BE', 'UTF-8'); diff --git a/lib/Remote/RemoteMouse.php b/lib/Remote/RemoteMouse.php index aa0d59829..4bd4a849b 100644 --- a/lib/Remote/RemoteMouse.php +++ b/lib/Remote/RemoteMouse.php @@ -30,16 +30,16 @@ class RemoteMouse implements WebDriverMouse /** * @var bool */ - private $w3cCompliant; + private $isW3cCompliant; /** * @param RemoteExecuteMethod $executor - * @param bool $w3cCompliant + * @param bool $isW3cCompliant */ - public function __construct(RemoteExecuteMethod $executor, $w3cCompliant = false) + public function __construct(RemoteExecuteMethod $executor, $isW3cCompliant = false) { $this->executor = $executor; - $this->w3cCompliant = $w3cCompliant; + $this->isW3cCompliant = $isW3cCompliant; } /** @@ -49,7 +49,7 @@ public function __construct(RemoteExecuteMethod $executor, $w3cCompliant = false */ public function click(WebDriverCoordinates $where = null) { - if ($this->w3cCompliant) { + if ($this->isW3cCompliant) { $moveAction = $where ? [$this->createMoveAction($where)] : []; $this->executor->execute(DriverCommand::ACTIONS, [ 'actions' => [ @@ -80,7 +80,7 @@ public function click(WebDriverCoordinates $where = null) */ public function contextClick(WebDriverCoordinates $where = null) { - if ($this->w3cCompliant) { + if ($this->isW3cCompliant) { $moveAction = $where ? [$this->createMoveAction($where)] : []; $this->executor->execute(DriverCommand::ACTIONS, [ 'actions' => [ @@ -122,7 +122,7 @@ public function contextClick(WebDriverCoordinates $where = null) */ public function doubleClick(WebDriverCoordinates $where = null) { - if ($this->w3cCompliant) { + if ($this->isW3cCompliant) { $clickActions = $this->createClickActions(); $moveAction = null === $where ? [] : [$this->createMoveAction($where)]; $this->executor->execute(DriverCommand::ACTIONS, [ @@ -152,7 +152,7 @@ public function doubleClick(WebDriverCoordinates $where = null) */ public function mouseDown(WebDriverCoordinates $where = null) { - if ($this->w3cCompliant) { + if ($this->isW3cCompliant) { $this->executor->execute(DriverCommand::ACTIONS, [ 'actions' => [ [ @@ -192,7 +192,7 @@ public function mouseMove( $x_offset = null, $y_offset = null ) { - if ($this->w3cCompliant) { + if ($this->isW3cCompliant) { $this->executor->execute(DriverCommand::ACTIONS, [ 'actions' => [ [ @@ -230,7 +230,7 @@ public function mouseMove( */ public function mouseUp(WebDriverCoordinates $where = null) { - if ($this->w3cCompliant) { + if ($this->isW3cCompliant) { $moveAction = $where ? [$this->createMoveAction($where)] : []; $this->executor->execute(DriverCommand::ACTIONS, [ diff --git a/lib/Remote/RemoteTargetLocator.php b/lib/Remote/RemoteTargetLocator.php index 55a879c0d..8bd89ef44 100644 --- a/lib/Remote/RemoteTargetLocator.php +++ b/lib/Remote/RemoteTargetLocator.php @@ -112,8 +112,8 @@ public function activeElement() $response = $this->driver->execute(DriverCommand::GET_ACTIVE_ELEMENT, []); $method = new RemoteExecuteMethod($this->driver); - $w3cCompliant = $this->driver instanceof RemoteWebDriver ? $this->driver->isW3cCompliant() : false; + $isW3cCompliant = ($this->driver instanceof RemoteWebDriver) ? $this->driver->isW3cCompliant() : false; - return new RemoteWebElement($method, JsonWireCompat::getElement($response), $w3cCompliant); + return new RemoteWebElement($method, JsonWireCompat::getElement($response), $isW3cCompliant); } } diff --git a/lib/Remote/RemoteWebDriver.php b/lib/Remote/RemoteWebDriver.php index 6d3a7d833..4d3573c0c 100644 --- a/lib/Remote/RemoteWebDriver.php +++ b/lib/Remote/RemoteWebDriver.php @@ -62,23 +62,23 @@ class RemoteWebDriver implements WebDriver, JavaScriptExecutor, WebDriverHasInpu /** * @var bool */ - protected $w3cCompliant; + protected $isW3cCompliant; /** * @param HttpCommandExecutor $commandExecutor * @param string $sessionId * @param WebDriverCapabilities|null $capabilities - * @param bool $w3cCompliant false to use the legacy JsonWire protocol, true for the W3C WebDriver spec + * @param bool $isW3cCompliant false to use the legacy JsonWire protocol, true for the W3C WebDriver spec */ protected function __construct( HttpCommandExecutor $commandExecutor, $sessionId, WebDriverCapabilities $capabilities = null, - $w3cCompliant = false + $isW3cCompliant = false ) { $this->executor = $commandExecutor; $this->sessionID = $sessionId; - $this->w3cCompliant = $w3cCompliant; + $this->isW3cCompliant = $isW3cCompliant; if ($capabilities !== null) { $this->capabilities = $capabilities; @@ -168,7 +168,7 @@ public static function create( $value = $response->getValue(); if (!$w3c_compliant = isset($value['capabilities'])) { - $executor->disableW3CCompliance(); + $executor->disableW3cCompliance(); } $returnedCapabilities = new DesiredCapabilities($w3c_compliant ? $value['capabilities'] : $value); @@ -232,7 +232,7 @@ public function findElement(WebDriverBy $by) { $raw_element = $this->execute( DriverCommand::FIND_ELEMENT, - JsonWireCompat::getUsing($by, $this->w3cCompliant) + JsonWireCompat::getUsing($by, $this->isW3cCompliant) ); return $this->newElement(JsonWireCompat::getElement($raw_element)); @@ -249,7 +249,7 @@ public function findElements(WebDriverBy $by) { $raw_elements = $this->execute( DriverCommand::FIND_ELEMENTS, - JsonWireCompat::getUsing($by, $this->w3cCompliant) + JsonWireCompat::getUsing($by, $this->isW3cCompliant) ); $elements = []; @@ -428,7 +428,7 @@ public function wait($timeout_in_second = 30, $interval_in_millisecond = 250) */ public function manage() { - return new WebDriverOptions($this->getExecuteMethod(), $this->w3cCompliant); + return new WebDriverOptions($this->getExecuteMethod(), $this->isW3cCompliant); } /** @@ -459,7 +459,7 @@ public function switchTo() public function getMouse() { if (!$this->mouse) { - $this->mouse = new RemoteMouse($this->getExecuteMethod(), $this->w3cCompliant); + $this->mouse = new RemoteMouse($this->getExecuteMethod(), $this->isW3cCompliant); } return $this->mouse; @@ -605,7 +605,7 @@ public function execute($command_name, $params = []) */ public function isW3cCompliant() { - return $this->w3cCompliant; + return $this->isW3cCompliant; } /** @@ -620,10 +620,12 @@ protected function prepareScriptArguments(array $arguments) foreach ($arguments as $key => $value) { if ($value instanceof WebDriverElement) { $args[$key] = [ - $this->w3cCompliant ? JsonWireCompat::WEB_DRIVER_ELEMENT_IDENTIFIER : 'ELEMENT' => $value->getID(), + $this->isW3cCompliant ? + JsonWireCompat::WEB_DRIVER_ELEMENT_IDENTIFIER + : 'ELEMENT' => $value->getID(), ]; } else { - if (\is_array($value)) { + if (is_array($value)) { $value = $this->prepareScriptArguments($value); } $args[$key] = $value; @@ -653,7 +655,7 @@ protected function getExecuteMethod() */ protected function newElement($id) { - return new RemoteWebElement($this->getExecuteMethod(), $id, $this->w3cCompliant); + return new RemoteWebElement($this->getExecuteMethod(), $id, $this->isW3cCompliant); } /** @@ -669,7 +671,7 @@ protected static function castToDesiredCapabilitiesObject($desired_capabilities return new DesiredCapabilities(); } - if (\is_array($desired_capabilities)) { + if (is_array($desired_capabilities)) { return new DesiredCapabilities($desired_capabilities); } diff --git a/lib/Remote/RemoteWebElement.php b/lib/Remote/RemoteWebElement.php index b07b18227..75dfa3ba2 100644 --- a/lib/Remote/RemoteWebElement.php +++ b/lib/Remote/RemoteWebElement.php @@ -46,19 +46,19 @@ class RemoteWebElement implements WebDriverElement, WebDriverLocatable /** * @var bool */ - protected $w3cCompliant; + protected $isW3cCompliant; /** * @param RemoteExecuteMethod $executor * @param string $id - * @param bool $w3cCompliant + * @param bool $isW3cCompliant */ - public function __construct(RemoteExecuteMethod $executor, $id, $w3cCompliant = false) + public function __construct(RemoteExecuteMethod $executor, $id, $isW3cCompliant = false) { $this->executor = $executor; $this->id = $id; $this->fileDetector = new UselessFileDetector(); - $this->w3cCompliant = $w3cCompliant; + $this->isW3cCompliant = $isW3cCompliant; } /** @@ -73,20 +73,22 @@ public function clear() [':id' => $this->id] ); - if ($this->w3cCompliant) { + if ($this->isW3cCompliant) { $this->executor->execute(DriverCommand::ACTIONS, [ - 'actions' => [[ - 'type' => 'key', - 'id' => 'keyboard', - 'actions' => [ - ['type' => 'keyDown' , 'value' => WebDriverKeys::CONTROL], - ['type' => 'keyDown' , 'value' => 'a'], - ['type' => 'keyUp' , 'value' => WebDriverKeys::CONTROL], - ['type' => 'keyUp' , 'value' => 'a'], - ['type' => 'keyDown' , 'value' => WebDriverKeys::BACKSPACE], - ['type' => 'keyUp' , 'value' => WebDriverKeys::BACKSPACE], + 'actions' => [ + [ + 'type' => 'key', + 'id' => 'keyboard', + 'actions' => [ + ['type' => 'keyDown', 'value' => WebDriverKeys::CONTROL], + ['type' => 'keyDown', 'value' => 'a'], + ['type' => 'keyUp', 'value' => WebDriverKeys::CONTROL], + ['type' => 'keyUp', 'value' => 'a'], + ['type' => 'keyDown', 'value' => WebDriverKeys::BACKSPACE], + ['type' => 'keyUp', 'value' => WebDriverKeys::BACKSPACE], + ], ], - ]], + ], ]); } @@ -118,7 +120,7 @@ public function click() */ public function findElement(WebDriverBy $by) { - $params = JsonWireCompat::getUsing($by, $this->w3cCompliant); + $params = JsonWireCompat::getUsing($by, $this->isW3cCompliant); $params[':id'] = $this->id; $raw_element = $this->executor->execute( @@ -139,7 +141,7 @@ public function findElement(WebDriverBy $by) */ public function findElements(WebDriverBy $by) { - $params = JsonWireCompat::getUsing($by, $this->w3cCompliant); + $params = JsonWireCompat::getUsing($by, $this->isW3cCompliant); $params[':id'] = $this->id; $raw_elements = $this->executor->execute( DriverCommand::FIND_CHILD_ELEMENTS, @@ -167,7 +169,7 @@ public function getAttribute($attribute_name) ':id' => $this->id, ]; - if ($this->w3cCompliant && ($attribute_name === 'value' || $attribute_name === 'index')) { + if ($this->isW3cCompliant && ($attribute_name === 'value' || $attribute_name === 'index')) { $value = $this->executor->execute(DriverCommand::GET_ELEMENT_PROPERTY, $params); if ($value === true) { @@ -357,7 +359,7 @@ public function sendKeys($value) { $local_file = $this->fileDetector->getLocalFile($value); if ($local_file === null) { - if ($this->w3cCompliant) { + if ($this->isW3cCompliant) { $params = [ 'text' => (string) $value, ':id' => $this->id, @@ -374,7 +376,7 @@ public function sendKeys($value) return $this; } - if ($this->w3cCompliant) { + if ($this->isW3cCompliant) { $params = [ 'text' => $local_file, ':id' => $this->id, @@ -420,7 +422,7 @@ public function setFileDetector(FileDetector $detector) */ public function submit() { - if ($this->w3cCompliant) { + if ($this->isW3cCompliant) { $this->executor->execute(DriverCommand::EXECUTE_SCRIPT, [ // cannot call the submit method directly in case an input of this form is named "submit" 'script' => sprintf( @@ -459,7 +461,7 @@ public function getID() */ public function equals(WebDriverElement $other) { - if ($this->w3cCompliant) { + if ($this->isW3cCompliant) { throw new UnsupportedOperationException('"elementEquals" is not supported by the W3C specification'); } @@ -478,7 +480,7 @@ public function equals(WebDriverElement $other) */ protected function newElement($id) { - return new static($this->executor, $id, $this->w3cCompliant); + return new static($this->executor, $id, $this->isW3cCompliant); } /** diff --git a/lib/WebDriverOptions.php b/lib/WebDriverOptions.php index 4c584bd07..682918a17 100644 --- a/lib/WebDriverOptions.php +++ b/lib/WebDriverOptions.php @@ -31,12 +31,12 @@ class WebDriverOptions /** * @var bool */ - protected $w3cCompliant; + protected $isW3cCompliant; - public function __construct(ExecuteMethod $executor, $w3cCompliant = false) + public function __construct(ExecuteMethod $executor, $isW3cCompliant = false) { $this->executor = $executor; - $this->w3cCompliant = $w3cCompliant; + $this->isW3cCompliant = $isW3cCompliant; } /** @@ -133,7 +133,7 @@ public function getCookies() */ public function timeouts() { - return new WebDriverTimeouts($this->executor, $this->w3cCompliant); + return new WebDriverTimeouts($this->executor, $this->isW3cCompliant); } /** diff --git a/lib/WebDriverTimeouts.php b/lib/WebDriverTimeouts.php index eeba9a10e..5735cb48d 100644 --- a/lib/WebDriverTimeouts.php +++ b/lib/WebDriverTimeouts.php @@ -30,12 +30,12 @@ class WebDriverTimeouts /** * @var bool */ - protected $w3cCompliant; + protected $isW3cCompliant; - public function __construct(ExecuteMethod $executor, $w3cCompliant = false) + public function __construct(ExecuteMethod $executor, $isW3cCompliant = false) { $this->executor = $executor; - $this->w3cCompliant = $w3cCompliant; + $this->isW3cCompliant = $isW3cCompliant; } /** @@ -46,7 +46,7 @@ public function __construct(ExecuteMethod $executor, $w3cCompliant = false) */ public function implicitlyWait($seconds) { - if ($this->w3cCompliant) { + if ($this->isW3cCompliant) { $this->executor->execute( DriverCommand::IMPLICITLY_WAIT, ['implicit' => $seconds * 1000] @@ -71,7 +71,7 @@ public function implicitlyWait($seconds) */ public function setScriptTimeout($seconds) { - if ($this->w3cCompliant) { + if ($this->isW3cCompliant) { $this->executor->execute( DriverCommand::SET_SCRIPT_TIMEOUT, ['script' => $seconds * 1000] @@ -96,7 +96,7 @@ public function setScriptTimeout($seconds) */ public function pageLoadTimeout($seconds) { - if ($this->w3cCompliant) { + if ($this->isW3cCompliant) { $this->executor->execute( DriverCommand::SET_SCRIPT_TIMEOUT, ['pageLoad' => $seconds * 1000] From bd34170cb3c17322dbc785fede30b6c88ffd651c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Mon, 28 Oct 2019 18:21:02 +0100 Subject: [PATCH 009/350] Compatibility with Symfony 5 --- CHANGELOG.md | 1 + composer.json | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f1e733516..e2662351d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ This project versioning adheres to [Semantic Versioning](http://semver.org/). ### Changed - Revert no longer needed workaround for Chromedriver bug [2943](https://bugs.chromium.org/p/chromedriver/issues/detail?id=2943). +- Allow installation of Symfony 5 components. ## 1.7.1 - 2019-06-13 ### Fixed diff --git a/composer.json b/composer.json index 2a9929725..3a841a504 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,7 @@ "minimum-stability": "beta", "require": { "php": "^5.6 || ~7.0", - "symfony/process": "^2.8 || ^3.1 || ^4.0", + "symfony/process": "^2.8 || ^3.1 || ^4.0 || ^5.0", "ext-curl": "*", "ext-zip": "*", "ext-mbstring": "*", @@ -26,7 +26,7 @@ "squizlabs/php_codesniffer": "^2.6", "php-mock/php-mock-phpunit": "^1.1", "php-coveralls/php-coveralls": "^2.0", - "symfony/var-dumper": "^3.3 || ^4.0", + "symfony/var-dumper": "^3.3 || ^4.0 || ^5.0", "jakub-onderka/php-parallel-lint": "^0.9.2" }, "suggest": { From e9277912d553971c2b73bf19df3fb44323f1f8b7 Mon Sep 17 00:00:00 2001 From: Mohammadreza Yektamaram Date: Tue, 1 Oct 2019 14:51:24 +0330 Subject: [PATCH 010/350] Minor grammar fixes --- README.md | 2 +- example.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b25bee453..0fa9db31c 100644 --- a/README.md +++ b/README.md @@ -115,7 +115,7 @@ We have a great community willing to help you! - **Via our Facebook Group** - If you have questions or are an active contributor consider joining our [facebook group](https://www.facebook.com/groups/phpwebdriver/) and contribute to communal discussion and support - **Via StackOverflow** - You can also [ask a question](https://stackoverflow.com/questions/ask?tags=php+selenium-webdriver) or find many already answered question on StackOverflow -- **Via GitHub** - Another option if you have a question (or bug report) is to [submit it here](https://github.com/facebook/php-webdriver/issues/new) as an new issue +- **Via GitHub** - Another option if you have a question (or bug report) is to [submit it here](https://github.com/facebook/php-webdriver/issues/new) as a new issue ## Contributing diff --git a/example.php b/example.php index f1109ed54..4dbb93900 100644 --- a/example.php +++ b/example.php @@ -23,7 +23,7 @@ require_once('vendor/autoload.php'); -// start Chrome with 5 second timeout +// start Chrome with 5 seconds timeout $host = 'http://localhost:4444/wd/hub'; // this is the default $capabilities = DesiredCapabilities::chrome(); $driver = RemoteWebDriver::create($host, $capabilities, 5000); From a21b3c82dabb8a777e81aed5a8239cba67efc115 Mon Sep 17 00:00:00 2001 From: "Benjamin R. White" Date: Tue, 27 Feb 2018 17:21:16 +0000 Subject: [PATCH 011/350] Allow combined not() and presenceOfElementLocated() functionality --- CHANGELOG.md | 3 +++ lib/WebDriverExpectedCondition.php | 9 ++++--- tests/unit/WebDriverExpectedConditionTest.php | 24 ++++++++++++++++++- 3 files changed, 32 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e2662351d..bbd2f4d52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ This project versioning adheres to [Semantic Versioning](http://semver.org/). - Revert no longer needed workaround for Chromedriver bug [2943](https://bugs.chromium.org/p/chromedriver/issues/detail?id=2943). - Allow installation of Symfony 5 components. +### Fixed +- `WebDriverExpectedCondition::presenceOfElementLocated()` works correctly when used within `WebDriverExpectedCondition::not()`. + ## 1.7.1 - 2019-06-13 ### Fixed - Error `Call to a member function toArray()` if capabilities were already converted to an array. diff --git a/lib/WebDriverExpectedCondition.php b/lib/WebDriverExpectedCondition.php index 15c7b3d2d..4bab4707b 100644 --- a/lib/WebDriverExpectedCondition.php +++ b/lib/WebDriverExpectedCondition.php @@ -148,7 +148,11 @@ public static function presenceOfElementLocated(WebDriverBy $by) { return new static( function (WebDriver $driver) use ($by) { - return $driver->findElement($by); + try { + return $driver->findElement($by); + } catch (NoSuchElementException $e) { + return false; + } } ); } @@ -423,8 +427,7 @@ function (WebDriver $driver) use ($by, $text) { */ public static function elementToBeClickable(WebDriverBy $by) { - $visibility_of_element_located = - self::visibilityOfElementLocated($by); + $visibility_of_element_located = self::visibilityOfElementLocated($by); return new static( function (WebDriver $driver) use ($visibility_of_element_located) { diff --git a/tests/unit/WebDriverExpectedConditionTest.php b/tests/unit/WebDriverExpectedConditionTest.php index 8cd4203a9..6dde484ef 100644 --- a/tests/unit/WebDriverExpectedConditionTest.php +++ b/tests/unit/WebDriverExpectedConditionTest.php @@ -135,6 +135,28 @@ public function testShouldDetectPresenceOfElementLocatedCondition() $this->assertSame($element, $this->wait->until($condition)); } + public function testShouldDetectNotPresenceOfElementLocatedCondition() + { + $element = new RemoteWebElement(new RemoteExecuteMethod($this->driverMock), 'id'); + + $this->driverMock->expects($this->at(0)) + ->method('findElement') + ->with($this->isInstanceOf(WebDriverBy::class)) + ->willReturn($element); + + $this->driverMock->expects($this->at(1)) + ->method('findElement') + ->with($this->isInstanceOf(WebDriverBy::class)) + ->willThrowException(new NoSuchElementException('')); + + $condition = WebDriverExpectedCondition::not( + WebDriverExpectedCondition::presenceOfElementLocated(WebDriverBy::cssSelector('.foo')) + ); + + $this->assertFalse(call_user_func($condition->getApply(), $this->driverMock)); + $this->assertTrue(call_user_func($condition->getApply(), $this->driverMock)); + } + public function testShouldDetectPresenceOfAllElementsLocatedByCondition() { $element = $this->createMock(RemoteWebElement::class); @@ -160,7 +182,7 @@ public function testShouldDetectVisibilityOfElementLocatedCondition() // Call #1: throws NoSuchElementException // Call #2: return Element, but isDisplayed will throw StaleElementReferenceException // Call #3: return Element, but isDisplayed will return false - // Call #4: return Element, isDisplayed will true and condition will match + // Call #4: return Element, isDisplayed will return true and condition will match $element = $this->createMock(RemoteWebElement::class); $element->expects($this->at(0)) From 1f8d242f98310b12d8dbfc39859aad6b13f35708 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Mon, 1 Oct 2018 12:16:55 +0200 Subject: [PATCH 012/350] Use polyfill-mbstring when ext-mbstring isn't available --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 3a841a504..e5aef5a03 100644 --- a/composer.json +++ b/composer.json @@ -14,9 +14,9 @@ "require": { "php": "^5.6 || ~7.0", "symfony/process": "^2.8 || ^3.1 || ^4.0 || ^5.0", + "symfony/polyfill-mbstring": "^1.12", "ext-curl": "*", "ext-zip": "*", - "ext-mbstring": "*", "ext-json": "*" }, "require-dev": { From dc50f3f0fd5506230f4fcc48670bf1edcf1d65ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Machulda?= Date: Mon, 11 Nov 2019 00:04:33 +0100 Subject: [PATCH 013/350] Skipped tests must be completely excluded from Saucelabs to avoid starting the driver there, otherwise the build is not marked passed on Saucelabs dashboard --- tests/functional/RemoteWebDriverFindElementTest.php | 3 +++ tests/functional/WebDriverActionsTest.php | 1 + 2 files changed, 4 insertions(+) diff --git a/tests/functional/RemoteWebDriverFindElementTest.php b/tests/functional/RemoteWebDriverFindElementTest.php index 066179f1f..2eb3e38c7 100644 --- a/tests/functional/RemoteWebDriverFindElementTest.php +++ b/tests/functional/RemoteWebDriverFindElementTest.php @@ -62,6 +62,9 @@ public function testShouldFindMultipleElements() $this->assertContainsOnlyInstancesOf(RemoteWebElement::class, $elements); } + /** + * @group exclude-saucelabs + */ public function testEscapeCssSelector() { if (getenv('GECKODRIVER') !== '1') { diff --git a/tests/functional/WebDriverActionsTest.php b/tests/functional/WebDriverActionsTest.php index 1f03c8899..07af88085 100644 --- a/tests/functional/WebDriverActionsTest.php +++ b/tests/functional/WebDriverActionsTest.php @@ -88,6 +88,7 @@ public function testShouldClickAndHoldOnElementAndRelease() } /** + * @group exclude-saucelabs * @covers ::__construct * @covers ::contextClick * @covers ::perform From a29494a551b8efb8afef51dc4f7af2cb7af98d9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Machulda?= Date: Sun, 10 Nov 2019 23:01:03 +0100 Subject: [PATCH 014/350] Add Chromedriver Travis build (without Selenium server proxy) --- .travis.yml | 32 +++++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index 950e8fe64..986df3e31 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,7 +17,8 @@ env: matrix: include: # Codestyle check build - - php: '7.3' + - name: 'Code style and static analysis' + php: '7.3' env: CHECK_CODESTYLE=1 before_install: - phpenv config-rm xdebug.ini @@ -30,18 +31,19 @@ matrix: after_success: ~ # Build with lowest possible dependencies on lowest possible PHP - - php: '5.6' + - name: 'Lowest dependencies build' + php: '5.6' env: DEPENDENCIES="--prefer-lowest" # Firefox inside Travis environment - - name: 'Firefox 45 on Travis (OSS protocol)' + - name: 'Firefox 45 on Travis (OSS protocol); via Selenium server' php: '7.3' env: BROWSER_NAME="firefox" addons: firefox: "45.8.0esr" # Firefox with Geckodriver (W3C mode) inside Travis environment - - name: 'Firefox latest on Travis (W3C protocol)' + - name: 'Firefox latest on Travis (W3C protocol); no Selenium server' php: 7.3 env: - BROWSER_NAME="firefox" @@ -49,9 +51,22 @@ matrix: addons: firefox: latest - # Stable Chrome + Chromedriver inside Travis environment - - php: '7.3' - env: BROWSER_NAME="chrome" CHROME_HEADLESS="1" + # Stable Chrome + Chromedriver inside Travis environment via Selenium server proxy + - name: 'Chrome stable on Travis; via Selenium server' + php: '7.3' + env: + - BROWSER_NAME="chrome" + - CHROME_HEADLESS="1" + addons: + chrome: stable + + # Stable Chrome + Chromedriver inside Travis environment directly via Chromedriver + - name: 'Chrome stable on Travis; no Selenium server' + php: '7.3' + env: + - BROWSER_NAME="chrome" + - CHROME_HEADLESS="1" + - CHROMEDRIVER="1" addons: chrome: stable @@ -113,6 +128,8 @@ before_script: - if [ ! -f jar/selenium-server-standalone-3.8.1.jar ]; then wget -q -t 3 -P jar https://selenium-release.storage.googleapis.com/3.8/selenium-server-standalone-3.8.1.jar; fi - if [ "$GECKODRIVER" = "1" ]; then geckodriver/geckodriver &> ./logs/geckodriver.log & + elif [ "$CHROMEDRIVER" = "1" ]; then + chromedriver/chromedriver --port=4444 --url-base=/wd/hub &> ./logs/chromedriver.log & else java -Dwebdriver.firefox.marionette=false -Dwebdriver.chrome.driver="$CHROMEDRIVER_PATH" -jar jar/selenium-server-standalone-3.8.1.jar -enablePassThrough false -log ./logs/selenium.log & fi @@ -130,6 +147,7 @@ after_script: - if [ -f ./logs/selenium.log ]; then cat ./logs/selenium.log; fi - if [ -f ./logs/php-server.log ]; then cat ./logs/php-server.log; fi - if [ -f ./logs/geckodriver.log ]; then cat ./logs/geckodriver.log; fi + - if [ -f ./logs/chromedriver.log ]; then cat ./logs/chromedriver.log; fi after_success: - travis_retry php vendor/bin/php-coveralls -v From 61ab881143846201c314bc9f9988a8831a75f26f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Machulda?= Date: Sun, 10 Nov 2019 23:51:18 +0100 Subject: [PATCH 015/350] Use newer Selenium server where possible But we can easily update only to 3.14.0 (latest version which includes HtmlUnit). However update to 3.14.159 should be done soon (but HtmlUnit needs to be sideloaded). --- .travis.yml | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index 986df3e31..fc7368965 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,6 +13,7 @@ env: global: - DISPLAY=:99.0 - BROWSER_NAME="htmlunit" + - SELENIUM_SERVER="https://selenium-release.storage.googleapis.com/3.14/selenium-server-standalone-3.14.0.jar" # Latest version including HtmlUnit matrix: include: @@ -36,9 +37,11 @@ matrix: env: DEPENDENCIES="--prefer-lowest" # Firefox inside Travis environment - - name: 'Firefox 45 on Travis (OSS protocol); via Selenium server' + - name: 'Firefox 45 on Travis (OSS protocol); via legacy Selenium server' php: '7.3' - env: BROWSER_NAME="firefox" + env: + - BROWSER_NAME="firefox" + - SELENIUM_SERVER="legacy" addons: firefox: "45.8.0esr" @@ -114,7 +117,6 @@ matrix: cache: directories: - $HOME/.composer/cache - - jar install: - travis_retry composer self-update @@ -123,15 +125,24 @@ install: before_script: - if [ "$BROWSER_NAME" = "chrome" ]; then mkdir chromedriver; CHROMEDRIVER_VERSION=$(wget -qO- "https://chromedriver.storage.googleapis.com/LATEST_RELEASE"); wget -q -t 3 https://chromedriver.storage.googleapis.com/$CHROMEDRIVER_VERSION/chromedriver_linux64.zip; unzip chromedriver_linux64 -d chromedriver; fi - if [ "$BROWSER_NAME" = "chrome" ]; then export CHROMEDRIVER_PATH=$PWD/chromedriver/chromedriver; fi - - if [ "$GECKODRIVER" = "1" ]; then mkdir geckodriver; wget -q -t 3 https://github.com/mozilla/geckodriver/releases/download/v0.26.0/geckodriver-v0.26.0-linux64.tar.gz; tar xzf geckodriver-v0.26.0-linux64.tar.gz -C geckodriver; fi - - sh -e /etc/init.d/xvfb start - - if [ ! -f jar/selenium-server-standalone-3.8.1.jar ]; then wget -q -t 3 -P jar https://selenium-release.storage.googleapis.com/3.8/selenium-server-standalone-3.8.1.jar; fi + - if [ "$GECKODRIVER" = "1" ]; then mkdir -p geckodriver; wget -q -t 3 https://github.com/mozilla/geckodriver/releases/download/v0.26.0/geckodriver-v0.26.0-linux64.tar.gz; tar xzf geckodriver-v0.26.0-linux64.tar.gz -C geckodriver; fi + - sh -e /etc/init.d/xvfb start # TODO: start only when needed (ie. not in headless mode) + - if [ ! -f jar/selenium-server-standalone.jar ] && [ -n "$SELENIUM_SERVER" ]; then + mkdir -p jar; + if [ "$SELENIUM_SERVER" = "legacy" ]; then + wget -q -t 3 -O jar/selenium-server-standalone.jar https://selenium-release.storage.googleapis.com/3.8/selenium-server-standalone-3.8.1.jar; + else + wget -q -t 3 -O jar/selenium-server-standalone.jar $SELENIUM_SERVER; + fi + fi - if [ "$GECKODRIVER" = "1" ]; then geckodriver/geckodriver &> ./logs/geckodriver.log & elif [ "$CHROMEDRIVER" = "1" ]; then chromedriver/chromedriver --port=4444 --url-base=/wd/hub &> ./logs/chromedriver.log & + elif [ "$SELENIUM_SERVER" = "legacy" ]; then + java -Dwebdriver.firefox.marionette=false -Dwebdriver.chrome.driver="$PWD/chromedriver/chromedriver" -jar jar/selenium-server-standalone.jar -enablePassThrough false -log ./logs/selenium.log & else - java -Dwebdriver.firefox.marionette=false -Dwebdriver.chrome.driver="$CHROMEDRIVER_PATH" -jar jar/selenium-server-standalone-3.8.1.jar -enablePassThrough false -log ./logs/selenium.log & + java -Dwebdriver.chrome.driver="$PWD/chromedriver/chromedriver" -Dwebdriver.gecko.driver="$PWD/geckodriver/geckodriver" -jar jar/selenium-server-standalone.jar -log ./logs/selenium.log & fi - until $(echo | nc localhost 4444); do sleep 1; echo Waiting for Selenium server on port 4444...; done; echo "Selenium server started" - php -S 127.0.0.1:8000 -t tests/functional/web/ &>>./logs/php-server.log & From a4b0d7130ed35c2083f62ae0bb135c563df68364 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Machulda?= Date: Mon, 11 Nov 2019 10:21:11 +0100 Subject: [PATCH 016/350] Convert W3C invalid capabilities to corresponding ones This avoids `Facebook\WebDriver\Exception\UnknownServerException: Illegal key values seen in w3c capabilities: [chromeOptions]` error with new Selenium server. This conversion is necessary to maintain BC - simply renaming chromeOptions to goog:chromeOptions will break capabilities with Chromedriver <2.31. --- lib/Chrome/ChromeOptions.php | 7 ++++++- lib/Remote/RemoteWebDriver.php | 32 ++++++++++++++++++++++++++++---- 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/lib/Chrome/ChromeOptions.php b/lib/Chrome/ChromeOptions.php index 901dc5388..949d6708b 100644 --- a/lib/Chrome/ChromeOptions.php +++ b/lib/Chrome/ChromeOptions.php @@ -25,9 +25,14 @@ class ChromeOptions { /** - * The key of chrome options in desired capabilities. + * The key of chrome options desired capabilities (in legacy OSS JsonWire protocol) + * @deprecated */ const CAPABILITY = 'chromeOptions'; + /** + * The key of chrome options desired capabilities (in W3C compatible protocol) + */ + const CAPABILITY_W3C = 'goog:chromeOptions'; /** * @var array */ diff --git a/lib/Remote/RemoteWebDriver.php b/lib/Remote/RemoteWebDriver.php index 4d3573c0c..c87a572f9 100644 --- a/lib/Remote/RemoteWebDriver.php +++ b/lib/Remote/RemoteWebDriver.php @@ -140,15 +140,15 @@ public static function create( // W3C $parameters = [ 'capabilities' => [ - 'firstMatch' => [$desired_capabilities->toArray()], + 'firstMatch' => [static::convertCapabilitiesToW3c($desired_capabilities->toArray())], ], ]; - // Legacy protocol - if (null !== $required_capabilities && $required_capabilities_array = $required_capabilities->toArray()) { - $parameters['capabilities']['alwaysMatch'] = $required_capabilities_array; + if ($required_capabilities !== null && $required_capabilities_array = $required_capabilities->toArray()) { + $parameters['capabilities']['alwaysMatch'] = static::convertCapabilitiesToW3c($required_capabilities_array); } + // Legacy protocol if ($required_capabilities !== null) { // TODO: Selenium (as of v3.0.1) does accept requiredCapabilities only as a property of desiredCapabilities. // This has changed with the W3C WebDriver spec, but is the only way how to pass these @@ -677,4 +677,28 @@ protected static function castToDesiredCapabilitiesObject($desired_capabilities return $desired_capabilities; } + + /** + * Convert keys invalid in W3C capabilities to corresponding ones for W3C. + * + * @param array $capabilitiesArray + * @return array + */ + protected static function convertCapabilitiesToW3c(array $capabilitiesArray) + { + if (array_key_exists(ChromeOptions::CAPABILITY, $capabilitiesArray)) { + if (array_key_exists(ChromeOptions::CAPABILITY_W3C, $capabilitiesArray)) { + $capabilitiesArray[ChromeOptions::CAPABILITY_W3C] = array_merge( + $capabilitiesArray[ChromeOptions::CAPABILITY], + $capabilitiesArray[ChromeOptions::CAPABILITY_W3C] + ); + } else { + $capabilitiesArray[ChromeOptions::CAPABILITY_W3C] = $capabilitiesArray[ChromeOptions::CAPABILITY]; + } + + unset($capabilitiesArray[ChromeOptions::CAPABILITY]); + } + + return $capabilitiesArray; + } } From 3597a6bcaddb2a462d71cf81620346892991ad47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Machulda?= Date: Mon, 11 Nov 2019 10:37:25 +0100 Subject: [PATCH 017/350] =?UTF-8?q?Enable=20W3C=20protocol=20for=20Chromed?= =?UTF-8?q?river=20=F0=9F=8E=89=20#469?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Until now, W3C was forcily disabled for Chromedriver using Capabilities on startup. As we now experimentaly support W3C protocol, this is no longer needed. If W3C is detected during initial handshake, the browser will be started using W3C dialect (otherwise it will use legacy OSS JsonWire protocol). --- lib/Remote/RemoteWebDriver.php | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/lib/Remote/RemoteWebDriver.php b/lib/Remote/RemoteWebDriver.php index c87a572f9..d2e302f44 100644 --- a/lib/Remote/RemoteWebDriver.php +++ b/lib/Remote/RemoteWebDriver.php @@ -112,23 +112,6 @@ public static function create( $desired_capabilities = self::castToDesiredCapabilitiesObject($desired_capabilities); - // Hotfix: W3C WebDriver protocol is not yet supported by php-webdriver, so we must force Chromedriver to - // not use the W3C protocol by default (which is what Chromedriver does starting with version 75). - if ($desired_capabilities->getBrowserName() === WebDriverBrowserType::CHROME - && mb_strpos($selenium_server_url, 'browserstack') === false // see https://github.com/facebook/php-webdriver/issues/644 - ) { - $currentChromeOptions = $desired_capabilities->getCapability(ChromeOptions::CAPABILITY); - $chromeOptions = !empty($currentChromeOptions) ? $currentChromeOptions : new ChromeOptions(); - - if ($chromeOptions instanceof ChromeOptions && !isset($chromeOptions->toArray()['w3c'])) { - $chromeOptions->setExperimentalOption('w3c', false); - } elseif (is_array($chromeOptions) && !isset($chromeOptions['w3c'])) { - $chromeOptions['w3c'] = false; - } - - $desired_capabilities->setCapability(ChromeOptions::CAPABILITY, $chromeOptions); - } - $executor = new HttpCommandExecutor($selenium_server_url, $http_proxy, $http_proxy_port); if ($connection_timeout_in_ms !== null) { $executor->setConnectionTimeout($connection_timeout_in_ms); From a9685bf2aa22de07d97ca93ff452e5016ee3141d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Machulda?= Date: Mon, 11 Nov 2019 12:41:45 +0100 Subject: [PATCH 018/350] Add JsonWire travis build on Chromedriver to make sure we won't break the old protocol --- .travis.yml | 19 +++++++++++++++---- tests/functional/WebDriverTestCase.php | 5 +++++ 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index fc7368965..4a8a268a8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -54,8 +54,8 @@ matrix: addons: firefox: latest - # Stable Chrome + Chromedriver inside Travis environment via Selenium server proxy - - name: 'Chrome stable on Travis; via Selenium server' + # Stable Chrome + Chromedriver (W3C mode) inside Travis environment via Selenium server proxy + - name: 'Chrome stable on Travis (W3C protocol); via Selenium server' php: '7.3' env: - BROWSER_NAME="chrome" @@ -63,8 +63,8 @@ matrix: addons: chrome: stable - # Stable Chrome + Chromedriver inside Travis environment directly via Chromedriver - - name: 'Chrome stable on Travis; no Selenium server' + # Stable Chrome + Chromedriver (W3C mode) inside Travis environment directly via Chromedriver + - name: 'Chrome stable on Travis (W3C protocol); no Selenium server' php: '7.3' env: - BROWSER_NAME="chrome" @@ -73,6 +73,17 @@ matrix: addons: chrome: stable + # Stable Chrome + Chromedriver (JsonWire OSS mode) inside Travis environment directly via Chromedriver + - name: 'Chrome stable on Travis (OSS protocol); no Selenium server' + php: '7.3' + env: + - BROWSER_NAME="chrome" + - CHROME_HEADLESS="1" + - CHROMEDRIVER="1" + - DISABLE_W3C_PROTOCOL="1" + addons: + chrome: stable + # Saucelabs builds - php: '7.3' env: SAUCELABS=1 BROWSER_NAME="firefox" VERSION="47.0" PLATFORM="Windows 10" diff --git a/tests/functional/WebDriverTestCase.php b/tests/functional/WebDriverTestCase.php index 7a0f8af23..920eb86a7 100644 --- a/tests/functional/WebDriverTestCase.php +++ b/tests/functional/WebDriverTestCase.php @@ -57,6 +57,11 @@ protected function setUp() $chromeOptions = new ChromeOptions(); // --no-sandbox is a workaround for Chrome crashing: https://github.com/SeleniumHQ/selenium/issues/4961 $chromeOptions->addArguments(['--headless', 'window-size=1024,768', '--no-sandbox']); + + if (getenv('DISABLE_W3C_PROTOCOL')) { + $chromeOptions->setExperimentalOption('w3c', false); + } + $this->desiredCapabilities->setCapability(ChromeOptions::CAPABILITY, $chromeOptions); } elseif (getenv('GECKODRIVER') === '1') { $this->serverUrl = 'http://localhost:4444/'; From 041658be9e1284578f5fba1198b10dc8922ceb0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Machulda?= Date: Mon, 11 Nov 2019 13:25:45 +0100 Subject: [PATCH 019/350] Improve W3C builds detection in conditionally-run tests --- .../RemoteWebDriverFindElementTest.php | 8 +++--- tests/functional/RemoteWebDriverTest.php | 1 - tests/functional/RemoteWebElementTest.php | 4 +-- tests/functional/WebDriverActionsTest.php | 4 +-- tests/functional/WebDriverTestCase.php | 26 +++++++++++++++++++ 5 files changed, 32 insertions(+), 11 deletions(-) diff --git a/tests/functional/RemoteWebDriverFindElementTest.php b/tests/functional/RemoteWebDriverFindElementTest.php index 2eb3e38c7..c5671b497 100644 --- a/tests/functional/RemoteWebDriverFindElementTest.php +++ b/tests/functional/RemoteWebDriverFindElementTest.php @@ -67,11 +67,9 @@ public function testShouldFindMultipleElements() */ public function testEscapeCssSelector() { - if (getenv('GECKODRIVER') !== '1') { - $this->markTestSkipped( - 'CSS selectors containing special characters are not supported by the legacy protocol' - ); - } + self::skipForJsonWireProtocol( + 'CSS selectors containing special characters are not supported by the legacy protocol' + ); $this->driver->get($this->getTestPageUrl('escape_css.html')); diff --git a/tests/functional/RemoteWebDriverTest.php b/tests/functional/RemoteWebDriverTest.php index 9105aa93f..72fc30505 100644 --- a/tests/functional/RemoteWebDriverTest.php +++ b/tests/functional/RemoteWebDriverTest.php @@ -150,7 +150,6 @@ public function testShouldCloseWindow() $this->driver->get($this->getTestPageUrl('open_new_window.html')); $this->driver->findElement(WebDriverBy::cssSelector('a'))->click(); - // Mandatory for Geckodriver $this->driver->wait()->until(WebDriverExpectedCondition::numberOfWindowsToBe(2)); $this->assertCount(2, $this->driver->getWindowHandles()); diff --git a/tests/functional/RemoteWebElementTest.php b/tests/functional/RemoteWebElementTest.php index a4684ace3..52877dcda 100644 --- a/tests/functional/RemoteWebElementTest.php +++ b/tests/functional/RemoteWebElementTest.php @@ -272,9 +272,7 @@ public function testShouldSubmitFormByClickOnSubmitInput() */ public function testShouldCompareEqualsElement() { - if (getenv('GECKODRIVER') === '1') { - $this->markTestSkipped('"equals" is not supported by the W3C specification'); - } + self::skipForW3cProtocol('"equals" is not supported by the W3C specification'); $this->driver->get($this->getTestPageUrl('index.html')); diff --git a/tests/functional/WebDriverActionsTest.php b/tests/functional/WebDriverActionsTest.php index 07af88085..9798f6e5a 100644 --- a/tests/functional/WebDriverActionsTest.php +++ b/tests/functional/WebDriverActionsTest.php @@ -49,7 +49,7 @@ public function testShouldClickOnElement() $logs = ['mouseover item-1', 'mousedown item-1', 'mouseup item-1', 'click item-1']; $loggedEvents = $this->retrieveLoggedEvents(); - if ('1' === getenv('GECKODRIVER')) { + if (getenv('GECKODRIVER') === '1') { $loggedEvents = array_slice($loggedEvents, 0, count($logs)); // Firefox sometimes triggers some extra events // it's not related to Geckodriver, it's Firefox's own behavior @@ -77,7 +77,7 @@ public function testShouldClickAndHoldOnElementAndRelease() ->release() ->perform(); - if ('1' === getenv('GECKODRIVER')) { + if (self::isW3cProtocolBuild()) { $this->assertArraySubset(['mouseover item-1', 'mousedown item-1'], $this->retrieveLoggedEvents()); } else { $this->assertSame( diff --git a/tests/functional/WebDriverTestCase.php b/tests/functional/WebDriverTestCase.php index 920eb86a7..8b35a7700 100644 --- a/tests/functional/WebDriverTestCase.php +++ b/tests/functional/WebDriverTestCase.php @@ -102,6 +102,32 @@ public static function isSauceLabsBuild() return getenv('SAUCELABS') ? true : false; } + /** + * @return bool + */ + public static function isW3cProtocolBuild() + { + return getenv('GECKODRIVER') === '1' + || (getenv('BROWSER_NAME') === 'chrome' + && getenv('DISABLE_W3C_PROTOCOL') !== '1' + && !self::isSauceLabsBuild()); + } + + public static function skipForW3cProtocol($message = 'Not supported by W3C specification') + { + if (static::isW3cProtocolBuild()) { + static::markTestSkipped($message); + } + } + + public static function skipForJsonWireProtocol($message = 'Not supported by JsonWire protocol') + { + if (getenv('GECKODRIVER') !== '1' + && (getenv('CHROMEDRIVER') !== '1' || getenv('DISABLE_W3C_PROTOCOL') === '1')) { + static::markTestSkipped($message); + } + } + /** * Get the URL of given test HTML on running webserver. * From d249dea3f0da638c2d44c028b4a5d9247cc2fa87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Machulda?= Date: Mon, 11 Nov 2019 16:36:05 +0100 Subject: [PATCH 020/350] Update changelog and readme to let the world know about W3C protocol experimental support --- CHANGELOG.md | 3 +++ README.md | 38 ++++++++++++++++++-------------------- 2 files changed, 21 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bbd2f4d52..b83a21743 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,9 @@ This project versioning adheres to [Semantic Versioning](http://semver.org/). ## Unreleased +### Added +- Experimental W3C WebDriver protocol support. The protocol will be used automatically when remote end (like Geckodriver, newer Chromedriver etc.) supports it. + ### Changed - Revert no longer needed workaround for Chromedriver bug [2943](https://bugs.chromium.org/p/chromedriver/issues/detail?id=2943). - Allow installation of Symfony 5 components. diff --git a/README.md b/README.md index 0fa9db31c..71742583b 100644 --- a/README.md +++ b/README.md @@ -9,16 +9,16 @@ ## Description Php-webdriver library is PHP language binding for Selenium WebDriver, which allows you to control web browsers from PHP. -This library is compatible with Selenium server version 2.x and 3.x. -It implements the [JsonWireProtocol](https://github.com/SeleniumHQ/selenium/wiki/JsonWireProtocol), which is currently supported -by the Selenium server and will also implement the [W3C WebDriver](https://w3c.github.io/webdriver/webdriver-spec.html) specification in the future. +This library is compatible with Selenium server version 2.x, 3.x and 4.x. + +The library supports [JsonWireProtocol](https://github.com/SeleniumHQ/selenium/wiki/JsonWireProtocol) and also +implements **experimental support** of [W3C WebDriver](https://w3c.github.io/webdriver/webdriver-spec.html). +The W3C WebDriver support is not yet full-featured, however it should allow to control Firefox via Geckodriver and new +versions of Chrome and Chromedriver with just a slight limitations. The concepts of this library are very similar to the "official" Java, .NET, Python and Ruby bindings from the [Selenium project](https://github.com/SeleniumHQ/selenium/). -**As of 2013, this PHP client has been rewritten from scratch.** -Using the old version? Check out [Adam Goucher's fork](https://github.com/Element-34/php-webdriver) of it. - Looking for API documentation of php-webdriver? See [https://facebook.github.io/php-webdriver/](https://facebook.github.io/php-webdriver/latest/) Any complaints, questions, or ideas? Post them in the user group https://www.facebook.com/groups/phpwebdriver/. @@ -41,24 +41,25 @@ Then install the library: The required server is the `selenium-server-standalone-#.jar` file provided here: http://selenium-release.storage.googleapis.com/index.html -Download and run the server by replacing # with the current server version. Keep in mind **you must have Java 8+ installed to run this command**. +Download and run the server by **replacing #** with the current server version. Keep in mind **you must have Java 8+ installed to run this command**. java -jar selenium-server-standalone-#.jar -**NOTE:** If using Firefox, see alternate command below. - ### Create a Browser Session When creating a browser session, be sure to pass the url of your running server. ```php // This would be the url of the host running the server-standalone.jar -$host = 'http://localhost:4444/wd/hub'; // this is the default +$host = 'http://localhost:4444/wd/hub'; // this is the default url and port where Selenium server starts ``` ##### Launch Chrome -Make sure to have latest Chrome and [Chromedriver](https://sites.google.com/a/chromium.org/chromedriver/downloads) versions installed. +Install latest Chrome and [Chromedriver](https://sites.google.com/a/chromium.org/chromedriver/downloads). + +The `chromedriver` binary must be placed in system `PATH` directory, otherwise you must provide the path when starting Selenium server +(eg. `java -Dwebdriver.chrome.driver="/path/to/chromedriver" -jar selenium-server-standalone-#.jar`). ```php $driver = RemoteWebDriver::create($host, DesiredCapabilities::chrome()); @@ -66,14 +67,11 @@ $driver = RemoteWebDriver::create($host, DesiredCapabilities::chrome()); ##### Launch Firefox -Make sure to have latest Firefox and [Geckodriver](https://github.com/mozilla/geckodriver/releases) installed. - -Because Firefox (and Geckodriver) only support the new W3C WebDriver protocol (which is yet to be implemented by php-webdriver - see [issue #469](https://github.com/facebook/php-webdriver/issues/469)), -the protocols must be translated by Selenium Server - this feature is *partially* available in Selenium Server versions 3.5.0-3.8.1 and you can enable it like this: +Install latest Firefox and [Geckodriver](https://github.com/mozilla/geckodriver/releases). - java -jar selenium-server-standalone-3.8.1.jar -enablePassThrough false +The `geckodriver` binary must be placed in system `PATH` directory, otherwise you must provide the path when starting Selenium server +(eg. `java -Dwebdriver.gecko.driver="/path/to/geckodriver" -jar selenium-server-standalone-#.jar`). -Now you can start Firefox from your code: ```php $driver = RemoteWebDriver::create($host, DesiredCapabilities::firefox()); @@ -82,9 +80,9 @@ $driver = RemoteWebDriver::create($host, DesiredCapabilities::firefox()); ### Customize Desired Capabilities ```php -$desired_capabilities = DesiredCapabilities::firefox(); -$desired_capabilities->setCapability('acceptSslCerts', false); -$driver = RemoteWebDriver::create($host, $desired_capabilities); +$desiredCapabilities = DesiredCapabilities::firefox(); +$desiredCapabilities->setCapability('acceptSslCerts', false); +$driver = RemoteWebDriver::create($host, $desiredCapabilities); ``` * See https://github.com/SeleniumHQ/selenium/wiki/DesiredCapabilities for more details. From 42f82dd233ea52f64f36f143a57c6d6df89c22c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Machulda?= Date: Tue, 12 Nov 2019 23:54:22 +0100 Subject: [PATCH 021/350] Define branch alias for version 1.8 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index e5aef5a03..a6aec98ae 100644 --- a/composer.json +++ b/composer.json @@ -59,7 +59,7 @@ }, "extra": { "branch-alias": { - "dev-community": "1.5-dev" + "dev-community": "1.8.x-dev" } } } From a6cad80b88fcb3c3823afe85d653d0c46c05944c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Machulda?= Date: Thu, 14 Nov 2019 19:10:16 +0100 Subject: [PATCH 022/350] Add convertor to W3C compatible capabilities (fixes #676, part of #469) --- lib/Remote/DesiredCapabilities.php | 73 ++++++++++ lib/Remote/RemoteWebDriver.php | 31 +--- tests/unit/Remote/DesiredCapabilitiesTest.php | 137 ++++++++++++++++++ 3 files changed, 213 insertions(+), 28 deletions(-) diff --git a/lib/Remote/DesiredCapabilities.php b/lib/Remote/DesiredCapabilities.php index 533c8873a..c78d2302b 100644 --- a/lib/Remote/DesiredCapabilities.php +++ b/lib/Remote/DesiredCapabilities.php @@ -178,6 +178,79 @@ public function toArray() return $this->capabilities; } + /** + * @return array + */ + public function toW3cCompatibleArray() + { + $ossToW3c = [ + WebDriverCapabilityType::PLATFORM => 'platformName', + WebDriverCapabilityType::VERSION => 'browserVersion', + WebDriverCapabilityType::ACCEPT_SSL_CERTS => 'acceptInsecureCerts', + ChromeOptions::CAPABILITY => ChromeOptions::CAPABILITY_W3C, + ]; + + $allowedW3cCapabilities = [ + 'browserName', + 'browserVersion', + 'platformName', + 'acceptInsecureCerts', + 'pageLoadStrategy', + 'proxy', + 'setWindowRect', + 'timeouts', + 'strictFileInteractability', + 'unhandledPromptBehavior', + ]; + + $ossCapabilities = $this->toArray(); + $w3cCapabilities = []; + + foreach ($ossCapabilities as $capabilityKey => $capabilityValue) { + // Copy already W3C compatible capabilities + if (in_array($capabilityKey, $allowedW3cCapabilities, true)) { + $w3cCapabilities[$capabilityKey] = $capabilityValue; + } + + // Convert capabilitites with changed name + if (array_key_exists($capabilityKey, $ossToW3c)) { + if ($capabilityKey === 'platform') { + $w3cCapabilities[$ossToW3c[$capabilityKey]] = mb_strtolower($capabilityValue); + } else { + $w3cCapabilities[$ossToW3c[$capabilityKey]] = $capabilityValue; + } + } + + // Copy vendor extensions + if (mb_strpos($capabilityKey, ':') !== false) { + $w3cCapabilities[$capabilityKey] = $capabilityValue; + } + } + + // Convert ChromeOptions + if (array_key_exists(ChromeOptions::CAPABILITY, $ossCapabilities)) { + if (array_key_exists(ChromeOptions::CAPABILITY_W3C, $ossCapabilities)) { + $w3cCapabilities[ChromeOptions::CAPABILITY_W3C] = array_merge_recursive( + $ossCapabilities[ChromeOptions::CAPABILITY], + $ossCapabilities[ChromeOptions::CAPABILITY_W3C] + ); + } else { + $w3cCapabilities[ChromeOptions::CAPABILITY_W3C] = $ossCapabilities[ChromeOptions::CAPABILITY]; + } + } + + // Convert Firefox profile + if (array_key_exists(FirefoxDriver::PROFILE, $ossCapabilities)) { + // Convert profile only if not already set in moz:firefoxOptions + if (!array_key_exists('moz:firefoxOptions', $ossCapabilities) + || !array_key_exists('profile', $ossCapabilities['moz:firefoxOptions'])) { + $w3cCapabilities['moz:firefoxOptions']['profile'] = $ossCapabilities[FirefoxDriver::PROFILE]; + } + } + + return $w3cCapabilities; + } + /** * @return static */ diff --git a/lib/Remote/RemoteWebDriver.php b/lib/Remote/RemoteWebDriver.php index d2e302f44..1762f44a8 100644 --- a/lib/Remote/RemoteWebDriver.php +++ b/lib/Remote/RemoteWebDriver.php @@ -15,7 +15,6 @@ namespace Facebook\WebDriver\Remote; -use Facebook\WebDriver\Chrome\ChromeOptions; use Facebook\WebDriver\Interactions\WebDriverActions; use Facebook\WebDriver\JavaScriptExecutor; use Facebook\WebDriver\WebDriver; @@ -123,12 +122,12 @@ public static function create( // W3C $parameters = [ 'capabilities' => [ - 'firstMatch' => [static::convertCapabilitiesToW3c($desired_capabilities->toArray())], + 'firstMatch' => [$desired_capabilities->toW3cCompatibleArray()], ], ]; - if ($required_capabilities !== null && $required_capabilities_array = $required_capabilities->toArray()) { - $parameters['capabilities']['alwaysMatch'] = static::convertCapabilitiesToW3c($required_capabilities_array); + if ($required_capabilities !== null && !empty($required_capabilities->toArray())) { + $parameters['capabilities']['alwaysMatch'] = $required_capabilities->toW3cCompatibleArray(); } // Legacy protocol @@ -660,28 +659,4 @@ protected static function castToDesiredCapabilitiesObject($desired_capabilities return $desired_capabilities; } - - /** - * Convert keys invalid in W3C capabilities to corresponding ones for W3C. - * - * @param array $capabilitiesArray - * @return array - */ - protected static function convertCapabilitiesToW3c(array $capabilitiesArray) - { - if (array_key_exists(ChromeOptions::CAPABILITY, $capabilitiesArray)) { - if (array_key_exists(ChromeOptions::CAPABILITY_W3C, $capabilitiesArray)) { - $capabilitiesArray[ChromeOptions::CAPABILITY_W3C] = array_merge( - $capabilitiesArray[ChromeOptions::CAPABILITY], - $capabilitiesArray[ChromeOptions::CAPABILITY_W3C] - ); - } else { - $capabilitiesArray[ChromeOptions::CAPABILITY_W3C] = $capabilitiesArray[ChromeOptions::CAPABILITY]; - } - - unset($capabilitiesArray[ChromeOptions::CAPABILITY]); - } - - return $capabilitiesArray; - } } diff --git a/tests/unit/Remote/DesiredCapabilitiesTest.php b/tests/unit/Remote/DesiredCapabilitiesTest.php index 6e14ef044..4ef4a055a 100644 --- a/tests/unit/Remote/DesiredCapabilitiesTest.php +++ b/tests/unit/Remote/DesiredCapabilitiesTest.php @@ -15,6 +15,7 @@ namespace Facebook\WebDriver\Remote; +use Facebook\WebDriver\Chrome\ChromeOptions; use Facebook\WebDriver\Firefox\FirefoxDriver; use Facebook\WebDriver\Firefox\FirefoxPreferences; use Facebook\WebDriver\Firefox\FirefoxProfile; @@ -130,4 +131,140 @@ public function testShouldSetupFirefoxProfileAndDisableReaderViewForFirefoxBrows $this->assertSame('false', $firefoxProfile->getPreference(FirefoxPreferences::READER_PARSE_ON_LOAD_ENABLED)); } + + /** + * @dataProvider provideW3cCapabilities + * @param DesiredCapabilities $inputJsonWireCapabilities + * @param array $expectedW3cCapabilities + */ + public function testShouldConvertCapabilitiesToW3cCompatible( + DesiredCapabilities $inputJsonWireCapabilities, + array $expectedW3cCapabilities + ) { + $this->assertEquals( + $expectedW3cCapabilities, + $inputJsonWireCapabilities->toW3cCompatibleArray() + ); + } + + /** + * @return array[] + */ + public function provideW3cCapabilities() + { + $chromeOptions = new ChromeOptions(); + $chromeOptions->addArguments([ + '--headless', + ]); + + $firefoxProfileEncoded = (new FirefoxProfile())->encode(); + + return [ + 'changed name' => [ + new DesiredCapabilities([ + WebDriverCapabilityType::BROWSER_NAME => WebDriverBrowserType::CHROME, + WebDriverCapabilityType::VERSION => '67.0.1', + WebDriverCapabilityType::PLATFORM => WebDriverPlatform::LINUX, + WebDriverCapabilityType::ACCEPT_SSL_CERTS => true, + ]), + [ + 'browserName' => 'chrome', + 'browserVersion' => '67.0.1', + 'platformName' => 'linux', + 'acceptInsecureCerts' => true, + ], + ], + 'removed capabilitites' => [ + new DesiredCapabilities([ + WebDriverCapabilityType::WEB_STORAGE_ENABLED => true, + WebDriverCapabilityType::TAKES_SCREENSHOT => false, + ]), + [], + ], + 'custom invalid capability should be removed' => [ + new DesiredCapabilities([ + 'customInvalidCapability' => 'shouldBeRemoved', + ]), + [], + ], + 'already W3C capabilitites' => [ + new DesiredCapabilities([ + 'pageLoadStrategy' => 'eager', + 'strictFileInteractability' => false, + ]), + [ + 'pageLoadStrategy' => 'eager', + 'strictFileInteractability' => false, + ], + ], + 'custom vendor extension' => [ + new DesiredCapabilities([ + 'vendor:prefix' => 'vendor extension should be kept', + ]), + [ + 'vendor:prefix' => 'vendor extension should be kept', + ], + ], + 'chromeOptions should be converted' => [ + new DesiredCapabilities([ + ChromeOptions::CAPABILITY => $chromeOptions, + ]), + [ + 'goog:chromeOptions' => [ + 'args' => ['--headless'], + 'binary' => '', + ], + ], + ], + 'chromeOptions should be merged if already defined' => [ + new DesiredCapabilities([ + ChromeOptions::CAPABILITY => $chromeOptions, + ChromeOptions::CAPABILITY_W3C => [ + 'debuggerAddress' => '127.0.0.1:38947', + 'args' => ['window-size=1024,768'], + ], + ]), + [ + 'goog:chromeOptions' => [ + 'args' => ['--headless', 'window-size=1024,768'], + 'binary' => '', + 'debuggerAddress' => '127.0.0.1:38947', + ], + ], + ], + 'firefox_profile should be converted' => [ + new DesiredCapabilities([ + FirefoxDriver::PROFILE => $firefoxProfileEncoded, + ]), + [ + 'moz:firefoxOptions' => [ + 'profile' => $firefoxProfileEncoded, + ], + ], + ], + 'firefox_profile should not be overwritten if already present' => [ + new DesiredCapabilities([ + FirefoxDriver::PROFILE => $firefoxProfileEncoded, + 'moz:firefoxOptions' => ['profile' => 'w3cProfile'], + ]), + [ + 'moz:firefoxOptions' => [ + 'profile' => 'w3cProfile', + ], + ], + ], + 'firefox_profile should be merged with moz:firefoxOptions if they already exists' => [ + new DesiredCapabilities([ + FirefoxDriver::PROFILE => $firefoxProfileEncoded, + 'moz:firefoxOptions' => ['args' => ['-headless']], + ]), + [ + 'moz:firefoxOptions' => [ + 'profile' => $firefoxProfileEncoded, + 'args' => ['-headless'], + ], + ], + ], + ]; + } } From f81659eaa82820b7a55be5ac173f56188af186c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Machulda?= Date: Fri, 15 Nov 2019 00:39:53 +0100 Subject: [PATCH 023/350] Run geckodriver tests in headless mode --- tests/functional/WebDriverTestCase.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/functional/WebDriverTestCase.php b/tests/functional/WebDriverTestCase.php index 8b35a7700..1f125634e 100644 --- a/tests/functional/WebDriverTestCase.php +++ b/tests/functional/WebDriverTestCase.php @@ -65,6 +65,10 @@ protected function setUp() $this->desiredCapabilities->setCapability(ChromeOptions::CAPABILITY, $chromeOptions); } elseif (getenv('GECKODRIVER') === '1') { $this->serverUrl = 'http://localhost:4444/'; + $this->desiredCapabilities->setCapability( + 'moz:firefoxOptions', + ['args' => ['-headless']] + ); } $this->desiredCapabilities->setBrowserName($browserName); From b4b82e2ccada9a7d251ae4703478e94b5b593370 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Machulda?= Date: Fri, 15 Nov 2019 00:38:54 +0100 Subject: [PATCH 024/350] Add W3C window size and position commands --- lib/Remote/HttpCommandExecutor.php | 9 ++- tests/functional/WebDriverWindowTest.php | 81 ++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 2 deletions(-) create mode 100644 tests/functional/WebDriverWindowTest.php diff --git a/lib/Remote/HttpCommandExecutor.php b/lib/Remote/HttpCommandExecutor.php index b87c94069..d137030a4 100644 --- a/lib/Remote/HttpCommandExecutor.php +++ b/lib/Remote/HttpCommandExecutor.php @@ -144,8 +144,9 @@ class HttpCommandExecutor implements WebDriverCommandExecutor DriverCommand::ACCEPT_ALERT => ['method' => 'POST', 'url' => '/session/:sessionId/alert/accept'], DriverCommand::ACTIONS => ['method' => 'POST', 'url' => '/session/:sessionId/actions'], DriverCommand::DISMISS_ALERT => ['method' => 'POST', 'url' => '/session/:sessionId/alert/dismiss'], - DriverCommand::EXECUTE_SCRIPT => ['method' => 'POST', 'url' => '/session/:sessionId/execute/sync'], DriverCommand::EXECUTE_ASYNC_SCRIPT => ['method' => 'POST', 'url' => '/session/:sessionId/execute/async'], + DriverCommand::EXECUTE_SCRIPT => ['method' => 'POST', 'url' => '/session/:sessionId/execute/sync'], + DriverCommand::GET_ALERT_TEXT => ['method' => 'GET', 'url' => '/session/:sessionId/alert/text'], DriverCommand::GET_CURRENT_WINDOW_HANDLE => ['method' => 'GET', 'url' => '/session/:sessionId/window'], DriverCommand::GET_ELEMENT_LOCATION => ['method' => 'GET', 'url' => '/session/:sessionId/element/:id/rect'], DriverCommand::GET_ELEMENT_PROPERTY => [ @@ -154,11 +155,15 @@ class HttpCommandExecutor implements WebDriverCommandExecutor ], DriverCommand::GET_ELEMENT_SIZE => ['method' => 'GET', 'url' => '/session/:sessionId/element/:id/rect'], DriverCommand::GET_WINDOW_HANDLES => ['method' => 'GET', 'url' => '/session/:sessionId/window/handles'], - DriverCommand::GET_ALERT_TEXT => ['method' => 'GET', 'url' => '/session/:sessionId/alert/text'], + DriverCommand::GET_WINDOW_POSITION => ['method' => 'GET', 'url' => '/session/:sessionId/window/rect'], + DriverCommand::GET_WINDOW_SIZE => ['method' => 'GET', 'url' => '/session/:sessionId/window/rect'], DriverCommand::IMPLICITLY_WAIT => ['method' => 'POST', 'url' => '/session/:sessionId/timeouts'], + DriverCommand::MAXIMIZE_WINDOW => ['method' => 'POST', 'url' => '/session/:sessionId/window/maximize'], DriverCommand::SET_ALERT_VALUE => ['method' => 'POST', 'url' => '/session/:sessionId/alert/text'], DriverCommand::SET_SCRIPT_TIMEOUT => ['method' => 'POST', 'url' => '/session/:sessionId/timeouts'], DriverCommand::SET_TIMEOUT => ['method' => 'POST', 'url' => '/session/:sessionId/timeouts'], + DriverCommand::SET_WINDOW_SIZE => ['method' => 'POST', 'url' => '/session/:sessionId/window/rect'], + DriverCommand::SET_WINDOW_POSITION => ['method' => 'POST', 'url' => '/session/:sessionId/window/rect'], ]; /** * @var string diff --git a/tests/functional/WebDriverWindowTest.php b/tests/functional/WebDriverWindowTest.php new file mode 100644 index 000000000..ccf041b16 --- /dev/null +++ b/tests/functional/WebDriverWindowTest.php @@ -0,0 +1,81 @@ +driver->manage() + ->window() + ->getPosition(); + + $this->assertGreaterThanOrEqual(0, $position->getX()); + $this->assertGreaterThanOrEqual(0, $position->getY()); + } + + public function testShouldGetSize() + { + $size = $this->driver->manage() + ->window() + ->getSize(); + + $this->assertGreaterThan(0, $size->getWidth()); + $this->assertGreaterThan(0, $size->getHeight()); + } + + public function testShouldMaximizeWindow() + { + $sizeBefore = $this->driver->manage() + ->window() + ->getSize(); + + $this->driver->manage() + ->window() + ->maximize(); + + $sizeAfter = $this->driver->manage() + ->window() + ->getSize(); + + $this->assertGreaterThanOrEqual($sizeBefore->getWidth(), $sizeAfter->getWidth()); + $this->assertGreaterThanOrEqual($sizeBefore->getHeight(), $sizeAfter->getHeight()); + } + + public function testShouldSetSize() + { + $sizeBefore = $this->driver->manage() + ->window() + ->getSize(); + $this->assertNotSame(500, $sizeBefore->getWidth()); + $this->assertNotSame(666, $sizeBefore->getHeight()); + + $this->driver->manage() + ->window() + ->setSize(new WebDriverDimension(500, 666)); + + $sizeAfter = $this->driver->manage() + ->window() + ->getSize(); + + $this->assertSame(500, $sizeAfter->getWidth()); + $this->assertSame(666, $sizeAfter->getHeight()); + } + + public function testShouldSetWindowPosition() + { + $this->driver->manage() + ->window() + ->setPosition(new WebDriverPoint(33, 66)); + + $positionAfter = $this->driver->manage() + ->window() + ->getPosition(); + + $this->assertSame(33, $positionAfter->getX()); + $this->assertSame(66, $positionAfter->getY()); + } +} From c7ee5e1852b42d73bc597bb70a1b03344bdcb233 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Machulda?= Date: Fri, 15 Nov 2019 12:17:12 +0100 Subject: [PATCH 025/350] Exclude window tests on Saucelabs, because they are not sane --- tests/functional/WebDriverWindowTest.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/functional/WebDriverWindowTest.php b/tests/functional/WebDriverWindowTest.php index ccf041b16..8de61d3e8 100644 --- a/tests/functional/WebDriverWindowTest.php +++ b/tests/functional/WebDriverWindowTest.php @@ -7,6 +7,9 @@ */ class WebDriverWindowTest extends WebDriverTestCase { + /** + * @group exclude-saucelabs + */ public function testShouldGetPosition() { $position = $this->driver->manage() @@ -45,6 +48,9 @@ public function testShouldMaximizeWindow() $this->assertGreaterThanOrEqual($sizeBefore->getHeight(), $sizeAfter->getHeight()); } + /** + * @group exclude-saucelabs + */ public function testShouldSetSize() { $sizeBefore = $this->driver->manage() From e9f4ec88e7ed10388613aa8b05cf1e3f4d2231d3 Mon Sep 17 00:00:00 2001 From: Lctrs Date: Fri, 22 Nov 2019 00:38:58 +0100 Subject: [PATCH 026/350] Download a compatible version of chromedriver with chrome Following the [algorithm described on the official chromedriver website](https://chromedriver.chromium.org/downloads/version-selection) : > Here are the steps to select the version of ChromeDriver to download: > > - First, find out which version of Chrome you are using. Let's say you have Chrome 72.0.3626.81. > - Take the Chrome version number, remove the last part, and append the result to URL "https://chromedriver.storage.googleapis.com/LATEST_RELEASE_". For example, with Chrome version 72.0.3626.81, you'd get a URL "https://chromedriver.storage.googleapis.com/LATEST_RELEASE_72.0.3626". > - Use the URL created in the last step to retrieve a small file containing the version of ChromeDriver to use. For example, the above URL will get your a file containing "72.0.3626.69". (The actual number may change in the future, of course.) > - Use the version number retrieved from the previous step to construct the URL to download ChromeDriver. With version 72.0.3626.69, the URL would be "https://chromedriver.storage.googleapis.com/index.html?path=72.0.3626.69/". --- .travis.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 4a8a268a8..0340b80e7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -134,7 +134,13 @@ install: - travis_retry composer update --no-interaction $DEPENDENCIES before_script: - - if [ "$BROWSER_NAME" = "chrome" ]; then mkdir chromedriver; CHROMEDRIVER_VERSION=$(wget -qO- "https://chromedriver.storage.googleapis.com/LATEST_RELEASE"); wget -q -t 3 https://chromedriver.storage.googleapis.com/$CHROMEDRIVER_VERSION/chromedriver_linux64.zip; unzip chromedriver_linux64 -d chromedriver; fi + - if [ "$BROWSER_NAME" = "chrome" ]; then + mkdir chromedriver; + CHROME_VERSION=$(google-chrome --product-version); + CHROME_VERSION=${CHROME_VERSION%.*}; + wget -q -t 3 https://chromedriver.storage.googleapis.com/$(curl -L https://chromedriver.storage.googleapis.com/LATEST_RELEASE_${CHROME_VERSION})/chromedriver_linux64.zip; + unzip chromedriver_linux64.zip -d chromedriver; + fi - if [ "$BROWSER_NAME" = "chrome" ]; then export CHROMEDRIVER_PATH=$PWD/chromedriver/chromedriver; fi - if [ "$GECKODRIVER" = "1" ]; then mkdir -p geckodriver; wget -q -t 3 https://github.com/mozilla/geckodriver/releases/download/v0.26.0/geckodriver-v0.26.0-linux64.tar.gz; tar xzf geckodriver-v0.26.0-linux64.tar.gz -C geckodriver; fi - sh -e /etc/init.d/xvfb start # TODO: start only when needed (ie. not in headless mode) From ee14c4392399a9fc7b362f488226141ca67e8348 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Machulda?= Date: Tue, 19 Nov 2019 13:14:34 +0100 Subject: [PATCH 027/350] Use W3C protocol on some Saucelabs builds --- .travis.yml | 20 +++++++------ tests/functional/WebDriverTestCase.php | 33 +++++++++++++++++----- tests/functional/WebDriverTimeoutsTest.php | 3 ++ 3 files changed, 41 insertions(+), 15 deletions(-) diff --git a/.travis.yml b/.travis.yml index 0340b80e7..2c80cbb18 100644 --- a/.travis.yml +++ b/.travis.yml @@ -85,8 +85,9 @@ matrix: chrome: stable # Saucelabs builds - - php: '7.3' - env: SAUCELABS=1 BROWSER_NAME="firefox" VERSION="47.0" PLATFORM="Windows 10" + - name: 'Sauce Labs, Firefox 47, OSS protocol' + php: '7.3' + env: SAUCELABS=1 BROWSER_NAME="firefox" VERSION="47.0" PLATFORM="Windows 10" DISABLE_W3C_PROTOCOL="1" before_script: - php -S 127.0.0.1:8000 -t tests/functional/web/ &>>./logs/php-server.log & - until $(echo | nc localhost 8000); do sleep 1; echo waiting for PHP server on port 8000...; done; echo "PHP server started" @@ -95,8 +96,9 @@ matrix: jwt: secure: HPq5xFhosa1eSGnaRdJzeyEuaE0mhRlG1gf3G7+dKS0VniF30husSyrxZhbGCCKBGxmIySoAQzd43BCwL69EkUEVKDN87Cpid1Ce9KrSfU3cnN8XIb+4QINyy7x1a47RUAfaaOEx53TrW0ShalvjD+ZwDE8LrgagSox6KQ+nQLE= - - php: '7.3' - env: SAUCELABS=1 BROWSER_NAME="chrome" VERSION="74.0" PLATFORM="Windows 10" # 74 is the last version which don't use W3C WebDriver by default + - name: 'Sauce Labs, Chrome 74, OSS protocol' + php: '7.3' + env: SAUCELABS=1 BROWSER_NAME="chrome" VERSION="74.0" PLATFORM="Windows 10" DISABLE_W3C_PROTOCOL="1" # 74 is the last version which don't use W3C WebDriver by default before_script: - php -S 127.0.0.1:8000 -t tests/functional/web/ &>>./logs/php-server.log & - until $(echo | nc localhost 8000); do sleep 1; echo waiting for PHP server on port 8000...; done; echo "PHP server started" @@ -105,8 +107,9 @@ matrix: jwt: secure: HPq5xFhosa1eSGnaRdJzeyEuaE0mhRlG1gf3G7+dKS0VniF30husSyrxZhbGCCKBGxmIySoAQzd43BCwL69EkUEVKDN87Cpid1Ce9KrSfU3cnN8XIb+4QINyy7x1a47RUAfaaOEx53TrW0ShalvjD+ZwDE8LrgagSox6KQ+nQLE= - - php: '7.3' - env: SAUCELABS=1 BROWSER_NAME="chrome" VERSION="75.0" PLATFORM="Windows 10" + - name: 'Sauce Labs, Chrome latest, W3C protocol' + php: '7.3' + env: SAUCELABS=1 BROWSER_NAME="chrome" VERSION="latest" PLATFORM="Windows 10" before_script: - php -S 127.0.0.1:8000 -t tests/functional/web/ &>>./logs/php-server.log & - until $(echo | nc localhost 8000); do sleep 1; echo waiting for PHP server on port 8000...; done; echo "PHP server started" @@ -115,8 +118,9 @@ matrix: jwt: secure: HPq5xFhosa1eSGnaRdJzeyEuaE0mhRlG1gf3G7+dKS0VniF30husSyrxZhbGCCKBGxmIySoAQzd43BCwL69EkUEVKDN87Cpid1Ce9KrSfU3cnN8XIb+4QINyy7x1a47RUAfaaOEx53TrW0ShalvjD+ZwDE8LrgagSox6KQ+nQLE= - - php: '7.3' - env: SAUCELABS=1 BROWSER_NAME="MicrosoftEdge" VERSION="16.16299" PLATFORM="Windows 10" + - name: 'Sauce Labs, Edge latest, W3C protocol' + php: '7.3' + env: SAUCELABS=1 BROWSER_NAME="MicrosoftEdge" VERSION="latest" PLATFORM="Windows 10" before_script: - php -S 127.0.0.1:8000 -t tests/functional/web/ &>>./logs/php-server.log & - until $(echo | nc localhost 8000); do sleep 1; echo waiting for PHP server on port 8000...; done; echo "PHP server started" diff --git a/tests/functional/WebDriverTestCase.php b/tests/functional/WebDriverTestCase.php index 1f125634e..f012d9068 100644 --- a/tests/functional/WebDriverTestCase.php +++ b/tests/functional/WebDriverTestCase.php @@ -112,9 +112,8 @@ public static function isSauceLabsBuild() public static function isW3cProtocolBuild() { return getenv('GECKODRIVER') === '1' - || (getenv('BROWSER_NAME') === 'chrome' - && getenv('DISABLE_W3C_PROTOCOL') !== '1' - && !self::isSauceLabsBuild()); + || (getenv('BROWSER_NAME') === 'chrome' && getenv('DISABLE_W3C_PROTOCOL') !== '1') + || getenv('BROWSER_NAME') === 'MicrosoftEdge'; } public static function skipForW3cProtocol($message = 'Not supported by W3C specification') @@ -153,12 +152,32 @@ protected function setUpSauceLabs() $this->desiredCapabilities->setBrowserName(getenv('BROWSER_NAME')); $this->desiredCapabilities->setVersion(getenv('VERSION')); $this->desiredCapabilities->setPlatform(getenv('PLATFORM')); - $this->desiredCapabilities->setCapability('name', get_class($this) . '::' . $this->getName()); - $this->desiredCapabilities->setCapability('tags', [get_class($this)]); + $name = get_class($this) . '::' . $this->getName(); + $tags = [get_class($this)]; if (getenv('TRAVIS_JOB_NUMBER')) { - $this->desiredCapabilities->setCapability('tunnel-identifier', getenv('TRAVIS_JOB_NUMBER')); - $this->desiredCapabilities->setCapability('build', getenv('TRAVIS_JOB_NUMBER')); + $tunnelIdentifier = getenv('TRAVIS_JOB_NUMBER'); + $build = getenv('TRAVIS_JOB_NUMBER'); + } + + if (!getenv('DISABLE_W3C_PROTOCOL')) { + $sauceOptions = [ + 'name' => $name, + 'tags' => $tags, + ]; + if (isset($tunnelIdentifier, $build)) { + $sauceOptions['tunnelIdentifier'] = $tunnelIdentifier; + $sauceOptions['build'] = $build; + } + $this->desiredCapabilities->setCapability('sauce:options', (object) $sauceOptions); + } else { + $this->desiredCapabilities->setCapability('name', $name); + $this->desiredCapabilities->setCapability('tags', $tags); + + if (isset($tunnelIdentifier, $build)) { + $this->desiredCapabilities->setCapability('tunnel-identifier', $tunnelIdentifier); + $this->desiredCapabilities->setCapability('build', $build); + } } } } diff --git a/tests/functional/WebDriverTimeoutsTest.php b/tests/functional/WebDriverTimeoutsTest.php index 733be545c..cd3e10d99 100644 --- a/tests/functional/WebDriverTimeoutsTest.php +++ b/tests/functional/WebDriverTimeoutsTest.php @@ -26,6 +26,9 @@ */ class WebDriverTimeoutsTest extends WebDriverTestCase { + /** + * @group exclude-saucelabs + */ public function testShouldFailGettingDelayedElementWithoutWait() { $this->driver->get($this->getTestPageUrl('delayed_element.html')); From a0d6be9a5fb351fe1a554bbd4f4a76f896727414 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Machulda?= Date: Tue, 19 Nov 2019 13:23:30 +0100 Subject: [PATCH 028/350] Simplify builds --- .travis.yml | 3 --- tests/functional/WebDriverAlertTest.php | 6 ------ tests/functional/WebDriverTestCase.php | 2 +- 3 files changed, 1 insertion(+), 10 deletions(-) diff --git a/.travis.yml b/.travis.yml index 2c80cbb18..9bb73cc1e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -59,7 +59,6 @@ matrix: php: '7.3' env: - BROWSER_NAME="chrome" - - CHROME_HEADLESS="1" addons: chrome: stable @@ -68,7 +67,6 @@ matrix: php: '7.3' env: - BROWSER_NAME="chrome" - - CHROME_HEADLESS="1" - CHROMEDRIVER="1" addons: chrome: stable @@ -78,7 +76,6 @@ matrix: php: '7.3' env: - BROWSER_NAME="chrome" - - CHROME_HEADLESS="1" - CHROMEDRIVER="1" - DISABLE_W3C_PROTOCOL="1" addons: diff --git a/tests/functional/WebDriverAlertTest.php b/tests/functional/WebDriverAlertTest.php index 163bee157..b99dad1fa 100644 --- a/tests/functional/WebDriverAlertTest.php +++ b/tests/functional/WebDriverAlertTest.php @@ -25,12 +25,6 @@ class WebDriverAlertTest extends WebDriverTestCase { protected function setUp() { - if (getenv('CHROME_HEADLESS') === '1') { - // Alerts in headless mode should be available in next Chrome version (61), see: - // https://bugs.chromium.org/p/chromium/issues/detail?id=718235 - $this->markTestSkipped('Alerts not yet supported by headless Chrome'); - } - parent::setUp(); $this->driver->get($this->getTestPageUrl('alert.html')); diff --git a/tests/functional/WebDriverTestCase.php b/tests/functional/WebDriverTestCase.php index f012d9068..9b302a1f3 100644 --- a/tests/functional/WebDriverTestCase.php +++ b/tests/functional/WebDriverTestCase.php @@ -113,7 +113,7 @@ public static function isW3cProtocolBuild() { return getenv('GECKODRIVER') === '1' || (getenv('BROWSER_NAME') === 'chrome' && getenv('DISABLE_W3C_PROTOCOL') !== '1') - || getenv('BROWSER_NAME') === 'MicrosoftEdge'; + || (self::isSauceLabsBuild() && getenv('DISABLE_W3C_PROTOCOL') !== '1'); } public static function skipForW3cProtocol($message = 'Not supported by W3C specification') From b54d50371b68926a60d2832db2ed94c1d3e95c12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Machulda?= Date: Sat, 23 Nov 2019 14:11:55 +0100 Subject: [PATCH 029/350] Skip tests utilizing xPath selectors, which are not properly supported in Edge --- tests/functional/WebDriverCheckboxesTest.php | 1 + tests/functional/WebDriverRadiosTest.php | 1 + 2 files changed, 2 insertions(+) diff --git a/tests/functional/WebDriverCheckboxesTest.php b/tests/functional/WebDriverCheckboxesTest.php index 48b72a53c..91513bd94 100644 --- a/tests/functional/WebDriverCheckboxesTest.php +++ b/tests/functional/WebDriverCheckboxesTest.php @@ -20,6 +20,7 @@ /** * @covers \Facebook\WebDriver\WebDriverCheckboxes * @covers \Facebook\WebDriver\AbstractWebDriverCheckboxOrRadio + * @group exclude-edge */ class WebDriverCheckboxesTest extends WebDriverTestCase { diff --git a/tests/functional/WebDriverRadiosTest.php b/tests/functional/WebDriverRadiosTest.php index 4d659772b..cf7b8378c 100644 --- a/tests/functional/WebDriverRadiosTest.php +++ b/tests/functional/WebDriverRadiosTest.php @@ -21,6 +21,7 @@ /** * @covers \Facebook\WebDriver\WebDriverRadios * @covers \Facebook\WebDriver\AbstractWebDriverCheckboxOrRadio + * @group exclude-edge */ class WebDriverRadiosTest extends WebDriverTestCase { From a419816547eed70cfcf54fd0763df33c94adcce2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Machulda?= Date: Sat, 23 Nov 2019 17:56:26 +0100 Subject: [PATCH 030/350] Skip file upload tests on Saucelabs, because W3C protocol does not support remote file upload See https://github.com/w3c/webdriver/issues/1355 --- lib/Remote/RemoteWebElement.php | 27 ++++++++++++--------------- tests/functional/FileUploadTest.php | 2 ++ 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/lib/Remote/RemoteWebElement.php b/lib/Remote/RemoteWebElement.php index 75dfa3ba2..33286504b 100644 --- a/lib/Remote/RemoteWebElement.php +++ b/lib/Remote/RemoteWebElement.php @@ -358,6 +358,7 @@ public function isSelected() public function sendKeys($value) { $local_file = $this->fileDetector->getLocalFile($value); + if ($local_file === null) { if ($this->isW3cCompliant) { $params = [ @@ -370,22 +371,18 @@ public function sendKeys($value) ':id' => $this->id, ]; } - - $this->executor->execute(DriverCommand::SEND_KEYS_TO_ELEMENT, $params); - - return $this; - } - - if ($this->isW3cCompliant) { - $params = [ - 'text' => $local_file, - ':id' => $this->id, - ]; } else { - $params = [ - 'value' => WebDriverKeys::encode($this->upload($local_file)), - ':id' => $this->id, - ]; + if ($this->isW3cCompliant) { + $params = [ + 'text' => $local_file, + ':id' => $this->id, + ]; + } else { + $params = [ + 'value' => WebDriverKeys::encode($this->upload($local_file)), + ':id' => $this->id, + ]; + } } $this->executor->execute(DriverCommand::SEND_KEYS_TO_ELEMENT, $params); diff --git a/tests/functional/FileUploadTest.php b/tests/functional/FileUploadTest.php index 5b2f6d2d3..1029a2363 100644 --- a/tests/functional/FileUploadTest.php +++ b/tests/functional/FileUploadTest.php @@ -26,6 +26,8 @@ class FileUploadTest extends WebDriverTestCase /** * @group exclude-edge * https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/6052385/ + * @group exclude-saucelabs + * W3C protocol does not support remote file upload: https://github.com/w3c/webdriver/issues/1355 */ public function testShouldUploadAFile() { From 9d5623f40ab320601f8b2cee2bb463d722bb4393 Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Mon, 25 Nov 2019 15:10:50 +0800 Subject: [PATCH 031/350] Fix mouseUp W3C action --- lib/Remote/RemoteMouse.php | 2 +- tests/functional/WebDriverActionsTest.php | 24 +++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/lib/Remote/RemoteMouse.php b/lib/Remote/RemoteMouse.php index 4bd4a849b..6b7375834 100644 --- a/lib/Remote/RemoteMouse.php +++ b/lib/Remote/RemoteMouse.php @@ -241,7 +241,7 @@ public function mouseUp(WebDriverCoordinates $where = null) 'parameters' => ['pointerType' => 'mouse'], 'actions' => array_merge($moveAction, [ [ - 'type' => 'pointerDown', + 'type' => 'pointerUp', 'duration' => 0, 'button' => 0, ], diff --git a/tests/functional/WebDriverActionsTest.php b/tests/functional/WebDriverActionsTest.php index 9798f6e5a..6799ef1d3 100644 --- a/tests/functional/WebDriverActionsTest.php +++ b/tests/functional/WebDriverActionsTest.php @@ -136,6 +136,30 @@ public function testShouldDoubleClickOnElement() $this->assertContains('dblclick item-3', $this->retrieveLoggedEvents()); } + /** + * @covers ::__construct + * @covers ::dragAndDrop + * @covers ::perform + */ + public function testShouldDragAndDrop() + { + if ($this->desiredCapabilities->getBrowserName() === WebDriverBrowserType::HTMLUNIT) { + $this->markTestSkipped('Not supported by HtmlUnit browser'); + } + + $element = $this->driver->findElement(WebDriverBy::id('item-3')); + $target = $this->driver->findElement(WebDriverBy::id('item-1')); + + $this->driver->action() + ->dragAndDrop($element, $target) + ->perform(); + + $this->assertContains('mouseover item-3', $this->retrieveLoggedEvents()); + $this->assertContains('mousedown item-3', $this->retrieveLoggedEvents()); + $this->assertContains('mouseover item-1', $this->retrieveLoggedEvents()); + $this->assertContains('mouseup item-1', $this->retrieveLoggedEvents()); + } + /** * @return array */ From a94e33b99a8330c90c56128e430ffc55c507d304 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Machulda?= Date: Mon, 25 Nov 2019 17:13:57 +0100 Subject: [PATCH 032/350] Exclude unstable drag and drop test on SauceLabs --- tests/functional/WebDriverActionsTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/functional/WebDriverActionsTest.php b/tests/functional/WebDriverActionsTest.php index 6799ef1d3..cb4359183 100644 --- a/tests/functional/WebDriverActionsTest.php +++ b/tests/functional/WebDriverActionsTest.php @@ -140,6 +140,7 @@ public function testShouldDoubleClickOnElement() * @covers ::__construct * @covers ::dragAndDrop * @covers ::perform + * @group exclude-saucelabs */ public function testShouldDragAndDrop() { From 574684f29b8d513ae213c4567f2f71da92734db7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Machulda?= Date: Sun, 24 Nov 2019 10:10:35 +0100 Subject: [PATCH 033/350] Add all W3C WebDriver exceptions --- .../ElementClickInterceptedException.php | 24 +++++ .../ElementNotInteractableException.php | 23 +++++ .../ElementNotSelectableException.php | 5 +- lib/Exception/ElementNotVisibleException.php | 3 + lib/Exception/ExpectedException.php | 3 + .../IMEEngineActivationFailedException.php | 3 + lib/Exception/IMENotAvailableException.php | 3 + lib/Exception/IndexOutOfBoundsException.php | 3 + .../InsecureCertificateException.php | 24 +++++ lib/Exception/InvalidArgumentException.php | 23 +++++ .../InvalidCookieDomainException.php | 3 + lib/Exception/InvalidCoordinatesException.php | 3 + .../InvalidElementStateException.php | 4 + lib/Exception/InvalidSelectorException.php | 3 + lib/Exception/InvalidSessionIdException.php | 24 +++++ lib/Exception/JavascriptErrorException.php | 23 +++++ .../MoveTargetOutOfBoundsException.php | 3 + lib/Exception/NoAlertOpenException.php | 5 +- lib/Exception/NoCollectionException.php | 3 + lib/Exception/NoScriptResultException.php | 3 + lib/Exception/NoStringException.php | 3 + lib/Exception/NoStringLengthException.php | 3 + lib/Exception/NoStringWrapperException.php | 3 + lib/Exception/NoSuchAlertException.php | 23 +++++ lib/Exception/NoSuchCollectionException.php | 3 + lib/Exception/NoSuchCookieException.php | 24 +++++ lib/Exception/NoSuchDocumentException.php | 5 +- lib/Exception/NoSuchDriverException.php | 3 + lib/Exception/NoSuchElementException.php | 3 + lib/Exception/NoSuchFrameException.php | 3 + lib/Exception/NoSuchWindowException.php | 3 + lib/Exception/NullPointerException.php | 3 + lib/Exception/ScriptTimeoutException.php | 3 + lib/Exception/SessionNotCreatedException.php | 3 + .../StaleElementReferenceException.php | 3 + lib/Exception/TimeoutException.php | 23 +++++ ...php => UnableToCaptureScreenException.php} | 5 +- lib/Exception/UnableToSetCookieException.php | 3 + .../UnexpectedAlertOpenException.php | 3 + .../UnexpectedJavascriptException.php | 5 +- lib/Exception/UnknownCommandException.php | 3 + lib/Exception/UnknownErrorException.php | 23 +++++ lib/Exception/UnknownMethodException.php | 23 +++++ lib/Exception/UnknownServerException.php | 5 +- .../UnsupportedOperationException.php | 3 + lib/Exception/WebDriverException.php | 92 ++++++++++++------- lib/Exception/XPathLookupException.php | 3 + lib/Net/URLChecker.php | 6 +- lib/WebDriverWait.php | 6 +- tests/functional/WebDriverAlertTest.php | 8 +- tests/functional/WebDriverTimeoutsTest.php | 6 +- .../unit/Exception/WebDriverExceptionTest.php | 49 ++++++++-- 52 files changed, 487 insertions(+), 55 deletions(-) create mode 100644 lib/Exception/ElementClickInterceptedException.php create mode 100644 lib/Exception/ElementNotInteractableException.php create mode 100644 lib/Exception/InsecureCertificateException.php create mode 100644 lib/Exception/InvalidArgumentException.php create mode 100644 lib/Exception/InvalidSessionIdException.php create mode 100644 lib/Exception/JavascriptErrorException.php create mode 100644 lib/Exception/NoSuchAlertException.php create mode 100644 lib/Exception/NoSuchCookieException.php create mode 100644 lib/Exception/TimeoutException.php rename lib/Exception/{TimeOutException.php => UnableToCaptureScreenException.php} (85%) create mode 100644 lib/Exception/UnknownErrorException.php create mode 100644 lib/Exception/UnknownMethodException.php diff --git a/lib/Exception/ElementClickInterceptedException.php b/lib/Exception/ElementClickInterceptedException.php new file mode 100644 index 000000000..e4b3a529e --- /dev/null +++ b/lib/Exception/ElementClickInterceptedException.php @@ -0,0 +1,24 @@ +driver->switchTo()->alert()->accept(); - $this->expectException(NoAlertOpenException::class); + if (self::isW3cProtocolBuild()) { + $this->expectException(NoSuchAlertException::class); + } else { + $this->expectException(NoAlertOpenException::class); + } + $this->driver->switchTo()->alert()->accept(); } diff --git a/tests/functional/WebDriverTimeoutsTest.php b/tests/functional/WebDriverTimeoutsTest.php index cd3e10d99..ca85075b8 100644 --- a/tests/functional/WebDriverTimeoutsTest.php +++ b/tests/functional/WebDriverTimeoutsTest.php @@ -17,7 +17,7 @@ use Facebook\WebDriver\Exception\NoSuchElementException; use Facebook\WebDriver\Exception\ScriptTimeoutException; -use Facebook\WebDriver\Exception\TimeOutException; +use Facebook\WebDriver\Exception\TimeoutException; use Facebook\WebDriver\Remote\RemoteWebElement; use Facebook\WebDriver\Remote\WebDriverBrowserType; @@ -65,8 +65,8 @@ public function testShouldFailIfPageIsLoadingLongerThanPageLoadTimeout() try { $this->driver->get($this->getTestPageUrl('slow_loading.html')); - $this->fail('ScriptTimeoutException or TimeOutException exception should be thrown'); - } catch (TimeOutException $e) { // thrown by Selenium 3.0.0+ + $this->fail('ScriptTimeoutException or TimeoutException exception should be thrown'); + } catch (TimeoutException $e) { // thrown by Selenium 3.0.0+ } catch (ScriptTimeoutException $e) { // thrown by Selenium 2 } } diff --git a/tests/unit/Exception/WebDriverExceptionTest.php b/tests/unit/Exception/WebDriverExceptionTest.php index f9aadd207..be3ab964c 100644 --- a/tests/unit/Exception/WebDriverExceptionTest.php +++ b/tests/unit/Exception/WebDriverExceptionTest.php @@ -29,14 +29,15 @@ public function testShouldStoreResultsOnInstantiation() } /** - * @dataProvider statusCodeProvider - * @param int $statusCode + * @dataProvider jsonWireStatusCodeProvider + * @dataProvider w3CWebDriverErrorCodeProvider + * @param int $errorCode * @param string $expectedExceptionType */ - public function testShouldThrowProperExceptionBasedOnSeleniumStatusCode($statusCode, $expectedExceptionType) + public function testShouldThrowProperExceptionBasedOnWebDriverErrorCode($errorCode, $expectedExceptionType) { try { - WebDriverException::throwException($statusCode, 'exception message', ['results']); + WebDriverException::throwException($errorCode, 'exception message', ['results']); } catch (WebDriverException $e) { $this->assertInstanceOf($expectedExceptionType, $e); @@ -48,7 +49,7 @@ public function testShouldThrowProperExceptionBasedOnSeleniumStatusCode($statusC /** * @return array[] */ - public function statusCodeProvider() + public function jsonWireStatusCodeProvider() { return [ [1337, UnrecognizedExceptionException::class], @@ -72,7 +73,7 @@ public function statusCodeProvider() [18, NoScriptResultException::class], [19, XPathLookupException::class], [20, NoSuchCollectionException::class], - [21, TimeOutException::class], + [21, TimeoutException::class], [22, NullPointerException::class], [23, NoSuchWindowException::class], [24, InvalidCookieDomainException::class], @@ -88,4 +89,40 @@ public function statusCodeProvider() [34, MoveTargetOutOfBoundsException::class], ]; } + + /** + * @return array[] + */ + public function w3CWebDriverErrorCodeProvider() + { + return [ + ['element click intercepted', ElementClickInterceptedException::class], + ['element not interactable', ElementNotInteractableException::class], + ['element not interactable', ElementNotInteractableException::class], + ['insecure certificate', InsecureCertificateException::class], + ['invalid argument', InvalidArgumentException::class], + ['invalid cookie domain', InvalidCookieDomainException::class], + ['invalid element state', InvalidElementStateException::class], + ['invalid selector', InvalidSelectorException::class], + ['invalid session id', InvalidSessionIdException::class], + ['javascript error', JavascriptErrorException::class], + ['move target out of bounds', MoveTargetOutOfBoundsException::class], + ['no such alert', NoSuchAlertException::class], + ['no such cookie', NoSuchCookieException::class], + ['no such element', NoSuchElementException::class], + ['no such frame', NoSuchFrameException::class], + ['no such window', NoSuchWindowException::class], + ['script timeout', ScriptTimeoutException::class], + ['session not created', SessionNotCreatedException::class], + ['stale element reference', StaleElementReferenceException::class], + ['timeout', TimeoutException::class], + ['unable to set cookie', UnableToSetCookieException::class], + ['unable to capture screen', UnableToCaptureScreenException::class], + ['unexpected alert open', UnexpectedAlertOpenException::class], + ['unknown command', UnknownCommandException::class], + ['unknown error', UnknownErrorException::class], + ['unknown method', UnknownMethodException::class], + ['unsupported operation', UnsupportedOperationException::class], + ]; + } } From 88fff59dac637265906d100fb91142d3f5475aca Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Thu, 21 Nov 2019 09:32:34 +0800 Subject: [PATCH 034/350] Pass W3C Compliance to RemoteTargetLocator --- lib/Remote/RemoteTargetLocator.php | 11 +++++++---- lib/Remote/RemoteWebDriver.php | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/lib/Remote/RemoteTargetLocator.php b/lib/Remote/RemoteTargetLocator.php index 8bd89ef44..1f8cb9270 100644 --- a/lib/Remote/RemoteTargetLocator.php +++ b/lib/Remote/RemoteTargetLocator.php @@ -33,11 +33,16 @@ class RemoteTargetLocator implements WebDriverTargetLocator * @var WebDriver */ protected $driver; + /** + * @var bool + */ + protected $isW3cCompliant; - public function __construct($executor, $driver) + public function __construct($executor, $driver, $isW3cCompliant = false) { $this->executor = $executor; $this->driver = $driver; + $this->isW3cCompliant = $isW3cCompliant; } /** @@ -112,8 +117,6 @@ public function activeElement() $response = $this->driver->execute(DriverCommand::GET_ACTIVE_ELEMENT, []); $method = new RemoteExecuteMethod($this->driver); - $isW3cCompliant = ($this->driver instanceof RemoteWebDriver) ? $this->driver->isW3cCompliant() : false; - - return new RemoteWebElement($method, JsonWireCompat::getElement($response), $isW3cCompliant); + return new RemoteWebElement($method, JsonWireCompat::getElement($response), $this->isW3cCompliant); } } diff --git a/lib/Remote/RemoteWebDriver.php b/lib/Remote/RemoteWebDriver.php index 1762f44a8..3164bad5e 100644 --- a/lib/Remote/RemoteWebDriver.php +++ b/lib/Remote/RemoteWebDriver.php @@ -432,7 +432,7 @@ public function navigate() */ public function switchTo() { - return new RemoteTargetLocator($this->getExecuteMethod(), $this); + return new RemoteTargetLocator($this->getExecuteMethod(), $this, $this->isW3cCompliant); } /** From 57f871eaa558b1108ece0fc4a713dd57796e4d50 Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Wed, 20 Nov 2019 20:54:38 +0800 Subject: [PATCH 035/350] W3C switchToWindow takes a handle The W3C specification dictates that the parameter used to select the window should be named `handle`. Source: https://www.w3.org/TR/webdriver/#switch-to-window --- lib/Remote/RemoteTargetLocator.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/Remote/RemoteTargetLocator.php b/lib/Remote/RemoteTargetLocator.php index 1f8cb9270..7e8dbcd1f 100644 --- a/lib/Remote/RemoteTargetLocator.php +++ b/lib/Remote/RemoteTargetLocator.php @@ -89,7 +89,12 @@ public function frame($frame) */ public function window($handle) { - $params = ['name' => (string) $handle]; + if ($this->isW3cCompliant) { + $params = ['handle' => (string) $handle]; + } else { + $params = ['name' => (string) $handle]; + } + $this->executor->execute(DriverCommand::SWITCH_TO_WINDOW, $params); return $this->driver; From 98686d91d96db975e1e22f6a8bdc5ccda2b54319 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Machulda?= Date: Mon, 25 Nov 2019 18:54:33 +0100 Subject: [PATCH 036/350] Add tests for switchTo()->window() of RemoteTargetLocator --- tests/functional/RemoteTargetLocatorTest.php | 55 ++++++++++++++++++++ tests/functional/WebDriverAlertTest.php | 1 + tests/functional/web/index.html | 2 + tests/functional/web/open_new_window.html | 2 +- 4 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 tests/functional/RemoteTargetLocatorTest.php diff --git a/tests/functional/RemoteTargetLocatorTest.php b/tests/functional/RemoteTargetLocatorTest.php new file mode 100644 index 000000000..210e90b18 --- /dev/null +++ b/tests/functional/RemoteTargetLocatorTest.php @@ -0,0 +1,55 @@ +driver->get($this->getTestPageUrl('open_new_window.html')); + $originalWindowHandle = $this->driver->getWindowHandle(); + $windowHandlesBefore = $this->driver->getWindowHandles(); + + $this->driver->findElement(WebDriverBy::cssSelector('a#open-new-window')) + ->click(); + + $this->driver->wait()->until( + WebDriverExpectedCondition::numberOfWindowsToBe(2) + ); + + // At first the window should not be switched + $this->assertContains('open_new_window.html', $this->driver->getCurrentURL()); + $this->assertSame($originalWindowHandle, $this->driver->getWindowHandle()); + + /** + * @see https://w3c.github.io/webdriver/#get-window-handles + * > "The order in which the window handles are returned is arbitrary." + * Thus we must first find out which window handle is the new one + */ + $windowHandlesAfter = $this->driver->getWindowHandles(); + $newWindowHandle = array_diff($windowHandlesAfter, $windowHandlesBefore); + $newWindowHandle = reset($newWindowHandle); + + $this->driver->switchTo()->window($newWindowHandle); + + // After switchTo() is called, the active window should be changed + $this->assertContains('index.html', $this->driver->getCurrentURL()); + $this->assertNotSame($originalWindowHandle, $this->driver->getWindowHandle()); + } +} diff --git a/tests/functional/WebDriverAlertTest.php b/tests/functional/WebDriverAlertTest.php index b5fd3dec5..72d38d82a 100644 --- a/tests/functional/WebDriverAlertTest.php +++ b/tests/functional/WebDriverAlertTest.php @@ -21,6 +21,7 @@ /** * @covers \Facebook\WebDriver\WebDriverAlert + * @covers \Facebook\WebDriver\Remote\RemoteTargetLocator */ class WebDriverAlertTest extends WebDriverTestCase { diff --git a/tests/functional/web/index.html b/tests/functional/web/index.html index cf5b9c6c1..ff80fe025 100644 --- a/tests/functional/web/index.html +++ b/tests/functional/web/index.html @@ -11,6 +11,8 @@

Welcome to the facebook/php-webdriver testing page.

| Form with file upload | + Form with checkboxes and radios + | New window opener test page | Delayed render diff --git a/tests/functional/web/open_new_window.html b/tests/functional/web/open_new_window.html index b56e93d69..3c790226a 100644 --- a/tests/functional/web/open_new_window.html +++ b/tests/functional/web/open_new_window.html @@ -5,6 +5,6 @@ php-webdriver test page - open new window + open new window From 51353795e94ed34d23f2657bb690827a072ad14a Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Thu, 21 Nov 2019 08:32:22 +0800 Subject: [PATCH 037/350] Make GET_ACTIVE_ELEMENT a GET for W3C As defined in the specification 12.2.6: HTTP Method URI Template GET /session/{session id}/element/active Source: https://www.w3.org/TR/webdriver/#get-active-element --- lib/Remote/HttpCommandExecutor.php | 1 + tests/functional/RemoteTargetLocatorTest.php | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/lib/Remote/HttpCommandExecutor.php b/lib/Remote/HttpCommandExecutor.php index d137030a4..a1b05c5ff 100644 --- a/lib/Remote/HttpCommandExecutor.php +++ b/lib/Remote/HttpCommandExecutor.php @@ -146,6 +146,7 @@ class HttpCommandExecutor implements WebDriverCommandExecutor DriverCommand::DISMISS_ALERT => ['method' => 'POST', 'url' => '/session/:sessionId/alert/dismiss'], DriverCommand::EXECUTE_ASYNC_SCRIPT => ['method' => 'POST', 'url' => '/session/:sessionId/execute/async'], DriverCommand::EXECUTE_SCRIPT => ['method' => 'POST', 'url' => '/session/:sessionId/execute/sync'], + DriverCommand::GET_ACTIVE_ELEMENT => ['method' => 'GET', 'url' => '/session/:sessionId/element/active'], DriverCommand::GET_ALERT_TEXT => ['method' => 'GET', 'url' => '/session/:sessionId/alert/text'], DriverCommand::GET_CURRENT_WINDOW_HANDLE => ['method' => 'GET', 'url' => '/session/:sessionId/window'], DriverCommand::GET_ELEMENT_LOCATION => ['method' => 'GET', 'url' => '/session/:sessionId/element/:id/rect'], diff --git a/tests/functional/RemoteTargetLocatorTest.php b/tests/functional/RemoteTargetLocatorTest.php index 210e90b18..becc194c8 100644 --- a/tests/functional/RemoteTargetLocatorTest.php +++ b/tests/functional/RemoteTargetLocatorTest.php @@ -15,6 +15,8 @@ namespace Facebook\WebDriver; +use Facebook\WebDriver\Remote\RemoteWebElement; + /** * @covers \Facebook\WebDriver\Remote\RemoteTargetLocator */ @@ -52,4 +54,21 @@ public function testShouldSwitchToWindow() $this->assertContains('index.html', $this->driver->getCurrentURL()); $this->assertNotSame($originalWindowHandle, $this->driver->getWindowHandle()); } + + /** + * @cover ::activeElement + */ + public function testActiveElement() + { + $this->driver->get($this->getTestPageUrl('index.html')); + + $activeElement = $this->driver->switchTo()->activeElement(); + $this->assertInstanceOf(RemoteWebElement::class, $activeElement); + $this->assertSame('body', $activeElement->getTagName()); + + $this->driver->findElement(WebDriverBy::name('test_name'))->click(); + $activeElement = $this->driver->switchTo()->activeElement(); + $this->assertSame('input', $activeElement->getTagName()); + $this->assertSame('test_name', $activeElement->getAttribute('name')); + } } From 1dcec4bcda282ae4ea6a4e1314a1cd3c953974d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Machulda?= Date: Tue, 26 Nov 2019 17:12:40 +0100 Subject: [PATCH 038/350] Minor codestyle improvements and unifications --- .php_cs.dist | 10 +++++++++- phpstan.neon | 16 ++++++++-------- tests/functional/RemoteWebDriverCreateTest.php | 2 +- tests/functional/RemoteWebDriverTest.php | 2 +- tests/functional/WebDriverActionsTest.php | 2 +- tests/functional/WebDriverAlertTest.php | 2 +- tests/functional/WebDriverByTest.php | 7 +++++-- tests/functional/WebDriverCheckboxesTest.php | 14 +++++++------- tests/functional/WebDriverNavigationTest.php | 2 +- tests/functional/WebDriverRadiosTest.php | 14 +++++++------- tests/functional/WebDriverSelectTest.php | 9 ++++++--- tests/functional/WebDriverTimeoutsTest.php | 4 ++-- tests/unit/CookieTest.php | 4 ++-- tests/unit/Exception/WebDriverExceptionTest.php | 8 ++++---- .../WebDriverButtonReleaseActionTest.php | 4 ++-- .../Internal/WebDriverClickActionTest.php | 4 ++-- .../Internal/WebDriverClickAndHoldActionTest.php | 4 ++-- .../Internal/WebDriverContextClickActionTest.php | 4 ++-- .../Internal/WebDriverCoordinatesTest.php | 10 +++++----- .../Internal/WebDriverDoubleClickActionTest.php | 4 ++-- .../Internal/WebDriverKeyDownActionTest.php | 4 ++-- .../Internal/WebDriverKeyUpActionTest.php | 4 ++-- .../Internal/WebDriverMouseMoveActionTest.php | 4 ++-- .../WebDriverMouseToOffsetActionTest.php | 4 ++-- .../Internal/WebDriverSendKeysActionTest.php | 4 ++-- tests/unit/Remote/DesiredCapabilitiesTest.php | 6 +++--- tests/unit/Remote/HttpCommandExecutorTest.php | 6 +++--- tests/unit/Remote/RemoteWebDriverTest.php | 2 +- tests/unit/Support/XPathEscaperTest.php | 4 ++-- tests/unit/WebDriverOptionsTest.php | 2 +- 30 files changed, 90 insertions(+), 76 deletions(-) diff --git a/.php_cs.dist b/.php_cs.dist index 48aa29de7..b771f8b04 100644 --- a/.php_cs.dist +++ b/.php_cs.dist @@ -13,6 +13,8 @@ return PhpCsFixer\Config::create() 'concat_space' => ['spacing' => 'one'], 'function_typehint_space' => true, 'general_phpdoc_annotation_remove' => ['author'], + 'implode_call' => true, + 'is_null' => true, 'linebreak_after_opening_tag' => true, 'lowercase_cast' => true, 'mb_str_functions' => true, @@ -51,9 +53,15 @@ return PhpCsFixer\Config::create() 'ordered_imports' => true, 'php_unit_construct' => true, 'php_unit_dedicate_assert' => true, - 'php_unit_expectation' => true, + 'php_unit_expectation' => ['target' => '5.6'], + 'php_unit_method_casing' => ['case' => 'camel_case'], 'php_unit_mock' => true, + 'php_unit_mock_short_will_return' => true, + 'php_unit_namespaced' => ['target' => '5.7'], 'php_unit_no_expectation_annotation' => true, + 'php_unit_ordered_covers' => true, + 'php_unit_set_up_tear_down_visibility' => true, + 'php_unit_test_case_static_method_calls' => ['call_type' => 'this'], 'phpdoc_add_missing_param_annotation' => true, 'phpdoc_indent' => true, 'phpdoc_no_access' => true, diff --git a/phpstan.neon b/phpstan.neon index 7023c466e..3ea02aee5 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,10 +1,10 @@ parameters: - ignoreErrors: - - '#Class Symfony\\Component\\Process\\ProcessBuilder not found.#' - - '#Instantiated class Symfony\\Component\\Process\\ProcessBuilder not found.#' - - '#Call to method setPrefix\(\) on an unknown class Symfony\\Component\\Process\\ProcessBuilder#' + ignoreErrors: + - '#Class Symfony\\Component\\Process\\ProcessBuilder not found.#' + - '#Instantiated class Symfony\\Component\\Process\\ProcessBuilder not found.#' + - '#Call to method setPrefix\(\) on an unknown class Symfony\\Component\\Process\\ProcessBuilder#' # To be fixed: - - '#Call to an undefined method RecursiveIteratorIterator::getSubPathName\(\)#' - - '#Call to an undefined method Facebook\\WebDriver\\WebDriver::getTouch\(\)#' - - '#Call to an undefined method Facebook\\WebDriver\\WebDriverElement::getCoordinates\(\)#' - - '#Call to an undefined method Facebook\\WebDriver\\WebDriverElement::equals\(\)#' + - '#Call to an undefined method RecursiveIteratorIterator::getSubPathName\(\)#' + - '#Call to an undefined method Facebook\\WebDriver\\WebDriver::getTouch\(\)#' + - '#Call to an undefined method Facebook\\WebDriver\\WebDriverElement::getCoordinates\(\)#' + - '#Call to an undefined method Facebook\\WebDriver\\WebDriverElement::equals\(\)#' diff --git a/tests/functional/RemoteWebDriverCreateTest.php b/tests/functional/RemoteWebDriverCreateTest.php index a2f6e9d0f..5abec763b 100644 --- a/tests/functional/RemoteWebDriverCreateTest.php +++ b/tests/functional/RemoteWebDriverCreateTest.php @@ -20,8 +20,8 @@ use Facebook\WebDriver\Remote\RemoteWebDriver; /** - * @covers \Facebook\WebDriver\Remote\RemoteWebDriver * @covers \Facebook\WebDriver\Remote\HttpCommandExecutor + * @covers \Facebook\WebDriver\Remote\RemoteWebDriver */ class RemoteWebDriverCreateTest extends WebDriverTestCase { diff --git a/tests/functional/RemoteWebDriverTest.php b/tests/functional/RemoteWebDriverTest.php index 72fc30505..6f3becb0b 100644 --- a/tests/functional/RemoteWebDriverTest.php +++ b/tests/functional/RemoteWebDriverTest.php @@ -38,8 +38,8 @@ public function testShouldGetPageTitle() } /** - * @covers ::getCurrentURL * @covers ::get + * @covers ::getCurrentURL */ public function testShouldGetCurrentUrl() { diff --git a/tests/functional/WebDriverActionsTest.php b/tests/functional/WebDriverActionsTest.php index cb4359183..deb700575 100644 --- a/tests/functional/WebDriverActionsTest.php +++ b/tests/functional/WebDriverActionsTest.php @@ -61,8 +61,8 @@ public function testShouldClickOnElement() /** * @covers ::__construct * @covers ::clickAndHold - * @covers ::release * @covers ::perform + * @covers ::release */ public function testShouldClickAndHoldOnElementAndRelease() { diff --git a/tests/functional/WebDriverAlertTest.php b/tests/functional/WebDriverAlertTest.php index 72d38d82a..d6f5c91a2 100644 --- a/tests/functional/WebDriverAlertTest.php +++ b/tests/functional/WebDriverAlertTest.php @@ -20,8 +20,8 @@ use Facebook\WebDriver\Remote\WebDriverBrowserType; /** - * @covers \Facebook\WebDriver\WebDriverAlert * @covers \Facebook\WebDriver\Remote\RemoteTargetLocator + * @covers \Facebook\WebDriver\WebDriverAlert */ class WebDriverAlertTest extends WebDriverTestCase { diff --git a/tests/functional/WebDriverByTest.php b/tests/functional/WebDriverByTest.php index 028db7920..4ff114751 100644 --- a/tests/functional/WebDriverByTest.php +++ b/tests/functional/WebDriverByTest.php @@ -24,7 +24,7 @@ class WebDriverByTest extends WebDriverTestCase { /** - * @dataProvider textElementsProvider + * @dataProvider provideTextElements * @param string $webDriverByLocatorMethod * @param string $webDriverByLocatorValue * @param string $expectedText @@ -52,7 +52,10 @@ public function testShouldFindTextElementByLocator( } } - public function textElementsProvider() + /** + * @return array[] + */ + public function provideTextElements() { return [ 'id' => ['id', 'id_test', 'Test by ID'], diff --git a/tests/functional/WebDriverCheckboxesTest.php b/tests/functional/WebDriverCheckboxesTest.php index 91513bd94..0ddcccd0d 100644 --- a/tests/functional/WebDriverCheckboxesTest.php +++ b/tests/functional/WebDriverCheckboxesTest.php @@ -18,8 +18,8 @@ use Facebook\WebDriver\Exception\NoSuchElementException; /** - * @covers \Facebook\WebDriver\WebDriverCheckboxes * @covers \Facebook\WebDriver\AbstractWebDriverCheckboxOrRadio + * @covers \Facebook\WebDriver\WebDriverCheckboxes * @group exclude-edge */ class WebDriverCheckboxesTest extends WebDriverTestCase @@ -137,7 +137,7 @@ public function testSelectByIndexInvalid() } /** - * @dataProvider selectByVisibleTextDataProvider + * @dataProvider provideSelectByVisibleTextData * * @param string $text * @param string $value @@ -154,9 +154,9 @@ public function testSelectByVisibleText($text, $value) } /** - * @return array + * @return array[] */ - public function selectByVisibleTextDataProvider() + public function provideSelectByVisibleTextData() { return [ ['J 2 B', 'j2b'], @@ -165,7 +165,7 @@ public function selectByVisibleTextDataProvider() } /** - * @dataProvider selectByVisiblePartialTextDataProvider + * @dataProvider provideSelectByVisiblePartialTextData * * @param string $text * @param string $value @@ -182,9 +182,9 @@ public function testSelectByVisiblePartialText($text, $value) } /** - * @return array + * @return array[] */ - public function selectByVisiblePartialTextDataProvider() + public function provideSelectByVisiblePartialTextData() { return [ ['2 B', 'j2b'], diff --git a/tests/functional/WebDriverNavigationTest.php b/tests/functional/WebDriverNavigationTest.php index d3aaebcb6..c2286f3b2 100644 --- a/tests/functional/WebDriverNavigationTest.php +++ b/tests/functional/WebDriverNavigationTest.php @@ -21,8 +21,8 @@ class WebDriverNavigationTest extends WebDriverTestCase { /** - * @covers ::to * @covers ::__construct + * @covers ::to */ public function testShouldNavigateToUrl() { diff --git a/tests/functional/WebDriverRadiosTest.php b/tests/functional/WebDriverRadiosTest.php index cf7b8378c..bb0eaa41e 100644 --- a/tests/functional/WebDriverRadiosTest.php +++ b/tests/functional/WebDriverRadiosTest.php @@ -19,8 +19,8 @@ use Facebook\WebDriver\Exception\UnsupportedOperationException; /** - * @covers \Facebook\WebDriver\WebDriverRadios * @covers \Facebook\WebDriver\AbstractWebDriverCheckboxOrRadio + * @covers \Facebook\WebDriver\WebDriverRadios * @group exclude-edge */ class WebDriverRadiosTest extends WebDriverTestCase @@ -115,7 +115,7 @@ public function testSelectByIndexInvalid() } /** - * @dataProvider selectByVisibleTextDataProvider + * @dataProvider provideSelectByVisibleTextData * * @param string $text * @param string $value @@ -128,9 +128,9 @@ public function testSelectByVisibleText($text, $value) } /** - * @return array + * @return array[] */ - public function selectByVisibleTextDataProvider() + public function provideSelectByVisibleTextData() { return [ ['J 3 B', 'j3b'], @@ -139,7 +139,7 @@ public function selectByVisibleTextDataProvider() } /** - * @dataProvider selectByVisiblePartialTextDataProvider + * @dataProvider provideSelectByVisiblePartialTextData * * @param string $text * @param string $value @@ -152,9 +152,9 @@ public function testSelectByVisiblePartialText($text, $value) } /** - * @return array + * @return array[] */ - public function selectByVisiblePartialTextDataProvider() + public function provideSelectByVisiblePartialTextData() { return [ ['3 B', 'j3b'], diff --git a/tests/functional/WebDriverSelectTest.php b/tests/functional/WebDriverSelectTest.php index b25dd03af..8d96a1944 100644 --- a/tests/functional/WebDriverSelectTest.php +++ b/tests/functional/WebDriverSelectTest.php @@ -21,8 +21,8 @@ /** * @group exclude-saucelabs - * @covers \Facebook\WebDriver\WebDriverSelect * @covers \Facebook\WebDriver\Exception\UnexpectedTagNameException + * @covers \Facebook\WebDriver\WebDriverSelect */ class WebDriverSelectTest extends WebDriverTestCase { @@ -58,7 +58,7 @@ public function testShouldThrowExceptionWhenNotInstantiatedOnSelectElement() } /** - * @dataProvider selectSelectorProvider + * @dataProvider provideSelectSelector * @param string $selector */ public function testShouldGetOptionsOfSelect($selector) @@ -72,7 +72,10 @@ public function testShouldGetOptionsOfSelect($selector) $this->assertCount(5, $options); } - public function selectSelectorProvider() + /** + * @return array[] + */ + public function provideSelectSelector() { return [ 'simple + +
+