diff --git a/src/language/CSSUtils.js b/src/language/CSSUtils.js index 725dbad14d6..c0dae818bd1 100644 --- a/src/language/CSSUtils.js +++ b/src/language/CSSUtils.js @@ -640,6 +640,8 @@ define(function (require, exports, module) { completeSelectors += info.selector; } return completeSelectors; + } else if (useGroup && info.selectorGroup) { + return info.selectorGroup; } return info.selector; @@ -843,17 +845,18 @@ define(function (require, exports, module) { } function _maybeProperty() { - return (state.state !== "top" && state.state !== "block" && + return (/^-(moz|ms|o|webkit)-$/.test(token) || + (state.state !== "top" && state.state !== "block" && // Has a semicolon as in "rgb(0,0,0);", but not one of those after a LESS // mixin parameter variable as in ".size(@width; @height)" - stream.string.indexOf(";") !== -1 && !/\([^)]+;/.test(stream.string)); + stream.string.indexOf(";") !== -1 && !/\([^)]+;/.test(stream.string))); } function _skipProperty() { var prevToken = ""; while (token !== ";") { // Skip tokens until the closing brace if we find an interpolated variable. - if (/#\{$/.test(token) || (token === "{" && /@$/.test(prevToken))) { + if (/#\{$/.test(token) || (token === "{" && /[#@]$/.test(prevToken))) { _skipToClosingBracket("{"); if (token === "}") { _nextToken(); // Skip the closing brace @@ -865,13 +868,14 @@ define(function (require, exports, module) { // If there is a '{' or '}' before the ';', // then stop skipping. if (token === "{" || token === "}") { - return; + return false; // can't tell if the entire property is skipped } prevToken = token; if (!_nextTokenSkippingComments()) { break; } } + return true; // skip the entire property } function _getParentSelectors() { @@ -892,7 +896,8 @@ define(function (require, exports, module) { // Everything until the next ',' or '{' is part of the current selector while ((token !== "," && token !== "{") || - (token === "{" && /@$/.test(currentSelector))) { + (token === "{" && /[#@]$/.test(currentSelector)) || + (token === "," && !_hasNonWhitespace(currentSelector))) { if (token === "{") { // Append the interpolated variable to selector currentSelector += _skipToClosingBracket("{"); @@ -913,7 +918,14 @@ define(function (require, exports, module) { // Collect everything inside the parentheses as a whole chunk so that // commas inside the parentheses won't be identified as selector separators // by while loop. - currentSelector += _skipToClosingBracket("("); + if (_hasNonWhitespace(currentSelector)) { + currentSelector += _skipToClosingBracket("("); + } else { + // Nothing in currentSelector yet. Skip to the closing parenthesis + // without collecting the selector since a selector cannot start with + // an opening parenthesis. + _skipToClosingBracket("("); + } } else if (_hasNonWhitespace(token) || _hasNonWhitespace(currentSelector)) { currentSelector += token; } @@ -1016,7 +1028,7 @@ define(function (require, exports, module) { if (sgLine === declListStartLine) { endChar = declListStartChar; } - selectorGroup += lines[sgLine].substring(startChar, endChar); + selectorGroup += lines[sgLine].substring(startChar, endChar).trim(); } selectorGroup = selectorGroup.trim(); } @@ -1115,13 +1127,13 @@ define(function (require, exports, module) { // Skip everything until the opening '{' while (token !== "{") { if (!_nextTokenSkippingComments()) { - return false; // eof + return; // eof } } // skip past '{', to next non-ws token if (!_nextTokenSkippingWhitespace()) { - return false; // eof + return; // eof } if (currentLevel <= level) { @@ -1137,30 +1149,25 @@ define(function (require, exports, module) { currentLevel--; } - } else if (/@(charset|import|namespace|include|extend|warn)/i.test(token) || - !/\{/.test(stream.string)) { - + } else { // This code handles @rules in this format: // @rule ... ; // Or any less variable that starts with @var ... ; // Skip everything until the next ';' while (token !== ";") { + // This code handle @rules that use this format: + // @rule ... { ... } + // such as @page, @keyframes (also -webkit-keyframes, etc.), and @font-face. + // Skip everything including nested braces until the next matching '}' + if (token === "{") { + _skipToClosingBracket("{"); + return; + } if (!_nextTokenSkippingComments()) { - return false; // eof + return; // eof } } - - } else { - // This code handle @rules that use this format: - // @rule ... { ... } - // such as @page, @keyframes (also -webkit-keyframes, etc.), and @font-face. - // Skip everything including nested braces until the next matching '}' - _skipToClosingBracket("{"); - if (token === "}") { - return false; - } } - return true; } // parse a style rule @@ -1174,17 +1181,16 @@ define(function (require, exports, module) { } _parseRuleList = function (escapeToken, level) { - var skipNext = true; while ((!escapeToken) || token !== escapeToken) { if (_isVariableInterpolatedProperty()) { - _skipProperty(); + if (!_skipProperty()) { + // We found a "{" or "}" while skipping a property. Return false to handle the + // opening or closing of a block properly. + return false; + } } else if (_isStartAtRule()) { // @rule - if (!_parseAtRule(level) && level > 0) { - skipNext = false; - } else { - skipNext = true; - } + _parseAtRule(level); } else if (_isStartComment()) { // comment - make this part of style rule if (includeCommentInNextRule()) { @@ -1194,15 +1200,12 @@ define(function (require, exports, module) { _parseComment(); } else if (_maybeProperty()) { // Skip the property. - _skipProperty(); - // If we find a "{" or "}" while skipping a property, then don't skip it in - // this while loop. Otherwise, we will get into an infinite loop in parsing - // since we miss to handle the opening or closing of a block properly. - if (token === "{" || token === "}") { + if (!_skipProperty()) { + // We found a "{" or "}" while skipping a property. Return false to handle the + // opening or closing of a block properly. return false; } } else { - skipNext = true; // reset skipNext // Otherwise, it's style rule if (!_parseRule(level === undefined ? 0 : level) && level > 0) { return false; @@ -1216,7 +1219,7 @@ define(function (require, exports, module) { ruleStartLine = -1; } - if (skipNext && !_nextTokenSkippingWhitespace()) { + if (!_nextTokenSkippingWhitespace()) { break; } } @@ -1251,6 +1254,57 @@ define(function (require, exports, module) { } */ + /** + * Helper function to remove whitespaces before and after a selector + * Returns trimmed selector if it is not an at-rule, or null if it starts with @. + * + * @param {string} selector + * @return {string} + */ + function _stripAtRules(selector) { + selector = selector.trim(); + if (selector.indexOf("@") === 0) { + return ""; + } + return selector; + } + + /** + * Converts the given selector array into the actual CSS selectors similar to + * those generated by a CSS preprocessor. + * + * @param {Array.} selectorArray + * @return {string} + */ + function _getSelectorInFinalCSSForm(selectorArray) { + var finalSelectorArray = [""], + parentSelectorArray = [], + group = []; + _.forEach(selectorArray, function (selector) { + selector = _stripAtRules(selector); + group = selector.split(","); + parentSelectorArray = []; + _.forEach(group, function (cs) { + var ampersandIndex = cs.indexOf("&"); + _.forEach(finalSelectorArray, function (ps) { + if (ampersandIndex === -1) { + cs = _stripAtRules(cs); + if (ps.length && cs.length) { + ps += " "; + } + ps += cs; + } else { + // Replace all instances of & with regexp + ps = _stripAtRules(cs.replace(/&/g, ps)); + } + parentSelectorArray.push(ps); + }); + }); + finalSelectorArray = parentSelectorArray; + }); + return finalSelectorArray.join(", "); + } + /** * Finds all instances of the specified selector in "text". * Returns an Array of Objects with start and end properties. @@ -1293,11 +1347,17 @@ define(function (require, exports, module) { var re = new RegExp(selector + "(\\[[^\\]]*\\]|:{1,2}[\\w-()]+|\\.[\\w-]+|#[\\w-]+)*\\s*$", classOrIdSelector ? "" : "i"); allSelectors.forEach(function (entry) { - if (entry.selector.search(re) !== -1) { + var actualSelector = entry.selector; + if (entry.selector.indexOf("&") !== -1 && entry.parentSelectors) { + var selectorArray = entry.parentSelectors.split(" / "); + selectorArray.push(entry.selector); + actualSelector = _getSelectorInFinalCSSForm(selectorArray); + } + if (actualSelector.search(re) !== -1) { result.push(entry); } else if (!classOrIdSelector) { // Special case for tag selectors - match "*" as the rightmost character - if (/\*\s*$/.test(entry.selector)) { + if (/\*\s*$/.test(actualSelector)) { result.push(entry); } } @@ -1443,41 +1503,6 @@ define(function (require, exports, module) { var isPreprocessorDoc = FileUtils.isCSSPreprocessorFile(editor.document.file.fullPath); var selectorArray = []; - function _stripAtRules(selector) { - selector = selector.trim(); - if (selector.indexOf("@") === 0) { - return ""; - } - return selector; - } - - function _getSelectorInFinalCSSForm() { - var finalSelectorArray = [""], - parentSelectorArray = [], - group = []; - _.forEach(selectorArray, function (selector) { - selector = _stripAtRules(selector); - group = selector.split(", "); - parentSelectorArray = []; - _.forEach(group, function (cs) { - var ampersandIndex = cs.indexOf("&"); - _.forEach(finalSelectorArray, function (ps) { - if (ampersandIndex === -1) { - if (ps.length) { - ps += " "; - } - ps += cs; - } else { - ps = cs.replace("&", ps); - } - parentSelectorArray.push(ps); - }); - }); - finalSelectorArray = parentSelectorArray; - }); - return finalSelectorArray.join(", "); - } - function _skipToOpeningBracket(ctx, startChar) { var unmatchedBraces = 0; if (!startChar) { @@ -1560,7 +1585,7 @@ define(function (require, exports, module) { } else if (ctx.token.string === "{") { selector = _parseSelector(ctx); if (isPreprocessorDoc) { - if (!skipPrevSibling && !/^\s*@media/i.test(selector)) { + if (!skipPrevSibling && !/^\s*@/.test(selector)) { selectorArray.unshift(selector); } if (skipPrevSibling) { @@ -1608,7 +1633,7 @@ define(function (require, exports, module) { if (ctx.token.type !== "comment") { if (ctx.token.string === "{") { selector = _parseSelector(ctx); - if (isPreprocessorDoc) { + if (isPreprocessorDoc && !/^\s*@/.test(selector)) { selectorArray.push(selector); } break; @@ -1623,7 +1648,7 @@ define(function (require, exports, module) { } if (isPreprocessorDoc) { - return _getSelectorInFinalCSSForm(); + return _getSelectorInFinalCSSForm(selectorArray); } return _stripAtRules(selector); diff --git a/test/UnitTestSuite.js b/test/UnitTestSuite.js index d6f98278a75..9ed54a1d57d 100644 --- a/test/UnitTestSuite.js +++ b/test/UnitTestSuite.js @@ -1,37 +1,38 @@ /* * Copyright (c) 2012 Adobe Systems Incorporated. All rights reserved. - * + * * Permission is hereby granted, free of charge, to any person obtaining a - * copy of this software and associated documentation files (the "Software"), - * to deal in the Software without restriction, including without limitation - * the rights to use, copy, modify, merge, publish, distribute, sublicense, - * and/or sell copies of the Software, and to permit persons to whom the + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. - * + * */ /*jslint vars: true, plusplus: true, devel: true, browser: true, nomen: true, indent: 4, maxerr: 50 */ /*global define */ define(function (require, exports, module) { "use strict"; - + require("spec/Async-test"); require("spec/CodeHint-test"); require("spec/CodeHintUtils-test"); require("spec/CodeInspection-test"); require("spec/CommandManager-test"); require("spec/CSSUtils-test"); + require("spec/CSSInlineEdit-test"); require("spec/JSUtils-test"); require("spec/Document-test"); require("spec/DocumentCommandHandlers-test"); @@ -84,4 +85,4 @@ define(function (require, exports, module) { require("spec/ViewUtils-test"); require("spec/WorkingSetView-test"); require("spec/WorkingSetSort-test"); -}); \ No newline at end of file +}); diff --git a/test/spec/CSSInlineEdit-test-files/css/test.css b/test/spec/CSSInlineEdit-test-files/css/test.css new file mode 100644 index 00000000000..16b36707233 --- /dev/null +++ b/test/spec/CSSInlineEdit-test-files/css/test.css @@ -0,0 +1,10 @@ +.standard { + background-color: red; +} + +.button { + padding: 30px; +} +.banner-new { + background-color: blue; +} diff --git a/test/spec/CSSInlineEdit-test-files/css/test2.css b/test/spec/CSSInlineEdit-test-files/css/test2.css new file mode 100644 index 00000000000..16b36707233 --- /dev/null +++ b/test/spec/CSSInlineEdit-test-files/css/test2.css @@ -0,0 +1,10 @@ +.standard { + background-color: red; +} + +.button { + padding: 30px; +} +.banner-new { + background-color: blue; +} diff --git a/test/spec/CSSInlineEdit-test-files/index-css.html b/test/spec/CSSInlineEdit-test-files/index-css.html new file mode 100644 index 00000000000..c1ebd6a27f7 --- /dev/null +++ b/test/spec/CSSInlineEdit-test-files/index-css.html @@ -0,0 +1,51 @@ + + + + + + + + + + testsite + + + + + + + + + + + +

+ Test +

+
+ scss rulez! +
+ + +
+ Issue https://github.com/adobe/brackets/issues/8875 +
+ + +
+ OK +
+ + Test Link + + +
Comment #1
+
Comment #2
+
Comment #3
+
Comment #4
+ + diff --git a/test/spec/CSSInlineEdit-test-files/index-less.html b/test/spec/CSSInlineEdit-test-files/index-less.html new file mode 100644 index 00000000000..6ba937a6690 --- /dev/null +++ b/test/spec/CSSInlineEdit-test-files/index-less.html @@ -0,0 +1,79 @@ + + + + + + + + + + testsite + + + + + + + + + +
+
+
+ This a level 3 text +
+
+
+ +
Comment #1 (single line)
+
Comment #2 (multi line)
+
Comment #3 (End of line)
+
Comment #4 (single line block comment)
+
Comment #5 (multi line block comment)
+ +
+ Confuse #1 +
+ Special +
+
+ +
+ Compressed +
+ +
+
ONE
+
TWO
+
+ +
+
TRES
+
QUATTRO
+
+ + +
+ This is a banner +
+ +
+
Mixin A
+
+ +
+ Widget +
+ +
Mixin Test Parameterized Default
+
+
+ Mixin Test Parameterized Custom +
+
+ + diff --git a/test/spec/CSSInlineEdit-test-files/less/test.less b/test/spec/CSSInlineEdit-test-files/less/test.less new file mode 100644 index 00000000000..97629c80b77 --- /dev/null +++ b/test/spec/CSSInlineEdit-test-files/less/test.less @@ -0,0 +1,12 @@ +body { + background-color: red; +} + +.banner { + background-color: red; +} +div { + &.banner-new2 { + background-color: blue; + } +} diff --git a/test/spec/CSSInlineEdit-test-files/less/test2.less b/test/spec/CSSInlineEdit-test-files/less/test2.less new file mode 100644 index 00000000000..cfa125fffa0 --- /dev/null +++ b/test/spec/CSSInlineEdit-test-files/less/test2.less @@ -0,0 +1,130 @@ +.banner-new2 { + background-color: blue; +} + +.level1 { + border: 10px; + .level2 { + border: 20px; + padding: 10px; + border-style: groove; + .level3 { + color: red; + } + } +} + +/* This is a single line block comment that should appear in the inline editor as first line */ +.comment1 { + text-align: center; +} + +/* This is a multiline block comment + * that should appear in the inline editor + */ +.comment2 { + text-decoration: overline; +} + +.comment3 { + text-decoration: underline; /* EOL comment {}(,;:) */ +} + +// This is a single line comment that should appear in the inline editor as first line +.comment4 { + text-align: center; +} + +// This is a multi line comment +// that should appear in the inline editor +.comment5 { + text-decoration: overline; +} + +.confuse1 { + /* {this comment is special(;:,)} */ + .special { + color: #ff8000; + } +} + +.compressed{color:red} + +// selector groups +#main { + .uno, .dos { + color: red + } + + .dos { + font-size: 20px; + } +} + +#main2 { + .tres, + .quattro { + color: blue; + } +} + +// Variables +@mySelector: testbanner; + +// Usage +.@{mySelector} { + font-weight: bold; + line-height: 40px; + margin: 0 auto; +} + +// Mixins +.a, #b { + color: red; +} + +.mixina-class { + .a(); +} + +.mixinb-id { + #b(); +} + +.desktop-and-old-ie(@rules) { + @media screen and (min-width: 1200) { @rules; } + html.lt-ie9 & { @rules; } +} + +header { + background-color: blue; + + .desktop-and-old-ie({ + background-color: red; + }); +} + +@property: color; + +.widget { + @{property}: #0ee; + background-@{property}: #999; +} + +.border-radius(@radius: 5px) { + -webkit-border-radius: @radius; + -moz-border-radius: @radius; + border-radius: @radius; +} + +#header-mixin-paramterized-default { + .border-radius; +} + +#header-mixin-paramterized-custom { + .border-radius(10px); + #header-mixin-paramterized-custom-1 { + .border-radius(20px); + } + +} diff --git a/test/spec/CSSInlineEdit-test-files/scss/test.scss b/test/spec/CSSInlineEdit-test-files/scss/test.scss new file mode 100644 index 00000000000..d511f5538d3 --- /dev/null +++ b/test/spec/CSSInlineEdit-test-files/scss/test.scss @@ -0,0 +1,62 @@ +$font-size: 10; +$height: 20; +$color: red; + +p { + $font-size: 12px; + $line-height: 30px; + font: #{$font-size}/#{$line-height}; +} + +#scss-1 { + background-color: blue; +} + +#scss-2 { + background-color: red; +} + +.button { + &-ok { + background-image: url("ok.png"); + } + &-cancel { + background-image: url("cancel.png"); + } + &-custom { + background-image: url("custom.png"); + } +} + +$name: foo; +$attr: border; +div.#{$name} { + #{$attr}-color: blue; +} + +a { + color: blue; +} + +// This is a single line comment +.comment-scss-1 { + background-color: red; +} + +// This is a single line comment +// spread over two lines +.comment-scss-2 { + background-color: green; +} + +/* This is a single line block comment */ +.comment-scss-3 { + background-color: blue; +} + +/* This is a multi line block comment + * on two lines + */ +.comment-scss-4 { + background-color: black; +} diff --git a/test/spec/CSSInlineEdit-test.js b/test/spec/CSSInlineEdit-test.js new file mode 100644 index 00000000000..8375be71c13 --- /dev/null +++ b/test/spec/CSSInlineEdit-test.js @@ -0,0 +1,807 @@ +/* + * Copyright (c) 2014 Adobe Systems Incorporated. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + */ + +/*jslint vars: true, plusplus: true, devel: true, browser: true, nomen: true, indent: 4, maxerr: 50 */ +/*global define, describe, it, expect, beforeEach, afterEach, waitsFor, waitsForDone, runs, $, beforeFirst, afterLast, waits, spyOn */ + +define(function (require, exports, module) { + "use strict"; + + var SpecRunnerUtils = require("spec/SpecRunnerUtils"), + Strings = require("strings"); + + var testPath = SpecRunnerUtils.getTestPath("/spec/CSSInlineEdit-test-files"); + + describe("CSS Inline Edit", function () { + this.category = "integration"; + + var testWindow, + brackets, + $, + EditorManager, + CommandManager, + DocumentManager, + Commands, + FileSystem, + Dialogs; + + beforeFirst(function () { + runs(function () { + SpecRunnerUtils.createTestWindowAndRun(this, function (w) { + testWindow = w; + // Load module instances from brackets.test + $ = testWindow.$; + brackets = testWindow.brackets; + EditorManager = brackets.test.EditorManager; + CommandManager = brackets.test.CommandManager; + FileSystem = brackets.test.FileSystem; + Dialogs = brackets.test.Dialogs; + DocumentManager = brackets.test.DocumentManager; + Commands = brackets.test.Commands; + }); + }); + + runs(function () { + SpecRunnerUtils.loadProjectInTestWindow(testPath); + }); + }); + + afterLast(function () { + EditorManager = null; + CommandManager = null; + DocumentManager = null; + Commands = null; + Dialogs = null; + FileSystem = null; + testWindow = null; + SpecRunnerUtils.closeTestWindow(); + }); + + function dropdownButton() { + return $(".btn.btn-dropdown.btn-mini.stylesheet-button"); + } + + function dropdownMenu() { + return $(".dropdown-menu.dropdownbutton-popup"); + } + + function getInlineEditorWidgets() { + return EditorManager.getCurrentFullEditor().getInlineWidgets(); + } + + function inlineEditorMessage(index) { + if (index === undefined) { + index = 0; + } + + return getInlineEditorWidgets()[index].$messageDiv; + } + + function inlineEditorFileName(inlineWidget) { + return inlineWidget.$header.find("a.filename"); + } + + function getRelatedFiles(inlineWidget) { + return inlineWidget.$relatedContainer.find(".related ul>li"); + } + + function loadFile(file) { + runs(function () { + var promise = SpecRunnerUtils.openProjectFiles([file]); + waitsForDone(promise); + }); + } + + function closeFilesInTestWindow() { + runs(function () { + var promise = CommandManager.execute(Commands.FILE_CLOSE_ALL); + waitsForDone(promise, "Close all open files in working set"); + }); + } + + function checkAvailableStylesheets(availableFilesInDropdown) { + expect(availableFilesInDropdown.length).toBe(5); + expect(availableFilesInDropdown[0].textContent).toEqual("test.css"); + expect(availableFilesInDropdown[1].textContent).toEqual("test.less"); + expect(availableFilesInDropdown[2].textContent).toEqual("test.scss"); + expect(availableFilesInDropdown[3].textContent).toEqual("test2.css"); + expect(availableFilesInDropdown[4].textContent).toEqual("test2.less"); + } + + function getInlineEditorContent(ranges) { + var document = ranges.textRange.document; + + return document.getRange({line: ranges.textRange.startLine, ch: 0}, {line: ranges.textRange.endLine, ch: document.getLine(ranges.textRange.endLine).length}); + } + + describe("CSS", function () { + beforeEach(function () { + loadFile("index-css.html"); + }); + + afterEach(function () { + closeFilesInTestWindow(); + }); + + it("should open two inline editors and show the matching rules", function () { + var inlineWidgets; + + runs(function () { + var promise = SpecRunnerUtils.toggleQuickEditAtOffset(EditorManager.getCurrentFullEditor(), {line: 15, ch: 22}); + waitsForDone(promise, "Open inline editor 1"); + + var promise2 = SpecRunnerUtils.toggleQuickEditAtOffset(EditorManager.getCurrentFullEditor(), {line: 20, ch: 25}); + waitsForDone(promise2, "Open inline editor 2"); + }); + + runs(function () { + inlineWidgets = getInlineEditorWidgets(); + + // Check Inline Editor 1 + expect(inlineEditorFileName(inlineWidgets[0])[0].text).toEqual("test.css : 1"); + + var ranges = inlineWidgets[0]._ranges[0]; + var document = ranges.textRange.document; + + expect(document.getRange({line: ranges.textRange.startLine, ch: 0}, {line: ranges.textRange.endLine, ch: document.getLine(ranges.textRange.endLine).length})).toEqual(".standard {\n background-color: red;\n}"); + + var files = getRelatedFiles(inlineWidgets[0]); + expect(files.length).toBe(2); + expect(files[0].textContent).toEqual(".standard — test.css : 1"); + expect(files[1].textContent).toEqual(".standard — test2.css : 1"); + + // Check Inline Editor 2 + expect(inlineEditorFileName(inlineWidgets[1])[0].text).toEqual("test.css : 8"); + + ranges = inlineWidgets[1]._ranges[0]; + document = ranges.textRange.document; + + expect(document.getRange({line: ranges.textRange.startLine, ch: 0}, {line: ranges.textRange.endLine, ch: document.getLine(ranges.textRange.endLine).length})).toEqual(".banner-new {\n background-color: blue;\n}"); + + files = getRelatedFiles(inlineWidgets[1]); + expect(files.length).toBe(2); + expect(files[0].textContent).toEqual(".banner-new — test.css : 8"); + expect(files[1].textContent).toEqual(".banner-new — test2.css : 8"); + }); + }); + + it("should show no matching rule in inline editor", function () { + runs(function () { + var promise = SpecRunnerUtils.toggleQuickEditAtOffset(EditorManager.getCurrentFullEditor(), {line: 16, ch: 7}); + waitsForDone(promise, "Open inline editor"); + }); + + runs(function () { + // open the dropdown + dropdownButton().click(); + + var availableFilesInDropdown = dropdownMenu().children(); + checkAvailableStylesheets(availableFilesInDropdown); + + expect(inlineEditorMessage().html()).toEqual(Strings.CSS_QUICK_EDIT_NO_MATCHES); + }); + }); + + it("should show one matching rule in inline editor", function () { + runs(function () { + var promise = SpecRunnerUtils.toggleQuickEditAtOffset(EditorManager.getCurrentFullEditor(), {line: 17, ch: 15}); + waitsForDone(promise, "Open inline editor"); + }); + + runs(function () { + // open the dropdown + dropdownButton().click(); + + var inlineWidgets = getInlineEditorWidgets(); + + var availableFilesInDropdown = dropdownMenu().children(); + checkAvailableStylesheets(availableFilesInDropdown); + expect(inlineEditorFileName(inlineWidgets[0])[0].text).toEqual("test.less : 5"); + + var ranges = inlineWidgets[0]._ranges[0]; + var document = ranges.textRange.document; + + expect(document.getRange({line: ranges.textRange.startLine, ch: 0}, {line: ranges.textRange.endLine, ch: document.getLine(ranges.textRange.endLine).length})).toEqual(".banner {\n background-color: red;\n}"); + }); + }); + + it("should show one matching rule in two files", function () { + runs(function () { + var promise = SpecRunnerUtils.toggleQuickEditAtOffset(EditorManager.getCurrentFullEditor(), {line: 20, ch: 25}); + waitsForDone(promise, "Open inline editor"); + }); + + runs(function () { + var inlineWidgets = getInlineEditorWidgets(); + + // open the dropdown + dropdownButton().click(); + + var availableFilesInDropdown = dropdownMenu().children(); + expect(availableFilesInDropdown.length).toBe(5); + checkAvailableStylesheets(availableFilesInDropdown); + expect(inlineEditorFileName(inlineWidgets[0])[0].text).toEqual("test.css : 8"); + + var ranges = inlineWidgets[0]._ranges[0]; + var document = ranges.textRange.document; + + expect(document.getRange({line: ranges.textRange.startLine, ch: 0}, {line: ranges.textRange.endLine, ch: document.getLine(ranges.textRange.endLine).length})).toEqual(".banner-new {\n background-color: blue;\n}"); + + var files = getRelatedFiles(inlineWidgets[0]); + expect(files.length).toBe(2); + expect(files[0].textContent).toEqual(".banner-new — test.css : 8"); + expect(files[1].textContent).toEqual(".banner-new — test2.css : 8"); + }); + }); + + it("should add the css file to the working set when modified in inline editor", function () { + runs(function () { + var promise = SpecRunnerUtils.toggleQuickEditAtOffset(EditorManager.getCurrentFullEditor(), {line: 48, ch: 25}); + waitsForDone(promise, "Open inline editor"); + + spyOn(Dialogs, 'showModalDialog').andCallFake(function (dlgClass, title, message, buttons) { + return {done: function (callback) { callback(Dialogs.DIALOG_BTN_DONTSAVE); } }; + }); + }); + + runs(function () { + var inlineWidgets = getInlineEditorWidgets(); + + expect(inlineEditorFileName(inlineWidgets[0])[0].text).toEqual("test.scss : 57"); + + var document = inlineWidgets[0].editor.document; + // modify scss to add it to the working set + var scssFile = FileSystem.getFileForPath(testPath + "/scss/test.scss"); + document.setText(".comment-scss-4 {\n background-color: black;\n}"); + + expect(DocumentManager.findInWorkingSet(scssFile.fullPath)).toBeGreaterThan(0); + }); + }); + }); + + describe("LESS", function () { + beforeEach(function () { + loadFile("index-less.html"); + }); + + afterEach(function () { + closeFilesInTestWindow(); + }); + + it("should show no matching rule in inline editor", function () { + runs(function () { + var promise = SpecRunnerUtils.toggleQuickEditAtOffset(EditorManager.getCurrentFullEditor(), {line: 16, ch: 10}); + waitsForDone(promise, "Open inline editor"); + }); + + runs(function () { + // open the dropdown + dropdownButton().click(); + + var availableFilesInDropdown = dropdownMenu().children(); + checkAvailableStylesheets(availableFilesInDropdown); + + expect(inlineEditorMessage().html()).toEqual(Strings.CSS_QUICK_EDIT_NO_MATCHES); + }); + }); + + it("should show one matching rule in inline editor", function () { + runs(function () { + var promise = SpecRunnerUtils.toggleQuickEditAtOffset(EditorManager.getCurrentFullEditor(), {line: 17, ch: 15}); + waitsForDone(promise, "Open inline editor"); + }); + + runs(function () { + // open the dropdown + dropdownButton().click(); + + var inlineWidgets = getInlineEditorWidgets(); + + var availableFilesInDropdown = dropdownMenu().children(); + checkAvailableStylesheets(availableFilesInDropdown); + expect(inlineEditorFileName(inlineWidgets[0])[0].text).toEqual("test.less : 5"); + + var ranges = inlineWidgets[0]._ranges[0]; + + expect(getInlineEditorContent(ranges)).toEqual(".banner {\n background-color: red;\n}"); + }); + }); + + it("should show one matching rule in two files", function () { + runs(function () { + var promise = SpecRunnerUtils.toggleQuickEditAtOffset(EditorManager.getCurrentFullEditor(), {line: 20, ch: 25}); + waitsForDone(promise, "Open inline editor"); + }); + + runs(function () { + // open the dropdown + dropdownButton().click(); + + var inlineWidgets = getInlineEditorWidgets(); + + var availableFilesInDropdown = dropdownMenu().children(); + checkAvailableStylesheets(availableFilesInDropdown); + expect(inlineEditorFileName(inlineWidgets[0])[0].text).toEqual("test.less : 9"); + + var ranges = inlineWidgets[0]._ranges[0]; + + expect(getInlineEditorContent(ranges)).toEqual(" &.banner-new2 {\n background-color: blue;\n }"); + + var files = inlineWidgets[0].$relatedContainer.find(".related ul>li"); + expect(files.length).toBe(2); + expect(files[0].textContent).toEqual("div / &.banner-new2 — test.less : 9"); + expect(files[1].textContent).toEqual(".banner-new2 — test2.less : 1"); + }); + }); + + describe("Check Rule appearance with matches in one LESS file", function () { + it("should return the nested rule", function () { + runs(function () { + var promise = SpecRunnerUtils.toggleQuickEditAtOffset(EditorManager.getCurrentFullEditor(), {line: 25, ch: 25}); + waitsForDone(promise, "Open inline editor"); + }); + + runs(function () { + var inlineWidgets = getInlineEditorWidgets(); + expect(inlineEditorFileName(inlineWidgets[0])[0].text).toEqual("test2.less : 11"); + + var ranges = inlineWidgets[0]._ranges[0]; + + expect(getInlineEditorContent(ranges)).toEqual(" .level3 {\n color: red;\n }"); + + // It's not visible + var files = inlineWidgets[0].$relatedContainer.find(".related ul>li"); + expect(files.length).toBe(1); + expect(files[0].textContent).toEqual(".level1 / .level2 / .level3 — test2.less : 11"); + }); + }); + + it("should show matching rule with comment above containing delimiter chars", function () { + runs(function () { + var promise = SpecRunnerUtils.toggleQuickEditAtOffset(EditorManager.getCurrentFullEditor(), {line: 37, ch: 25}); + waitsForDone(promise, "Open inline editor"); + }); + + runs(function () { + var inlineWidgets = getInlineEditorWidgets(); + expect(inlineEditorFileName(inlineWidgets[0])[0].text).toEqual("test2.less : 44"); + + var ranges = inlineWidgets[0]._ranges[0]; + + expect(getInlineEditorContent(ranges)).toEqual(".confuse1 {\n /* {this comment is special(;:,)} */\n .special {\n color: #ff8000;\n }\n}"); + + // It's not visible + var files = inlineWidgets[0].$relatedContainer.find(".related ul>li"); + expect(files.length).toBe(1); + expect(files[0].textContent).toEqual(".confuse1 — test2.less : 44"); + }); + }); + + it("should show matching rule without delimiting whitespace (compressed)", function () { + runs(function () { + var promise = SpecRunnerUtils.toggleQuickEditAtOffset(EditorManager.getCurrentFullEditor(), {line: 44, ch: 25}); + waitsForDone(promise, "Open inline editor"); + }); + + runs(function () { + var inlineWidgets = getInlineEditorWidgets(); + expect(inlineEditorFileName(inlineWidgets[0])[0].text).toEqual("test2.less : 51"); + + var ranges = inlineWidgets[0]._ranges[0]; + + expect(getInlineEditorContent(ranges)).toEqual(".compressed{color:red}"); + + // It's not visible + var files = inlineWidgets[0].$relatedContainer.find(".related ul>li"); + expect(files.length).toBe(1); + expect(files[0].textContent).toEqual(".compressed — test2.less : 51"); + }); + }); + }); + + describe("Comments", function () { + it("should show matching sub rule with comment above containing delimiter chars", function () { + runs(function () { + var promise = SpecRunnerUtils.toggleQuickEditAtOffset(EditorManager.getCurrentFullEditor(), {line: 39, ch: 25}); + waitsForDone(promise, "Open inline editor"); + }); + + runs(function () { + var inlineWidgets = getInlineEditorWidgets(); + expect(inlineEditorFileName(inlineWidgets[0])[0].text).toEqual("test2.less : 45"); + + var ranges = inlineWidgets[0]._ranges[0]; + + expect(getInlineEditorContent(ranges)).toEqual(" /* {this comment is special(;:,)} */\n .special {\n color: #ff8000;\n }"); + + // It's not visible + var files = getRelatedFiles(inlineWidgets[0]); + expect(files.length).toBe(1); + expect(files[0].textContent).toEqual(".confuse1 / .special — test2.less : 45"); + }); + }); + + it("should show the single line block style comment above the matching rule", function () { + runs(function () { + var promise = SpecRunnerUtils.toggleQuickEditAtOffset(EditorManager.getCurrentFullEditor(), {line: 31, ch: 25}); + waitsForDone(promise, "Open inline editor"); + }); + + runs(function () { + var inlineWidgets = getInlineEditorWidgets(); + expect(inlineEditorFileName(inlineWidgets[0])[0].text).toEqual("test2.less : 17"); + + var ranges = inlineWidgets[0]._ranges[0]; + + expect(getInlineEditorContent(ranges)).toEqual("/* This is a single line block comment that should appear in the inline editor as first line */\n.comment1 {\n text-align: center;\n}"); + + // It's not visible + var files = inlineWidgets[0].$relatedContainer.find(".related ul>li"); + expect(files.length).toBe(1); + expect(files[0].textContent).toEqual(".comment1 — test2.less : 17"); + }); + }); + + it("should show the single line inline comment above the matching rule", function () { + runs(function () { + var promise = SpecRunnerUtils.toggleQuickEditAtOffset(EditorManager.getCurrentFullEditor(), {line: 34, ch: 25}); + waitsForDone(promise, "Open inline editor"); + }); + + runs(function () { + var inlineWidgets = getInlineEditorWidgets(); + expect(inlineEditorFileName(inlineWidgets[0])[0].text).toEqual("test2.less : 33"); + + var ranges = inlineWidgets[0]._ranges[0]; + + expect(getInlineEditorContent(ranges)).toEqual("// This is a single line comment that should appear in the inline editor as first line\n.comment4 {\n text-align: center;\n}"); + + // It's not visible + var files = inlineWidgets[0].$relatedContainer.find(".related ul>li"); + expect(files.length).toBe(1); + expect(files[0].textContent).toEqual(".comment4 — test2.less : 33"); + }); + }); + + it("should show the multi line block comment above the matching rule", function () { + runs(function () { + var promise = SpecRunnerUtils.toggleQuickEditAtOffset(EditorManager.getCurrentFullEditor(), {line: 32, ch: 25}); + waitsForDone(promise, "Open inline editor"); + }); + + runs(function () { + var inlineWidgets = getInlineEditorWidgets(); + expect(inlineEditorFileName(inlineWidgets[0])[0].text).toEqual("test2.less : 22"); + + var ranges = inlineWidgets[0]._ranges[0]; + + expect(getInlineEditorContent(ranges)).toEqual("/* This is a multiline block comment\n * that should appear in the inline editor\n */\n.comment2 {\n text-decoration: overline;\n}"); + + // It's not visible + var files = inlineWidgets[0].$relatedContainer.find(".related ul>li"); + expect(files.length).toBe(1); + expect(files[0].textContent).toEqual(".comment2 — test2.less : 22"); + }); + }); + + it("should show the end of line comment after the matching rule", function () { + runs(function () { + var promise = SpecRunnerUtils.toggleQuickEditAtOffset(EditorManager.getCurrentFullEditor(), {line: 33, ch: 25}); + waitsForDone(promise, "Open inline editor"); + }); + + runs(function () { + var inlineWidgets = getInlineEditorWidgets(); + expect(inlineEditorFileName(inlineWidgets[0])[0].text).toEqual("test2.less : 29"); + + var ranges = inlineWidgets[0]._ranges[0]; + + expect(getInlineEditorContent(ranges)).toEqual(".comment3 {\n text-decoration: underline; /* EOL comment {}(,;:) */\n}"); + + // It's not visible + var files = inlineWidgets[0].$relatedContainer.find(".related ul>li"); + expect(files.length).toBe(1); + expect(files[0].textContent).toEqual(".comment3 — test2.less : 29"); + }); + }); + }); + + describe("Selector groups", function () { + it("should show matching selector group", function () { + runs(function () { + var promise = SpecRunnerUtils.toggleQuickEditAtOffset(EditorManager.getCurrentFullEditor(), {line: 49, ch: 25}); + waitsForDone(promise, "Open inline editor"); + }); + + runs(function () { + var inlineWidgets = getInlineEditorWidgets(); + expect(inlineEditorFileName(inlineWidgets[0])[0].text).toEqual("test2.less : 55"); + + var ranges = inlineWidgets[0]._ranges[0]; + + expect(getInlineEditorContent(ranges)).toEqual(" .uno, .dos {\n color: red\n }"); + + // It's not visible + var files = inlineWidgets[0].$relatedContainer.find(".related ul>li"); + expect(files.length).toBe(1); + expect(files[0].textContent).toEqual(".uno, .dos — test2.less : 55"); + }); + }); + + it("should show 1 matching rule of selector group", function () { + runs(function () { + var promise = SpecRunnerUtils.toggleQuickEditAtOffset(EditorManager.getCurrentFullEditor(), {line: 50, ch: 25}); + waitsForDone(promise, "Open inline editor"); + }); + + runs(function () { + var inlineWidgets = getInlineEditorWidgets(); + expect(inlineEditorFileName(inlineWidgets[0])[0].text).toEqual("test2.less : 55"); + + var ranges = inlineWidgets[0]._ranges[0]; + + expect(getInlineEditorContent(ranges)).toEqual(" .uno, .dos {\n color: red\n }"); + + // It's not visible + var files = inlineWidgets[0].$relatedContainer.find(".related ul>li"); + expect(files.length).toBe(2); + expect(files[0].textContent).toEqual(".uno, .dos — test2.less : 55"); + expect(files[1].textContent).toEqual("#main / .dos — test2.less : 59"); + }); + }); + + it("should show matching selector group with selectors on separate lines", function () { + runs(function () { + var promise = SpecRunnerUtils.toggleQuickEditAtOffset(EditorManager.getCurrentFullEditor(), {line: 54, ch: 25}); + waitsForDone(promise, "Open inline editor"); + }); + + runs(function () { + var inlineWidgets = getInlineEditorWidgets(); + expect(inlineEditorFileName(inlineWidgets[0])[0].text).toEqual("test2.less : 65"); + + var ranges = inlineWidgets[0]._ranges[0]; + + expect(getInlineEditorContent(ranges)).toEqual(" .tres,\n .quattro {\n color: blue;\n }"); + + // It's not visible + var files = inlineWidgets[0].$relatedContainer.find(".related ul>li"); + expect(files.length).toBe(1); + expect(files[0].textContent).toEqual(".tres, .quattro — test2.less : 65"); + }); + }); + }); + + describe("Mixins", function () { + it("should show matching mixin rule", function () { + runs(function () { + var promise = SpecRunnerUtils.toggleQuickEditAtOffset(EditorManager.getCurrentFullEditor(), {line: 64, ch: 30}); + waitsForDone(promise, "Open inline editor"); + }); + + runs(function () { + var inlineWidgets = getInlineEditorWidgets(); + expect(inlineEditorFileName(inlineWidgets[0])[0].text).toEqual("test2.less : 86"); + + var ranges = inlineWidgets[0]._ranges[0]; + + expect(getInlineEditorContent(ranges)).toEqual(".mixina-class {\n .a();\n}"); + + // It's not visible + var files = inlineWidgets[0].$relatedContainer.find(".related ul>li"); + expect(files.length).toBe(1); + expect(files[0].textContent).toEqual(".mixina-class — test2.less : 86"); + }); + }); + + it("should show matching rule which includes mixin with default parameter", function () { + runs(function () { + var promise = SpecRunnerUtils.toggleQuickEditAtOffset(EditorManager.getCurrentFullEditor(), {line: 71, ch: 20}); + waitsForDone(promise, "Open inline editor"); + }); + + runs(function () { + var inlineWidgets = getInlineEditorWidgets(); + expect(inlineEditorFileName(inlineWidgets[0])[0].text).toEqual("test2.less : 120"); + + var ranges = inlineWidgets[0]._ranges[0]; + + expect(getInlineEditorContent(ranges)).toEqual("#header-mixin-paramterized-default {\n .border-radius;\n}"); + + // It's not visible + var files = inlineWidgets[0].$relatedContainer.find(".related ul>li"); + expect(files.length).toBe(1); + expect(files[0].textContent).toEqual("#header-mixin-paramterized-default — test2.less : 120"); + }); + }); + + it("should show matching rule which includes a parameterized mixin", function () { + runs(function () { + var promise = SpecRunnerUtils.toggleQuickEditAtOffset(EditorManager.getCurrentFullEditor(), {line: 73, ch: 25}); + waitsForDone(promise, "Open inline editor"); + }); + + runs(function () { + var inlineWidgets = getInlineEditorWidgets(); + expect(inlineEditorFileName(inlineWidgets[0])[0].text).toEqual("test2.less : 126"); + + var ranges = inlineWidgets[0]._ranges[0]; + + expect(getInlineEditorContent(ranges)).toEqual(" #header-mixin-paramterized-custom-1 {\n .border-radius(20px);\n }"); + + // It's not visible + var files = inlineWidgets[0].$relatedContainer.find(".related ul>li"); + expect(files.length).toBe(1); + expect(files[0].textContent).toEqual("#header-mixin-paramterized-custom / #header-mixin-paramterized-custom-1 — test2.less : 126"); + }); + }); + }); + + describe("Interpolation", function () { + // Verify https://github.com/adobe/brackets/issues/8865 + it("should show a rule with variable interpolation in one of its property names", function () { + runs(function () { + var promise = SpecRunnerUtils.toggleQuickEditAtOffset(EditorManager.getCurrentFullEditor(), {line: 67, ch: 25}); + waitsForDone(promise, "Open inline editor"); + }); + + runs(function () { + var inlineWidgets = getInlineEditorWidgets(); + expect(inlineEditorFileName(inlineWidgets[0])[0].text).toEqual("test2.less : 109"); + + var ranges = inlineWidgets[0]._ranges[0]; + + expect(getInlineEditorContent(ranges)).toEqual(".widget {\n @{property}: #0ee;\n background-@{property}: #999;\n}"); + + // It's not visible + var files = inlineWidgets[0].$relatedContainer.find(".related ul>li"); + expect(files.length).toBe(1); + expect(files[0].textContent).toEqual(".widget — test2.less : 109"); + }); + }); + }); + + }); + + describe("SCSS", function () { + beforeEach(function () { + loadFile("index-css.html"); + }); + + afterEach(function () { + closeFilesInTestWindow(); + }); + + // The following tests will succeed, but once the issue is fixed they will fail + it("should show one matching rule in inline editor", function () { + runs(function () { + var promise = SpecRunnerUtils.toggleQuickEditAtOffset(EditorManager.getCurrentFullEditor(), {line: 25, ch: 10}); + waitsForDone(promise, "Open inline editor"); + }); + + runs(function () { + var inlineWidgets = getInlineEditorWidgets(); + expect(inlineEditorFileName(inlineWidgets[0])[0].text).toEqual("test.scss : 5"); + + var ranges = inlineWidgets[0]._ranges[0]; + + expect(getInlineEditorContent(ranges)).toEqual("p {\n $font-size: 12px;\n $line-height: 30px;\n font: #{$font-size}/#{$line-height};\n}"); + + // It's not visible + var files = inlineWidgets[0].$relatedContainer.find(".related ul>li"); + expect(files.length).toBe(1); + expect(files[0].textContent).toEqual("p — test.scss : 5"); + }); + }); + + // https://github.com/adobe/brackets/issues/8875 + it("should show one matching rule in inline editor which is defined after rule that uses variable interpolation as property value", function () { + runs(function () { + var promise = SpecRunnerUtils.toggleQuickEditAtOffset(EditorManager.getCurrentFullEditor(), {line: 28, ch: 20}); + waitsForDone(promise, "Open inline editor"); + }); + + runs(function () { + var inlineWidgets = getInlineEditorWidgets(); + expect(inlineEditorFileName(inlineWidgets[0])[0].text).toEqual("test.scss : 11"); + + var ranges = inlineWidgets[0]._ranges[0]; + + expect(getInlineEditorContent(ranges)).toEqual("#scss-1 {\n background-color: blue;\n}"); + + // It's not visible + var files = inlineWidgets[0].$relatedContainer.find(".related ul>li"); + expect(files.length).toBe(1); + expect(files[0].textContent).toEqual("#scss-1 — test.scss : 11"); + }); + }); + + // https://github.com/adobe/brackets/issues/8895 + it("should show one matching rule that is defined with parent selector", function () { + runs(function () { + var promise = SpecRunnerUtils.toggleQuickEditAtOffset(EditorManager.getCurrentFullEditor(), {line: 38, ch: 25}); + waitsForDone(promise, "Open inline editor"); + }); + + runs(function () { + var inlineWidgets = getInlineEditorWidgets(); + expect(inlineEditorFileName(inlineWidgets[0])[0].text).toEqual("test.scss : 20"); + + var ranges = inlineWidgets[0]._ranges[0]; + + expect(getInlineEditorContent(ranges)).toEqual(" &-ok {\n background-image: url(\"ok.png\");\n }"); + + // It's not visible + var files = inlineWidgets[0].$relatedContainer.find(".related ul>li"); + expect(files.length).toBe(1); + expect(files[0].textContent).toEqual(".button / &-ok — test.scss : 20"); + }); + }); + + // https://github.com/adobe/brackets/issues/8851 + it("should show matching rules that follow rules using variable interpolation in a selector", function () { + runs(function () { + var promise = SpecRunnerUtils.toggleQuickEditAtOffset(EditorManager.getCurrentFullEditor(), {line: 42, ch: 10}); + waitsForDone(promise, "Open inline editor"); + }); + + runs(function () { + var inlineWidgets = getInlineEditorWidgets(); + expect(inlineEditorFileName(inlineWidgets[0])[0].text).toEqual("test.scss : 37"); + + var ranges = inlineWidgets[0]._ranges[0]; + + expect(getInlineEditorContent(ranges)).toEqual("a {\n color: blue;\n}"); + + // It's not visible + var files = inlineWidgets[0].$relatedContainer.find(".related ul>li"); + expect(files.length).toBe(1); + expect(files[0].textContent).toEqual("a — test.scss : 37"); + }); + }); + + describe("Comments", function () { + it("should show single line comment above matching rule", function () { + runs(function () { + var promise = SpecRunnerUtils.toggleQuickEditAtOffset(EditorManager.getCurrentFullEditor(), {line: 45, ch: 25}); + waitsForDone(promise, "Open inline editor"); + }); + + runs(function () { + var inlineWidgets = getInlineEditorWidgets(); + expect(inlineEditorFileName(inlineWidgets[0])[0].text).toEqual("test.scss : 41"); + + var ranges = inlineWidgets[0]._ranges[0]; + + expect(getInlineEditorContent(ranges)).toEqual("// This is a single line comment\n.comment-scss-1 {\n background-color: red;\n}"); + + // It's not visible + var files = getRelatedFiles(inlineWidgets[0]); + expect(files.length).toBe(1); + expect(files[0].textContent).toEqual(".comment-scss-1 — test.scss : 41"); + }); + }); + }); + }); + }); +}); diff --git a/test/spec/CSSUtils-test-files/include-mixin.scss b/test/spec/CSSUtils-test-files/include-mixin.scss new file mode 100644 index 00000000000..a36c235fb11 --- /dev/null +++ b/test/spec/CSSUtils-test-files/include-mixin.scss @@ -0,0 +1,34 @@ +.sidebar { + //background: white; + position: fixed; + width: 220px; + height: 100%; + padding: 20px 40px 0; + font-size: 90%; + border-right: 1px solid $cauldron-border; + @include box-sizing(border-box); + + @include breakpoint(small) { + display: none; + } + + @include breakpoint(large) { + width: 190px; + } + + h3 { + padding-bottom: 8px; + margin-bottom: 0; + } + + a { + color: #777; + text-decoration: none; + font-weight: 500; + display: block; + line-height: 1.72; + &:hover { + color: black; + } + } +} diff --git a/test/spec/CSSUtils-test-files/mixins.less b/test/spec/CSSUtils-test-files/mixins.less new file mode 100644 index 00000000000..42417a7e67d --- /dev/null +++ b/test/spec/CSSUtils-test-files/mixins.less @@ -0,0 +1,79 @@ +.desktop-and-old-ie(@rules) { + @media screen and (min-width: 1200) { @rules; } + html.lt-ie9 & { @rules; } +} + +header { + background-color: blue; + + .desktop-and-old-ie({ + background-color: red; + }); +} + +.after-passing-ruleset-to-mixin { + color: blue; +} + +// Panels +// ------------------------- +.panel-variant(@border; @heading-text-color; @heading-bg-color; @heading-border) { + border-color: @border; + + & > .panel-heading { + color: @heading-text-color; + background-color: @heading-bg-color; + border-color: @heading-border; + + + .panel-collapse .panel-body { + border-top-color: @border; + } + } + & > .panel-footer { + + .panel-collapse .panel-body { + border-bottom-color: @border; + } + } +} + +// Form validation states +// +// Used in forms.less to generate the form validation CSS for warnings, errors, +// and successes. + +.form-control-validation(@text-color: #555; @border-color: #ccc; @background-color: #f5f5f5) { + // Color the label and help text + .help-block, + .control-label, + .radio, + .checkbox, + .radio-inline, + .checkbox-inline { + color: @text-color; + } + // Set the border and box shadow on specific inputs to match + .form-control { + border-color: @border-color; + .box-shadow(inset 0 1px 1px rgba(0,0,0,.075)); // Redeclare so transitions work + &:focus { + border-color: darken(@border-color, 10%); + @shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 6px lighten(@border-color, 20%); + .box-shadow(@shadow); + } + } + // Set validation states also for addons + .input-group-addon { + color: @text-color; + border-color: @border-color; + background-color: @background-color; + } + // Optional feedback icon + .form-control-feedback { + color: @text-color; + } +} + +// Rule after mixin with multiple parameters +div { + color: red; +} diff --git a/test/spec/CSSUtils-test-files/navbar.scss b/test/spec/CSSUtils-test-files/navbar.scss new file mode 100644 index 00000000000..bf68839f79d --- /dev/null +++ b/test/spec/CSSUtils-test-files/navbar.scss @@ -0,0 +1,57 @@ +// Navbar nav links +// +// Builds on top of the `.nav` components with it's own modifier class to make +// the nav the full height of the horizontal nav (above 768px). + +.navbar-nav { + margin: ($navbar-padding-vertical / 2) (-$navbar-padding-horizontal); + + > li > a { + padding-top: 10px; + padding-bottom: 10px; + line-height: $line-height-computed; + } + + @media (max-width: $grid-float-breakpoint-max) { + // Dropdowns get custom display when collapsed + .open .dropdown-menu { + position: static; + float: none; + width: auto; + margin-top: 0; + background-color: transparent; + border: 0; + box-shadow: none; + > li > a, + .dropdown-header { + padding: 5px 15px 5px 25px; + } + > li > a { + line-height: $line-height-computed; + &:hover, + &:focus { + background-image: none; + } + } + } + } + + // Uncollapse the nav + @media (min-width: $grid-float-breakpoint) { + float: left; + margin: 0; + + > li { + float: left; + > a { + padding-top: $navbar-padding-vertical; + padding-bottom: $navbar-padding-vertical; + } + } + + &.navbar-right:last-child { + margin-right: -$navbar-padding-horizontal; + } + } +} + diff --git a/test/spec/CSSUtils-test-files/panels.less b/test/spec/CSSUtils-test-files/panels.less new file mode 100755 index 00000000000..f2fe5eb3e44 --- /dev/null +++ b/test/spec/CSSUtils-test-files/panels.less @@ -0,0 +1,83 @@ +// Tables in panels +// +// Place a non-bordered `.table` within a panel (not within a `.panel-body`) and +// watch it go full width. + +.panel { + > .table, + > .table-responsive > .table { + margin-bottom: 0; + } + // Add border top radius for first one + > .table:first-child, + > .table-responsive:first-child > .table:first-child { + > thead:first-child, + > tbody:first-child { + > tr:first-child { + td:first-child, + th:first-child { + border-top-left-radius: (@panel-border-radius - 1); + } + td:last-child, + th:last-child { + border-top-right-radius: (@panel-border-radius - 1); + } + } + } + } + // Add border bottom radius for last one + > .table:last-child, + > .table-responsive:last-child > .table:last-child { + > tbody:last-child, + > tfoot:last-child { + > tr:last-child { + td:first-child, + th:first-child { + border-bottom-left-radius: (@panel-border-radius - 1); + } + td:last-child, + th:last-child { + border-bottom-right-radius: (@panel-border-radius - 1); + } + } + } + } + > .panel-body + .table, + > .panel-body + .table-responsive { + border-top: 1px solid @table-border-color; + } + > .table > tbody:first-child th, + > .table > tbody:first-child td { + border-top: 0; + } + > .table-bordered, + > .table-responsive > .table-bordered { + border: 0; + > thead, + > tbody, + > tfoot { + > tr { + > th:first-child, + > td:first-child { + border-left: 0; + } + > th:last-child, + > td:last-child { + border-right: 0; + } + &:first-child > th, + &:first-child > td { + border-top: 0; + } + &:last-child > th, + &:last-child > td { + border-bottom: 0; + } + } + } + } + > .table-responsive { + border: 0; + margin-bottom: 0; + } +} diff --git a/test/spec/CSSUtils-test-files/parent-selector.less b/test/spec/CSSUtils-test-files/parent-selector.less new file mode 100644 index 00000000000..403a2b858e7 --- /dev/null +++ b/test/spec/CSSUtils-test-files/parent-selector.less @@ -0,0 +1,42 @@ +.button { + &-ok { + background-image: url("ok.png"); + } + &-cancel { + background-image: url("cancel.png"); + } + + &-custom { + background-image: url("custom.png"); + } +} + +.grand { + .parent { + & > & { + color: red; + } + + & & { + color: green; + } + + && { + color: blue; + } + + &, &ish { + color: cyan; + } + } +} + +.header { + .menu { + border-radius: 5px; + .no-borderradius & { + background-image: url('images/button-background.png'); + } + } +} + diff --git a/test/spec/CSSUtils-test-files/print.less b/test/spec/CSSUtils-test-files/print.less new file mode 100755 index 00000000000..07277a3ca66 --- /dev/null +++ b/test/spec/CSSUtils-test-files/print.less @@ -0,0 +1,105 @@ +// +// Basic print styles +// -------------------------------------------------- +// Source: https://github.com/h5bp/html5-boilerplate/blob/master/css/main.css + +@media print { + + * { + text-shadow: none !important; + color: #000 !important; // Black prints faster: h5bp.com/s + background: transparent !important; + box-shadow: none !important; + } + + a, + a:visited { + text-decoration: underline; + } + + a[href]:after { + content: " (" attr(href) ")"; + } + + abbr[title]:after { + content: " (" attr(title) ")"; + } + + // Don't show links for images, or javascript/internal links + a[href^="javascript:"]:after, + a[href^="#"]:after { + content: ""; + } + + pre, + blockquote { + border: 1px solid #999; + page-break-inside: avoid; + } + + thead { + display: table-header-group; // h5bp.com/t + } + + tr, + img { + page-break-inside: avoid; + } + + img { + max-width: 100% !important; + } + + @page { + margin: 2cm .5cm; + } + + p, + h2, + h3 { + orphans: 3; + widows: 3; + } + + h2, + h3 { + page-break-after: avoid; + } + + // Chrome (OSX) fix for https://github.com/twbs/bootstrap/issues/11245 + // Once fixed, we can just straight up remove this. + select { + background: #fff !important; + } + + // Bootstrap components + .navbar { + display: none; + } + .table { + td, + th { + background-color: #fff !important; + } + } + .btn, + .dropup > .btn { + > .caret { + border-top-color: #000 !important; + } + } + .label { + border: 1px solid #000; + } + + .table { + border-collapse: collapse !important; + } + .table-bordered { + th, + td { + border: 1px solid #ddd !important; + } + } + +} diff --git a/test/spec/CSSUtils-test-files/property-list.css b/test/spec/CSSUtils-test-files/property-list.css new file mode 100644 index 00000000000..05557727f20 --- /dev/null +++ b/test/spec/CSSUtils-test-files/property-list.css @@ -0,0 +1,9 @@ +h1 { + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif +} + +.alert { + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.2); + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 1px 2px rgba(0, 0, 0, 0.05); +} diff --git a/test/spec/CSSUtils-test-files/table&button.scss b/test/spec/CSSUtils-test-files/table&button.scss new file mode 100644 index 00000000000..fc7298fe9ef --- /dev/null +++ b/test/spec/CSSUtils-test-files/table&button.scss @@ -0,0 +1,76 @@ + +// Tables +// ------------------------- +@mixin table-row-variant($state, $background) { + // Exact selectors below required to override `.table-striped` and prevent + // inheritance to nested tables. + .table { + > thead, + > tbody, + > tfoot { + > tr > .#{$state}, + > .#{$state} > td, + > .#{$state} > th { + background-color: $background; + } + } + } + + // Hover states for `.table-hover` + // Note: this is not available for cells or rows within `thead` or `tfoot`. + .table-hover > tbody { + > tr > .#{$state}:hover, + > .#{$state}:hover > td, + > .#{$state}:hover > th { + background-color: darken($background, 5%); + } + } +} + +// Button variants +// ------------------------- +// Easily pump out default styles, as well as :hover, :focus, :active, +// and disabled options for all buttons +@mixin button-variant($color, $background, $border) { + color: $color; + background-color: $background; + border-color: $border; + + &:hover, + &:focus, + &:active, + &.active { + color: $color; + background-color: darken($background, 8%); + border-color: darken($border, 12%); + } + .open & { &.dropdown-toggle { + color: $color; + background-color: darken($background, 8%); + border-color: darken($border, 12%); + } } + &:active, + &.active { + background-image: none; + } + .open & { &.dropdown-toggle { + background-image: none; + } } + &.disabled, + &[disabled], + fieldset[disabled] & { + &, + &:hover, + &:focus, + &:active, + &.active { + background-color: $background; + border-color: $border; + } + } + + .badge { + color: $background; + background-color: #fff; + } +} diff --git a/test/spec/CSSUtils-test-files/variables.less b/test/spec/CSSUtils-test-files/variables.less new file mode 100644 index 00000000000..4af2d6f2fdf --- /dev/null +++ b/test/spec/CSSUtils-test-files/variables.less @@ -0,0 +1,43 @@ +// Variables +@themes: "../../src/themes"; + +// Usage +@import "@{themes}/tidal-wave.less";@mySelector: banner; + +@fnord: "I am fnord."; +@var: "fnord"; +content: @@var; + +.@{mySelector} { + font-weight: bold; +} + +.after-variable-interpolated-selector { + color: green; +} + +@property: color; + +.widget { + @{property}: #0ee; + background-@{property}: #999; +} + +.after-variable-interpolated-property { + color: green; +} + +// Variables +@images: "../img"; + +// Usage +body { + color: #444; + background: url("@{images}/white-sand.png"); +} + +.after-variable-interpolated-url { + color: green; +} + + diff --git a/test/spec/CSSUtils-test-files/variables.scss b/test/spec/CSSUtils-test-files/variables.scss new file mode 100644 index 00000000000..a5ed528e0fb --- /dev/null +++ b/test/spec/CSSUtils-test-files/variables.scss @@ -0,0 +1,32 @@ +$family: unquote("Droid+Sans"); +@import url("http://fonts.googleapis.com/css?family=#{$family}"); + +$name: foo; +$attr: border; +.#{$name} a { + #{$attr}-color: blue; +} + +.after-variable-interpolated-selector { + color: green; +} + +$property: color; +div { + // test case for https://github.com/adobe/brackets/issues/9029 + background-#{$property}: blue +} + +p { + $font-size: 12px; + $line-height: 30px; + font: #{$font-size}/#{$line-height}; +} + +.after-variable-interpolated-property { + background-image: url(if($bootstrap-sass-asset-helper, twbs-image-path("#{$file-1x}"), "#{$file-1x}")); +} + +.after-variable-interpolated-url { + color: green; +} diff --git a/test/spec/CSSUtils-test.js b/test/spec/CSSUtils-test.js index b3c20a58551..e57e0d6b0e6 100644 --- a/test/spec/CSSUtils-test.js +++ b/test/spec/CSSUtils-test.js @@ -38,18 +38,28 @@ define(function (require, exports, module) { var testPath = SpecRunnerUtils.getTestPath("/spec/CSSUtils-test-files"), simpleCssFileEntry = FileSystem.getFileForPath(testPath + "/simple.css"), universalCssFileEntry = FileSystem.getFileForPath(testPath + "/universal.css"), + propListCssFileEntry = FileSystem.getFileForPath(testPath + "/property-list.css"), groupsFileEntry = FileSystem.getFileForPath(testPath + "/groups.css"), offsetsCssFileEntry = FileSystem.getFileForPath(testPath + "/offsets.css"), bootstrapCssFileEntry = FileSystem.getFileForPath(testPath + "/bootstrap.css"), escapesCssFileEntry = FileSystem.getFileForPath(testPath + "/escaped-identifiers.css"), embeddedHtmlFileEntry = FileSystem.getFileForPath(testPath + "/embedded.html"), - cssRegionsFileEntry = FileSystem.getFileForPath(testPath + "/regions.css"); + cssRegionsFileEntry = FileSystem.getFileForPath(testPath + "/regions.css"), + nestedGroupsFileEntry = FileSystem.getFileForPath(testPath + "/panels.less"); var contextTestCss = require("text!spec/CSSUtils-test-files/contexts.css"), selectorPositionsTestCss = require("text!spec/CSSUtils-test-files/selector-positions.css"), rangesTestCss = require("text!spec/CSSUtils-test-files/ranges.css"), - simpleTestCss = require("text!spec/CSSUtils-test-files/simple.css"); + simpleTestCss = require("text!spec/CSSUtils-test-files/simple.css"), + mediaTestScss = require("text!spec/CSSUtils-test-files/navbar.scss"), + mediaTestLess = require("text!spec/CSSUtils-test-files/print.less"), + mixinTestScss = require("text!spec/CSSUtils-test-files/table&button.scss"), + mixinTestLess = require("text!spec/CSSUtils-test-files/mixins.less"), + includeMixinTestScss = require("text!spec/CSSUtils-test-files/include-mixin.scss"), + parentSelectorTestLess = require("text!spec/CSSUtils-test-files/parent-selector.less"), + varInterpolationTestScss = require("text!spec/CSSUtils-test-files/variables.scss"), + varInterpolationTestLess = require("text!spec/CSSUtils-test-files/variables.less"); /** * Verifies whether one of the results returned by CSSUtils._findAllMatchingSelectorsInText() @@ -96,6 +106,20 @@ define(function (require, exports, module) { }); }); + it("should parse an empty string with less mode", function () { + runs(function () { + var result = CSSUtils._findAllMatchingSelectorsInText("", { tag: "div" }, "text/x-less"); + expect(result.length).toEqual(0); + }); + }); + + it("should parse an empty string with scss mode", function () { + runs(function () { + var result = CSSUtils._findAllMatchingSelectorsInText("", { tag: "div" }, "text/x-scss"); + expect(result.length).toEqual(0); + }); + }); + // it("should parse simple selectors from more than one file", function () { // // TODO: it'd be nice to revive this test by shimming FileIndexManager.getFileInfoList() or something // }); @@ -108,8 +132,8 @@ define(function (require, exports, module) { * results to equal the length of 'ranges'; each entry in range gives the {start, end} * of the expected line range for that Nth result. */ - function expectRuleRanges(spec, cssCode, selector, ranges) { - var result = CSSUtils._findAllMatchingSelectorsInText(cssCode, selector); + function expectRuleRanges(spec, cssCode, selector, ranges, mode) { + var result = CSSUtils._findAllMatchingSelectorsInText(cssCode, selector, mode); spec.expect(result.length).toEqual(ranges.length); ranges.forEach(function (range, i) { spec.expect(result[i].ruleStartLine).toEqual(range.start); @@ -126,8 +150,8 @@ define(function (require, exports, module) { * Expects the numbers of results to equal the length of 'ranges'; each entry in range gives * the {start, end} of the expected line range for that Nth result. */ - function expectGroupRanges(spec, cssCode, selector, ranges) { - var result = CSSUtils._findAllMatchingSelectorsInText(cssCode, selector); + function expectGroupRanges(spec, cssCode, selector, ranges, mode) { + var result = CSSUtils._findAllMatchingSelectorsInText(cssCode, selector, mode); spec.expect(result.length).toEqual(ranges.length); ranges.forEach(function (range, i) { spec.expect(result[i].selectorGroupStartLine).toEqual(range.start); @@ -180,6 +204,71 @@ define(function (require, exports, module) { }); }); + + it("should return correct rule ranges for rules with comma separators in property values", function () { + runs(function () { + init(this, propListCssFileEntry); + }); + + runs(function () { + // https://github.com/adobe/brackets/issues/9008 + expectRuleRanges(this, this.fileContent, "h1", [{start: 0, end: 2}]); + + // https://github.com/adobe/brackets/issues/8966 + expectRuleRanges(this, this.fileContent, ".alert", [{start: 4, end: 8}]); + }); + }); + + it("should return correct rule range and group range for different nested levels", function () { + runs(function () { + init(this, nestedGroupsFileEntry); + }); + + runs(function () { + expectRuleRanges(this, this.fileContent, ".table", [ + {start: 6, end: 9}, {start: 6, end: 9}, + {start: 10, end: 26}, {start: 10, end: 26}, + {start: 27, end: 43}, {start: 27, end: 43}, + {start: 44, end: 47} + ], "text/x-less"); + expectGroupRanges(this, this.fileContent, ".table", [ + {start: 6, end: 9}, {start: 6, end: 9}, + {start: 11, end: 26}, {start: 11, end: 26}, + {start: 28, end: 43}, {start: 28, end: 43}, + {start: 44, end: 47} + ], "text/x-less"); + + expectRuleRanges(this, this.fileContent, "tbody", [ + {start: 13, end: 25}, {start: 30, end: 42}, {start: 55, end: 76} + ], "text/x-less"); + expectGroupRanges(this, this.fileContent, "tbody", [ + {start: 13, end: 25}, {start: 30, end: 42}, {start: 55, end: 76} + ], "text/x-less"); + + expectRuleRanges(this, this.fileContent, "thead", [ + {start: 13, end: 25}, {start: 55, end: 76} + ], "text/x-less"); + expectGroupRanges(this, this.fileContent, "thead", [ + {start: 13, end: 25}, {start: 55, end: 76} + ], "text/x-less"); + + expectRuleRanges(this, this.fileContent, "tr", [ + {start: 15, end: 24}, {start: 32, end: 41}, {start: 58, end: 75} + ], "text/x-less"); + + expectRuleRanges(this, this.fileContent, "th", [ + {start: 16, end: 19}, {start: 20, end: 23}, {start: 33, end: 36}, + {start: 37, end: 40}, {start: 48, end: 51}, {start: 59, end: 62}, + {start: 63, end: 66}, {start: 67, end: 70}, {start: 71, end: 74} + ], "text/x-less"); + expectGroupRanges(this, this.fileContent, "th", [ + {start: 16, end: 19}, {start: 20, end: 23}, {start: 33, end: 36}, + {start: 37, end: 40}, {start: 48, end: 51}, {start: 59, end: 62}, + {start: 63, end: 66}, {start: 67, end: 70}, {start: 71, end: 74} + ], "text/x-less"); + + }); + }); }); describe("with the universal selector", function () { @@ -606,7 +695,7 @@ define(function (require, exports, module) { match, expectParseError; - function _findMatchingRules(cssCode, tagInfo) { + function _findMatchingRules(cssCode, tagInfo, mode) { if (tagInfo) { var selector = ""; if (tagInfo.tag) { @@ -618,10 +707,10 @@ define(function (require, exports, module) { if (tagInfo.id) { selector += "#" + tagInfo.id; } - return CSSUtils._findAllMatchingSelectorsInText(cssCode, selector); + return CSSUtils._findAllMatchingSelectorsInText(cssCode, selector, mode); } else { // If !tagInfo, we don't care about results; only making sure parse/search doesn't crash - CSSUtils._findAllMatchingSelectorsInText(cssCode, "dummy"); + CSSUtils._findAllMatchingSelectorsInText(cssCode, "dummy", mode); return null; } } @@ -631,10 +720,10 @@ define(function (require, exports, module) { * the given cssCode string in isolation (no CSS files are loaded). If tagInfo not specified, * returns no results; only tests that parsing plus a simple search won't crash. */ - var _match = function (cssCode, tagInfo) { + var _match = function (cssCode, tagInfo, mode) { lastCssCode = cssCode; try { - return _findMatchingRules(cssCode, tagInfo); + return _findMatchingRules(cssCode, tagInfo, mode); } catch (e) { this.fail(e.message + ": " + cssCode); return []; @@ -642,10 +731,13 @@ define(function (require, exports, module) { }; /** Tests against the same CSS text as the last call to match() */ - function matchAgain(tagInfo) { - return match(lastCssCode, tagInfo); + function matchAgain(tagInfo, mode) { + return match(lastCssCode, tagInfo, mode); } + function expectCompleteSelectors(selectorInfo, expectedStr) { + expect(CSSUtils.getCompleteSelectors(selectorInfo)).toBe(expectedStr); + } /** * Test helper function: expects CSS parsing to fail at the given 0-based offset within the @@ -1432,7 +1524,289 @@ define(function (require, exports, module) { }); // describe("Known Issues") + describe("Nested rules defined inside @media (SCSS)", function () { + var result; + it("should find all different levels of nested selectors", function () { + result = match(mediaTestScss, { tag: "a" }, "text/x-scss"); + expect(result.length).toBe(6); + + result = matchAgain({ clazz: "navbar-right" }, "text/x-scss"); + expect(result.length).toBe(1); + expectCompleteSelectors(result[0], ".navbar-nav / &.navbar-right:last-child"); + + result = matchAgain({ clazz: "dropdown-menu" }, "text/x-scss"); + expect(result.length).toBe(1); + expectCompleteSelectors(result[0], ".navbar-nav / .open .dropdown-menu"); + + result = matchAgain({ clazz: "dropdown-header" }, "text/x-scss"); + expect(result.length).toBe(1); + expectCompleteSelectors(result[0], ".navbar-nav / .open .dropdown-menu / .dropdown-header"); + + result = matchAgain({ clazz: "navbar-nav" }, "text/x-scss"); + expect(result.length).toBe(2); + expectCompleteSelectors(result[0], ".navbar-nav"); + expectCompleteSelectors(result[1], ".navbar-nav / &.navbar-right:last-child"); + }); + + it("should find the only one li tag selector that is also the right most of combinators", function () { + result = matchAgain({ tag: "li" }, "text/x-scss"); + expect(result.length).toBe(1); + expectCompleteSelectors(result[0], ".navbar-nav / > li"); + }); + + it("should not find a nested parent selector of a descendant combinator", function () { + // Verify that 'open' won't match '.open .dropdown-menu {}' rule + result = matchAgain({ clazz: "open" }, "text/x-scss"); + expect(result.length).toBe(0); + }); + + }); // describe("Nested rules defined inside @media (SCSS)") + + describe("Nested rules defined inside @media (LESS)", function () { + var result; + it("should find all different levels of nested selectors", function () { + result = match(mediaTestLess, { tag: "a" }, "text/x-less"); + expect(result.length).toBe(6); + + result = matchAgain({ tag: "th" }, "text/x-less"); + expect(result.length).toBe(3); + expectCompleteSelectors(result[0], "*"); + expectCompleteSelectors(result[1], ".table / th"); + expectCompleteSelectors(result[2], ".table-bordered / th"); + + result = matchAgain({ clazz: "btn" }, "text/x-less"); + expect(result.length).toBe(2); + expectCompleteSelectors(result[0], ".btn"); + expectCompleteSelectors(result[1], ".dropup > .btn"); + + result = matchAgain({ clazz: "table" }, "text/x-less"); + expect(result.length).toBe(2); + expectCompleteSelectors(result[0], ".table"); + expectCompleteSelectors(result[1], ".table"); + + result = matchAgain({ clazz: "caret" }, "text/x-less"); + expect(result.length).toBe(1); + // https://github.com/adobe/brackets/issues/8894 + expectCompleteSelectors(result[0], ".btn, .dropup > .btn / > .caret"); + }); + + it("should not find a nested parent selector of a child combinator", function () { + // Verify that 'dropup' won't match '.dropup > .btn' rule + result = matchAgain({ clazz: "dropup" }, "text/x-less"); + expect(result.length).toBe(0); + }); + + }); // describe("Nested rules defined inside @media (LESS)") + + describe("Nested rules with SCSS mixins", function () { + var result; + it("should find all different levels of nested selectors", function () { + result = match(mixinTestScss, { clazz: "table" }, "text/x-scss"); + expect(result.length).toBe(1); + expectCompleteSelectors(result[0], "@mixin table-row-variant($state, $background) / .table"); + + result = matchAgain({ tag: "th" }, "text/x-scss"); + expect(result.length).toBe(2); + expectCompleteSelectors(result[0], "@mixin table-row-variant($state, $background) / .table / > thead, > tbody, > tfoot / > .#{$state} > th"); + expectCompleteSelectors(result[1], "@mixin table-row-variant($state, $background) / .table-hover > tbody / > .#{$state}:hover > th"); + + result = matchAgain({ clazz: "dropdown-toggle" }, "text/x-scss"); + expect(result.length).toBe(2); + expectCompleteSelectors(result[0], "@mixin button-variant($color, $background, $border) / .open & / &.dropdown-toggle"); + expectCompleteSelectors(result[1], "@mixin button-variant($color, $background, $border) / .open & / &.dropdown-toggle"); + + result = matchAgain({ clazz: "badge" }, "text/x-scss"); + expect(result.length).toBe(1); + expectCompleteSelectors(result[0], "@mixin button-variant($color, $background, $border) / .badge"); + }); + + it("should not find a nested parent selector of a descendant combinator", function () { + result = matchAgain({ tag: "tr" }, "text/x-scss"); + expect(result.length).toBe(0); + }); + + }); // describe("Nested rules with SCSS mixins") + + describe("Nested rules with LESS mixins", function () { + var result; + it("should find all different levels of nested selectors", function () { + result = match(mixinTestLess, { clazz: "panel-body" }, "text/x-less"); + expect(result.length).toBe(2); + expectCompleteSelectors(result[0], ".panel-variant(@border; @heading-text-color; @heading-bg-color; @heading-border) / & > .panel-heading / + .panel-collapse .panel-body"); + expectCompleteSelectors(result[1], ".panel-variant(@border; @heading-text-color; @heading-bg-color; @heading-border) / & > .panel-footer / + .panel-collapse .panel-body"); + + result = matchAgain({ clazz: "panel-heading" }, "text/x-less"); + expect(result.length).toBe(1); + expectCompleteSelectors(result[0], ".panel-variant(@border; @heading-text-color; @heading-bg-color; @heading-border) / & > .panel-heading"); + + result = matchAgain({ clazz: "panel-footer" }, "text/x-less"); + expect(result.length).toBe(1); + expectCompleteSelectors(result[0], ".panel-variant(@border; @heading-text-color; @heading-bg-color; @heading-border) / & > .panel-footer"); + + result = matchAgain({ clazz: "input-group-addon" }, "text/x-less"); + expect(result.length).toBe(1); + expectCompleteSelectors(result[0], ".form-control-validation(@text-color: #555; @border-color: #ccc; @background-color: #f5f5f5) / .input-group-addon"); + }); + + // https://github.com/adobe/brackets/issues/8852 + it("should find the rule that follows the code passing in a ruleset to a mixin", function () { + result = matchAgain({ clazz: "after-passing-ruleset-to-mixin" }, "text/x-less"); + expect(result.length).toBe(1); + expectCompleteSelectors(result[0], ".after-passing-ruleset-to-mixin"); + }); + + // https://github.com/adobe/brackets/issues/8850 + it("should find the rule that succeeds the mixin with multiple parameters having default values and semicolons", function () { + result = matchAgain({ tag: "div" }, "text/x-less"); + expect(result.length).toBe(1); + expectCompleteSelectors(result[0], "div"); + }); + }); // describe("Nested rules with LESS mixins") + + describe("Variable interpolation in SCSS", function () { + var result; + // https://github.com/adobe/brackets/issues/8870 + it("should find a rule that has a variable interpolated selector", function () { + // Verify that "#{$name} a" can be searched with "a" tag + result = match(varInterpolationTestScss, { tag: "a" }, "text/x-scss"); + expect(result.length).toBe(1); + expectCompleteSelectors(result[0], ".#{$name} a"); + }); + + // https://github.com/adobe/brackets/issues/8851 + it("should find rules after a rule with variable interpolated selector", function () { + result = matchAgain({ clazz: "after-variable-interpolated-selector" }, "text/x-scss"); + expect(result.length).toBe(1); + expectCompleteSelectors(result[0], ".after-variable-interpolated-selector"); + }); + + // https://github.com/adobe/brackets/issues/8875 + it("should find rules after a rule with variable interpolated property", function () { + result = matchAgain({ clazz: "after-variable-interpolated-property" }, "text/x-scss"); + expect(result.length).toBe(1); + expectCompleteSelectors(result[0], ".after-variable-interpolated-property"); + }); + + it("should find rules after a rule with variable interpolated url", function () { + result = matchAgain({ clazz: "after-variable-interpolated-url" }, "text/x-scss"); + expect(result.length).toBe(1); + expectCompleteSelectors(result[0], ".after-variable-interpolated-url"); + }); + }); // describe("Variable interpolation in SCSS") + + describe("Variable interpolation in LESS", function () { + var result; + // https://github.com/adobe/brackets/issues/8870 + it("should find rules with variable interpolated selectors", function () { + result = match(varInterpolationTestLess, { clazz: "@{mySelector}" }, "text/x-less"); + expect(result.length).toBe(1); + expectCompleteSelectors(result[0], ".@{mySelector}"); + }); + + // https://github.com/adobe/brackets/issues/8851 + it("should find rules after a rule with variable interpolated selector", function () { + result = matchAgain({ clazz: "after-variable-interpolated-selector" }, "text/x-less"); + expect(result.length).toBe(1); + expectCompleteSelectors(result[0], ".after-variable-interpolated-selector"); + }); + + // https://github.com/adobe/brackets/issues/8875 + it("should find rules after a rule with variable interpolated property", function () { + result = matchAgain({ clazz: "after-variable-interpolated-property" }, "text/x-less"); + expect(result.length).toBe(1); + expectCompleteSelectors(result[0], ".after-variable-interpolated-property"); + }); + + it("should find rules after a rule with variable interpolated url", function () { + result = matchAgain({ clazz: "after-variable-interpolated-url" }, "text/x-less"); + expect(result.length).toBe(1); + expectCompleteSelectors(result[0], ".after-variable-interpolated-url"); + }); + }); // describe("Variable interpolation in LESS") + + describe("Parsing SCSS variable interpolation as LESS", function () { + var result; + it("should find a rule that has a variable interpolated selector", function () { + // Verify that "#{$name} a" can be searched with "a" tag + result = match(varInterpolationTestScss, { tag: "a" }, "text/x-less"); + expect(result.length).toBe(1); + expectCompleteSelectors(result[0], ".#{$name} a"); + }); + + it("should find rules after a rule with variable interpolated selector", function () { + result = matchAgain({ clazz: "after-variable-interpolated-selector" }, "text/x-less"); + expect(result.length).toBe(1); + expectCompleteSelectors(result[0], ".after-variable-interpolated-selector"); + }); + + // https://github.com/adobe/brackets/issues/8965 + it("should find rules after a rule with variable interpolated property", function () { + result = matchAgain({ clazz: "after-variable-interpolated-property" }, "text/x-less"); + expect(result.length).toBe(1); + expectCompleteSelectors(result[0], ".after-variable-interpolated-property"); + }); + + it("should find rules after a rule with variable interpolated url", function () { + result = matchAgain({ clazz: "after-variable-interpolated-url" }, "text/x-less"); + expect(result.length).toBe(1); + expectCompleteSelectors(result[0], ".after-variable-interpolated-url"); + }); + }); // describe("Parsing SCSS variable interpolation as LESS") + describe("Reference parent selector with &", function () { + var result; + it("should find rules that are prefixed with &", function () { + result = match(parentSelectorTestLess, { clazz: "button-custom" }, "text/x-less"); + expect(result.length).toBe(1); + expectCompleteSelectors(result[0], ".button / &-custom"); + + result = matchAgain({ clazz: "button-ok" }, "text/x-less"); + expect(result.length).toBe(1); + expectCompleteSelectors(result[0], ".button / &-ok"); + }); + + it("should find rules that prepend a selector to the inherited parent selector using &", function () { + result = matchAgain({ clazz: "menu" }, "text/x-less"); + expect(result.length).toBe(2); + expectCompleteSelectors(result[0], ".header / .menu"); + expectCompleteSelectors(result[1], ".header / .menu / .no-borderradius &"); + }); + + it("should find rules that have multiple & references to multiple levels of parent selectors", function () { + result = matchAgain({ clazz: "parent" }, "text/x-less"); + expect(result.length).toBe(5); + expectCompleteSelectors(result[0], ".grand / .parent"); + expectCompleteSelectors(result[1], ".grand / .parent / & > &"); + expectCompleteSelectors(result[2], ".grand / .parent / & &"); + expectCompleteSelectors(result[3], ".grand / .parent / &&"); + expectCompleteSelectors(result[4], ".grand / .parent / &"); + + result = matchAgain({ clazz: "parentish" }, "text/x-less"); + expect(result.length).toBe(1); + expectCompleteSelectors(result[0], ".grand / .parent / &ish"); + }); + + it("should not find a nested rule that has parent selector & as the rightmost selector", function () { + // Verify that '.no-borderradius' won't match '.no-borderradius & {}' rule + result = matchAgain({ clazz: "no-borderradius" }, "text/x-less"); + expect(result.length).toBe(0); + }); + }); // describe("Reference parent selector with &") + + // https://github.com/adobe/brackets/issues/8945 + describe("Nested rules following an @include block", function () { + it("should find rules that succeed @include blocks", function () { + var result = match(includeMixinTestScss, { tag: "h3" }, "text/x-scss"); + expect(result.length).toBe(1); + expectCompleteSelectors(result[0], ".sidebar / h3"); + + result = matchAgain({ tag: "a" }, "text/x-scss"); + expect(result.length).toBe(2); + expectCompleteSelectors(result[0], ".sidebar / a"); + expectCompleteSelectors(result[1], ".sidebar / a / &:hover"); + }); + }); // describe("Nested rules following an @include block") + describe("CSS Intgration Tests", function () { this.category = "integration"; diff --git a/test/spec/LiveDevelopment-test.js b/test/spec/LiveDevelopment-test.js index 5daaf2454a9..4b25c1b0b10 100644 --- a/test/spec/LiveDevelopment-test.js +++ b/test/spec/LiveDevelopment-test.js @@ -52,16 +52,17 @@ define(function (require, exports, module) { // Used as mocks require("LiveDevelopment/main"); - var CommandsModule = require("command/Commands"), - CommandsManagerModule = require("command/CommandManager"), - LiveDevelopmentModule = require("LiveDevelopment/LiveDevelopment"), - InspectorModule = require("LiveDevelopment/Inspector/Inspector"), - CSSDocumentModule = require("LiveDevelopment/Documents/CSSDocument"), - CSSAgentModule = require("LiveDevelopment/Agents/CSSAgent"), - HighlightAgentModule = require("LiveDevelopment/Agents/HighlightAgent"), - HTMLDocumentModule = require("LiveDevelopment/Documents/HTMLDocument"), - HTMLInstrumentationModule = require("language/HTMLInstrumentation"), - NativeAppModule = require("utils/NativeApp"); + var CommandsModule = require("command/Commands"), + CommandsManagerModule = require("command/CommandManager"), + LiveDevelopmentModule = require("LiveDevelopment/LiveDevelopment"), + InspectorModule = require("LiveDevelopment/Inspector/Inspector"), + CSSDocumentModule = require("LiveDevelopment/Documents/CSSDocument"), + CSSAgentModule = require("LiveDevelopment/Agents/CSSAgent"), + HighlightAgentModule = require("LiveDevelopment/Agents/HighlightAgent"), + HTMLDocumentModule = require("LiveDevelopment/Documents/HTMLDocument"), + HTMLInstrumentationModule = require("language/HTMLInstrumentation"), + NativeAppModule = require("utils/NativeApp"), + CSSPreprocessorDocumentModule = require("LiveDevelopment/Documents/CSSPreprocessorDocument"); var testPath = SpecRunnerUtils.getTestPath("/spec/LiveDevelopment-test-files"), tempDir = SpecRunnerUtils.getTempDirectory(), @@ -370,6 +371,226 @@ define(function (require, exports, module) { }); }); + describe("Highlighting elements in browser from a rule in scss", function () { + + var testDocument, + testEditor, + testCSSDoc, + liveDevelopmentConfig, + inspectorConfig, + fileContent = "div[role='main'] {\n" + + " @include background-image(linear-gradient(lighten($color1, 30%), lighten($color2, 30%)));\n" + + " ul {\n padding-top:14px;\n" + // line 2 and 3 + " li {\n" + + " a { }\n" + + " @media (max-width: 480px) {\n" + + " a {\n" + + " &:hover { }\n" + + " width: auto;\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + "}\n"; + + beforeEach(function () { + // save original configs + liveDevelopmentConfig = LiveDevelopmentModule.config; + inspectorConfig = InspectorModule.config; + + // force init + LiveDevelopmentModule.config = InspectorModule.config = {highlight: true}; + HighlightAgentModule.load(); + + // module spies + spyOn(CSSAgentModule, "styleForURL").andReturn([]); + spyOn(CSSAgentModule, "reloadCSSForDocument").andCallFake(function () { return new $.Deferred().resolve(); }); + spyOn(HighlightAgentModule, "redraw").andCallFake(function () {}); + spyOn(HighlightAgentModule, "rule").andCallFake(function () {}); + spyOn(LiveDevelopmentModule, "showHighlight").andCallFake(function () {}); + spyOn(LiveDevelopmentModule, "hideHighlight").andCallFake(function () {}); + + var mock = SpecRunnerUtils.createMockEditor(fileContent, "scss"); + testDocument = mock.doc; + testEditor = mock.editor; + testCSSDoc = new CSSPreprocessorDocumentModule(testDocument, testEditor); + }); + + afterEach(function () { + LiveDevelopmentModule.config = liveDevelopmentConfig; + InspectorModule.config = inspectorConfig; + + SpecRunnerUtils.destroyMockEditor(testDocument); + testDocument = null; + testEditor = null; + testCSSDoc = null; + }); + + it("should toggle the highlight via a command", function () { + var cmd = CommandsManagerModule.get(CommandsModule.FILE_LIVE_HIGHLIGHT); + cmd.setEnabled(true); + + // Run our tests in order depending on whether highlighting is on or off + // presently. By setting the order like this, we'll also leave highlighting + // in the state we found it in. + if (cmd.getChecked()) { + CommandsManagerModule.execute(CommandsModule.FILE_LIVE_HIGHLIGHT); + expect(LiveDevelopmentModule.hideHighlight).toHaveBeenCalled(); + + CommandsManagerModule.execute(CommandsModule.FILE_LIVE_HIGHLIGHT); + expect(LiveDevelopmentModule.showHighlight).toHaveBeenCalled(); + } else { + CommandsManagerModule.execute(CommandsModule.FILE_LIVE_HIGHLIGHT); + expect(LiveDevelopmentModule.showHighlight).toHaveBeenCalled(); + + CommandsManagerModule.execute(CommandsModule.FILE_LIVE_HIGHLIGHT); + expect(LiveDevelopmentModule.hideHighlight).toHaveBeenCalled(); + } + }); + + it("should redraw highlights when the cursor moves", function () { + testEditor.setCursorPos(0, 9); // after = sign in "div[role='main']" selector + expect(HighlightAgentModule.rule).toHaveBeenCalled(); + expect(HighlightAgentModule.rule.mostRecentCall.args[0]).toEqual("div[role='main']"); + + testEditor.setCursorPos(3, 12); // after - in "padding-top:14px;" on line 3 + expect(HighlightAgentModule.rule.calls.length).toEqual(3); + expect(HighlightAgentModule.rule.mostRecentCall.args[0]).toEqual("div[role='main'] ul"); + + testEditor.setCursorPos(5, 6); // right before "a" tag selector on line 5 + expect(HighlightAgentModule.rule.calls.length).toEqual(4); + expect(HighlightAgentModule.rule.mostRecentCall.args[0]).toEqual("div[role='main'] ul li a"); + + testEditor.setCursorPos(6, 25); // after "@media (max-width: " on line 6 + expect(HighlightAgentModule.rule.calls.length).toEqual(5); + expect(HighlightAgentModule.rule.mostRecentCall.args[0]).toEqual("div[role='main'] ul li"); + + testEditor.setCursorPos(8, 19); // after "&:hover {" on line 8 + expect(HighlightAgentModule.rule.calls.length).toEqual(6); + expect(HighlightAgentModule.rule.mostRecentCall.args[0]).toEqual("div[role='main'] ul li a:hover"); + }); + }); + + describe("Highlighting elements in browser from a rule in LESS", function () { + + var testDocument, + testEditor, + testCSSDoc, + liveDevelopmentConfig, + inspectorConfig, + fileContent = "@navbar-height: 50px;\n" + + ".navbar-collapse {\n" + + " &.in {\n \n}\n" + // line 2, 3 and 4 + " @media (min-width: @grid-float-breakpoint) {\n" + + " width: auto;\n" + + " &.collapse { } \n" + + " &.in { } \n" + + " }\n}\n" + // line 9 and 10 + "@text-color:@gray-dark;\n" + + ".container,\n.container-fluid {\n" + // line 12 and 13 + " > .navbar-header,\n > .navbar-collapse {\n" + // line 14 and 15 + " a, img { }\n" + + " }\n" + + "}"; + + beforeEach(function () { + // save original configs + liveDevelopmentConfig = LiveDevelopmentModule.config; + inspectorConfig = InspectorModule.config; + + // force init + LiveDevelopmentModule.config = InspectorModule.config = {highlight: true}; + HighlightAgentModule.load(); + + // module spies + spyOn(CSSAgentModule, "styleForURL").andReturn([]); + spyOn(CSSAgentModule, "reloadCSSForDocument").andCallFake(function () { return new $.Deferred().resolve(); }); + spyOn(HighlightAgentModule, "redraw").andCallFake(function () {}); + spyOn(HighlightAgentModule, "rule").andCallFake(function () {}); + spyOn(LiveDevelopmentModule, "showHighlight").andCallFake(function () {}); + spyOn(LiveDevelopmentModule, "hideHighlight").andCallFake(function () {}); + + // TODO: Due to https://github.com/adobe/brackets/issues/8837, we are + // using "scss" as language id instead of "less". + var mock = SpecRunnerUtils.createMockEditor(fileContent, "scss"); + testDocument = mock.doc; + testEditor = mock.editor; + testCSSDoc = new CSSPreprocessorDocumentModule(testDocument, testEditor); + }); + + afterEach(function () { + LiveDevelopmentModule.config = liveDevelopmentConfig; + InspectorModule.config = inspectorConfig; + + SpecRunnerUtils.destroyMockEditor(testDocument); + testDocument = null; + testEditor = null; + testCSSDoc = null; + }); + + it("should toggle the highlight via a command", function () { + var cmd = CommandsManagerModule.get(CommandsModule.FILE_LIVE_HIGHLIGHT); + cmd.setEnabled(true); + + // Run our tests in order depending on whether highlighting is on or off + // presently. By setting the order like this, we'll also leave highlighting + // in the state we found it in. + if (cmd.getChecked()) { + CommandsManagerModule.execute(CommandsModule.FILE_LIVE_HIGHLIGHT); + expect(LiveDevelopmentModule.hideHighlight).toHaveBeenCalled(); + + CommandsManagerModule.execute(CommandsModule.FILE_LIVE_HIGHLIGHT); + expect(LiveDevelopmentModule.showHighlight).toHaveBeenCalled(); + } else { + CommandsManagerModule.execute(CommandsModule.FILE_LIVE_HIGHLIGHT); + expect(LiveDevelopmentModule.showHighlight).toHaveBeenCalled(); + + CommandsManagerModule.execute(CommandsModule.FILE_LIVE_HIGHLIGHT); + expect(LiveDevelopmentModule.hideHighlight).toHaveBeenCalled(); + } + }); + + it("should not update any highlights when the cursor is in a LESS variable definition", function () { + testEditor.setCursorPos(0, 3); + expect(HighlightAgentModule.rule.calls.length).toEqual(0); + + testEditor.setCursorPos(11, 10); + expect(HighlightAgentModule.rule.calls.length).toEqual(0); + }); + + it("should redraw highlights when the cursor moves", function () { + testEditor.setCursorPos(1, 0); // before .navbar-collapse on line 1 + expect(HighlightAgentModule.rule).toHaveBeenCalled(); + expect(HighlightAgentModule.rule.mostRecentCall.args[0]).toEqual(".navbar-collapse"); + + testEditor.setCursorPos(4, 0); // before } of &.in rule that starts on line 2 + expect(HighlightAgentModule.rule.calls.length).toEqual(2); + expect(HighlightAgentModule.rule.mostRecentCall.args[0]).toEqual(".navbar-collapse.in"); + + testEditor.setCursorPos(7, 15); // before { of &.collapse rule on line 7 + expect(HighlightAgentModule.rule.calls.length).toEqual(3); + expect(HighlightAgentModule.rule.mostRecentCall.args[0]).toEqual(".navbar-collapse.collapse"); + + testEditor.setCursorPos(8, 5); // after & of &.in rule on line 8 + expect(HighlightAgentModule.rule.calls.length).toEqual(4); + expect(HighlightAgentModule.rule.mostRecentCall.args[0]).toEqual(".navbar-collapse.in"); + }); + + it("should redraw highlights on elements that have nested rules with group selectors", function () { + testEditor.setCursorPos(12, 11); // at the end of line 12 (ie. after .container,) + expect(HighlightAgentModule.rule).toHaveBeenCalled(); + expect(HighlightAgentModule.rule.mostRecentCall.args[0]).toEqual(".container, .container-fluid"); + + testEditor.setCursorPos(14, 22); // after "> .navbar-header,\n >" on line 14 + expect(HighlightAgentModule.rule.calls.length).toEqual(2); + expect(HighlightAgentModule.rule.mostRecentCall.args[0]).toEqual(".container > .navbar-header, .container-fluid > .navbar-header, .container > .navbar-collapse, .container-fluid > .navbar-collapse"); + + testEditor.setCursorPos(16, 7); // right before img tag in "a, img { }" on line 16 + expect(HighlightAgentModule.rule.calls.length).toEqual(3); + expect(HighlightAgentModule.rule.mostRecentCall.args[0]).toEqual(".container > .navbar-header a, .container-fluid > .navbar-header a, .container > .navbar-collapse a, .container-fluid > .navbar-collapse a, .container > .navbar-header img, .container-fluid > .navbar-header img, .container > .navbar-collapse img, .container-fluid > .navbar-collapse img"); + }); + }); + describe("Highlighting elements in browser from cursor positions in HTML page", function () { var testDocument,