diff --git a/.gitignore b/.gitignore index 3c3629e..55371e5 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ node_modules +.vscode \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 04f6d65..2df9852 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,4 +2,6 @@ sudo: false language: node_js node_js: - "0.12" - - "0.10" \ No newline at end of file + - "0.10" +env: + - NODE_ENV=test \ No newline at end of file diff --git a/README.md b/README.md index 1581632..a828523 100644 --- a/README.md +++ b/README.md @@ -328,10 +328,46 @@ sassaby.includedMixin('appearance').calledWithArgs('button').hasNumDeclarations( ``` #### declares -Assert that the mixin makes a declaration of the given rule-property pair. +Can be either called with a string as property value or with an Array object. + +Called with a String it asserts that the mixin makes a declaration of the given rule-property pair. ```js sassaby.includedMixin('appearance').calledWithArgs('button').declares('-webkit-appearance', 'button'); ``` +Called with an Object it asserts that the mixin makes a declaration of the given property with the provided fallback values. +E.g. for the mixin: +``` +@mixin font-size($font-size: 1.6) { + font-size: $font-size * 16 + px; + font-size: $font-size + rem; +} +``` +```js +sassaby.includedMixin('font-size').calledWithArgs('2').declares('max-height', ['32px', '2rem']); +``` + +#### declaresInSelector +Assert that the mixin makes a declaration of the given rule-property pair inside the provided selector. +E.g. for the mixin: +``` +@mixin clearfix() { + &:before, + &:after { + content: " "; // 1 + display: table; // 2 + } + &:after { + clear: both; + } +} +``` +```js +sassaby.includedMixin('clearfix').called().declaresInSelector(':before', 'content', " "); +sassaby.includedMixin('clearfix').called().declaresInSelector(':before', 'display', 'table'); +sassaby.includedMixin('clearfix').called().declaresInSelector(':after', 'content', " "); +sassaby.includedMixin('clearfix').called().declaresInSelector(':after', 'display', 'table'); +sassaby.includedMixin('clearfix').called().declaresInSelector(':after', 'clear', 'both'); +``` #### doesNotDeclare Assert that the mixin does not make a declaration of the given rule-property pair. diff --git a/package.json b/package.json index f7f859b..a0d0ffa 100644 --- a/package.json +++ b/package.json @@ -1,26 +1,37 @@ { - "name": "sassaby", - "version": "2.1.1", - "description": "A unit testing library for SASS", - "keywords": ["sass", "scss", "testing", "unit testing", "bdd", "tdd", "test"], - "author": "Ryan Bahniuk ", - "license": "MIT", - "repository": { - "type": "git", - "url": "https://github.com/ryanbahniuk/sassaby" - }, - "main": "src/sassaby", - "dependencies": { - "css": "^2.2.0", - "cssmin": "^0.4.3", - "mocha": "^2.2.5", - "node-sass": "^3.1.2" - }, - "devDependencies": { - "proxyquire": "^1.5.0", - "sinon": "^1.14.1" - }, - "scripts": { - "test": "NODE_ENV=test mocha" - } -} + "name": "sassaby", + "version": "2.1.1", + "description": "A unit testing library for SASS", + "keywords": [ + "sass", + "scss", + "testing", + "unit testing", + "bdd", + "tdd", + "test" + ], + "author": "Ryan Bahniuk ", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/ryanbahniuk/sassaby" + }, + "main": "src/sassaby", + "dependencies": { + "css": "^2.2.0", + "cssmin": "^0.4.3", + "mocha": "^2.2.5", + "node-sass": "2.0.1", + "request": "2.81.0", + "normalize-path": "^2.1.1" + }, + "devDependencies": { + "cross-env": "^5.1.0", + "proxyquire": "^1.5.0", + "sinon": "^1.14.1" + }, + "scripts": { + "test": "cross-env NODE_ENV=test & mocha" + } +} \ No newline at end of file diff --git a/src/mixinResult.js b/src/mixinResult.js index 97ce09a..ab2ae4f 100644 --- a/src/mixinResult.js +++ b/src/mixinResult.js @@ -117,9 +117,25 @@ MixinResult.prototype = { }, declares: function(property, value) { - var declaration = parsers.findDeclaration(this.ast, property); - var declarationValue = declaration ? utilities.scrubQuotes(declaration.value) : ''; + var declaration, declarationValue; + if (Array.isArray(value) && value.length > 1) { + declaration = parsers.findDeclarations(this.ast, property); + declarationValue = []; + declaration.forEach(function(declaration){ + declarationValue.push(declaration.value); + }); + } else { + declaration = parsers.findDeclaration(this.ast, property); + declarationValue = declaration ? utilities.scrubQuotes(declaration.value) : ''; + } var message = 'Value: ' + declarationValue + ' does not equal value: ' + value + '.'; + assert.equal(declarationValue.toString(), value.toString(), message); + }, + + declaresInSelector: function(selector, property, value) { + var declaration = parsers.findDeclarationInSelector(this.ast, selector, property); + var declarationValue = declaration ? utilities.scrubQuotes(declaration.value) : ''; + var message = 'In Selector:'+ selector + ' the value: ' + declarationValue + ' does not equal value: ' + value + '.'; assert.equal(declarationValue, value.toString(), message); }, diff --git a/src/parsers.js b/src/parsers.js index c88971e..5b53f66 100644 --- a/src/parsers.js +++ b/src/parsers.js @@ -23,7 +23,6 @@ function hasSelectorValue(rule, selectorValue) { function findDeclarationProperty(rule, declarationProperty) { var foundDeclaration; - if (rule.declarations) { rule.declarations.forEach(function(declaration) { if (declaration.property === declarationProperty) { @@ -31,10 +30,21 @@ function findDeclarationProperty(rule, declarationProperty) { } }); } - return foundDeclaration; } +function findDeclarationsProperties(rule, declarationProperty) { + var foundDeclarations = []; + if (rule.declarations) { + rule.declarations.forEach(function(declaration) { + if (declaration.property === declarationProperty) { + foundDeclarations.push(declaration); + } + }); + } + return foundDeclarations; +} + function isFontFace(rule) { if (rule.type === 'font-face') { return true; @@ -57,10 +67,8 @@ var Parsers = { }); return count; }, - findDeclaration: function(ast, property) { var found; - ast.stylesheet.rules.forEach(function(rule) { if (rule.type === 'media') { rule.rules.forEach(function(rule) { @@ -74,6 +82,45 @@ var Parsers = { return found; }, + findDeclarations: function(ast, property) { + var found = []; + ast.stylesheet.rules.forEach(function(rule) { + if (rule.type === 'media') { + rule.rules.forEach(function(rule) { + found.push(findDeclarationsProperties(rule, property)); + }); + } else { + found.push(findDeclarationsProperties(rule, property)); + } + }); + var declarationsArray; + found.forEach(function(value) { + if(value.length !== 0) { + declarationsArray = value; + } + }) + return declarationsArray; + }, + + findDeclarationInSelector: function(ast, selector, property) { + var found; + if (selector.indexOf("&") > -1) { + selector = selector.replace("&", ".test"); + } + if (selector.indexOf("@at-root") > -1) { + selector = selector.replace("@at-root ", ""); + } + ast.stylesheet.rules.forEach(function(rule) { + for(var i = 0; i < rule.selectors.length; i++) { + if(rule.selectors[i].indexOf(selector) >= 0 ) { + found = found || findDeclarationProperty(rule, property); + } + } + }); + console.log(found); + return found; + }, + findMedia: function(ast) { var found = []; diff --git a/src/sassaby.js b/src/sassaby.js index 2f60025..52d9638 100644 --- a/src/sassaby.js +++ b/src/sassaby.js @@ -5,6 +5,7 @@ var assert = require('assert'); var Mixin = require('./types/mixin'); var Func = require('./types/func'); var parsers = require('./parsers'); +var normalize = require('normalize-path'); function setVariables(varz) { var sassVariables = ''; @@ -17,7 +18,7 @@ function setVariables(varz) { function setDependencies(dependencies) { var sassImports = ''; dependencies.forEach(function(fileName) { - sassImports = sassImports + "@import '" + fileName + "';"; + sassImports = sassImports + "@import '" + normalize(fileName) + "';"; }); return sassImports; } @@ -25,7 +26,7 @@ function setDependencies(dependencies) { function Sassaby(path, options) { options = options || {}; this.path = path; - this.file = fs.readFileSync(path).toString(); + this.file = fs.readFileSync(normalize(path), 'utf-8').toString(); this.variables = ''; this.dependencies = ''; diff --git a/test/fixtures/ast.json b/test/fixtures/ast.json index 11fd863..f76ad35 100644 --- a/test/fixtures/ast.json +++ b/test/fixtures/ast.json @@ -86,6 +86,69 @@ "column": 18 } } + }, + { + "type": "declaration", + "property": "max-height", + "value": "16px", + "position": { + "start": { + "line": 9, + "column": 3 + }, + "end": { + "line": 9, + "column": 18 + } + } + }, + { + "type": "declaration", + "property": "max-height", + "value": "1rem", + "position": { + "start": { + "line": 10, + "column": 3 + }, + "end": { + "line": 10, + "column": 18 + } + } + } + ], + "position": { + "start": { + "line": 11, + "column": 1 + }, + "end": { + "line": 13, + "column": 2 + } + } + }, + { + "type": "rule", + "selectors": [ + ".test.testInSelector" + ], + "declarations": [ + { + "type": "declaration", + "property": "color", + "value": "red", + "position": { + "start": { + "line": 11, + "column": 3 + }, + "end": { + "line": 11, + "column": 13 + } + } } ], "position": { @@ -94,7 +157,7 @@ "column": 1 }, "end": { - "line": 9, + "line": 14, "column": 2 } } diff --git a/test/fixtures/astMediaQuery.json b/test/fixtures/astMediaQuery.json index 22453c4..89106b5 100644 --- a/test/fixtures/astMediaQuery.json +++ b/test/fixtures/astMediaQuery.json @@ -41,6 +41,36 @@ "column": 20 } } + }, + { + "type": "declaration", + "property": "max-height", + "value": "16px", + "position": { + "start": { + "line": 5, + "column": 3 + }, + "end": { + "line": 5, + "column": 18 + } + } + }, + { + "type": "declaration", + "property": "max-height", + "value": "1rem", + "position": { + "start": { + "line": 6, + "column": 3 + }, + "end": { + "line": 6, + "column": 18 + } + } } ], "position": { @@ -49,7 +79,7 @@ "column": 3 }, "end": { - "line": 5, + "line": 7, "column": 4 } } @@ -61,7 +91,7 @@ "column": 1 }, "end": { - "line": 6, + "line": 8, "column": 2 } } diff --git a/test/fixtures/sample.scss b/test/fixtures/sample.scss index 0d8ecbd..24d1da8 100644 --- a/test/fixtures/sample.scss +++ b/test/fixtures/sample.scss @@ -94,3 +94,13 @@ } } +@mixin fallback-font-size { + font-size: 16px; + font-size: 1rem; +} + +@mixin hover-link { + &:hover { + text-transform: none; + } +} diff --git a/test/integrationTests.js b/test/integrationTests.js index ad820c5..d47576c 100644 --- a/test/integrationTests.js +++ b/test/integrationTests.js @@ -218,4 +218,23 @@ describe('sample.scss', function() { mixin.called().calls('create-header'); }); }); + describe('fallback-font-size', function() { + beforeEach(function() { + mixin = sassaby.includedMixin('fallback-font-size'); + }); + + it('should have the correct color declaration', function() { + mixin.called().declares('font-size', ['16px', '1rem']); + }); + }); + + describe('hover-link', function() { + beforeEach(function() { + mixin = sassaby.includedMixin('hover-link'); + }); + + it('should have the correct color declaration', function() { + mixin.called().declaresInSelector('&:hover', 'text-transform', 'none'); + }); + }) }); diff --git a/test/integrationTestsWithBlocks.js b/test/integrationTestsWithBlocks.js index 3fc8ee8..63fc45c 100644 --- a/test/integrationTestsWithBlocks.js +++ b/test/integrationTestsWithBlocks.js @@ -126,4 +126,4 @@ describe('sample-with-blocks.scss', function() { mixin.calledWithBlockAndArgs(block, color).declares('width', '100%'); }); }); -}); +}); \ No newline at end of file diff --git a/test/parsersTests.js b/test/parsersTests.js index 130b318..7598ce1 100644 --- a/test/parsersTests.js +++ b/test/parsersTests.js @@ -8,9 +8,9 @@ var astNoSelectors = require('./fixtures/astNoSelectors.json'); var astMediaQuery = require('./fixtures/astMediaQuery.json'); var astMediaQueryFontFace = require('./fixtures/astMediaQueryFontFace.json'); -describe('Parsers', function() { - describe('escapeCharacters', function() { - it('should escape all special characters that can be used in a filename', function() { +describe('Parsers', function () { + describe('escapeCharacters', function () { + it('should escape all special characters that can be used in a filename', function () { assert.equal(parsers.escapeCharacters('test/this'), 'test\/this'); assert.equal(parsers.escapeCharacters('test.this'), 'test\.this'); assert.equal(parsers.escapeCharacters('test*this'), 'test\*this'); @@ -22,28 +22,28 @@ describe('Parsers', function() { }); }); - describe('hasSelectorValue', function() { + describe('hasSelectorValue', function () { var ruleWithoutSelectors = {}; var rule = { type: 'rule', selectors: ['.test', '.second'] }; - it('should return true if the selector is in the rule', function() { + it('should return true if the selector is in the rule', function () { assert(parsers.hasSelectorValue(rule, '.test')); assert(parsers.hasSelectorValue(rule, '.second')); }); - it('should return false if the selector is not in the rule', function() { + it('should return false if the selector is not in the rule', function () { assert(!parsers.hasSelectorValue(rule, '.blah')); }); - it('should return false if the rule has no selectors', function() { + it('should return false if the rule has no selectors', function () { assert(!parsers.hasSelectorValue(ruleWithoutSelectors, '.test')); }); }); - describe('findDeclarationProperty', function() { + describe('findDeclarationProperty', function () { var declaration = { 'type': 'declaration', 'property': 'color' @@ -63,20 +63,20 @@ describe('Parsers', function() { declarations: [declaration] }; - it('should return the declaration if it is in the rule', function() { + it('should return the declaration if it is in the rule', function () { assert.equal(parsers.findDeclarationProperty(rule, 'color'), declaration); }); - it('should return undefined if the declaration is not in the rule', function() { + it('should return undefined if the declaration is not in the rule', function () { assert.equal(parsers.findDeclarationProperty(ruleWithoutDeclaration, 'color'), undefined); }); - it('should return undefined if the rule has no declarations', function() { + it('should return undefined if the rule has no declarations', function () { assert.equal(parsers.findDeclarationProperty(ruleWithoutDeclarations, 'color'), undefined); }); }); - describe('isFontFace', function() { + describe('isFontFace', function () { var font = { type: 'font-face' }; @@ -85,27 +85,35 @@ describe('Parsers', function() { type: 'media' }; - it('should return true if the rule is a font-face type', function() { + it('should return true if the rule is a font-face type', function () { assert(parsers.isFontFace(font)); }); - it('should return false if the rule is not a font-face type', function() { + it('should return false if the rule is not a font-face type', function () { assert(!parsers.isFontFace(media)); }); }); - describe('countDeclarations', function() { - it('should return the number of declarations in output', function() { - assert.equal(parsers.countDeclarations(ast), 4); + describe('countDeclarations', function () { + it('should return the number of declarations in output', function () { + assert.equal(parsers.countDeclarations(ast), 7); }); - it('should return 0 if there are no declarations', function() { - assert.equal(parsers.countDeclarations({stylesheet: {rules: [{declarations: []}]}}), 0); + it('should return 0 if there are no declarations', function () { + assert.equal(parsers.countDeclarations({ + stylesheet: { + rules: [ + { + declarations: [] + } + ] + } + }), 0); }); }); - describe('findDeclaration', function() { - it('should return the declaration object if the property is found', function() { + describe('findDeclaration', function () { + it('should return the declaration object if the property is found', function () { var declaration = { 'type': 'declaration', 'property': 'background-color', @@ -124,7 +132,7 @@ describe('Parsers', function() { assert.deepEqual(parsers.findDeclaration(ast, 'background-color'), declaration); }); - it('should return the first declaration object if two of the property are found', function() { + it('should return the first declaration object if two of the property are found', function () { var declaration = { 'type': 'declaration', 'property': 'color', @@ -143,134 +151,244 @@ describe('Parsers', function() { assert.deepEqual(parsers.findDeclaration(ast, 'color'), declaration); }); - it('should return undefined if the declaration is not found', function() { + it('should return undefined if the declaration is not found', function () { assert.equal(parsers.findDeclaration(ast, 'display'), undefined); }); }); + describe('findDeclarations', function () { + it('should return the declaration object array if the property is found', function () { + var declaration = [ + { + "type": "declaration", + "property": "max-height", + "value": "16px", + "position": { + "start": { + "line": 9, + "column": 3 + }, + "end": { + "line": 9, + "column": 18 + } + } + }, { + "type": "declaration", + "property": "max-height", + "value": "1rem", + "position": { + "start": { + "line": 10, + "column": 3 + }, + "end": { + "line": 10, + "column": 18 + } + } + } + ]; + assert.deepEqual(parsers.findDeclarations(ast, 'max-height'), declaration); + }); + it('should return the declaration object array if the property is found inside a media query', function() { + var declaration = [ + { + "type": "declaration", + "property": "max-height", + "value": "16px", + "position": { + "start": { + "line": 5, + "column": 3 + }, + "end": { + "line": 5, + "column": 18 + } + } + }, { + "type": "declaration", + "property": "max-height", + "value": "1rem", + "position": { + "start": { + "line": 6, + "column": 3 + }, + "end": { + "line": 6, + "column": 18 + } + } + } + ]; + assert.deepEqual(parsers.findDeclarations(astMediaQuery, 'max-height'), declaration); + }) + }); + + describe('findDeclarationInSelector', function() { + it('should return the declaration Object found in the specified selector when @at-root is being used', function () { + var declaration = { + 'type': 'declaration', + 'property': 'color', + 'value': 'red', + 'position': { + 'start': { + 'line': 11, + 'column': 3 + }, + 'end': { + 'line': 11, + 'column': 13 + } + } + }; + assert.deepEqual(parsers.findDeclarationInSelector(ast, '@at-root .testInSelector', 'color'), declaration); + }) + + it('should return the declaration Object found in the specified selector when & selector is used', function () { + var declaration = { + 'type': 'declaration', + 'property': 'color', + 'value': 'red', + 'position': { + 'start': { + 'line': 11, + 'column': 3 + }, + 'end': { + 'line': 11, + 'column': 13 + } + } + }; + assert.deepEqual(parsers.findDeclarationInSelector(ast, '&.testInSelector', 'color'), declaration); + }) + }); - describe('findMedia', function() { - it('should return the first rule of the type media', function() { + describe('findMedia', function () { + it('should return the first rule of the type media', function () { var media = parsers.findMedia(astMediaQuery); assert.equal(media.type, 'media'); assert.equal(media.media, 'screen and (min-width: 600px)'); }); - it('should return undefined if there are no rules of the type media', function() { + it('should return undefined if there are no rules of the type media', function () { var media = parsers.findMedia(ast); assert.deepEqual(media, undefined); }); }); - describe('hasSelector', function() { - it('should return true if the selector is defined by itself', function() { + describe('hasSelector', function () { + it('should return true if the selector is defined by itself', function () { assert(parsers.hasSelector(ast, '.test')); }); - it('should return true if the selector is defined in a list', function() { + it('should return true if the selector is defined in a list', function () { assert(parsers.hasSelector(ast, '.hello')); assert(parsers.hasSelector(ast, '.blah')); }); - it('should return true if the selector is defined in a media query', function() { + it('should return true if the selector is defined in a media query', function () { assert(parsers.hasSelector(astMediaQuery, 'body')); }); - it('should return false if the selector not defined', function() { + it('should return false if the selector not defined', function () { assert(!parsers.hasSelector(ast, '.not-defined')); }); - it('should return false if no stylesheet is defined', function() { + it('should return false if no stylesheet is defined', function () { assert(!parsers.hasSelector(astNoSelectors, '.test')); }); - it('should return false if no rules are defined', function() { + it('should return false if no rules are defined', function () { assert(!parsers.hasSelector(astNoSelectors, '.test')); }); }); - describe('hasFontFace', function() { - it('should return true if font-face is defined', function() { + describe('hasFontFace', function () { + it('should return true if font-face is defined', function () { assert(parsers.hasFontFace(astNoSelectors)); }); - it('should return true if font-face is defined inside a media query', function() { + it('should return true if font-face is defined inside a media query', function () { assert(parsers.hasFontFace(astMediaQueryFontFace)); }); - it('should return false if font-face not defined', function() { + it('should return false if font-face not defined', function () { assert(!parsers.hasFontFace(ast)); }); }); - describe('hasImport', function() { + describe('hasImport', function () { var name = 'test'; - context('should return true if the import exists', function() { - it('if it uses single quotes', function() { + context('should return true if the import exists', function () { + it('if it uses single quotes', function () { var sass = "@import '" + name + "';"; assert(parsers.hasImport(sass, name)); }); - it('if it is nested in a directory', function() { + it('if it is nested in a directory', function () { var sass = "@import 'testing/" + name + "';"; assert(parsers.hasImport(sass, 'testing/' + name)); }); - it('if it uses double quotes', function() { + it('if it uses double quotes', function () { var sass = '@import "' + name + '";'; assert(parsers.hasImport(sass, name)); }); - it('if it has no space', function() { + it('if it has no space', function () { var sass = '@import"' + name + '";'; assert(parsers.hasImport(sass, name)); }); - it('if it has multiple on the same line', function() { + it('if it has multiple on the same line', function () { var sass = '@import "hello"; @import"' + name + '";'; assert(parsers.hasImport(sass, name)); }); - it('if it has multiple on different lines', function() { + it('if it has multiple on different lines', function () { var sass = '@import "hello";\n@import"' + name + '";'; assert(parsers.hasImport(sass, name)); }); - it('if it has multiple in a comma separated list', function() { + it('if it has multiple in a comma separated list', function () { var sass = '@import "hello", "' + name + '";'; assert(parsers.hasImport(sass, name)); }); }); - context('should return false if the import does not exist', function() { + context('should return false if the import does not exist', function () { var alternate = 'nope'; - it('if it uses single quotes', function() { + it('if it uses single quotes', function () { var sass = "@import '" + alternate + "';"; assert(!parsers.hasImport(sass, name)); }); - it('if it uses double quotes', function() { + it('if it uses double quotes', function () { var sass = '@import "' + alternate + '";'; assert(!parsers.hasImport(sass, name)); }); - it('if it has no space', function() { + it('if it has no space', function () { var sass = '@import"' + alternate + '";'; assert(!parsers.hasImport(sass, name)); }); - it('if it has multiple on the same line', function() { + it('if it has multiple on the same line', function () { var sass = '@import "hello"; @import"' + alternate + '";'; assert(!parsers.hasImport(sass, name)); }); - it('if it has multiple on different lines', function() { + it('if it has multiple on different lines', function () { var sass = '@import "hello";\n@import"' + alternate + '";'; assert(!parsers.hasImport(sass, name)); }); - it('if it has multiple in a comma separated list', function() { + it('if it has multiple in a comma separated list', function () { var sass = '@import "hello", "' + alternate + '";'; assert(!parsers.hasImport(sass, name)); });