diff --git a/features/scenario_outlines.feature b/features/scenario_outlines.feature new file mode 100644 index 000000000..9d520fbda --- /dev/null +++ b/features/scenario_outlines.feature @@ -0,0 +1,70 @@ +Feature: Scenario Outlines and Examples + + Scenario: Basic outline + Given the following feature: + """ + Feature: testing scenarios + Background: + Given a background step + + Scenario Outline: outline + When a step + Then i get + Examples: + | some | result | + | passing | passed | + | failing | skipped | + """ + And the step "a background step" has a passing mapping + And the step "a passing step" has a passing mapping + And the step "a failing step" has a failing mapping + And the step "i get passed" has a passing mapping + And the step "i get skipped" has a passing mapping + When Cucumber runs the feature + Then the scenario called "outline" is reported as failing + And the step "a background step" passes + And the step "a passing step" passes + And the step "a failing step" passes + And the step "i get passed" passes + And the step "i get skipped" is skipped + + Scenario: Outline with table + Given the following feature: + """ + Feature: testing scenarios + Scenario Outline: outline + When a table step: + | first | second | + | | | + Examples: + | first | second | + | 1 | 2 | + """ + And the step "a table step:" has a passing mapping that receives a data table + When Cucumber runs the feature + Then the received data table array equals the following: + """ + [["first","second"],["1","2"]] + """ + + Scenario: Outline with doc string + Given the following feature: + """ + Feature: testing scenarios + Scenario Outline: outline + When a doc sting step: + \"\"\" + I am doc string in example + And there are string + \"\"\" + Examples: + | example | string | + | first | some | + """ + And the step "a doc sting step:" has a passing mapping that receives a doc string + When Cucumber runs the feature + Then the received doc string equals the following: + """ + I am doc string in first example + And there are some string + """ \ No newline at end of file diff --git a/features/step_definitions/cucumber_steps.js b/features/step_definitions/cucumber_steps.js index 948b8bdbf..9417c9e99 100644 --- a/features/step_definitions/cucumber_steps.js +++ b/features/step_definitions/cucumber_steps.js @@ -128,6 +128,14 @@ setTimeout(callback.pending, 10);\ callback(); }); + Given(/^the step "([^"]*)" has a passing mapping that receives a doc string$/, function(stepName, callback) { + this.stepDefinitions += "Given(/^" + stepName + "$/, function(docString, callback) {\ + world.docString = docString;\ + callback();\ +});"; + callback(); + }); + Given(/^the following data table in a step:$/, function(dataTable, callback) { this.featureSource += "Feature:\n"; this.featureSource += " Scenario:\n"; @@ -315,6 +323,11 @@ callback();\ callback(); }); + Then(/^the received doc string equals the following:$/, function(docString, callback) { + this.assertEqual(docString, World.mostRecentInstance.docString); + callback(); + }); + this.Then(/^the explicit World object function should have been called$/, function(callback) { this.assertTrue(this.explicitWorldFunctionCalled); callback(); diff --git a/lib/cucumber/ast.js b/lib/cucumber/ast.js index e9677e382..20d95b534 100644 --- a/lib/cucumber/ast.js +++ b/lib/cucumber/ast.js @@ -7,6 +7,8 @@ Ast.Feature = require('./ast/feature'); Ast.Features = require('./ast/features'); Ast.Filter = require('./ast/filter'); Ast.Scenario = require('./ast/scenario'); +Ast.ScenarioOutline = require('./ast/scenario_outline'); +Ast.Examples = require('./ast/examples'); Ast.Step = require('./ast/step'); Ast.Tag = require('./ast/tag'); module.exports = Ast; diff --git a/lib/cucumber/ast/assembler.js b/lib/cucumber/ast/assembler.js index 178df8ced..e9e5e3c81 100644 --- a/lib/cucumber/ast/assembler.js +++ b/lib/cucumber/ast/assembler.js @@ -1,4 +1,4 @@ -var Assembler = function(features, filter) { +var Assembler = function (features, filter) { var currentFeature, currentScenarioOrBackground, currentStep; var stashedTags = []; @@ -81,6 +81,11 @@ var Assembler = function(features, filter) { currentFeature.addScenario(scenario); } }, + insertExamples: function insertExamples(examples) { + var currentScenarioOrBackground = self.getCurrentScenarioOrBackground(); + currentScenarioOrBackground.setExamples(examples); + self.setCurrentStep(examples); + }, insertStep: function insertStep(step) { self.setCurrentStep(step); diff --git a/lib/cucumber/ast/data_table.js b/lib/cucumber/ast/data_table.js index a3077aecf..3eeb6acbf 100644 --- a/lib/cucumber/ast/data_table.js +++ b/lib/cucumber/ast/data_table.js @@ -12,8 +12,16 @@ var DataTable = function() { return self; }, + getRows: function getRows(){ + var newRows = Cucumber.Type.Collection(); + rows.syncForEach(function(row){ + newRows.add(row); + }); + return newRows; + }, + raw: function raw() { - rawRows = []; + var rawRows = []; rows.syncForEach(function(row) { var rawRow = row.raw(); rawRows.push(rawRow); diff --git a/lib/cucumber/ast/data_table/row.js b/lib/cucumber/ast/data_table/row.js index 577e8d5d8..aeff77406 100644 --- a/lib/cucumber/ast/data_table/row.js +++ b/lib/cucumber/ast/data_table/row.js @@ -4,7 +4,11 @@ var Row = function(cells, line) { self = { raw: function raw() { return cells; + }, + getLine: function getLine(){ + return line; } + }; return self; } diff --git a/lib/cucumber/ast/examples.js b/lib/cucumber/ast/examples.js new file mode 100644 index 000000000..f296c3be7 --- /dev/null +++ b/lib/cucumber/ast/examples.js @@ -0,0 +1,49 @@ +var Examples = function (keyword, name, description, line) { + var Cucumber = require('../../cucumber'), + dataTable, + self; + return self = { + getKeyword: function getKeyword() { + return keyword; + }, + + getName: function getName() { + return name; + }, + + getDescription: function getKeyword() { + return keyword; + }, + + getLine: function getLine() { + return line; + }, + + getDataTable: function getDataTable() { + return dataTable; + }, + + hasDataTable: function hasDataTable() { + return !!dataTable; + }, + + attachDataTable: function attachDataTable(_dataTable) { + dataTable = _dataTable; + }, + + attachDataTableRow: function attachDataTableRow(row) { + self.ensureDataTableIsAttached(); + var dataTable = self.getDataTable(); + dataTable.attachRow(row); + }, + + ensureDataTableIsAttached: function ensureDataTableIsAttached() { + var dataTable = self.getDataTable(); + if (!dataTable) { + dataTable = Cucumber.Ast.DataTable(); + self.attachDataTable(dataTable); + } + } + } +}; +module.exports = Examples; diff --git a/lib/cucumber/ast/scenario.js b/lib/cucumber/ast/scenario.js index 373799aeb..2a2f2b284 100644 --- a/lib/cucumber/ast/scenario.js +++ b/lib/cucumber/ast/scenario.js @@ -6,6 +6,7 @@ var Scenario = function(keyword, name, description, line) { var tags = []; var self = { + payload_type: 'scenario', setBackground: function setBackground(newBackground) { background = newBackground; }, @@ -40,6 +41,10 @@ var Scenario = function(keyword, name, description, line) { return steps.getLast(); }, + getSteps: function getSteps(){ + return steps; + }, + addTags: function setTags(newTags) { tags = tags.concat(newTags); }, diff --git a/lib/cucumber/ast/scenario_outline.js b/lib/cucumber/ast/scenario_outline.js new file mode 100644 index 000000000..ecf8ae0ff --- /dev/null +++ b/lib/cucumber/ast/scenario_outline.js @@ -0,0 +1,88 @@ +var ScenarioOutline = function (keyword, name, description, line) { + var Cucumber = require('../../cucumber'), + self = Cucumber.Ast.Scenario(keyword, name, description, line), + examples; + self.payload_type = 'scenarioOutline'; + self.setExamples = function (newExamples) { + examples = newExamples; + }; + self.getExamples = function () { + return examples; + }; + function applyExampleRow(example, steps) { + return steps.syncMap(function (outline) { + var name = outline.getName(), + table = Cucumber.Ast.DataTable(), + rows = [], + hasDocString = outline.hasDocString(), + hasDataTable = outline.hasDataTable(), + oldDocString = hasDocString ? outline.getDocString() : null, + docString = hasDocString ? oldDocString.getContents() : null, + hashKey; + if (hasDataTable){ + rows = []; + outline.getDataTable().getRows().syncForEach(function(row){ + rows.push( + { line: row.getLine(), cells: JSON.stringify(row.raw()) } + ); + }); + + } + for (hashKey in example) { + if (Object.prototype.hasOwnProperty.call(example, hashKey)) { + name = name.replace('<' + hashKey + '>', example[hashKey]); + if (hasDataTable) { + rows = rows.map(function(row){ + return {line:row.line, cells:row.cells.replace('<' + hashKey + '>', example[hashKey])}; + }); + } + if (hasDocString) { + docString = docString.replace('<' + hashKey + '>', example[hashKey]); + } + } + } + var step = Cucumber.Ast.Step(outline.getKeyword(), name, outline.getLine()); + if (hasDataTable) { + rows.forEach(function(row){ + table.attachRow( Cucumber.Ast.DataTable.Row( JSON.parse(row.cells), row.line) ); + }); + step.attachDataTable(table); + } + if (hasDocString) { + step.attachDocString( Cucumber.Ast.DocString(oldDocString.getContentType(), docString, oldDocString.getLine())); + } + return step; + }); + } + self.acceptVisitor = function (visitor, callback) { + var rows = examples.getDataTable().getRows(), + first_row = rows.shift().raw(); + rows.syncForEach(function(row, index){ + var length = first_row.length, + i; + row.example = {}; + row.id = index.toString(); + for (i = 0; i < length; i++){ + row.example[first_row[i]] = row.raw()[i]; + } + }); + + rows.forEach(function (row, iterate){ + self.instructVisitorToVisitRowSteps(visitor, row, iterate); + },callback) + }; + self.instructVisitorToVisitRowSteps = function (visitor, row, callback) { + visitor.visitRow(row, self, callback); + + }; + self.visitRowSteps = function (visitor, row, callback) { + self.instructVisitorToVisitBackgroundSteps(visitor, function () { + var newSteps = self.applyExampleRow(row.example, self.getSteps()); + self.instructVisitorToVisitSteps(visitor, newSteps, callback); + }); + }; + + return self; +}; +module.exports = ScenarioOutline; + diff --git a/lib/cucumber/listener/progress_formatter.js b/lib/cucumber/listener/progress_formatter.js index d02ff4f46..1d68c3a91 100644 --- a/lib/cucumber/listener/progress_formatter.js +++ b/lib/cucumber/listener/progress_formatter.js @@ -22,6 +22,24 @@ var ProgressFormatter = function(options) { options = {}; if (options['logToConsole'] == undefined) options['logToConsole'] = true; + + function handleAfterScenarioEvent(payload){ + return function(event, callback){ + if (self.isCurrentScenarioFailing()) { + var scenario = event.getPayloadItem(payload); + self.storeFailedScenario(scenario); + self.witnessFailedScenario(); + } else if (self.isCurrentScenarioUndefined()) { + self.witnessUndefinedScenario(); + } else if (self.isCurrentScenarioPending()) { + self.witnessPendingScenario(); + } else { + self.witnessPassedScenario(); + } + callback(); + } + } + var self = { log: function log(string) { logs += string; @@ -67,6 +85,9 @@ var ProgressFormatter = function(options) { callback(); }, + handleBeforeScenarioOutlineEvent: this.handleBeforeScenarioEvent, + handleAfterScenarioOutlineEvent: handleAfterScenarioEvent('scenarioOutline'), + handleStepResultEvent: function handleStepResult(event, callback) { var stepResult = event.getPayloadItem('stepResult'); if (stepResult.isSuccessful()) @@ -118,20 +139,7 @@ var ProgressFormatter = function(options) { callback(); }, - handleAfterScenarioEvent: function handleAfterScenarioEvent(event, callback) { - if (self.isCurrentScenarioFailing()) { - var scenario = event.getPayloadItem('scenario'); - self.storeFailedScenario(scenario); - self.witnessFailedScenario(); - } else if (self.isCurrentScenarioUndefined()) { - self.witnessUndefinedScenario(); - } else if (self.isCurrentScenarioPending()) { - self.witnessPendingScenario(); - } else { - self.witnessPassedScenario(); - } - callback(); - }, + handleAfterScenarioEvent: handleAfterScenarioEvent('scenario'), prepareBeforeScenario: function prepareBeforeScenario() { currentScenarioFailing = false; diff --git a/lib/cucumber/parser.js b/lib/cucumber/parser.js index 52ecd9b36..d0b0e7a40 100644 --- a/lib/cucumber/parser.js +++ b/lib/cucumber/parser.js @@ -25,8 +25,11 @@ var Parser = function(featureSources, astFilter) { feature: self.handleFeature, row: self.handleDataTableRow, scenario: self.handleScenario, + scenario_outline: self.handleScenarioOutline, + examples: self.handleExamples, step: self.handleStep, tag: self.handleTag + }; }, @@ -63,7 +66,14 @@ var Parser = function(featureSources, astFilter) { var scenario = Cucumber.Ast.Scenario(keyword, name, description, line); astAssembler.insertScenario(scenario); }, - + handleScenarioOutline: function handleScenarioOutline(keyword, name, description, line) { + var outline = Cucumber.Ast.ScenarioOutline(keyword, name, description, line); + astAssembler.insertScenario(outline); + }, + handleExamples: function handleExamples(keyword, name, description, line) { + var examples = Cucumber.Ast.Examples(keyword, name, description, line); + astAssembler.insertExamples(examples); + }, handleStep: function handleStep(keyword, name, line) { var step = Cucumber.Ast.Step(keyword, name, line); astAssembler.insertStep(step); diff --git a/lib/cucumber/runtime/ast_tree_walker.js b/lib/cucumber/runtime/ast_tree_walker.js index 4da776fd7..bad01456d 100644 --- a/lib/cucumber/runtime/ast_tree_walker.js +++ b/lib/cucumber/runtime/ast_tree_walker.js @@ -43,8 +43,9 @@ var AstTreeWalker = function(features, supportCodeLibrary, listeners) { supportCodeLibrary.instantiateNewWorld(function(world) { self.setWorld(world); self.witnessNewScenario(); - var payload = { scenario: scenario }; - var event = AstTreeWalker.Event(AstTreeWalker.SCENARIO_EVENT_NAME, payload); + var payload = { }; + payload[scenario.payload_type] = scenario; + var event = AstTreeWalker.Event(AstTreeWalker[scenario.payload_type.toUpperCase() + '_EVENT_NAME'], payload); var hookedUpScenarioVisit = supportCodeLibrary.hookUpFunction( function(callback) { scenario.acceptVisitor(self, callback); }, scenario, @@ -57,7 +58,18 @@ var AstTreeWalker = function(features, supportCodeLibrary, listeners) { ); }); }, - + visitRow: function visitRow(row, scenario,callback){ + var payload = {exampleRow:row}, + event = AstTreeWalker.Event(AstTreeWalker.ROW_EVENT_NAME, payload); + self.witnessNewScenario(); + self.broadcastEventAroundUserFunction( + event, + function(callback){ + scenario.visitRowSteps(self, row, callback); + }, + callback + ); + }, visitStep: function visitStep(step, callback) { var payload = { step: step }; var event = AstTreeWalker.Event(AstTreeWalker.STEP_EVENT_NAME, payload); @@ -190,8 +202,10 @@ AstTreeWalker.FEATURES_EVENT_NAME = 'Features'; AstTreeWalker.FEATURE_EVENT_NAME = 'Feature'; AstTreeWalker.BACKGROUND_EVENT_NAME = 'Background'; AstTreeWalker.SCENARIO_EVENT_NAME = 'Scenario'; +AstTreeWalker.SCENARIOOUTLINE_EVENT_NAME = 'ScenarioOutline'; AstTreeWalker.STEP_EVENT_NAME = 'Step'; AstTreeWalker.STEP_RESULT_EVENT_NAME = 'StepResult'; +AstTreeWalker.ROW_EVENT_NAME = 'ExampleRow'; AstTreeWalker.BEFORE_EVENT_NAME_PREFIX = 'Before'; AstTreeWalker.AFTER_EVENT_NAME_PREFIX = 'After'; AstTreeWalker.NON_EVENT_LEADING_PARAMETERS_COUNT = 0; diff --git a/lib/cucumber/support_code/library.js b/lib/cucumber/support_code/library.js index 501e627b5..38d0e8d2a 100644 --- a/lib/cucumber/support_code/library.js +++ b/lib/cucumber/support_code/library.js @@ -51,8 +51,9 @@ var Library = function(supportCodeDefinition) { }, instantiateNewWorld: function instantiateNewWorld(callback) { - var world = new worldConstructor(function(explicitWorld) { - process.nextTick(function() { // release the constructor + var world; + world = new worldConstructor(function (explicitWorld) { + process.nextTick(function () { // release the constructor callback(explicitWorld || world); }); }); diff --git a/lib/cucumber/type/collection.js b/lib/cucumber/type/collection.js index 96cc9120f..51b085963 100644 --- a/lib/cucumber/type/collection.js +++ b/lib/cucumber/type/collection.js @@ -3,6 +3,7 @@ var Collection = function() { var self = { add: function add(item) { items.push(item); }, unshift: function unshift(item) { items.unshift(item); }, + shift: function shift() { return items.shift(); }, getLast: function getLast() { return items[items.length-1]; }, syncForEach: function syncForEach(userFunction) { items.forEach(userFunction); }, forEach: function forEach(userFunction, callback) { @@ -22,6 +23,13 @@ var Collection = function() { }; iterate(); }, + syncMap: function map(userFunction) { + var newCollection = Collection(); + items.map(function(item){ + newCollection.add(userFunction(item)); + }); + return newCollection; + }, length: function length() { return items.length; } }; return self; diff --git a/lib/cucumber/util/exception.js b/lib/cucumber/util/exception.js index 36314786a..c8a254b42 100644 --- a/lib/cucumber/util/exception.js +++ b/lib/cucumber/util/exception.js @@ -2,14 +2,14 @@ var Exception = { registerUncaughtExceptionHandler: function registerUncaughtExceptionHandler(exceptionHandler) { if (process.on) process.on('uncaughtException', exceptionHandler); - else + else if (typeof(window) != 'undefined') window.onerror = exceptionHandler; }, unregisterUncaughtExceptionHandler: function unregisterUncaughtExceptionHandler(exceptionHandler) { if (process.removeListener) process.removeListener('uncaughtException', exceptionHandler); - else + else if (typeof(window) != 'undefined') window.onerror = void(0); } }; diff --git a/package.json b/package.json index a98313797..6ac26c40e 100644 --- a/package.json +++ b/package.json @@ -1,48 +1,69 @@ -{ "name" : "cucumber" -, "description" : "The official JavaScript implementation of Cucumber." -, "keywords" : [ "testing", "bdd", "cucumber", "gherkin", "tests" ] -, "version" : "0.2.15" -, "homepage" : "http://github.com/cucumber/cucumber-js" -, "author" : "Julien Biezemans (http://jbpros.net)" -, "contributors" : [ - "Julien Biezemans (http://jbpros.net)" - , "Fernando Acorreia " - , "Paul Jensen " - , "Kushal Pisavadia" - , "Olivier Melcher " - , "Tristan Dunn " - , "Ted de Koning" - ] -, "repository" : - { "type" : "git" - , "url" : "git://github.com/cucumber/cucumber-js.git" - } -, "bugs" : - { "mail" : "cukes@googlegroups.com" - , "url" : "http://github.com/cucumber/cucumber-js/issues" - } -, "directories" : { "lib" : "./lib" } -, "main" : "./lib/cucumber" -, "engines" : { "node" : "0.4 || 0.5 || 0.6" } -, "dependencies" : - { "gherkin" : "2.6.8" - , "jasmine-node" : "1.0.13" - , "connect" : "1.8.1" - , "browserify" : "1.8.1" - , "nopt" : "1.0.10" - , "underscore" : "1.2.2" - , "rimraf" : "1.0.8" - , "mkdirp" : "0.2.1" - , "cucumber-html": "0.2.0" - , "findit": "0.1.1" - , "coffee-script": "1.1.2" - } -, "scripts" : - { "test" : "./bin/cucumber.js && jasmine-node spec" } -, "bin": { "cucumber.js": "./bin/cucumber.js", "cucumber-js": "./bin/cucumber.js" } -, "licenses" : - [ { "type" : "MIT" - , "url" : "http://github.com/cucumber/cucumber.js/LICENSE" +{ + "name": "cucumber", + "description": "The official JavaScript implementation of Cucumber.", + "keywords": [ + "testing", + "bdd", + "cucumber", + "gherkin", + "tests" + ], + "version": "0.2.18", + "homepage": "http://github.com/cucumber/cucumber-js", + "author": "Julien Biezemans (http://jbpros.net)", + "contributors": [ + "Julien Biezemans (http://jbpros.net)", + "Fernando Acorreia ", + "Paul Jensen ", + "Kushal Pisavadia", + "Olivier Melcher ", + "Tristan Dunn ", + "Ted de Koning", + "@renier", + "Aslak Hellesøy ", + "Aaron Garvey" + ], + "repository": { + "type": "git", + "url": "git://github.com/cucumber/cucumber-js.git" + }, + "bugs": { + "email": "cukes@googlegroups.com", + "url": "http://github.com/cucumber/cucumber-js/issues" + }, + "directories": { + "lib": "./lib" + }, + "main": "./lib/cucumber", + "engines": { + "node": "0.6 || 0.7 || 0.8" + }, + "dependencies": { + "gherkin": "2.11.0", + "jasmine-node": "1.0.26", + "connect": "2.3.2", + "browserify": "1.13.2", + "nopt": "1.0.10", + "underscore": "1.3.3", + "rimraf": "2.0.2", + "mkdirp": "0.3.3", + "cucumber-html": "0.2.0", + "walkdir": "0.0.4", + "coffee-script": "1.3.3" + }, + "scripts": { + "test": "./bin/cucumber.js && jasmine-node spec" + }, + "bin": { + "cucumber.js": "./bin/cucumber.js", + "cucumber-js": "./bin/cucumber.js" + }, + "licenses": [ + { + "type": "MIT", + "url": "http://github.com/cucumber/cucumber.js/LICENSE" } - ] -} + ], + "devDependencies": {}, + "optionalDependencies": {} +} \ No newline at end of file