Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
feat(core): The ability to TakeNotes and the associated TakeNote.of(q…
…uestion), which makes the Actor remember the answer to a question and Note.of(question), which makes the Actor retrieve the remembered value.

#318
  • Loading branch information
jan-molak committed Aug 5, 2019
1 parent bfa9cc6 commit a0e7f99
Show file tree
Hide file tree
Showing 10 changed files with 284 additions and 0 deletions.
10 changes: 10 additions & 0 deletions packages/core/spec/screenplay/EnsureSame.ts
@@ -0,0 +1,10 @@
import { AssertionError } from '../../src/errors';
import { Interaction, Question } from '../../src/screenplay';

export const EnsureSame = <T>(actual: Question<Promise<T>> | Question<T>, expected: T) =>
Interaction.where(`#actor ensures that both values are the same`, actor =>
actor.answer(actual).then(answer => {
if (answer !== expected) {
throw new AssertionError(`Expected ${ answer } to be the same as ${ expected }`, expected, actual);
}
}));
29 changes: 29 additions & 0 deletions packages/core/spec/screenplay/interactions/TakeNote.spec.ts
@@ -0,0 +1,29 @@
import * as sinon from 'sinon';
import { Actor, Note, Question, TakeNote, TakeNotes } from '../../../src/screenplay';
import { Stage } from '../../../src/stage';
import { EnsureSame } from '../EnsureSame';

describe('TakeNote', () => {

const
expectedHobby = 'DYI',
NameOfAHobby = () => Question.about(`the name of a hobby`, someActor => Promise.resolve(expectedHobby));

let stage: sinon.SinonStubbedInstance<Stage>,
Noah: Actor;

beforeEach(() => {
stage = sinon.createStubInstance(Stage);

Noah = new Actor('Noah', stage as unknown as Stage).whoCan(TakeNotes.usingAnEmptyNotepad());
});

/**
* @test {TakeNotes}
* @test {TakeNote}
*/
it('enables the Actor to remember an answer to a Question', () => Noah.attemptsTo(
TakeNote.of(NameOfAHobby()),
EnsureSame(Note.of(NameOfAHobby()), expectedHobby),
));
});
45 changes: 45 additions & 0 deletions packages/core/spec/screenplay/questions/Note.spec.ts
@@ -0,0 +1,45 @@
import * as sinon from 'sinon';
import { LogicError } from '../../../src/errors';
import { Ability, Actor, Log, Note, Question, TakeNotes } from '../../../src/screenplay';
import { Stage } from '../../../src/stage';
import { expect } from '../../expect';
import { EnsureSame } from '../EnsureSame';

describe('Note', () => {
const
expectedHobby = 'DYI',
NameOfAHobby = () => Question.about(`the name of a hobby`, someActor => Promise.resolve(expectedHobby));

let stage: sinon.SinonStubbedInstance<Stage>,
Noah: Actor;

beforeEach(() => {
stage = sinon.createStubInstance(Stage);

Noah = new Actor('Noah', stage as unknown as Stage)
.whoCan(TakeNotes.usingAnEmptyNotepad());
});

/**
* @test {TakeNotes}
* @test {Note}
*/
it('enables the actor to recall the answer to a given question', () =>
expect(actorWhoCan(new TakeNotes({ [NameOfAHobby().toString()]: 'DYI' })).attemptsTo(
EnsureSame(Note.of(NameOfAHobby()), 'DYI'),
)));

/**
* @test {TakeNotes}
* @test {Note}
*/
it('complains if no answer to a given question has ever been remembered', () =>
expect(actorWhoCan(TakeNotes.usingAnEmptyNotepad()).attemptsTo(
Log.the(Note.of(NameOfAHobby())),
)).to.be.rejectedWith(LogicError, 'The answer to "the name of a hobby" has never been recorded'));

function actorWhoCan(...abilities: Ability[]): Actor {
return new Actor('Noah', stage as unknown as Stage)
.whoCan(...abilities);
}
});
79 changes: 79 additions & 0 deletions packages/core/src/screenplay/abilities/TakeNotes.ts
@@ -0,0 +1,79 @@
import { LogicError } from '../../errors';
import { Ability } from '../Ability';
import { AnswersQuestions, UsesAbilities } from '../actor';
import { Question } from '../Question';

