src/extensions/default/JavaScriptQuickEdit/unittests.js
/*
* 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, waitsForFail */
define(function (require, exports, module) {
"use strict";
var CommandManager, // loaded from brackets.test
EditorManager, // loaded from brackets.test
PerfUtils, // loaded from brackets.test
JSUtils, // loaded from brackets.test
FileUtils = brackets.getModule("file/FileUtils"),
SpecRunnerUtils = brackets.getModule("spec/SpecRunnerUtils"),
Strings = brackets.getModule("strings"),
UnitTestReporter = brackets.getModule("test/UnitTestReporter");
var extensionPath = FileUtils.getNativeModuleDirectoryPath(module),
testPath = extensionPath + "/unittest-files/syntax",
tempPath = SpecRunnerUtils.getTempDirectory(),
testWindow,
initInlineTest;
function rewriteProject(spec) {
var result = new $.Deferred(),
infos = {},
options = {
parseOffsets : true,
infos : infos,
removePrefix : true
};
SpecRunnerUtils.copyPath(testPath, tempPath, options).done(function () {
spec.infos = infos;
result.resolve();
}).fail(function () {
result.reject();
});
return result.promise();
}
// Helper function for testing cursor position
function fixPos(pos) {
if (!("sticky" in pos)) {
pos.sticky = null;
}
return pos;
}
/**
* Performs setup for an inline editor test. Parses offsets (saved to Spec.offsets) for all files in
* the test project (testPath) and saves files back to disk without offset markup.
* When finished, open an editor for the specified project relative file path
* then attempts opens an inline editor at the given offset. Installs an after()
* function restore all file content back to original state with offset markup.
*
* @param {!string} openFile Project relative file path to open in a main editor.
* @param {!number} openOffset The offset index location within openFile to open an inline editor.
* @param {?boolean} expectInline Use false to verify that an inline editor should not be opened. Omit otherwise.
*/
var _initInlineTest = function (openFile, openOffset, expectInline, filesToOpen) {
var spec = this;
filesToOpen = filesToOpen || [];
expectInline = (expectInline !== undefined) ? expectInline : true;
runs(function () {
waitsForDone(rewriteProject(spec), "rewriteProject");
});
SpecRunnerUtils.loadProjectInTestWindow(tempPath);
runs(function () {
filesToOpen.push(openFile);
waitsForDone(SpecRunnerUtils.openProjectFiles(filesToOpen), "openProjectFiles");
});
if (openOffset !== undefined) {
runs(function () {
// open inline editor at specified offset index
waitsForDone(SpecRunnerUtils.toggleQuickEditAtOffset(
EditorManager.getCurrentFullEditor(),
spec.infos[openFile].offsets[openOffset]
), "toggleQuickEditAtOffset");
});
}
};
describe("JSQuickEdit", function () {
/*
*
*/
describe("javaScriptFunctionProvider", function () {
beforeEach(function () {
initInlineTest = _initInlineTest.bind(this);
SpecRunnerUtils.createTestWindowAndRun(this, function (w) {
testWindow = w;
EditorManager = testWindow.brackets.test.EditorManager;
CommandManager = testWindow.brackets.test.CommandManager;
JSUtils = testWindow.brackets.test.JSUtils;
});
this.addMatchers({
toHaveInlineEditorRange: function (range) {
var i = 0,
editor = this.actual,
hidden,
lineCount = editor.lineCount(),
shouldHide = [],
shouldShow = [],
startLine = range.startLine,
endLine = range.endLine,
visibleRangeCheck;
for (i = 0; i < lineCount; i++) {
hidden = editor._codeMirror.getLineHandle(i).hidden || false;
if (i < startLine) {
if (!hidden) {
shouldHide.push(i); // lines above start line should be hidden
}
} else if ((i >= startLine) && (i <= endLine)) {
if (hidden) {
shouldShow.push(i); // lines in the range should be visible
}
} else if (i > endLine) {
if (!hidden) {
shouldHide.push(i); // lines below end line should be hidden
}
}
}
visibleRangeCheck = (editor._visibleRange.startLine === startLine) &&
(editor._visibleRange.endLine === endLine);
this.message = function () {
var msg = "";
if (shouldHide.length > 0) {
msg += "Expected inline editor to hide [" + shouldHide.toString() + "].\n";
}
if (shouldShow.length > 0) {
msg += "Expected inline editor to show [" + shouldShow.toString() + "].\n";
}
if (!visibleRangeCheck) {
msg += "Editor._visibleRange [" +
editor._visibleRange.startLine + "," +
editor._visibleRange.endLine + "] should be [" +
startLine + "," + endLine + "].";
}
return msg;
};
return (shouldHide.length === 0) &&
(shouldShow.length === 0) &&
visibleRangeCheck;
}
});
});
afterEach(function () {
//debug visual confirmation of inline editor
//waits(1000);
// revert files to original content with offset markup
initInlineTest = null;
testWindow = null;
EditorManager = null;
CommandManager = null;
JSUtils = null;
SpecRunnerUtils.closeTestWindow();
});
it("should ignore tokens that are not function calls or references", function () {
var editor,
extensionRequire,
jsQuickEditMain,
tokensFile = "tokens.js",
promise,
offsets;
initInlineTest(tokensFile);
runs(function () {
extensionRequire = testWindow.brackets.getModule("utils/ExtensionLoader").getRequireContextForExtension("JavaScriptQuickEdit");
jsQuickEditMain = extensionRequire("main");
editor = EditorManager.getCurrentFullEditor();
offsets = this.infos[tokensFile];
// regexp token
promise = jsQuickEditMain.javaScriptFunctionProvider(editor, offsets[0]);
expect(promise).toBe(Strings.ERROR_JSQUICKEDIT_FUNCTIONNOTFOUND);
// multi-line comment
promise = jsQuickEditMain.javaScriptFunctionProvider(editor, offsets[1]);
expect(promise).toBe(Strings.ERROR_JSQUICKEDIT_FUNCTIONNOTFOUND);
// single-line comment
promise = jsQuickEditMain.javaScriptFunctionProvider(editor, offsets[2]);
expect(promise).toBe(Strings.ERROR_JSQUICKEDIT_FUNCTIONNOTFOUND);
// string, double quotes
promise = jsQuickEditMain.javaScriptFunctionProvider(editor, offsets[3]);
expect(promise).toBe(Strings.ERROR_JSQUICKEDIT_FUNCTIONNOTFOUND);
// string, single quotes
promise = jsQuickEditMain.javaScriptFunctionProvider(editor, offsets[4]);
expect(promise).toBe(Strings.ERROR_JSQUICKEDIT_FUNCTIONNOTFOUND);
});
});
it("should open a function with form: function functionName()", function () {
initInlineTest("test1main.js", 0);
runs(function () {
var inlineWidget = EditorManager.getCurrentFullEditor().getInlineWidgets()[0];
var inlinePos = inlineWidget.editor.getCursorPos();
// verify cursor position in inline editor
expect(fixPos(inlinePos)).toEqual(fixPos(this.infos["test1inline.js"].offsets[0]));
});
});
it("should open a function with form: functionName = function()", function () {
initInlineTest("test1main.js", 1);
runs(function () {
var inlineWidget = EditorManager.getCurrentFullEditor().getInlineWidgets()[0];
var inlinePos = inlineWidget.editor.getCursorPos();
// verify cursor position in inline editor
expect(fixPos(inlinePos)).toEqual(fixPos(this.infos["test1inline.js"].offsets[1]));
});
});
it("should open a function with form: functionName: function()", function () {
initInlineTest("test1main.js", 2);
runs(function () {
var inlineWidget = EditorManager.getCurrentFullEditor().getInlineWidgets()[0];
var inlinePos = inlineWidget.editor.getCursorPos();
// verify cursor position in inline editor
expect(fixPos(inlinePos)).toEqual(fixPos(this.infos["test1inline.js"].offsets[2]));
});
});
describe("Code hints tests within quick edit window ", function () {
var JSCodeHints,
ParameterHintProvider;
/*
* Ask provider for hints at current cursor position; expect it to
* return some
*
* @param {Object} provider - a CodeHintProvider object
* @param {string} key - the charCode of a key press that triggers the
* CodeHint provider
* @return {boolean} - whether the provider has hints in the context of
* the test editor
*/
function expectHints(provider, key) {
if (key === undefined) {
key = null;
}
expect(provider.hasHints(EditorManager.getActiveEditor(), key)).toBe(true);
return provider.getHints(null);
}
/*
* Wait for a hint response object to resolve, then apply a callback
* to the result
*
* @param {Object + jQuery.Deferred} hintObj - a hint response object,
* possibly deferred
* @param {Function} callback - the callback to apply to the resolved
* hint response object
*/
function _waitForHints(hintObj, callback) {
var complete = false,
hintList = null;
if (hintObj.hasOwnProperty("hints")) {
complete = true;
hintList = hintObj.hints;
} else {
hintObj.done(function (obj) {
complete = true;
hintList = obj.hints;
});
}
waitsFor(function () {
return complete;
}, "Expected hints did not resolve", 3000);
runs(function () { callback(hintList); });
}
/*
* Expect a given list of hints to be present in a given hint
* response object, and no more.
*
* @param {Object + jQuery.Deferred} hintObj - a hint response object,
* possibly deferred
* @param {Array.<string>} expectedHints - a list of hints that should be
* present in the hint response, and no more.
*/
function hintsPresentExact(hintObj, expectedHints) {
_waitForHints(hintObj, function (hintList) {
expect(hintList).toBeTruthy();
expect(hintList.length).toBe(expectedHints.length);
expectedHints.forEach(function (expectedHint, index) {
expect(hintList[index].data("token").value).toBe(expectedHint);
});
});
}
/**
* Show a function hint based on the code at the cursor. Verify the
* hint matches the passed in value.
*
* @param {Array<{name: string, type: string, isOptional: boolean}>}
* expectedParams - array of records, where each element of the array
* describes a function parameter. If null, then no hint is expected.
* @param {number} expectedParameter - the parameter at cursor.
*/
function expectParameterHint(expectedParams, expectedParameter) {
var requestHints = undefined,
request = null;
function expectHint(hint) {
var params = hint.parameters,
n = params.length,
i;
// compare params to expected params
expect(params.length).toBe(expectedParams.length);
expect(hint.currentIndex).toBe(expectedParameter);
for (i = 0; i < n; i++) {
expect(params[i].name).toBe(expectedParams[i].name);
expect(params[i].type).toBe(expectedParams[i].type);
if (params[i].isOptional) {
expect(expectedParams[i].isOptional).toBeTruthy();
} else {
expect(expectedParams[i].isOptional).toBeFalsy();
}
}
}
runs(function () {
request = ParameterHintProvider._getParameterHint();
if (expectedParams === null) {
request.fail(function (result) {
requestHints = result;
});
waitsForFail(request, "ParameterHints");
} else {
request.done(function (result) {
requestHints = result;
});
waitsForDone(request, "ParameterHints");
}
});
if (expectedParams === null) {
expect(requestHints).toBe(null);
} else {
expectHint(requestHints);
}
}
/**
* Wait for the editor to change positions, such as after a jump to
* definition has been triggered. Will timeout after 3 seconds
*
* @param {{line:number, ch:number}} oldLocation - the original line/col
* @param {Function} callback - the callback to apply once the editor has changed position
*/
function _waitForJump(oldLocation, callback) {
var cursor = null;
waitsFor(function () {
var activeEditor = EditorManager.getActiveEditor();
cursor = activeEditor.getCursorPos();
return (cursor.line !== oldLocation.line) ||
(cursor.ch !== oldLocation.ch);
}, "Expected jump did not occur", 3000);
runs(function () { callback(cursor); });
}
/**
* Trigger a jump to definition, and verify that the editor jumped to
* the expected location.
*
* @param {{line:number, ch:number, file:string}} expectedLocation - the
* line, column, and optionally the new file the editor should jump to. If the
* editor is expected to stay in the same file, then file may be omitted.
*/
function editorJumped(jsCodeHints, testEditor, expectedLocation) {
var oldLocation = testEditor.getCursorPos();
jsCodeHints.handleJumpToDefinition();
_waitForJump(oldLocation, function (newCursor) {
expect(newCursor.line).toBe(expectedLocation.line);
expect(newCursor.ch).toBe(expectedLocation.ch);
if (expectedLocation.file) {
var activeEditor = EditorManager.getActiveEditor();
expect(activeEditor.document.file.name).toBe(expectedLocation.file);
}
});
}
function initJSCodeHints() {
var extensionRequire = testWindow.brackets.getModule("utils/ExtensionLoader").
getRequireContextForExtension("JavaScriptCodeHints");
JSCodeHints = extensionRequire("main");
ParameterHintProvider = extensionRequire("ParameterHintsProvider").JSParameterHintsProvider();
}
beforeEach(function () {
initInlineTest("test.html");
initJSCodeHints();
});
afterEach(function () {
JSCodeHints = null;
ParameterHintProvider = null;
});
it("should see code hint lists in quick editor", function () {
var start = {line: 13, ch: 11 },
testPos = {line: 5, ch: 29},
testEditor;
runs(function () {
var openQuickEditor = SpecRunnerUtils.toggleQuickEditAtOffset(EditorManager.getCurrentFullEditor(), start);
waitsForDone(openQuickEditor, "Open quick editor");
});
runs(function () {
testEditor = EditorManager.getActiveEditor();
testEditor.setCursorPos(testPos);
expectParameterHint([{name: "mo", type: "Number"}], 0);
});
});
it("should see jump to definition on variable working in quick editor", function () {
var start = {line: 13, ch: 10 },
testPos = {line: 6, ch: 7},
testJumpPos = {line: 6, ch: 5},
jumpPos = {line: 3, ch: 6},
testEditor;
runs(function () {
var openQuickEditor = SpecRunnerUtils.toggleQuickEditAtOffset(EditorManager.getCurrentFullEditor(), start);
waitsForDone(openQuickEditor, "Open quick editor");
});
runs(function () {
testEditor = EditorManager.getActiveEditor();
testEditor.setCursorPos(testPos);
var hintObj = expectHints(JSCodeHints.jsHintProvider);
hintsPresentExact(hintObj, ["propA"]);
});
runs(function () {
testEditor = EditorManager.getActiveEditor();
testEditor.setCursorPos(testJumpPos);
editorJumped(JSCodeHints, testEditor, jumpPos);
});
});
// FIXME (issue #3951): jump to method inside quick editor doesn't jump
xit("should see jump to definition on method working in quick editor", function () {
var start = {line: 13, ch: 13 },
testPos = {line: 5, ch: 25},
jumpPos = {line: 9, ch: 21},
testEditor;
runs(function () {
var openQuickEditor = SpecRunnerUtils.toggleQuickEditAtOffset(EditorManager.getCurrentFullEditor(), start);
waitsForDone(openQuickEditor, "Open quick editor");
});
runs(function () {
testEditor = EditorManager.getActiveEditor();
testEditor.setCursorPos(testPos);
editorJumped(jumpPos);
});
});
});
});
describe("Performance suite", function () {
this.category = "performance";
var testPath = extensionPath + "/unittest-files/jquery-ui";
beforeEach(function () {
SpecRunnerUtils.createTestWindowAndRun(this, function (w) {
testWindow = w;
CommandManager = testWindow.brackets.test.CommandManager;
EditorManager = testWindow.brackets.test.EditorManager;
PerfUtils = testWindow.brackets.test.PerfUtils;
});
});
afterEach(function () {
testWindow = null;
CommandManager = null;
EditorManager = null;
PerfUtils = null;
SpecRunnerUtils.closeTestWindow();
});
it("should open inline editors", function () {
SpecRunnerUtils.loadProjectInTestWindow(testPath);
var extensionRequire,
JavaScriptQuickEdit,
i,
perfMeasurements;
runs(function () {
perfMeasurements = [
{
measure: PerfUtils.JAVASCRIPT_INLINE_CREATE,
children: [
{
measure: PerfUtils.JAVASCRIPT_FIND_FUNCTION,
children: [
{
measure: PerfUtils.JSUTILS_GET_ALL_FUNCTIONS,
children: [
{
measure: PerfUtils.DOCUMENT_MANAGER_GET_DOCUMENT_FOR_PATH,
name: "Document creation during this search",
operation: "sum"
},
{
measure: PerfUtils.JSUTILS_REGEXP,
operation: "sum"
}
]
},
{
measure: PerfUtils.JSUTILS_END_OFFSET,
operation: "sum"
}
]
}
]
}
];
});
runs(function () {
extensionRequire = testWindow.brackets.getModule("utils/ExtensionLoader").getRequireContextForExtension("JavaScriptQuickEdit");
JavaScriptQuickEdit = extensionRequire("main");
waitsForDone(SpecRunnerUtils.openProjectFiles(["ui/jquery.effects.core.js"]), "openProjectFiles");
});
var runCreateInlineEditor = function () {
var editor = EditorManager.getCurrentFullEditor();
// Set the cursor in the middle of a call to "extend" so the JS helper function works correctly.
editor.setCursorPos(271, 20);
waitsForDone(
JavaScriptQuickEdit._createInlineEditor(editor, "extend"),
"createInlineEditor",
5000
);
};
function logPerf() {
var reporter = UnitTestReporter.getActiveReporter();
reporter.logTestWindow(perfMeasurements);
reporter.clearTestWindow();
}
// repeat 5 times
for (i = 0; i < 5; i++) {
runs(runCreateInlineEditor);
runs(logPerf);
}
});
});
});
});