diff --git a/packages/allure-cucumberjs/README.md b/packages/allure-cucumberjs/README.md index 9bb747d04..4a82a4bc3 100644 --- a/packages/allure-cucumberjs/README.md +++ b/packages/allure-cucumberjs/README.md @@ -11,23 +11,40 @@ export default class Reporter extends CucumberJSAllureFormatter { constructor(options) { super( options, - new AllureRuntime({ resultsDir: "./out/allure-results" }), + new AllureRuntime({ resultsDir: "./allure-results" }), { labels: { - issue: [/@bug_(.*)/], - epic: [/@feature:(.*)/] + epic: [/@feature:(.*)/], + severity: [/@severity:(.*)/] + }, + links: { + issue: { + pattern: [/@issue=(.*)/], + urlTemplate: "http://localhost:8080/issue/%s" + }, + tms: { + pattern: [/@tms=(.*)/], + urlTemplate: "http://localhost:8080/tms/%s" + } } } ); } } ``` -This class MUST: -* Be a default export -* Extend `CucumberJSAllureFormatter` -* Take 1 argument in constructor and pass it to `super()` as first argument -* Second `super()` argument is `AllureRuntime` instance -* Third is a config, currently allows to map tags to Allure labels +This class **MUST**: +* Be a default export. +* Extend `CucumberJSAllureFormatter`. +* First `super()` argument is the first argument in the `constructor`. +* Second `super()` argument is an `AllureRuntime` instance. +* Third argument is a config object which allows: + * Map tags to Allure labels. + * Add links to external sites like JIRA, XRAY, etc. `%s` will be auto-replaced by the issue id. Example: +```gherkin +@issue=TEST-1 +Scenario: Example for scenario issue link check +Then the issue link should be "http://localhost:8080/issue/TEST-1" +``` Then pass with reporter as a Cucumber formatter: ``` @@ -68,3 +85,5 @@ Ilya Korobitsyn #### Contributors * Claudia Hardman +* Max Di Maria +* Daniel Montesinos diff --git a/packages/allure-cucumberjs/features/links.feature b/packages/allure-cucumberjs/features/links.feature new file mode 100644 index 000000000..65b8e125c --- /dev/null +++ b/packages/allure-cucumberjs/features/links.feature @@ -0,0 +1,50 @@ +Feature: Links + + Background: + Given a allure formatter file with config: + """ + { + links: { + issue: { + pattern: [/@issue=(.*)/], + urlTemplate: "http://localhost:8080/issue/%s" + }, + tms: { + pattern: [/@tms=(.*)/], + urlTemplate: "http://localhost:8080/tms/%s" + } + } + } + """ + + Scenario: Reporting scenario issue link + Given a feature: + """ + Feature: Feature issue links + @issue=TEST + Scenario: Example for scenario issue link check + When do passing step + Then do passing step + """ + When I run cucumber-js with allure + Then it passes + Then it has result for "Example for scenario issue link check" + + When I choose result for "Example for scenario issue link check" + Then it has link with url "http://localhost:8080/issue/TEST" with name "TEST" and with type "issue" + + Scenario: Reporting scenario tms link + Given a feature: + """ + Feature: Feature issue links + @tms=TEST + Scenario: Example for scenario tms link check + When do passing step + Then do passing step + """ + When I run cucumber-js with allure + Then it passes + Then it has result for "Example for scenario tms link check" + + When I choose result for "Example for scenario tms link check" + Then it has link with url "http://localhost:8080/tms/TEST" with name "TEST" and with type "tms" diff --git a/packages/allure-cucumberjs/features/step_definitions/report.steps.ts b/packages/allure-cucumberjs/features/step_definitions/report.steps.ts index ac1e1b04a..400486b5d 100644 --- a/packages/allure-cucumberjs/features/step_definitions/report.steps.ts +++ b/packages/allure-cucumberjs/features/step_definitions/report.steps.ts @@ -1,9 +1,7 @@ +import { StepResult, TestResult } from "allure-js-commons"; +import chai, { expect } from "chai"; import { Then, When } from "cucumber"; -import { expect } from "chai"; -import chai from "chai"; -import { TestResult } from "allure-js-commons"; import { ChaiPartial } from "../support/chai-partial"; -import { StepResult } from "allure-js-commons"; chai.use(ChaiPartial); @@ -35,3 +33,8 @@ Then(/^it has label "(.*)" with value "(.*)"$/, function(name: string, value: st Then(/^it has description "(.*)"$/, function(description: string) { expect(this.ctx).partial({ description }); }); + +Then(/^it has link with url "(.*)" with name "(.*)" and with type "(.*)"$/, + function(url: string, name: string, type: string) { + expect(this.ctx).partial({ links: [{ url, name, type }] }); + }); diff --git a/packages/allure-cucumberjs/src/CucumberAllureInterface.ts b/packages/allure-cucumberjs/src/CucumberAllureInterface.ts index 06374cba9..739c37b8a 100644 --- a/packages/allure-cucumberjs/src/CucumberAllureInterface.ts +++ b/packages/allure-cucumberjs/src/CucumberAllureInterface.ts @@ -79,6 +79,14 @@ export class CucumberAllureInterface extends Allure { addLabel(name: string, value: string): void { this.currentTest.addLabel(name, value); } + + addIssueLink(url: string, name: string) { + this.currentTest.addIssueLink(url, name); + } + + addTmsLink(url: string, name: string) { + this.currentTest.addTmsLink(url, name); + } } export class WrappedStep { diff --git a/packages/allure-cucumberjs/src/CucumberJSAllureReporter.ts b/packages/allure-cucumberjs/src/CucumberJSAllureReporter.ts index a4eeb9af1..3fe7099c8 100644 --- a/packages/allure-cucumberjs/src/CucumberJSAllureReporter.ts +++ b/packages/allure-cucumberjs/src/CucumberJSAllureReporter.ts @@ -1,7 +1,6 @@ -import { Formatter, World as CucumberWorld } from "cucumber"; import { - AllureGroup, Allure, + AllureGroup, AllureRuntime, AllureStep, AllureTest, @@ -10,14 +9,15 @@ import { ExecutableItemWrapper, LabelName } from "allure-js-commons"; -import { Result } from "./events/Result"; -import { SourceLocation } from "./events/SourceLocation"; +import { Formatter, World as CucumberWorld } from "cucumber"; +import { CucumberAllureInterface } from "./CucumberAllureInterface"; +import { examplesToSensibleFormat } from "./events/Example"; +import { GherkinDocument } from "./events/GherkinDocument"; import { GherkinStep } from "./events/GherkinStep"; import { GherkinTestCase } from "./events/GherkinTestCase"; -import { GherkinDocument } from "./events/GherkinDocument"; -import { examplesToSensibleFormat } from "./events/Example"; +import { Result } from "./events/Result"; +import { SourceLocation } from "./events/SourceLocation"; import { TestHookDefinition } from "./events/TestHookDefinition"; -import { CucumberAllureInterface } from "./CucumberAllureInterface"; import { applyExample, hash, @@ -33,31 +33,46 @@ export interface World extends CucumberWorld { } export class CucumberJSAllureFormatterConfig { - labels?: { - [key: string]: RegExp[]; - }; exceptionFormatter?: (message: string) => string; + labels?: { [key: string]: RegExp[]; }; + links?: { + issue?: { + pattern: RegExp[]; + urlTemplate: string; + }, + tms?: { + pattern: RegExp[]; + urlTemplate: string; + } + }; } export class CucumberJSAllureFormatter extends Formatter { - private readonly sourceMap: Map = new Map(); - private readonly stepsMap: Map = new Map(); + public readonly allureInterface: Allure; + currentAfter: ExecutableItemWrapper | null = null; + currentBefore: ExecutableItemWrapper | null = null; + currentGroup: AllureGroup | null = null; + currentTest: AllureTest | null = null; + private readonly afterHooks: TestHookDefinition[]; + private readonly beforeHooks: TestHookDefinition[]; + private readonly exceptionFormatter: (message: string) => string; private readonly featureMap: Map = new Map(); private readonly labels: { [key: string]: RegExp[]; }; - private readonly exceptionFormatter: (message: string) => string; - private readonly beforeHooks: TestHookDefinition[]; - private readonly afterHooks: TestHookDefinition[]; - + private readonly links: { + issue?: { + pattern: RegExp[]; + urlTemplate: string; + }, + tms?: { + pattern: RegExp[]; + urlTemplate: string; + } + }; + private readonly sourceMap: Map = new Map(); private stepStack: AllureStep[] = []; - currentGroup: AllureGroup | null = null; - currentTest: AllureTest | null = null; - currentBefore: ExecutableItemWrapper | null = null; - currentAfter: ExecutableItemWrapper | null = null; - - public readonly allureInterface: Allure; + private readonly stepsMap: Map = new Map(); - constructor(options: any, private readonly allureRuntime: AllureRuntime, - config: CucumberJSAllureFormatterConfig) { + constructor(options: any, private readonly allureRuntime: AllureRuntime, config: CucumberJSAllureFormatterConfig) { super(options); options.eventBroadcaster .on("source", this.onSource.bind(this)) @@ -70,6 +85,7 @@ export class CucumberJSAllureFormatter extends Formatter { .on("test-case-finished", this.onTestCaseFinished.bind(this)); this.labels = config.labels || {}; + this.links = config.links || {}; this.exceptionFormatter = function(message) { if (config.exceptionFormatter !== undefined) { try { @@ -87,8 +103,11 @@ export class CucumberJSAllureFormatter extends Formatter { this.afterHooks = options.supportCodeLibrary.afterTestCaseHookDefinitions; } - onSource(data: { uri: string, data: string, media: { encoding: string, type: string } }) { - this.sourceMap.set(data.uri, data.data.split(/\n/)); + get currentStep(): AllureStep | null { + if (this.stepStack.length > 0) { + return this.stepStack[this.stepStack.length - 1]; + } + return null; } onGherkinDocument(data: { uri: string, document: GherkinDocument }) { @@ -124,6 +143,22 @@ export class CucumberJSAllureFormatter extends Formatter { this.featureMap.set(data.uri, data.document); } + onSource(data: { uri: string, data: string, media: { encoding: string, type: string } }) { + this.sourceMap.set(data.uri, data.data.split(/\n/)); + } + + onTestCaseFinished(data: { result: Result } & SourceLocation) { + if (this.currentTest === null || this.currentGroup === null) { + throw new Error("No current test info"); + } + this.currentTest.status = statusTextToAllure(data.result.status); + this.currentTest.stage = statusTextToStage(data.result.status); + this.setException(this.currentTest, data.result.exception); + + this.currentTest.endTest(); + this.currentGroup.endGroup(); + } + onTestCasePrepared(data: { steps: SourceLocation[] } & SourceLocation) { this.stepsMap.clear(); this.stepsMap.set(SourceLocation.toKey(data), data.steps); @@ -150,13 +185,15 @@ export class CucumberJSAllureFormatter extends Formatter { uri: data.sourceLocation!.uri, f: feature.feature.name, t: test.name, - a: null + a: null }; if (test.example !== undefined) { info.a = test.example.arguments; for (const prop in test.example.arguments) { - if (!test.example.arguments[prop]) continue; + if (!test.example.arguments[prop]) { + continue; + } this.currentTest.addParameter(prop, test.example.arguments[prop]); } } @@ -172,7 +209,9 @@ export class CucumberJSAllureFormatter extends Formatter { this.currentTest.addLabel(LabelName.TAG, tag.name); for (const label in this.labels) { - if (!this.labels[label]) continue; + if (!this.labels[label]) { + continue; + } for (const reg of this.labels[label]) { const match = tag.name.match(reg); if (match != null && match.length > 1) { @@ -180,59 +219,125 @@ export class CucumberJSAllureFormatter extends Formatter { } } } + + if (this.links.issue) { + for (const reg of this.links.issue.pattern) { + const match = tag.name.match(reg); + if (match != null && match.length > 1) { + this.currentTest.addIssueLink(this.links.issue.urlTemplate.replace("%s", match[1]), match[1]); + } + } + } + + if (this.links.tms) { + for (const reg of this.links.tms.pattern) { + const match = tag.name.match(reg); + if (match != null && match.length > 1) { + this.currentTest.addTmsLink(this.links.tms.urlTemplate.replace("%s", match[1]), match[1]); + } + } + } } } + onTestStepAttachment( + data: { index: number, data: string, media: { type: string }, testCase: SourceLocation }) { + if (this.currentStep === null) { + throw new Error("There is no step to add attachment to"); + } + const type: ContentType = data.media.type; + let content: string | Buffer = data.data; + if ([ContentType.JPEG, ContentType.PNG, ContentType.WEBM].indexOf(type) >= 0) { + content = Buffer.from(content, "base64"); + } + const file = this.allureRuntime.writeAttachment(content, { contentType: type }); + this.currentStep.addAttachment("attached", type, file); + } + + onTestStepFinished(data: { index: number, result: Result, testCase: SourceLocation }) { + const currentStep = this.currentStep; // eslint-disable-line prefer-destructuring + if (currentStep === null) { + throw new Error("No current step defined"); + } + currentStep.status = statusTextToAllure(data.result.status); + currentStep.stage = statusTextToStage(data.result.status); + this.setException(currentStep, data.result.exception); + currentStep.endStep(); + this.popStep(); + } + onTestStepStarted(data: { index: number, testCase: SourceLocation }) { const location = (this.stepsMap.get(SourceLocation.toKey(data.testCase)) || [])[data.index]; const feature = this.featureMap.get(data.testCase.sourceLocation!.uri); - if (feature === undefined) throw new Error("Unknown feature"); + if (feature === undefined) { + throw new Error("Unknown feature"); + } const test = feature.caseMap === undefined ? undefined : feature.caseMap.get(data.testCase.sourceLocation!.line); - if (test === undefined) throw new Error("Unknown scenario"); + if (test === undefined) { + throw new Error("Unknown scenario"); + } let step: GherkinStep | undefined; if (location.sourceLocation !== undefined && feature.stepMap !== undefined) { step = test.stepMap.get(location.sourceLocation.line) || feature.stepMap.get(location.sourceLocation.line); } else { - if (location.actionLocation === undefined) location.actionLocation = { - uri: "unknown", - line: -1 - }; + if (location.actionLocation === undefined) { + location.actionLocation = { + uri: "unknown", + line: -1 + }; + } step = { location: { line: -1 }, text: `${location.actionLocation.uri}:${location.actionLocation.line}`, keyword: "" }; } - if (step === undefined) throw new Error("Unknown step"); + if (step === undefined) { + throw new Error("Unknown step"); + } let stepText = applyExample(`${step.keyword}${step.text}`, test.example); const isAfter = this.afterHooks.find(({ uri, line }) => { - if (location.actionLocation === undefined) return false; + if (location.actionLocation === undefined) { + return false; + } return uri === location.actionLocation.uri && line === location.actionLocation.line; }); const isBefore = this.beforeHooks.find(({ uri, line }) => { - if (location.actionLocation === undefined) return false; + if (location.actionLocation === undefined) { + return false; + } return uri === location.actionLocation.uri && line === location.actionLocation.line; }); if (step.isBackground) { - if (this.currentBefore === null) this.currentBefore = this.currentGroup!.addBefore(); + if (this.currentBefore === null) { + this.currentBefore = this.currentGroup!.addBefore(); + } } else if (isBefore) { - if (this.currentBefore === null) this.currentBefore = this.currentGroup!.addBefore(); + if (this.currentBefore === null) { + this.currentBefore = this.currentGroup!.addBefore(); + } stepText = `Before: ${isBefore.code!.name || step.text}`; } else if (isAfter) { - if (this.currentAfter === null) this.currentAfter = this.currentGroup!.addAfter(); + if (this.currentAfter === null) { + this.currentAfter = this.currentGroup!.addAfter(); + } stepText = `After: ${isAfter.code!.name || step.text}`; } else { - if (this.currentBefore !== null) this.currentBefore = null; - if (this.currentAfter !== null) this.currentAfter = null; + if (this.currentBefore !== null) { + this.currentBefore = null; + } + if (this.currentAfter !== null) { + this.currentAfter = null; + } } const allureStep = (this.currentAfter || this.currentBefore || this.currentTest)!.startStep(stepText); @@ -241,7 +346,7 @@ export class CucumberJSAllureFormatter extends Formatter { if (step.argument !== undefined) { if (step.argument.content !== undefined) { const file = this.allureRuntime.writeAttachment(step.argument.content, { - contentType: ContentType.TEXT, + contentType: ContentType.TEXT }); allureStep.addAttachment("Text", ContentType.TEXT, file); } @@ -259,38 +364,16 @@ export class CucumberJSAllureFormatter extends Formatter { } } - onTestStepAttachment( - data: { index: number, data: string, media: { type: string }, testCase: SourceLocation }) { - if (this.currentStep === null) throw new Error("There is no step to add attachment to"); - const type: ContentType = data.media.type; - let content: string | Buffer = data.data; - if ([ContentType.JPEG, ContentType.PNG, ContentType.WEBM].indexOf(type) >= 0) { - content = Buffer.from(content, "base64"); - } - const file = this.allureRuntime.writeAttachment(content, { contentType: type }); - this.currentStep.addAttachment("attached", type, file); + popStep(): void { + this.stepStack.pop(); } - onTestStepFinished(data: { index: number, result: Result, testCase: SourceLocation }) { - const currentStep = this.currentStep; // eslint-disable-line prefer-destructuring - if (currentStep === null) throw new Error("No current step defined"); - currentStep.status = statusTextToAllure(data.result.status); - currentStep.stage = statusTextToStage(data.result.status); - this.setException(currentStep, data.result.exception); - currentStep.endStep(); - this.popStep(); + pushStep(step: AllureStep): void { + this.stepStack.push(step); } - onTestCaseFinished(data: { result: Result } & SourceLocation) { - if (this.currentTest === null || this.currentGroup === null) { - throw new Error("No current test info"); - } - this.currentTest.status = statusTextToAllure(data.result.status); - this.currentTest.stage = statusTextToStage(data.result.status); - this.setException(this.currentTest, data.result.exception); - - this.currentTest.endTest(); - this.currentGroup.endGroup(); + writeAttachment(content: Buffer | string, options: ContentType | string | AttachmentOptions): string { + return this.allureRuntime.writeAttachment(content, options); } private setException(target: ExecutableItemWrapper, exception?: Error | string) { @@ -304,23 +387,6 @@ export class CucumberJSAllureFormatter extends Formatter { } } } - - pushStep(step: AllureStep): void { - this.stepStack.push(step); - } - - popStep(): void { - this.stepStack.pop(); - } - - get currentStep(): AllureStep | null { - if (this.stepStack.length > 0) return this.stepStack[this.stepStack.length - 1]; - return null; - } - - writeAttachment(content: Buffer | string, options: ContentType | string | AttachmentOptions): string { - return this.allureRuntime.writeAttachment(content, options); - } } diff --git a/packages/allure-js-commons/src/AllureTest.ts b/packages/allure-js-commons/src/AllureTest.ts index afdd58e82..939714d15 100644 --- a/packages/allure-js-commons/src/AllureTest.ts +++ b/packages/allure-js-commons/src/AllureTest.ts @@ -1,7 +1,7 @@ -import { ExecutableItemWrapper } from "./ExecutableItemWrapper"; -import { TestResult } from "./model"; -import { testResult } from "./constructors"; import { AllureRuntime } from "./AllureRuntime"; +import { testResult } from "./constructors"; +import { ExecutableItemWrapper } from "./ExecutableItemWrapper"; +import { LinkType, TestResult } from "./model"; export class AllureTest extends ExecutableItemWrapper { private readonly testResult: TestResult; @@ -37,4 +37,12 @@ export class AllureTest extends ExecutableItemWrapper { addLink(url: string, name?: string, type?: string): void { this.testResult.links.push({ name, url, type }); } + + addIssueLink(url: string, name: string) { + this.addLink(url, name, LinkType.ISSUE); + } + + addTmsLink(url: string, name: string) { + this.addLink(url, name, LinkType.TMS); + } }