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

@ -6,6 +6,8 @@ $vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(
'Zumba\\Mink\\Driver\\' => array($vendorDir . '/jcalderonzumba/mink-phantomjs-driver/src'),
'Zumba\\GastonJS\\' => array($vendorDir . '/jcalderonzumba/gastonjs/src'),
'Zend\\Stdlib\\' => array($vendorDir . '/zendframework/zend-stdlib/src'),
'Zend\\Hydrator\\' => array($vendorDir . '/zendframework/zend-hydrator/src'),
'Zend\\Feed\\' => array($vendorDir . '/zendframework/zend-feed/src'),

View file

@ -107,7 +107,7 @@
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/wikimedia/composer-merge-plugin/zipball/47bb3388cfeae41a38087ac8465a7d08fa92ea2e",
"url": "https://api.github.com/repos/wikimedia/composer-merge-plugin/zipball/6196fdb001faf681f92db2ae10abafb5815affde",
"reference": "47bb3388cfeae41a38087ac8465a7d08fa92ea2e",
"shasum": ""
},
@ -3757,5 +3757,127 @@
],
"description": "Symfony EventDispatcher Component",
"homepage": "https://symfony.com"
},
{
"name": "jcalderonzumba/gastonjs",
"version": "dev-master",
"version_normalized": "9999999-dev",
"source": {
"type": "git",
"url": "https://github.com/jcalderonzumba/gastonjs.git",
"reference": "5e231b4df98275c404e1371fc5fadd34f6a121ad"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/jcalderonzumba/gastonjs/zipball/5e231b4df98275c404e1371fc5fadd34f6a121ad",
"reference": "5e231b4df98275c404e1371fc5fadd34f6a121ad",
"shasum": ""
},
"require": {
"guzzlehttp/guzzle": "~5.0|~6.0",
"php": ">=5.4"
},
"require-dev": {
"phpunit/phpunit": "~4.6",
"silex/silex": "~1.2",
"symfony/phpunit-bridge": "~2.7",
"symfony/process": "~2.1"
},
"time": "2015-10-07 11:40:41",
"type": "phantomjs-api",
"extra": {
"branch-alias": {
"dev-master": "1.1.x-dev"
}
},
"installation-source": "source",
"autoload": {
"psr-4": {
"Zumba\\GastonJS\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Juan Francisco Calderón Zumba",
"email": "juanfcz@gmail.com",
"homepage": "http://github.com/jcalderonzumba"
}
],
"description": "PhantomJS API based server for webpage automation",
"homepage": "https://github.com/jcalderonzumba/gastonjs",
"keywords": [
"api",
"automation",
"browser",
"headless",
"phantomjs"
]
},
{
"name": "jcalderonzumba/mink-phantomjs-driver",
"version": "dev-master",
"version_normalized": "9999999-dev",
"source": {
"type": "git",
"url": "https://github.com/jcalderonzumba/MinkPhantomJSDriver.git",
"reference": "10d7c48c9a4129463052321b52450d98983c4332"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/jcalderonzumba/MinkPhantomJSDriver/zipball/10d7c48c9a4129463052321b52450d98983c4332",
"reference": "10d7c48c9a4129463052321b52450d98983c4332",
"shasum": ""
},
"require": {
"behat/mink": "~1.6",
"jcalderonzumba/gastonjs": "~1.0",
"php": ">=5.4",
"twig/twig": "~1.8"
},
"require-dev": {
"phpunit/phpunit": "~4.6",
"silex/silex": "~1.2",
"symfony/css-selector": "~2.1",
"symfony/phpunit-bridge": "~2.7",
"symfony/process": "~2.3"
},
"time": "2015-10-05 18:24:44",
"type": "mink-driver",
"extra": {
"branch-alias": {
"dev-master": "0.4.x-dev"
}
},
"installation-source": "source",
"autoload": {
"psr-4": {
"Zumba\\Mink\\Driver\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Juan Francisco Calderón Zumba",
"email": "juanfcz@gmail.com",
"homepage": "http://github.com/jcalderonzumba"
}
],
"description": "PhantomJS driver for Mink framework",
"homepage": "http://mink.behat.org/",
"keywords": [
"ajax",
"browser",
"headless",
"javascript",
"phantomjs",
"testing"
]
}
]

View file

@ -0,0 +1,37 @@
language: php
php:
- 5.4
- 5.5
- 5.6
- 7.0
- hhvm
matrix:
fast_finish: true
include:
- php: 5.4
env: COMPOSER_FLAGS='--prefer-lowest --prefer-stable' SYMFONY_DEPRECATIONS_HELPER=weak
- php: 5.6
env: DEPENDENCIES=dev
allow_failures:
- php: 7.0
- php: hhvm
cache:
directories:
- $HOME/.composer/cache/files
before_install:
- composer self-update
- if [ "$DEPENDENCIES" = "dev" ]; then perl -pi -e 's/^}$/,"minimum-stability":"dev"}/' composer.json; fi;
install:
- composer update $COMPOSER_FLAGS
script:
- bin/run-tests.sh
after_script:
- ps axo pid,command | grep phantomjs | grep -v grep | awk '{print $1}' | xargs -I {} kill {}
- ps axo pid,command | grep php | grep -v grep | awk '{print $1}' | xargs -I {} kill {}

