Update to Drupal 8.0.0-rc3. For more information, see https://www.drupal.org/node/2608078

This commit is contained in:
Pantheon Automation 2015-11-04 11:11:27 -08:00 committed by Greg Anderson
parent 6419a031d7
commit 4afb23bbd3
762 changed files with 20080 additions and 6368 deletions

View file

@ -0,0 +1,120 @@
<?php
namespace Zumba\GastonJS\Browser;
/**
* Class Browser
* @package Zumba\GastonJS
*/
class Browser extends BrowserBase {
use BrowserAuthenticationTrait;
use BrowserConfigurationTrait;
use BrowserCookieTrait;
use BrowserFileTrait;
use BrowserFrameTrait;
use BrowserHeadersTrait;
use BrowserMouseEventTrait;
use BrowserNavigateTrait;
use BrowserNetworkTrait;
use BrowserPageElementTrait;
use BrowserPageTrait;
use BrowserRenderTrait;
use BrowserScriptTrait;
use BrowserWindowTrait;
/**
* @param string $phantomJSHost
* @param mixed $logger
*/
public function __construct($phantomJSHost, $logger = null) {
$this->phantomJSHost = $phantomJSHost;
$this->logger = $logger;
$this->debug = false;
$this->createApiClient();
}
/**
* Returns the value of a given element in a page
* @param $pageId
* @param $elementId
* @return mixed
*/
public function value($pageId, $elementId) {
return $this->command('value', $pageId, $elementId);
}
/**
* Sets a value to a given element in a given page
* @param $pageId
* @param $elementId
* @param $value
* @return mixed
*/
public function set($pageId, $elementId, $value) {
return $this->command('set', $pageId, $elementId, $value);
}
/**
* Tells whether an element on a page is visible or not
* @param $pageId
* @param $elementId
* @return bool
*/
public function isVisible($pageId, $elementId) {
return $this->command('visible', $pageId, $elementId);
}
/**
* @param $pageId
* @param $elementId
* @return bool
*/
public function isDisabled($pageId, $elementId) {
return $this->command('disabled', $pageId, $elementId);
}
/**
* Drag an element to a another in a given page
* @param $pageId
* @param $fromId
* @param $toId
* @return mixed
*/
public function drag($pageId, $fromId, $toId) {
return $this->command('drag', $pageId, $fromId, $toId);
}
/**
* Selects a value in the given element and page
* @param $pageId
* @param $elementId
* @param $value
* @return mixed
*/
public function select($pageId, $elementId, $value) {
return $this->command('select', $pageId, $elementId, $value);
}
/**
* Triggers an event to a given element on the given page
* @param $pageId
* @param $elementId
* @param $event
* @return mixed
*/
public function trigger($pageId, $elementId, $event) {
return $this->command('trigger', $pageId, $elementId, $event);
}
/**
* TODO: not sure what this does, needs to do normalizeKeys
* @param int $pageId
* @param int $elementId
* @param array $keys
* @return mixed
*/
public function sendKeys($pageId, $elementId, $keys) {
return $this->command('send_keys', $pageId, $elementId, $this->normalizeKeys($keys));
}
}

View file

@ -0,0 +1,19 @@
<?php
namespace Zumba\GastonJS\Browser;
/**
* Trait BrowserAuthenticationTrait
* @package Zumba\GastonJS\Browser
*/
trait BrowserAuthenticationTrait {
/**
* Sets basic HTTP authentication
* @param $user
* @param $password
* @return bool
*/
public function setHttpAuth($user, $password) {
return $this->command('set_http_auth', $user, $password);
}
}

View file

@ -0,0 +1,124 @@
<?php
namespace Zumba\GastonJS\Browser;
use Zumba\GastonJS\Exception\BrowserError;
use Zumba\GastonJS\Exception\DeadClient;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\ConnectException;
use GuzzleHttp\Exception\ServerException;
/**
* Class BrowserBase
* @package Zumba\GastonJS\Browser
*/
class BrowserBase {
/** @var mixed */
protected $logger;
/** @var bool */
protected $debug;
/** @var string */
protected $phantomJSHost;
/** @var Client */
protected $apiClient;
/**
* Creates an http client to consume the phantomjs API
*/
protected function createApiClient() {
// Provide a BC switch between guzzle 5 and guzzle 6.
if (class_exists('GuzzleHttp\Psr7\Response')) {
$this->apiClient = new Client(array("base_uri" => $this->getPhantomJSHost()));
}
else {
$this->apiClient = new Client(array("base_url" => $this->getPhantomJSHost()));
}
}
/**
* TODO: not sure how to do the normalizeKeys stuff fix when needed
* @param $keys
* @return mixed
*/
protected function normalizeKeys($keys) {
return $keys;
}
/**
* @return Client
*/
public function getApiClient() {
return $this->apiClient;
}
/**
* @return string
*/
public function getPhantomJSHost() {
return $this->phantomJSHost;
}
/**
* @return mixed
*/
public function getLogger() {
return $this->logger;
}
/**
* Restarts the browser
*/
public function restart() {
//TODO: Do we really need to do this?, we are just a client
}
/**
* Sends a command to the browser
* @throws BrowserError
* @throws \Exception
* @return mixed
*/
public function command() {
try {
$args = func_get_args();
$commandName = $args[0];
array_shift($args);
$messageToSend = json_encode(array('name' => $commandName, 'args' => $args));
/** @var $commandResponse \GuzzleHttp\Psr7\Response|\GuzzleHttp\Message\Response */
$commandResponse = $this->getApiClient()->post("/api", array("body" => $messageToSend));
$jsonResponse = json_decode($commandResponse->getBody(), TRUE);
} catch (ServerException $e) {
$jsonResponse = json_decode($e->getResponse()->getBody()->getContents(), true);
} catch (ConnectException $e) {
throw new DeadClient($e->getMessage(), $e->getCode(), $e);
} catch (\Exception $e) {
throw $e;
}
if (isset($jsonResponse['error'])) {
throw $this->getErrorClass($jsonResponse);
}
return $jsonResponse['response'];
}
/**
* @param $error
* @return BrowserError
*/
protected function getErrorClass($error) {
$errorClassMap = array(
'Poltergeist.JavascriptError' => "Zumba\\GastonJS\\Exception\\JavascriptError",
'Poltergeist.FrameNotFound' => "Zumba\\GastonJS\\Exception\\FrameNotFound",
'Poltergeist.InvalidSelector' => "Zumba\\GastonJS\\Exception\\InvalidSelector",
'Poltergeist.StatusFailError' => "Zumba\\GastonJS\\Exception\\StatusFailError",
'Poltergeist.NoSuchWindowError' => "Zumba\\GastonJS\\Exception\\NoSuchWindowError",
'Poltergeist.ObsoleteNode' => "Zumba\\GastonJS\\Exception\\ObsoleteNode"
);
if (isset($error['error']['name']) && isset($errorClassMap[$error["error"]["name"]])) {
return new $errorClassMap[$error["error"]["name"]]($error);
}
return new BrowserError($error);
}
}

View file

@ -0,0 +1,39 @@
<?php
namespace Zumba\GastonJS\Browser;
/**
* Trait BrowserConfigurationTrait
* @package Zumba\GastonJS\Browser
*/
trait BrowserConfigurationTrait {
/**
* Set whether to fail or not on javascript errors found on the page
* @param bool $enabled
* @return bool
*/
public function jsErrors($enabled = true) {
return $this->command('set_js_errors', $enabled);
}
/**
* Set a blacklist of urls that we are not supposed to load
* @param array $blackList
* @return bool
*/
public function urlBlacklist($blackList) {
return $this->command('set_url_blacklist', $blackList);
}
/**
* Set the debug mode on the browser
* @param bool $enable
* @return bool
*/
public function debug($enable = false) {
$this->debug = $enable;
return $this->command('set_debug', $this->debug);
}
}

View file

@ -0,0 +1,65 @@
<?php
namespace Zumba\GastonJS\Browser;
use Zumba\GastonJS\Cookie;
/**
* Trait BrowserCookieTrait
* @package Zumba\GastonJS\Browser
*/
trait BrowserCookieTrait {
/**
* Gets the cookies on the browser
*
* @return Cookie[]
*/
public function cookies() {
$cookies = $this->command('cookies');
$objCookies = array();
foreach ($cookies as $cookie) {
$objCookies[$cookie["name"]] = new Cookie($cookie);
}
return $objCookies;
}
/**
* Sets a cookie on the browser, expires times is set in seconds
* @param $cookie
* @return mixed
*/
public function setCookie($cookie) {
//TODO: add error control when the cookie array is not valid
if (isset($cookie["expires"])) {
$cookie["expires"] = intval($cookie["expires"]) * 1000;
}
$cookie['value'] = urlencode($cookie['value']);
return $this->command('set_cookie', $cookie);
}
/**
* Deletes a cookie on the browser if exists
* @param $cookieName
* @return bool
*/
public function removeCookie($cookieName) {
return $this->command('remove_cookie', $cookieName);
}
/**
* Clear all the cookies
* @return bool
*/
public function clearCookies() {
return $this->command('clear_cookies');
}
/**
* Enables or disables the cookies con phantomjs
* @param bool $enabled
* @return bool
*/
public function cookiesEnabled($enabled = true) {
return $this->command('cookies_enabled', $enabled);
}
}

View file

@ -0,0 +1,20 @@
<?php
namespace Zumba\GastonJS\Browser;
/**
* Trait BrowserFileTrait
* @package Zumba\GastonJS\Browser
*/
trait BrowserFileTrait {
/**
* Selects a file to send to the browser to a given page
* @param $pageId
* @param $elementId
* @param $value
* @return mixed
*/
public function selectFile($pageId, $elementId, $value) {
return $this->command('select_file', $pageId, $elementId, $value);
}
}

View file

@ -0,0 +1,31 @@
<?php
namespace Zumba\GastonJS\Browser;
/**
* Trait BrowserFrameTrait
* @package Zumba\GastonJS\Browser
*/
trait BrowserFrameTrait {
/**
* Back to the parent of the iframe if possible
* @return mixed
* @throws \Zumba\GastonJS\Exception\BrowserError
* @throws \Exception
*/
public function popFrame() {
return $this->command("pop_frame");
}
/**
* Goes into the iframe to do stuff
* @param string $name
* @param int $timeout
* @return mixed
* @throws \Zumba\GastonJS\Exception\BrowserError
* @throws \Exception
*/
public function pushFrame($name, $timeout = null) {
return $this->command("push_frame", $name, $timeout);
}
}

View file

@ -0,0 +1,53 @@
<?php
namespace Zumba\GastonJS\Browser;
/**
* Trait BrowserHeadersTrait
* @package Zumba\GastonJS\Browser
*/
trait BrowserHeadersTrait {
/**
* Returns the headers of the current page that will be used the next request
* @return mixed
*/
public function getHeaders() {
return $this->command('get_headers');
}
/**
* Given an array of headers, set such headers for the requests, removing all others
* @param array $headers
* @return mixed
*/
public function setHeaders($headers) {
return $this->command('set_headers', $headers);
}
/**
* Adds headers to current page overriding the existing ones for the next requests
* @param $headers
* @return mixed
*/
public function addHeaders($headers) {
return $this->command('add_headers', $headers);
}
/**
* Adds a header to the page making it permanent if needed
* @param $header
* @param $permanent
* @return mixed
*/
public function addHeader($header, $permanent = false) {
return $this->command('add_header', $header, $permanent);
}
/**
* Gets the response headers after a request
* @return mixed
*/
public function responseHeaders() {
return $this->command('response_headers');
}
}

