diff --git a/README.md b/README.md index d5c9ff6df..d99f64b18 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ Cucumber.js is still a work in progress. Here is its current status. | [Failing steps](/~https://github.com/cucumber/cucumber-tck/blob/master/failing_steps.feature) | Done | | [Hooks](/~https://github.com/cucumber/cucumber-tck/blob/master/hooks.feature) | Done | | [I18n](/~https://github.com/cucumber/cucumber-tck/blob/master/i18n.feature) | To do | -| [JSON formatter](/~https://github.com/cucumber/cucumber-tck/blob/master/json_formatter.feature) | To do | +| [JSON formatter](/~https://github.com/cucumber/cucumber-tck/blob/master/json_formatter.feature) | WIP4 | | [Pretty formatter](/~https://github.com/cucumber/cucumber-tck/blob/master/pretty_formatter.feature) | WIP2 | | [Scenario outlines and examples](/~https://github.com/cucumber/cucumber-tck/blob/master/scenario_outlines_and_examples.feature) | To do | | [Stats collector](/~https://github.com/cucumber/cucumber-tck/blob/master/stats_collector.feature) | To do | @@ -38,6 +38,12 @@ Cucumber.js is still a work in progress. Here is its current status. 1. Not certified by [Cucumber TCK](/~https://github.com/cucumber/cucumber-tck) yet. 2. Considered for removal from [Cucumber TCK](/~https://github.com/cucumber/cucumber-tck). 3. Simple *Around*, *Before* and *After* hooks are available. +4. Missing 'matches' attributes. Simple wrapper for Gherkin JsonFormatter pending porting of: + +* /~https://github.com/cucumber/gherkin/blob/master/lib/gherkin/listener/formatter_listener.rb +* /~https://github.com/cucumber/gherkin/blob/master/lib/gherkin/formatter/filter_formatter.rb + +In Gherkin itself ### Cucumber.js-specific features diff --git a/features/cli.feature b/features/cli.feature index 5b3424f71..277b24790 100644 --- a/features/cli.feature +++ b/features/cli.feature @@ -102,4 +102,4 @@ Feature: Command line interface Scenario: display help (short flag) When I run `cucumber.js -h` - Then I see the help of Cucumber + Then I see the help of Cucumber \ No newline at end of file diff --git a/features/json_formatter.feature b/features/json_formatter.feature new file mode 100644 index 000000000..f4984f35d --- /dev/null +++ b/features/json_formatter.feature @@ -0,0 +1,1449 @@ +Feature: JSON Formatter + In order to simplify processing of Cucumber features and results + Developers should be able to consume features as JSON + + Scenario: output JSON for a feature with no scenarios + Given a file named "features/a.feature" with: + """ + Feature: some feature + """ + When I run `cucumber.js -f json` + Then it should output this json: + """ + [ + { + "id":"some-feature", + "name":"some feature", + "description":"", + "line":1, + "keyword":"Feature", + "uri":"/path/to/sandbox/tmp/cucumber-js-sandbox/features/a.feature" + } + ] + """ + + Scenario: output JSON for a feature with one undefined scenario + Given a file named "features/a.feature" with: + """ + Feature: some feature + + Scenario: I havn't done anything yet + """ + When I run `cucumber.js -f json` + Then it should output this json: + """ + [ + { + "id":"some-feature", + "name":"some feature", + "description":"", + "line":1, + "keyword":"Feature", + "uri":"/path/to/sandbox/tmp/cucumber-js-sandbox/features/a.feature", + "elements":[ + {"name":"I havn't done anything yet", + "id":"some-feature;i-havn't-done-anything-yet", + "line":3, + "keyword":"Scenario", + "description":"", + "type":"scenario" + } + ] + } + ] + """ + + Scenario: output JSON for a feature with one scenario with one undefined step + Given a file named "features/a.feature" with: + """ + Feature: some feature + + Scenario: I've declaired one step but not yet defined it + Given I have not defined this step + """ + When I run `cucumber.js -f json` + Then it should output this json: + """ + [ + { + "id": "some-feature", + "name": "some feature", + "description": "", + "line": 1, + "keyword": "Feature", + "uri":"/path/to/sandbox/tmp/cucumber-js-sandbox/features/a.feature", + "elements": [ + { + "name": "I've declaired one step but not yet defined it", + "id": "some-feature;i've-declaired-one-step-but-not-yet-defined-it", + "line": 3, + "keyword": "Scenario", + "description": "", + "type": "scenario", + "steps":[ + {"name":"I have not defined this step", + "line":4, + "keyword":"Given ", + "result": + {"status":"undefined" + }, + "match": { + } + } + ] + } + ] + } + ] + """ + + Scenario: output JSON for a feature with one undefined step and subsequent defined steps which should be skipped + Given a file named "features/a.feature" with: + """ + Feature: some feature + + Scenario: One pending step and two following steps which will be skipped + Given This step is undefined + Then this step should be skipped + + """ + And a file named "features/step_definitions/cucumber_steps.js" with: + """ + var cucumberSteps = function() { + this.Then(/^this step should be skipped$/, function(callback) { callback(); }); + }; + module.exports = cucumberSteps; + """ + + When I run `cucumber.js -f json` + Then it should output this json: + """ + [ + { + "id": "some-feature", + "name": "some feature", + "description": "", + "line": 1, + "keyword": "Feature", + "uri": "/path/to/sandbox/tmp/cucumber-js-sandbox/features/a.feature", + "elements": [ + { + "name": "One pending step and two following steps which will be skipped", + "id": "some-feature;one-pending-step-and-two-following-steps-which-will-be-skipped", + "line": 3, + "keyword": "Scenario", + "description": "", + "type": "scenario", + "steps": [ + { + "name": "This step is undefined", + "line": 4, + "keyword": "Given ", + "result": { + "status": "undefined" + }, + "match": { + } + }, + { + "name": "this step should be skipped", + "line": 5, + "keyword": "Then ", + "result": { + "status": "skipped" + }, + "match": { + } + } + ] + } + ] + } + ] + """ + + Scenario: output JSON for a feature with one scenario with one pending step + Given a file named "features/a.feature" with: + """ + Feature: some feature + + Scenario: I've declaired one step which is pending + Given This step is pending + """ + And a file named "features/step_definitions/cucumber_steps.js" with: + """ + var cucumberSteps = function() { + this.Given(/^This step is pending$/, function(callback) { callback.pending(); }); + }; + module.exports = cucumberSteps; + """ + + When I run `cucumber.js -f json` + Then it should output this json: + """ + [ + { + "id": "some-feature", + "name": "some feature", + "description": "", + "line": 1, + "keyword": "Feature", + "uri":"/path/to/sandbox/tmp/cucumber-js-sandbox/features/a.feature", + "elements": [ + { + "name": "I've declaired one step which is pending", + "id": "some-feature;i've-declaired-one-step-which-is-pending", + "line": 3, + "keyword": "Scenario", + "description": "", + "type": "scenario", + "steps": [ + { + "name": "This step is pending", + "line": 4, + "keyword": "Given ", + "result": { + "status": "pending" + }, + "match": { + } + } + ] + } + ] + } + ] + """ + Scenario: output JSON for a feature with one scenario with failing step + Given a file named "features/a.feature" with: + """ + Feature: some feature + + Scenario: I've declaired one step but it is failing + Given This step is failing + """ + And a file named "features/step_definitions/cucumber_steps.js" with: + """ + var cucumberSteps = function() { + this.Given(/^This step is failing$/, function(callback) { callback.fail(); }); + }; + module.exports = cucumberSteps; + """ + When I run `cucumber.js -f json` + Then it should output this json: + """ + [ + { + "id": "some-feature", + "name": "some feature", + "description": "", + "line": 1, + "keyword": "Feature", + "uri":"/path/to/sandbox/tmp/cucumber-js-sandbox/features/a.feature", + "elements": [ + { + "name": "I've declaired one step but it is failing", + "id": "some-feature;i've-declaired-one-step-but-it-is-failing", + "line": 3, + "keyword": "Scenario", + "description": "", + "type": "scenario", + "steps": [ + { + "name": "This step is failing", + "line": 4, + "keyword": "Given ", + "result": { + "error_message": "ERROR_MESSAGE", + "status": "failed" + }, + "match": { + } + } + ] + } + ] + } + ] + """ + Scenario: output JSON for a feature with one scenario with passing step + Given a file named "features/a.feature" with: + """ + Feature: some feature + + Scenario: I've declaired one step which passes + Given This step is passing + """ + And a file named "features/step_definitions/cucumber_steps.js" with: + """ + var cucumberSteps = function() { + this.Given(/^This step is passing$/, function(callback) { callback(); }); + }; + module.exports = cucumberSteps; + """ + When I run `cucumber.js -f json` + Then it should output this json: + """ + [ + { + "id": "some-feature", + "name": "some feature", + "description": "", + "line": 1, + "keyword": "Feature", + "uri":"/path/to/sandbox/tmp/cucumber-js-sandbox/features/a.feature", + "elements": [ + { + "name": "I've declaired one step which passes", + "id": "some-feature;i've-declaired-one-step-which-passes", + "line": 3, + "keyword": "Scenario", + "description": "", + "type": "scenario", + "steps": [ + { + "name": "This step is passing", + "line": 4, + "keyword": "Given ", + "result": { + "status": "passed" + }, + "match": { + } + } + ] + } + ] + } + ] + """ + + Scenario: output JSON for a scenario with a passing step follwed by one that is pending and one that fails + Given a file named "features/a.feature" with: + """ + Feature: some feature + + Scenario: I've declaired one step which is passing, one pending and one failing. + Given This step is passing + And This step is pending + And This step fails but will be skipped + """ + And a file named "features/step_definitions/cucumber_steps.js" with: + """ + var cucumberSteps = function() { + this.Given(/^This step is passing$/, function(callback) { callback(); }); + this.Given(/^This step is pending$/, function(callback) { callback.pending(); }); + this.Given(/^This step fails but will be skipped$/, function(callback) { callback.fail(); }); + }; + module.exports = cucumberSteps; + """ + When I run `cucumber.js -f json` + Then it should output this json: + """ + [ + { + "id": "some-feature", + "name": "some feature", + "description": "", + "line": 1, + "keyword": "Feature", + "uri": "/path/to/sandbox/tmp/cucumber-js-sandbox/features/a.feature", + "elements": [ + { + "name": "I've declaired one step which is passing, one pending and one failing.", + "id": "some-feature;i've-declaired-one-step-which-is-passing,-one-pending-and-one-failing.", + "line": 3, + "keyword": "Scenario", + "description": "", + "type": "scenario", + "steps": [ + { + "name": "This step is passing", + "line": 4, + "keyword": "Given ", + "result": { + "status": "passed" + }, + "match": { + } + }, + { + "name": "This step is pending", + "line": 5, + "keyword": "And ", + "result": { + "status": "pending" + }, + "match": { + } + }, + { + "name": "This step fails but will be skipped", + "line": 6, + "keyword": "And ", + "result": { + "status": "skipped" + }, + "match": { + } + } + ] + } + ] + } + ] + """ + + Scenario: output JSON for a scenario with a pending step follwed by one that passes and one that fails + Given a file named "features/a.feature" with: + """ + Feature: some feature + + Scenario: I've declaired one step which is passing, one pending and one failing. + Given This step is pending + And This step is passing but will be skipped + And This step fails but will be skipped + """ + And a file named "features/step_definitions/cucumber_steps.js" with: + """ + var cucumberSteps = function() { + this.Given(/^This step is pending$/, function(callback) { callback.pending(); }); + this.Given(/^This step is passing but will be skipped$/, function(callback) { callback(); }); + this.Given(/^This step fails but will be skipped$/, function(callback) { callback.fail(); }); + }; + module.exports = cucumberSteps; + """ + When I run `cucumber.js -f json` + Then it should output this json: + """ + [ + { + "id": "some-feature", + "name": "some feature", + "description": "", + "line": 1, + "keyword": "Feature", + "uri": "/path/to/sandbox/tmp/cucumber-js-sandbox/features/a.feature", + "elements": [ + { + "name": "I've declaired one step which is passing, one pending and one failing.", + "id": "some-feature;i've-declaired-one-step-which-is-passing,-one-pending-and-one-failing.", + "line": 3, + "keyword": "Scenario", + "description": "", + "type": "scenario", + "steps": [ + { + "name": "This step is pending", + "line": 4, + "keyword": "Given ", + "result": { + "status": "pending" + }, + "match": { + } + }, + { + "name": "This step is passing but will be skipped", + "line": 5, + "keyword": "And ", + "result": { + "status": "skipped" + }, + "match": { + } + }, + { + "name": "This step fails but will be skipped", + "line": 6, + "keyword": "And ", + "result": { + "status": "skipped" + }, + "match": { + } + } + ] + } + ] + } + ] + """ + + Scenario: output JSON for one feature, one passing scenario, one failing scenario + Given a file named "features/a.feature" with: + """ + Feature: one passes one fails + + Scenario: This one passes + Given This step is passing + Scenario: This one fails + Given This step is failing + """ + And a file named "features/step_definitions/cucumber_steps.js" with: + """ + var cucumberSteps = function() { + this.Given(/^This step is passing$/, function(callback) { callback(); }); + this.Given(/^This step is failing$/, function(callback) { callback.fail(); }); + }; + module.exports = cucumberSteps; + """ + When I run `cucumber.js -f json` + Then it should output this json: + """ + [ + { + "id": "one-passes one fails", + "name": "one passes one fails", + "description": "", + "line": 1, + "keyword": "Feature", + "uri": "/path/to/sandbox/tmp/cucumber-js-sandbox/features/a.feature", + "elements": [ + { + "name": "This one passes", + "id": "one-passes one fails;this-one-passes", + "line": 3, + "keyword": "Scenario", + "description": "", + "type": "scenario", + "steps": [ + { + "name": "This step is passing", + "line": 4, + "keyword": "Given ", + "result": { + "status": "passed" + }, + "match": { + } + } + ] + }, + { + "name": "This one fails", + "id": "one-passes one fails;this-one-fails", + "line": 5, + "keyword": "Scenario", + "description": "", + "type": "scenario", + "steps": [ + { + "name": "This step is failing", + "line": 6, + "keyword": "Given ", + "result": { + "error_message": "ERROR_MESSAGE", + "status": "failed" + }, + "match": { + } + } + ] + } + ] + } + ] + """ + Scenario: output JSON for multiple features + Given a file named "features/a.feature" with: + """ + Feature: feature a + + Scenario: This is the first feature + Given This step is passing + """ + And a file named "features/b.feature" with: + """ + Feature: feature b + + Scenario: This is the second feature + Given This step is passing + """ + And a file named "features/c.feature" with: + """ + Feature: feature c + + Scenario: This is the third feature + Given This step is passing + """ + And a file named "features/step_definitions/cucumber_steps.js" with: + """ + var cucumberSteps = function() { + this.Given(/^This step is passing$/, function(callback) { callback(); }); + }; + module.exports = cucumberSteps; + """ + When I run `cucumber.js -f json` + Then it should output this json: + """ + [ + { + "id": "feature-a", + "name": "feature a", + "description": "", + "line": 1, + "keyword": "Feature", + "uri": "/path/to/sandbox/tmp/cucumber-js-sandbox/features/a.feature", + "elements": [ + { + "name": "This is the first feature", + "id": "feature-a;this-is-the-first-feature", + "line": 3, + "keyword": "Scenario", + "description": "", + "type": "scenario", + "steps": [ + { + "name": "This step is passing", + "line": 4, + "keyword": "Given ", + "result": { + "status": "passed" + }, + "match": { + } + } + ] + } + ] + }, + { + "id": "feature-b", + "name": "feature b", + "description": "", + "line": 1, + "keyword": "Feature", + "uri": "/path/to/sandbox/tmp/cucumber-js-sandbox/features/b.feature", + "elements": [ + { + "name": "This is the second feature", + "id": "feature-b;this-is-the-second-feature", + "line": 3, + "keyword": "Scenario", + "description": "", + "type": "scenario", + "steps": [ + { + "name": "This step is passing", + "line": 4, + "keyword": "Given ", + "result": { + "status": "passed" + }, + "match": { + } + } + ] + } + ] + }, + { + "id": "feature-c", + "name": "feature c", + "description": "", + "line": 1, + "keyword": "Feature", + "uri": "/path/to/sandbox/tmp/cucumber-js-sandbox/features/c.feature", + "elements": [ + { + "name": "This is the third feature", + "id": "feature-c;this-is-the-third-feature", + "line": 3, + "keyword": "Scenario", + "description": "", + "type": "scenario", + "steps": [ + { + "name": "This step is passing", + "line": 4, + "keyword": "Given ", + "result": { + "status": "passed" + }, + "match": { + } + } + ] + } + ] + } + ] + """ + Scenario: output JSON for multiple features each with multiple scenarios + Given a file named "features/a.feature" with: + """ + Feature: feature a + + Scenario: This is the feature a scenario one + Given This step is passing + + Scenario: This is the feature a scenario two + Given This step is passing + + Scenario: This is the feature a scenario three + Given This step is passing + """ + And a file named "features/b.feature" with: + """ + Feature: feature b + + Scenario: This is the feature b scenario one + Given This step is passing + + Scenario: This is the feature b scenario two + Given This step is passing + + Scenario: This is the feature b scenario three + Given This step is passing + """ + And a file named "features/c.feature" with: + """ + Feature: feature c + + Scenario: This is the feature c scenario one + Given This step is passing + + Scenario: This is the feature c scenario two + Given This step is passing + + Scenario: This is the feature c scenario three + Given This step is passing + """ + And a file named "features/step_definitions/cucumber_steps.js" with: + """ + var cucumberSteps = function() { + this.Given(/^This step is passing$/, function(callback) { callback(); }); + }; + module.exports = cucumberSteps; + """ + When I run `cucumber.js -f json` + Then it should output this json: + """ + [ + { + "id": "feature-a", + "name": "feature a", + "description": "", + "line": 1, + "keyword": "Feature", + "uri": "/path/to/sandbox/tmp/cucumber-js-sandbox/features/a.feature", + "elements": [ + { + "name": "This is the feature a scenario one", + "id": "feature-a;this-is-the-feature-a-scenario-one", + "line": 3, + "keyword": "Scenario", + "description": "", + "type": "scenario", + "steps": [ + { + "name": "This step is passing", + "line": 4, + "keyword": "Given ", + "result": { + "status": "passed" + }, + "match": { + } + } + ] + }, + { + "name": "This is the feature a scenario two", + "id": "feature-a;this-is-the-feature-a-scenario-two", + "line": 6, + "keyword": "Scenario", + "description": "", + "type": "scenario", + "steps": [ + { + "name": "This step is passing", + "line": 7, + "keyword": "Given ", + "result": { + "status": "passed" + }, + "match": { + } + } + ] + }, + { + "name": "This is the feature a scenario three", + "id": "feature-a;this-is-the-feature-a-scenario-three", + "line": 9, + "keyword": "Scenario", + "description": "", + "type": "scenario", + "steps": [ + { + "name": "This step is passing", + "line": 10, + "keyword": "Given ", + "result": { + "status": "passed" + }, + "match": { + } + } + ] + } + ] + }, + { + "id": "feature-b", + "name": "feature b", + "description": "", + "line": 1, + "keyword": "Feature", + "uri": "/path/to/sandbox/tmp/cucumber-js-sandbox/features/b.feature", + "elements": [ + { + "name": "This is the feature b scenario one", + "id": "feature-b;this-is-the-feature-b-scenario-one", + "line": 3, + "keyword": "Scenario", + "description": "", + "type": "scenario", + "steps": [ + { + "name": "This step is passing", + "line": 4, + "keyword": "Given ", + "result": { + "status": "passed" + }, + "match": { + } + } + ] + }, + { + "name": "This is the feature b scenario two", + "id": "feature-b;this-is-the-feature-b-scenario-two", + "line": 6, + "keyword": "Scenario", + "description": "", + "type": "scenario", + "steps": [ + { + "name": "This step is passing", + "line": 7, + "keyword": "Given ", + "result": { + "status": "passed" + }, + "match": { + } + } + ] + }, + { + "name": "This is the feature b scenario three", + "id": "feature-b;this-is-the-feature-b-scenario-three", + "line": 9, + "keyword": "Scenario", + "description": "", + "type": "scenario", + "steps": [ + { + "name": "This step is passing", + "line": 10, + "keyword": "Given ", + "result": { + "status": "passed" + }, + "match": { + } + } + ] + } + ] + }, + { + "id": "feature-c", + "name": "feature c", + "description": "", + "line": 1, + "keyword": "Feature", + "uri": "/path/to/sandbox/tmp/cucumber-js-sandbox/features/c.feature", + "elements": [ + { + "name": "This is the feature c scenario one", + "id": "feature-c;this-is-the-feature-c-scenario-one", + "line": 3, + "keyword": "Scenario", + "description": "", + "type": "scenario", + "steps": [ + { + "name": "This step is passing", + "line": 4, + "keyword": "Given ", + "result": { + "status": "passed" + }, + "match": { + } + } + ] + }, + { + "name": "This is the feature c scenario two", + "id": "feature-c;this-is-the-feature-c-scenario-two", + "line": 6, + "keyword": "Scenario", + "description": "", + "type": "scenario", + "steps": [ + { + "name": "This step is passing", + "line": 7, + "keyword": "Given ", + "result": { + "status": "passed" + }, + "match": { + } + } + ] + }, + { + "name": "This is the feature c scenario three", + "id": "feature-c;this-is-the-feature-c-scenario-three", + "line": 9, + "keyword": "Scenario", + "description": "", + "type": "scenario", + "steps": [ + { + "name": "This step is passing", + "line": 10, + "keyword": "Given ", + "result": { + "status": "passed" + }, + "match": { + } + } + ] + } + ] + } + ] + """ + + Scenario: output JSON for a feature with a background + Given a file named "features/a.feature" with: + """ + Feature: some feature + + Background: + Given This applies to all scenarios + """ + And a file named "features/step_definitions/cucumber_steps.js" with: + """ + var cucumberSteps = function() { + this.Given(/^This applies to all scenarios$/, function(callback) { callback(); }); + }; + module.exports = cucumberSteps; + """ + When I run `cucumber.js -f json` + Then it should output this json: + """ + [ + { + "id": "some-feature", + "name": "some feature", + "description": "", + "line": 1, + "keyword": "Feature", + "uri": "/path/to/sandbox/tmp/cucumber-js-sandbox/features/a.feature", + "elements": [ + { + "name": "", + "keyword": "Background", + "description": "", + "type": "background", + "line": 3, + "steps": [ + { + "name": "This applies to all scenarios", + "line": 4, + "keyword": "Given " + } + ] + } + ] + } + ] + """ + + Scenario: output JSON for a feature with a failing background + # Since the background step is re-evaluated for each scenario that is where the result of the step is currently recorded in the JSON output + # If the background is being reevaluated for each scenario then it would be misleading to only output the result for the first time it was evaluated. + + Given a file named "features/a.feature" with: + """ + Feature: some feature + + Background: + Given This applies to all scenarios but fails + """ + And a file named "features/step_definitions/cucumber_steps.js" with: + """ + var cucumberSteps = function() { + this.Given(/^This applies to all scenarios but fails$/, function(callback) { callback.fail(); }); + }; + module.exports = cucumberSteps; + """ + When I run `cucumber.js -f json` + Then it should output this json: + """ + [ + { + "id": "some-feature", + "name": "some feature", + "description": "", + "line": 1, + "keyword": "Feature", + "uri": "/path/to/sandbox/tmp/cucumber-js-sandbox/features/a.feature", + "elements": [ + { + "name": "", + "keyword": "Background", + "description": "", + "type": "background", + "line": 3, + "steps": [ + { + "name": "This applies to all scenarios but fails", + "line": 4, + "keyword": "Given " + } + ] + } + ] + } + ] + """ + + Scenario: output JSON for a feature with a DocString + Given a file named "features/a.feature" with: + """ + Feature: some feature + + Scenario: Scenario with DocString + Given we have this DocString: + \"\"\" + This is a DocString + \"\"\" + """ + And a file named "features/step_definitions/cucumber_steps.js" with: + """ + var cucumberSteps = function() { + this.Given(/^we have this DocString:$/, function(string, callback) { callback(); }); + }; + module.exports = cucumberSteps; + """ + When I run `cucumber.js -f json` + Then it should output this json: + """ + [ + { + "id": "some-feature", + "name": "some feature", + "description": "", + "line": 1, + "keyword": "Feature", + "uri": "/path/to/sandbox/tmp/cucumber-js-sandbox/features/a.feature", + "elements": [ + { + "name": "Scenario with DocString", + "id": "some-feature;scenario-with-docstring", + "line": 3, + "keyword": "Scenario", + "description": "", + "type": "scenario", + "steps": [ + { + "name": "we have this DocString:", + "line": 4, + "keyword": "Given ", + "doc_string": + { + "value": "This is a DocString", + "line": 5, + "content_type": "" + }, + "result": { + "status": "passed" + }, + "match": { + } + } + ] + } + ] + } + ] + """ + + Scenario: output JSON for background step with a DocString + Given a file named "features/a.feature" with: + """ + Feature: some feature + + Background: Background with DocString + Given we have this DocString: + \"\"\" + This is a DocString + \"\"\" + """ + And a file named "features/step_definitions/cucumber_steps.js" with: + """ + var cucumberSteps = function() { + this.Given(/^we have this DocString:$/, function(string, callback) { callback(); }); + }; + module.exports = cucumberSteps; + """ + When I run `cucumber.js -f json` + Then it should output this json: + """ + [ + { + "id": "some-feature", + "name": "some feature", + "description": "", + "line": 1, + "keyword": "Feature", + "uri": "/path/to/sandbox/tmp/cucumber-js-sandbox/features/a.feature", + "elements": [ + { + "name": "Background with DocString", + "keyword": "Background", + "description": "", + "type": "background", + "line": 3, + "steps": [ + { + "name": "we have this DocString:", + "line": 4, + "keyword": "Given ", + "doc_string": { + "value": "This is a DocString", + "line": 5, + "content_type": "" + } + } + ] + } + ] + } + ] + """ + + Scenario: output JSON for a feature with tags + + Given a file named "features/a.feature" with: + """ + @alpha @beta @gamma + Feature: some feature + + Scenario: This scenario has no tags + Given This step is passing + """ + And a file named "features/step_definitions/cucumber_steps.js" with: + """ + var cucumberSteps = function() { + this.Given(/^This step is passing$/, function(callback) { callback(); }); + }; + module.exports = cucumberSteps; + """ + When I run `cucumber.js -f json` + Then it should output this json: + """ + [ + { + "id": "some-feature", + "name": "some feature", + "description": "", + "line": 2, + "keyword": "Feature", + "tags": [ + { + "name": "@alpha", + "line": 1 + }, + { + "name": "@beta", + "line": 1 + }, + { + "name": "@gamma", + "line": 1 + } + ], + "uri": "/path/to/sandbox/tmp/cucumber-js-sandbox/features/a.feature", + "elements": [ + { + "name": "This scenario has no tags", + "id": "some-feature;this-scenario-has-no-tags", + "line": 4, + "keyword": "Scenario", + "description": "", + "type": "scenario", + "steps": [ + { + "name": "This step is passing", + "line": 5, + "keyword": "Given ", + "result": { + "status": "passed" + }, + "match": { + } + } + ] + } + ] + } + ] + """ + + Scenario: output JSON for scenario with tags + + Given a file named "features/a.feature" with: + """ + Feature: some feature + + @one @two @three + Scenario: This scenario has tags + Given This step is passing + """ + And a file named "features/step_definitions/cucumber_steps.js" with: + """ + var cucumberSteps = function() { + this.Given(/^This step is passing$/, function(callback) { callback(); }); + }; + module.exports = cucumberSteps; + """ + When I run `cucumber.js -f json` + Then it should output this json: + """ + [ + { + "id": "some-feature", + "name": "some feature", + "description": "", + "line": 1, + "keyword": "Feature", + "uri": "/path/to/sandbox/tmp/cucumber-js-sandbox/features/a.feature", + "elements": [ + { + "name": "This scenario has tags", + "id": "some-feature;this-scenario-has-tags", + "line": 4, + "keyword": "Scenario", + "description": "", + "type": "scenario", + "tags": [ + { + "name": "@one", + "line": 3 + }, + { + "name": "@two", + "line": 3 + }, + { + "name": "@three", + "line": 3 + } + ], + "steps": [ + { + "name": "This step is passing", + "line": 5, + "keyword": "Given ", + "result": { + "status": "passed" + }, + "match": { + } + } + ] + } + ] + } + ] + """ + + Scenario: output JSON for a step with table + # Rows do not appear to support line attribute yet. + Given a file named "features/a.feature" with: + """ + Feature: some feature + + Scenario: This scenario contains a step with a table + Given This table: + |col 1|col 2|col 3| + |one |two |three| + |1 |2 |3 | + |! |~ |@ | + """ + And a file named "features/step_definitions/cucumber_steps.js" with: + """ + var cucumberSteps = function() { + this.Given(/^This table:$/, function(table, callback) { callback(); }); + }; + module.exports = cucumberSteps; + """ + When I run `cucumber.js -f json` + Then it should output this json: + """ + [ + { + "id": "some-feature", + "name": "some feature", + "description": "", + "line": 1, + "keyword": "Feature", + "uri": "/path/to/sandbox/tmp/cucumber-js-sandbox/features/a.feature", + "elements": [ + { + "name": "This scenario contains a step with a table", + "id": "some-feature;this-scenario-contains-a-step-with-a-table", + "line": 3, + "keyword": "Scenario", + "description": "", + "type": "scenario", + "steps": [ + { + "name": "This table:", + "line": 4, + "keyword": "Given ", + "rows": [ + { + "cells": [ + "col 1", + "col 2", + "col 3" + ] + }, + { + "cells": [ + "one", + "two", + "three" + ] + }, + { + "cells": [ + "1", + "2", + "3" + ] + }, + { + "cells": [ + "!", + "~", + "@" + ] + } + ], + "result": { + "status": "passed" + }, + "match": { + } + } + ] + } + ] + } + ] + """ + + Scenario: output JSON for background with table + # Rows do not appear to support line attribute yet. + Given a file named "features/a.feature" with: + """ + Feature: some feature + + Background: + Given This table: + |col 1|col 2|col 3| + |one |two |three| + |1 |2 |3 | + |! |~ |@ | + """ + And a file named "features/step_definitions/cucumber_steps.js" with: + """ + var cucumberSteps = function() { + this.Given(/^This table:$/, function(table, callback) { callback(); }); + }; + module.exports = cucumberSteps; + """ + When I run `cucumber.js -f json` + Then it should output this json: + """ + [ + { + "id": "some-feature", + "name": "some feature", + "description": "", + "line": 1, + "keyword": "Feature", + "uri": "/path/to/sandbox/tmp/cucumber-js-sandbox/features/a.feature", + "elements": [ + { + "name": "", + "keyword": "Background", + "description": "", + "type": "background", + "line": 3, + "steps": [ + { + "name": "This table:", + "line": 4, + "keyword": "Given ", + "rows": [ + { + "cells": [ + "col 1", + "col 2", + "col 3" + ] + }, + { + "cells": [ + "one", + "two", + "three" + ] + }, + { + "cells": [ + "1", + "2", + "3" + ] + }, + { + "cells": [ + "!", + "~", + "@" + ] + } + ] + } + ] + } + ] + } + ] + """ \ No newline at end of file diff --git a/features/step_definitions/cli_steps.js b/features/step_definitions/cli_steps.js index c731abdf3..ee1bd855d 100644 --- a/features/step_definitions/cli_steps.js +++ b/features/step_definitions/cli_steps.js @@ -8,7 +8,7 @@ var cliSteps = function cliSteps() { var tmpDir = baseDir + "/tmp/cucumber-js-sandbox"; var cleansingNeeded = true; - var lastRun; + var lastRun = { error: null, stdout: "", stderr: "" }; function tmpPath(path) { return (tmpDir + "/" + path); @@ -64,8 +64,60 @@ var cliSteps = function cliSteps() { this.Then(/^it should pass with:$/, function(expectedOutput, callback) { var actualOutput = lastRun['stdout']; + + var actualError = lastRun['error']; + var actualStderr = lastRun['stderr']; + if (actualOutput.indexOf(expectedOutput) == -1) - throw new Error("Expected output to match the following:\n'" + expectedOutput + "'\nGot:\n'" + actualOutput + "'."); + throw new Error("Expected output to match the following:\n'" + expectedOutput + "'\nGot:\n'" + actualOutput + "'.\n" + + "Error:\n'" + actualError + "'.\n" + + "stderr:\n'" + actualStderr +"'."); + callback(); + }); + + this.Given(/^CUCUMBER_JS_HOME environment variable has been set to the cucumber\-js install dir$/, function(callback) { + // This is needed to allow us to check the error_message produced for a failed steps which includes paths which + // contain the location where cucumber-js is installed. + if (process.env.CUCUMBER_JS_HOME) { + callback(); + } else { + callback.fail(new Error("CUCUMBER_JS_HOME has not been set.")); + } + }); + + this.Then(/^it should output this json:$/, function(expectedOutput, callback) { + var actualOutput = lastRun['stdout']; + + var actualError = lastRun['error']; + var actualStderr = lastRun['stderr']; + + try { + var actualJson = JSON.parse(actualOutput); + } + catch(err) { + throw new Error("Error parsing actual JSON:\n" + actualOutput); + } + + try { + var expectedJson = JSON.parse(expectedOutput); + } + catch(err) { + throw new Error("Error parsing expected JSON:\n" + expectedOutput); + } + + var actualJsonString = JSON.stringify(actualJson, null, 2); + + // remove path to sandbox from uris + actualJsonString = actualJsonString.replace(/(\"uri\"\:\ \")(.+?)(\/tmp\/cucumber-js-sandbox\/)/g, "$1/path/to/sandbox$3"); + // remove location specific error messages + actualJsonString = actualJsonString.replace(/(\"error_message\"\:\ \")(.+?)(\"\,)/g, "$1ERROR_MESSAGE$3"); + + var expectedJsonString = JSON.stringify(expectedJson, null, 2); + + if (actualJsonString != expectedJsonString) + throw new Error("Expected output to match the following:\n'" + expectedJsonString + "'\nGot:\n'" + actualJsonString + "'.\n" + + "Error:\n'" + actualError + "'.\n" + + "stderr:\n'" + actualStderr +"'."); callback(); }); diff --git a/lib/cucumber/ast/feature.js b/lib/cucumber/ast/feature.js index d2077a912..30048d8ca 100644 --- a/lib/cucumber/ast/feature.js +++ b/lib/cucumber/ast/feature.js @@ -1,4 +1,4 @@ -var Feature = function(keyword, name, description, uri, line) { + var Feature = function(keyword, name, description, uri, line) { var Cucumber = require('../../cucumber'); var background; @@ -26,6 +26,14 @@ var Feature = function(keyword, name, description, uri, line) { return line; }, + getUri: function getUri() { + if (!uri) { + return undefined; + } else { + return uri; + } + }, + addBackground: function addBackground(newBackground) { background = newBackground; }, diff --git a/lib/cucumber/cli.js b/lib/cucumber/cli.js index a69da1b0f..2413e1910 100644 --- a/lib/cucumber/cli.js +++ b/lib/cucumber/cli.js @@ -59,11 +59,11 @@ var Cli = function(argv) { \n\ -f, --format FORMAT How to format features (default: progress).\n\ Available formats:\n\ -\n\ - pretty : prints the feature as is\n\ - progress: prints one character per scenario\n\ - summary : prints a summary only, after all\n\ - scenarios were executed\n\ + pretty : prints the feature as is\n\ + progress : prints one character per scenario\n\ + json : Prints the feature as JSON\n\ + summary : prints a summary only, after all\n\ + scenarios were executed\n\ \n\ -v, --version Display Cucumber.js's version.\n\ \n\ diff --git a/lib/cucumber/cli/argument_parser.js b/lib/cucumber/cli/argument_parser.js index 9f9c17ee0..e9c283b79 100644 --- a/lib/cucumber/cli/argument_parser.js +++ b/lib/cucumber/cli/argument_parser.js @@ -112,11 +112,11 @@ ArgumentParser.FEATURE_FILENAME_REGEXP = /[\/\\][^\/\\]+\.feature$/i; ArgumentParser.LONG_OPTION_PREFIX = "--"; ArgumentParser.REQUIRE_OPTION_NAME = "require"; ArgumentParser.REQUIRE_OPTION_SHORT_NAME = "r"; -ArgumentParser.TAGS_OPTION_NAME = "tags"; -ArgumentParser.TAGS_OPTION_SHORT_NAME = "t"; ArgumentParser.FORMAT_OPTION_NAME = "format"; ArgumentParser.FORMAT_OPTION_SHORT_NAME = "f"; ArgumentParser.DEFAULT_FORMAT_VALUE = "progress"; +ArgumentParser.TAGS_OPTION_NAME = "tags"; +ArgumentParser.TAGS_OPTION_SHORT_NAME = "t"; ArgumentParser.HELP_FLAG_NAME = "help"; ArgumentParser.HELP_FLAG_SHORT_NAME = "h"; ArgumentParser.DEFAULT_HELP_FLAG_VALUE = false; diff --git a/lib/cucumber/cli/configuration.js b/lib/cucumber/cli/configuration.js index dca494e57..0c82d6003 100644 --- a/lib/cucumber/cli/configuration.js +++ b/lib/cucumber/cli/configuration.js @@ -15,6 +15,9 @@ var Configuration = function(argv) { case Configuration.PRETTY_FORMAT_NAME: formatter = Cucumber.Listener.PrettyFormatter(); break; + case Configuration.JSON_FORMAT_NAME: + formatter = Cucumber.Listener.JsonFormatter(); + break; case Configuration.SUMMARY_FORMAT_NAME: formatter = Cucumber.Listener.SummaryFormatter(); break; @@ -63,10 +66,12 @@ var Configuration = function(argv) { var isVersionRequested = argumentParser.isVersionRequested(); return isVersionRequested; } + }; return self; }; Configuration.PRETTY_FORMAT_NAME = "pretty"; Configuration.PROGRESS_FORMAT_NAME = "progress"; +Configuration.JSON_FORMAT_NAME = "json"; Configuration.SUMMARY_FORMAT_NAME = "summary"; module.exports = Configuration; diff --git a/lib/cucumber/listener.js b/lib/cucumber/listener.js index 09f9082df..bf04e36c8 100644 --- a/lib/cucumber/listener.js +++ b/lib/cucumber/listener.js @@ -36,6 +36,7 @@ Listener.EVENT_HANDLER_NAME_SUFFIX = 'Event'; Listener.Formatter = require('./listener/formatter'); Listener.PrettyFormatter = require('./listener/pretty_formatter'); Listener.ProgressFormatter = require('./listener/progress_formatter'); +Listener.JsonFormatter = require('./listener/json_formatter'); Listener.StatsJournal = require('./listener/stats_journal'); Listener.SummaryFormatter = require('./listener/summary_formatter'); module.exports = Listener; diff --git a/lib/cucumber/listener/json_formatter.js b/lib/cucumber/listener/json_formatter.js new file mode 100644 index 000000000..7351bef85 --- /dev/null +++ b/lib/cucumber/listener/json_formatter.js @@ -0,0 +1,157 @@ +var JsonFormatter = function() { + + var GherkinJsonFormatter = require('gherkin/lib/gherkin/formatter/json_formatter'); + var gherkinJsonFormatter = new GherkinJsonFormatter(process.stdout); + + var currentFeatureId = 'undefined'; + + var Cucumber = require('../../cucumber'); + + var self = Cucumber.Listener(); + + var parentFeatureTags; + + self.getGherkinFormatter = function() { + return gherkinJsonFormatter; + } + + self.formatStep = function formatStep(step) { + + var stepProperties = {name: step.getName(), line: step.getLine(), keyword: step.getKeyword()}; + if (step.hasDocString()) { + var docString = step.getDocString(); + stepProperties['doc_string'] = {value: docString.getContents(), line: docString.getLine(), content_type: docString.getContentType()}; + } + if (step.hasDataTable()) { + var tableContents = step.getDataTable().getContents(); + var raw = tableContents.raw(); + var tableProperties = []; + for (rawRow in raw) { + var row = {line: undefined, cells: raw[rawRow]}; + tableProperties.push(row); + } + stepProperties['rows'] = tableProperties; + } + + gherkinJsonFormatter.step(stepProperties); + + } + + self.formatTags = function formatTags(tags, parentTags) { + var tagsProperties = []; + for (tag in tags) { + var isParentTag = false; + for (parentTag in parentTags) { + if ((tags[tag].getName() == parentTags[parentTag].getName()) && (tags[tag].getLine() == parentTags[parentTag].getLine())) { + isParentTag = true; + } + } + if (!isParentTag) { + tagsProperties.push({name: tags[tag].getName(), line: tags[tag].getLine()}); + } + } + return tagsProperties; + } + + self.handleBeforeFeatureEvent = function handleBeforeFeatureEvent(event, callback) { + + var feature = event.getPayloadItem('feature'); + currentFeatureId = feature.getName().replace(' ','-'); + + gherkinJsonFormatter.uri(feature.getUri()); + + var featureProperties = {id: currentFeatureId, + name: feature.getName(), + description: feature.getDescription(), + line: feature.getLine(), + keyword: feature.getKeyword()} + + var tags = feature.getTags(); + if (tags.length > 0) { + featureProperties['tags'] = self.formatTags(tags, []); + } + + gherkinJsonFormatter.feature(featureProperties); + + parentFeatureTags = tags; + + callback(); + } + + self.handleBackgroundEvent = function handleBackgroundEvent(event, callback) { + var background = event.getPayloadItem('background'); + gherkinJsonFormatter.background({name: background.getName(), keyword: "Background", description: background.getDescription(), type: 'background', line: background.getLine()}) + var steps = background.getSteps(); + steps.forEach(function(value, index, ar) { self.formatStep(value); }); + callback(); + } + + self.handleBeforeScenarioEvent = function handleBeforeScenarioEvent(event, callback) { + + var scenario = event.getPayloadItem('scenario'); + + var id = currentFeatureId + ';' + scenario.getName().replace(/ /g, '-').toLowerCase(); + var scenarioProperties = {name: scenario.getName(), id: id, line: scenario.getLine(), keyword: 'Scenario', description: scenario.getDescription(), type: 'scenario'}; + + var tags = scenario.getTags(); + if (tags.length > 0) { + var tagsProperties = self.formatTags(tags, parentFeatureTags); + if (tagsProperties.length > 0) { + scenarioProperties['tags'] = tagsProperties; + } + } + + gherkinJsonFormatter.scenario(scenarioProperties); + + callback(); + } + + self.handleStepResultEvent = function handleStepResult(event, callback) { + var stepResult = event.getPayloadItem('stepResult'); + + var step = stepResult.getStep(); + self.formatStep(step); + + var stepOutput = {}; + var resultStatus = 'failed'; + + if (stepResult.isSuccessful()) { + resultStatus = 'passed'; + } + else if (stepResult.isPending()) { + resultStatus = 'pending'; + stepOutput['error_message'] = undefined; + } + else if (stepResult.isSkipped()) { + resultStatus = 'skipped'; + } + else if (stepResult.isUndefined()) { + resultStatus = 'undefined'; + } + else { + var failureMessage = stepResult.getFailureException(); + if (failureMessage) { + stepOutput['error_message'] = (failureMessage.stack || failureMessage); + } + } + + stepOutput['status'] = resultStatus; + + gherkinJsonFormatter.result(stepOutput); + gherkinJsonFormatter.match({location: undefined}); + callback(); + } + + self.handleAfterFeaturesEvent = function handleAfterFeaturesEvent(event, callback) { + + gherkinJsonFormatter.eof(); + gherkinJsonFormatter.done(); + + callback(); + } + + return self; +}; + +module.exports = JsonFormatter; + diff --git a/package.json b/package.json index 7771cef91..a998a0f3b 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "node": "0.6 || 0.7 || 0.8" }, "dependencies": { - "gherkin": "2.11.0", + "gherkin": "2.11.1", "jasmine-node": "1.0.26", "connect": "2.3.2", "browserify": "1.13.2", diff --git a/spec/cucumber/cli/argument_parser_spec.js b/spec/cucumber/cli/argument_parser_spec.js index ed165bf6a..de9ce1f4d 100644 --- a/spec/cucumber/cli/argument_parser_spec.js +++ b/spec/cucumber/cli/argument_parser_spec.js @@ -78,6 +78,12 @@ describe("Cucumber.Cli.ArgumentParser", function () { var knownOptionDefinitions = argumentParser.getKnownOptionDefinitions(); expect(knownOptionDefinitions[Cucumber.Cli.ArgumentParser.VERSION_FLAG_NAME]).toEqual(Boolean); }); + + it("defines an --output option to specify output format", function() { + var knownOptionDefinitions = argumentParser.getKnownOptionDefinitions(); + expect(knownOptionDefinitions[Cucumber.Cli.ArgumentParser.FORMAT_OPTION_NAME]).toEqual(String); + }); + }); describe("getShortenedOptionDefinitions()", function () { @@ -109,6 +115,7 @@ describe("Cucumber.Cli.ArgumentParser", function () { var shortenedOptionDefinitions = argumentParser.getShortenedOptionDefinitions(); expect(shortenedOptionDefinitions[aliasName]).toEqual(aliasValue); }); + }); describe("getFeatureFilePaths()", function () { diff --git a/spec/cucumber/cli/configuration_spec.js b/spec/cucumber/cli/configuration_spec.js index 93334a9a7..d6175bfde 100644 --- a/spec/cucumber/cli/configuration_spec.js +++ b/spec/cucumber/cli/configuration_spec.js @@ -261,4 +261,20 @@ describe("Cucumber.Cli.Configuration", function () { expect(configuration.isVersionRequested()).toBe(isVersionRequested); }); }); + + describe("getFormatter()", function() { + beforeEach(function() { + spyOnStub(argumentParser, 'getFormat').andReturn("progress"); + }); + + it("asks the argument parser which format we should be outputting", function() { + configuration.getFormatter(); + expect(argumentParser.getFormat).toHaveBeenCalled(); + }); +// +// it("returns the corresponding formatter for us to use", function() { +// // TODO: Fill this out when my head is in a better place +// }); + }); + }); diff --git a/spec/cucumber/listener/json_formatter_spec.js b/spec/cucumber/listener/json_formatter_spec.js new file mode 100644 index 000000000..a2afbb0f2 --- /dev/null +++ b/spec/cucumber/listener/json_formatter_spec.js @@ -0,0 +1,473 @@ +require('../../support/spec_helper'); + +describe("Cucumber.Listener.JsonFormatterWrapper", function() { + var Cucumber = requireLib('cucumber'); + var listener, failedStepResults; + + beforeEach(function() { + spyOn(process.stdout, 'write'); // prevent actual output during spec execution + listener = Cucumber.Listener.JsonFormatter(process.stdout); + formatter = listener.getGherkinFormatter(); + + spyOn(formatter, 'uri'); + spyOn(formatter, 'feature'); + spyOn(formatter, 'step'); + spyOn(formatter, 'background'); + spyOn(formatter, 'scenario'); + spyOn(formatter, 'result'); + spyOn(formatter, 'match'); + spyOn(formatter, 'eof'); + spyOn(formatter, 'done'); + + }); + + // Handle Feature + + describe("handleBeforeFeatureEvent()", function() { + var event, feature, callback; + + beforeEach(function() { + feature = createSpyWithStubs("feature", + {getKeyword: 'Feature', + getName: 'A Name', + getDescription: 'A Description', + getLine: 3, + getUri: undefined, + getTags: false}); + + event = createSpyWithStubs("event", {getPayloadItem: feature}); + + callback = createSpy("callback"); + }); + + it("adds the feature attributes to the output", function() { + listener.handleBeforeFeatureEvent(event, callback); + expect(formatter.uri).toHaveBeenCalledWith(undefined); + expect(formatter.feature).toHaveBeenCalledWith({id: 'A-Name', + name: 'A Name', + description: 'A Description', + line: 3, + keyword: 'Feature'}); + + }); + + }); + + // Handle Background + + describe("handleBackgroundEvent()", function() { + + var parent_feature_event, background, step, steps, event, callback; + + beforeEach(function() { + feature = createSpyWithStubs("feature", + {getKeyword: 'Feature', + getName: 'A Name', + getDescription: 'A Description', + getLine: 3, + getUri: 'feature-uri', + getTags: false}); + + parent_feature_event = createSpyWithStubs("event", {getPayloadItem: feature}); + + step = createSpyWithStubs("step", { + getName: 'Step', + getLine: 3, + getKeyword: 'Step', + hasDocString: false, + hasDataTable: false + }); + + steps = [step]; + + background = createSpyWithStubs("background", + {getKeyword: 'Background', + getName: 'A Name', + getDescription: 'A Description', + getLine: 3, + getSteps: steps}); + + event = createSpyWithStubs("event", {getPayloadItem: background}); + callback = createSpy("callback"); + }); + + it("adds the background attributes to the output", function() { + listener.handleBackgroundEvent(event, callback); + expect(formatter.background).toHaveBeenCalledWith({name: 'A Name', + keyword: 'Background', + description: 'A Description', + type: 'background', + line: 3 }); + }); + + }); + + // Handle Scenario + + describe("handleBeforeScenarioEvent()", function() { + var parent_feature_event, scenario, callback; + + beforeEach(function() { + feature = createSpyWithStubs("feature", + {getKeyword: 'Feature', + getName: 'A Name', + getDescription: 'A Description', + getLine: 3, + getUri: 'feature-uri', + getTags: false}); + + parent_feature_event = createSpyWithStubs("event", {getPayloadItem: feature}); + + scenario = createSpyWithStubs("scenario", + {getKeyword: 'Scenario', + getName: 'A Name', + getDescription: 'A Description', + getLine: 3, + getTags: false}); + + event = createSpyWithStubs("event", {getPayloadItem: scenario}); + callback = createSpy("callback"); + }); + + it("adds the scenario attributes to the output", function() { + listener.handleBeforeScenarioEvent(event, callback); + expect(formatter.scenario).toHaveBeenCalledWith({name: 'A Name', + id: 'undefined;a-name', + line: 3, + keyword: 'Scenario', + description: 'A Description', + type: 'scenario' }); + }); + + }); + + // Step Formatting + + describe("formatStep()", function() { + + it("adds name, line and keyword to the step properties", function(){ + + var step = createSpyWithStubs("step", { + getName: 'Step', + getLine: 3, + getKeyword: 'Step', + hasDocString: false, + hasDataTable: false + }); + + listener.formatStep(step); + expect(formatter.step).toHaveBeenCalledWith({ name : 'Step', line : 3, keyword : 'Step'}); + + }); + + it("if the step has one, adds a DocString to the step properties", function(){ + + var fakeDocString = createSpyWithStubs("docString", { + getContents: "This is a DocString", + getLine: 3, + getContentType: null}); + + var step = createSpyWithStubs("step", { + getName: 'Step', + getLine: 3, + getKeyword: 'Step', + hasDocString: true, + hasDataTable: false, + getDocString: fakeDocString + }); + + listener.formatStep(step); + expect(formatter.step).toHaveBeenCalledWith({name: 'Step', + line: 3, + keyword: 'Step', + doc_string: {value: 'This is a DocString', line: 3, content_type: null} + }); + + }); + + it("if the step has one, adds a DataTable to the step properties", function(){ + + var fakeContents = createSpyWithStubs("row", { + raw: [['a:1', 'a:2', 'a:3'],['b:1', 'b:2', 'b:3'],['c:1', 'c:2', 'c:3']] + }) + + var fakeDataTable = createSpyWithStubs("dataTable", { + getContents: fakeContents + }); + + var step = createSpyWithStubs("step", { + getName: 'Step', + getLine: 3, + getKeyword: 'Step', + hasDocString: false, + hasDataTable: true, + getDataTable: fakeDataTable + }); + + listener.formatStep(step); + expect(formatter.step).toHaveBeenCalledWith({name: 'Step', + line: 3, + keyword: 'Step', + rows: [{line : undefined, cells: ['a:1', 'a:2', 'a:3'] }, + {line : undefined, cells: ['b:1', 'b:2', 'b:3'] }, + {line : undefined, cells: ['c:1', 'c:2', 'c:3'] }] + }); + }); + + }); + + // Tag Formatting + + describe("formatTags()", function() { + + it("returns the given tags in the format expected by the JSON formatter", function(){ + + var tags = [createSpyWithStubs("tag", {getName: "tag_one", getLine:1}), + createSpyWithStubs("tag", {getName: "tag_two", getLine:2}), + createSpyWithStubs("tag", {getName: "tag_three", getLine:3})]; + + expect(listener.formatTags(tags, null)).toEqual([{name: 'tag_one', line :1}, + {name: 'tag_two', line :2}, + {name: 'tag_three', line :3}]); + + }); + + it("filters out any tags it is told to ignore - e.g. those of the parent feature", function(){ + + var tags = [createSpyWithStubs("tag", {getName: "tag_one", getLine:1}), + createSpyWithStubs("tag", {getName: "tag_two", getLine:2}), + createSpyWithStubs("tag", {getName: "parent_one", getLine:3}), + createSpyWithStubs("tag", {getName: "parent_two", getLine:3})]; + + var parent_tags = [createSpyWithStubs("tag", {getName: "parent_one", getLine:3}), + createSpyWithStubs("tag", {getName: "parent_two", getLine:3})]; + + + expect(listener.formatTags(tags, parent_tags)).toEqual([{name: 'tag_one', line :1}, + {name: 'tag_two', line :2}]); + }); + + }); + + // Handle Step Results + + describe("handleStepResultEvent()", function() { + var parent_feature_event, feature, parent_scenario_event, scenario, event, callback, stepResult; + + beforeEach(function() { + callback = createSpy("Callback"); + }); + + it("outputs a step with failed status where no result has been defined", function(){ + + step = createSpyWithStubs("step", { + getName: 'Step', + getLine: 3, + getKeyword: 'Step', + hasDocString: false, + hasDataTable: false + }); + + stepResult = createSpyWithStubs("stepResult", { + isSuccessful: undefined, + isPending: undefined, + isFailed: undefined, + isSkipped: undefined, + isUndefined: undefined, + getFailureException: false, + getStep: step + }); + + fakeEvent = createSpyWithStubs("event", {getPayloadItem: stepResult}); + + listener.handleStepResultEvent(fakeEvent, callback); + + expect(formatter.step).toHaveBeenCalledWith({name: 'Step', line: 3, keyword: 'Step'}); + expect(formatter.result).toHaveBeenCalledWith({status: 'failed'}); + expect(formatter.match).toHaveBeenCalledWith({location: undefined}); + + }); + + it("outputs a step with passed status for a successful step", function(){ + + step = createSpyWithStubs("step", { + getName: 'Step', + getLine: 3, + getKeyword: 'Step', + hasDocString: false, + hasDataTable: false + }); + + stepResult = createSpyWithStubs("stepResult", { + isSuccessful: undefined, + isPending: undefined, + isFailed: undefined, + isSkipped: undefined, + isUndefined: undefined, + getFailureException: false, + getStep: step + }); + + stepResult.isSuccessful.andReturn(true); + fakeEvent = createSpyWithStubs("event", {getPayloadItem: stepResult}); + + listener.handleStepResultEvent(fakeEvent, callback); + + expect(formatter.step).toHaveBeenCalledWith({name: 'Step', line: 3, keyword: 'Step'}); + expect(formatter.result).toHaveBeenCalledWith({status: 'passed'}); + expect(formatter.match).toHaveBeenCalledWith({location: undefined}); + + }); + + it("outputs a step with pending status where step is pending", function(){ + + step = createSpyWithStubs("step", { + getName: 'Step', + getLine: 3, + getKeyword: 'Step', + hasDocString: false, + hasDataTable: false + }); + + stepResult = createSpyWithStubs("stepResult", { + isSuccessful: undefined, + isPending: undefined, + isFailed: undefined, + isSkipped: undefined, + isUndefined: undefined, + getFailureException: false, + getStep: step + }); + + stepResult.isPending.andReturn(true); + fakeEvent = createSpyWithStubs("event", {getPayloadItem: stepResult}); + + listener.handleStepResultEvent(fakeEvent, callback); + + expect(formatter.step).toHaveBeenCalledWith({name: 'Step', line: 3, keyword: 'Step'}); + expect(formatter.result).toHaveBeenCalledWith({status: 'pending', error_message: undefined}); + expect(formatter.match).toHaveBeenCalledWith({location: undefined}); + + }); + + it("outputs a step with failed status where step fails", function(){ + + step = createSpyWithStubs("step", { + getName: 'Step', + getLine: 3, + getKeyword: 'Step', + hasDocString: false, + hasDataTable: false + }); + + stepResult = createSpyWithStubs("stepResult", { + isSuccessful: undefined, + isPending: undefined, + isFailed: undefined, + isSkipped: undefined, + isUndefined: undefined, + getFailureException: false, + getStep: step + }); + + stepResult.isFailed.andReturn(true); + fakeEvent = createSpyWithStubs("event", {getPayloadItem: stepResult}); + + listener.handleStepResultEvent(fakeEvent, callback); + + expect(formatter.step).toHaveBeenCalledWith({name: 'Step', line: 3, keyword: 'Step'}); + expect(formatter.result).toHaveBeenCalledWith({status: 'failed'}); + expect(formatter.match).toHaveBeenCalledWith({location: undefined}); + + }); + + it("outputs a step with skipped status where step should be skipped", function(){ + + step = createSpyWithStubs("step", { + getName: 'Step', + getLine: 3, + getKeyword: 'Step', + hasDocString: false, + hasDataTable: false + }); + + stepResult = createSpyWithStubs("stepResult", { + isSuccessful: undefined, + isPending: undefined, + isFailed: undefined, + isSkipped: undefined, + isUndefined: undefined, + getFailureException: false, + getStep: step + }); + + stepResult.isSkipped.andReturn(true); + fakeEvent = createSpyWithStubs("event", {getPayloadItem: stepResult}); + + listener.handleStepResultEvent(fakeEvent, callback); + + expect(formatter.step).toHaveBeenCalledWith({name: 'Step', line: 3, keyword: 'Step'}); + expect(formatter.result).toHaveBeenCalledWith({status: 'skipped'}); + expect(formatter.match).toHaveBeenCalledWith({location: undefined}); + + }); + + it("outputs a step with undefined status where step is undefined", function(){ + + step = createSpyWithStubs("step", { + getName: 'Step', + getLine: 3, + getKeyword: 'Step', + hasDocString: false, + hasDataTable: false + }); + + stepResult = createSpyWithStubs("stepResult", { + isSuccessful: undefined, + isPending: undefined, + isFailed: undefined, + isSkipped: undefined, + isUndefined: undefined, + getFailureException: false, + getStep: step + }); + + stepResult.isUndefined.andReturn(true); + fakeEvent = createSpyWithStubs("event", {getPayloadItem: stepResult}); + + listener.handleStepResultEvent(fakeEvent, callback); + + expect(formatter.step).toHaveBeenCalledWith({name: 'Step', line: 3, keyword: 'Step'}); + expect(formatter.result).toHaveBeenCalledWith({status: 'undefined'}); + expect(formatter.match).toHaveBeenCalledWith({location: undefined}); + + }); + + }); + + // We're all done. Output the JSON. + + describe("handleAfterFeaturesEvent()", function() { + var features, callback; + + beforeEach(function() { + event = createSpy("Event"); + callback = createSpy("Callback"); + + }); + + it("finalises output", function() { + listener.handleAfterFeaturesEvent(event, callback); + expect(formatter.eof).toHaveBeenCalled(); + expect(formatter.done).toHaveBeenCalled(); + }); + + it("calls back", function() { + listener.handleAfterFeaturesEvent(event, callback); + expect(callback).toHaveBeenCalled(); + }); + + }); + +}); + diff --git a/spec/tmp.txt b/spec/tmp.txt new file mode 100644 index 000000000..b1511927b --- /dev/null +++ b/spec/tmp.txt @@ -0,0 +1,728 @@ +{ + "name": "", + "id": "" +} +Cucumber.Listener.JsonFormatter + + constructor + creates a collection to store the failed steps + + hear() + checks wether there is a handler for the event + + when there is a handler for that event + gets the handler for that event + calls the handler with the event and the callback + does not callback + + when there are no handlers for that event + calls back + does not get the handler for the event + + hasHandlerForEvent + builds the name of the handler for that event + + when the handler exists + returns true + + when the handler does not exist + returns false + + buildHandlerNameForEvent + gets the name of the event + returns the name of the event with prefix 'handle' and suffix 'Event' + + getHandlerForEvent() + gets the name of the handler for the event + + when an event handler exists for the event + returns the event handler + + when no event handlers exist for the event + returns nothing + + handleStepResultEvent() + gets the step result from the event payload + checks wether the step was successful or not + + when the step passed + handles the successful step result + + when the step did not pass + does not handle a successful step result + checks wether the step is pending + + when the step was pending + handles the pending step result + + when the step was not pending + does not handle a pending step result + checks wether the step was skipped + + when the step was skipped + handles the skipped step result + + when the step was not skipped + does not handle a skipped step result + checks wether the step was undefined + + when the step was undefined + handles the undefined step result + + when the step was not undefined + does not handle a skipped step result + handles a failed step result + calls back + + handleSuccessfulStepResult() + witnesses a passed step + + handlePendingStepResult() + witnesses a pending step + marks the current scenario as pending + + handleSkippedStepResult() + counts one more skipped step + + handleUndefinedStepResult() + gets the step from the step result + stores the undefined step + witnesses an undefined step + marks the current scenario as undefined + + handleFailedStepResult() + stores the failed step result + witnesses a failed step + marks the current scenario as failing + + handleBeforeScenarioEvent + prepares for a new scenario + calls back + + handleAfterFeaturesEvent() + calls back + + handleAfterScenarioEvent() + checks wether the current scenario failed + + when the current scenario failed + witnesses a failed scenario + gets the scenario from the payload + stores the failed scenario + + when the current scenario did not fail + checks wether the current scenario is undefined + + when the current scenario is undefined + witnesses an undefined scenario + + when the current scenario is not undefined + checks wether the current scenario is pending + + when the current scenario is pending + witnesses a pending scenario + + when the current scenario is not pending (passed) + witnesses a passed scenario + calls back + + isCurrentScenarioFailing() + returns false when the current scenario did not fail yet + returns true when a step in the current scenario failed + + isCurrentScenarioPending() + returns false when the current scenario was not set pending yet + returns true when the current scenario was set pending + + isCurrentScenarioUndefined() + returns false when the current scenario was not set undefined yet + returns true when the current scenario was set undefined + + prepareBeforeScenario() + unmarks the current scenario as pending + unmarks the current scenario as failing + unmarks the current scenario as undefined + + storeFailedStepResult() + adds the result to the failed step result collection + + storeFailedScenario() + gets the name of the scenario + gets the line of the scenario + appends the scenario details to the failed scenario log buffer + + storeUndefinedStep() + creates a new step definition snippet builder + builds the step definition + appends the snippet to the undefined step log buffer + + getFailedScenarioLogBuffer() [appendStringToFailedScenarioLogBuffer()] + returns the logged failed scenario details + returns all logged failed scenario lines joined with a line break + + getUndefinedStepLogBuffer() [appendStringToUndefinedStepLogBuffer()] + returns the logged undefined step details + returns all logged failed scenario lines joined with a line break + + appendStringToUndefinedStepLogBuffer() [getUndefinedStepLogBuffer()] + does not log the same string twice + + getScenarioCount() + gets the number of passed scenarios + gets the number of undefined scenarios + gets the number of pending scenarios + gets the number of failed scenarios + returns the sum of passed, undefined, pending aand failed scenarios + + getStepCount() + gets the number of passed steps + gets the number of undefined steps + gets the number of skipped steps + gets the number of pending steps + gets the number of failed steps + returns the sum of passed steps and failed steps + + passed scenario counting + + witnessPassedScenario() + counts one more passed scenario + + getPassedScenarioCount() + returns 0 when no scenario passed + returns 1 when one scenario passed + returns 2 when two scenarios passed + returns 3 when three scenarios passed + + undefined scenario counting + + getUndefinedScenarioCount() + returns 0 when no scenarios undefined + returns 1 when one scenario passed + returns 2 when two scenarios passed + returns 3 when two scenarios passed + + pending scenario counting + + getPendingScenarioCount() + returns 0 when no scenarios pending + returns 1 when one scenario passed + returns 2 when two scenarios passed + returns 3 when two scenarios passed + + failed scenario counting + + getFailedScenarioCount() + returns 0 when no scenarios failed + returns 1 when one scenario passed + returns 2 when two scenarios passed + returns 3 when two scenarios passed + + passed step counting + + witnessPassedStep() + counts one more passed step + + getPassedStepCount() + returns 0 when no step passed + returns 1 when one step passed + returns 2 when two steps passed + returns 3 when three steps passed + + failed step counting + + getFailedStepCount() + returns 0 when no steps failed + returns 1 when one step passed + returns 2 when two steps passed + returns 3 when two steps passed + + skipped step counting + + getSkippedStepCount() + returns 0 when no steps skipped + returns 1 when one step passed + returns 2 when two steps passed + returns 3 when two steps passed + + undefined step counting + + getUndefinedStepCount() + returns 0 when no steps undefined + returns 1 when one step passed + returns 2 when two steps passed + returns 3 when two steps passed + + pending step counting + + getPendingStepCount() + returns 0 when no steps pending + returns 1 when one step passed + returns 2 when two steps passed + returns 3 when two steps passed + + witnessedAnyFailedStep() + returns false when no failed step were encountered + returns true when one or more steps were witnessed + + witnessedAnyUndefinedStep() + returns false when no undefined step were encountered + returns true when one or more steps were witnessed + +Cucumber.Listener.ProgressFormatter + + constructor + creates a collection to store the failed steps + + log() + records logged strings + outputs the logged string to STDOUT by default + + when asked to output to STDOUT + outputs the logged string to STDOUT + + when asked to not output to STDOUT + does not output anything to STDOUT + + when asked to output to a function + calls the function with the logged string + + getLogs() + returns the logged buffer + returns an empty string when the listener did not log anything yet + + hear() + checks wether there is a handler for the event + + when there is a handler for that event + gets the handler for that event + calls the handler with the event and the callback + does not callback + + when there are no handlers for that event + calls back + does not get the handler for the event + + hasHandlerForEvent + builds the name of the handler for that event + + when the handler exists + returns true + + when the handler does not exist + returns false + + buildHandlerNameForEvent + gets the name of the event + returns the name of the event with prefix 'handle' and suffix 'Event' + + getHandlerForEvent() + gets the name of the handler for the event + + when an event handler exists for the event + returns the event handler + + when no event handlers exist for the event + returns nothing + + handleStepResultEvent() + gets the step result from the event payload + checks wether the step was successful or not + + when the step passed + handles the successful step result + + when the step did not pass + does not handle a successful step result + checks wether the step is pending + + when the step was pending + handles the pending step result + + when the step was not pending + does not handle a pending step result + checks wether the step was skipped + + when the step was skipped + handles the skipped step result + + when the step was not skipped + does not handle a skipped step result + checks wether the step was undefined + + when the step was undefined + handles the undefined step result + + when the step was not undefined + does not handle a skipped step result + handles a failed step result + calls back + + handleSuccessfulStepResult() + witnesses a passed step + logs the passing step character + + handlePendingStepResult() + witnesses a pending step + marks the current scenario as pending + logs the pending step character + + handleSkippedStepResult() + counts one more skipped step + logs the skipped step character + + handleUndefinedStepResult() + gets the step from the step result + stores the undefined step + witnesses an undefined step + marks the current scenario as undefined + logs the undefined step character + + handleFailedStepResult() + stores the failed step result + witnesses a failed step + marks the current scenario as failing + logs the failed step character + + handleBeforeScenarioEvent + prepares for a new scenario + calls back + + handleAfterFeaturesEvent() + displays a summary + calls back + + handleAfterScenarioEvent() + checks wether the current scenario failed + + when the current scenario failed + witnesses a failed scenario + gets the scenario from the payload + stores the failed scenario + + when the current scenario did not fail + checks wether the current scenario is undefined + + when the current scenario is undefined + witnesses an undefined scenario + + when the current scenario is not undefined + checks wether the current scenario is pending + + when the current scenario is pending + witnesses a pending scenario + + when the current scenario is not pending (passed) + witnesses a passed scenario + calls back + + isCurrentScenarioFailing() + returns false when the current scenario did not fail yet + returns true when a step in the current scenario failed + + isCurrentScenarioPending() + returns false when the current scenario was not set pending yet + returns true when the current scenario was set pending + + isCurrentScenarioUndefined() + returns false when the current scenario was not set undefined yet + returns true when the current scenario was set undefined + + prepareBeforeScenario() + unmarks the current scenario as pending + unmarks the current scenario as failing + unmarks the current scenario as undefined + + storeFailedStepResult() + adds the result to the failed step result collection + + storeFailedScenario() + gets the name of the scenario + gets the line of the scenario + appends the scenario details to the failed scenario log buffer + + storeUndefinedStep() + creates a new step definition snippet builder + builds the step definition + appends the snippet to the undefined step log buffer + + getFailedScenarioLogBuffer() [appendStringToFailedScenarioLogBuffer()] + returns the logged failed scenario details + returns all logged failed scenario lines joined with a line break + + getUndefinedStepLogBuffer() [appendStringToUndefinedStepLogBuffer()] + returns the logged undefined step details + returns all logged failed scenario lines joined with a line break + + appendStringToUndefinedStepLogBuffer() [getUndefinedStepLogBuffer()] + does not log the same string twice + + logSummary() + logs two line feeds + checks wether there are failed steps or not + + when there are failed steps + logs the failed steps + + when there are no failed steps + does not log failed steps + logs the scenarios summary + logs the steps summary + checks wether there are undefined steps or not + + when there are undefined steps + logs the undefined step snippets + + when there are no undefined steps + does not log the undefined step snippets + + logFailedStepResults() + logs a failed steps header + iterates synchronously over the failed step results + + for each failed step result + tells the visitor to visit the feature and call back when finished + logs a failed scenarios header + gets the failed scenario details from its log buffer + logs the failed scenario details + logs a line break + + logFailedStepResult() + gets the failure exception from the step result + + when the failure exception has a stack + logs the stack + + when the failure exception has no stack + logs the exception itself + logs two line breaks + + logScenariosSummary() + gets the number of scenarios + + when there are no scenarios + logs 0 scenarios + does not log any details + + when there are scenarios + + when there is one scenario + logs one scenario + + when there are 2 or more scenarios + logs two or more scenarios + gets the number of failed scenarios + + when there are no failed scenarios + does not log failed scenarios + + when there is one failed scenario + logs a failed scenario + + when there are two or more failed scenarios + logs the number of failed scenarios + gets the number of undefined scenarios + + when there are no undefined scenarios + does not log passed scenarios + + when there is one undefined scenario + logs one undefined scenario + + when there are two or more undefined scenarios + logs the undefined scenarios + gets the number of pending scenarios + + when there are no pending scenarios + does not log passed scenarios + + when there is one pending scenario + logs one pending scenario + + when there are two or more pending scenarios + logs the pending scenarios + gets the number of passed scenarios + + when there are no passed scenarios + does not log passed scenarios + + when there is one passed scenario + logs 1 passed scenarios + + when there are two or more passed scenarios + logs the number of passed scenarios + + logStepsSummary() + gets the number of steps + + when there are no steps + logs 0 steps + does not log any details + + when there are steps + + when there is one step + logs 1 step + + when there are two or more steps + logs the number of steps + gets the number of failed steps + + when there are no failed steps + does not log failed steps + + when there is one failed step + logs one failed step + + when there is two or more failed steps + logs the number of failed steps + gets the number of undefined steps + + when there are no undefined steps + does not log undefined steps + + when there is one undefined step + logs one undefined steps + + when there are two or more undefined steps + logs the number of undefined steps + gets the number of pending steps + + when there are no pending steps + does not log pending steps + + when there is one pending step + logs one pending steps + + when there are two or more pending steps + logs the number of pending steps + gets the number of skipped steps + + when there are no skipped steps + does not log skipped steps + + when there is one skipped step + logs one skipped steps + + when there are two or more skipped steps + logs the number of skipped steps + gets the number of passed steps + + when there are no passed steps + does not log passed steps + + when there is one passed step + logs one passed step + + when there is two or more passed steps + logs the number of passed steps + + logUndefinedStepSnippets() + logs a little explanation about the snippets + gets the undefined steps log buffer + logs the undefined steps + + getScenarioCount() + gets the number of passed scenarios + gets the number of undefined scenarios + gets the number of pending scenarios + gets the number of failed scenarios + returns the sum of passed, undefined, pending aand failed scenarios + + getStepCount() + gets the number of passed steps + gets the number of undefined steps + gets the number of skipped steps + gets the number of pending steps + gets the number of failed steps + returns the sum of passed steps and failed steps + + passed scenario counting + + witnessPassedScenario() + counts one more passed scenario + + getPassedScenarioCount() + returns 0 when no scenario passed + returns 1 when one scenario passed + returns 2 when two scenarios passed + returns 3 when three scenarios passed + + undefined scenario counting + + getUndefinedScenarioCount() + returns 0 when no scenarios undefined + returns 1 when one scenario passed + returns 2 when two scenarios passed + returns 3 when two scenarios passed + + pending scenario counting + + getPendingScenarioCount() + returns 0 when no scenarios pending + returns 1 when one scenario passed + returns 2 when two scenarios passed + returns 3 when two scenarios passed + + failed scenario counting + + getFailedScenarioCount() + returns 0 when no scenarios failed + returns 1 when one scenario passed + returns 2 when two scenarios passed + returns 3 when two scenarios passed + + passed step counting + + witnessPassedStep() + counts one more passed step + + getPassedStepCount() + returns 0 when no step passed + returns 1 when one step passed + returns 2 when two steps passed + returns 3 when three steps passed + + failed step counting + + getFailedStepCount() + returns 0 when no steps failed + returns 1 when one step passed + returns 2 when two steps passed + returns 3 when two steps passed + + skipped step counting + + getSkippedStepCount() + returns 0 when no steps skipped + returns 1 when one step passed + returns 2 when two steps passed + returns 3 when two steps passed + + undefined step counting + + getUndefinedStepCount() + returns 0 when no steps undefined + returns 1 when one step passed + returns 2 when two steps passed + returns 3 when two steps passed + + pending step counting + + getPendingStepCount() + returns 0 when no steps pending + returns 1 when one step passed + returns 2 when two steps passed + returns 3 when two steps passed + + witnessedAnyFailedStep() + returns false when no failed step were encountered + returns true when one or more steps were witnessed + + witnessedAnyUndefinedStep() + returns false when no undefined step were encountered + returns true when one or more steps were witnessed + +Finished in 0.095 seconds +338 tests, 339 assertions, 0 failures + +