/**
* @desc
* Enables the {@link Actor} to remember an answer to a given {@link Question},
* and recall it later.
*
* @example
* import { Note, TakeNote, TakeNotes } from '@serenity-js/core'
* import { BrowseTheWeb, Target, Text } from '@serenity-js/protractor'
* import { by, protractor } from 'protractor';
*
* const VoucherCode = () => Target.the('voucher code').located(by.id('voucher'));
* const AppliedVoucherCode = () => Target.the('voucher code').located(by.id('applied-voucher'));
*
* const actor = Actor.named('Noah').whoCan(
* TakeNotes.usingAnEmptyNotepad(),
* BrowseTheWeb.using(protractor.browser),
* );
*
* actor.attemptsTo(
* TakeNote.of(Text.of(VoucherCode())),
* // ... add the product to a basket, go to checkout, etc.
* Ensure.that(AppliedVoucherCode(), equals(VoucherCode())),
* );
*
* @see {@link Note}
* @see {@link TakeNote}
*
* @implements {Ability}
*/
export class TakeNotes implements Ability {
/**
* @returns {TakeNotes}
*/
static usingAnEmptyNotepad(): TakeNotes {
return new TakeNotes({});
}

/**
* @param {UsesAbilities & AnswersQuestions} actor
* @returns {TakeNotes}
*/
static as(actor: UsesAbilities & AnswersQuestions): TakeNotes {
return actor.abilityTo(TakeNotes);
}

/**
* @param {object} notepad
*/
constructor(private readonly notepad: { [key: string]: any }) {
}

/**
* Records the answer to a given {@link Question}
*
* @param {Question<Promise<Answer>> | Question<Answer>} question
* @param {Promise<Answer> | Answer} value
*/
record<Answer>(question: Question<Promise<Answer>> | Question<Answer>, value: Promise<Answer> | Answer): void {
this.notepad[question.toString()] = value;
}

/**
* Recalls the answer to a given {@link Question}
*
* @param {Question<Promise<Answer>> | Question<Answer>} question
*/
answerTo<Answer>(question: Question<Promise<Answer>> | Question<Answer>): Promise<Answer> {
const key = question.toString();

return ! ~Object.keys(this.notepad).indexOf(key)
? Promise.reject(new LogicError(`The answer to "${ key }" has never been recorded`))
: Promise.resolve(this.notepad[key]);
}
}
1 change: 1 addition & 0 deletions packages/core/src/screenplay/abilities/index.ts
@@ -0,0 +1 @@
export * from './TakeNotes';
1 change: 1 addition & 0 deletions packages/core/src/screenplay/index.ts
@@ -1,6 +1,7 @@
export * from './Ability';
export * from './AbilityType';
export * from './Activity';
export * from './abilities';
export * from './actor';
export * from './Answerable';
export * from './Interaction';
Expand Down
59 changes: 59 additions & 0 deletions packages/core/src/screenplay/interactions/TakeNote.ts
@@ -0,0 +1,59 @@
import { TakeNotes } from '../abilities';
import { AnswersQuestions, UsesAbilities } from '../actor';
import { Interaction } from '../Interaction';
import { Question } from '../Question';