View file

@ -0,0 +1,69 @@
<?php
namespace Zumba\GastonJS\Browser;
/**
* Trait BrowserMouseEventTrait
* @package Zumba\GastonJS\Browser
*/
trait BrowserMouseEventTrait {
/**
* Click on a given page and element
* @param $pageId
* @param $elementId
* @return mixed
*/
public function click($pageId, $elementId) {
return $this->command('click', $pageId, $elementId);
}
/**
* Triggers a right click on a page an element
* @param $pageId
* @param $elementId
* @return mixed
*/
public function rightClick($pageId, $elementId) {
return $this->command('right_click', $pageId, $elementId);
}
/**
* Triggers a double click in a given page and element
* @param $pageId
* @param $elementId
* @return mixed
*/
public function doubleClick($pageId, $elementId) {
return $this->command('double_click', $pageId, $elementId);
}
/**
* Hovers over an element in a given page
* @param $pageId
* @param $elementId
* @return mixed
*/
public function hover($pageId, $elementId) {
return $this->command('hover', $pageId, $elementId);
}
/**
* Click on given coordinates, THIS DOES NOT depend on the page, it just clicks on where we are right now
* @param $coordX
* @param $coordY
* @return mixed
*/
public function clickCoordinates($coordX, $coordY) {
return $this->command('click_coordinates', $coordX, $coordY);
}
/**
* Scrolls the page by a given left and top coordinates
* @param $left
* @param $top
* @return mixed
*/
public function scrollTo($left, $top) {
return $this->command('scroll_to', $left, $top);
}
}

View file

@ -0,0 +1,56 @@
<?php
namespace Zumba\GastonJS\Browser;
use Zumba\GastonJS\Exception\BrowserError;
/**
* Trait BrowserNavigateTrait
* @package Zumba\GastonJS\Browser
*/
trait BrowserNavigateTrait {
/**
* Send a visit command to the browser
* @param $url
* @return mixed
*/
public function visit($url) {
return $this->command('visit', $url);
}
/**
* Gets the current url we are in
* @return mixed
*/
public function currentUrl() {
return $this->command('current_url');
}
/**
* Goes back on the browser history if possible
* @return bool
* @throws BrowserError
* @throws \Exception
*/
public function goBack() {
return $this->command('go_back');
}
/**
* Goes forward on the browser history if possible
* @return mixed
* @throws BrowserError
* @throws \Exception
*/
public function goForward() {
return $this->command('go_forward');
}
/**
* Reloads the current page we are in
*/
public function reload() {
return $this->command('reload');
}
}

View file

@ -0,0 +1,39 @@
<?php
namespace Zumba\GastonJS\Browser;
use Zumba\GastonJS\NetworkTraffic\Request;
/**
* Trait BrowserNetworkTrait
* @package Zumba\GastonJS\Browser
*/
trait BrowserNetworkTrait {
/**
* Get all the network traffic that the page have created
* @return array
*/
public function networkTraffic() {
$networkTraffic = $this->command('network_traffic');
$requestTraffic = array();
if (count($networkTraffic) === 0) {
return null;
}
foreach ($networkTraffic as $traffic) {
$requestTraffic[] = new Request($traffic["request"], $traffic["responseParts"]);
}
return $requestTraffic;
}
/**
* Clear the network traffic data stored on the phantomjs code
* @return mixed
*/
public function clearNetworkTraffic() {
return $this->command('clear_network_traffic');
}
}

View file

@ -0,0 +1,193 @@
<?php
namespace Zumba\GastonJS\Browser;
/**
* Trait BrowserPageElementTrait
* @package Zumba\GastonJS\Browser
*/
trait BrowserPageElementTrait {
/**
* Find elements given a method and a selector
* @param $method
* @param $selector
* @return array
*/
public function find($method, $selector) {
$result = $this->command('find', $method, $selector);
$found["page_id"] = $result["page_id"];
foreach ($result["ids"] as $id) {
$found["ids"][] = $id;
}
return $found;
}
/**
* Find elements within a page, method and selector
* @param $pageId
* @param $elementId
* @param $method
* @param $selector
* @return mixed
*/
public function findWithin($pageId, $elementId, $method, $selector) {
return $this->command('find_within', $pageId, $elementId, $method, $selector);
}
/**
* @param $pageId
* @param $elementId
* @return mixed
*/
public function getParents($pageId, $elementId) {
return $this->command('parents', $pageId, $elementId);
}
/**
* Returns the text of a given page and element
* @param $pageId
* @param $elementId
* @return mixed
*/
public function allText($pageId, $elementId) {
return $this->command('all_text', $pageId, $elementId);
}
/**
* Returns the inner or outer html of the given page and element
* @param $pageId
* @param $elementId
* @param $type
* @return mixed
* @throws \Zumba\GastonJS\Exception\BrowserError
* @throws \Exception
*/
public function allHtml($pageId, $elementId, $type = "inner") {
return $this->command('all_html', $pageId, $elementId, $type);
}
/**
* Returns ONLY the visible text of a given page and element
* @param $pageId
* @param $elementId
* @return mixed
*/
public function visibleText($pageId, $elementId) {
return $this->command('visible_text', $pageId, $elementId);
}
/**
* Deletes the text of a given page and element
* @param $pageId
* @param $elementId
* @return mixed
*/
public function deleteText($pageId, $elementId) {
return $this->command('delete_text', $pageId, $elementId);
}
/**
* Gets the tag name of a given element and page
* @param $pageId
* @param $elementId
* @return string
*/
public function tagName($pageId, $elementId) {
return strtolower($this->command('tag_name', $pageId, $elementId));
}
/**
* Check if two elements are the same on a give
* @param $pageId
* @param $firstId
* @param $secondId
* @return bool
*/
public function equals($pageId, $firstId, $secondId) {
return $this->command('equals', $pageId, $firstId, $secondId);
}
/**
* Returns the attributes of an element in a given page
* @param $pageId
* @param $elementId
* @return mixed
*/
public function attributes($pageId, $elementId) {
return $this->command('attributes', $pageId, $elementId);
}
/**
* Returns the attribute of an element by name in a given page
* @param $pageId
* @param $elementId
* @param $name
* @return mixed
*/
public function attribute($pageId, $elementId, $name) {
return $this->command('attribute', $pageId, $elementId, $name);
}
/**
* Set an attribute to the given element in the given page
* @param $pageId
* @param $elementId
* @param $name
* @param $value
* @return mixed
* @throws \Zumba\GastonJS\Exception\BrowserError
* @throws \Exception
*/
public function setAttribute($pageId, $elementId, $name, $value) {
return $this->command('set_attribute', $pageId, $elementId, $name, $value);
}
/**
* Remove an attribute for a given page and element
* @param $pageId
* @param $elementId
* @param $name
* @return mixed
* @throws \Zumba\GastonJS\Exception\BrowserError
* @throws \Exception
*/
public function removeAttribute($pageId, $elementId, $name) {
return $this->command('remove_attribute', $pageId, $elementId, $name);
}
/**
* Checks if an element is visible or not
* @param $pageId
* @param $elementId
* @return boolean
*/
public function isVisible($pageId, $elementId) {
return $this->command("visible", $pageId, $elementId);
}
/**
* Sends the order to execute a key event on a given element
* @param $pageId
* @param $elementId
* @param $keyEvent
* @param $key
* @param $modifier
* @return mixed
*/
public function keyEvent($pageId, $elementId, $keyEvent, $key, $modifier) {
return $this->command("key_event", $pageId, $elementId, $keyEvent, $key, $modifier);
}
/**
* Sends the command to select and option given a value
* @param $pageId
* @param $elementId
* @param $value
* @param bool $multiple
* @return mixed
*/
public function selectOption($pageId, $elementId, $value, $multiple = false) {
return $this->command("select_option", $pageId, $elementId, $value, $multiple);
}
}

View file

@ -0,0 +1,59 @@
<?php
namespace Zumba\GastonJS\Browser;
/**
* Trait BrowserPageTrait
* @package Zumba\GastonJS\Browser
*/
trait BrowserPageTrait {
/**
* Gets the status code of the request we are currently in
* @return mixed
*/
public function getStatusCode() {
return $this->command('status_code');
}
/**
* Returns the body of the response to a given browser request
* @return mixed
*/
public function getBody() {
return $this->command('body');
}
/**
* Returns the source of the current page
* @return mixed
*/
public function getSource() {
return $this->command('source');
}
/**
* Gets the current page title
* @return mixed
*/
public function getTitle() {
return $this->command('title');
}
/**
* Resize the current page
* @param $width
* @param $height
* @return mixed
*/
public function resize($width, $height) {
return $this->command('resize', $width, $height);
}
/**
* Resets the page we are in to a clean slate
* @return mixed
*/
public function reset() {
return $this->command('reset');
}
}

View file

@ -0,0 +1,65 @@
<?php
namespace Zumba\GastonJS\Browser;
/**
* Trait BrowserRenderTrait
* @package Zumba\GastonJS\Browser
*/
trait BrowserRenderTrait {
/**
* Check and fix render options
* @param $options
* @return mixed
*/
protected function checkRenderOptions($options) {
//Default is full and no selection
if (count($options) === 0) {
$options["full"] = true;
$options["selector"] = null;
}
if (isset($options["full"]) && isset($options["selector"])) {
if ($options["full"]) {
//Whatever it is, full is more powerful than selection
$options["selector"] = null;
}
} else {
if (!isset($options["full"]) && isset($options["selector"])) {
$options["full"] = false;
}
}
return $options;
}
/**
* Renders a page or selection to a file given by path
* @param string $path
* @param array $options
* @return mixed
*/
public function render($path, $options = array()) {
$fixedOptions = $this->checkRenderOptions($options);
return $this->command('render', $path, $fixedOptions["full"], $fixedOptions["selector"]);
}
/**
* Renders base64 a page or selection to a file given by path
* @param string $imageFormat (PNG, GIF, JPEG)
* @param array $options
* @return mixed
*/
public function renderBase64($imageFormat, $options = array()) {
$fixedOptions = $this->checkRenderOptions($options);
return $this->command('render_base64', $imageFormat, $fixedOptions["full"], $fixedOptions["selector"]);
}
/**
* Sets the paper size, useful when saving to PDF
* @param $paperSize
* @return mixed
*/
public function setPaperSize($paperSize) {
return $this->command('set_paper_size', $paperSize);
}
}

View file

@ -0,0 +1,41 @@
<?php
namespace Zumba\GastonJS\Browser;
/**
* Trait BrowserScriptTrait
* @package Zumba\GastonJS\Browser
*/
trait BrowserScriptTrait {
/**
* Evaluates a script on the browser
* @param $script
* @return mixed
*/
public function evaluate($script) {
return $this->command('evaluate', $script);
}
/**
* Executes a script on the browser
* @param $script
* @return mixed
*/
public function execute($script) {
return $this->command('execute', $script);
}
/**
* Add desired extensions to phantomjs
* @param $extensions
* @return bool
*/
public function extensions($extensions) {
//TODO: add error control for when extensions do not exist physically
foreach ($extensions as $extensionName) {
$this->command('add_extension', $extensionName);
}
return true;
}
}

View file

