diff --git a/package-lock.json b/package-lock.json index dd578792..19fbaf35 100644 --- a/package-lock.json +++ b/package-lock.json @@ -119,6 +119,42 @@ } } }, + "@sinonjs/commons": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.4.0.tgz", + "integrity": "sha512-9jHK3YF/8HtJ9wCAbG+j8cD0i0+ATS9A7gXFqS36TblLPNy6rEEc+SB0imo91eCboGaBYGV/MT1/br/J+EE7Tw==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/formatio": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-3.2.1.tgz", + "integrity": "sha512-tsHvOB24rvyvV2+zKMmPkZ7dXX6LSLKZ7aOtXY6Edklp0uRcgGpOsQTTGTcWViFyx4uhWc6GV8QdnALbIbIdeQ==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1", + "@sinonjs/samsam": "^3.1.0" + } + }, + "@sinonjs/samsam": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-3.3.1.tgz", + "integrity": "sha512-wRSfmyd81swH0hA1bxJZJ57xr22kC07a1N4zuIL47yTS04bDk6AoCkczcqHEjcRPmJ+FruGJ9WBQiJwMtIElFw==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.0.2", + "array-from": "^2.1.1", + "lodash": "^4.17.11" + } + }, + "@sinonjs/text-encoding": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz", + "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==", + "dev": true + }, "@xgbuils/pickle-lint-with-gherkin-5-test": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/@xgbuils/pickle-lint-with-gherkin-5-test/-/pickle-lint-with-gherkin-5-test-0.0.1.tgz", @@ -228,6 +264,12 @@ "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=", "dev": true }, + "array-from": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/array-from/-/array-from-2.1.1.tgz", + "integrity": "sha1-z+nYwmYoudxa7MYqn12PHzUsEZU=", + "dev": true + }, "array-union": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", @@ -2160,6 +2202,12 @@ "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", "dev": true }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -2364,6 +2412,12 @@ "verror": "1.10.0" } }, + "just-extend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.0.2.tgz", + "integrity": "sha512-FrLwOgm+iXrPV+5zDU6Jqu4gCRXbWEQg2O3SKONsWE4w7AXFRkryS53bpWdaL9cNol+AmR3AEYz6kn+o0fCPnw==", + "dev": true + }, "latest-version": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-3.1.0.tgz", @@ -2465,6 +2519,12 @@ "chalk": "^2.0.1" } }, + "lolex": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-4.0.1.tgz", + "integrity": "sha512-UHuOBZ5jjsKuzbB/gRNNW8Vg8f00Emgskdq2kvZxgBJCS0aqquAuXai/SkWORlKeZEiNQWZjFZOqIUcH9LqKCw==", + "dev": true + }, "loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -2820,6 +2880,27 @@ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", "dev": true }, + "nise": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/nise/-/nise-1.4.10.tgz", + "integrity": "sha512-sa0RRbj53dovjc7wombHmVli9ZihXbXCQ2uH3TNm03DyvOSIQbxg+pbqDKrk2oxMK1rtLGVlKxcB9rrc6X5YjA==", + "dev": true, + "requires": { + "@sinonjs/formatio": "^3.1.0", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "lolex": "^2.3.2", + "path-to-regexp": "^1.7.0" + }, + "dependencies": { + "lolex": { + "version": "2.7.5", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-2.7.5.tgz", + "integrity": "sha512-l9x0+1offnKKIzYVjyXU2SiwhXDLekRzKyhnbyldPHvC7BvLPVpdNUNR2KeMAiCN2D/kLNttZgQD5WjSxuBx3Q==", + "dev": true + } + } + }, "node-emoji": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.10.0.tgz", @@ -3437,6 +3518,15 @@ "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", "dev": true }, + "path-to-regexp": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz", + "integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=", + "dev": true, + "requires": { + "isarray": "0.0.1" + } + }, "path-type": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", @@ -3923,6 +4013,29 @@ "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", "dev": true }, + "sinon": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-7.3.2.tgz", + "integrity": "sha512-thErC1z64BeyGiPvF8aoSg0LEnptSaWE7YhdWWbWXgelOyThent7uKOnnEh9zBxDbKixtr5dEko+ws1sZMuFMA==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.4.0", + "@sinonjs/formatio": "^3.2.1", + "@sinonjs/samsam": "^3.3.1", + "diff": "^3.5.0", + "lolex": "^4.0.1", + "nise": "^1.4.10", + "supports-color": "^5.5.0" + }, + "dependencies": { + "diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", + "dev": true + } + } + }, "slice-ansi": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", diff --git a/package.json b/package.json index faa18c2f..0d8c0d8e 100644 --- a/package.json +++ b/package.json @@ -78,7 +78,8 @@ "eslint-config-google": "^0.13.0", "mocha": "6.1.4", "npm-check": "^5.9.0", - "nyc": "^14.1.1" + "nyc": "^14.1.1", + "sinon": "^7.3.2" }, "bin": { "pickle-lint": "src/main.js" diff --git a/src/linter/no-configurable-linter.js b/src/linter/no-configurable-linter.js index 5a6affed..6a4a7467 100644 --- a/src/linter/no-configurable-linter.js +++ b/src/linter/no-configurable-linter.js @@ -12,19 +12,17 @@ class NoConfigurableLinter { } lint(file) { - try { - return this.parser.parse(file) - .then(({feature, languageKeywords}) => [{ - feature, - languageKeywords, - file, - }]); - } catch (e) { - if (e.errors) { - return Promise.reject(processFatalErrors(e.errors)); - } - return Promise.reject(e); - } + return this.parser.parse(file) + .then(({feature, languageKeywords}) => [{ + feature, + languageKeywords, + file, + }], (e) => { + if (e.errors) { + return Promise.reject(processFatalErrors(e.errors)); + } + return Promise.reject(e); + }); } } diff --git a/src/parser-adapter/index.js b/src/parser-adapter/index.js index 257725d0..f68d5d8b 100644 --- a/src/parser-adapter/index.js +++ b/src/parser-adapter/index.js @@ -1,13 +1,58 @@ module.exports = (Gherkin) => { - const {Parser, DIALECTS} = Gherkin; - const parser = new Parser(); - return { - parse({content}) { - const {feature = {}} = parser.parse(content); - return Promise.resolve({ - languageKeywords: DIALECTS[feature.language], - feature, - }); - }, - }; + if (Gherkin.Parser) { + const {Parser, DIALECTS} = Gherkin; + const parser = new Parser(); + return { + parse({content}) { + try { + const {feature = {}} = parser.parse(content); + return Promise.resolve({ + languageKeywords: DIALECTS[feature.language], + feature, + }); + } catch (error) { + return Promise.reject(error); + } + }, + }; + } else { + const {fromPaths} = Gherkin; + return { + parse({path}) { + const stream = fromPaths([path], { + includeSource: false, + includePickles: false, + }); + + return new Promise((resolve, reject) => { + const gherkinDocuments = []; + const errors = []; + stream.on('error', (error) => { + reject(error); + }); + stream.on('data', ({gherkinDocument, attachment}) => { + if (attachment) { + errors.push(attachment); + } else { + gherkinDocuments.push(gherkinDocument); + } + }); + stream.on('end', () => { + if (errors.length > 0) { + reject(errors); + } else if (gherkinDocuments.length > 1) { + reject(new Error( + 'Unexpected error: fromPaths function returns multiple gherkin documents' + )); + } else if (gherkinDocuments.length === 1) { + const [gherkinDocument] = gherkinDocuments; + resolve(gherkinDocument); + } else { + reject(new Error('Unexpected error: zero gherkin documents')); + } + }); + }); + }, + }; + } }; diff --git a/test/linter/no-configurable-linter/no-configurable-linter.js b/test/linter/no-configurable-linter/no-configurable-linter.js index 5c95dfea..ef1e9294 100644 --- a/test/linter/no-configurable-linter/no-configurable-linter.js +++ b/test/linter/no-configurable-linter/no-configurable-linter.js @@ -238,7 +238,7 @@ describe('No Configurable File Linter', function() { }; const wrongParser = { parse() { - throw e; + return Promise.reject(e); }, }; const wrongLinter = new NoConfigurableLinter(wrongParser); @@ -258,7 +258,7 @@ describe('No Configurable File Linter', function() { const error = 'error'; const wrongParser = { parse() { - throw error; + return Promise.reject(error); }, }; const wrongLinter = new NoConfigurableLinter(wrongParser); diff --git a/test/parser-adapter/Example.feature b/test/parser-adapter/Example.feature deleted file mode 100644 index 3318837a..00000000 --- a/test/parser-adapter/Example.feature +++ /dev/null @@ -1,3 +0,0 @@ -Feature: ei - -Scenario: ui \ No newline at end of file diff --git a/test/parser-adapter/Other.feature b/test/parser-adapter/Other.feature deleted file mode 100644 index 0924a86d..00000000 --- a/test/parser-adapter/Other.feature +++ /dev/null @@ -1 +0,0 @@ -Feature: other \ No newline at end of file diff --git a/test/parser-adapter/index.js b/test/parser-adapter/index.js index 4ea27ce8..bfce4bb2 100644 --- a/test/parser-adapter/index.js +++ b/test/parser-adapter/index.js @@ -1,41 +1,224 @@ const ParserAdapter = require('../../src/parser-adapter/'); const {expect} = require('chai'); +const sinon = require('sinon'); +// const Gherkin = require('gherkin'); describe('ParserAdapter', () => { - context('gherkin 5.1', () => { - it('given the parser, builds a normalized parser', () => { - const language = 'en'; - const feature = { - language, - children: [], - }; - const tree = {feature}; - const content = 'file content'; - const file = {content}; - const languageKeywords = ['Then', 'When', 'Given']; - - const Gherkin = { - Parser: class Parser { - parse(contentParam) { - if (contentParam === content) { - return tree; - } - throw new Error('wrong parsing'); - } - }, - DIALECTS: { + describe('gherkin 5.1', () => { + const createGherkin5 = (parse, DIALECTS = {}) => { + const Parser = function() {}; + Parser.prototype.parse = parse; + return {Parser, DIALECTS}; + }; + + context('passed a file object with content prop', () => { + it('receives the content prop as parameter', () => { + const parse = sinon.fake.returns({}); + const Gherkin = createGherkin5(parse); + + const content = 'file content'; + const file = {content}; + const parser = ParserAdapter(Gherkin); + + return parser.parse(file).then(() => { + sinon.assert.calledOnce(parse); + sinon.assert.calledWithExactly(parse, content); + }, () => { + expect.fail('parser must not fail'); + }); + }); + }); + + context('parser method returns a well-formed gherkin document', () => { + it('parse method resolves a well-formed parsed feature', () => { + const language = 'en'; + const languageKeywords = ['Then', 'When', 'Given']; + const feature = {language}; + const parse = sinon.fake.returns({feature}); + const Gherkin = createGherkin5(parse, { [language]: languageKeywords, - }, - }; + }); + + const parser = ParserAdapter(Gherkin); + return parser.parse({}).then((result) => { + expect(result).to.be.deep.equal({ + feature, + languageKeywords, + }); + }, () => { + expect.fail('parser must not fail'); + }); + }); + }); + + context('parser method throws an error', () => { + it('parse method rejects the error', () => { + const error = new Error('error'); + const parse = () => { + throw error; + }; + const Gherkin = createGherkin5(parse); + + const parser = ParserAdapter(Gherkin); + return parser.parse({}).then(() => { + expect.fail('parser must fail'); + }, (actualError) => { + expect(actualError).to.be.equal(error); + }); + }); + }); + }); + + describe('gherkin 6.0', () => { + const successfulStream = (data) => ({ + on(event, cb) { + if (event === 'data') { + const items = Array.isArray(data) ? data : [data]; + items.forEach(cb); + } else if (event === 'end') { + cb(); + } + }, + }); + context('passed an object with a path prop', () => { + it('receives a singleton array with the path as parameter', () => { + const fromPaths = sinon.fake.returns(successfulStream({})); + const Gherkin = {fromPaths}; + const path = 'path/to/file.feature'; + + const parser = ParserAdapter(Gherkin); + return parser.parse({path}).then((e) => { + sinon.assert.calledOnce(fromPaths); + const [[firstArg, secondArg]] = fromPaths.args; + expect(firstArg).to.be.deep.equal([path]); + expect(secondArg).to.be.deep.equal({ + includeSource: false, + includePickles: false, + }); + }); + }); + }); + + context('fromPaths method returns an stream that dispatch a well-formed gherkin document', () => { + it('parse method resolves a well-formed parsed feature', () => { + const gherkinDocument = { + feature: { + tags: [], + children: [], + }, + }; + const fromPaths = sinon.fake.returns(successfulStream({ + gherkinDocument, + })); + const Gherkin = {fromPaths}; + + const parser = ParserAdapter(Gherkin); + return parser.parse({}).then((result) => { + expect(result).to.be.equal(gherkinDocument); + }, () => { + expect.fail('parser must not fail'); + }); + }); + }); + + context('fromPaths method returns an stream that dispatch a multiple attachments', () => { + it('parse method rejects an array of attachments', () => { + const firstAttachment = { + source: {uri: 'path/to/first'}, + data: 'first error', + }; + const secondAttachment = { + source: {uri: 'path/to/second'}, + data: 'second error', + }; + const attachments = [{ + attachment: firstAttachment, + }, { + attachment: secondAttachment, + }]; + const fromPaths = sinon.fake.returns(successfulStream(attachments)); + const Gherkin = {fromPaths}; + + const parser = ParserAdapter(Gherkin); + return parser.parse({}).then(() => { + expect.fail('parser must fail'); + }, (errors) => { + expect(errors).to.be.deep.equal([ + firstAttachment, + secondAttachment, + ]); + }); + }); + }); + + context('fromPaths method returns an stream that dispatch a multiple gherkin document', () => { + it('parse method rejects with unexpected error', () => { + const firstGherkinDocument = { + feature: { + tags: [{name: '@tag'}], + children: [], + }, + }; + const secondGherkinDocument = { + feature: { + children: [], + }, + }; + const gherkinDocuments = [{ + gherkinDocument: firstGherkinDocument, + }, { + gherkinDocument: secondGherkinDocument, + }]; + const fromPaths = sinon.fake.returns(successfulStream(gherkinDocuments)); + const Gherkin = {fromPaths}; + + const parser = ParserAdapter(Gherkin); + return parser.parse({}).then(() => { + expect.fail('parser must fail'); + }, (error) => { + expect(error.message).to.be.equal( + 'Unexpected error: fromPaths function returns multiple gherkin documents' + ); + }); + }); + }); + + context('fromPaths method returns an stream that does not dispatch gherkin documents', () => { + it('parse method rejects with unexpected error', () => { + const fromPaths = sinon.fake.returns(successfulStream([])); + const Gherkin = {fromPaths}; + + const parser = ParserAdapter(Gherkin); + return parser.parse({}).then(() => { + expect.fail('parser must fail'); + }, (error) => { + expect(error.message).to.be.equal( + 'Unexpected error: zero gherkin documents' + ); + }); + }); + }); + + context('fromPaths method dispatch an error', () => { + it('parse method rejects with unexpected error', () => { + const error = new Error('error'); + const fromPaths = sinon.fake.returns({ + on(event, cb) { + if (event === 'error') { + cb(error); + } else if (event === 'end') { + cb(); + } + }, + }); + const Gherkin = {fromPaths}; - const parser = ParserAdapter(Gherkin); - return parser.parse(file).then((result) => { - expect(result).to.be.deep.equal({ - feature, - languageKeywords, + const parser = ParserAdapter(Gherkin); + return parser.parse({}).then(() => { + expect.fail('parser must fail'); + }, (actualError) => { + expect(actualError).to.be.equal(error); }); - }, () => { - expect.fail('parser must not fail'); }); }); });