Fork me on GitHub Soda

Soda

The Selenium Node Adapter or Soda provides a unified client to both Selenium RC and Saucelabs OnDemand.

index

lib/soda/index.js

Export all of ./client.

exports = module.exports = require('./client');

Export sauce client.

exports.SauceClient = require('./sauce');
exports.createSauceClient = require('./sauce').createClient;

Library version.

  • type: String

exports.version = '0.0.1';

client

lib/soda/client.js

Module dependencies.

var http = require('http')
  , qs = require('querystring')
  , EventEmitter = require('events').EventEmitter;

Initialize a Client with the given options.

Options

  • host Hostname defaulting to localhost
  • port Port number defaulting to 4444
  • browser Browser name
  • url URL string

  • params: Object options

  • api: public

var Client = exports = module.exports = function Client(options) {
  this.host = options.host || 'localhost';
  this.port = options.port || 4444;
  this.browser = options.browser || 'firefox';
  this.url = options.url;

  // Allow optional "*" prefix
  if (this.browser[0] !== '*') {
    this.browser = '*' + this.browser;
  }

  EventEmitter.call(this);
};

Interit from EventEmitter.

Client.prototype.__proto__ = EventEmitter.prototype;

Initialize a new session, then callback fn(err, sid)

  • param: Function fn

  • return: Client

  • api: public

Client.prototype.session = function(fn){
  var self = this;
  if (!this.browser) throw new Error('browser required');
  if (!this.url) throw new Error('browser url required');
  if (this.queue) {
    return this.enqueue('getNewBrowserSession', [this.browser, this.url], function(body){
      self.sid = body;
    });
  } else {
    this.command('getNewBrowserSession', [this.browser, this.url], function(err, body){
      if (err) return fn(err);
      fn(null, self.sid = body);
    });
  }
};

Execute the given cmd / args, then callback fn(err, body, res).

  • param: String cmd

  • param: Array args

  • param: Function fn

  • return: Client for chaining

  • api: private

Client.prototype.command = function(cmd, args, fn){
  this.emit('command', cmd, args);

  // HTTP client
  var client = http.createClient(this.port, this.host);

  // Path construction
  var path = this.commandPath(cmd, args);

  // Request
  var req = client.request('GET'
    , path
    , { Host: this.host + (this.port ? ':' + this.port : '') });
    
  req.on('response', function(res){
    res.body = '';
    res.setEncoding('utf8');
    res.on('data', function(chunk){ res.body += chunk; });
    res.on('end', function(){
      if (res.body.indexOf('ERROR') === 0) {
        var err = res.body.replace(/^ERROR: */, '');
        err = cmd + '(' + args.join(', ') + '): ' + err; 
        fn(new Error(err), res.body, res);
      } else {
        if (res.body.indexOf('OK') === 0) {
          res.body = res.body.replace(/^OK,?/, '');
        }
        fn(null, res.body, res);
      }
    });
  });
  req.end();
  return this;
};

Construct a cmd path with the given args.

  • param: String name

  • param: Array args

  • return: String

  • api: private

Client.prototype.commandPath = function(cmd, args){
  var obj = { cmd: cmd };

  // Arguments by nth
  if (args) {
    args.forEach(function(arg, i){
      obj[i+1] = arg;
    });
  }
  // Ignore session id for getNewBrowserSession
  if (this.sid && cmd !== 'getNewBrowserSession') {
    obj.sessionId = this.sid;
  }

  return '/selenium-server/driver/?' + qs.stringify(obj);
};

Indicate that commands should be queued.

Example

 browser
   .chain
   .session()
   .open('/')
   .type('q', 'Hello World')
   .clickAndWait('btnG')
   .assertTitle('Hello World - Google')
   .testComplete()
   .end(function(err){ ... });
  • api: public

Client.prototype.__defineGetter__('chain', function(){
  this.queue = [];
  return this;
});

Callback fn(err) when the queue is complete, or when an exception has occurred.

  • param: Function fn

  • api: public

Client.prototype.end = function(fn){
  this._done = fn;
  this.queue.shift()();
};

Enqueue the given cmd and array of args for execution.

  • param: String cmd

  • param: Array args

  • return: Client

  • api: private

Client.prototype.enqueue = function(cmd, args, fn){
  var self = this
    , len = args.length;

  // Indirect callback support
  if (typeof args[len - 1] === 'function') {
    fn = args.pop();
  }

  this.queue.push(function(){
    self.command(cmd, args, function(err, body, res){
      // Callback support
      if (!err && fn) {
        try {
          fn(body, res);
        } catch (err) {
          return self._done(err, body, res);
        }
      }

      if (err) {
        self._done(err, body, res);
      } else if (self.queue.length) {
        self.queue.shift()();
      } else {
        self._done(null, body, res);
      }
    });
  });
  return this;
};

Shortcut for new soda.Client().

  • param: Object options

  • return: Client

  • api: public

exports.createClient = function(options){
  return new Client(options);
};

Command names.

  • type: Array

