Skip to content
Permalink
Browse files
fix(core): Serenity object configures the Stage correctly
  • Loading branch information
jan-molak committed Mar 28, 2019
1 parent ce0f05c commit 438fa4cb82a13cbc0ae801c467f5249c99260688
Show file tree
Hide file tree
Showing 22 changed files with 227 additions and 70 deletions.
@@ -18,9 +18,9 @@
"scripts": {
"clean": "rimraf lib target",
"lint": "tslint --project tsconfig-lint.json --config ../../tslint.json --format stylish",
"test": "failsafe clean test:update-serenity test:acceptance test:report",
"test": "mocha --opts ../../mocha.opts 'spec/**/*.spec.*'",
"compile": "tsc --project tsconfig.json",
"verify": "npm test || true",
"verify": "npm test",
"start": "node index.js",
"dev": "nodemon --exec 'cross-env PORT=3000 NODE_ENV=TEST npm start'"
},
@@ -1,5 +1,8 @@
Feature: Basic arithmetic operations

Background:
Given Dominique has requested a new calculation

Scenario: Addition

When Dominique enters 2
@@ -1,8 +1,14 @@
import { Operand, Operator } from '@serenity-js-examples/calculator-app';
import { Ensure, equals, not } from '@serenity-js/assertions';
import { Ensure, equals } from '@serenity-js/assertions';
import { WithStage } from '@serenity-js/cucumber';
import { Then, When } from 'cucumber';
import { EnterOperand, ResultOfCalculation, UseOperator } from '../support/screenplay';
import { Given, Then, When } from 'cucumber';
import { EnterOperand, RequestANewCalculation, ResultOfCalculation, UseOperator } from '../support/screenplay';

Given(/^(.*) has requested a new calculation/, function(this: WithStage, actorName: string) {
return this.stage.theActorCalled(actorName).attemptsTo(
RequestANewCalculation(),
);
});

