@serenity-js/cucumber
Version:
Serenity/JS test runner adapter for seamless integration with any version of Cucumber.js, facilitating BDD-style test automation and leveraging Serenity/JS reporting capabilities
227 lines (184 loc) • 9.53 kB
text/typescript
import { RuntimeError } from '@serenity-js/core';
import { AssertionError, ErrorSerialiser, ImplementationPendingError, TestCompromisedError } from '@serenity-js/core/lib/errors';
import { FileSystemLocation, Path } from '@serenity-js/core/lib/io';
import type {
Outcome} from '@serenity-js/core/lib/model';
import {
ExecutionCompromised,
ExecutionFailedWithAssertionError,
ExecutionFailedWithError,
ExecutionSkipped,
ExecutionSuccessful,
ImplementationPending,
Name
} from '@serenity-js/core/lib/model';
import { ensure, isDefined } from 'tiny-types';
import { AmbiguousStepDefinitionError } from '../../errors';
import type { CucumberFormatterOptions } from './CucumberFormatterOptions';
import type { Dependencies } from './Dependencies';
import { Feature, Hook, Scenario, ScenarioOutline, Step } from './gherkin';
interface Location {
uri: string;
line: number;
}
interface StepLocations {
actionLocation?: Location;
sourceLocation?: Location;
}
/**
* @private
*/
export function cucumberEventProtocolAdapter({ serenity, notifier, mapper, cache }: Dependencies) { // eslint-disable-line @typescript-eslint/explicit-module-boundary-types
return class CucumberEventProtocolAdapter {
// note: exported class expression can't have private properties
public readonly log: any;
constructor({ eventBroadcaster, log }: CucumberFormatterOptions) {
this.log = log;
eventBroadcaster.on('gherkin-document', ({ uri, document }) => {
ensure('gherkin-document :: uri', uri, isDefined());
ensure('gherkin-document :: document', document, isDefined());
const path = new Path(uri);
cache.set(path, mapper.map(document, path)); // eslint-disable-line unicorn/no-array-method-this-argument
});
eventBroadcaster.on('test-case-prepared', ({ steps, sourceLocation }: {
steps: StepLocations[],
sourceLocation: Location,
}) => {
ensure('test-case-prepared :: steps', steps, isDefined());
ensure('test-case-prepared :: sourceLocation', sourceLocation, isDefined());
const
path = new Path(sourceLocation.uri),
map = cache.get(path),
scenario = map.get(Scenario).onLine(sourceLocation.line);
if (scenario.outline) {
const outline = map.get(ScenarioOutline).onLine(scenario.outline.line);
map.set(new ScenarioOutline(
outline.location,
outline.name,
outline.description,
outline.steps,
outline.parameters,
)).onLine(scenario.outline.line);
}
map.set(new Scenario(
scenario.location,
scenario.name,
scenario.description,
interleaveStepsAndHooks(scenario.steps, steps),
scenario.tags,
scenario.outline,
)).onLine(sourceLocation.line);
});
eventBroadcaster.on('test-case-started', ({ sourceLocation }) => {
ensure('test-case-started :: sourceLocation', sourceLocation, isDefined());
const
map = cache.get(new Path(sourceLocation.uri)),
scenario = map.get(Scenario).onLine(sourceLocation.line),
sceneId = serenity.assignNewSceneId();
if (scenario.outline) {
const outline = map.get(ScenarioOutline).onLine(scenario.outline.line);
notifier.outlineDetected(sceneId, scenario, outline, map.getFirst(Feature));
}
notifier.scenarioStarts(sceneId, scenario, map.getFirst(Feature));
});
eventBroadcaster.on('test-step-started', ({ index, testCase }) => {
ensure('test-step-started :: index', index, isDefined());
ensure('test-step-started :: testCase', testCase, isDefined());
const
map = cache.get(new Path(testCase.sourceLocation.uri)),
scenario = map.get(Scenario).onLine(testCase.sourceLocation.line),
step = scenario.steps[index];
if (step instanceof Step) { // ignore hooks
notifier.stepStarts(step);
}
});
eventBroadcaster.on('test-step-finished', ({ index, result, testCase }) => {
ensure('test-step-finished :: index', index, isDefined());
ensure('test-step-finished :: result', result, isDefined());
ensure('test-step-finished :: testCase', testCase, isDefined());
const
map = cache.get(new Path(testCase.sourceLocation.uri)),
scenario = map.get(Scenario).onLine(testCase.sourceLocation.line),
step = scenario.steps[index];
if (step instanceof Step) { // ignore hooks
notifier.stepFinished(step, this.outcomeFrom(result));
}
});
eventBroadcaster.on('test-case-finished', ({ result, sourceLocation }) => {
ensure('test-case-finished :: result', result, isDefined());
ensure('test-case-finished :: sourceLocation', sourceLocation, isDefined());
const
map = cache.get(new Path(sourceLocation.uri)),
scenario = map.get(Scenario).onLine(sourceLocation.line),
nonHookSteps = scenario.steps.filter(step => step instanceof Step);
const outcome: Outcome = nonHookSteps.length > 0
? this.outcomeFrom(result)
: new ImplementationPending(new ImplementationPendingError(`"${ scenario.name.value }" has no test steps`));
notifier.scenarioFinished(scenario, map.getFirst(Feature), outcome);
});
}
outcomeFrom(result: { duration: number, exception: string | Error, status: string }): Outcome {
const error = !! result.exception && this.errorFrom(result.exception);
switch (result.status) {
case 'undefined':
return new ImplementationPending(new ImplementationPendingError('Step not implemented'));
case 'ambiguous':
case 'failed':
switch (true) {
case error instanceof AssertionError: return new ExecutionFailedWithAssertionError(error as AssertionError);
case error instanceof TestCompromisedError: return new ExecutionCompromised(error as TestCompromisedError);
default: return new ExecutionFailedWithError(error);
}
case 'pending':
return new ImplementationPending(new ImplementationPendingError('Step not implemented'));
case 'skipped':
return new ExecutionSkipped();
// case 'passed':
default:
return new ExecutionSuccessful();
}
}
errorFrom(maybeError: Error | string): Error {
switch (true) {
case maybeError instanceof RuntimeError:
return maybeError as Error;
case maybeError instanceof Error && maybeError.name === 'AssertionError' && maybeError.message && hasOwnProperty(maybeError, 'expected') && hasOwnProperty(maybeError, 'actual'):
return serenity.createError(AssertionError, {
message: (maybeError as any).message,
diff: {
expected: (maybeError as any).expected,
actual: (maybeError as any).actual,
},
cause: maybeError as Error
});
case typeof maybeError === 'string' && maybeError.startsWith('Multiple step definitions match'):
return new AmbiguousStepDefinitionError(maybeError as string);
default:
return ErrorSerialiser.deserialiseFromStackTrace(maybeError as string);
}
}
};
}
/**
* @private
*/
function interleaveStepsAndHooks(steps: Step[], stepsLocations: StepLocations[]): Array<Step | Hook> {
const
isAHook = (stepLocations: StepLocations) =>
stepLocations.actionLocation && ! stepLocations.sourceLocation,
matching = (location: StepLocations) =>
(step: Step) =>
step.location.path.equals(new Path(location.sourceLocation.uri)) &&
step.location.line === location.sourceLocation.line;
return stepsLocations.map(location =>
isAHook(location)
? new Hook(new FileSystemLocation(new Path(location.actionLocation.uri), location.actionLocation.line), new Name('Setup'))
: steps.find(matching(location)),
);
}
/**
* @private
*/
function hasOwnProperty(value: any, fieldName: string): boolean {
return Object.prototype.hasOwnProperty.call(value, fieldName);
}