Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
feat(cucumber): cucumber adapter reports scenario descriptions
affects: @serenity-js/core, @serenity-js/cucumber, @integration/cucumber
  • Loading branch information
jan-molak committed Jul 29, 2018
1 parent e19c358 commit adb3412
Show file tree
Hide file tree
Showing 25 changed files with 353 additions and 88 deletions.
11 changes: 11 additions & 0 deletions integration/cucumber/features/descriptions.feature
@@ -0,0 +1,11 @@
Feature: Serenity/JS recognises all the important elements of a scenario

In order to accurately report the scenario
Serenity/JS should recognise all of its important parts

Scenario: First scenario

A scenario where all the steps pass
Is reported as passing

Given a step that passes
6 changes: 6 additions & 0 deletions integration/cucumber/features/failing_scenario.feature
@@ -0,0 +1,6 @@
Feature: Serenity/JS recognises a failing scenario

Scenario: A failing scenario

Given a step that fails

5 changes: 0 additions & 5 deletions integration/cucumber/features/passing_scenario.feature
@@ -1,11 +1,6 @@
Feature: Serenity/JS recognises a passing scenario

In order to correctly report the outcome of a scenario
Serenity/JS should recognise when a scenario passes

Scenario: A passing scenario

A scenario where all the steps pass

Given a step that passes

41 changes: 41 additions & 0 deletions integration/cucumber/spec/recognises_descriptions.ts
@@ -0,0 +1,41 @@
import { expect, ifExitCodeIsOtherThan, logOutput } from '@integration/testing-tools';
import { SceneBackgroundDetected, SceneDescriptionDetected, SceneStarts } from '@serenity-js/core/lib/events';
import { Description, Name } from '@serenity-js/core/lib/model';

import 'mocha';
import { given } from 'mocha-testdata';

import { cucumber, Pick } from '../src';

describe('@serenity-js/cucumber', function() {

this.timeout(5000);

given([
'synchronous',
'promise',
'callback',
]).
it('recognises scenario descriptions', (stepInterface: string) =>
cucumber(
'--require', 'features/support/configure_serenity.ts',
'--require', `features/step_definitions/${ stepInterface }.steps.ts`,
'--require', 'node_modules/@serenity-js/cucumber/register.js',
'features/descriptions.feature',
).
then(ifExitCodeIsOtherThan(0, logOutput)).
then(res => {
expect(res.exitCode).to.equal(0);

expect(res.events).to.have.lengthOf(7);

Pick.from(res.events)
.next(SceneStarts, event => expect(event.value.name).to.equal(new Name('First scenario')))
.next(SceneDescriptionDetected, event => {
expect(event.description).to.equal(new Description(
'A scenario where all the steps pass\nIs reported as passing',
));
})
;
}));
});
48 changes: 48 additions & 0 deletions integration/cucumber/spec/recognises_failing_scenario.spec.ts
@@ -0,0 +1,48 @@
import { expect, ifExitCodeIsOtherThan, logOutput } from '@integration/testing-tools';
import {
ActivityFinished,
ActivityStarts,
SceneFinished,
SceneStarts,
SceneTagged,
TestRunnerDetected,
} from '@serenity-js/core/lib/events';
import { ExecutionFailedWithError, FeatureTag, Name } from '@serenity-js/core/lib/model';

import 'mocha';
import { given } from 'mocha-testdata';

import { cucumber, Pick } from '../src';

describe('@serenity-js/cucumber', function() {

this.timeout(5000);

given([
'synchronous',
'promise',
'callback',
]).
it('recognises a failing scenario', (stepInterface: string) =>
cucumber(
'--require', 'features/support/configure_serenity.ts',
'--require', `features/step_definitions/${ stepInterface }.steps.ts`,
'--require', 'node_modules/@serenity-js/cucumber/register.js',
'features/failing_scenario.feature',
).
then(ifExitCodeIsOtherThan(1, logOutput)).
then(res => {
expect(res.exitCode).to.equal(1);

expect(res.events).to.have.lengthOf(6);

Pick.from(res.events)
.next(SceneStarts, event => expect(event.value.name).to.equal(new Name('A failing scenario')))
.next(TestRunnerDetected, event => expect(event.value).to.equal(new Name('Cucumber')))
.next(SceneTagged, event => expect(event.tag).to.equal(new FeatureTag('Serenity/JS recognises a failing scenario')))
.next(ActivityStarts, event => expect(event.value.name).to.equal(new Name('Given a step that fails')))
.next(ActivityFinished, event => expect(event.outcome).to.be.instanceOf(ExecutionFailedWithError))
.next(SceneFinished, event => expect(event.outcome).to.be.instanceOf(ExecutionFailedWithError))
;
}));
});
36 changes: 9 additions & 27 deletions integration/cucumber/spec/recognises_passing_scenario.spec.ts
Expand Up @@ -5,7 +5,7 @@ import { ExecutionSuccessful, FeatureTag, Name } from '@serenity-js/core/lib/mod
import 'mocha';
import { given } from 'mocha-testdata';

