Skip to content

Commit

Permalink
feat(screenplay): The actor can now TakeNotes to assert on their co…
Browse files Browse the repository at this point in the history
…ntents later.

This change adds:
- an ability to `TakeNotes`
- a task to `TakeNote.of(q: Question)
- a task to `CompareNotes.toSeeIf(actual: Question, expectation: Expectation, topic: string)`

Where:
- `Expectation<T> = (expected: T) => Assertion<T>;`
- `Assertion<T>   = (actual: T) => PromiseLike<void>;`

For example:

```typescript
const includes = (expected: string) => (actual: string[]) => expect(actual).to.eventually.include(expected);
const actor = Actor.named('Benjamin').whoCan(TakeNotes.using(notepad));

actor.attemptsTo(
    TakeNote.of(Text.of(MarketingPopup.Voucher_Code),
    /* perform some other tasks */
    CompareNotes.toSeeIf(Text.ofAll(Checkout.Applied_Vouchers), includes, MarketingPopup.Voucher_Code),
);
 ```

 Also, the signature of the task to `See` is compatible with `CompareNotes` so that the assertions could be reused:

```typescript
const includes = (expected: string) => (actual: string[]) => expect(actual).to.eventually.include(expected);
const actor = Actor.named('Benjamin');

actor.attemptsTo(
    See.that(Text.ofAll(Checkout.Applied_Vouchers), includes('my voucher')),
);
 ```

 Closes #24
  • Loading branch information
jan-molak committed Feb 8, 2017
1 parent 8c79997 commit ab36827
Show file tree
Hide file tree
Showing 13 changed files with 247 additions and 20 deletions.
2 changes: 1 addition & 1 deletion examples/todomvc-model/src/user_interface/todo_list.ts
Expand Up @@ -5,7 +5,7 @@ export class TodoList {
static What_Needs_To_Be_Done = Target.the('"What needs to be done?" input box')
.located(by.id('new-todo'));

static Items = Target.the('List of Items')
static Items = Target.the('items on the list')
.located(by.repeater('todo in todos'));

static Filter = Target.the('filter')
Expand Down
22 changes: 9 additions & 13 deletions examples/todomvc-protractor-mocha/src/tasks/ensure.ts
Expand Up @@ -6,55 +6,51 @@ import chai = require('chai');
chai.use(require('chai-as-promised')); // tslint:disable-line:no-var-requires

export class Ensure {

static itemIsMarkedAsCompleted = (item: string): Task => new ItemMarkedAsCompleted(item);

static theListIncludes = (item: string): Task => new Includes(item);

static theListOnlyContains = (...items: string[]): Task => new Equals(items);
}

const isMarkedAs = expected => actual => chai.expect(actual).to.eventually.eql(expected);

class ItemMarkedAsCompleted implements Task {

@step('{0} ensures that \'#item\' is marked as complete')
performAs(actor: PerformsTasks): PromiseLike<void> {
return actor.attemptsTo(
See.that(ItemStatus.of(this.item), status => chai.expect(status).to.eventually.eql('completed')),
See.that(ItemStatus.of(this.item), isMarkedAs('completed')),
);
}

constructor(private item: string) {
}
}

class Equals implements Task {
const equals = expected => actual => chai.expect(actual).to.eventually.eql(expected);

private static fn = expected => actual => chai.expect(actual).to.eventually.eql(expected);
class Equals implements Task {

@step('{0} ensures that the list contains only #expectedItems')
performAs(actor: PerformsTasks): PromiseLike<void> {
return actor.attemptsTo(
See.that(TodoListItems.Displayed, Equals.fn(this.expectedItems)),
See.that(TodoListItems.Displayed, equals(this.expectedItems)),
);
}

constructor(private expectedItems: string[]) {
}

// used in @step as #description
private description() { // tslint:disable-line:no-unused-variable
return `"${ this.expectedItems.join('", "') }"`;
}
}

class Includes implements Task {
const includes = expected => actual => chai.expect(actual).to.eventually.include(expected);

private static fn = expected => actual => chai.expect(actual).to.eventually.include(expected);
class Includes implements Task {

@step('{0} ensures that the list includes #expectedItem')
performAs(actor: PerformsTasks): PromiseLike<void> {
return actor.attemptsTo(
See.that(TodoListItems.Displayed, Includes.fn(this.expectedItem)),
See.that(TodoListItems.Displayed, includes(this.expectedItem)),
);
}

Expand Down
157 changes: 157 additions & 0 deletions spec/api/screenplay/abilities/take_notes.spec.ts
@@ -0,0 +1,157 @@
import sinon = require('sinon');
import expect = require('../../../expect');
import {
Actor,
Notepad,
Question,
TakeNote,
TakeNotes,
UsesAbilities,
} from '../../../../src/serenity/screenplay';
import { Performable } from '../../../../src/serenity/screenplay/performables';

import { step } from '../../../../src/serenity/recording/step_annotation';

import { AnswersQuestions } from '../../../../src/serenity/screenplay/actor';
import { Expectation } from '../../../../src/serenity/screenplay/expectations';
import { OneOrMany } from '../../../../src/serenity/screenplay/lists';

describe('Abilities', () => {

describe('TakeNotes', () => {

let notepad: Notepad;

const include = (expected: string) => (actual: string[]) => expect(actual).to.eventually.include(expected),
includeAllOf = (expected: string[]) => (actual: string[]) => expect(actual).to.eventually.include.members(expected),
equals = <T>(expected: T) => (actual: T) => expect(actual).to.eventually.equal(expected);

beforeEach(() => notepad = {});

it ('stores notes in a notepad as promises to be resolved', () => {
let displayedVoucher = MyVoucherCode.shownAs('SUMMER2017');

let actor = Actor.named('Benjamin').whoCan(TakeNotes.using(notepad));

return actor.attemptsTo(
TakeNote.of(displayedVoucher).as('my voucher'),
).
then(() => notepad['my voucher']).
then(voucher => expect(voucher).to.equal('SUMMER2017'));
});

it('stores notes using a topic name derived from the question', () => {
let displayedVoucher = MyVoucherCode.shownAs('SUMMER2017'),
availableVoucher = AvailableVoucher.of('SUMMER2017');

let actor = Actor.named('Benjamin').whoCan(TakeNotes.usingAnEmptyNotepad());

return actor.attemptsTo(
TakeNote.of(displayedVoucher),
CompareNotes.toSeeIf(availableVoucher, equals, displayedVoucher),
);
});

it ('allows the Actor to remember a thing they\'ve seen', () => {
let displayedVoucher = MyVoucherCode.shownAs('SUMMER2017'),
availableVoucher = AvailableVoucher.of('SUMMER2017');

let actor = Actor.named('Benjamin').whoCan(TakeNotes.using(notepad));

return actor.attemptsTo(
TakeNote.of(displayedVoucher),
/* perform some other tasks */
CompareNotes.toSeeIf(availableVoucher, equals, displayedVoucher),
);
});

it ('allows the Actor to remember several things they\'ve seen one after another', () => {
let displayedVoucher = MyVoucherCode.shownAs('SUMMER2017'),
otherDisplayedVoucher = MyVoucherCode.shownAs('50_OFF'),
availableVouchers = AvailableVouchers.of('SUMMER2017', '50_OFF');

let actor = Actor.named('Benjamin').whoCan(TakeNotes.using(notepad));

return actor.attemptsTo(
TakeNote.of(displayedVoucher).as('my voucher'),
/* some other tasks */
TakeNote.of(otherDisplayedVoucher).as('my other voucher'),
/* some other tasks */
CompareNotes.toSeeIf(availableVouchers, include, 'my voucher'),
/* some other tasks */
CompareNotes.toSeeIf(availableVouchers, include, 'my other voucher'),
);
});

it ('allows the Actor to remember several things they\'ve seen at once', () => {
let displayedVouchers = MyVoucherCodes.shownAs('SUMMER2017', 'SPRINGCLEANING'),
availableVouchers = AvailableVouchers.of('SUMMER2017', '50_OFF', 'SPRINGCLEANING');

let actor = Actor.named('Benjamin').whoCan(TakeNotes.using(notepad));

return actor.attemptsTo(
TakeNote.of(displayedVouchers).as('my vouchers'),
CompareNotes.toSeeIf(availableVouchers, includeAllOf, 'my vouchers'),
);
});

it ('allows the Actor to complain if you ask them about the thing they have no notes on', () => {
let availableVouchers = AvailableVouchers.of('SUMMER2017', '5OFF', 'SPRINGCLEANING');

let actor = Actor.named('Benjamin').whoCan(TakeNotes.using(notepad));

return expect(actor.attemptsTo(
CompareNotes.toSeeIf(availableVouchers, include, 'my voucher'),
)).to.be.rejectedWith('I don\'t have any notes on the topic of "my voucher"');
});
});
});

export class CompareNotes<S> implements Performable {
static toSeeIf<A>(actual: Question<OneOrMany<A>>, expectation: Expectation<OneOrMany<A>>, topic: { toString: () => string }) {
return new CompareNotes<A>(actual, expectation, topic.toString());
}

@step('{0} compares notes on #actual')
performAs(actor: UsesAbilities & AnswersQuestions): PromiseLike<void> {
return TakeNotes.
as(actor).
read(this.topic).
then(expected => this.expect(expected)(actor.toSee(this.actual)));
}

constructor(private actual: Question<OneOrMany<S>>, private expect: Expectation<OneOrMany<S>>, private topic: string) {
}
}

class MyVoucherCode implements Question<string> {
static shownAs = (someValue: string) => new MyVoucherCode(someValue);
answeredBy = (actor: UsesAbilities) => Promise.resolve(this.value);
toString = () => 'My voucher code';
constructor(private value: string) {
}
}

class MyVoucherCodes implements Question<string[]> {
static shownAs = (...someValues: string[]) => new MyVoucherCodes(someValues);
answeredBy = (actor: UsesAbilities) => Promise.resolve(this.values);
toString = () => 'My voucher codes';
constructor(private values: string[]) {
}
}

class AvailableVoucher implements Question<string> {
static of = (someValue: string) => new AvailableVoucher(someValue);
answeredBy = (actor: UsesAbilities) => Promise.resolve(this.value);
toString = () => 'Available voucher';
constructor(private value: string) {
}
}

class AvailableVouchers implements Question<string[]> {
static of = (...someValues: string[]) => new AvailableVouchers(someValues);
answeredBy = (actor: UsesAbilities) => Promise.resolve(this.values);
toString = () => 'Available vouchers';
constructor(private values: string[]) {
}
}
1 change: 1 addition & 0 deletions src/serenity/screenplay/abilities/index.ts
@@ -0,0 +1 @@
export * from './take_notes';
26 changes: 26 additions & 0 deletions src/serenity/screenplay/abilities/take_notes.ts
@@ -0,0 +1,26 @@
import { Ability, UsesAbilities } from '../actor';

export interface Notepad {
[x: string]: PromiseLike<any>;
}

export class TakeNotes implements Ability {

static usingAnEmptyNotepad = () => TakeNotes.using({});
static using = (notepad: Notepad) => new TakeNotes(notepad);

static as(actor: UsesAbilities): TakeNotes {
return actor.abilityTo(TakeNotes);
}

note = (topic: string, contents: PromiseLike<any> | any) => (this.notepad[topic] = contents, Promise.resolve());

read(topic: string): PromiseLike<any> {
return !! this.notepad[topic]
? Promise.resolve(this.notepad[topic])
: Promise.reject(new Error(`I don\'t have any notes on the topic of "${ topic }"`));
}

