1
0
mirror of https://github.com/adobe/brackets.git synced 2024-11-20 18:02:54 +01:00
brackets/test/spec/JSUtils-test.js
Swagatam Mitra 92819092bd JSUtils modification to handle es6 constructs (#13635)
* JSUtils modification to handle es6 constructs

* Addressed review comments and added es6 test cases

* Address Code review comments and add support for ArrowFunctionExpressions with new test case

* Update comments

* Refactor to remove extra variables
2017-08-28 10:31:42 +05:30

673 lines
30 KiB
JavaScript

/*
* Copyright (c) 2012 - present 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.
*
*/
/*global describe, it, xit, expect, beforeEach, afterEach, waitsFor, runs, waitsForDone */
define(function (require, exports, module) {
'use strict';
var DocumentManager, // loaded from brackets.test
FileViewController, // loaded from brackets.test
ProjectManager, // loaded from brackets.test
JSUtils = require("language/JSUtils"),
FileSystem = require("filesystem/FileSystem"),
FileUtils = require("file/FileUtils"),
SpecRunnerUtils = require("spec/SpecRunnerUtils");
var testPath = SpecRunnerUtils.getTestPath("/spec/JSUtils-test-files"),
doneLoading = false;
// Verifies whether one of the results returned by JSUtils.findAllMatchingFunctionsInText()
// came from the expected function name or not.
var toMatchFunctionName = function (expected) {
return this.actual.functionName.trim() === expected;
};
var simpleJsFileEntry = FileSystem.getFileForPath(testPath + "/simple.js");
var trickyJsFileEntry = FileSystem.getFileForPath(testPath + "/tricky.js");
var invalidJsFileEntry = FileSystem.getFileForPath(testPath + "/invalid.js");
var jQueryJsFileEntry = FileSystem.getFileForPath(testPath + "/jquery-1.7.js");
var braceEndJsFileEntry = FileSystem.getFileForPath(testPath + "/braceEnd.js");
var eofJsFileEntry = FileSystem.getFileForPath(testPath + "/eof.js");
var eof2JsFileEntry = FileSystem.getFileForPath(testPath + "/eof2.js");
var es6ClassesFileEntry = FileSystem.getFileForPath(testPath + "/es6-classes.js");
var es6StaticsFileEntry = FileSystem.getFileForPath(testPath + "/es6-static-methods.js");
var es6InheritanceFileEntry = FileSystem.getFileForPath(testPath + "/es6-inheritance.js");
var es6GetterSetterFileEntry = FileSystem.getFileForPath(testPath + "/es6-getter-setter.js");
var es6AsyncAndArrowFileEntry = FileSystem.getFileForPath(testPath + "/es6-async-arrow.js");
function init(spec, fileEntry) {
if (fileEntry) {
spec.addMatchers({toMatchFunctionName: toMatchFunctionName});
runs(function () {
FileUtils.readAsText(fileEntry)
.done(function (text) {
spec.fileJsContent = text;
})
.always(function (text) {
doneLoading = true;
});
});
}
}
function cleanup(spec) {
spec.fileJsContent = null;
}
describe("JSUtils", function () {
describe("basics", function () {
it("should parse an empty string", function () {
runs(function () {
var result = JSUtils.findAllMatchingFunctionsInText("", "myFunc");
expect(result.length).toEqual(0);
});
});
});
// TODO (jason-sanjose): use offset markup in these test files
describe("line offsets", function () {
afterEach(function () {
cleanup(this);
});
// Checks the lines ranges of the results returned by JSUtils. 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 expectFunctionRanges(spec, jsCode, funcName, ranges) {
var result = JSUtils.findAllMatchingFunctionsInText(jsCode, funcName);
spec.expect(result.length).toEqual(ranges.length);
ranges.forEach(function (range, i) {
spec.expect(result[i].lineStart).toEqual(range.start);
spec.expect(result[i].lineEnd).toEqual(range.end);
});
}
function expectNoFunction(jsCode, functionName) {
var result = JSUtils.findAllMatchingFunctionsInText(jsCode, functionName);
expect(result.length).toBe(0);
}
it("should return correct start and end line numbers for es6 class definitions and methods", function () {
runs(function () {
doneLoading = false;
init(this, es6ClassesFileEntry);
});
waitsFor(function () { return doneLoading; }, 1000);
runs(function () {
expectFunctionRanges(this, this.fileJsContent, "Shape", [ {start: 0, end: 9} ]);
expectFunctionRanges(this, this.fileJsContent, "constructor", [ {start: 1, end: 4} ]);
expectFunctionRanges(this, this.fileJsContent, "move", [ {start: 5, end: 8} ]);
});
});
it("should return correct start and end line numbers for es6 static class methods", function () {
runs(function () {
doneLoading = false;
init(this, es6StaticsFileEntry);
});
waitsFor(function () { return doneLoading; }, 1000);
runs(function () {
expectFunctionRanges(this, this.fileJsContent, "Rectangle", [ {start: 0, end: 4} ]);
expectFunctionRanges(this, this.fileJsContent, "defaultRectangle", [ {start: 1, end: 3} ]);
});
});
it("should return correct start and end line numbers for es6 class inheritance", function () {
runs(function () {
doneLoading = false;
init(this, es6InheritanceFileEntry);
});
waitsFor(function () { return doneLoading; }, 1000);
runs(function () {
expectFunctionRanges(this, this.fileJsContent, "Rectangle", [ {start: 0, end: 6} ]);
expectFunctionRanges(this, this.fileJsContent, "Circle", [ {start: 7, end: 12} ]);
expectFunctionRanges(this, this.fileJsContent, "constructor", [ {start: 1, end: 5}, {start: 8, end: 11} ]);
});
});
it("should return correct start and end line numbers for es6 class members getters/setters", function () {
runs(function () {
doneLoading = false;
init(this, es6GetterSetterFileEntry);
});
waitsFor(function () { return doneLoading; }, 1000);
runs(function () {
expectFunctionRanges(this, this.fileJsContent, "Rectangle", [ {start: 0, end: 10} ]);
expectFunctionRanges(this, this.fileJsContent, "constructor", [ {start: 1, end: 4} ]);
expectFunctionRanges(this, this.fileJsContent, "width", [ {start: 5, end: 5}, {start: 6, end: 6} ]);
expectFunctionRanges(this, this.fileJsContent, "height", [ {start: 7, end: 7}, {start: 8, end: 8} ]);
expectFunctionRanges(this, this.fileJsContent, "area", [ {start: 9, end: 9} ]);
});
});
it("should return correct start and end line numbers for es6 async and arrow function expressions", function () {
runs(function () {
doneLoading = false;
init(this, es6AsyncAndArrowFileEntry);
});
waitsFor(function () { return doneLoading; }, 1000);
runs(function () {
expectFunctionRanges(this, this.fileJsContent, "bar", [ {start: 1, end: 1} ]);
expectFunctionRanges(this, this.fileJsContent, "fooAgain", [ {start: 3, end: 3} ]);
});
});
it("should return correct start and end line numbers for simple functions", function () {
runs(function () {
doneLoading = false;
init(this, simpleJsFileEntry);
});
waitsFor(function () { return doneLoading; }, 1000);
runs(function () {
expectFunctionRanges(this, this.fileJsContent, "simple1", [ {start: 0, end: 2} ]);
expectFunctionRanges(this, this.fileJsContent, "simple2", [ {start: 7, end: 9} ]);
expectFunctionRanges(this, this.fileJsContent, "simple3", [ {start: 11, end: 13} ]);
});
});
it("should return correct start and end line numbers for parameterized functions", function () {
runs(function () {
doneLoading = false;
init(this, simpleJsFileEntry);
});
waitsFor(function () { return doneLoading; }, 1000);
runs(function () {
expectFunctionRanges(this, this.fileJsContent, "param1", [ {start: 18, end: 19} ]);
expectFunctionRanges(this, this.fileJsContent, "param2", [ {start: 24, end: 26} ]);
expectFunctionRanges(this, this.fileJsContent, "param3", [ {start: 28, end: 32} ]);
});
});
it("should return correct start and end line numbers for single line functions", function () {
runs(function () {
doneLoading = false;
init(this, simpleJsFileEntry);
});
waitsFor(function () { return doneLoading; }, 1000);
runs(function () {
expectFunctionRanges(this, this.fileJsContent, "single1", [ {start: 35, end: 35} ]);
expectFunctionRanges(this, this.fileJsContent, "single2", [ {start: 36, end: 36} ]);
expectFunctionRanges(this, this.fileJsContent, "single3", [ {start: 37, end: 37} ]);
});
});
it("should return correct start and end line numbers for nested functions", function () {
runs(function () {
doneLoading = false;
init(this, simpleJsFileEntry);
});
waitsFor(function () { return doneLoading; }, 1000);
runs(function () {
expectFunctionRanges(this, this.fileJsContent, "nested1", [ {start: 42, end: 50} ]);
expectFunctionRanges(this, this.fileJsContent, "nested2", [ {start: 44, end: 49} ]);
expectFunctionRanges(this, this.fileJsContent, "nested3", [ {start: 47, end: 48} ]);
});
});
it("should return correct start and end line numbers for functions with keyword 'function' in name", function () {
runs(function () {
doneLoading = false;
init(this, simpleJsFileEntry);
});
waitsFor(function () { return doneLoading; }, 1000);
runs(function () {
//expectFunctionRanges(this, this.fileJsContent, "functionX", [ {start: 53, end: 55} ]);
expectFunctionRanges(this, this.fileJsContent, "my_function", [ {start: 56, end: 57} ]);
expectFunctionRanges(this, this.fileJsContent, "function3", [ {start: 58, end: 60} ]);
});
});
it("should return correct start and end line numbers for prototype method declarations", function () {
runs(function () {
doneLoading = false;
init(this, simpleJsFileEntry);
});
waitsFor(function () { return doneLoading; }, 1000);
runs(function () {
expectFunctionRanges(this, this.fileJsContent, "myMethod", [ {start: 66, end: 68} ]);
});
});
it("should handle various whitespace variations", function () {
runs(function () {
doneLoading = false;
init(this, simpleJsFileEntry);
});
waitsFor(function () { return doneLoading; }, 1000);
runs(function () {
expectFunctionRanges(this, this.fileJsContent, "noSpaceBeforeFunc", [ {start: 71, end: 71} ]);
expectFunctionRanges(this, this.fileJsContent, "spaceBeforeColon", [ {start: 73, end: 75} ]);
expectFunctionRanges(this, this.fileJsContent, "noSpaceAfterColon", [ {start: 77, end: 79} ]);
expectFunctionRanges(this, this.fileJsContent, "fakePeriodBeforeFunction", [ {start: 82, end: 84} ]);
expectFunctionRanges(this, this.fileJsContent, "noSpaceAfterFunction", [ {start: 86, end: 88} ]);
expectFunctionRanges(this, this.fileJsContent, "noSpaceAfterFunction2", [ {start: 90, end: 92} ]);
expectFunctionRanges(this, this.fileJsContent, "findMe", [ {start: 93, end: 93} ]);
});
});
it("should work with high-ascii characters in function names", function () {
runs(function () {
doneLoading = false;
init(this, simpleJsFileEntry);
});
waitsFor(function () { return doneLoading; }, 1000);
runs(function () {
expectFunctionRanges(this, this.fileJsContent, "highAscÍÍChars", [ {start: 95, end: 97} ]);
expectFunctionRanges(this, this.fileJsContent, "moreHighAscÍÍChars", [ {start: 99, end: 101} ]);
expectFunctionRanges(this, this.fileJsContent, "ÅsciiExtendedIdentifierStart", [ {start: 103, end: 104} ]);
});
});
it("should work with unicode characters in or around function names", function () {
runs(function () {
doneLoading = false;
init(this, simpleJsFileEntry);
});
waitsFor(function () { return doneLoading; }, 1000);
runs(function () {
expectFunctionRanges(this, this.fileJsContent, "ʸUnicodeModifierLettervalidIdentifierStart", [ {start: 106, end: 107} ]);
expectFunctionRanges(this, this.fileJsContent, "unicodeModifierLettervalidIdentifierPartʸ", [ {start: 112, end: 113} ]);
});
});
// TODO (issue #1125): support escaped unicode
xit("FAIL should work with unicode characters in or around function names", function () {
runs(function () {
doneLoading = false;
init(this, simpleJsFileEntry);
});
waitsFor(function () { return doneLoading; }, 1000);
runs(function () {
expectFunctionRanges(this, this.fileJsContent, "\u02b8UnicodeEscapedIdentifierStart", [ {start: 109, end: 110} ]);
expectFunctionRanges(this, this.fileJsContent, "unicodeEscapedIdentifierPart\u02b8", [ {start: 115, end: 116} ]);
expectFunctionRanges(this, this.fileJsContent, "unicodeTabBefore", [ {start: 118, end: 119} ]);
expectFunctionRanges(this, this.fileJsContent, "unicodeTabAfter", [ {start: 121, end: 122} ]);
});
});
it("should work when colliding with prototype properties", function () { // #1390, #2813
runs(function () {
doneLoading = false;
init(this, trickyJsFileEntry);
});
waitsFor(function () { return doneLoading; }, 1000);
runs(function () {
expectFunctionRanges(this, this.fileJsContent, "toString", [ {start: 1, end: 3} ]);
expectFunctionRanges(this, this.fileJsContent, "length", [ {start: 6, end: 8} ]);
expectFunctionRanges(this, this.fileJsContent, "hasOwnProperty", [ {start: 11, end: 13} ]);
});
});
it("should fail with invalid function names", function () {
runs(function () {
doneLoading = false;
init(this, invalidJsFileEntry);
});
waitsFor(function () { return doneLoading; }, 1000);
runs(function () {
expectNoFunction(this.fileJsContent, "0digitIdentifierStart");
expectNoFunction(this.fileJsContent, ".punctuationIdentifierStart");
expectNoFunction(this.fileJsContent, "punctuation.IdentifierPart");
});
});
});
describe("brace ends of functions", function () {
beforeEach(function () {
runs(function () {
doneLoading = false;
init(this, braceEndJsFileEntry);
});
waitsFor(function () { return doneLoading; }, 1000);
});
afterEach(function () {
cleanup(this);
});
function expectEndBrace(spec, funcName) {
var startPos = spec.fileJsContent.indexOf("function " + funcName);
expect(startPos).toNotBe(-1);
var endPos = JSUtils._getFunctionEndOffset(spec.fileJsContent, startPos);
var endMarker = spec.fileJsContent.slice(endPos);
expect(endMarker.indexOf("//END " + funcName)).toBe(0);
}
it("should handle a simple function", function () {
expectEndBrace(this, "simpleFunction");
});
it("should handle nested braces", function () {
expectEndBrace(this, "nestedBraces");
});
it("should handle a nested function", function () {
expectEndBrace(this, "nestedFunction");
});
it("should handle an end brace in a string", function () {
expectEndBrace(this, "endBraceInString");
});
it("should handle an end brace in a single-quoted string", function () {
expectEndBrace(this, "endBraceInSingleQuoteString");
});
it("should handle an end brace in a line comment", function () {
expectEndBrace(this, "endBraceInLineComment");
});
it("should handle an end brace in a block comment", function () {
expectEndBrace(this, "endBraceInBlockComment");
});
it("should handle an end brace in a multiline block comment", function () {
expectEndBrace(this, "endBraceInMultilineBlockComment");
});
it("should handle an end brace in a regexp", function () {
expectEndBrace(this, "endBraceInRegexp");
});
it("should handle a single-line function", function () {
expectEndBrace(this, "singleLine");
});
it("should handle a single-line function with a fake brace", function () {
expectEndBrace(this, "singleLineWithFakeBrace");
});
it("should handle a complicated case", function () {
expectEndBrace(this, "itsComplicated");
});
});
describe("brace end of function that ends at end of file", function () {
it("should find the end of a function that ends exactly at the end of the file", function () {
runs(function () {
doneLoading = false;
init(this, eofJsFileEntry);
});
waitsFor(function () { return doneLoading; }, 1000);
runs(function () {
expect(JSUtils._getFunctionEndOffset(this.fileJsContent, 0)).toBe(this.fileJsContent.length);
cleanup(this);
});
});
});
describe("end of function that's unclosed at end of file", function () {
it("should find the end of a function that is unclosed at the end of the file", function () {
runs(function () {
doneLoading = false;
init(this, eof2JsFileEntry);
});
waitsFor(function () { return doneLoading; }, 1000);
runs(function () {
expect(JSUtils._getFunctionEndOffset(this.fileJsContent, 0)).toBe(this.fileJsContent.length);
cleanup(this);
});
});
});
describe("with real-world jQuery JS code", function () {
beforeEach(function () {
runs(function () {
doneLoading = false;
init(this, jQueryJsFileEntry);
});
waitsFor(function () { return doneLoading; }, 1000);
});
afterEach(function () {
cleanup(this);
});
it("should find the first instance of the pushStack function", function () {
var funcNames = JSUtils.findAllMatchingFunctionsInText(this.fileJsContent, "pushStack");
expect(funcNames).toBeTruthy();
expect(funcNames.length).toBeGreaterThan(0);
expect(funcNames[0]).toBeTruthy();
expect(funcNames[0].lineStart).toBe(243);
expect(funcNames[0].lineEnd).toBe(267);
});
it("should find all instances of the ready function", function () {
var funcNames = JSUtils.findAllMatchingFunctionsInText(this.fileJsContent, "ready");
//expect(funcNames.length).toBe(3);
expect(funcNames.length).toBe(2);
expect(funcNames[0].lineStart).toBe(276);
expect(funcNames[0].lineEnd).toBe(284);
expect(funcNames[1].lineStart).toBe(419);
expect(funcNames[1].lineEnd).toBe(443);
//expect(funcNames[2].lineStart).toBe(3422); // not finding this one...
//expect(funcNames[2].lineEnd).toBe(3425);
});
it("should return an empty array when findAllMatchingSelectors() can't find any matches", function () {
var funcNames = JSUtils.findAllMatchingFunctionsInText(this.fileJsContent, "NO-SUCH-FUNCTION");
expect(funcNames.length).toBe(0);
});
});
}); // describe("JSUtils")
describe("JS Indexing", function () {
this.category = "integration";
var functions; // populated by indexAndFind()
beforeEach(function () {
SpecRunnerUtils.createTestWindowAndRun(this, function (testWindow) {
// Load module instances from brackets.test
var brackets = testWindow.brackets;
DocumentManager = brackets.test.DocumentManager;
FileViewController = brackets.test.FileViewController;
ProjectManager = brackets.test.ProjectManager;
JSUtils = brackets.test.JSUtils;
SpecRunnerUtils.loadProjectInTestWindow(testPath);
});
});
afterEach(function () {
DocumentManager = null;
FileViewController = null;
JSUtils = null;
ProjectManager = null;
SpecRunnerUtils.closeTestWindow();
});
function init(fileName) {
runs(function () {
waitsForDone(
FileViewController.openAndSelectDocument(
testPath + "/" + fileName,
FileViewController.PROJECT_MANAGER
),
"openAndSelectDocument"
);
});
}
/**
* Builds a fileInfos index of the project, as required to call findMatchingFunctions(). Calls the
* specified 'invoker' function with fileInfos, and populates the 'functions' var once it's done.
* Does not need to be wrapped in a runs() block.
* @param {function(Array.<File>):$.Promise} invokeFind
*/
function indexAndFind(invokeFind) {
runs(function () {
var result = new $.Deferred();
ProjectManager.getAllFiles().done(function (files) {
invokeFind(files)
.done(function (functionsResult) { functions = functionsResult; })
.then(result.resolve, result.reject);
});
waitsForDone(result, "Index and invoke JSUtils.findMatchingFunctions()");
});
}
describe("Index integrity", function () {
it("should handle colliding with prototype properties", function () { // #2813
// no init() needed - don't need any editors to be open
indexAndFind(function (fileInfos) {
return JSUtils.findMatchingFunctions("toString", fileInfos);
});
runs(function () {
expect(functions.length).toBe(1);
expect(functions[0].lineStart).toBe(1);
expect(functions[0].lineEnd).toBe(3);
});
indexAndFind(function (fileInfos) {
return JSUtils.findMatchingFunctions("length", fileInfos);
});
runs(function () {
expect(functions.length).toBe(1);
expect(functions[0].lineStart).toBe(6);
expect(functions[0].lineEnd).toBe(8);
});
indexAndFind(function (fileInfos) {
return JSUtils.findMatchingFunctions("hasOwnProperty", fileInfos);
});
runs(function () {
expect(functions.length).toBe(1);
expect(functions[0].lineStart).toBe(11);
expect(functions[0].lineEnd).toBe(13);
});
});
});
describe("Working with unsaved changes", function () {
function fileChangedTest(buildCache) {
init("edit.js");
// Populate JSUtils cache
if (buildCache) {
// Look for "edit2" function
indexAndFind(function (fileInfos) {
return JSUtils.findMatchingFunctions("edit2", fileInfos);
});
runs(function () {
expect(functions.length).toBe(1);
expect(functions[0].lineStart).toBe(7);
expect(functions[0].lineEnd).toBe(9);
});
}
// Add several blank lines at the beginning of the text
runs(function () {
var doc = DocumentManager.getCurrentDocument();
doc.setText("\n\n\n\n" + doc.getText());
});
// Look for function again, expecting line offsets to have changed
indexAndFind(function (fileInfos) {
return JSUtils.findMatchingFunctions("edit2", fileInfos);
});
runs(function () {
expect(functions.length).toBe(1);
expect(functions[0].lineStart).toBe(11);
expect(functions[0].lineEnd).toBe(13);
});
}
it("should return the correct offsets if the file has changed", function () {
fileChangedTest(false);
});
it("should return the correct offsets if the results were cached and the file has changed", function () {
fileChangedTest(true);
});
function insertFunctionTest(buildCache) {
init("edit.js");
// Populate JSUtils cache
if (buildCache) {
// Look for function that doesn't exist yet
indexAndFind(function (fileInfos) {
return JSUtils.findMatchingFunctions("TESTFUNCTION", fileInfos);
});
runs(function () {
expect(functions.length).toBe(0);
});
}
// Add a new function to the file
runs(function () {
var doc = DocumentManager.getCurrentDocument();
doc.setText(doc.getText() + "\n\nfunction TESTFUNCTION() {\n return true;\n}\n");
});
// Look for the function we just created
indexAndFind(function (fileInfos) {
return JSUtils.findMatchingFunctions("TESTFUNCTION", fileInfos);
});
runs(function () {
expect(functions.length).toBe(1);
expect(functions[0].lineStart).toBe(33);
expect(functions[0].lineEnd).toBe(35);
});
}
it("should return a newly created function in an unsaved file", function () {
insertFunctionTest(false);
});
it("should return a newly created function in an unsaved file that already has cached results", function () {
insertFunctionTest(true);
});
});
}); //describe("JS Indexing")
});