import { cucumber } from '../src';
import { cucumber, Pick } from '../src';

describe('@serenity-js/cucumber', function() {

Expand All @@ -19,8 +19,6 @@ describe('@serenity-js/cucumber', function() {
it('recognises a passing scenario', (stepInterface: string) =>
cucumber(
'--require', 'features/support/configure_serenity.ts',
// '--require', 'features/support/before_hook.ts',
// '--require', 'features/support/after_hook.ts',
'--require', `features/step_definitions/${ stepInterface }.steps.ts`,
'--require', 'node_modules/@serenity-js/cucumber/register.js',
'features/passing_scenario.feature',
Expand All @@ -31,29 +29,13 @@ describe('@serenity-js/cucumber', function() {

expect(res.events).to.have.lengthOf(6);

expect(res.events[0]).to.be.instanceOf(SceneStarts)
.and.have.property('value')
.that.has.property('name')
.equal(new Name('A passing scenario'));

expect(res.events[1]).to.be.instanceOf(TestRunnerDetected)
.and.have.property('value').equal(new Name('Cucumber'));

expect(res.events[2]).to.be.instanceOf(SceneTagged)
.and.have.property('tag')
.equal(new FeatureTag('Serenity/JS recognises a passing scenario'));

expect(res.events[3]).to.be.instanceOf(ActivityStarts)
.and.have.property('value')
.that.has.property('name')
.equal(new Name('Given a step that passes'));

expect(res.events[4]).to.be.instanceOf(ActivityFinished)
.and.have.property('outcome')
.equal(new ExecutionSuccessful());

expect(res.events[5]).to.be.instanceOf(SceneFinished)
.and.have.property('outcome')
.equal(new ExecutionSuccessful());
Pick.from(res.events)
.next(SceneStarts, event => expect(event.value.name).to.equal(new Name('A passing scenario')))
.next(TestRunnerDetected, event => expect(event.value).to.equal(new Name('Cucumber')))
.next(SceneTagged, event => expect(event.tag).to.equal(new FeatureTag('Serenity/JS recognises a passing scenario')))
.next(ActivityStarts, event => expect(event.value.name).to.equal(new Name('Given a step that passes')))
.next(ActivityFinished, event => expect(event.outcome).to.equal(new ExecutionSuccessful()))
.next(SceneFinished, event => expect(event.outcome).to.equal(new ExecutionSuccessful()))
;
}));
});
23 changes: 23 additions & 0 deletions integration/cucumber/src/Pick.ts
@@ -0,0 +1,23 @@
import { DomainEvent } from '@serenity-js/core/lib/events';

export class Pick {
static from = (events: DomainEvent[]) => new Pick(events);

constructor(private events: DomainEvent[]) {
}

next<T extends DomainEvent>(type: { new(...args: any[]): T }, assertion: (event: T) => void) {

const foundIndex = this.events.findIndex(event => event.constructor === type);

if (foundIndex < 0) {
throw new Error(`${ type.name } event not found within ${ this.events.map(e => e.constructor.name).join(', ') }`);
}

assertion(this.events[ foundIndex ] as T);

this.events = this.events.slice(foundIndex + 1);

return this;
}
}
1 change: 1 addition & 0 deletions integration/cucumber/src/index.ts
@@ -1 +1,2 @@
export * from './cucumber';
export * from './Pick';
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions package.json
Expand Up @@ -4,9 +4,9 @@
"description": "Serenity/JS: Next generation acceptance testing library for modern web applications.",
"private": true,
"author": {
"name" : "Jan Molak",
"email" : "jan.molak@smartcodeltd.co.uk",
"url" : "https://janmolak.com"
"name": "Jan Molak",
"email": "jan.molak@smartcodeltd.co.uk",
"url": "https://janmolak.com"
},
"homepage": "http://serenity-js.org",
"license": "Apache-2.0",
Expand Down
8 changes: 4 additions & 4 deletions packages/core/spec/model/outcomes.spec.ts
Expand Up @@ -3,8 +3,8 @@ import { given } from 'mocha-testdata';

import { TestCompromisedError } from '../../src/errors';
import {
AssertionFailed,
ErrorOccurred,
ExecutionFailedWithAssertionError,
ExecutionFailedWithError,
ExecutionCompromised,
ExecutionIgnored,
ExecutionSkipped,
Expand Down Expand Up @@ -36,8 +36,8 @@ describe('Outcome', () => {

given([
new ExecutionCompromised(new TestCompromisedError('Database is down')),
new ErrorOccurred(new Error(`Something's wrong`)),
new AssertionFailed(assertionError()),
new ExecutionFailedWithError(new Error(`Something's wrong`)),
new ExecutionFailedWithAssertionError(assertionError()),
]).
it('can be serialised and deserialised', (outcome: ProblemIndication) => {
const deserialised: any = Outcome.fromJSON(outcome.toJSON());
Expand Down
Expand Up @@ -6,10 +6,10 @@ import { TestCompromisedError } from '../../../../src/errors';
import { ArtifactGenerated, SceneFinished, SceneStarts, TestRunnerDetected } from '../../../../src/events';
import { Artifact, FileSystemLocation, FileType, Path } from '../../../../src/io';
import {
AssertionFailed,
ExecutionFailedWithAssertionError,
Category,
Duration,
ErrorOccurred,
ExecutionFailedWithError,
ExecutionCompromised,
ExecutionIgnored,
ExecutionSkipped,
Expand Down Expand Up @@ -196,7 +196,7 @@ describe('SerenityBDDReporter', () => {
} catch (assertionError) {

given(reporter).isNotifiedOfFollowingEvents(
new SceneFinished(defaultCardScenario, new AssertionFailed(assertionError)),
new SceneFinished(defaultCardScenario, new ExecutionFailedWithAssertionError(assertionError)),
);

report = stageManager.notifyOf.firstCall.lastArg.artifact.contents;
Expand All @@ -219,7 +219,7 @@ describe('SerenityBDDReporter', () => {
].join('\n');

given(reporter).isNotifiedOfFollowingEvents(
new SceneFinished(defaultCardScenario, new ErrorOccurred(error)),
new SceneFinished(defaultCardScenario, new ExecutionFailedWithError(error)),
);

report = stageManager.notifyOf.firstCall.lastArg.artifact.contents;
Expand Down
@@ -0,0 +1,55 @@
import 'mocha';

import * as sinon from 'sinon';

import {
SceneBackgroundDetected,
SceneDescriptionDetected,
SceneFinished,
SceneStarts,
} from '../../../../../src/events';
import { Description, ExecutionSuccessful, Name } from '../../../../../src/model';
import { SerenityBDDReporter, StageManager } from '../../../../../src/stage';
import { SerenityBDDReport } from '../../../../../src/stage/crew/serenity-bdd-reporter/SerenityBDDJsonSchema';
import { expect } from '../../../../expect';
import { given } from '../../given';
import { defaultCardScenario } from '../../samples';

describe('SerenityBDDReporter', () => {

let stageManager: sinon.SinonStubbedInstance<StageManager>,
reporter: SerenityBDDReporter;

beforeEach(() => {
stageManager = sinon.createStubInstance(StageManager);

reporter = new SerenityBDDReporter();
reporter.assignTo(stageManager as any);
});


it('captures information about scenario background', () => {
given(reporter).isNotifiedOfFollowingEvents(
new SceneStarts(defaultCardScenario),
new SceneBackgroundDetected(new Name('Background title'), new Description('Background description')),
new SceneFinished(defaultCardScenario, new ExecutionSuccessful()),
);

const report: SerenityBDDReport = stageManager.notifyOf.firstCall.lastArg.artifact.contents;

expect(report.backgroundTitle).to.equal('Background title');
expect(report.backgroundDescription).to.equal('Background description');
});

it('captures the description of the scenario', () => {
given(reporter).isNotifiedOfFollowingEvents(
new SceneStarts(defaultCardScenario),
new SceneDescriptionDetected(new Description('Scenario description')),
new SceneFinished(defaultCardScenario, new ExecutionSuccessful()),
);

const report: SerenityBDDReport = stageManager.notifyOf.firstCall.lastArg.artifact.contents;

expect(report.description).to.equal('Scenario description');
});
});
22 changes: 22 additions & 0 deletions packages/core/src/events/SceneBackgroundDetected.ts
@@ -0,0 +1,22 @@
import { ensure, isDefined, Serialised } from 'tiny-types';

import { Description, Name } from '../model';
import { DomainEvent } from './DomainEvent';

export class SceneBackgroundDetected extends DomainEvent {
public static fromJSON(o: Serialised<SceneBackgroundDetected>) {
return new SceneBackgroundDetected(
Name.fromJSON(o.name as string),
Description.fromJSON(o.description as string),
);
}

constructor(
public readonly name: Name,
public readonly description: Description,
) {
super();
ensure('name', name, isDefined());
ensure('description', description, isDefined());
}
}
19 changes: 19 additions & 0 deletions packages/core/src/events/SceneDescriptionDetected.ts
@@ -0,0 +1,19 @@
import { ensure, isDefined, Serialised } from 'tiny-types';

import { Description } from '../model';
import { DomainEvent } from './DomainEvent';

export class SceneDescriptionDetected extends DomainEvent {
public static fromJSON(o: Serialised<SceneDescriptionDetected>) {
return new SceneDescriptionDetected(
Description.fromJSON(o.description as string),
);
}

constructor(
public readonly description: Description,
) {
super();
ensure('description', description, isDefined());
}
}
2 changes: 2 additions & 0 deletions packages/core/src/events/index.ts
Expand Up @@ -3,6 +3,8 @@ export * from './ActivityFinished';
export * from './ArtifactGenerated';
export * from './AsyncOperationAttempted';
export * from './DomainEvent';
export * from './SceneBackgroundDetected';
export * from './SceneDescriptionDetected';
export * from './SceneStarts';
export * from './SceneFinished';
export * from './SceneTagged';
Expand Down

0 comments on commit adb3412

Please sign in to comment.