Skip to content
Permalink
Browse files
feat(protractor): Pick can be used with protractor questions and inte…
…ractions
  • Loading branch information
jan-molak committed Feb 21, 2019
1 parent 4307966 commit 6f7c5bd4a3aa6bd6ab0b2e427f54e4955d042494
Showing 17 changed files with 234 additions and 165 deletions.
@@ -4,7 +4,7 @@ import { Actor, Question } from '@serenity-js/core';

import { expect } from '@integration/testing-tools';
import { given } from 'mocha-testdata';
import { equals, NoAnswerFound, Pick, startsWith } from '../src';
import { equals, NoResultsMatching, Pick, startsWith } from '../src';

describe('Pick', () => {

@@ -139,16 +139,16 @@ describe('Pick', () => {
const picked = Pick.from(q(p(animals)));

it('returns the number of answers', () =>
expect(picked.count().toString()).to.equal('number of the animals'));
expect(picked.count().toString()).to.equal('the number of the animals'));

it('picks all the items', () =>
expect(picked.all().toString()).to.equal('all of the animals'));
expect(picked.all().toString()).to.equal('the animals'));

it('picks the first item', () =>
expect(picked.first().toString()).to.equal('first of the animals'));
expect(picked.first().toString()).to.equal('the first of the animals'));

it('picks the last item', () =>
expect(picked.last().toString()).to.equal('last of the animals'));
expect(picked.last().toString()).to.equal('the last of the animals'));

given([
{ description: '1st', index: 0 },
@@ -168,7 +168,7 @@ describe('Pick', () => {

const question = Question.about<string[]>('the alphabet letters', actor => 'abcdefghijklmnopqrstuvwxyz'.split(''));

expect(Pick.from(question).get(index).toString()).to.equal(`${ description } of the alphabet letters`);
expect(Pick.from(question).get(index).toString()).to.equal(`the ${ description } of the alphabet letters`);
});
});
});
@@ -178,13 +178,13 @@ describe('Pick', () => {
const picked = Pick.from(q([]));

it('complains when you try to access the first one', () =>
expect(picked.first().answeredBy(Alexandra)).to.eventually.be.rejectedWith(NoAnswerFound, `There's no first of the animals`));
expect(picked.first().answeredBy(Alexandra)).to.eventually.be.rejectedWith(NoResultsMatching, `Can't pick the first of the animals as there are no results matching`));

it('complains when you try to access the last one', () =>
expect(picked.last().answeredBy(Alexandra)).to.eventually.be.rejectedWith(NoAnswerFound, `There's no last of the animals`));
expect(picked.last().answeredBy(Alexandra)).to.eventually.be.rejectedWith(NoResultsMatching, `Can't pick the last of the animals as there are no results matching`));

it('complains when you try to access the nth one', () =>
expect(picked.get(1).answeredBy(Alexandra)).to.eventually.be.rejectedWith(NoAnswerFound, `There's no 2nd of the animals`));
expect(picked.get(1).answeredBy(Alexandra)).to.eventually.be.rejectedWith(NoResultsMatching, `Can't pick the 2nd of the animals as there are no results matching`));

it('returns a count of 0', () =>
expect(picked.count().answeredBy(Alexandra)).to.eventually.equal(0));
@@ -1,6 +1,6 @@
import { AnswersQuestions, KnowableUnknown, Question, UsesAbilities } from '@serenity-js/core';
import { formatted } from '@serenity-js/core/lib/io';
import { NoAnswerFound } from './errors';
import { NoResultsMatching } from './errors';
import { Expectation } from './Expectation';
import { ExpectationMet } from './outcomes';

@@ -91,7 +91,7 @@ class AllMatchingAnswers<T> implements Question<Promise<T[]>> {
}

toString() {
return formatted `all of ${ this.question }`;
return formatted `${ this.question }`;
}
}

@@ -107,15 +107,15 @@ class FirstAnswer<T> implements Question<Promise<T>> {
.then(items => this.filters.processAs(actor, items))
.then(items => {
if (items[0] === undefined) {
throw new NoAnswerFound(`There's no ${ this.toString() }`);
throw new NoResultsMatching(`Can't pick ${ this.toString() } as there are no results matching`);
}

return items[0];
});
}

toString() {
return formatted `first of ${ this.question }`;
return formatted `the first of ${ this.question }`;
}
}

