@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
215 lines • 12.8 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.CucumberMessagesParser = void 0;
const messages_1 = require("@cucumber/messages");
const core_1 = require("@serenity-js/core");
const events_1 = require("@serenity-js/core/lib/events");
const io_1 = require("@serenity-js/core/lib/io");
const io_2 = require("@serenity-js/core/lib/io");
const model_1 = require("@serenity-js/core/lib/model");
const TestStepFormatter_1 = require("./TestStepFormatter");
/**
* @package
*/
class CucumberMessagesParser {
serenity;
formatterHelpers;
shouldReportStep;
testStepFormatter = new TestStepFormatter_1.TestStepFormatter();
currentScenario;
currentStepActivityId;
cwd;
eventDataCollector;
snippetBuilder;
supportCodeLibrary;
requirementsHierarchy;
constructor(serenity, formatterHelpers, // eslint-disable-line @typescript-eslint/explicit-module-boundary-types
formatterOptionsAndDependencies, shouldReportStep) {
this.serenity = serenity;
this.formatterHelpers = formatterHelpers;
this.shouldReportStep = shouldReportStep;
this.cwd = formatterOptionsAndDependencies.cwd;
this.eventDataCollector = formatterOptionsAndDependencies.eventDataCollector;
this.snippetBuilder = formatterOptionsAndDependencies.snippetBuilder;
this.supportCodeLibrary = formatterOptionsAndDependencies.supportCodeLibrary;
this.requirementsHierarchy = new io_2.RequirementsHierarchy(new io_1.FileSystem(io_1.Path.from(formatterOptionsAndDependencies.cwd)), formatterOptionsAndDependencies.parsedArgvOptions?.specDirectory && io_1.Path.from(formatterOptionsAndDependencies.parsedArgvOptions?.specDirectory));
}
parseTestCaseStarted(message) {
const testCaseAttempt = this.eventDataCollector.getTestCaseAttempt(message.id), currentSceneId = this.serenity.assignNewSceneId();
this.currentScenario = this.scenarioDetailsFor(testCaseAttempt.gherkinDocument, testCaseAttempt.pickle, this.formatterHelpers.PickleParser.getPickleLocation(testCaseAttempt));
return [
...this.extract(this.outlineFrom(testCaseAttempt), (outline) => [
new events_1.SceneSequenceDetected(currentSceneId, outline.details, this.serenity.currentTime()),
new events_1.SceneTemplateDetected(currentSceneId, outline.template, this.serenity.currentTime()),
new events_1.SceneParametersDetected(currentSceneId, this.currentScenario, outline.parameters, this.serenity.currentTime()),
]),
...this.extract(this.scenarioFrom(testCaseAttempt), ({ featureDescription, rule, scenarioDescription, tags, testRunnerName }) => [
new events_1.SceneStarts(currentSceneId, this.currentScenario, this.serenity.currentTime()),
featureDescription && new events_1.FeatureNarrativeDetected(currentSceneId, featureDescription, this.serenity.currentTime()),
new events_1.TestRunnerDetected(currentSceneId, testRunnerName, this.serenity.currentTime()),
!!scenarioDescription && new events_1.SceneDescriptionDetected(currentSceneId, scenarioDescription, this.serenity.currentTime()),
!!rule && new events_1.BusinessRuleDetected(currentSceneId, this.currentScenario, rule, this.serenity.currentTime()),
...tags.map(tag => new events_1.SceneTagged(currentSceneId, tag, this.serenity.currentTime())),
]),
];
}
parseTestStepStarted(message) {
return this.extract(this.stepFrom(message), (step) => {
if (this.shouldReportStep(step)) {
const activityDetails = this.activityDetailsFor(step);
this.currentStepActivityId = this.serenity.assignNewActivityId(activityDetails);
return new events_1.TaskStarts(this.serenity.currentSceneId(), this.currentStepActivityId, this.activityDetailsFor(step), this.serenity.currentTime());
}
});
}
parseTestStepFinished(message) {
return this.extract(this.stepFrom(message), (step) => {
if (this.shouldReportStep(step)) {
return new events_1.TaskFinished(this.serenity.currentSceneId(), this.currentStepActivityId, this.activityDetailsFor(step), this.outcomeFrom(step.result, step), this.serenity.currentTime());
}
});
}
parseTestCaseFinished(message) {
const testCaseAttempt = this.eventDataCollector.getTestCaseAttempt(message.testCaseStartedId), currentSceneId = this.serenity.currentSceneId();
return this.extract(this.scenarioOutcomeFrom(testCaseAttempt), ({ outcome, willBeRetried, tags }) => [
willBeRetried ? new events_1.RetryableSceneDetected(currentSceneId, this.serenity.currentTime()) : undefined,
...tags.map(tag => new events_1.SceneTagged(currentSceneId, tag, this.serenity.currentTime())),
new events_1.SceneFinished(currentSceneId, this.currentScenario, outcome, this.serenity.currentTime()),
]);
}
// ---
extract(maybeValue, fn) {
return (maybeValue === undefined)
? []
: [].concat(fn(maybeValue)).filter(item => !!item);
}
scenarioDetailsFor(gherkinDocument, pickle, location) {
return new model_1.ScenarioDetails(new model_1.Name(pickle.name), new model_1.Category(gherkinDocument.feature.name), new io_1.FileSystemLocation(this.absolutePathFrom(gherkinDocument.uri), location.line, location.column));
}
outlineFrom(testCaseAttempt) {
const { gherkinDocument, pickle } = testCaseAttempt, gherkinScenarioMap = this.formatterHelpers.GherkinDocumentParser.getGherkinScenarioMap(gherkinDocument);
if (gherkinScenarioMap[pickle.astNodeIds[0]].examples.length === 0) {
return; // this is not an outline, skip it
}
const outline = gherkinScenarioMap[pickle.astNodeIds[0]];
const details = this.scenarioDetailsFor(gherkinDocument, outline, outline.location);
const template = new model_1.Description(outline.steps.map(step => this.testStepFormatter.format(step.keyword, step.text, step)).join('\n'));
const examples = flatten(outline.examples.map(exampleSet => exampleSet.tableBody.map(row => ({
header: exampleSet.tableHeader,
row,
name: exampleSet.name,
description: exampleSet.description,
})))).map((example) => ({
rowId: example.row.id,
name: example.name.trim(),
description: example.description.trim(),
values: example.header.cells
.map(cell => cell.value)
.reduce((values, header, i) => {
values[header] = example.row.cells[i].value;
return values;
}, {}),
}));
const parameters = examples.find(example => example.rowId === pickle.astNodeIds.at(-1));
return {
details, template, parameters: new model_1.ScenarioParameters(new model_1.Name(parameters.name), new model_1.Description(parameters.description), parameters.values),
};
}
scenarioFrom({ gherkinDocument, pickle }) {
const gherkinScenarioMap = this.formatterHelpers.GherkinDocumentParser.getGherkinScenarioMap(gherkinDocument), gherkinExampleRuleMap = this.formatterHelpers.GherkinDocumentParser.getGherkinExampleRuleMap(gherkinDocument), scenarioDescription = this.formatterHelpers.PickleParser.getScenarioDescription({ gherkinScenarioMap, pickle }), scenarioTags = flatten(pickle.tags.map(tag => model_1.Tags.from(tag.name))), rule = gherkinExampleRuleMap[pickle.astNodeIds[0]];
return {
featureDescription: gherkinDocument.feature.description && new model_1.Description(gherkinDocument.feature.description),
scenarioDescription: scenarioDescription && new model_1.Description(scenarioDescription),
rule: rule && new model_1.BusinessRule(new model_1.Name(rule.name), new model_1.Description(rule.description.trim())),
testRunnerName: new model_1.Name('JS'),
tags: this.requirementsHierarchy.requirementTagsFor(io_1.Path.from(this.cwd).resolve(io_1.Path.from(gherkinDocument.uri)), gherkinDocument.feature.name).concat(scenarioTags),
};
}
stepFrom(message) {
const { testCaseStartedId, testStepId } = message;
const testCaseAttempt = this.eventDataCollector.getTestCaseAttempt(testCaseStartedId);
const index = testCaseAttempt.testCase.testSteps.findIndex(step => step.id === testStepId);
return this.parseTestCaseAttempt(testCaseAttempt).testSteps[index];
}
parseTestCaseAttempt(testCaseAttempt) {
// workaround for a bug in Cucumber 7, that's fixed in Cucumber 8 by https://github.com/cucumber/cucumber-js/pull/1531
testCaseAttempt.testCase.testSteps.forEach(step => {
if (!testCaseAttempt.stepResults[step.id]) {
testCaseAttempt.stepResults[step.id] = { duration: { seconds: 0, nanos: 0 }, status: messages_1.TestStepResultStatus.UNKNOWN, willBeRetried: false };
}
});
// ---
return this.formatterHelpers.parseTestCaseAttempt({
cwd: this.cwd,
testCaseAttempt,
snippetBuilder: this.snippetBuilder,
supportCodeLibrary: this.supportCodeLibrary,
});
}
activityDetailsFor(parsedTestStep) {
const location = parsedTestStep.sourceLocation || parsedTestStep.actionLocation;
return new model_1.ActivityDetails(new model_1.Name(this.testStepFormatter.format(parsedTestStep.keyword, parsedTestStep.text || parsedTestStep.name, parsedTestStep.argument)), new io_1.FileSystemLocation(this.absolutePathFrom(location.uri), location.line));
}
outcomeFrom(worstResult, ...steps) {
const Status = messages_1.TestStepResultStatus;
// todo: how does it treat failed but retryable scenarios?
switch (worstResult.status) {
case Status.SKIPPED:
return new model_1.ExecutionSkipped();
case Status.UNDEFINED: {
const snippets = steps
.filter(step => step.result.status === Status.UNDEFINED)
.map(step => step.snippet);
const message = snippets.length > 0
? ['Step implementation missing:', ...snippets].join('\n\n')
: 'Step implementation missing';
return new model_1.ImplementationPending(new core_1.ImplementationPendingError(message));
}
case Status.PENDING:
return new model_1.ImplementationPending(new core_1.ImplementationPendingError('Step implementation pending'));
case Status.AMBIGUOUS:
case Status.FAILED: {
const error = core_1.ErrorSerialiser.deserialiseFromStackTrace(worstResult.message);
if (error instanceof core_1.AssertionError) {
return new model_1.ExecutionFailedWithAssertionError(error);
}
if (error instanceof core_1.TestCompromisedError) {
return new model_1.ExecutionCompromised(error);
}
return new model_1.ExecutionFailedWithError(error);
}
case Status.UNKNOWN:
// ignore
case Status.PASSED: // eslint-disable-line no-fallthrough
return new model_1.ExecutionSuccessful();
}
}
scenarioOutcomeFrom(testCaseAttempt) {
const parsed = this.formatterHelpers.parseTestCaseAttempt({
cwd: this.cwd,
snippetBuilder: this.snippetBuilder,
supportCodeLibrary: this.supportCodeLibrary,
testCaseAttempt
});
const worstStepResult = parsed.testCase.worstTestStepResult;
const willBeRetried = worstStepResult.willBeRetried || // Cucumber 7
testCaseAttempt.willBeRetried; // Cucumber 8
const outcome = this.outcomeFrom(worstStepResult, ...parsed.testSteps);
const tags = [];
if (testCaseAttempt.attempt > 0 || willBeRetried) {
tags.push(new model_1.ArbitraryTag('retried'));
}
if (testCaseAttempt.attempt > 0) {
tags.push(new model_1.ExecutionRetriedTag(testCaseAttempt.attempt));
}
return { outcome, willBeRetried, tags };
}
absolutePathFrom(relativePath) {
return io_1.Path.from(this.cwd).resolve(io_1.Path.from(relativePath));
}
}
exports.CucumberMessagesParser = CucumberMessagesParser;
function flatten(listOfLists) {
return listOfLists.reduce((acc, current) => acc.concat(current), []);
}
//# sourceMappingURL=CucumberMessagesParser.js.map