/**
* @desc
* Enables the {@link Actor} to remember an answer to a given {@link Question},
* and recall it later.
*
* @example
* import { Note, TakeNote, TakeNotes } from '@serenity-js/core'
* import { BrowseTheWeb, Target, Text } from '@serenity-js/protractor'
* import { by, protractor } from 'protractor';
*
* const VoucherCode = () => Target.the('voucher code').located(by.id('voucher'));
* const AppliedVoucherCode = () => Target.the('voucher code').located(by.id('applied-voucher'));
*
* const actor = Actor.named('Noah').whoCan(
* TakeNotes.usingAnEmptyNotepad(),
* BrowseTheWeb.using(protractor.browser),
* );
*
* actor.attemptsTo(
* TakeNote.of(Text.of(VoucherCode())),
* // ... add the product to a basket, go to checkout, etc.
* Ensure.that(AppliedVoucherCode(), equals(VoucherCode())),
* );
*
* @see {@link Note}
* @see {@link TakeNotes}
*
* @extends {Interaction}
*/
export class TakeNote<Answer> extends Interaction {

/**
* @param {Question<Promise<A>> | Question<A>} question
*/
static of<A>(question: Question<Promise<A>> | Question<A>) {
return new TakeNote<A>(question);
}

/**
* @param {Question<Promise<Answer>> | Question<Answer>} question
*/
constructor(private readonly question: Question<Promise<Answer>> | Question<Answer>) {
super();
}

/**
* @param {UsesAbilities & AnswersQuestions} actor
* @returns {PromiseLike<void>}
*/
performAs(actor: UsesAbilities & AnswersQuestions): PromiseLike<void> {
return actor.answer(this.question)
.then(answer => TakeNotes.as(actor).record(this.question, answer));
}
}
1 change: 1 addition & 0 deletions packages/core/src/screenplay/interactions/index.ts
@@ -1,2 +1,3 @@
export * from './Log';
export * from './See';
export * from './TakeNote';
58 changes: 58 additions & 0 deletions packages/core/src/screenplay/questions/Note.ts
@@ -0,0 +1,58 @@
import { TakeNotes } from '../abilities';
import { AnswersQuestions, UsesAbilities } from '../actor';
import { Question } from '../Question';

/**
* @desc
* Enables the {@link Actor} to recall an answer to a given {@link Question},
* recorded using {@link TakeNote}.
*
* @example
* import { Note, TakeNote, TakeNotes } from '@serenity-js/core'
* import { BrowseTheWeb, Target, Text } from '@serenity-js/protractor'
* import { by, protractor } from 'protractor';
*
* const VoucherCode = () => Target.the('voucher code').located(by.id('voucher'));
* const AppliedVoucherCode = () => Target.the('voucher code').located(by.id('applied-voucher'));
*
* const actor = Actor.named('Noah').whoCan(
* TakeNotes.usingAnEmptyNotepad(),
* BrowseTheWeb.using(protractor.browser),
* );
*
* actor.attemptsTo(
* TakeNote.of(Text.of(VoucherCode())),
* // ... add the product to a basket, go to checkout, etc.
* Ensure.that(AppliedVoucherCode(), equals(VoucherCode())),
* );
*
* @see {@link TakeNote}
* @see {@link TakeNotes}
*
* @extends {Question}
*/
export class Note<Answer> extends Question<Promise<Answer>> {

/**
* @param {Question<Promise<A>> | Question<A>} question
* @returns {Note<A>}
*/
static of<A>(question: Question<Promise<A>> | Question<A>) {
return new Note<A>(question);
}

/**
* @param {Question<Promise<Answer>> | Question<Answer>} question
*/
constructor(private readonly question: Question<Promise<Answer>> | Question<Answer>) {
super();
}

/**
* @param {AnswersQuestions & UsesAbilities} actor
* @returns {Promise<Answer>}
*/
answeredBy(actor: AnswersQuestions & UsesAbilities): Promise<Answer> {
return TakeNotes.as(actor).answerTo(this.question);
}
}
1 change: 1 addition & 0 deletions packages/core/src/screenplay/questions/index.ts
@@ -1 +1,2 @@
export * from './Note';
export * from './Transform';

0 comments on commit a0e7f99

Please sign in to comment.