@@ -131,15 +131,15 @@ class LastAnswer<T> implements Question<Promise<T>> {
.then(items => this.filters.processAs(actor, items))
.then(items => {
if (items[items.length - 1] === undefined) {
throw new NoAnswerFound(`There's no ${ this.toString() }`);
throw new NoResultsMatching(`Can't pick ${ this.toString() } as there are no results matching`);
}

return items[items.length - 1];
});
}

toString() {
return formatted `last of ${ this.question }`;
return formatted `the last of ${ this.question }`;
}
}

@@ -156,15 +156,15 @@ class NthAnswer<T> implements Question<Promise<T>> {
.then(items => this.filters.processAs(actor, items))
.then(items => {
if (items[this.index] === undefined) {
throw new NoAnswerFound(`There's no ${ this.toString() }`);
throw new NoResultsMatching(`Can't pick ${ this.toString() } as there are no results matching`);
}

return items[this.index];
});
}

toString() {
return this.ordinalSuffixOf(this.index + 1) + formatted ` of ${ this.question }`;
return `the ${ this.ordinalSuffixOf(this.index + 1) } ${ formatted `of ${ this.question }` }`;
}

private ordinalSuffixOf(index: number) {
@@ -199,6 +199,6 @@ class NumberOfMatchingAnswers<T> implements Question<Promise<number>> {
}

toString() {
return formatted `number of ${ this.question }`;
return formatted `the number of ${ this.question }`;
}
}
@@ -1,7 +1,7 @@
import { RuntimeError } from '@serenity-js/core';

