Skip to content

Commit

Permalink
fix(core): better support for abilities that are discarded asynchrono…
Browse files Browse the repository at this point in the history
…usly
  • Loading branch information
jan-molak committed Nov 22, 2020
1 parent e0fcf8b commit fb130b6
Show file tree
Hide file tree
Showing 8 changed files with 325 additions and 79 deletions.
@@ -0,0 +1,61 @@
const { actorCalled, engage, Interaction } = require('@serenity-js/core');

describe('Mocha reporting', () => {

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

describe('A screenplay scenario', () => {

// even if an ability is not discarded successfully, the subsequent tests should still be executed
it(`fails when discarding an ability fails`, () =>
actorCalled('Donald')
.attemptsTo(
NotDoTooMuch(),
));

it(`succeeds when ability is discarded successfully`, () =>
actorCalled('Alice')
.attemptsTo(
NotDoTooMuch(),
));

it(`fails if the ability fails to discard again`, () =>
actorCalled('Donald')
.attemptsTo(
NotDoTooMuch(),
));
});
});

class Actors {
prepare(actor) {
switch (actor.name) {
case 'Donald':
return actor.whoCan(new CauseErrorWhenAbilityDiscarded());
default:
return actor.whoCan(new SucceedWhenAbilityDiscarded());
}
}
}

const NotDoTooMuch = () => Interaction.where(`#actor doesn't do much`, () => void 0);

class CauseErrorWhenAbilityDiscarded {
static as(actor) {
return actor.abilityTo(CauseErrorWhenAbilityDiscarded);
}

discard() {
return Promise.reject(new TypeError(`Some internal error in ability`));
}
}

class SucceedWhenAbilityDiscarded {
static as(actor) {
return actor.abilityTo(CauseErrorWhenAbilityDiscarded);
}

discard() {
return Promise.resolve();
}
}
40 changes: 39 additions & 1 deletion integration/jasmine/spec/screenplay.spec.ts
Expand Up @@ -2,7 +2,7 @@ import 'mocha';

import { expect, ifExitCodeIsOtherThan, logOutput, PickEvent } from '@integration/testing-tools';
import { SceneFinished, SceneStarts, SceneTagged, TestRunnerDetected } from '@serenity-js/core/lib/events';
import { ExecutionFailedWithAssertionError, ExecutionFailedWithError, FeatureTag, Name, ProblemIndication } from '@serenity-js/core/lib/model';
import { ExecutionFailedWithAssertionError, ExecutionFailedWithError, ExecutionSuccessful, FeatureTag, Name, ProblemIndication } from '@serenity-js/core/lib/model';
import { jasmine } from '../src/jasmine';

describe('@serenity-js/Jasmine', function () {
Expand Down Expand Up @@ -79,5 +79,43 @@ describe('@serenity-js/Jasmine', function () {
})
;
}));

it(`executes all the scenarios in the test suite even when some of them fail because of an error when discarding an ability`, () =>
jasmine('examples/screenplay/ability-discard-error-should-not-affect-stage-cue.spec.js')
.then(ifExitCodeIsOtherThan(1, logOutput))
.then(res => {
expect(res.exitCode).to.equal(1);

PickEvent.from(res.events)
.next(SceneStarts, event => expect(event.details.name).to.equal(new Name('A screenplay scenario fails when discarding an ability fails')))
.next(SceneFinished, event => {
const outcome: ProblemIndication = event.outcome as ProblemIndication;

expect(outcome).to.be.instanceOf(ExecutionFailedWithError);
expect(outcome.error.name).to.equal('Error');

const message = outcome.error.message.split('\n');

expect(message[0]).to.equal('1 async operation has failed to complete:');
expect(message[1]).to.equal('[Stage] Dismissing Donald... - TypeError: Some internal error in ability');
})
.next(SceneStarts, event => expect(event.details.name).to.equal(new Name('A screenplay scenario succeeds when ability is discarded successfully')))
.next(SceneFinished, event => {
expect(event.outcome).to.be.instanceOf(ExecutionSuccessful);
})
.next(SceneStarts, event => expect(event.details.name).to.equal(new Name('A screenplay scenario fails if the ability fails to discard again')))
.next(SceneFinished, event => {
const outcome: ProblemIndication = event.outcome as ProblemIndication;

expect(outcome).to.be.instanceOf(ExecutionFailedWithError);
expect(outcome.error.name).to.equal('Error');

const message = outcome.error.message.split('\n');

expect(message[0]).to.equal('1 async operation has failed to complete:');
expect(message[1]).to.equal('[Stage] Dismissing Donald... - TypeError: Some internal error in ability');
})
;
}));
});
});
2 changes: 2 additions & 0 deletions integration/jasmine/src/jasmine.ts
Expand Up @@ -22,6 +22,8 @@ export function jasmine(...params: string[]): Promise<SpawnResult> {

...params,

'--random=false',

// The path to the reporter needs to be relative to the Jasmine module.
// Normally this will be simply "@serenity-js/jasmine" as the Serenity/JS adapter for Jasmine
// will be installed next to it.
Expand Down
@@ -0,0 +1,61 @@
const { actorCalled, engage, Interaction } = require('@serenity-js/core');

describe('Mocha reporting', () => {

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

describe('A screenplay scenario', () => {

// even if an ability is not discarded successfully, the subsequent tests should still be executed
it(`fails when discarding an ability fails`, () =>
actorCalled('Donald')
.attemptsTo(
NotDoTooMuch(),
));

it(`succeeds when ability is discarded successfully`, () =>
actorCalled('Alice')
.attemptsTo(
NotDoTooMuch(),
));

it(`fails if the ability fails to discard again`, () =>
actorCalled('Donald')
.attemptsTo(
NotDoTooMuch(),
));
});
});

class Actors {
prepare(actor) {
switch (actor.name) {
case 'Donald':
return actor.whoCan(new CauseErrorWhenAbilityDiscarded());
default:
return actor.whoCan(new SucceedWhenAbilityDiscarded());
}
}
}

const NotDoTooMuch = () => Interaction.where(`#actor doesn't do much`, () => void 0);

class CauseErrorWhenAbilityDiscarded {
static as(actor) {
return actor.abilityTo(CauseErrorWhenAbilityDiscarded);
}

discard() {
return Promise.reject(new TypeError(`Some internal error in ability`));
}
}

class SucceedWhenAbilityDiscarded {
static as(actor) {
return actor.abilityTo(CauseErrorWhenAbilityDiscarded);
}

discard() {
return Promise.resolve();
}
}
40 changes: 39 additions & 1 deletion integration/mocha/spec/screenplay.spec.ts
Expand Up @@ -2,7 +2,7 @@ import 'mocha';

import { expect, ifExitCodeIsOtherThan, logOutput, PickEvent } from '@integration/testing-tools';
import { SceneFinished, SceneStarts, SceneTagged, TestRunnerDetected } from '@serenity-js/core/lib/events';
import { ExecutionFailedWithAssertionError, ExecutionFailedWithError, FeatureTag, Name, ProblemIndication } from '@serenity-js/core/lib/model';
import { ExecutionFailedWithAssertionError, ExecutionFailedWithError, ExecutionSuccessful, FeatureTag, Name, ProblemIndication } from '@serenity-js/core/lib/model';
import { mocha } from '../src/mocha';

describe('@serenity-js/mocha', function () {
Expand Down Expand Up @@ -79,5 +79,43 @@ describe('@serenity-js/mocha', function () {
})
;
}));

