Skip to content
Permalink
Browse files
feat(core): discardable Abilities and shared notes
- Abilities implementing the new Discardable interface are automatically discarded after every
scenario. This allows them to free up any resources they're no longer using, such as a database or a
websocket connection, for example. | The ability to TakeNotes.usingASharedNotepad can be used to
share notes between the actors. This is useful when you have one actor start up the local test
server, and the other one navigate to it in their browser. Please see the API docs for examples.
  • Loading branch information
jan-molak committed May 14, 2020
1 parent a77091a commit 6cc2e2c936e20004f3e542e51f9fec602eba9093
Showing 18 changed files with 463 additions and 121 deletions.
@@ -31,6 +31,8 @@ describe('@serenity-js/jasmine', function () {
.next(InteractionStarts, event => expect(event.value.name).to.equal(new Name(`Jasmine navigates to 'chrome://version/'`)))
.next(SceneFinishes, event => expect(event.value.name).to.equal(new Name('A scenario passes the first time')))
.next(AsyncOperationAttempted, event => expect(event.taskDescription).to.equal(new Description('[ProtractorReporter] Invoking ProtractorRunner.afterEach...')))
.next(AsyncOperationAttempted, event => expect(event.taskDescription).to.equal(new Description('[Actor] Jasmine discards abilities...')))
.next(AsyncOperationCompleted, event => expect(event.taskDescription).to.equal(new Description('[Actor] Jasmine discarded abilities successfully')))
.next(AsyncOperationCompleted, event => expect(event.taskDescription).to.equal(new Description('[ProtractorReporter] ProtractorRunner.afterEach succeeded')))
.next(SceneFinished, event => expect(event.outcome).to.equal(new ExecutionSuccessful()))

@@ -39,6 +41,8 @@ describe('@serenity-js/jasmine', function () {
.next(InteractionStarts, event => expect(event.value.name).to.equal(new Name(`Jasmine navigates to 'chrome://accessibility/'`)))
.next(SceneFinishes, event => expect(event.value.name).to.equal(new Name('A scenario passes the second time')))
.next(AsyncOperationAttempted, event => expect(event.taskDescription).to.equal(new Description('[ProtractorReporter] Invoking ProtractorRunner.afterEach...')))
.next(AsyncOperationAttempted, event => expect(event.taskDescription).to.equal(new Description('[Actor] Jasmine discards abilities...')))
.next(AsyncOperationCompleted, event => expect(event.taskDescription).to.equal(new Description('[Actor] Jasmine discarded abilities successfully')))
.next(AsyncOperationCompleted, event => expect(event.taskDescription).to.equal(new Description('[ProtractorReporter] ProtractorRunner.afterEach succeeded')))
.next(SceneFinished, event => expect(event.outcome).to.equal(new ExecutionSuccessful()))

@@ -47,6 +51,8 @@ describe('@serenity-js/jasmine', function () {
.next(InteractionStarts, event => expect(event.value.name).to.equal(new Name(`Jasmine navigates to 'chrome://chrome-urls/'`)))
.next(SceneFinishes, event => expect(event.value.name).to.equal(new Name('A scenario passes the third time')))
.next(AsyncOperationAttempted, event => expect(event.taskDescription).to.equal(new Description('[ProtractorReporter] Invoking ProtractorRunner.afterEach...')))
.next(AsyncOperationAttempted, event => expect(event.taskDescription).to.equal(new Description('[Actor] Jasmine discards abilities...')))
.next(AsyncOperationCompleted, event => expect(event.taskDescription).to.equal(new Description('[Actor] Jasmine discarded abilities successfully')))
.next(AsyncOperationCompleted, event => expect(event.taskDescription).to.equal(new Description('[ProtractorReporter] ProtractorRunner.afterEach succeeded')))
.next(SceneFinished, event => expect(event.outcome).to.equal(new ExecutionSuccessful()))

@@ -70,6 +76,8 @@ describe('@serenity-js/jasmine', function () {
.next(InteractionStarts, event => expect(event.value.name).to.equal(new Name(`Jasmine navigates to 'chrome://version/'`)))
.next(SceneFinishes, event => expect(event.value.name).to.equal(new Name('A scenario passes the first time')))
.next(AsyncOperationAttempted, event => expect(event.taskDescription).to.equal(new Description('[ProtractorReporter] Invoking ProtractorRunner.afterEach...')))
.next(AsyncOperationAttempted, event => expect(event.taskDescription).to.equal(new Description('[Actor] Jasmine discards abilities...')))
.next(AsyncOperationCompleted, event => expect(event.taskDescription).to.equal(new Description('[Actor] Jasmine discarded abilities successfully')))
.next(AsyncOperationCompleted, event => expect(event.taskDescription).to.equal(new Description('[ProtractorReporter] ProtractorRunner.afterEach succeeded')))
.next(SceneFinished, event => expect(event.outcome).to.equal(new ExecutionSuccessful()))

@@ -78,6 +86,8 @@ describe('@serenity-js/jasmine', function () {
.next(InteractionStarts, event => expect(event.value.name).to.equal(new Name(`Jasmine navigates to 'chrome://accessibility/'`)))
.next(SceneFinishes, event => expect(event.value.name).to.equal(new Name('A scenario passes the second time')))
.next(AsyncOperationAttempted, event => expect(event.taskDescription).to.equal(new Description('[ProtractorReporter] Invoking ProtractorRunner.afterEach...')))
.next(AsyncOperationAttempted, event => expect(event.taskDescription).to.equal(new Description('[Actor] Jasmine discards abilities...')))
.next(AsyncOperationCompleted, event => expect(event.taskDescription).to.equal(new Description('[Actor] Jasmine discarded abilities successfully')))
.next(AsyncOperationCompleted, event => expect(event.taskDescription).to.equal(new Description('[ProtractorReporter] ProtractorRunner.afterEach succeeded')))
.next(SceneFinished, event => expect(event.outcome).to.equal(new ExecutionSuccessful()))

@@ -86,6 +96,8 @@ describe('@serenity-js/jasmine', function () {
.next(InteractionStarts, event => expect(event.value.name).to.equal(new Name(`Jasmine navigates to 'chrome://chrome-urls/'`)))
.next(SceneFinishes, event => expect(event.value.name).to.equal(new Name('A scenario passes the third time')))
.next(AsyncOperationAttempted, event => expect(event.taskDescription).to.equal(new Description('[ProtractorReporter] Invoking ProtractorRunner.afterEach...')))
.next(AsyncOperationAttempted, event => expect(event.taskDescription).to.equal(new Description('[Actor] Jasmine discards abilities...')))
.next(AsyncOperationCompleted, event => expect(event.taskDescription).to.equal(new Description('[Actor] Jasmine discarded abilities successfully')))
.next(AsyncOperationCompleted, event => expect(event.taskDescription).to.equal(new Description('[ProtractorReporter] ProtractorRunner.afterEach succeeded')))
.next(SceneFinished, event => expect(event.outcome).to.equal(new ExecutionSuccessful()))

@@ -0,0 +1,153 @@
import 'mocha';

import { actorCalled, engage, LogicError, serenity } from '../../../src';
import { SceneFinishes } from '../../../src/events';
import { FileSystemLocation, Path } from '../../../src/io';
import { Category, Name, ScenarioDetails } from '../../../src/model';
import { Actor, Note, Question, TakeNote, TakeNotes } from '../../../src/screenplay';
import { Cast } from '../../../src/stage';
import { expect } from '../../expect';
import { EnsureSame } from '../EnsureSame';

/** @test {TakeNotes} */
describe('TakeNotes', () => {

class Actors implements Cast {
prepare(actor: Actor): Actor {
switch (actor.name) {
case 'Alice':
case 'Bob':
return actor.whoCan(
TakeNotes.usingASharedNotepad(),
);
case 'Emma':
case 'Wendy':
default:
return actor.whoCan(
TakeNotes.usingAnEmptyNotepad()
);
}
}
}

const drinks = {
'Alice': 'Apple Juice',
'Bob': 'Beer',
'Emma': 'Earl Grey tea',
'Wendy': 'Water',
}

const AFavouriteDrink = () =>
Question.about(`a favourite drink`, (actor: Actor) => drinks[actor.name]);

beforeEach(() => engage(new Actors()));

/** @test {TakeNotes.usingAnEmptyNotepad} */
describe('usingAnEmptyNotepad', () => {

/**
* @test {TakeNotes.usingAnEmptyNotepad}
* @test {Note}
* @test {TakeNote}
*/
it('enables the actor to take note of an answer to a given question and recall it later', () =>
actorCalled('Emma').attemptsTo(
TakeNote.of(AFavouriteDrink()),
EnsureSame(Note.of(AFavouriteDrink()), drinks.Emma),
));

/**
* @test {TakeNotes.usingAnEmptyNotepad}
* @test {Note}
* @test {TakeNote}
*/
it(`ensures that actors have their own notepads and don't share notes`, () =>
actorCalled('Emma').attemptsTo(
TakeNote.of(AFavouriteDrink())
).
then(() => actorCalled('Wendy').attemptsTo(
TakeNote.of(AFavouriteDrink()),
)).
then(() => actorCalled('Emma').attemptsTo(
EnsureSame(Note.of(AFavouriteDrink()), drinks.Emma),
)).
then(() => actorCalled('Wendy').attemptsTo(
EnsureSame(Note.of(AFavouriteDrink()), drinks.Wendy),
)));

/**
* @test {TakeNotes.usingAnEmptyNotepad}
* @test {Note}
* @test {TakeNote}
*/
it(`ensures the notepad is cleared between test scenarios`, () =>
actorCalled('Emma').attemptsTo(
TakeNote.of(AFavouriteDrink())
).
then(() => {
serenity.announce(new SceneFinishes(scenarioDetails))
}).
then(() => serenity.waitForNextCue()).
then(() =>
expect(actorCalled('Emma').attemptsTo(
EnsureSame(Note.of(AFavouriteDrink()), drinks.Alice),
)).to.be.rejectedWith(LogicError, `The answer to "a favourite drink" has never been recorded`),
),
);
});

/** @test {TakeNotes.usingASharedNotepad} */
describe('usingASharedNotepad', () => {

/**
* @test {TakeNotes.usingASharedNotepad}
* @test {Note}
* @test {TakeNote}
*/
it('enables the actor to take note of an answer to a given question and recall it later', () =>
actorCalled('Alice').attemptsTo(
TakeNote.of(AFavouriteDrink()),
EnsureSame(Note.of(AFavouriteDrink()), drinks.Alice),
));

/**
* @test {TakeNotes.usingASharedNotepad}
* @test {Note}
* @test {TakeNote}
*/
it(`ensures that actors can share their notes`, () =>
actorCalled('Alice').attemptsTo(
TakeNote.of(AFavouriteDrink()),
).
then(() => actorCalled('Bob').attemptsTo(
EnsureSame(Note.of(AFavouriteDrink()), drinks.Alice),
))
);

/**
* @test {TakeNotes.usingASharedNotepad}
* @test {Note}
* @test {TakeNote}
*/
it(`ensures the notepad is cleared between test scenarios`, () =>
actorCalled('Alice').attemptsTo(
TakeNote.of(AFavouriteDrink()),
).
then(() => {
serenity.announce(new SceneFinishes(scenarioDetails))
}).
then(() => serenity.waitForNextCue()).
then(() =>
expect(actorCalled('Alice').attemptsTo(
EnsureSame(Note.of(AFavouriteDrink()), drinks.Alice),
)).to.be.rejectedWith(LogicError, `The answer to "a favourite drink" has never been recorded`),
),
);
});

const scenarioDetails = new ScenarioDetails(
new Name('ensures the notepad is cleared between test scenarios'),
new Category('TakeNotes'),
new FileSystemLocation(new Path('TakeNotes.spec.ts'))
);
});

This file was deleted.

@@ -1,3 +1,5 @@
import 'mocha';

import * as sinon from 'sinon';
import { LogicError } from '../../../src/errors';
import { ActivityDetails, Name } from '../../../src/model';
@@ -27,10 +29,12 @@ describe('Note', () => {
* @test {TakeNotes}
* @test {Note}
*/
it('enables the actor to recall the answer to a given question', () =>
expect(actorWhoCan(new TakeNotes({ [NameOfAHobby().toString()]: 'DYI' })).attemptsTo(
it('enables the actor to recall the answer to a given question', () => {
const notepad = new Map([ [NameOfAHobby().toString(), 'DYI'] ]);
expect(actorWhoCan(new TakeNotes(notepad)).attemptsTo(
EnsureSame(Note.of(NameOfAHobby()), 'DYI'),
)));
));
});

/**
* @test {TakeNotes}
@@ -133,7 +133,7 @@ describe('ArtifactArchiver', () => {
fs = sinon.createStubInstance(FileSystem);
stage = new Stage(new Extras(), stageManager as unknown as StageManager);

archiver = new ArtifactArchiver(fs as any);
archiver = new ArtifactArchiver(fs as any, stage);
stage.assign(archiver);

archiver.notifyOf(
@@ -156,7 +156,7 @@ describe('ArtifactArchiver', () => {

stage = new Stage(new Extras(), stageManager);

archiver = new ArtifactArchiver(fs as any);
archiver = new ArtifactArchiver(fs as any, stage);
stage.assign(archiver);

const notifyOf = sinon.spy(stageManager, 'notifyOf');
@@ -1,6 +1,6 @@
import { ensure, isDefined, isInstanceOf, property } from 'tiny-types';

import { DomainEvent, SceneStarts } from './events';
import { DomainEvent, SceneFinished, SceneStarts } from './events';
import { ErrorStackParser } from './io';
import { Duration, Timestamp } from './model';
import { Actor } from './screenplay/actor';
@@ -20,7 +20,7 @@ export class Serenity {
new StageManager(Serenity.defaultCueTimeout, clock),
);

this.stage.assign(new StageHand());
this.stage.assign(new StageHand(this.stage));
}

/**
@@ -48,10 +48,12 @@ export class Serenity {
this.engage(config.actors);
}

this.stage.assign(new StageHand());
this.stage.assign(new StageHand(this.stage));

if (Array.isArray(config.crew)) {
this.stage.assign(...config.crew);
this.stage.assign(
...config.crew.map(stageCrewMember => stageCrewMember.assignedTo(this.stage)),
);
}
}

@@ -193,7 +195,9 @@ export class Serenity {
setTheStage(...stageCrewMembers: StageCrewMember[]): void {
deprecated('serenity.setTheStage', 'Please use the new `configure({ crew: stageCrewMembers }) from @serenity-js/core instead.');

this.stage.assign(...stageCrewMembers);
this.stage.assign(
...stageCrewMembers.map(stageCrewMember => stageCrewMember.assignedTo(this.stage)),
);
}

/**
@@ -232,8 +236,8 @@ class StageHand implements StageCrewMember {
}

notifyOf(event: DomainEvent): void {
if (event instanceof SceneStarts) {
this.stage.resetActors();
if (event instanceof SceneFinished) {
this.stage.drawTheCurtain();
}
}
}
@@ -0,0 +1,10 @@
/**
* @desc
* An interface to be implemented by any {@link Ability} that needs to free up
* any resources it's using when the test scenario finishes.
*
* @public
*/
export interface Discardable {
discard(): Promise<void> | void;
}
@@ -50,15 +50,29 @@ import { AnswersQuestions, UsesAbilities } from './actor';
export abstract class Question<T> {

/**
* @desc
* Factory method that simplifies the process of defining custom questions.
*
* @example
* const EnvVariable = (name: string) =>
* Question.about(`the ${ name } env variable`, actor => process.env[name])
*
* @static
*
* @param {string} description
* @param {function(actor: AnswersQuestions & UsesAbilities): R} body
*
* @returns {Question<R>}
*/
static about<R>(description: string, body: (actor: AnswersQuestions & UsesAbilities) => R): Question<R> {
return new AnonymousQuestion<R>(description, body);
}

/**
* @desc
* Checks if the value is a {@link Question}
* Checks if the value is a {@link Question}.
*
* @static
*
* @param {any} maybeQuestion
* @returns {boolean}

0 comments on commit 6cc2e2c

Please sign in to comment.