constructor(private notepad: Notepad) {
}
}
2 changes: 2 additions & 0 deletions src/serenity/screenplay/expectations.ts
@@ -0,0 +1,2 @@
export type Assertion<T> = (actual: T) => PromiseLike<void>;
export type Expectation<T> = (expected: T) => Assertion<T>;
2 changes: 2 additions & 0 deletions src/serenity/screenplay/index.ts
@@ -1,4 +1,6 @@
export * from './abilities';
export * from './actor';
export * from './interactions';
export * from './performables';
export * from './question';
export * from './tasks';
1 change: 1 addition & 0 deletions src/serenity/screenplay/interactions/index.ts
@@ -0,0 +1 @@
export * from './take_note';
16 changes: 16 additions & 0 deletions src/serenity/screenplay/interactions/take_note.ts
@@ -0,0 +1,16 @@
import { step } from '../../recording/step_annotation';
import { Interaction, Question, TakeNotes, UsesAbilities } from '../index';

export class TakeNote<T> implements Interaction {
static of = <Answer>(question: Question<Answer>) => new TakeNote<Answer>(question, question);

as = (topic: string) => (this.topic = topic, this);

@step('{0} takes a note of #topic')
performAs(actor: UsesAbilities): PromiseLike<void> {
return TakeNotes.as(actor).note(this.topic.toString(), this.question.answeredBy(actor));
}

constructor(private question: Question<T>, private topic: { toString: () => string }) {
}
}
2 changes: 2 additions & 0 deletions src/serenity/screenplay/lists.ts
@@ -0,0 +1,2 @@
export type List<Item> = Item[];
export type OneOrMany<T> = T | List<T>;
24 changes: 24 additions & 0 deletions src/serenity/screenplay/tasks/compare_notes.ts
@@ -0,0 +1,24 @@
import { step } from '../../recording/step_annotation';
import { TakeNotes } from '../abilities';
import { AnswersQuestions, UsesAbilities } from '../actor';
import { Expectation } from '../expectations';
import { OneOrMany } from '../lists';
import { Performable } from '../performables';
import { Question } from '../question';

