RemoteWebDriver.php 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638
  1. <?php
  2. // Copyright 2004-present Facebook. All Rights Reserved.
  3. //
  4. // Licensed under the Apache License, Version 2.0 (the "License");
  5. // you may not use this file except in compliance with the License.
  6. // You may obtain a copy of the License at
  7. //
  8. // http://www.apache.org/licenses/LICENSE-2.0
  9. //
  10. // Unless required by applicable law or agreed to in writing, software
  11. // distributed under the License is distributed on an "AS IS" BASIS,
  12. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. // See the License for the specific language governing permissions and
  14. // limitations under the License.
  15. namespace Facebook\WebDriver\Remote;
  16. use Facebook\WebDriver\Chrome\ChromeOptions;
  17. use Facebook\WebDriver\Interactions\WebDriverActions;
  18. use Facebook\WebDriver\JavaScriptExecutor;
  19. use Facebook\WebDriver\WebDriver;
  20. use Facebook\WebDriver\WebDriverBy;
  21. use Facebook\WebDriver\WebDriverCapabilities;
  22. use Facebook\WebDriver\WebDriverCommandExecutor;
  23. use Facebook\WebDriver\WebDriverElement;
  24. use Facebook\WebDriver\WebDriverHasInputDevices;
  25. use Facebook\WebDriver\WebDriverNavigation;
  26. use Facebook\WebDriver\WebDriverOptions;
  27. use Facebook\WebDriver\WebDriverWait;
  28. class RemoteWebDriver implements WebDriver, JavaScriptExecutor, WebDriverHasInputDevices
  29. {
  30. /**
  31. * @var HttpCommandExecutor|null
  32. */
  33. protected $executor;
  34. /**
  35. * @var WebDriverCapabilities
  36. */
  37. protected $capabilities;
  38. /**
  39. * @var string
  40. */
  41. protected $sessionID;
  42. /**
  43. * @var RemoteMouse
  44. */
  45. protected $mouse;
  46. /**
  47. * @var RemoteKeyboard
  48. */
  49. protected $keyboard;
  50. /**
  51. * @var RemoteTouchScreen
  52. */
  53. protected $touch;
  54. /**
  55. * @var RemoteExecuteMethod
  56. */
  57. protected $executeMethod;
  58. /**
  59. * @param HttpCommandExecutor $commandExecutor
  60. * @param string $sessionId
  61. * @param WebDriverCapabilities|null $capabilities
  62. */
  63. protected function __construct(
  64. HttpCommandExecutor $commandExecutor,
  65. $sessionId,
  66. WebDriverCapabilities $capabilities = null
  67. ) {
  68. $this->executor = $commandExecutor;
  69. $this->sessionID = $sessionId;
  70. if ($capabilities !== null) {
  71. $this->capabilities = $capabilities;
  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. * @return static
  85. */
  86. public static function create(
  87. $selenium_server_url = 'http://localhost:4444/wd/hub',
  88. $desired_capabilities = null,
  89. $connection_timeout_in_ms = null,
  90. $request_timeout_in_ms = null,
  91. $http_proxy = null,
  92. $http_proxy_port = null,
  93. DesiredCapabilities $required_capabilities = null
  94. ) {
  95. $selenium_server_url = preg_replace('#/+$#', '', $selenium_server_url);
  96. $desired_capabilities = self::castToDesiredCapabilitiesObject($desired_capabilities);
  97. // Hotfix: W3C WebDriver protocol is not yet supported by php-webdriver, so we must force Chromedriver to
  98. // not use the W3C protocol by default (which is what Chromedriver does starting with version 75).
  99. if ($desired_capabilities->getBrowserName() === WebDriverBrowserType::CHROME
  100. && mb_strpos($selenium_server_url, 'browserstack') === false // see https://github.com/facebook/php-webdriver/issues/644
  101. ) {
  102. $currentChromeOptions = $desired_capabilities->getCapability(ChromeOptions::CAPABILITY);
  103. $chromeOptions = !empty($currentChromeOptions) ? $currentChromeOptions : new ChromeOptions();
  104. if ($chromeOptions instanceof ChromeOptions && !isset($chromeOptions->toArray()['w3c'])) {
  105. $chromeOptions->setExperimentalOption('w3c', false);
  106. } elseif (is_array($chromeOptions) && !isset($chromeOptions['w3c'])) {
  107. $chromeOptions['w3c'] = false;
  108. }
  109. $desired_capabilities->setCapability(ChromeOptions::CAPABILITY, $chromeOptions);
  110. }
  111. $executor = new HttpCommandExecutor($selenium_server_url, $http_proxy, $http_proxy_port);
  112. if ($connection_timeout_in_ms !== null) {
  113. $executor->setConnectionTimeout($connection_timeout_in_ms);
  114. }
  115. if ($request_timeout_in_ms !== null) {
  116. $executor->setRequestTimeout($request_timeout_in_ms);
  117. }
  118. if ($required_capabilities !== null) {
  119. // TODO: Selenium (as of v3.0.1) does accept requiredCapabilities only as a property of desiredCapabilities.
  120. // This will probably change in future with the W3C WebDriver spec, but is the only way how to pass these
  121. // values now.
  122. $desired_capabilities->setCapability('requiredCapabilities', $required_capabilities->toArray());
  123. }
  124. $command = new WebDriverCommand(
  125. null,
  126. DriverCommand::NEW_SESSION,
  127. ['desiredCapabilities' => $desired_capabilities->toArray()]
  128. );
  129. $response = $executor->execute($command);
  130. $returnedCapabilities = new DesiredCapabilities($response->getValue());
  131. $driver = new static($executor, $response->getSessionID(), $returnedCapabilities);
  132. return $driver;
  133. }
  134. /**
  135. * [Experimental] Construct the RemoteWebDriver by an existing session.
  136. *
  137. * This constructor can boost the performance a lot by reusing the same browser for the whole test suite.
  138. * You cannot pass the desired capabilities because the session was created before.
  139. *
  140. * @param string $selenium_server_url The url of the remote Selenium WebDriver server
  141. * @param string $session_id The existing session id
  142. * @param int|null $connection_timeout_in_ms Set timeout for the connect phase to remote Selenium WebDriver server
  143. * @param int|null $request_timeout_in_ms Set the maximum time of a request to remote Selenium WebDriver server
  144. * @return static
  145. */
  146. public static function createBySessionID(
  147. $session_id,
  148. $selenium_server_url = 'http://localhost:4444/wd/hub',
  149. $connection_timeout_in_ms = null,
  150. $request_timeout_in_ms = null
  151. ) {
  152. $executor = new HttpCommandExecutor($selenium_server_url);
  153. if ($connection_timeout_in_ms !== null) {
  154. $executor->setConnectionTimeout($connection_timeout_in_ms);
  155. }
  156. if ($request_timeout_in_ms !== null) {
  157. $executor->setRequestTimeout($request_timeout_in_ms);
  158. }
  159. return new static($executor, $session_id);
  160. }
  161. /**
  162. * Close the current window.
  163. *
  164. * @return RemoteWebDriver The current instance.
  165. */
  166. public function close()
  167. {
  168. $this->execute(DriverCommand::CLOSE, []);
  169. return $this;
  170. }
  171. /**
  172. * Find the first WebDriverElement using the given mechanism.
  173. *
  174. * @param WebDriverBy $by
  175. * @return RemoteWebElement NoSuchElementException is thrown in HttpCommandExecutor if no element is found.
  176. * @see WebDriverBy
  177. */
  178. public function findElement(WebDriverBy $by)
  179. {
  180. $params = ['using' => $by->getMechanism(), 'value' => $by->getValue()];
  181. $raw_element = $this->execute(
  182. DriverCommand::FIND_ELEMENT,
  183. $params
  184. );
  185. return $this->newElement($raw_element['ELEMENT']);
  186. }
  187. /**
  188. * Find all WebDriverElements within the current page using the given mechanism.
  189. *
  190. * @param WebDriverBy $by
  191. * @return RemoteWebElement[] A list of all WebDriverElements, or an empty array if nothing matches
  192. * @see WebDriverBy
  193. */
  194. public function findElements(WebDriverBy $by)
  195. {
  196. $params = ['using' => $by->getMechanism(), 'value' => $by->getValue()];
  197. $raw_elements = $this->execute(
  198. DriverCommand::FIND_ELEMENTS,
  199. $params
  200. );
  201. $elements = [];
  202. foreach ($raw_elements as $raw_element) {
  203. $elements[] = $this->newElement($raw_element['ELEMENT']);
  204. }
  205. return $elements;
  206. }
  207. /**
  208. * Load a new web page in the current browser window.
  209. *
  210. * @param string $url
  211. *
  212. * @return RemoteWebDriver The current instance.
  213. */
  214. public function get($url)
  215. {
  216. $params = ['url' => (string) $url];
  217. $this->execute(DriverCommand::GET, $params);
  218. return $this;
  219. }
  220. /**
  221. * Get a string representing the current URL that the browser is looking at.
  222. *
  223. * @return string The current URL.
  224. */
  225. public function getCurrentURL()
  226. {
  227. return $this->execute(DriverCommand::GET_CURRENT_URL);
  228. }
  229. /**
  230. * Get the source of the last loaded page.
  231. *
  232. * @return string The current page source.
  233. */
  234. public function getPageSource()
  235. {
  236. return $this->execute(DriverCommand::GET_PAGE_SOURCE);
  237. }
  238. /**
  239. * Get the title of the current page.
  240. *
  241. * @return string The title of the current page.
  242. */
  243. public function getTitle()
  244. {
  245. return $this->execute(DriverCommand::GET_TITLE);
  246. }
  247. /**
  248. * Return an opaque handle to this window that uniquely identifies it within this driver instance.
  249. *
  250. * @return string The current window handle.
  251. */
  252. public function getWindowHandle()
  253. {
  254. return $this->execute(
  255. DriverCommand::GET_CURRENT_WINDOW_HANDLE,
  256. []
  257. );
  258. }
  259. /**
  260. * Get all window handles available to the current session.
  261. *
  262. * @return array An array of string containing all available window handles.
  263. */
  264. public function getWindowHandles()
  265. {
  266. return $this->execute(DriverCommand::GET_WINDOW_HANDLES, []);
  267. }
  268. /**
  269. * Quits this driver, closing every associated window.
  270. */
  271. public function quit()
  272. {
  273. $this->execute(DriverCommand::QUIT);
  274. $this->executor = null;
  275. }
  276. /**
  277. * Inject a snippet of JavaScript into the page for execution in the context of the currently selected frame.
  278. * The executed script is assumed to be synchronous and the result of evaluating the script will be returned.
  279. *
  280. * @param string $script The script to inject.
  281. * @param array $arguments The arguments of the script.
  282. * @return mixed The return value of the script.
  283. */
  284. public function executeScript($script, array $arguments = [])
  285. {
  286. $params = [
  287. 'script' => $script,
  288. 'args' => $this->prepareScriptArguments($arguments),
  289. ];
  290. return $this->execute(DriverCommand::EXECUTE_SCRIPT, $params);
  291. }
  292. /**
  293. * Inject a snippet of JavaScript into the page for asynchronous execution in the context of the currently selected
  294. * frame.
  295. *
  296. * The driver will pass a callback as the last argument to the snippet, and block until the callback is invoked.
  297. *
  298. * You may need to define script timeout using `setScriptTimeout()` method of `WebDriverTimeouts` first.
  299. *
  300. * @param string $script The script to inject.
  301. * @param array $arguments The arguments of the script.
  302. * @return mixed The value passed by the script to the callback.
  303. */
  304. public function executeAsyncScript($script, array $arguments = [])
  305. {
  306. $params = [
  307. 'script' => $script,
  308. 'args' => $this->prepareScriptArguments($arguments),
  309. ];
  310. return $this->execute(
  311. DriverCommand::EXECUTE_ASYNC_SCRIPT,
  312. $params
  313. );
  314. }
  315. /**
  316. * Take a screenshot of the current page.
  317. *
  318. * @param string $save_as The path of the screenshot to be saved.
  319. * @return string The screenshot in PNG format.
  320. */
  321. public function takeScreenshot($save_as = null)
  322. {
  323. $screenshot = base64_decode(
  324. $this->execute(DriverCommand::SCREENSHOT)
  325. );
  326. if ($save_as) {
  327. file_put_contents($save_as, $screenshot);
  328. }
  329. return $screenshot;
  330. }
  331. /**
  332. * Construct a new WebDriverWait by the current WebDriver instance.
  333. * Sample usage:
  334. *
  335. * ```
  336. * $driver->wait(20, 1000)->until(
  337. * WebDriverExpectedCondition::titleIs('WebDriver Page')
  338. * );
  339. * ```
  340. * @param int $timeout_in_second
  341. * @param int $interval_in_millisecond
  342. *
  343. * @return WebDriverWait
  344. */
  345. public function wait($timeout_in_second = 30, $interval_in_millisecond = 250)
  346. {
  347. return new WebDriverWait(
  348. $this,
  349. $timeout_in_second,
  350. $interval_in_millisecond
  351. );
  352. }
  353. /**
  354. * An abstraction for managing stuff you would do in a browser menu. For example, adding and deleting cookies.
  355. *
  356. * @return WebDriverOptions
  357. */
  358. public function manage()
  359. {
  360. return new WebDriverOptions($this->getExecuteMethod());
  361. }
  362. /**
  363. * An abstraction allowing the driver to access the browser's history and to navigate to a given URL.
  364. *
  365. * @return WebDriverNavigation
  366. * @see WebDriverNavigation
  367. */
  368. public function navigate()
  369. {
  370. return new WebDriverNavigation($this->getExecuteMethod());
  371. }
  372. /**
  373. * Switch to a different window or frame.
  374. *
  375. * @return RemoteTargetLocator
  376. * @see RemoteTargetLocator
  377. */
  378. public function switchTo()
  379. {
  380. return new RemoteTargetLocator($this->getExecuteMethod(), $this);
  381. }
  382. /**
  383. * @return RemoteMouse
  384. */
  385. public function getMouse()
  386. {
  387. if (!$this->mouse) {
  388. $this->mouse = new RemoteMouse($this->getExecuteMethod());
  389. }
  390. return $this->mouse;
  391. }
  392. /**
  393. * @return RemoteKeyboard
  394. */
  395. public function getKeyboard()
  396. {
  397. if (!$this->keyboard) {
  398. $this->keyboard = new RemoteKeyboard($this->getExecuteMethod());
  399. }
  400. return $this->keyboard;
  401. }
  402. /**
  403. * @return RemoteTouchScreen
  404. */
  405. public function getTouch()
  406. {
  407. if (!$this->touch) {
  408. $this->touch = new RemoteTouchScreen($this->getExecuteMethod());
  409. }
  410. return $this->touch;
  411. }
  412. /**
  413. * Construct a new action builder.
  414. *
  415. * @return WebDriverActions
  416. */
  417. public function action()
  418. {
  419. return new WebDriverActions($this);
  420. }
  421. /**
  422. * Set the command executor of this RemoteWebdriver
  423. *
  424. * @deprecated To be removed in the future. Executor should be passed in the constructor.
  425. * @internal
  426. * @codeCoverageIgnore
  427. * @param WebDriverCommandExecutor $executor Despite the typehint, it have be an instance of HttpCommandExecutor.
  428. * @return RemoteWebDriver
  429. */
  430. public function setCommandExecutor(WebDriverCommandExecutor $executor)
  431. {
  432. $this->executor = $executor;
  433. return $this;
  434. }
  435. /**
  436. * Get the command executor of this RemoteWebdriver
  437. *
  438. * @return HttpCommandExecutor
  439. */
  440. public function getCommandExecutor()
  441. {
  442. return $this->executor;
  443. }
  444. /**
  445. * Set the session id of the RemoteWebDriver.
  446. *
  447. * @deprecated To be removed in the future. Session ID should be passed in the constructor.
  448. * @internal
  449. * @codeCoverageIgnore
  450. * @param string $session_id
  451. * @return RemoteWebDriver
  452. */
  453. public function setSessionID($session_id)
  454. {
  455. $this->sessionID = $session_id;
  456. return $this;
  457. }
  458. /**
  459. * Get current selenium sessionID
  460. *
  461. * @return string
  462. */
  463. public function getSessionID()
  464. {
  465. return $this->sessionID;
  466. }
  467. /**
  468. * Get capabilities of the RemoteWebDriver.
  469. *
  470. * @return WebDriverCapabilities
  471. */
  472. public function getCapabilities()
  473. {
  474. return $this->capabilities;
  475. }
  476. /**
  477. * Returns a list of the currently active sessions.
  478. *
  479. * @param string $selenium_server_url The url of the remote Selenium WebDriver server
  480. * @param int $timeout_in_ms
  481. * @return array
  482. */
  483. public static function getAllSessions($selenium_server_url = 'http://localhost:4444/wd/hub', $timeout_in_ms = 30000)
  484. {
  485. $executor = new HttpCommandExecutor($selenium_server_url);
  486. $executor->setConnectionTimeout($timeout_in_ms);
  487. $command = new WebDriverCommand(
  488. null,
  489. DriverCommand::GET_ALL_SESSIONS,
  490. []
  491. );
  492. return $executor->execute($command)->getValue();
  493. }
  494. public function execute($command_name, $params = [])
  495. {
  496. $command = new WebDriverCommand(
  497. $this->sessionID,
  498. $command_name,
  499. $params
  500. );
  501. if ($this->executor) {
  502. $response = $this->executor->execute($command);
  503. return $response->getValue();
  504. }
  505. return null;
  506. }
  507. /**
  508. * Prepare arguments for JavaScript injection
  509. *
  510. * @param array $arguments
  511. * @return array
  512. */
  513. protected function prepareScriptArguments(array $arguments)
  514. {
  515. $args = [];
  516. foreach ($arguments as $key => $value) {
  517. if ($value instanceof WebDriverElement) {
  518. $args[$key] = ['ELEMENT' => $value->getID()];
  519. } else {
  520. if (is_array($value)) {
  521. $value = $this->prepareScriptArguments($value);
  522. }
  523. $args[$key] = $value;
  524. }
  525. }
  526. return $args;
  527. }
  528. /**
  529. * @return RemoteExecuteMethod
  530. */
  531. protected function getExecuteMethod()
  532. {
  533. if (!$this->executeMethod) {
  534. $this->executeMethod = new RemoteExecuteMethod($this);
  535. }
  536. return $this->executeMethod;
  537. }
  538. /**
  539. * Return the WebDriverElement with the given id.
  540. *
  541. * @param string $id The id of the element to be created.
  542. * @return RemoteWebElement
  543. */
  544. protected function newElement($id)
  545. {
  546. return new RemoteWebElement($this->getExecuteMethod(), $id);
  547. }
  548. /**
  549. * Cast legacy types (array or null) to DesiredCapabilities object. To be removed in future when instance of
  550. * DesiredCapabilities will be required.
  551. *
  552. * @param array|DesiredCapabilities|null $desired_capabilities
  553. * @return DesiredCapabilities
  554. */
  555. protected static function castToDesiredCapabilitiesObject($desired_capabilities = null)
  556. {
  557. if ($desired_capabilities === null) {
  558. return new DesiredCapabilities();
  559. }
  560. if (is_array($desired_capabilities)) {
  561. return new DesiredCapabilities($desired_capabilities);
  562. }
  563. return $desired_capabilities;
  564. }
  565. }