diff --git a/analyzer.js b/analyzer.js index 7aa79d2..1cc163c 100644 --- a/analyzer.js +++ b/analyzer.js @@ -7,7 +7,20 @@ const path = require('path'); let workDir; const invalidKeys = ['And', 'But']; -const getLocation = scenario => (scenario.tags.length ? scenario.tags[0].location.line - 1 : scenario.location.line - 1); +const getLocation = ({ feature, scenario, rule }) => { + const message = feature || scenario || rule; + if (message.tags && message.tags.length) { + return message.tags[0].location.line - 1; + } + return message.location.line - 1; +}; + +const getLocations = ({ feature, scenario, rule }) => { + if (feature) return feature.children.flatMap(getLocations); + if (scenario) return [ getLocation({ scenario}) ]; + if (rule) return [getLocation({ rule }), ...rule.children.flatMap(getLocations)]; + return []; +}; const getTitle = scenario => { let { name } = scenario; @@ -28,37 +41,51 @@ const getScenarioCode = (source, feature, file) => { const sourceArray = source.split('\n'); const fileName = path.relative(workDir, file); const scenarios = []; - - for (let i = 0; i < feature.children.length; i += 1) { - const { scenario } = feature.children[i]; - if (scenario) { - if (!scenario.name) { - console.log(chalk.red('Title of scenario cannot be empty, skipping this')); + const [_, ...endLocations] = [...getLocations({ feature }), sourceArray.length]; + let inRule = false; + + const handleScenario = (scenario) => { + if (!scenario.name) { + console.log(chalk.red('Title of scenario cannot be empty, skipping this')); + } else { + console.log(inRule ? ' - ' : ' - ', scenario.name); + } + const steps = []; + let previousValidStep = ''; + const scenarioJson = { name: scenario.name, file: fileName }; + const start = getLocation({ scenario }); + const end = endLocations.shift(); + for (const step of scenario.steps) { + let keyword = step.keyword.trim(); + if (invalidKeys.includes(keyword)) { + keyword = previousValidStep; } else { - console.log(' - ', scenario.name); + previousValidStep = keyword; } - const steps = []; - let previousValidStep = ''; - const scenarioJson = { name: scenario.name, file: fileName }; - const start = getLocation(scenario); - const end = ((i === feature.children.length - 1) ? sourceArray.length : getLocation(feature.children[i + 1].scenario)); - for (const step of scenario.steps) { - let keyword = step.keyword.trim(); - if (invalidKeys.includes(keyword)) { - keyword = previousValidStep; - } else { - previousValidStep = keyword; - } - steps.push({ title: step.text, keyword }); - } - scenarioJson.line = start; - scenarioJson.tags = scenario.tags.map(t => t.name.slice(1)); - scenarioJson.code = sourceArray.slice(start, end).join('\n'); - scenarioJson.steps = steps; - scenarios.push(scenarioJson); + steps.push({ title: step.text, keyword }); } + scenarioJson.line = start; + scenarioJson.tags = scenario.tags.map(t => t.name.slice(1)); + scenarioJson.code = sourceArray.slice(start, end).join('\n'); + scenarioJson.steps = steps; + scenarios.push(scenarioJson); + }; + + const handleRule = (rule) => { + console.log(' - ', rule.name); + endLocations.shift(); + inRule = true; + rule.children.forEach(handleChild); + inRule = false; + }; + + const handleChild = ({ scenario, rule }) => { + if (scenario) handleScenario(scenario); + if (rule) handleRule(rule); } + feature.children.forEach(handleChild); + return scenarios; }; @@ -89,7 +116,7 @@ const parseFile = file => new Promise((resolve, reject) => { console.log(chalk.red('Title for feature is empty, skipping')); featureData.error = `${fileName} : Empty feature`; } - featureData.line = getLocation(data[1].gherkinDocument.feature) + 1; + featureData.line = getLocation({feature: data[1].gherkinDocument.feature}) + 1; featureData.tags = data[1].gherkinDocument.feature.tags.map(t => t.name.slice(1)); featureData.scenario = getScenarioCode(data[0].source.data, data[1].gherkinDocument.feature, file); } else { diff --git a/example/features/rules.feature b/example/features/rules.feature new file mode 100644 index 0000000..5f5ccf5 --- /dev/null +++ b/example/features/rules.feature @@ -0,0 +1,74 @@ +Feature: A feature with multiple rules + Description of the feature + + Rule: Rule 1 + Description of first rule + + Scenario: Scenario 1.1 + Description of first scenario + + Given I have something + When I do something + Then something happens + + Scenario: Scenario 1.2 + Description of second scenario + + Given I have something + When I do something + Then something happens + + Scenario: Scenario 1.3 + Description of third scenario + + Given I have something + When I do something + Then something happens + + Rule: Rule 2 + Description of second rule + + Scenario: Scenario 2.1 + Description of first scenario + + Given I have something + When I do something + Then something happens + + Scenario: Scenario 2.2 + Description of second scenario + + Given I have something + When I do something + Then something happens + + Scenario: Scenario 2.3 + Description of third scenario + + Given I have something + When I do something + Then something happens + + Rule: Rule 3 + Description of thrid rule + + Scenario: Scenario 3.1 + Description of first scenario + + Given I have something + When I do something + Then something happens + + Scenario: Scenario 3.2 + Description of second scenario + + Given I have something + When I do something + Then something happens + + Scenario: Scenario 3.3 + Description of third scenario + + Given I have something + When I do something + Then something happens diff --git a/tests/analyzer_test.js b/tests/analyzer_test.js index 19a1683..4ec2021 100644 --- a/tests/analyzer_test.js +++ b/tests/analyzer_test.js @@ -51,4 +51,38 @@ describe('Analyzer', () => { const features = await analyse('**/empty.feature', path.join(__dirname, '..', 'example')); expect(features[0].error).not.equal(undefined); }); + + it('Should include scenarios from rules', async () => { + const features = await analyse('**/rules.feature', path.join(__dirname, '..', 'example')); + const scenarios = features.reduce((acc, feature) => { + acc.push(...feature.scenario); + return acc; + }, []); + const scenariosTitles = scenarios.map(scenarioData => scenarioData.name); + + expect(features.length).equal(1); + expect(scenariosTitles).to.include('Scenario 1.1'); + expect(scenariosTitles).to.include('Scenario 1.2'); + expect(scenariosTitles).to.include('Scenario 1.3'); + expect(scenariosTitles).to.include('Scenario 2.1'); + expect(scenariosTitles).to.include('Scenario 2.2'); + expect(scenariosTitles).to.include('Scenario 2.3'); + expect(scenariosTitles).to.include('Scenario 3.1'); + expect(scenariosTitles).to.include('Scenario 3.2'); + expect(scenariosTitles).to.include('Scenario 3.3'); + expect(scenarios.length).equal(9); + }); + it('Should extract the code of scenarios in rules', async () => { + const features = await analyse('**/rules.feature', path.join(__dirname, '..', 'example')); + const scenarios = features.reduce((acc, feature) => { + acc.push(...feature.scenario); + return acc; + }, []); + + for (let scenario of scenarios) { + expect(scenario.code).to.include("Scenario"); + expect(scenario.code).to.not.include("Rule"); + expect(scenario.code.split('\n').length).to.equal(7); + } + }); });