Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Extending selenium webdriver js

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.

like image 833
mpen Avatar asked Sep 11 '13 17:09

mpen


People also ask

How do I use extends in selenium WebDriver?

public class T3EmployeeMaintenance extends Setup{ //public static WebDriver driver = new FirefoxDriver(); //no need of this now @BeforeClass public void invokeBrowser() { //... Show activity on this post. You have to finish class at the end. in your html simply add at the end of each class you declared.

Can I use JavaScript with selenium WebDriver?

Selenium WebDriver with JavaScript is a favorable combination to perform automated UI testing of applications. JavaScript offers efficiency with its well-built and structured patterns and functions, making the script more compact. It offers security and is well supported by a large community of developers.

What is extension selenium?

User extension is created, when Selenium IDE is not fulfilling the current requirement. To create user extension it is required to add javascript to selenium's object prototype. After creation of extension, it is required to add it in Selenium IDE and restart IDE.


2 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
like image 176
Noah Avatar answered Sep 23 '22 23:09

Noah


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.

like image 45
Iron Gremlin Avatar answered Sep 23 '22 23:09

Iron Gremlin