export class CompareNotes<S> implements Performable {
static toSeeIf<A>(actual: Question<OneOrMany<A>>, expectation: Expectation<OneOrMany<A>>, topic: { toString: () => string }) {
return new CompareNotes<A>(actual, expectation, topic.toString());
}

@step('{0} compares notes on #actual')
performAs(actor: UsesAbilities & AnswersQuestions): PromiseLike<void> {
return TakeNotes.
as(actor).
read(this.topic).
then(expected => this.expect(expected)(actor.toSee(this.actual)));
}

constructor(private actual: Question<OneOrMany<S>>, private expect: Expectation<OneOrMany<S>>, private topic: string) {
}
}
1 change: 1 addition & 0 deletions src/serenity/screenplay/tasks/index.ts
@@ -1 +1,2 @@
export * from './compare_notes'
export * from './see';
11 changes: 5 additions & 6 deletions src/serenity/screenplay/tasks/see.ts
@@ -1,18 +1,17 @@
import { AnswersQuestions, Performable, Question } from '..';
import { step } from '../../recording/step_annotation';

export type Expectation<S> = (subject: S) => PromiseLike<void>;
import { Assertion } from '../expectations';

export class See<S> implements Performable {
static that<S>(subject: Question<S>, verifier: Expectation<S>) {
return new See(subject, verifier);
static that<T>(question: Question<T>, assertion: Assertion<T>) {
return new See<T>(question, assertion);
}

@step('{0} looks at #question')
performAs(actor: AnswersQuestions): PromiseLike<void> {
return this.expect(actor.toSee(this.question));
return this.assert(actor.toSee(this.question));
}

constructor(private question: Question<S>, private expect: Expectation<S>) {
constructor(private question: Question<S>, private assert: Assertion<S>) {
}
}

0 comments on commit ab36827

Please sign in to comment.