Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix scopes #12

Merged
merged 6 commits into from
May 19, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
212 changes: 176 additions & 36 deletions DOCUMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@ Table of content
* [Components](#components)
* [`.collection`](#collection)
* [`.component`](#component)
* [Page and component `scope`](#page-and-component-scope)
* [Attribute options](#attribute-options)
* [`scope`](#attribute-scope)
* [`index`](#index)
* [Scopes](#scopes)

## Setup

Expand Down Expand Up @@ -570,7 +570,67 @@ var page = PO.build({

Note that if the plain object doesn't have attributes defined, the object is returned as is.

## Page and component `scope`
## Attribute options

A set of options can be passed as parameters when defining attributes.

### Attribute `scope`

The `scope` option can be used to override the page's `scope` configuration.

Given the following HTML

```html
<div class="article">
<p>Lorem ipsum dolor</p>
</div>
<div class="footer">
<p>Copyright 2015 - Acme Inc.</p>
</p>
```

the following configuration will match the footer element

```js
var page = PO.build({
scope: '.article',

textBody: PO.text('p'),

copyrightNotice: PO.text('p', { scope: '.footer' })
});

andThen(function() {
assert.equal(page.copyrightNotice(), 'Copyright 2015 - Acme Inc.');
});
```

### `index`

The `index` option can be used to reduce the set of matched elements to the one
at the specified index.

Given the following HTML

```html
<span>Lorem</span>
<span>ipsum</span>
<span>dolor</span>
```

the following configuration will match the second `span` element

```js
var page = PO.build({
word: PO.text('span', { index: 2 })
});

andThen(function() {
assert.equal(page.word(), 'ipsum'); // => ok
});
```

## Scopes

The `scope` attribute can be used to reduce the set of matched elements to the
ones enclosed by the given selector.
Expand All @@ -583,7 +643,7 @@ Given the following HTML
</div>
<div class="footer">
<p>Copyright 2015 - Acme Inc.</p>
</p>
</div>
```

the following configuration will match the article paragraph element
Expand Down Expand Up @@ -638,62 +698,142 @@ andThen(function() {
});
```

## Attribute options
### `collection` inherits parent scope by default

A set of options can be passed as parameters when defining attributes.
```html
<div class="todo">
<input type="text" value="invalid value" class="error" placeholder="To do..." />
<input type="text" placeholder="To do..." />
<input type="text" placeholder="To do..." />
<input type="text" placeholder="To do..." />

### Attribute `scope`
<button>Create</button>
</div>
```

The `scope` option can be used to override the page's `scope` configuration.
```js
var page = PageObject.build({
scope: '.todo',

Given the following HTML
todos: collection({
itemScope: 'input',

```html
<div class="article">
<p>Lorem ipsum dolor</p>
</div>
<div class="footer">
<p>Copyright 2015 - Acme Inc.</p>
</p>
item: {
value: value(),
hasError: hasClass('error')
},

create: clickable('button')
});
});
```

the following configuration will match the footer element
| | translates to |
| ------ | -------- |
| `page.todos().create()` | `click('.todo button')` |
| `page.todos(1).value()` | `find('.todo input:nth-of-type(1)').val()` |

You can reset parent scope by setting the `scope` attribute on the collection declaration.

```js
var page = PO.build({
scope: '.article',
var page = PageObject.build({
scope: '.todo',

textBody: PO.text('p'),
todos: collection({
scope: '',
itemScope: 'input',

copyrightNotice: PO.text('p', { scope: '.footer' })
});
item: {
value: value(),
hasError: hasClass('error')
},

andThen(function() {
assert.equal(page.copyrightNotice(), 'Copyright 2015 - Acme Inc.');
create: clickable('button')
});
});
```

### `index`
| | translates to |
| ------ | -------- |
| `page.todos().create()` | `click('button')` |
| `page.todos(1).value()` | `find('input:nth-of-type(1)').val()` |

The `index` option can be used to reduce the set of matched elements to the one
at the specified index.

Given the following HTML
`itemScope` is inherited as default scope on components defined inside the item object.

```html
<span>Lorem</span>
<span>ipsum</span>
<span>dolor</span>
<ul class="todos">
<li>
<span>To do</span>
<input value="" />
</li>
...
</ul>
```

```js
var page = PageObject.build({
scope: '.todos',

todos: collection({
itemScope: 'li',

item: {
label: text('span'),
input: {
value: value('input')
}
}
});
});
```

the following cofiguration will match the second `span` element
| | translates to |
| ------ | ------------ |
| `page.todos(1).input().value()` | `find('.todos li:nth-of-child(1) input).val()` |

### `component` inherits parent scope by default

```html
<div class="search">
<input placeholder="Search..." />
<button>Search</button>
</div>
```

```js
var page = PO.build({
word: PO.text('span', { index: 2 })
var page = PageObject.build({
search: {
scope: '.search',

input: {
fillIn: fillable('input'),
value: value('input')
}
}
});
```

andThen(function() {
assert.equal(page.word(), 'ipsum'); // => ok
| | translates |
| ------- | -------- |
| `page.search().input().value()` | `find('.search input').val()` |

You can reset parent scope by setting the `scope` attribute on the component declaration.

```js
var page = PageObject.build({
search: {
scope: '.search',

input: {
scope: 'input',

fillIn: fillable(),
value: value()
}
}
});
```

| | translates |
| ------- | -------- |
| `page.search().input().value()` | `find('input').val()` |
14 changes: 12 additions & 2 deletions test-support/page-object/build.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { isNullOrUndefined } from './helpers';

function Component() {
}

Expand Down Expand Up @@ -27,20 +29,28 @@ function buildComponentIfNeeded(candidate, key, parent) {

export function componentAttribute(definition) {
return {
buildPageObjectAttribute: function(/*key, parent*/) {
buildPageObjectAttribute: function(key, parent) {
let component = build(definition);

if (isNullOrUndefined(component.scope)) {
component.scope = parent.scope;
}

return function() {
return component;
};
}
};
}

export function build(definition) {
export function build(definition, key, parent) {
let component = new Component(),
keys = Object.keys(definition);

if (isNullOrUndefined(component.scope)) {
component.scope = definition.scope;
}

keys.forEach(function(key) {
let attr = definition[key];

Expand Down
55 changes: 43 additions & 12 deletions test-support/page-object/collection.js
Original file line number Diff line number Diff line change
@@ -1,38 +1,69 @@
import Ember from 'ember';
import { build } from './build';
import { countAttribute } from './queries';
import {
isNullOrUndefined,
qualifySelector
} from './helpers';

let extend = Ember.$.extend;

function dynamicScope(base, index) {
function shallowCopyAndExtend(...objects) {
return extend({}, ...objects);
}

function scopeWithIndex(base, index) {
return `${base}:nth-of-type(${index})`;
}

function plugAttribute(definition, attributeName, attributeDefinition, ...attributeParams) {
if (isNullOrUndefined(definition[attributeName])) {
definition[attributeName] = attributeDefinition(...attributeParams);
}
}

function extract(object, name) {
let attribute = object[name];

delete object[name];

return attribute;
}

export function collection(definition) {
return {
buildPageObjectAttribute: function(/*key, page*/) {
buildPageObjectAttribute: function(key, parent) {
let itemComponent,
itemScope,
collectionScope,
collectionComponent;

itemComponent = definition.item;
itemScope = definition.itemScope;

delete definition.item;
delete definition.itemScope;
itemComponent = extract(definition, 'item');
itemScope = extract(definition, 'itemScope');

// Add count attribute
if (definition.count === undefined) {
definition.count = countAttribute(itemScope);
}
plugAttribute(definition, 'count', countAttribute, itemScope);

collectionComponent = build(definition, key, parent);

collectionComponent = build(definition);
if (isNullOrUndefined(collectionComponent.scope)) {
collectionScope = parent.scope;
} else {
collectionScope = collectionComponent.scope;
}

return function(index) {
let component;

if (index) {
component = build(extend({}, itemComponent, { scope: dynamicScope(itemScope, index) }));
component = build(
shallowCopyAndExtend(
itemComponent,
{ scope: qualifySelector(collectionScope, scopeWithIndex(itemScope, index)) }
),
key,
parent
);
} else {
component = collectionComponent;
}
Expand Down
4 changes: 4 additions & 0 deletions test-support/page-object/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,7 @@ export function qualifySelector(...selectors) {
export function trim(text) {
return Ember.$.trim(text);
}

export function isNullOrUndefined(object) {
return object === undefined || object === null;
}
Loading