Skip to content

Instantly share code, notes, and snippets.

@rvagg
Last active December 21, 2015 18:49
Show Gist options
  • Save rvagg/6350237 to your computer and use it in GitHub Desktop.
Save rvagg/6350237 to your computer and use it in GitHub Desktop.
Selenese (Selenium scripting) in node: experimenting with a DSLish interface to WebDriver. Can connect to saucelabs or a local server or other Selenium installation. Looks sync but is heavily async, lots of talking via WebDriver.
const assert = require('referee').assert
, refute = require('referee').refute
, fail = require('referee').fail
// magic pixie dust to get us initialised
const $ = require('selenese_node/sequence')(module)
const loginEmail = 'testmystuff@mockmyid.com'
// some state to maintain along the way
var mainWindow
, personaWindow
// GO!
$.title(/some title regex here/i)
$.windowIds(function (ids) {
assert.equals(ids.length, 1, 'just have one window open')
mainWindow = ids[0] // save state for later
})
$.click('.persona-identify')
// function for this implemented below, simple helper because this is
// done twice
waitForWindowIdCriteria(function (ids) {
if (ids.length > 1) {
personaWindow = ids.filter(function (id) {
return id != mainWindow
})[0]
// we can sub-queue additional interactions within calls to create a
// hierarchical async call structure, execution won't continue until
// this call
$.selectWindow(personaWindow)
return true
}
return false
})
// now we're in the persona popup window
$.waitForVisible('#authentication_email', 30 * 1000) // timeout will cause a fail
$.wait(1000) // I don't know why we need to wait here, but I blame persona...
$.type('#authentication_email', loginEmail) // enter some text
$.click('.buttonrow .isDesktop.isStart.isAddressInfo') // click a button
// wait for only one window, i.e. the persona window ought to disappear
waitForWindowIdCriteria(function (ids) {
if (ids.length == 1) {
$.selectWindow(ids[0])
return true
}
return false
})
// and back at the main window, checking for a proper login
$.waitForElement('.signed-in a', 20 * 1000)
// this would tell us that we're logged in because the page has our email
$.textPresent('.signed-in a', loginEmail)
// and here we end!
$.wait(function (next) {
console.log('waiting 1s before we finish, for no good reason')
setTimeout(next, 1000)
})
// utility $.wait() function
function waitForWindowIdCriteria (criteria) {
$.wait(function (next) {
function windows () {
$.windowIds(function (ids) {
if (criteria(ids))
return next()
setTimeout(windows, 100)
})
}
windows()
})
}
// execute the sequence with this invocation
const run = require('selenese_node/run')
, personaSequence = require('./persona-sequence')
run('http://localhost:3000' , { browserName: 'firefox' }, personaSequence)
@rvagg
Copy link
Author

rvagg commented Aug 31, 2013

Take 2, this time I've just abandoned using Persona directly for testing because it's just so hard to get right across browsers and am using StubbyID instead to bypass logins (see here: https://gist.github.com/rvagg/6362798).

I've also refactored so that multiple concurrent tests can run so you can do many browsers simultaneously.

persona-sequence.js

const assert   = require('referee').assert
    , refute   = require('referee').refute
    , fail     = require('referee').fail
    , Sequence = require('selenese_node/sequence')

const loginEmail = 'whatever@mockmyid.com'

module.exports = function sequence () {
  var seq = Sequence()
    , _   = seq._

var mainWindow

_.title(/some title here/i)

_.windowIds(function (ids, callback) {
  assert.equals(ids.length, 1, 'one window')
  mainWindow = ids[0]
  callback()
})

_.click('.persona-identify')

_.alertKeys(loginEmail)

_.acceptAlert()

_.waitForElement('.signed-in a', 20 * 1000)

_.textPresent('.signed-in a', loginEmail)

_.wait(function (next) {
  console.log('waiting 1s')
  setTimeout(next, 1000)
})


return seq
}

This is the test runner that optionally uses SauceConnect to connect to Saucelabs to run the tests. if you set the WEBDRIVER_REMOTE environment variable.

persona-test.js

// sauce-connect is a very unpleasant npm package, avoid using it but use the bundled Sauce-Connect.jar
const SauceConnectJar = require.resolve('sauce-connect/ext/Sauce-Connect.jar')
    , childProcess    = require('child_process')
    , run             = require('selenese_node/run')
    , after           = require('after')
    , personaSequence = require('./persona-sequence')

var wdremote = process.env.WEBDRIVER_REMOTE
  , child
  , init
  , caps

if (wdremote) {
  // a remote webdriver test
  wdremote = JSON.parse(wdremote)
  var caps = [
      { browserName: 'chrome'           , platform: 'Windows 8', version: ''   }
    , { browserName: 'firefox'          , platform: 'Windows 8', version: ''   }
    , { browserName: 'internet explorer', platform: 'Windows 8', version: '10' }
    , { browserName: 'ipad'             , platform: 'OS X 10.8', version: '6'  }
    , { browserName: 'android'          , platform: 'Linux'    , version: '4.0', 'device-type': 'tablet' }
  ]

  init = function (cb) {
    childProcess.spawn('killall', [ 'java' ])
    child = childProcess.spawn(
        'java'
      , [ '-jar', SauceConnectJar, wdremote.user, wdremote.key, '-P', '9091' ]
    )
    child.stdout.pipe(process.stdout)
    child.stderr.pipe(process.stderr)
    wdremote = [ 'localhost', 9091, wdremote.user, wdremote.key ]
    child.stdout.on('data', function (line) {
      if (/Tunnel remote VM is running at /.test(line.toString()))
        setTimeout(cb.bind(null, caps), 2000)
    })
  }
} else {
  // else local webdriver
  init = function (cb) { cb([{ browserName: 'firefox' }]) }
  console.error('no WEBDRIVER_REMOTE set, using localhost WebDriver')
}


init(function (caps) {
  var done = after(caps.length, function () {
    child && child.kill()
  })

  caps.forEach(function (caps) {
    run(
        wdremote
      , 'http://localhost:3000'
      , caps
      , personaSequence()
      , done
    )
  })
})

@jfhbrook
Copy link

Cool. I'd give this a shot.

It'd be nice to add a pinch of sugar to

var seq = Sequence(),
    _   = seq._

and maybe do something like,

var _ = sequence.create()

@rvagg
Copy link
Author

rvagg commented Sep 27, 2013

the problem with just var _ = sequence.create() is that I need to return something, but you're right, it's boilerplate overhead and I need to remove it somehow.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment