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']
1 Answer 1
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
-
\$\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– abesto2013年10月06日 16:10:58 +00:00Commented Oct 6, 2013 at 16:10
-
\$\begingroup\$
module.exports = (dependency) -> class Foo
is the same asclass Foo; module.exports = (dependency) -> Foo
. But you're right exposing methods of big modules might be better done likemodule.exports = (dependency) -> moduleCode; {foo: bar, baz: "blub"}
. \$\endgroup\$flosse– flosse2013年10月07日 14:30:42 +00:00Commented Oct 7, 2013 at 14:30