Skip to content

Commit

Permalink
Scenario readability (#4)
Browse files Browse the repository at this point in the history
* #2 added first code version
* Updated dependencies
* Added Element
* Tests
* Fixed tests
* 1.1.0
* Fixed typo
  • Loading branch information
szikszail committed Jan 8, 2018
1 parent 83439f5 commit 2a5d981
Show file tree
Hide file tree
Showing 16 changed files with 205 additions and 61 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Expand Up @@ -2,4 +2,5 @@
/coverage
.idea
*.log
*.*~
*.*~
package-lock.json
3 changes: 2 additions & 1 deletion .npmignore
Expand Up @@ -2,4 +2,5 @@
/coverage
.idea
*.log
/test
/test
package-lock.json
6 changes: 6 additions & 0 deletions CHANGELOG.md
@@ -1,5 +1,11 @@
# Changelog

## 1.1.0 - 2018-01-07

### Added

- Add option to make scenarios more readable ([#3](https://github.com/szikszail/gherkin-assembler/issues/3))

## 1.0.1 - 2017-08-08

### Fixes
Expand Down
9 changes: 8 additions & 1 deletion README.md
Expand Up @@ -44,6 +44,7 @@ By passing an `AssemblerConfig` object to format method (or other Ast type metho
| Option | Description | Default |
|:-------|:------------|:--------|
| `oneTagPerLine` | Should tags rendered separately, one by line? | `false`, i.e. all tag of a scenario, feature, etc will be rendered in the same line |
| `separateStepGroups` | Should step groups (when-then) be separated? | `false` |
| `compact` | Should empty lines be skipped, removed from the result? | `false`, i.e. there will be empty lines in appropriate places |
| `lineBreak` | The line break character(s). | `\n`, i.e. it uses Unix line break, to set Windows style, set `\r\n` |
| `indentation` | The indentation character(s). | `' '`, i.e. it uses two space character to add indentation where it's appropriate |
Expand All @@ -54,7 +55,7 @@ The API provides types to be able to handle different parts of Gherkin feature f

```javascript
'use strict';
const {AST} = require('gherkin-assemble');
const {AST} = require('gherkin-assembler');
console.log(Object.keys(AST));
// Background,...,Feature,GherkinDocument,...,Tag
```
Expand Down Expand Up @@ -125,6 +126,8 @@ Background: Some background steps
#### Methods

* `new Background(keyword, name, description) : Background` - Creates a new `Background` object, with the given values.
* `{Background}.useNormalStepKeywords()` - Sets the keywords of all step to normal keywords, i.e. `Given`, `When`, `Then`.
* `{Background}.useReadableStepKeywords()` - Sets the keywords of steps to more readable ones, if applicable, i.e. replaces multiple normal keywords with `And` keyword.
* `{Background}.toString({AssemblerConfig}) : string` - Converts the background to string, i.e. formats it.
* `{Background}.clone() : Background` - Clones the background.
* `Background.parse({Object} object) : Background` - Parses the given [Background object](/test/data/base.ast.json#33) to a `Background`.
Expand Down Expand Up @@ -153,6 +156,8 @@ Description of the scenario
#### Methods

* `new Scenario(keyword, name, description) : Scenario` - Creates a new `Scenario` object, with the given values.
* `{Scenario}.useNormalStepKeywords()` - Sets the keywords of all step to normal keywords, i.e. `Given`, `When`, `Then`.
* `{Scenario}.useReadableStepKeywords()` - Sets the keywords of steps to more readable ones, if applicable, i.e. replaces multiple normal keywords with `And` keyword.
* `{Scenario}.toString({AssemblerConfig}) : string` - Converts the scenario to string, i.e. formats it.
* `{Scenario}.clone() : Scenario` - Clones the scenario.
* `Scenario.parse({Object} object) : Scenario` - Parses the given [Scenario object](/test/data/base.ast.json#98) to a `Scenario`.
Expand Down Expand Up @@ -189,6 +194,8 @@ Scenario Outline: Name of outline <key>
#### Methods

* `new ScenarioOutline(keyword, name, description) : ScenarioOutline` - Creates a new `ScenarioOutline` object, with the given values.
* `{ScenarioOutline}.useNormalStepKeywords()` - Sets the keywords of all step to normal keywords, i.e. `Given`, `When`, `Then`.
* `{ScenarioOutline}.useReadableStepKeywords()` - Sets the keywords of steps to more readable ones, if applicable, i.e. replaces multiple normal keywords with `And` keyword.
* `{ScenarioOutline}.toString({AssemblerConfig}) : string` - Converts the scenario outline to string, i.e. formats it.
* `{ScenarioOutline}.clone() : ScenarioOutline` - Clones the scenario outline.
* `ScenarioOutline.parse({Object} object) : ScenarioOutline` - Parses the given [ScenarioOutline object](/test/data/base.ast.json#343) to a `ScenarioOutline`.
Expand Down
27 changes: 9 additions & 18 deletions lib/ast/Background.js
@@ -1,30 +1,14 @@
'use strict';

const Step = require('./Step');
const Element = require('./Element');
const utils = require('../utils');

/**
* Model of a Cucumber Background scenario
* @class
*/
class Background {
/**
* @constructor
* @param {string} keyword The keyword of the background scenario
* @param {string} name The name of the background scenario
* @param {string} description The description of the background scenario
*/
constructor(keyword, name, description) {
/** @member {string} */
this.keyword = utils.normalize(keyword);
/** @member {string} */
this.name = utils.normalize(name);
/** @member {string} */
this.description = utils.normalize(description);
/** @member {Array<Step>} */
this.steps = [];
}

class Background extends Element {
/**
* Parses a Background object, based on the passed AST object.
* @param {Object} obj
Expand Down Expand Up @@ -55,7 +39,14 @@ class Background {
lines.add(this.description, null);
}
if (this.steps.length > 0) {
const addGroups = utils.config(options).separateStepGroups;
if (addGroups) {
this.useReadableStepKeywords();
}
this.steps.forEach(step => {
if (addGroups && step.keyword === 'When') {
lines.add();
}
lines.add(utils.indent(step.toString(options)));
});
}
Expand Down
56 changes: 56 additions & 0 deletions lib/ast/Element.js
@@ -0,0 +1,56 @@
'use strict';

const Tag = require('./Tag');
const Step = require('./Step');
const utils = require('../utils');

/**
* Model of a Cucumber Feature element
* @class
*/
class Element {
/**
* @constructor
* @param {string} keyword The keyword of the scenario
* @param {string} name The name of the scenario
* @param {string} description The description of the scenario
*/
constructor(keyword, name, description) {
/** @member {string} */
this.keyword = utils.normalize(keyword);
/** @member {string} */
this.name = utils.normalize(name);
/** @member {string} */
this.description = utils.normalize(description);
/** @member {Array<Step>} */
this.steps = [];
}

/**
* Sets the keywords of all step to normal keywords,
* i.e. Given, When, Then.
*/
useNormalStepKeywords() {
this.steps.forEach((step, i) => {
if (i && ['And', 'But'].indexOf(step.keyword) > -1) {
step.keyword = this.steps[i - 1].keyword;
}
});
}

/**
* Sets the keywords of steps to more readable ones,
* if applicable, i.e. replaces multiple normal keywords
* with And keyword.
*/
useReadableStepKeywords() {
this.useNormalStepKeywords();
for (let i = this.steps.length - 1; i > 0; --i) {
if (this.steps[i].keyword === this.steps[i - 1].keyword) {
this.steps[i].keyword = 'And';
}
}
}
}

module.exports = Element;
20 changes: 11 additions & 9 deletions lib/ast/Scenario.js
Expand Up @@ -2,30 +2,25 @@

const Tag = require('./Tag');
const Step = require('./Step');
const Element = require('./Element');
const utils = require('../utils');

/**
* Model of a Cucumber Scenario
* @class
*/
class Scenario {
class Scenario extends Element {
/**
* @constructor
* @param {string} keyword The keyword of the scenario
* @param {string} name The name of the scenario
* @param {string} description The description of the scenario
*/
constructor(keyword, name, description) {
/** @member {string} */
this.keyword = utils.normalize(keyword);
/** @member {string} */
this.name = utils.normalize(name);
/** @member {string} */
this.description = utils.normalize(description);
super(keyword, name, description);

/** @member {Array<Tag>} */
this.tags = [];
/** @member {Array<Step>} */
this.steps = [];
}

/**
Expand Down Expand Up @@ -64,7 +59,14 @@ class Scenario {
lines.add(this.description, null);
}
if (this.steps.length > 0) {
const addGroups = utils.config(options).separateStepGroups;
if (addGroups) {
this.useReadableStepKeywords();
}
this.steps.forEach(step => {
if (addGroups && step.keyword === 'When') {
lines.add();
}
lines.add(utils.indent(step.toString(options)));
});
}
Expand Down
2 changes: 2 additions & 0 deletions lib/utils.js
Expand Up @@ -22,6 +22,7 @@ class StrippedTable extends Table {
* @typedef {Object} AssemblerConfig
* @property {boolean} oneTagPerLine Should tags be rendered one per line?
* @property {boolean} compact Should empty lines be added?
* @property {boolean} separateStepGroups Should step groups (when-then) be separated?
* @property {string} lineBreak The linebreak character(s)
* @property {string} indentation The indentation character(s)
*/
Expand All @@ -30,6 +31,7 @@ class StrippedTable extends Table {
const DEFAULT_OPTIONS = {
oneTagPerLine: false,
compact: false,
separateStepGroups: false,
lineBreak: '\n',
indentation: ' '
};
Expand Down
10 changes: 5 additions & 5 deletions package.json
@@ -1,6 +1,6 @@
{
"name": "gherkin-assembler",
"version": "1.0.1",
"version": "1.1.0",
"description": "Assembling Gherkin ASTs to feature file string",
"main": "lib/index.js",
"scripts": {
Expand All @@ -24,21 +24,21 @@
"url": "https://github.com/szikszail/gherkin-assembler/issues"
},
"engines": {
"node": ">=4.9.2",
"node": ">=6.0.0",
"npm": ">=3.0.0"
},
"homepage": "https://github.com/szikszail/gherkin-assembler#readme",
"devDependencies": {
"chai": "^4.1.0",
"coveralls": "^2.13.1",
"coveralls": "^3.0.0",
"deep-copy": "^1.2.0",
"istanbul": "^0.4.5",
"mocha": "^3.4.2"
"mocha": "^4.1.0"
},
"dependencies": {
"cli-table": "^0.3.1",
"colors": "^1.1.2",
"gherkin": "^4.1.3",
"gherkin": "^5.0.1",
"proxyquire": "^1.8.0",
"split-lines": "^1.1.0"
}
Expand Down
16 changes: 11 additions & 5 deletions test/ast/Background.spec.js
@@ -1,11 +1,12 @@
'use strict';

const path = require('path');
const fs = require('fs');
const Background = require(path.resolve('lib/ast/Background.js'));
const {resolve} = require('path');
const {readFileSync} = require('fs');
const Background = require(resolve('lib/ast/Background.js'));
const Element = require(resolve('lib/ast/Element.js'));

const backgroundAst = require('../data/background.json');
const backgroundFeature = fs.readFileSync(path.resolve('test/data/background.txt'), 'utf8');
const backgroundFeature = readFileSync(resolve('test/data/background.txt'), 'utf8');

const expect = require('chai').expect;

Expand All @@ -19,6 +20,11 @@ describe('Ast.Background', () => {
expect(background.steps).to.eql([]);
});

it('should extend common Element class', () => {
const background = new Background('Background', 'this is a background', 'this is a good background\n a');
expect(background).to.be.instanceOf(Element);
});

it('should not parse regular objects', () => {
expect(() => Background.parse()).to.throw(TypeError);
expect(() => Background.parse({type: 'Type'})).to.throw(TypeError);
Expand All @@ -33,7 +39,7 @@ describe('Ast.Background', () => {

it('should have proper string representation', () => {
const background = Background.parse(backgroundAst);
expect(background.toString()).to.equal(backgroundFeature);
expect(background.toString().split(/\r?\n/g)).to.eql(backgroundFeature.split(/\r?\n/g));
});

it('should have method to clone it', () => {
Expand Down
57 changes: 57 additions & 0 deletions test/ast/Element.spec.js
@@ -0,0 +1,57 @@
'use strict';

const {resolve} = require('path');
const Element = require(resolve('lib/ast/Element.js'));
const Step = require(resolve('lib/ast/Step.js'));
const expect = require('chai').expect;

describe('Ast.Element', () => {
let element;

beforeEach(() => {
element = new Element();
element.steps.push(
new Step('Given', 'step 1'),
new Step('And', 'step 2'),
new Step('When', 'step 3'),
new Step('And', 'step 4'),
new Step('When', 'step 5'),
new Step('But', 'step 6'),
new Step('Then', 'step 7'),
new Step('Then', 'step 8'),
new Step('And', 'step 9'),
new Step('When', 'step 10'),
new Step('And', 'step 11'),
new Step('Then', 'step 12'),
new Step('And', 'step 13')
);
});

it('should have method to set normal step keywords', () => {
expect(element.useNormalStepKeywords).not.to.be.undefined;

element.useNormalStepKeywords();

expect(element.steps.map(step => step.keyword)).to.eql([
'Given', 'Given',
'When', 'When', 'When', 'When',
'Then', 'Then', 'Then',
'When', 'When',
'Then', 'Then'
]);
});

it('should have method to set readable step keywords', () => {
expect(element.useReadableStepKeywords).not.to.be.undefined;

element.useReadableStepKeywords();

expect(element.steps.map(step => step.keyword)).to.eql([
'Given', 'And',
'When', 'And', 'And', 'And',
'Then', 'And', 'And',
'When', 'And',
'Then', 'And'
]);
});
});
2 changes: 1 addition & 1 deletion test/ast/Feature.spec.js
Expand Up @@ -55,7 +55,7 @@ describe('Ast.Feature', () => {

it('should have proper string representation', () => {
const feature = Feature.parse(featureAst);
expect(feature.toString()).to.equal(featureFile);
expect(feature.toString().split(/\r?\n/g)).to.eql(featureFile.split(/\r?\n/g));
});

it('should have method to clone it', () => {
Expand Down
2 changes: 1 addition & 1 deletion test/ast/GherkinDocument.spec.js
Expand Up @@ -31,7 +31,7 @@ describe('Ast.GherkinDocument', () => {

it('should have proper string representation', () => {
const document = GherkinDocument.parse(featureAst);
expect(document.toString()).to.equal(featureFile);
expect(document.toString().split(/\r?\n/g)).to.eql(featureFile.split(/\r?\n/g));
});

it('should have method to clone it', () => {
Expand Down

0 comments on commit 2a5d981

Please sign in to comment.