diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f0d81c2f..36fcecc2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,14 +9,11 @@ env: jobs: Mink: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 strategy: matrix: - selenium: [ '3.141.59' ] - php: [ '7.2', '7.3', '7.4', '8.0', '8.1' ] - include: - - selenium: '2.53.1' - php: 'latest' + selenium: [ '4.16.1' ] + php: [ '7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3' ] fail-fast: false steps: diff --git a/README.md b/README.md index 5d4012b6..a180ef7b 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,41 @@ -WebDriver for Selenium 2 -======================== -This WebDriver client implementation is based on Meta/Facebook's original [php-webdriver](https://github.com/instaclick/php-webdriver/tree/upstream) -project by Justin Bishop. Meta/Facebook's current [php-webdriver](https://github.com/php-webdriver/php-webdriver) is a complete rewrite. - -Distinguishing features: -* Up-to-date with [WebDriver: W3C Editor's Draft 25 Octoberl 2022](https://w3c.github.io/webdriver/) -* Up-to-date with [Selenium 2 JSON Wire Protocol](https://github.com/SeleniumHQ/selenium/blob/trunk/java/src/org/openqa/selenium/remote/DriverCommand.java) (including WebDriver commands yet to be documented). -* In the *master* branch, class names and file organization follow PSR-0 conventions for namespaces. +# W3C WebDriver Client + +This "classic" W3C WebDriver client implementation is based on the +[php-webdriver](https://github.com/instaclick/php-webdriver/tree/upstream) +project by Justin Bishop. Originally conceived as a thin wrapper around the +JSON Wire Protocol, the client has been refactored to work with the W3C +WebDriver Protocol, with some fallback/emulation for older drivers. We'll +continue to track changes to the specs but there are no immediate plans to add +WebDriver-BiDi support. + +If you are starting a new project (using PHP 7.3 or above), you should +consider using Meta/Facebook's completely rewritten (and more actively +maintained) +[php-webdriver](https://github.com/php-webdriver/php-webdriver). + +### Distinguishing features: + +* Up-to-date with: + * [WebDriver: W3C Working Draft 14 November 2023](https://www.w3.org/TR/webdriver2) + * [Federated Credential Management API: Draft Community Group Report, 1 December 2023](https://fedidcg.github.io/FedCM/) + * [Web Authentication: An API for accessing Public Key Credentials, Level 2: W3C Recommendation, 8 April 2021](https://www.w3.org/TR/webauthn-2/) +* In the *master* branch, class names and file organization follow PSR-0 + conventions for namespaces. * Coding style follows PSR-1, PSR-2, and Symfony2 conventions. -* Auto-generate API documentation via [phpDocumentor 2.x](http://phpdoc.org/). [![Latest Stable Version](https://poser.pugx.org/instaclick/php-webdriver/v/stable.png)](https://packagist.org/packages/instaclick/php-webdriver) [![Total Downloads](https://poser.pugx.org/instaclick/php-webdriver/downloads.png)](https://packagist.org/packages/instaclick/php-webdriver) -Links -===== +## Links + * [Packagist](http://packagist.org/packages/instaclick/php-webdriver) * [Github](https://github.com/instaclick/php-webdriver) * [W3C/WebDriver](https://github.com/w3c/webdriver) -Notes -===== -* The *5.2.x* branch is no longer maintained. This branch features class names and file re-organization that follow PEAR/ZF1 conventions. Bug fixes and enhancements from the master branch likely won't be backported. +## Notes + +* The *1.x* branch is up-to-date with the legacy + [Selenium 2 JSON Wire Protocol](https://www.selenium.dev/documentation/legacy/json_wire_protocol/). +* The *5.2.x* branch is no longer maintained. This branch features class + names and file re-organization that follow PEAR/ZF1 conventions. Bug fixes + and enhancements from the master branch likely won't be backported. diff --git a/composer.json b/composer.json index 6edf268e..ff6d2a8e 100644 --- a/composer.json +++ b/composer.json @@ -6,7 +6,9 @@ "selenium", "webdriver", "webtest", - "browser" + "browser", + "test", + "automation" ], "homepage": "http://instaclick.com/", "license": "Apache-2.0", @@ -23,11 +25,11 @@ } ], "require": { - "php": ">=5.3.2", + "php": ">=7.2", "ext-curl": "*" }, "require-dev": { - "php": ">=7.1", + "php": ">=7.2", "phpunit/phpunit": "^8.5 || ^9.5" }, "minimum-stability": "dev", diff --git a/lib/WebDriver/AbstractWebDriver.php b/lib/WebDriver/AbstractWebDriver.php index 55dce988..cd4910de 100644 --- a/lib/WebDriver/AbstractWebDriver.php +++ b/lib/WebDriver/AbstractWebDriver.php @@ -42,36 +42,45 @@ abstract class AbstractWebDriver private $transientOptions; /** - * Return array of supported method names and corresponding HTTP request methods + * @var array + */ + private $extensions; + + /** + * Return array of protocol methods * * @return array */ - abstract protected function methods(); + protected function methods() + { + return []; + } /** - * Return array of obsolete method names and corresponding HTTP request methods + * Return array of chainable method/property names * * @return array */ - protected function obsoleteMethods() + protected function chainable() { - return array(); + return []; } /** * Constructor * - * @param string $url URL to Selenium server + * @param string $url */ - public function __construct($url = 'http://localhost:4444/wd/hub') + public function __construct($url) { $this->url = $url; - $this->transientOptions = array(); + $this->transientOptions = []; + $this->extensions = []; $this->curlService = ServiceFactory::getInstance()->getService('service.curl'); } /** - * Magic method which returns URL to Selenium server + * Return URL * * @return string */ @@ -81,7 +90,7 @@ public function __toString() } /** - * Returns URL to Selenium server + * Return URL * * @return string */ @@ -117,7 +126,7 @@ public function getCurlService() */ public function setTransientOptions($transientOptions) { - $this->transientOptions = is_array($transientOptions) ? $transientOptions : array(); + $this->transientOptions = is_array($transientOptions) ? $transientOptions : []; } /** @@ -128,12 +137,100 @@ public function getTransientOptions() return $this->transientOptions; } + /** + * Register extension + * + * @param string $extension + * @param string $className + * @param string $path + */ + public function register($extension, $className, $path) + { + if (class_exists($className, false)) { + $this->extensions[$extension] = [$className, $path]; + } + } + + /** + * Magic method that maps calls to class methods to execute WebDriver commands + * + * @param string $name Method name + * @param array $arguments Arguments + * + * @return mixed + * + * @throws \WebDriver\Exception if invalid WebDriver command + */ + public function __call($name, $arguments) + { + if (count($arguments) > 1) { + throw WebDriverException::factory( + WebDriverException::_JSON_PARAMETERS_EXPECTED, + 'Commands should have at most only one parameter, which should be the JSON Parameter object' + ); + } + + if (count($arguments) === 0 && is_array($this->extensions) && array_key_exists($name, $this->extensions)) { + $className = $this->extensions[$name][0]; + + return new $className($this->url . '/' . $this->extensions[$name][1]); + } + + if (count($arguments) === 0 && array_key_exists($name, $this->chainable())) { + return call_user_func([$this, $name]); + } + + if (preg_match('/^(get|post|delete)/', $name, $matches)) { + $requestMethod = strtoupper($matches[0]); + $webdriverCommand = strtolower(substr($name, strlen($requestMethod))); + + $this->getRequestMethod($webdriverCommand); // validation + } else { + $webdriverCommand = $name; + $requestMethod = $this->getRequestMethod($webdriverCommand); + } + + $methods = $this->methods(); + + if (! in_array($requestMethod, $methods[$webdriverCommand])) { + throw WebDriverException::factory( + WebDriverException::_INVALID_REQUEST, + sprintf( + '%s is not an available http request method for the command %s.', + $requestMethod, + $webdriverCommand + ) + ); + } + + $parameters = array_shift($arguments); + $result = $this->curl($requestMethod, '/' . $webdriverCommand, $parameters); + + return $result['value']; + } + + /** + * Magic method that maps property names to chainable methods + * + * @param string $name Property name + * + * @return mixed + */ + public function __get($name) + { + if (array_key_exists($name, $this->chainable())) { + return call_user_func([$this, $name]); + } + + trigger_error('Undefined property: ' . __CLASS__ . '::$' . $name, E_USER_WARNING); + } + /** * Curl request to webdriver server. * * @param string $requestMethod HTTP request method, e.g., 'GET', 'POST', or 'DELETE' * @param string $command If not defined in methods() this function will throw. - * @param array|integer|string $parameters If an array(), they will be posted as JSON parameters + * @param array|integer|string $parameters If an array, they will be posted as JSON parameters * If a number or string, "/$params" is appended to url * @param array $extraOptions key=>value pairs of curl options to pass to curl_setopt() * @@ -141,11 +238,11 @@ public function getTransientOptions() * * @throws \WebDriver\Exception if error */ - protected function curl($requestMethod, $command, $parameters = null, $extraOptions = array()) + protected function curl($requestMethod, $command, $parameters = null, $extraOptions = []) { if ($parameters && is_array($parameters) && $requestMethod !== 'POST') { throw WebDriverException::factory( - WebDriverException::NO_PARAMETERS_EXPECTED, + WebDriverException::_NO_PARAMETERS_EXPECTED, sprintf( 'The http request method called for %s is %s but it has to be POST if you want to pass the JSON parameters %s', $command, @@ -155,9 +252,11 @@ protected function curl($requestMethod, $command, $parameters = null, $extraOpti ); } - $url = sprintf('%s%s', $this->url, $command); + $url = $this->url . $command; - if ($parameters && (is_int($parameters) || is_string($parameters))) { + if (($requestMethod === 'GET' || $requestMethod === 'DELETE') + && $parameters && (is_int($parameters) || is_string($parameters)) + ) { $url .= '/' . $parameters; } @@ -170,13 +269,13 @@ protected function curl($requestMethod, $command, $parameters = null, $extraOpti array_replace($extraOptions, $this->transientOptions) ); - $this->transientOptions = array(); + $this->transientOptions = []; $httpCode = $info['http_code']; if ($httpCode === 0) { throw WebDriverException::factory( - WebDriverException::CURL_EXEC, + WebDriverException::_CURL_EXEC, $info['error'] ); } @@ -187,20 +286,20 @@ protected function curl($requestMethod, $command, $parameters = null, $extraOpti // Legacy webdriver 4xx responses are to be considered a plaintext error if ($httpCode >= 400 && $httpCode <= 499) { throw WebDriverException::factory( - WebDriverException::CURL_EXEC, + WebDriverException::_CURL_EXEC, 'Webdriver http error: ' . $httpCode . ', payload :' . substr($rawResult, 0, 1000) ); } throw WebDriverException::factory( - WebDriverException::CURL_EXEC, + WebDriverException::_CURL_EXEC, 'Payload received from webdriver is not valid json: ' . substr($rawResult, 0, 1000) ); } if (is_array($result) && ! array_key_exists('status', $result) && ! array_key_exists('value', $result)) { throw WebDriverException::factory( - WebDriverException::CURL_EXEC, + WebDriverException::_CURL_EXEC, 'Payload received from webdriver is valid but unexpected json: ' . substr($rawResult, 0, 1000) ); } @@ -234,63 +333,12 @@ protected function curl($requestMethod, $command, $parameters = null, $extraOpti ?: $this->offsetGet('sessionId', $value) ?: $this->offsetGet('webdriver.remote.sessionid', $value); - return array( + return [ 'value' => $value, 'info' => $info, 'sessionId' => $sessionId, 'sessionUrl' => $sessionId ? $this->url . '/session/' . $sessionId : $info['url'], - ); - } - - /** - * Magic method that maps calls to class methods to execute WebDriver commands - * - * @param string $name Method name - * @param array $arguments Arguments - * - * @return mixed - * - * @throws \WebDriver\Exception if invalid WebDriver command - */ - public function __call($name, $arguments) - { - if (count($arguments) > 1) { - throw WebDriverException::factory( - WebDriverException::JSON_PARAMETERS_EXPECTED, - 'Commands should have at most only one parameter, which should be the JSON Parameter object' - ); - } - - if (preg_match('/^(get|post|delete)/', $name, $matches)) { - $requestMethod = strtoupper($matches[0]); - $webdriverCommand = strtolower(substr($name, strlen($requestMethod))); - - $this->getRequestMethod($webdriverCommand); // validation - } else { - $webdriverCommand = $name; - $requestMethod = $this->getRequestMethod($webdriverCommand); - } - - $methods = $this->methods(); - - if (! in_array($requestMethod, (array) $methods[$webdriverCommand])) { - throw WebDriverException::factory( - WebDriverException::INVALID_REQUEST, - sprintf( - '%s is not an available http request method for the command %s.', - $requestMethod, - $webdriverCommand - ) - ); - } - - $result = $this->curl( - $requestMethod, - '/' . $webdriverCommand, - array_shift($arguments) - ); - - return $result['value']; + ]; } /** @@ -313,7 +361,7 @@ private function assertSerializable($parameters) } throw WebDriverException::factory( - WebDriverException::UNEXPECTED_PARAMETERS, + WebDriverException::_UNEXPECTED_PARAMETERS, sprintf( "Unable to serialize non-scalar type %s", is_object($parameters) ? get_class($parameters) : gettype($parameters) @@ -347,14 +395,13 @@ private function getRequestMethod($webdriverCommand) { if (! array_key_exists($webdriverCommand, $this->methods())) { throw WebDriverException::factory( - array_key_exists($webdriverCommand, $this->obsoleteMethods()) - ? WebDriverException::OBSOLETE_COMMAND : WebDriverException::UNKNOWN_COMMAND, + WebDriverException::UNKNOWN_COMMAND, sprintf('%s is not a valid WebDriver command.', $webdriverCommand) ); } $methods = $this->methods(); - $requestMethods = (array) $methods[$webdriverCommand]; + $requestMethods = $methods[$webdriverCommand]; return array_shift($requestMethods); } diff --git a/lib/WebDriver/Actions.php b/lib/WebDriver/Actions.php new file mode 100644 index 00000000..d7e8cbf0 --- /dev/null +++ b/lib/WebDriver/Actions.php @@ -0,0 +1,192 @@ + + */ + +namespace WebDriver; + +/** + * WebDriver\Actions class + * + * @package WebDriver + */ +class Actions extends AbstractWebDriver +{ + /** + * singleton + * + * @var \WebDriver\Actions + */ + private static $instance; + + /** + * @var array + */ + private $inputSources = [ + NullInput::TYPE => [], + KeyInput::TYPE => [], + PointerInput::TYPE => [], + WheelInput::TYPE => [], + ]; + + /** + * @var array + */ + private $actions; + + /** + * {@inheritDoc} + */ + private function __construct($url) + { + parent::__construct($url); + + $this->clearAllActions(); + } + + /** + * Get singleton instance + * + * @param string $url + * + * @return \WebDriver\Actions + */ + public static function getInstance($url) + { + if (self::$instance === null) { + self::$instance = new self($url); + } + + return self::$instance; + } + + /** + * Get Null Input Source + * + * @return \WebDriver\NullInput + */ + public function getNullInput($id = 0) + { + if (! array_key_exists($id, $this->inputSources[NullInput::TYPE])) { + $inputSource = new NullInput($id); + + $this->inputSources[NullInput::TYPE][$id] = $inputSource; + } + + return $this->inputSources[NullInput::TYPE][$id]; + } + + /** + * Get Key Input Source + * + * @return \WebDriver\KeyInput + */ + public function getKeyInput($id = 0) + { + if (! array_key_exists($id, $this->inputSources[KeyInput::TYPE])) { + $inputSource = new KeyInput($id); + + $this->inputSources[KeyInput::TYPE][$id] = $inputSource; + } + + return $this->inputSources[KeyInput::TYPE][$id]; + } + + /** + * Get Pointer Input Source + * + * @return \WebDriver\PointerInput + */ + public function getPointerInput($id = 0, $subType = PointerInput::MOUSE) + { + if (! array_key_exists($id, $this->inputSources[PointerInput::TYPE])) { + $inputSource = new PointerInput($id, $subType); + + $this->inputSources[PointerInput::TYPE][$id] = $inputSource; + } + + return $this->inputSources[PointerInput::TYPE][$id]; + } + + /** + * Get Wheel Input Source + * + * @return \WebDriver\WheelInput + */ + public function getWheelInput($id = 0) + { + if (! array_key_exists($id, $this->inputSources[WheelInput::TYPE])) { + $inputSource = new WheelInput($id); + + $this->inputSources[WheelInput::TYPE][$id] = $inputSource; + } + + return $this->inputSources[WheelInput::TYPE][$id]; + } + + /** + * Perform actions: /session/:sessionId/actions (POST) + * + * @return mixed + */ + public function perform() + { + $actions = $this->actions; + $parameters = ['actions' => $actions]; + + $this->clearAllActions(); + + $result = $this->curl('POST', '', $parameters); + + return $result['value']; + } + + /** + * Release all action state: /session/:sessionId/actions (DELETE) + * + * @return mixed + */ + public function releaseActions() + { + $result = $this->curl('DELETE', ''); + + return $result['value']; + } + + /** + * Clear all actions from the builder + */ + public function clearAllActions() + { + $this->actions = []; + } + + /** + * Add action + * + * @param array $action + * + * @return \WebDriver\Actions + */ + public function addAction($action) + { + if (($last = count($this->actions)) && + $this->actions[$last - 1]['id'] === $action['id'] && + $this->actions[$last - 1]['type'] === $action['type'] + ) { + foreach ($action['actions'] as $item) { + $this->actions[$last - 1]['actions'][] = $item; + } + } else { + $this->actions[] = $action; + } + + return $this; + } +} diff --git a/lib/WebDriver/Alert.php b/lib/WebDriver/Alert.php index 3e446d4b..2989483e 100644 --- a/lib/WebDriver/Alert.php +++ b/lib/WebDriver/Alert.php @@ -16,10 +16,13 @@ * * @package WebDriver * + * W3C * @method array accept() Accept alert. * @method array dismiss() Dismiss alert. * @method array getText() Get alert text. - * @method array postText($json) Send alert text. + * @method array postText($parameters) Set alert value. + * Selenium + * @method array credentials($parameters) Set alert credentials. */ class Alert extends AbstractWebDriver { @@ -28,10 +31,78 @@ class Alert extends AbstractWebDriver */ protected function methods() { - return array( - 'accept' => array('POST'), - 'dismiss' => array('POST'), - 'text' => array('GET', 'POST'), - ); + return [ + 'accept' => ['POST'], + 'dismiss' => ['POST'], + 'text' => ['GET', 'POST'], + + // @deprecated + 'credentials' => ['POST'], + ]; + } + + /** + * {@inheritdoc} + */ + protected function aliases() + { + return [ + // @deprecated + 'setAlertCredentials' => 'credentials', + ]; + } + + /** + * Accept alert: /session/:sessionId/alert/accept (POST) + * + * @return mixed + */ + public function acceptAlert() + { + $result = $this->curl('POST', '/accept'); + + return $result['value']; + } + + /** + * Dismiss alert: /session/:sessionId/alert/dismiss (POST) + * + * @return mixed + */ + public function dismissAlert() + { + $result = $this->curl('POST', '/dismiss'); + + return $result['value']; + } + + /** + * Get alert text: /session/:sessionId/alert/text (GET) + * + * @return mixed + */ + public function getAlertText() + { + $result = $this->curl('GET', '/text'); + + return $result['value']; + } + + /** + * Set alert value: /session/:sessionId/alert/text (POST) + * + * @param array|string $text Parameters {text: ...} + * + * @return mixed + */ + public function setAlertValue($text) + { + $parameters = is_array($text) + ? $text + : ['text' => $text]; + + $result = $this->curl('POST', '/text', $parameters); + + return $result['value']; } } diff --git a/lib/WebDriver/AppCacheStatus.php b/lib/WebDriver/AppCacheStatus.php index 879eac82..b306cfbc 100644 --- a/lib/WebDriver/AppCacheStatus.php +++ b/lib/WebDriver/AppCacheStatus.php @@ -16,7 +16,7 @@ * * @package WebDriver * - * @deprecated by W3C WebDriver + * @deprecated */ final class AppCacheStatus { diff --git a/lib/WebDriver/ApplicationCache.php b/lib/WebDriver/ApplicationCache.php index c64ac3aa..a70a885b 100644 --- a/lib/WebDriver/ApplicationCache.php +++ b/lib/WebDriver/ApplicationCache.php @@ -16,6 +16,8 @@ * * @package WebDriver * + * @deprecated + * * @method integer status() Get application cache status. */ class ApplicationCache extends AbstractWebDriver @@ -26,6 +28,7 @@ class ApplicationCache extends AbstractWebDriver protected function methods() { return array( + // @deprecated 'status' => array('GET'), ); } diff --git a/lib/WebDriver/Browser.php b/lib/WebDriver/Browser.php index 7e497774..ed51a34e 100644 --- a/lib/WebDriver/Browser.php +++ b/lib/WebDriver/Browser.php @@ -19,22 +19,14 @@ final class Browser { /** - * Check browser names used in static functions in the selenium source: - * @see https://github.com/SeleniumHQ/selenium/blob/trunk/java/src/org/openqa/selenium/remote/BrowserType.java + * @see https://github.com/SeleniumHQ/selenium/blob/trunk/java/src/org/openqa/selenium/remote/Browser.java */ - const ANDROID = 'android'; - const CHROME = 'chrome'; - const EDGE = 'edge'; - const EDGEHTML = 'EdgeHTML'; - const FIREFOX = 'firefox'; - const HTMLUNIT = 'htmlunit'; - const IE = 'internet explorer'; - const INTERNET_EXPLORER = 'internet explorer'; - const IPHONE = 'iPhone'; - const IPAD = 'iPad'; - const MSEDGE = 'MicrosoftEdge'; - const OPERA = 'opera'; - const OPERA_BLINK = 'operablink'; - const PHANTOMJS = 'phantomjs'; - const SAFARI = 'safari'; + const CHROME = 'chrome'; + const EDGE = 'MicrosoftEdge'; + const FIREFOX = 'firefox'; + const HTMLUNIT = 'htmlunit'; + const IE = 'internet explorer'; + const OPERA = 'opera'; + const SAFARI = 'safari'; + const SAFARI_TECH_PREVIEW = 'Safari Technology Preview'; } diff --git a/lib/WebDriver/Capability.php b/lib/WebDriver/Capability.php index 4b3cec9f..17ac65a2 100644 --- a/lib/WebDriver/Capability.php +++ b/lib/WebDriver/Capability.php @@ -24,45 +24,16 @@ final class Capability * @see https://w3c.github.io/webdriver/webdriver-spec.html#capabilities * @see https://github.com/SeleniumHQ/selenium/blob/trunk/java/src/org/openqa/selenium/remote/CapabilityType.java */ + const ACCEPT_INSECURE_CERTS = 'acceptInsecureCerts'; const BROWSER_NAME = 'browserName'; const BROWSER_VERSION = 'browserVersion'; + const ENABLE_DOWNLOADS = 'enableDownloads'; const PLATFORM_NAME = 'platformName'; const PLATFORM_VERSION = 'platformVersion'; - const ACCEPT_SSL_CERTS = 'acceptSslCerts'; const PAGE_LOAD_STRATEGY = 'pageLoadStrategy'; const PROXY = 'proxy'; - const TIMEOUTS = 'timeouts'; - - // legacy JSON Wire Protocol - const VERSION = 'version'; - const PLATFORM = 'platform'; - const JAVASCRIPT_ENABLED = 'javascriptEnabled'; - const TAKES_SCREENSHOT = 'takesScreenshot'; - const HANDLES_ALERTS = 'handlesAlerts'; - const DATABASE_ENABLED = 'databaseEnabled'; - const LOCATION_CONTEXT_ENABLED = 'locationContextEnabled'; - const APPLICATION_CACHE_ENABLED = 'applicationCacheEnabled'; - const BROWSER_CONNECTION_ENABLED = 'browserConnectionEnabled'; - const CSS_SELECTORS_ENABLED = 'cssSelectorsEnabled'; - const WEB_STORAGE_ENABLED = 'webStorageEnabled'; - const ROTATABLE = 'rotatable'; - const NATIVE_EVENTS = 'nativeEvents'; - const UNEXPECTED_ALERT_BEHAVIOUR = 'unexpectedAlertBehaviour'; - const ELEMENT_SCROLL_BEHAVIOR = 'elementScrollBehavior'; + const SET_WINDOW_RECT = 'setWindowRect'; const STRICT_FILE_INTERACTABILITY = 'strictFileInteractability'; + const TIMEOUTS = 'timeouts'; const UNHANDLED_PROMPT_BEHAVIOR = 'unhandlePromptBehavior'; - - /** - * Proxy types - * - * @see https://w3c.github.io/webdriver/webdriver-spec.html#proxy - */ - const AUTODETECT = 'autodetect'; - const MANUAL = 'manual'; - const NO_PROXY = 'noproxy'; - const PAC = 'pac'; - const SYSTEM = 'system'; - - // legacy JSON Wire Protocol - const DIRECT = 'direct'; } diff --git a/lib/WebDriver/Container.php b/lib/WebDriver/Container.php index 5739efdb..4f11c292 100644 --- a/lib/WebDriver/Container.php +++ b/lib/WebDriver/Container.php @@ -49,23 +49,19 @@ public function __construct($url) * * @throws \WebDriver\Exception if element not found, or invalid XPath */ - public function element($using = null, $value = null) + public function findElement($using = null, $value = null) { - $locatorJson = $this->parseArgs('element', func_get_args()); + $parameters = $this->parseArgs('element', func_get_args()); try { - $result = $this->curl( - 'POST', - '/element', - $locatorJson - ); + $result = $this->curl('POST', '/element', $parameters); } catch (WebDriverException\NoSuchElement $e) { throw WebDriverException::factory( WebDriverException::NO_SUCH_ELEMENT, sprintf( "Element not found with %s, %s\n\n%s", - $locatorJson['using'], - $locatorJson['value'], + $parameters['using'], + $parameters['value'], $e->getMessage() ), $e @@ -79,8 +75,8 @@ public function element($using = null, $value = null) WebDriverException::NO_SUCH_ELEMENT, sprintf( "Element not found with %s, %s\n", - $locatorJson['using'], - $locatorJson['value'] + $parameters['using'], + $parameters['value'] ) ); } @@ -100,69 +96,24 @@ public function element($using = null, $value = null) * * @throws \WebDriver\Exception if invalid XPath */ - public function elements($using = null, $value = null) + public function findElements($using = null, $value = null) { - $locatorJson = $this->parseArgs('elements', func_get_args()); + $parameters = $this->parseArgs('elements', func_get_args()); - $result = $this->curl( - 'POST', - '/elements', - $locatorJson - ); + $result = $this->curl('POST', '/elements', $parameters); if (! is_array($result['value'])) { - return array(); + return []; } return array_filter( array_map( - array($this, 'makeElement'), + [$this, 'makeElement'], $result['value'] ) ); } - /** - * Parse arguments allowing either separate $using and $value parameters, or - * as an array containing the JSON parameters - * - * @param string $method method name - * @param array $argv arguments - * - * @return array - * - * @throws \WebDriver\Exception if invalid number of arguments to the called method - */ - private function parseArgs($method, $argv) - { - $argc = count($argv); - - switch ($argc) { - case 2: - $using = $argv[0]; - $value = $argv[1]; - break; - - case 1: - $arg = $argv[0]; - - if (is_array($arg)) { - $using = $arg['using']; - $value = $arg['value']; - break; - } - - // fall through - default: - throw WebDriverException::factory( - WebDriverException::JSON_PARAMETERS_EXPECTED, - sprintf('Invalid arguments to %s method: %s', $method, print_r($argv, true)) - ); - } - - return $this->locate($using, $value); - } - /** * Return JSON parameter for element / elements command * @@ -177,15 +128,38 @@ public function locate($using, $value) { if (! in_array($using, $this->strategies)) { throw WebDriverException::factory( - WebDriverException::UNKNOWN_LOCATOR_STRATEGY, + WebDriverException::_UNKNOWN_LOCATOR_STRATEGY, sprintf('Invalid locator strategy %s', $using) ); } - return array( + return [ 'using' => $using, 'value' => $value, - ); + ]; + } + + /** + * {@inheritdoc} + */ + public function __call($name, $arguments) + { + // define the aliases here because superclasses may define their own aliases + $aliases = [ + 'element' => 'findElement', + 'elements' => 'findElements', + ]; + + if (array_key_exists($name, $aliases)) { + return call_user_func_array([$this, $aliases[$name]], $arguments); + } + + if (count($arguments) === 1 && in_array(str_replace('_', ' ', $name), $this->strategies)) { + return $this->locate($name, $arguments[0]); + } + + // fallback to executing WebDriver commands + return parent::__call($name, $arguments); } /** @@ -197,40 +171,19 @@ public function locate($using, $value) */ protected function makeElement($value) { - if (array_key_exists(LegacyElement::LEGACY_ELEMENT_ID, (array) $value)) { - $identifier = $value[LegacyElement::LEGACY_ELEMENT_ID]; - - return new LegacyElement( - $this->getIdentifierPath($identifier), - $identifier - ); + if (! is_array($value)) { + $value = [$value]; } if (array_key_exists(Element::WEB_ELEMENT_ID, (array) $value)) { $identifier = $value[Element::WEB_ELEMENT_ID]; - return new Element( - $this->getIdentifierPath($identifier), - $identifier - ); + return new Element($this->getNewIdentifierPath($identifier), $identifier); } return null; } - /** - * {@inheritdoc} - */ - public function __call($name, $arguments) - { - if (count($arguments) === 1 && in_array(str_replace('_', ' ', $name), $this->strategies)) { - return $this->locate($name, $arguments[0]); - } - - // fallback to executing WebDriver commands - return parent::__call($name, $arguments); - } - /** * Get wire protocol URL for an identifier * @@ -238,5 +191,46 @@ public function __call($name, $arguments) * * @return string */ - abstract protected function getIdentifierPath($identifier); + abstract protected function getNewIdentifierPath($identifier); + + /** + * Parse arguments allowing either separate $using and $value parameters, or + * as an array containing the JSON parameters + * + * @param string $method method name + * @param array $argv arguments + * + * @return array + * + * @throws \WebDriver\Exception if invalid number of arguments to the called method + */ + private function parseArgs($method, $argv) + { + $argc = count($argv); + + switch ($argc) { + case 2: + $using = $argv[0]; + $value = $argv[1]; + break; + + case 1: + $arg = $argv[0]; + + if (is_array($arg)) { + $using = $arg['using']; + $value = $arg['value']; + break; + } + + // fall through + default: + throw WebDriverException::factory( + WebDriverException::_JSON_PARAMETERS_EXPECTED, + sprintf('Invalid arguments to %s method: %s', $method, print_r($argv, true)) + ); + } + + return $this->locate($using, $value); + } } diff --git a/lib/WebDriver/Element.php b/lib/WebDriver/Element.php index d572834d..4706dcb2 100644 --- a/lib/WebDriver/Element.php +++ b/lib/WebDriver/Element.php @@ -16,21 +16,25 @@ * * @package WebDriver * + * W3C + * @method string attribute($attributeName) Get the value of an element's attribute. * @method void clear() Clear a TEXTAREA or text INPUT element's value. * @method void click() Click on an element. - * @method boolean displayed() Determine if an element is currently displayed. + * @method array computedlabel() Get ARIA Role. + * @method array computedrole() Get Accessible Name. + * @method string css($propertyName) Query the value of an element's computed CSS property. * @method boolean enabled() Determine if an element is currently enabled. - * @method boolean equals($otherId) Test if two element IDs refer to the same DOM element. - * @method array location() Determine an element's location on the page. - * @method array location_in_view() Determine an element's location on the screen once it has been scrolled into view. * @method string name() Query for an element's tag name. + * @method array property($propertyName) Get element property. * @method array rect() Get element rect. * @method array screenshot() Take element screenshot. - * @method array selected() Is element selected? - * @method array size() Determine an element's size in pixels. - * @method void submit() Submit a FORM element. + * @method boolean selected() Is element selected? * @method string text() Returns the visible text for the element. - * @method void postValue($json) Send a sequence of key strokes to an element. + * @method void value($parameters) Send a sequence of key strokes to an element. + * Selenium + * @method boolean equals($otherId) Test if two element IDs refer to the same DOM element. + * @method array location() Determine an element's location on the page. + * @method array size() Determine an element's size in pixels. */ class Element extends Container { @@ -48,42 +52,48 @@ class Element extends Container */ protected function methods() { - return array( - 'clear' => array('POST'), - 'click' => array('POST'), - 'enabled' => array('GET'), - 'name' => array('GET'), - 'rect' => array('GET'), - 'screenshot' => array('GET'), - 'selected' => array('GET'), - 'text' => array('GET'), - 'value' => array('POST'), - - // Legacy JSON Wire Protocol - 'displayed' => array('GET'), // @see https://w3c.github.io/webdriver/#element-displayedness - 'equals' => array('GET'), - 'location' => array('GET'), - 'location_in_view' => array('GET'), - 'size' => array('GET'), - 'submit' => array('POST'), - ); + return [ + 'attribute' => ['GET'], + 'clear' => ['POST'], + 'click' => ['POST'], + 'computedlabel' => ['GET'], + 'computedrole' => ['GET'], + 'css' => ['GET'], + 'enabled' => ['GET'], + 'name' => ['GET'], + 'property' => ['GET'], + 'rect' => ['GET'], + 'screenshot' => ['GET'], + 'selected' => ['GET'], + 'text' => ['GET'], + 'value' => ['POST'], + + // @deprecated + 'equals' => ['GET'], + ]; } /** * {@inheritdoc} */ - protected function obsoleteMethods() + protected function chainable() { - return array( - 'active' => array('GET'), - 'computedlabel' => array('GET'), - 'computedrole' => array('GET'), - 'drag' => array('POST'), - 'hover' => array('POST'), - 'selected' => array('POST'), - 'toggle' => array('POST'), - 'value' => array('GET'), - ); + return [ + 'shadow' => 'getShadowRoot', + ]; + } + + /** + * {@inheritdoc} + */ + protected function aliases() + { + return [ + // @deprecated + 'location' => 'rect', + 'size' => 'rect', + 'postValue' => 'sendKeys', + ]; } /** @@ -94,7 +104,7 @@ protected function obsoleteMethods() */ public function __construct($url, $id) { - parent::__construct($url); + parent::__construct($url . "/$id"); $this->id = $id; } @@ -110,78 +120,153 @@ public function getID() } /** - * Get the value of an element's attribute: /session/:sessionId/element/:id/attribute/:name + * Get Accessible Name: /session/:sessionId/element/:elementId/computedlabel * - * @param string name + * @return mixed + */ + public function getAccessibleName() + { + $result = $this->curl('GET', '/computedlabel'); + + return $result['value']; + } + + /** + * Get ARIA Role: /session/:sessionId/element/:elementId/computedrole * * @return mixed */ - public function attribute($name) + public function getAriaRole() { - $result = $this->curl('GET', "/attribute/$name"); + $result = $this->curl('GET', '/computedrole'); return $result['value']; } /** - * Query the value of an element’s computed CSS property: /session/:sessionId/element/:id/css/:propertyName + * Get element shadow root: /session/:sessionId/element/:elementId/shadow + * + * shadow root method chaining, e.g., + * - $element->shadow()->method() + * - $element->shadow->method() + * + * @return \WebDriver\Shadow|null + * + */ + public function getShadowRoot() + { + $result = $this->curl('POST', '/shadow'); + $value = $result['value']; + + if (! is_array($value)) { + $value = [$value]; + } + + if (array_key_exists(Shadow::SHADOW_ROOT_ID, $value)) { + $shadowRootReference = $value[Shadow::SHADOW_ROOT_ID]; + + return new Shadow($this->getSessionPath() . '/shadow', $shadowRootReference); + } + + return null; + } + + /** + * Is Element Enabled: /session/:sessionId/element/:elementId/enabled * - * @param string $propertyName + * @return mixed + */ + public function isEnabled() + { + $result = $this->curl('GET', '/enabled'); + + return $result['value']; + } + + /** + * Is Element Selected: /session/:sessionId/element/:elementId/selected * * @return mixed */ - public function css($propertyName) + public function isSelected() { - $result = $this->curl('GET', "/css/$propertyName"); + $result = $this->curl('GET', '/selected'); return $result['value']; } /** - * Get element property: /session/:sessionId/element/:id/property/:name + * Send Keys to Element: /session/:sessionId/element/:elementId/value * - * @param string $name + * @param array|string $text Parameters {text: ...} * * @return mixed */ - public function property($name) + public function sendKeys($text) { - $result = $this->curl('GET', "/property/$name"); + $parameters = is_array($text) + ? $text + : ['text' => $text, 'value' => [$text]]; + + if (! array_key_exists('text', $parameters) && array_key_exists('value', $parameters)) { + // trigger_error(__METHOD__ . ': use "text" property instead of "value"', E_USER_DEPRECATED); + + $parameters['text'] = implode($parameters['value']); + } + + if (array_key_exists('text', $parameters) && ! array_key_exists('value', $parameters)) { + $parameters['value'] = [$parameters['text']]; + } + + $result = $this->curl('POST', '/value', $parameters); return $result['value']; } /** - * Get element shadow root: /session/:sessionId/element/:elementId/shadow + * Submit a FORM Element: /session/:sessionId/element/:elementId/submit * - * shadow root method chaining, e.g., - * - $element->method() - * - * @return \WebDriver\Shadow|null + * @deprecated * + * @return mixed */ - public function shadow() + public function submit() { - $result = $this->curl('POST', '/shadow'); - $value = $result['value']; + // trigger_error(__METHOD__, E_USER_DEPRECATED); - if (array_key_exists(Shadow::SHADOW_ROOT_ID, (array) $value)) { - $shadowRootReference = $value[Shadow::SHADOW_ROOT_ID]; + try { + $result = $this->curl('POST', '/submit'); + + return $result['value']; + } catch (\Exception $e) { + $session = new Session($this->getSessionPath(), []); - return new Shadow( - preg_replace('/' . preg_quote('element/' . $this->id, '/') . '$/', '/', $this->url), // remove /element/:elementid - $shadowRootReference + return $session->execute()->sync( + ['script' => << [$this]] ); } - - return null; } /** * {@inheritdoc} */ - protected function getIdentifierPath($identifier) + protected function getNewIdentifierPath($identifier) + { + return preg_replace('~/' . preg_quote($this->id) . '$~', "/$identifier", $this->url); + } + + /** + * Get session path + * + * @return string + */ + private function getSessionPath() { - return preg_replace('/' . preg_quote($this->id) . '$/', $identifier, $this->url); + // remove /element/:elementid + return preg_replace('~/element/' . preg_quote($this->id) . '$~', '', $this->url); } } diff --git a/lib/WebDriver/Exception.php b/lib/WebDriver/Exception.php index edee0d3a..0de022f0 100644 --- a/lib/WebDriver/Exception.php +++ b/lib/WebDriver/Exception.php @@ -11,6 +11,8 @@ namespace WebDriver; +use WebDriver\Exception as E; + /** * WebDriver\Exception class * @@ -19,182 +21,175 @@ abstract class Exception extends \Exception { /** - * Response status codes + * Error codes * - * @see https://github.com/SeleniumHQ/selenium/blob/trunk/java/src/org/openqa/selenium/remote/ErrorCodes.java + * @see https://www.w3.org/TR/webdriver2/#errors */ - const SUCCESS = 0; - const NO_SUCH_DRIVER = 6; - const NO_SUCH_ELEMENT = 7; - const NO_SUCH_FRAME = 8; - const UNKNOWN_COMMAND = 9; - const STALE_ELEMENT_REFERENCE = 10; - const INVALID_ELEMENT_STATE = 12; - const UNKNOWN_ERROR = 13; - const JAVASCRIPT_ERROR = 17; - const XPATH_LOOKUP_ERROR = 19; - const TIMEOUT = 21; - const NO_SUCH_WINDOW = 23; - const INVALID_COOKIE_DOMAIN = 24; - const UNABLE_TO_SET_COOKIE = 25; - const UNEXPECTED_ALERT_OPEN = 26; - const NO_ALERT_OPEN_ERROR = 27; - const SCRIPT_TIMEOUT = 28; - const INVALID_ELEMENT_COORDINATES = 29; - const IME_NOT_AVAILABLE = 30; - const IME_ENGINE_ACTIVATION_FAILED = 31; - const INVALID_SELECTOR = 32; - const SESSION_NOT_CREATED = 33; - const MOVE_TARGET_OUT_OF_BOUNDS = 34; - const INVALID_XPATH_SELECTOR = 51; - const INVALID_XPATH_SELECTOR_RETURN_TYPER = 52; - const ELEMENT_NOT_INTERACTABLE = 60; - const INVALID_ARGUMENT = 61; - const NO_SUCH_COOKIE = 62; - const UNABLE_TO_CAPTURE_SCREEN = 63; - const ELEMENT_CLICK_INTERCEPTED = 64; - const NO_SUCH_SHADOW_ROOT = 65; - const METHOD_NOT_ALLOWED = 405; - - // obsolete - const INDEX_OUT_OF_BOUNDS = 1; - const NO_COLLECTION = 2; - const NO_STRING = 3; - const NO_STRING_LENGTH = 4; - const NO_STRING_WRAPPER = 5; - const OBSOLETE_ELEMENT = 10; - const ELEMENT_NOT_DISPLAYED = 11; - const ELEMENT_NOT_VISIBLE = 11; - const UNHANDLED = 13; - const EXPECTED = 14; - const ELEMENT_IS_NOT_SELECTABLE = 15; - const ELEMENT_NOT_SELECTABLE = 15; - const NO_SUCH_DOCUMENT = 16; - const UNEXPECTED_JAVASCRIPT = 17; - const NO_SCRIPT_RESULT = 18; - const NO_SUCH_COLLECTION = 20; - const NULL_POINTER = 22; - const NO_MODAL_DIALOG_OPEN_ERROR = 27; - - // user-defined - const CURL_EXEC = -1; - const OBSOLETE_COMMAND = -2; - const NO_PARAMETERS_EXPECTED = -3; - const JSON_PARAMETERS_EXPECTED = -4; - const UNEXPECTED_PARAMETERS = -5; - const INVALID_REQUEST = -6; - const UNKNOWN_LOCATOR_STRATEGY = -7; - const W3C_WEBDRIVER_ERROR = -8; - - private static $errs = array( -// self::SUCCESS => array('Success', 'This should never be thrown!'), - - self::NO_SUCH_DRIVER => array('NoSuchDriver', 'A session is either terminated or not started'), - self::NO_SUCH_ELEMENT => array('NoSuchElement', 'An element could not be located on the page using the given search parameters.'), - self::NO_SUCH_FRAME => array('NoSuchFrame', 'A request to switch to a frame could not be satisfied because the frame could not be found.'), - self::UNKNOWN_COMMAND => array('UnknownCommand', 'The requested resource could not be found, or a request was received using an HTTP method that is not supported by the mapped resource.'), - self::STALE_ELEMENT_REFERENCE => array('StaleElementReference', 'An element command failed because the referenced element is no longer attached to the DOM.'), - self::ELEMENT_NOT_VISIBLE => array('ElementNotVisible', 'An element command could not be completed because the element is not visible on the page.'), - self::INVALID_ELEMENT_STATE => array('InvalidElementState', 'An element command could not be completed because the element is in an invalid state (e.g., attempting to click a disabled element).'), - self::UNKNOWN_ERROR => array('UnknownError', 'An unknown server-side error occurred while processing the command.'), - self::ELEMENT_IS_NOT_SELECTABLE => array('ElementIsNotSelectable', 'An attempt was made to select an element that cannot be selected.'), - self::JAVASCRIPT_ERROR => array('JavaScriptError', 'An error occurred while executing user supplied JavaScript.'), - self::XPATH_LOOKUP_ERROR => array('XPathLookupError', 'An error occurred while searching for an element by XPath.'), - self::TIMEOUT => array('Timeout', 'An operation did not complete before its timeout expired.'), - self::NO_SUCH_WINDOW => array('NoSuchWindow', 'A request to switch to a different window could not be satisfied because the window could not be found.'), - self::INVALID_COOKIE_DOMAIN => array('InvalidCookieDomain', 'An illegal attempt was made to set a cookie under a different domain than the current page.'), - self::UNABLE_TO_SET_COOKIE => array('UnableToSetCookie', 'A request to set a cookie\'s value could not be satisfied.'), - self::UNEXPECTED_ALERT_OPEN => array('UnexpectedAlertOpen', 'A modal dialog was open, blocking this operation'), - self::NO_ALERT_OPEN_ERROR => array('NoAlertOpenError', 'An attempt was made to operate on a modal dialog when one was not open.'), - self::SCRIPT_TIMEOUT => array('ScriptTimeout', 'A script did not complete before its timeout expired.'), - self::INVALID_ELEMENT_COORDINATES => array('InvalidElementCoordinates', 'The coordinates provided to an interactions operation are invalid.'), - self::IME_NOT_AVAILABLE => array('IMENotAvailable', 'IME was not available.'), - self::IME_ENGINE_ACTIVATION_FAILED => array('IMEEngineActivationFailed', 'An IME engine could not be started.'), - self::INVALID_SELECTOR => array('InvalidSelector', 'Argument was an invalid selector (e.g., XPath/CSS).'), - self::SESSION_NOT_CREATED => array('SessionNotCreated', 'A new session could not be created (e.g., a required capability could not be set).'), - self::MOVE_TARGET_OUT_OF_BOUNDS => array('MoveTargetOutOfBounds', 'Target provided for a move action is out of bounds.'), + const DETACHED_SHADOW_ROOT = 'detached shadow roomt'; + const ELEMENT_CLICK_INTERCEPTED = 'element click intercepted'; + const ELEMENT_NOT_INTERACTABLE = 'element not interactable'; + const INSECURE_CERTIFICATE = 'insecure certificate'; + const INVALID_ARGUMENT = 'invalid argument'; + const INVALID_COOKIE_DOMAIN = 'invalid cookie domain'; + const INVALID_ELEMENT_STATE = 'invalid element state'; + const INVALID_SELECTOR = 'invalid selector'; + const INVALID_SESSION_ID = 'invalid session id'; + const JAVASCRIPT_ERROR = 'javascript error'; + const MOVE_TARGET_OUT_OF_BOUNDS = 'move target out of bounds'; + const NO_SUCH_ALERT = 'no such alert'; + const NO_SUCH_COOKIE = 'no such cookie'; + const NO_SUCH_ELEMENT = 'no such element'; + const NO_SUCH_FRAME = 'no such frame'; + const NO_SUCH_SHADOW_ROOT = 'no such shadow root'; + const NO_SUCH_WINDOW = 'no such window'; + const SCRIPT_TIMEOUT = 'script timeout'; + const SESSION_NOT_CREATED = 'session not created'; + const STALE_ELEMENT_REFERENCE = 'stale element reference'; + const TIMEOUT = 'timeout'; + const UNABLE_TO_CAPTURE_SCREEN = 'unable to capture screen'; + const UNABLE_TO_SET_COOKIE = 'unable to set cookie'; + const UNEXPECTED_ALERT_OPEN = 'unexpected alert open'; + const UNKNOWN_COMMAND = 'unknown command'; + const UNKNOWN_ERROR = 'unknown error'; + const UNKNOWN_METHOD = 'unknown method'; + const UNSUPPORTED_OPERATION = 'unsupported operation'; + + // @internal php-webdriver + const _CURL_EXEC = 'curl exec'; + const _INVALID_REQUEST = 'invalid request'; + const _JSON_PARAMETERS_EXPECTED = 'json parameters expected'; + const _NO_PARAMETERS_EXPECTED = 'no parameters expected'; + const _OBSOLETE_COMMAND = 'obsolete command'; + const _UNEXPECTED_PARAMETERS = 'unexpected parameters'; + const _UNKNOWN_LOCATOR_STRATEGY = 'unknown locator strategy'; - self::CURL_EXEC => array('CurlExec', 'curl_exec() error.'), - self::OBSOLETE_COMMAND => array('ObsoleteCommand', 'This WebDriver command is obsolete.'), - self::NO_PARAMETERS_EXPECTED => array('NoParametersExpected', 'This HTTP request method expects no parameters.'), - self::JSON_PARAMETERS_EXPECTED => array('JsonParameterExpected', 'This POST request expects a JSON parameter (array).'), - self::UNEXPECTED_PARAMETERS => array('UnexpectedParameters', 'This command does not expect this number of parameters.'), - self::INVALID_REQUEST => array('InvalidRequest', 'This command does not support this HTTP request method.'), - self::UNKNOWN_LOCATOR_STRATEGY => array('UnknownLocatorStrategy', 'This locator strategy is not supported.'), - self::INVALID_XPATH_SELECTOR => array('InvalidSelector', 'Argument was an invalid selector.'), - self::INVALID_XPATH_SELECTOR_RETURN_TYPER => array('InvalidSelector', 'Argument was an invalid selector.'), - self::ELEMENT_NOT_INTERACTABLE => array('ElementNotInteractable', 'A command could not be completed because the element is not pointer- or keyboard interactable.'), - self::INVALID_ARGUMENT => array('InvalidArgument', 'The arguments passed to a command are either invalid or malformed.'), - self::NO_SUCH_COOKIE => array('NoSuchCookie', 'No cookie matching the given path name was found amongst the associated cookies of the current browsing context\'s active document.'), - self::UNABLE_TO_CAPTURE_SCREEN => array('UnableToCaptureScreen', 'A screen capture was made impossible.'), - self::ELEMENT_CLICK_INTERCEPTED => array('ElementClickIntercepted', 'The Element Click command could not be completed because the element receiving the events is obscuring the element that was requested clicked.'), - self::NO_SUCH_SHADOW_ROOT => array('NoSuchShadowRoot', 'The element does not have a shadow root.'), - self::METHOD_NOT_ALLOWED => array('UnsupportedOperation', 'Indicates that a command that should have executed properly cannot be supported for some reason.'), - - // @ss https://w3c.github.io/webdriver/#errors - 'element not interactable' => array('ElementNotInteractable', 'A command could not be completed because the element is not pointer- or keyboard interactable.'), - 'element not selectable' => array('ElementIsNotSelectable', 'An attempt was made to select an element that cannot be selected.'), - 'insecure certificate' => array('InsecureCertificate', 'Navigation caused the user agent to hit a certificate warning, which is usually the result of an expired or invalid TLS certificate.'), - 'invalid argument' => array('InvalidArgument', 'The arguments passed to a command are either invalid or malformed.'), - 'invalid cookie domain' => array('InvalidCookieDomain', 'An illegal attempt was made to set a cookie under a different domain than the current page.'), - 'invalid coordinates' => array('InvalidCoordinates', 'The coordinates provided to an interactions operation are invalid.'), - 'invalid element state' => array('InvalidElementState', 'A command could not be completed because the element is in an invalid state, e.g. attempting to clear an element that isn\'t both editable and resettable.'), - 'invalid selector' => array('InvalidSelector', 'Argument was an invalid selector.'), - 'invalid session id' => array('InvalidSessionID', 'Occurs if the given session id is not in the list of active sessions, meaning the session either does not exist or that it\'s not active.'), - 'javascript error' => array('JavaScriptError', 'An error occurred while executing JavaScript supplied by the user.'), - 'move target out of bounds' => array('MoveTargetOutOfBounds', 'The target for mouse interaction is not in the browser\'s viewport and cannot be brought into that viewport.'), - 'no such alert' => array('NoSuchAlert', 'An attempt was made to operate on a modal dialog when one was not open.'), - 'no such cookie' => array('NoSuchCookie', 'No cookie matching the given path name was found amongst the associated cookies of the current browsing context\'s active document.'), - 'no such element' => array('NoSuchElement', 'An element could not be located on the page using the given search parameters.'), - 'no such frame' => array('NoSuchFrame', 'A command to switch to a frame could not be satisfied because the frame could not be found.'), - 'no such window' => array('NoSuchWindow', 'A command to switch to a window could not be satisfied because the window could not be found.'), - 'script timeout' => array('ScriptTimeout', 'A script did not complete before its timeout expired.'), - 'session not created' => array('SessionNotCreated', 'A new session could not be created.'), - 'stale element reference' => array('StaleElementReference', 'A command failed because the referenced element is no longer attached to the DOM.'), - 'timeout' => array('Timeout', 'An operation did not complete before its timeout expired.'), - 'unable to capture screen' => array('UnableToCaptureScreen', 'A screen capture was made impossible.'), - 'unable to set cookie' => array('UnableToSetCookie', 'A command to set a cookie\'s value could not be satisfied.'), - 'unexpected alert open' => array('UnexpectedAlertOpen', 'A modal dialog was open, blocking this operation.'), - 'unknown command' => array('UnknownCommand', 'A command could not be executed because the remote end is not aware of it.'), - 'unknown error' => array('UnknownError', 'An unknown error occurred in the remote end while processing the command.'), - 'unknown method' => array('UnknownMethod', 'The requested command matched a known URL but did not match an method for that URL.'), - 'unsupported operation' => array('UnsupportedOperation', 'Indicates that a command that should have executed properly cannot be supported for some reason.'), + /** + * Mapping JSON Wire Protocol (integer) error codes to W3C WebDriver (string) status codes + * + * @deprecated + * + * @see https://github.com/SeleniumHQ/selenium/blob/trunk/java/src/org/openqa/selenium/remote/ErrorCodes.java + * + * @var array + */ + private static $mapping = [ + 1 => 'index out of bounds', + 2 => 'no collection', + 3 => 'no string', + 4 => 'no string length', + 5 => 'no string wrapper', + 6 => self::SESSION_NOT_CREATED, // 'no such driver' + 7 => self::NO_SUCH_ELEMENT, + 8 => self::NO_SUCH_FRAME, + 9 => self::UNSUPPORTED_OPERATION, + 10 => self::STALE_ELEMENT_REFERENCE, // 'obsolete element' + 11 => 'element not displayed', + 12 => self::INVALID_ELEMENT_STATE, + 13 => self::UNKNOWN_ERROR, + 14 => 'expected', + 15 => self::ELEMENT_NOT_INTERACTABLE, // 'element is not selectable' + 16 => 'no such document', + 17 => self::JAVASCRIPT_ERROR, + 18 => 'no script result', + 19 => self::INVALID_SELECTOR, // 'xpath lookup error' + 20 => 'no such collection', + 21 => self::TIMEOUT, + 22 => 'null pointer', + 23 => self::NO_SUCH_WINDOW, + 24 => self::INVALID_COOKIE_DOMAIN, + 25 => self::UNABLE_TO_SET_COOKIE, + 26 => self::UNEXPECTED_ALERT_OPEN, + 27 => self::NO_SUCH_ALERT, // 'no modal dialog open error' + 28 => self::SCRIPT_TIMEOUT, + 29 => 'invalid element coordinates', + 30 => 'ime not available', + 31 => 'ime engine activation failed', + 32 => self::INVALID_SELECTOR, + 33 => self::SESSION_NOT_CREATED, + 34 => self::MOVE_TARGET_OUT_OF_BOUNDS, + 35 => 'sql database error', + 51 => self::INVALID_SELECTOR, // 'invalid xpath selector' + 52 => self::INVALID_SELECTOR, // 'invalid xpath selector return typer' + 60 => self::ELEMENT_NOT_INTERACTABLE, + 61 => self::INVALID_ARGUMENT, + 62 => self::NO_SUCH_COOKIE, + 63 => self::UNABLE_TO_CAPTURE_SCREEN, + 64 => self::ELEMENT_CLICK_INTERCEPTED, + 65 => self::NO_SUCH_SHADOW_ROOT, + 405 => self::UNSUPPORTED_OPERATION, // 'method not allowed' + ]; - // obsolete - 'detached shadow root' => array('DetachedShadowRoot', 'A command failed because the referenced shadow root is no longer attached to the DOM.'), - 'element click intercepted' => array('ElementClickIntercepted', 'The Element Click command could not be completed because the element receiving the events is obscuring the element that was requested clicked.'), - 'no such shadow root' => array('NoSuchShadowRoot', 'The element does not have a shadow root.'), - 'script timeout error' => array('ScriptTimeout', 'A script did not complete before its timeout expired.'), - ); + /** + * Error data dictionary + * + * @var array + */ + private static $errs = [ + self::DETACHED_SHADOW_ROOT => [E\DetachedShadowRoot::class, 'A command failed because the referenced shadow root is no longer attached to the DOM.'], + self::ELEMENT_CLICK_INTERCEPTED => [E\ElementClickIntercepted::class, 'The Element Click command could not be completed because the element receiving the events is obscuring the element that was requested clicked.'], + self::ELEMENT_NOT_INTERACTABLE => [E\ElementNotInteractable::class, 'A command could not be completed because the element is not pointer- or keyboard interactable.'], + self::INSECURE_CERTIFICATE => [E\InsecureCertificate::class, 'Navigation caused the user agent to hit a certificate warning, which is usually the result of an expired or invalid TLS certificate.'], + self::INVALID_ARGUMENT => [E\InvalidArgument::class, 'The arguments passed to a command are either invalid or malformed.'], + self::INVALID_COOKIE_DOMAIN => [E\InvalidCookieDomain::class, 'An illegal attempt was made to set a cookie under a different domain than the current page.'], + self::INVALID_ELEMENT_STATE => [E\InvalidElementState::class, 'A command could not be completed because the element is in an invalid state, e.g. attempting to clear an element that isn\'t both editable and resettable.'], + self::INVALID_SELECTOR => [E\InvalidSelector::class, 'Argument was an invalid selector.'], + self::INVALID_SESSION_ID => [E\InvalidSessionID::class, 'Occurs if the given session id is not in the list of active sessions, meaning the session either does not exist or that it\'s not active.'], + self::JAVASCRIPT_ERROR => [E\JavaScriptError::class, 'An error occurred while executing JavaScript supplied by the user.'], + self::MOVE_TARGET_OUT_OF_BOUNDS => [E\MoveTargetOutOfBounds::class, 'The target for mouse interaction is not in the browser\'s viewport and cannot be brought into that viewport.'], + self::NO_SUCH_ALERT => [E\NoSuchAlert::class, 'An attempt was made to operate on a modal dialog when one was not open.'], + self::NO_SUCH_COOKIE => [E\NoSuchCookie::class, 'No cookie matching the given path name was found amongst the associated cookies of the current browsing context\'s active document.'], + self::NO_SUCH_ELEMENT => [E\NoSuchElement::class, 'An element could not be located on the page using the given search parameters.'], + self::NO_SUCH_FRAME => [E\NoSuchFrame::class, 'A command to switch to a frame could not be satisfied because the frame could not be found.'], + self::NO_SUCH_SHADOW_ROOT => [E\NoSuchShadowRoot::class, 'The element does not have a shadow root.'], + self::NO_SUCH_WINDOW => [E\NoSuchWindow::class, 'A command to switch to a window could not be satisfied because the window could not be found.'], + self::SCRIPT_TIMEOUT => [E\ScriptTimeout::class, 'A script did not complete before its timeout expired.'], + self::SESSION_NOT_CREATED => [E\SessionNotCreated::class, 'A new session could not be created.'], + self::STALE_ELEMENT_REFERENCE => [E\StaleElementReference::class, 'A command failed because the referenced element is no longer attached to the DOM.'], + self::TIMEOUT => [E\Timeout::class, 'An operation did not complete before its timeout expired.'], + self::UNABLE_TO_CAPTURE_SCREEN => [E\UnableToCaptureScreen::class, 'A screen capture was made impossible.'], + self::UNABLE_TO_SET_COOKIE => [E\UnableToSetCookie::class, 'A command to set a cookie\'s value could not be satisfied.'], + self::UNEXPECTED_ALERT_OPEN => [E\UnexpectedAlertOpen::class, 'A modal dialog was open, blocking this operation.'], + self::UNKNOWN_COMMAND => [E\UnknownCommand::class, 'A command could not be executed because the remote end is not aware of it.'], + self::UNKNOWN_ERROR => [E\UnknownError::class, 'An unknown error occurred in the remote end while processing the command.'], + self::UNKNOWN_METHOD => [E\UnknownMethod::class, 'The requested command matched a known URL but did not match an method for that URL.'], + self::UNSUPPORTED_OPERATION => [E\UnsupportedOperation::class, 'Indicates that a command that should have executed properly cannot be supported for some reason.'], + + // @internal php-webdriver (user-defined) + self::_CURL_EXEC => [E\CurlExec::class, 'curl_exec() error.'], + self::_INVALID_REQUEST => [E\InvalidRequest::class, 'This command does not support this HTTP request method.'], + self::_JSON_PARAMETERS_EXPECTED => [E\JsonParameterExpected::class, 'This POST request expects a JSON parameter (array).'], + self::_NO_PARAMETERS_EXPECTED => [E\NoParametersExpected::class, 'This HTTP request method expects no parameters.'], + self::_OBSOLETE_COMMAND => [E\ObsoleteCommand::class, 'This WebDriver command is obsolete.'], + self::_UNEXPECTED_PARAMETERS => [E\UnexpectedParameters::class, 'This command does not expect this number of parameters.'], + self::_UNKNOWN_LOCATOR_STRATEGY => [E\UnknownLocatorStrategy::class, 'This locator strategy is not supported.'], + ]; /** * Factory method to create WebDriver\Exception objects * - * @param integer $code Code - * @param string $message Message - * @param \Exception $previousException Previous exception + * @param string|integer $code Status (or Error) Code + * @param string $message Message + * @param \Exception $previousException Previous exception * * @return \Exception */ public static function factory($code, $message = null, $previousException = null) { - // unknown error - if (! isset(self::$errs[$code])) { + if (is_numeric($code) && array_key_exists($code, self::$mapping)) { + $code = self::$mapping[$code]; + } + + if (! array_key_exists($code, self::$errs)) { $code = self::UNKNOWN_ERROR; } $errorDefinition = self::$errs[$code]; + $className = $errorDefinition[0]; if ($message === null || trim($message) === '') { $message = $errorDefinition[1]; } - if (! is_numeric($code)) { - $code = self::W3C_WEBDRIVER_ERROR; - } - - $className = __CLASS__ . '\\' . $errorDefinition[0]; - - return new $className($message, $code, $previousException); + return new $className($message, 0, $previousException); } } diff --git a/lib/WebDriver/Exception/CurlExec.php b/lib/WebDriver/Exception/CurlExec.php index bd9a5301..af95319e 100644 --- a/lib/WebDriver/Exception/CurlExec.php +++ b/lib/WebDriver/Exception/CurlExec.php @@ -16,6 +16,8 @@ /** * WebDriver\Exception\CurlExec class * + * @internal php-webdriver + * * @package WebDriver */ final class CurlExec extends BaseException @@ -23,7 +25,7 @@ final class CurlExec extends BaseException /** * @var array */ - private $curlInfo = array(); + private $curlInfo = []; /** * Get curl info diff --git a/lib/WebDriver/Exception/ElementIsNotSelectable.php b/lib/WebDriver/Exception/ElementIsNotSelectable.php deleted file mode 100644 index cadd219d..00000000 --- a/lib/WebDriver/Exception/ElementIsNotSelectable.php +++ /dev/null @@ -1,23 +0,0 @@ - - */ - -namespace WebDriver\Exception; - -use WebDriver\Exception as BaseException; - -/** - * WebDriver\Exception\ElementIsNotSelectable class - * - * @package WebDriver - */ -final class ElementIsNotSelectable extends BaseException -{ -} diff --git a/lib/WebDriver/Exception/ElementNotVisible.php b/lib/WebDriver/Exception/ElementNotVisible.php deleted file mode 100644 index ca3e64d5..00000000 --- a/lib/WebDriver/Exception/ElementNotVisible.php +++ /dev/null @@ -1,23 +0,0 @@ - - */ - -namespace WebDriver\Exception; - -use WebDriver\Exception as BaseException; - -/** - * WebDriver\Exception\ElementNotVisible class - * - * @package WebDriver - */ -final class ElementNotVisible extends BaseException -{ -} diff --git a/lib/WebDriver/Exception/IMEEngineActivationFailed.php b/lib/WebDriver/Exception/IMEEngineActivationFailed.php deleted file mode 100644 index 24f9d7ee..00000000 --- a/lib/WebDriver/Exception/IMEEngineActivationFailed.php +++ /dev/null @@ -1,23 +0,0 @@ - - */ - -namespace WebDriver\Exception; - -use WebDriver\Exception as BaseException; - -/** - * WebDriver\Exception\IMEEngineActivationFailed class - * - * @package WebDriver - */ -final class IMEEngineActivationFailed extends BaseException -{ -} diff --git a/lib/WebDriver/Exception/IMENotAvailable.php b/lib/WebDriver/Exception/IMENotAvailable.php deleted file mode 100644 index 4b249b63..00000000 --- a/lib/WebDriver/Exception/IMENotAvailable.php +++ /dev/null @@ -1,23 +0,0 @@ - - */ - -namespace WebDriver\Exception; - -use WebDriver\Exception as BaseException; - -/** - * WebDriver\Exception\IMENotAvailable class - * - * @package WebDriver - */ -final class IMENotAvailable extends BaseException -{ -} diff --git a/lib/WebDriver/Exception/InvalidCoordinates.php b/lib/WebDriver/Exception/InvalidCoordinates.php deleted file mode 100644 index a3ae5b2e..00000000 --- a/lib/WebDriver/Exception/InvalidCoordinates.php +++ /dev/null @@ -1,23 +0,0 @@ - - */ - -namespace WebDriver\Exception; - -use WebDriver\Exception as BaseException; - -/** - * WebDriver\Exception\InvalidCoordinates class - * - * @package WebDriver - */ -final class InvalidCoordinates extends BaseException -{ -} diff --git a/lib/WebDriver/Exception/InvalidElementCoordinates.php b/lib/WebDriver/Exception/InvalidElementCoordinates.php deleted file mode 100644 index 2748098a..00000000 --- a/lib/WebDriver/Exception/InvalidElementCoordinates.php +++ /dev/null @@ -1,23 +0,0 @@ - - */ - -namespace WebDriver\Exception; - -use WebDriver\Exception as BaseException; - -/** - * WebDriver\Exception\InvalidElementCoordinates class - * - * @package WebDriver - */ -final class InvalidElementCoordinates extends BaseException -{ -} diff --git a/lib/WebDriver/Exception/InvalidRequest.php b/lib/WebDriver/Exception/InvalidRequest.php index 63793613..673abbe2 100644 --- a/lib/WebDriver/Exception/InvalidRequest.php +++ b/lib/WebDriver/Exception/InvalidRequest.php @@ -16,6 +16,8 @@ /** * WebDriver\Exception\InvalidRequest class * + * @internal php-webdriver + * * @package WebDriver */ final class InvalidRequest extends BaseException diff --git a/lib/WebDriver/Exception/JsonParameterExpected.php b/lib/WebDriver/Exception/JsonParameterExpected.php index 9bb8a0c5..9d4001b5 100644 --- a/lib/WebDriver/Exception/JsonParameterExpected.php +++ b/lib/WebDriver/Exception/JsonParameterExpected.php @@ -16,6 +16,8 @@ /** * WebDriver\Exception\JsonParameterExpected class * + * @internal php-webdriver + * * @package WebDriver */ final class JsonParameterExpected extends BaseException diff --git a/lib/WebDriver/Exception/NoAlertOpenError.php b/lib/WebDriver/Exception/NoAlertOpenError.php deleted file mode 100644 index 9a2d8a66..00000000 --- a/lib/WebDriver/Exception/NoAlertOpenError.php +++ /dev/null @@ -1,23 +0,0 @@ - - */ - -namespace WebDriver\Exception; - -use WebDriver\Exception as BaseException; - -/** - * WebDriver\Exception\NoAlertOpenError class - * - * @package WebDriver - */ -final class NoAlertOpenError extends BaseException -{ -} diff --git a/lib/WebDriver/Exception/NoParametersExpected.php b/lib/WebDriver/Exception/NoParametersExpected.php index 6fb50da3..69016e1c 100644 --- a/lib/WebDriver/Exception/NoParametersExpected.php +++ b/lib/WebDriver/Exception/NoParametersExpected.php @@ -16,6 +16,8 @@ /** * WebDriver\Exception\NoParametersExpected class * + * @internal php-webdriver + * * @package WebDriver */ final class NoParametersExpected extends BaseException diff --git a/lib/WebDriver/Exception/NoSuchDriver.php b/lib/WebDriver/Exception/NoSuchDriver.php deleted file mode 100644 index 8b8a5ff9..00000000 --- a/lib/WebDriver/Exception/NoSuchDriver.php +++ /dev/null @@ -1,23 +0,0 @@ - - */ - -namespace WebDriver\Exception; - -use WebDriver\Exception as BaseException; - -/** - * WebDriver\Exception\NoSuchDriver class - * - * @package WebDriver - */ -final class NoSuchDriver extends BaseException -{ -} diff --git a/lib/WebDriver/Exception/ObsoleteCommand.php b/lib/WebDriver/Exception/ObsoleteCommand.php index b125f74f..1e0be8f4 100644 --- a/lib/WebDriver/Exception/ObsoleteCommand.php +++ b/lib/WebDriver/Exception/ObsoleteCommand.php @@ -16,6 +16,8 @@ /** * WebDriver\Exception\ObsoleteCommand class * + * @internal php-webdriver + * * @package WebDriver */ final class ObsoleteCommand extends BaseException diff --git a/lib/WebDriver/Exception/UnexpectedParameters.php b/lib/WebDriver/Exception/UnexpectedParameters.php index 6d3f4c65..56faad2b 100644 --- a/lib/WebDriver/Exception/UnexpectedParameters.php +++ b/lib/WebDriver/Exception/UnexpectedParameters.php @@ -16,6 +16,8 @@ /** * WebDriver\Exception\UnexpectedParameters class * + * @internal php-webdriver + * * @package WebDriver */ final class UnexpectedParameters extends BaseException diff --git a/lib/WebDriver/Exception/UnknownLocatorStrategy.php b/lib/WebDriver/Exception/UnknownLocatorStrategy.php index ddff5ba9..75a770ca 100644 --- a/lib/WebDriver/Exception/UnknownLocatorStrategy.php +++ b/lib/WebDriver/Exception/UnknownLocatorStrategy.php @@ -16,6 +16,8 @@ /** * WebDriver\Exception\UnknownLocatorStrategy class * + * @internal php-webdriver + * * @package WebDriver */ final class UnknownLocatorStrategy extends BaseException diff --git a/lib/WebDriver/Exception/XPathLookupError.php b/lib/WebDriver/Exception/XPathLookupError.php deleted file mode 100644 index bf6b2c2e..00000000 --- a/lib/WebDriver/Exception/XPathLookupError.php +++ /dev/null @@ -1,23 +0,0 @@ - - */ - -namespace WebDriver\Exception; - -use WebDriver\Exception as BaseException; - -/** - * WebDriver\Exception\XPathLookupError class - * - * @package WebDriver - */ -final class XPathLookupError extends BaseException -{ -} diff --git a/lib/WebDriver/Execute.php b/lib/WebDriver/Execute.php index 1ade8bae..ccd1bbe8 100644 --- a/lib/WebDriver/Execute.php +++ b/lib/WebDriver/Execute.php @@ -18,26 +18,23 @@ */ class Execute extends AbstractWebDriver { - /** - * {@inheritdoc} - */ - protected function methods() - { - return array(); - } - /** * Inject a snippet of JavaScript into the page for execution in the context of the currently selected frame. (asynchronous) * - * @param array{script: string, args: array} $jsonScript + * @param string|array $script + * @param array|null $args * * @return mixed */ - public function async(array $jsonScript) + public function async($script, $args = null) { - $jsonScript['args'] = $this->serializeArguments($jsonScript['args']); + $parameters = func_num_args() === 1 && is_array($script) + ? $script + : ['script' => $script, 'args' => $args]; + + $parameters['args'] = $this->serializeArguments($parameters['args']); - $result = $this->curl('POST', '/async', $jsonScript); + $result = $this->curl('POST', '/execute/async', $parameters); return $this->unserializeResult($result['value']); } @@ -45,15 +42,20 @@ public function async(array $jsonScript) /** * Inject a snippet of JavaScript into the page for execution in the context of the currently selected frame. (synchronous) * - * @param array{script: string, args: array} $jsonScript + * @param string|array $script + * @param array|null $args * * @return mixed */ - public function sync(array $jsonScript) + public function sync($script, $args = null) { - $jsonScript['args'] = $this->serializeArguments($jsonScript['args']); + $parameters = func_num_args() === 1 && is_array($script) + ? $script + : ['script' => $script, 'args' => $args]; - $result = $this->curl('POST', '/sync', $jsonScript); + $parameters['args'] = $this->serializeArguments($parameters['args']); + + $result = $this->curl('POST', '/execute/sync', $parameters); return $this->unserializeResult($result['value']); } @@ -70,9 +72,7 @@ public function sync(array $jsonScript) protected function serializeArguments(array $arguments) { foreach ($arguments as $key => $value) { - if ($value instanceof LegacyElement) { - $arguments[$key] = [LegacyElement::LEGACY_ELEMENT_ID => $value->getID()]; - } elseif ($value instanceof Element) { + if ($value instanceof Element) { $arguments[$key] = [Element::WEB_ELEMENT_ID => $value->getID()]; } elseif ($value instanceof Shadow) { $arguments[$key] = [Shadow::SHADOW_ROOT_ID => $value->getID()]; @@ -117,41 +117,18 @@ protected function unserializeResult($result) */ protected function makeElement($value) { - if (array_key_exists(LegacyElement::LEGACY_ELEMENT_ID, $value)) { - $identifier = $value[LegacyElement::LEGACY_ELEMENT_ID]; - - return new LegacyElement( - $this->getIdentifierPath('/element/' . $identifier), - $identifier - ); - } - if (array_key_exists(Element::WEB_ELEMENT_ID, $value)) { $identifier = $value[Element::WEB_ELEMENT_ID]; - return new Element( - $this->getIdentifierPath('/element/' . $identifier), - $identifier - ); + return new Element($this->url . '/element', $identifier); } if (array_key_exists(Shadow::SHADOW_ROOT_ID, $value)) { $identifier = $value[Shadow::SHADOW_ROOT_ID]; - return new Shadow( - $this->getIdentifierPath('/shadow/' . $identifier), - $identifier - ); + return new Shadow($this->url . '/shadow', $identifier); } return null; } - - /** - * {@inheritdoc} - */ - protected function getIdentifierPath($identifier) - { - return preg_replace('~/execute$~', '', $this->url) . $identifier; // remove /execute from path - } } diff --git a/lib/WebDriver/Extension/ChromeDevTools.php b/lib/WebDriver/Extension/ChromeDevTools.php new file mode 100644 index 00000000..9d8b523b --- /dev/null +++ b/lib/WebDriver/Extension/ChromeDevTools.php @@ -0,0 +1,41 @@ + + */ + +namespace WebDriver\Extension; + +use WebDriver\AbstractWebDriver; + +/** + * Chrome Dev Tools extension + * + * @package WebDriver + */ +class ChromeDevTools extends AbstractWebDriver +{ + /** + * Execute Command: /session/:sessionId/goog/cdp/execute (POST) + * + * @param array|string $cmd Command or Parameters {'cmd': ..., 'params': ...} + * @param mixed $params Optional paramaters + * + * @return mixed + */ + public function execute($cmd, $params = null) + { + $parameters = is_array($cmd) + ? $cmd + : ['cmd' => $cmd, 'params' => $params ?? new \stdclass]; + + $result = $this->curl('POST', '/execute', $parameters); + + return $result['value']; + } +} diff --git a/lib/WebDriver/Extension/FederatedCredentialManagementAPI.php b/lib/WebDriver/Extension/FederatedCredentialManagementAPI.php new file mode 100644 index 00000000..eb78f741 --- /dev/null +++ b/lib/WebDriver/Extension/FederatedCredentialManagementAPI.php @@ -0,0 +1,158 @@ + + */ + +namespace WebDriver\Extension; + +use WebDriver\AbstractWebDriver; + +/** + * Federated Credential Management API extensions + * + * @see https://fedidcg.github.io/FedCM/#automation + * + * @package WebDriver + * + * @method array canceldialog() Cancel dialog. + * @method array clickdialogbutton() Click dialog button. + * @method array selectaccount() Select account. + * @method array accountlist() Get accounts. + * @method array gettitle() Get FedCM title. + * @method array getdialogtype() Get FedCM dialog type. + * @method array setdelayenabled() Set delay enabled. + */ +class FederatedCredentialManagementAPI extends AbstractWebDriver +{ + /** + * {@inheritdoc} + */ + protected function methods() + { + return [ + 'canceldialog' => ['POST'], + 'clickdialogbutton' => ['POST'], + 'selectaccount' => ['POST'], + 'accountlist' => ['GET'], + 'gettitle' => ['GET'], + 'getdialogtype' => ['GET'], + 'setdelayenabled' => ['POST'], + ]; + } + + /** + * Cancel Dialog: /session/:sessionId/fedcm/canceldialog (POST) + * + * @return mixed + */ + public function cancelDialog() + { + $result = $this->curl('POST', '/canceldialog'); + + return $result['value']; + } + + /** + * Select Account: /session/:sessionId/fedcm/selectaccount (POST) + * + * @param mixed $parameters Parameters {accountIndex: ...} + * + * @return mixed + */ + public function selectAccount($parameters) + { + if (is_integer($parameters)) { + $parameters = ['accountIndex' => $parameters]; + } + + $result = $this->curl('POST', '/selectaccount', $parameters); + + return $result['value']; + } + + /** + * Click Dialog Button: /session/:sessionId/fedcm/clickdialogbutton (POST) + * + * @param array $parameters Parameters {dialogButton: ...} + * + * @return mixed + */ + public function clickDialogButton($parameters) + { + $result = $this->curl('POST', '/clickdialogbutton', $parameters); + + return $result['value']; + } + + /** + * Get Accounts: /session/:sessionId/fedcm/accountlist (GET) + * + * @return mixed + */ + public function getAccounts() + { + $result = $this->curl('GET', '/accountlist'); + + return $result['value']; + } + + /** + * Get FedCM Title: /session/:sessionId/fedcm/gettitle (GET) + * + * @return mixed + */ + public function getTitle() + { + $result = $this->curl('GET', '/gettitle'); + + return $result['value']; + } + + /** + * Get FedCM Dialog Type: /session/:sessionId/fedcm/getdialogtype (GET) + * + * @return mixed + */ + public function getDialogType() + { + $result = $this->curl('GET', '/getdialogtype'); + + return $result['value']; + } + + /** + * Set Delay Enabled: /session/:sessionId/fedcm/setdelayenabled (POST) + * + * @param mixed $parameters Parameters {enabled: ...} + * + * @return mixed + */ + public function setDelayEnabled($parameters) + { + if (is_bool($parameters)) { + $parameters = ['enabled' => $parameters]; + } + + $result = $this->curl('POST', '/setdelayenabled', $parameters); + + return $result['value']; + } + + /** + * Reset Cooldown: /session/:sessionId/fedcm/resetCooldown (POST) + * + * @return mixed + */ + public function resetCooldown() + { + $result = $this->curl('POST', '/resetCooldown'); + + return $result['value']; + } +} diff --git a/lib/WebDriver/Extension/Selenium.php b/lib/WebDriver/Extension/Selenium.php new file mode 100644 index 00000000..b4da88cd --- /dev/null +++ b/lib/WebDriver/Extension/Selenium.php @@ -0,0 +1,126 @@ + + */ + +namespace WebDriver\Extension; + +use WebDriver\AbstractWebDriver; + +/** + * Selenium extensions + * + * {@internal + * /se/files Downloads requires se:downloadsEnabled + * }} + * + * @package WebDriver + */ +class Selenium extends AbstractWebDriver +{ + /** + * Get log: /session/:sessionId/se/log + * + * @param mixed $parameters + * + * @return mixed + */ + public function getLog($parameters) + { + if (is_string($parameters)) { + $parameters = [ + 'type' => $parameters, + ]; + } + + $result = $this->curl('POST', '/log', $parameters); + + return $result['value']; + } + + /** + * Get available log types: /session/:sessionId/se/log/types + * + * @return mixed + */ + public function getAvailableLogTypes() + { + $result = $this->curl('GET', '/log/types'); + + return $result['value']; + } + + /** + * Download File: /session/:sessionId/se/files (POST) + * + * @param array|string $parameters Parameters {'name': ...} + * + * @eturn mixed + */ + public function downloadFile($parameters) + { + if (is_string($parameters)) { + $parameters = ['name' => $parameters]; + } + + $result = $this->curl('POST', '/se/files', $parameters); + + return $result['value']; + } + + /** + * Get Downloadable Files: /session/:sessionId/se/files (GET) + * + * @eturn mixed + */ + public function getDownloadableFiles() + { + $result = $this->curl('GET', '/se/files'); + + return $result['value']; + } + + /** + * Delete Downloadable Files: /session/:sessionId/se/files (DELETE) + * + * @eturn mixed + */ + public function deleteDownloadableFiles() + { + $result = $this->curl('DELETE', '/se/files'); + + return $result['value']; + } + + /** + * Upload File: /session/:sessionId/se/file (POST) + * + * @param array|string $parameters Parameters {file: ...} + * + * @eturn mixed + */ + public function uploadFile($parameters) + { + if (is_string($parameters)) { + $parameters = ['file' => $parameters]; + } + + // expecting ZIP file format + if (substr($parameters['file'], 0, 4) === "PK\x03\x04") { + $parameters['file'] = base64_encode($parameters['file']); + } elseif (substr($parameters['file'], 0, 5) !== 'UEsDB') { + // suspicious but looks intentional + trigger_error('UPLOAD_FILE expected base64 encoded ZIP file', E_USER_NOTICE); + } + + $result = $this->curl('POST', '/se/file', $parameters); + + return $result['value']; + } +} diff --git a/lib/WebDriver/LogType.php b/lib/WebDriver/Extension/Selenium/LogType.php similarity index 88% rename from lib/WebDriver/LogType.php rename to lib/WebDriver/Extension/Selenium/LogType.php index 939a325a..52a3b697 100644 --- a/lib/WebDriver/LogType.php +++ b/lib/WebDriver/Extension/Selenium/LogType.php @@ -9,7 +9,7 @@ * @author Anthon Pang */ -namespace WebDriver; +namespace WebDriver\Extension\Selenium; /** * WebDriver\LogType class @@ -27,6 +27,6 @@ final class LogType const CLIENT = 'client'; const DRIVER = 'driver'; const PERFORMANCE = 'performance'; - const PROFILER = 'driver'; + const PROFILER = 'profiler'; const SERVER = 'server'; } diff --git a/lib/WebDriver/Extension/WebAuthenticationAPI.php b/lib/WebDriver/Extension/WebAuthenticationAPI.php new file mode 100644 index 00000000..9bf7b7e6 --- /dev/null +++ b/lib/WebDriver/Extension/WebAuthenticationAPI.php @@ -0,0 +1,41 @@ + + */ + +namespace WebDriver\Extension; + +use WebDriver\AbstractWebDriver; +use WebDriver\Extension\WebAuthenticationAPI\VirtualAuthenticator; + +/** + * Web Authentication API + * + * @see https://www.w3.org/TR/webauthn-2/#sctn-automation + * + * @package WebDriver + */ +class WebAuthenticationAPI extends AbstractWebDriver +{ + /** + * Add virtual authenticator: /session/:sessionId/webauthn/authenticator (POST) + * + * @param mixed $parameters Authenticator Configuration {protocol: ... transport: ..., hasResidentKey: ..., hasUserVerification: ..., isUserConsenting: ..., isUserVerified: ..., extensions: ..., uvm: ...} + * + * @return mixed + */ + public function addVirtualAuthenticator($parameters) + { + $result = $this->curl('POST', '', $parameters); + + $authenticatorId = $result['value']; + + return new VirtualAuthenticator($this->url, $authenticatorId); + } +} diff --git a/lib/WebDriver/Extension/WebAuthenticationAPI/Credential.php b/lib/WebDriver/Extension/WebAuthenticationAPI/Credential.php new file mode 100644 index 00000000..8de37ac1 --- /dev/null +++ b/lib/WebDriver/Extension/WebAuthenticationAPI/Credential.php @@ -0,0 +1,45 @@ + + */ + +namespace WebDriver\Extension\WebAuthenticationAPI; + +use WebDriver\AbstractWebDriver; + +/** + * Virtual Authenticator Credential + * + * @package WebDriver + */ +class Credential extends AbstractWebDriver +{ + /** + * Constructor + * + * @param string $url URL + * @param string $id Credential ID + */ + public function __construct($url, $id) + { + parent::__construct($url . "/$id"); + } + + /** + * Remove Credential: /session/:sessionId/webauthn/authenticator/:authenticatorId/credentials/:credentialId (DELETE) + * + * @return mixed + */ + public function removeCredential() + { + $result = $this->curl('DELETE', ''); + + return $result['value']; + } +} diff --git a/lib/WebDriver/Extension/WebAuthenticationAPI/Credentials.php b/lib/WebDriver/Extension/WebAuthenticationAPI/Credentials.php new file mode 100644 index 00000000..b31f5815 --- /dev/null +++ b/lib/WebDriver/Extension/WebAuthenticationAPI/Credentials.php @@ -0,0 +1,58 @@ + + */ + +namespace WebDriver\Extension\WebAuthenticationAPI; + +use WebDriver\AbstractWebDriver; + +/** + * Virtual Authenticator Credentials + * + * @package WebDriver + * + * @method array credentials() Get credentials. + */ +class Credentials extends AbstractWebDriver +{ + /** + * {@inheritdoc} + */ + protected function methods() + { + return [ + 'credentials' => ['GET'], + ]; + } + + /** + * Get Credentials: /session/:sessionId/webauthn/authenticator/:authenticatorId/credentials (GET) + * + * @return mixed + */ + public function getCredentials() + { + $result = $this->curl('GET', ''); + + return $result['value']; + } + + /** + * Remove All Credentials: /session/:sessionId/webauthn/authenticator/:authenticatorId/credentials (DELETE) + * + * @return mixed + */ + public function removeCredentials() + { + $result = $this->curl('DELETE', ''); + + return $result['value']; + } +} diff --git a/lib/WebDriver/Extension/WebAuthenticationAPI/VirtualAuthenticator.php b/lib/WebDriver/Extension/WebAuthenticationAPI/VirtualAuthenticator.php new file mode 100644 index 00000000..74225b6f --- /dev/null +++ b/lib/WebDriver/Extension/WebAuthenticationAPI/VirtualAuthenticator.php @@ -0,0 +1,115 @@ + + */ + +namespace WebDriver\Extension\WebAuthenticationAPI; + +use WebDriver\AbstractWebDriver; + +/** + * Virtual Authenticator + * + * @package WebDriver + * + * @method array credential($parameters) Add credential. + * @method array uv($parameters) Set user verified. + */ +class VirtualAuthenticator extends AbstractWebDriver +{ + /** + * {@inheritdoc} + */ + protected function methods() + { + return [ + 'credential' => ['POST'], + 'uv' => ['POST'], + ]; + } + + /** + * {@inheritdoc} + */ + protected function chainable() + { + return [ + 'credentials' => 'credentials', + ]; + } + + /** + * Constructor + * + * @param string $url URL + * @param string $id Authenticator ID + */ + public function __construct($url, $id) + { + parent::__construct($url . "/$id"); + } + + /** + * Remove Virtual Authenticator: /session/:sessionId/webauthn/authenticator/:authenticatorId (DELETE) + * + * @return mixed + */ + public function removeVirtualAuthenticator() + { + $result = $this->curl('DELETE', ''); + + return $result['value']; + } + + /** + * Add credential: /session/:sessionId/webauthn/authenticator/:authenticatorId/credential (POST) + * + * @param array $parameters Credential Parameters {credentialId: ..., isResidentCredential: ..., rpId: ..., privateKey: ..., userHandle: ..., signCount: ..., largeBlob: ...} + * + * @return \WebDriver\Extension\WebAuthenticationAPI\Credential + */ + public function addCredential($parameters) + { + $credentialId = $parameters['credentialId']; + + $this->curl('POST', '/credential', $parameters); + + return new Credential($this->url . '/credentials', $credentialId); + } + + /** + * Set user verified: /session/:sessionId/webauthn/authenticator/:authenticatorId/uv (POST) + * + * @param array|boolean $parameters Parameters {isUserVerified: ...} + * + * @return mixed + */ + public function setUserVerified($parameters) + { + if (is_bool($parameters)) { + $parameters = ['isUserVerified' => $parameters]; + } + + $result = $this->curl('POST', '/uv', $parameters); + + return $result['value']; + } + + /** + * Get Credentials object for chaining + * - $authenticator->credentials()->method() + * - $authenticator->credentials->method() + * + * @return \WebDriver\Extension\WebAuthenticationAPI\Credentials + */ + protected function credentials() + { + return new Credentials($this->url . '/credentials'); + } +} diff --git a/lib/WebDriver/Frame.php b/lib/WebDriver/Frame.php index 37ff9164..53afc2ee 100644 --- a/lib/WebDriver/Frame.php +++ b/lib/WebDriver/Frame.php @@ -27,8 +27,8 @@ class Frame extends AbstractWebDriver */ protected function methods() { - return array( - 'parent' => array('POST'), - ); + return [ + 'parent' => ['POST'], + ]; } } diff --git a/lib/WebDriver/Ime.php b/lib/WebDriver/Ime.php deleted file mode 100644 index 8c419c2a..00000000 --- a/lib/WebDriver/Ime.php +++ /dev/null @@ -1,40 +0,0 @@ - - */ - -namespace WebDriver; - -/** - * WebDriver\Ime class - * - * @package WebDriver - * - * @method void activate($json) Make an engine that is available active. - * @method boolean activated() Indicates whether IME input is active at the moment. - * @method string active_engine() Get the name of the active IME engine. - * @method array available_engines() List all available engines on the machines. - * @method void deactivate() De-activates the currently active IME engine. - */ -class Ime extends AbstractWebDriver -{ - /** - * {@inheritdoc} - */ - protected function methods() - { - return array( - 'activate' => array('POST'), - 'activated' => array('GET'), - 'active_engine' => array('GET'), - 'available_engines' => array('GET'), - 'deactivate' => array('POST'), - ); - } -} diff --git a/lib/WebDriver/KeyInput.php b/lib/WebDriver/KeyInput.php new file mode 100644 index 00000000..543577f7 --- /dev/null +++ b/lib/WebDriver/KeyInput.php @@ -0,0 +1,74 @@ + + */ + +namespace WebDriver; + +/** + * WebDriver\KeyInput class + * + * @package WebDriver + */ +class KeyInput extends NullInput +{ + const TYPE = 'key'; + + // actions + const KEY_DOWN = 'keyDown'; + const KEY_UP = 'keyUp'; + + /** + * Key Down + * + * {@internal action item properties: + * value: string - mandatory; contains a single unicode code point + * }} + * + * @param array $action Action item + * + * @return array + */ + public function keyDown($action) + { + $action['type'] = self::KEY_DOWN; + + return [ + 'id' => $this->id, + 'type' => static::TYPE, + 'actions' => [ + $action, + ], + ]; + } + + /** + * Key Up + * + * {@internal action item properties: + * value: string - mandatory; contains a single unicode code point + * }} + * + * @param array $action Action item + * + * @return array + */ + public function keyUp($action) + { + $action['type'] = self::KEY_UP; + + return [ + 'id' => $this->id, + 'type' => static::TYPE, + 'actions' => [ + $action, + ], + ]; + } +} diff --git a/lib/WebDriver/LegacyElement.php b/lib/WebDriver/LegacyElement.php deleted file mode 100644 index e3b2e871..00000000 --- a/lib/WebDriver/LegacyElement.php +++ /dev/null @@ -1,40 +0,0 @@ - - */ - -namespace WebDriver; - -/** - * WebDriver\LegacyElement class - * - * @package WebDriver - * - * @method string attribute($attributeName) Get the value of an element's attribute. - * @method void clear() Clear a TEXTAREA or text INPUT element's value. - * @method void click() Click on an element. - * @method string css($propertyName) Query the value of an element's computed CSS property. - * @method boolean displayed() Determine if an element is currently displayed. - * @method boolean enabled() Determine if an element is currently enabled. - * @method boolean equals($otherId) Test if two element IDs refer to the same DOM element. - * @method array location() Determine an element's location on the page. - * @method array location_in_view() Determine an element's location on the screen once it has been scrolled into view. - * @method string name() Query for an element's tag name. - * @method array property($propertyName) Get element property. - * @method array rect() Get element rect. - * @method array screenshot() Take element screenshot. - * @method array size() Determine an element's size in pixels. - * @method void submit() Submit a FORM element. - * @method string text() Returns the visible text for the element. - * @method void postValue($json) Send a sequence of key strokes to an element. - */ -class LegacyElement extends Element -{ - const LEGACY_ELEMENT_ID = 'ELEMENT'; -} diff --git a/lib/WebDriver/LegacyExecute.php b/lib/WebDriver/LegacyExecute.php deleted file mode 100644 index 295dde4a..00000000 --- a/lib/WebDriver/LegacyExecute.php +++ /dev/null @@ -1,68 +0,0 @@ - - */ - -namespace WebDriver; - -/** - * WebDriver\LegacyExecute class - * - * @package WebDriver - */ -class LegacyExecute extends Execute -{ - /** - * {@inheritdoc} - */ - protected function methods() - { - return array(); - } - - /** - * Inject a snippet of JavaScript into the page for execution in the context of the currently selected frame. (asynchronous) - * - * @param array{script: string, args: array} $jsonScript - * - * @return mixed - */ - public function async(array $jsonScript) - { - $jsonScript['args'] = $this->serializeArguments($jsonScript['args']); - - $result = $this->curl('POST', '/execute_async', $jsonScript); - - return $this->unserializeResult($result['value']); - } - - /** - * Inject a snippet of JavaScript into the page for execution in the context of the currently selected frame. (synchronous) - * - * @param array{script: string, args: array} $jsonScript - * - * @return mixed - */ - public function sync(array $jsonScript) - { - $jsonScript['args'] = $this->serializeArguments($jsonScript['args']); - - $result = $this->curl('POST', '/execute', $jsonScript); - - return $this->unserializeResult($result['value']); - } - - /** - * {@inheritdoc} - */ - protected function getIdentifierPath($identifier) - { - return $this->url . $identifier; - } -} diff --git a/lib/WebDriver/LegacyWindow.php b/lib/WebDriver/LegacyWindow.php deleted file mode 100644 index 97010a2c..00000000 --- a/lib/WebDriver/LegacyWindow.php +++ /dev/null @@ -1,85 +0,0 @@ - - */ - -namespace WebDriver; - -/** - * WebDriver\LegacyWindow class - * - * @package WebDriver - * - * @method void maximize() Maximize the window if not already maximized. - * @method array getPosition() Get position of the window. - * @method void postPosition($json) Change position of the window. - * @method array getSize() Get size of the window. - * @method void postSize($json) Change the size of the window. - */ -class LegacyWindow extends AbstractWebDriver -{ - /** - * @var string - */ - private $windowHandle; - - /** - * {@inheritdoc} - */ - protected function methods() - { - return array( - // Legacy JSON Wire Protocol - 'maximize' => array('POST'), - 'position' => array('GET', 'POST'), - 'size' => array('GET', 'POST'), - ); - } - - /** - * {@inheritdoc} - */ - protected function obsoleteMethods() - { - return array( - 'restore' => array('POST'), - ); - } - - /** - * Constructor - * - * @param string $url - * @param string|null $windowHandle - */ - public function __construct($url, $windowHandle = null) - { - parent::__construct($url); - - $this->windowHandle = $windowHandle; - } - - /** - * Get window handle: /session/:sessionId/window_handle (GET) - * - $session->window($handle)->getHandle() - * - $session->window()->getHandle() - * - * @return string - */ - public function getHandle() - { - if (! $this->windowHandle) { - $result = $this->curl('GET', '_handle'); - - $this->windowHandle = $result['value']; - } - - return $this->windowHandle; - } -} diff --git a/lib/WebDriver/LocatorStrategy.php b/lib/WebDriver/LocatorStrategy.php index b0e6923e..9bb9e0ab 100644 --- a/lib/WebDriver/LocatorStrategy.php +++ b/lib/WebDriver/LocatorStrategy.php @@ -18,12 +18,14 @@ */ final class LocatorStrategy { - const CLASS_NAME = 'class name'; const CSS_SELECTOR = 'css selector'; - const ID = 'id'; - const NAME = 'name'; const LINK_TEXT = 'link text'; const PARTIAL_LINK_TEXT = 'partial link text'; const TAG_NAME = 'tag name'; const XPATH = 'xpath'; + + // deprecated + const CLASS_NAME = 'class name'; + const ID = 'id'; + const NAME = 'name'; } diff --git a/lib/WebDriver/Log.php b/lib/WebDriver/Log.php deleted file mode 100644 index 4a491737..00000000 --- a/lib/WebDriver/Log.php +++ /dev/null @@ -1,32 +0,0 @@ - - */ - -namespace WebDriver; - -/** - * WebDriver\Log class - * - * @package WebDriver - * - * @method array types() Get available log types. - */ -class Log extends AbstractWebDriver -{ - /** - * {@inheritdoc} - */ - protected function methods() - { - return array( - 'types' => array('GET'), - ); - } -} diff --git a/lib/WebDriver/NullInput.php b/lib/WebDriver/NullInput.php new file mode 100644 index 00000000..9cdefb07 --- /dev/null +++ b/lib/WebDriver/NullInput.php @@ -0,0 +1,62 @@ + + */ + +namespace WebDriver; + +/** + * WebDriver\NullInput class + * + * @package WebDriver + */ +class NullInput +{ + const TYPE = 'none'; + + // actions + const PAUSE = 'pause'; + + /** + * @var integer + */ + protected $id; + + /** + * @param integer $id + */ + public function __construct($id) + { + $this->id = $id; + } + + /** + * Creates a pause of the given duration + * + * {@internal action item properties: + * duration: int + * }} + * + * @param array $action Action item + * + * @return array + */ + public function pause($action) + { + $action['type'] = self::PAUSE; + + return [ + 'id' => $this->id, + 'type' => static::TYPE, + 'actions' => [ + $action, + ], + ]; + } +} diff --git a/lib/WebDriver/Platform.php b/lib/WebDriver/Platform.php new file mode 100644 index 00000000..65ef9682 --- /dev/null +++ b/lib/WebDriver/Platform.php @@ -0,0 +1,29 @@ + + */ + +namespace WebDriver; + +/** + * WebDriver\Platform class + * + * @internal non-exhaustive list + * + * @package WebDriver + */ +final class Platform +{ + const ANDROID = 'android'; + const IOS = 'ios'; + const LINUX = 'linux'; + const MAC = 'mac'; + const WINDOWS = 'windows'; + const UNIX = 'unix'; +} diff --git a/lib/WebDriver/PointerInput.php b/lib/WebDriver/PointerInput.php new file mode 100644 index 00000000..e7549bf0 --- /dev/null +++ b/lib/WebDriver/PointerInput.php @@ -0,0 +1,192 @@ + + */ + +namespace WebDriver; + +/** + * WebDriver\PointerInput class + * + * @package WebDriver + */ +class PointerInput extends NullInput +{ + const TYPE = 'pointer'; + + // sub-types + const MOUSE = 'mouse'; + const PEN = 'pen'; + const TOUCH = 'touch'; + + // actions + const POINTER_DOWN = 'pointerDown'; + const POINTER_UP = 'pointerUp'; + const POINTER_MOVE = 'pointerMove'; + const POINTER_CANCEL = 'pointerCancel'; + + // buttons + const LEFT_BUTTON = 0; + const MIDDLE_BUTTON = 1; + const RIGHT_BUTTON = 2; + const X1_BUTTON = 3; + const BACK_BUTTON = 3; + const X2_BUTTON = 4; + const FORWARD_BUTTON = 4; + + /** + * @var string + */ + private $subType; + + /** + * @param integer $id + * @param string $subType + */ + public function __construct($id, $subType) + { + parent::__construct($id); + + $this->subType = $subType; + } + + /** + * Pointer Down + * + * {@internal action item properties: + * button: int (0..) - mandatory + * height: number (0..) + * width: number (0..) + * pressure: float (0..1) + * tangentialPressure: float (-1..1) + * tiltX: int (-90..90) + * tiltY: int (-90..90) + * twist: int (0..359) + * altitudeAngle: float (0..pi/2) + * azimuthAngle: float (0..2pi) + * }} + * + * @param array $action Action item + * + * @return array + */ + public function pointerDown($action) + { + $action['type'] = self::POINTER_DOWN; + + return [ + 'id' => $this->id, + 'type' => static::TYPE, + 'actions' => [ + $action, + ], + 'parameters' => [ + 'pointerType' => $this->subType, + ], + ]; + } + + /** + * Pointer Up + * + * {@internal action item properties: + * button: int (0..) - mandatory + * height: number (0..) + * width: number (0..) + * pressure: float (0..1) + * tangentialPressure: float (-1..1) + * tiltX: int (-90..90) + * tiltY: int (-90..90) + * twist: int (0..359) + * altitudeAngle: float (0..pi/2) + * azimuthAngle: float (0..2pi) + * }} + * + * @param array $action Action item + * + * @return array + */ + public function pointerUp($action) + { + $action['type'] = self::POINTER_UP; + + return [ + 'id' => $this->id, + 'type' => static::TYPE, + 'actions' => [ + $action + ], + 'parameters' => [ + 'pointerType' => $this->subType, + ], + ]; + } + + /** + * Pointer Move + * + * {@internal action item properties: + * x: int (mandatory) + * y: int (mandatory) + * height: number (0..) + * width: number (0..) + * pressure: float (0..1) + * tangentialPressure: float (-1..1) + * tiltX: int (-90..90) + * tiltY: int (-90..90) + * twist: int (0..359) + * altitudeAngle: float (0..pi/2) + * azimuthAngle: float (0..2pi) + * duration: int (0..) + * origin: string + * }} + * + * @param array $action Action item + * + * @return array + */ + public function pointerMove($action) + { + $action['type'] = self::POINTER_MOVE; + + return [ + 'id' => $this->id, + 'type' => static::TYPE, + 'actions' => [ + $action, + ], + 'parameters' => [ + 'pointerType' => $this->subType, + ], + ]; + } + + /** + * Pointer Cancel + * + * @param array $action Action item + * + * @return array + */ + public function pointerCancel($action) + { + $action['type'] = self::POINTER_CANCEL; + + return [ + 'id' => $this->id, + 'type' => static::TYPE, + 'actions' => [ + $action, + ], + 'parameters' => [ + 'pointerType' => $this->subType, + ], + ]; + } +} diff --git a/lib/WebDriver/Proxy.php b/lib/WebDriver/Proxy.php new file mode 100644 index 00000000..95a73c0e --- /dev/null +++ b/lib/WebDriver/Proxy.php @@ -0,0 +1,34 @@ + + */ + +namespace WebDriver; + +/** + * WebDriver\Proxy class + * + * @package WebDriver + */ +final class Proxy +{ + /** + * Proxy configuration + * + * @see https://w3c.github.io/webdriver/webdriver-spec.html#proxy + */ + const PROXY_TYPE = 'proxyType'; + const PROXY_AUTOCONFIG_URL = 'proxyAutoconfigUrl'; + const FTP_PROXY = 'ftpProxy'; + const HTTP_PROXY = 'httpProxy'; + const NO_PROXY = 'noProxy'; + const SSL_PROXY = 'sslProxy'; + const SOCKS_PROXY = 'socksProxy'; + const SOCKS_VERSION = 'socksVersion'; +} diff --git a/lib/WebDriver/ProxyType.php b/lib/WebDriver/ProxyType.php new file mode 100644 index 00000000..25369116 --- /dev/null +++ b/lib/WebDriver/ProxyType.php @@ -0,0 +1,31 @@ + + */ + +namespace WebDriver; + +/** + * WebDriver\ProxyType class + * + * @package WebDriver + */ +final class ProxyType +{ + /** + * Proxy types + * + * @see https://w3c.github.io/webdriver/webdriver-spec.html#proxy + */ + const AUTODETECT = 'autodetect'; + const DIRECT = 'direct'; + const MANUAL = 'manual'; + const PAC = 'pac'; + const SYSTEM = 'system'; +} diff --git a/lib/WebDriver/SauceLabs/Capability.php b/lib/WebDriver/SauceLabs/Capability.php deleted file mode 100644 index cd487914..00000000 --- a/lib/WebDriver/SauceLabs/Capability.php +++ /dev/null @@ -1,113 +0,0 @@ - - */ - -namespace WebDriver\SauceLabs; - -/** - * WebDriver\SauceLabs\Capability class - * - * @package WebDriver - */ -final class Capability -{ - /** - * Desired capabilities - SauceLabs - * - * @see https://docs.saucelabs.com/dev/test-configuration-options/ - */ - - // Desktop Browser Capabilities - optional - const CHROMEDDRIVER_VERSION = 'chromedriverVersion'; // Use a specific Chrome Driver version - const EDGEDRIVER_VERSION = 'edgedriverVersion'; // Use a specific Edge Driver version - const IEDRIVER_VERSION = 'iedriverVersion'; // Use a specific Internet Explorer version - const GECKODRIVER_VERSION = 'geckodriverVersion'; // Use a specific Gecko Driver version - const SELENIUM_VERSION = 'seleniumVersion'; // Use a specific Selenium version - - const AVOID_PROXY = 'avoidProxy'; // Avoid proxy - const CAPTURE_PERFORMANCE = 'capturePerformance'; // Capture performance - const EXTENDED_DEBUGGING = 'extendedDebugging'; // Extended debugging - const SCREEN_RESOLUTION = 'screenResolution'; // Use specific screen resolution - - // Mobile App Appium Capabilities - required - const APP = 'app'; // Path to app you want to test - const DEVICE_NAME = 'deviceName'; // Name of simulator, emulator, or real device to use - const PLATFORM_VERSION = 'platformVersion'; // Mobile OS platform version - const AUTOMATION_NAME = 'automationName'; // Engine: Appium, UiAutomator2, or Selendroid - const APP_PACKAGE = 'appPackage'; // Name of Java package to run - const APP_ACTIVITY = 'appActivity'; // Name of Android activity to launch - const AUTO_ACCEPT_ALERTS = 'autoAcceptAlerts'; // Auto accept alerts (for iOS only) - - // Mobile App Appium Capabilities - optional - const APPIUM_VERSION = 'appiumVersion'; // Appium driver version you want to use - const DEVICE_ORIENTATION = 'deviceOrientation'; // Device orientation (portrait or landscape) - const ORIENTATION = 'orientation'; // (alias) - const DEVICE_TYPE = 'deviceType'; // Device type (phone or tablet) - const OTHER_APPS = 'otherApps'; // Dependent apps - const TABLET_ONLY = 'tabletOnly'; // Select only tablet devices for testing - const PHONE_ONLY = 'phoneOnly'; // Select only phone devices - const PRIVATE_DEVICES_ONLY = 'privateDevicesOnly'; // Request allocation of private devices - const PUBLIC_DEVICES_ONLY = 'publicDevicesOnly'; // Request allocation of public devices - const CARRIER_CONNECTIVITY_ONLY = 'carrierConnectivityOnly'; // Allocate only devices connected to a carrier network - const CACHE_ID = 'cacheId'; // Keeps a device allocated to you between test sessions - const SESSION_CREATION_TIMEOUT = 'sessionCreationTimeout'; // Number of times the test should attempt to launch a session - const NEW_COMMAND_TIMEOUT = 'newCommandTimeout'; // Amount of time (in seconds) that the test should allow to launch a test before failing - const NO_RESET = 'noReset'; // Keep a device allocated to you during the device cleaning proces - const CROSSWALK_APPLICATION = 'crosswalkApplication'; // Patched version of ChromeDriver that will work with Crosswalk - const AUTO_GRANT_PERMISSIONS = 'autoGrantPermissions'; // To disable auto grant permissions - const ENABLE_ANIMATIONS = 'enableAnimations'; // Enable animations - const RESIGNING_ENABLED = 'resigningEnabled'; // To allow testing of apps without resigning - const SAUCE_LABS_IMAGE_INJECTION_ENABLED = 'sauceLabsImageInjectionEnabled'; // Enables the camera image injection feature - const SAUCE_LABS_BYPASS_SCREENSHOT_RESTRICTION = 'sauceLabsBypassScreenshotRestriction'; // Bypasses the restriction on taking screenshots for secure screen - const ALLOW_TOUCH_ID_ENROLL = 'allowTouchIdEnroll'; // Enables the interception of biometric input - const GROUP_FOLDER_REDIRECT_ENABLED = 'groupFolderRedirectEnabled'; // Enables the use of the app's private app container directory - const SYSTEM_ALERTS_DELAY_ENABLED = 'systemAlertsDelayEnabled'; // Delays system alerts, - - // Desktop and Mobile Capabilities - optional - - // Job Annotation - const NAME = 'name'; // Name the job - const BUILD = 'build'; // Record the build number - const TAGS = 'tags'; // Tag your jobs - const PASSED = 'passed'; // Record pass/fail status - const ACCESS_KEY = 'accessKey'; // Access key - - // Job Sharing - const PUBLIC_RESULTS = 'public'; // Make public, private, or share jobs - - // Performance improvements and data collection - const CUSTOM_DATA = 'custom-data'; // Record custom data - const CAPTURE_HTML = 'captureHtml'; // HTML source capture - const QUIET_EXCEPTIONS = 'webdriverRemoteQuietExceptions'; // Enable Selenium 2's automatic screenshots - - const TUNNEL_IDENTIFIER = 'tunnelIdentifier'; // Use identified tunnel - const PARENT_TUNNEL = 'parentTunnel'; // Shared tunnels - const RECORD_LOGS = 'recordLogs'; // Log recording - const RECORD_SCREENSHOTS = 'recordScreenshots'; // Record step-by-step screenshots - const RECORD_VIDEO = 'recordVideo'; // Video recording - const VIDEO_UPLOAD_ON_PASS = 'videoUploadOnPass'; // Video upload on pass - - // Timeouts - const MAX_DURATION = 'maxDuration'; // Set maximum test duration - const COMMAND_TIMEOUT = 'commandTimeout'; // Set command timeout - const IDLE_TIMEOUT = 'idleTimeout'; // Set idle test timeout - - // Virtual Device Capabilities - const PRIORITY = 'priority'; // Prioritize jobs - const TIME_ZONE = 'timeZone'; // Time zone - const PRERUN = 'prerun'; // Prerun executables (primary key) - const EXECUTABLE = 'executable'; // Executable (secondary key) - const ARGS = 'args'; // Args (secondary key) - const BACKGROUND = 'background'; // Background (secondary key) - const TIMEOUT = 'timeout'; // Timeout (secondary key) - - // obsolete - const VERSION = 'version'; // Browser version -} diff --git a/lib/WebDriver/SauceLabs/SauceRest.php b/lib/WebDriver/SauceLabs/SauceRest.php deleted file mode 100644 index da40ead2..00000000 --- a/lib/WebDriver/SauceLabs/SauceRest.php +++ /dev/null @@ -1,356 +0,0 @@ - - * @author Fabrizio Branca - */ - -namespace WebDriver\SauceLabs; - -use WebDriver\ServiceFactory; - -/** - * WebDriver\SauceLabs\SauceRest class - * - * @package WebDriver - */ -class SauceRest -{ - /** - * @var string - */ - private $userId; - - /** - * @var string - */ - private $accessKey; - - /** - * Curl service - * - * @var \WebDriver\Service\CurlService|null - */ - private $curlService; - - /** - * Transient options - * - * @var array - */ - private $transientOptions; - - /** - * Constructor - * - * @param string $userId Your Sauce user name - * @param string $accessKey Your Sauce API key - */ - public function __construct($userId, $accessKey) - { - $this->userId = $userId; - $this->accessKey = $accessKey; - $this->curlService = null; - } - - /** - * Set curl service - * - * @param \WebDriver\Service\CurlService $curlService - */ - public function setCurlService($curlService) - { - $this->curlService = $curlService; - } - - /** - * Get curl service - * - * @return \WebDriver\Service\CurlService - */ - public function getCurlService() - { - return $this->curlService ?: ServiceFactory::getInstance()->getService('service.curl'); - } - - /** - * Set transient options - * - * @param mixed $transientOptions - */ - public function setTransientOptions($transientOptions) - { - $this->transientOptions = is_array($transientOptions) ? $transientOptions : array(); - } - - /** - * Execute Sauce Labs REST API command - * - * @param string $requestMethod HTTP request method - * @param string $url URL - * @param mixed $parameters Parameters - * @param array $extraOptions key=>value pairs of curl options to pass to curl_setopt() - * - * @return mixed - * - * @throws \WebDriver\Exception\CurlExec - * - * @see https://docs.saucelabs.com/secure-connections/sauce-connect/system-requirements/#rest-api-endpoints - */ - protected function execute($requestMethod, $url, $parameters = null, $extraOptions = array()) - { - $extraOptions = array( - CURLOPT_HTTPAUTH => CURLAUTH_BASIC, - CURLOPT_USERPWD => $this->userId . ':' . $this->accessKey, - - // don't verify SSL certificates - CURLOPT_SSL_VERIFYPEER => false, - CURLOPT_SSL_VERIFYHOST => false, - - CURLOPT_HTTPHEADER => array('Expect:'), - CURLOPT_FAILONERROR => true, - ); - - $url = 'https://saucelabs.com/rest/v1/' . $url; - - list($rawResult, $info) = $this->curlService->execute( - $requestMethod, - $url, - $parameters, - array_replace($extraOptions, $this->transientOptions) - ); - - $this->transientOptions = array(); - - return json_decode($rawResult, true); - } - - /** - * Get account details: /rest/v1/users/:userId (GET) - * - * @param string $userId - * - * @return array - */ - public function getAccountDetails($userId) - { - return $this->execute('GET', 'users/' . $userId); - } - - /** - * Check account limits: /rest/v1/limits (GET) - * - * @return array - */ - public function getAccountLimits() - { - return $this->execute('GET', 'limits'); - } - - /** - * Create new sub-account: /rest/v1/users/:userId (POST) - * - * For "partners", $accountInfo also contains 'plan' => (one of 'free', 'small', 'team', 'com', or 'complus') - * - * @param array $accountInfo array('username' => ..., 'password' => ..., 'name' => ..., 'email' => ...) - * - * @return array array('access_key' => ..., 'minutes' => ..., 'id' => ...) - */ - public function createSubAccount($accountInfo) - { - return $this->execute('POST', 'users/' . $this->userId, $accountInfo); - } - - /** - * Update sub-account service plan: /rest/v1/users/:userId/subscription (POST) - * - * @param string $userId User ID - * @param string $plan Plan - * - * @return array - */ - public function updateSubAccount($userId, $plan) - { - return $this->execute('POST', 'users/' . $userId . '/subscription', array('plan' => $plan)); - } - - /** - * Unsubscribe a sub-account: /rest/v1/users/:userId/subscription (DELETE) - * - * @param string $userId User ID - * - * @return array - */ - public function unsubscribeSubAccount($userId) - { - return $this->execute('DELETE', 'users/' . $userId . '/subscription'); - } - - /** - * Get current account activity: /rest/v1/:userId/activity (GET) - * - * @return array - */ - public function getActivity() - { - return $this->execute('GET', $this->userId . '/activity'); - } - - /** - * Get historical account usage: /rest/v1/:userId/usage (GET) - * - * @param string $start Optional start date YYYY-MM-DD - * @param string $end Optional end date YYYY-MM-DD - * - * @return array - */ - public function getUsage($start = null, $end = null) - { - $query = http_build_query(array( - 'start' => $start, - 'end' => $end, - )); - - return $this->execute('GET', $this->userId . '/usage' . (strlen($query) ? '?' . $query : '')); - } - - /** - * Get jobs: /rest/v1/:userId/jobs (GET) - * - * @param boolean $full - * - * @return array - */ - public function getJobs($full = null) - { - $query = http_build_query(array( - 'full' => (isset($full) && $full) ? 'true' : null, - )); - - return $this->execute('GET', $this->userId . '/jobs' . (strlen($query) ? '?' . $query : '')); - } - - /** - * Get full information for job: /rest/v1/:userId/jobs/:jobId (GET) - * - * @param string $jobId - * - * @return array - */ - public function getJob($jobId) - { - return $this->execute('GET', $this->userId . '/jobs/' . $jobId); - } - - /** - * Update existing job: /rest/v1/:userId/jobs/:jobId (PUT) - * - * @param string $jobId Job ID - * @param array $jobInfo Job information - * - * @return array - */ - public function updateJob($jobId, $jobInfo) - { - return $this->execute('PUT', $this->userId . '/jobs/' . $jobId, $jobInfo); - } - - /** - * Stop job: /rest/v1/:userId/jobs/:jobId/stop (PUT) - * - * @param string $jobId - * - * @return array - */ - public function stopJob($jobId) - { - return $this->execute('PUT', $this->userId . '/jobs/' . $jobId . '/stop'); - } - - /** - * Delete job: /rest/v1/:userId/jobs/:jobId (DELETE) - * - * @param string $jobId - * - * @return array - */ - public function deleteJob($jobId) - { - return $this->execute('DELETE', $this->userId . '/jobs/' . $jobId); - } - - /** - * Get running tunnels for a given user: /rest/v1/:userId/tunnels (GET) - * - * @return array - */ - public function getTunnels() - { - return $this->execute('GET', $this->userId . '/tunnels'); - } - - /** - * Get full information for a tunnel: /rest/v1/:userId/tunnels/:tunnelId (GET) - * - * @param string $tunnelId - * - * @return array - */ - public function getTunnel($tunnelId) - { - return $this->execute('GET', $this->userId . '/tunnels/' . $tunnelId); - } - - /** - * Shut down a tunnel: /rest/v1/:userId/tunnels/:tunnelId (DELETE) - * - * @param string $tunnelId - * - * @return array - */ - public function shutdownTunnel($tunnelId) - { - return $this->execute('DELETE', $this->userId . '/tunnels/' . $tunnelId); - } - - /** - * Get current status of Sauce Labs' services: /rest/v1/info/status (GET) - * - * @return array array('wait_time' => ..., 'service_operational' => ..., 'status_message' => ...) - */ - public function getStatus() - { - return $this->execute('GET', 'info/status'); - } - - /** - * Get currently supported browsers: /rest/v1/info/browsers (GET) - * - * @param string $termination Optional termination (one of "all", "selenium-rc", or "webdriver') - * - * @return array - */ - public function getBrowsers($termination = '') - { - if ($termination) { - return $this->execute('GET', 'info/browsers/' . $termination); - } - - return $this->execute('GET', 'info/browsers'); - } - - /** - * Get number of tests executed so far on Sauce Labs: /rest/v1/info/counter (GET) - * - * @return array - */ - public function getCounter() - { - return $this->execute('GET', 'info/counter'); - } -} diff --git a/lib/WebDriver/Service/CurlService.php b/lib/WebDriver/Service/CurlService.php index eac0a5f5..1adeeedc 100755 --- a/lib/WebDriver/Service/CurlService.php +++ b/lib/WebDriver/Service/CurlService.php @@ -30,20 +30,20 @@ class CurlService implements CurlServiceInterface * * @param mixed $defaultOptions */ - public function __construct($defaultOptions = array()) + public function __construct($defaultOptions = []) { - $this->defaultOptions = is_array($defaultOptions) ? $defaultOptions : array(); + $this->defaultOptions = is_array($defaultOptions) ? $defaultOptions : []; } /** * {@inheritdoc} */ - public function execute($requestMethod, $url, $parameters = null, $extraOptions = array()) + public function execute($requestMethod, $url, $parameters = null, $extraOptions = []) { - $customHeaders = array( + $customHeaders = [ 'Content-Type: application/json;charset=utf-8', 'Accept: application/json', - ); + ]; $curl = curl_init($url); curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); @@ -113,6 +113,6 @@ public function execute($requestMethod, $url, $parameters = null, $extraOptions curl_close($curl); - return array($rawResult, $info); + return [$rawResult, $info]; } } diff --git a/lib/WebDriver/Service/CurlServiceInterface.php b/lib/WebDriver/Service/CurlServiceInterface.php index 37d95311..7589a9b9 100755 --- a/lib/WebDriver/Service/CurlServiceInterface.php +++ b/lib/WebDriver/Service/CurlServiceInterface.php @@ -23,7 +23,7 @@ interface CurlServiceInterface * * @param string $requestMethod HTTP request method, e.g., 'GET', 'POST', or 'DELETE' * @param string $url Request URL - * @param array $parameters If an array(), they will be posted as JSON parameters + * @param array $parameters If an array, they will be posted as JSON parameters * If a number or string, "/$params" is appended to url * @param array $extraOptions key=>value pairs of curl options to pass to curl_setopt() * @@ -31,5 +31,5 @@ interface CurlServiceInterface * * @throws \WebDriver\Exception\CurlExec only if http error and CURLOPT_FAILONERROR has been set in extraOptions */ - public function execute($requestMethod, $url, $parameters = null, $extraOptions = array()); + public function execute($requestMethod, $url, $parameters = null, $extraOptions = []); } diff --git a/lib/WebDriver/ServiceFactory.php b/lib/WebDriver/ServiceFactory.php index fb806bb9..3842bbcd 100755 --- a/lib/WebDriver/ServiceFactory.php +++ b/lib/WebDriver/ServiceFactory.php @@ -11,6 +11,8 @@ namespace WebDriver; +use WebDriver\Service\CurlService; + /** * WebDriver\ServiceFactory class * @@ -42,11 +44,11 @@ class ServiceFactory */ private function __construct() { - $this->services = array(); + $this->services = []; - $this->serviceClasses = array( - 'service.curl' => '\\WebDriver\\Service\\CurlService', - ); + $this->serviceClasses = [ + 'service.curl' => CurlService::class, + ]; } /** diff --git a/lib/WebDriver/Session.php b/lib/WebDriver/Session.php index c8825eec..766dad75 100644 --- a/lib/WebDriver/Session.php +++ b/lib/WebDriver/Session.php @@ -11,43 +11,44 @@ namespace WebDriver; +use WebDriver\Extension\ChromeDevTools; +use WebDriver\Extension\FederatedCredentialManagementAPI; +use WebDriver\Extension\Selenium; +use WebDriver\Extension\WebAuthenticationAPI; + /** * WebDriver\Session class * * @package WebDriver * - * @method void accept_alert() Accepts the currently displayed alert dialog. + * W3C * @method array deleteActions() Release actions. - * @method array postActions($json) Perform actions. - * @method string getAlert_text() Gets the text of the currently displayed JavaScript alert(), confirm(), or prompt() dialog. - * @method void postAlert_text($jsonText) Sends keystrokes to a JavaScript prompt() dialog. + * @method array postActions($parameters) Perform actions. * @method void back() Navigates backward in the browser history, if possible. - * @method boolean getBrowser_connection() Is browser online? - * @method void postBrowser_connection($jsonState) Set browser online. - * @method void buttondown() Click and hold the left mouse button (at the coordinates set by the last moveto command). - * @method void buttonup() Releases the mouse button previously held (where the mouse is currently at). - * @method void click($jsonButton) Click any mouse button (at the coordinates set by the last moveto command). * @method array getCookie() Retrieve all cookies visible to the current page. - * @method array postCookie($jsonCookie) Set a cookie. - * @method void dismiss_alert() Dismisses the currently displayed alert dialog. - * @method void doubleclick() Double-clicks at the current mouse coordinates (set by moveto). - * @method array execute_sql($jsonQuery) Execute SQL. - * @method array file($jsonFile) Upload file. + * @method array postCookie($parameters) Set a cookie. * @method void forward() Navigates forward in the browser history, if possible. - * @method void keys($jsonKeys) Send a sequence of key strokes to the active element. - * @method array getLocation() Get the current geo location. - * @method void postLocation($jsonCoordinates) Set the current geo location. - * @method string getOrientation() Get the current browser orientation. - * @method void postOrientation($jsonOrientation) Set the current browser orientation. * @method array print() Print page. * @method void refresh() Refresh the current page. * @method string screenshot() Take a screenshot of the current page. * @method string source() Get the current page source. * @method string title() Get the current page title. * @method string url() Retrieve the URL of the current page. - * @method void postUrl($jsonUrl) Navigate to a new URL. - * @method string window_handle() Retrieve the current window handle. - * @method array window_handles() Retrieve the list of all window handles available to the session. + * @method void postUrl($parameters) Navigate to a new URL. + * Selenium + * @method boolean getBrowser_connection() Is browser online? + * @method void postBrowser_connection($parameters) Set browser online. + * @method array context() Get current context handle. + * @method void postContext() Switch to context. + * @method array contexts() Get context handles. + * @method array getLocation() Get the current geo location. + * @method void postLocation($parameters) Set the current geo location. + * @method string getNetworkConnection() Get the network connection. + * @method void postNetworkConnection($parameters) Set the network connection. + * @method string getOrientation() Get the current browser orientation. + * @method void postOrientation($parameters) Set the current browser orientation. + * @method string getRotation() Get screen rotation. + * @method void postOrientation($parameters) Set screen rotation. */ class Session extends Container { @@ -57,83 +58,113 @@ class Session extends Container private $capabilities = null; /** - * @var boolean + * {@inheritdoc} */ - private $w3c = null; + protected function methods() + { + return [ + 'actions' => ['POST', 'DELETE'], + 'back' => ['POST'], + 'cookie' => ['GET', 'POST'], // for DELETE, use deleteAllCookies() + 'forward' => ['POST'], + 'print' => ['POST'], + 'refresh' => ['POST'], + 'screenshot' => ['GET'], + 'source' => ['GET'], + 'title' => ['GET'], + 'url' => ['GET', 'POST'], // alternate for POST, use open($url) + + // @deprecated + 'browser_connection' => ['GET', 'POST'], + 'context' => ['GET', 'POST'], + 'contexts' => ['GET'], + 'location' => ['GET', 'POST'], + 'network_connection' => ['GET', 'POST'], + 'orientation' => ['GET', 'POST'], + 'rotation' => ['GET', 'POST'], + ]; + } /** * {@inheritdoc} */ - protected function methods() + public function aliases() { - return array( - 'actions' => array('POST', 'DELETE'), - 'back' => array('POST'), - 'cookie' => array('GET', 'POST'), // for DELETE, use deleteAllCookies() - 'forward' => array('POST'), - 'print' => array('POST'), - 'refresh' => array('POST'), - 'screenshot' => array('GET'), - 'source' => array('GET'), - 'title' => array('GET'), - 'url' => array('GET', 'POST'), // alternate for POST, use open($url) - - // specific to Java SeleniumServer - 'file' => array('POST'), - - // Legacy JSON Wire Protocol - 'accept_alert' => array('POST'), - 'alert_text' => array('GET', 'POST'), - 'browser_connection' => array('GET', 'POST'), - 'buttondown' => 'POST', - 'buttonup' => array('POST'), - 'click' => array('POST'), - 'dismiss_alert' => array('POST'), - 'doubleclick' => array('POST'), - 'execute_sql' => array('POST'), - 'keys' => array('POST'), - 'location' => array('GET', 'POST'), - 'orientation' => array('GET', 'POST'), - 'window_handle' => array('GET'), // see also getWindowHandle() - 'window_handles' => array('GET'), - ); + return [ + 'activeElement' => 'getActiveElement', + 'addCookie' => 'setCookie', + 'capabilities' => 'getCapabilities', + 'closeWindow' => 'deleteWindow', + 'focusWindow' => 'switchToWindow', + 'get' => 'open', + 'getCurrentUrl' => 'getUrl', + 'getPageSource' => 'source', + 'getPageTitle' => 'title', + 'goBack' => 'back', + 'goForward' => 'forward', + 'goTo' => 'open', + 'navigateTo' => 'open', + 'printPage' => 'print', + 'quit' => 'close', + + // deprecated + 'application_cache' => 'applicationCache', + 'execute_async' => 'executeAsyncScript', + 'isBrowserOnline' => 'getBrowserConnection', + 'setBrowserOnline' => 'setBrowserConnection', + 'window_handle' => 'getWindowHandle', + 'window_handles' => 'getWindowHandles', + ]; } /** * {@inheritdoc} */ - protected function obsoleteMethods() + protected function chainable() { - return array( - 'alert' => array('GET'), - 'modifier' => array('POST'), - 'speed' => array('GET', 'POST'), - 'visible' => array('GET', 'POST'), - ); + return [ + 'actions' => 'actions', + 'alert' => 'alert', + 'execute' => 'execute', + 'frame' => 'frame', + 'timeouts' => 'timeouts', + 'window' => 'window', + ]; } /** * Constructor * - * @param string $url URL to Selenium server + * @param string $url * @param array|null $capabilities */ public function __construct($url, $capabilities) { + // $url already includes :sessionId parent::__construct($url); $this->capabilities = $capabilities; - $this->w3c = !! $capabilities; + + $this->register('cdp', ChromeDevTools::class, 'goog/cdp'); + $this->register('fedcm', FederatedCredentialManagementAPI::class, 'fedcm'); + $this->register('selenium', Selenium::class, 'se'); + $this->register('webauthn', WebAuthenticationAPI::class, 'webauthn/authenticator'); } /** - * Is W3C webdriver? + * Get browser capabilities: /session/:sessionId (GET) * - * @return boolean + * @return mixed */ - public function isW3C() + public function getCapabilities() { - return $this->w3c; + if ($this->capabilities === null) { + $result = $this->curl('GET', ''); + + $this->capabilities = $result['value']; + } + + return $this->capabilities; } /** @@ -146,27 +177,11 @@ public function isW3C() */ public function open($url) { - $this->curl('POST', '/url', array('url' => $url)); + $this->curl('POST', '/url', ['url' => $url]); return $this; } - /** - * Get browser capabilities: /session/:sessionId (GET) - * - * @return mixed - */ - public function capabilities() - { - if (! isset($this->capabilities)) { - $result = $this->curl('GET', ''); - - $this->capabilities = $result['value']; - } - - return $this->capabilities; - } - /** * Close session: /session/:sessionId (DELETE) * @@ -200,15 +215,19 @@ public function getAllCookies() /** * Set cookie: /session/:sessionId/cookie (POST) - * Alternative to: $session->cookie($cookie_json); + * Alternative to: $session->cookie($parameters); * - * @param array $cookieJson + * @param array $cookieData * * @return \WebDriver\Session */ - public function setCookie($cookieJson) + public function setCookie($cookieData) { - $this->curl('POST', '/cookie', array('cookie' => $cookieJson)); + $parameters = array_key_exists('cookie', $cookieData) + ? $cookieData + : ['cookie' => $cookieData]; + + $this->curl('POST', '/cookie', $parameters); return $this; } @@ -241,7 +260,6 @@ public function deleteCookie($cookieName) /** * Get window handle: /session/:sessionId/window (GET) - * : /session/:sessionId/window_handle (GET) * - $session->getWindowHandle() * * An alternative to $session->window()->getHandle() @@ -250,7 +268,20 @@ public function deleteCookie($cookieName) */ public function getWindowHandle() { - $result = $this->curl('GET', $this->w3c ? '/window' : '/window_handle'); + $result = $this->curl('GET', '/window'); + + return $result['value']; + } + + /** + * Get window handles: /session/:sessionId/window/handles (GET) + * - $session->getWindowHandles() + * + * @return mixed + */ + public function getWindowHandles() + { + $result = $this->curl('GET', '/window/handles'); return $result['value']; } @@ -261,24 +292,28 @@ public function getWindowHandle() * * @internal "new" is a reserved keyword in PHP, so $session->window()->new() isn't possible * + * @param array|string $type Type or Paramters {type: ...} + * * @return \WebDriver\Window */ public function newWindow($type) { - $arg = func_get_arg(0); // json + $parameters = is_array($type) + ? $type + : ['type' => $type]; - $result = $this->curl('POST', '/window/new', $arg); + $result = $this->curl('POST', '/window/new', $parameters); return $result['value']; } /** - * window method chaining: /session/:sessionId/window (POST - * - $session->window($jsonHandle) - set focus - * - $session->window($handle)->method() - chaining + * window method chaining: /session/:sessionId/window (POST) + * - $session->window($parameters) - set focus + * - $session->window($parameters)->method() - chaining * - $session->window()->method() - chaining * - * @return \WebDriver\Session|\WebDriver\Window|\WebDriver\LegacyWindow + * @return \WebDriver\Session|\WebDriver\Window */ public function window() { @@ -286,17 +321,11 @@ public function window() // set window focus / switch to window if (func_num_args() === 1) { - $arg = func_get_arg(0); // window handle or name attribute - - if (is_array($arg)) { - $this->curl('POST', '/window', $arg); - - return $this; - } + return $this->switchToWindow(func_get_arg(0)); } // chaining (with optional handle in $arg) - return $this->w3c ? new Window($this->url . '/window', $arg) : new LegacyWindow($this->url . '/window', $arg); + return new Window($this->url . '/window', $arg); } /** @@ -312,32 +341,40 @@ public function deleteWindow() } /** - * Set focus to window: /session/:sessionId/window (POST) + * Switch to window: /session/:sessionId/window (POST) * * @param mixed $handle window handle (or legacy name) attribute * * @return \WebDriver\Session */ - public function focusWindow($handle) + public function switchToWindow($handle) { - $this->curl('POST', '/window', array('handle' => $handle, 'name' => $handle)); + $parameters = is_array($handle) + ? $handle + : ['handle' => $handle, 'name' => $handle]; + + $this->curl('POST', '/window', $parameters); return $this; } /** * frame methods: /session/:sessionId/frame (POST) - * - $session->frame($json) - change focus to another frame on the page + * - $session->frame($parameters) - change focus to another frame on the page * - $session->frame()->method() - chaining * + * @param array|string $id + * * @return \WebDriver\Session|\WebDriver\Frame */ - public function frame() + public function frame($id = null) { if (func_num_args() === 1) { - $arg = func_get_arg(0); // json + $parameters = is_array($id) + ? $id + : ['id' => $id]; - $this->curl('POST', '/frame', $arg); + $this->curl('POST', '/frame', $parameters); return $this; } @@ -346,46 +383,20 @@ public function frame() return new Frame($this->url . '/frame'); } - /** - * Get timeouts (W3C): /session/:sessionId/timeouts (GET) - * - $session->getTimeouts() - * - * @return mixed - */ - public function getTimeouts() - { - $result = $this->curl('GET', '/timeouts'); - - return $result['value']; - } - /** * timeouts methods: /session/:sessionId/timeouts (POST) - * - $session->timeouts($json) - set timeout for an operation + * - $session->timeouts($parameters) - set timeout for an operation * - $session->timeouts()->method() - chaining * * @return \WebDriver\Session|\WebDriver\Timeouts */ public function timeouts() { - // set timeouts - if (func_num_args() === 1) { - $arg = func_get_arg(0); // json - - $this->curl('POST', '/timeouts', $arg); - - return $this; - } - - if (func_num_args() === 2) { - $type = func_get_arg(0); // 'script', 'implicit', or 'pageLoad' (legacy: 'pageLoad') - $timeout = func_get_arg(1); // timeout in milliseconds - - $arg = $this->w3c ? array($type => $timeout) : array('type' => $type, 'ms' => $timeout); - - $this->curl('POST', '/timeouts', $arg); + // @deprecated + if (func_num_args() > 0) { + // trigger_error(__METHOD__ . ': use "Timeouts::setTimeout()" instead', E_USER_DEPRECATED); - return $this; + return call_user_func_array([$this, 'setTimeout'], func_get_args()); } // chaining @@ -393,14 +404,25 @@ public function timeouts() } /** - * ime method chaining, e.g., - * - $session->ime()->method() + * Get timeouts: /session/:sessionId/timeouts (GET) + * - $session->getTimeouts() + * + * @return mixed + */ + public function getTimeouts() + { + return call_user_func([$this->timeouts(), 'getTimeouts']); + } + + /** + * Set timeout: /session/:sessionId/timeouts (POST) + * - $session->setTimeout() * - * @return \WebDriver\Ime + * @return mixed */ - public function ime() + public function setTimeout() { - return new Ime($this->url . '/ime'); + return call_user_func_array([$this->timeouts(), 'setTimeout'], func_get_args()); } /** @@ -409,7 +431,7 @@ public function ime() * * @return mixed */ - public function activeElement() + public function getActiveElement() { $result = $this->curl('POST', '/element/active'); @@ -417,149 +439,399 @@ public function activeElement() } /** - * touch method chaining, e.g., - * - $session->touch()->method() + * actions method chaining, e.g., + * - $session->actions()->method() - chaining + * - $session->actions->method() - chaining * - * @return \WebDriver\Touch + * @return mixed + */ + protected function actions() + { + return Actions::getInstance($this->url . '/actions'); + } + + /** + * alert method chaining, e.g., + * - $session->alert()->method() - chaining + * - $session->alert->method() - chaining * + * @return mixed */ - public function touch() + protected function alert() { - return new Touch($this->url . '/touch'); + return new Alert($this->url . '/alert'); } /** - * local_storage method chaining, e.g., - * - $session->local_storage()->method() + * script execution method chaining, e.g., + * - $session->execute($parameters) - execute script + * - $session->execute()->method() - chaining * - * @return \WebDriver\Storage\Local + * @return mixed */ - public function localStorage() + public function execute() { - return new Storage\Local($this->url . '/local_storage'); + // @deprecated + if (func_num_args() > 0) { + // trigger_error(__METHOD__ . ': use "Execute::sync()" instead', E_USER_DEPRECATED); + + return call_user_func_array([$this, 'executeScript'], func_get_args()); + } + + return new Execute($this->url); } /** - * session_storage method chaining, e.g., - * - $session->session_storage()->method() + * async script execution: /session/:sessionId/execute_async + * + * @deprecated * - * @return \WebDriver\Storage\Session + * @return mixed */ - public function sessionStorage() + public function executeAsyncScript() { - return new Storage\Session($this->url . '/session_storage'); + // trigger_error(__METHOD__ . ': use "Execute::async()" instead', E_USER_DEPRECATED); + + return call_user_func_array([$this->execute(), 'async'], func_get_args()); + } + + /** + * sync script execution: /session/:sessionId/execute + * + * @deprecated + * + * @return mixed + */ + public function executeScript() + { + // trigger_error(__METHOD__ . ': use "Execute::sync()" instead', E_USER_DEPRECATED); + + return call_user_func_array([$this->execute(), 'sync'], func_get_args()); } /** * application cache chaining, e.g., * - $session->application_cache()->method() - chaining * + * @deprecated + * * @return \WebDriver\ApplicationCache */ public function applicationCache() { + // trigger_error(__METHOD__, E_USER_DEPRECATED); + return new ApplicationCache($this->url . '/application_cache'); } /** - * log methods: /session/:sessionId/log (POST) - * - $session->log($type) - get log for given log type - * - $session->log()->method() - chaining + * Move the mouse: /session/:sessionId/moveto (POST) + * + * @deprecated + * + * @param array $parameters * * @return mixed */ - public function log() + public function moveto($parameters) { - // get log for given log type - if (func_num_args() === 1) { - $arg = func_get_arg(0); - - if (is_string($arg)) { - $arg = array( - 'type' => $arg, - ); + // trigger_error(__METHOD__ . ': use actions() API instead', E_USER_DEPRECATED); + + try { + $result = $this->curl('POST', '/moveto', $parameters); + } catch (\Exception $e) { + if (! array_key_exists('element', $parameters)) { + $actionItem = [ + 'x' => $parameters['xoffset'], + 'y' => $parameters['yoffset'], + ]; + } else { + $element = new Element($this->url . '/element', $parameters['element']); + $rect = $element->rect(); + + $actionItem = [ + 'x' => $rect['x'] + ($parameters['xoffset'] ?? $rect['width'] / 2), + 'y' => $rect['y'] + ($parameters['yoffset'] ?? $rect['height'] / 2), + ]; } - $result = $this->curl('POST', '/log', $arg); + $mouse = $this->actions()->getPointerInput(0, PointerInput::MOUSE); - return $result['value']; + $result = $this->actions() + ->addAction($mouse->pointerMove($actionItem)) + ->perform(); } - // chaining - return new Log($this->url . '/log'); + return $result['value']; } /** - * alert method chaining, e.g., - * - $session->alert()->method() - chaining + * Click any mouse button: /session/:sessionId/click (POST) + * + * @deprecated + * + * @param array $parameters Parameters {button: ...} * * @return mixed */ - public function alert() + public function click($parameters) { - return new Alert($this->url . '/alert'); + // trigger_error(__METHOD__ . ': use actions() API instead', E_USER_DEPRECATED); + + try { + $result = $this->curl('POST', '/click', $parameters); + } catch (\Exception $e) { + $mouse = $this->actions()->getPointerInput(0, PointerInput::MOUSE); + + $result = $this->actions() + ->addAction($mouse->pointerDown([ + 'button' => PointerInput::LEFT_BUTTON, + ])) + ->addAction($mouse->pointerUp([ + 'button' => PointerInput::LEFT_BUTTON, + ])) + ->perform(); + } + + return $result['value']; } /** - * script execution method chaining, e.g., - * - $session->execute($jsonScript) - fallback for legacy JSON Wire Protocol - * - $session->execute()->method() - chaining + * Double click left mouse button: /session/:sessionId/doubleclick (POST) + * + * @deprecated + * + * @param array $parameters Parameters * * @return mixed */ - public function execute() + public function doubleclick($parameters) { - // execute script - if (func_num_args() > 0) { - $execute = $this->w3c ? new Execute($this->url . '/execute') : new LegacyExecute($this->url); - $result = $execute->sync(func_get_arg(0)); + // trigger_error(__METHOD__ . ': use actions() API instead', E_USER_DEPRECATED); + + try { + $result = $this->curl('POST', '/doubleclick', $parameters); + } catch (\Exception $e) { + $mouse = $this->actions()->getPointerInput(0, PointerInput::MOUSE); + + $result = $this->actions() + ->addAction($mouse->pointerDown([ + 'button' => PointerInput::LEFT_BUTTON, + ])) + ->addAction($mouse->pointerUp([ + 'button' => PointerInput::LEFT_BUTTON, + ])) + ->addAction($mouse->pointerDown([ + 'button' => PointerInput::LEFT_BUTTON, + ])) + ->addAction($mouse->pointerUp([ + 'button' => PointerInput::LEFT_BUTTON, + ])) + ->perform(); + } - return $result; + return $result['value']; + } + + /** + * Click and hold left mouse button down: /session/:sessionId/buttondown (POST) + * + * @deprecated + * + * @param array $parameters Parameters {button: ...} + * + * @return mixed + */ + public function buttondown($parameters) + { + // trigger_error(__METHOD__ . ': use actions() API instead', E_USER_DEPRECATED); + + try { + $result = $this->curl('POST', '/buttondown', $parameters); + } catch (\Exception $e) { + $mouse = $this->actions()->getPointerInput(0, PointerInput::MOUSE); + + $result = $this->actions() + ->addAction($mouse->pointerDown([ + 'button' => PointerInput::LEFT_BUTTON, + ])) + ->perform(); } - // W3C method chaining - return new Execute($this->url . '/execute'); + return $result['value']; } /** - * async script execution - * - $session->execute_async($jsonScript) + * Release mouse button: /session/:sessionId/buttonup (POST) + * + * @deprecated + * + * @param array $parameters Parameters {button: ...} * * @return mixed */ - public function executeAsync() + public function buttonup($parameters) { - $execute = $this->w3c ? new Execute($this->url . '/execute') : new LegacyExecute($this->url); - $result = $execute->async(func_get_arg(0)); + // trigger_error(__METHOD__ . ': use actions() API instead', E_USER_DEPRECATED); + + try { + $result = $this->curl('POST', '/buttonup', $parameters); + } catch (\Exception $e) { + $mouse = $this->actions()->getPointerInput(0, PointerInput::MOUSE); + + $result = $this->actions() + ->addAction($mouse->pointerUp([ + 'button' => PointerInput::LEFT_BUTTON, + ])) + ->perform(); + } - return $result; + return $result['value']; } /** - * {@inheritdoc} + * Gets the text of the currenty displayed Javascript dialog: /session/:sessionId/alert_text (GET) + * + * @deprecated + * + * @return mixed */ - protected function getIdentifierPath($identifier) + public function getAlertText() { - return sprintf('%s/element/%s', $this->url, $identifier); + // trigger_error(__METHOD__ . ': use "Alert::getAlertText()" instead', E_USER_DEPRECATED); + + try { + $result = $this->curl('GET', '/alert_text'); + } catch (\Exception $e) { + $result = $this->alert()->getAlertText(); + } + + return $result['value']; } /** - * {@inheritdoc} + * Send keystrokes to a Javascript dialog: /session/:sessionId/alert_text (POST) + * + * @deprecated + * + * @param array|string $text Parameters {text: ...} + * + * @return mixed */ - public function __call($name, $arguments) + public function postAlertText($text) { - $map = [ - 'application_cache' => 'applicationCache', - 'execute_async' => 'executeAsync', - 'local_storage' => 'localStorage', - 'session_storage' => 'sessionStorage', - ]; + $parameters = is_array($text) + ? $text + : ['text' => $text]; + + // trigger_error(__METHOD__ . ': use "Alert::setAlertText()" instead', E_USER_DEPRECATED); + + try { + $result = $this->curl('POST', '/alert_text', $parameters); + } catch (\Exception $e) { + $result = $this->alert()->setAlertText(); + } - if (array_key_exists($name, $map)) { - return call_user_func_array([$this, $map[$name]], $arguments); + return $result['value']; + } + + /** + * Accepts the currently displayed alert dialog: /session/:sessionId/accept_alert (POST) + * + * @deprecated + * + * @return mixed + */ + public function acceptAlert() + { + // trigger_error(__METHOD__ . ': use "Alert::acceptAlert()" instead', E_USER_DEPRECATED); + + try { + $result = $this->curl('POST', '/accept_alert'); + } catch (\Exception $e) { + $result = $this->alert()->acceptAlert(); } - // fallback to executing WebDriver commands - return parent::__call($name, $arguments); + return $result['value']; + } + + /** + * Dismisses the currently displayed alert dialog: /session/:sessionId/dismiss_alert (POST) + * + * @deprecated + * + * @return mixed + */ + public function dismissAlert() + { + // trigger_error(__METHOD__ . ': use "Alert::dismissAlert()" instead', E_USER_DEPRECATED); + + try { + $result = $this->curl('POST', '/dismiss_alert'); + } catch (\Exception $e) { + $result = $this->alert()->dismissAlert(); + } + + return $result['value']; + } + + /** + * Send a sequence of key strokes to the active element.: /session/:sessionId/keys (POST) + * + * @deprecated + * + * @param array|string $value Value or Parameters {value: ...} + * + * @return mixed + */ + public function keys($value) + { + $parameters = is_array($value) + ? $value + : ['value' => $value]; + + // trigger_error(__METHOD__ . ': use "Element::sendKeys()" instead', E_USER_DEPRECATED); + + try { + $result = $this->curl('POST', '/keys', $parameters); + } catch (\Exception $e) { + $result = $this->getActiveElement()->sendKeys($parameters); + } + + return $result['value']; + } + + /** + * Upload file: /session/:sessionId/file (POST) + * + * @deprecated + * + * @param array|string $file Parameters {file: ...} + * + * @return mixed + */ + public function file($file) + { + $parameters = is_array($file) + ? $file + : ['file' => $file]; + + // trigger_error(__METHOD__ . ': use "Selenium::uploadFile()" instead', E_USER_DEPRECATED); + + try { + $result = $this->curl('POST', '/file', $parameters); + } catch (\Exception $e) { + $result = $this->selenium()->uploadFile($parameters); + } + + return $result['value']; + } + + /** + * {@inheritdoc} + */ + protected function getNewIdentifierPath($identifier) + { + return $this->url . "/element/$identifier"; } } diff --git a/lib/WebDriver/Shadow.php b/lib/WebDriver/Shadow.php index b106e0d0..b644c7f9 100644 --- a/lib/WebDriver/Shadow.php +++ b/lib/WebDriver/Shadow.php @@ -15,8 +15,6 @@ * WebDriver\Shadow class * * @package WebDriver - * - * @deprecated by W3C WebDriver */ class Shadow extends Container { @@ -29,14 +27,6 @@ class Shadow extends Container */ private $id; - /** - * {@inheritdoc} - */ - protected function methods() - { - return array(); - } - /** * Constructor * @@ -45,7 +35,7 @@ protected function methods() */ public function __construct($url, $id) { - parent::__construct($url); + parent::__construct($url . "/$id"); $this->id = $id; } @@ -63,8 +53,8 @@ public function getID() /** * {@inheritdoc} */ - protected function getIdentifierPath($identifier) + protected function getNewIdentifierPath($identifier) { - return sprintf('%s/element/%s', $this->url, $identifier); + return preg_replace('~/shadow/' . preg_quote($this->id) . '$~', "/element/$identifier", $this->url); } } diff --git a/lib/WebDriver/Storage/AbstractStorage.php b/lib/WebDriver/Storage/AbstractStorage.php deleted file mode 100644 index cede7d69..00000000 --- a/lib/WebDriver/Storage/AbstractStorage.php +++ /dev/null @@ -1,114 +0,0 @@ - - */ - -namespace WebDriver\Storage; - -use WebDriver\AbstractWebDriver; -use WebDriver\Exception as WebDriverException; - -/** - * WebDriver\AbstractStorage class - * - * @package WebDriver - * - * @method mixed getKey($key) Get key/value pair. - * @method void deleteKey($key) Delete a specific key. - * @method integer size() Get the number of items in the storage. - */ -abstract class AbstractStorage extends AbstractWebDriver -{ - /** - * {@inheritdoc} - */ - protected function methods() - { - return array( - 'key' => array('GET', 'DELETE'), - 'size' => array('GET'), - ); - } - - /** - * Get all keys from storage or a specific key/value pair - * - * @return mixed - */ - public function get() - { - // get all keys - if (func_num_args() === 0) { - $result = $this->curl('GET', ''); - - return $result['value']; - } - - // get key/value pair - if (func_num_args() === 1) { - return $this->getKey(func_get_arg(0)); - } - - throw WebDriverException::factory(WebDriverException::UNEXPECTED_PARAMETERS); - } - - /** - * Set specific key/value pair - * - * @return \WebDriver\Storage\AbstractStorage - * - * @throw \WebDriver\Exception\UnexpectedParameters if unexpected parameters - */ - public function set() - { - if (func_num_args() === 1 && is_array($arg = func_get_arg(0))) { - $this->curl('POST', '', $arg); - - return $this; - } - - if (func_num_args() === 2) { - $arg = array( - 'key' => func_get_arg(0), - 'value' => func_get_arg(1), - ); - $this->curl('POST', '', $arg); - - return $this; - } - - throw WebDriverException::factory(WebDriverException::UNEXPECTED_PARAMETERS); - } - - /** - * Delete storage or a specific key - * - * @return \WebDriver\Storage\AbstractStorage - * - * @throw \WebDriver\Exception\UnexpectedParameters if unexpected parameters - */ - public function delete() - { - // delete storage - if (func_num_args() === 0) { - $this->curl('DELETE', ''); - - return $this; - } - - // delete key from storage - if (func_num_args() === 1) { - $this->deleteKey(func_get_arg(0)); - - return $this; - } - - throw WebDriverException::factory(WebDriverException::UNEXPECTED_PARAMETERS); - } -} diff --git a/lib/WebDriver/Storage/Local.php b/lib/WebDriver/Storage/Local.php deleted file mode 100644 index f25451e8..00000000 --- a/lib/WebDriver/Storage/Local.php +++ /dev/null @@ -1,21 +0,0 @@ - - */ - -namespace WebDriver\Storage; - -/** - * WebDriver\Storage\Local class - * - * @package WebDriver - */ -class Local extends AbstractStorage -{ -} diff --git a/lib/WebDriver/Storage/Session.php b/lib/WebDriver/Storage/Session.php deleted file mode 100644 index ffcf8189..00000000 --- a/lib/WebDriver/Storage/Session.php +++ /dev/null @@ -1,21 +0,0 @@ - - */ - -namespace WebDriver\Storage; - -/** - * WebDriver\Storage\Session class - * - * @package WebDriver - */ -class Session extends AbstractStorage -{ -} diff --git a/lib/WebDriver/Timeouts.php b/lib/WebDriver/Timeouts.php index 57a8de92..4f105ae6 100644 --- a/lib/WebDriver/Timeouts.php +++ b/lib/WebDriver/Timeouts.php @@ -18,21 +18,115 @@ * * @package WebDriver * - * @method void async_script($json) Set the amount of time, in milliseconds, that asynchronous scripts (executed by execute_async) are permitted to run before they are aborted and a timeout error is returned to the client. - * @method void implicit_wait($json) Set the amount of time the driver should wait when searching for elements. + * Selenium + * @method void async_script($parameters) Set the amount of time, in milliseconds, that asynchronous scripts (executed by execute_async) are permitted to run before they are aborted and a timeout error is returned to the client. + * @method void implicit_wait($parameters) Set the amount of time the driver should wait when searching for elements. */ class Timeouts extends AbstractWebDriver { + // Timeout name constants + const IMPLICIT = 'implicit'; + const PAGE_LOAD = 'pageLoad'; + const SCRPT = 'script'; + /** * {@inheritdoc} */ protected function methods() { - return array( - // Legacy JSON Wire Protocol - 'async_script' => array('POST'), - 'implicit_wait' => array('POST'), - ); + return [ + // @deprecated + 'async_script' => ['POST'], + 'implicit_wait' => ['POST'], + ]; + } + + /** + * {@inheritdoc} + */ + protected function aliases() + { + return [ + // @deprecated + 'async_script' => 'setScriptTimeout', + 'implicit_wait' => 'implicitlyWait', + ]; + } + + /** + * Get timeouts: /session/:sessionId/timeouts (GET) + * + * @return mixed + */ + public function getTimeouts() + { + $result = $this->curl('GET', ''); + + return $result['value']; + } + + /** + * Implicitly wait: /session/:sessionId/timeout/implicit_wait (POST) + * + * @deprecated + * + * @param array|integer $ms Parameters {ms: ...} + * + * @return \WebDriver\Timeouts + */ + public function implicitlyWait($ms) + { + $parameters = is_array($ms) + ? $ms + : ['ms' => $ms]; + + // trigger_error(__METHOD__ . ': use "setTimeout()" instead', E_USER_DEPRECATED); + + $result = $this->curl('POST', '/implicit_wait', $parameters); + + return $this; + } + + /** + * Set script timeout: /session/:sessionId/timeout/async_script (POST) + * + * @deprecated + * + * @param array|integer $ms Parameters {ms: ...} + * + * @return \WebDriver\Timeouts + */ + public function setScriptTimeout($ms) + { + $parameters = is_array($ms) + ? $ms + : ['ms' => $ms]; + + // trigger_error(__METHOD__ . ': use "setTimeout()" instead', E_USER_DEPRECATED); + + $result = $this->curl('POST', '/async_script', $parameters); + + return $this; + } + + /** + * Set timeout: /session/:sessionId/timeouts (POST) + * + * @param string|array $type Timeout name (see constants above) + * @param integer $timeout Duration in milliseconds + * + * @return \WebDriver\Timeouts + */ + public function setTimeout($type, $timeout = null) + { + // set timeouts + $parameters = func_num_args() === 1 && is_array($type) + ? $type + : [$type => $timeout]; + + $this->curl('POST', '', $parameters); + + return $this; } /** @@ -47,7 +141,7 @@ protected function methods() * * @throws \Exception if thrown by callback, or \WebDriver\Exception\Timeout if helper times out */ - public function wait($callback, $maxIterations = 1, $sleep = 0, $args = array()) + public function wait($callback, $maxIterations = 1, $sleep = 0, $args = []) { $i = max(1, $maxIterations); diff --git a/lib/WebDriver/Touch.php b/lib/WebDriver/Touch.php deleted file mode 100644 index 32c66e90..00000000 --- a/lib/WebDriver/Touch.php +++ /dev/null @@ -1,46 +0,0 @@ - - */ - -namespace WebDriver; - -/** - * WebDriver\Touch class - * - * @package WebDriver - * - * @method void click($jsonElement) Single tap on the touch enabled device. - * @method void doubleclick($jsonElement) Double tap on the touch screen using finger motion events. - * @method void down($jsonCoordinates) Finger down on the screen. - * @method void flick($json) Flick on the touch screen using finger motion events. - * @method void longclick($jsonElement) Long press on the touch screen using finger motion events. - * @method void move($jsonCoordinates) Finger move on the screen. - * @method void scroll($jsonCoordinates) Scroll on the touch screen using finger based motion events. Coordinates are either absolute, or relative to a element (if specified). - * @method void up($jsonCoordinates) Finger up on the screen. - */ -class Touch extends AbstractWebDriver -{ - /** - * {@inheritdoc} - */ - protected function methods() - { - return array( - 'click' => array('POST'), - 'doubleclick' => array('POST'), - 'down' => array('POST'), - 'flick' => array('POST'), - 'longclick' => array('POST'), - 'move' => array('POST'), - 'scroll' => array('POST'), - 'up' => array('POST'), - ); - } -} diff --git a/lib/WebDriver/WebDriver.php b/lib/WebDriver/WebDriver.php index 008a9f47..ce0c074e 100644 --- a/lib/WebDriver/WebDriver.php +++ b/lib/WebDriver/WebDriver.php @@ -12,13 +12,18 @@ namespace WebDriver; /** - * WebDriver class + * WebDriver client class * * @package WebDriver * + * W3C + * @method void session($parameters) New session. * @method array status() Returns information about whether a remote end is in a state in which it can create new sessions. + * Selenium + * @method array logs() Returns session logs mapped to session IDs. + * @method array sessions() Get all sessions. */ -class WebDriver extends AbstractWebDriver implements WebDriverInterface +class WebDriver extends AbstractWebDriver { /** * @var array @@ -30,57 +35,60 @@ class WebDriver extends AbstractWebDriver implements WebDriverInterface */ protected function methods() { - return array( - 'status' => 'GET', - ); + return [ + 'session' => ['POST'], + 'status' => ['GET'], + + // @deprecated + 'logs' => ['POST'], + 'sessions' => ['GET'], + ]; } /** * {@inheritdoc} */ - public function session($browserName = Browser::FIREFOX, $desiredCapabilities = null, $requiredCapabilities = null) + protected function aliases() { - // default to W3C WebDriver API - $firstMatch = $desiredCapabilities ?: array(); - $firstMatch[] = array('browserName' => Browser::CHROME); - - if ($browserName !== Browser::CHROME) { - $firstMatch[] = array('browserName' => $browserName); - } - - $parameters = array('capabilities' => array('firstMatch' => $firstMatch)); - - if (is_array($requiredCapabilities) && count($requiredCapabilities)) { - $parameters['capabilities']['alwaysMatch'] = $requiredCapabilities; - } + return [ + 'session' => 'newSession', + 'status' => 'getStatus', + + // @deprecated + 'logs' => 'getSessionLogs', + 'sessions' => 'getAllSessions', + ]; + } - try { - $result = $this->curl( - 'POST', - '/session', - $parameters, - array(CURLOPT_FOLLOWLOCATION => true) - ); - } catch (\Exception $e) { - // fallback to legacy JSON Wire Protocol - $capabilities = $desiredCapabilities ?: array(); - $capabilities[Capability::BROWSER_NAME] = $browserName; + /** + * New Session: /session (POST) + * Returns a session object suitable for chaining + * + * @param array|string $browserName Preferred browser + * @param array $desiredCapabilities Optional desired capabilities + * @param array $requiredCapabilities Optional required capabilities + * + * @return \WebDriver\Session + */ + public function newSession($browserName = Browser::CHROME, $desiredCapabilities = null, $requiredCapabilities = null) + { + if (func_num_args() === 1 && is_array($browserName)) { + $parameters = $browserName; + } else { + $firstMatch = $desiredCapabilities ?: []; + $firstMatch[] = ['browserName' => $browserName]; - $parameters = array('desiredCapabilities' => $capabilities); + $parameters = ['capabilities' => ['firstMatch' => $firstMatch]]; if (is_array($requiredCapabilities) && count($requiredCapabilities)) { - $parameters['requiredCapabilities'] = $requiredCapabilities; + $parameters['capabilities']['alwaysMatch'] = $requiredCapabilities; } - - $result = $this->curl( - 'POST', - '/session', - $parameters, - array(CURLOPT_FOLLOWLOCATION => true) - ); } - $this->capabilities = isset($result['value']['capabilities']) ? $result['value']['capabilities'] : null; + $options = [CURLOPT_FOLLOWLOCATION => true]; + $result = $this->curl('POST', '/session', $parameters, $options); + + $this->capabilities = $result['value']['capabilities'] ?? $result['value']['capabilities']; $session = new Session($result['sessionUrl'], $this->capabilities); @@ -88,17 +96,18 @@ public function session($browserName = Browser::FIREFOX, $desiredCapabilities = } /** - * Get Sessions: /sessions (GET) + * Get all sessions: /sessions (GET) * Get list of currently active sessions * + * @see https://github.com/SeleniumHQ/selenium/blob/trunk/java/src/org/openqa/selenium/remote/codec/AbstractHttpCommandCodec.java * @deprecated * * @return array an array of \WebDriver\Session objects */ - public function sessions() + public function getAllSessions() { $result = $this->curl('GET', '/sessions'); - $sessions = array(); + $sessions = []; foreach ($result['value'] as $session) { $session = new Session($this->url . '/session/' . $session['id'], $this->capabilities); @@ -108,4 +117,30 @@ public function sessions() return $sessions; } + + /** + * Get session logs: /logs + * + * @deprecated + * + * @return mixed + */ + public function getSessionLogs() + { + $result = $this->curl('POST', '/logs'); + + return $result['value']; + } + + /** + * Get status: /status + * + * @return mixed + */ + public function getStatus() + { + $result = $this->curl('GET', '/status'); + + return $result['value']; + } } diff --git a/lib/WebDriver/WebDriverInterface.php b/lib/WebDriver/WebDriverInterface.php deleted file mode 100644 index 3456a5dc..00000000 --- a/lib/WebDriver/WebDriverInterface.php +++ /dev/null @@ -1,32 +0,0 @@ - - */ - -namespace WebDriver; - -/** - * WebDriverInterface interface - * - * @package WebDriver - */ -interface WebDriverInterface -{ - /** - * New Session: /session (POST) - * Get session object for chaining - * - * @param string $browserName Preferred browser - * @param array $desiredCapabilities Optional desired capabilities - * @param array $requiredCapabilities Optional required capabilities - * - * @return \WebDriver\Session - */ - public function session($browserName = Browser::FIREFOX, $desiredCapabilities = null, $requiredCapabilities = null); -} diff --git a/lib/WebDriver/WheelInput.php b/lib/WebDriver/WheelInput.php new file mode 100644 index 00000000..1af32e04 --- /dev/null +++ b/lib/WebDriver/WheelInput.php @@ -0,0 +1,56 @@ + + */ + +namespace WebDriver; + +/** + * WebDriver\WheelInput class + * + * @package WebDriver + */ +class WheelInput extends NullInput +{ + const TYPE = 'wheel'; + + // actions + const SCROLL = 'scroll'; + + /** + * Scroll + * + * {@internal action item properties: + * x: int - mandatory + * y: int - mandatory + * deltaX: int - mandatory + * deltaY: int - mandatory + * duration: int + * origin: string + * }} + * + * @param array $action Action item + * + * @return array + */ + public function scroll($action) + { + $action = [ + 'type' => self::SCROLL, + ]; + + return [ + 'id' => $this->id, + 'type' => static::TYPE, + 'actions' => [ + $action, + ], + ]; + } +} diff --git a/lib/WebDriver/Window.php b/lib/WebDriver/Window.php index 38efada7..32cd7f7e 100644 --- a/lib/WebDriver/Window.php +++ b/lib/WebDriver/Window.php @@ -16,14 +16,17 @@ * * @package WebDriver * - * @method array handles() Get window handles. + * W3C * @method array fullscreen() Fullscreen window. * @method array maximize() Maximize the window if not already maximized. * @method array minimize() Minimize window. - * @method array getPosition() Get position of the window. - * @method void postPosition($json) Change position of the window. * @method array getRect() Get window rect. - * @method array postRect($json) Set window rect. + * @method array postRect($parameters) Set window rect. + * Selenium + * @method array getPosition() Get window position. + * @method array postPosition($parameters) Set window position. + * @method array getSize() Get window size. + * @method array postSize($parameters) Set window size. */ class Window extends AbstractWebDriver { @@ -39,25 +42,31 @@ class Window extends AbstractWebDriver */ protected function methods() { - return array( - 'handles' => array('GET'), - 'fullscreen' => array('POST'), - 'maximize' => array('POST'), - 'minimize' => array('POST'), - 'position' => array('GET', 'POST'), - 'rect' => array('GET', 'POST'), - ); + return [ + 'fullscreen' => ['POST'], + 'maximize' => ['POST'], + 'minimize' => ['POST'], + 'rect' => ['GET', 'POST'], + + // @deprecated + 'position' => ['GET', 'POST'], + 'size' => ['GET', 'POST'], + ]; } /** * {@inheritdoc} */ - protected function obsoleteMethods() + protected function aliases() { - return array( - // Legacy JSON Wire Protocol - 'size' => array('GET', 'POST'), - ); + return [ + 'getPosition' => 'getRect', + 'getSize' => 'getRect', + 'postPosition' => 'setRect', + 'postSize' => 'sesRect', + 'setPosition' => 'setRect', + 'setSize' => 'setRect', + ]; } /** @@ -73,6 +82,20 @@ public function __construct($url, $windowHandle = null) $this->windowHandle = $windowHandle; } + /** + * Set window rect: /session/:sessionId/window/rect (POST) + * + * @param array $parameters Parameters {width: ..., height: ..., x: ..., y: ...} + * + * @return mixed + */ + public function setRect($parameters) + { + $result = $this->curl('POST', '/rect', $parameters); + + return $result['value']; + } + /** * Get window handle: /session/:sessionId/window (GET) * - $session->window($handle)->getHandle() diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 1c57e1fe..ab990a98 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,4 +1,7 @@ parameters: - level: 4 + level: 5 paths: - lib + ignoreErrors: + - '#Call to an undefined method WebDriver\\Session::selenium\(\)\.#' + diff --git a/test/CI/Travis/setup_apache.sh b/test/CI/Travis/setup_apache.sh deleted file mode 100644 index c33abe77..00000000 --- a/test/CI/Travis/setup_apache.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/sh - -# set up Apache -# @see https://github.com/travis-ci/travis-ci.github.com/blob/master/docs/user/languages/php.md#apache--php - -sudo a2enmod rewrite actions fastcgi alias ssl - -# configure apache root dir -sudo sed -i -e "s,/var/www,$(pwd),g" /etc/apache2/sites-available/default -sudo service apache2 restart diff --git a/test/CI/Travis/setup_selenium.sh b/test/CI/Travis/setup_selenium.sh deleted file mode 100644 index ceb67562..00000000 --- a/test/CI/Travis/setup_selenium.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/sh - -# set up Selenium for functional tests - -wget --max-redirect=1 https://goo.gl/s4o9Vx -O selenium.jar - -java -jar selenium.jar & diff --git a/test/Test/WebDriver/ChromeDriverTest.php b/test/Test/WebDriver/ChromeDriverTest.php index 2fd4fa7b..95269212 100644 --- a/test/Test/WebDriver/ChromeDriverTest.php +++ b/test/Test/WebDriver/ChromeDriverTest.php @@ -1,19 +1,8 @@ + */ + +namespace Test\WebDriver; + +use PHPUnit\Framework\TestCase; +use WebDriver\Element; + +/** + * Test WebDriver\Element class + * + * @package WebDriver + * + * @group Unit + */ +class ElementTest extends TestCase +{ + /** + * test getID + */ + public function testGetID() + { + $element = new Element('http://example.com/session/:sessionId/element', 'element-1234567890'); + + $this->assertEquals('element-1234567890', $element->getID()); + } + + /** + * test getNewIdentifierPath + */ + public function testGetNewIdentifierPath() + { + $element = new Element('http://example.com/session/:sessionId/element', 'element-1234567890'); + + $method = $this->makeCallable($element, 'getNewIdentifierPath'); + + $this->assertEquals('http://example.com/session/:sessionId/element/element-9876543210', $method->invoke($element, 'element-9876543210')); + } + + /** + * test getSessionPath + */ + public function testGetSessionPath() + { + $element = new Element('http://example.com/session/:sessionId/element', 'element-1234567890'); + + $method = $this->makeCallable($element, 'getSessionPath'); + + $this->assertEquals('http://example.com/session/:sessionId', $method->invoke($element)); + } + + /** + * Make private and protected function callable + * + * @param mixed $object Subject under test + * @param string $function Function name + * + * @return \ReflectionMethod + */ + private function makeCallable($object, $function) + { + $method = new \ReflectionMethod($object, $function); + $method->setAccessible(true); + + return $method; + } +} diff --git a/test/Test/WebDriver/ExceptionTest.php b/test/Test/WebDriver/ExceptionTest.php index e9e19b6a..d5e0bc71 100644 --- a/test/Test/WebDriver/ExceptionTest.php +++ b/test/Test/WebDriver/ExceptionTest.php @@ -1,19 +1,8 @@ assertTrue($out instanceof Exception\UnknownError); $this->assertTrue($out->getMessage() === 'wtf'); - $out = Exception::factory(Exception::SUCCESS); + $out = Exception::factory(0); $this->assertTrue($out instanceof Exception\UnknownError); - $this->assertTrue($out->getMessage() === 'An unknown server-side error occurred while processing the command.'); + $this->assertTrue($out->getMessage() === 'An unknown error occurred in the remote end while processing the command.'); $out = Exception::factory(Exception::SESSION_NOT_CREATED); $this->assertTrue($out instanceof Exception\SessionNotCreated); $this->assertTrue(strpos($out->getMessage(), 'A new session could not be created') !== false); - $out = Exception::factory(Exception::CURL_EXEC); + $out = Exception::factory(Exception::_CURL_EXEC); $this->assertTrue($out instanceof Exception\CurlExec); $this->assertTrue($out->getMessage() === 'curl_exec() error.'); } diff --git a/test/Test/WebDriver/GeckoDriverTest.php b/test/Test/WebDriver/GeckoDriverTest.php index 92cef56c..9408b1ba 100644 --- a/test/Test/WebDriver/GeckoDriverTest.php +++ b/test/Test/WebDriver/GeckoDriverTest.php @@ -1,19 +1,8 @@ + */ + +namespace Test\WebDriver; + +use PHPUnit\Framework\TestCase; +use WebDriver\Session; + +/** + * Test WebDriver\Session class + * + * @package WebDriver + * + * @group Unit + */ +class SessionTest extends TestCase +{ + /** + * test getNewIdentifierPath + */ + public function testGetNewIdentifierPath() + { + $session = new Session('http://example.com/session/session-1234567890', []); + + $method = $this->makeCallable($session, 'getNewIdentifierPath'); + + $this->assertEquals('http://example.com/session/session-1234567890/element/element-9876543210', $method->invoke($session, 'element-9876543210')); + } + + /** + * Make private and protected function callable + * + * @param mixed $object Subject under test + * @param string $function Function name + * + * @return \ReflectionMethod + */ + private function makeCallable($object, $function) + { + $method = new \ReflectionMethod($object, $function); + $method->setAccessible(true); + + return $method; + } +} diff --git a/test/Test/WebDriver/ShadowTest.php b/test/Test/WebDriver/ShadowTest.php new file mode 100644 index 00000000..0a420eb7 --- /dev/null +++ b/test/Test/WebDriver/ShadowTest.php @@ -0,0 +1,63 @@ + + */ + +namespace Test\WebDriver; + +use PHPUnit\Framework\TestCase; +use WebDriver\Shadow; + +/** + * Test WebDriver\Shadow class + * + * @package WebDriver + * + * @group Unit + */ +class ShadowTest extends TestCase +{ + /** + * test getID + */ + public function testGetID() + { + $shadow = new Shadow('http://example.com/session/:sessionId/shadow', 'shadow-1234567890'); + + $this->assertEquals('shadow-1234567890', $shadow->getID()); + } + + /** + * test getNewIdentifierPath + */ + public function testGetNewIdentifierPath() + { + $shadow = new Shadow('http://example.com/session/:sessionId/shadow', 'shadow-1234567890'); + + $method = $this->makeCallable($shadow, 'getNewIdentifierPath'); + + $this->assertEquals('http://example.com/session/:sessionId/element/element-9876543210', $method->invoke($shadow, 'element-9876543210')); + } + + /** + * Make private and protected function callable + * + * @param mixed $object Subject under test + * @param string $function Function name + * + * @return \ReflectionMethod + */ + private function makeCallable($object, $function) + { + $method = new \ReflectionMethod($object, $function); + $method->setAccessible(true); + + return $method; + } +} diff --git a/test/Test/WebDriver/WebDriverTestBase.php b/test/Test/WebDriver/WebDriverTestBase.php index 1d7b9b96..3cebf1a5 100644 --- a/test/Test/WebDriver/WebDriverTestBase.php +++ b/test/Test/WebDriver/WebDriverTestBase.php @@ -1,24 +1,12 @@ - * @author Damian Mooyman */ namespace Test\WebDriver; diff --git a/test/Assets/index.html b/test/assets/index.html similarity index 100% rename from test/Assets/index.html rename to test/assets/index.html