Skip to content

Instantly share code, notes, and snippets.

@benjamine
Last active August 29, 2015 14:10
Show Gist options
  • Save benjamine/b8b944506a54a5f4204b to your computer and use it in GitHub Desktop.
Save benjamine/b8b944506a54a5f4204b to your computer and use it in GitHub Desktop.
driver-agnostic page objects

this is a preview of a page object model that works independently of the browser driver. page and component object define a hierarchy, and each of this objects expose a .s property that has an absolute css selector to find the element (eg. using document.querySelector), the way that property is built is by space-concatenating the css selectors of it's ancestors.

  var page = this.page('checkout');
  // page.orderConfirmation.purchase.s === ['#main .order-confirmation', 'button.purchase-btn'].join(' ')
  browserDriver.click(page.orderReview.purchase.s);

page and component objects receive a reference to the world, so dom interaction code can be encapsulated in them.

var util = require('util');
var Component = require('./component');

function OrderReview(world){
  Component.call(this, world);
  this.at('#main .order-confirmation');
  
  // children
  this.purchaseButton = this.component().at('button.purchase-btn');
}

util.inherits(OrderReview, Component);

OrderReview.prototype.purchase = function(callback) {
  var self = this;
  this.browser
      .waitForVisible(this.s)
      // this.purchaseButton.s === '#main .order-confirmation button.purchase-btn"
      .click(this.purchaseButton.s)
      .call(callback);
};

the page:

var util = require('util');
var Page = require('./page');

function Checkout(world){
  Page.call(this, world);
  this.orderReview = this.component('order-review');
}

util.inherits(Checkout, Page);

module.exports = Checkout;

Now we can rewrite the first code block as:

  this.page('checkout').orderReview.purchase();
var S = require('string');
function Component(world) {
this._children = [];
this.setWorld(world);
this.updateSel();
}
Component.prototype.component = function(name){
var child = name ? this.world.pageManager.component(name) :
new Component();
child.parent = this;
child.updateSel();
this._children.push(child);
return child;
};
Component.prototype.at = function(selector){
this._selector = selector;
this.updateSel();
return this;
};
Component.prototype.getAbsoluteSelector = function(){
var selector = [];
if (this.parent) {
selector.push(this.parent.getAbsoluteSelector());
}
if (this._selector) {
selector.push(this._selector);
}
return selector.join(' ').trim();
};
Component.prototype.updateSel = function(){
/*
// allow this type of syntax:
var page = this.page('cart');
this.browser.click(page.checkoutButton.s));
*/
if (!this._selector) {
this.s = '#' + S('component' + this.constructor.name).dasherize();
return;
}
this.s = this.getAbsoluteSelector();
this._children.forEach(function(child){
child.updateSel();
});
};
Component.prototype.setWorld = function(world){
this.world = world;
this.browser = this.world.browser;
this.config = this.world.config;
this._children.forEach(function(child){
child.setWorld(world);
});
};
module.exports = Component;
var url = require('url');
var path = require('path');
var Page = require('./page');
var requireDir = require('require-dir');
function PageManager(world, dir) {
this.world = world;
world.pageManager = this;
this.pages = requireDir(path.join(dir, 'pages'));
this.components = requireDir(path.join(dir, 'components'));
world.visit = function(targetUrl, callback){
return this.pageManager.visit(targetUrl, callback);
};
world.page = function(name){
return this.pageManager.page(name);
};
}
PageManager.prototype.page = function(name) {
var page = new (this.pages[name])(this.world);
return page;
};
PageManager.prototype.component = function(name) {
var component = new (this.components[name])(this.world);
return component;
};
PageManager.prototype.visit = function(targetUrl, callback){
if (!/^https?\:\/\//i.test(targetUrl)) {
targetUrl = url.resolve(this.world.config.baseUrl, targetUrl);
}
this.world.browser.url(targetUrl).call(function(err){
if (err) {
callback(err);
return;
}
callback();
}.bind(this));
};
module.exports = PageManager;
var url = require('url');
var util = require('util');
var Component = require('./component');
function Page(world){
Component.call(this, world);
}
util.inherits(Page, Component);
Page.prototype.visit = function(params, callback) {
if (typeof params === 'function') {
callback = params;
params = null;
}
// compose a target url
var targetUrl = this.route;
if (targetUrl.substr(0, 1) === '/') {
targetUrl = url.resolve(this.config.baseUrl, targetUrl);
}
var usedParams = {};
targetUrl = targetUrl
.replace(/\*.*$/, "")
.replace(/\:[a-z0-9]+[\?]?/g, function(match){
var name = match.substr(1, match.length - 2 -
(match.substr(match.length - 1) === '?') ? 1 : 0);
if (params && params[name]) {
usedParams[name] = true;
return params[name];
}
return '';
})
.replace(/\/[\/]+/g, '/');
// additional query params
Object.keys(params).forEach(function(name) {
if (name === '_hash' || usedParams[name]) {
return;
}
targetUrl += (targetUrl.indexOf('?') < 0) ? '?' : '&';
targetUrl += name + '=' + encodeURIComponent(value);
});
if (params._hash) {
targetUrl += '#' + params._hash;
}
this.browser.url(targetUrl).call(callback);
};
module.exports = Page;
var path = require('path');
var PageManager = require('../../util/page-manager');
module.exports = function() {
this.World = function World(callback) {
new PageManager(this, path.join(__dirname, '../../page-objects'));
};
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment