diff --git a/analyzer.js b/analyzer.js index 7aa79d2..bbd8b4a 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 = (message) => { + if (message.tags && message.tags.length) { + return message.tags[0].location.line - 1; + } + return message.location.line - 1; +}; + +const getLocations = ({ feature, scenario, rule, background }) => { + if (feature) return [getLocation(feature), ...feature.children.flatMap(getLocations) ]; + if (scenario) return [ getLocation(scenario) ]; + if (rule) return [getLocation(rule), ...rule.children.flatMap(getLocations)]; + if (background) return [ getLocation(background) ]; + return []; +}; const getTitle = scenario => { let { name } = scenario; @@ -24,45 +37,85 @@ const getTitle = scenario => { return name; }; -const getScenarioCode = (source, feature, file) => { +const getScenarioCode = (source, feature, file, { + includeFeatureCode, + includeRuleCode, + includeBackgroundCode, +}) => { 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 [featureStart, ...startLocations] = getLocations({ feature }); + const [featureEnd, ...endLocations] = [...startLocations, sourceArray.length]; + const context = includeFeatureCode ? sourceArray.slice(featureStart, featureEnd) : []; + + 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 = startLocations.shift(); + 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 = context.concat(sourceArray.slice(start, end)).join('\n'); + scenarioJson.steps = steps; + scenarios.push(scenarioJson); + }; + + const handleRule = (rule) => { + console.log(' - ', rule.name); + const oldContextLength = context.length; + const start = startLocations.shift(); + const end = endLocations.shift(); + + if (includeRuleCode) { + context.push(...sourceArray.slice(start, end)); + } + + inRule = true; + rule.children.forEach(handleChild); + inRule = false; + + context.splice(oldContextLength); + }; + + const handleBackground = (background) => { + const start = startLocations.shift(); + const end = endLocations.shift(); + if (includeBackgroundCode) { + context.push(...sourceArray.slice(start, end)); } + }; + + const handleChild = ({ scenario, rule, background }) => { + if (scenario) handleScenario(scenario); + if (rule) handleRule(rule); + if (background) handleBackground(background); } + feature.children.forEach(handleChild); + return scenarios; }; -const parseFile = file => new Promise((resolve, reject) => { +const parseFile = (file, scenarioCodeOptions) => new Promise((resolve, reject) => { try { const options = { includeSource: true, @@ -91,7 +144,7 @@ const parseFile = file => new Promise((resolve, reject) => { } featureData.line = getLocation(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); + featureData.scenario = getScenarioCode(data[0].source.data, data[1].gherkinDocument.feature, file, scenarioCodeOptions); } else { featureData.error = `${fileName} : ${data[1].attachment.data}`; console.log(chalk.red(`Wrong format, So skipping this: ${data[1].attachment.data}`)); @@ -109,8 +162,12 @@ const parseFile = file => new Promise((resolve, reject) => { * * @param {String} filePattern * @param {String} dir + * @param {Object} scenarioCodeOptions + * @param {boolean} scenarioCodeOptions.includeFeatureCode + * @param {boolean} scenarioCodeOptions.includeRuleCode + * @param {boolean} scenarioCodeOptions.includeBackgroundCode */ -const analyzeFeatureFiles = (filePattern, dir = '.') => { +const analyzeFeatureFiles = (filePattern, dir = '.', scenarioCodeOptions = {}) => { workDir = dir; console.log('\n 🗄️ Parsing files\n'); @@ -120,7 +177,7 @@ const analyzeFeatureFiles = (filePattern, dir = '.') => { const promiseArray = []; glob(pattern, (er, files) => { for (const file of files) { - const data = parseFile(file); + const data = parseFile(file, scenarioCodeOptions); promiseArray.push(data); } diff --git a/bin/check.js b/bin/check.js index 0d49af5..c4b5ed4 100755 --- a/bin/check.js +++ b/bin/check.js @@ -32,10 +32,17 @@ program .option('--create', 'Create tests and suites for missing IDs') .option('--no-empty', 'Remove empty suites after import') .option('--keep-structure', 'Prefer structure of source code over structure in Testomat.io') - .option('--no-detached', 'Don\t mark all unmatched tests as detached') + .option('--no-detached', 'Don\'t mark all unmatched tests as detached') + .option('--include-features', 'Add description of feature to scenario code') + .option('--include-rules', 'Add description of parent rule sections to scenario code') + .option('--include-backgrounds', 'Add description and steps of relevant background sections to scenario code') .action(async (filesArg, opts) => { const isPattern = checkPattern(filesArg); - const features = await analyze(filesArg || '**/*.feature', opts.dir || process.cwd()); + const features = await analyze(filesArg || '**/*.feature', opts.dir || process.cwd(), { + includeFeatureCode: opts.includeFeatures, + includeRuleCode: opts.includeRules, + includeBackgroundCode: opts.includeBackgrounds, + }); if (opts.cleanIds || opts.unsafeCleanIds) { let idMap = {}; if (apiKey) { diff --git a/example/features/rules.feature b/example/features/rules.feature new file mode 100644 index 0000000..ec0bf74 --- /dev/null +++ b/example/features/rules.feature @@ -0,0 +1,86 @@ +Feature: A feature with multiple rules + Description of the feature + + Background: + Given all rules have something + + Rule: Rule 1 + Description of first rule + + Background: + Given the first rule has something + + 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 + + Background: + Given the second rule has something + + 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 third rule + + Background: + Given the third rule has something + + 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..5f971b3 100644 --- a/tests/analyzer_test.js +++ b/tests/analyzer_test.js @@ -51,4 +51,135 @@ 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('Does not include the feature, rule and background description by default', async () => { + const features = await analyse('**/rules.feature', path.join(__dirname, '..', 'example'), { + includeFeatureCode: false, + includeRuleCode: false, + includeBackgroundCode: false, + }); + const scenarios = features.reduce((acc, feature) => { + acc.push(...feature.scenario); + return acc; + }, []); + + expect(features.length).equal(1); + for (let scenario of scenarios) { + const code = scenario.code.split('\n').map(line => line.trim()); + expect(code).to.not.include('Description of the feature'); + expect(code).to.not.include('Description of first rule'); + expect(code).to.not.include('Given all rules have something'); + } + expect(scenarios.length).equal(9); + }); + + it('Can include the features description', async () => { + const features = await analyse('**/rules.feature', path.join(__dirname, '..', 'example'), { + includeFeatureCode: true, + includeRuleCode: false, + includeBackgroundCode: false, + }); + const scenarios = features.reduce((acc, feature) => { + acc.push(...feature.scenario); + return acc; + }, []); + + expect(features.length).equal(1); + for (let scenario of scenarios) { + const code = scenario.code.split('\n').map(line => line.trim()); + expect(code).to.include('Description of the feature'); + } + expect(scenarios.length).equal(9); + }); + + it('Can include the parent rule description', async () => { + const features = await analyse('**/rules.feature', path.join(__dirname, '..', 'example'), { + includeFeatureCode: false, + includeRuleCode: true, + includeBackgroundCode: false, + }); + const scenarios = features.reduce((acc, feature) => { + acc.push(...feature.scenario); + return acc; + }, []); + + expect(features.length).equal(1); + for (let scenario of scenarios.slice(0, 3)) { + const code = scenario.code.split('\n').map(line => line.trim()); + expect(code).to.include('Description of first rule'); + expect(code).to.not.include('Description of second rule'); + expect(code).to.not.include('Description of third rule'); + } + for (let scenario of scenarios.slice(3, 6)) { + const code = scenario.code.split('\n').map(line => line.trim()); + expect(code).to.not.include('Description of first rule'); + expect(code).to.include('Description of second rule'); + expect(code).to.not.include('Description of third rule'); + } + for (let scenario of scenarios.slice(6, 9)) { + const code = scenario.code.split('\n').map(line => line.trim()); + expect(code).to.not.include('Description of first rule'); + expect(code).to.not.include('Description of second rule'); + expect(code).to.include('Description of third rule'); + } + expect(scenarios.length).equal(9); + }); + + it('Can include background steps', async () => { + const features = await analyse('**/rules.feature', path.join(__dirname, '..', 'example'), { + includeFeatureCode: false, + includeRuleCode: false, + includeBackgroundCode: true, + }); + const scenarios = features.reduce((acc, feature) => { + acc.push(...feature.scenario); + return acc; + }, []); + + expect(features.length).equal(1); + for (let scenario of scenarios) { + const code = scenario.code.split('\n').map(line => line.trim()); + expect(code).to.include('Given all rules have something'); + } + for (let scenario of scenarios.slice(0, 3)) { + const code = scenario.code.split('\n').map(line => line.trim()); + expect(code).to.include('Given the first rule has something'); + expect(code).to.not.include('Given the second rule has something'); + expect(code).to.not.include('Given the third rule has something'); + } + for (let scenario of scenarios.slice(3, 6)) { + const code = scenario.code.split('\n').map(line => line.trim()); + expect(code).to.not.include('Given the first rule has something'); + expect(code).to.include('Given the second rule has something'); + expect(code).to.not.include('Given the third rule has something'); + } + for (let scenario of scenarios.slice(6, 9)) { + const code = scenario.code.split('\n').map(line => line.trim()); + expect(code).to.not.include('Given the first rule has something'); + expect(code).to.not.include('Given the second rule has something'); + expect(code).to.include('Given the third rule has something'); + } + expect(scenarios.length).equal(9); + }); });