From bf23ac9f8b2749d50732adf9ddbe375caf02d55b Mon Sep 17 00:00:00 2001 From: David Goss Date: Wed, 18 Mar 2020 23:46:04 +0000 Subject: [PATCH] format: add support for gherkin rule/example syntax (#1273) * add basic feature for gherkin rule/example support * use require instead of import * assert on formatter output for passing example * add formatters.feature scenario for rejected pickle from rule * add formatters.feature scenario for passed from rule * add editorconfig for indentation control * formatting * make `getGherkinStepMap` rule-aware * restructure tests a bit * refactor tests a bit more, add some to cover getGherkinScenarioMap * account for Rule in getGherkinScenarioMap * rework getGherkinScenarioLocationMap to handle Rule * add unit test for Rule/Example in json formatter * report keyword as Scenario or Example correctly from scenario map * include rule name in concatenated scenario id for json formatter * make sure we work with a background within a rule * readability * add background usage to rule feature, assert on failure output * add acceptance test for message and json format on failure from rule * add a bit of coverage for the progress bar formatter * more on progress far - numbers/time reporting at end * work rule usage into progress formatter spec * work rule usage into rerun formatter spec * work rule usage into summary formatter spec * add coverage for pickle filtering on name * changelog update * rework progress formatter spec to have a seperate case for rule/example * remove negative test for rule/example in json formatter * rework gherkin document parser spec to avoid replication of structure * fix some lint * fix line numbers in formatter unit tests * fix dodgy import * fix overqualified references to messages interfaces * remove unnecessary tags * for progress bar formatter, break out new test for rule/example, retain scenario one * split out new test for rule/example in rerun formatter spec * remove superfluous formatter tests * fix whitespace * restore original names where qualifiers no longer needed --- .editorconfig | 9 + CHANGELOG.md | 4 + features/background.feature | 32 ++ .../fixtures/formatters/passed-rule.json.ts | 37 ++ .../formatters/passed-rule.message.json.ts | 143 +++++++ ...passed.json.ts => passed-scenario.json.ts} | 0 ...son.ts => passed-scenario.message.json.ts} | 0 features/formatters.feature | 24 +- features/rule.feature | 137 +++++++ .../helpers/gherkin_document_parser.ts | 75 +++- .../helpers/gherkin_document_parser_spec.ts | 376 ++++++++++++++++++ src/formatter/json_formatter.ts | 35 +- src/formatter/json_formatter_spec.ts | 112 ++++++ src/formatter/progress_bar_formatter_spec.ts | 55 ++- src/formatter/progress_formatter_spec.ts | 39 ++ src/formatter/rerun_formatter_spec.ts | 25 ++ src/formatter/summary_formatter_spec.ts | 33 ++ src/pickle_filter_spec.ts | 24 +- test/gherkin_helpers.ts | 5 +- 19 files changed, 1139 insertions(+), 26 deletions(-) create mode 100644 .editorconfig create mode 100644 features/fixtures/formatters/passed-rule.json.ts create mode 100644 features/fixtures/formatters/passed-rule.message.json.ts rename features/fixtures/formatters/{passed.json.ts => passed-scenario.json.ts} (100%) rename features/fixtures/formatters/{passed.message.json.ts => passed-scenario.message.json.ts} (100%) create mode 100644 features/rule.feature create mode 100644 src/formatter/helpers/gherkin_document_parser_spec.ts diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..3374b7879 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +# 4 space indentation +[*] +indent_style = space +indent_size = 2 diff --git a/CHANGELOG.md b/CHANGELOG.md index 29b4fa2b2..53636a122 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ Please see [CONTRIBUTING.md](https://github.com/cucumber/cucumber/blob/master/CO ### [Unreleased](https://github.com/cucumber/cucumber-js/compare/v6.0.5...master) (In Git) +#### New Features + +* Add support for Gherkin's [Rule/Example syntax](https://cucumber.io/docs/gherkin/reference/#rule) + #### Breaking changes * Drop support for Node.js 8 diff --git a/features/background.feature b/features/background.feature index f4a353b94..4f0e5da8b 100644 --- a/features/background.feature +++ b/features/background.feature @@ -46,3 +46,35 @@ Feature: Background | IDENTIFIER | | Given a background step | | When another scenario step | + + Scenario: Two examples within a rule with a background, plus a feature-level background + Given a file named "features/background.feature" with: + """ + Feature: a feature + Background: + Given a feature-level background step + + Rule: a rule + Background: + Given a rule-level background step + + Example: first example + When an example step + + Example: second example + When an example step + When another example step + """ + When I run cucumber-js + Then it fails + And the scenario "first example" has the steps: + | IDENTIFIER | + | Given a feature-level background step | + | Given a rule-level background step | + | When an example step | + And the scenario "second example" has the steps: + | IDENTIFIER | + | Given a feature-level background step | + | Given a rule-level background step | + | When an example step | + | When another example step | diff --git a/features/fixtures/formatters/passed-rule.json.ts b/features/fixtures/formatters/passed-rule.json.ts new file mode 100644 index 000000000..53bf451bc --- /dev/null +++ b/features/fixtures/formatters/passed-rule.json.ts @@ -0,0 +1,37 @@ +module.exports = [ + { + description: '', + elements: [ + { + description: '', + id: 'a-feature;a-rule;an-example', + keyword: 'Example', + line: 3, + name: 'an example', + steps: [ + { + arguments: [], + keyword: 'Given ', + line: 4, + match: { + location: 'features/step_definitions/steps.js:3', + }, + name: 'a step', + result: { + duration: 0, + status: 'passed', + }, + }, + ], + tags: [], + type: 'scenario', + }, + ], + id: 'a-feature', + keyword: 'Feature', + line: 1, + name: 'a feature', + tags: [], + uri: 'features/a.feature', + }, +] diff --git a/features/fixtures/formatters/passed-rule.message.json.ts b/features/fixtures/formatters/passed-rule.message.json.ts new file mode 100644 index 000000000..a209be852 --- /dev/null +++ b/features/fixtures/formatters/passed-rule.message.json.ts @@ -0,0 +1,143 @@ +module.exports = [ + { + source: { + uri: 'features/a.feature', + data: + 'Feature: a feature\n Rule: a rule\n Example: an example\n Given a step', + media: { + encoding: 'UTF8', + contentType: 'text/x.cucumber.gherkin+plain', + }, + }, + }, + { + gherkinDocument: { + uri: 'features/a.feature', + feature: { + location: { + line: 1, + column: 1, + }, + language: 'en', + keyword: 'Feature', + name: 'a feature', + children: [ + { + rule: { + location: { + line: 2, + column: 3, + }, + keyword: 'Rule', + name: 'a rule', + children: [ + { + scenario: { + location: { + line: 3, + column: 5, + }, + keyword: 'Example', + name: 'an example', + steps: [ + { + location: { + line: 4, + column: 7, + }, + keyword: 'Given ', + text: 'a step', + id: '1', + }, + ], + id: '2', + }, + }, + ], + }, + }, + ], + }, + }, + }, + { + pickle: { + id: '4', + uri: 'features/a.feature', + name: 'an example', + language: 'en', + steps: [ + { + text: 'a step', + id: '3', + astNodeIds: ['1'], + }, + ], + astNodeIds: ['2'], + }, + }, + { + pickleAccepted: { + pickleId: '4', + }, + }, + { + testRunStarted: {}, + }, + { + testCase: { + id: '5', + pickleId: '4', + testSteps: [ + { + id: '6', + pickleStepId: '3', + stepDefinitionIds: ['0'], + }, + ], + }, + }, + { + testCaseStarted: { + attempt: 0, + testCaseId: '5', + id: '7', + }, + }, + { + testStepStarted: { + testStepId: '6', + testCaseStartedId: '7', + }, + }, + { + testStepFinished: { + testResult: { + status: 'PASSED', + duration: { + seconds: '0', + nanos: 0, + }, + }, + testStepId: '6', + testCaseStartedId: '7', + }, + }, + { + testCaseFinished: { + testResult: { + status: 'PASSED', + duration: { + seconds: '0', + nanos: 0, + }, + }, + testCaseStartedId: '7', + }, + }, + { + testRunFinished: { + success: true, + }, + }, +] diff --git a/features/fixtures/formatters/passed.json.ts b/features/fixtures/formatters/passed-scenario.json.ts similarity index 100% rename from features/fixtures/formatters/passed.json.ts rename to features/fixtures/formatters/passed-scenario.json.ts diff --git a/features/fixtures/formatters/passed.message.json.ts b/features/fixtures/formatters/passed-scenario.message.json.ts similarity index 100% rename from features/fixtures/formatters/passed.message.json.ts rename to features/fixtures/formatters/passed-scenario.message.json.ts diff --git a/features/formatters.feature b/features/formatters.feature index 332ed6fd9..fbb3fe1b4 100644 --- a/features/formatters.feature +++ b/features/formatters.feature @@ -11,7 +11,7 @@ Feature: Formatters Then the "message" formatter output matches the fixture "formatters/rejected-pickle.message.json" Then the "json" formatter output matches the fixture "formatters/rejected-pickle.json" - Scenario: passed + Scenario: passed from Scenario Given a file named "features/a.feature" with: """ Feature: a feature @@ -25,8 +25,26 @@ Feature: Formatters Given(/^a step$/, function() {}) """ When I run cucumber-js with all formatters - Then the "message" formatter output matches the fixture "formatters/passed.message.json" - Then the "json" formatter output matches the fixture "formatters/passed.json" + Then the "message" formatter output matches the fixture "formatters/passed-scenario.message.json" + Then the "json" formatter output matches the fixture "formatters/passed-scenario.json" + + Scenario: passed from Rule + Given a file named "features/a.feature" with: + """ + Feature: a feature + Rule: a rule + Example: an example + Given a step + """ + Given a file named "features/step_definitions/steps.js" with: + """ + const {Given} = require('cucumber') + + Given(/^a step$/, function() {}) + """ + When I run cucumber-js with all formatters + Then the "message" formatter output matches the fixture "formatters/passed-rule.message.json" + Then the "json" formatter output matches the fixture "formatters/passed-rule.json" Scenario: failed Given a file named "features/a.feature" with: diff --git a/features/rule.feature b/features/rule.feature new file mode 100644 index 000000000..9af7bb866 --- /dev/null +++ b/features/rule.feature @@ -0,0 +1,137 @@ +Feature: Rule keyword + + Scenario: Rule with background and multiple examples, passing + Given a file named "features/highlander.feature" with: + """ + Feature: Highlander + + Rule: There can be only One + Background: + Given there are 3 ninjas + + Example: Only One -- More than one alive + Given there are more than one ninja alive + When 2 ninjas meet, they will fight + Then one ninja dies + And there is one ninja less alive + + Example: Only One -- One alive + Given there is only 1 ninja alive + Then they will live forever + """ + And a file named "features/step_definitions/cucumber_steps.js" with: + """ + const {Given, When, Then} = require('cucumber') + + Given('there are {int} ninjas', function(count) { + this.total = count + }) + + Given('there is only 1 ninja alive', function() { + this.living = 1 + }) + + Given('there are more than one ninja alive', function() { + this.living = 2 + }) + + When('2 ninjas meet, they will fight', function() { + this.deaths = 1 + this.living = 1 + }) + + Then('one ninja dies', function() { + + }) + + Then('there is one ninja less alive', function() { + + }) + + Then('they will live forever', function() { + + }) + """ + When I run cucumber-js + Then it passes + And it outputs the text: + """ + ........ + + 2 scenarios (2 passed) + 8 steps (8 passed) + + """ + + Scenario: Rule with background and multiple examples, failing + Given a file named "features/highlander.feature" with: + """ + Feature: Highlander + + Rule: There can be only One + Background: + Given there are 3 ninjas + + Example: Only One -- More than one alive + Given there are more than one ninja alive + When 2 ninjas meet, they will fight + Then one ninja dies + And there is one ninja less alive + + Example: Only One -- One alive + Given there is only 1 ninja alive + Then they will live forever + """ + And a file named "features/step_definitions/cucumber_steps.js" with: + """ + const {Given, When, Then} = require('cucumber') + + Given('there are {int} ninjas', function(count) { + this.total = count + }) + + Given('there is only 1 ninja alive', function() { + this.living = 1 + }) + + Given('there are more than one ninja alive', function() { + this.living = 2 + }) + + When('2 ninjas meet, they will fight', function() { + this.deaths = 1 + this.living = 1 + }) + + Then('one ninja dies', function() { + throw 'fail' + }) + + Then('there is one ninja less alive', function() { + + }) + + Then('they will live forever', function() { + + }) + """ + When I run cucumber-js + Then it fails + And it outputs the text: + """ + ...F-... + + Failures: + + 1) Scenario: Only One -- More than one alive # features/highlander.feature:7 + ✔ Given there are 3 ninjas # features/step_definitions/cucumber_steps.js:3 + ✔ Given there are more than one ninja alive # features/step_definitions/cucumber_steps.js:11 + ✔ When 2 ninjas meet, they will fight # features/step_definitions/cucumber_steps.js:15 + ✖ Then one ninja dies # features/step_definitions/cucumber_steps.js:20 + fail + - And there is one ninja less alive # features/step_definitions/cucumber_steps.js:24 + + 2 scenarios (1 failed, 1 passed) + 8 steps (1 failed, 1 skipped, 6 passed) + + """ diff --git a/src/formatter/helpers/gherkin_document_parser.ts b/src/formatter/helpers/gherkin_document_parser.ts index c5fcffe23..7a6986799 100644 --- a/src/formatter/helpers/gherkin_document_parser.ts +++ b/src/formatter/helpers/gherkin_document_parser.ts @@ -6,9 +6,8 @@ export function getGherkinStepMap( gherkinDocument: messages.IGherkinDocument ): Dictionary { return _.chain(gherkinDocument.feature.children) - .map((child: messages.GherkinDocument.Feature.IFeatureChild) => - doesHaveValue(child.background) ? child.background : child.scenario - ) + .map(extractStepContainers) + .flatten() .map('steps') .flatten() .map((step: messages.GherkinDocument.Feature.IStep) => [step.id, step]) @@ -16,10 +15,35 @@ export function getGherkinStepMap( .value() } +function extractStepContainers( + child: messages.GherkinDocument.Feature.IFeatureChild +): Array< + | messages.GherkinDocument.Feature.IScenario + | messages.GherkinDocument.Feature.IBackground +> { + if (doesHaveValue(child.background)) { + return [child.background] + } else if (doesHaveValue(child.rule)) { + return child.rule.children.map(ruleChild => + doesHaveValue(ruleChild.background) + ? ruleChild.background + : ruleChild.scenario + ) + } + return [child.scenario] +} + export function getGherkinScenarioMap( gherkinDocument: messages.IGherkinDocument ): Dictionary { return _.chain(gherkinDocument.feature.children) + .map((child: messages.GherkinDocument.Feature.IFeatureChild) => { + if (doesHaveValue(child.rule)) { + return child.rule.children + } + return [child] + }) + .flatten() .filter('scenario') .map('scenario') .map((scenario: messages.GherkinDocument.Feature.IScenario) => [ @@ -30,21 +54,42 @@ export function getGherkinScenarioMap( .value() } +export function getGherkinExampleRuleMap( + gherkinDocument: messages.IGherkinDocument +): Dictionary { + return _.chain(gherkinDocument.feature.children) + .filter('rule') + .map('rule') + .map(rule => { + return rule.children + .filter(child => doesHaveValue(child.scenario)) + .map(child => [child.scenario.id, rule]) + }) + .flatten() + .fromPairs() + .value() +} + export function getGherkinScenarioLocationMap( gherkinDocument: messages.IGherkinDocument ): Dictionary { - const map: Dictionary = {} - for (const child of gherkinDocument.feature.children) { - if (doesHaveValue(child.scenario)) { - map[child.scenario.id] = child.scenario.location - if (doesHaveValue(child.scenario.examples)) { - for (const examples of child.scenario.examples) { - for (const tableRow of examples.tableBody) { - map[tableRow.id] = tableRow.location - } - } + const locationMap: Dictionary = {} + const scenarioMap: Dictionary = getGherkinScenarioMap( + gherkinDocument + ) + _.entries(scenarioMap).forEach( + ([id, scenario]) => { + locationMap[id] = scenario.location + if (doesHaveValue(scenario.examples)) { + _.chain(scenario.examples) + .map('tableBody') + .flatten() + .forEach(tableRow => { + locationMap[tableRow.id] = tableRow.location + }) + .value() } } - } - return map + ) + return locationMap } diff --git a/src/formatter/helpers/gherkin_document_parser_spec.ts b/src/formatter/helpers/gherkin_document_parser_spec.ts new file mode 100644 index 000000000..303df8ab3 --- /dev/null +++ b/src/formatter/helpers/gherkin_document_parser_spec.ts @@ -0,0 +1,376 @@ +import { describe, it } from 'mocha' +import { expect } from 'chai' +import { + getGherkinExampleRuleMap, + getGherkinScenarioLocationMap, + getGherkinScenarioMap, + getGherkinStepMap, +} from './gherkin_document_parser' +import { + IParsedSourceWithEnvelopes, + parse, +} from '../../../test/gherkin_helpers' +import { messages } from 'cucumber-messages' +import IGherkinDocument = messages.IGherkinDocument + +describe('GherkinDocumentParser', () => { + describe('getGherkinStepMap', () => { + it('works for a Background and Scenario', async () => { + // Arrange + const gherkinDocument = await withBackgroundAndScenario() + + // Act + const output = getGherkinStepMap(gherkinDocument) + + // Assert + const backgroundStep = + gherkinDocument.feature.children[0].background.steps[0] + const scenarioStep = gherkinDocument.feature.children[1].scenario.steps[0] + expect(output).to.eql({ + [backgroundStep.id]: backgroundStep, + [scenarioStep.id]: scenarioStep, + }) + }) + + it('works for a Background and Scenario Outline', async () => { + // Arrange + const gherkinDocument = await withBackgroundAndScenarioOutline() + + // Act + const output = getGherkinStepMap(gherkinDocument) + + // Assert + const backgroundStep = + gherkinDocument.feature.children[0].background.steps[0] + const outlineStep = gherkinDocument.feature.children[1].scenario.steps[0] + expect(output).to.eql({ + [backgroundStep.id]: backgroundStep, + [outlineStep.id]: outlineStep, + }) + }) + + it('works for a Background and Rule with Examples', async () => { + // Arrange + const gherkinDocument = await withBackgroundAndRuleWithExamples() + + // Act + const output = getGherkinStepMap(gherkinDocument) + + // Assert + const backgroundStep = + gherkinDocument.feature.children[0].background.steps[0] + const example1When = + gherkinDocument.feature.children[1].rule.children[0].scenario.steps[0] + const example1Then = + gherkinDocument.feature.children[1].rule.children[0].scenario.steps[1] + const example2When = + gherkinDocument.feature.children[1].rule.children[1].scenario.steps[0] + const example2Then = + gherkinDocument.feature.children[1].rule.children[1].scenario.steps[1] + expect(output).to.eql({ + [backgroundStep.id]: backgroundStep, + [example1When.id]: example1When, + [example1Then.id]: example1Then, + [example2When.id]: example2When, + [example2Then.id]: example2Then, + }) + }) + + it('works for a Background and Rule with its own Background and Examples', async () => { + // Arrange + const gherkinDocument = await withBackgroundAndRuleWithBackgroundAndExamples() + + // Act + const output = getGherkinStepMap(gherkinDocument) + + // Assert + const featureBackgroundStep = + gherkinDocument.feature.children[0].background.steps[0] + const ruleBackgroundStep = + gherkinDocument.feature.children[1].rule.children[0].background.steps[0] + const example1When = + gherkinDocument.feature.children[1].rule.children[1].scenario.steps[0] + const example1Then = + gherkinDocument.feature.children[1].rule.children[1].scenario.steps[1] + const example2When = + gherkinDocument.feature.children[1].rule.children[2].scenario.steps[0] + const example2Then = + gherkinDocument.feature.children[1].rule.children[2].scenario.steps[1] + expect(output).to.eql({ + [featureBackgroundStep.id]: featureBackgroundStep, + [ruleBackgroundStep.id]: ruleBackgroundStep, + [example1When.id]: example1When, + [example1Then.id]: example1Then, + [example2When.id]: example2When, + [example2Then.id]: example2Then, + }) + }) + }) + + describe('getGherkinScenarioMap', () => { + it('works for a Background and Scenario', async () => { + // Arrange + const gherkinDocument = await withBackgroundAndScenario() + + // Act + const output = getGherkinScenarioMap(gherkinDocument) + + // Assert + const scenario = gherkinDocument.feature.children[1].scenario + expect(output).to.eql({ + [scenario.id]: scenario, + }) + }) + + it('works for a Background and Scenario Outline', async () => { + // Arrange + const gherkinDocument = await withBackgroundAndScenarioOutline() + + // Act + const output = getGherkinScenarioMap(gherkinDocument) + + // Assert + const scenario = gherkinDocument.feature.children[1].scenario + expect(output).to.eql({ + [scenario.id]: scenario, + }) + }) + + it('works for a Background and Rule with Examples', async () => { + // Arrange + const gherkinDocument = await withBackgroundAndRuleWithExamples() + + // Act + const output = getGherkinScenarioMap(gherkinDocument) + + // Assert + const example1 = + gherkinDocument.feature.children[1].rule.children[0].scenario + const example2 = + gherkinDocument.feature.children[1].rule.children[1].scenario + expect(output).to.eql({ + [example1.id]: example1, + [example2.id]: example2, + }) + }) + + it('works for a Background and Rule with its own Background and Examples', async () => { + // Arrange + const gherkinDocument = await withBackgroundAndRuleWithBackgroundAndExamples() + + // Act + const output = getGherkinScenarioMap(gherkinDocument) + + // Assert + const example1 = + gherkinDocument.feature.children[1].rule.children[1].scenario + const example2 = + gherkinDocument.feature.children[1].rule.children[2].scenario + expect(output).to.eql({ + [example1.id]: example1, + [example2.id]: example2, + }) + }) + }) + + describe('getGherkinExampleRuleMap', () => { + it('works for a Background and Scenario', async () => { + // Arrange + const gherkinDocument = await withBackgroundAndScenario() + + // Act + const output = await getGherkinExampleRuleMap(gherkinDocument) + + // Assert + expect(output).to.eql({}) + }) + + it('works for a Background and Scenario Outline', async () => { + // Arrange + const gherkinDocument = await withBackgroundAndScenarioOutline() + + // Act + const output = await getGherkinExampleRuleMap(gherkinDocument) + + // Assert + expect(output).to.eql({}) + }) + + it('works for a Background and Rule with Examples', async () => { + // Arrange + const gherkinDocument = await withBackgroundAndRuleWithExamples() + + // Act + const output = await getGherkinExampleRuleMap(gherkinDocument) + + // Assert + const rule = gherkinDocument.feature.children[1].rule + const example1 = rule.children[0].scenario + const example2 = rule.children[1].scenario + expect(output).to.eql({ + [example1.id]: rule, + [example2.id]: rule, + }) + }) + + it('works for a Background and Rule with its own Background and Examples', async () => { + // Arrange + const gherkinDocument = await withBackgroundAndRuleWithBackgroundAndExamples() + + // Act + const output = await getGherkinExampleRuleMap(gherkinDocument) + + // Assert + const rule = gherkinDocument.feature.children[1].rule + const example1 = rule.children[1].scenario + const example2 = rule.children[2].scenario + expect(output).to.eql({ + [example1.id]: rule, + [example2.id]: rule, + }) + }) + }) + + describe('getGherkinScenarioLocationMap', () => { + it('works for a Background and Scenario', async () => { + // Arrange + const gherkinDocument = await withBackgroundAndScenario() + + // Act + const output = await getGherkinScenarioLocationMap(gherkinDocument) + + // Assert + const scenario = gherkinDocument.feature.children[1].scenario + expect(output).to.eql({ + [scenario.id]: scenario.location, + }) + }) + + it('works for a Background and Scenario Outline', async () => { + // Arrange + const gherkinDocument = await withBackgroundAndScenarioOutline() + + // Act + const output = await getGherkinScenarioLocationMap(gherkinDocument) + + // Assert + const scenario = gherkinDocument.feature.children[1].scenario + const row1 = scenario.examples[0].tableBody[0] + const row2 = scenario.examples[0].tableBody[1] + expect(output).to.eql({ + [scenario.id]: scenario.location, + [row1.id]: row1.location, + [row2.id]: row2.location, + }) + }) + + it('works for a Background and Rule with Examples', async () => { + // Arrange + const gherkinDocument = await withBackgroundAndRuleWithExamples() + + // Act + const output = await getGherkinScenarioLocationMap(gherkinDocument) + + // Assert + const example1 = + gherkinDocument.feature.children[1].rule.children[0].scenario + const example2 = + gherkinDocument.feature.children[1].rule.children[1].scenario + expect(output).to.eql({ + [example1.id]: example1.location, + [example2.id]: example2.location, + }) + }) + + it('works for a Background and Rule with its own Background and Examples', async () => { + // Arrange + const gherkinDocument = await withBackgroundAndRuleWithBackgroundAndExamples() + + // Act + const output = await getGherkinScenarioLocationMap(gherkinDocument) + + // Assert + const example1 = + gherkinDocument.feature.children[1].rule.children[1].scenario + const example2 = + gherkinDocument.feature.children[1].rule.children[2].scenario + expect(output).to.eql({ + [example1.id]: example1.location, + [example2.id]: example2.location, + }) + }) + }) +}) + +async function parseGherkinDocument(data: string): Promise { + const parsed: IParsedSourceWithEnvelopes = await parse({ + data, + uri: 'features/a.feature', + }) + return parsed.gherkinDocument +} + +async function withBackgroundAndScenario(): Promise { + return parseGherkinDocument(`\ +Feature: a feature + Background: + Given a setup step + + Scenario: + When a regular step +`) +} + +async function withBackgroundAndScenarioOutline(): Promise { + return parseGherkinDocument(`\ +Feature: a feature + Background: + Given a setup step + + Scenario Outline: + When a templated step with + Examples: + | word | + | foo | + | bar | +`) +} + +async function withBackgroundAndRuleWithExamples(): Promise { + return parseGherkinDocument(`\ +Feature: a feature + Background: + Given a setup step + + Rule: a rule + Example: an example + When a regular step + Then an assertion + + Example: another example + When a regular step + Then an assertion +`) +} + +async function withBackgroundAndRuleWithBackgroundAndExamples(): Promise< + IGherkinDocument +> { + return parseGherkinDocument(`\ +Feature: a feature + Background: + Given a feature-level setup step + + Rule: a rule + Background: + Given a rule-level setup step + + Example: an example + When a regular step + Then an assertion + + Example: another example + When a regular step + Then an assertion +`) +} diff --git a/src/formatter/json_formatter.ts b/src/formatter/json_formatter.ts index 79a50da7c..44fa4138f 100644 --- a/src/formatter/json_formatter.ts +++ b/src/formatter/json_formatter.ts @@ -5,7 +5,10 @@ import { formatLocation, GherkinDocumentParser, PickleParser } from './helpers' import { durationToNanoseconds } from '../time' import path from 'path' import { messages } from 'cucumber-messages' -import { getGherkinScenarioLocationMap } from './helpers/gherkin_document_parser' +import { + getGherkinExampleRuleMap, + getGherkinScenarioLocationMap, +} from './helpers/gherkin_document_parser' import { ITestCaseAttempt } from './helpers/event_data_collector' import { doesHaveValue, doesNotHaveValue } from '../value_checker' import { parseStepArgument } from '../step_arguments' @@ -14,6 +17,7 @@ import IFeature = messages.GherkinDocument.IFeature import IPickle = messages.IPickle import IScenario = messages.GherkinDocument.Feature.IScenario import IEnvelope = messages.IEnvelope +import IRule = messages.GherkinDocument.Feature.FeatureChild.IRule const { getGherkinStepMap, getGherkinScenarioMap } = GherkinDocumentParser @@ -69,7 +73,8 @@ interface IBuildJsonFeatureOptions { interface IBuildJsonScenarioOptions { feature: messages.GherkinDocument.IFeature - gherkinScenarioMap: Dictionary + gherkinScenarioMap: Dictionary + gherkinExampleRuleMap: Dictionary gherkinScenarioLocationMap: Dictionary pickle: messages.IPickle steps: IJsonStep[] @@ -151,6 +156,7 @@ export default class JsonFormatter extends Formatter { const { gherkinDocument } = group[0] const gherkinStepMap = getGherkinStepMap(gherkinDocument) const gherkinScenarioMap = getGherkinScenarioMap(gherkinDocument) + const gherkinExampleRuleMap = getGherkinExampleRuleMap(gherkinDocument) const gherkinScenarioLocationMap = getGherkinScenarioLocationMap( gherkinDocument ) @@ -172,6 +178,7 @@ export default class JsonFormatter extends Formatter { return this.getScenarioData({ feature: gherkinDocument.feature, gherkinScenarioLocationMap, + gherkinExampleRuleMap, gherkinScenarioMap, pickle, steps, @@ -206,6 +213,7 @@ export default class JsonFormatter extends Formatter { getScenarioData({ feature, gherkinScenarioLocationMap, + gherkinExampleRuleMap, gherkinScenarioMap, pickle, steps, @@ -216,8 +224,8 @@ export default class JsonFormatter extends Formatter { }) return { description, - id: `${this.convertNameToId(feature)};${this.convertNameToId(pickle)}`, - keyword: 'Scenario', + id: this.formatScenarioId({ feature, pickle, gherkinExampleRuleMap }), + keyword: gherkinScenarioMap[pickle.astNodeIds[0]].keyword, line: gherkinScenarioLocationMap[pickle.astNodeIds[0]].line, name: pickle.name, steps, @@ -226,6 +234,25 @@ export default class JsonFormatter extends Formatter { } } + private formatScenarioId({ + feature, + pickle, + gherkinExampleRuleMap, + }: { + feature: IFeature + pickle: IPickle + gherkinExampleRuleMap: Dictionary + }): string { + let parts: any[] + const rule = gherkinExampleRuleMap[pickle.astNodeIds[0]] + if (doesHaveValue(rule)) { + parts = [feature, rule, pickle] + } else { + parts = [feature, pickle] + } + return parts.map(part => this.convertNameToId(part)).join(';') + } + getStepData({ isBeforeHook, gherkinStepMap, diff --git a/src/formatter/json_formatter_spec.ts b/src/formatter/json_formatter_spec.ts index 241fd3cac..5e4d5346e 100644 --- a/src/formatter/json_formatter_spec.ts +++ b/src/formatter/json_formatter_spec.ts @@ -331,4 +331,116 @@ describe('JsonFormatter', () => { }) }) }) + + describe('one rule with several examples (scenarios)', () => { + describe('passed', () => { + it('outputs the feature', async () => { + // Arrange + const sources = [ + { + data: [ + '@tag1 @tag2', + 'Feature: my feature', + ' my feature description', + '', + ' Rule: my rule', + ' my rule description', + '', + ' Example: first example', + ' first example description', + '', + ' Given a passing step', + '', + ' Example: second example', + ' second example description', + '', + ' Given a passing step', + ].join('\n'), + uri: 'a.feature', + }, + ] + + const supportCodeLibrary = getJsonFormatterSupportCodeLibrary(clock) + + // Act + const output = await testFormatter({ + sources, + supportCodeLibrary, + type: 'json', + }) + + // Assert + expect(JSON.parse(output)).to.eql([ + { + description: ' my feature description', + elements: [ + { + description: ' first example description', + id: 'my-feature;my-rule;first-example', + keyword: 'Example', + line: 8, + name: 'first example', + type: 'scenario', + steps: [ + { + arguments: [], + line: 11, + match: { + location: 'json_formatter_steps.ts:11', + }, + keyword: 'Given ', + name: 'a passing step', + result: { + status: 'passed', + duration: 1000000, + }, + }, + ], + tags: [ + { name: '@tag1', line: 1 }, + { name: '@tag2', line: 1 }, + ], + }, + { + description: ' second example description', + id: 'my-feature;my-rule;second-example', + keyword: 'Example', + line: 13, + name: 'second example', + type: 'scenario', + steps: [ + { + arguments: [], + line: 16, + match: { + location: 'json_formatter_steps.ts:11', + }, + keyword: 'Given ', + name: 'a passing step', + result: { + status: 'passed', + duration: 1000000, + }, + }, + ], + tags: [ + { name: '@tag1', line: 1 }, + { name: '@tag2', line: 1 }, + ], + }, + ], + id: 'my-feature', + keyword: 'Feature', + line: 2, + name: 'my feature', + tags: [ + { name: '@tag1', line: 1 }, + { name: '@tag2', line: 1 }, + ], + uri: 'a.feature', + }, + ]) + }) + }) + }) }) diff --git a/src/formatter/progress_bar_formatter_spec.ts b/src/formatter/progress_bar_formatter_spec.ts index 8ce8c1f9f..066417df5 100644 --- a/src/formatter/progress_bar_formatter_spec.ts +++ b/src/formatter/progress_bar_formatter_spec.ts @@ -85,7 +85,7 @@ async function testProgressBarFormatter({ describe('ProgressBarFormatter', () => { describe('pickleAccepted / testStepStarted', () => { - it('initializes a progress bar with the total number of steps', async () => { + it('initializes a progress bar with the total number of steps for a scenario', async () => { // Arrange const sources = [ { @@ -108,6 +108,30 @@ describe('ProgressBarFormatter', () => { // Assert expect(progressBarFormatter.progressBar.total).to.eql(5) }) + + it('initializes a progress bar with the total number of steps for a rule', async () => { + // Arrange + const sources = [ + { + data: 'Feature: a\nRule: b\nExample: c\nGiven a step\nThen a step', + uri: 'a.feature', + }, + { + data: + 'Feature: a\nRule: b\nExample: c\nGiven a step\nWhen a step\nThen a step', + uri: 'b.feature', + }, + ] + + // Act + const { progressBarFormatter } = await testProgressBarFormatter({ + shouldStopFn: envelope => doesHaveValue(envelope.testStepStarted), + sources, + }) + + // Assert + expect(progressBarFormatter.progressBar.total).to.eql(5) + }) }) describe('testStepFinished', () => { @@ -349,7 +373,7 @@ describe('ProgressBarFormatter', () => { clock.uninstall() }) - it('outputs step totals, scenario totals, and duration', async () => { + it('outputs step totals, scenario totals, and duration - singular', async () => { // Arrange const sources = [ { @@ -371,5 +395,32 @@ describe('ProgressBarFormatter', () => { '1 scenario (1 passed)\n' + '1 step (1 passed)\n' + '0m00.000s\n' ) }) + + it('outputs step totals, scenario totals, and duration - plural', async () => { + // Arrange + const sources = [ + { + data: 'Feature: a\nScenario: b\nGiven a passing step', + uri: 'a.feature', + }, + { + data: 'Feature: a\nRule: b\nExample: c\nGiven a passing step', + uri: 'b.feature', + }, + ] + const supportCodeLibrary = getBaseSupportCodeLibrary() + + // Act + const { output } = await testProgressBarFormatter({ + shouldStopFn: envelope => false, + sources, + supportCodeLibrary, + }) + + // Assert + expect(output).to.contain( + '2 scenarios (2 passed)\n' + '2 steps (2 passed)\n' + '0m00.000s\n' + ) + }) }) }) diff --git a/src/formatter/progress_formatter_spec.ts b/src/formatter/progress_formatter_spec.ts index 153fb7860..6b0a9615d 100644 --- a/src/formatter/progress_formatter_spec.ts +++ b/src/formatter/progress_formatter_spec.ts @@ -83,6 +83,45 @@ Warnings: 6 scenarios (1 failed, 1 ambiguous, 1 undefined, 1 pending, 1 skipped, 1 passed) 6 steps (1 failed, 1 ambiguous, 1 undefined, 1 pending, 1 skipped, 1 passed) 0m00.000s +`) + }) + + it('handles rule/example results', async () => { + // Arrange + const sources = [ + { + data: `\ +Feature: feature + Rule: rule1 + Example: example1 + Given a passing step + + Example: example2 + Given a passing step + + Rule: rule2 + Example: example1 + Given a passing step +`, + uri: 'a.feature', + }, + ] + const supportCodeLibrary = getBaseSupportCodeLibrary() + + // Act + const output = await testFormatter({ + sources, + supportCodeLibrary, + type: 'progress', + }) + + // Assert + expect(output).to.eql(`\ +... + +3 scenarios (3 passed) +3 steps (3 passed) +0m00.000s `) }) }) diff --git a/src/formatter/rerun_formatter_spec.ts b/src/formatter/rerun_formatter_spec.ts index dd6bbc248..8fbedcf21 100644 --- a/src/formatter/rerun_formatter_spec.ts +++ b/src/formatter/rerun_formatter_spec.ts @@ -195,6 +195,31 @@ describe('RerunFormatter', () => { // Assert expect(output).to.eql(`a.feature:2${separator.expected}b.feature:3`) }) + + it('outputs the reference needed to run the rule example again', async () => { + // Arrange + const parsedArgvOptions = { rerun: { separator: separator.opt } } + const sources = [ + { + data: 'Feature: a\nRule: b\nExample: c\nGiven a step', + uri: 'a.feature', + }, + { + data: 'Feature: a\n\nRule: b\nExample: c\nGiven a step', + uri: 'b.feature', + }, + ] + + // Act + const output = await testFormatter({ + parsedArgvOptions, + sources, + type: 'rerun', + }) + + // Assert + expect(output).to.eql(`a.feature:3${separator.expected}b.feature:4`) + }) }) } ) diff --git a/src/formatter/summary_formatter_spec.ts b/src/formatter/summary_formatter_spec.ts index d2a61f05d..1ce7965a2 100644 --- a/src/formatter/summary_formatter_spec.ts +++ b/src/formatter/summary_formatter_spec.ts @@ -51,6 +51,39 @@ describe('SummaryFormatter', () => { }) }) + describe('with a failing rule -> example', () => { + it('logs the issue', async () => { + // Arrange + const sources = [ + { + data: 'Feature: a\nRule: b\nExample: c\nGiven a failing step', + uri: 'a.feature', + }, + ] + const supportCodeLibrary = getBaseSupportCodeLibrary() + + // Act + const output = await testFormatter({ + sources, + supportCodeLibrary, + type: 'summary', + }) + + // Assert + expect(output).to.eql( + 'Failures:\n' + + '\n' + + '1) Scenario: c # a.feature:3\n' + + ` ${figures.cross} Given a failing step # steps.ts:9\n` + + ' error\n' + + '\n' + + '1 scenario (1 failed)\n' + + '1 step (1 failed)\n' + + '0m00.000s\n' + ) + }) + }) + describe('with an ambiguous step', () => { it('logs the issue', async () => { // Arrange diff --git a/src/pickle_filter_spec.ts b/src/pickle_filter_spec.ts index 8b1091ec9..8a44d2e2e 100644 --- a/src/pickle_filter_spec.ts +++ b/src/pickle_filter_spec.ts @@ -114,7 +114,7 @@ describe('PickleFilter', () => { }) }) - it('returns true if pickle name matches', async function() { + it('returns true if pickle name matches from scenario', async function() { // Arrange const { pickles: [pickle], @@ -135,6 +135,28 @@ describe('PickleFilter', () => { expect(result).to.eql(true) }) + it('returns true if pickle name matches from rule -> example', async function() { + // Arrange + const { + pickles: [pickle], + gherkinDocument, + } = await parse({ + data: [ + 'Feature: a', + 'Rule: nameR descriptionR', + 'Example: nameA descriptionA', + 'Given a step', + ].join('\n'), + uri: path.resolve(cwd, 'features/a.feature'), + }) + + // Act + const result = pickleFilter.matches({ pickle, gherkinDocument }) + + // Assert + expect(result).to.eql(true) + }) + it('returns false if pickle name does not match', async function() { // Arrange const { diff --git a/test/gherkin_helpers.ts b/test/gherkin_helpers.ts index 327c42a43..81e12c18a 100644 --- a/test/gherkin_helpers.ts +++ b/test/gherkin_helpers.ts @@ -1,6 +1,7 @@ import Gherkin from 'gherkin' import { messages } from 'cucumber-messages' import { doesHaveValue } from '../src/value_checker' +import IGherkinOptions from 'gherkin/src/IGherkinOptions' import { EventEmitter } from 'events' export interface IParsedSource { @@ -16,11 +17,13 @@ export interface IParsedSourceWithEnvelopes extends IParsedSource { export interface IParseRequest { data: string uri: string + options?: IGherkinOptions } export async function parse({ data, uri, + options, }: IParseRequest): Promise { const sources = [ { @@ -39,7 +42,7 @@ export async function parse({ let gherkinDocument: messages.IGherkinDocument const pickles: messages.IPickle[] = [] const envelopes: messages.IEnvelope[] = [] - const messageStream = Gherkin.fromSources(sources) + const messageStream = Gherkin.fromSources(sources, options) messageStream.on('data', (envelope: messages.IEnvelope) => { envelopes.push(envelope) if (doesHaveValue(envelope.source)) {