Skip to content
Permalink
Browse files
feat(protractor): Targets can be nested within one another
Closes #187, closes #143
  • Loading branch information
jan-molak committed Feb 19, 2019
1 parent ba9c3db commit b8f95c85c1fd9c328ae99a1132677c400899fb03
@@ -36,8 +36,8 @@ describe('Press', () => {
it('allows the actor to enter keys individually into a field', () => Bernie.attemptsTo(
Navigate.to(page),

Press.the('a').into(Form.Text_Field),
Press.the('A').into(Form.Text_Field),
Press.the('a').in(Form.Text_Field),
Press.the('A').in(Form.Text_Field),

Ensure.that(Value.of(Form.Text_Field), equals('aA')),
));
@@ -51,7 +51,7 @@ describe('Press', () => {
it('allows the actor to use keyboard shortcuts', () => Bernie.attemptsTo(
Navigate.to(page),

Press.the(Key.SHIFT, 'a').into(Form.Text_Field),
Press.the(Key.SHIFT, 'a').in(Form.Text_Field),

Ensure.that(Value.of(Form.Text_Field), equals(`A`)),
));
@@ -61,23 +61,23 @@ describe('Press', () => {
given([
{
description: 'single key',
interaction: Press.the('a').into(Form.Text_Field),
expected: `#actor types A in the text field`,
interaction: Press.the('a').in(Form.Text_Field),
expected: `#actor presses A in the text field`,
},
{
description: 'sequence of keys',
interaction: Press.the('a', 'b', 'c').into(Form.Text_Field),
expected: `#actor types A, B, C in the text field`,
interaction: Press.the('a', 'b', 'c').in(Form.Text_Field),
expected: `#actor presses A, B, C in the text field`,
},
{
description: 'keyboard shortcut',
interaction: Press.the(Key.CONTROL, 'a').into(Form.Text_Field),
expected: `#actor types Control-A in the text field`,
interaction: Press.the(Key.CONTROL, 'a').in(Form.Text_Field),
expected: `#actor presses Control-A in the text field`,
},
{
description: 'complex shortcut',
interaction: Press.the(Key.COMMAND, Key.ALT, 'a').into(Form.Text_Field),
expected: `#actor types Command-Alt-A in the text field`,
interaction: Press.the(Key.COMMAND, Key.ALT, 'a').in(Form.Text_Field),
expected: `#actor presses Command-Alt-A in the text field`,
},
]).
/** @test {Press#toString} */
@@ -0,0 +1,144 @@
import { expect } from '@integration/testing-tools';
import { contains, Ensure, equals } from '@serenity-js/assertions';
import { Actor } from '@serenity-js/core';
import { by, protractor } from 'protractor';
import { BrowseTheWeb, Navigate, Target, Text } from '../../../src';
import { pageFromTemplate } from '../../fixtures';

/** @target {Target} */
describe('Target', () => {

const shoppingListPage = pageFromTemplate(`
<html>
<body>
<div id="shopping-list-app">
<h1>Shopping <span>list</span></h1>
<h2 class="progress"><span>2</span> out of 3</h2>
<ul>
<li class="buy">oats</li>
<li class="buy">coconut milk</li>
<li class="bought">coffee</li>
</ul>
</div>
</body>
</html>
`);

class ShoppingList {
static App = Target.the('shopping list app').located(by.id('shopping-list-app'));
static Progress = Target.the('progress bar').in(ShoppingList.App).located(by.css('.progress'));
static Number_Of_Items_Left = Target.the('number of items left').in(ShoppingList.Progress).located(by.css('span'));

static Header = Target.the('header').located(by.tagName('h1'));
static List = Target.the('shopping list').located(by.tagName('ul'));
static Items = Target.all('items').in(ShoppingList.App).located(by.tagName('li'));
static Bought_Items = Target.all('bought items').in(ShoppingList.List).located(by.css('.bought'));
}

const Bernie = Actor.named('Bernie').whoCan(
BrowseTheWeb.using(protractor.browser),
);

describe('allows the actor to locate', () => {

it('a single web element matching the selector', () => Bernie.attemptsTo(
Navigate.to(shoppingListPage),

Ensure.that(Text.of(ShoppingList.Header), equals('Shopping list')),
));

it('all web elements matching the selector', () => Bernie.attemptsTo(
Navigate.to(shoppingListPage),

Ensure.that(Text.ofAll(ShoppingList.Items), contains('oats')),
));

it('an element relative to another target', () => Bernie.attemptsTo(
Navigate.to(shoppingListPage),

Ensure.that(Text.of(ShoppingList.Number_Of_Items_Left), equals('2')),
));

it('all elements relative to another target', () => Bernie.attemptsTo(
Navigate.to(shoppingListPage),

Ensure.that(Text.ofAll(ShoppingList.Bought_Items), equals(['coffee'])),
));
});

describe('provides a sensible description of', () => {
describe('an element that', () => {

it('is being targeted', () => {
expect(ShoppingList.Header.toString())
.to.equal('the header');
});

it('has been located', () => {
expect(ShoppingList.Header.answeredBy(Bernie).toString())
.to.equal('the header');
});

it('is nested', () => {
expect(ShoppingList.Number_Of_Items_Left.answeredBy(Bernie).toString())
.to.equal('the number of items left in the progress bar in the shopping list app');
});
});

describe('elements that', () => {

it('are being targeted', () => {
expect(ShoppingList.Items.toString())
.to.equal('all the items in the shopping list app');
});

it('have been located', () => {
expect(ShoppingList.Items.answeredBy(Bernie).toString())
.to.equal('all the items in the shopping list app');
});

it('are nested', () => {
expect(ShoppingList.Bought_Items.answeredBy(Bernie).toString())
.to.equal('all the bought items in the shopping list');
});
});
});

// it('allows the actor to locate first web elements matching the selector', () => Bernie.attemptsTo(
// Navigate.to(shoppingListPage),
//
// Ensure.that(Text.of(First.of(ShoppingList.Items)), contains('oats')),
// ));

// it('allows the actor to locate first web elements matching the selector and expectation', () => Bernie.attemptsTo(
// Navigate.to(shoppingListPage),
//
// Ensure.that(Text.of(First.of(ShoppingList.Items).that(in)), contains('oats')),
// ));

/*
Click.on(First.of(ShoppingList.Items))
Click.on(First.of(ShoppingList.Items).that(matches(/milk/)))
Click.on(Last.of(ShoppingList.Items))
Click.on(Last.of(ShoppingList.Items).that(matches(/milk/)))
Click.on(
First.of(
Those.of(ShoppingList.Items).whereEach(matches(/milk/))
).that(hasClassThat(matches(/bargain/))
)
Ensure.that(
Text.ofAll(
Those.of(ShoppingList.Items).whereEach(matches(/milk/))
),
includes('coconut milk')
)
*/

// it(`produces a sensible description of the question being asked`, () => {
// expect(Text.of(Target.the('header').located(by.tagName('h1'))).toString())
// .to.equal('the text of the header');
// });
});
@@ -6,7 +6,7 @@ import { by, protractor } from 'protractor';
import { BrowseTheWeb, Navigate, Target, Text } from '../../../src';
import { pageFromTemplate } from '../../fixtures';

describe('Text', function() {
describe('Text', () => {

const Bernie = Actor.named('Bernie').whoCan(
BrowseTheWeb.using(protractor.browser),
@@ -60,7 +60,7 @@ describe('Text', function() {

it(`produces sensible description of the question being asked`, () => {
expect(Text.ofAll(Shopping_List_Items).toString())
.to.equal('the text of all the shopping list items');
.to.equal('the text of the shopping list items');
});
});
});
@@ -5,7 +5,7 @@ import { by, protractor } from 'protractor';
import { BrowseTheWeb, Navigate, Target, Value } from '../../../src';
import { pageFromTemplate } from '../../fixtures';

describe('Value', function() {
describe('Value', () => {

const Bernie = Actor.named('Bernie').whoCan(
BrowseTheWeb.using(protractor.browser),
@@ -10,7 +10,7 @@ export class Press implements Interaction {

static the(...keys: string[]) {
return {
into: (field: Target<ElementFinder>) => new Press(keys, field),
in: (field: Target<ElementFinder>) => new Press(keys, field),
};
}

@@ -26,7 +26,7 @@ export class Press implements Interaction {
}

toString() {
return `#actor types ${ describeSequenceOf(this.keys) } in ${ this.field.toString() }`;
return `#actor presses ${ describeSequenceOf(this.keys) } in ${ this.field.toString() }`;
}
}

@@ -5,13 +5,25 @@ import { BrowseTheWeb } from '../abilities';
export abstract class Target<T extends WebElement> implements Question<T> {
static the(name: string) {
return {
located: (byLocator: Locator): Target<ElementFinder> => new TargetSingleElement(name, byLocator),
located: (byLocator: Locator): Target<ElementFinder> => new TargetElement(name, byLocator),
in: (parent: Target<ElementFinder>) => {
return {
located: (byLocator: Locator): Target<ElementFinder> =>
new TargetNestedElement(parent, name, byLocator),
};
},
};
}

static all(name: string) {
return {
located: (byLocator: Locator): Target<ElementArrayFinder> => new TargetMultipleElements(name, byLocator),
located: (byLocator: Locator): Target<ElementArrayFinder> => new TargetElements(name, byLocator),
in: (parent: Target<ElementFinder>) => {
return {
located: (byLocator: Locator): Target<ElementArrayFinder> =>
new TargetNestedElements(parent, name, byLocator),
};
},
};
}

@@ -24,22 +36,78 @@ export abstract class Target<T extends WebElement> implements Question<T> {
abstract answeredBy(actor: AnswersQuestions & UsesAbilities): T;
}

export class TargetSingleElement extends Target<ElementFinder> {
export class TargetElement extends Target<ElementFinder> {
answeredBy(actor: AnswersQuestions & UsesAbilities): ElementFinder {
return BrowseTheWeb.as(actor).locate(this.locator);
const elf = BrowseTheWeb.as(actor).locate(this.locator);

return override(elf, 'toString', this.toString.bind(this));
}

toString() {
return `the ${ this.name }`;
}
}

export class TargetMultipleElements extends Target<ElementArrayFinder> {
export class TargetNestedElement extends Target<ElementFinder> {

constructor(
private readonly parent: Target<ElementFinder>,
name: string,
locator: Locator,
) {
super(name, locator);
}

answeredBy(actor: AnswersQuestions & UsesAbilities): ElementFinder {
const elf = this.parent.answeredBy(actor).element(this.locator);

return override(elf, 'toString', this.toString.bind(this));
}

toString() {
return `the ${ this.name } in ${ this.parent }`;
}
}

export class TargetElements extends Target<ElementArrayFinder> {
answeredBy(actor: AnswersQuestions & UsesAbilities): ElementArrayFinder {
return BrowseTheWeb.as(actor).locateAll(this.locator);
const eaf = BrowseTheWeb.as(actor).locateAll(this.locator);

return override(eaf, 'toString', this.toString.bind(this));
}

toString() {
return `all the ${ this.name }`;
return `the ${ this.name }`;
}
}

export class TargetNestedElements extends Target<ElementArrayFinder> {

constructor(
private readonly parent: Target<ElementFinder>,
name: string,
locator: Locator,
) {
super(name, locator);
}

answeredBy(actor: AnswersQuestions & UsesAbilities): ElementArrayFinder {
const eaf = this.parent.answeredBy(actor).all(this.locator);

return override(eaf, 'toString', this.toString.bind(this));
}

toString() {
return `all the ${ this.name } in ${ this.parent }`;
}
}

function override<T extends object, K extends keyof T>(obj: T, name: K, implementation: T[K]) {
return new Proxy<T>(obj, {
get(o: T, prop: string | number) {
return prop === name
? implementation
: obj[prop];
},
});
}

0 comments on commit b8f95c8

Please sign in to comment.