Home » Nodejs » Extending selenium webdriver js

Extending selenium webdriver js

Posted by: admin January 30, 2018 Leave a comment

Questions:

Preface

I’m trying to write a few extensions for selenium-webdriver, like so:

var webdriver = require('selenium-webdriver');
var fs = require('fs');
var resumer = require('resumer');

webdriver.WebDriver.prototype.saveScreenshot = function(filename) {
    return this.takeScreenshot().then(function(data) {
        fs.writeFile(filename, data.replace(/^data:image\/png;base64,/,''), 'base64', function(err) {
            if(err) throw err;
        });
    });
};

webdriver.WebDriver.prototype.streamScreenshot = function() {
    var stream = resumer();
    this.takeScreenshot().then(function(data) {
        stream.queue(new Buffer(data.replace(/^data:image\/png;base64,/,''), 'base64')).end();
    });
    return stream;
};

module.exports = webdriver;

And then I just include my extended webdriver, instead of the official one:

var webdriver = require('./webdriver.ext');

I think that’s the proper way to extend things in Node JS.

Problem

The issue I’m having is with adding a custom Locator Strategy. The strategies look like this in the source:

/**
 * Factory methods for the supported locator strategies.
 * @type {Object.<function(string):!webdriver.Locator>}
 */
webdriver.Locator.Strategy = {
  'className': webdriver.Locator.factory_('class name'),
  'class name': webdriver.Locator.factory_('class name'),
  'css': webdriver.Locator.factory_('css selector'),
  'id': webdriver.Locator.factory_('id'),
  'js': webdriver.Locator.factory_('js'),
  'linkText': webdriver.Locator.factory_('link text'),
  'link text': webdriver.Locator.factory_('link text'),
  'name': webdriver.Locator.factory_('name'),
  'partialLinkText': webdriver.Locator.factory_('partial link text'),
  'partial link text': webdriver.Locator.factory_('partial link text'),
  'tagName': webdriver.Locator.factory_('tag name'),
  'tag name': webdriver.Locator.factory_('tag name'),
  'xpath': webdriver.Locator.factory_('xpath')
};
goog.exportSymbol('By', webdriver.Locator.Strategy);

I’m trying to add a new one by injecting it into that object:

webdriver.By.sizzle = function(selector) {
    driver.executeScript("return typeof Sizzle==='undefined'").then(function(noSizzle) {
        if(noSizzle) driver.executeScript(fs.readFileSync('sizzle.min.js', {encoding: 'utf8'}));
    });
    return new webdriver.By.js("return Sizzle("+JSON.stringify(selector)+")[0]");
};

This actually works fine for simple scripts where driver is defined (notice that I’m using a global variable).

Is there a way to access the “current driver” inside my function? Unlike the methods at the top, this isn’t a prototypical method, so I don’t have access to this.

I don’t know how those factory_s work; I was just guessing that I could inject a function directly.

Answers:

Setup a Custom constructor that inherits from webdriver.WebDriver. Inside the constructor you have access to the this object which you can use to add your custom locator

var util = require('util');
var webdriver = require('selenium-webdriver');
var WebDriver = webdriver.WebDriver
var fs = require('fs');
var resumer = require('resumer');


function CustomDriver() {
  WebDriver.call(this);
  // append your strategy here using the "this" object
  this...
}

util.inherits(WebDriver, CustomDriver);

CustomDriver.prototype.saveScreenshot = function(filename) {
  return this.takeScreenshot().then(function(data) {
    fs.writeFile(filename, data.replace(/^data:image\/png;base64,/, ''), 'base64', function(err) {
      if (err) throw err;
    });
  });
};

CustomerDriver.prototype.streamScreenshot = function() {
  var stream = resumer();
  this.takeScreenshot().then(function(data) {
    stream.queue(new Buffer(data.replace(/^data:image\/png;base64,/, ''), 'base64')).end();
  });
  return stream;
};

module.exports = CustomDriver

Questions:
Answers:

Another option:

Use function.prototype.bind –
Create a bunch of functions that are written as if their this context were a driver instance:

function myCustomMethod(){
this.seleniumDriverMethodOfSomeSort()
//etc.
}

And then export a single wrapping function to bind them onto the instance and assign them to method names:

function WrapDriverInstance(driver){
    driver.myCustomMethod = myCustomMethod.bind(driver)
}

You could even stick all your methods in an array like [{method : methodfunction, name : 'methodName'}] and then do this:

function bindAllMyMethodsAtOnce(driver){
    methodArray.forEach(item=>{
         driver[item.name] = item.method.bind(driver)
})
}

Or get really crazy and take advantage of the fact that .bind() lets you do partial function application:

function customClicker(selector){
    return this.findElement(By.css(selector)).click()
}
function customSendKeys(selector,keys){
return this.findElement(By.css(selector)).sendKeys(keys)
}
var arrayOfElementSelections = [{elementCSS : 'div.myclass', name : 'boxOStuff'}] //etc
function wrapCustomActions(driver){
    arrayOfElementSelections.forEach(element=>{
        driver[element.name+'Clicker'] = customClicker.bind(driver,element.elementCSS)
        driver[element.name+'Keyer'] = customSendKeys.bind(driver,element.elementCSS)
    })
 }

And now you have a function that can ‘prime’ a driver instance with a bunch of convenience methods for interacting with elements on a specific page.
You have to remember to call your wrapper on the driver instance instead of getting ‘free’ behavior on your overloaded constructor.

But, because of the partial application nature of .bind(), you can define more general purpose utility methods and specify their behaviors when you wrap them.

So instead of making a class to extend Driver for each test, you make a few wrappers that abstract the actual behavior you’re trying to accomplish – select an element, save a screenshot, etc. – and then on a per page or per feature basis, have the parameters like css selectors or filepaths saved somewhere, and invoke them ala-carte.