exports.commands = [
    'addSelection'
  , 'answerOnNextPrompt'
  , 'check'
  , 'chooseCancelOnNextConfirmation'
  , 'click'
  , 'clickAt'
  , 'clickAndWait'
  , 'close'
  , 'deleteCookie'
  , 'dragAndDrop'
  , 'fireEvent'
  , 'goBack'
  , 'getNewBrowserSession'
  , 'keyDown'
  , 'keyPress'
  , 'keyUp'
  , 'mouseDown'
  , 'mouseDownAt'
  , 'mouseMove'
  , 'mouseMoveAt'
  , 'mouseOut'
  , 'mouseOver'
  , 'mouseUp'
  , 'mouseUpAt'
  , 'open'
  , 'refresh'
  , 'removeSelection'
  , 'select'
  , 'selectFrame'
  , 'selectWindow'
  , 'setContext'
  , 'setCursorPosition'
  , 'setTimeout'
  , 'submit'
  , 'type'
  , 'testComplete'
  , 'uncheck'
  , 'waitForCondition'
  , 'waitForPageToLoad'
  , 'waitForPopUp'
  , 'windowFocus'
  , 'windowMaximize'
];

Accessor names.

  • type: Array

exports.accessors = [
    'Alert'
  , 'AllButtons'
  , 'AllFields'
  , 'AllLinks'
  , 'AllWindowIds'
  , 'AllWindowNames'
  , 'AllWindowTitles'
  , 'Attribute'
  , 'AttributeFromAllWindows'
  , 'BodyText'
  , 'Confirmation'
  , 'Cookie'
  , 'CursorPosition'
  , 'ElementHeight'
  , 'ElementIndex'
  , 'ElementPositionLeft'
  , 'ElementPositionTop'
  , 'ElementWidth'
  , 'Eval'
  , 'Expression'
  , 'HtmlSource'
  , 'Location'
  , 'LogMessages'
  , 'Prompt'
  , 'SelectedId'
  , 'SelectedIds'
  , 'SelectedIndex'
  , 'SelectedIndexes'
  , 'SelectedLabel'
  , 'SelectedLabels'
  , 'SelectedValue'
  , 'SelectedValues'
  , 'SelectedOptions'
  , 'Table'
  , 'Text'
  , 'Title'
  , 'Value'
  , 'WhetherThisFrameMatchFrameExpression'
  , 'AlertPresent'
  , 'Checked'
  , 'ConfirmationPresent'
  , 'Editable'
  , 'ElementPresent'
  , 'Ordered'
  , 'PromptPresent'
  , 'SomethingSelected'
  , 'TextPresent'
  , 'Visible'
];

Generate commands via accessors.

All accessors get prefixed with:

  • get
  • assert
  • assertNot
  • verify
  • verifyNot
  • waitFor
  • waitForNot

For example providing us with:

  • getTitle
  • assertTitle
  • verifyTitle
  • ...

exports.accessors.map(function(cmd){
  exports.commands.push(
      'get' + cmd
    , 'assert' + cmd
    , 'assertNot' + cmd
    , 'verify' + cmd
    , 'verifyNot' + cmd
    , 'waitFor' + cmd
    , 'waitForNot' + cmd);
});

Generate command methods.

exports.commands.map(function(cmd){
  Client.prototype[cmd] = function(){
    // Queue the command invocation
    if (this.queue) {
      var args = Array.prototype.slice.call(arguments);
      return this.enqueue(cmd, args);
    // Direct call
    } else {
      var len = arguments.length
        , fn = arguments[len - 1]
        , args = Array.prototype.slice.call(arguments, 0, len - 1);
      return this.command(cmd, args, fn);
    }
  };
});

sauce

lib/soda/sauce.js

Module dependencies.

var Client = require('./client');

Initialize a SauceClient with the given options.

Options

  • username Saucelabs username
  • access-key Account access key
  • os Operating system ex "Linux"
  • browser Browser name, ex "firefox"
  • browser-version Browser version, ex "3.0.", "7."
  • max-duration Maximum test duration in seconds, ex 300 (5 minutes)

  • params: Object options

  • api: public

var SauceClient = exports = module.exports = function SauceClient(options) {
  this.host = 'saucelabs.com';
  this.port = 4444;
  this.url = options.url;
  this.username = options.username;
  this.accessKey = options['access-key'];
  this.options = options;
  this.browser = JSON.stringify(options);
};

Interit from Client.

SauceClient.prototype.__proto__ = Client.prototype;

Return saucelabs video flv url.

  • return: String

  • api: public

SauceClient.prototype.__defineGetter__('videoUrl', function(){
  return exports.url(this.username, this.sid, 'video.flv');
});

Return saucelabs log file url.

  • return: String

  • api: public

SauceClient.prototype.__defineGetter__('logUrl', function(){
  return exports.url(this.username, this.sid, 'selenium-server.log');
});

Return saucelabs video embed script.

  • return: String

  • api: public

SauceClient.prototype.__defineGetter__('video', function(){
  return exports.video(this.username, this.accessKey, this.sid);
});

Shortcut for new soda.SauceClient().

  • param: Object options

  • return: Client

  • api: public

exports.createClient = function(options){
  return new SauceClient(options);
};

Return saucelabs url to jobId's filename.

  • param: String username

  • param: String jobId

  • param: String filename

  • return: String

  • api: public

exports.url = function(username, jobId, filename){
  return 'https://saucelabs.com/rest/'
    + username + '/jobs/'
    + jobId + '/results/'
    + filename;
};

Return saucelabs video embed script.

  • param: String username

  • param: String accessKey

  • param: String jobId

  • return: String

  • api: public

exports.video = function(username, accessKey, jobId){
  return '<script src="http://saucelabs.com/video-embed/'
    + jobId + '.js?username='
    + username + '&access_key='
    + accessKey + '"/>';
};