21
vendor/jcalderonzumba/gastonjs/LICENSE vendored Normal file
View file

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2015 Juan Francisco Calderón Zumba
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,8 @@
GastonJS for Webpage automation
================================
[![Build Status](https://travis-ci.org/jcalderonzumba/gastonjs.svg?branch=travis_ci)](https://travis-ci.org/jcalderonzumba/gastonjs)
[![Latest Stable Version](https://poser.pugx.org/jcalderonzumba/gastonjs/v/stable)](https://packagist.org/packages/jcalderonzumba/gastonjs)
[![Total Downloads](https://poser.pugx.org/jcalderonzumba/gastonjs/downloads)](https://packagist.org/packages/jcalderonzumba/gastonjs)
For full documentation go to [GastonJS doc](http://gastonjs.readthedocs.org/en/latest/)

View file

@ -0,0 +1,26 @@
#!/bin/sh
set -e
start_browser_api(){
CURRENT_DIR=$(pwd)
LOCAL_PHANTOMJS="${CURRENT_DIR}/bin/phantomjs"
if [ -f ${LOCAL_PHANTOMJS} ]; then
${LOCAL_PHANTOMJS} --ssl-protocol=any --ignore-ssl-errors=true src/Client/main.js 8510 1024 768 2>&1 &
else
phantomjs --ssl-protocol=any --ignore-ssl-errors=true src/Client/main.js 8510 1024 768 2>&1 >> /dev/null &
fi
sleep 2
}
stop_services(){
ps axo pid,command | grep phantomjs | grep -v grep | awk '{print $1}' | xargs -I {} kill {}
ps axo pid,command | grep php | grep -v grep | grep -v phpstorm | awk '{print $1}' | xargs -I {} kill {}
sleep 2
}
mkdir -p /tmp/jcalderonzumba/phantomjs
stop_services
start_browser_api
CURRENT_DIR=$(pwd)
${CURRENT_DIR}/bin/phpunit --configuration unit_tests.xml

View file

@ -0,0 +1,49 @@
{
"name": "jcalderonzumba/gastonjs",
"description": "PhantomJS API based server for webpage automation",
"keywords": [
"phantomjs",
"headless",
"api",
"automation",
"browser"
],
"homepage": "https://github.com/jcalderonzumba/gastonjs",
"type": "phantomjs-api",
"license": "MIT",
"authors": [
{
"name": "Juan Francisco Calderón Zumba",
"email": "juanfcz@gmail.com",
"homepage": "http://github.com/jcalderonzumba"
}
],
"require": {
"php": ">=5.4",
"guzzlehttp/guzzle": "~5.0|~6.0"
},
"require-dev": {
"symfony/process": "~2.1",
"symfony/phpunit-bridge": "~2.7",
"phpunit/phpunit": "~4.6",
"silex/silex": "~1.2"
},
"config": {
"bin-dir": "bin"
},
"autoload": {
"psr-4": {
"Zumba\\GastonJS\\": "src"
}
},
"autoload-dev": {
"psr-4": {
"Zumba\\GastonJS\\Tests\\": "tests/unit"
}
},
"extra": {
"branch-alias": {
"dev-master": "1.1.x-dev"
}
}
}

View file

@ -0,0 +1,40 @@
site_name: GastonJS Documentation
pages:
- GastonJS introduction: index.md
- GastonJS API :
- 'Introduction': api/index.md
- 'Available commands': api/command-list.md
- 'Navigation commands':
- 'visit': api/commands/navigation/visit.md
- 'current_url': api/commands/navigation/current_url.md
- 'reload': api/commands/navigation/reload.md
- 'go_back': api/commands/navigation/go_back.md
- 'go_forward': api/commands/navigation/go_forward.md
- 'Header commands' :
- 'get_headers': api/commands/headers/get_headers.md
- 'response_headers': api/commands/headers/response_headers.md
- 'set_headers': api/commands/headers/set_headers.md
- 'add_headers': api/commands/headers/add_headers.md
- 'add_header': api/commands/headers/add_header.md
- 'Javascript commands' :
- 'add_extension': api/commands/javascript/add_extension.md
- 'execute': api/commands/javascript/execute.md
- 'evaluate': api/commands/javascript/evaluate.md
- 'set_js_errors': api/commands/javascript/set_js_errors.md
- 'Cookies commands' :
- 'cookies': api/commands/cookies/cookies.md
- 'clear_cookies': api/commands/cookies/clear_cookies.md
- 'cookies_enabled': api/commands/cookies/cookies_enabled.md
- 'remove_cookie': api/commands/cookies/remove_cookie.md
- 'set_cookie': api/commands/cookies/set_cookie.md
- 'Mouse commands':
- 'click': api/commands/mouse/click.md
- 'right_click': api/commands/mouse/right_click.md
- 'hover': api/commands/mouse/hover.md
- 'double_click': api/commands/mouse/double_click.md
- 'click_coordinates': api/commands/mouse/click_coordinates.md
- 'mouse_event': api/commands/mouse/mouse_event.md
- 'Render commands':
- 'render': api/commands/render/render.md
- 'render_base64': api/commands/render/render_base64.md
- GastonJS PHP client: clients/php/index.md

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;
}
}

View file

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="./tests/unit/bootstrap.php"
backupGlobals="false"
backupStaticAttributes="false"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="true"
syntaxCheck="false">
<testsuites>
<testsuite>
<directory>tests/unit</directory>
</testsuite>
</testsuites>
<filter>
<whitelist>
<directory suffix=".php">src</directory>
<exclude>
<directory suffix="Trait.php">src/</directory>
</exclude>
</whitelist>
</filter>
</phpunit>

View file

@ -0,0 +1,40 @@
language: php
php:
- 5.4
- 5.5
- 5.6
- 7.0
- hhvm
matrix:
fast_finish: true
include:
- php: 5.4
env: COMPOSER_FLAGS='--prefer-lowest --prefer-stable' SYMFONY_DEPRECATIONS_HELPER=weak
- php: 5.6
env: DEPENDENCIES=dev
allow_failures:
- php: 7.0
- php: hhvm
cache:
directories:
- $HOME/.composer/cache/files
before_install:
- composer self-update
- if [ "$DEPENDENCIES" = "dev" ]; then perl -pi -e 's/^}$/,"minimum-stability":"dev"}/' composer.json; fi;
install:
- composer update $COMPOSER_FLAGS
before_script:
- mkdir -p /tmp/jcalderonzumba/phantomjs
script:
- bin/run-tests.sh
after_script:
- ps axo pid,command | grep phantomjs | grep -v grep | awk '{print $1}' | xargs -I {} kill {}
- ps axo pid,command | grep php | grep -v grep | awk '{print $1}' | xargs -I {} kill {}

View file

@ -0,0 +1,7 @@
CHANGELOG for 0.2.x
===================
This changelog references the relevant changes (bug and security fixes) done in 0.2 minor versions.
* 0.2.3
* bug #1 set_url_blacklist was not working properly (thanks to reporter)

View file

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2015 Juan Francisco Calderón Zumba
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,61 @@
Mink PhantomJS Driver
===========================
[![Build Status](https://travis-ci.org/jcalderonzumba/MinkPhantomJSDriver.svg?branch=master)](https://travis-ci.org/jcalderonzumba/MinkPhantomJSDriver)
[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/jcalderonzumba/MinkPhantomJSDriver/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/jcalderonzumba/MinkPhantomJSDriver/?branch=master)
[![Latest Stable Version](https://poser.pugx.org/jcalderonzumba/mink-phantomjs-driver/v/stable)](https://packagist.org/packages/jcalderonzumba/mink-phantomjs-driver)
[![Total Downloads](https://poser.pugx.org/jcalderonzumba/mink-phantomjs-driver/downloads)](https://packagist.org/packages/jcalderonzumba/mink-phantomjs-driver)
Installation & Compatibility
----------------------------
You need a working installation of [PhantomJS](http://phantomjs.org/download.html)
This driver is tested using PhantomJS 1.9.8 but it should work with 1.9.X or latest 2.0.X versions
This driver supports **PHP 5.4 or greater**, there is NO support for PHP 5.3
Use [Composer](https://getcomposer.org/) to install all required PHP dependencies:
```bash
$ composer require --dev behat/mink jcalderonzumba/mink-phantomjs-driver
```
How to use
-------------
Extension configuration (for the moment NONE).
```yml
default:
extensions:
Zumba\PhantomJSExtension:
```
Driver specific configuration:
```yml
Behat\MinkExtension:
phantomjs:
phantom_server: "http://localhost:8510/api"
template_cache: "/tmp/pjsdrivercache/phantomjs"
```
PhantomJS browser start:
```bash
phantomjs --ssl-protocol=any --ignore-ssl-errors=true vendor/jcalderonzumba/gastonjs/src/Client/main.js 8510 1024 768 2>&1 >> /tmp/gastonjs.log &
```
FAQ
---------
1. Is this a selenium based driver?:
**NO**, it has nothing to do with Selenium it's inspired on [Poltergeist](https://github.com/teampoltergeist/poltergeist)
2. What features does this driver implements?
**ALL** of the features defined in Mink DriverInterface. maximizeWindow is the only one not implemented since is a headless browser it does not make sense to implement it.
3. Do i need to modify my selenium based tests?
If you only use the standard behat driver defined methods then NO, you just have to change your default javascript driver.
Copyright
---------
Copyright (c) 2015 Juan Francisco Calderon Zumba <juanfcz@gmail.com>

View file

@ -0,0 +1,40 @@
#!/bin/sh
set -e
start_browser_api(){
CURRENT_DIR=$(pwd)
LOCAL_PHANTOMJS="${CURRENT_DIR}/bin/phantomjs"
if [ -f ${LOCAL_PHANTOMJS} ]; then
${LOCAL_PHANTOMJS} --ssl-protocol=any --ignore-ssl-errors=true vendor/jcalderonzumba/gastonjs/src/Client/main.js 8510 1024 768 2>&1 &
else
phantomjs --ssl-protocol=any --ignore-ssl-errors=true vendor/jcalderonzumba/gastonjs/src/Client/main.js 8510 1024 768 2>&1 >> /dev/null &
fi
sleep 2
}
stop_services(){
ps axo pid,command | grep phantomjs | grep -v grep | awk '{print $1}' | xargs -I {} kill {}
ps axo pid,command | grep php | grep -v grep | grep -v phpstorm | awk '{print $1}' | xargs -I {} kill {}
sleep 2
}
star_local_browser(){
CURRENT_DIR=$(pwd)
cd ${CURRENT_DIR}/vendor/behat/mink/driver-testsuite/web-fixtures
if [ "$TRAVIS" = true ]; then
echo "Starting webserver fox fixtures...."
~/.phpenv/versions/5.6/bin/php -S 127.0.0.1:6789 > /dev/null 2>&1 &
else
php -S 127.0.0.1:6789 2>&1 >> /dev/null &
fi
sleep 2
}
mkdir -p /tmp/jcalderonzumba/phantomjs
stop_services
start_browser_api
star_local_browser
cd ${CURRENT_DIR}
${CURRENT_DIR}/bin/phpunit --configuration integration_tests.xml
stop_services
start_browser_api

View file

@ -0,0 +1,53 @@
{
"name": "jcalderonzumba/mink-phantomjs-driver",
"description": "PhantomJS driver for Mink framework",
"keywords": [
"phantomjs",
"headless",
"javascript",
"ajax",
"testing",
"browser"
],
"homepage": "http://mink.behat.org/",
"type": "mink-driver",
"license": "MIT",
"authors": [
{
"name": "Juan Francisco Calderón Zumba",
"email": "juanfcz@gmail.com",
"homepage": "http://github.com/jcalderonzumba"
}
],
"require": {
"php": ">=5.4",
"behat/mink": "~1.6",
"twig/twig": "~1.8",
"jcalderonzumba/gastonjs": "~1.0"
},
"require-dev": {
"symfony/process": "~2.3",
"symfony/phpunit-bridge": "~2.7",
"symfony/css-selector": "~2.1",
"phpunit/phpunit": "~4.6",
"silex/silex": "~1.2"
},
"config": {
"bin-dir": "bin"
},
"autoload": {
"psr-4": {
"Zumba\\Mink\\Driver\\": "src"
}
},
"autoload-dev": {
"psr-4": {
"Behat\\Mink\\Tests\\Driver\\": "tests/integration"
}
},
"extra": {
"branch-alias": {
"dev-master": "0.4.x-dev"
}
}
}

View file

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit colors="true" bootstrap="./tests/integration/bootstrap.php" stopOnFailure="true">
<testsuites>
<testsuite name="PhantomJS Driver test suite">
<directory>tests/integration</directory>
<file>vendor/behat/mink/driver-testsuite/tests/Basic/BasicAuthTest.php</file>
<file>vendor/behat/mink/driver-testsuite/tests/Basic/ContentTest.php</file>
<file>vendor/behat/mink/driver-testsuite/tests/Basic/CookieTest.php</file>
<file>vendor/behat/mink/driver-testsuite/tests/Basic/ErrorHandlingTest.php</file>
<file>vendor/behat/mink/driver-testsuite/tests/Basic/IFrameTest.php</file>
<file>vendor/behat/mink/driver-testsuite/tests/Basic/ScreenshotTest.php</file>
<file>vendor/behat/mink/driver-testsuite/tests/Basic/TraversingTest.php</file>
<file>vendor/behat/mink/driver-testsuite/tests/Basic/VisibilityTest.php</file>
<directory>vendor/behat/mink/driver-testsuite/tests/Form</directory>
<directory>vendor/behat/mink/driver-testsuite/tests/Js</directory>
<!-- The following have been disabled and their respective equals added to Custom driver tests -->
<!--<directory>vendor/behat/mink/driver-testsuite/tests/Css</directory>-->
<!--<file>vendor/behat/mink/driver-testsuite/tests/Basic/StatusCodeTest.php</file>-->
<!--<file>vendor/behat/mink/driver-testsuite/tests/Basic/HeaderTest.php</file>-->
<!--<file>vendor/behat/mink/driver-testsuite/tests/Basic/NavigationTest.php</file>-->
</testsuite>
</testsuites>
<php>
<var name="driver_config_factory" value="Behat\Mink\Tests\Driver\PhantomJSConfig::getInstance"/>
<server name="WEB_FIXTURES_HOST" value="http://127.0.0.1:6789"/>
<!-- where driver will connect to -->
<server name="DRIVER_URL" value="http://127.0.0.1:8510/"/>
<server name="TEMPLATE_CACHE_DIR" value="/tmp/jcalderonzumba/phantomjs"/>
</php>
<filter>
<whitelist>
<directory>./src/Behat/Mink/Driver</directory>
</whitelist>
</filter>
</phpunit>

View file

@ -0,0 +1,109 @@
<?php
namespace Zumba\Mink\Driver;
use Behat\Mink\Driver\CoreDriver;
use Behat\Mink\Exception\DriverException;
use Behat\Mink\Session;
use Zumba\GastonJS\Browser\Browser;
/**
* Class BasePhantomJSDriver
* @package Zumba\Mink\Driver
*/
class BasePhantomJSDriver extends CoreDriver {
/** @var Session */
protected $session;
/** @var Browser */
protected $browser;
/** @var string */
protected $phantomHost;
/** @var \Twig_Loader_Filesystem */
protected $templateLoader;
/** @var \Twig_Environment */
protected $templateEnv;
/**
* Instantiates the driver
* @param string $phantomHost browser "api" oriented host
* @param string $templateCache where we are going to store the templates cache
*/
public function __construct($phantomHost, $templateCache = null) {
$this->phantomHost = $phantomHost;
$this->browser = new Browser($phantomHost);
$this->templateLoader = new \Twig_Loader_Filesystem(realpath(__DIR__ . '/Resources/Script'));
$this->templateEnv = new \Twig_Environment($this->templateLoader, array('cache' => $this->templateCacheSetup($templateCache), 'strict_variables' => true));
}
/**
* Sets up the cache template location for the scripts we are going to create with the driver
* @param $templateCache
* @return string
* @throws DriverException
*/
protected function templateCacheSetup($templateCache) {
$cacheDir = $templateCache;
if ($templateCache === null) {
$cacheDir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . "jcalderonzumba" . DIRECTORY_SEPARATOR . "phantomjs";
if (!file_exists($cacheDir)) {
mkdir($cacheDir, 0777, true);
}
}
if (!file_exists($cacheDir)) {
throw new DriverException("Template cache $cacheDir directory does not exist");
}
return $cacheDir;
}
/**
* Helper to find a node element given an xpath
* @param string $xpath
* @param int $max
* @returns int
* @throws DriverException
*/
protected function findElement($xpath, $max = 1) {
$elements = $this->browser->find("xpath", $xpath);
if (!isset($elements["page_id"]) || !isset($elements["ids"]) || count($elements["ids"]) !== $max) {
throw new DriverException("Failed to get elements with given $xpath");
}
return $elements;
}
/**
* {@inheritdoc}
* @param Session $session
*/
public function setSession(Session $session) {
$this->session = $session;
}
/**
* @return Browser
*/
public function getBrowser() {
return $this->browser;
}
/**
* @return \Twig_Environment
*/
public function getTemplateEnv() {
return $this->templateEnv;
}
/**
* Returns a javascript script via twig template engine
* @param $templateName
* @param $viewData
* @return string
*/
public function javascriptTemplateRender($templateName, $viewData) {
/** @var $templateEngine \Twig_Environment */
$templateEngine = $this->getTemplateEnv();
return $templateEngine->render($templateName, $viewData);
}
}

View file

@ -0,0 +1,45 @@
<?php
namespace Zumba\Mink\Driver;
use Zumba\GastonJS\Cookie;
/**
* Trait CookieTrait
* @package Zumba\Mink\Driver
*/
trait CookieTrait {
/**
* Sets a cookie on the browser, if null value then delete it
* @param string $name
* @param string $value
*/
public function setCookie($name, $value = null) {
if ($value === null) {
$this->browser->removeCookie($name);
}
//TODO: set the cookie with domain, not with url, meaning www.aaa.com or .aaa.com
if ($value !== null) {
$urlData = parse_url($this->getCurrentUrl());
$cookie = array("name" => $name, "value" => $value, "domain" => $urlData["host"]);
$this->browser->setCookie($cookie);
}
}
/**
* Gets a cookie by its name if exists, else it will return null
* @param string $name
* @return string
*/
public function getCookie($name) {
$cookies = $this->browser->cookies();
foreach ($cookies as $cookie) {
if ($cookie instanceof Cookie && strcmp($cookie->getName(), $name) === 0) {
return $cookie->getValue();
}
}
return null;
}
}

View file

@ -0,0 +1,168 @@
<?php
namespace Zumba\Mink\Driver;
use Behat\Mink\Exception\DriverException;
/**
* Trait FormManipulationTrait
* @package Zumba\Mink\Driver
*/
trait FormManipulationTrait {
/**
* Returns the value of a given xpath element
* @param string $xpath
* @return string
* @throws DriverException
*/
public function getValue($xpath) {
$this->findElement($xpath, 1);
$javascript = $this->javascriptTemplateRender("get_value.js.twig", array("xpath" => $xpath));
return $this->browser->evaluate($javascript);
}
/**
* @param string $xpath
* @param string $value
* @throws DriverException
*/
public function setValue($xpath, $value) {
$this->findElement($xpath, 1);
//This stuff is BECAUSE the way the driver works for setting values when being checkboxes, radios, etc.
if (is_bool($value)) {
$value = $this->boolToString($value);
}
$javascript = $this->javascriptTemplateRender("set_value.js.twig", array("xpath" => $xpath, "value" => json_encode($value)));
$this->browser->evaluate($javascript);
}
/**
* Submits a form given an xpath selector
* @param string $xpath
* @throws DriverException
*/
public function submitForm($xpath) {
$element = $this->findElement($xpath, 1);
$tagName = $this->browser->tagName($element["page_id"], $element["ids"][0]);
if (strcmp(strtolower($tagName), "form") !== 0) {
throw new DriverException("Can not submit something that is not a form");
}
$this->browser->trigger($element["page_id"], $element["ids"][0], "submit");
}
/**
* Helper method needed for twig and javascript stuff
* @param $boolValue
* @return string
*/
protected function boolToString($boolValue) {
if ($boolValue === true) {
return "1";
}
return "0";
}
/**
* Selects an option
* @param string $xpath
* @param string $value
* @param bool $multiple
* @return bool
* @throws DriverException
*/
public function selectOption($xpath, $value, $multiple = false) {
$element = $this->findElement($xpath, 1);
$tagName = strtolower($this->browser->tagName($element["page_id"], $element["ids"][0]));
$attributes = $this->browser->attributes($element["page_id"], $element["ids"][0]);
if (!in_array($tagName, array("input", "select"))) {
throw new DriverException(sprintf('Impossible to select an option on the element with XPath "%s" as it is not a select or radio input', $xpath));
}
if ($tagName === "input" && $attributes["type"] != "radio") {
throw new DriverException(sprintf('Impossible to select an option on the element with XPath "%s" as it is not a select or radio input', $xpath));
}
return $this->browser->selectOption($element["page_id"], $element["ids"][0], $value, $multiple);
}
/**
* Check control over an input element of radio or checkbox type
* @param $xpath
* @return bool
* @throws DriverException
*/
protected function inputCheckableControl($xpath) {
$element = $this->findElement($xpath, 1);
$tagName = strtolower($this->browser->tagName($element["page_id"], $element["ids"][0]));
$attributes = $this->browser->attributes($element["page_id"], $element["ids"][0]);
if ($tagName != "input") {
throw new DriverException("Can not check when the element is not of the input type");
}
if (!in_array($attributes["type"], array("checkbox", "radio"))) {
throw new DriverException("Can not check when the element is not checkbox or radio");
}
return true;
}
/**
* We click on the checkbox or radio when possible and needed
* @param string $xpath
* @throws DriverException
*/
public function check($xpath) {
$this->inputCheckableControl($xpath);
$javascript = $this->javascriptTemplateRender("check_element.js.twig", array("xpath" => $xpath, "check" => "true"));
$this->browser->evaluate($javascript);
}
/**
* We click on the checkbox or radio when possible and needed
* @param string $xpath
* @throws DriverException
*/
public function uncheck($xpath) {
$this->inputCheckableControl($xpath);
$javascript = $this->javascriptTemplateRender("check_element.js.twig", array("xpath" => $xpath, "check" => "false"));
$this->browser->evaluate($javascript);
}
/**
* Checks if the radio or checkbox is checked
* @param string $xpath
* @return bool
* @throws DriverException
*/
public function isChecked($xpath) {
$this->findElement($xpath, 1);
$javascript = $this->javascriptTemplateRender("is_checked.js.twig", array("xpath" => $xpath));
$checked = $this->browser->evaluate($javascript);
if ($checked === null) {
throw new DriverException("Can not check when the element is not checkbox or radio");
}
return $checked;
}
/**
* Checks if the option is selected or not
* @param string $xpath
* @return bool
* @throws DriverException
*/
public function isSelected($xpath) {
$elements = $this->findElement($xpath, 1);
$javascript = $this->javascriptTemplateRender("is_selected.js.twig", array("xpath" => $xpath));
$tagName = $this->browser->tagName($elements["page_id"], $elements["ids"][0]);
if (strcmp(strtolower($tagName), "option") !== 0) {
throw new DriverException("Can not assert on element that is not an option");
}
return $this->browser->evaluate($javascript);
}
}

View file

@ -0,0 +1,40 @@
<?php
namespace Zumba\Mink\Driver;
/**
* Class HeadersTrait
* @package Zumba\Mink\Driver
*/
trait HeadersTrait {
/**
* Gets the current request response headers
* Should be called only after a request, other calls are undefined behaviour
* @return array
*/
public function getResponseHeaders() {
return $this->browser->responseHeaders();
}
/**
* Current request status code response
* @return int
*/
public function getStatusCode() {
return $this->browser->getStatusCode();
}
/**
* The name say its all
* @param string $name
* @param string $value
*/
public function setRequestHeader($name, $value) {
$header = array();
$header[$name] = $value;
//TODO: as a limitation of the driver it self, we will send permanent for the moment
$this->browser->addHeader($header, true);
}
}

View file

@ -0,0 +1,49 @@
<?php
namespace Zumba\Mink\Driver;
use Behat\Mink\Exception\DriverException;
/**
* Class JavascriptTrait
* @package Zumba\Mink\Driver
*/
trait JavascriptTrait {
/**
* Executes a script on the browser
* @param string $script
*/
public function executeScript($script) {
$this->browser->execute($script);
}
/**
* Evaluates a script and returns the result
* @param string $script
* @return mixed
*/
public function evaluateScript($script) {
return $this->browser->evaluate($script);
}
/**
* Waits some time or until JS condition turns true.
*
* @param integer $timeout timeout in milliseconds
* @param string $condition JS condition
* @return boolean
* @throws DriverException When the operation cannot be done
*/
public function wait($timeout, $condition) {
$start = microtime(true);
$end = $start + $timeout / 1000.0;
do {
$result = $this->browser->evaluate($condition);
usleep(100000);
} while (microtime(true) < $end && !$result);
return (bool)$result;
}
}

View file

@ -0,0 +1,95 @@
<?php
namespace Zumba\Mink\Driver;
use Behat\Mink\Exception\DriverException;
/**
* Class KeyboardTrait
* @package Zumba\Mink\Driver
*/
trait KeyboardTrait {
/**
* Does some normalization for the char we want to do keyboard events with.
* @param $char
* @throws DriverException
* @return string
*/
protected function normalizeCharForKeyEvent($char) {
if (!is_int($char) && !is_string($char)) {
throw new DriverException("Unsupported key type, can only be integer or string");
}
if (is_string($char) && strlen($char) !== 1) {
throw new DriverException("Key can only have ONE character");
}
$key = $char;
if (is_int($char)) {
$key = chr($char);
}
return $key;
}
/**
* Does some control and normalization for the key event modifier
* @param $modifier
* @return string
* @throws DriverException
*/
protected function keyEventModifierControl($modifier) {
if ($modifier === null) {
$modifier = "none";
}
if (!in_array($modifier, array("none", "alt", "ctrl", "shift", "meta"))) {
throw new DriverException("Unsupported key modifier $modifier");
}
return $modifier;
}
/**
* Send a key-down event to the browser element
* @param $xpath
* @param $char
* @param string $modifier
* @throws DriverException
*/
public function keyDown($xpath, $char, $modifier = null) {
$element = $this->findElement($xpath, 1);
$key = $this->normalizeCharForKeyEvent($char);
$modifier = $this->keyEventModifierControl($modifier);
return $this->browser->keyEvent($element["page_id"], $element["ids"][0], "keydown", $key, $modifier);
}
/**
* @param string $xpath
* @param string $char
* @param string $modifier
* @throws DriverException
*/
public function keyPress($xpath, $char, $modifier = null) {
$element = $this->findElement($xpath, 1);
$key = $this->normalizeCharForKeyEvent($char);
$modifier = $this->keyEventModifierControl($modifier);
return $this->browser->keyEvent($element["page_id"], $element["ids"][0], "keypress", $key, $modifier);
}
/**
* Pressed up specific keyboard key.
*
* @param string $xpath
* @param string|integer $char could be either char ('b') or char-code (98)
* @param string $modifier keyboard modifier (could be 'ctrl', 'alt', 'shift' or 'meta')
*
* @throws DriverException When the operation cannot be done
*/
public function keyUp($xpath, $char, $modifier = null) {
$this->findElement($xpath, 1);
$element = $this->findElement($xpath, 1);
$key = $this->normalizeCharForKeyEvent($char);
$modifier = $this->keyEventModifierControl($modifier);
return $this->browser->keyEvent($element["page_id"], $element["ids"][0], "keyup", $key, $modifier);
}
}

View file

@ -0,0 +1,57 @@
<?php
namespace Zumba\Mink\Driver;
use Behat\Mink\Exception\DriverException;
/**
* Class MouseTrait
* @package Zumba\Mink\Driver
*/
trait MouseTrait {
/**
* Generates a mouseover event on the given element by xpath
* @param string $xpath
* @throws DriverException
*/
public function mouseOver($xpath) {
$element = $this->findElement($xpath, 1);
$this->browser->hover($element["page_id"], $element["ids"][0]);
}
/**
* Clicks if possible on an element given by xpath
* @param string $xpath
* @return mixed
* @throws DriverException
*/
public function click($xpath) {
$elements = $this->findElement($xpath, 1);
$this->browser->click($elements["page_id"], $elements["ids"][0]);
}
/**
* {@inheritdoc}
*/
/**
* Double click on element found via xpath
* @param string $xpath
* @throws DriverException
*/
public function doubleClick($xpath) {
$elements = $this->findElement($xpath, 1);
$this->browser->doubleClick($elements["page_id"], $elements["ids"][0]);
}
/**
* Right click on element found via xpath
* @param string $xpath
* @throws DriverException
*/
public function rightClick($xpath) {
$elements = $this->findElement($xpath, 1);
$this->browser->rightClick($elements["page_id"], $elements["ids"][0]);
}
}

View file

@ -0,0 +1,49 @@
<?php
namespace Zumba\Mink\Driver;
/**
* Trait NavigationTrait
* @package Zumba\Mink\Driver
*/
trait NavigationTrait {
/**
* Visits a given url
* @param string $url
*/
public function visit($url) {
$this->browser->visit($url);
}
/**
* Gets the current url if any
* @return string
*/
public function getCurrentUrl() {
return $this->browser->currentUrl();
}
/**
* Reloads the page if possible
*/
public function reload() {
$this->browser->reload();
}
/**
* Goes forward if possible
*/
public function forward() {
$this->browser->goForward();
}
/**
* Goes back if possible
*/
public function back() {
$this->browser->goBack();
}
}

View file

@ -0,0 +1,72 @@
<?php
namespace Zumba\Mink\Driver;
use Behat\Mink\Exception\DriverException;
/**
* Class PageContentTrait
* @package Zumba\Mink\Driver
*/
trait PageContentTrait {
/**
* @return string
*/
public function getContent() {
return $this->browser->getBody();
}
/**
* Given xpath, will try to get ALL the text, visible and not visible from such xpath
* @param string $xpath
* @return string
* @throws DriverException
*/
public function getText($xpath) {
$elements = $this->findElement($xpath, 1);
//allText works only with ONE element so it will be the first one and also returns new lines that we will remove
$text = $this->browser->allText($elements["page_id"], $elements["ids"][0]);
$text = trim(str_replace(array("\r", "\r\n", "\n"), ' ', $text));
$text = preg_replace('/ {2,}/', ' ', $text);
return $text;
}
/**
* Returns the inner html of a given xpath
* @param string $xpath
* @return string
* @throws DriverException
*/
public function getHtml($xpath) {
$elements = $this->findElement($xpath, 1);
//allText works only with ONE element so it will be the first one
return $this->browser->allHtml($elements["page_id"], $elements["ids"][0], "inner");
}
/**
* Gets the outer html of a given xpath
* @param string $xpath
* @return string
* @throws DriverException
*/
public function getOuterHtml($xpath) {
$elements = $this->findElement($xpath, 1);
//allText works only with ONE element so it will be the first one
return $this->browser->allHtml($elements["page_id"], $elements["ids"][0], "outer");
}
/**
* Returns the binary representation of the current page we are in
* @throws DriverException
* @return string
*/
public function getScreenshot() {
$options = array("full" => true, "selector" => null);
$b64ScreenShot = $this->browser->renderBase64("JPEG", $options);
if (($binaryScreenShot = base64_decode($b64ScreenShot, true)) === false) {
throw new DriverException("There was a problem while doing the screenshot of the current page");
}
return $binaryScreenShot;
}
}

View file

@ -0,0 +1,164 @@
<?php
namespace Zumba\Mink\Driver;
use Behat\Mink\Element\NodeElement;
use Behat\Mink\Exception\DriverException;
/**
* Class PhantomJSDriver
* @package Behat\Mink\Driver
*/
class PhantomJSDriver extends BasePhantomJSDriver {
use SessionTrait;
use NavigationTrait;
use CookieTrait;
use HeadersTrait;
use JavascriptTrait;
use MouseTrait;
use PageContentTrait;
use KeyboardTrait;
use FormManipulationTrait;
use WindowTrait;
/**
* Sets the basic auth user and password
* @param string $user
* @param string $password
*/
public function setBasicAuth($user, $password) {
$this->browser->setHttpAuth($user, $password);
}
/**
* Gets the tag name of a given xpath
* @param string $xpath
* @return string
* @throws DriverException
*/
public function getTagName($xpath) {
$elements = $this->findElement($xpath, 1);
return $this->browser->tagName($elements["page_id"], $elements["ids"][0]);
}
/**
* Gets the attribute value of a given element and name
* @param string $xpath
* @param string $name
* @return string
* @throws DriverException
*/
public function getAttribute($xpath, $name) {
$elements = $this->findElement($xpath, 1);
return $this->browser->attribute($elements["page_id"], $elements["ids"][0], $name);
}
/**
* Check if element given by xpath is visible or not
* @param string $xpath
* @return bool
* @throws DriverException
*/
public function isVisible($xpath) {
$elements = $this->findElement($xpath, 1);
return $this->browser->isVisible($elements["page_id"], $elements["ids"][0]);
}
/**
* Drags one element to another
* @param string $sourceXpath
* @param string $destinationXpath
* @throws DriverException
*/
public function dragTo($sourceXpath, $destinationXpath) {
$sourceElement = $this->findElement($sourceXpath, 1);
$destinationElement = $this->findElement($destinationXpath, 1);
$this->browser->drag($sourceElement["page_id"], $sourceElement["ids"][0], $destinationElement["ids"][0]);
}
/**
* Upload a file to the browser
* @param string $xpath
* @param string $path
* @throws DriverException
*/
public function attachFile($xpath, $path) {
if (!file_exists($path)) {
throw new DriverException("Wow there the file does not exist, you can not upload it");
}
if (($realPath = realpath($path)) === false) {
throw new DriverException("Wow there the file does not exist, you can not upload it");
}
$element = $this->findElement($xpath, 1);
$tagName = $this->getTagName($xpath);
if ($tagName != "input") {
throw new DriverException("The element is not an input element, you can not attach a file to it");
}
$attributes = $this->getBrowser()->attributes($element["page_id"], $element["ids"][0]);
if (!isset($attributes["type"]) || $attributes["type"] != "file") {
throw new DriverException("The element is not an input file type element, you can not attach a file to it");
}
$this->browser->selectFile($element["page_id"], $element["ids"][0], $realPath);
}
/**
* Puts the browser control inside the IFRAME
* You own the control, make sure to go back to the parent calling this method with null
* @param string $name
*/
public function switchToIFrame($name = null) {
//TODO: check response of the calls
if ($name === null) {
$this->browser->popFrame();
return;
} else {
$this->browser->pushFrame($name);
}
}
/**
* Focus on an element
* @param string $xpath
* @throws DriverException
*/
public function focus($xpath) {
$element = $this->findElement($xpath, 1);
$this->browser->trigger($element["page_id"], $element["ids"][0], "focus");
}
/**
* Blur on element
* @param string $xpath
* @throws DriverException
*/
public function blur($xpath) {
$element = $this->findElement($xpath, 1);
$this->browser->trigger($element["page_id"], $element["ids"][0], "blur");
}
/**
* Finds elements with specified XPath query.
* @param string $xpath
* @return NodeElement[]
* @throws DriverException When the operation cannot be done
*/
public function find($xpath) {
$elements = $this->browser->find("xpath", $xpath);
$nodeElements = array();
if (!isset($elements["ids"])) {
return null;
}
foreach ($elements["ids"] as $i => $elementId) {
$nodeElements[] = new NodeElement(sprintf('(%s)[%d]', $xpath, $i + 1), $this->session);
}
return $nodeElements;
}
}

View file

@ -0,0 +1,35 @@
{% autoescape 'js' %}
(function (xpath, check) {
function getPolterNode(xpath) {
var polterAgent = window.__poltergeist;
var ids = polterAgent.find("xpath", xpath, document);
return polterAgent.get(ids[0]);
}
var pNode = getPolterNode(xpath);
if (check && pNode.element.checked) {
//requested to check the element and is already check, do nothing.
return true;
}
if (!check && pNode.element.checked == false) {
//move along nothing to be done
return true;
}
if (check && pNode.element.checked == false) {
//we have to check the element, we will do so by triggering a click event so all change listeners are aware.
pNode.trigger("click");
pNode.element.checked = true;
}
if (!check && pNode.element.checked) {
//move along nothing to be done
pNode.trigger("click");
pNode.element.checked = false;
return true;
}
return false;
}('{{xpath}}', {{check}}));
{% endautoescape %}

View file

@ -0,0 +1,3 @@
{% autoescape false %}
{{ script }};
{% endautoescape %}

View file

@ -0,0 +1,63 @@
{% autoescape 'js' %}
(function (xpath) {
function getElement(xpath) {
var polterAgent = window.__poltergeist;
var ids = polterAgent.find("xpath", xpath, document);
var polterNode = polterAgent.get(ids[0]);
return polterNode.element;
}
function inputRadioGetValue(element){
var value = null;
var name = element.getAttribute('name');
if (!name){
return null;
}
var fields = window.document.getElementsByName(name);
var i;
var l = fields.length;
for (i = 0; i < l; i++) {
var field = fields.item(i);
if (field.form === element.form && field.checked) {
return field.value;
}
}
return null;
}
var node = getElement(xpath);
var tagName = node.tagName.toLowerCase();
var value = null;
if (tagName == "input") {
var type = node.type.toLowerCase();
if (type == "checkbox") {
value = node.checked ? node.value : null;
} else if (type == "radio") {
value = inputRadioGetValue(node);
} else {
value = node.value;
}
} else if (tagName == "textarea") {
value = node.value;
} else if (tagName == "select") {
if (node.multiple) {
value = [];
for (var i = 0; i < node.options.length; i++) {
if (node.options[i].selected) {
value.push(node.options[i].value);
}
}
} else {
var idx = node.selectedIndex;
if (idx >= 0) {
value = node.options.item(idx).value;
} else {
value = null;
}
}
} else {
value = node.value;
}
return value;
}('{{ xpath }}'));
{% endautoescape %}

View file

@ -0,0 +1,31 @@
{% autoescape 'js' %}
(function (xpath) {
function getElement(xpath, within) {
var result;
if (within === null || within === undefined) {
within = document;
}
result = document.evaluate(xpath, within, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
if (result.snapshotLength !== 1) {
return null;
}
return result.snapshotItem(0);
}
var node = getElement(xpath);
if (node === null) {
return null;
}
if(node.tagName.toLowerCase() != "input"){
return null;
}
if(node.type.toLowerCase() != "checkbox" && node.type.toLowerCase() != "radio"){
return null;
}
return node.checked;
}('{{ xpath }}'));
{% endautoescape %}

View file

@ -0,0 +1,16 @@
{% autoescape 'js' %}
(function (xpath) {
function getElement(xpath) {
var polterAgent = window.__poltergeist;
var ids = polterAgent.find("xpath", xpath, document);
var polterNode = polterAgent.get(ids[0]);
return polterNode.element;
}
var node = getElement(xpath);
if(typeof node.selected == "undefined"){
return null;
}
return node.selected;
}('{{xpath}}'));
{% endautoescape %}

View file

@ -0,0 +1,213 @@
{% autoescape 'js' %}
(function (xpath, value) {
function getElement(xpath, within) {
var result;
if (within === null || within === undefined) {
within = document;
}
result = document.evaluate(xpath, within, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
if (result.snapshotLength !== 1) {
return null;
}
return result.snapshotItem(0);
}
function isInput(element) {
if (element === null || element === undefined) {
return false;
}
return (element.tagName.toLowerCase() == "input");
}
function isTextArea(element) {
if (element === null || element === undefined) {
return false;
}
return (element.tagName.toLowerCase() == "textarea");
}
function isSelect(element) {
if (element === null || element === undefined) {
return false;
}
return (element.tagName.toLowerCase() == "select");
}
function deselectAllOptions(element) {
var i, l = element.options.length;
for (i = 0; i < l; i++) {
element.options[i].selected = false;
}
}
function xpathStringLiteral(s) {
if (s.indexOf('"') === -1)
return '"' + s + '"';
if (s.indexOf("'") === -1)
return "'" + s + "'";
return 'concat("' + s.replace(/"/g, '",\'"\',"') + '")';
}
function clickOnElement(element) {
// create a mouse click event
var event = document.createEvent('MouseEvents');
event.initMouseEvent('click', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
// send click to element
element.dispatchEvent(event);
//After dispatching the event let's wait for 2 seconds at least...
return setTimeout(function () {
}, 2);
}
function dispatchChange(element) {
var tagName =element.tagName.toLowerCase();
var elementType = element.getAttribute("type");
if (tagName != "option" || (tagName == "input" && elementType == "radio")){
return true;
}
//Force the change when element is option
var event;
event = document.createEvent('HTMLEvents');
event.initEvent('change', true, false);
element.dispatchEvent(event);
return true;
}
function selectOptionOnElement(element, option, multiple) {
var polterAgent = window.__poltergeist;
var escapedOption = xpathStringLiteral(option);
// 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 = polterAgent.find("xpath", optionQuery, element);
var polterNode = polterAgent.get(ids[0]);
var optionElement = polterNode.element;
if (multiple || !element.multiple) {
if (!optionElement.selected) {
clickOnElement(optionElement);
optionElement.selected = true;
}
return dispatchChange(optionElement);
}
deselectAllOptions(element);
clickOnElement(optionElement);
optionElement.selected = true;
return dispatchChange(optionElement);
}
function selectSetValue(element, value) {
var option;
if ((Array.isArray && Array.isArray(value)) || (value instanceof Array)) {
deselectAllOptions(element);
for (option in value) {
if (value.hasOwnProperty(option)) {
selectOptionOnElement(element, value[option], true);
}
}
return true;
}
selectOptionOnElement(element, value, false);
return true;
}
function selectRadioValue(element, value) {
if (element.value === value) {
clickOnElement(element);
element.checked=true;
dispatchChange(element);
return true;
}
var formElements = element.form.elements;
var name = element.getAttribute("name");
var radioElement, i;
if (!name) {
return null;
}
for (i = 0; i < formElements.length; i++) {
radioElement = formElements[i];
if (radioElement.tagName.toLowerCase() == 'input' && radioElement.type.toLowerCase() == 'radio' && radioElement.name === name) {
if (value === radioElement.value) {
clickOnElement(radioElement);
radioElement.checked=true;
dispatchChange(radioElement);
return true;
}
}
}
return null;
}
function inputSetValue(element, value, elementXpath) {
var allowedTypes = ['submit', 'image', 'button', 'reset'];
var elementType = element.type.toLowerCase();
var textLikeInputType = ['file', 'text', 'password', 'url', 'email', 'search', 'number', 'tel', 'range', 'date', 'month', 'week', 'time', 'datetime', 'color', 'datetime-local'];
if (allowedTypes.indexOf(elementType) !== -1) {
return null;
}
if (elementType == "checkbox") {
var booleanValue = false;
if (value == "1" || value == 1) {
booleanValue = true;
} else if (value == "0" || value == 0) {
booleanValue = false;
}
if ((element.checked && !booleanValue) || (!element.checked && booleanValue)) {
clickOnElement(element);
dispatchChange(element);
}
return true;
}
if (elementType == "radio") {
return selectRadioValue(element, value);
}
if (textLikeInputType.indexOf(elementType) !== -1) {
return textAreaSetValue(elementXpath, value);
}
//No support for the moment for file stuff or other input types
return null;
}
function textAreaSetValue(elementXpath, value) {
var polterAgent = window.__poltergeist;
var ids = polterAgent.find("xpath", elementXpath, document);
var polterNode = polterAgent.get(ids[0]);
polterNode.set(value);
return true;
}
var node = getElement(xpath);
if (node === null) {
return null;
}
if (isSelect(node)) {
return selectSetValue(node, value);
}
if (isInput(node)) {
return inputSetValue(node, value, xpath);
}
if (isTextArea(node)) {
return textAreaSetValue(xpath, value);
}
//for the moment everything else also to textArea stuff
return textAreaSetValue(xpath, value);
}('{{xpath}}', JSON.parse('{{ value }}')));
{% endautoescape %}

View file

@ -0,0 +1,50 @@
<?php
namespace Zumba\Mink\Driver;
/**
* Trait SessionTrait
* @package Zumba\Mink\Driver
*/
trait SessionTrait {
/** @var bool */
protected $started;
/**
* Starts a session to be used by the driver client
*/
public function start() {
$this->started = true;
}
/**
* Tells if the session is started or not
* @return bool
*/
public function isStarted() {
return $this->started;
}
/**
* Stops the session completely, clean slate for the browser
* @return bool
*/
public function stop() {
//Since we are using a remote browser "API", stopping is just like resetting, say good bye to cookies
//TODO: In the future we may want to control a start / stop of the remove browser
return $this->reset();
}
/**
* Clears the cookies in the browser, all of them
* @return bool
*/
public function reset() {
$this->getBrowser()->clearCookies();
$this->getBrowser()->reset();
$this->started = false;
return true;
}
}

View file

@ -0,0 +1,64 @@
<?php
namespace Zumba\Mink\Driver;
use Behat\Mink\Exception\DriverException;
/**
* Class WindowTrait
* @package Zumba\Mink\Driver
*/
trait WindowTrait {
/**
* Returns the current page window name
* @return string
*/
public function getWindowName() {
return $this->browser->windowName();
}
/**
* Return all the window handles currently present in phantomjs
* @return array
*/
public function getWindowNames() {
return $this->browser->windowHandles();
}
/**
* Switches to window by name if possible
* @param $name
* @throws DriverException
*/
public function switchToWindow($name = null) {
$handles = $this->browser->windowHandles();
if ($name === null) {
//null means back to the main window
return $this->browser->switchToWindow($handles[0]);
}
$windowHandle = $this->browser->windowHandle($name);
if (!empty($windowHandle)) {
$this->browser->switchToWindow($windowHandle);
} else {
throw new DriverException("Could not find window handle by a given window name: $name");
}
}
/**
* Resizing a window with specified size
* @param int $width
* @param int $height
* @param string $name
* @throws DriverException
*/
public function resizeWindow($width, $height, $name = null) {
if ($name !== null) {
//TODO: add this on the phantomjs stuff
throw new DriverException("Resizing other window than the main one is not supported yet");
}
$this->browser->resize($width, $height);
}
}