From 36e7d514a74f1e587806e2b1f3e3029fa0e3e13a Mon Sep 17 00:00:00 2001 From: vsiakka Date: Sat, 4 May 2019 23:56:45 +0300 Subject: [PATCH 1/3] no-pattern-restrictions rule - Make Scenario, ScenarioOutline and Background patterns apply to corresponding names and descriptions - Make all patterns case insensitive - Make sure rule doesn't break on an empty scenario - Add violated pattern to error message for more clarity (multiple patterns can produce multiple errors) --- README.md | 10 +- src/rules/no-restricted-patterns.js | 131 ++++++----- .../BackgroundViolations.feature | 7 + .../FeatureViolations.feature | 24 ++ .../NoViolations.feature | 13 ++ .../ScenarioOutlineViolations.feature | 13 ++ .../ScenarioViolations.feature | 8 + .../no-restricted-patterns/Violations.feature | 22 -- .../no-restriced-patterns.js | 218 ++++++++++++++---- 9 files changed, 326 insertions(+), 120 deletions(-) create mode 100644 test/rules/no-restricted-patterns/BackgroundViolations.feature create mode 100644 test/rules/no-restricted-patterns/FeatureViolations.feature create mode 100644 test/rules/no-restricted-patterns/ScenarioOutlineViolations.feature create mode 100644 test/rules/no-restricted-patterns/ScenarioViolations.feature delete mode 100644 test/rules/no-restricted-patterns/Violations.feature diff --git a/README.md b/README.md index 3fca03c9..3790a856 100644 --- a/README.md +++ b/README.md @@ -87,9 +87,9 @@ Any tag not included in this list won't be allowed. ### no-restricted-patterns -`no-restricted-patterns` is a list of exact steps, or partial patterns that will -disallow the use of any matching steps or feature/scenario descriptions: - +`no-restricted-patterns` is a list of exact or partial patterns whose matches are dissallowed in feature name and description, and in background, scenario and scenario outline name, description and steps. +All patterns are treated as case insensitive. +The rule can be configured like this: ``` { "no-restricted-patterns": ["on", {[ @@ -113,7 +113,9 @@ disallow the use of any matching steps or feature/scenario descriptions: } ``` -Note that the `Given`, `When`, and `Then` should not be included. +Notes: +- Step keywords `Given`, `When`, `Then` and `And` should not be included in the patterns. +- Description violations always get reported in the Feature/Scenario/etc definition line. This is due to the parsed gherkin tree not having information about which line the description appears. ### indentation diff --git a/src/rules/no-restricted-patterns.js b/src/rules/no-restricted-patterns.js index 12538de9..8a752e86 100644 --- a/src/rules/no-restricted-patterns.js +++ b/src/rules/no-restricted-patterns.js @@ -1,6 +1,5 @@ -var _ = require('lodash'); var rule = 'no-restricted-patterns'; -var availableConfigs ={ +var availableConfigs = { 'Global': [], 'Scenario': [], 'ScenarioOutline': [], @@ -8,72 +7,100 @@ var availableConfigs ={ 'Feature': [] }; -function allowedPatterns(feature, fileName, configuration) { - // Patterns applied to all features, backgrounds, etc. + +function noRestrictedPatterns(feature, fileName, configuration) { + var errors = []; + + // Patterns applied to everything; feature, scenarios, etc. var globalPatterns = (configuration.Global || []).map(function(pattern) { return new RegExp(pattern, 'i'); }); - var featurePatterns = (configuration.Feature || []).map(function(pattern) { - return new RegExp(pattern, 'i'); + + var restrictedPatterns = {}; + Object.keys(availableConfigs).forEach(function(type) { + restrictedPatterns[type] = (configuration[type] || []).map(function(pattern) { + return new RegExp(pattern, 'i'); + }); + restrictedPatterns[type] = restrictedPatterns[type].concat(globalPatterns); }); - var featureErrors = checkFeature(feature, globalPatterns.concat(featurePatterns)); + checkNode(feature, restrictedPatterns, errors); + return errors; +} - var childrenErrors = _(feature.children).map(function(child) { - // Combine global patterns with the child type (Background, Scenario, etc). - var allowedPatterns = globalPatterns.concat(configuration[child.type] || []); - return checkSteps(child || [], allowedPatterns); - }).flatten().value(); - return featureErrors.concat(childrenErrors); -} +function checkNode(node, restrictedPatterns, errors, parentNodeType) { + // Steps use the configuration rules provided for their parents (eg Background) + // so allow the function to take an override for the node's type + var nodeType = parentNodeType || node.type; + + if (node && restrictedPatterns.hasOwnProperty(nodeType)) { + restrictedPatterns[nodeType] + .forEach(function(pattern) { + check(node, 'name', pattern, errors); + check(node, 'description', pattern, errors, true); + check(node, 'text', pattern, errors); + }); + } -function checkFeature(feature, allowedPatterns) { - // Check title and description of the feature. - var name = feature.name || ''; - var description = feature.description || ''; - var errors = []; - allowedPatterns.forEach(function (pattern) { - if (name.match(pattern)) { - errors.push({message: 'Restricted pattern "' + name.trim() + '" on Feature', - rule: rule, - line: feature.location.line}); - } - if (description.match(pattern)) { - errors.push({message: 'Restricted pattern "' + description.trim() + '" on Feature', - rule: rule, - line: feature.location.line}); - } - }); - return errors; -} + // Background, Scenarios and Scenario Outlines are children of a feature + if (node.children) { + node.children.forEach(function(child) { + checkNode(child, restrictedPatterns, errors); + }); + } -function checkSteps(node, allowedSteps) { - return (node.steps || []).filter(function(step) { - return isDisallowed(step, allowedSteps); - }).map(function(step) { - return createError(node, step); - }); + if (node.steps) { + node.steps.forEach(function(step) { + // Use the node.type of the parent to determine which rule configuration to use + checkNode(step, restrictedPatterns, errors, node.type); + }); + } } -function isDisallowed(step, allowedSteps) { - var disallowed = false; - allowedSteps.forEach(function (pattern) { - if (step.text.match(pattern)) { - disallowed = true; + +function check(node, property, pattern, errors) { + if (!node[property]) { + return; + } + + var strings = [node[property]]; + + if (property == 'description') { + // Descriptions can be multiline, in which case the description will contain escapted + // newline characters "\n". If a multiline description matches one of the restricted patterns + // when the error message gets printed in the console, it will break the message into multiple lines. + // So let's split the description on newline chars and test each line separately. + + // To make sure we don't accidentally pick up a doubly escaped new line "\\n" which would appear + // if a user wrote the string "\n" in a description, let's replace all escaped new lines + // with a sentinel, split lines and then restore the doubly escaped new line + var escapedNewLineSentinel = ''; + var escapedNewLine = '\\n'; + strings = node[property] + .replace(escapedNewLine, escapedNewLineSentinel) + .split('\n') + .map(function(string) { + return string.replace(escapedNewLineSentinel, escapedNewLine); + }); + } + + for (var i = 0; i < strings.length; i++) { + // We use trim() on the examined string because names and descriptions can contain + // white space before and after, unlike steps + if (strings[i].trim().match(pattern)) { + errors.push({ + message: `${node.type} ${property}: "${strings[i].trim()}" matches restricted pattern "${pattern}"`, + rule: rule, + line: node.location.line + }); } - }); - return disallowed; + } } -function createError(node, step) { - return {message: 'Restricted pattern "' + step.text + '" on ' + node.type, - rule: rule, - line: step.location.line}; -} module.exports = { name: rule, - run: allowedPatterns, + run: noRestrictedPatterns, availableConfigs: availableConfigs }; diff --git a/test/rules/no-restricted-patterns/BackgroundViolations.feature b/test/rules/no-restricted-patterns/BackgroundViolations.feature new file mode 100644 index 00000000..393b00fd --- /dev/null +++ b/test/rules/no-restricted-patterns/BackgroundViolations.feature @@ -0,0 +1,7 @@ +Feature: No restricted patterns + No restricted patterns + +Background: +A bad description + Given disallowed background step + And a restricted global pattern diff --git a/test/rules/no-restricted-patterns/FeatureViolations.feature b/test/rules/no-restricted-patterns/FeatureViolations.feature new file mode 100644 index 00000000..92050389 --- /dev/null +++ b/test/rules/no-restricted-patterns/FeatureViolations.feature @@ -0,0 +1,24 @@ +Feature: Feature with disallowed patterns + A bad description + A restricted global pattern + +Background: + This is a good description + Given only allowed patterns are used + +Scenario: Allowed steps only +This is a good description + Given I use one allowed step + When another allowed step is used + Then no errors should be reported + +Scenario Outline: Allowed steps only +This is a good description + Given I use one allowed step + When another allowed step is used + Then no errors should be reported + + Examples: + | example | + | one | + | two | \ No newline at end of file diff --git a/test/rules/no-restricted-patterns/NoViolations.feature b/test/rules/no-restricted-patterns/NoViolations.feature index 9f1c964b..c045dae3 100644 --- a/test/rules/no-restricted-patterns/NoViolations.feature +++ b/test/rules/no-restricted-patterns/NoViolations.feature @@ -2,9 +2,22 @@ Feature: No restricted patterns No restricted patterns Background: +This is a good description Given only allowed patterns are used Scenario: Allowed steps only +This is a good description Given I use one allowed step When another allowed step is used Then no errors should be reported + +Scenario Outline: Allowed steps only +This is a good description + Given I use one allowed step + When another allowed step is used + Then no errors should be reported + + Examples: + | example | + | one | + | two | diff --git a/test/rules/no-restricted-patterns/ScenarioOutlineViolations.feature b/test/rules/no-restricted-patterns/ScenarioOutlineViolations.feature new file mode 100644 index 00000000..89965354 --- /dev/null +++ b/test/rules/no-restricted-patterns/ScenarioOutlineViolations.feature @@ -0,0 +1,13 @@ +Feature: No restricted patterns + No restricted patterns + +Scenario Outline: Disallowed exact and partial matching +A bad description + Given disallowed scenario outline step + And a restricted global pattern + Then allowed step "" + + Examples: + | example | + | one | + | two | \ No newline at end of file diff --git a/test/rules/no-restricted-patterns/ScenarioViolations.feature b/test/rules/no-restricted-patterns/ScenarioViolations.feature new file mode 100644 index 00000000..35104057 --- /dev/null +++ b/test/rules/no-restricted-patterns/ScenarioViolations.feature @@ -0,0 +1,8 @@ +Feature: No restricted patterns + No restricted patterns + +Scenario: Disallowed exact and partial matching +A bad description + Given disallowed scenario step + And a restricted global pattern + Then allowed step \ No newline at end of file diff --git a/test/rules/no-restricted-patterns/Violations.feature b/test/rules/no-restricted-patterns/Violations.feature deleted file mode 100644 index e59b191a..00000000 --- a/test/rules/no-restricted-patterns/Violations.feature +++ /dev/null @@ -1,22 +0,0 @@ -Feature: Feature with disallowed patterns - Bad feature text - A restricted global pattern - -Background: - Given disallowed background step - And a restricted global pattern - -Scenario: Disallowed exact and partial matching - Given disallowed scenario step - When an exact step to not allow - Then allowed step - -Scenario Outline: - Given disallowed scenario outline step - When an exact step to not allow - Then allowed step "" - - Examples: - | example | - | one | - | two | diff --git a/test/rules/no-restricted-patterns/no-restriced-patterns.js b/test/rules/no-restricted-patterns/no-restriced-patterns.js index 86b358b3..a35a6aa6 100644 --- a/test/rules/no-restricted-patterns/no-restriced-patterns.js +++ b/test/rules/no-restricted-patterns/no-restriced-patterns.js @@ -1,8 +1,8 @@ var ruleTestBase = require('../rule-test-base'); var rule = require('../../../dist/rules/no-restricted-patterns.js'); -var runTest = ruleTestBase.createRuleTest(rule, 'Restricted pattern "<%= steps %>" on <%= nodeType %>'); +var runTest = ruleTestBase.createRuleTest(rule, '<%= nodeType %> <%= property %>: "<%= string %>" matches restricted pattern "/<%= pattern %>/i"'); -describe('No Allowed Steps Rule', function() { +describe('No Restricted Patterns Rule', function() { it('doesn\'t raise errors when there are no violations', function() { runTest('no-restricted-patterns/NoViolations.feature', { 'Global': [ @@ -10,57 +10,191 @@ describe('No Allowed Steps Rule', function() { ]}, []); }); - it('detects errors for features, scenarios, backgrounds, and scenario outlines', function() { - runTest('no-restricted-patterns/Violations.feature', { + it('detects errors in Feature names and descriptions that match the Feature or Global config', function() { + var configuration = { 'Feature': [ - 'Bad feature text', '^.*disallowed.*$' ], + 'Global': [ + '^a restricted global pattern$', + 'a bad description' + ] + }; + + runTest('no-restricted-patterns/FeatureViolations.feature', configuration, [ + { + messageElements: { + string: 'Feature with disallowed patterns', + pattern: '^.*disallowed.*$', + nodeType:'Feature', + property: 'name' + }, + line: 1 + }, + { + messageElements: { + pattern: '^a restricted global pattern$', + string: 'A restricted global pattern', + nodeType:'Feature', + property: 'description' + }, + line: 1 + }, + { + messageElements: { + pattern: 'a bad description', + string: 'A bad description', + nodeType:'Feature', + property: 'description' + }, + line: 1 + } + ]); + }); + + it('detects errors in Background descriptions and steps that match the Background or Global config', function() { + var configuration = { 'Background': [ '^.*disallowed.*$' ], + 'Global': [ + '^a restricted global pattern$', + 'a bad description' + ] + }; + + runTest('no-restricted-patterns/BackgroundViolations.feature', configuration, [ + { + messageElements: { + pattern: 'a bad description', + string: 'A bad description', + nodeType:'Background', + property: 'description' + }, + line: 4 + }, + { + messageElements: { + string: 'disallowed background step', + pattern: '^.*disallowed.*$', + nodeType:'Step', + property: 'text' + }, + line: 6 + }, + { + messageElements: { + pattern: '^a restricted global pattern$', + string: 'a restricted global pattern', + nodeType:'Step', + property: 'text' + }, + line: 7 + } + ]); + }); + + it('detects errors in Scenario names, descriptions and steps that match the Background or Global config', function() { + var configuration = { 'Scenario': [ - '^an exact step to not allow$', - 'disallowed' + '^.*disallowed.*$' ], + 'Global': [ + '^a restricted global pattern$', + 'a bad description' + ] + }; + + runTest('no-restricted-patterns/ScenarioViolations.feature', configuration, [ + { + messageElements: { + pattern: 'a bad description', + string: 'A bad description', + nodeType:'Scenario', + property: 'description' + }, + line: 4 + }, + { + messageElements: { + string: 'Disallowed exact and partial matching', + pattern: '^.*disallowed.*$', + nodeType:'Scenario', + property: 'name' + }, + line: 4 + }, + { + messageElements: { + string: 'disallowed scenario step', + pattern: '^.*disallowed.*$', + nodeType:'Step', + property: 'text' + }, + line: 6 + }, + { + messageElements: { + pattern: '^a restricted global pattern$', + string: 'a restricted global pattern', + nodeType:'Step', + property: 'text' + }, + line: 7 + } + ]); + }); + + it('detects errors in ScenarioOutline names, descriptions and steps that match the Background or Global config', function() { + var configuration = { 'ScenarioOutline': [ - '^an exact step to not allow$', - 'disallowed' + '^.*disallowed.*$' ], 'Global': [ - '^a restricted global pattern$' + '^a restricted global pattern$', + 'a bad description' ] - }, [{ - messageElements: {steps: 'Bad feature text\n A restricted global pattern', nodeType:'Feature'}, - line: 1 - }, - { - messageElements: {steps: 'Feature with disallowed patterns', nodeType:'Feature'}, - line: 1 - }, - { - messageElements: {steps: 'disallowed background step', nodeType:'Background'}, - line: 6 - }, - { - messageElements: {steps: 'a restricted global pattern', nodeType:'Background'}, - line: 7 - }, - { - messageElements: {steps: 'disallowed scenario step', nodeType:'Scenario'}, - line: 10 - }, - { - messageElements: {steps: 'an exact step to not allow', nodeType:'Scenario'}, - line: 11 - }, - { - messageElements: {steps: 'disallowed scenario outline step', nodeType: 'ScenarioOutline'}, - line: 15 - }, - { - messageElements: {steps: 'an exact step to not allow', nodeType: 'ScenarioOutline'}, - line: 16 - }]); + }; + + runTest('no-restricted-patterns/ScenarioOutlineViolations.feature', configuration, [ + { + messageElements: { + pattern: 'a bad description', + string: 'A bad description', + nodeType:'ScenarioOutline', + property: 'description' + }, + line: 4 + }, + { + messageElements: { + string: 'Disallowed exact and partial matching', + pattern: '^.*disallowed.*$', + nodeType:'ScenarioOutline', + property: 'name' + }, + line: 4 + }, + { + messageElements: { + string: 'disallowed scenario outline step', + pattern: '^.*disallowed.*$', + nodeType:'Step', + property: 'text' + }, + line: 6 + }, + { + messageElements: { + pattern: '^a restricted global pattern$', + string: 'a restricted global pattern', + nodeType:'Step', + property: 'text' + }, + line: 7 + } + ]); }); + + }); From e4afbc1b7f0b6ce410c1718d4d79223baca4958c Mon Sep 17 00:00:00 2001 From: vsiakka Date: Sun, 5 May 2019 00:42:01 +0300 Subject: [PATCH 2/3] Split up function for checking different node types because they didn't really share much code --- src/rules/no-restricted-patterns.js | 55 +++++++++++-------- .../FeatureViolations.feature | 21 ------- 2 files changed, 32 insertions(+), 44 deletions(-) diff --git a/src/rules/no-restricted-patterns.js b/src/rules/no-restricted-patterns.js index 8a752e86..c32addcd 100644 --- a/src/rules/no-restricted-patterns.js +++ b/src/rules/no-restricted-patterns.js @@ -1,3 +1,4 @@ +var _ = require('lodash'); var rule = 'no-restricted-patterns'; var availableConfigs = { 'Global': [], @@ -24,38 +25,46 @@ function noRestrictedPatterns(feature, fileName, configuration) { restrictedPatterns[type] = restrictedPatterns[type].concat(globalPatterns); }); - checkNode(feature, restrictedPatterns, errors); + checkFeatureNode(feature, restrictedPatterns, errors); return errors; } -function checkNode(node, restrictedPatterns, errors, parentNodeType) { - // Steps use the configuration rules provided for their parents (eg Background) - // so allow the function to take an override for the node's type - var nodeType = parentNodeType || node.type; - - if (node && restrictedPatterns.hasOwnProperty(nodeType)) { - restrictedPatterns[nodeType] - .forEach(function(pattern) { - check(node, 'name', pattern, errors); - check(node, 'description', pattern, errors, true); - check(node, 'text', pattern, errors); - }); +function checkFeatureNode(node, restrictedPatterns, errors) { + if (_.isEmpty(node)) { + return; } + checkNameAndDescription(node, restrictedPatterns, errors); + node.children.forEach(function(child) { + checkFeatureChildNode(child, restrictedPatterns, errors); + }); +} - // Background, Scenarios and Scenario Outlines are children of a feature - if (node.children) { - node.children.forEach(function(child) { - checkNode(child, restrictedPatterns, errors); + +function checkNameAndDescription(node, restrictedPatterns, errors) { + restrictedPatterns[node.type] + .forEach(function(pattern) { + check(node, 'name', pattern, errors); + check(node, 'description', pattern, errors, true); }); - } +} - if (node.steps) { - node.steps.forEach(function(step) { - // Use the node.type of the parent to determine which rule configuration to use - checkNode(step, restrictedPatterns, errors, node.type); + +// Background, Scenarios and Scenario Outlines are children of a feature +function checkFeatureChildNode(node, restrictedPatterns, errors) { + checkNameAndDescription(node, restrictedPatterns, errors); + node.steps.forEach(function(step) { + // Use the node type of the parent to determine which rule configuration to use + checkStepNode(step, restrictedPatterns[node.type], errors); + }); +} + + +function checkStepNode(node, restrictedPatterns, errors) { + restrictedPatterns + .forEach(function(pattern) { + check(node, 'text', pattern, errors); }); - } } diff --git a/test/rules/no-restricted-patterns/FeatureViolations.feature b/test/rules/no-restricted-patterns/FeatureViolations.feature index 92050389..c4c6cda8 100644 --- a/test/rules/no-restricted-patterns/FeatureViolations.feature +++ b/test/rules/no-restricted-patterns/FeatureViolations.feature @@ -1,24 +1,3 @@ Feature: Feature with disallowed patterns A bad description A restricted global pattern - -Background: - This is a good description - Given only allowed patterns are used - -Scenario: Allowed steps only -This is a good description - Given I use one allowed step - When another allowed step is used - Then no errors should be reported - -Scenario Outline: Allowed steps only -This is a good description - Given I use one allowed step - When another allowed step is used - Then no errors should be reported - - Examples: - | example | - | one | - | two | \ No newline at end of file From 4cf53e01ae0a017ed72b2d62a9763139ee2985db Mon Sep 17 00:00:00 2001 From: vsiakka Date: Sun, 5 May 2019 01:57:38 +0300 Subject: [PATCH 3/3] - Add all-rules tests which verify that none of the rules crashes when it parses a test file that's missing some feature properties. - Fix a couple of rules that broke when parsing such features - Remove individual rule tests checking agains an empty feature file --- src/rules.js | 7 ++-- src/rules/max-scenarios-per-file.js | 4 +- src/rules/required-tags.js | 22 ++++++---- src/rules/scenario-size.js | 7 +++- test/rules/all-rules/ChildlessFeature.feature | 1 + test/rules/all-rules/EmptyExamples.feature | 6 +++ .../EmptyFeature.feature | 0 test/rules/all-rules/SteplessFeature.feature | 7 ++++ test/rules/all-rules/all-rules.js | 40 +++++++++++++++++++ test/rules/indentation/indentation.js | 4 -- test/rules/name-length/EmptyFeature.feature | 0 test/rules/name-length/name-length.js | 4 -- .../no-unnamed-scenarios/EmptyFeature.feature | 0 .../no-unnamed-scenarios.js | 6 +-- .../no-unused-variables/EmptyFeature.feature | 0 .../no-unused-variables.js | 5 --- .../EmptyFeature.feature | 0 .../one-space-between-tags.js | 4 -- test/rules/use-and/EmptyFeature.feature | 0 test/rules/use-and/use-and.js | 4 -- 20 files changed, 81 insertions(+), 40 deletions(-) create mode 100644 test/rules/all-rules/ChildlessFeature.feature create mode 100644 test/rules/all-rules/EmptyExamples.feature rename test/rules/{indentation => all-rules}/EmptyFeature.feature (100%) create mode 100644 test/rules/all-rules/SteplessFeature.feature create mode 100644 test/rules/all-rules/all-rules.js delete mode 100644 test/rules/name-length/EmptyFeature.feature delete mode 100644 test/rules/no-unnamed-scenarios/EmptyFeature.feature delete mode 100644 test/rules/no-unused-variables/EmptyFeature.feature delete mode 100644 test/rules/one-space-between-tags/EmptyFeature.feature delete mode 100644 test/rules/use-and/EmptyFeature.feature diff --git a/src/rules.js b/src/rules.js index 328eebeb..42aa82d0 100644 --- a/src/rules.js +++ b/src/rules.js @@ -35,14 +35,12 @@ function isRuleEnabled(ruleConfig) { function runAllEnabledRules(feature, file, configuration, additionalRulesDirs) { var errors = []; - var ignoreFutureErrors = false; var rules = getAllRules(additionalRulesDirs); Object.keys(rules).forEach(function(ruleName) { var rule = rules[ruleName]; - if (isRuleEnabled(configuration[rule.name]) && !ignoreFutureErrors) { + if (isRuleEnabled(configuration[rule.name])) { var ruleConfig = Array.isArray(configuration[rule.name]) ? configuration[rule.name][1] : {}; var error = rule.run(feature, file, ruleConfig); - if (error) { errors = errors.concat(error); } @@ -56,5 +54,6 @@ module.exports = { doesRuleExist: doesRuleExist, isRuleEnabled: isRuleEnabled, runAllEnabledRules: runAllEnabledRules, - getRule: getRule + getRule: getRule, + getAllRules: getAllRules }; diff --git a/src/rules/max-scenarios-per-file.js b/src/rules/max-scenarios-per-file.js index e162a136..c736e3f9 100644 --- a/src/rules/max-scenarios-per-file.js +++ b/src/rules/max-scenarios-per-file.js @@ -22,7 +22,9 @@ function maxScenariosPerFile(feature, unused, config) { if (scenario.examples) { count = count - 1; scenario.examples.forEach(function (example) { - count = count + example.tableBody.length; + if (example.tableBody) { + count = count + example.tableBody.length; + } }); } }); diff --git a/src/rules/required-tags.js b/src/rules/required-tags.js index bf4774fa..88c45232 100644 --- a/src/rules/required-tags.js +++ b/src/rules/required-tags.js @@ -24,14 +24,20 @@ const checkTagExists = (requiredTag, scenarioTags, scenarioType) => { const checkRequiredTagsExistInScenarios = (feature, file, config) => { let errors = []; - feature.children.forEach((scenario) => { - // Check each Scenario for the required tags - const requiredTagErrors = config.tags.map((requiredTag) => { - return checkTagExists(requiredTag, scenario.tags || [], scenario.type); - }).filter((item) => typeof item === 'object' && item.message); - // Update errors - errors = errors.concat(requiredTagErrors); - }); + if (feature.children) { + feature.children.forEach((scenario) => { + // Check each Scenario for the required tags + const requiredTagErrors = config.tags + .map((requiredTag) => { + return checkTagExists(requiredTag, scenario.tags || [], scenario.type); + }) + .filter((item) => + typeof item === 'object' && item.message + ); + // Update errors + errors = errors.concat(requiredTagErrors); + }); + } return errors; }; diff --git a/src/rules/scenario-size.js b/src/rules/scenario-size.js index daab42b0..7cbd2fbf 100644 --- a/src/rules/scenario-size.js +++ b/src/rules/scenario-size.js @@ -1,3 +1,5 @@ +var _ = require('lodash'); + var rule = 'scenario-size'; var availableConfigs = { 'steps-length': { @@ -6,7 +8,10 @@ var availableConfigs = { } }; -function scenarioSize(feature, fileName, configuration = availableConfigs) { +function scenarioSize(feature, fileName, configuration) { + if (_.isEmpty(configuration)) { + configuration = availableConfigs; + } const errors = []; if (feature.children) { feature.children.forEach((child) => { diff --git a/test/rules/all-rules/ChildlessFeature.feature b/test/rules/all-rules/ChildlessFeature.feature new file mode 100644 index 00000000..5eb9db3b --- /dev/null +++ b/test/rules/all-rules/ChildlessFeature.feature @@ -0,0 +1 @@ +Feature: This is a childless feature \ No newline at end of file diff --git a/test/rules/all-rules/EmptyExamples.feature b/test/rules/all-rules/EmptyExamples.feature new file mode 100644 index 00000000..42f85b36 --- /dev/null +++ b/test/rules/all-rules/EmptyExamples.feature @@ -0,0 +1,6 @@ +Feature: Some Feature + +Scenario Outline: Some Scenario Outline + Given this is a step + + Examples: \ No newline at end of file diff --git a/test/rules/indentation/EmptyFeature.feature b/test/rules/all-rules/EmptyFeature.feature similarity index 100% rename from test/rules/indentation/EmptyFeature.feature rename to test/rules/all-rules/EmptyFeature.feature diff --git a/test/rules/all-rules/SteplessFeature.feature b/test/rules/all-rules/SteplessFeature.feature new file mode 100644 index 00000000..1f6669f5 --- /dev/null +++ b/test/rules/all-rules/SteplessFeature.feature @@ -0,0 +1,7 @@ +Feature: + +Background: + +Scenario: + +Scenario Outline: diff --git a/test/rules/all-rules/all-rules.js b/test/rules/all-rules/all-rules.js new file mode 100644 index 00000000..5190f47b --- /dev/null +++ b/test/rules/all-rules/all-rules.js @@ -0,0 +1,40 @@ +var rules = require('../../../dist/rules.js'); +var linter = require('../../../dist/linter.js'); + +// Test cases for incomplete feature files that have broken over time accross multiple rules +describe('All rules', function() { + + function runAllEnabledRulesAgainstFile(featureFile) { + var allRules = rules.getAllRules(); + var configuration = {}; + Object.keys(allRules).forEach(function(rule) { + if (rule == 'new-line-at-eof') { + configuration[rule] = ['on', 'yes']; + } else if (rule == 'required-tags') { + configuration[rule] = ['on', {'tags': [] }]; + } else { + configuration[rule] = 'on'; + } + }); + + const {feature, file} = linter.readAndParseFile('test/rules/all-rules/' + featureFile, 'utf8'); + + rules.runAllEnabledRules(feature, file, configuration); + } + + it('do not throw exceptions when processing an empty feature', function() { + runAllEnabledRulesAgainstFile('EmptyFeature.feature'); + }); + + it('do not throw exceptions when processing a feature with no children', function() { + runAllEnabledRulesAgainstFile('ChildlessFeature.feature'); + }); + + it('do not throw exceptions when processing a feature with no steps', function() { + runAllEnabledRulesAgainstFile('SteplessFeature.feature'); + }); + + it('do not throw exceptions when processing a scenario outline with an empty examples table', function() { + runAllEnabledRulesAgainstFile('EmptyExamples.feature'); + }); +}); \ No newline at end of file diff --git a/test/rules/indentation/indentation.js b/test/rules/indentation/indentation.js index 286f8cab..1f4854c1 100644 --- a/test/rules/indentation/indentation.js +++ b/test/rules/indentation/indentation.js @@ -59,10 +59,6 @@ describe('Indentation rule', function() { runTest('indentation/CorrectIndentationTabs.feature', {}, []); }); - it('doesn\'t raise errors when parsing an empty feature', function() { - runTest('indentation/EmptyFeature.feature', {}, []); - }); - it('detects errors for features, backgrounds, scenarios, scenario outlines and steps (spaces)', function() { runTest('indentation/WrongIndentationSpaces.feature', {}, wrongIndenatationErrors); }); diff --git a/test/rules/name-length/EmptyFeature.feature b/test/rules/name-length/EmptyFeature.feature deleted file mode 100644 index e69de29b..00000000 diff --git a/test/rules/name-length/name-length.js b/test/rules/name-length/name-length.js index 8a0974b5..e3b15827 100644 --- a/test/rules/name-length/name-length.js +++ b/test/rules/name-length/name-length.js @@ -8,10 +8,6 @@ describe('Name length rule', function() { runTest('name-length/CorrectLength.feature', {}, []); }); - it('doesn\'t raise errors when parsing an empty feature', function() { - runTest('name-length/EmptyFeature.feature', {}, []); - }); - it('detects errors for features, scenarios, scenario outlines and steps', function() { runTest('name-length/WrongLength.feature', {}, [{ messageElements: {element: 'Feature', length: 89}, diff --git a/test/rules/no-unnamed-scenarios/EmptyFeature.feature b/test/rules/no-unnamed-scenarios/EmptyFeature.feature deleted file mode 100644 index e69de29b..00000000 diff --git a/test/rules/no-unnamed-scenarios/no-unnamed-scenarios.js b/test/rules/no-unnamed-scenarios/no-unnamed-scenarios.js index 745bd467..a74ecc59 100644 --- a/test/rules/no-unnamed-scenarios/no-unnamed-scenarios.js +++ b/test/rules/no-unnamed-scenarios/no-unnamed-scenarios.js @@ -6,11 +6,7 @@ describe('No Unnamed Scenarios Rule', function() { it('doesn\'t raise errors when there are no violations', function() { runTest('no-unnamed-scenarios/NoViolations.feature', {}, []); }); - - it('doesn\'t raise errors for an empty feature', function() { - runTest('no-unnamed-scenarios/EmptyFeature.feature', {}, []); - }); - + it('doesn\'t raise errors for a feature with no scenarios', function() { runTest('no-unnamed-scenarios/FeatureWithNoScenarios.feature', {}, []); }); diff --git a/test/rules/no-unused-variables/EmptyFeature.feature b/test/rules/no-unused-variables/EmptyFeature.feature deleted file mode 100644 index e69de29b..00000000 diff --git a/test/rules/no-unused-variables/no-unused-variables.js b/test/rules/no-unused-variables/no-unused-variables.js index e2c930b6..048baf08 100644 --- a/test/rules/no-unused-variables/no-unused-variables.js +++ b/test/rules/no-unused-variables/no-unused-variables.js @@ -7,11 +7,6 @@ describe('No Unused Variables Rule', function() { runTest('no-unused-variables/NoViolations.feature', {}, []); }); - it('doesn\'t raise errors when parsing an empty feature', function() { - var runTest = ruleTestBase.createRuleTest(rule, ''); - runTest('no-unused-variables/EmptyFeature.feature', {}, []); - }); - it('detects unused scenario variables', function() { var runTest = ruleTestBase.createRuleTest(rule, 'Step variable "<%= variable %>" does not exist the in examples table'); diff --git a/test/rules/one-space-between-tags/EmptyFeature.feature b/test/rules/one-space-between-tags/EmptyFeature.feature deleted file mode 100644 index e69de29b..00000000 diff --git a/test/rules/one-space-between-tags/one-space-between-tags.js b/test/rules/one-space-between-tags/one-space-between-tags.js index 23a667e5..2bffa6a3 100644 --- a/test/rules/one-space-between-tags/one-space-between-tags.js +++ b/test/rules/one-space-between-tags/one-space-between-tags.js @@ -8,10 +8,6 @@ describe('One Space Between Tags Rule', function() { runTest('one-space-between-tags/NoViolations.feature', {}, []); }); - it('doesn\'t raise errors when parsing an empty feature', function() { - runTest('one-space-between-tags/EmptyFeature.feature', {}, []); - }); - it('detects errors for tags on features, scenarios, and scenario outlines', function() { runTest('one-space-between-tags/Violations.feature', {}, [{ line: 1, diff --git a/test/rules/use-and/EmptyFeature.feature b/test/rules/use-and/EmptyFeature.feature deleted file mode 100644 index e69de29b..00000000 diff --git a/test/rules/use-and/use-and.js b/test/rules/use-and/use-and.js index f67b9b33..9a43d5d3 100644 --- a/test/rules/use-and/use-and.js +++ b/test/rules/use-and/use-and.js @@ -8,10 +8,6 @@ describe('Use And Rule', function() { runTest('use-and/NoViolations.feature', {}, []); }); - it('doesn\'t raise errors when parsing an empty feature', function() { - runTest('use-and/EmptyFeature.feature', {}, []); - }); - it('raises erros when there are violations', function() { runTest('use-and/Violations.feature', {}, [ {