@ -0,0 +1,81 @@
<?php
namespace Zumba\GastonJS\Browser;
/**
* Class BrowserWindowTrait
* @package Zumba\GastonJS\Browser
*/
trait BrowserWindowTrait {
/**
* Returns the current window handle name in the browser
* @param string $name
* @return mixed
*/
public function windowHandle($name = null) {
return $this->command('window_handle', $name);
}
/**
* Returns all the window handles present in the browser
* @return array
*/
public function windowHandles() {
return $this->command('window_handles');
}
/**
* Change the browser focus to another window
* @param $windowHandleName
* @return mixed
*/
public function switchToWindow($windowHandleName) {
return $this->command('switch_to_window', $windowHandleName);
}
/**
* Opens a new window on the browser
* @return mixed
*/
public function openNewWindow() {
return $this->command('open_new_window');
}
/**
* Closes a window on the browser by a given handler name
* @param $windowHandleName
* @return mixed
*/
public function closeWindow($windowHandleName) {
return $this->command('close_window', $windowHandleName);
}
/**
* Gets the current request window name
* @return string
* @throws \Zumba\GastonJS\Exception\BrowserError
* @throws \Exception
*/
public function windowName() {
return $this->command('window_name');
}
/**
* Zoom factor for a web page
* @param $zoomFactor
* @return mixed
*/
public function setZoomFactor($zoomFactor) {
return $this->command('set_zoom_factor', $zoomFactor);
}
/**
* Gets the window size
* @param $windowHandleName
* @return mixed
*/
public function windowSize($windowHandleName) {
return $this->command('window_size', $windowHandleName);
}
}

View file

@ -0,0 +1,17 @@
Poltergeist.BrowserError = (function (_super) {
__extends(BrowserError, _super);
function BrowserError(message, stack) {
this.message = message;
this.stack = stack;
}
BrowserError.prototype.name = "Poltergeist.BrowserError";
BrowserError.prototype.args = function () {
return [this.message, this.stack];
};
return BrowserError;
})(Poltergeist.Error);

View file

@ -0,0 +1,10 @@
/**
* Poltergeist base error class
*/
Poltergeist.Error = (function () {
function Error() {
}
return Error;
})();

View file

@ -0,0 +1,16 @@
Poltergeist.FrameNotFound = (function (_super) {
__extends(FrameNotFound, _super);
function FrameNotFound(frameName) {
this.frameName = frameName;
}
FrameNotFound.prototype.name = "Poltergeist.FrameNotFound";
FrameNotFound.prototype.args = function () {
return [this.frameName];
};
return FrameNotFound;
})(Poltergeist.Error);

View file

@ -0,0 +1,17 @@
Poltergeist.InvalidSelector = (function (_super) {
__extends(InvalidSelector, _super);
function InvalidSelector(method, selector) {
this.method = method;
this.selector = selector;
}
InvalidSelector.prototype.name = "Poltergeist.InvalidSelector";
InvalidSelector.prototype.args = function () {
return [this.method, this.selector];
};
return InvalidSelector;
})(Poltergeist.Error);

View file

@ -0,0 +1,16 @@
Poltergeist.JavascriptError = (function (_super) {
__extends(JavascriptError, _super);
function JavascriptError(errors) {
this.errors = errors;
}
JavascriptError.prototype.name = "Poltergeist.JavascriptError";
JavascriptError.prototype.args = function () {
return [this.errors];
};
return JavascriptError;
})(Poltergeist.Error);

View file

@ -0,0 +1,18 @@
Poltergeist.MouseEventFailed = (function (_super) {
__extends(MouseEventFailed, _super);
function MouseEventFailed(eventName, selector, position) {
this.eventName = eventName;
this.selector = selector;
this.position = position;
}
MouseEventFailed.prototype.name = "Poltergeist.MouseEventFailed";
MouseEventFailed.prototype.args = function () {
return [this.eventName, this.selector, this.position];
};
return MouseEventFailed;
})(Poltergeist.Error);

View file

@ -0,0 +1,17 @@
Poltergeist.NoSuchWindowError = (function (_super) {
__extends(NoSuchWindowError, _super);
function NoSuchWindowError() {
_ref2 = NoSuchWindowError.__super__.constructor.apply(this, arguments);
return _ref2;
}
NoSuchWindowError.prototype.name = "Poltergeist.NoSuchWindowError";
NoSuchWindowError.prototype.args = function () {
return [];
};
return NoSuchWindowError;
})(Poltergeist.Error);

View file

@ -0,0 +1,21 @@
Poltergeist.ObsoleteNode = (function (_super) {
__extends(ObsoleteNode, _super);
function ObsoleteNode() {
_ref = ObsoleteNode.__super__.constructor.apply(this, arguments);
return _ref;
}
ObsoleteNode.prototype.name = "Poltergeist.ObsoleteNode";
ObsoleteNode.prototype.args = function () {
return [];
};
ObsoleteNode.prototype.toString = function () {
return this.name;
};
return ObsoleteNode;
})(Poltergeist.Error);

View file

@ -0,0 +1,17 @@
Poltergeist.StatusFailError = (function (_super) {
__extends(StatusFailError, _super);
function StatusFailError() {
_ref1 = StatusFailError.__super__.constructor.apply(this, arguments);
return _ref1;
}
StatusFailError.prototype.name = "Poltergeist.StatusFailError";
StatusFailError.prototype.args = function () {
return [];
};
return StatusFailError;
})(Poltergeist.Error);

View file

@ -0,0 +1,80 @@
Poltergeist.Server = (function () {
/**
* Server constructor
* @param owner
* @param port
* @constructor
*/
function Server(owner, port) {
this.server = require('webserver').create();
this.port = port;
this.owner = owner;
this.webServer = null;
}
/**
* Starts the web server
*/
Server.prototype.start = function () {
var self = this;
this.webServer = this.server.listen(this.port, function (request, response) {
self.handleRequest(request, response);
});
};
/**
* Send error back with code and message
* @param response
* @param code
* @param message
* @return {boolean}
*/
Server.prototype.sendError = function (response, code, message) {
response.statusCode = code;
response.setHeader('Content-Type', 'application/json');
response.write(JSON.stringify(message, null, 4));
response.close();
return true;
};
/**
* Send response back to the client
* @param response
* @param data
* @return {boolean}
*/
Server.prototype.send = function (response, data) {
console.log("RESPONSE: " + JSON.stringify(data, null, 4).substr(0, 200));
response.statusCode = 200;
response.setHeader('Content-Type', 'application/json');
response.write(JSON.stringify(data, null, 4));
response.close();
return true;
};
/**
* Handles a request to the server
* @param request
* @param response
* @return {boolean}
*/
Server.prototype.handleRequest = function (request, response) {
var commandData;
if (request.method !== "POST") {
return this.sendError(response, 405, "Only POST method is allowed in the service");
}
console.log("REQUEST: " + request.post + "\n");
try {
commandData = JSON.parse(request.post);
} catch (parseError) {
return this.sendError(response, 400, "JSON data invalid error: " + parseError.message);
}
return this.owner.serverRunCommand(commandData, response);
};
return Server;
})();

View file

@ -0,0 +1,28 @@
var __extends;
/**
* Helper function so objects can inherit from another
* @param child
* @param parent
* @return {Object}
* @private
*/
__extends = function (child, parent) {
var __hasProp;
__hasProp = {}.hasOwnProperty;
for (var key in parent) {
if (parent.hasOwnProperty(key)) {
if (__hasProp.call(parent, key)) {
child[key] = parent[key];
}
}
}
function ClassConstructor() {
this.constructor = child;
}
ClassConstructor.prototype = parent.prototype;
child.prototype = new ClassConstructor();
child.__super__ = parent.prototype;
return child;
};

View file

