2
\$\begingroup\$

I'm using CasperJS and CoffeeScript for integration testing of a hobby project. Page Objects seem to be a useful abstraction. I had two major requirements:

  • Support for node.js style callbacks - mainly because tests can then be written using async.waterfall
  • Support extending (subclassing) to encapsulate common functionality, like a sidebar or a navbar

Here's the code, followed by a brief example of how I use it. It's a bit long, but that's about the shortest meaningful example I can give.

class PO # Base PO
  @casper: null

  # name: The name of the page object. Used to access it as PO[name], which is kind of like a forward declaration
  # selector: The page represented by this PO is considered to be ready for usage once this is visible
  # mixin: An object; properties of this will be mixed into the new instance
  constructor: (@name, @selector, mixin) ->
    if PO[@name] isnt undefined
      throw "Failed creating PO #{@name}: a PO instance or class method with this name already exists"
    PO[@name] = this
    @__defineGetter__ 'casper', -> PO.casper
    this[key] = val for key, val of mixin

  waitUntilVisible: (cb) -> @casper.waitUntilVisible @selector, cb

  # Switch to po, and call the callback cb with err and the po as parameters
  next: (cb, po, err=null) ->
    t0 = Date.now()
    msg = "Switching to PO #{po.name}"
    @casper.log msg, 'debug'
    po.waitUntilVisible =>
      @casper.log "#{msg}: done in #{Date.now() - t0}ms", 'debug'
      cb err, po
    po

new PO 'navbar', '',
  tabs: {}
  logout: (cb) ->
    @casper.click '#logout'
    @casper.waitFor (=> not @haveLogin()), (=> cb null, PO['login'])

  haveLogin: (cb) -> 
    result = nick == @casper.evaluate -> $('#current-user-nick').text()
    cb? result
    result

  addTab: ({name, switchSelector, readySelector, po}) ->
    @tabs[name] = arguments[0]

  toTab: (name, cb) ->
    cb "Navbar tab #{name} not known", null unless name of @tabs
    tab = @tabs[name]
    @casper.click tab.switchSelector
    @casper.waitUntilVisible tab.readySelector, => cb null, PO[tab.po]


class PON extends PO # PO with navbar
  constructor: (name, selector, mixin) ->
    super name, selector, mixin
    @navbar = PO['navbar']


new PON 'login', '.login-form',
  # ...

module.exports = (casper) ->
  PO.casper = casper
  PO['login']
\$\endgroup\$
2
\$\begingroup\$

Do we need to attach a casper reference to the PO class? By using the module pattern you inject the dependency anyway. Here is the P0 class with a tiny refactoring:

module.exports = (casper) ->

  class PO      
    constructor: (@name, @selector, mixin={}) ->
      if PO[@name]?
        throw new Error "Failed creating PO #{@name}: ..."
      PO[@name] = @
      @[key] ?= val for own key,val of mixin

    waitUntilVisible: (cb) ->
      casper.waitUntilVisible @selector, cb
      @

    next: (po, cb) ->
      t0 = Date.now()
      msg = "Switching to PO #{po.name}"
      casper.log msg, 'debug'
      po.waitUntilVisible (err) ->
        casper.log "#{msg}: done in #{Date.now() - t0}ms", 'debug'
        cb err, po

Then you would use it like this:

casper = require 'casper'
PO     = require('MyModuleName') casper

new PO 'navbar', '',
  tabs: {}
  logout: (cb) ->
    casper.click '#logout'
    casper.waitFor (=> not @haveLogin()), (err) -> cb err, PO.login

  haveLogin: (cb) ->
    result = nick is casper.evaluate -> ($ '#current-user-nick').text()
    cb? result
    result

  addTab: ({name, switchSelector, readySelector, po}) ->
    @tabs[name] = arguments[0]

  toTab: (name, cb) ->
    cb new Error "Navbar tab #{name} not known" unless name of @tabs
    tab = @tabs[name]
    casper.click tab.switchSelector
    casper.waitUntilVisible tab.readySelector, (err)-> cb err, PO[tab.po]

On extending the class you can skip the constructor (as long as it's the same like the one of the superclass) and attach the PO['navbar'] to the prototype.

class PON extends PO
  navbar: PO.navbar
\$\endgroup\$
2
  • \$\begingroup\$ Both are valid points, thank you! Regarding the injection of the casper object: I feel better if I put only the minimum amount of code into module.exports, because I feel it's easier to control what gets exported that way. I don't have any real data to support that though. Is there a best practice regarding this? \$\endgroup\$ – abesto Oct 6 '13 at 16:10
  • \$\begingroup\$ module.exports = (dependency) -> class Foo is the same as class Foo; module.exports = (dependency) -> Foo. But you're right exposing methods of big modules might be better done like module.exports = (dependency) -> moduleCode; {foo: bar, baz: "blub"}. \$\endgroup\$ – flosse Oct 7 '13 at 14:30

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.