From 79eba950d4995ed8880f1ffed1f16356569ebd2b Mon Sep 17 00:00:00 2001 From: Roman Dvornov Date: Wed, 18 Nov 2020 18:44:47 +0100 Subject: [PATCH] Refactor validate, added atrule validation --- CHANGELOG.md | 2 + lib/index.js | 15 +- lib/reporter/console.js | 8 +- lib/reporter/gnu.js | 12 +- lib/reporter/json.js | 2 +- lib/validate.js | 191 +++++++++++++++++++---- lib/validators.js | 2 +- package-lock.json | 14 +- package.json | 2 +- test/fixture/reporter/checkstyle | 6 +- test/fixture/reporter/console | 6 +- test/fixture/reporter/gnu | 6 +- test/fixture/reporter/json | 6 +- test/loc.js | 43 +----- test/validate.js | 256 ++++++++++++++----------------- test/validators.js | 156 +++++++++++++++++++ 16 files changed, 462 insertions(+), 265 deletions(-) create mode 100644 test/validators.js diff --git a/CHANGELOG.md b/CHANGELOG.md index da51104..368cce9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ - Droped support for Nodejs < 8 - CLI exits with code `1` and outputs to `stderr` when errors (#12) - Added built version for browsers: `dist/csstree-validator.js` (#11) +- Added at-rule validation for name, prelude and descriptor +- Added `validateAtrule`, `validateAtrulePrelude`, `validateAtruleDescriptor`, `validateRule` and `validateDeclaration` methods ## 1.6.0 (October 27, 2020) diff --git a/lib/index.js b/lib/index.js index e6d333b..701959a 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,16 +1,5 @@ -const { - validatePathList, - validatePath, - validateFile, - validateDictionary, - validateString -} = require('./validators.js'); - module.exports = { - validatePathList, - validatePath, - validateFile, - validateDictionary, - validateString, + ...require('./validators.js'), + ...require('./validate'), reporters: require('./reporter') }; diff --git a/lib/reporter/console.js b/lib/reporter/console.js index 1cae3d1..7cf9c2b 100644 --- a/lib/reporter/console.js +++ b/lib/reporter/console.js @@ -5,16 +5,14 @@ module.exports = function(data) { const errors = data[filename]; output.push('# ' + filename); - output.push.apply(output, errors.map(function(entry) { - const error = entry.error || entry; - + output.push.apply(output, errors.map(function(error) { if (error.name === 'SyntaxError') { return ' [ERROR] ' + error.message; } return ' * ' + - String(error.message) - .replace(/^[^\n]+/, entry.message) + String(error.details) + .replace(/^[^\n]+/, error.message) .replace(/\n/g, '\n '); })); output.push(''); diff --git a/lib/reporter/gnu.js b/lib/reporter/gnu.js index 68cf8e5..f9b575d 100644 --- a/lib/reporter/gnu.js +++ b/lib/reporter/gnu.js @@ -6,18 +6,16 @@ module.exports = function(data) { Object.keys(data).sort().forEach(function(filename) { const errors = data[filename]; - output.push(errors.map(function(entry) { - const error = entry.error || entry; - const line = entry.line || -1; - const column = entry.column || -1; - const message = entry.message || entry.error.rawMessage; + output.push(errors.map(function(error) { + const line = error.line || -1; + const column = error.column || -1; + const message = error.message; const value = error.css ? ': `' + error.css + '`' : ''; const allowed = error.syntax ? '; allowed: ' + error.syntax : ''; let position = line + '.' + column; if (error.loc) { - position = error.loc.start.line + '.' + - error.loc.start.column + '-' + + position += '-' + error.loc.end.line + '.' + error.loc.end.column; } diff --git a/lib/reporter/json.js b/lib/reporter/json.js index b64c6fb..2e3cf41 100644 --- a/lib/reporter/json.js +++ b/lib/reporter/json.js @@ -12,7 +12,7 @@ module.exports = function(data) { column: entry.column || 1, property: entry.property, message: entry.message, - details: error.rawMessage ? error.message : null + details: error.details || (error.rawMessage ? error.message : null) }; })); }, []); diff --git a/lib/validate.js b/lib/validate.js index 1922da0..488bb66 100644 --- a/lib/validate.js +++ b/lib/validate.js @@ -1,47 +1,172 @@ const csstree = require('css-tree'); const syntax = csstree.lexer; -module.exports = function validate(css, filename) { +function isTargetError(error) { + if (!error) { + return null; + } + + if (error.name !== 'SyntaxError' && + error.name !== 'SyntaxMatchError' && + error.name !== 'SyntaxReferenceError') { + return null; + } + + return error; +} + +function validateAtruleDescriptor(atrule, descriptor, value, descriptorLoc) { + const errors = []; + let error; + + if (error = isTargetError(syntax.checkAtruleDescriptorName(atrule, descriptor))) { + errors.push(Object.assign(error, { + atrule, + descriptor, + ...descriptorLoc || (value && value.loc && value.loc.start) + })); + } else { + if (error = isTargetError(syntax.matchAtruleDescriptor(atrule, descriptor, value).error)) { + errors.push(Object.assign(error, { + atrule, + descriptor, + ...error.rawMessage === 'Mismatch' && + { details: error.message, message: 'Invalid value for `' + descriptor + '` descriptor' } + })); + } + } + + return errors; +} + +function validateAtrule(node) { + const atrule = node.name; + const errors = []; + let error; + + if (error = isTargetError(syntax.checkAtruleName(atrule))) { + errors.push(Object.assign(error, { + ...node.loc && node.loc.start + })); + return errors; + } + + errors.push(...validateAtrulePrelude( + atrule, + node.prelude, + (node.prelude && node.prelude.loc && node.prelude.loc.start) || (node.loc && node.loc.start) + )); + + if (node.block && node.block.children) { + node.block.children.forEach(child => { + if (child.type === 'Declaration') { + errors.push(...validateAtruleDescriptor( + atrule, + child.property, + child.value, + child.loc && child.loc.start + )); + } + }); + } + + return errors; +} + +function validateAtrulePrelude(atrule, prelude, preludeLoc) { + const errors = []; + let error; + + if (error = isTargetError(syntax.checkAtrulePrelude(atrule, prelude))) { + errors.push(Object.assign(error, { + ...preludeLoc || (prelude && prelude.loc && prelude.loc.start) + })); + } else if (error = isTargetError(syntax.matchAtrulePrelude(atrule, prelude).error)) { + errors.push(Object.assign(error, { + ...error.rawMessage === 'Mismatch' && + { details: error.message, message: 'Invalid value for `@' + atrule + '` prelude' } + })); + } + + return errors; +} + +function validateDeclaration(property, value, valueLoc) { + const errors = []; + let error; + + if (error = isTargetError(syntax.checkPropertyName(property))) { + errors.push(Object.assign(error, { + property, + ...valueLoc || (value && value.loc && value.loc.start) + })); + } else if (error = isTargetError(syntax.matchProperty(property, value).error)) { + errors.push(Object.assign(error, { + property, + ...error.rawMessage === 'Mismatch' && + { details: error.message, message: 'Invalid value for `' + property + '` property' } + })); + } + + return errors; +} + +function validateRule(node) { + const errors = []; + + if (node.block && node.block.children) { + node.block.children.forEach(child => { + if (child.type === 'Declaration') { + errors.push(...validateDeclaration( + child.property, + child.value, + child.loc && child.loc.start + )); + } + }); + } + + return errors; +} + +function validate(css, filename) { const errors = []; - const ast = csstree.parse(css, { - filename, - positions: true, - onParseError(error) { - errors.push(error); + const ast = typeof css !== 'string' + ? css + : csstree.parse(css, { + filename, + positions: true, + parseAtrulePrelude: false, + parseRulePrelude: false, + parseValue: false, + parseCustomProperty: false, + onParseError(error) { + errors.push(error); + } + }); + + csstree.walk(ast, { + visit: 'Atrule', + enter(node) { + errors.push(...validateAtrule(node)); } }); csstree.walk(ast, { - visit: 'Declaration', + visit: 'Rule', enter(node) { - const { error } = syntax.matchDeclaration(node); - - if (error) { - let message = error.rawMessage || error.message || error; - - // ignore errors except those which make sense - if (error.name !== 'SyntaxMatchError' && - error.name !== 'SyntaxReferenceError') { - return; - } - - if (message === 'Mismatch') { - message = 'Invalid value for `' + node.property + '`'; - } - - errors.push({ - name: error.name, - node, - loc: error.loc || node.loc, - line: error.line || node.loc && node.loc.start && node.loc.start.line, - column: error.column || node.loc && node.loc.start && node.loc.start.column, - property: node.property, - message, - error - }); - } + errors.push(...validateRule(node)); } }); return errors; }; + +module.exports = { + validateAtrule, + validateAtrulePrelude, + validateAtruleDescriptor, + validateRule, + validateDeclaration, + validate +}; diff --git a/lib/validators.js b/lib/validators.js index 4765438..8c2a3ed 100644 --- a/lib/validators.js +++ b/lib/validators.js @@ -1,6 +1,6 @@ const fs = require('fs'); const path = require('path'); -const validate = require('./validate'); +const { validate } = require('./validate'); function defaultShouldBeValidated(filename) { return path.extname(filename) === '.css'; diff --git a/package-lock.json b/package-lock.json index 657ac6c..d21c633 100644 --- a/package-lock.json +++ b/package-lock.json @@ -399,11 +399,11 @@ } }, "css-tree": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0.tgz", - "integrity": "sha512-CdVYz/Yuqw0VdKhXPBIgi8DO3NicJVYZNWeX9XcIuSp9ZoFT5IcleVRW07O5rMjdcx1mb+MEJPknTTEW7DdsYw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.1.tgz", + "integrity": "sha512-NVN42M2fjszcUNpDbdkvutgQSlFYsr1z7kqeuCagHnNLBfYor6uP1WL1KrkmdYZ5Y1vTBCIOI/C/+8T98fJ71w==", "requires": { - "mdn-data": "2.0.12", + "mdn-data": "2.0.14", "source-map": "^0.6.1" } }, @@ -1234,9 +1234,9 @@ } }, "mdn-data": { - "version": "2.0.12", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.12.tgz", - "integrity": "sha512-ULbAlgzVb8IqZ0Hsxm6hHSlQl3Jckst2YEQS7fODu9ilNWy2LvcoSY7TRFIktABP2mdppBioc66va90T+NUs8Q==" + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", + "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==" }, "merge-stream": { "version": "2.0.0", diff --git a/package.json b/package.json index 3ca35ff..786691f 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ }, "dependencies": { "clap": "^1.1.1", - "css-tree": "^1.0.0" + "css-tree": "^1.1.1" }, "devDependencies": { "@rollup/plugin-commonjs": "^11.0.2", diff --git a/test/fixture/reporter/checkstyle b/test/fixture/reporter/checkstyle index 1826e46..65f592d 100644 --- a/test/fixture/reporter/checkstyle +++ b/test/fixture/reporter/checkstyle @@ -1,12 +1,12 @@ - - + + - + diff --git a/test/fixture/reporter/console b/test/fixture/reporter/console index bef6444..8767963 100644 --- a/test/fixture/reporter/console +++ b/test/fixture/reporter/console @@ -1,9 +1,9 @@ # match.css - * Invalid value for `color` + * Invalid value for `color` property syntax: value: 123 --------^ - * Invalid value for `border` + * Invalid value for `border` property syntax: || || value: 1px unknown red ------------^ @@ -11,7 +11,7 @@ # parse.css [ERROR] Colon is expected - * Invalid value for `color` + * Invalid value for `color` property syntax: value: red green ------------^ diff --git a/test/fixture/reporter/gnu b/test/fixture/reporter/gnu index e27dceb..85ab994 100644 --- a/test/fixture/reporter/gnu +++ b/test/fixture/reporter/gnu @@ -1,5 +1,5 @@ -"match.css":1.16-1.19: error: Invalid value for `color`: `123`; allowed: -"match.css":1.33-1.40: error: Invalid value for `border`: `1px unknown red`; allowed: || || +"match.css":1.16-1.19: error: Invalid value for `color` property: `123`; allowed: +"match.css":1.33-1.40: error: Invalid value for `border` property: `1px unknown red`; allowed: || || "match.css":1.46: error: Unknown property `unknown` "parse.css":1.11: error: Colon is expected -"parse.css":1.32-1.37: error: Invalid value for `color`: `red green`; allowed: +"parse.css":1.32-1.37: error: Invalid value for `color` property: `red green`; allowed: diff --git a/test/fixture/reporter/json b/test/fixture/reporter/json index 7597a57..c705d14 100644 --- a/test/fixture/reporter/json +++ b/test/fixture/reporter/json @@ -4,7 +4,7 @@ "line": 1, "column": 16, "property": "color", - "message": "Invalid value for `color`", + "message": "Invalid value for `color` property", "details": "Mismatch\n syntax: \n value: 123\n --------^" }, { @@ -12,7 +12,7 @@ "line": 1, "column": 33, "property": "border", - "message": "Invalid value for `border`", + "message": "Invalid value for `border` property", "details": "Mismatch\n syntax: || || \n value: 1px unknown red\n ------------^" }, { @@ -35,7 +35,7 @@ "line": 1, "column": 32, "property": "color", - "message": "Invalid value for `color`", + "message": "Invalid value for `color` property", "details": "Mismatch\n syntax: \n value: red green\n ------------^" } ] diff --git a/test/loc.js b/test/loc.js index 99698fb..c953a8e 100644 --- a/test/loc.js +++ b/test/loc.js @@ -16,61 +16,22 @@ describe('locations', function() { assert.strictEqual(error.message, 'Unknown property `abc`'); assert.strictEqual(error.line, 2); assert.strictEqual(error.column, 3); - assert.deepStrictEqual(error.loc, { - source: 'test', - start: { - offset: 12, - line: 2, - column: 3 - }, - end: { - offset: 18, - line: 2, - column: 9 - } - }); }); it('result should contain correct location of mismatch', function() { const error = validateString('.broken {\n color: rgb(1, green, 3);\n}', 'test').test[0]; - assert.strictEqual(error.message, 'Invalid value for `color`'); + assert.strictEqual(error.message, 'Invalid value for `color` property'); assert.strictEqual(error.line, 2); assert.strictEqual(error.column, 17); - assert.deepStrictEqual(error.loc, { - source: 'test', - start: { - offset: 26, - line: 2, - column: 17 - }, - end: { - offset: 31, - line: 2, - column: 22 - } - }); }); it('result should contain correct location of uncomplete mismatch', function() { const error = validateString('.broken {\n border: red 1xx solid;\n}', 'test').test[0]; - assert.strictEqual(error.message, 'Invalid value for `border`'); + assert.strictEqual(error.message, 'Invalid value for `border` property'); assert.strictEqual(error.line, 2); assert.strictEqual(error.column, 15); - assert.deepStrictEqual(error.loc, { - source: 'test', - start: { - offset: 24, - line: 2, - column: 15 - }, - end: { - offset: 27, - line: 2, - column: 18 - } - }); }); it('should not warn on custom properties', function() { diff --git a/test/validate.js b/test/validate.js index 438f642..4b06542 100644 --- a/test/validate.js +++ b/test/validate.js @@ -1,156 +1,124 @@ -const path = require('path'); const assert = require('assert'); -const fn = require('../lib'); -const fixturePath = path.join(__dirname, 'fixture/css'); +const { validate } = require('../lib'); -describe('validate functions', function() { - describe('validateString', function() { - it('should validate errors in CSS string', function() { - const errors = fn.validateString('foo { a: 1; color: bad; }', 'test'); - - assert.strictEqual(Array.isArray(errors.test), true); - assert.strictEqual(errors.test.length, 2); - }); - - it('filename should be optional', function() { - const errors = fn.validateString('foo {}'); - - assert.deepStrictEqual(Object.keys(errors), ['']); - }); - }); +function assertError(css, loc, expectedMsg) { + const res = validate(css); - it('validateDictionary', function() { - const errors = fn.validateDictionary({ - 'foo': 'foo { a: 1; color: bad; }', - 'bar': 'valid {}' - }); - - assert.deepStrictEqual(Object.keys(errors).sort(), ['bar', 'foo']); - assert.strictEqual(Array.isArray(errors.foo), true); - assert.strictEqual(errors.foo.length, 2); - assert.strictEqual(Array.isArray(errors.bar), true); - assert.strictEqual(errors.bar.length, 0); - }); + assert.strictEqual(Array.isArray(res), true, 'should return an array of errors'); + assert.strictEqual(res.length > 0, true, 'should return errors'); + assert.deepStrictEqual(res[0].message, expectedMsg); - describe('validateFile', function() { - it('should validate file content', function() { - const filename = path.join(fixturePath, 'style.css'); - const errors = fn.validateFile(filename); + if (loc) { + const { offset, line, column } = res[0]; + const expectedOffset = loc.length - 1 - (css.slice(0, loc.length - 1).match(/\n/g) || []).length; + const lines = css.slice(0, expectedOffset).split(/\n/); - assert.deepStrictEqual(Object.keys(errors), [filename]); - assert.strictEqual(errors[filename].length, 2); - assert.deepStrictEqual(errors[filename].map(function(error) { - return error.name; - }), ['SyntaxReferenceError', 'SyntaxMatchError']); + assert.deepStrictEqual({ offset, line, column }, { + offset: expectedOffset, + line: lines.length, + column: lines.pop().length + 1 }); + } +} - it('should not fail when file not found', function() { - const filename = String(Math.random()); - const errors = fn.validateFile(filename); - - assert.deepStrictEqual(Object.keys(errors), [filename]); - assert.strictEqual(errors[filename].length, 1); - assert.strictEqual(errors[filename][0].name, 'Error'); - }); - }); - - describe('validatePath', function() { - it('should validate all files with .css extension on path', function() { - const errors = fn.validatePath(fixturePath); +function assertOk(css) { + assert.deepStrictEqual(validate(css), []); +} - assert.deepStrictEqual(Object.keys(errors).map(function(filename) { - return path.relative(fixturePath, filename); - }).sort(), ['bar/style.css', 'foo/style.css', 'style.css']); - - Object.keys(errors).forEach(function(filename) { - assert.strictEqual(errors[filename].length, 2); - assert.deepStrictEqual(errors[filename].map(function(error) { - return error.name; - }), ['SyntaxReferenceError', 'SyntaxMatchError']); - }); - }); - - it('should validate all files that match shouldBeValidated on path', function() { - const errors = fn.validatePath(fixturePath, function(filename) { - return path.basename(filename) === 'not.a.css.file'; - }); - - assert.deepStrictEqual(Object.keys(errors).map(function(filename) { - return path.relative(fixturePath, filename); - }).sort(), ['bar/not.a.css.file']); - - Object.keys(errors).forEach(function(filename) { - assert.strictEqual(errors[filename].length, 2); - assert.deepStrictEqual(errors[filename].map(function(error) { - return error.name; - }), ['SyntaxReferenceError', 'SyntaxMatchError']); - }); - }); - - it('should not fail when path is invalid', function() { - const path = String(Math.random()); - const errors = fn.validatePath(path); - - assert.deepStrictEqual(Object.keys(errors), [path]); - assert.strictEqual(errors[path].length, 1); - assert.strictEqual(errors[path][0].name, 'Error'); - }); +describe('validate functions', function() { + describe('declaration', () => { + it('unknown property', () => + assertError( + '.a {\n foo: 123;\n}', + ' ^', + 'Unknown property `foo`' + ) + ); + + it('bad value', () => + assertError( + '.a {\n color: 123;\n}', + ' ^', + 'Invalid value for `color` property' + ) + ); + + it('bad value #2', () => + assertError( + '.a {\n color: red green;\n}', + ' ^', + 'Invalid value for `color` property' + ) + ); + + it('bad value #3', () => + assertError( + '.a {\n border: 1px unknown red;\n}', + ' ^', + 'Invalid value for `border` property' + ) + ); + + it('ok', () => + assertOk('.a {\n color: green;\n}') + ); }); - describe('validatePathList', function() { - it('should validate all files with .css extension on paths', function() { - const errors = fn.validatePathList([ - path.join(fixturePath, 'bar'), - path.join(fixturePath, 'foo') - ]); - - assert.deepStrictEqual(Object.keys(errors).map(function(filename) { - return path.relative(fixturePath, filename); - }).sort(), ['bar/style.css', 'foo/style.css']); - - Object.keys(errors).forEach(function(filename) { - assert.strictEqual(errors[filename].length, 2); - assert.deepStrictEqual(errors[filename].map(function(error) { - return error.name; - }), ['SyntaxReferenceError', 'SyntaxMatchError']); - }); - }); - - it('should validate all files that match shouldBeValidated on path', function() { - const errors = fn.validatePathList([ - path.join(fixturePath, 'bar'), - path.join(fixturePath, 'foo') - ], function(filename) { - return path.basename(filename) === 'not.a.css.file'; - }); - - assert.deepStrictEqual(Object.keys(errors).map(function(filename) { - return path.relative(fixturePath, filename); - }).sort(), ['bar/not.a.css.file']); - - Object.keys(errors).forEach(function(filename) { - assert.strictEqual(errors[filename].length, 2); - assert.deepStrictEqual(errors[filename].map(function(error) { - return error.name; - }), ['SyntaxReferenceError', 'SyntaxMatchError']); - }); - }); - - it('should not fail when path is invalid', function() { - const validPath = path.join(fixturePath, 'bar'); - const invalidPath = Math.random(); - const errors = fn.validatePathList([ - validPath, - invalidPath - ]); - - assert.deepStrictEqual(Object.keys(errors), [ - path.join(validPath, 'style.css'), - String(invalidPath) - ]); - assert.strictEqual(errors[path.join(validPath, 'style.css')].length, 2); - assert.strictEqual(errors[invalidPath].length, 1); - assert.strictEqual(errors[invalidPath][0].name, 'Error'); - }); + describe('atrule', () => { + it('unknown at-rule', () => + assertError( + '@a { color: green }', + '^', + 'Unknown at-rule `@a`' + ) + ); + + it('at-rule has no prelude', () => + assertError( + '@font-face xxx { color: green }', + ' ^', + 'At-rule `@font-face` should not contain a prelude' + ) + ); + + it('at-rule should has a prelude', () => + assertError( + '@document { color: green }', + '^', + 'At-rule `@document` should contain a prelude' + ) + ); + + it('bad value for at-rule prelude', () => + assertError( + '@document domain( foo /***/) { }', + ' ^', + 'Invalid value for `@document` prelude' + ) + ); + + it('ok at-rule prelude', () => + assertOk('@document url(foo) { }') + ); + + it('bad at-rule descriptor', () => + assertError( + '@font-face { color: green }', + ' ^', + 'Unknown at-rule descriptor `color`' + ) + ); + + it('bad at-rule descriptor value', () => + assertError( + '@font-face { font-display: foo }', + ' ^', + 'Invalid value for `font-display` descriptor' + ) + ); + + it('ok at-rule descriptor', () => + assertOk('@font-face { font-display: swap }') + ); }); }); diff --git a/test/validators.js b/test/validators.js new file mode 100644 index 0000000..59d3685 --- /dev/null +++ b/test/validators.js @@ -0,0 +1,156 @@ +const path = require('path'); +const assert = require('assert'); +const fn = require('../lib'); +const fixturePath = path.join(__dirname, 'fixture/css'); + +describe('validators', function() { + describe('validateString', function() { + it('should validate errors in CSS string', function() { + const errors = fn.validateString('foo { a: 1; color: bad; }', 'test'); + + assert.strictEqual(Array.isArray(errors.test), true); + assert.strictEqual(errors.test.length, 2); + }); + + it('filename should be optional', function() { + const errors = fn.validateString('foo {}'); + + assert.deepStrictEqual(Object.keys(errors), ['']); + }); + }); + + it('validateDictionary', function() { + const errors = fn.validateDictionary({ + 'foo': 'foo { a: 1; color: bad; }', + 'bar': 'valid {}' + }); + + assert.deepStrictEqual(Object.keys(errors).sort(), ['bar', 'foo']); + assert.strictEqual(Array.isArray(errors.foo), true); + assert.strictEqual(errors.foo.length, 2); + assert.strictEqual(Array.isArray(errors.bar), true); + assert.strictEqual(errors.bar.length, 0); + }); + + describe('validateFile', function() { + it('should validate file content', function() { + const filename = path.join(fixturePath, 'style.css'); + const errors = fn.validateFile(filename); + + assert.deepStrictEqual(Object.keys(errors), [filename]); + assert.strictEqual(errors[filename].length, 2); + assert.deepStrictEqual(errors[filename].map(function(error) { + return error.name; + }), ['SyntaxReferenceError', 'SyntaxMatchError']); + }); + + it('should not fail when file not found', function() { + const filename = String(Math.random()); + const errors = fn.validateFile(filename); + + assert.deepStrictEqual(Object.keys(errors), [filename]); + assert.strictEqual(errors[filename].length, 1); + assert.strictEqual(errors[filename][0].name, 'Error'); + }); + }); + + describe('validatePath', function() { + it('should validate all files with .css extension on path', function() { + const errors = fn.validatePath(fixturePath); + + assert.deepStrictEqual(Object.keys(errors).map(function(filename) { + return path.relative(fixturePath, filename); + }).sort(), ['bar/style.css', 'foo/style.css', 'style.css']); + + Object.keys(errors).forEach(function(filename) { + assert.strictEqual(errors[filename].length, 2); + assert.deepStrictEqual(errors[filename].map(function(error) { + return error.name; + }), ['SyntaxReferenceError', 'SyntaxMatchError']); + }); + }); + + it('should validate all files that match shouldBeValidated on path', function() { + const errors = fn.validatePath(fixturePath, function(filename) { + return path.basename(filename) === 'not.a.css.file'; + }); + + assert.deepStrictEqual(Object.keys(errors).map(function(filename) { + return path.relative(fixturePath, filename); + }).sort(), ['bar/not.a.css.file']); + + Object.keys(errors).forEach(function(filename) { + assert.strictEqual(errors[filename].length, 2); + assert.deepStrictEqual(errors[filename].map(function(error) { + return error.name; + }), ['SyntaxReferenceError', 'SyntaxMatchError']); + }); + }); + + it('should not fail when path is invalid', function() { + const path = String(Math.random()); + const errors = fn.validatePath(path); + + assert.deepStrictEqual(Object.keys(errors), [path]); + assert.strictEqual(errors[path].length, 1); + assert.strictEqual(errors[path][0].name, 'Error'); + }); + }); + + describe('validatePathList', function() { + it('should validate all files with .css extension on paths', function() { + const errors = fn.validatePathList([ + path.join(fixturePath, 'bar'), + path.join(fixturePath, 'foo') + ]); + + assert.deepStrictEqual(Object.keys(errors).map(function(filename) { + return path.relative(fixturePath, filename); + }).sort(), ['bar/style.css', 'foo/style.css']); + + Object.keys(errors).forEach(function(filename) { + assert.strictEqual(errors[filename].length, 2); + assert.deepStrictEqual(errors[filename].map(function(error) { + return error.name; + }), ['SyntaxReferenceError', 'SyntaxMatchError']); + }); + }); + + it('should validate all files that match shouldBeValidated on path', function() { + const errors = fn.validatePathList([ + path.join(fixturePath, 'bar'), + path.join(fixturePath, 'foo') + ], function(filename) { + return path.basename(filename) === 'not.a.css.file'; + }); + + assert.deepStrictEqual(Object.keys(errors).map(function(filename) { + return path.relative(fixturePath, filename); + }).sort(), ['bar/not.a.css.file']); + + Object.keys(errors).forEach(function(filename) { + assert.strictEqual(errors[filename].length, 2); + assert.deepStrictEqual(errors[filename].map(function(error) { + return error.name; + }), ['SyntaxReferenceError', 'SyntaxMatchError']); + }); + }); + + it('should not fail when path is invalid', function() { + const validPath = path.join(fixturePath, 'bar'); + const invalidPath = Math.random(); + const errors = fn.validatePathList([ + validPath, + invalidPath + ]); + + assert.deepStrictEqual(Object.keys(errors), [ + path.join(validPath, 'style.css'), + String(invalidPath) + ]); + assert.strictEqual(errors[path.join(validPath, 'style.css')].length, 2); + assert.strictEqual(errors[invalidPath].length, 1); + assert.strictEqual(errors[invalidPath][0].name, 'Error'); + }); + }); +});