export class NoAnswerFound extends RuntimeError {
export class NoResultsMatching extends RuntimeError {
constructor(message: string, cause?: Error) {
super(NoAnswerFound, message, cause);
super(NoResultsMatching, message, cause);
}
}
@@ -1 +1 @@
export * from './NoAnswersFound';
export * from './NoResultsMatching';
@@ -0,0 +1,52 @@
import { Ensure, equals } from '@serenity-js/assertions';
import { Actor } from '@serenity-js/core';
import { given } from 'mocha-testdata';
import { by, protractor } from 'protractor';

import { BrowseTheWeb, CSSClasses, Navigate, Target, Value } from '../../../src';
import { pageFromTemplate } from '../../fixtures';

describe('CSSClasses', () => {

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

const testPage = pageFromTemplate(`
<html>
<body>
<ul>
<li id="no-class-attribute"></li>
<li id="empty-class-attribute" class=""></li>
<li id="class-attribute-with-whitespace-only" class=" "></li>
<li id="single-class" class="pretty"></li>
<li id="several-classes" class="pretty css classes"></li>
<li id="several-classes-with-whitespace" class=" pretty css classes "></li>
</ul>
</body>
</html>
`);

/** @test {CSSClasses} */
/** @test {CSSClasses.of} */
describe('of', () => {

given([
{ description: 'no-class-attribute', expected: [] },
{ description: 'no-class-attribute', expected: [] },
{ description: 'class-attribute-with-whitespace-only', expected: [] },
{ description: 'single-class', expected: ['pretty'] },
{ description: 'several-classes', expected: ['pretty', 'css', 'classes'] },
{ description: 'several-classes-with-whitespace', expected: ['pretty', 'css', 'classes'] },
]).
it(`allows the actor to read the 'value' attribute of a DOM element matching the locator`, ({ description, expected }) =>
Bernie.attemptsTo(
Navigate.to(testPage),

Ensure.that(
CSSClasses.of(Target.the(`Element with ${ description }`).located(by.id(description))),
equals(expected),
),
));
});
});
@@ -1,8 +1,8 @@
import { expect } from '@integration/testing-tools';
import { contains, Ensure, equals } from '@serenity-js/assertions';
import { contains, Ensure, equals, Pick } from '@serenity-js/assertions';
import { Actor } from '@serenity-js/core';
import { by, protractor } from 'protractor';
import { BrowseTheWeb, Navigate, Target, Text } from '../../../src';
import { BrowseTheWeb, Click, CSSClasses, Navigate, Target, Text } from '../../../src';
import { pageFromTemplate } from '../../fixtures';

/** @target {Target} */
@@ -67,6 +67,7 @@ describe('Target', () => {
});

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

describe('an element that', () => {

it('is being targeted', () => {
@@ -79,10 +80,10 @@ describe('Target', () => {
.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');
});
it('is nested', () =>
ShoppingList.Number_Of_Items_Left.answeredBy(Bernie).then(finder =>
expect(finder.toString()).to.equal('the number of items left in the progress bar in the shopping list app'),
));
});

describe('elements that', () => {
@@ -92,53 +93,34 @@ describe('Target', () => {
.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('have been located', () =>
ShoppingList.Items.answeredBy(Bernie)
.then(answer => expect(answer.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('are nested', () =>
ShoppingList.Bought_Items.answeredBy(Bernie)
.then(answer => expect(answer.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');
// });
describe('when combined with Pick', () => {

it('allows the actor to filter the list of web elements to pick the ones of interest', () => Bernie.attemptsTo(
Navigate.to(shoppingListPage),

Ensure.that(
Text.of(
Pick.from(ShoppingList.Items).where(CSSClasses.of, contains('buy')).last(),
),
equals('coconut milk')),
));

it('provides a sensible description of the question being asked', () => {
const question = Text.of(
Pick.from(ShoppingList.Items).where(CSSClasses.of, contains('buy')).last(),
);

expect(question.toString()).to.equal('the text of the last of all the items in the shopping list app');
});
});
});
@@ -1,19 +1,17 @@
import { AnswersQuestions, Interaction, UsesAbilities } from '@serenity-js/core';
import { AnswersQuestions, Interaction, KnowableUnknown, UsesAbilities } from '@serenity-js/core';
import { formatted } from '@serenity-js/core/lib/io';
import { ElementFinder } from 'protractor';
import { promiseOf } from '../promiseOf';
import { Target } from '../questions';

export class Clear implements Interaction {
static theValueOf(field: Target<ElementFinder>) {
static theValueOf(field: KnowableUnknown<ElementFinder>) {
return new Clear(field);
}

constructor(private readonly field: Target<ElementFinder>) {
constructor(private readonly field: KnowableUnknown<ElementFinder>) {
}

performAs(actor: UsesAbilities & AnswersQuestions): PromiseLike<void> {
return promiseOf(this.field.answeredBy(actor).clear());
return actor.answer(this.field).then(finder => finder.clear());
}

toString(): string {
@@ -1,19 +1,17 @@
import { AnswersQuestions, Interaction, UsesAbilities } from '@serenity-js/core';
import { AnswersQuestions, Interaction, KnowableUnknown, UsesAbilities } from '@serenity-js/core';
import { formatted } from '@serenity-js/core/lib/io';
import { ElementFinder } from 'protractor';
import { promiseOf } from '../promiseOf';
import { Target } from '../questions';

export class Click implements Interaction {
static on(target: Target<ElementFinder>) {
static on(target: KnowableUnknown<ElementFinder>) {
return new Click(target);
}

constructor(private readonly target: Target<ElementFinder>) {
constructor(private readonly target: KnowableUnknown<ElementFinder>) {
}

performAs(actor: UsesAbilities & AnswersQuestions): PromiseLike<void> {
return promiseOf(this.target.answeredBy(actor).click());
return actor.answer(this.target).then(finder => finder.click());
}

toString(): string {
@@ -1,23 +1,21 @@
import { AnswersQuestions, Interaction, UsesAbilities } from '@serenity-js/core';
import { AnswersQuestions, Interaction, KnowableUnknown, UsesAbilities } from '@serenity-js/core';
import { formatted } from '@serenity-js/core/lib/io';
import { ElementFinder } from 'protractor';
import { BrowseTheWeb } from '../abilities';
import { promiseOf } from '../promiseOf';
import { Target } from '../questions';

export class DoubleClick implements Interaction {
static on(target: Target<ElementFinder>) {
static on(target: KnowableUnknown<ElementFinder>) {
return new DoubleClick(target);
}

constructor(private readonly target: Target<ElementFinder>) {
constructor(private readonly target: KnowableUnknown<ElementFinder>) {
}

performAs(actor: UsesAbilities & AnswersQuestions): PromiseLike<void> {
return promiseOf(BrowseTheWeb.as(actor).actions()
.doubleClick(this.target.answeredBy(actor))
.perform(),
);
return actor.answer(this.target)
.then(finder => BrowseTheWeb.as(actor).actions()
.doubleClick(finder)
.perform());
}

toString(): string {

0 comments on commit 6f7c5bd

Please sign in to comment.