Skip to content

Commit

Permalink
fix(cucumber): Corrected how steps are reported for scenarios that us…
Browse files Browse the repository at this point in the history
…e before/after hooks

See cucumber/cucumber-js#1195
  • Loading branch information
jan-molak committed Apr 23, 2019
1 parent 9599fe6 commit 6563309
Show file tree
Hide file tree
Showing 17 changed files with 465 additions and 47 deletions.
2 changes: 1 addition & 1 deletion integration/cucumber/spec/pending_scenarios.spec.ts
Expand Up @@ -170,7 +170,7 @@ describe('@serenity-js/cucumber', function () {
.next(TestRunnerDetected, event => expect(event.value).to.equal(new Name('Cucumber')))
.next(SceneTagged, event => expect(event.tag).to.equal(new FeatureTag('Serenity/JS recognises pending scenarios')))
.next(ActivityStarts, event => expect(event.value.name).to.equal(new Name(`Given step number one that passes`)))
.next(ActivityFinished, event => expect(event.outcome.constructor).to.equal(ImplementationPending))
.next(ActivityFinished, event => expect(event.outcome).to.equal(new ExecutionSkipped()))
.next(ActivityStarts, event => expect(event.value.name).to.equal(new Name(`And step number two that is marked as pending`)))
.next(ActivityFinished, event => expect(event.outcome).to.equal(new ExecutionSkipped()))
.next(ActivityStarts, event => expect(event.value.name).to.equal(new Name(`And step number three that fails with generic error`)))
Expand Down
21 changes: 7 additions & 14 deletions integration/cucumber/spec/screenplay_scenario.spec.ts
Expand Up @@ -26,17 +26,13 @@ describe('@serenity-js/cucumber', function () {
.withStepDefsIn('screenplay')
.toRun('features/screenplay_scenario.feature'),

// fixme: a bug in Event Protocol causes Cucumber 3-5 to incorrectly report test-step-start and test-step-finished events
// fixme: see https://github.com/cucumber/cucumber-js/issues/1195
// ...cucumberVersions(3, 4, 5)
// .thatRequires('lib/support/configure_serenity.js')
// .withStepDefsIn('tasty-cucumber')
// .withArgs(
// // '--format', 'node_modules/@serenity-js/cucumber/register.js',
// '--format', 'event-protocol',
// )
// // .toRun('features/screenplay_scenario.feature'),
// .toRun('features/tasty-cucumber.feature'),
...cucumberVersions(3, 4, 5)
.thatRequires('lib/support/configure_serenity.js')
.withStepDefsIn('screenplay')
.withArgs(
'--format', 'node_modules/@serenity-js/cucumber/register.js',
)
.toRun('features/screenplay_scenario.feature'),
]).
it('recognises Screenplay activities in any part of the Cucumber scenario', (runner: CucumberRunner) => runner.run().
then(ifExitCodeIsOtherThan(0, logOutput)).
Expand Down Expand Up @@ -68,7 +64,4 @@ describe('@serenity-js/cucumber', function () {
.next(SceneFinished, event => expect(event.outcome).to.equal(new ExecutionSuccessful()))
;
}));

// todo: I added a skipped test so that I remember to add the tests above back in when the Cucumber bug is fixed
it('recognises Screenplay activities in any part of the Cucumber scenario when the Event Protocol is being used');
});
6 changes: 3 additions & 3 deletions package-lock.json

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