@ -0,0 +1,896 @@
var PoltergeistAgent;
PoltergeistAgent = (function () {
function PoltergeistAgent() {
this.elements = [];
this.nodes = {};
}
/**
* Executes an external call done from the web page class
* @param name
* @param args
* @return {*}
*/
PoltergeistAgent.prototype.externalCall = function (name, args) {
var error;
try {
return {
value: this[name].apply(this, args)
};
} catch (_error) {
error = _error;
return {
error: {
message: error.toString(),
stack: error.stack
}
};
}
};
/**
* Object stringifycation
* @param object
* @return {*}
*/
PoltergeistAgent.stringify = function (object) {
var error;
try {
return JSON.stringify(object, function (key, value) {
if (Array.isArray(this[key])) {
return this[key];
} else {
return value;
}
});
} catch (_error) {
error = _error;
if (error instanceof TypeError) {
return '"(cyclic structure)"';
} else {
throw error;
}
}
};
/**
* Name speaks for itself
* @return {string}
*/
PoltergeistAgent.prototype.currentUrl = function () {
return encodeURI(decodeURI(window.location.href));
};
/**
* Given a method of selection (xpath or css), a selector and a possible element to search
* tries to find the elements that matches such selection
* @param method
* @param selector
* @param within
* @return {Array}
*/
PoltergeistAgent.prototype.find = function (method, selector, within) {
var elementForXpath, error, i, results, xpath, _i, _len, _results;
if (within == null) {
within = document;
}
try {
if (method === "xpath") {
xpath = document.evaluate(selector, within, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
results = (function () {
var _i, _ref, _results;
_results = [];
for (i = _i = 0, _ref = xpath.snapshotLength; 0 <= _ref ? _i < _ref : _i > _ref; i = 0 <= _ref ? ++_i : --_i) {
_results.push(xpath.snapshotItem(i));
}
return _results;
})();
} else {
results = within.querySelectorAll(selector);
}
_results = [];
for (_i = 0, _len = results.length; _i < _len; _i++) {
elementForXpath = results[_i];
_results.push(this.register(elementForXpath));
}
return _results;
} catch (_error) {
error = _error;
if (error.code === DOMException.SYNTAX_ERR || error.code === 51) {
throw new PoltergeistAgent.InvalidSelector;
} else {
throw error;
}
}
};
/**
* Register the element in the agent
* @param element
* @return {number}
*/
PoltergeistAgent.prototype.register = function (element) {
this.elements.push(element);
return this.elements.length - 1;
};
/**
* Gets the size of the document
* @return {{height: number, width: number}}
*/
PoltergeistAgent.prototype.documentSize = function () {
return {
height: document.documentElement.scrollHeight || document.documentElement.clientHeight,
width: document.documentElement.scrollWidth || document.documentElement.clientWidth
};
};
/**
* Gets a Node by a given id
* @param id
* @return {PoltergeistAgent.Node}
*/
PoltergeistAgent.prototype.get = function (id) {
if (typeof this.nodes[id] == "undefined" || this.nodes[id] === null) {
//Let's try now the elements approach
if (typeof this.elements[id] == "undefined" || this.elements[id] === null) {
throw new PoltergeistAgent.ObsoleteNode;
}
return new PoltergeistAgent.Node(this, this.elements[id]);
}
return this.nodes[id];
};
/**
* Calls a Node agent function from the Node caller via delegates
* @param id
* @param name
* @param args
* @return {*}
*/
PoltergeistAgent.prototype.nodeCall = function (id, name, args) {
var node;
node = this.get(id);
if (node.isObsolete()) {
throw new PoltergeistAgent.ObsoleteNode;
}
//TODO: add some error control here, we might not be able to call name function
return node[name].apply(node, args);
};
PoltergeistAgent.prototype.beforeUpload = function (id) {
return this.get(id).setAttribute('_poltergeist_selected', '');
};
PoltergeistAgent.prototype.afterUpload = function (id) {
return this.get(id).removeAttribute('_poltergeist_selected');
};
PoltergeistAgent.prototype.clearLocalStorage = function () {
//TODO: WTF where is variable...
return localStorage.clear();
};
return PoltergeistAgent;
})();
PoltergeistAgent.ObsoleteNode = (function () {
function ObsoleteNode() {
}
ObsoleteNode.prototype.toString = function () {
return "PoltergeistAgent.ObsoleteNode";
};
return ObsoleteNode;
})();
PoltergeistAgent.InvalidSelector = (function () {
function InvalidSelector() {
}
InvalidSelector.prototype.toString = function () {
return "PoltergeistAgent.InvalidSelector";
};
return InvalidSelector;
})();
PoltergeistAgent.Node = (function () {
Node.EVENTS = {
FOCUS: ['blur', 'focus', 'focusin', 'focusout'],
MOUSE: ['click', 'dblclick', 'mousedown', 'mouseenter', 'mouseleave', 'mousemove', 'mouseover', 'mouseout', 'mouseup', 'contextmenu'],
FORM: ['submit']
};
function Node(agent, element) {
this.agent = agent;
this.element = element;
}
/**
* Give me the node id of the parent of this node
* @return {number}
*/
Node.prototype.parentId = function () {
return this.agent.register(this.element.parentNode);
};
/**
* Returns all the node parents ids up to first child of the dom
* @return {Array}
*/
Node.prototype.parentIds = function () {
var ids, parent;
ids = [];
parent = this.element.parentNode;
while (parent !== document) {
ids.push(this.agent.register(parent));
parent = parent.parentNode;
}
return ids;
};
/**
* Finds and returns the node ids that matches the selector within this node
* @param method
* @param selector
* @return {Array}
*/
Node.prototype.find = function (method, selector) {
return this.agent.find(method, selector, this.element);
};
/**
* Checks whether the node is obsolete or not
* @return boolean
*/
Node.prototype.isObsolete = function () {
var obsolete;
obsolete = function (element) {
if (element.parentNode != null) {
if (element.parentNode === document) {
return false;
} else {
return obsolete(element.parentNode);
}
} else {
return true;
}
};
return obsolete(this.element);
};
Node.prototype.changed = function () {
var event;
event = document.createEvent('HTMLEvents');
event.initEvent('change', true, false);
return this.element.dispatchEvent(event);
};
Node.prototype.input = function () {
var event;
event = document.createEvent('HTMLEvents');
event.initEvent('input', true, false);
return this.element.dispatchEvent(event);
};
Node.prototype.keyupdowned = function (eventName, keyCode) {
var event;
event = document.createEvent('UIEvents');
event.initEvent(eventName, true, true);
event.keyCode = keyCode;
event.which = keyCode;
event.charCode = 0;
return this.element.dispatchEvent(event);
};
Node.prototype.keypressed = function (altKey, ctrlKey, shiftKey, metaKey, keyCode, charCode) {
var event;
event = document.createEvent('UIEvents');
event.initEvent('keypress', true, true);
event.window = this.agent.window;
event.altKey = altKey;
event.ctrlKey = ctrlKey;
event.shiftKey = shiftKey;
event.metaKey = metaKey;
event.keyCode = keyCode;
event.charCode = charCode;
event.which = keyCode;
return this.element.dispatchEvent(event);
};
/**
* Tells if the node is inside the body of the document and not somewhere else
* @return {boolean}
*/
Node.prototype.insideBody = function () {
return this.element === document.body || document.evaluate('ancestor::body', this.element, null, XPathResult.BOOLEAN_TYPE, null).booleanValue;
};
/**
* Returns all text visible or not of the node
* @return {string}
*/
Node.prototype.allText = function () {
return this.element.textContent;
};
/**
* Returns the inner html our outer
* @returns {string}
*/
Node.prototype.allHTML = function (type) {
var returnType = type || 'inner';
if (returnType === "inner") {
return this.element.innerHTML;
}
if (returnType === "outer") {
if (this.element.outerHTML) {
return this.element.outerHTML;
}
// polyfill:
var wrapper = document.createElement('div');
wrapper.appendChild(this.element.cloneNode(true));
return wrapper.innerHTML;
}
return '';
};
/**
* If the element is visible then we return the text
* @return {string}
*/
Node.prototype.visibleText = function () {
if (!this.isVisible(null)) {
return null;
}
if (this.element.nodeName === "TEXTAREA") {
return this.element.textContent;
}
return this.element.innerText;
};
/**
* Deletes the actual text being represented by a selection object from the node's element DOM.
* @return {*}
*/
Node.prototype.deleteText = function () {
var range;
range = document.createRange();
range.selectNodeContents(this.element);
window.getSelection().removeAllRanges();
window.getSelection().addRange(range);
return window.getSelection().deleteFromDocument();
};
/**
* Returns all the attributes {name:value} in the element
* @return {{}}
*/
Node.prototype.getAttributes = function () {
var attributes, i, elementAttributes;
elementAttributes = this.element.attributes;
attributes = {};
for (i = 0; i < elementAttributes.length; i++) {
attributes[elementAttributes[i].name] = elementAttributes[i].value.replace("\n", "\\n");
}
return attributes;
};
/**
* Name speaks for it self, returns the value of a given attribute by name
* @param name
* @return {string}
*/
Node.prototype.getAttribute = function (name) {
if (name === 'checked' || name === 'selected' || name === 'multiple') {
return this.element[name];
}
return this.element.getAttribute(name);
};
/**
* Scrolls the current element into the visible area of the browser window
* @return {*}
*/
Node.prototype.scrollIntoView = function () {
return this.element.scrollIntoViewIfNeeded();
};
/**
* Returns the element.value property with special treatment if the element is a select
* @return {*}
*/
Node.prototype.value = function () {
var options, i, values;
if (this.element.tagName.toLowerCase() === 'select' && this.element.multiple) {
values = [];
options = this.element.children;
for (i = 0; i < options.length; i++) {
if (options[i].selected) {
values.push(options[i].value);
}
}
return values;
}
return this.element.value;
};
/**
* Sets a given value in the element value property by simulation key interaction
* @param value
* @return {*}
*/
Node.prototype.set = function (value) {
var char, keyCode, i, len;
if (this.element.readOnly) {
return null;
}
//respect the maxLength property if present
if (this.element.maxLength >= 0) {
value = value.substr(0, this.element.maxLength);
}
this.element.value = '';
this.trigger('focus');
if (this.element.type === 'number') {
this.element.value = value;
} else {
for (i = 0, len = value.length; i < len; i++) {
char = value[i];
keyCode = this.characterToKeyCode(char);
this.keyupdowned('keydown', keyCode);
this.element.value += char;
this.keypressed(false, false, false, false, char.charCodeAt(0), char.charCodeAt(0));
this.keyupdowned('keyup', keyCode);
}
}
this.changed();
this.input();
return this.trigger('blur');
};
/**
* Is the node multiple
* @return {boolean}
*/
Node.prototype.isMultiple = function () {
return this.element.multiple;
};
/**
* Sets the value of an attribute given by name
* @param name
* @param value
* @return {boolean}
*/
Node.prototype.setAttribute = function (name, value) {
if (value === null) {
return this.removeAttribute(name);
}
this.element.setAttribute(name, value);
return true;
};
/**
* Removes and attribute by name
* @param name
* @return {boolean}
*/
Node.prototype.removeAttribute = function (name) {
this.element.removeAttribute(name);
return true;
};
/**
* Selects the current node
* @param value
* @return {boolean}
*/
Node.prototype.select = function (value) {
if (value === false && !this.element.parentNode.multiple) {
return false;
}
this.element.selected = value;
this.changed();
return true;
};
/**
* Selects the radio button that has the defined value
* @param value
* @return {boolean}
*/
Node.prototype.selectRadioValue = function (value) {
if (this.element.value == value) {
this.element.checked = true;
this.trigger('focus');
this.trigger('click');
this.changed();
return true;
}
var formElements = this.element.form.elements;
var name = this.element.getAttribute('name');
var element, i;
var deselectAllRadios = function (elements, radioName) {
var inputRadioElement;
for (i = 0; i < elements.length; i++) {
inputRadioElement = elements[i];
if (inputRadioElement.tagName.toLowerCase() == 'input' && inputRadioElement.type.toLowerCase() == 'radio' && inputRadioElement.name == radioName) {
inputRadioElement.checked = false;
}
}
};
var radioChange = function (radioElement) {
var radioEvent;
radioEvent = document.createEvent('HTMLEvents');
radioEvent.initEvent('change', true, false);
return radioElement.dispatchEvent(radioEvent);
};
var radioClickEvent = function (radioElement, name) {
var radioEvent;
radioEvent = document.createEvent('MouseEvent');
radioEvent.initMouseEvent(name, true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
return radioElement.dispatchEvent(radioEvent);
};
if (!name) {
throw new Poltergeist.BrowserError('The radio button does not have the value "' + value + '"');
}
for (i = 0; i < formElements.length; i++) {
element = formElements[i];
if (element.tagName.toLowerCase() == 'input' && element.type.toLowerCase() == 'radio' && element.name === name) {
if (value === element.value) {
deselectAllRadios(formElements, name);
element.checked = true;
radioClickEvent(element, 'click');
radioChange(element);
return true;
}
}
}
throw new Poltergeist.BrowserError('The radio group "' + name + '" does not have an option "' + value + '"');
};
/**
* Checks or uncheck a radio option
* @param value
* @return {boolean}
*/
Node.prototype.checked = function (value) {
//TODO: add error control for the checked stuff
this.element.checked = value;
return true;
};
/**
* Returns the element tag name as is, no transformations done
* @return {string}
*/
Node.prototype.tagName = function () {
return this.element.tagName;
};
/**
* Checks if the element is visible either by itself of because the parents are visible
* @param element
* @return {boolean}
*/
Node.prototype.isVisible = function (element) {
var nodeElement = element || this.element;
if (window.getComputedStyle(nodeElement).display === 'none') {
return false;
} else if (nodeElement.parentElement) {
return this.isVisible(nodeElement.parentElement);
} else {
return true;
}
};
/**
* Is the node disabled for operations with it?
* @return {boolean}
*/
Node.prototype.isDisabled = function () {
return this.element.disabled || this.element.tagName === 'OPTION' && this.element.parentNode.disabled;
};
/**
* Does the node contains the selections
* @return {boolean}
*/
Node.prototype.containsSelection = function () {
var selectedNode;
selectedNode = document.getSelection().focusNode;
if (!selectedNode) {
return false;
}
//this magic number is NODE.TEXT_NODE
if (selectedNode.nodeType === 3) {
selectedNode = selectedNode.parentNode;
}
return this.element.contains(selectedNode);
};
/**
* Returns the offset of the node in relation to the current frame
* @return {{top: number, left: number}}
*/
Node.prototype.frameOffset = function () {
var offset, rect, style, win;
win = window;
offset = {
top: 0,
left: 0
};
while (win.frameElement) {
rect = win.frameElement.getClientRects()[0];
style = win.getComputedStyle(win.frameElement);
win = win.parent;
offset.top += rect.top + parseInt(style.getPropertyValue("padding-top"), 10);
offset.left += rect.left + parseInt(style.getPropertyValue("padding-left"), 10);
}
return offset;
};
/**
* Returns the object position in relation to the window
* @return {{top: *, right: *, left: *, bottom: *, width: *, height: *}}
*/
Node.prototype.position = function () {
var frameOffset, pos, rect;
rect = this.element.getClientRects()[0];
if (!rect) {
throw new PoltergeistAgent.ObsoleteNode;
}
frameOffset = this.frameOffset();
pos = {
top: rect.top + frameOffset.top,
right: rect.right + frameOffset.left,
left: rect.left + frameOffset.left,
bottom: rect.bottom + frameOffset.top,
width: rect.width,
height: rect.height
};
return pos;
};
/**
* Triggers a DOM event related to the node element
* @param name
* @return {boolean}
*/
Node.prototype.trigger = function (name) {
var event;
if (Node.EVENTS.MOUSE.indexOf(name) !== -1) {
event = document.createEvent('MouseEvent');
event.initMouseEvent(name, true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
} else if (Node.EVENTS.FOCUS.indexOf(name) !== -1) {
event = this.obtainEvent(name);
} else if (Node.EVENTS.FORM.indexOf(name) !== -1) {
event = this.obtainEvent(name);
} else {
throw "Unknown event";
}
return this.element.dispatchEvent(event);
};
/**
* Creates a generic HTMLEvent to be use in the node element
* @param name
* @return {Event}
*/
Node.prototype.obtainEvent = function (name) {
var event;
event = document.createEvent('HTMLEvents');
event.initEvent(name, true, true);
return event;
};
/**
* Does a check to see if the coordinates given
* match the node element or some of the parents chain
* @param x
* @param y
* @return {*}
*/
Node.prototype.mouseEventTest = function (x, y) {
var elementForXpath, frameOffset, origEl;
frameOffset = this.frameOffset();
x -= frameOffset.left;
y -= frameOffset.top;
elementForXpath = origEl = document.elementFromPoint(x, y);
while (elementForXpath) {
if (elementForXpath === this.element) {
return {
status: 'success'
};
} else {
elementForXpath = elementForXpath.parentNode;
}
}
return {
status: 'failure',
selector: origEl && this.getSelector(origEl)
};
};
/**
* Returns the node selector in CSS style (NO xpath)
* @param elementForXpath
* @return {string}
*/
Node.prototype.getSelector = function (elementForXpath) {
var className, selector, i, len, classNames;
selector = elementForXpath.tagName !== 'HTML' ? this.getSelector(elementForXpath.parentNode) + ' ' : '';
selector += elementForXpath.tagName.toLowerCase();
if (elementForXpath.id) {
selector += "#" + elementForXpath.id;
}
classNames = elementForXpath.classList;
for (i = 0, len = classNames.length; i < len; i++) {
className = classNames[i];
selector += "." + className;
}
return selector;
};
/**
* Returns the key code that represents the character
* @param character
* @return {number}
*/
Node.prototype.characterToKeyCode = function (character) {
var code, specialKeys;
code = character.toUpperCase().charCodeAt(0);
specialKeys = {
96: 192,
45: 189,
61: 187,
91: 219,
93: 221,
92: 220,
59: 186,
39: 222,
44: 188,
46: 190,
47: 191,
127: 46,
126: 192,
33: 49,
64: 50,
35: 51,
36: 52,
37: 53,
94: 54,
38: 55,
42: 56,
40: 57,
41: 48,
95: 189,
43: 187,
123: 219,
125: 221,
124: 220,
58: 186,
34: 222,
60: 188,
62: 190,
63: 191
};
return specialKeys[code] || code;
};
/**
* Checks if one element is equal to other given by its node id
* @param other_id
* @return {boolean}
*/
Node.prototype.isDOMEqual = function (other_id) {
return this.element === this.agent.get(other_id).element;
};
/**
* The following function allows one to pass an element and an XML document to find a unique string XPath expression leading back to that element.
* @param element
* @return {string}
*/
Node.prototype.getXPathForElement = function (element) {
var elementForXpath = element || this.element;
var xpath = '';
var pos, tempitem2;
while (elementForXpath !== document.documentElement) {
pos = 0;
tempitem2 = elementForXpath;
while (tempitem2) {
if (tempitem2.nodeType === 1 && tempitem2.nodeName === elementForXpath.nodeName) { // If it is ELEMENT_NODE of the same name
pos += 1;
}
tempitem2 = tempitem2.previousSibling;
}
xpath = "*[name()='" + elementForXpath.nodeName + "' and namespace-uri()='" + (elementForXpath.namespaceURI === null ? '' : elementForXpath.namespaceURI) + "'][" + pos + ']' + '/' + xpath;
elementForXpath = elementForXpath.parentNode;
}
xpath = '/*' + "[name()='" + document.documentElement.nodeName + "' and namespace-uri()='" + (elementForXpath.namespaceURI === null ? '' : elementForXpath.namespaceURI) + "']" + '/' + xpath;
xpath = xpath.replace(/\/$/, '');
return xpath;
};
/**
* Deselect all the options for this element
*/
Node.prototype.deselectAllOptions = function () {
//TODO: error control when the node is not a select node
var i, l = this.element.options.length;
for (i = 0; i < l; i++) {
this.element.options[i].selected = false;
}
};
return Node;
})();
window.__poltergeist = new PoltergeistAgent;
document.addEventListener('DOMContentLoaded', function () {
return console.log('__DOMContentLoaded');
});
window.confirm = function (message) {
return true;
};
window.prompt = function (message, _default) {
return _default || null;
};

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,29 @@
var Poltergeist, system, _ref, _ref1, _ref2;
//Inheritance tool
phantom.injectJs("" + phantom.libraryPath + "/Tools/inherit.js");
//Poltergeist main object
phantom.injectJs("" + phantom.libraryPath + "/poltergeist.js");
//Errors that are controller in the poltergeist code
phantom.injectJs("" + phantom.libraryPath + "/Errors/error.js");
phantom.injectJs("" + phantom.libraryPath + "/Errors/obsolete_node.js");
phantom.injectJs("" + phantom.libraryPath + "/Errors/invalid_selector.js");
phantom.injectJs("" + phantom.libraryPath + "/Errors/frame_not_found.js");
phantom.injectJs("" + phantom.libraryPath + "/Errors/mouse_event_failed.js");
phantom.injectJs("" + phantom.libraryPath + "/Errors/javascript_error.js");
phantom.injectJs("" + phantom.libraryPath + "/Errors/browser_error.js");
phantom.injectJs("" + phantom.libraryPath + "/Errors/status_fail_error.js");
phantom.injectJs("" + phantom.libraryPath + "/Errors/no_such_window_error.js");
//web server to control the commands
phantom.injectJs("" + phantom.libraryPath + "/Server/server.js");
phantom.injectJs("" + phantom.libraryPath + "/web_page.js");
phantom.injectJs("" + phantom.libraryPath + "/node.js");
phantom.injectJs("" + phantom.libraryPath + "/browser.js");
system = require('system');
new Poltergeist(system.args[1], system.args[2], system.args[3]);

View file

@ -0,0 +1,161 @@
var __slice = [].slice;
Poltergeist.Node = (function () {
var name, _fn, _i, _len, _ref;
var xpathStringLiteral;
Node.DELEGATES = ['allText', 'visibleText', 'getAttribute', 'value', 'set', 'checked',
'setAttribute', 'isObsolete', 'removeAttribute', 'isMultiple',
'select', 'tagName', 'find', 'getAttributes', 'isVisible',
'position', 'trigger', 'input', 'parentId', 'parentIds', 'mouseEventTest',
'scrollIntoView', 'isDOMEqual', 'isDisabled', 'deleteText', 'selectRadioValue',
'containsSelection', 'allHTML', 'changed', 'getXPathForElement', 'deselectAllOptions'];
function Node(page, id) {
this.page = page;
this.id = id;
}
/**
* Returns the parent Node of this Node
* @return {Poltergeist.Node}
*/
Node.prototype.parent = function () {
return new Poltergeist.Node(this.page, this.parentId());
};
_ref = Node.DELEGATES;
_fn = function (name) {
return Node.prototype[name] = function () {
var args = [];
if (arguments.length >= 1) {
args = __slice.call(arguments, 0)
}
return this.page.nodeCall(this.id, name, args);
};
};
//Adding all the delegates from the agent Node to this Node
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
name = _ref[_i];
_fn(name);
}
xpathStringLiteral = function (s) {
if (s.indexOf('"') === -1)
return '"' + s + '"';
if (s.indexOf("'") === -1)
return "'" + s + "'";
return 'concat("' + s.replace(/"/g, '",\'"\',"') + '")';
};
/**
* Gets an x,y position tailored for mouse event actions
* @return {{x, y}}
*/
Node.prototype.mouseEventPosition = function () {
var middle, pos, viewport;
viewport = this.page.viewportSize();
pos = this.position();
middle = function (start, end, size) {
return start + ((Math.min(end, size) - start) / 2);
};
return {
x: middle(pos.left, pos.right, viewport.width),
y: middle(pos.top, pos.bottom, viewport.height)
};
};
/**
* Executes a phantomjs native mouse event
* @param name
* @return {{x, y}}
*/
Node.prototype.mouseEvent = function (name) {
var pos, test;
this.scrollIntoView();
pos = this.mouseEventPosition();
test = this.mouseEventTest(pos.x, pos.y);
if (test.status === 'success') {
if (name === 'rightclick') {
this.page.mouseEvent('click', pos.x, pos.y, 'right');
this.trigger('contextmenu');
} else {
this.page.mouseEvent(name, pos.x, pos.y);
}
return pos;
} else {
throw new Poltergeist.MouseEventFailed(name, test.selector, pos);
}
};
/**
* Executes a mouse based drag from one node to another
* @param other
* @return {{x, y}}
*/
Node.prototype.dragTo = function (other) {
var otherPosition, position;
this.scrollIntoView();
position = this.mouseEventPosition();
otherPosition = other.mouseEventPosition();
this.page.mouseEvent('mousedown', position.x, position.y);
return this.page.mouseEvent('mouseup', otherPosition.x, otherPosition.y);
};
/**
* Checks if one node is equal to another
* @param other
* @return {boolean}
*/
Node.prototype.isEqual = function (other) {
return this.page === other.page && this.isDOMEqual(other.id);
};
/**
* The value to select
* @param value
* @param multiple
*/
Node.prototype.select_option = function (value, multiple) {
var tagName = this.tagName().toLowerCase();
if (tagName === "select") {
var escapedOption = xpathStringLiteral(value);
// The value of an option is the normalized version of its text when it has no value attribute
var optionQuery = ".//option[@value = " + escapedOption + " or (not(@value) and normalize-space(.) = " + escapedOption + ")]";
var ids = this.find("xpath", optionQuery);
var polterNode = this.page.get(ids[0]);
if (multiple || !this.getAttribute('multiple')) {
if (!polterNode.getAttribute('selected')) {
polterNode.select(value);
this.trigger('click');
this.input();
}
return true;
}
this.deselectAllOptions();
polterNode.select(value);
this.trigger('click');
this.input();
return true;
} else if (tagName === "input" && this.getAttribute("type").toLowerCase() === "radio") {
return this.selectRadioValue(value);
}
throw new Poltergeist.BrowserError("The element is not a select or radio input");
};
return Node;
}).call(this);

View file

@ -0,0 +1,77 @@
Poltergeist = (function () {
/**
* The MAIN class of the project
* @param port
* @param width
* @param height
* @constructor
*/
function Poltergeist(port, width, height) {
var self;
this.browser = new Poltergeist.Browser(this, width, height);
this.commandServer = new Poltergeist.Server(this, port);
this.commandServer.start();
self = this;
phantom.onError = function (message, stack) {
return self.onError(message, stack);
};
this.running = false;
}
/**
* Tries to execute a command send by a client and returns the command response
* or error if something happened
* @param command
* @param serverResponse
* @return {boolean}
*/
Poltergeist.prototype.serverRunCommand = function (command, serverResponse) {
var error;
this.running = true;
try {
return this.browser.serverRunCommand(command, serverResponse);
} catch (_error) {
error = _error;
if (error instanceof Poltergeist.Error) {
return this.serverSendError(error, serverResponse);
}
return this.serverSendError(new Poltergeist.BrowserError(error.toString(), error.stack), serverResponse);
}
};
/**
* Sends error back to the client
* @param error
* @param serverResponse
* @return {boolean}
*/
Poltergeist.prototype.serverSendError = function (error, serverResponse) {
var errorObject;
errorObject = {
error: {
name: error.name || 'Generic',
args: error.args && error.args() || [error.toString()]
}
};
return this.commandServer.sendError(serverResponse, 500, errorObject);
};
/**
* Send the response back to the client
* @param response Data to send to the client
* @param serverResponse Phantomjs response object associated to the client request
* @return {boolean}
*/
Poltergeist.prototype.serverSendResponse = function (response, serverResponse) {
return this.commandServer.send(serverResponse, {response: response});
};
return Poltergeist;
})();
window.Poltergeist = Poltergeist;

View file

@ -0,0 +1,829 @@
var __slice = [].slice;
var __indexOf = [].indexOf || function (item) {
for (var i = 0, l = this.length; i < l; i++) {
if (i in this && this[i] === item) return i;
}
return -1;
};
Poltergeist.WebPage = (function () {
var command, delegate, commandFunctionBind, delegateFunctionBind, i, j, commandsLength, delegatesRefLength, commandsRef, delegatesRef,
_this = this;
//Native or not webpage callbacks
WebPage.CALLBACKS = ['onAlert', 'onConsoleMessage', 'onLoadFinished', 'onInitialized', 'onLoadStarted', 'onResourceRequested',
'onResourceReceived', 'onError', 'onNavigationRequested', 'onUrlChanged', 'onPageCreated', 'onClosing'];
// Delegates the execution to the phantomjs page native functions but directly available in the WebPage object
WebPage.DELEGATES = ['open', 'sendEvent', 'uploadFile', 'release', 'render', 'renderBase64', 'goBack', 'goForward', 'reload'];
//Commands to execute on behalf of the browser but on the current page
WebPage.COMMANDS = ['currentUrl', 'find', 'nodeCall', 'documentSize', 'beforeUpload', 'afterUpload', 'clearLocalStorage'];
WebPage.EXTENSIONS = [];
function WebPage(nativeWebPage) {
var callback, i, callBacksLength, callBacksRef;
//Lets create the native phantomjs webpage
if (nativeWebPage === null || typeof nativeWebPage == "undefined") {
this._native = require('webpage').create();
} else {
this._native = nativeWebPage;
}
this.id = 0;
this.source = null;
this.closed = false;
this.state = 'default';
this.urlBlacklist = [];
this.frames = [];
this.errors = [];
this._networkTraffic = {};
this._tempHeaders = {};
this._blockedUrls = [];
callBacksRef = WebPage.CALLBACKS;
for (i = 0, callBacksLength = callBacksRef.length; i < callBacksLength; i++) {
callback = callBacksRef[i];
this.bindCallback(callback);
}
}
//Bind the commands we can run from the browser to the current page
commandsRef = WebPage.COMMANDS;
commandFunctionBind = function (command) {
return WebPage.prototype[command] = function () {
var args;
args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
return this.runCommand(command, args);
};
};
for (i = 0, commandsLength = commandsRef.length; i < commandsLength; i++) {
command = commandsRef[i];
commandFunctionBind(command);
}
//Delegates bind applications
delegatesRef = WebPage.DELEGATES;
delegateFunctionBind = function (delegate) {
return WebPage.prototype[delegate] = function () {
return this._native[delegate].apply(this._native, arguments);
};
};
for (j = 0, delegatesRefLength = delegatesRef.length; j < delegatesRefLength; j++) {
delegate = delegatesRef[j];
delegateFunctionBind(delegate);
}
/**
* This callback is invoked after the web page is created but before a URL is loaded.
* The callback may be used to change global objects.
* @return {*}
*/
WebPage.prototype.onInitializedNative = function () {
this.id += 1;
this.source = null;
this.injectAgent();
this.removeTempHeaders();
return this.setScrollPosition({
left: 0,
top: 0
});
};
/**
* This callback is invoked when the WebPage object is being closed,
* either via page.close in the PhantomJS outer space or via window.close in the page's client-side.
* @return {boolean}
*/
WebPage.prototype.onClosingNative = function () {
this.handle = null;
return this.closed = true;
};
/**
* This callback is invoked when there is a JavaScript console message on the web page.
* The callback may accept up to three arguments: the string for the message, the line number, and the source identifier.
* @param message
* @param line
* @param sourceId
* @return {boolean}
*/
WebPage.prototype.onConsoleMessageNative = function (message, line, sourceId) {
if (message === '__DOMContentLoaded') {
this.source = this._native.content;
return false;
}
console.log(message);
return true;
};
/**
* This callback is invoked when the page starts the loading. There is no argument passed to the callback.
* @return {number}
*/
WebPage.prototype.onLoadStartedNative = function () {
this.state = 'loading';
return this.requestId = this.lastRequestId;
};
/**
* This callback is invoked when the page finishes the loading.
* It may accept a single argument indicating the page's status: 'success' if no network errors occurred, otherwise 'fail'.
* @param status
* @return {string}
*/
WebPage.prototype.onLoadFinishedNative = function (status) {
this.status = status;
this.state = 'default';
if (this.source === null || typeof this.source == "undefined") {
this.source = this._native.content;
} else {
this.source = this._native.content;
}
return this.source;
};
/**
* This callback is invoked when there is a JavaScript execution error.
* It is a good way to catch problems when evaluating a script in the web page context.
* The arguments passed to the callback are the error message and the stack trace [as an Array].
* @param message
* @param stack
* @return {Number}
*/
WebPage.prototype.onErrorNative = function (message, stack) {
var stackString;
stackString = message;
stack.forEach(function (frame) {
stackString += "\n";
stackString += " at " + frame.file + ":" + frame.line;
if (frame["function"] && frame["function"] !== '') {
return stackString += " in " + frame["function"];
}
});
return this.errors.push({
message: message,
stack: stackString
});
};
/**
* This callback is invoked when the page requests a resource.
* The first argument to the callback is the requestData metadata object.
* The second argument is the networkRequest object itself.
* @param requestData
* @param networkRequest
* @return {*}
*/
WebPage.prototype.onResourceRequestedNative = function (requestData, networkRequest) {
var abort;
abort = this.urlBlacklist.some(function (blacklistedUrl) {
return requestData.url.indexOf(blacklistedUrl) !== -1;
});
if (abort) {
if (this._blockedUrls.indexOf(requestData.url) === -1) {
this._blockedUrls.push(requestData.url);
}
//TODO: check this, as it raises onResourceError
return networkRequest.abort();
}
this.lastRequestId = requestData.id;
if (requestData.url === this.redirectURL) {
this.redirectURL = null;
this.requestId = requestData.id;
}
return this._networkTraffic[requestData.id] = {
request: requestData,
responseParts: []
};
};
/**
* This callback is invoked when a resource requested by the page is received.
* The only argument to the callback is the response metadata object.
* @param response
* @return {*}
*/
WebPage.prototype.onResourceReceivedNative = function (response) {
var networkTrafficElement;
if ((networkTrafficElement = this._networkTraffic[response.id]) != null) {
networkTrafficElement.responseParts.push(response);
}
if (this.requestId === response.id) {
if (response.redirectURL) {
return this.redirectURL = response.redirectURL;
}
this.statusCode = response.status;
return this._responseHeaders = response.headers;
}
};
/**
* Inject the poltergeist agent into the webpage
* @return {Array}
*/
WebPage.prototype.injectAgent = function () {
var extension, isAgentInjected, i, extensionsRefLength, extensionsRef, injectionResults;
isAgentInjected = this["native"]().evaluate(function () {
return typeof window.__poltergeist;
});
if (isAgentInjected === "undefined") {
this["native"]().injectJs("" + phantom.libraryPath + "/agent.js");
extensionsRef = WebPage.EXTENSIONS;
injectionResults = [];
for (i = 0, extensionsRefLength = extensionsRef.length; i < extensionsRefLength; i++) {
extension = extensionsRef[i];
injectionResults.push(this["native"]().injectJs(extension));
}
return injectionResults;
}
};
/**
* Injects a Javascript file extension into the
* @param file
* @return {*}
*/
WebPage.prototype.injectExtension = function (file) {
//TODO: add error control, for example, check if file already in the extensions array, check if the file exists, etc.
WebPage.EXTENSIONS.push(file);
return this["native"]().injectJs(file);
};
/**
* Returns the native phantomjs webpage object
* @return {*}
*/
WebPage.prototype["native"] = function () {
if (this.closed) {
throw new Poltergeist.NoSuchWindowError;
}
return this._native;
};
/**
* Returns the current page window name
* @return {*}
*/
WebPage.prototype.windowName = function () {
return this["native"]().windowName;
};
/**
* Returns the keyCode of a given key as set in the phantomjs values
* @param name
* @return {number}
*/
WebPage.prototype.keyCode = function (name) {
return this["native"]().event.key[name];
};
/**
* Waits for the page to reach a certain state
* @param state
* @param callback
* @return {*}
*/
WebPage.prototype.waitState = function (state, callback) {
var self = this;
if (this.state === state) {
return callback.call();
} else {
return setTimeout((function () {
return self.waitState(state, callback);
}), 100);
}
};
/**
* Sets the browser header related to basic authentication protocol
* @param user
* @param password
* @return {boolean}
*/
WebPage.prototype.setHttpAuth = function (user, password) {
var allHeaders = this.getCustomHeaders();
if (user === false || password === false) {
if (allHeaders.hasOwnProperty("Authorization")) {
delete allHeaders["Authorization"];
}
this.setCustomHeaders(allHeaders);
return true;
}
var userName = user || "";
var userPassword = password || "";
allHeaders["Authorization"] = "Basic " + btoa(userName + ":" + userPassword);
this.setCustomHeaders(allHeaders);
return true;
};
/**
* Returns all the network traffic associated to the rendering of this page
* @return {{}}
*/
WebPage.prototype.networkTraffic = function () {
return this._networkTraffic;
};
/**
* Clears all the recorded network traffic related to the current page
* @return {{}}
*/
WebPage.prototype.clearNetworkTraffic = function () {
return this._networkTraffic = {};
};
/**
* Returns the blocked urls that the page will not load
* @return {Array}
*/
WebPage.prototype.blockedUrls = function () {
return this._blockedUrls;
};
/**
* Clean all the urls that should not be loaded
* @return {Array}
*/
WebPage.prototype.clearBlockedUrls = function () {
return this._blockedUrls = [];
};
/**
* This property stores the content of the web page's currently active frame
* (which may or may not be the main frame), enclosed in an HTML/XML element.
* @return {string}
*/
WebPage.prototype.content = function () {
return this["native"]().frameContent;
};
/**
* Returns the current active frame title
* @return {string}
*/
WebPage.prototype.title = function () {
return this["native"]().frameTitle;
};
/**
* Returns if possible the frame url of the frame given by name
* @param frameName
* @return {string}
*/
WebPage.prototype.frameUrl = function (frameName) {
var query;
query = function (frameName) {
var iframeReference;
if ((iframeReference = document.querySelector("iframe[name='" + frameName + "']")) != null) {
return iframeReference.src;
}
return void 0;
};
return this.evaluate(query, frameName);
};
/**
* Remove the errors caught on the page
* @return {Array}
*/
WebPage.prototype.clearErrors = function () {
return this.errors = [];
};
/**
* Returns the response headers associated to this page
* @return {{}}
*/
WebPage.prototype.responseHeaders = function () {
var headers;
headers = {};
this._responseHeaders.forEach(function (item) {
return headers[item.name] = item.value;
});
return headers;
};
/**
* Get Cookies visible to the current URL (though, for setting, use of page.addCookie is preferred).
* This array will be pre-populated by any existing Cookie data visible to this URL that is stored in the CookieJar, if any.
* @return {*}
*/
WebPage.prototype.cookies = function () {
return this["native"]().cookies;
};
/**
* Delete any Cookies visible to the current URL with a 'name' property matching cookieName.
* Returns true if successfully deleted, otherwise false.
* @param name
* @return {*}
*/
WebPage.prototype.deleteCookie = function (name) {
return this["native"]().deleteCookie(name);
};
/**
* This property gets the size of the viewport for the layout process.
* @return {*}
*/
WebPage.prototype.viewportSize = function () {
return this["native"]().viewportSize;
};
/**
* This property sets the size of the viewport for the layout process.
* @param size
* @return {*}
*/
WebPage.prototype.setViewportSize = function (size) {
return this["native"]().viewportSize = size;
};
/**
* This property specifies the scaling factor for the page.render and page.renderBase64 functions.
* @param zoomFactor
* @return {*}
*/
WebPage.prototype.setZoomFactor = function (zoomFactor) {
return this["native"]().zoomFactor = zoomFactor;
};
/**
* This property defines the size of the web page when rendered as a PDF.
* See: http://phantomjs.org/api/webpage/property/paper-size.html
* @param size
* @return {*}
*/
WebPage.prototype.setPaperSize = function (size) {
return this["native"]().paperSize = size;
};
/**
* This property gets the scroll position of the web page.
* @return {*}
*/
WebPage.prototype.scrollPosition = function () {
return this["native"]().scrollPosition;
};
/**
* This property defines the scroll position of the web page.
* @param pos
* @return {*}
*/
WebPage.prototype.setScrollPosition = function (pos) {
return this["native"]().scrollPosition = pos;
};
/**
* This property defines the rectangular area of the web page to be rasterized when page.render is invoked.
* If no clipping rectangle is set, page.render will process the entire web page.
* @return {*}
*/
WebPage.prototype.clipRect = function () {
return this["native"]().clipRect;
};
/**
* This property defines the rectangular area of the web page to be rasterized when page.render is invoked.
* If no clipping rectangle is set, page.render will process the entire web page.
* @param rect
* @return {*}
*/
WebPage.prototype.setClipRect = function (rect) {
return this["native"]().clipRect = rect;
};
/**
* Returns the size of an element given by a selector and its position relative to the viewport.
* @param selector
* @return {Object}
*/
WebPage.prototype.elementBounds = function (selector) {
return this["native"]().evaluate(function (selector) {
return document.querySelector(selector).getBoundingClientRect();
}, selector);
};
/**
* Defines the user agent sent to server when the web page requests resources.
* @param userAgent
* @return {*}
*/
WebPage.prototype.setUserAgent = function (userAgent) {
return this["native"]().settings.userAgent = userAgent;
};
/**
* Returns the additional HTTP request headers that will be sent to the server for EVERY request.
* @return {{}}
*/
WebPage.prototype.getCustomHeaders = function () {
return this["native"]().customHeaders;
};
/**
* Gets the additional HTTP request headers that will be sent to the server for EVERY request.
* @param headers
* @return {*}
*/
WebPage.prototype.setCustomHeaders = function (headers) {
return this["native"]().customHeaders = headers;
};
/**
* Adds a one time only request header, after being used it will be deleted
* @param header
* @return {Array}
*/
WebPage.prototype.addTempHeader = function (header) {
var name, value, tempHeaderResult;
tempHeaderResult = [];
for (name in header) {
if (header.hasOwnProperty(name)) {
value = header[name];
tempHeaderResult.push(this._tempHeaders[name] = value);
}
}
return tempHeaderResult;
};
/**
* Remove the temporary headers we have set via addTempHeader
* @return {*}
*/
WebPage.prototype.removeTempHeaders = function () {
var allHeaders, name, value, tempHeadersRef;
allHeaders = this.getCustomHeaders();
tempHeadersRef = this._tempHeaders;
for (name in tempHeadersRef) {
if (tempHeadersRef.hasOwnProperty(name)) {
value = tempHeadersRef[name];
delete allHeaders[name];
}
}
return this.setCustomHeaders(allHeaders);
};
/**
* If possible switch to the frame given by name
* @param name
* @return {boolean}
*/
WebPage.prototype.pushFrame = function (name) {
if (this["native"]().switchToFrame(name)) {
this.frames.push(name);
return true;
}
return false;
};
/**
* Switch to parent frame, use with caution:
* popFrame assumes you are in frame, pop frame not being in a frame
* leaves unexpected behaviour
* @return {*}
*/
WebPage.prototype.popFrame = function () {
//TODO: add some error control here, some way to check we are in a frame or not
this.frames.pop();
return this["native"]().switchToParentFrame();
};
/**
* Returns the webpage dimensions
* @return {{top: *, bottom: *, left: *, right: *, viewport: *, document: {height: number, width: number}}}
*/
WebPage.prototype.dimensions = function () {
var scroll, viewport;
scroll = this.scrollPosition();
viewport = this.viewportSize();
return {
top: scroll.top,
bottom: scroll.top + viewport.height,
left: scroll.left,
right: scroll.left + viewport.width,
viewport: viewport,
document: this.documentSize()
};
};
/**
* Returns webpage dimensions that are valid
* @return {{top: *, bottom: *, left: *, right: *, viewport: *, document: {height: number, width: number}}}
*/
WebPage.prototype.validatedDimensions = function () {
var dimensions, documentDimensions;
dimensions = this.dimensions();
documentDimensions = dimensions.document;
if (dimensions.right > documentDimensions.width) {
dimensions.left = Math.max(0, dimensions.left - (dimensions.right - documentDimensions.width));
dimensions.right = documentDimensions.width;
}
if (dimensions.bottom > documentDimensions.height) {
dimensions.top = Math.max(0, dimensions.top - (dimensions.bottom - documentDimensions.height));
dimensions.bottom = documentDimensions.height;
}
this.setScrollPosition({
left: dimensions.left,
top: dimensions.top
});
return dimensions;
};
/**
* Returns a Poltergeist.Node given by an id
* @param id
* @return {Poltergeist.Node}
*/
WebPage.prototype.get = function (id) {
return new Poltergeist.Node(this, id);
};
/**
* Executes a phantomjs mouse event, for more info check: http://phantomjs.org/api/webpage/method/send-event.html
* @param name
* @param x
* @param y
* @param button
* @return {*}
*/
WebPage.prototype.mouseEvent = function (name, x, y, button) {
if (button == null) {
button = 'left';
}
this.sendEvent('mousemove', x, y);
return this.sendEvent(name, x, y, button);
};
/**
* Evaluates a javascript and returns the evaluation of such script
* @return {*}
*/
WebPage.prototype.evaluate = function () {
var args, fn;
fn = arguments[0];
args = [];
if (2 <= arguments.length) {
args = __slice.call(arguments, 1);
}
this.injectAgent();
return JSON.parse(this.sanitize(this["native"]().evaluate("function() { return PoltergeistAgent.stringify(" + (this.stringifyCall(fn, args)) + ") }")));
};
/**
* Does some string sanitation prior parsing
* @param potentialString
* @return {*}
*/
WebPage.prototype.sanitize = function (potentialString) {
if (typeof potentialString === "string") {
return potentialString.replace("\n", "\\n").replace("\r", "\\r");
}
return potentialString;
};
/**
* Executes a script into the current page scope
* @param script
* @return {*}
*/
WebPage.prototype.executeScript = function (script) {
return this["native"]().evaluateJavaScript(script);
};
/**
* Executes a script via phantomjs evaluation
* @return {*}
*/
WebPage.prototype.execute = function () {
var args, fn;
fn = arguments[0];
args = [];
if (2 <= arguments.length) {
args = __slice.call(arguments, 1);
}
return this["native"]().evaluate("function() { " + (this.stringifyCall(fn, args)) + " }");
};
/**
* Helper methods to do script evaluation and execution
* @param fn
* @param args
* @return {string}
*/
WebPage.prototype.stringifyCall = function (fn, args) {
if (args.length === 0) {
return "(" + (fn.toString()) + ")()";
}
return "(" + (fn.toString()) + ").apply(this, JSON.parse(" + (JSON.stringify(JSON.stringify(args))) + "))";
};
/**
* Binds callbacks to their respective Native implementations
* @param name
* @return {Function}
*/
WebPage.prototype.bindCallback = function (name) {
var self;
self = this;
return this["native"]()[name] = function () {
var result;
if (self[name + 'Native'] != null) {
result = self[name + 'Native'].apply(self, arguments);
}
if (result !== false && (self[name] != null)) {
return self[name].apply(self, arguments);
}
};
};
/**
* Runs a command delegating to the PoltergeistAgent
* @param name
* @param args
* @return {*}
*/
WebPage.prototype.runCommand = function (name, args) {
var method, result, selector;
result = this.evaluate(function (name, args) {
return window.__poltergeist.externalCall(name, args);
}, name, args);
if (result !== null) {
if (result.error != null) {
switch (result.error.message) {
case 'PoltergeistAgent.ObsoleteNode':
throw new Poltergeist.ObsoleteNode;
break;
case 'PoltergeistAgent.InvalidSelector':
method = args[0];
selector = args[1];
throw new Poltergeist.InvalidSelector(method, selector);
break;
default:
throw new Poltergeist.BrowserError(result.error.message, result.error.stack);
}
} else {
return result.value;
}
}
};
/**
* Tells if we can go back or not
* @return {boolean}
*/
WebPage.prototype.canGoBack = function () {
return this["native"]().canGoBack;
};
/**
* Tells if we can go forward or not in the browser history
* @return {boolean}
*/
WebPage.prototype.canGoForward = function () {
return this["native"]().canGoForward;
};
return WebPage;
}).call(this);

View file

@ -0,0 +1,79 @@
<?php
namespace Zumba\GastonJS;
/**
* Class Cookie
* @package Zumba\GastonJS
*/
class Cookie {
/** @var array */
protected $attributes;
/**
* @param $attributes
*/
public function __construct($attributes) {
$this->attributes = $attributes;
}
/**
* Returns the cookie name
* @return string
*/
public function getName() {
return $this->attributes['name'];
}
/**
* Returns the cookie value
* @return string
*/
public function getValue() {
return urldecode($this->attributes['value']);
}
/**
* Returns the cookie domain
* @return string
*/
public function getDomain() {
return $this->attributes['domain'];
}
/**
* Returns the path were the cookie is valid
* @return string
*/
public function getPath() {
return $this->attributes['path'];
}
/**
* Is a secure cookie?
* @return bool
*/
public function isSecure() {
return isset($this->attributes['secure']);
}
/**
* Is http only cookie?
* @return bool
*/
public function isHttpOnly() {
return isset($this->attributes['httponly']);
}
/**
* Returns cookie expiration time
* @return mixed
*/
public function getExpirationTime() {
//TODO: return a \DateTime object
if (isset($this->attributes['expiry'])) {
return $this->attributes['expiry'];
}
return null;
}
}

View file

@ -0,0 +1,44 @@
<?php
namespace Zumba\GastonJS\Exception;
/**
* Class BrowserError
* @package Zumba\GastonJS\Exception
*/
class BrowserError extends ClientError {
/**
* @param array $response
*/
public function __construct($response) {
parent::__construct($response);
$this->message = $this->message();
}
/**
* Gets the name of the browser error
* @return string
*/
public function getName() {
return $this->response["error"]["name"];
}
/**
* @return JSErrorItem
*/
public function javascriptError() {
//TODO: this need to be check, i don't know yet what comes in response
return new JSErrorItem($this->response["error"]["args"][0], $this->response["error"]["args"][1]);
}
/**
* Returns error message
* TODO: check how to proper implement if we have exceptions
* @return string
*/
public function message() {
return "There was an error inside the PhantomJS portion of GastonJS.\nThis is probably a bug, so please report it:\n" . $this->javascriptError();
}
}

View file

@ -0,0 +1,36 @@
<?php
namespace Zumba\GastonJS\Exception;
/**
* Class ClientError
* @package Zumba\GastonJS\Exception
*/
class ClientError extends \Exception {
/** @var mixed */
protected $response;
/**
* @param mixed $response
*/
public function __construct($response) {
$this->response = $response;
}
/**
* @return mixed
*/
public function getResponse() {
return $this->response;
}
/**
* @param mixed $response
*/
public function setResponse($response) {
$this->response = $response;
}
}

View file

@ -0,0 +1,21 @@
<?php
namespace Zumba\GastonJS\Exception;
/**
* Class DeadClient
* @package Zumba\GastonJS\Exception
*/
class DeadClient extends \Exception {
/**
* @param string $message
* @param int $code
* @param \Exception $previous
*/
public function __construct($message = "", $code = 0, \Exception $previous = null) {
$errorMsg = $message."\nPhantomjs browser server is not taking connections, most probably it has crashed\n";
parent::__construct($errorMsg, $code, $previous);
}
}

View file

@ -0,0 +1,26 @@
<?php
namespace Zumba\GastonJS\Exception;
/**
* Class FrameNotFound
* @package Zumba\GastonJS\Exception
*/
class FrameNotFound extends ClientError {
/**
* @return string
*/
public function getName() {
//TODO: check stuff here
return current(reset($this->response["args"]));
}
/**
* @return string
*/
public function message() {
//TODO: check the exception message stuff
return "The frame " . $this->getName() . " was not not found";
}
}

View file

@ -0,0 +1,32 @@
<?php
namespace Zumba\GastonJS\Exception;
/**
* Class InvalidSelector
* @package Zumba\GastonJS\Exception
*/
class InvalidSelector extends ClientError {
/**
* Gets the method of selection
* @return string
*/
public function getMethod() {
return $this->response["error"]["args"][0];
}
/**
* Gets the selector related to the method
* @return string
*/
public function getSelector() {
return $this->response["error"]["args"][1];
}
/**
* @return string
*/
public function message() {
return "The browser raised a syntax error while trying to evaluate" . $this->getMethod() . " selector " . $this->getSelector();
}
}

View file

@ -0,0 +1,31 @@
<?php
namespace Zumba\GastonJS\Exception;
/**
* Class JSErrorItem
* @package Zumba\GastonJS\Exception
*/
class JSErrorItem {
/** @var mixed */
protected $message;
/** @var mixed */
protected $stack;
/**
* @param $message
* @param $stack
*/
public function __construct($message, $stack) {
$this->message = $message;
$this->stack = $stack;
}
/**
* String representation of the class
* @return string
*/
public function __toString() {
return sprintf("%s\n%s", $this->message, $this->stack);
}
}

View file

@ -0,0 +1,48 @@
<?php
namespace Zumba\GastonJS\Exception;
/**
* Class JavascriptError
* @package Zumba\GastonJS\Exception
*/
class JavascriptError extends ClientError {
/**
* @param array $response
*/
public function __construct($response) {
parent::__construct($response);
$this->message = $this->message();
}
/**
* Get the javascript errors found during the use of the phantomjs
* @return array
*/
public function javascriptErrors() {
$jsErrors = array();
$errors = $this->response["error"]["args"][0];
foreach ($errors as $error) {
$jsErrors[] = new JSErrorItem($error["message"], $error["stack"]);
}
return $jsErrors;
}
/**
* Returns the javascript errors found
* @return string
*/
public function message() {
$error = "One or more errors were raised in the Javascript code on the page.
If you don't care about these errors, you can ignore them by
setting js_errors: false in your Poltergeist configuration (see documentation for details).";
//TODO: add javascript errors
$jsErrors = $this->javascriptErrors();
foreach($jsErrors as $jsError){
$error = "$error\n$jsError";
}
return $error;
}
}

View file

@ -0,0 +1,49 @@
<?php
namespace Zumba\GastonJS\Exception;
/**
* Class MouseEventFailed
* @package Zumba\GastonJS\Exception
*/
class MouseEventFailed extends NodeError {
/**
* Gets the name of the event
* @return string
*/
public function getName() {
return $this->response["args"][0];
}
/**
* Selector of the element to act with the mouse
* @return string
*/
public function getSelector() {
return $this->response["args"][1];
}
/**
* Returns the position where the click was done
* @return array
*/
public function getPosition() {
$position = array();
$position[0] = $this->response["args"][1]['x'];
$position[1] = $this->response["args"][2]['y'];
return $position;
}
/**
* @return string
*/
public function message() {
$name = $this->getName();
$position = implode(",", $this->getPosition());
return "Firing a $name at co-ordinates [$position] failed. Poltergeist detected
another element with CSS selector '#{selector}' at this position.
It may be overlapping the element you are trying to interact with.
If you don't care about overlapping elements, try using node.trigger('$name').";
}
}

View file

@ -0,0 +1,10 @@
<?php
namespace Zumba\GastonJS\Exception;
/**
* Class NoSuchWindowError
* @package Zumba\GastonJS\Exception
*/
class NoSuchWindowError extends ClientError {
}

View file

@ -0,0 +1,20 @@
<?php
namespace Zumba\GastonJS\Exception;
/**
* Class NodeError
* @package Zumba\GastonJS\Exception
*/
class NodeError extends ClientError {
protected $node;
/**
* @param mixed $node
* @param mixed $response
*/
public function __construct($node, $response) {
$this->node = $node;
parent::__construct($response);
}
}

View file

@ -0,0 +1,29 @@
<?php
namespace Zumba\GastonJS\Exception;
/**
* Class ObsoleteNode
* @package Zumba\GastonJS\Exception
*/
class ObsoleteNode extends ClientError {
/**
* @param array $response
*/
public function __construct($response) {
parent::__construct($response);
$this->message = $this->message();
}
/**
* @return string
*/
public function message() {
return "The element you are trying to interact with is either not part of the DOM, or is
not currently visible on the page (perhaps display: none is set).
It's possible the element has been replaced by another element and you meant to interact with
the new element. If so you need to do a new 'find' in order to get a reference to the
new element.";
}
}

View file

@ -0,0 +1,17 @@
<?php
namespace Zumba\GastonJS\Exception;
/**
* Class StatusFailError
* @package Zumba\GastonJS\Exception
*/
class StatusFailError extends ClientError {
/**
* @return string
*/
public function message() {
return "Request failed to reach server, check DNS and/or server status";
}
}

View file

@ -0,0 +1,23 @@
<?php
namespace Zumba\GastonJS\Exception;
/**
* Class TimeoutError
* @package Zumba\GastonJS\Exception
*/
class TimeoutError extends \Exception {
/**
* @param string $message
*/
public function __construct($message) {
$errorMessage = "Timed out waiting for response to {$message}. It's possible that this happened
because something took a very long time(for example a page load was slow).
If so, setting the Poltergeist :timeout option to a higher value will help
(see the docs for details). If increasing the timeout does not help, this is
probably a bug in Poltergeist - please report it to the issue tracker.";
parent::__construct($errorMessage);
}
}

View file

@ -0,0 +1,95 @@
<?php
namespace Zumba\GastonJS\NetworkTraffic;
/**
* Class Request
* @package Zumba\GastonJS\NetworkTraffic
*/
class Request {
/** @var array */
protected $data;
/** @var array */
protected $responseParts;
/**
* @param array $data
* @param array $responseParts
*/
public function __construct($data, $responseParts = null) {
$this->data = $data;
$this->responseParts = $this->createResponseParts($responseParts);
}
/**
* Creates an array of Response objects from a given response array
* @param $responseParts
* @return array
*/
protected function createResponseParts($responseParts) {
if ($responseParts === null) {
return array();
}
$responses = array();
foreach ($responseParts as $responsePart) {
$responses[] = new Response($responsePart);
}
return $responses;
}
/**
* @return array
*/
public function getResponseParts() {
return $this->responseParts;
}
/**
* @param array $responseParts
*/
public function setResponseParts($responseParts) {
$this->responseParts = $responseParts;
}
/**
* Returns the url where the request is going to be made
* @return string
*/
public function getUrl() {
//TODO: add isset maybe?
return $this->data['url'];
}
/**
* Returns the request method
* @return string
*/
public function getMethod() {
return $this->data['method'];
}
/**
* Gets the request headers
* @return array
*/
public function getHeaders() {
//TODO: Check if the data is actually an array, else make it array and see implications
return $this->data['headers'];
}
/**
* Returns if exists the request time
* @return \DateTime
*/
public function getTime() {
if (isset($this->data['time'])) {
$requestTime = new \DateTime();
//TODO: fix the microseconds to miliseconds
$requestTime->createFromFormat("Y-m-dTH:i:s.uZ", $this->data["time"]);
return $requestTime;
}
return null;
}
}

View file

@ -0,0 +1,97 @@
<?php
namespace Zumba\GastonJS\NetworkTraffic;
/**
* Class Response
* @package Zumba\GastonJS\NetworkTraffic
*/
class Response {
/** @var array */
protected $data;
/**
* @param $data
*/
public function __construct($data) {
$this->data = $data;
}
/**
* Gets Response url
* @return string
*/
public function getUrl() {
return $this->data['url'];
}
/**
* Gets the response status code
* @return int
*/
public function getStatus() {
return intval($this->data['status']);
}
/**
* Gets the status text of the response
* @return string
*/
public function getStatusText() {
return $this->data['statusText'];
}
/**
* Gets the response headers
* @return array
*/
public function getHeaders() {
return $this->data['headers'];
}
/**
* Get redirect url if response is a redirect
* @return string
*/
public function getRedirectUrl() {
if (isset($this->data['redirectUrl']) && !empty($this->data['redirectUrl'])) {
return $this->data['redirectUrl'];
}
return null;
}
/**
* Returns the size of the response body
* @return int
*/
public function getBodySize() {
if (isset($this->data['bodySize'])) {
return intval($this->data['bodySize']);
}
return 0;
}
/**
* Returns the content type of the response
* @return string
*/
public function getContentType() {
if (isset($this->data['contentType'])) {
return $this->data['contentType'];
}
return null;
}
/**
* Returns if exists the response time
* @return \DateTime
*/
public function getTime() {
if (isset($this->data['time'])) {
$requestTime = new \DateTime();
//TODO: fix the microseconds to miliseconds
$requestTime->createFromFormat("Y-m-dTH:i:s.uZ", $this->data["time"]);
return $requestTime;
}
return null;
}
}