diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..76edbb1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules +test/ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d0bcd29 --- /dev/null +++ b/Makefile @@ -0,0 +1,25 @@ +.PHONY: template test + +url = https://raw.github.com/mojombo/jekyll/master/features + +all: template generate test + +template: + @echo "... Build the template ...\n" + @node node_modules/hogan/bin/hulk body.mustache \ + | sed 's/{}/module.exports/' \ + | cat template/head.js - template/tail.js \ + > template/index.js + +generate: + @echo "... Build the test files, requesting some of jekyll features ...\n" + + node examples/stdout.js >> test/stdout.js + node examples/request.js $(url)/permalinks.feature >> test/permalinks.js + node examples/request.js $(url)/create_sites.feature >> test/create_sites.js + node examples/request.js $(url)/markdown.feature >> test/markdown.js + +test: + @echo "... Build the test files, requesting some of jekyll features ...\n" + node node_modules/.bin/mocha + diff --git a/body.mustache b/body.mustache new file mode 100644 index 0000000..fabbf51 --- /dev/null +++ b/body.mustache @@ -0,0 +1,25 @@ + +// +// Mocha generated tests +// + +describe("{{ feature.name }}", function() { + + describe("{{{ feature.desc }}}", function() { + {{#scenarios}} + + describe("{{{ name }}}", function() { + {{#steps}} + + it("{{{ name }}}", function() { + + }); + {{/steps}} + + }); + {{/scenarios}} + + }); + +}); + diff --git a/examples/basics.feature b/examples/basics.feature new file mode 100644 index 0000000..87e74ad --- /dev/null +++ b/examples/basics.feature @@ -0,0 +1,18 @@ +# features/myFeature.feature + +Feature: Example feature + As a user of cucumber.js + I want to have documentation on cucumber + So that I can concentrate on building awesome applications + + Scenario: Reading documentation + Given I am on the Cucumber.js Github repository + When I go to the README file + Then I should see "Usage" as the page title + + + Scenario: Reading the tests + Given I am in the text directory + When I read the index.js file + Then I should see "new Parser" somewhere + diff --git a/examples/jekyll.feature b/examples/jekyll.feature new file mode 100644 index 0000000..6935af3 --- /dev/null +++ b/examples/jekyll.feature @@ -0,0 +1,145 @@ +Feature: Site configuration + As a hacker who likes to blog + I want to be able to configure jekyll + In order to make setting up a site easier + + Scenario: Change destination directory + Given I have a blank site in "_sourcedir" + And I have an "_sourcedir/index.html" file that contains "Changing source directory" + And I have a configuration file with "source" set to "_sourcedir" + When I run jekyll + Then the _site directory should exist + And I should see "Changing source directory" in "_site/index.html" + + Scenario: Change destination directory + Given I have an "index.html" file that contains "Changing destination directory" + And I have a configuration file with "destination" set to "_mysite" + When I run jekyll + Then the _mysite directory should exist + And I should see "Changing destination directory" in "_mysite/index.html" + + Scenario: Exclude files inline + Given I have an "Rakefile" file that contains "I want to be excluded" + And I have an "README" file that contains "I want to be excluded" + And I have an "index.html" file that contains "I want to be included" + And I have a configuration file with "exclude" set to "Rakefile", "README" + When I run jekyll + Then I should see "I want to be included" in "_site/index.html" + And the "_site/Rakefile" file should not exist + And the "_site/README" file should not exist + + Scenario: Exclude files with YAML array + Given I have an "Rakefile" file that contains "I want to be excluded" + And I have an "README" file that contains "I want to be excluded" + And I have an "index.html" file that contains "I want to be included" + And I have a configuration file with "exclude" set to: + | value | + | README | + | Rakefile | + When I run jekyll + Then I should see "I want to be included" in "_site/index.html" + And the "_site/Rakefile" file should not exist + And the "_site/README" file should not exist + + Scenario: Use RDiscount for markup + Given I have an "index.markdown" page that contains "[Google](http://google.com)" + And I have a configuration file with "markdown" set to "rdiscount" + When I run jekyll + Then the _site directory should exist + And I should see "Google" in "_site/index.html" + + Scenario: Use Kramdown for markup + Given I have an "index.markdown" page that contains "[Google](http://google.com)" + And I have a configuration file with "markdown" set to "kramdown" + When I run jekyll + Then the _site directory should exist + And I should see "Google" in "_site/index.html" + + Scenario: Use Redcarpet for markup + Given I have an "index.markdown" page that contains "[Google](http://google.com)" + And I have a configuration file with "markdown" set to "redcarpet" + When I run jekyll + Then the _site directory should exist + And I should see "Google" in "_site/index.html" + + Scenario: Use Maruku for markup + Given I have an "index.markdown" page that contains "[Google](http://google.com)" + And I have a configuration file with "markdown" set to "maruku" + When I run jekyll + Then the _site directory should exist + And I should see "Google" in "_site/index.html" + + Scenario: Highlight code with pygments + Given I have an "index.html" file that contains "{% highlight ruby %} puts 'Hello world!' {% endhighlight %}" + And I have a configuration file with "pygments" set to "true" + When I run jekyll + Then the _site directory should exist + And I should see "puts 'Hello world!'" in "_site/index.html" + + Scenario: Set time and no future dated posts + Given I have a _layouts directory + And I have a page layout that contains "Page Layout: {{ site.posts.size }} on {{ site.time | date: "%Y-%m-%d" }}" + And I have a post layout that contains "Post Layout: {{ content }}" + And I have an "index.html" page with layout "page" that contains "site index page" + And I have a configuration file with: + | key | value | + | time | 2010-01-01 | + | future | false | + And I have a _posts directory + And I have the following posts: + | title | date | layout | content | + | entry1 | 12/31/2007 | post | content for entry1. | + | entry2 | 01/31/2020 | post | content for entry2. | + When I run jekyll + Then the _site directory should exist + And I should see "Page Layout: 1 on 2010-01-01" in "_site/index.html" + And I should see "Post Layout:

