UNPKG

@wdio/cucumber-framework

Version:
549 lines (546 loc) 18.4 kB
// 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 };