From 580f49e6ce35107f24d0b7b7b65b23bb38a4452f Mon Sep 17 00:00:00 2001 From: Dorin Date: Mon, 8 May 2017 21:53:10 +0300 Subject: [PATCH] Initial commit --- .gitignore | 2 + composer.json | 31 ++ docker/Dockerfile | 48 +++ docker/files/entrypoint.sh | 11 + docker/files/mink_vhost.conf | 31 ++ docker/files/supervisord.conf | 56 +++ phpunit.xml.dist | 20 + socket.php | 11 + src/ChromeDriver.php | 715 ++++++++++++++++++++++++++++++++++ src/HttpClient.php | 13 + tests/ChromeDriverConfig.php | 30 ++ 11 files changed, 968 insertions(+) create mode 100644 .gitignore create mode 100644 composer.json create mode 100644 docker/Dockerfile create mode 100755 docker/files/entrypoint.sh create mode 100644 docker/files/mink_vhost.conf create mode 100644 docker/files/supervisord.conf create mode 100644 phpunit.xml.dist create mode 100644 socket.php create mode 100644 src/ChromeDriver.php create mode 100644 src/HttpClient.php create mode 100644 tests/ChromeDriverConfig.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3a9875b --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/vendor/ +composer.lock diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..f1dd1c9 --- /dev/null +++ b/composer.json @@ -0,0 +1,31 @@ +{ + "name": "dmore/chrome-headless-mink-driver", + "description": "Mink driver for controlling headless chrome without selenium", + "type": "library", + "license": "BSD", + "authors": [ + { + "name": "Dorian More", + "email": "d.more@gmail.com" + } + ], + "autoload": { + "psr-4": { + "DMore\\ChromeDriver\\": "src/" + } + }, + + "autoload-dev": { + "psr-4": { + "DMore\\ChromeDriverTests\\": "tests/" + } + }, + "minimum-stability": "stable", + "require-dev": { + "phpunit/phpunit": "^5.0.0" + }, + "require": { + "textalk/websocket": "^1.2.0", + "behat/mink": "1.*" + } +} diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..c7ced4b --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,48 @@ +FROM php:7.0-fpm + +RUN sed -i -e "s/;daemonize\s*=\s*yes/daemonize = no/g" /usr/local/etc/php-fpm.conf \ + && sed -i -e "s/;catch_workers_output\s*=\s*yes/catch_workers_output = yes/g" /usr/local/etc/php-fpm.d/www.conf \ + && sed -i -e "s/pm.max_children = 5/pm.max_children = 9/g" /usr/local/etc/php-fpm.d/www.conf \ + && sed -i -e "s/pm.start_servers = 2/pm.start_servers = 3/g" /usr/local/etc/php-fpm.d/www.conf \ + && sed -i -e "s/pm.min_spare_servers = 1/pm.min_spare_servers = 2/g" /usr/local/etc/php-fpm.d/www.conf \ + && sed -i -e "s/pm.max_spare_servers = 3/pm.max_spare_servers = 4/g" /usr/local/etc/php-fpm.d/www.conf \ + && sed -i -e "s/pm.max_requests = 500/pm.max_requests = 200/g" /usr/local/etc/php-fpm.d/www.conf + +RUN apt-get update -qqy \ + && apt-get -qqy install wget ca-certificates apt-transport-https nginx supervisor ttf-wqy-zenhei ttf-unfonts-core \ + unzip git \ + && rm -rf /var/lib/apt/lists/* /var/cache/apt/* + + +RUN wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | apt-key add - \ + && echo "deb https://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google-chrome.list \ + && apt-get update -qqy \ + && apt-get -qqy install google-chrome-unstable \ + && rm /etc/apt/sources.list.d/google-chrome.list \ + && rm -rf /var/lib/apt/lists/* /var/cache/apt/* + +RUN useradd headless --shell /bin/bash --create-home \ + && usermod -a -G sudo headless \ + && echo 'ALL ALL = (ALL) NOPASSWD: ALL' >> /etc/sudoers \ + && echo 'headless:nopassword' | chpasswd + +RUN mkdir /data + +RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer \ + && rm -rf /etc/nginx/sites-enabled/default \ + && mkdir -p /root/.ssh \ + && echo "Host *\n\tStrictHostKeyChecking no\n" >> /root/.ssh/config + +VOLUME /code + +WORKDIR /code + +COPY files/supervisord.conf /etc/supervisord.conf + +COPY files/entrypoint.sh /entrypoint.sh + +COPY files/mink_vhost.conf /etc/nginx/sites-enabled/mink + +ENTRYPOINT ["/entrypoint.sh"] + +CMD ["bash"] diff --git a/docker/files/entrypoint.sh b/docker/files/entrypoint.sh new file mode 100755 index 0000000..69f9e67 --- /dev/null +++ b/docker/files/entrypoint.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +umask 000 + +/usr/bin/supervisord -n -c /etc/supervisord.conf > /dev/null 2>&1 & + +if [[ $# -eq 1 && $1 == "bash" ]]; then + $@ +else + exec "$@" +fi diff --git a/docker/files/mink_vhost.conf b/docker/files/mink_vhost.conf new file mode 100644 index 0000000..cbe7a6f --- /dev/null +++ b/docker/files/mink_vhost.conf @@ -0,0 +1,31 @@ +server { + listen 80 default_server; + listen [::]:80 default_server; + + root /code/vendor/behat/mink/driver-testsuite/web-fixtures; + + # Add index.php to the list if you are using PHP + index index.html index.htm index.nginx-debian.html; + + server_name _; + + location / { + # First attempt to serve request as file, then + # as directory, then fall back to displaying a 404. + try_files $uri $uri/ =404; + } + + + location ~ \.php$ { + index index.html index.htm index.php; + fastcgi_index index.php; + fastcgi_param PATH_INFO $fastcgi_path_info; + fastcgi_param PATH_TRANSLATED $document_root$fastcgi_path_info; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + + fastcgi_pass localhost:9000; + fastcgi_read_timeout 3600s; + fastcgi_split_path_info ^(.+\.php)(/.+)$; + include fastcgi_params; + } +} diff --git a/docker/files/supervisord.conf b/docker/files/supervisord.conf new file mode 100644 index 0000000..606a9f4 --- /dev/null +++ b/docker/files/supervisord.conf @@ -0,0 +1,56 @@ +[unix_http_server] +file=/tmp/supervisor.sock ; (the path to the socket file) + +[supervisord] +logfile=/tmp/supervisord.log ; (main log file;default $CWD/supervisord.log) +logfile_maxbytes=50MB ; (max main logfile bytes b4 rotation;default 50MB) +logfile_backups=10 ; (num of main logfile rotation backups;default 10) +loglevel=warn ; (log level;default info; others: debug,warn,trace) +pidfile=/tmp/supervisord.pid ; (supervisord pidfile;default supervisord.pid) +nodaemon=false ; (start in foreground if true;default false) +minfds=1024 ; (min. avail startup file descriptors;default 1024) +minprocs=200 ; (min. avail process descriptors;default 200) +user=root ; + +; the below section must remain in the config file for RPC +; (supervisorctl/web interface) to work, additional interfaces may be +; added by defining them in separate rpcinterface: sections +[rpcinterface:supervisor] +supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface + +[supervisorctl] +serverurl=unix:///tmp/supervisor.sock ; use a unix:// URL for a unix socket + +[program:php-fpm] +command=/usr/local/sbin/php-fpm -R +autostart=true +autorestart=true +priority=5 +stdout_logfile=NONE +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 + +[program:nginx] +command=/usr/sbin/nginx +autostart=true +autorestart=true +priority=10 +stdout_logfile=NONE +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 +stdout_events_enabled=false +stderr_events_enabled=true + +[program:chrome] +command=/usr/bin/google-chrome-unstable --disable-gpu --headless --remote-debugging-address=0.0.0.0 --remote-debugging-port=9222 --no-sandbox +autostart=true +autorestart=true +priority=10 +stdout_logfile=NONE +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 +stdout_events_enabled=false +stderr_events_enabled=true diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..c7c753e --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,20 @@ + + + + + + tests + vendor/behat/mink/driver-testsuite/tests + + + + + + + + + + ./src/Behat/Mink/Driver + + + diff --git a/socket.php b/socket.php new file mode 100644 index 0000000..9bdc992 --- /dev/null +++ b/socket.php @@ -0,0 +1,11 @@ +send($line); + var_dump($client->receive()); +} diff --git a/src/ChromeDriver.php b/src/ChromeDriver.php new file mode 100644 index 0000000..42d3991 --- /dev/null +++ b/src/ChromeDriver.php @@ -0,0 +1,715 @@ +http_client = $http_client; + $this->url = $chrome_url; + $this->base_url = $base_url; + } + + public function start() + { + $json = $this->http_client->get($this->url . '/json/new'); + $response = json_decode($json, true); + $ws_url = $response['webSocketDebuggerUrl']; + $this->id = $response['id']; + $this->client = new Client($ws_url); + $this->client->setFragmentSize(2000000); # Chrome closes the connection if a message is sent in fragments + $this->send('Page.enable'); + $this->send('DOM.enable'); + $this->send('Runtime.enable'); + $this->send('Network.enable'); + $this->is_started = true; + } + + /** + * Checks whether driver is started. + * + * @return Boolean + */ + public function isStarted() + { + return $this->is_started; + } + + /** + * Stops driver. + * + * Once stopped, the driver should be started again before using it again. + * + * Calling any action on a stopped driver is an undefined behavior. + * The only supported method call after stopping a driver is starting it again. + * + * Calling stop on a stopped driver is an undefined behavior. Driver + * implementations are free to handle it silently or to fail with an + * exception. + * + * @throws DriverException When the driver cannot be closed + */ + public function stop() + { + try { + $this->client->close(); + } catch (\WebSocket\ConnectionException $exception) { + } + $this->http_client->get($this->url . '/json/close/' . $this->id); + $this->is_started = false; + } + + /** + * Resets driver state. + * + * This should reset cookies, request headers and basic authentication. + * When possible, the history should be reset as well, but this is not enforced + * as some implementations may not be able to reset it without restarting the + * driver entirely. Consumers requiring a clean history should restart the driver + * to enforce it. + * + * Once reset, the driver should be ready to visit a page. + * Calling any action before visiting a page is an undefined behavior. + * The only supported method calls on a fresh driver are + * - visit() + * - setRequestHeader() + * - setBasicAuth() + * - reset() + * - stop() + * + * Calling reset on a stopped driver is an undefined behavior. + */ + public function reset() + { + $this->deleteAllCookies(); + $this->stop(); + $this->start(); + } + + /** + * Visit specified URL. + * + * @param string $url url of the page + * + * @throws UnsupportedDriverActionException When operation not supported by the driver + * @throws DriverException When the operation cannot be done + */ + public function visit($url) + { + $this->send('Page.navigate', ['url' => 'http://localhost' . $url]); + $this->waitForPage(); + } + + /** + * Returns current URL address. + * + * @return string + * + * @throws UnsupportedDriverActionException When operation not supported by the driver + * @throws DriverException When the operation cannot be done + */ + public function getCurrentUrl() + { + $response = $this->send('Page.getNavigationHistory'); + return str_replace($this->base_url, '', $response['entries'][$response['currentIndex']]['url']); + } + + /** + * Reloads current page. + * + * @throws UnsupportedDriverActionException When operation not supported by the driver + * @throws DriverException When the operation cannot be done + */ + public function reload() + { + $this->send('Page.reload'); + $this->waitForPage(); + } + + /** + * Moves browser forward 1 page. + * + * @throws UnsupportedDriverActionException When operation not supported by the driver + * @throws DriverException When the operation cannot be done + */ + public function forward() + { + $current_index = $this->send('Page.getNavigationHistory')['currentIndex']; + $this->send('Page.navigateToHistoryEntry', $current_index + 1); + $this->waitForPage(); + } + + /** + * Moves browser backward 1 page. + * + * @throws UnsupportedDriverActionException When operation not supported by the driver + * @throws DriverException When the operation cannot be done + */ + public function back() + { + $current_index = $this->send('Page.getNavigationHistory')['currentIndex']; + $this->send('Page.navigateToHistoryEntry', $current_index - 1); + $this->waitForPage(); + } + + /** + * {@inheritdoc} + */ + public function setBasicAuth($user, $password) + { + throw new UnsupportedDriverActionException('Basic auth setup is not supported by %s', $this); + } + + /** + * {@inheritdoc} + */ + public function switchToWindow($name = null) + { + throw new UnsupportedDriverActionException('Windows management is not supported by %s', $this); + } + + /** + * {@inheritdoc} + */ + public function switchToIFrame($name = null) + { + throw new UnsupportedDriverActionException('iFrames management is not supported by %s', $this); + } + + /** + * {@inheritdoc} + */ + public function setRequestHeader($name, $value) + { + throw new UnsupportedDriverActionException('Request headers manipulation is not supported by %s', $this); + } + + /** + * {@inheritdoc} + */ + public function getResponseHeaders() + { + throw new UnsupportedDriverActionException('Response headers are not available from %s', $this); + } + + /** + * Sets cookie. + * + * @param string $name + * @param string $value + * + * @throws UnsupportedDriverActionException When operation not supported by the driver + * @throws DriverException When the operation cannot be done + */ + public function setCookie($name, $value = null) + { + if ($value === null) { + $expiration = 'expires=Thu, 01 Jan 1970 00:00:01 GMT'; + foreach ($this->send('Network.getAllCookies')['cookies'] as $cookie) { + if ($name == $cookie['name']) { + $parameters = ['expression' => "document.cookie='$name=;$expiration; path={$cookie['path']}'"]; + $this->send('Runtime.evaluate', $parameters); + } + } + } else { + $expiration = 'expires=' . date(DATE_COOKIE, time() + 86400); + $value = urlencode($value); + $current_url = $this->getCurrentUrl(); + $path = substr($current_url, strpos($current_url, '/') - 1); + $name = urlencode($name); + $this->send('Runtime.evaluate', ['expression' => "document.cookie='$name=$value;$expiration; path=$path'"]); + } + } + + /** + * Returns cookie by name. + * + * @param string $name + * + * @return string|null + * + * @throws UnsupportedDriverActionException When operation not supported by the driver + * @throws DriverException When the operation cannot be done + */ + public function getCookie($name) + { + $result = $this->send('Network.getCookies'); + + foreach ($result['cookies'] as $cookie) { + if ($cookie['name'] == $name) { + return urldecode($cookie['value']); + } + } + return null; + } + + /** + * {@inheritdoc} + */ + public function getStatusCode() + { + throw new UnsupportedDriverActionException('Status code is not available from %s', $this); + } + + /** + * Returns last response content. + * + * @return string + * + * @throws UnsupportedDriverActionException When operation not supported by the driver + * @throws DriverException When the operation cannot be done + */ + public function getContent() + { + $frame = $this->send('Page.getResourceTree')['frameTree']['frame']; + $parameters = ['frameId' => $frame['id'], 'url' => $frame['url']]; + return $this->send('Page.getResourceContent', $parameters)['content']; + } + + /** + * Capture a screenshot of the current window. + * + * @return string screenshot of MIME type image/* depending + * on driver (e.g., image/png, image/jpeg) + * + * @throws UnsupportedDriverActionException When operation not supported by the driver + * @throws DriverException When the operation cannot be done + */ + public function getScreenshot() + { + return base64_decode($this->send('Page.captureScreenshot')); + } + + /** + * {@inheritdoc} + */ + public function getWindowNames() + { + throw new UnsupportedDriverActionException('Listing all window names is not supported by %s', $this); + } + + /** + * {@inheritdoc} + */ + public function getWindowName() + { + throw new UnsupportedDriverActionException('Listing this window name is not supported by %s', $this); + } + + /** + * Finds elements with specified XPath query. + * + * @param string $xpath + * @return \Behat\Mink\Element\NodeElement[] + * @throws ElementNotFoundException + */ + public function findElementXpaths($xpath) + { + $expression = $this->getXpathExpression($xpath) . ' var items = 0; ' . + 'while (xpath_result.iterateNext()) { items++; }; items;'; + $result = $this->send('Runtime.evaluate', ['expression' => $expression])['result']; + + $node_elements = []; + + for ($i = 1; $i <= $result['value']; $i++) { + $node_elements[] = sprintf('(%s)[%d]', $xpath, $i); + } + return $node_elements; + } + + /** + * Returns element's tag name by it's XPath query. + * + * @param string $xpath + * @return string + * @throws ElementNotFoundException + */ + public function getTagName($xpath) + { + $expression = $this->getXpathExpression($xpath) . ' xpath_result.iterateNext().tagName'; + $result = $this->send('Runtime.evaluate', ['expression' => $expression])['result']; + return $result['value']; + } + + /** + * Returns element's text by it's XPath query. + * + * @param string $xpath + * @return string + * @throws ElementNotFoundException + */ + public function getText($xpath) + { + $expression = $this->getXpathExpression($xpath) . ' xpath_result.iterateNext().textContent'; + $text = $this->send('Runtime.evaluate', ['expression' => $expression])['result']['value']; + $text = (string)str_replace(array("\r", "\r\n", "\n"), ' ', $text); + return $text; + } + + /** + * {@inheritdoc} + */ + public function getHtml($xpath) + { + $expression = $this->getXpathExpression($xpath) . ' xpath_result.iterateNext().innerHTML'; + $result = $this->send('Runtime.evaluate', ['expression' => $expression])['result']; + return $result['value']; + } + + /** + * {@inheritdoc} + */ + public function getOuterHtml($xpath) + { + $expression = $this->getXpathExpression($xpath) . ' xpath_result.iterateNext().outerHTML'; + $result = $this->send('Runtime.evaluate', ['expression' => $expression])['result']; + return $result['value']; + } + + /** + * {@inheritdoc} + */ + public function getAttribute($xpath, $name) + { + throw new UnsupportedDriverActionException('Getting the element attribute is not supported by %s', $this); + } + + /** + * {@inheritdoc} + */ + public function getValue($xpath) + { + throw new UnsupportedDriverActionException('Getting the field value is not supported by %s', $this); + } + + /** + * {@inheritdoc} + */ + public function setValue($xpath, $value) + { + throw new UnsupportedDriverActionException('Setting the field value is not supported by %s', $this); + } + + /** + * {@inheritdoc} + */ + public function check($xpath) + { + throw new UnsupportedDriverActionException('Checking a checkbox is not supported by %s', $this); + } + + /** + * {@inheritdoc} + */ + public function uncheck($xpath) + { + throw new UnsupportedDriverActionException('Unchecking a checkbox is not supported by %s', $this); + } + + /** + * {@inheritdoc} + */ + public function isChecked($xpath) + { + throw new UnsupportedDriverActionException('Getting the state of a checkbox is not supported by %s', $this); + } + + /** + * {@inheritdoc} + */ + public function selectOption($xpath, $value, $multiple = false) + { + throw new UnsupportedDriverActionException('Selecting an option is not supported by %s', $this); + } + + /** + * {@inheritdoc} + */ + public function click($xpath) + { + throw new UnsupportedDriverActionException('Clicking on an element is not supported by %s', $this); + } + + /** + * {@inheritdoc} + */ + public function attachFile($xpath, $path) + { + throw new UnsupportedDriverActionException('Attaching a file in an input is not supported by %s', $this); + } + + /** + * {@inheritdoc} + */ + public function doubleClick($xpath) + { + throw new UnsupportedDriverActionException('Double-clicking is not supported by %s', $this); + } + + /** + * {@inheritdoc} + */ + public function rightClick($xpath) + { + throw new UnsupportedDriverActionException('Right-clicking is not supported by %s', $this); + } + + /** + * {@inheritdoc} + */ + public function isVisible($xpath) + { + throw new UnsupportedDriverActionException('Element visibility check is not supported by %s', $this); + } + + /** + * {@inheritdoc} + */ + public function isSelected($xpath) + { + throw new UnsupportedDriverActionException('Element selection check is not supported by %s', $this); + } + + /** + * {@inheritdoc} + */ + public function mouseOver($xpath) + { + throw new UnsupportedDriverActionException('Mouse manipulations are not supported by %s', $this); + } + + /** + * {@inheritdoc} + */ + public function focus($xpath) + { + throw new UnsupportedDriverActionException('Mouse manipulations are not supported by %s', $this); + } + + /** + * {@inheritdoc} + */ + public function blur($xpath) + { + throw new UnsupportedDriverActionException('Mouse manipulations are not supported by %s', $this); + } + + /** + * {@inheritdoc} + */ + public function keyPress($xpath, $char, $modifier = null) + { + throw new UnsupportedDriverActionException('Keyboard manipulations are not supported by %s', $this); + } + + /** + * {@inheritdoc} + */ + public function keyDown($xpath, $char, $modifier = null) + { + throw new UnsupportedDriverActionException('Keyboard manipulations are not supported by %s', $this); + } + + /** + * {@inheritdoc} + */ + public function keyUp($xpath, $char, $modifier = null) + { + throw new UnsupportedDriverActionException('Keyboard manipulations are not supported by %s', $this); + } + + /** + * {@inheritdoc} + */ + public function dragTo($sourceXpath, $destinationXpath) + { + throw new UnsupportedDriverActionException('Mouse manipulations are not supported by %s', $this); + } + + /** + * {@inheritdoc} + */ + public function executeScript($script) + { + throw new UnsupportedDriverActionException('JS is not supported by %s', $this); + } + + /** + * {@inheritdoc} + */ + public function evaluateScript($script) + { + throw new UnsupportedDriverActionException('JS is not supported by %s', $this); + } + + /** + * {@inheritdoc} + */ + public function wait($timeout, $condition) + { + throw new UnsupportedDriverActionException('JS is not supported by %s', $this); + } + + /** + * {@inheritdoc} + */ + public function resizeWindow($width, $height, $name = null) + { + throw new UnsupportedDriverActionException('Window resizing is not supported by %s', $this); + } + + /** + * {@inheritdoc} + */ + public function maximizeWindow($name = null) + { + throw new UnsupportedDriverActionException('Window maximize is not supported by %s', $this); + } + + /** + * {@inheritdoc} + */ + public function submitForm($xpath) + { + throw new UnsupportedDriverActionException('Form submission is not supported by %s', $this); + } + + private function waitFor(callable $is_ready) + { + do { + $response = $this->client->receive(); + if (is_null($response)) { + return null; + } +// echo $response . PHP_EOL; + + $data = json_decode($response, true); + if (array_key_exists('error', $data)) { + throw new DriverException($data['error']['message'], $data['error']['code']); + } + + if (array_key_exists('method', $data)) { + switch ($data['method']) { + case 'Page.domContentEventFired': + $this->dom_ready = false; + break; + case 'DOM.documentUpdated': + $this->dom_ready = true; + $this->node_ids_ready = false; + break; + case 'Page.frameNavigated': + case 'Page.loadEventFired': + case 'Page.frameStartedLoading': + $this->page_ready = false; + $this->dom_ready = false; + $this->node_ids_ready = false; + break; + case 'Page.frameStoppedLoading': + $this->page_ready = true; + break; + case 'DOM.setChildNodes': + $this->node_ids_ready = true; + break; + default: +// echo "Unknown method {$data['method']}"; + continue; + } + } + } while (!$is_ready($data)); + + return $data; + } + + /** + * @param array $command + * @param array $parameters + * @return null|string + * @throws \Exception + */ + private function send($command, array $parameters = []) + { + $payload['id'] = $this->command_id++; + $payload['method'] = $command; + if (!empty($parameters)) { + $payload['params'] = $parameters; + } + + try { + $this->client->send(json_encode($payload)); + } catch (ConnectionException $exception) { + echo $exception->getMessage(); + echo '> ' . json_encode($payload) . PHP_EOL; + exit; + } + + $data = $this->waitFor(function ($data) use ($payload) { + return array_key_exists('id', $data) && $data['id'] == $payload['id']; + }); + + return $data['result']; + } + + private function waitForPage() + { + $this->waitFor(function () { + return $this->page_ready; + }); + } + + protected function deleteAllCookies() + { + $this->send('Network.clearBrowserCookies'); + } + + /** + * @param $xpath + * @return string + */ + protected function getXpathExpression($xpath):string + { + $xpath = addslashes($xpath); + $xpath = str_replace("\n", '\\n', $xpath); + return "var xpath_result = document.evaluate(\"{$xpath}\", document.body);"; + } +} diff --git a/src/HttpClient.php b/src/HttpClient.php new file mode 100644 index 0000000..db6248e --- /dev/null +++ b/src/HttpClient.php @@ -0,0 +1,13 @@ +