content for entry1.

" in "_site/2007/12/31/entry1.html" + And the "_site/2020/01/31/entry2.html" file should not exist + + Scenario: Set time and future dated posts allowed + Given I have a _layouts directory + And I have a page layout that contains "Page Layout: {{ site.posts.size }} on {{ site.time | date: "%Y-%m-%d" }}" + And I have a post layout that contains "Post Layout: {{ content }}" + And I have an "index.html" page with layout "page" that contains "site index page" + And I have a configuration file with: + | key | value | + | time | 2010-01-01 | + | future | true | + And I have a _posts directory + And I have the following posts: + | title | date | layout | content | + | entry1 | 12/31/2007 | post | content for entry1. | + | entry2 | 01/31/2020 | post | content for entry2. | + When I run jekyll + Then the _site directory should exist + And I should see "Page Layout: 2 on 2010-01-01" in "_site/index.html" + And I should see "Post Layout:

content for entry1.

" in "_site/2007/12/31/entry1.html" + And I should see "Post Layout:

content for entry2.

" in "_site/2020/01/31/entry2.html" + + Scenario: Limit the number of posts generated by most recent date + Given I have a _posts directory + And I have a configuration file with: + | key | value | + | limit_posts | 2 | + And I have the following posts: + | title | date | content | + | Apples | 3/27/2009 | An article about apples | + | Oranges | 4/1/2009 | An article about oranges | + | Bananas | 4/5/2009 | An article about bananas | + When I run jekyll + Then the _site directory should exist + And the "_site/2009/04/05/bananas.html" file should exist + And the "_site/2009/04/01/oranges.html" file should exist + And the "_site/2009/03/27/apples.html" file should not exist + + Scenario: Copy over normally excluded files when they are explicitly included + Given I have a ".gitignore" file that contains ".DS_Store" + And I have an ".htaccess" file that contains "SomeDirective" + And I have a configuration file with "include" set to: + | value | + | .gitignore | + | .foo | + When I run jekyll + Then the _site directory should exist + And I should see ".DS_Store" in "_site/.gitignore" + And the "_site/.htaccess" file should not exist diff --git a/examples/request.js b/examples/request.js new file mode 100644 index 0000000..b35775b --- /dev/null +++ b/examples/request.js @@ -0,0 +1,12 @@ + +var request = require('request'); + +var Parser = require('../'); + +var url = process.argv.slice(2)[0]; + +if(!url) process.exit(1); + +request(url) + .pipe(new Parser) + .pipe(process.stdout); diff --git a/examples/stdout.js b/examples/stdout.js new file mode 100644 index 0000000..20766f2 --- /dev/null +++ b/examples/stdout.js @@ -0,0 +1,16 @@ + +var fs = require('fs'), + path = require('path'), + Parser = require('..'); + +var args = process.argv.slice(2); + +// some test + +var feature = args[0] || 'basics.feature'; + +fs.createReadStream(path.join(__dirname, feature)) + .pipe(new Parser).on('close', console.log.bind(console)) + .pipe(process.stdout); + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..fd0f1f3 --- /dev/null +++ b/package.json @@ -0,0 +1,23 @@ +{ + "author": "", + "name": "mocha-gherkin", + "version": "0.0.0", + "repository": { + "url": "" + }, + "engines": { + "node": "~0.6.1" + }, + "dependencies": { + "gherkin": "~2.9.3" + }, + "devDependencies": { + "hogan": "~1.0.5-dev", + "mocha": "~1.0.1", + "request": "~2.9.202" + }, + "scripts": { + "test": "make" + }, + "main": "./parser" +} diff --git a/parser.js b/parser.js new file mode 100644 index 0000000..9e2a50a --- /dev/null +++ b/parser.js @@ -0,0 +1,140 @@ + +var fs = require('fs'), + path = require('path'), + util = require('util'), + stream = require('stream'), + Lexer = require('gherkin').Lexer('en'), + templates = require('./template'); + +// +// Basic gherkin feature -> mocha bdd test suite +// +// fs.createReadStream(something).pipe(new Parser).pipe(process.stdout); +// +// Basic gherkin to mocha mapping: +// +// - feature -> top-level describe() +// - scenario(s) -> inner describe(), might have one or more +// - step(s) -> it() +// + +module.exports = Parser; + +function Parser() { + this.readable = this.writable = true; + + this.indent = ''; + + this.steps = []; + this.features = []; + this.scenarios = []; + + this.chunks = []; + + this.lexer = new Lexer(this); + this.template = templates.body; + + stream.Stream.call(this); +} + +util.inherits(Parser, stream.Stream); + +Parser.prototype.parse = function(feature) { + this.lexer.scan(feature); +}; + +// +// Stream api +// + +Parser.prototype.write = function(chunk) { + // collect all the data before streaming out. + // should probably be done better by pausing / resuming as necessary + this.chunks = this.chunks.concat(chunk); +}; + +Parser.prototype.end = function() { + var feature = this.chunks.join(''); + this.parse(feature); + this.emit('data', this.mocha()); + this.emit('close'); +}; + +// +// Mocha output +// +// - feature -> top-level describe() +// - scenario(s) -> inner describe(), might have one or more +// - step(s) -> it() + +Parser.prototype.mocha = function() { + var steps = this.steps; + + var data = {}; + data.feature = this.features[0]; + data.feature.desc = data.feature.desc.replace(/\n/g, ' '); + data.scenarios = this.scenarios.map(function(scenario, i, arr) { + var ln = scenario.line, + last = i === arr.length - 1; + next = arr[i + 1] ? arr[i + 1].line : 0; + + scenario.steps = steps.filter(function(step) { + if(last) return step.line > ln; + return step.line > ln && step.line < next; + }); + + return scenario; + }); + + return this.template.render(data); +}; + +// +// Gherkin parser api +// + +Parser.prototype.background = function(keyword, name, desc, line) { + this.emit('background', name, desc, line); +}; + +Parser.prototype.comment = function(comment, line) { + this.emit('comment', comment, line); +}; + +Parser.prototype.doc_string = function(cotentType, string, line) { + this.emit('doc_string', contentType, string, line); +}; + +Parser.prototype.eof = function() { + this.emit('eof'); +}; + +Parser.prototype.feature = function(keyword, name, desc, line) { + this.emit('feature', name, desc, line); + + desc = desc.replace(/"/g, "'"); + this.features.push({ name: name, desc: desc, line: line }); +}; + +Parser.prototype.row = function(cells, line) { + // console.log('row>>', arguments); +}; + +Parser.prototype.scenario = function(keyword, name, desc, line) { + this.emit('scenario', name, desc, line); + + desc = desc.replace(/"/g, "'"); + this.scenarios.push({ name: name, desc: desc, line: line }); +}; + +Parser.prototype.step = function(keyword, name, line) { + this.emit('step', keyword, name, line); + this.emit(keyword, name, line); + + name = name.replace(/"/g, "'"); + this.steps.push({ step: keyword, name: keyword + name, line: line }); +}; + +Parser.prototype.tag = function(tag, line) { + this.emit('tag', tag, line); +}; diff --git a/template/head.js b/template/head.js new file mode 100644 index 0000000..5d0759e --- /dev/null +++ b/template/head.js @@ -0,0 +1,7 @@ + +var Hogan = require('hogan'); + +// +// hogan templates +// + diff --git a/template/index.js b/template/index.js new file mode 100644 index 0000000..de35198 --- /dev/null +++ b/template/index.js @@ -0,0 +1,11 @@ + +var Hogan = require('hogan'); + +// +// hogan templates +// + +var templates = module.exports; +templates.body = new Hogan.Template(function(c,p,i){var _=this;_.b(i=i||"");_.b("//");_.b("\n" + i);_.b("// Mocha generated tests");_.b("\n" + i);_.b("//");_.b("\n" + i);_.b("\n" + i);_.b("describe(\"");_.b(_.v(_.d("feature.name",c,p,0)));_.b("\", function() {");_.b("\n" + i);_.b("\n" + i);_.b(" describe(\"");_.b(_.t(_.d("feature.desc",c,p,0)));_.b("\", function() {");_.b("\n" + i);if(_.s(_.f("scenarios",c,p,1),c,p,0,143,284,"{{ }}")){_.rs(c,p,function(c,p,_){_.b("\n" + i);_.b(" describe(\"");_.b(_.t(_.f("name",c,p,0)));_.b("\", function() {");_.b("\n" + i);if(_.s(_.f("steps",c,p,1),c,p,0,203,260,"{{ }}")){_.rs(c,p,function(c,p,_){_.b("\n" + i);_.b(" it(\"");_.b(_.t(_.f("name",c,p,0)));_.b("\", function() {");_.b("\n" + i);_.b("\n" + i);_.b(" });");_.b("\n");});c.pop();}_.b("\n" + i);_.b(" });");_.b("\n");});c.pop();}_.b("\n" + i);_.b(" });");_.b("\n" + i);_.b("\n" + i);_.b("});");return _.fl();;}); + + diff --git a/template/tail.js b/template/tail.js new file mode 100644 index 0000000..139597f --- /dev/null +++ b/template/tail.js @@ -0,0 +1,2 @@ + + diff --git a/test/stdout.js b/test/stdout.js new file mode 100644 index 0000000..9984d8d --- /dev/null +++ b/test/stdout.js @@ -0,0 +1,86 @@ +// +// Mocha generated tests +// + +describe("Example feature", function() { + + describe("As a user of cucumber.js I want to have documentation on cucumber So that I can concentrate on building awesome applications", function() { + + describe("Reading documentation", function() { + + it("Given I am on the Cucumber.js Github repository", function() { + + }); + + it("When I go to the README file", function() { + + }); + + it("Then I should see 'Usage' as the page title", function() { + + }); + + }); + + describe("Reading the tests", function() { + + it("Given I am in the text directory", function() { + + }); + + it("When I read the index.js file", function() { + + }); + + it("Then I should see 'new Parser' somewhere", function() { + + }); + + }); + + }); + +}); +// +// Mocha generated tests +// + +describe("Example feature", function() { + + describe("As a user of cucumber.js I want to have documentation on cucumber So that I can concentrate on building awesome applications", function() { + + describe("Reading documentation", function() { + + it("Given I am on the Cucumber.js Github repository", function() { + + }); + + it("When I go to the README file", function() { + + }); + + it("Then I should see 'Usage' as the page title", function() { + + }); + + }); + + describe("Reading the tests", function() { + + it("Given I am in the text directory", function() { + + }); + + it("When I read the index.js file", function() { + + }); + + it("Then I should see 'new Parser' somewhere", function() { + + }); + + }); + + }); + +});