When(/^(.*) enters (\d+)$/, function(this: WithStage, actorName: string, operandValue: string) {
const actor = ! isPronoun(actorName) ? this.stage.actor(actorName) : this.stage.theActorInTheSpotlight();
@@ -14,7 +20,7 @@ When(/^(.*) enters (\d+)$/, function(this: WithStage, actorName: string, operand

When(/(?:he|she|they) uses? the (.) operator/, function(this: WithStage, operatorSymbol: string) {
return this.stage.theActorInTheSpotlight().attemptsTo(
UseOperator(Operator.from(operatorSymbol)),
UseOperator(Operator.fromString(operatorSymbol)),
);
});

@@ -6,15 +6,15 @@ import { WithStage } from '@serenity-js/cucumber';
import { setDefaultTimeout, setWorldConstructor } from 'cucumber';
import { Actors } from './screenplay';

setDefaultTimeout(1000);

setWorldConstructor(function(this: WithStage, { parameters }) {
this.stage = serenity.callToStageFor(new Actors());
});

// todo: implement serenity.configure(...)
serenity.stageManager.register(
serenity.setTheStage(
new ArtifactArchiver(new FileSystem(new Path('./target/site/serenity'))),
new SerenityBDDReporter(),
new ConsoleReporter(),
);

setDefaultTimeout(1000);

setWorldConstructor(function(this: WithStage, { parameters }) {
this.stage = serenity.callToStageFor(new Actors());
});
@@ -1,11 +1,10 @@
import { Calculator } from '@serenity-js-examples/calculator-app';
import { Actor } from '@serenity-js/core';
import { Cast } from '@serenity-js/core/lib/stage';
import { Actor, DressingRoom } from '@serenity-js/core';

import { InteractDirectly } from './abilities';

export class Actors implements Cast {
actor(name: string) {
return Actor.named(name).whoCan(InteractDirectly.with(new Calculator()));
export class Actors implements DressingRoom {
prepare(actor: Actor): Actor {
return actor.whoCan(InteractDirectly.with(new Calculator()));
}
}
@@ -28,6 +28,10 @@ export class InteractDirectly implements Ability {
constructor(private readonly calculator: Calculator) {
}

requestANewCalculationId(): void {
this.calculationId = CalculationId.create();
}

currentCalculationId(): CalculationId {
if (! this.calculationId) {
this.calculationId = CalculationId.create();
@@ -1,11 +1,11 @@
import { EnterOperandCommand, Operand } from '@serenity-js-examples/calculator-app';
import { Actor, EmitArtifact, Interaction } from '@serenity-js/core';
import { Actor, Interaction } from '@serenity-js/core';
import { JSONData } from '@serenity-js/core/lib/model';
import { InteractDirectly } from '../abilities';

export const EnterOperand = (operand: Operand) =>
Interaction.where(`#actor enters an operand of ${operand.value}`,
(actor: Actor, emitArtifact: EmitArtifact) => {
(actor: Actor) => {
const ability = InteractDirectly.as(actor);

const command = new EnterOperandCommand(
@@ -17,5 +17,5 @@ export const EnterOperand = (operand: Operand) =>
command,
);

emitArtifact(JSONData.fromJSON(command.toJSON()), command.constructor.name);
actor.collect(JSONData.fromJSON(command.toJSON()), command.constructor.name);
});
@@ -0,0 +1,8 @@
import { Actor, Interaction } from '@serenity-js/core';
import { InteractDirectly } from '../abilities';

export const RequestANewCalculation = () =>
Interaction.where(`#actor requests a new calculation`,
(actor: Actor) => {
InteractDirectly.as(actor).requestANewCalculationId();
});
@@ -1,10 +1,10 @@
import { EnterOperandCommand, Operand, Operator, UseOperatorCommand } from '@serenity-js-examples/calculator-app';
import { Actor, EmitArtifact, Interaction } from '@serenity-js/core';
import { Operator, UseOperatorCommand } from '@serenity-js-examples/calculator-app';
import { Actor, Interaction } from '@serenity-js/core';
import { JSONData } from '@serenity-js/core/lib/model';
import { InteractDirectly } from '../abilities';

export const UseOperator = (operator: Operator) => Interaction.where(`#actor uses the ${ operator.constructor.name }`,
(actor: Actor, emitArtifact: EmitArtifact) => {
(actor: Actor) => {
const ability = InteractDirectly.as(actor);

const command = new UseOperatorCommand(
@@ -16,5 +16,5 @@ export const UseOperator = (operator: Operator) => Interaction.where(`#actor use
command,
);

emitArtifact(JSONData.fromJSON(command.toJSON()), command.constructor.name);
actor.collect(JSONData.fromJSON(command.toJSON()), command.constructor.name);
});
@@ -1,2 +1,3 @@
export * from './EnterOperand';
export * from './RequestANewCalculation';
export * from './UseOperator';
@@ -18,9 +18,9 @@
"scripts": {
"clean": "rimraf target",
"lint": "tslint --project tsconfig-lint.json --config ../../tslint.json --format stylish",
"test:update-serenity": "serenity update",
"test:update-serenity": "serenity update --artifact net.serenity-bdd:serenity-cli:jar:all:2.1.5 --repository https://jcenter.bintray.com/ --ignoreSSL",
"test:acceptance": "cucumber-js --require-module ts-node/register --format node_modules/@serenity-js/cucumber/register.js --require ./features/step_definitions/domain-level.steps.ts --require ./features/support/configure_serenity.ts",
"test:report": "serenity run",
"test:report": "serenity run --artifact net.serenity-bdd:serenity-cli:jar:all:2.1.5",
"test": "failsafe clean test:update-serenity test:acceptance test:report",
"verify": "npm test"
},
@@ -43,7 +43,7 @@
"@types/cucumber": "4.0.4",
"cucumber": "5.0.1",
"npm-failsafe": "0.4.1",
"serenity-cli": "0.11.0",
"serenity-cli": "^0.11.3",
"ts-node": "7.0.1"
}
}
@@ -6,14 +6,8 @@ import { setDefaultTimeout } from 'cucumber';

setDefaultTimeout(5000);

// todo: implement serenity.configure(...)
const crewMembers = [
serenity.setTheStage(
new ArtifactArchiver(new FileSystem(new Path('./target/site/serenity'))),
new SerenityBDDReporter(),
new DebugReporter(),
];

crewMembers.forEach(crewMember => {
crewMember.assignTo(serenity.stageManager);
serenity.stageManager.register(crewMember);
});
// new DebugReporter(),
);
@@ -18,9 +18,9 @@
"scripts": {
"clean": "rimraf target",
"lint": "tslint --project tsconfig-lint.json --config ../../tslint.json --format stylish",
"test:update-serenity": "serenity update",
"test:update-serenity": "serenity update --artifact net.serenity-bdd:serenity-cli:jar:all:2.1.5 --repository https://jcenter.bintray.com/ --ignoreSSL",
"test:acceptance": "cucumber-js --require-module ts-node/register --format node_modules/@serenity-js/cucumber/register.js --require ./features/step_definitions/sample.steps.ts --require ./features/support/configure_serenity.ts",
"test:report": "serenity run",
"test:report": "serenity run --artifact net.serenity-bdd:serenity-cli:jar:all:2.1.5",
"test": "failsafe clean test:update-serenity test:acceptance test:report",
"verify": "npm test || true"
},
@@ -41,7 +41,7 @@
"@types/cucumber": "4.0.4",
"cucumber": "5.0.1",
"npm-failsafe": "0.4.1",
"serenity-cli": "0.11.0",
"serenity-cli": "^0.11.3",
"ts-node": "7.0.1"
}
}
@@ -9,7 +9,7 @@ Feature: Calculations API

| expression | expected_result | description |
| 2 | 2 | Literal |
| 2 + 2 | 5 | Addition |
| 2 + 2 | 4 | Addition |
| 2 - 3 | -1 | Subtraction |
| 2 * 5 | 10 | Multiplication |
| 5 / 2 | 2.5 | Division |
@@ -6,15 +6,14 @@ import { WithStage } from '@serenity-js/cucumber';
import { setDefaultTimeout, setWorldConstructor } from 'cucumber';
import { Actors } from './screenplay';

serenity.setTheStage(
new ArtifactArchiver(new FileSystem(new Path('./target/site/serenity'))),
new SerenityBDDReporter(),
new ConsoleReporter(),
);

setDefaultTimeout(1000);

setWorldConstructor(function(this: WithStage, { parameters }) {
this.stage = serenity.callToStageFor(new Actors());
});

// todo: implement serenity.configure(...)
serenity.stageManager.register(
new ArtifactArchiver(new FileSystem(new Path('./target/site/serenity'))),
new SerenityBDDReporter(),
new ConsoleReporter(),
);
@@ -1,13 +1,13 @@
import { Actor, Cast } from '@serenity-js/core';
import { Actor, DressingRoom } from '@serenity-js/core';
import { ManageALocalServer } from '@serenity-js/local-server';
import { CallAnApi } from '@serenity-js/rest';

import { requestHandler } from '@serenity-js-examples/calculator-app';

export class Actors implements Cast {
actor(name: string) {
return Actor.named(name).whoCan(
ManageALocalServer.using(requestHandler),
export class Actors implements DressingRoom {
prepare(actor: Actor): Actor {
return actor.whoCan(
ManageALocalServer.running(requestHandler),
CallAnApi.at('http://localhost'),
);
}
@@ -18,9 +18,9 @@
"scripts": {
"clean": "rimraf target",
"lint": "tslint --project tsconfig-lint.json --config ../../tslint.json --format stylish",
"test:update-serenity": "serenity update",
"test:update-serenity": "serenity update --artifact net.serenity-bdd:serenity-cli:jar:all:2.1.5 --repository https://jcenter.bintray.com/ --ignoreSSL",
"test:acceptance": "cucumber-js --require-module ts-node/register --format node_modules/@serenity-js/cucumber/register.js --require ./features/step_definitions/api-level.steps.ts --require ./features/support/configure_serenity.ts",
"test:report": "serenity run",
"test:report": "serenity run --artifact net.serenity-bdd:serenity-cli:jar:all:2.1.5",
"test": "failsafe clean test:update-serenity test:acceptance test:report",
"verify": "npm test"
},
@@ -47,7 +47,7 @@
"cucumber": "5.1.0",
"express": "4.16.4",
"npm-failsafe": "0.4.1",
"serenity-cli": "0.11.0",
"serenity-cli": "^0.11.3",
"ts-node": "7.0.1"
}
}
@@ -0,0 +1,106 @@
import sinon = require('sinon');
import { ActivityFinished, ActivityStarts, DomainEvent, TestRunnerDetected } from '../src/events';
import { Name } from '../src/model';
import { Actor, Interaction } from '../src/screenplay';
import { Serenity } from '../src/Serenity';
import { Clock, DressingRoom, Stage, StageCrewMember } from '../src/stage';
import { expect } from './expect';

describe('Serenity', () => {

it(`constructs a Stage and connects it with a provided DressingRoom`, () => {

const prepareSpy = sinon.spy();

// no-op actors with no special Abilities
class Extras implements DressingRoom {
prepare(actor: Actor): Actor {
prepareSpy(actor);
return actor;
}
}

const serenity = new Serenity(new Clock());

const stage = serenity.callToStageFor(new Extras());

const Joe = stage.theActorCalled('Joe');

expect(prepareSpy).to.have.been.calledOnce; // tslint:disable-line:no-unused-expression
expect(prepareSpy.getCall(0).args[0]).to.equal(Joe);
});

it(`enables propagation of DomainEvents triggered by Actors' Activities and StageCrewMembers`, () => {

class Extras implements DressingRoom {
prepare(actor: Actor): Actor {
return actor;
}
}

const PerformSomeInteraction = () => Interaction.where(`#actor performs some interaction`, actor => {
return void 0;
});

const frozenClock = new Clock(() => new Date('1983-07-03'));
const serenity = new Serenity(frozenClock);
const listener = new Listener<ActivityStarts | ActivityFinished>();

serenity.setTheStage(listener);

const theStage = serenity.callToStageFor(new Extras());
const Joe = theStage.theActorCalled('Joe');

return Joe.attemptsTo(
PerformSomeInteraction(),
).
then(() => serenity.waitForNextCue()).
then(() => {
expect(listener.events).to.have.lengthOf(2);

expect(listener.events[0]).to.be.instanceOf(ActivityStarts);
expect(listener.events[0].value.name.value).to.equal(`Joe performs some interaction`);

expect(listener.events[1]).to.be.instanceOf(ActivityFinished);
expect(listener.events[1].value.name.value).to.equal(`Joe performs some interaction`);
});
});

it(`allows for external parties, such as test runner adapters, to announce DomainEvents`, () => {

const frozenClock = new Clock(() => new Date('1983-07-03'));
const serenity = new Serenity(frozenClock);
const listener = new Listener<TestRunnerDetected>();

const testRunnerName = new Name('mocha');

serenity.setTheStage(listener);

serenity.announce(new TestRunnerDetected(testRunnerName, serenity.currentTime()));

return serenity.waitForNextCue().
then(() => {
expect(listener.events).to.have.lengthOf(1);

expect(listener.events[0]).to.be.instanceOf(TestRunnerDetected);
expect(listener.events[0].value).to.equal(testRunnerName);
});
});

class Listener<Event_Type extends DomainEvent> implements StageCrewMember {
public readonly events: Event_Type[] = [];

constructor(private stage: Stage = null) {
}

assignedTo(stage: Stage): StageCrewMember {
this.stage = stage;

return this;
}

notifyOf(event: Event_Type): void {
this.events.push(event);
}
}
});

0 comments on commit 438fa4c

Please sign in to comment.