Browse Source

Full rebuild of PHP PhantomJS. See README for features.

Jonny Wenmoth 11 năm trước cách đây
mục cha
commit
6af27880ec
78 tập tin đã thay đổi với 7330 bổ sung1265 xóa
  1. 1 2
      .gitignore
  2. 15 278
      README.md
  3. BIN
      bin/phantomjs
  4. 97 0
      bin/phantomloader
  5. 1 0
      bin/phpunit
  6. 16 4
      composer.json
  7. 4 0
      doc/advanced.rst
  8. 4 0
      doc/debugging.rst
  9. 4 0
      doc/examples.rst
  10. 98 0
      doc/installation.rst
  11. 98 0
      doc/intro.rst
  12. 15 0
      examples/basic-request.php
  13. 29 0
      examples/capture-request.php
  14. 40 0
      examples/custom-script.php
  15. 19 0
      examples/debug-request.php
  16. 26 0
      examples/delay-page-render.php
  17. 19 0
      examples/javascript-console.php
  18. 21 0
      examples/post-request.php
  19. 5 2
      phpunit.xml.dist
  20. 53 0
      src/JonnyW/PhantomJs/Cache/CacheInterface.php
  21. 183 0
      src/JonnyW/PhantomJs/Cache/FileCache.php
  22. 187 217
      src/JonnyW/PhantomJs/Client.php
  23. 95 20
      src/JonnyW/PhantomJs/ClientInterface.php
  24. 62 0
      src/JonnyW/PhantomJs/DependencyInjection/ServiceContainer.php
  25. 19 0
      src/JonnyW/PhantomJs/Exception/InvalidExecutableException.php
  26. 1 1
      src/JonnyW/PhantomJs/Exception/InvalidMethodException.php
  27. 1 1
      src/JonnyW/PhantomJs/Exception/InvalidUrlException.php
  28. 1 1
      src/JonnyW/PhantomJs/Exception/NotExistsException.php
  29. 1 1
      src/JonnyW/PhantomJs/Exception/NotWritableException.php
  30. 1 1
      src/JonnyW/PhantomJs/Exception/PhantomJsException.php
  31. 19 0
      src/JonnyW/PhantomJs/Exception/ProcedureFailedException.php
  32. 352 0
      src/JonnyW/PhantomJs/Message/AbstractRequest.php
  33. 185 0
      src/JonnyW/PhantomJs/Message/CaptureRequest.php
  34. 77 0
      src/JonnyW/PhantomJs/Message/CaptureRequestInterface.php
  35. 0 60
      src/JonnyW/PhantomJs/Message/Factory.php
  36. 0 40
      src/JonnyW/PhantomJs/Message/FactoryInterface.php
  37. 79 0
      src/JonnyW/PhantomJs/Message/MessageFactory.php
  38. 55 0
      src/JonnyW/PhantomJs/Message/MessageFactoryInterface.php
  39. 16 228
      src/JonnyW/PhantomJs/Message/Request.php
  40. 61 16
      src/JonnyW/PhantomJs/Message/RequestInterface.php
  41. 58 41
      src/JonnyW/PhantomJs/Message/Response.php
  42. 14 5
      src/JonnyW/PhantomJs/Message/ResponseInterface.php
  43. 38 0
      src/JonnyW/PhantomJs/Parser/JsonParser.php
  44. 25 0
      src/JonnyW/PhantomJs/Parser/ParserInterface.php
  45. 76 0
      src/JonnyW/PhantomJs/Procedure/ChainProcedureLoader.php
  46. 186 0
      src/JonnyW/PhantomJs/Procedure/Procedure.php
  47. 79 0
      src/JonnyW/PhantomJs/Procedure/ProcedureFactory.php
  48. 26 0
      src/JonnyW/PhantomJs/Procedure/ProcedureFactoryInterface.php
  49. 47 0
      src/JonnyW/PhantomJs/Procedure/ProcedureInterface.php
  50. 90 0
      src/JonnyW/PhantomJs/Procedure/ProcedureLoader.php
  51. 77 0
      src/JonnyW/PhantomJs/Procedure/ProcedureLoaderFactory.php
  52. 27 0
      src/JonnyW/PhantomJs/Procedure/ProcedureLoaderFactoryInterface.php
  53. 26 0
      src/JonnyW/PhantomJs/Procedure/ProcedureLoaderInterface.php
  54. 45 0
      src/JonnyW/PhantomJs/Resources/config/config.yml
  55. 82 0
      src/JonnyW/PhantomJs/Resources/config/services.yml
  56. 149 0
      src/JonnyW/PhantomJs/Resources/procedures/capture.proc
  57. 125 0
      src/JonnyW/PhantomJs/Resources/procedures/default.proc
  58. 50 0
      src/JonnyW/PhantomJs/Template/TemplateRenderer.php
  59. 27 0
      src/JonnyW/PhantomJs/Template/TemplateRendererInterface.php
  60. 30 0
      src/JonnyW/PhantomJs/Test/TestCase.php
  61. 525 0
      src/JonnyW/PhantomJs/Tests/Integration/ClientTest.php
  62. 152 0
      src/JonnyW/PhantomJs/Tests/Integration/Template/TemplateRendererTest.php
  63. 307 0
      src/JonnyW/PhantomJs/Tests/Unit/Cache/FileCacheTest.php
  64. 462 0
      src/JonnyW/PhantomJs/Tests/Unit/ClientTest.php
  65. 543 0
      src/JonnyW/PhantomJs/Tests/Unit/Message/CaptureRequestTest.php
  66. 195 0
      src/JonnyW/PhantomJs/Tests/Unit/Message/MessageFactoryTest.php
  67. 452 0
      src/JonnyW/PhantomJs/Tests/Unit/Message/RequestTest.php
  68. 389 0
      src/JonnyW/PhantomJs/Tests/Unit/Message/ResponseTest.php
  69. 209 0
      src/JonnyW/PhantomJs/Tests/Unit/Parser/JsonParserTest.php
  70. 156 0
      src/JonnyW/PhantomJs/Tests/Unit/Procedure/ChainProcedureLoaderTest.php
  71. 108 0
      src/JonnyW/PhantomJs/Tests/Unit/Procedure/ProcedureFactoryTest.php
  72. 120 0
      src/JonnyW/PhantomJs/Tests/Unit/Procedure/ProcedureLoaderFactoryTest.php
  73. 269 0
      src/JonnyW/PhantomJs/Tests/Unit/Procedure/ProcedureLoaderTest.php
  74. 205 0
      src/JonnyW/PhantomJs/Tests/Unit/Procedure/ProcedureTest.php
  75. 1 3
      src/JonnyW/PhantomJs/Tests/bootstrap.php
  76. 0 143
      test/JonnyW/PhantomJs/Test/ClientTest.php
  77. 0 62
      test/JonnyW/PhantomJs/Test/Message/FactoryTest.php
  78. 0 139
      test/JonnyW/PhantomJs/Test/Message/RequestTest.php

+ 1 - 2
.gitignore

@@ -1,5 +1,4 @@
 .DS_Store
 composer.phar
-test.sh
-docs.sh
+composer.lock
 vendor/

+ 15 - 278
README.md

@@ -1,287 +1,24 @@
 PHP PhantomJS
 =============
 
-PHP PhantomJS is a simple PHP library to load pages through the PhantomJS 
+PHP PhantomJS is a flexible PHP library to load pages through the PhantomJS 
 headless browser and return the page response. It is handy for testing
 websites that demand javascript support and also supports screen captures.
 