it(`executes all the scenarios in the test suite even when some of them fail because of an error when discarding an ability`, () =>
mocha('examples/screenplay/ability-discard-error-should-not-affect-stage-cue.spec.js')
.then(ifExitCodeIsOtherThan(2, logOutput))
.then(res => {
expect(res.exitCode).to.equal(2); // 2 failures, so Mocha returns an exit code of 2

PickEvent.from(res.events)
.next(SceneStarts, event => expect(event.details.name).to.equal(new Name('A screenplay scenario fails when discarding an ability fails')))
.next(SceneFinished, event => {
const outcome: ProblemIndication = event.outcome as ProblemIndication;

expect(outcome).to.be.instanceOf(ExecutionFailedWithError);
expect(outcome.error.name).to.equal('Error');

const message = outcome.error.message.split('\n');

expect(message[0]).to.equal('1 async operation has failed to complete:');
expect(message[1]).to.equal('[Stage] Dismissing Donald... - TypeError: Some internal error in ability');
})
.next(SceneStarts, event => expect(event.details.name).to.equal(new Name('A screenplay scenario succeeds when ability is discarded successfully')))
.next(SceneFinished, event => {
expect(event.outcome).to.be.instanceOf(ExecutionSuccessful);
})
.next(SceneStarts, event => expect(event.details.name).to.equal(new Name('A screenplay scenario fails if the ability fails to discard again')))
.next(SceneFinished, event => {
const outcome: ProblemIndication = event.outcome as ProblemIndication;

expect(outcome).to.be.instanceOf(ExecutionFailedWithError);
expect(outcome.error.name).to.equal('Error');

const message = outcome.error.message.split('\n');

expect(message[0]).to.equal('1 async operation has failed to complete:');
expect(message[1]).to.equal('[Stage] Dismissing Donald... - TypeError: Some internal error in ability');
})
;
}));
});
});
7 changes: 6 additions & 1 deletion packages/core/src/stage/Stage.ts
Expand Up @@ -74,6 +74,9 @@ export class Stage {
const newActor = new Actor(name, this);

actor = this.cast.prepare(newActor);

// todo this.manager.notifyOf(ActorStarts)
// todo: map this in Serenity BDD Reporter so that the "cast" is recorded
}
catch (error) {
throw new ConfigurationError(`${ this.typeOf(this.cast) } encountered a problem when preparing actor "${ name }" for stage`, error);
Expand Down Expand Up @@ -215,8 +218,10 @@ export class Stage {
new Description(`[${ this.constructor.name }] Dismissed ${ actor.name } successfully`),
id,
)))
// todo: ActorFinished
.catch(error =>
this.announce(new AsyncOperationFailed(error, id)),
this.announce(new AsyncOperationFailed(error, id)), // todo: serialise the error!
// todo: ActorFinished (error ?)
);

}))
Expand Down

0 comments on commit fb130b6

Please sign in to comment.