Skip to content

Commit

Permalink
Complete, maybe
Browse files Browse the repository at this point in the history
  • Loading branch information
szikszail committed Jul 22, 2017
1 parent b00821d commit 92de738
Show file tree
Hide file tree
Showing 23 changed files with 360 additions and 98 deletions.
5 changes: 2 additions & 3 deletions lib/ast/Background.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,11 @@ class Background {
*/
toString() {
const lines = utils.lines();
lines.add(`${this.keyword}: ${this.name}`);
lines.add(`${this.keyword}:${this.name ? ' ' + this.name : ''}`);
if (this.description) {
lines.add(utils.indent(this.description));
lines.add(this.description, null);
}
if (this.steps.length > 0) {
lines.add();
this.steps.forEach(step => {
lines.add(utils.indent(step.toString()));
});
Expand Down
Empty file removed lib/ast/Comment.js
Empty file.
2 changes: 1 addition & 1 deletion lib/ast/Examples.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ class Examples {
const lines = utils.lines();

if (this.tags.length > 0) {
lines.add(this.tags.join(' '));
lines.add(Tag.arrayToString(this.tags));
}
lines.add(utils.indent(`${this.keyword}: ${this.name}`));

Expand Down
38 changes: 33 additions & 5 deletions lib/ast/Feature.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,42 @@
const Scenario = require('./Scenario');
const ScenarioOutline = require('./ScenarioOutline');
const Background = require('./Background');
const Tag = require('./Tag');
const utils = require('../utils');

/**
* Model of a Cucumber Feature
* @class
*/
class Feature {
/**
* @constructor
* @param {string} keyword The keyword of the feature
* @param {string} name The name of the feature
* @param {string} description The description of the feature
* @param {string} language The language of the feature
*/
constructor(keyword, name, description, language) {
/** @member {string} */
this.keyword = utils.normalize(keyword);
this.name = name ? utils.normalize(name) : '';
this.description = description ? utils.normalize(description) : '';
/** @member {string} */
this.name = utils.normalize(name);
/** @member {string} */
this.description = utils.normalize(description);
/** @member {string} */
this.language = language || 'en';
/** @member {Array<Tag>} */
this.tags = [];
/** @member {Array<Scenario|ScenarioOutline|Background>} */
this.scenarios = [];
}

/**
* Parses a Feature object, based on the passed AST object.
* @param {Object} obj
* @returns {Feature}
* @throws {TypeError} If the passed object is not a Feature.
*/
static parse(obj) {
if (!obj || obj.type !== 'Feature') {
throw new TypeError('The given object is not a Feature!');
Expand All @@ -40,19 +64,23 @@ class Feature {
return feature;
}

/**
* Returns the Cucumber feature file
* representative text of the Feature.
* @returns {string}
*/
toString() {
const lines = utils.lines();
if (this.tags.length > 0) {
lines.add(this.tags.map(tag => tag.toString()).join(' '));
lines.add(Tag.arrayToString(this.tags));
}
lines.add(`${this.keyword}: ${this.name}`);
if (this.description) {
lines.add(utils.indent(this.description));
}
if (this.scenarios.length > 0) {
this.scenarios.forEach(item => {
lines.add();
lines.add(utils.indent(item.toString()));
lines.add(null, utils.indent(item.toString()));
});
}
return lines.toString();
Expand Down
29 changes: 23 additions & 6 deletions lib/ast/GherkinDocument.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,42 @@
'use strict';

const Feature = require('./Feature');
const Comment = require('./Comment');

/**
* Model of a complete Gherkin Document
* @class
*/
class GherkinDocument {
constructor(feature) {
/**
* @constructor
*/
constructor() {
this.feature = null;
this.comments = [];
}

/**
* Parses a GherkinDocument object, based on the passed AST object.
* @param {Object} obj
* @returns {GherkinDocument}
* @throws {TypeError} If the passed object is not a GherkinDocument.
*/
static parse(obj) {
if (!obj || obj.type !== 'GherkinDocument') {
throw new TypeError('The given object is not a GherkinDocument!');
}
const document = new GherkinDocument();
document.feature = Feature.parse(obj.feature);
if (Array.isArray(obj.comments)) {
document.comments = obj.comments.map(comment => Comment.parse(comment));
}
return document;
}

/**
* Returns the Cucumber feature file
* representative text of the Document.
* @returns {string}
*/
toString() {
return this.feature.toString();
}
}

module.exports = GherkinDocument;
4 changes: 2 additions & 2 deletions lib/ast/Scenario.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,11 @@ class Scenario {
toString() {
const lines = utils.lines();
if (this.tags.length > 0) {
lines.add(this.tags.join(' '));
lines.add(Tag.arrayToString(this.tags));
}
lines.add(`${this.keyword}: ${this.name}`);
if (this.description) {
lines.add(utils.indent(this.description), null);
lines.add(this.description, null);
}
if (this.steps.length > 0) {
this.steps.forEach(step => {
Expand Down
18 changes: 9 additions & 9 deletions lib/ast/ScenarioOutline.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@ const Tag = require('./Tag');
const Step = require('./Step');

/**
* Model of a Cucumber Scenario
* Model of a Cucumber ScenarioOutline
* @class
*/
class ScenarioOutline extends Scenario {
/**
* @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
* @param {string} keyword The keyword of the scenario outline
* @param {string} name The name of the scenario outline
* @param {string} description The description of the scenario outline
*/
constructor(keyword, name, description) {
super(keyword, name, description);
Expand All @@ -25,10 +25,10 @@ class ScenarioOutline extends Scenario {
}

/**
* Parses a Scenario object, based on the passed AST object.
* Parses a Scenario Outline object, based on the passed AST object.
* @param {Object} obj
* @returns {Scenario}
* @throws {TypeError} If the passed object is not a Scenario.
* @returns {ScenarioOutline}
* @throws {TypeError} If the passed object is not a ScenarioOutline.
*/
static parse(obj) {
if (!obj || obj.type !== 'ScenarioOutline') {
Expand All @@ -49,15 +49,15 @@ class ScenarioOutline extends Scenario {

/**
* Returns the Cucumber feature file
* representative text of the Scenario.
* representative text of the Scenario Outline.
* @returns {string}
*/
toString() {
const lines = utils.lines();
lines.add(super.toString());
if (this.examples.length > 0) {
this.examples.forEach(examples => {
lines.add(null, utils.indent(examples.toString()));
lines.add(null, examples.toString());
});
}
return lines.toString();
Expand Down
28 changes: 28 additions & 0 deletions lib/ast/Tag.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
'use strict';

let multiLine = false;
const utils = require('../utils');

/**
* Model of a Cucumber Tag (annotation)
* @class
Expand Down Expand Up @@ -36,6 +39,31 @@ class Tag {
toString() {
return this.name;
}

/**
* Returns the Cucumber feature file
* representative text of a Tag set.
* @param {Array<Tag>} tags
* @returns {string}
*/
static arrayToString(tags) {
if (!multiLine) {
return tags.join(' ');
}
const lines = utils.lines();
tags.forEach(tag => {
lines.add(tag.toString());
});
return lines.toString();
}

/**
* Set, whether the tags should be stringed as multi-line or single
* @param multi
*/
static setMultiLine(multi) {
multiLine = !!multi;
}
}

module.exports = Tag;
32 changes: 30 additions & 2 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,39 @@ const utils = require('./utils');
const assembler = {};
assembler.Ast = {};

fs.readdirSync('./ast').forEach(file => {
fs.readdirSync(path.resolve('lib/ast')).forEach(file => {
const className = file.replace('.js', '');
assembler.Ast[className] = require(`./ast/${file}`);
assembler.Ast[className] = require(path.resolve(`lib/ast/${file}`));
});

/**
* @typedef {Object} AssemblerConfig
* @property {boolean} oneTagPerLine Should tags be rendered one per line?
*/

/** @type {AssemblerConfig} */
const DEFAULT_OPTIONS = {
oneTagPerLine: false
};

/**
* Formats the given Gherkin Document to text.
* @param {GherkinDocument|Array<GherkinDocument>} document
* @param {AssemblerConfig|Object} options
* @returns {string|Array<string>}
*/
assembler.format = (document, options) => {
options = Object.assign({}, DEFAULT_OPTIONS, options || {});
assembler.Ast.Tag.setMultiLine(options.oneTagPerLine);
if (Array.isArray(document)) {
return document.map(doc => assembler.format(doc));
}
if (!(document instanceof assembler.Ast.GherkinDocument)) {
throw new TypeError(`The passed object is not a GherkinDocument!` + document);
}
return document.toString();
};

/**
* Set line endings for generated text
* @param {boolean} crLf In case of true, line ending
Expand Down
2 changes: 1 addition & 1 deletion lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ const utils = {
if (!indentation) {
return text;
}
return split(text).map(line => INDENTATION.repeat(indentation) + line).join(utils.LINE_BREAK);
return split(text).map(line => line ? INDENTATION.repeat(indentation) + line : '').join(utils.LINE_BREAK);
},

/**
Expand Down
6 changes: 6 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"devDependencies": {
"chai": "^4.1.0",
"coveralls": "^2.13.1",
"deep-copy": "^1.2.0",
"istanbul": "^0.4.5",
"mocha": "^3.4.2"
},
Expand Down
64 changes: 64 additions & 0 deletions test/ast/Feature.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
'use strict';

const path = require('path');
const fs = require('fs');

const Feature = require(path.resolve('lib/ast/Feature.js'));
const Scenario = require(path.resolve('lib/ast/Scenario.js'));
const ScenarioOutline = require(path.resolve('lib/ast/ScenarioOutline.js'));
const Background = require(path.resolve('lib/ast/Background.js'));
const Step = require(path.resolve('lib/ast/Step.js'));
const Tag = require(path.resolve('lib/ast/Tag.js'));

const featureAst = require('../data/base.ast.json').feature;
const featureFile = fs.readFileSync(path.resolve('test/data/base.feature'), 'utf8');

const expect = require('chai').expect;
const dc = require('deep-copy');

describe('Ast.Feature', () => {
it('should represent an Ast Feature instance', () => {
const feature = new Feature('Feature', 'this is a feature', 'this is a good feature\n a');
expect(feature).to.be.instanceOf(Feature);
expect(feature.keyword).to.equal('Feature');
expect(feature.name).to.equal('this is a feature');
expect(feature.description).to.equal('this is a good feature\na');
expect(feature.tags).to.eql([]);
expect(feature.scenarios).to.eql([]);
});

it('should not parse regular objects', () => {
expect(() => Feature.parse()).to.throw(TypeError);
expect(() => Feature.parse({type: 'Type'})).to.throw(TypeError);
});

it('should parse Gherkin Ast Feature type to Feature', () => {
const feature = Feature.parse(featureAst);
expect(feature).to.be.instanceOf(Feature);
expect(feature.keyword).to.equal(featureAst.keyword);
expect(feature.name).to.equal(featureAst.name);
expect(feature.tags).to.have.lengthOf(featureAst.tags.length);
feature.tags.forEach((tag, i) => {
expect(tag).to.be.instanceOf(Tag);
expect(tag.name).to.equal(featureAst.tags[i].name);
});
expect(feature.scenarios).to.have.lengthOf(featureAst.children.length);
feature.scenarios.forEach((scenario, i) => {
expect(scenario.constructor.name).to.equal(featureAst.children[i].type);
expect(scenario.name).to.equal(featureAst.children[i].name);
});
});

it('should not parse Gherkin Ast Feature if it has unsupported children', () => {
const wrongAst = dc(featureAst);
wrongAst.children.push({
type: 'ThereIsNoTypeLikeThis'
});
expect(() => Feature.parse(wrongAst)).to.throw(TypeError);
});

it('should have proper string representation', () => {
const feature = Feature.parse(featureAst);
expect(feature.toString()).to.equal(featureFile);
});
});
Loading

0 comments on commit 92de738

Please sign in to comment.