@wdio/cucumber-framework
Version:
A WebdriverIO plugin. Adapter for Cucumber.js testing framework.
549 lines (546 loc) • 18.4 kB
JavaScript
// src/cucumberFormatter.ts
import { Formatter, Status } from "@cucumber/cucumber";
import path2 from "node:path";
import logger2 from "@wdio/logger";
// src/utils.ts
import path from "node:path";
import logger from "@wdio/logger";
import { isFunctionAsync } from "@wdio/utils";
var log = logger("@wdio/cucumber-framework:utils");
function createStepArgument({ argument }) {
if (!argument) {
return void 0;
}
if (argument.dataTable) {
return {
rows: argument.dataTable.rows?.map((row) => ({
cells: row.cells?.map((cell) => cell.value)
}))
};
}
if (argument.docString) {
return argument.docString.content;
}
return void 0;
}
function formatMessage({ payload = {} }) {
const content = { ...payload };
if (payload.error && (payload.error.message || payload.error.stack)) {
const { name, message, stack } = payload.error;
content.error = { name, message, stack };
}
if (payload.title && payload.parent) {
content.fullTitle = `${payload.parent}: ${payload.title}`;
}
return content;
}
function getStepType(step) {
return step.hookId ? "hook" /* hook */ : "test" /* test */;
}
function getFeatureId(uri, feature) {
return `${path.basename(uri)}:${feature.location?.line}:${feature.location?.column}`;
}
function getTestStepTitle(keyword = "", text = "", type) {
const title = !text && type.toLowerCase() !== "hook" ? "Undefined Step" : text;
return `${keyword.trim()} ${title.trim()}`.trim();
}
function buildStepPayload(uri, feature, scenario, step, params) {
return {
...params,
uid: step.id,
// @ts-ignore
title: getTestStepTitle(step.keyword, step.text, params.type),
parent: scenario.id,
argument: createStepArgument(step),
file: uri,
tags: scenario.tags,
featureName: feature.name,
scenarioName: scenario.name
};
}
function getRule(feature, scenarioId) {
const rules = feature.children?.filter((child) => Object.keys(child)[0] === "rule");
const rule = rules.find((rule2) => {
const scenarioRule = rule2.rule?.children?.find((child) => child.scenario?.id === scenarioId);
if (scenarioRule) {
return rule2;
}
});
return rule?.rule?.name;
}
function addKeywordToStep(steps, feature) {
return steps.map((step) => {
if (step.astNodeIds && step.astNodeIds.length > 0 && feature.children) {
const astNodeId = step.astNodeIds[0];
const rules = feature.children.filter((child) => Object.keys(child)[0] === "rule");
let featureChildren = feature.children.filter((child) => Object.keys(child)[0] !== "rule");
const rulesChildrens = rules.map((child) => child.rule?.children).flat();
featureChildren = featureChildren.concat(rulesChildrens);
featureChildren.find(
(child) => (
// @ts-ignore
child[Object.keys(child)[0]].steps.find((featureScenarioStep) => {
if (featureScenarioStep.id === astNodeId.toString()) {
step.keyword = featureScenarioStep.keyword;
}
return;
})
)
);
return step;
}
return step;
});
}
function getScenarioDescription(feature, scenarioId) {
const children = feature.children?.find((child) => child?.scenario?.id === scenarioId);
return children?.scenario?.description || "";
}
function convertStatus(status) {
switch (status) {
case "PASSED":
return "pass";
case "PENDING":
return "pending";
case "SKIPPED":
return "skip";
case "AMBIGUOUS":
return "skip";
case "FAILED":
return "fail";
case "UNDEFINED":
return "pass";
default:
return "fail";
}
}
// src/cucumberFormatter.ts
var log2 = logger2("CucumberFormatter");
var CucumberFormatter = class extends Formatter {
_gherkinDocEvents = [];
_hookEvent = [];
_scenarios = [];
_testCases = [];
_currentTestCase;
_currentPickle = {};
_suiteMap = /* @__PURE__ */ new Map();
_pickleMap = /* @__PURE__ */ new Map();
_currentDoc = { comments: [] };
_startedFeatures = [];
reporter;
cid;
specs;
eventEmitter;
scenarioLevelReporter;
tagsInTitle;
ignoreUndefinedDefinitions;
failAmbiguousDefinitions;
failedCount = 0;
_featureStart;
_scenarioStart;
_testStart;
constructor(options) {
super(options);
let results = [];
options.eventBroadcaster.on("envelope", (envelope) => {
if (envelope.gherkinDocument) {
this.onGherkinDocument({ ...envelope.gherkinDocument, ...envelope.gherkinDocument.uri ? { uri: this.normalizeURI(envelope.gherkinDocument.uri) } : {} });
} else if (envelope.testRunStarted) {
this.onTestRunStarted();
} else if (envelope.pickle) {
this.onPickleAccepted({ ...envelope.pickle, uri: this.normalizeURI(envelope.pickle.uri) });
} else if (envelope.testCase) {
this.onTestCasePrepared(envelope.testCase);
} else if (envelope.testCaseStarted) {
results = [];
this.onTestCaseStarted(envelope.testCaseStarted);
} else if (envelope.testStepStarted) {
this.onTestStepStarted(envelope.testStepStarted);
} else if (envelope.testStepFinished) {
results.push(envelope.testStepFinished.testStepResult);
this.onTestStepFinished(envelope.testStepFinished);
} else if (envelope.testCaseFinished) {
if (envelope.testCaseFinished.willBeRetried) {
return log2.debug(
`test case with id ${envelope.testCaseFinished.testCaseStartedId} will be retried, ignoring result`
);
}
this.onTestCaseFinished(results);
} else if (envelope.testRunFinished) {
this.onTestRunFinished();
} else if (envelope.hook) {
this.onHook(envelope.hook);
} else {
}
});
this.reporter = options.parsedArgvOptions._reporter;
this.cid = options.parsedArgvOptions._cid;
this.specs = options.parsedArgvOptions._specs;
this.eventEmitter = options.parsedArgvOptions._eventEmitter;
this.scenarioLevelReporter = options.parsedArgvOptions._scenarioLevelReporter;
this.tagsInTitle = options.parsedArgvOptions._tagsInTitle;
this.ignoreUndefinedDefinitions = options.parsedArgvOptions._ignoreUndefinedDefinitions;
this.failAmbiguousDefinitions = options.parsedArgvOptions._failAmbiguousDefinitions;
}
updateCurrentPickle(params) {
this._currentPickle = params;
this.eventEmitter.emit("getHookParams", params);
}
normalizeURI(uri) {
return path2.isAbsolute(uri) ? uri : path2.resolve(uri);
}
emit(event, payload) {
const message = formatMessage({ payload });
message.cid = this.cid;
message.specs = this.specs;
message.uid = payload.uid;
this.reporter.emit(event, message);
}
usesSpecGrouping() {
return this._gherkinDocEvents.length > 1;
}
featureIsStarted(feature) {
return this._startedFeatures.includes(feature);
}
getTitle(featureOrScenario) {
const name = featureOrScenario.name;
const tags = featureOrScenario.tags;
if (!this.tagsInTitle || !tags || !tags.length) {
return name;
}
return `${tags.map((tag) => tag.name).join(", ")}: ${name}`;
}
afterHook(uri, feature, scenario, step, result) {
let error;
if (result.message) {
error = new Error(result.message.split("\n")[0]);
error.stack = result.message;
}
if (result.status === Status.FAILED) {
this.failedCount++;
}
const payload = buildStepPayload(uri, feature, scenario, step, {
type: "hook",
state: result.status,
error,
duration: this._testStart ? Date.now() - this._testStart.getTime() : 0
});
this.emit("hook:end", payload);
}
afterTest(uri, feature, scenario, step, result) {
let state = convertStatus(result.status);
let error = result.message ? new Error(result.message) : void 0;
let title = step ? step?.text : this.getTitle(scenario);
if (result.status === Status.UNDEFINED) {
if (this.ignoreUndefinedDefinitions) {
state = "pending";
title += " (undefined step)";
} else {
state = "fail";
this.failedCount++;
const err = new Error(
(step ? `Step "${title}" is not defined. ` : `Scenario ${title} has undefined steps. `) + "You can ignore this error by setting cucumberOpts.ignoreUndefinedDefinitions as true."
);
err.stack = `${err.message}
at Feature(${uri}):1:1
`;
const featChildren = feature.children?.find((c) => scenario.astNodeIds && c.scenario?.id === scenario.astNodeIds[0]);
if (featChildren) {
err.stack += ` at Scenario(${featChildren.scenario?.name}):${featChildren.scenario?.location?.line}:${featChildren.scenario?.location?.column}
`;
const featStep = featChildren.scenario?.steps?.find((s) => step.astNodeIds && s.id === step.astNodeIds[0]);
if (featStep) {
err.stack += ` at Step(${featStep.text}):${featStep.location?.line}:${featStep.location?.column}
`;
}
}
error = err;
}
} else if (result.status === Status.FAILED && !result.willBeRetried) {
state = "fail";
this.failedCount++;
error = new Error(result.message?.split("\n")[0]);
error.stack = result.message;
} else if (result.status === Status.AMBIGUOUS && this.failAmbiguousDefinitions) {
state = "fail";
this.failedCount++;
error = new Error(result.message?.split("\n")[0]);
error.stack = result.message;
}
const common = {
title,
state,
error,
duration: this._testStart ? Date.now() - this._testStart.getTime() : 0,
passed: ["pass", "skip"].includes(state),
file: uri
};
const payload = step ? buildStepPayload(uri, feature, scenario, step, {
type: "step",
...common
}) : {
type: "scenario",
uid: scenario.id,
parent: getFeatureId(uri, feature),
tags: scenario.tags,
...common
};
this.emit("test:" + state, payload);
}
onGherkinDocument(gherkinDocEvent) {
this.updateCurrentPickle({
uri: gherkinDocEvent.uri,
feature: gherkinDocEvent.feature
});
this._gherkinDocEvents.push(gherkinDocEvent);
}
onHook(hookEvent) {
this._hookEvent.push(hookEvent);
}
onTestRunStarted() {
if (this.usesSpecGrouping()) {
return;
}
const doc = this._gherkinDocEvents[this._gherkinDocEvents.length - 1];
this._featureStart = /* @__PURE__ */ new Date();
const payload = {
uid: getFeatureId(doc.uri, doc.feature),
title: this.getTitle(doc.feature),
type: "feature",
file: doc.uri,
tags: doc.feature?.tags,
description: doc.feature?.description,
keyword: doc.feature?.keyword
};
this.emit("suite:start", payload);
}
onPickleAccepted(pickleEvent) {
const id = this._suiteMap.size.toString();
this._suiteMap.set(pickleEvent.id, id);
this._pickleMap.set(id, pickleEvent.astNodeIds[0]);
const scenario = { ...pickleEvent, id };
this._scenarios.push(scenario);
}
onTestCasePrepared(testCase) {
this._testCases.push(testCase);
}
onTestCaseStarted(testcase) {
this._currentTestCase = testcase;
const tc = this._testCases.find((tc2) => tc2.id === testcase.testCaseId);
const scenario = this._scenarios.find(
(sc) => sc.id === this._suiteMap.get(tc?.pickleId)
);
if (!scenario) {
return;
}
const doc = this._gherkinDocEvents.find(
(gde) => gde.uri === scenario?.uri
);
const uri = doc?.uri;
const feature = doc?.feature;
if (this._currentDoc.uri && this._currentDoc.feature && this.usesSpecGrouping() && doc !== this._currentDoc && this.featureIsStarted(this._currentDoc.uri)) {
const payload2 = {
uid: getFeatureId(
this._currentDoc.uri,
this._currentDoc.feature
),
title: this.getTitle(this._currentDoc.feature),
type: "feature",
file: this._currentDoc.uri,
duration: this._featureStart ? Date.now() - this._featureStart.getTime() : 0,
tags: this._currentDoc.feature?.tags
};
this.emit("suite:end", payload2);
}
if (this.usesSpecGrouping() && doc && doc.uri && !this.featureIsStarted(doc.uri)) {
const payload2 = {
uid: getFeatureId(doc.uri, doc.feature),
title: this.getTitle(doc.feature),
type: "feature",
file: doc.uri,
tags: doc.feature?.tags,
description: doc.feature?.description,
keyword: doc.feature?.keyword
};
this.emit("suite:start", payload2);
this._currentDoc = doc;
this._startedFeatures.push(doc.uri);
}
if (scenario.steps && feature) {
scenario.steps = addKeywordToStep(
scenario.steps,
feature
);
}
this.updateCurrentPickle({ uri, feature, scenario });
const reporterScenario = scenario;
reporterScenario.rule = getRule(
doc?.feature,
this._pickleMap.get(scenario.id)
);
this._scenarioStart = /* @__PURE__ */ new Date();
this._testStart = /* @__PURE__ */ new Date();
const payload = {
uid: reporterScenario.id,
title: this.getTitle(reporterScenario),
parent: getFeatureId(scenario.uri, doc?.feature),
type: "scenario",
description: getScenarioDescription(doc?.feature, this._pickleMap.get(scenario.id)),
file: scenario.uri,
tags: reporterScenario.tags,
rule: reporterScenario.rule
};
const isRetry = typeof testcase.attempt === "number" && testcase.attempt > 0;
if (!this.scenarioLevelReporter && isRetry) {
return this.emit("suite:retry", payload);
}
this.emit(this.scenarioLevelReporter ? "test:start" : "suite:start", payload);
}
onTestStepStarted(testStepStartedEvent) {
if (!this.scenarioLevelReporter) {
const testcase = this._testCases.find(
(testcase2) => this._currentTestCase && testcase2.id === this._currentTestCase.testCaseId
);
const scenario = this._scenarios.find(
(sc) => sc.id === this._suiteMap.get(testcase?.pickleId)
);
const teststep = testcase?.testSteps?.find(
(step2) => step2.id === testStepStartedEvent.testStepId
);
const hook = this._hookEvent.find(
(h) => h.id === teststep?.hookId
);
const step = scenario?.steps?.find((s) => s.id === teststep?.pickleStepId) || { ...teststep, text: `${hook?.name || ""} ${hook?.tagExpression || ""}`.trim() };
const doc = this._gherkinDocEvents.find(
(gde) => gde.uri === scenario?.uri
);
const uri = doc?.uri;
const feature = doc?.feature;
if (!step) {
return;
}
this.updateCurrentPickle({ uri, feature, scenario, step });
this._testStart = /* @__PURE__ */ new Date();
const type = getStepType(step);
const payload = buildStepPayload(
uri,
feature,
scenario,
step,
{ type }
);
this.emit(`${type}:start`, payload);
}
}
onTestStepFinished(testStepFinishedEvent) {
if (this.scenarioLevelReporter) {
return;
}
const testcase = this._testCases.find(
(testcase2) => testcase2.id === this._currentTestCase?.testCaseId
);
const scenario = this._scenarios.find(
(sc) => sc.id === this._suiteMap.get(testcase?.pickleId)
);
const teststep = testcase?.testSteps?.find(
(step2) => step2.id === testStepFinishedEvent.testStepId
);
const step = scenario?.steps?.find((s) => s.id === teststep?.pickleStepId) || teststep;
const result = testStepFinishedEvent.testStepResult;
const doc = this._gherkinDocEvents.find(
(gde) => gde.uri === scenario?.uri
);
const uri = doc?.uri;
const feature = doc?.feature;
if (!step) {
return;
}
delete this._currentPickle;
const type = getStepType(step);
if (type === "hook") {
return this.afterHook(
uri,
feature,
scenario,
step,
result
);
}
return this.afterTest(
uri,
feature,
scenario,
step,
result
);
}
onTestCaseFinished(results) {
const tc = this._testCases.find(
(tc2) => tc2.id === this._currentTestCase?.testCaseId
);
const scenario = this._scenarios.find(
(sc) => sc.id === this._suiteMap.get(tc?.pickleId)
);
if (!scenario) {
return;
}
const finalResult = results.find((r) => r.status !== Status.PASSED) || results.pop();
const doc = this._gherkinDocEvents.find(
(gde) => gde.uri === scenario?.uri
);
const uri = doc?.uri;
const feature = doc?.feature;
this.updateCurrentPickle({ uri, feature, scenario });
const payload = {
uid: scenario.id,
title: this.getTitle(scenario),
parent: getFeatureId(doc?.uri, doc?.feature),
type: "scenario",
file: doc?.uri,
duration: this._scenarioStart ? Date.now() - this._scenarioStart.getTime() : 0,
tags: scenario.tags
};
if (this.scenarioLevelReporter) {
return this.afterTest(uri, feature, scenario, { id: scenario.id }, finalResult);
}
this.emit("suite:end", payload);
}
onTestRunFinished() {
delete this._currentTestCase;
this.eventEmitter.emit("getFailedCount", this.failedCount);
if (this.usesSpecGrouping()) {
const payload2 = {
uid: getFeatureId(
this._currentDoc.uri,
this._currentDoc.feature
),
title: this.getTitle(this._currentDoc.feature),
type: "feature",
file: this._currentDoc.uri,
duration: this._featureStart ? Date.now() - this._featureStart.getTime() : 0,
tags: this._currentDoc.feature?.tags
};
this.emit("suite:end", payload2);
return;
}
const gherkinDocEvent = this._gherkinDocEvents.pop();
if (!gherkinDocEvent) {
return;
}
const payload = {
uid: getFeatureId(
gherkinDocEvent.uri,
gherkinDocEvent.feature
),
title: this.getTitle(gherkinDocEvent.feature),
type: "feature",
file: gherkinDocEvent.uri,
duration: this._featureStart ? Date.now() - this._featureStart.getTime() : 0,
tags: gherkinDocEvent.feature?.tags
};
this.emit("suite:end", payload);
}
};
export {
CucumberFormatter as default
};