ember-cli-page-object
Advanced tools
Comparing version 0.1.0 to 0.2.0
@@ -18,2 +18,3 @@ # Page Object | ||
* [`.clickable`](#clickable) | ||
* [`.clickOnText`](#clickontext) | ||
* [`.fillable`](#fillable) | ||
@@ -63,9 +64,9 @@ * [`.visitable`](#visitable) | ||
``` | ||
```js | ||
var page = PO.build({ | ||
title: PO.text('.title) | ||
title: PO.text('.title') | ||
}); | ||
``` | ||
The following is a comprehensive documentation of the available PO attribute | ||
The following is a comprehensive documentation of the available `PO` attribute | ||
helpers. | ||
@@ -308,2 +309,41 @@ | ||
### `.clickOnText` | ||
Creates an action to click on an element by text. The text is case sensitive. | ||
Attribute signature | ||
```js | ||
PO.clickOnText(selector, [, scope: '']) | ||
``` | ||
Examples | ||
```html | ||
<button class="btn">Create</button> | ||
<button class="btn">Cancel</button> | ||
``` | ||
```js | ||
var page = PO.build({ | ||
click: clickOnText('.btn') | ||
}); | ||
page.click("Create"); | ||
andThen(function() { | ||
// ... | ||
}); | ||
page.click("Cancel"); | ||
andThen(function() { | ||
// ... | ||
}); | ||
``` | ||
> A string of text to look for. It's case sensitive. | ||
> The text must have matching case to be selected. | ||
> gwill match elements with the desired text block: | ||
### `.fillable` | ||
@@ -514,1 +554,18 @@ | ||
``` | ||
You can define components implicity by creating a plain object with attributes on it | ||
```js | ||
var page = PO.build({ | ||
visit: PO.visitable('/user/create'), | ||
title: PO.text('h1'), | ||
form: { | ||
firstName: PO.fillable('#firstName'), | ||
lastName: PO.fillable('#lastName'), | ||
submit: PO.clickable('button') | ||
} | ||
}); | ||
``` | ||
Note that if the plain object doesn't have attributes defined, the object is returned as is. |
{ | ||
"name": "ember-cli-page-object", | ||
"version": "0.1.0", | ||
"version": "0.2.0", | ||
"description": "Helper functions to implement the Page Object pattern in your tests", | ||
@@ -5,0 +5,0 @@ "directories": { |
# Ember Page Objects | ||
Represent the screens of your web app as a series of objects. | ||
Represent the screens of your web app as a series of objects. This ember-cli | ||
addon ease the construction of these objects on your acceptance tests. | ||
## References | ||
## What is a Page Object? | ||
* [Page Objects](https://code.google.com/p/selenium/wiki/PageObjects) - Selenium wiki | ||
* [PageObject](http://martinfowler.com/bliki/PageObject.html) - Martin Fowler | ||
An excerpt from the Selenium Wiki | ||
> Within your web app's UI there are areas that your tests interact with. A Page Object simply models these as objects within the test code. This reduces the amount of duplicated code and means that if the UI changes, the fix need only be applied in one place. | ||
The pattern was first introduced by the Selenium | ||
You can find more information about this design pattern here: | ||
* [Page Objects - Selenium wiki](https://code.google.com/p/selenium/wiki/PageObjects) | ||
* [PageObject - Martin Fowler](http://martinfowler.com/bliki/PageObject.html) | ||
## Usage | ||
First add the npm package to your ember-cli project | ||
Install the npm package on your ember-cli project | ||
@@ -18,2 +24,4 @@ ```sh | ||
then import the page-object helper | ||
```js | ||
@@ -24,26 +32,12 @@ import PO from '../page-object'; | ||
The previous example assumes that your test file is one level deep under | ||
`tests/` folder. i.e. `tests/unit/my-unit-test.js`. | ||
`tests/` folder. i.e. `tests/acceptance/my-acceptance-test.js`. | ||
Then you can start building your page objects as follows: | ||
```js | ||
import Ember from 'ember'; | ||
import { module, test } from 'qunit'; | ||
import startApp from '../helpers/start-app'; | ||
import PO from '../page-object'; | ||
var application; | ||
module('An Integration test', { | ||
beforeEach: function() { | ||
application = startApp(); | ||
}, | ||
afterEach: function() { | ||
Ember.run(application, 'destroy'); | ||
} | ||
}); | ||
var login = PO.build({ | ||
visit: PO.visitable('/login'), | ||
userName: PO.fillable('#username'), | ||
password: PO.fillable('#password'), | ||
submit: PO.clickable('#login'), | ||
visit: PO.visitable('/login'), | ||
userName: PO.fillable('#username'), | ||
password: PO.fillable('#password'), | ||
submit: PO.clickable('#login'), | ||
errorMessage: PO.text('.message') | ||
@@ -65,3 +59,3 @@ }); | ||
Support for tables and collections | ||
Built-in support for defining tables and collections: | ||
@@ -82,18 +76,2 @@ ```html | ||
```js | ||
import Ember from 'ember'; | ||
import { module, test } from 'qunit'; | ||
import startApp from '../helpers/start-app'; | ||
import PO from '../page-object'; | ||
var application; | ||
module('Users', { | ||
beforeEach: function() { | ||
application = startApp(); | ||
}, | ||
afterEach: function() { | ||
Ember.run(application, 'destroy'); | ||
} | ||
}); | ||
var page = PO.build({ | ||
@@ -107,3 +85,3 @@ visit: PO.visitable('/users'), | ||
firstName: PO.text('td:nth-of-type(1)'), | ||
lastName: PO.text('td:nth-of-type(2)') | ||
lastName: PO.text('td:nth-of-type(2)') | ||
} | ||
@@ -126,4 +104,23 @@ }) | ||
Check [DOCUMENTATION](./DOCUMENTATION.md) for more information. | ||
You can use ES6 destructuring to declutter even more your page definition: | ||
```js | ||
var { visitable, collection, text } = PO; | ||
var page = PO.build({ | ||
visit: visitable('/users'), | ||
users: collection({ | ||
itemScope: '#users tr', | ||
item: { | ||
firstName: text('td:nth-of-type(1)'), | ||
lastName: text('td:nth-of-type(2)') | ||
} | ||
}) | ||
}); | ||
``` | ||
Check the [DOCUMENTATION](./DOCUMENTATION.md) for more information. | ||
## Development | ||
@@ -130,0 +127,0 @@ |
@@ -1,34 +0,41 @@ | ||
import { build } from './page-object/build'; | ||
import { hasClass, notHasClass, isVisible, isHidden } from './page-object/predicates'; | ||
import { attribute, count, text, value } from './page-object/queries'; | ||
import { clickable, fillable, visitable } from './page-object/actions'; | ||
import { | ||
build, | ||
componentAttribute | ||
} from './page-object/build'; | ||
import { | ||
hasClassAttribute, | ||
notHasClassAttribute, | ||
isVisibleAttribute, | ||
isHiddenAttribute | ||
} from './page-object/predicates'; | ||
import { | ||
attributeAttribute, | ||
countAttribute, | ||
textAttribute, | ||
valueAttribute | ||
} from './page-object/queries'; | ||
import { | ||
clickableAttribute, | ||
clickOnTextAttribute, | ||
fillableAttribute, | ||
visitableAttribute | ||
} from './page-object/actions'; | ||
import { collection } from './page-object/collection'; | ||
function component(definition) { | ||
return { | ||
build: function(/*key, parent*/) { | ||
let component = build(definition); | ||
return function() { | ||
return component; | ||
}; | ||
} | ||
}; | ||
} | ||
export default { | ||
attribute, | ||
attribute: attributeAttribute, | ||
build, | ||
clickable, | ||
clickable: clickableAttribute, | ||
clickOnText: clickOnTextAttribute, | ||
collection, | ||
component, | ||
count, | ||
fillable, | ||
hasClass, | ||
isHidden, | ||
isVisible, | ||
notHasClass, | ||
text, | ||
value, | ||
visitable | ||
component: componentAttribute, | ||
count: countAttribute, | ||
fillable: fillableAttribute, | ||
hasClass: hasClassAttribute, | ||
isHidden: isHiddenAttribute, | ||
isVisible: isVisibleAttribute, | ||
notHasClass: notHasClassAttribute, | ||
text: textAttribute, | ||
value: valueAttribute, | ||
visitable: visitableAttribute | ||
}; |
/* global visit, fillIn, click */ | ||
import { qualifySelector } from './helpers'; | ||
import Attribute from './attribute'; | ||
function action(fn) { | ||
return function(selector, options = {}) { | ||
return { | ||
build: function(key, page) { | ||
return function(...args) { | ||
let qualifiedSelector = qualifySelector(options.scope || page.scope, selector); | ||
function visitable() { | ||
this.page.lastPromise = visit(this.path); | ||
page.lastPromise = fn(qualifiedSelector, ...args); | ||
return this.page; | ||
} | ||
return page; | ||
}; | ||
} | ||
}; | ||
}; | ||
function clickable() { | ||
this.page.lastPromise = click(this.qualifiedSelector()); | ||
return this.page; | ||
} | ||
export function visitable(path) { | ||
return { | ||
build: function(key, page) { | ||
return function() { | ||
page.lastPromise = visit(path); | ||
function fillable(text) { | ||
this.page.lastPromise = fillIn(this.qualifiedSelector(), text); | ||
return page; | ||
}; | ||
} | ||
}; | ||
return this.page; | ||
} | ||
export var fillable = action((selector, text) => fillIn(selector, text)); | ||
export var clickable = action((selector) => click(selector)); | ||
function clickOnText(text) { | ||
// Suppose that we have something like `<form><button>Submit</button></form>` | ||
// In this case <form> and <button> elements contains "Submit" text, so, we'll | ||
// want to __always__ click on the __last__ element that contains the text. | ||
let selector = this.qualifiedSelector(`:contains("${text}"):last`); | ||
// function clickableByText(selector, scope) { | ||
// var qualifiedSelector = qualifySelector(scope, selector); | ||
// | ||
// return function(text) { | ||
// return click('%@ :contains("%@"):last'.fmt(qualifiedSelector, text)); | ||
// }; | ||
// } | ||
this.page.lastPromise = click(selector); | ||
return this.page; | ||
} | ||
export function visitableAttribute(path) { | ||
return new Attribute(visitable, null, null, { path }); | ||
} | ||
export function clickableAttribute(selector, options = {}) { | ||
return new Attribute(clickable, selector, options); | ||
} | ||
export function fillableAttribute(selector, options = {}) { | ||
return new Attribute(fillable, selector, options); | ||
} | ||
export function clickOnTextAttribute(selector, options = {}) { | ||
return new Attribute(clickOnText, selector, options); | ||
} |
function Component() { | ||
} | ||
function isAttribute(candidate) { | ||
return $.isFunction(candidate.buildPageObjectAttribute); | ||
} | ||
function peekForAttributes(parent) { | ||
let keys = Object.keys(parent); | ||
for(let i = 0; i < keys.length; i++) { | ||
if (isAttribute(parent[keys[i]])) { | ||
return true; | ||
} | ||
} | ||
return false; | ||
} | ||
function buildComponentIfNeeded(candidate, key, parent) { | ||
if ($.isPlainObject(candidate) && peekForAttributes(candidate)) { | ||
return componentAttribute(candidate).buildPageObjectAttribute(key, parent); | ||
} | ||
return candidate; | ||
} | ||
export function componentAttribute(definition) { | ||
return { | ||
buildPageObjectAttribute: function(/*key, parent*/) { | ||
let component = build(definition); | ||
return function() { | ||
return component; | ||
}; | ||
} | ||
}; | ||
} | ||
export function build(definition) { | ||
@@ -11,3 +47,7 @@ let component = new Component(), | ||
component[key] = (attr.build) ? attr.build(key, component) : attr; | ||
if (isAttribute(attr)) { | ||
component[key] = attr.buildPageObjectAttribute(key, component); | ||
} else { | ||
component[key] = buildComponentIfNeeded(attr, key, component); | ||
} | ||
}); | ||
@@ -14,0 +54,0 @@ |
import Ember from 'ember'; | ||
import { build } from './build'; | ||
import { count } from './queries'; | ||
import { countAttribute } from './queries'; | ||
@@ -13,3 +13,3 @@ let extend = Ember.$.extend; | ||
return { | ||
build: function(/*key, page*/) { | ||
buildPageObjectAttribute: function(/*key, page*/) { | ||
let itemComponent, | ||
@@ -27,3 +27,3 @@ itemScope, | ||
if (definition.count === undefined) { | ||
definition.count = count(itemScope); | ||
definition.count = countAttribute(itemScope); | ||
} | ||
@@ -30,0 +30,0 @@ |
/* global find, findWithAssert */ | ||
import { qualifySelector } from './helpers'; | ||
import Attribute from './attribute'; | ||
export function hasClass(cssClass, selector, options = {}) { | ||
return { | ||
build: function(key, page) { | ||
return function() { | ||
let qualifiedSelector = qualifySelector(options.scope || page.scope, selector), | ||
element = findWithAssert(qualifiedSelector); | ||
function hasClass() { | ||
return this.elementOrRaise().hasClass(this.cssClass); | ||
} | ||
return element.hasClass(cssClass); | ||
}; | ||
} | ||
}; | ||
function notHasClass() { | ||
return !this.elementOrRaise().hasClass(this.cssClass); | ||
} | ||
export function notHasClass(cssClass, selector, options = {}) { | ||
return { | ||
build: function(key, page) { | ||
return function() { | ||
let qualifiedSelector = qualifySelector(options.scope || page.scope, selector), | ||
element = findWithAssert(qualifiedSelector); | ||
function isVisible() { | ||
return this.elementOrRaise().is(':visible'); | ||
} | ||
return !element.hasClass(cssClass); | ||
}; | ||
} | ||
}; | ||
function isHidden() { | ||
let element = this.element(); | ||
return (element.length > 0) ? element.is(':hidden') : true; | ||
} | ||
export function isVisible(selector, options = {}) { | ||
return { | ||
build: function(key, page) { | ||
return function() { | ||
let qualifiedSelector = qualifySelector(options.scope || page.scope, selector), | ||
element = findWithAssert(qualifiedSelector); | ||
export function notHasClassAttribute(cssClass, selector, options = {}) { | ||
return new Attribute(notHasClass, selector, options, { cssClass }); | ||
} | ||
return element.is(':visible'); | ||
}; | ||
} | ||
}; | ||
export function hasClassAttribute(cssClass, selector, options = {}) { | ||
return new Attribute(hasClass, selector, options, { cssClass }); | ||
} | ||
export function isHidden(selector, options = {}) { | ||
return { | ||
build: function(key, page) { | ||
return function() { | ||
let qualifiedSelector = qualifySelector(options.scope || page.scope, selector), | ||
element = find(qualifiedSelector); | ||
export function isVisibleAttribute(selector, options = {}) { | ||
return new Attribute(isVisible, selector, options); | ||
} | ||
return (element.length > 0) ? element.is(':hidden') : true; | ||
}; | ||
} | ||
}; | ||
export function isHiddenAttribute(selector, options = {}) { | ||
return new Attribute(isHidden, selector, options); | ||
} |
@@ -1,43 +0,34 @@ | ||
/* global findWithAssert */ | ||
import { trim } from './helpers'; | ||
import Attribute from './attribute'; | ||
import { qualifySelector, trim } from './helpers'; | ||
function attribute() { | ||
return this.elementOrRaise().attr(this.attributeName); | ||
} | ||
export function attribute(attributeName, selector, options = {}) { | ||
return { | ||
build: function(key, page) { | ||
return function(...args) { | ||
let qualifiedSelector = qualifySelector(options.scope || page.scope, selector), | ||
element = findWithAssert(qualifiedSelector); | ||
function count() { | ||
return this.element().length; | ||
} | ||
return element.attr(attributeName); | ||
}; | ||
} | ||
}; | ||
function text() { | ||
return trim(this.elementOrRaise().text()) | ||
} | ||
function query(fn, useFind = false) { | ||
return function(selector, options = {}) { | ||
return { | ||
build: function(key, page) { | ||
return function(...args) { | ||
let qualifiedSelector = qualifySelector(options.scope || page.scope, selector), | ||
element; | ||
function value() { | ||
return this.elementOrRaise().val(); | ||
} | ||
element = (useFind) ? find(qualifiedSelector) : findWithAssert(qualifiedSelector); | ||
export function attributeAttribute(attributeName, selector, options = {}) { | ||
return new Attribute(attribute, selector, options, { attributeName }); | ||
} | ||
return fn(element, ...args); | ||
}; | ||
} | ||
}; | ||
}; | ||
export function countAttribute(selector, options = {}) { | ||
return new Attribute(count, selector, options); | ||
} | ||
const count = query(elements => elements.length, true), | ||
text = query(element => trim(element.text())), | ||
value = query(element => element.val()); | ||
export function textAttribute(selector, options = {}) { | ||
return new Attribute(text, selector, options); | ||
} | ||
export { | ||
count, | ||
text, | ||
value | ||
}; | ||
export function valueAttribute(selector, options = {}) { | ||
return new Attribute(value, selector, options); | ||
} |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
8309447
33
66034
147