├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── README.md ├── bin └── cucumber ├── docs ├── classes.graffle └── classes.png ├── features ├── glue │ └── glue.js └── hello.feature ├── lib └── cucumber │ ├── boolean_expression_parser.js │ ├── glue.js │ ├── glue_loader.js │ ├── hook.js │ ├── pickle_loader.js │ ├── pretty_plugin.js │ ├── sequential_test_case_executor.js │ ├── source_reader.js │ ├── step_definition.js │ ├── tag_filter.js │ ├── test_case.js │ └── test_step.js ├── package.json ├── test-data ├── glue │ ├── hooks.js │ └── stepdefs.js └── hello.feature └── test └── cucumber ├── boolean_expression_parser_test.js ├── glue_loader_test.js ├── glue_test.js ├── hook_test.js ├── pickle_loader_test.js ├── pretty_plugin_test.js ├── sequential_test_case_executor_test.js ├── step_definition_test.js ├── test_case_test.js └── test_step_test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log 3 | .idea/ 4 | ./coverage/ 5 | coverage/ 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 5.0.0 3 | sudo: false 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Code coverage 2 | 3 | Just run the tests: 4 | 5 | npm test 6 | 7 | The build will fail if coverage drops below 100%. 8 | The report is in `coverage/lcov-report/index.html`. 9 | 10 | ## Counting Source Lines of Code (SLOC) 11 | 12 | See how big it is: 13 | 14 | npm run sloc 15 | 16 | Make sure it's not too big: 17 | 18 | npm run sloccheck 19 | 20 | ## Design philosophy 21 | 22 | * Single public method per class - makes it easier to translate to functional languages. Also good OO design (single responsibility). 23 | * Loosely coupled. Most classes only require 0 or 1 other classes. None require more than 2. 24 | 25 | ## TODO 26 | 27 | * [x] Support promises 28 | * [ ] Support callbacks 29 | * [ ] Support DocStrings and DataTables 30 | * [ ] Add feature source to exception backtrace 31 | * [x] Hooks 32 | * [ ] Make After Hooks run even if there is a failing pickle step 33 | * [ ] Tagged hooks 34 | * [ ] Progress formatter 35 | * [ ] Summary reporter: all errors and count summary. (skip all errors if using pretty formatter to same stream) 36 | * [ ] Pretty formatter (improve so it doesn't print filtered away stuff) 37 | * [ ] Reduce size 38 | * [ ] Remove toString from bool nodes 39 | * [ ] Add Markdown support in Gherkin3 and in pretty formatter 40 | * [ ] i18n stepdef API 41 | 42 | ## Maybe 43 | 44 | * --profile - if we do it, do it suites style 45 | * --order 46 | * Filter by line in tag/step/docstring/data table 47 | 48 | ## Out of scope 49 | 50 | * Print snippets 51 | * Dry run (who uses that anyway) 52 | * Turn off colour (--no-color) 53 | * --name filter (who needs that when we have line and tags) 54 | * --out - let's use plugin:out format instead 55 | * --i18n - refer to online docs instead 56 | * --fail-fast - nice feature, but not needed in microcuke 57 | * --init 58 | * --exclude - WTF didn't even know we had that 59 | * --no-multiline - more YAGNI 60 | * --strict 61 | * --guess STUPID 62 | * --expand STUPID TOO 63 | * --version 64 | * --help 65 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Microcuke 2 | 3 | [![Join the chat at https://gitter.im/cucumber/gherkin3](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/cucumber/microcuke?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 4 | [![Build Status](https://travis-ci.org/cucumber/microcuke.svg)](https://travis-ci.org/cucumber/microcuke) 5 | 6 | Microcuke is a tiny Cucumber implementation in 500 SLOC, based on 7 | [Gherkin3](https://github.com/cucumber/gherkin3). It's got 100% unit test coverage. 8 | 9 | The sole purpose of microcuke is to provide a very simple reference implementation that 10 | can be ported to a new programming language in a few days. Think of it as an aid to 11 | developers who wish to implement Cucumber for a new programming language. 12 | 13 | Microcuke is written in classic JavaScript (ES5), because that's a language most 14 | programmers can at least read. The source code is not heavily documented - 15 | we aim to write self-explanatory, simple code instead. If you find something 16 | hard to understand, that's a bug! Feel free to open a bug report. 17 | 18 | Most of Microcuke is written in synchronous JavaScript (for readability), but there are 19 | some parts that are asynchronous (using promises and callbacks). These constructs are 20 | fairly JavaScript-specific, so if you are using microcuke as a guid to write a Cucumber 21 | implementation for a new language, you should probably translate that code to simple synchronous code. 22 | 23 | Here is a high level class diagram to give you an idea: 24 | 25 | ![](https://github.com/cucumber/microcuke/blob/master/docs/classes.png) 26 | 27 | ## Try it out 28 | 29 | First, you need to install [Node.js](https://nodejs.org). 30 | Once you've done that you must install the libraries that microcuke depends on. 31 | 32 | npm install 33 | 34 | Now let's take it for a spin: 35 | 36 | ./bin/cucumber 37 | 38 | This will run the features under the `features` directory. 39 | -------------------------------------------------------------------------------- /bin/cucumber: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | var EventEmitter = require('events').EventEmitter; 3 | var Glue = require('../lib/cucumber/glue'); 4 | var GlueLoader = require('../lib/cucumber/glue_loader'); 5 | var PickleLoader = require('../lib/cucumber/pickle_loader'); 6 | var SequentialTestCaseExecutor = require('../lib/cucumber/sequential_test_case_executor'); 7 | var PrettyPlugin = require('../lib/cucumber/pretty_plugin'); 8 | var SourceReader = require('../lib/cucumber/source_reader'); 9 | var tagFilter = require('../lib/cucumber/tag_filter'); 10 | 11 | var argv = require('minimist')(process.argv.slice(2)); 12 | var featurePath = argv._[0] || 'features'; 13 | var gluePath = argv.glue || 'features'; // TODO: Derive from featurePath 14 | 15 | var glueLoader = new GlueLoader(); 16 | var glue = glueLoader.loadGlue(gluePath, function (stepDefinitions, hooks) { 17 | return new Glue(stepDefinitions, hooks); 18 | }); 19 | 20 | var filter = argv.tags ? tagFilter(argv.tags) : function () { 21 | return true; 22 | }; 23 | var pickleLoader = new PickleLoader(filter); 24 | var pickles = pickleLoader.loadPickles(featurePath); 25 | var testCases = pickles.map(glue.createTestCase); 26 | 27 | var executor = new SequentialTestCaseExecutor(testCases); 28 | var eventEmitter = new EventEmitter(); 29 | 30 | var plugin = new PrettyPlugin(process.stdout, new SourceReader()); 31 | plugin.subscribe(eventEmitter); 32 | 33 | var exitStatus = 0; 34 | eventEmitter.on('step-finished', function (step) { 35 | if (step.status === 'failed') exitStatus = 1; 36 | }); 37 | 38 | executor.execute(eventEmitter) 39 | .then(function () { 40 | process.exit(exitStatus); 41 | }) 42 | .catch(function (err) { 43 | console.err(err.stack); 44 | process.exit(1); 45 | }); 46 | 47 | -------------------------------------------------------------------------------- /docs/classes.graffle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cucumber-attic/microcuke/0d831da35d285b8400bcad0e03a71ff633062a0b/docs/classes.graffle -------------------------------------------------------------------------------- /docs/classes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cucumber-attic/microcuke/0d831da35d285b8400bcad0e03a71ff633062a0b/docs/classes.png -------------------------------------------------------------------------------- /features/glue/glue.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Given(/I (\w+) something/, function (what) { 5 | return Promise.resolve() 6 | }); 7 | 8 | Given(/something (\w+)/, function (what) { 9 | return Promise.resolve() 10 | }); 11 | -------------------------------------------------------------------------------- /features/hello.feature: -------------------------------------------------------------------------------- 1 | Feature: hello 2 | @foo 3 | Scenario: self test 4 | Given I have something 5 | And something else 6 | 7 | @bar 8 | Scenario: self test again 9 | Given I love something 10 | And something else -------------------------------------------------------------------------------- /lib/cucumber/boolean_expression_parser.js: -------------------------------------------------------------------------------- 1 | module.exports = function BooleanExpressionParser() { 2 | /** 3 | * Parses infix boolean expression (using Dijkstra's Shunting Yard algorithm) 4 | * and builds a tree of expressions. The root node of the expression is returned. 5 | * 6 | * This expression can be evaluated by passing in an array of literals that resolve to true 7 | */ 8 | this.parse = function (infix) { 9 | var tokens = infix.replace(/\(/g, ' ( ').replace(/\)/g, ' ) ').trim().split(/\s+/); 10 | var exprs = []; 11 | var ops = []; 12 | 13 | tokens.forEach(function (token) { 14 | if (isOp(token)) { 15 | while (ops.length > 0 && isOp(peek(ops)) && ( 16 | (ASSOC[token] === 'left' && PREC[token] <= PREC[peek(ops)]) 17 | || 18 | (ASSOC[token] === 'right' && PREC[token] < PREC[peek(ops)]) 19 | )) { 20 | pushExpr(pop(ops), exprs); 21 | } 22 | ops.push(token); 23 | } else if ('(' === token) { 24 | ops.push(token); 25 | } else if (')' === token) { 26 | while (ops.length > 0 && peek(ops) !== '(') { 27 | pushExpr(pop(ops), exprs); 28 | } 29 | if (ops.length === 0) { 30 | throw Error("Unclosed (") 31 | } 32 | if (peek(ops) === '(') { 33 | pop(ops); 34 | } 35 | } else { 36 | pushExpr(token, exprs); 37 | } 38 | }); 39 | 40 | while (ops.length > 0) { 41 | if (peek(ops) === '(') { 42 | throw Error("Unclosed (") 43 | } 44 | pushExpr(pop(ops), exprs); 45 | } 46 | 47 | var expr = pop(exprs); 48 | if (exprs.length > 0) { 49 | throw new Error("Not empty") 50 | } 51 | return expr; 52 | }; 53 | 54 | var ASSOC = { 55 | 'or': 'left', 56 | 'and': 'left', 57 | 'not': 'right' 58 | }; 59 | 60 | var PREC = { 61 | '(': -2, 62 | ')': -1, 63 | 'or': 0, 64 | 'and': 1, 65 | 'not': 2 66 | }; 67 | 68 | function isOp(token) { 69 | return ASSOC[token] !== undefined 70 | } 71 | 72 | function peek(stack) { 73 | return stack[stack.length - 1]; 74 | } 75 | 76 | function pop(stack) { 77 | if (stack.length === 0) throw new Error("empty stack"); 78 | return stack.pop(); 79 | } 80 | 81 | function pushExpr(token, stack) { 82 | if (token === 'and') { 83 | var rightAndExpr = pop(stack); 84 | stack.push(new And(pop(stack), rightAndExpr)); 85 | } else if (token === 'or') { 86 | var rightOrExpr = pop(stack); 87 | stack.push(new Or(pop(stack), rightOrExpr)); 88 | } else if (token === 'not') { 89 | stack.push(new Not(pop(stack))); 90 | } else { 91 | stack.push(new Literal(token)); 92 | } 93 | } 94 | 95 | function Literal(value) { 96 | this.evaluate = function (variables) { 97 | return variables.indexOf(value) !== -1; 98 | }; 99 | 100 | this.toString = function () { 101 | return value 102 | }; 103 | } 104 | 105 | function Or(leftExpr, rightExpr) { 106 | this.evaluate = function (variables) { 107 | return leftExpr.evaluate(variables) || rightExpr.evaluate(variables); 108 | }; 109 | 110 | this.toString = function () { 111 | return "( " + leftExpr.toString() + " or " + rightExpr.toString() + " )" 112 | }; 113 | } 114 | 115 | function And(leftExpr, rightExpr) { 116 | this.evaluate = function (variables) { 117 | return leftExpr.evaluate(variables) && rightExpr.evaluate(variables); 118 | }; 119 | 120 | this.toString = function () { 121 | return "( " + leftExpr.toString() + " and " + rightExpr.toString() + " )" 122 | }; 123 | } 124 | 125 | function Not(expr) { 126 | this.evaluate = function (variables) { 127 | return !expr.evaluate(variables); 128 | }; 129 | 130 | this.toString = function () { 131 | return "not ( " + expr.toString() + " )" 132 | }; 133 | } 134 | }; 135 | -------------------------------------------------------------------------------- /lib/cucumber/glue.js: -------------------------------------------------------------------------------- 1 | var TestCase = require('./test_case'); 2 | var TestStep = require('./test_step'); 3 | 4 | module.exports = function Glue(stepDefinitions, hooks) { 5 | this.createTestCase = function (pickle) { 6 | var testSteps = [] 7 | .concat(createHookSteps(pickle, 'before')) 8 | .concat(createPickleTestSteps(pickle)) 9 | .concat(createHookSteps(pickle, 'after')); 10 | return new TestCase(pickle, testSteps); 11 | }; 12 | 13 | function createHookSteps(pickle, scope) { 14 | return hooks.filter(function (hook) { 15 | return hook.scope === scope; // TODO: AND matches pickle's tags 16 | }).map(function (hook) { 17 | return hook.createTestStep(pickle); 18 | }); 19 | } 20 | 21 | function createPickleTestSteps(pickle) { 22 | return pickle.steps.map(function (pickleStep) { 23 | var testSteps = stepDefinitions.map(function (stepDefinition) { 24 | return stepDefinition.createTestStep(pickleStep); 25 | }).filter(function (testStep) { 26 | return testStep != null; 27 | }); 28 | 29 | switch (testSteps.length) { 30 | case 1: 31 | return testSteps[0]; 32 | case 0: 33 | // Create an undefined step (null bodyFn) 34 | return new TestStep(pickleStep.locations, [], null, null); 35 | default: 36 | throw new Error("Ambiguous match"); 37 | } 38 | }); 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /lib/cucumber/glue_loader.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var glob = require('glob').sync; 3 | var StepDefinition = require('./step_definition'); 4 | var Hook = require('./hook'); 5 | 6 | module.exports = function GlueLoader() { 7 | /** 8 | * Loads a Glue object from the gluePath. 9 | * The glueFactory is used to construct the Glue 10 | * object. 11 | * 12 | * How this is done is very different from language to language. 13 | * 14 | * Languages with closure/lambda/block/anonymous functions can define a DSL 15 | * similar to Microcuke - calling a global function passing in 16 | * a regexp and a block of code. 17 | * 18 | * Other languages will typically have to use regular fuctions, and annotate them. 19 | * 20 | * Loading the step definitions will require reflection or simply loading and executing 21 | * files in a particular path. 22 | */ 23 | this.loadGlue = function (gluePath, glueFactory) { 24 | var stepDefinitions = []; 25 | var hooks = []; 26 | 27 | var originalGlobals = {}; 28 | 29 | function registerGlobals(keywords, value) { 30 | keywords.forEach(function (keyword) { 31 | originalGlobals[keyword] = global[keyword]; 32 | global[keyword] = value; 33 | }); 34 | } 35 | 36 | function registerStepdef(regexp, bodyFn) { 37 | stepDefinitions.push(new StepDefinition(regexp, bodyFn, getLocationOfCaller())); 38 | } 39 | 40 | function registerBeforeHook(bodyFn) { 41 | hooks.push(new Hook(bodyFn, getLocationOfCaller(), "before")); 42 | } 43 | 44 | function registerAfterHook(bodyFn) { 45 | hooks.push(new Hook(bodyFn, getLocationOfCaller(), "after")); 46 | } 47 | 48 | registerGlobals(['Given', 'When', 'Then', 'And', 'But'], registerStepdef); 49 | registerGlobals(['Before'], registerBeforeHook); 50 | registerGlobals(['After'], registerAfterHook); 51 | 52 | glob(gluePath + "/**/*.js").forEach(function (glueFilePath) { 53 | var resolvedGlueFilePath = path.resolve(glueFilePath); 54 | require(path.resolve(resolvedGlueFilePath)); 55 | }); 56 | 57 | for (var keyword in originalGlobals) { 58 | global[keyword] = originalGlobals[keyword]; 59 | } 60 | 61 | return glueFactory(stepDefinitions, hooks); 62 | }; 63 | }; 64 | 65 | function getLocationOfCaller() { 66 | // Grab the 4th line of the stack trace, then grab the part between parentheses, 67 | // which is of the pattern path:line:column. Split that into 3 tokens. 68 | var pathLineColumn = new Error().stack.split('\n')[3].match(/\((.*)\)/)[1].split(':'); 69 | return { 70 | path: path.relative(process.cwd(), pathLineColumn[0]), 71 | line: parseInt(pathLineColumn[1]), 72 | column: parseInt(pathLineColumn[2]) 73 | }; 74 | } 75 | 76 | -------------------------------------------------------------------------------- /lib/cucumber/hook.js: -------------------------------------------------------------------------------- 1 | var TestStep = require('./test_step'); 2 | 3 | module.exports = function Hook(bodyFn, bodyLocation, scope) { 4 | this.scope = scope; 5 | 6 | this.createTestStep = function (pickle) { 7 | return new TestStep([], [], bodyFn, bodyLocation); 8 | } 9 | }; -------------------------------------------------------------------------------- /lib/cucumber/pickle_loader.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var glob = require('glob').sync; 3 | var Gherkin = require('gherkin'); 4 | 5 | var FILE_COLON_LINE_PATTERN = /^([\w\W]*?):([\d:]+)$/; 6 | 7 | // TODO: move otherFilter to path 8 | module.exports = function PickleLoader(otherFilter) { 9 | var parser = new Gherkin.Parser(); 10 | var compiler = new Gherkin.Compiler(); 11 | 12 | this.loadPickles = function (path) { 13 | var pickles = []; 14 | 15 | var lineFilter, paths; 16 | if (exists(path) && fs.statSync(path).isDirectory()) { 17 | paths = glob(path + "/**/*.feature"); 18 | lineFilter = passthrough; 19 | } else { 20 | var pathWithLines = new PathWithLines(path); 21 | paths = pathWithLines.paths; 22 | lineFilter = pathWithLines.filter; 23 | } 24 | 25 | paths.forEach(function (featurePath) { 26 | var gherkin = fs.readFileSync(featurePath, 'utf-8'); 27 | var gherkinDocument = parser.parse(gherkin); 28 | pickles = pickles.concat(compiler.compile(gherkinDocument, featurePath)); 29 | }); 30 | 31 | var filter = andFilters(lineFilter, otherFilter); 32 | pickles = pickles.filter(filter); 33 | 34 | return pickles; 35 | }; 36 | 37 | function PathWithLines(path) { 38 | var match = FILE_COLON_LINE_PATTERN.exec(path); 39 | if (match) { 40 | this.paths = [match[1]]; 41 | 42 | var lineNumbers = match[2].split(':').map(function (n) { 43 | return parseInt(n); 44 | }); 45 | this.filter = lineFilterFor(lineNumbers); 46 | } else { 47 | this.paths = [path]; 48 | this.filter = passthrough; 49 | } 50 | } 51 | 52 | function exists(path) { 53 | try { 54 | fs.statSync(path); 55 | return true; 56 | } catch (err) { 57 | return false; 58 | } 59 | } 60 | 61 | function andFilters(filterA, filterB) { 62 | return function (pickle) { 63 | return filterA(pickle) && filterB(pickle); 64 | } 65 | } 66 | 67 | function passthrough(pickle) { 68 | return true; 69 | } 70 | 71 | function lineFilterFor(lineNumbers) { 72 | return function (pickle) { 73 | var match = false; 74 | pickle.locations.forEach(function (location) { 75 | match = match || lineNumbers.indexOf(location.line) != -1; 76 | }); 77 | return match; 78 | } 79 | } 80 | }; 81 | -------------------------------------------------------------------------------- /lib/cucumber/pretty_plugin.js: -------------------------------------------------------------------------------- 1 | var chalk = require('chalk'); 2 | 3 | var colors = { 4 | failed: chalk.red, 5 | passed: chalk.green, 6 | skipped: chalk.cyan, 7 | undefined: chalk.yellow, 8 | comment: chalk.gray 9 | }; 10 | 11 | function stepColor(step) { 12 | return colors[step.status]; 13 | } 14 | 15 | module.exports = function PrettyPlugin(out, sourceReader) { 16 | var sourcePrinters = {}; 17 | var sourcePrinter; // Assumes sequential execution. 18 | 19 | this.subscribe = function (eventEmitter) { 20 | eventEmitter.on('scenario-started', function (scenario) { 21 | sourcePrinter = sourcePrinterFor(scenario); 22 | sourcePrinter.printLine(scenario.location); 23 | }); 24 | 25 | eventEmitter.on('step-started', function (step) { 26 | if (step.gherkinLocation) { // Hook steps don't have a location - ignore them 27 | sourcePrinter.printUntilExcluding(step.gherkinLocation); 28 | } 29 | }); 30 | 31 | eventEmitter.on('step-finished', function (step) { 32 | if (step.gherkinLocation) { 33 | sourcePrinter.printStep(step); 34 | } 35 | if (step.status == 'failed') { 36 | var indentedError = step.error.stack.replace(/^(.*)/gm, " $1"); 37 | var color = stepColor(step); 38 | out.write(color(indentedError) + "\n"); 39 | } 40 | }); 41 | }; 42 | 43 | function sourcePrinterFor(scenario) { 44 | var sourcePrinter = sourcePrinters[scenario.location.path]; 45 | if (!sourcePrinter) { 46 | var source = sourceReader.readSource(scenario.location.path); 47 | sourcePrinter = sourcePrinters[scenario.location.path] = new SourcePrinter(source); 48 | } 49 | sourcePrinter.commentIndents = getCommentIndents(scenario); 50 | 51 | return sourcePrinter; 52 | } 53 | 54 | function getCommentIndents(scenario) { 55 | var endColumns = scenario.pickleSteps.map(function (pickleStep) { 56 | var location = pickleStep.locations[pickleStep.locations.length - 1]; 57 | return location.column + pickleStep.text.length; 58 | }); 59 | var maxColumn = Math.max.apply(Math.max, endColumns); 60 | return endColumns.map(function (endColumn) { 61 | return maxColumn - endColumn; 62 | }); 63 | } 64 | 65 | function SourcePrinter(source) { 66 | var lines = source.split(/\n/); 67 | var lineIndex = 0; 68 | 69 | this.printLine = function (location) { 70 | lineIndex = location.line - 1; 71 | out.write(lines[lineIndex] + "\n"); 72 | lineIndex++; 73 | }; 74 | 75 | this.printUntilExcluding = function (location) { 76 | while (lineIndex < location.line - 1) { 77 | out.write(lines[lineIndex] + "\n"); 78 | lineIndex++; 79 | } 80 | }; 81 | 82 | this.printStep = function (step) { 83 | var color = stepColor(step); 84 | 85 | var textStart = 0; 86 | var textLine = lines[lineIndex]; 87 | var formattedLine = ''; 88 | step.matchedArguments.forEach(function (matchedArgument) { 89 | var text = textLine.substring(textStart, matchedArgument.offset - 1); 90 | formattedLine += color(text); 91 | formattedLine += color.bold(matchedArgument.value); 92 | 93 | textStart = matchedArgument.offset - 1 + matchedArgument.value.length; 94 | }); 95 | if (textStart != textLine.length) { 96 | var text = textLine.substring(textStart, textLine.length); 97 | formattedLine += color(text); 98 | } 99 | var locationForComment = step.bodyLocation || step.gherkinLocation; 100 | var comment = '# ' + locationForComment.path + ':' + locationForComment.line; 101 | formattedLine += ' ' + spaces(sourcePrinter.commentIndents.shift()) + colors.comment(comment); 102 | out.write(formattedLine + "\n"); 103 | lineIndex++; 104 | }; 105 | } 106 | 107 | function spaces(n) { 108 | var s = ''; 109 | for (var i = 0; i < n; i++) s += ' '; 110 | return s; 111 | } 112 | }; 113 | -------------------------------------------------------------------------------- /lib/cucumber/sequential_test_case_executor.js: -------------------------------------------------------------------------------- 1 | module.exports = function SequentialTestCaseExecutor(testCases) { 2 | this.execute = function (eventEmitter) { 3 | // This is essentially a for loop. If you think it looks weird it's 4 | // because we're using JavaScript promises, which is a mechanism 5 | // for running asynchronous code. 6 | return testCases.reduce(function (promise, testCase) { 7 | return promise.then(function () { 8 | return testCase.execute(eventEmitter) 9 | }); 10 | }, Promise.resolve()); 11 | }; 12 | }; 13 | -------------------------------------------------------------------------------- /lib/cucumber/source_reader.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | 3 | module.exports = function SourceReader() { 4 | this.readSource = function (path) { 5 | return fs.readFileSync(path, 'utf-8'); 6 | }; 7 | }; 8 | 9 | 10 | -------------------------------------------------------------------------------- /lib/cucumber/step_definition.js: -------------------------------------------------------------------------------- 1 | var TestStep = require('./test_step'); 2 | 3 | module.exports = function StepDefinition(regexp, bodyFn, bodyLocation) { 4 | this.bodyLocation = bodyLocation; 5 | 6 | /** 7 | * Maybe create a TestStep 8 | * 9 | * @param pickleStep compiled step 10 | * @returns TestStep if the pickleStep matches our regexp, null if not 11 | */ 12 | this.createTestStep = function (pickleStep, path) { 13 | var match = regexp.exec(pickleStep.text); 14 | 15 | if (match) { 16 | var captureGroupCount = match.length - 1; 17 | if (captureGroupCount != bodyFn.length) { 18 | throw new Error('Arity mismatch. ' + regexp + ' has ' + captureGroupCount + ' capture group(s), but the body function takes ' + bodyFn.length + ' argument(s)'); 19 | } 20 | 21 | var column = pickleStep.locations[pickleStep.locations.length - 1].column; 22 | var text = match[0]; 23 | var offset = 0; 24 | 25 | var matchedArguments = []; 26 | for (var i = 1; i < match.length; i++) { 27 | var value = match[i]; 28 | offset = text.indexOf(value, offset); 29 | matchedArguments.push({ 30 | offset: column + offset, 31 | value: value 32 | }); 33 | } 34 | 35 | // Create locations with path (Pickle location structs only have line and column) - TODO: FIX! 36 | var gherkinLocations = pickleStep.locations.map(function (location) { 37 | return { 38 | path: path, 39 | line: location.line, 40 | column: location.column 41 | } 42 | }); 43 | return new TestStep(gherkinLocations, matchedArguments, bodyFn, bodyLocation); 44 | } else { 45 | return null; 46 | } 47 | } 48 | }; 49 | -------------------------------------------------------------------------------- /lib/cucumber/tag_filter.js: -------------------------------------------------------------------------------- 1 | var BooleanExpressionParser = require('./boolean_expression_parser'); 2 | var parser = new BooleanExpressionParser(); 3 | 4 | module.exports = function tagFilter(expression) { 5 | var expr = parser.parse(expression); 6 | 7 | return function (pickle) { 8 | var tagNames = pickle.tags.map(function (tag) { 9 | return tag.name; 10 | }); 11 | return expr.evaluate(tagNames); 12 | } 13 | }; -------------------------------------------------------------------------------- /lib/cucumber/test_case.js: -------------------------------------------------------------------------------- 1 | module.exports = function TestCase(pickle, testSteps) { 2 | this.execute = function (eventEmitter) { 3 | var location = pickle.locations[pickle.locations.length - 1]; 4 | 5 | eventEmitter.emit('scenario-started', { 6 | location: location, 7 | pickleSteps: pickle.steps 8 | }); 9 | var world = {}; 10 | 11 | return runTestStepsInSequence(testSteps, eventEmitter, world) 12 | .then(function () { 13 | eventEmitter.emit('scenario-finished', { 14 | path: pickle.path, 15 | location: location 16 | }); 17 | }); 18 | }; 19 | 20 | function runTestStepsInSequence(testSteps, eventEmitter, world) { 21 | // This is essentially a for loop. If you think it looks weird it's 22 | // because we're using JavaScript promises, which is a mechanism 23 | // for running asynchronous code. 24 | return testSteps.reduce(function (promise, testStep) { 25 | return promise.then(function (run) { 26 | return testStep.execute(world, eventEmitter, run) 27 | }) 28 | }, Promise.resolve(true)); 29 | } 30 | }; -------------------------------------------------------------------------------- /lib/cucumber/test_step.js: -------------------------------------------------------------------------------- 1 | module.exports = function TestStep(gherkinLocations, matchedArguments, bodyFn, bodyLocation) { 2 | this.execute = function (world, eventEmitter, run) { 3 | eventEmitter.emit('step-started', { 4 | status: 'unknown', 5 | gherkinLocation: gherkinLocations[gherkinLocations.length - 1], 6 | bodyLocation: bodyLocation, 7 | matchedArguments: matchedArguments 8 | }); 9 | 10 | // We're returning a promise of a boolean here, because JavaScript. 11 | // Most languages should simply return a boolean and forget about 12 | // the promise stuff - simpler! 13 | return new Promise(function (resolve) { 14 | if (!bodyFn) { 15 | resolve({status: 'undefined'}); 16 | } else if (!run) { 17 | resolve({status: 'skipped'}); 18 | } else { 19 | var matchedArgumentValues = matchedArguments.map(function (arg) { 20 | return arg.value 21 | }); 22 | try { 23 | // Execute the step definition body 24 | var promise = bodyFn.apply(world, matchedArgumentValues); 25 | 26 | if (promise && typeof(promise.then) === 'function') { 27 | // ok, it's a promise 28 | promise.then(function () { 29 | resolve({status: 'passed'}); 30 | }).catch(function (err) { 31 | resolve({status: 'failed', error: err}); 32 | }) 33 | } else { 34 | resolve({status: 'passed'}); 35 | } 36 | } catch (err) { 37 | resolve({status: 'failed', error: err}); 38 | } 39 | } 40 | }).then(function (event) { 41 | event.gherkinLocation = gherkinLocations[gherkinLocations.length - 1]; 42 | event.bodyLocation = bodyLocation; 43 | event.matchedArguments = matchedArguments; 44 | eventEmitter.emit('step-finished', event); 45 | return event.status === 'passed'; 46 | }); 47 | } 48 | }; 49 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "microcuke", 3 | "version": "1.0.0", 4 | "description": "Mimimal Cucumber Reference Implementation", 5 | "scripts": { 6 | "test": "ISTANBUL=true istanbul cover _mocha -- --recursive", 7 | "posttest": "istanbul check-coverage --statements 100 --branches 94", 8 | "test-fast": "mocha --recursive", 9 | "sloc": "sloc lib", 10 | "sloccheck": "sloc --format csv lib | grep Total | cut -f3 -d',' | ruby -e 'sloc=STDIN.read.to_i; max=500; puts \"#{sloc} SLOC\"; raise \"#{sloc}>#{max}\" if sloc>max'" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git://github.com/cucumber/microcuke.git" 15 | }, 16 | "keywords": [ 17 | "cucumber", 18 | "gherkin", 19 | "bdd", 20 | "testing", 21 | "tests", 22 | "test" 23 | ], 24 | "author": "Cucumber Limited ", 25 | "license": "MIT", 26 | "bin": { 27 | "cucumber": "./bin/cucumber" 28 | }, 29 | "bugs": { 30 | "url": "https://github.com/cucumber/microcuke/issues" 31 | }, 32 | "homepage": "https://github.com/cucumber/microcuke#readme", 33 | "dependencies": { 34 | "chalk": "^1.1.1", 35 | "gherkin": "git+https://github.com/cucumber/gherkin-javascript.git", 36 | "glob": "^5.0.15", 37 | "minimist": "^1.2.0" 38 | }, 39 | "devDependencies": { 40 | "istanbul": "^0.4.0", 41 | "memory-streams": "^0.1.0", 42 | "mocha": "^2.3.3", 43 | "sloc": "^0.1.9" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /test-data/glue/hooks.js: -------------------------------------------------------------------------------- 1 | /* istanbul ignore next */ 2 | Before(function () { 3 | console.log("BEFORE HOOK"); 4 | }); 5 | 6 | /* istanbul ignore next */ 7 | After(function () { 8 | console.log("AFTER HOOK"); 9 | }); 10 | -------------------------------------------------------------------------------- /test-data/glue/stepdefs.js: -------------------------------------------------------------------------------- 1 | /* istanbul ignore next */ 2 | Given(/first step/, function () { 3 | throw new Error("and it fails"); 4 | }); -------------------------------------------------------------------------------- /test-data/hello.feature: -------------------------------------------------------------------------------- 1 | Feature: Hello 2 | @wip 3 | Scenario: first 4 | Given first step 5 | 6 | Scenario: second 7 | Given second step 8 | Given third step 9 | -------------------------------------------------------------------------------- /test/cucumber/boolean_expression_parser_test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var BooleanExpressionParser = require('../../lib/cucumber/boolean_expression_parser'); 3 | var parser = new BooleanExpressionParser(); 4 | 5 | describe("TagExpressionParser", function () { 6 | describe("#parse", function () { 7 | 8 | [ 9 | ["a and b", "( a and b )"], 10 | ["a or b", "( a or b )"], 11 | ["not a", "not ( a )"], 12 | ["( a and b ) or ( c and d )", "( ( a and b ) or ( c and d ) )"], 13 | ["not a or b and not c or not d or e and f", "( ( ( not ( a ) or ( b and not ( c ) ) ) or not ( d ) ) or ( e and f ) )"] 14 | // a or not b 15 | ].forEach(function (inOut) { 16 | it(inOut[0], function () { 17 | var infix = inOut[0]; 18 | var expr = parser.parse(infix); 19 | assert.equal(expr.toString(), inOut[1]); 20 | 21 | var roundtripTokens = expr.toString(); 22 | var roundtripExpr = parser.parse(roundtripTokens); 23 | assert.equal(roundtripExpr.toString(), inOut[1]); 24 | }); 25 | }); 26 | 27 | // evaluation 28 | 29 | it("evaluates not", function () { 30 | var expr = parser.parse("not x"); 31 | assert.equal(expr.evaluate(['x']), false); 32 | assert.equal(expr.evaluate(['y']), true); 33 | }); 34 | 35 | it("evaluates and", function () { 36 | var expr = parser.parse("x and y"); 37 | assert.equal(expr.evaluate(['x', 'y']), true); 38 | assert.equal(expr.evaluate(['y']), false); 39 | assert.equal(expr.evaluate(['x']), false); 40 | }); 41 | 42 | it("evaluates or", function () { 43 | var expr = parser.parse(" x or(y) "); 44 | assert.equal(expr.evaluate([]), false); 45 | assert.equal(expr.evaluate(['y']), true); 46 | assert.equal(expr.evaluate(['x']), true); 47 | }); 48 | 49 | // errors 50 | 51 | it("errors on extra close paren", function () { 52 | try { 53 | parser.parse("( a and b ) )"); 54 | throw new Error("expected error") 55 | } catch (expected) { 56 | assert.equal(expected.message, "Unclosed (") 57 | } 58 | }); 59 | 60 | it("errors on extra close paren", function () { 61 | try { 62 | parser.parse("a not ( and )"); 63 | throw new Error("expected error") 64 | } catch (expected) { 65 | assert.equal(expected.message, "empty stack") 66 | } 67 | }); 68 | 69 | it("errors on unclosed paren", function () { 70 | try { 71 | parser.parse("( ( a and b )"); 72 | throw new Error("expected error") 73 | } catch (expected) { 74 | assert.equal(expected.message, "Unclosed (") 75 | } 76 | }); 77 | 78 | it("errors when there are several expressions", function () { 79 | try { 80 | var expr = parser.parse("a b"); 81 | throw new Error("expected error") 82 | } catch (expected) { 83 | assert.equal(expected.message, "Not empty") 84 | } 85 | }); 86 | }); 87 | }); 88 | -------------------------------------------------------------------------------- /test/cucumber/glue_loader_test.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var assert = require('assert'); 3 | var glob = require('glob').sync; 4 | var GlueLoader = require('../../lib/cucumber/glue_loader'); 5 | 6 | function glueFactory(stepDefinitions, hooks) { 7 | return { 8 | stepDefinitions: stepDefinitions, 9 | hooks: hooks 10 | } 11 | } 12 | 13 | var testDataDir = path.join(__dirname, '../../test-data'); 14 | 15 | describe("GlueLoader", function () { 16 | describe("#loadGlue", function () { 17 | beforeEach(function () { 18 | glob(testDataDir + "/**/*.js").forEach(function (glueFilePath) { 19 | var resolvedGlueFilePath = path.resolve(glueFilePath); 20 | delete require.cache[resolvedGlueFilePath]; 21 | }); 22 | }); 23 | 24 | afterEach(function () { 25 | // Verify that we clean up temporary polution of global namespace 26 | assert.equal(global.Given, undefined); 27 | }); 28 | 29 | it("loads step definitions", function () { 30 | var glueLoader = new GlueLoader(); 31 | var stubGlue = glueLoader.loadGlue(testDataDir, glueFactory); 32 | assert.equal(stubGlue.stepDefinitions.length, 1); 33 | assert.deepEqual(stubGlue.stepDefinitions[0].bodyLocation, { 34 | path: 'test-data/glue/stepdefs.js', 35 | // The reported line number is mangled if we're running with code coverage on (ISTANBUL is set) 36 | line: process.env.ISTANBUL ? 9 : 2, 37 | column: process.env.ISTANBUL ? 39 : 1 38 | }); 39 | }); 40 | 41 | it("loads hooks", function () { 42 | var glueLoader = new GlueLoader(); 43 | var stubGlue = glueLoader.loadGlue(testDataDir, glueFactory); 44 | var hookScopes = stubGlue.hooks.map(function (hook) { 45 | return hook.scope 46 | }); 47 | assert.deepEqual(hookScopes, ['before', 'after']); 48 | }); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /test/cucumber/glue_test.js: -------------------------------------------------------------------------------- 1 | var EventEmitter = require('events').EventEmitter; 2 | var assert = require('assert'); 3 | var Gherkin = require('gherkin'); 4 | var Glue = require('../../lib/cucumber/glue'); 5 | 6 | function compile(gherkin) { 7 | var parser = new Gherkin.Parser(); 8 | var compiler = new Gherkin.Compiler(); 9 | return compiler.compile(parser.parse(gherkin), 'features/hello.feature'); 10 | } 11 | 12 | describe("Glue", function () { 13 | describe("#createTestCase", function () { 14 | it("tells step definitions to create test steps from pickle steps", function () { 15 | var executed = false; 16 | var stepDefinitions = [ 17 | { 18 | createTestStep: function (pickleStep) { 19 | assert.equal(pickleStep.text, 'this is defined'); 20 | return { 21 | execute: function () { 22 | executed = true; 23 | return Promise.resolve(); 24 | } 25 | }; 26 | } 27 | } 28 | ]; 29 | var glue = new Glue(stepDefinitions, []); 30 | var pickle = compile("Feature: hello\n Scenario: hello\n Given this is defined")[0]; 31 | var testCase = glue.createTestCase(pickle); 32 | 33 | assert(!executed); 34 | return testCase.execute(new EventEmitter()).then(function () { 35 | assert(executed); 36 | }); 37 | }); 38 | 39 | it("creates an undefined step when no stepdefs match", function () { 40 | var stepDefinitions = [{ 41 | createTestStep: function (pickleStep) { 42 | return null; 43 | } 44 | }]; 45 | var glue = new Glue(stepDefinitions, []); 46 | var pickle = compile("Feature: hello\n Scenario: hello\n Given this is defined")[0]; 47 | var testCase = glue.createTestCase(pickle); 48 | 49 | var eventEmitter = new EventEmitter(); 50 | var finished = false; 51 | eventEmitter.on('step-finished', function (step) { 52 | finished = true; 53 | assert.equal(step.status, 'undefined'); 54 | assert.deepEqual(step.gherkinLocation, {path: 'features/hello.feature', line: 3, column: 11}); 55 | }); 56 | return testCase.execute(eventEmitter) 57 | .then(function () { 58 | assert.ok(finished); 59 | }); 60 | }); 61 | 62 | it("throws an exception when two stepdefs match", function () { 63 | var stepDefinitions = [ 64 | { 65 | createTestStep: function (pickleStep) { 66 | return {}; 67 | } 68 | }, 69 | { 70 | createTestStep: function (pickleStep) { 71 | return {}; 72 | } 73 | } 74 | ]; 75 | var glue = new Glue(stepDefinitions, []); 76 | var pickle = compile("Feature: hello\n Scenario: hello\n Given this is defined")[0]; 77 | try { 78 | glue.createTestCase(pickle); 79 | throw new Error("Expected error"); 80 | } catch (err) { 81 | // TODO: Error message should have details/location about the step as well as the ambiguous step defs 82 | assert.equal(err.message, "Ambiguous match"); 83 | } 84 | }); 85 | 86 | it("creates test cases with hooks", function () { 87 | var result = []; 88 | var stepDefinitions = [ 89 | { 90 | createTestStep: function (pickleStep) { 91 | return { 92 | execute: function () { 93 | result.push('step'); 94 | return Promise.resolve(); 95 | } 96 | }; 97 | } 98 | } 99 | ]; 100 | 101 | var hooks = [ 102 | { 103 | scope: 'before', 104 | createTestStep: function (pickle) { 105 | return { 106 | execute: function () { 107 | result.push('before') 108 | return Promise.resolve(); 109 | } 110 | }; 111 | } 112 | }, 113 | { 114 | scope: 'after', 115 | createTestStep: function (pickle) { 116 | return { 117 | execute: function () { 118 | result.push('after'); 119 | return Promise.resolve(); 120 | } 121 | }; 122 | } 123 | } 124 | ]; 125 | var glue = new Glue(stepDefinitions, hooks); 126 | var pickle = compile("Feature: hello\n Scenario: hello\n Given this is defined")[0]; 127 | var testCase = glue.createTestCase(pickle); 128 | 129 | assert.deepEqual(result, []); 130 | return testCase.execute(new EventEmitter()).then(function () { 131 | assert.deepEqual(result, ['before', 'step', 'after']); 132 | }); 133 | }); 134 | }); 135 | }); 136 | -------------------------------------------------------------------------------- /test/cucumber/hook_test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var Hook = require('../../lib/cucumber/hook'); 3 | 4 | describe("Hook", function () { 5 | describe("#createTestStep", function () { 6 | it("returns null when the hook's tag expression doesn't match the pickle's tags"); 7 | 8 | it("returns a TestStep when there is no tag expression", function () { 9 | var hook = new Hook(function () { 10 | }); 11 | 12 | var pickle = {}; 13 | var testStep = hook.createTestStep(pickle); 14 | assert.ok(testStep); 15 | }); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /test/cucumber/pickle_loader_test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var PickleLoader = require('../../lib/cucumber/pickle_loader'); 3 | var tagFilter = require('../../lib/cucumber/tag_filter'); 4 | 5 | function passthrough(pickle) { 6 | return true; 7 | } 8 | 9 | describe("PickleLoader", function () { 10 | describe("#loadPickles", function () { 11 | it("loads pickles from directory", function () { 12 | var pickleLoader = new PickleLoader(passthrough); 13 | var pickles = pickleLoader.loadPickles('test-data'); 14 | assert.equal(pickles.length, 2); 15 | assert.equal(pickles[0].steps[0].text, "first step"); 16 | }); 17 | 18 | it("loads pickles from file", function () { 19 | var pickleLoader = new PickleLoader(passthrough); 20 | var pickles = pickleLoader.loadPickles('test-data/hello.feature'); 21 | assert.equal(pickles.length, 2); 22 | assert.equal(pickles[0].steps[0].text, "first step"); 23 | }); 24 | 25 | it("filters pickles by line number", function () { 26 | var pickleLoader = new PickleLoader(passthrough); 27 | var pickles = pickleLoader.loadPickles('test-data/hello.feature:6'); 28 | assert.equal(pickles.length, 1); 29 | assert.equal(pickles[0].steps[0].text, "second step"); 30 | }); 31 | 32 | it("filters pickles by tag expressions", function () { 33 | var filter = tagFilter("not @wip"); 34 | 35 | var pickleLoader = new PickleLoader(filter); 36 | var pickles = pickleLoader.loadPickles('test-data/hello.feature'); 37 | assert.equal(pickles.length, 1); 38 | assert.equal(pickles[0].steps[0].text, "second step"); 39 | }); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /test/cucumber/pretty_plugin_test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var EventEmitter = require('events').EventEmitter; 3 | var chalk = require('chalk'); 4 | var WritableStream = require('memory-streams').WritableStream; 5 | var PrettyPlugin = require('../../lib/cucumber/pretty_plugin'); 6 | 7 | describe('PrettyPlugin', function () { 8 | var stdout, eventEmitter, scenario, steps; 9 | 10 | beforeEach(function () { 11 | stdout = new WritableStream(); 12 | var sourceReader = { 13 | // override the readSource method 14 | readSource: function (path) { 15 | return "" + 16 | "Feature: don't print this\n" + 17 | " or this\n" + 18 | "\n" + 19 | " Scenario: hello\n" + 20 | "\n" + 21 | " Given I have 42 cukes\n" + 22 | " And I have fun\n"; 23 | } 24 | }; 25 | 26 | eventEmitter = new EventEmitter(); 27 | new PrettyPlugin(stdout, sourceReader).subscribe(eventEmitter); 28 | 29 | scenario = { 30 | location: {path: 'features/cukes.feature', line: 4, column: 1}, 31 | pickleSteps: [ 32 | { 33 | locations: [{path: 'features/cukes.feature', line: 6, column: 10}], 34 | text: 'I have 14 cukes' 35 | }, 36 | { 37 | locations: [{path: 'features/cukes.feature', line: 7, column: 8}], 38 | text: 'I have fun' 39 | } 40 | ] 41 | }; 42 | 43 | steps = [ 44 | { 45 | status: 'unknown', 46 | gherkinLocation: {path: 'features/cukes.feature', line: 6, column: 10}, 47 | matchedArguments: [ 48 | {offset: 18, value: "42"} 49 | ] 50 | }, 51 | { 52 | status: 'unknown', 53 | gherkinLocation: {path: 'features/cukes.feature', line: 7, column: 8}, 54 | matchedArguments: [ 55 | {offset: 16, value: "fun"} 56 | ] 57 | } 58 | ]; 59 | 60 | }); 61 | 62 | it("prints passing step as green", function () { 63 | eventEmitter.emit('scenario-started', scenario); 64 | 65 | eventEmitter.emit('step-started', steps[0]); 66 | steps[0].status = 'passed'; 67 | eventEmitter.emit('step-finished', steps[0]); 68 | 69 | eventEmitter.emit('step-started', steps[1]); 70 | steps[1].status = 'passed'; 71 | eventEmitter.emit('step-finished', steps[1]); 72 | 73 | var expected = " Scenario: hello\n\n" + 74 | chalk.green(' Given I have ') + chalk.green.bold('42') + chalk.green(' cukes') + ' ' + chalk.gray("# features/cukes.feature:6") + "\n" + 75 | chalk.green(' And I have ') + chalk.green.bold('fun') + ' ' + chalk.gray("# features/cukes.feature:7") + "\n"; 76 | assert.deepEqual(stdout.toString(), expected); 77 | }); 78 | 79 | it("prints failing step as red", function () { 80 | eventEmitter.emit('scenario-started', scenario); 81 | 82 | eventEmitter.emit('step-started', steps[0]); 83 | steps[0].status = 'failed'; 84 | steps[0].error = new Error("oops"); 85 | steps[0].error.stack = "Error: oops"; // more predictable - no frames 86 | eventEmitter.emit('step-finished', steps[0]); 87 | 88 | eventEmitter.emit('step-started', steps[1]); 89 | steps[1].status = 'skipped'; 90 | eventEmitter.emit('step-finished', steps[1]); 91 | 92 | var expected = " Scenario: hello\n\n" + 93 | chalk.red(' Given I have ') + chalk.red.bold('42') + chalk.red(' cukes') + ' ' + chalk.gray("# features/cukes.feature:6") + "\n" + 94 | chalk.red(' Error: oops') + "\n" + 95 | chalk.cyan(' And I have ') + chalk.cyan.bold('fun') + ' ' + chalk.gray("# features/cukes.feature:7") + "\n"; 96 | assert.equal(stdout.toString(), expected); 97 | }); 98 | }); 99 | -------------------------------------------------------------------------------- /test/cucumber/sequential_test_case_executor_test.js: -------------------------------------------------------------------------------- 1 | var EventEmitter = require('events').EventEmitter; 2 | var assert = require('assert'); 3 | var SequentialTestCaseExecutor = require('../../lib/cucumber/sequential_test_case_executor'); 4 | 5 | describe("Runtime", function () { 6 | describe("#execute", function () { 7 | it("runs test cases in sequence", function () { 8 | var order = []; 9 | var tc1 = { 10 | execute: function (eventEmitter) { 11 | order.push(1); 12 | return Promise.resolve(); 13 | } 14 | }; 15 | var tc2 = { 16 | execute: function (eventEmitter) { 17 | order.push(2); 18 | return Promise.resolve(); 19 | } 20 | }; 21 | 22 | var runtime = new SequentialTestCaseExecutor([tc1, tc2]); 23 | return runtime.execute(new EventEmitter()) 24 | .then(function () { 25 | assert.deepEqual(order, [1, 2]); 26 | }); 27 | }); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /test/cucumber/step_definition_test.js: -------------------------------------------------------------------------------- 1 | var EventEmitter = require('events').EventEmitter; 2 | var assert = require('assert'); 3 | var StepDefinition = require('../../lib/cucumber/step_definition'); 4 | 5 | describe("StepDefinition", function () { 6 | describe("#createTestStep", function () { 7 | it("returns null when the regexp doesn't match the pickleStep's text", function () { 8 | var stepDefinition = new StepDefinition(/I have (\d+) cukes/, function (n) { 9 | }); 10 | 11 | var pickleStep = {text: "wat"}; 12 | var testStep = stepDefinition.createTestStep(pickleStep); 13 | assert.equal(testStep, null); 14 | }); 15 | 16 | it("returns a TestStep when the regexp matches the pickleStep's text", function () { 17 | var stepDefinition = new StepDefinition(/I have (\d+) cukes/, function (n) { 18 | }); 19 | 20 | var pickleStep = {text: "I have 44 cukes", locations: [{column: 22}]}; 21 | var testStep = stepDefinition.createTestStep(pickleStep); 22 | assert.ok(testStep); 23 | }); 24 | 25 | it("creates a TestStep that passes captures to body function", function () { 26 | var n; 27 | var stepDefinition = new StepDefinition(/I have (\d+) cukes/, function (_n) { 28 | n = _n; 29 | }); 30 | 31 | var pickleStep = {text: "I have 44 cukes", locations: [{column: 22}]}; 32 | var testStep = stepDefinition.createTestStep(pickleStep); 33 | var world = {}; 34 | testStep.execute(world, new EventEmitter(), true); 35 | assert.equal(n, "44"); 36 | }); 37 | 38 | it("throws an error when the number of capture groups is different from the number of function parameters", function () { 39 | var stepDefinition = new StepDefinition(/I have (\d+) cukes/, function (n, wat) { 40 | }); 41 | var pickleStep = {text: "I have 44 cukes"}; 42 | try { 43 | stepDefinition.createTestStep(pickleStep); 44 | throw new Error("Expected error"); 45 | } catch (err) { 46 | // TODO: Error message should say where the step definition is defined (file and line) 47 | assert.equal(err.message, "Arity mismatch. /I have (\\d+) cukes/ has 1 capture group(s), but the body function takes 2 argument(s)"); 48 | } 49 | }); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /test/cucumber/test_case_test.js: -------------------------------------------------------------------------------- 1 | var EventEmitter = require('events').EventEmitter; 2 | var assert = require('assert'); 3 | var TestCase = require('../../lib/cucumber/test_case'); 4 | 5 | describe("TestCase", function () { 6 | describe("#execute", function () { 7 | it("tells next step to run when the previous one passed", function () { 8 | var done = false; 9 | var pickle = { 10 | path: 'features/test.feature', 11 | locations: [] 12 | }; 13 | var testCase = new TestCase(pickle, [ 14 | { 15 | execute: function (world, eventEmitter, run) { 16 | return Promise.resolve(true); 17 | } 18 | }, 19 | { 20 | execute: function (world, eventEmitter, run) { 21 | done = true; 22 | assert(run); 23 | return Promise.resolve(true); 24 | } 25 | } 26 | ]); 27 | 28 | return testCase.execute(new EventEmitter()) 29 | .then(function () { 30 | assert(done); 31 | }); 32 | }); 33 | 34 | it("uses the same this object across steps", function () { 35 | var done = false; 36 | var pickle = { 37 | path: 'features/test.feature', 38 | locations: [] 39 | }; 40 | var testCase = new TestCase(pickle, [ 41 | { 42 | execute: function (world, eventEmitter, run) { 43 | world.bingo = 'yes'; 44 | return Promise.resolve(true); 45 | } 46 | }, 47 | { 48 | execute: function (world, eventEmitter, run) { 49 | done = true; 50 | assert.equal(world.bingo, 'yes'); 51 | return Promise.resolve(true); 52 | } 53 | } 54 | ]); 55 | 56 | return testCase.execute(new EventEmitter()) 57 | .then(function () { 58 | assert(done); 59 | }); 60 | }); 61 | 62 | it("tells next step to not run when previous one failed", function () { 63 | var done = false; 64 | var pickle = { 65 | path: 'features/test.feature', 66 | locations: [] 67 | }; 68 | var testCase = new TestCase(pickle, [ 69 | { 70 | execute: function (world, eventEmitter, run) { 71 | return Promise.resolve(false); 72 | } 73 | }, 74 | { 75 | execute: function (world, eventEmitter, run) { 76 | done = true; 77 | assert(!run); 78 | return Promise.resolve(false); 79 | } 80 | } 81 | ]); 82 | 83 | return testCase.execute(new EventEmitter()) 84 | .then(function () { 85 | assert(done); 86 | }); 87 | }); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /test/cucumber/test_step_test.js: -------------------------------------------------------------------------------- 1 | var EventEmitter = require('events').EventEmitter; 2 | var assert = require('assert'); 3 | var TestStep = require('../../lib/cucumber/test_step'); 4 | 5 | describe("TestStep", function () { 6 | describe("#execute", function () { 7 | it("fires an event with status=unknown before a step is executed", function () { 8 | var locations = []; 9 | var testStep = new TestStep(locations, [], function () { 10 | }); 11 | 12 | var eventEmitter = new EventEmitter(); 13 | var step; 14 | eventEmitter.on('step-started', function (_step) { 15 | step = _step; 16 | }); 17 | 18 | var world = {}; 19 | testStep.execute(world, eventEmitter, true); 20 | assert.equal(step.status, 'unknown'); 21 | }); 22 | 23 | it("fires an event with status=failed when returning a rejected promise", function () { 24 | var locations = []; 25 | var testStep = new TestStep(locations, [], function () { 26 | return Promise.reject(Error("sad trombone")); 27 | }); 28 | 29 | var eventEmitter = new EventEmitter(); 30 | var step; 31 | eventEmitter.on('step-finished', function (_step) { 32 | step = _step; 33 | }); 34 | 35 | var world = {}; 36 | return testStep.execute(world, eventEmitter, true) 37 | .then(function (run) { 38 | assert(!run); 39 | assert.equal(step.status, 'failed'); 40 | }); 41 | }); 42 | 43 | it("fires an event with status=failed when an exception is thrown", function () { 44 | var locations = []; 45 | var testStep = new TestStep(locations, [], function () { 46 | throw new Error("sad trombone"); 47 | }); 48 | 49 | var eventEmitter = new EventEmitter(); 50 | var step; 51 | eventEmitter.on('step-finished', function (_step) { 52 | step = _step; 53 | }); 54 | 55 | var world = {}; 56 | return testStep.execute(world, eventEmitter, true) 57 | .then(function (run) { 58 | assert(!run); 59 | assert.equal(step.status, 'failed'); 60 | }); 61 | }); 62 | 63 | it("fires an event with status=skipped when the run parameter is false", function () { 64 | var locations = []; 65 | var testStep = new TestStep(locations, [], function () { 66 | throw new Error("sad trombone"); 67 | }); 68 | 69 | var eventEmitter = new EventEmitter(); 70 | var step; 71 | eventEmitter.on('step-finished', function (_step) { 72 | step = _step; 73 | }); 74 | 75 | var world = {}; 76 | return testStep.execute(world, eventEmitter, false) 77 | .then(function (run) { 78 | assert(!run); 79 | assert.equal(step.status, 'skipped'); 80 | }); 81 | }); 82 | 83 | it("fires an event with status=passed when no exception is thrown", function () { 84 | var locations = []; 85 | var testStep = new TestStep(locations, [], function () { 86 | }); 87 | 88 | var eventEmitter = new EventEmitter(); 89 | var step; 90 | eventEmitter.on('step-finished', function (_step) { 91 | step = _step; 92 | }); 93 | 94 | var world = {}; 95 | return testStep.execute(world, eventEmitter, true) 96 | .then(function (run) { 97 | assert(run); 98 | assert.equal(step.status, 'passed'); 99 | }); 100 | }); 101 | 102 | it("fires an event with status=passed when returning a resolved promise", function () { 103 | var locations = []; 104 | var testStep = new TestStep(locations, [], function () { 105 | return Promise.resolve(); 106 | }); 107 | 108 | var eventEmitter = new EventEmitter(); 109 | var step; 110 | eventEmitter.on('step-finished', function (_step) { 111 | step = _step; 112 | }); 113 | 114 | var world = {}; 115 | return testStep.execute(world, eventEmitter, true) 116 | .then(function (run) { 117 | assert(run); 118 | assert.equal(step.status, 'passed'); 119 | }); 120 | }); 121 | 122 | it("fires an event with status=undefined when no body exists", function () { 123 | var locations = []; 124 | var testStep = new TestStep(locations, [], null); 125 | 126 | var eventEmitter = new EventEmitter(); 127 | var step; 128 | eventEmitter.on('step-finished', function (_step) { 129 | step = _step; 130 | }); 131 | 132 | var world = {}; 133 | return testStep.execute(world, eventEmitter, true) 134 | .then(function (run) { 135 | assert(!run); 136 | assert.equal(step.status, 'undefined', true); 137 | }); 138 | }); 139 | 140 | it("passes argument values to body function", function () { 141 | var arg; 142 | 143 | var locations = []; 144 | var matchedArguments = [{value: 'hello'}]; 145 | var testStep = new TestStep(locations, matchedArguments, function (_arg) { 146 | arg = _arg; 147 | }); 148 | 149 | var eventEmitter = new EventEmitter(); 150 | var world = {}; 151 | return testStep.execute(world, eventEmitter, true) 152 | .then(function (run) { 153 | assert.equal(arg, 'hello'); 154 | }); 155 | }); 156 | }); 157 | }); 158 | --------------------------------------------------------------------------------