-[![Total Downloads](https://poser.pugx.org/jonnyw/php-phantomjs/downloads.png)](https://packagist.org/packages/jonnyw/php-phantomjs) [![Latest Stable Version](https://poser.pugx.org/jonnyw/php-phantomjs/v/stable.png)](https://packagist.org/packages/jonnyw/php-phantomjs) [![Build Status](https://travis-ci.org/jonnnnyw/php-phantomjs.png?branch=master)](https://travis-ci.org/jonnnnyw/php-phantomjs) [![Scrutinizer Quality Score](https://scrutinizer-ci.com/g/jonnnnyw/php-phantomjs/badges/quality-score.png?s=631d32fa1fbb9300eb84b9b52702c7ffeac046a1)](https://scrutinizer-ci.com/g/jonnnnyw/php-phantomjs/) [![Code Coverage](https://scrutinizer-ci.com/g/jonnnnyw/php-phantomjs/badges/coverage.png?s=893b5997da45448e32983b8568a39630b0b2d91b)](https://scrutinizer-ci.com/g/jonnnnyw/php-phantomjs/)
+[Full Documentation](http://jonnnnyw.github.io/php-phantomjs/)
 
-0.0 Table of Contents
+Feature List
 ---------------------
 
-* Introduction
-* Examples
-* Changelog
-* Troubleshooting
-
-
-1.0 Introduction
-----------------
-
-This library provides the ability to load pages through the PhantomJS 
-headless browser and return the page response. It also lets you screen
-capture pages and save the captures to disk. It is designed to meet a 
-simple objective and does not offer a PHP API to the full suite of 
-PhantomJS functionality.
-
-The PhantomJS executable comes bundled with the library. If installed
-via composer, the file will be symlinked in your bin folder.
-
-If you are looking for a PHP library to run external javascript files 
-through PhantomJS then this is not for you. Check out [PhantomJS runner](https://github.com/Dachande663/PHP-PhantomJS).
-
-
-2.0 Examples
-------------
-
-Request a URL and output the reponse:
-
-```php
-<?php
-
-use JonnyW\PhantomJs\Client;
-
-$client = Client::getInstance();
-
-/** 
- * @see JonnyW\PhantomJs\Message\Request 
- **/
-$request = $client->getMessageFactory()->createRequest('GET', 'http://google.com');
-
-/** 
- * @see JonnyW\PhantomJs\Message\Response 
- **/
-$response = $client->getMessageFactory()->createResponse();
-
-// Send the request
-$client->send($request, $response);
-
-if($response->getStatus() === 200) {
-	
-	// Dump the requested page content
-	echo $response->getContent();
-}
-```
-
-
-Request a URL with delay and output the reponse:
-
-```php
-<?php
-
-use JonnyW\PhantomJs\Client;
-
-$client = Client::getInstance();
-
-/** 
- * @see JonnyW\PhantomJs\Message\Request 
- **/
-$request = $client->getMessageFactory()->createRequest('GET', 'http://google.com');
-
-/** 
- * @see JonnyW\PhantomJs\Message\Response 
- **/
-$response = $client->getMessageFactory()->createResponse();
-
-// Send the request with delay in miliseconds
-$client->open($request, $response, $delay = 5000);
-
-if($response->getStatus() === 200) {
-	
-	// Dump the requested page content
-	echo $response->getContent();
-}
-```
-
-
-Request a URL, save a screen capture to provided location and return the response:
-
-```php
-<?php
-
-use JonnyW\PhantomJs\Client;
-
-$client = Client::getInstance();
-
-/** 
- * @see JonnyW\PhantomJs\Message\Request 
- **/
-$request = $client->getMessageFactory()->createRequest('GET', 'http://google.com');
-
-/** 
- * @see JonnyW\PhantomJs\Message\Response 
- **/
-$response = $client->getMessageFactory()->createResponse();
-
-// Send the request
-$client->send($request, $response, '/path/to/save/screen/capture.png');
-```
-
-Send post request with data:
-
-```php
-<?php
-
-use JonnyW\PhantomJs\Client;
-
-$client = Client::getInstance();
-
-/** 
- * @see JonnyW\PhantomJs\Message\Request 
- **/
-$request = $client->getMessageFactory()->createRequest();
-
-$request->setMethod('POST');
-$request->setUrl('http://google.com');
-
-$request->setRequestData(array(
-	'name' 	=> 'Test',
-	'email' => 'test@jonnyw.me'
-));
-
-/** 
- * @see JonnyW\PhantomJs\Message\Response 
- **/
-$response = $client->getMessageFactory()->createResponse();
-
-// Send the request
-$client->send($request, $response);
-```
-
-Set a request header:
-
-```php
-<?php
-
-use JonnyW\PhantomJs\Client;
-
-$client = Client::getInstance();
-
-/** 
- * @see JonnyW\PhantomJs\Message\Request 
- **/
-$request = $client->getMessageFactory()->createRequest();
-
-$request->addHeader('User-Agent', 'Mozilla/5.0 (Macintosh; Intel Mac OS X) AppleWebKit/534.34 (KHTML, like Gecko) PhantomJS/1.9.2 Safari/534.34');
-```
-
-Get a response header:
-
-```php
-<?php
-
-use JonnyW\PhantomJs\Client;
-
-$client = Client::getInstance();
-
-/** 
- * @see JonnyW\PhantomJs\Message\Request 
- **/
-$request = $client->getMessageFactory()->createRequest('GET', 'http://google.com');
-
-/** 
- * @see JonnyW\PhantomJs\Message\Response 
- **/
-$response = $client->getMessageFactory()->createResponse();
-
-// Send the request
-$client->send($request, $response, '/path/to/save/screen/capture.png');
-
-echo $response->getHeader('Cache-Control');
-```
-
-Handle response redirect:
-
-```php
-<?php
-
-use JonnyW\PhantomJs\Client;
-
-$client = Client::getInstance();
-
-/** 
- * @see JonnyW\PhantomJs\Message\Request 
- **/
-$request = $client->getMessageFactory()->createRequest('GET', 'http://google.com');
-
-/** 
- * @see JonnyW\PhantomJs\Message\Response 
- **/
-$response = $client->getMessageFactory()->createResponse();
-
-// Send the request
-$client->send($request, $response);
-
-if($response->isRedirect()) {
-	echo $response->getRedirectUrl();
-}
-
-```
-
-Define a new path to the PhantomJS executable:
-
-```php
-<?php
-
-use JonnyW\PhantomJs\Client;
-
-$client = Client::getInstance();
-$client->setPhantomJs('/path/to/phantomjs');
-```
-
-Set timeout for the request (defaults to 5 seconds):
-
-```php
-<?php
-
-use JonnyW\PhantomJs\Client;
-
-$client = Client::getInstance();
-$client->setTimeout(10000); // In milleseconds
-```
-
-3.0 Changelog
-------------
-
-### V2.0.0
-
-Version 2.0.0 changes the way requests are made. Requests now require you 
-to inject a request and response instance into the send method. The examples 
-above illustrate how to make requests in version 2.0.0 and will not work 
-for older versions.
-
-* Requests now require you to inject a request and response instance when sending.
-* Added message factory that can also be injected into client when instantiated.
-* Custom headers can be set in requests.
-* Request method can be set. Supports: OPTIONS, GET, HEAD, POST, PUT, DELETE, PATCH.
-* Request data can be set. Useful when making post requests.
-
-4.0 Troubleshooting
-------------
-
-If you are using V1.0.0 then the examples above won't work for you. It is reccommend that you upgrade to the latest version.
-
-Look at the response class (JonnyW\PhantomJs\Response) to see what data you have access to.
-
-An explanation of the errors that are thrown by the client:
-
-### CommandFailedException
-
-The command sent to the PhantomJS executable failed. This should be very rare and is probably my fault if this happens (sorry).
-
-### InvalidUrlException
-
-The URL you are providing is an invalid format. It is very loose verification.
-
-### InvalidMethodException
-
-The request method you are providing is invalid.
-
-### NoPhantomJsException
-
-The PhantomJS executable cannot be found or it is not executable. Check the path and permissions.
-
-### NotWriteableException
-
-The screen capture location you provided or your /tmp folder are not writeable. The /tmp folder is used to temporarily write the scripts that PhantomJS executes. They are deleted after execution or on failure.
+* Load webpages through the PhantomJS headless browser
+* View detailed response data including page content, headers, status code etc.
+* Handle redirects
+* View javascript console errors
+* View detailed PhantomJS debuged information
+* Save screen captures to local disk 
+* Define screen capture x, y, width and height parameters
+* Delay page rendering for a specified time
+* Execute PhantomJS with command line options
+* Easily build and run custom PhantomJS scripts
+
+[![Total Downloads](https://poser.pugx.org/jonnyw/php-phantomjs/downloads.png)](https://packagist.org/packages/jonnyw/php-phantomjs) [![Latest Stable Version](https://poser.pugx.org/jonnyw/php-phantomjs/v/stable.png)](https://packagist.org/packages/jonnyw/php-phantomjs) [![Build Status](https://travis-ci.org/jonnnnyw/php-phantomjs.png?branch=master)](https://travis-ci.org/jonnnnyw/php-phantomjs) [![Scrutinizer Quality Score](https://scrutinizer-ci.com/g/jonnnnyw/php-phantomjs/badges/quality-score.png?s=631d32fa1fbb9300eb84b9b52702c7ffeac046a1)](https://scrutinizer-ci.com/g/jonnnnyw/php-phantomjs/) [![Code Coverage](https://scrutinizer-ci.com/g/jonnnnyw/php-phantomjs/badges/coverage.png?s=893b5997da45448e32983b8568a39630b0b2d91b)](https://scrutinizer-ci.com/g/jonnnnyw/php-phantomjs/)

BIN
bin/phantomjs


+ 97 - 0
bin/phantomloader

@@ -0,0 +1,97 @@
+
+phantom.onError = function(msg, trace) {
+        
+    var system   = require('system'),
+        date     = new Date().toISOString().slice(0, -5),
+        response = { 'content':  date + ' [ERROR] PhantomJS - ' + msg },
+        stack    = [ date + ' [ERROR] PhantomJS - ' + msg ];
+    
+    if (trace && trace.length) {
+            
+        trace.forEach(function(t) {
+            stack.push(date + ' [STACK] PhantomJS    ->' + (t.file || t.sourceURL) + ': ' + t.line + (t.function ? ' (in function ' + t.function + ')' : ''));
+        });
+    }
+    
+    console.log(JSON.stringify(response, undefined, 4));
+    system.stderr.write(stack.join('\n') + '\n');
+    phantom.exit(1);
+};
+
+var wrappedWindow = (function() {
+
+    var system = require('system'),
+        fs     = require('fs'),
+        script;
+
+    if (system.args.length < 2) {
+        throw new Error('No script file was provided to PhantomJS executable.');
+    }
+
+    script = fs.absolute(system.args[1]);
+
+    if (!fs.exists(script)) {
+        throw new Error('Script file does not exist: ' + script);
+    }
+    
+    if (!fs.isFile(script)) {
+        throw new Error('Script file is not of type \'file\': ' + script);
+    }
+    
+    if (!fs.isReadable(script)) {
+        throw new Error('Script file is not readable: ' + script);
+    }
+
+    var p = Object.create(phantom),
+        s = Object.create(system),
+        w = Object.create(window);
+    
+    Object.defineProperty(p, 'scriptName', {
+        value: system.args[1].replace(/\\/g, '/').split('/').slice(-1)[0],
+        enumerable: true
+    });
+    
+    Object.defineProperty(p, 'libraryPath', {
+        value: require('fs').absolute(system.args[1].replace(/\\/g, '/').split('/').slice(0, -1).join('/')),
+        enumerable: true
+    });
+    
+    Object.defineProperty(p, 'args', {
+        value: system.args.slice(2),
+        enumerable: true
+    });
+    
+    Object.defineProperty(s, 'args', {
+        value: system.args.slice(1),
+        enumerable: true
+    });
+    
+    Object.defineProperty(w, 'phantom', {
+        value: p,
+        enumerable: true
+    });
+
+    var r = (function(require) {
+        return function(moduleId) {
+            if (moduleId === 'system') {
+                return s;
+            }
+            return require(moduleId);
+        };
+    })(require);
+
+    w.require = r;
+    
+    return w;
+})();
+
+(function(window, require, phantom) {
+
+    phantom.injectJs(require('system').args[0]);
+    
+}).call(
+    wrappedWindow, 
+    wrappedWindow, 
+    wrappedWindow.require, 
+    wrappedWindow.phantom
+);

+ 1 - 0
bin/phpunit

@@ -0,0 +1 @@
+../vendor/phpunit/phpunit/composer/bin/phpunit

+ 16 - 4
composer.json

@@ -13,7 +13,11 @@
     ],
     "minimum-stability": "stable",
     "require": {
-    	"php": ">=5.3.0"
+        "php": ">=5.3.0",
+        "symfony/config": "2.5.0",
+        "symfony/dependency-injection": "2.5.0",
+        "twig/twig": "1.16",
+        "jakoch/phantomjs-installer": "1.9.7"
     },
     "require-dev": {
         "phpunit/phpunit": "3.7.*"
@@ -27,7 +31,15 @@
     "config": {
         "bin-dir": "bin"
     },
-	"bin": [
-        "bin/phantomjs"
-    ]
+    "bin": [
+        "bin/phantomloader"
+    ],
+    "scripts": {
+        "post-install-cmd": [
+            "PhantomInstaller\\Installer::installPhantomJS"
+        ],
+        "post-update-cmd": [
+            "PhantomInstaller\\Installer::installPhantomJS"
+        ]
+    }
 }

+ 4 - 0
doc/advanced.rst

@@ -0,0 +1,4 @@
+Advanced
+=============
+
+Check back tomororw :)

+ 4 - 0
doc/debugging.rst

@@ -0,0 +1,4 @@
+Debugging
+=============
+
+Check back tomororw :)

+ 4 - 0
doc/examples.rst

@@ -0,0 +1,4 @@
+Examples
+=============
+
+Check back tomororw :)

+ 98 - 0
doc/installation.rst

@@ -0,0 +1,98 @@
+Installation
+=============
+
+PHP PhantomJS is a flexible PHP library to load pages through the PhantomJS 
+headless browser and return the page response. It is handy for testing
+websites that demand javascript support and also supports screen captures.
+
+Feature List
+---------------------
+
+* Load webpages through the PhantomJS headless browser
+* View detailed response data including page content, headers, status code etc.
+* Handle redirects
+* View javascript console errors
+* View detailed PhantomJS debuged information
+* Save screen captures to local disk 
+* Define screen capture x, y, width and height parameters
+* Delay page rendering for a specified time
+* Execute PhantomJS with command line options
+* Easily build and run custom PhantomJS scripts
+
+Prerequisites
+---------------------
+
+PHP PhantomJS requires PHP **5.3.0** or greater to run.
+
+Installation
+---------------------
+
+It is recommended that you use Composer to install PHP PhantomJS:
+
+```xml
+composer require "jonnyw/php-phantomjs:3.*"
+```
+
+If you would like to use another installation method or would like to see more detailed installation instruction, see the [installation](http://jonnnnyw.github.io/php-phantomjs/) documentation.
+
+
+Basic Usage
+---------------------
+
+The following illustrates how to make a basic GET request and output the page content:
+
+```php
+<?php
+
+use JonnyW\PhantomJs\Client;
+
+$client = Client::getInstance();
+
+/** 
+ * @see JonnyW\PhantomJs\Message\Request 
+ **/
+$request = $client->getMessageFactory()
+    ->createRequest('http://google.com', 'GET');
+
+/** 
+ * @see JonnyW\PhantomJs\Message\Response 
+ **/
+$response = $client->getMessageFactory()->createResponse();
+
+// Send the request
+$client->send($request, $response);
+
+if($response->getStatus() === 200) {
+
+    // Dump the requested page content
+    echo $response->getContent();
+}
+```
+
+And if you would like to save a screen capture to local disk:
+
+```php
+<?php
+
+use JonnyW\PhantomJs\Client;
+
+$client = Client::getInstance();
+
+/** 
+ * @see JonnyW\PhantomJs\Message\Request 
+ **/
+$request = $client->getMessageFactory()->createCaptureRequest('http://google.com', 'GET');
+$request->setCaptureFile('/path/to/save/capture/file.jpg');
+
+/** 
+ * @see JonnyW\PhantomJs\Message\Response 
+ **/
+$response = $client->getMessageFactory()->createResponse();
+
+// Send the request
+$client->send($request, $response);
+
+
+```
+
+For more detailed examples see the [examples](http://jonnnnyw.github.io/php-phantomjs/) section, otherwise to create your own custom scripts check out the [advanced](http://jonnnnyw.github.io/php-phantomjs/) documentation.

+ 98 - 0
doc/intro.rst

@@ -0,0 +1,98 @@
+Introduction
+=============
+
+PHP PhantomJS is a flexible PHP library to load pages through the PhantomJS 
+headless browser and return the page response. It is handy for testing
+websites that demand javascript support and also supports screen captures.
+
+Feature List
+---------------------
+
+* Load webpages through the PhantomJS headless browser
+* View detailed response data including page content, headers, status code etc.
+* Handle redirects
+* View javascript console errors
+* View detailed PhantomJS debuged information
+* Save screen captures to local disk 
+* Define screen capture x, y, width and height parameters
+* Delay page rendering for a specified time
+* Execute PhantomJS with command line options
+* Easily build and run custom PhantomJS scripts
+
+Prerequisites
+---------------------
+
+PHP PhantomJS requires PHP **5.3.0** or greater to run.
+
+Installation
+---------------------
+
+It is recommended that you use Composer to install PHP PhantomJS:
+
+```xml
+composer require "jonnyw/php-phantomjs:3.*"
+```
+
+If you would like to use another installation method or would like to see more detailed installation instruction, see the [installation](http://jonnnnyw.github.io/php-phantomjs/installation.html) documentation.
+
+
+Basic Usage
+---------------------
+
+The following illustrates how to make a basic GET request and output the page content:
+
+```php
+<?php
+
+use JonnyW\PhantomJs\Client;
+
+$client = Client::getInstance();
+
+/** 
+ * @see JonnyW\PhantomJs\Message\Request 
+ **/
+$request = $client->getMessageFactory()
+    ->createRequest('http://google.com', 'GET');
+
+/** 
+ * @see JonnyW\PhantomJs\Message\Response 
+ **/
+$response = $client->getMessageFactory()->createResponse();
+
+// Send the request
+$client->send($request, $response);
+
+if($response->getStatus() === 200) {
+
+    // Dump the requested page content
+    echo $response->getContent();
+}
+```
+
+And if you would like to save a screen capture to local disk:
+
+```php
+<?php
+
+use JonnyW\PhantomJs\Client;
+
+$client = Client::getInstance();
+
+/** 
+ * @see JonnyW\PhantomJs\Message\Request 
+ **/
+$request = $client->getMessageFactory()->createCaptureRequest('http://google.com', 'GET');
+$request->setCaptureFile('/path/to/save/capture/file.jpg');
+
+/** 
+ * @see JonnyW\PhantomJs\Message\Response 
+ **/
+$response = $client->getMessageFactory()->createResponse();
+
+// Send the request
+$client->send($request, $response);
+
+
+```
+
+For more detailed examples see the [examples](http://jonnnnyw.github.io/php-phantomjs/examples.html) section, or to create your own custom scripts check out the [advanced](http://jonnnnyw.github.io/php-phantomjs/advanced.html) documentation.

+ 15 - 0
examples/basic-request.php

@@ -0,0 +1,15 @@
+<?php
+
+use JonnyW\PhantomJs\Client;
+
+$client = Client::getInstance();
+
+$request  = $client->getMessageFactory()->createRequest();
+$response = $client->getMessageFactory()->createResponse();
+
+$request->setMethod('GET');
+$request->setUrl('http://google.com');
+
+$client->send($request, $response);
+
+var_dump($response);

+ 29 - 0
examples/capture-request.php

@@ -0,0 +1,29 @@
+<?php
+
+use JonnyW\PhantomJs\Client;
+
+$client = Client::getInstance();
+
+$request  = $client->getMessageFactory()->createCaptureRequest();
+$response = $client->getMessageFactory()->createResponse();
+
+$top    = 10;
+$left   = 10;
+$width  = 200;
+$height = 400;
+
+$request->setMethod('GET');
+$request->setUrl('http://google.com');
+$request->setCaptureFile(sprintf('%s/file.jpg', sys_get_temp_dir()));
+$request->setCaptureDimensions($width, $height, $top, $left);
+
+$client->send($request, $response);
+
+var_dump($response);
+
+// If the capture dimensions were applied
+// to the request, you will see an information
+// notice in the debug log. This is useful for
+// debugging captures and will always be present,
+// even if debug mode is disabled.
+var_dump($client->getLog());

+ 40 - 0
examples/custom-script.php

@@ -0,0 +1,40 @@
+<?php
+
+use JonnyW\PhantomJs\Client;
+use JonnyW\PhantomJs\DependencyInjection\ServiceContainer;
+
+// Create a file with a custom name 
+// e.g. custom_procedure.proc and save it 
+// somewhere. Set the parameters below to
+// the name of your file and the location of
+// your file. You can have many files in the 
+// same location and you only need to create
+// 1 procedure loader with the path to your
+// files. The only restriction is the extension
+// your files must be .proc
+
+$fileName = 'custom_procedure';
+$filePath = '/path/to/your/procedure/';
+
+$serviceContainer = ServiceContainer::getInstance();
+
+$procedureLoaderFactory = $serviceContainer->get('procedure_loader_factory');
+$procedureLoader        = $procedureLoaderFactory->createProcedureLoader($filePath); // Set the path to your custom procedure(s)
+
+$client = Client::getInstance();
+$client->getProcedureLoader()->addLoader($procedureLoader); // Add new loader with path to you procedures to the chain loader
+
+$request  = $client->getMessageFactory()->createRequest();
+$response = $client->getMessageFactory()->createResponse();
+
+$request->setType($fileName); // Set the request type to the name of your procedure you want to execute for this request
+
+$client->send($request, $response);
+
+var_dump($response);
+
+// If your debug log contains 'SyntaxError: Parse error'
+// then your custom procedure has a javascript error. Try 
+// setting $client->debug(true) before making your request
+// to get more information about your error.
+var_dump($client->getLog());

+ 19 - 0
examples/debug-request.php

@@ -0,0 +1,19 @@
+<?php
+
+use JonnyW\PhantomJs\Client;
+
+$client = Client::getInstance();
+$client->debug(true); // Set debug flag
+
+$request  = $client->getMessageFactory()->createRequest();
+$response = $client->getMessageFactory()->createResponse();
+
+$request->setMethod('GET');
+$request->setUrl('http://google.com');
+
+$client->send($request, $response);
+
+// The PhantomJS executable log. Will contain 
+// any script parse errors, script info and 
+// anything else PhantomJS outputs in debug mode.
+var_dump($client->getLog()); 

+ 26 - 0
examples/delay-page-render.php

@@ -0,0 +1,26 @@
+<?php
+
+use JonnyW\PhantomJs\Client;
+
+$client = Client::getInstance();
+
+$request  = $client->getMessageFactory()->createCaptureRequest();
+$response = $client->getMessageFactory()->createResponse();
+
+$delay = 5; // Seconds
+
+$request->setMethod('GET');
+$request->setUrl('http://google.com');
+$request->setCaptureFile(sprintf('%s/file.jpg', sys_get_temp_dir()));
+$request->setDelay($delay);
+
+$client->send($request, $response);
+
+var_dump($response);
+
+// A debug info notice will be written to
+// the log when the page render delay starts 
+// and when the page render executes. This is 
+// useful for debugging page render delay and 
+// will always be present, even if debug is disabled.
+var_dump($client->getLog());

+ 19 - 0
examples/javascript-console.php

@@ -0,0 +1,19 @@
+<?php
+
+use JonnyW\PhantomJs\Client;
+
+$client = Client::getInstance();
+
+$request  = $client->getMessageFactory()->createRequest();
+$response = $client->getMessageFactory()->createResponse();
+
+$request->setMethod('GET');
+$request->setUrl('http://google.com');
+
+$client->send($request, $response);
+
+// Any javascript errors that show up in 
+// the browser console will appear in 
+// response console data along with stack 
+// trace. console.log() data will not be present.
+var_dump($response->getConsole()); 

+ 21 - 0
examples/post-request.php

@@ -0,0 +1,21 @@
+<?php
+
+use JonnyW\PhantomJs\Client;
+
+$client = Client::getInstance();
+
+$request  = $client->getMessageFactory()->createRequest();
+$response = $client->getMessageFactory()->createResponse();
+
+$data = array(
+    'param1' => 'Param 1',
+    'param2' => 'Param 2'
+);
+
+$request->setMethod('POST');
+$request->setUrl('http://google.com');
+$request->setRequestData($data); // Set post data
+
+$client->send($request, $response);
+
+var_dump($response);

+ 5 - 2
phpunit.xml.dist

@@ -1,14 +1,17 @@
 <?xml version="1.0" encoding="UTF-8"?>
 
-<phpunit bootstrap="./test/bootstrap.php" colors="true">
+<phpunit bootstrap="./src/JonnyW/PhantomJs/Tests/bootstrap.php" colors="true">
     <testsuites>
         <testsuite name="PhantomJS Test Suite">
-            <directory suffix="Test.php">./test/</directory>
+            <directory suffix="Test.php">./src/JonnyW/PhantomJs/Tests/*</directory>
         </testsuite>
     </testsuites>
     <filter>
         <whitelist>
             <directory suffix=".php">./src/</directory>
+            <exclude>
+                <directory>./src/JonnyW/PhantomJs/Tests</directory>
+            </exclude>
         </whitelist>
     </filter>
 </phpunit>

+ 53 - 0
src/JonnyW/PhantomJs/Cache/CacheInterface.php

@@ -0,0 +1,53 @@
+<?php
+
+/*
+ * This file is part of the php-phantomjs.
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+namespace JonnyW\PhantomJs\Cache;
+
+/**
+ * PHP PhantomJs
+ *
+ * @author Jon Wenmoth <contact@jonnyw.me>
+ */
+interface CacheInterface
+{
+    /**
+     * Write data to storage.
+     *
+     * @access public
+     * @param  string $id
+     * @param  string $data
+     * @return string
+     */
+    public function save($id, $data);
+
+    /**
+     * Fetch data from file.
+     *
+     * @access public
+     * @return void
+     */
+    public function fetch($id);
+
+    /**
+     * Delete data from storage.
+     *
+     * @access public
+     * @param  string $id
+     * @return void
+     */
+    public function delete($id);
+
+    /**
+     * Data exists in storage.
+     *
+     * @access public
+     * @param  string  $id
+     * @return boolean
+     */
+    public function exists($id);
+}

+ 183 - 0
src/JonnyW/PhantomJs/Cache/FileCache.php

@@ -0,0 +1,183 @@
+<?php
+
+/*
+ * This file is part of the php-phantomjs.
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+namespace JonnyW\PhantomJs\Cache;
+
+use JonnyW\PhantomJs\Exception\NotWritableException;
+use JonnyW\PhantomJs\Exception\NotExistsException;
+
+/**
+ * PHP PhantomJs
+ *
+ * @author Jon Wenmoth <contact@jonnyw.me>
+ */
+class FileCache implements CacheInterface
+{
+    /**
+     * Default write directory
+     *
+     * @var string
+     * @access protected
+     */
+    protected $directory;
+
+    /**
+     * Default write extension
+     *
+     * @var string
+     * @access protected
+     */
+    protected $extension;
+
+    /**
+     * Internal constructor.
+     *
+     * @access public
+     * @param  string $directory
+     * @param  string $extension
+     * @return void
+     */
+    public function __construct($directory, $extension)
+    {
+        $this->directory = rtrim($directory, DIRECTORY_SEPARATOR);
+        $this->extension = $extension;
+    }
+
+    /**
+     * Write data to storage.
+     *
+     * @access public
+     * @param  string                                           $id
+     * @param  string                                           $data
+     * @return string
+     * @throws \JonnyW\PhantomJs\Exception\NotWritableException
+     */
+    public function save($id, $data)
+    {
+        $file = $this->getFilename($id);
+
+        if (!$this->isWritable($file)) {
+            throw new NotWritableException(sprintf('File could not be written to system as target is not writable: %s', $file));
+        }
+
+        if ($this->writeData($file, $data) === false) {
+
+            $this->delete($file);
+
+            throw new NotWritableException(sprintf('Data could not be written to file on system. Please make sure that file is writeable: %s', $file));
+        }
+
+        return $file;
+    }
+
+    /**
+     * Fetch data from file.
+     *
+     * @access public
+     * @return void
+     */
+    public function fetch($id)
+    {
+        $file = $this->getFilename($id);
+
+        if (!$this->exists($id)) {
+            throw new NotExistsException(sprintf('Could not fetch data from file as file does not exist: %s', $file));
+        }
+
+        return $this->readData($file);
+    }
+
+    /**
+     * Delete data from storage.
+     *
+     * @access public
+     * @param  string $id
+     * @return void
+     */
+    public function delete($id)
+    {
+        if ($this->exists($id)) {
+            unlink($this->getFilename($id));
+        }
+    }
+
+    /**
+     * Data exists in storage.
+     *
+     * @access public
+     * @param  string  $id
+     * @return boolean
+     */
+    public function exists($id)
+    {
+        return (bool) (file_exists($this->getFilename($id)));
+    }
+
+    /**
+     * Is data writeable.
+     *
+     * @access protected
+     * @param  string  $id
+     * @return boolean
+     */
+    protected function isWritable($file)
+    {
+        return (bool) ((file_exists($file) && is_writable($file)) || (!file_exists($file) && is_writable(dirname($file))));
+    }
+
+    /**
+     * Write data to file.
+     *
+     * @access protected
+     * @param  string  $file
+     * @param  string  $data
+     * @return boolean
+     */
+    protected function writeData($file, $data)
+    {
+        return file_put_contents($file, $data);
+    }
+
+    /**
+     * Read data from file.
+     *
+     * @access protected
+     * @param  string $file
+     * @return mixed
+     */
+    protected function readData($file)
+    {
+        return file_get_contents($file);
+    }
+
+    /**
+     * Get filename
+     *
+     * @access protected
+     * @param  string $id
+     * @return void
+     */
+    protected function getFileName($id)
+    {
+        if (is_dir($id)) {
+            return sprintf('%1$s/%2$s.%3$s', rtrim($id, DIRECTORY_SEPARATOR), uniqid(), $this->extension);
+        }
+
+        $dirName = dirname($id);
+
+        if (!file_exists($id) && $dirName === '.') {
+             return sprintf('%1$s/%2$s', $this->directory, $id);
+        }
+
+        if (!file_exists($id) && !is_writable($dirName)) {
+            throw new NotWritableException(sprintf('File could not be written to system as target is not writable: %s', $id));
+        }
+
+        return $id;
+    }
+}

+ 187 - 217
src/JonnyW/PhantomJs/Client.php

@@ -8,13 +8,12 @@
  */
 namespace JonnyW\PhantomJs;
 
-use JonnyW\PhantomJs\Exception\NoPhantomJsException;
-use JonnyW\PhantomJs\Exception\CommandFailedException;
-use JonnyW\PhantomJs\Exception\NotWriteableException;
-use JonnyW\PhantomJs\Message\FactoryInterface;
-use JonnyW\PhantomJs\Message\Factory;
+use JonnyW\PhantomJs\Exception\InvalidExecutableException;
+use JonnyW\PhantomJs\Procedure\ProcedureLoaderInterface;
+use JonnyW\PhantomJs\Message\MessageFactoryInterface;
 use JonnyW\PhantomJs\Message\RequestInterface;
 use JonnyW\PhantomJs\Message\ResponseInterface;
+use JonnyW\PhantomJs\DependencyInjection\ServiceContainer;
 
 /**
  * PHP PhantomJs
@@ -27,57 +26,99 @@ class Client implements ClientInterface
      * Client instance
      *
      * @var \JonnyW\PhantomJs\ClientInterface
+     * @access private
      */
     private static $instance;
 
+    /**
+     * Procedure loader instance
+     *
+     * @var \JonnyW\PhantomJs\Procedure\ProcedureLoaderInterface
+     * @access protected
+     */
+    protected $procedureLoader;
+
     /**
      * Message factory instance
      *
-     * @var \JonnyW\PhantomJs\Message\FactoryInterface
+     * @var \JonnyW\PhantomJs\Message\MessageFactoryInterface
+     * @access protected
      */
-    protected $factory;
+    protected $messageFactory;
 
     /**
-     * Path to phantomJS executable
+     * Path to PhantomJs executable
      *
      * @var string
+     * @access protected
      */
-    protected $phantomJS;
+    protected $phantomJs;
 
     /**
-     * Request timeout period
+     * Path to PhantomJs loader executable
      *
-     * @var int
+     * @var string
+     * @access protected
      */
-    protected $timeout;
+    protected $phantomLoader;
+
+    /**
+     * Debug.
+     *
+     * @var boolean
+     * @access protected
+     */
+    protected $debug;
+
+    /**
+     * Log info
+     *
+     * @var array
+     * @access protected
+     */
+    protected $log;
+
+    /**
+     * PhantomJs run options
+     *
+     * @var mixed
+     * @access protected
+     */
+    protected $options;
 
     /**
      * Internal constructor
      *
-     * @param  \JonnyW\PhantomJs\Message\FactoryInterface $factory
+     * @access public
+     * @param  \JonnyW\PhantomJs\Procedure\ProcedureLoaderInterface $procedureLoader
+     * @param  \JonnyW\PhantomJs\Message\MessageFactoryInterface    $messageFactory
      * @return void
      */
-    public function __construct(FactoryInterface $factory = null)
+    public function __construct(ProcedureLoaderInterface $procedureLoader, MessageFactoryInterface $messageFactory)
     {
-        if (!$factory instanceof FactoryInterface) {
-            $factory = Factory::getInstance();
-        }
-
-        $this->factory   = $factory;
-        $this->phantomJS = 'bin/phantomjs';
-        $this->timeout   = 5000;
+        $this->procedureLoader = $procedureLoader;
+        $this->messageFactory  = $messageFactory;
+        $this->phantomJs       = 'bin/phantomjs';
+        $this->phantomLoader   = 'bin/phantomloader';
+        $this->options         = array();
     }
 
     /**
      * Get singleton instance
      *
-     * @param  \JonnyW\PhantomJs\Message\FactoryInterface $factory
+     * @access public
      * @return \JonnyW\PhantomJs\Client
      */
-    public static function getInstance(FactoryInterface $factory = null)
+    public static function getInstance()
     {
         if (!self::$instance instanceof ClientInterface) {
-            self::$instance = new Client($factory);
+
+            $serviceContainer = ServiceContainer::getInstance();
+
+            self::$instance = new Client(
+                $serviceContainer->get('procedure_loader'),
+                $serviceContainer->get('message_factory')
+            );
         }
 
         return self::$instance;
@@ -86,299 +127,228 @@ class Client implements ClientInterface
     /**
      * Get message factory instance
      *
+     * @access public
      * @return \JonnyW\PhantomJs\Message\FactoryInterface
      */
     public function getMessageFactory()
     {
-        return $this->factory;
+        return $this->messageFactory;
     }
 
     /**
-     * Send request
+     * Get procedure loader instance
      *
-     * @param  \JonnyW\PhantomJs\Message\RequestInterface  $request
-     * @param  \JonnyW\PhantomJs\Message\ResponseInterface $response
-     * @param  string                                      $file
-     * @return \JonnyW\PhantomJs\Message\ResponseInterface
+     * @access public
+     * @return \JonnyW\PhantomJs\Procedure\ProcedureLoaderInterface
      */
-    public function send(RequestInterface $request, ResponseInterface $response, $file = null)
+    public function getProcedureLoader()
     {
-        if (!is_null($file)) {
-            return $this->capture($request, $response, $file);
-        }
-
-        return $this->open($request, $response);
+        return $this->procedureLoader;
     }
 
     /**
-     * Open page
+     * Send request
      *
+     * @access public
      * @param  \JonnyW\PhantomJs\Message\RequestInterface  $request
      * @param  \JonnyW\PhantomJs\Message\ResponseInterface $response
      * @return \JonnyW\PhantomJs\Message\ResponseInterface
      */
-    public function open(RequestInterface $request, ResponseInterface $response, $delay = 0)
+    public function send(RequestInterface $request, ResponseInterface $response)
     {
-        if ($delay) {
-            $cmd = sprintf($this->openCmdWithDelay, $delay);
-        } else {
-            $cmd = $this->openCmd;
-        }
+        $this->clearLog();
+
+        $procedure = $this->procedureLoader->load($request->getType());
+        $procedure->run($this, $request, $response);
 
-        return $this->request($request, $response, $cmd);
+        return $response;
     }
 
     /**
-     * Screen capture
+     * Get PhantomJs run command with
+     * loader and run options.
      *
-     * @param  \JonnyW\PhantomJs\Message\RequestInterface  $request
-     * @param  \JonnyW\PhantomJs\Message\ResponseInterface $response
-     * @param  string                                      $file
-     * @return \JonnyW\PhantomJs\Message\ResponseInterface
+     * @access public
+     * @return string
      */
-    public function capture(RequestInterface $request, ResponseInterface $response, $file)
+    public function getCommand()
     {
-        if (!is_writable(dirname($file))) {
-            throw new NotWriteableException(sprintf('Path is not writeable by PhantomJs: %s', $file));
-        }
+        $phantomJs     = $this->getPhantomJs();
+        $phantomLoader = $this->getPhantomLoader();
 
-        $cmd = sprintf($this->captureCmd, $file);
+        $this->validateExecutable($phantomJs);
+        $this->validateExecutable($phantomLoader);
 
-        return $this->request($request, $response, $cmd);
+        $options = $this->getOptions();
+
+        if ($this->debug) {
+            array_push($options, '--debug=true');
+        }
+
+        return sprintf('%s %s %s', $phantomJs, implode(' ', $options), $phantomLoader);
     }
 
     /**
-     * Set new PhantomJs path
+     * Set new PhantomJs executable path.
      *
+     * @access public
      * @param  string                   $path
      * @return \JonnyW\PhantomJs\Client
      */
     public function setPhantomJs($path)
     {
-        if (!file_exists($path) || !is_executable($path)) {
-            throw new NoPhantomJsException(sprintf('PhantomJs file does not exist or is not executable: %s', $path));
-        }
+        $this->validateExecutable($path);
 
-        $this->phantomJS = $path;
+        $this->phantomJs = $path;
 
         return $this;
     }
 
     /**
-     * Set timeout period (in milliseconds)
+     * Get PhantomJs executable path.
      *
-     * @param  int                      $period
-     * @return \JonnyW\PhantomJs\Client
+     * @access public
+     * @return string
      */
-    public function setTimeout($period)
+    public function getPhantomJs()
     {
-        $this->timeout = $period;
-
-        return $this;
+        return $this->phantomJs;
     }
 
     /**
-     * Make PhantomJS request
+     * Set PhantomJs loader executable path.
      *
-     * @param  \JonnyW\PhantomJs\Message\RequestInterface  $request
-     * @param  \JonnyW\PhantomJs\Message\ResponseInterface $response
-     * @param  string                                      $cmd
-     * @return \JonnyW\PhantomJs\Message\ResponseInterface
+     * @access public
+     * @param  string                   $path
+     * @return \JonnyW\PhantomJs\Client
      */
-    protected function request(RequestInterface $request, ResponseInterface $response, $cmd)
+    public function setPhantomLoader($path)
     {
+        $this->validateExecutable($path);
 
-        // Validate PhantomJS executable
-        if (!file_exists($this->phantomJS) || !is_executable($this->phantomJS)) {
-            throw new NoPhantomJsException(sprintf('PhantomJs file does not exist or is not executable: %s', $this->phantomJS));
-        }
-
-        try {
-
-            $script = false;
-
-            $data = sprintf(
-                $this->wrapper,
-                $request->getHeaders('json'),
-                $this->timeout,
-                $request->getUrl(),
-                $request->getMethod(),
-                $request->getBody(),
-                $cmd
-            );
-
-            $script = $this->writeScript($data);
-            $cmd  = escapeshellcmd(sprintf("%s %s", $this->phantomJS, $script));
-
-            $result = shell_exec($cmd);
-            $result = $this->parse($result);
-
-            $this->removeScript($script);
+        $this->phantomLoader = $path;
 
-            $response->setData($result);
-        } catch (NotWriteableException $e) {
-            throw $e;
-        } catch (\Exception $e) {
-
-            $this->removeScript($script);
-
-            throw new CommandFailedException(sprintf('Error when executing PhantomJs command: %s - %s', $cmd, $e->getMessage()));
-        }
-
-        return $response;
+        return $this;
     }
 
     /**
-     * Write temporary script file and
-     * return path to file
+     * Get PhantomJs loader executable path.
      *
-     * @param  string $data
+     * @access public
      * @return string
      */
-    protected function writeScript($data)
+    public function getPhantomLoader()
     {
-        $file = tempnam('/tmp', 'phantomjs');
-
-        // Could not create tmp file
-        if (!$file || !is_writable($file)) {
-            throw new NotWriteableException('Could not create tmp file on system. Please check your tmp directory and make sure it is writeable.');
-        }
-
-        // Could not write script data to tmp file
-        if (file_put_contents($file, $data) === false) {
-
-            $this->removeScript($file);
-
-            throw new NotWriteableException(sprintf('Could not write data to tmp file: %s. Please check your tmp directory and make sure it is writeable.', $file));
-        }
-
-        return $file;
+        return $this->phantomLoader;
     }
 
     /**
-     * Remove temporary script file
+     * Set PhantomJs run options.
      *
-     * @param  string|boolean           $file
+     * @access public
+     * @param  array                    $options
      * @return \JonnyW\PhantomJs\Client
      */
-    protected function removeScript($file)
+    public function setOptions(array $options)
     {
-        if (is_string($file) && file_exists($file)) {
-            unlink($file);
-        }
+        $this->options = $options;
 
         return $this;
     }
 
     /**
-     * If data from JSON string format
-     * and return array
+     * Get PhantomJs run options.
      *
-     * @param  string $data
+     * @access public
      * @return array
      */
-    protected function parse($data)
+    public function getOptions()
     {
-        // Data is invalid
-        if ($data === null || !is_string($data)) {
-            return array();
-        }
+        return (array) $this->options;
+    }
 
-        // Not a JSON string
-        if (substr($data, 0, 1) !== '{') {
-            return array();
+    /**
+     * Add single PhantomJs run option.
+     *
+     * @access public
+     * @param  string                   $option
+     * @return \JonnyW\PhantomJs\Client
+     */
+    public function addOption($option)
+    {
+        if (!in_array($option, $this->options)) {
+            $this->options[] = $option;
         }
 
-        // Return decoded JSON string
-        return (array) json_decode($data, true);
+        return $this;
     }
 
     /**
-     * PhantomJs base wrapper
+     * Debug.
      *
-     * @var string
+     * @access public
+     * @param  boolean                  $doDebug
+     * @return \JonnyW\PhantomJs\Client
      */
-    protected $wrapper = <<<EOF
-
-    var page = require('webpage').create(),
-        response = {},
-        headers = %1\$s;
-
-    page.settings.resourceTimeout = %2\$s;
-    page.onResourceTimeout = function (e) {
-        response 		= e;
-        response.status = e.errorCode;
-    };
-
-    page.onResourceReceived = function (r) {
-        if(!response.status) response = r;
-    };
-
-    page.customHeaders = headers ? headers : {};
-
-    page.open('%3\$s', '%4\$s', '%5\$s', function (status) {
+    public function debug($doDebug)
+    {
+        $this->debug = $doDebug;
 
-        if (status === 'success') {
-            %6\$s
-        } else {
-            console.log(JSON.stringify(response, undefined, 4));
-            phantom.exit();
-        }
-    });
-EOF;
+        return $this;
+    }
 
     /**
-     * PhantomJs screen capture
-     * command template
+     * Set log info.
      *
-     * @var string
+     * @access public
+     * @param  string                   $info
+     * @return \JonnyW\PhantomJs\Client
      */
-    protected $captureCmd = <<<EOF
-
-            page.render('%1\$s');
-
-            response.content = page.evaluate(function () {
-                return document.getElementsByTagName('html')[0].innerHTML
-            });
+    public function setLog($info)
+    {
+        $this->log = $info;
 
-            console.log(JSON.stringify(response, undefined, 4));
-            phantom.exit();
-EOF;
+        return $this;
+    }
 
     /**
-     * PhantomJs page open
-     * command template
+     * Get log info.
      *
-     * @var string
+     * @access public
+     * @return string
      */
-    protected $openCmd = <<<EOF
-
-            response.content = page.evaluate(function () {
-                return document.getElementsByTagName('html')[0].innerHTML
-            });
-
-            console.log(JSON.stringify(response, undefined, 4));
-            phantom.exit();
-EOF;
+    public function getLog()
+    {
+        return $this->log;
+    }
 
     /**
-     * PhantomJs page open
-     * command template with
-     * delay
+     * Clear log info.
      *
-     * @var string
+     * @access public
+     * @return \JonnyW\PhantomJs\Client
      */
-    protected $openCmdWithDelay = <<<EOF
-
-        window.setTimeout(function () {
+    public function clearLog()
+    {
+        $this->log = '';
 
-            response.content = page.evaluate(function () {
-                return document.getElementsByTagName('html')[0].innerHTML
-            });
+        return $this;
+    }
 
-            console.log(JSON.stringify(response, undefined, 4));
-            phantom.exit();
+    /**
+     * Validate execuable file.
+     *
+     * @access private
+     * @param  string                                                 $file
+     * @return boolean
+     * @throws \JonnyW\PhantomJs\Exception\InvalidExecutableException
+     */
+    private function validateExecutable($file)
+    {
+        if (!file_exists($file) || !is_executable($file)) {
+            throw new InvalidExecutableException(sprintf('File does not exist or is not executable: %s', $file));
+        }
 
-        }, %s);
-EOF;
+        return true;
+    }
 }

+ 95 - 20
src/JonnyW/PhantomJs/ClientInterface.php

@@ -19,39 +19,114 @@ use JonnyW\PhantomJs\Message\ResponseInterface;
 interface ClientInterface
 {
     /**
-     * Send request
+     * Get singleton instance
      *
-     * @param  \JonnyW\PhantomJs\Message\RequestInterface  $request
-     * @param  \JonnyW\PhantomJs\Message\ResponseInterface $response
-     * @param  string                                      $file
-     * @return \JonnyW\PhantomJs\Message\ResponseInterface
+     * @access public
+     * @return \JonnyW\PhantomJs\ClientInterface
      */
-    public function send(RequestInterface $request, ResponseInterface $response, $file = null);
+    public static function getInstance();
 
     /**
-     * Open page
+     * Get message factory instance
      *
-     * @param  \JonnyW\PhantomJs\Message\RequestInterface  $request
-     * @param  \JonnyW\PhantomJs\Message\ResponseInterface $response
-     * @return \JonnyW\PhantomJs\Message\ResponseInterface
+     * @access public
+     * @return \JonnyW\PhantomJs\Message\FactoryInterface
      */
-    public function open(RequestInterface $request, ResponseInterface $response);
+    public function getMessageFactory();
 
     /**
-     * Screen capture
+     * Send request
      *
-     * @param  \JonnyW\PhantomJs\Message\RequestInterface  $request
-     * @param  \JonnyW\PhantomJs\Message\ResponseInterface $response
-     * @param  string                                      $file
-     * @return \JonnyW\PhantomJs\Message\ResponseInterface
+     * @access public
+     * @param \JonnyW\PhantomJs\Message\RequestInterface  $request
+     * @param \JonnyW\PhantomJs\Message\ResponseInterface $response
      */
-    public function capture(RequestInterface $request, ResponseInterface $response, $file);
+    public function send(RequestInterface $request, ResponseInterface $response);
 
     /**
-     * Set new PhantomJs path
+     * Set new PhantomJs executable path.
      *
-     * @param  string                   $path
-     * @return \JonnyW\PhantomJs\Client
+     * @access public
+     * @param string $path
      */
     public function setPhantomJs($path);
+
+    /**
+     * Get PhantomJs executable path.
+     *
+     * @access public
+     * @return string
+     */
+    public function getPhantomJs();
+
+        /**
+     * Set PhantomJs loader executable path.
+     *
+     * @access public
+     * @param string $path
+     */
+    public function setPhantomLoader($path);
+
+    /**
+     * Get PhantomJs loader executable path.
+     *
+     * @access public
+     * @return string
+     */
+    public function getPhantomLoader();
+
+    /**
+     * Set PhantomJs run options.
+     *
+     * @access public
+     * @param array $options
+     */
+    public function setOptions(array $options);
+
+    /**
+     * Get PhantomJs run options.
+     *
+     * @access public
+     * @return array
+     */
+    public function getOptions();
+
+    /**
+     * Add single PhantomJs run option.
+     *
+     * @access public
+     * @param string $option
+     */
+    public function addOption($option);
+
+    /**
+     * Debug.
+     *
+     * @access public
+     * @param boolean $doDebug
+     */
+    public function debug($doDebug);
+
+    /**
+     * Set log info.
+     *
+     * @access public
+     * @param string $info
+     */
+    public function setLog($info);
+
+    /**
+     * Get log info.
+     *
+     * @access public
+     * @return string
+     */
+    public function getLog();
+
+    /**
+     * Clear log info.
+     *
+     * @access public
+     */
+    public function clearLog();
 }

+ 62 - 0
src/JonnyW/PhantomJs/DependencyInjection/ServiceContainer.php

@@ -0,0 +1,62 @@
+<?php
+
+/*
+ * This file is part of the php-phantomjs.
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+namespace JonnyW\PhantomJs\DependencyInjection;
+
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
+use Symfony\Component\Config\FileLocator;
+
+/**
+ * PHP PhantomJs
+ *
+ * @author Jon Wenmoth <contact@jonnyw.me>
+ */
+class ServiceContainer extends ContainerBuilder
+{
+    /**
+     * Service container instance
+     *
+     * @var \JonnyW\PhantomJs\ServiceContainer
+     * @access private
+     */
+    private static $instance;
+
+    /**
+     * Get singleton instance
+     *
+     * @access public
+     * @param  \JonnyW\PhantomJs\Message\FactoryInterface $messageFactory
+     * @return \JonnyW\PhantomJs\Client
+     */
+    public static function getInstance()
+    {
+        if (!self::$instance instanceof ServiceContainer) {
+
+            self::$instance = new ServiceContainer();
+            self::$instance->load();
+        }
+
+        return self::$instance;
+    }
+
+    /**
+     * Load service container.
+     *
+     * @access public
+     * @return void
+     */
+    public function load()
+    {
+        $loader = new YamlFileLoader($this, new FileLocator(__DIR__.'/../Resources/config'));
+        $loader->load('config.yml');
+        $loader->load('services.yml');
+
+        $this->setParameter('phantomjs.resource_dir', __DIR__.'/../Resources');
+    }
+}

+ 19 - 0
src/JonnyW/PhantomJs/Exception/InvalidExecutableException.php

@@ -0,0 +1,19 @@
+<?php
+
+/*
+ * This file is part of the php-phantomjs.
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+namespace JonnyW\PhantomJs\Exception;
+
+/**
+ * PHP PhantomJs
+ *
+ * @author Jon Wenmoth <contact@jonnyw.me>
+ */
+class InvalidExecutableException extends PhantomJsException
+{
+
+}

+ 1 - 1
src/JonnyW/PhantomJs/Exception/InvalidMethodException.php

@@ -13,7 +13,7 @@ namespace JonnyW\PhantomJs\Exception;
  *
  * @author Jon Wenmoth <contact@jonnyw.me>
  */
-class InvalidMethodException extends \Exception
+class InvalidMethodException extends PhantomJsException
 {
 
 }

+ 1 - 1
src/JonnyW/PhantomJs/Exception/InvalidUrlException.php

@@ -13,7 +13,7 @@ namespace JonnyW\PhantomJs\Exception;
  *
  * @author Jon Wenmoth <contact@jonnyw.me>
  */
-class InvalidUrlException extends \Exception
+class InvalidUrlException extends PhantomJsException
 {
 
 }

+ 1 - 1
src/JonnyW/PhantomJs/Exception/CommandFailedException.php → src/JonnyW/PhantomJs/Exception/NotExistsException.php

@@ -13,7 +13,7 @@ namespace JonnyW\PhantomJs\Exception;
  *
  * @author Jon Wenmoth <contact@jonnyw.me>
  */
-class CommandFailedException extends \Exception
+class NotExistsException extends PhantomJsException
 {
 
 }

+ 1 - 1
src/JonnyW/PhantomJs/Exception/NotWriteableException.php → src/JonnyW/PhantomJs/Exception/NotWritableException.php

@@ -13,7 +13,7 @@ namespace JonnyW\PhantomJs\Exception;
  *
  * @author Jon Wenmoth <contact@jonnyw.me>
  */
-class NotWriteableException extends \Exception
+class NotWritableException extends PhantomJsException
 {
 
 }

+ 1 - 1
src/JonnyW/PhantomJs/Exception/NoPhantomJsException.php → src/JonnyW/PhantomJs/Exception/PhantomJsException.php

@@ -13,7 +13,7 @@ namespace JonnyW\PhantomJs\Exception;
  *
  * @author Jon Wenmoth <contact@jonnyw.me>
  */
-class NoPhantomJsException extends \Exception
+class PhantomJsException extends \Exception
 {
 
 }

+ 19 - 0
src/JonnyW/PhantomJs/Exception/ProcedureFailedException.php

@@ -0,0 +1,19 @@
+<?php
+
+/*
+ * This file is part of the php-phantomjs.
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+namespace JonnyW\PhantomJs\Exception;
+
+/**
+ * PHP PhantomJs
+ *
+ * @author Jon Wenmoth <contact@jonnyw.me>
+ */
+class ProcedureFailedException extends PhantomJsException
+{
+
+}

+ 352 - 0
src/JonnyW/PhantomJs/Message/AbstractRequest.php

@@ -0,0 +1,352 @@
+<?php
+
+/*
+ * This file is part of the php-phantomjs.
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace JonnyW\PhantomJs\Message;
+
+use JonnyW\PhantomJs\Exception\InvalidUrlException;
+use JonnyW\PhantomJs\Exception\InvalidMethodException;
+
+/**
+ * PHP PhantomJs
+ *
+ * @author Jon Wenmoth <contact@jonnyw.me>
+ */
+abstract class AbstractRequest implements RequestInterface
+{
+    /**
+     * Headers
+     *
+     * @var array
+     * @access protected
+     */
+    protected $headers;
+
+    /**
+     * Request data
+     *
+     * @var array
+     * @access protected
+     */
+    protected $data;
+
+    /**
+     * Request URL
+     *
+     * @var string
+     * @access protected
+     */
+    protected $url;
+
+    /**
+     * Request method
+     *
+     * @var string
+     * @access protected
+     */
+    protected $method;
+
+    /**
+     * Timeout period
+     *
+     * @var int
+     * @access protected
+     */
+    protected $timeout;
+
+    /**
+     * Page load delay time.
+     *
+     * @var int
+     * @access protected
+     */
+    protected $delay;
+
+    /**
+     * Internal constructor
+     *
+     * @access public
+     * @param  string $url     (default: null)
+     * @param  string $method  (default: RequestInterface::METHOD_GET)
+     * @param  int    $timeout (default: 5000)
+     * @return void
+     */
+    public function __construct($url = null, $method = RequestInterface::METHOD_GET, $timeout = 5000)
+    {
+        $this->headers        = array();
+        $this->data           = array();
+        $this->delay          = 0;
+
+        $this->setMethod($method);
+        $this->setTimeout($timeout);
+
+        if ($url) {
+            $this->setUrl($url);
+        }
+    }
+
+    /**
+     * Set request method
+     *
+     * @access public
+     * @param  string                                             $method
+     * @return \JonnyW\PhantomJs\Message\AbstractRequest
+     * @throws \JonnyW\PhantomJs\Exception\InvalidMethodException
+     */
+    public function setMethod($method)
+    {
+        $method     = strtoupper($method);
+        $reflection = new \ReflectionClass('\JonnyW\PhantomJs\Message\RequestInterface');
+
+        if (!$reflection->hasConstant('METHOD_' . $method)) {
+            throw new InvalidMethodException(sprintf('Invalid method provided: %s', $method));
+        }
+
+        $this->method = $method;
+
+        return $this;
+    }
+
+    /**
+     * Get request method
+     *
+     * @access public
+     * @return string
+     */
+    public function getMethod()
+    {
+        return $this->method;
+    }
+
+    /**
+     * Set timeout period
+     *
+     * @access public
+     * @param  int                                       $timeout
+     * @return \JonnyW\PhantomJs\Message\AbstractRequest
+     */
+    public function setTimeout($timeout)
+    {
+        $this->timeout = $timeout;
+
+        return $this;
+    }
+
+    /**
+     * Get timeout period
+     *
+     * @access public
+     * @return int
+     */
+    public function getTimeout()
+    {
+        return $this->timeout;
+    }
+
+    /**
+     * Set page load delay time (seconds).
+     *
+     * @access public
+     * @param  int                                       $delay
+     * @return \JonnyW\PhantomJs\Message\AbstractRequest
+     */
+    public function setDelay($delay)
+    {
+        $this->delay = (int) $delay;
+
+        return $this;
+    }
+
+    /**
+     * Get page load delay time (seconds).
+     *
+     * @access public
+     * @return int
+     */
+    public function getDelay()
+    {
+        return (int) $this->delay;
+    }
+
+    /**
+     * Set request URL
+     *
+     * @access public
+     * @param  string                                          $url
+     * @return \JonnyW\PhantomJs\Message\AbstractRequest
+     * @throws \JonnyW\PhantomJs\Exception\InvalidUrlException
+     */
+    public function setUrl($url)
+    {
+        if (!filter_var($url, FILTER_VALIDATE_URL, FILTER_FLAG_HOST_REQUIRED)) {
+            throw new InvalidUrlException(sprintf('Invalid URL provided: %s', $url));
+        }
+
+        $this->url = $url;
+
+        return $this;
+    }
+
+    /**
+     * Get request URL
+     *  - Assembles query string for GET
+     *  and HEAD requests
+     *
+     * @access public
+     * @return string
+     */
+    public function getUrl()
+    {
+        if (!in_array($this->getMethod(), array(RequestInterface::METHOD_GET, RequestInterface::METHOD_HEAD))) {
+            return $this->url;
+        }
+
+        $url = $this->url;
+
+        if (count($this->data)) {
+
+            $url .= false === strpos($url, '?') ? '?' : '&';
+            $url .= urldecode(http_build_query($this->data));
+        }
+
+        return $url;
+    }
+
+    /**
+     * Get content body
+     *  - Returns query string if not GET or HEAD
+     *
+     * @access public
+     * @return string
+     */
+    public function getBody()
+    {
+        if (in_array($this->getMethod(), array(RequestInterface::METHOD_GET, RequestInterface::METHOD_HEAD))) {
+            return '';
+        }
+
+        return urldecode(http_build_query($this->getRequestData()));
+    }
+
+    /**
+     * Set request data
+     *
+     * @access public
+     * @param  array                                     $data
+     * @return \JonnyW\PhantomJs\Message\AbstractRequest
+     */
+    public function setRequestData(array $data)
+    {
+        $this->data = $data;
+
+        return $this;
+    }
+
+    /**
+     * Get request data
+     *
+     * @access public
+     * @param  boolean $flat
+     * @return array
+     */
+    public function getRequestData($flat = true)
+    {
+        if ($flat) {
+            return $this->flattenData($this->data);
+        }
+
+        return $this->data;
+    }
+
+    /**
+     * Set headers
+     *
+     * @access public
+     * @param  array                                     $headers
+     * @return \JonnyW\PhantomJs\Message\AbstractRequest
+     */
+    public function setHeaders(array $headers)
+    {
+        $this->headers = $headers;
+    }
+
+    /**
+     * Add single header
+     *
+     * @access public
+     * @param  string                                    $header
+     * @param  string                                    $value
+     * @return \JonnyW\PhantomJs\Message\AbstractRequest
+     */
+    public function addHeader($header, $value)
+    {
+        $this->headers[$header] = $value;
+
+        return $this;
+    }
+
+    /**
+     * Merge headers with existing
+     *
+     * @access public
+     * @param  array                                     $headers
+     * @return \JonnyW\PhantomJs\Message\AbstractRequest
+     */
+    public function addHeaders(array $headers)
+    {
+        $this->headers = array_merge($this->headers, $headers);
+
+        return $this;
+    }
+
+    /**
+     * Get request headers
+     *
+     * @access public
+     * @param  string $format
+     * @return array
+     */
+    public function getHeaders($format = 'default')
+    {
+        if ($format === 'json') {
+            return json_encode($this->headers);
+        }
+
+        return $this->headers;
+    }
+
+    /**
+     * Flatten data into single
+     * dimensional array
+     *
+     * @access protected
+     * @param  array  $data
+     * @param  string $prefix
+     * @param  string $format
+     * @return array
+     */
+    protected function flattenData(array $data, $prefix  = '', $format = '%s')
+    {
+        $flat = array();
+
+        foreach ($data as $name => $value) {
+
+            $ref = $prefix . sprintf($format, $name);
+
+            if (is_array($value)) {
+
+                $flat += $this->flattenData($value, $ref, '[%s]');
+                continue;
+            }
+
+            $flat[$ref] = $value;
+        }
+
+        return $flat;
+    }
+}

+ 185 - 0
src/JonnyW/PhantomJs/Message/CaptureRequest.php

@@ -0,0 +1,185 @@
+<?php
+
+/*
+ * This file is part of the php-phantomjs.
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace JonnyW\PhantomJs\Message;
+
+use JonnyW\PhantomJs\Exception\NotWritableException;
+
+/**
+ * PHP PhantomJs
+ *
+ * @author Jon Wenmoth <contact@jonnyw.me>
+ */
+class CaptureRequest extends AbstractRequest
+    implements CaptureRequestInterface
+{
+    /**
+     * File to save capture.
+     *
+     * @var string
+     * @access protected
+     */
+    protected $captureFile;
+
+    /**
+     * Rect top
+     *
+     * @var int
+     * @access protected
+     */
+    protected $rectTop;
+
+    /**
+     * Rect left
+     *
+     * @var int
+     * @access protected
+     */
+    protected $rectLeft;
+
+    /**
+     * Rect width
+     *
+     * @var int
+     * @access protected
+     */
+    protected $rectWidth;
+
+    /**
+     * Rect height
+     *
+     * @var int
+     * @access protected
+     */
+    protected $rectHeight;
+
+    /**
+     * Internal constructor
+     *
+     * @access public
+     * @param  string $url     (default: null)
+     * @param  string $method  (default: RequestInterface::METHOD_GET)
+     * @param  int    $timeout (default: 5000)
+     * @return void
+     */
+    public function __construct($url = null, $method = RequestInterface::METHOD_GET, $timeout = 5000)
+    {
+        parent::__construct($url, $method, $timeout);
+
+        $this->rectTop    = 0;
+        $this->rectLeft   = 0;
+        $this->rectWidth  = 0;
+        $this->rectHeight = 0;
+    }
+
+    /**
+     * Get request type.
+     *
+     * @access public
+     * @return string
+     */
+    public function getType()
+    {
+        return RequestInterface::REQUEST_TYPE_CAPTURE;
+    }
+
+    /**
+     * Set viewport size.
+     *
+     * @access public
+     * @param  int                                       $width
+     * @param  int                                       $height
+     * @param  int                                       $top    (default: 0)
+     * @param  int                                       $left   (default: 0)
+     * @return \JonnyW\PhantomJs\Message\AbstractRequest
+     */
+    public function setCaptureDimensions($width, $height, $top = 0, $left = 0)
+    {
+        $this->rectWidth  = (int) $width;
+        $this->rectHeight = (int) $height;
+        $this->rectTop    = (int) $top;
+        $this->rectLeft   = (int) $left;
+
+        return $this;
+    }
+
+    /**
+     * Get rect top.
+     *
+     * @access public
+     * @return int
+     */
+    public function getRectTop()
+    {
+        return (int) $this->rectTop;
+    }
+
+    /**
+     * Get rect left.
+     *
+     * @access public
+     * @return int
+     */
+    public function getRectLeft()
+    {
+        return (int) $this->rectLeft;
+    }
+
+    /**
+     * Get rect width.
+     *
+     * @access public
+     * @return int
+     */
+    public function getRectWidth()
+    {
+        return (int) $this->rectWidth;
+    }
+
+    /**
+     * Get rect height.
+     *
+     * @access public
+     * @return int
+     */
+    public function getRectHeight()
+    {
+        return (int) $this->rectHeight;
+    }
+
+    /**
+     * Set file to save screen capture.
+     *
+     * @access public
+     * @param  string                                            $file
+     * @return \JonnyW\PhantomJs\Message\CaptureRequest
+     * @throws \JonnyW\PhantomJs\Exception\NotWriteableException
+     */
+    public function setCaptureFile($file)
+    {
+        if (!is_writable(dirname($file))) {
+            throw new NotWritableException(sprintf('Capture file is not writeable by PhantomJs: %s', $file));
+        }
+
+        $this->captureFile = $file;
+
+        return $this;
+    }
+
+    /**
+     * Get capture file.
+     *
+     * @access public
+     * @return string
+     */
+    public function getCaptureFile()
+    {
+        return $this->captureFile;
+    }
+}

+ 77 - 0
src/JonnyW/PhantomJs/Message/CaptureRequestInterface.php

@@ -0,0 +1,77 @@
+<?php
+
+/*
+ * This file is part of the php-phantomjs.
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace JonnyW\PhantomJs\Message;
+
+/**
+ * PHP PhantomJs
+ *
+ * @author Jon Wenmoth <contact@jonnyw.me>
+ */
+interface CaptureRequestInterface
+{
+    /**
+     * Set viewport size.
+     *
+     * @access public
+     * @param int $width
+     * @param int $height
+     * @param int $top    (default: 0)
+     * @param int $left   (default: 0)
+     */
+    public function setCaptureDimensions($width, $height, $top = 0, $left = 0);
+
+    /**
+     * Get rect top.
+     *
+     * @access public
+     * @return int
+     */
+    public function getRectTop();
+
+    /**
+     * Get rect left.
+     *
+     * @access public
+     * @return int
+     */
+    public function getRectLeft();
+
+    /**
+     * Get rect width.
+     *
+     * @access public
+     * @return int
+     */
+    public function getRectWidth();
+
+    /**
+     * Get rect height.
+     *
+     * @access public
+     * @return int
+     */
+    public function getRectHeight();
+
+    /**
+     * Set file to save screen capture.
+     *
+     * @access public
+     * @param string $file
+     */
+    public function setCaptureFile($file);
+
+    /**
+     * Get capture file.
+     *
+     * @access public
+     * @return string
+     */
+    public function getCaptureFile();
+}

+ 0 - 60
src/JonnyW/PhantomJs/Message/Factory.php

@@ -1,60 +0,0 @@
-<?php
-
-/*
- * This file is part of the php-phantomjs.
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-namespace JonnyW\PhantomJs\Message;
-
-/**
- * PHP PhantomJs
- *
- * @author Jon Wenmoth <contact@jonnyw.me>
- */
-class Factory implements FactoryInterface
-{
-    /**
-     * Client instance
-     *
-     * @var \JonnyW\PhantomJs\Message\FactoryInterface
-     */
-    private static $instance;
-
-    /**
-     * Get singleton instance
-     *
-     * @return \JonnyW\PhantomJs\Message\Factory
-     */
-    public static function getInstance()
-    {
-        if (!self::$instance instanceof FactoryInterface) {
-            self::$instance = new Factory();
-        }
-
-        return self::$instance;
-    }
-
-    /**
-     * Create request instance
-     *
-     * @param  string                            $method
-     * @param  string                            $url
-     * @return \JonnyW\PhantomJs\Message\Request
-     */
-    public function createRequest($method = RequestInterface::METHOD_GET, $url = null)
-    {
-        return new Request($method, $url);
-    }
-
-    /**
-     * Create response instance
-     *
-     * @return \JonnyW\PhantomJs\Message\Response
-     */
-    public function createResponse()
-    {
-        return new Response();
-    }
-}

+ 0 - 40
src/JonnyW/PhantomJs/Message/FactoryInterface.php

@@ -1,40 +0,0 @@
-<?php
-
-/*
- * This file is part of the php-phantomjs.
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-namespace JonnyW\PhantomJs\Message;
-
-/**
- * PHP PhantomJs
- *
- * @author Jon Wenmoth <contact@jonnyw.me>
- */
-interface FactoryInterface
-{
-    /**
-     * Get singleton instance
-     *
-     * @return Factory
-     */
-    public static function getInstance();
-
-    /**
-     * Create request instance
-     *
-     * @param  string                                     $url
-     * @param  string                                     $method
-     * @return \JonnyW\PhantomJs\Message\RequestInterface
-     */
-    public function createRequest($url, $method = RequestInterface::METHOD_GET);
-
-    /**
-     * Create response instance
-     *
-     * @return \JonnyW\PhantomJs\Message\ResoibseInterface
-     */
-    public function createResponse();
-}

+ 79 - 0
src/JonnyW/PhantomJs/Message/MessageFactory.php

@@ -0,0 +1,79 @@
+<?php
+
+/*
+ * This file is part of the php-phantomjs.
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+namespace JonnyW\PhantomJs\Message;
+
+/**
+ * PHP PhantomJs
+ *
+ * @author Jon Wenmoth <contact@jonnyw.me>
+ */
+class MessageFactory implements MessageFactoryInterface
+{
+    /**
+     * Client instance
+     *
+     * @var \JonnyW\PhantomJs\Message\MessageFactory
+     * @access private
+     */
+    private static $instance;
+
+    /**
+     * Get singleton instance.
+     *
+     * @access public
+     * @return \JonnyW\PhantomJs\Message\MessageFactory
+     */
+    public static function getInstance()
+    {
+        if (!self::$instance instanceof MessageFactoryInterface) {
+            self::$instance = new MessageFactory();
+        }
+
+        return self::$instance;
+    }
+
+    /**
+     * Create request instance.
+     *
+     * @access public
+     * @param  string                            $url
+     * @param  string                            $method
+     * @param  int                               $timeout
+     * @return \JonnyW\PhantomJs\Message\Request
+     */
+    public function createRequest($url = null, $method = RequestInterface::METHOD_GET, $timeout = 5000)
+    {
+        return new Request($url, $method, $timeout);
+    }
+
+    /**
+     * Create capture request instance.
+     *
+     * @access public
+     * @param  string                            $url
+     * @param  string                            $method
+     * @param  int                               $timeout
+     * @return \JonnyW\PhantomJs\Message\Request
+     */
+    public function createCaptureRequest($url = null, $method = RequestInterface::METHOD_GET, $timeout = 5000)
+    {
+        return new CaptureRequest($url, $method, $timeout);
+    }
+
+    /**
+     * Create response instance.
+     *
+     * @access public
+     * @return \JonnyW\PhantomJs\Message\Response
+     */
+    public function createResponse()
+    {
+        return new Response();
+    }
+}

+ 55 - 0
src/JonnyW/PhantomJs/Message/MessageFactoryInterface.php

@@ -0,0 +1,55 @@
+<?php
+
+/*
+ * This file is part of the php-phantomjs.
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+namespace JonnyW\PhantomJs\Message;
+
+/**
+ * PHP PhantomJs
+ *
+ * @author Jon Wenmoth <contact@jonnyw.me>
+ */
+interface MessageFactoryInterface
+{
+    /**
+     * Get singleton instance.
+     *
+     * @access public
+     * @return \JonnyW\PhantomJs\Message\MessageFactoryInterface
+     */
+    public static function getInstance();
+
+    /**
+     * Create request instance.
+     *
+     * @access public
+     * @param  string                                     $url     (default: null)
+     * @param  string                                     $method  (default: RequestInterface::METHOD_GET)
+     * @param  int                                        $timeout (default: 5000)
+     * @return \JonnyW\PhantomJs\Message\RequestInterface
+     */
+    public function createRequest($url = null, $method = RequestInterface::METHOD_GET, $timeout = 5000);
+
+    /**
+     * Create capture request instance.
+     *
+     * @access public
+     * @param  string                                     $url     (default: null)
+     * @param  string                                     $method  (default: RequestInterface::METHOD_GET)
+     * @param  int                                        $timeout (default: 5000)
+     * @return \JonnyW\PhantomJs\Message\RequestInterface
+     */
+    public function createCaptureRequest($url = null, $method = RequestInterface::METHOD_GET, $timeout = 5000);
+
+    /**
+     * Create response instance.
+     *
+     * @access public
+     * @return \JonnyW\PhantomJs\Message\ResponseInterface
+     */
+    public function createResponse();
+}

+ 16 - 228
src/JonnyW/PhantomJs/Message/Request.php

@@ -9,259 +9,47 @@
 
 namespace JonnyW\PhantomJs\Message;
 
-use JonnyW\PhantomJs\Exception\InvalidUrlException;
-use JonnyW\PhantomJs\Exception\InvalidMethodException;
-
 /**
  * PHP PhantomJs
  *
  * @author Jon Wenmoth <contact@jonnyw.me>
  */
-class Request implements RequestInterface
+class Request extends AbstractRequest
 {
     /**
-     * Headers
-     *
-     * @var array
-     */
-    protected $headers;
-
-    /**
-     * Request data
-     *
-     * @var array
-     */
-    protected $data;
-
-    /**
-     * Request method
+     * Request type
      *
      * @var string
+     * @access protected
      */
-    protected $method;
-
-    /**
-     * Request URL
-     *
-     * @var string
-     */
-    protected $url;
-
-    /**
-     * Internal constructor
-     *
-     * @param  string $method
-     * @param  string $url
-     * @return void
-     */
-    public function __construct($method = RequestInterface::METHOD_GET, $url = null)
-    {
-        $this->headers  = array();
-        $this->data  = array();
-
-        $this->setMethod($method);
-
-        if ($url) {
-            $this->setUrl($url);
-        }
-    }
-
-    /**
-     * Set request method
-     *
-     * @param  string                            $method
-     * @return \JonnyW\PhantomJs\Message\Request
-     */
-    public function setMethod($method)
-    {
-        $method   = strtoupper($method);
-        $reflection  = new \ReflectionClass('JonnyW\PhantomJs\Message\RequestInterface');
-
-        // Validate method
-        if (!$reflection->hasConstant('METHOD_' . $method)) {
-            throw new InvalidMethodException(sprintf('Invalid method provided: %s', $method));
-        }
-
-        $this->method = $method;
-
-        return $this;
-    }
-
-    /**
-     * Get request method
-     *
-     * @return string
-     */
-    public function getMethod()
-    {
-        return $this->method;
-    }
+    protected $type;
 
     /**
-     * Set request URL
-     *
-     * @param  string                            $url
-     * @return \JonnyW\PhantomJs\Message\Request
-     */
-    public function setUrl($url)
-    {
-        // Validate URL
-        if (!filter_var($url, FILTER_VALIDATE_URL, FILTER_FLAG_HOST_REQUIRED)) {
-            throw new InvalidUrlException(sprintf('Invalid URL provided: %s', $url));
-        }
-
-        $this->url = $url;
-
-        return $this;
-    }
-
-    /**
-     * Get request URL
-     *  - Assembles query string for GET
-     *  and HEAD requests
+     * Get request type
      *
+     * @access public
      * @return string
      */
-    public function getUrl()
+    public function getType()
     {
-        if (!in_array($this->getMethod(), array(RequestInterface::METHOD_GET, RequestInterface::METHOD_HEAD))) {
-            return $this->url;
+        if (!$this->type) {
+            return RequestInterface::REQUEST_TYPE_DEFAULT;
         }
 
-        $url = $this->url;
-
-        // Add query string to URL
-        if (count($this->data)) {
-
-            $url  .= false === strpos($url, '?') ? '?' : '&';
-            $url  .= urldecode(http_build_query($this->data));
-        }
-
-        return $url;
+        return $this->type;
     }
 
     /**
-     * Get content body
-     *  - Returns query string if not GET or HEAD
+     * Set request type
      *
-     * @return string
+     * @access public
+     * @param  string                                    $type
+     * @return \JonnyW\PhantomJs\Message\AbstractRequest
      */
-    public function getBody()
+    public function setType($type)
     {
-        if (in_array($this->getMethod(), array(RequestInterface::METHOD_GET, RequestInterface::METHOD_HEAD))) {
-            return '';
-        }
-
-        return urldecode(http_build_query($this->getRequestData()));
-    }
-
-    /**
-     * Set request data
-     *
-     * @param  array                             $data
-     * @return \JonnyW\PhantomJs\Message\Request
-     */
-    public function setRequestData(array $data)
-    {
-        $this->data = $data;
+        $this->type = $type;
 
         return $this;
     }
-
-    /**
-     * Get request data
-     *
-     * @param  boolean $flat
-     * @return array
-     */
-    public function getRequestData($flat = true)
-    {
-        if ($flat) {
-            $this->flattenData($this->data);
-        }
-
-        return $this->data;
-    }
-
-    /**
-     * Flatten data into single
-     * dimensional array
-     *
-     * @param  array  $data
-     * @param  string $prefix
-     * @param  string $format
-     * @return array
-     */
-    protected function flattenData(array $data, $prefix  = '', $format = '%s')
-    {
-        $flat = array();
-
-        foreach ($data as $name => $value) {
-
-            $ref = $prefix . sprintf($format, $name);
-
-            if (is_array($value)) {
-
-                $flat += $this->flattenData($value, $ref, '[%s]');
-                continue;
-            }
-
-            $flat[$ref] = $value;
-        }
-
-        return $flat;
-    }
-
-    /**
-     * Set headers
-     *
-     * @param  array                             $headers
-     * @return \JonnyW\PhantomJs\Message\Request
-     */
-    public function setHeaders(array $headers)
-    {
-        $this->headers = $headers;
-    }
-
-    /**
-     * Add single header
-     *
-     * @param  string                            $header
-     * @param  string                            $value
-     * @return \JonnyW\PhantomJs\Message\Request
-     */
-    public function addHeader($header, $value)
-    {
-        $this->headers[$header] = $value;
-
-        return $this;
-    }
-
-    /**
-     * Merge headers with existing
-     *
-     * @param  array                             $headers
-     * @return \JonnyW\PhantomJs\Message\Request
-     */
-    public function addHeaders(array $headers)
-    {
-        $this->headers = array_merge($this->headers, $headers);
-
-        return $this;
-    }
-
-    /**
-     * Get request headers
-     *
-     * @param  string $format
-     * @return array
-     */
-    public function getHeaders($format = 'default')
-    {
-        if ($format == 'json') {
-            return json_encode($this->headers);
-        }
-
-        return $this->headers;
-    }
 }

+ 61 - 16
src/JonnyW/PhantomJs/Message/RequestInterface.php

@@ -24,42 +24,85 @@ interface RequestInterface
     const METHOD_DELETE  = 'DELETE';
     const METHOD_PATCH   = 'PATCH';
 
+    const REQUEST_TYPE_DEFAULT = 'default';
+    const REQUEST_TYPE_CAPTURE = 'capture';
+
+    /**
+     * Get request type
+     *
+     * @access public
+     * @return string
+     */
+    public function getType();
+
     /**
      * Set request method
      *
-     * @param  string                                     $method
-     * @return \JonnyW\PhantomJs\Message\RequestInterface
+     * @access public
+     * @param string $method
      */
     public function setMethod($method);
 
     /**
      * Get request method
      *
+     * @access public
      * @return string
      */
     public function getMethod();
 
+    /**
+     * Set timeout period
+     *
+     * @access public
+     * @param int $timeout
+     */
+    public function setTimeout($timeout);
+
+    /**
+     * Get timeout period
+     *
+     * @access public
+     * @return int
+     */
+    public function getTimeout();
+
+    /**
+     * Set page load delay time.
+     *
+     * @access public
+     * @param int $delay
+     */
+    public function setDelay($delay);
+
+    /**
+     * Get page load delay time.
+     *
+     * @access public
+     * @return int
+     */
+    public function getDelay();
+
     /**
      * Set request URL
      *
-     * @param  string                                     $url
-     * @return \JonnyW\PhantomJs\Message\RequestInterface
+     * @access public
+     * @param string $url
      */
     public function setUrl($url);
 
     /**
      * Get request URL
-     *  - Assembles query string for GET
-     *  and HEAD requests
      *
+     * @access public
      * @return string
      */
     public function getUrl();
 
     /**
      * Get content body
-     *  - Returns query string if not GET or HEAD
      *
+     * @access public
      * @return string
      */
     public function getBody();
@@ -67,14 +110,15 @@ interface RequestInterface
     /**
      * Set request data
      *
-     * @param  array                                      $data
-     * @return \JonnyW\PhantomJs\Message\RequestInterface
+     * @access public
+     * @param array $data
      */
     public function setRequestData(array $data);
 
     /**
      * Get request data
      *
+     * @access public
      * @param  boolean $flat
      * @return array
      */
@@ -83,31 +127,32 @@ interface RequestInterface
     /**
      * Set headers
      *
-     * @param  array                                      $headers
-     * @return \JonnyW\PhantomJs\Message\RequestInterface
+     * @access public
+     * @param array $headers
      */
     public function setHeaders(array $headers);
 
     /**
      * Add single header
      *
-     * @param  string                                     $header
-     * @param  string                                     $value
-     * @return \JonnyW\PhantomJs\Message\RequestInterface
+     * @access public
+     * @param string $header
+     * @param string $value
      */
     public function addHeader($header, $value);
 
     /**
      * Merge headers with existing
      *
-     * @param  array                                      $headers
-     * @return \JonnyW\PhantomJs\Message\RequestInterface
+     * @access public
+     * @param array $headers
      */
     public function addHeaders(array $headers);
 
     /**
      * Get request headers
      *
+     * @access public
      * @param  string $format
      * @return array
      */

+ 58 - 41
src/JonnyW/PhantomJs/Message/Response.php

@@ -19,93 +19,89 @@ class Response implements ResponseInterface
      * Http headers array
      *
      * @var array
+     * @access public
      */
-    protected $headers;
+    public $headers;
 
     /**
      * Response int
      *
      * @var string
+     * @access public
      */
-    protected $status;
+    public $status;
 
     /**
      * Response body
      *
      * @var string
+     * @access public
      */
-    protected $content;
+    public $content;
 
     /**
      * Response content type header
      *
      * @var string
+     * @access public
      */
-    protected $contentType;
+    public $contentType;
 
     /**
      * Requested URL
      *
      * @var string
+     * @access public
      */
-    protected $url;
+    public $url;
 
     /**
      * Redirected URL
      *
      * @var string
+     * @access public
      */
-    protected $redirectUrl;
+    public $redirectUrl;
 
     /**
      * Request time string
      *
      * @var string
+     * @access public
      */
-    protected $time;
+    public $time;
 
     /**
-     * Set response data
+     * Console messages
      *
+     * @var array
+     * @access public
+     */
+    public $console;
+
+    /**
+     * Import response data
+     *
+     * @access public
      * @return \JonnyW\PhantomJs\Message\Response
      */
-    public function setData(array $data)
+    public function import(array $data)
     {
-        $this->headers = array();
-
-        // Set headers array
-        if (isset($data['headers'])) {
-            $this->setHeaders((array) $data['headers']);
-        }
-
-        // Set status
-        if (isset($data['status'])) {
-            $this->status = $data['status'];
-        }
-
-        // Set content
-        if (isset($data['content'])) {
-            $this->content = $data['content'];
-        }
+        foreach ($data as $param => $value) {
 
-        // Set content type string
-        if (isset($data['contentType'])) {
-            $this->contentType = $data['contentType'];
-        }
+            if ($param === 'headers') {
+                continue;
+            }
 
-        // Set request URL
-        if (isset($data['url'])) {
-            $this->url = $data['url'];
+            if (property_exists($this, $param)) {
+                $this->$param = $value;
+            }
         }
 
-        // Set redirect URL
-        if (isset($data['redirectURL'])) {
-            $this->redirectUrl = $data['redirectURL'];
-        }
+        $this->headers = array();
 
-        // Set time string
-        if (isset($data['time'])) {
-            $this->time = $data['time'];
+        if (isset($data['headers'])) {
+            $this->setHeaders((array) $data['headers']);
         }
 
         return $this;
@@ -114,6 +110,7 @@ class Response implements ResponseInterface
     /**
      * Set headers array
      *
+     * @access protected
      * @param  array                              $headers
      * @return \JonnyW\PhantomJs\Message\Response
      */
@@ -132,6 +129,7 @@ class Response implements ResponseInterface
     /**
      * Get HTTP headers array
      *
+     * @access public
      * @return array
      */
     public function getHeaders()
@@ -142,7 +140,8 @@ class Response implements ResponseInterface
     /**
      * Get HTTP header value for code
      *
-     * @praam string $$code
+     * @access public
+     * @param  string $code
      * @return mixed
      */
     public function getHeader($code)
@@ -157,6 +156,7 @@ class Response implements ResponseInterface
     /**
      * Get response status code
      *
+     * @access public
      * @return integer
      */
     public function getStatus()
@@ -167,6 +167,7 @@ class Response implements ResponseInterface
     /**
      * Get page content from respone
      *
+     * @access public
      * @return string
      */
     public function getContent()
@@ -177,6 +178,7 @@ class Response implements ResponseInterface
     /**
      * Get content type header
      *
+     * @access public
      * @return string
      */
     public function getContentType()
@@ -187,6 +189,7 @@ class Response implements ResponseInterface
     /**
      * Get request URL
      *
+     * @access public
      * @return string
      */
     public function getUrl()
@@ -197,6 +200,7 @@ class Response implements ResponseInterface
     /**
      * Get redirect URL (if redirected)
      *
+     * @access public
      * @return string
      */
     public function getRedirectUrl()
@@ -208,22 +212,35 @@ class Response implements ResponseInterface
      * Is response a redirect
      *  - Checks status codes
      *
+     * @access public
      * @return boolean
      */
     public function isRedirect()
     {
         $status = $this->getStatus();
 
-        return (bool) ($status >= 300 && $status < 307);
+        return (bool) ($status >= 300 && $status <= 307);
     }
 
     /**
      * Get time string
      *
+     * @access public
      * @return string
      */
     public function getTime()
     {
         return $this->time;
     }
+
+    /**
+     * Get console messages
+     *
+     * @access public
+     * @return array
+     */
+    public function getConsole()
+    {
+        return $this->console;
+    }
 }

+ 14 - 5
src/JonnyW/PhantomJs/Message/ResponseInterface.php

@@ -17,15 +17,16 @@ namespace JonnyW\PhantomJs\Message;
 interface ResponseInterface
 {
     /**
-     * Set response data
+     * Import response data
      *
-     * @return \JonnyW\PhantomJs\Message\ResponseInterface
+     * @access public
      */
-    public function setData(array $data);
+    public function import(array $data);
 
     /**
      * Get HTTP headers array
      *
+     * @access public
      * @return array
      */
     public function getHeaders();
@@ -33,7 +34,8 @@ interface ResponseInterface
     /**
      * Get HTTP header value for code
      *
-     * @praam string $$code
+     * @access public
+     * @param  string $code
      * @return mixed
      */
     public function getHeader($code);
@@ -41,13 +43,15 @@ interface ResponseInterface
     /**
      * Get response status code
      *
-     * @return int|null
+     * @access public
+     * @return integer
      */
     public function getStatus();
 
     /**
      * Get page content from respone
      *
+     * @access public
      * @return string
      */
     public function getContent();
@@ -55,6 +59,7 @@ interface ResponseInterface
     /**
      * Get content type header
      *
+     * @access public
      * @return string
      */
     public function getContentType();
@@ -62,6 +67,7 @@ interface ResponseInterface
     /**
      * Get request URL
      *
+     * @access public
      * @return string
      */
     public function getUrl();
@@ -69,6 +75,7 @@ interface ResponseInterface
     /**
      * Get redirect URL (if redirected)
      *
+     * @access public
      * @return string
      */
     public function getRedirectUrl();
@@ -77,6 +84,7 @@ interface ResponseInterface
      * Is response a redirect
      *  - Checks status codes
      *
+     * @access public
      * @return boolean
      */
     public function isRedirect();
@@ -84,6 +92,7 @@ interface ResponseInterface
     /**
      * Get time string
      *
+     * @access public
      * @return string
      */
     public function getTime();

+ 38 - 0
src/JonnyW/PhantomJs/Parser/JsonParser.php

@@ -0,0 +1,38 @@
+<?php
+
+/*
+ * This file is part of the php-phantomjs.
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+namespace JonnyW\PhantomJs\Parser;
+
+/**
+ * PHP PhantomJs
+ *
+ * @author Jon Wenmoth <contact@jonnyw.me>
+ */
+class JsonParser implements ParserInterface
+{
+    /**
+     * Parse json string into array.
+     *
+     * @access public
+     * @param  string    $data
+     * @return \stdClass
+     */
+    public function parse($data)
+    {
+        if ($data === null || !is_string($data)) {
+            return array();
+        }
+
+        if (substr($data, 0, 1) !== '{' &&
+            substr($data, 0, 1) !== '[') {
+            return array();
+        }
+
+        return (array) json_decode($data, true);
+    }
+}

+ 25 - 0
src/JonnyW/PhantomJs/Parser/ParserInterface.php

@@ -0,0 +1,25 @@
+<?php
+
+/*
+ * This file is part of the php-phantomjs.
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+namespace JonnyW\PhantomJs\Parser;
+
+/**
+ * PHP PhantomJs
+ *
+ * @author Jon Wenmoth <contact@jonnyw.me>
+ */
+interface ParserInterface
+{
+    /**
+     * Parse data.
+     *
+     * @access public
+     * @param mixed $data
+     */
+    public function parse($data);
+}

+ 76 - 0
src/JonnyW/PhantomJs/Procedure/ChainProcedureLoader.php

@@ -0,0 +1,76 @@
+<?php
+
+/*
+ * This file is part of the php-phantomjs.
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+namespace JonnyW\PhantomJs\Procedure;
+
+use JonnyW\PhantomJs\Exception\NotExistsException;
+
+/**
+ * PHP PhantomJs
+ *
+ * @author Jon Wenmoth <contact@jonnyw.me>
+ */
+class ChainProcedureLoader implements ProcedureLoaderInterface
+{
+    /**
+     * Procedure loader storage.
+     *
+     * @var array
+     * @access protected
+     */
+    protected $procedureLoaders;
+
+    /**
+     * Internal cosntructure.
+     *
+     * @access public
+     * @param  array $procedureLoaders
+     * @return void
+     */
+    public function __construct(array $procedureLoaders)
+    {
+        $this->procedureLoaders = $procedureLoaders;
+    }
+
+    /**
+     * Add procedure loader.
+     *
+     * @access public
+     * @param  \JonnyW\PhantomJs\Procedure\ProcedureLoaderInterface $procedureLoader
+     * @return void
+     */
+    public function addLoader(ProcedureLoaderInterface $procedureLoader)
+    {
+        $this->procedureLoaders[] = $procedureLoader;
+    }
+
+    /**
+     * Load procedure instance by id.
+     *
+     * @access public
+     * @param  string                                         $id
+     * @return \JonnyW\PhantomJs\Procedure\ProcedureInterface
+     * @throws \JonnyW\PhantomJs\Exception\NotExistsException
+     */
+    public function load($id)
+    {
+        foreach ($this->procedureLoaders as $loader) {
+
+            try {
+
+                $procedure = $loader->load($id);
+
+                return $procedure;
+
+            } catch (\Exception $e) {}
+
+        }
+
+        throw new \InvalidArgumentException(sprintf('No valid procedure loader could be found to load the \'%s\' procedure.', $id));
+    }
+}

+ 186 - 0
src/JonnyW/PhantomJs/Procedure/Procedure.php

@@ -0,0 +1,186 @@
+<?php
+
+/*
+ * This file is part of the php-phantomjs.
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+namespace JonnyW\PhantomJs\Procedure;
+
+use JonnyW\PhantomJs\ClientInterface;
+use JonnyW\PhantomJs\Cache\CacheInterface;
+use JonnyW\PhantomJs\Parser\ParserInterface;
+use JonnyW\PhantomJs\Message\RequestInterface;
+use JonnyW\PhantomJs\Message\ResponseInterface;
+use JonnyW\PhantomJs\Template\TemplateRendererInterface;
+use JonnyW\PhantomJs\Exception\NotWritableException;
+use JonnyW\PhantomJs\Exception\ProcedureFailedException;
+
+/**
+ * PHP PhantomJs
+ *
+ * @author Jon Wenmoth <contact@jonnyw.me>
+ */
+class Procedure implements ProcedureInterface
+{
+    /**
+     * Parser instance.
+     *
+     * @var \JonnyW\PhantomJs\Parser\ParserInterface
+     * @access protected
+     */
+    protected $parser;
+
+    /**
+     * Cache handler instance.
+     *
+     * @var \JonnyW\PhantomJs\Cache\CacheInterface
+     * @access protected
+     */
+    protected $cacheHandler;
+
+    /**
+     * Procedure template.
+     *
+     * @var string
+     * @access protected
+     */
+    protected $procedure;
+
+    /**
+     * Template renderer.
+     *
+     * @var \JonnyW\PhantomJs\Template\TemplateRendererInterface
+     * @access protected
+     */
+    protected $renderer;
+
+    /**
+     * Internal constructor.
+     *
+     * @access public
+     * @param  \JonnyW\PhantomJs\Parser\ParserInterface             $parser
+     * @param  \JonnyW\PhantomJs\Cache\CacheInterface               $cacheHandler
+     * @param  \JonnyW\PhantomJs\Template\TemplateRendererInterface $renderer
+     * @return void
+     */
+    public function __construct(ParserInterface $parser, CacheInterface $cacheHandler, TemplateRendererInterface $renderer)
+    {
+        $this->parser       = $parser;
+        $this->cacheHandler = $cacheHandler;
+        $this->renderer     = $renderer;
+    }
+
+    /**
+     * Run procedure.
+     *
+     * @access public
+     * @param  \JonnyW\PhantomJs\ClientInterface                    $client
+     * @param  \JonnyW\PhantomJs\Message\RequestInterface           $request
+     * @param  \JonnyW\PhantomJs\Message\ResponseInterface          $response
+     * @return void
+     * @throws \JonnyW\PhantomJs\Exception\NotWriteableException
+     * @throws \JonnyW\PhantomJs\Exception\ProcedureFailedException
+     */
+    public function run(ClientInterface $client, RequestInterface $request, ResponseInterface $response)
+    {
+        try {
+
+            $template  = $this->getProcedure();
+            $procedure = $this->renderer->render($template, array('request' => $request));
+
+            $executable = $this->write($procedure);
+
+            $descriptorspec = array(
+                array('pipe', 'r'),
+                array('pipe', 'w'),
+                array('pipe', 'w')
+            );
+
+            $process = proc_open(escapeshellcmd(sprintf('%s %s', $client->getCommand(), $executable)), $descriptorspec, $pipes, null, null);
+
+            if (!is_resource($process)) {
+                throw new ProcedureFailedException('proc_open() did not return a resource');
+            }
+
+            $result = stream_get_contents($pipes[1]);
+            $log    = stream_get_contents($pipes[2]);
+
+            fclose($pipes[0]);
+            fclose($pipes[1]);
+            fclose($pipes[2]);
+
+            proc_close($process);
+
+            $response->import(
+                $this->parser->parse($result)
+            );
+
+            $client->setLog($log);
+
+            $this->remove($executable);
+
+        } catch (NotWritableException $e) {
+            throw $e;
+        } catch (\Exception $e) {
+
+            if (isset($executable)) {
+                $this->remove($executable);
+            }
+
+            throw new ProcedureFailedException(sprintf('Error when executing PhantomJs procedure "%s" - %s', $request->getType(), $e->getMessage()));
+        }
+    }
+
+    /**
+     * Load procedure.
+     *
+     * @access public
+     * @param  string                                $procedure
+     * @return \JonnyW\PhantomJs\Procedure\Procedure
+     */
+    public function load($procedure)
+    {
+        $this->procedure = $procedure;
+
+        return $this;
+    }
+
+    /**
+     * Get procedure template.
+     *
+     * @access public
+     * @return string
+     */
+    public function getProcedure()
+    {
+        return $this->procedure;
+    }
+
+    /**
+     * Write procedure script cache.
+     *
+     * @access protected
+     * @param  string $procedure
+     * @return string
+     */
+    protected function write($procedure)
+    {
+        $executable = $this->cacheHandler->save(uniqid(), $procedure);
+
+        return $executable;
+    }
+
+    /**
+     * Remove procedure script cache.
+     *
+     * @access protected
+     * @param  string $filePath
+     * @return void
+     */
+    protected function remove($filePath)
+    {
+        $this->cacheHandler->delete($filePath);
+    }
+}

+ 79 - 0
src/JonnyW/PhantomJs/Procedure/ProcedureFactory.php

@@ -0,0 +1,79 @@
+<?php
+
+/*
+ * This file is part of the php-phantomjs.
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace JonnyW\PhantomJs\Procedure;
+
+use \JonnyW\PhantomJs\Cache\CacheInterface;
+use \JonnyW\PhantomJs\Parser\ParserInterface;
+use \JonnyW\PhantomJs\Template\TemplateRendererInterface;
+
+/**
+ * PHP PhantomJs
+ *
+ * @author Jon Wenmoth <contact@jonnyw.me>
+ */
+class ProcedureFactory implements ProcedureFactoryInterface
+{
+    /**
+     * Parser.
+     *
+     * @var \JonnyW\PhantomJs\Parser\ParserInterface
+     * @access protected
+     */
+    protected $parser;
+
+    /**
+     * Cache handler.
+     *
+     * @var \JonnyW\PhantomJs\Cache\CacheInterface
+     * @access protected
+     */
+    protected $cacheHandler;
+
+    /**
+     * Template renderer.
+     *
+     * @var \JonnyW\PhantomJs\Template\TemplateRendererInterface
+     * @access protected
+     */
+    protected $renderer;
+
+    /**
+     * Internal constructor.
+     *
+     * @access public
+     * @param  \JonnyW\PhantomJs\Parser\ParserInterface             $parser
+     * @param  \JonnyW\PhantomJs\Cache\CacheInterface               $cacheHandler
+     * @param  \JonnyW\PhantomJs\Template\TemplateRendererInterface $renderer
+     * @return void
+     */
+    public function __construct(ParserInterface $parser, CacheInterface $cacheHandler, TemplateRendererInterface $renderer)
+    {
+        $this->parser       = $parser;
+        $this->cacheHandler = $cacheHandler;
+        $this->renderer     = $renderer;
+    }
+
+    /**
+     * Create new procedure instance.
+     *
+     * @access public
+     * @return \JonnyW\PhantomJs\Procedure\Procedure
+     */
+    public function createProcedure()
+    {
+        $procedure = new Procedure(
+            $this->parser,
+            $this->cacheHandler,
+            $this->renderer
+        );
+
+        return $procedure;
+    }
+}

+ 26 - 0
src/JonnyW/PhantomJs/Procedure/ProcedureFactoryInterface.php

@@ -0,0 +1,26 @@
+<?php
+
+/*
+ * This file is part of the php-phantomjs.
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace JonnyW\PhantomJs\Procedure;
+
+/**
+ * PHP PhantomJs
+ *
+ * @author Jon Wenmoth <contact@jonnyw.me>
+ */
+interface ProcedureFactoryInterface
+{
+    /**
+     * Create new procedure instance.
+     *
+     * @access public
+     * @return \JonnyW\PhantomJs\Procedure\ProcedureInterface
+     */
+    public function createProcedure();
+}

+ 47 - 0
src/JonnyW/PhantomJs/Procedure/ProcedureInterface.php

@@ -0,0 +1,47 @@
+<?php
+
+/*
+ * This file is part of the php-phantomjs.
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+namespace JonnyW\PhantomJs\Procedure;
+
+use JonnyW\PhantomJs\ClientInterface;
+use JonnyW\PhantomJs\Message\RequestInterface;
+use JonnyW\PhantomJs\Message\ResponseInterface;
+
+/**
+ * PHP PhantomJs
+ *
+ * @author Jon Wenmoth <contact@jonnyw.me>
+ */
+interface ProcedureInterface
+{
+    /**
+     * Run procedure.
+     *
+     * @access public
+     * @param \JonnyW\PhantomJs\ClientInterface           $client
+     * @param \JonnyW\PhantomJs\Message\RequestInterface  $request
+     * @param \JonnyW\PhantomJs\Message\ResponseInterface $response
+     */
+    public function run(ClientInterface $client, RequestInterface $request, ResponseInterface $response);
+
+    /**
+     * Load procedure.
+     *
+     * @access public
+     * @param string $procedure
+     */
+    public function load($procedure);
+
+    /**
+     * Get procedure template.
+     *
+     * @access public
+     * @return string
+     */
+    public function getProcedure();
+}

+ 90 - 0
src/JonnyW/PhantomJs/Procedure/ProcedureLoader.php

@@ -0,0 +1,90 @@
+<?php
+
+/*
+ * This file is part of the php-phantomjs.
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+namespace JonnyW\PhantomJs\Procedure;
+
+use Symfony\Component\Config\FileLocatorInterface;
+use JonnyW\PhantomJs\Exception\NotExistsException;
+
+/**
+ * PHP PhantomJs
+ *
+ * @author Jon Wenmoth <contact@jonnyw.me>
+ */
+class ProcedureLoader implements ProcedureLoaderInterface
+{
+    /**
+     * Procedure factory.
+     *
+     * @var mixed
+     * @access protected
+     */
+    protected $procedureFactory;
+
+    /**
+     * File locator.
+     *
+     * @var \Symfony\Component\Config\FileLocatorInterface
+     * @access protected
+     */
+    protected $locator;
+
+    /**
+     * Internal constructor.
+     *
+     * @access public
+     * @param  \JonnyW\PhantomJs\Procedure\ProcedureFactoryInterface $procedureFactory
+     * @param  \Symfony\Component\Config\FileLocatorInterface        $locator
+     * @return void
+     */
+    public function __construct(ProcedureFactoryInterface $procedureFactory, FileLocatorInterface $locator)
+    {
+        $this->procedureFactory = $procedureFactory;
+        $this->locator          = $locator;
+    }
+
+    /**
+     * Load procedure instance by id.
+     *
+     * @access public
+     * @param  string                                         $id
+     * @return \JonnyW\PhantomJs\Procedure\ProcedureInterface
+     */
+    public function load($id)
+    {
+        $path    = $this->locator->locate(sprintf('%s.proc', $id));
+        $content = $this->loadFile($path);
+
+        $procedure = $this->procedureFactory->createProcedure();
+        $procedure->load($content);
+
+        return $procedure;
+    }
+
+    /**
+     * Load procedure file content.
+     *
+     * @access protected
+     * @param  string                                         $file
+     * @return string
+     * @throws \InvalidArgumentException
+     * @throws \JonnyW\PhantomJs\Exception\NotExistsException
+     */
+    protected function loadFile($file)
+    {
+        if (!stream_is_local($file)) {
+            throw new \InvalidArgumentException(sprintf('Procedure file is not a local file: "%s"', $file));
+        }
+
+        if (!file_exists($file)) {
+            throw new NotExistsException(sprintf('Procedure file does not exist: "%s"', $file));
+        }
+
+        return file_get_contents($file);
+    }
+}

+ 77 - 0
src/JonnyW/PhantomJs/Procedure/ProcedureLoaderFactory.php

@@ -0,0 +1,77 @@
+<?php
+
+/*
+ * This file is part of the php-phantomjs.
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace JonnyW\PhantomJs\Procedure;
+
+use Symfony\Component\Config\FileLocator;
+
+/**
+ * PHP PhantomJs
+ *
+ * @author Jon Wenmoth <contact@jonnyw.me>
+ */
+class ProcedureLoaderFactory implements ProcedureLoaderFactoryInterface
+{
+    /**
+     * Procedure factory.
+     *
+     * @var \JonnyW\PhantomJs\Procedure\ProcedureFactoryInterface
+     * @access protected
+     */
+    protected $procedureFactory;
+
+    /**
+     * Internal constructor.
+     *
+     * @access public
+     * @param  \JonnyW\PhantomJs\Procedure\ProcedureFactoryInterface $procedureFactory
+     * @return void
+     */
+    public function __construct(ProcedureFactoryInterface $procedureFactory)
+    {
+        $this->procedureFactory = $procedureFactory;
+    }
+
+    /**
+     * Create procedure loader instance.
+     *
+     * @access public
+     * @param  string                                      $directory
+     * @return \JonnyW\PhantomJs\Procedure\ProcedureLoader
+     */
+    public function createProcedureLoader($directory)
+    {
+        $procedureFactory = $this->procedureFactory;
+        $fileLocator      = $this->createFileLocator($directory);
+
+        $procedureLoader = new ProcedureLoader(
+            $procedureFactory,
+            $fileLocator
+        );
+
+        return $procedureLoader;
+    }
+
+    /**
+     * Create file locator instance.
+     *
+     * @access protected
+     * @param  string                                $directory
+     * @return \Symfony\Component\Config\FileLocator
+     * @throws \InvalidArgumentException
+     */
+    protected function createFileLocator($directory)
+    {
+        if (!is_dir($directory) || !is_readable($directory)) {
+            throw new \InvalidArgumentException(sprintf('Could not create procedure loader as directory does not exist or is not readable: "%s"', $directory));
+        }
+
+        return new FileLocator($directory);
+    }
+}

+ 27 - 0
src/JonnyW/PhantomJs/Procedure/ProcedureLoaderFactoryInterface.php

@@ -0,0 +1,27 @@
+<?php
+
+/*
+ * This file is part of the php-phantomjs.
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace JonnyW\PhantomJs\Procedure;
+
+/**
+ * PHP PhantomJs
+ *
+ * @author Jon Wenmoth <contact@jonnyw.me>
+ */
+interface ProcedureLoaderFactoryInterface
+{
+    /**
+     * Create procedure loader instance.
+     *
+     * @access public
+     * @param  string                                               $directory
+     * @return \JonnyW\PhantomJs\Procedure\ProcedureLoaderInterface
+     */
+    public function createProcedureLoader($directory);
+}

+ 26 - 0
src/JonnyW/PhantomJs/Procedure/ProcedureLoaderInterface.php

@@ -0,0 +1,26 @@
+<?php
+
+/*
+ * This file is part of the php-phantomjs.
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+namespace JonnyW\PhantomJs\Procedure;
+
+/**
+ * PHP PhantomJs
+ *
+ * @author Jon Wenmoth <contact@jonnyw.me>
+ */
+interface ProcedureLoaderInterface
+{
+    /**
+     * Load procedure instance by id.
+     *
+     * @access public
+     * @param  string                                         $id
+     * @return \JonnyW\PhantomJs\Procedure\ProcedureInterface
+     */
+    public function load($id);
+}

+ 45 - 0
src/JonnyW/PhantomJs/Resources/config/config.yml

@@ -0,0 +1,45 @@
+parameters:
+
+#############################
+### CONFIGURATION OPTIONS ###
+#############################
+
+    phantomjs.cache_dir: /tmp
+    phantomjs.procedure_dir: "%phantomjs.resource_dir%/procedures"
+    phantomjs.procedure_extension: proc
+
+##################
+### PROCEDURES ###
+##################
+
+    phantomjs.procedure.procedure_loader.class: JonnyW\PhantomJs\Procedure\ProcedureLoader
+    phantomjs.procedure.chain_loader.class: JonnyW\PhantomJs\Procedure\ChainProcedureLoader
+    phantomjs.procedure.procedure_factory.class: JonnyW\PhantomJs\Procedure\ProcedureFactory
+    phantomjs.procedure.procedure_loader_factory.class: JonnyW\PhantomJs\Procedure\ProcedureLoaderFactory
+
+################
+### MESSAGES ###
+################
+
+    phantomjs.message.message_factory.class: JonnyW\PhantomJs\Message\MessageFactory
+
+################
+### PARSING ####
+################
+
+    phantomjs.parser.json_parser.class: JonnyW\PhantomJs\Parser\JsonParser
+
+##################
+### TEMPLATES ####
+##################
+
+    phantomjs.template.template_renderer.class: JonnyW\PhantomJs\Template\TemplateRenderer
+    phantomjs.twig.environment.class: Twig_Environment
+    phantomjs.twig.string_loader.class: Twig_Loader_String
+
+##################
+### RESOURCES ####
+##################
+
+    phantomjs.cache.file_cache.class: JonnyW\PhantomJs\Cache\FileCache
+    phantomjs.loader.file_locator.class: Symfony\Component\Config\FileLocator

+ 82 - 0
src/JonnyW/PhantomJs/Resources/config/services.yml

@@ -0,0 +1,82 @@
+services:
+
+##################
+### PROCEDURES ###
+##################
+
+    procedure_loader:
+        alias: phantomjs.procedure.chain_loader
+
+    procedure_loader_factory:
+        alias: phantomjs.procedure.procedure_loader_factory
+
+    phantomjs.procedure.file_locator:
+        class: %phantomjs.loader.file_locator.class%
+        arguments: [ %phantomjs.procedure_dir% ]
+        public: false
+
+    phantomjs.procedure.procedure_loader:
+        class: %phantomjs.procedure.procedure_loader.class%
+        arguments: [ @phantomjs.procedure.procedure_factory, @phantomjs.procedure.file_locator ]
+        public: false
+
+    phantomjs.procedure.chain_loader:
+        class: %phantomjs.procedure.chain_loader.class%
+        arguments: [ [ @phantomjs.procedure.procedure_loader ] ]
+        public: false
+
+    phantomjs.procedure.procedure_factory:
+        class: %phantomjs.procedure.procedure_factory.class%
+        arguments: [ @phantomjs.parser.json_parser, @phantomjs.cache.file_cache, @phantomjs.template.template_renderer ]
+        public: false
+
+    phantomjs.procedure.procedure_loader_factory:
+        class: %phantomjs.procedure.procedure_loader_factory.class%
+        arguments: [ @phantomjs.procedure.procedure_factory ]
+        public: false
+
+################
+### MESSAGES ###
+################
+
+    message_factory:
+        alias: phantomjs.message.message_factory
+
+    phantomjs.message.message_factory:
+        class: %phantomjs.message.message_factory.class%
+        public: false
+
+################
+### PARSING ####
+################
+
+    phantomjs.parser.json_parser:
+        class: %phantomjs.parser.json_parser.class%
+        public: false
+
+##################
+### TEMPLATES ####
+##################
+
+    phantomjs.template.template_renderer:
+        class: %phantomjs.template.template_renderer.class%
+        arguments: [ @phantomjs.twig.environment ]
+        public: false
+
+    phantomjs.twig.environment:
+        class: %phantomjs.twig.environment.class%
+        arguments: [ @phantomjs.twig.string_loader ]
+        public: false
+
+    phantomjs.twig.string_loader:
+        class: %phantomjs.twig.string_loader.class%
+        public: false
+
+##################
+### RESOURCES ####
+##################
+
+    phantomjs.cache.file_cache:
+        class: %phantomjs.cache.file_cache.class%
+        arguments: [ %phantomjs.cache_dir%, %phantomjs.procedure_extension% ]
+        public: false

+ 149 - 0
src/JonnyW/PhantomJs/Resources/procedures/capture.proc

@@ -0,0 +1,149 @@
+
+/**
+ * Set up page and script parameters.
+ */
+var page       = require('webpage').create(),
+    system     = require('system'),
+    response   = {},
+    headers    = {{ request.getHeaders('json') }},
+    delay      = {{ request.getDelay() }},
+    top        = {{ request.getRectTop() }},
+    left       = {{ request.getRectLeft() }},
+    width      = {{ request.getRectWidth() }},
+    height     = {{ request.getRectHeight() }},
+    debug      = [],
+    logs       = [],
+    procedure  = {};
+
+
+/**
+ * Define viewport width & height for capture.
+ */
+if(width && height) {
+    
+    debug.push(new Date().toISOString().slice(0, -5) + ' [INFO] PhantomJS - Set capture clipping size ~ top: ' + top + ' left: ' + left + ' ' + width + 'x' + height);
+    
+    page.clipRect = {
+        top: top,
+        left: left,
+        width: width,
+        height: height
+    };
+}
+
+/**
+ * Define custom headers.
+ */
+page.customHeaders = headers ? headers : {};
+
+/**
+ * Set timeout.
+ */
+page.settings.resourceTimeout = {{ request.getTimeout() }};
+
+/**
+ * Set error in response on timeout.
+ */
+page.onResourceTimeout = function (e) {
+    response        = e;
+    response.status = e.errorCode;
+};
+
+/**
+ * Set response from resource.
+ */
+page.onResourceReceived = function (r) {
+    if(!response.status) response = r;
+};
+
+/**
+ * Add page errors to logs.
+ */
+page.onError = function (msg, trace) {
+ 
+    var error = {
+        message: msg,
+        trace: []
+    };
+    
+    trace.forEach(function(t) {
+        error.trace.push(' -> ' + (t.file || t.sourceURL) + ': ' + t.line + (t.function ? ' (in function ' + t.function + ')' : ''));
+    });
+    
+    logs.push(error);
+};
+
+/**
+ * Global error handling.
+ */
+phantom.onError = function(msg, trace) {
+  
+    var stack = [];
+    
+    trace.forEach(function(t) {
+        stack.push(' -> ' + (t.file || t.sourceURL) + ': ' + t.line + (t.function ? ' (in function ' + t.function + ')' : ''));
+    });
+
+    response.status  = 500;
+    response.content = msg;
+    response.console = stack;
+
+    console.log(JSON.stringify(response, undefined, 4));
+    phantom.exit(1);
+};
+
+/**
+ * Open page.
+ *
+ * @param string $url
+ * @param string $method
+ * @param string $parameters
+ * @param callable $callback
+ */
+page.open ('{{ request.getUrl() }}', '{{ request.getMethod() }}', '{{ request.getBody() }}', function (status) {
+    
+    var delay = Number(delay);
+    
+    if(!delay) {
+        return procedure.execute(status);
+    }
+    
+    debug.push(new Date().toISOString().slice(0, -5) + ' [INFO] PhantomJS - Delaying page render for ' + delay + ' second(s)');
+    
+    window.setTimeout(function () { 
+    
+        debug.push(new Date().toISOString().slice(0, -5) + ' [INFO] PhantomJS - Rendering page after delaying for ' + delay + ' second(s)');
+        procedure.execute(status); 
+    
+    }, (delay * 1000));
+});
+
+/**
+ * Command to execute on page load.
+ */
+procedure.execute = function (status) {
+
+    if (status === 'success') {
+        
+        try {
+        
+            page.render('{{ request.getCaptureFile() }}');
+            
+            response.content = page.evaluate(function () {
+                return document.getElementsByTagName('html')[0].innerHTML
+            });
+        
+        } catch(e) {
+    
+            response.status  = 500;
+            response.content = e.message;
+        }
+    }
+    
+    response.console = logs;
+    
+    system.stderr.write(debug.join('\n') + '\n');
+    console.log(JSON.stringify(response, undefined, 4));
+    
+    phantom.exit();
+};

+ 125 - 0
src/JonnyW/PhantomJs/Resources/procedures/default.proc

@@ -0,0 +1,125 @@
+
+/**
+ * Set up page and script parameters.
+ */
+var page       = require('webpage').create(),
+    system     = require('system'),
+    headers    = {{ request.getHeaders('json') }},
+    delay      = {{ request.getDelay() }},
+    response   = {},
+    debug      = [],
+    logs       = [],
+    procedure  = {};
+
+/**
+ * Define custom headers.
+ */
+page.customHeaders = headers ? headers : {};
+
+/**
+ * Set timeout.
+ */
+page.settings.resourceTimeout = {{ request.getTimeout() }};
+
+/**
+ * Set error in response on timeout.
+ */
+page.onResourceTimeout = function (e) {
+    response        = e;
+    response.status = e.errorCode;
+};
+
+/**
+ * Set response from resource.
+ */
+page.onResourceReceived = function (r) {
+    if(!response.status) response = r;
+};
+
+/**
+ * Add page errors to logs.
+ */
+page.onError = function (msg, trace) {
+ 
+    var error = {
+        message: msg,
+        trace: []
+    };
+    
+    trace.forEach(function(t) {
+        error.trace.push((t.file || t.sourceURL) + ': ' + t.line + (t.function ? ' (in function ' + t.function + ')' : ''));
+    });
+    
+    logs.push(error);
+};
+
+/**
+ * Global error handling.
+ */
+phantom.onError = function(msg, trace) {
+  
+    var stack = [];
+    
+    trace.forEach(function(t) {
+        stack.push((t.file || t.sourceURL) + ': ' + t.line + (t.function ? ' (in function ' + t.function + ')' : ''));
+    });
+
+    response.status  = 500;
+    response.content = msg;
+    response.console = stack;
+
+    console.log(JSON.stringify(response, undefined, 4));
+    phantom.exit(1);
+};
+
+/**
+ * Open page.
+ *
+ * @param string $url
+ * @param string $method
+ * @param string $parameters
+ * @param callable $callback
+ */
+page.open ('{{ request.getUrl() }}', '{{ request.getMethod() }}', '{{ request.getBody() }}', function (status) {
+    
+    if(!delay) {
+        return procedure.execute(status);
+    }
+    
+    debug.push(new Date().toISOString().slice(0, -5) + ' [INFO] PhantomJS - Delaying page render for ' + delay + ' second(s)');
+    
+    window.setTimeout(function () { 
+    
+        debug.push(new Date().toISOString().slice(0, -5) + ' [INFO] PhantomJS - Rendering page after delaying for ' + delay + ' second(s)');
+        procedure.execute(status); 
+        
+    }, (delay * 1000));
+});
+
+/**
+ * Command to execute on page load.
+ */
+procedure.execute = function (status) {
+
+    if (status === 'success') {
+        
+        try {
+        
+            response.content = page.evaluate(function () {
+                return document.getElementsByTagName('html')[0].innerHTML
+            });
+        
+        } catch(e) {
+    
+            response.status  = 500;
+            response.content = e.message;
+        }
+    }
+    
+    response.console = logs;
+    
+    system.stderr.write(debug.join('\n') + '\n');
+    console.log(JSON.stringify(response, undefined, 4));
+    
+    phantom.exit();
+};

+ 50 - 0
src/JonnyW/PhantomJs/Template/TemplateRenderer.php

@@ -0,0 +1,50 @@
+<?php
+
+/*
+ * This file is part of the php-phantomjs.
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+namespace JonnyW\PhantomJs\Template;
+
+/**
+ * PHP PhantomJs
+ *
+ * @author Jon Wenmoth <contact@jonnyw.me>
+ */
+class TemplateRenderer implements TemplateRendererInterface
+{
+    /**
+     * Twig environment instance.
+     *
+     * @var \Twig_Environment
+     * @access protected
+     */
+    protected $twig;
+
+    /**
+     * Internal constructor.
+     *
+     * @access public
+     * @param  \Twig_Environment $twig
+     * @return void
+     */
+    public function __construct(\Twig_Environment $twig)
+    {
+        $this->twig = $twig;
+    }
+
+    /**
+     * Render template.
+     *
+     * @access public
+     * @param  string $template
+     * @param  array  $context  (default: array())
+     * @return string
+     */
+    public function render($template, array $context = array())
+    {
+        return $this->twig->render($template, $context);
+    }
+}

+ 27 - 0
src/JonnyW/PhantomJs/Template/TemplateRendererInterface.php

@@ -0,0 +1,27 @@
+<?php
+
+/*
+ * This file is part of the php-phantomjs.
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+namespace JonnyW\PhantomJs\Template;
+
+/**
+ * PHP PhantomJs
+ *
+ * @author Jon Wenmoth <contact@jonnyw.me>
+ */
+interface TemplateRendererInterface
+{
+    /**
+     * Render template.
+     *
+     * @access public
+     * @param  string $template
+     * @param  array  $context  (default: array())
+     * @return string
+     */
+    public function render($template, array $context = array());
+}

+ 30 - 0
src/JonnyW/PhantomJs/Test/TestCase.php

@@ -0,0 +1,30 @@
+<?php
+
+/*
+ * This file is part of the php-phantomjs.
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+namespace JonnyW\PhantomJs\Test;
+
+use JonnyW\PhantomJs\DependencyInjection\ServiceContainer;
+
+/**
+ * PHP PhantomJs
+ *
+ * @author Jon Wenmoth <contact@jonnyw.me>
+ */
+class TestCase extends \PHPUnit_Framework_TestCase
+{
+    /**
+     * Get dependency injection container.
+     *
+     * @access public
+     * @return \JonnyW\PhantomJs\DependencyInjection\ServiceContainer
+     */
+    public function getContainer()
+    {
+        return ServiceContainer::getInstance();
+    }
+}

+ 525 - 0
src/JonnyW/PhantomJs/Tests/Integration/ClientTest.php

@@ -0,0 +1,525 @@
+<?php
+
+/*
+ * This file is part of the php-phantomjs.
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+namespace JonnyW\PhantomJs\Tests\Integration;
+
+use JonnyW\PhantomJs\Test\TestCase;
+use JonnyW\PhantomJs\Client;
+
+/**
+ * PHP PhantomJs
+ *
+ * @author Jon Wenmoth <contact@jonnyw.me>
+ */
+class ClientTest extends TestCase
+{
+    /**
+     * Test filename
+     *
+     * @var string
+     * @access protected
+     */
+    protected $filename;
+
+    /**
+     * Test directory
+     *
+     * @var string
+     * @access protected
+     */
+    protected $directory;
+
+/** +++++++++++++++++++++++++++++++++++ **/
+/** ++++++++++++++ TESTS ++++++++++++++ **/
+/** +++++++++++++++++++++++++++++++++++ **/
+
+    /**
+     * Test additional procedures can be loaded
+     * through chain loader.
+     *
+     * @access public
+     * @return void
+     */
+    public function testAdditionalProceduresCanBeLoadedThroughChainLoader()
+    {
+        $content = 'TEST_PROCEDURE';
+
+        $procedure = <<<EOF
+    console.log(JSON.stringify({"content": "$content"}, undefined, 4));
+    phantom.exit(1);
+EOF;
+
+        $this->writeProcedure($procedure);
+
+        $procedureLoaderFactory = $this->getContainer()->get('procedure_loader_factory');
+        $procedureLoader        = $procedureLoaderFactory->createProcedureLoader($this->directory);
+
+        $client = $this->getClient();
+        $client->getProcedureLoader()->addLoader($procedureLoader);
+        
+        $request  = $client->getMessageFactory()->createRequest();
+        $response = $client->getMessageFactory()->createResponse();
+
+        $request->setType('test');
+
+        $client->send($request, $response);
+
+        $this->assertSame($content, $response->getContent());
+    }
+
+    /**
+     * Test request returns a status code of zero
+     * if a procedure parse exception is encountered.
+     *
+     * @access public
+     * @return void
+     */
+    public function testRequestReturnsAStatusCodeOfZeroIfAProcedureParseExceptionIsEncountered()
+    {
+        $content = 'TEST_PROCEDURE';
+
+        $procedure = <<<EOF
+    console.log(;
+EOF;
+
+        $this->writeProcedure($procedure);
+
+        $procedureLoaderFactory = $this->getContainer()->get('procedure_loader_factory');
+        $procedureLoader        = $procedureLoaderFactory->createProcedureLoader($this->directory);
+
+        $client = $this->getClient();
+        $client->getProcedureLoader()->addLoader($procedureLoader);
+
+        $request  = $client->getMessageFactory()->createRequest();
+        $response = $client->getMessageFactory()->createResponse();
+
+        $request->setType('test');
+
+        $client->send($request, $response);
+
+        $this->assertEquals(0, $response->getStatus());
+    }
+
+    /**
+     * Test client contains parse error in log if
+     * a parse exception is encountered.
+     *
+     * @access public
+     * @return void
+     */
+    public function testClientContainsParseErrorInLogIfAParseExceptionIsEncountered()
+    {
+        $content = 'TEST_PROCEDURE';
+
+        $procedure = <<<EOF
+    console.log(;
+EOF;
+
+        $this->writeProcedure($procedure);
+
+        $procedureLoaderFactory = $this->getContainer()->get('procedure_loader_factory');
+        $procedureLoader        = $procedureLoaderFactory->createProcedureLoader($this->directory);
+
+        $client = $this->getClient();
+        $client->getProcedureLoader()->addLoader($procedureLoader);
+
+        $request  = $client->getMessageFactory()->createRequest();
+        $response = $client->getMessageFactory()->createResponse();
+
+        $request->setType('test');
+
+        $client->send($request, $response);
+
+        $this->assertContains('SyntaxError: Parse error', $client->getLog());
+    }
+
+    /**
+     * Test response contains 200 status code if page
+     * is successfully loaded.
+     *
+     * @access public
+     * @return void
+     */
+    public function testResponseContains200StatusCodeIfPageIsSuccessfullyLoaded()
+    {
+        $client = $this->getClient();
+
+        $request  = $client->getMessageFactory()->createRequest();
+        $response = $client->getMessageFactory()->createResponse();
+
+        $request->setMethod('GET');
+        $request->setUrl('http://jonnnnyw.github.io/php-phantomjs/tests/test-default.html');
+
+        $client->send($request, $response);
+
+        $this->assertEquals(200, $response->getStatus());
+    }
+
+    /**
+     * Test response contains valid body if page is
+     * successfully loaded.
+     *
+     * @access public
+     * @return void
+     */
+    public function testResponseContainsValidBodyIfPageIsSuccessfullyLoaded()
+    {
+        $client = $this->getClient();
+
+        $request  = $client->getMessageFactory()->createRequest();
+        $response = $client->getMessageFactory()->createResponse();
+
+        $request->setMethod('GET');
+        $request->setUrl('http://jonnnnyw.github.io/php-phantomjs/tests/test-default.html');
+
+        $client->send($request, $response);
+
+        $this->assertContains('PHANTOMJS_DEFAULT_TEST', $response->getContent());
+    }
+
+    /**
+     * Test response contains console error if a
+     * javascript error exists on the page.
+     *
+     * @access public
+     * @return void
+     */
+    public function testResponseContainsConsoleErrorIfAJavascriptErrorExistsOnThePage()
+    {
+        $client = $this->getClient();
+
+        $request  = $client->getMessageFactory()->createRequest();
+        $response = $client->getMessageFactory()->createResponse();
+
+        $request->setMethod('GET');
+        $request->setUrl('http://jonnnnyw.github.io/php-phantomjs/tests/test-console-error.html');
+
+        $client->send($request, $response);
+
+        $console = $response->getConsole();
+
+        $this->assertCount(1, $console);
+        $this->assertContains('ReferenceError: Can\'t find variable: invalid', $console[0]['message']);
+    }
+
+    /**
+     * Test response contains console trace if a
+     * javascript error exists on the page.
+     *
+     * @access public
+     * @return void
+     */
+    public function testResponseContainsConsoleTraceIfAJavascriptErrorExistsOnThePage()
+    {
+        $client = $this->getClient();
+
+        $request  = $client->getMessageFactory()->createRequest();
+        $response = $client->getMessageFactory()->createResponse();
+
+        $request->setMethod('GET');
+        $request->setUrl('http://jonnnnyw.github.io/php-phantomjs/tests/test-console-error.html');
+
+        $client->send($request, $response);
+
+        $console = $response->getConsole();
+
+        $this->assertCount(1, $console[0]['trace']);
+    }
+
+    /**
+     * Test response contains headers.
+     *
+     * @access public
+     * @return void
+     */
+    public function testResponseContainsHeaders()
+    {
+        $client = $this->getClient();
+
+        $request  = $client->getMessageFactory()->createRequest();
+        $response = $client->getMessageFactory()->createResponse();
+
+        $request->setMethod('GET');
+        $request->setUrl('http://jonnnnyw.github.io/php-phantomjs/tests/test-console-error.html');
+
+        $client->send($request, $response);
+
+        $this->assertNotEmpty($response->getHeaders());
+    }
+
+    /**
+     * Test capture request saves file to
+     * to local disk.
+     *
+     * @access public
+     * @return void
+     */
+    public function testCaptureRequestSavesFileToLocalDisk()
+    {
+        $this->filename = 'test.jpg';
+        $file = ($this->directory . '/' . $this->filename);
+
+        $client = $this->getClient();
+
+        $request  = $client->getMessageFactory()->createCaptureRequest();
+        $response = $client->getMessageFactory()->createResponse();
+
+        $request->setMethod('GET');
+        $request->setUrl('http://jonnnnyw.github.io/php-phantomjs/tests/test-console-error.html');
+        $request->setCaptureFile($file);
+
+        $client->send($request, $response);
+
+        $this->assertTrue(file_exists($file));
+    }
+
+    /**
+     * Test capture request saves file to
+     * disk with correct capture dimensions.
+     *
+     * @access public
+     * @return void
+     */
+    public function testCaptureRequestSavesFileToDiskWithCorrectCaptureDimensions()
+    {
+        $this->filename = 'test.jpg';
+        $file = ($this->directory . '/' . $this->filename);
+
+        $width  = 200;
+        $height = 400;
+
+        $client = $this->getClient();
+
+        $request  = $client->getMessageFactory()->createCaptureRequest();
+        $response = $client->getMessageFactory()->createResponse();
+
+        $request->setMethod('GET');
+        $request->setUrl('http://jonnnnyw.github.io/php-phantomjs/tests/test-capture.html');
+        $request->setCaptureFile($file);
+        $request->setCaptureDimensions($width, $height);
+
+        $client->send($request, $response);
+
+        $imageInfo = getimagesize($file);
+
+        $this->assertEquals($width, $imageInfo[0]);
+        $this->assertEquals($height, $imageInfo[1]);
+    }
+
+    /**
+     * Test delay logs start time
+     * in client.
+     *
+     * @access public
+     * @return void
+     */
+    public function testDelayLogsStartTimeInClient()
+    {
+        $delay = 2;
+
+        $client = $this->getClient();
+
+        $request  = $client->getMessageFactory()->createRequest();
+        $response = $client->getMessageFactory()->createResponse();
+
+        $request->setMethod('GET');
+        $request->setUrl('http://jonnnnyw.github.io/php-phantomjs/tests/test-default.html');
+        $request->setDelay($delay);
+
+        $client->send($request, $response);
+
+        $logs = explode("\n", $client->getLog());
+
+        $startIndex = $this->getLogEntryIndex($logs, 'Delaying page render for');
+
+        $this->assertTrue(($startIndex !== false));
+    }
+
+    /**
+     * Test delay logs end time
+     * in client.
+     *
+     * @access public
+     * @return void
+     */
+    public function testDelayLogsEndTimeInClient()
+    {
+        $delay = 2;
+
+        $client = $this->getClient();
+
+        $request  = $client->getMessageFactory()->createRequest();
+        $response = $client->getMessageFactory()->createResponse();
+
+        $request->setMethod('GET');
+        $request->setUrl('http://jonnnnyw.github.io/php-phantomjs/tests/test-default.html');
+        $request->setDelay($delay);
+
+        $client->send($request, $response);
+
+        $logs = explode("\n", $client->getLog());
+
+        $endIndex = $this->getLogEntryIndex($logs, 'Rendering page after');
+
+        $this->assertTrue(($endIndex !== false));
+    }
+
+    /**
+     * Test delay delays page render for
+     * specified time.
+     *
+     * @access public
+     * @return void
+     */
+    public function testDelayDelaysPageRenderForSpecifiedTime()
+    {
+        $delay = 2;
+
+        $client = $this->getClient();
+
+        $request  = $client->getMessageFactory()->createRequest();
+        $response = $client->getMessageFactory()->createResponse();
+
+        $request->setMethod('GET');
+        $request->setUrl('http://jonnnnyw.github.io/php-phantomjs/tests/test-default.html');
+        $request->setDelay($delay);
+
+        $client->send($request, $response);
+
+        $logs = explode("\n", $client->getLog());
+
+        $startIndex = $this->getLogEntryIndex($logs, 'Delaying page render for');
+        $endIndex   = $this->getLogEntryIndex($logs, 'Rendering page after');
+
+        $startTime = strtotime(substr($logs[$startIndex], 0 , 19));
+        $endTime   = strtotime(substr($logs[$endIndex], 0 , 19));
+
+        $this->assertSame(($startTime+$delay), $endTime);
+    }
+
+    /**
+     * Test debug logs debug info to
+     * client log.
+     *
+     * @access public
+     * @return void
+     */
+    public function testDebugLogsDebugInfoToClientLog()
+    {
+        $client = $this->getClient();
+        $client->debug(true);
+
+        $request  = $client->getMessageFactory()->createRequest();
+        $response = $client->getMessageFactory()->createResponse();
+
+        $request->setMethod('GET');
+        $request->setUrl('http://jonnnnyw.github.io/php-phantomjs/tests/test-default.html');
+
+        $client->send($request, $response);
+
+        $this->assertContains('[DEBUG]', $client->getLog());
+    }
+
+/** +++++++++++++++++++++++++++++++++++ **/
+/** ++++++++++ TEST ENTITIES ++++++++++ **/
+/** +++++++++++++++++++++++++++++++++++ **/
+
+    /**
+     * Get client instance.
+     *
+     * @return \JonnyW\PhantomJs\Client
+     */
+    protected function getClient()
+    {
+        return Client::getInstance();
+    }
+
+/** +++++++++++++++++++++++++++++++++++ **/
+/** ++++++++++++ UTILITIES ++++++++++++ **/
+/** +++++++++++++++++++++++++++++++++++ **/
+
+    /**
+     * Set up test environment.
+     *
+     * @access public
+     * @return void
+     */
+    public function setUp()
+    {
+        $this->filename  = 'test.proc';
+        $this->directory = sys_get_temp_dir();
+
+        if (!is_writable($this->directory)) {
+            throw new \RuntimeException(sprintf('Test directory must be writable: %s', $this->directory));
+        }
+    }
+
+    /**
+     * Tear down test environment.
+     *
+     * @access public
+     * @return void
+     */
+    public function tearDown()
+    {
+        $filename = $this->getFilename();
+
+        if (file_exists($filename)) {
+            unlink($filename);
+        }
+    }
+
+    /**
+     * Get test filename.
+     *
+     * @access public
+     * @return string
+     */
+    public function getFilename()
+    {
+        return sprintf('%1$s/%2$s', $this->directory, $this->filename);
+    }
+
+    /**
+     * Write procedure body to file.
+     *
+     * @access public
+     * @param  string $data
+     * @return string
+     */
+    public function writeProcedure($procedure)
+    {
+        $filename = $this->getFilename();
+
+        file_put_contents($filename, $procedure);
+
+        return $filename;
+    }
+
+    /**
+     * Get log entry index.
+     *
+     * @access public
+     * @param  array     $logs
+     * @param  string    $search
+     * @return int|false
+     */
+    public function getLogEntryIndex(array $logs, $search)
+    {
+        foreach ($logs as $index => $log) {
+
+            $pos = stripos($log, $search);
+
+            if ($pos !== false) {
+                return $index;
+            }
+        }
+
+        return false;
+    }
+}

+ 152 - 0
src/JonnyW/PhantomJs/Tests/Integration/Template/TemplateRendererTest.php

@@ -0,0 +1,152 @@
+<?php
+
+/*
+ * This file is part of the php-phantomjs.
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+namespace JonnyW\PhantomJs\Tests\Integration\Template;
+
+use JonnyW\PhantomJs\Test\TestCase;
+use JonnyW\PhantomJs\Template\TemplateRenderer;
+
+/**
+ * PHP PhantomJs
+ *
+ * @author Jon Wenmoth <contact@jonnyw.me>
+ */
+class TemplateRendererTest extends TestCase
+{
+
+/** +++++++++++++++++++++++++++++++++++ **/
+/** ++++++++++++++ TESTS ++++++++++++++ **/
+/** +++++++++++++++++++++++++++++++++++ **/
+
+    /**
+     * Test render injects single parameter
+     * into template.
+     *
+     * @access public
+     * @return void
+     */
+    public function testRenderInjectsSingleParameterIntoTemplate()
+    {
+        $template = 'var param = "{{ test }}"';
+
+        $renderer = $this->getInjectedTemplateRenderer();
+        $result   = $renderer->render($template, array('test' => 'data'));
+
+        $this->assertSame('var param = "data"', $result);
+    }
+
+    /**
+     * Test render injects multiple parameters
+     * into template.
+     *
+     * @access public
+     * @return void
+     */
+    public function testRenderInjectsMultipleParametersIntoTemplates()
+    {
+        $template = 'var param = "{{ test }}", var param2 = "{{ test2 }}"';
+
+        $renderer = $this->getInjectedTemplateRenderer();
+        $result   = $renderer->render($template, array('test' => 'data', 'test2' => 'more data'));
+
+        $this->assertSame('var param = "data", var param2 = "more data"', $result);
+    }
+
+    /**
+     * Test render injects parameter into
+     * template using object method.
+     *
+     * @access public
+     * @return void
+     */
+    public function testRenderInjectsParameterIntoTemplateUsingObjectMethod()
+    {
+        $template = 'var param = {{ request.getTimeout() }}';
+
+        $request = $this->getRequest();
+        $request->expects($this->any())
+            ->method('getTimeout')
+            ->will($this->returnValue(5000));
+
+        $renderer = $this->getInjectedTemplateRenderer();
+        $result   = $renderer->render($template, array('request' => $request));
+
+        $this->assertSame('var param = 5000', $result);
+    }
+
+    /**
+     * Test render injects parameter into
+     * template using object method
+     * with parameter.
+     *
+     * @access public
+     * @return void
+     */
+    public function testRenderInjectsParameterIntoTemplateUsingObjectMethodWithParameter()
+    {
+        $template = 'var param = {{ request.getHeaders("json") }}';
+
+        $request = $this->getRequest();
+        $request->expects($this->once())
+            ->method('getHeaders')
+            ->with($this->identicalTo('json'));
+
+        $renderer = $this->getInjectedTemplateRenderer();
+        $renderer->render($template, array('request' => $request));
+    }
+
+/** +++++++++++++++++++++++++++++++++++ **/
+/** ++++++++++ TEST ENTITIES ++++++++++ **/
+/** +++++++++++++++++++++++++++++++++++ **/
+
+    /**
+     * Get template renderer instance.
+     *
+     * @param  \Twig_Environment                          $twig
+     * @return \JonnyW\PhantomJs\Message\TemplateRenderer
+     */
+    protected function getTemplateRenderer(\Twig_Environment $twig)
+    {
+        $templateRenderer = new TemplateRenderer($twig);
+
+        return $templateRenderer;
+    }
+
+    /**
+     * Get template renderer instance
+     * injected with dependencies.
+     *
+     * @access protected
+     * @return \JonnyW\PhantomJs\Message\TemplateRenderer
+     */
+    protected function getInjectedTemplateRenderer()
+    {
+        $twig = $this->getContainer()->get('phantomjs.twig.environment');
+
+        $templateRenderer = $this->getTemplateRenderer($twig);
+
+        return $templateRenderer;
+    }
+
+/** +++++++++++++++++++++++++++++++++++ **/
+/** ++++++++++ MOCKS / STUBS ++++++++++ **/
+/** +++++++++++++++++++++++++++++++++++ **/
+
+    /**
+     * Get mock request instance.
+     *
+     * @access protected
+     * @return \JonnyW\PhantomJs\Message\RequestInterface
+     */
+    protected function getRequest()
+    {
+        $mockRequest = $this->getMock('\JonnyW\PhantomJs\Message\RequestInterface');
+
+        return $mockRequest;
+    }
+}

+ 307 - 0
src/JonnyW/PhantomJs/Tests/Unit/Cache/FileCacheTest.php

@@ -0,0 +1,307 @@
+<?php
+
+/*
+ * This file is part of the php-phantomjs.
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+namespace JonnyW\PhantomJs\Tests\Unit\Cache;
+
+use JonnyW\PhantomJs\Cache\FileCache;
+
+/**
+ * PHP PhantomJs
+ *
+ * @author Jon Wenmoth <contact@jonnyw.me>
+ */
+class FileCacheTest extends \PHPUnit_Framework_TestCase
+{
+    /**
+     * Test filename
+     *
+     * @var string
+     * @access protected
+     */
+    protected $filename;
+
+    /**
+     * Test directory
+     *
+     * @var string
+     * @access protected
+     */
+    protected $directory;
+
+/** +++++++++++++++++++++++++++++++++++ **/
+/** ++++++++++++++ TESTS ++++++++++++++ **/
+/** +++++++++++++++++++++++++++++++++++ **/
+
+    /**
+     * Test exists returns false if
+     * file does not exist.
+     *
+     * @access public
+     * @return void
+     */
+    public function testExistsReturnsFalesIfFileDoesNotExist()
+    {
+        $fileCache = $this->getFileCache($this->directory, 'txt');
+
+        $this->assertFalse($fileCache->exists($this->filename, 'Test'));
+    }
+
+    /**
+     * Test exists returns true if
+     * file does exist.
+     *
+     * @access public
+     * @return void
+     */
+    public function testExistsReturnsTrueIfFileDoesExist()
+    {
+        touch($this->getFilename());
+
+        $fileCache = $this->getFileCache($this->directory, 'txt');
+
+        $this->assertTrue($fileCache->exists($this->filename));
+    }
+
+    /**
+     * Test save throws not writable
+     * exception if file is not writable
+     *
+     * @return void
+     */
+    public function testSaveThrowsNotWritableExceptionIfFileIsNotWritable()
+    {
+        $this->setExpectedException('\JonnyW\PhantomJs\Exception\NotWritableException');
+
+        $fileCache = $this->getFileCache('/This/Directory/Is/Not/Writable/', 'txt');
+        $fileCache->save($this->filename, 'Test');
+    }
+
+    /**
+     * Test save throws not writable
+     * exception if write data returns
+     * false
+     *
+     * @access public
+     * @return void
+     */
+    public function testSaveThrowsNotWritableExceptionIfWriteFileReturnsFalse()
+    {
+        $this->setExpectedException('\JonnyW\PhantomJs\Exception\NotWritableException');
+
+        $fileCache = $this->getMockFileCache(array('writeData'), $this->directory, 'txt');
+        $fileCache->expects($this->any())
+            ->method('writeData')
+            ->will($this->returnValue(false));
+
+        $fileCache->save($this->filename, 'Test');
+    }
+
+    /**
+     * Test save returns file location if
+     * file is successfully saved.
+     *
+     * @access public
+     * @return void
+     */
+    public function testSaveReturnsFileLocationIfFileIsSuccessfullySaved()
+    {
+        $fileCache  = $this->getFileCache($this->directory, 'txt');
+        $file       = $fileCache->save($this->filename, 'Test');
+
+        $this->assertInternalType('string', $file);
+        $this->assertFileExists($file);
+    }
+
+    /**
+     * Test save with directory path
+     * saves file to directory
+     *
+     * @access public
+     * @return void
+     */
+    public function testSaveWithDirectoryPathSavesFileToDirectory()
+    {
+        $fileCache  = $this->getFileCache('', 'txt');
+        $file       = $fileCache->save($this->directory, 'Test');
+
+        $this->assertSame(dirname($file), $this->directory);
+
+        unlink($file);
+    }
+
+    /**
+     * Test save with directory path
+     * throws not writable exception
+     * if path is not writable
+     *
+     * @access public
+     * @return void
+     */
+    public function testSaveWithDirectoryPathThrowsNotWritableExceptionIfPathIsNotWritable()
+    {
+        $this->setExpectedException('\JonnyW\PhantomJs\Exception\NotWritableException');
+
+        $fileCache  = $this->getFileCache($this->directory, 'txt');
+        $file       = $fileCache->save('/This/Directory/Is/Not/Writable/', 'Test');
+    }
+
+    /**
+     * Test save with absolute filename
+     * saves file.
+     *
+     * @access public
+     * @return void
+     */
+    public function testSaveWithAbsoluteFilenameSavesFile()
+    {
+        $test = sprintf('%1$s/%2$s', $this->directory, 'new-file.txt');
+
+        $fileCache = $this->getFileCache('', 'txt');
+        $file      = $fileCache->save($test, 'Test');
+
+        $this->assertSame($test, $file);
+
+        unlink($file);
+    }
+
+    /**
+     * Test fetch data returns not exists
+     * exception if file does not exist.
+     *
+     * @access public
+     * @return void
+     */
+    public function testFetchDataThrowsNotExistsExceptionIfFileDoesNotExist()
+    {
+        $this->setExpectedException('\JonnyW\PhantomJs\Exception\NotExistsException');
+
+        $fileCache = $this->getFileCache('', 'txt');
+
+        $this->assertFalse($fileCache->fetch($this->filename));
+    }
+
+    /**
+     * Test fetch data returns data if
+     * file exists..
+     *
+     * @access public
+     * @return void
+     */
+    public function testFetchDataReturnsDataIfFileExists()
+    {
+        $test = 'Test';
+
+        $fileCache = $this->getFileCache($this->directory, 'txt');
+        $fileCache->save($this->filename, $test);
+
+        $content = $fileCache->fetch($this->filename);
+
+        $this->assertSame($test, $content);
+    }
+
+    /**
+     * Test delete removes file.
+     *
+     * @access public
+     * @return void
+     */
+    public function testDeleteRemovesFile()
+    {
+        $fileCache = $this->getFileCache($this->directory, 'txt');
+
+        $file = $fileCache->save($this->filename, 'Test');
+
+        $fileCache->delete($this->filename);
+
+        $this->assertFileNotExists($file);
+    }
+
+/** +++++++++++++++++++++++++++++++++++ **/
+/** ++++++++++ TEST ENTITIES ++++++++++ **/
+/** +++++++++++++++++++++++++++++++++++ **/
+
+    /**
+     * Get file write instance
+     *
+     * @param  string                            $directory
+     * @param  string                            $extension
+     * @return \JonnyW\PhantomJs\Cache\FileCache
+     */
+    protected function getFileCache($directory, $extension)
+    {
+        $fileCache = new FileCache($directory, $extension);
+
+        return $fileCache;
+    }
+
+/** +++++++++++++++++++++++++++++++++++ **/
+/** ++++++++++ MOCKS / STUBS ++++++++++ **/
+/** +++++++++++++++++++++++++++++++++++ **/
+
+    /**
+     * Get mock file cache.
+     *
+     * @access protected
+     * @param  array                             $methods
+     * @param  string                            $directory
+     * @param  string                            $extension
+     * @return \JonnyW\PhantomJs\Cache\FileCache
+     */
+    protected function getMockFileCache(array $methods, $directory, $extension)
+    {
+        $mockFileCache = $this->getMock('\JonnyW\PhantomJs\Cache\FileCache', $methods, array($directory, $extension));
+
+        return $mockFileCache;
+    }
+
+/** +++++++++++++++++++++++++++++++++++ **/
+/** ++++++++++++ UTILITIES ++++++++++++ **/
+/** +++++++++++++++++++++++++++++++++++ **/
+
+    /**
+     * Set up test environment.
+     *
+     * @access public
+     * @return void
+     */
+    public function setUp()
+    {
+        $this->filename  = 'test.txt';
+        $this->directory = sys_get_temp_dir();
+
+        if (!is_writable($this->directory)) {
+            throw new \RuntimeException(sprintf('Test directory must be writable: %s', $this->directory));
+        }
+    }
+
+    /**
+     * Tear down test environment.
+     *
+     * @access public
+     * @return void
+     */
+    public function tearDown()
+    {
+        $filename = $this->getFilename();
+
+        if (file_exists($filename)) {
+            unlink($filename);
+        }
+    }
+
+    /**
+     * Get test filename.
+     *
+     * @access public
+     * @return string
+     */
+    public function getFilename()
+    {
+        return sprintf('%1$s/%2$s', $this->directory, $this->filename);
+    }
+}

+ 462 - 0
src/JonnyW/PhantomJs/Tests/Unit/ClientTest.php

@@ -0,0 +1,462 @@
+<?php
+
+/*
+ * This file is part of the php-phantomjs.
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+namespace JonnyW\PhantomJs\Tests\Unit;
+
+use JonnyW\PhantomJs\Client;
+use JonnyW\PhantomJs\Message\MessageFactoryInterface;
+use JonnyW\PhantomJs\Procedure\ProcedureLoaderInterface;
+
+/**
+ * PHP PhantomJs
+ *
+ * @author Jon Wenmoth <contact@jonnyw.me>
+ */
+class ClientTest extends \PHPUnit_Framework_TestCase
+{
+    /**
+     * Test get instance returns instance
+     * of client.
+     *
+     * @access public
+     * @return void
+     */
+    public function testGetInstanceReturnsInstanceOfClient()
+    {
+        $this->assertInstanceOf('\JonnyW\PhantomJs\Client', Client::getInstance());
+    }
+
+    /**
+     * Test get message factory returns instance
+     * of message factory.
+     *
+     * @return void
+     */
+    public function testGetMessageFactoryReturnsInstanceOfMessageFactory()
+    {
+        $procedureLoader = $this->getProcedureLoader();
+        $messageFactory  = $this->getMessageFactory();
+
+        $client = $this->getClient($procedureLoader, $messageFactory);
+
+        $this->assertInstanceOf('\JonnyW\PhantomJs\Message\MessageFactoryInterface', $client->getMessageFactory());
+    }
+    
+    /**
+     * Test get procedure loader returns
+     * instance of proecure loader.
+     *
+     * @return void
+     */
+    public function testGetProcedureLoaderReturnsInstanceOfProcedureLoader()
+    {
+        $procedureLoader = $this->getProcedureLoader();
+        $messageFactory  = $this->getMessageFactory();
+
+        $client = $this->getClient($procedureLoader, $messageFactory);
+
+        $this->assertInstanceOf('\JonnyW\PhantomJs\Procedure\ProcedureLoaderInterface', $client->getProcedureLoader());
+    }
+    
+    /**
+     * Test set phantom JS throws invalid
+     * executable exception if phantom
+     * JS path is invalid.
+     *
+     * @access public
+     * @return void
+     */
+    public function testSetPhantomJsThrowsInvalidExecutableExceptionIfPhantomJsPathIsInvalid()
+    {
+        $this->setExpectedException('\JonnyW\PhantomJs\Exception\InvalidExecutableException');
+
+        $procedureLoader = $this->getProcedureLoader();
+        $messageFactory  = $this->getMessageFactory();
+
+        $client = $this->getClient($procedureLoader, $messageFactory);
+        $client->setPhantomJs('/invalid/phantomjs/path');
+    }
+
+    /**
+     * Test set phantom loader throws invalid
+     * executable exception if phantom
+     * loader path is invalid.
+     *
+     * @access public
+     * @return void
+     */
+    public function testSetPhantomLoaderThrowsInvalidExecutableExceptionIfPhantomLoaderPathIsInvalid()
+    {
+        $this->setExpectedException('\JonnyW\PhantomJs\Exception\InvalidExecutableException');
+
+        $procedureLoader = $this->getProcedureLoader();
+        $messageFactory  = $this->getMessageFactory();
+
+        $client = $this->getClient($procedureLoader, $messageFactory);
+        $client->setPhantomLoader('/invalid/phantomloader/path');
+    }
+
+    /**
+     * Test get phantom JS returns default path
+     * to phantom JS executable.
+     *
+     * @access public
+     * @return void
+     */
+    public function testGetPhantomJsReturnsDefaultPathToPhantomJsExecutable()
+    {
+        $procedureLoader = $this->getProcedureLoader();
+        $messageFactory  = $this->getMessageFactory();
+
+        $client = $this->getClient($procedureLoader, $messageFactory);
+
+        $this->assertSame('bin/phantomjs', $client->getPhantomJs());
+    }
+
+    /**
+     * Test get phantom loader returns default path
+     * to phantom loader executable.
+     *
+     * @access public
+     * @return void
+     */
+    public function testGetPhantomLoaderReturnsDefaultPathToPhantomLoaderExecutable()
+    {
+        $procedureLoader = $this->getProcedureLoader();
+        $messageFactory  = $this->getMessageFactory();
+
+        $client = $this->getClient($procedureLoader, $messageFactory);
+
+        $this->assertSame('bin/phantomloader', $client->getPhantomLoader());
+    }
+
+    /**
+     * Test set log sets log info in
+     * client.
+     *
+     * @access public
+     * @return void
+     */
+    public function testSetLogSetsLogInfoInClient()
+    {
+        $procedureLoader = $this->getProcedureLoader();
+        $messageFactory  = $this->getMessageFactory();
+
+        $log = 'Test log info';
+
+        $client = $this->getClient($procedureLoader, $messageFactory);
+        $client->setLog($log);
+
+        $this->assertSame($log, $client->getLog());
+    }
+
+    /**
+     * Test clear log clears log
+     * info in client.
+     *
+     * @access public
+     * @return void
+     */
+    public function testClearLogsClearsLogInfoInClient()
+    {
+        $procedureLoader = $this->getProcedureLoader();
+        $messageFactory  = $this->getMessageFactory();
+
+        $log = 'Test log info';
+
+        $client = $this->getClient($procedureLoader, $messageFactory);
+        $client->setLog($log);
+        $client->clearLog();
+
+        $this->assertEmpty($client->getLog());
+    }
+
+    /**
+     * Test add option adds option to
+     * Phantom Js run options if option
+     * is not set.
+     *
+     * @access public
+     * @return void
+     */
+    public function testAddOptionAddsOptionToPhantomJsRunOptionsIfOptionIsNotSet()
+    {
+        $procedureLoader = $this->getProcedureLoader();
+        $messageFactory  = $this->getMessageFactory();
+
+        $options = array(
+            'option1',
+            'option2'
+        );
+
+        $client = $this->getClient($procedureLoader, $messageFactory);
+        $client->setOptions($options);
+        $client->addOption('option3');
+
+        array_push($options, 'option3');
+
+        $this->assertSame($options, $client->getOptions());
+    }
+
+    /**
+     * Test add option does not add option
+     * to Phantom Js run options if option
+     * is set.
+     *
+     * @access public
+     * @return void
+     */
+    public function testAddOptionDoesNotAddOptionToPhantomJsRunOptionsIfOptionIsSet()
+    {
+        $procedureLoader = $this->getProcedureLoader();
+        $messageFactory  = $this->getMessageFactory();
+
+        $options = array(
+            'option1',
+            'option2'
+        );
+
+        $client = $this->getClient($procedureLoader, $messageFactory);
+        $client->setOptions($options);
+        $client->addOption('option2');
+
+        $this->assertSame($options, $client->getOptions());
+    }
+
+    /**
+     * Test get command throws invalid executable
+     * exception if PhantomJs is invalid.
+     *
+     * @access public
+     * @return void
+     */
+    public function testGetCommandThrowsInvalidExecutableExceptionIfPhantomJsIsInvalid()
+    {
+        $this->setExpectedException('\JonnyW\PhantomJs\Exception\InvalidExecutableException');
+
+        $procedureLoader = $this->getProcedureLoader();
+        $messageFactory  = $this->getMessageFactory();
+
+        $client = $this->getClient($procedureLoader, $messageFactory);
+
+        $phantomJs = new \ReflectionProperty(get_class($client), 'phantomJs');
+        $phantomJs->setAccessible(true);
+        $phantomJs->setValue($client, 'invalid/path');
+
+        $client->getCommand();
+    }
+
+    /**
+     * Test get command throws invalid executable
+     * exception if PhantomLoader is invalid..
+     *
+     * @access public
+     * @return void
+     */
+    public function testGetCommandThrowsInvalidExecutableExceptionIfPhantomLoaderIsInvalid()
+    {
+        $this->setExpectedException('\JonnyW\PhantomJs\Exception\InvalidExecutableException');
+
+        $procedureLoader = $this->getProcedureLoader();
+        $messageFactory  = $this->getMessageFactory();
+
+        $client = $this->getClient($procedureLoader, $messageFactory);
+
+        $phantomLoader = new \ReflectionProperty(get_class($client), 'phantomLoader');
+        $phantomLoader->setAccessible(true);
+        $phantomLoader->setValue($client, 'invalid/path');
+
+        $client->getCommand();
+    }
+
+    /**
+     * Test get command contains PhantomKs executable.
+     *
+     * @access public
+     * @return void
+     */
+    public function testGetCommandContainsPhantomJsExecutable()
+    {
+        $procedureLoader = $this->getProcedureLoader();
+        $messageFactory  = $this->getMessageFactory();
+
+        $client = $this->getClient($procedureLoader, $messageFactory);
+
+        $this->assertContains($client->getPhantomJs(), $client->getCommand());
+    }
+
+    /**
+     * Test get command contains PhantomLoader executable.
+     *
+     * @access public
+     * @return void
+     */
+    public function testGetCommandContainsPhantomLoaderExecutable()
+    {
+        $procedureLoader = $this->getProcedureLoader();
+        $messageFactory  = $this->getMessageFactory();
+
+        $client = $this->getClient($procedureLoader, $messageFactory);
+
+        $this->assertContains($client->getPhantomLoader(), $client->getCommand());
+    }
+
+    /**
+     * Test get command sets debug flag if debug is
+     * set to true.
+     *
+     * @access public
+     * @return void
+     */
+    public function testGetCommandSetsDebugFlagIfDebugIsSetToTrue()
+    {
+        $procedureLoader = $this->getProcedureLoader();
+        $messageFactory  = $this->getMessageFactory();
+
+        $client = $this->getClient($procedureLoader, $messageFactory);
+        $client->debug(true);
+
+        $this->assertContains('--debug=true', $client->getCommand());
+    }
+
+    /**
+     * Test get command does not set debug flag if
+     * debug is set to false.
+     *
+     * @access public
+     * @return void
+     */
+    public function testGetCommandDoesNotSetDebugFlagIfDebugIsSetToFalse()
+    {
+        $procedureLoader = $this->getProcedureLoader();
+        $messageFactory  = $this->getMessageFactory();
+
+        $client = $this->getClient($procedureLoader, $messageFactory);
+        $client->debug(false);
+
+        $this->assertNotContains('--debug=true', $client->getCommand());
+    }
+
+    /**
+     * Test get command sets 1 option.
+     *
+     * @access public
+     * @return void
+     */
+    public function testGetCommandSets1Option()
+    {
+        $procedureLoader = $this->getProcedureLoader();
+        $messageFactory  = $this->getMessageFactory();
+
+        $option = '--local-storage-path=/some/path';
+
+        $client = $this->getClient($procedureLoader, $messageFactory);
+        $client->addOption($option);
+
+        $this->assertContains($option, $client->getCommand());
+    }
+
+    /**
+     * Test get command sets multiple options.
+     *
+     * @access public
+     * @return void
+     */
+    public function testGetCommandSetsMultipleOptions()
+    {
+        $procedureLoader = $this->getProcedureLoader();
+        $messageFactory  = $this->getMessageFactory();
+
+        $option1 = '--local-storage-path=/some/path';
+        $option2 = '--local-storage-quota=5';
+        $option3 = '--local-to-remote-url-access=true';
+
+        $client = $this->getClient($procedureLoader, $messageFactory);
+        $client->addOption($option1);
+        $client->addOption($option2);
+        $client->addOption($option3);
+
+        $command = $client->getCommand();
+
+        $this->assertContains($option1, $command);
+        $this->assertContains($option2, $command);
+        $this->assertContains($option3, $command);
+    }
+
+    /**
+     * Test get command sets debug option if
+     * additional options are set.
+     *
+     * @access public
+     * @return void
+     */
+    public function testGetCommandSetsDebugOptionIfAdditionalOptionsAreSet()
+    {
+        $procedureLoader = $this->getProcedureLoader();
+        $messageFactory  = $this->getMessageFactory();
+
+        $option = '--local-storage-path=/some/path';
+
+        $client = $this->getClient($procedureLoader, $messageFactory);
+        $client->addOption($option);
+        $client->debug(true);
+
+        $command = $client->getCommand();
+
+        $this->assertContains($option, $command);
+        $this->assertContains('--debug=true', $command);
+    }
+
+/** +++++++++++++++++++++++++++++++++++ **/
+/** ++++++++++ TEST ENTITIES ++++++++++ **/
+/** +++++++++++++++++++++++++++++++++++ **/
+
+    /**
+     * Get client instance
+     *
+     * @param  \JonnyW\PhantomJs\Procedure\ProcedureLoaderInterface $procedureLoader
+     * @param  \JonnyW\PhantomJs\Message\MessageFactoryInterface    $messageFactory
+     * @return \JonnyW\PhantomJs\Client
+     */
+    protected function getClient(ProcedureLoaderInterface $procedureLoader, MessageFactoryInterface $messageFactory)
+    {
+        $client = new Client($procedureLoader, $messageFactory);
+
+        return $client;
+    }
+
+/** +++++++++++++++++++++++++++++++++++ **/
+/** ++++++++++ MOCKS / STUBS ++++++++++ **/
+/** +++++++++++++++++++++++++++++++++++ **/
+
+    /**
+     * Get mock message factory instance.
+     *
+     * @access protected
+     * @return \JonnyW\PhantomJs\Message\MessageFactoryInterface
+     */
+    protected function getMessageFactory()
+    {
+        $mockMessageFactory = $this->getMock('\JonnyW\PhantomJs\Message\MessageFactoryInterface');
+
+        return $mockMessageFactory;
+    }
+
+    /**
+     * Get mock procedure loader instance.
+     *
+     * @access protected
+     * @return \JonnyW\PhantomJs\Procedure\ProcedureLoaderInterface
+     */
+    protected function getProcedureLoader()
+    {
+        $mockProcedureLoader = $this->getMock('\JonnyW\PhantomJs\Procedure\ProcedureLoaderInterface');
+
+        return $mockProcedureLoader;
+    }
+}

+ 543 - 0
src/JonnyW/PhantomJs/Tests/Unit/Message/CaptureRequestTest.php

@@ -0,0 +1,543 @@
+<?php
+
+/*
+ * This file is part of the php-phantomjs.
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+namespace JonnyW\PhantomJs\Tests\Unit\Message;
+
+use JonnyW\PhantomJs\Message\CaptureRequest;
+use JonnyW\PhantomJs\Message\RequestInterface;
+
+/**
+ * PHP PhantomJs
+ *
+ * @author Jon Wenmoth <contact@jonnyw.me>
+ */
+class CaptureRequestTest extends \PHPUnit_Framework_TestCase
+{
+
+/** +++++++++++++++++++++++++++++++++++ **/
+/** ++++++++++++++ TESTS ++++++++++++++ **/
+/** +++++++++++++++++++++++++++++++++++ **/
+
+    /**
+     * Test get type returns capture request type.
+     *
+     * @access public
+     * @return void
+     */
+    public function testGetTypeReturnsCaptureRequestType()
+    {
+        $captureRequest = $this->getCaptureRequest();
+
+        $this->assertSame(RequestInterface::REQUEST_TYPE_CAPTURE, $captureRequest->getType());
+    }
+
+    /**
+     * Test URL can be set via constructor.
+     *
+     * @access public
+     * @return void
+     */
+    public function testUrlCanBeSetViaConstructor()
+    {
+        $url            = 'http://test.com';
+        $captureRequest = $this->getCaptureRequest($url);
+
+        $this->assertSame($url, $captureRequest->getUrl());
+    }
+
+    /**
+     * Test method can be set via constructor.
+     *
+     * @access public
+     * @return void
+     */
+    public function testMethodCanBeSetViaConstructor()
+    {
+        $method         = 'GET';
+        $captureRequest = $this->getCaptureRequest(null, $method);
+
+        $this->assertSame($method, $captureRequest->getMethod());
+    }
+
+    /**
+     * Test timeout can be set via constructor.
+     *
+     * @access public
+     * @return void
+     */
+    public function testTimeoutCanBeSetViaConstructor()
+    {
+        $timeout        = 100000;
+        $captureRequest = $this->getCaptureRequest('http://test.com', 'GET', $timeout);
+
+        $this->assertSame($timeout, $captureRequest->getTimeout());
+    }
+
+    /**
+     * Test set method throws invalid method
+     * exception if an invalid method is set
+     *
+     * @access public
+     * @return void
+     */
+    public function testSetMethodThrowsInvalidMethodExceptionIfAnInvalidMethodIsSet()
+    {
+        $this->setExpectedException('\JonnyW\PhantomJs\Exception\InvalidMethodException');
+
+        $captureRequest = $this->getCaptureRequest();
+        $captureRequest->setMethod('INVALID_METHOD');
+    }
+
+  /**
+     * Test set method throws invalid method
+     * exception if an invalid method is set
+     * via constructor
+     *
+     * @access public
+     * @return void
+     */
+    public function testSetMethodThrowsInvalidMethodExceptionIfAnInvalidMethodIsSetViaConstructor()
+    {
+        $this->setExpectedException('\JonnyW\PhantomJs\Exception\InvalidMethodException');
+
+        $this->getCaptureRequest('http://test.com', 'INVALID_METHOD');
+    }
+
+    /**
+     * Test set capture dimensions sets
+     * rect width.
+     *
+     * @access public
+     * @return void
+     */
+    public function testSetCaptureDimensionsSetsRectWidth()
+    {
+        $width  = 100;
+        $height = 200;
+
+        $captureRequest = $this->getCaptureRequest();
+        $captureRequest->setCaptureDimensions($width, $height);
+
+        $this->assertSame($width, $captureRequest->getRectWidth());
+    }
+
+    /**
+     * Test set capture dimensions sets
+     * rect height.
+     *
+     * @access public
+     * @return void
+     */
+    public function testSetCaptureDimensionsSetsRectHeight()
+    {
+        $width  = 100;
+        $height = 200;
+
+        $captureRequest = $this->getCaptureRequest();
+        $captureRequest->setCaptureDimensions($width, $height);
+
+        $this->assertSame($height, $captureRequest->getRectHeight());
+    }
+
+    /**
+     * Test set capture dimensions sets
+     * rect top.
+     *
+     * @access public
+     * @return void
+     */
+    public function testSetCaptureDimensionsSetsRectTop()
+    {
+        $width  = 100;
+        $height = 200;
+        $top    = 50;
+
+        $captureRequest = $this->getCaptureRequest();
+        $captureRequest->setCaptureDimensions($width, $height, $top);
+
+        $this->assertSame($top, $captureRequest->getRectTop());
+    }
+
+    /**
+     * Test set capture dimensions sets
+     * rect left.
+     *
+     * @access public
+     * @return void
+     */
+    public function testSetCaptureDimensionsSetsRectLeft()
+    {
+        $width  = 100;
+        $height = 200;
+        $left   = 50;
+
+        $captureRequest = $this->getCaptureRequest();
+        $captureRequest->setCaptureDimensions($width, $height, 0, $left);
+
+        $this->assertSame($left, $captureRequest->getRectLeft());
+    }
+
+    /**
+     * Test set URL throws invalid
+     * URL exception if an invalid
+     * URL is set.
+     *
+     * @access public
+     * @return void
+     */
+    public function testSetUrlThrowsInvalidUrlExceptionIfAnInvalidUrlIsSet()
+    {
+        $this->setExpectedException('\JonnyW\PhantomJs\Exception\InvalidUrlException');
+
+        $captureRequest = $this->getCaptureRequest();
+        $captureRequest->setUrl('\\AnInvalidUrl');
+    }
+
+    /**
+     * Test get URL returns URL without query
+     * paramters if method is not HEAD or GET.
+     *
+     * @access public
+     * @return void
+     */
+    public function testGetUrlReturnsUrlWithoutQueryParametersIfMethodIsNotHeadOrGet()
+    {
+        $url = 'http://test.com';
+
+        $data = array(
+            'test_param1' => 'Testing1',
+            'test_param2' => 'Testing2'
+        );
+
+        $captureRequest = $this->getCaptureRequest();
+        $captureRequest->setMethod('POST');
+        $captureRequest->setUrl($url);
+        $captureRequest->setRequestData($data);
+
+        $this->assertSame($url, $captureRequest->getUrl());
+    }
+
+    /**
+     * Test get URL returns URL with query
+     * parameters if method is GET.
+     *
+     * @access public
+     * @return void
+     */
+    public function testGetUrlReturnsUrlWithQueryParametersIfMethodIsGet()
+    {
+        $url = 'http://test.com';
+
+        $data = array(
+            'test_param1' => 'Testing1',
+            'test_param2' => 'Testing2'
+        );
+
+        $captureRequest = $this->getCaptureRequest();
+        $captureRequest->setMethod('GET');
+        $captureRequest->setUrl($url);
+        $captureRequest->setRequestData($data);
+
+        $expectedUrl = $url . '?test_param1=Testing1&test_param2=Testing2';
+
+        $this->assertSame($expectedUrl, $captureRequest->getUrl());
+    }
+
+    /**
+     * Test get URL returns URL with query
+     * parameters if method is HEAD.
+     *
+     * @access public
+     * @return void
+     */
+    public function testGetUrlReturnsUrlWithQueryParametersIfMethodIsHead()
+    {
+        $url = 'http://test.com';
+
+        $data = array(
+            'test_param1' => 'Testing1',
+            'test_param2' => 'Testing2'
+        );
+
+        $captureRequest = $this->getCaptureRequest();
+        $captureRequest->setMethod('HEAD');
+        $captureRequest->setUrl($url);
+        $captureRequest->setRequestData($data);
+
+        $expectedUrl = $url . '?test_param1=Testing1&test_param2=Testing2';
+
+        $this->assertSame($expectedUrl, $captureRequest->getUrl());
+    }
+
+    /**
+     * Test get URL returns URL with query
+     * parameters if method is HEAD.
+     *
+     * @access public
+     * @return void
+     */
+    public function testGetUrlAppendsQueryParametersIfUrlHasExistingQueryParameters()
+    {
+        $url = 'http://test.com?existing_param=Existing';
+
+        $data = array(
+            'test_param1' => 'Testing1',
+            'test_param2' => 'Testing2'
+        );
+
+        $captureRequest = $this->getCaptureRequest();
+        $captureRequest->setMethod('GET');
+        $captureRequest->setUrl($url);
+        $captureRequest->setRequestData($data);
+
+        $expectedUrl = $url . '&test_param1=Testing1&test_param2=Testing2';
+
+        $this->assertSame($expectedUrl, $captureRequest->getUrl());
+    }
+
+    /**
+     * Test get body returns an empty
+     * string if method is GET.
+     *
+     * @access public
+     * @return void
+     */
+    public function testGetBodyReturnsAnEmptyStringIfMethodIsGet()
+    {
+        $data = array(
+            'test_param1' => 'Testing1',
+            'test_param2' => 'Testing2'
+        );
+
+        $captureRequest = $this->getCaptureRequest();
+        $captureRequest->setMethod('GET');
+        $captureRequest->setRequestData($data);
+
+        $this->assertSame('', $captureRequest->getBody());
+    }
+
+    /**
+     * Test get body returns and empty
+     * string if method is HEAD.
+     *
+     * @access public
+     * @return void
+     */
+    public function testGetBodyReturnsAnEmptyStringIfMethodIsHead()
+    {
+        $data = array(
+            'test_param1' => 'Testing1',
+            'test_param2' => 'Testing2'
+        );
+
+        $captureRequest = $this->getCaptureRequest();
+        $captureRequest->setMethod('HEAD');
+        $captureRequest->setRequestData($data);
+
+        $this->assertSame('', $captureRequest->getBody());
+    }
+
+    /**
+     * Test get body returns query string if
+     * method is not HEAD or GET.
+     *
+     * @access public
+     * @return void
+     */
+    public function testGetBodyReturnsQueryStringIfMethodIsNotHeadOrGet()
+    {
+        $data = array(
+            'test_param1' => 'Testing1',
+            'test_param2' => 'Testing2'
+        );
+
+        $captureRequest = $this->getCaptureRequest();
+        $captureRequest->setMethod('POST');
+        $captureRequest->setRequestData($data);
+
+        $body = 'test_param1=Testing1&test_param2=Testing2';
+
+        $this->assertSame($body, $captureRequest->getBody());
+    }
+
+    /**
+     * Test get request data returns flattened
+     * request data if flatten is set to true.
+     *
+     * @access public
+     * @return void
+     */
+    public function testGetRequestDataReturnsFlattenedRequestDataIfFlattenIsSetToTrue()
+    {
+        $data = array(
+            'test_param1' => 'Testing1',
+            'test_param2' => array(
+                'Testing2',
+                'Testing3'
+            )
+        );
+
+        $captureRequest = $this->getCaptureRequest();
+        $captureRequest->setRequestData($data);
+
+        $flatData = array(
+            'test_param1'    => 'Testing1',
+            'test_param2[0]' => 'Testing2',
+            'test_param2[1]' => 'Testing3'
+        );
+
+        $this->assertSame($flatData, $captureRequest->getRequestData(true));
+    }
+
+    /**
+     * Test get request data returns unflattened
+     * request data if flatten is set to false
+     *
+     * @access public
+     * @return void
+     */
+    public function testGetRequestDataReturnsUnflattenedRequestDataIfFlattenIsSetToFalse()
+    {
+        $data = array(
+            'test_param1' => 'Testing1',
+            'test_param2' => array(
+                'Testing2',
+                'Testing3'
+            )
+        );
+
+        $captureRequest = $this->getCaptureRequest();
+        $captureRequest->setRequestData($data);
+
+        $this->assertSame($data, $captureRequest->getRequestData(false));
+    }
+
+    /**
+     * Test add headers merge headers with
+     * existing headers.
+     *
+     * @access public
+     * @return void
+     */
+    public function testAddHeadersMergesHeadersWithExistingHeaders()
+    {
+        $existingHeaders = array(
+            'Header1' => 'Header 1'
+        );
+
+        $newHeaders = array(
+            'Header2' => 'Header 2',
+            'Header3' => 'Header 3'
+        );
+
+        $captureRequest = $this->getCaptureRequest();
+        $captureRequest->setHeaders($existingHeaders);
+        $captureRequest->addHeaders($newHeaders);
+
+        $expectedHeaders = array_merge($existingHeaders, $newHeaders);
+
+        $this->assertSame($expectedHeaders, $captureRequest->getHeaders());
+    }
+
+    /**
+     * Test get headers returns JSON encoded
+     * headers if format is set to JSON.
+     *
+     * @access public
+     * @return void
+     */
+    public function testGetHeadersReturnsJsonEncodedHeadersIfFormatIsSetToJson()
+    {
+        $headers = array(
+            'Header1' => 'Header 1',
+            'Header2' => 'Header 2'
+        );
+
+        $captureRequest = $this->getCaptureRequest();
+        $captureRequest->setHeaders($headers);
+
+        $expectedHeaders = json_encode($headers);
+
+        $this->assertSame($expectedHeaders, $captureRequest->getHeaders('json'));
+    }
+
+    /**
+     * Test get headers returns headers as
+     * array if format is not set to json
+     *
+     * @access public
+     * @return void
+     */
+    public function testGetHeadersReturnsHeadersAsArrayIfFormatIsNotSetToJson()
+    {
+        $headers = array(
+            'Header1' => 'Header 1',
+            'Header2' => 'Header 2'
+        );
+
+        $captureRequest = $this->getCaptureRequest();
+        $captureRequest->setHeaders($headers);
+
+        $this->assertSame($headers, $captureRequest->getHeaders('default'));
+    }
+
+    /**
+     * Test set capture file throws not
+     * writable exception if file path
+     * is not writable.
+     *
+     * @access public
+     * @return void
+     */
+    public function testSetCaptureFileThrowsNotWriteableExceptionIfFilePathIsNotWriteable()
+    {
+        $this->setExpectedException('\JonnyW\PhantomJs\Exception\NotWritableException');
+
+        $invalidPath = '/invalid/path';
+
+        $captureRequest = $this->getCaptureRequest();
+        $captureRequest->setCaptureFile($invalidPath);
+    }
+
+    /**
+     * Test get capture file returns capture
+     * file if capture file is set.
+     *
+     * @access public
+     * @return void
+     */
+    public function testGetCaptureFileReturnsCaptureFileIfCaptureFileIsSet()
+    {
+        $captureFile = sprintf('%s/test.jpg', sys_get_temp_dir());
+
+        $captureRequest = $this->getCaptureRequest();
+        $captureRequest->setCaptureFile($captureFile);
+
+        $this->assertSame($captureFile, $captureRequest->getCaptureFile());
+    }
+
+/** +++++++++++++++++++++++++++++++++++ **/
+/** ++++++++++ TEST ENTITIES ++++++++++ **/
+/** +++++++++++++++++++++++++++++++++++ **/
+
+    /**
+     * Get capture request instance.
+     *
+     * @access protected
+     * @param  string                                   $url     (default: null)
+     * @param  string                                   $method  (default: RequestInterface::METHOD_GET)
+     * @param  int                                      $timeout (default: 5000)
+     * @return \JonnyW\PhantomJs\Message\CaptureRequest
+     */
+    protected function getCaptureRequest($url = null, $method = RequestInterface::METHOD_GET, $timeout = 5000)
+    {
+        $captureRequest = new CaptureRequest($url, $method, $timeout);
+
+        return $captureRequest;
+    }
+}

+ 195 - 0
src/JonnyW/PhantomJs/Tests/Unit/Message/MessageFactoryTest.php

@@ -0,0 +1,195 @@
+<?php
+
+/*
+ * This file is part of the php-phantomjs.
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+namespace JonnyW\PhantomJs\Tests\Unit\Message;
+
+use JonnyW\PhantomJs\Message\MessageFactory;
+
+/**
+ * PHP PhantomJs
+ *
+ * @author Jon Wenmoth <contact@jonnyw.me>
+ */
+class MessageFactoryTest extends \PHPUnit_Framework_TestCase
+{
+
+/** +++++++++++++++++++++++++++++++++++ **/
+/** ++++++++++++++ TESTS ++++++++++++++ **/
+/** +++++++++++++++++++++++++++++++++++ **/
+
+    /**
+     * Test get instance returns instance of
+     * message factory.
+     *
+     * @access public
+     * @return void
+     */
+    public function testGetInstanceReturnsInstanceOfMessageFactory()
+    {
+        $this->assertInstanceOf('\JonnyW\PhantomJs\Message\MessageFactory', MessageFactory::getInstance());
+    }
+
+    /**
+     * Test create request returns instance of request.
+     *
+     * @access public
+     * @return void
+     */
+    public function testCreateRequestReturnsInstanceOfRequest()
+    {
+        $messageFactory = $this->getMessageFactory();
+
+        $this->assertInstanceOf('\JonnyW\PhantomJs\Message\Request', $messageFactory->createRequest());
+    }
+
+    /**
+     * Test create request with URL sets
+     * URL in request.
+     *
+     * @access public
+     * @return void
+     */
+    public function testCreateRequestWithUrlSetsUrlInRequest()
+    {
+        $url = 'http://test.com';
+
+        $messageFactory = $this->getMessageFactory();
+        $request        = $messageFactory->createRequest($url);
+
+        $this->assertSame($url, $request->getUrl());
+    }
+
+    /**
+     * Test create request with method sets
+     * method in request.
+     *
+     * @access public
+     * @return void
+     */
+    public function testCreateRequestWithMethodSetsMethodInRequest()
+    {
+        $method = 'POST';
+
+        $messageFactory = $this->getMessageFactory();
+        $request        = $messageFactory->createRequest(null, $method);
+
+        $this->assertSame($method, $request->getMethod());
+    }
+
+    /**
+     * Test create request with timeout sets
+     * timeout in request.
+     *
+     * @access public
+     * @return void
+     */
+    public function testCreateRequestWithTimeoutSetsTimeoutInRequest()
+    {
+        $timeout = 123456789;
+
+        $messageFactory = $this->getMessageFactory();
+        $request        = $messageFactory->createRequest(null, 'GET', $timeout);
+
+        $this->assertSame($timeout, $request->getTimeout());
+    }
+
+    /**
+     * Test create capture request returns
+     * instance of capture request.
+     *
+     * @access public
+     * @return void
+     */
+    public function testCreateCaptureRequestReturnsInstanceOfCaptureRequest()
+    {
+        $messageFactory = $this->getMessageFactory();
+
+        $this->assertInstanceOf('\JonnyW\PhantomJs\Message\CaptureRequest', $messageFactory->createCaptureRequest());
+    }
+
+    /**
+     * Test create capture request with URL sets
+     * URL in capture request.
+     *
+     * @access public
+     * @return void
+     */
+    public function testCreateCaptureRequestWithUrlSetsUrlInCaptureRequest()
+    {
+        $url = 'http://test.com';
+
+        $messageFactory = $this->getMessageFactory();
+        $captureRequest = $messageFactory->createCaptureRequest($url);
+
+        $this->assertSame($url, $captureRequest->getUrl());
+    }
+
+    /**
+     * Test create capture request with method sets
+     * method in capture request.
+     *
+     * @access public
+     * @return void
+     */
+    public function testCreateCaptureRequestWithMethodSetsMethodInCaptureRequest()
+    {
+        $method = 'POST';
+
+        $messageFactory = $this->getMessageFactory();
+        $captureRequest = $messageFactory->createCaptureRequest(null, $method);
+
+        $this->assertSame($method, $captureRequest->getMethod());
+    }
+
+    /**
+     * Test create capture request with timeout sets
+     * timeout in capture request.
+     *
+     * @access public
+     * @return void
+     */
+    public function testCreateCaptureRequestWithTimeoutSetsTimeoutInCaptureRequest()
+    {
+        $timeout = 123456789;
+
+        $messageFactory = $this->getMessageFactory();
+        $captureRequest = $messageFactory->createCaptureRequest(null, 'GET', $timeout);
+
+        $this->assertSame($timeout, $captureRequest->getTimeout());
+    }
+
+    /**
+     * Test create response returns instance of response.
+     *
+     * @access public
+     * @return void
+     */
+    public function testCreateResponseReturnsInstanceOfResponse()
+    {
+        $messageFactory = $this->getMessageFactory();
+
+        $this->assertInstanceOf('\JonnyW\PhantomJs\Message\Response', $messageFactory->createResponse());
+    }
+
+/** +++++++++++++++++++++++++++++++++++ **/
+/** ++++++++++ TEST ENTITIES ++++++++++ **/
+/** +++++++++++++++++++++++++++++++++++ **/
+
+    /**
+     * Get message factory instance.
+     *
+     * @access protected
+     * @return \JonnyW\PhantomJs\Message\MessageFactory
+     */
+    protected function getMessageFactory()
+    {
+        $messageFactory = new MessageFactory();
+
+        return $messageFactory;
+    }
+}

+ 452 - 0
src/JonnyW/PhantomJs/Tests/Unit/Message/RequestTest.php

@@ -0,0 +1,452 @@
+<?php
+
+/*
+ * This file is part of the php-phantomjs.
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+namespace JonnyW\PhantomJs\Tests\Unit\Message;
+
+use JonnyW\PhantomJs\Message\Request;
+use JonnyW\PhantomJs\Message\RequestInterface;
+
+/**
+ * PHP PhantomJs
+ *
+ * @author Jon Wenmoth <contact@jonnyw.me>
+ */
+class RequestTest extends \PHPUnit_Framework_TestCase
+{
+
+/** +++++++++++++++++++++++++++++++++++ **/
+/** ++++++++++++++ TESTS ++++++++++++++ **/
+/** +++++++++++++++++++++++++++++++++++ **/
+
+    /**
+     * Test get type returns default request
+     * type if not type is set.
+     *
+     * @access public
+     * @return void
+     */
+    public function testGetTypeReturnsDefaultRequestTypeIfNoTypeIsSet()
+    {
+        $request = $this->getRequest();
+
+        $this->assertSame(RequestInterface::REQUEST_TYPE_DEFAULT, $request->getType());
+    }
+
+    /**
+     * Test get type returns set request
+     * type.
+     *
+     * @access public
+     * @return void
+     */
+    public function testGetTypeReturnsSetRequestType()
+    {
+        $requestType = 'testType';
+
+        $request = $this->getRequest();
+        $request->setType($requestType);
+
+        $this->assertSame($requestType, $request->getType());
+    }
+
+    /**
+     * Test URL can be set via constructor.
+     *
+     * @access public
+     * @return void
+     */
+    public function testUrlCanBeSetViaConstructor()
+    {
+        $url     = 'http://test.com';
+        $request = $this->getRequest($url);
+
+        $this->assertSame($url, $request->getUrl());
+    }
+
+    /**
+     * Test method can be set via constructor.
+     *
+     * @access public
+     * @return void
+     */
+    public function testMethodCanBeSetViaConstructor()
+    {
+        $method  = 'GET';
+        $request = $this->getRequest(null, $method);
+
+        $this->assertSame($method, $request->getMethod());
+    }
+
+    /**
+     * Test timeout can be set via constructor.
+     *
+     * @access public
+     * @return void
+     */
+    public function testTimeoutCanBeSetViaConstructor()
+    {
+        $timeout = 100000;
+        $request = $this->getRequest('http://test.com', 'GET', $timeout);
+
+        $this->assertSame($timeout, $request->getTimeout());
+    }
+
+    /**
+     * Test set method throws invalid method
+     * exception if an invalid method is set
+     *
+     * @access public
+     * @return void
+     */
+    public function testSetMethodThrowsInvalidMethodExceptionIfAnInvalidMethodIsSet()
+    {
+        $this->setExpectedException('\JonnyW\PhantomJs\Exception\InvalidMethodException');
+
+        $request = $this->getRequest();
+        $request->setMethod('INVALID_METHOD');
+    }
+
+  /**
+     * Test set method throws invalid method
+     * exception if an invalid method is set
+     * via constructor
+     *
+     * @access public
+     * @return void
+     */
+    public function testSetMethodThrowsInvalidMethodExceptionIfAnInvalidMethodIsSetViaConstructor()
+    {
+        $this->setExpectedException('\JonnyW\PhantomJs\Exception\InvalidMethodException');
+
+        $this->getRequest('http://test.com', 'INVALID_METHOD');
+    }
+
+    /**
+     * Test set URL throws invalid
+     * URL exception if an invalid
+     * URL is set.
+     *
+     * @access public
+     * @return void
+     */
+    public function testSetUrlThrowsInvalidUrlExceptionIfAnInvalidUrlIsSet()
+    {
+        $this->setExpectedException('\JonnyW\PhantomJs\Exception\InvalidUrlException');
+
+        $request = $this->getRequest();
+        $request->setUrl('\\AnInvalidUrl');
+    }
+
+    /**
+     * Test get URL returns URL without query
+     * paramters if method is not HEAD or GET.
+     *
+     * @access public
+     * @return void
+     */
+    public function testGetUrlReturnsUrlWithoutQueryParametersIfMethodIsNotHeadOrGet()
+    {
+        $url = 'http://test.com';
+
+        $data = array(
+            'test_param1' => 'Testing1',
+            'test_param2' => 'Testing2'
+        );
+
+        $request = $this->getRequest();
+        $request->setMethod('POST');
+        $request->setUrl($url);
+        $request->setRequestData($data);
+
+        $this->assertSame($url, $request->getUrl());
+    }
+
+    /**
+     * Test get URL returns URL with query
+     * parameters if method is GET.
+     *
+     * @access public
+     * @return void
+     */
+    public function testGetUrlReturnsUrlWithQueryParametersIfMethodIsGet()
+    {
+        $url = 'http://test.com';
+
+        $data = array(
+            'test_param1' => 'Testing1',
+            'test_param2' => 'Testing2'
+        );
+
+        $request = $this->getRequest();
+        $request->setMethod('GET');
+        $request->setUrl($url);
+        $request->setRequestData($data);
+
+        $expectedUrl = $url . '?test_param1=Testing1&test_param2=Testing2';
+
+        $this->assertSame($expectedUrl, $request->getUrl());
+    }
+
+    /**
+     * Test get URL returns URL with query
+     * parameters if method is HEAD.
+     *
+     * @access public
+     * @return void
+     */
+    public function testGetUrlReturnsUrlWithQueryParametersIfMethodIsHead()
+    {
+        $url = 'http://test.com';
+
+        $data = array(
+            'test_param1' => 'Testing1',
+            'test_param2' => 'Testing2'
+        );
+
+        $request = $this->getRequest();
+        $request->setMethod('HEAD');
+        $request->setUrl($url);
+        $request->setRequestData($data);
+
+        $expectedUrl = $url . '?test_param1=Testing1&test_param2=Testing2';
+
+        $this->assertSame($expectedUrl, $request->getUrl());
+    }
+
+    /**
+     * Test get URL returns URL with query
+     * parameters if method is HEAD.
+     *
+     * @access public
+     * @return void
+     */
+    public function testGetUrlAppendsQueryParametersIfUrlHasExistingQueryParameters()
+    {
+        $url = 'http://test.com?existing_param=Existing';
+
+        $data = array(
+            'test_param1' => 'Testing1',
+            'test_param2' => 'Testing2'
+        );
+
+        $request = $this->getRequest();
+        $request->setMethod('GET');
+        $request->setUrl($url);
+        $request->setRequestData($data);
+
+        $expectedUrl = $url . '&test_param1=Testing1&test_param2=Testing2';
+
+        $this->assertSame($expectedUrl, $request->getUrl());
+    }
+
+    /**
+     * Test get body returns an empty
+     * string if method is GET.
+     *
+     * @access public
+     * @return void
+     */
+    public function testGetBodyReturnsAnEmptyStringIfMethodIsGet()
+    {
+        $data = array(
+            'test_param1' => 'Testing1',
+            'test_param2' => 'Testing2'
+        );
+
+        $request = $this->getRequest();
+        $request->setMethod('GET');
+        $request->setRequestData($data);
+
+        $this->assertSame('', $request->getBody());
+    }
+
+    /**
+     * Test get body returns and empty
+     * string if method is HEAD.
+     *
+     * @access public
+     * @return void
+     */
+    public function testGetBodyReturnsAnEmptyStringIfMethodIsHead()
+    {
+        $data = array(
+            'test_param1' => 'Testing1',
+            'test_param2' => 'Testing2'
+        );
+
+        $request = $this->getRequest();
+        $request->setMethod('HEAD');
+        $request->setRequestData($data);
+
+        $this->assertSame('', $request->getBody());
+    }
+
+    /**
+     * Test get body returns query string if
+     * method is not HEAD or GET.
+     *
+     * @access public
+     * @return void
+     */
+    public function testGetBodyReturnsQueryStringIfMethodIsNotHeadOrGet()
+    {
+        $data = array(
+            'test_param1' => 'Testing1',
+            'test_param2' => 'Testing2'
+        );
+
+        $request = $this->getRequest();
+        $request->setMethod('POST');
+        $request->setRequestData($data);
+
+        $body = 'test_param1=Testing1&test_param2=Testing2';
+
+        $this->assertSame($body, $request->getBody());
+    }
+
+    /**
+     * Test get request data returns flattened
+     * request data if flatten is set to true.
+     *
+     * @access public
+     * @return void
+     */
+    public function testGetRequestDataReturnsFlattenedRequestDataIfFlattenIsSetToTrue()
+    {
+        $data = array(
+            'test_param1' => 'Testing1',
+            'test_param2' => array(
+                'Testing2',
+                'Testing3'
+            )
+        );
+
+        $request = $this->getRequest();
+        $request->setRequestData($data);
+
+        $flatData = array(
+            'test_param1'    => 'Testing1',
+            'test_param2[0]' => 'Testing2',
+            'test_param2[1]' => 'Testing3'
+        );
+
+        $this->assertSame($flatData, $request->getRequestData(true));
+    }
+
+    /**
+     * Test get request data returns unflattened
+     * request data if flatten is set to false
+     *
+     * @access public
+     * @return void
+     */
+    public function testGetRequestDataReturnsUnflattenedRequestDataIfFlattenIsSetToFalse()
+    {
+        $data = array(
+            'test_param1' => 'Testing1',
+            'test_param2' => array(
+                'Testing2',
+                'Testing3'
+            )
+        );
+
+        $request = $this->getRequest();
+        $request->setRequestData($data);
+
+        $this->assertSame($data, $request->getRequestData(false));
+    }
+
+    /**
+     * Test add headers merge headers with
+     * existing headers.
+     *
+     * @access public
+     * @return void
+     */
+    public function testAddHeadersMergesHeadersWithExistingHeaders()
+    {
+        $existingHeaders = array(
+            'Header1' => 'Header 1'
+        );
+
+        $newHeaders = array(
+            'Header2' => 'Header 2',
+            'Header3' => 'Header 3'
+        );
+
+        $request = $this->getRequest();
+        $request->setHeaders($existingHeaders);
+        $request->addHeaders($newHeaders);
+
+        $expectedHeaders = array_merge($existingHeaders, $newHeaders);
+
+        $this->assertSame($expectedHeaders, $request->getHeaders());
+    }
+
+    /**
+     * Test get headers returns JSON encoded
+     * headers if format is set to JSON.
+     *
+     * @access public
+     * @return void
+     */
+    public function testGetHeadersReturnsJsonEncodedHeadersIfFormatIsSetToJson()
+    {
+        $headers = array(
+            'Header1' => 'Header 1',
+            'Header2' => 'Header 2'
+        );
+
+        $request = $this->getRequest();
+        $request->setHeaders($headers);
+
+        $expectedHeaders = json_encode($headers);
+
+        $this->assertSame($expectedHeaders, $request->getHeaders('json'));
+    }
+
+    /**
+     * Test get headers returns headers as
+     * array if format is not set to json
+     *
+     * @access public
+     * @return void
+     */
+    public function testGetHeadersReturnsHeadersAsArrayIfFormatIsNotSetToJson()
+    {
+        $headers = array(
+            'Header1' => 'Header 1',
+            'Header2' => 'Header 2'
+        );
+
+        $request = $this->getRequest();
+        $request->setHeaders($headers);
+
+        $this->assertSame($headers, $request->getHeaders('default'));
+    }
+
+/** +++++++++++++++++++++++++++++++++++ **/
+/** ++++++++++ TEST ENTITIES ++++++++++ **/
+/** +++++++++++++++++++++++++++++++++++ **/
+
+    /**
+     * Get request instance.
+     *
+     * @access protected
+     * @param  string                            $url     (default: null)
+     * @param  string                            $method  (default: RequestInterface::METHOD_GET)
+     * @param  int                               $timeout (default: 5000)
+     * @return \JonnyW\PhantomJs\Message\Request
+     */
+    protected function getRequest($url = null, $method = RequestInterface::METHOD_GET, $timeout = 5000)
+    {
+        $request = new Request($url, $method, $timeout);
+
+        return $request;
+    }
+}

+ 389 - 0
src/JonnyW/PhantomJs/Tests/Unit/Message/ResponseTest.php

@@ -0,0 +1,389 @@
+<?php
+
+/*
+ * This file is part of the php-phantomjs.
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+namespace JonnyW\PhantomJs\Tests\Unit\Message;
+
+use JonnyW\PhantomJs\Message\Response;
+
+/**
+ * PHP PhantomJs
+ *
+ * @author Jon Wenmoth <contact@jonnyw.me>
+ */
+class ResponseTest extends \PHPUnit_Framework_TestCase
+{
+
+/** +++++++++++++++++++++++++++++++++++ **/
+/** ++++++++++++++ TESTS ++++++++++++++ **/
+/** +++++++++++++++++++++++++++++++++++ **/
+
+    /**
+     * Test import sets status in response.
+     *
+     * @access public
+     * @return void
+     */
+    public function testImportSetsStatusInResponse()
+    {
+        $data = array(
+            'status' => 200
+        );
+
+        $response = $this->getResponse();
+        $response->import($data);
+
+        $this->assertSame(200, $response->getStatus());
+    }
+
+    /**
+     * Test import sets content in response.
+     *
+     * @access public
+     * @return void
+     */
+    public function testImportSetsContentInResponse()
+    {
+        $data = array(
+            'content' => 'Test content'
+        );
+
+        $response = $this->getResponse();
+        $response->import($data);
+
+        $this->assertSame('Test content', $response->getContent());
+    }
+
+    /**
+     * Test import sets content in response.
+     *
+     * @access public
+     * @return void
+     */
+    public function testImportSetsContentTypeInResponse()
+    {
+        $data = array(
+            'contentType' => 'text/html'
+        );
+
+        $response = $this->getResponse();
+        $response->import($data);
+
+        $this->assertSame('text/html', $response->getContentType());
+    }
+
+    /**
+     * Test import sets URL in response.
+     *
+     * @access public
+     * @return void
+     */
+    public function testImportSetsUrlInResponse()
+    {
+        $data = array(
+            'url' => 'http://test.com'
+        );
+
+        $response = $this->getResponse();
+        $response->import($data);
+
+        $this->assertSame('http://test.com', $response->getUrl());
+    }
+
+    /**
+     * Test import sets redirect URL in response.
+     *
+     * @access public
+     * @return void
+     */
+    public function testImportSetsRedirectUrlInResponse()
+    {
+        $data = array(
+            'redirectUrl' => 'http://test.com'
+        );
+
+        $response = $this->getResponse();
+        $response->import($data);
+
+        $this->assertSame('http://test.com', $response->getRedirectUrl());
+    }
+
+    /**
+     * Test import sets time in response.
+     *
+     * @access public
+     * @return void
+     */
+    public function testImportSetsTimeInResponse()
+    {
+        $data = array(
+            'time' => 123456789
+        );
+
+        $response = $this->getResponse();
+        $response->import($data);
+
+        $this->assertSame(123456789, $response->getTime());
+    }
+
+    /**
+     * Test import sets headers in response.
+     *
+     * @access public
+     * @return void
+     */
+    public function testImportSetsHeadersInResponse()
+    {
+        $headers = array(
+            array(
+                'name'  => 'Header1',
+                'value' => 'Test Header 1'
+            )
+        );
+
+        $data = array(
+            'headers' => $headers
+        );
+
+        $response = $this->getResponse();
+        $response->import($data);
+
+        $expectedHeaders = array(
+            $headers[0]['name'] => $headers[0]['value']
+        );
+
+        $this->assertSame($expectedHeaders, $response->getHeaders());
+    }
+
+    /**
+     * Test get header returns null if header
+     * is not set.
+     *
+     * @access public
+     * @return void
+     */
+    public function testGetHeadersReturnsNullIfHeaderIsNotSet()
+    {
+        $response = $this->getResponse();
+
+        $this->assertNull($response->getHeader('invalid_header'));
+    }
+
+    /**
+     * Test get header returns header if
+     * header is set.
+     *
+     * @access public
+     * @return void
+     */
+    public function testGetHeaderReturnsHeaderIfHeaderIsSet()
+    {
+        $headers = array(
+            array(
+                'name'  => 'Header1',
+                'value' => 'Test Header 1'
+            )
+        );
+
+        $data = array(
+            'headers' => $headers
+        );
+
+        $response = $this->getResponse();
+        $response->import($data);
+
+        $this->assertSame('Test Header 1', $response->getHeader('Header1'));
+    }
+
+    /**
+     * Test is redirect returns true if
+     * status equals 300.
+     *
+     * @access public
+     * @return void
+     */
+    public function testIsRedirectReturnsTrueIfStatusEquals300()
+    {
+        $data = array(
+            'status' => 300
+        );
+
+        $response = $this->getResponse();
+        $response->import($data);
+
+        $this->assertTrue($response->isRedirect());
+    }
+
+    /**
+     * Test is redirect returns true if
+     * status equals 301.
+     *
+     * @access public
+     * @return void
+     */
+    public function testIsRedirectReturnsTrueIfStatusEquals301()
+    {
+        $data = array(
+            'status' => 301
+        );
+
+        $response = $this->getResponse();
+        $response->import($data);
+
+        $this->assertTrue($response->isRedirect());
+    }
+
+    /**
+     * Test is redirect returns true if
+     * status equals 302.
+     *
+     * @access public
+     * @return void
+     */
+    public function testIsRedirectReturnsTrueIfStatusEquals302()
+    {
+        $data = array(
+            'status' => 302
+        );
+
+        $response = $this->getResponse();
+        $response->import($data);
+
+        $this->assertTrue($response->isRedirect());
+    }
+
+    /**
+     * Test is redirect returns true if
+     * status equals 303.
+     *
+     * @access public
+     * @return void
+     */
+    public function testIsRedirectReturnsTrueIfStatusEquals303()
+    {
+        $data = array(
+            'status' => 303
+        );
+
+        $response = $this->getResponse();
+        $response->import($data);
+
+        $this->assertTrue($response->isRedirect());
+    }
+
+    /**
+     * Test is redirect returns true if
+     * status equals 304.
+     *
+     * @access public
+     * @return void
+     */
+    public function testIsRedirectReturnsTrueIfStatusEquals304()
+    {
+        $data = array(
+            'status' => 304
+        );
+
+        $response = $this->getResponse();
+        $response->import($data);
+
+        $this->assertTrue($response->isRedirect());
+    }
+
+    /**
+     * Test is redirect returns true if
+     * status equals 305.
+     *
+     * @access public
+     * @return void
+     */
+    public function testIsRedirectReturnsTrueIfStatusEquals305()
+    {
+        $data = array(
+            'status' => 305
+        );
+
+        $response = $this->getResponse();
+        $response->import($data);
+
+        $this->assertTrue($response->isRedirect());
+    }
+
+    /**
+     * Test is redirect returns true if
+     * status equals 306.
+     *
+     * @access public
+     * @return void
+     */
+    public function testIsRedirectReturnsTrueIfStatusEquals306()
+    {
+        $data = array(
+            'status' => 306
+        );
+
+        $response = $this->getResponse();
+        $response->import($data);
+
+        $this->assertTrue($response->isRedirect());
+    }
+
+    /**
+     * Test is redirect returns true if
+     * status equals 307.
+     *
+     * @access public
+     * @return void
+     */
+    public function testIsRedirectReturnsTrueIfStatusEquals307()
+    {
+        $data = array(
+            'status' => 307
+        );
+
+        $response = $this->getResponse();
+        $response->import($data);
+
+        $this->assertTrue($response->isRedirect());
+    }
+
+    /**
+     * Test is redirect returns false if status
+     * code is not a valid redirect code.
+     *
+     * @access public
+     * @return void
+     */
+    public function testIsRedirectReturnsFalseIfStatusCodeIsNotAValidRedirectCode()
+    {
+        $data = array(
+            'status' => 401
+        );
+
+        $response = $this->getResponse();
+        $response->import($data);
+
+        $this->assertFalse($response->isRedirect());
+    }
+
+/** +++++++++++++++++++++++++++++++++++ **/
+/** ++++++++++ TEST ENTITIES ++++++++++ **/
+/** +++++++++++++++++++++++++++++++++++ **/
+
+    /**
+     * Get response instance.
+     *
+     * @access protected
+     * @return \JonnyW\PhantomJs\Message\Response
+     */
+    protected function getResponse()
+    {
+        $response = new Response();
+
+        return $response;
+    }
+}

+ 209 - 0
src/JonnyW/PhantomJs/Tests/Unit/Parser/JsonParserTest.php

@@ -0,0 +1,209 @@
+<?php
+
+/*
+ * This file is part of the php-phantomjs.
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+namespace JonnyW\PhantomJs\Tests\Unit\Parser;
+
+use JonnyW\PhantomJs\Parser\JsonParser;
+
+/**
+ * PHP PhantomJs
+ *
+ * @author Jon Wenmoth <contact@jonnyw.me>
+ */
+class JsonParserTest extends \PHPUnit_Framework_TestCase
+{
+
+/*****************/
+/***** TESTS *****/
+/*****************/
+
+    /**
+     * Test parse returns array if data
+     * is null.
+     *
+     * @access public
+     * @return void
+     */
+    public function testParseReturnsArrayIfDataIsNull()
+    {
+        $data = null;
+
+        $jsonParser = $this->getJsonParser();
+
+        $this->assertInternalType('array', $jsonParser->parse($data));
+    }
+
+    /**
+     * Test parse returns array if data
+     * is not a string.
+     *
+     * @access public
+     * @return void
+     */
+    public function testParseReturnsArrayIfDataIsNotAString()
+    {
+        $data = new \stdClass();
+
+        $jsonParser = $this->getJsonParser();
+
+        $this->assertInternalType('array', $jsonParser->parse($data));
+    }
+
+    /**
+     * Test parse returns array if data
+     * is invalid JSON format.
+     *
+     * @access public
+     * @return void
+     */
+    public function testParseReturnsArrayIfDataIsInvalidJsonFormat()
+    {
+        $data = 'Invalid JSON format';
+
+        $jsonParser = $this->getJsonParser();
+
+        $this->assertInternalType('array', $jsonParser->parse($data));
+    }
+
+    /**
+     * Test parse returns array if data
+     * is broken json format.
+     *
+     * @access public
+     * @return void
+     */
+    public function testParseReturnsArrayIfDataIsBrokenJsonFormat()
+    {
+        $data = '{data: Unquoted string}';
+
+        $jsonParser = $this->getJsonParser();
+
+        $this->assertInternalType('array', $jsonParser->parse($data));
+    }
+
+    /**
+     * Test parse returns array if data
+     * is valid JSON object.
+     *
+     * @access public
+     * @return void
+     */
+    public function testParseReturnsArrayIfDataIsValidJsonObject()
+    {
+        $data = '{"data": "Test data"}';
+
+        $jsonParser = $this->getJsonParser();
+
+        $this->assertInternalType('array', $jsonParser->parse($data));
+    }
+
+    /**
+     * Test parse returns array if data
+     * is valid JSON array.
+     *
+     * @access public
+     * @return void
+     */
+    public function testParseReturnsArrayIfDataIsValidJsonArray()
+    {
+        $data = '["Test data"]';
+
+        $jsonParser = $this->getJsonParser();
+
+        $this->assertInternalType('array', $jsonParser->parse($data));
+    }
+
+    /**
+     * Test parse successfully parses data
+     * if data is valid JSON object.
+     *
+     * @access public
+     * @return void
+     */
+    public function testParseSuccessfullyParsesDataIfDataIsValidJsonObject()
+    {
+        $data = '{"data": "Test data"}';
+
+        $jsonParser = $this->getJsonParser();
+        $parsedData = $jsonParser->parse($data);
+
+        $expectedData = array(
+            'data' => 'Test data'
+        );
+
+        $this->assertSame($parsedData, $expectedData);
+    }
+
+    /**
+     * Test parse successfully parses data
+     * if data is valid JSON array.
+     *
+     * @access public
+     * @return void
+     */
+    public function testParseSuccessfullyParsesDataIfDataIsValidJsonArray()
+    {
+        $data = '["Test data"]';
+
+        $jsonParser = $this->getJsonParser();
+        $parsedData = $jsonParser->parse($data);
+
+        $expectedData = array(
+            'Test data'
+        );
+
+        $this->assertSame($parsedData, $expectedData);
+    }
+
+    /**
+     * Test parse successfully parses
+     * multidimensional data if data is
+     * valid JSON format.
+     *
+     * @access public
+     * @return void
+     */
+    public function testParseSuccessfullyParsesMultidimensionalDataIfDataIsValidJsonFormat()
+    {
+         $data = '{
+            "data": {
+                "data": { "data": "Test data" },
+                "more_data": "More test data"
+            }
+         }';
+
+        $jsonParser = $this->getJsonParser();
+        $parsedData = $jsonParser->parse($data);
+
+        $expectedData = array(
+            'data' => array(
+                'data'      => array( 'data' => 'Test data' ),
+                'more_data' => 'More test data'
+            )
+        );
+
+        $this->assertSame($parsedData, $expectedData);
+    }
+
+/** +++++++++++++++++++++++++++++++++++ **/
+/** ++++++++++ TEST ENTITIES ++++++++++ **/
+/** +++++++++++++++++++++++++++++++++++ **/
+
+    /**
+     * Get JSON parser instance.
+     *
+     * @access protected
+     * @return \JonnyW\PhantomJs\Parser\JsonParser
+     */
+    protected function getJsonParser()
+    {
+        $jsonParser = new JsonParser();
+
+        return $jsonParser;
+    }
+}

+ 156 - 0
src/JonnyW/PhantomJs/Tests/Unit/Procedure/ChainProcedureLoaderTest.php

@@ -0,0 +1,156 @@
+<?php
+
+/*
+ * This file is part of the php-phantomjs.
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+namespace JonnyW\PhantomJs\Tests\Unit\Procedure;
+
+use JonnyW\PhantomJs\Procedure\ChainProcedureLoader;
+
+/**
+ * PHP PhantomJs
+ *
+ * @author Jon Wenmoth <contact@jonnyw.me>
+ */
+class ChainProcedureLoaderTest extends \PHPUnit_Framework_TestCase
+{
+
+/** +++++++++++++++++++++++++++++++++++ **/
+/** ++++++++++++++ TESTS ++++++++++++++ **/
+/** +++++++++++++++++++++++++++++++++++ **/
+
+    /**
+     * Test load throws invalid argument exception
+     * if no valid procedure loader could be found.
+     *
+     * @access public
+     * @return void
+     */
+    public function testLoadThrowsInvalidArgumentExceptionIfNoValidProcedureLoaderCouldBeFound()
+    {
+        $this->setExpectedException('\InvalidArgumentException');
+
+        $procedureLoaders = array();
+
+        $chainProcedureLoader = $this->getChainProcedureLoader($procedureLoaders);
+        $chainProcedureLoader->load('test');
+    }
+
+    /**
+     * Test load throws invalid argument exception if
+     * procedure loader throws exception.
+     *
+     * @access public
+     * @return void
+     */
+    public function testLoadThrowsInvalidArgumentExceptionIfProcedureLoaderThrowsException()
+    {
+        $this->setExpectedException('\InvalidArgumentException');
+
+        $procedureLoader = $this->getProcedureLoader();
+        $procedureLoader->expects($this->any())
+            ->method('load')
+            ->will($this->throwException(new \Exception()));
+
+        $procedureLoaders = array(
+            $procedureLoader
+        );
+
+        $chainProcedureLoader = $this->getChainProcedureLoader($procedureLoaders);
+        $chainProcedureLoader->load('test');
+    }
+
+    /**
+     * Test load returns instance of procedure.
+     *
+     * @access public
+     * @return void
+     */
+    public function testLoadReturnsInstanceOfProcedure()
+    {
+        $procedure = $this->getProcedure();
+
+        $procedureLoader = $this->getProcedureLoader();
+        $procedureLoader->expects($this->any())
+            ->method('load')
+            ->will($this->returnValue($procedure));
+
+        $procedureLoaders = array(
+            $procedureLoader
+        );
+
+        $chainProcedureLoader = $this->getChainProcedureLoader($procedureLoaders);
+
+        $this->assertInstanceOf('\JonnyW\PhantomJs\Procedure\ProcedureInterface', $chainProcedureLoader->load('test'));
+    }
+
+    /**
+     * Test add loader adds procedure loader
+     * to chain loader.
+     *
+     * @access public
+     * @return void
+     */
+    public function testAddLoaderAddsProcedureLoaderToChainLoader()
+    {
+        $chainProcedureLoader = $this->getChainProcedureLoader(array());
+
+        $procedureLoader =  $this->getProcedureLoader();
+        $procedureLoader->expects($this->once())
+            ->method('load');
+
+        $chainProcedureLoader->addLoader($procedureLoader);
+        $chainProcedureLoader->load('test');
+    }
+
+/** +++++++++++++++++++++++++++++++++++ **/
+/** ++++++++++ TEST ENTITIES ++++++++++ **/
+/** +++++++++++++++++++++++++++++++++++ **/
+
+    /**
+     * Get chain procedure loader instance.
+     *
+     * @access protected
+     * @param  array                                            $procedureLoaders
+     * @return \JonnyW\PhantomJs\Procedure\ChainProcedureLoader
+     */
+    protected function getChainProcedureLoader(array $procedureLoaders)
+    {
+        $chainProcedureLoader = new ChainProcedureLoader($procedureLoaders);
+
+        return $chainProcedureLoader;
+    }
+
+/** +++++++++++++++++++++++++++++++++++ **/
+/** ++++++++++ MOCKS / STUBS ++++++++++ **/
+/** +++++++++++++++++++++++++++++++++++ **/
+
+    /**
+     * Get mock procedure loader instance.
+     *
+     * @access protected
+     * @return \JonnyW\PhantomJs\Procedure\ProcedureLoaderInterface
+     */
+    protected function getProcedureLoader()
+    {
+        $mockProcedureLoader = $this->getMock('\JonnyW\PhantomJs\Procedure\ProcedureLoaderInterface');
+
+        return $mockProcedureLoader;
+    }
+
+    /**
+     * Get mock procedure instance.
+     *
+     * @access protected
+     * @return \JonnyW\PhantomJs\Procedure\ProcedureInterface
+     */
+    protected function getProcedure()
+    {
+        $mockProcedure = $this->getMock('\JonnyW\PhantomJs\Procedure\ProcedureInterface');
+
+        return $mockProcedure;
+    }
+}

+ 108 - 0
src/JonnyW/PhantomJs/Tests/Unit/Procedure/ProcedureFactoryTest.php

@@ -0,0 +1,108 @@
+<?php
+
+/*
+ * This file is part of the php-phantomjs.
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+namespace JonnyW\PhantomJs\Tests\Unit\Procedure;
+
+use JonnyW\PhantomJs\Cache\CacheInterface;
+use JonnyW\PhantomJs\Parser\ParserInterface;
+use JonnyW\PhantomJs\Template\TemplateRendererInterface;
+use JonnyW\PhantomJs\Procedure\ProcedureFactory;
+
+/**
+ * PHP PhantomJs
+ *
+ * @author Jon Wenmoth <contact@jonnyw.me>
+ */
+class ProcedureFactoryTest extends \PHPUnit_Framework_TestCase
+{
+
+/** +++++++++++++++++++++++++++++++++++ **/
+/** ++++++++++++++ TESTS ++++++++++++++ **/
+/** +++++++++++++++++++++++++++++++++++ **/
+
+    /**
+     * Test create procedure returns instance
+     * of procedure.
+     *
+     * @access public
+     * @return void
+     */
+    public function testCreateProcedureReturnsInstanceOfProcedure()
+    {
+        $parser    = $this->getParser();
+        $cache     = $this->getCache();
+        $renderer  = $this->getRenderer();
+
+        $procedureFactory = $this->getProcedureFactory($parser, $cache, $renderer);
+
+        $this->assertInstanceOf('\JonnyW\PhantomJs\Procedure\Procedure', $procedureFactory->createProcedure());
+    }
+
+/** +++++++++++++++++++++++++++++++++++ **/
+/** ++++++++++ TEST ENTITIES ++++++++++ **/
+/** +++++++++++++++++++++++++++++++++++ **/
+
+    /**
+     * Get procedure factory instance.
+     *
+     * @access protected
+     * @param  \JonnyW\PhantomJs\Parser\ParserInterface             $parser
+     * @param  \JonnyW\PhantomJs\Cache\CacheInterface               $cacheHandler
+     * @param  \JonnyW\PhantomJs\Template\TemplateRendererInterface $renderer
+     * @return \JonnyW\PhantomJs\Procedure\ProcedureFactory
+     */
+    protected function getProcedureFactory(ParserInterface $parser, CacheInterface $cacheHandler, TemplateRendererInterface $renderer)
+    {
+        $procedureFactory = new ProcedureFactory($parser, $cacheHandler, $renderer);
+
+        return $procedureFactory;
+    }
+
+/** +++++++++++++++++++++++++++++++++++ **/
+/** ++++++++++ MOCKS / STUBS ++++++++++ **/
+/** +++++++++++++++++++++++++++++++++++ **/
+
+    /**
+     * Get mock parser instance.
+     *
+     * @access protected
+     * @return \JonnyW\PhantomJs\Parser\ParserInterface
+     */
+    protected function getParser()
+    {
+        $mockParser = $this->getMock('\JonnyW\PhantomJs\Parser\ParserInterface');
+
+        return $mockParser;
+    }
+
+    /**
+     * Get mock cache instance.
+     *
+     * @access protected
+     * @return \JonnyW\PhantomJs\Cache\CacheInterface
+     */
+    protected function getCache()
+    {
+        $mockCache = $this->getMock('\JonnyW\PhantomJs\Cache\CacheInterface');
+
+        return $mockCache;
+    }
+
+    /**
+     * Get mock template renderer instance.
+     *
+     * @access protected
+     * @return \JonnyW\PhantomJs\Template\TemplateRendererInterface
+     */
+    protected function getRenderer()
+    {
+        $mockTemplateRenderer = $this->getMock('\JonnyW\PhantomJs\Template\TemplateRendererInterface');
+
+        return $mockTemplateRenderer;
+    }
+}

+ 120 - 0
src/JonnyW/PhantomJs/Tests/Unit/Procedure/ProcedureLoaderFactoryTest.php

@@ -0,0 +1,120 @@
+<?php
+
+/*
+ * This file is part of the php-phantomjs.
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+namespace JonnyW\PhantomJs\Tests\Unit\Procedure;
+
+use JonnyW\PhantomJs\Procedure\ProcedureFactoryInterface;
+use JonnyW\PhantomJs\Procedure\ProcedureLoaderFactory;
+
+/**
+ * PHP PhantomJs
+ *
+ * @author Jon Wenmoth <contact@jonnyw.me>
+ */
+class ProcedureLoaderFactoryTest extends \PHPUnit_Framework_TestCase
+{
+    /**
+     * Test directory
+     *
+     * @var string
+     * @access protected
+     */
+    protected $directory;
+
+/** +++++++++++++++++++++++++++++++++++ **/
+/** ++++++++++++++ TESTS ++++++++++++++ **/
+/** +++++++++++++++++++++++++++++++++++ **/
+
+    /**
+     * Test create procedure loader throws invalid
+     * argument exception if directory is not readable.
+     *
+     * @access public
+     * @return void
+     */
+    public function testCreateProcedureLoaderThrowsInvalidArgumentExceptionIfDirectoryIsNotReadable()
+    {
+        $this->setExpectedException('\InvalidArgumentException');
+
+        $procedureFactory = $this->getProcedureFactory();
+
+        $procedureLoaderFactory = $this->getProcedureLoaderFactory($procedureFactory);
+        $procedureLoaderFactory->createProcedureLoader('invalid/directory');
+    }
+
+    /**
+     * Test create procedure loader returns instance of
+     * procedure loader.
+     *
+     * @access public
+     * @return void
+     */
+    public function testCreateProcedureLoaderReturnsInstanceOfProcedureLoader()
+    {
+        $procedureFactory = $this->getProcedureFactory();
+
+        $procedureLoaderFactory = $this->getProcedureLoaderFactory($procedureFactory);
+        $procedureLoader = $procedureLoaderFactory->createProcedureLoader($this->directory);
+
+        $this->assertInstanceOf('\JonnyW\PhantomJs\Procedure\ProcedureLoaderInterface', $procedureLoader);
+    }
+
+/** +++++++++++++++++++++++++++++++++++ **/
+/** ++++++++++ TEST ENTITIES ++++++++++ **/
+/** +++++++++++++++++++++++++++++++++++ **/
+
+    /**
+     * Get procedure loader factory instance.
+     *
+     * @access public
+     * @param  \JonnyW\PhantomJs\Procedure\ProcedureFactoryInterface $procedureFactory
+     * @return \JonnyW\PhantomJs\Procedure\ProcedureLoaderFactory
+     */
+    protected function getProcedureLoaderFactory(ProcedureFactoryInterface $procedureFactory)
+    {
+        $procedureLoaderFactory = new ProcedureLoaderFactory($procedureFactory);
+
+        return $procedureLoaderFactory;
+    }
+
+/** +++++++++++++++++++++++++++++++++++ **/
+/** ++++++++++ MOCKS / STUBS ++++++++++ **/
+/** +++++++++++++++++++++++++++++++++++ **/
+
+    /**
+     * Get mock procedure factory instance.
+     *
+     * @access protected
+     * @return \JonnyW\PhantomJs\Procedure\ProcedureFactoryInterface
+     */
+    protected function getProcedureFactory()
+    {
+        $mockProcedureFactory = $this->getMock('\JonnyW\PhantomJs\Procedure\ProcedureFactoryInterface');
+
+        return $mockProcedureFactory;
+    }
+
+/** +++++++++++++++++++++++++++++++++++ **/
+/** ++++++++++++ UTILITIES ++++++++++++ **/
+/** +++++++++++++++++++++++++++++++++++ **/
+
+    /**
+     * Set up test environment.
+     *
+     * @access public
+     * @return void
+     */
+    public function setUp()
+    {
+        $this->directory = sys_get_temp_dir();
+
+        if (!is_readable($this->directory)) {
+            throw new \RuntimeException(sprintf('Test directory must be readable: %s', $this->directory));
+        }
+    }
+}

+ 269 - 0
src/JonnyW/PhantomJs/Tests/Unit/Procedure/ProcedureLoaderTest.php

@@ -0,0 +1,269 @@
+<?php
+
+/*
+ * This file is part of the php-phantomjs.
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+namespace JonnyW\PhantomJs\Tests\Unit\Procedure;
+
+use Symfony\Component\Config\FileLocatorInterface;
+use JonnyW\PhantomJs\Procedure\ProcedureFactoryInterface;
+use JonnyW\PhantomJs\Procedure\ProcedureLoader;
+
+/**
+ * PHP PhantomJs
+ *
+ * @author Jon Wenmoth <contact@jonnyw.me>
+ */
+class ProcedureLoaderTest extends \PHPUnit_Framework_TestCase
+{
+    /**
+     * Test filename
+     *
+     * @var string
+     * @access protected
+     */
+    protected $filename;
+
+    /**
+     * Test directory
+     *
+     * @var string
+     * @access protected
+     */
+    protected $directory;
+
+/** +++++++++++++++++++++++++++++++++++ **/
+/** ++++++++++++++ TESTS ++++++++++++++ **/
+/** +++++++++++++++++++++++++++++++++++ **/
+
+    /**
+     * Test load throws invalid argument exception
+     * if file is not a local stream.
+     *
+     * @access public
+     * @return void
+     */
+    public function testLoadThrowsInvalidArgumentExceptionIfFileIsNotALocalStream()
+    {
+        $this->setExpectedException('\InvalidArgumentException');
+
+        $procedureFactory = $this->getProcedureFactory();
+        $fileLocator      = $this->getFileLocator();
+
+        $fileLocator->expects($this->any())
+            ->method('locate')
+            ->will($this->returnValue('http://example.com/index.html'));
+
+        $procedureLoader = $this->getProcedureLoader($procedureFactory, $fileLocator);
+        $procedureLoader->load('test');
+    }
+
+    /**
+     * Test load throws not exists exception if
+     * file does not exist.
+     *
+     * @access public
+     * @return void
+     */
+    public function testLoadThrowsNotExistsExceptionIfFileDoesNotExist()
+    {
+        $this->setExpectedException('\JonnyW\PhantomJs\Exception\NotExistsException');
+
+        $procedureFactory = $this->getProcedureFactory();
+        $fileLocator      = $this->getFileLocator();
+
+        $fileLocator->expects($this->any())
+            ->method('locate')
+            ->will($this->returnValue('/invalid/file.proc'));
+
+        $procedureLoader = $this->getProcedureLoader($procedureFactory, $fileLocator);
+        $procedureLoader->load('test');
+    }
+
+    /**
+     * Test load returns procedure instance.
+     *
+     * @access public
+     * @return void
+     */
+    public function testLoadReturnsProcedureInstance()
+    {
+        $body = 'PROCEDURE BODY';
+        $file = $this->writeProcedure($body);
+
+        $procedureFactory = $this->getProcedureFactory();
+        $fileLocator      = $this->getFileLocator();
+        $procedure        = $this->getProcedure();
+
+        $fileLocator->expects($this->any())
+            ->method('locate')
+            ->will($this->returnValue($file));
+
+        $procedureFactory->expects($this->any())
+            ->method('createProcedure')
+            ->will($this->returnValue($procedure));
+
+        $procedureLoader = $this->getProcedureLoader($procedureFactory, $fileLocator);
+
+        $this->assertInstanceOf('\JonnyW\PhantomJs\Procedure\ProcedureInterface', $procedureLoader->load('test'));
+    }
+
+    /**
+     * Test load sets procedure body in
+     * procedure instance.
+     *
+     * @access public
+     * @return void
+     */
+    public function testLoadSetsProcedureBodyInProcedureInstance()
+    {
+        $body = 'PROCEDURE BODY';
+        $file = $this->writeProcedure($body);
+
+        $procedureFactory = $this->getProcedureFactory();
+        $fileLocator      = $this->getFileLocator();
+        $procedure        = $this->getProcedure();
+
+        $fileLocator->expects($this->any())
+            ->method('locate')
+            ->will($this->returnValue($file));
+
+        $procedureFactory->expects($this->any())
+            ->method('createProcedure')
+            ->will($this->returnValue($procedure));
+
+        $procedureLoader = $this->getProcedureLoader($procedureFactory, $fileLocator);
+
+        $this->assertSame($body, $procedureLoader->load('test')->getProcedure());
+    }
+
+/** +++++++++++++++++++++++++++++++++++ **/
+/** ++++++++++ TEST ENTITIES ++++++++++ **/
+/** +++++++++++++++++++++++++++++++++++ **/
+
+    /**
+     * Get procedure loader instance.
+     *
+     * @access public
+     * @param  \JonnyW\PhantomJs\Procedure\ProcedureFactoryInterface $procedureFactory
+     * @param  \Symfony\Component\Config\FileLocatorInterface        $locator
+     * @return \JonnyW\PhantomJs\Procedure\ProcedureLoader
+     */
+    protected function getProcedureLoader(ProcedureFactoryInterface $procedureFactory, FileLocatorInterface $locator)
+    {
+        $procedureLoader = new ProcedureLoader($procedureFactory, $locator);
+
+        return $procedureLoader;
+    }
+
+/** +++++++++++++++++++++++++++++++++++ **/
+/** ++++++++++ MOCKS / STUBS ++++++++++ **/
+/** +++++++++++++++++++++++++++++++++++ **/
+
+    /**
+     * Get mock procedure factory instance.
+     *
+     * @access protected
+     * @return \JonnyW\PhantomJs\Procedure\ProcedureFactoryInterface
+     */
+    protected function getProcedureFactory()
+    {
+        $mockProcedureFactory = $this->getMock('\JonnyW\PhantomJs\Procedure\ProcedureFactoryInterface');
+
+        return $mockProcedureFactory;
+    }
+
+    /**
+     * Get mock file locator instance.
+     *
+     * @access protected
+     * @return \Symfony\Component\Config\FileLocatorInterface
+     */
+    protected function getFileLocator()
+    {
+        $mockFileLocator = $this->getMock('\Symfony\Component\Config\FileLocatorInterface');
+
+        return $mockFileLocator;
+    }
+
+    /**
+     * Get mock procedure instance.
+     *
+     * @access protected
+     * @return \JonnyW\PhantomJs\Procedure\Procedure
+     */
+    protected function getProcedure()
+    {
+        $mockProcedure = $this->getMockBuilder('\JonnyW\PhantomJs\Procedure\Procedure')
+            ->setMethods(null)
+            ->disableOriginalConstructor()
+            ->getMock();
+
+        return $mockProcedure;
+    }
+
+/** +++++++++++++++++++++++++++++++++++ **/
+/** ++++++++++++ UTILITIES ++++++++++++ **/
+/** +++++++++++++++++++++++++++++++++++ **/
+
+    /**
+     * Set up test environment.
+     *
+     * @access public
+     * @return void
+     */
+    public function setUp()
+    {
+        $this->filename  = 'test.proc';
+        $this->directory = sys_get_temp_dir();
+
+        if (!is_writable($this->directory)) {
+            throw new \RuntimeException(sprintf('Test directory must be writable: %s', $this->directory));
+        }
+    }
+
+    /**
+     * Tear down test environment.
+     *
+     * @access public
+     * @return void
+     */
+    public function tearDown()
+    {
+        $filename = $this->getFilename();
+
+        if (file_exists($filename)) {
+            unlink($filename);
+        }
+    }
+
+    /**
+     * Get test filename.
+     *
+     * @access public
+     * @return string
+     */
+    public function getFilename()
+    {
+        return sprintf('%1$s/%2$s', $this->directory, $this->filename);
+    }
+
+    /**
+     * Write procedure body to file.
+     *
+     * @access public
+     * @param  string $data
+     * @return string
+     */
+    public function writeProcedure($procedure)
+    {
+        $filename = $this->getFilename();
+
+        file_put_contents($filename, $procedure);
+
+        return $filename;
+    }
+}

+ 205 - 0
src/JonnyW/PhantomJs/Tests/Unit/Procedure/ProcedureTest.php

@@ -0,0 +1,205 @@
+<?php
+
+/*
+ * This file is part of the php-phantomjs.
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+namespace JonnyW\PhantomJs\Tests\Unit\Procedure;
+
+use JonnyW\PhantomJs\Cache\CacheInterface;
+use JonnyW\PhantomJs\Parser\ParserInterface;
+use JonnyW\PhantomJs\Template\TemplateRendererInterface;
+use JonnyW\PhantomJs\Procedure\Procedure;
+
+/**
+ * PHP PhantomJs
+ *
+ * @author Jon Wenmoth <contact@jonnyw.me>
+ */
+class ProcedureTest extends \PHPUnit_Framework_TestCase
+{
+
+/** +++++++++++++++++++++++++++++++++++ **/
+/** ++++++++++++++ TESTS ++++++++++++++ **/
+/** +++++++++++++++++++++++++++++++++++ **/
+
+    /**
+     * Test load procedure sets procedure
+     * body in procedure instance.
+     *
+     * @access public
+     * @return void
+     */
+    public function testLoadProcedureSetsProcedureBodyInProcedureInstance()
+    {
+        $template = 'TEST PRODCEDURE';
+
+        $parser    = $this->getParser();
+        $cache     = $this->getCache();
+        $renderer  = $this->getRenderer();
+
+        $procedure = $this->getProcedure($parser, $cache, $renderer);
+        $procedure->load($template);
+
+        $this->assertSame($procedure->getProcedure(), $template);
+    }
+
+    /**
+     * Test run throws note writeable exception
+     * if procedure executable file cannot
+     * be written.
+     *
+     * @access public
+     * @return void
+     */
+    public function testRunThrowsNotWriteableExceptionIfProcedureExecutableFileCannotBeWritten()
+    {
+        $this->setExpectedException('\JonnyW\PhantomJs\Exception\NotWritableException');
+
+        $parser   = $this->getParser();
+        $renderer = $this->getRenderer();
+
+        $cache = $this->getCache();
+        $cache->expects($this->once())
+            ->method('save')
+            ->will($this->throwException(new \JonnyW\PhantomJs\Exception\NotWritableException()));
+
+        $client   = $this->getClient();
+        $request  = $this->getRequest();
+        $response = $this->getResponse();
+
+        $procedure = $this->getProcedure($parser, $cache, $renderer);
+        $procedure->run($client, $request, $response);
+    }
+
+    /**
+     * Test run throws procedure failed exception
+     * if an exception is encountered.
+     *
+     * @access public
+     * @return void
+     */
+    public function testRunThrowsProcedureFailedExceptionIfAnExceptionIsEncountered()
+    {
+        $this->setExpectedException('\JonnyW\PhantomJs\Exception\ProcedureFailedException');
+
+        $parser = $this->getParser();
+        $cache  = $this->getCache();
+
+        $renderer = $this->getRenderer();
+        $renderer->expects($this->once())
+            ->method('render')
+            ->will($this->throwException(new \Exception()));
+
+        $client   = $this->getClient();
+        $request  = $this->getRequest();
+        $response = $this->getResponse();
+
+        $procedure = $this->getProcedure($parser, $cache, $renderer);
+        $procedure->run($client, $request, $response);
+    }
+
+/** +++++++++++++++++++++++++++++++++++ **/
+/** ++++++++++ TEST ENTITIES ++++++++++ **/
+/** +++++++++++++++++++++++++++++++++++ **/
+
+    /**
+     * Get procedure instance.
+     *
+     * @access protected
+     * @param  \JonnyW\PhantomJs\Parser\ParserInterface             $parser
+     * @param  \JonnyW\PhantomJs\Cache\CacheInterface               $cacheHandler
+     * @param  \JonnyW\PhantomJs\Template\TemplateRendererInterface $renderer
+     * @return \JonnyW\PhantomJs\Procedure\Procedure
+     */
+    protected function getProcedure(ParserInterface $parser, CacheInterface $cacheHandler, TemplateRendererInterface $renderer)
+    {
+        $procedure = new Procedure($parser, $cacheHandler, $renderer);
+
+        return $procedure;
+    }
+
+/** +++++++++++++++++++++++++++++++++++ **/
+/** ++++++++++ MOCKS / STUBS ++++++++++ **/
+/** +++++++++++++++++++++++++++++++++++ **/
+
+    /**
+     * Get mock parser instance.
+     *
+     * @access protected
+     * @return \JonnyW\PhantomJs\Parser\ParserInterface
+     */
+    protected function getParser()
+    {
+        $mockParser = $this->getMock('\JonnyW\PhantomJs\Parser\ParserInterface');
+
+        return $mockParser;
+    }
+
+    /**
+     * Get mock cache instance.
+     *
+     * @access protected
+     * @return \JonnyW\PhantomJs\Cache\CacheInterface
+     */
+    protected function getCache()
+    {
+        $mockCache = $this->getMock('\JonnyW\PhantomJs\Cache\CacheInterface');
+
+        return $mockCache;
+    }
+
+    /**
+     * Get mock template renderer instance.
+     *
+     * @access protected
+     * @return \JonnyW\PhantomJs\Template\TemplateRendererInterface
+     */
+    protected function getRenderer()
+    {
+        $mockTemplateRenderer = $this->getMock('\JonnyW\PhantomJs\Template\TemplateRendererInterface');
+
+        return $mockTemplateRenderer;
+    }
+
+    /**
+     * Get mock client instance.
+     *
+     * @access protected
+     * @return \JonnyW\PhantomJs\ClientInterface
+     */
+    protected function getClient()
+    {
+        $mockClient = $this->getMock('\JonnyW\PhantomJs\ClientInterface');
+
+        return $mockClient;
+    }
+
+    /**
+     * Get mock request instance.
+     *
+     * @access protected
+     * @return \JonnyW\PhantomJs\Message\RequestInterface
+     */
+    protected function getRequest()
+    {
+        $mockRequest = $this->getMock('\JonnyW\PhantomJs\Message\RequestInterface');
+
+        return $mockRequest;
+    }
+
+    /**
+     * Get mock response instance.
+     *
+     * @access protected
+     * @return \JonnyW\PhantomJs\Message\ResponseInterface
+     */
+    protected function getResponse()
+    {
+        $mockResponse = $this->getMock('\JonnyW\PhantomJs\Message\ResponseInterface');
+
+        return $mockResponse;
+    }
+}

+ 1 - 3
test/bootstrap.php → src/JonnyW/PhantomJs/Tests/bootstrap.php

@@ -1,6 +1,6 @@
 <?php
 
-if (!$loader = @include __DIR__.'/../vendor/autoload.php') {
+if (!$loader = @include __DIR__.'/../../../../vendor/autoload.php') {
     echo <<<EOM
 You must set up the project dependencies by running the following commands:
 
@@ -11,5 +11,3 @@ EOM;
 
     exit(1);
 }
-
-$loader->add('JonnyW\PhantomJs\Test', __DIR__);

+ 0 - 143
test/JonnyW/PhantomJs/Test/ClientTest.php

@@ -1,143 +0,0 @@
-<?php
-
-/*
- * This file is part of the php-phantomjs.
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-namespace JonnyW\PhantomJs\Test;
-
-use JonnyW\PhantomJs\Client;
-
-/**
- * PHP PhantomJs
- *
- * @author Jon Wenmoth <contact@jonnyw.me>
- */
-class ClientTest extends \PHPUnit_Framework_TestCase
-{
-    /**
-     * Client instance
-     *
-     * @var \JonnyW\PhantomJs\Client
-     */
-    protected $client;
-
-    /**
-     * Setup tests
-     *
-     * @return void
-     */
-    protected function setUp()
-    {
-        parent::setUp();
-
-        $this->client = Client::getInstance();
-    }
-
-    /**
-     * Test get factory instance
-     *
-     * @return void
-     */
-    public function testMessageFactoryInstance()
-    {
-        $factory = $this->client->getMessageFactory();
-
-        $this->assertInstanceOf('JonnyW\PhantomJs\Message\FactoryInterface', $factory);
-    }
-
-    /**
-     * Test exception is thrown when
-     * PhantomJS executable cannot be run
-     *
-     * @return void
-     */
-    public function testBinNotExecutable()
-    {
-        $this->setExpectedException('JonnyW\PhantomJs\Exception\NoPhantomJsException');
-
-        $this->client->setPhantomJs('/path/does/not/exist/phantomjs');
-    }
-
-    /**
-     * Test exception is thrown when capture
-     * path is not writeable
-     *
-     * @return void
-     */
-    public function testPathNotWriteable()
-    {
-        $this->setExpectedException('JonnyW\PhantomJs\Exception\NotWriteableException');
-
-        $request  = $this->getMock('JonnyW\PhantomJs\Message\Request', null, array('method' => 'GET', 'url' => 'http://jonnyw.me'));
-        $response  = $this->getMock('JonnyW\PhantomJs\Message\Response', null);
-
-        $this->client->send($request, $response, 'path/does/not/exist/phantoms.png');
-    }
-
-    /**
-     * Test open page
-     *
-     * @return void
-     */
-    public function testOpenPage()
-    {
-        $client  = $this->getMock('JonnyW\PhantomJs\Client', array('request'));
-        $request  = $this->getMock('JonnyW\PhantomJs\Message\Request', null, array('method' => 'GET', 'url' => 'http://jonnyw.me'));
-        $response  = $this->getMock('JonnyW\PhantomJs\Message\Response', null);
-
-        $client->expects($this->once())
-        ->method('request')
-        ->will($this->returnValue($response));
-
-        $actual = $client->send($request, $response);
-
-        $this->assertSame($response, $actual);
-    }
-
-    /**
-     * Test screen capture page
-     *
-     * @return void
-     */
-    public function testCapturePage()
-    {
-        $client  = $this->getMock('JonnyW\PhantomJs\Client', array('request'));
-        $request  = $this->getMock('JonnyW\PhantomJs\Message\Request', null, array('method' => 'GET', 'url' => 'http://jonnyw.me'));
-        $response  = $this->getMock('JonnyW\PhantomJs\Message\Response', null);
-
-        $client->expects($this->once())
-        ->method('request')
-        ->will($this->returnValue($response));
-
-        $actual = $client->send($request, $response, '/tmp/testing.png');
-
-        $this->assertSame($response, $actual);
-    }
-
-    /**
-     * Test page is redirect
-     *
-     * @return void
-     */
-    public function testRedirectPage()
-    {
-        $client  = $this->getMock('JonnyW\PhantomJs\Client', array('request'));
-        $request  = $this->getMock('JonnyW\PhantomJs\Message\Request', null, array('method' => 'GET', 'url' => 'http://jonnyw.me'));
-        $response  = $this->getMock('JonnyW\PhantomJs\Message\Response', array('getStatus'));
-
-        $client->expects($this->once())
-        ->method('request')
-        ->will($this->returnValue($response));
-
-        $response->expects($this->once())
-        ->method('getStatus')
-        ->will($this->returnValue(301));
-
-        $actual = $client->send($request, $response);
-
-        $this->assertTrue($actual->isRedirect());
-    }
-}

+ 0 - 62
test/JonnyW/PhantomJs/Test/Message/FactoryTest.php

@@ -1,62 +0,0 @@
-<?php
-
-/*
- * This file is part of the php-phantomjs.
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-namespace JonnyW\PhantomJs\Test\Message;
-
-use JonnyW\PhantomJs\Message\Factory;
-
-/**
- * PHP PhantomJs
- *
- * @author Jon Wenmoth <contact@jonnyw.me>
- */
-class FactoryTest extends \PHPUnit_Framework_TestCase
-{
-    /**
-     * Factory instance
-     *
-     * @var \JonnyW\PhantomJs\Message\Factory
-     */
-    protected $factory;
-
-    /**
-     * Setup tests
-     *
-     * @return void
-     */
-    protected function setUp()
-    {
-        parent::setUp();
-
-        $this->factory = Factory::getInstance();
-    }
-
-    /**
-     * Test create request instance
-     *
-     * @return void
-     */
-    public function testRequestInstance()
-    {
-        $request = $this->factory->createRequest();
-
-        $this->assertInstanceOf('JonnyW\PhantomJs\Message\RequestInterface', $request);
-    }
-
-    /**
-     * Test create response instance
-     *
-     * @return void
-     */
-    public function testResponseInstance()
-    {
-        $response = $this->factory->createResponse();
-
-        $this->assertInstanceOf('JonnyW\PhantomJs\Message\ResponseInterface', $response);
-    }
-}

+ 0 - 139
test/JonnyW/PhantomJs/Test/Message/RequestTest.php

@@ -1,139 +0,0 @@
-<?php
-
-/*
- * This file is part of the php-phantomjs.
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-namespace JonnyW\PhantomJs\Test\Message;
-
-use JonnyW\PhantomJs\Message\Request;
-
-/**
- * PHP PhantomJs
- *
- * @author Jon Wenmoth <contact@jonnyw.me>
- */
-class RequestTest extends \PHPUnit_Framework_TestCase
-{
-    /**
-     * Test invalid URL's throw exception
-     *
-     * @dataProvider provideInvalidHosts
-     *
-     * @param  string $url
-     * @return void
-     */
-    public function testInvalidUrl($url)
-    {
-        $this->setExpectedException('JonnyW\\PhantomJs\\Exception\\InvalidUrlException');
-
-        $request = new Request();
-        $request->setUrl($url);
-    }
-
-    /**
-     * Invalid host data providers
-     *
-     * @return array
-     */
-    public function provideInvalidHosts()
-    {
-        return array(
-            array('invalid_url'),
-            array('invalid_url.test')
-        );
-    }
-
-    /**
-     * Test invalid methods throw exception
-     *
-     * @dataProvider provideInvalidMethods
-     *
-     * @param  string $method
-     * @return void
-     */
-    public function testInvalidMethod($method)
-    {
-        $this->setExpectedException('JonnyW\\PhantomJs\\Exception\\InvalidMethodException');
-
-        $request = new Request();
-        $request->setMethod($method);
-    }
-
-    /**
-     * Invalid method data providers
-     *
-     * @return array
-     */
-    public function provideInvalidMethods()
-    {
-        return array(
-            array('GOT'),
-            array('FIND')
-        );
-    }
-
-    /**
-     * Test invalid methods throw exception
-     *
-     * @return void
-     */
-    public function testGetRequestBody()
-    {
-        $data = array('name' => 'jonnyw', 'email' => 'contact@jonnyw.me');
-
-        $request = new Request('GET', 'http://jonnyw.me');
-        $request->setRequestData($data);
-
-        $this->assertEmpty($request->getBody());
-    }
-
-    /**
-     * Test GET URL parameters when URL
-     * does not have existing parameters
-     *
-     * @return void
-     */
-    public function testGetUrlQuery()
-    {
-        $data = array('name' => 'jonnyw', 'email' => 'contact@jonnyw.me');
-
-        $request = new Request('GET', 'http://jonnyw.me?query=true');
-        $request->setRequestData($data);
-
-        $this->assertEquals($request->getUrl(), 'http://jonnyw.me?query=true&' . urldecode(http_build_query($data)));
-    }
-
-    /**
-     * Test GET URL parameters when URL
-     * does not have existing parameters
-     *
-     * @return void
-     */
-    public function testGetUrlQueryClean()
-    {
-        $data = array('name' => 'jonnyw', 'email' => 'contact@jonnyw.me');
-
-        $request = new Request('GET', 'http://jonnyw.me');
-        $request->setRequestData($data);
-
-        $this->assertEquals($request->getUrl(), 'http://jonnyw.me?' . urldecode(http_build_query($data)));
-    }
-
-    /**
-     * Test invalid methods throw exception
-     *
-     * @return void
-     */
-    public function testPostRequestBody()
-    {
-        $data = array('name' => 'jonnyw', 'email' => 'contact@jonnyw.me');
-
-        $request = new Request('POST', 'http://jonnyw.me');
-        $request->setRequestData($data);
-
-        $this->assertEquals($request->getBody(), urldecode(http_build_query($data)));
-    }
-}