Skip to content
Browse files

Initial commit.

  • Loading branch information...
0 parents commit a4d19b914b2299cd8b6e37ed29abc964e1f2b08f @aflatter committed
Showing with 798 additions and 0 deletions.
  1. +3 −0 .gitignore
  2. +27 −0 README.md
  3. +31 −0 examples/google.js
  4. +203 −0 lib/client.js
  5. +38 −0 lib/client_mock.js
  6. +85 −0 lib/endpoint.js
  7. +111 −0 lib/exception.js
  8. +102 −0 lib/session.js
  9. +12 −0 lib/webdriver.js
  10. +22 −0 package.json
  11. +59 −0 test/endpoint_test.js
  12. +92 −0 test/session_test.js
  13. +13 −0 test/webdriver_test.js
3 .gitignore
@@ -0,0 +1,3 @@
+*.swp
+*.swo
+node_modules
27 README.md
@@ -0,0 +1,27 @@
+A webdriver client library for nodejs with focus on a fluent API and graceful error handling.
+
+Getting started
+===============
+
+ var webdriver = require('webdriver')
+ , client, endpoint, opts;
+
+ endpoint = webdriver.endpoint({host: 'localhost', port: 9515});
+
+ endpoint.status(function(err, result) {
+ console.log(result);
+ });
+
+ endpoint.session({browserName: 'chrome'}, function(err, session) {
+ session.url('http://example.com', function(err) {
+ var getLocation = function() {
+ return window.location.href;
+ };
+
+ session.execute(getLocation, function(err, res) {
+ assert.equal(res, 'http://example.com');
+ });
+ });
+ });
+
+You might want to take a look at the examples directory.
31 examples/google.js
@@ -0,0 +1,31 @@
+var wd = require('webdriver');
+
+wd.endpoint().session(function(err, session) {
+ if (err) {
+ throw err;
+ }
+
+ session.url('http://google.com', function(err) {
+ var fn = function() { return window.location.href; }
+
+ if (err) {
+ throw err;
+ }
+
+ session.execute(fn, function(err, res) {
+ if (err) {
+ throw err;
+ }
+
+ console.log("Current location is: %s", res);
+
+ session.quit(function() {
+ if (err) {
+ throw err;
+ }
+
+ console.log('Success!');
+ });
+ });
+ });
+});
203 lib/client.js
@@ -0,0 +1,203 @@
+var http = require('http')
+ , underscore = require('underscore')
+ , clone = underscore.clone
+ , Exception = require('./exception');
+
+var Client = module.exports = constructor
+ , proto = Client.prototype = {};
+
+Client.create = function() {
+ var instance = Object.create(proto);
+ constructor.apply(instance, arguments);
+ return instance;
+}
+
+/**
+ * Constructor
+ */
+function constructor(opts) {
+ opts = opts || {};
+
+ this.host = opts.host || 'localhost';
+ this.port = opts.port || 9515;
+ this.path = opts.path || '';
+
+ underscore.bindAll.call(underscore, [this] + this._bound);
+};
+
+/**
+ * Specifies which methods are bound to the instance.
+ */
+proto._bound = [
+ '_requestDidFail'
+ , '_requestDidError'
+ , '_requestDidComplete'
+];
+
+/**
+ * Default options for HTTP requests.
+ */
+proto.defaults = {
+ encoding: 'utf-8'
+ , agent: false
+ , headers: {
+ 'Accept': '*/*'
+ , 'Content-Type': 'application/json;charset=utf-8'
+ }
+};
+
+proto.get = function(resource, params, callback) {
+ if (typeof params === 'function') {
+ callback = params;
+ params = null;
+ }
+
+ this.request('GET', resource, params, callback);
+};
+
+proto.post = function(resource, params, callback) {
+ if (typeof params === 'function') {
+ callback = params;
+ params = null;
+ }
+
+ this.request('POST', resource, params, callback);
+};
+
+/**
+ * Low level method for sending a command to the server.
+ *
+ * @param {String} method The HTTP method to use.
+ * @param {String} resource Path to append to the base url, e.g. '/status'.
+ * @param {Object} [params] Optional parameters to send.
+ * @param {Function} callback The callback to invoke when the request finishes.
+ */
+proto.request = function(method, resource, data, callback) {
+ var client = this
+ , options = clone(this.defaults)
+ , req;
+
+ options.host = this.host;
+ options.port = this.port;
+ options.path = this.path + resource;
+ options.method = method;
+
+ if (data) {
+ data = JSON.stringify(data);
+ options.headers['Content-Length'] = data.length;
+ }
+
+ req = http.request(options)
+
+ // Buffer up the response body and proceed.
+ req.on('response', function(res) {
+ var body = "";
+
+ res.on('data', function(chunk) {
+ body += chunk;
+ });
+
+ res.on('end', function() {
+ client._requestDidComplete(res, body, callback);
+ });
+ });
+
+ req.on('error', function(error) {
+ client._requestDidError(req, error, callback);
+ });
+
+ req.end(data);
+};
+
+/**
+ * Called when the request fails on HTTP level or below.
+ *
+ * @param {http.ClientRequest} req
+ * @param {Error} error
+ * @param {Function} callback
+ * @private
+ */
+proto._requestDidError = function(req, error, callback) {
+ callback(error);
+};
+
+/**
+ * Called when the HTTP request completed successfully.
+
+ * @param {http.ClientResponse} res
+ * @param {Error} error
+ * @param {Function} callback
+ * @private
+ */
+proto._requestDidComplete = function(res, body, callback) {
+ var code = res.statusCode
+ , type = res.headers['content-type'] || ""
+ , error, data;
+
+ // A status between 400 and 599 means that an error occured.
+ if (code >= 400 && code < 600) {
+ return this._requestDidFail(res, body, callback);
+ }
+
+ // Automatically follow redirects.
+ if (code >= 300 && code < 400) {
+ return this.get(res.headers['location'], callback);
+ }
+
+ if (!type.match('application/json')) {
+ console.warn('Expected header "Content-Type" to be "application/json" but got "%s".', type);
+ }
+
+ try {
+ data = JSON.parse(body);
+
+ if (data.status !== 0) {
+ error = new Exception(data.status);
+ }
+ }
+ catch(e) {
+ error = e;
+ }
+
+ callback(error, data);
+};
+
+/**
+ This method is called when a request fails on the webdriver level.
+
+ @param {http.ClientResponse} res
+ @param {String} body
+ @param {Function} cb
+ @private
+*/
+proto._requestDidFail = function(res, body, cb) {
+ var type, data, error;
+
+ type = res.headers['content-type'] || ""
+
+ // The content-type is expected to be text/plain.
+ if (type !== 'text/plain') {
+ console.warn('Expected header "Content-Type" to be "text/plain" but got "%s".', type);
+ }
+
+ // The response body should contain a human readable error message. Some endpoints
+ // (e.g. chrome driver) return error messages encoded as JSON.
+ if (type.match("application/json")) {
+ try {
+ data = JSON.parse(body);
+ error = data && data.value && data.value.message;
+ }
+ catch(SyntaxError) {}
+ }
+ else {
+ error = body;
+ }
+
+ // This method is only called when the status code is between 400 and 600.
+ // According to the spec, a code 404 has a different message.
+ if (!error) {
+ error = res.statusCode === 404 ? "Unknown command" : "Unknown error";
+ }
+
+ cb(error);
+};
38 lib/client_mock.js
@@ -0,0 +1,38 @@
+/**
+ * A mock implementation of the Client interface.
+ *
+ * @class
+ */
+var ClientMock = module.exports = constructor
+ , proto = ClientMock.prototype = {};
+
+/**
+ * Constructor
+ */
+function constructor() {}
+
+/**
+ * Request
+ */
+proto.request = function(method, resource, params, callback) {
+ this.lastRequest = {
+ method: method
+ , resource: resource
+ , params: params
+ , callback: callback
+ };
+};
+
+/**
+ * Helper methods for HTTP requests.
+ */
+['get', 'post', 'put', 'delete'].forEach(function(method) {
+ proto[method] = function(resource, params, callback) {
+ if (typeof params === 'function') {
+ callback = params;
+ params = null;
+ }
+
+ this.request(method, resource, params, callback);
+ };
+});
85 lib/endpoint.js
@@ -0,0 +1,85 @@
+var Client = require('./client')
+ , Session = require('./session');
+
+var Endpoint = module.exports = constructor
+ , proto = Endpoint.prototype = {};
+
+/**
+ * Endpoint
+ *
+ * @class
+ */
+Endpoint.create = function() {
+ var instance = Object.create(proto);
+ constructor.apply(instance, arguments);
+ return instance;
+};
+
+/**
+ * Constructor
+ */
+function constructor(opts) {
+ opts = opts || {};
+
+ if (opts.client) {
+ this.client = opts.client;
+ return;
+ }
+
+ this.client = Client.create({
+ host: opts.host
+ , port: opts.port
+ , path: opts.path
+ });
+};
+
+/**
+ * Retrieves status of the endpoint.
+ *
+ * @param {Function} callback
+ */
+proto.status = function(callback) {
+ this.client.get('/status', function(err, res) {
+ if (err) {
+ return callback(err);
+ }
+
+ callback(null, res.value);
+ });
+};
+
+/**
+ * Creates a new session.
+ *
+ * @param {Object} [caps] Desired capabilities, e.g. {browserName: 'firefox'}.
+ * @param {Function} callback
+ */
+proto.session = function(caps, callback) {
+ var params;
+
+ // Capabilities are optional.
+ if (typeof caps === 'function') {
+ callback = caps;
+ caps = null;
+ }
+
+ // desiredCapabilities must be sent, even if empty.
+ params = {desiredCapabilities: caps || {}};
+
+ var self = this;
+ this.client.post('/session', params, function(err, res) {
+ var session;
+
+ if (err) {
+ return callback(err);
+ }
+
+ session = Session.create({
+ id: res.sessionId
+ , client: self.client
+ , capabilities: res.value
+ });
+
+ callback(null, session);
+ });
+};
111 lib/exception.js
@@ -0,0 +1,111 @@
+/**
+ * An exception class for webdriver errors.
+ *
+ * @class
+ */
+var Exception = module.exports = constructor,
+ proto = Exception.prototype = {};
+
+/**
+ * Constructor
+ */
+function constructor(status) {
+ var error = this.errors[status];
+
+ this.name = error.name;
+ this.message = error.message;
+};
+
+/**
+ * Error definitions from the spec.
+ */
+proto.errors = {
+ 0: {
+ name: "Success"
+ , message: "The command executed successfully."
+ }
+ , 7: {
+ name: "NoSuchElement"
+ , message: "An element could not be located on the page using the given search parameters."
+ }
+ , 8: {
+ name: "NoSuchFrame"
+ , message: "A request to switch to a frame could not be satisfied because the frame could not be found."
+ }
+ , 9: {
+ name: "UnknownCommand"
+ , message: "The requested resource could not be found, or a request was received using an HTTP method that is not supported by the mapped resource."
+ }
+ , 10: {
+ name: "StaleElementReference"
+ , message: "An element command failed because the referenced element is no longer attached to the DOM."
+ }
+ , 11: {
+ name: "ElementNotVisible"
+ , message: "An element command could not be completed because the element is not visible on the page."
+ }
+ , 12: {
+ name: "InvalidElementState"
+ , message: "An element command could not be completed because the element is in an invalid state (e.g. attempting to click a disabled element)."
+ }
+ , 13: {
+ name: "UnknownError"
+ , message: "An unknown server-side error occurred while processing the command."
+ }
+ , 15: {
+ name: "ElementIsNotSelectable"
+ , message: "An attempt was made to select an element that cannot be selected."
+ }
+ , 17: {
+ name: "JavaScriptError"
+ , message: "An error occurred while executing user supplied JavaScript."
+ }
+ , 19: {
+ name: "XPathLookupError"
+ , message: "An error occurred while searching for an element by XPath."
+ }
+ , 21: {
+ name: "Timeout"
+ , message: "An operation did not complete before its timeout expired."
+ }
+ , 23: {
+ name: "NoSuchWindow"
+ , message: "A request to switch to a different window could not be satisfied because the window could not be found."
+ }
+ , 24: {
+ name: "InvalidCookieDomain"
+ , message: "An illegal attempt was made to set a cookie under a different domain than the current page."
+ }
+ , 25: {
+ name: "UnableToSetCookie"
+ , message: "A request to set a cookie's value could not be satisfied."
+ }
+ , 26: {
+ name: "UnexpectedAlertOpen"
+ , message: "A modal dialog was open, blocking this operation"
+ }
+ , 27: {
+ name: "NoAlertOpenError"
+ , message: "An attempt was made to operate on a modal dialog when one was not open."
+ }
+ , 28: {
+ name: "ScriptTimeout"
+ , message: "A script did not complete before its timeout expired."
+ }
+ , 29: {
+ name: "InvalidElementCoordinates"
+ , message: "The coordinates provided to an interactions operation are invalid."
+ }
+ , 30: {
+ name: "IMENotAvailable"
+ , message: "IME was not available."
+ }
+ , 31: {
+ name: "IMEEngineActivationFailed"
+ , message: "An IME engine could not be started."
+ }
+ , 32: {
+ name: "InvalidSelector"
+ , message: "Argument was an invalid selector (e.g. XPath/CSS)."
+ }
+}
102 lib/session.js
@@ -0,0 +1,102 @@
+var Session = module.exports = constructor
+ , proto = Session.prototype = {};
+
+Session.create = function() {
+ var instance = Object.create(proto);
+ constructor.apply(instance, arguments);
+ return instance;
+}
+
+/**
+ * Constructor
+ */
+function constructor(opts) {
+ opts = opts || {};
+
+ this.id = opts.id;
+ this.client = opts.client;
+ this.capabilities = opts.capabilities;
+}
+
+/**
+ * Helper methods for HTTP requests.
+ */
+['get', 'post', 'put', 'delete'].forEach(function(method) {
+ proto[method] = function(resource, params, callback) {
+ if (typeof params === 'function') {
+ callback = params;
+ params = null;
+ }
+
+ this.request(method, resource, params, callback);
+ };
+});
+
+/**
+ * Sends a request with the path scoped to this session.
+ *
+ * @param {String} method
+ * @param {String} resource
+ * @param {Object} params
+ * @param {Function} callback
+ */
+proto.request = function(method, resource, params, callback) {
+ resource = '/session/' + this.id + resource;
+ this.client.request(method, resource, params, callback);
+};
+
+/**
+ * Gets or sets the url.
+ *
+ * @param {String} [url]
+ * @param {Function} callback
+ */
+proto.url = function(url, callback) {
+ if (typeof url === 'function') {
+ callback = url;
+ url = null;
+ }
+
+ if (url) {
+ this.post('/url', {url: url}, callback);
+ }
+ else {
+ this.get('/url', callback);
+ }
+};
+
+/**
+ * Executes javascript in the browser.
+ *
+ * @param {Function, String} fn A string of code to eval or a function.
+ * @param {Array} [args] Optional array of arguments to pass to the function.
+ * @param {Function} callback
+ */
+proto.execute = function(fn, args, callback) {
+ if (typeof fn === 'function') {
+ fn = fn.toString().replace(/^function \(\) \{ /, '').replace(/\s*\}\s*/, '');
+ }
+
+ if (typeof args === 'function') {
+ callback = args;
+ args = [];
+ }
+ this.post('/execute', {script: fn, args: args}, function(err, res) {
+ if (err) {
+ callback(err);
+ }
+
+ callback(null, res.value);
+ });
+};
+
+/**
+ * Quits the session.
+ *
+ * @param {Function} callback
+ */
+proto.quit = function(callback) {
+ this.delete('', null, function(err) {
+ callback(err);
+ });
+};
12 lib/webdriver.js
@@ -0,0 +1,12 @@
+var Endpoint = require('./endpoint')
+ , Client = require('./client')
+ , Session = require('./session');
+
+exports.Endpoint = Endpoint;
+exports.Client = Client;
+exports.Session = Session;
+
+exports.endpoint = function(opts) {
+ return Endpoint.create(opts);
+}
+
22 package.json
@@ -0,0 +1,22 @@
+{
+ "author": "Alexander Flatter <flatter@fastmail.fm>",
+ "name": "webdriver",
+ "description": "Client for the webdriver protocol.",
+ "version": "0.0.0",
+ "repository": {
+ "url": ""
+ },
+ "engines": {
+ "node": "~0.6.6"
+ },
+ "dependencies": {
+ "underscore": "~1.3.1"
+ },
+ "devDependencies": {
+ "mocha": "~0.10.2",
+ "chai": "~0.2.1",
+ "sinon": "~1.3.1"
+ },
+ "files": ["./lib"],
+ "main": "./lib/webdriver.js"
+}
59 test/endpoint_test.js
@@ -0,0 +1,59 @@
+var assert = require('chai').assert
+ , spy = require('sinon').spy
+ , Endpoint = require('webdriver/lib/endpoint')
+ , Session = require('webdriver/lib/session')
+ , ClientMock = require('webdriver/lib/client_mock');
+
+suite('Endpoint', function() {
+ test('status()', function() {
+ var client = new ClientMock()
+ , endpoint = Endpoint.create({client: client})
+ , callback = spy();
+
+ endpoint.status(callback);
+
+ // Verify that a proper request was made.
+ var lastRequest = client.lastRequest;
+ assert.ok(lastRequest, 'a request was made');
+ assert.equal(lastRequest.method, 'get');
+ assert.equal(lastRequest.resource, '/status');
+ assert.isNull(lastRequest.params);
+
+ lastRequest.callback(null, {status: 0, value: {foo: 'bar'}});
+
+ var error = callback.getCall(0).args[0]
+ , status = callback.getCall(0).args[1];
+
+ assert.isNull(error);
+ assert.deepEqual(status, {foo: 'bar'});
+ }); // status()
+ test('session()', function() {
+ var client = new ClientMock()
+ , endpoint = Endpoint.create({client: client})
+ , callback = spy();
+
+ endpoint.session(callback);
+
+ // Verify that a proper request was made.
+ var lastRequest = client.lastRequest;
+ assert.ok(lastRequest, 'a request was made');
+ assert.equal(lastRequest.method, 'post');
+ assert.equal(lastRequest.resource, '/session');
+ assert.deepEqual(lastRequest.params , {desiredCapabilities: {}});
+
+ // Fake a successfull request.
+ lastRequest.callback(null, {status: 0, sessionId: '1', value: {browserName: 'firefox'}});
+
+ assert.ok(callback.calledOnce);
+
+ var error = callback.getCall(0).args[0]
+ , session = callback.getCall(0).args[1];
+
+ assert.isNull(error);
+
+ assert.instanceOf(session, Session, 'result is an instance of Session');
+
+ assert.equal(session.id, '1');
+ assert.deepEqual(session.capabilities, {browserName: 'firefox'});
+ }); // session()
+}); // Endpoint
92 test/session_test.js
@@ -0,0 +1,92 @@
+var assert = require('chai').assert
+ ,spy = require('sinon').spy
+ , Session = require('webdriver/lib/session');
+
+var fakeClient = function() {
+ return {request: spy()};
+};
+
+suite('Session', function() {
+ var id = 1
+ , client = {}
+ , caps = {};
+
+ suite('constructor', function() {
+ test('making a new instance using .create()', function() {
+ var session = Session.create({id: id, client: client, capabilities: caps});
+
+ assert.equal(session.id, id, 'id is set on the instance');
+ assert.equal(session.client, client, 'client is set on the instance');
+ assert.equal(session.capabilities, caps, 'capabilities is set on the instance');
+
+ assert.instanceOf(session, Session);
+ });
+
+ test('making a new instance using the new keyword', function() {
+ var session = new Session({id: id, client: client, capabilities: caps});
+
+ assert.equal(session.id, id, 'id is set on the instance');
+ assert.equal(session.client, client, 'client is set on the instance');
+ assert.equal(session.capabilities, caps, 'capabilities is set on the instance');
+
+ assert.instanceOf(session, Session);
+ });
+ }); // constructor
+ suite('request', function() {
+ var method = 'GET'
+ , resource = '/foo'
+ , params = null
+ , callback = function() {};
+
+ test('scopes all paths to the session', function() {
+ var request = spy()
+ , client = {request: request}
+ , session = Session.create({id: id, client: client})
+ , args;
+
+ session.request(method, resource, params, callback);
+ assert.ok(request.calledOnce, 'client receives one request');
+
+ args = request.getCall(0).args;
+ assert.equal(args[0], method, 'method is not modified');
+ assert.equal(args[1], '/session/' + id + resource, 'resource is scoped');
+ assert.equal(args[2], params, 'params are not modified');
+ assert.equal(args[3], callback, 'callback is not modified');
+ });
+ }); // request
+ suite('url', function() {
+ test('retrieves url when called without string', function() {
+ var client = fakeClient()
+ , session = Session.create({id: 1, client: client})
+ , callback = spy()
+ , args;
+
+ session.url(callback);
+
+ assert.ok(client.request.calledOnce, 'client receives one request');
+
+ args = client.request.getCall(0).args;
+ assert.equal(args[0], 'get', 'method is not modified');
+ assert.equal(args[1], '/session/' + id + '/url', 'resource is scoped');
+ assert.equal(args[2], null, 'params are not defined');
+ assert.equal(args[3], callback, 'callback is passed');
+ });
+ test('navigates to url when called with a string', function() {
+ var client = fakeClient()
+ , session = Session.create({id: 1, client: client})
+ , callback = spy()
+ , url = 'http://example.com'
+ , args;
+
+ session.url(url, callback);
+
+ assert.ok(client.request.calledOnce, 'client receives one request');
+
+ args = client.request.getCall(0).args;
+ assert.equal(args[0], 'post', 'method is not modified');
+ assert.equal(args[1], '/session/' + id + '/url', 'resource is scoped');
+ assert.deepEqual(args[2], {url: url}, 'params includes url');
+ assert.equal(args[3], callback, 'callback is passed');
+ });
+ }); // url
+}); // Session
13 test/webdriver_test.js
@@ -0,0 +1,13 @@
+var wd = require('webdriver')
+ , assert = require('chai').assert
+ ;
+
+suite('webdriver', function() {
+ suite('.endpoint()', function() {
+ test('returns endpoint without arguments', function() {
+ var result = wd.endpoint();
+
+ assert.instanceOf(result, wd.Endpoint, 'returns an instance of Endpoint');
+ });
+ }); // endpoint
+}); // webdriver

0 comments on commit a4d19b9

Please sign in to comment.
Something went wrong with that request. Please try again.