1<?php 2 3namespace Facebook\WebDriver\Remote; 4 5use Facebook\WebDriver\Interactions\WebDriverActions; 6use Facebook\WebDriver\JavaScriptExecutor; 7use Facebook\WebDriver\WebDriver; 8use Facebook\WebDriver\WebDriverBy; 9use Facebook\WebDriver\WebDriverCapabilities; 10use Facebook\WebDriver\WebDriverCommandExecutor; 11use Facebook\WebDriver\WebDriverElement; 12use Facebook\WebDriver\WebDriverHasInputDevices; 13use Facebook\WebDriver\WebDriverNavigation; 14use Facebook\WebDriver\WebDriverOptions; 15use Facebook\WebDriver\WebDriverWait; 16 17class RemoteWebDriver implements WebDriver, JavaScriptExecutor, WebDriverHasInputDevices 18{ 19 /** 20 * @var HttpCommandExecutor|null 21 */ 22 protected $executor; 23 /** 24 * @var WebDriverCapabilities 25 */ 26 protected $capabilities; 27 28 /** 29 * @var string 30 */ 31 protected $sessionID; 32 /** 33 * @var RemoteMouse 34 */ 35 protected $mouse; 36 /** 37 * @var RemoteKeyboard 38 */ 39 protected $keyboard; 40 /** 41 * @var RemoteTouchScreen 42 */ 43 protected $touch; 44 /** 45 * @var RemoteExecuteMethod 46 */ 47 protected $executeMethod; 48 /** 49 * @var bool 50 */ 51 protected $isW3cCompliant; 52 53 /** 54 * @param HttpCommandExecutor $commandExecutor 55 * @param string $sessionId 56 * @param WebDriverCapabilities|null $capabilities 57 * @param bool $isW3cCompliant false to use the legacy JsonWire protocol, true for the W3C WebDriver spec 58 */ 59 protected function __construct( 60 HttpCommandExecutor $commandExecutor, 61 $sessionId, 62 WebDriverCapabilities $capabilities = null, 63 $isW3cCompliant = false 64 ) { 65 $this->executor = $commandExecutor; 66 $this->sessionID = $sessionId; 67 $this->isW3cCompliant = $isW3cCompliant; 68 69 if ($capabilities !== null) { 70 $this->capabilities = $capabilities; 71 } 72 } 73 74 /** 75 * Construct the RemoteWebDriver by a desired capabilities. 76 * 77 * @param string $selenium_server_url The url of the remote Selenium WebDriver server 78 * @param DesiredCapabilities|array $desired_capabilities The desired capabilities 79 * @param int|null $connection_timeout_in_ms Set timeout for the connect phase to remote Selenium WebDriver server 80 * @param int|null $request_timeout_in_ms Set the maximum time of a request to remote Selenium WebDriver server 81 * @param string|null $http_proxy The proxy to tunnel requests to the remote Selenium WebDriver through 82 * @param int|null $http_proxy_port The proxy port to tunnel requests to the remote Selenium WebDriver through 83 * @param DesiredCapabilities $required_capabilities The required capabilities 84 * 85 * @return static 86 */ 87 public static function create( 88 $selenium_server_url = 'http://localhost:4444/wd/hub', 89 $desired_capabilities = null, 90 $connection_timeout_in_ms = null, 91 $request_timeout_in_ms = null, 92 $http_proxy = null, 93 $http_proxy_port = null, 94 DesiredCapabilities $required_capabilities = null 95 ) { 96 $selenium_server_url = preg_replace('#/+$#', '', $selenium_server_url); 97 98 $desired_capabilities = self::castToDesiredCapabilitiesObject($desired_capabilities); 99 100 $executor = new HttpCommandExecutor($selenium_server_url, $http_proxy, $http_proxy_port); 101 if ($connection_timeout_in_ms !== null) { 102 $executor->setConnectionTimeout($connection_timeout_in_ms); 103 } 104 if ($request_timeout_in_ms !== null) { 105 $executor->setRequestTimeout($request_timeout_in_ms); 106 } 107 108 // W3C 109 $parameters = [ 110 'capabilities' => [ 111 'firstMatch' => [(object) $desired_capabilities->toW3cCompatibleArray()], 112 ], 113 ]; 114 115 if ($required_capabilities !== null && !empty($required_capabilities->toArray())) { 116 $parameters['capabilities']['alwaysMatch'] = (object) $required_capabilities->toW3cCompatibleArray(); 117 } 118 119 // Legacy protocol 120 if ($required_capabilities !== null) { 121 // TODO: Selenium (as of v3.0.1) does accept requiredCapabilities only as a property of desiredCapabilities. 122 // This has changed with the W3C WebDriver spec, but is the only way how to pass these 123 // values with the legacy protocol. 124 $desired_capabilities->setCapability('requiredCapabilities', (object) $required_capabilities->toArray()); 125 } 126 127 $parameters['desiredCapabilities'] = (object) $desired_capabilities->toArray(); 128 129 $command = WebDriverCommand::newSession($parameters); 130 131 $response = $executor->execute($command); 132 133 return static::createFromResponse($response, $executor); 134 } 135 136 /** 137 * [Experimental] Construct the RemoteWebDriver by an existing session. 138 * 139 * This constructor can boost the performance a lot by reusing the same browser for the whole test suite. 140 * You cannot pass the desired capabilities because the session was created before. 141 * 142 * @param string $selenium_server_url The url of the remote Selenium WebDriver server 143 * @param string $session_id The existing session id 144 * @param int|null $connection_timeout_in_ms Set timeout for the connect phase to remote Selenium WebDriver server 145 * @param int|null $request_timeout_in_ms Set the maximum time of a request to remote Selenium WebDriver server 146 * @param bool $isW3cCompliant True to use W3C WebDriver (default), false to use the legacy JsonWire protocol 147 * @return static 148 */ 149 public static function createBySessionID( 150 $session_id, 151 $selenium_server_url = 'http://localhost:4444/wd/hub', 152 $connection_timeout_in_ms = null, 153 $request_timeout_in_ms = null 154 ) { 155 // BC layer to not break the method signature 156 $isW3cCompliant = func_num_args() > 4 ? func_get_arg(4) : true; 157 158 $executor = new HttpCommandExecutor($selenium_server_url, null, null); 159 if ($connection_timeout_in_ms !== null) { 160 $executor->setConnectionTimeout($connection_timeout_in_ms); 161 } 162 if ($request_timeout_in_ms !== null) { 163 $executor->setRequestTimeout($request_timeout_in_ms); 164 } 165 166 if (!$isW3cCompliant) { 167 $executor->disableW3cCompliance(); 168 } 169 170 return new static($executor, $session_id, null, $isW3cCompliant); 171 } 172 173 /** 174 * Close the current window. 175 * 176 * @return RemoteWebDriver The current instance. 177 */ 178 public function close() 179 { 180 $this->execute(DriverCommand::CLOSE, []); 181 182 return $this; 183 } 184 185 /** 186 * Create a new top-level browsing context. 187 * 188 * @codeCoverageIgnore 189 * @deprecated Use $driver->switchTo()->newWindow() 190 * @return WebDriver The current instance. 191 */ 192 public function newWindow() 193 { 194 return $this->switchTo()->newWindow(); 195 } 196 197 /** 198 * Find the first WebDriverElement using the given mechanism. 199 * 200 * @param WebDriverBy $by 201 * @return RemoteWebElement NoSuchElementException is thrown in HttpCommandExecutor if no element is found. 202 * @see WebDriverBy 203 */ 204 public function findElement(WebDriverBy $by) 205 { 206 $raw_element = $this->execute( 207 DriverCommand::FIND_ELEMENT, 208 JsonWireCompat::getUsing($by, $this->isW3cCompliant) 209 ); 210 211 return $this->newElement(JsonWireCompat::getElement($raw_element)); 212 } 213 214 /** 215 * Find all WebDriverElements within the current page using the given mechanism. 216 * 217 * @param WebDriverBy $by 218 * @return RemoteWebElement[] A list of all WebDriverElements, or an empty array if nothing matches 219 * @see WebDriverBy 220 */ 221 public function findElements(WebDriverBy $by) 222 { 223 $raw_elements = $this->execute( 224 DriverCommand::FIND_ELEMENTS, 225 JsonWireCompat::getUsing($by, $this->isW3cCompliant) 226 ); 227 228 $elements = []; 229 foreach ($raw_elements as $raw_element) { 230 $elements[] = $this->newElement(JsonWireCompat::getElement($raw_element)); 231 } 232 233 return $elements; 234 } 235 236 /** 237 * Load a new web page in the current browser window. 238 * 239 * @param string $url 240 * 241 * @return RemoteWebDriver The current instance. 242 */ 243 public function get($url) 244 { 245 $params = ['url' => (string) $url]; 246 $this->execute(DriverCommand::GET, $params); 247 248 return $this; 249 } 250 251 /** 252 * Get a string representing the current URL that the browser is looking at. 253 * 254 * @return string The current URL. 255 */ 256 public function getCurrentURL() 257 { 258 return $this->execute(DriverCommand::GET_CURRENT_URL); 259 } 260 261 /** 262 * Get the source of the last loaded page. 263 * 264 * @return string The current page source. 265 */ 266 public function getPageSource() 267 { 268 return $this->execute(DriverCommand::GET_PAGE_SOURCE); 269 } 270 271 /** 272 * Get the title of the current page. 273 * 274 * @return string The title of the current page. 275 */ 276 public function getTitle() 277 { 278 return $this->execute(DriverCommand::GET_TITLE); 279 } 280 281 /** 282 * Return an opaque handle to this window that uniquely identifies it within this driver instance. 283 * 284 * @return string The current window handle. 285 */ 286 public function getWindowHandle() 287 { 288 return $this->execute( 289 DriverCommand::GET_CURRENT_WINDOW_HANDLE, 290 [] 291 ); 292 } 293 294 /** 295 * Get all window handles available to the current session. 296 * 297 * Note: Do not use `end($driver->getWindowHandles())` to find the last open window, for proper solution see: 298 * https://github.com/php-webdriver/php-webdriver/wiki/Alert,-tabs,-frames,-iframes#switch-to-the-new-window 299 * 300 * @return array An array of string containing all available window handles. 301 */ 302 public function getWindowHandles() 303 { 304 return $this->execute(DriverCommand::GET_WINDOW_HANDLES, []); 305 } 306 307 /** 308 * Quits this driver, closing every associated window. 309 */ 310 public function quit() 311 { 312 $this->execute(DriverCommand::QUIT); 313 $this->executor = null; 314 } 315 316 /** 317 * Inject a snippet of JavaScript into the page for execution in the context of the currently selected frame. 318 * The executed script is assumed to be synchronous and the result of evaluating the script will be returned. 319 * 320 * @param string $script The script to inject. 321 * @param array $arguments The arguments of the script. 322 * @return mixed The return value of the script. 323 */ 324 public function executeScript($script, array $arguments = []) 325 { 326 $params = [ 327 'script' => $script, 328 'args' => $this->prepareScriptArguments($arguments), 329 ]; 330 331 return $this->execute(DriverCommand::EXECUTE_SCRIPT, $params); 332 } 333 334 /** 335 * Inject a snippet of JavaScript into the page for asynchronous execution in the context of the currently selected 336 * frame. 337 * 338 * The driver will pass a callback as the last argument to the snippet, and block until the callback is invoked. 339 * 340 * You may need to define script timeout using `setScriptTimeout()` method of `WebDriverTimeouts` first. 341 * 342 * @param string $script The script to inject. 343 * @param array $arguments The arguments of the script. 344 * @return mixed The value passed by the script to the callback. 345 */ 346 public function executeAsyncScript($script, array $arguments = []) 347 { 348 $params = [ 349 'script' => $script, 350 'args' => $this->prepareScriptArguments($arguments), 351 ]; 352 353 return $this->execute( 354 DriverCommand::EXECUTE_ASYNC_SCRIPT, 355 $params 356 ); 357 } 358 359 /** 360 * Take a screenshot of the current page. 361 * 362 * @param string $save_as The path of the screenshot to be saved. 363 * @return string The screenshot in PNG format. 364 */ 365 public function takeScreenshot($save_as = null) 366 { 367 $screenshot = base64_decode($this->execute(DriverCommand::SCREENSHOT), true); 368 369 if ($save_as !== null) { 370 $directoryPath = dirname($save_as); 371 372 if (!file_exists($directoryPath)) { 373 mkdir($directoryPath, 0777, true); 374 } 375 376 file_put_contents($save_as, $screenshot); 377 } 378 379 return $screenshot; 380 } 381 382 /** 383 * Status returns information about whether a remote end is in a state in which it can create new sessions. 384 */ 385 public function getStatus() 386 { 387 $response = $this->execute(DriverCommand::STATUS); 388 389 return RemoteStatus::createFromResponse($response); 390 } 391 392 /** 393 * Construct a new WebDriverWait by the current WebDriver instance. 394 * Sample usage: 395 * 396 * ``` 397 * $driver->wait(20, 1000)->until( 398 * WebDriverExpectedCondition::titleIs('WebDriver Page') 399 * ); 400 * ``` 401 * @param int $timeout_in_second 402 * @param int $interval_in_millisecond 403 * 404 * @return WebDriverWait 405 */ 406 public function wait($timeout_in_second = 30, $interval_in_millisecond = 250) 407 { 408 return new WebDriverWait( 409 $this, 410 $timeout_in_second, 411 $interval_in_millisecond 412 ); 413 } 414 415 /** 416 * An abstraction for managing stuff you would do in a browser menu. For example, adding and deleting cookies. 417 * 418 * @return WebDriverOptions 419 */ 420 public function manage() 421 { 422 return new WebDriverOptions($this->getExecuteMethod(), $this->isW3cCompliant); 423 } 424 425 /** 426 * An abstraction allowing the driver to access the browser's history and to navigate to a given URL. 427 * 428 * @return WebDriverNavigation 429 * @see WebDriverNavigation 430 */ 431 public function navigate() 432 { 433 return new WebDriverNavigation($this->getExecuteMethod()); 434 } 435 436 /** 437 * Switch to a different window or frame. 438 * 439 * @return RemoteTargetLocator 440 * @see RemoteTargetLocator 441 */ 442 public function switchTo() 443 { 444 return new RemoteTargetLocator($this->getExecuteMethod(), $this, $this->isW3cCompliant); 445 } 446 447 /** 448 * @return RemoteMouse 449 */ 450 public function getMouse() 451 { 452 if (!$this->mouse) { 453 $this->mouse = new RemoteMouse($this->getExecuteMethod(), $this->isW3cCompliant); 454 } 455 456 return $this->mouse; 457 } 458 459 /** 460 * @return RemoteKeyboard 461 */ 462 public function getKeyboard() 463 { 464 if (!$this->keyboard) { 465 $this->keyboard = new RemoteKeyboard($this->getExecuteMethod(), $this, $this->isW3cCompliant); 466 } 467 468 return $this->keyboard; 469 } 470 471 /** 472 * @return RemoteTouchScreen 473 */ 474 public function getTouch() 475 { 476 if (!$this->touch) { 477 $this->touch = new RemoteTouchScreen($this->getExecuteMethod()); 478 } 479 480 return $this->touch; 481 } 482 483 /** 484 * Construct a new action builder. 485 * 486 * @return WebDriverActions 487 */ 488 public function action() 489 { 490 return new WebDriverActions($this); 491 } 492 493 /** 494 * Set the command executor of this RemoteWebdriver 495 * 496 * @deprecated To be removed in the future. Executor should be passed in the constructor. 497 * @internal 498 * @codeCoverageIgnore 499 * @param WebDriverCommandExecutor $executor Despite the typehint, it have be an instance of HttpCommandExecutor. 500 * @return RemoteWebDriver 501 */ 502 public function setCommandExecutor(WebDriverCommandExecutor $executor) 503 { 504 $this->executor = $executor; 505 506 return $this; 507 } 508 509 /** 510 * Get the command executor of this RemoteWebdriver 511 * 512 * @return HttpCommandExecutor 513 */ 514 public function getCommandExecutor() 515 { 516 return $this->executor; 517 } 518 519 /** 520 * Set the session id of the RemoteWebDriver. 521 * 522 * @deprecated To be removed in the future. Session ID should be passed in the constructor. 523 * @internal 524 * @codeCoverageIgnore 525 * @param string $session_id 526 * @return RemoteWebDriver 527 */ 528 public function setSessionID($session_id) 529 { 530 $this->sessionID = $session_id; 531 532 return $this; 533 } 534 535 /** 536 * Get current selenium sessionID 537 * 538 * @return string 539 */ 540 public function getSessionID() 541 { 542 return $this->sessionID; 543 } 544 545 /** 546 * Get capabilities of the RemoteWebDriver. 547 * 548 * @return WebDriverCapabilities 549 */ 550 public function getCapabilities() 551 { 552 return $this->capabilities; 553 } 554 555 /** 556 * Returns a list of the currently active sessions. 557 * 558 * @param string $selenium_server_url The url of the remote Selenium WebDriver server 559 * @param int $timeout_in_ms 560 * @return array 561 */ 562 public static function getAllSessions($selenium_server_url = 'http://localhost:4444/wd/hub', $timeout_in_ms = 30000) 563 { 564 $executor = new HttpCommandExecutor($selenium_server_url, null, null); 565 $executor->setConnectionTimeout($timeout_in_ms); 566 567 $command = new WebDriverCommand( 568 null, 569 DriverCommand::GET_ALL_SESSIONS, 570 [] 571 ); 572 573 return $executor->execute($command)->getValue(); 574 } 575 576 public function execute($command_name, $params = []) 577 { 578 $command = new WebDriverCommand( 579 $this->sessionID, 580 $command_name, 581 $params 582 ); 583 584 if ($this->executor) { 585 $response = $this->executor->execute($command); 586 587 return $response->getValue(); 588 } 589 590 return null; 591 } 592 593 /** 594 * Execute custom commands on remote end. 595 * For example vendor-specific commands or other commands not implemented by php-webdriver. 596 * 597 * @see https://github.com/php-webdriver/php-webdriver/wiki/Custom-commands 598 * @param string $endpointUrl 599 * @param string $method 600 * @param array $params 601 * @return mixed|null 602 */ 603 public function executeCustomCommand($endpointUrl, $method = 'GET', $params = []) 604 { 605 $command = new CustomWebDriverCommand( 606 $this->sessionID, 607 $endpointUrl, 608 $method, 609 $params 610 ); 611 612 if ($this->executor) { 613 $response = $this->executor->execute($command); 614 615 return $response->getValue(); 616 } 617 618 return null; 619 } 620 621 /** 622 * @internal 623 * @return bool 624 */ 625 public function isW3cCompliant() 626 { 627 return $this->isW3cCompliant; 628 } 629 630 /** 631 * Create instance based on response to NEW_SESSION command. 632 * Also detect W3C/OSS dialect and setup the driver/executor accordingly. 633 * 634 * @internal 635 * @return static 636 */ 637 protected static function createFromResponse(WebDriverResponse $response, HttpCommandExecutor $commandExecutor) 638 { 639 $responseValue = $response->getValue(); 640 641 if (!$isW3cCompliant = isset($responseValue['capabilities'])) { 642 $commandExecutor->disableW3cCompliance(); 643 } 644 645 if ($isW3cCompliant) { 646 $returnedCapabilities = DesiredCapabilities::createFromW3cCapabilities($responseValue['capabilities']); 647 } else { 648 $returnedCapabilities = new DesiredCapabilities($responseValue); 649 } 650 651 return new static($commandExecutor, $response->getSessionID(), $returnedCapabilities, $isW3cCompliant); 652 } 653 654 /** 655 * Prepare arguments for JavaScript injection 656 * 657 * @param array $arguments 658 * @return array 659 */ 660 protected function prepareScriptArguments(array $arguments) 661 { 662 $args = []; 663 foreach ($arguments as $key => $value) { 664 if ($value instanceof WebDriverElement) { 665 $args[$key] = [ 666 $this->isW3cCompliant ? 667 JsonWireCompat::WEB_DRIVER_ELEMENT_IDENTIFIER 668 : 'ELEMENT' => $value->getID(), 669 ]; 670 } else { 671 if (is_array($value)) { 672 $value = $this->prepareScriptArguments($value); 673 } 674 $args[$key] = $value; 675 } 676 } 677 678 return $args; 679 } 680 681 /** 682 * @return RemoteExecuteMethod 683 */ 684 protected function getExecuteMethod() 685 { 686 if (!$this->executeMethod) { 687 $this->executeMethod = new RemoteExecuteMethod($this); 688 } 689 690 return $this->executeMethod; 691 } 692 693 /** 694 * Return the WebDriverElement with the given id. 695 * 696 * @param string $id The id of the element to be created. 697 * @return RemoteWebElement 698 */ 699 protected function newElement($id) 700 { 701 return new RemoteWebElement($this->getExecuteMethod(), $id, $this->isW3cCompliant); 702 } 703 704 /** 705 * Cast legacy types (array or null) to DesiredCapabilities object. To be removed in future when instance of 706 * DesiredCapabilities will be required. 707 * 708 * @param array|DesiredCapabilities|null $desired_capabilities 709 * @return DesiredCapabilities 710 */ 711 protected static function castToDesiredCapabilitiesObject($desired_capabilities = null) 712 { 713 if ($desired_capabilities === null) { 714 return new DesiredCapabilities(); 715 } 716 717 if (is_array($desired_capabilities)) { 718 return new DesiredCapabilities($desired_capabilities); 719 } 720 721 return $desired_capabilities; 722 } 723} 724