3 changes: 0 additions & 3 deletions packages/core/src/io/ModuleLoader.ts
Expand Up @@ -9,9 +9,6 @@ export class ModuleLoader {
/**
* @package
*
* @param fromDir
* Directory where NPM should start the module lookup. For example: process.cwd()
*
* @param moduleId
* NPM module id, for example 'cucumber' or '@serenity-js/core'
*/
Expand Down
267 changes: 267 additions & 0 deletions packages/cucumber/spec/listeners/CucumberEventProtocolAdapter.spec.ts
@@ -0,0 +1,267 @@
import { EventRecorder, expect, PickEvent } from '@integration/testing-tools';
import { Serenity } from '@serenity-js/core';
import { SceneFinished, SceneStarts, SceneTagged, TaskFinished, TaskStarts, TestRunnerDetected } from '@serenity-js/core/lib/events';
import { FileSystemLocation, Path, Version } from '@serenity-js/core/lib/io';
import { Category, ExecutionFailedWithError, ExecutionSkipped, ExecutionSuccessful, FeatureTag, ImplementationPending, Name, ScenarioDetails } from '@serenity-js/core/lib/model';

import { EventEmitter } from 'events';

import * as sinon from 'sinon';
import { JSONObject } from 'tiny-types';
import { AmbiguousStepDefinitionError } from '../../src/errors';
import { listenerForCucumber } from '../../src/listeners';

describe('CucumberEventProtocolAdapter', () => {

type CucumberHook = () => Promise<void> | void;

const fakeCucumber = {
After: (hook: CucumberHook) => Promise.resolve(hook()),
AfterAll: (hook: CucumberHook) => Promise.resolve(hook()),
};

let recorder: EventRecorder,
serenity: Serenity,
log: typeof console.log,
eventBroadcaster: EventEmitter,
adapter: any;

beforeEach(() => {

log = sinon.spy();
serenity = new Serenity();
recorder = new EventRecorder();
eventBroadcaster = new EventEmitter();

serenity.setTheStage(
recorder,
);

const listener = listenerForCucumber(new Version('5.0.0'), fakeCucumber, serenity);

adapter = new listener({ eventBroadcaster, log });
});

it('correctly recognises Cucumber Event Protocol events', () => {

emitAllFrom(require('./samples/scenario-with-hooks.json'));

const expectedScenarioDetails = new ScenarioDetails(
new Name('Hooks'),
new Category('Event Protocol'),
new FileSystemLocation(
new Path('features/tasty-cucumber.feature'),
3,
3,
),
);

PickEvent.from(recorder.events)
.next(SceneStarts, e => expect(e.value).to.equal(expectedScenarioDetails))
.next(TestRunnerDetected, e => expect(e.value).to.equal(new Name('Cucumber')))
.next(SceneTagged, e => expect(e.tag).to.equal(new FeatureTag('Event Protocol')))
.next(TaskStarts, e => expect(e.value.name).to.equal(new Name('Given I have a tasty cucumber in my belly')))
.next(TaskFinished, e => {
expect(e.value.name).to.equal(new Name('Given I have a tasty cucumber in my belly'));
expect(e.outcome).to.equal(new ExecutionSuccessful());
})
.next(TaskStarts, e => expect(e.value.name).to.equal(new Name(`Then I'm very happy`)))
.next(TaskFinished, e => {
expect(e.value.name).to.equal(new Name(`Then I'm very happy`));
expect(e.outcome).to.equal(new ExecutionSuccessful());
})
.next(SceneFinished, e => {
expect(e.value).to.equal(expectedScenarioDetails);
expect(e.outcome).to.equal(new ExecutionSuccessful());
})
;
});

it('correctly recognises undefined steps', () => {

emitAllFrom(require('./samples/scenario-with-undefined-steps.json'));

const expectedScenarioDetails = new ScenarioDetails(
new Name('Undefined steps'),
new Category('Event Protocol'),
new FileSystemLocation(
new Path('features/undefined-steps.feature'),
3,
3,
),
);

PickEvent.from(recorder.events)
.next(SceneStarts, e => expect(e.value).to.equal(expectedScenarioDetails))
.next(TestRunnerDetected, e => expect(e.value).to.equal(new Name('Cucumber')))
.next(SceneTagged, e => expect(e.tag).to.equal(new FeatureTag('Event Protocol')))
.next(TaskStarts, e => expect(e.value.name).to.equal(new Name('Given I have an undefined step')))
.next(TaskFinished, e => {
expect(e.value.name).to.equal(new Name('Given I have an undefined step'));
expect(e.outcome).to.be.instanceOf(ImplementationPending);
})
.next(TaskStarts, e => expect(e.value.name).to.equal(new Name(`Then I should implement it`)))
.next(TaskFinished, e => {
expect(e.value.name).to.equal(new Name('Then I should implement it'));
expect(e.outcome).to.be.instanceOf(ImplementationPending);
})
.next(SceneFinished, e => {
expect(e.value).to.equal(expectedScenarioDetails);
expect(e.outcome).to.be.instanceOf(ImplementationPending);
})
;
});

it('correctly recognises pending steps', () => {

emitAllFrom(require('./samples/scenario-with-pending-steps.json'));

const expectedScenarioDetails = new ScenarioDetails(
new Name('Pending steps'),
new Category('Event Protocol'),
new FileSystemLocation(
new Path('features/pending-steps.feature'),
3,
3,
),
);

PickEvent.from(recorder.events)
.next(SceneStarts, e => expect(e.value).to.equal(expectedScenarioDetails))
.next(TestRunnerDetected, e => expect(e.value).to.equal(new Name('Cucumber')))
.next(SceneTagged, e => expect(e.tag).to.equal(new FeatureTag('Event Protocol')))
.next(TaskStarts, e => expect(e.value.name).to.equal(new Name('Given I have a pending step')))
.next(TaskFinished, e => {
expect(e.value.name).to.equal(new Name('Given I have a pending step'));
expect(e.outcome).to.be.instanceOf(ImplementationPending);
})
.next(TaskStarts, e => expect(e.value.name).to.equal(new Name(`Then I should implement it`)))
.next(TaskFinished, e => {
expect(e.value.name).to.equal(new Name('Then I should implement it'));
expect(e.outcome).to.be.instanceOf(ExecutionSkipped);
})
.next(SceneFinished, e => {
expect(e.value).to.equal(expectedScenarioDetails);
expect(e.outcome).to.be.instanceOf(ImplementationPending);
})
;
});

it('correctly recognises ambiguous steps', () => {

emitAllFrom(require('./samples/scenario-with-ambiguous-steps.json'));

const expectedScenarioDetails = new ScenarioDetails(
new Name('Ambiguous steps'),
new Category('Event Protocol'),
new FileSystemLocation(
new Path('features/ambiguous-steps.feature'),
3,
3,
),
);

PickEvent.from(recorder.events)
.next(SceneStarts, e => expect(e.value).to.equal(expectedScenarioDetails))
.next(TestRunnerDetected, e => expect(e.value).to.equal(new Name('Cucumber')))
.next(SceneTagged, e => expect(e.tag).to.equal(new FeatureTag('Event Protocol')))
.next(TaskStarts, e => expect(e.value.name).to.equal(new Name('Given I have an ambiguous step definition')))
.next(TaskFinished, e => {
expect(e.value.name).to.equal(new Name('Given I have an ambiguous step definition'));
expect(e.outcome).to.be.instanceOf(ExecutionFailedWithError);
expect((e.outcome as ExecutionFailedWithError).error).to.be.instanceOf(AmbiguousStepDefinitionError);
})
.next(TaskStarts, e => expect(e.value.name).to.equal(new Name(`Then I should correct it`)))
.next(TaskFinished, e => {
expect(e.value.name).to.equal(new Name('Then I should correct it'));
expect(e.outcome).to.be.instanceOf(ExecutionSkipped);
})
.next(SceneFinished, e => {
expect(e.value).to.equal(expectedScenarioDetails);
expect(e.outcome).to.be.instanceOf(ExecutionFailedWithError);
expect((e.outcome as ExecutionFailedWithError).error).to.be.instanceOf(AmbiguousStepDefinitionError);
})
;
});

it('correctly recognises errors thrown in steps', () => {

emitAllFrom(require('./samples/scenario-with-errors.json'));

const expectedScenarioDetails = new ScenarioDetails(
new Name('Errors in steps'),
new Category('Event Protocol'),
new FileSystemLocation(
new Path('features/errors-in-steps.feature'),
3,
3,
),
);

PickEvent.from(recorder.events)
.next(SceneStarts, e => expect(e.value).to.equal(expectedScenarioDetails))
.next(TestRunnerDetected, e => expect(e.value).to.equal(new Name('Cucumber')))
.next(SceneTagged, e => expect(e.tag).to.equal(new FeatureTag('Event Protocol')))
.next(TaskStarts, e => expect(e.value.name).to.equal(new Name('Given I have a step that throws an error')))
.next(TaskFinished, e => {
expect(e.value.name).to.equal(new Name('Given I have a step that throws an error'));
expect(e.outcome).to.be.instanceOf(ExecutionFailedWithError);
expect((e.outcome as ExecutionFailedWithError).error).to.be.instanceOf(Error);
expect((e.outcome as ExecutionFailedWithError).error.message).to.equal(`We're sorry, something happened`);
})
.next(SceneFinished, e => {
expect(e.value).to.equal(expectedScenarioDetails);
expect(e.outcome).to.be.instanceOf(ExecutionFailedWithError);
expect((e.outcome as ExecutionFailedWithError).error).to.be.instanceOf(Error);
expect((e.outcome as ExecutionFailedWithError).error.message).to.equal(`We're sorry, something happened`);
})
;
});

it('correctly recognises scenario outlines', () => {

emitAllFrom(require('./samples/scenario-outline.json'));

const expectedScenarioDetails = (line: number) => new ScenarioDetails(
new Name('The things I like'),
new Category('Event Protocol'),
new FileSystemLocation(
new Path('features/outlines.feature'),
line,
7,
),
);

PickEvent.from(recorder.events)
.next(SceneStarts, e => expect(e.value).to.equal(expectedScenarioDetails(10)))
.next(TestRunnerDetected, e => expect(e.value).to.equal(new Name('Cucumber')))
.next(SceneTagged, e => expect(e.tag).to.equal(new FeatureTag('Event Protocol')))
.next(TaskStarts, e => expect(e.value.name).to.equal(new Name('Given I like programming')))
.next(TaskFinished, e => expect(e.value.name).to.equal(new Name('Given I like programming')))
.next(SceneFinished, e => expect(e.value).to.equal(expectedScenarioDetails(10)))

.next(SceneStarts, e => expect(e.value).to.equal(expectedScenarioDetails(11)))
.next(TestRunnerDetected, e => expect(e.value).to.equal(new Name('Cucumber')))
.next(SceneTagged, e => expect(e.tag).to.equal(new FeatureTag('Event Protocol')))
.next(TaskStarts, e => expect(e.value.name).to.equal(new Name('Given I like to play guitar')))
.next(TaskFinished, e => expect(e.value.name).to.equal(new Name('Given I like to play guitar')))
.next(SceneFinished, e => expect(e.value).to.equal(expectedScenarioDetails(11)))

.next(SceneStarts, e => expect(e.value).to.equal(expectedScenarioDetails(12)))
.next(TestRunnerDetected, e => expect(e.value).to.equal(new Name('Cucumber')))
.next(SceneTagged, e => expect(e.tag).to.equal(new FeatureTag('Event Protocol')))
.next(TaskStarts, e => expect(e.value.name).to.equal(new Name('Given I like martial arts')))
.next(TaskFinished, e => expect(e.value.name).to.equal(new Name('Given I like martial arts')))
.next(SceneFinished, e => expect(e.value).to.equal(expectedScenarioDetails(12)))
;
});

function emitAllFrom(events: JSONObject[]): void {
events.forEach(event => {
// I can't use the convenient { type, ...body } construct because ESDoc/Babylon doesn't understand it; falling back to es5:
const emitted = Object.assign({}, event); // tslint:disable-line:prefer-object-spread
delete emitted.type;
eventBroadcaster.emit(event.type as string, emitted);
});
}
});

0 comments on commit 6563309

Please sign in to comment.