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']
asked Mar 29, 2012 at 14:16
\$\endgroup\$

1 Answer 1

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
answered Oct 5, 2013 at 15:04
\$\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\$ Commented Oct 6, 2013 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\$ Commented Oct 7, 2013 at 14:30

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.