spec/screenplay/actor.spec.ts
import 'mocha';
import * as sinon from 'sinon';
import { ConfigurationError, TestCompromisedError } from '../../src/errors';
import { InteractionFinished, InteractionStarts } from '../../src/events';
import { CorrelationId, ExecutionSuccessful, Name, Timestamp } from '../../src/model';
import { Ability, Actor, Initialisable, See } from '../../src/screenplay';
import { Stage } from '../../src/stage';
import { expect } from '../expect';
import { AcousticGuitar, Chords, Guitar, MusicSheets, NumberOfGuitarStringsLeft, PlayAChord, PlayAGuitar, PlayASong } from './example-implementation';
const equals = (expected: number) => (actual: PromiseLike<number>) => expect(actual).to.equal(expected);
describe('Actor', () => {
const
sceneId = new CorrelationId('some-scene-id'),
activityId = new CorrelationId('some-activity-id');
let
guitar: sinon.SinonStubbedInstance<Guitar>,
stage: sinon.SinonStubbedInstance<Stage>;
beforeEach(() => {
guitar = sinon.createStubInstance(AcousticGuitar);
stage = sinon.createStubInstance(Stage);
stage.assignNewActivityId.returns(activityId);
stage.currentSceneId.returns(sceneId);
stage.currentActivityId.returns(activityId);
});
function actor(name: string) {
return new Actor(name, stage as unknown as Stage);
}
it('can be identified by their name', () => {
expect(actor('Chris').name).to.equal('Chris');
});
it('provides a developer-friendly toString', () => {
class DoCoolThings implements Ability {
}
expect(actor('Chris').toString()).to.equal('Actor(name=Chris, abilities=[])');
expect(actor('Chris').whoCan(new DoCoolThings()).toString()).to.equal('Actor(name=Chris, abilities=[DoCoolThings])');
});
it('has Abilities allowing them to perform Activities and interact with a given interface of the system under test', () =>
actor('Chris').whoCan(PlayAGuitar.suchAs(guitar)).attemptsTo(
PlayAChord.of(Chords.AMajor),
).
then(() => {
expect(guitar.play).to.have.been.calledWith(Chords.AMajor);
}));
it('performs composite Tasks recursively to accomplish their Business Goals', () =>
actor('Chris').whoCan(PlayAGuitar.suchAs(guitar)).attemptsTo(
PlayASong.from(MusicSheets.Wild_Thing),
).
then(() => {
expect(guitar.play.getCall(0)).to.have.been.calledWith(Chords.AMajor);
expect(guitar.play.getCall(1)).to.have.been.calledWith(Chords.DMajor);
expect(guitar.play.getCall(2)).to.have.been.calledWith(Chords.EMajor);
}));
describe('asks Questions about the state of the system', () => {
it('fulfills the promise should the question be answered as expected', () => {
guitar.availableStrings.returns(Promise.resolve(['E2', 'A2', 'D3', 'G3', 'B3', 'E4' ]));
return expect(actor('Chris').whoCan(PlayAGuitar.suchAs(guitar)).attemptsTo(
PlayASong.from(MusicSheets.Wild_Thing),
See.if(NumberOfGuitarStringsLeft(), equals(6)),
)).to.be.fulfilled;
});
it('rejects the promise should the answer differ from what was expected', () => {
const oneStringMissing = ['E2', 'A2', 'D3', 'G3', 'B3' ];
guitar.availableStrings.returns(Promise.resolve(oneStringMissing));
return expect(actor('Chris').whoCan(PlayAGuitar.suchAs(guitar)).attemptsTo(
PlayASong.from(MusicSheets.Wild_Thing),
See.if(NumberOfGuitarStringsLeft(), equals(6)),
)).to.be.rejectedWith('expected 5 to equal 6');
});
});
describe('when using abilities', () => {
it('admits if it does not have the Ability necessary to accomplish a given Interaction', () =>
expect(actor('Ben').attemptsTo(
PlayAChord.of(Chords.AMajor),
)).to.be.eventually.rejectedWith(ConfigurationError, `Ben can't PlayAGuitar yet. Did you give them the ability to do so?`));
it('complains if given the same ability twice', () => {
expect(() =>
actor('Ben').whoCan(PlayAGuitar.suchAs(guitar), PlayAGuitar.suchAs(guitar))
).to.throw(ConfigurationError, `Ben already has an ability to PlayAGuitar, so you don't need to give it to them again.`);
});
describe('that have to be initialised', () => {
class UseDatabase implements Initialisable, Ability {
public callsToInitialise = 0;
private connection;
initialise(): Promise<void> | void {
this.connection = 'some connection';
this.callsToInitialise++;
}
isInitialised(): boolean {
return !! this.connection;
}
}
class UseBrokenDatabase implements Initialisable, Ability {
initialise(): Promise<void> | void {
throw new Error('DB server is down, please cheer it up');
}
isInitialised(): boolean {
return false;
}
}
it('initialises them upon the first call to attemptsTo', async () => {
const useDatabase = new UseDatabase();
await actor('Dibillo').whoCan(useDatabase).attemptsTo();
expect(useDatabase.isInitialised()).to.equal(true);
});
it(`initialises them only if they haven't been initialised before`, async () => {
const useDatabase = new UseDatabase();
await actor('Dibillo').whoCan(useDatabase).attemptsTo();
await actor('Dibillo').whoCan(useDatabase).attemptsTo();
await actor('Dibillo').whoCan(useDatabase).attemptsTo();
expect(useDatabase.callsToInitialise).to.equal(1);
});
it(`complains if the ability could not be initialised`, () => {
return expect(actor('Dibillo').whoCan(new UseBrokenDatabase()).attemptsTo())
.to.be.rejectedWith(TestCompromisedError, `Dibillo couldn't initialise the ability to UseBrokenDatabase`)
.then(error => {
expect(error.cause.message).to.equal('DB server is down, please cheer it up')
});
});
});
});
describe('DomainEvent handling', () => {
let Bob: Actor;
const now = new Timestamp(new Date('2018-06-10T22:57:07.112Z'));
const activityName = new Name('Bob plays the chord of A');
beforeEach(() => {
stage = sinon.createStubInstance(Stage);
stage.currentTime.returns(now);
stage.currentSceneId.returns(sceneId);
stage.assignNewActivityId.returns(activityId);
stage.currentActivityId.returns(activityId);
Bob = new Actor('Bob', stage as unknown as Stage);
});
describe('announces events about the activities it performs', () => {
it('notifies when an activity begins and ends', () => Bob.whoCan(PlayAGuitar.suchAs(guitar)).attemptsTo(
PlayAChord.of(Chords.AMajor),
).then(() => {
expect(stage.announce).to.have.callCount(2);
const
firstEvent = stage.announce.getCall(0).args[0],
secondEvent = stage.announce.getCall(1).args[0];
expect(firstEvent).to.be.instanceOf(InteractionStarts);
expect(firstEvent).to.have.property('details').property('name').equal(activityName);
expect(firstEvent).to.have.property('timestamp').equal(now);
expect(secondEvent).to.be.instanceOf(InteractionFinished);
expect(secondEvent).to.have.property('details').property('name').equal(activityName);
expect(secondEvent).to.have.property('outcome').equal(new ExecutionSuccessful());
expect(secondEvent).to.have.property('timestamp').equal(now);
}));
});
});
});