diff --git a/src/models/reportOptions.ts b/src/models/reportOptions.ts index 7b13e3a..5f63f23 100644 --- a/src/models/reportOptions.ts +++ b/src/models/reportOptions.ts @@ -13,6 +13,13 @@ export interface IReportOptions { /** Path to Cucumber file in JSON format */ jsonFile: string; + /** + * Comma separated tags + * @example + * tags: '@includeThis, @includeThis, ~@ignoreThis' + */ + tags?: string; + /** * Path to the custom template to use * @description A custom html template can be supplied. Templates use https://handlebarsjs.com/ diff --git a/src/reporter.spec.ts b/src/reporter.spec.ts index f194c0a..478e940 100644 --- a/src/reporter.spec.ts +++ b/src/reporter.spec.ts @@ -1,11 +1,11 @@ import { expect } from 'chai'; import * as fs from 'fs'; -import { } from 'mocha'; -import { IReportOptions } from './models/reportOptions'; -import { Reporter } from './reporter'; import { FeatureSummary } from './models/aggregator/featureSummary'; import { ScenarioSummary } from './models/aggregator/scenarioSummary'; +import { ICucumberFeatureSuite } from './models/reporter/cucumberFeatureSuite'; +import { IReportOptions } from './models/reportOptions'; +import { Reporter } from './reporter'; describe('reporter', () => { let reporter: Reporter; @@ -55,7 +55,7 @@ describe('reporter', () => { }); it('should update options with required defaults if the user does not supply them', () => { - const options = {}; + const options = {}; const actual = reporter.populateDefaultOptionsIfMissing(options); @@ -64,7 +64,7 @@ describe('reporter', () => { }); it('populateDefaultOptionsIfMissing should not overwrite existing values', () => { - const options = { + const options = { htmlTemplate: 'templatePath', jsonFile: 'somepath' }; @@ -166,11 +166,134 @@ describe('reporter', () => { const html = fs.readFileSync(options.output, 'utf8'); // tslint:disable-next-line: max-line-length - const expectedText = /Ability: Login<\/span>([.\n\s]*)([.\n\s]*)clear<\/i>1/; + const expectedText = /Ability: Login<\/span>([.\n\s]*)([.\n\s]*)clear<\/i>1/; expect(html.search(expectedText)).to.greaterThan(0); // Clean up test // Comment this out if you want to view the generated html fs.unlinkSync(options.output); }); + + it('should filter to includedTags', () => { + const rawFeatureSuite: ICucumberFeatureSuite = { + features: [ + { + description: 'Sample Feature Description', + keyword: 'Ability', + name: 'Login', + line: 1, + id: 'login', + tags: [{ + line: 1, + name: '@includeMe', + }], + uri: 'e2e\\src\\features\\abilities\\user\\login.feature', + elements: [ + { + id: 'login;login-via-login-page', + keyword: 'Scenario', + line: 11, + name: 'Login via login page', + tags: [{ + line: 1, + name: '@includeMe', + }], + type: 'scenario', + steps: [ + { + keyword: 'Before', + hidden: true, + match: { + location: 'e2e\\src\\steps\\searchForUser.steps.ts:10' + }, + result: { + status: 'passed', + duration: 1 + } + } + ] + }, + { + id: 'login;login-via-login-page', + keyword: 'Scenario', + line: 11, + name: 'Login via login page', + tags: [{ + line: 1, + name: '@dontIncludeMe', + }], + type: 'scenario', + steps: [ + { + keyword: 'Before', + hidden: true, + match: { + location: 'e2e\\src\\steps\\searchForUser.steps.ts:10' + }, + result: { + status: 'passed', + duration: 1 + } + } + ] + } + ] + }, + { + description: 'Sample Not going to be included', + keyword: 'Ability', + name: 'Login', + line: 1, + id: 'login', + tags: [ + { + line: 1, + name: '@dontIncludeMe', + } + ], + uri: 'e2e\\src\\features\\abilities\\user\\login.feature', + elements: [ + { + id: 'login;login-via-login-page', + keyword: 'Scenario', + line: 11, + name: 'Login via login page', + tags: [], + type: 'scenario', + steps: [ + { + keyword: 'Before', + hidden: true, + match: { + location: 'e2e\\src\\steps\\searchForUser.steps.ts:10' + }, + result: { + status: 'passed', + duration: 1 + } + } + ] + } + ] + } + ] + }; + + // Should filter to supplied tags + let filteredResults = reporter.filterResults(rawFeatureSuite, { tags: '@includeMe' } as IReportOptions); + expect(filteredResults.features.length).to.eq(1, 'Should only include features with matching tag'); + + // Should allow only ecluded tags to be supplied + filteredResults = reporter.filterResults(rawFeatureSuite, { tags: '~@whatever' } as IReportOptions); + expect(filteredResults.features.length).to.eq(2, 'Accept tags that only include exclusions'); + + // Should not filter if no tags supplied + filteredResults = reporter.filterResults(rawFeatureSuite, { } as IReportOptions); + expect(filteredResults.features.length).to.eq(2, 'Should include all features'); + + // Should strip out scenarios with excluded tags + filteredResults = reporter.filterResults(rawFeatureSuite, { tags: '~@dontIncludeMe' } as IReportOptions); + expect(rawFeatureSuite.features[0].elements.length).to.eq(2, 'Should start with two scenarios'); + expect(filteredResults.features[0].elements.length).to.eq(1, 'Should strip out scenarios with excluded tags'); + }); }); diff --git a/src/reporter.ts b/src/reporter.ts index c95fc3e..eae554d 100644 --- a/src/reporter.ts +++ b/src/reporter.ts @@ -27,7 +27,10 @@ export class Reporter { public generate(options: IReportOptions): void { options = this.populateDefaultOptionsIfMissing(options); - const results = this.parseJsonFile(options.jsonFile); + const rawResults = this.parseJsonFile(options.jsonFile); + + // if a filter has been passed in, filter out the results + const results = this.filterResults(rawResults, options); const aggregator = new ReportAggregator(); @@ -85,7 +88,7 @@ export class Reporter { this.getScenarioCss(scenarioSummary)); Handlebars.registerHelper('markdown2Html', (markdown: string) => - marked(markdown || '') + marked(markdown && markdown.trim() || '') ); Handlebars.registerHelper('getStepCss', (step: IStep) => { @@ -133,6 +136,38 @@ export class Reporter { } } + /** + * Filters the features and scenario's based on tags + */ + public filterResults(featureSuiteOrig: ICucumberFeatureSuite, options: IReportOptions): ICucumberFeatureSuite { + // Don't modify the original suite + const featureSuite = JSON.parse(JSON.stringify(featureSuiteOrig)) as ICucumberFeatureSuite; + + if (options.tags && options.tags.length) { + const tags: string[] = (options.tags || '').split(',').map(t => t.trim()); + const includeTags = tags.filter(t => t.startsWith('@')); + const excludeTags = tags.filter(t => t.startsWith('~')).map(t => t.substring(1)); // Drop the tilde + + let filteredFeatures = featureSuite.features.filter(f => { + const x = (!includeTags.length || f.tags.some(t => includeTags.includes(t.name))) ; + const y = (!excludeTags.length || !f.tags.some(t => excludeTags.includes(t.name))); + return x && y; + }); + + filteredFeatures.forEach(f => f.elements = f.elements.filter(el => { + const x = (!includeTags.length || el.tags.some(t => includeTags.includes(t.name))); + const y = (!excludeTags.length || !el.tags.some(t => excludeTags.includes(t.name))); + return x && y; + })); + + filteredFeatures = filteredFeatures.filter(f => f.elements.length); + + return { features: filteredFeatures }; + } + + return featureSuite; + } + /** * Parses a JSON String and returns a strongly typed data model * reflecting the Cucumber Test Report data structure